Route the 16 routable bare-primsize GUARD sites (is-primitive / struct-vs-prim dispatch) through the #101 aliasprimsize SSoT helper. Byte-NEUTRAL by construction: an alias-narrow name is already neutralized downstream by the same arm, so routing emits no new asm (the empty-flip-set ken oracled). Shape-A exclude-prim-early (3): cgenutil sretretsize / structparamsize / structfloatclass — `primsize>0 return` then structlookup→nil returns the same value; route returns it early, same. Shape-B prim-guard-then-structlookup (13): cgenutil 4604/4650 + cgenexpr 4136/10244 + the 9-site CALL/assign cluster — primsize==0 →structlookup→nil→fall to normal; route skips the block→same normal. Install the peellint bare-primsize FINALE (B7 lint-fuse contract): tools/peellint now rejects any bare primsize() in the ww stage outside the annotated whitelist. Evasion-hardened per the B7 lesson — a character scan (comments + string/char literals stripped first) and a LEFT+RIGHT word-bounded match of the bare `primsize` TOKEN (not just `primsize(`), so the aliasprimsize() wrapper is never a hit and every compiling spelling reds: the call primsize(nm), the paren-wrap (primsize)(nm), the function-value bind `let p = primsize`, and any line-split. ww-only (the C stage dealiases via type_chase_named, no primsize symbol). Two independent exemption windows (peel-ok vs primsize-ok) so neither rule blinds the other. Runs as a make-test dep. Whitelist the 6 designed exemptions with primsize-ok WHY-annotations: machinery — aliasprimsize body (SSoT chase) | typenodeprimresolved + exprprimresolved (#11/#33 prim-resolver chasers) | cgcast leaf-loop + cgenexpr #11 deref-store (own ps==0 fallback; route would regress #11) | the primsize oracle/definition itself (nothing below to chase). structural — elemsizeof x2 + paramfieldsize (chase lives in the -c twin elemsizeofc; threading c is the dormant #110). Empty-flip-set proof: zero C bytes; cstage binaries bit-identical; bootstrap byte-id 990-997 + 950 all green (w6c == w6c_ww on the full selfhost, self-rebuild identical); combined.ww (w6c + wwdump) regen idempotent; sizelint 0; peellint 0 (raw-peel AND bare-primsize over the whole tree = the close-by-construction proof, zero unwhitelisted survivors). Tests: 944_peellint_gate +14 rows (bare / space-before-paren / name-at-EOL split / string-blind opener / paren-wrap / fn-value-bind RED; aliasprimsize wrapper + primsize-ok annotated GREEN; corrupt annotation RED; independent peel/primsize windows; C-file out-of-scope). Closes the #101 primsize-alias family by construction. #109.
288 lines
10 KiB
C
288 lines
10 KiB
C
/*
|
|
* 944_peellint_gate — teeth for tools/peellint (#5 alias-arc B7).
|
|
*
|
|
* The lint is the enforcement half of the close-by-construction
|
|
* contract: zero raw under-token reads in scope outside the annotated
|
|
* whitelist. A gate without negative validation can rot green (B4
|
|
* precedent), so this test pins BOTH directions:
|
|
*
|
|
* 1. real tree at HEAD lints CLEAN (the closure proof itself);
|
|
* 2. a re-introduced raw peel REDS the lint — C `->under` ternary
|
|
* and ww `.under` if-peel, the four-times-burned spellings;
|
|
* 3. a corrupted whitelist annotation REDS the lint (token-bounded
|
|
* `peel-ok` matcher: `peel-okk-…` must NOT exempt);
|
|
* 4. regression rows that must stay GREEN: the check.ww:3683
|
|
* "io.underread" prose (token bound), a code read of a longer
|
|
* field (`s.underread`), comment-quoted `.under`/`->under`
|
|
* prose (comment strip), and the already-landed `peellint-ok`
|
|
* sibling spelling (history is not re-spelled);
|
|
* 5. review-found evasion spellings REDS (B7 review probes E1-E6,
|
|
* every one compiles): `t -> under` spacing, `t->`/EOL +
|
|
* `under` next line (both stages' split), C deref-dot
|
|
* `(*t).under`, ww `t. under`, and a string literal containing
|
|
* a block-comment OPENER token that blinded the old regex
|
|
* comment-strip for the rest of the file.
|
|
* 6. RULE 2 (#101/#109) — bare primsize() in the ww stage is the
|
|
* alias-blind width shape aliasprimsize() supersedes. A bare
|
|
* `primsize(` REDS; the SSoT wrapper `aliasprimsize(` must NOT
|
|
* (left word boundary); the evasion spellings (space-before-paren,
|
|
* name-at-EOL line split, string-blind block-comment opener in a
|
|
* literal, paren-wrap `(primsize)(nm)`, function-value bind
|
|
* `let p = primsize` — the last two reviewer-109-found, both
|
|
* compile + run) all RED; a
|
|
* `primsize-ok` annotation exempts; a corrupted one does not; the
|
|
* primsize-ok and peel-ok windows are independent (neither blinds
|
|
* the other's shape); and a C-file `primsize(` is out of scope
|
|
* (the C stage chases via type_chase_named, no primsize symbol).
|
|
*
|
|
* Scratch trees live under /tmp and exercise the lint via its ROOT
|
|
* override (sizelint-style), so the real tree is never touched.
|
|
*/
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
|
|
static char root[1024]; /* repo root (cwd when run via test/run) */
|
|
|
|
static int
|
|
runwait(const char *cmd)
|
|
{
|
|
int rc = system(cmd);
|
|
if (rc == -1) return -1;
|
|
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
write_file(const char *path, const char *body)
|
|
{
|
|
FILE *f = fopen(path, "wb");
|
|
if (!f) return -1;
|
|
fputs(body, f);
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
|
|
/* lint_scratch — run tools/peellint over a one-file scratch tree and
|
|
* return its exit code. relpath selects the in-scope directory. */
|
|
static int
|
|
lint_scratch(const char *scratch, const char *relpath, const char *body)
|
|
{
|
|
char cmd[2048], path[1400];
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s", scratch);
|
|
runwait(cmd);
|
|
snprintf(path, sizeof path, "%s/%s", scratch, relpath);
|
|
char dir[1400];
|
|
snprintf(dir, sizeof dir, "%s", path);
|
|
char *slash = strrchr(dir, '/');
|
|
if (slash) *slash = '\0';
|
|
snprintf(cmd, sizeof cmd, "mkdir -p %s", dir);
|
|
if (runwait(cmd) != 0) return -1;
|
|
if (write_file(path, body) != 0) return -1;
|
|
snprintf(cmd, sizeof cmd,
|
|
"ROOT=%s sh %s/tools/peellint >/dev/null 2>&1", scratch, root);
|
|
int rc = runwait(cmd);
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s", scratch);
|
|
runwait(cmd);
|
|
return rc;
|
|
}
|
|
|
|
struct lintrow { const char *label; const char *relpath;
|
|
const char *body; int wantexit; };
|
|
|
|
static const struct lintrow lintrows[] = {
|
|
{ "reinject_c_peel", "cmd/w6c/x.c",
|
|
"static Type *f(Type *t) {\n"
|
|
"\tType *u = (t->kind == TY_NAMED) ? t->under : t;\n"
|
|
"\treturn u;\n"
|
|
"}\n", 1 },
|
|
{ "reinject_ww_peel", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(t: *tinfo) *tinfo = {\n"
|
|
"\tif (t.kind == tykind.TY_NAMED) { return t.under; };\n"
|
|
"\treturn t;\n"
|
|
"};\n", 1 },
|
|
{ "annotated_c_peel_ok", "cmd/w6c/x.c",
|
|
"static Type *f(Type *t) {\n"
|
|
"\tType *u = (t->kind == TY_NAMED) ? t->under : t; "
|
|
"/* peel-ok: probe */\n"
|
|
"\treturn u;\n"
|
|
"}\n", 0 },
|
|
{ "corrupt_annotation", "cmd/w6c/x.c",
|
|
"static Type *f(Type *t) {\n"
|
|
"\tType *u = (t->kind == TY_NAMED) ? t->under : t; "
|
|
"/* peel-okk-corrupt: probe */\n"
|
|
"\treturn u;\n"
|
|
"}\n", 1 },
|
|
{ "peellint_ok_spelling", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(t: *tinfo, u: *tinfo) void = {\n"
|
|
"\t// peellint-ok: construction\n"
|
|
"\tt.under = u;\n"
|
|
"};\n", 0 },
|
|
/* check.ww:3683 regression: prose token "io.underread" must not
|
|
* trip the ww matcher (token bound), nor `.under` quoted in a
|
|
* line comment (comment strip). */
|
|
{ "io_underread_prose", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(x: int) int = {\n"
|
|
"\t// #199 repro io.underread -> (size|io.eof|io.error)\n"
|
|
"\t// the NAMED.under chain stays terminating\n"
|
|
"\tlet v: int = x + 2; // io.underread again\n"
|
|
"\treturn v;\n"
|
|
"};\n", 0 },
|
|
{ "code_longer_field", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(s: stream) int = { return s.underread; };\n", 0 },
|
|
{ "c_block_comment_prose", "cmd/wcc/x.c",
|
|
"/* walk the chain: a raw t->under read here\n"
|
|
" * would single-peel; t->under in prose only. */\n"
|
|
"int g(int x) { return x; }\n", 0 },
|
|
{ "lib_ww_in_scope", "lib/ww/x.ww",
|
|
"fn f(t: *tinfo) *tinfo = {\n"
|
|
"\tif (t.kind == tykind.TY_NAMED) { return t.under; };\n"
|
|
"\treturn t;\n"
|
|
"};\n", 1 },
|
|
/* Review-found evasions (all compile; pre-amendment lint passed
|
|
* every one of them green): the matcher must red each. */
|
|
{ "evade_c_spacing", "cmd/w6c/x.c",
|
|
"static Type *f(Type *t) {\n"
|
|
"\treturn (t->kind == TY_NAMED) ? t -> under : t;\n"
|
|
"}\n", 1 },
|
|
{ "evade_c_linesplit", "cmd/w6c/x.c",
|
|
"static Type *f(Type *t) {\n"
|
|
"\treturn (t->kind == TY_NAMED) ? t->\n"
|
|
"\t under : t;\n"
|
|
"}\n", 1 },
|
|
{ "evade_c_derefdot", "cmd/w6c/x.c",
|
|
"static Type *f(Type *t) {\n"
|
|
"\treturn (t->kind == TY_NAMED) ? (*t).under : t;\n"
|
|
"}\n", 1 },
|
|
{ "evade_ww_dotspace", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(t: *tinfo) *tinfo = {\n"
|
|
"\tif (t.kind == tykind.TY_NAMED) { return t. under; };\n"
|
|
"\treturn t;\n"
|
|
"};\n", 1 },
|
|
{ "evade_ww_linesplit", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(t: *tinfo) *tinfo = {\n"
|
|
"\tlet u: *tinfo = t.\n"
|
|
"\t\tunder;\n"
|
|
"\treturn u;\n"
|
|
"};\n", 1 },
|
|
{ "evade_c_string_blind", "cmd/w6c/x.c",
|
|
"static const char *s = \"/*\";\n"
|
|
"static Type *f(Type *t) { return t->under; }\n", 1 },
|
|
/* RULE 2 (#101/#109): bare primsize() outside the chase is the
|
|
* forbidden alias-blind width shape; aliasprimsize is the SSoT. */
|
|
{ "prim_bare", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(c: *cgen, nm: str) i32 = {\n"
|
|
"\tlet z: i32 = primsize(nm);\n"
|
|
"\treturn z;\n"
|
|
"};\n", 1 },
|
|
{ "prim_evade_spacing", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(c: *cgen, nm: str) i32 = {\n"
|
|
"\tlet z: i32 = primsize (nm);\n"
|
|
"\treturn z;\n"
|
|
"};\n", 1 },
|
|
{ "prim_evade_linesplit", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(c: *cgen, nm: str) i32 = {\n"
|
|
"\tlet z: i32 = primsize\n"
|
|
"\t (nm);\n"
|
|
"\treturn z;\n"
|
|
"};\n", 1 },
|
|
{ "prim_evade_string_blind", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(c: *cgen) str = {\n"
|
|
"\tlet s: str = \"/*\";\n"
|
|
"\tlet z: i32 = primsize(s);\n"
|
|
"\treturn s;\n"
|
|
"};\n", 1 },
|
|
/* review-109 evasions: both COMPILE + run (verified) yet slipped a
|
|
* `primsize(`-only matcher — the token rule reds them. */
|
|
{ "prim_evade_parenwrap", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(c: *cgen, nm: str) i32 = {\n"
|
|
"\tlet z: i32 = (primsize)(nm);\n"
|
|
"\treturn z;\n"
|
|
"};\n", 1 },
|
|
{ "prim_evade_fnvalue", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(c: *cgen, nm: str) i32 = {\n"
|
|
"\tlet p = primsize;\n"
|
|
"\treturn p(nm);\n"
|
|
"};\n", 1 },
|
|
/* aliasprimsize() is the SSoT wrapper — its `primsize` suffix must
|
|
* NOT trip the left-word-bounded matcher (the central evasion). */
|
|
{ "prim_alias_wrapper_ok", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(c: *cgen, nm: str) i32 = {\n"
|
|
"\tlet z: i32 = aliasprimsize(c, nm);\n"
|
|
"\treturn z;\n"
|
|
"};\n", 0 },
|
|
{ "prim_annotated_ok", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(c: *cgen, nm: str) i32 = {\n"
|
|
"\t// primsize-ok: chase body\n"
|
|
"\tlet z: i32 = primsize(nm);\n"
|
|
"\treturn z;\n"
|
|
"};\n", 0 },
|
|
{ "prim_corrupt_annotation", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(c: *cgen, nm: str) i32 = {\n"
|
|
"\t// primsize-okk-corrupt: nope\n"
|
|
"\tlet z: i32 = primsize(nm);\n"
|
|
"\treturn z;\n"
|
|
"};\n", 1 },
|
|
/* the two exemption windows are independent: primsize-ok must not
|
|
* blind an under-token peel, nor peel-ok a bare primsize. */
|
|
{ "prim_window_no_cross_under", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(t: *tinfo) *tinfo = {\n"
|
|
"\t// primsize-ok: must NOT exempt the under peel below\n"
|
|
"\treturn t.under;\n"
|
|
"};\n", 1 },
|
|
{ "peel_window_no_cross_prim", "selfhost/cmd/wcc/x.ww",
|
|
"fn f(c: *cgen, nm: str) i32 = {\n"
|
|
"\t// peel-ok: must NOT exempt the primsize below\n"
|
|
"\tlet z: i32 = primsize(nm);\n"
|
|
"\treturn z;\n"
|
|
"};\n", 1 },
|
|
/* RULE 2 is ww-only: the C stage dealiases via type_chase_named and
|
|
* has no primsize symbol — a C `primsize(` is not in scope. */
|
|
{ "prim_c_file_out_of_scope", "cmd/w6c/x.c",
|
|
"static int primsize(const char *n) { return 0; }\n"
|
|
"int g(void) { return primsize(\"u8\"); }\n", 0 },
|
|
};
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
if (getcwd(root, sizeof root) == NULL) return 1;
|
|
|
|
int total = 0, fail = 0;
|
|
char cmd[2048], scratch[256];
|
|
|
|
/* 1. The closure proof: the real tree lints clean at HEAD. */
|
|
total++;
|
|
snprintf(cmd, sizeof cmd, "sh %s/tools/peellint", root);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "peellint_gate: real tree NOT clean\n");
|
|
fail++;
|
|
}
|
|
|
|
/* 2-4. Scratch rows: negative validation + matcher regressions. */
|
|
int n = (int)(sizeof lintrows / sizeof lintrows[0]);
|
|
for (int i = 0; i < n; i++) {
|
|
total++;
|
|
snprintf(scratch, sizeof scratch, "/tmp/plint_%d_%d",
|
|
getpid(), i);
|
|
int got = lint_scratch(scratch, lintrows[i].relpath,
|
|
lintrows[i].body);
|
|
if (got != lintrows[i].wantexit) {
|
|
fprintf(stderr, "row[%s]: lint exit %d, want %d\n",
|
|
lintrows[i].label, got, lintrows[i].wantexit);
|
|
fail++;
|
|
}
|
|
}
|
|
|
|
if (fail) {
|
|
fprintf(stderr, "peellint_gate: %d/%d checks failed\n",
|
|
fail, total);
|
|
return 1;
|
|
}
|
|
printf("peellint_gate: %d/%d ok\n", total, total);
|
|
return 0;
|
|
}
|