w6c+w6c_ww: free() compiles to a no-op (ww has no free) (fix #27)

The free(x) builtin lowered to CALL ffi_resolve("free") in cstage and
fell through to a generic CALL free in wwstage (which had no free arm
at all) -- an undefined reference at w6l unless an @symbol decl
happened to be in scope. ww has no free by design (rt/alloc.s:30 --
the bump allocator cannot reclaim a mid-chunk pointer; process exit
does), so both stages now evaluate the operand for side effects
(Hare's free(expr) evaluates expr) and emit nothing else, letting
Hare code that calls free() port verbatim (regex fold-2b calls it at
4+ sites). The 2-arg os.free(p, n) public API is untouched: the
builtin gate requires exactly one bare-ident-callee arg.

930_free_noop_run pins per row: w6c/w6c_ww byte-id, no free symbol
in the .s, deref-after-free validity, and the operand side effect
running once per free() via a global counter.
This commit is contained in:
2026-06-04 06:07:18 +09:00
parent 64f6cc90f2
commit 9732061a7e
6 changed files with 302 additions and 23 deletions

View File

@@ -317,6 +317,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_modcall_widen_slice \
$(BIN)/test_match_4arm_cross_module \
$(BIN)/test_match_4arm_cross_module_run \
$(BIN)/test_free_noop_run \
$(BIN)/test_variant_typekey_run \
$(BIN)/test_enum_modshadow \
$(BIN)/test_struct_modshadow \
@@ -1525,6 +1526,12 @@ $(BIN)/test_match_4arm_cross_module_run: test/wcc/929_match_4arm_cross_module_ru
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_free_noop_run: test/wcc/930_free_noop_run.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_variant_typekey_run: test/wcc/931_variant_typekey_run.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \

View File

@@ -6241,29 +6241,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "free") == 0 && n->list &&
n->list->next == NULL) {
Node *p = n->list;
Type *pt = p->type;
Type *u = (pt && pt->kind == TY_NAMED) ? pt->under : pt;
if (u && u->kind == TY_PTR && u->sub) {
int sz = (int)u->sub->size;
if (sz == 0) sz = 8;
cgexpr(c, p, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
ins2(c, A_MOVQ, aimm(sz), areg(D_SI));
ins1(c, A_CALL, asym(ffi_resolve("free")));
} else if (u && u->kind == TY_SLICE
&& p->kind == N_IDENT) {
int off = localfind(locals, p->str);
int esz = (int)(u->sub ? u->sub->size : 1);
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_AX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_BX));
ins2(c, A_IMULQ, areg(D_BX), areg(D_AX));
}
ins2(c, A_MOVQ, areg(D_AX), areg(D_SI));
ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_DI));
ins1(c, A_CALL, asym(ffi_resolve("free")));
}
/* free(x) is a no-op: ww has no free by design
* (rt/alloc.s:30 — the bump allocator cannot reclaim
* a mid-chunk pointer; process exit does). The old
* CALL ffi_resolve("free") was an undefined reference
* unless an @symbol decl happened to be in scope (#27).
* The operand is still evaluated — Hare's free(expr)
* evaluates expr — so Hare code ports verbatim with
* its side effects intact. */
cgexpr(c, n->list, locals);
break;
}
if (n->lhs && n->lhs->kind == N_IDENT &&

View File

@@ -24634,6 +24634,22 @@ fn cgcall(c: *cgen, n: *node) void = {
return;
};
};
// free(x) — documented NO-OP, mirror of cstage's cgexpr
// N_CALL free arm (#27): ww has no free by design
// (rt/alloc.s:30 — the bump allocator cannot reclaim a
// mid-chunk pointer; process exit does). The operand is
// still evaluated — Hare's free(expr) evaluates expr —
// so Hare code ports verbatim with its side effects
// intact. Pre-#27 wwstage fell through to a generic
// CALL free → undefined reference at link.
if (streq(callee.str, "free")) {
if (n.list != nil) {
if (n.list.next == nil) {
cgexpr(c, n.list);
return;
};
};
};
};
};

View File

