compiler/radiance.rad 36.8 KiB 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::asm;
17
use std::arch::rv64::printer;
18
use std::lang::sexpr;
19
use std::lang::gen::data;
20
use std::lang::gen::types;
21
use std::sys;
22
use std::sys::unix;
23
use std::collections::dict;
24
25
/// Maximum number of modules we can load per package.
26
constant MAX_LOADED_MODULES: u32 = 64;
27
/// Maximum number of packages we can compile.
28
constant MAX_PACKAGES: u32 = 4;
29
/// Total module entries across all packages.
30
constant MAX_TOTAL_MODULES: u32 = 192;
31
/// Source code buffer arena (2 MB).
32
constant MAX_SOURCES_SIZE: u32 = 2097152;
33
/// Maximum number of test functions we can discover.
34
constant MAX_TESTS: u32 = 1024;
35
/// Maximum number of assembly source paths we can load per package.
36
constant MAX_ASM_MODULES: u32 = 64;
37
38
/// AST arena size (32 MB) - retains parsed nodes throughout compilation.
39
constant TEMP_ARENA_SIZE: u32 = 33554432;
40
/// Per-function lowering and register-allocation arena size (16 MB).
41
constant FN_ARENA_SIZE: u32 = 16777216;
42
/// Main arena size (96 MB) - lives throughout compilation.
43
/// Used for: resolver data, types, symbols, global IL data, and codegen output.
44
constant MAIN_ARENA_SIZE: u32 = 100663296;
45
46
/// AST storage arena.
47
unsafe static TEMP_ARENA: [u8; TEMP_ARENA_SIZE] = undefined;
48
/// Scratch storage reclaimed after each generated function.
49
unsafe static FN_ARENA: [u8; FN_ARENA_SIZE] = undefined;
50
/// Main storage arena - persists throughout compilation.
51
unsafe static MAIN_ARENA: [u8; MAIN_ARENA_SIZE] = undefined;
52
53
/// Module source code.
54
unsafe static MODULE_SOURCES: [u8; MAX_SOURCES_SIZE] = undefined;
55
/// Module entries for all packages.
56
unsafe static MODULE_ENTRIES: [module::ModuleEntry; MAX_TOTAL_MODULES] = undefined;
57
/// String pool.
58
unsafe static STRING_POOL: strings::Pool = strings::Pool { table: undefined, count: 0 };
59
60
/// Package scope.
61
unsafe static RESOLVER_PKG_SCOPE: resolver::Scope = undefined;
62
/// Errors emitted by resolver.
63
unsafe static RESOLVER_ERRORS: [resolver::Error; resolver::MAX_ERRORS] = undefined;
64
65
/// Code generation storage.
66
unsafe static CODEGEN_DATA_SYMS: [data::DataSym; data::MAX_DATA_SYMS] = undefined;
67
/// Hash table entries for data symbol lookup.
68
unsafe static CODEGEN_DATA_SYM_ENTRIES: [dict::Entry; data::DATA_SYM_TABLE_SIZE] = undefined;
69
70
/// Debug info file extension.
71
unsafe constant DEBUG_EXT: *[u8] = ".debug";
72
73
/// Maximum rodata size (4MB).
74
constant MAX_RO_DATA_SIZE: u32 = 4194304;
75
/// Maximum rwdata size (4MB).
76
constant MAX_RW_DATA_SIZE: u32 = 4194304;
77
/// Maximum path length.
78
constant MAX_PATH_LEN: u32 = 256;
79
/// Read-only data buffer.
80
unsafe static RO_DATA_BUF: [u8; MAX_RO_DATA_SIZE] = undefined;
81
/// Read-write data buffer.
82
unsafe static RW_DATA_BUF: [u8; MAX_RW_DATA_SIZE] = undefined;
83
/// Assembly module source buffer.
84
unsafe static ASM_SOURCE_BUF: [u8; MAX_SOURCES_SIZE] = undefined;
85
/// Temporary assembly text buffer.
86
unsafe static ASM_TEXT_BUF: [u32; 262144] = undefined;
87
/// Temporary assembly data buffer.
88
unsafe static ASM_DATA_BUF: [u8; MAX_RO_DATA_SIZE] = undefined;
89
/// Accumulated assembly read-only data.
90
unsafe static ASM_RO_DATA_BUF: [u8; MAX_RO_DATA_SIZE] = undefined;
91
92
/// Assembly source file extension.
93
unsafe constant ASM_SOURCE_EXT: *[u8] = ".ras";
94
/// Symbol name exported for startup code to call the semantic entry function.
95
unsafe constant DEFAULT_ENTRY_SYMBOL: *[u8] = "::default";
96
97
/// Usage string.
98
unsafe constant USAGE: *[u8] =
99
    "usage: radiance -pkg <name> [-start <input.ras>] -mod <input>.. [-pkg <name> -mod <input>..] -entry <pkg> -o <output>\n";
