test/runner.rad 10.5 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
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 RESOLVER_ARENA_STORAGE: [u8; ARENA_SIZE] = undefined;
57
unsafe static NODE_DATA_STORAGE: [resolver::NodeData; MAX_NODE_DATA] = undefined;
58
unsafe static ERROR_STORAGE: [resolver::Error; MAX_ERRORS] = undefined;
59
unsafe static ASM_TEXT_STORAGE: [u32; ASM_TEXT_CAPACITY] = undefined;
60
unsafe 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]) -> ?u32 {
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 len;
171
}
172
173
/// Write a self-contained RV64 image containing text and data sections.
174
unsafe fn writeImage(
175
    code: *[u32],
176
    roData: &[u8],
177
    rwData: *[u8],
178
    path: *[u8]
179
) -> bool {
180
    let mut header = rv64::imageHeader(code.len * rv64::INSTR_SIZE as u32, roData.len, rwData.len);
181
    let headerBytes: *unsafe [u8] = @sliceOf(&header[0] as *unsafe u8, header.len * rv64::WORD_SIZE as u32);
182
    let codeBytes = @sliceOf(code.ptr as *u8, code.len * rv64::INSTR_SIZE as u32);
183
184
    let fd = unix::openOpts(path, unix::OpenFlags(*unix::O_WRONLY | *unix::O_CREAT | *unix::O_TRUNC), 420);
185
    if fd < 0 { return false; }
186
    let written = unix::writeAll(fd, &headerBytes[..]) and unix::writeAll(fd, &codeBytes[..])
187
        and unix::writeAll(fd, roData) and unix::writeAll(fd, rwData);
188
    let closed = unix::close(fd) == 0;
189
    return written and closed;
190
}
191
192
unsafe fn assembleBinary(sourcePath: *[u8], outputPath: *[u8]) -> bool {
193
    let sourceLen = unix::readFile(sourcePath, &mut SOURCE_BUF[..]) else {
194
        io::printError("error: could not read source: ");
195
        io::printError(sourcePath);
196
        io::printError("\n");
197
        return false;
198
    };
199
200
    let source = &SOURCE_BUF[..sourceLen];
201
    let mut arena = alloc::new(&mut AST_ARENA_STORAGE[..]);
202
    let program = try asm::assemble(
203
        asm::scanner::SourceKind::File { path: sourcePath },
204
        source,
205
        &mut ASM_TEXT_STORAGE[..],
206
        &mut ASM_DATA_STORAGE[..],
207
        &mut arena,
208
        &mut STRING_POOL,
209
        rv64::RO_DATA_BASE
210
    ) catch {
211
        io::printError("error: assembly failed: ");
212
        io::printError(sourcePath);
213
        io::printError("\n");
214
        return false;
215
    };
216
217
    if not writeImage(program.text, &program.data[..], &[], outputPath) {
218
        io::printError("error: could not write output: ");
219
        io::printError(outputPath);
220
        io::printError("\n");
221
        return false;
222
    }
223
    return true;
224
}
225
226
/// Run a single IL snapshot test case. Returns `true` on success.
227
unsafe fn runTest(sourcePath: *[u8]) -> bool {
228
    // Path buffer.
229
    let mut rilPathBuf: [u8; MAX_PATH_LEN] = undefined;
230
    static pkgScope: resolver::Scope = undefined;
231
232
    // Derive .ril path from source path.
233
    let rilPathLen = deriveRilPath(sourcePath, &mut rilPathBuf[..]) else {
234
        io::print("error: invalid source path (must end in .rad): ");
235
        io::printLn(sourcePath);
236
        return false;
237
    };
238
239
    // Read expected IL.
240
    let expectedLen = unix::readFile(&rilPathBuf[..rilPathLen], &mut EXPECTED_BUF[..]) else {
241
        io::print("error: could not read expected IL: ");
242
        io::printLn(&rilPathBuf[..rilPathLen]);
243
        return false;
244
    };
245
246
    // Read source file.
247
    let sourceLen = unix::readFile(sourcePath, &mut SOURCE_BUF[..]) else {
248
        io::print("error: could not read source: ");
249
        io::printLn(sourcePath);
250
        return false;
251
    };
252
253
    let expected = &EXPECTED_BUF[..expectedLen];
254
    let source = &SOURCE_BUF[..sourceLen];
255
256
    // Parse source.
257
    let mut astArena = ast::nodeArena(&mut AST_ARENA_STORAGE[..]);
258
    let root: *ast::Node = 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 mut resolverArena = alloc::new(&mut RESOLVER_ARENA_STORAGE[..]);
265
    let storage = resolver::ResolverStorage {
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 arenaRef: 'arena = &mut resolverArena in {
272
        let mut res = resolver::resolver(arenaRef, storage, config);
273
        let diag = try resolver::resolveModuleRoot(&mut res, root) catch {
274
            io::printLn("error: resolver failed");
275
            return false;
276
        };
277
        if not resolver::success(&diag) {
278
            io::printLn("error: resolver failed");
279
            return false;
280
        }
281
282
        // Lower to IL.
283
        let mut ilArena = alloc::new(&mut IL_ARENA_STORAGE[..]);
284
        let program = try lower::lower(&res, root, "test", &mut ilArena) catch err {
285
            io::print("error: lowering failed: ");
286
            lower::printError(err);
287
            io::printLn("");
288
            return false;
289
        };
290
291
        // Print IL to buffer.
292
        let actual = printer::printProgramToBuffer(&program, &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
306
/// Run a single test specified as an argument.
307
@default unsafe fn main(env: *sys::Env) -> i32 {
308
    let args = env.args;
309
310
    if args.len == 4 and mem::eq(args[1], "assemble") {
311
        if assembleBinary(args[2], args[3]) {
312
            return 0;
313
        } else {
314
            return 1;
315
        }
316
    }
317
    if args.len <> 2 {
318
        io::printError("error: expected test file path as argument");
319
        return 1;
320
    }
321
    let sourcePath = args[1];
322
    io::print("test ");
323
    io::print(sourcePath);
324
    io::print(" ... ");
325
326
    if runTest(sourcePath) {
327
        return 0;
328
    } else {
329
        return 1;
330
    }
331
}