nomen-lang 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -6538,6 +6538,18 @@ var FunctionCallNode = class extends BaseNode {
6538
6538
  ref_param_indices;
6539
6539
  move_param_indices;
6540
6540
  /**
6541
+ * Indices of arguments bound to a `move T` parameter where T is a
6542
+ * pass-by-value owning struct on a registered free function: ownership
6543
+ * TRANSFERS at the boundary (the callee seeds the param's string-field
6544
+ * records and frees them at its exit), so the caller drops its records
6545
+ * instead of freeing them. The plain `move_param_indices` convention
6546
+ * (callee deep-copies, caller frees the originals) still governs
6547
+ * container stores and every non-transfer shape. Stamped by
6548
+ * check_function_call alongside owned_value_param_indices.
6549
+ */
6550
+ move_owned_param_indices;
6551
+ owned_value_param_indices;
6552
+ /**
6541
6553
  * Indices of arguments whose corresponding callee parameter is a nullable
6542
6554
  * struct value type (`T?` where T is a non-class struct). Populated during
6543
6555
  * the check pass when the callee's signature is resolved. The build uses
@@ -6958,6 +6970,12 @@ var FunctionNode = class extends BaseNode {
6958
6970
  * are dual-natured: called under their own name AND passed as values).
6959
6971
  */
6960
6972
  is_closure;
6973
+ /** The function's address escapes into a func-typed value (bound to a
6974
+ * `var func …` variable/field, passed as a func value, …) — so it may be
6975
+ * invoked through a pointer with a BORROWED argument: pass-by-value
6976
+ * normalization (owned-param record seeding / call-site shell
6977
+ * materialization) must not apply. Set during check. */
6978
+ address_escaped;
6961
6979
  /**
6962
6980
  * Captured outer locals (CLOSURE.md Phase 2). One entry per variable the
6963
6981
  * lambda body references from an enclosing function; the backends emit an
@@ -7366,6 +7384,16 @@ var DeclarationNode = class extends BaseNode {
7366
7384
  * paths strdup the initializer and free the temp's copy at scope exit.
7367
7385
  * Set at build time by hoist_field_overrides. */
7368
7386
  force_owned_string;
7387
+ /** True for an OWNED value-struct PARAMETER whose string fields the
7388
+ * callee frees at scope exit: the field accesses (and the record-free
7389
+ * emitted by free_scoped_declarations) go through the param POINTER
7390
+ * (`p->field`) instead of value syntax (`p.field`). Set at build time
7391
+ * when the function builder seeds pass-by-value struct params. */
7392
+ string_fields_via_pointer;
7393
+ /** True alongside string_fields_via_pointer when the caller materialized
7394
+ * a heap shell for the argument (pass-by-value): the callee frees the
7395
+ * shell itself at scope exit (`free(p)`) after freeing the fields. */
7396
+ owned_heap_shell;
7369
7397
  constructor(start, visibility, declaration, name, type, value) {
7370
7398
  super("declare", start);
7371
7399
  this.visibility = visibility;
@@ -10913,6 +10941,117 @@ function array_struct_name(type, status) {
10913
10941
  return status.structs.find((s) => s.name === mono && !s.is_generic) ? mono : void 0;
10914
10942
  }
10915
10943
  //#endregion
10944
+ //#region ../src/build_common/destroy_analysis.ts
10945
+ /**
10946
+ * Whether any field of the struct is an enum-with-data value owning heap
10947
+ * payloads (a string payload — always heap, case construction strdups it —
10948
+ * or a class/trait reference payload — construction transfers ownership).
10949
+ * Such a field must be reclaimed when the struct dies.
10950
+ */
10951
+ function struct_has_owning_enum_field(struct, table) {
10952
+ for (const field of struct.fields) {
10953
+ if (field.type.is_ref) continue;
10954
+ const en = table.enums?.find((e) => e.name === field.type.name);
10955
+ if (!en?.has_associated_data) continue;
10956
+ for (const c of en.cases) for (const p of c.params) {
10957
+ if (p.type.name === "string" && !p.type.is_array) return true;
10958
+ if (table.traits?.some((t) => t.name === p.type.name)) return true;
10959
+ if (table.structs.find((s) => s.name === p.type.name)?.is_class) return true;
10960
+ }
10961
+ }
10962
+ return false;
10963
+ }
10964
+ /**
10965
+ * Whether a struct declares its own `#destroy` (user side effects /
10966
+ * resource release).
10967
+ */
10968
+ function has_destroy(struct) {
10969
+ return !!struct.functions.find((f) => f.name === "#destroy");
10970
+ }
10971
+ /**
10972
+ * Whether any field (recursively) requires destroy handling — a class
10973
+ * field (heap instance to reclaim) or a nested struct with its own
10974
+ * `#destroy` (or nested fields that do). Deliberately excludes `string`
10975
+ * fields: a struct local whose only owning fields are strings is NOT
10976
+ * auto-destroyed at scope exit (the strings may be raw rodata arg
10977
+ * pointers, not heap) — see `struct_needs_auto_destroy` for the broader
10978
+ * form used to decide whether a `<Struct>_destroy` function is generated.
10979
+ */
10980
+ function has_struct_fields_with_destroy(struct, table) {
10981
+ for (const field of struct.fields) {
10982
+ if (field.type.is_ref) continue;
10983
+ const field_struct = resolve_struct_type(field.type, table);
10984
+ if (!field_struct) continue;
10985
+ if (field_struct.is_class) return true;
10986
+ if (has_destroy(field_struct)) return true;
10987
+ if (has_struct_fields_with_destroy(field_struct, table)) return true;
10988
+ }
10989
+ return false;
10990
+ }
10991
+ /**
10992
+ * Whether a struct (or any embedded struct field, recursively) needs a
10993
+ * destroy call at scope exit — it has a `#destroy`, a class-typed field,
10994
+ * or a nested struct field that itself needs destroying. This does NOT
10995
+ * count `string` fields (see `has_struct_fields_with_destroy`).
10996
+ */
10997
+ function struct_needs_destroy(struct, table) {
10998
+ return has_destroy(struct) || has_struct_fields_with_destroy(struct, table) || struct_has_owning_enum_field(struct, table);
10999
+ }
11000
+ /**
11001
+ * Whether a struct needs an auto-generated `<Struct>_destroy` function.
11002
+ * Broader than `struct_needs_destroy`: also returns true for structs with
11003
+ * `string` fields, because a Buffer<T> for such a struct deep-copies the
11004
+ * strings into slots (strdup on store) and the per-element destroy must
11005
+ * free them. The generated destroy frees each string field; it is called
11006
+ * from Buffer's specialized #destroy / replace_T, NOT from struct local
11007
+ * scope exit (`struct_needs_destroy` governs that path and excludes
11008
+ * strings).
11009
+ */
11010
+ function struct_needs_auto_destroy(struct, table) {
11011
+ if (struct_needs_destroy(struct, table)) return true;
11012
+ if (struct_has_owning_enum_field(struct, table)) return true;
11013
+ for (const field of struct.fields) {
11014
+ if (field.type.is_ref) continue;
11015
+ if (field.type.is_view) continue;
11016
+ if (field.type.name === "string" && !field.type.is_array) return true;
11017
+ const field_struct = resolve_struct_type(field.type, table);
11018
+ if (field_struct && !field_struct.is_class && struct_needs_auto_destroy(field_struct, table)) return true;
11019
+ }
11020
+ return false;
11021
+ }
11022
+ //#endregion
11023
+ //#region ../src/build_common/has_string_fields.ts
11024
+ /**
11025
+ * Whether any field (recursively, through nested owning value structs)
11026
+ * is an owned `string`. Shared by BOTH backends' owning-Buffer element
11027
+ * specialization (the slot deep-copies strdup'd strings, so a
11028
+ * string-field element is owning even when `struct_needs_destroy`
11029
+ * ignores string-only locals) — the C comment in
11030
+ * owning_buffer_specialize records the divergence this once caused.
11031
+ * `ref` and `view T` fields are skipped: a ref aliases the caller's
11032
+ * storage and a view's byte copy owns nothing.
11033
+ */
11034
+ function has_string_fields(node, status) {
11035
+ for (const field of node.fields) {
11036
+ if (field.type.is_ref) continue;
11037
+ if (field.type.is_view) continue;
11038
+ if (field.type.name === "string" && !field.type.is_array) return true;
11039
+ const field_struct = status.structs.find((s) => s.name === field.type.name && !s.is_simple_type && !s.is_generic);
11040
+ if (field_struct && !field_struct.is_class && has_string_fields(field_struct, status)) return true;
11041
+ }
11042
+ return false;
11043
+ }
11044
+ /**
11045
+ * The DIRECT `string` fields of a value struct (nested struct fields are not
11046
+ * descended into — nested string fields are outside the per-field ownership
11047
+ * tracking everywhere else too). Used by the return-boundary normalization:
11048
+ * the returned struct's unrecorded fields are strdup'd so the value the
11049
+ * caller receives is uniformly heap-owned.
11050
+ */
11051
+ function direct_string_fields(node) {
11052
+ return node.fields.filter((f) => !f.type.is_ref && !f.type.is_view && !f.type.is_array && f.type.name === "string");
11053
+ }
11054
+ //#endregion
10916
11055
  //#region ../src/build_common/scan_force_heap_strings.ts
10917
11056
  function scan_force_heap_strings(statements, structs) {
10918
11057
  const result = /* @__PURE__ */ new Set();
@@ -13391,85 +13530,6 @@ function emit_strdup(status) {
13391
13530
  emit_asm(status, `ldr x1, [sp], #16\n`);
13392
13531
  }
13393
13532
  //#endregion
13394
- //#region ../src/build_common/destroy_analysis.ts
13395
- /**
13396
- * Whether any field of the struct is an enum-with-data value owning heap
13397
- * payloads (a string payload — always heap, case construction strdups it —
13398
- * or a class/trait reference payload — construction transfers ownership).
13399
- * Such a field must be reclaimed when the struct dies.
13400
- */
13401
- function struct_has_owning_enum_field(struct, table) {
13402
- for (const field of struct.fields) {
13403
- if (field.type.is_ref) continue;
13404
- const en = table.enums?.find((e) => e.name === field.type.name);
13405
- if (!en?.has_associated_data) continue;
13406
- for (const c of en.cases) for (const p of c.params) {
13407
- if (p.type.name === "string" && !p.type.is_array) return true;
13408
- if (table.traits?.some((t) => t.name === p.type.name)) return true;
13409
- if (table.structs.find((s) => s.name === p.type.name)?.is_class) return true;
13410
- }
13411
- }
13412
- return false;
13413
- }
13414
- /**
13415
- * Whether a struct declares its own `#destroy` (user side effects /
13416
- * resource release).
13417
- */
13418
- function has_destroy(struct) {
13419
- return !!struct.functions.find((f) => f.name === "#destroy");
13420
- }
13421
- /**
13422
- * Whether any field (recursively) requires destroy handling — a class
13423
- * field (heap instance to reclaim) or a nested struct with its own
13424
- * `#destroy` (or nested fields that do). Deliberately excludes `string`
13425
- * fields: a struct local whose only owning fields are strings is NOT
13426
- * auto-destroyed at scope exit (the strings may be raw rodata arg
13427
- * pointers, not heap) — see `struct_needs_auto_destroy` for the broader
13428
- * form used to decide whether a `<Struct>_destroy` function is generated.
13429
- */
13430
- function has_struct_fields_with_destroy(struct, table) {
13431
- for (const field of struct.fields) {
13432
- if (field.type.is_ref) continue;
13433
- const field_struct = resolve_struct_type(field.type, table);
13434
- if (!field_struct) continue;
13435
- if (field_struct.is_class) return true;
13436
- if (has_destroy(field_struct)) return true;
13437
- if (has_struct_fields_with_destroy(field_struct, table)) return true;
13438
- }
13439
- return false;
13440
- }
13441
- /**
13442
- * Whether a struct (or any embedded struct field, recursively) needs a
13443
- * destroy call at scope exit — it has a `#destroy`, a class-typed field,
13444
- * or a nested struct field that itself needs destroying. This does NOT
13445
- * count `string` fields (see `has_struct_fields_with_destroy`).
13446
- */
13447
- function struct_needs_destroy(struct, table) {
13448
- return has_destroy(struct) || has_struct_fields_with_destroy(struct, table) || struct_has_owning_enum_field(struct, table);
13449
- }
13450
- /**
13451
- * Whether a struct needs an auto-generated `<Struct>_destroy` function.
13452
- * Broader than `struct_needs_destroy`: also returns true for structs with
13453
- * `string` fields, because a Buffer<T> for such a struct deep-copies the
13454
- * strings into slots (strdup on store) and the per-element destroy must
13455
- * free them. The generated destroy frees each string field; it is called
13456
- * from Buffer's specialized #destroy / replace_T, NOT from struct local
13457
- * scope exit (`struct_needs_destroy` governs that path and excludes
13458
- * strings).
13459
- */
13460
- function struct_needs_auto_destroy(struct, table) {
13461
- if (struct_needs_destroy(struct, table)) return true;
13462
- if (struct_has_owning_enum_field(struct, table)) return true;
13463
- for (const field of struct.fields) {
13464
- if (field.type.is_ref) continue;
13465
- if (field.type.is_view) continue;
13466
- if (field.type.name === "string" && !field.type.is_array) return true;
13467
- const field_struct = resolve_struct_type(field.type, table);
13468
- if (field_struct && !field_struct.is_class && struct_needs_auto_destroy(field_struct, table)) return true;
13469
- }
13470
- return false;
13471
- }
13472
- //#endregion
13473
13533
  //#region ../src/build_common/temp_anchor_consolidation.ts
13474
13534
  /**
13475
13535
  * Shared OWNERSHIP decision for hoisted call-temporary consolidation.
@@ -13504,6 +13564,144 @@ function superseded_param_temp_names(table, call_node, result_type_name) {
13504
13564
  return names;
13505
13565
  }
13506
13566
  //#endregion
13567
+ //#region ../src/build_aarch64/utils/string_pair.ts
13568
+ /**
13569
+ * Load the string variable `name` into the consecutive register pair
13570
+ * (base, base+1) — default x0/x1. Handles the plain slot case; callers
13571
+ * that have register-allocated or param forms handle theirs first.
13572
+ */
13573
+ function emit_string_pair_load(status, name, base = "x0") {
13574
+ const offset = status.stack_offsets?.get(name);
13575
+ if (offset === void 0) return false;
13576
+ const w = base.startsWith("x") ? base : "x0";
13577
+ const n = parseInt(w.substring(1), 10);
13578
+ if (offset + 8 > 504) {
13579
+ emit_asm(status, `ldr ${w}, [x29, #${offset}]\n`);
13580
+ emit_asm(status, `ldr x${n + 1}, [x29, #${offset + 8}]\n`);
13581
+ return true;
13582
+ }
13583
+ emit_asm(status, `ldp ${w}, x${n + 1}, [x29, #${offset}]\n`);
13584
+ return true;
13585
+ }
13586
+ /** Load the pair at [addr_reg, #offset] into (base, base+1). */
13587
+ function emit_string_pair_load_at(status, addr_reg, offset, base = "x0") {
13588
+ const w = base.startsWith("x") ? base : "x0";
13589
+ emit_asm(status, `ldp ${w}, x${parseInt(w.substring(1), 10) + 1}, [${addr_reg}, #${offset}]\n`);
13590
+ }
13591
+ /**
13592
+ * strdup the fat string in (x0=ptr, x1=len): preserve the len half across
13593
+ * the call and leave the owned copy's pair in (x0, x1). The caller keeps
13594
+ * ownership of the original.
13595
+ */
13596
+ function emit_strdup_string(status) {
13597
+ emit_asm(status, `str x1, [sp, #-16]!\n`);
13598
+ emit_asm(status, status.audit ? `bl _nomen_strdup_wrap\n` : `bl _strdup\n`);
13599
+ emit_asm(status, `ldr x1, [sp], #16\n`);
13600
+ }
13601
+ /**
13602
+ * Pair load/store at [x29, #offset] with an arbitrary register pair —
13603
+ * splits into two single-word accesses beyond the ldp/stp +504 range.
13604
+ */
13605
+ function emit_pair_load_x29(status, offset, a = "x0", b = "x1") {
13606
+ if (offset + 8 > 504) {
13607
+ emit_asm(status, `ldr ${a}, [x29, #${offset}]\n`);
13608
+ emit_asm(status, `ldr ${b}, [x29, #${offset + 8}]\n`);
13609
+ return;
13610
+ }
13611
+ emit_asm(status, `ldp ${a}, ${b}, [x29, #${offset}]\n`);
13612
+ }
13613
+ function emit_pair_store_x29(status, offset, a = "x0", b = "x1") {
13614
+ if (offset + 8 > 504) {
13615
+ emit_asm(status, `str ${a}, [x29, #${offset}]\n`);
13616
+ emit_asm(status, `str ${b}, [x29, #${offset + 8}]\n`);
13617
+ return;
13618
+ }
13619
+ emit_asm(status, `stp ${a}, ${b}, [x29, #${offset}]\n`);
13620
+ }
13621
+ /**
13622
+ * Owning specializations for Array<string>'s raw T-generic bodies
13623
+ * (`with` / `set`), replacing the shared-pointer copies with per-slot deep
13624
+ * copies — the aarch64 analog of Array.nm's `#if T_NEEDS_STRDUP` C branch.
13625
+ *
13626
+ * Conventions (matching Array.nm raw bodies):
13627
+ * - `with` (static): x0/x1 = value pair, x2 = count. Returns x0 = heap
13628
+ * buffer ([ptr] = length, elements at +8..).
13629
+ * - `set` (method): x19 = first element, x1 = index, x2/x3 = value pair.
13630
+ * Bounds are guaranteed by the Nomen-level constraints.
13631
+ */
13632
+ function emit_owning_array_string_specialize(func_name, status, self_reg = "x19") {
13633
+ if (func_name === "with") {
13634
+ emit_asm(status, `stp x19, x20, [sp, #-16]!\n`);
13635
+ emit_asm(status, `stp x21, x22, [sp, #-16]!\n`);
13636
+ emit_asm(status, `stp x24, x25, [sp, #-16]!\n`);
13637
+ emit_asm(status, `mov x19, x0\n`);
13638
+ emit_asm(status, `mov x20, x1\n`);
13639
+ emit_asm(status, `mov x21, x2\n`);
13640
+ emit_asm(status, `add x0, x21, #1\n`);
13641
+ emit_asm(status, `lsl x0, x0, #4\n`);
13642
+ emit_malloc(status);
13643
+ emit_asm(status, `mov x22, x0\n`);
13644
+ emit_asm(status, `str x21, [x22]\n`);
13645
+ emit_asm(status, `mov x23, #8\n`);
13646
+ emit_asm(status, `mov x24, #0\n`);
13647
+ emit_asm(status, `.Larr_str_with_loop:\n`);
13648
+ emit_asm(status, `cmp x24, x21\n`);
13649
+ emit_asm(status, `b.ge .Larr_str_with_done\n`);
13650
+ emit_asm(status, `mov x0, x19\n`);
13651
+ emit_strdup(status);
13652
+ emit_asm(status, `add x25, x22, x23\n`);
13653
+ emit_asm(status, `stp x0, x20, [x25]\n`);
13654
+ emit_asm(status, `add x23, x23, #16\n`);
13655
+ emit_asm(status, `add x24, x24, #1\n`);
13656
+ emit_asm(status, `b .Larr_str_with_loop\n`);
13657
+ emit_asm(status, `.Larr_str_with_done:\n`);
13658
+ emit_asm(status, `mov x0, x22\n`);
13659
+ emit_asm(status, `ldp x24, x25, [sp], #16\n`);
13660
+ emit_asm(status, `ldp x21, x22, [sp], #16\n`);
13661
+ emit_asm(status, `ldp x19, x20, [sp], #16\n`);
13662
+ return true;
13663
+ }
13664
+ if (func_name === "at" || func_name === "first" || func_name === "at_end") {
13665
+ emit_asm(status, `stp x20, x21, [sp, #-16]!\n`);
13666
+ if (func_name === "at") {
13667
+ emit_asm(status, `lsl x9, x1, #4\n`);
13668
+ emit_asm(status, `add x9, ${self_reg}, x9\n`);
13669
+ } else if (func_name === "at_end") {
13670
+ emit_asm(status, `ldr x9, [${self_reg}, #-8]\n`);
13671
+ emit_asm(status, `sub x9, x9, #1\n`);
13672
+ emit_asm(status, `lsl x9, x9, #4\n`);
13673
+ emit_asm(status, `add x9, ${self_reg}, x9\n`);
13674
+ } else emit_asm(status, `add x9, ${self_reg}, #0\n`);
13675
+ emit_asm(status, `ldp x0, x1, [x9]\n`);
13676
+ emit_asm(status, `ldp x20, x21, [sp], #16\n`);
13677
+ return true;
13678
+ }
13679
+ if (func_name === "set") {
13680
+ emit_asm(status, `stp x20, x21, [sp, #-16]!\n`);
13681
+ emit_asm(status, `lsl x9, x1, #4\n`);
13682
+ emit_asm(status, `add x9, ${self_reg}, x9\n`);
13683
+ emit_asm(status, `ldr x0, [x9]\n`);
13684
+ emit_asm(status, `mov x20, x2\n`);
13685
+ emit_asm(status, `mov x21, x3\n`);
13686
+ emit_free(status);
13687
+ emit_asm(status, `mov x0, x20\n`);
13688
+ emit_strdup(status);
13689
+ emit_asm(status, `str x0, [x9]\n`);
13690
+ emit_asm(status, `str x21, [x9, #8]\n`);
13691
+ emit_asm(status, `ldp x20, x21, [sp], #16\n`);
13692
+ return true;
13693
+ }
13694
+ return false;
13695
+ }
13696
+ /** Pair store to [base_reg, #offset] with stur fallback (misaligned/far). */
13697
+ function emit_pair_store_to(status, base_reg, offset, a, b) {
13698
+ if (offset % 8 === 0 && offset + 8 <= 504 && offset >= 0) emit_asm(status, `stp ${a}, ${b}, [${base_reg}, #${offset}]\n`);
13699
+ else {
13700
+ emit_asm(status, `stur ${a}, [${base_reg}, #${offset}]\n`);
13701
+ emit_asm(status, `stur ${b}, [${base_reg}, #${offset + 8}]\n`);
13702
+ }
13703
+ }
13704
+ //#endregion
13507
13705
  //#region ../src/build_aarch64/utils/auto_destroy.ts
13508
13706
  /**
13509
13707
  * Whether a declaration's initializer is a non-`move` FIELD ACCESS — a shallow
@@ -14346,6 +14544,27 @@ function mark_moved_if_struct(value, status, opts) {
14346
14544
  status.moved.add(var_name);
14347
14545
  }
14348
14546
  }
14547
+ /**
14548
+ * strdup the UNRECORDED direct string fields of a value struct whose base is
14549
+ * in `base_reg` (+ base_offset) — the return-boundary normalization: the
14550
+ * sret byte-copy transfers the RECORDED (heap) fields raw, but unrecorded
14551
+ * fields may hold rodata/borrowed bytes the caller cannot free, so they are
14552
+ * deep-copied here and the returned struct is uniformly heap-owned.
14553
+ */
14554
+ function emit_string_field_strdups_at(status, struct_name, base_reg, base_offset = 0, skip_fields) {
14555
+ const struct = status.structs.find((s) => s.name === struct_name && !s.is_simple_type && !s.is_class);
14556
+ if (!struct) return;
14557
+ for (const field of direct_string_fields(struct)) {
14558
+ if (skip_fields?.has(field.name)) continue;
14559
+ const off = base_offset + get_field_offset(struct_name, field.name, status);
14560
+ emit_asm(status, `str ${base_reg}, [sp, #-16]!\n`);
14561
+ emit_string_pair_load_at(status, base_reg, off);
14562
+ emit_strdup_string(status);
14563
+ emit_asm(status, `mov x13, x0\n`);
14564
+ emit_asm(status, `ldr ${base_reg}, [sp], #16\n`);
14565
+ emit_pair_store_to(status, base_reg, off, "x13", "x1");
14566
+ }
14567
+ }
14349
14568
  //#endregion
14350
14569
  //#region ../src/build_aarch64/utils/closure_a64.ts
14351
14570
  /**
@@ -16823,28 +17042,6 @@ function value_number_loops(body, ast_list, status, mutate_ast) {
16823
17042
  };
16824
17043
  }
16825
17044
  //#endregion
16826
- //#region ../src/build_common/has_string_fields.ts
16827
- /**
16828
- * Whether any field (recursively, through nested owning value structs)
16829
- * is an owned `string`. Shared by BOTH backends' owning-Buffer element
16830
- * specialization (the slot deep-copies strdup'd strings, so a
16831
- * string-field element is owning even when `struct_needs_destroy`
16832
- * ignores string-only locals) — the C comment in
16833
- * owning_buffer_specialize records the divergence this once caused.
16834
- * `ref` and `view T` fields are skipped: a ref aliases the caller's
16835
- * storage and a view's byte copy owns nothing.
16836
- */
16837
- function has_string_fields(node, status) {
16838
- for (const field of node.fields) {
16839
- if (field.type.is_ref) continue;
16840
- if (field.type.is_view) continue;
16841
- if (field.type.name === "string" && !field.type.is_array) return true;
16842
- const field_struct = status.structs.find((s) => s.name === field.type.name && !s.is_simple_type && !s.is_generic);
16843
- if (field_struct && !field_struct.is_class && has_string_fields(field_struct, status)) return true;
16844
- }
16845
- return false;
16846
- }
16847
- //#endregion
16848
17045
  //#region ../src/build_aarch64/utils/owning_buffer_specialize.ts
16849
17046
  /**
16850
17047
  * Detect whether a monomorphized struct is a `Buffer_<T>` whose element type
@@ -19458,6 +19655,16 @@ function build_function_node$1(node, status) {
19458
19655
  }
19459
19656
  param_idx++;
19460
19657
  if (param.declaration === "var") status.function_param_vars.add(param.name);
19658
+ const is_free_function = (!node.scope || node.scope.node_type === "root" || node.scope.node_type === "func") && !node.params.some((p) => p.is_self_param);
19659
+ const owned_value_param = !param.is_self_param && !param.is_variadic && !param.type.is_ref && !param.type.is_nullable && !param.type.is_view && !param.type.is_array && param.type.name ? status.structs.find((st) => st.name === param.type.name && !st.is_simple_type && !st.is_class) : void 0;
19660
+ if (is_free_function && !node.address_escaped && owned_value_param && direct_string_fields(owned_value_param).length > 0 && !(owned_value_param.traits ?? []).length && !struct_needs_destroy(owned_value_param, status) && (status.normalized_struct_returners?.has(node.name) || status.normalized_struct_returners?.has(label_name)) && !status.func_address_taken?.has(node.name) && !status.func_address_taken?.has(label_name)) {
19661
+ status.function_param_vars.add(param.name);
19662
+ const param_decl = new DeclarationNode(param.start, "private", "var", param.name, param.type);
19663
+ param_decl.string_fields_via_pointer = true;
19664
+ status.scoped_declarations.push(param_decl);
19665
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
19666
+ for (const field of direct_string_fields(owned_value_param)) status.heap_string_fields.add(`${param.name}.${field.name}`);
19667
+ }
19461
19668
  if (param.type.is_array) {
19462
19669
  if (!param.is_variadic ? array_struct_name(param.type, status) : void 0) {
19463
19670
  if (!status.heap_array_vars) status.heap_array_vars = /* @__PURE__ */ new Set();
@@ -19669,144 +19876,6 @@ function build_function_node$1(node, status) {
19669
19876
  status.function_return_type = void 0;
19670
19877
  }
19671
19878
  //#endregion
19672
- //#region ../src/build_aarch64/utils/string_pair.ts
19673
- /**
19674
- * Load the string variable `name` into the consecutive register pair
19675
- * (base, base+1) — default x0/x1. Handles the plain slot case; callers
19676
- * that have register-allocated or param forms handle theirs first.
19677
- */
19678
- function emit_string_pair_load(status, name, base = "x0") {
19679
- const offset = status.stack_offsets?.get(name);
19680
- if (offset === void 0) return false;
19681
- const w = base.startsWith("x") ? base : "x0";
19682
- const n = parseInt(w.substring(1), 10);
19683
- if (offset + 8 > 504) {
19684
- emit_asm(status, `ldr ${w}, [x29, #${offset}]\n`);
19685
- emit_asm(status, `ldr x${n + 1}, [x29, #${offset + 8}]\n`);
19686
- return true;
19687
- }
19688
- emit_asm(status, `ldp ${w}, x${n + 1}, [x29, #${offset}]\n`);
19689
- return true;
19690
- }
19691
- /** Load the pair at [addr_reg, #offset] into (base, base+1). */
19692
- function emit_string_pair_load_at(status, addr_reg, offset, base = "x0") {
19693
- const w = base.startsWith("x") ? base : "x0";
19694
- emit_asm(status, `ldp ${w}, x${parseInt(w.substring(1), 10) + 1}, [${addr_reg}, #${offset}]\n`);
19695
- }
19696
- /**
19697
- * strdup the fat string in (x0=ptr, x1=len): preserve the len half across
19698
- * the call and leave the owned copy's pair in (x0, x1). The caller keeps
19699
- * ownership of the original.
19700
- */
19701
- function emit_strdup_string(status) {
19702
- emit_asm(status, `str x1, [sp, #-16]!\n`);
19703
- emit_asm(status, status.audit ? `bl _nomen_strdup_wrap\n` : `bl _strdup\n`);
19704
- emit_asm(status, `ldr x1, [sp], #16\n`);
19705
- }
19706
- /**
19707
- * Pair load/store at [x29, #offset] with an arbitrary register pair —
19708
- * splits into two single-word accesses beyond the ldp/stp +504 range.
19709
- */
19710
- function emit_pair_load_x29(status, offset, a = "x0", b = "x1") {
19711
- if (offset + 8 > 504) {
19712
- emit_asm(status, `ldr ${a}, [x29, #${offset}]\n`);
19713
- emit_asm(status, `ldr ${b}, [x29, #${offset + 8}]\n`);
19714
- return;
19715
- }
19716
- emit_asm(status, `ldp ${a}, ${b}, [x29, #${offset}]\n`);
19717
- }
19718
- function emit_pair_store_x29(status, offset, a = "x0", b = "x1") {
19719
- if (offset + 8 > 504) {
19720
- emit_asm(status, `str ${a}, [x29, #${offset}]\n`);
19721
- emit_asm(status, `str ${b}, [x29, #${offset + 8}]\n`);
19722
- return;
19723
- }
19724
- emit_asm(status, `stp ${a}, ${b}, [x29, #${offset}]\n`);
19725
- }
19726
- /**
19727
- * Owning specializations for Array<string>'s raw T-generic bodies
19728
- * (`with` / `set`), replacing the shared-pointer copies with per-slot deep
19729
- * copies — the aarch64 analog of Array.nm's `#if T_NEEDS_STRDUP` C branch.
19730
- *
19731
- * Conventions (matching Array.nm raw bodies):
19732
- * - `with` (static): x0/x1 = value pair, x2 = count. Returns x0 = heap
19733
- * buffer ([ptr] = length, elements at +8..).
19734
- * - `set` (method): x19 = first element, x1 = index, x2/x3 = value pair.
19735
- * Bounds are guaranteed by the Nomen-level constraints.
19736
- */
19737
- function emit_owning_array_string_specialize(func_name, status, self_reg = "x19") {
19738
- if (func_name === "with") {
19739
- emit_asm(status, `stp x19, x20, [sp, #-16]!\n`);
19740
- emit_asm(status, `stp x21, x22, [sp, #-16]!\n`);
19741
- emit_asm(status, `stp x24, x25, [sp, #-16]!\n`);
19742
- emit_asm(status, `mov x19, x0\n`);
19743
- emit_asm(status, `mov x20, x1\n`);
19744
- emit_asm(status, `mov x21, x2\n`);
19745
- emit_asm(status, `add x0, x21, #1\n`);
19746
- emit_asm(status, `lsl x0, x0, #4\n`);
19747
- emit_malloc(status);
19748
- emit_asm(status, `mov x22, x0\n`);
19749
- emit_asm(status, `str x21, [x22]\n`);
19750
- emit_asm(status, `mov x23, #8\n`);
19751
- emit_asm(status, `mov x24, #0\n`);
19752
- emit_asm(status, `.Larr_str_with_loop:\n`);
19753
- emit_asm(status, `cmp x24, x21\n`);
19754
- emit_asm(status, `b.ge .Larr_str_with_done\n`);
19755
- emit_asm(status, `mov x0, x19\n`);
19756
- emit_strdup(status);
19757
- emit_asm(status, `add x25, x22, x23\n`);
19758
- emit_asm(status, `stp x0, x20, [x25]\n`);
19759
- emit_asm(status, `add x23, x23, #16\n`);
19760
- emit_asm(status, `add x24, x24, #1\n`);
19761
- emit_asm(status, `b .Larr_str_with_loop\n`);
19762
- emit_asm(status, `.Larr_str_with_done:\n`);
19763
- emit_asm(status, `mov x0, x22\n`);
19764
- emit_asm(status, `ldp x24, x25, [sp], #16\n`);
19765
- emit_asm(status, `ldp x21, x22, [sp], #16\n`);
19766
- emit_asm(status, `ldp x19, x20, [sp], #16\n`);
19767
- return true;
19768
- }
19769
- if (func_name === "at" || func_name === "first" || func_name === "at_end") {
19770
- emit_asm(status, `stp x20, x21, [sp, #-16]!\n`);
19771
- if (func_name === "at") {
19772
- emit_asm(status, `lsl x9, x1, #4\n`);
19773
- emit_asm(status, `add x9, ${self_reg}, x9\n`);
19774
- } else if (func_name === "at_end") {
19775
- emit_asm(status, `ldr x9, [${self_reg}, #-8]\n`);
19776
- emit_asm(status, `sub x9, x9, #1\n`);
19777
- emit_asm(status, `lsl x9, x9, #4\n`);
19778
- emit_asm(status, `add x9, ${self_reg}, x9\n`);
19779
- } else emit_asm(status, `add x9, ${self_reg}, #0\n`);
19780
- emit_asm(status, `ldp x0, x1, [x9]\n`);
19781
- emit_asm(status, `ldp x20, x21, [sp], #16\n`);
19782
- return true;
19783
- }
19784
- if (func_name === "set") {
19785
- emit_asm(status, `stp x20, x21, [sp, #-16]!\n`);
19786
- emit_asm(status, `lsl x9, x1, #4\n`);
19787
- emit_asm(status, `add x9, ${self_reg}, x9\n`);
19788
- emit_asm(status, `ldr x0, [x9]\n`);
19789
- emit_asm(status, `mov x20, x2\n`);
19790
- emit_asm(status, `mov x21, x3\n`);
19791
- emit_free(status);
19792
- emit_asm(status, `mov x0, x20\n`);
19793
- emit_strdup(status);
19794
- emit_asm(status, `str x0, [x9]\n`);
19795
- emit_asm(status, `str x21, [x9, #8]\n`);
19796
- emit_asm(status, `ldp x20, x21, [sp], #16\n`);
19797
- return true;
19798
- }
19799
- return false;
19800
- }
19801
- /** Pair store to [base_reg, #offset] with stur fallback (misaligned/far). */
19802
- function emit_pair_store_to(status, base_reg, offset, a, b) {
19803
- if (offset % 8 === 0 && offset + 8 <= 504 && offset >= 0) emit_asm(status, `stp ${a}, ${b}, [${base_reg}, #${offset}]\n`);
19804
- else {
19805
- emit_asm(status, `stur ${a}, [${base_reg}, #${offset}]\n`);
19806
- emit_asm(status, `stur ${b}, [${base_reg}, #${offset + 8}]\n`);
19807
- }
19808
- }
19809
- //#endregion
19810
19879
  //#region ../src/build_aarch64/build_struct_node.ts
19811
19880
  let field_strdup_guard_counter = 0;
19812
19881
  function emit_typed_store(status, src_reg, dst_base, offset, size) {
@@ -21327,7 +21396,7 @@ function build_block_node$1(node, status) {
21327
21396
  heap_slots: [],
21328
21397
  struct_decls: []
21329
21398
  });
21330
- const declarations_before = status.scoped_declarations.length;
21399
+ const declarations_before = node.node_type === "func" ? 0 : status.scoped_declarations.length;
21331
21400
  for (let child of node.statements) if (is_struct_node(child)) {
21332
21401
  if (!should_emit_definition(child, status.emit_mode, status.structs, status.system_struct_names)) continue;
21333
21402
  build_struct_node$1(child, status);
@@ -21421,6 +21490,130 @@ function call_in_set(set, call) {
21421
21490
  return set.has(call.name.replace(/#/g, ""));
21422
21491
  }
21423
21492
  //#endregion
21493
+ //#region ../src/build_common/call_init_string_fields.ts
21494
+ /**
21495
+ * Whether a declaration-initializer expression is a value the callee's
21496
+ * return-boundary normalization made UNIFORMLY heap-owned, so the fresh
21497
+ * binding records every string field and frees them at scope exit:
21498
+ *
21499
+ * - a plain function call registered as a normalized struct returner
21500
+ * (the registry is populated at the callee's return sites — a callee
21501
+ * built later, or one that returns slot borrows, is not registered and
21502
+ * the binding keeps the pre-existing borrow behavior);
21503
+ * - an owned accessor (`pop`/`move_T`/`copy`) — move-out semantics
21504
+ * transfer slot-owned heap buffers to the binding.
21505
+ *
21506
+ * NOT normalized (never recorded): struct constructions (`Info(...)` — its
21507
+ * `#init` leaves rodata borrows and bypasses the return path) and container
21508
+ * BORROW accessors (`.at`/`.first`/`.slice`/`load` — the slot keeps
21509
+ * ownership; recording would free the container's storage).
21510
+ */
21511
+ function is_normalized_struct_call(value, status) {
21512
+ if (!value) return false;
21513
+ if (value.node_type === "func_call") {
21514
+ const call = value;
21515
+ return !!status.normalized_struct_returners?.has(call.name);
21516
+ }
21517
+ if (value.node_type === "access") {
21518
+ const access = value.access;
21519
+ if (access?.node_type !== "access_func") return false;
21520
+ const name = access.name ?? "";
21521
+ if (is_container_borrow_accessor_name(name)) return false;
21522
+ if (status.normalized_struct_returners?.has(name)) return true;
21523
+ const target = value.target;
21524
+ const receiver_type = target ? target.type ?? void 0 : void 0;
21525
+ if (receiver_type?.name) {
21526
+ const label = `${mono_type_name(receiver_type)}_${name}`;
21527
+ if (status.normalized_struct_returners?.has(label)) return true;
21528
+ }
21529
+ return false;
21530
+ }
21531
+ return false;
21532
+ }
21533
+ /**
21534
+ * Record every direct string field of a call-initialized value-struct
21535
+ * binding (`var Info i = make()`) as heap-owned: the callee's return
21536
+ * normalization guarantees each field is a heap buffer, so scope-exit
21537
+ * auto_free must free them, and a later `i.field = …` write sees
21538
+ * old_was_heap and reclaims the displaced buffer instead of leaking it.
21539
+ * No-op for non-struct, class, view, and array types, for structs without
21540
+ * direct string fields, and for non-call initializers.
21541
+ */
21542
+ function record_call_init_string_fields(node, status) {
21543
+ if (!is_normalized_struct_call(node.value, status)) return;
21544
+ const type = node.type;
21545
+ if (!type?.name) return;
21546
+ const mono_name = mono_type_name(type);
21547
+ if (mono_name.startsWith("_Tuple_")) return;
21548
+ const struct = status.structs.find((s) => s.name === mono_name && !s.is_simple_type);
21549
+ if (!struct || struct.is_class) return;
21550
+ const fields = direct_string_fields(struct);
21551
+ if (!fields.length) return;
21552
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
21553
+ for (const field of fields) status.heap_string_fields.add(`${node.name}.${field.name}`);
21554
+ }
21555
+ //#endregion
21556
+ //#region ../src/build_common/fold_string_const.ts
21557
+ /**
21558
+ * Fold a top-level `const`'s string `+` chain into a single string literal.
21559
+ *
21560
+ * Module-level `const` declarations are inlined at every use site (see
21561
+ * `top_level_consts` in both backends' BuildStatus): the build replays the
21562
+ * const's initializer expression wherever the name appears. For a composed
21563
+ * pattern like `pub const HTML_TAG_REGEX = "^(" + TAG_NAME + "|…)"` that
21564
+ * replay lowers to a malloc/copy/free `string_add` chain PER USE — the
21565
+ * allmark port's inline-token loop rebuilt its ~1 KB pattern on every
21566
+ * attempt, and the chain's calls were also the trigger for the aarch64
21567
+ * ref-argument clobber (PORT.md).
21568
+ *
21569
+ * When the initializer is a pure `+` chain of plain string literals and
21570
+ * references to other foldable consts, this returns one merged literal
21571
+ * ValueNode — semantically the string the source spells — that the use site
21572
+ * emits as rodata instead. Anything else (a runtime operand, a struct ctor,
21573
+ * an operator overload) returns null and the tree builds as before.
21574
+ *
21575
+ * Raw token merging is byte-preserving: escape sequences are self-contained
21576
+ * within a part's content (a content ending in a lone `\` would have
21577
+ * escaped that part's closing quote, so it cannot occur), and both
21578
+ * emitters' `\x`-as-octal re-encoding plus the 2/3-digit caps keep merged
21579
+ * escapes from absorbing a following part's text.
21580
+ */
21581
+ const fold_cache = /* @__PURE__ */ new WeakMap();
21582
+ function fold_string_const(decl, consts) {
21583
+ if (fold_cache.has(decl)) return fold_cache.get(decl);
21584
+ const folded = fold_value(decl.value, consts, /* @__PURE__ */ new Set());
21585
+ fold_cache.set(decl, folded);
21586
+ return folded;
21587
+ }
21588
+ function fold_value(node, consts, active) {
21589
+ if (!node) return null;
21590
+ if (node.node_type === "value") {
21591
+ const raw = node.value;
21592
+ if (typeof raw !== "string") return null;
21593
+ if (raw.startsWith("\"")) return literal(raw, node.start);
21594
+ if (active.has(raw)) return null;
21595
+ const ref = consts.get(raw);
21596
+ if (!ref) return null;
21597
+ active.add(raw);
21598
+ const folded = fold_value(ref.value, consts, active);
21599
+ active.delete(raw);
21600
+ return folded;
21601
+ }
21602
+ if (node.node_type === "op") {
21603
+ const op = node;
21604
+ if (!(op.op === "+" && (!op.operator_func || op.operator_func.struct_name === "string"))) return null;
21605
+ const left = fold_value(op.left_value, consts, active);
21606
+ if (!left) return null;
21607
+ const right = fold_value(op.right_value, consts, active);
21608
+ if (!right) return null;
21609
+ return literal(left.value.slice(0, -1) + right.value.slice(1), node.start);
21610
+ }
21611
+ return null;
21612
+ }
21613
+ function literal(raw, start) {
21614
+ return new ValueNode(start, raw, new Type("string", true));
21615
+ }
21616
+ //#endregion
21424
21617
  //#region ../src/build_common/string_escapes.ts
21425
21618
  /**
21426
21619
  * Source string-literal escape handling shared by both backends.
@@ -21455,8 +21648,8 @@ function reencode_hex_escapes(raw) {
21455
21648
  let j = i + 2;
21456
21649
  let value = 0;
21457
21650
  let digits = 0;
21458
- while (j < raw.length && digits < 2 && is_hex_digit(raw[j])) {
21459
- value = value * 16 + hex_value(raw[j]);
21651
+ while (j < raw.length && digits < 2 && is_hex_digit$1(raw[j])) {
21652
+ value = value * 16 + hex_value$1(raw[j]);
21460
21653
  j += 1;
21461
21654
  digits += 1;
21462
21655
  }
@@ -21471,10 +21664,10 @@ function reencode_hex_escapes(raw) {
21471
21664
  }
21472
21665
  return out;
21473
21666
  }
21474
- function is_hex_digit(c) {
21667
+ function is_hex_digit$1(c) {
21475
21668
  return c >= "0" && c <= "9" || c >= "a" && c <= "f" || c >= "A" && c <= "F";
21476
21669
  }
21477
- function hex_value(c) {
21670
+ function hex_value$1(c) {
21478
21671
  if (c <= "9") return c.charCodeAt(0) - 48;
21479
21672
  if (c <= "F") return c.charCodeAt(0) - 55;
21480
21673
  return c.charCodeAt(0) - 87;
@@ -21504,7 +21697,7 @@ function scan_string_escapes(raw) {
21504
21697
  if (next === "x") {
21505
21698
  let j = i + 2;
21506
21699
  let digits = 0;
21507
- while (j < end && digits < 2 && is_hex_digit(raw[j])) {
21700
+ while (j < end && digits < 2 && is_hex_digit$1(raw[j])) {
21508
21701
  j += 1;
21509
21702
  digits += 1;
21510
21703
  }
@@ -21539,6 +21732,16 @@ function scan_string_escapes(raw) {
21539
21732
  }
21540
21733
  return issues;
21541
21734
  }
21735
+ /** Escape one raw string-literal token (quotes included) for a GAS
21736
+ * `.asciz` directive: source `\xHH` hex escapes re-encode as 3-digit octal
21737
+ * (GAS consumes `\x` greedily — see reencode_hex_escapes) and raw newlines
21738
+ * (multi-line strings) become `\n` so the directive stays one line. */
21739
+ function escape_asciz(value) {
21740
+ const reencoded = reencode_hex_escapes(value);
21741
+ if (!reencoded.includes("\n")) return reencoded;
21742
+ const quote = reencoded[0];
21743
+ return quote + reencoded.slice(1, reencoded.endsWith(quote) ? -1 : void 0).replace(/\n/g, "\\n") + (reencoded.endsWith(quote) ? quote : "");
21744
+ }
21542
21745
  //#endregion
21543
21746
  //#region ../src/build_aarch64/build_range_node.ts
21544
21747
  function build_range_node$1(node, status) {
@@ -21752,12 +21955,6 @@ function is_value_struct_type_node(node, status) {
21752
21955
  const t = type_from_value_node$1(node);
21753
21956
  return !!t?.name && !!status.structs.find((s) => s.name === t.name && !s.is_simple_type && !s.is_class);
21754
21957
  }
21755
- function escape_asciz(value) {
21756
- const reencoded = reencode_hex_escapes(value);
21757
- if (!reencoded.includes("\n")) return reencoded;
21758
- const quote = reencoded[0];
21759
- return quote + reencoded.slice(1, reencoded.endsWith(quote) ? -1 : void 0).replace(/\n/g, "\\n") + (reencoded.endsWith(quote) ? quote : "");
21760
- }
21761
21958
  /** Allocate an array on the stack with an 8-byte length prefix.
21762
21959
  * Writes the length at the start and returns the offset of the first element. */
21763
21960
  function alloc_array_with_prefix(status, length, element_size) {
@@ -22227,6 +22424,93 @@ function resolve_string_op(op, status) {
22227
22424
  }
22228
22425
  return null;
22229
22426
  }
22427
+ const FLOAT_LITERAL_RE = /^(\+|-)?\d+\.\d+([eE](\+|-)?\d+)?$/;
22428
+ /**
22429
+ * Fold a module-level primitive `const`/`var` initializer to a literal string
22430
+ * when it is a compile-time constant: literal operands, references to other
22431
+ * foldable module-level consts, and arithmetic/bitwise operators over them.
22432
+ * Returns null when the value can only be computed at runtime (a call, a
22433
+ * field read, …), which a file-scope `.quad` initializer cannot express.
22434
+ *
22435
+ * Without this, `pub const N = 3 + 4` at module scope reserved
22436
+ * `N: .space 8` and emitted its store as dead code between functions, so
22437
+ * every read observed 0 (FOLLOWUP.md). Integer math is exact via BigInt so
22438
+ * 64-bit constants are not rounded through JS doubles; float arithmetic
22439
+ * falls back to JS doubles (which match Nomen's `float`).
22440
+ */
22441
+ function fold_const_scalar(node, status, seen) {
22442
+ if (!node) return null;
22443
+ if (node.node_type === "grouped") return fold_const_scalar(node.value, status, seen);
22444
+ if (node.node_type === "value") {
22445
+ const raw = get_raw_value$1(node, status);
22446
+ if (is_int_literal(raw)) return to_decimal_string(raw);
22447
+ if (FLOAT_LITERAL_RE.test(raw)) return raw;
22448
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(raw) && !seen.has(raw)) {
22449
+ const decl = find_global_const(raw, status);
22450
+ if (decl?.value) {
22451
+ seen.add(raw);
22452
+ const folded = fold_const_scalar(decl.value, status, seen);
22453
+ seen.delete(raw);
22454
+ return folded;
22455
+ }
22456
+ }
22457
+ return null;
22458
+ }
22459
+ if (node.node_type === "op") {
22460
+ const op = node;
22461
+ if (op.operator_func) return null;
22462
+ const left = fold_const_scalar(op.left_value, status, seen);
22463
+ if (left === null) return null;
22464
+ const right = fold_const_scalar(op.right_value, status, seen);
22465
+ if (right === null) return null;
22466
+ return apply_const_op(op.op, left, right);
22467
+ }
22468
+ return null;
22469
+ }
22470
+ /** Apply one arithmetic/bitwise operator to two folded literal strings.
22471
+ * Float operands use JS doubles; integral operands stay exact via BigInt. */
22472
+ function apply_const_op(op, left, right) {
22473
+ if (FLOAT_LITERAL_RE.test(left) || FLOAT_LITERAL_RE.test(right)) {
22474
+ const a = Number(left);
22475
+ const b = Number(right);
22476
+ if (!isFinite(a) || !isFinite(b)) return null;
22477
+ switch (op) {
22478
+ case "+": return String(a + b);
22479
+ case "-": return String(a - b);
22480
+ case "*": return String(a * b);
22481
+ case "/": return b === 0 ? null : String(a / b);
22482
+ default: return null;
22483
+ }
22484
+ }
22485
+ let a;
22486
+ let b;
22487
+ try {
22488
+ a = BigInt(left);
22489
+ b = BigInt(right);
22490
+ } catch {
22491
+ return null;
22492
+ }
22493
+ switch (op) {
22494
+ case "+": return (a + b).toString();
22495
+ case "-": return (a - b).toString();
22496
+ case "*": return (a * b).toString();
22497
+ case "/": return b === 0n ? null : (a / b).toString();
22498
+ case "%": return b === 0n ? null : (a % b).toString();
22499
+ case "<<": return (a << b).toString();
22500
+ case ">>": return (a >> b).toString();
22501
+ case "&": return (a & b).toString();
22502
+ case "|": return (a | b).toString();
22503
+ case "^": return (a ^ b).toString();
22504
+ default: return null;
22505
+ }
22506
+ }
22507
+ /** The root-scope `const` declaration named `name` (used to fold a const
22508
+ * reference through to its initializer). Mutable `var`s are deliberately
22509
+ * excluded: their value is established by module init, so baking in the
22510
+ * declaration's initializer could differ from the runtime value. */
22511
+ function find_global_const(name, status) {
22512
+ return (status.root.statements ?? []).find((s) => s.node_type === "declare" && s.declaration === "const" && s.name === name);
22513
+ }
22230
22514
  /**
22231
22515
  * Emit a declaration INITIALIZER expression. Under NIR-driven emission the
22232
22516
  * lowered `NirExpr` rides in and descends through `emit_expr_from_nir` (the
@@ -22258,7 +22542,7 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
22258
22542
  const prev_heap = status.last_result_is_heap;
22259
22543
  if (node.value?.node_type === "value") {
22260
22544
  const inlined = status.top_level_consts?.get(node.value.value);
22261
- if (inlined?.value) node.value = inlined.value;
22545
+ if (inlined?.value) node.value = fold_string_const(inlined, status.top_level_consts) ?? inlined.value;
22262
22546
  }
22263
22547
  function check_heap() {
22264
22548
  if (status.last_result_is_heap) {
@@ -22402,12 +22686,15 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
22402
22686
  build_swap_params(func_call, status);
22403
22687
  } else {
22404
22688
  const old_buffer = status.struct_return_buffer;
22689
+ const old_buffer_var = status.struct_return_buffer_var;
22405
22690
  const old_preset = status.call_x8_preset;
22406
22691
  emit_var_address(status, "x8", node.name);
22692
+ status.struct_return_buffer_var = node.name;
22407
22693
  status.struct_return_buffer = "x8";
22408
22694
  status.call_x8_preset = true;
22409
22695
  emit_init_value$1(node.value, nir_init, status);
22410
22696
  status.struct_return_buffer = old_buffer;
22697
+ status.struct_return_buffer_var = old_buffer_var;
22411
22698
  status.call_x8_preset = old_preset;
22412
22699
  }
22413
22700
  return;
@@ -22933,14 +23220,18 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
22933
23220
  build_swap_params(func_call, status);
22934
23221
  } else if (status.structs.find((s) => s.name === (func_call.type?.name ?? func_call.name) && !s.is_simple_type && !s.is_class) && status.function_return_label) {
22935
23222
  const old_buffer = status.struct_return_buffer;
23223
+ const old_buffer_var = status.struct_return_buffer_var;
22936
23224
  const old_preset = status.call_x8_preset;
22937
23225
  emit_var_address(status, "x8", node.name);
23226
+ status.struct_return_buffer_var = node.name;
22938
23227
  status.struct_return_buffer = "x8";
22939
23228
  status.call_x8_preset = true;
22940
23229
  emit_init_value$1(node.value, nir_init, status);
22941
23230
  status.struct_return_buffer = old_buffer;
23231
+ status.struct_return_buffer_var = old_buffer_var;
22942
23232
  status.call_x8_preset = old_preset;
22943
23233
  emit_var_address(status, "x0", node.name);
23234
+ record_call_init_string_fields(node, status);
22944
23235
  } else {
22945
23236
  emit_init_value$1(node.value, nir_init, status);
22946
23237
  emit_var_store(status, "x0", node.name, struct_size);
@@ -22976,6 +23267,7 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
22976
23267
  const field_type = (target_type.name ? status.structs.find((s) => s.name === target_type.name) : void 0)?.fields.find((f) => f.name === field_name)?.type ?? type_from_value_node$1(access.access);
22977
23268
  emit_struct_copy("x1", "x0", get_field_offset(target_type.name, field_name, status), get_struct_size(field_type.name, status), status);
22978
23269
  }
23270
+ record_call_init_string_fields(node, status);
22979
23271
  }
22980
23272
  }
22981
23273
  } else if (node.value) {
@@ -23200,7 +23492,15 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23200
23492
  if (status.function_return_label) {
23201
23493
  const offset = declare_slot_offset(status, node.name, size);
23202
23494
  status.stack_offsets.set(node.name, offset);
23203
- } else emit_data(status, `${node.name}: .space ${size}\n`);
23495
+ } else {
23496
+ const folded = fold_const_scalar(node.value, status, /* @__PURE__ */ new Set());
23497
+ if (folded !== null) {
23498
+ emit_data(status, `${node.name}: ${directive} ${folded}\n`);
23499
+ if (size % 4 !== 0) emit_data(status, `.p2align 2\n`);
23500
+ return;
23501
+ }
23502
+ emit_data(status, `${node.name}: .space ${size}\n`);
23503
+ }
23204
23504
  if (node.type.name === "string" && !node.type.is_view && is_view_value(node.value, status)) {
23205
23505
  emit_view_string_arg(node.value, status);
23206
23506
  emit_view_materialize_owned(status);
@@ -24807,6 +25107,18 @@ function build_return_node$1(node, status, nir_value) {
24807
25107
  const field_off = get_field_offset(ret_struct.name, field.name, status);
24808
25108
  emit_enum_payload_strdups_at(status, field_enum.name, "x8", field_off);
24809
25109
  }
25110
+ if (ret_struct && !returns_nullable_struct) {
25111
+ const returned_var = node.value.node_type === "value" ? node.value.value : "";
25112
+ if (returned_var !== "" && returned_var !== "0" && returned_var !== "null" && all_scope_frames(status).flat().some((d) => d.name === returned_var)) {
25113
+ let skip_fields;
25114
+ const prefix = `${node.value.value}.`;
25115
+ for (const key of status.heap_string_fields ?? []) if (key.startsWith(prefix)) {
25116
+ skip_fields ??= /* @__PURE__ */ new Set();
25117
+ skip_fields.add(key.slice(prefix.length));
25118
+ }
25119
+ emit_string_field_strdups_at(status, ret_struct.name, "x8", 0, skip_fields);
25120
+ }
25121
+ }
24810
25122
  }
24811
25123
  }
24812
25124
  if (status.return_assign) {
@@ -28436,8 +28748,39 @@ function build_function_call_node$1(node, status) {
28436
28748
  emit_asm(status, `str x9, [x29, #${off + struct_size}]\n`);
28437
28749
  emit_asm(status, `add x0, x29, #${off}\n`);
28438
28750
  }
28439
- } else if (is_struct_type$3(param_type, status) || is_enum_with_data_type$2(param_type, status)) emit_struct_address(node.params[i], status);
28440
- else if (arg_deferrable(i)) {
28751
+ } else if (is_struct_type$3(param_type, status) || is_enum_with_data_type$2(param_type, status)) {
28752
+ if (node.move_owned_param_indices?.includes(i) && node.params[i].node_type === "value") {
28753
+ const transfer_struct = status.structs.find((st) => st.name === resolve_struct_name(param_type, param.type?.type_args, status) && !st.is_simple_type && !st.is_class && !st.is_generic);
28754
+ const transfer_var = node.params[i].value;
28755
+ if (transfer_struct && direct_string_fields(transfer_struct).length > 0 && transfer_var) {
28756
+ const recorded = /* @__PURE__ */ new Set();
28757
+ for (const key of status.heap_string_fields ?? []) if (key.startsWith(`${transfer_var}.`)) recorded.add(key.slice(transfer_var.length + 1));
28758
+ if (direct_string_fields(transfer_struct).filter((f) => !recorded.has(f.name)).length > 0) {
28759
+ emit_struct_address(node.params[i], status);
28760
+ ensure_newline(status);
28761
+ emit_string_field_strdups_at(status, transfer_struct.name, "x0", 0, recorded);
28762
+ if (status.call_x8_preset && status.struct_return_buffer_var) emit_var_address(status, "x8", status.struct_return_buffer_var);
28763
+ }
28764
+ }
28765
+ }
28766
+ const arg_struct = status.structs.find((st) => st.name === resolve_struct_name(param_type, param.type?.type_args, status) && !st.is_simple_type && !st.is_class && !st.is_generic);
28767
+ if (node.owned_value_param_indices?.includes(i) && arg_struct && !(arg_struct.traits ?? []).length && direct_string_fields(arg_struct).length > 0 && status.normalized_struct_returners?.has(func_name) && !status.func_address_taken?.has(func_name)) {
28768
+ const mono_name = arg_struct.name;
28769
+ const struct_size = get_struct_size(mono_name, status);
28770
+ const off = allocate_stack_space(status, struct_size);
28771
+ emit_struct_address(node.params[i], status);
28772
+ ensure_newline(status);
28773
+ emit_asm(status, `mov x1, x0\n`);
28774
+ emit_asm(status, `add x0, x29, #${off}\n`);
28775
+ for (let b = 0; b < struct_size; b += 8) {
28776
+ emit_asm(status, `ldr x2, [x1, #${b}]\n`);
28777
+ emit_asm(status, `str x2, [x0, #${b}]\n`);
28778
+ }
28779
+ emit_asm(status, `add x0, x29, #${off}\n`);
28780
+ emit_string_field_strdups_at(status, mono_name, "x0");
28781
+ if (status.call_x8_preset && status.struct_return_buffer_var) emit_var_address(status, "x8", status.struct_return_buffer_var);
28782
+ } else emit_struct_address(node.params[i], status);
28783
+ } else if (arg_deferrable(i)) {
28441
28784
  deferred_args.push({
28442
28785
  param: node.params[i],
28443
28786
  slot: arg_slot[i]
@@ -28569,6 +28912,7 @@ function build_function_call_node$1(node, status) {
28569
28912
  const tname = all_scope_frames(status).flat().find((d) => d.name === vname)?.type?.name ?? param.type?.name;
28570
28913
  if (tname === "string") continue;
28571
28914
  if (tname && status.enums.find((e) => e.name === tname && e.has_associated_data)) continue;
28915
+ if (vname !== void 0 && (node.move_owned_param_indices?.includes(idx) ?? false)) clear_heap_string_fields(status, vname);
28572
28916
  }
28573
28917
  if (param) mark_moved_if_struct(param, status);
28574
28918
  }
@@ -30897,11 +31241,23 @@ function free_scoped_declarations(status, decls, persist_string_field_records =
30897
31241
  commented = true;
30898
31242
  }
30899
31243
  const dot = key.indexOf(".");
30900
- status.code += `free(${c_function_name(key.substring(0, dot))}${key.substring(dot)}.ptr);\n`;
31244
+ const base = c_function_name(key.substring(0, dot));
31245
+ const arrow = dec.string_fields_via_pointer ? "->" : ".";
31246
+ const field = key.substring(dot + 1);
31247
+ const free_fn = status.audit ? "nomen_free_wrap" : "free";
31248
+ status.code += `${free_fn}(${base}${arrow}${field}.ptr);\n`;
30901
31249
  if (!persist_string_field_records) status.heap_string_fields.delete(key);
30902
31250
  }
30903
31251
  }
30904
31252
  for (const dec of decls) {
31253
+ if (dec.owned_heap_shell) {
31254
+ if (!commented) {
31255
+ status.code += "\n// Auto-free\n";
31256
+ commented = true;
31257
+ }
31258
+ status.code += `${status.audit ? "nomen_free_wrap" : "free"}(${c_function_name(dec.name)});\n`;
31259
+ continue;
31260
+ }
30905
31261
  if (persist_string_field_records && dec.force_owned_string === true) continue;
30906
31262
  if (status.moved?.has(dec.name)) continue;
30907
31263
  const cname = c_function_name(dec.name);
@@ -31499,7 +31855,8 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
31499
31855
  status.init_assigned_fields.add(tracked_key);
31500
31856
  }
31501
31857
  const old_was_heap = !!target_struct?.is_class && !first_init_write || !!status.heap_string_fields?.has(tracked_key);
31502
- if (target_struct && target_var && (!self_target || target_struct.is_class)) {
31858
+ const is_class_target = !!target_struct?.is_class;
31859
+ if (target_struct && (target_var || is_class_target) && (!self_target || is_class_target)) {
31503
31860
  const fresh_heap = is_owned_heap_temp$1(node.right_value, status);
31504
31861
  const rhs_is_null_value = node.right_value.node_type === "value" && node.right_value.value === "null";
31505
31862
  const saved_len = begin_code_scratch(status);
@@ -32581,6 +32938,7 @@ function build_declaration_node(node, status, nir_init, nir_swap) {
32581
32938
  emit_swap_value$1(node.swap, nir_swap, status);
32582
32939
  }
32583
32940
  if (has_field_overrides(node.value)) emit_field_overrides(safe_name, node.value, build_node$1, status, ";\n", ";\n");
32941
+ record_call_init_string_fields(node, status);
32584
32942
  }
32585
32943
  }
32586
32944
  }
@@ -33096,6 +33454,7 @@ function build_return_node(node, status, nir_value) {
33096
33454
  }
33097
33455
  }
33098
33456
  let returned_value_decl;
33457
+ let returned_recorded_string_fields;
33099
33458
  if (node.value.node_type === "value") {
33100
33459
  const value = node.value.value;
33101
33460
  returned_value_decl = find_decl_across_scopes(value, status);
@@ -33109,7 +33468,11 @@ function build_return_node(node, status, nir_value) {
33109
33468
  }
33110
33469
  if (status.heap_string_fields?.size) {
33111
33470
  const prefix = `${value}.`;
33112
- for (const key of Array.from(status.heap_string_fields)) if (key.startsWith(prefix)) status.heap_string_fields.delete(key);
33471
+ for (const key of Array.from(status.heap_string_fields)) if (key.startsWith(prefix)) {
33472
+ returned_recorded_string_fields ??= /* @__PURE__ */ new Set();
33473
+ returned_recorded_string_fields.add(key.slice(prefix.length));
33474
+ status.heap_string_fields.delete(key);
33475
+ }
33113
33476
  }
33114
33477
  }
33115
33478
  if (ret_type?.is_array && return_array_var && return_array_len > 0) {
@@ -33218,6 +33581,11 @@ function build_return_node(node, status, nir_value) {
33218
33581
  status.code += `;\n`;
33219
33582
  if (node.value.node_type === "access" && node.value.access?.node_type === "access_field" && !returns_string_zero && !returns_struct_zero && !returns_view_value && ret_type.name !== "string" && !ret_type.is_view && !ret_type.is_array && status.enums.find((e) => e.name === ret_type.name && e.has_associated_data)) status.code += `_return_val = ${ret_type.name}_copy(_return_val);\n`;
33220
33583
  if (has_field_overrides(node.value)) emit_field_overrides("_return_val", node.value, build_node$1, status, "", ";\n");
33584
+ const bare_local_return = node.value.node_type === "value" && node.value.value !== "0" && node.value.value !== "null" && !!returned_value_decl;
33585
+ if (is_struct && !return_is_class && !ret_type.is_view && !ret_type.is_array && return_struct && !returns_struct_zero && bare_local_return) for (const field of direct_string_fields(return_struct)) {
33586
+ if (returned_recorded_string_fields?.has(field.name)) continue;
33587
+ status.code += `_return_val.${field.name} = nomen_str_dup(_return_val.${field.name});\n`;
33588
+ }
33221
33589
  emit_nursery_joins_on_return_c(status);
33222
33590
  reclaim_all_c_scopes(status);
33223
33591
  status.code += `return _return_val;\n`;
@@ -35168,6 +35536,18 @@ function build_function_node(node, status) {
35168
35536
  if (param_trait && !param_struct?.is_class) decl.trait_class_trait = param.type.name;
35169
35537
  status.scoped_declarations.push(decl);
35170
35538
  }
35539
+ const is_free_function = (!node.scope || node.scope.node_type === "root" || node.scope.node_type === "func") && !node.params.some((p) => p.is_self_param);
35540
+ const fn_name = node.name;
35541
+ const fn_label = emission_label(node);
35542
+ const owned_value_struct = is_free_function && !node.address_escaped && !param.is_self_param && !param.is_variadic && !param.type.is_ref && !param.type.is_nullable && !param.type.is_view && !param.type.is_array && param.type.name ? status.structs.find((st) => st.name === param.type.name && !st.is_simple_type && !st.is_class) : void 0;
35543
+ if (owned_value_struct && !(owned_value_struct.traits ?? []).length && direct_string_fields(owned_value_struct).length > 0 && !struct_needs_destroy(owned_value_struct, status) && (status.normalized_struct_returners?.has(fn_name) || status.normalized_struct_returners?.has(fn_label)) && !status.func_address_taken?.has(fn_name) && !status.func_address_taken?.has(fn_label)) {
35544
+ const decl = new DeclarationNode(param.start, "private", "var", pname, param.type);
35545
+ decl.string_fields_via_pointer = true;
35546
+ decl.owned_heap_shell = !param.is_moved;
35547
+ status.scoped_declarations.push(decl);
35548
+ if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
35549
+ for (const field of direct_string_fields(owned_value_struct)) status.heap_string_fields.add(`${pname}.${field.name}`);
35550
+ }
35171
35551
  }
35172
35552
  }
35173
35553
  const nir = node.statements?.length ? lower_function(node) : void 0;
@@ -36408,6 +36788,27 @@ function build_function_call_node(node, status) {
36408
36788
  continue;
36409
36789
  }
36410
36790
  const param_type = type_from_value_node$1(node.params[i]);
36791
+ if (node.move_owned_param_indices?.includes(i) && node.params[i].node_type === "value") {
36792
+ const transfer_struct = status.structs.find((st) => st.name === mono_type_name(param_type) && !st.is_simple_type && !st.is_class && !st.is_generic);
36793
+ const transfer_var = node.params[i].value;
36794
+ if (transfer_struct && direct_string_fields(transfer_struct).length > 0 && transfer_var) {
36795
+ const recorded = /* @__PURE__ */ new Set();
36796
+ for (const key of status.heap_string_fields ?? []) if (key.startsWith(`${transfer_var}.`)) recorded.add(key.slice(transfer_var.length + 1));
36797
+ const unrecorded = direct_string_fields(transfer_struct).filter((f) => !recorded.has(f.name));
36798
+ if (unrecorded.length > 0) {
36799
+ const saved_arg = begin_code_scratch(status);
36800
+ status.code += `(void *)&`;
36801
+ status.suppress_dereference = true;
36802
+ build_node$1(node.params[i], status);
36803
+ status.suppress_dereference = false;
36804
+ const addr_text = end_code_scratch(status, saved_arg);
36805
+ status.code += `(void *)({ struct ${transfer_struct.name} *_t = (struct ${transfer_struct.name} *)${addr_text}; `;
36806
+ for (const f of unrecorded) status.code += `_t->${f.name} = nomen_str_dup(_t->${f.name}); `;
36807
+ status.code += `(void *)_t; })`;
36808
+ continue;
36809
+ }
36810
+ }
36811
+ }
36411
36812
  if (node.nullable_param_indices?.includes(i) && node.params[i].node_type === "value" && node.params[i].value === "null") {
36412
36813
  status.code += `(void *)&(struct ${param_type.name}){0}, 0`;
36413
36814
  continue;
@@ -36454,6 +36855,23 @@ function build_function_call_node(node, status) {
36454
36855
  status.suppress_dereference = false;
36455
36856
  continue;
36456
36857
  }
36858
+ const arg_struct = status.structs.find((st) => st.name === mono_type_name(param_type) && !st.is_simple_type && !st.is_class && !st.is_generic);
36859
+ if (node.owned_value_param_indices?.includes(i) && !param_is_class && arg_struct && !(arg_struct.traits ?? []).length && direct_string_fields(arg_struct).length > 0 && status.normalized_struct_returners?.has(func_name) && !status.func_address_taken?.has(func_name)) {
36860
+ const struct_name = arg_struct.name;
36861
+ const fields = direct_string_fields(arg_struct);
36862
+ const saved_arg = begin_code_scratch(status);
36863
+ status.code += `(void *)&`;
36864
+ status.suppress_dereference = true;
36865
+ build_node$1(node.params[i], status);
36866
+ status.suppress_dereference = false;
36867
+ const addr_text = end_code_scratch(status, saved_arg);
36868
+ status.code += `(void *)({ struct ${struct_name} *_src = (struct ${struct_name} *)${addr_text}; `;
36869
+ status.code += `struct ${struct_name} *_own = (struct ${struct_name} *)malloc(sizeof(struct ${struct_name})); `;
36870
+ status.code += `*_own = *_src; `;
36871
+ for (const field of fields) status.code += `_own->${field.name} = nomen_str_dup(_own->${field.name}); `;
36872
+ status.code += `_own; })`;
36873
+ continue;
36874
+ }
36457
36875
  if (!param_is_class && !!status.structs.find((s) => s.name === param_type.name && !s.is_simple_type) || !!status.traits.find((t) => t.name === param_type.name) || is_ref_param) {
36458
36876
  if (node.params[i].node_type === "value" && (status.function_ref_params?.has(node.params[i].value) || status.class_vars?.has(node.params[i].value))) status.suppress_dereference = true;
36459
36877
  else if (is_ref_param) status.code += `&`;
@@ -36530,9 +36948,12 @@ function build_function_call_node(node, status) {
36530
36948
  if (decl_hit) decl_hit.frame.splice(decl_hit.index, 1);
36531
36949
  if (is_value_struct) {
36532
36950
  const prefix = `${vname}.`;
36951
+ const transfer = node.move_owned_param_indices?.includes(idx) ?? false;
36533
36952
  for (const key of Array.from(status.heap_string_fields ?? [])) if (key.startsWith(prefix)) {
36534
- if (!status.pending_string_releases) status.pending_string_releases = [];
36535
- status.pending_string_releases.push(`free(${key}.ptr);`);
36953
+ if (!transfer) {
36954
+ if (!status.pending_string_releases) status.pending_string_releases = [];
36955
+ status.pending_string_releases.push(`free(${key}.ptr);`);
36956
+ }
36536
36957
  status.heap_string_fields.delete(key);
36537
36958
  }
36538
36959
  }
@@ -36796,7 +37217,7 @@ function build_value_node$1(node, status) {
36796
37217
  }
36797
37218
  const inlined = status.top_level_consts?.get(value);
36798
37219
  if (inlined?.value) {
36799
- build_node$1(inlined.value, status);
37220
+ build_node$1(fold_string_const(inlined, status.top_level_consts) ?? inlined.value, status);
36800
37221
  return;
36801
37222
  }
36802
37223
  if (node.is_enum_shorthand) {
@@ -37737,7 +38158,7 @@ function build_value_node(node, status) {
37737
38158
  }
37738
38159
  const inlined = status.top_level_consts?.get(original_value);
37739
38160
  if (inlined?.value) {
37740
- build_node(inlined.value, status);
38161
+ build_node(fold_string_const(inlined, status.top_level_consts) ?? inlined.value, status);
37741
38162
  return;
37742
38163
  }
37743
38164
  if (node.is_enum_shorthand) {
@@ -38193,6 +38614,10 @@ function get_base_address(access, status, reg) {
38193
38614
  if (paramReg !== reg) emit_asm(status, `mov ${reg}, ${paramReg}\n`);
38194
38615
  } else if (is_local_ref_var(name, status)) emit_deref_var_address(status, reg, name);
38195
38616
  else emit_var_address(status, reg, name);
38617
+ } else if (access_chain_crosses_class(access, status)) {
38618
+ build_node(access.target, status);
38619
+ ensure_newline(status);
38620
+ if (reg !== "x0") emit_asm(status, `mov ${reg}, x0\n`);
38196
38621
  } else {
38197
38622
  emit_address_of(access.target, status);
38198
38623
  ensure_newline(status);
@@ -41711,6 +42136,9 @@ function emit_address_of(node, status) {
41711
42136
  if (paramReg !== "x0") emit_asm(status, `mov x0, ${paramReg}\n`);
41712
42137
  } else if (is_local_ref_var(name, status)) emit_deref_var_address(status, "x0", name);
41713
42138
  else emit_var_address(status, "x0", name);
42139
+ } else if (access_chain_crosses_class(access, status)) {
42140
+ build_node(access.target, status);
42141
+ ensure_newline(status);
41714
42142
  } else {
41715
42143
  emit_address_of(access.target, status);
41716
42144
  ensure_newline(status);
@@ -42980,8 +43408,6 @@ function build_access_method(node, access_func, status) {
42980
43408
  return true;
42981
43409
  };
42982
43410
  const overflow_count = Math.max(0, total_arg_slots - (8 - start_reg));
42983
- let overflow_base = 0;
42984
- if (overflow_count > 0) overflow_base = allocate_stack_space(status, overflow_count * 8, 16);
42985
43411
  const lambda_arg_indices = /* @__PURE__ */ new Set();
42986
43412
  for (let i = 0; i < access_func.params.length; i++) {
42987
43413
  const p = access_func.params[i];
@@ -42992,21 +43418,17 @@ function build_access_method(node, access_func, status) {
42992
43418
  lambda_arg_indices.add(i);
42993
43419
  }
42994
43420
  const lambda_arg_slots = [];
42995
- const has_pair_args = view_arg_set.size > 0 || string_arg_set.size > 0;
42996
- let view_spill_base = 0;
42997
- if (has_pair_args) view_spill_base = allocate_stack_space(status, total_arg_slots * 8, 16);
42998
- const view_half_store = (j, half) => {
42999
- const half_slot = start_reg + arg_slot[j] + half;
43000
- return half_slot >= 8 ? overflow_base + (half_slot - 8) * 8 : view_spill_base + (arg_slot[j] + half) * 8;
43421
+ const arg_builds_in_loop = (i) => {
43422
+ if (view_arg_set.has(i) || string_arg_set.has(i)) return true;
43423
+ if ((access_func.ref_param_indices ?? []).includes(i)) return true;
43424
+ const param_type = access_func.params[i].type?.name || "";
43425
+ if (is_struct_type(param_type, status) || is_enum_with_data_type(param_type, status)) return true;
43426
+ return !arg_deferrable(i);
43001
43427
  };
43002
- const param0 = access_func.params[0];
43003
- const param0_is_pair = view_arg_set.has(0) || string_arg_set.has(0);
43004
- const param0_is_ref = (access_func.ref_param_indices ?? []).includes(0);
43005
- const param0_type_name = param0?.type?.name || "";
43006
- const param0_builds_in_loop = access_func.params.length > 0 && (param0_is_ref || is_struct_type(param0_type_name, status) || is_enum_with_data_type(param0_type_name, status) || !arg_deferrable(0));
43007
- const static_arg0_needs_spill = start_reg === 0 && access_func.params.length > 0 && !param0_is_pair && param0_builds_in_loop;
43008
- let static_arg0_spill = 0;
43009
- if (static_arg0_needs_spill) static_arg0_spill = allocate_stack_space(status, 8, 8);
43428
+ const any_in_loop_arg = access_func.params.some((_, i) => arg_builds_in_loop(i));
43429
+ let arg_spill_base = 0;
43430
+ if (any_in_loop_arg) arg_spill_base = allocate_stack_space(status, total_arg_slots * 8, 16);
43431
+ const view_half_store = (j, half) => arg_spill_base + (arg_slot[j] + half) * 8;
43010
43432
  const ref_class_param_reload = [];
43011
43433
  const ref_class_sync_names = [];
43012
43434
  for (let i = access_func.params.length - 1; i >= 0; i--) {
@@ -43076,31 +43498,20 @@ function build_access_method(node, access_func, status) {
43076
43498
  lambda_arg_slots.push(dispose_slot);
43077
43499
  }
43078
43500
  }
43079
- const slot = start_reg + arg_slot[i];
43080
43501
  ensure_newline(status);
43081
- if (slot >= 8) emit_asm(status, `str x0, [x29, #${overflow_base + (slot - 8) * 8}]\n`);
43082
- else {
43083
- const reg = `x${slot}`;
43084
- if (reg !== "x0") emit_asm(status, `mov ${reg}, x0\n`);
43085
- else if (static_arg0_needs_spill) emit_asm(status, `str x0, [x29, #${static_arg0_spill}]\n`);
43086
- }
43502
+ emit_asm(status, `str x0, [x29, #${arg_spill_base + arg_slot[i] * 8}]\n`);
43087
43503
  }
43088
43504
  deferred_args.sort((a, b) => b.slot - a.slot);
43089
43505
  for (const d of deferred_args) {
43090
43506
  build_operand(d.param, `x${start_reg + d.slot}`, status);
43091
43507
  ensure_newline(status);
43092
43508
  }
43093
- if (has_pair_args) for (let j = 0; j < access_func.params.length; j++) {
43094
- if (!view_arg_set.has(j) && !string_arg_set.has(j)) continue;
43095
- for (const half of [0, 1]) {
43096
- const half_slot = start_reg + arg_slot[j] + half;
43097
- if (half_slot >= 8) continue;
43098
- emit_asm(status, `ldr x${half_slot}, [x29, #${view_spill_base + (arg_slot[j] + half) * 8}]\n`);
43099
- }
43100
- }
43101
- if (static_arg0_needs_spill) {
43102
- ensure_newline(status);
43103
- emit_asm(status, `ldr x0, [x29, #${static_arg0_spill}]\n`);
43509
+ const deferred_slots = new Set(deferred_args.map((d) => start_reg + d.slot));
43510
+ for (let s = 0; s < total_arg_slots; s++) {
43511
+ const slot = start_reg + s;
43512
+ if (slot >= 8) continue;
43513
+ if (deferred_slots.has(slot)) continue;
43514
+ emit_asm(status, `ldr x${slot}, [x29, #${arg_spill_base + s * 8}]\n`);
43104
43515
  }
43105
43516
  ensure_newline(status);
43106
43517
  if (receiver_is_string) emit_asm(status, frees_string_receiver ? `ldp x0, x1, [sp]\n` : `ldp x0, x1, [sp], #16\n`);
@@ -43110,8 +43521,9 @@ function build_access_method(node, access_func, status) {
43110
43521
  if (overflow_count > 0) {
43111
43522
  outgoing_size = Math.ceil(overflow_count * 8 / 16) * 16;
43112
43523
  emit_asm(status, `sub sp, sp, #${outgoing_size}\n`);
43524
+ const overflow_first = 8 - start_reg;
43113
43525
  for (let k = 0; k < overflow_count; k++) {
43114
- emit_asm(status, `ldr x9, [x29, #${overflow_base + k * 8}]\n`);
43526
+ emit_asm(status, `ldr x9, [x29, #${arg_spill_base + (overflow_first + k) * 8}]\n`);
43115
43527
  emit_asm(status, `str x9, [sp, #${k * 8}]\n`);
43116
43528
  }
43117
43529
  }
@@ -44110,6 +44522,195 @@ function scan_func(func, struct_name, result, class_type_names) {
44110
44522
  }
44111
44523
  }
44112
44524
  //#endregion
44525
+ //#region ../src/build_common/struct_return_classification.ts
44526
+ /**
44527
+ * Pre-build classification of which functions return value structs whose
44528
+ * string fields are UNIFORMLY heap-owned via return-boundary normalization.
44529
+ * Populates `status.normalized_struct_returners`; a caller binding a call
44530
+ * result of a registered function records the struct's string fields and
44531
+ * frees them at scope exit, while bindings of unregistered callees keep the
44532
+ * pre-existing borrow behavior (never recorded, never freed — sound, though
44533
+ * it may leak on slot overwrites, which is the documented status quo).
44534
+ *
44535
+ * The rule is deliberately narrow: a function normalizes iff EVERY
44536
+ * struct-return statement returns a BARE VARIABLE (a local or parameter).
44537
+ * That is the transfer shape — the returned variable dies at the return, so
44538
+ * the boundary normalization (transfer recorded heap fields, strdup the
44539
+ * rest) is sound and leak-free. Every other shape keeps the status quo:
44540
+ *
44541
+ * - container borrow accessors (`at`/`first`/`slice`/`load` — e.g.
44542
+ * `Map.get`'s `return self.values.load_T(idx)`) yield SLOT-owned bytes;
44543
+ * normalizing them would leak in expression-temp consumers
44544
+ * (`m.get(k).field`) that nobody records or frees;
44545
+ * - owned accessors (`pop`/`move_T`/`copy`) transfer slot-owned heap
44546
+ * buffers — the caller's recording is sound WITHOUT normalization, so
44547
+ * they record via the owned-accessor rule (see
44548
+ * call_init_string_fields.ts) and the callee skips the copy;
44549
+ * - forwarded calls (`return make()`) are skipped when the callee is
44550
+ * registered (its result is already uniformly owned — copying it would
44551
+ * leak the inner buffers) and keep the status quo when it is not.
44552
+ *
44553
+ * This runs as a WHOLE-PROGRAM AST PRE-PASS (before any function body is
44554
+ * built) because a build-order-sensitive registry cannot work: nested
44555
+ * functions are built after their enclosing function's body, so a runtime
44556
+ * registration would be invisible to the caller's earlier lookup.
44557
+ */
44558
+ /** Collect every function and struct declaration under the AST (including
44559
+ * nested functions, struct methods, and function bodies at any depth). */
44560
+ function collect(node, fns, structs, seen) {
44561
+ if (!node || typeof node !== "object" || seen.has(node)) return;
44562
+ seen.add(node);
44563
+ if (node.node_type === "func") fns.push(node);
44564
+ if (node.node_type === "struct") structs.push(node);
44565
+ for (const key of Object.keys(node)) {
44566
+ if (key === "parent" || key === "scope") continue;
44567
+ const value = node[key];
44568
+ if (Array.isArray(value)) {
44569
+ for (const item of value) if (item && typeof item === "object" && "node_type" in item) collect(item, fns, structs, seen);
44570
+ } else if (value && typeof value === "object" && "node_type" in value) collect(value, fns, structs, seen);
44571
+ }
44572
+ }
44573
+ /** The DIRECT `return <expr>` statements of a function body (nested function
44574
+ * declarations are not descended into — their returns belong to them). */
44575
+ function direct_returns(fn) {
44576
+ const out = [];
44577
+ const seen = /* @__PURE__ */ new Set();
44578
+ const walk = (node) => {
44579
+ if (!node || typeof node !== "object" || seen.has(node)) return;
44580
+ seen.add(node);
44581
+ if (node.node_type === "func" && node !== fn) return;
44582
+ if (node.node_type === "return") out.push(node);
44583
+ for (const key of Object.keys(node)) {
44584
+ if (key === "parent" || key === "scope") continue;
44585
+ const value = node[key];
44586
+ if (Array.isArray(value)) {
44587
+ for (const item of value) if (item && typeof item === "object" && "node_type" in item) walk(item);
44588
+ } else if (value && typeof value === "object" && "node_type" in value) walk(value);
44589
+ }
44590
+ };
44591
+ for (const statement of fn.statements ?? []) walk(statement);
44592
+ return out;
44593
+ }
44594
+ /** Whether the function is a FREE function: not a method of a struct (whose
44595
+ * element/slot params are passed by address for in-place access) and not a
44596
+ * monomorphized container clone. */
44597
+ function is_free_function_node(fn) {
44598
+ const scope = fn.scope;
44599
+ return !scope || scope.node_type === "root" || scope.node_type === "func";
44600
+ }
44601
+ /**
44602
+ * Whole-program pre-pass: classify free functions that take an OWNING value
44603
+ * struct parameter (string fields) — those get pass-by-value semantics:
44604
+ *
44605
+ * - the CALL SITE materializes a uniformly heap-owned shell (struct-copy
44606
+ * + per-string-field strdup) and passes its address;
44607
+ * - the CALLEE seeds the param's string-field records at entry and frees
44608
+ * the surviving fields (and the shell) at scope exit.
44609
+ *
44610
+ * Writes through the param then stay local to the callee; the caller's
44611
+ * variable is never aliased or mutated. Functions whose address escapes
44612
+ * into a func-typed value (`var f = g`, `apply(g, …)`, `b.modify(0, touch)`)
44613
+ * are EXCLUDED: they may be invoked through a pointer with a borrowed
44614
+ * argument, and keep the status quo (borrow + the documented write leak).
44615
+ *
44616
+ * Populates `status.normalized_struct_returners` (the materializing set) and
44617
+ * `status.func_address_taken` (the excluded set) for the call-site and
44618
+ * function-definition builders.
44619
+ */
44620
+ function gather_normalized_struct_returners(root, status) {
44621
+ const fns = [];
44622
+ const structs = [];
44623
+ collect(root, fns, structs, /* @__PURE__ */ new Set());
44624
+ const address_taken = /* @__PURE__ */ new Set();
44625
+ const value_walk = (node) => {
44626
+ if (!node || typeof node !== "object") return;
44627
+ if (node.node_type === "value") {
44628
+ const v = node;
44629
+ if (typeof v.value === "string" && v.type?.name === "func" && /^[A-Za-z_]/.test(v.value)) address_taken.add(v.value);
44630
+ }
44631
+ for (const key of Object.keys(node)) {
44632
+ if (key === "parent" || key === "scope") continue;
44633
+ const child = node[key];
44634
+ if (Array.isArray(child)) {
44635
+ for (const item of child) if (item && typeof item === "object" && "node_type" in item) value_walk(item);
44636
+ } else if (child && typeof child === "object" && "node_type" in child) value_walk(child);
44637
+ }
44638
+ };
44639
+ value_walk(root);
44640
+ const decl_walk = (node) => {
44641
+ if (!node || typeof node !== "object") return;
44642
+ const d = node;
44643
+ if (d.node_type === "declaration" && typeof d.name === "string" && d.type?.name === "func") address_taken.add(d.name);
44644
+ for (const key of Object.keys(node)) {
44645
+ if (key === "parent" || key === "scope") continue;
44646
+ const child = node[key];
44647
+ if (Array.isArray(child)) {
44648
+ for (const item of child) if (item && typeof item === "object" && "node_type" in item) decl_walk(item);
44649
+ } else if (child && typeof child === "object" && "node_type" in child) decl_walk(child);
44650
+ }
44651
+ };
44652
+ decl_walk(root);
44653
+ const return_candidates = [];
44654
+ for (const fn of fns) {
44655
+ if (!is_free_function_node(fn)) continue;
44656
+ if (address_taken.has(fn.name)) continue;
44657
+ const ret = fn.return_type;
44658
+ if (!ret || ret.is_array || ret.is_view) continue;
44659
+ const ret_struct = structs.find((st) => st.name === ret.name && !st.is_simple_type && !st.is_class);
44660
+ if (!ret_struct || direct_string_fields(ret_struct).length === 0) continue;
44661
+ return_candidates.push(fn);
44662
+ }
44663
+ const return_normalizing = new Set(return_candidates.map((c) => c.name));
44664
+ let changed = true;
44665
+ while (changed) {
44666
+ changed = false;
44667
+ for (const fn of return_candidates) {
44668
+ if (!return_normalizing.has(fn.name)) continue;
44669
+ for (const ret of direct_returns(fn)) {
44670
+ const value = ret.value ?? void 0;
44671
+ if (value && value.node_type === "value") continue;
44672
+ if (value && value.node_type === "func_call" && return_normalizing.has(value.name ?? "")) continue;
44673
+ return_normalizing.delete(fn.name);
44674
+ changed = true;
44675
+ break;
44676
+ }
44677
+ }
44678
+ }
44679
+ for (const fn of fns) {
44680
+ if (!is_free_function_node(fn)) continue;
44681
+ if (address_taken.has(fn.name)) continue;
44682
+ if (fn.params.some((p) => p.is_self_param)) continue;
44683
+ if (fn.address_escaped) continue;
44684
+ if (!fn.has_body) continue;
44685
+ for (const param of fn.params ?? []) {
44686
+ if (param.is_self_param || param.is_variadic) continue;
44687
+ if (param.type.is_ref || param.type.is_nullable || param.type.is_view || param.type.is_array) continue;
44688
+ const struct = param.type.name ? structs.find((st) => st.name === param.type.name && !st.is_simple_type && !st.is_class) : void 0;
44689
+ if (!struct) continue;
44690
+ if (direct_string_fields(struct).length > 0 && !(struct.traits ?? []).length && !struct_needs_destroy(struct, status)) {
44691
+ if (!status.normalized_struct_returners) status.normalized_struct_returners = /* @__PURE__ */ new Set();
44692
+ status.normalized_struct_returners.add(fn.name);
44693
+ status.normalized_struct_returners.add(emission_label(fn));
44694
+ break;
44695
+ }
44696
+ }
44697
+ if (return_normalizing.has(fn.name)) {
44698
+ if (!status.normalized_struct_returners) status.normalized_struct_returners = /* @__PURE__ */ new Set();
44699
+ status.normalized_struct_returners.add(fn.name);
44700
+ status.normalized_struct_returners.add(emission_label(fn));
44701
+ }
44702
+ }
44703
+ for (const struct of structs) {
44704
+ if (struct.is_class || struct.is_generic || struct.is_simple_type) continue;
44705
+ if (!struct.functions.some((f) => f.name === "copy" && f.has_body)) continue;
44706
+ if (direct_string_fields(struct).length === 0) continue;
44707
+ if (!status.normalized_struct_returners) status.normalized_struct_returners = /* @__PURE__ */ new Set();
44708
+ status.normalized_struct_returners.add(`${struct.name}_copy`);
44709
+ }
44710
+ if (!status.func_address_taken) status.func_address_taken = /* @__PURE__ */ new Set();
44711
+ for (const name of address_taken) status.func_address_taken.add(name);
44712
+ }
44713
+ //#endregion
44113
44714
  //#region ../src/build.ts
44114
44715
  function build(root, options = {}) {
44115
44716
  let status = {
@@ -44143,6 +44744,7 @@ function build(root, options = {}) {
44143
44744
  reset_match_temp_counter();
44144
44745
  reset_ns_tmp_counter();
44145
44746
  stamp_last_use_moves(root);
44747
+ gather_normalized_struct_returners(root, status);
44146
44748
  if (options.arch === "aarch64") {
44147
44749
  install_asm_code_buffer(status);
44148
44750
  reset_string_counter$1();
@@ -44190,10 +44792,7 @@ function build(root, options = {}) {
44190
44792
  }
44191
44793
  if (status.strings && status.strings.size > 0) {
44192
44794
  status.code += "\n";
44193
- for (const [label, value] of status.strings) {
44194
- const escaped = value.replace(/\n/g, "\\n");
44195
- status.code += `${label}: .asciz ${escaped}\n`;
44196
- }
44795
+ for (const [label, value] of status.strings) status.code += `${label}: .asciz ${escape_asciz(value)}\n`;
44197
44796
  }
44198
44797
  if (status.float_literals && status.float_literals.size > 0) {
44199
44798
  status.code += "\n.p2align 2\n";
@@ -46369,6 +46968,23 @@ function is_owning_struct_type_requiring_move(type, status) {
46369
46968
  if (!s || s.is_class) return false;
46370
46969
  return struct_owns_non_string_heap(s, status, /* @__PURE__ */ new Set());
46371
46970
  }
46971
+ /**
46972
+ * Whether `type` is a concrete value struct with direct string fields whose
46973
+ * only ownership those strings — the pass-by-value convention's domain: a
46974
+ * parameter of this type is fully owned by the callee (the call boundary
46975
+ * materializes a uniformly heap-owned copy), so `var` field writes inside
46976
+ * the body stay local and no call-site acknowledgment is needed. Excludes
46977
+ * structs that own non-string heap (those keep `move ... swap` / `ref`
46978
+ * semantics) and structs conforming to traits (trait dispatch aliases).
46979
+ */
46980
+ function is_pass_by_value_owning_struct(type, status) {
46981
+ const s = resolve_struct$1(type, status);
46982
+ if (!s || s.is_class || s.is_generic || s.is_simple_type) return false;
46983
+ if ((s.traits ?? []).length > 0) return false;
46984
+ if (direct_string_fields(s).length === 0) return false;
46985
+ if (struct_owns_non_string_heap(s, status, /* @__PURE__ */ new Set())) return false;
46986
+ return true;
46987
+ }
46372
46988
  function struct_owns_non_string_heap(s, status, visited) {
46373
46989
  if (visited.has(s.name)) return false;
46374
46990
  visited.add(s.name);
@@ -49009,6 +49625,12 @@ function check_declaration_node(decl, status) {
49009
49625
  if (is_view_keyword && decl.type_start !== void 0) decl.type.is_view = true;
49010
49626
  if (decl.func_params) {
49011
49627
  if (is_view_keyword) add_error(status, `'view' cannot declare a function-typed binding`, decl.start);
49628
+ if (decl.value?.node_type === "func") decl.value.address_escaped = true;
49629
+ else if (decl.value?.node_type === "value") {
49630
+ const fn = status.functions.findLast((f) => f.name === decl.value.value);
49631
+ if (fn) fn.address_escaped = true;
49632
+ }
49633
+ if (decl.value?.node_type === "value" && decl.value.value === "null" && !(decl.type.name === "func" && decl.type.is_nullable)) add_error(status, `cannot assign null to a non-nullable func — declare it 'func?' (or 'Func<...>?')`, decl.value.start);
49012
49634
  if (decl.func_return_type) {
49013
49635
  check_type_exists(decl.func_return_type, status, -1);
49014
49636
  const materialized = materialize_type(decl.func_return_type, status);
@@ -49073,7 +49695,7 @@ function check_declaration_node(decl, status) {
49073
49695
  status.values.push({
49074
49696
  declaration,
49075
49697
  name: decl.name,
49076
- type: decl.func_return_type || decl.type,
49698
+ type: decl.type.name === "func" && decl.type.is_nullable ? decl.type : decl.func_return_type || decl.type,
49077
49699
  is_set: !!decl.value,
49078
49700
  start: decl.start,
49079
49701
  is_null: decl.value?.node_type === "value" && decl.value.value === "null",
@@ -49571,7 +50193,7 @@ function check_struct_node(struct, status) {
49571
50193
  continue;
49572
50194
  }
49573
50195
  if (decl.func_params) {
49574
- decl.type = new Type("func");
50196
+ if (decl.type.name !== "func") decl.type = new Type("func");
49575
50197
  check_declaration_node(decl, status);
49576
50198
  continue;
49577
50199
  }
@@ -49879,6 +50501,7 @@ function synthesize_for_struct(struct, status) {
49879
50501
  if (struct.traits.includes("Stringable") && !struct_has_function(struct, "to_string") && fields.every((f) => field_is_stringable(f, status, /* @__PURE__ */ new Set()))) struct.functions.push(build_to_string(struct, fields));
49880
50502
  if (struct.traits.includes("Equatable") && !struct_has_function(struct, "eq") && !struct_has_function(struct, "ne") && fields.every((f) => field_is_equatable(f, status, /* @__PURE__ */ new Set()))) struct.functions.push(build_eq(struct, fields));
49881
50503
  if (struct.traits.includes("Hashable") && !struct_has_function(struct, "hash") && fields.every((f) => field_is_hashable(f, status, /* @__PURE__ */ new Set()))) struct.functions.push(build_hash(struct, fields));
50504
+ synthesize_copy_method(struct, status);
49882
50505
  }
49883
50506
  function build_to_string(struct, fields) {
49884
50507
  const parts = [string_literal(`${struct.name}(`)];
@@ -49910,6 +50533,81 @@ function build_hash(struct, fields) {
49910
50533
  const ret = new ReturnNode(-1, expr);
49911
50534
  return new FunctionNode(-1, "pub", "hash", new Type("uint"), [self_param(struct)], [ret]);
49912
50535
  }
50536
+ /**
50537
+ * Whether `struct` is eligible for a synthesized `copy` method: a value
50538
+ * struct whose only ownership is its DIRECT string fields — the same set
50539
+ * the pass-by-value argument convention covers. A struct that additionally
50540
+ * owns non-string heap (a resource-releasing `#destroy`, a class field, a
50541
+ * nested requiring-move struct) cannot be deep-copied by "byte copy +
50542
+ * strdup the strings" — its copy stays unwritten (move/swap remains the
50543
+ * escape hatch) — and a struct with no string fields is already soundly
50544
+ * byte-copyable, so it needs no method.
50545
+ */
50546
+ function struct_is_copyable(struct, status) {
50547
+ if (struct.is_class || struct.is_generic || struct.is_simple_type) return false;
50548
+ if (direct_string_fields(struct).length === 0) return false;
50549
+ if (struct_owns_non_string_heap(struct, status, /* @__PURE__ */ new Set())) return false;
50550
+ return struct_copy_ctor_args(struct) !== null;
50551
+ }
50552
+ /**
50553
+ * How a synthesized `copy` constructs the fresh value: the zero-argument
50554
+ * `#init` overload when one exists (auto or user), else the bodyless AUTO
50555
+ * `#init` whose params map 1:1 onto non-defaulted fields (pass
50556
+ * `self.<param>` for each). Null when neither shape exists — a user #init
50557
+ * with params only cannot be invoked without re-running its custom logic,
50558
+ * so no `copy` is synthesized.
50559
+ */
50560
+ function struct_copy_ctor_args(struct) {
50561
+ const inits = struct.functions.filter((f) => f.name === "#init");
50562
+ if (inits.some((f) => f.params.filter((p) => !p.is_self_param).length === 0)) return [];
50563
+ const auto = inits.find((f) => !f.has_body);
50564
+ if (!auto) return null;
50565
+ return auto.params.filter((p) => !p.is_self_param).map((p) => p.name);
50566
+ }
50567
+ /** Whether `type` names a struct that has (or will get) a `copy` method.
50568
+ * Order-independent: it re-evaluates the eligibility predicate instead of
50569
+ * looking for an already-synthesized method, so an outer struct nested in
50570
+ * the same pre-pass resolves a later-declared field struct. */
50571
+ function field_type_is_copyable(type, status) {
50572
+ if (!type.name || type.is_array || type.is_ref || type.is_view || type.is_nullable) return false;
50573
+ const nested = status.structs.find((s) => s.name === type.name);
50574
+ return !!nested && struct_is_copyable(nested, status);
50575
+ }
50576
+ /**
50577
+ * Synthesize `pub func copy(self) -> Struct` for owning value structs: a
50578
+ * fresh `Struct()` whose every field is rewritten from `self` — string
50579
+ * field writes strdup the source (the backends' field-write path), nested
50580
+ * copyable structs recurse through their own `copy`, everything else byte
50581
+ * copies. `return c` hits the return-boundary normalization (all fields
50582
+ * recorded → transfer raw), so the caller receives a uniformly heap-owned
50583
+ * value with no extra copies.
50584
+ */
50585
+ function build_copy(struct, status) {
50586
+ const ctor_args = struct_copy_ctor_args(struct) ?? [];
50587
+ const ctor = new FunctionCallNode(-1, struct.name, new Type(struct.name), ctor_args.map((name) => field_access("self", name)));
50588
+ const statements = [new DeclarationNode(-1, "private", "var", "c", new Type(struct.name), ctor)];
50589
+ for (const field of struct.fields) {
50590
+ const right = field_type_is_copyable(field.type, status) ? method_call(field_access("self", field.name), "copy") : field_access("self", field.name);
50591
+ statements.push(new AssignmentNode(-1, field_access("c", field.name), right, "="));
50592
+ }
50593
+ statements.push(new ReturnNode(-1, new ValueNode(-1, "c")));
50594
+ return new FunctionNode(-1, "pub", "copy", new Type(struct.name), [self_param(struct)], statements);
50595
+ }
50596
+ /**
50597
+ * Synthesize the `copy` method on `struct` when eligible and not already
50598
+ * present. Called from the block-level derive pre-pass (which runs before
50599
+ * the struct is checked, so the method's body is checked with it) AND from
50600
+ * `monomorphize` — a mono struct materializes mid-check, after that pre-pass
50601
+ * ran, so without this hook `Box<string>.copy()` — advertised by the
50602
+ * copy-discipline error messages — would resolve to "Function not found".
50603
+ * The caller is responsible for checking the synthesized body when no later
50604
+ * struct check will (see monomorphize).
50605
+ */
50606
+ function synthesize_copy_method(struct, status) {
50607
+ if (struct_has_function(struct, "copy")) return;
50608
+ if (!struct_is_copyable(struct, status)) return;
50609
+ struct.functions.push(build_copy(struct, status));
50610
+ }
49913
50611
  //#endregion
49914
50612
  //#region ../src/check/check_block_node.ts
49915
50613
  function check_block_node(node, status) {
@@ -50018,16 +50716,22 @@ function must_use_type_display_name(en, type) {
50018
50716
  */
50019
50717
  function gather_top_level_consts(block, status) {
50020
50718
  const seen = /* @__PURE__ */ new Set();
50719
+ const candidates = [];
50021
50720
  for (const child of block.statements) {
50022
50721
  if (child.node_type !== "declare") continue;
50023
50722
  const decl = child;
50024
50723
  if (decl.declaration !== "const") continue;
50025
- if (!decl.name || !decl.type?.name) continue;
50026
- if (decl.type.is_array) continue;
50724
+ if (!decl.name) continue;
50027
50725
  if (seen.has(decl.name)) continue;
50028
50726
  seen.add(decl.name);
50727
+ candidates.push(decl);
50728
+ }
50729
+ infer_const_decl_types(candidates, status);
50730
+ for (const decl of candidates) {
50731
+ if (!decl.type?.name) continue;
50732
+ if (decl.type.is_array) continue;
50029
50733
  status.values.push({
50030
- declaration: decl.declaration,
50734
+ declaration: "const",
50031
50735
  name: decl.name,
50032
50736
  type: decl.type,
50033
50737
  is_set: !!decl.value,
@@ -50037,6 +50741,60 @@ function gather_top_level_consts(block, status) {
50037
50741
  }
50038
50742
  }
50039
50743
  /**
50744
+ * Give every root-level `const` a declared type BEFORE the statement walk,
50745
+ * so a const declared BELOW its first use resolves like a free function
50746
+ * (which hoists across the whole file). Parse can only infer the type when
50747
+ * the initializer is a bare literal; a composed chain (`const TAG = "<" +
50748
+ * TAG_NAME + ">"`, the allmark htmlPatterns shape) stayed typeless and its
50749
+ * use-above-declaration failed with "Unknown value".
50750
+ *
50751
+ * The inference iterates until nothing new is derivable, so a chain may
50752
+ * reference consts declared BELOW itself too. Recognized initializers:
50753
+ * literals, references to other (typed) consts, and arithmetic/concat
50754
+ * operations (the result type is the left operand's, mirroring
50755
+ * check_operation_node). Anything else (constructor calls, function calls)
50756
+ * keeps today's behavior: annotate the type or declare before use.
50757
+ */
50758
+ function infer_const_decl_types(decls, status) {
50759
+ const known = /* @__PURE__ */ new Map();
50760
+ for (const decl of decls) if (decl.type?.name) known.set(decl.name, decl.type);
50761
+ let progress = true;
50762
+ while (progress) {
50763
+ progress = false;
50764
+ for (const decl of decls) {
50765
+ if (decl.type?.name) continue;
50766
+ const inferred = infer_const_expr_type(decl.value, known, status);
50767
+ if (!inferred) continue;
50768
+ decl.type = inferred;
50769
+ known.set(decl.name, inferred);
50770
+ progress = true;
50771
+ }
50772
+ }
50773
+ }
50774
+ function infer_const_expr_type(node, known, status) {
50775
+ if (!node) return void 0;
50776
+ if (node.node_type === "grouped") return infer_const_expr_type(node.value, known, status);
50777
+ if (node.node_type === "value") {
50778
+ const raw = node.value;
50779
+ if (typeof raw !== "string") return void 0;
50780
+ if (raw.startsWith("\"")) return new Type("string", true);
50781
+ if (raw === "true" || raw === "false") return new Type("bool", true);
50782
+ if (raw.startsWith("'") && raw.endsWith("'")) return new Type("char", true);
50783
+ if (is_int_literal(raw)) return new Type("int", true);
50784
+ if (/^(\+|-)*\d+.\d+([eE](\+|-)?\d+)?$/.test(raw)) return new Type("float", true);
50785
+ const declared = known.get(raw);
50786
+ if (declared) return declared;
50787
+ return;
50788
+ }
50789
+ if (node.node_type === "op") return infer_const_expr_type(node.left_value, known, status);
50790
+ if (node.node_type === "func_call") {
50791
+ const call = node;
50792
+ const target = status.structs.find((s) => s.name === call.name && !s.is_simple_type);
50793
+ if (target && target.type_params.length === 0) return new Type(target.name);
50794
+ return;
50795
+ }
50796
+ }
50797
+ /**
50040
50798
  * A bare statement that constructs (or obtains) a class instance without
50041
50799
  * binding it to a variable would leak: classes are heap-allocated and freed
50042
50800
  * by the owning variable's scope-exit cleanup, so an unbound instance is
@@ -50301,8 +51059,8 @@ function check_function_parameter_node(param, status, is_library_scope = false)
50301
51059
  if (param.type.is_pointer && !is_library_scope) add_error(status, `'ptr' parameters are reserved for the System library — raw pointer manipulation is not available to user code`, param.type_start ?? param.start);
50302
51060
  instantiate_generic_type(param.type, status);
50303
51061
  }
50304
- if (param.declaration === "var" && !param.is_moved && !param.is_self_param && !param.type.is_ref) add_error(status, `'var' parameters are not allowed — use 'ref' to mutate the caller's value (with 'ref' at the call site), or take it read-only and make a local 'var' copy`, param.start);
50305
- if (param.is_moved && param.type.name && !is_class_type$1(param.type.name, status) && !is_trait_type(param.type.name, status) && !is_owning_struct_type_requiring_move(param.type, status) && !status.type_params.includes(param.type.name) && !status.structs.some((s) => s.type_params.includes(param.type.name))) add_error(status, `move is only allowed for class, trait, or owning struct types, not '${param.type.name}'`, param.start);
51062
+ if (param.declaration === "var" && !param.is_moved && !param.is_self_param && !param.type.is_ref && !is_pass_by_value_owning_struct(param.type, status)) add_error(status, `'var' parameters are not allowed — use 'ref' to mutate the caller's value (with 'ref' at the call site), or take it read-only and make a local 'var' copy`, param.start);
51063
+ if (param.is_moved && param.type.name && !is_class_type$1(param.type.name, status) && !is_trait_type(param.type.name, status) && !is_owning_struct_type_requiring_move(param.type, status) && !is_pass_by_value_owning_struct(param.type, status) && !status.type_params.includes(param.type.name) && !status.structs.some((s) => s.type_params.includes(param.type.name))) add_error(status, `move is only allowed for class, trait, or owning struct types, not '${param.type.name}'`, param.start);
50306
51064
  if (param.is_variadic && param.default_value) {
50307
51065
  add_error(status, `Variadic parameter cannot have a default value`, param.start);
50308
51066
  return;
@@ -50314,7 +51072,7 @@ function check_function_parameter_node(param, status, is_library_scope = false)
50314
51072
  }
50315
51073
  if (status.values.some((v) => v.name === param.name && v.declaration !== "const" && !(is_library_scope && v.is_global))) add_error(status, `Parameter already declared: ${param.name}`, param.start);
50316
51074
  status.values.push({
50317
- declaration: param.declaration,
51075
+ declaration: param.declaration === "var" || is_pass_by_value_owning_struct(param.type, status) ? "var" : param.declaration,
50318
51076
  name: param.name,
50319
51077
  type: param.type,
50320
51078
  is_set: true,
@@ -50374,6 +51132,7 @@ function clone_status(status) {
50374
51132
  var_name_counter: status.var_name_counter,
50375
51133
  type_params: status.type_params,
50376
51134
  errors: status.errors,
51135
+ warnings: status.warnings,
50377
51136
  buffer_caps: status.buffer_caps,
50378
51137
  equal_lengths: status.equal_lengths?.slice(),
50379
51138
  mutated_local_names: status.mutated_local_names,
@@ -50730,7 +51489,7 @@ function check_function_call_node(node, status) {
50730
51489
  return false;
50731
51490
  }
50732
51491
  if (magic_ctor && spawn_ctor_shape) return check_magic_ctor(node, status, magic_ctor);
50733
- let func = find_free_function(status, node.name);
51492
+ let func = status.values.findLast((v) => v.name === node.name && v.func_params !== void 0) ? void 0 : find_free_function(status, node.name);
50734
51493
  if (func?.is_closure && func.captures?.length) func = void 0;
50735
51494
  if (!func) {
50736
51495
  const struct = resolve_declared_struct(node.name, status);
@@ -50799,6 +51558,10 @@ function check_function_call_node(node, status) {
50799
51558
  const param_index = status.values.findLastIndex((v) => v.name === node.name);
50800
51559
  const param_value = param_index >= 0 ? status.values[param_index] : void 0;
50801
51560
  if (!!param_value && (param_value.type.name === "func" || param_value.func_params !== void 0 || param_value.func_return_type !== void 0) && param_value) {
51561
+ if (param_value.is_null && !status.allow_null_value) {
51562
+ add_error(status, `Variable '${node.name}' may be null`, node.start);
51563
+ return false;
51564
+ }
50802
51565
  if (status.moved_variables?.has(node.name)) {
50803
51566
  add_error(status, `Variable '${node.name}' used after move`, node.start);
50804
51567
  return false;
@@ -51027,6 +51790,30 @@ function monomorphize(generic_struct, type_args, status) {
51027
51790
  const init_func = new FunctionNode(generic_struct.start, "pub", "#init", init_return_type, init_params);
51028
51791
  mono_struct.functions.push(init_func);
51029
51792
  }
51793
+ synthesize_copy_method(mono_struct, status);
51794
+ const mono_copy = mono_struct.functions.find((f) => f.name === "copy");
51795
+ if (mono_copy && !mono_copy.checked) {
51796
+ mono_copy.scope = mono_struct;
51797
+ check_function_node(mono_copy, {
51798
+ stack: status.stack,
51799
+ scope_depth: status.scope_depth,
51800
+ types: status.types,
51801
+ values: [],
51802
+ function_value_base: 0,
51803
+ structs: status.structs,
51804
+ traits: status.traits,
51805
+ enums: status.enums,
51806
+ bitsets: status.bitsets,
51807
+ functions: status.functions,
51808
+ allocations: [],
51809
+ var_name_counter: status.var_name_counter,
51810
+ type_params: [],
51811
+ errors: status.errors,
51812
+ function_emission_names: status.function_emission_names,
51813
+ allow_internal: status.allow_internal,
51814
+ library_boundary: status.library_boundary
51815
+ });
51816
+ }
51030
51817
  const root = status.stack[0];
51031
51818
  if (!root.statements.some((s) => s.node_type === "struct" && s.name === mono_name)) root.statements.push(mono_struct);
51032
51819
  return mono_struct;
@@ -51530,6 +52317,8 @@ function check_magic_ctor(node, status, name) {
51530
52317
  add_error(status, `Spawned call '${call.name}' did not resolve`, node.start);
51531
52318
  return false;
51532
52319
  }
52320
+ const spawned = call.resolved_function;
52321
+ if (spawned) spawned.address_escaped = true;
51533
52322
  validate_spawn_args_sendable(call, status);
51534
52323
  stamp_spawn_ctor(node, name);
51535
52324
  const return_type = call.type;
@@ -52474,6 +53263,143 @@ function is_visible(declaring_scope, visibility, access_scope, stack, declaring_
52474
53263
  return access_scope === declaring_scope;
52475
53264
  }
52476
53265
  //#endregion
53266
+ //#region ../src/check/utils/regex_pattern_lint.ts
53267
+ /**
53268
+ * Compile-time lint for Regex pattern literals (FOLLOWUP "Regex pattern
53269
+ * escapes").
53270
+ *
53271
+ * The Regex engine (core/System/Text/Regex.nm) recognizes only the
53272
+ * shorthands (`\d \D \w \W \s \S`), backreferences (`\1`-`\9`), the control
53273
+ * escapes (`\n \r \t`), and escaped metacharacters. Any OTHER engine escape
53274
+ — most notably a typo like `\z` — silently matches a literal letter. When a
53275
+ * Regex entry point's pattern argument is a static string literal, the check
53276
+ * can flag those as probable typos; dynamic runtime patterns can't be
53277
+ * validated this way.
53278
+ *
53279
+ * The literal arrives RAW (quotes included, Nomen escapes unresolved), so it
53280
+ * must first be decoded to the engine-visible bytes: a Nomen `"\n"` is a real
53281
+ * LF byte the engine treats as a literal, while `"\n"` reaches the engine as
53282
+ * a backslash + 'n' escape. Only the second spelling can carry a typo.
53283
+ */
53284
+ /** Decode a raw string-literal token (quotes included) to its engine-visible
53285
+ * bytes, resolving Nomen's own string escapes. Mirrors the pair-counting of
53286
+ * string_literal_length and the decode rules of decode_char_literal. */
53287
+ function decode_string_literal_bytes(raw) {
53288
+ const bytes = [];
53289
+ let i = 1;
53290
+ const end = raw.endsWith("\"") ? raw.length - 1 : raw.length;
53291
+ while (i < end) {
53292
+ if (raw[i] !== "\\") {
53293
+ const cp = raw.codePointAt(i) ?? 0;
53294
+ if (cp < 128) {
53295
+ bytes.push(cp);
53296
+ i += 1;
53297
+ } else if (cp < 2048) {
53298
+ bytes.push(192 | cp >> 6, 128 | cp & 63);
53299
+ i += 1;
53300
+ } else if (cp < 65536) {
53301
+ bytes.push(224 | cp >> 12, 128 | cp >> 6 & 63, 128 | cp & 63);
53302
+ i += 1;
53303
+ } else {
53304
+ bytes.push(240 | cp >> 18, 128 | cp >> 12 & 63, 128 | cp >> 6 & 63, 128 | cp & 63);
53305
+ i += 2;
53306
+ }
53307
+ continue;
53308
+ }
53309
+ const next = raw[i + 1];
53310
+ if (next === void 0) break;
53311
+ if (next === "x") {
53312
+ let j = i + 2;
53313
+ let value = 0;
53314
+ let digits = 0;
53315
+ while (j < end && digits < 2 && is_hex_digit(raw[j])) {
53316
+ value = value * 16 + hex_value(raw[j]);
53317
+ j += 1;
53318
+ digits += 1;
53319
+ }
53320
+ if (digits > 0) {
53321
+ bytes.push(value & 255);
53322
+ i = j;
53323
+ continue;
53324
+ }
53325
+ i += 2;
53326
+ continue;
53327
+ }
53328
+ if (next >= "0" && next <= "7") {
53329
+ let j = i + 1;
53330
+ let value = 0;
53331
+ let digits = 0;
53332
+ while (j < end && digits < 3 && raw[j] >= "0" && raw[j] <= "7") {
53333
+ value = value * 8 + (raw[j].charCodeAt(0) - 48);
53334
+ j += 1;
53335
+ digits += 1;
53336
+ }
53337
+ bytes.push(value & 255);
53338
+ i = j;
53339
+ continue;
53340
+ }
53341
+ switch (next) {
53342
+ case "\\":
53343
+ bytes.push(92);
53344
+ break;
53345
+ case "n":
53346
+ bytes.push(10);
53347
+ break;
53348
+ case "t":
53349
+ bytes.push(9);
53350
+ break;
53351
+ case "r":
53352
+ bytes.push(13);
53353
+ break;
53354
+ case "0":
53355
+ bytes.push(0);
53356
+ break;
53357
+ case "\"":
53358
+ bytes.push(34);
53359
+ break;
53360
+ case "'":
53361
+ bytes.push(39);
53362
+ break;
53363
+ default: bytes.push(next.charCodeAt(0));
53364
+ }
53365
+ i += 2;
53366
+ }
53367
+ return bytes;
53368
+ }
53369
+ function is_hex_digit(c) {
53370
+ return c >= "0" && c <= "9" || c >= "a" && c <= "f" || c >= "A" && c <= "F";
53371
+ }
53372
+ function hex_value(c) {
53373
+ if (c <= "9") return c.charCodeAt(0) - 48;
53374
+ if (c <= "F") return c.charCodeAt(0) - 55;
53375
+ return c.charCodeAt(0) - 87;
53376
+ }
53377
+ function is_ascii_letter(code) {
53378
+ return code >= 65 && code <= 90 || code >= 97 && code <= 122;
53379
+ }
53380
+ /** Scan the decoded pattern bytes for escapes outside the engine's known set
53381
+ * (`\d \D \w \W \s \S`, `\1`-`\9`, `\n \r \t`, escaped punctuation) and
53382
+ * return one warning message per probable typo. */
53383
+ function lint_regex_pattern(raw) {
53384
+ const bytes = decode_string_literal_bytes(raw);
53385
+ const messages = [];
53386
+ let i = 0;
53387
+ while (i < bytes.length) {
53388
+ if (bytes[i] !== 92) {
53389
+ i += 1;
53390
+ continue;
53391
+ }
53392
+ const escaped = bytes[i + 1];
53393
+ if (escaped === void 0) break;
53394
+ if (!(escaped === 100 || escaped === 68 || escaped === 119 || escaped === 87 || escaped === 115 || escaped === 83 || escaped === 110 || escaped === 114 || escaped === 116 || escaped >= 49 && escaped <= 57) && is_ascii_letter(escaped)) {
53395
+ const letter = String.fromCharCode(escaped);
53396
+ messages.push(`Regex pattern: unsupported escape '\\${letter}' matches a literal '${letter}' — supported escapes are \\d \\D \\w \\W \\s \\S \\1-\\9 \\n \\r \\t and escaped punctuation`);
53397
+ }
53398
+ i += 2;
53399
+ }
53400
+ return messages;
53401
+ }
53402
+ //#endregion
52477
53403
  //#region ../src/check/utils/string_mutation_scan.ts
52478
53404
  /**
52479
53405
  * Interprocedural no-mutation scan for borrow-position `to_string()` elision
@@ -52867,6 +53793,7 @@ function is_inside_core_method(status) {
52867
53793
  }
52868
53794
  function check_function_call(node, status, func, target_type, self_value, self_path) {
52869
53795
  set_resolved_function(node, func);
53796
+ lint_regex_entry_pattern(node, func, status);
52870
53797
  const access_scope = status.stack.at(-1);
52871
53798
  if (func.name === "#init") {
52872
53799
  const struct_name = target_type?.name || func.return_type.name;
@@ -52978,6 +53905,13 @@ function check_function_call(node, status, func, target_type, self_value, self_p
52978
53905
  const param_type = type_from_value_node(param, status);
52979
53906
  const param_value = value_from_value_node(param);
52980
53907
  const has_ref_keyword = node.ref_param_indices?.includes(i) ?? false;
53908
+ if (func && func_param && func.has_body && (!func.scope || func.scope.node_type !== "struct") && !func.params.some((p) => p.is_self_param) && !func.address_escaped && !func_param.is_self_param && !func_param.is_variadic && !func_param.type.is_ref && !func_param.type.is_nullable && func_param.type.name && !is_owning_struct_type_requiring_move(func_param.type, status)) {
53909
+ const owned_struct = status.structs.find((st) => st.name === func_param.type.name && !st.is_simple_type && !st.is_class);
53910
+ if (owned_struct && !(owned_struct.traits ?? []).length && direct_string_fields(owned_struct).length > 0 && !struct_needs_destroy(owned_struct, status)) {
53911
+ const indices = func_param.is_moved ? node.move_owned_param_indices ??= [] : node.owned_value_param_indices ??= [];
53912
+ if (!indices.includes(i)) indices.push(i);
53913
+ }
53914
+ }
52981
53915
  const has_mov_keyword = node.move_param_indices?.includes(i) ?? false;
52982
53916
  if (func_param && func_param.type.name === "string" && !func_param.type.is_view && !func_param.type.is_array && param_type.name === "string" && param_type.is_view) add_error(status, `cannot pass a 'view string' to string parameter '${func_param.name}' — call .to_string() to materialize an owned copy`, param.start);
52983
53917
  if (func_param.type.is_ref && !has_ref_keyword) add_error(status, `Missing 'ref' keyword for ref parameter '${func_param.name}'`, param.start);
@@ -53002,12 +53936,13 @@ function check_function_call(node, status, func, target_type, self_value, self_p
53002
53936
  else if (!func_param.type.is_ref && (arg_is_stack_array_var || arg_is_literal) && elem_struct && !elem_struct.is_simple_type && !elem_struct.is_class && struct_has_string_fields(elem_struct, status)) add_error(status, `Cannot pass an array of owning value struct '${param_type.name}' (string fields) to 'Array<${param_type.name}>' parameter '${func_param.name}' — the materialised heap copy would share the element strings. Use 'List<${param_type.name}>' instead: its Buffer deep-copies owning elements soundly`, param.start);
53003
53937
  }
53004
53938
  const param_is_class = func_param.type.name && is_class_type$1(func_param.type.name, status);
53005
- const param_is_owning_struct = func_param.is_moved && !func_param.type.is_nullable && !!func_param.type.name && is_owning_struct_type_requiring_move(func_param.type, status);
53939
+ const param_transfers = !!node.move_owned_param_indices?.includes(i);
53940
+ const param_is_owning_struct = func_param.is_moved && !func_param.type.is_nullable && !!func_param.type.name && (is_owning_struct_type_requiring_move(func_param.type, status) || param_transfers);
53006
53941
  const arg_is_owned_value = param.node_type === "value" && borrow_depth_of(param, status) === void 0;
53007
53942
  if (func_param.is_moved && !has_mov_keyword && (param_is_class || param_is_owning_struct) && arg_is_owned_value) add_error(status, `Missing 'move' keyword for move parameter '${func_param.name}'`, param.start);
53008
53943
  else if (!func_param.is_moved && has_mov_keyword) add_error(status, `Unexpected 'move' keyword for non-move parameter '${func_param.name}'`, param.start);
53009
53944
  if (has_mov_keyword && param_value && !node.swap_params?.has(i)) {
53010
- if (func_param.type.name && (is_class_type$1(func_param.type.name, status) || func_param.is_moved && is_owning_struct_type_requiring_move(func_param.type, status))) {
53945
+ if (func_param.type.name && (is_class_type$1(func_param.type.name, status) || func_param.is_moved && is_owning_struct_type(func_param.type, status))) {
53011
53946
  if (!status.moved_variables) status.moved_variables = /* @__PURE__ */ new Set();
53012
53947
  status.moved_variables.add(param_value);
53013
53948
  }
@@ -53018,7 +53953,7 @@ function check_function_call(node, status, func, target_type, self_value, self_p
53018
53953
  if (field_type.name && is_class_type$1(field_type.name, status)) {
53019
53954
  const field_name = access.access.name;
53020
53955
  add_error(status, `cannot move '${field_name}' out of struct — struct owns its class fields`, param.start);
53021
- } else if (field_type.name && !field_type.is_nullable && is_owning_struct_type_requiring_move(field_type, status)) {
53956
+ } else if (field_type.name && !field_type.is_nullable && (is_owning_struct_type_requiring_move(field_type, status) || param_transfers && is_pass_by_value_owning_struct(field_type, status))) {
53022
53957
  const field_name = access.access.name;
53023
53958
  add_error(status, `cannot move '${field_name}' out of struct by value — it owns heap resources; use 'move ... swap <replacement>' to revalidate the field`, param.start);
53024
53959
  }
@@ -53378,6 +54313,59 @@ function receiver_is_const(target_type, self_value, status) {
53378
54313
  const binding = status.values.findLast((v) => v.name === self_value);
53379
54314
  return !!binding && binding.declaration === "const" && !binding.type.is_ref;
53380
54315
  }
54316
+ /**
54317
+ * Regex entry points whose FIRST parameter is the pattern. When that argument
54318
+ * is a static string literal, flag engine escapes outside the known set as
54319
+ * probable typos (see regex_pattern_lint.ts) — a dynamic runtime pattern
54320
+ * can't be validated this way.
54321
+ */
54322
+ const REGEX_PATTERN_ENTRY_POINTS = /* @__PURE__ */ new Set([
54323
+ "test",
54324
+ "match",
54325
+ "count",
54326
+ "find",
54327
+ "replace_all",
54328
+ "captures",
54329
+ "test_ci",
54330
+ "match_ci",
54331
+ "find_ci",
54332
+ "captures_ci",
54333
+ "replace_all_ci"
54334
+ ]);
54335
+ /**
54336
+ * Lint a static string-literal pattern passed to a Regex entry point. Only
54337
+ * the library's own `Regex` struct qualifies (a user struct of the same name
54338
+ * with a same-named method is left alone), and only when the first argument
54339
+ * is a literal — anything else is a runtime value.
54340
+ */
54341
+ function lint_regex_entry_pattern(node, func, status) {
54342
+ if (!is_library_regex_func(func, status)) return;
54343
+ if (!REGEX_PATTERN_ENTRY_POINTS.has(func.name)) return;
54344
+ const pattern = node.params[0];
54345
+ if (!pattern || pattern.node_type !== "value") return;
54346
+ const raw = pattern.value;
54347
+ if (!raw.startsWith("\"")) return;
54348
+ if (!status.warnings) status.warnings = [];
54349
+ for (const message of lint_regex_pattern(raw)) status.warnings.push({
54350
+ message,
54351
+ start: pattern.start,
54352
+ line: 0,
54353
+ column: 0
54354
+ });
54355
+ }
54356
+ /**
54357
+ * Whether `func` is a method of the library's `Regex` struct. Two paths: the
54358
+ * method's enclosing struct is stamped on it for calls checked after the
54359
+ * struct gather (library bodies, later files), and for user call sites —
54360
+ * which are checked BEFORE the gather stamps `func.scope` — the callee is
54361
+ * matched against the gathered library struct's own methods.
54362
+ */
54363
+ function is_library_regex_func(func, status) {
54364
+ const scope = func.scope;
54365
+ if (scope && scope.node_type === "struct" && scope.is_library && scope.name === "Regex") return true;
54366
+ const regex_struct = status.structs.find((s) => s.is_library && s.name === "Regex");
54367
+ return !!regex_struct && regex_struct.functions.includes(func);
54368
+ }
53381
54369
  //#endregion
53382
54370
  //#region ../src/check/check_access_node.ts
53383
54371
  function check_access_node(node, status) {
@@ -54338,14 +55326,17 @@ function check_assignment_node(assign, status) {
54338
55326
  return_type: fd?.func_return_type
54339
55327
  };
54340
55328
  }
54341
- } else if (lhs_value?.func_params?.length) lambda_signature = {
55329
+ } else if (lhs_value?.func_params !== void 0) lambda_signature = {
54342
55330
  params: lhs_value.func_params,
54343
55331
  return_type: lhs_value.func_return_type
54344
55332
  };
54345
- if (assign.right_value.node_type === "func" && lambda_signature.params && lambda_signature.params.length) {
55333
+ if (assign.right_value.node_type === "func" && Array.isArray(lambda_signature.params)) {
55334
+ if (!lhs_value && assign.left_value.node_type === "value") {
55335
+ add_error(status, `cannot assign a lambda to '${lhs_value_name}' — the binding was initialized with a capture-free lambda and has no reassignable slot; declare it with '= null' (or from a named function) to reassign`, assign.right_value.start);
55336
+ return false;
55337
+ }
54346
55338
  const rhs_func = assign.right_value;
54347
- if (assign.left_value.node_type === "value") rhs_func.name = lhs_value_name;
54348
- else if (!rhs_func.name) synthesize_lambda_name(rhs_func, status);
55339
+ if (!rhs_func.name || assign.left_value.node_type === "value") synthesize_lambda_name(rhs_func, status);
54349
55340
  if (rhs_func.params.length === lambda_signature.params.length) {
54350
55341
  for (let i = 0; i < rhs_func.params.length; i++) if (!rhs_func.params[i].type.name && lambda_signature.params[i].type.name) rhs_func.params[i].type = lambda_signature.params[i].type;
54351
55342
  }
@@ -55577,6 +56568,8 @@ function check_spawn_node(node, status) {
55577
56568
  add_error(status, "Spawned call did not resolve", node.start);
55578
56569
  return false;
55579
56570
  }
56571
+ const spawned = node.call.resolved_function;
56572
+ if (spawned) spawned.address_escaped = true;
55580
56573
  for (const param of node.call.params) {
55581
56574
  let arg_type = type_from_value_node(param, status);
55582
56575
  const stamped = param.type;
@@ -56394,13 +57387,14 @@ function parse_type(status) {
56394
57387
  type.type_args = void 0;
56395
57388
  }
56396
57389
  if (type.name === "Func" && type.type_args?.length) {
56397
- if (type.is_nullable || type.is_array || type.is_pointer || type.is_view || type.is_ref) {
56398
- add_error(status, `Func<...> cannot be combined with '?'/'[]'/'ptr'/'view'/'ref'`, start);
57390
+ if (type.is_array || type.is_pointer || type.is_view || type.is_ref) {
57391
+ add_error(status, `Func<...> cannot be combined with '[]'/'ptr'/'view'/'ref'`, start);
56399
57392
  return type;
56400
57393
  }
56401
57394
  const args = type.type_args;
56402
57395
  const func_type = new Type("func");
56403
57396
  func_type.start = type.start;
57397
+ if (type.is_nullable) func_type.is_nullable = true;
56404
57398
  func_type.func_params = [];
56405
57399
  for (let i = 0; i < args.length - 1; i++) {
56406
57400
  if (args[i].name === "void") {
@@ -56513,10 +57507,14 @@ function default_visibility(status) {
56513
57507
  * - a func RETURN carries its signature on the Type
56514
57508
  * (`type.func_params` / `type.func_return_type`).
56515
57509
  */
56516
- /** Whether the current token starts a nested func type: `func` immediately
56517
- * followed by `(` (the bare word `func` alone is the plain marker type). */
57510
+ /** Whether the current token starts a nested func type: `func` (optionally
57511
+ * nullable, `func?`) immediately followed by `(` (the bare word `func` alone
57512
+ * is the plain marker type). */
56518
57513
  function at_func_type(status) {
56519
- return peek_current(status) === "func" && status.tokens[status.i + 1]?.value === "(";
57514
+ if (peek_current(status) !== "func") return false;
57515
+ const next = status.tokens[status.i + 1]?.value;
57516
+ if (next === "(") return true;
57517
+ return next === "?" && status.tokens[status.i + 2]?.value === "(";
56520
57518
  }
56521
57519
  /**
56522
57520
  * Reject `void` where it has no meaning. Only the last type argument of
@@ -56537,6 +57535,7 @@ function reject_void_type(type, status, at) {
56537
57535
  function adopt_func_param_type(param, type) {
56538
57536
  if (type.name !== "func") return false;
56539
57537
  param.type = new Type("func");
57538
+ param.type.is_nullable = type.is_nullable;
56540
57539
  param.func_params = type.func_params;
56541
57540
  param.func_return_type = type.func_return_type;
56542
57541
  return true;
@@ -56559,11 +57558,13 @@ function parse_func_type_signature(param, status) {
56559
57558
  }
56560
57559
  /**
56561
57560
  * Parse a full nested func type — the `func` word is consumed by the caller
56562
- * (at_func_type confirmed the `(` follows) — and return it as a Type whose
56563
- * func_params/func_return_type carry the signature.
57561
+ * (at_func_type confirmed the `(` follows, possibly after a `?`) — and return
57562
+ * it as a Type whose func_params/func_return_type carry the signature. A
57563
+ * `func?` spelling marks the type nullable (it may hold `null`).
56564
57564
  */
56565
57565
  function parse_func_type(status) {
56566
57566
  const type = new Type("func");
57567
+ if (accept("?", status)) type.is_nullable = true;
56567
57568
  const param = new ParameterNode(get_index(status), "");
56568
57569
  parse_func_type_signature(param, status);
56569
57570
  type.func_params = param.func_params;
@@ -56603,6 +57604,7 @@ function parse_signature_params(params, status) {
56603
57604
  if (at_func_type(status)) {
56604
57605
  consume(status);
56605
57606
  param.type = new Type("func");
57607
+ if (accept("?", status)) param.type.is_nullable = true;
56606
57608
  parse_func_type_signature(param, status);
56607
57609
  } else {
56608
57610
  param.type = parse_type(status);
@@ -56643,7 +57645,11 @@ function parse_declaration(visibility, declaration, status, is_readonly = false)
56643
57645
  if (is_func_alias) {
56644
57646
  decl.func_params = decl.type.func_params ?? [];
56645
57647
  decl.func_return_type = decl.type.func_return_type;
56646
- decl.type = new Type("");
57648
+ if (decl.type.is_nullable) {
57649
+ const nullable_marker = new Type("func");
57650
+ nullable_marker.is_nullable = true;
57651
+ decl.type = nullable_marker;
57652
+ } else decl.type = new Type("");
56647
57653
  }
56648
57654
  if (peek_current(status) === "=" || status.i >= status.tokens.length) {
56649
57655
  const modifier = status.tokens[saved_i]?.value;
@@ -56705,6 +57711,7 @@ function parse_declaration(visibility, declaration, status, is_readonly = false)
56705
57711
  }
56706
57712
  function parse_function_type_declaration(decl, status) {
56707
57713
  accept("func", status);
57714
+ const func_nullable = accept("?", status);
56708
57715
  if (expect("(", status)) {
56709
57716
  const params = [];
56710
57717
  let return_type;
@@ -56717,6 +57724,10 @@ function parse_function_type_declaration(decl, status) {
56717
57724
  decl.name = consume_name(status);
56718
57725
  decl.func_params = params.filter((p) => !p.type.is_return_type);
56719
57726
  decl.func_return_type = return_type;
57727
+ if (func_nullable) {
57728
+ decl.type = new Type("func");
57729
+ decl.type.is_nullable = true;
57730
+ }
56720
57731
  const has_equals = accept("=", status);
56721
57732
  if (peek_current(status) === "{") {
56722
57733
  const func = new FunctionNode(get_index(status), default_visibility(status), decl.name, new Type(""));
@@ -56816,6 +57827,7 @@ function parse_anon_function_parameter(func, status) {
56816
57827
  if (at_func_type(status)) {
56817
57828
  consume(status);
56818
57829
  param.type = new Type("func");
57830
+ if (accept("?", status)) param.type.is_nullable = true;
56819
57831
  parse_func_type_signature(param, status);
56820
57832
  param.name_start = get_index(status);
56821
57833
  param.name = consume_name(status);
@@ -57749,6 +58761,7 @@ function parse_function_parameter(parent, func, status) {
57749
58761
  if (at_func_type(status)) {
57750
58762
  consume(status);
57751
58763
  param.type = new Type("func");
58764
+ if (accept("?", status)) param.type.is_nullable = true;
57752
58765
  parse_func_type_signature(param, status);
57753
58766
  param.name_start = get_index(status);
57754
58767
  param.name = consume_name(status);
@@ -59107,7 +60120,8 @@ nomen test
59107
60120
  function find_bundled(filename) {
59108
60121
  const here = path.dirname(fileURLToPath(import.meta.url));
59109
60122
  const cli_root = path.dirname(here);
59110
- const candidates = [path.join(cli_root, filename), path.join(cli_root, "..", filename)];
60123
+ const repo_root = path.join(cli_root, "..");
60124
+ const candidates = !cli_root.includes("node_modules") ? [path.join(repo_root, filename), path.join(cli_root, filename)] : [path.join(cli_root, filename), path.join(repo_root, filename)];
59111
60125
  for (const c of candidates) if (fs.existsSync(c)) return path.resolve(c);
59112
60126
  }
59113
60127
  /**
@@ -59329,6 +60343,10 @@ function parse_records(stdout) {
59329
60343
  other
59330
60344
  };
59331
60345
  }
60346
+ /** Pull the exit-time audit "LEAK: N allocation(s)" lines out of stdout. */
60347
+ function extract_leaks(stdout) {
60348
+ return stdout.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("LEAK:"));
60349
+ }
59332
60350
  /**
59333
60351
  * Compile one `*.test.nm` file (with the generated harness) and run it,
59334
60352
  * returning its parsed records and any crash info.
@@ -59344,6 +60362,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
59344
60362
  fails: [],
59345
60363
  benches: [],
59346
60364
  other: [],
60365
+ leaks: [],
59347
60366
  ms: 0
59348
60367
  };
59349
60368
  const resolved = path.resolve(entry_path);
@@ -59352,6 +60371,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
59352
60371
  const parsed = parse(input + "\n" + harness, library, resolved);
59353
60372
  if (parsed.errors.length) {
59354
60373
  result.ok = false;
60374
+ result.phase = "parse";
59355
60375
  result.crashed = parsed.errors.map((e) => `${e.message} (${e.line ?? "?"}:${e.column ?? "?"})`).join("\n");
59356
60376
  result.ms = performance.now() - start;
59357
60377
  return result;
@@ -59362,6 +60382,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
59362
60382
  });
59363
60383
  if (buildResult.errors && buildResult.errors.length) {
59364
60384
  result.ok = false;
60385
+ result.phase = "build";
59365
60386
  result.crashed = buildResult.errors.map((e) => `${e.message} (${e.line ?? "?"}:${e.column ?? "?"})`).join("\n");
59366
60387
  result.ms = performance.now() - start;
59367
60388
  return result;
@@ -59373,6 +60394,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
59373
60394
  const runtime_src = audit_runtime ? path.resolve(audit_runtime) : find_audit_runtime(path.dirname(resolved));
59374
60395
  if (!runtime_src || !fs.existsSync(runtime_src)) {
59375
60396
  result.ok = false;
60397
+ result.phase = "setup";
59376
60398
  result.crashed = "Audit enabled but audit_runtime.c was not found. Pass --audit-runtime <path>.";
59377
60399
  result.ms = performance.now() - start;
59378
60400
  return result;
@@ -59410,6 +60432,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
59410
60432
  } catch (err) {
59411
60433
  const stderr = err.stderr ? err.stderr.toString() : err.message ?? "";
59412
60434
  result.ok = false;
60435
+ result.phase = "link";
59413
60436
  result.crashed = `link failed: ${stderr.trim() || "clang error"}`;
59414
60437
  result.ms = performance.now() - start;
59415
60438
  return result;
@@ -59437,8 +60460,11 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
59437
60460
  result.fails = records.fails;
59438
60461
  result.benches = records.benches;
59439
60462
  result.other = records.other;
60463
+ result.leaks = audit ? extract_leaks(runStdout) : [];
60464
+ if (result.leaks.length) result.other = result.other.filter((l) => !l.trim().startsWith("LEAK:"));
59440
60465
  result.crashed = crashed;
59441
- result.ok = !(result.fails.length > 0 || result.tests.some((t) => t.failed > 0)) && crashed === void 0;
60466
+ if (crashed) result.phase = "run";
60467
+ result.ok = !(result.fails.length > 0 || result.tests.some((t) => t.failed > 0)) && crashed === void 0 && result.leaks.length === 0;
59442
60468
  result.ms = performance.now() - start;
59443
60469
  return result;
59444
60470
  }
@@ -59465,7 +60491,8 @@ const C = {
59465
60491
  function report_file(result) {
59466
60492
  const rel = path.relative(process.cwd(), result.file);
59467
60493
  if (result.crashed && result.tests.length === 0 && result.fails.length === 0) {
59468
- console.log(` ${C.red("✗")} ${rel} ${C.red("(failed to build)")}`);
60494
+ const label = result.phase === "run" ? "(crashed before records)" : result.phase === "link" ? "(link failed)" : result.phase === "setup" ? "(setup failed)" : "(failed to build)";
60495
+ console.log(` ${C.red("✗")} ${rel} ${C.red(label)}`);
59469
60496
  console.log(C.red(result.crashed));
59470
60497
  return;
59471
60498
  }
@@ -59474,6 +60501,7 @@ function report_file(result) {
59474
60501
  console.log(` ${mark} ${rel} ${C.dim(`(${totalTests} tests)`)} ${C.dim(format_duration(result.ms))}`);
59475
60502
  if (result.crashed) console.log(C.red(` ${result.crashed}`));
59476
60503
  for (const f of result.fails) console.log(` ${C.red("✗")} ${f.test} ${C.dim(">")} ${f.message}`);
60504
+ for (const leak of result.leaks) console.log(` ${C.yellow("⚠")} ${C.yellow(leak)}`);
59477
60505
  for (const b of result.benches) console.log(` ${C.cyan("⏱")} ${b.label} ${C.dim(`(n=${b.n})`)} ${C.dim("min")} ${fmt_ns(b.min)} ${C.dim("median")} ${fmt_ns(b.median)} ${C.dim("mean")} ${fmt_ns(Math.round(b.mean))} ${C.dim("max")} ${fmt_ns(b.max)} ${C.dim("±")} ${fmt_ns(Math.round(b.stddev))}`);
59478
60506
  if (result.other.length) {
59479
60507
  console.log(C.dim(" --- test stdout ---"));
@@ -59515,10 +60543,12 @@ function runTests(root, options = {}) {
59515
60543
  const totalTests = results.reduce((a, r) => a + r.tests.length, 0);
59516
60544
  const totalFailed = results.reduce((a, r) => a + r.tests.reduce((x, t) => x + t.failed, 0), 0);
59517
60545
  const totalPassed = totalTests - totalFailed;
60546
+ const totalLeaks = results.reduce((a, r) => a + r.leaks.length, 0);
59518
60547
  const anyFailed = results.some((r) => !r.ok);
59519
60548
  console.log("");
59520
60549
  console.log(` ${C.dim("Files ")} ${C.green(`${totalFilesPassed} passed`)}` + (totalFilesFailed ? ` ${C.dim("|")} ${C.red(`${totalFilesFailed} failed`)}` : "") + C.dim(` (${totalFiles})`));
59521
60550
  console.log(` ${C.dim("Tests ")} ${C.green(`${totalPassed} passed`)}` + (totalFailed ? ` ${C.dim("|")} ${C.red(`${totalFailed} failed`)}` : "") + C.dim(` (${totalTests})`));
60551
+ if (totalLeaks) console.log(` ${C.dim("Leaks ")} ${C.yellow(`${totalLeaks} reported`)}` + C.dim(" (--audit; leaked files count as failures)"));
59522
60552
  console.log(` ${C.dim(" Time ")} ${format_duration(elapsed)}`);
59523
60553
  console.log("");
59524
60554
  return !anyFailed;
@@ -59738,7 +60768,7 @@ function processFolder(folder, config, mode, program_args) {
59738
60768
  const dir = fs.opendirSync(folder);
59739
60769
  let dirent;
59740
60770
  while ((dirent = dir.readSync()) !== null) if (shouldProcessFile(dirent.name)) {
59741
- processFile(path.join(folder, dirent.name), config, mode, program_args, false);
60771
+ processFile(path.join(folder, dirent.name), config, mode, program_args);
59742
60772
  fs.watch;
59743
60773
  }
59744
60774
  dir.closeSync();
@@ -59746,45 +60776,15 @@ function processFolder(folder, config, mode, program_args) {
59746
60776
  function shouldProcessFile(filename) {
59747
60777
  return path.extname(filename) === SUPPORTED_EXTENSION;
59748
60778
  }
59749
- /**
59750
- * The project's entry file for `file_path`: walk up to the nearest
59751
- * `package.jsonc` and resolve its `entry` field, so any file inside a project
59752
- * is checked with the entry's import context (`import types` resolves
59753
- * against the project's `src/`, not the file's own folder). Returns
59754
- * undefined when the file is standalone, IS the entry, sits in an
59755
- * entry-less project (e.g. a library checkout), or the config is malformed.
59756
- */
59757
- function project_entry_for(file_path) {
59758
- let dir = path.dirname(file_path);
59759
- for (let i = 0; i < 20; i++) {
59760
- const config_path = path.join(dir, "package.jsonc");
59761
- if (fs.existsSync(config_path)) {
59762
- try {
59763
- const json = fs.readFileSync(config_path, "utf8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
59764
- const parsed = JSON.parse(json);
59765
- if (parsed.entry) {
59766
- const entry = path.resolve(dir, parsed.entry);
59767
- if (entry !== file_path && fs.existsSync(entry)) return entry;
59768
- }
59769
- } catch {}
59770
- return;
59771
- }
59772
- const parent = path.dirname(dir);
59773
- if (parent === dir) return void 0;
59774
- dir = parent;
59775
- }
59776
- }
59777
- function processFile(filename, config, mode, program_args, anchor_to_project = true) {
60779
+ function processFile(filename, config, mode, program_args) {
59778
60780
  console.log("Processing", filename);
59779
60781
  const arch = config.arch || "aarch64";
59780
60782
  const platform = config.platform || default_platform();
59781
60783
  const resolved = path.resolve(filename);
59782
60784
  if (!config.lib) config.lib = resolve_lib(resolved);
59783
- const anchor = anchor_to_project && !resolved.endsWith(".test.nm") ? project_entry_for(resolved) ?? resolved : resolved;
59784
- if (anchor !== resolved) console.log("Anchored to project entry", anchor);
59785
60785
  let startTime = performance.now();
59786
- const input = join$1(anchor, config.lib);
59787
- const parsed = parse(input, config.lib ? get_library(config.lib) : void 0, anchor);
60786
+ const input = join$1(resolved, config.lib);
60787
+ const parsed = parse(input, config.lib ? get_library(config.lib) : void 0, resolved);
59788
60788
  let errors = parsed.errors;
59789
60789
  if (errors.length) {
59790
60790
  console.log(render_errors(input, errors));