Emit self-contained RV64 images
5e9e4cfcf161661a9b70211336c7c5f429dbf85afe4f6a3a8984f6c6f402cd1a
Change compiler and standalone assembly output from separate text, `.ro.data`, and `.rw.data` files to one RV64 image containing a small header followed by text, read-only data, and read-write data. This lets the emulator load one artifact without sidecar data files.
1 parent
2e5fdaa8
.gitignore
+0 -2
| 12 | 12 | /.ai |
|
| 13 | 13 | ||
| 14 | 14 | # Compiler/build output |
|
| 15 | 15 | /bin |
|
| 16 | 16 | *.rv64 |
|
| 17 | - | *.rv64.ro.data |
|
| 18 | - | *.rv64.rw.data |
|
| 19 | 17 | *.rv64.debug |
|
| 20 | 18 | *.o |
|
| 21 | 19 | ||
| 22 | 20 | # Seed binary (tracked, but not stage intermediates) |
|
| 23 | 21 | !seed/ |
Makefile
+7 -10
| 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 std-test bin-test |
|
| 26 | + | test: emulator seed-test std-test bin-test |
|
| 27 | + | ||
| 28 | + | seed-test: |
|
| 29 | + | @seed/test |
|
| 27 | 30 | ||
| 28 | 31 | # Emulator command check |
|
| 29 | 32 | ||
| 30 | 33 | emulator: |
|
| 31 | 34 | ifeq ($(EMU_PATH),) |
| 58 | 61 | ||
| 59 | 62 | clean-std-test: |
|
| 60 | 63 | @rm -f lib/std.test.rv64 \ |
|
| 61 | 64 | lib/std.test.rv64.debug \ |
|
| 62 | 65 | lib/std.test.rv64.s \ |
|
| 63 | - | lib/std.test.rv64.o \ |
|
| 64 | - | lib/std.test.rv64.rw.data \ |
|
| 65 | - | lib/std.test.rv64.ro.data |
|
| 66 | + | lib/std.test.rv64.o |
|
| 66 | 67 | ||
| 67 | 68 | # Binary Tests |
|
| 68 | 69 | ||
| 69 | 70 | BIN_TEST_DIR := test/tests |
|
| 70 | 71 | # Only tests with `//! returns:` are compiled to binaries and executed. |
| 103 | 104 | clean-bin-test: |
|
| 104 | 105 | @rm -f $(BIN_RUNNER) \ |
|
| 105 | 106 | $(BIN_RUNNER:.rv64=.rv64.debug) \ |
|
| 106 | 107 | $(BIN_RUNNER:.rv64=.rv64.s) \ |
|
| 107 | 108 | $(BIN_RUNNER:.rv64=.rv64.o) \ |
|
| 108 | - | $(BIN_RUNNER:.rv64=.rv64.rw.data) \ |
|
| 109 | - | $(BIN_RUNNER:.rv64=.rv64.ro.data) \ |
|
| 110 | 109 | $(BIN_TEST_EXE_BIN) \ |
|
| 111 | 110 | $(wildcard $(BIN_TEST_DIR)/*.rv64.debug) \ |
|
| 112 | - | $(wildcard $(BIN_TEST_DIR)/*.rv64.s) \ |
|
| 113 | - | $(wildcard $(BIN_TEST_DIR)/*.rv64.rw.data) \ |
|
| 114 | - | $(wildcard $(BIN_TEST_DIR)/*.rv64.ro.data) |
|
| 111 | + | $(wildcard $(BIN_TEST_DIR)/*.rv64.s) |
|
| 115 | 112 | ||
| 116 | 113 | seed: |
|
| 117 | 114 | seed/update |
|
| 118 | 115 | ||
| 119 | 116 | clean-rad: |
| 123 | 120 | clean: clean-std-test clean-bin-test clean-rad |
|
| 124 | 121 | ||
| 125 | 122 | t: test |
|
| 126 | 123 | c: clean |
|
| 127 | 124 | ||
| 128 | - | .PHONY: test clean default std-test bin-test seed \ |
|
| 125 | + | .PHONY: test clean default seed-test std-test bin-test seed \ |
|
| 129 | 126 | clean-std-test clean-bin-test clean-rad emulator |
|
| 130 | 127 | .SUFFIXES: |
|
| 131 | 128 | .DELETE_ON_ERROR: |
|
| 132 | 129 | .SILENT: |
compiler/radiance.rad
+24 -19
| 65 | 65 | static CODEGEN_DATA_SYM_ENTRIES: [dict::Entry; data::DATA_SYM_TABLE_SIZE] = undefined; |
|
| 66 | 66 | ||
| 67 | 67 | /// Debug info file extension. |
|
| 68 | 68 | constant DEBUG_EXT: *[u8] = ".debug"; |
|
| 69 | 69 | ||
| 70 | - | /// Read-only data file extension. |
|
| 71 | - | constant RO_DATA_EXT: *[u8] = ".ro.data"; |
|
| 72 | - | /// Read-write data file extension. |
|
| 73 | - | constant RW_DATA_EXT: *[u8] = ".rw.data"; |
|
| 74 | 70 | /// Maximum rodata size (4MB). |
|
| 75 | 71 | constant MAX_RO_DATA_SIZE: u32 = 4194304; |
|
| 76 | 72 | /// Maximum rwdata size (4MB). |
|
| 77 | 73 | constant MAX_RW_DATA_SIZE: u32 = 4194304; |
|
| 78 | 74 | /// Maximum path length. |
| 773 | 769 | }; |
|
| 774 | 770 | let stmts = block.statements.append(decl, alloc::arenaAllocator(&mut arena.arena)); |
|
| 775 | 771 | set blockNode.value = ast::NodeValue::Block(ast::Block { statements: stmts }); |
|
| 776 | 772 | } |
|
| 777 | 773 | ||
| 778 | - | /// Write code buffer to file as raw bytes. |
|
| 779 | - | fn writeCode(code: *[u32], path: *[u8]) -> bool { |
|
| 780 | - | // Convert `u32` slice to `u8` slice (little-endian). |
|
| 781 | - | let byteLen = code.len * 4; |
|
| 782 | - | let bytePtr = code.ptr as *u8; |
|
| 783 | - | let bytes = @sliceOf(bytePtr, byteLen); |
|
| 784 | - | ||
| 785 | - | return unix::writeFile(path, bytes); |
|
| 774 | + | /// Write a self-contained RV64 image containing text and data sections. |
|
| 775 | + | fn writeImage( |
|
| 776 | + | code: *[u32], |
|
| 777 | + | roData: *[u8], |
|
| 778 | + | rwData: *[u8], |
|
| 779 | + | path: *[u8] |
|
| 780 | + | ) -> bool { |
|
| 781 | + | let mut header = rv64::imageHeader(code.len * rv64::INSTR_SIZE as u32, roData.len, rwData.len); |
|
| 782 | + | let headerWords = &header[..]; |
|
| 783 | + | let headerBytes = @sliceOf(headerWords.ptr as *u8, headerWords.len * rv64::WORD_SIZE as u32); |
|
| 784 | + | let codeBytes = @sliceOf(code.ptr as *u8, code.len * rv64::INSTR_SIZE as u32); |
|
| 785 | + | ||
| 786 | + | return unix::writeFileParts(path, &[headerBytes, codeBytes, roData, rwData]); |
|
| 786 | 787 | } |
|
| 787 | 788 | ||
| 788 | 789 | /// Write a data section to a file at `basePath` + `ext`. |
|
| 789 | 790 | /// Empty data truncates any stale sidecar left by an earlier build. |
|
| 790 | 791 | fn writeDataWithExt( |
| 976 | 977 | ) -> rv64::Program throws (Error) { |
|
| 977 | 978 | let entryIdx = ctx.entryPkgIdx else { |
|
| 978 | 979 | panic "lowerAndGenerateAllPackages: no entry package"; |
|
| 979 | 980 | }; |
|
| 980 | 981 | let entryPkg = &ctx.packages[entryIdx]; |
|
| 982 | + | let startupPath = getEntryStartupPath(ctx); |
|
| 981 | 983 | let options = lower::LowerOptions { debug: ctx.debug, buildTest: ctx.config.buildTest }; |
|
| 982 | 984 | let storage = rv64::Storage { |
|
| 983 | 985 | dataSyms: &mut CODEGEN_DATA_SYMS[..], |
|
| 984 | 986 | dataSymEntries: &mut CODEGEN_DATA_SYM_ENTRIES[..], |
|
| 985 | 987 | }; |
| 1004 | 1006 | set low.output = lower::FnOutput::Stream(lower::FnSink { |
|
| 1005 | 1007 | ctx: &mut codegenCtx as *mut opaque, |
|
| 1006 | 1008 | emitFn: generateLoweredFn, |
|
| 1007 | 1009 | }); |
|
| 1008 | 1010 | let mut asmDataLen: u32 = 0; |
|
| 1009 | - | if let startupPath = getEntryStartupPath(ctx) { |
|
| 1010 | - | try assembleAsmModule(&mut generator, entryPkg, startupPath, &mut asmDataLen, &mut res.arena); |
|
| 1011 | + | if let path = startupPath { |
|
| 1012 | + | try assembleAsmModule(&mut generator, entryPkg, path, &mut asmDataLen, &mut res.arena); |
|
| 1011 | 1013 | } |
|
| 1012 | 1014 | try lowerAllPackagesInto(ctx, res, &mut low); |
|
| 1013 | 1015 | let asmData = try assembleAsmInputs(ctx, &mut generator, &mut asmDataLen, &mut res.arena); |
|
| 1014 | 1016 | ||
| 1015 | 1017 | match generator.entryPatch { |
| 1056 | 1058 | // Generate binary output if path specified. |
|
| 1057 | 1059 | let outPath = ctx.outputPath else { |
|
| 1058 | 1060 | try lowerAllPackages(ctx, res); |
|
| 1059 | 1061 | return; |
|
| 1060 | 1062 | }; |
|
| 1063 | + | let startupPath = getEntryStartupPath(ctx); |
|
| 1061 | 1064 | let result = try lowerAndGenerateAllPackages(ctx, res, fnArena, CodegenOptions { |
|
| 1062 | 1065 | logPath: outPath, |
|
| 1063 | 1066 | debug: ctx.debug, |
|
| 1064 | 1067 | entryMode: CodegenEntryMode::None |
|
| 1065 | - | if getEntryStartupPath(ctx) <> nil |
|
| 1068 | + | if startupPath <> nil |
|
| 1066 | 1069 | else CodegenEntryMode::DefaultEntry, |
|
| 1067 | 1070 | }); |
|
| 1068 | 1071 | ||
| 1069 | - | if not writeCode(result.code, outPath) { |
|
| 1072 | + | if not writeImage( |
|
| 1073 | + | result.code, |
|
| 1074 | + | &RO_DATA_BUF[..result.roDataSize], |
|
| 1075 | + | &RW_DATA_BUF[..result.rwDataSize], |
|
| 1076 | + | outPath |
|
| 1077 | + | ) { |
|
| 1070 | 1078 | throw error(&["fatal:", "failed to write output file"]); |
|
| 1071 | 1079 | } |
|
| 1072 | - | // Write data files. |
|
| 1073 | - | try writeDataWithExt(&RO_DATA_BUF[..result.roDataSize], outPath, RO_DATA_EXT); |
|
| 1074 | - | try writeDataWithExt(&RW_DATA_BUF[..result.rwDataSize], outPath, RW_DATA_EXT); |
|
| 1075 | 1080 | ||
| 1076 | 1081 | // Write debug info file if enabled. |
|
| 1077 | 1082 | if ctx.debug { |
|
| 1078 | 1083 | try writeDebugInfo(result.debugEntries, &ctx.graph, outPath, &mut res.arena); |
|
| 1079 | 1084 | } |
lib/std/arch/rv64.rad
+11 -0
| 147 | 147 | export constant RO_DATA_BASE: u32 = 0x10000; |
|
| 148 | 148 | ||
| 149 | 149 | /// Base address where read-write data is loaded. |
|
| 150 | 150 | export constant RW_DATA_BASE: u32 = 0xFFFFF0; |
|
| 151 | 151 | ||
| 152 | + | /// Single-file RV64 image magic, "RAD0" as a little-endian u32. |
|
| 153 | + | export constant IMAGE_MAGIC: u32 = 0x30444152; |
|
| 154 | + | ||
| 155 | + | /// Single-file RV64 image format version. |
|
| 156 | + | export constant IMAGE_VERSION: u32 = 1; |
|
| 157 | + | ||
| 158 | + | /// Build a single-file RV64 image header. |
|
| 159 | + | export fn imageHeader(codeBytes: u32, roDataBytes: u32, rwDataBytes: u32) -> [u32; 5] { |
|
| 160 | + | return [IMAGE_MAGIC, IMAGE_VERSION, codeBytes, roDataBytes, rwDataBytes]; |
|
| 161 | + | } |
|
| 162 | + | ||
| 152 | 163 | /// Storage buffers passed from driver for code generation. |
|
| 153 | 164 | export record Storage { |
|
| 154 | 165 | /// Buffer for data symbols. |
|
| 155 | 166 | dataSyms: *mut [data::DataSym], |
|
| 156 | 167 | /// Hash table entries for data symbol lookup. |
lib/std/arch/rv64/emit.rad
+1 -6
| 304 | 304 | ||
| 305 | 305 | /// Record a function address load needing later patching. |
|
| 306 | 306 | /// Emits placeholder instructions that will be patched to load the function's address. |
|
| 307 | 307 | /// Uses two slots to compute long-distance addresses. |
|
| 308 | 308 | export fn recordAddrLoad(e: *mut Emitter, target: *[u8], rd: gen::Reg) { |
|
| 309 | - | e.pendingAddrLoads.append(PendingAddrLoad { |
|
| 310 | - | index: e.codeLen, |
|
| 311 | - | target, |
|
| 312 | - | rd: rd, |
|
| 313 | - | isData: false, |
|
| 314 | - | }, e.allocator); |
|
| 309 | + | recordAddrLoadAt(e, target, rd, e.codeLen); |
|
| 315 | 310 | ||
| 316 | 311 | emit(e, encode::nop()); // Placeholder for AUIPC. |
|
| 317 | 312 | emit(e, encode::nop()); // Placeholder for ADDI. |
|
| 318 | 313 | } |
|
| 319 | 314 |
lib/std/sys/unix.rad
+25 -9
| 71 | 71 | /// Returns the number of bytes written, or a negative value on error. |
|
| 72 | 72 | export fn write(fd: i64, buf: *[u8]) -> i64 { |
|
| 73 | 73 | return intrinsics::ecall(64, fd, buf.ptr as i64, buf.len as i64, 0); |
|
| 74 | 74 | } |
|
| 75 | 75 | ||
| 76 | + | /// Writes the entire contents of a buffer to a file descriptor. |
|
| 77 | + | /// Returns `false` when the descriptor cannot accept the full buffer. |
|
| 78 | + | export fn writeAll(fd: i64, data: *[u8]) -> bool { |
|
| 79 | + | let mut written: u32 = 0; |
|
| 80 | + | while written < data.len { |
|
| 81 | + | let n = write(fd, &data[written..]); |
|
| 82 | + | if n <= 0 { |
|
| 83 | + | return false; |
|
| 84 | + | } |
|
| 85 | + | set written += n as u32; |
|
| 86 | + | } |
|
| 87 | + | return true; |
|
| 88 | + | } |
|
| 89 | + | ||
| 76 | 90 | /// Closes a file descriptor. |
|
| 77 | 91 | /// Returns `0` on success, or a negative value on error. |
|
| 78 | 92 | export fn close(fd: i64) -> i64 { |
|
| 79 | 93 | return intrinsics::ecall(57, fd, 0, 0, 0); |
|
| 80 | 94 | } |
| 104 | 118 | ||
| 105 | 119 | /// Writes the entire contents of a buffer to a file at the given path. |
|
| 106 | 120 | /// Creates the file if it doesn't exist, truncates if it does. |
|
| 107 | 121 | /// Returns `true` on success. |
|
| 108 | 122 | export fn writeFile(path: *[u8], data: *[u8]) -> bool { |
|
| 123 | + | return writeFileParts(path, &[data]); |
|
| 124 | + | } |
|
| 125 | + | ||
| 126 | + | /// Writes each buffer to a file at the given path. |
|
| 127 | + | /// Creates the file if it doesn't exist, truncates if it does. |
|
| 128 | + | /// Returns `true` only when every part and the final close succeed. |
|
| 129 | + | export fn writeFileParts(path: *[u8], parts: *[*[u8]]) -> bool { |
|
| 109 | 130 | let flags = OpenFlags(*O_WRONLY | *O_CREAT | *O_TRUNC); |
|
| 110 | 131 | let fd = openOpts(path, flags, 420); // 0644 in octal. |
|
| 111 | 132 | if fd < 0 { |
|
| 112 | 133 | return false; |
|
| 113 | 134 | } |
|
| 114 | - | let mut written: u32 = 0; |
|
| 115 | - | while written < data.len { |
|
| 116 | - | let chunk = &data[written..]; |
|
| 117 | - | let n = write(fd, chunk); |
|
| 118 | - | if n <= 0 { |
|
| 135 | + | ||
| 136 | + | for part in parts { |
|
| 137 | + | if not writeAll(fd, part) { |
|
| 119 | 138 | close(fd); |
|
| 120 | 139 | return false; |
|
| 121 | 140 | } |
|
| 122 | - | set written += n as u32; |
|
| 123 | 141 | } |
|
| 124 | - | close(fd); |
|
| 125 | - | ||
| 126 | - | return true; |
|
| 142 | + | return close(fd) == 0; |
|
| 127 | 143 | } |
lib/std/tests.rad
+14 -0
| 2 | 2 | ||
| 3 | 3 | use std::fmt; |
|
| 4 | 4 | use std::mem; |
|
| 5 | 5 | use std::vec; |
|
| 6 | 6 | use std::testing; |
|
| 7 | + | use std::sys::unix; |
|
| 7 | 8 | ||
| 8 | 9 | // Data types ////////////////////////////////////////////////////////////////// |
|
| 9 | 10 | ||
| 10 | 11 | record Point { |
|
| 11 | 12 | x: i32, |
| 238 | 239 | set threw = true; |
|
| 239 | 240 | }; |
|
| 240 | 241 | try testing::expect(threw); |
|
| 241 | 242 | } |
|
| 242 | 243 | ||
| 244 | + | @test fn testWriteFileParts() throws (testing::TestError) { |
|
| 245 | + | let path = "/tmp/radiance-std-write-file-parts.test"; |
|
| 246 | + | let mut buffer: [u8; 16] = undefined; |
|
| 247 | + | ||
| 248 | + | if not unix::writeFileParts(path, &["hello", " ", "world"]) { |
|
| 249 | + | throw testing::TestError::Failed; |
|
| 250 | + | } |
|
| 251 | + | let data = unix::readFile(path, &mut buffer[..]) else { |
|
| 252 | + | throw testing::TestError::Failed; |
|
| 253 | + | }; |
|
| 254 | + | try testing::expectBytesEq(data, "hello world"); |
|
| 255 | + | } |
|
| 256 | + | ||
| 243 | 257 | @test fn testStripPrefixMatch() throws (testing::TestError) { |
|
| 244 | 258 | let input: *[u8] = "hello world"; |
|
| 245 | 259 | let prefix: *[u8] = "hello"; |
|
| 246 | 260 | ||
| 247 | 261 | let result = mem::stripPrefix(prefix, input) else { |
seed/README
+2 -3
| 37 | 37 | affected by. |
|
| 38 | 38 | ||
| 39 | 39 | ||
| 40 | 40 | FILES |
|
| 41 | 41 | ||
| 42 | - | seed/radiance.rv64 Seed binary (RISC-V machine code). |
|
| 43 | - | seed/radiance.rv64.ro.data Read-only data section. |
|
| 44 | - | seed/radiance.rv64.rw.data Read-write data section. |
|
| 42 | + | seed/radiance.rv64 Self-contained RV64 image containing the seed's |
|
| 43 | + | text, read-only data, and read-write data. |
|
| 45 | 44 | seed/radiance.rv64.git SHA-256 of the git commit whose *source* |
|
| 46 | 45 | was compiled to produce this seed. |
|
| 47 | 46 | seed/update Tool that finds the fixed point and |
|
| 48 | 47 | updates the seed. |
|
| 49 | 48 |
seed/radiance.rv64
+0 -0
Binary file changed.
seed/radiance.rv64.git
+1 -1
| 1 | - | 6d59132f5ff1fc6ab5aeb22087349fd934102d27eafb0e79b8d2309900c12d7c |
|
| 1 | + | d024ed30c27cc671d9371988d0155b3e08ff004582cf59f1b7eac8ffdf3e3467 |
seed/radiance.rv64.ro.data
deleted
+0 -0
Binary file changed.
seed/radiance.rv64.rw.data
deleted
+0 -0
Binary file changed.
seed/test
added
+24 -0
| 1 | + | #!/bin/sh |
|
| 2 | + | # Verify that the checked-in seed and its workflow use self-contained images. |
|
| 3 | + | set -eu |
|
| 4 | + | ||
| 5 | + | fail() { |
|
| 6 | + | printf 'seed test: %s\n' "$*" >&2 |
|
| 7 | + | exit 1 |
|
| 8 | + | } |
|
| 9 | + | ||
| 10 | + | magic="$(od -An -tx4 -N4 seed/radiance.rv64 | tr -d ' ')" |
|
| 11 | + | [ "$magic" = "30444152" ] || fail "seed is not a self-contained RV64 image" |
|
| 12 | + | ||
| 13 | + | version="$(od -An -tu4 -j4 -N4 seed/radiance.rv64 | tr -d ' ')" |
|
| 14 | + | [ "$version" = "1" ] || fail "unsupported seed image version: $version" |
|
| 15 | + | ||
| 16 | + | size="$(wc -c < seed/radiance.rv64 | tr -d ' ')" |
|
| 17 | + | code="$(od -An -tu4 -j8 -N4 seed/radiance.rv64 | tr -d ' ')" |
|
| 18 | + | ro="$(od -An -tu4 -j12 -N4 seed/radiance.rv64 | tr -d ' ')" |
|
| 19 | + | rw="$(od -An -tu4 -j16 -N4 seed/radiance.rv64 | tr -d ' ')" |
|
| 20 | + | ||
| 21 | + | [ "$size" -eq $((20 + code + ro + rw)) ] || |
|
| 22 | + | fail "seed image size $size does not match header sections" |
|
| 23 | + | ||
| 24 | + | printf 'seed test: ok\n' |
seed/update
+2 -6
| 74 | 74 | # --------------------------------------------------------------------------- |
|
| 75 | 75 | # Compare two stage binaries. Returns 0 if identical, 1 if they differ. |
|
| 76 | 76 | # --------------------------------------------------------------------------- |
|
| 77 | 77 | ||
| 78 | 78 | compareStages() { |
|
| 79 | - | if cmp -s "$1" "$2" \ |
|
| 80 | - | && cmp -s "$1.ro.data" "$2.ro.data" \ |
|
| 81 | - | && cmp -s "$1.rw.data" "$2.rw.data"; then |
|
| 79 | + | if cmp -s "$1" "$2"; then |
|
| 82 | 80 | printf " IDENTICAL\n sha256: %s\n" "$(sha256sum "$2" | cut -d' ' -f1)" |
|
| 83 | 81 | return 0 |
|
| 84 | 82 | else |
|
| 85 | 83 | printf " DIFFERENT\n" |
|
| 86 | 84 | return 1 |
| 90 | 88 | # --------------------------------------------------------------------------- |
|
| 91 | 89 | # Copy the fixed-point binary to the seed. |
|
| 92 | 90 | # --------------------------------------------------------------------------- |
|
| 93 | 91 | ||
| 94 | 92 | updateSeed() { |
|
| 95 | - | [ "$1" != seed/radiance.rv64 ] && cp "$1" seed/radiance.rv64 |
|
| 96 | - | [ "$1.ro.data" != seed/radiance.rv64.ro.data ] && cp "$1.ro.data" seed/radiance.rv64.ro.data |
|
| 97 | - | [ "$1.rw.data" != seed/radiance.rv64.rw.data ] && cp "$1.rw.data" seed/radiance.rv64.rw.data |
|
| 93 | + | [ "$1" != seed/radiance.rv64 ] && cp "$1" seed/radiance.rv64 |
|
| 98 | 94 | ||
| 99 | 95 | git rev-parse HEAD > seed/radiance.rv64.git |
|
| 100 | 96 | ||
| 101 | 97 | printf "\n" |
|
| 102 | 98 | printf "Fixed point reached at %s\n" "$2" |
test/run
+3 -9
| 8 | 8 | # For each test: |
|
| 9 | 9 | # - If a `.ril` file exists alongside it, the IL output is checked |
|
| 10 | 10 | # against it via the runner binary. |
|
| 11 | 11 | # - If `//! returns: N` appears in the file, the test is compiled to |
|
| 12 | 12 | # a binary and executed; the exit code must match N. |
|
| 13 | - | # - If `//! rw-data-size: N` appears in the file, the emitted `.rw.data` |
|
| 14 | - | # sidecar size must match N bytes. Missing sidecars count as zero bytes. |
|
| 13 | + | # - If `//! rw-data-size: N` appears in the file, the emitted image header's |
|
| 14 | + | # rwdata size must match N bytes. |
|
| 15 | 15 | ||
| 16 | 16 | RUNNER="test/runner.rv64" |
|
| 17 | 17 | TEST_DIR="test/tests" |
|
| 18 | 18 | EMU="${RAD_EMULATOR:-emulator} -stack-size=1024 -run" |
|
| 19 | 19 | EMU_RUN="${RAD_EMULATOR:-emulator} -no-jit -run" |
| 49 | 49 | *) base="$test" ;; |
|
| 50 | 50 | esac |
|
| 51 | 51 | ||
| 52 | 52 | ril="${base}.ril" |
|
| 53 | 53 | bin="${base}.rv64" |
|
| 54 | - | rw_data="${bin}.rw.data" |
|
| 55 | - | ||
| 56 | 54 | # IL check: run the runner if a .ril file exists. |
|
| 57 | 55 | if [ -f "$ril" ]; then |
|
| 58 | 56 | if $EMU "$RUNNER" -- "$test"; then |
|
| 59 | 57 | passed=$((passed + 1)) |
|
| 60 | 58 | else |
| 93 | 91 | echo "FAILED (binary not found: $bin)" |
|
| 94 | 92 | failed=$((failed + 1)) |
|
| 95 | 93 | continue |
|
| 96 | 94 | fi |
|
| 97 | 95 | ||
| 98 | - | if [ -f "$rw_data" ]; then |
|
| 99 | - | actual_rw_data_size=$(wc -c < "$rw_data" | tr -d ' ') |
|
| 100 | - | else |
|
| 101 | - | actual_rw_data_size=0 |
|
| 102 | - | fi |
|
| 96 | + | actual_rw_data_size=$(od -An -tu4 -j16 -N4 "$bin" | tr -d ' ') |
|
| 103 | 97 | ||
| 104 | 98 | if [ "$actual_rw_data_size" -eq "$rw_data_size" ]; then |
|
| 105 | 99 | echo "ok" |
|
| 106 | 100 | passed=$((passed + 1)) |
|
| 107 | 101 | else |
test/runner.rad
+8 -35
| 43 | 43 | constant MAX_ERRORS: u32 = 16; |
|
| 44 | 44 | /// Maximum number of text words in a `.ras` test binary. |
|
| 45 | 45 | constant ASM_TEXT_CAPACITY: u32 = 256; |
|
| 46 | 46 | /// Maximum number of data bytes in a `.ras` test binary. |
|
| 47 | 47 | constant ASM_DATA_CAPACITY: u32 = 1024; |
|
| 48 | - | constant RO_DATA_EXT: *[u8] = ".ro.data"; |
|
| 49 | 48 | ||
| 50 | 49 | // Static storage for large buffers to avoid stack overflow. |
|
| 51 | 50 | // Tests run serially so sharing these is safe. |
|
| 52 | 51 | static SOURCE_BUF: [u8; SOURCE_BUF_SIZE] = undefined; |
|
| 53 | 52 | static EXPECTED_BUF: [u8; EXPECTED_BUF_SIZE] = undefined; |
| 170 | 169 | set buf[len] = 0; |
|
| 171 | 170 | ||
| 172 | 171 | return &buf[..len]; |
|
| 173 | 172 | } |
|
| 174 | 173 | ||
| 175 | - | fn appendPathExt(basePath: *[u8], ext: *[u8], buf: *mut [u8]) -> ?*[u8] { |
|
| 176 | - | if basePath.len + ext.len + 1 > buf.len { |
|
| 177 | - | return nil; |
|
| 178 | - | } |
|
| 179 | - | let mut pos: u32 = 0; |
|
| 174 | + | /// Write a self-contained RV64 image containing text and data sections. |
|
| 175 | + | fn writeImage(code: *[u32], roData: *[u8], rwData: *[u8], path: *[u8]) -> bool { |
|
| 176 | + | let mut header = rv64::imageHeader(code.len * rv64::INSTR_SIZE as u32, roData.len, rwData.len); |
|
| 177 | + | let headerWords = &header[..]; |
|
| 178 | + | let headerBytes = @sliceOf(headerWords.ptr as *u8, headerWords.len * rv64::WORD_SIZE as u32); |
|
| 179 | + | let codeBytes = @sliceOf(code.ptr as *u8, code.len * rv64::INSTR_SIZE as u32); |
|
| 180 | 180 | ||
| 181 | - | set pos += try! mem::copy(&mut buf[pos..], basePath); |
|
| 182 | - | set pos += try! mem::copy(&mut buf[pos..], ext); |
|
| 183 | - | set buf[pos] = 0; |
|
| 184 | - | ||
| 185 | - | return &buf[..pos]; |
|
| 186 | - | } |
|
| 187 | - | ||
| 188 | - | fn writeCode(code: *[u32], path: *[u8]) -> bool { |
|
| 189 | - | let bytes = @sliceOf(code.ptr as *u8, code.len * 4); |
|
| 190 | - | return unix::writeFile(path, bytes); |
|
| 181 | + | return unix::writeFileParts(path, &[headerBytes, codeBytes, roData, rwData]); |
|
| 191 | 182 | } |
|
| 192 | 183 | ||
| 193 | 184 | fn assembleBinary(sourcePath: *[u8], outputPath: *[u8]) -> bool { |
|
| 194 | - | let mut roDataPathBuf: [u8; MAX_PATH_LEN] = undefined; |
|
| 195 | 185 | let source = unix::readFile(sourcePath, &mut SOURCE_BUF[..]) else { |
|
| 196 | 186 | io::printError("error: could not read source: "); |
|
| 197 | 187 | io::printError(sourcePath); |
|
| 198 | 188 | io::printError("\n"); |
|
| 199 | 189 | return false; |
|
| 200 | 190 | }; |
|
| 201 | - | let roDataPath = appendPathExt(outputPath, RO_DATA_EXT, &mut roDataPathBuf[..]) else { |
|
| 202 | - | io::printError("error: output path too long\n"); |
|
| 203 | - | return false; |
|
| 204 | - | }; |
|
| 205 | 191 | ||
| 206 | 192 | let mut arena = alloc::new(&mut AST_ARENA_STORAGE[..]); |
|
| 207 | 193 | let program = try asm::assemble( |
|
| 208 | 194 | asm::scanner::SourceKind::File { path: sourcePath }, |
|
| 209 | 195 | source, |
| 216 | 202 | io::printError("error: assembly failed: "); |
|
| 217 | 203 | io::printError(sourcePath); |
|
| 218 | 204 | io::printError("\n"); |
|
| 219 | 205 | return false; |
|
| 220 | 206 | }; |
|
| 221 | - | // A standalone assembly binary has no whole-program emitter to resolve |
|
| 222 | - | // external text references, so they are undefined symbols here. |
|
| 223 | - | if program.externalFixups.len > 0 { |
|
| 224 | - | io::printError("error: undefined symbol: "); |
|
| 225 | - | io::printError(program.externalFixups[0].symbol); |
|
| 226 | - | io::printError("\n"); |
|
| 227 | - | return false; |
|
| 228 | - | } |
|
| 207 | + | ||
| 229 | 208 | if not writeImage(program.text, program.data, &[], outputPath) { |
|
| 230 | 209 | io::printError("error: could not write output: "); |
|
| 231 | 210 | io::printError(outputPath); |
|
| 232 | 211 | io::printError("\n"); |
|
| 233 | 212 | return false; |
|
| 234 | 213 | } |
|
| 235 | - | if not unix::writeFile(roDataPath, program.data) { |
|
| 236 | - | io::printError("error: could not write data: "); |
|
| 237 | - | io::printError(roDataPath); |
|
| 238 | - | io::printError("\n"); |
|
| 239 | - | return false; |
|
| 240 | - | } |
|
| 241 | 214 | return true; |
|
| 242 | 215 | } |
|
| 243 | 216 | ||
| 244 | 217 | /// Run a single IL snapshot test case. Returns `true` on success. |
|
| 245 | 218 | fn runTest(sourcePath: *[u8]) -> bool { |
test/tests/static.array.mutate.rad
+1 -0
| 1 | 1 | //! returns: 0 |
|
| 2 | + | //! rw-data-size: 12 |
|
| 2 | 3 | //! Test mutating a static array. |
|
| 3 | 4 | ||
| 4 | 5 | static BUFFER: [i32; 3] = [1, 2, 3]; |
|
| 5 | 6 | ||
| 6 | 7 | fn bump(slot: u32, amount: i32) -> i32 { |