nomen-lang 0.6.8 → 0.6.10

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,13 +1,14 @@
1
1
  #! /usr/bin/env node
2
- import { execFileSync, execSync } from "node:child_process";
2
+ import { execFile, execFileSync, execSync } 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";
6
6
  import { EventEmitter } from "node:events";
7
7
  import { lstat, open, readdir, realpath, stat as stat$1 } from "node:fs/promises";
8
8
  import { Readable } from "node:stream";
9
- import { type } from "node:os";
9
+ import os, { type } from "node:os";
10
10
  import { fileURLToPath } from "node:url";
11
+ import { promisify } from "node:util";
11
12
  //#region ../node_modules/.pnpm/readdirp@5.1.1/node_modules/readdirp/index.js
12
13
  const EntryTypes = {
13
14
  FILE_TYPE: "files",
@@ -2121,8 +2122,40 @@ function try_parse_operands(parts) {
2121
2122
  * is not an instruction — label, directive, comment, blank, OR an
2122
2123
  * instruction outside the contract table). Shared by the validator and the
2123
2124
  * phase-2 optimizer so both see identical shapes.
2124
- */
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
+ }
2125
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) {
2126
2159
  const t = strip_comment$1(text).trim();
2127
2160
  const m = /^([a-z][a-z0-9.]*)\s*(.*)$/.exec(t);
2128
2161
  if (!m) return null;
@@ -2406,34 +2439,13 @@ function validate_asm(code) {
2406
2439
  }
2407
2440
  return errors;
2408
2441
  }
2409
- /**
2410
- * The deferred phase-1 check: every function's sp must return to its entry
2411
- * value at each `ret`. A LINEAR scan double-counts across diamond control
2412
- * flow (`b .epilogue` skipping the sibling path's `add sp` — the classic
2413
- * false positive on bool_to_string), so this runs a per-block dataflow over
2414
- * the lifted function bodies:
2415
- *
2416
- * - Block splitting: labels and branch/terminator instructions bound blocks,
2417
- * exactly like the CFG the emitters produce.
2418
- * - Per-instruction sp effects: pre-indexed `stp/str [sp, #-N]!` subtracts,
2419
- * post-indexed `ldp/ldr [sp], #N` adds, `sub/add sp, sp, #imm` adjusts;
2420
- * any other sp write (`mov sp, xN`, register-scaled adds, `ldr sp, …`)
2421
- * is UNKNOWN and conservatively poisons downstream deltas.
2422
- * - Join = require equal deltas or unknown: a label reached by paths with
2423
- * differing deltas becomes unknown; only a KNOWN non-zero delta at a
2424
- * `ret` is reported. Unknown never errors — the check is conservative by
2425
- * construction (no false positives, only silence).
2426
- * - `bl`/`blr` preserve sp (the callee rebalances under the ABI) and do not
2427
- * terminate blocks; `br xN` (tail-jump) ends propagation with no check;
2428
- * `svc` continues (the kernel preserves sp).
2429
- * - Branch targets resolve within the function, including the numeric local
2430
- * form raw `#arch` blocks use (`1:` / `b.hs 1f` — forward/backward by
2431
- * suffix); an unresolvable target makes downstream deltas unknown.
2432
- */
2433
- function validate_stack_balance(code) {
2434
- const { result } = lift_functions(code);
2435
- if (!result.ok) return [];
2442
+ /** Stack-balance check over an ALREADY-lifted result — lets the build
2443
+ * pipeline lift+validate once and share the structured functions between
2444
+ * the shape/branch validation and the balance pass (lift_functions runs
2445
+ * validate_asm internally, so re-lifting per check doubled the cost). */
2446
+ function stack_balance_errors_from_lift(result) {
2436
2447
  const errors = [];
2448
+ if (!result.ok) return [];
2437
2449
  for (const fn of result.functions) check_function_balance(fn, errors);
2438
2450
  return errors;
2439
2451
  }
@@ -2561,37 +2573,37 @@ function line_effect(text) {
2561
2573
  };
2562
2574
  }
