@coldsmirk/abacus-core 0.4.1 → 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
@@ -1,4 +1,3 @@
1
- //#region src/engine/errors.ts
2
1
  var ExpressionError = class extends Error {
3
2
  expression;
4
3
  constructor(message, expression, cause) {
@@ -13,8 +12,42 @@ var ExpressionNotReadyError = class extends ExpressionError {
13
12
  this.name = "ExpressionNotReadyError";
14
13
  }
15
14
  };
16
- //#endregion
17
- //#region src/internal/predicates.ts
15
+ function isAscii(source) {
16
+ for (let index = 0; index < source.length; index += 1) if (source.codePointAt(index) > 127) return false;
17
+ return true;
18
+ }
19
+ function utf8ByteLength(codePoint) {
20
+ if (codePoint < 128) return 1;
21
+ if (codePoint < 2048) return 2;
22
+ return codePoint < 65536 ? 3 : 4;
23
+ }
24
+ function utf8OffsetConverter(source) {
25
+ if (isAscii(source)) return (byteOffset) => Math.max(0, Math.min(byteOffset, source.length));
26
+ const byteStarts = [];
27
+ const unitStarts = [];
28
+ let bytes = 0;
29
+ let units = 0;
30
+ for (const char of source) {
31
+ byteStarts.push(bytes);
32
+ unitStarts.push(units);
33
+ bytes += utf8ByteLength(char.codePointAt(0));
34
+ units += char.length;
35
+ }
36
+ byteStarts.push(bytes);
37
+ unitStarts.push(units);
38
+ return (byteOffset) => {
39
+ if (byteOffset <= 0) return 0;
40
+ if (byteOffset >= bytes) return source.length;
41
+ let low = 0;
42
+ let high = byteStarts.length - 1;
43
+ while (low < high) {
44
+ const middle = low + high + 1 >> 1;
45
+ if (byteStarts[middle] <= byteOffset) low = middle;
46
+ else high = middle - 1;
47
+ }
48
+ return unitStarts[low];
49
+ };
50
+ }
18
51
  function isUndefined(value) {
19
52
  return value === void 0;
20
53
  }
@@ -30,13 +63,13 @@ function isNullish(value) {
30
63
  function isRecord(value) {
31
64
  return typeof value === "object" && value !== null && !Array.isArray(value);
32
65
  }
33
- //#endregion
34
- //#region src/engine/loader.ts
35
66
  let enginePromise = null;
36
67
  let engineSync = null;
37
68
  let engineError = null;
38
69
  let configuredInput;
39
70
  let typeContextCache = null;
71
+ let loadEpoch = 0;
72
+ let lastLoad = null;
40
73
  function evaluateSafely(expression, run) {
41
74
  try {
42
75
  return run();
@@ -56,9 +89,13 @@ function configureEngine(options) {
56
89
  function loadEngine() {
57
90
  if (enginePromise) return enginePromise;
58
91
  engineError = null;
59
- enginePromise = (async () => {
92
+ const epoch = loadEpoch;
93
+ const predecessor = lastLoad;
94
+ const input = configuredInput;
95
+ const load = (async () => {
96
+ if (predecessor) await predecessor.catch(() => void 0);
60
97
  const zen = await import("@gorules/zen-engine-wasm");
61
- await zen.default(isUndefined(configuredInput) ? void 0 : { module_or_path: configuredInput });
98
+ await zen.default(isUndefined(input) ? void 0 : { module_or_path: input });
62
99
  const toVariableType = (type) => zen.VariableType.fromJson(type);
63
100
  const ensureTypeContext = (variables) => {
64
101
  if (typeContextCache && typeContextCache.variables === variables) return typeContextCache;
@@ -87,9 +124,19 @@ function loadEngine() {
87
124
  analyze: (variables, source, unary) => {
88
125
  const context = ensureTypeContext(variables);
89
126
  const rawSpans = unary ? context.handle.typeCheckUnary(source) : context.handle.typeCheck(source);
127
+ if (!Array.isArray(rawSpans)) return {
128
+ rootKind: context.rootKind,
129
+ spans: []
130
+ };
131
+ const toUtf16 = utf8OffsetConverter(source);
90
132
  return {
91
133
  rootKind: context.rootKind,
92
- spans: Array.isArray(rawSpans) ? rawSpans : []
134
+ spans: rawSpans.map((span) => {
135
+ return {
136
+ ...span,
137
+ span: [toUtf16(span.span[0]), toUtf16(span.span[1])]
138
+ };
139
+ })
93
140
  };
94
141
  },
95
142
  satisfies: (actual, expected) => {
@@ -107,12 +154,17 @@ function loadEngine() {
107
154
  },
108
155
  isReady: () => zen.isReady()
109
156
  });
110
- engineSync = engine;
157
+ if (epoch === loadEpoch) engineSync = engine;
111
158
  return engine;
112
- })().catch((error) => {
113
- enginePromise = null;
114
- engineError = new ExpressionError(loadFailureMessage(), void 0, error);
115
- throw engineError;
159
+ })();
160
+ lastLoad = load;
161
+ enginePromise = load.catch((error) => {
162
+ const failure = new ExpressionError(loadFailureMessage(), void 0, error);
163
+ if (epoch === loadEpoch) {
164
+ enginePromise = null;
165
+ engineError = failure;
166
+ }
167
+ throw failure;
116
168
  });
117
169
  return enginePromise;
118
170
  }
@@ -127,6 +179,7 @@ function getEngineSync() {
127
179
  return engineSync;
128
180
  }
129
181
  function resetEngine() {
182
+ loadEpoch += 1;
130
183
  typeContextCache?.handle.free();
131
184
  typeContextCache = null;
132
185
  enginePromise = null;
@@ -134,16 +187,178 @@ function resetEngine() {
134
187
  engineError = null;
135
188
  configuredInput = void 0;
136
189
  }
137
- //#endregion
138
- //#region src/internal/env.ts
139
190
  const isDev = detectDev();
140
191
  function detectDev() {
141
192
  if (typeof process === "undefined") return false;
142
193
  return process.env ? process.env.NODE_ENV !== "production" : false;
143
194
  }
144
- //#endregion
145
- //#region src/condition/subject.ts
195
+ const U64_MAX = 18446744073709551615n;
196
+ const I64_MIN_MAGNITUDE = 9223372036854775808n;
197
+ const ZEN_DECIMAL_MANTISSA_LIMIT = 2n ** 96n;
198
+ const ZEN_DECIMAL_MANTISSA_LIMIT_TEXT = String(ZEN_DECIMAL_MANTISSA_LIMIT);
199
+ const DECIMAL_SIGN_BIT = 2n ** 95n;
200
+ const EXCESS_PRECISION_LIMIT = 2n ** 52n;
201
+ const NUMBER_TEXT_PATTERN = /^(?<integer>\d+)(?:\.(?<fraction>\d+))?(?:e(?<exponent>[+-]\d+))?$/;
202
+ const UNSIGNED_INTEGER_TEXT_PATTERN = /^\d+$/;
203
+ function isZenUnsignedIntegerText(value) {
204
+ if (!UNSIGNED_INTEGER_TEXT_PATTERN.test(value)) return false;
205
+ const normalized = value.replace(/^0+(?=\d)/, "");
206
+ return normalized.length < ZEN_DECIMAL_MANTISSA_LIMIT_TEXT.length || normalized.length === ZEN_DECIMAL_MANTISSA_LIMIT_TEXT.length && normalized < ZEN_DECIMAL_MANTISSA_LIMIT_TEXT;
207
+ }
208
+ function powerOfTen(exponent) {
209
+ return Number(`1e${exponent}`);
210
+ }
211
+ function normalize(digits, exponent) {
212
+ let d = digits;
213
+ let e = exponent;
214
+ while (d !== 0n && d % 10n === 0n) {
215
+ d /= 10n;
216
+ e += 1;
217
+ }
218
+ return d === 0n ? {
219
+ digits: 0n,
220
+ exponent: 0
221
+ } : {
222
+ digits: d,
223
+ exponent: e
224
+ };
225
+ }
226
+ function parseAsSerdeJson(text) {
227
+ const match = NUMBER_TEXT_PATTERN.exec(text);
228
+ if (match?.groups === void 0) return null;
229
+ const { integer, fraction, exponent } = match.groups;
230
+ let significand = 0n;
231
+ let scale = 0;
232
+ let overflowed = false;
233
+ for (const digit of integer) {
234
+ if (!overflowed && significand * 10n + BigInt(digit) > U64_MAX) overflowed = true;
235
+ if (overflowed) scale += 1;
236
+ else significand = significand * 10n + BigInt(digit);
237
+ }
238
+ if (fraction === void 0 && exponent === void 0 && !overflowed) return {
239
+ kind: "int",
240
+ int: significand,
241
+ f64: 0
242
+ };
243
+ if (fraction !== void 0) for (const digit of fraction) {
244
+ if (overflowed || significand * 10n + BigInt(digit) > U64_MAX) break;
245
+ significand = significand * 10n + BigInt(digit);
246
+ scale -= 1;
247
+ }
248
+ if (exponent !== void 0) scale += Number(exponent);
249
+ let value = Number(significand);
250
+ let remaining = scale;
251
+ for (;;) {
252
+ if (Math.abs(remaining) <= 308) {
253
+ value = remaining >= 0 ? value * powerOfTen(remaining) : value / powerOfTen(-remaining);
254
+ break;
255
+ }
256
+ if (value === 0) break;
257
+ if (remaining >= 0) return null;
258
+ value /= 1e308;
259
+ remaining += 308;
260
+ }
261
+ return Number.isFinite(value) ? {
262
+ kind: "f64",
263
+ int: 0n,
264
+ f64: value
265
+ } : null;
266
+ }
267
+ function decimalFromF64(value) {
268
+ if (value === 0) return {
269
+ digits: 0n,
270
+ exponent: 0
271
+ };
272
+ const view = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(8));
273
+ view.setFloat64(0, value);
274
+ const raw = view.getBigUint64(0);
275
+ const biasedExponent = Number(raw >> 52n & 2047n);
276
+ const fractionBits = raw & 4503599627370495n;
277
+ let bits = biasedExponent === 0 ? fractionBits : fractionBits | 1n << 52n;
278
+ const exponent2 = (biasedExponent === 0 ? 1 : biasedExponent) - 1023 - 52;
279
+ let exponent5 = -exponent2;
280
+ let exponent10 = exponent2;
281
+ while (exponent5 > 0) if ((bits & 1n) === 0n) {
282
+ exponent10 += 1;
283
+ exponent5 -= 1;
284
+ bits >>= 1n;
285
+ } else {
286
+ exponent5 -= 1;
287
+ const timesFive = bits * 5n;
288
+ if (timesFive < ZEN_DECIMAL_MANTISSA_LIMIT) bits = timesFive;
289
+ else {
290
+ exponent10 += 1;
291
+ bits >>= 1n;
292
+ }
293
+ }
294
+ while (exponent5 < 0) if ((bits & DECIMAL_SIGN_BIT) === 0n) {
295
+ exponent10 -= 1;
296
+ exponent5 += 1;
297
+ bits <<= 1n;
298
+ } else if (exponent10 * 2 > -exponent5) return null;
299
+ else {
300
+ exponent5 += 1;
301
+ bits /= 5n;
302
+ }
303
+ while (exponent10 > 0) {
304
+ const timesTen = bits * 10n;
305
+ if (timesTen >= ZEN_DECIMAL_MANTISSA_LIMIT) return null;
306
+ bits = timesTen;
307
+ exponent10 -= 1;
308
+ }
309
+ while (exponent10 < -28) {
310
+ const remainder = bits % 10n;
311
+ bits /= 10n;
312
+ exponent10 += 1;
313
+ if (bits === 0n) exponent10 = 0;
314
+ else if (remainder >= 5n) bits += 1n;
315
+ }
316
+ while (exponent10 < 0 && bits >= EXCESS_PRECISION_LIMIT) {
317
+ const remainder = bits % 10n;
318
+ bits /= 10n;
319
+ exponent10 += 1;
320
+ if (remainder >= 5n) bits += 1n;
321
+ }
322
+ while (exponent10 < 0 && bits % 10n === 0n) {
323
+ bits /= 10n;
324
+ exponent10 += 1;
325
+ }
326
+ return {
327
+ digits: bits,
328
+ exponent: exponent10
329
+ };
330
+ }
331
+ function contextDecimal(value) {
332
+ const magnitude = Math.abs(value);
333
+ if (magnitude === 0) return {
334
+ digits: 0n,
335
+ exponent: 0
336
+ };
337
+ const parsed = parseAsSerdeJson(String(magnitude));
338
+ if (parsed === null) return null;
339
+ if (parsed.kind === "int") {
340
+ if (value >= 0 || parsed.int <= I64_MIN_MAGNITUDE) return normalize(parsed.int, 0);
341
+ const viaF64 = decimalFromF64(Number(parsed.int));
342
+ return viaF64 === null ? null : normalize(viaF64.digits, viaF64.exponent);
343
+ }
344
+ const converted = decimalFromF64(parsed.f64);
345
+ return converted === null ? null : normalize(converted.digits, converted.exponent);
346
+ }
347
+ function literalDecimal(value) {
348
+ const match = NUMBER_TEXT_PATTERN.exec(String(Math.abs(value)));
349
+ if (match?.groups === void 0) return null;
350
+ const { integer, fraction = "", exponent = "0" } = match.groups;
351
+ return normalize(BigInt(integer + fraction), Number(exponent) - fraction.length);
352
+ }
353
+ function isZenConsistentNumber(value) {
354
+ if (!Number.isFinite(value)) return false;
355
+ const literal = literalDecimal(value);
356
+ if (literal === null || literal.digits >= ZEN_DECIMAL_MANTISSA_LIMIT) return false;
357
+ const context = contextDecimal(value);
358
+ return context !== null && literal.digits === context.digits && literal.exponent === context.exponent;
359
+ }
146
360
  const SUBJECT_PATTERN = /^[A-Z_$][\w$]*(?:\.[A-Z_$][\w$]*|\[\d+\])*$/i;
361
+ const SUBJECT_INDEX_PATTERN = /\[(?<index>\d+)\]/g;
147
362
  const ZEN_RESERVED_WORDS = new Set([
148
363
  "and",
149
364
  "or",
@@ -154,17 +369,24 @@ const ZEN_RESERVED_WORDS = new Set([
154
369
  "null"
155
370
  ]);
156
371
  function isIdentifierPath(subject) {
157
- return SUBJECT_PATTERN.test(subject) && (subject.match(/[A-Z_$][\w$]*/gi) ?? []).every((segment) => !ZEN_RESERVED_WORDS.has(segment));
372
+ if (!SUBJECT_PATTERN.test(subject) || (subject.match(/[A-Z_$][\w$]*/gi) ?? []).some((segment) => ZEN_RESERVED_WORDS.has(segment))) return false;
373
+ for (const match of subject.matchAll(SUBJECT_INDEX_PATTERN)) if (match.groups?.index === void 0 || !isZenUnsignedIntegerText(match.groups.index)) return false;
374
+ return true;
375
+ }
376
+ function isZenRepresentableNumber(value) {
377
+ return isZenConsistentNumber(value);
158
378
  }
159
- //#endregion
160
- //#region src/condition/compile.ts
161
379
  function toZenLiteral(value) {
162
380
  if (isNullish(value)) return "null";
163
381
  if (typeof value === "number") {
164
- if (!Number.isFinite(value)) throw new ExpressionError(`Number ${String(value)} has no ZEN literal representation`);
382
+ if (!isZenRepresentableNumber(value)) throw new ExpressionError(`Number ${String(value)} has no faithful ZEN literal representation`);
383
+ return String(value);
384
+ }
385
+ if (typeof value === "bigint") {
386
+ if (value >= ZEN_DECIMAL_MANTISSA_LIMIT || -value >= ZEN_DECIMAL_MANTISSA_LIMIT) throw new ExpressionError(`Bigint ${String(value)} has no ZEN literal representation`);
165
387
  return String(value);
166
388
  }
167
- if (typeof value === "boolean" || typeof value === "bigint") return String(value);
389
+ if (typeof value === "boolean") return String(value);
168
390
  if (isString(value)) return encodeZenString(value);
169
391
  if (isArray(value)) return `[${value.map((item) => toZenLiteral(item)).join(", ")}]`;
170
392
  throw new ExpressionError(`Value of type "${typeof value}" has no ZEN literal representation`);
@@ -260,8 +482,6 @@ function reportEvaluationFailure(engine, expression, error) {
260
482
  async function selectBranch(branches, context) {
261
483
  return selectBranchWith(branches, context, await loadEngine());
262
484
  }
263
- //#endregion
264
- //#region src/condition/types.ts
265
485
  const CONDITION_OPERATORS = [
266
486
  "eq",
267
487
  "ne",
@@ -297,9 +517,83 @@ const CONDITION_OPERATOR_ARITIES = {
297
517
  function conditionOperatorArity(operator) {
298
518
  return CONDITION_OPERATOR_ARITIES[operator];
299
519
  }
300
- //#endregion
301
- //#region src/condition/compile-tree.ts
520
+ const MAX_CONDITION_TREE_DEPTH = 64;
521
+ const CONDITION_TREE_OPERATORS = CONDITION_OPERATORS;
522
+ const nodeIdRealm = Math.random().toString(36).slice(2, 7);
523
+ let nodeIdCounter = 0;
524
+ function newConditionNodeId() {
525
+ nodeIdCounter += 1;
526
+ return `cn-${nodeIdRealm}-${nodeIdCounter}`;
527
+ }
528
+ function emptyConditionGroup() {
529
+ return {
530
+ kind: "group",
531
+ id: newConditionNodeId(),
532
+ op: "and",
533
+ items: []
534
+ };
535
+ }
536
+ function assertConditionTreeStructure(tree) {
537
+ if (!isRecord(tree) || tree.kind !== "group") throw new ExpressionError("Condition tree root must be a group");
538
+ const ancestors = /* @__PURE__ */ new Set();
539
+ const stack = [{
540
+ group: tree,
541
+ depth: 0,
542
+ leaving: false
543
+ }];
544
+ while (stack.length > 0) {
545
+ const frame = stack.pop();
546
+ const { group } = frame;
547
+ if (frame.leaving) {
548
+ ancestors.delete(group);
549
+ continue;
550
+ }
551
+ if (frame.depth > 64) throw new ExpressionError(`Condition tree exceeds the maximum depth of 64`);
552
+ if (ancestors.has(group)) throw new ExpressionError("Condition tree contains a cycle");
553
+ if (group.op !== "and" && group.op !== "or") throw new ExpressionError(`Unsupported group operator: ${String(group.op)}`);
554
+ if (!isArray(group.items)) throw new ExpressionError("Condition tree group items must be an array");
555
+ ancestors.add(group);
556
+ stack.push({
557
+ ...frame,
558
+ leaving: true
559
+ });
560
+ for (let index = group.items.length - 1; index >= 0; index -= 1) {
561
+ const node = group.items[index];
562
+ if (!isRecord(node) || node.kind !== "group" && node.kind !== "rule") throw new ExpressionError("Condition tree contains an invalid node");
563
+ if (node.kind === "group") stack.push({
564
+ group: node,
565
+ depth: frame.depth + 1,
566
+ leaving: false
567
+ });
568
+ }
569
+ }
570
+ }
571
+ function withRuleId(rule) {
572
+ return rule.id === void 0 ? {
573
+ ...rule,
574
+ id: newConditionNodeId()
575
+ } : rule;
576
+ }
577
+ function withGroupIds(group) {
578
+ const items = group.items.map((item) => item.kind === "group" ? withGroupIds(item) : withRuleId(item));
579
+ if (group.id !== void 0 && items.every((item, index) => item === group.items[index])) return group;
580
+ return {
581
+ ...group,
582
+ id: group.id ?? newConditionNodeId(),
583
+ items
584
+ };
585
+ }
586
+ const normalizedTrees = /* @__PURE__ */ new WeakMap();
587
+ function ensureConditionNodeIds(tree) {
588
+ assertConditionTreeStructure(tree);
589
+ const cached = normalizedTrees.get(tree);
590
+ if (cached) return cached;
591
+ const normalized = withGroupIds(tree);
592
+ normalizedTrees.set(tree, normalized);
593
+ return normalized;
594
+ }
302
595
  function compileConditionTree(tree) {
596
+ assertConditionTreeStructure(tree);
303
597
  const normalized = normalizeNode(tree);
304
598
  return normalized === null ? "" : emitNode(normalized, true);
305
599
  }
@@ -316,9 +610,16 @@ function normalizeNode(node) {
316
610
  }
317
611
  function emitNode(node, topLevel) {
318
612
  if (node.kind === "rule") return compileRule(node);
319
- const joined = node.items.map((item) => emitNode(item, false)).join(node.op === "and" ? " and " : " or ");
613
+ const joined = node.items.map((item) => emitNode(item, false)).join(groupJoiner(node.op));
320
614
  return topLevel ? joined : `(${joined})`;
321
615
  }
616
+ function groupJoiner(op) {
617
+ switch (op) {
618
+ case "and": return " and ";
619
+ case "or": return " or ";
620
+ default: throw new ExpressionError(`Unsupported group operator: ${String(op)}`);
621
+ }
622
+ }
322
623
  function compileRule(rule) {
323
624
  if (!matchesOperatorArity(rule)) return null;
324
625
  return compileCondition({
@@ -341,8 +642,6 @@ function isConditionScalar(value) {
341
642
  function isScalarArray(value) {
342
643
  return isArray(value) && value.every((item) => isConditionScalar(item));
343
644
  }
344
- //#endregion
345
- //#region src/condition/lift-tree.ts
346
645
  const TWO_CHAR_PUNCTUATION = new Set([
347
646
  "==",
348
647
  "!=",
@@ -365,6 +664,10 @@ const IDENT_PART = /[\w$]/;
365
664
  const DIGIT = /\d/;
366
665
  const NUMBER_PATTERN = /^\d+(?:\.\d+)?(?:e[+-]?\d+)?/i;
367
666
  const INTEGER_PATTERN = /^\d+$/;
667
+ function parseCanonicalNumber(text) {
668
+ const value = Number(text);
669
+ return String(value) === text && isZenRepresentableNumber(value) ? value : null;
670
+ }
368
671
  const COMPARISON_OPERATORS = {
369
672
  "==": "eq",
370
673
  "!=": "ne",
@@ -378,7 +681,6 @@ const CALL_OPERATORS = {
378
681
  startsWith: "starts_with",
379
682
  endsWith: "ends_with"
380
683
  };
381
- const MAX_GROUP_DEPTH = 64;
382
684
  function liftConditionTree(expression) {
383
685
  const tokens = tokenize(expression);
384
686
  if (tokens === null || tokens.length === 0) return null;
@@ -424,8 +726,10 @@ function liftConditionTree(expression) {
424
726
  return token.value;
425
727
  }
426
728
  if (token.kind === "number") {
729
+ const value = parseCanonicalNumber(token.value);
730
+ if (value === null) return null;
427
731
  pos += 1;
428
- return Number(token.value);
732
+ return value;
429
733
  }
430
734
  if (token.kind === "ident") {
431
735
  if (token.value === "true") {
@@ -441,8 +745,10 @@ function liftConditionTree(expression) {
441
745
  if (token.kind === "punct" && token.value === "-") {
442
746
  const digits = peek(1);
443
747
  if (digits === void 0 || digits.kind !== "number") return null;
748
+ const value = parseCanonicalNumber(digits.value);
749
+ if (value === null || value === 0 || !isZenRepresentableNumber(-value)) return null;
444
750
  pos += 2;
445
- return -Number(digits.value);
751
+ return -value;
446
752
  }
447
753
  return null;
448
754
  }
@@ -471,6 +777,7 @@ function liftConditionTree(expression) {
471
777
  if (right === null || !consumePunct(")")) return null;
472
778
  return {
473
779
  kind: "rule",
780
+ id: newConditionNodeId(),
474
781
  left,
475
782
  operator,
476
783
  right
@@ -483,6 +790,7 @@ function liftConditionTree(expression) {
483
790
  if (right === null || !consumePunct(")")) return null;
484
791
  return {
485
792
  kind: "rule",
793
+ id: newConditionNodeId(),
486
794
  left,
487
795
  operator: "not_in",
488
796
  right
@@ -514,6 +822,7 @@ function liftConditionTree(expression) {
514
822
  pos += expected.length;
515
823
  return {
516
824
  kind: "rule",
825
+ id: newConditionNodeId(),
517
826
  left: read.path,
518
827
  operator
519
828
  };
@@ -549,6 +858,7 @@ function liftConditionTree(expression) {
549
858
  const right = parseArray();
550
859
  return right === null ? null : {
551
860
  kind: "rule",
861
+ id: newConditionNodeId(),
552
862
  left,
553
863
  operator: "in",
554
864
  right
@@ -561,6 +871,7 @@ function liftConditionTree(expression) {
561
871
  const right = parseLiteral();
562
872
  return right === null ? null : {
563
873
  kind: "rule",
874
+ id: newConditionNodeId(),
564
875
  left,
565
876
  operator,
566
877
  right
@@ -572,7 +883,7 @@ function liftConditionTree(expression) {
572
883
  const emptiness = tryEmptiness();
573
884
  if (emptiness !== null) return emptiness;
574
885
  if (isPunct(peek(), "(")) {
575
- if (depth >= MAX_GROUP_DEPTH) return null;
886
+ if (depth >= 64) return null;
576
887
  pos += 1;
577
888
  const inner = parseGroup(depth + 1);
578
889
  if (inner === null || !consumePunct(")")) return null;
@@ -602,6 +913,7 @@ function liftConditionTree(expression) {
602
913
  }
603
914
  return items.length === 1 ? items[0] : {
604
915
  kind: "group",
916
+ id: newConditionNodeId(),
605
917
  op: op ?? "and",
606
918
  items
607
919
  };
@@ -640,6 +952,7 @@ function readPath(tokens, from) {
640
952
  function asGroup(node) {
641
953
  return node.kind === "group" ? node : {
642
954
  kind: "group",
955
+ id: newConditionNodeId(),
643
956
  op: "and",
644
957
  items: [node]
645
958
  };
@@ -707,11 +1020,6 @@ function tokenize(input) {
707
1020
  }
708
1021
  return tokens;
709
1022
  }
710
- //#endregion
711
- //#region src/condition/tree-types.ts
712
- const CONDITION_TREE_OPERATORS = CONDITION_OPERATORS;
713
- //#endregion
714
- //#region src/engine/evaluate.ts
715
1023
  async function evaluate(expression, context) {
716
1024
  await loadEngine();
717
1025
  return evaluateSync(expression, context);
@@ -726,8 +1034,6 @@ function evaluateSync(expression, context) {
726
1034
  function evaluateUnarySync(expression, context) {
727
1035
  return getEngineSync().evaluateUnary(expression, context);
728
1036
  }
729
- //#endregion
730
- //#region src/engine/messages.ts
731
1037
  const COMPLETION_INFO_ZH = {
732
1038
  "Returns the length of variable": "返回变量的长度",
733
1039
  "Checks if variable contains a needle": "检查变量是否包含指定元素",
@@ -860,8 +1166,6 @@ function configureExpressionMessages({ locale, messages }) {
860
1166
  function getExpressionMessages() {
861
1167
  return activeMessages;
862
1168
  }
863
- //#endregion
864
- //#region src/engine/intellisense.ts
865
1169
  function parseOffset(text) {
866
1170
  const trimmed = text?.trim();
867
1171
  return trimmed ? Number(trimmed) : NaN;
@@ -880,7 +1184,9 @@ function normalizeDiagnostic(raw, source) {
880
1184
  if (raw === null || raw === void 0) return null;
881
1185
  const errorType = isRecord(raw) && typeof raw.type === "string" ? raw.type : void 0;
882
1186
  const message = isRecord(raw) && typeof raw.source === "string" ? raw.source : String(raw);
883
- const [from, to] = extractPosition(message) ?? [0, source.length];
1187
+ const bytePosition = extractPosition(message);
1188
+ const toUtf16 = utf8OffsetConverter(source);
1189
+ const [from, to] = bytePosition === null ? [0, source.length] : [toUtf16(bytePosition[0]), toUtf16(bytePosition[1])];
884
1190
  return {
885
1191
  from,
886
1192
  to,
@@ -932,8 +1238,6 @@ async function satisfiesType(actual, expected) {
932
1238
  function satisfiesTypeSync(actual, expected) {
933
1239
  return getEngineSync().satisfies(actual, expected);
934
1240
  }
935
- //#endregion
936
- //#region src/engine/template.ts
937
1241
  const HOLE_PATTERN = /\{\{(?<expression>[^{}]*)\}\}/g;
938
1242
  function parseTemplateHoles(source) {
939
1243
  const holes = [];
@@ -994,5 +1298,4 @@ async function getTemplateDiagnostics(source) {
994
1298
  await loadEngine();
995
1299
  return getTemplateDiagnosticsSync(source);
996
1300
  }
997
- //#endregion
998
- export { CONDITION_OPERATORS, CONDITION_TREE_OPERATORS, ExpressionError, ExpressionNotReadyError, analyzeTemplate, analyzeTemplateSync, analyzeTypes, analyzeTypesSync, compileBranch, compileCondition, compileConditionTree, compileGroup, conditionOperatorArity, configureEngine, configureExpressionMessages, enMessages, evaluate, evaluateSync, evaluateUnary, evaluateUnarySync, getCompletionItems, getCompletionItemsSync, getDiagnostics, getDiagnosticsSync, getEngineError, getEngineSync, getExpressionMessages, getTemplateDiagnostics, getTemplateDiagnosticsSync, isEngineReady, liftConditionTree, loadEngine, parseTemplateHoles, registerExpressionLocale, resetEngine, satisfiesType, satisfiesTypeSync, selectBranch, selectBranchWith, templateHoleAt, toZenLiteral, zhCNMessages };
1301
+ export { CONDITION_OPERATORS, CONDITION_TREE_OPERATORS, ExpressionError, ExpressionNotReadyError, MAX_CONDITION_TREE_DEPTH, analyzeTemplate, analyzeTemplateSync, analyzeTypes, analyzeTypesSync, compileBranch, compileCondition, compileConditionTree, compileGroup, conditionOperatorArity, configureEngine, configureExpressionMessages, emptyConditionGroup, enMessages, ensureConditionNodeIds, evaluate, evaluateSync, evaluateUnary, evaluateUnarySync, getCompletionItems, getCompletionItemsSync, getDiagnostics, getDiagnosticsSync, getEngineError, getEngineSync, getExpressionMessages, getTemplateDiagnostics, getTemplateDiagnosticsSync, isEngineReady, isZenRepresentableNumber, liftConditionTree, loadEngine, newConditionNodeId, parseTemplateHoles, registerExpressionLocale, resetEngine, satisfiesType, satisfiesTypeSync, selectBranch, selectBranchWith, templateHoleAt, toZenLiteral, zhCNMessages };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldsmirk/abacus-core",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Framework-agnostic ZEN expression engine: compile, evaluate, and type-analyze expressions over the GoRules ZEN WASM engine.",
5
5
  "keywords": [
6
6
  "zen",
@@ -46,7 +46,7 @@
46
46
  "@gorules/zen-engine-wasm": "^0.23.1"
47
47
  },
48
48
  "engines": {
49
- "node": ">=22"
49
+ "node": ">=24"
50
50
  },
51
51
  "publishConfig": {
52
52
  "access": "public"