compiler/
lib/
scripts/
seed/
sublime/
test/
tests/
run
2.7 KiB
runner.rad
10.2 KiB
vim/
.gitignore
336 B
.gitsigners
112 B
LICENSE
1.1 KiB
Makefile
3.7 KiB
README
4.8 KiB
STYLE
2.5 KiB
std.lib
1.2 KiB
std.lib.test
347 B
test/runner.rad
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 | } |