nomen-lang 0.6.2 → 0.6.3

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
@@ -6538,6 +6538,18 @@ var FunctionCallNode = class extends BaseNode {
6538
6538
  ref_param_indices;
6539
6539
  move_param_indices;
6540
6540
  /**
6541
+ * Indices of arguments bound to a `move T` parameter where T is a
6542
+ * pass-by-value owning struct on a registered free function: ownership
6543
+ * TRANSFERS at the boundary (the callee seeds the param's string-field
6544
+ * records and frees them at its exit), so the caller drops its records
6545
+ * instead of freeing them. The plain `move_param_indices` convention
6546
+ * (callee deep-copies, caller frees the originals) still governs
6547
+ * container stores and every non-transfer shape. Stamped by
6548
+ * check_function_call alongside owned_value_param_indices.
6549
+ */
6550
+ move_owned_param_indices;
6551
+ owned_value_param_indices;
6552
+ /**
6541
6553
  * Indices of arguments whose corresponding callee parameter is a nullable
6542
6554
  * struct value type (`T?` where T is a non-class struct). Populated during
6543
6555
  * the check pass when the callee's signature is resolved. The build uses
@@ -6958,6 +6970,12 @@ var FunctionNode = class extends BaseNode {
6958
6970
  * are dual-natured: called under their own name AND passed as values).
6959
6971
  */
6960
6972
  is_closure;
6973
+ /** The function's address escapes into a func-typed value (bound to a
6974
+ * `var func …` variable/field, passed as a func value, …) — so it may be
6975
+ * invoked through a pointer with a BORROWED argument: pass-by-value
6976
+ * normalization (owned-param record seeding / call-site shell
6977
+ * materialization) must not apply. Set during check. */
6978
+ address_escaped;
6961
6979
  /**
6962
6980
  * Captured outer locals (CLOSURE.md Phase 2). One entry per variable the
6963
6981
  * lambda body references from an enclosing function; the backends emit an
@@ -7366,6 +7384,16 @@ var DeclarationNode = class extends BaseNode {
7366
7384
  * paths strdup the initializer and free the temp's copy at scope exit.
7367
7385
  * Set at build time by hoist_field_overrides. */
7368
7386
  force_owned_string;
7387
+ /** True for an OWNED value-struct PARAMETER whose string fields the
7388
+ * callee frees at scope exit: the field accesses (and the record-free
7389
+ * emitted by free_scoped_declarations) go through the param POINTER
7390
+ * (`p->field`) instead of value syntax (`p.field`). Set at build time
7391
+ * when the function builder seeds pass-by-value struct params. */
7392
+ string_fields_via_pointer;
7393
+ /** True alongside string_fields_via_pointer when the caller materialized
7394
+ * a heap shell for the argument (pass-by-value): the callee frees the
7395
+ * shell itself at scope exit (`free(p)`) after freeing the fields. */
7396
+ owned_heap_shell;
7369
7397
  constructor(start, visibility, declaration, name, type, value) {
7370
7398
  super("declare", start);
7371
7399
  this.visibility = visibility;
@@ -10913,6 +10941,117 @@ function array_struct_name(type, status) {
10913
10941
  return status.structs.find((s) => s.name === mono && !s.is_generic) ? mono : void 0;
10914
10942
  }
10915
10943
  //#endregion
10944
+ //#region ../src/build_common/destroy_analysis.ts
10945
+ /**
10946
+ * Whether any field of the struct is an enum-with-data value owning heap
10947
+ * payloads (a string payload — always heap, case construction strdups it —
10948
+ * or a class/trait reference payload — construction transfers ownership).
10949
+ * Such a field must be reclaimed when the struct dies.
10950
+ */
10951
+ function struct_has_owning_enum_field(struct, table) {
10952
+ for (const field of struct.fields) {
10953
+ if (field.type.is_ref) continue;
10954
+ const en = table.enums?.find((e) => e.name === field.type.name);
10955
+ if (!en?.has_associated_data) continue;
10956
+ for (const c of en.cases) for (const p of c.params) {
10957
+ if (p.type.name === "string" && !p.type.is_array) return true;
10958
+ if (table.traits?.some((t) => t.name === p.type.name)) return true;
10959
+ if (table.structs.find((s) => s.name === p.type.name)?.is_class) return true;
10960
+ }
10961
+ }
10962
+ return false;
10963
+ }
10964
+ /**
10965
+ * Whether a struct declares its own `#destroy` (user side effects /
10966
+ * resource release).
10967
+ */
10968
+ function has_destroy(struct) {
10969
+ return !!struct.functions.find((f) => f.name === "#destroy");
10970
+ }
10971
+ /**
10972
+ * Whether any field (recursively) requires destroy handling — a class
10973
+ * field (heap instance to reclaim) or a nested struct with its own
10974
+ * `#destroy` (or nested fields that do). Deliberately excludes `string`
10975
+ * fields: a struct local whose only owning fields are strings is NOT
10976
+ * auto-destroyed at scope exit (the strings may be raw rodata arg
10977
+ * pointers, not heap) — see `struct_needs_auto_destroy` for the broader
10978
+ * form used to decide whether a `<Struct>_destroy` function is generated.
10979
+ */
10980
+ function has_struct_fields_with_destroy(struct, table) {
10981
+ for (const field of struct.fields) {
10982
+ if (field.type.is_ref) continue;
10983
+ const field_struct = resolve_struct_type(field.type, table);
10984
+ if (!field_struct) continue;
10985
+ if (field_struct.is_class) return true;
10986
+ if (has_destroy(field_struct)) return true;
10987
+ if (has_struct_fields_with_destroy(field_struct, table)) return true;
10988
+ }
10989
+ return false;
10990
+ }
10991
+ /**
10992
+ * Whether a struct (or any embedded struct field, recursively) needs a
10993
+ * destroy call at scope exit — it has a `#destroy`, a class-typed field,
10994
+ * or a nested struct field that itself needs destroying. This does NOT
10995
+ * count `string` fields (see `has_struct_fields_with_destroy`).
10996
+ */
10997
+ function struct_needs_destroy(struct, table) {
10998
+ return has_destroy(struct) || has_struct_fields_with_destroy(struct, table) || struct_has_owning_enum_field(struct, table);
10999
+ }
11000
+ /**
11001
+ * Whether a struct needs an auto-generated `<Struct>_destroy` function.
11002
+ * Broader than `struct_needs_destroy`: also returns true for structs with
11003
+ * `string` fields, because a Buffer<T> for such a struct deep-copies the
11004
+ * strings into slots (strdup on store) and the per-element destroy must
11005
+ * free them. The generated destroy frees each string field; it is called
11006
+ * from Buffer's specialized #destroy / replace_T, NOT from struct local
11007
+ * scope exit (`struct_needs_destroy` governs that path and excludes
11008
+ * strings).
11009
+ */
11010
+ function struct_needs_auto_destroy(struct, table) {
11011
+ if (struct_needs_destroy(struct, table)) return true;
11012
+ if (struct_has_owning_enum_field(struct, table)) return true;
11013
+ for (const field of struct.fields) {
11014
+ if (field.type.is_ref) continue;
11015
+ if (field.type.is_view) continue;
11016
+ if (field.type.name === "string" && !field.type.is_array) return true;
11017
+ const field_struct = resolve_struct_type(field.type, table);
11018
+ if (field_struct && !field_struct.is_class && struct_needs_auto_destroy(field_struct, table)) return true;
11019
+ }
11020
+ return false;
11021
+ }
11022
+ //#endregion
11023
+ //#region ../src/build_common/has_string_fields.ts
11024
+ /**
11025
+ * Whether any field (recursively, through nested owning value structs)
11026
+ * is an owned `string`. Shared by BOTH backends' owning-Buffer element
11027
+ * specialization (the slot deep-copies strdup'd strings, so a
11028
+ * string-field element is owning even when `struct_needs_destroy`
11029
+ * ignores string-only locals) — the C comment in
11030
+ * owning_buffer_specialize records the divergence this once caused.
11031
+ * `ref` and `view T` fields are skipped: a ref aliases the caller's
11032
+ * storage and a view's byte copy owns nothing.
11033
+ */
11034
+ function has_string_fields(node, status) {
11035
+ for (const field of node.fields) {
11036
+ if (field.type.is_ref) continue;
11037
+ if (field.type.is_view) continue;
11038
+ if (field.type.name === "string" && !field.type.is_array) return true;
11039
+ const field_struct = status.structs.find((s) => s.name === field.type.name && !s.is_simple_type && !s.is_generic);
11040
+ if (field_struct && !field_struct.is_class && has_string_fields(field_struct, status)) return true;
11041
+ }
11042
+ return false;
11043
+ }
11044
+ /**
11045
+ * The DIRECT `string` fields of a value struct (nested struct fields are not
11046
+ * descended into — nested string fields are outside the per-field ownership
11047
+ * tracking everywhere else too). Used by the return-boundary normalization:
11048
+ * the returned struct's unrecorded fields are strdup'd so the value the
11049
+ * caller receives is uniformly heap-owned.
11050
+ */
11051
+ function direct_string_fields(node) {
11052
+ return node.fields.filter((f) => !f.type.is_ref && !f.type.is_view && !f.type.is_array && f.type.name === "string");
11053
+ }
11054
+ //#endregion
10916
11055
  //#region ../src/build_common/scan_force_heap_strings.ts
10917
11056
  function scan_force_heap_strings(statements, structs) {
10918
11057
  const result = /* @__PURE__ */ new Set();
@@ -13391,85 +13530,6 @@ function emit_strdup(status) {
13391
13530
  emit_asm(status, `ldr x1, [sp], #16\n`);
13392
13531
  }
13393
13532
  //#endregion
13394
- //#region ../src/build_common/destroy_analysis.ts
13395
- /**
13396
- * Whether any field of the struct is an enum-with-data value owning heap
13397
- * payloads (a string payload — always heap, case construction strdups it —
13398
- * or a class/trait reference payload — construction transfers ownership).
13399
- * Such a field must be reclaimed when the struct dies.
13400
- */
13401
- function struct_has_owning_enum_field(struct, table) {
13402
- for (const field of struct.fields) {
13403
- if (field.type.is_ref) continue;
13404
- const en = table.enums?.find((e) => e.name === field.type.name);
13405
- if (!en?.has_associated_data) continue;
13406
- for (const c of en.cases) for (const p of c.params) {
13407
- if (p.type.name === "string" && !p.type.is_array) return true;
13408
- if (table.traits?.some((t) => t.name === p.type.name)) return true;
13409
- if (table.structs.find((s) => s.name === p.type.name)?.is_class) return true;
13410
- }
13411
- }
13412
- return false;
13413
- }
13414
- /**
13415
- * Whether a struct declares its own `#destroy` (user side effects /
13416
- * resource release).
13417
- */
13418
- function has_destroy(struct) {
13419
- return !!struct.functions.find((f) => f.name === "#destroy");
13420
- }
13421
- /**
13422
- * Whether any field (recursively) requires destroy handling — a class
13423
- * field (heap instance to reclaim) or a nested struct with its own
13424
- * `#destroy` (or nested fields that do). Deliberately excludes `string`
13425
- * fields: a struct local whose only owning fields are strings is NOT
13426
- * auto-destroyed at scope exit (the strings may be raw rodata arg
13427
- * pointers, not heap) — see `struct_needs_auto_destroy` for the broader
13428
- * form used to decide whether a `<Struct>_destroy` function is generated.
13429
- */
13430
- function has_struct_fields_with_destroy(struct, table) {
13431
- for (const field of struct.fields) {
13432
- if (field.type.is_ref) continue;
13433
- const field_struct = resolve_struct_type(field.type, table);
13434
- if (!field_struct) continue;
13435
- if (field_struct.is_class) return true;
13436
- if (has_destroy(field_struct)) return true;
13437
- if (has_struct_fields_with_destroy(field_struct, table)) return true;
13438
- }
13439
- return false;
13440
- }
13441
- /**
13442
- * Whether a struct (or any embedded struct field, recursively) needs a
13443
- * destroy call at scope exit — it has a `#destroy`, a class-typed field,
13444
- * or a nested struct field that itself needs destroying. This does NOT
13445
- * count `string` fields (see `has_struct_fields_with_destroy`).
13446
- */
13447
- function struct_needs_destroy(struct, table) {
13448
- return has_destroy(struct) || has_struct_fields_with_destroy(struct, table) || struct_has_owning_enum_field(struct, table);
13449
- }
13450
- /**
13451
- * Whether a struct needs an auto-generated `<Struct>_destroy` function.
13452
- * Broader than `struct_needs_destroy`: also returns true for structs with
13453
- * `string` fields, because a Buffer<T> for such a struct deep-copies the
13454
- * strings into slots (strdup on store) and the per-element destroy must
13455
- * free them. The generated destroy frees each string field; it is called
13456
- * from Buffer's specialized #destroy / replace_T, NOT from struct local
13457
- * scope exit (`struct_needs_destroy` governs that path and excludes
13458
- * strings).
13459
- */
13460
- function struct_needs_auto_destroy(struct, table) {
13461
- if (struct_needs_destroy(struct, table)) return true;
13462
- if (struct_has_owning_enum_field(struct, table)) return true;
13463
- for (const field of struct.fields) {
13464
- if (field.type.is_ref) continue;
13465
- if (field.type.is_view) continue;
13466
- if (field.type.name === "string" && !field.type.is_array) return true;
13467
- const field_struct = resolve_struct_type(field.type, table);
13468
- if (field_struct && !field_struct.is_class && struct_needs_auto_destroy(field_struct, table)) return true;
13469
- }
13470
- return false;
13471
- }
13472
- //#endregion
13473
13533
  //#region ../src/build_common/temp_anchor_consolidation.ts
13474
13534
  /**
13475
13535
  * Shared OWNERSHIP decision for hoisted call-temporary consolidation.
@@ -13504,6 +13564,144 @@ function superseded_param_temp_names(table, call_node, result_type_name) {
13504
13564
  return names;
13505
13565
  }
13506
13566
  //#endregion
13567
+ //#region ../src/build_aarch64/utils/string_pair.ts
13568
+ /**
13569
+ * Load the string variable `name` into the consecutive register pair
13570
+ * (base, base+1) — default x0/x1. Handles the plain slot case; callers
13571
+ * that have register-allocated or param forms handle theirs first.
13572
+ */
13573
+ function emit_string_pair_load(status, name, base = "x0") {
13574
+ const offset = status.stack_offsets?.get(name);
13575
+ if (offset === void 0) return false;
13576
+ const w = base.startsWith("x") ? base : "x0";
13577
+ const n = parseInt(w.substring(1), 10);
13578
+ if (offset + 8 > 504) {
13579
+ emit_asm(status, `ldr ${w}, [x29, #${offset}]\n`);
13580
+ emit_asm(status, `ldr x${n + 1}, [x29, #${offset + 8}]\n`);
13581
+ return true;
13582
+ }
13583
+ emit_asm(status, `ldp ${w}, x${n + 1}, [x29, #${offset}]\n`);
13584
+ return true;
13585
+ }
13586
+ /** Load the pair at [addr_reg, #offset] into (base, base+1). */
13587
+ function emit_string_pair_load_at(status, addr_reg, offset, base = "x0") {
13588
+ const w = base.startsWith("x") ? base : "x0";
13589
+ emit_asm(status, `ldp ${w}, x${parseInt(w.substring(1), 10) + 1}, [${addr_reg}, #${offset}]\n`);
13590
+ }
13591
+ /**
13592
+ * strdup the fat string in (x0=ptr, x1=len): preserve the len half across
13593
+ * the call and leave the owned copy's pair in (x0, x1). The caller keeps
13594
+ * ownership of the original.
13595
+ */
13596
+ function emit_strdup_string(status) {
13597
+ emit_asm(status, `str x1, [sp, #-16]!\n`);
13598
+ emit_asm(status, status.audit ? `bl _nomen_strdup_wrap\n` : `bl _strdup\n`);
13599
+ emit_asm(status, `ldr x1, [sp], #16\n`);
13600
+ }
13601
+ /**
13602
+ * Pair load/store at [x29, #offset] with an arbitrary register pair —
13603
+ * splits into two single-word accesses beyond the ldp/stp +504 range.
13604
+ */
13605
+ function emit_pair_load_x29(status, offset, a = "x0", b = "x1") {
13606
+ if (offset + 8 > 504) {
13607
+ emit_asm(status, `ldr ${a}, [x29, #${offset}]\n`);
13608
+ emit_asm(status, `ldr ${b}, [x29, #${offset + 8}]\n`);
13609
+ return;
13610
+ }
13611
+ emit_asm(status, `ldp ${a}, ${b}, [x29, #${offset}]\n`);
13612
+ }
13613
+ function emit_pair_store_x29(status, offset, a = "x0", b = "x1") {
13614
+ if (offset + 8 > 504) {
13615
+ emit_asm(status, `str ${a}, [x29, #${offset}]\n`);
13616
+ emit_asm(status, `str ${b}, [x29, #${offset + 8}]\n`);
13617
+ return;
13618
+ }
13619
+ emit_asm(status, `stp ${a}, ${b}, [x29, #${offset}]\n`);
13620
+ }
13621
+ /**
13622
+ * Owning specializations for Array<string>'s raw T-generic bodies
13623
+ * (`with` / `set`), replacing the shared-pointer copies with per-slot deep
13624
+ * copies — the aarch64 analog of Array.nm's `#if T_NEEDS_STRDUP` C branch.
13625
+ *
13626
+ * Conventions (matching Array.nm raw bodies):
13627
+ * - `with` (static): x0/x1 = value pair, x2 = count. Returns x0 = heap
13628
+ * buffer ([ptr] = length, elements at +8..).
13629
+ * - `set` (method): x19 = first element, x1 = index, x2/x3 = value pair.
13630
+ * Bounds are guaranteed by the Nomen-level constraints.
13631
+ */
13632
+ function emit_owning_array_string_specialize(func_name, status, self_reg = "x19") {
13633
+ if (func_name === "with") {
13634
+ emit_asm(status, `stp x19, x20, [sp, #-16]!\n`);
13635
+ emit_asm(status, `stp x21, x22, [sp, #-16]!\n`);
13636
+ emit_asm(status, `stp x24, x25, [sp, #-16]!\n`);
13637
+ emit_asm(status, `mov x19, x0\n`);
13638
+ emit_asm(status, `mov x20, x1\n`);
13639
+ emit_asm(status, `mov x21, x2\n`);
13640
+ emit_asm(status, `add x0, x21, #1\n`);
13641
+ emit_asm(status, `lsl x0, x0, #4\n`);
13642
+ emit_malloc(status);
13643
+ emit_asm(status, `mov x22, x0\n`);
13644
+ emit_asm(status, `str x21, [x22]\n`);
13645
+ emit_asm(status, `mov x23, #8\n`);
13646
+ emit_asm(status, `mov x24, #0\n`);
13647
+ emit_asm(status, `.Larr_str_with_loop:\n`);
13648
+ emit_asm(status, `cmp x24, x21\n`);
13649
+ emit_asm(status, `b.ge .Larr_str_with_done\n`);
13650
+ emit_asm(status, `mov x0, x19\n`);
13651
+ emit_strdup(status);
13652
+ emit_asm(status, `add x25, x22, x23\n`);
13653
+ emit_asm(status, `stp x0, x20, [x25]\n`);
13654
+ emit_asm(status, `add x23, x23, #16\n`);
13655
+ emit_asm(status, `add x24, x24, #1\n`);
13656
+ emit_asm(status, `b .Larr_str_with_loop\n`);
13657
+ emit_asm(status, `.Larr_str_with_done:\n`);
13658
+ emit_asm(status, `mov x0, x22\n`);
13659
+ emit_asm(status, `ldp x24, x25, [sp], #16\n`);
13660
+ emit_asm(status, `ldp x21, x22, [sp], #16\n`);
13661
+ emit_asm(status, `ldp x19, x20, [sp], #16\n`);
13662
+ return true;
13663
+ }
13664
+ if (func_name === "at" || func_name === "first" || func_name === "at_end") {
13665
+ emit_asm(status, `stp x20, x21, [sp, #-16]!\n`);
13666
+ if (func_name === "at") {
13667
+ emit_asm(status, `lsl x9, x1, #4\n`);
13668
+ emit_asm(status, `add x9, ${self_reg}, x9\n`);
13669
+ } else if (func_name === "at_end") {
13670
+ emit_asm(status, `ldr x9, [${self_reg}, #-8]\n`);
13671
+ emit_asm(status, `sub x9, x9, #1\n`);
13672
+ emit_asm(status, `lsl x9, x9, #4\n`);
13673
+ emit_asm(status, `add x9, ${self_reg}, x9\n`);
13674
+ } else emit_asm(status, `add x9, ${self_reg}, #0\n`);
13675
+ emit_asm(status, `ldp x0, x1, [x9]\n`);
13676
+ emit_asm(status, `ldp x20, x21, [sp], #16\n`);
13677
+ return true;
13678
+ }
13679
+ if (func_name === "set") {
13680
+ emit_asm(status, `stp x20, x21, [sp, #-16]!\n`);
13681
+ emit_asm(status, `lsl x9, x1, #4\n`);
13682
+ emit_asm(status, `add x9, ${self_reg}, x9\n`);
13683
+ emit_asm(status, `ldr x0, [x9]\n`);
13684
+ emit_asm(status, `mov x20, x2\n`);
13685
+ emit_asm(status, `mov x21, x3\n`);
13686
+ emit_free(status);
13687
+ emit_asm(status, `mov x0, x20\n`);
13688
+ emit_strdup(status);
13689
+ emit_asm(status, `str x0, [x9]\n`);
13690
+ emit_asm(status, `str x21, [x9, #8]\n`);
13691
+ emit_asm(status, `ldp x20, x21, [sp], #16\n`);
13692
+ return true;
13693
+ }
13694
+ return false;
13695
+ }
13696
+ /** Pair store to [base_reg, #offset] with stur fallback (misaligned/far). */
13697
+ function emit_pair_store_to(status, base_reg, offset, a, b) {
13698
+ if (offset % 8 === 0 && offset + 8 <= 504 && offset >= 0) emit_asm(status, `stp ${a}, ${b}, [${base_reg}, #${offset}]\n`);
13699
+ else {
13700
+ emit_asm(status, `stur ${a}, [${base_reg}, #${offset}]\n`);
13701
+ emit_asm(status, `stur ${b}, [${base_reg}, #${offset + 8}]\n`);
13702
+ }
13703
+ }
13704
+ //#endregion
13507
13705
  //#region ../src/build_aarch64/utils/auto_destroy.ts
13508
13706
  /**
13509
13707
  * Whether a declaration's initializer is a non-`move` FIELD ACCESS — a shallow
@@ -14346,6 +14544,27 @@ function mark_moved_if_struct(value, status, opts) {
14346
14544
  status.moved.add(var_name);
14347
14545
  }
14348
14546
  }
14547
+ /**
14548
+ * strdup the UNRECORDED direct string fields of a value struct whose base is
14549
+ * in `base_reg` (+ base_offset) — the return-boundary normalization: the
14550
+ * sret byte-copy transfers the RECORDED (heap) fields raw, but unrecorded
14551
+ * fields may hold rodata/borrowed bytes the caller cannot free, so they are
14552
+ * deep-copied here and the returned struct is uniformly heap-owned.
14553
+ */
14554
+ function emit_string_field_strdups_at(status, struct_name, base_reg, base_offset = 0, skip_fields) {
14555
+ const struct = status.structs.find((s) => s.name === struct_name && !s.is_simple_type && !s.is_class);
14556
+ if (!struct) return;
14557
+ for (const field of direct_string_fields(struct)) {
14558
+ if (skip_fields?.has(field.name)) continue;
14559
+ const off = base_offset + get_field_offset(struct_name, field.name, status);
14560
+ emit_asm(status, `str ${base_reg}, [sp, #-16]!\n`);
14561
+ emit_string_pair_load_at(status, base_reg, off);
14562
+ emit_strdup_string(status);
14563
+ emit_asm(status, `mov x13, x0\n`);
14564
+ emit_asm(status, `ldr ${base_reg}, [sp], #16\n`);
14565
+ emit_pair_store_to(status, base_reg, off, "x13", "x1");
14566
+ }
14567
+ }
14349
14568
  //#endregion
14350
14569
  //#region ../src/build_aarch64/utils/closure_a64.ts
14351
14570
  /**
@@ -16823,28 +17042,6 @@ function value_number_loops(body, ast_list, status, mutate_ast) {
16823
17042
  };
16824
17043
  }
16825
17044
  //#endregion
16826
- //#region ../src/build_common/has_string_fields.ts
16827
- /**
16828
- * Whether any field (recursively, through nested owning value structs)
16829
- * is an owned `string`. Shared by BOTH backends' owning-Buffer element
16830
- * specialization (the slot deep-copies strdup'd strings, so a
16831
- * string-field element is owning even when `struct_needs_destroy`
16832
- * ignores string-only locals) — the C comment in
16833
- * owning_buffer_specialize records the divergence this once caused.
16834
- * `ref` and `view T` fields are skipped: a ref aliases the caller's
16835
- * storage and a view's byte copy owns nothing.
16836
- */
16837
- function has_string_fields(node, status) {
16838
- for (const field of node.fields) {
16839
- if (field.type.is_ref) continue;
16840
- if (field.type.is_view) continue;
16841
- if (field.type.name === "string" && !field.type.is_array) return true;
16842
- const field_struct = status.structs.find((s) => s.name === field.type.name && !s.is_simple_type && !s.is_generic);
16843
- if (field_struct && !field_struct.is_class && has_string_fields(field_struct, status)) return true;
16844
- }
16845
- return false;
16846
- }
16847
- //#endregion
16848
17045
  //#region ../src/build_aarch64/utils/owning_buffer_specialize.ts
16849
17046
  /**
16850
17047
  * Detect whether a monomorphized struct is a `Buffer_<T>` whose element type
@@ -19458,6 +19655,16 @@ function build_function_node$1(node, status) {
19458
19655
  }
19459
19656
  param_idx++;
19460
19657
  if (param.declaration === "var") status.function_param_vars.add(param.name);
19658
+ const is_free_function = (!node.scope || node.scope.node_type === "root" || node.scope.node_type === "func") && !node.params.some((p) => p.is_self_param);
19659
+ const owned_value_param = !param.is_self_param && !param.is_variadic && !param.type.is_ref && !param.type.is_nullable && !param.type.is_view && !param.type.is_array && param.type.name ? status.structs.find((st) => st.name === param.type.name && !st.is_simple_type && !st.is_class) : void 0;
19660
+ if (is_free_function && !node.address_escaped && owned_value_param && direct_string_fields(owned_value_param).length > 0 && !(owned_value_param.traits ?? []).length && !struct_needs_destroy(owned_value_param, status) && (status.normalized_struct_returners?.has(node.name) || status.normalized_struct_returners?.has(label_name)) && !status.func_address_taken?.has(node.name) && !status.func_address_taken?.has(label_name)) {
19661
+ status.function_param_vars.add(param.name);
19662
+ const param_decl = new DeclarationNode(param.start, "private", "var", param.name, param.type);
19663
+ param_decl.string_fields_via_pointer = true;
19664
+ status.scoped_declarations.push(param_decl);
19665
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
19666
+ for (const field of direct_string_fields(owned_value_param)) status.heap_string_fields.add(`${param.name}.${field.name}`);
19667
+ }
19461
19668
  if (param.type.is_array) {
19462
19669
  if (!param.is_variadic ? array_struct_name(param.type, status) : void 0) {
19463
19670
  if (!status.heap_array_vars) status.heap_array_vars = /* @__PURE__ */ new Set();
@@ -19669,144 +19876,6 @@ function build_function_node$1(node, status) {
19669
19876
  status.function_return_type = void 0;
19670
19877
  }
19671
19878
  //#endregion
19672
- //#region ../src/build_aarch64/utils/string_pair.ts
19673
- /**
19674
- * Load the string variable `name` into the consecutive register pair
19675
- * (base, base+1) — default x0/x1. Handles the plain slot case; callers
19676
- * that have register-allocated or param forms handle theirs first.
19677
- */
19678
- function emit_string_pair_load(status, name, base = "x0") {
19679
- const offset = status.stack_offsets?.get(name);
19680
- if (offset === void 0) return false;
19681
- const w = base.startsWith("x") ? base : "x0";
19682
- const n = parseInt(w.substring(1), 10);
19683
- if (offset + 8 > 504) {
19684
- emit_asm(status, `ldr ${w}, [x29, #${offset}]\n`);
19685
- emit_asm(status, `ldr x${n + 1}, [x29, #${offset + 8}]\n`);
19686
- return true;
19687
- }
19688
- emit_asm(status, `ldp ${w}, x${n + 1}, [x29, #${offset}]\n`);
19689
- return true;
19690
- }
19691
- /** Load the pair at [addr_reg, #offset] into (base, base+1). */
19692
- function emit_string_pair_load_at(status, addr_reg, offset, base = "x0") {
19693
- const w = base.startsWith("x") ? base : "x0";
19694
- emit_asm(status, `ldp ${w}, x${parseInt(w.substring(1), 10) + 1}, [${addr_reg}, #${offset}]\n`);
19695
- }
19696
- /**
19697
- * strdup the fat string in (x0=ptr, x1=len): preserve the len half across
19698
- * the call and leave the owned copy's pair in (x0, x1). The caller keeps
19699
- * ownership of the original.
19700
- */
19701
- function emit_strdup_string(status) {
19702
- emit_asm(status, `str x1, [sp, #-16]!\n`);
19703
- emit_asm(status, status.audit ? `bl _nomen_strdup_wrap\n` : `bl _strdup\n`);
19704
- emit_asm(status, `ldr x1, [sp], #16\n`);
19705
- }
19706
- /**
19707
- * Pair load/store at [x29, #offset] with an arbitrary register pair —
19708
- * splits into two single-word accesses beyond the ldp/stp +504 range.
19709
- */
19710
- function emit_pair_load_x29(status, offset, a = "x0", b = "x1") {
19711
- if (offset + 8 > 504) {
19712
- emit_asm(status, `ldr ${a}, [x29, #${offset}]\n`);
19713
- emit_asm(status, `ldr ${b}, [x29, #${offset + 8}]\n`);
19714
- return;
19715
- }
19716
- emit_asm(status, `ldp ${a}, ${b}, [x29, #${offset}]\n`);
19717
- }
19718
- function emit_pair_store_x29(status, offset, a = "x0", b = "x1") {
19719
- if (offset + 8 > 504) {
19720
- emit_asm(status, `str ${a}, [x29, #${offset}]\n`);
19721
- emit_asm(status, `str ${b}, [x29, #${offset + 8}]\n`);
19722
- return;
19723
- }
19724
- emit_asm(status, `stp ${a}, ${b}, [x29, #${offset}]\n`);
19725
- }
19726
- /**
19727
- * Owning specializations for Array<string>'s raw T-generic bodies
19728
- * (`with` / `set`), replacing the shared-pointer copies with per-slot deep
19729
- * copies — the aarch64 analog of Array.nm's `#if T_NEEDS_STRDUP` C branch.
19730
- *
19731
- * Conventions (matching Array.nm raw bodies):
19732
- * - `with` (static): x0/x1 = value pair, x2 = count. Returns x0 = heap
19733
- * buffer ([ptr] = length, elements at +8..).
19734
- * - `set` (method): x19 = first element, x1 = index, x2/x3 = value pair.
19735
- * Bounds are guaranteed by the Nomen-level constraints.
19736
- */
19737
- function emit_owning_array_string_specialize(func_name, status, self_reg = "x19") {
19738
- if (func_name === "with") {
19739
- emit_asm(status, `stp x19, x20, [sp, #-16]!\n`);
19740
- emit_asm(status, `stp x21, x22, [sp, #-16]!\n`);
19741
- emit_asm(status, `stp x24, x25, [sp, #-16]!\n`);
19742
- emit_asm(status, `mov x19, x0\n`);
19743
- emit_asm(status, `mov x20, x1\n`);
19744
- emit_asm(status, `mov x21, x2\n`);
19745
- emit_asm(status, `add x0, x21, #1\n`);
19746
- emit_asm(status, `lsl x0, x0, #4\n`);
19747
- emit_malloc(status);
19748
- emit_asm(status, `mov x22, x0\n`);
19749
- emit_asm(status, `str x21, [x22]\n`);
19750
- emit_asm(status, `mov x23, #8\n`);
19751
- emit_asm(status, `mov x24, #0\n`);
19752
- emit_asm(status, `.Larr_str_with_loop:\n`);
19753
- emit_asm(status, `cmp x24, x21\n`);
19754
- emit_asm(status, `b.ge .Larr_str_with_done\n`);
19755
- emit_asm(status, `mov x0, x19\n`);
19756
- emit_strdup(status);
19757
- emit_asm(status, `add x25, x22, x23\n`);
19758
- emit_asm(status, `stp x0, x20, [x25]\n`);
19759
- emit_asm(status, `add x23, x23, #16\n`);
19760
- emit_asm(status, `add x24, x24, #1\n`);
19761
- emit_asm(status, `b .Larr_str_with_loop\n`);
19762
- emit_asm(status, `.Larr_str_with_done:\n`);
19763
- emit_asm(status, `mov x0, x22\n`);
19764
- emit_asm(status, `ldp x24, x25, [sp], #16\n`);
19765
- emit_asm(status, `ldp x21, x22, [sp], #16\n`);
19766
- emit_asm(status, `ldp x19, x20, [sp], #16\n`);
19767
- return true;
19768
- }
19769
- if (func_name === "at" || func_name === "first" || func_name === "at_end") {
19770
- emit_asm(status, `stp x20, x21, [sp, #-16]!\n`);
19771
- if (func_name === "at") {
19772
- emit_asm(status, `lsl x9, x1, #4\n`);
19773
- emit_asm(status, `add x9, ${self_reg}, x9\n`);
19774
- } else if (func_name === "at_end") {
19775
- emit_asm(status, `ldr x9, [${self_reg}, #-8]\n`);
19776
- emit_asm(status, `sub x9, x9, #1\n`);
19777
- emit_asm(status, `lsl x9, x9, #4\n`);
19778
- emit_asm(status, `add x9, ${self_reg}, x9\n`);
19779
- } else emit_asm(status, `add x9, ${self_reg}, #0\n`);
19780
- emit_asm(status, `ldp x0, x1, [x9]\n`);
19781
- emit_asm(status, `ldp x20, x21, [sp], #16\n`);
19782
- return true;
19783
- }
19784
- if (func_name === "set") {
19785
- emit_asm(status, `stp x20, x21, [sp, #-16]!\n`);
19786
- emit_asm(status, `lsl x9, x1, #4\n`);
19787
- emit_asm(status, `add x9, ${self_reg}, x9\n`);
19788
- emit_asm(status, `ldr x0, [x9]\n`);
19789
- emit_asm(status, `mov x20, x2\n`);
19790
- emit_asm(status, `mov x21, x3\n`);
19791
- emit_free(status);
19792
- emit_asm(status, `mov x0, x20\n`);
19793
- emit_strdup(status);
19794
- emit_asm(status, `str x0, [x9]\n`);
19795
- emit_asm(status, `str x21, [x9, #8]\n`);
19796
- emit_asm(status, `ldp x20, x21, [sp], #16\n`);
19797
- return true;
19798
- }
19799
- return false;
19800
- }
19801
- /** Pair store to [base_reg, #offset] with stur fallback (misaligned/far). */
19802
- function emit_pair_store_to(status, base_reg, offset, a, b) {
19803
- if (offset % 8 === 0 && offset + 8 <= 504 && offset >= 0) emit_asm(status, `stp ${a}, ${b}, [${base_reg}, #${offset}]\n`);
19804
- else {
19805
- emit_asm(status, `stur ${a}, [${base_reg}, #${offset}]\n`);
19806
- emit_asm(status, `stur ${b}, [${base_reg}, #${offset + 8}]\n`);
19807
- }
19808
- }
19809
- //#endregion
19810
19879
  //#region ../src/build_aarch64/build_struct_node.ts
19811
19880
  let field_strdup_guard_counter = 0;
19812
19881
  function emit_typed_store(status, src_reg, dst_base, offset, size) {
@@ -21327,7 +21396,7 @@ function build_block_node$1(node, status) {
21327
21396
  heap_slots: [],
21328
21397
  struct_decls: []
21329
21398
  });
21330
- const declarations_before = status.scoped_declarations.length;
21399
+ const declarations_before = node.node_type === "func" ? 0 : status.scoped_declarations.length;
21331
21400
  for (let child of node.statements) if (is_struct_node(child)) {
21332
21401
  if (!should_emit_definition(child, status.emit_mode, status.structs, status.system_struct_names)) continue;
21333
21402
  build_struct_node$1(child, status);
@@ -21421,6 +21490,69 @@ function call_in_set(set, call) {
21421
21490
  return set.has(call.name.replace(/#/g, ""));
21422
21491
  }
21423
21492
  //#endregion
21493
+ //#region ../src/build_common/call_init_string_fields.ts
21494
+ /**
21495
+ * Whether a declaration-initializer expression is a value the callee's
21496
+ * return-boundary normalization made UNIFORMLY heap-owned, so the fresh
21497
+ * binding records every string field and frees them at scope exit:
21498
+ *
21499
+ * - a plain function call registered as a normalized struct returner
21500
+ * (the registry is populated at the callee's return sites — a callee
21501
+ * built later, or one that returns slot borrows, is not registered and
21502
+ * the binding keeps the pre-existing borrow behavior);
21503
+ * - an owned accessor (`pop`/`move_T`/`copy`) — move-out semantics
21504
+ * transfer slot-owned heap buffers to the binding.
21505
+ *
21506
+ * NOT normalized (never recorded): struct constructions (`Info(...)` — its
21507
+ * `#init` leaves rodata borrows and bypasses the return path) and container
21508
+ * BORROW accessors (`.at`/`.first`/`.slice`/`load` — the slot keeps
21509
+ * ownership; recording would free the container's storage).
21510
+ */
21511
+ function is_normalized_struct_call(value, status) {
21512
+ if (!value) return false;
21513
+ if (value.node_type === "func_call") {
21514
+ const call = value;
21515
+ return !!status.normalized_struct_returners?.has(call.name);
21516
+ }
21517
+ if (value.node_type === "access") {
21518
+ const access = value.access;
21519
+ if (access?.node_type !== "access_func") return false;
21520
+ const name = access.name ?? "";
21521
+ if (is_container_borrow_accessor_name(name)) return false;
21522
+ if (status.normalized_struct_returners?.has(name)) return true;
21523
+ const target = value.target;
21524
+ const receiver_type = target ? target.type ?? void 0 : void 0;
21525
+ if (receiver_type?.name) {
21526
+ const label = `${mono_type_name(receiver_type)}_${name}`;
21527
+ if (status.normalized_struct_returners?.has(label)) return true;
21528
+ }
21529
+ return false;
21530
+ }
21531
+ return false;
21532
+ }
21533
+ /**
21534
+ * Record every direct string field of a call-initialized value-struct
21535
+ * binding (`var Info i = make()`) as heap-owned: the callee's return
21536
+ * normalization guarantees each field is a heap buffer, so scope-exit
21537
+ * auto_free must free them, and a later `i.field = …` write sees
21538
+ * old_was_heap and reclaims the displaced buffer instead of leaking it.
21539
+ * No-op for non-struct, class, view, and array types, for structs without
21540
+ * direct string fields, and for non-call initializers.
21541
+ */
21542
+ function record_call_init_string_fields(node, status) {
21543
+ if (!is_normalized_struct_call(node.value, status)) return;
21544
+ const type = node.type;
21545
+ if (!type?.name) return;
21546
+ const mono_name = mono_type_name(type);
21547
+ if (mono_name.startsWith("_Tuple_")) return;
21548
+ const struct = status.structs.find((s) => s.name === mono_name && !s.is_simple_type);
21549
+ if (!struct || struct.is_class) return;
21550
+ const fields = direct_string_fields(struct);
21551
+ if (!fields.length) return;
21552
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
21553
+ for (const field of fields) status.heap_string_fields.add(`${node.name}.${field.name}`);
21554
+ }
21555
+ //#endregion
21424
21556
  //#region ../src/build_common/fold_string_const.ts
21425
21557
  /**
21426
21558
  * Fold a top-level `const`'s string `+` chain into a single string literal.
@@ -22554,12 +22686,15 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
22554
22686
  build_swap_params(func_call, status);
22555
22687
  } else {
22556
22688
  const old_buffer = status.struct_return_buffer;
22689
+ const old_buffer_var = status.struct_return_buffer_var;
22557
22690
  const old_preset = status.call_x8_preset;
22558
22691
  emit_var_address(status, "x8", node.name);
22692
+ status.struct_return_buffer_var = node.name;
22559
22693
  status.struct_return_buffer = "x8";
22560
22694
  status.call_x8_preset = true;
22561
22695
  emit_init_value$1(node.value, nir_init, status);
22562
22696
  status.struct_return_buffer = old_buffer;
22697
+ status.struct_return_buffer_var = old_buffer_var;
22563
22698
  status.call_x8_preset = old_preset;
22564
22699
  }
22565
22700
  return;
@@ -23085,14 +23220,18 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23085
23220
  build_swap_params(func_call, status);
23086
23221
  } else if (status.structs.find((s) => s.name === (func_call.type?.name ?? func_call.name) && !s.is_simple_type && !s.is_class) && status.function_return_label) {
23087
23222
  const old_buffer = status.struct_return_buffer;
23223
+ const old_buffer_var = status.struct_return_buffer_var;
23088
23224
  const old_preset = status.call_x8_preset;
23089
23225
  emit_var_address(status, "x8", node.name);
23226
+ status.struct_return_buffer_var = node.name;
23090
23227
  status.struct_return_buffer = "x8";
23091
23228
  status.call_x8_preset = true;
23092
23229
  emit_init_value$1(node.value, nir_init, status);
23093
23230
  status.struct_return_buffer = old_buffer;
23231
+ status.struct_return_buffer_var = old_buffer_var;
23094
23232
  status.call_x8_preset = old_preset;
23095
23233
  emit_var_address(status, "x0", node.name);
23234
+ record_call_init_string_fields(node, status);
23096
23235
  } else {
23097
23236
  emit_init_value$1(node.value, nir_init, status);
23098
23237
  emit_var_store(status, "x0", node.name, struct_size);
@@ -23128,6 +23267,7 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23128
23267
  const field_type = (target_type.name ? status.structs.find((s) => s.name === target_type.name) : void 0)?.fields.find((f) => f.name === field_name)?.type ?? type_from_value_node$1(access.access);
23129
23268
  emit_struct_copy("x1", "x0", get_field_offset(target_type.name, field_name, status), get_struct_size(field_type.name, status), status);
23130
23269
  }
23270
+ record_call_init_string_fields(node, status);
23131
23271
  }
23132
23272
  }
23133
23273
  } else if (node.value) {
@@ -24967,6 +25107,18 @@ function build_return_node$1(node, status, nir_value) {
24967
25107
  const field_off = get_field_offset(ret_struct.name, field.name, status);
24968
25108
  emit_enum_payload_strdups_at(status, field_enum.name, "x8", field_off);
24969
25109
  }
25110
+ if (ret_struct && !returns_nullable_struct) {
25111
+ const returned_var = node.value.node_type === "value" ? node.value.value : "";
25112
+ if (returned_var !== "" && returned_var !== "0" && returned_var !== "null" && all_scope_frames(status).flat().some((d) => d.name === returned_var)) {
25113
+ let skip_fields;
25114
+ const prefix = `${node.value.value}.`;
25115
+ for (const key of status.heap_string_fields ?? []) if (key.startsWith(prefix)) {
25116
+ skip_fields ??= /* @__PURE__ */ new Set();
25117
+ skip_fields.add(key.slice(prefix.length));
25118
+ }
25119
+ emit_string_field_strdups_at(status, ret_struct.name, "x8", 0, skip_fields);
25120
+ }
25121
+ }
24970
25122
  }
24971
25123
  }
24972
25124
  if (status.return_assign) {
@@ -28596,8 +28748,39 @@ function build_function_call_node$1(node, status) {
28596
28748
  emit_asm(status, `str x9, [x29, #${off + struct_size}]\n`);
28597
28749
  emit_asm(status, `add x0, x29, #${off}\n`);
28598
28750
  }
28599
- } else if (is_struct_type$3(param_type, status) || is_enum_with_data_type$2(param_type, status)) emit_struct_address(node.params[i], status);
28600
- else if (arg_deferrable(i)) {
28751
+ } else if (is_struct_type$3(param_type, status) || is_enum_with_data_type$2(param_type, status)) {
28752
+ if (node.move_owned_param_indices?.includes(i) && node.params[i].node_type === "value") {
28753
+ const transfer_struct = status.structs.find((st) => st.name === resolve_struct_name(param_type, param.type?.type_args, status) && !st.is_simple_type && !st.is_class && !st.is_generic);
28754
+ const transfer_var = node.params[i].value;
28755
+ if (transfer_struct && direct_string_fields(transfer_struct).length > 0 && transfer_var) {
28756
+ const recorded = /* @__PURE__ */ new Set();
28757
+ for (const key of status.heap_string_fields ?? []) if (key.startsWith(`${transfer_var}.`)) recorded.add(key.slice(transfer_var.length + 1));
28758
+ if (direct_string_fields(transfer_struct).filter((f) => !recorded.has(f.name)).length > 0) {
28759
+ emit_struct_address(node.params[i], status);
28760
+ ensure_newline(status);
28761
+ emit_string_field_strdups_at(status, transfer_struct.name, "x0", 0, recorded);
28762
+ if (status.call_x8_preset && status.struct_return_buffer_var) emit_var_address(status, "x8", status.struct_return_buffer_var);
28763
+ }
28764
+ }
28765
+ }
28766
+ const arg_struct = status.structs.find((st) => st.name === resolve_struct_name(param_type, param.type?.type_args, status) && !st.is_simple_type && !st.is_class && !st.is_generic);
28767
+ if (node.owned_value_param_indices?.includes(i) && arg_struct && !(arg_struct.traits ?? []).length && direct_string_fields(arg_struct).length > 0 && status.normalized_struct_returners?.has(func_name) && !status.func_address_taken?.has(func_name)) {
28768
+ const mono_name = arg_struct.name;
28769
+ const struct_size = get_struct_size(mono_name, status);
28770
+ const off = allocate_stack_space(status, struct_size);
28771
+ emit_struct_address(node.params[i], status);
28772
+ ensure_newline(status);
28773
+ emit_asm(status, `mov x1, x0\n`);
28774
+ emit_asm(status, `add x0, x29, #${off}\n`);
28775
+ for (let b = 0; b < struct_size; b += 8) {
28776
+ emit_asm(status, `ldr x2, [x1, #${b}]\n`);
28777
+ emit_asm(status, `str x2, [x0, #${b}]\n`);
28778
+ }
28779
+ emit_asm(status, `add x0, x29, #${off}\n`);
28780
+ emit_string_field_strdups_at(status, mono_name, "x0");
28781
+ if (status.call_x8_preset && status.struct_return_buffer_var) emit_var_address(status, "x8", status.struct_return_buffer_var);
28782
+ } else emit_struct_address(node.params[i], status);
28783
+ } else if (arg_deferrable(i)) {
28601
28784
  deferred_args.push({
28602
28785
  param: node.params[i],
28603
28786
  slot: arg_slot[i]
@@ -28729,6 +28912,7 @@ function build_function_call_node$1(node, status) {
28729
28912
  const tname = all_scope_frames(status).flat().find((d) => d.name === vname)?.type?.name ?? param.type?.name;
28730
28913
  if (tname === "string") continue;
28731
28914
  if (tname && status.enums.find((e) => e.name === tname && e.has_associated_data)) continue;
28915
+ if (vname !== void 0 && (node.move_owned_param_indices?.includes(idx) ?? false)) clear_heap_string_fields(status, vname);
28732
28916
  }
28733
28917
  if (param) mark_moved_if_struct(param, status);
28734
28918
  }
@@ -31057,11 +31241,23 @@ function free_scoped_declarations(status, decls, persist_string_field_records =
31057
31241
  commented = true;
31058
31242
  }
31059
31243
  const dot = key.indexOf(".");
31060
- status.code += `free(${c_function_name(key.substring(0, dot))}${key.substring(dot)}.ptr);\n`;
31244
+ const base = c_function_name(key.substring(0, dot));
31245
+ const arrow = dec.string_fields_via_pointer ? "->" : ".";
31246
+ const field = key.substring(dot + 1);
31247
+ const free_fn = status.audit ? "nomen_free_wrap" : "free";
31248
+ status.code += `${free_fn}(${base}${arrow}${field}.ptr);\n`;
31061
31249
  if (!persist_string_field_records) status.heap_string_fields.delete(key);
31062
31250
  }
31063
31251
  }
31064
31252
  for (const dec of decls) {
31253
+ if (dec.owned_heap_shell) {
31254
+ if (!commented) {
31255
+ status.code += "\n// Auto-free\n";
31256
+ commented = true;
31257
+ }
31258
+ status.code += `${status.audit ? "nomen_free_wrap" : "free"}(${c_function_name(dec.name)});\n`;
31259
+ continue;
31260
+ }
31065
31261
  if (persist_string_field_records && dec.force_owned_string === true) continue;
31066
31262
  if (status.moved?.has(dec.name)) continue;
31067
31263
  const cname = c_function_name(dec.name);
@@ -31659,7 +31855,8 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
31659
31855
  status.init_assigned_fields.add(tracked_key);
31660
31856
  }
31661
31857
  const old_was_heap = !!target_struct?.is_class && !first_init_write || !!status.heap_string_fields?.has(tracked_key);
31662
- if (target_struct && target_var && (!self_target || target_struct.is_class)) {
31858
+ const is_class_target = !!target_struct?.is_class;
31859
+ if (target_struct && (target_var || is_class_target) && (!self_target || is_class_target)) {
31663
31860
  const fresh_heap = is_owned_heap_temp$1(node.right_value, status);
31664
31861
  const rhs_is_null_value = node.right_value.node_type === "value" && node.right_value.value === "null";
31665
31862
  const saved_len = begin_code_scratch(status);
@@ -32741,6 +32938,7 @@ function build_declaration_node(node, status, nir_init, nir_swap) {
32741
32938
  emit_swap_value$1(node.swap, nir_swap, status);
32742
32939
  }
32743
32940
  if (has_field_overrides(node.value)) emit_field_overrides(safe_name, node.value, build_node$1, status, ";\n", ";\n");
32941
+ record_call_init_string_fields(node, status);
32744
32942
  }
32745
32943
  }
32746
32944
  }