2563
2575
  /** Resolve a branch target token to a body line index, or null when the
2564
- * target is outside this function / unresolved (numeric f/b forms scan
2565
- * linearly, matching the assembler's semantics). */
2566
- function resolve_target(token, from, body) {
2576
+ * target is outside this function. Backed by the per-function label maps
2577
+ * built once in check_function_balance — a linear rescan of the body per
2578
+ * pending branch used to be the single hottest spot in large builds
2579
+ * (branches × body-length re-stripping + regex). Numeric `Nf`/`Nb` forms
2580
+ * resolve directionally against the (possibly redefined) definitions,
2581
+ * matching the assembler's semantics; a named label resolves to its FIRST
2582
+ * definition (the old body scan returned the first match too). */
2583
+ function resolve_target(token, from, named_labels, numeric_labels) {
2567
2584
  const numeric = /^(\d+)([fb])$/.exec(token);
2568
2585
  if (numeric) {
2569
- const n = numeric[1];
2586
+ const defs = numeric_labels.get(numeric[1]);
2587
+ if (!defs) return null;
2570
2588
  if (numeric[2] === "f") {
2571
- for (let i = from + 1; i < body.length; i++) {
2572
- const lm = /^(\d+):/.exec(strip_comment$1(body[i].text).trim());
2573
- if (lm && lm[1] === n) return i;
2574
- }
2589
+ for (let k = 0; k < defs.length; k++) if (defs[k] > from) return defs[k];
2575
2590
  return null;
2576
2591
  }
2577
- for (let i = from - 1; i >= 0; i--) {
2578
- const lm = /^(\d+):/.exec(strip_comment$1(body[i].text).trim());
2579
- if (lm && lm[1] === n) return i;
2580
- }
2592
+ for (let k = defs.length - 1; k >= 0; k--) if (defs[k] < from) return defs[k];
2581
2593
  return null;
2582
2594
  }
2583
- for (let i = 0; i < body.length; i++) {
2584
- const lm = LABEL_RE.exec(strip_comment$1(body[i].text).trim());
2585
- if (lm && lm[1] === token) return i;
2586
- }
2587
- return null;
2595
+ return named_labels.get(token) ?? null;
2588
2596
  }
2589
2597
  function check_function_balance(fn, errors) {
2590
2598
  const body = fn.body;
2599
+ const stripped_text = [];
2591
2600
  const effects = [];
2592
2601
  const is_code = [];
2602
+ const named_labels = /* @__PURE__ */ new Map();
2603
+ const numeric_labels = /* @__PURE__ */ new Map();
2593
2604
  for (let i = 0; i < body.length; i++) {
2594
2605
  const t = strip_comment$1(body[i].text).trim();
2606
+ stripped_text.push(t);
2595
2607
  effects.push({
2596
2608
  delta: null,
2597
2609
  branch: null,
@@ -2600,8 +2612,15 @@ function check_function_balance(fn, errors) {
2600
2612
  unknown_jump: false
2601
2613
  });
2602
2614
  is_code.push(false);
2603
- if (!t) continue;
2604
2615
  const lm = LABEL_RE.exec(t);
2616
+ if (lm) {
2617
+ if (/^\d+$/.test(lm[1])) {
2618
+ const defs = numeric_labels.get(lm[1]);
2619
+ if (defs) defs.push(i);
2620
+ else numeric_labels.set(lm[1], [i]);
2621
+ } else if (!named_labels.has(lm[1])) named_labels.set(lm[1], i);
2622
+ }
2623
+ if (!t) continue;
2605
2624
  const rest = lm ? lm[2].trim() : t;
2606
2625
  if (rest.startsWith(".") || /^[\w.$]+\s*=\s*[\w.$]+$/.test(rest) || !rest) continue;
2607
2626
  is_code[i] = true;
@@ -2613,7 +2632,7 @@ function check_function_balance(fn, errors) {
2613
2632
  let cur = -1;
2614
2633
  let terminated = true;
2615
2634
  for (let i = 0; i < body.length; i++) {
2616
- const t = strip_comment$1(body[i].text).trim();
2635
+ const t = stripped_text[i];
2617
2636
  if (!t) continue;
2618
2637
  if (LABEL_RE.test(t)) {
2619
2638
  cur++;
@@ -2661,7 +2680,7 @@ function check_function_balance(fn, errors) {
2661
2680
  }
2662
2681
  }
2663
2682
  for (const p of pending) {
2664
- const line = resolve_target(p.token, p.from, body);
2683
+ const line = resolve_target(p.token, p.from, named_labels, numeric_labels);
2665
2684
  blocks[p.block].targets.push(line === null ? null : line_block.get(line) ?? null);
2666
2685
  }
2667
2686
  const preds = Array.from({ length: blocks.length }, () => []);
@@ -8971,6 +8990,85 @@ function record_ctor_heap_default_fields(var_name, struct_node, status, is_owned
8971
8990
  }
8972
8991
  }
8973
8992
  //#endregion
8993
+ //#region ../src/build_common/destroy_analysis.ts
8994
+ /**
8995
+ * Whether any field of the struct is an enum-with-data value owning heap
8996
+ * payloads (a string payload — always heap, case construction strdups it —
8997
+ * or a class/trait reference payload — construction transfers ownership).
8998
+ * Such a field must be reclaimed when the struct dies.
8999
+ */
9000
+ function struct_has_owning_enum_field(struct, table) {
9001
+ for (const field of struct.fields) {
9002
+ if (field.type.is_ref) continue;
9003
+ const en = table.enums?.find((e) => e.name === field.type.name);
9004
+ if (!en?.has_associated_data) continue;
9005
+ for (const c of en.cases) for (const p of c.params) {
9006
+ if (p.type.name === "string" && !p.type.is_array) return true;
9007
+ if (table.traits?.some((t) => t.name === p.type.name)) return true;
9008
+ if (table.structs.find((s) => s.name === p.type.name)?.is_class) return true;
9009
+ }
9010
+ }
9011
+ return false;
9012
+ }
9013
+ /**
9014
+ * Whether a struct declares its own `#destroy` (user side effects /
9015
+ * resource release).
9016
+ */
9017
+ function has_destroy(struct) {
9018
+ return !!struct.functions.find((f) => f.name === "#destroy");
9019
+ }
9020
+ /**
9021
+ * Whether any field (recursively) requires destroy handling — a class
9022
+ * field (heap instance to reclaim) or a nested struct with its own
9023
+ * `#destroy` (or nested fields that do). Deliberately excludes `string`
9024
+ * fields: a struct local whose only owning fields are strings is NOT
9025
+ * auto-destroyed at scope exit (the strings may be raw rodata arg
9026
+ * pointers, not heap) — see `struct_needs_auto_destroy` for the broader
9027
+ * form used to decide whether a `<Struct>_destroy` function is generated.
9028
+ */
9029
+ function has_struct_fields_with_destroy(struct, table) {
9030
+ for (const field of struct.fields) {
9031
+ if (field.type.is_ref) continue;
9032
+ const field_struct = resolve_struct_type(field.type, table);
9033
+ if (!field_struct) continue;
9034
+ if (field_struct.is_class) return true;
9035
+ if (has_destroy(field_struct)) return true;
9036
+ if (has_struct_fields_with_destroy(field_struct, table)) return true;
9037
+ }
9038
+ return false;
9039
+ }
9040
+ /**
9041
+ * Whether a struct (or any embedded struct field, recursively) needs a
9042
+ * destroy call at scope exit — it has a `#destroy`, a class-typed field,
9043
+ * or a nested struct field that itself needs destroying. This does NOT
9044
+ * count `string` fields (see `has_struct_fields_with_destroy`).
9045
+ */
9046
+ function struct_needs_destroy(struct, table) {
9047
+ return has_destroy(struct) || has_struct_fields_with_destroy(struct, table) || struct_has_owning_enum_field(struct, table);
9048
+ }
9049
+ /**
9050
+ * Whether a struct needs an auto-generated `<Struct>_destroy` function.
9051
+ * Broader than `struct_needs_destroy`: also returns true for structs with
9052
+ * `string` fields, because a Buffer<T> for such a struct deep-copies the
9053
+ * strings into slots (strdup on store) and the per-element destroy must
9054
+ * free them. The generated destroy frees each string field; it is called
9055
+ * from Buffer's specialized #destroy / replace_T, NOT from struct local
9056
+ * scope exit (`struct_needs_destroy` governs that path and excludes
9057
+ * strings).
9058
+ */
9059
+ function struct_needs_auto_destroy(struct, table) {
9060
+ if (struct_needs_destroy(struct, table)) return true;
9061
+ if (struct_has_owning_enum_field(struct, table)) return true;
9062
+ for (const field of struct.fields) {
9063
+ if (field.type.is_ref) continue;
9064
+ if (field.type.is_view) continue;
9065
+ if (field.type.name === "string" && !field.type.is_array) return true;
9066
+ const field_struct = resolve_struct_type(field.type, table);
9067
+ if (field_struct && !field_struct.is_class && struct_needs_auto_destroy(field_struct, table)) return true;
9068
+ }
9069
+ return false;
9070
+ }
9071
+ //#endregion
8974
9072
  //#region ../src/nodes/child_nodes.ts
8975
9073
  /** Whether `value` is an AST node (carries a string `node_type`). */
8976
9074
  function is_ast_node(value) {
@@ -11647,85 +11745,6 @@ function array_struct_name(type, status) {
11647
11745
  return status.structs.find((s) => s.name === mono && !s.is_generic) ? mono : void 0;
11648
11746
  }
11649
11747
  //#endregion
11650
- //#region ../src/build_common/destroy_analysis.ts
11651
- /**
11652
- * Whether any field of the struct is an enum-with-data value owning heap
11653
- * payloads (a string payload — always heap, case construction strdups it —
11654
- * or a class/trait reference payload — construction transfers ownership).
11655
- * Such a field must be reclaimed when the struct dies.
11656
- */
11657
- function struct_has_owning_enum_field(struct, table) {
11658
- for (const field of struct.fields) {
11659
- if (field.type.is_ref) continue;
11660
- const en = table.enums?.find((e) => e.name === field.type.name);
11661
- if (!en?.has_associated_data) continue;
11662
- for (const c of en.cases) for (const p of c.params) {
11663
- if (p.type.name === "string" && !p.type.is_array) return true;
11664
- if (table.traits?.some((t) => t.name === p.type.name)) return true;
11665
- if (table.structs.find((s) => s.name === p.type.name)?.is_class) return true;
11666
- }
11667
- }
11668
- return false;
11669
- }
11670
- /**
11671
- * Whether a struct declares its own `#destroy` (user side effects /
11672
- * resource release).
11673
- */
11674
- function has_destroy(struct) {
11675
- return !!struct.functions.find((f) => f.name === "#destroy");
11676
- }
11677
- /**
11678
- * Whether any field (recursively) requires destroy handling — a class
11679
- * field (heap instance to reclaim) or a nested struct with its own
11680
- * `#destroy` (or nested fields that do). Deliberately excludes `string`
11681
- * fields: a struct local whose only owning fields are strings is NOT
11682
- * auto-destroyed at scope exit (the strings may be raw rodata arg
11683
- * pointers, not heap) — see `struct_needs_auto_destroy` for the broader
11684
- * form used to decide whether a `<Struct>_destroy` function is generated.
11685
- */
11686
- function has_struct_fields_with_destroy(struct, table) {
11687
- for (const field of struct.fields) {
11688
- if (field.type.is_ref) continue;
11689
- const field_struct = resolve_struct_type(field.type, table);
11690
- if (!field_struct) continue;
11691
- if (field_struct.is_class) return true;
11692
- if (has_destroy(field_struct)) return true;
11693
- if (has_struct_fields_with_destroy(field_struct, table)) return true;
11694
- }
11695
- return false;
11696
- }
11697
- /**
11698
- * Whether a struct (or any embedded struct field, recursively) needs a
11699
- * destroy call at scope exit — it has a `#destroy`, a class-typed field,
11700
- * or a nested struct field that itself needs destroying. This does NOT
11701
- * count `string` fields (see `has_struct_fields_with_destroy`).
11702
- */
11703
- function struct_needs_destroy(struct, table) {
11704
- return has_destroy(struct) || has_struct_fields_with_destroy(struct, table) || struct_has_owning_enum_field(struct, table);
11705
- }
11706
- /**
11707
- * Whether a struct needs an auto-generated `<Struct>_destroy` function.
11708
- * Broader than `struct_needs_destroy`: also returns true for structs with
11709
- * `string` fields, because a Buffer<T> for such a struct deep-copies the
11710
- * strings into slots (strdup on store) and the per-element destroy must
11711
- * free them. The generated destroy frees each string field; it is called
11712
- * from Buffer's specialized #destroy / replace_T, NOT from struct local
11713
- * scope exit (`struct_needs_destroy` governs that path and excludes
11714
- * strings).
11715
- */
11716
- function struct_needs_auto_destroy(struct, table) {
11717
- if (struct_needs_destroy(struct, table)) return true;
11718
- if (struct_has_owning_enum_field(struct, table)) return true;
11719
- for (const field of struct.fields) {
11720
- if (field.type.is_ref) continue;
11721
- if (field.type.is_view) continue;
11722
- if (field.type.name === "string" && !field.type.is_array) return true;
11723
- const field_struct = resolve_struct_type(field.type, table);
11724
- if (field_struct && !field_struct.is_class && struct_needs_auto_destroy(field_struct, table)) return true;
11725
- }
11726
- return false;
11727
- }
11728
- //#endregion
11729
11748
  //#region ../src/build_common/scan_force_heap_strings.ts
11730
11749
  function scan_force_heap_strings(statements, structs) {
11731
11750
  const result = /* @__PURE__ */ new Set();
@@ -14908,7 +14927,33 @@ function emit_base_ptr(status, decl_name, is_class_parent) {
14908
14927
  if (is_class_parent) emit_var_load(status, "x0", decl_name, 8);
14909
14928
  else emit_var_address(status, "x0", decl_name);
14910
14929
  }
14911
- function emit_field_destroys(status, struct_type, decl_name, base_offset, is_class_parent, free_strings = true) {
14930
+ /**
14931
+ * Reclaim a struct's owning fields in place (base + every nested owning field).
14932
+ *
14933
+ * `base_offset` (added to each field offset) and `is_class_parent` (a class's
14934
+ * base is a pointer LOADED from the slot; a value struct is inline AT it)
14935
+ * locate `decl_name`'s storage.
14936
+ *
14937
+ * `free_strings` is REQUIRED — it is a per-call-site OWNOWNITY fact, not a
14938
+ * per-type one, so it deliberately has no default:
14939
+ * - `true` from a DESTROY function (`<T>_destroy`), which only ever runs on
14940
+ * heap-owned storage: a class's plain string fields are always heap-owned
14941
+ * (`_init` strdup's defaults, assignments strdup a non-heap RHS), and a
14942
+ * VALUE struct arrives with the per-slot copies `store_T`/`_copy` strdup'd
14943
+ * (Buffer per-element teardown, the displaced-field reclaim, the
14944
+ * anchor-slot walk). Matches C's build_auto_destroy, which frees string
14945
+ * fields unconditionally.
14946
+ * - `false` from struct-LOCAL scope exit, where a string field may still hold
14947
+ * a rodata literal (aarch64 constructors don't strdup) and freeing it would
14948
+ * `free` rodata (SIGABRT).
14949
+ *
14950
+ * A default (`= true`) previously stood in for the `true` arm while
14951
+ * `node.is_class` (undefined on plain parsed structs) was passed as if it
14952
+ * selected between the arms — so the day `is_class` becomes explicitly false,
14953
+ * value-struct slot strings would silently stop being reclaimed. Passing the
14954
+ * boolean at every call site makes both arms visible.
14955
+ */
14956
+ function emit_field_destroys(status, struct_type, decl_name, base_offset, is_class_parent, free_strings) {
14912
14957
  const destroy_fields = struct_type.is_class ? layout_fields(struct_type, status) : struct_type.fields;
14913
14958
  for (const field of destroy_fields) {
14914
14959
  const offset = get_field_offset_of_fields(destroy_fields, field.name, status);
@@ -15134,6 +15179,33 @@ function emit_destroy_for_scope(status, declarations_before) {
15134
15179
  emit_asm(status, `ldr x19, [sp], #16\n`);
15135
15180
  continue;
15136
15181
  }
15182
+ if (status.heap_array_vars?.has(decl.name)) {
15183
+ if (status.heap_owned_string_arrays?.has(decl.name)) {
15184
+ emit_asm(status, `str x19, [sp, #-16]!\n`);
15185
+ emit_asm(status, `str x20, [sp, #-16]!\n`);
15186
+ emit_var_load(status, "x0", decl.name, 8);
15187
+ emit_asm(status, `mov x19, x0\n`);
15188
+ emit_asm(status, `ldr x20, [x19]\n`);
15189
+ emit_asm(status, `add x19, x19, #8\n`);
15190
+ const loop = `.Lhosa_${decl.name}`;
15191
+ emit_asm(status, `${loop}:\n`);
15192
+ emit_asm(status, `cbz x20, .Lhosa_done_${decl.name}\n`);
15193
+ emit_asm(status, `ldr x0, [x19]\n`);
15194
+ emit_free(status);
15195
+ emit_asm(status, `add x19, x19, #16\n`);
15196
+ emit_asm(status, `sub x20, x20, #1\n`);
15197
+ emit_asm(status, `b ${loop}\n`);
15198
+ emit_asm(status, `.Lhosa_done_${decl.name}:\n`);
15199
+ emit_var_load(status, "x0", decl.name, 8);
15200
+ emit_free(status);
15201
+ emit_asm(status, `ldr x20, [sp], #16\n`);
15202
+ emit_asm(status, `ldr x19, [sp], #16\n`);
15203
+ continue;
15204
+ }
15205
+ emit_var_load(status, "x0", decl.name, 8);
15206
+ emit_free(status);
15207
+ continue;
15208
+ }
15137
15209
  if (status.heap_strings?.has(decl.name)) {
15138
15210
  emit_var_load(status, "x0", decl.name, 8);
15139
15211
  emit_free(status);
@@ -21187,7 +21259,7 @@ function build_destroy_function(node, func, status) {
21187
21259
  status.code = status.code.slice(0, after_prologue) + saves + status.code.slice(after_prologue);
21188
21260
  }
21189
21261
  }
21190
- if (node.is_class) emit_field_destroys(status, node, "self", void 0, false);
21262
+ if (node.is_class) emit_field_destroys(status, node, "self", void 0, false, true);
21191
21263
  emit_asm(status, `${return_label}:\n`);
21192
21264
  const total_stack = Math.ceil((status.stack_size || 0) / 16) * 16;
21193
21265
  status.code = status.code.replace(`sub sp, sp, #${stack_placeholder}`, total_stack > 0 ? `sub sp, sp, #${total_stack}` : `// no stack needed`);
@@ -21244,7 +21316,7 @@ function build_auto_destroy_function(node, status) {
21244
21316
  status.function_param_regs.set("self", "x19");
21245
21317
  emit_asm(status, `sub sp, sp, #${stack_placeholder}\n`);
21246
21318
  emit_asm(status, `mov x29, sp\n`);
21247
- emit_field_destroys(status, node, "self", void 0, false, node.is_class);
21319
+ emit_field_destroys(status, node, "self", void 0, false, true);
21248
21320
  emit_asm(status, `${return_label}:\n`);
21249
21321
  const total_stack = Math.ceil((status.stack_size || 0) / 16) * 16;
21250
21322
  status.code = status.code.replace(`sub sp, sp, #${stack_placeholder}`, total_stack > 0 ? `sub sp, sp, #${total_stack}` : `// no stack needed`);
@@ -24960,6 +25032,17 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
24960
25032
  check_heap();
24961
25033
  return;
24962
25034
  }
25035
+ 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";
25036
+ if (decl_string_field_read && !!status.force_heap_strings?.has(node.name)) {
25037
+ emit_init_value$1(node.value, nir_init, status);
25038
+ ensure_newline(status);
25039
+ emit_strdup_string(status);
25040
+ emit_var_store(status, "x0", node.name, size);
25041
+ mark_heap_string(status, node.name);
25042
+ status.last_result_is_heap = false;
25043
+ return;
25044
+ }
25045
+ if (decl_string_field_read) status.last_result_is_heap = false;
24963
25046
  emit_init_value$1(node.value, nir_init, status);
24964
25047
  emit_var_store(status, "x0", node.name, size);
24965
25048
  check_heap();
@@ -33540,7 +33623,8 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
33540
33623
  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) {
33541
33624
  const access_lhs = node.left_value;
33542
33625
  const field_type = access_lhs.access.type;
33543
- const field_struct = field_type?.name && !field_type.is_view && !field_type.is_array ? status.structs.find((s) => s.name === field_type.name && !s.is_simple_type && !s.is_class) : void 0;
33626
+ const field_resolved = field_type?.name && !field_type.is_view && !field_type.is_array ? resolve_struct_type(field_type, status) : void 0;
33627
+ const field_struct = field_resolved && !field_resolved.is_class ? field_resolved : void 0;
33544
33628
  if (field_struct) {
33545
33629
  splice_decl_from_c_scopes(status, node.right_value.value);
33546
33630
  const saved_len = begin_code_scratch(status);
@@ -39017,10 +39101,31 @@ function build_access_node$1(node, status) {
39017
39101
  status.code += `${ret_c} ${ctor_temp_val} = `;
39018
39102
  }
39019
39103
  }
39104
+ const owned_string_recv = !ctor_temp_receiver && (node.target.node_type === "func_call" || node.target.node_type === "access" && node.target.access.node_type === "access_func") && method_type?.name === "string" && !method_type.is_view && !is_string_borrow(node.target);
39105
+ let owned_recv_temp;
39106
+ let owned_recv_val;
39107
+ if (owned_string_recv) {
39108
+ const id = status.label_counter = (status.label_counter ?? 0) + 1;
39109
+ owned_recv_temp = `_osrecv_${id}`;
39110
+ status.code += `({ nomen_string ${owned_recv_temp} = `;
39111
+ {
39112
+ const saved_suppress = status.suppress_dereference;
39113
+ status.suppress_dereference = true;
39114
+ build_node$1(node.target, status);
39115
+ status.suppress_dereference = saved_suppress;
39116
+ }
39117
+ status.code += `; `;
39118
+ const recv_ret_c = c_return_type((target_method ?? trait_default_func)?.return_type ?? access_func.type, status);
39119
+ if (recv_ret_c !== "void") {
39120
+ owned_recv_val = `_osrecvv_${id}`;
39121
+ status.code += `${recv_ret_c} ${owned_recv_val} = `;
39122
+ }
39123
+ }
39020
39124
  if (lambda_ret_tmp) status.code += `${lambda_ret_c} ${lambda_ret_tmp} = `;
39021
39125
  status.code += `${label}(`;
39022
39126
  if (!access_func.is_static) {
39023
39127
  if (ctor_temp_free) status.code += ctor_temp_free;
39128
+ else if (owned_recv_temp) status.code += owned_recv_temp;
39024
39129
  else if (array_wrap) status.code += array_wrap.self_expr;
39025
39130
  else {
39026
39131
  const method_self_is_ref = !!target_method?.params?.some((p) => p.is_self_param && (p.is_ref || p.type?.is_ref));
@@ -39105,6 +39210,11 @@ function build_access_node$1(node, status) {
39105
39210
  if (ctor_temp_val) status.code += ` ${ctor_temp_val};`;
39106
39211
  status.code += ` })`;
39107
39212
  }
39213
+ if (owned_recv_temp) {
39214
+ status.code += `; ${status.audit ? "nomen_free_wrap" : "free"}(${owned_recv_temp}.ptr);`;
39215
+ if (owned_recv_val) status.code += ` ${owned_recv_val};`;
39216
+ status.code += ` })`;
39217
+ }
39108
39218
  if (lambda_arg_temps.size > 0) {
39109
39219
  for (const tmp of lambda_arg_temps.values()) status.code += `; ${closure_dispose_arm(tmp)}`;
39110
39220
  if (lambda_ret_tmp) status.code += `; ${lambda_ret_tmp}`;
@@ -41629,7 +41739,9 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41629
41739
  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;
41630
41740
  const move_source = rhs_is_string_var && node.last_use_move && move_on_last_use_enabled() ? rhs_value.value : void 0;
41631
41741
  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;
41632
- const borrow_needs_dup = !node.swap && size === 16 && is_string_borrow(node.right_value) && !!status.force_heap_strings?.has(name);
41742
+ const rhs_is_borrow_reception = !node.swap && size === 16 && is_string_borrow(node.right_value);
41743
+ const rhs_is_field_read = !node.swap && size === 16 && node.right_value.node_type === "access" && node.right_value.access.node_type === "access_field";
41744
+ const borrow_needs_dup = (rhs_is_borrow_reception || rhs_is_field_read) && !!status.force_heap_strings?.has(name);
41633
41745
  const move_source_owns_heap = move_source !== void 0 && !!status.heap_strings?.has(move_source);
41634
41746
  if (rhs_is_view_into_owned) {
41635
41747
  if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
@@ -41639,7 +41751,8 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41639
41751
  if (move_source !== void 0 && move_source_owns_heap) status.heap_strings?.delete(move_source);
41640
41752
  if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
41641
41753
  status.heap_strings.add(name);
41642
- } else if (status.last_result_is_heap) {
41754
+ } else if (rhs_is_field_read) status.last_result_is_heap = false;
41755
+ else if (status.last_result_is_heap) {
41643
41756
  if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
41644
41757
  status.heap_strings.add(name);
41645
41758
  } else if (explicit_move_source !== void 0 && status.heap_strings?.delete(explicit_move_source)) status.heap_strings.add(name);
@@ -41856,6 +41969,22 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41856
41969
  get_base_address(access, status, "x0");
41857
41970
  emit_asm(status, `str x0, [sp, #-16]!\n`);
41858
41971
  }
41972
+ if (!node.operator && node.right_value.node_type === "value" && node.right_value.is_moved) {
41973
+ const field_resolved = resolve_struct_type(field_type, status);
41974
+ const field_target_var = access.target.node_type === "value" ? access.target.value : "";
41975
+ const first_init_write = is_first_init_field_write(field_target_var, field_name, status);
41976
+ mark_init_field_write(field_target_var, field_name, status);
41977
+ if (field_resolved && !field_resolved.is_class && !first_init_write && struct_needs_auto_destroy(field_resolved, status)) {
41978
+ if (base_reg !== void 0) {
41979
+ if (offset !== 0) emit_asm(status, `add x0, ${base_reg}, #${offset}\n`);
41980
+ else emit_asm(status, `mov x0, ${base_reg}\n`);
41981
+ } else {
41982
+ emit_asm(status, `ldr x0, [sp]\n`);
41983
+ if (offset !== 0) emit_asm(status, `add x0, x0, #${offset}\n`);
41984
+ }
41985
+ emit_asm(status, `bl ${field_resolved.name}_destroy\n`);
41986
+ }
41987
+ }
41859
41988
  get_source_address(node.right_value, status, nir_rhs);
41860
41989
  ensure_newline(status);
41861
41990
  emit_asm(status, `mov x1, x0\n`);
@@ -45635,7 +45764,7 @@ function build_access_method(node, access_func, status) {
45635
45764
  status.last_result_is_heap = false;
45636
45765
  build_node(node.target, status);
45637
45766
  ensure_newline(status);
45638
- frees_string_receiver = method_name === "string_to_string" && status.last_result_is_heap;
45767
+ frees_string_receiver = status.last_result_is_heap;
45639
45768
  status.last_result_is_heap = false;
45640
45769
  emit_asm(status, `stp x0, x1, [sp, #-16]!\n`);
45641
45770
  }
@@ -47257,7 +47386,8 @@ function build(root, options = {}) {
47257
47386
  status.code = status.code.replaceAll("bl _strdup\n", "bl _nomen_strdup_wrap\n");
47258
47387
  status.code = status.code.replaceAll("bl _free\n", "bl _nomen_free_wrap\n");
47259
47388
  }
47260
- const lift_errors = validate_asm(status.code);
47389
+ const { result: lifted } = lift_functions(status.code);
47390
+ const lift_errors = lifted.errors;
47261
47391
  if (lift_errors.length > 0) {
47262
47392
  if (!status.build_errors) status.build_errors = [];
47263
47393
  for (const e of lift_errors.slice(0, 20)) status.build_errors.push({
@@ -47265,7 +47395,7 @@ function build(root, options = {}) {
47265
47395
  start: 0
47266
47396
  });
47267
47397
  }
47268
- const balance_errors = validate_stack_balance(status.code);
47398
+ const balance_errors = stack_balance_errors_from_lift(lifted);
47269
47399
  if (balance_errors.length > 0) {
47270
47400
  if (!status.build_errors) status.build_errors = [];
47271
47401
  for (const e of balance_errors.slice(0, 20)) status.build_errors.push({
@@ -62443,7 +62573,9 @@ const STRING_OPTIONS = /* @__PURE__ */ new Map([
62443
62573
  ["p", "platform"],
62444
62574
  ["lib", "lib"],
62445
62575
  ["l", "lib"],
62446
- ["audit-runtime", "audit_runtime"]
62576
+ ["audit-runtime", "audit_runtime"],
62577
+ ["jobs", "jobs"],
62578
+ ["j", "jobs"]
62447
62579
  ]);
62448
62580
  const BOOLEAN_OPTIONS = /* @__PURE__ */ new Map([
62449
62581
  ["watch", "watch"],
@@ -62553,8 +62685,9 @@ function print_help() {
62553
62685
  " --platform, -p <name> Target platform: macos, ios, linux, android, windows, web",
62554
62686
  " --lib, -l <path> Path to System library directory (containing package.jsonc)",
62555
62687
  " --audit Audit the generated program for memory issues",
62556
- " --audit-runtime <path> Path to audit_runtime.c, linked in when --audit is set",
62557
- " --release, -r Build with optimizations (clang -O2; asm passes on aarch64)",
62688
+ " --audit-runtime <path> Path to audit_runtime.c (default: auto-discovered — walk-up or the CLI-bundled copy)",
62689
+ " --jobs, -j <n> For `nomen test`: max test files run in parallel (default: CPU count)",
62690
+ " --release, -r Build with optimizations (clang -O2; aarch64 asm cleanup passes)",
62558
62691
  " --fast-math Allow FP reassociation (float loop reductions vectorize on aarch64)",
62559
62692
  " --check For `nomen format`: report files that would change",
62560
62693
  " -h, --help Show this help"
@@ -62902,6 +63035,9 @@ function find_bundled(filename) {
62902
63035
  const candidates = !cli_root.includes("node_modules") ? [path.join(repo_root, filename), path.join(cli_root, filename)] : [path.join(cli_root, filename), path.join(repo_root, filename)];
62903
63036
  for (const c of candidates) if (fs.existsSync(c)) return path.resolve(c);
62904
63037
  }
63038
+ function find_bundled_audit_runtime() {
63039
+ return find_bundled("src/audit_runtime.c") ?? find_bundled("audit_runtime.c");
63040
+ }
62905
63041
  /**
62906
63042
  * `nomen init <name>`: scaffold a new project under `./<name>` with a sensible
62907
63043
  * starting layout (package.jsonc, src/main.nm, a starter test, README and an
@@ -62980,6 +63116,7 @@ function outfile_for(input_path, is_test, out) {
62980
63116
  //#endregion
62981
63117
  //#region src/test.ts
62982
63118
  const RECORD_PREFIX = "\\nomen|";
63119
+ const execFile_async = promisify(execFile);
62983
63120
  /** Recursively collect `*.test.nm` files under `root`, skipping build output. */
62984
63121
  function collect_test_files(root) {
62985
63122
  const out = [];
@@ -63063,11 +63200,19 @@ function generate_harness(tests, benches) {
63063
63200
  const n = Math.min(raw, 4096);
63064
63201
  return template.replace(/__NAME__/g, b.name).replace(/__TARGET__/g, b.target).replace(/__N__/g, String(n));
63065
63202
  }).join("\n");
63066
- let main = "\nimport System\nimport System::Test\n\npub func main = () {\n";
63203
+ let main = "\nimport System\nimport System::Test\n";
63204
+ main += "\npub func main = () {\n";
63067
63205
  main += " var Tester t = Tester()\n";
63206
+ main += " var int64 audit_mark = 0\n";
63207
+ main += " var int64 audit_leaked = 0\n";
63068
63208
  for (const test of tests) {
63209
+ main += `\taudit_mark = Tester.audit_count()\n`;
63069
63210
  main += `\tt.begin_test("${escape_nm_string(test.name)}")\n`;
63070
63211
  main += `\t${test.name}(ref t)\n`;
63212
+ main += `\taudit_leaked = Tester.audit_count() - audit_mark\n`;
63213
+ main += `\tif audit_leaked > 0 {\n`;
63214
+ main += `\t\tConsole.write_line("\\\\nomen|leaks|" + "${escape_nm_string(test.name)}" + "|\\{audit_leaked}")\n`;
63215
+ main += `\t}\n`;
63071
63216
  main += `\tt.end_test()\n`;
63072
63217
  }
63073
63218
  for (const bench of benches) {
@@ -63086,6 +63231,7 @@ function parse_records(stdout) {
63086
63231
  const tests = [];
63087
63232
  const fails = [];
63088
63233
  const benches = [];
63234
+ const leaks = [];
63089
63235
  const other = [];
63090
63236
  for (const line of stdout.split("\n")) {
63091
63237
  if (!line.startsWith(RECORD_PREFIX)) {
@@ -63104,6 +63250,10 @@ function parse_records(stdout) {
63104
63250
  test: parts[1],
63105
63251
  message: parts.slice(2).join("|")
63106
63252
  });
63253
+ else if (kind === "leaks") leaks.push({
63254
+ test: parts[1],
63255
+ count: parseInt(parts[2] || "0", 10)
63256
+ });
63107
63257
  else if (kind === "bench") benches.push({
63108
63258
  label: parts[1],
63109
63259
  n: parseInt(parts[2] || "0", 10),
@@ -63118,18 +63268,25 @@ function parse_records(stdout) {
63118
63268
  tests,
63119
63269
  fails,
63120
63270
  benches,
63271
+ leaks,
63121
63272
  other
63122
63273
  };
63123
63274
  }
63124
- /** Pull the exit-time audit "LEAK: N allocation(s)" lines out of stdout. */
63275
+ /**
63276
+ * Pull the exit-time audit lines out of stdout: "LEAK: N allocation(s)" (a
63277
+ * nonzero malloc/free balance) and "AUDIT-STALE-FREE: N pointer(s) freed that
63278
+ * were not live audit allocations" (a free that matched no live block — a
63279
+ * foreign pointer or a double free; the runtime no longer passes those to
63280
+ * libc free, so this is the only record they leave). Both fail the file.
63281
+ */
63125
63282
  function extract_leaks(stdout) {
63126
- return stdout.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("LEAK:"));
63283
+ return stdout.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("LEAK:") || l.startsWith("AUDIT-STALE-FREE:"));
63127
63284
  }
63128
63285
  /**
63129
63286
  * Compile one `*.test.nm` file (with the generated harness) and run it,
63130
63287
  * returning its parsed records and any crash info.
63131
63288
  */
63132
- function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime, release = false, cwd) {
63289
+ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime, release = false, cwd) {
63133
63290
  const start = performance.now();
63134
63291
  const source_text = fs.readFileSync(entry_path, "utf8");
63135
63292
  const harness = generate_harness(extract_test_functions(source_text), extract_bench_functions(source_text));
@@ -63139,8 +63296,9 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
63139
63296
  tests: [],
63140
63297
  fails: [],
63141
63298
  benches: [],
63142
- other: [],
63143
63299
  leaks: [],
63300
+ leaking_tests: [],
63301
+ other: [],
63144
63302
  ms: 0
63145
63303
  };
63146
63304
  const resolved = path.resolve(entry_path);
@@ -63168,24 +63326,21 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
63168
63326
  }
63169
63327
  const buildDir = build_dir_for(resolved, true);
63170
63328
  if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
63171
- let audit_obj;
63172
- if (audit) {
63173
- const runtime_src = audit_runtime ? path.resolve(audit_runtime) : find_audit_runtime(path.dirname(resolved));
63174
- if (!runtime_src || !fs.existsSync(runtime_src)) {
63175
- result.ok = false;
63176
- result.phase = "setup";
63177
- result.crashed = "Audit enabled but audit_runtime.c was not found. Pass --audit-runtime <path>.";
63178
- result.ms = performance.now() - start;
63179
- return result;
63180
- }
63181
- audit_obj = path.join(buildDir, "audit_runtime.o");
63182
- execFileSync("clang", [
63183
- "-c",
63184
- runtime_src,
63185
- "-o",
63186
- audit_obj
63187
- ]);
63329
+ const runtime_src = audit_runtime ? path.resolve(audit_runtime) : find_audit_runtime(path.dirname(resolved)) ?? find_bundled_audit_runtime();
63330
+ if (!runtime_src || !fs.existsSync(runtime_src)) {
63331
+ result.ok = false;
63332
+ result.phase = "setup";
63333
+ result.crashed = "audit_runtime.c was not found. Pass --audit-runtime <path>.";
63334
+ result.ms = performance.now() - start;
63335
+ return result;
63188
63336
  }
63337
+ const audit_obj = path.join(buildDir, "audit_runtime.o");
63338
+ await execFile_async("clang", [
63339
+ "-c",
63340
+ runtime_src,
63341
+ "-o",
63342
+ audit_obj
63343
+ ]);
63189
63344
  const platform = default_platform();
63190
63345
  const ext = arch === "aarch64" ? ".s" : platform === "macos" || platform === "ios" ? ".m" : ".c";
63191
63346
  const codefile = path.join(buildDir, path.basename(entry_path, ".nm") + ext);
@@ -63208,14 +63363,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
63208
63363
  if (audit_obj) link_args.push(audit_obj);
63209
63364
  if (platform === "macos" || platform === "ios") link_args.push("-framework", "CoreGraphics", "-framework", "Foundation", "-framework", "AppKit", "-lobjc");
63210
63365
  try {
63211
- execFileSync("clang", link_args, {
63212
- encoding: "utf8",
63213
- stdio: [
63214
- "ignore",
63215
- "pipe",
63216
- "pipe"
63217
- ]
63218
- });
63366
+ await execFile_async("clang", link_args, { encoding: "utf8" });
63219
63367
  } catch (err) {
63220
63368
  const stderr = err.stderr ? err.stderr.toString() : err.message ?? "";
63221
63369
  result.ok = false;
@@ -63227,17 +63375,12 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
63227
63375
  let runStdout = "";
63228
63376
  let crashed;
63229
63377
  try {
63230
- runStdout = execFileSync(outfile, [], {
63378
+ runStdout = (await execFile_async(outfile, [], {
63231
63379
  encoding: "utf8",
63232
- stdio: [
63233
- "ignore",
63234
- "pipe",
63235
- "pipe"
63236
- ],
63237
63380
  timeout: 3e4,
63238
63381
  maxBuffer: 16777216,
63239
63382
  cwd: cwd ?? process.cwd()
63240
- });
63383
+ })).stdout;
63241
63384
  } catch (err) {
63242
63385
  runStdout = err.stdout ? err.stdout.toString() : "";
63243
63386
  if (err.signal === "SIGTERM") crashed = "test binary timed out after 30s";
@@ -63247,9 +63390,10 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
63247
63390
  result.tests = records.tests;
63248
63391
  result.fails = records.fails;
63249
63392
  result.benches = records.benches;
63393
+ result.leaking_tests = records.leaks;
63250
63394
  result.other = records.other;
63251
63395
  result.leaks = audit ? extract_leaks(runStdout) : [];
63252
- if (result.leaks.length) result.other = result.other.filter((l) => !l.trim().startsWith("LEAK:"));
63396
+ if (result.leaks.length) result.other = result.other.filter((l) => !l.trim().startsWith("LEAK:") && !l.trim().startsWith("AUDIT-STALE-FREE:"));
63253
63397
  result.crashed = crashed;
63254
63398
  if (crashed) result.phase = "run";
63255
63399
  result.ok = !(result.fails.length > 0 || result.tests.some((t) => t.failed > 0)) && crashed === void 0 && result.leaks.length === 0;
@@ -63290,16 +63434,26 @@ function report_file(result) {
63290
63434
  if (result.crashed) console.log(C.red(` ${result.crashed}`));
63291
63435
  for (const f of result.fails) console.log(` ${C.red("✗")} ${f.test} ${C.dim(">")} ${f.message}`);
63292
63436
  for (const leak of result.leaks) console.log(` ${C.yellow("⚠")} ${C.yellow(leak)}`);
63437
+ if (result.leaks.length) {
63438
+ for (const lt of result.leaking_tests) console.log(` ${C.yellow("⚠")} ${lt.test} ${C.dim(`leaked ${lt.count} allocation(s)`)}`);
63439
+ 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")}`);
63440
+ }
63293
63441
  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))}`);
63294
63442
  if (result.other.length) {
63295
63443
  console.log(C.dim(" --- test stdout ---"));
63296
63444
  for (const line of result.other) console.log(C.dim(` ${line}`));
63297
63445
  }
63298
63446
  }
63447
+ /** The default concurrency for `runTests` when no `jobs` is given. */
63448
+ function default_jobs() {
63449
+ const n = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length;
63450
+ return Math.max(1, n);
63451
+ }
63299
63452
  /**
63300
63453
  * Locate audit_runtime.c for an audited test build: an explicit path wins,
63301
63454
  * otherwise walk up from `dir` looking for a `src/audit_runtime.c` (the same
63302
- * convention the run/build commands use).
63455
+ * convention the run/build commands use). When both miss, the caller falls
63456
+ * back to the CLI-bundled runtime (`find_bundled_audit_runtime`).
63303
63457
  */
63304
63458
  function find_audit_runtime(dir) {
63305
63459
  let cur = dir;
@@ -63312,19 +63466,25 @@ function find_audit_runtime(dir) {
63312
63466
  }
63313
63467
  }
63314
63468
  /** Discover, run, and report every `*.test.nm` under `root`. */
63315
- function runTests(root, options = {}) {
63469
+ async function runTests(root, options = {}) {
63316
63470
  const arch = options.arch ?? "aarch64";
63317
63471
  const lib = resolve_lib_for(root);
63318
63472
  const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
63319
63473
  console.log(`Found ${files.length} test file(s)\n`);
63320
63474
  const startTime = performance.now();
63321
63475
  const binary_cwd = path.resolve(root);
63322
- const results = [];
63323
- for (const file of files) {
63324
- const result = run_test_file(file, lib, arch, options.audit, options.audit_runtime, options.release, binary_cwd);
63325
- report_file(result);
63326
- results.push(result);
63327
- }
63476
+ const jobs = Math.max(1, Math.min(options.jobs ?? default_jobs(), files.length));
63477
+ const results = new Array(files.length);
63478
+ let next = 0;
63479
+ const worker = async () => {
63480
+ for (;;) {
63481
+ const i = next++;
63482
+ if (i >= files.length) return;
63483
+ results[i] = await run_test_file(files[i], lib, arch, options.audit, options.audit_runtime, options.release, binary_cwd);
63484
+ }
63485
+ };
63486
+ await Promise.all(Array.from({ length: jobs }, worker));
63487
+ for (const result of results) report_file(result);
63328
63488
  const elapsed = performance.now() - startTime;
63329
63489
  const totalFiles = results.length;
63330
63490
  const totalFilesFailed = results.reduce((a, r) => a + (r.ok ? 0 : 1), 0);
@@ -63333,11 +63493,12 @@ function runTests(root, options = {}) {
63333
63493
  const totalFailed = results.reduce((a, r) => a + r.tests.reduce((x, t) => x + t.failed, 0), 0);
63334
63494
  const totalPassed = totalTests - totalFailed;
63335
63495
  const totalLeaks = results.reduce((a, r) => a + r.leaks.length, 0);
63496
+ const totalLeakingTests = results.reduce((a, r) => a + r.leaking_tests.length, 0);
63336
63497
  const anyFailed = results.some((r) => !r.ok);
63337
63498
  console.log("");
63338
63499
  console.log(` ${C.dim("Files ")} ${C.green(`${totalFilesPassed} passed`)}` + (totalFilesFailed ? ` ${C.dim("|")} ${C.red(`${totalFilesFailed} failed`)}` : "") + C.dim(` (${totalFiles})`));
63339
63500
  console.log(` ${C.dim("Tests ")} ${C.green(`${totalPassed} passed`)}` + (totalFailed ? ` ${C.dim("|")} ${C.red(`${totalFailed} failed`)}` : "") + C.dim(` (${totalTests})`));
63340
- if (totalLeaks) console.log(` ${C.dim("Leaks ")} ${C.yellow(`${totalLeaks} reported`)}` + C.dim(" (--audit; leaked files count as failures)"));
63501
+ 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)"));
63341
63502
  console.log(` ${C.dim(" Time ")} ${format_duration(elapsed)}`);
63342
63503
  console.log("");
63343
63504
  return !anyFailed;
@@ -63425,12 +63586,13 @@ try {
63425
63586
  if (command === "test") {
63426
63587
  const root = args.in ?? process.cwd();
63427
63588
  const filter = args.filter ? new RegExp(args.filter) : void 0;
63428
- const ok = runTests(root, {
63589
+ const ok = await runTests(root, {
63429
63590
  arch: args.arch ?? "aarch64",
63430
63591
  filter,
63431
63592
  audit: args.audit,
63432
63593
  audit_runtime: args.audit_runtime,
63433
- release: args.release
63594
+ release: args.release,
63595
+ jobs: args.jobs !== void 0 ? parseInt(args.jobs, 10) : void 0
63434
63596
  });
63435
63597
  process.exit(ok ? 0 : 1);
63436
63598
  }
@@ -63541,6 +63703,7 @@ function resolve_audit_runtime(config, input_path) {
63541
63703
  if (parent === dir) break;
63542
63704
  dir = parent;
63543
63705
  }
63706
+ return find_bundled_audit_runtime();
63544
63707
  }
63545
63708
  function compile_audit_runtime(config, input_path, buildDir) {
63546
63709
  const runtime_src = resolve_audit_runtime(config, input_path);
@@ -63617,7 +63780,7 @@ function processFile(filename, config, mode, program_args) {
63617
63780
  console.log(`Created ${codefile} in ${(compileTime - startTime).toFixed(2)}ms`);
63618
63781
  console.log("");
63619
63782
  startTime = performance.now();
63620
- const audit_obj = config.audit ? compile_audit_runtime(config, resolved, buildDir) : void 0;
63783
+ const audit_obj = config.audit || result.code.includes("nomen_audit_count") ? compile_audit_runtime(config, resolved, buildDir) : void 0;
63621
63784
  let link_inputs = codefile;
63622
63785
  if (companionfile) link_inputs += ` ${companionfile}`;
63623
63786
  if (audit_obj) link_inputs += ` ${audit_obj}`;