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