tools/sizelint + CLAUDE.md rule 13: gate hardcoded size literals

Drew's Hare-discipline framing: "no hardcoded size literals anywhere in
the compiler." This session spent 32 commits sweeping after-the-fact
and STILL kept introducing new bypass sites in our own structural
work (A.5's tupleelemslot/fieldslotsize most recently). The cure is a
gate that catches new violations at commit time, not a deeper sweep.

tools/sizelint (sh+gawk):
- Always-on: `.size = NN` / `->size = NN` / `prim(...,"name",NN,...)`.
- Context-gated literals (NN(u64|i64) and `return NN`) in files or fns
  matching size|slot|elem|field|stride|paramfield|tinfo|primtype|
  slotsize|letemit|tagged.
- Allow-list via `// sizelint-ok: <reason>` or `/* sizelint-ok: ... */`.
- Comment strip happens after allow-list match so prose mentions of
  16/24 stay quiet.

Makefile: `test: all sizelint $(TESTS)` so the gate runs before any
binary builds.

CLAUDE.md rule 13 documents the discipline + escape hatch + optional
pre-commit-hook symlink.

Audit caught 3 real cstage bugs (cmd/wcc/check.c resolve_type:1002,
1079, 1531 hardcoded `tt->size = 16` / `= 32` for tagged-with-ptr and
tagged-with-slice payloads — should read `8 + sub.size`). Fixed
inline; behavioral no-op today (pt->size=16, st->size=24, sub.size=24
match the prior literals) but the SSoT seam carries forward through
#1/#34/#65.

8 SSoT-seed allow-lists added (cstage type.c ty_str/ty_slice prim
factories; wwstage primtypesize/tyslicesize; lib/ww/typ.ww tystr +
slice fields + their main.combined.ww mirrors). One amalloc-overalloc
allow-list at lib/ww/typ.ww:273 cites pending #36 (typed amalloc).

#66 filed for extending the filter once #65 routes lib/bytes +
lib/getopt's sizeof(slice) / sizeof(option) literals through SSoT —
naive line-pattern extension would false-positive on 22+ ELF wire-
format sites in dynout.ww.

131/131 + 994 + 995 + bootstrap green with `make sizelint` exit 0.
This commit is contained in:
2026-05-20 15:22:21 +09:00
parent 03b7336cae
commit f80927201b
9 changed files with 162 additions and 25 deletions

View File

@@ -10,3 +10,4 @@
10. Symmetric stages. cstage and wwstage MUST emit byte-identical asm for the same input. When inference power differs, align the richer side DOWN to the leaner side, not the other way. 10. Symmetric stages. cstage and wwstage MUST emit byte-identical asm for the same input. When inference power differs, align the richer side DOWN to the leaner side, not the other way.
11. Split commits when they bundle unrelated concerns. Bisect-cleanliness is the default. Multi-fix commits need a body paragraph explaining why they couldn't split. 11. Split commits when they bundle unrelated concerns. Bisect-cleanliness is the default. Multi-fix commits need a body paragraph explaining why they couldn't split.
12. Simple data, simple algorithms. Sea-of-stars style. Mirror Hare's structural choices over clever alternatives. 12. Simple data, simple algorithms. Sea-of-stars style. Mirror Hare's structural choices over clever alternatives.
13. No hardcoded size literals in size-computation contexts. Always route through the type table (tinfo.size / Type.size / ty_*->size / size(T) / primtypesize / tyslicesize). Per Drew's framing of Hare's discipline. `make sizelint` enforces and runs as a dep of `make test`. Exemptions documented inline with `// sizelint-ok: <reason>` (ww) or `/* sizelint-ok: <reason> */` (C). Optional local enforcement: `ln -s ../../tools/sizelint .git/hooks/pre-commit`.

View File

@@ -926,7 +926,10 @@ $(BIN)/test_random_run: test/wcc/999_random_run.c $(BIN)/ww $(BIN)/w6c \
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $< $(CC) $(CFLAGS) -o $@ $<
test: all $(TESTS) sizelint:
@sh tools/sizelint
test: all sizelint $(TESTS)
@WW=$(BIN)/ww BIN=$(BIN) sh test/run @WW=$(BIN)/ww BIN=$(BIN) sh test/run
install: all install: all
@@ -1074,4 +1077,4 @@ nocc:
@echo @echo
@echo "NOCC OK: $(STAGE0)/* reproduces itself from source. cc not invoked." @echo "NOCC OK: $(STAGE0)/* reproduces itself from source. cc not invoked."
.PHONY: all cstage wwstage test install clean bootstrap nocc bootstrap-snapshot .PHONY: all cstage wwstage test sizelint install clean bootstrap nocc bootstrap-snapshot

View File

@@ -998,7 +998,8 @@ cexpr(Checker *c, Node *n)
vp->type = pt; vp->next = ve; vp->type = pt; vp->next = ve;
ve->type = ty_nomem; ve->next = NULL; ve->type = ty_nomem; ve->next = NULL;
tt->params = vp; tt->params = vp;
tt->size = 16; /* #64: tag (8) + max-variant payload, per resolve_type:433. */
tt->size = 8 + pt->size;
tt->align = 8; tt->align = 8;
n->type = tt; n->type = tt;
n->lhs->type = ty_err; n->lhs->type = ty_err;
@@ -1074,7 +1075,8 @@ cexpr(Checker *c, Node *n)
vs->type = st; vs->next = ve; vs->type = st; vs->next = ve;
ve->type = ty_nomem; ve->next = NULL; ve->type = ty_nomem; ve->next = NULL;
tt->params = vs; tt->params = vs;
tt->size = 32; /* #64: tag (8) + slice payload, per resolve_type:433. */
tt->size = 8 + st->size;
tt->align = 8; tt->align = 8;
n->type = tt; n->type = tt;
n->lhs->type = ty_err; n->lhs->type = ty_err;
@@ -1525,7 +1527,8 @@ clet(Checker *c, Node *n)
vs->type = st; vs->next = ve; vs->type = st; vs->next = ve;
ve->type = ty_nomem; ve->next = NULL; ve->type = ty_nomem; ve->next = NULL;
tt->params = vs; tt->params = vs;
tt->size = 32; /* #64: tag (8) + slice payload, per resolve_type:433. */
tt->size = 8 + st->size;
tt->align = 8; tt->align = 8;
call->type = tt; call->type = tt;
if (wrap) { if (wrap) {

View File

@@ -61,7 +61,7 @@ typesinit(Arena *a)
ty_f32 = prim(a, TY_F32, "f32", 4, 4); ty_f32 = prim(a, TY_F32, "f32", 4, 4);
ty_f64 = prim(a, TY_F64, "f64", 8, 8); ty_f64 = prim(a, TY_F64, "f64", 8, 8);
/* str is { *u8, len } — 16 bytes on amd64. ABI: pointer + u64. */ /* str is { *u8, len } — 16 bytes on amd64. ABI: pointer + u64. */
ty_str = prim(a, TY_STR, "str", 16, 8); ty_str = prim(a, TY_STR, "str", 16, 8); /* sizelint-ok: SSoT for ty_str (#64) */
ty_err = prim(a, TY_ERR, "<err>", 0, 1); ty_err = prim(a, TY_ERR, "<err>", 0, 1);
ty_never = prim(a, TY_NEVER, "never", 0, 1); ty_never = prim(a, TY_NEVER, "never", 0, 1);
/* #29: predeclared `type nomem = !void;`. NAMED so variant_match /* #29: predeclared `type nomem = !void;`. NAMED so variant_match
@@ -98,7 +98,7 @@ type_slice(Arena *a, Type *sub)
{ {
Type *t = newtype(a, TY_SLICE); Type *t = newtype(a, TY_SLICE);
t->sub = sub; t->sub = sub;
t->size = 24; /* { *T, len, cap } */ t->size = 24; /* { *T, len, cap } */ /* sizelint-ok: SSoT for ty_slice (#64) */
t->align = 8; t->align = 8;
return t; return t;
} }

View File

@@ -186,7 +186,7 @@ export fn typesinit(c: *tctx, a: *arena) void = {
c.tyuintptr= prim(a, tykind.TY_UINTPTR, "uintptr", 8u64, 8u64); c.tyuintptr= prim(a, tykind.TY_UINTPTR, "uintptr", 8u64, 8u64);
c.tyf32 = prim(a, tykind.TY_F32, "f32", 4u64, 4u64); c.tyf32 = prim(a, tykind.TY_F32, "f32", 4u64, 4u64);
c.tyf64 = prim(a, tykind.TY_F64, "f64", 8u64, 8u64); c.tyf64 = prim(a, tykind.TY_F64, "f64", 8u64, 8u64);
c.tystr = prim(a, tykind.TY_STR, "str", 16u64, 8u64); c.tystr = prim(a, tykind.TY_STR, "str", 16u64, 8u64); // sizelint-ok: SSoT for tystr (#64)
c.tyerr = prim(a, tykind.TY_ERR, "<err>", 0u64, 1u64); c.tyerr = prim(a, tykind.TY_ERR, "<err>", 0u64, 1u64);
c.tynever = prim(a, tykind.TY_NEVER, "never", 0u64, 1u64); c.tynever = prim(a, tykind.TY_NEVER, "never", 0u64, 1u64);
@@ -210,9 +210,9 @@ export fn typeptr(a: *arena, sub: *tinfo) *tinfo = {
export fn typeslice(a: *arena, sub: *tinfo) *tinfo = { export fn typeslice(a: *arena, sub: *tinfo) *tinfo = {
let t: *tinfo = newtype(a, tykind.TY_SLICE); let t: *tinfo = newtype(a, tykind.TY_SLICE);
t.sub = sub; t.sub = sub;
t.size = 24u64; t.size = 24u64; // sizelint-ok: SSoT for slice header (#64)
t.align = 8u64; t.align = 8u64;
t.slotsize = 24u64; t.slotsize = 24u64; // sizelint-ok: SSoT for slice slotsize (#64)
return t; return t;
}; };
@@ -270,7 +270,9 @@ export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = {
}; };
export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = { export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = {
let e: *tinfocacheent = amalloc(c.a, 32u64): *tinfocacheent; // #36 (typed amalloc) — over-size the 24B tinfocacheent struct to
// dodge the cstage amalloc<size silent corruption (selfhost/CLAUDE.md).
let e: *tinfocacheent = amalloc(c.a, 32u64): *tinfocacheent; // sizelint-ok: amalloc over-size pending #36
e.key = key; e.key = key;
e.val = val; e.val = val;
e.cnext = c.tinfocache; e.cnext = c.tinfocache;

View File

@@ -6613,7 +6613,7 @@ export fn typesinit(c: *tctx, a: *arena) void = {
c.tyuintptr= prim(a, tykind.TY_UINTPTR, "uintptr", 8u64, 8u64); c.tyuintptr= prim(a, tykind.TY_UINTPTR, "uintptr", 8u64, 8u64);
c.tyf32 = prim(a, tykind.TY_F32, "f32", 4u64, 4u64); c.tyf32 = prim(a, tykind.TY_F32, "f32", 4u64, 4u64);
c.tyf64 = prim(a, tykind.TY_F64, "f64", 8u64, 8u64); c.tyf64 = prim(a, tykind.TY_F64, "f64", 8u64, 8u64);
c.tystr = prim(a, tykind.TY_STR, "str", 16u64, 8u64); c.tystr = prim(a, tykind.TY_STR, "str", 16u64, 8u64); // sizelint-ok: SSoT for tystr (#64)
c.tyerr = prim(a, tykind.TY_ERR, "<err>", 0u64, 1u64); c.tyerr = prim(a, tykind.TY_ERR, "<err>", 0u64, 1u64);
c.tynever = prim(a, tykind.TY_NEVER, "never", 0u64, 1u64); c.tynever = prim(a, tykind.TY_NEVER, "never", 0u64, 1u64);
@@ -6637,9 +6637,9 @@ export fn typeptr(a: *arena, sub: *tinfo) *tinfo = {
export fn typeslice(a: *arena, sub: *tinfo) *tinfo = { export fn typeslice(a: *arena, sub: *tinfo) *tinfo = {
let t: *tinfo = newtype(a, tykind.TY_SLICE); let t: *tinfo = newtype(a, tykind.TY_SLICE);
t.sub = sub; t.sub = sub;
t.size = 24u64; t.size = 24u64; // sizelint-ok: SSoT for slice header (#64)
t.align = 8u64; t.align = 8u64;
t.slotsize = 24u64; t.slotsize = 24u64; // sizelint-ok: SSoT for slice slotsize (#64)
return t; return t;
}; };
@@ -6697,7 +6697,9 @@ export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = {
}; };
export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = { export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = {
let e: *tinfocacheent = amalloc(c.a, 32u64): *tinfocacheent; // #36 (typed amalloc) — over-size the 24B tinfocacheent struct to
// dodge the cstage amalloc<size silent corruption (selfhost/CLAUDE.md).
let e: *tinfocacheent = amalloc(c.a, 32u64): *tinfocacheent; // sizelint-ok: amalloc over-size pending #36
e.key = key; e.key = key;
e.val = val; e.val = val;
e.cnext = c.tinfocache; e.cnext = c.tinfocache;
@@ -7757,14 +7759,14 @@ fn primtypesize(nm: str) i64 = {
if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; }; if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; };
if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64")) { return 8i64; }; if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64")) { return 8i64; };
if (streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr")) { return 8i64; }; if (streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr")) { return 8i64; };
if (streq(nm, "str")) { return 16i64; }; if (streq(nm, "str")) { return 16i64; }; // sizelint-ok: SSoT for ty_str primtype (#64)
return -1i64; return -1i64;
}; };
// #43: SSoT for slice header size (ptr+len+cap = 24B today). Mirrors // #43: SSoT for slice header size (ptr+len+cap = 24B today). Mirrors
// cstage cmd/wcc/type.c:103 (ty_slice->size = 24). Bumping a slice's // cstage cmd/wcc/type.c:103 (ty_slice->size = 24). Bumping a slice's
// header layout in #34 touches only this constant. // header layout in #34 touches only this constant.
fn tyslicesize() i64 = { return 24i64; }; fn tyslicesize() i64 = { return 24i64; }; // sizelint-ok: SSoT for ty_slice header (#64)
// #42: AST-level layout helpers for the size(T)/align(T)/offset(e.f) // #42: AST-level layout helpers for the size(T)/align(T)/offset(e.f)
// fold. Mirror cstage resolve_type's size/align computation // fold. Mirror cstage resolve_type's size/align computation

View File

@@ -670,14 +670,14 @@ fn primtypesize(nm: str) i64 = {
if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; }; if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; };
if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64")) { return 8i64; }; if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64")) { return 8i64; };
if (streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr")) { return 8i64; }; if (streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr")) { return 8i64; };
if (streq(nm, "str")) { return 16i64; }; if (streq(nm, "str")) { return 16i64; }; // sizelint-ok: SSoT for ty_str primtype (#64)
return -1i64; return -1i64;
}; };
// #43: SSoT for slice header size (ptr+len+cap = 24B today). Mirrors // #43: SSoT for slice header size (ptr+len+cap = 24B today). Mirrors
// cstage cmd/wcc/type.c:103 (ty_slice->size = 24). Bumping a slice's // cstage cmd/wcc/type.c:103 (ty_slice->size = 24). Bumping a slice's
// header layout in #34 touches only this constant. // header layout in #34 touches only this constant.
fn tyslicesize() i64 = { return 24i64; }; fn tyslicesize() i64 = { return 24i64; }; // sizelint-ok: SSoT for ty_slice header (#64)
// #42: AST-level layout helpers for the size(T)/align(T)/offset(e.f) // #42: AST-level layout helpers for the size(T)/align(T)/offset(e.f)
// fold. Mirror cstage resolve_type's size/align computation // fold. Mirror cstage resolve_type's size/align computation

