@nudojs/core 1.13.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-A2PLG2QV.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());
@@ -15445,6 +15502,80 @@ function primOf(a) {
15445
15502
  return void 0;
15446
15503
  }
15447
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
+
15448
15579
  // src/algebra/exec/runtime/loop-widen.ts
15449
15580
  function mergeGrowth(a, b) {
15450
15581
  if (a === true || b === true) return true;
@@ -15730,9 +15861,9 @@ function makeLoopWidener() {
15730
15861
  const base = acc ?? final;
15731
15862
  const g = interrupted ? true : growth;
15732
15863
  if (g === null) {
15733
- return collapseArrSums(base);
15864
+ return applyInvariantsToPack(collapseArrSums(base));
15734
15865
  }
15735
- return widenWithPath(normalizeForWiden(base, g), g);
15866
+ return applyInvariantsToPack(widenWithPath(normalizeForWiden(base, g), g));
15736
15867
  }
15737
15868
  };
15738
15869
  }
@@ -16364,7 +16495,7 @@ function $idx(a, i, opts) {
16364
16495
  if (idx === void 0) {
16365
16496
  return joinAbs(a.shape.element, oobUndef());
16366
16497
  }
16367
- return a.shape.element;
16498
+ return markDomainShared(a.shape.element);
16368
16499
  }
16369
16500
  if (a.shape.k === "brand") {
16370
16501
  const elem = TYPED_ARRAY_ELEMENT[a.shape.name ?? ""];
@@ -16528,7 +16659,7 @@ function abstractLen(a) {
16528
16659
  let t = lenTermMemo.get(a);
16529
16660
  if (!t) {
16530
16661
  t = { op: "var", id: `len#${lenVarSeq++}` };
16531
- lenTermMemo.set(a, t);
16662
+ if (!isDomainShared(a)) lenTermMemo.set(a, t);
16532
16663
  }
16533
16664
  return abs({ k: "prim", type: "number" }, t, ge(t, lit(0)), "path");
16534
16665
  }
@@ -16960,7 +17091,7 @@ function $tpl(cooked, raw) {
16960
17091
  }
16961
17092
  function elemsOf(a) {
16962
17093
  if (a.shape.k === "tuple") return [...a.shape.elements];
16963
- if (a.shape.k === "arr") return [a.shape.element];
17094
+ if (a.shape.k === "arr") return [markDomainShared(a.shape.element)];
16964
17095
  if (isSetAbs(a)) return setElementsAbs(a);
16965
17096
  if (isMapAbs(a)) return mapEntriesAbs(a);
16966
17097
  const drained = drainGenElements(a);
@@ -18082,6 +18213,9 @@ function $for(init, test, step, body, maxIters = DEFAULT_MAX_LOOP_ITERS, opts) {
18082
18213
  return exitJoin !== void 0 ? counterWiden.widenFinal(exitJoin) : state;
18083
18214
  }
18084
18215
  function widenLoopJoin(a) {
18216
+ return applyInvariantsToPack(widenLoopJoinInner(a));
18217
+ }
18218
+ function widenLoopJoinInner(a) {
18085
18219
  const widenValue = (v2) => {
18086
18220
  if (v2.shape.k !== "sum") return v2;
18087
18221
  const members = v2.shape.members;
@@ -22030,7 +22164,7 @@ function invokeArrMethod(arr, method, args) {
22030
22164
  "path"
22031
22165
  );
22032
22166
  }
22033
- const out = callFn(args[0], args[1], shape2.element, unknownIdx(), arr);
22167
+ const out = callFn(args[0], args[1], markDomainShared(shape2.element), fromIdxAbs(), arr);
22034
22168
  const el = mapElementFallback(asAbs(args[0]), shape2.element, out);
22035
22169
  const conf = el === out ? confJoin(arr.conf, out.conf) : "partial";
22036
22170
  return abs({ k: "arr", element: el }, void 0, void 0, conf);
@@ -22067,7 +22201,7 @@ function invokeArrMethod(arr, method, args) {
22067
22201
  }
22068
22202
  return acc;
22069
22203
  }
22070
- return callFn(fn2, void 0, acc, shape2.element, unknownIdx(), arr);
22204
+ return callFn(fn2, void 0, acc, markDomainShared(shape2.element), fromIdxAbs(), arr);
22071
22205
  }
22072
22206
  if (method === "reduceRight") {
22073
22207
  validateCallableArg(args[0], "reduceRight callback may not be callable");
@@ -22103,7 +22237,7 @@ function invokeArrMethod(arr, method, args) {
22103
22237
  cause: "reduceRight of possibly-empty array with no initial value"
22104
22238
  });
22105
22239
  }
