The unspecified tail of a short array literal repeated the last value
instead of zeroing — wwstage only (cstage already zeroes; the review's
both-stages reading didn't survive ground truth). Zero-fill the tail
per the zero-value semantics ruling. Review item #13.
The str-size arm of the global data emit was size-keyed and matched a
24-sized array, emitting a second DATAW for the same symbol. Gate on
the array kind (!isarr8). Review item #12.
A tagged for-range destructure binding took the 8-byte default
(paramfieldsize had no tagged arm), skipping the full-extent copy —
the wwstage twin of the just-closed cstage destructure family. Read
the stamped tinfo size through the type table (twin of the slice
arm; cstage reads tp->type->size). Task #53.
cstage dereferenced 0(BP) for the base — a SEGV on every store
through a module-global struct pointer; wwstage was already correct
(the inverse-set member). Mirror ww's global-pointer load. New
989_globptrfield_run pins cs==ww both stages. Task #47.
The arg-drain loop checked node_isfloat before popping a widened
arg's GP words, so a float arg adjacent to a widened (tagged) arg
read the wrong stack slot: the f64 took the widened payload (#30), or
the float arm ate the tag word into X0 and the payload landed in DI
as the tag (#48). One missing branch, two manifestations — mirror
cstage's widen-first pop (cgen.c:9650-9665). Review items #30+#48
(fold reviewer-verified one-mechanism against the cstage twin).
Casting a global str to []u8 dropped the cap=len synth (the trailing
MOVQ BX,CX) — the synth was gated on a local-ident source shape.
Key it on the source type so local/global/field/call sources all get
the header. Review item #19.
s.f *= v silently became s.f = v (and the other non-+=/-= ops dropped
likewise) in BOTH stages across five lvalue sub-arms: via-ptr field,
direct local field, str/slice pseudo-field, and the two global-field
forms. Funnel all five through a shared combine dispatch
(cgdotfieldcombine / cg_dotfield_combine) emitting the load-OP-store
sequence at field width, hard-erroring the unhandled kinds — close-by-
construction so no arm stays on the old PLUSEQ-only path (the #133
BUS-routing lesson; #227 sites A/B are the closed siblings). The
refactor routes the corpus's existing +=/-= sites through the same
helper output-identically (byte-id held). Review item #34.
Both stages move in one commit: one emission contract; splitting the
halves would leave the byte-id gates red in between.
arr[i].field /= %= <<= >>= silently dropped the op (load-combine-store
emitted plain assignment) in BOTH stages — gate-blind, the #133 class.
Route every compound op through the combine dispatch at the indexed-
field arm and hard-error the unhandled operand kinds (float/str/slice/
tagged), per the #133 template (3986818). The runtime-correct target is
the op's own algebra (a OP= b == a = a OP b). Review item #33.
Both stages move in one commit: the fix is a single emission contract —
splitting cstage cgen.c from selfhost cgenexpr.ww would leave the
byte-id gates red between the halves.
'as' on a nullable value compared the pointer itself to a tag (the
missing arm). Mirror cgtypetest's nullable fold and the cstage twin
(cgen.c:10694). Review item #17.
Try-propagation on a nullable value fell to the tagged path and
compared the pointer to a tag — the null check came out inverted.
Mirror ww's own cgtypetest nullable fold (cgenexpr.ww:652) and the
cstage twin (cgen.c:10366/10504). Review item #15.
exprtype N_MATCH took the first arm's yield type without walking the
rest — int-vs-str arms silently produced garbage downstream. Walk all
arms: typeeq-equal accepts; a definite coarse-family mismatch
(num/str/bool via the new yieldclass classifier, unknown classes stay
lenient) rejects. Same-family non-assignable pairs remain lenient —
the documented precision residual is task #52 (needs tinfo-level
type_assignable). The wave's table-driven reject test lands here:
989_catA_f2_reject, 19 rows x 2 stages, each member pre-fix-red-proven.
A module-global float-bearing struct passed by value drained all-GP —
the SSE-cursor classify keyed on the node shape and missed the global
ident; key off the stamped struct type (per-eightbyte classify,
F7-flavored stamp fix). Review item #31; dual-stage rows red-proven.
Widening a global tagged union into a wider tagged slot copied
nothing of the box; treat the global ident as a tagged source and
copy the full box from g(SB) through the nested arm. Both-wrong pair:
cstage spills frame garbage as the box (filed task #44, residual
non-deterministic so the rows assert divergence only). The non-nested
SUBSET-widen shape remains open as task #49 (site comment at the
fall-through). Review item #51.
Widening a global struct into a tagged slot copied word0 only; copy
the full payload from g(SB). Both-wrong pair: cstage zero-fills the
payload (filed task #43); rows pin ww-runtime-correct with the
documented cstage residual. Review item #50.
Returning a global struct by name emitted nothing into the return
scratch (ww) — copy from the g(SB) base. Both-wrong pair: cstage
zeroes the retscr instead (filed task #42); rows pin ww-runtime-correct
with the documented cstage residual. Review item #41.
Reassigning a module-global tagged union stored the payload into the
tag word; emit the full tag+payload store to g(SB). Both-wrong pair:
cstage silently DROPS the store entirely (filed task #41) — rows pin
ww-runtime-correct and the documented cstage residual. Review item #32.
The probe accepted any inner ident, hijacking same-leaf locals as a
module qualifier; gate on the untyped(module) inner only (mirror the
cstage twin). Review item #20; dual-stage rows red-proven.
The global tagged ident operand read saved BP instead of the global:
'is' compared garbage as the tag; 'as' never had a payload. Route the
load through the g(SB) base — tag at +0, payload at +8, cap at +16 for
str (one mechanism, both consumers). The 'is' half aligns ww UP
(cs==ww pinned); the 'as' half is a both-wrong pair — cstage spills an
uninitialized payload register (its N_TYPEASSERT assumes cgexpr filled
AX/DX/CX; filed as task #46), so its rows assert ww-runtime-correct
with the cs divergence documented until #46 lands. Review item #18.
Slicing a module-global str with default hi emitted nothing for the
bound; load the len word from g(SB)+8 (mirror the cstage twin).
Review item #47; dual-stage rows red-proven.
A slice-typed field of a module-global struct stored only the str-form
words; widen the arm to the full slice header via the g(SB) base
(mirror cgen.c sibling arm). Review item #35; dual-stage rows red-proven.
wwstage computed tagged sizes/tags off the raw variant list — size()
folded wrong constants (size((*u8|void)) 16 vs 8, (i32|never) 16 vs 4)
and duplicate variants got divergent tag numbering vs cstage, while
ww's own cgen layout folded nullable but its size() didn't. Make
tinfofornode's N_TTAGGED arm the normalization SSoT mirroring cstage
resolve_type (check.c:801-882): never-drop, duplicate dedup via
structural typeeq, single-variant collapse, nullable fold on the
normalized pair; astsize/astalign delegate, and voidvariantindex reads
the normalized ti.params (cgen.c:900-911) so construct/match/void tag
readers agree. Corpus-neutral (zero-move on all combineds);
989_tagnorm_run pins the folds dual-stage, red-proven. Review items
#1/#3; residual #45 filed (AST-keyed nullable gate at global emit).
nodefnptr matched bare idents by NAME against the fn table, so a
global var colliding with a fn leaf classified as a fn pointer —
wwstage silently built what cstage rejects at link (review finding
#14). Key on the stamped type; the #124 &mod.fn arm is preserved.
989_fnptrcollide_run pins both stages reject (red 1/2 pre-fix:
wwstage built rc=7).
cgindex's tagged-element classification whitelisted base node kinds;
call- and slice-based tagged elements fell off the list and dropped
the payload words (review finding #23). Classify by the stamped
element type. 989_taggedidx_run pins cs==ww (red 2/6 pre-fix).
A bare module-global unsigned operand got signed IDIV/SAR/Jcc — the
predicate's ident arm only consulted the local table, so globals fell
through to signed (review finding #25). Read the stamp for the global
arm; corpus emission is unmoved (no bootstrap code div/shift/cmps a
bare unsigned global). 989_gunsigned_run pins cs==ww (red 3/8
pre-fix).
for-range destructure of an element with a slice- or tuple-typed field
strode by the default 8 (paramfieldsize had no N_TSLICE/N_TTUPLE arms),
silently reading the wrong words (review finding #43; live repro
cs=42 vs ww=8). Add the arms routed through tinfo per rule 13.
989_tupfieldsize_run pins cs==ww (red 1/2 pre-fix); rows assert
convergence, not absolutes — cstage's own single-word destructure-load
bug is filed as task #40. The N_TARRAY arm is deferred (task #39,
rule-7 comment at the fall-through).
cgindex read one word for a str/slice ELEMENT of a chained index
(xs[i][j], f().s[i]) — the element-kind gate keyed on node shape and
missed non-simple bases, dropping the 24B/16B header load (review
finding #22). Key on the element-type stamp; 989_chainidx_run pins
cs==ww (red 3/8 pre-fix).
A slice/str ELEMENT of an indexed expression passed as a call-arg
pushed one word instead of the 24B/16B header — the predicates had no
N_INDEX arm, so element-typed args fell to the scalar path (review
findings #45/#46). Read the element-type stamp; dual-stage rows in
989_idxarg_run pin cs==ww (red 2/8 on pre-fix binaries).
wwstage's desugarcallargs ran no general per-arg typecheck (only the
narrow #258 array-to-slice arm): any mistyped scalar call-arg silently
miscompiled (int read as a 24B slice header; the -T face was a user
const __wwtests building a garbage test binary). Route every call-arg
through the predicate union isassignable()||assignableaddrfn(),
mirroring cstage type_assignable||assignable_addrfn and the check.c:1869
diagnostic. Confident scalar/aggregate and aggregate/aggregate
kind-mismatch rejects live in shared isassignable; the concrete-to-
tagged arm is shape-matched-lenient via tagshape() (AST mirror of cgen
taggedvariantindext) so genuine variant members keep flowing while
shape-mismatched aggregates reject. Reserve __wwtests under -T in both
stages (mirror the main reservation, check.c:2996). New table-driven
989_callarg_typecheck, 31 fixtures, reject rows proven red on pre-fix
binaries.
Deferred, filed, site-commented: the assign seam rides #178->#36
(typeeqast cannot compare variadic/module-qualified fn sigs); the
same-coarse-shape same-leaf nominal collision over-accept rides #37
(#10/#66 — the distinguishing module is absent from the AST surface
isassignable operates on).
`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)
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)
Lone -I/-L/-l/-o now error "ww <cmd>: -X needs an argument" (rc2),
matching selfhost main.ww. do_test rewritten to an -I-only loop that
loud-rejects -l/-L/-o/unknown instead of silently swallowing them.
949_driver_flagargs: table-driven, 10 rows, each run against both
twins asserting identical rc+stderr. (#15-team)
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.
The 990-997/950/989_lib_byteid gates are pure functions of (stage
binaries, tracked sources, fixtures, harness, Makefile) and eat 87% of
full-test wall (measured, .ai/testperf-baseline.md). test-commit hashes
exactly those inputs; a match with the last green full run reports the
gates as 'cached' (counted separately, never silently) and runs
everything else - 25s vs 212s on a hit, full run on any miss. A red
run never records the key; make test itself is never cached and stays
the pre-push bar (rule 14 updated). Key gap caught in review:
examples/ feeds 996_dyn_ww - included.
Both driver twins: an import that neither locates as a file nor is
satisfied by an inline 'package <name>' declaration in the unit is now
fatal "ww: cannot find package <name>" (was a silent skip that masked
dead imports and typos). The inline scan is a new every-line helper on
the uncapped comment-skip core - peekpackage stops at the first decl,
and single-file multi-package fixtures declare several. 949 pins both
branches (miss->fatal, inline->build+run); 993 adds ww_ww parity.
check-(c): a package importing itself (any spelling) is a hard error,
mirroring Go. Predicate is leaf==owner at the N_USE/installdecl seam —
sound only after the PREP commits (dotted-test renames, package-less
boundary directive). Identical wording both stages; diagnostics-only,
byte-id-neutral. 948 pins the reject in both compilers; 708's
pos_selfimp (which pinned the abolished self-import skip) converts to
neg_selfimp + new pos_crossmod preserving the param-shadow tolerance
the case existed for. Checks (a) unused and (b)/(d) name-membership
stay deferred to the multi-package arc: imports are filename-keyed
pulls, so those need import->file provenance this compiler lacks.
@test was parsed then dropped (no consumer); `ww test` needed a hand-written
main listing each test by hand, so adding a @test and forgetting the call
silently skipped it. -T makes the checker collect @test N_FNDECLs in source
order, loud-reject a user main, and append a synthetic
`export fn main() i32 { t0(); ...; return 0; }` at the install->body-check seam;
the existing cgfn emits it (cgen untouched) -> byte-identical by construction.
Mirrors harec's checker-side is_test placement.
Plan-9-lean reduction (user-sanctioned, reinstatable post-CSP): sequential,
abort/nonzero=fail; no setjmp isolation, no fnmatch filter, no file:line.
910/997 rewired from a regex scanner to driving `w6c -T` directly (thin trusted
drivers; the @test content stays ww), with a cross-stage byte-id assert on the
-T output. attest_userman/attest_badsig pin the user-main and bad-signature
rejects.
wwstage's tagged→tagged subset-assign arm accepted all (lenient escape), silently miscompiling implicit wide→narrow: a (int|bool|str) holding a str, assigned to (int|bool), ran the int arm and read the str pointer as int. cstage rejects loud; this escape was the lone divergence.
Replace the escape with cstage's subset walk (src ⊆ dst): a src variant is covered iff typeeqast matches (structural — []u8/nested/primitives) OR both are N_TNAME with equal leaf names. The leaf bridge covers cross-module forwards where a callee's bare inline-union variant (utf8's `done`) meets the consumer's qualified `utf8.done` — raw typeeqast can't, and the escape was masking it for every forward. Mirrors casecovers (check.ww:4136). cstage untouched (align-up); spread-bearing unions keep the escape (#199b orthogonal).
Honest floor: leaf-only defers true module identity to #10 — a callee's defining module for an inline-union return is unrecoverable at check-time (the call node is gone; #211 fnretlookupmod is cgen-only). Retained divergence = cross-module same-leaf-collision over-accept (absent from bootstrap), documented at the site and filed as #10-A7 / census cat-A.
Test 989_tagged_subset_reject is table-driven with composition-discriminating rows: []u8 subset (typeeqast-only), bare↔qualified xmod forward (leaf-only), xmod named genuine-absence (reject). 352 green; w6c unchanged, w6c_ww 4b4496b8→4b316f01.
typeeqast's N_TNAME arm compared variant types by SURFACE string
(streq(aa.str, bb.str)), so a cross-module type referenced bare
(oserror) vs qualified (os.oserror) mis-compared unequal -> A6:
concrete->tagged silently mis-identified the variant. Resolve each
name to its canonical type sym before comparing: keep the streq
fast-path, else aliassym(c,aa)==aliassym(c,bb) (ww's existing resolver
maps bare [#53] and qualified [#51] to the same SK_TYPE sym). AST
analog of cstage type.c:278 TY_NAMED a==b / harec types.c:579
ident_equal -- NO interning. `c` threaded into 21 typeeqast sites.
B-full Layer 1: closes A6/#14; Layer 2 (A7 tagged->tagged subset
reject) stays deferred to the #199b flatten arc. Byte-id-neutral;
graduates shlex #59.15 (989 M_WWREJECT->M_ID). test/wcc/839 pins
both-stage symmetry (discriminating teeth = 989 #59.15, per 839 doc).
A module-global tagged init via cast (`let g: u = true: u;`) silently
emitted tag=0; cstage louds. Route emittaggeddata through flatvariantidx
(the cstage cg_tag_for_variant twin) so an unsupported variant louds while
valid str/slice cast-inits still accept+run. test/wcc/838.
A same-type tagged cast return (`return x: u`, u == fnret) emitted tag=0
(a 2nd-variant value returned the wrong payload). Route it through the
widener (needswiden when N_CAST && ru1==fu1, beside S1's ru1!=fu1) which
peels the identity cast internally -> correct; same-type non-cast stays on
the passthrough. Byte-id-neutral. test/wcc/837.
isassignable's untyped-float (A4) and nil (A5) arms over-accepted a source
into a tagged dst where no variant accepts it -> silent over-accept then
tag-0 miscompile; cstage louds. Reject (cerr + c.errs+=1). Part of the
isassignable tagged-dst over-accept class (#23 fixed arm3; A6/A7 deferred
to the B-full nominal arc). test/wcc/836.
isassignable's untyped-int arm fell through *confident=false/return true,
silently accepting an untyped int into a union whose variant is itself a
nested (non-flattened) union; ww emitted tag=0 (wrong arm) where cstage
louds. Reject unless a direct variant is numeric-or-enum (N_TENUM accept
mirrors cstage type_isnum). Faithful flatten+rebox deferred (nominal
identity, post-CSP). test/wcc/835 (new) + Makefile.
An inferred-type array global -- let xs = [1,2,3]; -- hard-failed wwstage
with 'asserttyped: int' exit 1, while cstage compiled+ran it. The
array-twin of the inferred-global family (#135 inferred-float, #150-B
inferred-Sym-repoint).
exprtype's N_ARRLIT arm synthesizes the array type for an unannotated
literal but left two synthesized child nodes unstamped: the count literal
(asserttyped trips on it -> the loud failure) and the element TNAME (cgen
then drops a non-scalar element's header load -> the silent miscompile
that merely accepting on alone would introduce: an inferred str-array's
xs[1].len read 24 not 3). Both are now stamped at the synthesis site:
cn.type_ (asserttyped facet) and elt.type_ (cgen facet). Inferred int /
u8 / str / struct / multi-dim array globals + locals + args now compile
byte-identically to the explicit-typed form (== cstage).
cstage unchanged (w6c md5 unchanged); selfhost has no inferred array
globals so byte-id 990-997 8/8, no lib pin flips. test/wcc/833.
An overlong array literal as a tuple element -- let t: ([2]int, i32) =
([1,2,3], 5) -- was silently accepted by wwstage; cstage loud-rejects it.
The #12+#106 over-fill coverage wired checkarrlitfits for direct-array,
slice and alias lhs positions but not the tuple-element position.
wwstage-only checker, reject-align: checkletassign gains an N_TTUPLE arm
that walks the lhs element types (llhs.list) lockstep with the rhs values
(n.rhs.list), calling the existing alias-aware checkarrlitfits per array
element (no-ops scalars, recurses nested arrays). cstage unchanged (w6c
md5 unchanged); reject-only, 990-997 8/8, no lib pin flips. test/wcc/832.
Two sibling tuple-element positions stay open (filed, not folded -- they
are reject-aligns on invalid programs, no selfhost byte-id impact): #25
tuple-RETURN overlong, #26 nested tuple-in-tuple.
Indexing a non-ident pointer-yielding base -- a direct cast
((&a):*[4]u32)[i], a call result mk(&a)[i], a slice, a type-assertion --
used wwstage's default 8-byte element stride/load instead of the real
element type's, reading garbage (cast-base i32 index: cs=30, ww=0). The
cgindex esz derivation gated on a whitelist of base node-kinds (DOT /
UN-deref / INDEX); an N_CAST/N_CALL/N_SLICE/N_TYPEASSERT base matched none.
Rather than extend the whitelist (whack-a-mole), this mirrors cstage's
uniform idx_eff read: N_INDEX keeps its own arm (chained-index byte-id
preserved), and every other non-ident base now derives esz/stride/load-
width/signedness from the stamped n.type_ -- closing the class by
construction (base set ident/dot/un/index/cast/call/slice/typeassert).
cstage was already correct (uniform); w6c md5 unchanged. byte-id 990-997
8/8, no lib pin flips. test/wcc/830 (9 base shapes, byte-id per width,
signed + unsigned). Folds the N_CALL sibling #22.
A let-global slice or str passed by value as an argument was silently
field-dropped by wwstage: pushargsrev's global branch had only the #150-A
struct arm, and wwstage's nodeisslice/nodeisstr are local-keyed (false for
a global), so a global slice/str ident fell to the scalar single-PUSHQ,
pushing one of the three header words {ptr,len,cap} -> the callee read
garbage for .len/.cap. cstage was already correct.
wwstage-only, caller-side only (slice/str params already received
correctly). A global slice/str arm in pushargsrev, type-keyed on
tichase(arg.type_).kind, with two arms byte-matching cstage's two distinct
sequences -- str via cgslicehdr (CX-base, cgen.c:1866), slice per-word
(BX-base, cgen.c:9124). Gate is isletvar-only (a def has no name(SB)
holder; cstage const-folds it -- def-str/slice-by-value is the residual
task #21). cstage unchanged (w6c md5 unchanged); byte-id 990-997 8/8,
no lib pin flips. test/wcc/829 table-driven, byte-id per type.
An overlong array literal (more initializers than the declared length) is
invalid -- cstage loud-rejects it everywhere -- but wwstage silently
accepted (and truncated) it in several positions; #9 wired only the decl
position. This folds the remaining three (one class: checkarrlitfits
over-fill coverage), all wwstage-only reject-align:
- #12a return fn f() [2]int = [1,2,3] -- silently accepted.
- #12b call-arg g([1,2,3]) -- louded only late via cgen #271.
- #106 alias type A=[2]int; let g: A = [1,2,3] -- silently truncated;
checkarrlitfits bailed on the N_TNAME alias without chasing.
Four inserts in check.ww: an alias-chase (resolvealias) at the top of
checkarrlitfits (makes all callers alias-aware), the over-fill check wired
into checkretassign (hoisted above the isassignable short-circuit) and
desugarcallargs, and the alias-let-global guard made alias-aware. cstage
unchanged (w6c md5 unchanged); reject-only, so no asm moves -- 990-997 8/8,
no lib byte-id pin flips. A 5th position (tuple-element overlong) is a
separate pre-existing hole, filed (#20). test/wcc/828 table-driven.
wwstage compiled str ==/!= as a single CMPQ on the eager-eval'd ptr word
(len ignored), so two distinct-pointer equal-content strings compared
unequal. cstage was already correct (CALLs rt_streq, the #154 cbinop fix).
The wwstage cgbin had no str-awareness -- every comparison fell to the
generic CMPQ tail; the #154 fix was never mirrored.
wwstage-only: a cgstreqpush helper + a str ==/!= branch at the top of
cgbin (before the generic eval collapses the header), byte-matching cstage
cbinop:4564-4623 -- push rhs/lhs (len,ptr), POPQ DI/SI/DX/CX, CALL
rt_streq, XORQ $1 for !=. Gated on typeisstr (= cstage node_isstr, which
also catches module-global str idents). cstage cgen unchanged (w6c md5
unchanged). The str== .s is byte-identical cs==ww for local, global,
aliased, chained, and condition operands.
Graduates 3 lib byte-id pins (test/wcc/989_lib_byteid #59.1 asciitest,
#59.11 toktest, #59.12 asttest) M_DIVERGE->M_ID -- they used == on str and
were pinned divergent because of this bug; now byte-identical. byte-id
990-997 8/8. test/wcc/827 table-driven.