#!/bin/sh # tools/peellint — gate against two raw single-resolve shapes that # bypass the alias-chase accessors. # # RULE 1 (raw TY_NAMED single-peel reads) — 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. # # RULE 2 (bare primsize() name-keyed width) — the #101/#109 close-by- # construction contract. primsize(name) is the ww-stage primitive-width # table; it is ALIAS-BLIND — a narrow alias (`type my32 = u32`) returns # 0, defaulting strides/widths to 8 (the #101 struct-fill miscompile and # the #109 is-primitive GUARD family). aliasprimsize(c, name) is the # SSoT chase (primsize else aliaslookup-chase, cgenutil.ww). A bare # primsize() outside the chase machinery is the forbidden shape: the # size/guard sites route through aliasprimsize so an alias resolves. # This rule is ww-only — the C side dealiases via type_chase_named and # has no primsize symbol. ZERO bare primsize() calls may exist in the # ww stage outside the annotated whitelist. # # Both rules land in the same commit that deletes the last raw shape and # keep the class unwritable. # # What rule 1 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. Rule 2 has the symmetric edge: a size # computed by a hardcoded literal instead of primsize is caught by # sizelint (rule 13), not here. # # 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 tokens are matched accessor-spelling-wide: # rule 1: `->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). # rule 2 (ww only): the bare `primsize` token, LEFT+RIGHT word-bounded # so the superstring `aliasprimsize` is NOT a hit. Matching the # standalone token (not just `primsize(`) closes the call form # `primsize(nm)`, the paren-wrap `(primsize)(nm)`, the function-value # bind `let p = primsize`, and every line-split — all of which # compile and reintroduce the alias-blind width (review probes). # Right/left token bounds keep prose like "io.underread" (check.ww) and # "aliasprimsize" out. # # Exemption: a line containing `peel-ok` (or the equivalent landed # spelling `peellint-ok`) exempts rule-1 violations on itself and the # following 9 lines; a line containing `primsize-ok` exempts rule-2 # violations over the same window. The windows are kept separate so a # rule-1 annotation cannot blind a rule-2 bug and vice versa. Wide # enough that one annotation atop a short construction/chase body covers # it, narrow enough that a stray shape can't hide behind a distant # annotation. Reasons stay WHY-only (rule 8): construction, chase body, # recursive chase, resolve-state probe, structural-by-design sizer, 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)$/) und_exempt_until = 0 prim_exempt_until = 0 in_block = 0 pending_und = 0 } # Whitelist annotations: arm the exemption windows 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. The two windows are independent. tolower($0) ~ /peel(lint)?-ok([^a-z0-9_]|$)/ { if (FNR + 9 > und_exempt_until) und_exempt_until = FNR + 9 } tolower($0) ~ /primsize-ok([^a-z0-9_]|$)/ { if (FNR + 9 > prim_exempt_until) prim_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++ } } { und_exempt = (FNR <= und_exempt_until) prim_exempt = (FNR <= prim_exempt_until) blank = (code ~ /^[ \t]*$/) # RULE 1 — raw under-token peel. Both C spellings peel: # p->under and (*p).under / v.under. if (!und_exempt) { if (is_c) ure = "(->|\\.)[ \t]*under([^A-Za-z0-9_]|$)" else ure = "\\.[ \t]*under([^A-Za-z0-9_]|$)" uhit = (code ~ ure) # Line-split continuation: operator at EOL, token opening the # next code line. Comment-only lines keep the pend alive. if (!uhit && pending_und && code ~ /^[ \t]*under([^A-Za-z0-9_]|$)/) uhit = 1 if (uhit) { 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++ } } # RULE 2 — bare primsize token (ww only). LEFT+RIGHT word bounds # so the superstring aliasprimsize() is never a hit and a longer # identifier with a primsize prefix is not matched. Matching the # token (not `primsize(`) catches the paren-wrap `(primsize)(nm)`, # the function-value bind `let p = primsize`, and every line-split — # all compile and reintroduce the alias-blind width (review probes). if (!is_c && !prim_exempt) { if (code ~ /(^|[^A-Za-z0-9_])primsize([^A-Za-z0-9_]|$)/) { printf("%s:%d: bare primsize outside the chase accessor; " \ "route via aliasprimsize (ww), or annotate " \ "primsize-ok: \n", cur_file, FNR) nviol++ } } # Pending updates. An exempt line resets its pend (an annotated # operator/name must not carry into a non-exempt next line); a blank # or comment-only line keeps the pend alive; otherwise re-derive. if (und_exempt) pending_und = 0 else if (!blank) { if (is_c) pending_und = (code ~ /(->|\.)[ \t]*$/) else pending_und = (code ~ /\.[ \t]*$/) } } END { exit (nviol > 0 ? 1 : 0) } ' $files