22106
- return callFn(fn2, void 0, acc, shape2.element, unknownIdx(), arr);
22240
+ return callFn(fn2, void 0, acc, markDomainShared(shape2.element), fromIdxAbs(), arr);
22107
22241
  }
22108
22242
  if (method === "filter") {
22109
22243
  validateCallableArg(args[0], "filter callback may not be callable");
@@ -22148,7 +22282,7 @@ function invokeArrMethod(arr, method, args) {
22148
22282
  );
22149
22283
  return projectFlatMapResult(arr.conf, mapped);
22150
22284
  }
22151
- const out = callFn(args[0], args[1], shape2.element, unknownIdx(), arr);
22285
+ const out = callFn(args[0], args[1], markDomainShared(shape2.element), fromIdxAbs(), arr);
22152
22286
  return projectFlatMapResult(arr.conf, [out]);
22153
22287
  }
22154
22288
  if (method === "forEach") {
@@ -22158,7 +22292,7 @@ function invokeArrMethod(arr, method, args) {
22158
22292
  if (!isHole(i)) callFn(args[0], args[1], el, $lit(i), arr);
22159
22293
  });
22160
22294
  } else {
22161
- callFn(args[0], args[1], shape2.element, unknownIdx(), arr);
22295
+ callFn(args[0], args[1], markDomainShared(shape2.element), fromIdxAbs(), arr);
22162
22296
  }
22163
22297
  return undefAbs();
22164
22298
  }
@@ -22177,7 +22311,7 @@ function invokeArrMethod(arr, method, args) {
22177
22311
  if (method === "every" && !t) return boolLit(false);
22178
22312
  }
22179
22313
  } else {
22180
- 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));
22181
22315
  if (t === void 0) return bool();
22182
22316
  if (method === "some" && t) return boolLit(true);
22183
22317
  if (method === "every" && !t) return boolLit(false);
@@ -22202,8 +22336,8 @@ function invokeArrMethod(arr, method, args) {
22202
22336
  }
22203
22337
  return undefAbs();
22204
22338
  }
22205
- const el = shape2.element;
22206
- 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));
22207
22341
  if (t === true) return el;
22208
22342
  if (t === false) return undefAbs();
22209
22343
  return joinAbs(el, undefAbs());
@@ -22220,7 +22354,7 @@ function invokeArrMethod(arr, method, args) {
22220
22354
  if (undecided) return joinAbs(unknownIdx(), $lit(-1));
22221
22355
  return $lit(-1);
22222
22356
  }
22223
- 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));
22224
22358
  if (t === true) return unknownIdx();
22225
22359
  if (t === false) return $lit(-1);
22226
22360
  return joinAbs(unknownIdx(), $lit(-1));
@@ -22241,7 +22375,7 @@ function invokeArrMethod(arr, method, args) {
22241
22375
  }
22242
22376
  return wantIndex ? $lit(-1) : undefAbs();
22243
22377
  }
