@elaraai/east 1.0.76 → 1.0.78

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.
Files changed (70) hide show
  1. package/dist/src/fuzz.d.ts +103 -11
  2. package/dist/src/fuzz.d.ts.map +1 -1
  3. package/dist/src/fuzz.js +226 -16
  4. package/dist/src/fuzz.js.map +1 -1
  5. package/dist/src/patch/fuzz.d.ts +26 -0
  6. package/dist/src/patch/fuzz.d.ts.map +1 -1
  7. package/dist/src/patch/fuzz.js +42 -4
  8. package/dist/src/patch/fuzz.js.map +1 -1
  9. package/dist/src/patch/type_of_patch.d.ts +14 -1
  10. package/dist/src/patch/type_of_patch.d.ts.map +1 -1
  11. package/dist/src/patch/type_of_patch.js +8 -13
  12. package/dist/src/patch/type_of_patch.js.map +1 -1
  13. package/dist/src/patch/type_of_patch.spec.d.ts +6 -0
  14. package/dist/src/patch/type_of_patch.spec.d.ts.map +1 -0
  15. package/dist/src/patch/type_of_patch.spec.js +74 -0
  16. package/dist/src/patch/type_of_patch.spec.js.map +1 -0
  17. package/dist/src/serialization/beast2/index.d.ts +22 -1
  18. package/dist/src/serialization/beast2/index.d.ts.map +1 -1
  19. package/dist/src/serialization/beast2/index.js +56 -1
  20. package/dist/src/serialization/beast2/index.js.map +1 -1
  21. package/dist/src/serialization/beast2/index.spec.js +16 -0
  22. package/dist/src/serialization/beast2/index.spec.js.map +1 -1
  23. package/dist/src/serialization/beast2/v4/container.js +10 -15
  24. package/dist/src/serialization/beast2/v4/container.js.map +1 -1
  25. package/dist/src/serialization/beast2/v4/type-table.d.ts +63 -34
  26. package/dist/src/serialization/beast2/v4/type-table.d.ts.map +1 -1
  27. package/dist/src/serialization/beast2/v4/type-table.js +353 -403
  28. package/dist/src/serialization/beast2/v4/type-table.js.map +1 -1
  29. package/dist/src/serialization/beast2/v5/deflate.d.ts.map +1 -1
  30. package/dist/src/serialization/beast2/v5/deflate.js +147 -50
  31. package/dist/src/serialization/beast2/v5/deflate.js.map +1 -1
  32. package/dist/src/serialization/beast2/v5/deflate.spec.d.ts +6 -0
  33. package/dist/src/serialization/beast2/v5/deflate.spec.d.ts.map +1 -0
  34. package/dist/src/serialization/beast2/v5/deflate.spec.js +237 -0
  35. package/dist/src/serialization/beast2/v5/deflate.spec.js.map +1 -0
  36. package/dist/src/serialization/beast2/v5/frame-pool.d.ts +119 -0
  37. package/dist/src/serialization/beast2/v5/frame-pool.d.ts.map +1 -0
  38. package/dist/src/serialization/beast2/v5/frame-pool.js +235 -0
  39. package/dist/src/serialization/beast2/v5/frame-pool.js.map +1 -0
  40. package/dist/src/serialization/beast2/v5/frame-pool.spec.d.ts +6 -0
  41. package/dist/src/serialization/beast2/v5/frame-pool.spec.d.ts.map +1 -0
  42. package/dist/src/serialization/beast2/v5/frame-pool.spec.js +287 -0
  43. package/dist/src/serialization/beast2/v5/frame-pool.spec.js.map +1 -0
  44. package/dist/src/serialization/beast2/v5/frame-worker.d.ts +6 -0
  45. package/dist/src/serialization/beast2/v5/frame-worker.d.ts.map +1 -0
  46. package/dist/src/serialization/beast2/v5/frame-worker.js +40 -0
  47. package/dist/src/serialization/beast2/v5/frame-worker.js.map +1 -0
  48. package/dist/src/serialization/beast2/v5/index.spec.js +292 -5
  49. package/dist/src/serialization/beast2/v5/index.spec.js.map +1 -1
  50. package/dist/src/serialization/beast2/v5/lazy.d.ts +11 -8
  51. package/dist/src/serialization/beast2/v5/lazy.d.ts.map +1 -1
  52. package/dist/src/serialization/beast2/v5/lazy.js +49 -39
  53. package/dist/src/serialization/beast2/v5/lazy.js.map +1 -1
  54. package/dist/src/serialization/beast2/v5/lazy.spec.js +98 -3
  55. package/dist/src/serialization/beast2/v5/lazy.spec.js.map +1 -1
  56. package/dist/src/serialization/beast2/v5/stream.d.ts +88 -2
  57. package/dist/src/serialization/beast2/v5/stream.d.ts.map +1 -1
  58. package/dist/src/serialization/beast2/v5/stream.js +175 -22
  59. package/dist/src/serialization/beast2/v5/stream.js.map +1 -1
  60. package/dist/src/serialization/index.d.ts +1 -1
  61. package/dist/src/serialization/index.d.ts.map +1 -1
  62. package/dist/src/serialization/index.js +1 -1
  63. package/dist/src/serialization/index.js.map +1 -1
  64. package/dist/src/type_of_type.d.ts +0 -5
  65. package/dist/src/type_of_type.d.ts.map +1 -1
  66. package/dist/src/type_of_type.js +51 -49
  67. package/dist/src/type_of_type.js.map +1 -1
  68. package/dist/src/type_of_type.to_etv.spec.js +75 -0
  69. package/dist/src/type_of_type.to_etv.spec.js.map +1 -1
  70. package/package.json +1 -1
@@ -3,15 +3,41 @@
3
3
  * Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
4
4
  */
