Add explicit startup assembly support

9e960a2c8581b3860388241907c77118a1af50ca71df41876c66fc8f9a3cb697
Add a `-start <input.ras>` option for the entry package so callers can
provide their own startup text before generated functions. When explicit
startup is present, the compiler skips the synthesized entry jump and
exposes the semantic default function through the `::default` text
symbol.

Thread startup assembly data into the existing assembly data prefix and
allow unresolved assembly jumps/address loads to be resolved by
whole-program RV64 patching. Add executable tests for both a startup
handoff to `@default` and startup code that exits directly.
Alexis Sellier committed ago 1 parent 8a64c0a3
Makefile +3 -1
70 70
# Only tests with `//! returns:` are compiled to binaries and executed.
71 71
BIN_TEST_EXE_SRC := $(shell grep -rl '^//! returns:' $(BIN_TEST_DIR))
72 72
BIN_TEST_RAD_EXE_SRC := $(filter %.rad,$(BIN_TEST_EXE_SRC))
73 73
BIN_TEST_RAS_EXE_SRC := $(filter %.ras,$(BIN_TEST_EXE_SRC))
74 74
BIN_TEST_RAD_ASM_SRC := $(wildcard $(BIN_TEST_RAD_EXE_SRC:.rad=.ras))
75 +
BIN_TEST_RAD_START_SRC := $(wildcard $(BIN_TEST_RAD_EXE_SRC:.rad=.start.ras))
75 76
BIN_TEST_EXE_BIN := $(patsubst %.rad,%.rv64,$(BIN_TEST_RAD_EXE_SRC)) \
76 77
	$(patsubst %.ras,%.rv64,$(BIN_TEST_RAS_EXE_SRC))
77 78
BIN_RUNNER   := test/runner.rv64
78 79
BIN_TEST_RUN := test/run
79 80
86 87
	@echo "radiance test/runner.rad => $@"
87 88
	@$(RADIANCE) $(STD) -pkg runner -mod test/runner.rad -entry runner -o $@
88 89
89 90
# A `.rad` executable test can have a same-basename `.ras` module.
90 91
$(patsubst %.ras,%.rv64,$(BIN_TEST_RAD_ASM_SRC)): %.rv64: %.ras
92 +
$(patsubst %.start.ras,%.rv64,$(BIN_TEST_RAD_START_SRC)): %.rv64: %.start.ras
91 93
92 94
# Compile each executable test to a binary.
93 95
$(BIN_TEST_DIR)/%.rv64: $(BIN_TEST_DIR)/%.rad $(RAD_BIN)
94 96
	@echo "radiance $< => $@"
95 -
	@$(RADIANCE) -pkg test -mod $< $(patsubst %,-mod %,$(wildcard $(@:.rv64=.ras))) -o $@
97 +
	@$(RADIANCE) -pkg test $(patsubst %,-start %,$(wildcard $(@:.rv64=.start.ras))) -mod $< $(patsubst %,-mod %,$(wildcard $(@:.rv64=.ras))) -o $@
96 98
97 99
$(BIN_TEST_DIR)/%.rv64: $(BIN_TEST_DIR)/%.ras $(BIN_RUNNER)
98 100
	@echo "asm $< => $@"
99 101
	@$(EMU) $(EMU_FLAGS) -run $(BIN_RUNNER) -- assemble $< $@
100 102
compiler/radiance.rad +51 -12
90 90
/// Accumulated assembly read-only data.
91 91
static ASM_RO_DATA_BUF: [u8; MAX_RO_DATA_SIZE] = undefined;
92 92
93 93
/// Assembly source file extension.
94 94
constant ASM_SOURCE_EXT: *[u8] = ".ras";
95 +
/// Symbol name exported for startup code to call the semantic entry function.
96 +
constant DEFAULT_ENTRY_SYMBOL: *[u8] = "::default";
95 97
96 98
/// Usage string.
97 99
constant USAGE: *[u8] =
98 -
    "usage: radiance -pkg <name> -mod <input>.. [-pkg <name> -mod <input>..] -entry <pkg> -o <output>\n";
100 +
    "usage: radiance -pkg <name> [-start <input.ras>] -mod <input>.. [-pkg <name> -mod <input>..] -entry <pkg> -o <output>\n";
99 101
100 102
/// Compiler error.
101 103
union Error {
102 104
    Other,
103 105
}
126 128
127 129
/// Source inputs belonging to one command-line package.
128 130
record PackageInput {
129 131
    /// Package name from the `-pkg` argument.
130 132
    name: *[u8],
133 +
    /// Optional startup assembly emitted before generated text.
134 +
    startupPath: ?*[u8],
131 135
    /// Radiance source paths for this package.
132 136
    radPaths: [*[u8]; MAX_LOADED_MODULES],
133 137
    /// Number of Radiance source paths.
134 138
    radPathCount: u32,
135 139
    /// Assembly source paths for this package.
168 172
169 173
/// State carried by the streaming lowerer/codegen callback.
170 174
record CodegenSinkContext {
171 175
    /// RV64 generator receiving lowered functions.
172 176
    generator: *mut rv64::Generator,
173 -
    /// Arena used for codegen scratch allocations.
174 -
    codegenArena: *mut alloc::Arena,
175 177
    /// Arena holding the current function's lowered IL.
176 178
    fnArena: *mut alloc::Arena,
177 179
}
178 180
179 181
/// Entry handling for streamed code generation.
234 236
235 237
/// Create an empty source input set for one package.
236 238
fn packageInput(name: *[u8]) -> PackageInput {
237 239
    return PackageInput {
238 240
        name,
241 +
        startupPath: nil,
239 242
        radPaths: undefined,
240 243
        radPathCount: 0,
241 244
        asmPaths: undefined,
242 245
        asmPathCount: 0,
243 246
    };
339 342
                    throw error(&["too many modules specified for package"]);
340 343
                }
341 344
                set input.radPaths[input.radPathCount] = args[idx];
342 345
                set input.radPathCount += 1;
343 346
            }
347 +
        } else if mem::eq(arg, "-start") {
348 +
            try nextArg(args, &mut idx, &["`-start` requires an assembly path"]);
349 +
            let pkgIdx = currentPkgIdx else {
350 +
                throw error(&["`-start` must follow a `-pkg` argument"]);
351 +
            };
352 +
            let input = &mut inputs[pkgIdx];
353 +
            if input.startupPath <> nil {
354 +
                throw error(&["package", input.name, "has more than one startup file"]);
355 +
            }
356 +
            if not hasExtension(args[idx], ASM_SOURCE_EXT) {
357 +
                throw error(&["`-start` requires a `.ras` assembly file"]);
358 +
            }
359 +
            set input.startupPath = args[idx];
344 360
        } else if mem::eq(arg, "-entry") {
345 361
            try nextArg(args, &mut idx, &["`-entry` requires a package name"]);
346 362
            set entryPkgName = args[idx];
347 363
        } else if mem::eq(arg, "-test") {
348 364
            set buildTest = true;
397 413
        }
398 414
        if entryPkgIdx == nil {
399 415
            throw error(&["fatal:", "entry package", entryName, "not found"]);
400 416
        }
401 417
    }
418 +
    let entryIdx = entryPkgIdx else {
419 +
        panic "processCommand: no entry package";
420 +
    };
421 +
    for i in 0..pkgCount {
422 +
        if i <> entryIdx and inputs[i].startupPath <> nil {
423 +
            throw error(&["`-start` is only supported on the entry package"]);
424 +
        }
425 +
    }
402 426
    let graph = module::moduleGraph(&mut MODULE_ENTRIES[..], &mut STRING_POOL, arena);
403 427
    let mut ctx = CompileContext {
404 428
        packages: undefined,
405 429
        inputs,
406 430
        packageCount: pkgCount,
430 454
        throw error(&["no entry package specified"]);
431 455
    };
432 456
    return &ctx.packages[entryIdx];