22244
- 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));
22245
22379
  if (wantIndex) {
22246
22380
  if (t === true) return unknownIdx();
22247
22381
  if (t === false) return $lit(-1);
@@ -22291,7 +22425,7 @@ function invokeArrMethod(arr, method, args) {
22291
22425
  }
22292
22426
  return abstractSeqIteratorAbs(
22293
22427
  abs(
22294
- { k: "tuple", elements: [unknownIdx(), shape2.element] },
22428
+ { k: "tuple", elements: [fromIdxAbs(), markDomainShared(shape2.element)] },
22295
22429
  void 0,
22296
22430
  void 0,
22297
22431
  "partial"
@@ -28018,11 +28152,75 @@ function collectExclusionsFromPhi(phi2, id, acc) {
28018
28152
  noteStrictExclusion(p, isTarget, acc);
28019
28153
  }
28020
28154
  }
28021
- function sub(a, b, phi2 = pTrue) {
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) {
28022
28220
  {
28023
28221
  const fa = builtinBrandToPrimitive(a, "number");
28024
28222
  const fb = builtinBrandToPrimitive(b, "number");
28025
- if (fa || fb) return sub(fa ?? a, fb ?? b, phi2);
28223
+ if (fa || fb) return sub(fa ?? a, fb ?? b, phi2, assume);
28026
28224
  }
28027
28225
  if (isSymbolAbs(a) || isSymbolAbs(b)) {
28028
28226
  throw new NudoThrow(errorTypeAbs("TypeError"));
@@ -28041,6 +28239,12 @@ function sub(a, b, phi2 = pTrue) {
28041
28239
  return numLit(Number(ra.value) - Number(rb.value));
28042
28240
  }
28043
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
+ }
28044
28248
  const term = simplifyTerm(app("-", [a.term, b.term]));
28045
28249
  const ab = numericBounds(a, phi2);
28046
28250
  const bb = numericBounds(b, phi2);
@@ -28059,6 +28263,8 @@ function sub(a, b, phi2 = pTrue) {
28059
28263
  facts.push(strict ? lt(term, lit(hi)) : le(term, lit(hi)));
28060
28264
  }
28061
28265
  }
28266
+ const assumePred = subAssumeBounds(a, b, term, assume);
28267
+ if (assumePred) facts.push(assumePred);
28062
28268
  const conf = term.op === "lit" ? "exact" : confJoin(confJoin(a.conf, b.conf), facts.length ? "path" : "path");
28063
28269
  return abs({ k: "prim", type: "number" }, term, facts.length ? and(...facts) : void 0, conf);
28064
28270
  }
@@ -28071,7 +28277,7 @@ function sub(a, b, phi2 = pTrue) {
28071
28277
  if (isAnyLike(a) || isAnyLike(b)) {
28072
28278
  return toNumberResult(a, b, "-");
28073
28279
  }
28074
- const distributed = distributeSumBinOp("-", a, b, phi2, sub);
28280
+ const distributed = distributeSumBinOp("-", a, b, phi2, (x, y, p) => sub(x, y, p, assume));
28075
28281
  if (distributed) return distributed;
28076
28282
  noteAbstractArithMayThrow(a, b);
28077
28283
  return abs({ k: "unknown" }, void 0, void 0, "partial");
@@ -28531,55 +28737,6 @@ function mathMethod(name) {
28531
28737
  const impl = Math[name];
28532
28738
  return typeof impl === "function" ? impl : void 0;
28533
28739
  }
28534
- function isNonNaN(a, phi2) {
28535
- if (a.term?.op === "lit") {
28536
- return typeof a.term.value === "number" && !Number.isNaN(a.term.value);
28537
- }
28538
- if (a.shape.k === "sum") {
28539
- return a.shape.members.every(
28540
- (m) => isNonNaN(m, phi2)
28541
- );
28542
- }
28543
- const hasNonNaNFact = (p) => {
28544
- if (!p) return false;
28545
- if (p.op === "and") return p.args.some(hasNonNaNFact);
28546
- if (p.op === "assumeFinite") {
28547
- return !!a.term && termEquals(p.t, a.term);
28548
- }
28549
- if (p.op === "ne") {
28550
- if (p.b.op === "lit" && typeof p.b.value === "number" && Number.isNaN(p.b.value) && a.term && termEquals(p.a, a.term)) {
28551
- return true;
28552
- }
28553
- }
28554
- if (p.op === "gt" || p.op === "ge" || p.op === "lt" || p.op === "le") {
28555
- if (a.term && (termEquals(p.a, a.term) || termEquals(p.b, a.term))) return true;
28556
- }
28557
- return false;
28558
- };
28559
- if (hasNonNaNFact(a.pred)) return true;
28560
- if (a.term?.op === "var") {
28561
- const id = a.term.id;
28562
- const conjs = phi2.op === "and" ? phi2.args : [phi2];
28563
- for (const p of conjs) {
28564
- if (p.op === "assumeFinite" && p.t.op === "var" && p.t.id === id) {
28565
- return true;
28566
- }
28567
- if (p.op === "ne") {
28568
- const b = p.b;
28569
- if (b.op === "lit" && typeof b.value === "number" && Number.isNaN(b.value) && p.a.op === "var" && p.a.id === id) {
28570
- return true;
28571
- }
28572
- }
28573
- if (p.op === "gt" || p.op === "ge" || p.op === "lt" || p.op === "le") {
28574
- const sides = [p.a, p.b];
28575
- if (sides.some((t) => t.op === "var" && t.id === id) && sides.some((t) => t.op === "lit" && typeof t.value === "number")) {
28576
- return true;
28577
- }
28578
- }
28579
- }
28580
- }
28581
- return false;
28582
- }
28583
28740
  function ieeeBoundNum(n) {
28584
28741
  return Number.isFinite(n) || n === Infinity || n === -Infinity ? n : void 0;
28585
28742
  }
@@ -28984,6 +29141,7 @@ export {
28984
29141
  withExecPhi,
28985
29142
  currentAssumePremises,
28986
29143
  withAssumePremises,
29144
+ withInvariantSpecs,
28987
29145
  asAbsVal,
28988
29146
  $absVal,
28989
29147
  callAtFunctionBoundary,
package/dist/exec.js CHANGED
@@ -206,7 +206,7 @@ import {
206
206
  withAssumePremises,
207
207
  withEvalBudgetOverride,
208
208
  withExecPhi
209
- } from "./chunk-A2PLG2QV.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-V2XC3V72.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-A2PLG2QV.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-V2XC3V72.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-A2PLG2QV.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.13.0",
3
+ "version": "1.14.0",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },