/* * 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), * free in a 1M-iteration loop (the no-op must not accumulate stack * damage — a leaked push per free would segfault), * 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 #include #include #include #include #include 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 }, /* free() in a hot loop: the no-op must not accumulate stack * damage — a leaked 8B push per free would blow the 8MiB stack * long before 1M iterations (segfault, not a wrong exit code). */ { "free_loop_no_stack_damage", "let g: i32 = 0;\n" "fn bump(p: *i32) *i32 = {\n" " g = g + 1;\n" " return p;\n" "};\n" "export fn main() i32 = {\n" " let x: i32 = 1;\n" " let i: i32 = 0;\n" " for (i < 1000000) {\n" " free(bump(&x));\n" " i += 1;\n" " };\n" " if (g != 1000000) { return 5; };\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; }