nomen-lang 0.6.11 → 0.6.13

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.
@@ -93,6 +93,35 @@ pub struct Buffer<T> {
93
93
  ```
94
94
  }
95
95
 
96
+ // Byte-width primitives (uint8 elements): the width-matched family the
97
+ // NEON planner's `.16b` descriptor keys on — one 16-byte group carries
98
+ // 16 lanes. `ldrb`/`strb` zero-extend/clobber exactly one byte.
99
+ internal inline func load_u8 = (self, int i: i >= 0 && i < self.cap, out uint8) {
100
+ ```
101
+ #arch: c
102
+ return ((unsigned char*)(unsigned long long)self->data)[i];
103
+ ```
104
+ ```
105
+ #arch: aarch64
106
+ ldr x0, [x19, #8]
107
+ add x0, x0, x1
108
+ ldrb w0, [x0]
109
+ ```
110
+ }
111
+
112
+ internal inline func store_u8 = (ref self, int i: i >= 0 && i < self.cap, uint8 val) {
113
+ ```
114
+ #arch: c
115
+ ((unsigned char*)(unsigned long long)self->data)[i] = val;
116
+ ```
117
+ ```
118
+ #arch: aarch64
119
+ ldr x0, [x19, #8]
120
+ add x0, x0, x1
121
+ strb w2, [x0]
122
+ ```
123
+ }
124
+
96
125
  internal func alloc_int = (ref self, int size, out int: out >= size) {
97
126
  self.cap = size
98
127
  self.data = calloc(size, 8)
package/dist/index.mjs CHANGED
@@ -4815,6 +4815,7 @@ function instr_defs(instr) {
4815
4815
  }
4816
4816
  return defs;
4817
4817
  }
4818
+ let store_store_kill_on = process.env.NOMEN_SS_KILL !== "0";
4818
4819
  function optimize_frame_slots(code) {
4819
4820
  const lines = code.split("\n");
4820
4821
  const out = [];
@@ -4839,14 +4840,26 @@ function optimize_frame_slots(code) {
4839
4840
  };
4840
4841
  /** A register is REDEFINED: pends sourced from it must reach memory now
4841
4842
  * (deferring would store the new value); availability held in it dies.
4842
- * Width siblings share fate (w/x and s/d views of one register). */
4843
- const clobber = (reg) => {
4843
+ * Width siblings share fate (w/x and s/d views of one register).
4844
+ * EXCEPTION (the store-store kill): when the block's NEXT access to
4845
+ * the pending slot is another STORE of the same family (no read, no
4846
+ * label/branch/call between), the orphaned store is unobservable —
4847
+ * the later store overwrites it before anything could read — and
4848
+ * drops instead of materializing. mul_to's carry-flag residue
4849
+ * (`str x0, [x29, #144]` twice per iteration of the hottest pidigits
4850
+ * loop, the value living in registers between) is exactly this
4851
+ * shape. */
4852
+ const clobber = (reg, at_idx = -1) => {
4844
4853
  if (!reg) return;
4845
4854
  const sib = sibling_reg$1(reg);
4846
4855
  zextW.delete(reg.startsWith("w") ? `x${reg.slice(1)}` : reg);
4847
4856
  if (sib && sib.startsWith("x")) zextW.delete(sib);
4848
4857
  for (const [off, s] of [...slots.entries()].sort((a, b) => a[0] - b[0])) {
4849
4858
  if (s.pendText !== void 0 && s.pendReg && (s.pendReg === reg || sib !== null && s.pendReg === sib)) {
4859
+ if (store_store_kill_on && at_idx >= 0 && s.pendIdx !== void 0 && no_slot_read_between(off, s.pendIdx + 1, at_idx) && next_slot_access_is_store(off, at_idx + 1)) {
4860
+ slots.delete(off);
4861
+ continue;
4862
+ }
4850
4863
  out.push(s.pendText);
4851
4864
  slots.delete(off);
4852
4865
  continue;
@@ -4857,7 +4870,44 @@ function optimize_frame_slots(code) {
4857
4870
  }
4858
4871
  }
4859
4872
  };
4860
- function process_frame_access(instr, off) {
4873
+ /** Whether any instruction in [from, to) READS slot `off` (or touches
4874
+ * it in a way the store-store reasoning cannot see through). */
4875
+ const no_slot_read_between = (off, from, to) => {
4876
+ for (let j = from; j < to && j < lines.length; j++) {
4877
+ const t = (lines[j] ?? "").replace(/\/\/.*$/, "").trim();
4878
+ if (!t) continue;
4879
+ const m = t.match(/^(str|stur|ldr|ldur|strb|ldrb|strh|ldrh)\s+\S+,\s*\[x29,\s*#(\d+)\]$/);
4880
+ if (m && parseInt(m[2], 10) === off && m[1].startsWith("ld")) return false;
4881
+ if (/^(ldp|stp)\b/.test(t) && t.includes(`#${off}`)) return false;
4882
+ if (/^add\b[^,]*,\s*x29\b/.test(t)) return false;
4883
+ if (/^(bl|blr|br|svc|ret)\b/.test(t)) return false;
4884
+ if (/^[A-Za-z_.$][\w.$]*:/.test(t)) return false;
4885
+ }
4886
+ return true;
4887
+ };
4888
+ const X29_SLOT_ANY = /\[x29,\s*#(\d+)\]/;
4889
+ /** Whether the next access to `off` in the current block (from
4890
+ * `from` to the next label/branch/call boundary) is a STORE. Loads,
4891
+ * block boundaries, escape-prone address builds, and anything
4892
+ * unparseable answer false (the conservative materialize). */
4893
+ const next_slot_access_is_store = (off, from) => {
4894
+ for (let j = from; j < lines.length; j++) {
4895
+ const t = (lines[j] ?? "").replace(/\/\/.*$/, "").trim();
4896
+ if (!t) continue;
4897
+ if (/^[A-Za-z_.$][\w.$]*:/.test(t)) return false;
4898
+ if (/^b/.test(t)) return false;
4899
+ if (/^(blr|br|svc|ret)\b/.test(t)) return false;
4900
+ if (/^add\b[^,]*,\s*x29\b/.test(t)) return false;
4901
+ if (/^(ldp|stp)\b/.test(t)) {
4902
+ const pm = t.match(X29_SLOT_ANY);
4903
+ return pm ? parseInt(pm[1], 10) === off && t.startsWith("stp") : false;
4904
+ }
4905
+ const m = t.match(/^(str|stur|ldr|ldur|strb|ldrb|strh|ldrh)\s+\S+,\s*\[x29,\s*#(\d+)\]$/);
4906
+ if (m) return parseInt(m[2], 10) === off && m[1].startsWith("str");
4907
+ }
4908
+ return false;
4909
+ };
4910
+ function process_frame_access(instr, off, i) {
4861
4911
  const data_reg = instr.operands[0];
4862
4912
  if (data_reg.kind !== "reg") {
4863
4913
  out.push(instr.text);
@@ -4880,7 +4930,7 @@ function optimize_frame_slots(code) {
4880
4930
  slots.set(off, fresh);
4881
4931
  return;
4882
4932
  }
4883
- clobber(data_reg.name);
4933
+ clobber(data_reg.name, i);
4884
4934
  }
4885
4935
  let s = slots.get(off);
4886
4936
  if (!s) {
@@ -4892,7 +4942,8 @@ function optimize_frame_slots(code) {
4892
4942
  slots.set(off, {
4893
4943
  pendKey: key,
4894
4944
  pendText: instr.text,
4895
- pendReg: data_reg.name
4945
+ pendReg: data_reg.name,
4946
+ pendIdx: i
4896
4947
  });
4897
4948
  return;
4898
4949
  }
@@ -4952,7 +5003,7 @@ function optimize_frame_slots(code) {
4952
5003
  const off = Number(instr.operands[2].value);
4953
5004
  const next = parse_asm_instruction(lines[i + 1] ?? "", i + 2);
4954
5005
  if (next && (next.op === "ldr" || next.op === "str") && next.operands.length === 2 && next.operands[1].kind === "mem" && next.operands[1].base === scratch && !next.operands[1].offset && !next.operands[1].scale && !next.operands[1].writeback) {
4955
- clobber(scratch);
5006
+ clobber(scratch, i);
4956
5007
  out.push(text);
4957
5008
  const new_text = next.text.replace(/\[[^\]]*\]/, `[x29, #${off}]`);
4958
5009
  process_frame_access({
@@ -4966,11 +5017,11 @@ function optimize_frame_slots(code) {
4966
5017
  value: BigInt(off)
4967
5018
  }
4968
5019
  }]
4969
- }, off);
5020
+ }, off, i);
4970
5021
  i++;
4971
5022
  continue;
4972
5023
  }
4973
- clobber(scratch);
5024
+ clobber(scratch, i);
4974
5025
  flush_all();
4975
5026
  clear_all();
4976
5027
  out.push(text);
@@ -4991,7 +5042,7 @@ function optimize_frame_slots(code) {
4991
5042
  continue;
4992
5043
  }
4993
5044
  if (mem && mem.kind === "mem" && mem.base === "x29" && mem.offset?.kind === "imm" && !mem.writeback) {
4994
- process_frame_access(instr, Number(mem.offset.value));
5045
+ process_frame_access(instr, Number(mem.offset.value), i);
4995
5046
  continue;
4996
5047
  }
4997
5048
  if (mem && mem.kind === "mem" && (mem.base === "x29" || mem.base === "fp") && (!mem.offset || mem.offset.kind === "reg" || mem.writeback)) {
@@ -5001,7 +5052,7 @@ function optimize_frame_slots(code) {
5001
5052
  continue;
5002
5053
  }
5003
5054
  if (op === "bl" || op === "blr" || op === "br" || op === "svc") {
5004
- for (const r of CALLER_SAVED) clobber(r);
5055
+ for (const r of CALLER_SAVED) clobber(r, i);
5005
5056
  out.push(text);
5006
5057
  continue;
5007
5058
  }
@@ -5010,7 +5061,7 @@ function optimize_frame_slots(code) {
5010
5061
  out.push(text);
5011
5062
  continue;
5012
5063
  }
5013
- for (const d of instr_defs(instr)) clobber(d);
5064
+ for (const d of instr_defs(instr)) clobber(d, i);
5014
5065
  const load_dst = instr.operands[0];
5015
5066
  if ((op === "ldrb" || op === "ldrh" || op === "ldr") && load_dst.kind === "reg" && /^w\d+$/.test(load_dst.name)) zextW.set(`x${load_dst.name.slice(1)}`, op === "ldrb" ? 8 : op === "ldrh" ? 16 : 32);
5016
5067
  out.push(text);
@@ -20884,10 +20935,12 @@ function build_function_node$1(node, status) {
20884
20935
  if (moved_param_save_slots.size > 0 && node.name !== "main") {
20885
20936
  const need_guard = return_is_class;
20886
20937
  const need_save = !!node.return_type?.name;
20938
+ const returns_fat_pair = node.return_type?.name === "string" && !node.return_type.is_array;
20887
20939
  let return_save;
20888
20940
  if (need_guard || need_save) {
20889
- return_save = allocate_stack_space(status, 8);
20890
- emit_asm(status, `str x0, [x29, #${return_save}]\n`);
20941
+ return_save = allocate_stack_space(status, returns_fat_pair ? 16 : 8);
20942
+ if (returns_fat_pair) emit_asm(status, `stp x0, x1, [x29, #${return_save}]\n`);
20943
+ else emit_asm(status, `str x0, [x29, #${return_save}]\n`);
20891
20944
  }
20892
20945
  for (const [name, info] of moved_param_save_slots) {
20893
20946
  if (moved_set?.has(name)) continue;
@@ -20913,7 +20966,10 @@ function build_function_node$1(node, status) {
20913
20966
  emit_free(status);
20914
20967
  if (need_guard) emit_asm(status, `${keep_prefix}_${name}:\n`);
20915
20968
  }
20916
- if (need_guard || need_save) emit_asm(status, `ldr x0, [x29, #${return_save}]\n`);
20969
+ if (need_guard || need_save) {
20970
+ if (returns_fat_pair) emit_asm(status, `ldp x0, x1, [x29, #${return_save}]\n`);
20971
+ else emit_asm(status, `ldr x0, [x29, #${return_save}]\n`);
20972
+ }
20917
20973
  }
20918
20974
  const total_stack = Math.ceil((status.stack_size || 0) / 16) * 16;
20919
20975
  status.code = status.code.replace(`sub sp, sp, #${stack_placeholder}`, total_stack > 0 ? `sub sp, sp, #${total_stack}` : `// no stack needed`);
@@ -22306,11 +22362,13 @@ function build_struct_functions$1(node, status) {
22306
22362
  if (moved_param_save_slots.size > 0) {
22307
22363
  const ret_is_class = !!func.return_type?.name && !!status.structs.find((s) => s.name === func.return_type.name && s.is_class);
22308
22364
  const need_save = !!func.return_type?.name;
22365
+ const returns_fat_pair = func.return_type?.name === "string" && !func.return_type.is_array;
22309
22366
  const keep_prefix = `.Lkeep_mparam_${func_label.replace(/[^\w]/g, "_")}`;
22310
22367
  let return_save;
22311
22368
  if (ret_is_class || need_save) {
22312
- return_save = allocate_stack_space(status, 8);
22313
- emit_asm(status, `str x0, [x29, #${return_save}]\n`);
22369
+ return_save = allocate_stack_space(status, returns_fat_pair ? 16 : 8);
22370
+ if (returns_fat_pair) emit_asm(status, `stp x0, x1, [x29, #${return_save}]\n`);
22371
+ else emit_asm(status, `str x0, [x29, #${return_save}]\n`);
22314
22372
  }
22315
22373
  for (const [name, info] of moved_param_save_slots) {
22316
22374
  if (status.moved?.has(name) && !moved_before.has(name)) continue;
@@ -22336,7 +22394,10 @@ function build_struct_functions$1(node, status) {
22336
22394
  emit_free(status);
22337
22395
  if (ret_is_class) emit_asm(status, `${keep_prefix}_${name}:\n`);
22338
22396
  }
22339
- if (ret_is_class || need_save) emit_asm(status, `ldr x0, [x29, #${return_save}]\n`);
22397
+ if (ret_is_class || need_save) {
22398
+ if (returns_fat_pair) emit_asm(status, `ldp x0, x1, [x29, #${return_save}]\n`);
22399
+ else emit_asm(status, `ldr x0, [x29, #${return_save}]\n`);
22400
+ }
22340
22401
  }
22341
22402
  const total_stack = Math.ceil((status.stack_size || 0) / 16) * 16;
22342
22403
  status.code = status.code.replace(`sub sp, sp, #${stack_placeholder}`, total_stack > 0 ? `sub sp, sp, #${total_stack}` : `// no stack needed`);
@@ -25241,11 +25302,15 @@ function lane_reg_of(name, plan) {
25241
25302
  const idx = temps.indexOf(name);
25242
25303
  return TEMP_REGS[idx] ?? "v4";
25243
25304
  }
25244
- function emit_lane_expr(e, status, plan, buffer_regs, idx, depth) {
25305
+ function emit_lane_expr(e, status, plan, buffer_regs, shifted_regs, idx, depth) {
25245
25306
  switch (e.k) {
25246
- case "load":
25247
- emit_asm(status, `ldr q0, [${buffer_regs.get(e.buffer) ?? "x11"}, ${idx}, lsl #4]\n`);
25307
+ case "load": {
25308
+ const key = `${e.buffer} ${e.shift}`;
25309
+ const shifted = shifted_regs.get(key);
25310
+ if (shifted) emit_asm(status, `ldr q0, [${shifted}, ${idx}, lsl #4]\n`);
25311
+ else emit_asm(status, `ldr q0, [${buffer_regs.get(e.buffer) ?? "x11"}, ${idx}, lsl #4]\n`);
25248
25312
  return;
25313
+ }
25249
25314
  case "lit":
25250
25315
  case "scalar":
25251
25316
  if (plan.elem.float) {
@@ -25254,7 +25319,7 @@ function emit_lane_expr(e, status, plan, buffer_regs, idx, depth) {
25254
25319
  } else {
25255
25320
  build_node(e.node, status);
25256
25321
  ensure_newline(status);
25257
- emit_asm(status, `dup v0.${plan.elem.arr}, ${plan.elem.arr === "4s" ? "w0" : "x0"}\n`);
25322
+ emit_asm(status, `dup v0.${plan.elem.arr}, ${plan.elem.arr === "2d" ? "x0" : "w0"}\n`);
25258
25323
  }
25259
25324
  return;
25260
25325
  case "temp":
@@ -25267,11 +25332,11 @@ function emit_lane_expr(e, status, plan, buffer_regs, idx, depth) {
25267
25332
  const right_mul = left_mul ? null : mul_of(e.right);
25268
25333
  const mul = left_mul ?? right_mul;
25269
25334
  if (mul && (e.op === "+" || !left_mul)) {
25270
- emit_lane_expr(left_mul ? e.right : e.left, status, plan, buffer_regs, idx, depth + 1);
25335
+ emit_lane_expr(left_mul ? e.right : e.left, status, plan, buffer_regs, shifted_regs, idx, depth + 1);
25271
25336
  emit_asm(status, `mov v8.16b, v0.16b\n`);
25272
- emit_lane_expr(mul.left, status, plan, buffer_regs, idx, depth + 1);
25337
+ emit_lane_expr(mul.left, status, plan, buffer_regs, shifted_regs, idx, depth + 1);
25273
25338
  emit_asm(status, `mov ${spill_reg(depth + 1)}.16b, v0.16b\n`);
25274
- emit_lane_expr(mul.right, status, plan, buffer_regs, idx, depth + 1);
25339
+ emit_lane_expr(mul.right, status, plan, buffer_regs, shifted_regs, idx, depth + 1);
25275
25340
  emit_asm(status, `mov v1.16b, v0.16b\n`);
25276
25341
  emit_asm(status, `mov v0.16b, ${spill_reg(depth + 1)}.16b\n`);
25277
25342
  emit_asm(status, `${e.op === "+" ? "fmla" : "fmls"} v8.2d, v0.2d, v1.2d\n`);
@@ -25279,9 +25344,9 @@ function emit_lane_expr(e, status, plan, buffer_regs, idx, depth) {
25279
25344
  return;
25280
25345
  }
25281
25346
  }
25282
- emit_lane_expr(e.left, status, plan, buffer_regs, idx, depth + 1);
25347
+ emit_lane_expr(e.left, status, plan, buffer_regs, shifted_regs, idx, depth + 1);
25283
25348
  emit_asm(status, `mov ${spill_reg(depth)}.16b, v0.16b\n`);
25284
- emit_lane_expr(e.right, status, plan, buffer_regs, idx, depth + 1);
25349
+ emit_lane_expr(e.right, status, plan, buffer_regs, shifted_regs, idx, depth + 1);
25285
25350
  emit_asm(status, `mov v1.16b, v0.16b\n`);
25286
25351
  emit_asm(status, `mov v0.16b, ${spill_reg(depth)}.16b\n`);
25287
25352
  const { mnemonic, arr } = lane_op(e.op, plan.elem);
@@ -25336,19 +25401,19 @@ function lane_op(op, elem) {
25336
25401
  };
25337
25402
  }
25338
25403
  }
25339
- function emit_lane_stmt(lane, status, plan, buffer_regs, idx, acc_regs) {
25404
+ function emit_lane_stmt(lane, status, plan, buffer_regs, shifted_regs, idx, acc_regs) {
25340
25405
  if (lane.kind === "temp_def") {
25341
- emit_lane_expr(lane.value, status, plan, buffer_regs, idx, 0);
25406
+ emit_lane_expr(lane.value, status, plan, buffer_regs, shifted_regs, idx, 0);
25342
25407
  emit_asm(status, `mov ${lane_reg_of(lane.name, plan)}.16b, v0.16b\n`);
25343
25408
  return;
25344
25409
  }
25345
25410
  if (lane.kind === "reduction") {
25346
- emit_lane_expr(lane.operand, status, plan, buffer_regs, idx, 0);
25411
+ emit_lane_expr(lane.operand, status, plan, buffer_regs, shifted_regs, idx, 0);
25347
25412
  const acc = acc_regs.get(lane.name) ?? "v2";
25348
25413
  emit_asm(status, `${lane.op === "+" ? plan.elem.float ? "fadd" : "add" : "fmul"} ${acc}.${plan.elem.arr}, ${acc}.${plan.elem.arr}, v0.${plan.elem.arr}\n`);
25349
25414
  return;
25350
25415
  }
25351
- emit_lane_expr(lane.value, status, plan, buffer_regs, idx, 0);
25416
+ emit_lane_expr(lane.value, status, plan, buffer_regs, shifted_regs, idx, 0);
25352
25417
  emit_asm(status, `str q0, [${buffer_regs.get(lane.buffer) ?? "x11"}, ${idx}, lsl #4]\n`);
25353
25418
  }
25354
25419
  /**
@@ -25381,6 +25446,37 @@ function emit_neon_vector_loop(plan, status) {
25381
25446
  emit_asm(status, `ldr ${reg}, [x9, #8]\n`);
25382
25447
  buffer_regs.set(b.name, reg);
25383
25448
  });
25449
+ const SHIFT_PTR_REGS = [
25450
+ "x15",
25451
+ "x16",
25452
+ "x17"
25453
+ ];
25454
+ const shifted_regs = /* @__PURE__ */ new Map();
25455
+ {
25456
+ const elem_size = 16 / plan.elem.group_elems;
25457
+ let spi = 0;
25458
+ const seen = /* @__PURE__ */ new Set();
25459
+ const visit = (e) => {
25460
+ if (!e) return;
25461
+ if (e.k === "load" && e.shift > 0) {
25462
+ const key = `${e.buffer} ${e.shift}`;
25463
+ if (!seen.has(key)) {
25464
+ seen.add(key);
25465
+ if (spi >= SHIFT_PTR_REGS.length) return;
25466
+ const base = buffer_regs.get(e.buffer) ?? "x11";
25467
+ const reg = SHIFT_PTR_REGS[spi++];
25468
+ emit_asm(status, `add ${reg}, ${base}, #${e.shift * elem_size}\n`);
25469
+ shifted_regs.set(key, reg);
25470
+ }
25471
+ } else if (e.k === "op") {
25472
+ visit(e.left);
25473
+ visit(e.right);
25474
+ }
25475
+ };
25476
+ for (const lane of plan.lanes) if (lane.kind === "store") visit(lane.value);
25477
+ else if (lane.kind === "temp_def") visit(lane.value);
25478
+ else if (lane.kind === "reduction") visit(lane.operand);
25479
+ }
25384
25480
  const acc_regs = /* @__PURE__ */ new Map();
25385
25481
  plan.reductions.forEach((r, i) => {
25386
25482
  const reg = REDUCTION_REGS[i] ?? "v2";
@@ -25390,12 +25486,13 @@ function emit_neon_vector_loop(plan, status) {
25390
25486
  } else {
25391
25487
  build_node(r.init_node, status);
25392
25488
  ensure_newline(status);
25393
- emit_asm(status, `dup ${reg}.${plan.elem.arr}, ${plan.elem.arr === "4s" ? "w0" : "x0"}\n`);
25489
+ emit_asm(status, `dup ${reg}.${plan.elem.arr}, ${plan.elem.arr === "2d" ? "x0" : "w0"}\n`);
25394
25490
  }
25395
25491
  acc_regs.set(r.name, reg);
25396
25492
  });
25397
25493
  build_node(plan.bound_node, status);
25398
25494
  ensure_newline(status);
25495
+ if (plan.max_shift > 0) emit_asm(status, `sub x0, x0, #${plan.max_shift}\n`);
25399
25496
  emit_asm(status, `mov x9, x0\n`);
25400
25497
  emit_asm(status, `asr x9, x9, #${shift}\n`);
25401
25498
  emit_asm(status, `bic x9, x9, #1\n`);
@@ -25404,9 +25501,9 @@ function emit_neon_vector_loop(plan, status) {
25404
25501
  emit_asm(status, `${label}:\n`);
25405
25502
  emit_asm(status, `cmp x10, x9\n`);
25406
25503
  emit_asm(status, `b.hs ${label}_end\n`);
25407
- for (const lane of plan.lanes) emit_lane_stmt(lane, status, plan, buffer_regs, "x10", acc_regs);
25504
+ for (const lane of plan.lanes) emit_lane_stmt(lane, status, plan, buffer_regs, shifted_regs, "x10", acc_regs);
25408
25505
  emit_asm(status, `add x14, x10, #1\n`);
25409
- for (const lane of plan.lanes) emit_lane_stmt(lane, status, plan, buffer_regs, "x14", acc_regs);
25506
+ for (const lane of plan.lanes) emit_lane_stmt(lane, status, plan, buffer_regs, shifted_regs, "x14", acc_regs);
25410
25507
  emit_asm(status, `add x10, x10, #2\n`);
25411
25508
  emit_asm(status, `b ${label}\n`);
25412
25509
  emit_asm(status, `${label}_end:\n`);
@@ -27213,8 +27310,9 @@ function flag_form_enabled() {
27213
27310
  *
27214
27311
  * Element kinds (tranche 2): the method pair determines the descriptor —
27215
27312
  * `load_float`/`store_float` (f64, `.2d`), `load_int`/`store_int` (8-byte
27216
- * int, `.2d`), `load`/`store` (4-byte int, `.4s`). All accesses in one
27217
- * loop must agree on the descriptor; lanes per group follow the width.
27313
+ * int, `.2d`), `load_u32`/`store_u32` (4-byte int, `.4s`),
27314
+ * `load_u8`/`store_u8` (byte, `.16b` — 16 lanes per group). All accesses in
27315
+ * one loop must agree on the descriptor; lanes per group follow the width.
27218
27316
  *
27219
27317
  * Soundness model (why this is legal):
27220
27318
  *
@@ -27223,12 +27321,24 @@ function flag_form_enabled() {
27223
27321
  * a vector iteration touches the same element set the scalar iterations
27224
27322
  * it replaces would. Lane k reads element base+k before lane k writes
27225
27323
  * element base+k; no lane reads an element another lane writes, whatever
27226
- * buffers alias. This is why shifted indices (`load_float(i + 1)`) are
27227
- * rejected outright.
27324
+ * buffers alias.
27325
+ * - SHIFTED READS (per-element event-order rule): `load_T(i + c)` with a
27326
+ * LITERAL c >= 1 is admitted. Scalar iteration k reads base+k+c, which
27327
+ * (c > 0) is written — if at all — at iteration k+c, AFTER the read; the
27328
+ * vector group loads every lane before any lane stores, so the group
27329
+ * sees exactly the scalar pre-group values (reads of elements the group
27330
+ * itself writes land on their pre-group contents, matching scalar
27331
+ * order). c <= 0 reads elements an EARLIER same-group lane wrote — the
27332
+ * scalar loop would see the fresh value, the vector lanes would not —
27333
+ * refused. STORES stay exact-induction. The trip limit shrinks by the
27334
+ * max shift (max lane read = base+group-1+max_shift stays < bound);
27335
+ * the scalar tail runs the original loop verbatim, so its own guard
27336
+ * still covers the remainder.
27228
27337
  * - TRIP SEMANTICS: the vector loop runs whole 16-byte groups (unrolled to
27229
- * two groups per iteration); the limit is floor(N / group) rounded down
27230
- * to a multiple of the unroll — floor semantics keep every lane index
27231
- * < N for ANY signed bound (negative → loop skipped). The ORIGINAL
27338
+ * two groups per iteration); the limit is floor((N - max_shift) / group)
27339
+ * rounded down to a multiple of the unroll — floor semantics keep every
27340
+ * lane index (+ its read shift) < N for ANY signed bound (negative →
27341
+ * loop skipped). The ORIGINAL
27232
27342
  * scalar loop is emitted unchanged afterwards as the tail (the induction
27233
27343
  * is synced to the vector loop's exit counter first), covering the
27234
27344
  * remainder — including the sub-unroll and negative-N cases where the
@@ -27262,6 +27372,9 @@ function flag_form_enabled() {
27262
27372
  /** Max distinct Buffers per plan — the preheader pins each data pointer in
27263
27373
  * a dedicated scratch register (x11..x13). */
27264
27374
  const MAX_BUFFERS = 3;
27375
+ /** Adjusted read-pointer registers the emitter can materialize (one per
27376
+ * distinct (buffer, shift > 0) pair -- x15/x16/x17, call-free bodies). */
27377
+ const MAX_SHIFTED_PTRS = 3;
27265
27378
  /** Max per-lane temps (v4..v7). */
27266
27379
  const MAX_TEMPS = 4;
27267
27380
  /** Max binary-op nesting depth in lane expressions (spill regs v14→v11). */
@@ -27295,10 +27408,18 @@ const ELEM_DESCS = [
27295
27408
  arr: "4s",
27296
27409
  group_elems: 4,
27297
27410
  shift: 2
27411
+ },
27412
+ {
27413
+ load: "load_u8",
27414
+ store: "store_u8",
27415
+ float: false,
27416
+ arr: "16b",
27417
+ group_elems: 16,
27418
+ shift: 4
27298
27419
  }
27299
27420
  ];
27300
27421
  function class_of_elem(elem) {
27301
- return elem.float ? "float" : elem.group_elems === 2 ? "e8" : "e4";
27422
+ return elem.float ? "float" : elem.group_elems === 2 ? "e8" : elem.group_elems === 4 ? "e4" : "e1";
27302
27423
  }
27303
27424
  function class_of_type_name(name) {
27304
27425
  if (!name) return null;
@@ -27307,6 +27428,7 @@ function class_of_type_name(name) {
27307
27428
  const size = aarch64_size(name);
27308
27429
  if (size === 8) return "e8";
27309
27430
  if (size === 4) return "e4";
27431
+ if (size === 1) return "e1";
27310
27432
  } catch {}
27311
27433
  return null;
27312
27434
  }
@@ -27558,7 +27680,15 @@ function alloc_temps_of(s, walk) {
27558
27680
  if (has_allocations(d.value)) return null;
27559
27681
  const cls = class_of_type_name(d.type?.name);
27560
27682
  if (!cls) return null;
27561
- if (walk.elem && cls !== class_of_elem(walk.elem)) return null;
27683
+ if (walk.elem && cls !== class_of_elem(walk.elem)) {
27684
+ const nir = ast_to_nir_shallow(d.value);
27685
+ if (!nir || shifted_index_of(nir, walk.induction) === null) return null;
27686
+ out.push({
27687
+ name: d.name,
27688
+ value: d.value
27689
+ });
27690
+ continue;
27691
+ }
27562
27692
  walk.temp_classes.set(d.name, cls);
27563
27693
  out.push({
27564
27694
  name: d.name,
@@ -27567,6 +27697,24 @@ function alloc_temps_of(s, walk) {
27567
27697
  }
27568
27698
  return out;
27569
27699
  }
27700
+ /** `i + c` with i the induction and c an int literal >= 1 -- the shifted
27701
+ * read index (event-order rule: scalar iteration k reads base+k+c before
27702
+ * iteration k+c writes it; the vector group loads all lanes before any
27703
+ * stores, so the group sees exactly the scalar pre-group values). */
27704
+ function shifted_index_of(e, induction) {
27705
+ if (e.kind !== "binary") return null;
27706
+ if (e.node.op !== "+") return null;
27707
+ const lit_of = (x) => {
27708
+ if (x.kind !== "leaf" || x.name) return null;
27709
+ const v = node_value(x);
27710
+ if (v === null || !/^\d+$/.test(v)) return null;
27711
+ return Number(v);
27712
+ };
27713
+ const base_of = (x) => x.kind === "leaf" && x.name === induction;
27714
+ const c = base_of(e.left) ? lit_of(e.right) : base_of(e.right) ? lit_of(e.left) : null;
27715
+ if (c === null || c < 1) return null;
27716
+ return c;
27717
+ }
27570
27718
  function lane_expr(e, walk, induction, status, depth) {
27571
27719
  if (depth > walk.max_depth) walk.max_depth = depth;
27572
27720
  if (depth > MAX_OP_DEPTH) return null;
@@ -27577,13 +27725,34 @@ function lane_expr(e, walk, induction, status, depth) {
27577
27725
  if (e.receiver.kind !== "leaf" || !e.receiver.name) return null;
27578
27726
  if (e.facts.args.length !== 1) return null;
27579
27727
  const idx = e.facts.args[0];
27580
- if (idx.kind !== "leaf" || idx.name !== induction) return null;
27728
+ let shift = 0;
27729
+ if (idx.kind === "leaf") {
27730
+ if (idx.name !== induction) {
27731
+ const aliased = walk.induction_alias.get(idx.name);
27732
+ if (aliased === void 0) return null;
27733
+ shift = aliased;
27734
+ }
27735
+ } else if (idx.kind === "binary") {
27736
+ const c = shifted_index_of(idx, induction);
27737
+ if (c === null) return null;
27738
+ shift = c;
27739
+ } else return null;
27581
27740
  if (!is_buffer_type_name(type_from_value_node$1(e.receiver.node)?.name ?? "")) return null;
27582
27741
  if (buffer_index_of(walk, e.receiver.name, e.receiver.node) === null) return null;
27742
+ if (shift > 0) {
27743
+ const key = `${e.receiver.name} ${shift}`;
27744
+ if (!walk.shifted.has(key)) walk.shifted.set(key, {
27745
+ buffer: e.receiver.name,
27746
+ node: e.receiver.node,
27747
+ shift
27748
+ });
27749
+ if (walk.shifted.size > MAX_SHIFTED_PTRS) return null;
27750
+ }
27583
27751
  return {
27584
27752
  k: "load",
27585
27753
  buffer: e.receiver.name,
27586
- node: e.receiver.node
27754
+ node: e.receiver.node,
27755
+ shift
27587
27756
  };
27588
27757
  }
27589
27758
  case "leaf": {
@@ -27780,6 +27949,26 @@ function extract_bound$1(bound, induction) {
27780
27949
  name: null
27781
27950
  };
27782
27951
  }
27952
+ if (bound.kind === "binary") {
27953
+ const op_node = bound.node;
27954
+ if (op_node.op !== "+" && op_node.op !== "-") return null;
27955
+ const lit_of = (x) => x.kind === "leaf" && !x.name && node_value(x) !== null && INT_LIT_RE.test(node_value(x));
27956
+ const base_name_of = (x) => {
27957
+ if (x.kind === "path") return x.receiver.kind === "leaf" && x.receiver.name ? x.receiver.name : null;
27958
+ if (x.kind === "leaf") return x.name ?? null;
27959
+ return null;
27960
+ };
27961
+ let base = null;
27962
+ if (op_node.op === "+") {
27963
+ if (lit_of(bound.left)) base = base_name_of(bound.right);
27964
+ else if (lit_of(bound.right)) base = base_name_of(bound.left);
27965
+ } else if (lit_of(bound.right)) base = base_name_of(bound.left);
27966
+ if (base === null || base === induction) return null;
27967
+ return {
27968
+ node: bound.node,
27969
+ name: base
27970
+ };
27971
+ }
27783
27972
  if (bound.kind !== "path") return null;
27784
27973
  const root = bound.receiver;
27785
27974
  if (root.kind !== "leaf" || !root.name || root.name === induction) return null;
@@ -27834,6 +28023,9 @@ function plan_common(lanes_raw, bound_node, bound_name, induction, index, list,
27834
28023
  lanes: [],
27835
28024
  buffers: [],
27836
28025
  buffer_index: /* @__PURE__ */ new Map(),
28026
+ shifted: /* @__PURE__ */ new Map(),
28027
+ induction_alias: /* @__PURE__ */ new Map(),
28028
+ induction,
27837
28029
  temps: [],
27838
28030
  defs,
27839
28031
  defed_so_far: /* @__PURE__ */ new Set(),
@@ -27849,9 +28041,13 @@ function plan_common(lanes_raw, bound_node, bound_name, induction, index, list,
27849
28041
  }
27850
28042
  continue;
27851
28043
  }
28044
+ if (process.env.NOMEN_DBG_NEON) console.error(`NEON: [${status.current_function_name}] refuse: stmt kind ${s.kind}`);
28045
+ return null;
28046
+ }
28047
+ if (bound_name !== null && defs.has(bound_name)) {
28048
+ if (process.env.NOMEN_DBG_NEON) console.error(`NEON: [${status.current_function_name}] refuse: bound ${bound_name} defined in loop`);
27852
28049
  return null;
27853
28050
  }
27854
- if (bound_name !== null && defs.has(bound_name)) return null;
27855
28051
  const reduction_inits = /* @__PURE__ */ new Map();
27856
28052
  for (const name of reduction_candidates.keys()) {
27857
28053
  const scan = scan_init(list, index, name);
@@ -27862,6 +28058,9 @@ function plan_common(lanes_raw, bound_node, bound_name, induction, index, list,
27862
28058
  lanes: [],
27863
28059
  buffers: [],
27864
28060
  buffer_index: /* @__PURE__ */ new Map(),
28061
+ shifted: /* @__PURE__ */ new Map(),
28062
+ induction_alias: /* @__PURE__ */ new Map(),
28063
+ induction,
27865
28064
  temps: [],
27866
28065
  defs,
27867
28066
  defed_so_far: /* @__PURE__ */ new Set(),
@@ -27941,8 +28140,27 @@ function plan_common(lanes_raw, bound_node, bound_name, induction, index, list,
27941
28140
  for (const t of temps) {
27942
28141
  const nir = ast_to_nir_shallow(t.value);
27943
28142
  if (!nir) return null;
28143
+ const add = shifted_index_of(nir, induction);
28144
+ if (add !== null) {
28145
+ walk.induction_alias.set(t.name, add);
28146
+ continue;
28147
+ }
27944
28148
  const tvalue = lane_expr(nir, walk, induction, status, 0);
27945
- if (!tvalue) return null;
28149
+ if (!tvalue) {
28150
+ if (process.env.NOMEN_DBG_NEON) {
28151
+ const dump = (x, d) => {
28152
+ if (!x || d > 3) return "?";
28153
+ const nt = x.node?.node_type ?? "?";
28154
+ const op = x.node?.op ?? x.node?.operator ?? "";
28155
+ const val = x.node?.node_type === "value" ? `=${x.node.value}` : "";
28156
+ const name = x.name ? `@${x.name}` : "";
28157
+ const kids = x.left || x.right ? `(${dump(x.left, d + 1)} ${dump(x.right, d + 1)})` : x.inner !== void 0 ? `(${dump(x.inner, d + 1)})` : x.facts ? `(${x.args?.map((a) => dump(a, d + 1)).join(",")})` : "";
28158
+ return `<${x.kind} ${nt}${op ? ":" + op : ""}${val}${name}${kids}>`;
28159
+ };
28160
+ console.error(`NEON: temp refused t=${t.name} ${dump(nir, 0)}`);
28161
+ }
28162
+ return null;
28163
+ }
27946
28164
  walk.defed_so_far.add(t.name);
27947
28165
  walk.temps.push(t.name);
27948
28166
  walk.lanes.push({
@@ -27960,17 +28178,41 @@ function plan_common(lanes_raw, bound_node, bound_name, induction, index, list,
27960
28178
  value
27961
28179
  });
27962
28180
  }
27963
- if (walk.temps.length > MAX_TEMPS) return null;
27964
- if (!walk.lanes.some((l) => l.kind === "store") && reductions.length === 0) return null;
28181
+ if (walk.temps.length > MAX_TEMPS) {
28182
+ if (process.env.NOMEN_DBG_NEON) console.error(`NEON: [${status.current_function_name}] refuse: too many temps`);
28183
+ return null;
28184
+ }
28185
+ if (!walk.lanes.some((l) => l.kind === "store") && reductions.length === 0) {
28186
+ if (process.env.NOMEN_DBG_NEON) console.error(`NEON: [${status.current_function_name}] refuse: no store lane`);
28187
+ return null;
28188
+ }
27965
28189
  if (!walk.elem) return null;
27966
28190
  const want = class_of_elem(walk.elem);
27967
- for (const cls of walk.temp_classes.values()) if (cls !== want) return null;
28191
+ for (const cls of walk.temp_classes.values()) if (cls !== want) {
28192
+ if (process.env.NOMEN_DBG_NEON) console.error(`NEON: [${status.current_function_name}] refuse: temp class ${cls} != ${want}`);
28193
+ return null;
28194
+ }
27968
28195
  if (reductions.length > MAX_REDUCTIONS) return null;
27969
28196
  if (walk.temps.length > 0) {
27970
28197
  const after = /* @__PURE__ */ new Set();
27971
28198
  for (let k = index + 1; k < list.length; k++) stmt_reads(list[k], after);
27972
- for (const t of walk.temps) if (after.has(t)) return null;
28199
+ for (const t of walk.temps) if (after.has(t)) {
28200
+ if (process.env.NOMEN_DBG_NEON) console.error(`NEON: [${status.current_function_name}] refuse: temp ${t} read after loop`);
28201
+ return null;
28202
+ }
27973
28203
  }
28204
+ let max_shift = 0;
28205
+ const scan_shift = (e) => {
28206
+ if (!e) return;
28207
+ if (e.k === "load" && e.shift > max_shift) max_shift = e.shift;
28208
+ if (e.k === "op") {
28209
+ scan_shift(e.left);
28210
+ scan_shift(e.right);
28211
+ }
28212
+ };
28213
+ for (const lane of walk.lanes) if (lane.kind === "store") scan_shift(lane.value);
28214
+ else if (lane.kind === "temp_def") scan_shift(lane.value);
28215
+ else if (lane.kind === "reduction") scan_shift(lane.operand);
27974
28216
  return {
27975
28217
  induction,
27976
28218
  bound_node,
@@ -27978,7 +28220,8 @@ function plan_common(lanes_raw, bound_node, bound_name, induction, index, list,
27978
28220
  buffers: walk.buffers,
27979
28221
  elem: walk.elem,
27980
28222
  reductions,
27981
- fma_contract: allow_reductions
28223
+ fma_contract: allow_reductions,
28224
+ max_shift
27982
28225
  };
27983
28226
  }
27984
28227
  /**
@@ -28006,6 +28249,7 @@ function plan_vector_loop(nstmt, index, list, status) {
28006
28249
  }
28007
28250
  const init = scan_init(list, index, induction);
28008
28251
  if (!init.ok || !init.init_node || !is_zero_ast_node(init.init_node)) return null;
28252
+ if (process.env.NOMEN_DBG_NEON) console.error(`NEON: [${status.current_function_name}] loop@${index} reaching plan_common`);
28009
28253
  return plan_common(lanes_raw, bound.node, bound.name, induction, index, list, status, status.fast_math === true);
28010
28254
  }
28011
28255
  /**
@@ -29638,6 +29882,12 @@ const active_splices = /* @__PURE__ */ new Set();
29638
29882
  function inline_splice_active(struct_name, func_name) {
29639
29883
  return active_splices.has(`${struct_name}.${func_name}`);
29640
29884
  }
29885
+ /** How many inline splices are currently open (build_access_node passes
29886
+ * this to is_auto_inline_method as the admission depth, so the emit-time
29887
+ * chain cap agrees with the plan-time recursion). */
29888
+ function inline_splice_depth() {
29889
+ return active_splices.size;
29890
+ }
29641
29891
  function begin_inline_splice(struct_name, func_name) {
29642
29892
  active_splices.add(`${struct_name}.${func_name}`);
29643
29893
  }
@@ -29741,6 +29991,11 @@ function build_inline_method(struct_node, func, status) {
29741
29991
  const old_return_buffer_offset = status.return_buffer_stack_offset;
29742
29992
  const old_function_return_type = status.function_return_type;
29743
29993
  const old_register_allocations = status.register_allocations;
29994
+ const old_nir_site_allocs = status.nir_site_allocs;
29995
+ const old_int_dest_hint = status.int_dest_hint;
29996
+ const old_float_dest_hint = status.float_dest_hint;
29997
+ status.int_dest_hint = void 0;
29998
+ status.float_dest_hint = void 0;
29744
29999
  const old_buffer_data_cache = status.buffer_data_cache;
29745
30000
  const old_buffer_fold_cache = status.buffer_fold_cache;
29746
30001
  const old_array_ptr_cache = status.array_ptr_cache;
@@ -29890,6 +30145,9 @@ function build_inline_method(struct_node, func, status) {
29890
30145
  status.return_buffer_stack_offset = old_return_buffer_offset;
29891
30146
  status.function_return_type = old_function_return_type;
29892
30147
  status.register_allocations = old_register_allocations;
30148
+ status.nir_site_allocs = old_nir_site_allocs;
30149
+ status.int_dest_hint = old_int_dest_hint;
30150
+ status.float_dest_hint = old_float_dest_hint;
29893
30151
  status.buffer_data_cache = old_buffer_data_cache;
29894
30152
  status.buffer_fold_cache = old_buffer_fold_cache;
29895
30153
  status.array_ptr_cache = old_array_ptr_cache;
@@ -29920,6 +30178,11 @@ function build_inline_function(func, status) {
29920
30178
  const old_return_buffer_offset = status.return_buffer_stack_offset;
29921
30179
  const old_function_return_type = status.function_return_type;
29922
30180
  const old_register_allocations = status.register_allocations;
30181
+ const old_nir_site_allocs = status.nir_site_allocs;
30182
+ const old_int_dest_hint = status.int_dest_hint;
30183
+ const old_float_dest_hint = status.float_dest_hint;
30184
+ status.int_dest_hint = void 0;
30185
+ status.float_dest_hint = void 0;
29923
30186
  const old_buffer_data_cache = status.buffer_data_cache;
29924
30187
  const old_buffer_fold_cache = status.buffer_fold_cache;
29925
30188
  const old_array_ptr_cache = status.array_ptr_cache;
@@ -30050,6 +30313,9 @@ function build_inline_function(func, status) {
30050
30313
  status.return_buffer_stack_offset = old_return_buffer_offset;
30051
30314
  status.function_return_type = old_function_return_type;
30052
30315
  status.register_allocations = old_register_allocations;
30316
+ status.nir_site_allocs = old_nir_site_allocs;
30317
+ status.int_dest_hint = old_int_dest_hint;
30318
+ status.float_dest_hint = old_float_dest_hint;
30053
30319
  status.buffer_data_cache = old_buffer_data_cache;
30054
30320
  status.buffer_fold_cache = old_buffer_fold_cache;
30055
30321
  status.array_ptr_cache = old_array_ptr_cache;
@@ -44257,6 +44523,8 @@ function load_nursery_struct_address(target, status) {
44257
44523
  const MAX_STATEMENTS = 15;
44258
44524
  const MAX_AUTO_METHOD_STATEMENTS = 3;
44259
44525
  const MAX_AUTO_METHOD_PARAMS = 3;
44526
+ const MAX_AUTO_CALLING_STATEMENTS = 10;
44527
+ const MAX_AUTO_SPLICE_DEPTH = 2;
44260
44528
  /**
44261
44529
  * ensure/grow/clear-shaped methods (ASM_PLAN_7 tranche 7): small real
44262
44530
  * bodies the method-inline path can splice WITHOUT the explicit `inline`
@@ -44268,11 +44536,19 @@ const MAX_AUTO_METHOD_PARAMS = 3;
44268
44536
  * scalar-or-void return. The standalone body is still emitted — trait
44269
44537
  * dispatch, method values, and overflow-arg call sites keep taking the
44270
44538
  * `bl`, so nothing here may skip emission.
44539
+ *
44540
+ * `depth` is the number of inline splices already open at the dispatch
44541
+ * site (build_access_node passes `active_splices.size`); the admission
44542
+ * recursion for a candidate's callees runs at `depth + 1`, so a chain
44543
+ * deeper than MAX_AUTO_SPLICE_DEPTH is refused everywhere — the plan-time
44544
+ * and emit-time verdicts then agree.
44271
44545
  */
44272
- function is_auto_inline_method(func) {
44546
+ function is_auto_inline_method(func, depth = 0) {
44273
44547
  if (!func.has_body) return false;
44274
44548
  if (func.is_inline) return false;
44275
- if (func.statements.length === 0 || func.statements.length > MAX_AUTO_METHOD_STATEMENTS) return false;
44549
+ const calls = body_calls(func.statements);
44550
+ const max_statements = calls || depth > 0 ? MAX_AUTO_CALLING_STATEMENTS : MAX_AUTO_METHOD_STATEMENTS;
44551
+ if (func.statements.length === 0 || func.statements.length > max_statements) return false;
44276
44552
  if (func.returns_move) return false;
44277
44553
  if (func.statements.some((s) => s.node_type === "raw")) return false;
44278
44554
  if (func.params.length > MAX_AUTO_METHOD_PARAMS) return false;
@@ -44295,22 +44571,55 @@ function is_auto_inline_method(func) {
44295
44571
  if (func.return_type?.is_array || func.return_type?.is_view) return false;
44296
44572
  if (func.return_type?.is_nullable) return false;
44297
44573
  if (func.return_type?.name && !SIMPLE_TYPES.includes(func.return_type.name)) return false;
44298
- if (body_has_call(func.statements)) return false;
44299
- return true;
44574
+ if (!calls) return true;
44575
+ if (depth >= MAX_AUTO_SPLICE_DEPTH) return false;
44576
+ return body_calls_splice_through(func.statements, depth);
44300
44577
  }
44301
- /** Whether the subtree contains any call (method or function). */
44302
- function body_has_call(node) {
44578
+ function body_calls(node) {
44303
44579
  if (!node) return false;
44304
44580
  if (Array.isArray(node)) {
44305
- for (const item of node) if (body_has_call(item)) return true;
44581
+ for (const item of node) if (body_calls(item)) return true;
44306
44582
  return false;
44307
44583
  }
44308
44584
  if (typeof node !== "object") return false;
44309
44585
  const nt = node.node_type;
44310
44586
  if (nt === "access_func" || nt === "func_call") return true;
44311
- for (const child of child_nodes(node)) if (body_has_call(child)) return true;
44587
+ for (const child of child_nodes(node)) if (body_calls(child)) return true;
44312
44588
  return false;
44313
44589
  }
44590
+ /**
44591
+ * The call-bearing cost model: every call in the subtree must either be
44592
+ * an `extern` (a `bl` whether the host splices or not — the expansion
44593
+ * changes nothing about the call count) or splice through — the callee is
44594
+ * a user-`inline` method (the splice path already handles call-bearing
44595
+ * user bodies) or itself admitted as an auto candidate at `depth + 1`.
44596
+ * Anything else keeps the host on the real `bl` path: expanding a call
44597
+ * whose callee cannot splice replaces one `bl` with the same `bl` plus
44598
+ * parking, which is the shape the tranche-7 receipts measured as a loss.
44599
+ *
44600
+ * `resolved_function` is the checker's stamp — present on every call the
44601
+ * checker accepted, and clone_node preserves it. A call without one is
44602
+ * refused (conservative).
44603
+ */
44604
+ function body_calls_splice_through(node, depth) {
44605
+ if (!node) return true;
44606
+ if (Array.isArray(node)) {
44607
+ for (const item of node) if (!body_calls_splice_through(item, depth)) return false;
44608
+ return true;
44609
+ }
44610
+ if (typeof node !== "object") return true;
44611
+ const any_node = node;
44612
+ const nt = any_node.node_type;
44613
+ if (nt === "access_func" || nt === "func_call") {
44614
+ const callee = any_node.resolved_function;
44615
+ if (!callee) return false;
44616
+ if (callee.is_extern) return true;
44617
+ if (callee.is_inline) return true;
44618
+ return is_auto_inline_method(callee, depth + 1);
44619
+ }
44620
+ for (const child of child_nodes(node)) if (!body_calls_splice_through(child, depth)) return false;
44621
+ return true;
44622
+ }
44314
44623
  function scan_inline_candidates(root) {
44315
44624
  const counts = /* @__PURE__ */ new Map();
44316
44625
  const defs = /* @__PURE__ */ new Map();
@@ -46110,7 +46419,7 @@ function build_access_method(node, access_func, status) {
46110
46419
  }
46111
46420
  }
46112
46421
  const target_struct = status.structs.find((s) => s.name === mono_struct_name);
46113
- const inline_func = target_struct?.functions.find((f) => (f.is_inline || is_auto_inline_method(f)) && f.name === access_func.name && (access_func.mangled_name ? mangled_label(f, mono_struct_name) === access_func.mangled_name : true));
46422
+ const inline_func = target_struct?.functions.find((f) => (f.is_inline || is_auto_inline_method(f, inline_splice_depth())) && f.name === access_func.name && (access_func.mangled_name ? mangled_label(f, mono_struct_name) === access_func.mangled_name : true));
46114
46423
  const trait_target = status.traits.find((t) => t.name === target_type.name);
46115
46424
  const trait_func = trait_target?.functions.find((f) => f.name === access_func.name);
46116
46425
  if (trait_target && trait_func) {
@@ -50438,6 +50747,17 @@ function expr_to_string(node, status) {
50438
50747
  }
50439
50748
  }
50440
50749
  }
50750
+ if (node.node_type === "op") {
50751
+ const op = node;
50752
+ if (op.op === "+" || op.op === "-") {
50753
+ const l = op.left_value ? expr_to_string(op.left_value, status) : void 0;
50754
+ const r = op.right_value ? expr_to_string(op.right_value, status) : void 0;
50755
+ if (l && r && !/^[+-]?\d+$/.test(l)) {
50756
+ if (/^[+-]?\d+$/.test(r)) return `${l} ${op.op} ${r}`;
50757
+ if (/^[A-Za-z_]\w*(\.\w+)*$/.test(r)) return `${l} ${op.op} ${r}`;
50758
+ }
50759
+ }
50760
+ }
50441
50761
  }
50442
50762
  /**
50443
50763
  * Rewrite a `<base>.<field>` path through the assumed parallel-length
@@ -50678,6 +50998,19 @@ function apply_bounds(condition, status, is_loop = false) {
50678
50998
  var_decl.path_bounds.set(bound.var_name, path_entry);
50679
50999
  }
50680
51000
  }
51001
+ const expr_mirror_target = /^\w+(\.\w+)+$/.test(bound.expr) ? bound.expr.split(".")[0] : void 0;
51002
+ let mirror_entry;
51003
+ if (expr_mirror_target && bound.expr !== bound.var_name) {
51004
+ const expr_decl = status.values.findLast((v) => v.name === expr_mirror_target);
51005
+ if (expr_decl) {
51006
+ if (!expr_decl.path_bounds) expr_decl.path_bounds = /* @__PURE__ */ new Map();
51007
+ mirror_entry = expr_decl.path_bounds.get(bound.expr);
51008
+ if (!mirror_entry) {
51009
+ mirror_entry = {};
51010
+ expr_decl.path_bounds.set(bound.expr, mirror_entry);
51011
+ }
51012
+ }
51013
+ }
50681
51014
  const push_unique = (arr, expr) => {
50682
51015
  const out = arr ?? [];
50683
51016
  if (!out.includes(expr)) out.push(expr);
@@ -50690,9 +51023,11 @@ function apply_bounds(condition, status, is_loop = false) {
50690
51023
  if (inclusive) {
50691
51024
  var_decl.upper_bound_inclusive_exprs = push_unique(var_decl.upper_bound_inclusive_exprs, bound.expr);
50692
51025
  if (path_entry) path_entry.upper_inclusive = push_unique(path_entry.upper_inclusive, bound.expr);
51026
+ if (mirror_entry && bound.var_name) mirror_entry.lower_inclusive = push_unique(mirror_entry.lower_inclusive, bound.var_name);
50693
51027
  } else {
50694
51028
  var_decl.upper_bound_exprs = push_unique(var_decl.upper_bound_exprs, bound.expr);
50695
51029
  if (path_entry) path_entry.upper = push_unique(path_entry.upper, bound.expr);
51030
+ if (mirror_entry && bound.var_name) mirror_entry.lower = push_unique(mirror_entry.lower, bound.var_name);
50696
51031
  }
50697
51032
  var_decl.upper_bound_expr = bound.expr;
50698
51033
  const num = numeric_interval(string_to_node(bound.expr), status);
@@ -50712,16 +51047,21 @@ function apply_bounds(condition, status, is_loop = false) {
50712
51047
  if (inclusive) {
50713
51048
  var_decl.lower_bound_inclusive_exprs = push_unique(var_decl.lower_bound_inclusive_exprs, bound.expr);
50714
51049
  if (path_entry) path_entry.lower_inclusive = push_unique(path_entry.lower_inclusive, bound.expr);
51050
+ if (mirror_entry && bound.var_name) mirror_entry.upper_inclusive = push_unique(mirror_entry.upper_inclusive, bound.var_name);
50715
51051
  } else {
50716
51052
  var_decl.lower_bound_exprs = push_unique(var_decl.lower_bound_exprs, bound.expr);
50717
51053
  if (path_entry) path_entry.lower = push_unique(path_entry.lower, bound.expr);
51054
+ if (mirror_entry && bound.var_name) mirror_entry.upper = push_unique(mirror_entry.upper, bound.var_name);
50718
51055
  }
50719
51056
  var_decl.lower_bound_expr = bound.expr;
50720
51057
  const num = numeric_interval(string_to_node(bound.expr), status);
50721
51058
  if (num) {
50722
51059
  const lo = bound.op === ">=" ? num.lower : num.upper;
50723
51060
  if (var_decl.range_lower === void 0 || lo > var_decl.range_lower) var_decl.range_lower = lo;
50724
- if (path_entry && (path_entry.range_lower === void 0 || lo > path_entry.range_lower)) path_entry.range_lower = lo;
51061
+ if (path_entry) {
51062
+ if (is_loop) path_entry.range_lower = lo;
51063
+ else if (path_entry.range_lower === void 0 || lo > path_entry.range_lower) path_entry.range_lower = lo;
51064
+ }
50725
51065
  }
50726
51066
  }
50727
51067
  apply_inverse_numeric_bound(bound, var_decl, status);
@@ -50848,6 +51188,39 @@ function apply_shifted_bound(condition, status, is_loop = false) {
50848
51188
  }
50849
51189
  }
50850
51190
  /**
51191
+ * Sweep every stored fact that REFERENCES `token` (any variable reassigned
51192
+ * or shadowed, or bound through a `ref` argument): offset facts are only as
51193
+ * fresh as their offset operand, and facts live on the BOUNDED variable's
51194
+ * entry — a mutation of the offset variable would otherwise leave a stale
51195
+ * `j < n - d` in place after `d` changed. Entries whose OWN name matches
51196
+ * are skipped (their facts die by entry shadowing on the reassignment
51197
+ * itself).
51198
+ */
51199
+ function invalidate_token_facts(token, status) {
51200
+ const re = new RegExp(`\\b${token}\\b`);
51201
+ const scrub = (arr) => {
51202
+ if (!arr) return arr;
51203
+ const out = arr.filter((e) => !re.test(e));
51204
+ return out.length === arr.length ? arr : out.length > 0 ? out : void 0;
51205
+ };
51206
+ for (const v of status.values) {
51207
+ if (v.name === token) continue;
51208
+ if (!(v.upper_bound_exprs && v.upper_bound_exprs.some((e) => re.test(e)) || v.lower_bound_exprs && v.lower_bound_exprs.some((e) => re.test(e)) || v.upper_bound_inclusive_exprs && v.upper_bound_inclusive_exprs.some((e) => re.test(e)) || v.lower_bound_inclusive_exprs && v.lower_bound_inclusive_exprs.some((e) => re.test(e)))) continue;
51209
+ v.upper_bound_exprs = scrub(v.upper_bound_exprs);
51210
+ v.lower_bound_exprs = scrub(v.lower_bound_exprs);
51211
+ v.upper_bound_inclusive_exprs = scrub(v.upper_bound_inclusive_exprs);
51212
+ v.lower_bound_inclusive_exprs = scrub(v.lower_bound_inclusive_exprs);
51213
+ if (v.upper_bound_expr && re.test(v.upper_bound_expr)) v.upper_bound_expr = void 0;
51214
+ if (v.lower_bound_expr && re.test(v.lower_bound_expr)) v.lower_bound_expr = void 0;
51215
+ if (v.path_bounds) for (const pb of v.path_bounds.values()) {
51216
+ pb.upper = scrub(pb.upper);
51217
+ pb.upper_inclusive = scrub(pb.upper_inclusive);
51218
+ pb.lower = scrub(pb.lower);
51219
+ pb.lower_inclusive = scrub(pb.lower_inclusive);
51220
+ }
51221
+ }
51222
+ }
51223
+ /**
50851
51224
  * Substitute `out` and parameter names in a return-constraint expression with
50852
51225
  * caller-side expressions. Used to translate a function's `out TYPE: contract`
50853
51226
  * into bounds on the caller's LHS variable.
@@ -51445,8 +51818,13 @@ function evaluate_operation(op, status) {
51445
51818
  if (left_decl.upper_bound_exprs.some((u) => upper_implies(u, target_str))) return true;
51446
51819
  for (const u of left_decl.upper_bound_exprs) {
51447
51820
  const v = parse_offset_expr(u);
51448
- if (v && v.offset === 0 && u !== target_str) {
51821
+ if (v && v.offset <= 0 && u !== target_str) {
51449
51822
  const vdecl = status.values.findLast((vv) => vv.name === v.base);
51823
+ const vpath = v.base.includes(".") ? status.values.findLast((vv) => vv.name === v.base.split(".")[0])?.path_bounds?.get(v.base) : void 0;
51824
+ if (vpath) {
51825
+ if (vpath.upper?.includes(target_str)) return true;
51826
+ if (vpath.upper_inclusive?.includes(target_str)) return true;
51827
+ }
51450
51828
  if (vdecl?.upper_bound_exprs?.includes(target_str)) return true;
51451
51829
  if (vdecl?.upper_bound_inclusive_exprs?.includes(target_str) && op.op === "<=") return true;
51452
51830
  if (op.op === "<" && vdecl?.upper_bound_inclusive_exprs?.includes(target_str)) return true;
@@ -51458,8 +51836,13 @@ function evaluate_operation(op, status) {
51458
51836
  if (left_decl.upper_bound_inclusive_exprs.some((u) => upper_implies(u, target_str))) return true;
51459
51837
  for (const u of left_decl.upper_bound_inclusive_exprs) {
51460
51838
  const v = parse_offset_expr(u);
51461
- if (v && v.offset === 0 && u !== target_str) {
51839
+ if (v && v.offset <= 0 && u !== target_str) {
51462
51840
  const vdecl = status.values.findLast((vv) => vv.name === v.base);
51841
+ const vpath = v.base.includes(".") ? status.values.findLast((vv) => vv.name === v.base.split(".")[0])?.path_bounds?.get(v.base) : void 0;
51842
+ if (vpath) {
51843
+ if (vpath.upper?.includes(target_str)) return true;
51844
+ if (vpath.upper_inclusive?.includes(target_str)) return true;
51845
+ }
51463
51846
  if (vdecl?.upper_bound_exprs?.includes(target_str)) return true;
51464
51847
  if (vdecl?.upper_bound_inclusive_exprs?.includes(target_str)) return true;
51465
51848
  }
@@ -52331,6 +52714,7 @@ function check_declaration_node(decl, status) {
52331
52714
  });
52332
52715
  if (decl.value) {
52333
52716
  move_closure_source(decl.value, status);
52717
+ invalidate_token_facts(decl.name, status);
52334
52718
  track_assignment_bounds(decl.name, decl.value, status);
52335
52719
  }
52336
52720
  } else {
@@ -52455,7 +52839,10 @@ function check_declaration_node(decl, status) {
52455
52839
  is_global: !in_function(status),
52456
52840
  trait_slot_conformer: trait_slot_conformer_of_decl(declaration, decl.type, decl.value, status)
52457
52841
  });
52458
- if (decl.value) track_assignment_bounds(decl.name, decl.value, status);
52842
+ if (decl.value) {
52843
+ invalidate_token_facts(decl.name, status);
52844
+ track_assignment_bounds(decl.name, decl.value, status);
52845
+ }
52459
52846
  if (status.pending_return_bounds) {
52460
52847
  const pending = status.pending_return_bounds.get(decl.name);
52461
52848
  if (pending) {
@@ -56679,6 +57066,7 @@ function check_function_call(node, status, func, target_type, self_value, self_p
56679
57066
  const param_type = type_from_value_node(param, status);
56680
57067
  const param_value = value_from_value_node(param);
56681
57068
  const has_ref_keyword = node.ref_param_indices?.includes(i) ?? false;
57069
+ if (has_ref_keyword && param.node_type === "value") invalidate_token_facts(param.value, status);
56682
57070
  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)) {
56683
57071
  const owned_struct = status.structs.find((st) => st.name === func_param.type.name && !st.is_simple_type && !st.is_class);
56684
57072
  if (owned_struct && !(owned_struct.traits ?? []).length && direct_string_fields(owned_struct).length > 0 && !struct_needs_destroy(owned_struct, status)) {
@@ -56827,18 +57215,56 @@ function check_function_call(node, status, func, target_type, self_value, self_p
56827
57215
  if (base_path) base_name = base_path.split(".")[0];
56828
57216
  }
56829
57217
  }
57218
+ let offset_token;
57219
+ let offset_token_sign = 1;
57220
+ if (!base_name && !base_path) {
57221
+ const tok = (n) => n.node_type === "value" && /^[A-Za-z_]\w*$/.test(n.value) ? n.value : void 0;
57222
+ if (pop.left_value.node_type === "value" && tok(pop.right_value)) {
57223
+ base_name = pop.left_value.value;
57224
+ offset_token = tok(pop.right_value);
57225
+ offset_token_sign = pop.op === "-" ? -1 : 1;
57226
+ } else if (pop.right_value.node_type === "value" && tok(pop.left_value)) {
57227
+ base_name = pop.right_value.value;
57228
+ offset_token = tok(pop.left_value);
57229
+ offset_token_sign = pop.op === "-" ? -1 : 1;
57230
+ }
57231
+ }
56830
57232
  if (base_name) {
56831
57233
  const decl = status.values.findLast((v) => v.name === base_name);
56832
57234
  if (decl) {
56833
57235
  const shift = (exprs) => exprs?.map((e) => shift_offset_expr(e, c));
56834
57236
  range_lower = decl.range_lower !== void 0 ? decl.range_lower + c : void 0;
56835
57237
  range_upper = decl.range_upper !== void 0 ? decl.range_upper + c : void 0;
56836
- upper_bound_exprs = shift(decl.upper_bound_exprs);
56837
- lower_bound_exprs = shift(decl.lower_bound_exprs);
56838
- upper_bound_inclusive_exprs = shift(decl.upper_bound_inclusive_exprs);
56839
- lower_bound_inclusive_exprs = shift(decl.lower_bound_inclusive_exprs);
56840
- upper_bound_expr = decl.upper_bound_expr ? shift_offset_expr(decl.upper_bound_expr, c) : void 0;
56841
- lower_bound_expr = decl.lower_bound_expr ? shift_offset_expr(decl.lower_bound_expr, c) : void 0;
57238
+ if (offset_token) {
57239
+ range_upper = void 0;
57240
+ range_lower = void 0;
57241
+ if (offset_token_sign > 0 && decl.range_lower !== void 0) {
57242
+ const tok_decl = status.values.findLast((v) => v.name === offset_token);
57243
+ if (tok_decl?.range_lower !== void 0 && tok_decl.range_lower >= 0) range_lower = decl.range_lower;
57244
+ }
57245
+ const cancel = (exprs) => {
57246
+ if (!exprs) return void 0;
57247
+ const out = [];
57248
+ for (const e of exprs) {
57249
+ const m = e.match(new RegExp(`^(\\w+(?:\\.\\w+)*)\\s*([+-])\\s*${offset_token}$`));
57250
+ if (m && m[2] === (offset_token_sign > 0 ? "-" : "+")) out.push(m[1]);
57251
+ }
57252
+ return out.length > 0 ? out : void 0;
57253
+ };
57254
+ upper_bound_exprs = cancel(decl.upper_bound_exprs);
57255
+ lower_bound_exprs = cancel(decl.lower_bound_exprs);
57256
+ upper_bound_inclusive_exprs = cancel(decl.upper_bound_inclusive_exprs);
57257
+ lower_bound_inclusive_exprs = cancel(decl.lower_bound_inclusive_exprs);
57258
+ upper_bound_expr = void 0;
57259
+ lower_bound_expr = void 0;
57260
+ } else {
57261
+ upper_bound_exprs = shift(decl.upper_bound_exprs);
57262
+ lower_bound_exprs = shift(decl.lower_bound_exprs);
57263
+ upper_bound_inclusive_exprs = shift(decl.upper_bound_inclusive_exprs);
57264
+ lower_bound_inclusive_exprs = shift(decl.lower_bound_inclusive_exprs);
57265
+ upper_bound_expr = decl.upper_bound_expr ? shift_offset_expr(decl.upper_bound_expr, c) : void 0;
57266
+ lower_bound_expr = decl.lower_bound_expr ? shift_offset_expr(decl.lower_bound_expr, c) : void 0;
57267
+ }
56842
57268
  alias_of = decl.alias_of ? shift_offset_expr(decl.alias_of, c) : base_path !== void 0 ? shift_offset_expr(base_path, c) : void 0;
56843
57269
  }
56844
57270
  }
@@ -58241,6 +58667,7 @@ function check_assignment_node(assign, status) {
58241
58667
  const self_snapshot = left_value_name === left_value.name && (!is_compound && is_self_shifted(assign.right_value, left_value.name) || is_trackable_compound) ? snapshot_bounds(left_value.name, status) : void 0;
58242
58668
  left_value.range_lower = void 0;
58243
58669
  left_value.range_upper = void 0;
58670
+ invalidate_token_facts(left_value.name, status);
58244
58671
  left_value.upper_bound_expr = void 0;
58245
58672
  left_value.lower_bound_expr = void 0;
58246
58673
  left_value.upper_bound_exprs = void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nomen-lang",
3
- "version": "0.6.11",
3
+ "version": "0.6.13",
4
4
  "description": "The CLI for the Nomen programming language.",
5
5
  "keywords": [],
6
6
  "license": "ISC",