compiler: Parse command-line inputs in checked code
b74f809eab359b36114cc608d7e1bff66d9767b013604eb472efeb59afa72e82
1 parent
1582c496
Makefile
+6 -2
| 79 | 79 | $(addsuffix .s,$(STD_LIB_TEST)) \ |
|
| 80 | 80 | $(addsuffix .o,$(STD_LIB_TEST)) |
|
| 81 | 81 | ||
| 82 | 82 | # Binary Tests |
|
| 83 | 83 | ||
| 84 | - | # Driver fixtures exercise test discovery through the compiler command line. |
|
| 84 | + | # Driver fixtures exercise argument parsing and test discovery. |
|
| 85 | 85 | driver-test: $(RAD_BIN) |
|
| 86 | + | @RAD_EMULATOR="$(EMU)" RAD_BIN="$(RAD_BIN)" test/command |
|
| 86 | 87 | @RAD_EMULATOR="$(EMU)" RAD_BIN="$(RAD_BIN)" test/driver |
|
| 87 | 88 | ||
| 89 | + | driver-command: $(RAD_BIN) |
|
| 90 | + | @$(RADIANCE) $(DRIVER_ARGUMENTS) |
|
| 91 | + | ||
| 88 | 92 | driver-test-image: $(RAD_BIN) |
|
| 89 | 93 | @$(RADIANCE) -test $(STD) -mod lib/std/testing.rad $(DRIVER_TEST_INPUTS) $(DRIVER_TEST_FLAGS) \ |
|
| 90 | 94 | -entry selected -o $(DRIVER_TEST_OUTPUT) |
|
| 91 | 95 | ||
| 92 | 96 | BIN_TEST_DIR := test/tests |
| 149 | 153 | ||
| 150 | 154 | t: test |
|
| 151 | 155 | c: clean |
|
| 152 | 156 | ||
| 153 | 157 | .PHONY: test clean default seed-test std-test bin-test seed \ |
|
| 154 | - | clean-std-test clean-bin-test clean-rad emulator driver-test driver-test-image |
|
| 158 | + | clean-std-test clean-bin-test clean-rad emulator driver-test driver-test-image driver-command |
|
| 155 | 159 | .SUFFIXES: |
|
| 156 | 160 | .DELETE_ON_ERROR: |
|
| 157 | 161 | .SILENT: |
compiler/radiance.rad
+51 -16
| 174 | 174 | asmPaths: [*[u8]; MAX_ASM_MODULES], |
|
| 175 | 175 | /// Number of assembly source paths. |
|
| 176 | 176 | asmPathCount: u32, |
|
| 177 | 177 | } |
|
| 178 | 178 | ||
| 179 | + | /// Validated command-line compilation options. |
|
| 180 | + | record Command { |
|
| 181 | + | /// Number of packages to compile. |
|
| 182 | + | packageCount: u32, |
|
| 183 | + | /// Index of the selected entry package. |
|
| 184 | + | entryPkgIdx: u32, |
|
| 185 | + | /// Resolver configuration. |
|
| 186 | + | config: resolver::Config, |
|
| 187 | + | /// What to dump during compilation. |
|
| 188 | + | dump: Dump, |
|
| 189 | + | /// Output path for binary. |
|
| 190 | + | outputPath: ?*[u8], |
|
| 191 | + | /// Output directory for separate binary RIL packages. |
|
| 192 | + | rilDirectory: ?*[u8], |
|
| 193 | + | /// Whether to emit debug info (.debug file). |
|
| 194 | + | debug: bool, |
|
| 195 | + | } |
|
| 196 | + | ||
| 179 | 197 | /// Compilation context. |
|
| 180 | 198 | record CompileContext { |
|
| 181 | 199 | /// Array of packages to compile. |
|
| 182 | 200 | packages: [package::Package; MAX_PACKAGES], |
|
| 183 | 201 | /// Driver inputs for each package slot. |
| 320 | 338 | throw error(msg); |
|
| 321 | 339 | } |
|
| 322 | 340 | return args[*idx]; |
|
| 323 | 341 | } |
|
| 324 | 342 | ||
| 325 | - | /// Parse CLI arguments and return compilation context. |
|
| 326 | - | unsafe fn processCommand( |
|
| 343 | + | /// Parse and validate command-line inputs and compilation options. |
|
| 344 | + | fn parseCommand( |
|
| 327 | 345 | args: *[*[u8]], |
|
| 328 | - | arena: &mut ast::NodeArena |
|
| 329 | - | ) -> CompileContext throws (Error) { |
|
| 346 | + | inputs: &mut [PackageInput] |
|
| 347 | + | ) -> Command throws (Error) { |
|
| 330 | 348 | let mut buildTest = false; |
|
| 331 | 349 | let mut debugEnabled = false; |
|
| 332 | 350 | let mut outputPath: ?*[u8] = nil; |
|
| 333 | 351 | let mut rilDirectory: ?*[u8] = nil; |
|
| 334 | 352 | let mut dump = Dump::None; |
|
| 335 | 353 | let mut entryPkgName: ?*[u8] = nil; |
|
| 336 | 354 | ||
| 337 | 355 | // Per-package source path tracking. |
|
| 338 | - | let mut inputs: [PackageInput; MAX_PACKAGES] = undefined; |
|
| 339 | 356 | let mut pkgCount: u32 = 0; |
|
| 340 | 357 | let mut currentPkgIdx: ?u32 = nil; |
|
| 341 | 358 | ||
| 342 | 359 | if args.len == 0 { |
|
| 343 | 360 | io::printError(USAGE); |
| 347 | 364 | ||
| 348 | 365 | while idx < args.len { |
|
| 349 | 366 | let arg = args[idx]; |
|
| 350 | 367 | if mem::eq(arg, "-pkg") { |
|
| 351 | 368 | try nextArg(args, &mut idx, &["`-pkg` requires a package name"]); |
|
| 352 | - | if pkgCount >= MAX_PACKAGES { |
|
| 369 | + | if pkgCount >= inputs.len { |
|
| 353 | 370 | throw error(&["too many packages specified"]); |
|
| 354 | 371 | } |
|
| 355 | 372 | set inputs[pkgCount] = packageInput(args[idx]); |
|
| 356 | 373 | set currentPkgIdx = pkgCount; |
|
| 357 | 374 | set pkgCount += 1; |
|
| 358 | 375 | } else if mem::eq(arg, "-mod") { |
|
| 359 | 376 | try nextArg(args, &mut idx, &["`-mod` requires a module path"]); |
|
| 360 | 377 | let pkgIdx = currentPkgIdx else { |
|
| 361 | 378 | throw error(&["`-mod` must follow a `-pkg` argument"]); |
|
| 362 | 379 | }; |
|
| 363 | - | let input: *unsafe mut PackageInput = &mut inputs[pkgIdx]; |
|
| 380 | + | let input = &mut inputs[pkgIdx]; |
|
| 364 | 381 | if hasExtension(args[idx], ASM_SOURCE_EXT) { |
|
| 365 | 382 | if input.asmPathCount >= MAX_ASM_MODULES { |
|
| 366 | 383 | throw error(&["too many assembly modules specified"]); |
|
| 367 | 384 | } |
|
| 368 | 385 | set input.asmPaths[input.asmPathCount] = args[idx]; |
| 377 | 394 | } else if mem::eq(arg, "-start") { |
|
| 378 | 395 | try nextArg(args, &mut idx, &["`-start` requires an assembly path"]); |
|
| 379 | 396 | let pkgIdx = currentPkgIdx else { |
|
| 380 | 397 | throw error(&["`-start` must follow a `-pkg` argument"]); |
|
| 381 | 398 | }; |
|
| 382 | - | let input: *unsafe mut PackageInput = &mut inputs[pkgIdx]; |
|
| 399 | + | let input = &mut inputs[pkgIdx]; |
|
| 383 | 400 | if input.startupPath <> nil { |
|
| 384 | 401 | throw error(&["package", input.name, "has more than one startup file"]); |
|
| 385 | 402 | } |
|
| 386 | 403 | if not hasExtension(args[idx], ASM_SOURCE_EXT) { |
|
| 387 | 404 | throw error(&["`-start` requires a `.ras` assembly file"]); |
| 447 | 464 | if entryPkgIdx == nil { |
|
| 448 | 465 | throw error(&["fatal:", "entry package", entryName, "not found"]); |
|
| 449 | 466 | } |
|
| 450 | 467 | } |
|
| 451 | 468 | let entryIdx = entryPkgIdx else { |
|
| 452 | - | panic "processCommand: no entry package"; |
|
| 469 | + | panic "parseCommand: no entry package"; |
|
| 453 | 470 | }; |
|
| 454 | 471 | for i in 0..pkgCount { |
|
| 455 | 472 | if i <> entryIdx and inputs[i].startupPath <> nil { |
|
| 456 | 473 | throw error(&["`-start` is only supported on the entry package"]); |
|
| 457 | 474 | } |
|
| 458 | 475 | } |
|
| 459 | 476 | if rilDirectory <> nil and (outputPath <> nil or dump <> Dump::None) { |
|
| 460 | 477 | throw error(&["`-ril` requires a separate invocation from `-o` or `-dump`"]); |
|
| 461 | 478 | } |
|
| 462 | - | let graph = module::moduleGraph(&mut MODULE_ENTRIES[..], arena); |
|
| 463 | - | let mut ctx = CompileContext { |
|
| 464 | - | packages: undefined, |
|
| 465 | - | inputs, |
|
| 479 | + | return Command { |
|
| 466 | 480 | packageCount: pkgCount, |
|
| 467 | - | entryPkgIdx, |
|
| 468 | - | graph, |
|
| 481 | + | entryPkgIdx: entryIdx, |
|
| 469 | 482 | config: resolver::Config { buildTest }, |
|
| 470 | 483 | dump, |
|
| 471 | 484 | outputPath, |
|
| 472 | 485 | rilDirectory, |
|
| 473 | 486 | debug: debugEnabled, |
|
| 474 | 487 | }; |
|
| 488 | + | } |
|
| 489 | + | ||
| 490 | + | /// Parse CLI arguments and return compilation context. |
|
| 491 | + | unsafe fn processCommand( |
|
| 492 | + | args: *[*[u8]], |
|
| 493 | + | arena: &mut ast::NodeArena |
|
| 494 | + | ) -> CompileContext throws (Error) { |
|
| 495 | + | let mut inputs = [packageInput(""); MAX_PACKAGES]; |
|
| 496 | + | let command = try parseCommand(args, &mut inputs[..]); |
|
| 497 | + | let graph = module::moduleGraph(&mut MODULE_ENTRIES[..], arena); |
|
| 498 | + | let mut ctx = CompileContext { |
|
| 499 | + | packages: undefined, |
|
| 500 | + | inputs, |
|
| 501 | + | packageCount: command.packageCount, |
|
| 502 | + | entryPkgIdx: command.entryPkgIdx, |
|
| 503 | + | graph, |
|
| 504 | + | config: command.config, |
|
| 505 | + | dump: command.dump, |
|
| 506 | + | outputPath: command.outputPath, |
|
| 507 | + | rilDirectory: command.rilDirectory, |
|
| 508 | + | debug: command.debug, |
|
| 509 | + | }; |
|
| 475 | 510 | // Initialize and parse all packages. |
|
| 476 | 511 | let mut sourceArena = alloc::new(&mut MODULE_SOURCES[..]); |
|
| 477 | - | for i in 0..pkgCount { |
|
| 512 | + | for i in 0..ctx.packageCount { |
|
| 478 | 513 | let name = ctx.inputs[i].name; |
|
| 479 | 514 | package::init(&mut ctx.packages[i], i as u16, name, &mut STRING_POOL); |
|
| 480 | 515 | ||
| 481 | 516 | for j in 0..ctx.inputs[i].radPathCount { |
|
| 482 | 517 | let path = ctx.inputs[i].radPaths[j]; |
test/command
added
+98 -0
| 1 | + | #!/bin/sh |
|
| 2 | + | # Verify command-line parsing and validation through the compiler driver. |
|
| 3 | + | set -eu |
|
| 4 | + | work=$(mktemp -d) |
|
| 5 | + | trap 'rm -rf "$work"' EXIT HUP INT TERM |
|
| 6 | + | checks=0 |
|
| 7 | + | ||
| 8 | + | # Use the compiler's standard Make memory and stack settings. |
|
| 9 | + | command() { |
|
| 10 | + | timeout 30 "${MAKE:-make}" --no-print-directory driver-command \ |
|
| 11 | + | RAD_BIN="${RAD_BIN:-bin/radiance.rv64.dev}" \ |
|
| 12 | + | RAD_EMULATOR="${RAD_EMULATOR:-emulator}" \ |
|
| 13 | + | DRIVER_ARGUMENTS="$1" > "$work/command.log" 2>&1 |
|
| 14 | + | } |
|
| 15 | + | ||
| 16 | + | # Require a specific diagnostic, including checks that precede file loading. |
|
| 17 | + | reject() { |
|
| 18 | + | if command "$1"; then |
|
| 19 | + | echo "command: unexpected success: $1" |
|
| 20 | + | exit 1 |
|
| 21 | + | fi |
|
| 22 | + | if ! grep -Fq -- "$2" "$work/command.log"; then |
|
| 23 | + | cat "$work/command.log" |
|
| 24 | + | echo "command: missing diagnostic: $2" |
|
| 25 | + | exit 1 |
|
| 26 | + | fi |
|
| 27 | + | checks=$((checks + 1)) |
|
| 28 | + | } |
|
| 29 | + | ||
| 30 | + | # Require successful compilation or dumping. |
|
| 31 | + | accept() { |
|
| 32 | + | if ! command "$1"; then |
|
| 33 | + | cat "$work/command.log" |
|
| 34 | + | exit 1 |
|
| 35 | + | fi |
|
| 36 | + | checks=$((checks + 1)) |
|
| 37 | + | } |
|
| 38 | + | ||
| 39 | + | while IFS='|' read -r arguments expected; do |
|
| 40 | + | reject "$arguments" "$expected" |
|
| 41 | + | done <<'CASES' |
|
| 42 | + | |usage: radiance |
|
| 43 | + | -pkg|`-pkg` requires a package name |
|
| 44 | + | -mod|`-mod` requires a module path |
|
| 45 | + | -start|`-start` requires an assembly path |
|
| 46 | + | -entry|`-entry` requires a package name |
|
| 47 | + | -o|`-o` requires an output path |
|
| 48 | + | -ril|`-ril` requires an output directory |
|
| 49 | + | -dump|`-dump` requires a mode |
|
| 50 | + | -mod absent.rad|`-mod` must follow a `-pkg` argument |
|
| 51 | + | -start absent.ras|`-start` must follow a `-pkg` argument |
|
| 52 | + | -dump invalid|unknown dump mode invalid |
|
| 53 | + | -unknown|unknown argument -unknown |
|
| 54 | + | -test|no package specified |
|
| 55 | + | -pkg p|package p has no Radiance modules specified |
|
| 56 | + | -pkg p -mod absent.ras|package p has no Radiance modules specified |
|
| 57 | + | -pkg p -mod p.rad -start x.rad|`-start` requires a `.ras` assembly file |
|
| 58 | + | -pkg p -mod p.rad -start x.ras -start y.ras|has more than one startup file |
|
| 59 | + | -pkg p -mod p.rad -pkg q -mod q.rad|`-entry` required when multiple packages specified |
|
| 60 | + | -pkg p -mod p.rad -pkg q -mod q.rad -entry missing|entry package missing not found |
|
| 61 | + | -pkg p -mod p.rad -start x.ras -pkg q -mod q.rad -entry q|`-start` is only supported on the entry package |
|
| 62 | + | -pkg p -mod p.rad -ril out -o out.rv64|`-ril` requires a separate invocation |
|
| 63 | + | -pkg p -mod p.rad -ril out -dump ast|`-ril` requires a separate invocation |
|
| 64 | + | CASES |
|
| 65 | + | ||
| 66 | + | # At each path limit, parsing reaches the deliberately absent source file. |
|
| 67 | + | arguments='-pkg p' |
|
| 68 | + | for count in $(seq 1 129); do |
|
| 69 | + | arguments="$arguments -mod $work/absent.rad" |
|
| 70 | + | if [ "$count" -eq 128 ]; then reject "$arguments" 'error reading file'; fi |
|
| 71 | + | done |
|
| 72 | + | reject "$arguments" 'too many modules specified for package' |
|
| 73 | + | arguments="-pkg p -mod $work/absent.rad" |
|
| 74 | + | for count in $(seq 1 65); do |
|
| 75 | + | arguments="$arguments -mod $work/absent.ras" |
|
| 76 | + | if [ "$count" -eq 64 ]; then reject "$arguments" 'error reading file'; fi |
|
| 77 | + | done |
|
| 78 | + | reject "$arguments" 'too many assembly modules specified' |
|
| 79 | + | reject '-pkg a -pkg b -pkg c -pkg d -pkg e' 'too many packages specified' |
|
| 80 | + | ||
| 81 | + | printf '%s\n' '/// Executable command fixture.' '@default fn main() -> u32 { return 0; }' > "$work/p.rad" |
|
| 82 | + | for mode in ast graph il asm; do |
|
| 83 | + | accept "-pkg p -mod $work/p.rad -dump $mode" |
|
| 84 | + | done |
|
| 85 | + | # A single package selects itself; the final output option selects the path. |
|
| 86 | + | accept "-pkg p -mod $work/p.rad -entry unused -debug -o $work/first.rv64 -o $work/final.rv64" |
|
| 87 | + | test ! -e "$work/first.rv64" |
|
| 88 | + | test -s "$work/final.rv64" |
|
| 89 | + | timeout 10 "${RAD_EMULATOR:-emulator}" -run "$work/final.rv64" |
|
| 90 | + | ||
| 91 | + | arguments='' |
|
| 92 | + | for name in a b c; do |
|
| 93 | + | printf '%s\n' '/// Package fixture value.' 'constant value: u32 = 0;' > "$work/$name.rad" |
|
| 94 | + | arguments="$arguments -pkg $name -mod $work/$name.rad" |
|
| 95 | + | done |
|
| 96 | + | accept "$arguments -pkg p -mod $work/p.rad -entry p -o $work/multiple.rv64" |
|
| 97 | + | timeout 10 "${RAD_EMULATOR:-emulator}" -run "$work/multiple.rv64" |
|
| 98 | + | echo "command tests: $checks passed" |