433 457
}
434 458
459 +
/// Return the startup assembly path for the entry package, if one was supplied.
460 +
fn getEntryStartupPath(ctx: *CompileContext) -> ?*[u8] {
461 +
    let entryIdx = ctx.entryPkgIdx else {
462 +
        panic "getEntryStartupPath: no entry package";
463 +
    };
464 +
    return ctx.inputs[entryIdx].startupPath;
465 +
}
466 +
435 467
/// Get root module info from a package.
436 468
fn getRootModule(pkg: *package::Package, graph: *module::ModuleGraph) -> RootModule throws (Error) {
437 469
    let rootId = pkg.rootModuleId else {
438 470
        throw error(&["no root module found"]);
439 471
    };
855 887
fn generateLoweredFn(ctxPtr: *mut opaque, func: *il::Fn, role: lower::FnRole) {
856 888
    let ctx = ctxPtr as *mut CodegenSinkContext;
857 889
858 890
    match role {
859 891
        case lower::FnRole::Default => {
892 +
            rv64::recordFunctionAlias(ctx.generator, DEFAULT_ENTRY_SYMBOL);
860 893
            match ctx.generator.entryPatch {
861 894
                case rv64::EntryPatch::Reserved(_) => {
862 895
                    set ctx.generator.entryPatch = rv64::EntryPatch::Reserved(func.name);
863 896
                }
864 -
                else => panic "generateLoweredFn: entry jump was not reserved",
897 +
                // No entry jump was reserved: startup assembly calls the
898 +
                // default function through `DEFAULT_ENTRY_SYMBOL` instead.
899 +
                case rv64::EntryPatch::None => {}
865 900
            }
866 901
        }
867 902
        else => {}
868 903
    }
869 -
    rv64::generateFunction(ctx.generator, func, ctx.codegenArena);
904 +
    rv64::generateFunction(ctx.generator, func, ctx.fnArena);
870 905
    alloc::reset(ctx.fnArena);
871 906
}
872 907
873 908
/// Assemble one `.ras` input and merge it into the active code generator.
874 909
///
912 947
913 948
/// Assemble all inputs collected in the package inputs.
914 949
fn assembleAsmInputs(
915 950
    ctx: *CompileContext,
916 951
    generator: *mut rv64::Generator,
952 +
    asmDataLen: *mut u32,
917 953
    arena: *mut alloc::Arena
918 954
) -> *[u8] throws (Error) {
919 -
    let mut asmDataLen: u32 = 0;
920 -
921 955
    for i in 0..ctx.packageCount {
922 956
        let input = &ctx.inputs[i];
923 957
        for j in 0..input.asmPathCount {
924 958
            try assembleAsmModule(
925 959
                generator,
926 960
                &ctx.packages[i],
927 961
                input.asmPaths[j],
928 -
                &mut asmDataLen,
962 +
                asmDataLen,
929 963
                arena
930 964
            );
931 965
        }
932 966
    }
933 -
    return &ASM_RO_DATA_BUF[..asmDataLen];
967 +
    return &ASM_RO_DATA_BUF[..*asmDataLen];
934 968
}
935 969
936 970
/// Lower all packages while streaming each lowered function into RV64 codegen.
937 971
fn lowerAndGenerateAllPackages(
938 972
    ctx: *mut CompileContext,
960 994
        rv64::ProgramOptions { entryPatch, debug: codegenOptions.debug },
961 995
        &mut res.arena
962 996
    );
963 997
    let mut codegenCtx = CodegenSinkContext {
964 998
        generator: &mut generator,
965 -
        codegenArena: &mut res.arena,
966 999
        fnArena,
967 1000
    };
968 1001
    let mut low = lower::lowerer(
969 1002
        res, &ctx.graph, entryPkg.name, &mut res.arena, fnArena, options
970 1003
    );
971 1004
    set low.output = lower::FnOutput::Stream(lower::FnSink {
972 1005
        ctx: &mut codegenCtx as *mut opaque,
973 1006
        emitFn: generateLoweredFn,
974 1007
    });
1008 +
    let mut asmDataLen: u32 = 0;
1009 +
    if let startupPath = getEntryStartupPath(ctx) {
1010 +
        try assembleAsmModule(&mut generator, entryPkg, startupPath, &mut asmDataLen, &mut res.arena);
1011 +
    }
975 1012
    try lowerAllPackagesInto(ctx, res, &mut low);
976 -
    let asmData = try assembleAsmInputs(ctx, &mut generator, &mut res.arena);
1013 +
    let asmData = try assembleAsmInputs(ctx, &mut generator, &mut asmDataLen, &mut res.arena);
977 1014
978 1015
    match generator.entryPatch {
979 1016
        case rv64::EntryPatch::Reserved(targetName) => {
980 1017
            if targetName == nil {
981 1018
                throw error(&["fatal:", "no default function found"]);
1022 1059
        return;
1023 1060
    };
1024 1061
    let result = try lowerAndGenerateAllPackages(ctx, res, fnArena, CodegenOptions {
1025 1062
        logPath: outPath,
1026 1063
        debug: ctx.debug,
1027 -
        entryMode: CodegenEntryMode::DefaultEntry,
1064 +
        entryMode: CodegenEntryMode::None
1065 +
            if getEntryStartupPath(ctx) <> nil
1066 +
            else CodegenEntryMode::DefaultEntry,
1028 1067
    });
1029 1068
1030 1069
    if not writeCode(result.code, outPath) {
1031 1070
        throw error(&["fatal:", "failed to write output file"]);
1032 1071
    }
lib/std/arch/rv64.rad +18 -1
239 239
240 240
    // Reclaim unused memory after instruction selection.
241 241
    alloc::restore(arena, checkpoint);
242 242
}
243 243
244 +
/// Record an alternate name for the next function emitted.
245 +
export fn recordFunctionAlias(generator: *mut Generator, name: *[u8]) {
246 +
    emit::recordFuncOffsetAt(&mut generator.e, name, generator.e.codeLen);
247 +
}
248 +
244 249
/// Add the text section of an assembled program to the generator.
245 250
///
246 251
/// This function snapshots the generator's current code length as the base
247 252
/// index, converts each text symbol's byte offset to an instruction index, adds
248 253
/// that base, and records the final address for printing. Only `.export` text
263 268
            if symbol.isExported {
264 269
                emit::recordFuncOffsetAt(&mut generator.e, symbol.name, index);
265 270
            }
266 271
        }
267 272
    }
273 +
    for fixup in program.externalFixups {
274 +
        match fixup.info {
275 +
            case asm::FixupInfo::Jal { rd, index } => {
276 +
                emit::recordJumpAt(&mut generator.e, fixup.symbol, rd, baseIndex + index);
277 +
            }
278 +
            case asm::FixupInfo::Addr { rd, index } => {
279 +
                emit::recordAddrLoadAt(&mut generator.e, fixup.symbol, rd, baseIndex + index);
280 +
            }
281 +
            else => panic "addAssembly: invalid external fixup",
282 +
        }
283 +
    }
268 284
    for word in program.text {
269 285
        emit::emit(&mut generator.e, word);
270 286
    }
271 287
}
272 288
303 319
            emit::patch(&mut generator.e, 1, encode::jalr(ZERO, SCRATCH1, s.lo));
304 320
        }
305 321
        else => {}
306 322
    }
307 323
    // Patch function calls and address loads now that all functions are emitted.
324 +
    emit::patchJumps(&mut generator.e);
308 325
    emit::patchCalls(&mut generator.e);
309 326
    emit::patchAddrLoads(&mut generator.e, &dataSymMap);
310 327
311 328
    // Emit data sections.
312 329
    assert roDataPrefix.len <= roDataBuf.len, "finishProgram: rodata prefix buffer overflow";
318 335
    let rwDataSize = data::emitSection(
319 336
        globalData, &dataSymMap, &generator.e.labels, codeBase, rwDataBuf, false
320 337
    );
321 338
    return Program {
322 339
        code: emit::getCode(&generator.e),
323 -
        funcs: emit::getFuncs(&generator.e),
340 +
        funcs: generator.e.funcs,
324 341
        roDataSize,
325 342
        rwDataSize,
326 343
        debugEntries: emit::getDebugEntries(&generator.e),
327 344
    };
328 345
}
lib/std/arch/rv64/asm.rad +7 -0
78 78
    text: *[u32],
79 79
    /// Raw bytes in the data section.
80 80
    data: *[u8],
81 81
    /// Symbols defined by the source.
