@nudojs/core 1.12.0 → 1.14.0

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.
@@ -47,7 +47,7 @@ import {
47
47
  withEvalBudgetOverride,
48
48
  withExecPhi,
49
49
  withFnBudgetOverride
50
- } from "./chunk-56GO5HYS.js";
50
+ } from "./chunk-W3Z77Y2I.js";
51
51
 
52
52
  // src/algebra/directive-scan.ts
53
53
  function cleanDirectiveLine(raw, kind) {
@@ -622,6 +622,14 @@ function scanAssumeSegments(lines) {
622
622
  }
623
623
  return out;
624
624
  }
625
+ function scanInvariantSegments(lines) {
626
+ const out = [];
627
+ for (const line of lines) {
628
+ const m = line.match(/^@nudo:invariant\s+(.+)$/);
629
+ if (m) out.push(m[1].trim().replace(/\*\/$/, "").trim());
630
+ }
631
+ return out;
632
+ }
625
633
  function scanThrowsDecl(lines) {
626
634
  const kinds = /* @__PURE__ */ new Set();
627
635
  let any = false;
@@ -1516,6 +1524,35 @@ function extractAssumeSegs(source, fnName) {
1516
1524
  }
1517
1525
  return out;
1518
1526
  }
1527
+ var INVARIANT_SEG_REGEX = /^([A-Za-z_$][\w$]*)((?:\[\*\]|\.[A-Za-z_$][\w$]*)*)\s*(>=|<=|>|<)\s*(-?\d+(?:\.\d+)?)$/;
1528
+ function extractInvariantSegs(source, fnName) {
1529
+ if (!stripStringsKeepComments(source).includes("@nudo:invariant")) return [];
1530
+ const out = [];
1531
+ for (const seg of scanInvariantSegments(fnDirectiveCommentLines(source, fnName))) {
1532
+ const m = seg.match(INVARIANT_SEG_REGEX);
1533
+ if (!m) {
1534
+ out.push({ kind: "bad", raw: seg });
1535
+ continue;
1536
+ }
1537
+ const pathStr = m[2] ?? "";
1538
+ const path = [];
1539
+ for (const pm of pathStr.matchAll(/\[\*\]|\.[A-Za-z_$][\w$]*/g)) {
1540
+ const t = pm[0];
1541
+ if (t === "[*]") path.push({ k: "idx" });
1542
+ else path.push({ k: "prop", name: t.slice(1) });
1543
+ }
1544
+ const relTxt = m[3];
1545
+ out.push({
1546
+ kind: "ok",
1547
+ name: m[1],
1548
+ path,
1549
+ rel: relTxt === ">=" ? "ge" : relTxt === "<=" ? "le" : relTxt === ">" ? "gt" : "lt",
1550
+ value: Number(m[4]),
1551
+ display: `${m[1]}${pathStr} ${relTxt} ${m[4]}`
1552
+ });
1553
+ }
1554
+ return out;
1555
+ }
1519
1556
  function fnTopLevelLocalNames(body) {
1520
1557
  const block = body;
1521
1558
  if (!block || block.type !== "BlockStatement" || !Array.isArray(block.body)) return [];
@@ -5568,6 +5605,7 @@ export {
5568
5605
  execNudoModule,
5569
5606
  resetNudoModuleExecCache,
5570
5607
  extractAssumeSegs,
5608
+ extractInvariantSegs,
5571
5609
  fnTopLevelLocalNames,
5572
5610
  extractRefinesFromSource,
5573
5611
  refineToIndexedFull,
@@ -28,6 +28,27 @@ function termEquals(a, b) {
28
28
  }
29
29
  return false;
30
30
  }
31
+ var DETERMINISTIC_APP_FNS = /* @__PURE__ */ new Set([
32
+ "Math.max",
33
+ "Math.min",
34
+ "Math.abs",
35
+ "Math.round",
36
+ "Math.floor",
37
+ "Math.ceil",
38
+ "Math.trunc",
39
+ "Math.sign",
40
+ "+",
41
+ "-",
42
+ "*",
43
+ "/",
44
+ "%"
45
+ ]);
46
+ var LEN_TERM_ID_RE = /^len#\d+$/;
47
+ function isDeterministicTerm(t) {
48
+ if (t.op === "lit") return true;
49
+ if (t.op === "var") return LEN_TERM_ID_RE.test(t.id);
50
+ return DETERMINISTIC_APP_FNS.has(t.fn) && t.args.every(isDeterministicTerm);
51
+ }
31
52
  function litKeyString(v2) {
32
53
  if (typeof v2 === "number" && Object.is(v2, -0)) return "-0";
33
54
  return String(v2);
@@ -4471,6 +4492,19 @@ function withAssumePremises(p, body) {
4471
4492
  assumePremises = prev;
4472
4493
  }
4473
4494
  }
4495
+ var invariantSpecs = [];
4496
+ function currentInvariantSpecs() {
4497
+ return invariantSpecs;
4498
+ }
4499
+ function withInvariantSpecs(specs, body) {
4500
+ const prev = invariantSpecs;
4501
+ invariantSpecs = specs;
4502
+ try {
4503
+ return body();
4504
+ } finally {
4505
+ invariantSpecs = prev;
4506
+ }
4507
+ }
4474
4508
  var noBody2 = { type: "BlockStatement", body: [], directives: [] };
4475
4509
  var hostGlobalFnCall;
4476
4510
  function setHostGlobalFnCall(bridge) {
@@ -4511,8 +4545,11 @@ function $absVal(v2) {
4511
4545
  return asAbsVal(v2);
4512
4546
  }
4513
4547
  function callAtFunctionBoundary(body) {
4514
- if (assumePremises !== pTrue) {
4515
- return withAssumePremises(pTrue, () => boundaryBody(body));
4548
+ if (assumePremises !== pTrue || invariantSpecs.length > 0) {
4549
+ return withAssumePremises(
4550
+ pTrue,
4551
+ () => withInvariantSpecs([], () => boundaryBody(body))
4552
+ );
4516
4553
  }
4517
4554
  return boundaryBody(body);
4518
4555
  }
@@ -5043,6 +5080,16 @@ function bumpEvalForkBudget() {
5043
5080
  return false;
5044
5081
  }
5045
5082
 
5083
+ // src/algebra/len-sharing.ts
5084
+ var domainShared = /* @__PURE__ */ new WeakSet();
5085
+ function markDomainShared(a) {
5086
+ domainShared.add(a);
5087
+ return a;
5088
+ }
5089
+ function isDomainShared(a) {
5090
+ return domainShared.has(a);
5091
+ }
5092
+
5046
5093
  // src/algebra/exec/match-iter.ts
5047
5094
  var tables = /* @__PURE__ */ new WeakMap();
5048
5095
  function registerMatchIter(val, elements) {
@@ -6585,6 +6632,12 @@ function makeArrayCtorAbs(args) {
6585
6632
  }
6586
6633
  return abs({ k: "tuple", elements: [a0] }, void 0, void 0, "exact");
6587
6634
  }
6635
+ var fromIdxSeq = 0;
6636
+ function fromIdxAbs(lenTerm2, lenNumeric = false) {
6637
+ const t = v(`fromIdx#${fromIdxSeq++}`);
6638
+ const pred = lenNumeric && lenTerm2 ? and(ge(t, lit(0)), lt(t, lenTerm2)) : ge(t, lit(0));
6639
+ return abs({ k: "prim", type: "number" }, t, pred, "path");
6640
+ }
6588
6641
  function evalArrayFrom(args) {
6589
6642
  if (args[0] && typeof args[0] === "object" && args[0].shape.k === "brand" && (args[0].shape.name === "Arguments" || args[0].shape.name === "Proxy")) {
6590
6643
  args = [args[0].shape.shape, ...args.slice(1)];
@@ -6609,6 +6662,7 @@ function evalArrayFrom(args) {
6609
6662
  if (!hasMapFn) return el;
6610
6663
  return applyCallbackValue(mapFn, [el, i], emptyEnv(), pTrue, defaultLeakBudget);
6611
6664
  };
6665
+ const idxOrDrop = (lenTerm2, lenNumeric = false) => hasMapFn ? fromIdxAbs(lenTerm2, lenNumeric) : unknown;
6612
6666
  if (isSetAbs(a0) || isMapAbs(a0)) {
6613
6667
  const els = isSetAbs(a0) ? setElementsAbs(a0) : mapEntriesAbs(a0);
6614
6668
  if (els.length === 0) {
@@ -6643,7 +6697,7 @@ function evalArrayFrom(args) {
6643
6697
  }
6644
6698
  const k = a0.shape.k;
6645
6699
  if (k === "arr") {
6646
- const el = mapOne(a0.shape.element, numPrim());
6700
+ const el = mapOne(markDomainShared(a0.shape.element), idxOrDrop());
6647
6701
  return abs({ k: "arr", element: el }, void 0, void 0, "path");
6648
6702
  }
6649
6703
  if (k === "tuple") {
@@ -6717,16 +6771,19 @@ function evalArrayFrom(args) {
6717
6771
  }
6718
6772
  return abs({ k: "tuple", elements: els }, void 0, void 0, "exact");
6719
6773
  }
6720
- let el2 = mapOne(undefAbs(), numLit(0));
6721
- for (const v2 of allIdxSlotValues()) el2 = joinAbs(el2, mapOne(v2, numPrim()));
6722
- if (open) el2 = joinAbs(el2, mapOne(unknown, numPrim()));
6774
+ const iAbs = idxOrDrop(lit(nLit), true);
6775
+ let el2 = mapOne(undefAbs(), iAbs);
6776
+ for (const vv of allIdxSlotValues()) el2 = joinAbs(el2, mapOne(vv, iAbs));
6777
+ if (open) el2 = joinAbs(el2, mapOne(unknown, iAbs));
6723
6778
  return abs({ k: "arr", element: el2 }, void 0, void 0, "path");
6724
6779
  }
6780
+ const lenT = lenAbs?.term;
6781
+ const lenNum = !!lenT && lenAbs.shape.k === "prim" && lenAbs.shape.type === "number";
6725
6782
  const idxValues = allIdxSlotValues();
6726
6783
  let el = idxValues.length > 0 ? idxValues.reduce((x, y) => joinAbs(x, y)) : undefAbs();
6727
6784
  el = joinAbs(el, undefAbs());
6728
6785
  if (open) el = joinAbs(el, unknown);
6729
- el = mapOne(el, numPrim());
6786
+ el = mapOne(markDomainShared(el), idxOrDrop(lenT, lenNum));
6730
6787
  return abs({ k: "arr", element: el }, void 0, void 0, "path");
6731
6788
  }
6732
6789
  return unknown;
@@ -13700,7 +13757,7 @@ function $add(a, b) {
13700
13757
  return add(a, b, currentExecPhi());
13701
13758
  }
13702
13759
  function $sub(a, b) {
13703
- return sub(a, b, currentExecPhi());
13760
+ return sub(a, b, currentExecPhi(), currentAssumePremises());
13704
13761
  }
13705
13762
  function $mul(a, b) {
13706
13763
  return mul(a, b, currentExecPhi());
@@ -13729,6 +13786,16 @@ function $assumeAnchor(v2, name) {
13729
13786
  if (!mentions) return v2;
13730
13787
  const a = v2;
13731
13788
  if (typeof a !== "object" || a === null || a.term === void 0 || a.term.op === "lit" || a.term.op === "var" && a.term.id === name) {
13789
+ if (a && typeof a === "object" && a.shape?.k === "sum") {
13790
+ const members = a.shape.members.map((m) => {
13791
+ if (m.term === void 0 || m.term.op === "lit" || m.term.op === "var" && m.term.id === name) {
13792
+ return m;
13793
+ }
13794
+ const nt2 = { op: "var", id: name };
13795
+ return { ...m, term: nt2, pred: rewritePred(m.pred, m.term, nt2) };
13796
+ });
13797
+ return { ...a, shape: { ...a.shape, members } };
13798
+ }
13732
13799
  return v2;
13733
13800
  }
13734
13801
  const nt = { op: "var", id: name };
@@ -13808,11 +13875,13 @@ function $not(a) {
13808
13875
  }
13809
13876
  function $eq(a, b) {
13810
13877
  const r = strictEqAbs(a, b);
13811
- return r === void 0 ? bool() : boolLit(r);
13878
+ if (r !== void 0) return boolLit(r);
13879
+ return cmp("eq", a, b, currentExecPhi());
13812
13880
  }
13813
13881
  function $ne(a, b) {
13814
13882
  const r = strictEqAbs(a, b);
13815
- return r === void 0 ? bool() : boolLit(!r);
13883
+ if (r !== void 0) return boolLit(!r);
13884
+ return cmp("ne", a, b, currentExecPhi());
13816
13885
  }
13817
13886
  function $eqLoose(a, b) {
13818
13887
  const r = looseEqAbs(a, b);
@@ -15433,6 +15502,80 @@ function primOf(a) {
15433
15502
  return void 0;
15434
15503
  }
15435
15504
 
15505
+ // src/algebra/exec/runtime/invariant-apply.ts
15506
+ var mintSeq = 0;
15507
+ function strengthenLeaf(a, spec) {
15508
+ const s = a.shape;
15509
+ if (s.k !== "prim" || s.type !== "number") return a;
15510
+ if (a.term?.op === "lit") return a;
15511
+ const term = a.term ?? v(`inv#${mintSeq++}`);
15512
+ const bound = lit(spec.value);
15513
+ const rel = spec.rel === "ge" ? ge(term, bound) : spec.rel === "le" ? le(term, bound) : spec.rel === "gt" ? gt(term, bound) : lt(term, bound);
15514
+ return abs(s, term, a.pred ? and(a.pred, rel) : rel, "path");
15515
+ }
15516
+ function drill(v2, path, spec) {
15517
+ if (path.length === 0) return strengthenLeaf(v2, spec);
15518
+ const [step, ...rest] = path;
15519
+ const s = v2.shape;
15520
+ if (step.k === "idx") {
15521
+ if (s.k === "arr") {
15522
+ const element = drill(s.element, rest, spec);
15523
+ return element === s.element ? v2 : abs({ k: "arr", element }, void 0, void 0, v2.conf);
15524
+ }
15525
+ if (s.k === "tuple") {
15526
+ let changed = false;
15527
+ const elements = s.elements.map((e) => {
15528
+ const w = drill(e, rest, spec);
15529
+ if (w !== e) changed = true;
15530
+ return w;
15531
+ });
15532
+ return changed ? abs({ ...s, elements }, void 0, void 0, v2.conf) : v2;
15533
+ }
15534
+ if (s.k === "sum") {
15535
+ let changed = false;
15536
+ const members = s.members.map((m) => {
15537
+ const w = drill(m, rest, spec);
15538
+ if (w !== m) changed = true;
15539
+ return w;
15540
+ });
15541
+ return changed ? abs({ k: "sum", members }, void 0, void 0, v2.conf) : v2;
15542
+ }
15543
+ return v2;
15544
+ }
15545
+ if (s.k === "obj") {
15546
+ const slot = s.slots[step.name];
15547
+ if (!slot) return v2;
15548
+ const w = drill(slot.value, rest, spec);
15549
+ if (w === slot.value) return v2;
15550
+ return abs(
15551
+ { ...s, slots: { ...s.slots, [step.name]: { ...slot, value: w } } },
15552
+ void 0,
15553
+ void 0,
15554
+ v2.conf
15555
+ );
15556
+ }
15557
+ return v2;
15558
+ }
15559
+ function applyInvariantsToPack(pack) {
15560
+ const specs = currentInvariantSpecs();
15561
+ if (specs.length === 0) return pack;
15562
+ const s = pack.shape;
15563
+ if (s.k !== "obj") return pack;
15564
+ let slots;
15565
+ let changed = false;
15566
+ for (const spec of specs) {
15567
+ const cur = (slots ?? s.slots)[spec.name];
15568
+ if (!cur) continue;
15569
+ const w = drill(cur.value, spec.path, spec);
15570
+ if (w === cur.value) continue;
15571
+ if (!slots) slots = { ...s.slots };
15572
+ slots[spec.name] = { ...cur, value: w };
15573
+ changed = true;
15574
+ }
15575
+ if (!changed) return pack;
15576
+ return abs({ ...s, slots }, void 0, void 0, pack.conf);
15577
+ }
15578
+
15436
15579
  // src/algebra/exec/runtime/loop-widen.ts
15437
15580
  function mergeGrowth(a, b) {
15438
15581
  if (a === true || b === true) return true;
@@ -15718,9 +15861,9 @@ function makeLoopWidener() {
15718
15861
  const base = acc ?? final;
15719
15862
  const g = interrupted ? true : growth;
15720
15863
  if (g === null) {
15721
- return collapseArrSums(base);
15864
+ return applyInvariantsToPack(collapseArrSums(base));
15722
15865
  }
15723
- return widenWithPath(normalizeForWiden(base, g), g);
15866
+ return applyInvariantsToPack(widenWithPath(normalizeForWiden(base, g), g));
15724
15867
  }
15725
15868
  };
15726
15869
  }
@@ -16352,7 +16495,7 @@ function $idx(a, i, opts) {
16352
16495
  if (idx === void 0) {
16353
16496
  return joinAbs(a.shape.element, oobUndef());
16354
16497
  }
16355
- return a.shape.element;
16498
+ return markDomainShared(a.shape.element);
16356
16499
  }
16357
16500
  if (a.shape.k === "brand") {
16358
16501
  const elem = TYPED_ARRAY_ELEMENT[a.shape.name ?? ""];
@@ -16516,7 +16659,7 @@ function abstractLen(a) {
16516
16659
  let t = lenTermMemo.get(a);
16517
16660
  if (!t) {
16518
16661
  t = { op: "var", id: `len#${lenVarSeq++}` };
16519
- lenTermMemo.set(a, t);
16662
+ if (!isDomainShared(a)) lenTermMemo.set(a, t);
16520
16663
  }
16521
16664
  return abs({ k: "prim", type: "number" }, t, ge(t, lit(0)), "path");
16522
16665
  }
@@ -16948,7 +17091,7 @@ function $tpl(cooked, raw) {
16948
17091
  }
16949
17092
  function elemsOf(a) {
16950
17093
  if (a.shape.k === "tuple") return [...a.shape.elements];
16951
- if (a.shape.k === "arr") return [a.shape.element];
17094
+ if (a.shape.k === "arr") return [markDomainShared(a.shape.element)];
16952
17095
  if (isSetAbs(a)) return setElementsAbs(a);
16953
17096
  if (isMapAbs(a)) return mapEntriesAbs(a);
16954
17097
  const drained = drainGenElements(a);
@@ -18070,6 +18213,9 @@ function $for(init, test, step, body, maxIters = DEFAULT_MAX_LOOP_ITERS, opts) {
18070
18213
  return exitJoin !== void 0 ? counterWiden.widenFinal(exitJoin) : state;
18071
18214
  }
18072
18215
  function widenLoopJoin(a) {
18216
+ return applyInvariantsToPack(widenLoopJoinInner(a));
18217
+ }
18218
+ function widenLoopJoinInner(a) {
18073
18219
  const widenValue = (v2) => {
18074
18220
  if (v2.shape.k !== "sum") return v2;
18075
18221
  const members = v2.shape.members;
@@ -22018,7 +22164,7 @@ function invokeArrMethod(arr, method, args) {
22018
22164
  "path"
22019
22165
  );
22020
22166
  }
22021
- const out = callFn(args[0], args[1], shape2.element, unknownIdx(), arr);
22167
+ const out = callFn(args[0], args[1], markDomainShared(shape2.element), fromIdxAbs(), arr);
22022
22168
  const el = mapElementFallback(asAbs(args[0]), shape2.element, out);
22023
22169
  const conf = el === out ? confJoin(arr.conf, out.conf) : "partial";
22024
22170
  return abs({ k: "arr", element: el }, void 0, void 0, conf);
@@ -22055,7 +22201,7 @@ function invokeArrMethod(arr, method, args) {
22055
22201
  }
22056
22202
  return acc;
22057
22203
  }
22058
- return callFn(fn2, void 0, acc, shape2.element, unknownIdx(), arr);
22204
+ return callFn(fn2, void 0, acc, markDomainShared(shape2.element), fromIdxAbs(), arr);
22059
22205
  }
22060
22206
  if (method === "reduceRight") {
22061
22207
  validateCallableArg(args[0], "reduceRight callback may not be callable");
@@ -22091,7 +22237,7 @@ function invokeArrMethod(arr, method, args) {
22091
22237
  cause: "reduceRight of possibly-empty array with no initial value"
22092
22238
  });
22093
22239
  }
22094
- return callFn(fn2, void 0, acc, shape2.element, unknownIdx(), arr);
22240
+ return callFn(fn2, void 0, acc, markDomainShared(shape2.element), fromIdxAbs(), arr);
22095
22241
  }
22096
22242
  if (method === "filter") {
22097
22243
  validateCallableArg(args[0], "filter callback may not be callable");
@@ -22136,7 +22282,7 @@ function invokeArrMethod(arr, method, args) {
22136
22282
  );
22137
22283
  return projectFlatMapResult(arr.conf, mapped);
22138
22284
  }
22139
- const out = callFn(args[0], args[1], shape2.element, unknownIdx(), arr);
22285
+ const out = callFn(args[0], args[1], markDomainShared(shape2.element), fromIdxAbs(), arr);
22140
22286
  return projectFlatMapResult(arr.conf, [out]);
22141
22287
  }
22142
22288
  if (method === "forEach") {
@@ -22146,7 +22292,7 @@ function invokeArrMethod(arr, method, args) {
22146
22292
  if (!isHole(i)) callFn(args[0], args[1], el, $lit(i), arr);
22147
22293
  });
22148
22294
  } else {
22149
- callFn(args[0], args[1], shape2.element, unknownIdx(), arr);
22295
+ callFn(args[0], args[1], markDomainShared(shape2.element), fromIdxAbs(), arr);
22150
22296
  }
22151
22297
  return undefAbs();
22152
22298
  }
@@ -22165,7 +22311,7 @@ function invokeArrMethod(arr, method, args) {
22165
22311
  if (method === "every" && !t) return boolLit(false);
22166
22312
  }
22167
22313
  } else {
22168
- const t = callbackTruth(callFn(args[0], args[1], shape2.element, unknownIdx(), arr));
22314
+ const t = callbackTruth(callFn(args[0], args[1], markDomainShared(shape2.element), fromIdxAbs(), arr));
22169
22315
  if (t === void 0) return bool();
22170
22316
  if (method === "some" && t) return boolLit(true);
22171
22317
  if (method === "every" && !t) return boolLit(false);
@@ -22190,8 +22336,8 @@ function invokeArrMethod(arr, method, args) {
22190
22336
  }
22191
22337
  return undefAbs();
22192
22338
  }
22193
- const el = shape2.element;
22194
- const t = callbackTruth(callFn(args[0], args[1], el, unknownIdx(), arr));
22339
+ const el = markDomainShared(shape2.element);
22340
+ const t = callbackTruth(callFn(args[0], args[1], el, fromIdxAbs(), arr));
22195
22341
  if (t === true) return el;
22196
22342
  if (t === false) return undefAbs();
22197
22343
  return joinAbs(el, undefAbs());
@@ -22208,7 +22354,7 @@ function invokeArrMethod(arr, method, args) {
22208
22354
  if (undecided) return joinAbs(unknownIdx(), $lit(-1));
22209
22355
  return $lit(-1);
22210
22356
  }
22211
- const t = callbackTruth(callFn(args[0], args[1], shape2.element, unknownIdx(), arr));
22357
+ const t = callbackTruth(callFn(args[0], args[1], markDomainShared(shape2.element), fromIdxAbs(), arr));
22212
22358
  if (t === true) return unknownIdx();
22213
22359
  if (t === false) return $lit(-1);
22214
22360
  return joinAbs(unknownIdx(), $lit(-1));
@@ -22229,7 +22375,7 @@ function invokeArrMethod(arr, method, args) {
22229
22375
  }
22230
22376
  return wantIndex ? $lit(-1) : undefAbs();
22231
22377
  }
22232
- const t = callbackTruth(callFn(args[0], args[1], shape2.element, unknownIdx(), arr));
22378
+ const t = callbackTruth(callFn(args[0], args[1], markDomainShared(shape2.element), fromIdxAbs(), arr));
22233
22379
  if (wantIndex) {
22234
22380
  if (t === true) return unknownIdx();
22235
22381
  if (t === false) return $lit(-1);
@@ -22279,7 +22425,7 @@ function invokeArrMethod(arr, method, args) {
22279
22425
  }
22280
22426
  return abstractSeqIteratorAbs(
22281
22427
  abs(
22282
- { k: "tuple", elements: [unknownIdx(), shape2.element] },
22428
+ { k: "tuple", elements: [fromIdxAbs(), markDomainShared(shape2.element)] },
22283
22429
  void 0,
22284
22430
  void 0,
22285
22431
  "partial"
@@ -27909,6 +28055,12 @@ function numericBounds(a, phi2 = pTrue) {
27909
28055
  if (a.term?.op === "var") {
27910
28056
  collectBoundsFromPhi(phi2, a.term.id, result);
27911
28057
  }
28058
+ if (a.pred && a.term) {
28059
+ collectExclusionsFromPred(a.pred, a.term, result);
28060
+ }
28061
+ if (a.term?.op === "var") {
28062
+ collectExclusionsFromPhi(phi2, a.term.id, result);
28063
+ }
27912
28064
  if (a.term?.op === "lit" && typeof a.term.value === "number" && !Number.isNaN(a.term.value)) {
27913
28065
  result.lo = { value: a.term.value, strict: false };
27914
28066
  result.hi = { value: a.term.value, strict: false };
@@ -27962,11 +28114,113 @@ function collectBoundsFromPhi(phi2, id, acc) {
27962
28114
  noteBoundFromRel(p, isTarget, acc);
27963
28115
  }
27964
28116
  }
27965
- function sub(a, b, phi2 = pTrue) {
28117
+ function noteStrictExclusion(p, isTarget, acc) {
28118
+ if (p.op !== "ne") return;
28119
+ const num2 = exclusionLitOf(p, isTarget);
28120
+ if (num2 === void 0) return;
28121
+ if (acc.lo !== void 0 && acc.lo.value === num2 && !acc.lo.strict) {
28122
+ acc.lo = { value: num2, strict: true };
28123
+ }
28124
+ if (acc.hi !== void 0 && acc.hi.value === num2 && !acc.hi.strict) {
28125
+ acc.hi = { value: num2, strict: true };
28126
+ }
28127
+ }
28128
+ function exclusionLitOf(p, isTarget) {
28129
+ if (isTarget(p.a) && p.b.op === "lit" && typeof p.b.value === "number" && !Number.isNaN(p.b.value)) {
28130
+ return p.b.value;
28131
+ }
28132
+ if (isTarget(p.b) && p.a.op === "lit" && typeof p.a.value === "number" && !Number.isNaN(p.a.value)) {
28133
+ return p.a.value;
28134
+ }
28135
+ return void 0;
28136
+ }
28137
+ function collectExclusionsFromPred(pred, term, acc) {
28138
+ const isTarget = (t) => termToString(t) === termToString(term);
28139
+ const apply = (p) => {
28140
+ if (p.op === "and") {
28141
+ p.args.forEach(apply);
28142
+ return;
28143
+ }
28144
+ noteStrictExclusion(p, isTarget, acc);
28145
+ };
28146
+ apply(pred);
28147
+ }
28148
+ function collectExclusionsFromPhi(phi2, id, acc) {
28149
+ const isTarget = (t) => t.op === "var" && t.id === id;
28150
+ const conjs = phi2.op === "and" ? phi2.args : [phi2];
28151
+ for (const p of conjs) {
28152
+ noteStrictExclusion(p, isTarget, acc);
28153
+ }
28154
+ }
28155
+ function isNonNaN(a, phi2) {
28156
+ if (a.term?.op === "lit") {
28157
+ return typeof a.term.value === "number" && !Number.isNaN(a.term.value);
28158
+ }
28159
+ if (a.shape.k === "sum") {
28160
+ return a.shape.members.every(
28161
+ (m) => isNonNaN(m, phi2)
28162
+ );
28163
+ }
28164
+ const hasNonNaNFact = (p) => {
28165
+ if (!p) return false;
28166
+ if (p.op === "and") return p.args.some(hasNonNaNFact);
28167
+ if (p.op === "assumeFinite") {
28168
+ return !!a.term && termEquals(p.t, a.term);
28169
+ }
28170
+ if (p.op === "ne") {
28171
+ if (p.b.op === "lit" && typeof p.b.value === "number" && Number.isNaN(p.b.value) && a.term && termEquals(p.a, a.term)) {
28172
+ return true;
28173
+ }
28174
+ }
28175
+ if (p.op === "gt" || p.op === "ge" || p.op === "lt" || p.op === "le") {
28176
+ if (a.term && (termEquals(p.a, a.term) || termEquals(p.b, a.term))) return true;
28177
+ }
28178
+ return false;
28179
+ };
28180
+ if (hasNonNaNFact(a.pred)) return true;
28181
+ if (a.term?.op === "var") {
28182
+ const id = a.term.id;
28183
+ const conjs = phi2.op === "and" ? phi2.args : [phi2];
28184
+ for (const p of conjs) {
28185
+ if (p.op === "assumeFinite" && p.t.op === "var" && p.t.id === id) {
28186
+ return true;
28187
+ }
28188
+ if (p.op === "ne") {
28189
+ const b = p.b;
28190
+ if (b.op === "lit" && typeof b.value === "number" && Number.isNaN(b.value) && p.a.op === "var" && p.a.id === id) {
28191
+ return true;
28192
+ }
28193
+ }
28194
+ if (p.op === "gt" || p.op === "ge" || p.op === "lt" || p.op === "le") {
28195
+ const sides = [p.a, p.b];
28196
+ if (sides.some((t) => t.op === "var" && t.id === id) && sides.some((t) => t.op === "lit" && typeof t.value === "number")) {
28197
+ return true;
28198
+ }
28199
+ }
28200
+ }
28201
+ }
28202
+ return false;
28203
+ }
28204
+ function subAssumeBounds(a, b, resultTerm, assume) {
28205
+ const premises = assumePremiseList(assume);
28206
+ if (premises.length === 0) return void 0;
28207
+ if (!a.term || !b.term) return void 0;
28208
+ const facts = [];
28209
+ for (const p of premises) {
28210
+ if (p.op !== "le") continue;
28211
+ if (termEquals(p.a, a.term) && termEquals(p.b, b.term)) {
28212
+ facts.push(le(resultTerm, lit(0)));
28213
+ } else if (termEquals(p.a, b.term) && termEquals(p.b, a.term)) {
28214
+ facts.push(ge(resultTerm, lit(0)));
28215
+ }
28216
+ }
28217
+ return facts.length ? and(...facts) : void 0;
28218
+ }
28219
+ function sub(a, b, phi2 = pTrue, assume) {
27966
28220
  {
27967
28221
  const fa = builtinBrandToPrimitive(a, "number");
27968
28222
  const fb = builtinBrandToPrimitive(b, "number");
27969
- if (fa || fb) return sub(fa ?? a, fb ?? b, phi2);
28223
+ if (fa || fb) return sub(fa ?? a, fb ?? b, phi2, assume);
27970
28224
  }
27971
28225
  if (isSymbolAbs(a) || isSymbolAbs(b)) {
27972
28226
  throw new NudoThrow(errorTypeAbs("TypeError"));
@@ -27985,6 +28239,12 @@ function sub(a, b, phi2 = pTrue) {
27985
28239
  return numLit(Number(ra.value) - Number(rb.value));
27986
28240
  }
27987
28241
  if (isNumericLike(a) && isNumericLike(b) && a.term && b.term) {
28242
+ if (termEquals(a.term, b.term) && isDeterministicTerm(a.term) && // 域共享门(len-sharing.ts,第二类洞):同一带项 Abs 经元素域分发
28243
+ // 到两个下标位(xs.map(s=>s.length) 的 a[0] 与 a[1])——term 结构
28244
+ // 相同但代表不同具体值,$idx/回调实参分发位已标记,此处拒绝折叠。
28245
+ !isDomainShared(a) && !isDomainShared(b)) {
28246
+ return isNonNaN(a, phi2) || isNonNaN(b, phi2) ? numLit(0) : makeSum(numLit(NaN), numLit(0));
28247
+ }
27988
28248
  const term = simplifyTerm(app("-", [a.term, b.term]));
27989
28249
  const ab = numericBounds(a, phi2);
27990
28250
  const bb = numericBounds(b, phi2);
@@ -28003,6 +28263,8 @@ function sub(a, b, phi2 = pTrue) {
28003
28263
  facts.push(strict ? lt(term, lit(hi)) : le(term, lit(hi)));
28004
28264
  }
28005
28265
  }
28266
+ const assumePred = subAssumeBounds(a, b, term, assume);
28267
+ if (assumePred) facts.push(assumePred);
28006
28268
  const conf = term.op === "lit" ? "exact" : confJoin(confJoin(a.conf, b.conf), facts.length ? "path" : "path");
28007
28269
  return abs({ k: "prim", type: "number" }, term, facts.length ? and(...facts) : void 0, conf);
28008
28270
  }
@@ -28015,7 +28277,7 @@ function sub(a, b, phi2 = pTrue) {
28015
28277
  if (isAnyLike(a) || isAnyLike(b)) {
28016
28278
  return toNumberResult(a, b, "-");
28017
28279
  }
28018
- const distributed = distributeSumBinOp("-", a, b, phi2, sub);
28280
+ const distributed = distributeSumBinOp("-", a, b, phi2, (x, y, p) => sub(x, y, p, assume));
28019
28281
  if (distributed) return distributed;
28020
28282
  noteAbstractArithMayThrow(a, b);
28021
28283
  return abs({ k: "unknown" }, void 0, void 0, "partial");
@@ -28170,6 +28432,11 @@ function div(a, b, phi2 = pTrue, assume) {
28170
28432
  }
28171
28433
  return numLit(va / vb);
28172
28434
  }
28435
+ if (va === 0 && !Object.is(va, -0)) {
28436
+ const bb = numericBounds(b, phi2);
28437
+ const excludesZero = bb !== void 0 && (bb.lo !== void 0 && (bb.lo.value > 0 || bb.lo.value === 0 && bb.lo.strict) || bb.hi !== void 0 && (bb.hi.value < 0 || bb.hi.value === 0 && bb.hi.strict));
28438
+ if (excludesZero) return numLit(0);
28439
+ }
28173
28440
  if (ra.ok && rb.ok && coercibleLit(ra.value) && coercibleLit(rb.value)) {
28174
28441
  return numLit(Number(va) / Number(vb));
28175
28442
  }
@@ -28285,7 +28552,7 @@ function mod(a, b, phi2 = pTrue) {
28285
28552
  return abs({ k: "unknown" }, void 0, void 0, "partial");
28286
28553
  }
28287
28554
  function cmp(op, a, b, phi2 = pTrue) {
28288
- {
28555
+ if (op !== "eq" && op !== "ne") {
28289
28556
  const fa = builtinBrandToPrimitive(a, "number");
28290
28557
  const fb = builtinBrandToPrimitive(b, "number");
28291
28558
  if (fa || fb) return cmp(op, fa ?? a, fb ?? b, phi2);
@@ -28470,55 +28737,6 @@ function mathMethod(name) {
28470
28737
  const impl = Math[name];
28471
28738
  return typeof impl === "function" ? impl : void 0;
28472
28739
  }
28473
- function isNonNaN(a, phi2) {
28474
- if (a.term?.op === "lit") {
28475
- return typeof a.term.value === "number" && !Number.isNaN(a.term.value);
28476
- }
28477
- if (a.shape.k === "sum") {
28478
- return a.shape.members.every(
28479
- (m) => isNonNaN(m, phi2)
28480
- );
28481
- }
28482
- const hasNonNaNFact = (p) => {
28483
- if (!p) return false;
28484
- if (p.op === "and") return p.args.some(hasNonNaNFact);
28485
- if (p.op === "assumeFinite") {
28486
- return !!a.term && termEquals(p.t, a.term);
28487
- }
28488
- if (p.op === "ne") {
28489
- if (p.b.op === "lit" && typeof p.b.value === "number" && Number.isNaN(p.b.value) && a.term && termEquals(p.a, a.term)) {
28490
- return true;
28491
- }
28492
- }
28493
- if (p.op === "gt" || p.op === "ge" || p.op === "lt" || p.op === "le") {
28494
- if (a.term && (termEquals(p.a, a.term) || termEquals(p.b, a.term))) return true;
28495
- }
28496
- return false;
28497
- };
28498
- if (hasNonNaNFact(a.pred)) return true;
28499
- if (a.term?.op === "var") {
28500
- const id = a.term.id;
28501
- const conjs = phi2.op === "and" ? phi2.args : [phi2];
28502
- for (const p of conjs) {
28503
- if (p.op === "assumeFinite" && p.t.op === "var" && p.t.id === id) {
28504
- return true;
28505
- }
28506
- if (p.op === "ne") {
28507
- const b = p.b;
28508
- if (b.op === "lit" && typeof b.value === "number" && Number.isNaN(b.value) && p.a.op === "var" && p.a.id === id) {
28509
- return true;
28510
- }
28511
- }
28512
- if (p.op === "gt" || p.op === "ge" || p.op === "lt" || p.op === "le") {
28513
- const sides = [p.a, p.b];
28514
- if (sides.some((t) => t.op === "var" && t.id === id) && sides.some((t) => t.op === "lit" && typeof t.value === "number")) {
28515
- return true;
28516
- }
28517
- }
28518
- }
28519
- }
28520
- return false;
28521
- }
28522
28740
  function ieeeBoundNum(n) {
28523
28741
  return Number.isFinite(n) || n === Infinity || n === -Infinity ? n : void 0;
28524
28742
  }
@@ -28923,6 +29141,7 @@ export {
28923
29141
  withExecPhi,
28924
29142
  currentAssumePremises,
28925
29143
  withAssumePremises,
29144
+ withInvariantSpecs,
28926
29145
  asAbsVal,
28927
29146
  $absVal,
28928
29147
  callAtFunctionBoundary,
package/dist/exec.js CHANGED
@@ -206,7 +206,7 @@ import {
206
206
  withAssumePremises,
207
207
  withEvalBudgetOverride,
208
208
  withExecPhi
209
- } from "./chunk-56GO5HYS.js";
209
+ } from "./chunk-W3Z77Y2I.js";
210
210
  export {
211
211
  $absVal,
212
212
  $add,
package/dist/index.d.ts CHANGED
@@ -255,7 +255,7 @@ declare function asAbs(v: unknown): Abs | undefined;
255
255
 
256
256
  declare function add(a: Abs, b: Abs, phi?: Phi): Abs;
257
257
  /** 减法:a - b = a + (-b),数值上做单调性 */
258
- declare function sub(a: Abs, b: Abs, phi?: Phi): Abs;
258
+ declare function sub(a: Abs, b: Abs, phi?: Phi, assume?: Phi): Abs;
259
259
  /** 乘法:字面量直接求值;×正数同向缩放;×负数翻转不等式;×0 归零 */
260
260
  declare function mul(a: Abs, b: Abs, phi?: Phi): Abs;
261
261
  /**
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  extractFileEnvNames,
15
15
  extractFn,
16
16
  extractFnBudget,
17
+ extractInvariantSegs,
17
18
  extractMockModuleRecords,
18
19
  extractNudoImportRecords,
19
20
  extractNudoImports,
@@ -66,7 +67,7 @@ import {
66
67
  takeInterfaceDiagsSince,
67
68
  takeRefineDiags,
68
69
  takeRefineDiagsSince
69
- } from "./chunk-JI2XDARI.js";
70
+ } from "./chunk-N7AGFMSD.js";
70
71
  import {
71
72
  $add,
72
73
  $arguments,
@@ -525,8 +526,9 @@ import {
525
526
  v,
526
527
  withAssumePremises,
527
528
  withExecPhi,
529
+ withInvariantSpecs,
528
530
  withVar
529
- } from "./chunk-56GO5HYS.js";
531
+ } from "./chunk-W3Z77Y2I.js";
530
532
 
531
533
  // src/environment.ts
532
534
  function createEnvironment(parent, bindings = /* @__PURE__ */ new Map()) {
@@ -2413,6 +2415,49 @@ function assumePremisesPhiOf(name, source, file, issues) {
2413
2415
  if (preds.length === 0) return void 0;
2414
2416
  return { phi: and(...preds), display };
2415
2417
  }
2418
+ function invariantSpecsOf(name, source, file, issues) {
2419
+ const segs = extractInvariantSegs(source, name);
2420
+ if (segs.length === 0) return void 0;
2421
+ const meta = extractFn(source, name, file);
2422
+ const bindable = /* @__PURE__ */ new Set([
2423
+ ...contractParamNameSet(meta?.formals ?? []),
2424
+ ...meta?.params ?? [],
2425
+ ...fnTopLevelLocalNames(meta?.body)
2426
+ ]);
2427
+ const surface = [...bindable].join(", ");
2428
+ const specs = [];
2429
+ const display = [];
2430
+ for (const seg of segs) {
2431
+ if (seg.kind === "bad") {
2432
+ issues.push({
2433
+ severity: "warning",
2434
+ code: "nudo:invariant-invalid",
2435
+ message: `${name}: malformed @nudo:invariant '${seg.raw}' (expected <name><path> <rel> <number>; path = [*] or .member steps)`,
2436
+ actual: seg.raw,
2437
+ expected: "<name><path> <rel> <number>",
2438
+ suggestion: "loop-carried bounds: @nudo:invariant dp[*][*] >= 0 \u2014 scalar: @nudo:invariant last >= 1",
2439
+ fn: name
2440
+ });
2441
+ continue;
2442
+ }
2443
+ if (!bindable.has(seg.name)) {
2444
+ issues.push({
2445
+ severity: "warning",
2446
+ code: "nudo:invariant-invalid",
2447
+ message: `${name}: @nudo:invariant name not bound as a parameter or top-level local (${seg.name})`,
2448
+ actual: seg.name,
2449
+ expected: surface || "(no params/locals)",
2450
+ suggestion: `reference one of: ${surface || "(no params or top-level locals)"}`,
2451
+ fn: name
2452
+ });
2453
+ continue;
2454
+ }
2455
+ specs.push({ name: seg.name, path: seg.path, rel: seg.rel, value: seg.value });
2456
+ display.push(seg.display);
2457
+ }
2458
+ if (specs.length === 0) return void 0;
2459
+ return { specs, display };
2460
+ }
2416
2461
  function checkSourceInner(filePath, source, file, phi, opts, issues, signatures, names, truncated, identityOpts = opts, depsFp, sidecarFp) {
2417
2462
  const hasRefineDirective = stripStringsKeepComments(source).includes("@nudo:contract");
2418
2463
  const entryNames = localNamedExports(source);
@@ -2441,7 +2486,11 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2441
2486
  };
2442
2487
  for (const name of names) {
2443
2488
  const assumePhi = assumePremisesPhiOf(name, source, file, issues);
2444
- const runSeeded = (body) => assumePhi === void 0 ? body() : withAssumePremises(assumePhi.phi, body);
2489
+ const inv = invariantSpecsOf(name, source, file, issues);
2490
+ const runSeeded = (body) => {
2491
+ if (assumePhi === void 0 && inv === void 0) return body();
2492
+ return withAssumePremises(assumePhi?.phi ?? pTrue, () => withInvariantSpecs(inv?.specs ?? [], body));
2493
+ };
2445
2494
  if (opts.skips?.has(name)) {
2446
2495
  const declared = opts.skips.get(name) ?? null;
2447
2496
  const meta = extractFn(source, name, file);
@@ -2676,7 +2725,8 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2676
2725
  display,
2677
2726
  eff.returns.constraint,
2678
2727
  g.symbolic,
2679
- assumePhi?.display
2728
+ assumePhi?.display,
2729
+ inv?.display
2680
2730
  );
2681
2731
  if (retIssues.some((i) => i.severity === "error")) returnViolated.add(name);
2682
2732
  issues.push(...retIssues);
@@ -2877,23 +2927,33 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2877
2927
  budget: { ...budget }
2878
2928
  };
2879
2929
  }
2880
- function checkReturnConstraint(fnName, cName, constraint, ret, assumeDisplay) {
2930
+ function checkReturnConstraint(fnName, cName, constraint, ret, assumeDisplay, invariantDisplay) {
2881
2931
  const proof = assertImplies(ret, constraint);
2882
2932
  if (proof.status === "proved") {
2933
+ const relied = [];
2883
2934
  if (assumeDisplay && assumeDisplay.length > 0) {
2884
- return [
2885
- {
2886
- severity: "info",
2887
- code: "nudo:assume-relied",
2888
- message: `${fnName}: return contract proved via @nudo:assume ${assumeDisplay.join("; ")}`,
2889
- actual: `proof relies on user lemma${assumeDisplay.length > 1 ? "s" : ""}`,
2890
- expected: "engine verifies the obligation, not the lemma itself",
2891
- suggestion: "@nudo:assume is a user-supplied axiom \u2014 falsify it with concrete @nudo:case runs (nudo test)",
2892
- fn: fnName
2893
- }
2894
- ];
2935
+ relied.push({
2936
+ severity: "info",
2937
+ code: "nudo:assume-relied",
2938
+ message: `${fnName}: return contract proved via @nudo:assume ${assumeDisplay.join("; ")}`,
2939
+ actual: `proof relies on user lemma${assumeDisplay.length > 1 ? "s" : ""}`,
2940
+ expected: "engine verifies the obligation, not the lemma itself",
2941
+ suggestion: "@nudo:assume is a user-supplied axiom \u2014 falsify it with concrete @nudo:case runs (nudo test)",
2942
+ fn: fnName
2943
+ });
2944
+ }
2945
+ if (invariantDisplay && invariantDisplay.length > 0) {
2946
+ relied.push({
2947
+ severity: "info",
2948
+ code: "nudo:invariant-relied",
2949
+ message: `${fnName}: return contract proved via @nudo:invariant ${invariantDisplay.join("; ")}`,
2950
+ actual: `proof relies on user invariant${invariantDisplay.length > 1 ? "s" : ""}`,
2951
+ expected: "engine verifies the obligation, not the loop invariant itself",
2952
+ suggestion: "@nudo:invariant is a user-supplied axiom \u2014 falsify it with concrete @nudo:case runs (nudo test)",
2953
+ fn: fnName
2954
+ });
2895
2955
  }
2896
- return [];
2956
+ return relied;
2897
2957
  }
2898
2958
  const push = (severity, actual, expected, suggestion) => [
2899
2959
  {
package/dist/internal.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  stableAnalyzeKeySource,
22
22
  stablePathKey,
23
23
  stablePathKeyGraph
24
- } from "./chunk-JI2XDARI.js";
24
+ } from "./chunk-N7AGFMSD.js";
25
25
  import {
26
26
  $tryDigestSoft,
27
27
  $tryMarkSoft,
@@ -169,7 +169,7 @@ import {
169
169
  withEvalBudgetOverride,
170
170
  withFnBudgetOverride,
171
171
  wrapPromise
172
- } from "./chunk-56GO5HYS.js";
172
+ } from "./chunk-W3Z77Y2I.js";
173
173
 
174
174
  // src/algebra/inlay.ts
175
175
  function listFunctions(source) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nudojs/core",
3
- "version": "1.12.0",
3
+ "version": "1.14.0",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },