test/runner.rad 10.4 KiB raw
1
//! IL snapshot test runner and `.ras` asm helper.
2
//!
3
//! Given a `.rad` source file, lowers it to IL and compares the output
4
//! against the corresponding `.ril` snapshot file. It also supports an
5
//! `assemble <input.ras> <output.rv64>` subcommand used by `bin-test`.
6
7
use std::io;
8
use std::mem;
9
use std::sys;
10
use std::sys::unix;
11
use std::lang::alloc;
12
use std::lang::ast;
13
use std::lang::il::printer;
14
use std::lang::parser;
15
use std::lang::scanner;
16
use std::lang::resolver;
17
use std::lang::strings;
18
use std::lang::lower;
19
use std::arch::rv64;
20
use std::arch::rv64::asm;
21
22
/// Buffer size for reading source files (8 KB).
23
constant SOURCE_BUF_SIZE: u32 = 8192;
24
/// Buffer size for reading expected IL files (32 KB).
25
constant EXPECTED_BUF_SIZE: u32 = 32768;
26
/// Buffer size for generated IL output (32 KB).
27
constant OUTPUT_BUF_SIZE: u32 = 32768;
28
/// Arena size for AST/IL allocations (512 KB).
29
constant ARENA_SIZE: u32 = 524288;
30
/// Maximum path length for expected IL file path.
31
constant MAX_PATH_LEN: u32 = 256;
32
/// Source file extension for binary tests.
33
unsafe constant SOURCE_EXT: *[u8] = ".rad";
34
/// IL snapshot file extension for binary tests.
35
unsafe constant SNAPSHOT_EXT: *[u8] = ".ril";
36
37
/// String pool.
38
unsafe static STRING_POOL: strings::Pool = strings::Pool { table: undefined, count: 0 };
39
40
/// Maximum number of AST nodes per test file.
41
constant MAX_NODE_DATA: u32 = 4096;
42
/// Maximum number of resolver errors per test file.
43
constant MAX_ERRORS: u32 = 16;
44
/// Maximum number of text words in a `.ras` test binary.
45
constant ASM_TEXT_CAPACITY: u32 = 256;
46
/// Maximum number of data bytes in a `.ras` test binary.
47
constant ASM_DATA_CAPACITY: u32 = 1024;
48
49
// Static storage for large buffers to avoid stack overflow.
50
// Tests run serially so sharing these is safe.
51
unsafe static SOURCE_BUF: [u8; SOURCE_BUF_SIZE] = undefined;
52
unsafe static EXPECTED_BUF: [u8; EXPECTED_BUF_SIZE] = undefined;
53
unsafe static OUTPUT_BUF: [u8; OUTPUT_BUF_SIZE] = undefined;
54
unsafe static AST_ARENA_STORAGE: [u8; ARENA_SIZE] = undefined;
55
unsafe static IL_ARENA_STORAGE: [u8; ARENA_SIZE] = undefined;
56
unsafe static PRINT_ARENA_STORAGE: [u8; ARENA_SIZE] = undefined;
57
unsafe static RESOLVER_ARENA_STORAGE: [u8; ARENA_SIZE] = undefined;
58
unsafe static NODE_DATA_STORAGE: [resolver::NodeData; MAX_NODE_DATA] = undefined;
59
unsafe static ERROR_STORAGE: [resolver::Error; MAX_ERRORS] = undefined;
60
unsafe static ASM_TEXT_STORAGE: [u32; ASM_TEXT_CAPACITY] = undefined;
61
unsafe static ASM_DATA_STORAGE: [u8; ASM_DATA_CAPACITY] = undefined;
62
63
/// Strip a `//` comment from a line, preserving `//` inside quoted strings.
64
/// Returns the content before the comment, trimmed of trailing whitespace.
65
unsafe fn stripLine(line: *[u8]) -> *[u8] {
66
    let mut end = line.len;
67
    let mut i: u32 = 0;
68
    let mut inString = false;
69
    let mut escaped = false;
70
71
    while i < line.len {
72
        let ch = line[i];
73
74
        if inString {
75
            if escaped {
76
                set escaped = false;
77
            } else if ch == '\\' {
78
                set escaped = true;
79
            } else if ch == '"' {
80
                set inString = false;
81
            }
82
        } else if ch == '"' {
83
            set inString = true;
84
        } else if ch == '/' and i + 1 < line.len and line[i + 1] == '/' {
85
            set end = i;
86
            break;
87
        }
88
        set i += 1;
89
    }
90
91
    while end > 0 and (line[end - 1] == ' ' or line[end - 1] == '\t') {
92
        set end -= 1;
93
    }
94
    return &line[..end];
95
}
96
97
/// Get next line from string at offset. Returns the line and updates offset past newline.
98
unsafe fn nextLine(s: *[u8], offset: *mut u32) -> *[u8] {
99
    let start = *offset;
100
    let mut i = start;
101
102
    while i < s.len and s[i] <> '\n' {
103
        set i += 1;
104
    }
105
    let line = &s[start..i];
106
    if i < s.len {
107
        set *offset = i + 1;
108
    } else {
109
        set *offset = i;
110
    }
111
    return line;
112
}
113
114
/// Compare two strings ignoring comments, line by line.
115
unsafe fn stringsEqual(a: *[u8], b: *[u8]) -> bool {
116
    let mut ai: u32 = 0;
117
    let mut bi: u32 = 0;
118
119
    while ai < a.len or bi < b.len {
120
        let mut aLine = "";
121
        while ai < a.len and aLine.len == 0 {
122
            set aLine = stripLine(nextLine(a, &mut ai));
123
        }
124
        let mut bLine = "";
125
        while bi < b.len and bLine.len == 0 {
126
            set bLine = stripLine(nextLine(b, &mut bi));
127
        }
128
        if not mem::eq(aLine, bLine) {
129
            return false;
130
        }
131
    }
132
    return true;
133
}
134
135
/// Print diff between expected and actual output.
136
unsafe fn printDiff(expected: *[u8], actual: *[u8]) {
137
    io::printLn("\n// Expected");
138
    io::print(expected);
139
    io::print("\n");
140
141
    io::printLn("// Actual");
142
    io::print(actual);
143
    io::print("\n");
144
}
145
146
/// Derive the `.ril` path from a `.rad` source path. Returns nil if the path
147
/// does not end in `.rad` or the buffer is too small. The result is
148
/// null-terminated for use with syscalls.
149
unsafe fn deriveRilPath(sourcePath: *[u8], buf: *mut [u8]) -> ?*[u8] {
150
    let len = sourcePath.len;
151
    if len < SOURCE_EXT.len {
152
        return nil;
153
    }
154
    let extStart = len - SOURCE_EXT.len;
155
    // Check for .rad extension.
156
    if not mem::eq(&sourcePath[extStart..len], SOURCE_EXT) {
157
        return nil;
158
    }
159
    if len + 1 > buf.len {
160
        return nil;
161
    }
162
    // Copy full path then overwrite extension and add null terminator.
163
    try mem::copy(buf, sourcePath) catch {
164
        return nil;
165
    };
166
    try mem::copy(&mut buf[extStart..len], SNAPSHOT_EXT) catch {
167
        return nil;
168
    };
169
    set buf[len] = 0;
170
171
    return &buf[..len];
172
}
173
174
/// Write a self-contained RV64 image containing text and data sections.
175
unsafe 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
181
    let mut success = false;
