nomen-lang 0.6.9 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #! /usr/bin/env node
2
- import { execFile, execFileSync, execSync } from "node:child_process";
2
+ import { execFile, execFileSync, execSync, spawn } from "node:child_process";
3
3
  import fs, { stat, unwatchFile, watch, watchFile } from "node:fs";
4
4
  import * as sp from "node:path";
5
5
  import path, { join, resolve, sep } from "node:path";
@@ -2122,8 +2122,40 @@ function try_parse_operands(parts) {
2122
2122
  * is not an instruction — label, directive, comment, blank, OR an
2123
2123
  * instruction outside the contract table). Shared by the validator and the
2124
2124
  * phase-2 optimizer so both see identical shapes.
2125
- */
2125
+ *
2126
+ * Memoized by raw text: the post-processing pipeline (frame slots, coalesce,
2127
+ * loop promote, if-convert, float forwarding, widen masks, …) plus the
2128
+ * validators each re-parse the SAME instruction texts — prologue/epilogue
2129
+ * lines repeat across every function, so the hit rate is very high. Every
2130
+ * call returns a PRIVATE deep copy (the passes treat parsed instructions as
2131
+ * mutable IR — coalesce renames operands in place — so sharing a parsed
2132
+ * object would corrupt the memo and cross-pass state).
2133
+ */
2134
+ const parse_memo = /* @__PURE__ */ new Map();
2135
+ const PARSE_MEMO_CAP = 2e5;
2136
+ function clone_parsed(c, line) {
2137
+ return {
2138
+ text: c.text,
2139
+ op: c.op,
2140
+ setsFlags: c.setsFlags,
2141
+ readsFlags: c.readsFlags,
2142
+ operands: c.operands.map((o) => {
2143
+ const copy = { ...o };
2144
+ if (copy.kind === "mem" && copy.offset) copy.offset = { ...copy.offset };
2145
+ return copy;
2146
+ }),
2147
+ line
2148
+ };
2149
+ }
2126
2150
  function parse_asm_instruction(text, line) {
2151
+ const cached = parse_memo.get(text);
2152
+ if (cached !== void 0) return cached === null ? null : clone_parsed(cached, line);
2153
+ const parsed = parse_asm_instruction_uncached(text, line);
2154
+ if (parse_memo.size >= PARSE_MEMO_CAP) parse_memo.clear();
2155
+ parse_memo.set(text, parsed ? clone_parsed(parsed, 0) : null);
2156
+ return parsed;
2157
+ }
2158
+ function parse_asm_instruction_uncached(text, line) {
2127
2159
  const t = strip_comment$1(text).trim();
2128
2160
  const m = /^([a-z][a-z0-9.]*)\s*(.*)$/.exec(t);
2129
2161
  if (!m) return null;
@@ -2274,6 +2306,13 @@ function parse_mem_inner(inner) {
2274
2306
  if (tail_shift) mem.scale = 1 << parseInt(tail_shift[1], 10);
2275
2307
  continue;
2276
2308
  }
2309
+ if (/^[A-Za-z_.$][\w.$]*@[A-Za-z0-9_]+$/.test(comp)) {
2310
+ mem.offset = {
2311
+ kind: "reg",
2312
+ name: comp
2313
+ };
2314
+ continue;
2315
+ }
2277
2316
  return null;
2278
2317
  }
2279
2318
  return mem;
@@ -2407,34 +2446,13 @@ function validate_asm(code) {
2407
2446
  }
2408
2447
  return errors;
2409
2448
  }
2410
- /**
2411
- * The deferred phase-1 check: every function's sp must return to its entry
2412
- * value at each `ret`. A LINEAR scan double-counts across diamond control
2413
- * flow (`b .epilogue` skipping the sibling path's `add sp` — the classic
2414
- * false positive on bool_to_string), so this runs a per-block dataflow over
2415
- * the lifted function bodies:
2416
- *
2417
- * - Block splitting: labels and branch/terminator instructions bound blocks,
2418
- * exactly like the CFG the emitters produce.
2419
- * - Per-instruction sp effects: pre-indexed `stp/str [sp, #-N]!` subtracts,
2420
- * post-indexed `ldp/ldr [sp], #N` adds, `sub/add sp, sp, #imm` adjusts;
2421
- * any other sp write (`mov sp, xN`, register-scaled adds, `ldr sp, …`)
2422
- * is UNKNOWN and conservatively poisons downstream deltas.
2423
- * - Join = require equal deltas or unknown: a label reached by paths with
2424
- * differing deltas becomes unknown; only a KNOWN non-zero delta at a
2425
- * `ret` is reported. Unknown never errors — the check is conservative by
2426
- * construction (no false positives, only silence).
2427
- * - `bl`/`blr` preserve sp (the callee rebalances under the ABI) and do not
2428
- * terminate blocks; `br xN` (tail-jump) ends propagation with no check;
2429
- * `svc` continues (the kernel preserves sp).
2430
- * - Branch targets resolve within the function, including the numeric local
2431
- * form raw `#arch` blocks use (`1:` / `b.hs 1f` — forward/backward by
2432
- * suffix); an unresolvable target makes downstream deltas unknown.
2433
- */
2434
- function validate_stack_balance(code) {
2435
- const { result } = lift_functions(code);
2436
- if (!result.ok) return [];
2449
+ /** Stack-balance check over an ALREADY-lifted result — lets the build
2450
+ * pipeline lift+validate once and share the structured functions between
2451
+ * the shape/branch validation and the balance pass (lift_functions runs
2452
+ * validate_asm internally, so re-lifting per check doubled the cost). */
2453
+ function stack_balance_errors_from_lift(result) {
2437
2454
  const errors = [];
2455
+ if (!result.ok) return [];
2438
2456
  for (const fn of result.functions) check_function_balance(fn, errors);
2439
2457
  return errors;
2440
2458
  }
@@ -2562,37 +2580,37 @@ function line_effect(text) {
2562
2580
  };
2563
2581
  }
