Each @test forks into its own process group with a nonblocking control
pipe carrying framed ready/complete/skip/expect-abort records; timeouts
escalate SIGTERM then SIGKILL over the group. New exports skip,
expectabort, current; new flags -list, -timeout-ms=, -package=. A clean
exit without a completion frame is a harness error, and the exit code
is 0/1 with a discovered/selected/started/completed accounting line.
toktest drops its fixed /tmp path for temp.named so parallel invocations
cannot collide.
Flip the soft-default to a hard "missing package clause" error symmetrically in
both stages (cmd/wcc/parse.c + lib/ww/syntax/parse.ww): the first real decl of a
primary section with empty pathmod/resetmod and no seen clause is now rejected.
Closes the documented soft-default divergence (the 63-wrapper carve-out).
The gate flip can't be split from the migration it breaks, so this is one atomic
commit: ~80 test/wcc wrappers gain `package main;` via a shared wwtestpkg.h
helper, 6 data fixtures plus 17 asm-grep assertions update for the bare->main.<leaf>
root-helper mangle shift, and rt/ declares `package rt;` with @symbol pinning the
bare rt_ensure/rt_malloc linker names.
Root mangling narrows: the executable entry `main` stays bare (existing
carve-out), but root helper symbols become main.X. The #84 cluster is rewritten
to assert main.run distinct from aa.run/test.run; its cgen fix and bare machinery
are retained — still load-bearing for package-less module-reset deps. New
table-driven test 782_strict_package.c (6 rows, both stages).
Retiring //ww:module-reset is deferred to #24b: it is load-bearing (clears the
.wwi pathmod so the body's package clause asserts), not a vestige; fusing its
removal here would be a silent mismatch.
All byte-id gates green; full make test reports "all 335 tests passed".
fold-2 chunk C1 (drew's Fam8-13 plan): 960_opaque_decl_run.c and
962_opaque_assign_cast_run.c were value-row C drivers. Migrate their 3+3
cases to test/lang/opaque_decl_test.ww and opaque_assign_cast_test.ww as
@test row-tables. The test-lang byte-id (LANGBYTEID) gate gives cs==ww
automatically -- 960 was cstage-only-run before, so this strengthens it.
Both byte-id clean, no cs!=ww carve. Retire the 2 C drivers (LANGBYTEID floor
57->59) and repoint a dead comment ref in lib/sort/sort.ww. 961_opaque_guards
(reject/guards) stays in C -- fold-3 territory, not in the Fam12 fold-2
worklist.
wwi_emit took the .wwi `package` leaf from the first primary decl's module tag;
a fully empty primary module body (zero decls) had none, so the leaf stayed the
literal default "main" and the importer rejected it ("package main does not
match import path <leaf>"). The module identity is only available at parse time
(curmod is overwritten by imported //ww:module sections before emit), so stamp
the primary path onto the N_FILE node (TK_MODULE and TK_MODRESET rp!=NULL sites,
only-if-empty so a bare-reset `package main` root stays "main") and, when the
decl-scan finds no leaf, fall back to that stamped path. Symmetric cstage+
selfhost; both detect scan-miss via the same found-flag so the emitted .wwi
stays byte-identical.
Regression: test/wcc/989_wwileaf_run.c, table-driven over {empty body,
comment-only, nested a.b.c} decl-less shapes, non-vacuity proven.
BUG-A #9: when an empty/export-less inlined module body produces `//ww:module <p>` immediately followed by `//ww:module-reset` in one skipws run, lexnext drained TK_MODRESET first then emitted the STALE TK_MODPATH past the reset boundary, re-binding pathmod=<p> so the root `package main` was validated against <p> -> wrong reject. Fix clears the pending modpath at the reset-RECOGNITION site in skipws (NOT lexnext drain): cstage l->modpath=NULL after both l->modreset=1 (cmd/wcc/lex.c); selfhost l.modpathset=0 after all three l.modreset=1 (lib/ww/syntax/lex.ww). Symmetric (rule-10): identical token streams, bootstrap byte-id holds. The normal RESET-then-PATH boundary is unaffected (clear is a no-op when no path is pending). Pin: test/wcc/989_modresetadj_run.c feeds the raw composed adjacency to w6c+w6c_ww (accept + byte-id), red-path-proven (reverting a clear reddens it). A separate wwi-producer defect surfaced (decl-less .wwi defaults to 'package main') filed as #11; not touched here.
Mirror Hare's os env surface: getenvs() builds an owned []str of
NAME=VALUE entries from the rt_envp table (platform_environ.ha:41),
and getenv iterates it (environ.ha:32) so there is exactly one env
walker. strings.dup is unusable here -- lib/strings imports os, so os
importing strings would cycle; the owned copy is inlined (dup.ha:7).
test_getenvs_entries pins the []str shape non-vacuously.
Mirror cstage's C-variadic call handling in the ww self-host: parse a
bare `...` param (decl.ww), skip param-keyed desugar for it to avoid a
nil-deref (check.ww), and emit AL = XMM-reg count plus CVTSS2SD
promotion of f32 args in the variadic tail (cgenutil.ww, cgenexpr.ww).
Closes the cat-A wwstage silent miscompile (AL=0, unpromoted f32 tail).
Parse/check/cgen are one atomic align-up (parse alone miscompiles, so
not bisect-splittable). 989_ffivariadic now runs dual-stage (cstage ww
+ wwstage ww_ww), 12/12; w6c==w6c_ww byte-identical. Byte-id alone is
blind here (the bootstrap calls no float-bearing C variadic), so the
ww_ww runtime rows are the real net.
option = struct{flag:rune, value:str} is 32B now that str is 24B (Phase 3: cstage type.c:68, wwstage typ.ww:239 -- rune 4 + pad 4 + str 24). The three free sites (tryparse's two error paths + finish) freed (cap)*24u64 -- an 8-byte-per-element under-free of a buffer that append() grew at the real 32B stride, plus a rule-13 hardcoded-size-literal violation. Route all three through size(option).
Also corrects the stale 16B-era layout comments, including the error struct: error is 32B, so its (void|error) return rides sret/MEMORY (the 40B slot exceeds the 24B register cap), not the register ABI -- verified sound at runtime (ken; see #38).
Test (getopttest.ww, test 982_getopt_run): optionsizepin pins size(option)==32 as the regression guard; freeroundtrip is a table over {0,1,3} options exercising tryparse-then-finish. Full make test: 450 green incl. 990-997 byte-id.
The out/.pkgcache content-keyed store copied each artifact IN-PLACE
(cp -f / copyfile) to the fixed paths P.wwi/P.o/P.key. Key-last gave
crash-consistency but NOT concurrent-read safety: two same-stage builds
of a shared lib pkg (rt/time/os) target one out/.pkgcache/<pkg>/P.{wwi,o};
once an early finisher writes P.key, a later build's cache_lookup copies
P.wwi/P.o while a mid-finisher is still mid-write -> torn read -> corrupt
link / cs!=ww. The key is content-only, so it is purely the non-atomic
write.
Fix (Go-build-cache pattern, both stages in lock-step, rule 10): write
each artifact to a per-pid same-dir temp (P.wwi.tmp.<pid> etc.) then
rename() into place. Same dir => rename is atomic (cross-fs is not);
per-pid temp => concurrent writers don't clobber each other mid-copy;
content-keyed => last-writer-wins is byte-identical. Key renamed LAST so
a reader that sees the new key always finds complete artifacts. On any
mid-store error the per-pid temps are unlinked so a failed store leaves
no litter (cstage goto cleanup; wwstage cachermtmp helper).
cstage cmd/ww/main.c cache_store: libc rename(2) + getpid().
wwstage selfhost/cmd/ww/main.ww cachestore: new os.rename + cachetmp.
lib/os/os.ww: add rename(2) (RENAME=82), ref/hare/os/os.ha:17 -- returns
raw i32 errno like sibling remove/mkdir/rmdir (ww's os is the flat
syscall floor, no fs:: layer); a second pathbuf2 slot holds newpath
since kpath's single pathbuf can't carry both paths.
cache_lookup is unchanged: it reads cache->private scratch, and an atomic
source is never torn.
The torn-read race is closed BY CONSTRUCTION; a deterministic behavioral
regression-guard isn't feasible through the product build path (content-
keying => concurrent COLD builds all MISS+STORE, never HIT-read a mid-store
entry; a warm cache is never re-stored). The deferred white-box guard is
TASK #105. A WHY-comment at both fix sites records this.
Tests: 989_sepbuild_run KEEPS its private per-pid WW_PKGCACHE -- the
comment is corrected: the pin is NOT a torn-read mask (closed by
construction) but cold-compile isolation for the test's INTERMEDIATE
(.s/.unit.ww) byte-id compare, which a cache HIT legitimately skips
producing. The former 989_pkgcache_atomic_run is renamed to
989_pkgcache_concurrent_run and HONESTLY relabeled: it is a concurrent
shared-cache build-correctness smoke (N concurrent --sep builds sharing
one cache -> every binary byte-identical to an isolated reference + correct
run, both stages), NOT a torn-read/atomicity proof (a review revert-
experiment proved the original claim vacuous). Shrunk to 4 concurrent
builds x 1 batch x both stages. COLD/dev-only, off every byte-id/bootstrap
gate.
selfhost/cmd/ww/main.combined.ww remains stale (its writer was deleted at
the M4 E3-C1 flip; #90 deletes the file) -- not regenerated.
make test: all 445 passed; make sizelint clean; 990-997 byte-id hold.
connect/bind export `*sockaddrin`, so under --sep the .wwi producer's
check_exported_type (both stages, correctly) rejects an exported decl
referencing the unexported type. Invisible on the old combined path
(net inlined, fed to w6c without -I). Hare exports sockaddr_in
(ref/hare/sys/+linux/socket.ha:11) and lib/net plays Hare's sys role,
so the caller must be able to name it. Same class as #48. No codegen
change (export is a checker property); byte-id holds.
The -T harness synthesized `use test;` after name-binding, so the lib/test runner run keyed the bare scope and collided with a user-defined bare fn run — a spurious "duplicate fn run" reject (the E1 tolerance seam). Prepending the synth use before binding keys the runner as test.run in the test module namespace, distinct from the user bare run; the two coexist. Hare-faithful: the runner is its own test module (ref/hare/test/+test.ha:97). Inverts attest_userrun.ww from the #23-mandated reject to a coexist fixture; gate asserts exactly 1 TEXT run + 1 TEXT test.run on the -T asm (distinct symbols, not a dead-dup). Closes#80.
The parser folded a qualified type pkg.Type into two different node shapes by position: declaration position collapsed it into one N_TNAME (resolved via the strrchr-leaf path), but literal position left an N_DOT chain that the struct-literal typeref handoff had no resolver arm for, so pkg.Type{...} rejected with "expected type expression".
Normalize the literal-position N_DOT chain into the same source-order N_TNAME the declaration path emits, reusing the existing resolver; no new checker arm. cstage flattens at parseprimary struct-lit handoff; wwstage (no token peek) folds dots in parsepostfix and normalizes there, guarding numeric tuple components and staying in the postfix loop so trailing ops still chain. Both stages emit identical N_STRUCTLIT(N_TNAME). Prereq for qualifying wcc syntax refs (#75).
After the frontend consolidated into one syntax package, the wcc backend
imports syntax and calls its exported fns — whose signatures reference
types that were unexported. Producing syntax's .wwi interface re-triggered
check_exported_type ("exported declaration references unexported type"):
the residual of BUG-A at the one surviving syntax->wcc boundary. Export
the 16 types that appear in syntax's wcc-facing public surface (directly
in an exported signature, or via a recursively-referenced exported struct
field): nkind, node, lex, tok, tkind, parser, scope, sym, skind, tinfo,
tykind, tfield, tparam, ttupleelem, tctx, tinfocacheent. The set is
minimal (unexporting any one re-breaks the producer) and complete; pos
stays internal. Pure source change — exporting a type emits no code, so
the bootstrap binaries are byte-identical (verified against a clean base
build); only syntax's .wwi gains the type decls.
Post-frontend-reorg residual (#74). syntax now sep-produces clean both
stages. The separate concern of wcc's currently-unqualified refs to
syntax symbols (#75) is a distinct follow-up. Gate 989_syntaxexport_run.
The ww compiler frontend was split across packages lex (lex+tok), ww
(ast+sym+typ), and parse — mirroring Hare's ref/hare/hare/{ast,lex,parse}.
That split's only payoff is third-party reuse, which ww has zero of: the
frontend is consumed by exactly one client, the wcc backend. The split's
cost is a wide cross-package export surface — every fn over a sibling
package's type must export it, and under separate compilation that
re-triggers check_exported_type, plus a phantom `import tok;` (tok lives
in package lex). Consolidate into ONE package lib/ww/syntax/, modelled on
Go's cmd/compile/internal/syntax. The 9 files move in (package syntax);
the intra-frontend mutual references become same-package; wcc and the
tool mains import syntax. No cstage C change (the C frontend mangles from
the source package clause). Internal data shapes (AST kinds, token model,
lexer/parser state) still mirror ref/hare/hare per rule 6/12 — only the
module decomposition collapses; the stdlib is untouched.
USER-approved (#74); spec .ai/rob-frontend-reorg.md (drew2 fidelity-
confirmed). Rule-6 carve-out documented in CLAUDE.md. Dissolves the tok
phantom import; collapses the intra-frontend export sprawl. Byte-id
rebaseline (lex.X/parse.X/ww.X -> syntax.X); cs==ww held. The residual
syntax->wcc export surface (10 types) + the unqualified-ref question are
separate follow-ups (#72/#75).
The .wwi (separate-compile interface) producer could not serialize an
exported def whose initializer is a struct/array literal (N_STRUCTLIT/
N_ARRLIT) — `export def f64info: floatinfo = floatinfo{...}` aborted with
"unhandled const-expr node kind 15". Such a def is a DATA-global per the
#52 model, so its value lives once in the defining package's .o; the
interface needs only the type+symbol. Emit a value-less prototype
`export def X: T;` for aggregate-initializer defs; scalar fold-eligible
defs keep their value (the importer const-folds those). The parser gains
an optional-init arm so the importer can parse the prototype — value-less
`def X: T;` is now legal in any source, symmetric with the existing
bodyless-fn prototype `fn f();` (USER ruling: unconditional; a value-less
def with no defining .o is a loud undefined-symbol error at link, never
silent). Both stages; producer + parser fold into one commit (the
producer's output is unparseable without the parser arm).
M3-tail commit-6 prerequisite #2 (surfaced by the c6 scout). The
aggregate-def-field const-fold boundary is documented inline (#71). Gate
989_sepstructdef_run proves struct+array exported defs sep-build, link,
and run via external DATA refs, cs==ww, with a value-less .wwi.
A separately-compiled package's primary body was emitted under a bare
`//ww:module-reset`, so its own `package <leaf>;` clause set curmod to
the leaf (e.g. utf8) while the importer spliced the .wwi under the full
`//ww:module encoding.utf8` — definer mangled `utf8.X`, importer wanted
`encoding.utf8.X`, unresolved. Thread the dotted path through the
directive: `//ww:module-reset <path>` sets curmod to the dotted path
(imported stays 0, so the root `fn main` stays bare per #32), and the
body's package clause is demoted to a leaf==last-component assertion
instead of overwriting curmod. Aligns sep-build to the M1 path-mangle
model; only the SEP emitter changes (the combined build_one arm is
untouched, so all combined byte-id gates hold). Both stages mirrored.
Commit-6 broad-soak prerequisite. Gate 989_sepdotpath_run sep-builds a
2-level dotted package and proves definer==importer qualification +
single-component non-vacuity, cs==ww.
lib/types limit consts were bare `def`s, so the .wwi (sep-compile's
interface) correctly omitted them while the flat combined.ww let a
cross-package user (lib/strings splitn → types.I32_MAX) reach the
private def — sep-compile then failed (wwstage `asserttyped: dot
'I32_MAX'`; cstage undefined-ref). Hare exports types::I32_MAX and the
whole limit family (ref/hare/types/limits.ha, arch+x86_64.ha); ww not
exporting them was the divergence.
export the 24 existing limit defs ({I,U}{8,16,32,64}_{MIN,MAX},
INT/UINT/SIZE/UINTPTR_{MIN,MAX}) and the existing RUNE_MIN, and add
exported RUNE_MAX. ww's derived machine-word int/uint/size/uintptr
VALUES are kept verbatim (user-ratified 64-bit-int divergence); fidelity
here is the NAME SET + export-visibility, not the values. RUNE_MAX is
written `0x10ffff: rune` — same codepoint as Hare's '\U0010ffff', forced
because ww's lexer has no \u/\U escape (#50).
Exporting the consts made `w6c -I` walk them and fatal on RUNE_MIN
('\0'): the .wwi const-expr unparser had no N_RUNELIT arm. Add one,
both stages (wwi_rune / wwirune), rendering a \xHH-escaped rune literal
(>0xFF fails loud, #50). Const casts need no arm — the checker folds
them to integer literals before the producer runs. 989_m2wwi_run gains
a types.wwi gate (byte-id + re-parse + asserts export def I32_MAX and
RUNE_MAX reach the interface). byte-id-neutral: a def emits no symbol.
Switch symbol mangling from the import leaf clause to the full dotted import path for directory packages; single-file imports keep package-clause mangling (isdir-gate: imported<=>directory-import). The root build unit's fn main stays bare, every other top-level decl mangles, closing #31's duplicate-main hazard by construction (#32). Both stages, byte-identical.
Single commit, not split: the bare rename (f244af3) is red on its own because it unmasks cross-module resolution gaps that do not reproduce pre-M1, so the fixes are intrinsic to making the rename correct. Included: wwstage fnret/fnparamslookupmod map import alias->path (#199b cross-module union-variant scrutinee resolved the wrong fn's union); cstage use_path prefers the referencing module's import for an ambiguous leaf alias (sha256 crypto.math vs strconv math). Tests table-driven: 989_m1mangle_run/_sym, 989_m1union_run (gate-visible per-arm exit codes + cs==ww byte-id).
wwstage's tuple-parse loop checked the RPAREN-break at the top, so
`(a,)` parsed as a 1-element N_TUPLE and reached cgen — a silent
wrong-accept. A trailing comma is legal only after >=2 elements.
Align the loop order to cstage cmd/wcc/parse.c:552-558 (parse each
element before the RPAREN-break); `(a,)` now errors at the next
parseexpr, `(a, b)` / `(a, b,)` are unchanged. cstage already
rejected; this brings wwstage's w6c_ww parser into agreement.
Regen w6c/wwdump combined.ww (parser embeds in both). Valid-program
codegen unchanged → cs==ww byte-id gate stays green.
memio.fixedwrite returned a successful 0-byte write once the sink
filled, so an overflowing fprintf/bsprintf surfaced a truncated prefix
as a successful str instead of an error. Hare's fixed_write returns
nomem there (ref/hare/memio/stream.ha:161); the bsprintf/fprintf
io.error arm already forwards it, so the prefix-on-overflow path is the
only divergence.
Mirror Hare's full guard order: an empty input buf short-circuits to 0
(stream.ha:157) before the full-sink nomem guard, so a 0-byte write to
a full sink stays 0 (no new divergence). fmt.bsprintf/formatone keep
their logic; only their now-stale WHY-comments are rewritten, and
formatone's tail-pad counter is left as-is (the width-form restore is a
deferred follow-up, out of F-R scope). memio's own `fixed` doc comment,
which still claimed ww surfaces 0 on a full buffer, is corrected to the
new nomem contract.
Tests: flip the two fmt rows that pinned the prefix bug (bsprintf_trunc,
bsprintf_width_trunc) plus memiotest fixedwritecases' overflow row to
assert `is nomem`; add positive controls (bsprintf_exact must still
succeed) + an empty-sink discriminator (bsprintf_empty) + a dedicated
fixedwritefull unit pinning the memio.ww:190 contract.
Regenerates the w6c and wwdump combined.ww (memio's fixedwrite change
and `fixed` doc comment are the only embedded changes; fmt is
dead-code-eliminated from both).
Three lib functions had lost their Hare loud-abort preconditions, so an
out-of-domain argument silently returned garbage instead of aborting:
random.u32n / random.u64n assert(n != 0) ref/hare/math/random/random.ha:26,42
base64.decodedsize assert(sz%4 == 0) ref/hare/encoding/base64/base64.ha:597
Source-bundled lib change, identical on both stages (byte-id neutral).
989_libprecond_abort pins each precondition: n=0 / sz%4!=0 abort (rc!=0),
valid args return 0, run on cstage and wwstage.
The scanner readahead silently fell through when start==0 and the
buffer was full at maxread, producing no bytes and no error. scanbyte
then spun forever re-requesting bytes that never came (catB-144) and
scanrune nil-dereferenced s.ptr[s.start] (catB-145).
Make readahead the single overflow choke-point: at the ceiling it
returns a bufio-local `overflow` before the grow, propagated through
scanbyte/scanrune/scanbytes (scanbytes drops its now-redundant manual
pre-check). Mirrors ref/hare/bufio/scanner.ha:174-182, which returns
errors::overflow there; ww uses bufio-local overflow because io.error
is a closed enum without an overflow member. Consumers (regex, the 778
embedded source) gain the totality arm.
Table-driven @test crosses {nil-ptr, zero-len} x {scanbyte, scanrune};
neutralizing the overflow return reproduces the catB-144 hang.
The one-sided guard `v > 214748364` never fired for the last digit:
at v==214748364 a next digit of '8'/'9' made `v*10+digit` overflow
i32 and wrap negative, slipping past the signed args-index bound
check at fmt.ww:703 -> OOB arg read -> SIGSEGV on any format
directive carrying an over-i32 digit run (index, width or precision).
Complete it to the canonical two-part pre-multiply Horner guard
(MAX/10, MAX%10). Hand-rolled in signed i32, not Hare scan_sz's
unsigned post-multiply wrap-check (ref/hare/strconv/stou.ha:60),
which would be signed-overflow UB-class here; noted at the site.
Table-driven subprocess test over all three scandigits call sites,
5 rows x both stages; reverting the guard reproduces exit=139.
prev()'s walk-back decremented offs (i32) past 0 to -1 and returned
`more`; a subsequent next() then passed the signed `-1 < len` guard and
read d.src[-1] — a silent OOB decode of a garbage rune (no runtime
bounds net). Hare's decoder.offs is `size`: the underflow wraps to
SIZE_MAX so every `offs < len` guard exits safely (next returns more,
not a rune). Change offs to size and spell prev's loop as the Hare-form
`offs < len` guard; index sites take an i32 temp (ww's slice index is
i32 and `[...]` reads ':' as the slice separator).
No-runtime-net residual: remaining() would silently build a ptr-1/len+1
OOB view when called in the post-`more` state; guard it with a loud
abort (caller contract: don't call after `more`). The offs type ripples
into strings.ww's iterator<->decoder bridge (move/slice) — cast at the
four sites, safe on the rune-return path where offs is in range.
utf8/strings embed into all five selfhost combined.ww snapshots plus the
smoke.combined.ww test amalgamation; all regen'd. utf8test gains
prev_more_then_next_no_oob pinning the closed OOB.
dirs build() capped the composed path at the 256B pathbuf with a silent
break, so a HOME (or XDG_*) near/over ~240 bytes produced a truncated
path that lookup() then mkdir'd and returned rc=0 — a silently-wrong,
freshly-created directory. ref/hare/dirs/xdg.ha routes through
path::set/push whose too_long error the `!` aborts loudly. Precompute
the composition length in build() and rt_abort when it won't fit; drop
the now-dead silent caps. (Shape (a); routing dirs through lib/path is
the filed fidelity follow-up.)
975_dirs_toolong_run pins the abort + no-stray-dir on both driver twins.
writeone and formatraw's rune arm did `buf[0] = r: u8; putbytes(...,1)`,
emitting only the low byte — invalid UTF-8 for any rune > 0x7F, with a
success return. rawlen's rune arm hardcoded 1, desyncing width-padding
for multibyte runes. Route both write arms through utf8.encoderune and
rawlen through utf8.runesz (ref/hare/fmt/print.ha:84). Uses the current
in-tree encoderune(out: []u8, r) caller-buffer signature; report-#168's
signature realignment is a separate item, not folded here.
fmttest gains fprintrune (1/2/3/4-byte runes) + fprintf_rune_width
(multibyte pad).
i64dec computed the magnitude with `n = -n` inside i64, which wraps at
i64::MIN (-MIN == MIN stays negative), so the `for (n > 0)` loop never
ran and println(i64::MIN) emitted only the bare '-'. The whole print/
println/fprint family was affected at the single boundary value (and
int MIN, since int is 8B). ref/hare/fmt/print.ha:124-129 takes the
magnitude through math::absi64 into a u64; do the same (math.absi64
exists, lib/math/math.ww:12). ww's own printf path was already correct
((-v): u64), so print and printf now agree.
fmttest gains fprinti64min + fprintintmin pinning the boundary.
parsefile's recovery fallback chewed an unrecognized top-level
construct to the next ';' without emitting an error or bumping p.errs,
so a typo'd keyword / stray token silently vanished from the AST and
the build succeeded rc=0 with the declared work gone — no link error
catches a dropped @test or unreferenced exported fn. The C twin
(parse.c:1395-1400) errorf+p->errs++ and rejects. Emit errmsg in the
fallback arm; wwstage now rejects in lockstep with cstage.
lib/ww embeds into the w6c/wwdump combined.ww snapshots; both regen'd.
989_unknowndecl_reject pins the reject-matrix on both driver twins.
wwstage parseint dropped the pre-multiply overflow guard the C twin
carries (cmd/wcc/lex.c:156, if (v > (u64)~0ULL / (u64)base)), so any
integer literal exceeding u64 was silently accepted mod 2^64 while
cstage loudly rejected with 'bad integer literal' — a rule-10 stage
divergence and a silent wrong constant. Port the guard before the
multiply-add; wwstage now rejects in lockstep with cstage.
lib/ww embeds into the w6c/wwdump combined.ww snapshots; both regen'd.
989_intoverflow_reject pins the reject-matrix on both driver twins.
A type lookup in a bundled build resolved to the newest-installed
same-leaf symbol from ANY module; prefer the current module first
(mirror cstage sym.c:131; the prior attempt's failure was its own
u64-vs-i32 guard bug, not a deeper layer — probe-proven). Also adds
the rule-7 #58 notes at the latent varianterr/scruttype pair and
rewrites the stale deferral block to closing cites. Report item [5].
wwstage stamped N_RUNELIT as concrete rune (cstage: untyped_rune,
check.c:1296), so an uncast rune literal into a fitting integer slot
over-rejected where harec accepts (promote_flexible). Add coercerunelit
(mirror of coercefloatlit) at the scalar seams — let-init, return,
call-arg, index-store; coarse accept, no range check (mirror cstage
type.c:379); array-literal elements keep the range gate
(arrlit_init_fits, types.c:923). Teeth: the fnmatch.ww '\\': u8
workaround cast and getopttest's 'X': u8 index-store casts are removed
and compile green. Checker-only (no e.type_ restamp): byte-id held.
White-box @tests move into a colocated 'package regex' file (Hare
+test.ha analogue; #6 non-T drop plays the build-tag role), black-box
@tests stay in package regex_test — Go's foo/foo_test split. The
wb/ stub driver is documented-temporary scaffolding: a same-dir
importer file-resolves to regex.ww before dir-enumeration, so only a
different-dir import bundles the whitebox sibling (task #33 retires
it). CLAUDE.md rule-9 carve-out updated (#5 closed, gate was #6).
`ww test <file> <pattern>` runs only the @test fns whose names match the
fnmatch glob; no pattern runs all (byte-for-byte the pre-filter path);
zero matches prints "No tests run" and exits 0 (Hare ground truth
ref/hare/test/+test.ha:114-117). A pattern in directory mode is rejected
"ww test: pattern needs a single test file" (rc 2), identical wording in
both twins (cmd/ww/main.c do_test + selfhost/cmd/ww/main.ww dotest).
Mechanism (a): rt/start.s stashes argc/argv into rt_argc/rt_argv getters
(rt_envp twin shape, -T synth untouched so 990-997 byte-id holds);
lib/os.args() rebuilds the []str view, build-once-cached; lib/test/run.ww
imports fnmatch and filters av[1..] (argv[0] is the binary path). The
driver forwards the 2nd positional as argv[1] via fork/execv (cstage) /
procrun (wwstage) so glob metachars aren't shell-expanded.
os.args() is the first `alloc`-caller in the base os module, so os.ww now
imports rt — the `alloc` builtin's malloc lowers to rt_malloc only when
the rt binding is bundled (mirror lib/strings/strings.ww:30); without it a
plain `ww build` of any os-importing program links bare libc `malloc`
(undefined). os is bundled by ~every program, so this is load-bearing.
The lib/test floor rises os-only -> os+fnmatch+ascii+strings in every -T
build; the bundled `ascii` module vs a `@test fn ascii` collision that
exposed is closed by the preceding #30 promote commit. 989_test_filter
pins the full matrix on both twins byte-identically; 949 gains the
dir-mode reject row. (#17)
A primary-package top-level decl whose leaf also names a bundled module
(`type sym` vs `import sym`; the lib/test->fnmatch->ascii -T floor's
`ascii` module vs a `@test fn ascii`) collided in the flat scope: #23's
installtop dup-check false-fired "duplicate fn/type" where cstage
coexists. cstage keys the module name out of the value namespace by
PROMOTING the same-leaf SK_USE in place to the value kind with
use_alias=1 (cmd/wcc/check.c:2831/2848/2871/2907/2928/2951), so one
correctly-kinded sym serves bare refs (call/structlit/var) while
`name.member` still resolves the module via the `kind==SK_USE ||
use_alias` N_DOT guards (check.c:87/1337).
This REVERSES wwstage's documented two-sym coexistence design
(lib/ww/sym.ww scopelookupuselocal): keeping the SK_USE as a separate
coexisting sym ripples into every bare-reference resolver (~25
scopelookup sites), and a missed site is a byte-id-consistent-but-wrong
cat-A risk the gate cannot prove away — the same failure mode retired
with the name-keyed variant-match cluster. The promote model is correct
by construction: one sym of the right kind, identical to cstage.
sym gains a use_alias field; the three N_DOT/N_CALL module-qualified
guards honor use_alias. cstage installs every SK_USE in a dedicated
first pass, so its value-arm promote is order-INDEPENDENT; wwstage
installs in source order, so BOTH directions of the collision are
promoted to reach cstage's identical single-sym end state:
- use-before-value (`import aa` then `fn aa`): installtop promotes the
pre-installed SK_USE to the value kind (use_alias=1).
- value-before-use (`fn aa` then `import aa`): installdecl's N_USE arm
promotes the pre-installed value sym in place (set use_alias=1,
no coexisting SK_USE), mirroring cstage's self-import N_USE arm
(check.c:2823-2834 `if (prev) prev->use_alias = 1`).
Both orders compile + are cs/ww byte-identical AND byte-identical to
each other. Cannot split: promote without the guards leaves
`name.member` red on the promoted sym; the guards without promote are
inert (no use_alias is ever set) — no bisect-clean intermediate. (#30)
Pins (910/997): modfn_coexist_ok (use-before-value) AND
modfn_coexist_vbu_ok (value-before-use) both accept on cstage + are
cs/ww byte-id + run to exit 6 (bare fn and qualified module both
resolve); the dup_fn row still rejects both stages (regression). fnmatch
byte-id holds. The order-dependence is exactly what regresses silently,
so both orders are pinned.
wwstage over-rejects harec's flexible-rune-const narrow (rune lit
promotes into any int whose range fits — harec check.c:1910,
types.c promote_flexible); explicit cast until that promotion lands
in ww (#29, revert to bare '\\' then). With the enum-as fix fnmatch
is byte-identical across stages: 989 #59.14 graduates
M_WWREJECT -> M_ID, transferring 972's cstage 8/8 behavior to the
provably-identical wwstage binary — the dark-regression gate. (#29)
installdecl routes all four kinds (fn/type/def/let) through installtop,
which turns scopedefineinmodule's nil return into cstage's exact
"duplicate <kind> <name>" reject, keyed (name,mod) so cross-package
same-leaf decls coexist. Builtin redecls are dropped, not dup-errored:
cstage never scopes builtins (lookup_builtin first, check.c:69), so a
user redecl is dead there — wwstage mirrors via scopesamekeysym +
no-source-decl test. -T synth __wwtests installs direct, mirroring
check.c:3079. Closes the silent dup-fn hole (user fn run vs lib/test
run built a broken test binary with no diagnostic). Per-kind reject
rows + cross-package/builtin accept byte-id rows + -T collision parity
row in 910/997. (#23-team, category-A addendum closed)
lib/test/run.ww: fork+wait4 runner; each @test runs in its own child,
abort/SEGV/FPE decoded from wait-status, failures recorded and the run
continues; exit = fail count. Tests are hermetic: module globals do not
persist test-to-test (fresh fork image; sanctioned divergence from
harec's shared-process __test_main, no setjmp/signal layer needed).
-T synth (both stages) emits a module-global (str,*fn() void) table +
return run(table) instead of straight-line calls. Driver twins bundle
lib/test under test mode and gain ww test -c/-o (go test -c) so the
byte-id gates diff the same artifact the real path builds. Gates
989/910/997 rewired onto it; new 911 pins record-and-continue across
all three fault classes; 949 +3 rows. (#17-team commit-2)
'ww test' gains the istest build path (-T injection in build_one/
buildone) and do_test/dotest accept -I, mirroring do_run - both twins.
The 35 converted lib tests drop their interim bare mains (-T
synthesizes the entry from @test fns and rejects a user main); their
35 C run-drivers flip 'ww run' -> 'ww test'; 989_lib_byteid compiles
lib tests under -T (8 user-main probe fixtures stay non-T, gated on
the fixture field). Abort-on-first-failure stands until the deferred
record-and-continue harness lands with the multi-package arc.
A bare 'let x: T;' with 1 <= size(T) <= 7 matched no zero-fill arm in
either stage (8B and >8B were already zeroed) - 'let c: [3]u8;' read
stack garbage. User-ruled zero-value semantics: cstage gate sz>8 ->
sz>0; wwstage zsz==8 arm hoisted above the fill-run arm (required -
8B would otherwise route into the run and diverge) and run gate
zsz>0. New 840 pin: dirty-frame probe rows, dual-dim (run + cs/ww
byte-id); discriminators fail exit-154 on pre-fix binaries.
Fused with the lib/bytes test conversion (rule 11): either half alone
turns 967 red. The old exit(signalled+10) wrapped a real 1782-count
ltrim failure to exit 0 - green depended on the garbage. Converted to
assert form (completes the 35/35 @test conversion); ltrim rows keep
the bare 'let c: [3]u8;' as the consumer proof of the fix.