@@ -4764,6 +4764,22 @@ fn cgcall(c: *cgen, n: *node) void = {
return;
};
};
// free(x) — documented NO-OP, mirror of cstage's cgexpr
// N_CALL free arm (#27): ww has no free by design
// (rt/alloc.s:30 — the bump allocator cannot reclaim a
// mid-chunk pointer; process exit does). The operand is
// still evaluated — Hare's free(expr) evaluates expr —
// so Hare code ports verbatim with its side effects
// intact. Pre-#27 wwstage fell through to a generic
// CALL free → undefined reference at link.
if (streq(callee.str, "free")) {
if (n.list != nil) {
if (n.list.next == nil) {
cgexpr(c, n.list);
return;
};
};
};
};
};

View File

@@ -24634,6 +24634,22 @@ fn cgcall(c: *cgen, n: *node) void = {
return;
};
};
// free(x) — documented NO-OP, mirror of cstage's cgexpr
// N_CALL free arm (#27): ww has no free by design
// (rt/alloc.s:30 — the bump allocator cannot reclaim a
// mid-chunk pointer; process exit does). The operand is
// still evaluated — Hare's free(expr) evaluates expr —
// so Hare code ports verbatim with its side effects
// intact. Pre-#27 wwstage fell through to a generic
// CALL free → undefined reference at link.
if (streq(callee.str, "free")) {
if (n.list != nil) {
if (n.list.next == nil) {
cgexpr(c, n.list);
return;
};
};
};
};
};

View File