82 82
    symbols: *[Symbol],
83 +
    /// Text references resolved by the whole-program emitter.
84 +
    externalFixups: *[Fixup],
83 85
}
84 86
85 87
/// Errors reported while assembling source text.
86 88
export union Error {
87 89
    /// Invalid syntax or operand form at a source offset.
468 470
    constMap: dict::Dict,
469 471
    /// Names marked by `.export`.
470 472
    exportMap: dict::Dict,
471 473
    /// Pending fixups.
472 474
    fixups: *mut [Fixup],
475 +
    /// Fixups that reference text outside this assembly fragment.
476 +
    externalFixups: *mut [Fixup],
473 477
    /// Absolute runtime address of data-section offset zero.
474 478
    dataBase: u32,
475 479
}
476 480
477 481
/// Assemble source using `dataBase` as the runtime address of the data-section.
487 491
    let slotCap = source.len + SOURCE_CAP_PADDING;
488 492
    let tableCap = nextPowerOfTwo(slotCap * TABLE_CAPACITY_SCALE);
489 493
490 494
    let symbols = try! alloc::allocSlice(arena, @sizeOf(Symbol), @alignOf(Symbol), slotCap);
491 495
    let fixups = try! alloc::allocSlice(arena, @sizeOf(Fixup), @alignOf(Fixup), slotCap);
496 +
    let externalFixups = try! alloc::allocSlice(arena, @sizeOf(Fixup), @alignOf(Fixup), slotCap);
492 497
    let entries = try! alloc::allocSlice(arena, @sizeOf(dict::Entry), @alignOf(dict::Entry), tableCap);
493 498
    let constEntries = try! alloc::allocSlice(arena, @sizeOf(dict::Entry), @alignOf(dict::Entry), tableCap);
494 499
    let exportEntries = try! alloc::allocSlice(arena, @sizeOf(dict::Entry), @alignOf(dict::Entry), tableCap);
495 500
496 501
    let mut a = Assembler {
502 507
        symbols: @sliceOf((symbols as *mut [Symbol]).ptr, 0, (symbols as *mut [Symbol]).len),
503 508
        symbolMap: dict::init(entries as *mut [dict::Entry]),
504 509
        constMap: dict::init(constEntries as *mut [dict::Entry]),
505 510
        exportMap: dict::init(exportEntries as *mut [dict::Entry]),
506 511
        fixups: @sliceOf((fixups as *mut [Fixup]).ptr, 0, (fixups as *mut [Fixup]).len),
512 +
        externalFixups: @sliceOf((externalFixups as *mut [Fixup]).ptr, 0, (externalFixups as *mut [Fixup]).len),
507 513
        dataBase,
508 514
    };
509 515
    // Parse assembly source and emit instructions.
510 516
    try parser::parseProgram(&mut a);
511 517
    // Resolve fixups and finalize program.
513 519
514 520
    return Program {
515 521
        text: a.text,
516 522
        data: a.data,
517 523
        symbols: a.symbols,
524 +
        externalFixups: a.externalFixups,
518 525
    };
