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