nomen-lang 0.6.12 → 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);
@@ -25251,11 +25302,15 @@ function lane_reg_of(name, plan) {
25251
25302
  const idx = temps.indexOf(name);
25252
25303
  return TEMP_REGS[idx] ?? "v4";
25253
25304
  }
25254
- 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) {
25255
25306
  switch (e.k) {
25256
- case "load":
25257
- 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`);
25258
25312
  return;
25313
+ }
25259
25314
  case "lit":
25260
25315
  case "scalar":
25261
25316
  if (plan.elem.float) {
@@ -25264,7 +25319,7 @@ function emit_lane_expr(e, status, plan, buffer_regs, idx, depth) {
25264
25319
  } else {
25265
25320
  build_node(e.node, status);
25266
25321
  ensure_newline(status);
25267
- 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`);
25268
25323
  }
25269
25324
  return;
25270
25325
  case "temp":
@@ -25277,11 +25332,11 @@ function emit_lane_expr(e, status, plan, buffer_regs, idx, depth) {
25277
25332
  const right_mul = left_mul ? null : mul_of(e.right);
25278
25333
  const mul = left_mul ?? right_mul;
25279
25334
  if (mul && (e.op === "+" || !left_mul)) {
25280
- 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);
25281
25336
  emit_asm(status, `mov v8.16b, v0.16b\n`);
25282
- 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);
25283
25338
  emit_asm(status, `mov ${spill_reg(depth + 1)}.16b, v0.16b\n`);
25284
- 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);
25285
25340
  emit_asm(status, `mov v1.16b, v0.16b\n`);
25286
25341
  emit_asm(status, `mov v0.16b, ${spill_reg(depth + 1)}.16b\n`);
25287
25342
  emit_asm(status, `${e.op === "+" ? "fmla" : "fmls"} v8.2d, v0.2d, v1.2d\n`);
@@ -25289,9 +25344,9 @@ function emit_lane_expr(e, status, plan, buffer_regs, idx, depth) {
25289
25344
  return;
25290
25345
  }
25291
25346
  }
25292
- 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);
25293
25348
  emit_asm(status, `mov ${spill_reg(depth)}.16b, v0.16b\n`);
25294
- 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);
25295
25350
  emit_asm(status, `mov v1.16b, v0.16b\n`);
25296
25351
  emit_asm(status, `mov v0.16b, ${spill_reg(depth)}.16b\n`);
25297
25352
  const { mnemonic, arr } = lane_op(e.op, plan.elem);
@@ -25346,19 +25401,19 @@ function lane_op(op, elem) {
25346
25401
  };
25347
25402
  }
25348
25403
  }
25349
- 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) {
25350
25405
  if (lane.kind === "temp_def") {
25351
- 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);
25352
25407
  emit_asm(status, `mov ${lane_reg_of(lane.name, plan)}.16b, v0.16b\n`);
25353
25408
  return;
25354
25409
  }
25355
25410
  if (lane.kind === "reduction") {
25356
- 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);
25357
25412
  const acc = acc_regs.get(lane.name) ?? "v2";
25358
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`);
25359
25414
  return;
25360
25415
  }
25361
- 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);
25362
25417
  emit_asm(status, `str q0, [${buffer_regs.get(lane.buffer) ?? "x11"}, ${idx}, lsl #4]\n`);
25363
25418
  }
25364
25419
  /**
@@ -25391,6 +25446,37 @@ function emit_neon_vector_loop(plan, status) {
25391
25446
  emit_asm(status, `ldr ${reg}, [x9, #8]\n`);
25392
25447
  buffer_regs.set(b.name, reg);
25393
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
+ }
25394
25480
  const acc_regs = /* @__PURE__ */ new Map();
