compiler: Emit separate binary RIL packages

b06f8930a27caa2f22fb797f2fc53bfa289bbed526f61330f5224631e4eb1b9b
Assisted-by: Codex:gpt-6
Alexis Sellier committed ago 1 parent f720761a
Makefile +9 -2
21 21
22 22
# Verify the emulator binary exists.
23 23
EMU_PATH := $(shell command -v $(EMU) 2>/dev/null)
24 24
25 25
default: emulator $(RAD_BIN)
26 -
test: emulator seed-test std-test bin-test kernel-test
26 +
test: emulator seed-test std-test bin-test kernel-test package-test
27 27
28 28
seed-test:
29 29
	@seed/test
30 30
31 31
# Emulator command check
77 77
78 78
$(KERNEL_TEST): $(KERNEL_SRC) $(KERNEL_TEST_SRC) $(STD_LIB) $(RAD_BIN)
79 79
	@echo "radiance kernel tests => $@"
80 80
	@$(RADIANCE) -test $(STD_TEST) $(KERNEL) $(patsubst %,-mod %,$(KERNEL_TEST_SRC)) -entry kernel -o $@
81 81
82 +
# Separate binary package emission and decode checks.
83 +
package-test: emulator $(BIN_DIR)/packages.check.rv64
84 +
	@sh test/packages/run
85 +
86 +
$(BIN_DIR)/packages.check.rv64: test/packages/check.rad $(STD_LIB) $(RAD_BIN)
87 +
	@$(RADIANCE) $(STD) -pkg check -mod $< -entry check -o $@
88 +
82 89
# Binary Tests
83 90
84 91
BIN_TEST_DIR := test/tests
85 92
# Only tests with `//! returns:` are compiled to binaries and executed.
86 93
BIN_TEST_EXE_SRC := $(shell grep -rl '^//! returns:' $(BIN_TEST_DIR))
134 141
clean: clean-std-test clean-bin-test clean-rad
135 142
136 143
t: test
137 144
c: clean
138 145
139 -
.PHONY: test clean default seed-test std-test bin-test kernel-test seed \
146 +
.PHONY: test clean default seed-test std-test bin-test kernel-test package-test seed \
140 147
	clean-std-test clean-bin-test clean-rad emulator
141 148
.SUFFIXES:
142 149
.DELETE_ON_ERROR:
143 150
.SILENT:
compiler/radiance.rad +145 -1
17 17
use std::arch::rv64;
18 18
use std::arch::rv64::asm;
19 19
use std::arch::rv64::printer;
20 20
use std::lang::sexpr;
21 21
use std::lang::gen::data;
22 +
use std::lang::il::binary;
23 +
use std::lang::il::binary::collect;
24 +
use std::lang::il::binary::program;
22 25
use std::lang::gen::types;
23 26
use std::sys;
24 27
use std::sys::unix;
25 28
use std::collections::dict;
26 29
67 70
/// Code generation storage.
68 71
unsafe static CODEGEN_DATA_SYMS: [data::DataSym; data::MAX_DATA_SYMS] = undefined;
69 72
/// Hash table entries for data symbol lookup.
70 73
unsafe static CODEGEN_DATA_SYM_ENTRIES: [dict::Entry; data::DATA_SYM_TABLE_SIZE] = undefined;
71 74
75 +
/// Maximum number of exported definitions in one binary package.
76 +
constant MAX_PACKAGE_EXPORTS: u32 = 8192;
77 +
/// Maximum number of indexed names in one binary package.
78 +
constant MAX_PACKAGE_SYMBOLS: u32 = 16384;
79 +
/// Export workspace reused for each binary package.
80 +
unsafe static PACKAGE_EXPORTS: [binary::Export; MAX_PACKAGE_EXPORTS] = undefined;
81 +
/// Symbol workspace reused for each binary package.
82 +
unsafe static PACKAGE_SYMBOLS: [*[u8]; MAX_PACKAGE_SYMBOLS] = undefined;
83 +
72 84
/// Debug info file extension.
73 85
constant DEBUG_EXT: *[u8] = ".debug";
74 86
75 87
/// Maximum rodata size (4MB).
76 88
constant MAX_RO_DATA_SIZE: u32 = 4194304;
96 108
/// Symbol name exported for startup code to call the semantic entry function.
97 109
export constant DEFAULT_ENTRY_SYMBOL: *[u8] = "::default";
98 110
99 111
/// Usage string.
100 112
constant USAGE: *[u8] =
101 -
    "usage: radiance -pkg <name> [-start <input.ras>] -mod <input>.. [-pkg <name> -mod <input>..] -entry <pkg> -o <output>\n";
113 +
    "usage: radiance -pkg <name> [-start <input.ras>] -mod <input>.. [-pkg <name> -mod <input>..] -entry <pkg> [-o <output> | -ril <directory>]\n";
102 114
103 115
/// Compiler error.
104 116
union Error: Copy {
105 117
    Other,
106 118
}
159 171
    config: resolver::Config,
160 172
    /// What to dump during compilation.
161 173
    dump: Dump,
162 174
    /// Output path for binary.
163 175
    outputPath: ?*[u8],
176 +
    /// Output directory for separate binary RIL packages.
177 +
    rilDirectory: ?*[u8],
164 178
    /// Whether to emit debug info (.debug file).
165 179
    debug: bool,
166 180
}
167 181
168 182
/// Root module info for a package.
293 307
    arena: &mut ast::NodeArena
294 308
) -> CompileContext throws (Error) {
295 309
    let mut buildTest = false;
296 310
    let mut debugEnabled = false;
297 311
    let mut outputPath: ?*[u8] = nil;
312 +
    let mut rilDirectory: ?*[u8] = nil;
298 313
    let mut dump = Dump::None;
299 314
    let mut entryPkgName: ?*[u8] = nil;
300 315
301 316
    // Per-package source path tracking.
302 317
    let mut inputs: [PackageInput; MAX_PACKAGES] = undefined;
359 374
        } else if mem::eq(arg, "-debug") {
360 375
            set debugEnabled = true;
361 376
        } else if mem::eq(arg, "-o") {
362 377
            try nextArg(args, &mut idx, &["`-o` requires an output path"]);
363 378
            set outputPath = args[idx];
379 +
        } else if mem::eq(arg, "-ril") {
380 +
            try nextArg(args, &mut idx, &["`-ril` requires an output directory"]);
381 +
            set rilDirectory = args[idx];
364 382
        } else if mem::eq(arg, "-dump") {
365 383
            try nextArg(args, &mut idx, &["`-dump` requires a mode (eg. ast)"]);
366 384
            let mode = args[idx];
367 385
            if mem::eq(mode, "ast") {
368 386
                set dump = Dump::Ast;
415 433
    for i in 0..pkgCount {
416 434
        if i <> entryIdx and inputs[i].startupPath <> nil {
417 435
            throw error(&["`-start` is only supported on the entry package"]);
418 436
        }
419 437
    }
438 +
    if rilDirectory <> nil and (outputPath <> nil or dump <> Dump::None) {
439 +
        throw error(&["`-ril` requires a separate invocation from `-o` or `-dump`"]);
440 +
    }
420 441
    let graph = module::moduleGraph(&mut MODULE_ENTRIES[..], arena);
421 442
    let mut ctx = CompileContext {
422 443
        packages: undefined,
423 444
        inputs,
424 445
        packageCount: pkgCount,
425 446
        entryPkgIdx,
426 447
        graph,
427 448
        config: resolver::Config { buildTest },
428 449
        dump,
429 450
        outputPath,
451 +
        rilDirectory,
430 452
        debug: debugEnabled,
431 453
    };
432 454
    // Initialize and parse all packages.
433 455
    let mut sourceArena = alloc::new(&mut MODULE_SOURCES[..]);
434 456
    for i in 0..pkgCount {
1027 1049
        pkgLog(entryPkg, &["generating code", "(", path, ")", ".."]);
1028 1050
    }
1029 1051
    return rv64::finishProgram(&mut generator, low.data, storage, asmData, &mut RO_DATA_BUF[..], &mut RW_DATA_BUF[..]);
1030 1052
}
1031 1053
1054 +
/// Source exports selected for one binary RIL package.
1055 +
record PackageExports: Copy {
1056 +
    /// Number of initialized entries in the caller's export table.
1057 +
    count: u32,
1058 +
    /// Default function entry.
1059 +
    entry: ?*[u8],
1060 +
}
1061 +
1062 +
/// Collect exported definitions and the default entry from a package's source modules.
1063 +
unsafe fn packageExports(
1064 +
    ctx: &CompileContext,
1065 +
    pkg: &package::Package,
1066 +
    ilProgram: &il::Program,
1067 +
    exports: &mut [binary::Export],
1068 +
    arena: &mut alloc::Arena
1069 +
) -> PackageExports throws (Error) {
1070 +
    let mut count: u32 = 0;
1071 +
    let mut entryName: ?*[u8] = nil;
1072 +
    for i in 0..ctx.graph.entriesLen {
1073 +
        let modEntry = module::get(&ctx.graph, i as u16) else continue;
1074 +
        if modEntry.packageId <> pkg.id { continue; }
1075 +
        let root = modEntry.ast else continue;
1076 +
        let case ast::NodeValue::Block(block) = root.value else continue;
1077 +
        for node in block.statements {
1078 +
            let mut ident: ?*ast::Node = nil;
1079 +
            let mut attrs: ?ast::Attributes = nil;
1080 +
            let mut kind = binary::ExportKind::Function;
1081 +
            match node.value {
1082 +
                case ast::NodeValue::FnDecl(decl) => { set ident = decl.name; set attrs = decl.attrs; },
1083 +
                case ast::NodeValue::ConstDecl(decl) => {
1084 +
                    set ident = decl.ident; set attrs = decl.attrs; set kind = binary::ExportKind::Data;
1085 +
                },
1086 +
                case ast::NodeValue::StaticDecl(decl) => {
1087 +
                    set ident = decl.ident; set attrs = decl.attrs; set kind = binary::ExportKind::Data;
1088 +
                },
1089 +
                else => continue,
1090 +
            }
1091 +
            let attributes = attrs else continue;
1092 +
            let isDefault = ast::attributesContains(&attributes, ast::Attribute::Default)
1093 +
                and pkg.rootModuleId == modEntry.id;
1094 +
            if not isDefault and not ast::attributesContains(&attributes, ast::Attribute::Export) {
1095 +
                continue;
1096 +
            }
1097 +
            let nameNode = ident else continue;
1098 +
            let case ast::NodeValue::Ident(name) = nameNode.value else continue;
1099 +
            let qualified = il::formatQualifiedName(arena, module::moduleQualifiedPath(modEntry), name);
1100 +
            let mut present = false;
1101 +
            match kind {
1102 +
                case binary::ExportKind::Function => {
1103 +
                    for func in ilProgram.fns {
1104 +
                        if mem::eq(func.name, qualified) { set present = true; }
1105 +
                    }
1106 +
                },
1107 +
                case binary::ExportKind::Data => {
1108 +
                    for item in ilProgram.data {
1109 +
                        if mem::eq(item.name, qualified) { set present = true; }
1110 +
                    }
1111 +
                },
1112 +
            }
1113 +
            if not present { continue; }
1114 +
            if count == exports.len { throw error(&["too many package exports"]); }
1115 +
            set exports[count] = binary::Export { name: qualified, kind };
1116 +
            set count += 1;
1117 +
            if isDefault { set entryName = qualified; }
1118 +
        }
1119 +
    }
1120 +
    return PackageExports { count, entry: entryName };
1121 +
}
1122 +
1123 +
/// Emit one binary RIL file per package into an existing directory.
1124 +
unsafe fn emitPackages(ctx: *unsafe mut CompileContext, res: *unsafe mut resolver::Resolver, directory: *[u8]) throws (Error) {
1125 +
    for i in 0..ctx.packageCount {
1126 +
        if ctx.inputs[i].asmPathCount > 0 or ctx.inputs[i].startupPath <> nil {
1127 +
            throw error(&["binary RIL output requires Radiance source modules"]);
1128 +
        }
1129 +
    }
1130 +
    let unified = try lowerAllPackages(ctx, res);
1131 +
    let allocator = alloc::arenaAllocator(&mut res.arena);
1132 +
    for i in 0..ctx.packageCount {
1133 +
        let pkg = &ctx.packages[i];
1134 +
        let mut dataItems: *mut [il::Data] = &mut [];
1135 +
        let mut functions: *unsafe mut [*unsafe il::Fn] = &mut [];
1136 +
        for item in unified.data {
1137 +
            if ownsSymbol(pkg.name, item.name) { dataItems.append(item, allocator); }
1138 +
        }
1139 +
        for func in unified.fns {
1140 +
            if ownsSymbol(pkg.name, func.name) { functions.append(func, allocator); }
1141 +
        }
1142 +
        let local = il::Program { data: &dataItems[..], fns: functions };
1143 +
        let selected = try packageExports(ctx, pkg, &local, &mut PACKAGE_EXPORTS[..], &mut res.arena);
1144 +
        let mut dependencies: [*[u8]; MAX_PACKAGES] = undefined;
1145 +
        let mut names = collect::new(&mut PACKAGE_SYMBOLS[..], &mut dependencies[..]);
1146 +
        let image = try collect::package(&mut names, pkg.name, local, &PACKAGE_EXPORTS[..selected.count], selected.entry) catch {
1147 +
            throw error(&["cannot collect binary RIL package", pkg.name]);
1148 +
        };
1149 +
        let length = try program::encode(&mut FN_ARENA[..], &image) catch {
1150 +
            throw error(&["binary RIL output capacity exceeded", pkg.name]);
1151 +
        };
1152 +
        let mut path: [u8; MAX_PATH_LEN] = undefined;
1153 +
        let mut pos: u32 = 0;
1154 +
        for part in &[directory, "/", pkg.name, ".ril"] {
1155 +
            set pos += try mem::copy(&mut path[pos..MAX_PATH_LEN - 1], part) catch {
1156 +
                throw error(&["binary RIL output path is too long"]);
1157 +
            };
1158 +
        }
1159 +
        set path[pos] = 0;
1160 +
        if not unix::writeFile(&path[..pos], &FN_ARENA[..length]) {
1161 +
            throw error(&["cannot write binary RIL package", pkg.name]);
1162 +
        }
1163 +
    }
1164 +
}
1165 +
1166 +
/// Match a qualified definition to its package name.
1167 +
fn ownsSymbol(owner: *[u8], name: *[u8]) -> bool {
1168 +
    let suffix = mem::stripPrefix(owner, name) else return false;
1169 +
    return suffix.len > 2 and suffix[0] == ':' and suffix[1] == ':';
1170 +
}
1171 +
1032 1172
/// Lower, optionally dump, and optionally generate binary output.
1033 1173
unsafe fn compile(
1034 1174
    ctx: *unsafe mut CompileContext,
1035 1175
    res: *unsafe mut resolver::Resolver,
1036 1176
    fnArena: &mut alloc::Arena
1037 1177
) throws (Error) {
1038 1178
    let entryPkg = try getEntryPackage(ctx);
1179 +
    if let directory = ctx.rilDirectory {
1180 +
        try emitPackages(ctx, res, directory);
1181 +
        return;
1182 +
    }
1039 1183
    let mut out = sexpr::Output::Stdout;
1040 1184
1041 1185
    if ctx.dump == Dump::Il {
1042 1186
        // Lower all packages into a single unified IL program for dumping.
1043 1187
        let program = try lowerAllPackages(ctx, res);
lib/std/lang/il/binary.rad +1 -0
3 3
//! Optional fields use a byte: zero for absent, one for present.
4 4
5 5
export mod writer;
6 6
export mod reader;
7 7
export mod program;
8 +
export mod collect;
8 9
9 10
use std::lang::il;
10 11
@test export mod tests;
11 12
@test export mod decodeTests;
12 13
lib/std/lang/il/binary/collect.rad added +186 -0
1 +
//! Deterministic symbol and dependency collection for package-local IL.
2 +
3 +
use std::mem;
4 +
use std::lang::il;
5 +
use std::lang::il::binary;
6 +
7 +
/// Caller-owned storage for a package's indexed names.
8 +
/// Both tables must outlive the collector and all packages that use them.
9 +
export record Names: Copy {
10 +
    /// Unique names in first-use order.
11 +
    symbols: *unsafe mut [*[u8]],
12 +
    /// Number of initialized symbol entries.
13 +
    symbolCount: u32,
14 +
    /// Unique dependency names in first-use order.
15 +
    dependencies: *unsafe mut [*[u8]],
16 +
    /// Number of initialized dependency entries.
17 +
    dependencyCount: u32,
18 +
}
19 +
20 +
/// Start collection with empty caller-provided tables.
21 +
/// The caller must retain exclusive access to both tables during collection.
22 +
export unsafe fn new(symbols: &mut [*[u8]], dependencies: &mut [*[u8]]) -> Names {
23 +
    return Names { symbols: symbols as *unsafe mut [*[u8]], symbolCount: 0, dependencies: dependencies as *unsafe mut [*[u8]], dependencyCount: 0 };
24 +
}
25 +
26 +
/// Add a unique name and check the symbol table's capacity.
27 +
unsafe fn add(names: &mut Names, name: *[u8]) throws (binary::Error) {
28 +
    if name.len == 0 {
29 +
        throw binary::Error::Invalid;
30 +
    }
31 +
    for i in 0..names.symbolCount {
32 +
        if mem::eq(names.symbols[i], name) {
33 +
            return;
34 +
        }
35 +
    }
36 +
    if names.symbolCount == names.symbols.len {
37 +
        throw binary::Error::Capacity;
38 +
    }
39 +
    set names.symbols[names.symbolCount] = name;
40 +
    set names.symbolCount += 1;
41 +
}
42 +
43 +
/// Add a qualified reference and its owning package dependency.
44 +
unsafe fn reference(names: &mut Names, owner: *[u8], name: *[u8]) throws (binary::Error) {
45 +
    try add(names, name);
46 +
    for i in 0..name.len {
47 +
        if name[i] == ':' and i + 1 < name.len and name[i + 1] == ':' {
48 +
            if i == 0 or i + 2 == name.len {
49 +
                throw binary::Error::Invalid;
50 +
            }
51 +
            let dependency = &name[..i];
52 +
            if mem::eq(dependency, owner) {
53 +
                return;
54 +
            }
55 +
            try add(names, dependency);
56 +
            for j in 0..names.dependencyCount {
57 +
                if mem::eq(names.dependencies[j], dependency) {
58 +
                    return;
59 +
                }
60 +
            }
61 +
            if names.dependencyCount == names.dependencies.len {
62 +
                throw binary::Error::Capacity;
63 +
            }
64 +
            set names.dependencies[names.dependencyCount] = dependency;
65 +
            set names.dependencyCount += 1;
66 +
            return;
67 +
        }
68 +
    }
69 +
    throw binary::Error::Invalid;
70 +
}
71 +
72 +
/// Collect names referenced by one IL value.
73 +
unsafe fn value(names: &mut Names, owner: *[u8], item: il::Val) throws (binary::Error) {
74 +
    match item {
75 +
        case il::Val::DataSym(name) => try reference(names, owner, name),
76 +
        case il::Val::FnAddr(name) => try reference(names, owner, name),
77 +
        else => {},
78 +
    }
79 +
}
80 +
81 +
/// Collect names from an instruction's variable-length operand sequence.
82 +
unsafe fn values(names: &mut Names, owner: *[u8], items: &[il::Val]) throws (binary::Error) {
83 +
    for item in items {
84 +
        try value(names, owner, item);
85 +
    }
86 +
}
87 +
88 +
/// Collect all symbolic instruction operands.
89 +
unsafe fn instruction(names: &mut Names, owner: *[u8], instr: il::Instr) throws (binary::Error) {
90 +
    match instr {
91 +
        case il::Instr::Reserve { size, .. } => try value(names, owner, size),
92 +
        case il::Instr::Blit { size, .. } => try value(names, owner, size),
93 +
        case il::Instr::Store { src, .. } => try value(names, owner, src),
94 +
        case il::Instr::Copy { val, .. } => try value(names, owner, val),
95 +
        case il::Instr::Zext { val, .. } => try value(names, owner, val),
96 +
        case il::Instr::Sext { val, .. } => try value(names, owner, val),
97 +
        case il::Instr::BinOp { a, b, .. } => {
98 +
            try value(names, owner, a);
99 +
            try value(names, owner, b);
100 +
        },
101 +
        case il::Instr::UnOp { a, .. } => try value(names, owner, a),
102 +
        case il::Instr::Call { func, args, .. } => {
103 +
            try value(names, owner, func);
104 +
            try values(names, owner, args);
105 +
        },
106 +
        case il::Instr::Ret { val } => {
107 +
            if let item = val {
108 +
                try value(names, owner, item);
109 +
            }
110 +
        },
111 +
        case il::Instr::Jmp { args, .. } => try values(names, owner, args),
112 +
        case il::Instr::Br { a, b, thenArgs, elseArgs, .. } => {
113 +
            try value(names, owner, a);
114 +
            try value(names, owner, b);
115 +
            try values(names, owner, thenArgs);
116 +
            try values(names, owner, elseArgs);
117 +
        },
118 +
        case il::Instr::Switch { val, defaultArgs, cases, .. } => {
119 +
            try value(names, owner, val);
120 +
            try values(names, owner, defaultArgs);
121 +
            for branch in cases {
122 +
                try values(names, owner, branch.args);
123 +
            }
124 +
        },
125 +
        case il::Instr::Ecall { num, a0, a1, a2, a3, .. } => {
126 +
            try value(names, owner, num);
127 +
            try value(names, owner, a0);
128 +
            try value(names, owner, a1);
129 +
            try value(names, owner, a2);
130 +
            try value(names, owner, a3);
131 +
        },
132 +
        case il::Instr::Load { .. }, il::Instr::Sload { .. }, il::Instr::Unreachable,
133 +
             il::Instr::Ebreak, il::Instr::MemoryFence => {},
134 +
    }
135 +
}
136 +
137 +
/// Check that a definition belongs to the selected package.
138 +
unsafe fn definition(names: &mut Names, owner: *[u8], name: *[u8]) throws (binary::Error) {
139 +
    let rest = mem::stripPrefix(owner, name) else {
140 +
        throw binary::Error::Invalid;
141 +
    };
142 +
    if rest.len < 3 or rest[0] <> ':' or rest[1] <> ':' {
143 +
        throw binary::Error::Invalid;
144 +
    }
145 +
    try add(names, name);
146 +
}
147 +
148 +
/// Build package tables from local definitions and their qualified references.
149 +
/// Returned tables borrow `names`. Reset the collector before building another package.
150 +
export unsafe fn package(
151 +
    names: &mut Names,
152 +
    owner: *[u8],
153 +
    program: il::Program,
154 +
    exports: &[binary::Export],
155 +
    entry: ?*[u8]
156 +
) -> binary::Package throws (binary::Error) {
157 +
    try add(names, owner);
158 +
    for item in exports {
159 +
        try definition(names, owner, item.name);
160 +
    }
161 +
    if let name = entry {
162 +
        try definition(names, owner, name);
163 +
    }
164 +
    for item in program.data {
165 +
        try definition(names, owner, item.name);
166 +
        for data in item.values {
167 +
            match data.item {
168 +
                case il::DataItem::Sym(name) => try reference(names, owner, name),
169 +
                case il::DataItem::Fn(name) => try reference(names, owner, name),
170 +
                else => {},
171 +
            }
172 +
        }
173 +
    }
174 +
    for func in program.fns {
175 +
        try definition(names, owner, func.name);
176 +
        for block in func.blocks {
177 +
            for instr in block.instrs {
178 +
                try instruction(names, owner, instr);
179 +
            }
180 +
        }
181 +
    }
182 +
    return binary::Package {
183 +
        symbols: &names.symbols[..names.symbolCount], name: owner,
184 +
        dependencies: &names.dependencies[..names.dependencyCount], exports: exports as *unsafe [binary::Export], entry, program,
185 +
    };
186 +
}
lib/std/lang/il/binary/decodeTests.rad +41 -0
4 4
use std::lang::alloc;
5 5
use std::lang::il;
6 6
use std::lang::il::binary;
7 7
use std::lang::il::binary::reader;
8 8
use std::lang::il::binary::program;
9 +
use std::lang::il::binary::collect;
9 10
10 11
/// Decode arena backing storage. Tests reset it before each use.
11 12
static MEMORY: [u8; 2048] = [0; 2048];
12 13
13 14
/// Structural limits for small test programs.
302 303
        };
303 304
        try testing::expect(failed);
304 305
    }
305 306
}
306 307
308 +
/// Check symbol collection capacity and package ownership failures.
309 +
@test unsafe fn collectionBounds() throws (testing::TestError) {
310 +
    let mut symbols: [*[u8]; 8] = undefined;
311 +
    let mut dependencies: [*[u8]; 1] = undefined;
312 +
    let empty = il::Program { data: &[], fns: &[] };
313 +
    let mut names = collect::new(&mut symbols[..0], &mut dependencies[..]);
314 +
    let mut failed = false;
315 +
    try collect::package(&mut names, "p", empty, &[], nil) catch err {
316 +
        try testing::expect(err == binary::Error::Capacity);
317 +
        set failed = true;
318 +
    };
319 +
    try testing::expect(failed);
320 +
    let item = il::Data {
321 +
        name: "p::data", size: 8, alignment: 8, readOnly: false, isZeroInit: false,
322 +
        values: &[il::DataValue { item: il::DataItem::Fn("dep::fn"), count: 1 }],
323 +
    };
324 +
    let local = il::Program { data: retainData(&[item]), fns: &[] };
325 +
    set names = collect::new(&mut symbols[..], &mut dependencies[..0]);
326 +
    set failed = false;
327 +
    try collect::package(&mut names, "p", local, &[], nil) catch err {
328 +
        try testing::expect(err == binary::Error::Capacity);
329 +
        set failed = true;
330 +
    };
331 +
    try testing::expect(failed);
332 +
    set names = collect::new(&mut symbols[..], &mut dependencies[..]);
333 +
    set failed = false;
334 +
    try collect::package(&mut names, "other", local, &[], nil) catch err {
335 +
        try testing::expect(err == binary::Error::Invalid);
336 +
        set failed = true;
337 +
    };
338 +
    try testing::expect(failed);
339 +
    set names = collect::new(&mut symbols[..], &mut dependencies[..]);
340 +
    let package = try collect::package(&mut names, "p", local, &[], nil) catch {
341 +
        throw testing::TestError::Failed;
342 +
    };
343 +
    try testing::expect(package.dependencies.len == 1);
344 +
    try testing::expectBytesEq(package.dependencies[0], "dep");
345 +
    try testing::expect(package.symbols.len == 4);
346 +
}
347 +
307 348
/// Copy data fixtures into stable storage for the package descriptor.
308 349
unsafe fn retainData(items: &[il::Data]) -> *[il::Data] {
309 350
    unsafe static DATA: [il::Data; 2] = undefined;
310 351
    assert items.len <= DATA.len;
311 352
    for item, i in items { set DATA[i] = item; }
std.lib +1 -0
33 33
lib/std/lang/il/printer.rad
34 34
lib/std/lang/il/binary.rad
35 35
lib/std/lang/il/binary/writer.rad
36 36
lib/std/lang/il/binary/reader.rad
37 37
lib/std/lang/il/binary/program.rad
38 +
lib/std/lang/il/binary/collect.rad
38 39
lib/std/lang/resolver.rad
39 40
lib/std/lang/resolver/printer.rad
40 41
lib/std/lang/lower.rad
41 42
lib/std/lang/module.rad
42 43
lib/std/lang/module/printer.rad
test/packages/app.rad added +4 -0
1 +
//! Application package fixture.
2 +
use support;
3 +
/// Return the shared package's state.
4 +
@default fn main() -> u64 { return support::read() + support::value; }
test/packages/check.rad added +70 -0
1 +
//! Verify compiler-emitted binary RIL package boundaries.
2 +
3 +
use std::sys;
4 +
use std::sys::unix;
5 +
use std::mem;
6 +
use std::lang::alloc;
7 +
use std::lang::il;
8 +
use std::lang::il::binary;
9 +
use std::lang::il::binary::program;
10 +
11 +
/// Maximum fixture image size.
12 +
constant IMAGE_SIZE: u32 = 4096;
13 +
/// Arena capacity for both decoded fixture packages.
14 +
constant ARENA_SIZE: u32 = 32768;
15 +
/// Input image storage.
16 +
static IMAGE: [u8; IMAGE_SIZE] = [0; IMAGE_SIZE];
17 +
/// Decoded package storage.
18 +
static MEMORY: [u8; ARENA_SIZE] = [0; ARENA_SIZE];
19 +
20 +
/// Decode one compiler output file.
21 +
unsafe fn load(path: *[u8], arena: &mut alloc::Arena) -> binary::Package {
22 +
    let length = unix::readFile(path, &mut IMAGE[..]) else panic "load: cannot read fixture";
23 +
    return try! program::decode(&IMAGE[..length], arena, binary::Limits { registers: 8192, blocks: 4096 });
24 +
}
25 +
26 +
/// Check identity, ownership, linkage, and source exports for two packages.
27 +
@default unsafe fn main(env: *sys::Env) -> i32 {
28 +
    assert env.args.len == 3, "main: expected two package paths";
29 +
    let mut arena = alloc::new(&mut MEMORY[..]);
30 +
    let support = load(env.args[1], &mut arena);
31 +
    let app = load(env.args[2], &mut arena);
32 +
    assert mem::eq(support.name, "support"), "main: mem::eq(support.name, \"support\")";
33 +
    assert mem::eq(app.name, "app"), "main: mem::eq(app.name, \"app\")";
34 +
    assert support.dependencies.len == 0, "main: support.dependencies.len == 0";
35 +
    assert app.dependencies.len == 1, "main: app.dependencies.len == 1";
36 +
    assert mem::eq(app.dependencies[0], "support"), "main: mem::eq(app.dependencies[0], \"support\")";
37 +
    assert support.program.fns.len == 1, "main: support.program.fns.len == 1";
38 +
    assert support.program.data.len == 1, "main: support.program.data.len == 1";
39 +
    assert mem::eq(support.program.fns[0].name, "support::read"), "main: mem::eq(support.program.fns[0].name, \"support::read\")";
40 +
    assert mem::eq(support.program.data[0].name, "support::value"), "main: mem::eq(support.program.data[0].name, \"support::value\")";
41 +
    assert app.program.fns.len == 1, "main: app.program.fns.len == 1";
42 +
    assert app.program.data.len == 0, "main: app.program.data.len == 0";
43 +
    assert mem::eq(app.program.fns[0].name, "app::main"), "main: mem::eq(app.program.fns[0].name, \"app::main\")";
44 +
    assert support.exports.len == 2, "main: support.exports.len == 2";
45 +
    assert app.exports.len == 1, "main: app.exports.len == 1";
46 +
    assert app.exports[0].kind == binary::ExportKind::Function, "main: app.exports[0].kind == binary::ExportKind::Function";
47 +
    let entry = app.entry else panic "main: missing package entry";
48 +
    assert mem::eq(entry, "app::main"), "main: mem::eq(entry, \"app::main\")";
49 +
    let mut callFound = false;
50 +
    let mut dataFound = false;
51 +
    for block in app.program.fns[0].blocks {
52 +
        for instr in block.instrs {
53 +
            match instr {
54 +
                case il::Instr::Call { func, .. } => {
55 +
                    if let case il::Val::FnAddr(name) = func {
56 +
                        if mem::eq(name, "support::read") { set callFound = true; }
57 +
                    }
58 +
                },
59 +
                case il::Instr::Copy { val, .. } => {
60 +
                    if let case il::Val::DataSym(name) = val {
61 +
                        if mem::eq(name, "support::value") { set dataFound = true; }
62 +
                    }
63 +
                },
64 +
                else => {},
65 +
            }
66 +
        }
67 +
    }
68 +
    assert callFound and dataFound, "main: callFound and dataFound";
69 +
    return 0;
70 +
}
test/packages/run added +24 -0
1 +
#!/bin/sh
2 +
# Check separate RIL package output and reproducible bytes.
3 +
set -eu
4 +
emulator=${RAD_EMULATOR:-emulator}
5 +
work=$(mktemp -d)
6 +
trap 'rm -rf "$work"' EXIT HUP INT TERM
7 +
mkdir "$work/first" "$work/second"
8 +
for output in first second; do
9 +
    "$emulator" -memory-size=385024 -data-size=348160 -stack-size=512 \
10 +
        -run bin/radiance.rv64.dev \
11 +
        -pkg support -mod test/packages/support.rad \
12 +
        -pkg app -mod test/packages/app.rad -entry app -ril "$work/$output"
13 +
done
14 +
cmp "$work/first/support.ril" "$work/second/support.ril"
15 +
cmp "$work/first/app.ril" "$work/second/app.ril"
16 +
"$emulator" -run bin/packages.check.rv64 -- "$work/first/support.ril" "$work/first/app.ril"
17 +
"$emulator" -memory-size=385024 -data-size=348160 -stack-size=512 \
18 +
    -run bin/radiance.rv64.dev \
19 +
    -pkg support -mod test/packages/support.rad \
20 +
    -pkg app -mod test/packages/app.rad -entry app -o "$work/hosted.rv64"
21 +
status=0
22 +
"$emulator" -run "$work/hosted.rv64" || status=$?
23 +
[ "$status" -eq 14 ]
24 +
printf 'package output: ownership, linkage, exports, determinism, and hosted execution passed\n'
test/packages/support.rad added +5 -0
1 +
//! Shared package fixture.
2 +
/// Shared mutable data.
3 +
export static value: u64 = 7;
4 +
/// Read shared mutable data.
5 +
export fn read() -> u64 { return value; }