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.
Alexis Sellier committed ago 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 {