From 4b118fa8f8ae8b931c1cd039fdb88b5ec8a823f3 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 6 Jun 2026 05:51:05 +0900 Subject: [PATCH] =?UTF-8?q?cgen:=20B7=20emitter=20elem=20chases=20+=20tool?= =?UTF-8?q?s/peellint=20gate=20=E2=80=94=20#5=20alias-arc=20cs=20side=20cl?= =?UTF-8?q?osed=20by=20construction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last four raw `->under` reads outside the whitelist were the static-DATA emitters' ELEMENT-type single peels (the outer type already chased): emit_array_lit_bytes:14356, emit_strarray_data:14574, emit_slice_data:14788, let_pre_intern:15088 -> type_chase_named. :15088 is the :14574 row's label-order leg and must flip in the same commit or _S_ labels intern in emit order, not decl order (the in-tree comment at the site); the strarr row's byte-id is the coupling proof. Behavior moves (ken B7 first-position oracle + impl pre-state, all pre-observed at 05f7af7): - [N]alias-struct + [N]alias-str globals graduate cs link-ERR ("undefined reference") -> 0/0 BYTE-ID (cs emits ww's DATAW). - zero-consumer latent silence closed: a never-referenced 2-level-elem-alias global silently lacked DATA (no reference, no link error); now emits, pinned by the byte-id cell. - []alias-str diagnostic routing: the alias escaped the 3-way slice-of-{str,slice,tagged} fatal onto the downstream "not a foldable constant" text — now the intended 3-way text (== control). - []alias-tagged DESIGNED NARROWING: the alias dodged the 3-way fatal ENTIRELY — cs silently accepted + RAN WRONG for reachable consumer shapes (review-verified at base: a len+payload-read probe exits 1; the len-only row was luck-correct). Now loud with the 3-way text; widen what the gate SEES, never what it ACCEPTS (B6-c2 precedent). - kb7_slc/slc0 scalar legs byte-NEUTRAL (the synthesized-array choke-point already handled them); full kb corpus sweep: movers are exactly the two graduation shapes, nothing else. tools/peellint (sizelint clone, dep of test/test-unit): character-scan strips comments and string/char literals, then matches the under-token accessor-spelling-wide — `->under`/`.under` in C (deref-dot is the same peel), `.under` in ww, optional whitespace after the operator, and the line-split continuation (operator at EOL, `under` next line). Scope cmd/wcc + cmd/w6c + selfhost/cmd/wcc + lib/ww (lib/ww/typ.ww ruled IN — it is type.c's ww mirror, the accessor layer itself); `peel-ok`/`peellint-ok` annotations exempt a 10-line window. Green at this tip = zero unwhitelisted raw peels survive; the gate lands in the commit that deletes the last raw read (the-funnel-completing-commit- carries-the-gate; sizelint precedent). Whitelist, 27 entries: cmd/wcc/type.c :78 :141 construction, :162 chase body, :180 :193 :214 recursive chase cmd/wcc/check.c :102 :2572 resolve-state probes, :2586 construction cmd/w6c/cgen.c :731 probe-cleared scan peel (B5-c1), :813/:814 :834/:835 peel-ok #218 variant-match lib/ww/typ.ww :316 construction, :374 :385 :410 :437 :447 :463 :475 :488 :514 recursive chase selfhost/cmd/wcc/cgenutil.ww :1302 chase body (tichase), :2759 probe-cleared peel selfhost/cmd/wcc/check.ww :1815 construction (peellint-ok) Negative validation wired into 944_peellint_gate (B4 precedent): re-introduced raw peel (C and ww spellings) REDS the lint; corrupted annotation (peel-okk-…, token-bounded matcher) REDS the lint; the check.ww:3683 "io.underread" prose, a code read of a longer field, and comment-quoted tokens are pinned green regression rows; real tree must lint clean. 944_alias_emit_b7_run pins all four emit paths table-driven (14 rows / 36 checks) incl. ken's ww observation cells (ww checker rejects slice-literal globals, "let: not assignable" — unmoved; plain []str louds at ww's own emitslicedata 3-way, pinned by the shared needle). REVIEW AMENDMENT (reviewer-B7, fix-what-you-find): the frozen tip's regex matcher passed five compiling evasion spellings green — `t -> under` spacing, `t->`/EOL + `under` next-line (both stages; ww parses `t.`/EOL too), C deref-dot `(*t).under`, ww `t. under`, and a string literal containing a block-comment opener that blinded the regex comment-strip for the rest of the file. The matcher is now a character scan (comments + string/char literals stripped before matching) with the widened token rule above; all six spellings are pinned RED rows in 944_peellint_gate (checks 10 -> 16). The 10-line annotation window stays as designed (a peel within an annotation's window is exempt by construction — the window IS the exemption mechanism). Lint + test bytes only; zero compiler-source bytes moved in review. What this does NOT close, said out loud (f2-ruling): a consumer that never spells `under` at all — a switch on t->kind that simply never peels — has no token for the lint to see. The accessor+lint closes the WRONG-PEEL class (single-peel where chase was needed) by construction; the NO-PEEL class is closed only at sites where classification routes through the internalized chasing helpers, and contained elsewhere by the acceptance-commit-carries-tripwires doctrine, which stays standing for every future acceptance widening. The gate does not make alias bugs impossible; it makes the four-times-burned shape unwritable. Rule-11 note: forced fuse — the four conversions ARE the last raw-read deletions; peellint cannot be green one commit earlier (consumer-graph -forces-the-fuse precedent, #61). Invariants: cs asm byte-NEUTRAL on the whole bootstrap corpus (five mains + smoke, base-input pre==post); five mains cs==ww byte-id at tip; _ww binary quartet bit-identical to the W2 baseline (ww changes are comment-only annotation bytes — codegen-inert, proven by the md5 hold); w6c_ww+wwdump main.combined.ww regen'd via make, idempotent; 989 lib ratchet zero flips (31 byte-id / 9 pinned-divergent / 3 pinned-wwreject across 43 units); sizelint 0; peellint 0; make test-unit "all 294 tests passed" (292 + the two new suites). --- Makefile | 20 +- cmd/w6c/cgen.c | 9 +- cmd/wcc/check.c | 4 +- cmd/wcc/type.c | 15 +- lib/ww/typ.ww | 11 +- selfhost/cmd/w6c/main.combined.ww | 12 +- selfhost/cmd/wcc/cgenutil.ww | 1 + selfhost/cmd/wwdump/main.combined.ww | 12 +- test/wcc/944_alias_emit_b7_run.c | 436 +++++++++++++++++++++++++++ test/wcc/944_peellint_gate.c | 202 +++++++++++++ tools/peellint | 139 +++++++++ 11 files changed, 843 insertions(+), 18 deletions(-) create mode 100644 test/wcc/944_alias_emit_b7_run.c create mode 100644 test/wcc/944_peellint_gate.c create mode 100755 tools/peellint diff --git a/Makefile b/Makefile index cb3af3b4..3055cf65 100644 --- a/Makefile +++ b/Makefile @@ -310,6 +310,8 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_alias_global_decl_run \ $(BIN)/test_alias_cgen_b5_run \ $(BIN)/test_alias_cgen_b6_run \ + $(BIN)/test_alias_emit_b7_run \ + $(BIN)/test_peellint_gate \ $(BIN)/test_tuple_nary_destructure_run \ $(BIN)/test_rvalue_tuple_destructure_run \ $(BIN)/test_overcap_tuple_field_store_run \ @@ -1502,6 +1504,15 @@ $(BIN)/test_alias_cgen_b6_run: test/wcc/944_alias_cgen_b6_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_alias_emit_b7_run: test/wcc/944_alias_emit_b7_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_peellint_gate: test/wcc/944_peellint_gate.c tools/peellint | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_tuple_nary_destructure_run: test/wcc/945_tuple_nary_destructure_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ @@ -2194,10 +2205,13 @@ $(BIN)/test_structret_run: test/wcc/946_structret_run.c $(BIN)/ww \ sizelint: @sh tools/sizelint -test: all sizelint $(TESTS) +peellint: + @sh tools/peellint + +test: all sizelint peellint $(TESTS) @WW=$(BIN)/ww BIN=$(BIN) sh test/run -test-unit: all sizelint $(TESTS) +test-unit: all sizelint peellint $(TESTS) @WW=$(BIN)/ww BIN=$(BIN) UNIT=1 sh test/run # ---- smoke: inner-loop cross-stage byte-id check on a SMALL input ------ @@ -2367,4 +2381,4 @@ nocc: @echo @echo "NOCC OK: $(STAGE0)/* reproduces itself from source. cc not invoked." -.PHONY: all cstage wwstage test test-unit smoke sizelint install clean bootstrap nocc bootstrap-snapshot +.PHONY: all cstage wwstage test test-unit smoke sizelint peellint install clean bootstrap nocc bootstrap-snapshot diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 7a6ad59c..77e365e4 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -14353,7 +14353,7 @@ emit_array_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, int emit_phase) Type *u = type_chase_named(t); if (u == NULL || u->kind != TY_ARRAY) return 0; Type *etype = u->sub; - Type *eu = (etype && etype->kind == TY_NAMED) ? etype->under : etype; + Type *eu = type_chase_named(etype); int esz = etype ? (int)etype->size : 1; int alen = (int)u->alen; @@ -14571,7 +14571,7 @@ emit_strarray_data(FILE *out, Cg *c, const char *directive, Type *u = type_chase_named(t); if (u == NULL || u->kind != TY_ARRAY) return 0; Type *etype = u->sub; - Type *eu = (etype && etype->kind == TY_NAMED) ? etype->under : etype; + Type *eu = type_chase_named(etype); if (eu == NULL || eu->kind != TY_STR) return 0; if (strcmp(directive, "DATAW") != 0) return 0; int esz = (int)etype->size; @@ -14785,7 +14785,7 @@ emit_slice_data(FILE *out, Cg *c, const char *directive, const char *name, "writable `let` (DATAR holder must be DATAW, w6a " "asm.c:362); read-only `def` unsupported (#10, rule 7)"); Type *etype = u->sub; - Type *eu = (etype && etype->kind == TY_NAMED) ? etype->under : etype; + Type *eu = type_chase_named(etype); if (eu && (eu->kind == TY_STR || eu->kind == TY_SLICE || eu->kind == TY_TAGGED)) fatal("emit_slice_data: slice-of-{str,slice,tagged} literal " @@ -15084,8 +15084,7 @@ let_pre_intern(Cg *c, Node *file) Type *u = type_chase_named(d->type); if (u != NULL && u->kind == TY_ARRAY && r != NULL && r->kind == N_ARRLIT) { - Type *eu = (u->sub && u->sub->kind == TY_NAMED) - ? u->sub->under : u->sub; + Type *eu = type_chase_named(u->sub); if (eu != NULL && eu->kind == TY_STR) { int alen = (int)u->alen; int cnt = 0; diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 4885a0b9..f3c53ea8 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -98,6 +98,7 @@ resolve_typename(Checker *c, Node *n) * handing its type out; no consumer may ever see the size-0 * placeholder. Mirrors wwstage's demand-driven tinfofornode, the * measured order-independent side. */ + /* peel-ok: under==NULL probes resolve state, not a dealias */ if (s->type && s->type->kind == TY_NAMED && s->type->under == NULL && s->decl && s->decl->kind == N_TYPEDECL) resolve_typedecl(c, s->decl); @@ -2567,6 +2568,7 @@ static void resolve_typedecl(Checker *c, Node *d) { Type *t = d->type; + /* peel-ok: under!=NULL probes resolve state, not a dealias */ if (t == NULL || t->under != NULL || t->resolving) return; t->resolving = 1; const char *save = c->cur_mod; @@ -2581,7 +2583,7 @@ resolve_typedecl(Checker *c, Node *d) if (circular_named(c, under, d->pos)) under = ty_err; t->resolving = 0; - t->under = under; + t->under = under; /* peel-ok: construction */ if (under) { t->size = under->size; t->align = under->align; diff --git a/cmd/wcc/type.c b/cmd/wcc/type.c index 570d5c54..a1d48bba 100644 --- a/cmd/wcc/type.c +++ b/cmd/wcc/type.c @@ -75,7 +75,7 @@ typesinit(Arena *a) * the same way a user-declared alias would. */ ty_nomem = newtype(a, TY_NAMED); ty_nomem->name = "nomem"; - ty_nomem->under = ty_void; + ty_nomem->under = ty_void; /* peel-ok: construction */ ty_nomem->size = ty_void->size; ty_nomem->align = ty_void->align; ty_nomem->iserror = 1; @@ -138,7 +138,7 @@ type_named(Arena *a, const char *name, Type *under) { Type *t = newtype(a, TY_NAMED); t->name = name; - t->under = under; + t->under = under; /* peel-ok: construction */ if (under) { t->size = under->size; t->align = under->align; @@ -159,7 +159,7 @@ type_named(Arena *a, const char *name, Type *under) Type * type_chase_named(Type *t) { - while (t && t->kind == TY_NAMED) t = t->under; + while (t && t->kind == TY_NAMED) t = t->under; /* peel-ok: chase body */ return t; } @@ -176,7 +176,8 @@ type_isint(Type *t) case TY_UNTYPED_RUNE: return 1; case TY_ENUM: return type_isint(t->sub); - case TY_NAMED: return type_isint(t->under); + case TY_NAMED: /* peel-ok: recursive chase */ + return type_isint(t->under); default: return 0; } } @@ -188,7 +189,8 @@ type_isfloat(Type *t) switch (t->kind) { case TY_F32: case TY_F64: case TY_UNTYPED_FLOAT: return 1; - case TY_NAMED: return type_isfloat(t->under); + case TY_NAMED: /* peel-ok: recursive chase */ + return type_isfloat(t->under); default: return 0; } } @@ -208,7 +210,8 @@ type_isunsigned(Type *t) case TY_UINT: case TY_UINTPTR: case TY_SIZE: case TY_RUNE: return 1; - case TY_NAMED: return type_isunsigned(t->under); + case TY_NAMED: /* peel-ok: recursive chase */ + return type_isunsigned(t->under); case TY_ENUM: return type_isunsigned(t->sub); default: return 0; } diff --git a/lib/ww/typ.ww b/lib/ww/typ.ww index 8bc04909..98494ddb 100644 --- a/lib/ww/typ.ww +++ b/lib/ww/typ.ww @@ -313,7 +313,7 @@ export fn typechan(sub: *tinfo) *tinfo = { export fn typenamed(name: str, under: *tinfo) *tinfo = { let t: *tinfo = newtype(tykind.TY_NAMED); t.name = name; - t.under = under; + t.under = under; // peel-ok: construction if (under != nil) { t.size = under.size; t.align = under.align; @@ -370,6 +370,7 @@ export fn typeisint(t: *tinfo) bool = { if (k == tykind.TY_UNTYPED_INT) { return true; }; if (k == tykind.TY_UNTYPED_RUNE) { return true; }; if (k == tykind.TY_ENUM) { return typeisint(t.sub); }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisint(t.under); }; return false; }; @@ -380,6 +381,7 @@ export fn typeisfloat(t: *tinfo) bool = { if (k == tykind.TY_F32) { return true; }; if (k == tykind.TY_F64) { return true; }; if (k == tykind.TY_UNTYPED_FLOAT) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisfloat(t.under); }; return false; }; @@ -404,6 +406,7 @@ export fn typeisunsigned(t: *tinfo) bool = { if (k == tykind.TY_UINTPTR) { return true; }; if (k == tykind.TY_SIZE) { return true; }; if (k == tykind.TY_RUNE){ return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisunsigned(t.under); }; if (k == tykind.TY_ENUM) { return typeisunsigned(t.sub); }; return false; @@ -430,6 +433,7 @@ export fn typeisstr(t: *tinfo) bool = { let k: tykind = t.kind; if (k == tykind.TY_STR) { return true; }; if (k == tykind.TY_UNTYPED_STR) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisstr(t.under); }; return false; }; @@ -439,6 +443,7 @@ export fn typeisslice(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_SLICE) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisslice(t.under); }; return false; }; @@ -454,6 +459,7 @@ export fn typeistagged(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_TAGGED) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeistagged(t.under); }; return false; }; @@ -465,6 +471,7 @@ export fn typeisf32(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_F32) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisf32(t.under); }; return false; }; @@ -477,6 +484,7 @@ export fn typeisnullable(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_TAGGED) { return t.nullable != 0; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisnullable(t.under); }; return false; }; @@ -502,6 +510,7 @@ export fn typeis8byteprim(t: *tinfo) bool = { if (k == tykind.TY_STR) { return false; }; if (k == tykind.TY_STRUCT) { return false; }; if (k == tykind.TY_ARRAY) { return t.size == 8u64; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeis8byteprim(t.under); }; // Remaining: primitives (i8/u8/.../i64/u64/bool/rune/f32/f64/ // int/uint/uintptr) and TY_VOID. All slot-pad to 8 and zero-init diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 571601cd..7039e92f 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -9699,7 +9699,7 @@ export fn typechan(sub: *tinfo) *tinfo = { export fn typenamed(name: str, under: *tinfo) *tinfo = { let t: *tinfo = newtype(tykind.TY_NAMED); t.name = name; - t.under = under; + t.under = under; // peel-ok: construction if (under != nil) { t.size = under.size; t.align = under.align; @@ -9756,6 +9756,7 @@ export fn typeisint(t: *tinfo) bool = { if (k == tykind.TY_UNTYPED_INT) { return true; }; if (k == tykind.TY_UNTYPED_RUNE) { return true; }; if (k == tykind.TY_ENUM) { return typeisint(t.sub); }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisint(t.under); }; return false; }; @@ -9766,6 +9767,7 @@ export fn typeisfloat(t: *tinfo) bool = { if (k == tykind.TY_F32) { return true; }; if (k == tykind.TY_F64) { return true; }; if (k == tykind.TY_UNTYPED_FLOAT) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisfloat(t.under); }; return false; }; @@ -9790,6 +9792,7 @@ export fn typeisunsigned(t: *tinfo) bool = { if (k == tykind.TY_UINTPTR) { return true; }; if (k == tykind.TY_SIZE) { return true; }; if (k == tykind.TY_RUNE){ return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisunsigned(t.under); }; if (k == tykind.TY_ENUM) { return typeisunsigned(t.sub); }; return false; @@ -9816,6 +9819,7 @@ export fn typeisstr(t: *tinfo) bool = { let k: tykind = t.kind; if (k == tykind.TY_STR) { return true; }; if (k == tykind.TY_UNTYPED_STR) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisstr(t.under); }; return false; }; @@ -9825,6 +9829,7 @@ export fn typeisslice(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_SLICE) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisslice(t.under); }; return false; }; @@ -9840,6 +9845,7 @@ export fn typeistagged(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_TAGGED) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeistagged(t.under); }; return false; }; @@ -9851,6 +9857,7 @@ export fn typeisf32(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_F32) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisf32(t.under); }; return false; }; @@ -9863,6 +9870,7 @@ export fn typeisnullable(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_TAGGED) { return t.nullable != 0; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisnullable(t.under); }; return false; }; @@ -9888,6 +9896,7 @@ export fn typeis8byteprim(t: *tinfo) bool = { if (k == tykind.TY_STR) { return false; }; if (k == tykind.TY_STRUCT) { return false; }; if (k == tykind.TY_ARRAY) { return t.size == 8u64; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeis8byteprim(t.under); }; // Remaining: primitives (i8/u8/.../i64/u64/bool/rune/f32/f64/ // int/uint/uintptr) and TY_VOID. All slot-pad to 8 and zero-init @@ -17387,6 +17396,7 @@ fn elemisarrayc(c: *cgen, t: *node) bool = { // outside it are the lint target (rob F2 ruling). fn tichase(t0: *tinfo) *tinfo = { let t: *tinfo = t0; + // peel-ok: chase body for (t != nil && t.kind == tykind.TY_NAMED) { t = t.under; }; return t; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index f7ae639f..748e3a17 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1298,6 +1298,7 @@ fn elemisarrayc(c: *cgen, t: *node) bool = { // outside it are the lint target (rob F2 ruling). fn tichase(t0: *tinfo) *tinfo = { let t: *tinfo = t0; + // peel-ok: chase body for (t != nil && t.kind == tykind.TY_NAMED) { t = t.under; }; return t; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 0e3989b1..0d11a8c8 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -9699,7 +9699,7 @@ export fn typechan(sub: *tinfo) *tinfo = { export fn typenamed(name: str, under: *tinfo) *tinfo = { let t: *tinfo = newtype(tykind.TY_NAMED); t.name = name; - t.under = under; + t.under = under; // peel-ok: construction if (under != nil) { t.size = under.size; t.align = under.align; @@ -9756,6 +9756,7 @@ export fn typeisint(t: *tinfo) bool = { if (k == tykind.TY_UNTYPED_INT) { return true; }; if (k == tykind.TY_UNTYPED_RUNE) { return true; }; if (k == tykind.TY_ENUM) { return typeisint(t.sub); }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisint(t.under); }; return false; }; @@ -9766,6 +9767,7 @@ export fn typeisfloat(t: *tinfo) bool = { if (k == tykind.TY_F32) { return true; }; if (k == tykind.TY_F64) { return true; }; if (k == tykind.TY_UNTYPED_FLOAT) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisfloat(t.under); }; return false; }; @@ -9790,6 +9792,7 @@ export fn typeisunsigned(t: *tinfo) bool = { if (k == tykind.TY_UINTPTR) { return true; }; if (k == tykind.TY_SIZE) { return true; }; if (k == tykind.TY_RUNE){ return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisunsigned(t.under); }; if (k == tykind.TY_ENUM) { return typeisunsigned(t.sub); }; return false; @@ -9816,6 +9819,7 @@ export fn typeisstr(t: *tinfo) bool = { let k: tykind = t.kind; if (k == tykind.TY_STR) { return true; }; if (k == tykind.TY_UNTYPED_STR) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisstr(t.under); }; return false; }; @@ -9825,6 +9829,7 @@ export fn typeisslice(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_SLICE) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisslice(t.under); }; return false; }; @@ -9840,6 +9845,7 @@ export fn typeistagged(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_TAGGED) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeistagged(t.under); }; return false; }; @@ -9851,6 +9857,7 @@ export fn typeisf32(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_F32) { return true; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisf32(t.under); }; return false; }; @@ -9863,6 +9870,7 @@ export fn typeisnullable(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_TAGGED) { return t.nullable != 0; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeisnullable(t.under); }; return false; }; @@ -9888,6 +9896,7 @@ export fn typeis8byteprim(t: *tinfo) bool = { if (k == tykind.TY_STR) { return false; }; if (k == tykind.TY_STRUCT) { return false; }; if (k == tykind.TY_ARRAY) { return t.size == 8u64; }; + // peel-ok: recursive chase if (k == tykind.TY_NAMED) { return typeis8byteprim(t.under); }; // Remaining: primitives (i8/u8/.../i64/u64/bool/rune/f32/f64/ // int/uint/uintptr) and TY_VOID. All slot-pad to 8 and zero-init @@ -17387,6 +17396,7 @@ fn elemisarrayc(c: *cgen, t: *node) bool = { // outside it are the lint target (rob F2 ruling). fn tichase(t0: *tinfo) *tinfo = { let t: *tinfo = t0; + // peel-ok: chase body for (t != nil && t.kind == tykind.TY_NAMED) { t = t.under; }; return t; }; diff --git a/test/wcc/944_alias_emit_b7_run.c b/test/wcc/944_alias_emit_b7_run.c new file mode 100644 index 00000000..dc2bf783 --- /dev/null +++ b/test/wcc/944_alias_emit_b7_run.c @@ -0,0 +1,436 @@ +/* + * 944_alias_emit_b7_run — #5 alias arc B7 (finale): the four cgen.c + * static-DATA emitter ELEM-type chases. All four sites already chased + * the OUTER type; the residual single peel was on the ELEMENT type + * (u->sub), so a 2-level-elem-alias global missed the TY_STRUCT / + * TY_STR / TY_TAGGED kind tests and fell off the foldable-literal + * path. cs-only: every graduation is a cs-side flip onto ww's + * already-correct emit (align cs UP); _ww binaries bit-identical + * across the commit (comment-only ww annotation bytes are + * codegen-inert). + * + * Sites (numbering at 05f7af7): emit_array_lit_bytes:14356 (struct + * arm + raw-bytes path), emit_strarray_data:14574 (str DATAW+DATAR + * rows), emit_slice_data:14788 (3-way slice-of-{str,slice,tagged} + * fatal), let_pre_intern:15088 (the :14574 row's label-order leg — + * MUST chase in the same commit or _S_ labels intern in emit order, + * not decl order; strarr_2lvl's byte-id IS the coupling proof). + * + * Row sources: ken's B7 first-position oracle (/tmp/ken_b5/src/kb7_*, + * .ai/ken-b7-oracle.md, matrix at 05f7af7) + impl pre-state probes. + * NOTE: `type el = el0` where el0 is itself a type decl is ALREADY two + * NAMED layers (NAMED(el)→NAMED(el0)→struct), so the "_1lvl" struct + * spelling graduates with the 2-level rows; only a direct alias of a + * primitive (type ms = str) is a true single layer. + * + * row | shape | cs/ww + * -------------------+--------------------------------------+------ + * sarr_2lvl | kb7_sarr [2]el 2-lvl alias struct | + * | elem: cs emitted NO DATA (eu peel | + * | missed TY_STRUCT → return 0 → caller | + * | skipped the def) → link-loud. LIVE | + * | graduation → 0/0 byte-id | 0/0 + * sarr_1lvl | [2]el, el = el0 — two NAMED layers | + * | by construction; same graduation | 0/0 + * sarr_plain_ctl | kb7_sarr0 [2]el0 control | 0/0 + * sarr_unref_2lvl | NEVER-REFERENCED 2-lvl-elem global: | + * | pre-B7 cs silently lacked DATA (no | + * | reference → no link error → latent | + * | silence, ken's caveat). Closure pin: | + * | byte-id vs ww (which always emitted) | 0/0 + * strarr_2lvl | kb7_strarr [2]ms 2-lvl alias str: | + * | eu != TY_STR → generic path can't | + * | fold strlits → link-loud. Graduation | + * | + the :15088 intern-order coupling | 0/0 + * strarr_1lvl_ctl | type ms = str — TRUE 1-level; worked | + * | pre-B7, byte-NEUTRAL regression net | 0/0 + * strarr_plain_ctl | kb7_strarr0 [2]str control | 0/0 + * scalararr_2lvl | [3]my64b raw-bytes path: scalar fold | + * | never consulted eu — worked pre-B7, | + * | byte-NEUTRAL regression net | 0/0 + * slc_2lvl | kb7_slc []my64b: cs ok (synthesized- | + * | array choke-point + mirrored esz), | + * | ww checker-rejects ALL slice-literal | + * | globals ("let: not assignable", | + * | pre-existing acceptance divergence, | + * | #66-R2/#29 kin) — ww cell MUST NOT | + * | move across B7 | 0/err + * slc_plain_ctl | kb7_slc0 []i64 — same split | 0/err + * slcstr_2lvl | kb7_slcstr []ms: pre-B7 the alias | + * | ESCAPED the 3-way fatal and hit the | + * | downstream "not a foldable constant" | + * | — loud but WRONG DIAGNOSTIC. Post: | + * | the intended 3-way text (== control) | err/err + * slcstr_plain_ctl | kb7_slcstr0 []str pins the 3-way | + * | text on the control | err/err + * slctag_2lvl | []u1 2-lvl alias tagged elem: pre-B7 | + * | the alias DODGED the 3-way fatal | + * | ENTIRELY — silent accept + run | + * | (wrong-DATA latency). DESIGNED | + * | NARROWING: post-B7 louds with the | + * | 3-way text ("widen what the gate | + * | SEES", B6-c2 fsarg2 precedent) | err/err + * slcslc_2lvl | []ms (ms = []i64): nested slice-lit | + * | rejected UPSTREAM by the cs checker | + * | ("let G init not assignable") — the | + * | TY_SLICE arm of the 3-way is not | + * | reachable via a literal; pin the | + * | checker bound (alias-independent) | err/err + * + * K_RUN rows build+run BOTH drivers (exit==want) and assert cs/ww asm + * byte-id. K_BUILDERR rows must FAIL with experr on BOTH drivers; + * experr_ww overrides the ww text when the stages loud at different + * sites. K_CSRUN_WWERR rows run on the cs driver and must FAIL with + * experr_ww on the ww driver (ken's observation cells; byte-id N/A). + * NNN<950, self-contained /tmp sources, no imports (944 precedent). + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa), cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +#define K_RUN 0 /* build+run BOTH, exits==wants, + byte-id */ +#define K_BUILDERR 1 /* build FAILS with experr on BOTH drivers; + * experr_ww overrides the ww-side text */ +#define K_CSRUN_WWERR 2 /* cs builds+runs (exit==cswant); ww must + * FAIL with experr_ww (pre-existing + * acceptance divergence cell); byte-id + * N/A — ww emits nothing */ + +struct row { const char *label; const char *src; + int cswant; int wwwant; int kind; + const char *experr; const char *experr_ww; }; + +/* errlog_has — a builderr cell must fail WITH its diagnostic; any other + * failure (parse error, crash, hang-kill) is a vacuous reject (940 + * precedent). */ +static int +errlog_has(const char *path, const char *needle) +{ + FILE *f = fopen(path, "rb"); + if (!f) return 0; + char buf[8192]; + size_t got = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + buf[got] = '\0'; + return strstr(buf, needle) != NULL; +} + +#define ERR_3WAY "slice-of-{str,slice,tagged} literal static-init unsupported" +#define ERR_WWLET "let: not assignable" + +static const struct row rows[] = { + /* ---- emit_array_lit_bytes struct arm (:14356) */ + { "sarr_2lvl", + "package main;\n" + "type el0 = struct { a: i64, b: i64 };\n" + "type el1 = el0;\n" + "type el = el1;\n" + "let G: [2]el = [el0 { a = 1, b = 2 }, el0 { a = 3, b = 4 }];\n" + "export fn main() i32 = {\n" + " if (G[0].a + G[0].b + G[1].a + G[1].b != 10) { return 1; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL, NULL }, + { "sarr_1lvl", + "package main;\n" + "type el0 = struct { a: i64, b: i64 };\n" + "type el = el0;\n" + "let G: [2]el = [el0 { a = 1, b = 2 }, el0 { a = 3, b = 4 }];\n" + "export fn main() i32 = {\n" + " if (G[0].a + G[0].b + G[1].a + G[1].b != 10) { return 1; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL, NULL }, + { "sarr_plain_ctl", + "package main;\n" + "type el0 = struct { a: i64, b: i64 };\n" + "let G: [2]el0 = [el0 { a = 1, b = 2 }, el0 { a = 3, b = 4 }];\n" + "export fn main() i32 = {\n" + " if (G[0].a + G[0].b + G[1].a + G[1].b != 10) { return 1; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL, NULL }, + /* Zero-consumer latent silence (ken's caveat): unreferenced, so + * the pre-B7 missing DATA never linked-loud — the byte-id cell + * vs ww (which always emitted the row) is the closure pin. */ + { "sarr_unref_2lvl", + "package main;\n" + "type el0 = struct { a: i64, b: i64 };\n" + "type el1 = el0;\n" + "type el = el1;\n" + "let G: [2]el = [el0 { a = 1, b = 2 }, el0 { a = 3, b = 4 }];\n" + "export fn main() i32 = {\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL, NULL }, + /* ---- emit_strarray_data (:14574) + let_pre_intern (:15088) */ + { "strarr_2lvl", + "package main;\n" + "type ms0 = str;\n" + "type ms = ms0;\n" + "let G: [2]ms = [\"aa\", \"bbb\"];\n" + "export fn main() i32 = {\n" + " if (len(G[0]) != 2) { return 1; };\n" + " if (len(G[1]) != 3) { return 2; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL, NULL }, + { "strarr_1lvl_ctl", + "package main;\n" + "type ms = str;\n" + "let G: [2]ms = [\"aa\", \"bbb\"];\n" + "export fn main() i32 = {\n" + " if (len(G[0]) != 2) { return 1; };\n" + " if (len(G[1]) != 3) { return 2; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL, NULL }, + { "strarr_plain_ctl", + "package main;\n" + "let G: [2]str = [\"aa\", \"bbb\"];\n" + "export fn main() i32 = {\n" + " if (len(G[0]) != 2) { return 1; };\n" + " if (len(G[1]) != 3) { return 2; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL, NULL }, + /* Raw-bytes scalar fold never consulted eu — byte-NEUTRAL net. */ + { "scalararr_2lvl", + "package main;\n" + "type my64 = i64;\n" + "type my64b = my64;\n" + "let G: [3]my64b = [5, 6, 7];\n" + "export fn main() i32 = {\n" + " if (G[0] + G[1] + G[2] != 18) { return 1; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL, NULL }, + /* ---- emit_slice_data (:14788) — ken's ww observation cells + * (checker-rejects every slice-literal global) MUST NOT move. */ + { "slc_2lvl", + "package main;\n" + "type my64 = i64;\n" + "type my64b = my64;\n" + "let G: []my64b = [5, 6, 7];\n" + "export fn main() i32 = {\n" + " if (G[0] + G[1] + G[2] != 18) { return 1; };\n" + " if (len(G) != 3) { return 2; };\n" + " return 0;\n" + "};\n", 0, 1, K_CSRUN_WWERR, NULL, ERR_WWLET }, + { "slc_plain_ctl", + "package main;\n" + "let G: []i64 = [5, 6, 7];\n" + "export fn main() i32 = {\n" + " if (G[0] + G[1] + G[2] != 18) { return 1; };\n" + " if (len(G) != 3) { return 2; };\n" + " return 0;\n" + "};\n", 0, 1, K_CSRUN_WWERR, NULL, ERR_WWLET }, + /* Diagnostic-routing fix: pre-B7 the 2-lvl alias escaped the + * 3-way fatal onto the downstream "not a foldable constant" + * text — loud either way, wrong message. Post: == control. */ + { "slcstr_2lvl", + "package main;\n" + "type ms0 = str;\n" + "type ms = ms0;\n" + "let G: []ms = [\"aa\", \"bbb\"];\n" + "export fn main() i32 = {\n" + " if (len(G[0]) != 2) { return 1; };\n" + " return 0;\n" + "};\n", 1, 1, K_BUILDERR, ERR_3WAY, ERR_WWLET }, + /* ww cell observed: plain []str PASSES the ww checker and louds + * at ww's own emitslicedata 3-way — the shared needle pins both + * stages' texts (cs "emit_slice_data:", ww "emitslicedata:"); + * only ALIAS slice spellings reject at the ww checker. */ + { "slcstr_plain_ctl", + "package main;\n" + "let G: []str = [\"aa\", \"bbb\"];\n" + "export fn main() i32 = {\n" + " if (len(G[0]) != 2) { return 1; };\n" + " return 0;\n" + "};\n", 1, 1, K_BUILDERR, ERR_3WAY, NULL }, + /* DESIGNED NARROWING: pre-B7 the tagged-elem alias dodged the + * 3-way fatal entirely — cs silently accepted and ran (wrong- + * DATA latency). The chase widens what the gate SEES (B6-c2 + * fsarg2 precedent); the loud stays loud for what it guards. */ + { "slctag_2lvl", + "package main;\n" + "type u0 = (void | i64);\n" + "type u1 = u0;\n" + "let G: []u1 = [1i64, 2i64];\n" + "export fn main() i32 = {\n" + " if (len(G) != 2) { return 1; };\n" + " return 0;\n" + "};\n", 1, 1, K_BUILDERR, ERR_3WAY, ERR_WWLET }, + /* Slice-of-slice literal never reaches the emitter: the cs + * checker bounds it upstream, alias-independent. Pin the bound + * so a checker widening re-opens this row deliberately. */ + { "slcslc_2lvl", + "package main;\n" + "type ms0 = []i64;\n" + "type ms = ms0;\n" + "let G: []ms = [[1, 2], [3]];\n" + "export fn main() i32 = {\n" + " if (len(G) != 2) { return 1; };\n" + " return 0;\n" + "};\n", 1, 1, K_BUILDERR, "let G init not assignable", ERR_WWLET }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i, int expect_err, + int want, const char *experr) +{ + char src[96], tmpdir[96], errf[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/ab7_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/ab7_%d_d_%d", getpid(), i); + snprintf(errf, sizeof errf, "/tmp/ab7_%d_e_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, + "cd %s && timeout 20 %s build %s >/dev/null 2>%s", + tmpdir, driver, src, errf); + int brc = runwait(cmd); + if (expect_err) { + int ok = (brc != 0) + && (experr == NULL || errlog_has(errf, experr)); + if (!ok) + fprintf(stderr, "row[%s]: %s expected loud builderr " + "\"%s\" (brc=%d)\n", r->label, driver, + experr ? experr : "", brc); + unlink(src); unlink(errf); rmdir(tmpdir); + return ok ? 0 : 1; + } + if (brc != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); unlink(errf); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[256]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); unlink(errf); rmdir(tmpdir); + if (got != want) { + fprintf(stderr, "row[%s]: %s exit %d, want %d\n", + r->label, driver, got, want); + return 1; + } + return 0; +} + +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[96], cs[96], ws[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/ab7_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/ab7_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/ab7_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + int rc = slurp_eq(cs, ws); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[2080]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[2120], wdrv[2120]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + total++; + int cs_err = rows[i].kind == K_BUILDERR; + if (run_driver(cdrv, &rows[i], i, cs_err, + rows[i].cswant, rows[i].experr) != 0) fail++; + } + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + int ww_err = rows[i].kind == K_BUILDERR + || rows[i].kind == K_CSRUN_WWERR; + const char *we = rows[i].experr_ww + ? rows[i].experr_ww : rows[i].experr; + if (run_driver(wdrv, &rows[i], i, ww_err, + rows[i].wwwant, we) != 0) fail++; + } + for (int i = 0; i < n; i++) { + if (rows[i].kind != K_RUN) + continue; + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) fail++; + } + } + + if (fail) { + fprintf(stderr, "alias_emit_b7: %d/%d checks failed\n", + fail, total); + return 1; + } + printf("alias_emit_b7: %d/%d ok\n", total, total); + return 0; +} diff --git a/test/wcc/944_peellint_gate.c b/test/wcc/944_peellint_gate.c new file mode 100644 index 00000000..88a6a01a --- /dev/null +++ b/test/wcc/944_peellint_gate.c @@ -0,0 +1,202 @@ +/* + * 944_peellint_gate — teeth for tools/peellint (#5 alias-arc B7). + * + * The lint is the enforcement half of the close-by-construction + * contract: zero raw under-token reads in scope outside the annotated + * whitelist. A gate without negative validation can rot green (B4 + * precedent), so this test pins BOTH directions: + * + * 1. real tree at HEAD lints CLEAN (the closure proof itself); + * 2. a re-introduced raw peel REDS the lint — C `->under` ternary + * and ww `.under` if-peel, the four-times-burned spellings; + * 3. a corrupted whitelist annotation REDS the lint (token-bounded + * `peel-ok` matcher: `peel-okk-…` must NOT exempt); + * 4. regression rows that must stay GREEN: the check.ww:3683 + * "io.underread" prose (token bound), a code read of a longer + * field (`s.underread`), comment-quoted `.under`/`->under` + * prose (comment strip), and the already-landed `peellint-ok` + * sibling spelling (history is not re-spelled); + * 5. review-found evasion spellings REDS (B7 review probes E1-E6, + * every one compiles): `t -> under` spacing, `t->`/EOL + + * `under` next line (both stages' split), C deref-dot + * `(*t).under`, ww `t. under`, and a string literal containing + * a block-comment OPENER token that blinded the old regex + * comment-strip for the rest of the file. + * + * Scratch trees live under /tmp and exercise the lint via its ROOT + * override (sizelint-style), so the real tree is never touched. + */ +#include +#include +#include +#include +#include +#include + +static char root[1024]; /* repo root (cwd when run via test/run) */ + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +static int +write_file(const char *path, const char *body) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(body, f); + fclose(f); + return 0; +} + +/* lint_scratch — run tools/peellint over a one-file scratch tree and + * return its exit code. relpath selects the in-scope directory. */ +static int +lint_scratch(const char *scratch, const char *relpath, const char *body) +{ + char cmd[2048], path[1400]; + snprintf(cmd, sizeof cmd, "rm -rf %s", scratch); + runwait(cmd); + snprintf(path, sizeof path, "%s/%s", scratch, relpath); + char dir[1400]; + snprintf(dir, sizeof dir, "%s", path); + char *slash = strrchr(dir, '/'); + if (slash) *slash = '\0'; + snprintf(cmd, sizeof cmd, "mkdir -p %s", dir); + if (runwait(cmd) != 0) return -1; + if (write_file(path, body) != 0) return -1; + snprintf(cmd, sizeof cmd, + "ROOT=%s sh %s/tools/peellint >/dev/null 2>&1", scratch, root); + int rc = runwait(cmd); + snprintf(cmd, sizeof cmd, "rm -rf %s", scratch); + runwait(cmd); + return rc; +} + +struct lintrow { const char *label; const char *relpath; + const char *body; int wantexit; }; + +static const struct lintrow lintrows[] = { + { "reinject_c_peel", "cmd/w6c/x.c", + "static Type *f(Type *t) {\n" + "\tType *u = (t->kind == TY_NAMED) ? t->under : t;\n" + "\treturn u;\n" + "}\n", 1 }, + { "reinject_ww_peel", "selfhost/cmd/wcc/x.ww", + "fn f(t: *tinfo) *tinfo = {\n" + "\tif (t.kind == tykind.TY_NAMED) { return t.under; };\n" + "\treturn t;\n" + "};\n", 1 }, + { "annotated_c_peel_ok", "cmd/w6c/x.c", + "static Type *f(Type *t) {\n" + "\tType *u = (t->kind == TY_NAMED) ? t->under : t; " + "/* peel-ok: probe */\n" + "\treturn u;\n" + "}\n", 0 }, + { "corrupt_annotation", "cmd/w6c/x.c", + "static Type *f(Type *t) {\n" + "\tType *u = (t->kind == TY_NAMED) ? t->under : t; " + "/* peel-okk-corrupt: probe */\n" + "\treturn u;\n" + "}\n", 1 }, + { "peellint_ok_spelling", "selfhost/cmd/wcc/x.ww", + "fn f(t: *tinfo, u: *tinfo) void = {\n" + "\t// peellint-ok: construction\n" + "\tt.under = u;\n" + "};\n", 0 }, + /* check.ww:3683 regression: prose token "io.underread" must not + * trip the ww matcher (token bound), nor `.under` quoted in a + * line comment (comment strip). */ + { "io_underread_prose", "selfhost/cmd/wcc/x.ww", + "fn f(x: int) int = {\n" + "\t// #199 repro io.underread -> (size|io.eof|io.error)\n" + "\t// the NAMED.under chain stays terminating\n" + "\tlet v: int = x + 2; // io.underread again\n" + "\treturn v;\n" + "};\n", 0 }, + { "code_longer_field", "selfhost/cmd/wcc/x.ww", + "fn f(s: stream) int = { return s.underread; };\n", 0 }, + { "c_block_comment_prose", "cmd/wcc/x.c", + "/* walk the chain: a raw t->under read here\n" + " * would single-peel; t->under in prose only. */\n" + "int g(int x) { return x; }\n", 0 }, + { "lib_ww_in_scope", "lib/ww/x.ww", + "fn f(t: *tinfo) *tinfo = {\n" + "\tif (t.kind == tykind.TY_NAMED) { return t.under; };\n" + "\treturn t;\n" + "};\n", 1 }, + /* Review-found evasions (all compile; pre-amendment lint passed + * every one of them green): the matcher must red each. */ + { "evade_c_spacing", "cmd/w6c/x.c", + "static Type *f(Type *t) {\n" + "\treturn (t->kind == TY_NAMED) ? t -> under : t;\n" + "}\n", 1 }, + { "evade_c_linesplit", "cmd/w6c/x.c", + "static Type *f(Type *t) {\n" + "\treturn (t->kind == TY_NAMED) ? t->\n" + "\t under : t;\n" + "}\n", 1 }, + { "evade_c_derefdot", "cmd/w6c/x.c", + "static Type *f(Type *t) {\n" + "\treturn (t->kind == TY_NAMED) ? (*t).under : t;\n" + "}\n", 1 }, + { "evade_ww_dotspace", "selfhost/cmd/wcc/x.ww", + "fn f(t: *tinfo) *tinfo = {\n" + "\tif (t.kind == tykind.TY_NAMED) { return t. under; };\n" + "\treturn t;\n" + "};\n", 1 }, + { "evade_ww_linesplit", "selfhost/cmd/wcc/x.ww", + "fn f(t: *tinfo) *tinfo = {\n" + "\tlet u: *tinfo = t.\n" + "\t\tunder;\n" + "\treturn u;\n" + "};\n", 1 }, + { "evade_c_string_blind", "cmd/w6c/x.c", + "static const char *s = \"/*\";\n" + "static Type *f(Type *t) { return t->under; }\n", 1 }, +}; + +int +main(void) +{ + if (getcwd(root, sizeof root) == NULL) return 1; + + int total = 0, fail = 0; + char cmd[2048], scratch[256]; + + /* 1. The closure proof: the real tree lints clean at HEAD. */ + total++; + snprintf(cmd, sizeof cmd, "sh %s/tools/peellint", root); + if (runwait(cmd) != 0) { + fprintf(stderr, "peellint_gate: real tree NOT clean\n"); + fail++; + } + + /* 2-4. Scratch rows: negative validation + matcher regressions. */ + int n = (int)(sizeof lintrows / sizeof lintrows[0]); + for (int i = 0; i < n; i++) { + total++; + snprintf(scratch, sizeof scratch, "/tmp/plint_%d_%d", + getpid(), i); + int got = lint_scratch(scratch, lintrows[i].relpath, + lintrows[i].body); + if (got != lintrows[i].wantexit) { + fprintf(stderr, "row[%s]: lint exit %d, want %d\n", + lintrows[i].label, got, lintrows[i].wantexit); + fail++; + } + } + + if (fail) { + fprintf(stderr, "peellint_gate: %d/%d checks failed\n", + fail, total); + return 1; + } + printf("peellint_gate: %d/%d ok\n", total, total); + return 0; +} diff --git a/tools/peellint b/tools/peellint new file mode 100755 index 00000000..14fbc1e8 --- /dev/null +++ b/tools/peellint @@ -0,0 +1,139 @@ +#!/bin/sh +# tools/peellint — gate against raw TY_NAMED single-peel reads outside +# the chase accessors. The #5 alias-arc close-by-construction contract +# (rob F2/B7 rulings): one chased accessor is the only spelled way to +# dealias — type_chase_named (cmd/wcc/type.c) on the C side, tichase +# (selfhost/cmd/wcc/cgenutil.ww) on the ww side. A raw `->under` / +# `.under` read peels exactly one NAMED layer; chain-of-aliases stacks +# two, so every kind-gated consumer downstream of a single peel falls +# to a scalar shape (the four-times-burned family: #60/#61/#62/#70…). +# ZERO raw under-token reads may exist in scope outside the annotated +# whitelist; this gate lands in the same commit that deletes the last +# raw read and keeps the class unwritable. +# +# What this does NOT close (stated honestly, per the f2-ruling): a +# consumer that never spells `under` at all — a switch on t->kind that +# simply never peels — has no token to see here. That NO-PEEL class is +# closed only where classification routes through the internalized +# chasing helpers, and contained elsewhere by the acceptance-commit- +# carries-tripwires doctrine. +# +# Matcher: a character scan strips block/line comments and string/char +# literals first (a regex pass mis-nests `/*` inside a string — review +# probe E6), then the under-token is matched accessor-spelling-wide: +# `->under`/`.under` in C (deref-dot `(*t).under` is the same peel), +# `.under` in ww, with optional whitespace after the operator and a +# line-split continuation (`t->` at EOL, `under` next line) — all the +# compiling spellings the B7 review probes evaded with. Right token +# bound keeps prose like "io.underread" (check.ww) out. +# +# Exemption: a line containing `peel-ok` (or the equivalent landed +# spelling `peellint-ok`) exempts itself and the following 9 lines — +# wide enough that one annotation atop a short construction/chase body +# covers it, narrow enough that a stray peel can't hide behind a +# distant annotation. Reasons stay WHY-only (rule 8): construction, +# chase body, recursive chase, resolve-state probe, or a cited task. +# +# Scope: cmd/wcc cmd/w6c selfhost/cmd/wcc lib/ww (skip *.combined.ww). +# lib/ww/typ.ww is in scope deliberately — it is type.c's ww mirror, +# the accessor/classifier layer itself (B7 ruling: excluding it leaves +# an unwatched file where the forbidden shape could be written). +# Exit code: 0 if clean, 1 with one diagnostic per violation. + +set -u + +ROOT=${ROOT:-$(cd "$(dirname "$0")/.." && pwd)} +cd "$ROOT" + +dirs= +for d in cmd/wcc cmd/w6c selfhost/cmd/wcc lib/ww; do + [ -d "$d" ] && dirs="$dirs $d" +done +[ -z "$dirs" ] && exit 0 + +files=$(find $dirs \ + \( -type f \( -name '*.c' -o -name '*.h' -o -name '*.ww' \) \ + ! -name '*.combined.ww' -print \) ) +[ -z "$files" ] && exit 0 + +exec awk -v sq="'" ' +BEGIN { nviol = 0 } + +FNR == 1 { + cur_file = FILENAME + is_c = (cur_file ~ /\.(c|h)$/) + exempt_until = 0 + in_block = 0 + pending = 0 +} + +# Whitelist annotation: arm the exemption window on the RAW line so an +# annotation inside a comment still counts. `peellint-ok` is the +# already-landed sibling spelling (check.ww construction) — accepted +# as-is, history is not re-spelled. +tolower($0) ~ /peel(lint)?-ok([^a-z0-9_]|$)/ { + if (FNR + 9 > exempt_until) exempt_until = FNR + 9 +} + +# Strip comments and string/char literals by character scan: a comment +# opener inside a string is not a comment (E6), and literal text is +# never code. in_block carries across lines; strings/chars do not. +{ + code = "" + n = length($0) + i = 1 + in_str = 0; in_chr = 0 + while (i <= n) { + c = substr($0, i, 1) + c2 = substr($0, i, 2) + if (in_block) { + if (c2 == "*/") { in_block = 0; i += 2 } else i++ + continue + } + if (in_str) { + if (c == "\\") i += 2 + else { if (c == "\"") in_str = 0; i++ } + continue + } + if (in_chr) { + if (c == "\\") i += 2 + else { if (c == sq) in_chr = 0; i++ } + continue + } + if (c2 == "//") break + if (c2 == "/*") { in_block = 1; i += 2; continue } + if (c == "\"") { in_str = 1; i++; continue } + if (c == sq) { in_chr = 1; i++; continue } + code = code c + i++ + } +} + +{ + if (FNR <= exempt_until) { pending = 0; next } + # Both C spellings peel: p->under and (*p).under / v.under. + if (is_c) + re = "(->|\\.)[ \t]*under([^A-Za-z0-9_]|$)" + else + re = "\\.[ \t]*under([^A-Za-z0-9_]|$)" + hit = (code ~ re) + # Line-split continuation: operator at EOL, token opening the + # next code line. Comment-only lines keep the pend alive. + if (!hit && pending && code ~ /^[ \t]*under([^A-Za-z0-9_]|$)/) + hit = 1 + if (hit) { + printf("%s:%d: raw under-token read outside the chase accessor; " \ + "route via type_chase_named (C) / tichase (ww), or annotate " \ + "peel-ok: \n", cur_file, FNR) + nviol++ + } + if (code !~ /^[ \t]*$/) { + if (is_c) + pending = (code ~ /(->|\.)[ \t]*$/) + else + pending = (code ~ /\.[ \t]*$/) + } +} + +END { exit (nviol > 0 ? 1 : 0) } +' $files