compiler: Parse command-line inputs in checked code

b74f809eab359b36114cc608d7e1bff66d9767b013604eb472efeb59afa72e82
Alexis Sellier committed ago 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"