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