/* * 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 #include #include #include #include #include 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; }