519 526
}
520 527
521 528
/// Return the next power of two at least as large as `value`.
522 529
fn nextPowerOfTwo(value: u32) -> u32 {
lib/std/arch/rv64/asm/emit.rad +18 -25
3 3
use std::arch::rv64::encode;
4 4
use std::arch::rv64;
5 5
use std::fmt;
6 6
7 7
use std::collections::dict;
8 +
use std::lang::alloc;
8 9
use std::lang::gen;
9 10
10 11
/// Define a symbol at the current text or data offset.
11 12
export fn defineSymbol(a: *mut super::Assembler, name: *[u8]) {
12 -
    if a.symbols.len >= a.symbols.cap {
13 -
        panic "asm: symbol buffer full";
14 -
    }
15 13
    let idx = a.symbols.len;
16 14
    let offset: i32 = a.data.len as i32
17 15
        if a.section == super::Section::Data
18 16
        else a.text.len as i32 * rv64::INSTR_SIZE;
19 17
20 -
    set a.symbols = @sliceOf(a.symbols.ptr, idx + 1, a.symbols.cap);
21 -
    set a.symbols[idx] = super::Symbol {
18 +
    a.symbols.append(super::Symbol {
22 19
        name,
23 20
        section: a.section,
24 21
        offset,
25 22
        isExported: dict::get(&a.exportMap, name) <> nil,
26 -
    };
23 +
    }, alloc::arenaAllocator(a.arena));
27 24
    dict::insert(&mut a.symbolMap, name, idx as i32);
28 25
}
29 26
30 27
/// Append one encoded instruction word to the text section.
31 28
export fn emitText(a: *mut super::Assembler, word: u32) throws (super::Error) {
32 -
    if a.text.len >= a.text.cap {
33 -
        throw super::Error::TextOverflow;
34 -
    }
35 -
    let idx = a.text.len;
36 -
    set a.text = @sliceOf(a.text.ptr, idx + 1, a.text.cap);
37 -
    set a.text[idx] = word;
29 +
    a.text.append(word, alloc::arenaAllocator(a.arena));
38 30
}
39 31
40 32
/// Append `words` no-op instructions to the text section.
41 33
export fn emitTextPadding(a: *mut super::Assembler, words: u32) throws (super::Error) {
42 34
    for _ in 0..words {
44 36
    }
45 37
}
46 38
47 39
/// Append one byte to the data section.
48 40
export fn emitByte(a: *mut super::Assembler, byte: u8) throws (super::Error) {
49 -
    if a.data.len >= a.data.cap {
50 -
        throw super::Error::DataOverflow;
51 -
    }
52 -
    let idx = a.data.len;
53 -
    set a.data = @sliceOf(a.data.ptr, idx + 1, a.data.cap);
54 -
    set a.data[idx] = byte;
41 +
    a.data.append(byte, alloc::arenaAllocator(a.arena));
55 42
}
56 43
57 44
/// Emit a little-endian integer with `bytes` bytes.
58 45
fn emitDataInt(a: *mut super::Assembler, bits: u64, bytes: u32) throws (super::Error) {
59 46
    for i in 0..bytes {
91 78
    }
92 79
}
93 80
94 81
/// Record a pending symbol fixup.
95 82
fn recordFixup(a: *mut super::Assembler, symbol: *[u8], info: super::FixupInfo) {
96 -
    if a.fixups.len >= a.fixups.cap {
97 -
        panic "asm: fixup buffer full";
98 -
    }
99 -
    let idx = a.fixups.len as u32;
100 -
    set a.fixups = @sliceOf(a.fixups.ptr, idx + 1, a.fixups.cap);
101 -
    set a.fixups[idx] = super::Fixup { symbol, info };
83 +
    a.fixups.append(super::Fixup { symbol, info }, alloc::arenaAllocator(a.arena));
84 +
}
85 +
86 +
/// Record a text fixup that must be resolved after all program text is known.
87 +
fn recordExternalFixup(a: *mut super::Assembler, fixup: super::Fixup) {
88 +
    a.externalFixups.append(fixup, alloc::arenaAllocator(a.arena));
102 89
}
103 90
104 91
/// Record a text-section symbol fixup and reserve its instruction words.
105 92
export fn recordTextFixup(a: *mut super::Assembler, symbol: *[u8], info: super::FixupInfo, words: u32) throws (super::Error) {
106 93
    recordFixup(a, symbol, info);
125 112
/// Resolve final symbol references and patch all delayed output.
126 113
export fn finishProgram(a: *mut super::Assembler) throws (super::Error) {
127 114
    for i in 0..a.fixups.len {
128 115
        let fixup = a.fixups[i];
129 116
        let symbol = findSymbol(a, fixup.symbol) else {
130 -
            throw super::Error::Invalid { offset: 0, message: "undefined symbol" };
117 +
            match fixup.info {
118 +
                case super::FixupInfo::Jal { .. }, super::FixupInfo::Addr { .. } => {
119 +
                    recordExternalFixup(a, fixup);
120 +
                    continue;
121 +
                }
122 +
                else => throw super::Error::Invalid { offset: 0, message: "undefined symbol" },
123 +
            }
131 124
        };
132 125
        match fixup.info {
133 126
            case super::FixupInfo::Branch { op, rs1, rs2, index } => {
134 127
                if symbol.section <> super::Section::Text {
135 128
                    throw super::Error::Invalid { offset: 0, message: "branch target must be in text section" };
lib/std/arch/rv64/asm/parser.rad +3 -8
566 566
fn parseSpaceDirective(a: *mut super::Assembler) throws (super::Error) {
567 567
    let count = try parseValue(a);
568 568
    if count < 0 {
569 569
        throw fail(a, "space size must be non-negative");
570 570
    }
571 -
    let remaining = a.data.cap - a.data.len;
572 -
    if count > remaining as i64 {
571 +
    // The data section grows on demand; only reject sizes that cannot be
572 +
    // represented as a section offset.
573 +
    if count > super::U32_MAX_VALUE - a.data.len as i64 {
573 574
        throw super::Error::DataOverflow;
574 575
    }
575 576
    for _ in 0..count as u32 {
576 577
        try emit::emitByte(a, 0);
577 578
    }
598 599
            let bytes = a.text.len * rv64::INSTR_SIZE as u32;
599 600
            let aligned = checkedAlignUp(bytes, amount) else {
600 601
                throw super::Error::TextOverflow;
601 602
            };
602 603
            let words = (aligned - bytes) / rv64::INSTR_SIZE as u32;
603 -
            if words > a.text.cap - a.text.len {
604 -
                throw super::Error::TextOverflow;
605 -
            }
606 604
            try emit::emitTextPadding(a, words);
607 605
        }
608 606
        case super::Section::Data => {
609 607
            let aligned = checkedAlignUp(a.data.len, amount) else {
610 608
                throw super::Error::DataOverflow;
611 609
            };
612 -
            if aligned > a.data.cap {
613 -
                throw super::Error::DataOverflow;
614 -
            }
615 610
            for _ in a.data.len..aligned {
616 611
                try emit::emitByte(a, 0);
617 612
            }
618 613
        }
619 614
    }
lib/std/arch/rv64/emit.rad +64 -44
56 56
    index: u32,
57 57
    /// Target function name.
58 58
    target: *[u8],
59 59
}
60 60
61 +
/// Assembly jump that needs offset patching after all text is emitted.
62 +
export record PendingJump {
63 +
    /// Index in code buffer where the jump was emitted.
64 +
    index: u32,
65 +
    /// Target function name.
66 +
    target: *[u8],
67 +
    /// Destination register.
68 +
    rd: gen::Reg,
69 +
}
70 +
61 71
/// Address load that needs patching after layout is known.
62 72
export record PendingAddrLoad {
63 73
    /// Index in code buffer where the load was emitted.
64 74
    index: u32,
65 75
    /// Target function or data symbol name.
86 96
    offset: i32,
87 97
}
88 98
89 99
/// Emission context. Tracks state during code generation.
90 100
export record Emitter {
101 +
    /// Allocator for growing append-backed emitter lists.
102 +
    allocator: alloc::Allocator,
91 103
    /// Emitted instructions storage.
92 104
    code: *mut [u32],
93 105
    /// Current number of emitted instructions.
94 106
    codeLen: u32,
95 107
    /// Local branches needing offset patching.
96 108
    pendingBranches: *mut [PendingBranch],
97 -
    /// Number of pending local branches.
98 -
    pendingBranchesLen: u32,
99 109
    /// Function calls needing offset patching.
100 110
    pendingCalls: *mut [PendingCall],
101 -
    /// Number of pending calls.
102 -
    pendingCallsLen: u32,
111 +
    /// Assembly jumps needing offset patching.
112 +
    pendingJumps: *mut [PendingJump],
103 113
    /// Function address loads needing offset patching.
104 114
    pendingAddrLoads: *mut [PendingAddrLoad],
105 -
    /// Number of pending address loads.
106 -
    pendingAddrLoadsLen: u32,
107 115
    /// Block label tracking.
108 116
    labels: labels::Labels,
109 117
    /// Function start positions for printing.
110 118
    funcs: *mut [types::FuncAddr],
111 -
    /// Number of recorded functions.
112 -
    funcsLen: u32,
113 119
    /// Debug entries mapping PCs to source locations.
114 120
    debugEntries: *mut [types::DebugEntry],
115 121
    /// Number of debug entries recorded.
116 122
    debugEntriesLen: u32,
117 123
}
178 184
/// Create a new emitter.
179 185
export fn emitter(arena: *mut alloc::Arena, debug: bool) -> Emitter throws (alloc::AllocError) {
180 186
    let code = try alloc::allocSlice(arena, @sizeOf(u32), @alignOf(u32), MAX_INSTRS);
181 187
    let pendingBranches = try alloc::allocSlice(arena, @sizeOf(PendingBranch), @alignOf(PendingBranch), MAX_PENDING);
182 188
    let pendingCalls = try alloc::allocSlice(arena, @sizeOf(PendingCall), @alignOf(PendingCall), MAX_PENDING);
189 +
    let pendingJumps = try alloc::allocSlice(arena, @sizeOf(PendingJump), @alignOf(PendingJump), MAX_PENDING);
183 190
    let pendingAddrLoads = try alloc::allocSlice(arena, @sizeOf(PendingAddrLoad), @alignOf(PendingAddrLoad), MAX_PENDING);
184 191
    let blockOffsets = try alloc::allocSlice(arena, @sizeOf(i32), @alignOf(i32), labels::MAX_BLOCKS_PER_FN);
185 192
    let funcEntries = try alloc::allocSlice(arena, @sizeOf(dict::Entry), @alignOf(dict::Entry), labels::FUNC_TABLE_SIZE);
186 193
    let funcs = try alloc::allocSlice(arena, @sizeOf(types::FuncAddr), @alignOf(types::FuncAddr), MAX_FUNCS);
187 194
190 197
        set debugEntries = try alloc::allocSlice(
191 198
            arena, @sizeOf(types::DebugEntry), @alignOf(types::DebugEntry), MAX_DEBUG_ENTRIES
192 199
        ) as *mut [types::DebugEntry];
193 200
    }
194 201
    return Emitter {
202 +
        allocator: alloc::arenaAllocator(arena),
195 203
        code: code as *mut [u32],
196 204
        codeLen: 0,
197 -
        pendingBranches: pendingBranches as *mut [PendingBranch],
198 -
        pendingBranchesLen: 0,
199 -
        pendingCalls: pendingCalls as *mut [PendingCall],
200 -
        pendingCallsLen: 0,
201 -
        pendingAddrLoads: pendingAddrLoads as *mut [PendingAddrLoad],
202 -
        pendingAddrLoadsLen: 0,
205 +
        pendingBranches: @sliceOf((pendingBranches as *mut [PendingBranch]).ptr, 0, MAX_PENDING),
206 +
        pendingCalls: @sliceOf((pendingCalls as *mut [PendingCall]).ptr, 0, MAX_PENDING),
207 +
        pendingJumps: @sliceOf((pendingJumps as *mut [PendingJump]).ptr, 0, MAX_PENDING),
208 +
        pendingAddrLoads: @sliceOf((pendingAddrLoads as *mut [PendingAddrLoad]).ptr, 0, MAX_PENDING),
203 209
        labels: labels::init(blockOffsets as *mut [i32], funcEntries as *mut [dict::Entry]),
204 -
        funcs: funcs as *mut [types::FuncAddr],
205 -
        funcsLen: 0,
210 +
        funcs: @sliceOf((funcs as *mut [types::FuncAddr]).ptr, 0, MAX_FUNCS),
206 211
        debugEntries,
207 212
        debugEntriesLen: 0,
208 213
    };
209 214
}
210 215
251 256
    recordFuncAt(e, name, e.codeLen);
252 257
}
253 258
254 259
/// Record a function's start position at `index` for printing.
255 260
export fn recordFuncAt(e: *mut Emitter, name: *[u8], index: u32) {
256 -
    assert e.funcsLen < e.funcs.len, "recordFunc: funcs buffer full";
257 -
    set e.funcs[e.funcsLen] = types::FuncAddr { name, index };
258 -
    set e.funcsLen += 1;
261 +
    e.funcs.append(types::FuncAddr { name, index }, e.allocator);
259 262
}
260 263
261 264
/// Record a local branch needing later patching.
262 265
/// Unconditional jumps use a single slot (J-type, +-1MB range).
263 266
/// Conditional branches use two slots (B-type has only +-4KB range,
264 267
/// so large functions may need the inverted-branch + JAL fallback).
265 268
export fn recordBranch(e: *mut Emitter, targetBlock: u32, kind: BranchKind) {
266 -
    assert e.pendingBranchesLen < e.pendingBranches.len, "recordBranch: buffer full";
267 -
    set e.pendingBranches[e.pendingBranchesLen] = PendingBranch {
269 +
    e.pendingBranches.append(PendingBranch {
268 270
        index: e.codeLen,
269 271
        target: targetBlock,
270 272
        kind: kind,
271 -
    };
272 -
    set e.pendingBranchesLen += 1;
273 +
    }, e.allocator);
273 274
274 275
    emit(e, encode::nop()); // First slot, always needed.
275 276
276 277
    match kind {
277 278
        case BranchKind::Jump => {},
281 282
282 283
/// Record a function call needing later patching.
283 284
/// Emits placeholder instructions that will be patched later.
284 285
/// Uses two slots to support long-distance calls.
285 286
export fn recordCall(e: *mut Emitter, target: *[u8]) {
286 -
    assert e.pendingCallsLen < e.pendingCalls.len, "recordCall: buffer full";
287 -
    set e.pendingCalls[e.pendingCallsLen] = PendingCall {
287 +
    e.pendingCalls.append(PendingCall {
288 288
        index: e.codeLen,
289 289
        target,
290 -
    };
291 -
    set e.pendingCallsLen += 1;
290 +
    }, e.allocator);
292 291
293 292
    emit(e, encode::nop()); // Placeholder for AUIPC.
294 293
    emit(e, encode::nop()); // Placeholder for JALR.
295 294
}
296 295
296 +
/// Record a jump emitted by assembly that needs whole-program patching.
297 +
export fn recordJumpAt(e: *mut Emitter, target: *[u8], rd: gen::Reg, index: u32) {
298 +
    e.pendingJumps.append(PendingJump {
299 +
        index,
300 +
        target,
301 +
        rd,
302 +
    }, e.allocator);
303 +
}
304 +
297 305
/// Record a function address load needing later patching.
298 306
/// Emits placeholder instructions that will be patched to load the function's address.
299 307
/// Uses two slots to compute long-distance addresses.
300 308
export fn recordAddrLoad(e: *mut Emitter, target: *[u8], rd: gen::Reg) {
301 -
    assert e.pendingAddrLoadsLen < e.pendingAddrLoads.len, "recordAddrLoad: buffer full";
302 -
    set e.pendingAddrLoads[e.pendingAddrLoadsLen] = PendingAddrLoad {
309 +
    e.pendingAddrLoads.append(PendingAddrLoad {
303 310
        index: e.codeLen,
304 311
        target,
305 312
        rd: rd,
306 313
        isData: false,
307 -
    };
308 -
    set e.pendingAddrLoadsLen += 1;
314 +
    }, e.allocator);
309 315
310 316
    emit(e, encode::nop()); // Placeholder for AUIPC.
311 317
    emit(e, encode::nop()); // Placeholder for ADDI.
312 318
}
313 319
320 +
/// Record a function address load already reserved by assembly.
321 +
export fn recordAddrLoadAt(e: *mut Emitter, target: *[u8], rd: gen::Reg, index: u32) {
322 +
    e.pendingAddrLoads.append(PendingAddrLoad {
323 +
        index,
324 +
        target,
325 +
        rd: rd,
326 +
        isData: false,
327 +
    }, e.allocator);
328 +
}
329 +
314 330
/// Record a data address load needing later patching.
315 331
/// Uses an absolute 32-bit load sequence matching the current data memory map.
316 332
export fn recordDataAddrLoad(e: *mut Emitter, target: *[u8], rd: gen::Reg) {
317 -
    assert e.pendingAddrLoadsLen < e.pendingAddrLoads.len, "recordDataAddrLoad: buffer full";
318 -
    set e.pendingAddrLoads[e.pendingAddrLoadsLen] = PendingAddrLoad {
333 +
    e.pendingAddrLoads.append(PendingAddrLoad {
319 334
        index: e.codeLen,
320 335
        target,
321 336
        rd: rd,
322 337
        isData: true,
323 -
    };
324 -
    set e.pendingAddrLoadsLen += 1;
338 +
    }, e.allocator);
325 339
326 340
    emit(e, encode::nop()); // Placeholder for LUI.
327 341
    emit(e, encode::nop()); // Placeholder for ADDIW.
328 342
}
329 343
332 346
/// Called after each function.
333 347
///
334 348
/// Uses two-instruction sequences: short branches use `branch` and `nop`,
335 349
/// long branches use inverted branch  and `jal` or `auipc` and `jalr`.
336 350
export fn patchLocalBranches(e: *mut Emitter) {
337 -
    for i in 0..e.pendingBranchesLen {
351 +
    for i in 0..e.pendingBranches.len {
338 352
        let p = e.pendingBranches[i];
339 353
        let offset = labels::branchToBlock(&e.labels, p.index, p.target, super::INSTR_SIZE);
340 354
        match p.kind {
341 355
            case BranchKind::Cond { op, rs1, rs2 } => {
342 356
                if encode::isBranchImm(offset) {
363 377
                assert encode::isJumpImm(offset), "patchLocalBranches: jump offset too large";
364 378
                patch(e, p.index, encode::jal(super::ZERO, offset));
365 379
            },
366 380
        }
367 381
    }
368 -
    set e.pendingBranchesLen = 0;
382 +
    set e.pendingBranches = @sliceOf(e.pendingBranches.ptr, 0, e.pendingBranches.cap);
369 383
}
370 384
371 385
/// Encode a conditional branch instruction.
372 386
fn encodeCondBranch(op: il::CmpOp, rs1: gen::Reg, rs2: gen::Reg, offset: i32) -> u32 {
373 387
    match op {
389 403
}
390 404
391 405
/// Patch all pending function calls.
392 406
/// Called after all functions have been generated.
393 407
export fn patchCalls(e: *mut Emitter) {
394 -
    for i in 0..e.pendingCallsLen {
408 +
    for i in 0..e.pendingCalls.len {
395 409
        let p = e.pendingCalls[i];
396 410
        let offset = branchOffsetToFunc(e, p.index, p.target);
397 411
        let s = splitImm(offset);
398 412
399 413
        // `AUIPC scratch, hi(offset)`.
401 415
        // `JALR ra, scratch, lo(offset)`.
402 416
        patch(e, p.index + 1, encode::jalr(super::RA, super::SCRATCH1, s.lo));
403 417
    }
404 418
}
405 419
420 +
/// Patch all pending assembly jumps.
421 +
export fn patchJumps(e: *mut Emitter) {
422 +
    for i in 0..e.pendingJumps.len {
423 +
        let p = e.pendingJumps[i];
424 +
        let offset = branchOffsetToFunc(e, p.index, p.target);
425 +
426 +
        assert encode::isJumpImm(offset), "patchJumps: jump offset too large";
427 +
        patch(e, p.index, encode::jal(p.rd, offset));
428 +
    }
429 +
}
430 +
406 431
/// Patch all pending function and data address loads.
407 432
/// Called after all functions have been generated and data layout is known.
408 433
export fn patchAddrLoads(e: *mut Emitter, dataSymMap: *data::DataSymMap) {
409 -
    for i in 0..e.pendingAddrLoadsLen {
434 +
    for i in 0..e.pendingAddrLoads.len {
410 435
        let p = e.pendingAddrLoads[i];
411 436
        if p.isData {
412 437
            let addr = data::lookupAddr(dataSymMap, p.target) else {
413 438
                panic "patchAddrLoads: data symbol not found";
414 439
            };
688 713
/// Get emitted code as a slice.
689 714
export fn getCode(e: *Emitter) -> *[u32] {
690 715
    return &e.code[..e.codeLen];
691 716
}
692 717
693 -
/// Get function addresses for printing.
694 -
export fn getFuncs(e: *Emitter) -> *[types::FuncAddr] {
695 -
    return &e.funcs[..e.funcsLen];
696 -
}
697 -
698 718
/// Record a debug entry mapping the current PC to a source location.
699 719
/// Deduplicates consecutive entries with the same location.
700 720
export fn recordSrcLoc(e: *mut Emitter, loc: il::SrcLoc) {
701 721
    let pc = e.codeLen * super::INSTR_SIZE as u32;
702 722
lib/std/arch/rv64/tests.rad +6 -1
41 41
        super::ProgramOptions { entryPatch: super::EntryPatch::None, debug: false },
42 42
        &mut arena
43 43
    );
44 44
    super::addAssembly(
45 45
        &mut generator,
46 -
        asm::Program { text: &ASSEMBLY_TEXT_STORAGE[..], data: &[], symbols: symbolSlice }
46 +
        asm::Program {
47 +
            text: &ASSEMBLY_TEXT_STORAGE[..],
48 +
            data: &[],
49 +
            symbols: symbolSlice,
50 +
            externalFixups: &[],
51 +
        }
47 52
    );
48 53
49 54
    try testing::expect(dict::get(&generator.e.labels.funcs, "local") == nil);
50 55
    let exportedOffset = dict::get(&generator.e.labels.funcs, "exported") else {
51 56
        throw testing::TestError::Failed;
test/runner.rad +9 -1
216 216
        io::printError("error: assembly failed: ");
217 217
        io::printError(sourcePath);
218 218
        io::printError("\n");
219 219
        return false;
220 220
    };
221 -
    if not writeCode(program.text, outputPath) {
221 +
    // A standalone assembly binary has no whole-program emitter to resolve
222 +
    // external text references, so they are undefined symbols here.
223 +
    if program.externalFixups.len > 0 {
224 +
        io::printError("error: undefined symbol: ");
225 +
        io::printError(program.externalFixups[0].symbol);
226 +
        io::printError("\n");
227 +
        return false;
228 +
    }
229 +
    if not writeImage(program.text, program.data, &[], outputPath) {
222 230
        io::printError("error: could not write output: ");
223 231
        io::printError(outputPath);
224 232
        io::printError("\n");
225 233
        return false;
226 234
    }
test/tests/start.default.rad added +5 -0
1 +
//! returns: 43
2 +
3 +
@default fn main() -> i32 {
4 +
    return 43;
5 +
}
test/tests/start.default.start.ras added +2 -0
1 +
.text;
2 +
	tail   @"::default";
test/tests/start.exit.rad added +5 -0
1 +
//! returns: 37
2 +
3 +
fn unused() -> i32 {
4 +
    return 1;
5 +
}
test/tests/start.exit.start.ras added +4 -0
1 +
.text;
2 +
	li     %a0    37;
3 +
	li     %a7    93;
4 +
	ecall;