/* * 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. * * Runtime behavior is carried by the r930_free_* wwfixtures. This wrapper * retains w6c vs w6c_ww byte identity plus the negative assembly assertion * that no `free` symbol survives. */ #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; } 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]; /* #8: source, .sepwork scratch, both driver binaries and the * byte-id .s dumps all live under one tmpdir; rm -rf on every * exit path. Symbol mangling is package/import-derived, not * entry-path derived, so the in-tmpdir source keeps the cs==ww * .s identical. Temp names must avoid the substring 'free': * has_free_sym greps the whole .s, so an embedded path would * false-fail every row. */ char tmpdir[64], src[128], cs_s[128], ww_s[128], rmcmd[160]; snprintf(tmpdir, sizeof tmpdir, "/tmp/noopfr_%d_d_%d", getpid(), i); /* an unowned path (stale dir, full /tmp) must not be compiled * in — or rm -rf'd — below. */ if (mkdir(tmpdir, 0755) != 0) { perror(tmpdir); return 1; } snprintf(src, sizeof src, "%s/noopfr_%d_%d.ww", tmpdir, getpid(), i); snprintf(cs_s, sizeof cs_s, "%s/cs.s", tmpdir); snprintf(ww_s, sizeof ww_s, "%s/ww.s", tmpdir); snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir); FILE *f = fopen(src, "wb"); if (!f) { perror(src); if (runwait(rmcmd) != 0) fprintf(stderr, "free_noop_run: cleanup " "%s failed\n", tmpdir); return 1; } fputs("package main;\n\n", f); fputs(r->src, f); fclose(f); total++; int rowfail = 0; 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++; rowfail = 1; } else { if (!file_eq(cs_s, ww_s)) { fprintf(stderr, "FAIL row[%s]: cs != ww .s\n", r->label); fail++; rowfail = 1; } 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++; rowfail = 1; } } /* a silent cleanup failure must fail an otherwise-passing * row without masking its own diagnostic. */ if (runwait(rmcmd) != 0) { fprintf(stderr, "FAIL row[%s]: cleanup %s failed\n", r->label, tmpdir); if (!rowfail) fail++; } } if (fail) { fprintf(stderr, "free_noop_run: %d/%d rows failed\n", fail, total); return 1; } printf("free_noop_run: %d assembly rows ok\n", total); return 0; }