25395
25481
  plan.reductions.forEach((r, i) => {
25396
25482
  const reg = REDUCTION_REGS[i] ?? "v2";
@@ -25400,12 +25486,13 @@ function emit_neon_vector_loop(plan, status) {
25400
25486
  } else {
25401
25487
  build_node(r.init_node, status);
25402
25488
  ensure_newline(status);
25403
- 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`);
25404
25490
  }
25405
25491
  acc_regs.set(r.name, reg);
25406
25492
  });
25407
25493
  build_node(plan.bound_node, status);
25408
25494
  ensure_newline(status);
25495
+ if (plan.max_shift > 0) emit_asm(status, `sub x0, x0, #${plan.max_shift}\n`);
25409
25496
  emit_asm(status, `mov x9, x0\n`);
25410
25497
  emit_asm(status, `asr x9, x9, #${shift}\n`);
25411
25498
  emit_asm(status, `bic x9, x9, #1\n`);
@@ -25414,9 +25501,9 @@ function emit_neon_vector_loop(plan, status) {
25414
25501
  emit_asm(status, `${label}:\n`);
25415
25502
  emit_asm(status, `cmp x10, x9\n`);
25416
25503
  emit_asm(status, `b.hs ${label}_end\n`);
25417
- 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);
25418
25505
  emit_asm(status, `add x14, x10, #1\n`);
25419
- 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);
25420
25507
  emit_asm(status, `add x10, x10, #2\n`);
25421
25508
  emit_asm(status, `b ${label}\n`);
25422
25509
  emit_asm(status, `${label}_end:\n`);
@@ -27223,8 +27310,9 @@ function flag_form_enabled() {
27223
27310
  *
27224
27311
  * Element kinds (tranche 2): the method pair determines the descriptor —
27225
27312
  * `load_float`/`store_float` (f64, `.2d`), `load_int`/`store_int` (8-byte
27226
- * int, `.2d`), `load`/`store` (4-byte int, `.4s`). All accesses in one
27227
- * 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.
27228
27316
  *
27229
27317
  * Soundness model (why this is legal):
27230
27318
  *
@@ -27233,12 +27321,24 @@ function flag_form_enabled() {
27233
27321
  * a vector iteration touches the same element set the scalar iterations
27234
27322
  * it replaces would. Lane k reads element base+k before lane k writes
27235
27323
  * element base+k; no lane reads an element another lane writes, whatever
27236
- * buffers alias. This is why shifted indices (`load_float(i + 1)`) are
27237
- * 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.
27238
27337
  * - TRIP SEMANTICS: the vector loop runs whole 16-byte groups (unrolled to
27239
- * two groups per iteration); the limit is floor(N / group) rounded down
27240
- * to a multiple of the unroll — floor semantics keep every lane index
27241
- * < 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
27242
27342
  * scalar loop is emitted unchanged afterwards as the tail (the induction
27243
27343
  * is synced to the vector loop's exit counter first), covering the
27244
27344
  * remainder — including the sub-unroll and negative-N cases where the
@@ -27272,6 +27372,9 @@ function flag_form_enabled() {
27272
27372
  /** Max distinct Buffers per plan — the preheader pins each data pointer in
27273
27373
  * a dedicated scratch register (x11..x13). */
27274
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;
27275
27378
  /** Max per-lane temps (v4..v7). */
27276
27379
  const MAX_TEMPS = 4;
27277
27380
  /** Max binary-op nesting depth in lane expressions (spill regs v14→v11). */
@@ -27305,10 +27408,18 @@ const ELEM_DESCS = [
27305
27408
  arr: "4s",
27306
27409
  group_elems: 4,
27307
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
27308
27419
  }
27309
27420
  ];
27310
27421
  function class_of_elem(elem) {
27311
- 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";
27312
27423
  }
27313
27424
  function class_of_type_name(name) {
27314
27425
  if (!name) return null;
@@ -27317,6 +27428,7 @@ function class_of_type_name(name) {
27317
27428
  const size = aarch64_size(name);
27318
27429
  if (size === 8) return "e8";
27319
27430
  if (size === 4) return "e4";
27431
+ if (size === 1) return "e1";
27320
27432
  } catch {}
27321
27433
  return null;
27322
27434
  }
@@ -27568,7 +27680,15 @@ function alloc_temps_of(s, walk) {
27568
27680
  if (has_allocations(d.value)) return null;
27569
27681
  const cls = class_of_type_name(d.type?.name);
27570
27682
  if (!cls) return null;
27571
- 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
+ }
27572
27692
  walk.temp_classes.set(d.name, cls);
27573
27693
  out.push({
27574
27694
  name: d.name,
@@ -27577,6 +27697,24 @@ function alloc_temps_of(s, walk) {
27577
27697
  }
27578
27698
  return out;
27579
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
+ }
27580
27718
  function lane_expr(e, walk, induction, status, depth) {
27581
27719
  if (depth > walk.max_depth) walk.max_depth = depth;
27582
27720
  if (depth > MAX_OP_DEPTH) return null;
@@ -27587,13 +27725,34 @@ function lane_expr(e, walk, induction, status, depth) {
27587
27725
  if (e.receiver.kind !== "leaf" || !e.receiver.name) return null;
27588
27726
  if (e.facts.args.length !== 1) return null;
27589
27727
  const idx = e.facts.args[0];
27590
- 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;
27591
27740
  if (!is_buffer_type_name(type_from_value_node$1(e.receiver.node)?.name ?? "")) return null;
27592
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
+ }
27593
27751
  return {
27594
27752
  k: "load",
27595
27753
  buffer: e.receiver.name,
27596
- node: e.receiver.node
27754
+ node: e.receiver.node,
27755
+ shift
27597
27756
  };
27598
27757
  }
27599
27758
  case "leaf": {
@@ -27790,6 +27949,26 @@ function extract_bound$1(bound, induction) {
27790
27949
  name: null
27791
27950
  };
27792
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
+ }
27793
27972
  if (bound.kind !== "path") return null;
27794
27973
  const root = bound.receiver;
27795
27974
  if (root.kind !== "leaf" || !root.name || root.name === induction) return null;
@@ -27844,6 +28023,9 @@ function plan_common(lanes_raw, bound_node, bound_name, induction, index, list,
27844
28023
  lanes: [],
27845
28024
  buffers: [],
27846
28025
  buffer_index: /* @__PURE__ */ new Map(),
28026
+ shifted: /* @__PURE__ */ new Map(),
28027
+ induction_alias: /* @__PURE__ */ new Map(),
28028
+ induction,
27847
28029
  temps: [],
27848
28030
  defs,
27849
28031
  defed_so_far: /* @__PURE__ */ new Set(),
@@ -27859,9 +28041,13 @@ function plan_common(lanes_raw, bound_node, bound_name, induction, index, list,
27859
28041
  }
27860
28042
  continue;
27861
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`);
27862
28049
  return null;
27863
28050
  }
27864
- if (bound_name !== null && defs.has(bound_name)) return null;
27865
28051
  const reduction_inits = /* @__PURE__ */ new Map();
27866
28052
  for (const name of reduction_candidates.keys()) {
27867
28053
  const scan = scan_init(list, index, name);
@@ -27872,6 +28058,9 @@ function plan_common(lanes_raw, bound_node, bound_name, induction, index, list,
27872
28058
  lanes: [],
27873
28059
  buffers: [],
27874
28060
  buffer_index: /* @__PURE__ */ new Map(),
28061
+ shifted: /* @__PURE__ */ new Map(),
28062
+ induction_alias: /* @__PURE__ */ new Map(),
28063
+ induction,
27875
28064
  temps: [],
27876
28065
  defs,
27877
28066
  defed_so_far: /* @__PURE__ */ new Set(),
@@ -27951,8 +28140,27 @@ function plan_common(lanes_raw, bound_node, bound_name, induction, index, list,
27951
28140
  for (const t of temps) {
27952
28141
  const nir = ast_to_nir_shallow(t.value);
27953
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
+ }
27954
28148
  const tvalue = lane_expr(nir, walk, induction, status, 0);
27955
- 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
+ }
27956
28164
  walk.defed_so_far.add(t.name);
27957
28165
  walk.temps.push(t.name);
27958
28166
  walk.lanes.push({
@@ -27970,17 +28178,41 @@ function plan_common(lanes_raw, bound_node, bound_name, induction, index, list,
27970
28178
  value
27971
28179
  });
27972
28180
  }
27973
- if (walk.temps.length > MAX_TEMPS) return null;
27974
- 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
+ }
27975
28189
  if (!walk.elem) return null;
27976
28190
  const want = class_of_elem(walk.elem);
27977
- 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
+ }
27978
28195
  if (reductions.length > MAX_REDUCTIONS) return null;
27979
28196
  if (walk.temps.length > 0) {
27980
28197
  const after = /* @__PURE__ */ new Set();
27981
28198
  for (let k = index + 1; k < list.length; k++) stmt_reads(list[k], after);
27982
- 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
+ }
27983
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);
27984
28216
  return {
27985
28217
  induction,
27986
28218
  bound_node,
@@ -27988,7 +28220,8 @@ function plan_common(lanes_raw, bound_node, bound_name, induction, index, list,
27988
28220
  buffers: walk.buffers,
27989
28221
  elem: walk.elem,
27990
28222
  reductions,
27991
- fma_contract: allow_reductions
28223
+ fma_contract: allow_reductions,
28224
+ max_shift
27992
28225
  };
27993
28226
  }
27994
28227
  /**
@@ -28016,6 +28249,7 @@ function plan_vector_loop(nstmt, index, list, status) {
28016
28249
  }
28017
28250
  const init = scan_init(list, index, induction);
28018
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`);
28019
28253
  return plan_common(lanes_raw, bound.node, bound.name, induction, index, list, status, status.fast_math === true);
28020
28254
  }
28021
28255
  /**
@@ -29648,6 +29882,12 @@ const active_splices = /* @__PURE__ */ new Set();
29648
29882
  function inline_splice_active(struct_name, func_name) {
29649
29883
  return active_splices.has(`${struct_name}.${func_name}`);
29650
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
+ }
29651
29891
  function begin_inline_splice(struct_name, func_name) {
29652
29892
  active_splices.add(`${struct_name}.${func_name}`);
29653
29893
  }
@@ -29751,6 +29991,11 @@ function build_inline_method(struct_node, func, status) {
29751
29991
  const old_return_buffer_offset = status.return_buffer_stack_offset;
29752
29992
  const old_function_return_type = status.function_return_type;
29753
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;
29754
29999
  const old_buffer_data_cache = status.buffer_data_cache;
29755
30000
  const old_buffer_fold_cache = status.buffer_fold_cache;
29756
30001
  const old_array_ptr_cache = status.array_ptr_cache;
@@ -29900,6 +30145,9 @@ function build_inline_method(struct_node, func, status) {
29900
30145
  status.return_buffer_stack_offset = old_return_buffer_offset;
29901
30146
  status.function_return_type = old_function_return_type;
29902
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;
29903
30151
  status.buffer_data_cache = old_buffer_data_cache;
29904
30152
  status.buffer_fold_cache = old_buffer_fold_cache;
29905
30153
  status.array_ptr_cache = old_array_ptr_cache;
@@ -29930,6 +30178,11 @@ function build_inline_function(func, status) {
29930
30178
  const old_return_buffer_offset = status.return_buffer_stack_offset;
29931
30179
  const old_function_return_type = status.function_return_type;
29932
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;
29933
30186
  const old_buffer_data_cache = status.buffer_data_cache;
29934
30187
  const old_buffer_fold_cache = status.buffer_fold_cache;
29935
30188
  const old_array_ptr_cache = status.array_ptr_cache;
@@ -30060,6 +30313,9 @@ function build_inline_function(func, status) {
30060
30313
  status.return_buffer_stack_offset = old_return_buffer_offset;
30061
30314
  status.function_return_type = old_function_return_type;
30062
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;
30063
30319
  status.buffer_data_cache = old_buffer_data_cache;
30064
30320
  status.buffer_fold_cache = old_buffer_fold_cache;
30065
30321
  status.array_ptr_cache = old_array_ptr_cache;
@@ -44267,6 +44523,8 @@ function load_nursery_struct_address(target, status) {
44267
44523
  const MAX_STATEMENTS = 15;
44268
44524
  const MAX_AUTO_METHOD_STATEMENTS = 3;
44269
44525
  const MAX_AUTO_METHOD_PARAMS = 3;
44526
+ const MAX_AUTO_CALLING_STATEMENTS = 10;
44527
+ const MAX_AUTO_SPLICE_DEPTH = 2;
44270
44528
  /**
44271
44529
  * ensure/grow/clear-shaped methods (ASM_PLAN_7 tranche 7): small real
44272
44530
  * bodies the method-inline path can splice WITHOUT the explicit `inline`
@@ -44278,11 +44536,19 @@ const MAX_AUTO_METHOD_PARAMS = 3;
44278
44536
  * scalar-or-void return. The standalone body is still emitted — trait
44279
44537
  * dispatch, method values, and overflow-arg call sites keep taking the
44280
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.
44281
44545
  */
44282
- function is_auto_inline_method(func) {
44546
+ function is_auto_inline_method(func, depth = 0) {
44283
44547
  if (!func.has_body) return false;
44284
44548
  if (func.is_inline) return false;
44285
- 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;
44286
44552
  if (func.returns_move) return false;
44287
44553
  if (func.statements.some((s) => s.node_type === "raw")) return false;
44288
44554
  if (func.params.length > MAX_AUTO_METHOD_PARAMS) return false;
@@ -44305,22 +44571,55 @@ function is_auto_inline_method(func) {
44305
44571
  if (func.return_type?.is_array || func.return_type?.is_view) return false;
44306
44572
  if (func.return_type?.is_nullable) return false;
44307
44573
  if (func.return_type?.name && !SIMPLE_TYPES.includes(func.return_type.name)) return false;
44308
- if (body_has_call(func.statements)) return false;
44309
- 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);
44310
44577
  }
44311
- /** Whether the subtree contains any call (method or function). */
44312
- function body_has_call(node) {
44578
+ function body_calls(node) {
44313
44579
  if (!node) return false;
44314
44580
  if (Array.isArray(node)) {
44315
- for (const item of node) if (body_has_call(item)) return true;
44581
+ for (const item of node) if (body_calls(item)) return true;
44316
44582
  return false;
44317
44583
  }
44318
44584
  if (typeof node !== "object") return false;
44319
44585
  const nt = node.node_type;
44320
44586
  if (nt === "access_func" || nt === "func_call") return true;
44321
- 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;
44322
44588
  return false;
44323
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
+ }
44324
44623
  function scan_inline_candidates(root) {
44325
44624
  const counts = /* @__PURE__ */ new Map();
44326
44625
  const defs = /* @__PURE__ */ new Map();
@@ -46120,7 +46419,7 @@ function build_access_method(node, access_func, status) {
46120
46419
  }
46121
46420
  }
46122
46421
  const target_struct = status.structs.find((s) => s.name === mono_struct_name);
46123
- 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));
46124
46423
  const trait_target = status.traits.find((t) => t.name === target_type.name);
46125
46424
  const trait_func = trait_target?.functions.find((f) => f.name === access_func.name);
46126
46425
  if (trait_target && trait_func) {
@@ -50448,6 +50747,17 @@ function expr_to_string(node, status) {
50448
50747
  }
50449
50748
  }
50450
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
+ }
50451
50761
  }
50452
50762
  /**
50453
50763
  * Rewrite a `<base>.<field>` path through the assumed parallel-length
@@ -50688,6 +50998,19 @@ function apply_bounds(condition, status, is_loop = false) {
50688
50998
  var_decl.path_bounds.set(bound.var_name, path_entry);
50689
50999
  }
50690
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
+ }
50691
51014
  const push_unique = (arr, expr) => {
50692
51015
  const out = arr ?? [];
50693
51016
  if (!out.includes(expr)) out.push(expr);
@@ -50700,9 +51023,11 @@ function apply_bounds(condition, status, is_loop = false) {
50700
51023
  if (inclusive) {
50701
51024
  var_decl.upper_bound_inclusive_exprs = push_unique(var_decl.upper_bound_inclusive_exprs, bound.expr);
50702
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);
50703
51027
  } else {
50704
51028
  var_decl.upper_bound_exprs = push_unique(var_decl.upper_bound_exprs, bound.expr);
50705
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);
50706
51031
  }
50707
51032
  var_decl.upper_bound_expr = bound.expr;
50708
51033
  const num = numeric_interval(string_to_node(bound.expr), status);
@@ -50722,16 +51047,21 @@ function apply_bounds(condition, status, is_loop = false) {
50722
51047
  if (inclusive) {
50723
51048
  var_decl.lower_bound_inclusive_exprs = push_unique(var_decl.lower_bound_inclusive_exprs, bound.expr);
50724
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);
50725
51051
  } else {
50726
51052
  var_decl.lower_bound_exprs = push_unique(var_decl.lower_bound_exprs, bound.expr);
50727
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);
50728
51055
  }
50729
51056
  var_decl.lower_bound_expr = bound.expr;
50730
51057
  const num = numeric_interval(string_to_node(bound.expr), status);
50731
51058
  if (num) {
50732
51059
  const lo = bound.op === ">=" ? num.lower : num.upper;
50733
51060
  if (var_decl.range_lower === void 0 || lo > var_decl.range_lower) var_decl.range_lower = lo;
50734
- 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
+ }
50735
51065
  }
50736
51066
  }
50737
51067
  apply_inverse_numeric_bound(bound, var_decl, status);
@@ -50858,6 +51188,39 @@ function apply_shifted_bound(condition, status, is_loop = false) {
50858
51188
  }
50859
51189
  }
50860
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
+ /**
50861
51224
  * Substitute `out` and parameter names in a return-constraint expression with
50862
51225
  * caller-side expressions. Used to translate a function's `out TYPE: contract`
50863
51226
  * into bounds on the caller's LHS variable.
@@ -51455,8 +51818,13 @@ function evaluate_operation(op, status) {
51455
51818
  if (left_decl.upper_bound_exprs.some((u) => upper_implies(u, target_str))) return true;
51456
51819
  for (const u of left_decl.upper_bound_exprs) {
51457
51820
  const v = parse_offset_expr(u);
51458
- if (v && v.offset === 0 && u !== target_str) {
51821
+ if (v && v.offset <= 0 && u !== target_str) {
51459
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
+ }
51460
51828
  if (vdecl?.upper_bound_exprs?.includes(target_str)) return true;
51461
51829
  if (vdecl?.upper_bound_inclusive_exprs?.includes(target_str) && op.op === "<=") return true;
51462
51830
  if (op.op === "<" && vdecl?.upper_bound_inclusive_exprs?.includes(target_str)) return true;
@@ -51468,8 +51836,13 @@ function evaluate_operation(op, status) {
51468
51836
  if (left_decl.upper_bound_inclusive_exprs.some((u) => upper_implies(u, target_str))) return true;
51469
51837
  for (const u of left_decl.upper_bound_inclusive_exprs) {
51470
51838
  const v = parse_offset_expr(u);
51471
- if (v && v.offset === 0 && u !== target_str) {
51839
+ if (v && v.offset <= 0 && u !== target_str) {
51472
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
+ }
51473
51846
  if (vdecl?.upper_bound_exprs?.includes(target_str)) return true;
51474
51847
  if (vdecl?.upper_bound_inclusive_exprs?.includes(target_str)) return true;
51475
51848
  }
@@ -52341,6 +52714,7 @@ function check_declaration_node(decl, status) {
52341
52714
  });
52342
52715
  if (decl.value) {
52343
52716
  move_closure_source(decl.value, status);
52717
+ invalidate_token_facts(decl.name, status);
52344
52718
  track_assignment_bounds(decl.name, decl.value, status);
52345
52719
  }
52346
52720
  } else {
@@ -52465,7 +52839,10 @@ function check_declaration_node(decl, status) {
52465
52839
  is_global: !in_function(status),
52466
52840
  trait_slot_conformer: trait_slot_conformer_of_decl(declaration, decl.type, decl.value, status)
52467
52841
  });
52468
- 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
+ }
52469
52846
  if (status.pending_return_bounds) {
52470
52847
  const pending = status.pending_return_bounds.get(decl.name);
52471
52848
  if (pending) {
@@ -56689,6 +57066,7 @@ function check_function_call(node, status, func, target_type, self_value, self_p
56689
57066
  const param_type = type_from_value_node(param, status);
56690
57067
  const param_value = value_from_value_node(param);
56691
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);
56692
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)) {
56693
57071
  const owned_struct = status.structs.find((st) => st.name === func_param.type.name && !st.is_simple_type && !st.is_class);
56694
57072
  if (owned_struct && !(owned_struct.traits ?? []).length && direct_string_fields(owned_struct).length > 0 && !struct_needs_destroy(owned_struct, status)) {
@@ -56837,18 +57215,56 @@ function check_function_call(node, status, func, target_type, self_value, self_p
56837
57215
  if (base_path) base_name = base_path.split(".")[0];
56838
57216
  }
56839
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
+ }
56840
57232
  if (base_name) {
56841
57233
  const decl = status.values.findLast((v) => v.name === base_name);
56842
57234
  if (decl) {
56843
57235
  const shift = (exprs) => exprs?.map((e) => shift_offset_expr(e, c));
56844
57236
  range_lower = decl.range_lower !== void 0 ? decl.range_lower + c : void 0;
56845
57237
  range_upper = decl.range_upper !== void 0 ? decl.range_upper + c : void 0;
56846
- upper_bound_exprs = shift(decl.upper_bound_exprs);
56847
- lower_bound_exprs = shift(decl.lower_bound_exprs);
56848
- upper_bound_inclusive_exprs = shift(decl.upper_bound_inclusive_exprs);
56849
- lower_bound_inclusive_exprs = shift(decl.lower_bound_inclusive_exprs);
56850
- upper_bound_expr = decl.upper_bound_expr ? shift_offset_expr(decl.upper_bound_expr, c) : void 0;
56851
- 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
+ }
56852
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;
56853
57269
  }
56854
57270
  }
@@ -58251,6 +58667,7 @@ function check_assignment_node(assign, status) {
58251
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;
58252
58668
  left_value.range_lower = void 0;
58253
58669
  left_value.range_upper = void 0;
58670
+ invalidate_token_facts(left_value.name, status);
58254
58671
  left_value.upper_bound_expr = void 0;
58255
58672
  left_value.lower_bound_expr = void 0;
58256
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.12",
3
+ "version": "0.6.13",
4
4
  "description": "The CLI for the Nomen programming language.",
5
5
  "keywords": [],
6
6
  "license": "ISC",