diff --git a/CLAUDE.md b/CLAUDE.md index 9e4fd3c1..b4eb01d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. 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. +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: ` (ww) or `/* sizelint-ok: */` (C). Optional local enforcement: `ln -s ../../tools/sizelint .git/hooks/pre-commit`. diff --git a/Makefile b/Makefile index 05286937..1b3ce205 100644 --- a/Makefile +++ b/Makefile @@ -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) $(CC) $(CFLAGS) -o $@ $< -test: all $(TESTS) +sizelint: + @sh tools/sizelint + +test: all sizelint $(TESTS) @WW=$(BIN)/ww BIN=$(BIN) sh test/run install: all @@ -1074,4 +1077,4 @@ nocc: @echo @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 diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index e75e05db..dac9e892 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -998,7 +998,8 @@ cexpr(Checker *c, Node *n) vp->type = pt; vp->next = ve; ve->type = ty_nomem; ve->next = NULL; tt->params = vp; - tt->size = 16; + /* #64: tag (8) + max-variant payload, per resolve_type:433. */ + tt->size = 8 + pt->size; tt->align = 8; n->type = tt; n->lhs->type = ty_err; @@ -1074,7 +1075,8 @@ cexpr(Checker *c, Node *n) vs->type = st; vs->next = ve; ve->type = ty_nomem; ve->next = NULL; tt->params = vs; - tt->size = 32; + /* #64: tag (8) + slice payload, per resolve_type:433. */ + tt->size = 8 + st->size; tt->align = 8; n->type = tt; n->lhs->type = ty_err; @@ -1525,7 +1527,8 @@ clet(Checker *c, Node *n) vs->type = st; vs->next = ve; ve->type = ty_nomem; ve->next = NULL; tt->params = vs; - tt->size = 32; + /* #64: tag (8) + slice payload, per resolve_type:433. */ + tt->size = 8 + st->size; tt->align = 8; call->type = tt; if (wrap) { diff --git a/cmd/wcc/type.c b/cmd/wcc/type.c index a91cd033..06e97fa6 100644 --- a/cmd/wcc/type.c +++ b/cmd/wcc/type.c @@ -61,7 +61,7 @@ typesinit(Arena *a) ty_f32 = prim(a, TY_F32, "f32", 4, 4); ty_f64 = prim(a, TY_F64, "f64", 8, 8); /* 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, "", 0, 1); ty_never = prim(a, TY_NEVER, "never", 0, 1); /* #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); 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; return t; } diff --git a/lib/ww/typ.ww b/lib/ww/typ.ww index 6b434330..4a8ddef0 100644 --- a/lib/ww/typ.ww +++ b/lib/ww/typ.ww @@ -186,7 +186,7 @@ export fn typesinit(c: *tctx, a: *arena) void = { c.tyuintptr= prim(a, tykind.TY_UINTPTR, "uintptr", 8u64, 8u64); c.tyf32 = prim(a, tykind.TY_F32, "f32", 4u64, 4u64); 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, "", 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 = { let t: *tinfo = newtype(a, tykind.TY_SLICE); t.sub = sub; - t.size = 24u64; + t.size = 24u64; // sizelint-ok: SSoT for slice header (#64) t.align = 8u64; - t.slotsize = 24u64; + t.slotsize = 24u64; // sizelint-ok: SSoT for slice slotsize (#64) return t; }; @@ -270,7 +270,9 @@ export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = { }; 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", 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 = { let t: *tinfo = newtype(a, tykind.TY_SLICE); t.sub = sub; - t.size = 24u64; + t.size = 24u64; // sizelint-ok: SSoT for slice header (#64) t.align = 8u64; - t.slotsize = 24u64; + t.slotsize = 24u64; // sizelint-ok: SSoT for slice slotsize (#64) return t; }; @@ -6697,7 +6697,9 @@ export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = { }; 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 amallocsize = 24). Bumping a slice's // 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) // fold. Mirror cstage resolve_type's size/align computation diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index f5c54f96..68a7e01d 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -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, "i64") || streq(nm, "u64") || streq(nm, "f64")) { 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; }; // #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 // 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) // fold. Mirror cstage resolve_type's size/align computation diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 9f1e8a3f..51eac1e7 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -6613,7 +6613,7 @@ export fn typesinit(c: *tctx, a: *arena) void = { c.tyuintptr= prim(a, tykind.TY_UINTPTR, "uintptr", 8u64, 8u64); c.tyf32 = prim(a, tykind.TY_F32, "f32", 4u64, 4u64); 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, "", 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 = { let t: *tinfo = newtype(a, tykind.TY_SLICE); t.sub = sub; - t.size = 24u64; + t.size = 24u64; // sizelint-ok: SSoT for slice header (#64) t.align = 8u64; - t.slotsize = 24u64; + t.slotsize = 24u64; // sizelint-ok: SSoT for slice slotsize (#64) return t; }; @@ -6697,7 +6697,9 @@ export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = { }; 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 amallocsize = 24). Bumping a slice's // 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) // fold. Mirror cstage resolve_type's size/align computation diff --git a/tools/sizelint b/tools/sizelint new file mode 100755 index 00000000..b0828391 --- /dev/null +++ b/tools/sizelint @@ -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: ` +# 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 = "\\]+\"[ \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