compiler/
lib/
examples/
std/
arch/
collections/
lang/
alloc/
ast/
gen/
il/
module/
parser/
resolver/
scanner/
alloc.rad
4.2 KiB
ast.rad
22.4 KiB
gen.rad
489 B
il.rad
15.1 KiB
lower.rad
259.5 KiB
module.rad
13.4 KiB
package.rad
1.2 KiB
parser.rad
78.5 KiB
resolver.rad
244.3 KiB
scanner.rad
18.1 KiB
sexpr.rad
6.3 KiB
strings.rad
2.2 KiB
sys/
arch.rad
65 B
collections.rad
36 B
fmt.rad
3.8 KiB
intrinsics.rad
399 B
io.rad
1.2 KiB
lang.rad
222 B
mem.rad
2.1 KiB
sys.rad
167 B
testing.rad
2.3 KiB
tests.rad
11.6 KiB
vec.rad
3.1 KiB
std.rad
231 B
scripts/
seed/
test/
vim/
.gitignore
353 B
.gitsigners
112 B
LICENSE
1.1 KiB
Makefile
3.0 KiB
README
2.5 KiB
std.lib
1.0 KiB
std.lib.test
252 B
lib/std/lang/module.rad
raw
| 1 | //! Module graph and loader. |
| 2 | //! |
| 3 | //! This module tracks source files, parent/child relationships, and basic state for each |
| 4 | //! module so that later phases (parser, semantic analyzer) can resolve imports (`use`) |
| 5 | //! and avoid reloading the same file twice. |
| 6 | |
| 7 | pub mod printer; |
| 8 | |
| 9 | @test mod tests; |
| 10 | |
| 11 | use std::mem; |
| 12 | use std::lang::alloc; |
| 13 | use std::lang::ast; |
| 14 | use std::lang::strings; |
| 15 | |
| 16 | /// Maximum number of modules tracked in a single compilation graph. |
| 17 | pub const MAX_MODULES: u32 = 128; |
| 18 | /// Maximum number of characters for a module path. |
| 19 | const MAX_PATH_LEN: u32 = 256; |
| 20 | /// Maximum number of components that make up a logical module path. |
| 21 | const MAX_MODULE_PATH_DEPTH: u32 = 16; |
| 22 | /// Filesystem separator used when constructing child paths. |
| 23 | const PATH_SEP: u8 = '/'; |
| 24 | /// Source file extension handled by the loader. |
| 25 | const SOURCE_EXT: *[u8] = ".rad"; |
| 26 | |
| 27 | /// Lifecycle state for modules in the dependency graph. |
| 28 | pub union ModuleState { |
| 29 | /// Slot unused or yet to be initialized. |
| 30 | Vacant, |
| 31 | /// Module registered with a known path but not parsed yet. |
| 32 | Registered, |
| 33 | /// Module is currently being parsed (used for cycle detection). |
| 34 | Parsing, |
| 35 | /// Module finished parsing successfully. |
| 36 | Parsed, |
| 37 | /// Module is undergoing semantic analysis. |
| 38 | Analyzing, |
| 39 | /// Module finished semantic analysis and is ready for codegen. |
| 40 | Ready, |
| 41 | } |
| 42 | |
| 43 | /// Error categories that can be produced by the module graph. |
| 44 | pub union ModuleError { |
| 45 | /// Module not found. |
| 46 | NotFound(u16), |
| 47 | /// Adding the module would exceed the fixed storage. |
| 48 | CapacityExceeded, |
| 49 | /// Provided path is missing the required `.rad` extension or otherwise invalid. |
| 50 | InvalidPath, |
| 51 | /// Module graph path exceeded the maximum logical depth. |
| 52 | PathTooDeep, |
| 53 | /// Attempt to register a module before its parent. |
| 54 | MissingParent, |
| 55 | } |
| 56 | |
| 57 | /// Module metadata recorded inside the dependency graph. |
| 58 | pub record ModuleEntry { |
| 59 | /// Numeric identifier for the slot (index in the graph array). |
| 60 | id: u16, |
| 61 | /// Package identifier this module belongs to. |
| 62 | packageId: u16, |
| 63 | /// Parent module identifier, or `nil` for root modules. |
| 64 | parent: ?u16, |
| 65 | /// Absolute or workspace-relative path to the `.rad` source file. |
| 66 | filePath: *[u8], |
| 67 | /// Number of bytes covering the directory portion of `filePath`. |
| 68 | dirLen: u32, |
| 69 | /// Module name. |
| 70 | name: *[u8], |
| 71 | /// Logical path from the root module to this module. |
| 72 | path: [*[u8]; MAX_MODULE_PATH_DEPTH], |
| 73 | /// Number of segments inside `path`. |
| 74 | pathDepth: u32, |
| 75 | /// Current lifecycle state. |
| 76 | state: ModuleState, |
| 77 | /// Child module identifiers declared directly inside this module. |
| 78 | children: [u16; MAX_MODULES], |
| 79 | /// Number of entries stored in `children`. |
| 80 | childrenLen: u32, |
| 81 | /// Parsed AST root for this module when available. |
| 82 | ast: ?*mut ast::Node, |
| 83 | /// Source text for this module (for error reporting). |
| 84 | source: ?*[u8], |
| 85 | } |
| 86 | |
| 87 | /// Dense storage for all modules referenced by the compilation unit. |
| 88 | pub record ModuleGraph { |
| 89 | entries: *mut [ModuleEntry], |
| 90 | entriesLen: u32, |
| 91 | pool: *mut strings::Pool, |
| 92 | /// Arena used for all AST node allocations. |
| 93 | arena: ?*ast::NodeArena, |
| 94 | } |
| 95 | |
| 96 | /// Initialize an empty module graph backed by the provided storage. |
| 97 | pub fn moduleGraph( |
| 98 | storage: *mut [ModuleEntry], |
| 99 | pool: *mut strings::Pool, |
| 100 | arena: *mut ast::NodeArena |
| 101 | ) -> ModuleGraph { |
| 102 | return ModuleGraph { |
| 103 | entries: storage, |
| 104 | entriesLen: 0, |
| 105 | pool, |
| 106 | arena, |
| 107 | }; |
| 108 | } |
| 109 | |
| 110 | /// Register a root module residing at `path` for a package. |
| 111 | pub fn registerRoot(graph: *mut ModuleGraph, packageId: u16, filePath: *[u8]) -> u16 throws (ModuleError) { |
| 112 | let name = try basenameSlice(filePath); |
| 113 | return try registerRootWithName(graph, packageId, name, filePath); |
| 114 | } |
| 115 | |
| 116 | /// Register a root module with an explicit name and file path for a package. |
| 117 | pub fn registerRootWithName( |
| 118 | graph: *mut ModuleGraph, |
| 119 | packageId: u16, |
| 120 | name: *[u8], |
| 121 | filePath: *[u8] |
| 122 | ) -> u16 throws (ModuleError) { |
| 123 | let m = try allocModule(graph, packageId, name, filePath); |
| 124 | try appendPathSegment(m, m.name); |
| 125 | |
| 126 | m.state = ModuleState::Registered; |
| 127 | |
| 128 | return m.id; |
| 129 | } |
| 130 | |
| 131 | /// Register a child module. |
| 132 | /// Returns the module identifier. |
| 133 | pub fn registerChild( |
| 134 | graph: *mut ModuleGraph, |
| 135 | parentId: u16, |
| 136 | name: *[u8], |
| 137 | filePath: *[u8] |
| 138 | ) -> u16 throws (ModuleError) { |
| 139 | assert name.len > 0, "registerChild: name must not be empty"; |
| 140 | assert filePath.len > 0, "registerChild: file path must not be empty"; |
| 141 | |
| 142 | let parent = getMut(graph, parentId) |
| 143 | else throw ModuleError::NotFound(parentId); |
| 144 | |
| 145 | // If the child already exists under this parent, return it. |
| 146 | if let child = findChild(graph, name, parentId) { |
| 147 | return child.id; |
| 148 | } |
| 149 | // Inherit packageId from parent. |
| 150 | let m = try allocModule(graph, parent.packageId, name, filePath); |
| 151 | |
| 152 | m.dirLen = dirLength(m.filePath); |
| 153 | m.state = ModuleState::Registered; |
| 154 | m.parent = parentId; |
| 155 | |
| 156 | // Inherit path prefix from parent. |
| 157 | for p in moduleQualifiedPath(parent) { |
| 158 | try appendPathSegment(m, p); |
| 159 | } |
| 160 | try appendPathSegment(m, m.name); |
| 161 | |
| 162 | return try addChild(parent, m.id); |
| 163 | } |
| 164 | |
| 165 | /// Fetch a read-only view of the module identified by `id`. |
| 166 | pub fn get(graph: *ModuleGraph, id: u16) -> ?*ModuleEntry { |
| 167 | if not isValidId(graph, id) { |
| 168 | return nil; |
| 169 | } |
| 170 | return &graph.entries[id as u32]; |
| 171 | } |
| 172 | |
| 173 | /// Access a mutable entry by identifier. |
| 174 | fn getMut(graph: *mut ModuleGraph, id: u16) -> ?*mut ModuleEntry { |
| 175 | if not isValidId(graph, id) { |
| 176 | return nil; |
| 177 | } |
| 178 | return &mut graph.entries[id as u32]; |
| 179 | } |
| 180 | |
| 181 | /// Return the identifier of the child stored at `index`. |
| 182 | pub fn childAt(m: *ModuleEntry, index: u32) -> u16 { |
| 183 | assert index < m.childrenLen, "childAt: index must be valid"; |
| 184 | return m.children[index]; |
| 185 | } |
| 186 | |
| 187 | /// Public accessor for a module's directory prefix. |
| 188 | pub fn moduleDir(m: *ModuleEntry) -> *[u8] { |
| 189 | return &m.filePath[..m.dirLen]; |
| 190 | } |
| 191 | |
| 192 | /// Public accessor to the logical module path segments. |
| 193 | pub fn moduleQualifiedPath(m: *ModuleEntry) -> *[*[u8]] { |
| 194 | assert m.pathDepth > 0, "moduleQualifiedPath: path must not be empty"; |
| 195 | return &m.path[..m.pathDepth]; |
| 196 | } |
| 197 | |
| 198 | /// Retrieve the lifecycle state for `id`. |
| 199 | pub fn state(graph: *ModuleGraph, id: u16) -> ModuleState throws (ModuleError) { |
| 200 | let m = get(graph, id) else { |
| 201 | throw ModuleError::NotFound(id); |
| 202 | }; |
| 203 | return m.state; |
| 204 | } |
| 205 | |
| 206 | /// Record the parsed AST root for `id`. |
| 207 | pub fn setAst(graph: *mut ModuleGraph, id: u16, root: *mut ast::Node) throws (ModuleError) { |
| 208 | let m = getMut(graph, id) else throw ModuleError::NotFound(id); |
| 209 | m.ast = root; |
| 210 | m.state = ModuleState::Parsed; |
| 211 | } |
| 212 | |
| 213 | /// Set the source text for a module. |
| 214 | pub fn setSource(graph: *mut ModuleGraph, id: u16, source: *[u8]) throws (ModuleError) { |
| 215 | let m = getMut(graph, id) else throw ModuleError::NotFound(id); |
| 216 | m.source = source; |
| 217 | } |
| 218 | |
| 219 | /// Look up a child module by name under the given parent. |
| 220 | pub fn findChild(graph: *ModuleGraph, name: *[u8], parentId: u16) -> ?*ModuleEntry { |
| 221 | assert isValidId(graph, parentId), "findChild: parent identifier is valid"; |
| 222 | |
| 223 | let parent = &graph.entries[parentId as u32]; |
| 224 | for i in 0..parent.childrenLen { |
| 225 | let childId = parent.children[i]; |
| 226 | let child = &graph.entries[childId as u32]; |
| 227 | if mem::eq(child.name, name) { |
| 228 | return child; |
| 229 | } |
| 230 | } |
| 231 | return nil; |
| 232 | } |
| 233 | |
| 234 | /// Parse a file path into components by splitting on '/'. |
| 235 | /// Expects and removes the '.rad' extension from the last component. |
| 236 | /// Returns the number of components extracted, or `nil` if the path is invalid. |
| 237 | pub fn parsePath(filePath: *[u8], components: *mut [*[u8]]) -> ?u32 { |
| 238 | let mut count: u32 = 0; |
| 239 | let mut last: u32 = 0; |
| 240 | |
| 241 | // Split on '/' to extract all but the last component. |
| 242 | for i in 0..filePath.len { |
| 243 | if filePath[i] == PATH_SEP { |
| 244 | if i > last { |
| 245 | assert count < components.len, "parsePath: output slice is large enough"; |
| 246 | components[count] = &filePath[last..i]; |
| 247 | count += 1; |
| 248 | } |
| 249 | last = i + 1; |
| 250 | } |
| 251 | } |
| 252 | if last >= filePath.len { |
| 253 | return nil; // Path ends with separator or is empty. |
| 254 | } |
| 255 | |
| 256 | // Handle the last component by removing extension. |
| 257 | assert count < components.len, "parsePath: output slice is large enough"; |
| 258 | if let name = trimExtension(&filePath[last..]) { |
| 259 | components[count] = name; |
| 260 | } else { |
| 261 | return nil; |
| 262 | }; |
| 263 | count += 1; |
| 264 | |
| 265 | return count; |
| 266 | } |
| 267 | |
| 268 | /// Register a module from a file path, creating the full hierarchy as needed. |
| 269 | /// The path is split into components and the module hierarchy is built accordingly. |
| 270 | /// If `rootId` is `nil`, registers a new root for the given package. |
| 271 | /// Returns the module ID of the last component. |
| 272 | pub fn registerFromPath(graph: *mut ModuleGraph, packageId: u16, rootId: ?u16, filePath: *[u8]) -> u16 throws (ModuleError) { |
| 273 | let root = rootId else { |
| 274 | return try registerRoot(graph, packageId, filePath); |
| 275 | }; |
| 276 | let rootEntry = get(graph, root) else { |
| 277 | panic "registerFromPath: root is missing from storage"; |
| 278 | }; |
| 279 | |
| 280 | // Strip the base directory from the file path. |
| 281 | let baseDir = moduleDir(rootEntry); |
| 282 | let pathSuffix = mem::stripPrefix(baseDir, filePath) else { |
| 283 | throw ModuleError::InvalidPath; |
| 284 | }; |
| 285 | |
| 286 | // Parse the stripped path into components. |
| 287 | let mut parts: [*[u8]; MAX_MODULE_PATH_DEPTH] = undefined; |
| 288 | let partsLen = parsePath(pathSuffix, &mut parts[..]) else { |
| 289 | throw ModuleError::InvalidPath; |
| 290 | }; |
| 291 | if partsLen == 0 { |
| 292 | throw ModuleError::InvalidPath; |
| 293 | } |
| 294 | |
| 295 | // Strip the root's qualified path from the parsed components. |
| 296 | let rootPath = moduleQualifiedPath(rootEntry); |
| 297 | let childPath = stripPathPrefix(rootPath, &parts[..partsLen]) else { |
| 298 | throw ModuleError::InvalidPath; |
| 299 | }; |
| 300 | if childPath.len == 0 { |
| 301 | throw ModuleError::InvalidPath; |
| 302 | } |
| 303 | let childName = childPath[childPath.len - 1]; |
| 304 | |
| 305 | // Navigate through all but the last segment to find the parent. |
| 306 | let mut parentId = root; |
| 307 | for part in &childPath[..childPath.len - 1] { |
| 308 | let child = findChild(graph, part, parentId) else { |
| 309 | throw ModuleError::MissingParent; |
| 310 | }; |
| 311 | parentId = child.id; |
| 312 | } |
| 313 | return try registerChild(graph, parentId, childName, filePath); |
| 314 | } |
| 315 | |
| 316 | /// Allocate a fresh entry in the graph. |
| 317 | fn allocModule(graph: *mut ModuleGraph, packageId: u16, name: *[u8], filePath: *[u8]) -> *mut ModuleEntry throws (ModuleError) { |
| 318 | if graph.entriesLen >= graph.entries.len { |
| 319 | throw ModuleError::CapacityExceeded; |
| 320 | } |
| 321 | let idx = graph.entriesLen; |
| 322 | graph.entriesLen += 1; |
| 323 | |
| 324 | // TODO: This is a common pattern that needs better syntax. |
| 325 | let m = &mut graph.entries[idx]; |
| 326 | *m = ModuleEntry { |
| 327 | id: idx as u16, |
| 328 | packageId, |
| 329 | parent: nil, |
| 330 | filePath, |
| 331 | dirLen: 0, |
| 332 | name: strings::intern(graph.pool, name), |
| 333 | path: undefined, |
| 334 | pathDepth: 0, |
| 335 | state: ModuleState::Vacant, |
| 336 | children: undefined, |
| 337 | childrenLen: 0, |
| 338 | ast: nil, |
| 339 | source: nil, |
| 340 | }; |
| 341 | m.dirLen = dirLength(m.filePath); |
| 342 | |
| 343 | return m; |
| 344 | } |
| 345 | |
| 346 | /// Append a logical path segment (module identifier) to the entry. |
| 347 | fn appendPathSegment(entry: *mut ModuleEntry, segment: *[u8]) throws (ModuleError) { |
| 348 | if entry.pathDepth >= entry.path.len { |
| 349 | throw ModuleError::PathTooDeep; |
| 350 | } |
| 351 | entry.path[entry.pathDepth] = segment; |
| 352 | entry.pathDepth += 1; |
| 353 | } |
| 354 | |
| 355 | /// Append a child identifier to the parent's child list. |
| 356 | fn addChild(parent: *mut ModuleEntry, childId: u16) -> u16 throws (ModuleError) { |
| 357 | if parent.childrenLen >= parent.children.len { |
| 358 | throw ModuleError::CapacityExceeded; |
| 359 | } |
| 360 | parent.children[parent.childrenLen] = childId; |
| 361 | parent.childrenLen += 1; |
| 362 | |
| 363 | return childId; |
| 364 | } |
| 365 | |
| 366 | /// Check if `id` points at an allocated entry. |
| 367 | fn isValidId(graph: *ModuleGraph, id: u16) -> bool { |
| 368 | return (id as u32) < graph.entriesLen; |
| 369 | } |
| 370 | |
| 371 | /// Return the length of the directory prefix for `path`. |
| 372 | fn dirLength(path: *[u8]) -> u32 { |
| 373 | if path.len == 0 { |
| 374 | return 0; |
| 375 | } |
| 376 | let mut last: i32 = -1; |
| 377 | for i in 0..path.len { |
| 378 | if path[i] == PATH_SEP { |
| 379 | last = i as i32; |
| 380 | } |
| 381 | } |
| 382 | if last < 0 { |
| 383 | return 0; |
| 384 | } |
| 385 | return (last as u32) + 1; |
| 386 | } |
| 387 | |
| 388 | /// Produce a subslice for the basename of `path` (without extension). |
| 389 | fn basenameSlice(path: *[u8]) -> *[u8] throws (ModuleError) { |
| 390 | let start = basenameStart(path); |
| 391 | let withoutExt = trimExtension(path) else { |
| 392 | throw ModuleError::InvalidPath; |
| 393 | }; |
| 394 | return &withoutExt[start..]; |
| 395 | } |
| 396 | |
| 397 | /// Find the byte offset immediately following the last separator. |
| 398 | fn basenameStart(path: *[u8]) -> u32 { |
| 399 | let mut start: u32 = 0; |
| 400 | for i in 0..path.len { |
| 401 | if path[i] == PATH_SEP { |
| 402 | start = i + 1; |
| 403 | } |
| 404 | } |
| 405 | return start; |
| 406 | } |
| 407 | |
| 408 | /// Trim the `.rad` extension from `path` if present. |
| 409 | /// Returns the path slice without the extension. |
| 410 | pub fn trimExtension(path: *[u8]) -> ?*[u8] { |
| 411 | if path.len < SOURCE_EXT.len { |
| 412 | return nil; |
| 413 | } |
| 414 | let extStart = path.len - SOURCE_EXT.len; |
| 415 | for ext, i in SOURCE_EXT { |
| 416 | if path[extStart + i] != ext { |
| 417 | return nil; |
| 418 | } |
| 419 | } |
| 420 | return &path[..extStart]; |
| 421 | } |
| 422 | |
| 423 | /// Strip prefix from path, and return the suffix. |
| 424 | fn stripPathPrefix(prefix: *[*[u8]], path: *[*[u8]]) -> ?*[*[u8]] { |
| 425 | if prefix.len == 0 { |
| 426 | return path; |
| 427 | } |
| 428 | if prefix.len > path.len { |
| 429 | return nil; |
| 430 | } |
| 431 | for segment, i in prefix { |
| 432 | if not mem::eq(segment, path[i]) { |
| 433 | return nil; |
| 434 | } |
| 435 | } |
| 436 | return &path[prefix.len..]; |
| 437 | } |