From d61805262eff036c1eda2cc19419c418bde431f6 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 22 Aug 2026 01:59:02 +0900 Subject: [PATCH] ww output: match Go exact -o parsing --- cmd/ww/main.c | 24 +- docs/build-system.md | 170 ++++++++++++++ docs/spec.md | 35 ++- docs/test-system-v2.md | 24 +- internal/wwpackage/package.ww | 14 +- selfhost/cmd/ww/main.ww | 41 ++-- test/package/package_test.ww | 429 ++++++++++++++++++++++++++++++++++ 7 files changed, 701 insertions(+), 36 deletions(-) diff --git a/cmd/ww/main.c b/cmd/ww/main.c index c634fc5e..f7056faf 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -7375,7 +7375,8 @@ parse_build_flags(const char *cmd, int argc, char **argv, } else if (strncmp(argv[i], "-I", 2) == 0 && argv[i][2]) { if (include_append(cmd, incs, incsz, argv[i] + 2) < 0) return -1; - } else if (strcmp(argv[i], "-o") == 0) { + } else if (strcmp(argv[i], "-o") == 0 || + (strcmp(cmd, "build") == 0 && strcmp(argv[i], "--o") == 0)) { if (i + 1 >= argc) { fprintf(stderr, "ww %s: -o needs an argument\n", cmd); @@ -7383,7 +7384,16 @@ parse_build_flags(const char *cmd, int argc, char **argv, } if (cli_copy(cmd, "-o", outpath, outsz, argv[++i]) < 0) return -1; - } else if (strncmp(argv[i], "-o", 2) == 0 && argv[i][2]) { + } else if (strcmp(cmd, "build") == 0 && + strncmp(argv[i], "-o=", 3) == 0) { + if (cli_copy(cmd, "-o", outpath, outsz, argv[i] + 3) < 0) + return -1; + } else if (strcmp(cmd, "build") == 0 && + strncmp(argv[i], "--o=", 4) == 0) { + if (cli_copy(cmd, "-o", outpath, outsz, argv[i] + 4) < 0) + return -1; + } else if (strcmp(cmd, "run") == 0 && + strncmp(argv[i], "-o", 2) == 0 && argv[i][2]) { if (cli_copy(cmd, "-o", outpath, outsz, argv[i] + 2) < 0) return -1; } else if (argv[i][0] == '-') { @@ -7863,7 +7873,8 @@ do_test(int argc, char **argv) } else if (strncmp(argv[i], "-timeout-ms=", 12) == 0 && argv[i][12] != '\0') { packageopts = 1; - } else if (strcmp(argv[i], "-o") == 0) { + } else if (strcmp(argv[i], "-o") == 0 || + strcmp(argv[i], "--o") == 0) { if (i + 1 >= argc) { fprintf(stderr, "ww test: -o needs an argument\n"); @@ -7871,9 +7882,12 @@ do_test(int argc, char **argv) } if (cli_copy("test", "-o", outstem, sizeof outstem, argv[++i]) < 0) return 2; - } else if (argv[i][1] == 'o' && argv[i][2]) { + } else if (strncmp(argv[i], "-o=", 3) == 0) { if (cli_copy("test", "-o", outstem, sizeof outstem, - argv[i] + 2) < 0) return 2; + argv[i] + 3) < 0) return 2; + } else if (strncmp(argv[i], "--o=", 4) == 0) { + if (cli_copy("test", "-o", outstem, sizeof outstem, + argv[i] + 4) < 0) return 2; } else if (strcmp(argv[i], "-w") == 0) { if (i + 1 >= argc) { fprintf(stderr, diff --git a/docs/build-system.md b/docs/build-system.md index a7012d31..3caee2a8 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -8658,6 +8658,176 @@ executable, and retained-test-product bytes are unchanged. Build workdir format remains `18`, test workdir format remains `19`, and semantic storage format remains `3`. +### 11.44 Implemented exact output-option name and value semantics + +The `ww build` and `ww test` output option has the exact registered name `o`. +Its accepted forms are `-o VALUE`, `--o VALUE`, `-o=VALUE`, and +`--o=VALUE`. An equals form splits at the first `=` and preserves the complete +remaining value, including an empty value and additional `=` bytes. Repeated +occurrences are last-value-wins. A final empty value means no effective +explicit output: a single command uses its ordinary default, a multi-command +build performs its ordinary no-public-output build, a running test retains no +copy, and compile-only testing uses its ordinary default retained name. +Concatenated names such as `-oVALUE` and `--oVALUE` are unknown flags rather +than output requests. Build option parsing stops at the first root operand; +test parsing continues to recognize known test options, including exact `o`, +on either side of its package operands. + +#### Pinned evidence and fact classification + +The sole authority is official Go 1.26.5 at +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- `init` registers exactly string flag name `o` for the build command in + [`cmd/go/internal/work/build.go`, lines 241–248](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L241-L248), + and `invoke` parses that flag set before passing only remaining operands to + `runBuild` in + [`cmd/go/main.go`, lines 290–322](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/main.go#L290-L322). +- `(*FlagSet).parseOne` accepts one or two leading dashes, splits the first + `=`, consumes the next argument only when there was no equals delimiter, and + rejects an unregistered concatenated name in + [`flag/flag.go`, lines 1074–1146](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/flag/flag.go#L1074-L1146). + `(*FlagSet).Parse` stops at the first non-flag operand at lines 1149–1176. + `stringValue.Set` and `(*FlagSet).Set` overwrite a repeated string value at + lines 240–250 and 494–528. `runBuild` derives effective explicit output from + the final value's nonzero length and otherwise selects its default behavior + in + [`cmd/go/internal/work/build.go`, lines 459–478](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L478). +- Build command testdata uses an equals-delimited output successfully in + [`version_buildvcs_nested.txt`, line 57](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/version_buildvcs_nested.txt#L57). + `testParse` exercises exact names with two leading dashes and separate + values at + [`flag_test.go`, lines 164–215](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/flag/flag_test.go#L164-L215), + `TestUserDefined` asserts an equals-delimited string value at lines 251–267, + and `TestUsage` asserts unknown-flag failure at lines 153–161. +- Test command `init` registers exactly string flag name `o` in + [`cmd/go/internal/test/testflag.go`, lines 32–38](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/testflag.go#L32-L38). + `cmdflag.ParseOne` accepts one or two dashes, cuts the first `=`, preserves + empty and remaining-equals value bytes, and rejects unknown names in + [`cmd/go/internal/cmdflag/flag.go`, lines 53–118](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/cmdflag/flag.go#L53-L118). + `testFlags` recognizes known flags before and after the package list and + rejects an unknown flag with `-c` in + [`cmd/go/internal/test/testflag.go`, lines 219–349](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/testflag.go#L219-L349). + `testNeedBinary` and the output-classification branch treat final empty + `testO` as no explicit retained destination in + [`cmd/go/internal/test/test.go`, lines 631–646 and 771–781](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L631-L781). +- Official command testdata uses compile-only test output successfully in + [`devnull.txt`, lines 3–8](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/devnull.txt#L3-L8) + and + [`test_race_tag.txt`, lines 1–9](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_race_tag.txt#L1-L9). + [`test2json_interrupt.txt`, line 10](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test2json_interrupt.txt#L10) + places `-o` after a package operand, while + [`test_flag.txt`, lines 11–16](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_flag.txt#L11-L16) + asserts unknown-flag rejection with `-c` across supported placements. + +Those registrations, parser branches, effective-output branches, and official +assertions are **behavior directly implemented or asserted by pinned Go**. +Applying their exact-name, first-equals, final-value, and command-placement +rules to WW's local command surface while retaining WW's diagnostic wording is +**behavior derived from the pinned implementation**. The measured WW matrix +below is **directly measured WW behavior**; no installed host Go result is used +as authority. + +#### Direct pre-fix Cstage and WWstage matrix + +Both stages had identical pre-fix behavior in every row: + +| Route and spelling | Directly measured pre-fix result | +| --- | --- | +| single build, `-o=name` | exited 0 with empty streams, left `name` absent, and published executable `=name` | +| single build, `-oname` | exited 0 with empty streams and incorrectly accepted the concatenated name | +| single build, `--o name` or `--o=name` | exited 2 with empty stdout and byte-identical `ww build: unknown flag\n` stderr | +| single build, final `-o=` | exited 0 with empty streams and published literal file `=` rather than the default command name | +| build values containing `=` | retained an erroneous leading `=`; repeated separate forms were already last-value-wins | +| build option after the first root | remained package input rather than being reparsed, matching the required placement boundary | +| two-command coordinator, `-o=DIR/` | published both products beneath a literal leading-`=` directory | +| two-command coordinator, `-oDIR/` | incorrectly accepted the joined name and published beneath the requested directory | +| two-command coordinator, final `-o=` | exited 2 as `cannot use -o with multiple packages` instead of selecting no effective explicit output | +| compile-only directory test, `-o=name` | exited 0 with empty streams, left `name` absent, and published test binary `=name` | +| compile-only directory test, `-oname` | incorrectly accepted the concatenated name and published that retained binary | +| compile-only directory test, `--o=name` | exited 2 with empty stdout and byte-identical `ww test: unknown flag\n` stderr | +| compile-only directory test, final `-o=` or a value containing `=` | published literal `=` or an erroneous leading-`=` destination | +| test `-o` after its package operand | was already recognized, matching the required test placement boundary | + +The wrong-path single-build executables were stage-byte-identical 4,268-byte +files with SHA-256 +`866c1eb875dad271d37572f43fb9d9b0eb6a2344d2e61646e655bb09f7909bf6`. +The wrong-path retained test binaries were stage-byte-identical 112,829-byte +files with SHA-256 +`5ea3ac9add844dc4cd98cc07fb64816415b5e3c03745dc4cc756ec093cf5cea7`. +The build and test unknown-flag diagnostics were respectively 23 and 22 bytes, +also byte-identical between Cstage and WWstage. These byte counts and hashes +describe only the direct pre-fix measurements. + +#### Ownership and complete four-axis result + +The Cstage command owners are `parse_build_flags` and `do_test` in +`cmd/ww/main.c`. Their WWstage semantic twins are `dobuild` and `dotest` in +`selfhost/cmd/ww/main.ww`. The shared multi-package owner is +`packagecommand` in `internal/wwpackage/package.ww`. Each recognizes exact +one-/two-dash separate/equals forms, replaces prior occurrences with the final +value, and derives effective explicit-output state from that final value's +non-emptiness. The build parser retains its first-root stop, while test and the +coordinator retain their established after-package recognition. `dorun` and +the shared `run` route are not changed. The compiler, assembler, archiver, +linker, runtime, package checker, and import resolver do not own this rule. + +- **Go-like build:** exact accepted forms select the same established output + path as separate `-o VALUE`; a final empty value selects the existing + default/no-public-output branch. Invalid concatenated names reject before + loading, graph or action construction, producers, publication, or runtime. +- **Go-like test:** the same exact forms select retained destinations on both + sides of package operands. Final empty means no running-test retention or + the normal compile-only default. Discovery, variants, generated main, + filtering, execution, accounting, result annotation, and absence of a + test-result cache do not change. +- **Go-like package:** output bytes remain presentation metadata. Source + eligibility, package clauses, declared names, variants, command + classification, canonical package representatives, graph nodes, actions, + symbols, artifacts, and persistence keys are unchanged. +- **Go-like import:** output spelling creates no binding or edge and changes no + dotted import spelling, alias, search, local/vendor/internal rule, + visibility, cycle, initialization order, canonical identity, or `.wwi` + ownership. + +#### Loading, lifecycle, parity, and proof + +Accepted forms enter the same existing loading, graph, scheduling, compiler, +assembler, archiver, linker, runtime, publication, persistence, reuse, and +invalidation paths as `-o VALUE`. They add no action, process, transaction, +cache, key, artifact byte, or runtime state. Producer or runtime failure, +late output rejection, rollback, prior-state preservation, concurrent +publication, interruption, and process-group cleanup therefore retain their +established owners and results. Output installation keeps the existing +transaction, object-safety, mode, null-device, output-directory, and +running-retained-test guard rules. + +Invalid concatenated names stop before all loading and work, create no +diagnostic competitor or product, and leave no unit, interface, assembly, +object, archive, executable, retained test binary, capture, output prefix, +`.new`, `.install`, `.wwtxn.*`, or scratch residue. A final empty value cannot +create literal `=`, `=.sepwork`, or transaction residue. Cold and warm +accepted requests use the ordinary publication and reuse paths; changing only +an accepted spelling does not rekey semantic work. Independent concurrent +requests own independent argument state, workdirs, stages, captures, and +outputs. Build starts no runtime; running tests keep their private executable +and publish only after successful execution. + +The WW-native `output_flag_exact_name_and_value_semantics` observer is the +focused owner for both public stages and all three parser routes. It covers the +four accepted forms, extra and empty equals values, repetition, concatenated +name rejection, build/test placement controls, multi-command behavior, +diagnostic parity, runnable and retained artifact-byte parity, warm reuse, and +absence of literal-equals and transaction residue. Existing transaction, +producer/runtime failure, rollback, concurrency, interruption, output-mode, +and cleanup observers remain authoritative for the unchanged downstream +mechanisms. Post-fix byte counts and hashes are recorded only after direct +focused measurement; this section does not infer them from the implementation. + +This is command parsing and output presentation only. No persisted-byte +contract changes: build workdir format remains `18`, test workdir format +remains `19`, and semantic storage format remains `3`. + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/docs/spec.md b/docs/spec.md index e962488e..bd187f56 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -344,7 +344,19 @@ ImportPath = ident { "." ident } . arguments and result; this rule does not adopt Go's source signature. An ordinary import of a package declared `main` is rejected, except for the toolchain's colocated external-test wiring. -- For `ww build`, an explicit `-o` names an output directory when ordinary +- For `ww build`, the output-option name is exactly `o`. The accepted forms are + `-o VALUE`, `--o VALUE`, `-o=VALUE`, and `--o=VALUE`. An equals form splits + at its first `=` and preserves every later byte, including further `=` + characters; an empty value is valid. Repetition is last-value-wins, and a + final empty value means that there is no effective explicit output, so the + ordinary default-output or no-public-output rule applies. A concatenated + spelling such as `-oVALUE` or `--oVALUE` is an unknown flag, not an output + option. Build option parsing stops at the first package or source operand; + later flag-like arguments remain operands and are not reparsed as `-o`. + These spelling and placement rules select only caller-visible output + disposition and never supply package, import, graph, action, symbol, + artifact, `.wwi`, or persistence identity. + A nonempty effective `-o` names an output directory when ordinary `stat` reports an existing directory (following symlinks) or its spelling ends in `/`. This classification is independent of whether one or many package roots were requested. Each selected command is published beneath @@ -771,11 +783,22 @@ coordinator reports that successful validation exactly as `? [no test files]\n`. Every test-bearing directory product links one request-private runnable. -`-c` retains an executable copy and suppresses its execution; `-o` retains a -copy at the named destination and still executes unless `-c` is present. With -no explicit output, `-c` writes `.test` in the invocation -directory. An output ending in `/` or naming an existing directory receives -that basename and may have missing parent directories created. One +The test output-option name is exactly `o`, with the accepted forms `-o VALUE`, +`--o VALUE`, `-o=VALUE`, and `--o=VALUE`. Equals forms split only at their +first `=` and preserve an empty value or any additional `=` bytes. Repeated +occurrences are last-value-wins. A final empty value means no effective +explicit output and therefore requests no running-test retention; with `-c`, +the ordinary default retained name applies. A concatenated `-oVALUE` or +`--oVALUE` is unknown. Unlike build option parsing, known test options, +including these exact output forms, are recognized before or after package +operands. Invalid output-option names reject before package loading, product +construction, execution, or publication. +`-c` retains an executable copy and suppresses its execution; a nonempty +effective `-o` retains a copy at the named destination and still executes +unless `-c` is present. With no effective explicit output, `-c` writes +`.test` in the invocation directory. An output ending in `/` or +naming an existing directory receives that basename and may have missing +parent directories created. One non-directory output may name only one package. Multiple packages whose visible import leaves would produce the same test-binary name reject before tools or output creation; exact `/dev/null` is the discard exception. It keeps diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 53613e20..549fdcff 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -236,13 +236,23 @@ warning on stderr and accounting on stdout because no coordinator combines its descriptors or emits a package result. Directory test binaries are always linked under the coordinator's temporary -product root. `-c` independently requests a caller-visible executable copy and -suppresses execution. `-o` independently requests a copy and still runs the -temporary binary unless `-c` is also present. Without `-o`, `-c` publishes -`.test` in the invocation directory; an output ending in `/` or -naming an existing directory receives one such name per selected package and -missing parents are created. A single non-directory output is legal for one -package only. Multi-package non-directory output and duplicate visible binary +product root. The retained-output option has the exact registered name `o` and +accepts `-o VALUE`, `--o VALUE`, `-o=VALUE`, and `--o=VALUE`. An equals form +splits only at its first `=`, preserving an empty value and any later `=` +bytes. Repeated occurrences are last-value-wins. A final empty value is no +effective explicit output: it requests no retained copy for a running test, +while `-c` falls back to the ordinary `.test` name. Concatenated +`-oVALUE` and `--oVALUE` spell unknown flags and reject before loading, product +construction, tools, execution, or publication. Known test flags, including +these exact output forms, remain recognized before or after package operands. +`-c` independently requests a caller-visible executable copy and suppresses +execution. A nonempty effective `-o` independently requests a copy and still +runs the temporary binary unless `-c` is also present. Without an effective +explicit output, `-c` publishes `.test` in the invocation +directory; an output ending in `/` or naming an existing directory receives +one such name per selected package and missing parents are created. A single +non-directory output is legal for one package only. Multi-package +non-directory output and duplicate visible binary names reject before tools; exact `/dev/null` discards every copy and permits duplicate names. The private test link still runs unless `-c` suppresses it; the raw single-file compatibility route also links into driver-owned private diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index 01f68563..80c5362b 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -2055,16 +2055,20 @@ export fn packagecommand(args: []str) int = { i += 1; continue; }; - if (strings.compare(a, "-o") == 0) { + if (strings.compare(a, "-o") == 0 + || strings.compare(a, "--o") == 0) { if (i + 1 >= args.len) { pkgusage(); return 2; }; outname = args[i + 1]; - explicitout = true; + explicitout = outname.len != 0; i += 2; continue; }; - if (strings.hasprefix(a, "-o") && a.len > 2) { - outname = a[2:a.len]; - explicitout = true; + if (strings.hasprefix(a, "-o=") + || strings.hasprefix(a, "--o=")) { + let valueoff: i32 = 3; + if (a[1] == '-') { valueoff = 4; }; + outname = a[valueoff:a.len]; + explicitout = outname.len != 0; i += 1; continue; }; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 6f7709f1..a82642e4 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -8853,19 +8853,25 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { }; libs[nlibs] = nm; nlibs += 1; - } else { if (p[1u64] == 111u8) { // '-o' - if (p[2u64] != 0u8) { - outflag = p + 2u64; + } else { if (cstreqlit(p, "-o") || cstreqlit(p, "--o") + || (p[1u64] == 111u8 && p[2u64] == 61u8) + || (p[1u64] == 45u8 && p[2u64] == 111u8 + && p[3u64] == 61u8)) { + if (p[1u64] == 111u8 && p[2u64] == 61u8) { + outflag = p + 3u64; + } else { if (p[1u64] == 45u8 + && p[2u64] == 111u8 && p[3u64] == 61u8) { + outflag = p + 4u64; } else { if (i + 1 >= argc) { cerr("ww build: -o needs an argument\n"); return 2; }; i += 1; - outflag = argv[i]; - }; - if (!clipathfits("build", "-o", outflag)) { return 2; }; - } else { if (p[1u64] == 119u8) { // '-w' + outflag = argv[i]; + }; }; + if (!clipathfits("build", "-o", outflag)) { return 2; }; + } else { if (p[1u64] == 119u8) { // '-w' if (p[2u64] != 0u8) { workdir = p + 2u64; } else { @@ -8892,6 +8898,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { }; i += 1; }; + if (outflag != nil && outflag[0u64] == 0u8) { outflag = nil; }; if (src == nil) { let dot: [2]u8 = ['.': u8, 0u8]; @@ -9656,19 +9663,26 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { && ps.len > 12) { packageopts = true; i += 1; continue; }; - if (p[1u64] == 111u8) { // '-o' - if (p[2u64] != 0u8) { - outstem = p + 2u64; + if (cstreqlit(p, "-o") || cstreqlit(p, "--o") + || (p[1u64] == 111u8 && p[2u64] == 61u8) + || (p[1u64] == 45u8 && p[2u64] == 111u8 + && p[3u64] == 61u8)) { + if (p[1u64] == 111u8 && p[2u64] == 61u8) { + outstem = p + 3u64; + } else { if (p[1u64] == 45u8 + && p[2u64] == 111u8 && p[3u64] == 61u8) { + outstem = p + 4u64; } else { if (i + 1 >= argc) { cerr("ww test: -o needs an argument\n"); return 2; }; i += 1; - outstem = argv[i]; + outstem = argv[i]; }; - if (!clipathfits("test", "-o", outstem)) { return 2; }; - i += 1; continue; + }; + if (!clipathfits("test", "-o", outstem)) { return 2; }; + i += 1; continue; }; if (p[1u64] == 119u8) { // '-w' if (p[2u64] != 0u8) { @@ -9691,6 +9705,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { }; i += 1; }; + if (outstem != nil && outstem[0u64] == 0u8) { outstem = nil; }; if (target == nil) { let dot: [2]u8 = ['.': u8, 0u8]; target = &dot[0]; diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 812c6480..dc38af71 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -13816,6 +13816,435 @@ fn runtimepath(relative: str) str = { clean(root); }; +@test fn output_flag_exact_name_and_value_semantics() void = { + let root: str = fresh(); + let source: str = strings.concat(root, "/source"); + let multi_a: str = strings.concat(source, "/alpha"); + let multi_b: str = strings.concat(source, "/beta"); + let tests: str = strings.concat(source, "/tests"); + let outputs: str = strings.concat(root, "/outputs"); + mkdirall(source); mkdirall(multi_a); mkdirall(multi_b); + mkdirall(tests); mkdirall(outputs); + let single: str = strings.concat(source, "/single.ww"); + writefile(single, + "package main;\nfn main() i32 = { return 29; };\n"); + writefile(strings.concat(multi_a, "/main.ww"), + "package main;\nfn main() i32 = { return 31; };\n"); + writefile(strings.concat(multi_b, "/main.ww"), + "package main;\nfn main() i32 = { return 37; };\n"); + writefile(strings.concat(tests, "/tests.ww"), + "package exactflags;\nfn value() i32 = { return 41; };\n"); + writefile(strings.concat(tests, "/tests_test.ww"), strings.concat( + "package exactflags;\n", + "@test fn exact() void = { assert(value() == 41); };\n")); + let rawtest: str = strings.concat(source, "/raw_test.ww"); + writefile(rawtest, strings.concat( + "package exactraw;\n", + "@test fn raw_exact() void = { assert(true); };\n")); + + let compilerwrapper: str = strings.concat(root, "/output-flag-w6c.sh"); + writeexecutable(compilerwrapper, strings.concat( + "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$WW_OUTPUT_FLAG_TRACE\"\n", + "exec \"$WW_OUTPUT_FLAG_REAL_C\" \"$@\"\n")); + let stages: []str = ["ww", "ww_ww"]; + let compilers: []str = ["w6c", "w6c_ww"]; + let tags: []str = ["c", "ww"]; + let forms: []str = ["one-separate", "one-equals", "two-separate", + "two-equals"]; + let buildref: str = ""; + let testref: str = ""; + let alpharef: str = ""; + let betaref: str = ""; + let rawref: str = ""; + let rejectrefs: []str = ["", "", "", "", ""]; + let coordinatorrejectref: str = ""; + let buildmissingref: str = ""; + let testmissingref: str = ""; + let stopref: str = ""; + let baseenv: []str = os.getenvs(); + let out: commandout; + let si: i32 = 0; + for (si < stages.len) { + let trace: str = strings.concat(root, "/output-flag-", tags[si], + ".trace"); + writefile(trace, ""); + let env: []str = alloc([], (baseenv.len + 3): u64)!; + let ei: i32 = 0; + for (ei < baseenv.len) { + if (!strings.hasprefix(baseenv[ei], "WW_W6C=") + && !strings.hasprefix(baseenv[ei], "WW_OUTPUT_FLAG_")) { + append(env, baseenv[ei]); + }; + ei += 1; + }; + append(env, strings.concat("WW_W6C=", compilerwrapper)); + append(env, strings.concat("WW_OUTPUT_FLAG_REAL_C=", + driver(compilers[si]))); + append(env, strings.concat("WW_OUTPUT_FLAG_TRACE=", trace)); + let ignoredrunout: str = strings.concat(outputs, "/ignored-run-", + tags[si]); + let runcontrol: []str = [driver(stages[si]), "run", "-I", source, + strings.concat("-o", ignoredrunout), single]; + runcommand(root, strings.concat("output-flag-run-control-", tags[si]), + runcontrol, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 29); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !os.exists(ignoredrunout)); + + // Exact output names change only publication. Distinct accepted + // spellings retain the same action and executable bytes. + let buildwork: str = strings.concat(root, "/build-work-", tags[si]); + mkdirall(buildwork); + let fi: i32 = 0; + for (fi < forms.len) { + let binary: str = strings.concat(outputs, "/build-", tags[si], + "-", forms[fi], "=tail"); + let av: []str = [driver(stages[si]), "build", "-w", buildwork, + "-I", source]; + if (fi == 0) { append(av, "-o"); append(av, binary); } + else if (fi == 1) { append(av, strings.concat("-o=", binary)); } + else if (fi == 2) { append(av, "--o"); append(av, binary); } + else { append(av, strings.concat("--o=", binary)); }; + append(av, single); + rewritefile(trace, ""); + runcommandenv(root, strings.concat("output-flag-build-", tags[si], + "-", forms[fi]), av, env, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && os.exists(binary)); + if (fi == 0) { assert(readfile(trace).len != 0); } + else { assert(readfile(trace).len == 0); }; + let bytes: str = readfile(binary); + if (buildref.len == 0) { buildref = strings.dup(bytes); } + else { assert(same(buildref, bytes)); }; + let runav: []str = [binary]; + runcommand(root, strings.concat("output-flag-build-run-", tags[si], + "-", forms[fi]), runav, time.second, &out); + expectexit(&out, 29); + assert(out.stdout.len == 0 && out.stderr.len == 0); + fi += 1; + }; + + // Repeated string flags are last-wins. A final empty value returns to + // default naming and cannot create a file or scratch tree named '='. + let stale: str = strings.concat(outputs, "/stale-build-", tags[si]); + let winner: str = strings.concat(outputs, "/winner-build-", tags[si], + "=last"); + let lastav: []str = [driver(stages[si]), "build", "-w", buildwork, + "-I", source, "-o", stale, strings.concat("--o=", winner), single]; + rewritefile(trace, ""); + runcommandenv(root, strings.concat("output-flag-last-", tags[si]), + lastav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !os.exists(stale) && same(buildref, readfile(winner)) + && readfile(trace).len == 0); + let defaultbin: str = strings.concat(root, "/single"); + let emptyav: []str = [driver(stages[si]), "build", "-w", buildwork, + "-I", source, "-o", stale, "-o=", single]; + rewritefile(trace, ""); + runcommandenvdir(root, strings.concat("output-flag-empty-", tags[si]), + emptyav, env, root, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !os.exists(stale) && same(buildref, readfile(defaultbin)) + && readfile(trace).len == 0 + && !os.exists(strings.concat(root, "/=")) + && !os.exists(strings.concat(root, "/=.sepwork"))); + assert(os.remove(defaultbin) == 0); + clean(strings.concat(defaultbin, ".sepwork")); + + // Build parsing stops at its first operand: a later flag-like value is + // package input, not a second output option. + let early: str = strings.concat(outputs, "/early"); + let late: str = strings.concat(outputs, "/late"); + let stopav: []str = [driver(stages[si]), "build", "-I", source, + "-o", early, single, strings.concat("--o=", late)]; + runcommand(root, strings.concat("output-flag-stop-", tags[si]), stopav, + (60i64 * (time.second: i64)): time.duration, &out); + assert(out.termination == exec.termination.EXIT && out.code != 0); + assert(out.stdout.len == 0 && !has(out.stderr, "unknown flag") + && !os.exists(early) && !os.exists(late)); + if (si == 0) { stopref = strings.dup(out.stderr); } + else { assert(same(stopref, out.stderr)); }; + + // Joined names are unknown before loading, work creation, or output. + let ri: i32 = 0; + for (ri < 2) { + let badout: str = strings.concat(outputs, "/bad-build-", tags[si], + "-", forms[ri]); + let badwork: str = strings.concat(root, "/bad-build-work-", tags[si], + "-", forms[ri]); + let prefix: str = "-o"; + if (ri != 0) { prefix = "--o"; }; + let joined: str = strings.concat(prefix, badout); + let badav: []str = [driver(stages[si]), "build", "-w", badwork, + "-I", source, joined, single]; + runcommand(root, strings.concat("output-flag-reject-build-", tags[si], + "-", forms[ri]), badav, time.second, &out); + expectexit(&out, 2); + assert(out.stdout.len == 0 + && same(out.stderr, "ww build: unknown flag\n") + && !os.exists(badout) && !os.exists(badwork)); + if (si == 0) { rejectrefs[ri] = strings.dup(out.stderr); } + else { assert(same(rejectrefs[ri], out.stderr)); }; + ri += 1; + }; + let buildmissingwork: str = strings.concat(root, + "/missing-build-work-", tags[si]); + let buildmissing: []str = [driver(stages[si]), "build", "-w", + buildmissingwork, "-I", source, "--o"]; + runcommand(root, strings.concat("output-flag-missing-build-", tags[si]), + buildmissing, time.second, &out); + expectexit(&out, 2); + assert(out.stdout.len == 0 + && same(out.stderr, "ww build: -o needs an argument\n") + && !os.exists(buildmissingwork)); + if (si == 0) { buildmissingref = strings.dup(out.stderr); } + else { assert(same(buildmissingref, out.stderr)); }; + + // Two roots exercise coordinator output planning. The second spelling + // reuses the first request's compiled actions, and final empty performs + // the established no-public-output multi-build. + let multiwork: str = strings.concat(root, "/multi-work-", tags[si]); + mkdirall(multiwork); + let multione: str = strings.concat(root, "/multi-one-", tags[si], "/"); + let multiav: []str = [driver(stages[si]), "build", "-w", multiwork, + "-I", source, strings.concat("--o=", multione), multi_a, multi_b]; + rewritefile(trace, ""); + runcommandenvdir(root, strings.concat("output-flag-multi-one-", tags[si]), + multiav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && readfile(trace).len != 0); + let alphaone: str = strings.concat(multione, "alpha"); + let betaone: str = strings.concat(multione, "beta"); + if (alpharef.len == 0) { + alpharef = readfile(alphaone); betaref = readfile(betaone); + } else { + assert(same(alpharef, readfile(alphaone)) + && same(betaref, readfile(betaone))); + }; + let alpharun: []str = [alphaone]; + runcommand(root, strings.concat("output-flag-alpha-run-", tags[si]), + alpharun, time.second, &out); + expectexit(&out, 31); + assert(out.stdout.len == 0 && out.stderr.len == 0); + let betarun: []str = [betaone]; + runcommand(root, strings.concat("output-flag-beta-run-", tags[si]), + betarun, time.second, &out); + expectexit(&out, 37); + assert(out.stdout.len == 0 && out.stderr.len == 0); + let multitwo: str = strings.concat(root, "/multi-two-", tags[si], "/"); + let warmav: []str = [driver(stages[si]), "build", "-w", multiwork, + "-I", source, "-o", multitwo, multi_a, multi_b]; + rewritefile(trace, ""); + runcommandenvdir(root, strings.concat("output-flag-multi-warm-", tags[si]), + warmav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && readfile(trace).len == 0 + && same(alpharef, readfile(strings.concat(multitwo, "alpha"))) + && same(betaref, readfile(strings.concat(multitwo, "beta")))); + let unusedmulti: str = strings.concat(root, "/multi-unused-", tags[si], + "/"); + let multiempty: []str = [driver(stages[si]), "build", "-w", multiwork, + "-I", source, "-o", unusedmulti, "--o=", multi_a, multi_b]; + rewritefile(trace, ""); + runcommandenvdir(root, strings.concat("output-flag-multi-empty-", tags[si]), + multiempty, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && readfile(trace).len == 0 && !os.exists(unusedmulti) + && !os.exists(strings.concat(root, "/alpha")) + && !os.exists(strings.concat(root, "/beta"))); + let multibadout: str = strings.concat(outputs, "/bad-multi-", tags[si]); + let multibadwork: str = strings.concat(root, "/bad-multi-work-", tags[si]); + let multibad: []str = [driver(stages[si]), "build", "-w", multibadwork, + "-I", source, strings.concat("--o", multibadout), multi_a, multi_b]; + runcommand(root, strings.concat("output-flag-reject-multi-", tags[si]), + multibad, time.second, &out); + expectexit(&out, 2); + assert(out.stdout.len == 0 + && same(out.stderr, "ww build: unknown flag\n") + && !os.exists(multibadout) && !os.exists(multibadwork)); + if (si == 0) { rejectrefs[4] = strings.dup(out.stderr); } + else { assert(same(rejectrefs[4], out.stderr)); }; + let directbadout: str = strings.concat(outputs, + "/bad-direct-coordinator-", tags[si]); + let directbadwork: str = strings.concat(root, + "/bad-direct-coordinator-work-", tags[si]); + let directbad: []str = [driver("wwtest"), "package", + "--ww-operation", "build", "--ww-driver", driver(stages[si]), + "-w", directbadwork, "-I", source, + strings.concat("-o", directbadout), multi_a, multi_b]; + runcommand(root, strings.concat("output-flag-reject-coordinator-", + tags[si]), directbad, time.second, &out); + expectexit(&out, 2); + assert(out.stdout.len == 0 && has(out.stderr, "usage: wwtest package") + && !os.exists(directbadout) && !os.exists(directbadwork)); + if (si == 0) { coordinatorrejectref = strings.dup(out.stderr); } + else { assert(same(coordinatorrejectref, out.stderr)); }; + + // Compile-only directory testing accepts the same four spellings. Its + // final empty option may follow the package and restores the default. + let testwork: str = strings.concat(root, "/test-work-", tags[si]); + mkdirall(testwork); + fi = 0; + for (fi < forms.len) { + let binary: str = strings.concat(outputs, "/test-", tags[si], "-", + forms[fi], "=tail"); + let av: []str = [driver(stages[si]), "test", "-c", "-w", testwork, + "-I", source]; + if (fi == 0) { append(av, "-o"); append(av, binary); } + else if (fi == 1) { append(av, strings.concat("-o=", binary)); } + else if (fi == 2) { append(av, "--o"); append(av, binary); } + else { append(av, strings.concat("--o=", binary)); }; + append(av, tests); + rewritefile(trace, ""); + runcommandenv(root, strings.concat("output-flag-test-", tags[si], "-", + forms[fi]), av, env, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && os.exists(binary)); + if (fi == 0) { assert(readfile(trace).len != 0); } + else { assert(readfile(trace).len == 0); }; + let bytes: str = readfile(binary); + if (testref.len == 0) { testref = strings.dup(bytes); } + else { assert(same(testref, bytes)); }; + let runav: []str = [binary]; + runcommand(root, strings.concat("output-flag-test-run-", tags[si], "-", + forms[fi]), runav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0 && has(out.stdout, "exact ... ok\n")); + fi += 1; + }; + let staletest: str = strings.concat(outputs, "/stale-test-", tags[si]); + let runningretained: str = strings.concat(outputs, + "/running-test-", tags[si], "=tail"); + let runningretainedav: []str = [driver(stages[si]), "test", "-w", + testwork, "-I", source, strings.concat("--o=", runningretained), + tests]; + rewritefile(trace, ""); + runcommandenvdir(root, strings.concat("output-flag-test-running-retained-", + tags[si]), runningretainedav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0 && has(out.stdout, "exact ... ok\n") + && same(testref, readfile(runningretained)) + && readfile(trace).len == 0); + let defaulttest: str = strings.concat(root, "/tests.test"); + let testempty: []str = [driver(stages[si]), "test", "-c", "-w", + testwork, "-I", source, "-o", staletest, tests, "--o", ""]; + rewritefile(trace, ""); + runcommandenvdir(root, strings.concat("output-flag-test-empty-", tags[si]), + testempty, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !os.exists(staletest) && same(testref, readfile(defaulttest)) + && readfile(trace).len == 0 + && !os.exists(strings.concat(root, "/="))); + assert(os.remove(defaulttest) == 0); + let runningstale: str = strings.concat(outputs, + "/stale-running-test-", tags[si]); + let runningempty: []str = [driver(stages[si]), "test", "-w", + testwork, "-I", source, "-o", runningstale, tests, "-o="]; + rewritefile(trace, ""); + runcommandenvdir(root, strings.concat("output-flag-test-running-empty-", + tags[si]), runningempty, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0 && has(out.stdout, "exact ... ok\n") + && !os.exists(runningstale) && readfile(trace).len == 0); + + // The raw single-file route owns output state in the public driver, so + // final empty must normalize before its retain/copy decision too. + let rawwork: str = strings.concat(root, "/raw-work-", tags[si]); + mkdirall(rawwork); + let rawretained: str = strings.concat(outputs, "/raw-test-", tags[si], + "=tail"); + let rawretainedav: []str = [driver(stages[si]), "test", "-w", rawwork, + strings.concat("--o=", rawretained), rawtest]; + runcommandenvdir(root, strings.concat("output-flag-raw-retained-", + tags[si]), rawretainedav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0 && has(out.stdout, "raw_exact ... ok\n") + && os.exists(rawretained)); + let rawpublishedbytes: str = readfile(rawretained); + if (rawref.len == 0) { rawref = strings.dup(rawpublishedbytes); } + else { assert(same(rawref, rawpublishedbytes)); }; + let rawstale: str = strings.concat(outputs, "/stale-raw-", tags[si]); + let rawempty: []str = [driver(stages[si]), "test", "-c", "-w", + rawwork, "-o", rawstale, "--o=", rawtest]; + runcommandenvdir(root, strings.concat("output-flag-raw-empty-", tags[si]), + rawempty, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let rawbinary: str = strings.concat(rawwork, "/main"); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !os.exists(rawstale) && os.exists(rawbinary) + && !os.exists(strings.concat(root, "/="))); + let rawbytes: str = readfile(rawbinary); + assert(same(rawref, rawbytes)); + ri = 0; + for (ri < 2) { + let badout: str = strings.concat(outputs, "/bad-test-", tags[si], + "-", forms[ri]); + let badwork: str = strings.concat(root, "/bad-test-work-", tags[si], + "-", forms[ri]); + let prefix: str = "-o"; + if (ri != 0) { prefix = "--o"; }; + let joined: str = strings.concat(prefix, badout); + let badav: []str = [driver(stages[si]), "test", "-c", "-w", + badwork, "-I", source, joined, tests]; + runcommand(root, strings.concat("output-flag-reject-test-", tags[si], + "-", forms[ri]), badav, time.second, &out); + expectexit(&out, 2); + assert(out.stdout.len == 0 + && same(out.stderr, "ww test: unknown flag\n") + && !os.exists(badout) && !os.exists(badwork)); + if (si == 0) { rejectrefs[2 + ri] = strings.dup(out.stderr); } + else { assert(same(rejectrefs[2 + ri], out.stderr)); }; + ri += 1; + }; + let testmissingwork: str = strings.concat(root, + "/missing-test-work-", tags[si]); + let testmissing: []str = [driver(stages[si]), "test", "-c", "-w", + testmissingwork, "-I", source, "--o"]; + runcommand(root, strings.concat("output-flag-missing-test-", tags[si]), + testmissing, time.second, &out); + expectexit(&out, 2); + assert(out.stdout.len == 0 + && same(out.stderr, "ww test: -o needs an argument\n") + && !os.exists(testmissingwork)); + if (si == 0) { testmissingref = strings.dup(out.stderr); } + else { assert(same(testmissingref, out.stderr)); }; + + let cleanpaths: []str = [root, outputs, source, multi_a, multi_b, tests, + buildwork, multiwork, testwork, rawwork]; + let ci: i32 = 0; + for (ci < cleanpaths.len) { + assert(!directoryhasnew(cleanpaths[ci]) + && !directoryhasfragment(cleanpaths[ci], ".install") + && !directoryhasfragment(cleanpaths[ci], ".wwtxn.")); + ci += 1; + }; + si += 1; + }; + assert(!os.exists(strings.concat(root, "/=")) + && !os.exists(strings.concat(root, "/=.sepwork"))); + clean(root); +}; + @test fn compile_artifact_naming() void = { let root: str = fresh(); let pdir: str = strings.concat(root, "/pkg");