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