compiler/
radiance.rad
28.5 KiB
lib/
scripts/
seed/
test/
vim/
.gitignore
353 B
.gitsigners
112 B
LICENSE
1.1 KiB
Makefile
3.1 KiB
README
2.5 KiB
std.lib
987 B
std.lib.test
252 B
compiler/radiance.rad
raw
| 1 | //! Radiance compiler front-end. |
| 2 | use std::mem; |
| 3 | use std::fmt; |
| 4 | use std::io; |
| 5 | use std::lang::alloc; |
| 6 | use std::lang::ast; |
| 7 | use std::lang::parser; |
| 8 | use std::lang::scanner; |
| 9 | use std::lang::resolver; |
| 10 | use std::lang::module; |
| 11 | use std::lang::strings; |
| 12 | use std::lang::package; |
| 13 | use std::lang::il; |
| 14 | use std::lang::lower; |
| 15 | use std::arch::rv64; |
| 16 | use std::arch::rv64::printer; |
| 17 | use std::lang::sexpr; |
| 18 | use std::sys; |
| 19 | use std::sys::unix; |
| 20 | |
| 21 | /// Maximum number of modules we can load per package. |
| 22 | const MAX_LOADED_MODULES: u32 = 64; |
| 23 | /// Maximum number of packages we can compile. |
| 24 | const MAX_PACKAGES: u32 = 4; |
| 25 | /// Total module entries across all packages. |
| 26 | const MAX_TOTAL_MODULES: u32 = 192; |
| 27 | /// Source code buffer arena (2 MB). |
| 28 | const MAX_SOURCES_SIZE: u32 = 2097152; |
| 29 | /// Maximum number of test functions we can discover. |
| 30 | const MAX_TESTS: u32 = 1024; |
| 31 | |
| 32 | /// Temporary arena size (32 MB) - retains all parsed AST until resolution. |
| 33 | /// Used for: AST during parsing, then codegen scratch space. |
| 34 | const TEMP_ARENA_SIZE: u32 = 33554432; |
| 35 | /// Main arena size (128 MB) - lives throughout compilation. |
| 36 | /// Used for: resolver data, types, symbols, lowered IL. |
| 37 | const MAIN_ARENA_SIZE: u32 = 134217728; |
| 38 | |
| 39 | /// Temporary storage arena - reusable between phases. |
| 40 | static TEMP_ARENA: [u8; TEMP_ARENA_SIZE] = undefined; |
| 41 | /// Main storage arena - persists throughout compilation. |
| 42 | static MAIN_ARENA: [u8; MAIN_ARENA_SIZE] = undefined; |
| 43 | |
| 44 | /// Module source code. |
| 45 | static MODULE_SOURCES: [u8; MAX_SOURCES_SIZE] = undefined; |
| 46 | /// Module entries for all packages. |
| 47 | static MODULE_ENTRIES: [module::ModuleEntry; MAX_TOTAL_MODULES] = undefined; |
| 48 | /// String pool. |
| 49 | static STRING_POOL: strings::Pool = strings::Pool { table: undefined, count: 0 }; |
| 50 | |
| 51 | /// Package scope. |
| 52 | static RESOLVER_PKG_SCOPE: resolver::Scope = undefined; |
| 53 | /// Errors emitted by resolver. |
| 54 | static RESOLVER_ERRORS: [resolver::Error; resolver::MAX_ERRORS] = undefined; |
| 55 | |
| 56 | /// Code generation storage. |
| 57 | static CODEGEN_DATA_SYMS: [rv64::DataSym; rv64::MAX_DATA_SYMS] = undefined; |
| 58 | |
| 59 | /// Debug info file extension. |
| 60 | const DEBUG_EXT: *[u8] = ".debug"; |
| 61 | |
| 62 | /// Read-only data file extension. |
| 63 | const RO_DATA_EXT: *[u8] = ".ro.data"; |
| 64 | /// Read-write data file extension. |
| 65 | const RW_DATA_EXT: *[u8] = ".rw.data"; |
| 66 | /// Maximum rodata size (1MB). |
| 67 | const MAX_RO_DATA_SIZE: u32 = 1048576; |
| 68 | /// Maximum rwdata size (1MB). |
| 69 | const MAX_RW_DATA_SIZE: u32 = 1048576; |
| 70 | /// Maximum path length. |
| 71 | const MAX_PATH_LEN: u32 = 256; |
| 72 | /// Read-only data buffer. |
| 73 | static RO_DATA_BUF: [u8; MAX_RO_DATA_SIZE] = undefined; |
| 74 | /// Read-write data buffer. |
| 75 | static RW_DATA_BUF: [u8; MAX_RW_DATA_SIZE] = undefined; |
| 76 | |
| 77 | /// Usage string. |
| 78 | const USAGE: *[u8] = |
| 79 | "usage: radiance -pkg <name> -mod <input>.. [-pkg <name> -mod <input>..] -entry <pkg> -o <output>\n"; |
| 80 | |
| 81 | /// Compiler error. |
| 82 | union Error { |
| 83 | Other, |
| 84 | } |
| 85 | |
| 86 | /// What to dump during compilation. |
| 87 | union Dump { |
| 88 | /// Don't dump anything. |
| 89 | None, |
| 90 | /// Dump the parsed AST before semantic analysis. |
| 91 | Ast, |
| 92 | /// Dump the module graph. |
| 93 | Graph, |
| 94 | /// Dump the IL. |
| 95 | Il, |
| 96 | /// Dump the generated assembly. |
| 97 | Asm, |
| 98 | } |
| 99 | |
| 100 | /// A discovered test function. |
| 101 | record TestDesc { |
| 102 | /// Full module qualified path segments (eg. ["std", "tests"]). |
| 103 | modPath: *[*[u8]], |
| 104 | /// Test function name (eg. "testFoo"). |
| 105 | fnName: *[u8], |
| 106 | } |
| 107 | |
| 108 | /// Compilation context. |
| 109 | record CompileContext { |
| 110 | /// Array of packages to compile. |
| 111 | packages: [package::Package; MAX_PACKAGES], |
| 112 | /// Number of packages. |
| 113 | packageCount: u32, |
| 114 | /// Index of entry package. |
| 115 | entryPkgIdx: ?u32, |
| 116 | /// Global module graph shared by all packages. |
| 117 | graph: module::ModuleGraph, |
| 118 | /// Resolver configuration. |
| 119 | config: resolver::Config, |
| 120 | /// What to dump during compilation. |
| 121 | dump: Dump, |
| 122 | /// Output path for binary. |
| 123 | outputPath: ?*[u8], |
| 124 | /// Whether to emit debug info (.debug file). |
| 125 | debug: bool, |
| 126 | } |
| 127 | |
| 128 | /// Root module info for a package. |
| 129 | record RootModule { |
| 130 | entry: *module::ModuleEntry, |
| 131 | ast: *mut ast::Node, |
| 132 | } |
| 133 | |
| 134 | /// Print a log line for the given package. |
| 135 | fn pkgLog(pkg: *package::Package, msg: *[*[u8]]) { |
| 136 | io::printError("radiance: "); |
| 137 | io::printError(pkg.name); |
| 138 | io::printError(": "); |
| 139 | |
| 140 | for part, i in msg { |
| 141 | io::printError(part); |
| 142 | if i < msg.len - 1 { |
| 143 | io::printError(" "); |
| 144 | } |
| 145 | } |
| 146 | io::printError("\n"); |
| 147 | } |
| 148 | |
| 149 | /// Register, load, and parse `path` within `pkg`. |
| 150 | fn processModule( |
| 151 | pkg: *mut package::Package, |
| 152 | graph: *mut module::ModuleGraph, |
| 153 | path: *[u8], |
| 154 | nodeArena: *mut ast::NodeArena, |
| 155 | sourceArena: *mut alloc::Arena |
| 156 | ) throws (Error) { |
| 157 | pkgLog(pkg, &["parsing", "(", path, ")", ".."]); |
| 158 | |
| 159 | let moduleId = try package::registerModule(pkg, graph, path) catch { |
| 160 | io::printError("radiance: error registering module\n"); |
| 161 | throw Error::Other; |
| 162 | }; |
| 163 | // Read file into remaining arena space. |
| 164 | let buffer = alloc::remainingBuf(sourceArena); |
| 165 | if buffer.len == 0 { |
| 166 | io::printError("radiance: fatal: source arena exhausted\n"); |
| 167 | throw Error::Other; |
| 168 | } |
| 169 | let source = unix::readFile(path, buffer) else { |
| 170 | io::printError("radiance: error reading file\n"); |
| 171 | throw Error::Other; |
| 172 | }; |
| 173 | if source.len == buffer.len { |
| 174 | io::printError("radiance: fatal: source arena too small, file truncated: "); |
| 175 | io::printError(path); |
| 176 | io::printError("\n"); |
| 177 | throw Error::Other; |
| 178 | } |
| 179 | // Commit only what was read. |
| 180 | alloc::commit(sourceArena, source.len); |
| 181 | |
| 182 | let ast = try parser::parse(scanner::SourceLoc::File(path), source, nodeArena, &mut STRING_POOL) catch { |
| 183 | throw Error::Other; |
| 184 | }; |
| 185 | try module::setAst(graph, moduleId, ast) catch { |
| 186 | io::printError("radiance: error setting AST\n"); |
| 187 | throw Error::Other; |
| 188 | }; |
| 189 | try module::setSource(graph, moduleId, source) catch { |
| 190 | io::printError("radiance: error setting source\n"); |
| 191 | throw Error::Other; |
| 192 | }; |
| 193 | } |
| 194 | |
| 195 | /// Parse CLI arguments and return compilation context. |
| 196 | fn processCommand( |
| 197 | args: *[*[u8]], |
| 198 | arena: *mut ast::NodeArena |
| 199 | ) -> CompileContext throws (Error) { |
| 200 | let mut buildTest = false; |
| 201 | let mut debugEnabled = false; |
| 202 | let mut outputPath: ?*[u8] = nil; |
| 203 | let mut dump = Dump::None; |
| 204 | let mut entryPkgName: ?*[u8] = nil; |
| 205 | |
| 206 | // Per-package module path tracking. |
| 207 | let mut moduleCounts: [u32; MAX_PACKAGES] = undefined; |
| 208 | let mut modulePaths: [[*[u8]; MAX_LOADED_MODULES]; MAX_PACKAGES] = undefined; |
| 209 | let mut pkgNames: [*[u8]; MAX_PACKAGES] = undefined; |
| 210 | let mut pkgCount: u32 = 0; |
| 211 | let mut currentPkgIdx: ?u32 = nil; |
| 212 | |
| 213 | for i in 0..MAX_PACKAGES { |
| 214 | moduleCounts[i] = 0; |
| 215 | } |
| 216 | if args.len == 0 { |
| 217 | io::printError(USAGE); |
| 218 | throw Error::Other; |
| 219 | } |
| 220 | let mut idx: u32 = 0; |
| 221 | |
| 222 | while idx < args.len { |
| 223 | let arg = args[idx]; |
| 224 | if mem::eq(arg, "-pkg") { |
| 225 | idx += 1; |
| 226 | if idx >= args.len { |
| 227 | io::printError("radiance: `-pkg` requires a package name\n"); |
| 228 | throw Error::Other; |
| 229 | } |
| 230 | if pkgCount >= MAX_PACKAGES { |
| 231 | io::printError("radiance: too many packages specified\n"); |
| 232 | throw Error::Other; |
| 233 | } |
| 234 | pkgNames[pkgCount] = args[idx]; |
| 235 | currentPkgIdx = pkgCount; |
| 236 | pkgCount += 1; |
| 237 | } else if mem::eq(arg, "-mod") { |
| 238 | idx += 1; |
| 239 | if idx >= args.len { |
| 240 | io::printError("radiance: `-mod` requires a module path\n"); |
| 241 | throw Error::Other; |
| 242 | } |
| 243 | let pkgIdx = currentPkgIdx else { |
| 244 | io::printError("radiance: `-mod` must follow a `-pkg` argument\n"); |
| 245 | throw Error::Other; |
| 246 | }; |
| 247 | if moduleCounts[pkgIdx] >= MAX_LOADED_MODULES { |
| 248 | io::printError("radiance: too many modules specified for package\n"); |
| 249 | throw Error::Other; |
| 250 | } |
| 251 | modulePaths[pkgIdx][moduleCounts[pkgIdx]] = args[idx]; |
| 252 | moduleCounts[pkgIdx] += 1; |
| 253 | } else if mem::eq(arg, "-entry") { |
| 254 | idx += 1; |
| 255 | if idx >= args.len { |
| 256 | io::printError("radiance: `-entry` requires a package name\n"); |
| 257 | throw Error::Other; |
| 258 | } |
| 259 | entryPkgName = args[idx]; |
| 260 | } else if mem::eq(arg, "-test") { |
| 261 | buildTest = true; |
| 262 | } else if mem::eq(arg, "-debug") { |
| 263 | debugEnabled = true; |
| 264 | } else if mem::eq(arg, "-o") { |
| 265 | idx += 1; |
| 266 | if idx >= args.len { |
| 267 | io::printError("radiance: `-o` requires an output path\n"); |
| 268 | throw Error::Other; |
| 269 | } |
| 270 | outputPath = args[idx]; |
| 271 | } else if mem::eq(arg, "-dump") { |
| 272 | idx += 1; |
| 273 | if idx >= args.len { |
| 274 | io::printError("radiance: `-dump` requires a mode (eg. ast)\n"); |
| 275 | throw Error::Other; |
| 276 | } |
| 277 | let mode = args[idx]; |
| 278 | if mem::eq(mode, "ast") { |
| 279 | dump = Dump::Ast; |
| 280 | } else if mem::eq(mode, "graph") { |
| 281 | dump = Dump::Graph; |
| 282 | } else if mem::eq(mode, "il") { |
| 283 | dump = Dump::Il; |
| 284 | } else if mem::eq(mode, "asm") { |
| 285 | dump = Dump::Asm; |
| 286 | } else { |
| 287 | io::printError("radiance: unknown dump mode `"); |
| 288 | io::printError(mode); |
| 289 | io::printError("` (expected: ast, graph, il, asm)\n"); |
| 290 | throw Error::Other; |
| 291 | } |
| 292 | } else { |
| 293 | io::printError("radiance: unknown argument `"); |
| 294 | io::printError(arg); |
| 295 | io::printError("`\n"); |
| 296 | throw Error::Other; |
| 297 | } |
| 298 | idx += 1; |
| 299 | } |
| 300 | if pkgCount == 0 { |
| 301 | io::printError("radiance: no package specified\n"); |
| 302 | throw Error::Other; |
| 303 | } |
| 304 | |
| 305 | // Determine entry package index. |
| 306 | let mut entryPkgIdx: ?u32 = nil; |
| 307 | if pkgCount == 1 { |
| 308 | // Single package: it is the entry. |
| 309 | entryPkgIdx = 0; |
| 310 | } else { |
| 311 | // Multiple packages: need -entry. |
| 312 | let entryName = entryPkgName else { |
| 313 | io::printError("radiance: `-entry` required when multiple packages specified\n"); |
| 314 | throw Error::Other; |
| 315 | }; |
| 316 | for i in 0..pkgCount { |
| 317 | if mem::eq(pkgNames[i], entryName) { |
| 318 | entryPkgIdx = i; |
| 319 | break; |
| 320 | } |
| 321 | } |
| 322 | if entryPkgIdx == nil { |
| 323 | io::printError("radiance: fatal: entry package `"); |
| 324 | io::printError(entryName); |
| 325 | io::printError("` not found\n"); |
| 326 | |
| 327 | throw Error::Other; |
| 328 | } |
| 329 | } |
| 330 | let graph = module::moduleGraph(&mut MODULE_ENTRIES[..], &mut STRING_POOL, arena); |
| 331 | let mut ctx = CompileContext { |
| 332 | packages: undefined, |
| 333 | packageCount: pkgCount, |
| 334 | entryPkgIdx, |
| 335 | graph, |
| 336 | config: resolver::Config { buildTest }, |
| 337 | dump, |
| 338 | outputPath, |
| 339 | debug: debugEnabled, |
| 340 | }; |
| 341 | // Initialize and parse all packages. |
| 342 | let mut sourceArena = alloc::new(&mut MODULE_SOURCES[..]); |
| 343 | for i in 0..pkgCount { |
| 344 | package::init(&mut ctx.packages[i], i as u16, pkgNames[i], &mut STRING_POOL); |
| 345 | |
| 346 | for j in 0..moduleCounts[i] { |
| 347 | let path = modulePaths[i][j]; |
| 348 | try processModule(&mut ctx.packages[i], &mut ctx.graph, path, arena, &mut sourceArena); |
| 349 | } |
| 350 | } |
| 351 | return ctx; |
| 352 | } |
| 353 | |
| 354 | /// Get the entry package from the context. |
| 355 | fn getEntryPackage(ctx: *CompileContext) -> *package::Package throws (Error) { |
| 356 | let entryIdx = ctx.entryPkgIdx else { |
| 357 | io::printError("radiance: no entry package specified\n"); |
| 358 | throw Error::Other; |
| 359 | }; |
| 360 | return &ctx.packages[entryIdx]; |
| 361 | } |
| 362 | |
| 363 | /// Get root module info from a package. |
| 364 | fn getRootModule(pkg: *package::Package, graph: *module::ModuleGraph) -> RootModule throws (Error) { |
| 365 | let rootId = pkg.rootModuleId else { |
| 366 | io::printError("radiance: no root module found\n"); |
| 367 | throw Error::Other; |
| 368 | }; |
| 369 | let rootEntry = module::get(graph, rootId) else { |
| 370 | io::printError("radiance: root module entry not found\n"); |
| 371 | throw Error::Other; |
| 372 | }; |
| 373 | let rootAst = rootEntry.ast else { |
| 374 | io::printError("radiance: root module has no AST\n"); |
| 375 | throw Error::Other; |
| 376 | }; |
| 377 | return RootModule { entry: rootEntry, ast: rootAst }; |
| 378 | } |
| 379 | |
| 380 | /// Dump the module graph. |
| 381 | fn dumpGraph(ctx: *CompileContext) { |
| 382 | let mut arena = alloc::new(&mut MAIN_ARENA[..]); |
| 383 | module::printer::printGraph(&ctx.graph, &mut arena); |
| 384 | } |
| 385 | |
| 386 | /// Dump the parsed AST. |
| 387 | fn dumpAst(ctx: *CompileContext) throws (Error) { |
| 388 | let pkg = try getEntryPackage(ctx); |
| 389 | let root = try getRootModule(pkg, &ctx.graph); |
| 390 | let mut arena = alloc::new(&mut MAIN_ARENA[..]); |
| 391 | |
| 392 | ast::printer::printTree(root.ast, &mut arena); |
| 393 | } |
| 394 | |
| 395 | /// Lower all packages into a single IL program. |
| 396 | /// Dependencies are lowered first, then the entry package. |
| 397 | fn lowerAllPackages( |
| 398 | ctx: *mut CompileContext, |
| 399 | res: *mut resolver::Resolver |
| 400 | ) -> il::Program throws (Error) { |
| 401 | let entryIdx = ctx.entryPkgIdx else { |
| 402 | panic "lowerAllPackages: no entry package"; |
| 403 | }; |
| 404 | let entryPkg = &ctx.packages[entryIdx]; |
| 405 | |
| 406 | // Create the lowerer accumulator using entry package's name. |
| 407 | let options = lower::LowerOptions { debug: ctx.debug, buildTest: ctx.config.buildTest }; |
| 408 | let mut low = lower::lowerer(res, &ctx.graph, entryPkg.name, &mut res.arena, options); |
| 409 | |
| 410 | // Lower all packages except entry. |
| 411 | for i in 0..ctx.packageCount { |
| 412 | if i != entryIdx { |
| 413 | try lowerPackage(ctx, res, &mut low, &mut ctx.packages[i], false); |
| 414 | } |
| 415 | } |
| 416 | // Lower entry package. |
| 417 | let defaultFn = try lowerPackage(ctx, res, &mut low, &mut ctx.packages[entryIdx], true); |
| 418 | |
| 419 | // Finalize and return the unified program. |
| 420 | return lower::finalize(&low, defaultFn); |
| 421 | } |
| 422 | |
| 423 | /// Lower all modules in a package into the lowerer accumulator. |
| 424 | fn lowerPackage( |
| 425 | ctx: *CompileContext, |
| 426 | res: *mut resolver::Resolver, |
| 427 | low: *mut lower::Lowerer, |
| 428 | pkg: *mut package::Package, |
| 429 | isEntry: bool |
| 430 | ) -> ?u32 throws (Error) { |
| 431 | let rootId = pkg.rootModuleId else { |
| 432 | io::printError("radiance: no root module found\n"); |
| 433 | throw Error::Other; |
| 434 | }; |
| 435 | // Set lowerer's package context for qualified name generation. |
| 436 | // TODO: We shouldn't have to call this manually. |
| 437 | lower::setPackage(low, &ctx.graph, pkg.name); |
| 438 | |
| 439 | return try lowerModuleTreeInto(ctx, low, &ctx.graph, rootId, isEntry, pkg); |
| 440 | } |
| 441 | |
| 442 | /// Recursively lower a module and all its children into the accumulator. |
| 443 | fn lowerModuleTreeInto( |
| 444 | ctx: *CompileContext, |
| 445 | low: *mut lower::Lowerer, |
| 446 | graph: *module::ModuleGraph, |
| 447 | modId: u16, |
| 448 | isRoot: bool, |
| 449 | pkg: *package::Package |
| 450 | ) -> ?u32 throws (Error) { |
| 451 | let entry = module::get(graph, modId) else { |
| 452 | io::printError("radiance: module entry not found\n"); |
| 453 | throw Error::Other; |
| 454 | }; |
| 455 | let modAst = entry.ast else { |
| 456 | io::printError("radiance: module has no AST\n"); |
| 457 | throw Error::Other; |
| 458 | }; |
| 459 | pkgLog(pkg, &["lowering", "(", entry.filePath, ")", ".."]); |
| 460 | |
| 461 | let defaultFn = try lower::lowerModule(low, modId, modAst, isRoot) catch err { |
| 462 | io::printError("radiance: internal error during lowering: "); |
| 463 | lower::printError(err); |
| 464 | io::printError("\n"); |
| 465 | throw Error::Other; |
| 466 | }; |
| 467 | // Recurse into children. |
| 468 | for i in 0..entry.childrenLen { |
| 469 | let childId = module::childAt(entry, i); |
| 470 | try lowerModuleTreeInto(ctx, low, graph, childId, false, pkg); |
| 471 | } |
| 472 | return defaultFn; |
| 473 | } |
| 474 | |
| 475 | /// Build a scope access chain: a::b::c from a slice of identifiers. |
| 476 | fn synthScopeAccess(arena: *mut ast::NodeArena, path: *[*[u8]]) -> *ast::Node { |
| 477 | let mut result = ast::synthNode( |
| 478 | arena, |
| 479 | ast::NodeValue::Ident(strings::intern(&mut STRING_POOL, path[0])) |
| 480 | ); |
| 481 | for i in 1..path.len { |
| 482 | let child = ast::synthNode( |
| 483 | arena, |
| 484 | ast::NodeValue::Ident(strings::intern(&mut STRING_POOL, path[i])) |
| 485 | ); |
| 486 | result = ast::synthNode(arena, ast::NodeValue::ScopeAccess(ast::Access { |
| 487 | parent: result, child, |
| 488 | })); |
| 489 | } |
| 490 | return result; |
| 491 | } |
| 492 | |
| 493 | /// Check if a function declaration has the `@test` attribute and return its name if so. |
| 494 | fn getTestFnName(decl: *ast::FnDecl) -> ?*[u8] { |
| 495 | let attrs = decl.attrs else { return nil; }; |
| 496 | if not ast::attributesContains(&attrs, ast::Attribute::Test) { |
| 497 | return nil; |
| 498 | } |
| 499 | let case ast::NodeValue::Ident(name) = decl.name.value |
| 500 | else return nil; |
| 501 | |
| 502 | return name; |
| 503 | } |
| 504 | |
| 505 | /// Scan a single module's AST for `@test` functions and append them to `tests`. |
| 506 | fn collectModuleTests( |
| 507 | entry: *module::ModuleEntry, |
| 508 | tests: *mut [TestDesc], |
| 509 | testCount: *mut u32 |
| 510 | ) { |
| 511 | let modAst = entry.ast else { |
| 512 | return; |
| 513 | }; |
| 514 | let case ast::NodeValue::Block(block) = modAst.value else { |
| 515 | return; |
| 516 | }; |
| 517 | let modPath = module::moduleQualifiedPath(entry); |
| 518 | |
| 519 | for stmt in block.statements { |
| 520 | if let case ast::NodeValue::FnDecl(decl) = stmt.value { |
| 521 | if let fnName = getTestFnName(&decl) { |
| 522 | if *testCount < tests.len { |
| 523 | tests[*testCount] = TestDesc { modPath, fnName }; |
| 524 | *testCount += 1; |
| 525 | } else { |
| 526 | panic "collectModuleTests: too many tests"; |
| 527 | } |
| 528 | } |
| 529 | } |
| 530 | } |
| 531 | } |
| 532 | |
| 533 | /// Synthesize a `testing::test("mod", "name", mod::fn)` call for one test. |
| 534 | fn synthTestCall(arena: *mut ast::NodeArena, desc: *TestDesc) -> *ast::Node { |
| 535 | let callee = synthScopeAccess(arena, &["testing", "test"]); |
| 536 | let modStr = il::formatQualifiedName( |
| 537 | &mut arena.arena, |
| 538 | &desc.modPath[..desc.modPath.len - 1], |
| 539 | desc.modPath[desc.modPath.len - 1] |
| 540 | ); |
| 541 | let modArg = ast::synthNode(arena, ast::NodeValue::String(modStr)); |
| 542 | let nameArg = ast::synthNode(arena, ast::NodeValue::String(desc.fnName)); |
| 543 | |
| 544 | // Intra-package path: skip the package name prefix. |
| 545 | let mut funcPath: [*[u8]; 16] = undefined; |
| 546 | for j in 1..desc.modPath.len { |
| 547 | funcPath[j - 1] = desc.modPath[j]; |
| 548 | } |
| 549 | funcPath[desc.modPath.len - 1] = desc.fnName; |
| 550 | let funcArg = synthScopeAccess(arena, &funcPath[..desc.modPath.len]); |
| 551 | |
| 552 | let a = ast::nodeAllocator(arena); |
| 553 | let mut args = ast::nodeSlice(arena, 3); |
| 554 | args.append(modArg, a); |
| 555 | args.append(nameArg, a); |
| 556 | args.append(funcArg, a); |
| 557 | |
| 558 | return ast::synthNode(arena, ast::NodeValue::Call(ast::Call { callee, args })); |
| 559 | } |
| 560 | |
| 561 | /// Inject a test runner into the entry package's root module. |
| 562 | /// |
| 563 | /// Scans all parsed modules for `@test fn` declarations, then appends |
| 564 | /// a synthetic entry point to the root module's AST block: |
| 565 | /// |
| 566 | /// ``` |
| 567 | /// @default fn #testMain() -> i32 { |
| 568 | /// return testing::runAllTests(&[ |
| 569 | /// testing::test("std::tests", "testFoo", tests::testFoo), |
| 570 | /// ... |
| 571 | /// ]); |
| 572 | /// } |
| 573 | /// ``` |
| 574 | /// |
| 575 | /// Uses `#`-prefixed names to avoid conflicts with user code. |
| 576 | fn generateTestRunner( |
| 577 | ctx: *mut CompileContext, |
| 578 | arena: *mut ast::NodeArena |
| 579 | ) throws (Error) { |
| 580 | let entryPkg = try getEntryPackage(ctx); |
| 581 | let root = try getRootModule(entryPkg, &ctx.graph); |
| 582 | |
| 583 | // Collect all test functions across all modules. |
| 584 | let mut tests: [TestDesc; MAX_TESTS] = undefined; |
| 585 | let mut testCount: u32 = 0; |
| 586 | |
| 587 | for modIdx in 0..ctx.graph.entriesLen { |
| 588 | if let entry = module::get(&ctx.graph, modIdx as u16) { |
| 589 | collectModuleTests(entry, &mut tests[..], &mut testCount); |
| 590 | } |
| 591 | } |
| 592 | if testCount == 0 { |
| 593 | io::printError("radiance: fatal: no test functions found\n"); |
| 594 | throw Error::Other; |
| 595 | } |
| 596 | let mut countBuf: [u8; 10] = undefined; |
| 597 | let countStr = fmt::formatU32(testCount, &mut countBuf[..]); |
| 598 | pkgLog(entryPkg, &["found", countStr, "test(s)"]); |
| 599 | |
| 600 | // Synthesize the `@default` function and append to the root module. |
| 601 | let fnDecl = synthTestMainFn(arena, &tests[..testCount]); |
| 602 | |
| 603 | injectIntoBlock(root.ast, arena, fnDecl); |
| 604 | } |
| 605 | |
| 606 | /// Synthesize the test entry point. |
| 607 | fn synthTestMainFn(arena: *mut ast::NodeArena, tests: *[TestDesc]) -> *ast::Node { |
| 608 | // Build array literal: `[testing::test(...), ...]`. |
| 609 | let a = ast::nodeAllocator(arena); |
| 610 | let mut elements = ast::nodeSlice(arena, tests.len as u32); |
| 611 | for i in 0..tests.len { |
| 612 | elements.append(synthTestCall(arena, &tests[i]), a); |
| 613 | } |
| 614 | let arrayLit = ast::synthNode(arena, ast::NodeValue::ArrayLit(elements)); |
| 615 | |
| 616 | // Build: `&[...]`. |
| 617 | let testsRef = ast::synthNode(arena, ast::NodeValue::AddressOf(ast::AddressOf { |
| 618 | target: arrayLit, mutable: false, |
| 619 | })); |
| 620 | |
| 621 | // Build: `testing::runAllTests(&[...])`. |
| 622 | let runFn = synthScopeAccess(arena, &["testing", "runAllTests"]); |
| 623 | let mut callArgs = ast::nodeSlice(arena, 1); |
| 624 | callArgs.append(testsRef, a); |
| 625 | let callExpr = ast::synthNode(arena, ast::NodeValue::Call(ast::Call { |
| 626 | callee: runFn, args: callArgs, |
| 627 | })); |
| 628 | |
| 629 | // Build: `return testing::runAllTests(&[...]);` |
| 630 | let retStmt = ast::synthNode(arena, ast::NodeValue::Return { value: callExpr }); |
| 631 | let mut bodyStmts = ast::nodeSlice(arena, 1); |
| 632 | bodyStmts.append(retStmt, a); |
| 633 | let fnBody = ast::synthNode(arena, ast::NodeValue::Block(ast::Block { statements: bodyStmts })); |
| 634 | |
| 635 | // Build: `fn #testMain() -> i32` |
| 636 | let fnName = ast::synthNode(arena, ast::NodeValue::Ident(strings::intern(&mut STRING_POOL, "#testMain"))); |
| 637 | let returnType = ast::synthNode(arena, ast::NodeValue::TypeSig(ast::TypeSig::Integer { |
| 638 | width: 4, sign: ast::Signedness::Signed, |
| 639 | })); |
| 640 | let fnSig = ast::FnSig { |
| 641 | params: ast::nodeSlice(arena, 0), |
| 642 | returnType, |
| 643 | throwList: ast::nodeSlice(arena, 0), |
| 644 | }; |
| 645 | |
| 646 | // `@default` attribute. |
| 647 | let attrNode = ast::synthNode(arena, ast::NodeValue::Attribute(ast::Attribute::Default)); |
| 648 | let mut attrList = ast::nodeSlice(arena, 1); |
| 649 | attrList.append(attrNode, a); |
| 650 | let fnAttrs = ast::Attributes { list: attrList }; |
| 651 | |
| 652 | return ast::synthNode(arena, ast::NodeValue::FnDecl(ast::FnDecl { |
| 653 | name: fnName, sig: fnSig, body: fnBody, attrs: fnAttrs, |
| 654 | })); |
| 655 | } |
| 656 | |
| 657 | /// Append a declaration to a block node's statement list. |
| 658 | fn injectIntoBlock( |
| 659 | blockNode: *mut ast::Node, |
| 660 | arena: *mut ast::NodeArena, |
| 661 | decl: *ast::Node |
| 662 | ) { |
| 663 | let case ast::NodeValue::Block(block) = blockNode.value else { |
| 664 | panic "injectIntoBlock: expected Block node"; |
| 665 | }; |
| 666 | let mut stmts = block.statements; |
| 667 | stmts.append(decl, ast::nodeAllocator(arena)); |
| 668 | |
| 669 | blockNode.value = ast::NodeValue::Block(ast::Block { statements: stmts }); |
| 670 | } |
| 671 | |
| 672 | /// Write code buffer to file as raw bytes. |
| 673 | fn writeCode(code: *[u32], path: *[u8]) -> bool { |
| 674 | // Convert `u32` slice to `u8` slice (little-endian). |
| 675 | let byteLen = code.len * 4; |
| 676 | let bytePtr = code.ptr as *u8; |
| 677 | let bytes = @sliceOf(bytePtr, byteLen); |
| 678 | |
| 679 | return unix::writeFile(path, bytes); |
| 680 | } |
| 681 | |
| 682 | /// Write a data section to a file at `basePath` + `ext`. |
| 683 | /// If the data is empty, no file is written. |
| 684 | fn writeDataWithExt( |
| 685 | data: *[u8], |
| 686 | basePath: *[u8], |
| 687 | ext: *[u8] |
| 688 | ) throws (Error) { |
| 689 | if data.len == 0 { |
| 690 | return; |
| 691 | } |
| 692 | let mut path: [u8; MAX_PATH_LEN] = undefined; |
| 693 | let mut pos: u32 = 0; |
| 694 | |
| 695 | pos += try! mem::copy(&mut path[pos..], basePath); |
| 696 | pos += try! mem::copy(&mut path[pos..], ext); |
| 697 | path[pos] = 0; // Null-terminate for syscall. |
| 698 | |
| 699 | if not unix::writeFile(&path[..pos], data) { |
| 700 | io::printError("radiance: fatal: failed to write data file\n"); |
| 701 | throw Error::Other; |
| 702 | } |
| 703 | } |
| 704 | |
| 705 | /// Serialize debug entries and write the `.debug` file. |
| 706 | /// Resolves module IDs to file paths via the module graph. |
| 707 | /// Format per entry is `{pc: u32, offset: u32, filePath: [u8], NULL}`. |
| 708 | fn writeDebugInfo( |
| 709 | entries: *[rv64::emit::DebugEntry], |
| 710 | graph: *module::ModuleGraph, |
| 711 | basePath: *[u8], |
| 712 | arena: *mut alloc::Arena |
| 713 | ) throws (Error) { |
| 714 | if entries.len == 0 { |
| 715 | return; |
| 716 | } |
| 717 | // Use remaining arena space as serialization buffer. |
| 718 | let buf = alloc::remainingBuf(arena); |
| 719 | let mut pos: u32 = 0; |
| 720 | |
| 721 | for i in 0..entries.len { |
| 722 | let entry = &entries[i]; |
| 723 | let modEntry = module::get(graph, entry.moduleId) else { |
| 724 | panic "writeDebugInfo: module not found for debug entry"; |
| 725 | }; |
| 726 | pos += try! mem::copy(&mut buf[pos..], @sliceOf(&entry.pc as *u8, 4)); |
| 727 | pos += try! mem::copy(&mut buf[pos..], @sliceOf(&entry.offset as *u8, 4)); |
| 728 | pos += try! mem::copy(&mut buf[pos..], modEntry.filePath); |
| 729 | |
| 730 | buf[pos] = 0; |
| 731 | pos += 1; |
| 732 | } |
| 733 | try writeDataWithExt(&buf[..pos], basePath, DEBUG_EXT); |
| 734 | } |
| 735 | |
| 736 | /// Run the resolver on the parsed modules. |
| 737 | fn runResolver(ctx: *mut CompileContext, nodeCount: u32) -> resolver::Resolver throws (Error) { |
| 738 | let mut mainArena = alloc::new(&mut MAIN_ARENA[..]); |
| 739 | let entryPkg = try getEntryPackage(ctx); |
| 740 | |
| 741 | pkgLog(entryPkg, &["resolving", ".."]); |
| 742 | |
| 743 | let nodeDataSize = nodeCount * @sizeOf(resolver::NodeData); |
| 744 | let nodeDataPtr = try! alloc::alloc(&mut mainArena, nodeDataSize, @alignOf(resolver::NodeData)); |
| 745 | let nodeData = @sliceOf(nodeDataPtr as *mut resolver::NodeData, nodeCount); |
| 746 | let storage = resolver::ResolverStorage { |
| 747 | arena: mainArena, |
| 748 | nodeData, |
| 749 | pkgScope: &mut RESOLVER_PKG_SCOPE, |
| 750 | errors: &mut RESOLVER_ERRORS[..], |
| 751 | }; |
| 752 | let mut res = resolver::resolver(storage, ctx.config); |
| 753 | |
| 754 | // Build package inputs. |
| 755 | let mut packages: [resolver::Pkg; MAX_PACKAGES] = undefined; |
| 756 | for i in 0..ctx.packageCount { |
| 757 | let pkg = &ctx.packages[i]; |
| 758 | let root = try getRootModule(pkg, &ctx.graph); |
| 759 | |
| 760 | packages[i] = resolver::Pkg { |
| 761 | rootEntry: root.entry, |
| 762 | rootAst: root.ast, |
| 763 | }; |
| 764 | } |
| 765 | |
| 766 | // Resolve all packages. |
| 767 | // TODO: Fix this error printing dance. |
| 768 | let diags = try resolver::resolve(&mut res, &ctx.graph, &packages[..ctx.packageCount]) catch { |
| 769 | let diags = resolver::Diagnostics { errors: res.errors }; |
| 770 | resolver::printer::printDiagnostics(&diags, &res); |
| 771 | throw Error::Other; |
| 772 | }; |
| 773 | if not resolver::success(&diags) { |
| 774 | resolver::printer::printDiagnostics(&diags, &res); |
| 775 | io::print("radiance: failed: "); |
| 776 | io::printU32(diags.errors.len); |
| 777 | io::printLn(" errors"); |
| 778 | throw Error::Other; |
| 779 | } |
| 780 | return res; |
| 781 | } |
| 782 | |
| 783 | /// Lower, optionally dump, and optionally generate binary output. |
| 784 | fn compile(ctx: *mut CompileContext, res: *mut resolver::Resolver) throws (Error) { |
| 785 | let entryPkg = try getEntryPackage(ctx); |
| 786 | |
| 787 | // Lower all packages into a single unified IL program. |
| 788 | // Dependencies must be lowered before the packages that use them. |
| 789 | let program = try lowerAllPackages(ctx, res); |
| 790 | let mut out = sexpr::Output::Stdout; |
| 791 | |
| 792 | if ctx.dump == Dump::Il { |
| 793 | il::printer::printProgram(&mut out, &mut res.arena, &program); |
| 794 | io::print("\n"); |
| 795 | return; |
| 796 | } |
| 797 | if ctx.dump == Dump::Asm { |
| 798 | // TODO: Why do we generate code in two places? |
| 799 | let result = rv64::generate(&program, rv64::Storage { dataSyms: &mut CODEGEN_DATA_SYMS[..] }, &mut RO_DATA_BUF[..], &mut RW_DATA_BUF[..], &mut res.arena, false); |
| 800 | printer::printCodeTo(&mut out, entryPkg.name, result.code, result.funcs, &mut res.arena); |
| 801 | io::print("\n"); |
| 802 | return; |
| 803 | } |
| 804 | // Generate binary output if path specified. |
| 805 | let outPath = ctx.outputPath else { |
| 806 | return; |
| 807 | }; |
| 808 | // Check that we have an entry point. |
| 809 | if program.defaultFnIdx == nil { |
| 810 | io::printError("radiance: fatal: no default function found\n"); |
| 811 | throw Error::Other; |
| 812 | } |
| 813 | pkgLog(entryPkg, &["generating code", "(", outPath, ")", ".."]); |
| 814 | |
| 815 | let storage = rv64::Storage { dataSyms: &mut CODEGEN_DATA_SYMS[..] }; |
| 816 | let result = rv64::generate(&program, storage, &mut RO_DATA_BUF[..], &mut RW_DATA_BUF[..], &mut res.arena, ctx.debug); |
| 817 | if not writeCode(result.code, outPath) { |
| 818 | io::printError("radiance: fatal: failed to write output file\n"); |
| 819 | throw Error::Other; |
| 820 | } |
| 821 | // Write data files. |
| 822 | try writeDataWithExt(&RO_DATA_BUF[..result.roDataSize], outPath, RO_DATA_EXT); |
| 823 | try writeDataWithExt(&RW_DATA_BUF[..result.rwDataSize], outPath, RW_DATA_EXT); |
| 824 | |
| 825 | // Write debug info file if enabled. |
| 826 | if ctx.debug { |
| 827 | try writeDebugInfo(result.debugEntries, &ctx.graph, outPath, &mut res.arena); |
| 828 | } |
| 829 | pkgLog(entryPkg, &["ok", "(", outPath, ")"]); |
| 830 | } |
| 831 | |
| 832 | @default fn main(env: *sys::Env) -> i32 { |
| 833 | let mut arena = ast::nodeArena(&mut TEMP_ARENA[..]); |
| 834 | let mut ctx = try processCommand(env.args, &mut arena) catch { |
| 835 | return 1; |
| 836 | }; |
| 837 | match ctx.dump { |
| 838 | case Dump::Ast => { |
| 839 | try dumpAst(&ctx) catch { |
| 840 | return 1; |
| 841 | }; |
| 842 | return 0; |
| 843 | } |
| 844 | case Dump::Graph => { |
| 845 | dumpGraph(&ctx); |
| 846 | return 0; |
| 847 | } |
| 848 | else => {} |
| 849 | } |
| 850 | // Generate test runner if in test mode. |
| 851 | if ctx.config.buildTest { |
| 852 | try generateTestRunner(&mut ctx, &mut arena) catch { |
| 853 | return 1; |
| 854 | }; |
| 855 | } |
| 856 | // Run resolution phase. |
| 857 | let mut res = try runResolver(&mut ctx, arena.nextId) catch { |
| 858 | return 1; |
| 859 | }; |
| 860 | // Lower, dump, and/or generate output. |
| 861 | try compile(&mut ctx, &mut res) catch { |
| 862 | return 1; |
| 863 | }; |
| 864 | return 0; |
| 865 | } |