@@ -33256,6 +33454,7 @@ function build_return_node(node, status, nir_value) {
33256
33454
  }
33257
33455
  }
33258
33456
  let returned_value_decl;
33457
+ let returned_recorded_string_fields;
33259
33458
  if (node.value.node_type === "value") {
33260
33459
  const value = node.value.value;
33261
33460
  returned_value_decl = find_decl_across_scopes(value, status);
@@ -33269,7 +33468,11 @@ function build_return_node(node, status, nir_value) {
33269
33468
  }
33270
33469
  if (status.heap_string_fields?.size) {
33271
33470
  const prefix = `${value}.`;
33272
- for (const key of Array.from(status.heap_string_fields)) if (key.startsWith(prefix)) status.heap_string_fields.delete(key);
33471
+ for (const key of Array.from(status.heap_string_fields)) if (key.startsWith(prefix)) {
33472
+ returned_recorded_string_fields ??= /* @__PURE__ */ new Set();
33473
+ returned_recorded_string_fields.add(key.slice(prefix.length));
33474
+ status.heap_string_fields.delete(key);
33475
+ }
33273
33476
  }
33274
33477
  }
33275
33478
  if (ret_type?.is_array && return_array_var && return_array_len > 0) {
@@ -33378,6 +33581,11 @@ function build_return_node(node, status, nir_value) {
33378
33581
  status.code += `;\n`;
33379
33582
  if (node.value.node_type === "access" && node.value.access?.node_type === "access_field" && !returns_string_zero && !returns_struct_zero && !returns_view_value && ret_type.name !== "string" && !ret_type.is_view && !ret_type.is_array && status.enums.find((e) => e.name === ret_type.name && e.has_associated_data)) status.code += `_return_val = ${ret_type.name}_copy(_return_val);\n`;
33380
33583
  if (has_field_overrides(node.value)) emit_field_overrides("_return_val", node.value, build_node$1, status, "", ";\n");
33584
+ const bare_local_return = node.value.node_type === "value" && node.value.value !== "0" && node.value.value !== "null" && !!returned_value_decl;
33585
+ if (is_struct && !return_is_class && !ret_type.is_view && !ret_type.is_array && return_struct && !returns_struct_zero && bare_local_return) for (const field of direct_string_fields(return_struct)) {
33586
+ if (returned_recorded_string_fields?.has(field.name)) continue;
33587
+ status.code += `_return_val.${field.name} = nomen_str_dup(_return_val.${field.name});\n`;
33588
+ }
33381
33589
  emit_nursery_joins_on_return_c(status);
33382
33590
  reclaim_all_c_scopes(status);
33383
33591
  status.code += `return _return_val;\n`;
@@ -35328,6 +35536,18 @@ function build_function_node(node, status) {
35328
35536
  if (param_trait && !param_struct?.is_class) decl.trait_class_trait = param.type.name;
35329
35537
  status.scoped_declarations.push(decl);
35330
35538
  }
35539
+ const is_free_function = (!node.scope || node.scope.node_type === "root" || node.scope.node_type === "func") && !node.params.some((p) => p.is_self_param);
35540
+ const fn_name = node.name;
35541
+ const fn_label = emission_label(node);
35542
+ const owned_value_struct = is_free_function && !node.address_escaped && !param.is_self_param && !param.is_variadic && !param.type.is_ref && !param.type.is_nullable && !param.type.is_view && !param.type.is_array && param.type.name ? status.structs.find((st) => st.name === param.type.name && !st.is_simple_type && !st.is_class) : void 0;
35543
+ if (owned_value_struct && !(owned_value_struct.traits ?? []).length && direct_string_fields(owned_value_struct).length > 0 && !struct_needs_destroy(owned_value_struct, status) && (status.normalized_struct_returners?.has(fn_name) || status.normalized_struct_returners?.has(fn_label)) && !status.func_address_taken?.has(fn_name) && !status.func_address_taken?.has(fn_label)) {
35544
+ const decl = new DeclarationNode(param.start, "private", "var", pname, param.type);
35545
+ decl.string_fields_via_pointer = true;
35546
+ decl.owned_heap_shell = !param.is_moved;
35547
+ status.scoped_declarations.push(decl);
35548
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
35549
+ for (const field of direct_string_fields(owned_value_struct)) status.heap_string_fields.add(`${pname}.${field.name}`);
35550
+ }
35331
35551
  }
35332
35552
  }
