@callcium/sdk 0.4.0 → 0.5.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.
package/dist/index.js CHANGED
@@ -172,54 +172,71 @@ function lookupScope(code) {
172
172
  if (!info) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${code}`);
173
173
  return info;
174
174
  }
175
+ /** ABI type code ranges and sentinel values for the descriptor format. */
176
+ const TypeCode = {
177
+ UINT_MIN: 0,
178
+ UINT_MAX: 31,
179
+ INT_MIN: 32,
180
+ INT_MAX: 63,
181
+ ADDRESS: 64,
182
+ BOOL: 65,
183
+ FUNCTION: 66,
184
+ FIXED_BYTES_MIN: 80,
185
+ FIXED_BYTES_MAX: 111,
186
+ BYTES: 112,
187
+ STRING: 113,
188
+ STATIC_ARRAY: 128,
189
+ DYNAMIC_ARRAY: 129,
190
+ TUPLE: 144
191
+ };
175
192
  const CTX_PROP_TABLE = [
176
193
  {
177
194
  key: "MSG_SENDER",
178
195
  code: 0,
179
196
  label: "msg.sender",
180
- typeCode: 64
197
+ typeCode: TypeCode.ADDRESS
181
198
  },
182
199
  {
183
200
  key: "MSG_VALUE",
184
201
  code: 1,
185
202
  label: "msg.value",
186
- typeCode: 31
203
+ typeCode: TypeCode.UINT_MAX
187
204
  },
188
205
  {
189
206
  key: "BLOCK_TIMESTAMP",
190
207
  code: 2,
191
208
  label: "block.timestamp",
192
- typeCode: 31
209
+ typeCode: TypeCode.UINT_MAX
193
210
  },
194
211
  {
195
212
  key: "BLOCK_NUMBER",
196
213
  code: 3,
197
214
  label: "block.number",
198
- typeCode: 31
215
+ typeCode: TypeCode.UINT_MAX
199
216
  },
200
217
  {
201
218
  key: "CHAIN_ID",
202
219
  code: 4,
203
220
  label: "block.chainid",
204
- typeCode: 31
221
+ typeCode: TypeCode.UINT_MAX
205
222
  },
206
223
  {
207
224
  key: "TX_ORIGIN",
208
225
  code: 5,
209
226
  label: "tx.origin",
210
- typeCode: 64
227
+ typeCode: TypeCode.ADDRESS
211
228
  },
212
229
  {
213
230
  key: "BASE_FEE",
214
231
  code: 6,
215
232
  label: "block.basefee",
216
- typeCode: 31
233
+ typeCode: TypeCode.UINT_MAX
217
234
  },
218
235
  {
219
236
  key: "GAS_PRICE",
220
237
  code: 7,
221
238
  label: "tx.gasprice",
222
- typeCode: 31
239
+ typeCode: TypeCode.UINT_MAX
223
240
  }
224
241
  ];
225
242
  /** Well-known context property IDs for context-scope rules. */
@@ -426,23 +443,6 @@ function lookupQuantifier(code) {
426
443
  if (!info) throw new CallciumError("INVALID_QUANTIFIER", `Unknown quantifier step 0x${code.toString(16)}`);
427
444
  return info;
428
445
  }
429
- /** ABI type code ranges and sentinel values for the descriptor format. */
430
- const TypeCode = {
431
- UINT_MIN: 0,
432
- UINT_MAX: 31,
433
- INT_MIN: 32,
434
- INT_MAX: 63,
435
- ADDRESS: 64,
436
- BOOL: 65,
437
- FUNCTION: 66,
438
- FIXED_BYTES_MIN: 80,
439
- FIXED_BYTES_MAX: 111,
440
- BYTES: 112,
441
- STRING: 113,
442
- STATIC_ARRAY: 128,
443
- DYNAMIC_ARRAY: 129,
444
- TUPLE: 144
445
- };
446
446
  /** Throw an UNKNOWN_TYPE_CODE error. */
447
447
  function unknownTypeCode(code) {
448
448
  throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code 0x${code.toString(16).padStart(2, "0")}`);
@@ -1457,6 +1457,13 @@ function isSigned(typeCode) {
1457
1457
  return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
1458
1458
  }
1459
1459
  /**
1460
+ * Return true when the type code represents a left-aligned type (fixed bytes or function),
1461
+ * whose value occupies the high bytes of the word with zero padding below.
1462
+ */
1463
+ function isLeftAligned(typeCode) {
1464
+ return typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX || typeCode === TypeCode.FUNCTION;
1465
+ }
1466
+ /**
1460
1467
  * Canonicalize a raw 256-bit calldata word to its ABI value for the declared type (Policy Spec §7.8).
1461
1468
  * A scalar loaded from untrusted calldata may carry bits outside the declared width; masking
1462
1469
  * unsigned/address/bool/function/bytesN to width and sign-extending signed integers makes the
@@ -1490,6 +1497,15 @@ function isLengthOp(opCode) {
1490
1497
  const base = opCode & ~Op.NOT;
1491
1498
  return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
1492
1499
  }
1500
+ /** Map a LENGTH_* opcode base to its core value-comparison twin. */
1501
+ function lengthToValueOp(base) {
1502
+ if (base === Op.LENGTH_EQ) return Op.EQ;
1503
+ if (base === Op.LENGTH_GT) return Op.GT;
1504
+ if (base === Op.LENGTH_LT) return Op.LT;
1505
+ if (base === Op.LENGTH_GTE) return Op.GTE;
1506
+ if (base === Op.LENGTH_LTE) return Op.LTE;
1507
+ return Op.BETWEEN;
1508
+ }
1493
1509
  /** Return true when LENGTH_* operators are valid for the given type code (bytes, string, or dynamic array). */
