@bitspark/horos 0.0.0-stage → 0.6.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/LICENSE +202 -0
- package/LICENSE-MIT +21 -0
- package/NOTICE +6 -0
- package/README.md +10 -2
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +26 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/core.d.ts +77 -0
- package/dist/src/core.js +936 -0
- package/dist/src/core.js.map +1 -0
- package/dist/src/diagnose.d.ts +56 -0
- package/dist/src/diagnose.js +399 -0
- package/dist/src/diagnose.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +55 -4
package/dist/src/core.js
ADDED
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* horos — the form contract (TypeScript core).
|
|
3
|
+
*
|
|
4
|
+
* A byte-for-byte port of the Rust reference core (core/rs/src/lib.rs) and the Go
|
|
5
|
+
* core (core/go/horos.go). It implements the v1 judgment surface — wf(T) and v : T
|
|
6
|
+
* (`conforms`) — over the horos/* descriptor DSL, as TOTAL, SEARCH-FREE structural
|
|
7
|
+
* recursion over the finite trees of the value v and the descriptor T (both ordinary
|
|
8
|
+
* ontos-core Values). Scalar and collection recognition delegate to the frozen
|
|
9
|
+
* ontos/data recognizers; horos re-mints nothing.
|
|
10
|
+
*
|
|
11
|
+
* See docs/spec/horos.md (the frozen v1 surface) and
|
|
12
|
+
* docs/architecture/decisions/0002-form-reasoning-cut.md.
|
|
13
|
+
*
|
|
14
|
+
* Both `wf` and `conforms` are TOTAL (they never throw), SEARCH-FREE, and
|
|
15
|
+
* byte-for-byte equivalent to the Rust reference; the shared
|
|
16
|
+
* vectors/conformance.json oracle pins the three cores (go/rs/ts) together.
|
|
17
|
+
*
|
|
18
|
+
* rec/ref guarded recursion
|
|
19
|
+
* -------------------------
|
|
20
|
+
* `rec` binds a recursion name to its body descriptor; `ref` unfolds it, but only
|
|
21
|
+
* when guarded — every unfolding must have descended into a strict substructure of v
|
|
22
|
+
* since the binding. `env` binds each in-scope rec name to its body; `pending` holds
|
|
23
|
+
* the names bound or unfolded at the current v-node WITHOUT a descent since, so a
|
|
24
|
+
* `ref` to a pending name is unguarded (it would loop) and yields false. Descending
|
|
25
|
+
* into a child value clears `pending` (a constructor of v was consumed); same-v
|
|
26
|
+
* combinators (union/and/not) thread it unchanged. Termination: between two
|
|
27
|
+
* unfoldings of the same `ref` there must be a descent, and each descent strictly
|
|
28
|
+
* shrinks the finite v — so unfoldings are bounded by |v|.
|
|
29
|
+
*/
|
|
30
|
+
import { Atom, Tuple, bytesEqual } from "@bitspark/ontos-core";
|
|
31
|
+
import { readBool, readInt, readList, readMap, readSet, readText, recognizeInt, } from "@bitspark/ontos-data";
|
|
32
|
+
/**
|
|
33
|
+
* The reserved descriptor-label namespace. Every descriptor combinator is a
|
|
34
|
+
* `Tuple(Atom("horos/<ctor>"), args...)`; a tuple whose first child is not an atom
|
|
35
|
+
* under this prefix is not a horos descriptor (per ontos-data §1.1 reservation).
|
|
36
|
+
*/
|
|
37
|
+
export const NAMESPACE = "horos/";
|
|
38
|
+
/**
|
|
39
|
+
* The inclusive i128 range an `atom-len` bound must fit in to be well-formed (horos-internal#18):
|
|
40
|
+
* `[-2^127, 2^127 - 1]`. The Rust reference reads bounds into an `i128` and errors
|
|
41
|
+
* (→ non-wf / false) past this range; our `readInt` returns an unbounded `bigint`, so we
|
|
42
|
+
* apply the SAME cap explicitly via {@link atomLenBoundInRange}. The three cores then
|
|
43
|
+
* agree byte-for-byte at the cap — a bound = 2^127 is non-wf in all of them — and below
|
|
44
|
+
* it for every real byte-length bound. This makes atom-len byte-identity a theorem rather
|
|
45
|
+
* than a "never happens" argument; byte-length bounds are tiny, so it rejects nothing
|
|
46
|
+
* meaningful. (The Go core mirrors this with `atomLenBound*`, the Rust core implicitly via
|
|
47
|
+
* its `read_int` → i128 materialization.)
|
|
48
|
+
*/
|
|
49
|
+
export const ATOM_LEN_BOUND_MAX = (1n << 127n) - 1n; // 2^127 - 1 = i128::MAX
|
|
50
|
+
export const ATOM_LEN_BOUND_MIN = -(1n << 127n); // -2^127 = i128::MIN
|
|
51
|
+
/**
|
|
52
|
+
* Whether a bound fits in the signed 128-bit range — the explicit analogue of the Rust
|
|
53
|
+
* core's `read_int` succeeding (vs. returning `IntOutOfRange`). A bound outside the range
|
|
54
|
+
* makes the atom-len descriptor non-wf / non-conforming, matching Rust and Go.
|
|
55
|
+
*/
|
|
56
|
+
function atomLenBoundInRange(n) {
|
|
57
|
+
return n >= ATOM_LEN_BOUND_MIN && n <= ATOM_LEN_BOUND_MAX;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The shared recursion-depth guard (horos-internal#37) — the SAME constant in all three cores (Rust
|
|
61
|
+
* `MAX_RECURSION_DEPTH`, Go `MaxRecursionDepth`). `conformsIn`/`wfIn` recurse on the
|
|
62
|
+
* native stack; termination is bounded by `|v|` (the `pending` guard) but there is no
|
|
63
|
+
* stack bound, so an adversarial-but-finite value (e.g. a tuple nested hundreds of
|
|
64
|
+
* thousands deep) could overflow the stack — a `RangeError: Maximum call stack size
|
|
65
|
+
* exceeded` — at a core-dependent depth, breaking the total/never-throws contract.
|
|
66
|
+
*
|
|
67
|
+
* Past `MAX_RECURSION_DEPTH` frames, `conformsIn`/`wfIn` return a DETERMINISTIC result
|
|
68
|
+
* (`false` / non-wf), never a throw, at the same logical depth as Rust and Go (each
|
|
69
|
+
* increments a depth counter at identical recursion points). Termination is thus bounded
|
|
70
|
+
* by `min(|v|, MAX_RECURSION_DEPTH)`; the cap is a robustness guardrail, not a change to
|
|
71
|
+
* the recognized language for in-bound inputs. `512` is far above any descriptor/value
|
|
72
|
+
* the spec, tests, or vectors exercise (< ~30, so no judgment changes) and comfortably
|
|
73
|
+
* below the smallest native stack budget of the three cores (validated empirically).
|
|
74
|
+
*/
|
|
75
|
+
export const MAX_RECURSION_DEPTH = 512;
|
|
76
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
77
|
+
/** Push `head` onto `tail`, returning a new node that shares `tail` (no copy). */
|
|
78
|
+
function cons(head, tail) {
|
|
79
|
+
return { head, tail };
|
|
80
|
+
}
|
|
81
|
+
/** Whether the name chain holds `name` (by content) — the analogue of `pending.contains`. */
|
|
82
|
+
function consContainsName(s, name) {
|
|
83
|
+
for (let n = s; n !== undefined; n = n.tail) {
|
|
84
|
+
if (bytesEqual(n.head, name))
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* If `t` is `Tuple(Atom("horos/<ctor>"), args...)` with a valid-UTF-8 label under the
|
|
91
|
+
* reserved namespace, return `{ ctor, args }`. Otherwise `undefined` (not a horos
|
|
92
|
+
* descriptor). Total — never throws.
|
|
93
|
+
*
|
|
94
|
+
* The Rust core requires the label to be valid UTF-8 (`str::from_utf8`); we mirror
|
|
95
|
+
* that with a fatal UTF-8 decode. Every ctor we match against is ASCII, so a non-UTF-8
|
|
96
|
+
* label never matches a known ctor and falls through to a miss either way; the explicit
|
|
97
|
+
* validity gate mirrors Rust/Go without changing any decision.
|
|
98
|
+
*/
|
|
99
|
+
function head(t) {
|
|
100
|
+
if (!(t instanceof Tuple))
|
|
101
|
+
return undefined;
|
|
102
|
+
const items = t.items();
|
|
103
|
+
if (items.length === 0)
|
|
104
|
+
return undefined;
|
|
105
|
+
const labelChild = items[0];
|
|
106
|
+
if (!(labelChild instanceof Atom))
|
|
107
|
+
return undefined;
|
|
108
|
+
let label;
|
|
109
|
+
try {
|
|
110
|
+
label = utf8Decoder.decode(labelChild.bytes());
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
if (!label.startsWith(NAMESPACE))
|
|
116
|
+
return undefined;
|
|
117
|
+
return { ctor: label.slice(NAMESPACE.length), args: items.slice(1) };
|
|
118
|
+
}
|
|
119
|
+
/** Read the bytes of an atom value, or `undefined` if it is a tuple. */
|
|
120
|
+
function atomBytes(v) {
|
|
121
|
+
return v instanceof Atom ? v.bytes() : undefined;
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// data-encoded refinements (horos-internal#68) — the four named grammar-class predicates the
|
|
125
|
+
// visual-atlas consumer needs (atlas#533 Q4 / visual-atlas#534), each a closed,
|
|
126
|
+
// total, deterministic, single-value predicate (spec §5 admissibility). They
|
|
127
|
+
// measure Unicode CHARACTERS (code points), not bytes, so the multi-byte cases
|
|
128
|
+
// where char-count ≠ byte-count agree with the Rust and Go cores. Implemented as
|
|
129
|
+
// explicit, dependency-free code-point scans (no RegExp) so the three cores decide
|
|
130
|
+
// byte-for-byte. CHARACTER count means CODE POINTS, not UTF-16 units: `[...s]` /
|
|
131
|
+
// `Array.from(s)` iterate the string by code point (a single astral character is
|
|
132
|
+
// one unit), matching Rust's `chars()` and Go's runes exactly. The §5 grammar
|
|
133
|
+
// literals are ASCII, so a non-ASCII code point never satisfies a grammar class.
|
|
134
|
+
/** Whether a code point is an ASCII lowercase letter a–z. */
|
|
135
|
+
function isAsciiLower(cp) {
|
|
136
|
+
return cp >= 0x61 && cp <= 0x7a;
|
|
137
|
+
}
|
|
138
|
+
/** Whether a code point is an ASCII digit 0–9. */
|
|
139
|
+
function isAsciiDigit(cp) {
|
|
140
|
+
return cp >= 0x30 && cp <= 0x39;
|
|
141
|
+
}
|
|
142
|
+
/** Whether a code point is an ASCII hex digit 0–9 / a–f (lowercase only). */
|
|
143
|
+
function isLowerHex(cp) {
|
|
144
|
+
return isAsciiDigit(cp) || (cp >= 0x61 && cp <= 0x66);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The fixed ASCII whitespace set used by {@link isNonBlank} — mirrored byte-for-byte
|
|
148
|
+
* in the Rust (`is_ascii_ws`) and Go (`isASCIIWS`) cores: space, tab, LF, CR, VT, FF.
|
|
149
|
+
* Deliberately NOT Unicode `White_Space` (which varies by runtime and would break
|
|
150
|
+
* tri-core parity).
|
|
151
|
+
*/
|
|
152
|
+
function isAsciiWs(cp) {
|
|
153
|
+
return (cp === 0x20 || cp === 0x09 || cp === 0x0a || cp === 0x0d || cp === 0x0b || cp === 0x0c);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* `horos/key-grammar`: the decoded text matches `^[a-z0-9][a-z0-9._-]{0,63}$` — a
|
|
157
|
+
* per-code-point class test over the decoded string (1–64 code points total).
|
|
158
|
+
*/
|
|
159
|
+
function matchesKeyGrammar(s) {
|
|
160
|
+
const cps = [...s];
|
|
161
|
+
if (cps.length === 0 || cps.length > 64)
|
|
162
|
+
return false;
|
|
163
|
+
const first = cps[0].codePointAt(0);
|
|
164
|
+
if (!(isAsciiLower(first) || isAsciiDigit(first)))
|
|
165
|
+
return false;
|
|
166
|
+
for (let i = 1; i < cps.length; i += 1) {
|
|
167
|
+
const cp = cps[i].codePointAt(0);
|
|
168
|
+
if (!(isAsciiLower(cp) || isAsciiDigit(cp) || cp === 0x2e || cp === 0x5f || cp === 0x2d)) {
|
|
169
|
+
return false; // . _ -
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* `horos/text-non-blank`: the decoded text has at least one non-whitespace code
|
|
176
|
+
* point. A non-ASCII code point is never whitespace (so it counts as non-blank),
|
|
177
|
+
* mirroring the Rust/Go cores exactly.
|
|
178
|
+
*/
|
|
179
|
+
function isNonBlank(s) {
|
|
180
|
+
for (const ch of s) {
|
|
181
|
+
if (!isAsciiWs(ch.codePointAt(0)))
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* `horos/lowercase-uuid`: the decoded text matches the lowercase canonical UUID
|
|
188
|
+
* grammar `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$` — scanned
|
|
189
|
+
* by code point so a multi-byte char in a malformed input is rejected cleanly.
|
|
190
|
+
*/
|
|
191
|
+
function matchesLowercaseUuid(s) {
|
|
192
|
+
const groups = [8, 4, 4, 4, 12];
|
|
193
|
+
const cps = [...s];
|
|
194
|
+
let pos = 0;
|
|
195
|
+
for (let gi = 0; gi < groups.length; gi += 1) {
|
|
196
|
+
for (let i = 0; i < groups[gi]; i += 1) {
|
|
197
|
+
if (pos >= cps.length || !isLowerHex(cps[pos].codePointAt(0)))
|
|
198
|
+
return false;
|
|
199
|
+
pos += 1;
|
|
200
|
+
}
|
|
201
|
+
if (gi < groups.length - 1) {
|
|
202
|
+
if (pos >= cps.length || cps[pos].codePointAt(0) !== 0x2d)
|
|
203
|
+
return false; // hyphen
|
|
204
|
+
pos += 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return pos === cps.length; // no trailing code points
|
|
208
|
+
}
|
|
209
|
+
// ===========================================================================
|
|
210
|
+
// conformance — v : T
|
|
211
|
+
// ===========================================================================
|
|
212
|
+
/**
|
|
213
|
+
* Decide `v : T` — "does the value `v` exhibit the form described by `T`?"
|
|
214
|
+
*
|
|
215
|
+
* TOTAL and search-free. A value that does not conform — and a `T` that is not a
|
|
216
|
+
* well-formed descriptor — both yield `false`; this never errors and never throws. A
|
|
217
|
+
* value may conform to many descriptors at once (overlapping classification, not a
|
|
218
|
+
* new identity).
|
|
219
|
+
*/
|
|
220
|
+
export function conforms(v, t) {
|
|
221
|
+
// Soundness gate (spec §3 MUST): `v : T` presupposes `wf(T)`. A descriptor that is
|
|
222
|
+
// not well-formed admits no values, even where an individual arm would otherwise
|
|
223
|
+
// match — so check `wf` once at the entry and never arm-by-arm below.
|
|
224
|
+
return wf(t) && conformsIn(v, t, undefined, undefined, 0);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* `conforms` with the recursion environment threaded through:
|
|
228
|
+
*
|
|
229
|
+
* - `env` binds each in-scope `rec` name to its body descriptor.
|
|
230
|
+
* - `pending` holds the names bound or unfolded at the current v-node without a
|
|
231
|
+
* descent since — a `ref` to a pending name is unguarded (it would loop) and
|
|
232
|
+
* yields `false`. Descending into a child value clears `pending`; same-v
|
|
233
|
+
* combinators (union/and/not) thread it unchanged.
|
|
234
|
+
*/
|
|
235
|
+
function conformsIn(v, t, env, pending, depth) {
|
|
236
|
+
// Recursion-depth guard (horos-internal#37): past MAX_RECURSION_DEPTH frames, return a deterministic
|
|
237
|
+
// `false` rather than risk a native-stack overflow (RangeError). Counted at the same
|
|
238
|
+
// point in all three cores so the trip depth is byte-identical.
|
|
239
|
+
if (depth > MAX_RECURSION_DEPTH)
|
|
240
|
+
return false;
|
|
241
|
+
const h = head(t);
|
|
242
|
+
if (h === undefined)
|
|
243
|
+
return false; // not a horos descriptor
|
|
244
|
+
const { ctor, args } = h;
|
|
245
|
+
switch (ctor) {
|
|
246
|
+
// Top.
|
|
247
|
+
case "any":
|
|
248
|
+
return args.length === 0;
|
|
249
|
+
// Atoms.
|
|
250
|
+
case "atom":
|
|
251
|
+
return args.length === 0 && v instanceof Atom;
|
|
252
|
+
case "atom-exact": {
|
|
253
|
+
if (args.length !== 1)
|
|
254
|
+
return false;
|
|
255
|
+
const vb = atomBytes(v);
|
|
256
|
+
const bb = atomBytes(args[0]);
|
|
257
|
+
if (vb === undefined || bb === undefined)
|
|
258
|
+
return false;
|
|
259
|
+
return bytesEqual(vb, bb);
|
|
260
|
+
}
|
|
261
|
+
case "atom-len": {
|
|
262
|
+
if (args.length !== 2)
|
|
263
|
+
return false;
|
|
264
|
+
const vb = atomBytes(v);
|
|
265
|
+
if (vb === undefined)
|
|
266
|
+
return false;
|
|
267
|
+
// Bounds use readInt (materialize). Our readInt returns an unbounded `bigint`,
|
|
268
|
+
// whereas the Rust reference's read_int materializes into an `i128` and errors
|
|
269
|
+
// (→ false) for any bound past that width. We close that divergence (horos-internal#18) by
|
|
270
|
+
// applying the SAME i128 cap explicitly via atomLenBoundInRange: a bound outside
|
|
271
|
+
// [ATOM_LEN_BOUND_MIN, ATOM_LEN_BOUND_MAX] is rejected here, exactly as Rust's
|
|
272
|
+
// read_int returns IntOutOfRange. So the cores agree at the cap (a bound = 2^127 is
|
|
273
|
+
// non-conforming everywhere) and below it for every real byte-length bound.
|
|
274
|
+
let lo;
|
|
275
|
+
let hi;
|
|
276
|
+
try {
|
|
277
|
+
lo = readInt(args[0]);
|
|
278
|
+
hi = readInt(args[1]);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
if (!atomLenBoundInRange(lo) || !atomLenBoundInRange(hi))
|
|
284
|
+
return false;
|
|
285
|
+
const len = BigInt(vb.length);
|
|
286
|
+
return lo <= len && len <= hi;
|
|
287
|
+
}
|
|
288
|
+
case "atom-in": {
|
|
289
|
+
if (args.length !== 1)
|
|
290
|
+
return false;
|
|
291
|
+
const vb = atomBytes(v);
|
|
292
|
+
if (vb === undefined)
|
|
293
|
+
return false;
|
|
294
|
+
let elems;
|
|
295
|
+
try {
|
|
296
|
+
elems = readSet(args[0]);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
for (const e of elems) {
|
|
302
|
+
const eb = atomBytes(e);
|
|
303
|
+
if (eb !== undefined && bytesEqual(eb, vb))
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
// Text refinements (horos-internal#68) — each first recognizes v as a canonical utf8-text
|
|
309
|
+
// (delegating to the frozen ontos/data recognizer, re-minting nothing) and then
|
|
310
|
+
// applies a closed, total, single-value character predicate. Char-count (code
|
|
311
|
+
// points, not bytes) is measured, so multi-byte text is bounded/matched
|
|
312
|
+
// identically across the three cores.
|
|
313
|
+
case "text-len": {
|
|
314
|
+
if (args.length !== 2)
|
|
315
|
+
return false;
|
|
316
|
+
let s;
|
|
317
|
+
let lo;
|
|
318
|
+
let hi;
|
|
319
|
+
try {
|
|
320
|
+
s = readText(v);
|
|
321
|
+
lo = readInt(args[0]);
|
|
322
|
+
hi = readInt(args[1]);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (!atomLenBoundInRange(lo) || !atomLenBoundInRange(hi))
|
|
328
|
+
return false;
|
|
329
|
+
const n = BigInt([...s].length); // code points, not UTF-16 units
|
|
330
|
+
return lo <= n && n <= hi;
|
|
331
|
+
}
|
|
332
|
+
case "key-grammar": {
|
|
333
|
+
if (args.length !== 0)
|
|
334
|
+
return false;
|
|
335
|
+
let s;
|
|
336
|
+
try {
|
|
337
|
+
s = readText(v);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
return matchesKeyGrammar(s);
|
|
343
|
+
}
|
|
344
|
+
case "text-non-blank": {
|
|
345
|
+
if (args.length !== 0)
|
|
346
|
+
return false;
|
|
347
|
+
let s;
|
|
348
|
+
try {
|
|
349
|
+
s = readText(v);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
return isNonBlank(s);
|
|
355
|
+
}
|
|
356
|
+
case "lowercase-uuid": {
|
|
357
|
+
if (args.length !== 0)
|
|
358
|
+
return false;
|
|
359
|
+
let s;
|
|
360
|
+
try {
|
|
361
|
+
s = readText(v);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
return matchesLowercaseUuid(s);
|
|
367
|
+
}
|
|
368
|
+
// Scalars — delegate to the frozen ontos/data recognizers.
|
|
369
|
+
case "scalar": {
|
|
370
|
+
if (args.length !== 1)
|
|
371
|
+
return false;
|
|
372
|
+
const k = atomBytes(args[0]);
|
|
373
|
+
if (k === undefined)
|
|
374
|
+
return false;
|
|
375
|
+
const kind = bytesToAscii(k);
|
|
376
|
+
switch (kind) {
|
|
377
|
+
// Structural recognition — a canonical value beyond a binding's host integer
|
|
378
|
+
// width is still recognized (the materialization limit is not a recognition
|
|
379
|
+
// miss, ontos-data §5.1), so all cores agree. horos re-mints nothing.
|
|
380
|
+
case "int":
|
|
381
|
+
return ok(() => recognizeInt(v));
|
|
382
|
+
case "utf8-text":
|
|
383
|
+
return ok(() => readText(v));
|
|
384
|
+
case "bool":
|
|
385
|
+
return ok(() => readBool(v));
|
|
386
|
+
default:
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Tuples & compounds. Each child descriptor types a child VALUE — a descent, so
|
|
391
|
+
// the guard (pending) is cleared.
|
|
392
|
+
case "tuple": {
|
|
393
|
+
if (!(v instanceof Tuple))
|
|
394
|
+
return false;
|
|
395
|
+
const items = v.items();
|
|
396
|
+
if (items.length !== args.length)
|
|
397
|
+
return false;
|
|
398
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
399
|
+
if (!conformsIn(items[i], args[i], env, undefined, depth + 1))
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
case "seq": {
|
|
405
|
+
if (args.length !== 1)
|
|
406
|
+
return false;
|
|
407
|
+
if (!(v instanceof Tuple))
|
|
408
|
+
return false;
|
|
409
|
+
for (const c of v.items()) {
|
|
410
|
+
if (!conformsIn(c, args[0], env, undefined, depth + 1))
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
case "compound": {
|
|
416
|
+
if (args.length < 1)
|
|
417
|
+
return false;
|
|
418
|
+
const label = args[0];
|
|
419
|
+
const ts = args.slice(1);
|
|
420
|
+
if (!(v instanceof Tuple))
|
|
421
|
+
return false;
|
|
422
|
+
const lb = atomBytes(label);
|
|
423
|
+
if (lb === undefined)
|
|
424
|
+
return false;
|
|
425
|
+
const items = v.items();
|
|
426
|
+
if (items.length !== ts.length + 1)
|
|
427
|
+
return false;
|
|
428
|
+
const head0 = atomBytes(items[0]);
|
|
429
|
+
if (head0 === undefined || !bytesEqual(head0, lb))
|
|
430
|
+
return false;
|
|
431
|
+
for (let i = 0; i < ts.length; i += 1) {
|
|
432
|
+
if (!conformsIn(items[i + 1], ts[i], env, undefined, depth + 1))
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
// Structural collections — well-formedness delegates to ontos/data; the element
|
|
438
|
+
// descriptors add the per-element checks (descents).
|
|
439
|
+
case "list-of": {
|
|
440
|
+
if (args.length !== 1)
|
|
441
|
+
return false;
|
|
442
|
+
let elems;
|
|
443
|
+
try {
|
|
444
|
+
elems = readList(v);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
for (const e of elems) {
|
|
450
|
+
if (!conformsIn(e, args[0], env, undefined, depth + 1))
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
case "set-of": {
|
|
456
|
+
if (args.length !== 1)
|
|
457
|
+
return false;
|
|
458
|
+
let elems;
|
|
459
|
+
try {
|
|
460
|
+
elems = readSet(v);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
for (const e of elems) {
|
|
466
|
+
if (!conformsIn(e, args[0], env, undefined, depth + 1))
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
case "map-of": {
|
|
472
|
+
if (args.length !== 2)
|
|
473
|
+
return false;
|
|
474
|
+
let entries;
|
|
475
|
+
try {
|
|
476
|
+
entries = readMap(v);
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
for (const [k, val] of entries) {
|
|
482
|
+
if (!conformsIn(k, args[0], env, undefined, depth + 1) ||
|
|
483
|
+
!conformsIn(val, args[1], env, undefined, depth + 1)) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
// Records — a well-formed map keyed by utf8-text, with required/optional fields
|
|
490
|
+
// and a mandatory open/closed flag.
|
|
491
|
+
case "record": {
|
|
492
|
+
if (args.length < 1)
|
|
493
|
+
return false;
|
|
494
|
+
return conformsRecord(v, args[0], args.slice(1), env, depth);
|
|
495
|
+
}
|
|
496
|
+
// Composition — recurse on the SAME v, so the guard threads through unchanged.
|
|
497
|
+
case "union": {
|
|
498
|
+
for (const ct of args) {
|
|
499
|
+
if (conformsIn(v, ct, env, pending, depth + 1))
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
case "and": {
|
|
505
|
+
for (const ct of args) {
|
|
506
|
+
if (!conformsIn(v, ct, env, pending, depth + 1))
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
case "not": {
|
|
512
|
+
if (args.length !== 1)
|
|
513
|
+
return false;
|
|
514
|
+
return !conformsIn(v, args[0], env, pending, depth + 1);
|
|
515
|
+
}
|
|
516
|
+
// Singleton — membership by ontos structural identity (defines no new equality).
|
|
517
|
+
case "exact": {
|
|
518
|
+
if (args.length !== 1)
|
|
519
|
+
return false;
|
|
520
|
+
return exactEqual(v, args[0]);
|
|
521
|
+
}
|
|
522
|
+
// Recursion. rec binds the name; ref unfolds it, but only when guarded.
|
|
523
|
+
case "rec": {
|
|
524
|
+
if (args.length !== 2)
|
|
525
|
+
return false;
|
|
526
|
+
const nm = args[0];
|
|
527
|
+
if (!(nm instanceof Atom))
|
|
528
|
+
return false;
|
|
529
|
+
const nb = nm.bytes();
|
|
530
|
+
const body = args[1];
|
|
531
|
+
// Push one shared node onto each chain (no per-frame spread copy); the child
|
|
532
|
+
// frame sees this binding innermost-first.
|
|
533
|
+
const e = cons({ name: nb, body }, env);
|
|
534
|
+
const p = cons(nb, pending);
|
|
535
|
+
return conformsIn(v, body, e, p, depth + 1);
|
|
536
|
+
}
|
|
537
|
+
case "ref": {
|
|
538
|
+
if (args.length !== 1)
|
|
539
|
+
return false;
|
|
540
|
+
const nm = args[0];
|
|
541
|
+
if (!(nm instanceof Atom))
|
|
542
|
+
return false;
|
|
543
|
+
const nb = nm.bytes();
|
|
544
|
+
if (consContainsName(pending, nb))
|
|
545
|
+
return false; // unguarded at this node — would not terminate
|
|
546
|
+
// Innermost-first scan reproduces env.iter().rev(): an inner rec shadows an
|
|
547
|
+
// outer binding of the same name.
|
|
548
|
+
const body = lookupLast(env, nb);
|
|
549
|
+
if (body === undefined)
|
|
550
|
+
return false; // unbound reference
|
|
551
|
+
const p = cons(nb, pending);
|
|
552
|
+
return conformsIn(v, body, env, p, depth + 1);
|
|
553
|
+
}
|
|
554
|
+
// Unknown ctor or wrong arity → not a recognized descriptor.
|
|
555
|
+
default:
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* A field descriptor: `Tuple(Atom("horos/field"), utf8-text(name), Atom(req|opt), T)`.
|
|
561
|
+
* Returns `{ name, required, ty }` if `f` is a well-formed field, else `undefined`.
|
|
562
|
+
*/
|
|
563
|
+
function readField(f) {
|
|
564
|
+
const h = head(f);
|
|
565
|
+
if (h === undefined || h.ctor !== "field")
|
|
566
|
+
return undefined;
|
|
567
|
+
const { args } = h;
|
|
568
|
+
if (args.length !== 3)
|
|
569
|
+
return undefined;
|
|
570
|
+
let name;
|
|
571
|
+
try {
|
|
572
|
+
name = readText(args[0]);
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
return undefined;
|
|
576
|
+
}
|
|
577
|
+
const presence = atomBytes(args[1]);
|
|
578
|
+
if (presence === undefined)
|
|
579
|
+
return undefined;
|
|
580
|
+
const p = bytesToAscii(presence);
|
|
581
|
+
let required;
|
|
582
|
+
if (p === "req")
|
|
583
|
+
required = true;
|
|
584
|
+
else if (p === "opt")
|
|
585
|
+
required = false;
|
|
586
|
+
else
|
|
587
|
+
return undefined;
|
|
588
|
+
return { name, required, ty: args[2] };
|
|
589
|
+
}
|
|
590
|
+
function conformsRecord(v, mode, fields, env, depth) {
|
|
591
|
+
// mode is a bare atom flag (open | closed).
|
|
592
|
+
const mb = atomBytes(mode);
|
|
593
|
+
if (mb === undefined)
|
|
594
|
+
return false;
|
|
595
|
+
let open;
|
|
596
|
+
const m = bytesToAscii(mb);
|
|
597
|
+
if (m === "open")
|
|
598
|
+
open = true;
|
|
599
|
+
else if (m === "closed")
|
|
600
|
+
open = false;
|
|
601
|
+
else
|
|
602
|
+
return false;
|
|
603
|
+
// v must be a well-formed ontos/data map.
|
|
604
|
+
let entries;
|
|
605
|
+
try {
|
|
606
|
+
entries = readMap(v);
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
// Index the map's utf8-text keys to their values in a single pass — a map-backed
|
|
612
|
+
// lookup replacing the old per-field linear `present.find`. `readMap` already
|
|
613
|
+
// guarantees keys are sorted and dup-free by codec bytes; distinct keys therefore
|
|
614
|
+
// decode to distinct text strings, so no `set` ever collides and the map preserves
|
|
615
|
+
// the Rust core's "first match in map order" exactly. A non-text key makes v not a
|
|
616
|
+
// record-shaped value (same early miss as the linear scan).
|
|
617
|
+
const present = new Map();
|
|
618
|
+
for (const [k, val] of entries) {
|
|
619
|
+
let key;
|
|
620
|
+
try {
|
|
621
|
+
key = readText(k);
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
present.set(key, val);
|
|
627
|
+
}
|
|
628
|
+
// Single pass over the declared fields drives BOTH checks (no second readField
|
|
629
|
+
// sweep): per field, look the name up in `present` (O(1)) for the required/optional
|
|
630
|
+
// verdict, and — in closed mode — count how many present keys a declared field
|
|
631
|
+
// covers. Field names are pairwise-distinct (`wf(record)`, checked at entry), so a
|
|
632
|
+
// present declared key is counted at most once.
|
|
633
|
+
let covered = 0;
|
|
634
|
+
for (const f of fields) {
|
|
635
|
+
const fld = readField(f);
|
|
636
|
+
if (fld === undefined)
|
|
637
|
+
return false; // malformed field descriptor
|
|
638
|
+
const val = present.get(fld.name);
|
|
639
|
+
if (val !== undefined) {
|
|
640
|
+
covered += 1;
|
|
641
|
+
if (!conformsIn(val, fld.ty, env, undefined, depth + 1))
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
else if (fld.required) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// Closed: no present key outside the declared fields. With distinct field names,
|
|
649
|
+
// `covered` is exactly the number of present keys that match some declared field; a
|
|
650
|
+
// present key outside the declared set exists iff `covered < present.size`. This
|
|
651
|
+
// replaces the Rust core's second readField pass + O(fields×keys) sweep.
|
|
652
|
+
if (!open && covered !== present.size)
|
|
653
|
+
return false;
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
// ===========================================================================
|
|
657
|
+
// ===========================================================================
|
|
658
|
+
/**
|
|
659
|
+
* Whether `ctor` is a LEAF descriptor (a §4 leaf / §5 refinement / `exact`) — i.e. NOT a
|
|
660
|
+
*/
|
|
661
|
+
export function isLeafCtor(ctor) {
|
|
662
|
+
switch (ctor) {
|
|
663
|
+
case "any":
|
|
664
|
+
case "atom":
|
|
665
|
+
case "atom-exact":
|
|
666
|
+
case "atom-len":
|
|
667
|
+
case "atom-in":
|
|
668
|
+
case "text-len":
|
|
669
|
+
case "key-grammar":
|
|
670
|
+
case "text-non-blank":
|
|
671
|
+
case "lowercase-uuid":
|
|
672
|
+
case "scalar":
|
|
673
|
+
case "exact":
|
|
674
|
+
return true;
|
|
675
|
+
default:
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Decide a LEAF descriptor `t` against `v` (the `horos/leaf(T, v)` builtin). `false` for any
|
|
681
|
+
* non-leaf ctor. Reuses the recognizer's leaf arms via `conformsIn` at depth 0 with empty
|
|
682
|
+
* scopes — a leaf arm never recurses into a value position, so the scopes are inert.
|
|
683
|
+
*/
|
|
684
|
+
export function leafClassifies(v, t) {
|
|
685
|
+
const h = head(t);
|
|
686
|
+
if (h === undefined || !isLeafCtor(h.ctor))
|
|
687
|
+
return false;
|
|
688
|
+
return conformsIn(v, t, undefined, undefined, 0);
|
|
689
|
+
}
|
|
690
|
+
/** Decide whether a LEAF descriptor `t` is well-formed (the `horos/wf-leaf(T)` builtin). */
|
|
691
|
+
export function leafWellFormed(t) {
|
|
692
|
+
const h = head(t);
|
|
693
|
+
if (h === undefined || !isLeafCtor(h.ctor))
|
|
694
|
+
return false;
|
|
695
|
+
return wfIn(t, undefined, undefined, 0);
|
|
696
|
+
}
|
|
697
|
+
/** Read a field descriptor for diagnostics. */
|
|
698
|
+
export function readFieldDescriptor(f) {
|
|
699
|
+
return readField(f);
|
|
700
|
+
}
|
|
701
|
+
// ===========================================================================
|
|
702
|
+
// well-formedness — wf(T)
|
|
703
|
+
// ===========================================================================
|
|
704
|
+
/**
|
|
705
|
+
* Decide `wf(T)` — "is `T` a well-formed horos descriptor?" Total and search-free;
|
|
706
|
+
* recurses into sub-descriptors (which are finite). A descriptor that fails `wf` is a
|
|
707
|
+
* valid ontos value, simply not a recognized descriptor; `conforms(_, T)` over such a
|
|
708
|
+
* `T` is `false`.
|
|
709
|
+
*
|
|
710
|
+
* For rec/ref: a `ref` must be BOUND by an enclosing `rec` and GUARDED — reachable
|
|
711
|
+
* only after a descent into the value. An unbound or unguarded `ref` (e.g.
|
|
712
|
+
* `rec(x, ref(x))` or `rec(x, union(ref(x), …))`) is not well-formed.
|
|
713
|
+
*/
|
|
714
|
+
export function wf(t) {
|
|
715
|
+
return wfIn(t, undefined, undefined, 0);
|
|
716
|
+
}
|
|
717
|
+
function wfIn(t, bound, pending, depth) {
|
|
718
|
+
// Recursion-depth guard (horos-internal#37): mirrors conformsIn — past MAX_RECURSION_DEPTH frames a
|
|
719
|
+
// descriptor is treated as non-wf, deterministically and at the same depth in all three
|
|
720
|
+
// cores, so wf can never overflow the stack (RangeError) on a deep-but-finite descriptor.
|
|
721
|
+
if (depth > MAX_RECURSION_DEPTH)
|
|
722
|
+
return false;
|
|
723
|
+
const h = head(t);
|
|
724
|
+
if (h === undefined)
|
|
725
|
+
return false;
|
|
726
|
+
const { ctor, args } = h;
|
|
727
|
+
switch (ctor) {
|
|
728
|
+
case "any":
|
|
729
|
+
case "atom":
|
|
730
|
+
return args.length === 0;
|
|
731
|
+
case "atom-exact":
|
|
732
|
+
return args.length === 1 && args[0] instanceof Atom;
|
|
733
|
+
case "atom-len": {
|
|
734
|
+
if (args.length !== 2)
|
|
735
|
+
return false;
|
|
736
|
+
let lo;
|
|
737
|
+
let hi;
|
|
738
|
+
try {
|
|
739
|
+
lo = readInt(args[0]);
|
|
740
|
+
hi = readInt(args[1]);
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
// horos-internal#18 cap: a bound past i128 is non-wf, mirroring Rust's read_int (which returns
|
|
746
|
+
// IntOutOfRange). Then 0 <= lo <= hi.
|
|
747
|
+
if (!atomLenBoundInRange(lo) || !atomLenBoundInRange(hi))
|
|
748
|
+
return false;
|
|
749
|
+
return lo >= 0n && lo <= hi;
|
|
750
|
+
}
|
|
751
|
+
case "atom-in": {
|
|
752
|
+
if (args.length !== 1)
|
|
753
|
+
return false;
|
|
754
|
+
let elems;
|
|
755
|
+
try {
|
|
756
|
+
elems = readSet(args[0]);
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
// Every element must be an atom (an atom-membership set).
|
|
762
|
+
return elems.every((e) => e instanceof Atom);
|
|
763
|
+
}
|
|
764
|
+
// Text refinements (horos-internal#68). text-len carries char-count bounds with the same
|
|
765
|
+
// 0 <= lo <= hi + i128-cap discipline as atom-len (horos-internal#18); the three grammar-class
|
|
766
|
+
// refinements are nullary named predicates.
|
|
767
|
+
case "text-len": {
|
|
768
|
+
if (args.length !== 2)
|
|
769
|
+
return false;
|
|
770
|
+
let lo;
|
|
771
|
+
let hi;
|
|
772
|
+
try {
|
|
773
|
+
lo = readInt(args[0]);
|
|
774
|
+
hi = readInt(args[1]);
|
|
775
|
+
}
|
|
776
|
+
catch {
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
if (!atomLenBoundInRange(lo) || !atomLenBoundInRange(hi))
|
|
780
|
+
return false;
|
|
781
|
+
return lo >= 0n && lo <= hi;
|
|
782
|
+
}
|
|
783
|
+
case "key-grammar":
|
|
784
|
+
case "text-non-blank":
|
|
785
|
+
case "lowercase-uuid":
|
|
786
|
+
return args.length === 0;
|
|
787
|
+
case "scalar": {
|
|
788
|
+
if (args.length !== 1)
|
|
789
|
+
return false;
|
|
790
|
+
const k = atomBytes(args[0]);
|
|
791
|
+
if (k === undefined)
|
|
792
|
+
return false;
|
|
793
|
+
const kind = bytesToAscii(k);
|
|
794
|
+
return kind === "int" || kind === "utf8-text" || kind === "bool";
|
|
795
|
+
}
|
|
796
|
+
// Descent combinators clear pending (a value constructor is consumed).
|
|
797
|
+
case "tuple":
|
|
798
|
+
return args.every((ct) => wfIn(ct, bound, undefined, depth + 1));
|
|
799
|
+
case "seq":
|
|
800
|
+
return args.length === 1 && wfIn(args[0], bound, undefined, depth + 1);
|
|
801
|
+
case "compound": {
|
|
802
|
+
if (args.length < 1)
|
|
803
|
+
return false;
|
|
804
|
+
if (!(args[0] instanceof Atom))
|
|
805
|
+
return false;
|
|
806
|
+
return args.slice(1).every((ct) => wfIn(ct, bound, undefined, depth + 1));
|
|
807
|
+
}
|
|
808
|
+
case "list-of":
|
|
809
|
+
case "set-of":
|
|
810
|
+
return args.length === 1 && wfIn(args[0], bound, undefined, depth + 1);
|
|
811
|
+
case "map-of":
|
|
812
|
+
return (args.length === 2 &&
|
|
813
|
+
wfIn(args[0], bound, undefined, depth + 1) &&
|
|
814
|
+
wfIn(args[1], bound, undefined, depth + 1));
|
|
815
|
+
case "record": {
|
|
816
|
+
if (args.length < 1)
|
|
817
|
+
return false;
|
|
818
|
+
return wfRecord(args[0], args.slice(1), bound, depth);
|
|
819
|
+
}
|
|
820
|
+
// Same-v combinators thread the guard through.
|
|
821
|
+
case "union":
|
|
822
|
+
case "and":
|
|
823
|
+
return args.every((ct) => wfIn(ct, bound, pending, depth + 1));
|
|
824
|
+
case "not":
|
|
825
|
+
return args.length === 1 && wfIn(args[0], bound, pending, depth + 1);
|
|
826
|
+
case "exact":
|
|
827
|
+
// Any single ontos value is a valid singleton.
|
|
828
|
+
return args.length === 1;
|
|
829
|
+
case "rec": {
|
|
830
|
+
if (args.length !== 2)
|
|
831
|
+
return false;
|
|
832
|
+
const nm = args[0];
|
|
833
|
+
if (!(nm instanceof Atom))
|
|
834
|
+
return false;
|
|
835
|
+
const nb = nm.bytes();
|
|
836
|
+
// Shared push onto each chain (no per-frame spread copy).
|
|
837
|
+
const b = cons(nb, bound);
|
|
838
|
+
const p = cons(nb, pending);
|
|
839
|
+
return wfIn(args[1], b, p, depth + 1);
|
|
840
|
+
}
|
|
841
|
+
case "ref": {
|
|
842
|
+
if (args.length !== 1)
|
|
843
|
+
return false;
|
|
844
|
+
const nm = args[0];
|
|
845
|
+
if (!(nm instanceof Atom))
|
|
846
|
+
return false;
|
|
847
|
+
const nb = nm.bytes();
|
|
848
|
+
return consContainsName(bound, nb) && !consContainsName(pending, nb);
|
|
849
|
+
}
|
|
850
|
+
default:
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
function wfRecord(mode, fields, bound, depth) {
|
|
855
|
+
const mb = atomBytes(mode);
|
|
856
|
+
if (mb === undefined)
|
|
857
|
+
return false;
|
|
858
|
+
const m = bytesToAscii(mb);
|
|
859
|
+
if (m !== "open" && m !== "closed")
|
|
860
|
+
return false;
|
|
861
|
+
const names = [];
|
|
862
|
+
for (const f of fields) {
|
|
863
|
+
const fld = readField(f);
|
|
864
|
+
if (fld === undefined)
|
|
865
|
+
return false;
|
|
866
|
+
if (!wfIn(fld.ty, bound, undefined, depth + 1))
|
|
867
|
+
return false;
|
|
868
|
+
if (names.includes(fld.name))
|
|
869
|
+
return false; // duplicate field name
|
|
870
|
+
names.push(fld.name);
|
|
871
|
+
}
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
// ===========================================================================
|
|
875
|
+
// internal helpers
|
|
876
|
+
// ===========================================================================
|
|
877
|
+
// Literals under `exact` are data, not descriptor recursion. Compare their finite
|
|
878
|
+
// trees without consuming the JavaScript call stack or changing structural identity.
|
|
879
|
+
function exactEqual(left, right) {
|
|
880
|
+
const work = [[left, right]];
|
|
881
|
+
while (work.length > 0) {
|
|
882
|
+
const [a, b] = work.pop();
|
|
883
|
+
if (a.kind !== b.kind)
|
|
884
|
+
return false;
|
|
885
|
+
if (a.kind === "atom") {
|
|
886
|
+
if (b.kind !== "atom" || !bytesEqual(a.bytes(), b.bytes()))
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
if (b.kind !== "tuple")
|
|
891
|
+
return false;
|
|
892
|
+
const aa = a.items(), bb = b.items();
|
|
893
|
+
if (aa.length !== bb.length)
|
|
894
|
+
return false;
|
|
895
|
+
for (let i = 0; i < aa.length; i++)
|
|
896
|
+
work.push([aa[i], bb[i]]);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Find the body bound to `name`, searching from the innermost (head) binding outward —
|
|
903
|
+
* the Rust core does `scope_iter(env).find_map(...)` (i.e. `env.iter().rev()`), so an
|
|
904
|
+
* inner `rec` shadows an outer binding of the same name. Returns `undefined` if unbound.
|
|
905
|
+
*/
|
|
906
|
+
function lookupLast(env, name) {
|
|
907
|
+
for (let n = env; n !== undefined; n = n.tail) {
|
|
908
|
+
if (bytesEqual(n.head.name, name))
|
|
909
|
+
return n.head.body;
|
|
910
|
+
}
|
|
911
|
+
return undefined;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Decode bytes as a Latin-1/ASCII string for matching against the small set of fixed
|
|
915
|
+
* ASCII ctor/kind/flag literals. The Rust core matches raw byte slices (e.g. `b"int"`);
|
|
916
|
+
* decoding each byte to its code point reproduces that comparison exactly for ASCII and
|
|
917
|
+
* round-trips any byte 1:1 (no UTF-8 multi-byte folding), so a non-ASCII kind atom never
|
|
918
|
+
* spuriously matches a literal.
|
|
919
|
+
*/
|
|
920
|
+
function bytesToAscii(bytes) {
|
|
921
|
+
let s = "";
|
|
922
|
+
for (const b of bytes)
|
|
923
|
+
s += String.fromCharCode(b);
|
|
924
|
+
return s;
|
|
925
|
+
}
|
|
926
|
+
/** Run `fn` and report whether it returned without throwing (the recognizer succeeded). */
|
|
927
|
+
function ok(fn) {
|
|
928
|
+
try {
|
|
929
|
+
fn();
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
//# sourceMappingURL=core.js.map
|