kernel: execute source-built binary images through the shared backend

14a97c541f8c53d773c51cd00bce8b3ecd15fc62f58d06d124c46c69bc0b6cd1
Verified: make -C kernel check; make std-test bin-test; seven binary catalogs execute in U-mode, including bounds faults, Page access, and event consumption.
Alexis Sellier committed ago 1 parent 55538f16
.gitignore +1 -0
14 14
# Compiler/build output
15 15
/bin
16 16
*.rv64
17 17
*.rv64.debug
18 18
*.o
19 +
/kernel/build/
19 20
20 21
# Seed binary (tracked, but not stage intermediates)
21 22
!seed/
22 23
!seed/*
23 24
seed/radiance.rv64.s*
Makefile +1 -1
36 36
endif
37 37
38 38
# Compiler build
39 39
40 40
SEED      := seed/radiance.rv64
41 -
COMPILER_SOURCES := compiler/radiance.rad compiler/radiance/binary.rad
41 +
COMPILER_SOURCES := compiler/radiance.rad $(wildcard compiler/radiance/*.rad compiler/radiance/*/*.rad)
42 42
SEED_OPTS := $(STD) -pkg radiance $(patsubst %,-mod %,$(COMPILER_SOURCES)) -entry radiance
43 43
44 44
$(RAD_BIN): $(STD_LIB) $(COMPILER_SOURCES) | $(BIN_DIR)
45 45
	@echo "radiance $(SEED) => $@"
46 46
	@$(EMU) $(EMU_FLAGS) -run $(SEED) $(SEED_OPTS) -o $@
compiler/radiance.rad +87 -12
1 1
//! Radiance compiler front-end.
2 2
3 3
/// Binary RIL file output and native loading.
4 4
mod binary;
5 +
/// Trusted binary image catalogs linked into a native program.
6 +
mod images;
5 7
use std::mem;
6 8
use std::fmt;
7 9
use std::io;
8 10
use std::lang::alloc;
9 11
use std::lang::ast;
35 37
constant MAX_SOURCES_SIZE: u32 = 2097152;
36 38
/// Maximum number of test functions we can discover.
37 39
constant MAX_TESTS: u32 = 1024;
38 40
/// Maximum number of assembly source paths we can load per package.
39 41
constant MAX_ASM_MODULES: u32 = 64;
42 +
/// Maximum binary image inputs linked into a native catalog.
43 +
constant MAX_IMAGES: u32 = 64;
40 44
41 45
/// AST arena size (32 MB) - retains parsed nodes throughout compilation.
42 46
constant TEMP_ARENA_SIZE: u32 = 33554432;
43 47
/// Per-function lowering and register-allocation arena size (16 MB).
44 48
constant FN_ARENA_SIZE: u32 = 16777216;
97 101
/// Symbol name exported for startup code to call the semantic entry function.
98 102
constant DEFAULT_ENTRY_SYMBOL: *[u8] = "::default";
99 103
100 104
/// Usage string.
101 105
constant USAGE: *[u8] =
102 -
    "usage: radiance -pkg <name> [-start <input.ras>] -mod <input>.. [-pkg <name> -mod <input>..] -entry <pkg> [-emit ril] -o <output>\n       radiance -load <input.ril> [-start <input.ras>] [-mod <input.ras>] -o <output>\n";
106 +
    "usage: radiance -pkg <name> [-start <input.ras>] -mod <input>.. [-pkg <name> -mod <input>..] -entry <pkg> [-emit ril] [-image <input.ril>] -o <output>\n       radiance -load <input.ril> [-start <input.ras>] [-mod <input.ras>] -o <output>\n       radiance -catalog <output.rad> -image <input.ril>..\n";
103 107
104 108
/// Compiler error.
105 109
union Error: Copy {
106 110
    Other,
107 111
}
164 168
    outputPath: ?*[u8],
165 169
    /// Whether to emit debug info (.debug file).
166 170
    debug: bool,
167 171
    /// Write binary RIL instead of native instructions.
168 172
    emitIl: bool,
173 +
    /// Ordered trusted binary image inputs for native linking.
174 +
    imagePaths: [*[u8]; MAX_IMAGES],
175 +
    /// Number of binary image paths.
176 +
    imageCount: u32,
177 +
    /// Include zero-initialized storage in the native image's data sections.
178 +
    zeroBss: bool,
169 179
}
170 180
181 +
/// Stable driver input storage for the single compilation invocation.
182 +
static CONTEXT: CompileContext = undefined;
183 +
171 184
/// Root module info for a package.
172 185
record RootModule: Copy {
173 186
    entry: *module::ModuleEntry,
174 187
    ast: *mut ast::Node,
175 188
}
305 318
        throw error(msg);
306 319
    }
307 320
    return args[*idx];
308 321
}
309 322
310 -
/// Parse CLI arguments and return compilation context.
323 +
/// Write a catalog declaration from the ordered binary image inputs.
324 +
fn catalogCommand(args: *[*[u8]]) -> i32 {
325 +
    if args.len < 4 {
326 +
        let _error = error(&["`-catalog` requires an output path and image inputs"]);
327 +
        return 1;
328 +
    }
329 +
    let mut paths: [*[u8]; MAX_IMAGES] = undefined;
330 +
    let mut count: u32 = 0;
331 +
    let mut index: u32 = 2;
332 +
    while index < args.len {
333 +
        if not mem::eq(args[index], "-image") or index + 1 >= args.len or count == MAX_IMAGES {
334 +
            let _error = error(&["invalid catalog image arguments"]);
335 +
            return 1;
336 +
        }
337 +
        set paths[count] = args[index + 1];
338 +
        set count += 1;
339 +
        set index += 2;
340 +
    }
341 +
    try images::catalog(&paths[..count], args[1], &mut STRING_POOL) catch e {
342 +
        let _error = error(&[e.message]);
343 +
        return 1;
344 +
    };
345 +
    return 0;
346 +
}
347 +
348 +
/// Parse CLI arguments and initialize the caller's compilation context.
311 349
fn processCommand(
312 350
    args: *[*[u8]],
313 -
    arena: *mut ast::NodeArena
314 -
) -> CompileContext throws (Error) {
351 +
    arena: *mut ast::NodeArena,
352 +
    ctx: *mut CompileContext
353 +
) throws (Error) {
315 354
    let mut buildTest = false;
316 355
    let mut debugEnabled = false;
317 356
    let mut outputPath: ?*[u8] = nil;
318 357
    let mut dump = Dump::None;
319 358
    let mut entryPkgName: ?*[u8] = nil;
320 359
    let mut emitIl = false;
360 +
    let mut zeroBss = false;
361 +
    let mut imagePaths: [*[u8]; MAX_IMAGES] = undefined;
362 +
    let mut imageCount: u32 = 0;
321 363
322 364
    // Per-package source path tracking.
323 365
    let mut inputs: [PackageInput; MAX_PACKAGES] = undefined;
324 366
    let mut pkgCount: u32 = 0;
325 367
    let mut currentPkgIdx: ?u32 = nil;
377 419
            set entryPkgName = args[idx];
378 420
        } else if mem::eq(arg, "-test") {
379 421
            set buildTest = true;
380 422
        } else if mem::eq(arg, "-debug") {
381 423
            set debugEnabled = true;
424 +
        } else if mem::eq(arg, "-zero-bss") {
425 +
            set zeroBss = true;
382 426
        } else if mem::eq(arg, "-o") {
383 427
            try nextArg(args, &mut idx, &["`-o` requires an output path"]);
384 428
            set outputPath = args[idx];
429 +
        } else if mem::eq(arg, "-image") {
430 +
            let path = try nextArg(args, &mut idx, &["`-image` requires a binary RIL path"]);
431 +
            if imageCount == MAX_IMAGES { throw error(&["too many binary image inputs"]); }
432 +
            set imagePaths[imageCount] = path;
433 +
            set imageCount += 1;
385 434
        } else if mem::eq(arg, "-emit") {
386 435
            let mode = try nextArg(args, &mut idx, &["`-emit` requires `ril`"]);
387 436
            if not mem::eq(mode, "ril") {
388 437
                throw error(&["unknown output format", mode, "(expected: ril)"]);
389 438
            }
408 457
        set idx += 1;
409 458
    }
410 459
    if emitIl and (outputPath == nil or dump <> Dump::None) {
411 460
        throw error(&["`-emit ril` requires `-o` and cannot be combined with `-dump`"]);
412 461
    }
462 +
    if (imageCount <> 0 or zeroBss) and (emitIl or (dump <> Dump::None and dump <> Dump::Asm)) {
463 +
        throw error(&["binary images and `-zero-bss` require native output"]);
464 +
    }
413 465
    if pkgCount == 0 {
414 466
        throw error(&["no package specified"]);
415 467
    }