1494
1510
  function isLengthValidType(typeCode) {
1495
1511
  return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
@@ -1529,7 +1545,12 @@ function isIn(value, data) {
1529
1545
  */
1530
1546
  function applyOperator(opCode, value, valueLength, operandData, typeCode) {
1531
1547
  const negate = (opCode & Op.NOT) !== 0;
1532
- const base = opCode & ~Op.NOT;
1548
+ let base = opCode & ~Op.NOT;
1549
+ if (isLengthOp(base)) {
1550
+ value = BigInt(valueLength);
1551
+ typeCode = TypeCode.UINT_MAX;
1552
+ base = lengthToValueOp(base);
1553
+ }
1533
1554
  let result;
1534
1555
  switch (base) {
1535
1556
  case Op.EQ:
@@ -1561,32 +1582,14 @@ function applyOperator(opCode, value, valueLength, operandData, typeCode) {
1561
1582
  result = (value & mask) === mask;
1562
1583
  break;
1563
1584
  }
1564
- case Op.BITMASK_ANY:
1565
- result = (value & toBigInt(operandData, 0)) !== 0n;
1566
- break;
1567
- case Op.BITMASK_NONE:
1568
- result = (value & toBigInt(operandData, 0)) === 0n;
1569
- break;
1570
- case Op.LENGTH_EQ:
1571
- result = BigInt(valueLength) === toBigInt(operandData, 0);
1572
- break;
1573
- case Op.LENGTH_GT:
1574
- result = BigInt(valueLength) > toBigInt(operandData, 0);
1575
- break;
1576
- case Op.LENGTH_LT:
1577
- result = BigInt(valueLength) < toBigInt(operandData, 0);
1578
- break;
1579
- case Op.LENGTH_GTE:
1580
- result = BigInt(valueLength) >= toBigInt(operandData, 0);
1581
- break;
1582
- case Op.LENGTH_LTE:
1583
- result = BigInt(valueLength) <= toBigInt(operandData, 0);
1585
+ case Op.BITMASK_ANY: {
1586
+ const mask = toBigInt(operandData, 0);
1587
+ result = (value & mask) !== 0n;
1584
1588
  break;
1585
- case Op.LENGTH_BETWEEN: {
1586
- const length = BigInt(valueLength);
1587
- const min = toBigInt(operandData, 0);
1588
- const max = toBigInt(operandData, 32);
1589
- result = length >= min && length <= max;
1589
+ }
1590
+ case Op.BITMASK_NONE: {
1591
+ const mask = toBigInt(operandData, 0);
1592
+ result = (value & mask) === 0n;
1590
1593
  break;
1591
1594
  }
1592
1595
  default: throw new CallciumError("INVALID_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}.`);
@@ -1596,189 +1599,394 @@ function applyOperator(opCode, value, valueLength, operandData, typeCode) {
1596
1599
  //#endregion
1597
1600
  //#region src/validation-issue.ts
1598
1601
  const ZERO = `0x${"0".repeat(64)}`;
1599
- /** Construct a fully populated Issue. */
1600
- function makeIssue(severity, category, groupIndex, constraintIndex, code, value1, value2, message) {
1602
+ /** Create an issue from operator compatibility results. */
1603
+ function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
1601
1604
  return {
1602
- severity,
1603
- category,
1605
+ severity: "error",
1606
+ category: "typeMismatch",
1604
1607
  groupIndex,
1605
1608
  constraintIndex,
1606
1609
  code,
1607
- value1,
1608
- value2,
1610
+ value1: opCode,
1611
+ value2: ZERO,
1609
1612
  message
1610
1613
  };
1611
1614
  }
1612
- /** Create an issue from operator compatibility results. */
1613
- function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
1614
- return makeIssue("error", "typeMismatch", groupIndex, constraintIndex, code, opCode, ZERO, message);
1615
+ /** Issue for an operand that is not canonically encoded for the declared type. */
1616
+ function nonCanonicalOperand(groupIndex, constraintIndex, operand, canonical) {
1617
+ return {
1618
+ severity: "error",
1619
+ category: "typeMismatch",
1620
+ groupIndex,
1621
+ constraintIndex,
1622
+ code: "NON_CANONICAL_OPERAND",
1623
+ value1: operand,
1624
+ value2: canonical,
1625
+ message: "Operand is not the canonical encoding for the declared type"
1626
+ };
1615
1627
  }
1616
- /** eq(v) and neq(v) on same path. */
1617
- function eqNeqContradiction(groupIndex, constraintIndex, value) {
1618
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "EQ_NEQ_CONTRADICTION", value, ZERO, "eq(v) and neq(v) on same path");
1628
+ /** eq(v)/neq(v) or lengthEq(v)/lengthNeq(v) on the same path. */
1629
+ function eqNeqContradiction(isLength, groupIndex, constraintIndex, value) {
1630
+ return {
1631
+ severity: "error",
1632
+ category: "contradiction",
1633
+ groupIndex,
1634
+ constraintIndex,
1635
+ code: isLength ? "LENGTH_EQ_NEQ_CONTRADICTION" : "EQ_NEQ_CONTRADICTION",
1636
+ value1: value,
1637
+ value2: ZERO,
1638
+ message: isLength ? "lengthEq(v) and lengthNeq(v) on same path" : "eq(v) and neq(v) on same path"
1639
+ };
1619
1640
  }
1620
- /** Multiple eq() operators with different values. */
1621
- function conflictingEquality(groupIndex, constraintIndex, existing, newValue) {
1622
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "CONFLICTING_EQUALITY", existing, newValue, "Multiple eq() operators with different values");
1641
+ /** Multiple eq()/lengthEq() operators with different values. */
1642
+ function conflictingEquality(isLength, groupIndex, constraintIndex, existing, newValue) {
1643
+ return {
1644
+ severity: "error",
1645
+ category: "contradiction",
1646
+ groupIndex,
1647
+ constraintIndex,
1648
+ code: isLength ? "CONFLICTING_LENGTH" : "CONFLICTING_EQUALITY",
1649
+ value1: existing,
1650
+ value2: newValue,
1651
+ message: isLength ? "Multiple lengthEq() operators with different values" : "Multiple eq() operators with different values"
1652
+ };
1623
1653
  }
1624
- /** Value is outside the physical range of the type. */
1625
- function outOfPhysicalBounds(groupIndex, constraintIndex, value) {
1626
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "OUT_OF_PHYSICAL_BOUNDS", value, ZERO, "Value is outside the physical range of the type");
1654
+ /** Value/length is outside the physical range of its type. */
1655
+ function outOfPhysicalBounds(isLength, groupIndex, constraintIndex, value) {
1656
+ return {
1657
+ severity: "error",
1658
+ category: "contradiction",
1659
+ groupIndex,
1660
+ constraintIndex,
1661
+ code: isLength ? "OUT_OF_PHYSICAL_LENGTH_BOUNDS" : "OUT_OF_PHYSICAL_BOUNDS",
1662
+ value1: value,
1663
+ value2: ZERO,
1664
+ message: isLength ? "Length value is outside the physical range" : "Value is outside the physical range of the type"
1665
+ };
1627
1666
  }
1628
- /** gt() on type maximum is impossible. */
1629
- function impossibleGt(groupIndex, constraintIndex, value) {
1630
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_GT", value, ZERO, "gt() on type maximum is impossible");
1667
+ /** gt()/lengthGt() on the maximum is impossible. */
1668
+ function impossibleGt(isLength, groupIndex, constraintIndex, value) {
1669
+ return {
1670
+ severity: "error",
1671
+ category: "contradiction",
1672
+ groupIndex,
1673
+ constraintIndex,
1674
+ code: isLength ? "IMPOSSIBLE_LENGTH_GT" : "IMPOSSIBLE_GT",
1675
+ value1: value,
1676
+ value2: ZERO,
1677
+ message: isLength ? "lengthGt() on maximum length is impossible" : "gt() on type maximum is impossible"
1678
+ };
1631
1679
  }
1632
- /** lt() on type minimum is impossible. */
1633
- function impossibleLt(groupIndex, constraintIndex, value) {
1634
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LT", value, ZERO, "lt() on type minimum is impossible");
1680
+ /** lt()/lengthLt() on the minimum is impossible. */
1681
+ function impossibleLt(isLength, groupIndex, constraintIndex, value) {
1682
+ return {
1683
+ severity: "error",
1684
+ category: "contradiction",
1685
+ groupIndex,
1686
+ constraintIndex,
1687
+ code: isLength ? "IMPOSSIBLE_LENGTH_LT" : "IMPOSSIBLE_LT",
1688
+ value1: value,
1689
+ value2: ZERO,
1690
+ message: isLength ? "lengthLt() on minimum length is impossible" : "lt() on type minimum is impossible"
1691
+ };
1635
1692
  }
1636
- /** eq() value is excluded by bound. */
1637
- function boundsExcludeEquality(groupIndex, constraintIndex, eq, bound) {
1638
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "BOUNDS_EXCLUDE_EQUALITY", eq, bound, "eq() value is excluded by bound");
1693
+ /** eq()/lengthEq() value is excluded by a bound. */
1694
+ function boundsExcludeEquality(isLength, groupIndex, constraintIndex, eq, bound) {
1695
+ return {
1696
+ severity: "error",
1697
+ category: "contradiction",
1698
+ groupIndex,
1699
+ constraintIndex,
1700
+ code: isLength ? "BOUNDS_EXCLUDE_LENGTH" : "BOUNDS_EXCLUDE_EQUALITY",
1701
+ value1: eq,
1702
+ value2: bound,
1703
+ message: isLength ? "lengthEq() value is excluded by bound" : "eq() value is excluded by bound"
1704
+ };
1639
1705
  }
1640
- /** Lower bound exceeds upper bound. */
1641
- function impossibleRange(groupIndex, constraintIndex, lower, upper) {
1642
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_RANGE", lower, upper, "Lower bound exceeds upper bound");
1706
+ /** Lower bound/length exceeds the upper bound. */
1707
+ function impossibleRange(isLength, groupIndex, constraintIndex, lower, upper) {
1708
+ return {
1709
+ severity: "error",
1710
+ category: "contradiction",
1711
+ groupIndex,
1712
+ constraintIndex,
1713
+ code: isLength ? "IMPOSSIBLE_LENGTH_RANGE" : "IMPOSSIBLE_RANGE",
1714
+ value1: lower,
1715
+ value2: upper,
1716
+ message: isLength ? "Lower length bound exceeds upper bound" : "Lower bound exceeds upper bound"
1717
+ };
1643
1718
  }
1644
1719
  /** Set operation excludes existing eq() value. */
1645
1720
  function setExcludesEquality(groupIndex, constraintIndex, value) {
1646
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "SET_EXCLUDES_EQUALITY", value, ZERO, "Set operation excludes existing eq() value");
1721
+ return {
1722
+ severity: "error",
1723
+ category: "contradiction",
1724
+ groupIndex,
1725
+ constraintIndex,
1726
+ code: "SET_EXCLUDES_EQUALITY",
1727
+ value1: value,
1728
+ value2: ZERO,
1729
+ message: "Set operation excludes existing eq() value"
1730
+ };
1647
1731
  }
1648
1732
  /** Multiple isIn() sets have no intersection. */
1649
1733
  function emptySetIntersection(groupIndex, constraintIndex) {
1650
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "EMPTY_SET_INTERSECTION", ZERO, ZERO, "Multiple isIn() sets have no intersection");
1734
+ return {
1735
+ severity: "error",
1736
+ category: "contradiction",
1737
+ groupIndex,
1738
+ constraintIndex,
1739
+ code: "EMPTY_SET_INTERSECTION",
1740
+ value1: ZERO,
1741
+ value2: ZERO,
1742
+ message: "Multiple isIn() sets have no intersection"
1743
+ };
1651
1744
  }
1652
1745
  /** All values in isIn() set are excluded by neq/notIn. */
1653
1746
  function setFullyExcluded(groupIndex, constraintIndex) {
1654
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "SET_FULLY_EXCLUDED", ZERO, ZERO, "All values in isIn() set are excluded by neq/notIn");
1655
- }
1656
- /** lengthEq(v) and lengthNeq(v) on same path. */
1657
- function lengthEqNeqContradiction(groupIndex, constraintIndex, value) {
1658
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "LENGTH_EQ_NEQ_CONTRADICTION", value, ZERO, "lengthEq(v) and lengthNeq(v) on same path");
1659
- }
1660
- /** Multiple lengthEq() operators with different values. */
1661
- function conflictingLength(groupIndex, constraintIndex, existing, newValue) {
1662
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "CONFLICTING_LENGTH", existing, newValue, "Multiple lengthEq() operators with different values");
1663
- }
1664
- /** lengthEq() value is excluded by bound. */
1665
- function boundsExcludeLength(groupIndex, constraintIndex, eq, bound) {
1666
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "BOUNDS_EXCLUDE_LENGTH", eq, bound, "lengthEq() value is excluded by bound");
1667
- }
1668
- /** Lower length bound exceeds upper bound. */
1669
- function impossibleLengthRange(groupIndex, constraintIndex, lower, upper) {
1670
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LENGTH_RANGE", lower, upper, "Lower length bound exceeds upper bound");
1671
- }
1672
- /** Length value is outside the physical range. */
1673
- function outOfPhysicalLengthBounds(groupIndex, constraintIndex, value) {
1674
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "OUT_OF_PHYSICAL_LENGTH_BOUNDS", value, ZERO, "Length value is outside the physical range");
1675
- }
1676
- /** lengthGt() on maximum length is impossible. */
1677
- function impossibleLengthGt(groupIndex, constraintIndex, value) {
1678
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LENGTH_GT", value, ZERO, "lengthGt() on maximum length is impossible");
1679
- }
1680
- /** lengthLt() on minimum length is impossible. */
1681
- function impossibleLengthLt(groupIndex, constraintIndex, value) {
1682
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LENGTH_LT", value, ZERO, "lengthLt() on minimum length is impossible");
1747
+ return {
1748
+ severity: "error",
1749
+ category: "contradiction",
1750
+ groupIndex,
1751
+ constraintIndex,
1752
+ code: "SET_FULLY_EXCLUDED",
1753
+ value1: ZERO,
1754
+ value2: ZERO,
1755
+ message: "All values in isIn() set are excluded by neq/notIn"
1756
+ };
1683
1757
  }
1684
1758
  /** Bitmask operators conflict. */
1685
1759
  function bitmaskContradiction(groupIndex, constraintIndex, mask, conflicting) {
1686
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "BITMASK_CONTRADICTION", mask, conflicting, "Bitmask operators conflict");
1760
+ return {
1761
+ severity: "error",
1762
+ category: "contradiction",
1763
+ groupIndex,
1764
+ constraintIndex,
1765
+ code: "BITMASK_CONTRADICTION",
1766
+ value1: mask,
1767
+ value2: conflicting,
1768
+ message: "Bitmask operators conflict"
1769
+ };
1687
1770
  }
1688
1771
  /** bitmaskAny is impossible because all bits are forbidden. */
1689
1772
  function bitmaskAnyImpossible(groupIndex, constraintIndex, mask, mustBeZero) {
1690
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "BITMASK_ANY_IMPOSSIBLE", mask, mustBeZero, "bitmaskAny is impossible because all bits are forbidden");
1773
+ return {
1774
+ severity: "error",
1775
+ category: "contradiction",
1776
+ groupIndex,
1777
+ constraintIndex,
1778
+ code: "BITMASK_ANY_IMPOSSIBLE",
1779
+ value1: mask,
1780
+ value2: mustBeZero,
1781
+ message: "bitmaskAny is impossible because all bits are forbidden"
1782
+ };
1691
1783
  }
1692
1784
  /** isIn/notIn set is not strictly sorted and deduplicated. */
1693
1785
  function unsortedInSet(groupIndex, constraintIndex) {
1694
- return makeIssue("error", "contradiction", groupIndex, constraintIndex, "UNSORTED_IN_SET", ZERO, ZERO, "isIn/notIn set is not strictly sorted and deduplicated.");
1786
+ return {
1787
+ severity: "error",
1788
+ category: "contradiction",
1789
+ groupIndex,
1790
+ constraintIndex,
1791
+ code: "UNSORTED_IN_SET",
1792
+ value1: ZERO,
1793
+ value2: ZERO,
1794
+ message: "isIn/notIn set is not strictly sorted and deduplicated."
1795
+ };
1695
1796
  }
1696
1797
  /** Group contains zero constraints. */
1697
1798
  function emptyGroup(groupIndex) {
1698
- return makeIssue("error", "vacuity", groupIndex, 0, "EMPTY_GROUP", ZERO, ZERO, "Group contains zero constraints.");
1799
+ return {
1800
+ severity: "error",
1801
+ category: "vacuity",
1802
+ groupIndex,
1803
+ constraintIndex: 0,
1804
+ code: "EMPTY_GROUP",
1805
+ value1: ZERO,
1806
+ value2: ZERO,
1807
+ message: "Group contains zero constraints."
1808
+ };
1699
1809
  }
1700
- /** Numeric bound is dominated by a stricter bound. */
1701
- function dominatedBound(groupIndex, constraintIndex, value) {
1702
- return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "DOMINATED_BOUND", value, ZERO, "Numeric bound is dominated by a stricter bound");
1810
+ /** Numeric/length bound dominated by a stricter bound. */
1811
+ function dominatedBound(isLength, groupIndex, constraintIndex, value) {
1812
+ return {
1813
+ severity: "warning",
1814
+ category: "redundancy",
1815
+ groupIndex,
1816
+ constraintIndex,
1817
+ code: isLength ? "DOMINATED_LENGTH_BOUND" : "DOMINATED_BOUND",
1818
+ value1: value,
1819
+ value2: ZERO,
1820
+ message: isLength ? "Length bound is dominated by a stricter bound" : "Numeric bound is dominated by a stricter bound"
1821
+ };
1703
1822
  }
1704
- /** Bound is redundant because eq() is set. */
1705
- function redundantBound(groupIndex, constraintIndex, bound, eq) {
1706
- return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "REDUNDANT_BOUND", bound, eq, "Bound is redundant because eq() is set");
1823
+ /** Bound redundant because eq()/lengthEq() is set. */
1824
+ function redundantBound(isLength, groupIndex, constraintIndex, bound, eq) {
1825
+ return {
1826
+ severity: "warning",
1827
+ category: "redundancy",
1828
+ groupIndex,
1829
+ constraintIndex,
1830
+ code: isLength ? "REDUNDANT_LENGTH_BOUND" : "REDUNDANT_BOUND",
1831
+ value1: bound,
1832
+ value2: eq,
1833
+ message: isLength ? "Length bound is redundant because lengthEq() is set" : "Bound is redundant because eq() is set"
1834
+ };
1707
1835
  }
1708
1836
  /** notIn() value was present in isIn() set. */
1709
1837
  function setReduction(groupIndex, constraintIndex, value) {
1710
- return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "SET_REDUCTION", value, ZERO, "notIn() value was present in isIn() set");
1838
+ return {
1839
+ severity: "warning",
1840
+ category: "redundancy",
1841
+ groupIndex,
1842
+ constraintIndex,
1843
+ code: "SET_REDUCTION",
1844
+ value1: value,
1845
+ value2: ZERO,
1846
+ message: "notIn() value was present in isIn() set"
1847
+ };
1711
1848
  }