View File

@@ -6613,7 +6613,7 @@ export fn typesinit(c: *tctx, a: *arena) void = {
c.tyuintptr= prim(a, tykind.TY_UINTPTR, "uintptr", 8u64, 8u64); c.tyuintptr= prim(a, tykind.TY_UINTPTR, "uintptr", 8u64, 8u64);
c.tyf32 = prim(a, tykind.TY_F32, "f32", 4u64, 4u64); c.tyf32 = prim(a, tykind.TY_F32, "f32", 4u64, 4u64);
c.tyf64 = prim(a, tykind.TY_F64, "f64", 8u64, 8u64); c.tyf64 = prim(a, tykind.TY_F64, "f64", 8u64, 8u64);
c.tystr = prim(a, tykind.TY_STR, "str", 16u64, 8u64); c.tystr = prim(a, tykind.TY_STR, "str", 16u64, 8u64); // sizelint-ok: SSoT for tystr (#64)
c.tyerr = prim(a, tykind.TY_ERR, "<err>", 0u64, 1u64); c.tyerr = prim(a, tykind.TY_ERR, "<err>", 0u64, 1u64);
c.tynever = prim(a, tykind.TY_NEVER, "never", 0u64, 1u64); c.tynever = prim(a, tykind.TY_NEVER, "never", 0u64, 1u64);
@@ -6637,9 +6637,9 @@ export fn typeptr(a: *arena, sub: *tinfo) *tinfo = {
export fn typeslice(a: *arena, sub: *tinfo) *tinfo = { export fn typeslice(a: *arena, sub: *tinfo) *tinfo = {
let t: *tinfo = newtype(a, tykind.TY_SLICE); let t: *tinfo = newtype(a, tykind.TY_SLICE);
t.sub = sub; t.sub = sub;
t.size = 24u64; t.size = 24u64; // sizelint-ok: SSoT for slice header (#64)
t.align = 8u64; t.align = 8u64;
t.slotsize = 24u64; t.slotsize = 24u64; // sizelint-ok: SSoT for slice slotsize (#64)
return t; return t;
}; };
@@ -6697,7 +6697,9 @@ export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = {
}; };
export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = { export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = {
let e: *tinfocacheent = amalloc(c.a, 32u64): *tinfocacheent; // #36 (typed amalloc) — over-size the 24B tinfocacheent struct to
// dodge the cstage amalloc<size silent corruption (selfhost/CLAUDE.md).
let e: *tinfocacheent = amalloc(c.a, 32u64): *tinfocacheent; // sizelint-ok: amalloc over-size pending #36
e.key = key; e.key = key;
e.val = val; e.val = val;
e.cnext = c.tinfocache; e.cnext = c.tinfocache;
@@ -7757,14 +7759,14 @@ fn primtypesize(nm: str) i64 = {
if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; }; if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; };
if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64")) { return 8i64; }; if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64")) { return 8i64; };
if (streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr")) { return 8i64; }; if (streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr")) { return 8i64; };
if (streq(nm, "str")) { return 16i64; }; if (streq(nm, "str")) { return 16i64; }; // sizelint-ok: SSoT for ty_str primtype (#64)
return -1i64; return -1i64;
}; };
// #43: SSoT for slice header size (ptr+len+cap = 24B today). Mirrors // #43: SSoT for slice header size (ptr+len+cap = 24B today). Mirrors
// cstage cmd/wcc/type.c:103 (ty_slice->size = 24). Bumping a slice's // cstage cmd/wcc/type.c:103 (ty_slice->size = 24). Bumping a slice's
// header layout in #34 touches only this constant. // header layout in #34 touches only this constant.
fn tyslicesize() i64 = { return 24i64; }; fn tyslicesize() i64 = { return 24i64; }; // sizelint-ok: SSoT for ty_slice header (#64)
// #42: AST-level layout helpers for the size(T)/align(T)/offset(e.f) // #42: AST-level layout helpers for the size(T)/align(T)/offset(e.f)
// fold. Mirror cstage resolve_type's size/align computation // fold. Mirror cstage resolve_type's size/align computation

124
tools/sizelint Executable file
View File

@@ -0,0 +1,124 @@
#!/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 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