nomen-lang 0.6.2 → 0.6.4

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
@@ -6352,6 +6352,12 @@ var AccessFieldNode = class extends BaseNode {
6352
6352
  destructure_index;
6353
6353
  /** `[field = new_name]` rename form — struct/class targets only. */
6354
6354
  is_destructure_rename;
6355
+ /** `move` binding (`var [move a, field = move n] = expr`): the binding
6356
+ * TAKES OWNERSHIP of the field. The checker synthesizes the
6357
+ * revalidating swap (a fresh default of the field's type) once the
6358
+ * field resolves, so the build reuses the plain
6359
+ * `move field swap <replacement>` pipeline unchanged. */
6360
+ destructure_move;
6355
6361
  constructor(start, name, type) {
6356
6362
  super("access_field", start);
6357
6363
  this.name = name;
@@ -6538,6 +6544,18 @@ var FunctionCallNode = class extends BaseNode {
6538
6544
  ref_param_indices;
6539
6545
  move_param_indices;
6540
6546
  /**
6547
+ * Indices of arguments bound to a `move T` parameter where T is a
6548
+ * pass-by-value owning struct on a registered free function: ownership
6549
+ * TRANSFERS at the boundary (the callee seeds the param's string-field
6550
+ * records and frees them at its exit), so the caller drops its records
6551
+ * instead of freeing them. The plain `move_param_indices` convention
6552
+ * (callee deep-copies, caller frees the originals) still governs
6553
+ * container stores and every non-transfer shape. Stamped by
6554
+ * check_function_call alongside owned_value_param_indices.
6555
+ */
6556
+ move_owned_param_indices;
6557
+ owned_value_param_indices;
6558
+ /**
6541
6559
  * Indices of arguments whose corresponding callee parameter is a nullable
6542
6560
  * struct value type (`T?` where T is a non-class struct). Populated during
6543
6561
  * the check pass when the callee's signature is resolved. The build uses
@@ -6736,6 +6754,14 @@ var ValueNode = class extends BaseNode {
6736
6754
  * name. Mirrors FunctionCallNode.resolved_function.
6737
6755
  */
6738
6756
  resolved_function;
6757
+ /**
6758
+ * Stamped by the literal-element last-use pass (stamp_literal_element_moves):
6759
+ * this bare variable is a bracket-literal element (`[t, c]`) at its last
6760
+ * use, so the checker's tuple-element ownership rule treats it as an
6761
+ * inferred `move` (ownership transfers; the source's cleanup is
6762
+ * suppressed) instead of rejecting the aliasing copy.
6763
+ */
6764
+ literal_last_use_move;
6739
6765
  constructor(start, value, type) {
6740
6766
  super("value", start);
6741
6767
  this.value = value;
@@ -6958,6 +6984,12 @@ var FunctionNode = class extends BaseNode {
6958
6984
  * are dual-natured: called under their own name AND passed as values).
6959
6985
  */
6960
6986
  is_closure;
6987
+ /** The function's address escapes into a func-typed value (bound to a
6988
+ * `var func …` variable/field, passed as a func value, …) — so it may be
6989
+ * invoked through a pointer with a BORROWED argument: pass-by-value
6990
+ * normalization (owned-param record seeding / call-site shell
6991
+ * materialization) must not apply. Set during check. */
6992
+ address_escaped;
6961
6993
  /**
6962
6994
  * Captured outer locals (CLOSURE.md Phase 2). One entry per variable the
6963
6995
  * lambda body references from an enclosing function; the backends emit an
@@ -7335,6 +7367,13 @@ var DeclarationNode = class extends BaseNode {
7335
7367
  * struct layout. Set at check time by check_function_call; consumed by the
7336
7368
  * build backends' declaration emitters. */
7337
7369
  is_heap_array_literal;
7370
+ /** True for a root `const string[] = [...]` declaration: Mach-O arm64
7371
+ * forbids pointer relocations in data sections, so the file-scope data
7372
+ * emission lays out only the length word + zeroed element space, and the
7373
+ * declaration is collected into main's prologue to store the rodata
7374
+ * label addresses at runtime. Set by the aarch64 build_block_node root
7375
+ * scan; consumed by build_declaration_node's rebuild inside main. */
7376
+ global_runtime_init;
7338
7377
  /** True for a hoisted call-argument temp (`_param_N`) that is a heap COPY
7339
7378
  * of a stack-array local, bound to a heap `Array<T>` param (non-`ref`).
7340
7379
  * `value` is the source ValueNode; the temp is materialised as a heap
@@ -7366,6 +7405,16 @@ var DeclarationNode = class extends BaseNode {
7366
7405
  * paths strdup the initializer and free the temp's copy at scope exit.
7367
7406
  * Set at build time by hoist_field_overrides. */
7368
7407
  force_owned_string;
7408
+ /** True for an OWNED value-struct PARAMETER whose string fields the
7409
+ * callee frees at scope exit: the field accesses (and the record-free
7410
+ * emitted by free_scoped_declarations) go through the param POINTER
7411
+ * (`p->field`) instead of value syntax (`p.field`). Set at build time
7412
+ * when the function builder seeds pass-by-value struct params. */
7413
+ string_fields_via_pointer;
7414
+ /** True alongside string_fields_via_pointer when the caller materialized
7415
+ * a heap shell for the argument (pass-by-value): the callee frees the
7416
+ * shell itself at scope exit (`free(p)`) after freeing the fields. */
7417
+ owned_heap_shell;
7369
7418
  constructor(start, visibility, declaration, name, type, value) {
7370
7419
  super("declare", start);
7371
7420
  this.visibility = visibility;
@@ -7986,6 +8035,38 @@ function hoist_field_overrides(value, build, status, declaration_terminator = ""
7986
8035
  carrier._fov_hoisted = true;
7987
8036
  }
7988
8037
  //#endregion
8038
+ //#region ../src/build_common/has_string_fields.ts
8039
+ /**
8040
+ * Whether any field (recursively, through nested owning value structs)
8041
+ * is an owned `string`. Shared by BOTH backends' owning-Buffer element
8042
+ * specialization (the slot deep-copies strdup'd strings, so a
8043
+ * string-field element is owning even when `struct_needs_destroy`
8044
+ * ignores string-only locals) — the C comment in
8045
+ * owning_buffer_specialize records the divergence this once caused.
8046
+ * `ref` and `view T` fields are skipped: a ref aliases the caller's
8047
+ * storage and a view's byte copy owns nothing.
8048
+ */
8049
+ function has_string_fields(node, status) {
8050
+ for (const field of node.fields) {
8051
+ if (field.type.is_ref) continue;
8052
+ if (field.type.is_view) continue;
8053
+ if (field.type.name === "string" && !field.type.is_array) return true;
8054
+ const field_struct = status.structs.find((s) => s.name === field.type.name && !s.is_simple_type && !s.is_generic);
8055
+ if (field_struct && !field_struct.is_class && has_string_fields(field_struct, status)) return true;
8056
+ }
8057
+ return false;
8058
+ }
8059
+ /**
8060
+ * The DIRECT `string` fields of a value struct (nested struct fields are not
8061
+ * descended into — nested string fields are outside the per-field ownership
8062
+ * tracking everywhere else too). Used by the return-boundary normalization:
8063
+ * the returned struct's unrecorded fields are strdup'd so the value the
8064
+ * caller receives is uniformly heap-owned.
8065
+ */
8066
+ function direct_string_fields(node) {
8067
+ return node.fields.filter((f) => !f.type.is_ref && !f.type.is_view && !f.type.is_array && f.type.name === "string");
8068
+ }
8069
+ //#endregion
7989
8070
  //#region ../src/build_common/string_return_analysis.ts
7990
8071
  const EMPTY_SET = /* @__PURE__ */ new Set();
7991
8072
  /**
@@ -8329,6 +8410,69 @@ function is_owned_string_branch_value(node, table) {
8329
8410
  return false;
8330
8411
  }
8331
8412
  //#endregion
8413
+ //#region ../src/build_common/call_init_string_fields.ts
8414
+ /**
8415
+ * Whether a declaration-initializer expression is a value the callee's
8416
+ * return-boundary normalization made UNIFORMLY heap-owned, so the fresh
8417
+ * binding records every string field and frees them at scope exit:
8418
+ *
8419
+ * - a plain function call registered as a normalized struct returner
8420
+ * (the registry is populated at the callee's return sites — a callee
8421
+ * built later, or one that returns slot borrows, is not registered and
8422
+ * the binding keeps the pre-existing borrow behavior);
8423
+ * - an owned accessor (`pop`/`move_T`/`copy`) — move-out semantics
8424
+ * transfer slot-owned heap buffers to the binding.
8425
+ *
8426
+ * NOT normalized (never recorded): struct constructions (`Info(...)` — its
8427
+ * `#init` leaves rodata borrows and bypasses the return path) and container
8428
+ * BORROW accessors (`.at`/`.first`/`.slice`/`load` — the slot keeps
8429
+ * ownership; recording would free the container's storage).
8430
+ */
8431
+ function is_normalized_struct_call(value, status) {
8432
+ if (!value) return false;
8433
+ if (value.node_type === "func_call") {
8434
+ const call = value;
8435
+ return !!status.normalized_struct_returners?.has(call.name);
8436
+ }
8437
+ if (value.node_type === "access") {
8438
+ const access = value.access;
8439
+ if (access?.node_type !== "access_func") return false;
8440
+ const name = access.name ?? "";
8441
+ if (is_container_borrow_accessor_name(name)) return false;
8442
+ if (status.normalized_struct_returners?.has(name)) return true;
8443
+ const target = value.target;
8444
+ const receiver_type = target ? target.type ?? void 0 : void 0;
8445
+ if (receiver_type?.name) {
8446
+ const label = `${mono_type_name(receiver_type)}_${name}`;
8447
+ if (status.normalized_struct_returners?.has(label)) return true;
8448
+ }
8449
+ return false;
8450
+ }
8451
+ return false;
8452
+ }
8453
+ /**
8454
+ * Record every direct string field of a call-initialized value-struct
8455
+ * binding (`var Info i = make()`) as heap-owned: the callee's return
8456
+ * normalization guarantees each field is a heap buffer, so scope-exit
8457
+ * auto_free must free them, and a later `i.field = …` write sees
8458
+ * old_was_heap and reclaims the displaced buffer instead of leaking it.
8459
+ * No-op for non-struct, class, view, and array types, for structs without
8460
+ * direct string fields, and for non-call initializers.
8461
+ */
8462
+ function record_call_init_string_fields(node, status) {
8463
+ if (!is_normalized_struct_call(node.value, status)) return;
8464
+ const type = node.type;
8465
+ if (!type?.name) return;
8466
+ const mono_name = mono_type_name(type);
8467
+ if (mono_name.startsWith("_Tuple_")) return;
8468
+ const struct = status.structs.find((s) => s.name === mono_name && !s.is_simple_type);
8469
+ if (!struct || struct.is_class) return;
8470
+ const fields = direct_string_fields(struct);
8471
+ if (!fields.length) return;
8472
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
8473
+ for (const field of fields) status.heap_string_fields.add(`${node.name}.${field.name}`);
8474
+ }
8475
+ //#endregion
8332
8476
  //#region ../src/nodes/child_nodes.ts
8333
8477
  /** Whether `value` is an AST node (carries a string `node_type`). */
8334
8478
  function is_ast_node(value) {
@@ -8445,7 +8589,21 @@ var Walk = class {
8445
8589
  reads = [];
8446
8590
  declares = [];
8447
8591
  assignments = [];
8592
+ literals = [];
8448
8593
  loop_stack = [];
8594
+ literal(node, names, body) {
8595
+ const loop_ranges = [...this.loop_stack];
8596
+ const enter = ++this.stamp;
8597
+ body();
8598
+ const exit = ++this.stamp;
8599
+ this.literals.push({
8600
+ node,
8601
+ enter,
8602
+ exit,
8603
+ loop_ranges,
8604
+ names
8605
+ });
8606
+ }
8449
8607
  interval(body) {
8450
8608
  const enter = ++this.stamp;
8451
8609
  body();
@@ -8582,6 +8740,80 @@ function classify_declare(decl, walk, param_names, func_label) {
8582
8740
  desc: `var ${node.name} = ${source}`
8583
8741
  };
8584
8742
  }
8743
+ /**
8744
+ * Stamps `literal_last_use_move` on bracket-literal elements (`[a, b]`) that
8745
+ * are bare `var` locals at their LAST USE: the literal then TRANSFERS
8746
+ * ownership of those locals instead of aliasing them (the checker's
8747
+ * tuple-element ownership rule consumes the stamp as an inferred `move`).
8748
+ *
8749
+ * Same conservative posture as the string pass: it may MISS a last use (the
8750
+ * element then needs an explicit `move`), never invent one — reads after the
8751
+ * literal, reads under an enclosing loop whose binding outlives the loop,
8752
+ * reads in spawn/async subtrees, any re-assignment of the source, and any
8753
+ * raw `#arch` body in the function all refuse the inference. Types are NOT
8754
+ * consulted here (this runs before checking) — the checker applies the
8755
+ * owning-type filter when consuming the stamp.
8756
+ *
8757
+ * Only the FIRST occurrence of a name per literal is stamped: two elements
8758
+ * moving the same local would alias each other inside the tuple.
8759
+ */
8760
+ function stamp_literal_element_moves(root) {
8761
+ let count = 0;
8762
+ const visit_functions = (node) => {
8763
+ if (node.node_type === "func") count += scan_function_literal_element_moves(node);
8764
+ for (const child of child_nodes(node)) visit_functions(child);
8765
+ };
8766
+ visit_functions(root);
8767
+ return count;
8768
+ }
8769
+ function scan_function_literal_element_moves(func) {
8770
+ if (!func.statements?.length) return 0;
8771
+ const walk = new Walk();
8772
+ const param_names = new Set(func.params.map((p) => p.name));
8773
+ for (const stmt of func.statements) walk_stmt(walk, stmt);
8774
+ if (walk.has_raw) return 0;
8775
+ let count = 0;
8776
+ for (const site of walk.literals) {
8777
+ const eligible = [];
8778
+ for (const name of site.names) {
8779
+ if (eligible.includes(name)) continue;
8780
+ if (param_names.has(name)) continue;
8781
+ const binding = walk.declares.filter((d) => d.name === name && d.enter < site.enter).at(-1);
8782
+ if (!binding || binding.declaration !== "var") continue;
8783
+ if (walk.writes.filter((w) => w.name === name).length > 1) continue;
8784
+ let killed = false;
8785
+ for (const r of walk.reads) {
8786
+ if (r.name !== name) continue;
8787
+ if (r.opaque) {
8788
+ killed = true;
8789
+ break;
8790
+ }
8791
+ if (r.enter > site.exit) {
8792
+ killed = true;
8793
+ break;
8794
+ }
8795
+ if (r.enter >= site.enter && r.enter <= site.exit) continue;
8796
+ for (const loop of site.loop_ranges) if (r.enter >= loop.enter && r.enter <= loop.exit) {
8797
+ if (!(binding.enter >= loop.enter && binding.exit <= loop.exit)) {
8798
+ killed = true;
8799
+ break;
8800
+ }
8801
+ }
8802
+ if (killed) break;
8803
+ }
8804
+ if (!killed) eligible.push(name);
8805
+ }
8806
+ const values = site.node.values;
8807
+ for (const v of values) {
8808
+ const vn = v;
8809
+ if (vn.node_type === "value" && typeof vn.value === "string" && eligible.includes(vn.value)) {
8810
+ vn.literal_last_use_move = true;
8811
+ count++;
8812
+ }
8813
+ }
8814
+ }
8815
+ return count;
8816
+ }
8585
8817
  function walk_stmt(walk, node) {
8586
8818
  switch (node.node_type) {
8587
8819
  case "declare": {
@@ -8718,7 +8950,10 @@ function walk_expr$1(walk, node) {
8718
8950
  }
8719
8951
  case "array": {
8720
8952
  const arr = node;
8721
- for (const v of arr.values) walk_expr$1(walk, v);
8953
+ const names = arr.values.map((v) => v).filter((v) => v.node_type === "value" && typeof v.value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(v.value)).map((v) => v.value);
8954
+ walk.literal(node, names, () => {
8955
+ for (const v of arr.values) walk_expr$1(walk, v);
8956
+ });
8722
8957
  return;
8723
8958
  }
8724
8959
  default: for (const child of child_nodes(node)) walk_expr$1(walk, child);
@@ -10913,6 +11148,85 @@ function array_struct_name(type, status) {
10913
11148
  return status.structs.find((s) => s.name === mono && !s.is_generic) ? mono : void 0;
10914
11149
  }
10915
11150
  //#endregion
11151
+ //#region ../src/build_common/destroy_analysis.ts
11152
+ /**
11153
+ * Whether any field of the struct is an enum-with-data value owning heap
11154
+ * payloads (a string payload — always heap, case construction strdups it —
11155
+ * or a class/trait reference payload — construction transfers ownership).
11156
+ * Such a field must be reclaimed when the struct dies.
11157
+ */
11158
+ function struct_has_owning_enum_field(struct, table) {
11159
+ for (const field of struct.fields) {
11160
+ if (field.type.is_ref) continue;
11161
+ const en = table.enums?.find((e) => e.name === field.type.name);
11162
+ if (!en?.has_associated_data) continue;
11163
+ for (const c of en.cases) for (const p of c.params) {
11164
+ if (p.type.name === "string" && !p.type.is_array) return true;
11165
+ if (table.traits?.some((t) => t.name === p.type.name)) return true;
11166
+ if (table.structs.find((s) => s.name === p.type.name)?.is_class) return true;
11167
+ }
11168
+ }
11169
+ return false;
11170
+ }
11171
+ /**
11172
+ * Whether a struct declares its own `#destroy` (user side effects /
11173
+ * resource release).
11174
+ */
11175
+ function has_destroy(struct) {
11176
+ return !!struct.functions.find((f) => f.name === "#destroy");
11177
+ }
11178
+ /**
11179
+ * Whether any field (recursively) requires destroy handling — a class
11180
+ * field (heap instance to reclaim) or a nested struct with its own
11181
+ * `#destroy` (or nested fields that do). Deliberately excludes `string`
11182
+ * fields: a struct local whose only owning fields are strings is NOT
11183
+ * auto-destroyed at scope exit (the strings may be raw rodata arg
11184
+ * pointers, not heap) — see `struct_needs_auto_destroy` for the broader
11185
+ * form used to decide whether a `<Struct>_destroy` function is generated.
11186
+ */
11187
+ function has_struct_fields_with_destroy(struct, table) {
11188
+ for (const field of struct.fields) {
11189
+ if (field.type.is_ref) continue;
11190
+ const field_struct = resolve_struct_type(field.type, table);
11191
+ if (!field_struct) continue;
11192
+ if (field_struct.is_class) return true;
11193
+ if (has_destroy(field_struct)) return true;
11194
+ if (has_struct_fields_with_destroy(field_struct, table)) return true;
11195
+ }
11196
+ return false;
11197
+ }
11198
+ /**
11199
+ * Whether a struct (or any embedded struct field, recursively) needs a
11200
+ * destroy call at scope exit — it has a `#destroy`, a class-typed field,
11201
+ * or a nested struct field that itself needs destroying. This does NOT
11202
+ * count `string` fields (see `has_struct_fields_with_destroy`).
11203
+ */
11204
+ function struct_needs_destroy(struct, table) {
11205
+ return has_destroy(struct) || has_struct_fields_with_destroy(struct, table) || struct_has_owning_enum_field(struct, table);
11206
+ }
11207
+ /**
11208
+ * Whether a struct needs an auto-generated `<Struct>_destroy` function.
11209
+ * Broader than `struct_needs_destroy`: also returns true for structs with
11210
+ * `string` fields, because a Buffer<T> for such a struct deep-copies the
11211
+ * strings into slots (strdup on store) and the per-element destroy must
11212
+ * free them. The generated destroy frees each string field; it is called
11213
+ * from Buffer's specialized #destroy / replace_T, NOT from struct local
11214
+ * scope exit (`struct_needs_destroy` governs that path and excludes
11215
+ * strings).
11216
+ */
11217
+ function struct_needs_auto_destroy(struct, table) {
11218
+ if (struct_needs_destroy(struct, table)) return true;
11219
+ if (struct_has_owning_enum_field(struct, table)) return true;
11220
+ for (const field of struct.fields) {
11221
+ if (field.type.is_ref) continue;
11222
+ if (field.type.is_view) continue;
11223
+ if (field.type.name === "string" && !field.type.is_array) return true;
11224
+ const field_struct = resolve_struct_type(field.type, table);
11225
+ if (field_struct && !field_struct.is_class && struct_needs_auto_destroy(field_struct, table)) return true;
11226
+ }
11227
+ return false;
11228
+ }
11229
+ //#endregion
10916
11230
  //#region ../src/build_common/scan_force_heap_strings.ts
10917
11231
  function scan_force_heap_strings(statements, structs) {
10918
11232
  const result = /* @__PURE__ */ new Set();
@@ -13391,85 +13705,6 @@ function emit_strdup(status) {
13391
13705
  emit_asm(status, `ldr x1, [sp], #16\n`);
13392
13706
  }
13393
13707
  //#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
13708
  //#region ../src/build_common/temp_anchor_consolidation.ts
13474
13709
  /**
13475
13710
  * Shared OWNERSHIP decision for hoisted call-temporary consolidation.
@@ -13504,6 +13739,144 @@ function superseded_param_temp_names(table, call_node, result_type_name) {
13504
13739
  return names;
13505
13740
  }
13506
13741
  //#endregion
13742
+ //#region ../src/build_aarch64/utils/string_pair.ts
13743
+ /**
13744
+ * Load the string variable `name` into the consecutive register pair
13745
+ * (base, base+1) — default x0/x1. Handles the plain slot case; callers
13746
+ * that have register-allocated or param forms handle theirs first.
13747
+ */
13748
+ function emit_string_pair_load(status, name, base = "x0") {
13749
+ const offset = status.stack_offsets?.get(name);
13750
+ if (offset === void 0) return false;
13751
+ const w = base.startsWith("x") ? base : "x0";
13752
+ const n = parseInt(w.substring(1), 10);
13753
+ if (offset + 8 > 504) {
13754
+ emit_asm(status, `ldr ${w}, [x29, #${offset}]\n`);
13755
+ emit_asm(status, `ldr x${n + 1}, [x29, #${offset + 8}]\n`);
13756
+ return true;
13757
+ }
13758
+ emit_asm(status, `ldp ${w}, x${n + 1}, [x29, #${offset}]\n`);
13759
+ return true;
13760
+ }
13761
+ /** Load the pair at [addr_reg, #offset] into (base, base+1). */
13762
+ function emit_string_pair_load_at(status, addr_reg, offset, base = "x0") {
13763
+ const w = base.startsWith("x") ? base : "x0";
13764
+ emit_asm(status, `ldp ${w}, x${parseInt(w.substring(1), 10) + 1}, [${addr_reg}, #${offset}]\n`);
13765
+ }
13766
+ /**
13767
+ * strdup the fat string in (x0=ptr, x1=len): preserve the len half across
13768
+ * the call and leave the owned copy's pair in (x0, x1). The caller keeps
13769
+ * ownership of the original.
13770
+ */
13771
+ function emit_strdup_string(status) {
13772
+ emit_asm(status, `str x1, [sp, #-16]!\n`);
13773
+ emit_asm(status, status.audit ? `bl _nomen_strdup_wrap\n` : `bl _strdup\n`);
13774
+ emit_asm(status, `ldr x1, [sp], #16\n`);
13775
+ }
13776
+ /**
13777
+ * Pair load/store at [x29, #offset] with an arbitrary register pair —
13778
+ * splits into two single-word accesses beyond the ldp/stp +504 range.
13779
+ */
13780
+ function emit_pair_load_x29(status, offset, a = "x0", b = "x1") {
13781
+ if (offset + 8 > 504) {
13782
+ emit_asm(status, `ldr ${a}, [x29, #${offset}]\n`);
13783
+ emit_asm(status, `ldr ${b}, [x29, #${offset + 8}]\n`);
13784
+ return;
13785
+ }
13786
+ emit_asm(status, `ldp ${a}, ${b}, [x29, #${offset}]\n`);
13787
+ }
13788
+ function emit_pair_store_x29(status, offset, a = "x0", b = "x1") {
13789
+ if (offset + 8 > 504) {
13790
+ emit_asm(status, `str ${a}, [x29, #${offset}]\n`);
13791
+ emit_asm(status, `str ${b}, [x29, #${offset + 8}]\n`);
13792
+ return;
13793
+ }
13794
+ emit_asm(status, `stp ${a}, ${b}, [x29, #${offset}]\n`);
13795
+ }
13796
+ /**
13797
+ * Owning specializations for Array<string>'s raw T-generic bodies
13798
+ * (`with` / `set`), replacing the shared-pointer copies with per-slot deep
13799
+ * copies — the aarch64 analog of Array.nm's `#if T_NEEDS_STRDUP` C branch.
13800
+ *
13801
+ * Conventions (matching Array.nm raw bodies):
13802
+ * - `with` (static): x0/x1 = value pair, x2 = count. Returns x0 = heap
13803
+ * buffer ([ptr] = length, elements at +8..).
13804
+ * - `set` (method): x19 = first element, x1 = index, x2/x3 = value pair.
13805
+ * Bounds are guaranteed by the Nomen-level constraints.
13806
+ */
13807
+ function emit_owning_array_string_specialize(func_name, status, self_reg = "x19") {
13808
+ if (func_name === "with") {
13809
+ emit_asm(status, `stp x19, x20, [sp, #-16]!\n`);
13810
+ emit_asm(status, `stp x21, x22, [sp, #-16]!\n`);
13811
+ emit_asm(status, `stp x24, x25, [sp, #-16]!\n`);
13812
+ emit_asm(status, `mov x19, x0\n`);
13813
+ emit_asm(status, `mov x20, x1\n`);
13814
+ emit_asm(status, `mov x21, x2\n`);
13815
+ emit_asm(status, `add x0, x21, #1\n`);
13816
+ emit_asm(status, `lsl x0, x0, #4\n`);
13817
+ emit_malloc(status);
13818
+ emit_asm(status, `mov x22, x0\n`);
13819
+ emit_asm(status, `str x21, [x22]\n`);
13820
+ emit_asm(status, `mov x23, #8\n`);
13821
+ emit_asm(status, `mov x24, #0\n`);
13822
+ emit_asm(status, `.Larr_str_with_loop:\n`);
13823
+ emit_asm(status, `cmp x24, x21\n`);
13824
+ emit_asm(status, `b.ge .Larr_str_with_done\n`);
13825
+ emit_asm(status, `mov x0, x19\n`);
13826
+ emit_strdup(status);
13827
+ emit_asm(status, `add x25, x22, x23\n`);
13828
+ emit_asm(status, `stp x0, x20, [x25]\n`);
13829
+ emit_asm(status, `add x23, x23, #16\n`);
13830
+ emit_asm(status, `add x24, x24, #1\n`);
13831
+ emit_asm(status, `b .Larr_str_with_loop\n`);
13832
+ emit_asm(status, `.Larr_str_with_done:\n`);
13833
+ emit_asm(status, `mov x0, x22\n`);
13834
+ emit_asm(status, `ldp x24, x25, [sp], #16\n`);
13835
+ emit_asm(status, `ldp x21, x22, [sp], #16\n`);
13836
+ emit_asm(status, `ldp x19, x20, [sp], #16\n`);
13837
+ return true;
13838
+ }
13839
+ if (func_name === "at" || func_name === "first" || func_name === "at_end") {
13840
+ emit_asm(status, `stp x20, x21, [sp, #-16]!\n`);
13841
+ if (func_name === "at") {
13842
+ emit_asm(status, `lsl x9, x1, #4\n`);
13843
+ emit_asm(status, `add x9, ${self_reg}, x9\n`);
13844
+ } else if (func_name === "at_end") {
13845
+ emit_asm(status, `ldr x9, [${self_reg}, #-8]\n`);
13846
+ emit_asm(status, `sub x9, x9, #1\n`);
13847
+ emit_asm(status, `lsl x9, x9, #4\n`);
13848
+ emit_asm(status, `add x9, ${self_reg}, x9\n`);
13849
+ } else emit_asm(status, `add x9, ${self_reg}, #0\n`);
13850
+ emit_asm(status, `ldp x0, x1, [x9]\n`);
13851
+ emit_asm(status, `ldp x20, x21, [sp], #16\n`);
13852
+ return true;
13853
+ }
13854
+ if (func_name === "set") {
13855
+ emit_asm(status, `stp x20, x21, [sp, #-16]!\n`);
13856
+ emit_asm(status, `lsl x9, x1, #4\n`);
13857
+ emit_asm(status, `add x9, ${self_reg}, x9\n`);
13858
+ emit_asm(status, `ldr x0, [x9]\n`);
13859
+ emit_asm(status, `mov x20, x2\n`);
13860
+ emit_asm(status, `mov x21, x3\n`);
13861
+ emit_free(status);
13862
+ emit_asm(status, `mov x0, x20\n`);
13863
+ emit_strdup(status);
13864
+ emit_asm(status, `str x0, [x9]\n`);
13865
+ emit_asm(status, `str x21, [x9, #8]\n`);
13866
+ emit_asm(status, `ldp x20, x21, [sp], #16\n`);
13867
+ return true;
13868
+ }
13869
+ return false;
13870
+ }
13871
+ /** Pair store to [base_reg, #offset] with stur fallback (misaligned/far). */
13872
+ function emit_pair_store_to(status, base_reg, offset, a, b) {
13873
+ if (offset % 8 === 0 && offset + 8 <= 504 && offset >= 0) emit_asm(status, `stp ${a}, ${b}, [${base_reg}, #${offset}]\n`);
13874
+ else {
13875
+ emit_asm(status, `stur ${a}, [${base_reg}, #${offset}]\n`);
13876
+ emit_asm(status, `stur ${b}, [${base_reg}, #${offset + 8}]\n`);
13877
+ }
13878
+ }
13879
+ //#endregion
13507
13880
  //#region ../src/build_aarch64/utils/auto_destroy.ts
13508
13881
  /**
13509
13882
  * Whether a declaration's initializer is a non-`move` FIELD ACCESS — a shallow
@@ -13540,12 +13913,27 @@ function clear_heap_string_fields(status, var_name) {
13540
13913
  * fields are unconditionally heap and freed by the destroy path. Called from
13541
13914
  * emit_destroy_for_decl and directly from cleanup loops that skip moved
13542
13915
  * declarations before reaching it.
13916
+ *
13917
+ * For a nullable struct local (`is_nullable`), the frees are guarded on the
13918
+ * companion `_has` flag: the null representation leaves the value bytes
13919
+ * uninitialized (a `return null` callee writes only the flag), so reading a
13920
+ * field pointer without the guard frees stale stack garbage. Mirrors the C
13921
+ * backend's `if (<name>_has)` auto-free guard.
13543
13922
  */
13544
- function release_heap_string_fields(status, decl_name, decl_type_name) {
13923
+ function release_heap_string_fields(status, decl_name, decl_type_name, is_nullable) {
13545
13924
  if (!status.heap_string_fields?.size) return;
13546
13925
  const prefix = `${decl_name}.`;
13547
13926
  const fields = Array.from(status.heap_string_fields).filter((k) => k.startsWith(prefix)).map((k) => k.slice(prefix.length));
13548
13927
  if (!fields.length) return;
13928
+ let skip_label;
13929
+ if (is_nullable) {
13930
+ const flag_off = status.stack_offsets?.get(has_flag_name(decl_name));
13931
+ if (flag_off !== void 0 && flag_off >= 0) {
13932
+ skip_label = `.Lskip_rhs_${status.label_counter = (status.label_counter ?? 0) + 1}`;
13933
+ emit_asm(status, `ldr x0, [x29, #${flag_off}]\n`);
13934
+ emit_asm(status, `cbz x0, ${skip_label}\n`);
13935
+ }
13936
+ }
13549
13937
  for (const field of fields) {
13550
13938
  const offset = get_field_offset(decl_type_name, field, status);
13551
13939
  emit_var_address(status, "x0", decl_name);
@@ -13553,6 +13941,7 @@ function release_heap_string_fields(status, decl_name, decl_type_name) {
13553
13941
  emit_free(status);
13554
13942
  status.heap_string_fields.delete(`${decl_name}.${field}`);
13555
13943
  }
13944
+ if (skip_label) emit_asm(status, `${skip_label}:\n`);
13556
13945
  }
13557
13946
  /**
13558
13947
  * Swap in a fresh scoped_declarations frame for a nested scope (if/while/
@@ -13911,7 +14300,7 @@ function is_struct_type$4(type_name, status) {
13911
14300
  }
13912
14301
  function emit_destroy_for_decl(status, decl_name, decl_type_name, addr_offset, type_args, is_nullable) {
13913
14302
  const moved = status.moved ?? /* @__PURE__ */ new Set();
13914
- release_heap_string_fields(status, decl_name, decl_type_name);
14303
+ release_heap_string_fields(status, decl_name, decl_type_name, is_nullable);
13915
14304
  if (moved.has(decl_name)) return;
13916
14305
  if (decl_type_name === "func" && status.stack_offsets?.has(decl_name)) {
13917
14306
  const skip = `.Lclosure_free_done_${status.label_counter = (status.label_counter ?? 0) + 1}`;
@@ -13964,11 +14353,19 @@ function emit_destroy_for_decl(status, decl_name, decl_type_name, addr_offset, t
13964
14353
  return;
13965
14354
  }
13966
14355
  const guard_null = !!is_nullable && struct_type.is_class && addr_offset === void 0;
14356
+ const guard_has_flag = !!is_nullable && !struct_type.is_class && addr_offset === void 0;
13967
14357
  let skip_label;
13968
14358
  if (guard_null) {
13969
14359
  emit_var_load(status, "x0", decl_name, 8);
13970
14360
  skip_label = `.Lskip_nd_${status.label_counter = (status.label_counter ?? 0) + 1}`;
13971
14361
  emit_asm(status, `cbz x0, ${skip_label}\n`);
14362
+ } else if (guard_has_flag && (has_destroy(struct_type) || struct_needs_destroy(struct_type, status))) {
14363
+ const flag_off = status.stack_offsets?.get(has_flag_name(decl_name));
14364
+ if (flag_off !== void 0 && flag_off >= 0) {
14365
+ skip_label = `.Lskip_nd_${status.label_counter = (status.label_counter ?? 0) + 1}`;
14366
+ emit_asm(status, `ldr x0, [x29, #${flag_off}]\n`);
14367
+ emit_asm(status, `cbz x0, ${skip_label}\n`);
14368
+ }
13972
14369
  }
13973
14370
  if (has_destroy(struct_type) || struct_type.is_class) {
13974
14371
  if (struct_type.is_class) {
@@ -14123,13 +14520,51 @@ function resolve_decl_struct(decl, status) {
14123
14520
  }
14124
14521
  }
14125
14522
  }
14523
+ /**
14524
+ * Emit the reclaim of a nullable struct local's OLD value before a
14525
+ * reassignment (`p = <value>` / `p = null`): when the slot's `_has` flag is
14526
+ * set, free every heap string field currently recorded for it. The frees are
14527
+ * emitted reading the slot's CURRENT bytes, so this must run before the new
14528
+ * value is copied in. Records are left intact — the caller retargets them
14529
+ * for the new value (the scope-exit release still owns the final state).
14530
+ * Returns true when anything was emitted.
14531
+ */
14532
+ function emit_recorded_field_reclaim_for_slot(status, decl_name, decl_type_name) {
14533
+ if (!status.heap_string_fields?.size) return false;
14534
+ const prefix = `${decl_name}.`;
14535
+ const fields = Array.from(status.heap_string_fields).filter((k) => k.startsWith(prefix)).map((k) => k.slice(prefix.length));
14536
+ if (!fields.length) return false;
14537
+ const flag_off = status.stack_offsets?.get(has_flag_name(decl_name));
14538
+ if (flag_off === void 0 || flag_off < 0) return false;
14539
+ const skip = `.Lskip_reclaim_${status.label_counter = (status.label_counter ?? 0) + 1}`;
14540
+ emit_asm(status, `ldr x0, [x29, #${flag_off}]\n`);
14541
+ emit_asm(status, `cbz x0, ${skip}\n`);
14542
+ for (const field of fields) {
14543
+ const offset = get_field_offset(decl_type_name, field, status);
14544
+ emit_var_address(status, "x0", decl_name);
14545
+ emit_asm(status, `ldr x0, [x0, #${offset}]\n`);
14546
+ emit_free(status);
14547
+ }
14548
+ emit_asm(status, `${skip}:\n`);
14549
+ return true;
14550
+ }
14551
+ /**
14552
+ * Drop every heap-string-field record under `prefix` (a variable name plus
14553
+ * dot). Used when a slot's ownership is retargeted — the records describe
14554
+ * the OLD value, which the caller has just reclaimed or displaced.
14555
+ */
14556
+ function drop_heap_string_field_records(status, decl_name) {
14557
+ if (!status.heap_string_fields?.size) return;
14558
+ const prefix = `${decl_name}.`;
14559
+ for (const key of Array.from(status.heap_string_fields)) if (key.startsWith(prefix)) status.heap_string_fields.delete(key);
14560
+ }
14126
14561
  function emit_destroy_for_scope(status, declarations_before) {
14127
14562
  const moved = status.moved ?? /* @__PURE__ */ new Set();
14128
14563
  const current_scope = status.heap_cleanup_stack?.[status.heap_cleanup_stack.length - 1];
14129
14564
  if (current_scope?.heap_slots.length) {
14130
14565
  for (let i = declarations_before; i < status.scoped_declarations.length; i++) {
14131
14566
  const decl = status.scoped_declarations[i];
14132
- release_heap_string_fields(status, decl.name, decl.type.name);
14567
+ release_heap_string_fields(status, decl.name, decl.type.name, decl.type.is_nullable);
14133
14568
  if (moved.has(decl.name)) continue;
14134
14569
  const scope_enum = status.enums.find((e) => e.name === decl.type.name && e.has_associated_data);
14135
14570
  if (scope_enum && !decl.type.is_array) {
@@ -14172,6 +14607,18 @@ function emit_destroy_for_scope(status, declarations_before) {
14172
14607
  emit_free(status);
14173
14608
  continue;
14174
14609
  }
14610
+ const tuple_field_offsets = status.heap_string_tuple_fields?.get(decl.name);
14611
+ if (tuple_field_offsets?.length) {
14612
+ const var_offset = status.stack_offsets?.get(decl.name);
14613
+ if (var_offset !== void 0 && var_offset >= 0) {
14614
+ for (const field_offset of tuple_field_offsets) {
14615
+ emit_asm(status, `add x0, x29, #${var_offset}\n`);
14616
+ emit_asm(status, `ldr x0, [x0, #${field_offset}]\n`);
14617
+ emit_free(status);
14618
+ }
14619
+ continue;
14620
+ }
14621
+ }
14175
14622
  if (decl.type.name === "func") {
14176
14623
  emit_destroy_for_decl(status, decl.name, "func");
14177
14624
  continue;
@@ -14191,7 +14638,7 @@ function emit_destroy_for_scope(status, declarations_before) {
14191
14638
  }
14192
14639
  for (let i = declarations_before; i < status.scoped_declarations.length; i++) {
14193
14640
  const decl = status.scoped_declarations[i];
14194
- release_heap_string_fields(status, decl.name, decl.type.name);
14641
+ release_heap_string_fields(status, decl.name, decl.type.name, decl.type.is_nullable);
14195
14642
  if (moved.has(decl.name)) continue;
14196
14643
  const scope_enum = status.enums.find((e) => e.name === decl.type.name && e.has_associated_data);
14197
14644
  if (scope_enum && !decl.type.is_array) {
@@ -14265,6 +14712,18 @@ function emit_destroy_for_scope(status, declarations_before) {
14265
14712
  emit_free(status);
14266
14713
  continue;
14267
14714
  }
14715
+ const tuple_field_offsets = status.heap_string_tuple_fields?.get(decl.name);
14716
+ if (tuple_field_offsets?.length) {
14717
+ const var_offset = status.stack_offsets?.get(decl.name);
14718
+ if (var_offset !== void 0 && var_offset >= 0) {
14719
+ for (const field_offset of tuple_field_offsets) {
14720
+ emit_asm(status, `add x0, x29, #${var_offset}\n`);
14721
+ emit_asm(status, `ldr x0, [x0, #${field_offset}]\n`);
14722
+ emit_free(status);
14723
+ }
14724
+ continue;
14725
+ }
14726
+ }
14268
14727
  if (is_field_struct_borrow(decl)) continue;
14269
14728
  const resolved_decl = resolve_decl_struct(decl, status);
14270
14729
  if (!resolved_decl) continue;
@@ -14346,6 +14805,27 @@ function mark_moved_if_struct(value, status, opts) {
14346
14805
  status.moved.add(var_name);
14347
14806
  }
14348
14807
  }
14808
+ /**
14809
+ * strdup the UNRECORDED direct string fields of a value struct whose base is
14810
+ * in `base_reg` (+ base_offset) — the return-boundary normalization: the
14811
+ * sret byte-copy transfers the RECORDED (heap) fields raw, but unrecorded
14812
+ * fields may hold rodata/borrowed bytes the caller cannot free, so they are
14813
+ * deep-copied here and the returned struct is uniformly heap-owned.
14814
+ */
14815
+ function emit_string_field_strdups_at(status, struct_name, base_reg, base_offset = 0, skip_fields) {
14816
+ const struct = status.structs.find((s) => s.name === struct_name && !s.is_simple_type && !s.is_class);
14817
+ if (!struct) return;
14818
+ for (const field of direct_string_fields(struct)) {
14819
+ if (skip_fields?.has(field.name)) continue;
14820
+ const off = base_offset + get_field_offset(struct_name, field.name, status);
14821
+ emit_asm(status, `str ${base_reg}, [sp, #-16]!\n`);
14822
+ emit_string_pair_load_at(status, base_reg, off);
14823
+ emit_strdup_string(status);
14824
+ emit_asm(status, `mov x13, x0\n`);
14825
+ emit_asm(status, `ldr ${base_reg}, [sp], #16\n`);
14826
+ emit_pair_store_to(status, base_reg, off, "x13", "x1");
14827
+ }
14828
+ }
14349
14829
  //#endregion
14350
14830
  //#region ../src/build_aarch64/utils/closure_a64.ts
14351
14831
  /**
@@ -14520,13 +15000,31 @@ function materialize_func_value_a64(func, status) {
14520
15000
  const lines = [];
14521
15001
  lines.push(`.p2align 2`);
14522
15002
  lines.push(`${thunk}:`);
14523
- const reg_shift = Math.min(slots, 8);
14524
- for (let s = 0; s < reg_shift - 1; s++) lines.push(`mov x${s}, x${s + 1}`);
14525
- for (let k = 0; slots + k > 8 && k < 64; k++) {
14526
- lines.push(`ldr x9, [sp, #${(k + 1) * 8}]`);
15003
+ const normalizes_borrow_return = func.return_type?.name === "string" && !func.return_type?.is_view && !func.returns_move && func.returns_string_borrow !== false && !(status.heap_returning_functions?.has(target) || (status.heap_returning_functions?.has(func.name ?? "") ?? false));
15004
+ const V = slots - 1;
15005
+ const dest_stack_slots = Math.max(0, V - 8);
15006
+ const dest_area = normalizes_borrow_return ? Math.ceil(dest_stack_slots * 8 / 16) * 16 : 0;
15007
+ const frame_pad = normalizes_borrow_return ? dest_area + 16 : 0;
15008
+ if (normalizes_borrow_return) {
15009
+ lines.push(`stp x29, x30, [sp, #-16]!`);
15010
+ if (dest_area > 0) lines.push(`sub sp, sp, #${dest_area}`);
15011
+ }
15012
+ const reg_moves = Math.min(V, 7);
15013
+ for (let s = 0; s < reg_moves; s++) lines.push(`mov x${s}, x${s + 1}`);
15014
+ if (V >= 8) lines.push(`ldr x7, [sp, #${frame_pad}]`);
15015
+ for (let k = 0; k < V - 8; k++) {
15016
+ lines.push(`ldr x9, [sp, #${frame_pad + (k + 1) * 8}]`);
14527
15017
  lines.push(`str x9, [sp, #${k * 8}]`);
14528
15018
  }
14529
- lines.push(`b ${target}`);
15019
+ if (normalizes_borrow_return) {
15020
+ lines.push(`bl ${target}`);
15021
+ lines.push(`str x1, [sp, #-16]!`);
15022
+ lines.push(status.audit ? `bl _nomen_strdup_wrap` : `bl _strdup`);
15023
+ lines.push(`ldr x1, [sp], #16`);
15024
+ if (dest_area > 0) lines.push(`add sp, sp, #${dest_area}`);
15025
+ lines.push(`ldp x29, x30, [sp], #16`);
15026
+ lines.push(`ret`);
15027
+ } else lines.push(`b ${target}`);
14530
15028
  lines.push(``);
14531
15029
  status.closure_definitions = (status.closure_definitions ?? "") + lines.join("\n") + "\n";
14532
15030
  status.closure_definitions = (status.closure_definitions ?? "") + DATA_SECTION_OPEN + `.p2align 3\n${descriptor}:\n\t.quad ${thunk}\n\t.quad 0\n\t.quad 0\n\t.quad 0\n` + TEXT_SECTION_RESTORE;
@@ -16823,28 +17321,6 @@ function value_number_loops(body, ast_list, status, mutate_ast) {
16823
17321
  };
16824
17322
  }
16825
17323
  //#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
17324
  //#region ../src/build_aarch64/utils/owning_buffer_specialize.ts
16849
17325
  /**
16850
17326
  * Detect whether a monomorphized struct is a `Buffer_<T>` whose element type
@@ -19070,6 +19546,8 @@ function build_function_node$1(node, status) {
19070
19546
  if (check_c_fallback(node, void 0, status)) return;
19071
19547
  const old_function_name = status.current_function_name;
19072
19548
  status.current_function_name = emission_label(node);
19549
+ const old_is_closure = status.current_function_is_closure;
19550
+ status.current_function_is_closure = !!node.is_closure;
19073
19551
  status.function_nursery_depth = status.nursery_stack?.length ?? 0;
19074
19552
  const old_scoped_declarations = status.scoped_declarations;
19075
19553
  status.scoped_declarations = [];
@@ -19458,6 +19936,16 @@ function build_function_node$1(node, status) {
19458
19936
  }
19459
19937
  param_idx++;
19460
19938
  if (param.declaration === "var") status.function_param_vars.add(param.name);
19939
+ const is_free_function = (!node.scope || node.scope.node_type === "root" || node.scope.node_type === "func") && !node.params.some((p) => p.is_self_param);
19940
+ 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;
19941
+ 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)) {
19942
+ status.function_param_vars.add(param.name);
19943
+ const param_decl = new DeclarationNode(param.start, "private", "var", param.name, param.type);
19944
+ param_decl.string_fields_via_pointer = true;
19945
+ status.scoped_declarations.push(param_decl);
19946
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
19947
+ for (const field of direct_string_fields(owned_value_param)) status.heap_string_fields.add(`${param.name}.${field.name}`);
19948
+ }
19461
19949
  if (param.type.is_array) {
19462
19950
  if (!param.is_variadic ? array_struct_name(param.type, status) : void 0) {
19463
19951
  if (!status.heap_array_vars) status.heap_array_vars = /* @__PURE__ */ new Set();
@@ -19648,6 +20136,7 @@ function build_function_node$1(node, status) {
19648
20136
  status.heap_cleanup_stack = old_heap_cleanup_stack;
19649
20137
  status.trait_class_frames = old_trait_class_frames;
19650
20138
  status.current_function_name = old_function_name;
20139
+ status.current_function_is_closure = old_is_closure;
19651
20140
  status.stack_size = old_stack_size;
19652
20141
  status.stack_offsets = old_stack_offsets;
19653
20142
  status.closure_env_slot = old_closure_env_slot;
@@ -19669,144 +20158,6 @@ function build_function_node$1(node, status) {
19669
20158
  status.function_return_type = void 0;
19670
20159
  }
19671
20160
  //#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
20161
  //#region ../src/build_aarch64/build_struct_node.ts
19811
20162
  let field_strdup_guard_counter = 0;
19812
20163
  function emit_typed_store(status, src_reg, dst_base, offset, size) {
@@ -21312,7 +21663,13 @@ function build_block_node$1(node, status) {
21312
21663
  if (is_struct_node(child) || is_trait_node(child) || is_function_node(child) || child.node_type === "enum" || child.node_type === "bitset") continue;
21313
21664
  if (child.node_type === "declare") {
21314
21665
  const decl = child;
21315
- if (decl.declaration === "const") continue;
21666
+ if (decl.declaration === "const") {
21667
+ if (decl.type?.name === "string" && decl.type.is_array && decl.value?.node_type === "array") {
21668
+ decl.global_runtime_init = true;
21669
+ module_init_statements.push(child);
21670
+ }
21671
+ continue;
21672
+ }
21316
21673
  if (inlined_const_names.has(decl.name)) continue;
21317
21674
  if (is_literal_init(decl.value)) continue;
21318
21675
  module_init_statements.push(child);
@@ -21327,7 +21684,7 @@ function build_block_node$1(node, status) {
21327
21684
  heap_slots: [],
21328
21685
  struct_decls: []
21329
21686
  });
21330
- const declarations_before = status.scoped_declarations.length;
21687
+ const declarations_before = node.node_type === "func" ? 0 : status.scoped_declarations.length;
21331
21688
  for (let child of node.statements) if (is_struct_node(child)) {
21332
21689
  if (!should_emit_definition(child, status.emit_mode, status.structs, status.system_struct_names)) continue;
21333
21690
  build_struct_node$1(child, status);
@@ -21482,6 +21839,24 @@ function literal(raw, start) {
21482
21839
  return new ValueNode(start, raw, new Type("string", true));
21483
21840
  }
21484
21841
  //#endregion
21842
+ //#region ../src/build_common/mark_tuple_literal_move_owners.ts
21843
+ /**
21844
+ * A tuple-literal constructor call (`[move t, move c]` lowered to
21845
+ * `_Tuple_.._init(&t, &c)`) copies its fields BY POINTER, and a `move`
21846
+ * element transfers ownership: the source local's scope-exit cleanup must
21847
+ * be suppressed (the tuple's own field cleanup now owns and frees the
21848
+ * buffer). Registers every bare `move <local>` argument into the build's
21849
+ * moved set — the same suppression the move-assignment path uses. Called
21850
+ * from every constructor-call build site (declarations, assignments, and
21851
+ * the generic call builder), since monomorphized tuple inits route through
21852
+ * each of them.
21853
+ */
21854
+ function mark_tuple_literal_move_owners(node, status) {
21855
+ if (!node.type?.name?.startsWith("_Tuple_")) return;
21856
+ if (!status.moved) status.moved = /* @__PURE__ */ new Set();
21857
+ for (const p of node.params) if (p.node_type === "value" && p.is_moved && typeof p.value === "string") status.moved.add(p.value);
21858
+ }
21859
+ //#endregion
21485
21860
  //#region ../src/build_common/string_escapes.ts
21486
21861
  /**
21487
21862
  * Source string-literal escape handling shared by both backends.
@@ -21611,6 +21986,37 @@ function escape_asciz(value) {
21611
21986
  return quote + reencoded.slice(1, reencoded.endsWith(quote) ? -1 : void 0).replace(/\n/g, "\\n") + (reencoded.endsWith(quote) ? quote : "");
21612
21987
  }
21613
21988
  //#endregion
21989
+ //#region ../src/build_common/tuple_return_owned_element.ts
21990
+ /**
21991
+ * Whether a RETURN-position tuple-literal STRING element already owns a heap
21992
+ * buffer of its own, so the return boundary can leave it raw (normalization
21993
+ * skips it). Two shapes qualify:
21994
+ *
21995
+ * - a bare local that was TRANSFERRED into the tuple (`move`, or the
21996
+ * last-use inference) whose buffer is genuinely heap — the callee's
21997
+ * cleanup skips it, so the field owns the original allocation. A
21998
+ * transferred RODATA local (a string literal on aarch64) does NOT
21999
+ * qualify: the field would alias static storage and the caller's
22000
+ * ownership of it would free static memory.
22001
+ * - a fresh expression result that is not a borrow (a call, interpolation,
22002
+ * concat) — it owns its heap already.
22003
+ *
22004
+ * Everything else (string literals, non-transferred locals, parameters,
22005
+ * borrow accessors such as `.at(0)`) must be strdup'd at the return so the
22006
+ * caller can uniformly own every string field of the returned tuple (see the
22007
+ * caller-side binding ownership).
22008
+ */
22009
+ function tuple_return_owned_element(element, heap_strings) {
22010
+ if (element.node_type === "value") {
22011
+ const v = element.value ?? "";
22012
+ if (typeof v !== "string") return false;
22013
+ if (v.startsWith("\"")) return false;
22014
+ if (!/^[A-Za-z_]/.test(v) || v === "null") return false;
22015
+ return !!element.is_moved && !!heap_strings?.has(v);
22016
+ }
22017
+ return !is_string_borrow(element);
22018
+ }
22019
+ //#endregion
21614
22020
  //#region ../src/build_aarch64/build_range_node.ts
21615
22021
  function build_range_node$1(node, status) {
21616
22022
  const start = evaluate_constant$1(node.left_value);
@@ -22006,6 +22412,7 @@ function emit_struct_constructor_to_slot(fc, slot_addr, status) {
22006
22412
  * so register slots start at x1 — overflow begins at the 8th arg (slot 8).
22007
22413
  */
22008
22414
  function build_constructor_params(fc, param_regs, status) {
22415
+ mark_tuple_literal_move_owners(fc, status);
22009
22416
  const variadic_idx = fc.variadic_param_index;
22010
22417
  if (variadic_idx !== void 0 && fc.params[variadic_idx] && fc.params[variadic_idx].node_type === "array") {
22011
22418
  const arr = fc.params[variadic_idx];
@@ -22405,6 +22812,43 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
22405
22812
  if (!status.function_local_types) status.function_local_types = /* @__PURE__ */ new Map();
22406
22813
  status.function_local_types.set(node.name, node.type.name);
22407
22814
  }
22815
+ if (node.declaration === "var" && node.type.name?.startsWith("_Tuple_") && !node.name.startsWith("_tuple_dst_") && node.value?.node_type === "func_call") {
22816
+ const tuple_call = node.value;
22817
+ const tuple_struct = status.structs.find((st) => st.name === node.type.name && !st.is_simple_type);
22818
+ const init_is_tuple_call = !String(tuple_call.name ?? "").startsWith("_Tuple_") && !!tuple_call.type?.name?.startsWith("_Tuple_");
22819
+ const init_is_tuple_literal = String(tuple_call.name ?? "").startsWith("_Tuple_");
22820
+ if (tuple_struct && (init_is_tuple_call || init_is_tuple_literal)) {
22821
+ const offsets = [];
22822
+ for (const field of direct_string_fields(tuple_struct)) {
22823
+ if (init_is_tuple_literal) {
22824
+ const idx = parseInt(field.name.replace(/^_/, ""), 10);
22825
+ const element = tuple_call.params[idx];
22826
+ if (!element || !tuple_return_owned_element(element, status.heap_strings)) continue;
22827
+ }
22828
+ offsets.push(get_field_offset(tuple_struct.name, field.name, status));
22829
+ }
22830
+ if (offsets.length) {
22831
+ if (!status.heap_string_tuple_fields) status.heap_string_tuple_fields = /* @__PURE__ */ new Map();
22832
+ status.heap_string_tuple_fields.set(node.name, offsets);
22833
+ }
22834
+ }
22835
+ }
22836
+ if (node.declaration === "var" && node.type.name === "string" && !node.type.is_array) {
22837
+ const binding_value = node.value;
22838
+ if (binding_value?.node_type === "access" && binding_value.access.node_type === "access_field" && binding_value.target.node_type === "value" && String(binding_value.target.value ?? "").startsWith("_tuple_dst_")) {
22839
+ const temp_name = String(binding_value.target.value);
22840
+ const temp_init = status.scoped_declarations.findLast((d) => d.name === temp_name)?.value;
22841
+ const m = /^_(\d+)$/.exec(String(binding_value.access.name ?? ""));
22842
+ if (temp_init?.node_type === "func_call" && !String(temp_init.name ?? "").startsWith("_Tuple_") && !!temp_init.type?.name?.startsWith("_Tuple_")) {
22843
+ mark_heap_string(status, node.name);
22844
+ binding_value.is_moved = true;
22845
+ } else if (temp_init?.type?.name?.startsWith("_Tuple_") && m) {
22846
+ const param = temp_init.params[parseInt(m[1], 10)];
22847
+ const src_name = param?.node_type === "value" ? String(param.value) : "";
22848
+ if (param?.node_type === "value" && param.is_moved && !!src_name && !!status.heap_strings?.has(src_name)) mark_heap_string(status, node.name);
22849
+ }
22850
+ }
22851
+ }
22408
22852
  hoist_field_overrides(node.value, build_node, status, "", node.name);
22409
22853
  status.last_result_is_heap = false;
22410
22854
  const prev_heap = status.last_result_is_heap;
@@ -22554,12 +22998,15 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
22554
22998
  build_swap_params(func_call, status);
22555
22999
  } else {
22556
23000
  const old_buffer = status.struct_return_buffer;
23001
+ const old_buffer_var = status.struct_return_buffer_var;
22557
23002
  const old_preset = status.call_x8_preset;
22558
23003
  emit_var_address(status, "x8", node.name);
23004
+ status.struct_return_buffer_var = node.name;
22559
23005
  status.struct_return_buffer = "x8";
22560
23006
  status.call_x8_preset = true;
22561
23007
  emit_init_value$1(node.value, nir_init, status);
22562
23008
  status.struct_return_buffer = old_buffer;
23009
+ status.struct_return_buffer_var = old_buffer_var;
22563
23010
  status.call_x8_preset = old_preset;
22564
23011
  }
22565
23012
  return;
@@ -22867,13 +23314,33 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
22867
23314
  } else if (status.function_return_label) {
22868
23315
  const labels = emit_string_array_labels(array_values.values, status);
22869
23316
  if (needs_runtime_array_init(array_values.values, status)) {
22870
- const offset = alloc_array_with_prefix(status, array_values.values.length, element_size);
22871
- status.stack_offsets.set(node.name, offset);
22872
- array_values.values.forEach((value, i) => {
22873
- const slot_offset = offset + i * element_size;
22874
- const resolved = resolve_static_value(value, status);
22875
- if (resolved !== null) emit_string_array_element(status, resolved, labels, "x29", slot_offset);
22876
- });
23317
+ if (node.global_runtime_init) {
23318
+ emit_data(status, `.data\n`);
23319
+ emit_data(status, `.quad ${array_values.values.length}\n`);
23320
+ emit_data(status, `${node.name}:\n`);
23321
+ emit_data(status, `.space ${array_values.values.length * 16}\n`);
23322
+ emit_data(status, `.p2align 2\n`);
23323
+ emit_data(status, `.text\n`);
23324
+ emit_asm(status, `adrp x0, ${node.name}@PAGE\n`);
23325
+ emit_asm(status, `add x0, x0, ${node.name}@PAGEOFF\n`);
23326
+ array_values.values.forEach((value, i) => {
23327
+ const resolved = resolve_static_value(value, status);
23328
+ if (resolved !== null && resolved.startsWith("\"")) {
23329
+ emit_asm(status, `adr x1, ${resolve_array_element(resolved, labels)}\n`);
23330
+ emit_asm(status, `str x1, [x0, #${i * 16}]\n`);
23331
+ emit_asm(status, `mov x1, #${string_literal_length(resolved)}\n`);
23332
+ emit_asm(status, `str x1, [x0, #${i * 16 + 8}]\n`);
23333
+ }
23334
+ });
23335
+ } else {
23336
+ const offset = alloc_array_with_prefix(status, array_values.values.length, element_size);
23337
+ status.stack_offsets.set(node.name, offset);
23338
+ array_values.values.forEach((value, i) => {
23339
+ const slot_offset = offset + i * element_size;
23340
+ const resolved = resolve_static_value(value, status);
23341
+ if (resolved !== null) emit_string_array_element(status, resolved, labels, "x29", slot_offset);
23342
+ });
23343
+ }
22877
23344
  } else {
22878
23345
  emit_data(status, `${node.name}: ${directive} `);
22879
23346
  let first = true;
@@ -22889,19 +23356,16 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
22889
23356
  emit_data(status, `\n.p2align 2\n`);
22890
23357
  }
22891
23358
  } else if (node.type.name === "string" && node.type.is_array) {
22892
- const labels = emit_string_array_labels(array_values.values, status);
22893
- emit_asm(status, `${node.name}: ${directive} `);
22894
- array_values.values.forEach((value, i) => {
22895
- if (i > 0) emit_data(status, ", ");
23359
+ emit_data(status, `.quad ${array_values.values.length}\n`);
23360
+ emit_data(status, `${node.name}: .space ${array_values.values.length * 16}\n\t.p2align 2\n`);
23361
+ } else {
23362
+ emit_data(status, `.quad ${array_values.values.length}\n`);
23363
+ emit_data(status, `${node.name}:\n`);
23364
+ array_values.values.forEach((value) => {
22896
23365
  const resolved = resolve_static_value(value, status);
22897
- if (resolved !== null && resolved.startsWith("\"")) emit_asm(status, `.quad ${resolve_array_element(resolved, labels)}\n\t.quad ${string_literal_length(resolved)}`);
22898
- else emit_asm(status, ".quad 0\n .quad 0");
23366
+ emit_data(status, `\t${directive} ${resolved !== null ? resolved : "0"}\n`);
22899
23367
  });
22900
- emit_asm(status, `\n.p2align 2\n`);
22901
- } else {
22902
- emit_asm(status, `${node.name}: ${directive} `);
22903
- build_array_values_node$1(array_values, status);
22904
- emit_asm(status, `\n.p2align 2\n`);
23368
+ emit_data(status, `\t.p2align 2\n`);
22905
23369
  }
22906
23370
  } else if (node.value && node.value.node_type === "range") {
22907
23371
  if (status.function_return_label) {
@@ -23085,14 +23549,18 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23085
23549
  build_swap_params(func_call, status);
23086
23550
  } 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
23551
  const old_buffer = status.struct_return_buffer;
23552
+ const old_buffer_var = status.struct_return_buffer_var;
23088
23553
  const old_preset = status.call_x8_preset;
23089
23554
  emit_var_address(status, "x8", node.name);
23555
+ status.struct_return_buffer_var = node.name;
23090
23556
  status.struct_return_buffer = "x8";
23091
23557
  status.call_x8_preset = true;
23092
23558
  emit_init_value$1(node.value, nir_init, status);
23093
23559
  status.struct_return_buffer = old_buffer;
23560
+ status.struct_return_buffer_var = old_buffer_var;
23094
23561
  status.call_x8_preset = old_preset;
23095
23562
  emit_var_address(status, "x0", node.name);
23563
+ record_call_init_string_fields(node, status);
23096
23564
  } else {
23097
23565
  emit_init_value$1(node.value, nir_init, status);
23098
23566
  emit_var_store(status, "x0", node.name, struct_size);
@@ -23128,6 +23596,7 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23128
23596
  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
23597
  emit_struct_copy("x1", "x0", get_field_offset(target_type.name, field_name, status), get_struct_size(field_type.name, status), status);
23130
23598
  }
23599
+ record_call_init_string_fields(node, status);
23131
23600
  }
23132
23601
  }
23133
23602
  } else if (node.value) {
@@ -24785,7 +25254,7 @@ function build_return_node$1(node, status, nir_value) {
24785
25254
  const finalized = status.moved ?? /* @__PURE__ */ new Set();
24786
25255
  for (const decl of all_scope_frames(status).flat()) {
24787
25256
  if (finalized.has(decl.name)) {
24788
- release_heap_string_fields(status, decl.name, decl.type.name);
25257
+ release_heap_string_fields(status, decl.name, decl.type.name, decl.type.is_nullable);
24789
25258
  continue;
24790
25259
  }
24791
25260
  if (is_field_struct_borrow(decl)) continue;
@@ -24805,7 +25274,7 @@ function build_return_node$1(node, status, nir_value) {
24805
25274
  const finalized = status.moved ?? /* @__PURE__ */ new Set();
24806
25275
  for (const decl of all_scope_frames(status).flat()) {
24807
25276
  if (finalized.has(decl.name)) {
24808
- release_heap_string_fields(status, decl.name, decl.type.name);
25277
+ release_heap_string_fields(status, decl.name, decl.type.name, decl.type.is_nullable);
24809
25278
  continue;
24810
25279
  }
24811
25280
  if (is_field_struct_borrow(decl)) continue;
@@ -24893,7 +25362,10 @@ function build_return_node$1(node, status, nir_value) {
24893
25362
  if (anon.base.is_moved) mark_moved_if_struct(anon.base, status);
24894
25363
  emit_field_overrides(temp_name, anon, build_node, status);
24895
25364
  emit_var_address(status, "x0", temp_name);
24896
- } else emit_return_value$1(node.value, nir_value, status);
25365
+ } else {
25366
+ status.last_result_is_heap = false;
25367
+ emit_return_value$1(node.value, nir_value, status);
25368
+ }
24897
25369
  if (ret_is_float) {
24898
25370
  ensure_newline(status);
24899
25371
  if (status.float_result_in_d0) emit_asm(status, `fmov d0, x0\n`);
@@ -24905,7 +25377,7 @@ function build_return_node$1(node, status, nir_value) {
24905
25377
  if (current_return_is_string(status) && node.value.node_type === "value" && node.value.value.length >= 2 && node.value.value.startsWith("\"") && node.value.value.endsWith("\"")) {
24906
25378
  const fn_name = status.current_function_name;
24907
25379
  const mangled = status.current_struct ? `${status.current_struct.name}_${fn_name}` : fn_name;
24908
- if (fn_name && (!!status.heap_returning_functions && (status.heap_returning_functions.has(fn_name) || mangled !== void 0 && status.heap_returning_functions.has(mangled)) || current_function_returns_move_string(status))) {
25380
+ if (fn_name && (status.current_function_is_closure || !!status.heap_returning_functions && (status.heap_returning_functions.has(fn_name) || mangled !== void 0 && status.heap_returning_functions.has(mangled)) || current_function_returns_move_string(status))) {
24909
25381
  emit_strdup_string(status);
24910
25382
  ensure_newline(status);
24911
25383
  status.last_result_is_heap = true;
@@ -24913,7 +25385,7 @@ function build_return_node$1(node, status, nir_value) {
24913
25385
  }
24914
25386
  const borrow_fn_name = status.current_function_name;
24915
25387
  const borrow_mangled = status.current_struct && borrow_fn_name ? `${status.current_struct.name}_${borrow_fn_name}` : void 0;
24916
- const normalizes_borrow_returns = !!borrow_fn_name && !is_call_site_borrow_accessor(borrow_fn_name) && current_return_is_string(status) && (!!status.heap_returning_functions && (status.heap_returning_functions.has(borrow_fn_name) || borrow_mangled !== void 0 && status.heap_returning_functions.has(borrow_mangled)) || current_function_returns_move_string(status));
25388
+ const normalizes_borrow_returns = !!borrow_fn_name && !is_call_site_borrow_accessor(borrow_fn_name) && current_return_is_string(status) && (!!status.current_function_is_closure || !!status.heap_returning_functions && (status.heap_returning_functions.has(borrow_fn_name) || borrow_mangled !== void 0 && status.heap_returning_functions.has(borrow_mangled)) || current_function_returns_move_string(status));
24917
25389
  let needs_borrow_strdup = false;
24918
25390
  if (normalizes_borrow_returns) {
24919
25391
  if (node.value.node_type === "value") {
@@ -24923,6 +25395,7 @@ function build_return_node$1(node, status, nir_value) {
24923
25395
  if (!is_str_lit && !is_numeric && raw_value !== "null" && !status.heap_strings?.has(raw_value)) needs_borrow_strdup = true;
24924
25396
  } else if (is_container_borrow_access(node.value)) needs_borrow_strdup = true;
24925
25397
  else if (node.value.node_type === "access" && node.value.access.node_type === "access_field") needs_borrow_strdup = true;
25398
+ else if (status.current_function_is_closure && (node.value.node_type === "func_call" || node.value.node_type === "access" && node.value.access.node_type === "access_func") && !status.last_result_is_heap && !is_container_borrow_access(node.value)) needs_borrow_strdup = true;
24926
25399
  else if ((node.value.node_type === "match" || node.value.node_type === "switch") && !return_join_owned_string) needs_borrow_strdup = true;
24927
25400
  }
24928
25401
  if (needs_borrow_strdup) {
@@ -24967,6 +25440,28 @@ function build_return_node$1(node, status, nir_value) {
24967
25440
  const field_off = get_field_offset(ret_struct.name, field.name, status);
24968
25441
  emit_enum_payload_strdups_at(status, field_enum.name, "x8", field_off);
24969
25442
  }
25443
+ if (ret_struct && !returns_nullable_struct) {
25444
+ const returned_var = node.value.node_type === "value" ? node.value.value : "";
25445
+ if (returned_var !== "" && returned_var !== "0" && returned_var !== "null" && all_scope_frames(status).flat().some((d) => d.name === returned_var)) {
25446
+ let skip_fields;
25447
+ const prefix = `${node.value.value}.`;
25448
+ for (const key of status.heap_string_fields ?? []) if (key.startsWith(prefix)) {
25449
+ skip_fields ??= /* @__PURE__ */ new Set();
25450
+ skip_fields.add(key.slice(prefix.length));
25451
+ }
25452
+ emit_string_field_strdups_at(status, ret_struct.name, "x8", 0, skip_fields);
25453
+ }
25454
+ if (node.value?.node_type === "func_call" && String(node.value.name ?? "").startsWith("_Tuple_") && node.value.type?.name?.startsWith("_Tuple_")) {
25455
+ const tuple_call = node.value;
25456
+ const skip_fields = /* @__PURE__ */ new Set();
25457
+ for (const field of direct_string_fields(ret_struct)) {
25458
+ const idx = parseInt(field.name.replace(/^_/, ""), 10);
25459
+ const element = tuple_call.params[idx];
25460
+ if (element && tuple_return_owned_element(element, status.heap_strings)) skip_fields.add(field.name);
25461
+ }
25462
+ emit_string_field_strdups_at(status, ret_struct.name, "x8", 0, skip_fields);
25463
+ }
25464
+ }
24970
25465
  }
24971
25466
  }
24972
25467
  if (status.return_assign) {
@@ -25034,7 +25529,7 @@ function build_return_node$1(node, status, nir_value) {
25034
25529
  emit_nursery_joins_on_return_a64(status);
25035
25530
  for (const decl of all_scope_frames(status).flat()) {
25036
25531
  if (finalized.has(decl.name)) {
25037
- release_heap_string_fields(status, decl.name, decl.type.name);
25532
+ release_heap_string_fields(status, decl.name, decl.type.name, decl.type.is_nullable);
25038
25533
  continue;
25039
25534
  }
25040
25535
  if (is_field_struct_borrow(decl)) continue;
@@ -28271,6 +28766,7 @@ function build_function_call_node$1(node, status) {
28271
28766
  }
28272
28767
  }
28273
28768
  const is_struct = status.structs.find((s) => s.name === node.name && !s.is_simple_type);
28769
+ mark_tuple_literal_move_owners(node, status);
28274
28770
  const func_name = is_struct ? node.mangled_name ?? `${node.name}_init` : emission_label(node.resolved_function ?? node);
28275
28771
  const param_regs = [
28276
28772
  "x0",
@@ -28310,8 +28806,25 @@ function build_function_call_node$1(node, status) {
28310
28806
  const env_off = status.closure_env_offsets?.get(node.name);
28311
28807
  const paramReg = func_offset === void 0 ? status.function_param_regs?.get(node.name) : void 0;
28312
28808
  const desc_source = env_off !== void 0 ? `ldr x9, [x29, #${status.closure_env_slot}]\nldr x9, [x9, #${env_off}]\n` : func_offset !== void 0 ? `ldr x9, [x29, #${func_offset}]\n` : paramReg ? `mov x9, ${paramReg}\n` : `adr x9, ${node.name}\n`;
28809
+ const param_slots = [];
28810
+ let total_visible_slots = 0;
28811
+ for (const p of node.params) {
28812
+ param_slots.push(total_visible_slots);
28813
+ total_visible_slots += type_from_value_node$1(p)?.name === "string" ? 2 : 1;
28814
+ }
28815
+ const reg_capacity = 8 - (start_reg + 1);
28816
+ const overflow_count = Math.max(0, total_visible_slots - reg_capacity);
28817
+ let outgoing_size = 0;
28818
+ let spill_base = 0;
28819
+ if (overflow_count > 0) spill_base = allocate_stack_space(status, overflow_count * 8, 8);
28313
28820
  const lambda_arg_slots = [];
28314
- let arg_slot = start_reg + 1;
28821
+ const store_arg_half = (value_reg, slot) => {
28822
+ if (slot < 8) {
28823
+ const reg = param_regs[slot];
28824
+ if (reg === value_reg) emit_asm(status, `\n`);
28825
+ else emit_asm(status, `\nmov ${reg}, ${value_reg}\n`);
28826
+ } else emit_asm(status, `\nstr ${value_reg}, [x29, #${spill_base + (slot - 8) * 8}]\n`);
28827
+ };
28315
28828
  for (let i = node.params.length - 1; i >= 0; i--) {
28316
28829
  const param = node.params[i];
28317
28830
  const is_pair = type_from_value_node$1(param)?.name === "string";
@@ -28321,19 +28834,25 @@ function build_function_call_node$1(node, status) {
28321
28834
  emit_asm(status, `str x0, [x29, #${dispose_slot}]\n`);
28322
28835
  lambda_arg_slots.push(dispose_slot);
28323
28836
  }
28324
- const reg = param_regs[arg_slot];
28325
- const len_reg = param_regs[arg_slot + 1];
28326
- if (is_pair && len_reg !== "x1") emit_asm(status, `\nmov ${len_reg}, x1\n`);
28327
- if (reg !== "x0") emit_asm(status, `\nmov ${reg}, x0\n`);
28328
- else emit_asm(status, `\n`);
28329
- if (is_pair) arg_slot += 2;
28330
- else arg_slot += 1;
28837
+ const arg_slot = start_reg + 1 + param_slots[i];
28838
+ if (is_pair) store_arg_half("x1", arg_slot + 1);
28839
+ store_arg_half("x0", arg_slot);
28840
+ }
28841
+ if (overflow_count > 0) {
28842
+ outgoing_size = Math.ceil(overflow_count * 8 / 16) * 16;
28843
+ emit_asm(status, `sub sp, sp, #${outgoing_size}\n`);
28844
+ for (let k = 0; k < overflow_count; k++) {
28845
+ emit_asm(status, `ldr x9, [x29, #${spill_base + k * 8}]\n`);
28846
+ emit_asm(status, `str x9, [sp, #${k * 8}]\n`);
28847
+ }
28331
28848
  }
28332
28849
  emit_asm(status, desc_source);
28333
28850
  emit_asm(status, `ldr x8, [x9]\n`);
28334
28851
  emit_asm(status, `ldr x0, [x9, #8]\n`);
28335
28852
  emit_asm(status, `blr x8\n`);
28336
28853
  emit_dispose_lambda_args_a64(status, lambda_arg_slots);
28854
+ if (outgoing_size > 0) emit_asm(status, `add sp, sp, #${outgoing_size}\n`);
28855
+ if (node.type?.name === "string" && !node.type.is_view) status.last_result_is_heap = true;
28337
28856
  } else {
28338
28857
  const variadic_idx = node.variadic_param_index;
28339
28858
  const ref_class_sync_names = [];
@@ -28596,8 +29115,39 @@ function build_function_call_node$1(node, status) {
28596
29115
  emit_asm(status, `str x9, [x29, #${off + struct_size}]\n`);
28597
29116
  emit_asm(status, `add x0, x29, #${off}\n`);
28598
29117
  }
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)) {
29118
+ } else if (is_struct_type$3(param_type, status) || is_enum_with_data_type$2(param_type, status)) {
29119
+ if (node.move_owned_param_indices?.includes(i) && node.params[i].node_type === "value") {
29120
+ 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);
29121
+ const transfer_var = node.params[i].value;
29122
+ if (transfer_struct && direct_string_fields(transfer_struct).length > 0 && transfer_var) {
29123
+ const recorded = /* @__PURE__ */ new Set();
29124
+ for (const key of status.heap_string_fields ?? []) if (key.startsWith(`${transfer_var}.`)) recorded.add(key.slice(transfer_var.length + 1));
29125
+ if (direct_string_fields(transfer_struct).filter((f) => !recorded.has(f.name)).length > 0) {
29126
+ emit_struct_address(node.params[i], status);
29127
+ ensure_newline(status);
29128
+ emit_string_field_strdups_at(status, transfer_struct.name, "x0", 0, recorded);
29129
+ if (status.call_x8_preset && status.struct_return_buffer_var) emit_var_address(status, "x8", status.struct_return_buffer_var);
29130
+ }
29131
+ }
29132
+ }
29133
+ 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);
29134
+ 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)) {
29135
+ const mono_name = arg_struct.name;
29136
+ const struct_size = get_struct_size(mono_name, status);
29137
+ const off = allocate_stack_space(status, struct_size);
29138
+ emit_struct_address(node.params[i], status);
29139
+ ensure_newline(status);
29140
+ emit_asm(status, `mov x1, x0\n`);
29141
+ emit_asm(status, `add x0, x29, #${off}\n`);
29142
+ for (let b = 0; b < struct_size; b += 8) {
29143
+ emit_asm(status, `ldr x2, [x1, #${b}]\n`);
29144
+ emit_asm(status, `str x2, [x0, #${b}]\n`);
29145
+ }
29146
+ emit_asm(status, `add x0, x29, #${off}\n`);
29147
+ emit_string_field_strdups_at(status, mono_name, "x0");
29148
+ if (status.call_x8_preset && status.struct_return_buffer_var) emit_var_address(status, "x8", status.struct_return_buffer_var);
29149
+ } else emit_struct_address(node.params[i], status);
29150
+ } else if (arg_deferrable(i)) {
28601
29151
  deferred_args.push({
28602
29152
  param: node.params[i],
28603
29153
  slot: arg_slot[i]
@@ -28729,6 +29279,7 @@ function build_function_call_node$1(node, status) {
28729
29279
  const tname = all_scope_frames(status).flat().find((d) => d.name === vname)?.type?.name ?? param.type?.name;
28730
29280
  if (tname === "string") continue;
28731
29281
  if (tname && status.enums.find((e) => e.name === tname && e.has_associated_data)) continue;
29282
+ if (vname !== void 0 && (node.move_owned_param_indices?.includes(idx) ?? false)) clear_heap_string_fields(status, vname);
28732
29283
  }
28733
29284
  if (param) mark_moved_if_struct(param, status);
28734
29285
  }
@@ -31030,6 +31581,20 @@ function emit_enum_payload_reclaim(cname, enum_node, c, p, status) {
31030
31581
  if (payload_is_reference$2(p.type, status)) status.code += `${guard} { ${p.type.name}_destroy(${cname}._data._${c.name}.${p.name}); free(${cname}._data._${c.name}.${p.name}); }\n`;
31031
31582
  else if (p.type.name === "string") status.code += `${guard} { free(${cname}._data._${c.name}.${p.name}.ptr); }\n`;
31032
31583
  }
31584
+ /**
31585
+ * Drop the `name.field` heap-string records belonging to `decls` WITHOUT
31586
+ * emitting any free code. Used when the emitted reclaim would be unreachable
31587
+ * (a function body that cannot fall through — see build_function_node): the
31588
+ * return-site reclaim already emitted the frees, so the records must be
31589
+ * cleared for the next function build but must not produce a second block.
31590
+ */
31591
+ function discard_string_field_records(status, decls) {
31592
+ if (!status.heap_string_fields?.size) return;
31593
+ for (const dec of decls) {
31594
+ const prefix = `${dec.name}.`;
31595
+ for (const key of Array.from(status.heap_string_fields)) if (key.startsWith(prefix)) status.heap_string_fields.delete(key);
31596
+ }
31597
+ }
31033
31598
  function build_auto_free(status) {
31034
31599
  free_scoped_declarations(status, status.scoped_declarations);
31035
31600
  if (status.deferred_frees?.length) {
@@ -31051,17 +31616,34 @@ function free_scoped_declarations(status, decls, persist_string_field_records =
31051
31616
  let commented = false;
31052
31617
  if (status.heap_string_fields?.size) for (const dec of decls) {
31053
31618
  const prefix = `${dec.name}.`;
31054
- for (const key of Array.from(status.heap_string_fields)) if (key.startsWith(prefix)) {
31055
- if (!commented) {
31056
- status.code += "\n// Auto-free\n";
31057
- commented = true;
31058
- }
31619
+ const keys = Array.from(status.heap_string_fields).filter((key) => key.startsWith(prefix));
31620
+ if (!keys.length) continue;
31621
+ if (!commented) {
31622
+ status.code += "\n// Auto-free\n";
31623
+ commented = true;
31624
+ }
31625
+ const nullable_flag = is_nullable_struct_type(dec.type, status) ? has_flag_name(c_function_name(dec.name)) : void 0;
31626
+ if (nullable_flag) status.code += `if (${nullable_flag}) { `;
31627
+ for (const key of keys) {
31059
31628
  const dot = key.indexOf(".");
31060
- status.code += `free(${c_function_name(key.substring(0, dot))}${key.substring(dot)}.ptr);\n`;
31629
+ const base = c_function_name(key.substring(0, dot));
31630
+ const arrow = dec.string_fields_via_pointer ? "->" : ".";
31631
+ const field = key.substring(dot + 1);
31632
+ const free_fn = status.audit ? "nomen_free_wrap" : "free";
31633
+ status.code += `${free_fn}(${base}${arrow}${field}.ptr);\n`;
31061
31634
  if (!persist_string_field_records) status.heap_string_fields.delete(key);
31062
31635
  }
31636
+ if (nullable_flag) status.code += `}\n`;
31063
31637
  }
31064
31638
  for (const dec of decls) {
31639
+ if (dec.owned_heap_shell) {
31640
+ if (!commented) {
31641
+ status.code += "\n// Auto-free\n";
31642
+ commented = true;
31643
+ }
31644
+ status.code += `${status.audit ? "nomen_free_wrap" : "free"}(${c_function_name(dec.name)});\n`;
31645
+ continue;
31646
+ }
31065
31647
  if (persist_string_field_records && dec.force_owned_string === true) continue;
31066
31648
  if (status.moved?.has(dec.name)) continue;
31067
31649
  const cname = c_function_name(dec.name);
@@ -31569,6 +32151,49 @@ function emit_rhs_value$1(rhs, nir, status) {
31569
32151
  }
31570
32152
  function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
31571
32153
  hoist_field_overrides(node.right_value, build_node$1, status, ";\n", node.left_value.node_type === "value" ? node.left_value.value : void 0);
32154
+ const lhs_nullable_type = lhs_nullable_struct_type(node, status);
32155
+ if (!node.operator && lhs_nullable_type) {
32156
+ const lhs_expr = capture_build(node.left_value, status);
32157
+ const flag = `${lhs_expr}_has`;
32158
+ const rhs_is_null = node.right_value.node_type === "value" && node.right_value.value === "null";
32159
+ const lhs_is_local = node.left_value.node_type === "value";
32160
+ const lhs_name = lhs_is_local ? node.left_value.value : "";
32161
+ const rhs_is_same_var = lhs_is_local && node.right_value.node_type === "value" && node.right_value.value === lhs_name;
32162
+ const type_name = (lhs_is_local ? status.scoped_declarations.find((d) => d.name === lhs_name) : void 0)?.type?.name || "";
32163
+ if (lhs_is_local && !rhs_is_same_var && type_name) {
32164
+ const prefix = `${lhs_name}.`;
32165
+ const recorded = status.heap_string_fields ? Array.from(status.heap_string_fields).filter((k) => k.startsWith(prefix)) : [];
32166
+ if (recorded.length > 0) {
32167
+ const free_fn = status.audit ? "nomen_free_wrap" : "free";
32168
+ const frees = recorded.map((k) => {
32169
+ const dot = k.indexOf(".");
32170
+ return `${free_fn}(${lhs_expr}.${k.slice(dot + 1)}.ptr)`;
32171
+ });
32172
+ status.code += `if (${flag}) { ${frees.join("; ")}; }\n`;
32173
+ for (const k of recorded) status.heap_string_fields.delete(k);
32174
+ }
32175
+ if (!rhs_is_null && is_normalized_struct_call(node.right_value, status)) {
32176
+ const struct = status.structs.find((s) => s.name === type_name && !s.is_simple_type && !s.is_class);
32177
+ if (struct) {
32178
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
32179
+ for (const field of direct_string_fields(struct)) status.heap_string_fields.add(`${lhs_name}.${field.name}`);
32180
+ }
32181
+ }
32182
+ }
32183
+ if (rhs_is_null) status.code += `${flag} = 0`;
32184
+ else if (node.right_value.node_type === "func_call" && is_nullable_struct_type(node.right_value.type, status)) {
32185
+ const old = status.current_nullable_call_flag;
32186
+ status.current_nullable_call_flag = flag;
32187
+ status.code += `${lhs_expr} = `;
32188
+ emit_rhs_value$1(node.right_value, nir_rhs, status);
32189
+ status.current_nullable_call_flag = old;
32190
+ } else {
32191
+ status.code += `${lhs_expr} = `;
32192
+ emit_rhs_value$1(node.right_value, nir_rhs, status);
32193
+ status.code += `;\n${flag} = 1`;
32194
+ }
32195
+ return;
32196
+ }
31572
32197
  if (node.left_value.node_type === "access") {
31573
32198
  const accessNode = node.left_value;
31574
32199
  if (accessNode.target.node_type === "value") {
@@ -31659,7 +32284,8 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
31659
32284
  status.init_assigned_fields.add(tracked_key);
31660
32285
  }
31661
32286
  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)) {
32287
+ const is_class_target = !!target_struct?.is_class;
32288
+ if (target_struct && (target_var || is_class_target) && (!self_target || is_class_target)) {
31663
32289
  const fresh_heap = is_owned_heap_temp$1(node.right_value, status);
31664
32290
  const rhs_is_null_value = node.right_value.node_type === "value" && node.right_value.value === "null";
31665
32291
  const saved_len = begin_code_scratch(status);
@@ -31942,18 +32568,6 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
31942
32568
  }
31943
32569
  }
31944
32570
  status.code += ``;
31945
- const lhs_nullable_type = lhs_nullable_struct_type(node, status);
31946
- if (!node.operator && lhs_nullable_type) {
31947
- const lhs_expr = capture_build(node.left_value, status);
31948
- const flag = `${lhs_expr}_has`;
31949
- if (node.right_value.node_type === "value" && node.right_value.value === "null") status.code += `${flag} = 0`;
31950
- else {
31951
- status.code += `${lhs_expr} = `;
31952
- emit_rhs_value$1(node.right_value, nir_rhs, status);
31953
- status.code += `;\n${flag} = 1`;
31954
- }
31955
- return;
31956
- }
31957
32571
  if (!node.operator && !node.swap && node.left_value.node_type === "value") {
31958
32572
  const lhs_name = node.left_value.value;
31959
32573
  const lhs_type = type_from_value_node$1(node.left_value);
@@ -32263,6 +32877,101 @@ function emit_nursery_joins_on_return_c(status) {
32263
32877
  for (let i = status.nursery_stack.length - 1; i >= base; i--) emit_nursery_join_c(status, status.nursery_stack[i], true);
32264
32878
  }
32265
32879
  //#endregion
32880
+ //#region ../src/build_common/decode_char_literal.ts
32881
+ /**
32882
+ * Decode a Nomen char literal token (raw value INCLUDING its surrounding
32883
+ * quotes, as carried by ValueNode) to its numeric code. The tokenizer
32884
+ * consumes escape pairs as a unit, so multi-char forms like `'\\\\'`,
32885
+ * `'\\n'`, `'\\''` and `'\\xNN'` arrive raw and must be decoded here.
32886
+ * Returns undefined for malformed literals.
32887
+ */
32888
+ function decode_char_literal(raw) {
32889
+ if (raw.length < 3 || raw[0] !== "'" || raw[raw.length - 1] !== "'") return;
32890
+ const inner = raw.slice(1, -1);
32891
+ if (inner.length === 1) return inner.charCodeAt(0);
32892
+ if (inner[0] !== "\\") return;
32893
+ const esc = inner[1];
32894
+ switch (esc) {
32895
+ case "\\": return 92;
32896
+ case "n": return 10;
32897
+ case "t": return 9;
32898
+ case "r": return 13;
32899
+ case "'": return 39;
32900
+ case "\"": return 34;
32901
+ case "0": return 0;
32902
+ case "x": {
32903
+ const hex = inner.slice(2);
32904
+ if (!/^[0-9a-fA-F]+$/.test(hex)) return void 0;
32905
+ return parseInt(hex, 16);
32906
+ }
32907
+ default: return esc.charCodeAt(0);
32908
+ }
32909
+ }
32910
+ //#endregion
32911
+ //#region ../src/build_c/utils/c_char_literal.ts
32912
+ function c_char_literal(value) {
32913
+ const code = decode_char_literal(value);
32914
+ if (code === void 0) return value;
32915
+ if (code === 92) return `'\\\\'`;
32916
+ if (code === 39) return `'\\''`;
32917
+ if (code === 10) return `'\\n'`;
32918
+ if (code === 13) return `'\\r'`;
32919
+ if (code === 9) return `'\\t'`;
32920
+ if (code < 32 || code > 126) return `'\\x${code.toString(16)}'`;
32921
+ return `'${String.fromCharCode(code)}'`;
32922
+ }
32923
+ //#endregion
32924
+ //#region ../src/build_c/build_value_node.ts
32925
+ const INT_LITERAL_SUFFIX = {
32926
+ int: "L",
32927
+ uint: "UL",
32928
+ int64: "LL",
32929
+ uint64: "ULL"
32930
+ };
32931
+ function build_value_node$1(node, status) {
32932
+ let value = node.value;
32933
+ if (status.closure_env?.has(value)) {
32934
+ if (!(!!status.current_function?.params.some((p) => p.name === value) || !!find_decl_in_c_scopes(status, value))) {
32935
+ status.code += status.closure_env.get(value);
32936
+ return;
32937
+ }
32938
+ }
32939
+ const inlined = status.top_level_consts?.get(value);
32940
+ if (inlined?.value) {
32941
+ build_node$1(fold_string_const(inlined, status.top_level_consts) ?? inlined.value, status);
32942
+ return;
32943
+ }
32944
+ if (node.is_enum_shorthand) {
32945
+ const enum_node = status.enums.find((e) => value.startsWith(e.name + "_"));
32946
+ if (enum_node) {
32947
+ if (enum_node.has_associated_data) status.code += `${value}_init()`;
32948
+ else status.code += value;
32949
+ return;
32950
+ }
32951
+ }
32952
+ if (value === "null") value = "0";
32953
+ else if (value === "true") value = "1";
32954
+ else if (value === "false") value = "0";
32955
+ else if (value === "default") value = "_nomen_default";
32956
+ else if (is_int_literal(value)) value = value.replace(/_/g, "") + (INT_LITERAL_SUFFIX[node.type?.name ?? "int"] ?? "");
32957
+ else if (node.resolved_function) {
32958
+ status.code += materialize_func_value(node.resolved_function, status);
32959
+ return;
32960
+ } else if (value.startsWith("'") && value.endsWith("'")) value = c_char_literal(value);
32961
+ else value = c_function_name(value);
32962
+ if (value.startsWith("\"")) value = `nomen_str_lit(${escape_c_string(value)}, ${string_literal_length(value)})`;
32963
+ if (value !== "self" && status.function_ref_params?.has(value) && !status.suppress_dereference) status.code += `*`;
32964
+ if (value === "self" && status.function_ref_params?.has("self") && !status.suppress_dereference) status.code += `*`;
32965
+ if (value !== "self" && status.ref_class_params?.has(value) && !status.suppress_dereference) {
32966
+ status.code += `(*${value})`;
32967
+ return;
32968
+ }
32969
+ status.code += value;
32970
+ }
32971
+ function escape_c_string(s) {
32972
+ return reencode_hex_escapes(s.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"));
32973
+ }
32974
+ //#endregion
32266
32975
  //#region ../src/build_c/build_array_values_node.ts
32267
32976
  /**
32268
32977
  * Build an array literal's `{a, b, c}` initializer text. Under NIR-driven
@@ -32274,11 +32983,13 @@ function emit_nursery_joins_on_return_c(status) {
32274
32983
  function build_array_values_node(node, status, nir_elements) {
32275
32984
  status.code += `{`;
32276
32985
  const elem_is_string = node.type?.name === "string";
32986
+ const static_init = status.static_string_array_inits === true;
32277
32987
  node.values.forEach((value, i) => {
32278
32988
  if (i > 0) status.code += ", ";
32279
32989
  const is_string_literal = elem_is_string && value.node_type === "value" && value.value.length >= 2 && value.value.startsWith("\"") && value.value.endsWith("\"");
32280
32990
  const nir_elem = nir_elements?.[i];
32281
- if (is_string_literal) {
32991
+ if (is_string_literal && static_init) status.code += `{ ${escape_c_string(value.value)}, ${string_literal_length(value.value)} }`;
32992
+ else if (is_string_literal) {
32282
32993
  status.code += `nomen_str_dup(`;
32283
32994
  if (nir_elem && nir_elem.node === value) emit_expr_from_nir(nir_elem, status);
32284
32995
  else build_node$1(value, status);
@@ -32421,6 +33132,42 @@ function build_declaration_node(node, status, nir_init, nir_swap) {
32421
33132
  status.code += `] = {${variables.map((v) => `&${v}`).join(", ")}}`;
32422
33133
  } else {
32423
33134
  const safe_name = c_function_name(node.name);
33135
+ if (node.type.name?.startsWith("_Tuple_") && !node.name.startsWith("_tuple_dst_") && node.value?.node_type === "func_call") {
33136
+ const tuple_call = node.value;
33137
+ const tuple_struct = status.structs.find((st) => st.name === node.type.name && !st.is_simple_type);
33138
+ const init_is_tuple_call = !String(tuple_call.name ?? "").startsWith("_Tuple_") && !!tuple_call.type?.name?.startsWith("_Tuple_");
33139
+ const init_is_tuple_literal = String(tuple_call.name ?? "").startsWith("_Tuple_");
33140
+ if (tuple_struct && init_is_tuple_call) for (const field of direct_string_fields(tuple_struct)) {
33141
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
33142
+ status.heap_string_fields.add(`${node.name}.${field.name}`);
33143
+ }
33144
+ else if (tuple_struct && init_is_tuple_literal) for (const field of direct_string_fields(tuple_struct)) {
33145
+ const idx = parseInt(field.name.replace(/^_/, ""), 10);
33146
+ const element = tuple_call.params[idx];
33147
+ if (element && tuple_return_owned_element(element, status.heap_strings)) {
33148
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
33149
+ status.heap_string_fields.add(`${node.name}.${field.name}`);
33150
+ }
33151
+ }
33152
+ }
33153
+ if (node.declaration === "var" && node.type.name === "string" && !node.type.is_array && node.value?.node_type === "access" && node.value.access.node_type === "access_field" && node.value.target.node_type === "value" && String(node.value.target.value ?? "").startsWith("_tuple_dst_")) {
33154
+ const tuple_binding_value = node.value;
33155
+ const temp_name = String(tuple_binding_value.target.value);
33156
+ const temp_init = status.scoped_declarations.findLast((d) => d.name === temp_name)?.value;
33157
+ const m = /^_(\d+)$/.exec(String(tuple_binding_value.access.name ?? ""));
33158
+ if (temp_init?.node_type === "func_call" && !String(temp_init.name ?? "").startsWith("_Tuple_") && !!temp_init.type?.name?.startsWith("_Tuple_")) {
33159
+ if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
33160
+ status.heap_strings.add(safe_name);
33161
+ tuple_binding_value.is_moved = true;
33162
+ } else if (temp_init?.type?.name?.startsWith("_Tuple_") && m) {
33163
+ const param = temp_init.params[parseInt(m[1], 10)];
33164
+ if (param?.node_type === "value" && param.is_moved) {
33165
+ if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
33166
+ status.heap_strings.add(safe_name);
33167
+ tuple_binding_value.is_moved = true;
33168
+ }
33169
+ }
33170
+ }
32424
33171
  const mono_name = mono_type_name(node.type);
32425
33172
  const mono_struct = status.structs.find((s) => s.name === mono_name && !s.is_simple_type && !s.is_generic);
32426
33173
  const is_class_type = !!mono_struct?.is_class;
@@ -32624,6 +33371,7 @@ function build_declaration_node(node, status, nir_init, nir_swap) {
32624
33371
  status.current_nullable_call_flag = flag;
32625
33372
  emit_init_value(node.value, nir_init, status);
32626
33373
  status.current_nullable_call_flag = old;
33374
+ record_call_init_string_fields(node, status);
32627
33375
  } else {
32628
33376
  emit_init_value(node.value, nir_init, status);
32629
33377
  status.code += `;\n${flag} = 1`;
@@ -32693,6 +33441,7 @@ function build_declaration_node(node, status, nir_init, nir_swap) {
32693
33441
  } else if (node.value.node_type === "value" && node.value.value === "null" && node.type.name === "string" && !node.type.is_view && !node.type.is_array) status.code += ` = {0, 0}`;
32694
33442
  else {
32695
33443
  status.code += " = ";
33444
+ status.static_string_array_inits = is_stack_array && !status.current_function && node.type.name === "string";
32696
33445
  const val_node = node.value;
32697
33446
  const val_is_string_literal = node.value.node_type === "value" && val_node.value.length >= 2 && val_node.value.startsWith("\"") && val_node.value.endsWith("\"");
32698
33447
  const val_is_heap_string_var = node.value.node_type === "value" && !val_is_string_literal && !!status.scoped_declarations.find((d) => d.name === val_node.value);
@@ -32730,6 +33479,7 @@ function build_declaration_node(node, status, nir_init, nir_swap) {
32730
33479
  emit_init_value(node.value, nir_init, status);
32731
33480
  status.suppress_dereference = false;
32732
33481
  } else emit_init_value(node.value, nir_init, status);
33482
+ status.static_string_array_inits = false;
32733
33483
  }
32734
33484
  }
32735
33485
  if (node.swap && node.value.node_type === "access") {
@@ -32741,6 +33491,7 @@ function build_declaration_node(node, status, nir_init, nir_swap) {
32741
33491
  emit_swap_value$1(node.swap, nir_swap, status);
32742
33492
  }
32743
33493
  if (has_field_overrides(node.value)) emit_field_overrides(safe_name, node.value, build_node$1, status, ";\n", ";\n");
33494
+ record_call_init_string_fields(node, status);
32744
33495
  }
32745
33496
  }
32746
33497
  }
@@ -33256,6 +34007,7 @@ function build_return_node(node, status, nir_value) {
33256
34007
  }
33257
34008
  }
33258
34009
  let returned_value_decl;
34010
+ let returned_recorded_string_fields;
33259
34011
  if (node.value.node_type === "value") {
33260
34012
  const value = node.value.value;
33261
34013
  returned_value_decl = find_decl_across_scopes(value, status);
@@ -33269,7 +34021,11 @@ function build_return_node(node, status, nir_value) {
33269
34021
  }
33270
34022
  if (status.heap_string_fields?.size) {
33271
34023
  const prefix = `${value}.`;
33272
- for (const key of Array.from(status.heap_string_fields)) if (key.startsWith(prefix)) status.heap_string_fields.delete(key);
34024
+ for (const key of Array.from(status.heap_string_fields)) if (key.startsWith(prefix)) {
34025
+ returned_recorded_string_fields ??= /* @__PURE__ */ new Set();
34026
+ returned_recorded_string_fields.add(key.slice(prefix.length));
34027
+ status.heap_string_fields.delete(key);
34028
+ }
33273
34029
  }
33274
34030
  }
33275
34031
  if (ret_type?.is_array && return_array_var && return_array_len > 0) {
@@ -33378,6 +34134,20 @@ function build_return_node(node, status, nir_value) {
33378
34134
  status.code += `;\n`;
33379
34135
  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
34136
  if (has_field_overrides(node.value)) emit_field_overrides("_return_val", node.value, build_node$1, status, "", ";\n");
34137
+ const bare_local_return = node.value.node_type === "value" && node.value.value !== "0" && node.value.value !== "null" && !!returned_value_decl;
34138
+ 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)) {
34139
+ if (returned_recorded_string_fields?.has(field.name)) continue;
34140
+ status.code += `_return_val.${field.name} = nomen_str_dup(_return_val.${field.name});\n`;
34141
+ }
34142
+ if (is_struct && !return_is_class && !ret_type.is_view && !ret_type.is_array && return_struct && !returns_struct_zero && node.value?.node_type === "func_call" && String(node.value.name ?? "").startsWith("_Tuple_") && !!node.value.type?.name?.startsWith("_Tuple_")) {
34143
+ const tuple_call = node.value;
34144
+ for (const field of direct_string_fields(return_struct)) {
34145
+ const idx = parseInt(field.name.replace(/^_/, ""), 10);
34146
+ const element = tuple_call.params[idx];
34147
+ if (element && tuple_return_owned_element(element, status.heap_strings)) continue;
34148
+ status.code += `_return_val.${field.name} = nomen_str_dup(_return_val.${field.name});\n`;
34149
+ }
34150
+ }
33381
34151
  emit_nursery_joins_on_return_c(status);
33382
34152
  reclaim_all_c_scopes(status);
33383
34153
  status.code += `return _return_val;\n`;
@@ -35142,6 +35912,22 @@ function emit_closure_env_type(func, status) {
35142
35912
  }
35143
35913
  //#endregion
35144
35914
  //#region ../src/build_c/build_function_node.ts
35915
+ /**
35916
+ * True when control cannot reach the end of `statements` — the last statement
35917
+ * is a `return`, or an `if`/`else` whose every arm itself definitely returns.
35918
+ * Used to suppress the fall-through auto-free: when no path falls through, the
35919
+ * epilogue reclaim is unreachable text after the last `return`.
35920
+ */
35921
+ function statements_definitely_return(statements) {
35922
+ if (!statements?.length) return false;
35923
+ const last = statements[statements.length - 1];
35924
+ if (last.node_type === "return") return true;
35925
+ if (last.node_type === "if") {
35926
+ const branch = last;
35927
+ return !!branch.if_branch && !!branch.else_branch && statements_definitely_return(branch.if_branch.statements) && statements_definitely_return(branch.else_branch.statements);
35928
+ }
35929
+ return false;
35930
+ }
35145
35931
  function build_function_node(node, status) {
35146
35932
  if (node.is_generic) return;
35147
35933
  if (node.is_extern) {
@@ -35328,6 +36114,18 @@ function build_function_node(node, status) {
35328
36114
  if (param_trait && !param_struct?.is_class) decl.trait_class_trait = param.type.name;
35329
36115
  status.scoped_declarations.push(decl);
35330
36116
  }
36117
+ const is_free_function = (!node.scope || node.scope.node_type === "root" || node.scope.node_type === "func") && !node.params.some((p) => p.is_self_param);
36118
+ const fn_name = node.name;
36119
+ const fn_label = emission_label(node);
36120
+ 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;
36121
+ 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)) {
36122
+ const decl = new DeclarationNode(param.start, "private", "var", pname, param.type);
36123
+ decl.string_fields_via_pointer = true;
36124
+ decl.owned_heap_shell = !param.is_moved;
36125
+ status.scoped_declarations.push(decl);
36126
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
36127
+ for (const field of direct_string_fields(owned_value_struct)) status.heap_string_fields.add(`${pname}.${field.name}`);
36128
+ }
35331
36129
  }
35332
36130
  }
35333
36131
  const nir = node.statements?.length ? lower_function(node) : void 0;
@@ -35351,7 +36149,8 @@ function build_function_node(node, status) {
35351
36149
  status.current_function_name = old_function_name;
35352
36150
  status.current_function = old_current_function;
35353
36151
  status.closure_env = old_closure_env;
35354
- build_auto_free(status);
36152
+ if (statements_definitely_return(node.statements)) discard_string_field_records(status, status.scoped_declarations);
36153
+ else build_auto_free(status);
35355
36154
  if (node.name.toLocaleLowerCase() === "main" && status.used_fibers) {
35356
36155
  status.code += `\n__nomen_fiber_drain_all();\n`;
35357
36156
  status.code += `__nomen_io_shutdown();\n`;
@@ -36535,6 +37334,7 @@ function build_function_call_node(node, status) {
36535
37334
  const func_name = is_struct ? node.mangled_name ?? `${node.name}_init` : c_function_name(emission_label(node.resolved_function ?? node));
36536
37335
  const callee_params = node.resolved_function?.params?.filter((p) => !p.is_self_param);
36537
37336
  const lambda_arg_temps = /* @__PURE__ */ new Map();
37337
+ mark_tuple_literal_move_owners(node, status);
36538
37338
  if (!is_struct) for (let i = 0; i < node.params.length; i++) {
36539
37339
  const p = node.params[i];
36540
37340
  if (p.node_type !== "func") continue;
@@ -36568,6 +37368,27 @@ function build_function_call_node(node, status) {
36568
37368
  continue;
36569
37369
  }
36570
37370
  const param_type = type_from_value_node$1(node.params[i]);
37371
+ if (node.move_owned_param_indices?.includes(i) && node.params[i].node_type === "value") {
37372
+ const transfer_struct = status.structs.find((st) => st.name === mono_type_name(param_type) && !st.is_simple_type && !st.is_class && !st.is_generic);
37373
+ const transfer_var = node.params[i].value;
37374
+ if (transfer_struct && direct_string_fields(transfer_struct).length > 0 && transfer_var) {
37375
+ const recorded = /* @__PURE__ */ new Set();
37376
+ for (const key of status.heap_string_fields ?? []) if (key.startsWith(`${transfer_var}.`)) recorded.add(key.slice(transfer_var.length + 1));
37377
+ const unrecorded = direct_string_fields(transfer_struct).filter((f) => !recorded.has(f.name));
37378
+ if (unrecorded.length > 0) {
37379
+ const saved_arg = begin_code_scratch(status);
37380
+ status.code += `(void *)&`;
37381
+ status.suppress_dereference = true;
37382
+ build_node$1(node.params[i], status);
37383
+ status.suppress_dereference = false;
37384
+ const addr_text = end_code_scratch(status, saved_arg);
37385
+ status.code += `(void *)({ struct ${transfer_struct.name} *_t = (struct ${transfer_struct.name} *)${addr_text}; `;
37386
+ for (const f of unrecorded) status.code += `_t->${f.name} = nomen_str_dup(_t->${f.name}); `;
37387
+ status.code += `(void *)_t; })`;
37388
+ continue;
37389
+ }
37390
+ }
37391
+ }
36571
37392
  if (node.nullable_param_indices?.includes(i) && node.params[i].node_type === "value" && node.params[i].value === "null") {
36572
37393
  status.code += `(void *)&(struct ${param_type.name}){0}, 0`;
36573
37394
  continue;
@@ -36614,6 +37435,23 @@ function build_function_call_node(node, status) {
36614
37435
  status.suppress_dereference = false;
36615
37436
  continue;
36616
37437
  }
37438
+ const arg_struct = status.structs.find((st) => st.name === mono_type_name(param_type) && !st.is_simple_type && !st.is_class && !st.is_generic);
37439
+ 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)) {
37440
+ const struct_name = arg_struct.name;
37441
+ const fields = direct_string_fields(arg_struct);
37442
+ const saved_arg = begin_code_scratch(status);
37443
+ status.code += `(void *)&`;
37444
+ status.suppress_dereference = true;
37445
+ build_node$1(node.params[i], status);
37446
+ status.suppress_dereference = false;
37447
+ const addr_text = end_code_scratch(status, saved_arg);
37448
+ status.code += `(void *)({ struct ${struct_name} *_src = (struct ${struct_name} *)${addr_text}; `;
37449
+ status.code += `struct ${struct_name} *_own = (struct ${struct_name} *)malloc(sizeof(struct ${struct_name})); `;
37450
+ status.code += `*_own = *_src; `;
37451
+ for (const field of fields) status.code += `_own->${field.name} = nomen_str_dup(_own->${field.name}); `;
37452
+ status.code += `_own; })`;
37453
+ continue;
37454
+ }
36617
37455
  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
37456
  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
37457
  else if (is_ref_param) status.code += `&`;
@@ -36690,9 +37528,12 @@ function build_function_call_node(node, status) {
36690
37528
  if (decl_hit) decl_hit.frame.splice(decl_hit.index, 1);
36691
37529
  if (is_value_struct) {
36692
37530
  const prefix = `${vname}.`;
37531
+ const transfer = node.move_owned_param_indices?.includes(idx) ?? false;
36693
37532
  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);`);
37533
+ if (!transfer) {
37534
+ if (!status.pending_string_releases) status.pending_string_releases = [];
37535
+ status.pending_string_releases.push(`free(${key}.ptr);`);
37536
+ }
36696
37537
  status.heap_string_fields.delete(key);
36697
37538
  }
36698
37539
  }
@@ -36895,101 +37736,6 @@ function build_todo_node$1(node, status) {
36895
37736
  status.code += `exit(EXIT_FAILURE);\n`;
36896
37737
  }
36897
37738
  //#endregion
36898
- //#region ../src/build_common/decode_char_literal.ts
36899
- /**
36900
- * Decode a Nomen char literal token (raw value INCLUDING its surrounding
36901
- * quotes, as carried by ValueNode) to its numeric code. The tokenizer
36902
- * consumes escape pairs as a unit, so multi-char forms like `'\\\\'`,
36903
- * `'\\n'`, `'\\''` and `'\\xNN'` arrive raw and must be decoded here.
36904
- * Returns undefined for malformed literals.
36905
- */
36906
- function decode_char_literal(raw) {
36907
- if (raw.length < 3 || raw[0] !== "'" || raw[raw.length - 1] !== "'") return;
36908
- const inner = raw.slice(1, -1);
36909
- if (inner.length === 1) return inner.charCodeAt(0);
36910
- if (inner[0] !== "\\") return;
36911
- const esc = inner[1];
36912
- switch (esc) {
36913
- case "\\": return 92;
36914
- case "n": return 10;
36915
- case "t": return 9;
36916
- case "r": return 13;
36917
- case "'": return 39;
36918
- case "\"": return 34;
36919
- case "0": return 0;
36920
- case "x": {
36921
- const hex = inner.slice(2);
36922
- if (!/^[0-9a-fA-F]+$/.test(hex)) return void 0;
36923
- return parseInt(hex, 16);
36924
- }
36925
- default: return esc.charCodeAt(0);
36926
- }
36927
- }
36928
- //#endregion
36929
- //#region ../src/build_c/utils/c_char_literal.ts
36930
- function c_char_literal(value) {
36931
- const code = decode_char_literal(value);
36932
- if (code === void 0) return value;
36933
- if (code === 92) return `'\\\\'`;
36934
- if (code === 39) return `'\\''`;
36935
- if (code === 10) return `'\\n'`;
36936
- if (code === 13) return `'\\r'`;
36937
- if (code === 9) return `'\\t'`;
36938
- if (code < 32 || code > 126) return `'\\x${code.toString(16)}'`;
36939
- return `'${String.fromCharCode(code)}'`;
36940
- }
36941
- //#endregion
36942
- //#region ../src/build_c/build_value_node.ts
36943
- const INT_LITERAL_SUFFIX = {
36944
- int: "L",
36945
- uint: "UL",
36946
- int64: "LL",
36947
- uint64: "ULL"
36948
- };
36949
- function build_value_node$1(node, status) {
36950
- let value = node.value;
36951
- if (status.closure_env?.has(value)) {
36952
- if (!(!!status.current_function?.params.some((p) => p.name === value) || !!find_decl_in_c_scopes(status, value))) {
36953
- status.code += status.closure_env.get(value);
36954
- return;
36955
- }
36956
- }
36957
- const inlined = status.top_level_consts?.get(value);
36958
- if (inlined?.value) {
36959
- build_node$1(fold_string_const(inlined, status.top_level_consts) ?? inlined.value, status);
36960
- return;
36961
- }
36962
- if (node.is_enum_shorthand) {
36963
- const enum_node = status.enums.find((e) => value.startsWith(e.name + "_"));
36964
- if (enum_node) {
36965
- if (enum_node.has_associated_data) status.code += `${value}_init()`;
36966
- else status.code += value;
36967
- return;
36968
- }
36969
- }
36970
- if (value === "null") value = "0";
36971
- else if (value === "true") value = "1";
36972
- else if (value === "false") value = "0";
36973
- else if (value === "default") value = "_nomen_default";
36974
- else if (is_int_literal(value)) value = value.replace(/_/g, "") + (INT_LITERAL_SUFFIX[node.type?.name ?? "int"] ?? "");
36975
- else if (node.resolved_function) {
36976
- status.code += materialize_func_value(node.resolved_function, status);
36977
- return;
36978
- } else if (value.startsWith("'") && value.endsWith("'")) value = c_char_literal(value);
36979
- else value = c_function_name(value);
36980
- if (value.startsWith("\"")) value = `nomen_str_lit(${escape_c_string(value)}, ${string_literal_length(value)})`;
36981
- if (value !== "self" && status.function_ref_params?.has(value) && !status.suppress_dereference) status.code += `*`;
36982
- if (value === "self" && status.function_ref_params?.has("self") && !status.suppress_dereference) status.code += `*`;
36983
- if (value !== "self" && status.ref_class_params?.has(value) && !status.suppress_dereference) {
36984
- status.code += `(*${value})`;
36985
- return;
36986
- }
36987
- status.code += value;
36988
- }
36989
- function escape_c_string(s) {
36990
- return reencode_hex_escapes(s.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"));
36991
- }
36992
- //#endregion
36993
37739
  //#region ../src/build_c/build_node.ts
36994
37740
  function build_node$1(node, status, with_semicolon = false) {
36995
37741
  if (node.allocations) {
@@ -38353,6 +39099,10 @@ function get_base_address(access, status, reg) {
38353
39099
  if (paramReg !== reg) emit_asm(status, `mov ${reg}, ${paramReg}\n`);
38354
39100
  } else if (is_local_ref_var(name, status)) emit_deref_var_address(status, reg, name);
38355
39101
  else emit_var_address(status, reg, name);
39102
+ } else if (access_chain_crosses_class(access, status)) {
39103
+ build_node(access.target, status);
39104
+ ensure_newline(status);
39105
+ if (reg !== "x0") emit_asm(status, `mov ${reg}, x0\n`);
38356
39106
  } else {
38357
39107
  emit_address_of(access.target, status);
38358
39108
  ensure_newline(status);
@@ -38475,13 +39225,49 @@ function build_nullable_struct_assignment(node, status, nir_rhs) {
38475
39225
  if (node.left_value.node_type === "value") {
38476
39226
  const name = node.left_value.value;
38477
39227
  const flag_name = has_flag_name(name);
39228
+ const type_name = status.scoped_declarations.find((d) => d.name === name)?.type?.name || status.variable_types?.get(name)?.name || "";
39229
+ const reclaimable = !(node.right_value.node_type === "value" && node.right_value.value === name) && !!status.heap_string_fields?.size && Array.from(status.heap_string_fields).some((k) => k.startsWith(`${name}.`));
38478
39230
  if (rhs_is_null) {
39231
+ if (reclaimable) emit_recorded_field_reclaim_for_slot(status, name, type_name);
38479
39232
  emit_var_store(status, "xzr", flag_name, 8);
39233
+ drop_heap_string_field_records(status, name);
39234
+ return;
39235
+ }
39236
+ const retarget_records = () => {
39237
+ drop_heap_string_field_records(status, name);
39238
+ if (is_normalized_struct_call(node.right_value, status)) {
39239
+ const struct = status.structs.find((s) => s.name === type_name && !s.is_simple_type && !s.is_class);
39240
+ if (struct) {
39241
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
39242
+ for (const field of direct_string_fields(struct)) status.heap_string_fields.add(`${name}.${field.name}`);
39243
+ }
39244
+ }
39245
+ };
39246
+ if (node.right_value.node_type === "func_call" && is_nullable_struct_type(node.right_value.type, status)) {
39247
+ if (reclaimable) emit_recorded_field_reclaim_for_slot(status, name, type_name);
39248
+ retarget_records();
39249
+ const old_buffer = status.struct_return_buffer;
39250
+ const old_buffer_var = status.struct_return_buffer_var;
39251
+ const old_preset = status.call_x8_preset;
39252
+ emit_var_address(status, "x8", name);
39253
+ status.struct_return_buffer_var = name;
39254
+ status.struct_return_buffer = "x8";
39255
+ status.call_x8_preset = true;
39256
+ emit_rhs_value(node.right_value, nir_rhs, status);
39257
+ status.struct_return_buffer = old_buffer;
39258
+ status.struct_return_buffer_var = old_buffer_var;
39259
+ status.call_x8_preset = old_preset;
38480
39260
  return;
38481
39261
  }
38482
39262
  emit_rhs_value(node.right_value, nir_rhs, status);
38483
39263
  ensure_newline(status);
38484
- const struct_size = get_struct_size(status.scoped_declarations.find((d) => d.name === name)?.type?.name || status.variable_types?.get(name)?.name || "", status);
39264
+ if (reclaimable) {
39265
+ emit_asm(status, `str x0, [sp, #-16]!\n`);
39266
+ emit_recorded_field_reclaim_for_slot(status, name, type_name);
39267
+ emit_asm(status, `ldr x0, [sp], #16\n`);
39268
+ }
39269
+ retarget_records();
39270
+ const struct_size = get_struct_size(type_name, status);
38485
39271
  const dst_offset = status.stack_offsets?.get(name);
38486
39272
  emit_struct_copy("x0", "x29", dst_offset ?? 0, struct_size, status);
38487
39273
  emit_asm(status, `mov x9, #1\n`);
@@ -40775,6 +41561,7 @@ function is_owned_heap_temp(node, status) {
40775
41561
  if (check_type_name !== "string") return false;
40776
41562
  if (check_node.node_type === "op") return true;
40777
41563
  if (check_node.node_type === "func_call" || check_node.node_type === "access_func") {
41564
+ if (!check_node.type?.is_view && (check_node.is_func_param || check_node.is_func_field_call)) return true;
40778
41565
  const raw_name = check_node.name;
40779
41566
  const mangled = check_node.mangled_name || raw_name;
40780
41567
  const dbg = process.env.NOMEN_DBG_TEMP && typeof mangled === "string" && mangled.indexOf("replace") >= 0;
@@ -41871,6 +42658,9 @@ function emit_address_of(node, status) {
41871
42658
  if (paramReg !== "x0") emit_asm(status, `mov x0, ${paramReg}\n`);
41872
42659
  } else if (is_local_ref_var(name, status)) emit_deref_var_address(status, "x0", name);
41873
42660
  else emit_var_address(status, "x0", name);
42661
+ } else if (access_chain_crosses_class(access, status)) {
42662
+ build_node(access.target, status);
42663
+ ensure_newline(status);
41874
42664
  } else {
41875
42665
  emit_address_of(access.target, status);
41876
42666
  ensure_newline(status);
@@ -42014,6 +42804,22 @@ function resolve_access_target_type(target, status) {
42014
42804
  * final field is read. Without this, `state.rules.blocks.length` (State/
42015
42805
  * BlockParserState is a class) summed 8+8+8 and loaded from `state + 24`.
42016
42806
  */
42807
+ /**
42808
+ * True when every hop in the access chain is a FIELD access (no call
42809
+ * segments) — the shape whose field address is the root base plus the sum of
42810
+ * field offsets. A call segment (`arena.get(handle).children`,
42811
+ * `make().field`) makes the summed-offset computation invalid: the call
42812
+ * result is different storage than the root base, so the receiver address
42813
+ * must be built from the call (`build_node` of the object) plus the field
42814
+ * offset instead.
42815
+ */
42816
+ function chain_is_pure_fields(node) {
42817
+ if (node.node_type === "value") return true;
42818
+ if (node.node_type !== "access") return false;
42819
+ const acc = node;
42820
+ if (acc.access.node_type !== "access_field") return false;
42821
+ return chain_is_pure_fields(acc.target);
42822
+ }
42017
42823
  function access_chain_crosses_class(node, status) {
42018
42824
  const base_type = resolve_access_target_type(get_base_target(node), status);
42019
42825
  if (base_type?.name && status.structs.find((s) => s.name === base_type.name && s.is_class)) return true;
@@ -42436,6 +43242,12 @@ function build_access_field(node, status) {
42436
43242
  ensure_newline(status);
42437
43243
  const final_offset = offset;
42438
43244
  const field_type = access_field.type?.name || "";
43245
+ const method_field_type = status.structs.find((s) => s.name === (target_type?.name || "") && !s.is_simple_type)?.fields.find((f) => f.name === access_field.name)?.type ?? access_field.type;
43246
+ const method_field_name = method_field_type?.type_args?.length ? mono_type_name(method_field_type) : method_field_type?.name || "";
43247
+ if (method_field_name && !method_field_type?.is_ref && !method_field_type?.is_nullable && is_struct_type(method_field_name, status)) {
43248
+ if (final_offset > 0) emit_asm(status, `add x0, x0, #${final_offset}\n`);
43249
+ return;
43250
+ }
42439
43251
  const at_reg = status.at_addr_reg;
42440
43252
  status.at_addr_reg = void 0;
42441
43253
  const load_base = at_reg ?? "x0";
@@ -42457,7 +43269,7 @@ function build_access_field(node, status) {
42457
43269
  else emit_asm(status, `ldr x0, [${load_base}, #${final_offset}]\n`);
42458
43270
  return;
42459
43271
  }
42460
- if (target_is_class_access) {
43272
+ if (target_is_class_access || !chain_is_pure_fields(node.target)) {
42461
43273
  build_node(node.target, status);
42462
43274
  ensure_newline(status);
42463
43275
  const final_offset = get_field_offset(target_type?.name || "", access_field.name, status);
@@ -43060,9 +43872,11 @@ function build_access_method(node, access_func, status) {
43060
43872
  } else if (!target_is_simple && node.target.node_type === "access") {
43061
43873
  const access_target = node.target;
43062
43874
  if (access_target.access.node_type === "access_field") {
43063
- if (access_chain_crosses_class(access_target, status)) {
43064
- build_node(access_target, status);
43875
+ if (!chain_is_pure_fields(access_target) || access_chain_crosses_class(access_target, status)) {
43876
+ const field_offset = get_field_offset(type_from_value_node$1(access_target.target)?.name || "", access_target.access.name, status);
43877
+ build_node(access_target.target, status);
43065
43878
  ensure_newline(status);
43879
+ if (field_offset > 0) emit_asm(status, `add x0, x0, #${field_offset}\n`);
43066
43880
  } else {
43067
43881
  const offset = compute_field_offset(access_target, status);
43068
43882
  const base = get_base_target(access_target);
@@ -44254,6 +45068,195 @@ function scan_func(func, struct_name, result, class_type_names) {
44254
45068
  }
44255
45069
  }
44256
45070
  //#endregion
45071
+ //#region ../src/build_common/struct_return_classification.ts
45072
+ /**
45073
+ * Pre-build classification of which functions return value structs whose
45074
+ * string fields are UNIFORMLY heap-owned via return-boundary normalization.
45075
+ * Populates `status.normalized_struct_returners`; a caller binding a call
45076
+ * result of a registered function records the struct's string fields and
45077
+ * frees them at scope exit, while bindings of unregistered callees keep the
45078
+ * pre-existing borrow behavior (never recorded, never freed — sound, though
45079
+ * it may leak on slot overwrites, which is the documented status quo).
45080
+ *
45081
+ * The rule is deliberately narrow: a function normalizes iff EVERY
45082
+ * struct-return statement returns a BARE VARIABLE (a local or parameter).
45083
+ * That is the transfer shape — the returned variable dies at the return, so
45084
+ * the boundary normalization (transfer recorded heap fields, strdup the
45085
+ * rest) is sound and leak-free. Every other shape keeps the status quo:
45086
+ *
45087
+ * - container borrow accessors (`at`/`first`/`slice`/`load` — e.g.
45088
+ * `Map.get`'s `return self.values.load_T(idx)`) yield SLOT-owned bytes;
45089
+ * normalizing them would leak in expression-temp consumers
45090
+ * (`m.get(k).field`) that nobody records or frees;
45091
+ * - owned accessors (`pop`/`move_T`/`copy`) transfer slot-owned heap
45092
+ * buffers — the caller's recording is sound WITHOUT normalization, so
45093
+ * they record via the owned-accessor rule (see
45094
+ * call_init_string_fields.ts) and the callee skips the copy;
45095
+ * - forwarded calls (`return make()`) are skipped when the callee is
45096
+ * registered (its result is already uniformly owned — copying it would
45097
+ * leak the inner buffers) and keep the status quo when it is not.
45098
+ *
45099
+ * This runs as a WHOLE-PROGRAM AST PRE-PASS (before any function body is
45100
+ * built) because a build-order-sensitive registry cannot work: nested
45101
+ * functions are built after their enclosing function's body, so a runtime
45102
+ * registration would be invisible to the caller's earlier lookup.
45103
+ */
45104
+ /** Collect every function and struct declaration under the AST (including
45105
+ * nested functions, struct methods, and function bodies at any depth). */
45106
+ function collect(node, fns, structs, seen) {
45107
+ if (!node || typeof node !== "object" || seen.has(node)) return;
45108
+ seen.add(node);
45109
+ if (node.node_type === "func") fns.push(node);
45110
+ if (node.node_type === "struct") structs.push(node);
45111
+ for (const key of Object.keys(node)) {
45112
+ if (key === "parent" || key === "scope") continue;
45113
+ const value = node[key];
45114
+ if (Array.isArray(value)) {
45115
+ for (const item of value) if (item && typeof item === "object" && "node_type" in item) collect(item, fns, structs, seen);
45116
+ } else if (value && typeof value === "object" && "node_type" in value) collect(value, fns, structs, seen);
45117
+ }
45118
+ }
45119
+ /** The DIRECT `return <expr>` statements of a function body (nested function
45120
+ * declarations are not descended into — their returns belong to them). */
45121
+ function direct_returns(fn) {
45122
+ const out = [];
45123
+ const seen = /* @__PURE__ */ new Set();
45124
+ const walk = (node) => {
45125
+ if (!node || typeof node !== "object" || seen.has(node)) return;
45126
+ seen.add(node);
45127
+ if (node.node_type === "func" && node !== fn) return;
45128
+ if (node.node_type === "return") out.push(node);
45129
+ for (const key of Object.keys(node)) {
45130
+ if (key === "parent" || key === "scope") continue;
45131
+ const value = node[key];
45132
+ if (Array.isArray(value)) {
45133
+ for (const item of value) if (item && typeof item === "object" && "node_type" in item) walk(item);
45134
+ } else if (value && typeof value === "object" && "node_type" in value) walk(value);
45135
+ }
45136
+ };
45137
+ for (const statement of fn.statements ?? []) walk(statement);
45138
+ return out;
45139
+ }
45140
+ /** Whether the function is a FREE function: not a method of a struct (whose
45141
+ * element/slot params are passed by address for in-place access) and not a
45142
+ * monomorphized container clone. */
45143
+ function is_free_function_node(fn) {
45144
+ const scope = fn.scope;
45145
+ return !scope || scope.node_type === "root" || scope.node_type === "func";
45146
+ }
45147
+ /**
45148
+ * Whole-program pre-pass: classify free functions that take an OWNING value
45149
+ * struct parameter (string fields) — those get pass-by-value semantics:
45150
+ *
45151
+ * - the CALL SITE materializes a uniformly heap-owned shell (struct-copy
45152
+ * + per-string-field strdup) and passes its address;
45153
+ * - the CALLEE seeds the param's string-field records at entry and frees
45154
+ * the surviving fields (and the shell) at scope exit.
45155
+ *
45156
+ * Writes through the param then stay local to the callee; the caller's
45157
+ * variable is never aliased or mutated. Functions whose address escapes
45158
+ * into a func-typed value (`var f = g`, `apply(g, …)`, `b.modify(0, touch)`)
45159
+ * are EXCLUDED: they may be invoked through a pointer with a borrowed
45160
+ * argument, and keep the status quo (borrow + the documented write leak).
45161
+ *
45162
+ * Populates `status.normalized_struct_returners` (the materializing set) and
45163
+ * `status.func_address_taken` (the excluded set) for the call-site and
45164
+ * function-definition builders.
45165
+ */
45166
+ function gather_normalized_struct_returners(root, status) {
45167
+ const fns = [];
45168
+ const structs = [];
45169
+ collect(root, fns, structs, /* @__PURE__ */ new Set());
45170
+ const address_taken = /* @__PURE__ */ new Set();
45171
+ const value_walk = (node) => {
45172
+ if (!node || typeof node !== "object") return;
45173
+ if (node.node_type === "value") {
45174
+ const v = node;
45175
+ if (typeof v.value === "string" && v.type?.name === "func" && /^[A-Za-z_]/.test(v.value)) address_taken.add(v.value);
45176
+ }
45177
+ for (const key of Object.keys(node)) {
45178
+ if (key === "parent" || key === "scope") continue;
45179
+ const child = node[key];
45180
+ if (Array.isArray(child)) {
45181
+ for (const item of child) if (item && typeof item === "object" && "node_type" in item) value_walk(item);
45182
+ } else if (child && typeof child === "object" && "node_type" in child) value_walk(child);
45183
+ }
45184
+ };
45185
+ value_walk(root);
45186
+ const decl_walk = (node) => {
45187
+ if (!node || typeof node !== "object") return;
45188
+ const d = node;
45189
+ if (d.node_type === "declaration" && typeof d.name === "string" && d.type?.name === "func") address_taken.add(d.name);
45190
+ for (const key of Object.keys(node)) {
45191
+ if (key === "parent" || key === "scope") continue;
45192
+ const child = node[key];
45193
+ if (Array.isArray(child)) {
45194
+ for (const item of child) if (item && typeof item === "object" && "node_type" in item) decl_walk(item);
45195
+ } else if (child && typeof child === "object" && "node_type" in child) decl_walk(child);
45196
+ }
45197
+ };
45198
+ decl_walk(root);
45199
+ const return_candidates = [];
45200
+ for (const fn of fns) {
45201
+ if (!is_free_function_node(fn)) continue;
45202
+ if (address_taken.has(fn.name)) continue;
45203
+ const ret = fn.return_type;
45204
+ if (!ret || ret.is_array || ret.is_view) continue;
45205
+ const ret_struct = structs.find((st) => st.name === ret.name && !st.is_simple_type && !st.is_class);
45206
+ if (!ret_struct || direct_string_fields(ret_struct).length === 0) continue;
45207
+ return_candidates.push(fn);
45208
+ }
45209
+ const return_normalizing = new Set(return_candidates.map((c) => c.name));
45210
+ let changed = true;
45211
+ while (changed) {
45212
+ changed = false;
45213
+ for (const fn of return_candidates) {
45214
+ if (!return_normalizing.has(fn.name)) continue;
45215
+ for (const ret of direct_returns(fn)) {
45216
+ const value = ret.value ?? void 0;
45217
+ if (value && value.node_type === "value") continue;
45218
+ if (value && value.node_type === "func_call" && return_normalizing.has(value.name ?? "")) continue;
45219
+ return_normalizing.delete(fn.name);
45220
+ changed = true;
45221
+ break;
45222
+ }
45223
+ }
45224
+ }
45225
+ for (const fn of fns) {
45226
+ if (!is_free_function_node(fn)) continue;
45227
+ if (address_taken.has(fn.name)) continue;
45228
+ if (fn.params.some((p) => p.is_self_param)) continue;
45229
+ if (fn.address_escaped) continue;
45230
+ if (!fn.has_body) continue;
45231
+ for (const param of fn.params ?? []) {
45232
+ if (param.is_self_param || param.is_variadic) continue;
45233
+ if (param.type.is_ref || param.type.is_nullable || param.type.is_view || param.type.is_array) continue;
45234
+ const struct = param.type.name ? structs.find((st) => st.name === param.type.name && !st.is_simple_type && !st.is_class) : void 0;
45235
+ if (!struct) continue;
45236
+ if (direct_string_fields(struct).length > 0 && !(struct.traits ?? []).length && !struct_needs_destroy(struct, status)) {
45237
+ if (!status.normalized_struct_returners) status.normalized_struct_returners = /* @__PURE__ */ new Set();
45238
+ status.normalized_struct_returners.add(fn.name);
45239
+ status.normalized_struct_returners.add(emission_label(fn));
45240
+ break;
45241
+ }
45242
+ }
45243
+ if (return_normalizing.has(fn.name)) {
45244
+ if (!status.normalized_struct_returners) status.normalized_struct_returners = /* @__PURE__ */ new Set();
45245
+ status.normalized_struct_returners.add(fn.name);
45246
+ status.normalized_struct_returners.add(emission_label(fn));
45247
+ }
45248
+ }
45249
+ for (const struct of structs) {
45250
+ if (struct.is_class || struct.is_generic || struct.is_simple_type) continue;
45251
+ if (!struct.functions.some((f) => f.name === "copy" && f.has_body)) continue;
45252
+ if (direct_string_fields(struct).length === 0) continue;
45253
+ if (!status.normalized_struct_returners) status.normalized_struct_returners = /* @__PURE__ */ new Set();
45254
+ status.normalized_struct_returners.add(`${struct.name}_copy`);
45255
+ }
45256
+ if (!status.func_address_taken) status.func_address_taken = /* @__PURE__ */ new Set();
45257
+ for (const name of address_taken) status.func_address_taken.add(name);
45258
+ }
45259
+ //#endregion
44257
45260
  //#region ../src/build.ts
44258
45261
  function build(root, options = {}) {
44259
45262
  let status = {
@@ -44287,6 +45290,7 @@ function build(root, options = {}) {
44287
45290
  reset_match_temp_counter();
44288
45291
  reset_ns_tmp_counter();
44289
45292
  stamp_last_use_moves(root);
45293
+ gather_normalized_struct_returners(root, status);
44290
45294
  if (options.arch === "aarch64") {
44291
45295
  install_asm_code_buffer(status);
44292
45296
  reset_string_counter$1();
@@ -46510,6 +47514,23 @@ function is_owning_struct_type_requiring_move(type, status) {
46510
47514
  if (!s || s.is_class) return false;
46511
47515
  return struct_owns_non_string_heap(s, status, /* @__PURE__ */ new Set());
46512
47516
  }
47517
+ /**
47518
+ * Whether `type` is a concrete value struct with direct string fields whose
47519
+ * only ownership those strings — the pass-by-value convention's domain: a
47520
+ * parameter of this type is fully owned by the callee (the call boundary
47521
+ * materializes a uniformly heap-owned copy), so `var` field writes inside
47522
+ * the body stay local and no call-site acknowledgment is needed. Excludes
47523
+ * structs that own non-string heap (those keep `move ... swap` / `ref`
47524
+ * semantics) and structs conforming to traits (trait dispatch aliases).
47525
+ */
47526
+ function is_pass_by_value_owning_struct(type, status) {
47527
+ const s = resolve_struct$1(type, status);
47528
+ if (!s || s.is_class || s.is_generic || s.is_simple_type) return false;
47529
+ if ((s.traits ?? []).length > 0) return false;
47530
+ if (direct_string_fields(s).length === 0) return false;
47531
+ if (struct_owns_non_string_heap(s, status, /* @__PURE__ */ new Set())) return false;
47532
+ return true;
47533
+ }
46513
47534
  function struct_owns_non_string_heap(s, status, visited) {
46514
47535
  if (visited.has(s.name)) return false;
46515
47536
  visited.add(s.name);
@@ -47051,9 +48072,11 @@ function mismatch(expected, actual, status, start) {
47051
48072
  * when either side's signature is unavailable.
47052
48073
  */
47053
48074
  function check_func_argument_signature(expected, arg, status, start) {
48075
+ const expected_params = expected.func_params ?? expected.type?.func_params;
48076
+ const expected_return = expected.func_return_type ?? expected.type?.func_return_type;
47054
48077
  const expected_sig = {
47055
- params: expected.func_params ?? expected.type?.func_params,
47056
- return_type: expected.func_return_type ?? expected.type?.func_return_type
48078
+ params: expected_params ?? (expected_return ? [] : void 0),
48079
+ return_type: expected_return
47057
48080
  };
47058
48081
  if (!expected_sig.params && !expected_sig.return_type) return;
47059
48082
  const actual_sig = arg_signature(arg, status);
@@ -49150,6 +50173,12 @@ function check_declaration_node(decl, status) {
49150
50173
  if (is_view_keyword && decl.type_start !== void 0) decl.type.is_view = true;
49151
50174
  if (decl.func_params) {
49152
50175
  if (is_view_keyword) add_error(status, `'view' cannot declare a function-typed binding`, decl.start);
50176
+ if (decl.value?.node_type === "func") decl.value.address_escaped = true;
50177
+ else if (decl.value?.node_type === "value") {
50178
+ const fn = status.functions.findLast((f) => f.name === decl.value.value);
50179
+ if (fn) fn.address_escaped = true;
50180
+ }
50181
+ 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
50182
  if (decl.func_return_type) {
49154
50183
  check_type_exists(decl.func_return_type, status, -1);
49155
50184
  const materialized = materialize_type(decl.func_return_type, status);
@@ -49214,7 +50243,7 @@ function check_declaration_node(decl, status) {
49214
50243
  status.values.push({
49215
50244
  declaration,
49216
50245
  name: decl.name,
49217
- type: decl.func_return_type || decl.type,
50246
+ type: decl.type.name === "func" && decl.type.is_nullable ? decl.type : decl.func_return_type || decl.type,
49218
50247
  is_set: !!decl.value,
49219
50248
  start: decl.start,
49220
50249
  is_null: decl.value?.node_type === "value" && decl.value.value === "null",
@@ -49278,10 +50307,18 @@ function check_declaration_node(decl, status) {
49278
50307
  if (decl.value?.node_type === "access" && decl.value.access.node_type === "access_field") {
49279
50308
  const field_type = type_from_value_node(decl.value, status);
49280
50309
  if (field_type.name && is_owning_struct_type_requiring_move(field_type, status)) {
49281
- const field_name = decl.value.access.name;
49282
- if (!decl.value.is_moved) add_error(status, `cannot copy '${field_type.name}' out of field '${field_name}' by value — it owns heap resources; use 'move ... swap <replacement>'`, decl.value.start);
49283
- else if (!decl.swap) add_error(status, `move out of a field requires a swap to revalidate it`, decl.value.start);
49284
- else if (check_node(decl.swap, status)) check_type_and_value_match(field_type, type_from_value_node(decl.swap, status), void 0, status, decl.swap.start, "swap");
50310
+ const access_field = decl.value.access;
50311
+ const field_name = access_field.name;
50312
+ const temp_rhs = af_is_destructure_temp(decl.value.target.node_type === "value" ? decl.value.target.value : "");
50313
+ if (!decl.value.is_moved && !temp_rhs) add_error(status, `cannot copy '${field_type.name}' out of field '${field_name}' by value — it owns heap resources; use 'move ... swap <replacement>'`, decl.value.start);
50314
+ else if (!decl.swap) {
50315
+ if (!access_field.destructure_move && !temp_rhs) add_error(status, `move out of a field requires a swap to revalidate it`, decl.value.start);
50316
+ else {
50317
+ decl.value.is_moved = true;
50318
+ decl.swap = synthesized_swap_value(field_type, decl.value.start);
50319
+ if (check_node(decl.swap, status)) check_type_and_value_match(field_type, type_from_value_node(decl.swap, status), void 0, status, decl.swap.start, "swap");
50320
+ }
50321
+ } else if (check_node(decl.swap, status)) check_type_and_value_match(field_type, type_from_value_node(decl.swap, status), void 0, status, decl.swap.start, "swap");
49285
50322
  }
49286
50323
  }
49287
50324
  if (decl.value?.node_type === "value" && decl.value.is_moved && !decl.swap) {
@@ -49425,6 +50462,33 @@ function extract_const_value(value) {
49425
50462
  if (/^[+-]?\d+$/.test(vn.value)) return parseInt(vn.value, 10);
49426
50463
  if (/^[+-]?\d+\.\d+$/.test(vn.value)) return parseFloat(vn.value);
49427
50464
  }
50465
+ /**
50466
+ * The revalidating swap value synthesized for a move-DESTRUCTURING binding
50467
+ * (`var [move a] = t`): a fresh default of the moved field's type. A string
50468
+ * swaps in an empty literal; an owning struct (List/Buffer/Map/...) swaps in
50469
+ * its no-argument constructor (type_args carried so `List<string>` swaps in
50470
+ * `List<string>()`). The zero/empty replacement makes the source field's own
50471
+ * cleanup a no-op instead of a double-free of the transferred buffer.
50472
+ */
50473
+ function synthesized_swap_value(field_type, start) {
50474
+ if (field_type.name === "string") return new ValueNode(start, "\"\"", new Type("string"));
50475
+ const constructor = new FunctionCallNode(start, field_type.name);
50476
+ constructor.type = new Type(field_type.name);
50477
+ if (field_type.type_args?.length) constructor.type_args = field_type.type_args;
50478
+ return constructor;
50479
+ }
50480
+ /**
50481
+ * Whether a destructuring binding's access targets the compiler-created
50482
+ * `_tuple_dst_N` temp (a non-simple destructuring RHS): the temp exists only
50483
+ * to feed the bindings and is never read again, so an owning field's
50484
+ * ownership transfer to the binding is INFERRED (a view binding would
50485
+ * dangle — the temp's cleanup frees its fields). (`is_destructure` itself
50486
+ * is cleared by the time the binding's owning-field check runs — the access
50487
+ * rewrite consumes it — so only the temp-name convention identifies this.)
50488
+ */
50489
+ function af_is_destructure_temp(target_name) {
50490
+ return !!target_name && target_name.startsWith("_tuple_dst_");
50491
+ }
49428
50492
  //#endregion
49429
50493
  //#region ../src/check/utils/check_init_fields.ts
49430
50494
  const checked_structs = /* @__PURE__ */ new WeakSet();
@@ -49712,7 +50776,7 @@ function check_struct_node(struct, status) {
49712
50776
  continue;
49713
50777
  }
49714
50778
  if (decl.func_params) {
49715
- decl.type = new Type("func");
50779
+ if (decl.type.name !== "func") decl.type = new Type("func");
49716
50780
  check_declaration_node(decl, status);
49717
50781
  continue;
49718
50782
  }
@@ -50020,6 +51084,7 @@ function synthesize_for_struct(struct, status) {
50020
51084
  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
51085
  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
51086
  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));
51087
+ synthesize_copy_method(struct, status);
50023
51088
  }
50024
51089
  function build_to_string(struct, fields) {
50025
51090
  const parts = [string_literal(`${struct.name}(`)];
@@ -50051,6 +51116,81 @@ function build_hash(struct, fields) {
50051
51116
  const ret = new ReturnNode(-1, expr);
50052
51117
  return new FunctionNode(-1, "pub", "hash", new Type("uint"), [self_param(struct)], [ret]);
50053
51118
  }
51119
+ /**
51120
+ * Whether `struct` is eligible for a synthesized `copy` method: a value
51121
+ * struct whose only ownership is its DIRECT string fields — the same set
51122
+ * the pass-by-value argument convention covers. A struct that additionally
51123
+ * owns non-string heap (a resource-releasing `#destroy`, a class field, a
51124
+ * nested requiring-move struct) cannot be deep-copied by "byte copy +
51125
+ * strdup the strings" — its copy stays unwritten (move/swap remains the
51126
+ * escape hatch) — and a struct with no string fields is already soundly
51127
+ * byte-copyable, so it needs no method.
51128
+ */
51129
+ function struct_is_copyable(struct, status) {
51130
+ if (struct.is_class || struct.is_generic || struct.is_simple_type) return false;
51131
+ if (direct_string_fields(struct).length === 0) return false;
51132
+ if (struct_owns_non_string_heap(struct, status, /* @__PURE__ */ new Set())) return false;
51133
+ return struct_copy_ctor_args(struct) !== null;
51134
+ }
51135
+ /**
51136
+ * How a synthesized `copy` constructs the fresh value: the zero-argument
51137
+ * `#init` overload when one exists (auto or user), else the bodyless AUTO
51138
+ * `#init` whose params map 1:1 onto non-defaulted fields (pass
51139
+ * `self.<param>` for each). Null when neither shape exists — a user #init
51140
+ * with params only cannot be invoked without re-running its custom logic,
51141
+ * so no `copy` is synthesized.
51142
+ */
51143
+ function struct_copy_ctor_args(struct) {
51144
+ const inits = struct.functions.filter((f) => f.name === "#init");
51145
+ if (inits.some((f) => f.params.filter((p) => !p.is_self_param).length === 0)) return [];
51146
+ const auto = inits.find((f) => !f.has_body);
51147
+ if (!auto) return null;
51148
+ return auto.params.filter((p) => !p.is_self_param).map((p) => p.name);
51149
+ }
51150
+ /** Whether `type` names a struct that has (or will get) a `copy` method.
51151
+ * Order-independent: it re-evaluates the eligibility predicate instead of
51152
+ * looking for an already-synthesized method, so an outer struct nested in
51153
+ * the same pre-pass resolves a later-declared field struct. */
51154
+ function field_type_is_copyable(type, status) {
51155
+ if (!type.name || type.is_array || type.is_ref || type.is_view || type.is_nullable) return false;
51156
+ const nested = status.structs.find((s) => s.name === type.name);
51157
+ return !!nested && struct_is_copyable(nested, status);
51158
+ }
51159
+ /**
51160
+ * Synthesize `pub func copy(self) -> Struct` for owning value structs: a
51161
+ * fresh `Struct()` whose every field is rewritten from `self` — string
51162
+ * field writes strdup the source (the backends' field-write path), nested
51163
+ * copyable structs recurse through their own `copy`, everything else byte
51164
+ * copies. `return c` hits the return-boundary normalization (all fields
51165
+ * recorded → transfer raw), so the caller receives a uniformly heap-owned
51166
+ * value with no extra copies.
51167
+ */
51168
+ function build_copy(struct, status) {
51169
+ const ctor_args = struct_copy_ctor_args(struct) ?? [];
51170
+ const ctor = new FunctionCallNode(-1, struct.name, new Type(struct.name), ctor_args.map((name) => field_access("self", name)));
51171
+ const statements = [new DeclarationNode(-1, "private", "var", "c", new Type(struct.name), ctor)];
51172
+ for (const field of struct.fields) {
51173
+ const right = field_type_is_copyable(field.type, status) ? method_call(field_access("self", field.name), "copy") : field_access("self", field.name);
51174
+ statements.push(new AssignmentNode(-1, field_access("c", field.name), right, "="));
51175
+ }
51176
+ statements.push(new ReturnNode(-1, new ValueNode(-1, "c")));
51177
+ return new FunctionNode(-1, "pub", "copy", new Type(struct.name), [self_param(struct)], statements);
51178
+ }
51179
+ /**
51180
+ * Synthesize the `copy` method on `struct` when eligible and not already
51181
+ * present. Called from the block-level derive pre-pass (which runs before
51182
+ * the struct is checked, so the method's body is checked with it) AND from
51183
+ * `monomorphize` — a mono struct materializes mid-check, after that pre-pass
51184
+ * ran, so without this hook `Box<string>.copy()` — advertised by the
51185
+ * copy-discipline error messages — would resolve to "Function not found".
51186
+ * The caller is responsible for checking the synthesized body when no later
51187
+ * struct check will (see monomorphize).
51188
+ */
51189
+ function synthesize_copy_method(struct, status) {
51190
+ if (struct_has_function(struct, "copy")) return;
51191
+ if (!struct_is_copyable(struct, status)) return;
51192
+ struct.functions.push(build_copy(struct, status));
51193
+ }
50054
51194
  //#endregion
50055
51195
  //#region ../src/check/check_block_node.ts
50056
51196
  function check_block_node(node, status) {
@@ -50502,8 +51642,8 @@ function check_function_parameter_node(param, status, is_library_scope = false)
50502
51642
  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
51643
  instantiate_generic_type(param.type, status);
50504
51644
  }
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);
51645
+ 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);
51646
+ 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
51647
  if (param.is_variadic && param.default_value) {
50508
51648
  add_error(status, `Variadic parameter cannot have a default value`, param.start);
50509
51649
  return;
@@ -50515,7 +51655,7 @@ function check_function_parameter_node(param, status, is_library_scope = false)
50515
51655
  }
50516
51656
  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
51657
  status.values.push({
50518
- declaration: param.declaration,
51658
+ declaration: param.declaration === "var" || is_pass_by_value_owning_struct(param.type, status) ? "var" : param.declaration,
50519
51659
  name: param.name,
50520
51660
  type: param.type,
50521
51661
  is_set: true,
@@ -50932,7 +52072,7 @@ function check_function_call_node(node, status) {
50932
52072
  return false;
50933
52073
  }
50934
52074
  if (magic_ctor && spawn_ctor_shape) return check_magic_ctor(node, status, magic_ctor);
50935
- let func = find_free_function(status, node.name);
52075
+ let func = status.values.findLast((v) => v.name === node.name && v.func_params !== void 0) ? void 0 : find_free_function(status, node.name);
50936
52076
  if (func?.is_closure && func.captures?.length) func = void 0;
50937
52077
  if (!func) {
50938
52078
  const struct = resolve_declared_struct(node.name, status);
@@ -51001,6 +52141,10 @@ function check_function_call_node(node, status) {
51001
52141
  const param_index = status.values.findLastIndex((v) => v.name === node.name);
51002
52142
  const param_value = param_index >= 0 ? status.values[param_index] : void 0;
51003
52143
  if (!!param_value && (param_value.type.name === "func" || param_value.func_params !== void 0 || param_value.func_return_type !== void 0) && param_value) {
52144
+ if (param_value.is_null && !status.allow_null_value) {
52145
+ add_error(status, `Variable '${node.name}' may be null`, node.start);
52146
+ return false;
52147
+ }
51004
52148
  if (status.moved_variables?.has(node.name)) {
51005
52149
  add_error(status, `Variable '${node.name}' used after move`, node.start);
51006
52150
  return false;
@@ -51229,6 +52373,30 @@ function monomorphize(generic_struct, type_args, status) {
51229
52373
  const init_func = new FunctionNode(generic_struct.start, "pub", "#init", init_return_type, init_params);
51230
52374
  mono_struct.functions.push(init_func);
51231
52375
  }
52376
+ synthesize_copy_method(mono_struct, status);
52377
+ const mono_copy = mono_struct.functions.find((f) => f.name === "copy");
52378
+ if (mono_copy && !mono_copy.checked) {
52379
+ mono_copy.scope = mono_struct;
52380
+ check_function_node(mono_copy, {
52381
+ stack: status.stack,
52382
+ scope_depth: status.scope_depth,
52383
+ types: status.types,
52384
+ values: [],
52385
+ function_value_base: 0,
52386
+ structs: status.structs,
52387
+ traits: status.traits,
52388
+ enums: status.enums,
52389
+ bitsets: status.bitsets,
52390
+ functions: status.functions,
52391
+ allocations: [],
52392
+ var_name_counter: status.var_name_counter,
52393
+ type_params: [],
52394
+ errors: status.errors,
52395
+ function_emission_names: status.function_emission_names,
52396
+ allow_internal: status.allow_internal,
52397
+ library_boundary: status.library_boundary
52398
+ });
52399
+ }
51232
52400
  const root = status.stack[0];
51233
52401
  if (!root.statements.some((s) => s.node_type === "struct" && s.name === mono_name)) root.statements.push(mono_struct);
51234
52402
  return mono_struct;
@@ -51732,6 +52900,8 @@ function check_magic_ctor(node, status, name) {
51732
52900
  add_error(status, `Spawned call '${call.name}' did not resolve`, node.start);
51733
52901
  return false;
51734
52902
  }
52903
+ const spawned = call.resolved_function;
52904
+ if (spawned) spawned.address_escaped = true;
51735
52905
  validate_spawn_args_sendable(call, status);
51736
52906
  stamp_spawn_ctor(node, name);
51737
52907
  const return_type = call.type;
@@ -53318,6 +54488,13 @@ function check_function_call(node, status, func, target_type, self_value, self_p
53318
54488
  const param_type = type_from_value_node(param, status);
53319
54489
  const param_value = value_from_value_node(param);
53320
54490
  const has_ref_keyword = node.ref_param_indices?.includes(i) ?? false;
54491
+ 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)) {
54492
+ const owned_struct = status.structs.find((st) => st.name === func_param.type.name && !st.is_simple_type && !st.is_class);
54493
+ if (owned_struct && !(owned_struct.traits ?? []).length && direct_string_fields(owned_struct).length > 0 && !struct_needs_destroy(owned_struct, status)) {
54494
+ const indices = func_param.is_moved ? node.move_owned_param_indices ??= [] : node.owned_value_param_indices ??= [];
54495
+ if (!indices.includes(i)) indices.push(i);
54496
+ }
54497
+ }
53321
54498
  const has_mov_keyword = node.move_param_indices?.includes(i) ?? false;
53322
54499
  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
54500
  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 +54519,13 @@ function check_function_call(node, status, func, target_type, self_value, self_p
53342
54519
  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
54520
  }
53344
54521
  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);
54522
+ const param_transfers = !!node.move_owned_param_indices?.includes(i);
54523
+ 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
54524
  const arg_is_owned_value = param.node_type === "value" && borrow_depth_of(param, status) === void 0;
53347
54525
  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
54526
  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
54527
  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))) {
54528
+ 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
54529
  if (!status.moved_variables) status.moved_variables = /* @__PURE__ */ new Set();
53352
54530
  status.moved_variables.add(param_value);
53353
54531
  }
@@ -53358,7 +54536,7 @@ function check_function_call(node, status, func, target_type, self_value, self_p
53358
54536
  if (field_type.name && is_class_type$1(field_type.name, status)) {
53359
54537
  const field_name = access.access.name;
53360
54538
  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)) {
54539
+ } 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
54540
  const field_name = access.access.name;
53363
54541
  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
54542
  }
@@ -53795,6 +54973,10 @@ function check_access_node(node, status) {
53795
54973
  if (af.is_destructure) {
53796
54974
  const i = af.destructure_index;
53797
54975
  if (target_type.is_array) {
54976
+ if (af.destructure_move) {
54977
+ add_error(status, `move destructuring requires a tuple or struct (an array element has no field storage to revalidate)`, node.start);
54978
+ return false;
54979
+ }
53798
54980
  if (af.is_destructure_rename) {
53799
54981
  add_error(status, `Cannot rename elements when destructuring an array`, node.start);
53800
54982
  return false;
@@ -54556,6 +55738,55 @@ function check_array_values_node(array, status) {
54556
55738
  * On success, mutates `array` in-place into a FunctionCallNode that constructs
54557
55739
  * the appropriate auto-generated tuple struct.
54558
55740
  */
55741
+ /**
55742
+ * Tuple-element ownership resolution. A bare owning-struct variable element
55743
+ * (`[t, c]` where t is a `List<string>`) byte-copies the struct — the
55744
+ * tuple's field would alias the local's buffer, and both cleanups free it
55745
+ * (double-free). Three outcomes per element:
55746
+ *
55747
+ * - explicit `move t` — transfer (registered like the move-assignment
55748
+ * path; the tuple's field cleanup owns the buffer now);
55749
+ * - plain `t` stamped by the literal last-use pass — INFERRED transfer
55750
+ * (t is provably never read again, so copy-then-drop and move are
55751
+ * observationally identical; the move is free);
55752
+ * - plain `t` still referenced later — rejected, exactly like the
55753
+ * declaration-path owning-struct copy rule: transfer with `move` or
55754
+ * deep-copy with `.copy()`.
55755
+ *
55756
+ * Strings and non-owning structs are ordinary by-value copies (views).
55757
+ */
55758
+ function check_tuple_element_ownership(array, status) {
55759
+ let rejected = false;
55760
+ for (const value of array.values) {
55761
+ const vn = value;
55762
+ if (vn.node_type !== "value") continue;
55763
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(vn.value) || vn.value === "null") continue;
55764
+ const t = type_from_value_node(value, status);
55765
+ if (!t.name) continue;
55766
+ if (is_owning_struct_type(t, status)) {
55767
+ if (vn.is_moved || vn.literal_last_use_move) {
55768
+ vn.is_moved = true;
55769
+ if (!status.moved_variables) status.moved_variables = /* @__PURE__ */ new Set();
55770
+ status.moved_variables.add(vn.value);
55771
+ } else {
55772
+ add_error(status, `cannot copy '${t.name}' by value — it owns heap resources; use 'move ${vn.value}' or '${vn.value}.copy()'`, value.start);
55773
+ rejected = true;
55774
+ }
55775
+ continue;
55776
+ }
55777
+ if (t.name === "string" && !t.is_view && !t.is_array) {
55778
+ if (vn.is_moved || vn.literal_last_use_move) {
55779
+ vn.is_moved = true;
55780
+ if (!status.moved_variables) status.moved_variables = /* @__PURE__ */ new Set();
55781
+ status.moved_variables.add(vn.value);
55782
+ } else {
55783
+ add_error(status, `string local '${vn.value}' is read again after the literal — use 'move ${vn.value}' or '${vn.value}.to_string()'`, value.start);
55784
+ rejected = true;
55785
+ }
55786
+ }
55787
+ }
55788
+ return rejected;
55789
+ }
54559
55790
  function check_as_tuple(array, tuple_element_types, status) {
54560
55791
  if (array.values.length !== tuple_element_types.length) return check_as_array_or_inferred_tuple(array, status, tuple_element_types);
54561
55792
  let result = true;
@@ -54574,6 +55805,7 @@ function check_as_tuple(array, tuple_element_types, status) {
54574
55805
  value_types.push(vt);
54575
55806
  check_type_and_value_match(tuple_element_types[i], vt, value_from_value_node(value), status, value.start, "tuple");
54576
55807
  }
55808
+ if (check_tuple_element_ownership(array, status)) result = false;
54577
55809
  const struct_name = tuple_struct_name(tuple_element_types);
54578
55810
  get_or_create_tuple_struct(tuple_element_types, status);
54579
55811
  const constructor = new FunctionCallNode(array.start, struct_name);
@@ -54656,12 +55888,19 @@ function check_as_array_or_inferred_tuple(array, status, forced_tuple_types) {
54656
55888
  if (!has_outer_expected) {
54657
55889
  const meaningful = value_types.filter((t) => t.name && t.name !== "null");
54658
55890
  const first_meaningful = meaningful[0];
54659
- if (!(meaningful.length > 0 && meaningful.every((t) => t.name === first_meaningful.name && !t.tuple_types?.length && !first_meaningful.tuple_types?.length)) && array.values.length > 0) {
55891
+ const same_generic_shape = (a, b) => {
55892
+ if (a.name !== b.name) return false;
55893
+ const aa = a.type_args ?? [];
55894
+ const bb = b.type_args ?? [];
55895
+ return aa.length === bb.length && aa.every((t, i) => t.name === bb[i]?.name) && !a.tuple_types?.length && !b.tuple_types?.length;
55896
+ };
55897
+ if (!(meaningful.length > 0 && meaningful.every((t) => same_generic_shape(t, first_meaningful))) && array.values.length > 0) {
54660
55898
  const elem_types = value_types.map((t, _i) => {
54661
55899
  if (!t.name || t.name === "null") return meaningful[0] || new Type("int");
54662
55900
  return t;
54663
55901
  });
54664
55902
  for (let i = 0; i < value_types.length; i++) check_type_and_value_match(elem_types[i], value_types[i], value_from_value_node(array.values[i]), status, array.values[i].start, "tuple");
55903
+ if (check_tuple_element_ownership(array, status)) result = false;
54665
55904
  const struct_name = tuple_struct_name(elem_types);
54666
55905
  get_or_create_tuple_struct(elem_types, status);
54667
55906
  const constructor = new FunctionCallNode(array.start, struct_name);
@@ -54731,14 +55970,17 @@ function check_assignment_node(assign, status) {
54731
55970
  return_type: fd?.func_return_type
54732
55971
  };
54733
55972
  }
54734
- } else if (lhs_value?.func_params?.length) lambda_signature = {
55973
+ } else if (lhs_value?.func_params !== void 0) lambda_signature = {
54735
55974
  params: lhs_value.func_params,
54736
55975
  return_type: lhs_value.func_return_type
54737
55976
  };
54738
- if (assign.right_value.node_type === "func" && lambda_signature.params && lambda_signature.params.length) {
55977
+ if (assign.right_value.node_type === "func" && Array.isArray(lambda_signature.params)) {
55978
+ if (!lhs_value && assign.left_value.node_type === "value") {
55979
+ 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);
55980
+ return false;
55981
+ }
54739
55982
  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);
55983
+ if (!rhs_func.name || assign.left_value.node_type === "value") synthesize_lambda_name(rhs_func, status);
54742
55984
  if (rhs_func.params.length === lambda_signature.params.length) {
54743
55985
  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
55986
  }
@@ -55155,7 +56397,7 @@ function list_element_type(t, status) {
55155
56397
  return struct.source_type_args?.[0];
55156
56398
  }
55157
56399
  function check_for_loop_node(for_loop, status) {
55158
- if (for_loop.list && !(for_loop.list instanceof RangeNode) && for_loop.list.node_type === "value") {
56400
+ if (for_loop.list && !(for_loop.list instanceof RangeNode) && (for_loop.list.node_type === "value" || for_loop.list.node_type === "access")) {
55159
56401
  const list_type = type_from_value_node(for_loop.list, status);
55160
56402
  const enumerable = list_type.name ? has_trait(list_type.name, "Enumerable", status) : false;
55161
56403
  const has_at = list_type.name !== void 0 && !!status.structs.find((s) => s.name === "Array_" + list_type.name && s.functions.some((f) => f.name === "at"));
@@ -55181,6 +56423,16 @@ function check_for_loop_node(for_loop, status) {
55181
56423
  check_node(for_loop.list, for_status);
55182
56424
  const list_type = type_from_value_node(for_loop.list, for_status);
55183
56425
  const is_enumerable = list_type.name ? has_trait(list_type.name, "Enumerable", for_status) : false;
56426
+ const chained_list_elem = list_element_type(list_type, for_status);
56427
+ if (chained_list_elem && !is_enumerable && chain_is_call_free(for_loop.list)) {
56428
+ if (for_loop.item_is_ref) {
56429
+ add_error(for_status, `'ref' iteration is not supported for List<T> — index explicitly (for i of 0..xs.length) and use .set to write back`, for_loop.item.start);
56430
+ return;
56431
+ }
56432
+ desugar_array_for_loop(for_loop, list_type, chained_list_elem);
56433
+ check_for_loop_node(for_loop, for_status);
56434
+ return;
56435
+ }
55184
56436
  if (!list_type.is_array && !is_enumerable && list_type.name) add_error(for_status, `For loop list must be an array, List, or Enumerable, not ${list_type.name}`, for_loop.list.start);
55185
56437
  if (for_loop.item_is_ref && list_type.is_array && for_loop.list.node_type === "value") {
55186
56438
  const list_name = for_loop.list.value;
@@ -55233,6 +56485,23 @@ function check_for_loop_node(for_loop, status) {
55233
56485
  * break/continue so mutations persist on all exit paths. (The write-back is
55234
56486
  * skipped on `return`, matching Rust's copy semantics.)
55235
56487
  */
56488
+ /**
56489
+ * Whether an iterable expression contains no call segments — the guard for
56490
+ * the post-check List desugar (`for x of holder.children`). A chain through
56491
+ * a CALL result (`arena.get(handle).children`) cannot discharge the
56492
+ * desugared `.at` bounds and miscompiles on aarch64 (FOLLOWUP.md), so it
56493
+ * keeps the clear must-be-array-or-Enumerable error.
56494
+ */
56495
+ function chain_is_call_free(node) {
56496
+ if (!node || typeof node !== "object") return true;
56497
+ if (node.node_type === "func_call") return false;
56498
+ if (node.node_type === "access") {
56499
+ const access = node;
56500
+ if (access.access.node_type === "access_func") return false;
56501
+ return chain_is_call_free(access.target);
56502
+ }
56503
+ return true;
56504
+ }
55236
56505
  function desugar_array_for_loop(for_loop, array_type, elem_type) {
55237
56506
  const list = for_loop.list;
55238
56507
  const start = list.start;
@@ -55970,6 +57239,8 @@ function check_spawn_node(node, status) {
55970
57239
  add_error(status, "Spawned call did not resolve", node.start);
55971
57240
  return false;
55972
57241
  }
57242
+ const spawned = node.call.resolved_function;
57243
+ if (spawned) spawned.address_escaped = true;
55973
57244
  for (const param of node.call.params) {
55974
57245
  let arg_type = type_from_value_node(param, status);
55975
57246
  const stamped = param.type;
@@ -56476,6 +57747,7 @@ function check(root, options) {
56476
57747
  allow_internal: options?.allow_internal,
56477
57748
  library_boundary: options?.library_boundary
56478
57749
  };
57750
+ stamp_literal_element_moves(root);
56479
57751
  check_node(root, status);
56480
57752
  unwrap_unsafe_blocks(root);
56481
57753
  if (!status.errors.length) emit_warnings(root, status);
@@ -56787,13 +58059,14 @@ function parse_type(status) {
56787
58059
  type.type_args = void 0;
56788
58060
  }
56789
58061
  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);
58062
+ if (type.is_array || type.is_pointer || type.is_view || type.is_ref) {
58063
+ add_error(status, `Func<...> cannot be combined with '[]'/'ptr'/'view'/'ref'`, start);
56792
58064
  return type;
56793
58065
  }
56794
58066
  const args = type.type_args;
56795
58067
  const func_type = new Type("func");
56796
58068
  func_type.start = type.start;
58069
+ if (type.is_nullable) func_type.is_nullable = true;
56797
58070
  func_type.func_params = [];
56798
58071
  for (let i = 0; i < args.length - 1; i++) {
56799
58072
  if (args[i].name === "void") {
@@ -56906,10 +58179,14 @@ function default_visibility(status) {
56906
58179
  * - a func RETURN carries its signature on the Type
56907
58180
  * (`type.func_params` / `type.func_return_type`).
56908
58181
  */
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). */
58182
+ /** Whether the current token starts a nested func type: `func` (optionally
58183
+ * nullable, `func?`) immediately followed by `(` (the bare word `func` alone
58184
+ * is the plain marker type). */
56911
58185
  function at_func_type(status) {
56912
- return peek_current(status) === "func" && status.tokens[status.i + 1]?.value === "(";
58186
+ if (peek_current(status) !== "func") return false;
58187
+ const next = status.tokens[status.i + 1]?.value;
58188
+ if (next === "(") return true;
58189
+ return next === "?" && status.tokens[status.i + 2]?.value === "(";
56913
58190
  }
56914
58191
  /**
56915
58192
  * Reject `void` where it has no meaning. Only the last type argument of
@@ -56930,6 +58207,7 @@ function reject_void_type(type, status, at) {
56930
58207
  function adopt_func_param_type(param, type) {
56931
58208
  if (type.name !== "func") return false;
56932
58209
  param.type = new Type("func");
58210
+ param.type.is_nullable = type.is_nullable;
56933
58211
  param.func_params = type.func_params;
56934
58212
  param.func_return_type = type.func_return_type;
56935
58213
  return true;
@@ -56952,11 +58230,13 @@ function parse_func_type_signature(param, status) {
56952
58230
  }
56953
58231
  /**
56954
58232
  * 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.
58233
+ * (at_func_type confirmed the `(` follows, possibly after a `?`) — and return
58234
+ * it as a Type whose func_params/func_return_type carry the signature. A
58235
+ * `func?` spelling marks the type nullable (it may hold `null`).
56957
58236
  */
56958
58237
  function parse_func_type(status) {
56959
58238
  const type = new Type("func");
58239
+ if (accept("?", status)) type.is_nullable = true;
56960
58240
  const param = new ParameterNode(get_index(status), "");
56961
58241
  parse_func_type_signature(param, status);
56962
58242
  type.func_params = param.func_params;
@@ -56996,6 +58276,7 @@ function parse_signature_params(params, status) {
56996
58276
  if (at_func_type(status)) {
56997
58277
  consume(status);
56998
58278
  param.type = new Type("func");
58279
+ if (accept("?", status)) param.type.is_nullable = true;
56999
58280
  parse_func_type_signature(param, status);
57000
58281
  } else {
57001
58282
  param.type = parse_type(status);
@@ -57036,7 +58317,11 @@ function parse_declaration(visibility, declaration, status, is_readonly = false)
57036
58317
  if (is_func_alias) {
57037
58318
  decl.func_params = decl.type.func_params ?? [];
57038
58319
  decl.func_return_type = decl.type.func_return_type;
57039
- decl.type = new Type("");
58320
+ if (decl.type.is_nullable) {
58321
+ const nullable_marker = new Type("func");
58322
+ nullable_marker.is_nullable = true;
58323
+ decl.type = nullable_marker;
58324
+ } else decl.type = new Type("");
57040
58325
  }
57041
58326
  if (peek_current(status) === "=" || status.i >= status.tokens.length) {
57042
58327
  const modifier = status.tokens[saved_i]?.value;
@@ -57098,6 +58383,7 @@ function parse_declaration(visibility, declaration, status, is_readonly = false)
57098
58383
  }
57099
58384
  function parse_function_type_declaration(decl, status) {
57100
58385
  accept("func", status);
58386
+ const func_nullable = accept("?", status);
57101
58387
  if (expect("(", status)) {
57102
58388
  const params = [];
57103
58389
  let return_type;
@@ -57110,6 +58396,10 @@ function parse_function_type_declaration(decl, status) {
57110
58396
  decl.name = consume_name(status);
57111
58397
  decl.func_params = params.filter((p) => !p.type.is_return_type);
57112
58398
  decl.func_return_type = return_type;
58399
+ if (func_nullable) {
58400
+ decl.type = new Type("func");
58401
+ decl.type.is_nullable = true;
58402
+ }
57113
58403
  const has_equals = accept("=", status);
57114
58404
  if (peek_current(status) === "{") {
57115
58405
  const func = new FunctionNode(get_index(status), default_visibility(status), decl.name, new Type(""));
@@ -57209,6 +58499,7 @@ function parse_anon_function_parameter(func, status) {
57209
58499
  if (at_func_type(status)) {
57210
58500
  consume(status);
57211
58501
  param.type = new Type("func");
58502
+ if (accept("?", status)) param.type.is_nullable = true;
57212
58503
  parse_func_type_signature(param, status);
57213
58504
  param.name_start = get_index(status);
57214
58505
  param.name = consume_name(status);
@@ -57264,6 +58555,10 @@ function looks_like_destructuring(status, start_idx) {
57264
58555
  * - bare name: `a` — positional (tuples/arrays) or same-named field (structs)
57265
58556
  * - rename: `field = name` — struct/class field accessed as `field`,
57266
58557
  * bound to `name`
58558
+ *
58559
+ * Either form may be prefixed with `move` (`[move a]`, `[field = move n]`):
58560
+ * the binding TAKES OWNERSHIP of the field (see destructure_move) instead of
58561
+ * binding a non-owning view.
57267
58562
  */
57268
58563
  function parse_destructuring(visibility, declaration, start, status) {
57269
58564
  expect("[", status);
@@ -57274,8 +58569,17 @@ function parse_destructuring(visibility, declaration, start, status) {
57274
58569
  let field = first;
57275
58570
  let name = first;
57276
58571
  let rename = false;
57277
- if (peek_current(status) === "=") {
58572
+ let move = false;
58573
+ if (first === "move" && peek_current(status) !== "=" && peek_current(status) !== "]") {
58574
+ move = true;
58575
+ name = consume(status);
58576
+ field = name;
58577
+ } else if (peek_current(status) === "=") {
57278
58578
  accept("=", status);
58579
+ if (peek_current(status) === "move") {
58580
+ consume(status);
58581
+ move = true;
58582
+ }
57279
58583
  name = consume_name(status);
57280
58584
  field = first;
57281
58585
  rename = true;
@@ -57283,7 +58587,8 @@ function parse_destructuring(visibility, declaration, start, status) {
57283
58587
  bindings.push({
57284
58588
  name,
57285
58589
  field,
57286
- rename
58590
+ rename,
58591
+ move
57287
58592
  });
57288
58593
  index++;
57289
58594
  if (!accept(",", status)) break;
@@ -57313,7 +58618,7 @@ function parse_destructuring(visibility, declaration, start, status) {
57313
58618
  const temp_counter = status.__tuple_destructure_counter || 0;
57314
58619
  status.__tuple_destructure_counter = temp_counter + 1;
57315
58620
  const temp_name = `_tuple_dst_${temp_counter}`;
57316
- push_to_parent(new DeclarationNode(start, "private", "const", temp_name, new Type(""), value));
58621
+ push_to_parent(new DeclarationNode(start, "private", bindings.some((b) => b.move) ? "var" : "const", temp_name, new Type(""), value));
57317
58622
  base_node = new ValueNode(start, temp_name);
57318
58623
  }
57319
58624
  for (let i = 0; i < bindings.length; i++) {
@@ -57323,7 +58628,9 @@ function parse_destructuring(visibility, declaration, start, status) {
57323
58628
  access_field.is_destructure = true;
57324
58629
  access_field.destructure_index = i;
57325
58630
  access_field.is_destructure_rename = binding.rename;
58631
+ access_field.destructure_move = binding.move;
57326
58632
  const access = new AccessNode(start, base_node, access_field);
58633
+ access.is_moved = binding.move;
57327
58634
  push_to_parent(new DeclarationNode(start, visibility, declaration, binding.name, new Type(""), access));
57328
58635
  }
57329
58636
  }
@@ -58142,6 +59449,7 @@ function parse_function_parameter(parent, func, status) {
58142
59449
  if (at_func_type(status)) {
58143
59450
  consume(status);
58144
59451
  param.type = new Type("func");
59452
+ if (accept("?", status)) param.type.is_nullable = true;
58145
59453
  parse_func_type_signature(param, status);
58146
59454
  param.name_start = get_index(status);
58147
59455
  param.name = consume_name(status);
@@ -59500,7 +60808,8 @@ nomen test
59500
60808
  function find_bundled(filename) {
59501
60809
  const here = path.dirname(fileURLToPath(import.meta.url));
59502
60810
  const cli_root = path.dirname(here);
59503
- const candidates = [path.join(cli_root, filename), path.join(cli_root, "..", filename)];
60811
+ const repo_root = path.join(cli_root, "..");
60812
+ 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
60813
  for (const c of candidates) if (fs.existsSync(c)) return path.resolve(c);
59505
60814
  }
59506
60815
  /**
@@ -59722,6 +61031,10 @@ function parse_records(stdout) {
59722
61031
  other
59723
61032
  };
59724
61033
  }
61034
+ /** Pull the exit-time audit "LEAK: N allocation(s)" lines out of stdout. */
61035
+ function extract_leaks(stdout) {
61036
+ return stdout.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("LEAK:"));
61037
+ }
59725
61038
  /**
59726
61039
  * Compile one `*.test.nm` file (with the generated harness) and run it,
59727
61040
  * returning its parsed records and any crash info.
@@ -59737,6 +61050,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
59737
61050
  fails: [],
59738
61051
  benches: [],
59739
61052
  other: [],
61053
+ leaks: [],
59740
61054
  ms: 0
59741
61055
  };
59742
61056
  const resolved = path.resolve(entry_path);
@@ -59834,9 +61148,11 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
59834
61148
  result.fails = records.fails;
59835
61149
  result.benches = records.benches;
59836
61150
  result.other = records.other;
61151
+ result.leaks = audit ? extract_leaks(runStdout) : [];
61152
+ if (result.leaks.length) result.other = result.other.filter((l) => !l.trim().startsWith("LEAK:"));
59837
61153
  result.crashed = crashed;
59838
61154
  if (crashed) result.phase = "run";
59839
- result.ok = !(result.fails.length > 0 || result.tests.some((t) => t.failed > 0)) && crashed === void 0;
61155
+ result.ok = !(result.fails.length > 0 || result.tests.some((t) => t.failed > 0)) && crashed === void 0 && result.leaks.length === 0;
59840
61156
  result.ms = performance.now() - start;
59841
61157
  return result;
59842
61158
  }
@@ -59873,6 +61189,7 @@ function report_file(result) {
59873
61189
  console.log(` ${mark} ${rel} ${C.dim(`(${totalTests} tests)`)} ${C.dim(format_duration(result.ms))}`);
59874
61190
  if (result.crashed) console.log(C.red(` ${result.crashed}`));
59875
61191
  for (const f of result.fails) console.log(` ${C.red("✗")} ${f.test} ${C.dim(">")} ${f.message}`);
61192
+ for (const leak of result.leaks) console.log(` ${C.yellow("⚠")} ${C.yellow(leak)}`);
59876
61193
  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
61194
  if (result.other.length) {
59878
61195
  console.log(C.dim(" --- test stdout ---"));
@@ -59914,10 +61231,12 @@ function runTests(root, options = {}) {
59914
61231
  const totalTests = results.reduce((a, r) => a + r.tests.length, 0);
59915
61232
  const totalFailed = results.reduce((a, r) => a + r.tests.reduce((x, t) => x + t.failed, 0), 0);
59916
61233
  const totalPassed = totalTests - totalFailed;
61234
+ const totalLeaks = results.reduce((a, r) => a + r.leaks.length, 0);
59917
61235
  const anyFailed = results.some((r) => !r.ok);
59918
61236
  console.log("");
59919
61237
  console.log(` ${C.dim("Files ")} ${C.green(`${totalFilesPassed} passed`)}` + (totalFilesFailed ? ` ${C.dim("|")} ${C.red(`${totalFilesFailed} failed`)}` : "") + C.dim(` (${totalFiles})`));
59920
61238
  console.log(` ${C.dim("Tests ")} ${C.green(`${totalPassed} passed`)}` + (totalFailed ? ` ${C.dim("|")} ${C.red(`${totalFailed} failed`)}` : "") + C.dim(` (${totalTests})`));
61239
+ if (totalLeaks) console.log(` ${C.dim("Leaks ")} ${C.yellow(`${totalLeaks} reported`)}` + C.dim(" (--audit; leaked files count as failures)"));
59921
61240
  console.log(` ${C.dim(" Time ")} ${format_duration(elapsed)}`);
59922
61241
  console.log("");
59923
61242
  return !anyFailed;
@@ -60137,7 +61456,7 @@ function processFolder(folder, config, mode, program_args) {
60137
61456
  const dir = fs.opendirSync(folder);
60138
61457
  let dirent;
60139
61458
  while ((dirent = dir.readSync()) !== null) if (shouldProcessFile(dirent.name)) {
60140
- processFile(path.join(folder, dirent.name), config, mode, program_args, false);
61459
+ processFile(path.join(folder, dirent.name), config, mode, program_args);
60141
61460
  fs.watch;
60142
61461
  }
60143
61462
  dir.closeSync();
@@ -60145,45 +61464,15 @@ function processFolder(folder, config, mode, program_args) {
60145
61464
  function shouldProcessFile(filename) {
60146
61465
  return path.extname(filename) === SUPPORTED_EXTENSION;
60147
61466
  }
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) {
61467
+ function processFile(filename, config, mode, program_args) {
60177
61468
  console.log("Processing", filename);
60178
61469
  const arch = config.arch || "aarch64";
60179
61470
  const platform = config.platform || default_platform();
60180
61471
  const resolved = path.resolve(filename);
60181
61472
  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
61473
  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);
61474
+ const input = join$1(resolved, config.lib);
61475
+ const parsed = parse(input, config.lib ? get_library(config.lib) : void 0, resolved);
60187
61476
  let errors = parsed.errors;
60188
61477
  if (errors.length) {
60189
61478
  console.log(render_errors(input, errors));