nomen-lang 0.6.7 → 0.6.9

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",
@@ -6811,6 +6812,27 @@ function find_enum(name, status) {
6811
6812
  }
6812
6813
  //#endregion
6813
6814
  //#region ../src/build_common/mono_name.ts
6815
+ /**
6816
+ * Whether `name` is a monomorphized owning-container struct whose
6817
+ * `<Mono>_copy` is safe to call from the owning-`Buffer.load_T`
6818
+ * specialization — `List_<T>` / `Buffer_<T>` (and their bare generics).
6819
+ * These own a heap slab their `#destroy` frees unconditionally, and `copy`
6820
+ * yields an independent slab, so a returned element's field must be
6821
+ * deep-copied or the caller's unconditional container-field teardown
6822
+ * double-frees the slot's slab.
6823
+ *
6824
+ * `Map`/`Set` are deliberately EXCLUDED from the automatic deep copy: their
6825
+ * monomorphized `copy` (which composes several `Buffer.copy` calls) is
6826
+ * verified standalone on both backends, but invoking it from the load
6827
+ * specialization corrupts the returned struct on aarch64 (a codegen
6828
+ * interaction still open — see FOLLOWUP.md). A `Map`/`Set` FIELD reached
6829
+ * through a container element's `load` therefore stays shallow (the
6830
+ * pre-existing bounded gap); an EXPLICIT `.copy()` on the owning struct or
6831
+ * the Map/Set itself works on both backends.
6832
+ */
6833
+ function is_load_deep_copy_container(name) {
6834
+ return name === "List" || name === "Buffer" || name.startsWith("List_") || name.startsWith("Buffer_");
6835
+ }
6814
6836
  function mono_type_name(type_or_name, type_args) {
6815
6837
  const name = typeof type_or_name === "string" ? type_or_name : type_or_name.name;
6816
6838
  const args = typeof type_or_name === "string" ? type_args : type_or_name.type_args;
@@ -7676,10 +7698,15 @@ function direct_string_fields(node) {
7676
7698
  * callee's own records are scope-local and die at return, so without the
7677
7699
  * transfer the stored copy leaks.
7678
7700
  *
7679
- * Only DIRECT statements qualify: a store nested in a branch/loop may not
7680
- * execute, and a record over the pre-call value would free it at the caller's
7681
- * scope exit (possibly rodata — an invalid free). Memoized on the function
7682
- * node; forwarding cycles are cut by a visited set (the cycle member's own
7701
+ * Only DIRECT statements qualify for the plain transfer: a store nested in a
7702
+ * branch/loop may not execute, and a record over the pre-call value would
7703
+ * free it at the caller's scope exit (possibly rodata — an invalid free).
7704
+ * Those CONDITIONAL stores are reported separately
7705
+ * (scan_ref_param_string_field_may_writes): the callee's function builder
7706
+ * emits an ENTRY DUP for each of them (`field = strdup(field)` at function
7707
+ * entry, recorded), which makes the field heap-owned on EVERY path and the
7708
+ * may-store record sound to transfer. Memoized on the function node;
7709
+ * forwarding cycles are cut by a visited set (the cycle member's own
7683
7710
  * contribution is skipped, bounded-leak).
7684
7711
  */
7685
7712
  function scan_ref_param_string_field_writes(func, structs) {
@@ -7687,6 +7714,51 @@ function scan_ref_param_string_field_writes(func, structs) {
7687
7714
  if (stamp._ref_param_string_field_writes) return stamp._ref_param_string_field_writes;
7688
7715
  const result = /* @__PURE__ */ new Map();
7689
7716
  stamp._ref_param_string_field_writes = result;
7717
+ const tracked = tracked_ref_params(func, structs);
7718
+ if (tracked.size) scan_top_level(func.statements ?? [], tracked, structs, result);
7719
+ return result;
7720
+ }
7721
+ /**
7722
+ * The CONDITIONAL (`may-executed`) ref-param field stores: direct stores
7723
+ * nested in if/else branches, switch cases, and loop bodies. Forwarded
7724
+ * calls from a nested position are deliberately EXCLUDED: a helper's store
7725
+ * builds with its own (fresh) record set, so it cannot reclaim the
7726
+ * displaced entry-dup — dup'ing for a forwarded store would trade the
7727
+ * old bounded leak for a new one. Those chains keep the documented
7728
+ * bounded-leak posture (the stored copy leaks, never an invalid free).
7729
+ * The function builder entry-dups these fields (see
7730
+ * ref_param_entry_dup_fields), making the may-store record transferable.
7731
+ */
7732
+ function scan_ref_param_string_field_may_writes(func, structs) {
7733
+ const stamp = func;
7734
+ if (stamp._ref_param_string_field_may_writes) return stamp._ref_param_string_field_may_writes;
7735
+ const result = /* @__PURE__ */ new Map();
7736
+ stamp._ref_param_string_field_may_writes = result;
7737
+ const tracked = tracked_ref_params(func, structs);
7738
+ if (tracked.size) scan_nested(func.statements ?? [], tracked, result);
7739
+ return result;
7740
+ }
7741
+ /**
7742
+ * The fields a function's builder must ENTRY DUP: the conditional
7743
+ * (may-executed) ref-param string-field stores that are NOT also
7744
+ * definitely-executed (a must-store already leaves the field heap-owned on
7745
+ * every path — dup'ing it too would just add a pointless strdup per call).
7746
+ * The dup runs at callee entry on both backends and records the field, so a
7747
+ * call site may transfer these as heap records soundly.
7748
+ */
7749
+ function ref_param_entry_dup_fields(func, structs) {
7750
+ const must = scan_ref_param_string_field_writes(func, structs);
7751
+ const may = scan_ref_param_string_field_may_writes(func, structs);
7752
+ const result = /* @__PURE__ */ new Map();
7753
+ for (const [param, fields] of may) {
7754
+ const dup = /* @__PURE__ */ new Set();
7755
+ for (const field of fields) if (!must.get(param)?.has(field)) dup.add(field);
7756
+ if (dup.size) result.set(param, dup);
7757
+ }
7758
+ return result;
7759
+ }
7760
+ /** The tracked ref params of `func`: name -> writable plain string fields. */
7761
+ function tracked_ref_params(func, structs) {
7690
7762
  const tracked = /* @__PURE__ */ new Map();
7691
7763
  for (const param of func.params ?? []) {
7692
7764
  if (param.is_self_param) continue;
@@ -7695,8 +7767,7 @@ function scan_ref_param_string_field_writes(func, structs) {
7695
7767
  const fields = ref_param_writable_fields(param.type.name, structs);
7696
7768
  if (fields.size) tracked.set(param.name, fields);
7697
7769
  }
7698
- if (tracked.size) scan_function(func, tracked, structs, result, /* @__PURE__ */ new Set());
7699
- return result;
7770
+ return tracked;
7700
7771
  }
7701
7772
  /** The writable plain string fields of the value struct `type_name` resolves
7702
7773
  * to, or the empty set (non-struct, class, or no string fields). */
@@ -7706,46 +7777,91 @@ function ref_param_writable_fields(type_name, structs) {
7706
7777
  if (!struct) return /* @__PURE__ */ new Set();
7707
7778
  return new Set(direct_string_fields(struct).map((f) => f.name));
7708
7779
  }
7709
- function scan_function(func, tracked, structs, result, visiting) {
7710
- visiting.add(func);
7711
- for (const stmt of func.statements ?? []) {
7780
+ /**
7781
+ * The top-level (definitely-executed) walk: direct `param.field = …`
7782
+ * statements and top-level forwarded calls only — nothing nested.
7783
+ */
7784
+ function scan_top_level(statements, tracked, structs, result) {
7785
+ for (const stmt of statements) {
7712
7786
  if (stmt.node_type === "assign") {
7713
- const lhs = stmt.left_value;
7714
- if (lhs?.node_type === "access") {
7715
- const access = lhs;
7716
- if (access.access.node_type === "access_field" && access.target.node_type === "value") {
7717
- const name = access.target.value ?? "";
7718
- const fields = tracked.get(name);
7719
- const field = access.access.name ?? "";
7720
- if (fields?.has(field)) {
7721
- if (!result.has(name)) result.set(name, /* @__PURE__ */ new Set());
7722
- result.get(name).add(field);
7723
- }
7724
- }
7725
- }
7787
+ record_direct_store(stmt, tracked, result);
7726
7788
  continue;
7727
7789
  }
7728
7790
  const call = ref_forward_call(stmt);
7729
- if (!call) continue;
7730
- const [callee, args] = call;
7731
- const non_self_params = (callee.params ?? []).filter((p) => !p.is_self_param);
7732
- for (let i = 0; i < args.length && i < non_self_params.length; i++) {
7733
- const arg = args[i];
7734
- if (arg?.node_type !== "value") continue;
7735
- const arg_name = arg.value ?? "";
7736
- const my_fields = tracked.get(arg_name);
7737
- if (!my_fields) continue;
7738
- const callee_param = non_self_params[i];
7739
- if (!(callee_param.is_ref || callee_param.type.is_ref)) continue;
7740
- if (visiting.has(callee)) continue;
7741
- const callee_writes = scan_ref_param_string_field_writes(callee, structs).get(callee_param.name);
7742
- if (!callee_writes?.size) continue;
7743
- if (!result.has(arg_name)) result.set(arg_name, /* @__PURE__ */ new Set());
7744
- const mine = result.get(arg_name);
7745
- for (const field of callee_writes) if (my_fields.has(field)) mine.add(field);
7746
- }
7747
- }
7748
- visiting.delete(func);
7791
+ if (call) merge_forwarded(call, tracked, structs, result);
7792
+ }
7793
+ }
7794
+ /**
7795
+ * The nested walk: direct stores at ANY depth below the top level are
7796
+ * may-stores. Forwarded calls are skipped everywhere (their contributions
7797
+ * would need cross-function displaced-value knowledge the callee's fresh
7798
+ * record set cannot carry — see the may-scan doc).
7799
+ */
7800
+ function scan_nested(statements, tracked, result) {
7801
+ for (const stmt of statements) {
7802
+ if (stmt.node_type === "assign") {
7803
+ record_direct_store(stmt, tracked, result);
7804
+ continue;
7805
+ }
7806
+ for (const nested of nested_statement_lists(stmt)) scan_nested(nested, tracked, result);
7807
+ }
7808
+ }
7809
+ /** Record a direct `param.<field> = …` store into `result`. */
7810
+ function record_direct_store(stmt, tracked, result) {
7811
+ const lhs = stmt.left_value;
7812
+ if (lhs?.node_type !== "access") return;
7813
+ const access = lhs;
7814
+ if (access.access.node_type !== "access_field" || access.target.node_type !== "value") return;
7815
+ const name = access.target.value ?? "";
7816
+ const fields = tracked.get(name);
7817
+ const field = access.access.name ?? "";
7818
+ if (!fields?.has(field)) return;
7819
+ if (!result.has(name)) result.set(name, /* @__PURE__ */ new Set());
7820
+ result.get(name).add(field);
7821
+ }
7822
+ /** Merge a forwarded call's callee stores for the forwarded param: the
7823
+ * callee's MUST stores at a top-level position. */
7824
+ function merge_forwarded(call, tracked, structs, result) {
7825
+ const [callee, args] = call;
7826
+ const non_self_params = (callee.params ?? []).filter((p) => !p.is_self_param);
7827
+ for (let i = 0; i < args.length && i < non_self_params.length; i++) {
7828
+ const arg = args[i];
7829
+ if (arg?.node_type !== "value") continue;
7830
+ const arg_name = arg.value ?? "";
7831
+ const my_fields = tracked.get(arg_name);
7832
+ if (!my_fields) continue;
7833
+ const callee_param = non_self_params[i];
7834
+ if (!(callee_param.is_ref || callee_param.type.is_ref)) continue;
7835
+ const source = scan_ref_param_string_field_writes(callee, structs).get(callee_param.name);
7836
+ if (!source?.size) continue;
7837
+ if (!result.has(arg_name)) result.set(arg_name, /* @__PURE__ */ new Set());
7838
+ const mine = result.get(arg_name);
7839
+ for (const field of source) if (my_fields.has(field)) mine.add(field);
7840
+ }
7841
+ }
7842
+ /** The statement lists nested inside `stmt` (branch bodies, loop bodies) —
7843
+ * the same walk scan_reassigned_vars uses. */
7844
+ function nested_statement_lists(stmt) {
7845
+ const branch_of = (b) => b?.statements ?? [];
7846
+ const lists = [];
7847
+ if (stmt.node_type === "while" || stmt.node_type === "for") {
7848
+ const nested = stmt.statements;
7849
+ if (nested) lists.push(nested);
7850
+ return lists;
7851
+ }
7852
+ if (stmt.node_type === "if") {
7853
+ const n = stmt;
7854
+ if (n.if_branch) lists.push(branch_of(n.if_branch));
7855
+ if (n.else_branch) lists.push(branch_of(n.else_branch));
7856
+ return lists;
7857
+ }
7858
+ if (stmt.node_type === "switch" || stmt.node_type === "match") {
7859
+ const n = stmt;
7860
+ for (const c of n.cases ?? []) if (c.branch) lists.push(branch_of(c.branch));
7861
+ if (n.else_branch) lists.push(branch_of(n.else_branch));
7862
+ return lists;
7863
+ }
7864
+ return lists;
7749
7865
  }
7750
7866
  /**
7751
7867
  * The forwarded-call shape of a top-level statement: a resolved free-function
@@ -7776,6 +7892,12 @@ function ref_forward_call(stmt) {
7776
7892
  * stores (scan_ref_param_string_field_writes) onto the caller's records for
7777
7893
  * the argument variable, making the caller's scope exit free them.
7778
7894
  *
7895
+ * The callee's CONDITIONAL stores transfer too
7896
+ * (ref_param_entry_dup_fields): the callee's builder entry-dups those
7897
+ * fields (`field = strdup(field)` at entry, recorded), so the field is
7898
+ * heap-owned on EVERY path out of the call — the not-taken path holds the
7899
+ * dup — and a caller-side record is sound.
7900
+ *
7779
7901
  * `arg_is_trackable_local` must report true only for variables whose storage
7780
7902
  * THIS function's scope exit owns — a plain local. A `ref` param of the
7781
7903
  * enclosing function forwards caller storage, so a record here would free the
@@ -7783,19 +7905,22 @@ function ref_forward_call(stmt) {
7783
7905
  */
7784
7906
  function transfer_ref_param_field_records(callee, args, status, arg_is_trackable_local) {
7785
7907
  const writes = scan_ref_param_string_field_writes(callee, status.structs);
7786
- if (!writes.size) return;
7908
+ const dups = ref_param_entry_dup_fields(callee, status.structs);
7909
+ if (!writes.size && !dups.size) return;
7787
7910
  const non_self_params = (callee.params ?? []).filter((p) => !p.is_self_param);
7788
7911
  for (let i = 0; i < args.length && i < non_self_params.length; i++) {
7789
7912
  const param = non_self_params[i];
7790
7913
  if (!(param.is_ref || param.type.is_ref)) continue;
7791
7914
  const fields = writes.get(param.name);
7792
- if (!fields?.size) continue;
7915
+ const dup_fields = dups.get(param.name);
7916
+ if (!fields?.size && !dup_fields?.size) continue;
7793
7917
  const arg = args[i];
7794
7918
  if (arg?.node_type !== "value") continue;
7795
7919
  const name = arg.value;
7796
7920
  if (!name || !arg_is_trackable_local(name)) continue;
7797
7921
  if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
7798
- for (const field of fields) status.heap_string_fields.add(`${name}.${field}`);
7922
+ for (const field of fields ?? []) status.heap_string_fields.add(`${name}.${field}`);
7923
+ for (const field of dup_fields ?? []) status.heap_string_fields.add(`${name}.${field}`);
7799
7924
  }
7800
7925
  }
7801
7926
  //#endregion
@@ -8288,7 +8413,7 @@ function is_container_borrow_accessor_name(name) {
8288
8413
  * site (see build_return_node), so it counts as owned for classification.
8289
8414
  */
8290
8415
  function is_call_site_borrow_accessor(fn_name) {
8291
- return fn_name === "at" || fn_name === "first";
8416
+ return fn_name === "at" || fn_name === "first" || fn_name === "load";
8292
8417
  }
8293
8418
  /**
8294
8419
  * Whether `node` is a container/buffer borrow accessor CALL (an
@@ -15010,6 +15135,33 @@ function emit_destroy_for_scope(status, declarations_before) {
15010
15135
  emit_asm(status, `ldr x19, [sp], #16\n`);
15011
15136
  continue;
15012
15137
  }
15138
+ if (status.heap_array_vars?.has(decl.name)) {
15139
+ if (status.heap_owned_string_arrays?.has(decl.name)) {
15140
+ emit_asm(status, `str x19, [sp, #-16]!\n`);
15141
+ emit_asm(status, `str x20, [sp, #-16]!\n`);
15142
+ emit_var_load(status, "x0", decl.name, 8);
15143
+ emit_asm(status, `mov x19, x0\n`);
15144
+ emit_asm(status, `ldr x20, [x19]\n`);
15145
+ emit_asm(status, `add x19, x19, #8\n`);
15146
+ const loop = `.Lhosa_${decl.name}`;
15147
+ emit_asm(status, `${loop}:\n`);
15148
+ emit_asm(status, `cbz x20, .Lhosa_done_${decl.name}\n`);
15149
+ emit_asm(status, `ldr x0, [x19]\n`);
15150
+ emit_free(status);
15151
+ emit_asm(status, `add x19, x19, #16\n`);
15152
+ emit_asm(status, `sub x20, x20, #1\n`);
15153
+ emit_asm(status, `b ${loop}\n`);
15154
+ emit_asm(status, `.Lhosa_done_${decl.name}:\n`);
15155
+ emit_var_load(status, "x0", decl.name, 8);
15156
+ emit_free(status);
15157
+ emit_asm(status, `ldr x20, [sp], #16\n`);
15158
+ emit_asm(status, `ldr x19, [sp], #16\n`);
15159
+ continue;
15160
+ }
15161
+ emit_var_load(status, "x0", decl.name, 8);
15162
+ emit_free(status);
15163
+ continue;
15164
+ }
15013
15165
  if (status.heap_strings?.has(decl.name)) {
15014
15166
  emit_var_load(status, "x0", decl.name, 8);
15015
15167
  emit_free(status);
@@ -17957,7 +18109,7 @@ function emit_load_deep_copy_fields$1(elem, status, sret, slot) {
17957
18109
  const size = get_type_size(field.type, status);
17958
18110
  if (!field.type.is_ref && !field.type.is_view && !field.type.is_array && !field.type.is_nullable && field.type.name !== "string") {
17959
18111
  const field_struct = resolve_struct_type(field.type, status);
17960
- if (field_struct && !field_struct.is_class && field_struct.name.startsWith("List_") && struct_needs_destroy(field_struct, status) && field_struct.functions.find((f) => f.name === "copy")) {
18112
+ if (field_struct && !field_struct.is_class && is_load_deep_copy_container(field_struct.name) && struct_needs_destroy(field_struct, status) && field_struct.functions.find((f) => f.name === "copy")) {
17961
18113
  emit_asm(status, `add x8, ${sret}, #${offset}\n`);
17962
18114
  emit_asm(status, `add x0, ${slot}, #${offset}\n`);
17963
18115
  emit_asm(status, `bl ${field_struct.name}_copy\n`);
@@ -20611,6 +20763,7 @@ function build_function_node$1(node, status) {
20611
20763
  } else status.nir_emit_ctx = void 0;
20612
20764
  const old_raw_param_reloads = status.raw_param_reloads;
20613
20765
  install_raw_reload_plan(status, raw_reload_plan_lines);
20766
+ emit_ref_param_entry_dups$1(node, status);
20614
20767
  try {
20615
20768
  build_block_node$1(node, status);
20616
20769
  } finally {
@@ -20756,6 +20909,49 @@ function build_function_node$1(node, status) {
20756
20909
  status.return_buffer_stack_offset = void 0;
20757
20910
  status.function_return_type = void 0;
20758
20911
  }
20912
+ /**
20913
+ * Entry-dup the ref-param string fields the body stores through
20914
+ * CONDITIONALLY (see ref_param_entry_dup_fields): load the field pair's
20915
+ * ptr half, strdup it, store the fresh pointer back — with the field
20916
+ * recorded in heap_string_fields — so the field is heap-owned on every
20917
+ * path out of the function and the call-site record transfer is sound.
20918
+ * Mirrors the C backend's emit_ref_param_entry_dups; the len half is
20919
+ * unchanged by a dup, so only the ptr half is rewritten. A nullable
20920
+ * string field guards the dup (strdup(NULL) traps; a null pair stores as
20921
+ * null and every reclaim path no-ops on free(NULL)).
20922
+ */
20923
+ function emit_ref_param_entry_dups$1(node, status) {
20924
+ const dups = ref_param_entry_dup_fields(node, status.structs);
20925
+ if (!dups.size) return;
20926
+ for (const param of node.params ?? []) {
20927
+ if (param.is_self_param) continue;
20928
+ if (!(param.is_ref || param.type.is_ref)) continue;
20929
+ const fields = dups.get(param.name);
20930
+ if (!fields?.size) continue;
20931
+ const struct = status.structs.find((s) => s.name === param.type.name && !s.is_simple_type && !s.is_class);
20932
+ for (const field of fields) {
20933
+ const offset = get_field_offset(param.type.name, field, status);
20934
+ const paramReg = status.function_param_regs?.get(param.name);
20935
+ if (paramReg) emit_asm(status, `mov x0, ${paramReg}\n`);
20936
+ else if (is_local_ref_var(param.name, status)) emit_deref_var_address(status, "x0", param.name);
20937
+ else emit_var_address(status, "x0", param.name);
20938
+ emit_asm(status, `str x0, [sp, #-16]!\n`);
20939
+ emit_asm(status, `ldr x0, [x0, #${offset}]\n`);
20940
+ const nullable = !!struct?.fields.find((f) => f.name === field)?.type.is_nullable;
20941
+ let skip_label;
20942
+ if (nullable) {
20943
+ skip_label = `.Lentry_dup_${status.label_counter = (status.label_counter ?? 0) + 1}`;
20944
+ emit_asm(status, `cbz x0, ${skip_label}\n`);
20945
+ }
20946
+ emit_strdup(status);
20947
+ if (skip_label) emit_asm(status, `${skip_label}:\n`);
20948
+ emit_asm(status, `ldr x1, [sp], #16\n`);
20949
+ emit_asm(status, `str x0, [x1, #${offset}]\n`);
20950
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
20951
+ status.heap_string_fields.add(`${param.name}.${field}`);
20952
+ }
20953
+ }
20954
+ }
20759
20955
  //#endregion
20760
20956
  //#region ../src/build_aarch64/build_struct_node.ts
20761
20957
  let field_strdup_guard_counter = 0;
@@ -23997,6 +24193,14 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23997
24193
  if (element_size === 1) emit_asm(status, `strb w0, [x9, #${slot}]\n`);
23998
24194
  else if (element_size === 4) emit_asm(status, `str w0, [x9, #${slot}]\n`);
23999
24195
  else emit_asm(status, `str x0, [x9, #${slot}]\n`);
24196
+ } else if (element_size > 8 && struct_element && !struct_element.is_class) {
24197
+ emit_struct_address_param(value, status);
24198
+ ensure_newline(status);
24199
+ emit_asm(status, `ldr x9, [x29, #${offset}]\n`);
24200
+ emit_asm(status, `mov x1, x0\n`);
24201
+ emit_asm(status, `add x0, x9, #${slot}\n`);
24202
+ emit_asm(status, `mov x2, #${element_size}\n`);
24203
+ emit_asm(status, `bl _memcpy\n`);
24000
24204
  } else {
24001
24205
  emit_init_value$1(value, nir_array_element(nir_init, array_literal_node, i), status);
24002
24206
  ensure_newline(status);
@@ -24036,6 +24240,14 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
24036
24240
  if (element_size === 1) emit_asm(status, `strb w0, [x9, #${slot}]\n`);
24037
24241
  else if (element_size === 4) emit_asm(status, `str w0, [x9, #${slot}]\n`);
24038
24242
  else emit_asm(status, `str x0, [x9, #${slot}]\n`);
24243
+ } else if (element_size > 8 && struct_element && !struct_element.is_class) {
24244
+ emit_struct_address_param(value, status);
24245
+ ensure_newline(status);
24246
+ emit_asm(status, `ldr x9, [${node.name}]\n`);
24247
+ emit_asm(status, `mov x1, x0\n`);
24248
+ emit_asm(status, `add x0, x9, #${slot}\n`);
24249
+ emit_asm(status, `mov x2, #${element_size}\n`);
24250
+ emit_asm(status, `bl _memcpy\n`);
24039
24251
  } else {
24040
24252
  emit_init_value$1(value, nir_array_element(nir_init, array_literal_node, i), status);
24041
24253
  ensure_newline(status);
@@ -24142,6 +24354,13 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
24142
24354
  if (element_size === 1) emit_asm(status, `strb w0, [x29, #${slot_offset}]\n`);
24143
24355
  else if (element_size === 4) emit_asm(status, `str w0, [x29, #${slot_offset}]\n`);
24144
24356
  else emit_asm(status, `str x0, [x29, #${slot_offset}]\n`);
24357
+ } else if (element_size > 8 && struct_element && !struct_element.is_class) {
24358
+ emit_struct_address_param(value, status);
24359
+ ensure_newline(status);
24360
+ emit_asm(status, `mov x1, x0\n`);
24361
+ emit_asm(status, `add x0, x29, #${slot_offset}\n`);
24362
+ emit_asm(status, `mov x2, #${element_size}\n`);
24363
+ emit_asm(status, `bl _memcpy\n`);
24145
24364
  } else {
24146
24365
  emit_init_value$1(value, nir_array_element(nir_init, array_values, i), status);
24147
24366
  ensure_newline(status);
@@ -24167,6 +24386,13 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
24167
24386
  if (element_size === 1) emit_asm(status, `strb w0, [${node.name} + ${i * element_size}]\n`);
24168
24387
  else if (element_size === 4) emit_asm(status, `str w0, [${node.name} + ${i * element_size}]\n`);
24169
24388
  else emit_asm(status, `str x0, [${node.name} + ${i * element_size}]\n`);
24389
+ } else if (element_size > 8 && struct_element && !struct_element.is_class) {
24390
+ emit_struct_address_param(value, status);
24391
+ ensure_newline(status);
24392
+ emit_asm(status, `mov x1, x0\n`);
24393
+ emit_global_slot_addr(status, node.name, i * element_size);
24394
+ emit_asm(status, `mov x2, #${element_size}\n`);
24395
+ emit_asm(status, `bl _memcpy\n`);
24170
24396
  } else {
24171
24397
  emit_init_value$1(value, nir_array_element(nir_init, array_values, i), status);
24172
24398
  ensure_newline(status);
@@ -33342,7 +33568,8 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
33342
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) {
33343
33569
  const access_lhs = node.left_value;
33344
33570
  const field_type = access_lhs.access.type;
33345
- 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;
33571
+ const field_resolved = field_type?.name && !field_type.is_view && !field_type.is_array ? resolve_struct_type(field_type, status) : void 0;
33572
+ const field_struct = field_resolved && !field_resolved.is_class ? field_resolved : void 0;
33346
33573
  if (field_struct) {
33347
33574
  splice_decl_from_c_scopes(status, node.right_value.value);
33348
33575
  const saved_len = begin_code_scratch(status);
@@ -34953,6 +35180,7 @@ function build_if_else_node(node, status, nir) {
34953
35180
  status.code += `) {\n`;
34954
35181
  let then_fields;
34955
35182
  if (node.if_branch) {
35183
+ status.heap_string_fields = new Set(pre_heap_string_fields ?? []);
34956
35184
  build_block_with_cursor(node.if_branch, nir?.then_branch, status);
34957
35185
  build_auto_free(status);
34958
35186
  }
@@ -34961,7 +35189,7 @@ function build_if_else_node(node, status, nir) {
34961
35189
  if (node.else_branch) {
34962
35190
  status.scoped_declarations = enter_c_scope(status);
34963
35191
  status.deferred_frees = [];
34964
- status.heap_string_fields = pre_heap_string_fields;
35192
+ status.heap_string_fields = new Set(pre_heap_string_fields ?? []);
34965
35193
  status.code += `} else {\n`;
34966
35194
  build_block_with_cursor(node.else_branch, nir?.else_branch, status);
34967
35195
  build_auto_free(status);
@@ -35395,7 +35623,7 @@ function build_switch_node(node, status, nir) {
35395
35623
  for (let i = 0; i < node.cases.length; i++) {
35396
35624
  const c = node.cases[i];
35397
35625
  status.scoped_declarations = enter_c_scope(status);
35398
- status.heap_string_fields = pre_heap_string_fields;
35626
+ status.heap_string_fields = new Set(pre_heap_string_fields ?? []);
35399
35627
  const saved_code = begin_code_scratch(status);
35400
35628
  build_node$1(c.condition, status);
35401
35629
  let cond_code = end_code_scratch(status, saved_code);
@@ -35441,7 +35669,7 @@ function build_switch_node(node, status, nir) {
35441
35669
  if (node.else_branch) {
35442
35670
  status.code += "} else ";
35443
35671
  status.scoped_declarations = enter_c_scope(status);
35444
- status.heap_string_fields = pre_heap_string_fields;
35672
+ status.heap_string_fields = new Set(pre_heap_string_fields ?? []);
35445
35673
  status.code += `{\n`;
35446
35674
  build_block_with_cursor(node.else_branch, nir?.otherwise ?? void 0, status);
35447
35675
  build_auto_free(status);
@@ -35864,11 +36092,13 @@ function emit_transfer_owning_fields(elem, src, status, prefix = "") {
35864
36092
  /**
35865
36093
  * Emit the deep-copy half of owning load_T: for each field whose type has a
35866
36094
  * monomorphized `copy` helper that yields an independent value (a `List<T>`
35867
- * field — `<List_T>_copy` deep-copies slots per the owning store contract —
35868
- * or an enum-with-data field whose string payloads `<E>_copy` duplicates),
35869
- * overwrite the shallow-copied bytes with an owned copy. Fields without a
35870
- * copyable shape (nested owning user structs, class fields) stay shallow —
35871
- * recorded in FOLLOWUP.md ("remaining shallow container-field shapes").
36095
+ * /`Buffer<T>`/`Map`/`Set` field — `<Mono>_copy` deep-copies the slots or
36096
+ * member buffers — or an enum-with-data field whose string payloads
36097
+ * `<E>_copy` duplicates), overwrite the shallow-copied bytes with an owned
36098
+ * copy. A struct local's CONTAINER fields are reclaimed unconditionally at
36099
+ * scope exit, so a shallow return would double-free the slot's slab. Fields
36100
+ * without a copyable shape (class fields, nested owning user structs without
36101
+ * a synthesized copy) stay shallow — recorded in FOLLOWUP.md.
35872
36102
  */
35873
36103
  function emit_load_deep_copy_fields(elem, dst, status) {
35874
36104
  for (const field of elem.fields) {
@@ -35876,7 +36106,7 @@ function emit_load_deep_copy_fields(elem, dst, status) {
35876
36106
  if (field.type.is_nullable) continue;
35877
36107
  if (field.type.name === "string") continue;
35878
36108
  const field_struct = resolve_struct_type(field.type, status);
35879
- if (field_struct && !field_struct.is_class && field_struct.name.startsWith("List_") && struct_needs_destroy(field_struct, status) && field_struct.functions.find((f) => f.name === "copy")) {
36109
+ if (field_struct && !field_struct.is_class && is_load_deep_copy_container(field_struct.name) && struct_needs_destroy(field_struct, status) && field_struct.functions.find((f) => f.name === "copy")) {
35880
36110
  status.code += `${dst}.${field.name} = ${field_struct.name}_copy(&${dst}.${field.name});\n`;
35881
36111
  continue;
35882
36112
  }
@@ -37470,6 +37700,7 @@ function build_function_node(node, status) {
37470
37700
  stmts: nir.body,
37471
37701
  ast: node.statements
37472
37702
  } : void 0;
37703
+ emit_ref_param_entry_dups(node, status);
37473
37704
  build_block_node(node, status, false);
37474
37705
  status.nir_emit_ctx = old_nir_ctx;
37475
37706
  status.function_ref_params = old_ref_params;
@@ -37515,6 +37746,37 @@ function build_function_node(node, status) {
37515
37746
  status.stack_array_lengths = old_stack_array_lengths;
37516
37747
  status.moved = old_moved;
37517
37748
  }
37749
+ /**
37750
+ * Emit the entry-dup for every `ref` value-struct param string field the
37751
+ * body stores through CONDITIONALLY (see ref_param_entry_dup_fields):
37752
+ * `{ field = nomen_str_dup(field) }` at function entry, recorded in
37753
+ * heap_string_fields, so the field is heap-owned on every path out of the
37754
+ * function and the call-site record transfer is sound. Nullable string
37755
+ * fields guard the dup (a null pair stores as null — free(NULL) is a
37756
+ * no-op on every reclaim path).
37757
+ */
37758
+ function emit_ref_param_entry_dups(node, status) {
37759
+ const dups = ref_param_entry_dup_fields(node, status.structs);
37760
+ if (!dups.size) return;
37761
+ for (const param of node.params ?? []) {
37762
+ if (param.is_self_param) continue;
37763
+ if (!(param.is_ref || param.type.is_ref)) continue;
37764
+ const fields = dups.get(param.name);
37765
+ if (!fields?.size) continue;
37766
+ const struct = status.structs.find((s) => s.name === param.type.name && !s.is_simple_type && !s.is_class);
37767
+ const base = c_function_name(param.name);
37768
+ for (const field of fields) {
37769
+ const nullable = !!struct?.fields.find((f) => f.name === field)?.type.is_nullable;
37770
+ if (nullable) status.code += `if (${base}->${field}.ptr) {\n`;
37771
+ const temp = `_dup_t_${status.label_counter = (status.label_counter ?? 0) + 1}`;
37772
+ status.code += `nomen_string ${temp} = nomen_str_dup(${base}->${field});\n`;
37773
+ status.code += `${base}->${field} = ${temp};\n`;
37774
+ if (nullable) status.code += `}\n`;
37775
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
37776
+ status.heap_string_fields.add(`${param.name}.${field}`);
37777
+ }
37778
+ }
37779
+ }
37518
37780
  function emit_nested_declarations(node, status) {
37519
37781
  const block = node;
37520
37782
  for (let child of block.statements) switch (child.node_type) {
@@ -37805,6 +38067,17 @@ function build_function_call_node(node, status) {
37805
38067
  status.code += `${func_name}(${is_closure_callee ? "NULL" : ""}`;
37806
38068
  if (is_closure_callee && node.params.length > 0) status.code += ", ";
37807
38069
  const variadic_idx = node.variadic_param_name ? node.params.findIndex((p) => p.node_type === "array") : -1;
38070
+ if (!node.variadic_param_name) {
38071
+ const resolved = node.resolved_function ?? (is_struct ? status.structs.find((s) => s.name === node.name)?.functions.find((f) => f.name === "#init") : void 0);
38072
+ const variadic_param = resolved?.params?.find((p) => p.is_variadic);
38073
+ if (resolved && variadic_param) {
38074
+ if (node.params.length === 1 && node.params[0].node_type === "array") node.variadic_param_name = variadic_param.name;
38075
+ else if (node.params.length === 0 && resolved.params.filter((p) => !p.is_self_param && !p.is_variadic).length === 0) {
38076
+ const elem_c = c_type(variadic_param.type.name);
38077
+ status.code += `0, (${elem_c}[]){ }`;
38078
+ }
38079
+ }
38080
+ }
37808
38081
  for (let i = 0; i < node.params.length; i++) {
37809
38082
  if (i > 0) status.code += ", ";
37810
38083
  if (i === variadic_idx && node.params[i].node_type === "array") {
@@ -38773,10 +39046,31 @@ function build_access_node$1(node, status) {
38773
39046
  status.code += `${ret_c} ${ctor_temp_val} = `;
38774
39047
  }
38775
39048
  }
39049
+ 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);
39050
+ let owned_recv_temp;
39051
+ let owned_recv_val;
39052
+ if (owned_string_recv) {
39053
+ const id = status.label_counter = (status.label_counter ?? 0) + 1;
39054
+ owned_recv_temp = `_osrecv_${id}`;
39055
+ status.code += `({ nomen_string ${owned_recv_temp} = `;
39056
+ {
39057
+ const saved_suppress = status.suppress_dereference;
39058
+ status.suppress_dereference = true;
39059
+ build_node$1(node.target, status);
39060
+ status.suppress_dereference = saved_suppress;
39061
+ }
39062
+ status.code += `; `;
39063
+ const recv_ret_c = c_return_type((target_method ?? trait_default_func)?.return_type ?? access_func.type, status);
39064
+ if (recv_ret_c !== "void") {
39065
+ owned_recv_val = `_osrecvv_${id}`;
39066
+ status.code += `${recv_ret_c} ${owned_recv_val} = `;
39067
+ }
39068
+ }
38776
39069
  if (lambda_ret_tmp) status.code += `${lambda_ret_c} ${lambda_ret_tmp} = `;
38777
39070
  status.code += `${label}(`;
38778
39071
  if (!access_func.is_static) {
38779
39072
  if (ctor_temp_free) status.code += ctor_temp_free;
39073
+ else if (owned_recv_temp) status.code += owned_recv_temp;
38780
39074
  else if (array_wrap) status.code += array_wrap.self_expr;
38781
39075
  else {
38782
39076
  const method_self_is_ref = !!target_method?.params?.some((p) => p.is_self_param && (p.is_ref || p.type?.is_ref));
@@ -38861,6 +39155,11 @@ function build_access_node$1(node, status) {
38861
39155
  if (ctor_temp_val) status.code += ` ${ctor_temp_val};`;
38862
39156
  status.code += ` })`;
38863
39157
  }
39158
+ if (owned_recv_temp) {
39159
+ status.code += `; ${status.audit ? "nomen_free_wrap" : "free"}(${owned_recv_temp}.ptr);`;
39160
+ if (owned_recv_val) status.code += ` ${owned_recv_val};`;
39161
+ status.code += ` })`;
39162
+ }
38864
39163
  if (lambda_arg_temps.size > 0) {
38865
39164
  for (const tmp of lambda_arg_temps.values()) status.code += `; ${closure_dispose_arm(tmp)}`;
38866
39165
  if (lambda_ret_tmp) status.code += `; ${lambda_ret_tmp}`;
@@ -41412,7 +41711,11 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41412
41711
  if (name === "self" && status.current_struct) target_type = new Type(status.current_struct.name);
41413
41712
  else if (status.variable_types?.has(name)) target_type = status.variable_types.get(name);
41414
41713
  }
41415
- const field_type = access.access.type;
41714
+ let field_type = access.access.type;
41715
+ if (!field_type?.name && target_type?.name) {
41716
+ const tf = status.structs.find((s) => s.name === target_type.name)?.fields.find((f) => f.name === field_name);
41717
+ if (tf) field_type = tf.type;
41718
+ }
41416
41719
  const field_is_struct = is_struct_type$2(field_type, status);
41417
41720
  if (access.target.node_type === "value") {
41418
41721
  const name = access.target.value;
@@ -45387,7 +45690,7 @@ function build_access_method(node, access_func, status) {
45387
45690
  status.last_result_is_heap = false;
45388
45691
  build_node(node.target, status);
45389
45692
  ensure_newline(status);
45390
- frees_string_receiver = method_name === "string_to_string" && status.last_result_is_heap;
45693
+ frees_string_receiver = status.last_result_is_heap;
45391
45694
  status.last_result_is_heap = false;
45392
45695
  emit_asm(status, `stp x0, x1, [sp, #-16]!\n`);
45393
45696
  }
@@ -56114,6 +56417,14 @@ function check_function_call(node, status, func, target_type, self_value, self_p
56114
56417
  }
56115
56418
  node.type = func.return_type;
56116
56419
  node.is_static = func.is_static;
56420
+ if (target_type?.type_args?.length && node.type?.type_args?.length) {
56421
+ const owner = func.scope;
56422
+ if (owner && owner.node_type === "struct" && owner.is_generic && owner.type_params.length === target_type.type_args.length) {
56423
+ const rebind = /* @__PURE__ */ new Map();
56424
+ owner.type_params.forEach((tp, i) => rebind.set(tp, target_type.type_args[i].name));
56425
+ node.type = substitute_type(node.type, rebind);
56426
+ }
56427
+ }
56117
56428
  if (node.type?.type_args?.length) instantiate_generic_type(node.type, status);
56118
56429
  if (node.type && node.type.name && self_value && is_class_type$1(node.type.name, status) && receiver_is_const(target_type, self_value, status)) {
56119
56430
  node.type = clone_type$2(node.type);
@@ -56297,7 +56608,10 @@ function check_function_call(node, status, func, target_type, self_value, self_p
56297
56608
  else {
56298
56609
  const stamped = String(func_param.type.length.value);
56299
56610
  const incoming = String(param_type.length.value);
56300
- if (stamped !== incoming) add_error(status, `Call passes ${incoming} elements to array parameter '${func_param.name}' whose compile-time length is ${stamped} — the raw array ABI carries no runtime length, so one parameter serves one compile-time length; use Array<${func_param.type.name}> for varying lengths`, param.start);
56611
+ if (stamped !== incoming) {
56612
+ if (is_heap_array_type(func_param.type)) func_param.type.length = void 0;
56613
+ else add_error(status, `Call passes ${incoming} elements to array parameter '${func_param.name}' whose compile-time length is ${stamped} — the raw array ABI carries no runtime length, so one parameter serves one compile-time length; use Array<${func_param.type.name}> for varying lengths`, param.start);
56614
+ }
56301
56615
  }
56302
56616
  }
56303
56617
  let arg_value;
@@ -56528,6 +56842,13 @@ function check_function_call(node, status, func, target_type, self_value, self_p
56528
56842
  if (typeof size_val === "number" && size_val > 0) record_buffer_cap(self_path, size_val, status);
56529
56843
  }
56530
56844
  }
56845
+ if (self_path && self_path !== "?" && is_bufferish && func.name === "copy") {
56846
+ const source_cap = lookup_buffer_cap(self_path, status);
56847
+ if (source_cap !== void 0) {
56848
+ const lhs_name = find_lhs_var_name(status);
56849
+ if (lhs_name) record_buffer_cap(lhs_name, source_cap, status);
56850
+ }
56851
+ }
56531
56852
  if (func.return_constraint) {
56532
56853
  const param_to_arg = /* @__PURE__ */ new Map();
56533
56854
  if (self_path && self_path !== "?") param_to_arg.set("self", path_to_node(self_path));
@@ -60581,6 +60902,68 @@ function parse_case_branch(status) {
60581
60902
  return null;
60582
60903
  }
60583
60904
  //#endregion
60905
+ //#region ../src/parse/utils/parse_embed_file.ts
60906
+ /**
60907
+ * `#embed_file("relative/path")` — a compile-time string literal whose value
60908
+ * is the contents of the file at parse time. The path resolves against the
60909
+ * package root (the directory containing package.jsonc, found by climbing
60910
+ * from the entry file), falling back to the entry file's folder; with no
60911
+ * known base it resolves against the process CWD.
60912
+ *
60913
+ * The parsed form is a plain string-literal ValueNode: escapes are encoded
60914
+ * with the source-level escape set (`\\`, `\"`, `\n`, `\r`, `\t`, and
60915
+ * three-digit octal for other control bytes — self-terminating, matching the
60916
+ * escape scanner), so every downstream consumer (length counting, C/asm
60917
+ * emission) treats it exactly like a hand-written literal.
60918
+ */
60919
+ function parse_embed_file(status) {
60920
+ const start = get_index(status);
60921
+ consume(status);
60922
+ const name = consume(status);
60923
+ if (name !== "embed_file") {
60924
+ add_error(status, `Unknown compiler directive '#${name}' — did you mean '#embed_file'?`, start);
60925
+ return new ValueNode(start, "\"\"");
60926
+ }
60927
+ if (!expect("(", status)) return new ValueNode(start, "\"\"");
60928
+ const path_token = status.tokens[status.i];
60929
+ if (!path_token || !path_token.value.startsWith("\"")) {
60930
+ add_error(status, "Expected a string literal path in #embed_file", start);
60931
+ return new ValueNode(start, "\"\"");
60932
+ }
60933
+ status.i += 1;
60934
+ const rel_path = path_token.value.slice(1, -1);
60935
+ if (!expect(")", status)) return new ValueNode(start, "\"\"");
60936
+ if (!rel_path) {
60937
+ add_error(status, "#embed_file path is empty", start);
60938
+ return new ValueNode(start, "\"\"");
60939
+ }
60940
+ const base = status.embed_root ?? process.cwd();
60941
+ const full = path.isAbsolute(rel_path) ? rel_path : path.join(base, rel_path);
60942
+ let content;
60943
+ try {
60944
+ content = fs.readFileSync(full, "utf8");
60945
+ } catch {
60946
+ add_error(status, `#embed_file could not read '${rel_path}' (resolved '${full}')`, start);
60947
+ return new ValueNode(start, "\"\"");
60948
+ }
60949
+ return new ValueNode(start, `"${escape_embedded(content)}"`);
60950
+ }
60951
+ /** Encode file contents as a source-level string literal body. */
60952
+ function escape_embedded(content) {
60953
+ let out = "";
60954
+ for (const ch of content) {
60955
+ const code = ch.charCodeAt(0);
60956
+ if (ch === "\\") out += "\\\\";
60957
+ else if (ch === "\"") out += "\\\"";
60958
+ else if (ch === "\n") out += "\\n";
60959
+ else if (ch === "\r") out += "\\r";
60960
+ else if (ch === " ") out += "\\t";
60961
+ else if (code < 32 || code === 127) out += `\\${code.toString(8).padStart(3, "0")}`;
60962
+ else out += ch;
60963
+ }
60964
+ return out;
60965
+ }
60966
+ //#endregion
60584
60967
  //#region ../src/parse/parse_expression.ts
60585
60968
  function restructure_op(start, current_op, current_prec, current_node, expression) {
60586
60969
  if (current_prec <= operator_precedence(expression.op)) {
@@ -60720,6 +61103,7 @@ function parse_primary(status, value) {
60720
61103
  expect(")", status);
60721
61104
  return node;
60722
61105
  }
61106
+ case "#": return parse_embed_file(status);
60723
61107
  case "if": return parse_if_else(status);
60724
61108
  case "match": return parse_match(status);
60725
61109
  case "switch": return parse_switch(status);
@@ -61862,7 +62246,8 @@ function parse(source, library, file_path, options) {
61862
62246
  qualified_paths: [],
61863
62247
  library,
61864
62248
  errors: [],
61865
- allow_user_raw: options?.allow_user_raw
62249
+ allow_user_raw: options?.allow_user_raw,
62250
+ embed_root: embed_root_for(file_path)
61866
62251
  };
61867
62252
  if (library) status.unsafe_boundary = user_source_length;
61868
62253
  parse_statement(status);
@@ -61939,6 +62324,23 @@ function mark_library_nodes(root, boundary) {
61939
62324
  walk(root);
61940
62325
  }
61941
62326
  /**
62327
+ * The base directory for `#embed_file` paths: the package root (the nearest
62328
+ * enclosing directory with a package.jsonc), falling back to the entry
62329
+ * file's folder. No file path — undefined, so `#embed_file` resolves
62330
+ * against the process CWD.
62331
+ */
62332
+ function embed_root_for(file_path) {
62333
+ if (!file_path) return void 0;
62334
+ let dir = path.dirname(path.resolve(file_path));
62335
+ for (let i = 0; i < 32; i++) {
62336
+ if (fs.existsSync(path.join(dir, "package.jsonc"))) return dir;
62337
+ const parent = path.dirname(dir);
62338
+ if (parent === dir) break;
62339
+ dir = parent;
62340
+ }
62341
+ return path.dirname(path.resolve(file_path));
62342
+ }
62343
+ /**
61942
62344
  * Append the library source that `source` needs, exactly as `parse` does.
61943
62345
  * Exported so tooling (e.g. the editor extension) can reproduce the combined
61944
62346
  * source that node offsets refer to.
@@ -62096,7 +62498,9 @@ const STRING_OPTIONS = /* @__PURE__ */ new Map([
62096
62498
  ["p", "platform"],
62097
62499
  ["lib", "lib"],
62098
62500
  ["l", "lib"],
62099
- ["audit-runtime", "audit_runtime"]
62501
+ ["audit-runtime", "audit_runtime"],
62502
+ ["jobs", "jobs"],
62503
+ ["j", "jobs"]
62100
62504
  ]);
62101
62505
  const BOOLEAN_OPTIONS = /* @__PURE__ */ new Map([
62102
62506
  ["watch", "watch"],
@@ -62192,7 +62596,7 @@ function print_help() {
62192
62596
  " nomen check --in [file/folder] Parse and check only",
62193
62597
  " nomen format [--in folder] Reformat every .nm file",
62194
62598
  " nomen docs [--in file] Generate markdown documentation",
62195
- " nomen test [--in folder] Discover and run *.test.nm files",
62599
+ " nomen test [--in folder] Discover and run *.test.nm files (binary CWD = --in folder)",
62196
62600
  " nomen init <name> Scaffold a new project in ./<name>",
62197
62601
  " nomen lib-path Print the bundled System library path",
62198
62602
  "",
@@ -62206,8 +62610,9 @@ function print_help() {
62206
62610
  " --platform, -p <name> Target platform: macos, ios, linux, android, windows, web",
62207
62611
  " --lib, -l <path> Path to System library directory (containing package.jsonc)",
62208
62612
  " --audit Audit the generated program for memory issues",
62209
- " --audit-runtime <path> Path to audit_runtime.c, linked in when --audit is set",
62210
- " --release, -r Build with optimizations (clang -O2; asm passes on aarch64)",
62613
+ " --audit-runtime <path> Path to audit_runtime.c (default: auto-discovered — walk-up or the CLI-bundled copy)",
62614
+ " --jobs, -j <n> For `nomen test`: max test files run in parallel (default: CPU count)",
62615
+ " --release, -r Build with optimizations (clang -O2; aarch64 asm cleanup passes)",
62211
62616
  " --fast-math Allow FP reassociation (float loop reductions vectorize on aarch64)",
62212
62617
  " --check For `nomen format`: report files that would change",
62213
62618
  " -h, --help Show this help"
@@ -62555,6 +62960,9 @@ function find_bundled(filename) {
62555
62960
  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)];
62556
62961
  for (const c of candidates) if (fs.existsSync(c)) return path.resolve(c);
62557
62962
  }
62963
+ function find_bundled_audit_runtime() {
62964
+ return find_bundled("src/audit_runtime.c") ?? find_bundled("audit_runtime.c");
62965
+ }
62558
62966
  /**
62559
62967
  * `nomen init <name>`: scaffold a new project under `./<name>` with a sensible
62560
62968
  * starting layout (package.jsonc, src/main.nm, a starter test, README and an
@@ -62633,6 +63041,7 @@ function outfile_for(input_path, is_test, out) {
62633
63041
  //#endregion
62634
63042
  //#region src/test.ts
62635
63043
  const RECORD_PREFIX = "\\nomen|";
63044
+ const execFile_async = promisify(execFile);
62636
63045
  /** Recursively collect `*.test.nm` files under `root`, skipping build output. */
62637
63046
  function collect_test_files(root) {
62638
63047
  const out = [];
@@ -62782,7 +63191,7 @@ function extract_leaks(stdout) {
62782
63191
  * Compile one `*.test.nm` file (with the generated harness) and run it,
62783
63192
  * returning its parsed records and any crash info.
62784
63193
  */
62785
- function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime) {
63194
+ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime, release = false, cwd) {
62786
63195
  const start = performance.now();
62787
63196
  const source_text = fs.readFileSync(entry_path, "utf8");
62788
63197
  const harness = generate_harness(extract_test_functions(source_text), extract_bench_functions(source_text));
@@ -62809,7 +63218,8 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
62809
63218
  }
62810
63219
  const buildResult = build(parsed.root, {
62811
63220
  arch,
62812
- audit
63221
+ audit,
63222
+ optimize: release
62813
63223
  });
62814
63224
  if (buildResult.errors && buildResult.errors.length) {
62815
63225
  result.ok = false;
@@ -62822,7 +63232,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
62822
63232
  if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
62823
63233
  let audit_obj;
62824
63234
  if (audit) {
62825
- const runtime_src = audit_runtime ? path.resolve(audit_runtime) : find_audit_runtime(path.dirname(resolved));
63235
+ const runtime_src = audit_runtime ? path.resolve(audit_runtime) : find_audit_runtime(path.dirname(resolved)) ?? find_bundled_audit_runtime();
62826
63236
  if (!runtime_src || !fs.existsSync(runtime_src)) {
62827
63237
  result.ok = false;
62828
63238
  result.phase = "setup";
@@ -62831,7 +63241,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
62831
63241
  return result;
62832
63242
  }
62833
63243
  audit_obj = path.join(buildDir, "audit_runtime.o");
62834
- execFileSync("clang", [
63244
+ await execFile_async("clang", [
62835
63245
  "-c",
62836
63246
  runtime_src,
62837
63247
  "-o",
@@ -62844,22 +63254,23 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
62844
63254
  const outfile = path.join(buildDir, path.basename(entry_path, ".nm"));
62845
63255
  fs.writeFileSync(path.join(buildDir, "main.h"), buildResult.headers ?? "");
62846
63256
  fs.writeFileSync(codefile, buildResult.code);
63257
+ let companionfile;
63258
+ if (buildResult.companion) {
63259
+ const comp_ext = platform === "macos" || platform === "ios" ? ".m" : ".c";
63260
+ companionfile = path.join(buildDir, path.basename(entry_path, ".nm") + "_companion" + comp_ext);
63261
+ fs.writeFileSync(companionfile, buildResult.companion);
63262
+ }
62847
63263
  const link_args = [
62848
63264
  "-o",
62849
63265
  outfile,
62850
63266
  codefile
62851
63267
  ];
63268
+ if (companionfile) link_args.push(companionfile);
63269
+ if (release) link_args.unshift("-O2");
62852
63270
  if (audit_obj) link_args.push(audit_obj);
62853
63271
  if (platform === "macos" || platform === "ios") link_args.push("-framework", "CoreGraphics", "-framework", "Foundation", "-framework", "AppKit", "-lobjc");
62854
63272
  try {
62855
- execFileSync("clang", link_args, {
62856
- encoding: "utf8",
62857
- stdio: [
62858
- "ignore",
62859
- "pipe",
62860
- "pipe"
62861
- ]
62862
- });
63273
+ await execFile_async("clang", link_args, { encoding: "utf8" });
62863
63274
  } catch (err) {
62864
63275
  const stderr = err.stderr ? err.stderr.toString() : err.message ?? "";
62865
63276
  result.ok = false;
@@ -62871,16 +63282,12 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
62871
63282
  let runStdout = "";
62872
63283
  let crashed;
62873
63284
  try {
62874
- runStdout = execFileSync(outfile, [], {
63285
+ runStdout = (await execFile_async(outfile, [], {
62875
63286
  encoding: "utf8",
62876
- stdio: [
62877
- "ignore",
62878
- "pipe",
62879
- "pipe"
62880
- ],
62881
63287
  timeout: 3e4,
62882
- maxBuffer: 16777216
62883
- });
63288
+ maxBuffer: 16777216,
63289
+ cwd: cwd ?? process.cwd()
63290
+ })).stdout;
62884
63291
  } catch (err) {
62885
63292
  runStdout = err.stdout ? err.stdout.toString() : "";
62886
63293
  if (err.signal === "SIGTERM") crashed = "test binary timed out after 30s";
@@ -62939,10 +63346,16 @@ function report_file(result) {
62939
63346
  for (const line of result.other) console.log(C.dim(` ${line}`));
62940
63347
  }
62941
63348
  }
63349
+ /** The default concurrency for `runTests` when no `jobs` is given. */
63350
+ function default_jobs() {
63351
+ const n = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length;
63352
+ return Math.max(1, n);
63353
+ }
62942
63354
  /**
62943
63355
  * Locate audit_runtime.c for an audited test build: an explicit path wins,
62944
63356
  * otherwise walk up from `dir` looking for a `src/audit_runtime.c` (the same
62945
- * convention the run/build commands use).
63357
+ * convention the run/build commands use). When both miss, the caller falls
63358
+ * back to the CLI-bundled runtime (`find_bundled_audit_runtime`).
62946
63359
  */
62947
63360
  function find_audit_runtime(dir) {
62948
63361
  let cur = dir;
@@ -62955,18 +63368,25 @@ function find_audit_runtime(dir) {
62955
63368
  }
62956
63369
  }
62957
63370
  /** Discover, run, and report every `*.test.nm` under `root`. */
62958
- function runTests(root, options = {}) {
63371
+ async function runTests(root, options = {}) {
62959
63372
  const arch = options.arch ?? "aarch64";
62960
63373
  const lib = resolve_lib_for(root);
62961
63374
  const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
62962
63375
  console.log(`Found ${files.length} test file(s)\n`);
62963
63376
  const startTime = performance.now();
62964
- const results = [];
62965
- for (const file of files) {
62966
- const result = run_test_file(file, lib, arch, options.audit, options.audit_runtime);
62967
- report_file(result);
62968
- results.push(result);
62969
- }
63377
+ const binary_cwd = path.resolve(root);
63378
+ const jobs = Math.max(1, Math.min(options.jobs ?? default_jobs(), files.length));
63379
+ const results = new Array(files.length);
63380
+ let next = 0;
63381
+ const worker = async () => {
63382
+ for (;;) {
63383
+ const i = next++;
63384
+ if (i >= files.length) return;
63385
+ results[i] = await run_test_file(files[i], lib, arch, options.audit, options.audit_runtime, options.release, binary_cwd);
63386
+ }
63387
+ };
63388
+ await Promise.all(Array.from({ length: jobs }, worker));
63389
+ for (const result of results) report_file(result);
62970
63390
  const elapsed = performance.now() - startTime;
62971
63391
  const totalFiles = results.length;
62972
63392
  const totalFilesFailed = results.reduce((a, r) => a + (r.ok ? 0 : 1), 0);
@@ -63067,11 +63487,13 @@ try {
63067
63487
  if (command === "test") {
63068
63488
  const root = args.in ?? process.cwd();
63069
63489
  const filter = args.filter ? new RegExp(args.filter) : void 0;
63070
- const ok = runTests(root, {
63490
+ const ok = await runTests(root, {
63071
63491
  arch: args.arch ?? "aarch64",
63072
63492
  filter,
63073
63493
  audit: args.audit,
63074
- audit_runtime: args.audit_runtime
63494
+ audit_runtime: args.audit_runtime,
63495
+ release: args.release,
63496
+ jobs: args.jobs !== void 0 ? parseInt(args.jobs, 10) : void 0
63075
63497
  });
63076
63498
  process.exit(ok ? 0 : 1);
63077
63499
  }
@@ -63182,6 +63604,7 @@ function resolve_audit_runtime(config, input_path) {
63182
63604
  if (parent === dir) break;
63183
63605
  dir = parent;
63184
63606
  }
63607
+ return find_bundled_audit_runtime();
63185
63608
  }
63186
63609
  function compile_audit_runtime(config, input_path, buildDir) {
63187
63610
  const runtime_src = resolve_audit_runtime(config, input_path);