@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/README.md +19 -10
- package/dist/index.cjs +354 -46
- package/dist/index.d.cts +92 -40
- package/dist/index.d.ts +92 -40
- package/dist/index.js +350 -47
- package/package.json +2 -2
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
-
})()
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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 (!
|
|
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"
|
|
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
|
-
|
|
301
|
-
|
|
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
|
|
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
|
|
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 -
|
|
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 >=
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": ">=
|
|
49
|
+
"node": ">=24"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|