35333
35553
  const nir = node.statements?.length ? lower_function(node) : void 0;
@@ -36568,6 +36788,27 @@ function build_function_call_node(node, status) {
36568
36788
  continue;
36569
36789
  }
36570
36790
  const param_type = type_from_value_node$1(node.params[i]);
36791
+ if (node.move_owned_param_indices?.includes(i) && node.params[i].node_type === "value") {
36792
+ const transfer_struct = status.structs.find((st) => st.name === mono_type_name(param_type) && !st.is_simple_type && !st.is_class && !st.is_generic);
36793
+ const transfer_var = node.params[i].value;
36794
+ if (transfer_struct && direct_string_fields(transfer_struct).length > 0 && transfer_var) {
36795
+ const recorded = /* @__PURE__ */ new Set();
36796
+ for (const key of status.heap_string_fields ?? []) if (key.startsWith(`${transfer_var}.`)) recorded.add(key.slice(transfer_var.length + 1));
36797
+ const unrecorded = direct_string_fields(transfer_struct).filter((f) => !recorded.has(f.name));
36798
+ if (unrecorded.length > 0) {
36799
+ const saved_arg = begin_code_scratch(status);
36800
+ status.code += `(void *)&`;
36801
+ status.suppress_dereference = true;
36802
+ build_node$1(node.params[i], status);
36803
+ status.suppress_dereference = false;
36804
+ const addr_text = end_code_scratch(status, saved_arg);
36805
+ status.code += `(void *)({ struct ${transfer_struct.name} *_t = (struct ${transfer_struct.name} *)${addr_text}; `;
36806
+ for (const f of unrecorded) status.code += `_t->${f.name} = nomen_str_dup(_t->${f.name}); `;
36807
+ status.code += `(void *)_t; })`;
36808
+ continue;
36809
+ }
36810
+ }
36811
+ }
36571
36812
  if (node.nullable_param_indices?.includes(i) && node.params[i].node_type === "value" && node.params[i].value === "null") {
36572
36813
  status.code += `(void *)&(struct ${param_type.name}){0}, 0`;
36573
36814
  continue;
@@ -36614,6 +36855,23 @@ function build_function_call_node(node, status) {
36614
36855
  status.suppress_dereference = false;
36615
36856
  continue;
36616
36857
  }
36858
+ const arg_struct = status.structs.find((st) => st.name === mono_type_name(param_type) && !st.is_simple_type && !st.is_class && !st.is_generic);
36859
+ if (node.owned_value_param_indices?.includes(i) && !param_is_class && arg_struct && !(arg_struct.traits ?? []).length && direct_string_fields(arg_struct).length > 0 && status.normalized_struct_returners?.has(func_name) && !status.func_address_taken?.has(func_name)) {
36860
+ const struct_name = arg_struct.name;
36861
+ const fields = direct_string_fields(arg_struct);
36862
+ const saved_arg = begin_code_scratch(status);
36863
+ status.code += `(void *)&`;
36864
+ status.suppress_dereference = true;
36865
+ build_node$1(node.params[i], status);
36866
+ status.suppress_dereference = false;
36867
+ const addr_text = end_code_scratch(status, saved_arg);
36868
+ status.code += `(void *)({ struct ${struct_name} *_src = (struct ${struct_name} *)${addr_text}; `;
36869
+ status.code += `struct ${struct_name} *_own = (struct ${struct_name} *)malloc(sizeof(struct ${struct_name})); `;
36870
+ status.code += `*_own = *_src; `;
36871
+ for (const field of fields) status.code += `_own->${field.name} = nomen_str_dup(_own->${field.name}); `;
36872
+ status.code += `_own; })`;
36873
+ continue;
36874
+ }
36617
36875
  if (!param_is_class && !!status.structs.find((s) => s.name === param_type.name && !s.is_simple_type) || !!status.traits.find((t) => t.name === param_type.name) || is_ref_param) {
36618
36876
  if (node.params[i].node_type === "value" && (status.function_ref_params?.has(node.params[i].value) || status.class_vars?.has(node.params[i].value))) status.suppress_dereference = true;
36619
36877
  else if (is_ref_param) status.code += `&`;
@@ -36690,9 +36948,12 @@ function build_function_call_node(node, status) {
36690
36948
  if (decl_hit) decl_hit.frame.splice(decl_hit.index, 1);
36691
36949
  if (is_value_struct) {
36692
36950
  const prefix = `${vname}.`;
36951
+ const transfer = node.move_owned_param_indices?.includes(idx) ?? false;
36693
36952
  for (const key of Array.from(status.heap_string_fields ?? [])) if (key.startsWith(prefix)) {
36694
- if (!status.pending_string_releases) status.pending_string_releases = [];
36695
- status.pending_string_releases.push(`free(${key}.ptr);`);
36953
+ if (!transfer) {
36954
+ if (!status.pending_string_releases) status.pending_string_releases = [];
36955
+ status.pending_string_releases.push(`free(${key}.ptr);`);
36956
+ }
36696
36957
  status.heap_string_fields.delete(key);
36697
36958
  }
36698
36959
  }
@@ -38353,6 +38614,10 @@ function get_base_address(access, status, reg) {
38353
38614
  if (paramReg !== reg) emit_asm(status, `mov ${reg}, ${paramReg}\n`);
38354
38615
  } else if (is_local_ref_var(name, status)) emit_deref_var_address(status, reg, name);
38355
38616
  else emit_var_address(status, reg, name);
38617
+ } else if (access_chain_crosses_class(access, status)) {
38618
+ build_node(access.target, status);
38619
+ ensure_newline(status);
38620
+ if (reg !== "x0") emit_asm(status, `mov ${reg}, x0\n`);
38356
38621
  } else {
38357
38622
  emit_address_of(access.target, status);
38358
38623
  ensure_newline(status);
@@ -41871,6 +42136,9 @@ function emit_address_of(node, status) {
41871
42136
  if (paramReg !== "x0") emit_asm(status, `mov x0, ${paramReg}\n`);
41872
42137
  } else if (is_local_ref_var(name, status)) emit_deref_var_address(status, "x0", name);
41873
42138
  else emit_var_address(status, "x0", name);
42139
+ } else if (access_chain_crosses_class(access, status)) {
42140
+ build_node(access.target, status);
42141
+ ensure_newline(status);
41874
42142
  } else {
41875
42143
  emit_address_of(access.target, status);
41876
42144
  ensure_newline(status);
@@ -44254,6 +44522,195 @@ function scan_func(func, struct_name, result, class_type_names) {
44254
44522
  }
44255
44523
  }
44256
44524
  //#endregion
44525
+ //#region ../src/build_common/struct_return_classification.ts
44526
+ /**
44527
+ * Pre-build classification of which functions return value structs whose
44528
+ * string fields are UNIFORMLY heap-owned via return-boundary normalization.
44529
+ * Populates `status.normalized_struct_returners`; a caller binding a call
44530
+ * result of a registered function records the struct's string fields and
44531
+ * frees them at scope exit, while bindings of unregistered callees keep the
44532
+ * pre-existing borrow behavior (never recorded, never freed — sound, though
44533
+ * it may leak on slot overwrites, which is the documented status quo).
44534
+ *
44535
+ * The rule is deliberately narrow: a function normalizes iff EVERY
44536
+ * struct-return statement returns a BARE VARIABLE (a local or parameter).
44537
+ * That is the transfer shape — the returned variable dies at the return, so
44538
+ * the boundary normalization (transfer recorded heap fields, strdup the
44539
+ * rest) is sound and leak-free. Every other shape keeps the status quo:
44540
+ *
44541
+ * - container borrow accessors (`at`/`first`/`slice`/`load` — e.g.
44542
+ * `Map.get`'s `return self.values.load_T(idx)`) yield SLOT-owned bytes;
44543
+ * normalizing them would leak in expression-temp consumers
44544
+ * (`m.get(k).field`) that nobody records or frees;
44545
+ * - owned accessors (`pop`/`move_T`/`copy`) transfer slot-owned heap
44546
+ * buffers — the caller's recording is sound WITHOUT normalization, so
44547
+ * they record via the owned-accessor rule (see
44548
+ * call_init_string_fields.ts) and the callee skips the copy;
44549
+ * - forwarded calls (`return make()`) are skipped when the callee is
44550
+ * registered (its result is already uniformly owned — copying it would
44551
+ * leak the inner buffers) and keep the status quo when it is not.
44552
+ *
44553
+ * This runs as a WHOLE-PROGRAM AST PRE-PASS (before any function body is
44554
+ * built) because a build-order-sensitive registry cannot work: nested
44555
+ * functions are built after their enclosing function's body, so a runtime
44556
+ * registration would be invisible to the caller's earlier lookup.
44557
+ */
44558
+ /** Collect every function and struct declaration under the AST (including
44559
+ * nested functions, struct methods, and function bodies at any depth). */
44560
+ function collect(node, fns, structs, seen) {
44561
+ if (!node || typeof node !== "object" || seen.has(node)) return;
44562
+ seen.add(node);
44563
+ if (node.node_type === "func") fns.push(node);
44564
+ if (node.node_type === "struct") structs.push(node);
44565
+ for (const key of Object.keys(node)) {
44566
+ if (key === "parent" || key === "scope") continue;
44567
+ const value = node[key];
44568
+ if (Array.isArray(value)) {
44569
+ for (const item of value) if (item && typeof item === "object" && "node_type" in item) collect(item, fns, structs, seen);
44570
+ } else if (value && typeof value === "object" && "node_type" in value) collect(value, fns, structs, seen);
44571
+ }
44572
+ }
44573
+ /** The DIRECT `return <expr>` statements of a function body (nested function
44574
+ * declarations are not descended into — their returns belong to them). */
44575
+ function direct_returns(fn) {
44576
+ const out = [];
44577
+ const seen = /* @__PURE__ */ new Set();
44578
+ const walk = (node) => {
44579
+ if (!node || typeof node !== "object" || seen.has(node)) return;
44580
+ seen.add(node);
44581
+ if (node.node_type === "func" && node !== fn) return;
44582
+ if (node.node_type === "return") out.push(node);
44583
+ for (const key of Object.keys(node)) {
44584
+ if (key === "parent" || key === "scope") continue;
44585
+ const value = node[key];
44586
+ if (Array.isArray(value)) {
44587
+ for (const item of value) if (item && typeof item === "object" && "node_type" in item) walk(item);
44588
+ } else if (value && typeof value === "object" && "node_type" in value) walk(value);
44589
+ }
44590
+ };
44591
+ for (const statement of fn.statements ?? []) walk(statement);
44592
+ return out;
44593
+ }
44594
+ /** Whether the function is a FREE function: not a method of a struct (whose
44595
+ * element/slot params are passed by address for in-place access) and not a
44596
+ * monomorphized container clone. */
44597
+ function is_free_function_node(fn) {
44598
+ const scope = fn.scope;
44599
+ return !scope || scope.node_type === "root" || scope.node_type === "func";
44600
+ }
44601
+ /**
44602
+ * Whole-program pre-pass: classify free functions that take an OWNING value
44603
+ * struct parameter (string fields) — those get pass-by-value semantics:
44604
+ *
44605
+ * - the CALL SITE materializes a uniformly heap-owned shell (struct-copy
44606
+ * + per-string-field strdup) and passes its address;
44607
+ * - the CALLEE seeds the param's string-field records at entry and frees
44608
+ * the surviving fields (and the shell) at scope exit.
44609
+ *
44610
+ * Writes through the param then stay local to the callee; the caller's
44611
+ * variable is never aliased or mutated. Functions whose address escapes
44612
+ * into a func-typed value (`var f = g`, `apply(g, …)`, `b.modify(0, touch)`)
44613
+ * are EXCLUDED: they may be invoked through a pointer with a borrowed
44614
+ * argument, and keep the status quo (borrow + the documented write leak).
44615
+ *
44616
+ * Populates `status.normalized_struct_returners` (the materializing set) and
44617
+ * `status.func_address_taken` (the excluded set) for the call-site and
44618
+ * function-definition builders.
44619
+ */
44620
+ function gather_normalized_struct_returners(root, status) {
44621
+ const fns = [];
44622
+ const structs = [];
44623
+ collect(root, fns, structs, /* @__PURE__ */ new Set());
44624
+ const address_taken = /* @__PURE__ */ new Set();
44625
+ const value_walk = (node) => {
44626
+ if (!node || typeof node !== "object") return;
44627
+ if (node.node_type === "value") {
44628
+ const v = node;
44629
+ if (typeof v.value === "string" && v.type?.name === "func" && /^[A-Za-z_]/.test(v.value)) address_taken.add(v.value);
44630
+ }
44631
+ for (const key of Object.keys(node)) {
44632
+ if (key === "parent" || key === "scope") continue;
44633
+ const child = node[key];
44634
+ if (Array.isArray(child)) {
44635
+ for (const item of child) if (item && typeof item === "object" && "node_type" in item) value_walk(item);
44636
+ } else if (child && typeof child === "object" && "node_type" in child) value_walk(child);
44637
+ }
44638
+ };
44639
+ value_walk(root);
44640
+ const decl_walk = (node) => {
44641
+ if (!node || typeof node !== "object") return;
44642
+ const d = node;
44643
+ if (d.node_type === "declaration" && typeof d.name === "string" && d.type?.name === "func") address_taken.add(d.name);
44644
+ for (const key of Object.keys(node)) {
44645
+ if (key === "parent" || key === "scope") continue;
44646
+ const child = node[key];
44647
+ if (Array.isArray(child)) {
44648
+ for (const item of child) if (item && typeof item === "object" && "node_type" in item) decl_walk(item);
44649
+ } else if (child && typeof child === "object" && "node_type" in child) decl_walk(child);
44650
+ }
44651
+ };
44652
+ decl_walk(root);
44653
+ const return_candidates = [];
44654
+ for (const fn of fns) {
44655
+ if (!is_free_function_node(fn)) continue;
44656
+ if (address_taken.has(fn.name)) continue;
44657
+ const ret = fn.return_type;
44658
+ if (!ret || ret.is_array || ret.is_view) continue;
44659
+ const ret_struct = structs.find((st) => st.name === ret.name && !st.is_simple_type && !st.is_class);
44660
+ if (!ret_struct || direct_string_fields(ret_struct).length === 0) continue;
44661
+ return_candidates.push(fn);
44662
+ }
44663
+ const return_normalizing = new Set(return_candidates.map((c) => c.name));
44664
+ let changed = true;
44665
+ while (changed) {
44666
+ changed = false;
44667
+ for (const fn of return_candidates) {
44668
+ if (!return_normalizing.has(fn.name)) continue;
44669
+ for (const ret of direct_returns(fn)) {
44670
+ const value = ret.value ?? void 0;
44671
+ if (value && value.node_type === "value") continue;
44672
+ if (value && value.node_type === "func_call" && return_normalizing.has(value.name ?? "")) continue;
44673
+ return_normalizing.delete(fn.name);
44674
+ changed = true;
44675
+ break;
44676
+ }
44677
+ }
44678
+ }
44679
+ for (const fn of fns) {
44680
+ if (!is_free_function_node(fn)) continue;
44681
+ if (address_taken.has(fn.name)) continue;
44682
+ if (fn.params.some((p) => p.is_self_param)) continue;
44683
+ if (fn.address_escaped) continue;
44684
+ if (!fn.has_body) continue;
44685
+ for (const param of fn.params ?? []) {
44686
+ if (param.is_self_param || param.is_variadic) continue;
44687
+ if (param.type.is_ref || param.type.is_nullable || param.type.is_view || param.type.is_array) continue;
44688
+ const struct = param.type.name ? structs.find((st) => st.name === param.type.name && !st.is_simple_type && !st.is_class) : void 0;
44689
+ if (!struct) continue;
44690
+ if (direct_string_fields(struct).length > 0 && !(struct.traits ?? []).length && !struct_needs_destroy(struct, status)) {
44691
+ if (!status.normalized_struct_returners) status.normalized_struct_returners = /* @__PURE__ */ new Set();
44692
+ status.normalized_struct_returners.add(fn.name);
44693
+ status.normalized_struct_returners.add(emission_label(fn));
44694
+ break;
44695
+ }
44696
+ }
44697
+ if (return_normalizing.has(fn.name)) {
44698
+ if (!status.normalized_struct_returners) status.normalized_struct_returners = /* @__PURE__ */ new Set();
44699
+ status.normalized_struct_returners.add(fn.name);
44700
+ status.normalized_struct_returners.add(emission_label(fn));
44701
+ }
44702
+ }
44703
+ for (const struct of structs) {
44704
+ if (struct.is_class || struct.is_generic || struct.is_simple_type) continue;
44705
+ if (!struct.functions.some((f) => f.name === "copy" && f.has_body)) continue;
44706
+ if (direct_string_fields(struct).length === 0) continue;
44707
+ if (!status.normalized_struct_returners) status.normalized_struct_returners = /* @__PURE__ */ new Set();
44708
+ status.normalized_struct_returners.add(`${struct.name}_copy`);
44709
+ }
44710
+ if (!status.func_address_taken) status.func_address_taken = /* @__PURE__ */ new Set();
44711
+ for (const name of address_taken) status.func_address_taken.add(name);
44712
+ }
44713
+ //#endregion
44257
44714
  //#region ../src/build.ts
44258
44715
  function build(root, options = {}) {
44259
44716
  let status = {
@@ -44287,6 +44744,7 @@ function build(root, options = {}) {
44287
44744
  reset_match_temp_counter();
44288
44745
  reset_ns_tmp_counter();
44289
44746
  stamp_last_use_moves(root);
44747
+ gather_normalized_struct_returners(root, status);
44290
44748
  if (options.arch === "aarch64") {
44291
44749
  install_asm_code_buffer(status);
44292
44750
  reset_string_counter$1();
@@ -46510,6 +46968,23 @@ function is_owning_struct_type_requiring_move(type, status) {
46510
46968
  if (!s || s.is_class) return false;
46511
46969
  return struct_owns_non_string_heap(s, status, /* @__PURE__ */ new Set());
46512
46970
  }
46971
+ /**
46972
+ * Whether `type` is a concrete value struct with direct string fields whose
46973
+ * only ownership those strings — the pass-by-value convention's domain: a
46974
+ * parameter of this type is fully owned by the callee (the call boundary
46975
+ * materializes a uniformly heap-owned copy), so `var` field writes inside
46976
+ * the body stay local and no call-site acknowledgment is needed. Excludes
46977
+ * structs that own non-string heap (those keep `move ... swap` / `ref`
46978
+ * semantics) and structs conforming to traits (trait dispatch aliases).
46979
+ */
46980
+ function is_pass_by_value_owning_struct(type, status) {
46981
+ const s = resolve_struct$1(type, status);
46982
+ if (!s || s.is_class || s.is_generic || s.is_simple_type) return false;
46983
+ if ((s.traits ?? []).length > 0) return false;
46984
+ if (direct_string_fields(s).length === 0) return false;
46985
+ if (struct_owns_non_string_heap(s, status, /* @__PURE__ */ new Set())) return false;
46986
+ return true;
46987
+ }
46513
46988
  function struct_owns_non_string_heap(s, status, visited) {
46514
46989
  if (visited.has(s.name)) return false;
46515
46990
  visited.add(s.name);
@@ -49150,6 +49625,12 @@ function check_declaration_node(decl, status) {
49150
49625
  if (is_view_keyword && decl.type_start !== void 0) decl.type.is_view = true;
49151
49626
  if (decl.func_params) {
49152
49627
  if (is_view_keyword) add_error(status, `'view' cannot declare a function-typed binding`, decl.start);
49628
+ if (decl.value?.node_type === "func") decl.value.address_escaped = true;
49629
+ else if (decl.value?.node_type === "value") {
49630
+ const fn = status.functions.findLast((f) => f.name === decl.value.value);
49631
+ if (fn) fn.address_escaped = true;
49632
+ }
49633
+ if (decl.value?.node_type === "value" && decl.value.value === "null" && !(decl.type.name === "func" && decl.type.is_nullable)) add_error(status, `cannot assign null to a non-nullable func — declare it 'func?' (or 'Func<...>?')`, decl.value.start);
49153
49634
  if (decl.func_return_type) {
49154
49635
  check_type_exists(decl.func_return_type, status, -1);
49155
49636
  const materialized = materialize_type(decl.func_return_type, status);
@@ -49214,7 +49695,7 @@ function check_declaration_node(decl, status) {
49214
49695
  status.values.push({
49215
49696
  declaration,
49216
49697
  name: decl.name,
49217
- type: decl.func_return_type || decl.type,
49698
+ type: decl.type.name === "func" && decl.type.is_nullable ? decl.type : decl.func_return_type || decl.type,
49218
49699
  is_set: !!decl.value,
49219
49700
  start: decl.start,
49220
49701
  is_null: decl.value?.node_type === "value" && decl.value.value === "null",
@@ -49712,7 +50193,7 @@ function check_struct_node(struct, status) {
49712
50193
  continue;
49713
50194
  }
49714
50195
  if (decl.func_params) {
49715
- decl.type = new Type("func");
50196
+ if (decl.type.name !== "func") decl.type = new Type("func");
49716
50197
  check_declaration_node(decl, status);
49717
50198
  continue;
49718
50199
  }
@@ -50020,6 +50501,7 @@ function synthesize_for_struct(struct, status) {
50020
50501
  if (struct.traits.includes("Stringable") && !struct_has_function(struct, "to_string") && fields.every((f) => field_is_stringable(f, status, /* @__PURE__ */ new Set()))) struct.functions.push(build_to_string(struct, fields));
50021
50502
  if (struct.traits.includes("Equatable") && !struct_has_function(struct, "eq") && !struct_has_function(struct, "ne") && fields.every((f) => field_is_equatable(f, status, /* @__PURE__ */ new Set()))) struct.functions.push(build_eq(struct, fields));
50022
50503
  if (struct.traits.includes("Hashable") && !struct_has_function(struct, "hash") && fields.every((f) => field_is_hashable(f, status, /* @__PURE__ */ new Set()))) struct.functions.push(build_hash(struct, fields));
50504
+ synthesize_copy_method(struct, status);
50023
50505
  }
50024
50506
  function build_to_string(struct, fields) {
50025
50507
  const parts = [string_literal(`${struct.name}(`)];
@@ -50051,6 +50533,81 @@ function build_hash(struct, fields) {
50051
50533
  const ret = new ReturnNode(-1, expr);
50052
50534
  return new FunctionNode(-1, "pub", "hash", new Type("uint"), [self_param(struct)], [ret]);
50053
50535
  }
50536
+ /**
50537
+ * Whether `struct` is eligible for a synthesized `copy` method: a value
50538
+ * struct whose only ownership is its DIRECT string fields — the same set
50539
+ * the pass-by-value argument convention covers. A struct that additionally
50540
+ * owns non-string heap (a resource-releasing `#destroy`, a class field, a
50541
+ * nested requiring-move struct) cannot be deep-copied by "byte copy +
50542
+ * strdup the strings" — its copy stays unwritten (move/swap remains the
50543
+ * escape hatch) — and a struct with no string fields is already soundly
50544
+ * byte-copyable, so it needs no method.
50545
+ */
50546
+ function struct_is_copyable(struct, status) {
50547
+ if (struct.is_class || struct.is_generic || struct.is_simple_type) return false;
50548
+ if (direct_string_fields(struct).length === 0) return false;
50549
+ if (struct_owns_non_string_heap(struct, status, /* @__PURE__ */ new Set())) return false;
50550
+ return struct_copy_ctor_args(struct) !== null;
50551
+ }
50552
+ /**
50553
+ * How a synthesized `copy` constructs the fresh value: the zero-argument
50554
+ * `#init` overload when one exists (auto or user), else the bodyless AUTO
50555
+ * `#init` whose params map 1:1 onto non-defaulted fields (pass
50556
+ * `self.<param>` for each). Null when neither shape exists — a user #init
50557
+ * with params only cannot be invoked without re-running its custom logic,
50558
+ * so no `copy` is synthesized.
50559
+ */
50560
+ function struct_copy_ctor_args(struct) {
50561
+ const inits = struct.functions.filter((f) => f.name === "#init");
50562
+ if (inits.some((f) => f.params.filter((p) => !p.is_self_param).length === 0)) return [];
50563
+ const auto = inits.find((f) => !f.has_body);
50564
+ if (!auto) return null;
50565
+ return auto.params.filter((p) => !p.is_self_param).map((p) => p.name);
50566
+ }
50567
+ /** Whether `type` names a struct that has (or will get) a `copy` method.
50568
+ * Order-independent: it re-evaluates the eligibility predicate instead of
50569
+ * looking for an already-synthesized method, so an outer struct nested in
50570
+ * the same pre-pass resolves a later-declared field struct. */
50571
+ function field_type_is_copyable(type, status) {
50572
+ if (!type.name || type.is_array || type.is_ref || type.is_view || type.is_nullable) return false;
50573
+ const nested = status.structs.find((s) => s.name === type.name);
50574
+ return !!nested && struct_is_copyable(nested, status);
50575
+ }
50576
+ /**
50577
+ * Synthesize `pub func copy(self) -> Struct` for owning value structs: a
50578
+ * fresh `Struct()` whose every field is rewritten from `self` — string
50579
+ * field writes strdup the source (the backends' field-write path), nested
50580
+ * copyable structs recurse through their own `copy`, everything else byte
50581
+ * copies. `return c` hits the return-boundary normalization (all fields
50582
+ * recorded → transfer raw), so the caller receives a uniformly heap-owned
50583
+ * value with no extra copies.
50584
+ */
50585
+ function build_copy(struct, status) {
50586
+ const ctor_args = struct_copy_ctor_args(struct) ?? [];
50587
+ const ctor = new FunctionCallNode(-1, struct.name, new Type(struct.name), ctor_args.map((name) => field_access("self", name)));
50588
+ const statements = [new DeclarationNode(-1, "private", "var", "c", new Type(struct.name), ctor)];
50589
+ for (const field of struct.fields) {
50590
+ const right = field_type_is_copyable(field.type, status) ? method_call(field_access("self", field.name), "copy") : field_access("self", field.name);
50591
+ statements.push(new AssignmentNode(-1, field_access("c", field.name), right, "="));
50592
+ }
50593
+ statements.push(new ReturnNode(-1, new ValueNode(-1, "c")));
50594
+ return new FunctionNode(-1, "pub", "copy", new Type(struct.name), [self_param(struct)], statements);
50595
+ }
50596
+ /**
50597
+ * Synthesize the `copy` method on `struct` when eligible and not already
50598
+ * present. Called from the block-level derive pre-pass (which runs before
50599
+ * the struct is checked, so the method's body is checked with it) AND from
50600
+ * `monomorphize` — a mono struct materializes mid-check, after that pre-pass
50601
+ * ran, so without this hook `Box<string>.copy()` — advertised by the
50602
+ * copy-discipline error messages — would resolve to "Function not found".
50603
+ * The caller is responsible for checking the synthesized body when no later
50604
+ * struct check will (see monomorphize).
50605
+ */
50606
+ function synthesize_copy_method(struct, status) {
50607
+ if (struct_has_function(struct, "copy")) return;
50608
+ if (!struct_is_copyable(struct, status)) return;
50609
+ struct.functions.push(build_copy(struct, status));
50610
+ }
50054
50611
  //#endregion
50055
50612
  //#region ../src/check/check_block_node.ts
50056
50613
  function check_block_node(node, status) {
@@ -50502,8 +51059,8 @@ function check_function_parameter_node(param, status, is_library_scope = false)
50502
51059
  if (param.type.is_pointer && !is_library_scope) add_error(status, `'ptr' parameters are reserved for the System library — raw pointer manipulation is not available to user code`, param.type_start ?? param.start);
50503
51060
  instantiate_generic_type(param.type, status);
50504
51061
  }
50505
- if (param.declaration === "var" && !param.is_moved && !param.is_self_param && !param.type.is_ref) add_error(status, `'var' parameters are not allowed — use 'ref' to mutate the caller's value (with 'ref' at the call site), or take it read-only and make a local 'var' copy`, param.start);
50506
- if (param.is_moved && param.type.name && !is_class_type$1(param.type.name, status) && !is_trait_type(param.type.name, status) && !is_owning_struct_type_requiring_move(param.type, status) && !status.type_params.includes(param.type.name) && !status.structs.some((s) => s.type_params.includes(param.type.name))) add_error(status, `move is only allowed for class, trait, or owning struct types, not '${param.type.name}'`, param.start);
51062
+ if (param.declaration === "var" && !param.is_moved && !param.is_self_param && !param.type.is_ref && !is_pass_by_value_owning_struct(param.type, status)) add_error(status, `'var' parameters are not allowed — use 'ref' to mutate the caller's value (with 'ref' at the call site), or take it read-only and make a local 'var' copy`, param.start);
51063
+ if (param.is_moved && param.type.name && !is_class_type$1(param.type.name, status) && !is_trait_type(param.type.name, status) && !is_owning_struct_type_requiring_move(param.type, status) && !is_pass_by_value_owning_struct(param.type, status) && !status.type_params.includes(param.type.name) && !status.structs.some((s) => s.type_params.includes(param.type.name))) add_error(status, `move is only allowed for class, trait, or owning struct types, not '${param.type.name}'`, param.start);
50507
51064
  if (param.is_variadic && param.default_value) {
50508
51065
  add_error(status, `Variadic parameter cannot have a default value`, param.start);
50509
51066
  return;
@@ -50515,7 +51072,7 @@ function check_function_parameter_node(param, status, is_library_scope = false)
50515
51072
  }
50516
51073
  if (status.values.some((v) => v.name === param.name && v.declaration !== "const" && !(is_library_scope && v.is_global))) add_error(status, `Parameter already declared: ${param.name}`, param.start);
50517
51074
  status.values.push({
50518
- declaration: param.declaration,
51075
+ declaration: param.declaration === "var" || is_pass_by_value_owning_struct(param.type, status) ? "var" : param.declaration,
50519
51076
  name: param.name,
50520
51077
  type: param.type,
50521
51078
  is_set: true,
@@ -50932,7 +51489,7 @@ function check_function_call_node(node, status) {
50932
51489
  return false;
50933
51490
  }
50934
51491
  if (magic_ctor && spawn_ctor_shape) return check_magic_ctor(node, status, magic_ctor);
50935
- let func = find_free_function(status, node.name);
51492
+ let func = status.values.findLast((v) => v.name === node.name && v.func_params !== void 0) ? void 0 : find_free_function(status, node.name);
50936
51493
  if (func?.is_closure && func.captures?.length) func = void 0;
50937
51494
  if (!func) {
50938
51495
  const struct = resolve_declared_struct(node.name, status);
@@ -51001,6 +51558,10 @@ function check_function_call_node(node, status) {
51001
51558
  const param_index = status.values.findLastIndex((v) => v.name === node.name);
51002
51559
  const param_value = param_index >= 0 ? status.values[param_index] : void 0;
51003
51560
  if (!!param_value && (param_value.type.name === "func" || param_value.func_params !== void 0 || param_value.func_return_type !== void 0) && param_value) {
51561
+ if (param_value.is_null && !status.allow_null_value) {
51562
+ add_error(status, `Variable '${node.name}' may be null`, node.start);
51563
+ return false;
51564
+ }
51004
51565
  if (status.moved_variables?.has(node.name)) {
51005
51566
  add_error(status, `Variable '${node.name}' used after move`, node.start);
51006
51567
  return false;
@@ -51229,6 +51790,30 @@ function monomorphize(generic_struct, type_args, status) {
51229
51790
  const init_func = new FunctionNode(generic_struct.start, "pub", "#init", init_return_type, init_params);
51230
51791
  mono_struct.functions.push(init_func);
51231
51792
  }
51793
+ synthesize_copy_method(mono_struct, status);
51794
+ const mono_copy = mono_struct.functions.find((f) => f.name === "copy");
51795
+ if (mono_copy && !mono_copy.checked) {
51796
+ mono_copy.scope = mono_struct;
51797
+ check_function_node(mono_copy, {
51798
+ stack: status.stack,
51799
+ scope_depth: status.scope_depth,
51800
+ types: status.types,
51801
+ values: [],
51802
+ function_value_base: 0,
51803
+ structs: status.structs,
51804
+ traits: status.traits,
51805
+ enums: status.enums,
51806
+ bitsets: status.bitsets,
51807
+ functions: status.functions,
51808
+ allocations: [],
51809
+ var_name_counter: status.var_name_counter,
51810
+ type_params: [],
51811
+ errors: status.errors,
51812
+ function_emission_names: status.function_emission_names,
51813
+ allow_internal: status.allow_internal,
51814
+ library_boundary: status.library_boundary
51815
+ });
51816
+ }
51232
51817
  const root = status.stack[0];
51233
51818
  if (!root.statements.some((s) => s.node_type === "struct" && s.name === mono_name)) root.statements.push(mono_struct);
51234
51819
  return mono_struct;
@@ -51732,6 +52317,8 @@ function check_magic_ctor(node, status, name) {
51732
52317
  add_error(status, `Spawned call '${call.name}' did not resolve`, node.start);
51733
52318
  return false;
51734
52319
  }
52320
+ const spawned = call.resolved_function;
52321
+ if (spawned) spawned.address_escaped = true;
51735
52322
  validate_spawn_args_sendable(call, status);
51736
52323
  stamp_spawn_ctor(node, name);
51737
52324
  const return_type = call.type;
@@ -53318,6 +53905,13 @@ function check_function_call(node, status, func, target_type, self_value, self_p
53318
53905
  const param_type = type_from_value_node(param, status);
53319
53906
  const param_value = value_from_value_node(param);
53320
53907
  const has_ref_keyword = node.ref_param_indices?.includes(i) ?? false;
53908
+ if (func && func_param && func.has_body && (!func.scope || func.scope.node_type !== "struct") && !func.params.some((p) => p.is_self_param) && !func.address_escaped && !func_param.is_self_param && !func_param.is_variadic && !func_param.type.is_ref && !func_param.type.is_nullable && func_param.type.name && !is_owning_struct_type_requiring_move(func_param.type, status)) {
53909
+ const owned_struct = status.structs.find((st) => st.name === func_param.type.name && !st.is_simple_type && !st.is_class);
53910
+ if (owned_struct && !(owned_struct.traits ?? []).length && direct_string_fields(owned_struct).length > 0 && !struct_needs_destroy(owned_struct, status)) {
53911
+ const indices = func_param.is_moved ? node.move_owned_param_indices ??= [] : node.owned_value_param_indices ??= [];
53912
+ if (!indices.includes(i)) indices.push(i);
53913
+ }
53914
+ }
53321
53915
  const has_mov_keyword = node.move_param_indices?.includes(i) ?? false;
53322
53916
  if (func_param && func_param.type.name === "string" && !func_param.type.is_view && !func_param.type.is_array && param_type.name === "string" && param_type.is_view) add_error(status, `cannot pass a 'view string' to string parameter '${func_param.name}' — call .to_string() to materialize an owned copy`, param.start);
53323
53917
  if (func_param.type.is_ref && !has_ref_keyword) add_error(status, `Missing 'ref' keyword for ref parameter '${func_param.name}'`, param.start);
@@ -53342,12 +53936,13 @@ function check_function_call(node, status, func, target_type, self_value, self_p
53342
53936
  else if (!func_param.type.is_ref && (arg_is_stack_array_var || arg_is_literal) && elem_struct && !elem_struct.is_simple_type && !elem_struct.is_class && struct_has_string_fields(elem_struct, status)) add_error(status, `Cannot pass an array of owning value struct '${param_type.name}' (string fields) to 'Array<${param_type.name}>' parameter '${func_param.name}' — the materialised heap copy would share the element strings. Use 'List<${param_type.name}>' instead: its Buffer deep-copies owning elements soundly`, param.start);
53343
53937
  }
53344
53938
  const param_is_class = func_param.type.name && is_class_type$1(func_param.type.name, status);
53345
- const param_is_owning_struct = func_param.is_moved && !func_param.type.is_nullable && !!func_param.type.name && is_owning_struct_type_requiring_move(func_param.type, status);
53939
+ const param_transfers = !!node.move_owned_param_indices?.includes(i);
53940
+ const param_is_owning_struct = func_param.is_moved && !func_param.type.is_nullable && !!func_param.type.name && (is_owning_struct_type_requiring_move(func_param.type, status) || param_transfers);
53346
53941
  const arg_is_owned_value = param.node_type === "value" && borrow_depth_of(param, status) === void 0;
53347
53942
  if (func_param.is_moved && !has_mov_keyword && (param_is_class || param_is_owning_struct) && arg_is_owned_value) add_error(status, `Missing 'move' keyword for move parameter '${func_param.name}'`, param.start);
53348
53943
  else if (!func_param.is_moved && has_mov_keyword) add_error(status, `Unexpected 'move' keyword for non-move parameter '${func_param.name}'`, param.start);
53349
53944
  if (has_mov_keyword && param_value && !node.swap_params?.has(i)) {
53350
- if (func_param.type.name && (is_class_type$1(func_param.type.name, status) || func_param.is_moved && is_owning_struct_type_requiring_move(func_param.type, status))) {
53945
+ if (func_param.type.name && (is_class_type$1(func_param.type.name, status) || func_param.is_moved && is_owning_struct_type(func_param.type, status))) {
53351
53946
  if (!status.moved_variables) status.moved_variables = /* @__PURE__ */ new Set();
53352
53947
  status.moved_variables.add(param_value);
53353
53948
  }
@@ -53358,7 +53953,7 @@ function check_function_call(node, status, func, target_type, self_value, self_p
53358
53953
  if (field_type.name && is_class_type$1(field_type.name, status)) {
53359
53954
  const field_name = access.access.name;
53360
53955
  add_error(status, `cannot move '${field_name}' out of struct — struct owns its class fields`, param.start);
53361
- } else if (field_type.name && !field_type.is_nullable && is_owning_struct_type_requiring_move(field_type, status)) {
53956
+ } else if (field_type.name && !field_type.is_nullable && (is_owning_struct_type_requiring_move(field_type, status) || param_transfers && is_pass_by_value_owning_struct(field_type, status))) {
53362
53957
  const field_name = access.access.name;
53363
53958
  add_error(status, `cannot move '${field_name}' out of struct by value — it owns heap resources; use 'move ... swap <replacement>' to revalidate the field`, param.start);
53364
53959
  }
@@ -54731,14 +55326,17 @@ function check_assignment_node(assign, status) {
54731
55326
  return_type: fd?.func_return_type
54732
55327
  };
54733
55328
  }
54734
- } else if (lhs_value?.func_params?.length) lambda_signature = {
55329
+ } else if (lhs_value?.func_params !== void 0) lambda_signature = {
54735
55330
  params: lhs_value.func_params,
54736
55331
  return_type: lhs_value.func_return_type
54737
55332
  };
54738
- if (assign.right_value.node_type === "func" && lambda_signature.params && lambda_signature.params.length) {
55333
+ if (assign.right_value.node_type === "func" && Array.isArray(lambda_signature.params)) {
55334
+ if (!lhs_value && assign.left_value.node_type === "value") {
55335
+ add_error(status, `cannot assign a lambda to '${lhs_value_name}' — the binding was initialized with a capture-free lambda and has no reassignable slot; declare it with '= null' (or from a named function) to reassign`, assign.right_value.start);
55336
+ return false;
55337
+ }
54739
55338
  const rhs_func = assign.right_value;
54740
- if (assign.left_value.node_type === "value") rhs_func.name = lhs_value_name;
54741
- else if (!rhs_func.name) synthesize_lambda_name(rhs_func, status);
55339
+ if (!rhs_func.name || assign.left_value.node_type === "value") synthesize_lambda_name(rhs_func, status);
54742
55340
  if (rhs_func.params.length === lambda_signature.params.length) {
54743
55341
  for (let i = 0; i < rhs_func.params.length; i++) if (!rhs_func.params[i].type.name && lambda_signature.params[i].type.name) rhs_func.params[i].type = lambda_signature.params[i].type;
54744
55342
  }
@@ -55970,6 +56568,8 @@ function check_spawn_node(node, status) {
55970
56568
  add_error(status, "Spawned call did not resolve", node.start);
55971
56569
  return false;
55972
56570
  }
56571
+ const spawned = node.call.resolved_function;
56572
+ if (spawned) spawned.address_escaped = true;
55973
56573
  for (const param of node.call.params) {
55974
56574
  let arg_type = type_from_value_node(param, status);
55975
56575
  const stamped = param.type;
@@ -56787,13 +57387,14 @@ function parse_type(status) {
56787
57387
  type.type_args = void 0;
56788
57388
  }
56789
57389
  if (type.name === "Func" && type.type_args?.length) {
56790
- if (type.is_nullable || type.is_array || type.is_pointer || type.is_view || type.is_ref) {
56791
- add_error(status, `Func<...> cannot be combined with '?'/'[]'/'ptr'/'view'/'ref'`, start);
57390
+ if (type.is_array || type.is_pointer || type.is_view || type.is_ref) {
57391
+ add_error(status, `Func<...> cannot be combined with '[]'/'ptr'/'view'/'ref'`, start);
56792
57392
  return type;
56793
57393
  }
56794
57394
  const args = type.type_args;
56795
57395
  const func_type = new Type("func");
56796
57396
  func_type.start = type.start;
57397
+ if (type.is_nullable) func_type.is_nullable = true;
56797
57398
  func_type.func_params = [];
56798
57399
  for (let i = 0; i < args.length - 1; i++) {
56799
57400
  if (args[i].name === "void") {
@@ -56906,10 +57507,14 @@ function default_visibility(status) {
56906
57507
  * - a func RETURN carries its signature on the Type
56907
57508
  * (`type.func_params` / `type.func_return_type`).
56908
57509
  */
56909
- /** Whether the current token starts a nested func type: `func` immediately
56910
- * followed by `(` (the bare word `func` alone is the plain marker type). */
57510
+ /** Whether the current token starts a nested func type: `func` (optionally
57511
+ * nullable, `func?`) immediately followed by `(` (the bare word `func` alone
57512
+ * is the plain marker type). */
56911
57513
  function at_func_type(status) {
56912
- return peek_current(status) === "func" && status.tokens[status.i + 1]?.value === "(";
57514
+ if (peek_current(status) !== "func") return false;
57515
+ const next = status.tokens[status.i + 1]?.value;
57516
+ if (next === "(") return true;
57517
+ return next === "?" && status.tokens[status.i + 2]?.value === "(";
56913
57518
  }
56914
57519
  /**
56915
57520
  * Reject `void` where it has no meaning. Only the last type argument of
@@ -56930,6 +57535,7 @@ function reject_void_type(type, status, at) {
56930
57535
  function adopt_func_param_type(param, type) {
56931
57536
  if (type.name !== "func") return false;
56932
57537
  param.type = new Type("func");
57538
+ param.type.is_nullable = type.is_nullable;
56933
57539
  param.func_params = type.func_params;
56934
57540
  param.func_return_type = type.func_return_type;
56935
57541
  return true;
@@ -56952,11 +57558,13 @@ function parse_func_type_signature(param, status) {
56952
57558
  }
56953
57559
  /**
56954
57560
  * Parse a full nested func type — the `func` word is consumed by the caller
56955
- * (at_func_type confirmed the `(` follows) — and return it as a Type whose
56956
- * func_params/func_return_type carry the signature.
57561
+ * (at_func_type confirmed the `(` follows, possibly after a `?`) — and return
57562
+ * it as a Type whose func_params/func_return_type carry the signature. A
57563
+ * `func?` spelling marks the type nullable (it may hold `null`).
56957
57564
  */
56958
57565
  function parse_func_type(status) {
56959
57566
  const type = new Type("func");
57567
+ if (accept("?", status)) type.is_nullable = true;
56960
57568
  const param = new ParameterNode(get_index(status), "");
56961
57569
  parse_func_type_signature(param, status);
56962
57570
  type.func_params = param.func_params;
@@ -56996,6 +57604,7 @@ function parse_signature_params(params, status) {
56996
57604
  if (at_func_type(status)) {
56997
57605
  consume(status);
56998
57606
  param.type = new Type("func");
57607
+ if (accept("?", status)) param.type.is_nullable = true;
56999
57608
  parse_func_type_signature(param, status);
57000
57609
  } else {
57001
57610
  param.type = parse_type(status);
@@ -57036,7 +57645,11 @@ function parse_declaration(visibility, declaration, status, is_readonly = false)
57036
57645
  if (is_func_alias) {
57037
57646
  decl.func_params = decl.type.func_params ?? [];
57038
57647
  decl.func_return_type = decl.type.func_return_type;
57039
- decl.type = new Type("");
57648
+ if (decl.type.is_nullable) {
57649
+ const nullable_marker = new Type("func");
57650
+ nullable_marker.is_nullable = true;
57651
+ decl.type = nullable_marker;
57652
+ } else decl.type = new Type("");
57040
57653
  }
57041
57654
  if (peek_current(status) === "=" || status.i >= status.tokens.length) {
57042
57655
  const modifier = status.tokens[saved_i]?.value;
@@ -57098,6 +57711,7 @@ function parse_declaration(visibility, declaration, status, is_readonly = false)
57098
57711
  }
57099
57712
  function parse_function_type_declaration(decl, status) {
57100
57713
  accept("func", status);
57714
+ const func_nullable = accept("?", status);
57101
57715
  if (expect("(", status)) {
57102
57716
  const params = [];
57103
57717
  let return_type;
@@ -57110,6 +57724,10 @@ function parse_function_type_declaration(decl, status) {
57110
57724
  decl.name = consume_name(status);
57111
57725
  decl.func_params = params.filter((p) => !p.type.is_return_type);
57112
57726
  decl.func_return_type = return_type;
57727
+ if (func_nullable) {
57728
+ decl.type = new Type("func");
57729
+ decl.type.is_nullable = true;
57730
+ }
57113
57731
  const has_equals = accept("=", status);
57114
57732
  if (peek_current(status) === "{") {
57115
57733
  const func = new FunctionNode(get_index(status), default_visibility(status), decl.name, new Type(""));
@@ -57209,6 +57827,7 @@ function parse_anon_function_parameter(func, status) {
57209
57827
  if (at_func_type(status)) {
57210
57828
  consume(status);
57211
57829
  param.type = new Type("func");
57830
+ if (accept("?", status)) param.type.is_nullable = true;
57212
57831
  parse_func_type_signature(param, status);
57213
57832
  param.name_start = get_index(status);
57214
57833
  param.name = consume_name(status);
@@ -58142,6 +58761,7 @@ function parse_function_parameter(parent, func, status) {
58142
58761
  if (at_func_type(status)) {
58143
58762
  consume(status);
58144
58763
  param.type = new Type("func");
58764
+ if (accept("?", status)) param.type.is_nullable = true;
58145
58765
  parse_func_type_signature(param, status);
58146
58766
  param.name_start = get_index(status);
58147
58767
  param.name = consume_name(status);
@@ -59500,7 +60120,8 @@ nomen test
59500
60120
  function find_bundled(filename) {
59501
60121
  const here = path.dirname(fileURLToPath(import.meta.url));
59502
60122
  const cli_root = path.dirname(here);
59503
- const candidates = [path.join(cli_root, filename), path.join(cli_root, "..", filename)];
60123
+ const repo_root = path.join(cli_root, "..");
60124
+ 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)];
59504
60125
  for (const c of candidates) if (fs.existsSync(c)) return path.resolve(c);
59505
60126
  }
59506
60127
  /**
@@ -59722,6 +60343,10 @@ function parse_records(stdout) {
59722
60343
  other
59723
60344
  };
59724
60345
  }
60346
+ /** Pull the exit-time audit "LEAK: N allocation(s)" lines out of stdout. */
60347
+ function extract_leaks(stdout) {
60348
+ return stdout.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("LEAK:"));
60349
+ }
59725
60350
  /**
59726
60351
  * Compile one `*.test.nm` file (with the generated harness) and run it,
59727
60352
  * returning its parsed records and any crash info.
@@ -59737,6 +60362,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
59737
60362
  fails: [],
59738
60363
  benches: [],
59739
60364
  other: [],
60365
+ leaks: [],
59740
60366
  ms: 0
59741
60367
  };
59742
60368
  const resolved = path.resolve(entry_path);
@@ -59834,9 +60460,11 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
59834
60460
  result.fails = records.fails;
59835
60461
  result.benches = records.benches;
59836
60462
  result.other = records.other;
60463
+ result.leaks = audit ? extract_leaks(runStdout) : [];
60464
+ if (result.leaks.length) result.other = result.other.filter((l) => !l.trim().startsWith("LEAK:"));
59837
60465
  result.crashed = crashed;
59838
60466
  if (crashed) result.phase = "run";
59839
- result.ok = !(result.fails.length > 0 || result.tests.some((t) => t.failed > 0)) && crashed === void 0;
60467
+ result.ok = !(result.fails.length > 0 || result.tests.some((t) => t.failed > 0)) && crashed === void 0 && result.leaks.length === 0;
59840
60468
  result.ms = performance.now() - start;
59841
60469
  return result;
59842
60470
  }
@@ -59873,6 +60501,7 @@ function report_file(result) {
59873
60501
  console.log(` ${mark} ${rel} ${C.dim(`(${totalTests} tests)`)} ${C.dim(format_duration(result.ms))}`);
59874
60502
  if (result.crashed) console.log(C.red(` ${result.crashed}`));
59875
60503
  for (const f of result.fails) console.log(` ${C.red("✗")} ${f.test} ${C.dim(">")} ${f.message}`);
60504
+ for (const leak of result.leaks) console.log(` ${C.yellow("⚠")} ${C.yellow(leak)}`);
59876
60505
  for (const b of result.benches) console.log(` ${C.cyan("⏱")} ${b.label} ${C.dim(`(n=${b.n})`)} ${C.dim("min")} ${fmt_ns(b.min)} ${C.dim("median")} ${fmt_ns(b.median)} ${C.dim("mean")} ${fmt_ns(Math.round(b.mean))} ${C.dim("max")} ${fmt_ns(b.max)} ${C.dim("±")} ${fmt_ns(Math.round(b.stddev))}`);
59877
60506
  if (result.other.length) {
59878
60507
  console.log(C.dim(" --- test stdout ---"));
@@ -59914,10 +60543,12 @@ function runTests(root, options = {}) {
59914
60543
  const totalTests = results.reduce((a, r) => a + r.tests.length, 0);
59915
60544
  const totalFailed = results.reduce((a, r) => a + r.tests.reduce((x, t) => x + t.failed, 0), 0);
59916
60545
  const totalPassed = totalTests - totalFailed;
60546
+ const totalLeaks = results.reduce((a, r) => a + r.leaks.length, 0);
59917
60547
  const anyFailed = results.some((r) => !r.ok);
59918
60548
  console.log("");
59919
60549
  console.log(` ${C.dim("Files ")} ${C.green(`${totalFilesPassed} passed`)}` + (totalFilesFailed ? ` ${C.dim("|")} ${C.red(`${totalFilesFailed} failed`)}` : "") + C.dim(` (${totalFiles})`));
59920
60550
  console.log(` ${C.dim("Tests ")} ${C.green(`${totalPassed} passed`)}` + (totalFailed ? ` ${C.dim("|")} ${C.red(`${totalFailed} failed`)}` : "") + C.dim(` (${totalTests})`));
60551
+ if (totalLeaks) console.log(` ${C.dim("Leaks ")} ${C.yellow(`${totalLeaks} reported`)}` + C.dim(" (--audit; leaked files count as failures)"));
59921
60552
  console.log(` ${C.dim(" Time ")} ${format_duration(elapsed)}`);
59922
60553
  console.log("");
59923
60554
  return !anyFailed;
@@ -60137,7 +60768,7 @@ function processFolder(folder, config, mode, program_args) {
60137
60768
  const dir = fs.opendirSync(folder);
60138
60769
  let dirent;
60139
60770
  while ((dirent = dir.readSync()) !== null) if (shouldProcessFile(dirent.name)) {
60140
- processFile(path.join(folder, dirent.name), config, mode, program_args, false);
60771
+ processFile(path.join(folder, dirent.name), config, mode, program_args);
60141
60772
  fs.watch;
60142
60773
  }
60143
60774
  dir.closeSync();
@@ -60145,45 +60776,15 @@ function processFolder(folder, config, mode, program_args) {
60145
60776
  function shouldProcessFile(filename) {
60146
60777
  return path.extname(filename) === SUPPORTED_EXTENSION;
60147
60778
  }
60148
- /**
60149
- * The project's entry file for `file_path`: walk up to the nearest
60150
- * `package.jsonc` and resolve its `entry` field, so any file inside a project
60151
- * is checked with the entry's import context (`import types` resolves
60152
- * against the project's `src/`, not the file's own folder). Returns
60153
- * undefined when the file is standalone, IS the entry, sits in an
60154
- * entry-less project (e.g. a library checkout), or the config is malformed.
60155
- */
60156
- function project_entry_for(file_path) {
60157
- let dir = path.dirname(file_path);
60158
- for (let i = 0; i < 20; i++) {
60159
- const config_path = path.join(dir, "package.jsonc");
60160
- if (fs.existsSync(config_path)) {
60161
- try {
60162
- const json = fs.readFileSync(config_path, "utf8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
60163
- const parsed = JSON.parse(json);
60164
- if (parsed.entry) {
60165
- const entry = path.resolve(dir, parsed.entry);
60166
- if (entry !== file_path && fs.existsSync(entry)) return entry;
60167
- }
60168
- } catch {}
60169
- return;
60170
- }
60171
- const parent = path.dirname(dir);
60172
- if (parent === dir) return void 0;
60173
- dir = parent;
60174
- }
60175
- }
60176
- function processFile(filename, config, mode, program_args, anchor_to_project = true) {
60779
+ function processFile(filename, config, mode, program_args) {
60177
60780
  console.log("Processing", filename);
60178
60781
  const arch = config.arch || "aarch64";
60179
60782
  const platform = config.platform || default_platform();
60180
60783
  const resolved = path.resolve(filename);
60181
60784
  if (!config.lib) config.lib = resolve_lib(resolved);
60182
- const anchor = anchor_to_project && !resolved.endsWith(".test.nm") ? project_entry_for(resolved) ?? resolved : resolved;
60183
- if (anchor !== resolved) console.log("Anchored to project entry", anchor);
60184
60785
  let startTime = performance.now();
60185
- const input = join$1(anchor, config.lib);
60186
- const parsed = parse(input, config.lib ? get_library(config.lib) : void 0, anchor);
60786
+ const input = join$1(resolved, config.lib);
60787
+ const parsed = parse(input, config.lib ? get_library(config.lib) : void 0, resolved);
60187
60788
  let errors = parsed.errors;
60188
60789
  if (errors.length) {
60189
60790
  console.log(render_errors(input, errors));