1712
1849
  /** isIn() sets partially overlap. */
1713
1850
  function setRedundancy(groupIndex, constraintIndex, intersectionCount) {
1714
- return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "SET_REDUNDANCY", intersectionCount, ZERO, "isIn() sets partially overlap");
1851
+ return {
1852
+ severity: "warning",
1853
+ category: "redundancy",
1854
+ groupIndex,
1855
+ constraintIndex,
1856
+ code: "SET_REDUNDANCY",
1857
+ value1: intersectionCount,
1858
+ value2: ZERO,
1859
+ message: "isIn() sets partially overlap"
1860
+ };
1715
1861
  }
1716
1862
  /** Some values in isIn() set are excluded by neq/notIn. */
1717
1863
  function setPartiallyExcluded(groupIndex, constraintIndex, excludedCount) {
1718
- return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "SET_PARTIALLY_EXCLUDED", excludedCount, ZERO, "Some values in isIn() set are excluded by neq/notIn");
1719
- }
1720
- /** Length bound is dominated by a stricter bound. */
1721
- function dominatedLengthBound(groupIndex, constraintIndex, value) {
1722
- return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "DOMINATED_LENGTH_BOUND", value, ZERO, "Length bound is dominated by a stricter bound");
1723
- }
1724
- /** Length bound is redundant because lengthEq() is set. */
1725
- function redundantLengthBound(groupIndex, constraintIndex, bound, eq) {
1726
- return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "REDUNDANT_LENGTH_BOUND", bound, eq, "Length bound is redundant because lengthEq() is set");
1864
+ return {
1865
+ severity: "warning",
1866
+ category: "redundancy",
1867
+ groupIndex,
1868
+ constraintIndex,
1869
+ code: "SET_PARTIALLY_EXCLUDED",
1870
+ value1: excludedCount,
1871
+ value2: ZERO,
1872
+ message: "Some values in isIn() set are excluded by neq/notIn"
1873
+ };
1727
1874
  }
