Files
ww/tools/peellint
Hojun-Cho 4b118fa8f8 cgen: B7 emitter elem chases + tools/peellint gate — #5 alias-arc cs side closed by construction
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).
2026-06-06 06:05:46 +09:00

140 lines
4.8 KiB
Bash
Executable File

#!/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: <why>\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