nomen-lang 0.6.7 → 0.6.8

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/NOMEN_AGENTS.md CHANGED
@@ -219,9 +219,9 @@ Special function names: `#init` (constructor), `#destroy` (auto-raii destructor)
219
219
 
220
220
  ```nomen
221
221
  if cond { ... }
222
- switch x {
223
- 0 { ... }
224
- 1 { ... }
222
+ switch {
223
+ case x == 0 { ... }
224
+ case x == 1 { ... }
225
225
  else { ... }
226
226
  }
227
227
  for i of 0 .. 10 { ... } // exclusive range
@@ -280,6 +280,15 @@ const typed = Array<int>(1, 2, 3)
280
280
  for n of nums { Console.write("\{n}\n") }
281
281
  ```
282
282
 
283
+ ### Compile-time embedding
284
+
285
+ ```nomen
286
+ const corpus = #embed_file("bench/full-markdown.md") // string literal of the
287
+ // file's contents; path
288
+ // resolves from the
289
+ // package root
290
+ ```
291
+
283
292
  ## Reference
284
293
 
285
294
  - Full language spec: [SPEC.md](https://github.com/andrewjk/nomen/blob/main/SPEC.md)
@@ -510,6 +510,17 @@ pub struct Buffer<T> {
510
510
  ```
511
511
  }
512
512
 
513
+ // Move-taking write, mirroring `List.set`: the caller's value is
514
+ // consumed and an independent copy is installed in the slot (replace
515
+ // deep-copies owning elements). The owning entry point a caller uses
516
+ // when it wants to hand over a value — `replace`'s by-value parameter
517
+ // cannot accept the `move` keyword, so a borrow being transferred
518
+ // (e.g. a loaded element) must go through here rather than be bound to
519
+ // a freed local.
520
+ pub func set = (ref self, int i: i >= 0 && i < self.cap, move T value) {
521
+ self.replace(i, value)
522
+ }
523
+
513
524
  // Move slot src into slot dst: overwrite dst with src's bytes and zero
514
525
  // src, so exactly ONE slot owns the value afterwards. The
515
526
  // ownership-transferring primitive behind backward-shift deletion
@@ -620,6 +631,33 @@ pub struct Buffer<T> {
620
631
  memset(self.data, 0, len * T_SIZE)
621
632
  }
622
633
 
634
+ // An independent copy: fresh slab, per-element deep copies. Each slot
635
+ // round-trips load→replace through a local, so owning element types get
636
+ // the same treatment a List<T>.copy slot does (the load materializes an
637
+ // owned copy, replace gives the destination its own). Class elements
638
+ // never reach this body (a `Buffer<T>` with T a class monomorphizes to
639
+ // `ClassBuffer<T>`, which has no copy).
640
+ pub func copy = (self, out Buffer<T>: out.cap == self.cap) {
641
+ var dst = Buffer<T>()
642
+ if self.cap > 0 {
643
+ dst.alloc(self.cap)
644
+ var i = 0
645
+ while i < self.cap {
646
+ // Bind the loaded element and hand it to `set` (move): for a
647
+ // string element load returns a BORROW of the source slot, and
648
+ // the move transfers it into the write without a scope-exit
649
+ // reclaim (which would free the source's buffer); for an owning
650
+ // struct element load returns an owned copy that transfers the
651
+ // same way. Mirrors List.copy's `var T v = self.at(i)` +
652
+ // `dst.push(v)`.
653
+ var T v = self.load(i)
654
+ dst.set(i, v)
655
+ i += 1
656
+ }
657
+ }
658
+ return dst
659
+ }
660
+
623
661
  internal inline func store_or_int = (ref self, int i: i >= 0 && i < self.cap, int val) {
624
662
  ```
625
663
  #arch: c
package/dist/index.mjs CHANGED
@@ -6811,6 +6811,27 @@ function find_enum(name, status) {
6811
6811
  }
6812
6812
  //#endregion
6813
6813
  //#region ../src/build_common/mono_name.ts
6814
+ /**
6815
+ * Whether `name` is a monomorphized owning-container struct whose
6816
+ * `<Mono>_copy` is safe to call from the owning-`Buffer.load_T`
6817
+ * specialization — `List_<T>` / `Buffer_<T>` (and their bare generics).
6818
+ * These own a heap slab their `#destroy` frees unconditionally, and `copy`
6819
+ * yields an independent slab, so a returned element's field must be
6820
+ * deep-copied or the caller's unconditional container-field teardown
6821
+ * double-frees the slot's slab.
6822
+ *
6823
+ * `Map`/`Set` are deliberately EXCLUDED from the automatic deep copy: their
6824
+ * monomorphized `copy` (which composes several `Buffer.copy` calls) is
6825
+ * verified standalone on both backends, but invoking it from the load
6826
+ * specialization corrupts the returned struct on aarch64 (a codegen
6827
+ * interaction still open — see FOLLOWUP.md). A `Map`/`Set` FIELD reached
6828
+ * through a container element's `load` therefore stays shallow (the
6829
+ * pre-existing bounded gap); an EXPLICIT `.copy()` on the owning struct or
6830
+ * the Map/Set itself works on both backends.
6831
+ */
6832
+ function is_load_deep_copy_container(name) {
6833
+ return name === "List" || name === "Buffer" || name.startsWith("List_") || name.startsWith("Buffer_");
6834
+ }
6814
6835
  function mono_type_name(type_or_name, type_args) {
6815
6836
  const name = typeof type_or_name === "string" ? type_or_name : type_or_name.name;
6816
6837
  const args = typeof type_or_name === "string" ? type_args : type_or_name.type_args;
@@ -7676,10 +7697,15 @@ function direct_string_fields(node) {
7676
7697
  * callee's own records are scope-local and die at return, so without the
7677
7698
  * transfer the stored copy leaks.
7678
7699
  *
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
7700
+ * Only DIRECT statements qualify for the plain transfer: a store nested in a
7701
+ * branch/loop may not execute, and a record over the pre-call value would
7702
+ * free it at the caller's scope exit (possibly rodata — an invalid free).
7703
+ * Those CONDITIONAL stores are reported separately
7704
+ * (scan_ref_param_string_field_may_writes): the callee's function builder
7705
+ * emits an ENTRY DUP for each of them (`field = strdup(field)` at function
7706
+ * entry, recorded), which makes the field heap-owned on EVERY path and the
7707
+ * may-store record sound to transfer. Memoized on the function node;
7708
+ * forwarding cycles are cut by a visited set (the cycle member's own
7683
7709
  * contribution is skipped, bounded-leak).
7684
7710
  */
7685
7711
  function scan_ref_param_string_field_writes(func, structs) {
@@ -7687,6 +7713,51 @@ function scan_ref_param_string_field_writes(func, structs) {
7687
7713
  if (stamp._ref_param_string_field_writes) return stamp._ref_param_string_field_writes;
7688
7714
  const result = /* @__PURE__ */ new Map();
7689
7715
  stamp._ref_param_string_field_writes = result;
7716
+ const tracked = tracked_ref_params(func, structs);
7717
+ if (tracked.size) scan_top_level(func.statements ?? [], tracked, structs, result);
7718
+ return result;
7719
+ }
7720
+ /**
7721
+ * The CONDITIONAL (`may-executed`) ref-param field stores: direct stores
7722
+ * nested in if/else branches, switch cases, and loop bodies. Forwarded
7723
+ * calls from a nested position are deliberately EXCLUDED: a helper's store
7724
+ * builds with its own (fresh) record set, so it cannot reclaim the
7725
+ * displaced entry-dup — dup'ing for a forwarded store would trade the
7726
+ * old bounded leak for a new one. Those chains keep the documented
7727
+ * bounded-leak posture (the stored copy leaks, never an invalid free).
7728
+ * The function builder entry-dups these fields (see
7729
+ * ref_param_entry_dup_fields), making the may-store record transferable.
7730
+ */
7731
+ function scan_ref_param_string_field_may_writes(func, structs) {
7732
+ const stamp = func;
7733
+ if (stamp._ref_param_string_field_may_writes) return stamp._ref_param_string_field_may_writes;
7734
+ const result = /* @__PURE__ */ new Map();
7735
+ stamp._ref_param_string_field_may_writes = result;
7736
+ const tracked = tracked_ref_params(func, structs);
7737
+ if (tracked.size) scan_nested(func.statements ?? [], tracked, result);
7738
+ return result;
7739
+ }
7740
+ /**
7741
+ * The fields a function's builder must ENTRY DUP: the conditional
7742
+ * (may-executed) ref-param string-field stores that are NOT also
7743
+ * definitely-executed (a must-store already leaves the field heap-owned on
7744
+ * every path — dup'ing it too would just add a pointless strdup per call).
7745
+ * The dup runs at callee entry on both backends and records the field, so a
7746
+ * call site may transfer these as heap records soundly.
7747
+ */
7748
+ function ref_param_entry_dup_fields(func, structs) {
7749
+ const must = scan_ref_param_string_field_writes(func, structs);
7750
+ const may = scan_ref_param_string_field_may_writes(func, structs);
7751
+ const result = /* @__PURE__ */ new Map();
7752
+ for (const [param, fields] of may) {
7753
+ const dup = /* @__PURE__ */ new Set();
7754
+ for (const field of fields) if (!must.get(param)?.has(field)) dup.add(field);
7755
+ if (dup.size) result.set(param, dup);
7756
+ }
7757
+ return result;
7758
+ }
7759
+ /** The tracked ref params of `func`: name -> writable plain string fields. */
7760
+ function tracked_ref_params(func, structs) {
7690
7761
  const tracked = /* @__PURE__ */ new Map();
7691
7762
  for (const param of func.params ?? []) {
7692
7763
  if (param.is_self_param) continue;
@@ -7695,8 +7766,7 @@ function scan_ref_param_string_field_writes(func, structs) {
7695
7766
  const fields = ref_param_writable_fields(param.type.name, structs);
7696
7767
  if (fields.size) tracked.set(param.name, fields);
7697
7768
  }
7698
- if (tracked.size) scan_function(func, tracked, structs, result, /* @__PURE__ */ new Set());
7699
- return result;
7769
+ return tracked;
7700
7770
  }
7701
7771
  /** The writable plain string fields of the value struct `type_name` resolves
7702
7772
  * to, or the empty set (non-struct, class, or no string fields). */
@@ -7706,46 +7776,91 @@ function ref_param_writable_fields(type_name, structs) {
7706
7776
  if (!struct) return /* @__PURE__ */ new Set();
7707
7777
  return new Set(direct_string_fields(struct).map((f) => f.name));
7708
7778
  }
7709
- function scan_function(func, tracked, structs, result, visiting) {
7710
- visiting.add(func);
7711
- for (const stmt of func.statements ?? []) {
7779
+ /**
7780
+ * The top-level (definitely-executed) walk: direct `param.field = …`
7781
+ * statements and top-level forwarded calls only — nothing nested.
7782
+ */
7783
+ function scan_top_level(statements, tracked, structs, result) {
7784
+ for (const stmt of statements) {
7712
7785
  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
- }
7786
+ record_direct_store(stmt, tracked, result);
7726
7787
  continue;
7727
7788
  }
7728
7789
  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);
7790
+ if (call) merge_forwarded(call, tracked, structs, result);
7791
+ }
7792
+ }
7793
+ /**
7794
+ * The nested walk: direct stores at ANY depth below the top level are
7795
+ * may-stores. Forwarded calls are skipped everywhere (their contributions
7796
+ * would need cross-function displaced-value knowledge the callee's fresh
7797
+ * record set cannot carry — see the may-scan doc).
7798
+ */
7799
+ function scan_nested(statements, tracked, result) {
7800
+ for (const stmt of statements) {
7801
+ if (stmt.node_type === "assign") {
7802
+ record_direct_store(stmt, tracked, result);
7803
+ continue;
7804
+ }
7805
+ for (const nested of nested_statement_lists(stmt)) scan_nested(nested, tracked, result);
7806
+ }
7807
+ }
7808
+ /** Record a direct `param.<field> = …` store into `result`. */
7809
+ function record_direct_store(stmt, tracked, result) {
7810
+ const lhs = stmt.left_value;
7811
+ if (lhs?.node_type !== "access") return;
7812
+ const access = lhs;
7813
+ if (access.access.node_type !== "access_field" || access.target.node_type !== "value") return;
7814
+ const name = access.target.value ?? "";
7815
+ const fields = tracked.get(name);
7816
+ const field = access.access.name ?? "";
7817
+ if (!fields?.has(field)) return;
7818
+ if (!result.has(name)) result.set(name, /* @__PURE__ */ new Set());
7819
+ result.get(name).add(field);
7820
+ }
7821
+ /** Merge a forwarded call's callee stores for the forwarded param: the
7822
+ * callee's MUST stores at a top-level position. */
7823
+ function merge_forwarded(call, tracked, structs, result) {
7824
+ const [callee, args] = call;
7825
+ const non_self_params = (callee.params ?? []).filter((p) => !p.is_self_param);
7826
+ for (let i = 0; i < args.length && i < non_self_params.length; i++) {
7827
+ const arg = args[i];
7828
+ if (arg?.node_type !== "value") continue;
7829
+ const arg_name = arg.value ?? "";
7830
+ const my_fields = tracked.get(arg_name);
7831
+ if (!my_fields) continue;
7832
+ const callee_param = non_self_params[i];
7833
+ if (!(callee_param.is_ref || callee_param.type.is_ref)) continue;
7834
+ const source = scan_ref_param_string_field_writes(callee, structs).get(callee_param.name);
7835
+ if (!source?.size) continue;
7836
+ if (!result.has(arg_name)) result.set(arg_name, /* @__PURE__ */ new Set());
7837
+ const mine = result.get(arg_name);
7838
+ for (const field of source) if (my_fields.has(field)) mine.add(field);
7839
+ }
7840
+ }
7841
+ /** The statement lists nested inside `stmt` (branch bodies, loop bodies) —
7842
+ * the same walk scan_reassigned_vars uses. */
7843
+ function nested_statement_lists(stmt) {
7844
+ const branch_of = (b) => b?.statements ?? [];
7845
+ const lists = [];
7846
+ if (stmt.node_type === "while" || stmt.node_type === "for") {
7847
+ const nested = stmt.statements;
7848
+ if (nested) lists.push(nested);
7849
+ return lists;
7850
+ }
7851
+ if (stmt.node_type === "if") {
7852
+ const n = stmt;
7853
+ if (n.if_branch) lists.push(branch_of(n.if_branch));
7854
+ if (n.else_branch) lists.push(branch_of(n.else_branch));
7855
+ return lists;
7856
+ }
7857
+ if (stmt.node_type === "switch" || stmt.node_type === "match") {
7858
+ const n = stmt;
7859
+ for (const c of n.cases ?? []) if (c.branch) lists.push(branch_of(c.branch));
7860
+ if (n.else_branch) lists.push(branch_of(n.else_branch));
7861
+ return lists;
7862
+ }
7863
+ return lists;
7749
7864
  }
7750
7865
  /**
7751
7866
  * The forwarded-call shape of a top-level statement: a resolved free-function
@@ -7776,6 +7891,12 @@ function ref_forward_call(stmt) {
7776
7891
  * stores (scan_ref_param_string_field_writes) onto the caller's records for
7777
7892
  * the argument variable, making the caller's scope exit free them.
7778
7893
  *
7894
+ * The callee's CONDITIONAL stores transfer too
7895
+ * (ref_param_entry_dup_fields): the callee's builder entry-dups those
7896
+ * fields (`field = strdup(field)` at entry, recorded), so the field is
7897
+ * heap-owned on EVERY path out of the call — the not-taken path holds the
7898
+ * dup — and a caller-side record is sound.
7899
+ *
7779
7900
  * `arg_is_trackable_local` must report true only for variables whose storage
7780
7901
  * THIS function's scope exit owns — a plain local. A `ref` param of the
7781
7902
  * enclosing function forwards caller storage, so a record here would free the
@@ -7783,19 +7904,22 @@ function ref_forward_call(stmt) {
7783
7904
  */
7784
7905
  function transfer_ref_param_field_records(callee, args, status, arg_is_trackable_local) {
7785
7906
  const writes = scan_ref_param_string_field_writes(callee, status.structs);
7786
- if (!writes.size) return;
7907
+ const dups = ref_param_entry_dup_fields(callee, status.structs);
7908
+ if (!writes.size && !dups.size) return;
7787
7909
  const non_self_params = (callee.params ?? []).filter((p) => !p.is_self_param);
7788
7910
  for (let i = 0; i < args.length && i < non_self_params.length; i++) {
7789
7911
  const param = non_self_params[i];
7790
7912
  if (!(param.is_ref || param.type.is_ref)) continue;
7791
7913
  const fields = writes.get(param.name);
7792
- if (!fields?.size) continue;
7914
+ const dup_fields = dups.get(param.name);
7915
+ if (!fields?.size && !dup_fields?.size) continue;
7793
7916
  const arg = args[i];
7794
7917
  if (arg?.node_type !== "value") continue;
7795
7918
  const name = arg.value;
7796
7919
  if (!name || !arg_is_trackable_local(name)) continue;
7797
7920
  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}`);
7921
+ for (const field of fields ?? []) status.heap_string_fields.add(`${name}.${field}`);
7922
+ for (const field of dup_fields ?? []) status.heap_string_fields.add(`${name}.${field}`);
7799
7923
  }
7800
7924
  }
7801
7925
  //#endregion
@@ -8288,7 +8412,7 @@ function is_container_borrow_accessor_name(name) {
8288
8412
  * site (see build_return_node), so it counts as owned for classification.
8289
8413
  */
8290
8414
  function is_call_site_borrow_accessor(fn_name) {
8291
- return fn_name === "at" || fn_name === "first";
8415
+ return fn_name === "at" || fn_name === "first" || fn_name === "load";
8292
8416
  }
8293
8417
  /**
8294
8418
  * Whether `node` is a container/buffer borrow accessor CALL (an
@@ -17957,7 +18081,7 @@ function emit_load_deep_copy_fields$1(elem, status, sret, slot) {
17957
18081
  const size = get_type_size(field.type, status);
17958
18082
  if (!field.type.is_ref && !field.type.is_view && !field.type.is_array && !field.type.is_nullable && field.type.name !== "string") {
17959
18083
  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")) {
18084
+ 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
18085
  emit_asm(status, `add x8, ${sret}, #${offset}\n`);
17962
18086
  emit_asm(status, `add x0, ${slot}, #${offset}\n`);
17963
18087
  emit_asm(status, `bl ${field_struct.name}_copy\n`);
@@ -20611,6 +20735,7 @@ function build_function_node$1(node, status) {
20611
20735
  } else status.nir_emit_ctx = void 0;
20612
20736
  const old_raw_param_reloads = status.raw_param_reloads;
20613
20737
  install_raw_reload_plan(status, raw_reload_plan_lines);
20738
+ emit_ref_param_entry_dups$1(node, status);
20614
20739
  try {
20615
20740
  build_block_node$1(node, status);
20616
20741
  } finally {
@@ -20756,6 +20881,49 @@ function build_function_node$1(node, status) {
20756
20881
  status.return_buffer_stack_offset = void 0;
20757
20882
  status.function_return_type = void 0;
20758
20883
  }
20884
+ /**
20885
+ * Entry-dup the ref-param string fields the body stores through
20886
+ * CONDITIONALLY (see ref_param_entry_dup_fields): load the field pair's
20887
+ * ptr half, strdup it, store the fresh pointer back — with the field
20888
+ * recorded in heap_string_fields — so the field is heap-owned on every
20889
+ * path out of the function and the call-site record transfer is sound.
20890
+ * Mirrors the C backend's emit_ref_param_entry_dups; the len half is
20891
+ * unchanged by a dup, so only the ptr half is rewritten. A nullable
20892
+ * string field guards the dup (strdup(NULL) traps; a null pair stores as
20893
+ * null and every reclaim path no-ops on free(NULL)).
20894
+ */
20895
+ function emit_ref_param_entry_dups$1(node, status) {
20896
+ const dups = ref_param_entry_dup_fields(node, status.structs);
20897
+ if (!dups.size) return;
20898
+ for (const param of node.params ?? []) {
20899
+ if (param.is_self_param) continue;
20900
+ if (!(param.is_ref || param.type.is_ref)) continue;
20901
+ const fields = dups.get(param.name);
20902
+ if (!fields?.size) continue;
20903
+ const struct = status.structs.find((s) => s.name === param.type.name && !s.is_simple_type && !s.is_class);
20904
+ for (const field of fields) {
20905
+ const offset = get_field_offset(param.type.name, field, status);
20906
+ const paramReg = status.function_param_regs?.get(param.name);
20907
+ if (paramReg) emit_asm(status, `mov x0, ${paramReg}\n`);
20908
+ else if (is_local_ref_var(param.name, status)) emit_deref_var_address(status, "x0", param.name);
20909
+ else emit_var_address(status, "x0", param.name);
20910
+ emit_asm(status, `str x0, [sp, #-16]!\n`);
20911
+ emit_asm(status, `ldr x0, [x0, #${offset}]\n`);
20912
+ const nullable = !!struct?.fields.find((f) => f.name === field)?.type.is_nullable;
20913
+ let skip_label;
20914
+ if (nullable) {
20915
+ skip_label = `.Lentry_dup_${status.label_counter = (status.label_counter ?? 0) + 1}`;
20916
+ emit_asm(status, `cbz x0, ${skip_label}\n`);
20917
+ }
20918
+ emit_strdup(status);
20919
+ if (skip_label) emit_asm(status, `${skip_label}:\n`);
20920
+ emit_asm(status, `ldr x1, [sp], #16\n`);
20921
+ emit_asm(status, `str x0, [x1, #${offset}]\n`);
20922
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
20923
+ status.heap_string_fields.add(`${param.name}.${field}`);
20924
+ }
20925
+ }
20926
+ }
20759
20927
  //#endregion
20760
20928
  //#region ../src/build_aarch64/build_struct_node.ts
20761
20929
  let field_strdup_guard_counter = 0;
@@ -23997,6 +24165,14 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23997
24165
  if (element_size === 1) emit_asm(status, `strb w0, [x9, #${slot}]\n`);
23998
24166
  else if (element_size === 4) emit_asm(status, `str w0, [x9, #${slot}]\n`);
23999
24167
  else emit_asm(status, `str x0, [x9, #${slot}]\n`);
24168
+ } else if (element_size > 8 && struct_element && !struct_element.is_class) {
24169
+ emit_struct_address_param(value, status);
24170
+ ensure_newline(status);
24171
+ emit_asm(status, `ldr x9, [x29, #${offset}]\n`);
24172
+ emit_asm(status, `mov x1, x0\n`);
24173
+ emit_asm(status, `add x0, x9, #${slot}\n`);
24174
+ emit_asm(status, `mov x2, #${element_size}\n`);
24175
+ emit_asm(status, `bl _memcpy\n`);
24000
24176
  } else {
24001
24177
  emit_init_value$1(value, nir_array_element(nir_init, array_literal_node, i), status);
24002
24178
  ensure_newline(status);
@@ -24036,6 +24212,14 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
24036
24212
  if (element_size === 1) emit_asm(status, `strb w0, [x9, #${slot}]\n`);
24037
24213
  else if (element_size === 4) emit_asm(status, `str w0, [x9, #${slot}]\n`);
24038
24214
  else emit_asm(status, `str x0, [x9, #${slot}]\n`);
24215
+ } else if (element_size > 8 && struct_element && !struct_element.is_class) {
24216
+ emit_struct_address_param(value, status);
24217
+ ensure_newline(status);
24218
+ emit_asm(status, `ldr x9, [${node.name}]\n`);
24219
+ emit_asm(status, `mov x1, x0\n`);
24220
+ emit_asm(status, `add x0, x9, #${slot}\n`);
24221
+ emit_asm(status, `mov x2, #${element_size}\n`);
24222
+ emit_asm(status, `bl _memcpy\n`);
24039
24223
  } else {
24040
24224
  emit_init_value$1(value, nir_array_element(nir_init, array_literal_node, i), status);
24041
24225
  ensure_newline(status);
@@ -24142,6 +24326,13 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
24142
24326
  if (element_size === 1) emit_asm(status, `strb w0, [x29, #${slot_offset}]\n`);
24143
24327
  else if (element_size === 4) emit_asm(status, `str w0, [x29, #${slot_offset}]\n`);
24144
24328
  else emit_asm(status, `str x0, [x29, #${slot_offset}]\n`);
24329
+ } else if (element_size > 8 && struct_element && !struct_element.is_class) {
24330
+ emit_struct_address_param(value, status);
24331
+ ensure_newline(status);
24332
+ emit_asm(status, `mov x1, x0\n`);
24333
+ emit_asm(status, `add x0, x29, #${slot_offset}\n`);
24334
+ emit_asm(status, `mov x2, #${element_size}\n`);
24335
+ emit_asm(status, `bl _memcpy\n`);
24145
24336
  } else {
24146
24337
  emit_init_value$1(value, nir_array_element(nir_init, array_values, i), status);
24147
24338
  ensure_newline(status);
@@ -24167,6 +24358,13 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
24167
24358
  if (element_size === 1) emit_asm(status, `strb w0, [${node.name} + ${i * element_size}]\n`);
24168
24359
  else if (element_size === 4) emit_asm(status, `str w0, [${node.name} + ${i * element_size}]\n`);
24169
24360
  else emit_asm(status, `str x0, [${node.name} + ${i * element_size}]\n`);
24361
+ } else if (element_size > 8 && struct_element && !struct_element.is_class) {
24362
+ emit_struct_address_param(value, status);
24363
+ ensure_newline(status);
24364
+ emit_asm(status, `mov x1, x0\n`);
24365
+ emit_global_slot_addr(status, node.name, i * element_size);
24366
+ emit_asm(status, `mov x2, #${element_size}\n`);
24367
+ emit_asm(status, `bl _memcpy\n`);
24170
24368
  } else {
24171
24369
  emit_init_value$1(value, nir_array_element(nir_init, array_values, i), status);
24172
24370
  ensure_newline(status);
@@ -34953,6 +35151,7 @@ function build_if_else_node(node, status, nir) {
34953
35151
  status.code += `) {\n`;
34954
35152
  let then_fields;
34955
35153
  if (node.if_branch) {
35154
+ status.heap_string_fields = new Set(pre_heap_string_fields ?? []);
34956
35155
  build_block_with_cursor(node.if_branch, nir?.then_branch, status);
34957
35156
  build_auto_free(status);
34958
35157
  }
@@ -34961,7 +35160,7 @@ function build_if_else_node(node, status, nir) {
34961
35160
  if (node.else_branch) {
34962
35161
  status.scoped_declarations = enter_c_scope(status);
34963
35162
  status.deferred_frees = [];
34964
- status.heap_string_fields = pre_heap_string_fields;
35163
+ status.heap_string_fields = new Set(pre_heap_string_fields ?? []);
34965
35164
  status.code += `} else {\n`;
34966
35165
  build_block_with_cursor(node.else_branch, nir?.else_branch, status);
34967
35166
  build_auto_free(status);
@@ -35395,7 +35594,7 @@ function build_switch_node(node, status, nir) {
35395
35594
  for (let i = 0; i < node.cases.length; i++) {
35396
35595
  const c = node.cases[i];
35397
35596
  status.scoped_declarations = enter_c_scope(status);
35398
- status.heap_string_fields = pre_heap_string_fields;
35597
+ status.heap_string_fields = new Set(pre_heap_string_fields ?? []);
35399
35598
  const saved_code = begin_code_scratch(status);
35400
35599
  build_node$1(c.condition, status);
35401
35600
  let cond_code = end_code_scratch(status, saved_code);
@@ -35441,7 +35640,7 @@ function build_switch_node(node, status, nir) {
35441
35640
  if (node.else_branch) {
35442
35641
  status.code += "} else ";
35443
35642
  status.scoped_declarations = enter_c_scope(status);
35444
- status.heap_string_fields = pre_heap_string_fields;
35643
+ status.heap_string_fields = new Set(pre_heap_string_fields ?? []);
35445
35644
  status.code += `{\n`;
35446
35645
  build_block_with_cursor(node.else_branch, nir?.otherwise ?? void 0, status);
35447
35646
  build_auto_free(status);
@@ -35864,11 +36063,13 @@ function emit_transfer_owning_fields(elem, src, status, prefix = "") {
35864
36063
  /**
35865
36064
  * Emit the deep-copy half of owning load_T: for each field whose type has a
35866
36065
  * 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").
36066
+ * /`Buffer<T>`/`Map`/`Set` field — `<Mono>_copy` deep-copies the slots or
36067
+ * member buffers — or an enum-with-data field whose string payloads
36068
+ * `<E>_copy` duplicates), overwrite the shallow-copied bytes with an owned
36069
+ * copy. A struct local's CONTAINER fields are reclaimed unconditionally at
36070
+ * scope exit, so a shallow return would double-free the slot's slab. Fields
36071
+ * without a copyable shape (class fields, nested owning user structs without
36072
+ * a synthesized copy) stay shallow — recorded in FOLLOWUP.md.
35872
36073
  */
35873
36074
  function emit_load_deep_copy_fields(elem, dst, status) {
35874
36075
  for (const field of elem.fields) {
@@ -35876,7 +36077,7 @@ function emit_load_deep_copy_fields(elem, dst, status) {
35876
36077
  if (field.type.is_nullable) continue;
35877
36078
  if (field.type.name === "string") continue;
35878
36079
  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")) {
36080
+ 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
36081
  status.code += `${dst}.${field.name} = ${field_struct.name}_copy(&${dst}.${field.name});\n`;
35881
36082
  continue;
35882
36083
  }
@@ -37470,6 +37671,7 @@ function build_function_node(node, status) {
37470
37671
  stmts: nir.body,
37471
37672
  ast: node.statements
37472
37673
  } : void 0;
37674
+ emit_ref_param_entry_dups(node, status);
37473
37675
  build_block_node(node, status, false);
37474
37676
  status.nir_emit_ctx = old_nir_ctx;
37475
37677
  status.function_ref_params = old_ref_params;
@@ -37515,6 +37717,37 @@ function build_function_node(node, status) {
37515
37717
  status.stack_array_lengths = old_stack_array_lengths;
37516
37718
  status.moved = old_moved;
37517
37719
  }
37720
+ /**
37721
+ * Emit the entry-dup for every `ref` value-struct param string field the
37722
+ * body stores through CONDITIONALLY (see ref_param_entry_dup_fields):
37723
+ * `{ field = nomen_str_dup(field) }` at function entry, recorded in
37724
+ * heap_string_fields, so the field is heap-owned on every path out of the
37725
+ * function and the call-site record transfer is sound. Nullable string
37726
+ * fields guard the dup (a null pair stores as null — free(NULL) is a
37727
+ * no-op on every reclaim path).
37728
+ */
37729
+ function emit_ref_param_entry_dups(node, status) {
37730
+ const dups = ref_param_entry_dup_fields(node, status.structs);
37731
+ if (!dups.size) return;
37732
+ for (const param of node.params ?? []) {
37733
+ if (param.is_self_param) continue;
37734
+ if (!(param.is_ref || param.type.is_ref)) continue;
37735
+ const fields = dups.get(param.name);
37736
+ if (!fields?.size) continue;
37737
+ const struct = status.structs.find((s) => s.name === param.type.name && !s.is_simple_type && !s.is_class);
37738
+ const base = c_function_name(param.name);
37739
+ for (const field of fields) {
37740
+ const nullable = !!struct?.fields.find((f) => f.name === field)?.type.is_nullable;
37741
+ if (nullable) status.code += `if (${base}->${field}.ptr) {\n`;
37742
+ const temp = `_dup_t_${status.label_counter = (status.label_counter ?? 0) + 1}`;
37743
+ status.code += `nomen_string ${temp} = nomen_str_dup(${base}->${field});\n`;
37744
+ status.code += `${base}->${field} = ${temp};\n`;
37745
+ if (nullable) status.code += `}\n`;
37746
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
37747
+ status.heap_string_fields.add(`${param.name}.${field}`);
37748
+ }
37749
+ }
37750
+ }
37518
37751
  function emit_nested_declarations(node, status) {
37519
37752
  const block = node;
37520
37753
  for (let child of block.statements) switch (child.node_type) {
@@ -37805,6 +38038,17 @@ function build_function_call_node(node, status) {
37805
38038
  status.code += `${func_name}(${is_closure_callee ? "NULL" : ""}`;
37806
38039
  if (is_closure_callee && node.params.length > 0) status.code += ", ";
37807
38040
  const variadic_idx = node.variadic_param_name ? node.params.findIndex((p) => p.node_type === "array") : -1;
38041
+ if (!node.variadic_param_name) {
38042
+ const resolved = node.resolved_function ?? (is_struct ? status.structs.find((s) => s.name === node.name)?.functions.find((f) => f.name === "#init") : void 0);
38043
+ const variadic_param = resolved?.params?.find((p) => p.is_variadic);
38044
+ if (resolved && variadic_param) {
38045
+ if (node.params.length === 1 && node.params[0].node_type === "array") node.variadic_param_name = variadic_param.name;
38046
+ else if (node.params.length === 0 && resolved.params.filter((p) => !p.is_self_param && !p.is_variadic).length === 0) {
38047
+ const elem_c = c_type(variadic_param.type.name);
38048
+ status.code += `0, (${elem_c}[]){ }`;
38049
+ }
38050
+ }
38051
+ }
37808
38052
  for (let i = 0; i < node.params.length; i++) {
37809
38053
  if (i > 0) status.code += ", ";
37810
38054
  if (i === variadic_idx && node.params[i].node_type === "array") {
@@ -41412,7 +41656,11 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41412
41656
  if (name === "self" && status.current_struct) target_type = new Type(status.current_struct.name);
41413
41657
  else if (status.variable_types?.has(name)) target_type = status.variable_types.get(name);
41414
41658
  }
41415
- const field_type = access.access.type;
41659
+ let field_type = access.access.type;
41660
+ if (!field_type?.name && target_type?.name) {
41661
+ const tf = status.structs.find((s) => s.name === target_type.name)?.fields.find((f) => f.name === field_name);
41662
+ if (tf) field_type = tf.type;
41663
+ }
41416
41664
  const field_is_struct = is_struct_type$2(field_type, status);
41417
41665
  if (access.target.node_type === "value") {
41418
41666
  const name = access.target.value;
@@ -56114,6 +56362,14 @@ function check_function_call(node, status, func, target_type, self_value, self_p
56114
56362
  }
56115
56363
  node.type = func.return_type;
56116
56364
  node.is_static = func.is_static;
56365
+ if (target_type?.type_args?.length && node.type?.type_args?.length) {
56366
+ const owner = func.scope;
56367
+ if (owner && owner.node_type === "struct" && owner.is_generic && owner.type_params.length === target_type.type_args.length) {
56368
+ const rebind = /* @__PURE__ */ new Map();
56369
+ owner.type_params.forEach((tp, i) => rebind.set(tp, target_type.type_args[i].name));
56370
+ node.type = substitute_type(node.type, rebind);
56371
+ }
56372
+ }
56117
56373
  if (node.type?.type_args?.length) instantiate_generic_type(node.type, status);
56118
56374
  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
56375
  node.type = clone_type$2(node.type);
@@ -56297,7 +56553,10 @@ function check_function_call(node, status, func, target_type, self_value, self_p
56297
56553
  else {
56298
56554
  const stamped = String(func_param.type.length.value);
56299
56555
  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);
56556
+ if (stamped !== incoming) {
56557
+ if (is_heap_array_type(func_param.type)) func_param.type.length = void 0;
56558
+ 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);
56559
+ }
56301
56560
  }
56302
56561
  }
56303
56562
  let arg_value;
@@ -56528,6 +56787,13 @@ function check_function_call(node, status, func, target_type, self_value, self_p
56528
56787
  if (typeof size_val === "number" && size_val > 0) record_buffer_cap(self_path, size_val, status);
56529
56788
  }
56530
56789
  }
56790
+ if (self_path && self_path !== "?" && is_bufferish && func.name === "copy") {
56791
+ const source_cap = lookup_buffer_cap(self_path, status);
56792
+ if (source_cap !== void 0) {
56793
+ const lhs_name = find_lhs_var_name(status);
56794
+ if (lhs_name) record_buffer_cap(lhs_name, source_cap, status);
56795
+ }
56796
+ }
56531
56797
  if (func.return_constraint) {
56532
56798
  const param_to_arg = /* @__PURE__ */ new Map();
56533
56799
  if (self_path && self_path !== "?") param_to_arg.set("self", path_to_node(self_path));
@@ -60581,6 +60847,68 @@ function parse_case_branch(status) {
60581
60847
  return null;
60582
60848
  }
60583
60849
  //#endregion
60850
+ //#region ../src/parse/utils/parse_embed_file.ts
60851
+ /**
60852
+ * `#embed_file("relative/path")` — a compile-time string literal whose value
60853
+ * is the contents of the file at parse time. The path resolves against the
60854
+ * package root (the directory containing package.jsonc, found by climbing
60855
+ * from the entry file), falling back to the entry file's folder; with no
60856
+ * known base it resolves against the process CWD.
60857
+ *
60858
+ * The parsed form is a plain string-literal ValueNode: escapes are encoded
60859
+ * with the source-level escape set (`\\`, `\"`, `\n`, `\r`, `\t`, and
60860
+ * three-digit octal for other control bytes — self-terminating, matching the
60861
+ * escape scanner), so every downstream consumer (length counting, C/asm
60862
+ * emission) treats it exactly like a hand-written literal.
60863
+ */
60864
+ function parse_embed_file(status) {
60865
+ const start = get_index(status);
60866
+ consume(status);
60867
+ const name = consume(status);
60868
+ if (name !== "embed_file") {
60869
+ add_error(status, `Unknown compiler directive '#${name}' — did you mean '#embed_file'?`, start);
60870
+ return new ValueNode(start, "\"\"");
60871
+ }
60872
+ if (!expect("(", status)) return new ValueNode(start, "\"\"");
60873
+ const path_token = status.tokens[status.i];
60874
+ if (!path_token || !path_token.value.startsWith("\"")) {
60875
+ add_error(status, "Expected a string literal path in #embed_file", start);
60876
+ return new ValueNode(start, "\"\"");
60877
+ }
60878
+ status.i += 1;
60879
+ const rel_path = path_token.value.slice(1, -1);
60880
+ if (!expect(")", status)) return new ValueNode(start, "\"\"");
60881
+ if (!rel_path) {
60882
+ add_error(status, "#embed_file path is empty", start);
60883
+ return new ValueNode(start, "\"\"");
60884
+ }
60885
+ const base = status.embed_root ?? process.cwd();
60886
+ const full = path.isAbsolute(rel_path) ? rel_path : path.join(base, rel_path);
60887
+ let content;
60888
+ try {
60889
+ content = fs.readFileSync(full, "utf8");
60890
+ } catch {
60891
+ add_error(status, `#embed_file could not read '${rel_path}' (resolved '${full}')`, start);
60892
+ return new ValueNode(start, "\"\"");
60893
+ }
60894
+ return new ValueNode(start, `"${escape_embedded(content)}"`);
60895
+ }
60896
+ /** Encode file contents as a source-level string literal body. */
60897
+ function escape_embedded(content) {
60898
+ let out = "";
60899
+ for (const ch of content) {
60900
+ const code = ch.charCodeAt(0);
60901
+ if (ch === "\\") out += "\\\\";
60902
+ else if (ch === "\"") out += "\\\"";
60903
+ else if (ch === "\n") out += "\\n";
60904
+ else if (ch === "\r") out += "\\r";
60905
+ else if (ch === " ") out += "\\t";
60906
+ else if (code < 32 || code === 127) out += `\\${code.toString(8).padStart(3, "0")}`;
60907
+ else out += ch;
60908
+ }
60909
+ return out;
60910
+ }
60911
+ //#endregion
60584
60912
  //#region ../src/parse/parse_expression.ts
60585
60913
  function restructure_op(start, current_op, current_prec, current_node, expression) {
60586
60914
  if (current_prec <= operator_precedence(expression.op)) {
@@ -60720,6 +61048,7 @@ function parse_primary(status, value) {
60720
61048
  expect(")", status);
60721
61049
  return node;
60722
61050
  }
61051
+ case "#": return parse_embed_file(status);
60723
61052
  case "if": return parse_if_else(status);
60724
61053
  case "match": return parse_match(status);
60725
61054
  case "switch": return parse_switch(status);
@@ -61862,7 +62191,8 @@ function parse(source, library, file_path, options) {
61862
62191
  qualified_paths: [],
61863
62192
  library,
61864
62193
  errors: [],
61865
- allow_user_raw: options?.allow_user_raw
62194
+ allow_user_raw: options?.allow_user_raw,
62195
+ embed_root: embed_root_for(file_path)
61866
62196
  };
61867
62197
  if (library) status.unsafe_boundary = user_source_length;
61868
62198
  parse_statement(status);
@@ -61939,6 +62269,23 @@ function mark_library_nodes(root, boundary) {
61939
62269
  walk(root);
61940
62270
  }
61941
62271
  /**
62272
+ * The base directory for `#embed_file` paths: the package root (the nearest
62273
+ * enclosing directory with a package.jsonc), falling back to the entry
62274
+ * file's folder. No file path — undefined, so `#embed_file` resolves
62275
+ * against the process CWD.
62276
+ */
62277
+ function embed_root_for(file_path) {
62278
+ if (!file_path) return void 0;
62279
+ let dir = path.dirname(path.resolve(file_path));
62280
+ for (let i = 0; i < 32; i++) {
62281
+ if (fs.existsSync(path.join(dir, "package.jsonc"))) return dir;
62282
+ const parent = path.dirname(dir);
62283
+ if (parent === dir) break;
62284
+ dir = parent;
62285
+ }
62286
+ return path.dirname(path.resolve(file_path));
62287
+ }
62288
+ /**
61942
62289
  * Append the library source that `source` needs, exactly as `parse` does.
61943
62290
  * Exported so tooling (e.g. the editor extension) can reproduce the combined
61944
62291
  * source that node offsets refer to.
@@ -62192,7 +62539,7 @@ function print_help() {
62192
62539
  " nomen check --in [file/folder] Parse and check only",
62193
62540
  " nomen format [--in folder] Reformat every .nm file",
62194
62541
  " nomen docs [--in file] Generate markdown documentation",
62195
- " nomen test [--in folder] Discover and run *.test.nm files",
62542
+ " nomen test [--in folder] Discover and run *.test.nm files (binary CWD = --in folder)",
62196
62543
  " nomen init <name> Scaffold a new project in ./<name>",
62197
62544
  " nomen lib-path Print the bundled System library path",
62198
62545
  "",
@@ -62782,7 +63129,7 @@ function extract_leaks(stdout) {
62782
63129
  * Compile one `*.test.nm` file (with the generated harness) and run it,
62783
63130
  * returning its parsed records and any crash info.
62784
63131
  */
62785
- function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime) {
63132
+ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime, release = false, cwd) {
62786
63133
  const start = performance.now();
62787
63134
  const source_text = fs.readFileSync(entry_path, "utf8");
62788
63135
  const harness = generate_harness(extract_test_functions(source_text), extract_bench_functions(source_text));
@@ -62809,7 +63156,8 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
62809
63156
  }
62810
63157
  const buildResult = build(parsed.root, {
62811
63158
  arch,
62812
- audit
63159
+ audit,
63160
+ optimize: release
62813
63161
  });
62814
63162
  if (buildResult.errors && buildResult.errors.length) {
62815
63163
  result.ok = false;
@@ -62844,11 +63192,19 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
62844
63192
  const outfile = path.join(buildDir, path.basename(entry_path, ".nm"));
62845
63193
  fs.writeFileSync(path.join(buildDir, "main.h"), buildResult.headers ?? "");
62846
63194
  fs.writeFileSync(codefile, buildResult.code);
63195
+ let companionfile;
63196
+ if (buildResult.companion) {
63197
+ const comp_ext = platform === "macos" || platform === "ios" ? ".m" : ".c";
63198
+ companionfile = path.join(buildDir, path.basename(entry_path, ".nm") + "_companion" + comp_ext);
63199
+ fs.writeFileSync(companionfile, buildResult.companion);
63200
+ }
62847
63201
  const link_args = [
62848
63202
  "-o",
62849
63203
  outfile,
62850
63204
  codefile
62851
63205
  ];
63206
+ if (companionfile) link_args.push(companionfile);
63207
+ if (release) link_args.unshift("-O2");
62852
63208
  if (audit_obj) link_args.push(audit_obj);
62853
63209
  if (platform === "macos" || platform === "ios") link_args.push("-framework", "CoreGraphics", "-framework", "Foundation", "-framework", "AppKit", "-lobjc");
62854
63210
  try {
@@ -62879,7 +63235,8 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
62879
63235
  "pipe"
62880
63236
  ],
62881
63237
  timeout: 3e4,
62882
- maxBuffer: 16777216
63238
+ maxBuffer: 16777216,
63239
+ cwd: cwd ?? process.cwd()
62883
63240
  });
62884
63241
  } catch (err) {
62885
63242
  runStdout = err.stdout ? err.stdout.toString() : "";
@@ -62961,9 +63318,10 @@ function runTests(root, options = {}) {
62961
63318
  const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
62962
63319
  console.log(`Found ${files.length} test file(s)\n`);
62963
63320
  const startTime = performance.now();
63321
+ const binary_cwd = path.resolve(root);
62964
63322
  const results = [];
62965
63323
  for (const file of files) {
62966
- const result = run_test_file(file, lib, arch, options.audit, options.audit_runtime);
63324
+ const result = run_test_file(file, lib, arch, options.audit, options.audit_runtime, options.release, binary_cwd);
62967
63325
  report_file(result);
62968
63326
  results.push(result);
62969
63327
  }
@@ -63071,7 +63429,8 @@ try {
63071
63429
  arch: args.arch ?? "aarch64",
63072
63430
  filter,
63073
63431
  audit: args.audit,
63074
- audit_runtime: args.audit_runtime
63432
+ audit_runtime: args.audit_runtime,
63433
+ release: args.release
63075
63434
  });
63076
63435
  process.exit(ok ? 0 : 1);
63077
63436
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nomen-lang",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "The CLI for the Nomen programming language.",
5
5
  "keywords": [],
6
6
  "license": "ISC",
package/src/args.ts CHANGED
@@ -160,7 +160,7 @@ export function print_help(): void {
160
160
  " nomen check --in [file/folder] Parse and check only",
161
161
  " nomen format [--in folder] Reformat every .nm file",
162
162
  " nomen docs [--in file] Generate markdown documentation",
163
- " nomen test [--in folder] Discover and run *.test.nm files",
163
+ " nomen test [--in folder] Discover and run *.test.nm files (binary CWD = --in folder)",
164
164
  " nomen init <name> Scaffold a new project in ./<name>",
165
165
  " nomen lib-path Print the bundled System library path",
166
166
  "",
package/src/index.ts CHANGED
@@ -109,6 +109,7 @@ try {
109
109
  filter,
110
110
  audit: args.audit,
111
111
  audit_runtime: args.audit_runtime,
112
+ release: args.release,
112
113
  });
113
114
  process.exit(ok ? 0 : 1);
114
115
  }
package/src/test.ts CHANGED
@@ -278,6 +278,8 @@ export function run_test_file(
278
278
  arch: string,
279
279
  audit = false,
280
280
  audit_runtime?: string,
281
+ release = false,
282
+ cwd?: string,
281
283
  ): TestFileResult {
282
284
  const start = performance.now();
283
285
  const source_text = fs.readFileSync(entry_path, "utf8");
@@ -318,6 +320,7 @@ export function run_test_file(
318
320
  const buildResult = build(parsed.root, {
319
321
  arch: arch as "aarch64" | "c",
320
322
  audit,
323
+ optimize: release,
321
324
  });
322
325
  if (buildResult.errors && buildResult.errors.length) {
323
326
  result.ok = false;
@@ -363,11 +366,28 @@ export function run_test_file(
363
366
  fs.writeFileSync(path.join(buildDir, "main.h"), buildResult.headers ?? "");
364
367
  fs.writeFileSync(codefile, buildResult.code);
365
368
 
369
+ // Companion C (raw-block functions + the pool/fiber/file-scope C infra):
370
+ // `run`/`build` compile and link it next to the main TU. The aarch64 .s
371
+ // references its symbols via `bl` (spawn machinery), so dropping it makes
372
+ // companion-bearing test files fail to link. Same .m/.c rule as the
373
+ // run/build command's companion emission.
374
+ let companionfile: string | undefined;
375
+ if (buildResult.companion) {
376
+ const comp_ext = platform === "macos" || platform === "ios" ? ".m" : ".c";
377
+ companionfile = path.join(buildDir, path.basename(entry_path, ".nm") + "_companion" + comp_ext);
378
+ fs.writeFileSync(companionfile, buildResult.companion);
379
+ }
380
+
366
381
  // Link the harness binary. Frameworks are passed on Apple platforms for
367
382
  // the same reason as the run/build command: the joined source may reference
368
383
  // ObjC runtime symbols even when the test body doesn't, and unused
369
- // frameworks cost nothing at link time.
384
+ // frameworks cost nothing at link time. Release mode compiles the C (and
385
+ // companion C) with -O2, matching the run/build command; the aarch64 .s is
386
+ // assembled verbatim — its optimizations are the passes enabled via
387
+ // `optimize` above.
370
388
  const link_args = ["-o", outfile, codefile];
389
+ if (companionfile) link_args.push(companionfile);
390
+ if (release) link_args.unshift("-O2");
371
391
  if (audit_obj) link_args.push(audit_obj);
372
392
  if (platform === "macos" || platform === "ios") {
373
393
  link_args.push(
@@ -396,7 +416,11 @@ export function run_test_file(
396
416
 
397
417
  // Run the binary and collect the records it streams over stdout. A
398
418
  // non-zero exit (crash, abort) still surfaces whatever records were
399
- // emitted before the crash; a timeout is treated as a crash.
419
+ // emitted before the crash; a timeout is treated as a crash. The
420
+ // documented CWD contract: the binary runs in the `--in` root (the
421
+ // folder `nomen test` scanned for *.test.nm files), so fixture paths in
422
+ // tests resolve against that folder regardless of where the CLI was
423
+ // invoked from.
400
424
  let runStdout = "";
401
425
  let crashed: string | undefined;
402
426
  try {
@@ -405,6 +429,7 @@ export function run_test_file(
405
429
  stdio: ["ignore", "pipe", "pipe"],
406
430
  timeout: 30_000,
407
431
  maxBuffer: 16 * 1024 * 1024,
432
+ cwd: cwd ?? process.cwd(),
408
433
  });
409
434
  } catch (err: any) {
410
435
  runStdout = err.stdout ? err.stdout.toString() : "";
@@ -519,6 +544,7 @@ export interface RunTestsOptions {
519
544
  filter?: RegExp;
520
545
  audit?: boolean;
521
546
  audit_runtime?: string;
547
+ release?: boolean;
522
548
  }
523
549
 
524
550
  /**
@@ -547,10 +573,21 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
547
573
  console.log(`Found ${files.length} test file(s)\n`);
548
574
 
549
575
  const startTime = performance.now();
576
+ // The CWD contract: test binaries run in the --in root, so relative
577
+ // fixture paths resolve there no matter where the CLI was invoked from.
578
+ const binary_cwd = path.resolve(root);
550
579
 
551
580
  const results: TestFileResult[] = [];
552
581
  for (const file of files) {
553
- const result = run_test_file(file, lib, arch, options.audit, options.audit_runtime);
582
+ const result = run_test_file(
583
+ file,
584
+ lib,
585
+ arch,
586
+ options.audit,
587
+ options.audit_runtime,
588
+ options.release,
589
+ binary_cwd,
590
+ );
554
591
  report_file(result);
555
592
  results.push(result);
556
593
  }