5
5
  /**
6
- * Beast2 v2 flat type table encoder and decoder.
6
+ * Beast2 flat type table — encoder and decoder.
7
7
  *
8
- * The type table is a flat array where each unique sub-type is defined once
9
- * and referenced by varint index. Tag bytes serve as direct array indices
10
- * into dispatch tables for zero-branch decoding.
8
+ * The type table is a flat array in which each distinct type is one entry,
9
+ * referenced by varint index; tag bytes double as indices into the decoder's
10
+ * dispatch table. The v4 container writes it as its first section and the v5
11
+ * container reuses it verbatim as the structural type section.
11
12
  *
12
- * See devdocs/BEAST2.md for the format specification.
13
+ * **The table is canonical** (#770): a pure function of the type, so the same
14
+ * type writes the same bytes in every runtime (east-c's `type_table.c` applies
15
+ * the same rules, and east-py encodes through it) and whether the type was
16
+ * built in code or read back off the wire. e3 content-addresses beast2 bytes,
17
+ * so anything less puts one value under several hashes. The rules, as
18
+ * {@link TypeTableBuilder} applies them:
19
+ *
20
+ * - Entries are committed in a post-order walk over the type in declaration
21
+ * order: Struct fields as declared, Variant cases as sorted, Function inputs
22
+ * then output, Dict key then value. A child therefore sits at a lower index
23
+ * than its parent, which the decoders rely on.
24
+ * - A Recursive wrapper takes its index before its body is walked, so the
25
+ * body's self-references name that index.
26
+ * - An entry whose bytes (tag + parameters) are already in the table is not
27
+ * written again. Two occurrences of one type — inside or outside a
28
+ * recursive wrapper's body, as an `EastType` or as an `EastTypeValue`, as
29
+ * one object or as many — are one entry.
30
+ * - Two Recursive wrappers are one entry when {@link isTypeValueEqual} says
31
+ * so. A wrapper's id is a runtime artefact (a type id in one process, a
32
+ * table index off the wire) and is not written, so wrappers compare up to
33
+ * their naming.
34
+ *
35
+ * Nothing here constructs an `EastType`. Type ids are serialized into IR, so
36
+ * an encode must not shift the ids of the types built after it.
37
+ *
38
+ * See v4/SPEC.md for the entry grammar and v5/SPEC.md for the type section.
13
39
  */
14
- import { toEastTypeValue } from "../../../type_of_type.js";
40
+ import { toEastTypeValue, isTypeValueEqual } from "../../../type_of_type.js";
15
41
  import { getTypeId } from "../../../types.js";
16
42
  import { isVariant, variant } from "../../../containers/variant.js";
17
43
  import { BufferWriter, BufferReader } from "../../binary-utils.js";
@@ -45,313 +71,240 @@ const TAG_FOR_TYPE = {
45
71
  Function: TAG_FUNCTION, AsyncFunction: TAG_ASYNC_FUNCTION, Recursive: TAG_RECURSIVE,
46
72
  };
47
73
  /**
48
- * Builds a flat type table from types. Handles both EastType (which has
49
- * recursive wrappers with .node) and EastTypeValue (which has depth-based
50
- * Recursive(N) — no inner pointer, so cannot be recursive root).
74
+ * Builds the canonical flat type table of one or more types (see the module
75
+ * comment for the rules). Accepts `EastType` (converted with
76
+ * {@link toEastTypeValue}, which allocates no type) and `EastTypeValue`.
51
77
  *
52
78
  * Usage:
53
79
  * const builder = new TypeTableBuilder();
54
80
  * const rootIdx = builder.add(myType); // EastType or EastTypeValue
55
- * builder.addETV(someIRTypeAnnotation); // EastTypeValue from IR
81
+ * builder.add(someIRTypeAnnotation); // a later type shares entries
56
82
  * writeTypeTableSection(rootIdx, builder.entries, writer);
57
83
  */