1728
1875
  /** Bitmask operation is redundant. */
1729
1876
  function redundantBitmask(groupIndex, constraintIndex, mask, existing) {
1730
- return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "REDUNDANT_BITMASK", mask, existing, "Bitmask operation is redundant");
1877
+ return {
1878
+ severity: "warning",
1879
+ category: "redundancy",
1880
+ groupIndex,
1881
+ constraintIndex,
1882
+ code: "REDUNDANT_BITMASK",
1883
+ value1: mask,
1884
+ value2: existing,
1885
+ message: "Bitmask operation is redundant"
1886
+ };
1887
+ }
1888
+ /** gte/lte pair fusible into a single range operator. */
1889
+ function fusibleRange(isLength, groupIndex, constraintIndex, low, high) {
1890
+ return {
1891
+ severity: "warning",
1892
+ category: "redundancy",
1893
+ groupIndex,
1894
+ constraintIndex,
1895
+ code: isLength ? "FUSIBLE_LENGTH_RANGE" : "FUSIBLE_RANGE",
1896
+ value1: low,
1897
+ value2: high,
1898
+ message: isLength ? "lengthGte() and lengthLte() fuse into a single lengthBetween()" : "gte() and lte() fuse into a single between()"
1899
+ };
1731
1900
  }
1732
1901
  /** Duplicate operator in constraint. */
1733
1902
  function duplicateConstraint(groupIndex, constraintIndex) {
1734
- return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "DUPLICATE_CONSTRAINT", ZERO, ZERO, "Duplicate operator in constraint");
1735
- }
1736
- /** gte() bound equals type minimum (always true). */
1737
- function vacuousGte(groupIndex, constraintIndex, value) {
1738
- return makeIssue("info", "vacuity", groupIndex, constraintIndex, "VACUOUS_GTE", value, ZERO, "gte() bound equals type minimum (always true)");
1739
- }
1740
- /** lte() bound equals type maximum (always true). */
1741
- function vacuousLte(groupIndex, constraintIndex, value) {
1742
- return makeIssue("info", "vacuity", groupIndex, constraintIndex, "VACUOUS_LTE", value, ZERO, "lte() bound equals type maximum (always true)");
1903
+ return {
1904
+ severity: "warning",
1905
+ category: "redundancy",
1906
+ groupIndex,
1907
+ constraintIndex,
1908
+ code: "DUPLICATE_CONSTRAINT",
1909
+ value1: ZERO,
1910
+ value2: ZERO,
1911
+ message: "Duplicate operator in constraint"
1912
+ };
1743
1913
  }
1744
- /** lengthGte(0) is always true. */
1745
- function vacuousLengthGte(groupIndex, constraintIndex, value) {
1746
- return makeIssue("info", "vacuity", groupIndex, constraintIndex, "VACUOUS_LENGTH_GTE", value, ZERO, "lengthGte(0) is always true");
1914
+ /** gte(min)/lengthGte(0) is always true. */
1915
+ function vacuousGte(isLength, groupIndex, constraintIndex, value) {
1916
+ return {
1917
+ severity: "info",
1918
+ category: "vacuity",
1919
+ groupIndex,
1920
+ constraintIndex,
1921
+ code: isLength ? "VACUOUS_LENGTH_GTE" : "VACUOUS_GTE",
1922
+ value1: value,
1923
+ value2: ZERO,
1924
+ message: isLength ? "lengthGte(0) is always true" : "gte() bound equals type minimum (always true)"
1925
+ };
1747
1926
  }
1748
- /** lengthLte() bound equals maximum (always true). */
1749
- function vacuousLengthLte(groupIndex, constraintIndex, value) {
1750
- return makeIssue("info", "vacuity", groupIndex, constraintIndex, "VACUOUS_LENGTH_LTE", value, ZERO, "lengthLte() bound equals maximum (always true)");
1927
+ /** lte(max)/lengthLte(max) is always true. */
1928
+ function vacuousLte(isLength, groupIndex, constraintIndex, value) {
1929
+ return {
1930
+ severity: "info",
1931
+ category: "vacuity",
1932
+ groupIndex,
1933
+ constraintIndex,
1934
+ code: isLength ? "VACUOUS_LENGTH_LTE" : "VACUOUS_LTE",
1935
+ value1: value,
1936
+ value2: ZERO,
1937
+ message: isLength ? "lengthLte() bound equals maximum (always true)" : "lte() bound equals type maximum (always true)"
1938
+ };
1751
1939
  }
1752
1940
  /** Path deeper than the reference enforcer's cap. */
1753
1941
  function pathDepthExceeded(groupIndex, constraintIndex, depth, maxDepth) {
1754
- return makeIssue("warning", "compatibility", groupIndex, constraintIndex, "PATH_DEPTH_EXCEEDED", depth, maxDepth, "Path depth exceeds the reference enforcer cap");
1942
+ return {
1943
+ severity: "warning",
1944
+ category: "compatibility",
1945
+ groupIndex,
1946
+ constraintIndex,
1947
+ code: "PATH_DEPTH_EXCEEDED",
1948
+ value1: depth,
1949
+ value2: maxDepth,
1950
+ message: "Path depth exceeds the reference enforcer cap"
1951
+ };
1755
1952
  }
1756
1953
  /** Quantifier over a static array beyond the reference enforcer's iteration cap. */
1757
1954
  function quantifierOverStaticLimit(groupIndex, constraintIndex, arrayLength, maxLength) {
1758
- return makeIssue("warning", "compatibility", groupIndex, constraintIndex, "QUANTIFIER_OVER_STATIC_LIMIT", arrayLength, maxLength, "Quantifier over static array exceeds the reference enforcer cap");
1955
+ return {
1956
+ severity: "warning",
1957
+ category: "compatibility",
1958
+ groupIndex,
1959
+ constraintIndex,
1960
+ code: "QUANTIFIER_OVER_STATIC_LIMIT",
1961
+ value1: arrayLength,
1962
+ value2: maxLength,
1963
+ message: "Quantifier over static array exceeds the reference enforcer cap"
1964
+ };
1759
1965
  }
1760
1966
  /** Context property ID outside the assigned set. */
1761
1967
  function unknownContextProperty(groupIndex, constraintIndex, contextId, maxContextId) {
1762
- return makeIssue("warning", "compatibility", groupIndex, constraintIndex, "UNKNOWN_CONTEXT_PROPERTY", contextId, maxContextId, "Unknown context property ID");
1968
+ return {
1969
+ severity: "warning",
1970
+ category: "compatibility",
1971
+ groupIndex,
1972
+ constraintIndex,
1973
+ code: "UNKNOWN_CONTEXT_PROPERTY",
1974
+ value1: contextId,
1975
+ value2: maxContextId,
1976
+ message: "Unknown context property ID"
1977
+ };
1763
1978
  }
1764
1979
  /** Negated operator under the existential quantifier. */
1765
1980
  function negationUnderAny(groupIndex, constraintIndex, opCode) {
1766
- return makeIssue("warning", "compatibility", groupIndex, constraintIndex, "NEGATION_UNDER_ANY", opCode, ZERO, "Negated operator under any() passes when a decoy element differs");
1767
- }
1768
- /** Pre-select value or length issue factories for a bound domain. */
1769
- function boundIssues(isLength) {
1770
1981
  return {
1771
- eqNeqContradiction: isLength ? lengthEqNeqContradiction : eqNeqContradiction,
1772
- conflicting: isLength ? conflictingLength : conflictingEquality,
1773
- outOfPhysicalBounds: isLength ? outOfPhysicalLengthBounds : outOfPhysicalBounds,
1774
- impossibleGt: isLength ? impossibleLengthGt : impossibleGt,
1775
- impossibleLt: isLength ? impossibleLengthLt : impossibleLt,
1776
- boundsExcludeEquality: isLength ? boundsExcludeLength : boundsExcludeEquality,
1777
- impossibleRange: isLength ? impossibleLengthRange : impossibleRange,
1778
- dominated: isLength ? dominatedLengthBound : dominatedBound,
1779
- redundant: isLength ? redundantLengthBound : redundantBound,
1780
- vacuousGte: isLength ? vacuousLengthGte : vacuousGte,
1781
- vacuousLte: isLength ? vacuousLengthLte : vacuousLte
1982
+ severity: "warning",
1983
+ category: "compatibility",
1984
+ groupIndex,
1985
+ constraintIndex,
1986
+ code: "NEGATION_UNDER_ANY",
1987
+ value1: opCode,
1988
+ value2: ZERO,
1989
+ message: "Negated operator under any() passes when a decoy element differs"
1782
1990
  };
1783
1991
  }
1784
1992
  //#endregion
@@ -1801,25 +2009,11 @@ const MAX_NOT_IN = 8;
1801
2009
  const LENGTH_MAX = 4294967295n;
1802
2010
  const UINT256_MAX = (1n << 256n) - 1n;
1803
2011
  const INT256_SIGN_BIT = 1n << 255n;
1804
- /** Reinterpret a uint256 bigint as a signed int256. */
1805
- function toSigned(v) {
1806
- return v >= INT256_SIGN_BIT ? v - (1n << 256n) : v;
1807
- }
1808
- /** a > b. */
1809
- function isGt(a, b, signed) {
1810
- return signed ? toSigned(a) > toSigned(b) : a > b;
1811
- }
1812
- /** a >= b. */
1813
- function isGte(a, b, signed) {
1814
- return signed ? toSigned(a) >= toSigned(b) : a >= b;
1815
- }
1816
- /** a < b. */
1817
- function isLt(a, b, signed) {
1818
- return signed ? toSigned(a) < toSigned(b) : a < b;
1819
- }
1820
- /** a <= b. */
1821
- function isLte(a, b, signed) {
1822
- return signed ? toSigned(a) <= toSigned(b) : a <= b;
2012
+ /** Signed-aware difference of two raw uint256 values: negative if a<b, zero if equal, positive if a>b. */
2013
+ function signedCompare(a, b, signed) {
2014
+ if (!signed) return a - b;
2015
+ const toS = (v) => v >= INT256_SIGN_BIT ? v - (1n << 256n) : v;
2016
+ return toS(a) - toS(b);
1823
2017
  }
1824
2018
  /** True for uint and int type codes. */
1825
2019
  function isNumericType(typeCode) {
@@ -1914,22 +2108,6 @@ function getIncompat(opBase, typeInfo) {
1914
2108
  message: "Unknown operator code"
1915
2109
  };
1916
2110
  }
1917
- /** Check operator–type compatibility and return an issue descriptor on mismatch. */
1918
- function checkCompatibility(opBase, typeCode, isDynamic, staticSize) {
1919
- const incompat = getIncompat(opBase, {
1920
- typeCode,
1921
- isDynamic,
1922
- staticSize
1923
- });
1924
- return incompat ? {
1925
- compatible: false,
1926
- ...incompat
1927
- } : {
1928
- compatible: true,
1929
- code: "",
1930
- message: ""
1931
- };
1932
- }
1933
2111
  /**
1934
2112
  * True if operator `opCode` is allowed on a target with the given type info.
1935
2113
  * Tolerates the NOT flag on `opCode`.
@@ -1941,6 +2119,19 @@ function isOpAllowed(opCode, typeInfo) {
1941
2119
  function readValue(opHex) {
1942
2120
  return BigInt(`0x${opHex.slice(4)}`);
1943
2121
  }
2122
+ /** First payload word deviating from the canonical encoding for the type, with its canonical form, or null. */
2123
+ function findNonCanonicalWord(opHex, typeCode) {
2124
+ const body = opHex.slice(4);
2125
+ for (let i = 0; i + 64 <= body.length; i += 64) {
2126
+ const word = BigInt(`0x${body.slice(i, i + 64)}`);
2127
+ const canonical = canonicalize(word, typeCode);
2128
+ if (canonical !== word) return {
2129
+ word,
2130
+ canonical
2131
+ };
2132
+ }
2133
+ return null;
2134
+ }
1944
2135
  /** Read a pair of uint256 values from a BETWEEN operator. */
1945
2136
  function readPair(opHex) {
1946
2137
  const body = opHex.slice(4);
@@ -1979,6 +2170,23 @@ function negateBoundOp(base) {
1979
2170
  if (base === Op.LTE) return Op.GT;
1980
2171
  return base;
1981
2172
  }
2173
+ /** Fresh bound domain with no constraints recorded yet. */
2174
+ function emptyBoundDomain(boundIsSigned, min, max) {
2175
+ return {
2176
+ isSigned: boundIsSigned,
2177
+ min,
2178
+ max,
2179
+ hasEq: false,
2180
+ eq: 0n,
2181
+ hasLower: false,
2182
+ lower: 0n,
2183
+ lowerInclusive: false,
2184
+ hasUpper: false,
2185
+ upper: 0n,
2186
+ upperInclusive: false,
2187
+ holes: []
2188
+ };
2189
+ }
1982
2190
  /** Create a fresh constraint context with domain limits from the type. */
1983
2191
  function initContext(scope, path, typeInfo) {
1984
2192
  const { min, max } = getDomainLimits(typeInfo.typeCode);
@@ -1986,38 +2194,12 @@ function initContext(scope, path, typeInfo) {
1986
2194
  scope,
1987
2195
  path,
1988
2196
  typeInfo,
1989
- numeric: {
1990
- isSigned: isSigned(typeInfo.typeCode),
1991
- min,
1992
- max,
1993
- hasEq: false,
1994
- eq: 0n,
1995
- hasLower: false,
1996
- lower: 0n,
1997
- lowerInclusive: false,
1998
- hasUpper: false,
1999
- upper: 0n,
2000
- upperInclusive: false,
2001
- holes: []
2002
- },
2197
+ numeric: emptyBoundDomain(isSigned(typeInfo.typeCode), min, max),
2003
2198
  bitmask: {
2004
2199
  mustBeOne: 0n,
2005
2200
  mustBeZero: 0n
2006
2201
  },
2007
- length: {
2008
- isSigned: false,
2009
- min: 0n,
2010
- max: LENGTH_MAX,
2011
- hasEq: false,
2012
- eq: 0n,
2013
- hasLower: false,
2014
- lower: 0n,
2015
- lowerInclusive: false,
2016
- hasUpper: false,
2017
- upper: 0n,
2018
- upperInclusive: false,
2019
- holes: []
2020
- },
2202
+ length: emptyBoundDomain(false, 0n, LENGTH_MAX),
2021
2203
  set: {
2022
2204
  hasIn: false,
2023
2205
  inValues: [],
@@ -2026,33 +2208,33 @@ function initContext(scope, path, typeInfo) {
2026
2208
  };
2027
2209
  }
2028
2210
  /** Narrow the numeric bound domain with an operator and emit issues on contradiction or redundancy. */
2029
- function updateBound(domain, base, isNegated, value, emit, groupIndex, constraintIndex, issues) {
2211
+ function updateBound(domain, base, isNegated, value, isLength, groupIndex, constraintIndex, issues) {
2030
2212
  let changedEq = false;
2031
2213
  let changedLower = false;
2032
2214
  let changedUpper = false;
2033
2215
  if (isNegated) {
2034
2216
  if (base === Op.EQ) {
2035
- if (domain.hasEq && domain.eq === value) issues.push(emit.eqNeqContradiction(groupIndex, constraintIndex, bigintToHex(value)));
2217
+ if (domain.hasEq && domain.eq === value) issues.push(eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2036
2218
  let alreadyHole = false;
2037
2219
  for (let j = 0; j < domain.holes.length; j++) if (domain.holes[j] === value) {
2038
2220
  alreadyHole = true;
2039
2221
  break;
2040
2222
  }
2041
2223
  if (!alreadyHole && domain.holes.length < MAX_HOLES) domain.holes.push(value);
2042
- } else updateBound(domain, negateBoundOp(base), false, value, emit, groupIndex, constraintIndex, issues);
2224
+ } else updateBound(domain, negateBoundOp(base), false, value, isLength, groupIndex, constraintIndex, issues);
2043
2225
  return;
2044
2226
  }
2045
- if (base === Op.GTE && value === domain.min) issues.push(emit.vacuousGte(groupIndex, constraintIndex, bigintToHex(value)));
2046
- else if (base === Op.LTE && value === domain.max) issues.push(emit.vacuousLte(groupIndex, constraintIndex, bigintToHex(value)));
2047
- if (isLt(value, domain.min, domain.isSigned) || isGt(value, domain.max, domain.isSigned)) issues.push(emit.outOfPhysicalBounds(groupIndex, constraintIndex, bigintToHex(value)));
2048
- else if (base === Op.GT && value === domain.max) issues.push(emit.impossibleGt(groupIndex, constraintIndex, bigintToHex(value)));
2049
- else if (base === Op.LT && value === domain.min) issues.push(emit.impossibleLt(groupIndex, constraintIndex, bigintToHex(value)));
2227
+ if (base === Op.GTE && value === domain.min) issues.push(vacuousGte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2228
+ else if (base === Op.LTE && value === domain.max) issues.push(vacuousLte(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2229
+ if (signedCompare(value, domain.min, domain.isSigned) < 0n || signedCompare(value, domain.max, domain.isSigned) > 0n) issues.push(outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2230
+ else if (base === Op.GT && value === domain.max) issues.push(impossibleGt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2231
+ else if (base === Op.LT && value === domain.min) issues.push(impossibleLt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2050
2232
  if (base === Op.EQ) {
2051
2233
  if (!domain.hasEq || domain.eq !== value) changedEq = true;
2052
2234
  if (domain.hasEq) {
2053
- if (domain.eq !== value) issues.push(emit.conflicting(groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(value)));
2235
+ if (domain.eq !== value) issues.push(conflictingEquality(isLength, groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(value)));
2054
2236
  }
2055
- for (let j = 0; j < domain.holes.length; j++) if (domain.holes[j] === value) issues.push(emit.eqNeqContradiction(groupIndex, constraintIndex, bigintToHex(value)));
2237
+ for (let j = 0; j < domain.holes.length; j++) if (domain.holes[j] === value) issues.push(eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2056
2238
  domain.hasEq = true;
2057
2239
  domain.eq = value;
2058
2240
  } else if (base === Op.GT || base === Op.GTE) {
@@ -2060,15 +2242,15 @@ function updateBound(domain, base, isNegated, value, emit, groupIndex, constrain
2060
2242
  if (domain.hasLower) {
2061
2243
  let redundant = false;
2062
2244
  let strictlyBetter = false;
2063
- if (isLt(value, domain.lower, domain.isSigned)) redundant = true;
2245
+ if (signedCompare(value, domain.lower, domain.isSigned) < 0n) redundant = true;
2064
2246
  else if (value === domain.lower) {
2065
2247
  if (domain.lowerInclusive) if (!inclusive) strictlyBetter = true;
2066
2248
  else redundant = true;
2067
2249
  else if (!inclusive) redundant = true;
2068
2250
  } else strictlyBetter = true;
2069
- if (redundant) issues.push(emit.dominated(groupIndex, constraintIndex, bigintToHex(value)));
2251
+ if (redundant) issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2070
2252
  if (strictlyBetter) {
2071
- issues.push(emit.dominated(groupIndex, constraintIndex, bigintToHex(domain.lower)));
2253
+ issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.lower)));
2072
2254
  domain.lower = value;
2073
2255
  domain.lowerInclusive = inclusive;
2074
2256
  changedLower = true;
@@ -2084,15 +2266,15 @@ function updateBound(domain, base, isNegated, value, emit, groupIndex, constrain
2084
2266
  if (domain.hasUpper) {
2085
2267
  let redundant = false;
2086
2268
  let strictlyBetter = false;
2087
- if (isGt(value, domain.upper, domain.isSigned)) redundant = true;
2269
+ if (signedCompare(value, domain.upper, domain.isSigned) > 0n) redundant = true;
2088
2270
  else if (value === domain.upper) {
2089
2271
  if (domain.upperInclusive) if (!inclusive) strictlyBetter = true;
2090
2272
  else redundant = true;
2091
2273
  else if (!inclusive) redundant = true;
2092
2274
  } else strictlyBetter = true;
2093
- if (redundant) issues.push(emit.dominated(groupIndex, constraintIndex, bigintToHex(value)));
2275
+ if (redundant) issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2094
2276
  if (strictlyBetter) {
2095
- issues.push(emit.dominated(groupIndex, constraintIndex, bigintToHex(domain.upper)));
2277
+ issues.push(dominatedBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.upper)));
2096
2278
  domain.upper = value;
2097
2279
  domain.upperInclusive = inclusive;
2098
2280
  changedUpper = true;
@@ -2105,16 +2287,16 @@ function updateBound(domain, base, isNegated, value, emit, groupIndex, constrain
2105
2287
  }
2106
2288
  }
2107
2289
  if (changedEq || changedLower) {
2108
- if (domain.hasEq && domain.hasLower) if (domain.lowerInclusive ? isLt(domain.eq, domain.lower, domain.isSigned) : isLte(domain.eq, domain.lower, domain.isSigned)) issues.push(emit.boundsExcludeEquality(groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(domain.lower)));
2109
- else issues.push(emit.redundant(groupIndex, constraintIndex, bigintToHex(domain.lower), bigintToHex(domain.eq)));
2290
+ if (domain.hasEq && domain.hasLower) if (domain.lowerInclusive ? signedCompare(domain.eq, domain.lower, domain.isSigned) < 0n : signedCompare(domain.eq, domain.lower, domain.isSigned) <= 0n) issues.push(boundsExcludeEquality(isLength, groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(domain.lower)));
2291
+ else issues.push(redundantBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.lower), bigintToHex(domain.eq)));
2110
2292
  }
2111
2293
  if (changedEq || changedUpper) {
2112
- if (domain.hasEq && domain.hasUpper) if (domain.upperInclusive ? isGt(domain.eq, domain.upper, domain.isSigned) : isGte(domain.eq, domain.upper, domain.isSigned)) issues.push(emit.boundsExcludeEquality(groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(domain.upper)));
2113
- else issues.push(emit.redundant(groupIndex, constraintIndex, bigintToHex(domain.upper), bigintToHex(domain.eq)));
2294
+ if (domain.hasEq && domain.hasUpper) if (domain.upperInclusive ? signedCompare(domain.eq, domain.upper, domain.isSigned) > 0n : signedCompare(domain.eq, domain.upper, domain.isSigned) >= 0n) issues.push(boundsExcludeEquality(isLength, groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(domain.upper)));
2295
+ else issues.push(redundantBound(isLength, groupIndex, constraintIndex, bigintToHex(domain.upper), bigintToHex(domain.eq)));
2114
2296
  }
2115
2297
  if (changedLower || changedUpper) {
2116
2298
  if (domain.hasLower && domain.hasUpper) {
2117
- if (isGt(domain.lower, domain.upper, domain.isSigned) || domain.lower === domain.upper && (!domain.lowerInclusive || !domain.upperInclusive)) issues.push(emit.impossibleRange(groupIndex, constraintIndex, bigintToHex(domain.lower), bigintToHex(domain.upper)));
2299
+ if (signedCompare(domain.lower, domain.upper, domain.isSigned) > 0n || domain.lower === domain.upper && (!domain.lowerInclusive || !domain.upperInclusive)) issues.push(impossibleRange(isLength, groupIndex, constraintIndex, bigintToHex(domain.lower), bigintToHex(domain.upper)));
2118
2300
  }
2119
2301
  }
2120
2302
  }
@@ -2210,6 +2392,40 @@ function checkDuplicates(issues, operators, groupIndex, constraintIndex) {
2210
2392
  seen.add(key);
2211
2393
  }
2212
2394
  }
2395
+ /**
2396
+ * Report a lone unnegated gte/lte pair (value or length domain) as fusible into the
2397
+ * corresponding single range operator. Fires only for a satisfiable pair on a compatible
2398
+ * type; contradictions and dominated bounds are already reported by the domain checks.
2399
+ */
2400
+ function checkFusibleRange(ctx, operators, groupIndex, constraintIndex, issues) {
2401
+ const numericPair = findLonePair(operators, Op.GTE, Op.LTE);
2402
+ if (numericPair && getIncompat(Op.GTE, ctx.typeInfo) === null && signedCompare(numericPair.low, numericPair.high, ctx.numeric.isSigned) <= 0n) issues.push(fusibleRange(false, groupIndex, constraintIndex, bigintToHex(numericPair.low), bigintToHex(numericPair.high)));
2403
+ const lengthPair = findLonePair(operators, Op.LENGTH_GTE, Op.LENGTH_LTE);
2404
+ if (lengthPair && getIncompat(Op.LENGTH_GTE, ctx.typeInfo) === null && lengthPair.low <= lengthPair.high) issues.push(fusibleRange(true, groupIndex, constraintIndex, bigintToHex(lengthPair.low), bigintToHex(lengthPair.high)));
2405
+ }
2406
+ /** Return the operand pair when the operators contain exactly one of each unnegated bound opcode. */
2407
+ function findLonePair(operators, lowerOp, upperOp) {
2408
+ let lowerCount = 0;
2409
+ let upperCount = 0;
2410
+ let low = 0n;
2411
+ let high = 0n;
2412
+ for (const opHex of operators) {
2413
+ const opCode = parseInt(opHex.slice(2, 4), 16);
2414
+ const dataLength = (opHex.length - 4) / 2;
2415
+ if (dataLength > 65535 || !isValidOperatorData(opCode & ~Op.NOT, dataLength)) continue;
2416
+ if (opCode === lowerOp) {
2417
+ lowerCount++;
2418
+ low = readValue(opHex);
2419
+ } else if (opCode === upperOp) {
2420
+ upperCount++;
2421
+ high = readValue(opHex);
2422
+ }
2423
+ }
2424
+ return lowerCount === 1 && upperCount === 1 ? {
2425
+ low,
2426
+ high
2427
+ } : null;
2428
+ }
2213
2429
  /** Validate all operators on a single constraint against its domain context. */
2214
2430
  function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues) {
2215
2431
  const operators = constraint.operators;
@@ -2218,21 +2434,36 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
2218
2434
  const opCode = parseInt(opHex.slice(2, 4), 16);
2219
2435
  const base = opCode & ~Op.NOT;
2220
2436
  const isNegated = (opCode & Op.NOT) !== 0;
2437
+ const dataLength = (opHex.length - 4) / 2;
2438
+ if (dataLength > 65535 || !isValidOperatorData(base, dataLength)) {
2439
+ issues.push(fromOpRule("UNKNOWN_OPERATOR", "Unknown operator code", groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
2440
+ continue;
2441
+ }
2221
2442
  if (underAny && isNegated) issues.push(negationUnderAny(groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
2222
- const compat = checkCompatibility(base, ctx.typeInfo.typeCode, ctx.typeInfo.isDynamic, ctx.typeInfo.staticSize);
2223
- if (!compat.compatible) {
2224
- issues.push(fromOpRule(compat.code, compat.message, groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
2443
+ const incompat = getIncompat(base, ctx.typeInfo);
2444
+ if (incompat) {
2445
+ issues.push(fromOpRule(incompat.code, incompat.message, groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
2225
2446
  continue;
2226
2447
  }
2227
2448
  if (isValueOp(base)) {
2449
+ const typeCode = ctx.typeInfo.typeCode;
2450
+ if (isLeftAligned(typeCode)) {
2451
+ const nonCanonical = findNonCanonicalWord(opHex, typeCode);
2452
+ if (nonCanonical !== null) {
2453
+ issues.push(nonCanonicalOperand(groupIndex, constraintIndex, bigintToHex(nonCanonical.word), bigintToHex(nonCanonical.canonical)));
2454
+ continue;
2455
+ }
2456
+ }
2228
2457
  if (base >= Op.EQ && base <= Op.BETWEEN) if (base === Op.BETWEEN) {
2229
- const { low, high } = readPair(opHex);
2230
- updateBound(ctx.numeric, Op.GTE, isNegated, low, boundIssues(false), groupIndex, constraintIndex, issues);
2231
- updateBound(ctx.numeric, Op.LTE, isNegated, high, boundIssues(false), groupIndex, constraintIndex, issues);
2458
+ if (!isNegated) {
2459
+ const { low, high } = readPair(opHex);
2460
+ updateBound(ctx.numeric, Op.GTE, false, low, false, groupIndex, constraintIndex, issues);
2461
+ updateBound(ctx.numeric, Op.LTE, false, high, false, groupIndex, constraintIndex, issues);
2462
+ }
2232
2463
  } else {
2233
2464
  const value = readValue(opHex);
2234
2465
  const holesBefore = ctx.numeric.holes.length;
2235
- updateBound(ctx.numeric, base, isNegated, value, boundIssues(false), groupIndex, constraintIndex, issues);
2466
+ updateBound(ctx.numeric, base, isNegated, value, false, groupIndex, constraintIndex, issues);
2236
2467
  if (ctx.numeric.holes.length > holesBefore) checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
2237
2468
  }
2238
2469
  else if (isBitmaskOp(base)) updateBitmask(ctx, base, isNegated, readValue(opHex), groupIndex, constraintIndex, issues);
@@ -2242,12 +2473,14 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
2242
2473
  else updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues);
2243
2474
  }
2244
2475
  } else if (isLengthOp(base)) if (base === Op.LENGTH_BETWEEN) {
2245
- const { low, high } = readPair(opHex);
2246
- updateBound(ctx.length, Op.GTE, isNegated, low, boundIssues(true), groupIndex, constraintIndex, issues);
2247
- updateBound(ctx.length, Op.LTE, isNegated, high, boundIssues(true), groupIndex, constraintIndex, issues);
2476
+ if (!isNegated) {
2477
+ const { low, high } = readPair(opHex);
2478
+ updateBound(ctx.length, Op.GTE, false, low, true, groupIndex, constraintIndex, issues);
2479
+ updateBound(ctx.length, Op.LTE, false, high, true, groupIndex, constraintIndex, issues);
2480
+ }
2248
2481
  } else {
2249
2482
  const value = readValue(opHex);
2250
- updateBound(ctx.length, normalizeLengthOp(base), isNegated, value, boundIssues(true), groupIndex, constraintIndex, issues);
2483
+ updateBound(ctx.length, normalizeLengthOp(base), isNegated, value, true, groupIndex, constraintIndex, issues);
2251
2484
  }
2252
2485
  }
2253
2486
  checkDuplicates(issues, operators, groupIndex, constraintIndex);
@@ -2291,6 +2524,7 @@ function validateGroup(data, descBytes, groupIndex, issues) {
2291
2524
  contexts.push(ctx);
2292
2525
  }
2293
2526
  validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues);
2527
+ checkFusibleRange(ctx, constraint.operators, groupIndex, constraintIndex, issues);
2294
2528
  }
2295
2529
  }
2296
2530
  /**
@@ -2508,6 +2742,12 @@ var PolicyBuilder = class PolicyBuilder {
2508
2742
  };
2509
2743
  //#endregion
2510
2744
  //#region src/constraint.ts
2745
+ /** Strip an optional 0x prefix and validate a 40-hex-char (20-byte) address body. */
2746
+ function addressBody(value) {
2747
+ const body = value.startsWith("0x") ? value.slice(2) : value;
2748
+ if (body.length !== 40) throw new CallciumError("INVALID_CONSTRAINT", `Invalid address length: expected 40 hex chars, got ${body.length}`);
2749
+ return body;
2750
+ }
2511
2751
  /** Convert a scalar value to a 32-byte big-endian word. */
2512
2752
  function encodeWord(value) {
2513
2753
  const word = new Uint8Array(32);
@@ -2516,8 +2756,7 @@ function encodeWord(value) {
2516
2756
  return word;
2517
2757
  }
2518
2758
  if (typeof value === "string") {
2519
- const body = value.startsWith("0x") ? value.slice(2) : value;
2520
- if (body.length !== 40) throw new CallciumError("INVALID_CONSTRAINT", `Invalid address length: expected 40 hex chars, got ${body.length}`);
2759
+ const body = addressBody(value);
2521
2760
  for (let i = 0; i < 20; i++) {
2522
2761
  const byte = parseInt(body.substring(i * 2, i * 2 + 2), 16);
2523
2762
  if (Number.isNaN(byte)) throw new CallciumError("INVALID_CONSTRAINT", `Invalid hex character in address at position ${i * 2}`);
@@ -2554,8 +2793,7 @@ function setOp(opCode, values) {
2554
2793
  if (typeof v === "bigint") return v;
2555
2794
  if (typeof v === "number") return BigInt(v);
2556
2795
  if (typeof v === "boolean") return v ? 1n : 0n;
2557
- const body = v.startsWith("0x") ? v.slice(2) : v;
2558
- return BigInt("0x" + (body || "0"));
2796
+ return BigInt("0x" + addressBody(v));
2559
2797
  });
2560
2798
  bigs.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
2561
2799
  const deduped = [];
@@ -2754,7 +2992,7 @@ function descendTuple(node, callData, head, base, childIndex) {
2754
2992
  if (node.isDynamic) {
2755
2993
  const ptrResult = readPointer(callData, head);
2756
2994
  if (typeof ptrResult !== "number") return {
2757
- type: "violation",
2995
+ ok: false,
2758
2996
  code: ptrResult.code
2759
2997
  };
2760
2998
  tupleBase = base + ptrResult;
@@ -2765,42 +3003,48 @@ function descendTuple(node, callData, head, base, childIndex) {
2765
3003
  }
2766
3004
  for (let i = 0; i < childIndex; i++) cursor += headContribution(node.fields[i]);
2767
3005
  return {
2768
- type: "ok",
2769
- head: cursor,
2770
- base: tupleBase,
2771
- node: node.fields[childIndex]
3006
+ ok: true,
3007
+ location: {
3008
+ head: cursor,
3009
+ base: tupleBase,
3010
+ node: node.fields[childIndex]
3011
+ }
2772
3012
  };
2773
3013
  }
2774
3014
  /** Descend into a dynamic array element by index. */
2775
3015
  function descendDynamicArray(node, callData, head, base, childIndex) {
2776
3016
  const arrayBaseResult = readPointer(callData, head);
2777
3017
  if (typeof arrayBaseResult !== "number") return {
2778
- type: "violation",
3018
+ ok: false,
2779
3019
  code: arrayBaseResult.code
2780
3020
  };
2781
3021
  const arrayBase = base + arrayBaseResult;
2782
3022
  const lengthResult = readPointer(callData, arrayBase);
2783
3023
  if (typeof lengthResult !== "number") return {
2784
- type: "violation",
3024
+ ok: false,
2785
3025
  code: lengthResult.code
2786
3026
  };
2787
3027
  if (childIndex >= lengthResult) return {
2788
- type: "violation",
3028
+ ok: false,
2789
3029
  code: "ARRAY_INDEX_OUT_OF_BOUNDS"
2790
3030
  };
2791
3031
  const headsBase = arrayBase + 32;
2792
3032
  const elem = node.element;
2793
3033
  if (elem.isDynamic) return {
2794
- type: "ok",
2795
- head: headsBase + childIndex * 32,
2796
- base: headsBase,
2797
- node: elem
3034
+ ok: true,
3035
+ location: {
3036
+ head: headsBase + childIndex * 32,
3037
+ base: headsBase,
3038
+ node: elem
3039
+ }
2798
3040
  };
2799
3041
  return {
2800
- type: "ok",
2801
- head: headsBase + childIndex * elem.staticSize,
2802
- base: arrayBase,
2803
- node: elem
3042
+ ok: true,
3043
+ location: {
3044
+ head: headsBase + childIndex * elem.staticSize,
3045
+ base: arrayBase,
3046
+ node: elem
3047
+ }
2804
3048
  };
2805
3049
  }
2806
3050
  /** Descend into a static array element by index. */
@@ -2810,22 +3054,26 @@ function descendStaticArray(node, callData, head, base, childIndex) {
2810
3054
  if (elem.isDynamic) {
2811
3055
  const ptrResult = readPointer(callData, head);
2812
3056
  if (typeof ptrResult !== "number") return {
2813
- type: "violation",
3057
+ ok: false,
2814
3058
  code: ptrResult.code
2815
3059
  };
2816
3060
  const arrayBase = base + ptrResult;
2817
3061
  return {
2818
- type: "ok",
2819
- head: arrayBase + childIndex * 32,
2820
- base: arrayBase,
2821
- node: elem
3062
+ ok: true,
3063
+ location: {
3064
+ head: arrayBase + childIndex * 32,
3065
+ base: arrayBase,
3066
+ node: elem
3067
+ }
2822
3068
  };
2823
3069
  }
2824
3070
  return {
2825
- type: "ok",
2826
- head: head + childIndex * elem.staticSize,
2827
- base,
2828
- node: elem
3071
+ ok: true,
3072
+ location: {
3073
+ head: head + childIndex * elem.staticSize,
3074
+ base,
3075
+ node: elem
3076
+ }
2829
3077
  };
2830
3078
  }
2831
3079
  /**
@@ -2865,13 +3113,13 @@ function locate(tree, callData, pathBytes, baseOffset) {
2865
3113
  };
2866
3114
  }
2867
3115
  const result = descend(node, callData, head, base, step);
2868
- if (result.type === "violation") return {
3116
+ if (!result.ok) return {
2869
3117
  ok: false,
2870
3118
  code: result.code
2871
3119
  };
2872
- head = result.head;
2873
- base = result.base;
2874
- node = result.node;
3120
+ head = result.location.head;
3121
+ base = result.location.base;
3122
+ node = result.location.node;
2875
3123
  }
2876
3124
  return {
2877
3125
  ok: true,
@@ -2883,6 +3131,29 @@ function locate(tree, callData, pathBytes, baseOffset) {
2883
3131
  }
2884
3132
  };
2885
3133
  }
3134
+ /**
3135
+ * Build the element-shape fields shared by dynamic- and static-array nodes, branching only on
3136
+ * whether the element itself is dynamic.
3137
+ */
3138
+ function makeArrayShape(length, elem, dynamicHeadsBase, staticDataOffset, staticCompositeBase) {
3139
+ return elem.isDynamic ? {
3140
+ length,
3141
+ elementNode: elem,
3142
+ elementIsDynamic: true,
3143
+ elementStaticSize: 0,
3144
+ headsBase: dynamicHeadsBase,
3145
+ dataOffset: 0,
3146
+ compositeBase: dynamicHeadsBase
3147
+ } : {
3148
+ length,
3149
+ elementNode: elem,
3150
+ elementIsDynamic: false,
3151
+ elementStaticSize: elem.staticSize,
3152
+ headsBase: 0,
3153
+ dataOffset: staticDataOffset,
3154
+ compositeBase: staticCompositeBase
3155
+ };
3156
+ }
2886
3157
  /** Extract array shape from a location pointing to an array node. */
2887
3158
  function arrayShape(callData, location) {
2888
3159
  const { head, base, node } = location;
@@ -2898,66 +3169,26 @@ function arrayShape(callData, location) {
2898
3169
  ok: false,
2899
3170
  code: lengthResult.code
2900
3171
  };
2901
- const elem = node.element;
2902
3172
  const headsBase = arrayBase + 32;
2903
- if (elem.isDynamic) return {
2904
- ok: true,
2905
- shape: {
2906
- length: lengthResult,
2907
- elementNode: elem,
2908
- elementIsDynamic: true,
2909
- elementStaticSize: 0,
2910
- headsBase,
2911
- dataOffset: 0,
2912
- compositeBase: headsBase
2913
- }
2914
- };
2915
3173
  return {
2916
3174
  ok: true,
2917
- shape: {
2918
- length: lengthResult,
2919
- elementNode: elem,
2920
- elementIsDynamic: false,
2921
- elementStaticSize: elem.staticSize,
2922
- headsBase: 0,
2923
- dataOffset: headsBase,
2924
- compositeBase: arrayBase
2925
- }
3175
+ shape: makeArrayShape(lengthResult, node.element, headsBase, headsBase, arrayBase)
2926
3176
  };
2927
3177
  }
2928
3178
  if (node.type === "staticArray") {
2929
3179
  const elem = node.element;
3180
+ let dynamicHeadsBase = 0;
2930
3181
  if (elem.isDynamic) {
2931
3182
  const arrayBaseResult = readPointer(callData, head);
2932
3183
  if (typeof arrayBaseResult !== "number") return {
2933
3184
  ok: false,
2934
3185
  code: arrayBaseResult.code
2935
3186
  };
2936
- const arrayBase = base + arrayBaseResult;
2937
- return {
2938
- ok: true,
2939
- shape: {
2940
- length: node.length,
2941
- elementNode: elem,
2942
- elementIsDynamic: true,
2943
- elementStaticSize: 0,
2944
- headsBase: arrayBase,
2945
- dataOffset: 0,
2946
- compositeBase: arrayBase
2947
- }
2948
- };
3187
+ dynamicHeadsBase = base + arrayBaseResult;
2949
3188
  }
2950
3189
  return {
2951
3190
  ok: true,
2952
- shape: {
2953
- length: node.length,
2954
- elementNode: elem,
2955
- elementIsDynamic: false,
2956
- elementStaticSize: elem.staticSize,
2957
- headsBase: 0,
2958
- dataOffset: head,
2959
- compositeBase: base
2960
- }
3191
+ shape: makeArrayShape(node.length, elem, dynamicHeadsBase, head, base)
2961
3192
  };
2962
3193
  }
2963
3194
  throw new CallciumError("INVALID_PATH", `Cannot compute array shape for non-array node (${node.type}).`);
@@ -3035,13 +3266,13 @@ function descendPath(callData, location, pathBytes) {
3035
3266
  for (let stepIndex = 0; stepIndex < stepCount; stepIndex++) {
3036
3267
  const step = readStep(pathBytes, stepIndex);
3037
3268
  const result = descend(node, callData, head, base, step);
3038
- if (result.type === "violation") return {
3269
+ if (!result.ok) return {
3039
3270
  ok: false,
3040
3271
  code: result.code
3041
3272
  };
3042
- head = result.head;
3043
- base = result.base;
3044
- node = result.node;
3273
+ head = result.location.head;
3274
+ base = result.location.base;
3275
+ node = result.location.node;
3045
3276
  }
3046
3277
  return {
3047
3278
  ok: true,
@@ -3227,6 +3458,7 @@ function applyLeafOperator(callDataBytes, location, opCode, operandData) {
3227
3458
  resolvedValue: bigintToHex(BigInt(valueLength))
3228
3459
  };
3229
3460
  }
3461
+ if (node.isDynamic || node.type !== "elementary") throw new CallciumError("NOT_SCALAR", "Value operator on a target without a scalar word.");
3230
3462
  const result = loadScalar(callDataBytes, location);
3231
3463
  if (!result.ok) return { error: result.code };
3232
3464
  const value = canonicalize(toBigInt(result.value, 0), node.typeCode);