nomen-lang 0.6.3 → 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.
Files changed (2) hide show
  1. package/dist/index.mjs +955 -267
  2. package/package.json +1 -1
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;
@@ -6748,6 +6754,14 @@ var ValueNode = class extends BaseNode {
6748
6754
  * name. Mirrors FunctionCallNode.resolved_function.
6749
6755
  */
6750
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;
6751
6765
  constructor(start, value, type) {
6752
6766
  super("value", start);
6753
6767
  this.value = value;
@@ -7353,6 +7367,13 @@ var DeclarationNode = class extends BaseNode {
7353
7367
  * struct layout. Set at check time by check_function_call; consumed by the
7354
7368
  * build backends' declaration emitters. */
7355
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;
7356
7377
  /** True for a hoisted call-argument temp (`_param_N`) that is a heap COPY
7357
7378
  * of a stack-array local, bound to a heap `Array<T>` param (non-`ref`).
7358
7379
  * `value` is the source ValueNode; the temp is materialised as a heap
@@ -8014,6 +8035,38 @@ function hoist_field_overrides(value, build, status, declaration_terminator = ""
8014
8035
  carrier._fov_hoisted = true;
8015
8036
  }
8016
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
8017
8070
  //#region ../src/build_common/string_return_analysis.ts
8018
8071
  const EMPTY_SET = /* @__PURE__ */ new Set();
8019
8072
  /**
@@ -8357,6 +8410,69 @@ function is_owned_string_branch_value(node, table) {
8357
8410
  return false;
8358
8411
  }
8359
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
8360
8476
  //#region ../src/nodes/child_nodes.ts
8361
8477
  /** Whether `value` is an AST node (carries a string `node_type`). */
8362
8478
  function is_ast_node(value) {
@@ -8473,7 +8589,21 @@ var Walk = class {
8473
8589
  reads = [];
8474
8590
  declares = [];
8475
8591
  assignments = [];
8592
+ literals = [];
8476
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
+ }
8477
8607
  interval(body) {
8478
8608
  const enter = ++this.stamp;
8479
8609
  body();
@@ -8610,6 +8740,80 @@ function classify_declare(decl, walk, param_names, func_label) {
8610
8740
  desc: `var ${node.name} = ${source}`
8611
8741
  };
8612
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
+ }
8613
8817
  function walk_stmt(walk, node) {
8614
8818
  switch (node.node_type) {
8615
8819
  case "declare": {
@@ -8746,7 +8950,10 @@ function walk_expr$1(walk, node) {
8746
8950
  }
8747
8951
  case "array": {
8748
8952
  const arr = node;
8749
- 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
+ });
8750
8957
  return;
8751
8958
  }
8752
8959
  default: for (const child of child_nodes(node)) walk_expr$1(walk, child);
@@ -11020,38 +11227,6 @@ function struct_needs_auto_destroy(struct, table) {
11020
11227
  return false;
11021
11228
  }
11022
11229
  //#endregion
11023
- //#region ../src/build_common/has_string_fields.ts
11024
- /**
11025
- * Whether any field (recursively, through nested owning value structs)
11026
- * is an owned `string`. Shared by BOTH backends' owning-Buffer element
11027
- * specialization (the slot deep-copies strdup'd strings, so a
11028
- * string-field element is owning even when `struct_needs_destroy`
11029
- * ignores string-only locals) — the C comment in
11030
- * owning_buffer_specialize records the divergence this once caused.
11031
- * `ref` and `view T` fields are skipped: a ref aliases the caller's
11032
- * storage and a view's byte copy owns nothing.
11033
- */
11034
- function has_string_fields(node, status) {
11035
- for (const field of node.fields) {
11036
- if (field.type.is_ref) continue;
11037
- if (field.type.is_view) continue;
11038
- if (field.type.name === "string" && !field.type.is_array) return true;
11039
- const field_struct = status.structs.find((s) => s.name === field.type.name && !s.is_simple_type && !s.is_generic);
11040
- if (field_struct && !field_struct.is_class && has_string_fields(field_struct, status)) return true;
11041
- }
11042
- return false;
11043
- }
11044
- /**
11045
- * The DIRECT `string` fields of a value struct (nested struct fields are not
11046
- * descended into — nested string fields are outside the per-field ownership
11047
- * tracking everywhere else too). Used by the return-boundary normalization:
11048
- * the returned struct's unrecorded fields are strdup'd so the value the
11049
- * caller receives is uniformly heap-owned.
11050
- */
11051
- function direct_string_fields(node) {
11052
- return node.fields.filter((f) => !f.type.is_ref && !f.type.is_view && !f.type.is_array && f.type.name === "string");
11053
- }
11054
- //#endregion
11055
11230
  //#region ../src/build_common/scan_force_heap_strings.ts
11056
11231
  function scan_force_heap_strings(statements, structs) {
11057
11232
  const result = /* @__PURE__ */ new Set();
@@ -13738,12 +13913,27 @@ function clear_heap_string_fields(status, var_name) {
13738
13913
  * fields are unconditionally heap and freed by the destroy path. Called from
13739
13914
  * emit_destroy_for_decl and directly from cleanup loops that skip moved
13740
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.
13741
13922
  */
13742
- 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) {
13743
13924
  if (!status.heap_string_fields?.size) return;
13744
13925
  const prefix = `${decl_name}.`;
13745
13926
  const fields = Array.from(status.heap_string_fields).filter((k) => k.startsWith(prefix)).map((k) => k.slice(prefix.length));
13746
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
+ }
13747
13937
  for (const field of fields) {
13748
13938
  const offset = get_field_offset(decl_type_name, field, status);
13749
13939
  emit_var_address(status, "x0", decl_name);
@@ -13751,6 +13941,7 @@ function release_heap_string_fields(status, decl_name, decl_type_name) {
13751
13941
  emit_free(status);
13752
13942
  status.heap_string_fields.delete(`${decl_name}.${field}`);
13753
13943
  }
13944
+ if (skip_label) emit_asm(status, `${skip_label}:\n`);
13754
13945
  }
13755
13946
  /**
13756
13947
  * Swap in a fresh scoped_declarations frame for a nested scope (if/while/
@@ -14109,7 +14300,7 @@ function is_struct_type$4(type_name, status) {
14109
14300
  }
14110
14301
  function emit_destroy_for_decl(status, decl_name, decl_type_name, addr_offset, type_args, is_nullable) {
14111
14302
  const moved = status.moved ?? /* @__PURE__ */ new Set();
14112
- release_heap_string_fields(status, decl_name, decl_type_name);
14303
+ release_heap_string_fields(status, decl_name, decl_type_name, is_nullable);
14113
14304
  if (moved.has(decl_name)) return;
14114
14305
  if (decl_type_name === "func" && status.stack_offsets?.has(decl_name)) {
14115
14306
  const skip = `.Lclosure_free_done_${status.label_counter = (status.label_counter ?? 0) + 1}`;
@@ -14162,11 +14353,19 @@ function emit_destroy_for_decl(status, decl_name, decl_type_name, addr_offset, t
14162
14353
  return;
14163
14354
  }
14164
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;
14165
14357
  let skip_label;
14166
14358
  if (guard_null) {
14167
14359
  emit_var_load(status, "x0", decl_name, 8);
14168
14360
  skip_label = `.Lskip_nd_${status.label_counter = (status.label_counter ?? 0) + 1}`;
14169
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
+ }
14170
14369
  }
14171
14370
  if (has_destroy(struct_type) || struct_type.is_class) {
14172
14371
  if (struct_type.is_class) {
@@ -14321,13 +14520,51 @@ function resolve_decl_struct(decl, status) {
14321
14520
  }
14322
14521
  }
14323
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
+ }
14324
14561
  function emit_destroy_for_scope(status, declarations_before) {
14325
14562
  const moved = status.moved ?? /* @__PURE__ */ new Set();
14326
14563
  const current_scope = status.heap_cleanup_stack?.[status.heap_cleanup_stack.length - 1];
14327
14564
  if (current_scope?.heap_slots.length) {
14328
14565
  for (let i = declarations_before; i < status.scoped_declarations.length; i++) {
14329
14566
  const decl = status.scoped_declarations[i];
14330
- 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);
14331
14568
  if (moved.has(decl.name)) continue;
14332
14569
  const scope_enum = status.enums.find((e) => e.name === decl.type.name && e.has_associated_data);
14333
14570
  if (scope_enum && !decl.type.is_array) {
@@ -14370,6 +14607,18 @@ function emit_destroy_for_scope(status, declarations_before) {
14370
14607
  emit_free(status);
14371
14608
  continue;
14372
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
+ }
14373
14622
  if (decl.type.name === "func") {
14374
14623
  emit_destroy_for_decl(status, decl.name, "func");
14375
14624
  continue;
@@ -14389,7 +14638,7 @@ function emit_destroy_for_scope(status, declarations_before) {
14389
14638
  }
14390
14639
  for (let i = declarations_before; i < status.scoped_declarations.length; i++) {
14391
14640
  const decl = status.scoped_declarations[i];
14392
- 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);
14393
14642
  if (moved.has(decl.name)) continue;
14394
14643
  const scope_enum = status.enums.find((e) => e.name === decl.type.name && e.has_associated_data);
14395
14644
  if (scope_enum && !decl.type.is_array) {
@@ -14463,6 +14712,18 @@ function emit_destroy_for_scope(status, declarations_before) {
14463
14712
  emit_free(status);
14464
14713
  continue;
14465
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
+ }
14466
14727
  if (is_field_struct_borrow(decl)) continue;
14467
14728
  const resolved_decl = resolve_decl_struct(decl, status);
14468
14729
  if (!resolved_decl) continue;
@@ -14739,13 +15000,31 @@ function materialize_func_value_a64(func, status) {
14739
15000
  const lines = [];
14740
15001
  lines.push(`.p2align 2`);
14741
15002
  lines.push(`${thunk}:`);
14742
- const reg_shift = Math.min(slots, 8);
14743
- for (let s = 0; s < reg_shift - 1; s++) lines.push(`mov x${s}, x${s + 1}`);
14744
- for (let k = 0; slots + k > 8 && k < 64; k++) {
14745
- 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}]`);
14746
15017
  lines.push(`str x9, [sp, #${k * 8}]`);
14747
15018
  }
14748
- 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}`);
14749
15028
  lines.push(``);
14750
15029
  status.closure_definitions = (status.closure_definitions ?? "") + lines.join("\n") + "\n";
14751
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;
@@ -19267,6 +19546,8 @@ function build_function_node$1(node, status) {
19267
19546
  if (check_c_fallback(node, void 0, status)) return;
19268
19547
  const old_function_name = status.current_function_name;
19269
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;
19270
19551
  status.function_nursery_depth = status.nursery_stack?.length ?? 0;
19271
19552
  const old_scoped_declarations = status.scoped_declarations;
19272
19553
  status.scoped_declarations = [];
@@ -19855,6 +20136,7 @@ function build_function_node$1(node, status) {
19855
20136
  status.heap_cleanup_stack = old_heap_cleanup_stack;
19856
20137
  status.trait_class_frames = old_trait_class_frames;
19857
20138
  status.current_function_name = old_function_name;
20139
+ status.current_function_is_closure = old_is_closure;
19858
20140
  status.stack_size = old_stack_size;
19859
20141
  status.stack_offsets = old_stack_offsets;
19860
20142
  status.closure_env_slot = old_closure_env_slot;
@@ -21381,7 +21663,13 @@ function build_block_node$1(node, status) {
21381
21663
  if (is_struct_node(child) || is_trait_node(child) || is_function_node(child) || child.node_type === "enum" || child.node_type === "bitset") continue;
21382
21664
  if (child.node_type === "declare") {
21383
21665
  const decl = child;
21384
- 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
+ }
21385
21673
  if (inlined_const_names.has(decl.name)) continue;
21386
21674
  if (is_literal_init(decl.value)) continue;
21387
21675
  module_init_statements.push(child);
@@ -21490,69 +21778,6 @@ function call_in_set(set, call) {
21490
21778
  return set.has(call.name.replace(/#/g, ""));
21491
21779
  }
21492
21780
  //#endregion
21493
- //#region ../src/build_common/call_init_string_fields.ts
21494
- /**
21495
- * Whether a declaration-initializer expression is a value the callee's
21496
- * return-boundary normalization made UNIFORMLY heap-owned, so the fresh
21497
- * binding records every string field and frees them at scope exit:
21498
- *
21499
- * - a plain function call registered as a normalized struct returner
21500
- * (the registry is populated at the callee's return sites — a callee
21501
- * built later, or one that returns slot borrows, is not registered and
21502
- * the binding keeps the pre-existing borrow behavior);
21503
- * - an owned accessor (`pop`/`move_T`/`copy`) — move-out semantics
21504
- * transfer slot-owned heap buffers to the binding.
21505
- *
21506
- * NOT normalized (never recorded): struct constructions (`Info(...)` — its
21507
- * `#init` leaves rodata borrows and bypasses the return path) and container
21508
- * BORROW accessors (`.at`/`.first`/`.slice`/`load` — the slot keeps
21509
- * ownership; recording would free the container's storage).
21510
- */
21511
- function is_normalized_struct_call(value, status) {
21512
- if (!value) return false;
21513
- if (value.node_type === "func_call") {
21514
- const call = value;
21515
- return !!status.normalized_struct_returners?.has(call.name);
21516
- }
21517
- if (value.node_type === "access") {
21518
- const access = value.access;
21519
- if (access?.node_type !== "access_func") return false;
21520
- const name = access.name ?? "";
21521
- if (is_container_borrow_accessor_name(name)) return false;
21522
- if (status.normalized_struct_returners?.has(name)) return true;
21523
- const target = value.target;
21524
- const receiver_type = target ? target.type ?? void 0 : void 0;
21525
- if (receiver_type?.name) {
21526
- const label = `${mono_type_name(receiver_type)}_${name}`;
21527
- if (status.normalized_struct_returners?.has(label)) return true;
21528
- }
21529
- return false;
21530
- }
21531
- return false;
21532
- }
21533
- /**
21534
- * Record every direct string field of a call-initialized value-struct
21535
- * binding (`var Info i = make()`) as heap-owned: the callee's return
21536
- * normalization guarantees each field is a heap buffer, so scope-exit
21537
- * auto_free must free them, and a later `i.field = …` write sees
21538
- * old_was_heap and reclaims the displaced buffer instead of leaking it.
21539
- * No-op for non-struct, class, view, and array types, for structs without
21540
- * direct string fields, and for non-call initializers.
21541
- */
21542
- function record_call_init_string_fields(node, status) {
21543
- if (!is_normalized_struct_call(node.value, status)) return;
21544
- const type = node.type;
21545
- if (!type?.name) return;
21546
- const mono_name = mono_type_name(type);
21547
- if (mono_name.startsWith("_Tuple_")) return;
21548
- const struct = status.structs.find((s) => s.name === mono_name && !s.is_simple_type);
21549
- if (!struct || struct.is_class) return;
21550
- const fields = direct_string_fields(struct);
21551
- if (!fields.length) return;
21552
- if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
21553
- for (const field of fields) status.heap_string_fields.add(`${node.name}.${field.name}`);
21554
- }
21555
- //#endregion
21556
21781
  //#region ../src/build_common/fold_string_const.ts
21557
21782
  /**
21558
21783
  * Fold a top-level `const`'s string `+` chain into a single string literal.
@@ -21614,6 +21839,24 @@ function literal(raw, start) {
21614
21839
  return new ValueNode(start, raw, new Type("string", true));
21615
21840
  }
21616
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
21617
21860
  //#region ../src/build_common/string_escapes.ts
21618
21861
  /**
21619
21862
  * Source string-literal escape handling shared by both backends.
@@ -21743,6 +21986,37 @@ function escape_asciz(value) {
21743
21986
  return quote + reencoded.slice(1, reencoded.endsWith(quote) ? -1 : void 0).replace(/\n/g, "\\n") + (reencoded.endsWith(quote) ? quote : "");
21744
21987
  }
21745
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
21746
22020
  //#region ../src/build_aarch64/build_range_node.ts
21747
22021
  function build_range_node$1(node, status) {
21748
22022
  const start = evaluate_constant$1(node.left_value);
@@ -22138,6 +22412,7 @@ function emit_struct_constructor_to_slot(fc, slot_addr, status) {
22138
22412
  * so register slots start at x1 — overflow begins at the 8th arg (slot 8).
22139
22413
  */
22140
22414
  function build_constructor_params(fc, param_regs, status) {
22415
+ mark_tuple_literal_move_owners(fc, status);
22141
22416
  const variadic_idx = fc.variadic_param_index;
22142
22417
  if (variadic_idx !== void 0 && fc.params[variadic_idx] && fc.params[variadic_idx].node_type === "array") {
22143
22418
  const arr = fc.params[variadic_idx];
@@ -22537,6 +22812,43 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
22537
22812
  if (!status.function_local_types) status.function_local_types = /* @__PURE__ */ new Map();
22538
22813
  status.function_local_types.set(node.name, node.type.name);
22539
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
+ }
22540
22852
  hoist_field_overrides(node.value, build_node, status, "", node.name);
22541
22853
  status.last_result_is_heap = false;
22542
22854
  const prev_heap = status.last_result_is_heap;
@@ -23002,13 +23314,33 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23002
23314
  } else if (status.function_return_label) {
23003
23315
  const labels = emit_string_array_labels(array_values.values, status);
23004
23316
  if (needs_runtime_array_init(array_values.values, status)) {
23005
- const offset = alloc_array_with_prefix(status, array_values.values.length, element_size);
23006
- status.stack_offsets.set(node.name, offset);
23007
- array_values.values.forEach((value, i) => {
23008
- const slot_offset = offset + i * element_size;
23009
- const resolved = resolve_static_value(value, status);
23010
- if (resolved !== null) emit_string_array_element(status, resolved, labels, "x29", slot_offset);
23011
- });
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
+ }
23012
23344
  } else {
23013
23345
  emit_data(status, `${node.name}: ${directive} `);
23014
23346
  let first = true;
@@ -23024,19 +23356,16 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23024
23356
  emit_data(status, `\n.p2align 2\n`);
23025
23357
  }
23026
23358
  } else if (node.type.name === "string" && node.type.is_array) {
23027
- const labels = emit_string_array_labels(array_values.values, status);
23028
- emit_asm(status, `${node.name}: ${directive} `);
23029
- array_values.values.forEach((value, i) => {
23030
- 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) => {
23031
23365
  const resolved = resolve_static_value(value, status);
23032
- if (resolved !== null && resolved.startsWith("\"")) emit_asm(status, `.quad ${resolve_array_element(resolved, labels)}\n\t.quad ${string_literal_length(resolved)}`);
23033
- else emit_asm(status, ".quad 0\n .quad 0");
23366
+ emit_data(status, `\t${directive} ${resolved !== null ? resolved : "0"}\n`);
23034
23367
  });
23035
- emit_asm(status, `\n.p2align 2\n`);
23036
- } else {
23037
- emit_asm(status, `${node.name}: ${directive} `);
23038
- build_array_values_node$1(array_values, status);
23039
- emit_asm(status, `\n.p2align 2\n`);
23368
+ emit_data(status, `\t.p2align 2\n`);
23040
23369
  }
23041
23370
  } else if (node.value && node.value.node_type === "range") {
23042
23371
  if (status.function_return_label) {
@@ -24925,7 +25254,7 @@ function build_return_node$1(node, status, nir_value) {
24925
25254
  const finalized = status.moved ?? /* @__PURE__ */ new Set();
24926
25255
  for (const decl of all_scope_frames(status).flat()) {
24927
25256
  if (finalized.has(decl.name)) {
24928
- 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);
24929
25258
  continue;
24930
25259
  }
24931
25260
  if (is_field_struct_borrow(decl)) continue;
@@ -24945,7 +25274,7 @@ function build_return_node$1(node, status, nir_value) {
24945
25274
  const finalized = status.moved ?? /* @__PURE__ */ new Set();
24946
25275
  for (const decl of all_scope_frames(status).flat()) {
24947
25276
  if (finalized.has(decl.name)) {
24948
- 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);
24949
25278
  continue;
24950
25279
  }
24951
25280
  if (is_field_struct_borrow(decl)) continue;
@@ -25033,7 +25362,10 @@ function build_return_node$1(node, status, nir_value) {
25033
25362
  if (anon.base.is_moved) mark_moved_if_struct(anon.base, status);
25034
25363
  emit_field_overrides(temp_name, anon, build_node, status);
25035
25364
  emit_var_address(status, "x0", temp_name);
25036
- } 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
+ }
25037
25369
  if (ret_is_float) {
25038
25370
  ensure_newline(status);
25039
25371
  if (status.float_result_in_d0) emit_asm(status, `fmov d0, x0\n`);
@@ -25045,7 +25377,7 @@ function build_return_node$1(node, status, nir_value) {
25045
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("\"")) {
25046
25378
  const fn_name = status.current_function_name;
25047
25379
  const mangled = status.current_struct ? `${status.current_struct.name}_${fn_name}` : fn_name;
25048
- 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))) {
25049
25381
  emit_strdup_string(status);
25050
25382
  ensure_newline(status);
25051
25383
  status.last_result_is_heap = true;
@@ -25053,7 +25385,7 @@ function build_return_node$1(node, status, nir_value) {
25053
25385
  }
25054
25386
  const borrow_fn_name = status.current_function_name;
25055
25387
  const borrow_mangled = status.current_struct && borrow_fn_name ? `${status.current_struct.name}_${borrow_fn_name}` : void 0;
25056
- 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));
25057
25389
  let needs_borrow_strdup = false;
25058
25390
  if (normalizes_borrow_returns) {
25059
25391
  if (node.value.node_type === "value") {
@@ -25063,6 +25395,7 @@ function build_return_node$1(node, status, nir_value) {
25063
25395
  if (!is_str_lit && !is_numeric && raw_value !== "null" && !status.heap_strings?.has(raw_value)) needs_borrow_strdup = true;
25064
25396
  } else if (is_container_borrow_access(node.value)) needs_borrow_strdup = true;
25065
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;
25066
25399
  else if ((node.value.node_type === "match" || node.value.node_type === "switch") && !return_join_owned_string) needs_borrow_strdup = true;
25067
25400
  }
25068
25401
  if (needs_borrow_strdup) {
@@ -25118,6 +25451,16 @@ function build_return_node$1(node, status, nir_value) {
25118
25451
  }
25119
25452
  emit_string_field_strdups_at(status, ret_struct.name, "x8", 0, skip_fields);
25120
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
+ }
25121
25464
  }
25122
25465
  }
25123
25466
  }
@@ -25186,7 +25529,7 @@ function build_return_node$1(node, status, nir_value) {
25186
25529
  emit_nursery_joins_on_return_a64(status);
25187
25530
  for (const decl of all_scope_frames(status).flat()) {
25188
25531
  if (finalized.has(decl.name)) {
25189
- 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);
25190
25533
  continue;
25191
25534
  }
25192
25535
  if (is_field_struct_borrow(decl)) continue;
@@ -28423,6 +28766,7 @@ function build_function_call_node$1(node, status) {
28423
28766
  }
28424
28767
  }
28425
28768
  const is_struct = status.structs.find((s) => s.name === node.name && !s.is_simple_type);
28769
+ mark_tuple_literal_move_owners(node, status);
28426
28770
  const func_name = is_struct ? node.mangled_name ?? `${node.name}_init` : emission_label(node.resolved_function ?? node);
28427
28771
  const param_regs = [
28428
28772
  "x0",
@@ -28462,8 +28806,25 @@ function build_function_call_node$1(node, status) {
28462
28806
  const env_off = status.closure_env_offsets?.get(node.name);
28463
28807
  const paramReg = func_offset === void 0 ? status.function_param_regs?.get(node.name) : void 0;
28464
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);
28465
28820
  const lambda_arg_slots = [];
28466
- 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
+ };
28467
28828
  for (let i = node.params.length - 1; i >= 0; i--) {
28468
28829
  const param = node.params[i];
28469
28830
  const is_pair = type_from_value_node$1(param)?.name === "string";
@@ -28473,19 +28834,25 @@ function build_function_call_node$1(node, status) {
28473
28834
  emit_asm(status, `str x0, [x29, #${dispose_slot}]\n`);
28474
28835
  lambda_arg_slots.push(dispose_slot);
28475
28836
  }
28476
- const reg = param_regs[arg_slot];
28477
- const len_reg = param_regs[arg_slot + 1];
28478
- if (is_pair && len_reg !== "x1") emit_asm(status, `\nmov ${len_reg}, x1\n`);
28479
- if (reg !== "x0") emit_asm(status, `\nmov ${reg}, x0\n`);
28480
- else emit_asm(status, `\n`);
28481
- if (is_pair) arg_slot += 2;
28482
- 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
+ }
28483
28848
  }
28484
28849
  emit_asm(status, desc_source);
28485
28850
  emit_asm(status, `ldr x8, [x9]\n`);
28486
28851
  emit_asm(status, `ldr x0, [x9, #8]\n`);
28487
28852
  emit_asm(status, `blr x8\n`);
28488
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;
28489
28856
  } else {
28490
28857
  const variadic_idx = node.variadic_param_index;
28491
28858
  const ref_class_sync_names = [];
@@ -31214,6 +31581,20 @@ function emit_enum_payload_reclaim(cname, enum_node, c, p, status) {
31214
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`;
31215
31582
  else if (p.type.name === "string") status.code += `${guard} { free(${cname}._data._${c.name}.${p.name}.ptr); }\n`;
31216
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
+ }
31217
31598
  function build_auto_free(status) {
31218
31599
  free_scoped_declarations(status, status.scoped_declarations);
31219
31600
  if (status.deferred_frees?.length) {
@@ -31235,11 +31616,15 @@ function free_scoped_declarations(status, decls, persist_string_field_records =
31235
31616
  let commented = false;
31236
31617
  if (status.heap_string_fields?.size) for (const dec of decls) {
31237
31618
  const prefix = `${dec.name}.`;
31238
- for (const key of Array.from(status.heap_string_fields)) if (key.startsWith(prefix)) {
31239
- if (!commented) {
31240
- status.code += "\n// Auto-free\n";
31241
- commented = true;
31242
- }
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) {
31243
31628
  const dot = key.indexOf(".");
31244
31629
  const base = c_function_name(key.substring(0, dot));
31245
31630
  const arrow = dec.string_fields_via_pointer ? "->" : ".";
@@ -31248,6 +31633,7 @@ function free_scoped_declarations(status, decls, persist_string_field_records =
31248
31633
  status.code += `${free_fn}(${base}${arrow}${field}.ptr);\n`;
31249
31634
  if (!persist_string_field_records) status.heap_string_fields.delete(key);
31250
31635
  }
31636
+ if (nullable_flag) status.code += `}\n`;
31251
31637
  }
31252
31638
  for (const dec of decls) {
31253
31639
  if (dec.owned_heap_shell) {
@@ -31765,6 +32151,49 @@ function emit_rhs_value$1(rhs, nir, status) {
31765
32151
  }
31766
32152
  function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
31767
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
+ }
31768
32197
  if (node.left_value.node_type === "access") {
31769
32198
  const accessNode = node.left_value;
31770
32199
  if (accessNode.target.node_type === "value") {
@@ -32139,18 +32568,6 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
32139
32568
  }
32140
32569
  }
32141
32570
  status.code += ``;
32142
- const lhs_nullable_type = lhs_nullable_struct_type(node, status);
32143
- if (!node.operator && lhs_nullable_type) {
32144
- const lhs_expr = capture_build(node.left_value, status);
32145
- const flag = `${lhs_expr}_has`;
32146
- if (node.right_value.node_type === "value" && node.right_value.value === "null") status.code += `${flag} = 0`;
32147
- else {
32148
- status.code += `${lhs_expr} = `;
32149
- emit_rhs_value$1(node.right_value, nir_rhs, status);
32150
- status.code += `;\n${flag} = 1`;
32151
- }
32152
- return;
32153
- }
32154
32571
  if (!node.operator && !node.swap && node.left_value.node_type === "value") {
32155
32572
  const lhs_name = node.left_value.value;
32156
32573
  const lhs_type = type_from_value_node$1(node.left_value);
@@ -32460,6 +32877,101 @@ function emit_nursery_joins_on_return_c(status) {
32460
32877
  for (let i = status.nursery_stack.length - 1; i >= base; i--) emit_nursery_join_c(status, status.nursery_stack[i], true);
32461
32878
  }
32462
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
32463
32975
  //#region ../src/build_c/build_array_values_node.ts
32464
32976
  /**
32465
32977
  * Build an array literal's `{a, b, c}` initializer text. Under NIR-driven
@@ -32471,11 +32983,13 @@ function emit_nursery_joins_on_return_c(status) {
32471
32983
  function build_array_values_node(node, status, nir_elements) {
32472
32984
  status.code += `{`;
32473
32985
  const elem_is_string = node.type?.name === "string";
32986
+ const static_init = status.static_string_array_inits === true;
32474
32987
  node.values.forEach((value, i) => {
32475
32988
  if (i > 0) status.code += ", ";
32476
32989
  const is_string_literal = elem_is_string && value.node_type === "value" && value.value.length >= 2 && value.value.startsWith("\"") && value.value.endsWith("\"");
32477
32990
  const nir_elem = nir_elements?.[i];
32478
- 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) {
32479
32993
  status.code += `nomen_str_dup(`;
32480
32994
  if (nir_elem && nir_elem.node === value) emit_expr_from_nir(nir_elem, status);
32481
32995
  else build_node$1(value, status);
@@ -32618,6 +33132,42 @@ function build_declaration_node(node, status, nir_init, nir_swap) {
32618
33132
  status.code += `] = {${variables.map((v) => `&${v}`).join(", ")}}`;
32619
33133
  } else {
32620
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
+ }
32621
33171
  const mono_name = mono_type_name(node.type);
32622
33172
  const mono_struct = status.structs.find((s) => s.name === mono_name && !s.is_simple_type && !s.is_generic);
32623
33173
  const is_class_type = !!mono_struct?.is_class;
@@ -32821,6 +33371,7 @@ function build_declaration_node(node, status, nir_init, nir_swap) {
32821
33371
  status.current_nullable_call_flag = flag;
32822
33372
  emit_init_value(node.value, nir_init, status);
32823
33373
  status.current_nullable_call_flag = old;
33374
+ record_call_init_string_fields(node, status);
32824
33375
  } else {
32825
33376
  emit_init_value(node.value, nir_init, status);
32826
33377
  status.code += `;\n${flag} = 1`;
@@ -32890,6 +33441,7 @@ function build_declaration_node(node, status, nir_init, nir_swap) {
32890
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}`;
32891
33442
  else {
32892
33443
  status.code += " = ";
33444
+ status.static_string_array_inits = is_stack_array && !status.current_function && node.type.name === "string";
32893
33445
  const val_node = node.value;
32894
33446
  const val_is_string_literal = node.value.node_type === "value" && val_node.value.length >= 2 && val_node.value.startsWith("\"") && val_node.value.endsWith("\"");
32895
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);
@@ -32927,6 +33479,7 @@ function build_declaration_node(node, status, nir_init, nir_swap) {
32927
33479
  emit_init_value(node.value, nir_init, status);
32928
33480
  status.suppress_dereference = false;
32929
33481
  } else emit_init_value(node.value, nir_init, status);
33482
+ status.static_string_array_inits = false;
32930
33483
  }
32931
33484
  }
32932
33485
  if (node.swap && node.value.node_type === "access") {
@@ -33586,6 +34139,15 @@ function build_return_node(node, status, nir_value) {
33586
34139
  if (returned_recorded_string_fields?.has(field.name)) continue;
33587
34140
  status.code += `_return_val.${field.name} = nomen_str_dup(_return_val.${field.name});\n`;
33588
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
+ }
33589
34151
  emit_nursery_joins_on_return_c(status);
33590
34152
  reclaim_all_c_scopes(status);
33591
34153
  status.code += `return _return_val;\n`;
@@ -35350,6 +35912,22 @@ function emit_closure_env_type(func, status) {
35350
35912
  }
35351
35913
  //#endregion
35352
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
+ }
35353
35931
  function build_function_node(node, status) {
35354
35932
  if (node.is_generic) return;
35355
35933
  if (node.is_extern) {
@@ -35571,7 +36149,8 @@ function build_function_node(node, status) {
35571
36149
  status.current_function_name = old_function_name;
35572
36150
  status.current_function = old_current_function;
35573
36151
  status.closure_env = old_closure_env;
35574
- 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);
35575
36154
  if (node.name.toLocaleLowerCase() === "main" && status.used_fibers) {
35576
36155
  status.code += `\n__nomen_fiber_drain_all();\n`;
35577
36156
  status.code += `__nomen_io_shutdown();\n`;
@@ -36755,6 +37334,7 @@ function build_function_call_node(node, status) {
36755
37334
  const func_name = is_struct ? node.mangled_name ?? `${node.name}_init` : c_function_name(emission_label(node.resolved_function ?? node));
36756
37335
  const callee_params = node.resolved_function?.params?.filter((p) => !p.is_self_param);
36757
37336
  const lambda_arg_temps = /* @__PURE__ */ new Map();
37337
+ mark_tuple_literal_move_owners(node, status);
36758
37338
  if (!is_struct) for (let i = 0; i < node.params.length; i++) {
36759
37339
  const p = node.params[i];
36760
37340
  if (p.node_type !== "func") continue;
@@ -37156,101 +37736,6 @@ function build_todo_node$1(node, status) {
37156
37736
  status.code += `exit(EXIT_FAILURE);\n`;
37157
37737
  }
37158
37738
  //#endregion
37159
- //#region ../src/build_common/decode_char_literal.ts
37160
- /**
37161
- * Decode a Nomen char literal token (raw value INCLUDING its surrounding
37162
- * quotes, as carried by ValueNode) to its numeric code. The tokenizer
37163
- * consumes escape pairs as a unit, so multi-char forms like `'\\\\'`,
37164
- * `'\\n'`, `'\\''` and `'\\xNN'` arrive raw and must be decoded here.
37165
- * Returns undefined for malformed literals.
37166
- */
37167
- function decode_char_literal(raw) {
37168
- if (raw.length < 3 || raw[0] !== "'" || raw[raw.length - 1] !== "'") return;
37169
- const inner = raw.slice(1, -1);
37170
- if (inner.length === 1) return inner.charCodeAt(0);
37171
- if (inner[0] !== "\\") return;
37172
- const esc = inner[1];
37173
- switch (esc) {
37174
- case "\\": return 92;
37175
- case "n": return 10;
37176
- case "t": return 9;
37177
- case "r": return 13;
37178
- case "'": return 39;
37179
- case "\"": return 34;
37180
- case "0": return 0;
37181
- case "x": {
37182
- const hex = inner.slice(2);
37183
- if (!/^[0-9a-fA-F]+$/.test(hex)) return void 0;
37184
- return parseInt(hex, 16);
37185
- }
37186
- default: return esc.charCodeAt(0);
37187
- }
37188
- }
37189
- //#endregion
37190
- //#region ../src/build_c/utils/c_char_literal.ts
37191
- function c_char_literal(value) {
37192
- const code = decode_char_literal(value);
37193
- if (code === void 0) return value;
37194
- if (code === 92) return `'\\\\'`;
37195
- if (code === 39) return `'\\''`;
37196
- if (code === 10) return `'\\n'`;
37197
- if (code === 13) return `'\\r'`;
37198
- if (code === 9) return `'\\t'`;
37199
- if (code < 32 || code > 126) return `'\\x${code.toString(16)}'`;
37200
- return `'${String.fromCharCode(code)}'`;
37201
- }
37202
- //#endregion
37203
- //#region ../src/build_c/build_value_node.ts
37204
- const INT_LITERAL_SUFFIX = {
37205
- int: "L",
37206
- uint: "UL",
37207
- int64: "LL",
37208
- uint64: "ULL"
37209
- };
37210
- function build_value_node$1(node, status) {
37211
- let value = node.value;
37212
- if (status.closure_env?.has(value)) {
37213
- if (!(!!status.current_function?.params.some((p) => p.name === value) || !!find_decl_in_c_scopes(status, value))) {
37214
- status.code += status.closure_env.get(value);
37215
- return;
37216
- }
37217
- }
37218
- const inlined = status.top_level_consts?.get(value);
37219
- if (inlined?.value) {
37220
- build_node$1(fold_string_const(inlined, status.top_level_consts) ?? inlined.value, status);
37221
- return;
37222
- }
37223
- if (node.is_enum_shorthand) {
37224
- const enum_node = status.enums.find((e) => value.startsWith(e.name + "_"));
37225
- if (enum_node) {
37226
- if (enum_node.has_associated_data) status.code += `${value}_init()`;
37227
- else status.code += value;
37228
- return;
37229
- }
37230
- }
37231
- if (value === "null") value = "0";
37232
- else if (value === "true") value = "1";
37233
- else if (value === "false") value = "0";
37234
- else if (value === "default") value = "_nomen_default";
37235
- else if (is_int_literal(value)) value = value.replace(/_/g, "") + (INT_LITERAL_SUFFIX[node.type?.name ?? "int"] ?? "");
37236
- else if (node.resolved_function) {
37237
- status.code += materialize_func_value(node.resolved_function, status);
37238
- return;
37239
- } else if (value.startsWith("'") && value.endsWith("'")) value = c_char_literal(value);
37240
- else value = c_function_name(value);
37241
- if (value.startsWith("\"")) value = `nomen_str_lit(${escape_c_string(value)}, ${string_literal_length(value)})`;
37242
- if (value !== "self" && status.function_ref_params?.has(value) && !status.suppress_dereference) status.code += `*`;
37243
- if (value === "self" && status.function_ref_params?.has("self") && !status.suppress_dereference) status.code += `*`;
37244
- if (value !== "self" && status.ref_class_params?.has(value) && !status.suppress_dereference) {
37245
- status.code += `(*${value})`;
37246
- return;
37247
- }
37248
- status.code += value;
37249
- }
37250
- function escape_c_string(s) {
37251
- return reencode_hex_escapes(s.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"));
37252
- }
37253
- //#endregion
37254
37739
  //#region ../src/build_c/build_node.ts
37255
37740
  function build_node$1(node, status, with_semicolon = false) {
37256
37741
  if (node.allocations) {
@@ -38740,13 +39225,49 @@ function build_nullable_struct_assignment(node, status, nir_rhs) {
38740
39225
  if (node.left_value.node_type === "value") {
38741
39226
  const name = node.left_value.value;
38742
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}.`));
38743
39230
  if (rhs_is_null) {
39231
+ if (reclaimable) emit_recorded_field_reclaim_for_slot(status, name, type_name);
38744
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;
38745
39260
  return;
38746
39261
  }
38747
39262
  emit_rhs_value(node.right_value, nir_rhs, status);
38748
39263
  ensure_newline(status);
38749
- 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);
38750
39271
  const dst_offset = status.stack_offsets?.get(name);
38751
39272
  emit_struct_copy("x0", "x29", dst_offset ?? 0, struct_size, status);
38752
39273
  emit_asm(status, `mov x9, #1\n`);
@@ -41040,6 +41561,7 @@ function is_owned_heap_temp(node, status) {
41040
41561
  if (check_type_name !== "string") return false;
41041
41562
  if (check_node.node_type === "op") return true;
41042
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;
41043
41565
  const raw_name = check_node.name;
41044
41566
  const mangled = check_node.mangled_name || raw_name;
41045
41567
  const dbg = process.env.NOMEN_DBG_TEMP && typeof mangled === "string" && mangled.indexOf("replace") >= 0;
@@ -42282,6 +42804,22 @@ function resolve_access_target_type(target, status) {
42282
42804
  * final field is read. Without this, `state.rules.blocks.length` (State/
42283
42805
  * BlockParserState is a class) summed 8+8+8 and loaded from `state + 24`.
42284
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
+ }
42285
42823
  function access_chain_crosses_class(node, status) {
42286
42824
  const base_type = resolve_access_target_type(get_base_target(node), status);
42287
42825
  if (base_type?.name && status.structs.find((s) => s.name === base_type.name && s.is_class)) return true;
@@ -42704,6 +43242,12 @@ function build_access_field(node, status) {
42704
43242
  ensure_newline(status);
42705
43243
  const final_offset = offset;
42706
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
+ }
42707
43251
  const at_reg = status.at_addr_reg;
42708
43252
  status.at_addr_reg = void 0;
42709
43253
  const load_base = at_reg ?? "x0";
@@ -42725,7 +43269,7 @@ function build_access_field(node, status) {
42725
43269
  else emit_asm(status, `ldr x0, [${load_base}, #${final_offset}]\n`);
42726
43270
  return;
42727
43271
  }
42728
- if (target_is_class_access) {
43272
+ if (target_is_class_access || !chain_is_pure_fields(node.target)) {
42729
43273
  build_node(node.target, status);
42730
43274
  ensure_newline(status);
42731
43275
  const final_offset = get_field_offset(target_type?.name || "", access_field.name, status);
@@ -43328,9 +43872,11 @@ function build_access_method(node, access_func, status) {
43328
43872
  } else if (!target_is_simple && node.target.node_type === "access") {
43329
43873
  const access_target = node.target;
43330
43874
  if (access_target.access.node_type === "access_field") {
43331
- if (access_chain_crosses_class(access_target, status)) {
43332
- 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);
43333
43878
  ensure_newline(status);
43879
+ if (field_offset > 0) emit_asm(status, `add x0, x0, #${field_offset}\n`);
43334
43880
  } else {
43335
43881
  const offset = compute_field_offset(access_target, status);
43336
43882
  const base = get_base_target(access_target);
@@ -47526,9 +48072,11 @@ function mismatch(expected, actual, status, start) {
47526
48072
  * when either side's signature is unavailable.
47527
48073
  */
47528
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;
47529
48077
  const expected_sig = {
47530
- params: expected.func_params ?? expected.type?.func_params,
47531
- return_type: expected.func_return_type ?? expected.type?.func_return_type
48078
+ params: expected_params ?? (expected_return ? [] : void 0),
48079
+ return_type: expected_return
47532
48080
  };
47533
48081
  if (!expected_sig.params && !expected_sig.return_type) return;
47534
48082
  const actual_sig = arg_signature(arg, status);
@@ -49759,10 +50307,18 @@ function check_declaration_node(decl, status) {
49759
50307
  if (decl.value?.node_type === "access" && decl.value.access.node_type === "access_field") {
49760
50308
  const field_type = type_from_value_node(decl.value, status);
49761
50309
  if (field_type.name && is_owning_struct_type_requiring_move(field_type, status)) {
49762
- const field_name = decl.value.access.name;
49763
- 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);
49764
- else if (!decl.swap) add_error(status, `move out of a field requires a swap to revalidate it`, decl.value.start);
49765
- 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");
49766
50322
  }
49767
50323
  }
49768
50324
  if (decl.value?.node_type === "value" && decl.value.is_moved && !decl.swap) {
@@ -49906,6 +50462,33 @@ function extract_const_value(value) {
49906
50462
  if (/^[+-]?\d+$/.test(vn.value)) return parseInt(vn.value, 10);
49907
50463
  if (/^[+-]?\d+\.\d+$/.test(vn.value)) return parseFloat(vn.value);
49908
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
+ }
49909
50492
  //#endregion
49910
50493
  //#region ../src/check/utils/check_init_fields.ts
49911
50494
  const checked_structs = /* @__PURE__ */ new WeakSet();
@@ -54390,6 +54973,10 @@ function check_access_node(node, status) {
54390
54973
  if (af.is_destructure) {
54391
54974
  const i = af.destructure_index;
54392
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
+ }
54393
54980
  if (af.is_destructure_rename) {
54394
54981
  add_error(status, `Cannot rename elements when destructuring an array`, node.start);
54395
54982
  return false;
@@ -55151,6 +55738,55 @@ function check_array_values_node(array, status) {
55151
55738
  * On success, mutates `array` in-place into a FunctionCallNode that constructs
55152
55739
  * the appropriate auto-generated tuple struct.
55153
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
+ }
55154
55790
  function check_as_tuple(array, tuple_element_types, status) {
55155
55791
  if (array.values.length !== tuple_element_types.length) return check_as_array_or_inferred_tuple(array, status, tuple_element_types);
55156
55792
  let result = true;
@@ -55169,6 +55805,7 @@ function check_as_tuple(array, tuple_element_types, status) {
55169
55805
  value_types.push(vt);
55170
55806
  check_type_and_value_match(tuple_element_types[i], vt, value_from_value_node(value), status, value.start, "tuple");
55171
55807
  }
55808
+ if (check_tuple_element_ownership(array, status)) result = false;
55172
55809
  const struct_name = tuple_struct_name(tuple_element_types);
55173
55810
  get_or_create_tuple_struct(tuple_element_types, status);
55174
55811
  const constructor = new FunctionCallNode(array.start, struct_name);
@@ -55251,12 +55888,19 @@ function check_as_array_or_inferred_tuple(array, status, forced_tuple_types) {
55251
55888
  if (!has_outer_expected) {
55252
55889
  const meaningful = value_types.filter((t) => t.name && t.name !== "null");
55253
55890
  const first_meaningful = meaningful[0];
55254
- 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) {
55255
55898
  const elem_types = value_types.map((t, _i) => {
55256
55899
  if (!t.name || t.name === "null") return meaningful[0] || new Type("int");
55257
55900
  return t;
55258
55901
  });
55259
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;
55260
55904
  const struct_name = tuple_struct_name(elem_types);
55261
55905
  get_or_create_tuple_struct(elem_types, status);
55262
55906
  const constructor = new FunctionCallNode(array.start, struct_name);
@@ -55753,7 +56397,7 @@ function list_element_type(t, status) {
55753
56397
  return struct.source_type_args?.[0];
55754
56398
  }
55755
56399
  function check_for_loop_node(for_loop, status) {
55756
- 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")) {
55757
56401
  const list_type = type_from_value_node(for_loop.list, status);
55758
56402
  const enumerable = list_type.name ? has_trait(list_type.name, "Enumerable", status) : false;
55759
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"));
@@ -55779,6 +56423,16 @@ function check_for_loop_node(for_loop, status) {
55779
56423
  check_node(for_loop.list, for_status);
55780
56424
  const list_type = type_from_value_node(for_loop.list, for_status);
55781
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
+ }
55782
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);
55783
56437
  if (for_loop.item_is_ref && list_type.is_array && for_loop.list.node_type === "value") {
55784
56438
  const list_name = for_loop.list.value;
@@ -55831,6 +56485,23 @@ function check_for_loop_node(for_loop, status) {
55831
56485
  * break/continue so mutations persist on all exit paths. (The write-back is
55832
56486
  * skipped on `return`, matching Rust's copy semantics.)
55833
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
+ }
55834
56505
  function desugar_array_for_loop(for_loop, array_type, elem_type) {
55835
56506
  const list = for_loop.list;
55836
56507
  const start = list.start;
@@ -57076,6 +57747,7 @@ function check(root, options) {
57076
57747
  allow_internal: options?.allow_internal,
57077
57748
  library_boundary: options?.library_boundary
57078
57749
  };
57750
+ stamp_literal_element_moves(root);
57079
57751
  check_node(root, status);
57080
57752
  unwrap_unsafe_blocks(root);
57081
57753
  if (!status.errors.length) emit_warnings(root, status);
@@ -57883,6 +58555,10 @@ function looks_like_destructuring(status, start_idx) {
57883
58555
  * - bare name: `a` — positional (tuples/arrays) or same-named field (structs)
57884
58556
  * - rename: `field = name` — struct/class field accessed as `field`,
57885
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.
57886
58562
  */
57887
58563
  function parse_destructuring(visibility, declaration, start, status) {
57888
58564
  expect("[", status);
@@ -57893,8 +58569,17 @@ function parse_destructuring(visibility, declaration, start, status) {
57893
58569
  let field = first;
57894
58570
  let name = first;
57895
58571
  let rename = false;
57896
- 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) === "=") {
57897
58578
  accept("=", status);
58579
+ if (peek_current(status) === "move") {
58580
+ consume(status);
58581
+ move = true;
58582
+ }
57898
58583
  name = consume_name(status);
57899
58584
  field = first;
57900
58585
  rename = true;
@@ -57902,7 +58587,8 @@ function parse_destructuring(visibility, declaration, start, status) {
57902
58587
  bindings.push({
57903
58588
  name,
57904
58589
  field,
57905
- rename
58590
+ rename,
58591
+ move
57906
58592
  });
57907
58593
  index++;
57908
58594
  if (!accept(",", status)) break;
@@ -57932,7 +58618,7 @@ function parse_destructuring(visibility, declaration, start, status) {
57932
58618
  const temp_counter = status.__tuple_destructure_counter || 0;
57933
58619
  status.__tuple_destructure_counter = temp_counter + 1;
57934
58620
  const temp_name = `_tuple_dst_${temp_counter}`;
57935
- 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));
57936
58622
  base_node = new ValueNode(start, temp_name);
57937
58623
  }
57938
58624
  for (let i = 0; i < bindings.length; i++) {
@@ -57942,7 +58628,9 @@ function parse_destructuring(visibility, declaration, start, status) {
57942
58628
  access_field.is_destructure = true;
57943
58629
  access_field.destructure_index = i;
57944
58630
  access_field.is_destructure_rename = binding.rename;
58631
+ access_field.destructure_move = binding.move;
57945
58632
  const access = new AccessNode(start, base_node, access_field);
58633
+ access.is_moved = binding.move;
57946
58634
  push_to_parent(new DeclarationNode(start, visibility, declaration, binding.name, new Type(""), access));
57947
58635
  }
57948
58636
  }