@omgbase/oqx 0.10.2 → 0.12.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/CHANGELOG.md +162 -0
- package/README.md +130 -30
- package/dist/context.d.ts +18 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +21 -9
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +80 -61
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +47 -21
- package/dist/lexer.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +356 -163
- package/dist/parser.js.map +1 -1
- package/dist/regex.d.ts +19 -0
- package/dist/regex.d.ts.map +1 -0
- package/dist/regex.js +0 -0
- package/dist/regex.js.map +1 -0
- package/dist/semantics.d.ts +37 -9
- package/dist/semantics.d.ts.map +1 -1
- package/dist/semantics.js +138 -36
- package/dist/semantics.js.map +1 -1
- package/package.json +8 -7
- package/src/context.ts +34 -9
- package/src/engine.ts +81 -60
- package/src/index.ts +7 -1
- package/src/lexer.ts +44 -14
- package/src/parser.ts +340 -119
- package/src/regex.ts +0 -0
- package/src/semantics.ts +128 -31
package/src/regex.ts
ADDED
|
Binary file
|
package/src/semantics.ts
CHANGED
|
@@ -10,12 +10,20 @@
|
|
|
10
10
|
// • Absence: `undefined` and `null` are the same absent value.
|
|
11
11
|
// • Equality (== / !=): typed and strict — no cross-type coercion, so
|
|
12
12
|
// `5 == "5"` is false. Two absent values are equal.
|
|
13
|
-
// • Ordering (< <= > >=):
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// •
|
|
17
|
-
//
|
|
13
|
+
// • Ordering (< <= > >=): only number/number and string/string order — numbers
|
|
14
|
+
// numerically, strings by Unicode code point. Every other pairing (an absent
|
|
15
|
+
// operand, mixed types, booleans, containers) is false; nothing throws.
|
|
16
|
+
// • Arithmetic: an absent operand makes the result absent (`+` included, even
|
|
17
|
+
// when the other side is a string). `+` with a string side concatenates the
|
|
18
|
+
// operands' string forms.
|
|
19
|
+
// • Truthiness: absent, false, 0, -0, and "" are falsy; everything else truthy.
|
|
20
|
+
// • `in`: membership in an array (by ==), substring in a string, OWN key of an
|
|
18
21
|
// object, or coverage by a range (lo..hi / lo...hi / open-ended).
|
|
22
|
+
// • Identity / structural keys: `canonicalKey` — a type-tagged serialization,
|
|
23
|
+
// so `1` and `"1"` differ and object key order is ignored. Never `String()`.
|
|
24
|
+
|
|
25
|
+
import { compileRegex } from "./regex.ts";
|
|
26
|
+
import type { RegexDialect } from "./regex.ts";
|
|
19
27
|
|
|
20
28
|
/** Equality with absence-normalization (undefined ≡ null) and strict typing. */
|
|
21
29
|
export function equals(a: unknown, b: unknown): boolean {
|
|
@@ -24,23 +32,58 @@ export function equals(a: unknown, b: unknown): boolean {
|
|
|
24
32
|
return x === y;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
/**
|
|
35
|
+
/** Compare two strings by Unicode code point (not UTF-16 code unit): -1, 0, 1. */
|
|
36
|
+
export function compareStrings(a: string, b: string): number {
|
|
37
|
+
if (a === b) return 0;
|
|
38
|
+
let i = 0, j = 0;
|
|
39
|
+
while (i < a.length && j < b.length) {
|
|
40
|
+
const ca = a.codePointAt(i)!, cb = b.codePointAt(j)!;
|
|
41
|
+
if (ca !== cb) return ca < cb ? -1 : 1;
|
|
42
|
+
i += ca > 0xffff ? 2 : 1;
|
|
43
|
+
j += cb > 0xffff ? 2 : 1;
|
|
44
|
+
}
|
|
45
|
+
return i < a.length ? 1 : j < b.length ? -1 : 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The ordering of two values, or `undefined` when they do not order: only two
|
|
49
|
+
* numbers (numerically) or two strings (by code point) order. Absent operands,
|
|
50
|
+
* mixed types, booleans, arrays, and objects never order. */
|
|
51
|
+
export function compare(a: unknown, b: unknown): number | undefined {
|
|
52
|
+
if (typeof a === "number" && typeof b === "number") return a < b ? -1 : a > b ? 1 : 0;
|
|
53
|
+
if (typeof a === "string" && typeof b === "string") return compareStrings(a, b);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The six relational operators. Pairs that do not order (see `compare`) yield false. */
|
|
28
58
|
export function relate(op: string, a: unknown, b: unknown): boolean {
|
|
29
59
|
if (op === "==") return equals(a, b);
|
|
30
60
|
if (op === "!=") return !equals(a, b);
|
|
31
|
-
|
|
61
|
+
const c = compare(a, b);
|
|
62
|
+
if (c === undefined) return false;
|
|
32
63
|
switch (op) {
|
|
33
|
-
case "<": return
|
|
34
|
-
case "<=": return
|
|
35
|
-
case ">": return
|
|
36
|
-
case ">=": return
|
|
64
|
+
case "<": return c < 0;
|
|
65
|
+
case "<=": return c <= 0;
|
|
66
|
+
case ">": return c > 0;
|
|
67
|
+
case ">=": return c >= 0;
|
|
37
68
|
default: throw new Error(`not a relational operator: ${op}`);
|
|
38
69
|
}
|
|
39
70
|
}
|
|
40
71
|
|
|
41
|
-
/**
|
|
72
|
+
/** The string form of a scalar (the text `+` concatenates and `.lower()` maps):
|
|
73
|
+
* a string is itself; a number renders as a double with no fraction when
|
|
74
|
+
* integer-valued (`-0` is `"0"`); booleans are the words. Absent has no string
|
|
75
|
+
* form (`undefined`), so the operation using it yields absent. */
|
|
76
|
+
export function stringForm(v: unknown): string | undefined {
|
|
77
|
+
if (v == null) return undefined;
|
|
78
|
+
if (typeof v === "string") return v;
|
|
79
|
+
return String(v);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Arithmetic. An absent operand makes the result absent; `+` concatenates the
|
|
83
|
+
* string forms when either side is a string. */
|
|
42
84
|
export function arith(op: string, a: unknown, b: unknown): unknown {
|
|
43
|
-
if (
|
|
85
|
+
if (a == null || b == null) return undefined;
|
|
86
|
+
if (op === "+" && (typeof a === "string" || typeof b === "string")) return stringForm(a)! + stringForm(b)!;
|
|
44
87
|
const x = toNumber(a);
|
|
45
88
|
const y = toNumber(b);
|
|
46
89
|
switch (op) {
|
|
@@ -57,8 +100,8 @@ export function membership(needle: unknown, haystack: unknown): boolean {
|
|
|
57
100
|
if (haystack == null) return false;
|
|
58
101
|
if (isRange(haystack)) return rangeCovers(haystack, needle);
|
|
59
102
|
if (Array.isArray(haystack)) return haystack.some((x) => equals(x, needle));
|
|
60
|
-
if (typeof haystack === "string") return haystack.includes(
|
|
61
|
-
if (typeof haystack === "object")
|
|
103
|
+
if (typeof haystack === "string") { const s = stringForm(needle); return s !== undefined && haystack.includes(s); }
|
|
104
|
+
if (typeof haystack === "object") { const s = stringForm(needle); return s !== undefined && Object.hasOwn(haystack, s); }
|
|
62
105
|
return false;
|
|
63
106
|
}
|
|
64
107
|
|
|
@@ -84,12 +127,13 @@ export function isRange(v: unknown): v is OqxRange {
|
|
|
84
127
|
}
|
|
85
128
|
|
|
86
129
|
/** Whether `x` falls within `range`: `lo <= x` (when lo is present) and either
|
|
87
|
-
* `x <= hi` (inclusive) or `x < hi` (exclusive end) (when hi is present).
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
130
|
+
* `x <= hi` (inclusive) or `x < hi` (exclusive end) (when hi is present). Only a
|
|
131
|
+
* number or a string can be covered — an absent `x` is covered by no range, not
|
|
132
|
+
* even a fully open one — and bound checks go through `relate`, so an `x` that
|
|
133
|
+
* doesn't order against a bound (mixed types) is simply not covered (never
|
|
134
|
+
* throws). ISO-8601 strings therefore make date ranges work. */
|
|
92
135
|
export function rangeCovers(range: OqxRange, x: unknown): boolean {
|
|
136
|
+
if (typeof x !== "number" && typeof x !== "string") return false;
|
|
93
137
|
const geLo = range.lo == null || relate(">=", x, range.lo);
|
|
94
138
|
const leHi = range.hi == null || relate(range.exclusiveEnd ? "<" : "<=", x, range.hi);
|
|
95
139
|
return geLo && leHi;
|
|
@@ -103,13 +147,25 @@ export function toNumber(v: unknown): number {
|
|
|
103
147
|
return typeof v === "number" ? v : Number(v);
|
|
104
148
|
}
|
|
105
149
|
|
|
150
|
+
/** `size(x)`: a string's code-point count, an array's length, an object's own
|
|
151
|
+
* key count; absent and other scalars are 0. */
|
|
106
152
|
export function sizeOf(v: unknown): number {
|
|
107
153
|
if (v == null) return 0;
|
|
108
|
-
if (typeof v === "string"
|
|
154
|
+
if (typeof v === "string") return codePointCount(v);
|
|
155
|
+
if (Array.isArray(v)) return v.length;
|
|
109
156
|
if (typeof v === "object") return Object.keys(v).length;
|
|
110
157
|
return 0;
|
|
111
158
|
}
|
|
112
159
|
|
|
160
|
+
function codePointCount(s: string): number {
|
|
161
|
+
let n = 0;
|
|
162
|
+
for (let i = 0; i < s.length; i++) {
|
|
163
|
+
n++;
|
|
164
|
+
if (s.codePointAt(i)! > 0xffff) i++;
|
|
165
|
+
}
|
|
166
|
+
return n;
|
|
167
|
+
}
|
|
168
|
+
|
|
113
169
|
export function toList(v: unknown): unknown[] {
|
|
114
170
|
if (v == null) return [];
|
|
115
171
|
if (Array.isArray(v)) return v;
|
|
@@ -131,15 +187,38 @@ export function coerceCollection(v: unknown): unknown[] {
|
|
|
131
187
|
return [v];
|
|
132
188
|
}
|
|
133
189
|
|
|
134
|
-
/**
|
|
190
|
+
/** Order for `order by`: absent values sort last; present values order by
|
|
191
|
+
* `compare` (numbers numerically, strings by code point); a pair that does not
|
|
192
|
+
* order (mixed types, booleans) is left where it is (compares equal). */
|
|
135
193
|
export function compareForSort(a: unknown, b: unknown): number {
|
|
136
194
|
const an = a == null, bn = b == null;
|
|
137
195
|
if (an && bn) return 0;
|
|
138
196
|
if (an) return 1;
|
|
139
197
|
if (bn) return -1;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
198
|
+
return compare(a, b) ?? 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---- structural identity ----------------------------------------------------
|
|
202
|
+
|
|
203
|
+
/** A canonical, type-tagged serialization of a value, used wherever OQX needs a
|
|
204
|
+
* value's IDENTITY as a key (`follow` cycle detection and dedup, `distinct`):
|
|
205
|
+
* two values get the same key iff they are structurally equal — absent ≡ null,
|
|
206
|
+
* numbers as doubles (`-0` ≡ `0`), object key order ignored. The type tags keep
|
|
207
|
+
* `1`, `"1"`, and `true` apart, and `{}`/`[]` apart from every scalar. */
|
|
208
|
+
export function canonicalKey(v: unknown): string {
|
|
209
|
+
if (v == null) return "n";
|
|
210
|
+
switch (typeof v) {
|
|
211
|
+
case "boolean": return v ? "t" : "f";
|
|
212
|
+
case "number": return `d${v === 0 ? 0 : v}`;
|
|
213
|
+
case "string": return `s${JSON.stringify(v)}`;
|
|
214
|
+
case "object": {
|
|
215
|
+
if (Array.isArray(v)) return `[${v.map(canonicalKey).join(",")}]`;
|
|
216
|
+
if (isEntry(v)) return canonicalKey(v.value);
|
|
217
|
+
const o = v as Record<string, unknown>;
|
|
218
|
+
return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${canonicalKey(o[k])}`).join(",")}}`;
|
|
219
|
+
}
|
|
220
|
+
default: return `?${String(v)}`;
|
|
221
|
+
}
|
|
143
222
|
}
|
|
144
223
|
|
|
145
224
|
/** Parse a string as a range value (`1..5`, `1...5`, `..5`, `5..`, or an
|
|
@@ -219,17 +298,35 @@ export const BUILTIN_FUNCTIONS: Record<string, (args: unknown[]) => unknown> = {
|
|
|
219
298
|
range: (args) => (isRange(args[0]) ? args[0] : typeof args[0] === "string" ? parseRangeString(args[0]) : null),
|
|
220
299
|
};
|
|
221
300
|
|
|
301
|
+
// ---- regex: the OQX baseline ------------------------------------------------
|
|
302
|
+
//
|
|
303
|
+
// `matches(pattern, flags)` compiles the pattern in the OQX regex baseline (see
|
|
304
|
+
// regex.ts): a fixed grammar with spec-pinned semantics, rejected up front when
|
|
305
|
+
// it uses anything else, so every pattern that runs is portable. A host may opt
|
|
306
|
+
// into its engine's native dialect through `DataContext.regexDialect`.
|
|
307
|
+
|
|
308
|
+
export { compileRegex, parseRegexFlags } from "./regex.ts";
|
|
309
|
+
export type { RegexDialect, RegexFlags } from "./regex.ts";
|
|
310
|
+
|
|
311
|
+
/** `recv.matches(pattern, flags?)`: false for an absent receiver; otherwise
|
|
312
|
+
* whether the pattern (compiled in `dialect`) matches anywhere in the
|
|
313
|
+
* receiver's string form. Bad flags or a bad pattern are eval errors. */
|
|
314
|
+
export function regexMatches(recv: unknown, args: unknown[], dialect: RegexDialect = "oqx"): boolean {
|
|
315
|
+
const s = stringForm(recv);
|
|
316
|
+
return s !== undefined && compileRegex(String(args[0]), args[1], dialect).test(s);
|
|
317
|
+
}
|
|
318
|
+
|
|
222
319
|
/** Methods callable as `recv.name(args)`. */
|
|
223
320
|
export const BUILTIN_METHODS: Record<string, (recv: unknown, args: unknown[]) => unknown> = {
|
|
224
321
|
contains: (recv, args) => {
|
|
225
|
-
if (typeof recv === "string")
|
|
322
|
+
if (typeof recv === "string") { const s = stringForm(args[0]); return s !== undefined && recv.includes(s); }
|
|
226
323
|
if (Array.isArray(recv)) return recv.some((x) => equals(x, args[0]));
|
|
227
324
|
return false;
|
|
228
325
|
},
|
|
229
|
-
startsWith: (recv, args) => typeof recv === "string" && recv.startsWith(
|
|
230
|
-
endsWith: (recv, args) => typeof recv === "string" && recv.endsWith(
|
|
231
|
-
matches: (recv, args) => recv
|
|
326
|
+
startsWith: (recv, args) => { const s = stringForm(args[0]); return typeof recv === "string" && s !== undefined && recv.startsWith(s); },
|
|
327
|
+
endsWith: (recv, args) => { const s = stringForm(args[0]); return typeof recv === "string" && s !== undefined && recv.endsWith(s); },
|
|
328
|
+
matches: (recv, args) => regexMatches(recv, args, "oqx"),
|
|
232
329
|
size: (recv) => sizeOf(recv),
|
|
233
|
-
lower: (recv) =>
|
|
234
|
-
upper: (recv) =>
|
|
330
|
+
lower: (recv) => stringForm(recv)?.toLowerCase(),
|
|
331
|
+
upper: (recv) => stringForm(recv)?.toUpperCase(),
|
|
235
332
|
};
|