100
101
/// Compiler error.
102
union Error {
103
    Other,
104
}
105
106
/// What to dump during compilation.
107
union Dump {
108
    /// Don't dump anything.
109
    None,
110
    /// Dump the parsed AST before semantic analysis.
111
    Ast,
112
    /// Dump the module graph.
113
    Graph,
114
    /// Dump the IL.
115
    Il,
116
    /// Dump the generated assembly.
117
    Asm,
118
}
119
120
/// A discovered test function.
121
record TestDesc {
122
    /// Full module qualified path segments (eg. ["std", "tests"]).
123
    modPath: *[*[u8]],
124
    /// Test function name (eg. "testFoo").
125
    fnName: *[u8],
126
}
127
128
/// Source inputs belonging to one command-line package.
129
record PackageInput {
130
    /// Package name from the `-pkg` argument.
131
    name: *[u8],
132
    /// Optional startup assembly emitted before generated text.
133
    startupPath: ?*[u8],
134
    /// Radiance source paths for this package.
135
    radPaths: [*[u8]; MAX_LOADED_MODULES],
136
    /// Number of Radiance source paths.
137
    radPathCount: u32,
138
    /// Assembly source paths for this package.
139
    asmPaths: [*[u8]; MAX_ASM_MODULES],
140
    /// Number of assembly source paths.
141
    asmPathCount: u32,
142
}
143
144
/// Compilation context.
145
record CompileContext {
146
    /// Array of packages to compile.
147
    packages: [package::Package; MAX_PACKAGES],
148
    /// Driver inputs for each package slot.
149
    inputs: [PackageInput; MAX_PACKAGES],
150
    /// Number of packages.
151
    packageCount: u32,
152
    /// Index of entry package.
153
    entryPkgIdx: ?u32,
154
    /// Global module graph shared by all packages.
155
    graph: module::ModuleGraph,
156
    /// Resolver configuration.
157
    config: resolver::Config,
158
    /// What to dump during compilation.
159
    dump: Dump,
160
    /// Output path for binary.
161
    outputPath: ?*[u8],
162
    /// Whether to emit debug info (.debug file).
163
    debug: bool,
164
}
165
166
/// Root module info for a package.
167
record RootModule {
168
    entry: *module::ModuleEntry,
169
    ast: *mut ast::Node,
170
}
171
172
/// Trusted implementation for streamed code generation.
173
unsafe mod codegenSink;
174
175
/// Entry handling for streamed code generation.
176
union CodegenEntryMode {
177
    /// Do not reserve an entry jump.
178
    None,
179
    /// Reserve and patch an entry jump to the `@default` function.
180
    DefaultEntry,
181
}
182
183
/// Options controlling streamed lowering and code generation.
184
record CodegenOptions {
185
    /// Optional output path used for progress logging.
186
    logPath: ?*[u8],
187
    /// Whether to emit debug source locations.
188
    debug: bool,
189
    /// How the generated program should handle entry.
190
    entryMode: CodegenEntryMode,
191
}
192
193
/// Print a driver error line.
194
unsafe fn error(msg: *[*[u8]]) -> Error {
195
    io::printError("radiance: ");
196
197
    for part, i in msg {
198
        io::printError(part);
199
        if i < msg.len - 1 {
200
            io::printError(" ");
201
        }
202
    }
203
    io::printError("\n");
204
    return Error::Other;
205
}
206
207
/// Print a log line for the given package.
208
unsafe fn pkgLog(pkg: *package::Package, msg: *[*[u8]]) {
209
    io::printError("radiance: ");
210
    io::printError(pkg.name);
211
    io::printError(": ");
212
213
    for part, i in msg {
214
        io::printError(part);
215
        if i < msg.len - 1 {
216
            io::printError(" ");
217
        }
218
    }
219
    io::printError("\n");
220
}
221
222
/// Return `true` when `path` ends with `ext`.
223
unsafe fn hasExtension(path: *[u8], ext: *[u8]) -> bool {
224
    if path.len < ext.len {
225
        return false;
226
    }
227
    let start = path.len - ext.len;
228
    return mem::eq(&path[start..], ext);
229
}
230
231
/// Create an empty source input set for one package.
232
unsafe fn packageInput(name: *[u8]) -> PackageInput {
233
    return PackageInput {
234
        name,
235
        startupPath: nil,
236
        radPaths: undefined,
237
        radPathCount: 0,
238
        asmPaths: undefined,
239
        asmPathCount: 0,
240
    };
241
}
242
243
/// Register, load, and parse `path` within `pkg`.
244
unsafe fn processModule(
245
    pkg: *mut package::Package,
246
    graph: *mut module::ModuleGraph,
247
    path: *[u8],
248
    nodeArena: *mut ast::NodeArena,
249
    sourceArena: *mut alloc::Arena
250
) throws (Error) {
251
    pkgLog(pkg, &["parsing", "(", path, ")", ".."]);
252
253
    let moduleId = try package::registerModule(pkg, graph, path) catch {
254
        throw error(&["error registering module"]);
255
    };
256
    // Read file into remaining arena space.
257
    let buffer = alloc::remainingBuf(sourceArena);
258
    if buffer.len == 0 {
259
        throw error(&["fatal:", "source arena exhausted"]);
260
    }
261
    let source = unix::readFile(path, buffer) else {
262
        throw error(&["error reading file"]);
263
    };
264
    if source.len == buffer.len {
265
        throw error(&["fatal:", "source arena too small, file truncated:", path]);
266
    }
267
    // Commit only what was read.
268
    alloc::commit(sourceArena, source.len);
269
270
    let ast = try parser::parse(scanner::SourceLoc::File(path), source, nodeArena, &mut STRING_POOL) catch {
271
        throw Error::Other;
272
    };
273
    try module::setAst(graph, moduleId, ast) catch {
274
        throw error(&["error setting AST"]);
275
    };
276
    try module::setSource(graph, moduleId, source) catch {
277
        throw error(&["error setting source"]);
278
    };
279
}
280
281
/// Consume the next argument, or print an error and throw.
282
unsafe fn nextArg(args: *[*[u8]], idx: *mut u32, msg: *[*[u8]]) -> *[u8] throws (Error) {
283
    set *idx += 1;
284
    if *idx >= args.len {
285
        throw error(msg);
286
    }
287
    return args[*idx];
288
}
289
290
/// Parse CLI arguments and return compilation context.
291
unsafe fn processCommand(
292
    args: *[*[u8]],
293
    arena: *mut ast::NodeArena
294
) -> CompileContext throws (Error) {
295
    let mut buildTest = false;
296
    let mut debugEnabled = false;
297
    let mut outputPath: ?*[u8] = nil;
298
    let mut dump = Dump::None;
299
    let mut entryPkgName: ?*[u8] = nil;
300
301
    // Per-package source path tracking.
302
    let mut inputs: [PackageInput; MAX_PACKAGES] = undefined;
303
    let mut pkgCount: u32 = 0;
304
    let mut currentPkgIdx: ?u32 = nil;
305
306
    if args.len == 0 {
307
        io::printError(USAGE);
308
        throw Error::Other;
309
    }
310
    let mut idx: u32 = 0;
311
312
    while idx < args.len {
313
        let arg = args[idx];
314
        if mem::eq(arg, "-pkg") {
315
            try nextArg(args, &mut idx, &["`-pkg` requires a package name"]);
316
            if pkgCount >= MAX_PACKAGES {
317
                throw error(&["too many packages specified"]);
318
            }
319
            set inputs[pkgCount] = packageInput(args[idx]);
320
            set currentPkgIdx = pkgCount;
321
            set pkgCount += 1;
322
        } else if mem::eq(arg, "-mod") {
323
            try nextArg(args, &mut idx, &["`-mod` requires a module path"]);
324
            let pkgIdx = currentPkgIdx else {
325
                throw error(&["`-mod` must follow a `-pkg` argument"]);
326
            };
327
            let input = &mut inputs[pkgIdx];
328
            if hasExtension(args[idx], ASM_SOURCE_EXT) {
329
                if input.asmPathCount >= MAX_ASM_MODULES {
330
                    throw error(&["too many assembly modules specified"]);
331
                }
332
                set input.asmPaths[input.asmPathCount] = args[idx];
333
                set input.asmPathCount += 1;
334
            } else {
335
                if input.radPathCount >= MAX_LOADED_MODULES {
336
                    throw error(&["too many modules specified for package"]);
337
                }
338
                set input.radPaths[input.radPathCount] = args[idx];
339
                set input.radPathCount += 1;
340
            }
341
        } else if mem::eq(arg, "-start") {
342
            try nextArg(args, &mut idx, &["`-start` requires an assembly path"]);
343
            let pkgIdx = currentPkgIdx else {
344
                throw error(&["`-start` must follow a `-pkg` argument"]);
345
            };
346
            let input = &mut inputs[pkgIdx];
347
            if input.startupPath <> nil {
348
                throw error(&["package", input.name, "has more than one startup file"]);
349
            }
350
            if not hasExtension(args[idx], ASM_SOURCE_EXT) {
351
                throw error(&["`-start` requires a `.ras` assembly file"]);
352
            }
353
            set input.startupPath = args[idx];
354
        } else if mem::eq(arg, "-entry") {
355
            try nextArg(args, &mut idx, &["`-entry` requires a package name"]);
356
            set entryPkgName = args[idx];
357
        } else if mem::eq(arg, "-test") {
358
            set buildTest = true;
359
        } else if mem::eq(arg, "-debug") {
360
            set debugEnabled = true;
361
        } else if mem::eq(arg, "-o") {
362
            try nextArg(args, &mut idx, &["`-o` requires an output path"]);
363
            set outputPath = args[idx];
364
        } else if mem::eq(arg, "-dump") {
365
            try nextArg(args, &mut idx, &["`-dump` requires a mode (eg. ast)"]);
366
            let mode = args[idx];
367
            if mem::eq(mode, "ast") {
368
                set dump = Dump::Ast;
369
            } else if mem::eq(mode, "graph") {
370
                set dump = Dump::Graph;
371
            } else if mem::eq(mode, "il") {
372
                set dump = Dump::Il;
373
            } else if mem::eq(mode, "asm") {
374
                set dump = Dump::Asm;
375
            } else {
376
                throw error(&["unknown dump mode", mode, "(expected: ast, graph, il, asm)"]);
377
            }
378
        } else {
379
            throw error(&["unknown argument", arg]);
380
        }
381
        set idx += 1;
382
    }
383
    if pkgCount == 0 {
384
        throw error(&["no package specified"]);
385
    }
386
    for i in 0..pkgCount {
387
        if inputs[i].radPathCount == 0 {
388
            throw error(&["package", inputs[i].name, "has no Radiance modules specified"]);
389
        }
390
    }
391
392
    // Determine entry package index.
393
    let mut entryPkgIdx: ?u32 = nil;
394
    if pkgCount == 1 {
395
        // Single package: it is the entry.
396
        set entryPkgIdx = 0;
397
    } else {
398
        // Multiple packages: need -entry.
399
        let entryName = entryPkgName else {
400
            throw error(&["`-entry` required when multiple packages specified"]);
401
        };
402
        for i in 0..pkgCount {
403
            if mem::eq(inputs[i].name, entryName) {
404
                set entryPkgIdx = i;
405
                break;
406
            }
407
        }
408
        if entryPkgIdx == nil {
409
            throw error(&["fatal:", "entry package", entryName, "not found"]);
410
        }
411
    }
412
    let entryIdx = entryPkgIdx else {
413
        panic "processCommand: no entry package";
414
    };
415
    for i in 0..pkgCount {
416
        if i <> entryIdx and inputs[i].startupPath <> nil {
417
            throw error(&["`-start` is only supported on the entry package"]);
418
        }
419
    }
420
    let graph = module::moduleGraph(&mut MODULE_ENTRIES[..], &mut STRING_POOL, arena);
421
    let mut ctx = CompileContext {
422
        packages: undefined,
423
        inputs,
424
        packageCount: pkgCount,
425
        entryPkgIdx,
426
        graph,
427
        config: resolver::Config { buildTest },
428
        dump,
429
        outputPath,
430
        debug: debugEnabled,
431
    };
432
    // Initialize and parse all packages.
433
    let mut sourceArena = alloc::new(&mut MODULE_SOURCES[..]);
434
    for i in 0..pkgCount {
435
        package::init(&mut ctx.packages[i], i as u16, ctx.inputs[i].name, &mut STRING_POOL);
436
437
        for j in 0..ctx.inputs[i].radPathCount {
438
            let path = ctx.inputs[i].radPaths[j];
439
            try processModule(&mut ctx.packages[i], &mut ctx.graph, path, arena, &mut sourceArena);
440
        }
441
    }
442
    return ctx;
443
}
444
445
/// Get the entry package from the context.
446
unsafe fn getEntryPackage(ctx: *CompileContext) -> *package::Package throws (Error) {
447
    let entryIdx = ctx.entryPkgIdx else {
448
        throw error(&["no entry package specified"]);
449
    };
450
    return &ctx.packages[entryIdx];
451
}
452
453
/// Return the startup assembly path for the entry package, if one was supplied.
454
unsafe fn getEntryStartupPath(ctx: *CompileContext) -> ?*[u8] {
455
    let entryIdx = ctx.entryPkgIdx else {
456
        panic "getEntryStartupPath: no entry package";
457
    };
458
    return ctx.inputs[entryIdx].startupPath;
459
}
460
461
/// Get root module info from a package.
462
unsafe fn getRootModule(pkg: *package::Package, graph: *module::ModuleGraph) -> RootModule throws (Error) {
463
    let rootId = pkg.rootModuleId else {
464
        throw error(&["no root module found"]);
465
    };
466
    let rootEntry = module::get(graph, rootId) else {
467
        throw error(&["root module entry not found"]);
468
    };
469
    let rootAst = rootEntry.ast else {
470
        throw error(&["root module has no AST"]);
471
    };
472
    return RootModule { entry: rootEntry, ast: rootAst };
473
}
474
475
/// Dump the module graph.
476
unsafe fn dumpGraph(ctx: *CompileContext) {
477
    let mut arena = alloc::new(&mut MAIN_ARENA[..]);
478
    module::printer::printGraph(&ctx.graph, &mut arena);
479
}
480
481
/// Dump the parsed AST.
482
unsafe fn dumpAst(ctx: *CompileContext) throws (Error) {
483
    let pkg = try getEntryPackage(ctx);
484
    let root = try getRootModule(pkg, &ctx.graph);
485
    let mut arena = alloc::new(&mut MAIN_ARENA[..]);
486
487
    ast::printer::printTree(root.ast, &mut arena);
488
}
489
490
/// Lower all packages into a single IL program.
491
/// Dependencies are lowered first, then the entry package.
492
unsafe fn lowerAllPackages(
493
    ctx: *mut CompileContext,
494
    res: *mut resolver::Resolver
495
) -> il::Program throws (Error) {
496
    let entryIdx = ctx.entryPkgIdx else {
497
        panic "lowerAllPackages: no entry package";
498
    };
499
    let entryPkg = &ctx.packages[entryIdx];
500
501
    // Create the lowerer accumulator using entry package's name.
502
    let options = lower::LowerOptions { debug: ctx.debug, buildTest: ctx.config.buildTest };
503
    let mut low = lower::lowerer(
504
        res, &ctx.graph, entryPkg.name, &mut res.arena, &mut res.arena, options
505
    );
506
    try lowerAllPackagesInto(ctx, res, &mut low);
507
508
    // Finalize and return the unified program.
509
    return lower::finalize(&low);
510
}
511
512
/// Lower all packages into an existing lowerer.
513
unsafe fn lowerAllPackagesInto(
514
    ctx: *mut CompileContext,
515
    res: *mut resolver::Resolver,
516
    low: *mut lower::Lowerer
517
) throws (Error) {
518
    let entryIdx = ctx.entryPkgIdx else {
519
        panic "lowerAllPackagesInto: no entry package";
520
    };
521
    // Lower all packages except entry.
522
    for i in 0..ctx.packageCount {
523
        if i <> entryIdx {
524
            try lowerPackage(ctx, res, low, &mut ctx.packages[i], false);
525
        }
526
    }
527
    // Lower entry package.
528
    try lowerPackage(ctx, res, low, &mut ctx.packages[entryIdx], true);
529
}
530
531
/// Lower all modules in a package into the lowerer accumulator.
532
unsafe fn lowerPackage(
533
    ctx: *CompileContext,
534
    res: *mut resolver::Resolver,
535
    low: *mut lower::Lowerer,
536
    pkg: *mut package::Package,
537
    isEntry: bool
538
) throws (Error) {
539
    let rootId = pkg.rootModuleId else {
540
        throw error(&["no root module found"]);
541
    };
542
    // Set lowerer's package context for qualified name generation.
543
    // TODO: We shouldn't have to call this manually.
544
    lower::setPackage(low, &ctx.graph, pkg.name);
545
546
    try lowerModuleTreeInto(ctx, low, &ctx.graph, rootId, isEntry, pkg);
547
}
548
549
/// Recursively lower a module and all its children into the accumulator.
550
unsafe fn lowerModuleTreeInto(
551
    ctx: *CompileContext,
552
    low: *mut lower::Lowerer,
553
    graph: *module::ModuleGraph,
554
    modId: u16,
555
    isRoot: bool,
556
    pkg: *package::Package
557
) throws (Error) {
558
    let entry = module::get(graph, modId) else {
559
        throw error(&["module entry not found"]);
560
    };
561
    let modAst = entry.ast else {
562
        throw error(&["module has no AST"]);
563
    };
564
    pkgLog(pkg, &["lowering", "(", entry.filePath, ")", ".."]);
565
566
    try lower::lowerModule(low, modId, modAst, isRoot) catch err {
567
        io::printError("radiance: ");
568
        io::printError("internal error during lowering: ");
569
        lower::printError(err);
570
        io::printError("\n");
571
572
        throw Error::Other;
573
    };
574
    // Recurse into children.
575
    for i in 0..entry.childrenLen {
576
        let childId = module::childAt(entry, i);
577
        try lowerModuleTreeInto(ctx, low, graph, childId, false, pkg);
578
    }
579
}
580
581
/// Build a scope access chain: a::b::c from a slice of identifiers.
582
unsafe fn synthScopeAccess(arena: *mut ast::NodeArena, path: *[*[u8]]) -> *ast::Node {
583
    let mut result = ast::synthNode(
584
        arena,
585
        ast::NodeValue::Ident(strings::intern(&mut STRING_POOL, path[0]))
586
    );
587
    for i in 1..path.len {
588
        let child = ast::synthNode(
589
            arena,
590
            ast::NodeValue::Ident(strings::intern(&mut STRING_POOL, path[i]))
591
        );
592
        set result = ast::synthNode(arena, ast::NodeValue::ScopeAccess(ast::Access {
593
            parent: result, child,
594
        }));
595
    }
596
    return result;
597
}
598
599
/// Check if a function declaration has the `@test` attribute and return its name if so.
600
unsafe fn getTestFnName(decl: *ast::FnDecl) -> ?*[u8] {
601
    let attrs = decl.attrs else { return nil; };
602
    if not ast::attributesContains(&attrs, ast::Attribute::Test) {
603
        return nil;
604
    }
605
    let case ast::NodeValue::Ident(name) = decl.name.value
606
        else return nil;
607
608
    return name;
609
}
610
611
/// Scan a single module's AST for `@test` functions and append them to `tests`.
612
unsafe fn collectModuleTests(
613
    entry: *module::ModuleEntry,
614
    tests: *mut [TestDesc],
615
    testCount: *mut u32
616
) {
617
    let modAst = entry.ast else {
618
        return;
619
    };
620
    let case ast::NodeValue::Block(block) = modAst.value else {
621
        return;
622
    };
623
    let modPath = module::moduleQualifiedPath(entry);
624
625
    for stmt in block.statements {
626
        if let case ast::NodeValue::FnDecl(decl) = stmt.value {
627
            if let fnName = getTestFnName(&decl) {
628
                if *testCount < tests.len {
629
                    set tests[*testCount] = TestDesc { modPath, fnName };
630
                    set *testCount += 1;
631
                } else {
632
                    panic "collectModuleTests: too many tests";
633
                }
634
            }
635
        }
636
    }
637
}
638
639
/// Synthesize a `testing::test("mod", "name", mod::fn)` call for one test.
640
unsafe fn synthTestCall(arena: *mut ast::NodeArena, desc: *TestDesc) -> *ast::Node {
641
    let callee = synthScopeAccess(arena, &["testing", "test"]);
642
    let modStr = il::formatQualifiedName(
643
        &mut arena.arena,
644
        &desc.modPath[..desc.modPath.len - 1],
645
        desc.modPath[desc.modPath.len - 1]
646
    );
647
    let modArg = ast::synthNode(arena, ast::NodeValue::String(modStr));
648
    let nameArg = ast::synthNode(arena, ast::NodeValue::String(desc.fnName));
649
650
    // Intra-package path: skip the package name prefix.
651
    let mut funcPath: [*[u8]; 16] = undefined;
652
    for j in 1..desc.modPath.len {
653
        set funcPath[j - 1] = desc.modPath[j];
654
    }
655
    set funcPath[desc.modPath.len - 1] = desc.fnName;
656
    let funcArg = synthScopeAccess(arena, &funcPath[..desc.modPath.len]);
657
658
    let a = alloc::arenaAllocator(&mut arena.arena);
659
    let args = ast::nodeSlice(arena, 3)
660
        .append(modArg, a)
661
        .append(nameArg, a)
662
        .append(funcArg, a);
663
664
    return ast::synthNode(arena, ast::NodeValue::Call(ast::Call { callee, args }));
665
}
666
667
/// Inject a test runner into the entry package's root module.
668
///
669
/// Scans all parsed modules for `@test fn` declarations, then appends
670
/// a synthetic entry point to the root module's AST block:
671
///
672
/// ```
673
/// @default fn #testMain() -> i32 {
674
///     return testing::runAllTests(&[
675
///         testing::test("std::tests", "testFoo", tests::testFoo),
676
///         ...
677
///     ]);
678
/// }
679
/// ```
680
///
681
/// Uses `#`-prefixed names to avoid conflicts with user code.
682
unsafe fn generateTestRunner(
683
    ctx: *mut CompileContext,
684
    arena: *mut ast::NodeArena
685
) throws (Error) {
686
    let entryPkg = try getEntryPackage(ctx);
687
    let root = try getRootModule(entryPkg, &ctx.graph);
688
689
    // Collect all test functions across all modules.
690
    let mut tests: [TestDesc; MAX_TESTS] = undefined;
691
    let mut testCount: u32 = 0;
692
693
    for modIdx in 0..ctx.graph.entriesLen {
694
        if let entry = module::get(&ctx.graph, modIdx as u16) {
695
            collectModuleTests(entry, &mut tests[..], &mut testCount);
696
        }
697
    }
698
    if testCount == 0 {
699
        throw error(&["fatal:", "no test functions found"]);
700
    }
701
    let mut countBuf: [u8; 10] = undefined;
702
    let countStr = fmt::formatU32(testCount, &mut countBuf[..]);
703
    pkgLog(entryPkg, &["found", countStr, "test(s)"]);
704
705
    // Synthesize the `@default` function and append to the root module.
706
    let fnDecl = synthTestMainFn(arena, &tests[..testCount]);
707
708
    injectIntoBlock(root.ast, arena, fnDecl);
709
}
710
711
/// Synthesize the test entry point.
712
unsafe fn synthTestMainFn(arena: *mut ast::NodeArena, tests: *[TestDesc]) -> *ast::Node {
713
    // Build array literal: `[testing::test(...), ...]`.
714
    let a = alloc::arenaAllocator(&mut arena.arena);
715
    let mut elements = ast::nodeSlice(arena, tests.len as u32);
716
    for i in 0..tests.len {
717
        elements.append(synthTestCall(arena, &tests[i]), a);
718
    }
719
    let arrayLit = ast::synthNode(arena, ast::NodeValue::ArrayLit(elements));
720
721
    // Build: `&[...]`.
722
    let testsRef = ast::synthNode(arena, ast::NodeValue::AddressOf(ast::AddressOf {
723
        target: arrayLit, mutable: false,
724
    }));
725
726
    // Build: `testing::runAllTests(&[...])`.
727
    let runFn = synthScopeAccess(arena, &["testing", "runAllTests"]);
728
    let callArgs = ast::nodeSlice(arena, 1).append(testsRef, a);
729
    let callExpr = ast::synthNode(arena, ast::NodeValue::Call(ast::Call {
730
        callee: runFn, args: callArgs,
731
    }));
732
733
    // Build: `return testing::runAllTests(&[...]);`
734
    let retStmt = ast::synthNode(arena, ast::NodeValue::Return { value: callExpr });
735
    let bodyStmts = ast::nodeSlice(arena, 1).append(retStmt, a);
736
    let fnBody = ast::synthNode(arena, ast::NodeValue::Block(ast::Block { statements: bodyStmts }));
737
738
    // Build: `fn #testMain() -> i32`
739
    let fnName = ast::synthNode(arena, ast::NodeValue::Ident(strings::intern(&mut STRING_POOL, "#testMain")));
740
    let returnType = ast::synthNode(arena, ast::NodeValue::TypeSig(ast::TypeSig::Integer {
741
        width: 4, sign: ast::Signedness::Signed,
742
    }));
743
    let fnSig = ast::FnSig {
744
        params: ast::nodeSlice(arena, 0),
745
        returnType,
746
        throwList: ast::nodeSlice(arena, 0),
747
    };
748
749
    // `@default` attribute.
750
    let attrNode = ast::synthNode(arena, ast::NodeValue::Attribute(ast::Attribute::Default));
751
    let attrList = ast::nodeSlice(arena, 1).append(attrNode, a);
752
    let fnAttrs = ast::Attributes { list: attrList };
753
754
    return ast::synthNode(arena, ast::NodeValue::FnDecl(ast::FnDecl {
755
        name: fnName, sig: fnSig, body: fnBody, attrs: fnAttrs,
756
    }));
757
}
758
759
/// Append a declaration to a block node's statement list.
760
unsafe fn injectIntoBlock(
761
    blockNode: *mut ast::Node,
762
    arena: *mut ast::NodeArena,
763
    decl: *ast::Node
764
) {
765
    let case ast::NodeValue::Block(block) = blockNode.value else {
766
        panic "injectIntoBlock: expected Block node";
767
    };
768
    let stmts = block.statements.append(decl, alloc::arenaAllocator(&mut arena.arena));
769
    set blockNode.value = ast::NodeValue::Block(ast::Block { statements: stmts });
770
}
771
772
/// Write a self-contained RV64 image containing text and data sections.
773
unsafe fn writeImage(
774
    code: *[u32],
775
    roData: *[u8],
776
    rwData: *[u8],
777
    path: *[u8]
778
) -> bool {
779
    let mut header = rv64::imageHeader(code.len * rv64::INSTR_SIZE as u32, roData.len, rwData.len);
780
    let headerWords = &header[..];
781
    let headerBytes = @sliceOf(headerWords.ptr as *u8, headerWords.len * rv64::WORD_SIZE as u32);
782
    let codeBytes = @sliceOf(code.ptr as *u8, code.len * rv64::INSTR_SIZE as u32);
783
784
    return unix::writeFileParts(path, &[headerBytes, codeBytes, roData, rwData]);
785
}
786
787
/// Write a data section to a file at `basePath` + `ext`.
788
/// Empty data truncates any stale sidecar left by an earlier build.
789
unsafe fn writeDataWithExt(
790
    data: *[u8],
791
    basePath: *[u8],
792
    ext: *[u8]
793
) throws (Error) {
794
    let mut path: [u8; MAX_PATH_LEN] = undefined;
795
    let mut pos: u32 = 0;
796
797
    set pos += try! mem::copy(&mut path[pos..], basePath);
798
    set pos += try! mem::copy(&mut path[pos..], ext);
799
    set path[pos] = 0; // Null-terminate for syscall.
800
801
    if not unix::writeFile(&path[..pos], data) {
802
        throw error(&["fatal:", "failed to write data file"]);
803
    }
804
}
805
806
/// Serialize debug entries and write the `.debug` file.
807
/// Resolves module IDs to file paths via the module graph.
808
/// Format per entry is `{pc: u32,  offset: u32, filePath: [u8], NULL}`.
809
unsafe fn writeDebugInfo(
810
    entries: *[types::DebugEntry],
811
    graph: *module::ModuleGraph,
812
    basePath: *[u8],
813
    arena: *mut alloc::Arena
814
) throws (Error) {
815
    if entries.len == 0 {
816
        return;
817
    }
818
    // Use remaining arena space as serialization buffer.
819
    let buf = alloc::remainingBuf(arena);
820
    let mut pos: u32 = 0;
821
822
    for i in 0..entries.len {
823
        let entry = &entries[i];
824
        let modEntry = module::get(graph, entry.moduleId) else {
825
            panic "writeDebugInfo: module not found for debug entry";
826
        };
827
        set pos += try! mem::copy(&mut buf[pos..], @sliceOf(&entry.pc as *u8, 4));
828
        set pos += try! mem::copy(&mut buf[pos..], @sliceOf(&entry.offset as *u8, 4));
829
        set pos += try! mem::copy(&mut buf[pos..], modEntry.filePath);
830
831
        set buf[pos] = 0;
832
        set pos += 1;
833
    }
834
    try writeDataWithExt(&buf[..pos], basePath, DEBUG_EXT);
835
}
836
837
/// Run the resolver on the parsed modules.
838
unsafe fn runResolver(ctx: *mut CompileContext, nodeCount: u32) -> resolver::Resolver throws (Error) {
839
    let mut mainArena = alloc::new(&mut MAIN_ARENA[..]);
840
    let entryPkg = try getEntryPackage(ctx);
841
842
    pkgLog(entryPkg, &["resolving", ".."]);
843
844
    let nodeDataSize = nodeCount * @sizeOf(resolver::NodeData);
845
    let nodeDataPtr = try! alloc::alloc(&mut mainArena, nodeDataSize, @alignOf(resolver::NodeData));
846
    let nodeData = @sliceOf(nodeDataPtr as *mut resolver::NodeData, nodeCount);
847
    let storage = resolver::ResolverStorage {
848
        arena: mainArena,
849
        nodeData,
850
        pkgScope: &mut RESOLVER_PKG_SCOPE,
851
        errors: &mut RESOLVER_ERRORS[..],
852
    };
853
    let mut res = resolver::resolver(storage, ctx.config);
854
855
    // Build the semantic package list consumed by the resolver.
856
    let mut resolverPkgs: [resolver::Pkg; MAX_PACKAGES] = undefined;
857
    let mut resolverPackageCount: u32 = 0;
858
    for i in 0..ctx.packageCount {
859
        let pkg = &ctx.packages[i];
860
        let root = try getRootModule(pkg, &ctx.graph);
861
862
        set resolverPkgs[resolverPackageCount] = resolver::Pkg {
863
            rootEntry: root.entry,
864
            rootAst: root.ast,
865
        };
866
        set resolverPackageCount += 1;
867
    }
868
869
    // Resolve all packages.
870
    // TODO: Fix this error printing dance.
871
    let diags = try resolver::resolve(&mut res, &ctx.graph, &resolverPkgs[..resolverPackageCount]) catch {
872
        let diags = resolver::Diagnostics { errors: res.errors };
873
        resolver::printer::printDiagnostics(&diags, &res);
874
        throw Error::Other;
875
    };
876
    if not resolver::success(&diags) {
877
        resolver::printer::printDiagnostics(&diags, &res);
878
        let mut countBuf: [u8; 10] = undefined;
879
        let countStr = fmt::formatU32(diags.errors.len, &mut countBuf[..]);
880
        throw error(&["failed:", countStr, "errors"]);
881
    }
882
    return res;
883
}
884
885
886
/// Assemble one `.ras` input and merge it into the active code generator.
887
///
888
/// Text symbols are appended to `generator`. Data emitted by the assembler is
889
/// copied into `ASM_RO_DATA_BUF` at `*asmDataLen`, and `*asmDataLen` is advanced
890
/// so the next assembly module receives the correct rodata base address.
891
unsafe fn assembleAsmModule(
892
    generator: *mut rv64::Generator,
893
    pkg: *package::Package,
894
    path: *[u8],
895
    asmDataLen: *mut u32,
896
    arena: *mut alloc::Arena
897
) throws (Error) {
898
    pkgLog(pkg, &["asm:", "parsing", "(", path, ")", ".."]);
899
900
    let source = unix::readFile(path, &mut ASM_SOURCE_BUF[..]) else {
901
        throw error(&["error reading assembly file"]);
902
    };
903
    if source.len == ASM_SOURCE_BUF.len {
904
        throw error(&["fatal:", "assembly source too large:", path]);
905
    }
906
    let program = try asm::assemble(
907
        asm::scanner::SourceKind::File { path },
908
        source,
909
        &mut ASM_TEXT_BUF[..],
910
        &mut ASM_DATA_BUF[..],
911
        arena,
912
        &mut STRING_POOL,
913
        rv64::RO_DATA_BASE + *asmDataLen
914
    ) catch {
915
        throw error(&["assembly failed:", path]);
916
    };
917
    if *asmDataLen + program.data.len > ASM_RO_DATA_BUF.len {
918
        throw error(&["fatal:", "assembly rodata too large"]);
919
    }
920
    try! mem::copy(&mut ASM_RO_DATA_BUF[*asmDataLen..], program.data);
921
    set *asmDataLen += program.data.len;
922
923
    rv64::addAssembly(generator, program);
924
}
925
926
/// Assemble all inputs collected in the package inputs.
927
unsafe fn assembleAsmInputs(
928
    ctx: *CompileContext,
929
    generator: *mut rv64::Generator,
930
    asmDataLen: *mut u32,
931
    arena: *mut alloc::Arena
932
) -> *[u8] throws (Error) {
933
    for i in 0..ctx.packageCount {
934
        let input = &ctx.inputs[i];
935
        for j in 0..input.asmPathCount {
936
            try assembleAsmModule(
937
                generator,
938
                &ctx.packages[i],
939
                input.asmPaths[j],
940
                asmDataLen,
941
                arena
942
            );
943
        }
944
    }
945
    return &ASM_RO_DATA_BUF[..*asmDataLen];
946
}
947
948
/// Lower all packages while streaming each lowered function into RV64 codegen.
949
unsafe fn lowerAndGenerateAllPackages(
950
    ctx: *mut CompileContext,
951
    res: *mut resolver::Resolver,
952
    fnArena: *mut alloc::Arena,
953
    codegenOptions: CodegenOptions
954
) -> rv64::Program throws (Error) {
955
    let entryIdx = ctx.entryPkgIdx else {
956
        panic "lowerAndGenerateAllPackages: no entry package";
957
    };
958
    let entryPkg = &ctx.packages[entryIdx];
959
    let startupPath = getEntryStartupPath(ctx);
960
    let options = lower::LowerOptions { debug: ctx.debug, buildTest: ctx.config.buildTest };
961
    let storage = rv64::Storage {
962
        dataSyms: &mut CODEGEN_DATA_SYMS[..],
963
        dataSymEntries: &mut CODEGEN_DATA_SYM_ENTRIES[..],
964
    };
965
    let mut entryPatch = rv64::EntryPatch::None;
966
    match codegenOptions.entryMode {
967
        case CodegenEntryMode::DefaultEntry => {
968
            set entryPatch = rv64::EntryPatch::Reserved(nil);
969
        }
970
        else => {}
971
    }
972
    let mut generator = rv64::beginProgram(
973
        rv64::ProgramOptions { entryPatch, debug: codegenOptions.debug },
974
        &mut res.arena
975
    );
976
    let mut codegenCtx = codegenSink::Context {
977
        generator: &mut generator,
978
        fnArena,
979
    };
980
    let mut low = lower::lowerer(
981
        res, &ctx.graph, entryPkg.name, &mut res.arena, fnArena, options
982
    );
983
    set low.output = lower::FnOutput::Stream(lower::FnSink {
984
        ctx: codegenSink::eraseContext(&mut codegenCtx),
985
        emitFn: codegenSink::generate,
986
    });
987
    let mut asmDataLen: u32 = 0;
988
    if let path = startupPath {
989
        try assembleAsmModule(&mut generator, entryPkg, path, &mut asmDataLen, &mut res.arena);
990
    }
991
    try lowerAllPackagesInto(ctx, res, &mut low);
992
    let asmData = try assembleAsmInputs(ctx, &mut generator, &mut asmDataLen, &mut res.arena);
993
994
    match generator.entryPatch {
995
        case rv64::EntryPatch::Reserved(targetName) => {
996
            if targetName == nil {
997
                throw error(&["fatal:", "no default function found"]);
998
            }
999
        }
1000
        else => {}
1001
    }
1002
    if let path = codegenOptions.logPath {
1003
        pkgLog(entryPkg, &["generating code", "(", path, ")", ".."]);
1004
    }
1005
    return rv64::finishProgram(&mut generator, &low.data[..], storage, asmData, &mut RO_DATA_BUF[..], &mut RW_DATA_BUF[..]);
1006
}
1007
1008
/// Lower, optionally dump, and optionally generate binary output.
1009
unsafe fn compile(
1010
    ctx: *mut CompileContext,
1011
    res: *mut resolver::Resolver,
1012
    fnArena: *mut alloc::Arena
1013
) throws (Error) {
1014
    let entryPkg = try getEntryPackage(ctx);
1015
    let mut out = sexpr::Output::Stdout;
1016
1017
    if ctx.dump == Dump::Il {
1018
        // Lower all packages into a single unified IL program for dumping.
1019
        let program = try lowerAllPackages(ctx, res);
1020
        il::printer::printProgram(&mut out, &mut res.arena, &program);
1021
        io::print("\n");
1022
        return;
1023
    }
1024
    if ctx.dump == Dump::Asm {
1025
        let result = try lowerAndGenerateAllPackages(ctx, res, fnArena, CodegenOptions {
1026
            logPath: nil,
1027
            debug: false,
1028
            entryMode: CodegenEntryMode::None,
1029
        });
1030
        printer::printCodeTo(&mut out, entryPkg.name, result.code, result.funcs, &mut res.arena);
1031
        io::print("\n");
1032
1033
        return;
1034
    }
1035
    // Generate binary output if path specified.
1036
    let outPath = ctx.outputPath else {
1037
        try lowerAllPackages(ctx, res);
1038
        return;
1039
    };
1040
    let startupPath = getEntryStartupPath(ctx);
1041
    let result = try lowerAndGenerateAllPackages(ctx, res, fnArena, CodegenOptions {
1042
        logPath: outPath,
1043
        debug: ctx.debug,
1044
        entryMode: CodegenEntryMode::None
1045
            if startupPath <> nil
1046
            else CodegenEntryMode::DefaultEntry,
1047
    });
1048
1049
    if not writeImage(
1050
        result.code,
1051
        &RO_DATA_BUF[..result.roDataSize],
1052
        &RW_DATA_BUF[..result.rwDataSize],
1053
        outPath
1054
    ) {
1055
        throw error(&["fatal:", "failed to write output file"]);
1056
    }
1057
1058
    // Write debug info file if enabled.
1059
    if ctx.debug {
1060
        try writeDebugInfo(result.debugEntries, &ctx.graph, outPath, &mut res.arena);
1061
    }
1062
    pkgLog(entryPkg, &["ok", "(", outPath, ")"]);
1063
}
1064
1065
@default unsafe fn main(env: *sys::Env) -> i32 {
1066
    let mut arena = ast::nodeArena(&mut TEMP_ARENA[..]);
1067
    let mut ctx = try processCommand(env.args, &mut arena) catch {
1068
        return 1;
1069
    };
1070
    match ctx.dump {
1071
        case Dump::Ast => {
1072
            try dumpAst(&ctx) catch {
1073
                return 1;
1074
            };
1075
            return 0;
1076
        }
1077
        case Dump::Graph => {
1078
            dumpGraph(&ctx);
1079
            return 0;
1080
        }
1081
        else => {}
1082
    }
1083
    // Generate test runner if in test mode.
1084
    if ctx.config.buildTest {
1085
        try generateTestRunner(&mut ctx, &mut arena) catch {
1086
            return 1;
1087
        };
1088
    }
1089
    // Run resolution phase.
1090
    let mut res = try runResolver(&mut ctx, arena.nextId) catch {
1091
        return 1;
1092
    };
1093
    let mut fnArena = alloc::new(&mut FN_ARENA[..]);
1094
1095
    // Lower, dump, and/or generate output.
1096
    try compile(&mut ctx, &mut res, &mut fnArena) catch {
1097
        return 1;
1098
    };
1099
    return 0;
1100
}