File size: 13,705 Bytes
a10b189 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 | {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-|
Module : Assertica.Core.ModuleEnv
Description : Module environment and qualified name resolution
Copyright : (c) 2026 Ahmad Ali Parr
License : BSL-1.0
The ModuleEnv manages loaded modules and handles name resolution:
1. **Module Loading**: Track loaded modules by qualified name
2. **Name Resolution**: Resolve qualified and unqualified names
3. **Import Resolution**: Handle qualified, unqualified, and selective imports
4. **Visibility Checking**: Enforce public/private boundaries
5. **Conflict Detection**: Error on name ambiguities
6. **Dependency Ordering**: Compute safe compilation order
-}
module Assertica.Core.ModuleEnv
( -- * Module environment
ModuleEnv (..)
, emptyModuleEnv
, addModule
-- * Name resolution
, resolveName
, resolveQName
, resolveImports
, resolveDependencies
-- * Module lookup
, lookupModule
, findModule
-- * Conflict detection
, ConflictError (..)
, detectNameConflicts
, validateImports
-- * Compilation ordering
, CompilationOrder
, computeCompilationOrder
-- * Visibility
, isNameVisible
, checkNameAccess
) where
import Data.Text (Text)
import qualified Data.Text as T
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Set (Set)
import qualified Data.Set as Set
import Data.List (sortBy, group, sort)
import Data.Ord (comparing)
import GHC.Generics (Generic)
import Data.Data (Data)
import Data.Typeable (Typeable)
import Control.Monad (when)
import Data.Either (partitionEithers)
import Assertica.Core.AST (QName(..))
import Assertica.Core.Module
( Module(..), Import(..), Export(..), ExportItem(..)
, Visibility(..), ModuleInterface(..), ImportStyle(..)
, Definition(..)
, extractInterface, exportedNames, qualifyName
)
-- ============================================================================
-- MODULE ENVIRONMENT
-- ============================================================================
{-| The module environment tracks all loaded modules and enables name resolution.
-}
data ModuleEnv = ModuleEnv
{ envModules :: Map QName Module -- ^ All loaded modules
, envInterfaces :: Map QName ModuleInterface -- ^ Module interfaces
, envNameIndex :: Map Text [QName] -- ^ Map from name to qualified names
}
deriving (Eq, Show, Generic, Typeable, Data)
{-| Create an empty module environment.
-}
emptyModuleEnv :: ModuleEnv
emptyModuleEnv = ModuleEnv
{ envModules = Map.empty
, envInterfaces = Map.empty
, envNameIndex = Map.empty
}
{-| Add a module to the environment.
Returns error if the module already exists or has circular dependencies.
-}
addModule :: ModuleEnv -> Module -> Either String ModuleEnv
addModule env m = do
let modName = moduleName m
when (Map.member modName (envModules env)) $
Left $ "Module " ++ show modName ++ " already loaded"
-- Check for circular dependencies
let env' = env { envModules = Map.insert modName m (envModules env) }
validateCircularDependencies env' modName
-- Build interface and update index
let iface = extractInterface m
let env'' = env'
{ envInterfaces = Map.insert modName iface (envInterfaces env')
}
return env''
{-| Check if adding a module would create circular dependencies.
-}
validateCircularDependencies :: ModuleEnv -> QName -> Either String ()
validateCircularDependencies env modName =
case detectCircularChain env modName of
[] -> Right ()
chain -> Left $ "Circular dependency: " ++ show chain
{-| Detect if a module creates a circular dependency chain.
-}
detectCircularChain :: ModuleEnv -> QName -> [QName]
detectCircularChain env start =
detectChain env start start []
where
detectChain _ _ _ path | length path > 100 = [] -- Prevent infinite loops
detectChain env current target path
| current /= start && current == target = current : path
| otherwise =
case Map.lookup current (envModules env) of
Nothing -> []
Just m ->
let deps = [importModule imp | imp <- moduleImports m]
in concatMap (\d -> detectChain env d target (current : path)) deps
-- ============================================================================
-- NAME RESOLUTION
-- ============================================================================
{-| Resolve an unqualified name in a module's context.
Returns the fully qualified name and definition.
-}
resolveName :: ModuleEnv -> Module -> Text -> Either String (QName, Definition)
resolveName env currentMod name = do
-- First check local definitions
case Map.lookup name (moduleDefinitions currentMod) of
Just def -> Right (qualifyName currentMod name, def)
Nothing -> do
-- Then check imports
candidates <- resolveNameViaImports env currentMod name
case candidates of
[] -> Left $ "Name '" ++ T.unpack name ++ "' not found"
[(qn, def)] -> Right (qn, def)
multiple -> Left $ "Ambiguous name '" ++ T.unpack name ++ "': " ++
show (map fst multiple)
{-| Resolve a qualified name.
-}
resolveQName :: ModuleEnv -> QName -> Either String Definition
resolveQName env qname@(QName mod _) = do
-- Look up the module
let modQName = if null mod then error "Empty module path" else QName (init mod) (head (reverse mod))
m <- case Map.lookup modQName (envModules env) of
Just m -> Right m
Nothing -> Left $ "Module " ++ show modQName ++ " not found"
-- Look up the definition in the module
case Map.lookup (last (qnameModule qname ++ [qnameLocal qname])) (moduleDefinitions m) of
Just def -> Right def
Nothing -> Left $ "Definition " ++ show qname ++ " not found in module"
{-| Find all possible qualified names for an unqualified name through imports.
-}
resolveNameViaImports :: ModuleEnv -> Module -> Text -> Either String [(QName, Definition)]
resolveNameViaImports env currentMod name = do
let imports = moduleImports currentMod
candidates <- concat <$> mapM (resolveImport env currentMod name) imports
case candidates of
[] -> Left $ "Name '" ++ T.unpack name ++ "' not imported"
lst -> Right lst
{-| Resolve a name through a specific import statement.
-}
resolveImport :: ModuleEnv -> Module -> Text -> Import -> Either String [(QName, Definition)]
resolveImport env _ name imp = do
let importedMod = importModule imp
m <- case Map.lookup importedMod (envModules env) of
Just m -> Right m
Nothing -> Left $ "Module " ++ show importedMod ++ " not found"
-- Check if the name is exported
unless (Set.member name (exportedNames m))
(Left $ "Name '" ++ T.unpack name ++ "' not exported from " ++ show importedMod)
-- Look up in the module
case Map.lookup name (moduleDefinitions m) of
Just def -> do
let qn = QName (qnameModule importedMod) name
Right [(qn, def)]
Nothing -> Left $ "Definition '" ++ T.unpack name ++ "' not found"
where
unless cond err = if cond then Right () else Left err
{-| Resolve all imports in a module.
Returns a map from import alias to module.
-}
resolveImports :: ModuleEnv -> Module -> Either String (Map QName Module)
resolveImports env m = do
pairs <- mapM (resolveImportToModule env) (moduleImports m)
return $ Map.fromList pairs
where
resolveImportToModule env imp = do
let impMod = importModule imp
m <- case Map.lookup impMod (envModules env) of
Just m -> Right m
Nothing -> Left $ "Cannot resolve import: " ++ show impMod
let alias = case importAlias imp of
Just a -> a
Nothing -> impMod
return (alias, m)
{-| Resolve all dependencies of a module.
Returns modules sorted in dependency order.
-}
resolveDependencies :: ModuleEnv -> QName -> Either String [Module]
resolveDependencies env modName = do
m <- case Map.lookup modName (envModules env) of
Just m -> Right m
Nothing -> Left $ "Module not found: " ++ show modName
-- Collect all direct dependencies
let depNames = [importModule imp | imp <- moduleImports m]
deps <- mapM (\name -> case Map.lookup name (envModules env) of
Just m -> Right m
Nothing -> Left $ "Dependency not found: " ++ show name)
depNames
return (m : deps)
-- ============================================================================
-- MODULE LOOKUP
-- ============================================================================
{-| Look up a module by name.
-}
lookupModule :: ModuleEnv -> QName -> Maybe Module
lookupModule env name = Map.lookup name (envModules env)
{-| Find a module by partial name.
Returns all modules whose name ends with the given suffix.
-}
findModule :: ModuleEnv -> [Text] -> [Module]
findModule env suffix =
filter matches (Map.elems (envModules env))
where
matches m =
let (QName mod _) = moduleName m
in take (length suffix) (reverse mod) == reverse suffix
-- ============================================================================
-- CONFLICT DETECTION
-- ============================================================================
{-| An error from conflict detection.
-}
data ConflictError
= DuplicateExport Text QName QName -- ^ Same name exported from two modules
| DuplicateImport Text QName QName -- ^ Same name imported twice
| PrivateAccessDenied Text QName -- ^ Attempt to access private name
| CircularDependency [QName] -- ^ Circular import chain
| UnresolvedImport QName -- ^ Import cannot be resolved
deriving (Eq, Show, Generic, Typeable, Data)
{-| Detect name conflicts in imports.
Returns a list of conflicts.
-}
detectNameConflicts :: ModuleEnv -> Module -> [ConflictError]
detectNameConflicts env m =
detectDuplicateImports m ++
detectPrivateAccess env m
{-| Detect duplicate imports.
-}
detectDuplicateImports :: Module -> [ConflictError]
detectDuplicateImports m =
let imports = moduleImports m
importedNames = concatMap extractImportedNames imports
grouped = group (sort importedNames)
duplicates = [name | group <- grouped, length group > 1, let name = head group]
in map (\name -> DuplicateImport (fst name) (snd name) (snd (head (filter (\n -> fst n == name) importedNames))))
(nub duplicates)
where
nub = map head . group . sort
extractImportedNames imp = case importStyle imp of
Selective names -> [(name, importModule imp) | name <- names]
_ -> []
{-| Detect attempts to access private definitions.
-}
detectPrivateAccess :: ModuleEnv -> Module -> [ConflictError]
detectPrivateAccess env m =
let imports = moduleImports m
in concatMap checkImportAccess imports
where
checkImportAccess imp =
case Map.lookup (importModule imp) (envModules env) of
Nothing -> [UnresolvedImport (importModule imp)]
Just impMod ->
let exported = exportItems (moduleExports impMod)
privateNames = [name | ExportDef name Private <- exported]
in case importStyle imp of
Selective names ->
[PrivateAccessDenied name (importModule imp) | name <- names, elem name privateNames]
_ -> []
{-| Validate all imports in a module.
Returns list of validation errors.
-}
validateImports :: ModuleEnv -> Module -> Either String ()
validateImports env m = do
let errors = detectNameConflicts env m
if null errors
then Right ()
else Left $ "Import validation failed: " ++ show errors
-- ============================================================================
-- COMPILATION ORDERING
-- ============================================================================
{-| Compilation order: modules sorted such that dependencies come first.
-}
type CompilationOrder = [QName]
{-| Compute compilation order using topological sort.
Returns modules in order (dependencies first).
-}
computeCompilationOrder :: ModuleEnv -> Either String CompilationOrder
computeCompilationOrder env = do
let mods = Map.keys (envModules env)
topologicalSort env mods
where
topologicalSort env modNames
| null modNames = Right []
| otherwise =
case findModulesWithoutDeps env modNames of
[] -> Left "Circular dependencies detected"
roots -> do
let remaining = filter (`notElem` roots) modNames
rest <- topologicalSort env remaining
Right (roots ++ rest)
findModulesWithoutDeps env modNames =
filter (noDeps env modNames) modNames
noDeps env allMods modName =
case Map.lookup modName (envModules env) of
Nothing -> False
Just m ->
let deps = [importModule imp | imp <- moduleImports m]
in all (`notElem` allMods) deps
-- ============================================================================
-- VISIBILITY CHECKING
-- ============================================================================
{-| Check if a name is visible from outside its module.
-}
isNameVisible :: Module -> Text -> Bool
isNameVisible m name =
Set.member name (exportedNames m)
{-| Check if a name can be accessed by another module.
-}
checkNameAccess :: ModuleEnv -> QName -> QName -> Either String ()
checkNameAccess env fromMod toMod@(QName _ name) = do
targetMod <- case Map.lookup toMod (envModules env) of
Just m -> Right m
Nothing -> Left $ "Module not found: " ++ show toMod
unless (isNameVisible targetMod name)
(Left $ "Name " ++ show toMod ++ " is not exported")
where
unless cond err = if cond then Right () else Left err
|