58
84
  export class TypeTableBuilder {
59
85
  entries = [];
60
- /** EastType pointer → table index */
61
- etMap = new Map();
62
- /** EastTypeValue object → table index (identity-based) */
63
- etvMap = new Map();
64
- /** type_id → table index (for cross-object lookup when identity fails) */
65
- tidMap = new Map();
66
- /** Recursive wrapper source ID (bigint) → table index */
67
- recIdMap = new Map();
68
- /** Get the type_id → index map (for IR type substitution). */
69
- getIndexMap() {
70
- return this.tidMap;
71
- }
72
- /** Create a shallow clone with the same entries and index maps. */
86
+ /** Entry bytes (tag + parameters) → index: the same bytes are the same wire node. */
87
+ byContent = new Map();
88
+ /** Type object (EastType or EastTypeValue) → index: a memo over the walk. */
89
+ byObject = new Map();
90
+ /** type_id → index, for the values `toEastTypeValue` stamps. */
91
+ byTid = new Map();
92
+ /** Recursive wrapper id → the wrapper's index, for every wrapper added. */
93
+ byWrapperId = new Map();
94
+ /** Committed wrappers, deduplicated structurally. */
95
+ wrappers = [];
96
+ /** Wrappers whose bodies are being walked, innermost last. */
97
+ open = [];
98
+ /** Create a copy with the same entries and lookups (the v4 encoder extends a
99
+ * cached root closure per value). */
73
100
  clone() {
74
101
  const c = new TypeTableBuilder();
75
102
  for (const e of this.entries)
76
103
  c.entries.push(e);
77
- for (const [k, v] of this.etMap)
78
- c.etMap.set(k, v);
79
- for (const [k, v] of this.etvMap)
80
- c.etvMap.set(k, v);
81
- for (const [k, v] of this.tidMap)
82
- c.tidMap.set(k, v);
83
- for (const [k, v] of this.recIdMap)
84
- c.recIdMap.set(k, v);
104
+ for (const [k, v] of this.byContent)
105
+ c.byContent.set(k, v);
106
+ for (const [k, v] of this.byObject)
107
+ c.byObject.set(k, v);
108
+ for (const [k, v] of this.byTid)
109
+ c.byTid.set(k, v);
110
+ for (const [k, v] of this.byWrapperId)
111
+ c.byWrapperId.set(k, v);
112
+ for (const w of this.wrappers)
113
+ c.wrappers.push(w);
114
+ for (const w of this.open)
115
+ c.open.push(w);
85
116
  return c;
86
117
  }
87
- /** Add a type. Accepts both EastType and EastTypeValue. */
118
+ /** Add a type and return its index; a type already in the table returns its
119
+ * existing index and writes nothing. Accepts both EastType and EastTypeValue. */
88
120
  add(type) {
89
- if (isVariant(type)) {
90
- return this.visitETV(type);
91
- }
92
- return this.visitET(type);
121
+ const memo = this.byObject.get(type);
122
+ if (memo !== undefined)
123
+ return memo;
124
+ const etv = isVariant(type) ? type : toEastTypeValue(type);
125
+ const idx = this.visit(etv);
126
+ this.byObject.set(type, idx);
127
+ return idx;
93
128
  }
94
129
  /** Add an EastTypeValue (for IR type annotations). */
95
130
  addETV(etv) {
96
- return this.visitETV(etv);
131
+ return this.add(etv);
97
132
  }
98
- /** Look up an EastTypeValue's table index by identity, type_id, or recursive ref ID. */
133
+ /** Look up a type's index without adding it: by object, by type_id, or — for
134
+ * a Recursive ref or wrapper — by the wrapper's id. */
99
135
  indexOf(etv) {
100
- const idx = this.etvMap.get(etv);
101
- if (idx !== undefined)
102
- return idx;
136
+ const idx = this.lookup(etv);
137
+ if (idx === undefined)
138
+ throw new Error(`Type not in table: ${etv.type}`);
139
+ return idx;
140
+ }
141
+ /** Whether {@link indexOf} would find the type. `add` is idempotent, so a
142
+ * miss here is never a reason not to add. */
143
+ has(etv) {
144
+ return this.lookup(etv) !== undefined;
145
+ }
146
+ lookup(etv) {
147
+ const memo = this.byObject.get(etv);
148
+ if (memo !== undefined)
149
+ return memo;
103
150
  const tid = getTypeId(etv);
104
151
  if (tid !== undefined) {
105
- const tidIdx = this.tidMap.get(tid);
106
- if (tidIdx !== undefined)
107
- return tidIdx;
152
+ const known = this.byTid.get(tid);
153
+ if (known !== undefined)
154
+ return known;
108
155
  }
109
- // Recursive ref: look up by the ref's bigint ID
110
- if (etv.type === "Recursive" && etv.value?.type === "ref") {
111
- const refId = etv.value.value;
112
- const recIdx = this.recIdMap.get(refId);
113
- if (recIdx !== undefined)
114
- return recIdx;
156
+ if (etv.type === "Recursive") {
157
+ const id = recursiveId(etv.value);
158
+ return this.openWrapper(id) ?? this.byWrapperId.get(id);
115
159
  }
116
- throw new Error(`Type not in table: ${etv.type}`);
160
+ return undefined;
117
161
  }
118
- /** Check if an EastTypeValue is in the table (by identity, type_id, or recursive ref ID). */
119
- has(etv) {
120
- if (this.etvMap.has(etv))
121
- return true;
162
+ // ── The walk ────────────────────────────────────────────────────────
163
+ visit(etv) {
164
+ const memo = this.byObject.get(etv);
165
+ if (memo !== undefined)
166
+ return memo;
122
167
  const tid = getTypeId(etv);
123
- if (tid !== undefined && this.tidMap.has(tid))
124
- return true;
125
- // Recursive ref: check by bigint ID
126
- if (etv.type === "Recursive" && etv.value?.type === "ref") {
127
- return this.recIdMap.has(etv.value.value);
128
- }
129
- return false;
130
- }
131
- // ── EastType walker (handles Recursive wrappers) ────────────────────
132
- visitET(type) {
133
- const existing = this.etMap.get(type);
134
- if (existing !== undefined)
135
- return existing;
136
- const t = type;
137
- if (t.type === "Recursive") {
138
- // Allocate index BEFORE recursing — self-references find this index
139
- const idx = this.allocate();
140
- this.etMap.set(type, idx);
141
- // Register type_id so ETV path can find this entry via tidMap
142
- const recTid = getTypeId(type);
143
- if (recTid !== undefined) {
144
- this.tidMap.set(recTid, idx);
145
- this.recIdMap.set(BigInt(recTid), idx);
168
+ if (tid !== undefined) {
169
+ const known = this.byTid.get(tid);
170
+ if (known !== undefined) {
171
+ this.byObject.set(etv, known);
172
+ return known;
146
173
  }
147
- const innerIdx = this.visitET(t.node);
148
- this.entries[idx] = { tag: TAG_RECURSIVE, params: varint(innerIdx) };
149
- return idx;
150
- }
151
- // Check EastTypeValue identity cache for dedup
152
- const etv = toEastTypeValue(type);
153
- const etvIdx = this.etvMap.get(etv);
154
- if (etvIdx !== undefined) {
155
- this.etMap.set(type, etvIdx);
156
- return etvIdx;
157
- }
158
- // Primitives
159
- if (t.type === "Never" || t.type === "Null" || t.type === "Boolean" ||
160
- t.type === "Integer" || t.type === "Float" || t.type === "String" ||
161
- t.type === "DateTime" || t.type === "Blob") {
162
- return this.emitPrimitive(type, etv, t.type);
163
- }
164
- // Single-parameter containers
165
- if (t.type === "Array" || t.type === "Ref") {
166
- const elemIdx = this.visitET(t.value);
167
- return this.emit(type, etv, TAG_FOR_TYPE[t.type], varint(elemIdx));
168
174
  }
169
- if (t.type === "Set") {
170
- const elemIdx = this.visitET(t.key);
171
- return this.emit(type, etv, TAG_SET, varint(elemIdx));
172
- }
173
- if (t.type === "Vector" || t.type === "Matrix") {
174
- const elemIdx = this.visitET(t.element);
175
- return this.emit(type, etv, TAG_FOR_TYPE[t.type], varint(elemIdx));
176
- }
177
- if (t.type === "Dict") {
178
- const ki = this.visitET(t.key);
179
- const vi = this.visitET(t.value);
180
- return this.emit(type, etv, TAG_DICT, varints(ki, vi));
181
- }
182
- if (t.type === "Struct") {
183
- return this.emitNamedFields(type, etv, TAG_STRUCT, Object.keys(t.fields), (name) => this.visitET(t.fields[name]));
184
- }
185
- if (t.type === "Variant") {
186
- return this.emitNamedFields(type, etv, TAG_VARIANT, Object.keys(t.cases), (name) => this.visitET(t.cases[name]));
187
- }
188
- if (t.type === "Function" || t.type === "AsyncFunction") {
189
- return this.emitFunc(type, etv, t.type, t.inputs.map((i) => this.visitET(i)), this.visitET(t.output));
190
- }
191
- throw new Error(`Unknown EastType: ${t.type}`);
175
+ const idx = this.visitNode(etv);
176
+ this.byObject.set(etv, idx);
177
+ if (tid !== undefined)
178
+ this.byTid.set(tid, idx);
179
+ return idx;
192
180
  }
193
- // ── EastTypeValue walker (for lazily-discovered types) ──────────────
194
- visitETV(etv) {
195
- const existing = this.etvMap.get(etv);
196
- if (existing !== undefined)
197
- return existing;
198
- // Check type_id for cross-path dedup (ET path may have already added this type)
199
- const etvTid = getTypeId(etv);
200
- if (etvTid !== undefined) {
201
- const tidIdx = this.tidMap.get(etvTid);
202
- if (tidIdx !== undefined)
203
- return tidIdx;
204
- }
205
- const caseName = etv.type;
206
- if (caseName === "Recursive") {
207
- const inner = etv.value;
208
- if (inner.type === "wrapper") {
209
- // Recursive wrapper: allocate first, recurse into inner, fill.
210
- const idx = this.allocate();
211
- this.entries[idx] = { tag: TAG_RECURSIVE, params: EMPTY };
212
- this.registerETV(etv, idx);
213
- // Register wrapper's bigint ID so refs can find it
214
- const wrapperId = inner.value.id;
215
- this.recIdMap.set(wrapperId, idx);
216
- const innerIdx = this.visitETV(inner.value.inner);
217
- this.entries[idx] = { tag: TAG_RECURSIVE, params: varint(innerIdx) };
218
- return idx;
181
+ /** Commit the entry of a node after its children (post-order). */
182
+ visitNode(etv) {
183
+ switch (etv.type) {
184
+ case "Never":
185
+ case "Null":
186
+ case "Boolean":
187
+ case "Integer":
188
+ case "Float":
189
+ case "String":
190
+ case "DateTime":
191
+ case "Blob":
192
+ return this.commit(TAG_FOR_TYPE[etv.type], EMPTY);
193
+ case "Array":
194
+ case "Set":
195
+ case "Ref":
196
+ case "Vector":
197
+ case "Matrix":
198
+ return this.commit(TAG_FOR_TYPE[etv.type], varint(this.visit(etv.value)));
199
+ case "Dict": {
200
+ const ki = this.visit(etv.value.key);
201
+ const vi = this.visit(etv.value.value);
202
+ return this.commit(TAG_DICT, varints(ki, vi));
219
203
  }
220
- // Self-reference Recursive(ref) — find existing wrapper by ID or type_id.
221
- const refId = etv.value.value;
222
- const recIdx = this.recIdMap.get(refId);
223
- if (recIdx !== undefined)
224
- return recIdx;
225
- const tid = getTypeId(etv);
226
- if (tid !== undefined) {
227
- const tidIdx = this.tidMap.get(tid);
228
- if (tidIdx !== undefined)
229
- return tidIdx;
204
+ case "Struct":
205
+ case "Variant": {
206
+ const members = etv.value;
207
+ const indices = members.map(m => this.visit(m.type));
208
+ const w = new BufferWriter();
209
+ w.writeVarint(members.length);
210
+ for (let i = 0; i < members.length; i++) {
211
+ w.writeStringUtf8Varint(members[i].name);
212
+ w.writeVarint(indices[i]);
213
+ }
214
+ return this.commit(TAG_FOR_TYPE[etv.type], w.toUint8Array());
230
215
  }
231
- // Scan for wrapper by tag
232
- for (let i = 0; i < this.entries.length; i++) {
233
- if (this.entries[i]?.tag === TAG_RECURSIVE)
234
- return i;
216
+ case "Function":
217
+ case "AsyncFunction": {
218
+ const inputs = etv.value.inputs.map(i => this.visit(i));
219
+ const output = this.visit(etv.value.output);
220
+ const w = new BufferWriter();
221
+ w.writeVarint(inputs.length);
222
+ for (const ii of inputs)
223
+ w.writeVarint(ii);
224
+ w.writeVarint(output);
225
+ return this.commit(TAG_FOR_TYPE[etv.type], w.toUint8Array());
235
226
  }
236
- throw new Error("Recursive self-reference but no wrapper in table");
237
- }
238
- // Primitives
239
- if (caseName === "Never" || caseName === "Null" || caseName === "Boolean" ||
240
- caseName === "Integer" || caseName === "Float" || caseName === "String" ||
241
- caseName === "DateTime" || caseName === "Blob") {
242
- const idx = this.allocate();
243
- this.registerETV(etv, idx);
244
- this.entries[idx] = { tag: TAG_FOR_TYPE[caseName], params: EMPTY };
245
- return idx;
246
- }
247
- // Single-parameter containers
248
- if (caseName === "Array" || caseName === "Set" || caseName === "Ref" ||
249
- caseName === "Vector" || caseName === "Matrix") {
250
- const elemIdx = this.visitETV(etv.value);
251
- const idx = this.allocate();
252
- this.registerETV(etv, idx);
253
- this.entries[idx] = { tag: TAG_FOR_TYPE[caseName], params: varint(elemIdx) };
254
- return idx;
255
- }
256
- if (caseName === "Dict") {
257
- const ki = this.visitETV(etv.value.key);
258
- const vi = this.visitETV(etv.value.value);
259
- const idx = this.allocate();
260
- this.registerETV(etv, idx);
261
- this.entries[idx] = { tag: TAG_DICT, params: varints(ki, vi) };
262
- return idx;
263
- }
264
- if (caseName === "Struct") {
265
- const fields = etv.value;
266
- const indices = fields.map(f => this.visitETV(f.type));
267
- const idx = this.emitNamedFieldsETV(etv, TAG_STRUCT, fields.map(f => f.name), indices);
268
- return idx;
269
- }
270
- if (caseName === "Variant") {
271
- const cases = etv.value;
272
- const indices = cases.map(c => this.visitETV(c.type));
273
- const idx = this.emitNamedFieldsETV(etv, TAG_VARIANT, cases.map(c => c.name), indices);
274
- return idx;
275
- }
276
- if (caseName === "Function" || caseName === "AsyncFunction") {
277
- const inputs = etv.value.inputs;
278
- const inputIndices = inputs.map(i => this.visitETV(i));
279
- const outputIdx = this.visitETV(etv.value.output);
280
- const idx = this.emitFuncETV(etv, caseName, inputIndices, outputIdx);
281
- return idx;
227
+ case "Recursive":
228
+ return this.visitRecursive(etv.value, etv);
229
+ default:
230
+ throw new Error(`Unknown EastTypeValue case: ${etv.type}`);
282
231
  }
283
- throw new Error(`Unknown EastTypeValue case: ${caseName}`);
284
232
  }
285
- // ── Helpers ─────────────────────────────────────────────────────────
286
- allocate() {
233
+ visitRecursive(payload, etv) {
234
+ const id = recursiveId(payload);
235
+ // A self-reference — a ref, or the wrapper spelled out again inside its
236
+ // own body — names the wrapper being walked.
237
+ const open = this.openWrapper(id);
238
+ if (open !== undefined)
239
+ return open;
240
+ if (payload.type === "ref") {
241
+ // A ref outside its wrapper's body stands for the whole recursive type.
242
+ const committed = this.byWrapperId.get(id);
243
+ if (committed !== undefined)
244
+ return committed;
245
+ throw new Error(`beast2 type table: Recursive ref(${id}) is bound by no wrapper`);
246
+ }
247
+ // The wrapper's entry exists only once its body does, so a repeat is found
248
+ // by comparing types, not bytes — up to the naming of wrappers.
249
+ for (const w of this.wrappers) {
250
+ if (isTypeValueEqual(w.etv, etv)) {
251
+ // Bind THIS id to the shared entry too. One recursive type reaches a
252
+ // table under different wrapper ids — an interned type id in this
253
+ // process, a table index off the wire — and the id is a runtime
254
+ // artefact that is never written. Binding only the first wrapper's
255
+ // would leave a later bare `ref` under the second id bound by no
256
+ // wrapper, which is an encode failure, not a naming difference.
257
+ this.byWrapperId.set(id, w.idx);
258
+ return w.idx;
259
+ }
260
+ }
261
+ // Take the index before the body so its self-references resolve.
287
262
  const idx = this.entries.length;
288
- this.entries.push(null); // placeholder, must be filled
263
+ this.entries.push(null);
264
+ this.open.push({ id, idx });
265
+ let innerIdx;
266
+ try {
267
+ innerIdx = this.visit(payload.value.inner);
268
+ }
269
+ finally {
270
+ this.open.pop();
271
+ }
272
+ this.entries[idx] = { tag: TAG_RECURSIVE, params: varint(innerIdx) };
273
+ this.wrappers.push({ etv, idx });
274
+ this.byWrapperId.set(id, idx);
289
275
  return idx;
290
276
  }
291
- /** Register an EastTypeValue in both identity and type_id maps. */
292
- registerETV(etv, idx) {
293
- this.etvMap.set(etv, idx);
294
- const tid = getTypeId(etv);
295
- if (tid !== undefined)
296
- this.tidMap.set(tid, idx);
297
- }
298
- emitPrimitive(et, etv, typeName) {
299
- const idx = this.allocate();
300
- this.etMap.set(et, idx);
301
- this.registerETV(etv, idx);
302
- this.entries[idx] = { tag: TAG_FOR_TYPE[typeName], params: EMPTY };
303
- return idx;
304
- }
305
- emit(et, etv, tag, params) {
306
- const idx = this.allocate();
307
- this.etMap.set(et, idx);
308
- this.registerETV(etv, idx);
309
- this.entries[idx] = { tag, params };
310
- return idx;
311
- }
312
- emitNamedFields(et, etv, tag, names, resolve) {
313
- const indices = names.map(resolve);
314
- const w = new BufferWriter();
315
- w.writeVarint(names.length);
316
- for (let i = 0; i < names.length; i++) {
317
- w.writeStringUtf8Varint(names[i]);
318
- w.writeVarint(indices[i]);
277
+ openWrapper(id) {
278
+ for (let i = this.open.length - 1; i >= 0; i--) {
279
+ if (this.open[i].id === id)
280
+ return this.open[i].idx;
319
281
  }
320
- return this.emit(et, etv, tag, w.toUint8Array());
321
- }
322
- emitNamedFieldsETV(etv, tag, names, indices) {
323
- const w = new BufferWriter();
324
- w.writeVarint(names.length);
325
- for (let i = 0; i < names.length; i++) {
326
- w.writeStringUtf8Varint(names[i]);
327
- w.writeVarint(indices[i]);
328
- }
329
- const idx = this.allocate();
330
- this.registerETV(etv, idx);
331
- this.entries[idx] = { tag, params: w.toUint8Array() };
332
- return idx;
282
+ return undefined;
333
283
  }
334
- emitFunc(et, etv, typeName, inputIndices, outputIdx) {
335
- const w = new BufferWriter();
336
- w.writeVarint(inputIndices.length);
337
- for (const ii of inputIndices)
338
- w.writeVarint(ii);
339
- w.writeVarint(outputIdx);
340
- return this.emit(et, etv, TAG_FOR_TYPE[typeName], w.toUint8Array());
341
- }
342
- emitFuncETV(etv, caseName, inputIndices, outputIdx) {
343
- const w = new BufferWriter();
344
- w.writeVarint(inputIndices.length);
345
- for (const ii of inputIndices)
346
- w.writeVarint(ii);
347
- w.writeVarint(outputIdx);
348
- const idx = this.allocate();
349
- this.registerETV(etv, idx);
350
- this.entries[idx] = { tag: TAG_FOR_TYPE[caseName], params: w.toUint8Array() };
284
+ /** Append an entry unless the same bytes are already in the table. */
285
+ commit(tag, params) {
286
+ const key = contentKey(tag, params);
287
+ const existing = this.byContent.get(key);
288
+ if (existing !== undefined)
289
+ return existing;
290
+ const idx = this.entries.length;
291
+ this.entries.push({ tag, params });
292
+ this.byContent.set(key, idx);
351
293
  return idx;
352
294
  }
353
295
  }
354
296
  const EMPTY = new Uint8Array(0);
297
+ /** The id a `Recursive` payload names — the wrapper's own id, or a ref's. */
298
+ function recursiveId(payload) {
299
+ return payload.type === "ref" ? payload.value : payload.value.id;
300
+ }
301
+ /** The entry's bytes as a map key (one code unit per byte). */
302
+ function contentKey(tag, params) {
303
+ let key = String.fromCharCode(tag);
304
+ for (let i = 0; i < params.length; i++)
305
+ key += String.fromCharCode(params[i]);
306
+ return key;
307
+ }
355
308
  function varint(value) {
356
309
  const w = new BufferWriter();
357
310
  w.writeVarint(value);
@@ -457,13 +410,25 @@ function parseTypeTableSectionPayload(payload) {
457
410
  // Phase 1: Parse raw entries
458
411
  const parsed = new Array(entryCount);
459
412
  for (let i = 0; i < entryCount; i++) {
460
- parsed[i] = ENTRY_PARSERS[reader.readUint8()](reader);
413
+ const tag = reader.readUint8();
414
+ const parse = ENTRY_PARSERS[tag];
415
+ if (!parse)
416
+ throw new Error(`Type table entry ${i} has unknown tag 0x${tag.toString(16)}`);
417
+ parsed[i] = parse(reader);
418
+ // Every child index names an entry of this table.
419
+ for (const child of parsed[i].childIndices) {
420
+ if (child >= entryCount)
421
+ throw new Error(`Type table entry ${i} references entry ${child} of ${entryCount}`);
422
+ }
461
423
  }
462
424
  if (reader.offset !== payload.length) {
463
425
  throw new Error(`Type table size mismatch: expected offset ${payload.length}, got ${reader.offset}`);
464
426
  }
465
- // Phase 2: Reconstruct EastTypeValue tree with depth-based Recursive references
466
- const table = reconstructTypes(parsed, rootIdx);
427
+ if (rootIdx >= entryCount) {
428
+ throw new Error(`Type table root index ${rootIdx} out of range (${entryCount} entries)`);
429
+ }
430
+ // Phase 2: Reconstruct EastTypeValue tree with Recursive wrapper/ref references
431
+ const table = reconstructTypes(parsed);
467
432
  return { rootType: table[rootIdx], typeTable: table };
468
433
  }
469
434
  /**
@@ -528,7 +493,7 @@ const ENTRY_PARSERS = [
528
493
  /* 0x11 */ parseFunc(TAG_ASYNC_FUNCTION),
529
494
  /* 0x12 */ parseRecursive,
530
495
  ];
531
- // ─�� Reconstruction: flat table → EastTypeValue with Recursive(depth) ──
496
+ // ── Reconstruction: flat table → EastTypeValue with wrapper/ref recursion ──
532
497
  const TAG_TO_CASE = [
533
498
  "Null", "String", "Integer", "Float", "Boolean", "DateTime", "Blob", "Never",
534
499
  "Variant", "Struct", "Array", "Dict", "Set", "Ref", "Vector", "Matrix",
@@ -547,165 +512,150 @@ const PRIMITIVE_ETV = [
547
512
  /**
548
513
  * Reconstruct EastTypeValue objects from parsed flat table entries.
549
514
  *
550
- * Recursive entries are converted to proper EastTypeValue trees where
551
- * self-references use depth-based Recursive(N) — compatible with
552
- * _decodeCursorFor's typeCtx stack mechanism.
515
+ * A Recursive entry becomes `wrapper({id, inner})` whose id is its table
516
+ * index; inside its body a reference back to it is `ref(id)`. The returned
517
+ * table holds every entry's **closed** form — built in no wrapper's scope, so
518
+ * it is a complete type wherever it is used (v4 value-table entries name
519
+ * element types by index).
553
520
  *
554
- * One table index yields exactly **one** object, shared by every reference to
555
- * it. That is what makes a decoded type round-trip: {@link TypeTableBuilder}
556
- * dedups EastTypeValues by identity, so re-encoding a decoded value must see
557
- * the same object wherever the wire table said "same index". Rebuilding a
558
- * nested recursive type per reference (as this did before) gave `IRType` 25
559
- * wrapper entries instead of 2 and made TS re-encode a decoded IR program to
560
- * 5426 bytes where east-c — which interns decoded types — produced the
561
- * original 1792.
521
+ * The same entry can be reached from inside a wrapper's body and from outside
522
+ * it — a canonical table writes `Array<T>` once whether it occurs as `T`'s
523
+ * child or as another field's type — and the two readings are different
524
+ * objects: inside, references to `T` are `ref`s; outside, they are the
525
+ * wrapper. So an object built under a wrapper (one that holds a `ref` to it)
526
+ * is memoized only for that wrapper's scope, and an object that references no
527
+ * open wrapper is closed and memoized for good. One table index yields one
528
+ * closed object, shared by every reference to it, so a decoded type is a
529
+ * canonical input to {@link TypeTableBuilder} in turn.
530
+ *
531
+ * The walk is top-down, so the outside reading of `Array<T>` can be under
532
+ * construction when `T` — its child, indexed before it — opens and its body
533
+ * reaches `Array<T>` again. That re-entry is `T`'s own recursion, built as
534
+ * the inside reading in `T`'s scope; only a cycle with no wrapper in it is
535
+ * malformed. So entries under construction are tracked per open wrapper
536
+ * (#773 — east-ui's `TreeView.nodes`, an `Array<Inner>` whose `Inner` has
537
+ * `children: Array<self>`, under the `UIComponentType` wrapper).
562
538
  */
563
- function reconstructTypes(parsed, _rootIdx) {
564
- const table = new Array(parsed.length);
565
- // Track which indices are Recursive wrappers and their inner indices
566
- const recursiveEntries = new Map(); // wrapper_idx → inner_idx
567
- // Wrapper indices whose rebuilt subtree references no *enclosing* wrapper.
568
- // Such a wrapper is self-contained, so one object can serve every reference
569
- // to it — see the `closed` computation below.
570
- const closedWrappers = new Set();
571
- // Shallowest depthStack position referenced by the subtree currently under
572
- // construction (Infinity = nothing referenced). Read/reset around each
573
- // wrapper to decide whether that wrapper escaped its own scope.
574
- let minRefPos = Infinity;
575
- // First pass: identify all Recursive entries
576
- for (let i = 0; i < parsed.length; i++) {
577
- if (parsed[i].tag === TAG_RECURSIVE) {
578
- recursiveEntries.set(i, parsed[i].childIndices[0]);
579
- }
580
- }
581
- // Build each entry. For non-recursive entries, this is straightforward.
582
- // For the inner types of recursive entries, self-references (indices pointing
583
- // back to a Recursive wrapper) must be replaced with Recursive(depth).
584
- //
585
- // We process bottom-up: entries with lower indices are built first (they
586
- // have no forward references except through Recursive wrappers).
587
- // recursionStack tracks which Recursive wrapper indices are "in scope"
588
- // as we build types, so we can compute depth for Recursive(N) references.
589
- function build(idx, depthStack) {
590
- // If this index is a Recursive wrapper currently being built, emit Recursive(ref(id)).
591
- // The id is the table index of the wrapper entry.
592
- const stackPos = depthStack.indexOf(idx);
593
- if (stackPos !== -1) {
594
- if (stackPos < minRefPos)
595
- minRefPos = stackPos;
539
+ function reconstructTypes(parsed) {
540
+ const closed = new Array(parsed.length);
541
+ // Open wrapper indices, outermost first; `scopes[i]` memoizes what was
542
+ // built under `stack[i]` and reaches an open wrapper.
543
+ const stack = [];
544
+ const scopes = [];
545
+ // Non-wrapper entries under construction since the innermost open wrapper
546
+ // was pushed (`building[0]` for none): a cycle through them has no wrapper
547
+ // to close it and is malformed. Reaching one from inside a wrapper opened
548
+ // later is that wrapper's recursion, so each wrapper starts a fresh set.
549
+ const building = [new Set()];
550
+ // Shallowest stack position referenced by the subtree under construction
551
+ // (Infinity = nothing referenced).
552
+ let minRef = Infinity;
553
+ function build(idx) {
554
+ const pos = stack.indexOf(idx);
555
+ if (pos !== -1) {
556
+ if (pos < minRef)
557
+ minRef = pos;
596
558
  return variant("Recursive", variant("ref", BigInt(idx)));
597
559
  }
598
- // If already built, reuse. A Recursive wrapper only qualifies once it is
599
- // known to be closed: an *open* wrapper's `ref`s resolve against the scope
600
- // it was built in, so it cannot be shared with a different scope.
601
- if (table[idx] !== undefined && (!recursiveEntries.has(idx) || closedWrappers.has(idx))) {
602
- return table[idx];
560
+ const done = closed[idx];
561
+ if (done !== undefined)
562
+ return done;
563
+ if (scopes.length > 0) {
564
+ const scoped = scopes[scopes.length - 1].get(idx);
565
+ if (scoped !== undefined) {
566
+ if (scoped.minRef < minRef)
567
+ minRef = scoped.minRef;
568
+ return scoped.value;
569
+ }
603
570
  }
604
571
  const entry = parsed[idx];
605
- // Recursive wrapper: push wrapper index on stack, recurse into inner compound.
606
- // toEastTypeValue pushes BOTH the RecursiveType wrapper and the inner compound
607
- // type onto its stack, so we mirror that here for matching depth values.
572
+ if (entry.tag <= TAG_NEVER) {
573
+ const primitive = PRIMITIVE_ETV[entry.tag];
574
+ closed[idx] = primitive;
575
+ return primitive;
576
+ }
577
+ const depth = stack.length;
578
+ const outerMinRef = minRef;
579
+ minRef = Infinity;
580
+ let result;
608
581
  if (entry.tag === TAG_RECURSIVE) {
609
- const innerIdx = entry.childIndices[0];
610
- const base = depthStack.length; // the position this wrapper occupies
611
- const outerMinRefPos = minRefPos;
612
- minRefPos = Infinity;
613
- depthStack.push(idx); // Push wrapper — stays on stack for depth counting
614
- const inner = buildCompound(innerIdx, depthStack, null);
615
- depthStack.pop();
616
- // Closed ⇔ nothing under this wrapper referenced a wrapper above it.
617
- // East guarantees this for every type it can construct (RecursiveType
618
- // rejects SCC > 1), so this is the universal case; the check only keeps
619
- // a hand-crafted mutually-recursive table from sharing a scoped object.
620
- const closed = minRefPos >= base;
621
- minRefPos = closed ? outerMinRefPos : Math.min(outerMinRefPos, minRefPos);
622
- const wrapped = variant("Recursive", variant("wrapper", { id: BigInt(idx), inner }));
623
- table[idx] = wrapped;
624
- if (closed)
625
- closedWrappers.add(idx);
626
- return wrapped;
582
+ stack.push(idx);
583
+ scopes.push(new Map());
584
+ building.push(new Set());
585
+ let inner;
586
+ try {
587
+ inner = build(entry.childIndices[0]);
588
+ }
589
+ finally {
590
+ stack.pop();
591
+ scopes.pop();
592
+ building.pop();
593
+ }
594
+ result = variant("Recursive", variant("wrapper", { id: BigInt(idx), inner }));
627
595
  }
628
- // Primitives
629
- if (entry.tag <= TAG_NEVER) {
630
- table[idx] = PRIMITIVE_ETV[entry.tag];
631
- return table[idx];
596
+ else {
597
+ const open = building[building.length - 1];
598
+ if (open.has(idx)) {
599
+ throw new Error(`Type table entry ${idx} refers to itself without a Recursive wrapper`);
600
+ }
601
+ open.add(idx);
602
+ try {
603
+ result = buildCompound(entry);
604
+ }
605
+ finally {
606
+ open.delete(idx);
607
+ }
608
+ }
609
+ const mine = minRef;
610
+ // Closed ⇔ nothing under this entry referenced a wrapper open above it
611
+ // (a wrapper's own self-references sit at `depth`, so it stays closed).
612
+ if (mine >= depth) {
613
+ closed[idx] = result;
632
614
  }
633
- const result = buildCompound(idx, depthStack, null);
634
- table[idx] = result;
615
+ else {
616
+ scopes[scopes.length - 1].set(idx, { value: result, minRef: mine });
617
+ }
618
+ minRef = Math.min(outerMinRef, mine);
635
619
  return result;
636
620
  }
637
- /**
638
- * Build a compound type entry, pushing to the depth stack.
639
- * If recursiveOwner is set, self-references to that index (the Recursive
640
- * wrapper) will be found on the stack at the position where this compound
641
- * type is pushed.
642
- */
643
- function buildCompound(idx, depthStack, recursiveOwner) {
644
- const entry = parsed[idx];
645
- // Push: the index that self-references should find.
646
- // If this compound is the inner type of a Recursive wrapper, push the
647
- // wrapper's index so Recursive(depth) resolves to it.
648
- const stackEntry = recursiveOwner ?? idx;
649
- depthStack.push(stackEntry);
650
- let result;
621
+ function buildCompound(entry) {
651
622
  switch (entry.tag) {
652
- case TAG_VARIANT: {
653
- const cases = entry.names.map((name, i) => ({
654
- name, type: build(entry.childIndices[i], depthStack),
655
- }));
656
- result = variant("Variant", cases);
657
- break;
658
- }
623
+ case TAG_VARIANT:
659
624
  case TAG_STRUCT: {
660
- const fields = entry.names.map((name, i) => ({
661
- name, type: build(entry.childIndices[i], depthStack),
662
- }));
663
- result = variant("Struct", fields);
664
- break;
625
+ const members = entry.names.map((name, i) => ({ name, type: build(entry.childIndices[i]) }));
626
+ return variant(TAG_TO_CASE[entry.tag], members);
665
627
  }
666
628
  case TAG_ARRAY:
667
- result = variant("Array", build(entry.childIndices[0], depthStack));
668
- break;
669
- case TAG_DICT:
670
- result = variant("Dict", {
671
- key: build(entry.childIndices[0], depthStack),
672
- value: build(entry.childIndices[1], depthStack),
673
- });
674
- break;
675
629
  case TAG_SET:
676
- result = variant("Set", build(entry.childIndices[0], depthStack));
677
- break;
678
630
  case TAG_REF:
679
- result = variant("Ref", build(entry.childIndices[0], depthStack));
680
- break;
681
631
  case TAG_VECTOR:
682
- result = variant("Vector", build(entry.childIndices[0], depthStack));
683
- break;
684
632
  case TAG_MATRIX:
685
- result = variant("Matrix", build(entry.childIndices[0], depthStack));
686
- break;
633
+ return variant(TAG_TO_CASE[entry.tag], build(entry.childIndices[0]));
634
+ case TAG_DICT: {
635
+ const key = build(entry.childIndices[0]);
636
+ const value = build(entry.childIndices[1]);
637
+ return variant("Dict", { key, value });
638
+ }
687
639
  case TAG_FUNCTION:
688
640
  case TAG_ASYNC_FUNCTION: {
689
641
  const n = entry.childIndices.length - 1;
690
642
  const inputs = [];
691
643
  for (let i = 0; i < n; i++)
692
- inputs.push(build(entry.childIndices[i], depthStack));
693
- const output = build(entry.childIndices[n], depthStack);
694
- result = variant(TAG_TO_CASE[entry.tag], { inputs, output });
695
- break;
644
+ inputs.push(build(entry.childIndices[i]));
645
+ const output = build(entry.childIndices[n]);
646
+ return variant(TAG_TO_CASE[entry.tag], { inputs, output });
696
647
  }
697
648
  default:
698
649
  throw new Error(`Unexpected compound tag 0x${entry.tag.toString(16)}`);
699
650
  }
700
- depthStack.pop();
701
- return result;
702
651
  }
703
- // Build ALL entries (not just root-reachable). Extra entries from IR
704
- // capture types are needed for function IR type restoration during decode.
652
+ // Build ALL entries (not just root-reachable) in their closed form. Extra
653
+ // entries from IR capture types are needed for function IR type restoration
654
+ // during v4 decode.
705
655
  for (let i = 0; i < parsed.length; i++) {
706
- if (table[i] === undefined)
707
- build(i, []);
656
+ if (closed[i] === undefined)
657
+ build(i);
708
658
  }
709
- return table;
659
+ return closed;
710
660
  }
711
661
  //# sourceMappingURL=type-table.js.map