182
    unsafe {
183
        set success = unix::writeFileParts(path, &[headerBytes, codeBytes, roData, rwData]);
184
    }
185
    return success;
186
}
187
188
/// Assemble one source file and write its RV64 image.
189
unsafe fn assembleBinary(sourcePath: *[u8], outputPath: *[u8]) -> bool {
190
    let mut sourceResult: ?*[u8] = nil;
191
    unsafe { set sourceResult = unix::readFile(sourcePath, &mut SOURCE_BUF[..]); }
192
    let source = sourceResult else {
193
        io::printError("error: could not read source: ");
194
        io::printError(sourcePath);
195
        io::printError("\n");
196
        return false;
197
    };
198
199
    let mut arena = alloc::new(&mut AST_ARENA_STORAGE[..]);
200
    let program = try asm::assemble(
201
        asm::scanner::SourceKind::File { path: sourcePath },
202
        source,
203
        &mut ASM_TEXT_STORAGE[..],
204
        &mut ASM_DATA_STORAGE[..],
205
        &mut arena,
206
        &mut STRING_POOL,
207
        rv64::RO_DATA_BASE
208
    ) catch {
209
        io::printError("error: assembly failed: ");
210
        io::printError(sourcePath);
211
        io::printError("\n");
212
        return false;
213
    };
214
215
    if not writeImage(program.text, program.data, &[], outputPath) {
216
        io::printError("error: could not write output: ");
217
        io::printError(outputPath);
218
        io::printError("\n");
219
        return false;
220
    }
221
    return true;
222
}
223
224
/// Run a single IL snapshot test case. Returns `true` on success.
225
unsafe fn runTest(sourcePath: *[u8]) -> bool {
226
    // Path buffer.
227
    let mut rilPathBuf: [u8; MAX_PATH_LEN] = undefined;
228
    let mut pkgScope: resolver::Scope = undefined;
229
230
    // Derive .ril path from source path.
231
    let rilPath = deriveRilPath(sourcePath, &mut rilPathBuf[..]) else {
232
        io::print("error: invalid source path (must end in .rad): ");
233
        io::printLn(sourcePath);
234
        return false;
235
    };
236
237
    // Read expected IL.
238
    let mut expectedResult: ?*[u8] = nil;
239
    unsafe { set expectedResult = unix::readFile(rilPath, &mut EXPECTED_BUF[..]); }
240
    let expected = expectedResult else {
241
        io::print("error: could not read expected IL: ");
242
        io::printLn(rilPath);
243
        return false;
244
    };
245
246
    // Read source file.
247
    let mut sourceResult: ?*[u8] = nil;
248
    unsafe { set sourceResult = unix::readFile(sourcePath, &mut SOURCE_BUF[..]); }
249
    let source = sourceResult else {
250
        io::print("error: could not read source: ");
251
        io::printLn(sourcePath);
252
        return false;
253
    };
254
255
    // Parse source.
256
    let mut astArena = ast::nodeArena(&mut AST_ARENA_STORAGE[..]);
257
    let root = try parser::parse(scanner::SourceLoc::String, source, &mut astArena, &mut STRING_POOL) catch {
258
        io::printLn("error: parsing failed");
259
        return false;
260
    };
261
262
    // Run resolver.
263
    let storage = resolver::ResolverStorage {
264
        arena: alloc::new(&mut RESOLVER_ARENA_STORAGE[..]),
265
        nodeData: &mut NODE_DATA_STORAGE[..],
266
        pkgScope: &mut pkgScope,
267
        errors: &mut ERROR_STORAGE[..],
268
    };
269
    let config = resolver::Config { buildTest: false };
270
    let mut res = resolver::resolver(storage, config);
271
    let diag = try resolver::resolveModuleRoot(&mut res, root) catch {
272
        io::printLn("error: resolver failed");
273
        return false;
274
    };
275
    if not resolver::success(&diag) {
276
        io::printLn("error: resolver failed");
277
        return false;
278
    }
279
280
    // Lower to IL.
281
    let mut ilArena = alloc::new(&mut IL_ARENA_STORAGE[..]);
282
    let program = try lower::lower(&res, root, "test", &mut ilArena) catch err {
283
        io::print("error: lowering failed: ");
284
        lower::printError(err);
285
        io::printLn("");
286
        return false;
287
    };
288
289
    // Print IL to buffer.
290
    let mut printArena = alloc::new(&mut PRINT_ARENA_STORAGE[..]);
291
    let actual = printer::printProgramToBuffer(&program, &mut printArena, &mut OUTPUT_BUF[..]);
292
293
    // Compare ignoring comments.
294
    if not stringsEqual(actual, expected) {
295
        io::printLn("FAILED");
296
        printDiff(expected, actual);
297
        return false;
298
    }
299
    io::printLn("ok");
300
301
    return true;
302
}
303
304
/// Run a single test specified as an argument.
305
@default unsafe fn main(env: *sys::Env) -> i32 {
306
    let args = env.args;
307
308
    if args.len == 4 and mem::eq(args[1], "assemble") {
309
        if assembleBinary(args[2], args[3]) {
310
            return 0;
311
        } else {
312
            return 1;
313
        }
314
    }
315
    if args.len <> 2 {
316
        io::printError("error: expected test file path as argument");
317
        return 1;
318
    }
319
    let sourcePath = args[1];
320
    io::print("test ");
321
    io::print(sourcePath);
322
    io::print(" ... ");
323
324
    if runTest(sourcePath) {
325
        return 0;
326
    } else {
327
        return 1;
328
    }
329
}