animader 0.0.0-stage → 0.1.1
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 +121 -0
- package/package.json +23 -4
- package/src/index.js +482 -0
- package/src/log.js +304 -0
- package/src/machine.js +712 -0
- package/src/piece.js +222 -0
- package/src/route.js +116 -0
- package/src/session.js +480 -0
- package/wasm/animader.wasm +0 -0
- package/README.md +0 -3
package/src/machine.js
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
// animader/machine - the Machine class over the machine boundary (plan 02,
|
|
2
|
+
// phase 6).
|
|
3
|
+
//
|
|
4
|
+
// This file mirrors src/wasm_entry.zig's machine section byte for byte.
|
|
5
|
+
// The host writes a document into the shared input zone, calls
|
|
6
|
+
// `anm_machine_load` once, then drives the cached machine:
|
|
7
|
+
//
|
|
8
|
+
// - `anm_machine_step(events_len, now)` takes an 'ANEV' blob (the log
|
|
9
|
+
// records a session parses to) and writes the outputs wire;
|
|
10
|
+
// - `anm_machine_snapshot()` / `anm_machine_restore(blob_len)` move the
|
|
11
|
+
// `'ANMS'` snapshot;
|
|
12
|
+
// - `anm_machine_setInput(name, lane0)` and `anm_machine_fireTrigger`
|
|
13
|
+
// apply one `(input-write ...)` record at the snapshot's current time;
|
|
14
|
+
// - `anm_machine_meta()` writes the `'ANMM'` listing;
|
|
15
|
+
// - `anm_machine_diag()` writes the last load's `'ANMD'` diagnostics;
|
|
16
|
+
// - `anm_machine_emits()` writes the last stepping call's `'ANME'`
|
|
17
|
+
// emitted events, the frame contract's `host.handle` step;
|
|
18
|
+
// - `anm_machine_eject(doc_len, verb_index)` is the phase-5 export.
|
|
19
|
+
//
|
|
20
|
+
// Return convention, mirrored from the entry header: a call answers a byte
|
|
21
|
+
// length (>= 0) or a negative status, never data:
|
|
22
|
+
//
|
|
23
|
+
// -1 diagnostics; the load is kept so `diag()` answers its blob
|
|
24
|
+
// -2 bad call (over a zone cap, a malformed blob, no clean machine)
|
|
25
|
+
// -3 out of memory
|
|
26
|
+
// -4 output over its zone
|
|
27
|
+
// -5 a component over its cap
|
|
28
|
+
// -6 a well-formed snapshot from a different machine
|
|
29
|
+
//
|
|
30
|
+
// The outputs wire (`anm_machine_step`):
|
|
31
|
+
//
|
|
32
|
+
// [count u32]
|
|
33
|
+
// count x { index u16, arity u8, pad u8 = 0, lanes [4]f64 LE }
|
|
34
|
+
//
|
|
35
|
+
// The snapshot is `'ANMS'`: 28-byte header (magic, version, machine_hash
|
|
36
|
+
// u64, time i64, input_count u16, layer_count u16), then per input a value
|
|
37
|
+
// f64 and an armed byte with 7 pad bytes, then per layer a state u16, a
|
|
38
|
+
// blend_from u16, 4 pad bytes and three i64 clocks, then two u64 counters.
|
|
39
|
+
// The meta is `'ANMM'`: a 32-byte header of six u32 counts (inputs, hit
|
|
40
|
+
// areas, outputs, layers, states, transitions) and the records in that
|
|
41
|
+
// order, each followed by its name bytes. The diagnostics blob is `'ANMD'`:
|
|
42
|
+
// magic, version, count, then per record a source byte, a severity byte, a
|
|
43
|
+
// code u16, two u32 span ends, a message length u32 and the message.
|
|
44
|
+
//
|
|
45
|
+
// The emits blob is `'ANME'` (src/Emits.zig), the `'ANEV'` envelope's own
|
|
46
|
+
// shape: a 20-byte header (magic, version, count, strtab_len, the last
|
|
47
|
+
// four bytes zero pad), then per emit a 10-byte record (`at` i64
|
|
48
|
+
// microseconds, `name` u16 index), then the string table of NUL-separated
|
|
49
|
+
// names in first-occurrence order. An emit carries no value.
|
|
50
|
+
//
|
|
51
|
+
// Memory views are re-taken from `exports.memory.buffer` on every call
|
|
52
|
+
// (the pacer discipline): a view taken before a grow would detach.
|
|
53
|
+
|
|
54
|
+
const encoder = new TextEncoder();
|
|
55
|
+
const decoder = new TextDecoder();
|
|
56
|
+
|
|
57
|
+
const MAGIC_ANMS = 0x534d4e41; // 'ANMS' read little-endian
|
|
58
|
+
const SNAPSHOT_VERSION = 1;
|
|
59
|
+
const SNAPSHOT_HEADER_LEN = 28;
|
|
60
|
+
const INPUT_STATE_LEN = 16;
|
|
61
|
+
const LAYER_STATE_LEN = 32;
|
|
62
|
+
const SNAPSHOT_TAIL_LEN = 16;
|
|
63
|
+
const MAX_INPUTS = 256;
|
|
64
|
+
const MAX_LAYERS = 32;
|
|
65
|
+
const NO_INDEX = 0xffff;
|
|
66
|
+
|
|
67
|
+
const MAGIC_ANMD = 0x444d4e41; // 'ANMD' read little-endian
|
|
68
|
+
const DIAGS_VERSION = 1;
|
|
69
|
+
const DIAGS_HEADER_LEN = 12;
|
|
70
|
+
const DIAG_RECORD_LEN = 16;
|
|
71
|
+
|
|
72
|
+
const MAGIC_ANMM = 0x4d4d4e41; // 'ANMM' read little-endian
|
|
73
|
+
const META_VERSION = 1;
|
|
74
|
+
const META_HEADER_LEN = 32;
|
|
75
|
+
const INPUT_REC_LEN = 24;
|
|
76
|
+
const HIT_AREA_REC_LEN = 40;
|
|
77
|
+
const OUTPUT_REC_LEN = 4;
|
|
78
|
+
const LAYER_REC_LEN = 16;
|
|
79
|
+
const STATE_REC_LEN = 24;
|
|
80
|
+
const TRANSITION_REC_LEN = 24;
|
|
81
|
+
|
|
82
|
+
const MAGIC_ANME = 0x454d4e41; // 'ANME' read little-endian
|
|
83
|
+
const EMITS_VERSION = 1;
|
|
84
|
+
const EMITS_HEADER_LEN = 20;
|
|
85
|
+
const EMIT_RECORD_LEN = 10;
|
|
86
|
+
|
|
87
|
+
const OUTPUTS_HEADER_LEN = 4;
|
|
88
|
+
const OUTPUTS_ENTRY_LEN = 36;
|
|
89
|
+
|
|
90
|
+
const STATUS_DIAGNOSTICS = -1;
|
|
91
|
+
const STATUS_BAD_CALL = -2;
|
|
92
|
+
const STATUS_OOM = -3;
|
|
93
|
+
const STATUS_OUTPUT_TOO_SMALL = -4;
|
|
94
|
+
const STATUS_COMPONENT_TOO_LARGE = -5;
|
|
95
|
+
const STATUS_SNAPSHOT_MISMATCH = -6;
|
|
96
|
+
|
|
97
|
+
/** The machine diagnostic ordinals (`Machine.Code`), append-only. */
|
|
98
|
+
export const MACHINE_CODES = {
|
|
99
|
+
state_duplicate: 0,
|
|
100
|
+
transition_cross_layer: 1,
|
|
101
|
+
output_multiply_owned: 2,
|
|
102
|
+
output_undefined_in_state: 3,
|
|
103
|
+
output_arity_mismatch: 4,
|
|
104
|
+
when_references_time: 5,
|
|
105
|
+
transition_cycle: 6,
|
|
106
|
+
expr_invalid: 7,
|
|
107
|
+
verb_invalid: 8,
|
|
108
|
+
input_cap_exceeded: 9,
|
|
109
|
+
layer_cap_exceeded: 10,
|
|
110
|
+
state_cap_exceeded: 11,
|
|
111
|
+
transition_cap_exceeded: 12,
|
|
112
|
+
snapshot_mismatch: 13,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** The status semantics, so a caller does not have to know the integers. */
|
|
116
|
+
export const STATUS = {
|
|
117
|
+
DIAGNOSTICS: STATUS_DIAGNOSTICS,
|
|
118
|
+
BAD_CALL: STATUS_BAD_CALL,
|
|
119
|
+
OOM: STATUS_OOM,
|
|
120
|
+
OUTPUT_TOO_SMALL: STATUS_OUTPUT_TOO_SMALL,
|
|
121
|
+
COMPONENT_TOO_LARGE: STATUS_COMPONENT_TOO_LARGE,
|
|
122
|
+
SNAPSHOT_MISMATCH: STATUS_SNAPSHOT_MISMATCH,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const describeStatus = (status) => {
|
|
126
|
+
switch (status) {
|
|
127
|
+
case STATUS_DIAGNOSTICS:
|
|
128
|
+
return "the document has diagnostics";
|
|
129
|
+
case STATUS_BAD_CALL:
|
|
130
|
+
return "bad call";
|
|
131
|
+
case STATUS_OOM:
|
|
132
|
+
return "out of memory";
|
|
133
|
+
case STATUS_OUTPUT_TOO_SMALL:
|
|
134
|
+
return "the output would not fit its zone";
|
|
135
|
+
case STATUS_COMPONENT_TOO_LARGE:
|
|
136
|
+
return "a component is over its cap";
|
|
137
|
+
case STATUS_SNAPSHOT_MISMATCH:
|
|
138
|
+
return "the snapshot belongs to a different machine";
|
|
139
|
+
default:
|
|
140
|
+
return `unknown status ${status}`;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** A boundary failure, a diagnosed document, or a malformed blob. */
|
|
145
|
+
export class MachineError extends Error {
|
|
146
|
+
/**
|
|
147
|
+
* @param {string} message
|
|
148
|
+
* @param {{ status?: number | null, code?: number | null, diagnostics?: Array<object> | null }} [details]
|
|
149
|
+
* `status` is the boundary's negative return (null for a JS-side
|
|
150
|
+
* defect); `code` is the first machine diagnostic code for a refused
|
|
151
|
+
* restore; `diagnostics` is the decoded `'ANMD'` list for a diagnosed
|
|
152
|
+
* load.
|
|
153
|
+
*/
|
|
154
|
+
constructor(message, { status = null, code = null, diagnostics = null } = {}) {
|
|
155
|
+
super(message);
|
|
156
|
+
this.name = "MachineError";
|
|
157
|
+
this.status = status;
|
|
158
|
+
this.code = code;
|
|
159
|
+
this.diagnostics = diagnostics;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function toBytes(blob) {
|
|
164
|
+
if (blob instanceof Uint8Array) return blob;
|
|
165
|
+
if (blob instanceof ArrayBuffer) return new Uint8Array(blob);
|
|
166
|
+
if (ArrayBuffer.isView(blob)) return new Uint8Array(blob.buffer, blob.byteOffset, blob.byteLength);
|
|
167
|
+
throw new TypeError("animader: expected an ArrayBuffer or a typed array");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function micros(value, what) {
|
|
171
|
+
if (!Number.isSafeInteger(value)) {
|
|
172
|
+
throw new MachineError(`animader: ${what} must be a safe integer (microseconds), got ${value}`, {
|
|
173
|
+
status: STATUS_BAD_CALL,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function viewOf(bytes) {
|
|
180
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function nameAt(bytes, off, len) {
|
|
184
|
+
return decoder.decode(bytes.subarray(off, off + len));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** One `'ANMS'` blob decoded field by field; throws on any defect. */
|
|
188
|
+
export function decodeSnapshot(blob) {
|
|
189
|
+
const bytes = toBytes(blob);
|
|
190
|
+
if (bytes.byteLength < SNAPSHOT_HEADER_LEN) {
|
|
191
|
+
throw new MachineError(`animader: 'ANMS' blob is ${bytes.byteLength} bytes, under the header`);
|
|
192
|
+
}
|
|
193
|
+
const view = viewOf(bytes);
|
|
194
|
+
if (view.getUint32(0, true) !== MAGIC_ANMS) throw new MachineError("animader: 'ANMS' magic is missing");
|
|
195
|
+
if (view.getUint32(4, true) !== SNAPSHOT_VERSION) {
|
|
196
|
+
throw new MachineError(`animader: 'ANMS' version ${view.getUint32(4, true)} is not ${SNAPSHOT_VERSION}`);
|
|
197
|
+
}
|
|
198
|
+
const machineHash = view.getBigUint64(8, true);
|
|
199
|
+
const time = Number(view.getBigInt64(16, true));
|
|
200
|
+
const inputCount = view.getUint16(24, true);
|
|
201
|
+
const layerCount = view.getUint16(26, true);
|
|
202
|
+
if (inputCount > MAX_INPUTS) throw new MachineError(`animader: 'ANMS' input count ${inputCount} over the cap`);
|
|
203
|
+
if (layerCount > MAX_LAYERS) throw new MachineError(`animader: 'ANMS' layer count ${layerCount} over the cap`);
|
|
204
|
+
const need =
|
|
205
|
+
SNAPSHOT_HEADER_LEN + inputCount * INPUT_STATE_LEN + layerCount * LAYER_STATE_LEN + SNAPSHOT_TAIL_LEN;
|
|
206
|
+
if (bytes.byteLength !== need) {
|
|
207
|
+
throw new MachineError("animader: 'ANMS' length does not match its header");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const inputs = [];
|
|
211
|
+
let off = SNAPSHOT_HEADER_LEN;
|
|
212
|
+
for (let i = 0; i < inputCount; i++, off += INPUT_STATE_LEN) {
|
|
213
|
+
inputs.push({
|
|
214
|
+
value: view.getBigUint64(off, true),
|
|
215
|
+
armed: view.getUint8(off + 8) !== 0,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const layers = [];
|
|
219
|
+
for (let i = 0; i < layerCount; i++, off += LAYER_STATE_LEN) {
|
|
220
|
+
layers.push({
|
|
221
|
+
state: view.getUint16(off, true),
|
|
222
|
+
blendFrom: view.getUint16(off + 2, true),
|
|
223
|
+
enteredAt: Number(view.getBigInt64(off + 8, true)),
|
|
224
|
+
blendStart: Number(view.getBigInt64(off + 16, true)),
|
|
225
|
+
fromEnteredAt: Number(view.getBigInt64(off + 24, true)),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
machineHash,
|
|
230
|
+
time,
|
|
231
|
+
inputCount,
|
|
232
|
+
layerCount,
|
|
233
|
+
inputs,
|
|
234
|
+
layers,
|
|
235
|
+
chainCapHits: Number(view.getBigUint64(off, true)),
|
|
236
|
+
unknownInputHits: Number(view.getBigUint64(off + 8, true)),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The `anm_machine_step` outputs wire, decoded; throws on any defect. */
|
|
241
|
+
export function decodeOutputs(blob) {
|
|
242
|
+
const bytes = toBytes(blob);
|
|
243
|
+
if (bytes.byteLength < OUTPUTS_HEADER_LEN) {
|
|
244
|
+
throw new MachineError(`animader: outputs wire is ${bytes.byteLength} bytes, under the header`);
|
|
245
|
+
}
|
|
246
|
+
const view = viewOf(bytes);
|
|
247
|
+
const count = view.getUint32(0, true);
|
|
248
|
+
if (bytes.byteLength !== OUTPUTS_HEADER_LEN + count * OUTPUTS_ENTRY_LEN) {
|
|
249
|
+
throw new MachineError("animader: outputs wire length does not match its header");
|
|
250
|
+
}
|
|
251
|
+
const out = [];
|
|
252
|
+
for (let i = 0; i < count; i++) {
|
|
253
|
+
const off = OUTPUTS_HEADER_LEN + i * OUTPUTS_ENTRY_LEN;
|
|
254
|
+
const bits = [];
|
|
255
|
+
const value = [];
|
|
256
|
+
for (let lane = 0; lane < 4; lane++) {
|
|
257
|
+
const b = view.getBigUint64(off + 4 + lane * 8, true);
|
|
258
|
+
bits.push(b);
|
|
259
|
+
value.push(view.getFloat64(off + 4 + lane * 8, true));
|
|
260
|
+
}
|
|
261
|
+
out.push({ index: view.getUint16(off, true), arity: view.getUint8(off + 2), value, bits });
|
|
262
|
+
}
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The `'ANMM'` listing decoded; throws on any defect. */
|
|
267
|
+
export function decodeMeta(blob) {
|
|
268
|
+
const bytes = toBytes(blob);
|
|
269
|
+
if (bytes.byteLength < META_HEADER_LEN) {
|
|
270
|
+
throw new MachineError(`animader: 'ANMM' blob is ${bytes.byteLength} bytes, under the header`);
|
|
271
|
+
}
|
|
272
|
+
const view = viewOf(bytes);
|
|
273
|
+
if (view.getUint32(0, true) !== MAGIC_ANMM) throw new MachineError("animader: 'ANMM' magic is missing");
|
|
274
|
+
if (view.getUint32(4, true) !== META_VERSION) {
|
|
275
|
+
throw new MachineError(`animader: 'ANMM' version ${view.getUint32(4, true)} is not ${META_VERSION}`);
|
|
276
|
+
}
|
|
277
|
+
const counts = {
|
|
278
|
+
inputs: view.getUint32(8, true),
|
|
279
|
+
hitAreas: view.getUint32(12, true),
|
|
280
|
+
outputs: view.getUint32(16, true),
|
|
281
|
+
layers: view.getUint32(20, true),
|
|
282
|
+
states: view.getUint32(24, true),
|
|
283
|
+
transitions: view.getUint32(28, true),
|
|
284
|
+
};
|
|
285
|
+
let off = META_HEADER_LEN;
|
|
286
|
+
const take = (len, what) => {
|
|
287
|
+
if (off + len > bytes.byteLength) throw new MachineError(`animader: 'ANMM' ${what} runs past the blob`);
|
|
288
|
+
const start = off;
|
|
289
|
+
off += len;
|
|
290
|
+
return start;
|
|
291
|
+
};
|
|
292
|
+
const nameOf = (recOff, nameLen) => nameAt(bytes, recOff, nameLen);
|
|
293
|
+
|
|
294
|
+
const inputs = [];
|
|
295
|
+
for (let i = 0; i < counts.inputs; i++) {
|
|
296
|
+
const rec = take(INPUT_REC_LEN, "input record");
|
|
297
|
+
const nameLen = view.getUint16(rec, true);
|
|
298
|
+
inputs.push({
|
|
299
|
+
name: nameOf(take(nameLen, "input name"), nameLen),
|
|
300
|
+
kind: ["number", "bool", "trigger"][view.getUint8(rec + 2)],
|
|
301
|
+
valueSpan: { start: view.getUint32(rec + 4, true), end: view.getUint32(rec + 8, true) },
|
|
302
|
+
defaultBits: view.getBigUint64(rec + 16, true),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const hitAreas = [];
|
|
306
|
+
for (let i = 0; i < counts.hitAreas; i++) {
|
|
307
|
+
const rec = take(HIT_AREA_REC_LEN, "hit-area record");
|
|
308
|
+
const nameLen = view.getUint16(rec, true);
|
|
309
|
+
hitAreas.push({
|
|
310
|
+
name: nameOf(take(nameLen, "hit-area name"), nameLen),
|
|
311
|
+
rect: [0, 1, 2, 3].map((lane) => view.getFloat64(rec + 8 + lane * 8, true)),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const outputs = [];
|
|
315
|
+
for (let i = 0; i < counts.outputs; i++) {
|
|
316
|
+
const rec = take(OUTPUT_REC_LEN, "output record");
|
|
317
|
+
const nameLen = view.getUint16(rec, true);
|
|
318
|
+
outputs.push({ name: nameOf(take(nameLen, "output name"), nameLen), arity: view.getUint8(rec + 2) });
|
|
319
|
+
}
|
|
320
|
+
const layers = [];
|
|
321
|
+
for (let i = 0; i < counts.layers; i++) {
|
|
322
|
+
const rec = take(LAYER_REC_LEN, "layer record");
|
|
323
|
+
const nameLen = view.getUint16(rec, true);
|
|
324
|
+
layers.push({
|
|
325
|
+
name: nameOf(take(nameLen, "layer name"), nameLen),
|
|
326
|
+
entry: view.getUint16(rec + 4, true),
|
|
327
|
+
span: { start: view.getUint32(rec + 8, true), end: view.getUint32(rec + 12, true) },
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
const states = [];
|
|
331
|
+
for (let i = 0; i < counts.states; i++) {
|
|
332
|
+
const rec = take(STATE_REC_LEN, "state record");
|
|
333
|
+
const nameLen = view.getUint16(rec, true);
|
|
334
|
+
states.push({
|
|
335
|
+
name: nameOf(take(nameLen, "state name"), nameLen),
|
|
336
|
+
layer: view.getUint16(rec + 2, true),
|
|
337
|
+
origin: view.getUint8(rec + 4),
|
|
338
|
+
span: { start: view.getUint32(rec + 8, true), end: view.getUint32(rec + 12, true) },
|
|
339
|
+
verbSpan: { start: view.getUint32(rec + 16, true), end: view.getUint32(rec + 20, true) },
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const transitions = [];
|
|
343
|
+
for (let i = 0; i < counts.transitions; i++) {
|
|
344
|
+
const rec = take(TRANSITION_REC_LEN, "transition record");
|
|
345
|
+
transitions.push({
|
|
346
|
+
from: view.getUint16(rec, true),
|
|
347
|
+
layer: view.getUint16(rec + 2, true),
|
|
348
|
+
to: view.getUint16(rec + 4, true),
|
|
349
|
+
origin: view.getUint8(rec + 6),
|
|
350
|
+
span: { start: view.getUint32(rec + 8, true), end: view.getUint32(rec + 12, true) },
|
|
351
|
+
verbSpan: { start: view.getUint32(rec + 16, true), end: view.getUint32(rec + 20, true) },
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (off !== bytes.byteLength) throw new MachineError("animader: 'ANMM' has trailing bytes");
|
|
355
|
+
return { counts, inputs, hitAreas, outputs, layers, states, transitions };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** The `'ANMD'` diagnostics decoded; throws on any defect. */
|
|
359
|
+
export function decodeDiags(blob) {
|
|
360
|
+
const bytes = toBytes(blob);
|
|
361
|
+
if (bytes.byteLength < DIAGS_HEADER_LEN) {
|
|
362
|
+
throw new MachineError(`animader: 'ANMD' blob is ${bytes.byteLength} bytes, under the header`);
|
|
363
|
+
}
|
|
364
|
+
const view = viewOf(bytes);
|
|
365
|
+
if (view.getUint32(0, true) !== MAGIC_ANMD) throw new MachineError("animader: 'ANMD' magic is missing");
|
|
366
|
+
if (view.getUint32(4, true) !== DIAGS_VERSION) {
|
|
367
|
+
throw new MachineError(`animader: 'ANMD' version ${view.getUint32(4, true)} is not ${DIAGS_VERSION}`);
|
|
368
|
+
}
|
|
369
|
+
const count = view.getUint32(8, true);
|
|
370
|
+
const diagnostics = [];
|
|
371
|
+
let off = DIAGS_HEADER_LEN;
|
|
372
|
+
for (let i = 0; i < count; i++) {
|
|
373
|
+
if (off + DIAG_RECORD_LEN > bytes.byteLength) throw new MachineError("animader: 'ANMD' record runs past the blob");
|
|
374
|
+
const msgLen = view.getUint32(off + 12, true);
|
|
375
|
+
if (off + DIAG_RECORD_LEN + msgLen > bytes.byteLength) {
|
|
376
|
+
throw new MachineError("animader: 'ANMD' message runs past the blob");
|
|
377
|
+
}
|
|
378
|
+
diagnostics.push({
|
|
379
|
+
source: view.getUint8(off),
|
|
380
|
+
severity: view.getUint8(off + 1),
|
|
381
|
+
code: view.getUint16(off + 2, true),
|
|
382
|
+
span: { start: view.getUint32(off + 4, true), end: view.getUint32(off + 8, true) },
|
|
383
|
+
message: nameAt(bytes, off + DIAG_RECORD_LEN, msgLen),
|
|
384
|
+
});
|
|
385
|
+
off += DIAG_RECORD_LEN + msgLen;
|
|
386
|
+
}
|
|
387
|
+
return { count, diagnostics };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The `'ANME'` emitted-events blob decoded, `anm_machine_emits()`'s wire
|
|
392
|
+
* (src/Emits.zig): `{ count, emits }` with each emit `{ name, at }`, the
|
|
393
|
+
* name borrowed from the machine's table and `at` the entry instant in
|
|
394
|
+
* microseconds. Throws on any defect.
|
|
395
|
+
* @param {BufferSource} blob
|
|
396
|
+
* @returns {{ count: number, emits: Array<{ name: string, at: number }> }}
|
|
397
|
+
*/
|
|
398
|
+
export function decodeEmits(blob) {
|
|
399
|
+
const bytes = toBytes(blob);
|
|
400
|
+
if (bytes.byteLength < EMITS_HEADER_LEN) {
|
|
401
|
+
throw new MachineError(`animader: 'ANME' blob is ${bytes.byteLength} bytes, under the header`);
|
|
402
|
+
}
|
|
403
|
+
const view = viewOf(bytes);
|
|
404
|
+
if (view.getUint32(0, true) !== MAGIC_ANME) throw new MachineError("animader: 'ANME' magic is missing");
|
|
405
|
+
if (view.getUint32(4, true) !== EMITS_VERSION) {
|
|
406
|
+
throw new MachineError(`animader: 'ANME' version ${view.getUint32(4, true)} is not ${EMITS_VERSION}`);
|
|
407
|
+
}
|
|
408
|
+
const count = view.getUint32(8, true);
|
|
409
|
+
const strtabLen = view.getUint32(12, true);
|
|
410
|
+
const strtabOff = EMITS_HEADER_LEN + count * EMIT_RECORD_LEN;
|
|
411
|
+
if (strtabOff + strtabLen !== bytes.byteLength) {
|
|
412
|
+
throw new MachineError("animader: 'ANME' length does not match its header");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const strings = [];
|
|
416
|
+
let pos = strtabOff;
|
|
417
|
+
while (pos < bytes.byteLength) {
|
|
418
|
+
const end = bytes.indexOf(0, pos);
|
|
419
|
+
if (end === -1) {
|
|
420
|
+
strings.push(decoder.decode(bytes.subarray(pos)));
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
strings.push(decoder.decode(bytes.subarray(pos, end)));
|
|
424
|
+
pos = end + 1;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const emits = [];
|
|
428
|
+
for (let i = 0; i < count; i++) {
|
|
429
|
+
const off = EMITS_HEADER_LEN + i * EMIT_RECORD_LEN;
|
|
430
|
+
const raw = view.getUint16(off + 8, true);
|
|
431
|
+
if (raw >= strings.length) {
|
|
432
|
+
throw new MachineError(`animader: 'ANME' record name index ${raw} has no table entry`);
|
|
433
|
+
}
|
|
434
|
+
emits.push({ name: strings[raw], at: Number(view.getBigInt64(off, true)) });
|
|
435
|
+
}
|
|
436
|
+
return { count, emits };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const IMPORTS = { env: { sjon_host_invoke_plugin: () => 0 } };
|
|
440
|
+
|
|
441
|
+
/** The loaded machine boundary and the JS-side blob decoders. */
|
|
442
|
+
export class Machine {
|
|
443
|
+
#e;
|
|
444
|
+
#doc;
|
|
445
|
+
#meta;
|
|
446
|
+
#outputsWire = null;
|
|
447
|
+
#emits = [];
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Instantiate the artifact synchronously (node, tests) and load a
|
|
451
|
+
* document. `bytes` is the built animader.wasm, e.g. from
|
|
452
|
+
* `readFileSync`. Throws `MachineError` on a diagnosed document.
|
|
453
|
+
* @param {BufferSource} bytes
|
|
454
|
+
* @param {string} text
|
|
455
|
+
* @returns {Machine}
|
|
456
|
+
*/
|
|
457
|
+
static loadSync(bytes, text) {
|
|
458
|
+
return new Machine(new WebAssembly.Instance(new WebAssembly.Module(bytes), IMPORTS), text);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Instantiate the artifact from bytes or a compiled module (browser) and
|
|
463
|
+
* load a document.
|
|
464
|
+
* @param {BufferSource | WebAssembly.Module} source
|
|
465
|
+
* @param {string} text
|
|
466
|
+
* @returns {Promise<Machine>}
|
|
467
|
+
*/
|
|
468
|
+
static async load(source, text) {
|
|
469
|
+
const instance =
|
|
470
|
+
source instanceof WebAssembly.Module
|
|
471
|
+
? await WebAssembly.instantiate(source, IMPORTS)
|
|
472
|
+
: (await WebAssembly.instantiate(source, IMPORTS)).instance;
|
|
473
|
+
return new Machine(instance, text);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** @param {WebAssembly.Instance} instance @param {string} text */
|
|
477
|
+
constructor(instance, text) {
|
|
478
|
+
this.#e = instance.exports;
|
|
479
|
+
if (typeof this.#e.animader_abiVersion !== "function" || this.#e.animader_abiVersion() !== 1) {
|
|
480
|
+
throw new MachineError("animader: unsupported or missing ABI version 1");
|
|
481
|
+
}
|
|
482
|
+
for (const name of [
|
|
483
|
+
"anm_log_inPtr",
|
|
484
|
+
"anm_log_outPtr",
|
|
485
|
+
"anm_log_outLen",
|
|
486
|
+
"anm_machine_load",
|
|
487
|
+
"anm_machine_step",
|
|
488
|
+
"anm_machine_snapshot",
|
|
489
|
+
"anm_machine_restore",
|
|
490
|
+
"anm_machine_setInput",
|
|
491
|
+
"anm_machine_fireTrigger",
|
|
492
|
+
"anm_machine_meta",
|
|
493
|
+
"anm_machine_diag",
|
|
494
|
+
"anm_machine_emits",
|
|
495
|
+
"anm_machine_eject",
|
|
496
|
+
"memory",
|
|
497
|
+
]) {
|
|
498
|
+
if (this.#e[name] === undefined) throw new MachineError(`animader: missing export ${name}`);
|
|
499
|
+
}
|
|
500
|
+
this.#doc = text;
|
|
501
|
+
this.#load(text);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** The boundary ABI version (1). */
|
|
505
|
+
get abiVersion() {
|
|
506
|
+
return this.#e.animader_abiVersion();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
#writeInput(bytes) {
|
|
510
|
+
const e = this.#e;
|
|
511
|
+
new Uint8Array(e.memory.buffer, e.anm_log_inPtr(), bytes.byteLength).set(bytes);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
#output(len) {
|
|
515
|
+
return new Uint8Array(this.#e.memory.buffer, this.#e.anm_log_outPtr(), len).slice();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
#check(status, what) {
|
|
519
|
+
if (status < 0) {
|
|
520
|
+
throw new MachineError(`animader: ${what}: ${describeStatus(status)} (status ${status})`, { status });
|
|
521
|
+
}
|
|
522
|
+
return status;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#load(text) {
|
|
526
|
+
const bytes = encoder.encode(text);
|
|
527
|
+
this.#writeInput(bytes);
|
|
528
|
+
const status = this.#e.anm_machine_load(bytes.byteLength);
|
|
529
|
+
if (status === STATUS_DIAGNOSTICS) {
|
|
530
|
+
const blob = this.#output(this.#e.anm_machine_diag());
|
|
531
|
+
const diagnostics = decodeDiags(blob).diagnostics;
|
|
532
|
+
throw new MachineError(
|
|
533
|
+
`animader: the machine document has diagnostics (${diagnostics.length} record(s))`,
|
|
534
|
+
{ status, diagnostics },
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
this.#check(status, "load");
|
|
538
|
+
this.#meta = decodeMeta(this.#output(this.#e.anm_machine_meta()));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Apply an 'ANEV' events blob (or null for an empty batch) and advance
|
|
543
|
+
* to `now` (session microseconds). The outputs at `now` are cached for
|
|
544
|
+
* `outputs()` and the emitted events for `emits()`.
|
|
545
|
+
* @param {Uint8Array | ArrayBuffer | null} events
|
|
546
|
+
* @param {number} now
|
|
547
|
+
* @returns {Machine}
|
|
548
|
+
*/
|
|
549
|
+
step(events, now) {
|
|
550
|
+
const at = micros(now, "now");
|
|
551
|
+
let bytes = null;
|
|
552
|
+
if (events !== null && events !== undefined) {
|
|
553
|
+
bytes = toBytes(events);
|
|
554
|
+
this.#writeInput(bytes);
|
|
555
|
+
}
|
|
556
|
+
// A failed step leaves no emits (the boundary's own rule), so the
|
|
557
|
+
// cache is cleared before the call, never after a throw.
|
|
558
|
+
this.#emits = [];
|
|
559
|
+
const status = this.#e.anm_machine_step(bytes === null ? 0 : bytes.byteLength, BigInt(at));
|
|
560
|
+
this.#check(status, "step");
|
|
561
|
+
this.#outputsWire = this.#output(status);
|
|
562
|
+
this.#syncEmits();
|
|
563
|
+
return this;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* The last stepping call's emitted events, in order: `{ name, at }` per
|
|
568
|
+
* `(emit ...)` of every state the call entered, `at` the entry instant
|
|
569
|
+
* in microseconds (not necessarily the step's `now`). A clean load
|
|
570
|
+
* reports the empty list until a step runs.
|
|
571
|
+
* @returns {Array<{ name: string, at: number }>}
|
|
572
|
+
*/
|
|
573
|
+
emits() {
|
|
574
|
+
return this.#emits.slice();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* The outputs evaluated at the last `step`'s time, in declaration order:
|
|
579
|
+
* `{ name, index, arity, value: [4 numbers] }`. Throws when no step has
|
|
580
|
+
* run.
|
|
581
|
+
* @returns {Array<{ name: string, index: number, arity: number, value: number[] }>}
|
|
582
|
+
*/
|
|
583
|
+
outputs() {
|
|
584
|
+
if (this.#outputsWire === null) {
|
|
585
|
+
throw new MachineError("animader: outputs are evaluated by step; call step first", {
|
|
586
|
+
status: STATUS_BAD_CALL,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
return decodeOutputs(this.#outputsWire).map((o) => {
|
|
590
|
+
const decl = this.#meta.outputs[o.index];
|
|
591
|
+
return { name: decl === undefined ? null : decl.name, index: o.index, arity: o.arity, value: o.value };
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** The raw outputs wire of the last `step`, or null before one. */
|
|
596
|
+
outputsWire() {
|
|
597
|
+
return this.#outputsWire === null ? null : this.#outputsWire.slice();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* The current `'ANMS'` snapshot.
|
|
602
|
+
* @returns {Uint8Array}
|
|
603
|
+
*/
|
|
604
|
+
snapshot() {
|
|
605
|
+
const status = this.#e.anm_machine_snapshot();
|
|
606
|
+
this.#check(status, "snapshot");
|
|
607
|
+
return this.#output(status);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Replace the cached snapshot from an `'ANMS'` blob. A well-formed blob
|
|
612
|
+
* from a different machine throws with status -6 and the
|
|
613
|
+
* `snapshot_mismatch` code.
|
|
614
|
+
* @param {Uint8Array} blob
|
|
615
|
+
* @returns {Machine}
|
|
616
|
+
*/
|
|
617
|
+
restore(blob) {
|
|
618
|
+
const bytes = toBytes(blob);
|
|
619
|
+
this.#writeInput(bytes);
|
|
620
|
+
const status = this.#e.anm_machine_restore(bytes.byteLength);
|
|
621
|
+
if (status === STATUS_SNAPSHOT_MISMATCH) {
|
|
622
|
+
throw new MachineError("animader: the snapshot belongs to a different machine (status -6)", {
|
|
623
|
+
status,
|
|
624
|
+
code: MACHINE_CODES.snapshot_mismatch,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
this.#check(status, "restore");
|
|
628
|
+
return this;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Apply one `(input-write ...)` record for `name` at the snapshot's current
|
|
633
|
+
* time, exactly as `step` would. `value` is lane 0 (i32): a number
|
|
634
|
+
* input takes it, a bool takes truthiness, a trigger is armed.
|
|
635
|
+
* @param {string} name
|
|
636
|
+
* @param {number} [value]
|
|
637
|
+
* @returns {Machine}
|
|
638
|
+
*/
|
|
639
|
+
setInput(name, value = 1) {
|
|
640
|
+
return this.#input(name, value, "setInput");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Arm the named input for the current instant: `setInput(name, 1)` with
|
|
645
|
+
* the trigger's own name.
|
|
646
|
+
* @param {string} name
|
|
647
|
+
* @returns {Machine}
|
|
648
|
+
*/
|
|
649
|
+
fireTrigger(name) {
|
|
650
|
+
return this.#input(name, 1, "fireTrigger");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
#input(name, value, what) {
|
|
654
|
+
if (!Number.isInteger(value) || value < -2147483648 || value > 2147483647) {
|
|
655
|
+
throw new MachineError(`animader: ${what} value must be an i32, got ${value}`, {
|
|
656
|
+
status: STATUS_BAD_CALL,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
const bytes = encoder.encode(name);
|
|
660
|
+
this.#writeInput(bytes);
|
|
661
|
+
this.#emits = [];
|
|
662
|
+
const status =
|
|
663
|
+
what === "setInput"
|
|
664
|
+
? this.#e.anm_machine_setInput(bytes.byteLength, value)
|
|
665
|
+
: this.#e.anm_machine_fireTrigger(bytes.byteLength);
|
|
666
|
+
this.#check(status, what);
|
|
667
|
+
this.#syncEmits();
|
|
668
|
+
return this;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Read and decode the boundary's current 'ANME' blob. */
|
|
672
|
+
#syncEmits() {
|
|
673
|
+
const status = this.#e.anm_machine_emits();
|
|
674
|
+
this.#check(status, "emits");
|
|
675
|
+
this.#emits = decodeEmits(this.#output(status)).emits;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* The loaded machine's `'ANMM'` listing.
|
|
680
|
+
* @returns {Uint8Array}
|
|
681
|
+
*/
|
|
682
|
+
meta() {
|
|
683
|
+
const status = this.#e.anm_machine_meta();
|
|
684
|
+
this.#check(status, "meta");
|
|
685
|
+
return this.#output(status);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* The last load's `'ANMD'` diagnostics: a zero-count blob for a clean
|
|
690
|
+
* document.
|
|
691
|
+
* @returns {Uint8Array}
|
|
692
|
+
*/
|
|
693
|
+
diag() {
|
|
694
|
+
const status = this.#e.anm_machine_diag();
|
|
695
|
+
this.#check(status, "diag");
|
|
696
|
+
return this.#output(status);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* The document with the verb at `verbIndex` replaced by its emitted
|
|
701
|
+
* core forms, the verb kept above them as a comment.
|
|
702
|
+
* @param {number} verbIndex
|
|
703
|
+
* @returns {string}
|
|
704
|
+
*/
|
|
705
|
+
eject(verbIndex) {
|
|
706
|
+
const bytes = encoder.encode(this.#doc);
|
|
707
|
+
this.#writeInput(bytes);
|
|
708
|
+
const status = this.#e.anm_machine_eject(bytes.byteLength, verbIndex);
|
|
709
|
+
this.#check(status, "eject");
|
|
710
|
+
return decoder.decode(this.#output(status));
|
|
711
|
+
}
|
|
712
|
+
}
|