2564
2582
  /** Resolve a branch target token to a body line index, or null when the
2565
- * target is outside this function / unresolved (numeric f/b forms scan
2566
- * linearly, matching the assembler's semantics). */
2567
- function resolve_target(token, from, body) {
2583
+ * target is outside this function. Backed by the per-function label maps
2584
+ * built once in check_function_balance — a linear rescan of the body per
2585
+ * pending branch used to be the single hottest spot in large builds
2586
+ * (branches × body-length re-stripping + regex). Numeric `Nf`/`Nb` forms
2587
+ * resolve directionally against the (possibly redefined) definitions,
2588
+ * matching the assembler's semantics; a named label resolves to its FIRST
2589
+ * definition (the old body scan returned the first match too). */
2590
+ function resolve_target(token, from, named_labels, numeric_labels) {
2568
2591
  const numeric = /^(\d+)([fb])$/.exec(token);
2569
2592
  if (numeric) {
2570
- const n = numeric[1];
2593
+ const defs = numeric_labels.get(numeric[1]);
2594
+ if (!defs) return null;
2571
2595
  if (numeric[2] === "f") {
2572
- for (let i = from + 1; i < body.length; i++) {
2573
- const lm = /^(\d+):/.exec(strip_comment$1(body[i].text).trim());
2574
- if (lm && lm[1] === n) return i;
2575
- }
2596
+ for (let k = 0; k < defs.length; k++) if (defs[k] > from) return defs[k];
2576
2597
  return null;
2577
2598
  }
2578
- for (let i = from - 1; i >= 0; i--) {
2579
- const lm = /^(\d+):/.exec(strip_comment$1(body[i].text).trim());
2580
- if (lm && lm[1] === n) return i;
2581
- }
2599
+ for (let k = defs.length - 1; k >= 0; k--) if (defs[k] < from) return defs[k];
2582
2600
  return null;
2583
2601
  }
2584
- for (let i = 0; i < body.length; i++) {
2585
- const lm = LABEL_RE.exec(strip_comment$1(body[i].text).trim());
2586
- if (lm && lm[1] === token) return i;
2587
- }
2588
- return null;
2602
+ return named_labels.get(token) ?? null;
2589
2603
  }
2590
2604
  function check_function_balance(fn, errors) {
2591
2605
  const body = fn.body;
2606
+ const stripped_text = [];
2592
2607
  const effects = [];
2593
2608
  const is_code = [];
2609
+ const named_labels = /* @__PURE__ */ new Map();
2610
+ const numeric_labels = /* @__PURE__ */ new Map();
2594
2611
  for (let i = 0; i < body.length; i++) {
2595
2612
  const t = strip_comment$1(body[i].text).trim();
2613
+ stripped_text.push(t);
2596
2614
  effects.push({
2597
2615
  delta: null,
2598
2616
  branch: null,
@@ -2601,8 +2619,15 @@ function check_function_balance(fn, errors) {
2601
2619
  unknown_jump: false
2602
2620
  });
2603
2621
  is_code.push(false);
2604
- if (!t) continue;
2605
2622
  const lm = LABEL_RE.exec(t);
2623
+ if (lm) {
2624
+ if (/^\d+$/.test(lm[1])) {
2625
+ const defs = numeric_labels.get(lm[1]);
2626
+ if (defs) defs.push(i);
2627
+ else numeric_labels.set(lm[1], [i]);
2628
+ } else if (!named_labels.has(lm[1])) named_labels.set(lm[1], i);
2629
+ }
2630
+ if (!t) continue;
2606
2631
  const rest = lm ? lm[2].trim() : t;
2607
2632
  if (rest.startsWith(".") || /^[\w.$]+\s*=\s*[\w.$]+$/.test(rest) || !rest) continue;
2608
2633
  is_code[i] = true;
@@ -2614,7 +2639,7 @@ function check_function_balance(fn, errors) {
2614
2639
  let cur = -1;
2615
2640
  let terminated = true;
2616
2641
  for (let i = 0; i < body.length; i++) {
2617
- const t = strip_comment$1(body[i].text).trim();
2642
+ const t = stripped_text[i];
2618
2643
  if (!t) continue;
2619
2644
  if (LABEL_RE.test(t)) {
2620
2645
  cur++;
@@ -2662,7 +2687,7 @@ function check_function_balance(fn, errors) {
2662
2687
  }
2663
2688
  }
2664
2689
  for (const p of pending) {
2665
- const line = resolve_target(p.token, p.from, body);
2690
+ const line = resolve_target(p.token, p.from, named_labels, numeric_labels);
2666
2691
  blocks[p.block].targets.push(line === null ? null : line_block.get(line) ?? null);
2667
2692
  }
2668
2693
  const preds = Array.from({ length: blocks.length }, () => []);
@@ -7511,6 +7536,13 @@ var DeclarationNode = class extends BaseNode {
7511
7536
  * a heap shell for the argument (pass-by-value): the callee frees the
7512
7537
  * shell itself at scope exit (`free(p)`) after freeing the fields. */
7513
7538
  owned_heap_shell;
7539
+ /** True for a `move` param of an owning VALUE-struct type: the caller
7540
+ * passed its storage by address, so the callee owns the CONTENTS but not
7541
+ * the storage. free_scoped_declarations destroys the fields' owned
7542
+ * resources at `(*param)` at scope exit — never a free. Set at build
7543
+ * time by the C function/method builders; consumed by
7544
+ * free_scoped_declarations. */
7545
+ moved_value_struct_param;
7514
7546
  constructor(start, visibility, declaration, name, type, value) {
7515
7547
  super("declare", start);
7516
7548
  this.visibility = visibility;
@@ -8972,6 +9004,85 @@ function record_ctor_heap_default_fields(var_name, struct_node, status, is_owned
8972
9004
  }
8973
9005
  }
8974
9006
  //#endregion
9007
+ //#region ../src/build_common/destroy_analysis.ts
9008
+ /**
9009
+ * Whether any field of the struct is an enum-with-data value owning heap
9010
+ * payloads (a string payload — always heap, case construction strdups it —
9011
+ * or a class/trait reference payload — construction transfers ownership).
9012
+ * Such a field must be reclaimed when the struct dies.
9013
+ */
9014
+ function struct_has_owning_enum_field(struct, table) {
9015
+ for (const field of struct.fields) {
9016
+ if (field.type.is_ref) continue;
9017
+ const en = table.enums?.find((e) => e.name === field.type.name);
9018
+ if (!en?.has_associated_data) continue;
9019
+ for (const c of en.cases) for (const p of c.params) {
9020
+ if (p.type.name === "string" && !p.type.is_array) return true;
9021
+ if (table.traits?.some((t) => t.name === p.type.name)) return true;
9022
+ if (table.structs.find((s) => s.name === p.type.name)?.is_class) return true;
9023
+ }
9024
+ }
9025
+ return false;
9026
+ }
9027
+ /**
9028
+ * Whether a struct declares its own `#destroy` (user side effects /
9029
+ * resource release).
9030
+ */
9031
+ function has_destroy(struct) {
9032
+ return !!struct.functions.find((f) => f.name === "#destroy");
9033
+ }
9034
+ /**
9035
+ * Whether any field (recursively) requires destroy handling — a class
9036
+ * field (heap instance to reclaim) or a nested struct with its own
9037
+ * `#destroy` (or nested fields that do). Deliberately excludes `string`
9038
+ * fields: a struct local whose only owning fields are strings is NOT
9039
+ * auto-destroyed at scope exit (the strings may be raw rodata arg
9040
+ * pointers, not heap) — see `struct_needs_auto_destroy` for the broader
9041
+ * form used to decide whether a `<Struct>_destroy` function is generated.
9042
+ */
9043
+ function has_struct_fields_with_destroy(struct, table) {
9044
+ for (const field of struct.fields) {
9045
+ if (field.type.is_ref) continue;
9046
+ const field_struct = resolve_struct_type(field.type, table);
9047
+ if (!field_struct) continue;
9048
+ if (field_struct.is_class) return true;
9049
+ if (has_destroy(field_struct)) return true;
9050
+ if (has_struct_fields_with_destroy(field_struct, table)) return true;
9051
+ }
9052
+ return false;
9053
+ }
9054
+ /**
9055
+ * Whether a struct (or any embedded struct field, recursively) needs a
9056
+ * destroy call at scope exit — it has a `#destroy`, a class-typed field,
9057
+ * or a nested struct field that itself needs destroying. This does NOT
9058
+ * count `string` fields (see `has_struct_fields_with_destroy`).
9059
+ */
9060
+ function struct_needs_destroy(struct, table) {
9061
+ return has_destroy(struct) || has_struct_fields_with_destroy(struct, table) || struct_has_owning_enum_field(struct, table);
9062
+ }
9063
+ /**
9064
+ * Whether a struct needs an auto-generated `<Struct>_destroy` function.
9065
+ * Broader than `struct_needs_destroy`: also returns true for structs with
9066
+ * `string` fields, because a Buffer<T> for such a struct deep-copies the
9067
+ * strings into slots (strdup on store) and the per-element destroy must
9068
+ * free them. The generated destroy frees each string field; it is called
9069
+ * from Buffer's specialized #destroy / replace_T, NOT from struct local
9070
+ * scope exit (`struct_needs_destroy` governs that path and excludes
9071
+ * strings).
9072
+ */
9073
+ function struct_needs_auto_destroy(struct, table) {
9074
+ if (struct_needs_destroy(struct, table)) return true;
9075
+ if (struct_has_owning_enum_field(struct, table)) return true;
9076
+ for (const field of struct.fields) {
9077
+ if (field.type.is_ref) continue;
9078
+ if (field.type.is_view) continue;
9079
+ if (field.type.name === "string" && !field.type.is_array) return true;
9080
+ const field_struct = resolve_struct_type(field.type, table);
9081
+ if (field_struct && !field_struct.is_class && struct_needs_auto_destroy(field_struct, table)) return true;
9082
+ }
9083
+ return false;
9084
+ }
9085
+ //#endregion
8975
9086
  //#region ../src/nodes/child_nodes.ts
8976
9087
  /** Whether `value` is an AST node (carries a string `node_type`). */
8977
9088
  function is_ast_node(value) {
@@ -11648,85 +11759,6 @@ function array_struct_name(type, status) {
11648
11759
  return status.structs.find((s) => s.name === mono && !s.is_generic) ? mono : void 0;
11649
11760
  }
11650
11761
  //#endregion
11651
- //#region ../src/build_common/destroy_analysis.ts
11652
- /**
11653
- * Whether any field of the struct is an enum-with-data value owning heap
11654
- * payloads (a string payload — always heap, case construction strdups it —
11655
- * or a class/trait reference payload — construction transfers ownership).
11656
- * Such a field must be reclaimed when the struct dies.
11657
- */
11658
- function struct_has_owning_enum_field(struct, table) {
11659
- for (const field of struct.fields) {
11660
- if (field.type.is_ref) continue;
11661
- const en = table.enums?.find((e) => e.name === field.type.name);
11662
- if (!en?.has_associated_data) continue;
11663
- for (const c of en.cases) for (const p of c.params) {
11664
- if (p.type.name === "string" && !p.type.is_array) return true;
11665
- if (table.traits?.some((t) => t.name === p.type.name)) return true;
11666
- if (table.structs.find((s) => s.name === p.type.name)?.is_class) return true;
11667
- }
11668
- }
11669
- return false;
11670
- }
11671
- /**
11672
- * Whether a struct declares its own `#destroy` (user side effects /
11673
- * resource release).
11674
- */
11675
- function has_destroy(struct) {
11676
- return !!struct.functions.find((f) => f.name === "#destroy");
11677
- }
11678
- /**
11679
- * Whether any field (recursively) requires destroy handling — a class
11680
- * field (heap instance to reclaim) or a nested struct with its own
11681
- * `#destroy` (or nested fields that do). Deliberately excludes `string`
11682
- * fields: a struct local whose only owning fields are strings is NOT
11683
- * auto-destroyed at scope exit (the strings may be raw rodata arg
11684
- * pointers, not heap) — see `struct_needs_auto_destroy` for the broader
11685
- * form used to decide whether a `<Struct>_destroy` function is generated.
11686
- */
11687
- function has_struct_fields_with_destroy(struct, table) {
11688
- for (const field of struct.fields) {
11689
- if (field.type.is_ref) continue;
11690
- const field_struct = resolve_struct_type(field.type, table);
11691
- if (!field_struct) continue;
11692
- if (field_struct.is_class) return true;
11693
- if (has_destroy(field_struct)) return true;
11694
- if (has_struct_fields_with_destroy(field_struct, table)) return true;
11695
- }
11696
- return false;
11697
- }
11698
- /**
11699
- * Whether a struct (or any embedded struct field, recursively) needs a
11700
- * destroy call at scope exit — it has a `#destroy`, a class-typed field,
11701
- * or a nested struct field that itself needs destroying. This does NOT
11702
- * count `string` fields (see `has_struct_fields_with_destroy`).
11703
- */
11704
- function struct_needs_destroy(struct, table) {
11705
- return has_destroy(struct) || has_struct_fields_with_destroy(struct, table) || struct_has_owning_enum_field(struct, table);
11706
- }
11707
- /**
11708
- * Whether a struct needs an auto-generated `<Struct>_destroy` function.
11709
- * Broader than `struct_needs_destroy`: also returns true for structs with
11710
- * `string` fields, because a Buffer<T> for such a struct deep-copies the
11711
- * strings into slots (strdup on store) and the per-element destroy must
11712
- * free them. The generated destroy frees each string field; it is called
11713
- * from Buffer's specialized #destroy / replace_T, NOT from struct local
11714
- * scope exit (`struct_needs_destroy` governs that path and excludes
11715
- * strings).
11716
- */
11717
- function struct_needs_auto_destroy(struct, table) {
11718
- if (struct_needs_destroy(struct, table)) return true;
11719
- if (struct_has_owning_enum_field(struct, table)) return true;
11720
- for (const field of struct.fields) {
11721
- if (field.type.is_ref) continue;
11722
- if (field.type.is_view) continue;
11723
- if (field.type.name === "string" && !field.type.is_array) return true;
11724
- const field_struct = resolve_struct_type(field.type, table);
11725
- if (field_struct && !field_struct.is_class && struct_needs_auto_destroy(field_struct, table)) return true;
11726
- }
11727
- return false;
11728
- }
11729
- //#endregion
11730
11762
  //#region ../src/build_common/scan_force_heap_strings.ts
11731
11763
  function scan_force_heap_strings(statements, structs) {
11732
11764
  const result = /* @__PURE__ */ new Set();
@@ -14909,7 +14941,33 @@ function emit_base_ptr(status, decl_name, is_class_parent) {
14909
14941
  if (is_class_parent) emit_var_load(status, "x0", decl_name, 8);
14910
14942
  else emit_var_address(status, "x0", decl_name);
14911
14943
  }
14912
- function emit_field_destroys(status, struct_type, decl_name, base_offset, is_class_parent, free_strings = true) {
14944
+ /**
14945
+ * Reclaim a struct's owning fields in place (base + every nested owning field).
14946
+ *
14947
+ * `base_offset` (added to each field offset) and `is_class_parent` (a class's
14948
+ * base is a pointer LOADED from the slot; a value struct is inline AT it)
14949
+ * locate `decl_name`'s storage.
14950
+ *
14951
+ * `free_strings` is REQUIRED — it is a per-call-site OWNOWNITY fact, not a
14952
+ * per-type one, so it deliberately has no default:
14953
+ * - `true` from a DESTROY function (`<T>_destroy`), which only ever runs on
14954
+ * heap-owned storage: a class's plain string fields are always heap-owned
14955
+ * (`_init` strdup's defaults, assignments strdup a non-heap RHS), and a
14956
+ * VALUE struct arrives with the per-slot copies `store_T`/`_copy` strdup'd
14957
+ * (Buffer per-element teardown, the displaced-field reclaim, the
14958
+ * anchor-slot walk). Matches C's build_auto_destroy, which frees string
14959
+ * fields unconditionally.
14960
+ * - `false` from struct-LOCAL scope exit, where a string field may still hold
14961
+ * a rodata literal (aarch64 constructors don't strdup) and freeing it would
14962
+ * `free` rodata (SIGABRT).
14963
+ *
14964
+ * A default (`= true`) previously stood in for the `true` arm while
14965
+ * `node.is_class` (undefined on plain parsed structs) was passed as if it
14966
+ * selected between the arms — so the day `is_class` becomes explicitly false,
14967
+ * value-struct slot strings would silently stop being reclaimed. Passing the
14968
+ * boolean at every call site makes both arms visible.
14969
+ */
14970
+ function emit_field_destroys(status, struct_type, decl_name, base_offset, is_class_parent, free_strings) {
14913
14971
  const destroy_fields = struct_type.is_class ? layout_fields(struct_type, status) : struct_type.fields;
14914
14972
  for (const field of destroy_fields) {
14915
14973
  const offset = get_field_offset_of_fields(destroy_fields, field.name, status);
@@ -20720,16 +20778,19 @@ function build_function_node$1(node, status) {
20720
20778
  if (param.is_moved) {
20721
20779
  const is_class = !!status.structs.find((s) => s.name === param.type.name && s.is_class);
20722
20780
  const is_trait = !!status.traits.find((t) => t.name === param.type.name);
20723
- if (is_class || is_trait) {
20781
+ const vstruct = !is_class && !is_trait ? resolve_struct_type(param.type, status) : void 0;
20782
+ const is_owning_value_struct = !!vstruct && !vstruct.is_class && !param.type.is_nullable && struct_needs_destroy(vstruct, status);
20783
+ if (is_class || is_trait || is_owning_value_struct) {
20724
20784
  const reg = callee_map.get(param.name) ?? param_regs[pidx];
20725
- status.moved_class_params.set(param.name, reg);
20785
+ if (is_class || is_trait) status.moved_class_params.set(param.name, reg);
20726
20786
  const save_offset = allocate_stack_space(status, 8);
20727
20787
  emit_asm(status, `str ${reg}, [x29, #${save_offset}]\n`);
20728
20788
  moved_param_save_slots.set(param.name, {
20729
20789
  offset: save_offset,
20730
20790
  type_name: param.type.name,
20731
20791
  type_args: param.type.type_args,
20732
- is_nullable: param.type.is_nullable
20792
+ is_nullable: param.type.is_nullable,
20793
+ value_struct_destroy_name: is_owning_value_struct ? vstruct.name : void 0
20733
20794
  });
20734
20795
  }
20735
20796
  }
@@ -20837,6 +20898,16 @@ function build_function_node$1(node, status) {
20837
20898
  emit_asm(status, `cmp x0, x1\n`);
20838
20899
  emit_asm(status, `beq ${keep_prefix}_${name}\n`);
20839
20900
  }
20901
+ if (info.value_struct_destroy_name) {
20902
+ const vstruct = status.structs.find((s) => s.name === info.value_struct_destroy_name);
20903
+ release_heap_string_fields(status, name, info.value_struct_destroy_name);
20904
+ if (vstruct && has_destroy(vstruct)) {
20905
+ emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
20906
+ emit_asm(status, `bl ${info.value_struct_destroy_name}_destroy\n`);
20907
+ } else if (vstruct) emit_field_destroys(status, vstruct, name, void 0, void 0, false);
20908
+ if (need_guard) emit_asm(status, `${keep_prefix}_${name}:\n`);
20909
+ continue;
20910
+ }
20840
20911
  emit_destroy_for_anchor_slot(status, info.offset, info.type_name, info.type_args, info.is_nullable);
20841
20912
  emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
20842
20913
  emit_free(status);
@@ -21047,6 +21118,7 @@ function init_nullable_field_default(node, field, base_reg, status) {
21047
21118
  * sites record it (record_ctor_heap_default_fields) so scope exit frees it.
21048
21119
  */
21049
21120
  function init_expr_field_default(node, field, offset, base_reg, status) {
21121
+ status.last_result_is_heap = false;
21050
21122
  build_node(field.value, status);
21051
21123
  ensure_newline(status);
21052
21124
  const t = field.type;
@@ -21215,7 +21287,7 @@ function build_destroy_function(node, func, status) {
21215
21287
  status.code = status.code.slice(0, after_prologue) + saves + status.code.slice(after_prologue);
21216
21288
  }
21217
21289
  }
21218
- if (node.is_class) emit_field_destroys(status, node, "self", void 0, false);
21290
+ if (node.is_class) emit_field_destroys(status, node, "self", void 0, false, true);
21219
21291
  emit_asm(status, `${return_label}:\n`);
21220
21292
  const total_stack = Math.ceil((status.stack_size || 0) / 16) * 16;
21221
21293
  status.code = status.code.replace(`sub sp, sp, #${stack_placeholder}`, total_stack > 0 ? `sub sp, sp, #${total_stack}` : `// no stack needed`);
@@ -21272,7 +21344,7 @@ function build_auto_destroy_function(node, status) {
21272
21344
  status.function_param_regs.set("self", "x19");
21273
21345
  emit_asm(status, `sub sp, sp, #${stack_placeholder}\n`);
21274
21346
  emit_asm(status, `mov x29, sp\n`);
21275
- emit_field_destroys(status, node, "self", void 0, false, node.is_class);
21347
+ emit_field_destroys(status, node, "self", void 0, false, true);
21276
21348
  emit_asm(status, `${return_label}:\n`);
21277
21349
  const total_stack = Math.ceil((status.stack_size || 0) / 16) * 16;
21278
21350
  status.code = status.code.replace(`sub sp, sp, #${stack_placeholder}`, total_stack > 0 ? `sub sp, sp, #${total_stack}` : `// no stack needed`);
@@ -22172,7 +22244,10 @@ function build_struct_functions$1(node, status) {
22172
22244
  const moved_param_save_slots = /* @__PURE__ */ new Map();
22173
22245
  for (const param of func.params) {
22174
22246
  if (!param.is_moved || param.is_self_param) continue;
22175
- if (!status.structs.find((s) => s.name === param.type.name && s.is_class)) continue;
22247
+ const is_class = !!status.structs.find((s) => s.name === param.type.name && s.is_class);
22248
+ const vstruct = !is_class ? resolve_struct_type(param.type, status) : void 0;
22249
+ const is_owning_value_struct = !!vstruct && !vstruct.is_class && !param.type.is_nullable && struct_needs_destroy(vstruct, status);
22250
+ if (!is_class && !is_owning_value_struct) continue;
22176
22251
  const reg = status.function_param_regs.get(param.name);
22177
22252
  if (reg) {
22178
22253
  const save_offset = allocate_stack_space(status, 8);
@@ -22181,7 +22256,8 @@ function build_struct_functions$1(node, status) {
22181
22256
  offset: save_offset,
22182
22257
  type_name: param.type.name,
22183
22258
  type_args: param.type.type_args,
22184
- is_nullable: param.type.is_nullable
22259
+ is_nullable: param.type.is_nullable,
22260
+ value_struct_destroy_name: is_owning_value_struct ? vstruct.name : void 0
22185
22261
  });
22186
22262
  } else {
22187
22263
  const offset = status.stack_offsets.get(param.name);
@@ -22189,7 +22265,8 @@ function build_struct_functions$1(node, status) {
22189
22265
  offset,
22190
22266
  type_name: param.type.name,
22191
22267
  type_args: param.type.type_args,
22192
- is_nullable: param.type.is_nullable
22268
+ is_nullable: param.type.is_nullable,
22269
+ value_struct_destroy_name: is_owning_value_struct ? vstruct.name : void 0
22193
22270
  });
22194
22271
  }
22195
22272
  }
@@ -22244,6 +22321,16 @@ function build_struct_functions$1(node, status) {
22244
22321
  emit_asm(status, `cmp x0, x1\n`);
22245
22322
  emit_asm(status, `beq ${keep_prefix}_${name}\n`);
22246
22323
  }
22324
+ if (info.value_struct_destroy_name) {
22325
+ const vstruct = status.structs.find((s) => s.name === info.value_struct_destroy_name);
22326
+ release_heap_string_fields(status, name, info.value_struct_destroy_name);
22327
+ if (vstruct && has_destroy(vstruct)) {
22328
+ emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
22329
+ emit_asm(status, `bl ${info.value_struct_destroy_name}_destroy\n`);
22330
+ } else if (vstruct) emit_field_destroys(status, vstruct, name, void 0, void 0, false);
22331
+ if (ret_is_class) emit_asm(status, `${keep_prefix}_${name}:\n`);
22332
+ continue;
22333
+ }
22247
22334
  emit_destroy_for_anchor_slot(status, info.offset, info.type_name, info.type_args, info.is_nullable);
22248
22335
  emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
22249
22336
  emit_free(status);
@@ -22689,6 +22776,7 @@ function build_block_node$1(node, status) {
22689
22776
  const prologue = status.module_init_statements;
22690
22777
  status.module_init_statements = void 0;
22691
22778
  for (const stmt of prologue) {
22779
+ status.last_result_is_heap = false;
22692
22780
  emit_allocations$1(stmt, status);
22693
22781
  emit_stmt_from_nir$1(stmt, prologue.indexOf(stmt), prologue, status);
22694
22782
  }
@@ -22698,6 +22786,7 @@ function build_block_node$1(node, status) {
22698
22786
  if (module_init_set.has(child)) continue;
22699
22787
  if (!is_trait_node(child) && !is_struct_node(child) && !is_function_node(child) && child.node_type !== "enum" && child.node_type !== "bitset" && !(child.node_type === "declare" && inlined_const_names.has(child.name))) {
22700
22788
  if (child.node_type !== "while") emit_allocations$1(child, status);
22789
+ status.last_result_is_heap = false;
22701
22790
  const prev_forwarded = status.forwarded_param_inits;
22702
22791
  const consumed = emit_stmt_from_nir$1(child, index, node.statements, status);
22703
22792
  index += consumed - 1;
@@ -23874,7 +23963,6 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23874
23963
  }
23875
23964
  hoist_field_overrides(node.value, build_node, status, "", node.name);
23876
23965
  status.last_result_is_heap = false;
23877
- const prev_heap = status.last_result_is_heap;
23878
23966
  if (node.value?.node_type === "value") {
23879
23967
  const inlined = status.top_level_consts?.get(node.value.value);
23880
23968
  if (inlined?.value) node.value = fold_string_const(inlined, status.top_level_consts) ?? inlined.value;
@@ -23896,7 +23984,6 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23896
23984
  consolidate_temp_anchors$1(status, node.value, node.type.name);
23897
23985
  }
23898
23986
  }
23899
- status.last_result_is_heap = prev_heap;
23900
23987
  }
23901
23988
  if (node.func_params) {
23902
23989
  if (status.function_return_label) {
@@ -24784,8 +24871,12 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
24784
24871
  emit_init_value$1(node.value, nir_init, status);
24785
24872
  ensure_newline(status);
24786
24873
  status.int_dest_hint = void 0;
24787
- if (size === 16 && node.type.name === "string") emit_pair_store_x29(status, offset);
24788
- else emit_var_store(status, "x0", node.name, size);
24874
+ if (size === 16 && node.type.name === "string") {
24875
+ const force_heap_init = (status.force_heap_strings?.has(node.name) ?? false) && raw !== "null";
24876
+ if (force_heap_init) emit_strdup_string(status);
24877
+ emit_pair_store_x29(status, offset);
24878
+ if (force_heap_init) mark_heap_string(status, node.name);
24879
+ } else emit_var_store(status, "x0", node.name, size);
24789
24880
  } else if (is_float_type$1(node.type.name)) {
24790
24881
  const label = `_float_const_${decl_const_counter++}`;
24791
24882
  emit_data(status, `${label}: .double ${raw}\n.p2align 2\n`);
@@ -24988,6 +25079,17 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
24988
25079
  check_heap();
24989
25080
  return;
24990
25081
  }
25082
+ const decl_string_field_read = node.type.name === "string" && !node.type.is_view && !node.type.is_array && size === 16 && node.value.node_type === "access" && node.value.access.node_type === "access_field";
25083
+ if (decl_string_field_read && !!status.force_heap_strings?.has(node.name)) {
25084
+ emit_init_value$1(node.value, nir_init, status);
25085
+ ensure_newline(status);
25086
+ emit_strdup_string(status);
25087
+ emit_var_store(status, "x0", node.name, size);
25088
+ mark_heap_string(status, node.name);
25089
+ status.last_result_is_heap = false;
25090
+ return;
25091
+ }
25092
+ if (decl_string_field_read) status.last_result_is_heap = false;
24991
25093
  emit_init_value$1(node.value, nir_init, status);
24992
25094
  emit_var_store(status, "x0", node.name, size);
24993
25095
  check_heap();
@@ -31044,6 +31146,10 @@ function build_auto_free(status) {
31044
31146
  function free_scoped_declarations(status, decls, persist_string_field_records = false) {
31045
31147
  let commented = false;
31046
31148
  if (status.heap_string_fields?.size) for (const dec of decls) {
31149
+ if (dec.moved_value_struct_param) {
31150
+ const dec_struct = status.structs.find((s) => s.name === mono_type_name(dec.type) && !s.is_simple_type && !s.is_generic);
31151
+ if (dec_struct && has_destroy(dec_struct)) continue;
31152
+ }
31047
31153
  const prefix = `${dec.name}.`;
31048
31154
  const keys = Array.from(status.heap_string_fields).filter((key) => key.startsWith(prefix));
31049
31155
  if (!keys.length) continue;
@@ -31075,6 +31181,18 @@ function free_scoped_declarations(status, decls, persist_string_field_records =
31075
31181
  }
31076
31182
  if (persist_string_field_records && dec.force_owned_string === true) continue;
31077
31183
  if (status.moved?.has(dec.name)) continue;
31184
+ if (dec.moved_value_struct_param) {
31185
+ const mono_name = mono_type_name(dec.type);
31186
+ const param_struct = status.structs.find((s) => s.name === mono_name && !s.is_simple_type && !s.is_generic);
31187
+ if (param_struct && struct_needs_destroy(param_struct, status)) {
31188
+ if (!commented) {
31189
+ status.code += "\n// Auto-free\n";
31190
+ commented = true;
31191
+ }
31192
+ emit_struct_destroys(status, param_struct, `(*${c_function_name(dec.name)})`);
31193
+ }
31194
+ continue;
31195
+ }
31078
31196
  const cname = c_function_name(dec.name);
31079
31197
  if (dec.type.name === "func") {
31080
31198
  if (!commented) {
@@ -33565,13 +33683,15 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
33565
33683
  }
33566
33684
  }
33567
33685
  }
33568
- if (!node.operator && node.left_value.node_type === "access" && node.left_value.access.node_type === "access_field" && node.right_value.node_type === "value" && node.right_value.is_moved) {
33686
+ const assign_rhs_is_moved_value = node.right_value.node_type === "value" && node.right_value.is_moved;
33687
+ const assign_rhs_is_fresh_call = !node.swap && node.right_value.node_type === "func_call";
33688
+ if (!node.operator && node.left_value.node_type === "access" && node.left_value.access.node_type === "access_field" && (assign_rhs_is_moved_value || assign_rhs_is_fresh_call)) {
33569
33689
  const access_lhs = node.left_value;
33570
33690
  const field_type = access_lhs.access.type;
33571
33691
  const field_resolved = field_type?.name && !field_type.is_view && !field_type.is_array ? resolve_struct_type(field_type, status) : void 0;
33572
33692
  const field_struct = field_resolved && !field_resolved.is_class ? field_resolved : void 0;
33573
33693
  if (field_struct) {
33574
- splice_decl_from_c_scopes(status, node.right_value.value);
33694
+ if (assign_rhs_is_moved_value) splice_decl_from_c_scopes(status, node.right_value.value);
33575
33695
  const saved_len = begin_code_scratch(status);
33576
33696
  build_node$1(node.left_value, status);
33577
33697
  const field_access = end_code_scratch(status, saved_len);
@@ -36790,9 +36910,16 @@ function build_struct_functions(node, status, skip_init = false) {
36790
36910
  for (const param of func.params) {
36791
36911
  if (param.is_self_param) continue;
36792
36912
  const param_struct = status.structs.find((s) => s.name === param.type.name);
36793
- if (param.is_moved && param_struct?.is_class && !moved_param_is_consumed(func, param.name, param_struct.name, status.structs)) {
36913
+ if (!param.is_moved) continue;
36914
+ if (param_struct?.is_class && !moved_param_is_consumed(func, param.name, param_struct.name, status.structs)) {
36794
36915
  const pname = c_function_name(param.name);
36795
36916
  status.scoped_declarations.push(new DeclarationNode(param.start, "private", "move", pname, param.type));
36917
+ } else if (param_struct && !param_struct.is_class && !param_struct.is_simple_type && !param_struct.is_generic && !param.type.is_nullable && struct_needs_destroy(param_struct, status) && !moved_param_is_consumed(func, param.name, param_struct.name, status.structs)) {
36918
+ const pname = c_function_name(param.name);
36919
+ const decl = new DeclarationNode(param.start, "private", "move", pname, param.type);
36920
+ decl.moved_value_struct_param = true;
36921
+ decl.string_fields_via_pointer = true;
36922
+ status.scoped_declarations.push(decl);
36796
36923
  }
36797
36924
  }
36798
36925
  const saved_sig = begin_code_scratch(status);
@@ -37679,6 +37806,12 @@ function build_function_node(node, status) {
37679
37806
  if (param_trait && !param_struct?.is_class) decl.trait_class_trait = param.type.name;
37680
37807
  status.scoped_declarations.push(decl);
37681
37808
  }
37809
+ if (param.is_moved && !moved_owning_param && param_struct && !param_struct.is_class && !param_struct.is_generic && !param.type.is_nullable && node.name !== "main" && struct_needs_destroy(param_struct, status) && !moved_param_is_consumed(node, param.name, param_struct.name, status.structs)) {
37810
+ const decl = new DeclarationNode(param.start, "private", "move", pname, param.type);
37811
+ decl.moved_value_struct_param = true;
37812
+ decl.string_fields_via_pointer = true;
37813
+ status.scoped_declarations.push(decl);
37814
+ }
37682
37815
  const is_free_function = (!node.scope || node.scope.node_type === "root" || node.scope.node_type === "func") && !node.params.some((p) => p.is_self_param);
37683
37816
  const fn_name = node.name;
37684
37817
  const fn_label = emission_label(node);
@@ -41684,7 +41817,11 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41684
41817
  const rhs_is_string_var = !node.swap && !!rhs_value && typeof rhs_value.value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(rhs_value.value) && rhs_value.value !== "true" && rhs_value.value !== "false" && rhs_value.value !== "null" && !rhs_value.is_moved && size === 16 && rhs_value.type?.name === "string" && !rhs_value.type?.is_view;
41685
41818
  const move_source = rhs_is_string_var && node.last_use_move && move_on_last_use_enabled() ? rhs_value.value : void 0;
41686
41819
  const explicit_move_source = !node.swap && !!rhs_value && rhs_value.is_moved && size === 16 && rhs_value.type?.name === "string" && !rhs_value.type?.is_view ? rhs_value.value : void 0;
41687
- const borrow_needs_dup = !node.swap && size === 16 && is_string_borrow(node.right_value) && !!status.force_heap_strings?.has(name);
41820
+ const rhs_is_borrow_reception = !node.swap && size === 16 && is_string_borrow(node.right_value);
41821
+ const rhs_is_field_read = !node.swap && size === 16 && node.right_value.node_type === "access" && node.right_value.access.node_type === "access_field";
41822
+ const rhs_var_name = !node.swap && size === 16 && node.right_value.node_type === "value" && !node.right_value.type?.is_view ? node.right_value.value : void 0;
41823
+ const rhs_is_unrecorded_var_read = rhs_var_name !== void 0 && !status.heap_strings?.has(rhs_var_name);
41824
+ const borrow_needs_dup = (rhs_is_borrow_reception || rhs_is_field_read || rhs_is_unrecorded_var_read) && !!status.force_heap_strings?.has(name);
41688
41825
  const move_source_owns_heap = move_source !== void 0 && !!status.heap_strings?.has(move_source);
41689
41826
  if (rhs_is_view_into_owned) {
41690
41827
  if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
@@ -41694,7 +41831,8 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41694
41831
  if (move_source !== void 0 && move_source_owns_heap) status.heap_strings?.delete(move_source);
41695
41832
  if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
41696
41833
  status.heap_strings.add(name);
41697
- } else if (status.last_result_is_heap) {
41834
+ } else if (rhs_is_field_read || rhs_is_unrecorded_var_read) status.last_result_is_heap = false;
41835
+ else if (status.last_result_is_heap) {
41698
41836
  if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
41699
41837
  status.heap_strings.add(name);
41700
41838
  } else if (explicit_move_source !== void 0 && status.heap_strings?.delete(explicit_move_source)) status.heap_strings.add(name);
@@ -41911,6 +42049,24 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41911
42049
  get_base_address(access, status, "x0");
41912
42050
  emit_asm(status, `str x0, [sp, #-16]!\n`);
41913
42051
  }
42052
+ const field_rhs_is_moved_value = node.right_value.node_type === "value" && node.right_value.is_moved;
42053
+ const field_rhs_is_fresh_call = !node.swap && node.right_value.node_type === "func_call";
42054
+ if (!node.operator && (field_rhs_is_moved_value || field_rhs_is_fresh_call)) {
42055
+ const field_resolved = resolve_struct_type(field_type, status);
42056
+ const field_target_var = access.target.node_type === "value" ? access.target.value : "";
42057
+ const first_init_write = is_first_init_field_write(field_target_var, field_name, status);
42058
+ mark_init_field_write(field_target_var, field_name, status);
42059
+ if (field_resolved && !field_resolved.is_class && !first_init_write && struct_needs_auto_destroy(field_resolved, status)) {
42060
+ if (base_reg !== void 0) {
42061
+ if (offset !== 0) emit_asm(status, `add x0, ${base_reg}, #${offset}\n`);
42062
+ else emit_asm(status, `mov x0, ${base_reg}\n`);
42063
+ } else {
42064
+ emit_asm(status, `ldr x0, [sp]\n`);
42065
+ if (offset !== 0) emit_asm(status, `add x0, x0, #${offset}\n`);
42066
+ }
42067
+ emit_asm(status, `bl ${field_resolved.name}_destroy\n`);
42068
+ }
42069
+ }
41914
42070
  get_source_address(node.right_value, status, nir_rhs);
41915
42071
  ensure_newline(status);
41916
42072
  emit_asm(status, `mov x1, x0\n`);
@@ -47312,7 +47468,8 @@ function build(root, options = {}) {
47312
47468
  status.code = status.code.replaceAll("bl _strdup\n", "bl _nomen_strdup_wrap\n");
47313
47469
  status.code = status.code.replaceAll("bl _free\n", "bl _nomen_free_wrap\n");
47314
47470
  }
47315
- const lift_errors = validate_asm(status.code);
47471
+ const { result: lifted } = lift_functions(status.code);
47472
+ const lift_errors = lifted.errors;
47316
47473
  if (lift_errors.length > 0) {
47317
47474
  if (!status.build_errors) status.build_errors = [];
47318
47475
  for (const e of lift_errors.slice(0, 20)) status.build_errors.push({
@@ -47320,7 +47477,7 @@ function build(root, options = {}) {
47320
47477
  start: 0
47321
47478
  });
47322
47479
  }
47323
- const balance_errors = validate_stack_balance(status.code);
47480
+ const balance_errors = stack_balance_errors_from_lift(lifted);
47324
47481
  if (balance_errors.length > 0) {
47325
47482
  if (!status.build_errors) status.build_errors = [];
47326
47483
  for (const e of balance_errors.slice(0, 20)) status.build_errors.push({
@@ -63125,11 +63282,20 @@ function generate_harness(tests, benches) {
63125
63282
  const n = Math.min(raw, 4096);
63126
63283
  return template.replace(/__NAME__/g, b.name).replace(/__TARGET__/g, b.target).replace(/__N__/g, String(n));
63127
63284
  }).join("\n");
63128
- let main = "\nimport System\nimport System::Test\n\npub func main = () {\n";
63285
+ let main = "\nimport System\nimport System::Test\n";
63286
+ main += "\npub func main = () {\n";
63287
+ main += `\tConsole.unbuffered()\n`;
63129
63288
  main += " var Tester t = Tester()\n";
63289
+ main += " var int64 audit_mark = 0\n";
63290
+ main += " var int64 audit_leaked = 0\n";
63130
63291
  for (const test of tests) {
63292
+ main += `\taudit_mark = Tester.audit_count()\n`;
63131
63293
  main += `\tt.begin_test("${escape_nm_string(test.name)}")\n`;
63132
63294
  main += `\t${test.name}(ref t)\n`;
63295
+ main += `\taudit_leaked = Tester.audit_count() - audit_mark\n`;
63296
+ main += `\tif audit_leaked > 0 {\n`;
63297
+ main += `\t\tConsole.write_line("\\\\nomen|leaks|" + "${escape_nm_string(test.name)}" + "|\\{audit_leaked}")\n`;
63298
+ main += `\t}\n`;
63133
63299
  main += `\tt.end_test()\n`;
63134
63300
  }
63135
63301
  for (const bench of benches) {
@@ -63148,6 +63314,7 @@ function parse_records(stdout) {
63148
63314
  const tests = [];
63149
63315
  const fails = [];
63150
63316
  const benches = [];
63317
+ const leaks = [];
63151
63318
  const other = [];
63152
63319
  for (const line of stdout.split("\n")) {
63153
63320
  if (!line.startsWith(RECORD_PREFIX)) {
@@ -63166,6 +63333,10 @@ function parse_records(stdout) {
63166
63333
  test: parts[1],
63167
63334
  message: parts.slice(2).join("|")
63168
63335
  });
63336
+ else if (kind === "leaks") leaks.push({
63337
+ test: parts[1],
63338
+ count: parseInt(parts[2] || "0", 10)
63339
+ });
63169
63340
  else if (kind === "bench") benches.push({
63170
63341
  label: parts[1],
63171
63342
  n: parseInt(parts[2] || "0", 10),
@@ -63180,12 +63351,19 @@ function parse_records(stdout) {
63180
63351
  tests,
63181
63352
  fails,
63182
63353
  benches,
63354
+ leaks,
63183
63355
  other
63184
63356
  };
63185
63357
  }
63186
- /** Pull the exit-time audit "LEAK: N allocation(s)" lines out of stdout. */
63358
+ /**
63359
+ * Pull the exit-time audit lines out of stdout: "LEAK: N allocation(s)" (a
63360
+ * nonzero malloc/free balance) and "AUDIT-STALE-FREE: N pointer(s) freed that
63361
+ * were not live audit allocations" (a free that matched no live block — a
63362
+ * foreign pointer or a double free; the runtime no longer passes those to
63363
+ * libc free, so this is the only record they leave). Both fail the file.
63364
+ */
63187
63365
  function extract_leaks(stdout) {
63188
- return stdout.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("LEAK:"));
63366
+ return stdout.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("LEAK:") || l.startsWith("AUDIT-STALE-FREE:"));
63189
63367
  }
63190
63368
  /**
63191
63369
  * Compile one `*.test.nm` file (with the generated harness) and run it,
@@ -63201,8 +63379,9 @@ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_ru
63201
63379
  tests: [],
63202
63380
  fails: [],
63203
63381
  benches: [],
63204
- other: [],
63205
63382
  leaks: [],
63383
+ leaking_tests: [],
63384
+ other: [],
63206
63385
  ms: 0
63207
63386
  };
63208
63387
  const resolved = path.resolve(entry_path);
@@ -63230,30 +63409,30 @@ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_ru
63230
63409
  }
63231
63410
  const buildDir = build_dir_for(resolved, true);
63232
63411
  if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
63233
- let audit_obj;
63234
- if (audit) {
63235
- const runtime_src = audit_runtime ? path.resolve(audit_runtime) : find_audit_runtime(path.dirname(resolved)) ?? find_bundled_audit_runtime();
63236
- if (!runtime_src || !fs.existsSync(runtime_src)) {
63237
- result.ok = false;
63238
- result.phase = "setup";
63239
- result.crashed = "Audit enabled but audit_runtime.c was not found. Pass --audit-runtime <path>.";
63240
- result.ms = performance.now() - start;
63241
- return result;
63242
- }
63243
- audit_obj = path.join(buildDir, "audit_runtime.o");
63244
- await execFile_async("clang", [
63245
- "-c",
63246
- runtime_src,
63247
- "-o",
63248
- audit_obj
63249
- ]);
63412
+ const runtime_src = audit_runtime ? path.resolve(audit_runtime) : find_audit_runtime(path.dirname(resolved)) ?? find_bundled_audit_runtime();
63413
+ if (!runtime_src || !fs.existsSync(runtime_src)) {
63414
+ result.ok = false;
63415
+ result.phase = "setup";
63416
+ result.crashed = "audit_runtime.c was not found. Pass --audit-runtime <path>.";
63417
+ result.ms = performance.now() - start;
63418
+ return result;
63250
63419
  }
63420
+ const audit_obj = path.join(buildDir, path.basename(entry_path, ".nm") + "_audit.o");
63421
+ await execFile_async("clang", [
63422
+ "-c",
63423
+ runtime_src,
63424
+ "-o",
63425
+ audit_obj
63426
+ ]);
63251
63427
  const platform = default_platform();
63252
63428
  const ext = arch === "aarch64" ? ".s" : platform === "macos" || platform === "ios" ? ".m" : ".c";
63253
63429
  const codefile = path.join(buildDir, path.basename(entry_path, ".nm") + ext);
63254
63430
  const outfile = path.join(buildDir, path.basename(entry_path, ".nm"));
63255
- fs.writeFileSync(path.join(buildDir, "main.h"), buildResult.headers ?? "");
63256
- fs.writeFileSync(codefile, buildResult.code);
63431
+ const header_name = path.basename(entry_path, ".nm") + ".main.h";
63432
+ fs.writeFileSync(path.join(buildDir, header_name), buildResult.headers ?? "");
63433
+ let code_text = buildResult.code;
63434
+ if (arch !== "aarch64") code_text = code_text.replace("#include \"main.h\"", `#include "${header_name}"`);
63435
+ fs.writeFileSync(codefile, code_text);
63257
63436
  let companionfile;
63258
63437
  if (buildResult.companion) {
63259
63438
  const comp_ext = platform === "macos" || platform === "ios" ? ".m" : ".c";
@@ -63279,27 +63458,56 @@ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_ru
63279
63458
  result.ms = performance.now() - start;
63280
63459
  return result;
63281
63460
  }
63461
+ const run_out = outfile + ".out";
63462
+ const run_err = outfile + ".err";
63282
63463
  let runStdout = "";
63464
+ let runStderr = "";
63283
63465
  let crashed;
63466
+ const out_fd = fs.openSync(run_out, "w");
63467
+ const err_fd = fs.openSync(run_err, "w");
63284
63468
  try {
63285
- runStdout = (await execFile_async(outfile, [], {
63286
- encoding: "utf8",
63469
+ const child = spawn(outfile, [], {
63287
63470
  timeout: 3e4,
63288
- maxBuffer: 16777216,
63471
+ stdio: [
63472
+ "ignore",
63473
+ out_fd,
63474
+ err_fd
63475
+ ],
63289
63476
  cwd: cwd ?? process.cwd()
63290
- })).stdout;
63291
- } catch (err) {
63292
- runStdout = err.stdout ? err.stdout.toString() : "";
63293
- if (err.signal === "SIGTERM") crashed = "test binary timed out after 30s";
63294
- else crashed = `test binary exited abnormally (signal ${err.signal ?? err.code})`;
63477
+ });
63478
+ const run = await new Promise((resolve) => {
63479
+ child.on("error", () => resolve({
63480
+ code: null,
63481
+ signal: null
63482
+ }));
63483
+ child.on("close", (code, signal) => resolve({
63484
+ code,
63485
+ signal
63486
+ }));
63487
+ });
63488
+ if (run.signal === "SIGTERM") crashed = "test binary timed out after 30s";
63489
+ else if (run.signal !== null) crashed = `test binary exited abnormally (signal ${run.signal})`;
63490
+ else if (run.code === null) crashed = "test binary could not be run";
63491
+ else if (run.code !== 0) crashed = `test binary exited abnormally (signal ${run.code})`;
63492
+ } finally {
63493
+ fs.closeSync(out_fd);
63494
+ fs.closeSync(err_fd);
63295
63495
  }
63496
+ runStdout = fs.readFileSync(run_out, "utf8");
63497
+ try {
63498
+ runStderr = fs.readFileSync(run_err, "utf8");
63499
+ } catch {
63500
+ runStderr = "";
63501
+ }
63502
+ if (crashed && runStderr.trim()) crashed += `\n${runStderr.trim()}`;
63296
63503
  const records = parse_records(runStdout);
63297
63504
  result.tests = records.tests;
63298
63505
  result.fails = records.fails;
63299
63506
  result.benches = records.benches;
63507
+ result.leaking_tests = records.leaks;
63300
63508
  result.other = records.other;
63301
63509
  result.leaks = audit ? extract_leaks(runStdout) : [];
63302
- if (result.leaks.length) result.other = result.other.filter((l) => !l.trim().startsWith("LEAK:"));
63510
+ if (result.leaks.length) result.other = result.other.filter((l) => !l.trim().startsWith("LEAK:") && !l.trim().startsWith("AUDIT-STALE-FREE:"));
63303
63511
  result.crashed = crashed;
63304
63512
  if (crashed) result.phase = "run";
63305
63513
  result.ok = !(result.fails.length > 0 || result.tests.some((t) => t.failed > 0)) && crashed === void 0 && result.leaks.length === 0;
@@ -63340,6 +63548,10 @@ function report_file(result) {
63340
63548
  if (result.crashed) console.log(C.red(` ${result.crashed}`));
63341
63549
  for (const f of result.fails) console.log(` ${C.red("✗")} ${f.test} ${C.dim(">")} ${f.message}`);
63342
63550
  for (const leak of result.leaks) console.log(` ${C.yellow("⚠")} ${C.yellow(leak)}`);
63551
+ if (result.leaks.length) {
63552
+ for (const lt of result.leaking_tests) console.log(` ${C.yellow("⚠")} ${lt.test} ${C.dim(`leaked ${lt.count} allocation(s)`)}`);
63553
+ if (!result.leaking_tests.length) console.log(` ${C.yellow("⚠")} ${C.yellow("LEAK: no per-test attribution — leaked outside a test (harness/setup/teardown) or crashed mid-test")}`);
63554
+ }
63343
63555
  for (const b of result.benches) console.log(` ${C.cyan("⏱")} ${b.label} ${C.dim(`(n=${b.n})`)} ${C.dim("min")} ${fmt_ns(b.min)} ${C.dim("median")} ${fmt_ns(b.median)} ${C.dim("mean")} ${fmt_ns(Math.round(b.mean))} ${C.dim("max")} ${fmt_ns(b.max)} ${C.dim("±")} ${fmt_ns(Math.round(b.stddev))}`);
63344
63556
  if (result.other.length) {
63345
63557
  console.log(C.dim(" --- test stdout ---"));
@@ -63395,11 +63607,12 @@ async function runTests(root, options = {}) {
63395
63607
  const totalFailed = results.reduce((a, r) => a + r.tests.reduce((x, t) => x + t.failed, 0), 0);
63396
63608
  const totalPassed = totalTests - totalFailed;
63397
63609
  const totalLeaks = results.reduce((a, r) => a + r.leaks.length, 0);
63610
+ const totalLeakingTests = results.reduce((a, r) => a + r.leaking_tests.length, 0);
63398
63611
  const anyFailed = results.some((r) => !r.ok);
63399
63612
  console.log("");
63400
63613
  console.log(` ${C.dim("Files ")} ${C.green(`${totalFilesPassed} passed`)}` + (totalFilesFailed ? ` ${C.dim("|")} ${C.red(`${totalFilesFailed} failed`)}` : "") + C.dim(` (${totalFiles})`));
63401
63614
  console.log(` ${C.dim("Tests ")} ${C.green(`${totalPassed} passed`)}` + (totalFailed ? ` ${C.dim("|")} ${C.red(`${totalFailed} failed`)}` : "") + C.dim(` (${totalTests})`));
63402
- if (totalLeaks) console.log(` ${C.dim("Leaks ")} ${C.yellow(`${totalLeaks} reported`)}` + C.dim(" (--audit; leaked files count as failures)"));
63615
+ if (totalLeaks) console.log(` ${C.dim("Leaks ")} ${C.yellow(`${totalLeakingTests} leaking test(s)`)}` + (totalLeakingTests !== totalLeaks ? C.dim(` (${totalLeaks} leaked file(s))`) : "") + C.dim(" (--audit; leaking tests fail their file)"));
63403
63616
  console.log(` ${C.dim(" Time ")} ${format_duration(elapsed)}`);
63404
63617
  console.log("");
63405
63618
  return !anyFailed;
@@ -63681,7 +63894,7 @@ function processFile(filename, config, mode, program_args) {
63681
63894
  console.log(`Created ${codefile} in ${(compileTime - startTime).toFixed(2)}ms`);
63682
63895
  console.log("");
63683
63896
  startTime = performance.now();
63684
- const audit_obj = config.audit ? compile_audit_runtime(config, resolved, buildDir) : void 0;
63897
+ const audit_obj = config.audit || result.code.includes("nomen_audit_count") ? compile_audit_runtime(config, resolved, buildDir) : void 0;
63685
63898
  let link_inputs = codefile;
63686
63899
  if (companionfile) link_inputs += ` ${companionfile}`;
63687
63900
  if (audit_obj) link_inputs += ` ${audit_obj}`;