416 468
    for i in 0..pkgCount {
417 469
        if inputs[i].radPathCount == 0 {
449 501
        if i <> entryIdx and inputs[i].startupPath <> nil {
450 502
            throw error(&["`-start` is only supported on the entry package"]);
451 503
        }
452 504
    }
453 505
    let graph = module::moduleGraph(&mut MODULE_ENTRIES[..], &mut STRING_POOL, arena);
454 -
    let mut ctx = CompileContext {
506 +
    set *ctx = CompileContext {
455 507
        packages: undefined,
456 508
        inputs,
457 509
        packageCount: pkgCount,
458 510
        entryPkgIdx,
459 511
        graph,
460 512
        config: resolver::Config { buildTest },
461 513
        dump,
462 514
        outputPath,
463 515
        debug: debugEnabled,
464 516
        emitIl,
517 +
        imagePaths,
518 +
        imageCount,
519 +
        zeroBss,
465 520
    };
466 521
    // Initialize and parse all packages.
467 522
    let mut sourceArena = alloc::new(&mut MODULE_SOURCES[..]);
468 523
    for i in 0..pkgCount {
469 524
        package::init(&mut ctx.packages[i], i as u16, ctx.inputs[i].name, &mut STRING_POOL);
471 526
        for j in 0..ctx.inputs[i].radPathCount {
472 527
            let path = ctx.inputs[i].radPaths[j];
473 528
            try processModule(&mut ctx.packages[i], &mut ctx.graph, path, arena, &mut sourceArena);
474 529
        }
475 530
    }
476 -
    return ctx;
477 531
}
478 532
479 533
/// Get the entry package from the context.
480 534
fn getEntryPackage(ctx: *CompileContext) -> *package::Package throws (Error) {
481 535
    let entryIdx = ctx.entryPkgIdx else {
1051 1105
    if let path = startupPath {
1052 1106
        try assembleAsmModule(&mut generator, entryPkg, path, &mut asmDataLen, &mut res.arena);
1053 1107
    }
1054 1108
    try lowerAllPackagesInto(ctx, res, &mut low);
1055 1109
    let asmData = try assembleAsmInputs(ctx, &mut generator, &mut asmDataLen, &mut res.arena);
1110 +
    try images::append(&mut generator, &ctx.imagePaths[..ctx.imageCount], &mut STRING_POOL,
1111 +
        &mut res.arena, fnArena, &mut low.data) catch e {
1112 +
        throw error(&[e.message]);
1113 +
    };
1114 +
    if ctx.zeroBss {
1115 +
        for i in 0..low.data.len {
1116 +
            let item = &mut low.data[i];
1117 +
            if not item.isZeroInit { continue; }
1118 +
            let value = try! alloc::alloc(&mut res.arena, @sizeOf(il::DataValue), @alignOf(il::DataValue)) as *mut il::DataValue;
1119 +
            set *value = il::DataValue { item: il::DataItem::Undef, count: item.size };
1120 +
            set item.values = @sliceOf(value, 1);
1121 +
            set item.isZeroInit = false;
1122 +
        }
1123 +
    }
1124 +
    try binary::layout(&low.data[..], asmData.len) catch e {
1125 +
        throw error(&[e.message]);
1126 +
    };
1056 1127
1057 1128
    match generator.entryPatch {
1058 1129
        case rv64::EntryPatch::Reserved(targetName) => {
1059 1130
            if targetName == nil {
1060 1131
                throw error(&["fatal:", "no default function found"]);
1136 1207
1137 1208
@default fn main(env: *sys::Env) -> i32 {
1138 1209
    if env.args.len > 0 and mem::eq(env.args[0], "-load") {
1139 1210
        return binary::run(env.args, &mut STRING_POOL);
1140 1211
    }
1212 +
    if env.args.len > 0 and mem::eq(env.args[0], "-catalog") {
1213 +
        return catalogCommand(env.args);
1214 +
    }
1141 1215
    let mut arena = ast::nodeArena(&mut TEMP_ARENA[..]);
1142 -
    let mut ctx = try processCommand(env.args, &mut arena) catch {
1216 +
    let ctx = &mut CONTEXT;
1217 +
    try processCommand(env.args, &mut arena, ctx) catch {
1143 1218
        return 1;
1144 1219
    };
1145 1220
    match ctx.dump {
1146 1221
        case Dump::Ast => {
1147 -
            try dumpAst(&ctx) catch {
1222 +
            try dumpAst(ctx) catch {
1148 1223
                return 1;
1149 1224
            };
1150 1225
            return 0;
1151 1226
        }
1152 1227
        case Dump::Graph => {
1153 -
            dumpGraph(&ctx);
1228 +
            dumpGraph(ctx);
1154 1229
            return 0;
1155 1230
        }
1156 1231
        else => {}
1157 1232
    }
1158 1233
    // Generate test runner if in test mode.
1159 1234
    if ctx.config.buildTest {
1160 -
        try generateTestRunner(&mut ctx, &mut arena) catch {
1235 +
        try generateTestRunner(ctx, &mut arena) catch {
1161 1236
            return 1;
1162 1237
        };
1163 1238
    }
1164 1239
    // Run resolution phase.
1165 -
    let mut res = try runResolver(&mut ctx, arena.nextId) catch {
1240 +
    let mut res = try runResolver(ctx, arena.nextId) catch {
1166 1241
        return 1;
1167 1242
    };
1168 1243
    let mut fnArena = alloc::new(&mut FN_ARENA[..]);
1169 1244
1170 1245
    // Lower, dump, and/or generate output.
1171 -
    try compile(&mut ctx, &mut res, &mut fnArena) catch {
1246 +
    try compile(ctx, &mut res, &mut fnArena) catch {
1172 1247
        return 1;
1173 1248
    };
1174 1249
    return 0;
1175 1250
}
compiler/radiance/binary.rad +13 -5
92 92
    if result.output.len == 0 { throw error("-load requires -o"); }
93 93
    return result;
94 94
}
95 95
96 96
/// Check section capacity before the backend computes native addresses.
97 -
fn layout(program: *il::Program, prefix: u32) throws (il::binary::Error) {
98 -
    if program.data.len > data::MAX_DATA_SYMS { throw error("too many data symbols"); }
97 +
export fn layout(items: *[il::Data], prefix: u32) throws (il::binary::Error) {
98 +
    if items.len > data::MAX_DATA_SYMS { throw error("too many data symbols"); }
99 99
    for section in 0..2 {
100 100
        let readOnly = section == 0;
101 101
        let mut offset = prefix as u64 if readOnly else 0 as u64;
102 +
        let base = rv64::RO_DATA_BASE if readOnly else rv64::RW_DATA_BASE;
103 +
        if offset > MAX_DATA as u64 { throw error("native data section exceeds capacity"); }
102 104
        for pass in 0..2 {
103 -
            for item in program.data {
105 +
            for item in items {
104 106
                if item.readOnly <> readOnly or item.isZeroInit <> (pass == 1) { continue; }
105 107
                let alignment = item.alignment as u64;
106 108
                if alignment == 0 or alignment & (alignment - 1) <> 0 {
107 109
                    throw error("invalid data alignment");
108 110
                }
109 111
                set offset = ((offset + alignment - 1) & ~(alignment - 1)) + item.size as u64;
110 -
                if offset > MAX_DATA as u64 { throw error("native data section exceeds capacity"); }
112 +
                if (not item.isZeroInit or readOnly) and offset > MAX_DATA as u64 {
113 +
                    throw error("native data section exceeds capacity");
114 +
                }
115 +
                // Native data address loads use signed 32-bit immediates.
116 +
                if offset + base as u64 > 0x7fffffff {
117 +
                    throw error("native data address exceeds capacity");
118 +
                }
111 119
            }
112 120
        }
113 121
    }
114 122
}
115 123
182 190
    if let previous = dict::get(&generator.e.labels.funcs, "::default") {
183 191
        if previous <> entryOffset { throw error("conflicting startup entry symbol"); }
184 192
    }
185 193
    emit::recordFuncOffsetAt(&mut generator.e, "::default", entryOffset as u32 / rv64::INSTR_SIZE as u32);
186 194
    try link(&generator, &image.program);
187 -
    try layout(&image.program, prefix);
195 +
    try layout(image.program.data, prefix);
188 196
    let result = rv64::finishProgram(&mut generator, image.program.data,
189 197
        rv64::Storage { dataSyms: &mut DATA_SYMBOLS[..], dataSymEntries: &mut DATA_INDEX[..] },
190 198
        &ASM_PREFIX[..prefix], &mut RO_DATA[..], &mut RW_DATA[..]);
191 199
    let code = @sliceOf(result.code.ptr as *u8, result.code.len * rv64::INSTR_SIZE as u32);
192 200
    let header = rv64::imageHeader(code.len, result.roDataSize, result.rwDataSize);
compiler/radiance/images.rad added +164 -0
1 +
//! Binary RIL image catalogs and native image append through the shared backend.
2 +
3 +
mod graph;
4 +
mod native;
5 +
6 +
use std::mem;
7 +
use std::fmt;
8 +
use std::sys::unix;
9 +
use std::collections::dict;
10 +
use std::lang::alloc;
11 +
use std::lang::strings;
12 +
use std::lang::il;
13 +
use std::arch::rv64;
14 +
15 +
/// Maximum input bytes, plus a sentinel byte to detect oversized files.
16 +
constant MAX_IMAGE: u32 = 16 * 1024 * 1024;
17 +
/// Catalog order is image identity; image zero is the boot root.
18 +
constant MAX_IMAGES: u32 = 64;
19 +
/// Bounded reusable binary input, borrowed only until the next image is loaded.
20 +
static INPUT: [u8; MAX_IMAGE + 1] = undefined;
21 +
/// Decoding names never escape into the caller's persistent interning pool.
22 +
static DECODE_POOL: strings::Pool = strings::Pool { table: undefined, count: 0 };
23 +
/// Catalog-only graph storage, reclaimed after every image's placement is known.
24 +
static CATALOG_ARENA: [u8; 48 * 1024 * 1024] = undefined;
25 +
/// Generated trusted Radiance source, independent of decode scratch storage.
26 +
static SOURCE: [u8; 256 * 1024] = undefined;
27 +
28 +
/// Bounded source writer for the small trusted catalog module.
29 +
record Text: Copy { bytes: *mut [u8], len: u32 }
30 +
31 +
/// Append static text without exposing allocator or formatting assertions.
32 +
fn put(text: *mut Text, bytes: *[u8]) throws (il::binary::Error) {
33 +
    if bytes.len > text.bytes.len - text.len { throw il::binary::error(0, "image catalog source exceeds capacity"); }
34 +
    try! mem::copy(&mut text.bytes[text.len..], bytes);
35 +
    set text.len += bytes.len;
36 +
}
37 +
38 +
/// Append one decimal constant or trusted helper-name suffix.
39 +
fn number(text: *mut Text, value: u32) throws (il::binary::Error) {
40 +
    let mut digits: [u8; 10] = undefined;
41 +
    try put(text, fmt::formatU32(value, &mut digits[..]));
42 +
}
43 +
44 +
/// Read only one bounded input; decoder-owned names remain local to this image.
45 +
fn load(path: *[u8], arena: *mut alloc::Arena) -> il::binary::Image throws (il::binary::Error) {
46 +
    for i in 0..DECODE_POOL.table.len { set DECODE_POOL.table[i] = ""; }
47 +
    set DECODE_POOL.count = 0;
48 +
    let source = unix::readFile(path, &mut INPUT[..]) else { throw il::binary::error(0, "cannot read binary image"); };
49 +
    if source.len > MAX_IMAGE { throw il::binary::error(0, "binary image exceeds capacity"); }
50 +
    return try il::binary::decode(source, arena, &mut DECODE_POOL);
51 +
}
52 +
53 +
/// Require a real root and bound IDs before catalog arrays or name generation.
54 +
fn checkPaths(paths: *[*[u8]]) throws (il::binary::Error) {
55 +
    if paths.len == 0 { throw il::binary::error(0, "image catalog requires a root image"); }
56 +
    if paths.len > MAX_IMAGES { throw il::binary::error(0, "too many catalog images"); }
57 +
}
58 +
59 +
/// Write the trusted source catalog. The caller pool is deliberately not populated
60 +
/// with names borrowing the temporary binary input; only numeric metadata escapes.
61 +
export fn catalog(paths: *[*[u8]], output: *[u8], pool: *mut strings::Pool) throws (il::binary::Error) {
62 +
    try checkPaths(paths);
63 +
    let _ = pool;
64 +
    let mut sizes: [u32; MAX_IMAGES] = undefined;
65 +
    let mut alignments: [u32; MAX_IMAGES] = undefined;
66 +
    let mut arena = alloc::new(&mut CATALOG_ARENA[..]);
67 +
    for path, id in paths {
68 +
        let image = try load(path, &mut arena);
69 +
        let plan = try graph::plan(&image, &mut arena);
70 +
        set sizes[id] = plan.size;
71 +
        set alignments[id] = plan.alignment;
72 +
        alloc::reset(&mut arena);
73 +
    }
74 +
    let mut text = Text { bytes: &mut SOURCE[..], len: 0 };
75 +
    try put(&mut text, "//! Trusted binary image catalog. Catalog order is image identity.\n\n");
76 +
    for id in 0..paths.len {
77 +
        try put(&mut text, "fn entry_"); try number(&mut text, id); try put(&mut text, "() -> u64;\n");
78 +
        try put(&mut text, "fn initial_"); try number(&mut text, id); try put(&mut text, "() -> u64;\n");
79 +
        try put(&mut text, "fn relocate_"); try number(&mut text, id); try put(&mut text, "(base: u64);\n");
80 +
    }
81 +
    try put(&mut text, "\n/// Number of immutable image descriptors.\nexport constant COUNT: u32 = ");
82 +
    try number(&mut text, paths.len);
83 +
    try put(&mut text, ";\n/// The first command-line image is the boot root.\nexport constant ROOT: u32 = 0;\n\n");
84 +
    try put(&mut text, "/// Shared code and initializer with separately allocated private state.\nexport record Descriptor: Copy {\n");
85 +
    try put(&mut text, "    /// Native entry accepting the context Env pointer.\n    entry: u64,\n");
86 +
    try put(&mut text, "    /// Shared read-only bytes copied into each domain.\n    initial: u64,\n");
87 +
    try put(&mut text, "    /// Bytes required by private image state.\n    stateSize: u32,\n");
88 +
    try put(&mut text, "    /// Power-of-two alignment of the private allocation.\n    stateAlignment: u32,\n");
89 +
    try put(&mut text, "    /// Fix private pointers after copying initial bytes to base.\n    relocate: fn(u64),\n}\n\n");
90 +
    try put(&mut text, "/// Return linked addresses; out-of-range indices are kernel bugs.\nexport fn get(index: u32) -> Descriptor {\n    assert index < COUNT;\n");
91 +
    for id in 0..paths.len {
92 +
        if id + 1 < paths.len { try put(&mut text, "    if index == "); try number(&mut text, id); try put(&mut text, " {\n"); }
93 +
        let indent = "        " if id + 1 < paths.len else "    ";
94 +
        try put(&mut text, indent); try put(&mut text, "return Descriptor {\n");
95 +
        try put(&mut text, indent); try put(&mut text, "    entry: entry_"); try number(&mut text, id); try put(&mut text, "(),\n");
96 +
        try put(&mut text, indent); try put(&mut text, "    initial: initial_"); try number(&mut text, id); try put(&mut text, "(),\n");
97 +
        try put(&mut text, indent); try put(&mut text, "    stateSize: "); try number(&mut text, sizes[id]); try put(&mut text, ",\n");
98 +
        try put(&mut text, indent); try put(&mut text, "    stateAlignment: "); try number(&mut text, alignments[id]); try put(&mut text, ",\n");
99 +
        try put(&mut text, indent); try put(&mut text, "    relocate: relocate_"); try number(&mut text, id); try put(&mut text, ",\n");
100 +
        try put(&mut text, indent); try put(&mut text, "};\n");
101 +
        if id + 1 < paths.len { try put(&mut text, "    }\n"); }
102 +
    }
103 +
    try put(&mut text, "}\n");
104 +
    if not unix::writeFile(output, &SOURCE[..text.len]) { throw il::binary::error(0, "cannot write image catalog source"); }
105 +
}
106 +
107 +
/// Append one decoded graph. All stored data, names, and pending relocations are
108 +
/// copied into the persistent arena before scratch or input bytes are reused.
109 +
fn appendImage(generator: *mut rv64::Generator, path: *[u8], id: u32, pool: *mut strings::Pool,
110 +
    arena: *mut alloc::Arena, scratch: *mut alloc::Arena, dataItems: *mut *mut [il::Data]) throws (il::binary::Error) {
111 +
    let image = try load(path, scratch);
112 +
    let plan = try graph::plan(&image, scratch);
113 +
    for function in image.program.fns {
114 +
        if function.isExtern and dict::get(&generator.e.labels.funcs, function.name) == nil {
115 +
            throw il::binary::error(0, "unresolved image sys native declaration");
116 +
        }
117 +
    }
118 +
    let names = try graph::names(&image.program, id, pool, arena);
119 +
    let stateName = try graph::name("images::state_", id, "", pool, arena);
120 +
    let initial = try native::initializer(&plan, &names, stateName, arena, scratch);
121 +
    if (*dataItems).len as u64 + initial.items.len as u64 > 8192 {
122 +
        throw il::binary::error(0, "native data symbol capacity exceeded");
123 +
    }
124 +
    let mut definitions = try il::binary::dictionary(scratch, (*dataItems).len + initial.items.len, 0);
125 +
    for item in *dataItems { dict::insert(&mut definitions, item.name, 0); }
126 +
    for item in initial.items {
127 +
        if dict::get(&definitions, item.name) <> nil { throw il::binary::error(0, "duplicate native image data symbol"); }
128 +
        dict::insert(&mut definitions, item.name, 0);
129 +
    }
130 +
    try native::appendData(dataItems, initial.items, arena);
131 +
    let mut offsets = try il::binary::dictionary(scratch, plan.order.len, 0);
132 +
    for item, i in plan.items {
133 +
        if not item.readOnly { dict::insert(&mut offsets, names.data[i], plan.offsets[i] as i32); }
134 +
    }
135 +
    set generator.e.instanceData = &offsets;
136 +
    for function, i in image.program.fns {
137 +
        if function.isExtern { continue; }
138 +
        let renamed = try graph::function(function, i, &plan, &names, scratch);
139 +
        try native::function(generator, &renamed, scratch);
140 +
    }
141 +
    set generator.e.instanceData = nil;
142 +
    try native::helpers(generator, id, &plan, &names, &initial, pool, arena);
143 +
}
144 +
145 +
/// Append native images to the kernel's generator and persistent growable data.
146 +
/// The native sys assembly must already be present in the global symbol table.
147 +
/// Caller-owned function scratch is restored after each fully emitted image;
148 +
/// persistent data and relocation names remain valid until finishProgram.
149 +
export fn append(generator: *mut rv64::Generator, paths: *[*[u8]], pool: *mut strings::Pool,
150 +
    arena: *mut alloc::Arena, fnArena: *mut alloc::Arena, dataItems: *mut *mut [il::Data]) throws (il::binary::Error) {
151 +
    if paths.len == 0 { return; }
152 +
    try checkPaths(paths);
153 +
    let saved = alloc::save(fnArena);
154 +
    let previous = generator.e.instanceData;
155 +
    for path, id in paths {
156 +
        try appendImage(generator, path, id, pool, arena, fnArena, dataItems) catch error {
157 +
            set generator.e.instanceData = previous;
158 +
            alloc::restore(fnArena, saved);
159 +
            throw error;
160 +
        };
161 +
        set generator.e.instanceData = previous;
162 +
        alloc::restore(fnArena, saved);
163 +
    }
164 +
}
compiler/radiance/images/graph.rad added +312 -0
1 +
//! Closed binary image graphs, private-state placement, and symbol rewriting.
2 +
3 +
use std::mem;
4 +
use std::fmt;
5 +
use std::collections::dict;
6 +
use std::lang::alloc;
7 +
use std::lang::strings;
8 +
use std::lang::il;
9 +
use std::lang::gen::data;
10 +
11 +
/// Maximum bytes in an image's private state or one native data section.
12 +
export constant MAX_DATA: u32 = 4 * 1024 * 1024;
13 +
14 +
/// One reverse initializer edge used to promote address-bearing constants.
15 +
record Edge: Copy {
16 +
    /// Declaration containing the pointer initializer.
17 +
    owner: u32,
18 +
    /// Next reverse edge for the same target.
19 +
    next: ?u32,
20 +
}
21 +
22 +
/// Shared placement facts used by catalog generation and native emission.
23 +
export record Plan: Copy {
24 +
    /// Original data declaration indexes.
25 +
    dataIndex: dict::Dict,
26 +
    /// Original function declaration indexes.
27 +
    fnIndex: dict::Dict,
28 +
    /// Data declarations classified for the ordinary data layout routine.
29 +
    items: *mut [il::Data],
30 +
    /// Private byte offsets, indexed by the original data declaration.
31 +
    offsets: *mut [u32],
32 +
    /// Native layout order of the private declarations.
33 +
    order: *[data::DataSym],
34 +
    /// Number of private bytes.
35 +
    size: u32,
36 +
    /// Required private allocation alignment.
37 +
    alignment: u32,
38 +
    /// Entry function declaration index.
39 +
    entry: u32,
40 +
    /// The entry returns a status word.
41 +
    returnsStatus: bool,
42 +
}
43 +
44 +
/// Persistent names corresponding to the declaration indexes in Plan.
45 +
export record Names: Copy {
46 +
    /// Namespaced data declaration names.
47 +
    data: *[*[u8]],
48 +
    /// Namespaced function names and permitted native external names.
49 +
    functions: *[*[u8]],
50 +
}
51 +
52 +
/// Allocate through the binary codec's checked public arena interface.
53 +
export fn storage(arena: *mut alloc::Arena, size: u32, alignment: u32, count: u32) -> *mut [opaque] throws (il::binary::Error) {
54 +
    return try il::binary::storage(arena, size, alignment, count, 0);
55 +
}
56 +
57 +
/// Copy bytes whose lifetime would otherwise end with the decoded image.
58 +
export fn copy(arena: *mut alloc::Arena, bytes: *[u8]) -> *[u8] throws (il::binary::Error) {
59 +
    let result = try storage(arena, 1, 1, bytes.len) as *mut [u8];
60 +
    try! mem::copy(result, bytes);
61 +
    return result;
62 +
}
63 +
64 +
/// Intern persistent names without overflowing the caller's shared pool.
65 +
fn intern(pool: *mut strings::Pool, arena: *mut alloc::Arena, bytes: *[u8]) -> *[u8] throws (il::binary::Error) {
66 +
    if let existing = strings::find(pool, bytes) { return existing; }
67 +
    if pool.count >= pool.table.len / 2 { throw il::binary::error(0, "image string pool exhausted"); }
68 +
    return strings::intern(pool, try copy(arena, bytes));
69 +
}
70 +
71 +
/// Construct a trusted helper or namespace name with a decimal image ID.
72 +
export fn name(prefix: *[u8], id: u32, suffix: *[u8], pool: *mut strings::Pool, arena: *mut alloc::Arena) -> *[u8] throws (il::binary::Error) {
73 +
    let mut digits: [u8; 10] = undefined;
74 +
    let number = fmt::formatU32(id, &mut digits[..]);
75 +
    let size = prefix.len as u64 + number.len as u64 + suffix.len as u64;
76 +
    if size > 0x7FFFFFFF { throw il::binary::error(0, "image symbol is too long"); }
77 +
    let saved = alloc::save(arena);
78 +
    let bytes = try storage(arena, 1, 1, size as u32) as *mut [u8];
79 +
    let mut pos = try! mem::copy(bytes, prefix);
80 +
    set pos += try! mem::copy(&mut bytes[pos..], number);
81 +
    try! mem::copy(&mut bytes[pos..], suffix);
82 +
    if let existing = strings::find(pool, bytes) {
83 +
        alloc::restore(arena, saved);
84 +
        return existing;
85 +
    }
86 +
    if pool.count >= pool.table.len / 2 { throw il::binary::error(0, "image string pool exhausted"); }
87 +
    return strings::intern(pool, bytes);
88 +
}
89 +
90 +
/// Decode already established declaration references into an index.
91 +
export fn index(map: *dict::Dict, symbol: *[u8]) -> u32 throws (il::binary::Error) {
92 +
    let value = dict::get(map, symbol) else { throw il::binary::error(0, "unresolved image graph reference"); };
93 +
    return value as u32;
94 +
}
95 +
96 +
/// Width of one repetition, matching the shared data emitter.
97 +
export fn width(item: il::DataItem) -> u32 {
98 +
    match item {
99 +
        case il::DataItem::Val { typ, .. } => return il::typeSize(typ),
100 +
        case il::DataItem::Sym(_), il::DataItem::Fn(_) => return 8,
101 +
        case il::DataItem::Str(bytes) => return bytes.len,
102 +
        case il::DataItem::Undef => return 1,
103 +
    }
104 +
}
105 +
106 +
/// Classify transitive private pointers, then use the ordinary data layout order.
107 +
export fn plan(image: *il::binary::Image, arena: *mut alloc::Arena) -> Plan throws (il::binary::Error) {
108 +
    let program = &image.program;
109 +
    if program.data.len >= data::MAX_DATA_SYMS { throw il::binary::error(0, "too many image data symbols"); }
110 +
    if program.fns.len > 8192 { throw il::binary::error(0, "too many image functions"); }
111 +
    let mut dataIndex = try il::binary::dictionary(arena, program.data.len, 0);
112 +
    let mut fnIndex = try il::binary::dictionary(arena, program.fns.len, 0);
113 +
    let items = try storage(arena, @sizeOf(il::Data), @alignOf(il::Data), program.data.len) as *mut [il::Data];
114 +
    let offsets = try storage(arena, @sizeOf(u32), @alignOf(u32), items.len) as *mut [u32];
115 +
    let heads = try storage(arena, @sizeOf(?u32), @alignOf(?u32), items.len) as *mut [?u32];
116 +
    let queue = try storage(arena, @sizeOf(u32), @alignOf(u32), items.len) as *mut [u32];
117 +
    let mut queued: u32 = 0;
118 +
    let mut edgeCount: u64 = 0;
119 +
    for item, i in program.data {
120 +
        dict::insert(&mut dataIndex, item.name, i as i32);
121 +
        set items[i] = item;
122 +
        set offsets[i] = 0;
123 +
        set heads[i] = nil;
124 +
        if not item.readOnly { set queue[queued] = i; set queued += 1; }
125 +
        for value in item.values {
126 +
            if value.count == 0 { continue; }
127 +
            if let case il::DataItem::Sym(_) = value.item { set edgeCount += 1; }
128 +
        }
129 +
    }
130 +
    for function, i in program.fns {
131 +
        dict::insert(&mut fnIndex, function.name, i as i32);
132 +
        if function.isExtern and (function.name.len <= 11 or not mem::eq(&function.name[..11], "user::sys::")) {
133 +
            throw il::binary::error(0, "image extern must be a declared sys native function");
134 +
        }
135 +
    }
136 +
    if edgeCount > 0x7FFFFFFF { throw il::binary::error(0, "too many image initializer references"); }
137 +
    let edges = try storage(arena, @sizeOf(Edge), @alignOf(Edge), edgeCount as u32) as *mut [Edge];
138 +
    let mut edgeIndex: u32 = 0;
139 +
    for item, i in items {
140 +
        for value in item.values {
141 +
            if value.count == 0 { continue; }
142 +
            match value.item {
143 +
                case il::DataItem::Sym(symbol) => {
144 +
                    let target = try index(&dataIndex, symbol);
145 +
                    set edges[edgeIndex] = Edge { owner: i, next: heads[target] };
146 +
                    set heads[target] = edgeIndex;
147 +
                    set edgeIndex += 1;
148 +
                }
149 +
                case il::DataItem::Fn(symbol) => { let _ = try index(&fnIndex, symbol); }
150 +
                else => {},
151 +
            }
152 +
        }
153 +
    }
154 +
    let mut cursor: u32 = 0;
155 +
    while cursor < queued {
156 +
        let mut next = heads[queue[cursor]];
157 +
        while let e = next {
158 +
            let edge = edges[e];
159 +
            if items[edge.owner].readOnly {
160 +
                set items[edge.owner].readOnly = false;
161 +
                set queue[queued] = edge.owner;
162 +
                set queued += 1;
163 +
            }
164 +
            set next = edge.next;
165 +
        }
166 +
        set cursor += 1;
167 +
    }
168 +
    let mut alignment: u32 = 8;
169 +
    let mut expected: u64 = 0;
170 +
    for pass in 0..2 {
171 +
        for item in items {
172 +
            if item.readOnly or item.isZeroInit <> (pass == 1) { continue; }
173 +
            let a = item.alignment as u64;
174 +
            if a == 0 or a & (a - 1) <> 0 or a > MAX_DATA as u64 {
175 +
                throw il::binary::error(0, "image state alignment exceeds capacity");
176 +
            }
177 +
            if item.alignment > alignment { set alignment = item.alignment; }
178 +
            set expected = ((expected + a - 1) & ~(a - 1)) + item.size as u64;
179 +
            if expected > MAX_DATA as u64 { throw il::binary::error(0, "image private state exceeds capacity"); }
180 +
        }
181 +
    }
182 +
    let symbols = try storage(arena, @sizeOf(data::DataSym), @alignOf(data::DataSym), queued) as *mut [data::DataSym];
183 +
    let mut symbolCount: u32 = 0;
184 +
    let size = data::layoutSection(items, symbols, &mut symbolCount, 0, false);
185 +
    for symbol in symbols { set offsets[try index(&dataIndex, symbol.name)] = symbol.addr; }
186 +
    let entryName = image.entry else { throw il::binary::error(0, "image has no entry function"); };
187 +
    let entry = try index(&fnIndex, entryName);
188 +
    let function = program.fns[entry];
189 +
    if function.isExtern or function.params.len <> 1 or il::typeSize(function.params[0].type) <> 8 {
190 +
        throw il::binary::error(0, "image entry must receive one Env pointer");
191 +
    }
192 +
    let mut returnsStatus = false;
193 +
    let mut returnsVoid = false;
194 +
    for block in function.blocks {
195 +
        for instruction in block.instrs {
196 +
            if let case il::Instr::Ret { val } = instruction {
197 +
                if let value = val {
198 +
                    match value {
199 +
                        case il::Val::Undef => set returnsVoid = true,
200 +
                        else => set returnsStatus = true,
201 +
                    }
202 +
                } else { set returnsVoid = true; }
203 +
            }
204 +
        }
205 +
    }
206 +
    if returnsStatus and (returnsVoid or il::typeSize(function.returnType) <> 4) {
207 +
        throw il::binary::error(0, "image entry must return void or u32");
208 +
    }
209 +
    return Plan { dataIndex, fnIndex, items, offsets, order: symbols, size, alignment, entry, returnsStatus };
210 +
}
211 +
212 +
/// Allocate all names once; native externs alone retain their original spelling.
213 +
export fn names(program: *il::Program, id: u32, pool: *mut strings::Pool, arena: *mut alloc::Arena) -> Names throws (il::binary::Error) {
214 +
    let dataNames = try storage(arena, @sizeOf(*[u8]), @alignOf(*[u8]), program.data.len) as *mut [*[u8]];
215 +
    let fnNames = try storage(arena, @sizeOf(*[u8]), @alignOf(*[u8]), program.fns.len) as *mut [*[u8]];
216 +
    let prefix = try name("images::image_", id, "::", pool, arena);
217 +
    for item, i in program.data { set dataNames[i] = try qualified(prefix, item.name, pool, arena); }
218 +
    for function, i in program.fns {
219 +
        set fnNames[i] = try intern(pool, arena, function.name) if function.isExtern
220 +
            else try qualified(prefix, function.name, pool, arena);
221 +
    }
222 +
    return Names { data: dataNames, functions: fnNames };
223 +
}
224 +
225 +
/// Prefix an original whole-graph name, retaining its complete qualified path.
226 +
fn qualified(prefix: *[u8], suffix: *[u8], pool: *mut strings::Pool, arena: *mut alloc::Arena) -> *[u8] throws (il::binary::Error) {
227 +
    let size = prefix.len as u64 + suffix.len as u64;
228 +
    if size > 0x7FFFFFFF { throw il::binary::error(0, "image symbol is too long"); }
229 +
    let saved = alloc::save(arena);
230 +
    let bytes = try storage(arena, 1, 1, size as u32) as *mut [u8];
231 +
    let pos = try! mem::copy(bytes, prefix);
232 +
    try! mem::copy(&mut bytes[pos..], suffix);
233 +
    if let existing = strings::find(pool, bytes) { alloc::restore(arena, saved); return existing; }
234 +
    if pool.count >= pool.table.len / 2 { throw il::binary::error(0, "image string pool exhausted"); }
235 +
    return strings::intern(pool, bytes);
236 +
}
237 +
238 +
/// Rewrite a typed symbol reference, rejecting unresolved graph references.
239 +
fn val(value: il::Val, plan: *Plan, names: *Names) -> il::Val throws (il::binary::Error) {
240 +
    match value {
241 +
        case il::Val::DataSym(symbol) => return il::Val::DataSym(names.data[try index(&plan.dataIndex, symbol)]),
242 +
        case il::Val::FnAddr(symbol) => return il::Val::FnAddr(names.functions[try index(&plan.fnIndex, symbol)]),
243 +
        else => return value,
244 +
    }
245 +
}
246 +
247 +
/// Rewrite mutable block-edge arguments in their decoder-owned storage.
248 +
fn args(values: *mut [il::Val], plan: *Plan, names: *Names) throws (il::binary::Error) {
249 +
    for value, i in values { set values[i] = try val(value, plan, names); }
250 +
}
251 +
252 +
/// Copy immutable call arguments only when they contain a symbol to rename.
253 +
fn callArgs(values: *[il::Val], plan: *Plan, names: *Names, arena: *mut alloc::Arena) -> *[il::Val] throws (il::binary::Error) {
254 +
    for value in values {
255 +
        match value {
256 +
            case il::Val::DataSym(_), il::Val::FnAddr(_) => {
257 +
                let copied = try storage(arena, @sizeOf(il::Val), @alignOf(il::Val), values.len) as *mut [il::Val];
258 +
                for argument, i in values { set copied[i] = try val(argument, plan, names); }
259 +
                return copied;
260 +
            },
261 +
            else => {},
262 +
        }
263 +
    }
264 +
    return values;
265 +
}
266 +
267 +
/// Rewrite every symbolic operand without reconstructing any source-language types.
268 +
fn instruction(item: il::Instr, plan: *Plan, names: *Names, arena: *mut alloc::Arena) -> il::Instr throws (il::binary::Error) {
269 +
    match item {
270 +
        case il::Instr::Reserve { dst, size, alignment } => return il::Instr::Reserve { dst, size: try val(size, plan, names), alignment },
271 +
        case il::Instr::Store { typ, src, dst, offset } => return il::Instr::Store { typ, src: try val(src, plan, names), dst, offset },
272 +
        case il::Instr::Blit { dst, src, size, alignment } => return il::Instr::Blit { dst, src, size: try val(size, plan, names), alignment },
273 +
        case il::Instr::Copy { dst, val: value } => return il::Instr::Copy { dst, val: try val(value, plan, names) },
274 +
        case il::Instr::BinOp { op, typ, dst, a, b } => return il::Instr::BinOp { op, typ, dst, a: try val(a, plan, names), b: try val(b, plan, names) },
275 +
        case il::Instr::UnOp { op, typ, dst, a } => return il::Instr::UnOp { op, typ, dst, a: try val(a, plan, names) },
276 +
        case il::Instr::Zext { typ, dst, val: value } => return il::Instr::Zext { typ, dst, val: try val(value, plan, names) },
277 +
        case il::Instr::Sext { typ, dst, val: value } => return il::Instr::Sext { typ, dst, val: try val(value, plan, names) },
278 +
        case il::Instr::Call { retTy, dst, func, args: values } => {
279 +
            return il::Instr::Call { retTy, dst, func: try val(func, plan, names), args: try callArgs(values, plan, names, arena) };
280 +
        }
281 +
        case il::Instr::Ret { val: value } => {
282 +
            if let v = value { return il::Instr::Ret { val: try val(v, plan, names) }; }
283 +
            return item;
284 +
        }
285 +
        case il::Instr::Jmp { args: values, .. } => { try args(values, plan, names); return item; }
286 +
        case il::Instr::Br { op, typ, a, b, thenTarget, thenArgs, elseTarget, elseArgs } => {
287 +
            try args(thenArgs, plan, names); try args(elseArgs, plan, names);
288 +
            return il::Instr::Br { op, typ, a: try val(a, plan, names), b: try val(b, plan, names), thenTarget, thenArgs, elseTarget, elseArgs };
289 +
        }
290 +
        case il::Instr::Switch { val: value, defaultTarget, defaultArgs, cases } => {
291 +
            try args(defaultArgs, plan, names);
292 +
            for c in cases { try args(c.args, plan, names); }
293 +
            return il::Instr::Switch { val: try val(value, plan, names), defaultTarget, defaultArgs, cases };
294 +
        }
295 +
        case il::Instr::Ecall { dst, num, a0, a1, a2, a3 } => return il::Instr::Ecall {
296 +
            dst, num: try val(num, plan, names), a0: try val(a0, plan, names), a1: try val(a1, plan, names),
297 +
            a2: try val(a2, plan, names), a3: try val(a3, plan, names),
298 +
        },
299 +
        case il::Instr::Load { .. }, il::Instr::Sload { .. }, il::Instr::Unreachable,
300 +
             il::Instr::Ebreak, il::Instr::MemoryFence => return item,
301 +
    }
302 +
}
303 +
304 +
/// Rename a decoder-owned body in place; its storage survives until emission ends.
305 +
export fn function(function: *il::Fn, index: u32, plan: *Plan, names: *Names, arena: *mut alloc::Arena) -> il::Fn throws (il::binary::Error) {
306 +
    let mut result = *function;
307 +
    set result.name = names.functions[index];
308 +
    for block in result.blocks {
309 +
        for item, i in block.instrs { set block.instrs[i] = try instruction(item, plan, names, arena); }
310 +
    }
311 +
    return result;
312 +
}
compiler/radiance/images/native.rad added +343 -0
1 +
//! Shared-backend image code, read-only initializers, and trusted linker glue.
2 +
3 +
use std::collections::dict;
4 +
use std::lang::alloc;
5 +
use std::lang::strings;
6 +
use std::lang::il;
7 +
use std::lang::gen::data;
8 +
use std::lang::gen::bitset;
9 +
use std::lang::gen::regalloc;
10 +
use std::arch::rv64;
11 +
use std::arch::rv64::emit;
12 +
use std::arch::rv64::encode;
13 +
use super::graph;
14 +
15 +
/// One repeated private pointer initializer, expressed in state-relative bytes.
16 +
record Fixup: Copy {
17 +
    /// First pointer slot's private byte offset.
18 +
    offset: u32,
19 +
    /// Target declaration's private byte offset.
20 +
    target: u32,
21 +
    /// Number of consecutive eight-byte pointer slots.
22 +
    count: u32,
23 +
}
24 +
25 +
/// Persistent read-only definitions and scratch relocation runs.
26 +
export record Initializer: Copy {
27 +
    /// Shared initializer and immutable data definitions.
28 +
    items: *[il::Data],
29 +
    /// Private pointer runs applied to each new instance.
30 +
    fixups: *[Fixup],
31 +
}
32 +
33 +
/// Retain initializer payloads and keep ordinary RO/function relocations intact.
34 +
fn value(value: il::DataValue, plan: *graph::Plan, names: *graph::Names, arena: *mut alloc::Arena) -> il::DataValue throws (il::binary::Error) {
35 +
    let mut item = value.item;
36 +
    match item {
37 +
        case il::DataItem::Sym(symbol) => {
38 +
            let index = try graph::index(&plan.dataIndex, symbol);
39 +
            set item = il::DataItem::Sym(names.data[index]) if plan.items[index].readOnly
40 +
                else il::DataItem::Val { typ: il::Type::W64, val: plan.offsets[index] as i64 };
41 +
        }
42 +
        case il::DataItem::Fn(symbol) => set item = il::DataItem::Fn(names.functions[try graph::index(&plan.fnIndex, symbol)]),
43 +
        case il::DataItem::Str(bytes) => set item = il::DataItem::Str(try graph::copy(arena, bytes)),
44 +
        else => {},
45 +
    }
46 +
    return il::DataValue { item, count: value.count };
47 +
}
48 +
49 +
/// Flatten private declarations into one RO initializer using their native layout.
50 +
export fn initializer(plan: *graph::Plan, names: *graph::Names, stateName: *[u8], arena: *mut alloc::Arena, scratch: *mut alloc::Arena) -> Initializer throws (il::binary::Error) {
51 +
    let mut shared: u32 = 0;
52 +
    let mut count: u64 = plan.order.len as u64 * 2;
53 +
    let mut fixupCount: u64 = 0;
54 +
    for item in plan.items {
55 +
        if item.readOnly { set shared += 1; continue; }
56 +
        if item.isZeroInit { continue; }
57 +
        set count += item.values.len as u64;
58 +
        for v in item.values {
59 +
            if v.count == 0 { continue; }
60 +
            if let case il::DataItem::Sym(symbol) = v.item {
61 +
                if not plan.items[try graph::index(&plan.dataIndex, symbol)].readOnly { set fixupCount += 1; }
62 +
            }
63 +
        }
64 +
    }
65 +
    if count > 0x7FFFFFFF or fixupCount > 0x7FFFFFFF { throw il::binary::error(0, "too many image initializer values"); }
66 +
    let values = try graph::storage(arena, @sizeOf(il::DataValue), @alignOf(il::DataValue), count as u32) as *mut [il::DataValue];
67 +
    let fixups = try graph::storage(scratch, @sizeOf(Fixup), @alignOf(Fixup), fixupCount as u32) as *mut [Fixup];
68 +
    let items = try graph::storage(arena, @sizeOf(il::Data), @alignOf(il::Data), shared + 1) as *mut [il::Data];
69 +
    let mut valueCount: u32 = 0;
70 +
    let mut fixupIndex: u32 = 0;
71 +
    let mut offset: u32 = 0;
72 +
    for symbol in plan.order {
73 +
        let item = plan.items[try graph::index(&plan.dataIndex, symbol.name)];
74 +
        if symbol.addr > offset {
75 +
            set values[valueCount] = il::DataValue { item: il::DataItem::Undef, count: symbol.addr - offset };
76 +
            set valueCount += 1;
77 +
            set offset = symbol.addr;
78 +
        }
79 +
        if item.isZeroInit {
80 +
            set values[valueCount] = il::DataValue { item: il::DataItem::Undef, count: item.size };
81 +
            set valueCount += 1;
82 +
            set offset += item.size;
83 +
            continue;
84 +
        }
85 +
        for v in item.values {
86 +
            if v.count > 0 {
87 +
                if let case il::DataItem::Sym(target) = v.item {
88 +
                    let targetIndex = try graph::index(&plan.dataIndex, target);
89 +
                    if not plan.items[targetIndex].readOnly {
90 +
                        set fixups[fixupIndex] = Fixup { offset, target: plan.offsets[targetIndex], count: v.count };
91 +
                        set fixupIndex += 1;
92 +
                    }
93 +
                }
94 +
            }
95 +
            set values[valueCount] = try value(v, plan, names, arena);
96 +
            set valueCount += 1;
97 +
            set offset += graph::width(v.item) * v.count;
98 +
        }
99 +
    }
100 +
    set items[0] = il::Data { name: stateName, size: plan.size, alignment: plan.alignment,
101 +
        readOnly: true, isZeroInit: false, values: &values[..valueCount] };
102 +
    let mut itemIndex: u32 = 1;
103 +
    for item, i in plan.items {
104 +
        if not item.readOnly { continue; }
105 +
        let count = 1 if item.isZeroInit else item.values.len;
106 +
        let copied = try graph::storage(arena, @sizeOf(il::DataValue), @alignOf(il::DataValue), count) as *mut [il::DataValue];
107 +
        if item.isZeroInit { set copied[0] = il::DataValue { item: il::DataItem::Undef, count: item.size }; }
108 +
        else { for v, j in item.values { set copied[j] = try value(v, plan, names, arena); } }
109 +
        set items[itemIndex] = il::Data { name: names.data[i], size: item.size, alignment: item.alignment,
110 +
            readOnly: true, isZeroInit: false, values: copied };
111 +
        set itemIndex += 1;
112 +
    }
113 +
    return Initializer { items, fixups };
114 +
}
115 +
116 +
/// Reserve the caller's growable list using checked allocation before any append.
117 +
export fn appendData(target: *mut *mut [il::Data], items: *[il::Data], arena: *mut alloc::Arena) throws (il::binary::Error) {
118 +
    let total = (*target).len as u64 + items.len as u64;
119 +
    if total > data::MAX_DATA_SYMS as u64 { throw il::binary::error(0, "native data symbol capacity exceeded"); }
120 +
    let mut slice = *target;
121 +
    if total > slice.cap as u64 {
122 +
        let mut capacity: u32 = 16;
123 +
        while capacity < total as u32 { set capacity *= 2; }
124 +
        let grown = try graph::storage(arena, @sizeOf(il::Data), @alignOf(il::Data), capacity) as *mut [il::Data];
125 +
        for item, i in slice { set grown[i] = item; }
126 +
        set slice = @sliceOf(grown.ptr, slice.len, capacity);
127 +
    }
128 +
    let allocator = alloc::arenaAllocator(arena);
129 +
    for item in items { slice.append(item, allocator); }
130 +
    set *target = slice;
131 +
}
132 +
133 +
/// Check hard backend limits and a conservative expansion bound for one function.
134 +
fn bounds(e: *emit::Emitter, function: *il::Fn) throws (il::binary::Error) {
135 +
    if function.params.len > rv64::ARG_REGS.len { throw il::binary::error(0, "image function has too many arguments"); }
136 +
    let mut blocks = function.blocks.len as u64 + 1;
137 +
    let mut words: u64 = 256;
138 +
    let mut reserve: u64 = 0;
139 +
    let mut operands: u64 = 0;
140 +
    let mut calls: u64 = 0;
141 +
    let mut branches: u64 = 0;
142 +
    for block in function.blocks {
143 +
        if block.params.len > 16 { throw il::binary::error(0, "image block has too many parameters"); }
144 +
        for item in block.instrs {
145 +
            set words += 64;
146 +
            set operands += 5;
147 +
            set branches += 2;
148 +
            match item {
149 +
                case il::Instr::Call { args, .. } => {
150 +
                    if args.len > rv64::ARG_REGS.len { throw il::binary::error(0, "image call has too many arguments"); }
151 +
                    set words += args.len as u64 * 32;
152 +
                    set operands += args.len as u64;
153 +
                    set calls += 1;
154 +
                }
155 +
                case il::Instr::Jmp { args, .. } => {
156 +
                    set words += args.len as u64 * 32;
157 +
                    set operands += args.len as u64;
158 +
                }
159 +
                case il::Instr::Br { thenArgs, elseArgs, .. } => {
160 +
                    if thenArgs.len > 0 and elseArgs.len > 0 { throw il::binary::error(0, "image branch requires split argument edges"); }
161 +
                    set words += (thenArgs.len as u64 + elseArgs.len as u64) * 32;
162 +
                    set operands += thenArgs.len as u64 + elseArgs.len as u64;
163 +
                }
164 +
                case il::Instr::Switch { defaultArgs, cases, .. } => {
165 +
                    set words += defaultArgs.len as u64 * 32;
166 +
                    set operands += defaultArgs.len as u64;
167 +
                    for c in cases {
168 +
                        set words += 32 + c.args.len as u64 * 32;
169 +
                        set operands += c.args.len as u64;
170 +
                        set branches += 2;
171 +
                        if c.args.len > 0 { set blocks += 1; }
172 +
                    }
173 +
                }
174 +
                case il::Instr::Reserve { size, alignment, .. } => {
175 +
                    if alignment == 0 or alignment & (alignment - 1) <> 0 or alignment > graph::MAX_DATA {
176 +
                        throw il::binary::error(0, "image stack alignment exceeds capacity");
177 +
                    }
178 +
                    match size {
179 +
                        case il::Val::Imm(bytes) => {
180 +
                            if bytes < 0 or bytes > graph::MAX_DATA as i64 { throw il::binary::error(0, "image stack reservation exceeds capacity"); }
181 +
                            let a = alignment as u64;
182 +
                            set reserve = ((reserve + a - 1) & ~(a - 1)) + bytes as u64;
183 +
                            if reserve > graph::MAX_DATA as u64 { throw il::binary::error(0, "image stack frame exceeds capacity"); }
184 +
                        }
185 +
                        case il::Val::Reg(_) => {
186 +
                            if alignment > 2048 { throw il::binary::error(0, "dynamic image stack alignment exceeds backend capacity"); }
187 +
                        }
188 +
                        else => throw il::binary::error(0, "invalid image stack reservation"),
189 +
                    }
190 +
                }
191 +
                case il::Instr::Blit { size, alignment, .. } => {
192 +
                    let case il::Val::Imm(bytes) = size else { throw il::binary::error(0, "image blit requires an immediate size"); };
193 +
                    if bytes < 0 or bytes > graph::MAX_DATA as i64 or alignment == 0 or alignment & (alignment - 1) <> 0 {
194 +
                        throw il::binary::error(0, "image blit exceeds backend capacity");
195 +
                    }
196 +
                    // A spilled base can force an unrolled copy with adjusted addresses.
197 +
                    let width = il::typeSize(il::copyType(bytes as u32, alignment)) if bytes > 0 else 1;
198 +
                    set words += (bytes as u64 / width as u64 + 8) * 12;
199 +
                }
200 +
                else => {},
201 +
            }
202 +
        }
203 +
    }
204 +
    if blocks > e.labels.blockOffsets.len as u64 { throw il::binary::error(0, "image function exceeds block capacity"); }
205 +
    // Local jumps use one JAL slot and must stay strictly inside its positive range.
206 +
    if words >= 0x100000 / rv64::INSTR_SIZE as u64 { throw il::binary::error(0, "image function exceeds local branch reach"); }
207 +
    if words > (e.code.len - e.codeLen) as u64 { throw il::binary::error(0, "native code capacity exceeded"); }
208 +
    if e.labels.funcs.count >= e.labels.funcs.entries.len / 2 { throw il::binary::error(0, "native function symbol capacity exceeded"); }
209 +
    if dict::get(&e.labels.funcs, function.name) <> nil { throw il::binary::error(0, "duplicate native image function"); }
210 +
    if e.funcs.len >= e.funcs.cap or operands + e.pendingAddrLoads.len as u64 > e.pendingAddrLoads.cap as u64
211 +
        or calls + e.pendingCalls.len as u64 > e.pendingCalls.cap as u64
212 +
        or branches + e.pendingBranches.len as u64 > e.pendingBranches.cap as u64 {
213 +
        throw il::binary::error(0, "native image relocation capacity exceeded");
214 +
    }
215 +
}
216 +
217 +
/// Shared liveness state used to bound the spill-candidate buffer.
218 +
record Pressure: Copy {
219 +
    /// Registers live at the inspected instruction.
220 +
    live: bitset::Bitset,
221 +
    /// Number of live registers.
222 +
    count: u32,
223 +
}
224 +
225 +
/// Add one existing IL operand to the current live register set.
226 +
fn liveUse(reg: il::Reg, context: *mut opaque) {
227 +
    let pressure = context as *mut Pressure;
228 +
    if not bitset::contains(&pressure.live, reg.n) {
229 +
        bitset::put(&mut pressure.live, reg.n);
230 +
        set pressure.count += 1;
231 +
    }
232 +
}
233 +
234 +
/// Run the existing allocation stages, checking their fixed pressure capacity.
235 +
fn allocate(function: *il::Fn, arena: *mut alloc::Arena) -> regalloc::AllocResult throws (il::binary::Error) {
236 +
    let config = rv64::targetConfig();
237 +
    let live = try regalloc::liveness::analyze(function, arena)
238 +
        catch { throw il::binary::error(0, "image liveness arena exhausted"); };
239 +
    let saved = alloc::save(arena);
240 +
    let words = try graph::storage(arena, @sizeOf(u32), @alignOf(u32), bitset::wordsFor(live.maxReg)) as *mut [u32];
241 +
    let mut pressure = Pressure { live: bitset::init(words), count: 0 };
242 +
    for block, b in function.blocks {
243 +
        bitset::copy(&mut pressure.live, &live.liveOut[b]);
244 +
        set pressure.count = bitset::count(&pressure.live);
245 +
        if pressure.count > 256 { throw il::binary::error(0, "image exceeds live register capacity"); }
246 +
        let mut index = block.instrs.len;
247 +
        while index > 0 {
248 +
            set index -= 1;
249 +
            let item = block.instrs[index];
250 +
            if let dst = il::instrDst(item) {
251 +
                if bitset::contains(&pressure.live, dst.n) {
252 +
                    bitset::clear(&mut pressure.live, dst.n);
253 +
                    set pressure.count -= 1;
254 +
                }
255 +
            }
256 +
            il::forEachReg(item, liveUse, &mut pressure as *mut opaque);
257 +
            if pressure.count > 256 { throw il::binary::error(0, "image exceeds live register capacity"); }
258 +
        }
259 +
    }
260 +
    alloc::restore(arena, saved);
261 +
    let spill = try regalloc::spill::analyze(function, &live, config.allocatable.len, config.calleeSaved.len, config.slotSize, arena)
262 +
        catch { throw il::binary::error(0, "image spill arena exhausted"); };
263 +
    let assignment = try regalloc::assign::assign(function, &live, &spill, &config, arena)
264 +
        catch { throw il::binary::error(0, "image register assignment arena exhausted"); };
265 +
    return regalloc::AllocResult { assignments: assignment.assignments, spill, usedCalleeSaved: assignment.usedCalleeSaved };
266 +
}
267 +
268 +
/// Emit through the existing allocator and selector, propagating scratch exhaustion.
269 +
export fn function(generator: *mut rv64::Generator, function: *il::Fn, scratch: *mut alloc::Arena) throws (il::binary::Error) {
270 +
    try bounds(&generator.e, function);
271 +
    let saved = alloc::save(scratch);
272 +
    let allocation = try allocate(function, scratch) catch error {
273 +
        alloc::restore(scratch, saved);
274 +
        throw error;
275 +
    };
276 +
    rv64::isel::selectFn(&mut generator.e, &allocation, function);
277 +
    alloc::restore(scratch, saved);
278 +
}
279 +
280 +
/// Reserve a trusted leaf helper in the same global symbol and relocation tables.
281 +
fn helper(e: *mut emit::Emitter, name: *[u8], words: u32) throws (il::binary::Error) {
282 +
    if dict::get(&e.labels.funcs, name) <> nil { throw il::binary::error(0, "duplicate image helper symbol"); }
283 +
    if e.labels.funcs.count >= e.labels.funcs.entries.len / 2 { throw il::binary::error(0, "native function symbol capacity exceeded"); }
284 +
    if words > e.code.len - e.codeLen { throw il::binary::error(0, "native code capacity exceeded"); }
285 +
    if e.funcs.len >= e.funcs.cap or e.pendingCalls.len == e.pendingCalls.cap
286 +
        or e.pendingAddrLoads.len == e.pendingAddrLoads.cap {
287 +
        throw il::binary::error(0, "native image helper capacity exceeded");
288 +
    }
289 +
    emit::recordFunc(e, name);
290 +
    emit::recordFuncOffset(e, name);
291 +
}
292 +
293 +
/// Build trusted startup, address getters, and private-pointer relocation routines.
294 +
export fn helpers(generator: *mut rv64::Generator, id: u32, plan: *graph::Plan, names: *graph::Names,
295 +
    initial: *Initializer, pool: *mut strings::Pool, arena: *mut alloc::Arena) throws (il::binary::Error) {
296 +
    let stateName = initial.items[0].name;
297 +
    let entry = try graph::name("images::native_", id, "", pool, arena);
298 +
    let entryGetter = try graph::name("images::entry_", id, "", pool, arena);
299 +
    let initialGetter = try graph::name("images::initial_", id, "", pool, arena);
300 +
    let relocate = try graph::name("images::relocate_", id, "", pool, arena);
301 +
    let e = &mut generator.e;
302 +
    try helper(e, entry, 16);
303 +
    emit::emit(e, encode::mv(rv64::TP, rv64::A0));
304 +
    emit::emitLd(e, rv64::SP, rv64::A0, 72);
305 +
    emit::recordCall(e, names.functions[plan.entry]);
306 +
    if not plan.returnsStatus { emit::emit(e, encode::mv(rv64::A0, rv64::ZERO)); }
307 +
    else {
308 +
        emit::emit(e, encode::slli(rv64::A0, rv64::A0, 32));
309 +
        emit::emit(e, encode::srli(rv64::A0, rv64::A0, 32));
310 +
    }
311 +
    emit::loadImm(e, rv64::A7, 49);
312 +
    emit::emit(e, encode::ecall());
313 +
    emit::emit(e, encode::ebreak());
314 +
    try helper(e, entryGetter, 3);
315 +
    emit::recordAddrLoad(e, entry, rv64::A0);
316 +
    emit::emit(e, encode::ret());
317 +
    try helper(e, initialGetter, 3);
318 +
    emit::recordDataAddrLoad(e, stateName, rv64::A0);
319 +
    emit::emit(e, encode::ret());
320 +
    let words = initial.fixups.len as u64 * 32 + 1;
321 +
    if words > 0x7FFFFFFF { throw il::binary::error(0, "image relocation helper exceeds capacity"); }
322 +
    try helper(e, relocate, words as u32);
323 +
    for fixup in initial.fixups {
324 +
        emit::emitAddImm(e, rv64::T0, rv64::A0, fixup.offset as i32);
325 +
        emit::emitAddImm(e, rv64::T1, rv64::A0, fixup.target as i32);
326 +
        emit::loadImm(e, rv64::T2, fixup.count as i64);
327 +
        let start = e.codeLen;
328 +
        if fixup.offset % 8 == 0 { emit::emitSd(e, rv64::T1, rv64::T0, 0); }
329 +
        else {
330 +
            emit::emit(e, encode::mv(rv64::T3, rv64::T1));
331 +
            for byte in 0..8 {
332 +
                emit::emitSb(e, rv64::T3, rv64::T0, byte as i32);
333 +
                if byte < 7 { emit::emit(e, encode::srli(rv64::T3, rv64::T3, 8)); }
334 +
            }
335 +
        }
336 +
        if fixup.count > 1 {
337 +
            emit::emit(e, encode::addi(rv64::T0, rv64::T0, 8));
338 +
            emit::emit(e, encode::addi(rv64::T2, rv64::T2, -1));
339 +
            emit::emit(e, encode::bne(rv64::T2, rv64::ZERO, (start as i32 - e.codeLen as i32) * rv64::INSTR_SIZE));
340 +
        }
341 +
    }
342 +
    emit::emit(e, encode::ret());
343 +
}
kernel/Makefile +39 -22
1 1
# Freestanding kernel and hosted mechanism checks.
2 2
EMU ?= $(or $(RAD_EMULATOR),emulator)
3 3
HOST_EMU ?= $(EMU)
4 4
COMPILER := ../bin/radiance.rv64.dev
5 5
COMPILE := $(HOST_EMU) -memory-size=385024 -data-size=348160 -stack-size=512 -run $(COMPILER)
6 -
MODULES := core/fdt.rad core/platform.rad core/frames.rad core/abi.rad core/handles.rad \
7 -
	core/events.rad core/domains.rad core/resources.rad core/capabilities.rad \
8 -
	core/memory.rad core/state.rad core/pages.rad core/atomic.rad core/cpu.rad \
9 -
	core/budgets.rad core/contexts.rad core/clock.rad core/budget_caps.rad \
10 -
	core/timers.rad core/notifications.rad core/devices.rad core/interrupts.rad core/mmio.rad
11 -
CORE_ASM := arch/atomic.ras arch/context.ras arch/clock.ras arch/mmio.ras
6 +
MODULES := $(wildcard core/*.rad)
7 +
CORE_ASM := arch/atomic.ras arch/context.ras arch/clock.ras arch/mmio.ras arch/physical.ras
12 8
CORE := -pkg core -mod core.rad $(addprefix -mod ,$(MODULES) $(CORE_ASM))
13 -
CHECK_MODULES := check/boot.rad check/fixture.rad check/frames.rad check/handles.rad \
14 -
	check/domains.rad check/capabilities.rad check/pages.rad check/events.rad \
15 -
	check/budgets.rad check/budget_caps.rad check/timers.rad check/notifications.rad check/devices.rad
9 +
CHECK_MODULES := $(wildcard check/*.rad)
10 +
USER_INPUTS := user.rad $(wildcard user/*.rad user/*/*.rad) core/abi.rad
11 +
USER_BASE := -pkg abi -mod core/abi.rad -pkg user -mod user.rad -mod user/sys.rad -pkg probe -mod user/probe.rad
12 +
BASE_IMAGES := sample_root sample_control sample_scalars sample_memory sample_overflow sample_events sample_alias
13 +
BASE_RIL := $(addprefix build/,$(addsuffix .ril,$(BASE_IMAGES)))
14 +
IMAGE_MODULES_sample_control := user/sample_control/math.rad
15 +
BASE_CATALOG := -zero-bss -pkg images -mod build/baseline/images.rad -mod user/sys.ras $(addprefix -image ,$(BASE_RIL))
16 +
MACHINE := $(EMU) -machine -no-guard-stack -max-steps=100000000 -count-instructions
16 17
17 18
.PHONY: all check clean compiler-check
19 +
.DELETE_ON_ERROR:
18 20
all: kernel.rv64
19 21
20 22
compiler-check:
21 23
22 24
$(COMPILER): compiler-check
23 25
	$(MAKE) -C .. RAD_EMULATOR=$(abspath $(shell command -v $(HOST_EMU)))
24 26
25 -
kernel.rv64: main.rad arch/entry.ras core.rad $(MODULES) $(CORE_ASM) $(COMPILER)
26 -
	$(COMPILE) $(CORE) -pkg kernel -start arch/entry.ras -mod main.rad -entry kernel -o $@
27 +
build:
28 +
	mkdir -p $@
27 29
28 -
check.rv64: check.rad core.rad $(MODULES) $(CORE_ASM) $(CHECK_MODULES) $(COMPILER)
29 -
	$(COMPILE) $(CORE) -pkg check -mod check.rad $(addprefix -mod ,$(CHECK_MODULES)) -entry check -o $@
30 +
build/%.ril: user/%.rad $(USER_INPUTS) $(COMPILER) | build
31 +
	$(COMPILE) $(USER_BASE) -pkg $* -mod $< $(addprefix -mod ,$(IMAGE_MODULES_$*)) -entry $* -emit ril -o $@
30 32
31 -
context.rv64: context.rad context/wait.rad check/context.ras arch/entry.ras core.rad $(MODULES) $(CORE_ASM) $(COMPILER)
32 -
	$(COMPILE) $(CORE) -pkg context -start arch/entry.ras -mod context.rad -mod context/wait.rad -mod check/context.ras -entry context -o $@
33 +
build/baseline/images.rad: $(BASE_RIL) $(COMPILER)
34 +
	mkdir -p $(@D)
35 +
	$(COMPILE) -catalog $@ $(addprefix -image ,$(BASE_RIL))
33 36
34 -
interrupt.rv64: interrupt.rad check/interrupt.ras arch/entry.ras core.rad $(MODULES) $(CORE_ASM) $(COMPILER)
35 -
	$(COMPILE) $(CORE) -pkg interrupt -start arch/entry.ras -mod interrupt.rad -mod check/interrupt.ras -entry interrupt -o $@
37 +
kernel.rv64: main.rad arch/entry.ras core.rad $(MODULES) $(CORE_ASM) build/baseline/images.rad user/sys.ras $(COMPILER)
38 +
	$(COMPILE) $(BASE_CATALOG) $(CORE) -pkg kernel -start arch/entry.ras -mod main.rad -entry kernel -o $@
36 39
37 -
check: all check.rv64 context.rv64 interrupt.rv64
40 +
check.rv64: check.rad core.rad $(MODULES) $(CORE_ASM) $(CHECK_MODULES) build/baseline/images.rad user/sys.ras $(COMPILER)
41 +
	$(COMPILE) $(BASE_CATALOG) $(CORE) -pkg check -mod check.rad $(addprefix -mod ,$(CHECK_MODULES)) -entry check -o $@
42 +
43 +
context.rv64: context.rad context/wait.rad check/context.ras arch/entry.ras core.rad $(MODULES) $(CORE_ASM) build/baseline/images.rad user/sys.ras $(COMPILER)
44 +
	$(COMPILE) $(BASE_CATALOG) $(CORE) -pkg context -start arch/entry.ras -mod context.rad -mod context/wait.rad -mod check/context.ras -entry context -o $@
45 +
46 +
interrupt.rv64: interrupt.rad check/interrupt.ras arch/entry.ras core.rad $(MODULES) $(CORE_ASM) build/baseline/images.rad user/sys.ras $(COMPILER)
47 +
	$(COMPILE) $(BASE_CATALOG) $(CORE) -pkg interrupt -start arch/entry.ras -mod interrupt.rad -mod check/interrupt.ras -entry interrupt -o $@
48 +
49 +
native.rv64: native.rad arch/entry.ras core.rad $(MODULES) $(CORE_ASM) build/baseline/images.rad user/sys.ras $(COMPILER)
50 +
	$(COMPILE) $(BASE_CATALOG) $(CORE) -pkg native -start arch/entry.ras -mod native.rad -entry native -o $@
51 +
52 +
check: all check.rv64 context.rv64 interrupt.rv64 native.rv64
38 53
	$(HOST_EMU) -run check.rv64
39 -
	$(EMU) -machine -no-guard-stack -max-steps=100000000 -count-instructions -run kernel.rv64
40 -
	$(EMU) -machine -no-guard-stack -max-steps=1000000 -count-instructions -run context.rv64
41 -
	$(EMU) -machine -harts=2 -no-guard-stack -irq=3 -irq-at=4000000 -uart-rx=52 -uart-rx-at=8000000 -max-steps=100000000 -count-instructions -run interrupt.rv64
54 +
	$(MACHINE) -run kernel.rv64
55 +
	$(MACHINE) -max-steps=1000000 -run context.rv64
56 +
	$(MACHINE) -harts=2 -irq=3 -irq-at=4000000 -uart-rx=52 -uart-rx-at=8000000 -run interrupt.rv64
57 +
	$(MACHINE) -run native.rv64
42 58
43 59
clean:
44 -
	rm -f kernel.rv64 check.rv64 context.rv64 interrupt.rv64
60 +
	rm -rf build
61 +
	rm -f kernel.rv64 check.rv64 context.rv64 interrupt.rv64 native.rv64
kernel/NOTES.md +29 -2
1 1
# Kernel implementation decisions
2 2
3 3
The specification at https://radiant.computer/system/kernel takes precedence
4 4
for fixed call numbers, handle layout, rights, and object behavior. These notes
5 -
record the contracts established through step 16 of the 22-step plan.
5 +
record the contracts established through step 17 of the 22-step plan.
6 6
7 7
## Source and trust boundary
8 8
9 9
- Kernel mechanisms use freestanding Radiance; RAS owns machine entry, register
10 10
  state, atomics, and MMIO. Hosted checks exercise the same mechanism modules.
212 212
  Native compilation uses std::arch::rv64, its allocator, and its relocations.
213 213
- Structural validation is not pointer-provenance or type-safety verification.
214 214
  The trusted-input boundary remains unchanged. Round trips exercise switch
215 215
  arguments, loops with aggregate returns, and writable function pointers.
216 216
217 +
## Shared catalogs and trusted native images
218 +
219 +
- -catalog output.rad -image input.ril ... defines up to 64 ordered images.
220 +
  Index zero is root; an index is not creation or execution authority. Native
221 +
  linking consumes the same ordered inputs through -image.
222 +
- Use the compiler's RV64 instruction selection, register allocation, assembler,
223 +
  data layout, and relocations. Make supplies paths and dependency order.
224 +
- Namespace image functions and constants. Writable globals and data referring
225 +
  to them are instance state. Copy the immutable initializer and relocate private
226 +
  pointers, including repeated and unaligned pointer fields.
227 +
- gp addresses private image state and tp the current Env. Native locals,
228 +
  saved registers, and return addresses use a private 64 KiB sp stack. Trusted
229 +
  inputs must respect its bounds; the supplied Page stack is an authorized Env
230 +
  range, not the native spill stack.
231 +
- The immutable 80-byte Env contains ten consecutive u64 fields: argument base,
232 +
  argument size, Events handle, Events address, private image-state base,
233 +
  Page-stack lower bound, Page-stack upper bound, context identity, private-stack
234 +
  lower bound, private-stack upper bound. Env and native-stack memory remain
235 +
  private to the context and are not granted as Pages.
236 +
- External native references are restricted to linked user::sys:: primitives.
237 +
  Typed wrappers check kernel results before constructing Page views or issuing
238 +
  MMIO. Events consumption uses acquire/release and a shared consumer lock.
239 +
- -zero-bss includes zero-initialized kernel storage in native data sections so
240 +
  the image header and firmware reservations cover globals. Physical clear/copy
241 +
  callbacks accept validated owned ranges; constructing physical byte views is
242 +
  unsafe.
243 +
217 244
## Validation
218 245
219 246
Use the current machine-capable sibling emulator. Set `RAD_EMULATOR`, pass
220 247
`EMU` to the kernel Make invocation, or put `emulator` on PATH. The kernel build
221 248
checks compiler dependencies. From the repository root, run:
223 250
```sh
224 251
make -C kernel check
225 252
make std-test bin-test
226 253
```
227 254
228 -
Exercise malformed binary inputs, every incomplete prefix, and source-to-binary-to-native graphs with switch block arguments, aggregate-return loops, and writable function pointers. Kernel behavior remains the retained mechanism baseline.
255 +
Run seven source-compiled binary images through native machine traps. The fixture handles PageAllocate (30), QueryPage (44), checked Page materialization (60), and Exit (49), plus breakpoint fault entry. Check control flow, scalar operations, Page bounds, Events consumption, and overlapping memory. Fixture setup supplies private state, Env, and stack; this is not the complete calls dispatcher.
229 256
230 257
Run the context reservation probe with an emulator that retains LR/SC
231 258
reservations across traps. This checks the kernel's reservation invalidation.
kernel/arch/physical.ras added +29 -0
1 +
// Validated physical byte views and disjoint kernel-owned RAM operations.
2 +
.text;
3 +
.export @"core::physical::pointer";
4 +
@"core::physical::pointer"
5 +
    ret;
6 +
7 +
.export @"core::physical::clear";
8 +
@"core::physical::clear"
9 +
    beqz %a1 @clearDone;
10 +
    add %t0 %a0 %a1;
11 +
@clearWord
12 +
    sd %zero 0(%a0);
13 +
    addi %a0 %a0 8;
14 +
    bltu %a0 %t0 @clearWord;
15 +
@clearDone
16 +
    ret;
17 +
18 +
.export @"core::physical::copy";
19 +
@"core::physical::copy"
20 +
    beqz %a2 @copyDone;
21 +
    add %t0 %a1 %a2;
22 +
@copyByte
23 +
    lbu %t1 0(%a1);
24 +
    sb %t1 0(%a0);
25 +
    addi %a0 %a0 1;
26 +
    addi %a1 %a1 1;
27 +
    bltu %a1 %t0 @copyByte;
28 +
@copyDone
29 +
    ret;
kernel/core.rad +1 -0
21 21
export mod timers;
22 22
export mod notifications;
23 23
export mod devices;
24 24
export mod interrupts;
25 25
export mod mmio;
26 +
export mod physical;
kernel/core/physical.rad added +17 -0
1 +
//! Physical access for kernel-owned RAM and immutable image initializers.
2 +
3 +
/// Convert a validated mapped address at the physical boundary.
4 +
unsafe fn pointer(base: u64) -> *mut u8;
5 +
6 +
/// View mapped bytes whose address, extent, and lifetime the caller validated.
7 +
export unsafe fn bytes(base: u64, size: u32) -> *mut [u8] {
8 +
    return @sliceOf(pointer(base), size);
9 +
}
10 +
11 +
/// Allocator callback for an owned range aligned to eight bytes in base and size.
12 +
/// Only the serialized frame allocator can supply this physical range.
13 +
export fn clear(base: u64, size: u32);
14 +
15 +
/// Initializer callback for disjoint validated source and destination ranges.
16 +
/// Source bytes are immutable image data; destination bytes are owned RAM.
17 +
export fn copy(destination: u64, source: u64, size: u32);
kernel/native.rad added +124 -0
1 +
//! Real user-mode execution of admitted graphs and their memory-safety checks.
2 +
3 +
use core::abi;
4 +
use core::atomic;
5 +
use core::cpu;
6 +
use core::contexts;
7 +
use core::domains;
8 +
use core::events;
9 +
use core::fdt;
10 +
use core::frames;
11 +
use core::memory;
12 +
use core::pages;
13 +
use core::physical;
14 +
use core::platform;
15 +
use core::resources;
16 +
use core::state;
17 +
use images;
18 +
19 +
/// Isolated consumer used by the native graph regressions.
20 +
static DOMAINS: [domains::Domain; 1] = undefined;
21 +
/// Page objects exercised by checked materialization calls.
22 +
static OBJECTS: [resources::Slot; 16] = undefined;
23 +
/// Storage for the execution-state view.
24 +
static CONTEXTS: [contexts::Context; 1] = undefined;
25 +
/// Physical frame ownership from the firmware reservations.
26 +
static POOL: frames::Pool = undefined;
27 +
/// Persistent physical Page pins.
28 +
static PINS: [u16; frames::MAX_FRAMES] = undefined;
29 +
/// Live Page claims.
30 +
static CLAIMS: [bool; frames::MAX_FRAMES] = undefined;
31 +
/// Domain-lifetime frame bitmap.
32 +
static GRANTS: [u64; frames::MAX_FRAMES / 64] = undefined;
33 +
/// Actual capability and Page mechanisms used by the fixture calls.
34 +
static KERNEL: state::State = undefined;
35 +
36 +
/// Run one graph against actual Page authority and check its terminal call.
37 +
unsafe fn run(image: u32, expected: u32, own: abi::Handle, queued: bool) {
38 +
    let descriptor = images::get(image);
39 +
    let stack = try! pages::allocate(&mut KERNEL, 0, own, 1, physical::clear);
40 +
    let range = try! pages::access(&KERNEL, 0, stack, abi::READ | abi::WRITE);
41 +
    let args = physical::bytes(range.base, 8).ptr as *mut u64;
42 +
    set *args = stack.bits if image == 6 else own.bits;
43 +
    let private = frames::install(try! frames::allocate(&mut POOL, 16, physical::clear));
44 +
    let data = frames::install(try! frames::allocate(&mut POOL, 1, physical::clear));
45 +
    let privateBase = private.first as u64 * frames::PAGE_SIZE as u64;
46 +
    let dataBase = data.first as u64 * frames::PAGE_SIZE as u64;
47 +
    assert descriptor.stateSize <= frames::PAGE_SIZE and descriptor.stateAlignment <= frames::PAGE_SIZE;
48 +
    physical::copy(dataBase, descriptor.initial, descriptor.stateSize);
49 +
    descriptor.relocate(dataBase);
50 +
    let mut env = DOMAINS[0].env;
51 +
    set env.argsPointer = range.base;
52 +
    set env.argsSize = 8;
53 +
    set env.stackBase = range.base;
54 +
    set env.stackTop = range.base + range.size;
55 +
    set env.stateBase = dataBase;
56 +
    set env.privateStackBase = privateBase;
57 +
    set env.eventsPointer = &DOMAINS[0].events.ring as u64;
58 +
    set env.privateStackTop = privateBase + 65536;
59 +
    if queued {
60 +
        try! events::push(&mut DOMAINS[0].events, events::Event {
61 +
            kind: 1, reserved: 0, code: 42, value: 0x123456789abcdef,
62 +
        }, events::Class::Ordinary);
63 +
    }
64 +
    let mut frame: cpu::Frame = undefined;
65 +
    cpu::init(&mut frame, descriptor.entry, env.stackTop, &env as u64, dataBase);
66 +
    loop {
67 +
        cpu::enter(&mut frame);
68 +
        assert frame.status & 0x1800 == 0;
69 +
        if image == 4 {
70 +
            assert frame.cause == 3;
71 +
            break;
72 +
        }
73 +
        assert frame.cause == 8;
74 +
        let args = &frame.registers;
75 +
        match args[17] {
76 +
            case 30 => {
77 +
                let page = try! pages::allocate(&mut KERNEL, 0, abi::Handle { bits: args[10] }, args[11], physical::clear);
78 +
                cpu::reply(&mut frame, abi::Error::Ok, page.bits, 0, 0, 0);
79 +
            },
80 +
            case 44 => {
81 +
                let area = try! pages::access(&KERNEL, 0, abi::Handle { bits: args[10] }, 0);
82 +
                cpu::reply(&mut frame, abi::Error::Ok, area.base, area.size / frames::PAGE_SIZE as u64, 0, 0);
83 +
            },
84 +
            case 60 => {
85 +
                assert args[11] <= 3;
86 +
                let area = try! pages::access(&KERNEL, 0, abi::Handle { bits: args[10] }, args[11] as u16);
87 +
                cpu::reply(&mut frame, abi::Error::Ok, area.base, area.size, 0, 0);
88 +
            },
89 +
            case 49 => {
90 +
                assert args[10] == expected as u64;
91 +
                break;
92 +
            },
93 +
            else => panic "native: unexpected kernel call",
94 +
        }
95 +
    }
96 +
    if image == 5 {
97 +
        assert atomic::load(&DOMAINS[0].events.ring.head) == atomic::load(&DOMAINS[0].events.ring.tail);
98 +
    }
99 +
    frames::reclaim(&mut POOL, private);
100 +
    frames::reclaim(&mut POOL, data);
101 +
}
102 +
103 +
/// Check graph linking, native control flow, bounded memory, and protected spills.
104 +
@default unsafe fn main(hart: u64, description: *u8) -> u32 {
105 +
    assert hart == 0 and images::COUNT == 7;
106 +
    let size = try! fdt::word(@sliceOf(description, 40), 4);
107 +
    assert size >= 40 and size <= fdt::MAX_BYTES;
108 +
    let mut tree: fdt::Tree = undefined;
109 +
    try! fdt::decode(@sliceOf(description, size), &mut tree);
110 +
    let mut machine: platform::Platform = undefined;
111 +
    try! platform::discover(&tree, &mut machine);
112 +
    try! frames::init(&mut POOL, &machine);
113 +
    domains::init(&mut DOMAINS[..]);
114 +
    resources::init(&mut OBJECTS[..]);
115 +
    contexts::init(&mut CONTEXTS[0], 0);
116 +
    let mut ram: memory::Memory = undefined;
117 +
    memory::init(&mut ram, &mut POOL, &mut PINS[..], &mut CLAIMS[..], &mut GRANTS[..], 1);
118 +
    set KERNEL = state::State { domains: &mut DOMAINS[..], resources: &mut OBJECTS[..], contexts: &mut CONTEXTS[..], memory: ram };
119 +
    let own = domains::root(&mut DOMAINS[..], images::ROOT);
120 +
    let expected: [u32; 7] = [42, 21, 73, 77, 0, 42, 81];
121 +
    for i in 0..images::COUNT { run(i, expected[i], own, i == 5); }
122 +
    run(5, 42, own, false);
123 +
    return 0;
124 +
}
kernel/user.rad added +3 -0
1 +
//! User bindings for the kernel's capability-checked direct-call ABI.
2 +
3 +
export unsafe mod sys;
kernel/user/probe.rad added +20 -0
1 +
//! Shared argument and Page views for source image probes.
2 +
use abi;
3 +
use user::sys;
4 +
5 +
/// Decode a caller-local handle supplied in the workload arguments.
6 +
export fn handle(bits: u64) -> abi::Handle { return abi::Handle { bits }; }
7 +
/// View aligned startup arguments as complete u64 words.
8 +
export fn args(env: *sys::Env) -> *[u64] {
9 +
    let bytes = sys::envArgs(env);
10 +
    assert bytes.len % 8 == 0;
11 +
    assert bytes.ptr as u64 % 8 == 0;
12 +
    return @sliceOf(bytes.ptr as *u64, bytes.len / 8);
13 +
}
14 +
/// Obtain a writable word view through Page authority.
15 +
export fn words(page: abi::Handle) -> *mut [u64] {
16 +
    let info = sys::queryPage(page);
17 +
    assert info.count <= 0xffffffff / 4096;
18 +
    let bytes = try! sys::pageSliceMut(page, 0, info.count * 4096);
19 +
    return @sliceOf(bytes.ptr as *mut u64, bytes.len / 8);
20 +
}
kernel/user/sample_alias.rad added +15 -0
1 +
//! Wiping the supplied Page stack preserves private native frames.
2 +
use abi;
3 +
use user::sys;
4 +
use probe;
5 +
6 +
fn wipe(page: abi::Handle) -> u32 {
7 +
    let sentinel: [u32; 8] = [81, 82, 83, 84, 85, 86, 87, 88];
8 +
    let words = probe::words(page);
9 +
    for i in 0..words.len { set words[i] = 0; }
10 +
    return sentinel[0];
11 +
}
12 +
@default fn main(env: *sys::Env) -> u32 {
13 +
    let args = probe::args(env);
14 +
    return wipe(probe::handle(args[0]));
15 +
}
kernel/user/sample_control.rad added +17 -0
1 +
//! Indirect recursion and simultaneous loop-variable swaps terminate with 21.
2 +
use user::sys;
3 +
mod math;
4 +
5 +
@default fn main(env: *sys::Env) -> u32 {
6 +
    let f = math::fib;
7 +
    let result = f(8);
8 +
    let mut a: u32 = 1;
9 +
    let mut b: u32 = 2;
10 +
    for i in 0..3 {
11 +
        let previous = a;
12 +
        set a = b;
13 +
        set b = previous;
14 +
    }
15 +
    if a <> 2 or b <> 1 { return 100; }
16 +
    return result;
17 +
}
kernel/user/sample_control/math.rad added +9 -0
1 +
//! Recursive calls preserve private local values and return addresses.
2 +
/// Compute Fibonacci recursively while retaining a local array across calls.
3 +
export fn fib(n: u32) -> u32 {
4 +
    if n < 2 { return n; }
5 +
    let local: [u32; 8] = [n, 0, 0, 0, 0, 0, 0, 0];
6 +
    let left = fib(n - 1);
7 +
    let right = fib(local[0] - 2);
8 +
    return left + right;
9 +
}
kernel/user/sample_events.rad added +13 -0
1 +
//! Optional event payload widths and repeated empty pops terminate with 42.
2 +
use user::sys;
3 +
4 +
@default fn main(env: *sys::Env) -> u32 {
5 +
    if let event = sys::eventsPop(env) {
6 +
        if event.kind <> 1 or event.code <> 42 or event.value <> 0x123456789abcdef {
7 +
            return 100;
8 +
        }
9 +
    }
10 +
    if let _event = sys::eventsPop(env) { return 100; }
11 +
    if let _event = sys::eventsPop(env) { return 100; }
12 +
    return 42;
13 +
}
kernel/user/sample_memory.rad added +20 -0
1 +
//! Aggregate copies, borrowed fields and bounded subslices terminate with 77.
2 +
use user::sys;
3 +
4 +
record Pair: Copy { first: u64, second: u64 }
5 +
constant SOURCE: Pair = Pair { first: 77, second: 88 };
6 +
7 +
fn borrow(base: *mut Pair) -> *mut u64 { return &mut base.second; }
8 +
fn copy(destination: *mut Pair, source: *Pair) { set *destination = *source; }
9 +
10 +
@default fn main(env: *sys::Env) -> u32 {
11 +
    let mut local: Pair = undefined;
12 +
    copy(&mut local, &SOURCE);
13 +
    let field = borrow(&mut local);
14 +
    set *field = local.first;
15 +
    // Self-copy exercises aliasing through the same aggregate API.
16 +
    copy(&mut local, &local);
17 +
    let words = @sliceOf(&local.first, 2);
18 +
    let middle = &words[1..2];
19 +
    return middle[0] as u32;
20 +
}
kernel/user/sample_overflow.rad added +13 -0
1 +
//! An invalid runtime subrange faults before a store or normal terminal report.
2 +
use user::sys;
3 +
4 +
fn outside(words: *mut [u64], index: u32) {
5 +
    set words[index] = 123;
6 +
}
7 +
@default fn main(env: *sys::Env) -> u32 {
8 +
    let mut local: [u64; 2] = [0, 0];
9 +
    // Radiance slices have u32 lengths; source bounds traps replace text RIL's
10 +
    // unrepresentable u64 byte-extent overflow with the same no-write behavior.
11 +
    outside(&mut local[..], 0xffffffff);
12 +
    return 100;
13 +
}
kernel/user/sample_root.rad added +15 -0
1 +
//! Allocation and checked Page access terminate with status 42.
2 +
use user::sys;
3 +
use probe;
4 +
5 +
static COUNT: u64 = 0;
6 +
7 +
@default fn main(env: *sys::Env) -> u32 {
8 +
    let args = probe::args(env);
9 +
    let page = try sys::pageAllocate(probe::handle(args[0]), 1) catch { return 100; };
10 +
    let words = probe::words(page);
11 +
    set words[0] = 42;
12 +
    set COUNT = words[0];
13 +
    if COUNT <> 42 { return 100; }
14 +
    return 42;
15 +
}
kernel/user/sample_scalars.rad added +38 -0
1 +
//! Narrow signedness, scalar operations, wide data and matching return 73.
2 +
use user::sys;
3 +
4 +
constant WIDE: [u64; 4] = [0x7fffffff, 0x80000000, 0x8000000000000000, 0x123456789abcdef];
5 +
6 +
fn check(a: u8, b: u16, c: u32, negative: i8, unsigned: u64) -> bool {
7 +
    let add: u8 = a + 10;
8 +
    let sub: u16 = b - 3;
9 +
    let mul: u32 = c * 7;
10 +
    let div: i8 = negative / 2;
11 +
    let rem: i8 = negative % 2;
12 +
    let one: i8 = 1;
13 +
    let zero: u8 = 0;
14 +
    let x: u8 = 10;
15 +
    let y: u8 = 6;
16 +
    let signed: i8 = -8;
17 +
    let high: u8 = 248;
18 +
    let minus: i8 = -1;
19 +
    let byte: u8 = 255;
20 +
    return add == 4 and sub == 65534 and mul == 21
21 +
        and div == -3 and rem == -1
22 +
        and unsigned / 3 == 3 and unsigned % 3 == 1
23 +
        and -one == -1 and ~zero == 255
24 +
        and (x & y) == 2 and (x | y) == 14 and (x ^ y) == 12
25 +
        and (one << 1) == 2 and (signed >> 2) == -2 and (high >> 2) == 62
26 +
        and minus as i32 == -1 and byte as u32 == 255
27 +
        and x <> y and minus < one and one >= minus
28 +
        and 1 as u8 < byte and byte >= 1 as u8;
29 +
}
30 +
@default fn main(env: *sys::Env) -> u32 {
31 +
    let passed = check(250, 1, 3, -7, 10)
32 +
        and WIDE[0] == 0x7fffffff and WIDE[1] == 0x80000000
33 +
        and WIDE[2] == 0x8000000000000000 and WIDE[3] == 0x123456789abcdef;
34 +
    match passed {
35 +
        case true => return 73,
36 +
        case false => return 100,
37 +
    }
38 +
}
kernel/user/sys.rad added +180 -0
1 +
//! Capability-checked user calls and views of kernel-authorized memory.
2 +
//! The image catalog links the native primitives in user/sys.ras.
3 +
4 +
use abi;
5 +
6 +
/// Startup ABI: exactly the ten u64 fields of core::domains::Env, in order.
7 +
/// Each entry receives its own immutable Env pointer in a0. Retain that pointer
8 +
/// explicitly; a process-global cached Env would name the wrong context.
9 +
export record Env: Copy {
10 +
    /// Readable startup argument address.
11 +
    argsPointer: u64,
12 +
    /// Number of readable startup argument bytes.
13 +
    argsSize: u64,
14 +
    /// Installed local Events handle.
15 +
    eventsHandle: u64,
16 +
    /// Address of the domain's shared event ring.
17 +
    eventsPointer: u64,
18 +
    /// Private image-instance mutable data base.
19 +
    stateBase: u64,
20 +
    /// Lowest address in the authorized Page stack.
21 +
    stackBase: u64,
22 +
    /// Exclusive upper bound of the authorized Page stack.
23 +
    stackTop: u64,
24 +
    /// Exact execution-context incarnation, not authority.
25 +
    context: u64,
26 +
    /// Lowest native stack address for locals and saved registers.
27 +
    privateStackBase: u64,
28 +
    /// Exclusive upper bound of the context's private native stack.
29 +
    privateStackTop: u64,
30 +
}
31 +
32 +
/// Native reply layout: a0 error followed by a1..a4 value words.
33 +
record Reply: Copy {
34 +
    /// Kernel ABI error number.
35 +
    error: u64,
36 +
    /// Operation-specific result words.
37 +
    values: [u64; 4],
38 +
}
39 +
40 +
/// Seven contiguous argument words for the native register bridge.
41 +
record Arguments: Copy {
42 +
    /// Values loaded into a0 through a6.
43 +
    registers: [u64; 7],
44 +
}
45 +
46 +
/// Native bridge has only three formal parameters; arguments is a thin pointer
47 +
/// to seven words, not a slice. The bridge preserves ordinary callee-saved state.
48 +
fn rawCall(number: u64, arguments: *Arguments, reply: *mut Reply);
49 +
/// Convert only an address validated by the kernel or the trusted startup ABI.
50 +
unsafe fn pointer(address: u64) -> *mut opaque;
51 +
/// Acquire one naturally aligned shared u32 through readable Page authority.
52 +
export fn loadAcquire(address: *u32) -> u32;
53 +
/// Release one naturally aligned shared u32 through writable Page authority.
54 +
export fn storeRelease(address: *mut u32, value: u32);
55 +
/// Exchange the private consumer lock; never expose producer state as an API.
56 +
fn lockAcquire(address: *mut u32) -> u32;
57 +
/// Release the shared consumer lock after copying an event.
58 +
fn lockRelease(address: *mut u32);
59 +
60 +
/// All input registers are initialized, including unused argument words.
61 +
fn invoke(number: u64, a: u64, b: u64, c: u64, d: u64) -> Reply {
62 +
    let arguments = Arguments { registers: [a, b, c, d, 0, 0, 0] };
63 +
    let mut reply: Reply = undefined;
64 +
    rawCall(number, &arguments, &mut reply);
65 +
    return reply;
66 +
}
67 +
68 +
/// Decode the kernel ABI error number. Unknown numbers are fatal ABI errors.
69 +
fn check(error: u64) throws (abi::Error) {
70 +
    match error {
71 +
        case 0 => return,
72 +
        case 1 => throw abi::Error::BadHandle,
73 +
        case 2 => throw abi::Error::Denied,
74 +
        case 3 => throw abi::Error::OutOfMemory,
75 +
        case 4 => throw abi::Error::InvalidArg,
76 +
        case 5 => throw abi::Error::Busy,
77 +
        case 6 => throw abi::Error::VerifyFailed,
78 +
        case 7 => throw abi::Error::NotPending,
79 +
        case 8 => throw abi::Error::Exhausted,
80 +
        else => panic "sys: unknown ABI error",
81 +
    }
82 +
}
83 +
84 +
/// Allocate count contiguous zeroed frames using explicit Allocate authority.
85 +
export fn pageAllocate(authority: abi::Handle, count: u64) -> abi::Handle throws (abi::Error) {
86 +
    let reply = invoke(30, authority.bits, count, 0, 0);
87 +
    try check(reply.error);
88 +
    return abi::Handle { bits: reply.values[0] };
89 +
}
90 +
91 +
/// Page metadata only; base alone is not dereferenceable authority.
92 +
export record PageInfo: Copy {
93 +
    /// Physical address of the first frame.
94 +
    base: u64,
95 +
    /// Number of contiguous 4 KiB frames.
96 +
    count: u32,
97 +
}
98 +
99 +
/// Invalid metadata queries fault the domain in the kernel, never return Error.
100 +
export fn queryPage(handle: abi::Handle) -> PageInfo {
101 +
    let reply = invoke(44, handle.bits, 0, 0, 0);
102 +
    return PageInfo { base: reply.values[0], count: reply.values[1] as u32 };
103 +
}
104 +
105 +
/// Materialize a checked byte subrange, never a metadata-derived arbitrary pointer.
106 +
/// Subtraction-based checks avoid overflow before any pointer is constructed.
107 +
fn pageRange(handle: abi::Handle, offset: u64, size: u32, rights: u16) -> u64 throws (abi::Error) {
108 +
    let reply = invoke(60, handle.bits, rights as u64, 0, 0);
109 +
    try check(reply.error);
110 +
    let base = reply.values[0];
111 +
    let extent = reply.values[1];
112 +
    if offset > extent or size as u64 > extent - offset
113 +
        or base > 0xffffffffffffffff - offset {
114 +
        throw abi::Error::InvalidArg;
115 +
    }
116 +
    let address = base + offset;
117 +
    if address > 0xffffffffffffffff - size as u64 { throw abi::Error::InvalidArg; }
118 +
    return address;
119 +
}
120 +
121 +
/// Mutable slices permit both loads and stores, therefore require Read AND Write.
122 +
export fn pageSliceMut(handle: abi::Handle, offset: u64, size: u32) -> *mut [u8] throws (abi::Error) {
123 +
    let address = try pageRange(handle, offset, size, abi::READ | abi::WRITE);
124 +
    return @sliceOf(pointer(address) as *mut u8, size);
125 +
}
126 +
127 +
/// Immutable startup bytes; fail rather than truncate an unrepresentable extent.
128 +
export fn envArgs(env: *Env) -> *[u8] {
129 +
    assert env.argsSize <= 0xffffffff;
130 +
    assert env.argsPointer <= 0xffffffffffffffff - env.argsSize;
131 +
    return @sliceOf(pointer(env.argsPointer) as *u8, env.argsSize as u32);
132 +
}
133 +
134 +
/// The fixed shared event ring capacity, matching core::events::CAPACITY.
135 +
constant EVENTS_CAPACITY: u32 = 256;
136 +
/// Notification wire layout: kind@0, reserved@2, code@4, value@8; 16 bytes.
137 +
export record Event: Copy {
138 +
    /// Interrupt=1, Timeout=2, Fault=3, ChildExit=4, Wakeup=5.
139 +
    kind: u16,
140 +
    /// Reserved ABI field, always zero in kernel-produced events.
141 +
    reserved: u16,
142 +
    /// Token, source number, exit status, or fault code.
143 +
    code: u32,
144 +
    /// Event-specific payload or sender identity.
145 +
    value: u64,
146 +
}
147 +
/// Shared layout only; no user API exposes this mutable producer state.
148 +
record Events: Copy {
149 +
    /// Fixed notification slots.
150 +
    data: [Event; EVENTS_CAPACITY],
151 +
    /// Consumer-released count of copied entries.
152 +
    head: u32,
153 +
    /// Kernel-published count of produced entries.
154 +
    tail: u32,
155 +
    /// Ring index mask.
156 +
    mask: u32,
157 +
    /// Shared lock for consumers in the same domain.
158 +
    consumer: u32,
159 +
}
160 +
161 +
/// Consume at most one event from the current domain's installed ring. Every
162 +
/// context uses this same consumer lock; no syscall or callback runs while held.
163 +
/// Acquire tail before copying; release head only after the entire record is
164 +
/// private. Empty and nonempty paths both release the lock. Kernel production
165 +
/// does not acquire this lock. Counters wrap at 32 bits, not at ring capacity.
166 +
export fn eventsPop(env: *Env) -> ?Event {
167 +
    let ring = pointer(env.eventsPointer) as *mut Events;
168 +
    while lockAcquire(&mut ring.consumer) <> 0 {}
169 +
    let head = loadAcquire(&ring.head);
170 +
    let tail = loadAcquire(&ring.tail);
171 +
    if head == tail {
172 +
        lockRelease(&mut ring.consumer);
173 +
        return nil;
174 +
    }
175 +
    let event = ring.data[head & (EVENTS_CAPACITY - 1)];
176 +
    let next = ((head as u64 + 1) & 0xffffffff) as u32;
177 +
    storeRelease(&mut ring.head, next);
178 +
    lockRelease(&mut ring.consumer);
179 +
    return event;
180 +
}
kernel/user/sys.ras added +115 -0
1 +
// Trusted RV64 primitives for the user::sys module.
2 +
// Register bridges, ordered memory access, and checked-address materialization
3 +
// use the compiler's native calling convention.
4 +
.text;
5 +
6 +
// a0=number, a1=thin pointer to seven u64 arguments, a2=Reply pointer.
7 +
// Reply is error@0 then four u64 values@8..32. Preserve ordinary callee-saved
8 +
// registers by never changing them. Save continuation/output on the aligned
9 +
// private stack: no caller-saved register is assumed to survive an ecall.
10 +
.export @"user::sys::rawCall";
11 +
@"user::sys::rawCall"
12 +
    addi %sp %sp -16;
13 +
    sd %a2 0(%sp);
14 +
    sd %ra 8(%sp);
15 +
    mv %a7 %a0;
16 +
    mv %t0 %a1;
17 +
    ld %a0 0(%t0);
18 +
    ld %a1 8(%t0);
19 +
    ld %a2 16(%t0);
20 +
    ld %a3 24(%t0);
21 +
    ld %a4 32(%t0);
22 +
    ld %a5 40(%t0);
23 +
    ld %a6 48(%t0);
24 +
    ecall;
25 +
    ld %t0 0(%sp);
26 +
    sd %a0 0(%t0);
27 +
    sd %a1 8(%t0);
28 +
    sd %a2 16(%t0);
29 +
    sd %a3 24(%t0);
30 +
    sd %a4 32(%t0);
31 +
    ld %ra 8(%sp);
32 +
    addi %sp %sp 16;
33 +
    ret;
34 +
35 +
// Identity conversion; only the RAD wrappers supply validated mapped addresses.
36 +
.export @"user::sys::pointer";
37 +
@"user::sys::pointer"
38 +
    ret;
39 +
40 +
// Naturally aligned shared u32 accesses. Full fences match core::atomic.
41 +
.export @"user::sys::loadAcquire";
42 +
@"user::sys::loadAcquire"
43 +
    lwu %a0 0(%a0);
44 +
    fence;
45 +
    ret;
46 +
.export @"user::sys::storeRelease";
47 +
@"user::sys::storeRelease"
48 +
    fence;
49 +
    sw %a1 0(%a0);
50 +
    ret;
51 +
52 +
// The consumer lock is separate from kernel producer progress. Acquiring it
53 +
// returns its prior value; release does not change head or tail.
54 +
.export @"user::sys::lockAcquire";
55 +
@"user::sys::lockAcquire"
56 +
    li %t0 1;
57 +
    amoswap.w.aq %a0 %t0 0(%a0);
58 +
    slli %a0 %a0 32;
59 +
    srli %a0 %a0 32;
60 +
    ret;
61 +
.export @"user::sys::lockRelease";
62 +
@"user::sys::lockRelease"
63 +
    amoswap.w.rl %zero %zero 0(%a0);
64 +
    ret;
65 +
66 +
// One-shot MMIO accesses. RAD obtains each address from call 61 immediately
67 +
// before invoking its matching primitive; no reusable Device pointer escapes.
68 +
.export @"user::sys::read8";
69 +
@"user::sys::read8"
70 +
    fence;
71 +
    lbu %a0 0(%a0);
72 +
    fence;
73 +
    ret;
74 +
.export @"user::sys::read16";
75 +
@"user::sys::read16"
76 +
    fence;
77 +
    lhu %a0 0(%a0);
78 +
    fence;
79 +
    ret;
80 +
.export @"user::sys::read32";
81 +
@"user::sys::read32"
82 +
    fence;
83 +
    lwu %a0 0(%a0);
84 +
    fence;
85 +
    ret;
86 +
.export @"user::sys::read64";
87 +
@"user::sys::read64"
88 +
    fence;
89 +
    ld %a0 0(%a0);
90 +
    fence;
91 +
    ret;
92 +
.export @"user::sys::write8";
93 +
@"user::sys::write8"
94 +
    fence;
95 +
    sb %a1 0(%a0);
96 +
    fence;
97 +
    ret;
98 +
.export @"user::sys::write16";
99 +
@"user::sys::write16"
100 +
    fence;
101 +
    sh %a1 0(%a0);
102 +
    fence;
103 +
    ret;
104 +
.export @"user::sys::write32";
105 +
@"user::sys::write32"
106 +
    fence;
107 +
    sw %a1 0(%a0);
108 +
    fence;
109 +
    ret;
110 +
.export @"user::sys::write64";
111 +
@"user::sys::write64"
112 +
    fence;
113 +
    sd %a1 0(%a0);
114 +
    fence;
115 +
    ret;
lib/std/arch/rv64/emit.rad +3 -0
110 110
    pendingCalls: *mut [PendingCall],
111 111
    /// Assembly jumps needing offset patching.
112 112
    pendingJumps: *mut [PendingJump],
113 113
    /// Function address loads needing offset patching.
114 114
    pendingAddrLoads: *mut [PendingAddrLoad],
115 +
    /// Current image's private data offsets relative to gp, or nil for fixed data.
116 +
    instanceData: ?*dict::Dict,
115 117
    /// Block label tracking.
116 118
    labels: labels::Labels,
117 119
    /// Function start positions for printing.
118 120
    funcs: *mut [types::FuncAddr],
119 121
    /// Debug entries mapping PCs to source locations.
204 206
        codeLen: 0,
205 207
        pendingBranches: @sliceOf((pendingBranches as *mut [PendingBranch]).ptr, 0, MAX_PENDING),
206 208
        pendingCalls: @sliceOf((pendingCalls as *mut [PendingCall]).ptr, 0, MAX_PENDING),
207 209
        pendingJumps: @sliceOf((pendingJumps as *mut [PendingJump]).ptr, 0, MAX_PENDING),
208 210
        pendingAddrLoads: @sliceOf((pendingAddrLoads as *mut [PendingAddrLoad]).ptr, 0, MAX_PENDING),
211 +
        instanceData: nil,
209 212
        labels: labels::init(blockOffsets as *mut [i32], funcEntries as *mut [dict::Entry]),
210 213
        funcs: @sliceOf((funcs as *mut [types::FuncAddr]).ptr, 0, MAX_FUNCS),
211 214
        debugEntries,
212 215
        debugEntriesLen: 0,
213 216
    };
lib/std/arch/rv64/isel.rad +14 -0
26 26
//!   loadVal(rd, val) -> Reg
27 27
//!     Force an [`il::Val`] into a specific register `rd`. Built on [`resolveVal`] + [`emitMv`].
28 28
//!     Used when the instruction requires the value in `rd` (e.g. `sub rd, rd, rs2`).
29 29
30 30
use std::mem;
31 +
use std::collections::dict;
31 32
use std::lang::il;
32 33
use std::lang::gen;
33 34
use std::lang::gen::regalloc;
34 35
use std::lang::gen::labels;
35 36
169 170
            }
170 171
            emit::loadImm(s.e, scratch, imm);
171 172
            return scratch;
172 173
        },
173 174
        case il::Val::DataSym(name) => {
175 +
            if let offsets = s.e.instanceData {
176 +
                if let offset = dict::get(offsets, name) {
177 +
                    assert offset >= 0;
178 +
                    if offset == 0 { return super::GP; }
179 +
                    if offset <= super::MAX_IMM {
180 +
                        emit::emit(s.e, encode::addi(scratch, super::GP, offset));
181 +
                    } else {
182 +
                        emit::loadImm(s.e, scratch, offset as i64);
183 +
                        emit::emit(s.e, encode::add(scratch, super::GP, scratch));
184 +
                    }
185 +
                    return scratch;
186 +
                }
187 +
            }
174 188
            emit::recordDataAddrLoad(s.e, name, scratch);
175 189
            return scratch;
176 190
        },
177 191
        case il::Val::FnAddr(name) => {
178 192
            emit::recordAddrLoad(s.e, name, scratch);
seed/update +2 -1
39 39
# ---------------------------------------------------------------------------
40 40
# Command line flags for Radiance compiler
41 41
# ---------------------------------------------------------------------------
42 42
43 43
STD_MODS="$(sed 's/^/-mod /' std.lib | tr '\n' ' ')"
44 -
OPTS="-pkg std ${STD_MODS} -pkg radiance -mod compiler/radiance.rad -mod compiler/radiance/binary.rad -entry radiance"
44 +
COMPILER_MODS="$(for source in compiler/radiance.rad compiler/radiance/*.rad compiler/radiance/*/*.rad; do if [ -f "$source" ]; then printf '%s %s ' -mod "$source"; fi; done)"
45 +
OPTS="-pkg std ${STD_MODS} -pkg radiance ${COMPILER_MODS} -entry radiance"
45 46
46 47
# ---------------------------------------------------------------------------
47 48
# Emulator settings
48 49
# ---------------------------------------------------------------------------
49 50