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