wcc: -T test-mode collects @test fns + synthesizes entry, both stages (#15)
@test was parsed then dropped (no consumer); `ww test` needed a hand-written
main listing each test by hand, so adding a @test and forgetting the call
silently skipped it. -T makes the checker collect @test N_FNDECLs in source
order, loud-reject a user main, and append a synthetic
`export fn main() i32 { t0(); ...; return 0; }` at the install->body-check seam;
the existing cgfn emits it (cgen untouched) -> byte-identical by construction.
Mirrors harec's checker-side is_test placement.
Plan-9-lean reduction (user-sanctioned, reinstatable post-CSP): sequential,
abort/nonzero=fail; no setjmp isolation, no fnmatch filter, no file:line.
910/997 rewired from a regex scanner to driving `w6c -T` directly (thin trusted
drivers; the @test content stays ww), with a cross-stage byte-id assert on the
-T output. attest_userman/attest_badsig pin the user-main and bad-signature
rejects.
This commit is contained in:
2
Makefile
2
Makefile
@@ -2128,7 +2128,7 @@ $(BIN)/test_dyn_ww: test/wcc/996_dyn_ww.c $(BIN)/ww $(BIN)/w6l $(BIN)/w6l_ww \
|
|||||||
$(CC) $(CFLAGS) -o $@ $<
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
$(BIN)/test_at_test_ww: test/wcc/997_at_test_ww.c $(BIN)/ww_ww $(BIN)/w6c_ww \
|
$(BIN)/test_at_test_ww: test/wcc/997_at_test_ww.c $(BIN)/ww_ww $(BIN)/w6c_ww \
|
||||||
$(BIN)/w6a_ww $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN)
|
$(BIN)/w6a_ww $(BIN)/w6l_ww $(BIN)/w6c $(LIB)/libwwrt.a | $(BIN)
|
||||||
$(CC) $(CFLAGS) -o $@ $<
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
$(BIN)/test_fmt_run: test/wcc/970_fmt_run.c $(BIN)/ww $(BIN)/w6c \
|
$(BIN)/test_fmt_run: test/wcc/970_fmt_run.c $(BIN)/ww $(BIN)/w6c \
|
||||||
|
|||||||
@@ -33,10 +33,13 @@ main(int argc, char **argv)
|
|||||||
{
|
{
|
||||||
const char *src = NULL;
|
const char *src = NULL;
|
||||||
const char *out = NULL;
|
const char *out = NULL;
|
||||||
|
int testmode = 0;
|
||||||
for (int i = 1; i < argc; i++) {
|
for (int i = 1; i < argc; i++) {
|
||||||
const char *a = argv[i];
|
const char *a = argv[i];
|
||||||
if (strcmp(a, "-o") == 0 && i + 1 < argc) {
|
if (strcmp(a, "-o") == 0 && i + 1 < argc) {
|
||||||
out = argv[++i];
|
out = argv[++i];
|
||||||
|
} else if (strcmp(a, "-T") == 0) {
|
||||||
|
testmode = 1;
|
||||||
} else if (a[0] == '-') {
|
} else if (a[0] == '-') {
|
||||||
fprintf(stderr, "w6c: unknown flag %s\n", a);
|
fprintf(stderr, "w6c: unknown flag %s\n", a);
|
||||||
return 2;
|
return 2;
|
||||||
@@ -48,7 +51,7 @@ main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (src == NULL) {
|
if (src == NULL) {
|
||||||
fputs("usage: w6c [-o out.s] file.ww\n", stderr);
|
fputs("usage: w6c [-T] [-o out.s] file.ww\n", stderr);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +74,7 @@ main(int argc, char **argv)
|
|||||||
if (l.errs || p.errs) return 1;
|
if (l.errs || p.errs) return 1;
|
||||||
|
|
||||||
check_init(&c, a);
|
check_init(&c, a);
|
||||||
|
c.is_test = testmode;
|
||||||
check_file(&c, file);
|
check_file(&c, file);
|
||||||
if (c.errs) return 1;
|
if (c.errs) return 1;
|
||||||
|
|
||||||
|
|||||||
@@ -2650,6 +2650,7 @@ check_init(Checker *c, Arena *a)
|
|||||||
{
|
{
|
||||||
memset(c, 0, sizeof *c);
|
memset(c, 0, sizeof *c);
|
||||||
c->a = a;
|
c->a = a;
|
||||||
|
c->is_test = 0; /* #15: caller (w6c main) sets it after init */
|
||||||
typesinit(a);
|
typesinit(a);
|
||||||
c->top = newscope(a, NULL);
|
c->top = newscope(a, NULL);
|
||||||
c->cur = c->top;
|
c->cur = c->top;
|
||||||
@@ -2951,6 +2952,92 @@ check_file(Checker *c, Node *file)
|
|||||||
}
|
}
|
||||||
c->cur_mod = NULL;
|
c->cur_mod = NULL;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* #15 @test harness — under `w6c -T`, synthesize the entry the
|
||||||
|
* driver would otherwise hand-wire. We sit at the seam between
|
||||||
|
* fn-install (pass 1, all names now in scope so the synth callees
|
||||||
|
* resolve) and fn-body-check (pass 2 below, which stamps the
|
||||||
|
* appended entry for free). This mirrors harec's checker-side
|
||||||
|
* is_test work — keep @test fns + suppress/own the hosted main
|
||||||
|
* (ref/harec/src/check.c:3941,4000) — NOT the build driver, which
|
||||||
|
* only flips a mode bit (ref/hare/cmd/hare/build.ha:46-49). cgen is
|
||||||
|
* untouched: the appended N_FNDECL rides the existing cgfn path, so
|
||||||
|
* byte-id holds at the gated choke point by construction.
|
||||||
|
*
|
||||||
|
* D1-D5 SANCTIONED PLAN-9-LEAN REDUCTION of hare-test (user-ratified,
|
||||||
|
* reinstatable post-CSP; this is a documented departure, not a hare
|
||||||
|
* cite): D1 no __test_array linker section — straight-line calls;
|
||||||
|
* D2 call @test fns by their real symbols (no testfunc.%d rename);
|
||||||
|
* D3 no per-test setjmp/onabort isolation — run-to-completion = pass,
|
||||||
|
* abort/div0/SIGSEGV/nonzero = fail and STOPS the run; D4 no sort,
|
||||||
|
* no fnmatch filter — source/collection order; D5 no reflective
|
||||||
|
* file:line, no per-test ok/FAIL line — failure is the nonzero exit.
|
||||||
|
*/
|
||||||
|
if (c->is_test) {
|
||||||
|
Pos fp = file->pos;
|
||||||
|
/* (b) the synth entry OWNS `main` — loud-reject a user one. */
|
||||||
|
for (Node *d = file->list; d; d = d->next)
|
||||||
|
if (d->kind == N_FNDECL && d->str
|
||||||
|
&& strcmp(d->str, "main") == 0 && d->body != NULL)
|
||||||
|
err(c, d->pos, "test mode: main is synthesized "
|
||||||
|
"by -T; remove the explicit main");
|
||||||
|
/* (c) collect @test fns in file->list order, build the body. */
|
||||||
|
Node *bhead = NULL, *btail = NULL;
|
||||||
|
for (Node *d = file->list; d; d = d->next) {
|
||||||
|
if (d->kind != N_FNDECL)
|
||||||
|
continue;
|
||||||
|
int istest = 0;
|
||||||
|
for (Node *at = d->attr; at; at = at->next)
|
||||||
|
if (at->str && strcmp(at->str, "test") == 0) {
|
||||||
|
istest = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!istest)
|
||||||
|
continue;
|
||||||
|
/* `fn f() void` parses the explicit `void` into d->lhs
|
||||||
|
* (parse.c:1329), so void-returning is lhs==NULL OR an
|
||||||
|
* N_TNAME "void" — not lhs==NULL alone. */
|
||||||
|
int retvoid = d->lhs == NULL
|
||||||
|
|| (d->lhs->kind == N_TNAME && d->lhs->str
|
||||||
|
&& strcmp(d->lhs->str, "void") == 0);
|
||||||
|
if (d->list != NULL || !retvoid) {
|
||||||
|
err(c, d->pos, "@test fn '%s' must be fn() void",
|
||||||
|
d->str);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Node *call = newnode(c->a, N_CALL, fp);
|
||||||
|
call->lhs = newnode(c->a, N_IDENT, fp);
|
||||||
|
call->lhs->str = d->str;
|
||||||
|
Node *es = newnode(c->a, N_EXPRSTMT, fp);
|
||||||
|
es->lhs = call;
|
||||||
|
if (bhead == NULL) bhead = es;
|
||||||
|
else btail->next = es;
|
||||||
|
btail = es;
|
||||||
|
}
|
||||||
|
Node *ret = newnode(c->a, N_RETURN, fp);
|
||||||
|
ret->lhs = newnode(c->a, N_INTLIT, fp);
|
||||||
|
ret->lhs->uval = 0;
|
||||||
|
if (bhead == NULL) bhead = ret;
|
||||||
|
else btail->next = ret;
|
||||||
|
Node *body = newnode(c->a, N_BLOCK, fp);
|
||||||
|
body->list = bhead;
|
||||||
|
Node *m = newnode(c->a, N_FNDECL, fp);
|
||||||
|
m->str = "main";
|
||||||
|
m->export = 1;
|
||||||
|
m->lhs = newnode(c->a, N_TNAME, fp);
|
||||||
|
m->lhs->str = "i32";
|
||||||
|
m->body = body;
|
||||||
|
m->type = build_fn_type(c, m);
|
||||||
|
/* pass 1 already ran, so the install loop never stamped m's
|
||||||
|
* type; set it explicitly (pass 2 below reads d->type). */
|
||||||
|
Node *tl = file->list;
|
||||||
|
if (tl == NULL) file->list = m;
|
||||||
|
else {
|
||||||
|
while (tl->next) tl = tl->next;
|
||||||
|
tl->next = m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* pass 2: check def initialisers and fn bodies */
|
/* pass 2: check def initialisers and fn bodies */
|
||||||
for (Node *d = file->list; d; d = d->next) {
|
for (Node *d = file->list; d; d = d->next) {
|
||||||
c->cur_mod = decl_mod(file, d);
|
c->cur_mod = decl_mod(file, d);
|
||||||
|
|||||||
@@ -555,6 +555,8 @@ struct Checker {
|
|||||||
* would shadow an imported module bareword. */
|
* would shadow an imported module bareword. */
|
||||||
int loops; /* nesting count for break/continue */
|
int loops; /* nesting count for break/continue */
|
||||||
int errs;
|
int errs;
|
||||||
|
int is_test; /* #15: `w6c -T` — collect @test fns + synth
|
||||||
|
* the entry; loud-reject a user main. */
|
||||||
Node *alloc_octx; /* #3/B': the one empty `alloc([], n)` call node
|
Node *alloc_octx; /* #3/B': the one empty `alloc([], n)` call node
|
||||||
* that has let-declared slice context this walk;
|
* that has let-declared slice context this walk;
|
||||||
* any OTHER empty alloc has no element-type hint
|
* any OTHER empty alloc has no element-type hint
|
||||||
|
|||||||
@@ -10311,6 +10311,8 @@ type checker = struct {
|
|||||||
nresolved: i32,
|
nresolved: i32,
|
||||||
nunresolved: i32,
|
nunresolved: i32,
|
||||||
errs: i32,
|
errs: i32,
|
||||||
|
istest: i32, // #15: `w6c_ww -T` — collect @test fns +
|
||||||
|
// synth the entry; loud-reject a user main.
|
||||||
verbose: i32, // when non-zero, log each unresolved name
|
verbose: i32, // when non-zero, log each unresolved name
|
||||||
fnret: *node, // enclosing fn's return type AST (for `?`)
|
fnret: *node, // enclosing fn's return type AST (for `?`)
|
||||||
curmod: str, // importing-module bareword for the decl
|
curmod: str, // importing-module bareword for the decl
|
||||||
@@ -15705,6 +15707,7 @@ export fn checkinit(c: *checker, tc: *tctx) void = {
|
|||||||
c.nresolved = 0;
|
c.nresolved = 0;
|
||||||
c.nunresolved = 0;
|
c.nunresolved = 0;
|
||||||
c.errs = 0;
|
c.errs = 0;
|
||||||
|
c.istest = 0i32; // #15: caller (w6c main) sets it after init
|
||||||
c.verbose = 0;
|
c.verbose = 0;
|
||||||
c.fnret = nil;
|
c.fnret = nil;
|
||||||
let empty: str;
|
let empty: str;
|
||||||
@@ -15726,6 +15729,110 @@ export fn checkfile(c: *checker, file: *node) void = {
|
|||||||
d = d.next;
|
d = d.next;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// #15 @test harness — under `w6c_ww -T`, synthesize the entry the
|
||||||
|
// driver would otherwise hand-wire. We sit at the seam between
|
||||||
|
// fn-install (Pass 1, all names now in scope so the synth callees
|
||||||
|
// resolve) and fn-body-resolve (Pass 2 below, which stamps the
|
||||||
|
// appended entry for free). Mirrors harec's checker-side is_test
|
||||||
|
// work — keep @test fns + suppress/own the hosted main
|
||||||
|
// (ref/harec/src/check.c:3941,4000) — NOT the build driver. cgen is
|
||||||
|
// untouched: the appended N_FNDECL rides cgfn, byte-id by
|
||||||
|
// construction (rule 10; cstage twin at cmd/wcc/check.c).
|
||||||
|
//
|
||||||
|
// D1-D5 SANCTIONED PLAN-9-LEAN REDUCTION of hare-test (user-ratified,
|
||||||
|
// reinstatable post-CSP; documented departure, not a hare cite):
|
||||||
|
// D1 no __test_array section — straight-line calls; D2 call @test
|
||||||
|
// fns by their real symbols (no testfunc.%d); D3 no per-test
|
||||||
|
// setjmp/onabort isolation — run-to-completion = pass, abort/div0/
|
||||||
|
// SIGSEGV/nonzero = fail and STOPS the run; D4 no sort, no fnmatch
|
||||||
|
// filter — source/collection order; D5 no reflective file:line, no
|
||||||
|
// per-test ok/FAIL line — failure is the nonzero process exit.
|
||||||
|
if (c.istest != 0) {
|
||||||
|
let pf: str = file.file;
|
||||||
|
let pl: i32 = file.line;
|
||||||
|
let pc: i32 = file.col;
|
||||||
|
// (b) the synth entry OWNS `main` — loud-reject a user one.
|
||||||
|
let u: *node = file.list;
|
||||||
|
for (u != nil) {
|
||||||
|
if (u.kind == nkind.N_FNDECL && u.body != nil
|
||||||
|
&& streq(u.str, "main")) {
|
||||||
|
cerr(u.file);
|
||||||
|
cerr(": error: test mode: main is synthesized by -T; remove the explicit main\n");
|
||||||
|
c.errs += 1;
|
||||||
|
};
|
||||||
|
u = u.next;
|
||||||
|
};
|
||||||
|
// (c) collect @test fns in file.list order; build the body chain.
|
||||||
|
let bhead: *node = nil;
|
||||||
|
let btail: *node = nil;
|
||||||
|
let t: *node = file.list;
|
||||||
|
for (t != nil) {
|
||||||
|
if (t.kind == nkind.N_FNDECL) {
|
||||||
|
let istest: bool = false;
|
||||||
|
let at: *node = t.attr;
|
||||||
|
for (at != nil) {
|
||||||
|
if (at.kind == nkind.N_ATTR
|
||||||
|
&& streq(at.str, "test")) {
|
||||||
|
istest = true;
|
||||||
|
};
|
||||||
|
at = at.next;
|
||||||
|
};
|
||||||
|
if (istest) {
|
||||||
|
// `fn f() void` parses the explicit void
|
||||||
|
// into t.lhs, so void-returning is lhs==nil
|
||||||
|
// OR an N_TNAME "void".
|
||||||
|
let retvoid: bool = t.lhs == nil
|
||||||
|
|| (t.lhs.kind == nkind.N_TNAME
|
||||||
|
&& streq(t.lhs.str, "void"));
|
||||||
|
if (t.list != nil || !retvoid) {
|
||||||
|
cerr(t.file);
|
||||||
|
cerr(": error: @test fn '");
|
||||||
|
cerr(t.str);
|
||||||
|
cerr("' must be fn() void\n");
|
||||||
|
c.errs += 1;
|
||||||
|
} else {
|
||||||
|
let call: *node = newnode(nkind.N_CALL, pf, pl, pc);
|
||||||
|
let cid: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||||
|
cid.str = t.str;
|
||||||
|
call.lhs = cid;
|
||||||
|
let es: *node = newnode(nkind.N_EXPRSTMT, pf, pl, pc);
|
||||||
|
es.lhs = call;
|
||||||
|
if (bhead == nil) { bhead = es; }
|
||||||
|
else { btail.next = es; };
|
||||||
|
btail = es;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
t = t.next;
|
||||||
|
};
|
||||||
|
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
|
||||||
|
let zero: *node = newnode(nkind.N_INTLIT, pf, pl, pc);
|
||||||
|
zero.uval = 0u64;
|
||||||
|
ret.lhs = zero;
|
||||||
|
if (bhead == nil) { bhead = ret; } else { btail.next = ret; };
|
||||||
|
btail = ret;
|
||||||
|
let body: *node = newnode(nkind.N_BLOCK, pf, pl, pc);
|
||||||
|
body.list = bhead;
|
||||||
|
let m: *node = newnode(nkind.N_FNDECL, pf, pl, pc);
|
||||||
|
m.str = "main";
|
||||||
|
m.exported = 1i32;
|
||||||
|
let rety: *node = newnode(nkind.N_TNAME, pf, pl, pc);
|
||||||
|
rety.str = "i32";
|
||||||
|
m.lhs = rety;
|
||||||
|
m.body = body;
|
||||||
|
// Append to file.list tail. No type_ pre-set: installdecl
|
||||||
|
// never stamps a fn's type_ on the ww side either — cgfn reads
|
||||||
|
// lhs/list on demand, like every parsed fn. Pass-2 below
|
||||||
|
// resolve-walks the appended body.
|
||||||
|
let tl: *node = file.list;
|
||||||
|
if (tl == nil) {
|
||||||
|
file.list = m;
|
||||||
|
} else {
|
||||||
|
for (tl.next != nil) { tl = tl.next; };
|
||||||
|
tl.next = m;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Pass 2: walk decl bodies/types and resolve identifiers.
|
// Pass 2: walk decl bodies/types and resolve identifiers.
|
||||||
// Track the per-decl module bareword so bare-leaf lookups inside
|
// Track the per-decl module bareword so bare-leaf lookups inside
|
||||||
// the body prefer same-module entries over alphabetically-earlier
|
// the body prefer same-module entries over alphabetically-earlier
|
||||||
@@ -42878,6 +42985,7 @@ fn slurp(path: *u8) (*u8, u64) = {
|
|||||||
export fn main(argc: i32, argv: **u8) i32 = {
|
export fn main(argc: i32, argv: **u8) i32 = {
|
||||||
let src: *u8 = nil;
|
let src: *u8 = nil;
|
||||||
let out: *u8 = nil;
|
let out: *u8 = nil;
|
||||||
|
let testmode: i32 = 0i32; // #15: `-T` test-mode
|
||||||
|
|
||||||
let i: i32 = 1;
|
let i: i32 = 1;
|
||||||
for (i < argc) {
|
for (i < argc) {
|
||||||
@@ -42890,6 +42998,8 @@ export fn main(argc: i32, argv: **u8) i32 = {
|
|||||||
return 2;
|
return 2;
|
||||||
};
|
};
|
||||||
out = argv[i];
|
out = argv[i];
|
||||||
|
} else { if (cstreq(a, "-T")) {
|
||||||
|
testmode = 1i32;
|
||||||
} else { if (a[0u64] == 45u8) {
|
} else { if (a[0u64] == 45u8) {
|
||||||
let m: str = "w6c: unknown flag\n";
|
let m: str = "w6c: unknown flag\n";
|
||||||
os.write(2, m.ptr, m.len: u64);
|
os.write(2, m.ptr, m.len: u64);
|
||||||
@@ -42901,12 +43011,12 @@ export fn main(argc: i32, argv: **u8) i32 = {
|
|||||||
return 2;
|
return 2;
|
||||||
};
|
};
|
||||||
src = a;
|
src = a;
|
||||||
}; };
|
}; }; };
|
||||||
i += 1;
|
i += 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (src == nil) {
|
if (src == nil) {
|
||||||
let m: str = "usage: w6c_ww [-o out.s] file.ww\n";
|
let m: str = "usage: w6c_ww [-T] [-o out.s] file.ww\n";
|
||||||
os.write(2, m.ptr, m.len: u64);
|
os.write(2, m.ptr, m.len: u64);
|
||||||
return 2;
|
return 2;
|
||||||
};
|
};
|
||||||
@@ -42967,6 +43077,7 @@ export fn main(argc: i32, argv: **u8) i32 = {
|
|||||||
typesinit(&tc);
|
typesinit(&tc);
|
||||||
let ck: checker;
|
let ck: checker;
|
||||||
checkinit(&ck, &tc);
|
checkinit(&ck, &tc);
|
||||||
|
ck.istest = testmode;
|
||||||
checkfile(&ck, f);
|
checkfile(&ck, f);
|
||||||
if (ck.errs > 0) { return 1; };
|
if (ck.errs > 0) { return 1; };
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ fn slurp(path: *u8) (*u8, u64) = {
|
|||||||
export fn main(argc: i32, argv: **u8) i32 = {
|
export fn main(argc: i32, argv: **u8) i32 = {
|
||||||
let src: *u8 = nil;
|
let src: *u8 = nil;
|
||||||
let out: *u8 = nil;
|
let out: *u8 = nil;
|
||||||
|
let testmode: i32 = 0i32; // #15: `-T` test-mode
|
||||||
|
|
||||||
let i: i32 = 1;
|
let i: i32 = 1;
|
||||||
for (i < argc) {
|
for (i < argc) {
|
||||||
@@ -91,6 +92,8 @@ export fn main(argc: i32, argv: **u8) i32 = {
|
|||||||
return 2;
|
return 2;
|
||||||
};
|
};
|
||||||
out = argv[i];
|
out = argv[i];
|
||||||
|
} else { if (cstreq(a, "-T")) {
|
||||||
|
testmode = 1i32;
|
||||||
} else { if (a[0u64] == 45u8) {
|
} else { if (a[0u64] == 45u8) {
|
||||||
let m: str = "w6c: unknown flag\n";
|
let m: str = "w6c: unknown flag\n";
|
||||||
os.write(2, m.ptr, m.len: u64);
|
os.write(2, m.ptr, m.len: u64);
|
||||||
@@ -102,12 +105,12 @@ export fn main(argc: i32, argv: **u8) i32 = {
|
|||||||
return 2;
|
return 2;
|
||||||
};
|
};
|
||||||
src = a;
|
src = a;
|
||||||
}; };
|
}; }; };
|
||||||
i += 1;
|
i += 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (src == nil) {
|
if (src == nil) {
|
||||||
let m: str = "usage: w6c_ww [-o out.s] file.ww\n";
|
let m: str = "usage: w6c_ww [-T] [-o out.s] file.ww\n";
|
||||||
os.write(2, m.ptr, m.len: u64);
|
os.write(2, m.ptr, m.len: u64);
|
||||||
return 2;
|
return 2;
|
||||||
};
|
};
|
||||||
@@ -168,6 +171,7 @@ export fn main(argc: i32, argv: **u8) i32 = {
|
|||||||
typesinit(&tc);
|
typesinit(&tc);
|
||||||
let ck: checker;
|
let ck: checker;
|
||||||
checkinit(&ck, &tc);
|
checkinit(&ck, &tc);
|
||||||
|
ck.istest = testmode;
|
||||||
checkfile(&ck, f);
|
checkfile(&ck, f);
|
||||||
if (ck.errs > 0) { return 1; };
|
if (ck.errs > 0) { return 1; };
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ type checker = struct {
|
|||||||
nresolved: i32,
|
nresolved: i32,
|
||||||
nunresolved: i32,
|
nunresolved: i32,
|
||||||
errs: i32,
|
errs: i32,
|
||||||
|
istest: i32, // #15: `w6c_ww -T` — collect @test fns +
|
||||||
|
// synth the entry; loud-reject a user main.
|
||||||
verbose: i32, // when non-zero, log each unresolved name
|
verbose: i32, // when non-zero, log each unresolved name
|
||||||
fnret: *node, // enclosing fn's return type AST (for `?`)
|
fnret: *node, // enclosing fn's return type AST (for `?`)
|
||||||
curmod: str, // importing-module bareword for the decl
|
curmod: str, // importing-module bareword for the decl
|
||||||
@@ -5424,6 +5426,7 @@ export fn checkinit(c: *checker, tc: *tctx) void = {
|
|||||||
c.nresolved = 0;
|
c.nresolved = 0;
|
||||||
c.nunresolved = 0;
|
c.nunresolved = 0;
|
||||||
c.errs = 0;
|
c.errs = 0;
|
||||||
|
c.istest = 0i32; // #15: caller (w6c main) sets it after init
|
||||||
c.verbose = 0;
|
c.verbose = 0;
|
||||||
c.fnret = nil;
|
c.fnret = nil;
|
||||||
let empty: str;
|
let empty: str;
|
||||||
@@ -5445,6 +5448,110 @@ export fn checkfile(c: *checker, file: *node) void = {
|
|||||||
d = d.next;
|
d = d.next;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// #15 @test harness — under `w6c_ww -T`, synthesize the entry the
|
||||||
|
// driver would otherwise hand-wire. We sit at the seam between
|
||||||
|
// fn-install (Pass 1, all names now in scope so the synth callees
|
||||||
|
// resolve) and fn-body-resolve (Pass 2 below, which stamps the
|
||||||
|
// appended entry for free). Mirrors harec's checker-side is_test
|
||||||
|
// work — keep @test fns + suppress/own the hosted main
|
||||||
|
// (ref/harec/src/check.c:3941,4000) — NOT the build driver. cgen is
|
||||||
|
// untouched: the appended N_FNDECL rides cgfn, byte-id by
|
||||||
|
// construction (rule 10; cstage twin at cmd/wcc/check.c).
|
||||||
|
//
|
||||||
|
// D1-D5 SANCTIONED PLAN-9-LEAN REDUCTION of hare-test (user-ratified,
|
||||||
|
// reinstatable post-CSP; documented departure, not a hare cite):
|
||||||
|
// D1 no __test_array section — straight-line calls; D2 call @test
|
||||||
|
// fns by their real symbols (no testfunc.%d); D3 no per-test
|
||||||
|
// setjmp/onabort isolation — run-to-completion = pass, abort/div0/
|
||||||
|
// SIGSEGV/nonzero = fail and STOPS the run; D4 no sort, no fnmatch
|
||||||
|
// filter — source/collection order; D5 no reflective file:line, no
|
||||||
|
// per-test ok/FAIL line — failure is the nonzero process exit.
|
||||||
|
if (c.istest != 0) {
|
||||||
|
let pf: str = file.file;
|
||||||
|
let pl: i32 = file.line;
|
||||||
|
let pc: i32 = file.col;
|
||||||
|
// (b) the synth entry OWNS `main` — loud-reject a user one.
|
||||||
|
let u: *node = file.list;
|
||||||
|
for (u != nil) {
|
||||||
|
if (u.kind == nkind.N_FNDECL && u.body != nil
|
||||||
|
&& streq(u.str, "main")) {
|
||||||
|
cerr(u.file);
|
||||||
|
cerr(": error: test mode: main is synthesized by -T; remove the explicit main\n");
|
||||||
|
c.errs += 1;
|
||||||
|
};
|
||||||
|
u = u.next;
|
||||||
|
};
|
||||||
|
// (c) collect @test fns in file.list order; build the body chain.
|
||||||
|
let bhead: *node = nil;
|
||||||
|
let btail: *node = nil;
|
||||||
|
let t: *node = file.list;
|
||||||
|
for (t != nil) {
|
||||||
|
if (t.kind == nkind.N_FNDECL) {
|
||||||
|
let istest: bool = false;
|
||||||
|
let at: *node = t.attr;
|
||||||
|
for (at != nil) {
|
||||||
|
if (at.kind == nkind.N_ATTR
|
||||||
|
&& streq(at.str, "test")) {
|
||||||
|
istest = true;
|
||||||
|
};
|
||||||
|
at = at.next;
|
||||||
|
};
|
||||||
|
if (istest) {
|
||||||
|
// `fn f() void` parses the explicit void
|
||||||
|
// into t.lhs, so void-returning is lhs==nil
|
||||||
|
// OR an N_TNAME "void".
|
||||||
|
let retvoid: bool = t.lhs == nil
|
||||||
|
|| (t.lhs.kind == nkind.N_TNAME
|
||||||
|
&& streq(t.lhs.str, "void"));
|
||||||
|
if (t.list != nil || !retvoid) {
|
||||||
|
cerr(t.file);
|
||||||
|
cerr(": error: @test fn '");
|
||||||
|
cerr(t.str);
|
||||||
|
cerr("' must be fn() void\n");
|
||||||
|
c.errs += 1;
|
||||||
|
} else {
|
||||||
|
let call: *node = newnode(nkind.N_CALL, pf, pl, pc);
|
||||||
|
let cid: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||||
|
cid.str = t.str;
|
||||||
|
call.lhs = cid;
|
||||||
|
let es: *node = newnode(nkind.N_EXPRSTMT, pf, pl, pc);
|
||||||
|
es.lhs = call;
|
||||||
|
if (bhead == nil) { bhead = es; }
|
||||||
|
else { btail.next = es; };
|
||||||
|
btail = es;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
t = t.next;
|
||||||
|
};
|
||||||
|
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
|
||||||
|
let zero: *node = newnode(nkind.N_INTLIT, pf, pl, pc);
|
||||||
|
zero.uval = 0u64;
|
||||||
|
ret.lhs = zero;
|
||||||
|
if (bhead == nil) { bhead = ret; } else { btail.next = ret; };
|
||||||
|
btail = ret;
|
||||||
|
let body: *node = newnode(nkind.N_BLOCK, pf, pl, pc);
|
||||||
|
body.list = bhead;
|
||||||
|
let m: *node = newnode(nkind.N_FNDECL, pf, pl, pc);
|
||||||
|
m.str = "main";
|
||||||
|
m.exported = 1i32;
|
||||||
|
let rety: *node = newnode(nkind.N_TNAME, pf, pl, pc);
|
||||||
|
rety.str = "i32";
|
||||||
|
m.lhs = rety;
|
||||||
|
m.body = body;
|
||||||
|
// Append to file.list tail. No type_ pre-set: installdecl
|
||||||
|
// never stamps a fn's type_ on the ww side either — cgfn reads
|
||||||
|
// lhs/list on demand, like every parsed fn. Pass-2 below
|
||||||
|
// resolve-walks the appended body.
|
||||||
|
let tl: *node = file.list;
|
||||||
|
if (tl == nil) {
|
||||||
|
file.list = m;
|
||||||
|
} else {
|
||||||
|
for (tl.next != nil) { tl = tl.next; };
|
||||||
|
tl.next = m;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Pass 2: walk decl bodies/types and resolve identifiers.
|
// Pass 2: walk decl bodies/types and resolve identifiers.
|
||||||
// Track the per-decl module bareword so bare-leaf lookups inside
|
// Track the per-decl module bareword so bare-leaf lookups inside
|
||||||
// the body prefer same-module entries over alphabetically-earlier
|
// the body prefer same-module entries over alphabetically-earlier
|
||||||
|
|||||||
@@ -10311,6 +10311,8 @@ type checker = struct {
|
|||||||
nresolved: i32,
|
nresolved: i32,
|
||||||
nunresolved: i32,
|
nunresolved: i32,
|
||||||
errs: i32,
|
errs: i32,
|
||||||
|
istest: i32, // #15: `w6c_ww -T` — collect @test fns +
|
||||||
|
// synth the entry; loud-reject a user main.
|
||||||
verbose: i32, // when non-zero, log each unresolved name
|
verbose: i32, // when non-zero, log each unresolved name
|
||||||
fnret: *node, // enclosing fn's return type AST (for `?`)
|
fnret: *node, // enclosing fn's return type AST (for `?`)
|
||||||
curmod: str, // importing-module bareword for the decl
|
curmod: str, // importing-module bareword for the decl
|
||||||
@@ -15705,6 +15707,7 @@ export fn checkinit(c: *checker, tc: *tctx) void = {
|
|||||||
c.nresolved = 0;
|
c.nresolved = 0;
|
||||||
c.nunresolved = 0;
|
c.nunresolved = 0;
|
||||||
c.errs = 0;
|
c.errs = 0;
|
||||||
|
c.istest = 0i32; // #15: caller (w6c main) sets it after init
|
||||||
c.verbose = 0;
|
c.verbose = 0;
|
||||||
c.fnret = nil;
|
c.fnret = nil;
|
||||||
let empty: str;
|
let empty: str;
|
||||||
@@ -15726,6 +15729,110 @@ export fn checkfile(c: *checker, file: *node) void = {
|
|||||||
d = d.next;
|
d = d.next;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// #15 @test harness — under `w6c_ww -T`, synthesize the entry the
|
||||||
|
// driver would otherwise hand-wire. We sit at the seam between
|
||||||
|
// fn-install (Pass 1, all names now in scope so the synth callees
|
||||||
|
// resolve) and fn-body-resolve (Pass 2 below, which stamps the
|
||||||
|
// appended entry for free). Mirrors harec's checker-side is_test
|
||||||
|
// work — keep @test fns + suppress/own the hosted main
|
||||||
|
// (ref/harec/src/check.c:3941,4000) — NOT the build driver. cgen is
|
||||||
|
// untouched: the appended N_FNDECL rides cgfn, byte-id by
|
||||||
|
// construction (rule 10; cstage twin at cmd/wcc/check.c).
|
||||||
|
//
|
||||||
|
// D1-D5 SANCTIONED PLAN-9-LEAN REDUCTION of hare-test (user-ratified,
|
||||||
|
// reinstatable post-CSP; documented departure, not a hare cite):
|
||||||
|
// D1 no __test_array section — straight-line calls; D2 call @test
|
||||||
|
// fns by their real symbols (no testfunc.%d); D3 no per-test
|
||||||
|
// setjmp/onabort isolation — run-to-completion = pass, abort/div0/
|
||||||
|
// SIGSEGV/nonzero = fail and STOPS the run; D4 no sort, no fnmatch
|
||||||
|
// filter — source/collection order; D5 no reflective file:line, no
|
||||||
|
// per-test ok/FAIL line — failure is the nonzero process exit.
|
||||||
|
if (c.istest != 0) {
|
||||||
|
let pf: str = file.file;
|
||||||
|
let pl: i32 = file.line;
|
||||||
|
let pc: i32 = file.col;
|
||||||
|
// (b) the synth entry OWNS `main` — loud-reject a user one.
|
||||||
|
let u: *node = file.list;
|
||||||
|
for (u != nil) {
|
||||||
|
if (u.kind == nkind.N_FNDECL && u.body != nil
|
||||||
|
&& streq(u.str, "main")) {
|
||||||
|
cerr(u.file);
|
||||||
|
cerr(": error: test mode: main is synthesized by -T; remove the explicit main\n");
|
||||||
|
c.errs += 1;
|
||||||
|
};
|
||||||
|
u = u.next;
|
||||||
|
};
|
||||||
|
// (c) collect @test fns in file.list order; build the body chain.
|
||||||
|
let bhead: *node = nil;
|
||||||
|
let btail: *node = nil;
|
||||||
|
let t: *node = file.list;
|
||||||
|
for (t != nil) {
|
||||||
|
if (t.kind == nkind.N_FNDECL) {
|
||||||
|
let istest: bool = false;
|
||||||
|
let at: *node = t.attr;
|
||||||
|
for (at != nil) {
|
||||||
|
if (at.kind == nkind.N_ATTR
|
||||||
|
&& streq(at.str, "test")) {
|
||||||
|
istest = true;
|
||||||
|
};
|
||||||
|
at = at.next;
|
||||||
|
};
|
||||||
|
if (istest) {
|
||||||
|
// `fn f() void` parses the explicit void
|
||||||
|
// into t.lhs, so void-returning is lhs==nil
|
||||||
|
// OR an N_TNAME "void".
|
||||||
|
let retvoid: bool = t.lhs == nil
|
||||||
|
|| (t.lhs.kind == nkind.N_TNAME
|
||||||
|
&& streq(t.lhs.str, "void"));
|
||||||
|
if (t.list != nil || !retvoid) {
|
||||||
|
cerr(t.file);
|
||||||
|
cerr(": error: @test fn '");
|
||||||
|
cerr(t.str);
|
||||||
|
cerr("' must be fn() void\n");
|
||||||
|
c.errs += 1;
|
||||||
|
} else {
|
||||||
|
let call: *node = newnode(nkind.N_CALL, pf, pl, pc);
|
||||||
|
let cid: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||||
|
cid.str = t.str;
|
||||||
|
call.lhs = cid;
|
||||||
|
let es: *node = newnode(nkind.N_EXPRSTMT, pf, pl, pc);
|
||||||
|
es.lhs = call;
|
||||||
|
if (bhead == nil) { bhead = es; }
|
||||||
|
else { btail.next = es; };
|
||||||
|
btail = es;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
t = t.next;
|
||||||
|
};
|
||||||
|
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
|
||||||
|
let zero: *node = newnode(nkind.N_INTLIT, pf, pl, pc);
|
||||||
|
zero.uval = 0u64;
|
||||||
|
ret.lhs = zero;
|
||||||
|
if (bhead == nil) { bhead = ret; } else { btail.next = ret; };
|
||||||
|
btail = ret;
|
||||||
|
let body: *node = newnode(nkind.N_BLOCK, pf, pl, pc);
|
||||||
|
body.list = bhead;
|
||||||
|
let m: *node = newnode(nkind.N_FNDECL, pf, pl, pc);
|
||||||
|
m.str = "main";
|
||||||
|
m.exported = 1i32;
|
||||||
|
let rety: *node = newnode(nkind.N_TNAME, pf, pl, pc);
|
||||||
|
rety.str = "i32";
|
||||||
|
m.lhs = rety;
|
||||||
|
m.body = body;
|
||||||
|
// Append to file.list tail. No type_ pre-set: installdecl
|
||||||
|
// never stamps a fn's type_ on the ww side either — cgfn reads
|
||||||
|
// lhs/list on demand, like every parsed fn. Pass-2 below
|
||||||
|
// resolve-walks the appended body.
|
||||||
|
let tl: *node = file.list;
|
||||||
|
if (tl == nil) {
|
||||||
|
file.list = m;
|
||||||
|
} else {
|
||||||
|
for (tl.next != nil) { tl = tl.next; };
|
||||||
|
tl.next = m;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Pass 2: walk decl bodies/types and resolve identifiers.
|
// Pass 2: walk decl bodies/types and resolve identifiers.
|
||||||
// Track the per-decl module bareword so bare-leaf lookups inside
|
// Track the per-decl module bareword so bare-leaf lookups inside
|
||||||
// the body prefer same-module entries over alphabetically-earlier
|
// the body prefer same-module entries over alphabetically-earlier
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
/*
|
/*
|
||||||
* 910_at_test — runs @test fn blocks in a source file by
|
* 910_at_test — drives the compiler's `-T` @test mode (task #15).
|
||||||
* generating a synthetic main() that calls each one. A test
|
|
||||||
* passes iff the generated binary exits 0; if any @test calls
|
|
||||||
* abort/exit(1), the run fails.
|
|
||||||
*
|
*
|
||||||
* Scanning is regex-free, matching the literal sequence
|
* Under `w6c -T`, the checker collects every @test fn and synthesizes
|
||||||
* `\n@test\s+fn\s+(IDENT)`. Same parser shape as ww accepts.
|
* `export fn main() i32 { t0(); t1(); ...; return 0; }`, calling each in
|
||||||
|
* source order; a test that runs to completion passes, an abort/div0/
|
||||||
|
* nonzero exit FAILS the run. This replaces the old driver-side regex
|
||||||
|
* scan (find_attest) — the @test set now comes from the AST, the single
|
||||||
|
* source of truth (rob #15 ruling). cgen is untouched: the synthesized
|
||||||
|
* entry rides the existing cgfn path.
|
||||||
|
*
|
||||||
|
* Probes:
|
||||||
|
* 1. RUN — `w6c -T` the @test-only fixture, assemble, link, run; the
|
||||||
|
* synthesized entry must exit 0 (every @test passed). Imports are
|
||||||
|
* resolved by `ww build` into a *.combined.ww (the fixture uses
|
||||||
|
* `alloc`, whose rt_malloc symbol the driver concatenates); we then
|
||||||
|
* compile that combined unit with `-T`.
|
||||||
|
* 2. REJECT — `-T` of a fixture that also defines a user `main` must
|
||||||
|
* exit nonzero (the synth entry owns main).
|
||||||
|
* 3. GUARD — `-T` of a fixture whose @test fn is not `fn() void` must
|
||||||
|
* exit nonzero (rule-7 loud, no silent skip/coerce).
|
||||||
*/
|
*/
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
|
||||||
#include <ctype.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
|
|
||||||
@@ -23,149 +34,104 @@ runwait(const char *cmd)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *
|
||||||
|
absbin(void)
|
||||||
|
{
|
||||||
|
const char *b = getenv("BIN");
|
||||||
|
if (!b) b = "out/bin";
|
||||||
|
if (b[0] == '/') return b;
|
||||||
|
static char buf[2048];
|
||||||
|
char cwd[1024];
|
||||||
|
if (getcwd(cwd, sizeof cwd) == NULL) return NULL;
|
||||||
|
snprintf(buf, sizeof buf, "%s/%s", cwd, b);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* run_fixture — `<comp> -T` the @test fixture (import-resolved into a
|
||||||
|
* combined unit by <drv> build), assemble, link, run; the synthesized
|
||||||
|
* entry must exit 0. Returns 0 on success. */
|
||||||
static int
|
static int
|
||||||
isws(int c)
|
run_fixture(const char *bin, const char *comp, const char *drv)
|
||||||
{
|
{
|
||||||
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
|
int pid = getpid();
|
||||||
}
|
char src[256], comb[256], asmf[256], obj[256], exe[256];
|
||||||
|
char rt[1024], cmd[4096];
|
||||||
|
snprintf(src, sizeof src, "/tmp/at910_%s_%d.ww", comp, pid);
|
||||||
|
snprintf(comb, sizeof comb, "/tmp/at910_%s_%d.combined.ww", comp, pid);
|
||||||
|
snprintf(asmf, sizeof asmf, "/tmp/at910_%s_%d.s", comp, pid);
|
||||||
|
snprintf(obj, sizeof obj, "/tmp/at910_%s_%d.o", comp, pid);
|
||||||
|
snprintf(exe, sizeof exe, "/tmp/at910_%s_%d.exe", comp, pid);
|
||||||
|
snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin);
|
||||||
|
|
||||||
static int
|
snprintf(cmd, sizeof cmd, "cp test/wcc/data/attest_pass.ww %s", src);
|
||||||
isident(int c)
|
if (runwait(cmd) != 0) { fprintf(stderr, "910 FAIL: cp\n"); return 1; }
|
||||||
{
|
|
||||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
||||||
(c >= '0' && c <= '9') || c == '_';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* find_attest — scan src for `@test\s+fn\s+(ident)`; on a match,
|
/* Driver resolves `import rt` into <src>.combined.ww. The build
|
||||||
* write the identifier into `out` (NUL-terminated) and return the
|
* itself exits nonzero (the @test-only fixture has no main — that
|
||||||
* scan position past the matched ident. Returns -1 when no more. */
|
* is exactly what -T synthesizes), but the combined unit is written
|
||||||
static long
|
* before any compile step, so we depend on the ARTIFACT, not the
|
||||||
find_attest(const char *src, long pos, char *out, size_t outsz)
|
* exit code. */
|
||||||
{
|
snprintf(cmd, sizeof cmd, "%s/%s build %s > /dev/null 2>&1", bin, drv, src);
|
||||||
long n = (long)strlen(src);
|
runwait(cmd);
|
||||||
while (pos < n) {
|
if (access(comb, 0) != 0) {
|
||||||
const char *q = strstr(src + pos, "@test");
|
fprintf(stderr, "910 FAIL: %s build produced no %s\n", drv, comb);
|
||||||
if (q == NULL) return -1;
|
return 1;
|
||||||
long off = q - src;
|
|
||||||
/* must be a top-level marker: previous non-space char is
|
|
||||||
* '\n' or we're at the file start. */
|
|
||||||
long pre = off - 1;
|
|
||||||
while (pre >= 0 && (src[pre] == ' ' || src[pre] == '\t')) pre--;
|
|
||||||
if (pre >= 0 && src[pre] != '\n') {
|
|
||||||
pos = off + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
long c = off + 5; /* past "@test" */
|
|
||||||
while (c < n && isws(src[c])) c++;
|
|
||||||
if (c + 2 > n || strncmp(src + c, "fn", 2) != 0) {
|
|
||||||
pos = off + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
c += 2;
|
|
||||||
if (c < n && isident(src[c])) {
|
|
||||||
pos = off + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
while (c < n && isws(src[c])) c++;
|
|
||||||
long ids = c;
|
|
||||||
while (c < n && isident(src[c])) c++;
|
|
||||||
if (c == ids) {
|
|
||||||
pos = off + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
size_t len = (size_t)(c - ids);
|
|
||||||
if (len + 1 > outsz) return -1;
|
|
||||||
memcpy(out, src + ids, len);
|
|
||||||
out[len] = '\0';
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static char *
|
|
||||||
slurp(const char *path)
|
|
||||||
{
|
|
||||||
FILE *f = fopen(path, "rb");
|
|
||||||
if (!f) return NULL;
|
|
||||||
fseek(f, 0, SEEK_END);
|
|
||||||
long n = ftell(f);
|
|
||||||
fseek(f, 0, SEEK_SET);
|
|
||||||
char *b = malloc((size_t)n + 1);
|
|
||||||
if (!b) { fclose(f); return NULL; }
|
|
||||||
if (fread(b, 1, (size_t)n, f) != (size_t)n) {
|
|
||||||
free(b); fclose(f); return NULL;
|
|
||||||
}
|
|
||||||
b[n] = '\0';
|
|
||||||
fclose(f);
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
runtests(const char *path, const char *bin)
|
|
||||||
{
|
|
||||||
char *src = slurp(path);
|
|
||||||
if (!src) {
|
|
||||||
fprintf(stderr, "910 FAIL: cannot read %s\n", path);
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Build a synthetic test driver: original source + a main()
|
snprintf(cmd, sizeof cmd, "%s/%s -T %s -o %s 2>/dev/null", bin, comp, comb, asmf);
|
||||||
* that calls each @test fn. Each test that runs to completion
|
if (runwait(cmd) != 0) {
|
||||||
* (no abort/exit) counts as a pass. */
|
fprintf(stderr, "910 FAIL: %s -T errored\n", comp);
|
||||||
char tmpsrc[256];
|
return 1;
|
||||||
snprintf(tmpsrc, sizeof tmpsrc, "/tmp/wwd_attest_%d.ww", getpid());
|
|
||||||
FILE *out = fopen(tmpsrc, "wb");
|
|
||||||
if (!out) { free(src); return -1; }
|
|
||||||
fputs(src, out);
|
|
||||||
fputs("\nexport fn main() i32 = {\n", out);
|
|
||||||
|
|
||||||
long pos = 0;
|
|
||||||
int count = 0;
|
|
||||||
for (;;) {
|
|
||||||
char name[128];
|
|
||||||
long next = find_attest(src, pos, name, sizeof name);
|
|
||||||
if (next < 0) break;
|
|
||||||
fprintf(out, "\t%s();\n", name);
|
|
||||||
pos = next;
|
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
fputs("\treturn 0;\n};\n", out);
|
snprintf(cmd, sizeof cmd, "%s/w6a -o %s %s 2>/dev/null", bin, obj, asmf);
|
||||||
fclose(out);
|
if (runwait(cmd) != 0) { fprintf(stderr, "910 FAIL: w6a\n"); return 1; }
|
||||||
free(src);
|
snprintf(cmd, sizeof cmd, "%s/w6l -o %s %s %s 2>/dev/null", bin, exe, obj, rt);
|
||||||
|
if (runwait(cmd) != 0) { fprintf(stderr, "910 FAIL: w6l\n"); return 1; }
|
||||||
|
|
||||||
if (count == 0) {
|
int rc = runwait(exe);
|
||||||
fprintf(stderr, "910 FAIL: no @test fns found in %s\n", path);
|
|
||||||
unlink(tmpsrc);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
char cmd[2048];
|
|
||||||
snprintf(cmd, sizeof cmd, "%s/ww run %s 2>/dev/null", bin, tmpsrc);
|
|
||||||
int rc = runwait(cmd);
|
|
||||||
unlink(tmpsrc);
|
|
||||||
if (rc != 0) {
|
if (rc != 0) {
|
||||||
fprintf(stderr, "910 FAIL: %s: %d @test fn(s), driver exited %d\n",
|
fprintf(stderr, "910 FAIL: synth entry exited %d (a @test failed)\n", rc);
|
||||||
path, count, rc);
|
return 1;
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
return count;
|
unlink(src); unlink(comb); unlink(asmf); unlink(obj); unlink(exe);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* reject — `<comp> -T <fixture>` must exit nonzero. */
|
||||||
|
static int
|
||||||
|
reject(const char *bin, const char *comp, const char *fixture, const char *what)
|
||||||
|
{
|
||||||
|
char cmd[4096];
|
||||||
|
snprintf(cmd, sizeof cmd, "%s/%s -T %s -o /dev/null 2>/dev/null",
|
||||||
|
bin, comp, fixture);
|
||||||
|
if (runwait(cmd) == 0) {
|
||||||
|
fprintf(stderr, "910 FAIL: %s -T accepted %s (expected reject)\n",
|
||||||
|
comp, what);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Each row: a fixture `-T` must loud-reject, and why. */
|
||||||
|
static const struct {
|
||||||
|
const char *fixture;
|
||||||
|
const char *what;
|
||||||
|
} rejects[] = {
|
||||||
|
{ "test/wcc/data/attest_userman.ww", "user main" },
|
||||||
|
{ "test/wcc/data/attest_badsig.ww", "non-void @test" },
|
||||||
|
};
|
||||||
|
|
||||||
int
|
int
|
||||||
main(void)
|
main(void)
|
||||||
{
|
{
|
||||||
const char *bin = getenv("BIN");
|
const char *bin = absbin();
|
||||||
if (!bin) bin = "out/bin";
|
if (!bin) { fprintf(stderr, "910 FAIL: getcwd\n"); return 1; }
|
||||||
const char *files[] = {
|
|
||||||
"test/wcc/data/attest_pass.ww",
|
if (run_fixture(bin, "w6c", "ww") != 0) return 1;
|
||||||
NULL,
|
for (size_t i = 0; i < sizeof rejects / sizeof rejects[0]; i++)
|
||||||
};
|
if (reject(bin, "w6c", rejects[i].fixture, rejects[i].what) != 0)
|
||||||
int total = 0;
|
return 1;
|
||||||
for (int i = 0; files[i]; i++) {
|
|
||||||
int n = runtests(files[i], bin);
|
printf("@test -T: run ok + user-main and bad-signature rejected\n");
|
||||||
if (n < 0) return 1;
|
|
||||||
total += n;
|
|
||||||
}
|
|
||||||
printf("@test: %d test(s) ran ok\n", total);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
/*
|
/*
|
||||||
* 997_at_test_ww — same as 910_at_test, driven through `ww_ww`
|
* 997_at_test_ww — the wwstage twin of 910_at_test, driving the `-T`
|
||||||
* (the selfhost driver) instead of `ww` (the C driver). Confirms
|
* @test mode through `w6c_ww` (the ww-built compiler) instead of `w6c`
|
||||||
* the ww-built toolchain parses, compiles, and runs an @test
|
* (the C bootstrap). Task #15.
|
||||||
* fixture end-to-end.
|
|
||||||
*
|
*
|
||||||
* Scanning is regex-free, matching the literal sequence
|
* Probes:
|
||||||
* `\n@test\s+fn\s+(IDENT)`. Same parser shape as ww accepts.
|
* 1. RUN — `w6c_ww -T` the @test-only fixture (import-resolved by
|
||||||
|
* `ww_ww build` into a combined unit), assemble, link, run; the
|
||||||
|
* synthesized entry must exit 0.
|
||||||
|
* 2. BYTE-ID (rule 10) — `w6c -T` and `w6c_ww -T` must emit
|
||||||
|
* byte-identical asm for the same source. The synthesized entry
|
||||||
|
* rides cgfn at the gated choke point, so the @test set + the
|
||||||
|
* collection-order calls are identical by construction. This is
|
||||||
|
* the new entry-synth byte-id gate.
|
||||||
|
* 3. REJECT — `-T` of a user-main fixture exits nonzero.
|
||||||
|
* 4. GUARD — `-T` of a non-`fn() void` @test exits nonzero.
|
||||||
*/
|
*/
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -22,149 +30,144 @@ runwait(const char *cmd)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static const char *
|
||||||
isws(int c)
|
absbin(void)
|
||||||
{
|
{
|
||||||
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
|
const char *b = getenv("BIN");
|
||||||
|
if (!b) b = "out/bin";
|
||||||
|
if (b[0] == '/') return b;
|
||||||
|
static char buf[2048];
|
||||||
|
char cwd[1024];
|
||||||
|
if (getcwd(cwd, sizeof cwd) == NULL) return NULL;
|
||||||
|
snprintf(buf, sizeof buf, "%s/%s", cwd, b);
|
||||||
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
isident(int c)
|
slurp(const char *path, char **outbuf, size_t *outlen)
|
||||||
{
|
|
||||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
||||||
(c >= '0' && c <= '9') || c == '_';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* find_attest — scan src for `@test\s+fn\s+(ident)`; on a match,
|
|
||||||
* write the identifier into `out` (NUL-terminated) and return the
|
|
||||||
* scan position past the matched ident. Returns -1 when no more. */
|
|
||||||
static long
|
|
||||||
find_attest(const char *src, long pos, char *out, size_t outsz)
|
|
||||||
{
|
|
||||||
long n = (long)strlen(src);
|
|
||||||
while (pos < n) {
|
|
||||||
const char *q = strstr(src + pos, "@test");
|
|
||||||
if (q == NULL) return -1;
|
|
||||||
long off = q - src;
|
|
||||||
/* must be a top-level marker: previous non-space char is
|
|
||||||
* '\n' or we're at the file start. */
|
|
||||||
long pre = off - 1;
|
|
||||||
while (pre >= 0 && (src[pre] == ' ' || src[pre] == '\t')) pre--;
|
|
||||||
if (pre >= 0 && src[pre] != '\n') {
|
|
||||||
pos = off + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
long c = off + 5; /* past "@test" */
|
|
||||||
while (c < n && isws(src[c])) c++;
|
|
||||||
if (c + 2 > n || strncmp(src + c, "fn", 2) != 0) {
|
|
||||||
pos = off + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
c += 2;
|
|
||||||
if (c < n && isident(src[c])) {
|
|
||||||
pos = off + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
while (c < n && isws(src[c])) c++;
|
|
||||||
long ids = c;
|
|
||||||
while (c < n && isident(src[c])) c++;
|
|
||||||
if (c == ids) {
|
|
||||||
pos = off + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
size_t len = (size_t)(c - ids);
|
|
||||||
if (len + 1 > outsz) return -1;
|
|
||||||
memcpy(out, src + ids, len);
|
|
||||||
out[len] = '\0';
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static char *
|
|
||||||
slurp(const char *path)
|
|
||||||
{
|
{
|
||||||
FILE *f = fopen(path, "rb");
|
FILE *f = fopen(path, "rb");
|
||||||
if (!f) return NULL;
|
if (!f) return -1;
|
||||||
fseek(f, 0, SEEK_END);
|
fseek(f, 0, SEEK_END);
|
||||||
long n = ftell(f);
|
long n = ftell(f);
|
||||||
fseek(f, 0, SEEK_SET);
|
fseek(f, 0, SEEK_SET);
|
||||||
|
if (n < 0) { fclose(f); return -1; }
|
||||||
char *b = malloc((size_t)n + 1);
|
char *b = malloc((size_t)n + 1);
|
||||||
if (!b) { fclose(f); return NULL; }
|
if (!b) { fclose(f); return -1; }
|
||||||
if (fread(b, 1, (size_t)n, f) != (size_t)n) {
|
if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; }
|
||||||
free(b); fclose(f); return NULL;
|
|
||||||
}
|
|
||||||
b[n] = '\0';
|
b[n] = '\0';
|
||||||
fclose(f);
|
fclose(f);
|
||||||
return b;
|
*outbuf = b;
|
||||||
|
*outlen = (size_t)n;
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
runtests(const char *path, const char *bin)
|
run_fixture(const char *bin)
|
||||||
{
|
{
|
||||||
char *src = slurp(path);
|
int pid = getpid();
|
||||||
if (!src) {
|
char src[256], comb[256], asmf[256], obj[256], exe[256];
|
||||||
fprintf(stderr, "997 FAIL: cannot read %s\n", path);
|
char rt[1024], cmd[4096];
|
||||||
return -1;
|
snprintf(src, sizeof src, "/tmp/at997_%d.ww", pid);
|
||||||
|
snprintf(comb, sizeof comb, "/tmp/at997_%d.combined.ww", pid);
|
||||||
|
snprintf(asmf, sizeof asmf, "/tmp/at997_%d.s", pid);
|
||||||
|
snprintf(obj, sizeof obj, "/tmp/at997_%d.o", pid);
|
||||||
|
snprintf(exe, sizeof exe, "/tmp/at997_%d.exe", pid);
|
||||||
|
snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin);
|
||||||
|
|
||||||
|
snprintf(cmd, sizeof cmd, "cp test/wcc/data/attest_pass.ww %s", src);
|
||||||
|
if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: cp\n"); return 1; }
|
||||||
|
|
||||||
|
/* ww_ww build resolves `import rt` into the combined unit; it exits
|
||||||
|
* nonzero (no main — that is what -T synthesizes), so we gate on the
|
||||||
|
* combined ARTIFACT existing, not the exit code. */
|
||||||
|
snprintf(cmd, sizeof cmd, "%s/ww_ww build %s > /dev/null 2>&1", bin, src);
|
||||||
|
runwait(cmd);
|
||||||
|
if (access(comb, 0) != 0) {
|
||||||
|
fprintf(stderr, "997 FAIL: ww_ww build produced no %s\n", comb);
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Build a synthetic test driver: original source + a main()
|
snprintf(cmd, sizeof cmd, "%s/w6c_ww -T %s -o %s 2>/dev/null", bin, comb, asmf);
|
||||||
* that calls each @test fn. Each test that runs to completion
|
if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6c_ww -T errored\n"); return 1; }
|
||||||
* (no abort/exit) counts as a pass. */
|
snprintf(cmd, sizeof cmd, "%s/w6a_ww -o %s %s 2>/dev/null", bin, obj, asmf);
|
||||||
char tmpsrc[256];
|
if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6a_ww\n"); return 1; }
|
||||||
snprintf(tmpsrc, sizeof tmpsrc, "/tmp/wwd_attest_ww_%d.ww", getpid());
|
snprintf(cmd, sizeof cmd, "%s/w6l_ww -o %s %s %s 2>/dev/null", bin, exe, obj, rt);
|
||||||
FILE *out = fopen(tmpsrc, "wb");
|
if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6l_ww\n"); return 1; }
|
||||||
if (!out) { free(src); return -1; }
|
|
||||||
fputs(src, out);
|
|
||||||
fputs("\nexport fn main() i32 = {\n", out);
|
|
||||||
|
|
||||||
long pos = 0;
|
int rc = runwait(exe);
|
||||||
int count = 0;
|
|
||||||
for (;;) {
|
|
||||||
char name[128];
|
|
||||||
long next = find_attest(src, pos, name, sizeof name);
|
|
||||||
if (next < 0) break;
|
|
||||||
fprintf(out, "\t%s();\n", name);
|
|
||||||
pos = next;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
fputs("\treturn 0;\n};\n", out);
|
|
||||||
fclose(out);
|
|
||||||
free(src);
|
|
||||||
|
|
||||||
if (count == 0) {
|
|
||||||
fprintf(stderr, "997 FAIL: no @test fns found in %s\n", path);
|
|
||||||
unlink(tmpsrc);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
char cmd[2048];
|
|
||||||
snprintf(cmd, sizeof cmd, "%s/ww_ww run %s 2>/dev/null", bin, tmpsrc);
|
|
||||||
int rc = runwait(cmd);
|
|
||||||
unlink(tmpsrc);
|
|
||||||
if (rc != 0) {
|
if (rc != 0) {
|
||||||
fprintf(stderr, "997 FAIL: %s: %d @test fn(s), driver exited %d\n",
|
fprintf(stderr, "997 FAIL: synth entry exited %d (a @test failed)\n", rc);
|
||||||
path, count, rc);
|
return 1;
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
return count;
|
unlink(src); unlink(comb); unlink(asmf); unlink(obj); unlink(exe);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* byteid — `w6c -T` and `w6c_ww -T` on the same source must agree. */
|
||||||
|
static int
|
||||||
|
byteid(const char *bin)
|
||||||
|
{
|
||||||
|
int pid = getpid();
|
||||||
|
char cs[256], ws[256], cmd[4096];
|
||||||
|
snprintf(cs, sizeof cs, "/tmp/at997_bid_c_%d.s", pid);
|
||||||
|
snprintf(ws, sizeof ws, "/tmp/at997_bid_w_%d.s", pid);
|
||||||
|
|
||||||
|
snprintf(cmd, sizeof cmd, "%s/w6c -T test/wcc/data/attest_pass.ww -o %s 2>/dev/null",
|
||||||
|
bin, cs);
|
||||||
|
if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6c -T (byteid)\n"); return 1; }
|
||||||
|
snprintf(cmd, sizeof cmd, "%s/w6c_ww -T test/wcc/data/attest_pass.ww -o %s 2>/dev/null",
|
||||||
|
bin, ws);
|
||||||
|
if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6c_ww -T (byteid)\n"); return 1; }
|
||||||
|
|
||||||
|
char *bc = NULL, *bw = NULL;
|
||||||
|
size_t nc = 0, nw = 0;
|
||||||
|
int rc = 0;
|
||||||
|
if (slurp(cs, &bc, &nc) < 0 || slurp(ws, &bw, &nw) < 0) {
|
||||||
|
fprintf(stderr, "997 FAIL: slurp byteid asm\n");
|
||||||
|
rc = 1;
|
||||||
|
} else if (nc != nw || memcmp(bc, bw, nc) != 0) {
|
||||||
|
fprintf(stderr, "997 FAIL: -T asm differs (cs %zu, ww %zu)\n", nc, nw);
|
||||||
|
rc = 1;
|
||||||
|
}
|
||||||
|
free(bc); free(bw);
|
||||||
|
unlink(cs); unlink(ws);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
reject(const char *bin, const char *fixture, const char *what)
|
||||||
|
{
|
||||||
|
char cmd[4096];
|
||||||
|
snprintf(cmd, sizeof cmd, "%s/w6c_ww -T %s -o /dev/null 2>/dev/null", bin, fixture);
|
||||||
|
if (runwait(cmd) == 0) {
|
||||||
|
fprintf(stderr, "997 FAIL: w6c_ww -T accepted %s (expected reject)\n", what);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Each row: a fixture `w6c_ww -T` must loud-reject, and why. */
|
||||||
|
static const struct {
|
||||||
|
const char *fixture;
|
||||||
|
const char *what;
|
||||||
|
} rejects[] = {
|
||||||
|
{ "test/wcc/data/attest_userman.ww", "user main" },
|
||||||
|
{ "test/wcc/data/attest_badsig.ww", "non-void @test" },
|
||||||
|
};
|
||||||
|
|
||||||
int
|
int
|
||||||
main(void)
|
main(void)
|
||||||
{
|
{
|
||||||
const char *bin = getenv("BIN");
|
const char *bin = absbin();
|
||||||
if (!bin) bin = "out/bin";
|
if (!bin) { fprintf(stderr, "997 FAIL: getcwd\n"); return 1; }
|
||||||
const char *files[] = {
|
|
||||||
"test/wcc/data/attest_pass.ww",
|
if (run_fixture(bin) != 0) return 1;
|
||||||
NULL,
|
if (byteid(bin) != 0) return 1;
|
||||||
};
|
for (size_t i = 0; i < sizeof rejects / sizeof rejects[0]; i++)
|
||||||
int total = 0;
|
if (reject(bin, rejects[i].fixture, rejects[i].what) != 0)
|
||||||
for (int i = 0; files[i]; i++) {
|
return 1;
|
||||||
int n = runtests(files[i], bin);
|
|
||||||
if (n < 0) return 1;
|
printf("@test -T (ww_ww): run ok + cs/ww byte-id + rejects ok\n");
|
||||||
total += n;
|
|
||||||
}
|
|
||||||
printf("@test (ww_ww): %d test(s) ran ok\n", total);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
18
test/wcc/data/attest_badsig.ww
Normal file
18
test/wcc/data/attest_badsig.ww
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// #15 negative fixture: an @test fn with a non-`fn() void` signature.
|
||||||
|
// The collector must loud-reject (nonzero exit) rather than silently
|
||||||
|
// skip or coerce — rule-7. Pins the signature guard on both stages.
|
||||||
|
//
|
||||||
|
// TEETH: a non-void RETURN with NO params is the case ONLY the explicit
|
||||||
|
// guard catches. The synth entry emits `bad();` (arg count matches 0
|
||||||
|
// params) and a discarded non-void return is silently allowed, so
|
||||||
|
// without the guard this compiles clean (exit 0). A param-bearing @test
|
||||||
|
// would instead trip the checker's independent "not enough arguments"
|
||||||
|
// (check.c:1879) on the synth `bad()` call — so a param fixture would
|
||||||
|
// pass the reject probe whether or not the guard exists, isolating
|
||||||
|
// nothing. Hence the no-param / non-void-return shape.
|
||||||
|
|
||||||
|
package data;
|
||||||
|
|
||||||
|
@test fn bad() i32 = {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
17
test/wcc/data/attest_userman.ww
Normal file
17
test/wcc/data/attest_userman.ww
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// #15 negative fixture: @test fns PLUS an explicit user `main`. Under
|
||||||
|
// `-T` the entry is synthesized, so a hand-written main collides — both
|
||||||
|
// stages MUST loud-reject (nonzero exit), mirroring harec's hosted-main
|
||||||
|
// suppression under is_test (ref/harec/src/check.c:4000).
|
||||||
|
|
||||||
|
package data;
|
||||||
|
|
||||||
|
@test fn check_ok() void = {
|
||||||
|
let a: i32 = 1;
|
||||||
|
if (a != 1) {
|
||||||
|
let _: i32 = 1 / 0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export fn main() i32 = {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user