@@ -0,0 +1,238 @@
/*
* 930_free_noop_run — the free() builtin compiles to a documented
* NO-OP (#27).
*
* ww has no free by design (rt/alloc.s:30 — the bump allocator cannot
* reclaim a mid-chunk pointer; process exit does; drop-amalloc). The
* pre-#27 lowering emitted CALL ffi_resolve("free") — an undefined
* reference at w6l unless an @symbol decl happened to be in scope —
* and wwstage had no free arm at all (generic CALL free, same link
* failure). Post-#27 both stages evaluate the operand for side
* effects (Hare's free(expr) evaluates expr — regex fold-2b calls
* free() at 4+ sites; finish() ports verbatim) and emit nothing else.
*
* Rows pin: free of a plain local pointer with deref-after-free (the
* no-op's documented leak semantics — the pointee stays valid), free
* of a struct field, free of a CALL operand twice (the side effect
* must run per call — a global counter observes both evaluations),
* and the Hare-port shape alloc-then-free round-trip (import rt;
* *i64 — i64 sidesteps the pre-existing unrelated cs≠ww alloc(value)
* size divergence on narrow pointee types, filed separately). The
* 2-arg `os.free(p, n)` public API is NOT intercepted (the builtin
* gate requires exactly one arg) and keeps resolving via its
* @symbol("rt_free") decl — covered by the lisp example / stdlib
* suites, not re-pinned here.
*
* Per row: w6c vs w6c_ww .s byte-id (rule 10), a negative grep that
* no `free` symbol survives anywhere in the .s (byte-id alone would
* pass if BOTH stages still emitted CALL free), and runtime via both
* drivers.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return -1;
}
struct row {
const char *label;
const char *src;
int want;
};
static const struct row rows[] = {
/* free(ident): compiles, links, and the pointee stays valid —
* the no-op's documented semantics (a harmless leak, never a
* dangling pointer). */
{ "free_local_ptr_deref_after",
"export fn main() i32 = {\n"
" let x: i32 = 5;\n"
" let p: *i32 = &x;\n"
" free(p);\n"
" if (*p != 5) { return 1; };\n"
" return 0;\n"
"};\n",
0 },
/* free(field): the N_DOT operand shape (regex finish() frees
* re.insts / re.charsets through a pointer field). */
{ "free_struct_field",
"type holder = struct { p: *i32, n: i32 };\n"
"export fn main() i32 = {\n"
" let x: i32 = 3;\n"
" let h: holder = holder { p = &x, n = 4 };\n"
" free(h.p);\n"
" if (h.n != 4) { return 2; };\n"
" return 0;\n"
"};\n",
0 },
/* free(f(...)): Hare evaluates free's operand, so the call must
* run — twice freed, twice bumped. Pins the
* evaluate-for-side-effects half of the lowering (emitting
* nothing at all would leave g at 0). */
{ "free_call_operand_effects",
"let g: i32 = 0;\n"
"fn bump(p: *i32) *i32 = {\n"
" g = g + 2;\n"
" return p;\n"
"};\n"
"export fn main() i32 = {\n"
" let x: i32 = 1;\n"
" free(bump(&x));\n"
" free(bump(&x));\n"
" if (g != 4) { return 3; };\n"
" return 0;\n"
"};\n",
0 },
/* The verbatim Hare-port shape: alloc then free, deref after.
* Pre-#27 this was THE w6l undefined-reference repro. */
{ "free_alloc_roundtrip",
"import rt;\n"
"export fn main() i32 = {\n"
" let p: *i64 = alloc(11i64)!;\n"
" free(p);\n"
" if (*p != 11i64) { return 4; };\n"
" return 0;\n"
"};\n",
0 },
};
static const char *g_bin;
static int
compile_s(const char *tool, const char *src, const char *outpath)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "%s/%s %s > %s 2>&1",
g_bin, tool, src, outpath);
return runwait(cmd);
}
static int
file_eq(const char *a, const char *b)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "cmp -s %s %s", a, b);
return runwait(cmd) == 0;
}
static int
has_free_sym(const char *s_path)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "grep -q 'free' %s", s_path);
return runwait(cmd) == 0;
}
static int
run_driver(const char *driver, const char *src, const char *label)
{
char tmpdir[128], cmd[1024];
snprintf(tmpdir, sizeof tmpdir, "/tmp/freenoop_%d_d", getpid());
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s/%s build %s >/dev/null 2>&1",
tmpdir, g_bin, driver, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
label, driver);
return -1;
}
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[256];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
unlink(outbin);
rmdir(tmpdir);
return got;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
static char absbin[512];
if (bin[0] != '/') {
char cwd[256];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
g_bin = bin;
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int i = 0; i < n; i++) {
const struct row *r = &rows[i];
char src[128], cs_s[128], ww_s[128];
snprintf(src, sizeof src, "/tmp/freenoop_%d_%d.ww",
getpid(), i);
snprintf(cs_s, sizeof cs_s, "/tmp/freenoop_%d_%d_cs.s",
getpid(), i);
snprintf(ww_s, sizeof ww_s, "/tmp/freenoop_%d_%d_ww.s",
getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return 1;
fputs("package main;\n\n", f);
fputs(r->src, f);
fclose(f);
total++;
int cs_rc = compile_s("w6c", src, cs_s);
int ww_rc = compile_s("w6c_ww", src, ww_s);
if (cs_rc != 0 || ww_rc != 0) {
fprintf(stderr, "FAIL row[%s]: compile rc cs=%d "
"ww=%d\n", r->label, cs_rc, ww_rc);
fail++;
unlink(src); unlink(cs_s); unlink(ww_s);
continue;
}
if (!file_eq(cs_s, ww_s)) {
fprintf(stderr, "FAIL row[%s]: cs != ww .s\n",
r->label);
fail++;
}
if (has_free_sym(cs_s)) {
fprintf(stderr, "FAIL row[%s]: 'free' survives in "
"the .s — lowering is not a no-op\n", r->label);
fail++;
}
int got_cs = run_driver("ww", src, r->label);
if (got_cs != r->want) {
fprintf(stderr, "FAIL row[%s] cstage: want %d "
"got %d\n", r->label, r->want, got_cs);
fail++;
}
char wwdrv[600];
snprintf(wwdrv, sizeof wwdrv, "%s/ww_ww", g_bin);
if (access(wwdrv, X_OK) == 0) {
int got_ww = run_driver("ww_ww", src, r->label);
if (got_ww != r->want) {
fprintf(stderr, "FAIL row[%s] wwstage: "
"want %d got %d\n",
r->label, r->want, got_ww);
fail++;
}
}
unlink(src); unlink(cs_s); unlink(ww_s);
}
if (fail) {
fprintf(stderr, "free_noop_run: %d/%d rows failed\n",
fail, total);
return 1;
}
printf("free_noop_run: %d rows ok\n", total);
return 0;
}