#!/bin/sh
# tools/sizelint — gate against hardcoded size literals in size-computation
# contexts.  Per Drew's framing of Hare's discipline and ww task #64:
# every byte size that names a type's footprint must route through the
# type table (tinfo.size / Type.size / ty_*->size / size(T)).  Bare
# numerics encode the layout twice and silently desync (#1, #43, #60,
# #65 sweeps caught one site at a time post-hoc).
#
# Patterns are matched in two tiers:
#   1) Always-on strong signals — direct writes to a type's size /
#      slotsize / align field, the `prim(...,size,align)` factory call,
#      and any literal in the rhs of `*->size = ` style assignments.
#   2) Context-gated literals — bare 16/24/32 (suffix-tagged or not) only
#      inside files or functions whose name matches one of:
#        size|slot|elem|field|stride|paramfield|tinfo|primtype|slotsize|
#        letemit|tagged
#
# Exempt a single line with an end-of-line `// sizelint-ok: <reason>`
# comment.  Use sparingly and cite a task or structural reason.
#
# Scope: cmd/ selfhost/ lib/  (skip ref/ out/ bootstrap/ .combined.ww).
# Exit code: 0 if clean, 1 with one diagnostic per violation.

set -u

ROOT=${ROOT:-$(cd "$(dirname "$0")/.." && pwd)}
cd "$ROOT"

files=$(find cmd selfhost lib \
	\( -type d -name out -prune \) -o \
	\( -type d -name bootstrap -prune \) -o \
	\( -type d -name ref -prune \) -o \
	\( -type d -name .git -prune \) -o \
	\( -type d -name .claude -prune \) -o \
	\( -type d -name .ai -prune \) -o \
	\( -type d -name '*.sepwork' -prune \) -o \
	\( -type f \( -name '*.c' -o -name '*.h' -o -name '*.ww' \) \
		! -name '*.combined.ww' -print \) )

exec awk '
BEGIN {
	ctx_re = "size|slot|elem|field|stride|paramfield|tinfo|primtype|slotsize|letemit|tagged"
	tagged_lit_re = "(16|24|32)(u64|i64)"
	return_lit_re = "return[ \t]+(16|24|32)[ \t]*[;}]"
	# Strong signal: assignment to .size / ->size / .slotsize / ->slotsize.
	# Value 8 is the natural pointer/scalar size — leave un-flagged so
	# typeptr/typechan stay quiet without an allow-list per assignment.
	size_assign_re = "(\\.|->)[ \t]*(size|slotsize)[ \t]*=[ \t]*(16|24|32)([^0-9]|$)"
	# Strong signal: type-table factory `prim(arena, kind, "name", SIZE, ALIGN)`.
	prim_call_re = "\\<prim[ \t]*\\([^)]*\"[A-Za-z_<>]+\"[ \t]*,[ \t]*(16|24|32)"
	nviol = 0
}

FNR == 1 {
	cur_file = FILENAME
	file_in_ctx = (tolower(cur_file) ~ ctx_re)
	is_c = (cur_file ~ /\.(c|h)$/)
	fn_name = ""
	fn_in_ctx = 0
}

# Track function context (ww + C styles).
{
	if (match($0, /^[ \t]*(export[ \t]+)?fn[ \t]+[A-Za-z_][A-Za-z0-9_]*/)) {
		s = substr($0, RSTART, RLENGTH)
		sub(/^[ \t]*(export[ \t]+)?fn[ \t]+/, "", s)
		fn_name = s
		fn_in_ctx = (tolower(fn_name) ~ ctx_re)
	} else if (is_c && match($0, /^[A-Za-z_][A-Za-z0-9_]*\(/)) {
		s = substr($0, RSTART, RLENGTH - 1)
		fn_name = s
		fn_in_ctx = (tolower(fn_name) ~ ctx_re)
	}
}

# Allow-list (case-insensitive on the keyword) — check the raw line so a
# doc comment can still exempt itself.  Accepts both `// sizelint-ok:` (ww
# and C99) and `/* sizelint-ok: ... */` (Plan 9 C style).
tolower($0) ~ /sizelint-ok:/ { next }

# Strip line and block comments before pattern matching — a comment that
# quotes a hardcoded size in prose is not a violation.  Single-line block
# comments only; multi-line `/* ... */` spans rarely contain assignments
# we care about.
{
	code = $0
	sub(/\/\/.*$/, "", code)
	gsub(/\/\*[^*]*\*+([^/*][^*]*\*+)*\//, "", code)
}

# Always-on strong-signal patterns.
{
	if (match(code, size_assign_re)) {
		report(substr(code, RSTART, RLENGTH),
		       "assignment to .size/.slotsize with literal; route via type SSoT (tinfo.size / ty_*->size)")
	}
	if (match(code, prim_call_re)) {
		report(substr(code, RSTART, RLENGTH),
		       "literal in prim(...) size slot; type-table is SSoT (cmd/wcc/type.c, lib/ww/typ.ww)")
	}
}

# Context-gated patterns.
file_in_ctx || fn_in_ctx {
	line = code
	while (match(line, tagged_lit_re)) {
		report(substr(line, RSTART, RLENGTH),
		       "size literal in size-context; route via size(T) / primtypesize / tyslicesize SSoT")
		line = substr(line, RSTART + RLENGTH)
	}
	if (match(code, return_lit_re)) {
		report(substr(code, RSTART, RLENGTH),
		       "return of bare size literal in size-context; route via type SSoT")
	}
}

function report(snip, msg) {
	# Trim trailing newline / extra whitespace from snippet.
	gsub(/[ \t]+$/, "", snip)
	printf("%s:%d: %s; suggest: %s\n", cur_file, FNR, snip, msg)
	nviol++
}

END { exit (nviol > 0 ? 1 : 0) }
' $files
