@factoidal/core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +122 -0
- package/NOTICE +31 -0
- package/README.md +131 -1
- package/bin/engine.mjs +124 -0
- package/bin/factoidal.mjs +894 -0
- package/bin/pack-host.mjs +341 -0
- package/bin/pack-worker.mjs +31 -0
- package/bin/pack.mjs +174 -0
- package/bin/store.mjs +238 -0
- package/l4-assets/l4factoidal.js +125 -5
- package/l4-assets/l4factoidal.mjs +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/package.json +4 -0
- package/l4-assets/version.json +5 -5
- package/l4.d.ts +14 -0
- package/l4.js +15 -1
- package/package.json +17 -2
- package/sample-store/CURRENT +1 -0
- package/sample-store/gen-1/manifest.sbm2 +0 -0
- package/sample-store/gen-1/manifest.tsv +14 -0
- package/sample-store/gen-1/predicate-0.ibk3 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-0.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-0.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-0.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.oli2.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-1.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-10.ibk3 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-10.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.oli2.merkle +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-10.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-11.ibk3 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-11.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-11.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-12.ibk3 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-12.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-12.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-12.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-2.ibk3 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-2.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-2.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.sri2.merkle +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-3.ibk3 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-3.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-3.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-3.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-4.ibk3 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-4.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-4.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.tli1.merkle +2 -0
- package/sample-store/gen-1/predicate-5.ibk3 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-5.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-5.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-5.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-6.ibk3 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-6.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-6.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-6.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-7.ibk3 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.oli2.merkle +2 -0
- package/sample-store/gen-1/predicate-7.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-7.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-8.ibk3 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-8.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-8.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-8.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-9.ibk3 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-9.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-9.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.sri2.merkle +3 -0
- package/sample-store/gen-1/predicate-9.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.tli1.merkle +2 -0
- package/sample-store.d.ts +16 -0
- package/sample-store.mjs +37 -0
- package/store-host/deno.mjs +262 -0
- package/store-host/errors.mjs +55 -0
- package/store-host/index.mjs +247 -0
- package/store-host/node.mjs +273 -0
- package/store-host/paths.mjs +77 -0
- package/version.json +22 -21
package/bin/store.mjs
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// Driving the three WebAssembly store operations from a JavaScript host.
|
|
2
|
+
// https://github.com/danbri/factoidal/issues/641
|
|
3
|
+
//
|
|
4
|
+
// WHAT THIS FILE IS ALLOWED TO DO
|
|
5
|
+
// Read CURRENT, read a manifest file by name, read the artifact files the
|
|
6
|
+
// engine's plan named, concatenate their bytes, and hand them over. It
|
|
7
|
+
// never parses a manifest, never verifies a digest, never decodes a block
|
|
8
|
+
// and never decides which artifact answers a query. Every one of those is
|
|
9
|
+
// a format decision and it lives in `formal/lean4/Wasm/Ops/Store.lean`
|
|
10
|
+
// (iron rule 7 of CLAUDE.md). A reviewer who finds a magic number, a
|
|
11
|
+
// field offset or a hash in this file has found a rule violation.
|
|
12
|
+
//
|
|
13
|
+
// The operations and their envelopes are described in
|
|
14
|
+
// `docs/designissues/2026-09-03-wasm-shardborough-store-ops.md`.
|
|
15
|
+
|
|
16
|
+
import { openCollection, readWhole } from '../store-host/index.mjs'
|
|
17
|
+
import { joinPath } from '../store-host/paths.mjs'
|
|
18
|
+
import { hexOfBytes } from './engine.mjs'
|
|
19
|
+
|
|
20
|
+
/** The manifest file names a generation directory can carry, in the order
|
|
21
|
+
* `Harness.ShardMerklePread.readManifest` tries them. */
|
|
22
|
+
const MANIFEST_NAMES = ['manifest.sbm2', 'manifest.sbm1']
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An error the store operations reported. `capName` is set when the
|
|
26
|
+
* refusal was one of the operation's caps; `message` is always the
|
|
27
|
+
* engine's own words.
|
|
28
|
+
*/
|
|
29
|
+
export class StoreOperationError extends Error {
|
|
30
|
+
constructor (message, detail = {}) {
|
|
31
|
+
super(message)
|
|
32
|
+
this.name = 'StoreOperationError'
|
|
33
|
+
this.capValue = detail.capValue === undefined ? null : detail.capValue
|
|
34
|
+
this.capLimit = detail.capLimit === undefined ? null : detail.capLimit
|
|
35
|
+
this.digestKey = detail.digestKey === undefined ? null : detail.digestKey
|
|
36
|
+
this.stackLimit = detail.stackLimit === true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The engine's refusals arrive as an Error whose message is
|
|
41
|
+
// "l4factoidal: <the operation's own text>". These two patterns say only
|
|
42
|
+
// WHICH refusal it was, so the command can add a next step; the text the
|
|
43
|
+
// user sees is always the engine's, never a rewrite of it.
|
|
44
|
+
const CAP_PATTERN = /the plan selects (\d+) (?:artifacts|artifact bytes|rows), the cap is (\d+)/
|
|
45
|
+
const DIGEST_PATTERN = /artifact '([^']*)' does not match the SHA-256/
|
|
46
|
+
|
|
47
|
+
function asStoreError (error) {
|
|
48
|
+
const raw = error && error.message ? String(error.message) : String(error)
|
|
49
|
+
const message = raw.replace(/^l4factoidal:\s*/, '')
|
|
50
|
+
// Not a refusal by the engine: the host runtime ran out of call stack
|
|
51
|
+
// inside the wasm module. Some evaluator paths recurse once per row.
|
|
52
|
+
// Measured 2026-09-03 against the committed wasm, on a 6455-row
|
|
53
|
+
// generation: `SELECT ?s ?p ?o WHERE { ?s ?p ?o }` and the same query
|
|
54
|
+
// with `ORDER BY` overflow under Node's default WebAssembly frame
|
|
55
|
+
// budget, while `SELECT *`, and either query with a LIMIT, do not.
|
|
56
|
+
// `node --stack-size=4000` clears all of them, and Deno clears them at
|
|
57
|
+
// its own default.
|
|
58
|
+
if (error instanceof RangeError || message.indexOf('call stack size exceeded') >= 0) {
|
|
59
|
+
return new StoreOperationError(message, { stackLimit: true })
|
|
60
|
+
}
|
|
61
|
+
const cap = CAP_PATTERN.exec(message)
|
|
62
|
+
if (cap !== null) {
|
|
63
|
+
return new StoreOperationError(message,
|
|
64
|
+
{ capValue: Number(cap[1]), capLimit: Number(cap[2]) })
|
|
65
|
+
}
|
|
66
|
+
const digest = DIGEST_PATTERN.exec(message)
|
|
67
|
+
if (digest !== null) {
|
|
68
|
+
return new StoreOperationError(message, { digestKey: digest[1] })
|
|
69
|
+
}
|
|
70
|
+
return new StoreOperationError(message)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* What to print when the runtime, not the engine, ran out of call stack.
|
|
75
|
+
*
|
|
76
|
+
* One copy of this text, used by every command that can hit the frame
|
|
77
|
+
* budget. `remedy` is the one line that differs: a query can be made
|
|
78
|
+
* smaller with a LIMIT, a pack cannot.
|
|
79
|
+
*
|
|
80
|
+
* The pack path normally never reaches this, because it runs on a worker
|
|
81
|
+
* thread with a raised stack (`bin/pack-host.mjs`,
|
|
82
|
+
* https://github.com/danbri/factoidal/issues/649). It is what a reader
|
|
83
|
+
* sees when the worker route is refused with --no-worker, is unavailable
|
|
84
|
+
* on their platform, or is not enough.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} remedy
|
|
87
|
+
* @returns {string[]} the lines, in order, for stderr
|
|
88
|
+
*/
|
|
89
|
+
export function stackLimitAdvice (remedy) {
|
|
90
|
+
return [
|
|
91
|
+
'The runtime ran out of call stack inside the engine, not the store.',
|
|
92
|
+
'Some engine paths recurse once per row or per input chunk, and enough',
|
|
93
|
+
"of them exceed the runtime's default WebAssembly frame budget.",
|
|
94
|
+
remedy
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The remedies for each command that can run out of frames. */
|
|
99
|
+
export const STACK_REMEDY = {
|
|
100
|
+
query: 'Raise it with node --stack-size=4000, add a LIMIT, or use Deno.',
|
|
101
|
+
pack: 'Raise it with node --stack-size=8000, or ' +
|
|
102
|
+
'deno run --v8-flags=--stack-size=8000.'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Open a store and return its manifest bytes.
|
|
107
|
+
*
|
|
108
|
+
* With no `generationName` the activated generation is opened through
|
|
109
|
+
* CURRENT. With one, that generation is opened directly, which is how a
|
|
110
|
+
* generation that has not been activated is inspected.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} root the collection root that holds CURRENT
|
|
113
|
+
* @param {string|null} generationName
|
|
114
|
+
* @returns {{root: string, generation: string, generationDir: string,
|
|
115
|
+
* manifestName: string, manifest: Uint8Array,
|
|
116
|
+
* manifestHex: string, activated: boolean}}
|
|
117
|
+
*/
|
|
118
|
+
export function openStore (root, generationName = null) {
|
|
119
|
+
if (typeof generationName !== 'string') {
|
|
120
|
+
const opened = openCollection(root)
|
|
121
|
+
return { ...opened, manifestHex: hexOfBytes(opened.manifest), activated: true }
|
|
122
|
+
}
|
|
123
|
+
const generationDir = joinPath(root, generationName)
|
|
124
|
+
for (const manifestName of MANIFEST_NAMES) {
|
|
125
|
+
let manifest
|
|
126
|
+
try {
|
|
127
|
+
manifest = readWhole(joinPath(generationDir, manifestName))
|
|
128
|
+
} catch (_error) {
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
root,
|
|
133
|
+
generation: generationName,
|
|
134
|
+
generationDir,
|
|
135
|
+
manifestName,
|
|
136
|
+
manifest,
|
|
137
|
+
manifestHex: hexOfBytes(manifest),
|
|
138
|
+
activated: false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw new StoreOperationError(
|
|
142
|
+
`${generationDir} has none of ${MANIFEST_NAMES.join(', ')}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** `storeManifestInspect` — decode one manifest. */
|
|
146
|
+
export function inspectManifest (engine, store) {
|
|
147
|
+
try {
|
|
148
|
+
return engine.call('storeManifestInspect', [store.manifestHex])
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw asStoreError(error)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** `storeQueryPlan` — the artifact keys this query needs, and the mode. */
|
|
155
|
+
export function planQuery (engine, store, sparql) {
|
|
156
|
+
try {
|
|
157
|
+
return engine.call('storeQueryPlan', [store.manifestHex, sparql])
|
|
158
|
+
} catch (error) {
|
|
159
|
+
throw asStoreError(error)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Ask the engine to decide the caps before any file is read.
|
|
165
|
+
*
|
|
166
|
+
* `storeQuery` checks its three caps against the manifest's own
|
|
167
|
+
* declarations BEFORE it looks at the artifact descriptors, so a call
|
|
168
|
+
* carrying an empty descriptor list gets the cap decision without moving
|
|
169
|
+
* a byte. The caps themselves stay where they are defined
|
|
170
|
+
* (`Wasm/Ops/Store.lean`); this host holds none of their values.
|
|
171
|
+
*
|
|
172
|
+
* @returns the envelope when the plan needs no artifact at all, else null
|
|
173
|
+
* @throws {StoreOperationError} when a cap refused the plan
|
|
174
|
+
*/
|
|
175
|
+
function capDecision (engine, store, sparql) {
|
|
176
|
+
try {
|
|
177
|
+
return engine.callBlob('storeQuery',
|
|
178
|
+
[store.manifestHex, sparql, '[]'], new Uint8Array(0))
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const refusal = asStoreError(error)
|
|
181
|
+
if (refusal.message.indexOf('no bytes were supplied for artifact') >= 0) {
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
throw refusal
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Evaluate one SPARQL query against one generation.
|
|
190
|
+
*
|
|
191
|
+
* The sequence is: plan, cap decision, read exactly the artifacts the
|
|
192
|
+
* plan named, concatenate them into one buffer, and call `storeQuery`
|
|
193
|
+
* with a `{"key","offset","len"}` window per artifact. The buffer is
|
|
194
|
+
* written straight into the wasm heap by `engine.callBlob` with no
|
|
195
|
+
* encoding, and the engine bounds-checks every window.
|
|
196
|
+
*
|
|
197
|
+
* @returns {{plan: object, result: object, blobBytes: number,
|
|
198
|
+
* artifacts: {key: string, offset: number, len: number}[]}}
|
|
199
|
+
*/
|
|
200
|
+
export function queryStore (engine, store, sparql) {
|
|
201
|
+
const plan = planQuery(engine, store, sparql)
|
|
202
|
+
const empty = capDecision(engine, store, sparql)
|
|
203
|
+
if (empty !== null) {
|
|
204
|
+
return { plan, result: empty, blobBytes: 0, artifacts: [] }
|
|
205
|
+
}
|
|
206
|
+
const chunks = plan.keys.map((key) => readWhole(joinPath(store.generationDir, key)))
|
|
207
|
+
let total = 0
|
|
208
|
+
for (const chunk of chunks) total += chunk.length
|
|
209
|
+
const blob = new Uint8Array(total)
|
|
210
|
+
const artifacts = []
|
|
211
|
+
let offset = 0
|
|
212
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
213
|
+
blob.set(chunks[index], offset)
|
|
214
|
+
artifacts.push({ key: plan.keys[index], offset, len: chunks[index].length })
|
|
215
|
+
offset += chunks[index].length
|
|
216
|
+
}
|
|
217
|
+
let result
|
|
218
|
+
try {
|
|
219
|
+
result = engine.callBlob('storeQuery',
|
|
220
|
+
[store.manifestHex, sparql, JSON.stringify(artifacts)], blob)
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw asStoreError(error)
|
|
223
|
+
}
|
|
224
|
+
return { plan, result, blobBytes: total, artifacts }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* `serializeTurtle` — the engine's own Turtle writer, over the N-Quads a
|
|
229
|
+
* CONSTRUCT answered. Named graphs are flattened into the default graph
|
|
230
|
+
* on this path; `--format nquads` is the fidelity-preserving one.
|
|
231
|
+
*/
|
|
232
|
+
export function turtleOfNQuads (engine, nquads) {
|
|
233
|
+
try {
|
|
234
|
+
return engine.call('serializeTurtle', [nquads]).turtle
|
|
235
|
+
} catch (error) {
|
|
236
|
+
throw asStoreError(error)
|
|
237
|
+
}
|
|
238
|
+
}
|
package/l4-assets/l4factoidal.js
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
// bytes change.
|
|
38
38
|
|
|
39
39
|
// Stamped by formal/lean4/Wasm/build-wasm.sh step 9 -- do not hand-edit.
|
|
40
|
-
const WASM_VERSION = "
|
|
40
|
+
const WASM_VERSION = "125d391e0ccc";
|
|
41
41
|
|
|
42
42
|
import createModule from './l4factoidal.mjs';
|
|
43
43
|
|
|
@@ -72,8 +72,6 @@ export function loadL4() {
|
|
|
72
72
|
const Module = await createModule(moduleArg);
|
|
73
73
|
|
|
74
74
|
const cVersion = Module.cwrap('l4_version_c', 'number', []);
|
|
75
|
-
const cBgpQuery = Module.cwrap('l4_bgp_query_c', 'number', ['string', 'string']);
|
|
76
|
-
const cCall = Module.cwrap('l4_call_c', 'number', ['string', 'string']);
|
|
77
75
|
const cFree = Module.cwrap('l4_free_result', null, ['number']);
|
|
78
76
|
const cInit = Module.cwrap('l4_init', 'number', []);
|
|
79
77
|
|
|
@@ -89,6 +87,27 @@ export function loadL4() {
|
|
|
89
87
|
|
|
90
88
|
const asJson = (v) => (typeof v === 'string' ? v : JSON.stringify(v));
|
|
91
89
|
|
|
90
|
+
// Emscripten's cwrap `string` converter uses the WebAssembly stack.
|
|
91
|
+
// Multi-megabyte RDF/block requests can therefore overflow STACK_SIZE
|
|
92
|
+
// before Lean sees them. Allocate input UTF-8 on the wasm heap instead;
|
|
93
|
+
// the C shim copies each input into a Lean String synchronously, so these
|
|
94
|
+
// buffers can be released as soon as the exported call returns.
|
|
95
|
+
const callWithHeapStrings = (fn, texts) => {
|
|
96
|
+
const pointers = [];
|
|
97
|
+
try {
|
|
98
|
+
for (const text of texts) {
|
|
99
|
+
const size = Module.lengthBytesUTF8(text) + 1;
|
|
100
|
+
const ptr = Module._malloc(size);
|
|
101
|
+
if (!ptr) throw new Error('l4factoidal: could not allocate a WASM input buffer');
|
|
102
|
+
Module.stringToUTF8(text, ptr, size);
|
|
103
|
+
pointers.push(ptr);
|
|
104
|
+
}
|
|
105
|
+
return fn(...pointers);
|
|
106
|
+
} finally {
|
|
107
|
+
for (let i = pointers.length - 1; i >= 0; i--) Module._free(pointers[i]);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
92
111
|
return {
|
|
93
112
|
/** The Lean-side ABI version string. */
|
|
94
113
|
version() { return take(cVersion()); },
|
|
@@ -105,7 +124,9 @@ export function loadL4() {
|
|
|
105
124
|
* @throws if the Lean side reports a decoding error
|
|
106
125
|
*/
|
|
107
126
|
bgpQuery(data, bgp) {
|
|
108
|
-
const
|
|
127
|
+
const resultPtr = callWithHeapStrings(Module._l4_bgp_query_c,
|
|
128
|
+
[asJson(data), asJson(bgp)]);
|
|
129
|
+
const parsed = JSON.parse(take(resultPtr));
|
|
109
130
|
if (parsed.error) throw new Error(`l4factoidal: ${parsed.error}`);
|
|
110
131
|
return parsed;
|
|
111
132
|
},
|
|
@@ -122,11 +143,110 @@ export function loadL4() {
|
|
|
122
143
|
* @throws if the Lean side reports {"ok":false,"error":...}
|
|
123
144
|
*/
|
|
124
145
|
call(op, args) {
|
|
125
|
-
const
|
|
146
|
+
const resultPtr = callWithHeapStrings(Module._l4_call_c,
|
|
147
|
+
[op, JSON.stringify(args)]);
|
|
148
|
+
const parsed = JSON.parse(take(resultPtr));
|
|
126
149
|
if (parsed.ok === false) throw new Error(`l4factoidal: ${parsed.error}`);
|
|
127
150
|
return parsed;
|
|
128
151
|
},
|
|
129
152
|
|
|
153
|
+
/**
|
|
154
|
+
* The dispatch ABI, plus ONE contiguous byte region.
|
|
155
|
+
*
|
|
156
|
+
* For ops whose input is block bytes rather than text
|
|
157
|
+
* (`storeQuery`; the `ops` reflection lists them under
|
|
158
|
+
* `blobOps`). The bytes are written straight into the wasm heap
|
|
159
|
+
* with no encoding — no hex, no base64 — and copied once into a
|
|
160
|
+
* Lean ByteArray on the Lean side. Which bytes belong to which
|
|
161
|
+
* artifact is said in `args`, as {"key","offset","len"} windows
|
|
162
|
+
* into the region; Lean bounds-checks every one of them, so this
|
|
163
|
+
* call cannot pass a stale or out-of-range pointer.
|
|
164
|
+
*
|
|
165
|
+
* @param op the method name, e.g. "storeQuery"
|
|
166
|
+
* @param args array of positional STRING arguments
|
|
167
|
+
* @param blob Uint8Array (or ArrayBuffer) of the concatenated bytes
|
|
168
|
+
* @returns the parsed {"ok":true,...} envelope
|
|
169
|
+
* @throws if the Lean side reports {"ok":false,"error":...}
|
|
170
|
+
*/
|
|
171
|
+
callBlob(op, args, blob) {
|
|
172
|
+
const bytes = blob instanceof Uint8Array ? blob : new Uint8Array(blob ?? 0);
|
|
173
|
+
const blobPtr = bytes.length > 0 ? Module._malloc(bytes.length) : 0;
|
|
174
|
+
if (bytes.length > 0 && !blobPtr) {
|
|
175
|
+
throw new Error('l4factoidal: could not allocate a WASM blob buffer');
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
if (bytes.length > 0) Module.HEAPU8.set(bytes, blobPtr);
|
|
179
|
+
const resultPtr = callWithHeapStrings(
|
|
180
|
+
(opPtr, argsPtr) => Module._l4_call_blob_c(opPtr, argsPtr, blobPtr, bytes.length),
|
|
181
|
+
[op, JSON.stringify(args)]);
|
|
182
|
+
const parsed = JSON.parse(take(resultPtr));
|
|
183
|
+
if (parsed.ok === false) throw new Error(`l4factoidal: ${parsed.error}`);
|
|
184
|
+
return parsed;
|
|
185
|
+
} finally {
|
|
186
|
+
if (blobPtr) Module._free(blobPtr);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The dispatch ABI, plus ONE byte region IN and ONE byte region
|
|
192
|
+
* OUT.
|
|
193
|
+
*
|
|
194
|
+
* For the ops of `L4Wasm.blobIoOpNames` (the `ops` envelope lists
|
|
195
|
+
* them under `blobIoOps`), whose RESULT is bytes rather than
|
|
196
|
+
* text. The bytes leave the module raw — no hex, no base64 — and
|
|
197
|
+
* are copied out of the wasm heap into a fresh Uint8Array before
|
|
198
|
+
* the module's buffer is released. The copy is required: the heap
|
|
199
|
+
* is detached and replaced when the module grows, so a subarray
|
|
200
|
+
* view of it can go stale between calls.
|
|
201
|
+
*
|
|
202
|
+
* Every other op answers as `call` does, with an empty region.
|
|
203
|
+
*
|
|
204
|
+
* @param op the method name, e.g. "blobEcho"
|
|
205
|
+
* @param args array of positional STRING arguments
|
|
206
|
+
* @param blobIn Uint8Array (or ArrayBuffer) carried IN; may be omitted
|
|
207
|
+
* @returns { envelope, bytes } — the parsed {"ok":true,...}
|
|
208
|
+
* envelope and a Uint8Array of the out region
|
|
209
|
+
* @throws if the Lean side reports {"ok":false,"error":...}
|
|
210
|
+
*/
|
|
211
|
+
callBlobIO(op, args, blobIn) {
|
|
212
|
+
const bytes = blobIn instanceof Uint8Array
|
|
213
|
+
? blobIn
|
|
214
|
+
: new Uint8Array(blobIn ?? 0);
|
|
215
|
+
// Two 32-bit out parameters, uint8_t **out_ptr and size_t
|
|
216
|
+
// *out_len, in one 8-byte cell.
|
|
217
|
+
const outCell = Module._malloc(8);
|
|
218
|
+
if (!outCell) throw new Error('l4factoidal: could not allocate the out-parameter cell');
|
|
219
|
+
const blobPtr = bytes.length > 0 ? Module._malloc(bytes.length) : 0;
|
|
220
|
+
if (bytes.length > 0 && !blobPtr) {
|
|
221
|
+
Module._free(outCell);
|
|
222
|
+
throw new Error('l4factoidal: could not allocate a WASM blob buffer');
|
|
223
|
+
}
|
|
224
|
+
let outPtr = 0;
|
|
225
|
+
try {
|
|
226
|
+
Module.setValue(outCell, 0, 'i32');
|
|
227
|
+
Module.setValue(outCell + 4, 0, 'i32');
|
|
228
|
+
if (bytes.length > 0) Module.HEAPU8.set(bytes, blobPtr);
|
|
229
|
+
const resultPtr = callWithHeapStrings(
|
|
230
|
+
(opPtr, argsPtr) => Module._l4_call_blob_io_c(
|
|
231
|
+
opPtr, argsPtr, blobPtr, bytes.length, outCell, outCell + 4),
|
|
232
|
+
[op, JSON.stringify(args)]);
|
|
233
|
+
outPtr = Module.getValue(outCell, 'i32') >>> 0;
|
|
234
|
+
const outLen = Module.getValue(outCell + 4, 'i32') >>> 0;
|
|
235
|
+
const envelope = JSON.parse(take(resultPtr));
|
|
236
|
+
if (envelope.ok === false) throw new Error(`l4factoidal: ${envelope.error}`);
|
|
237
|
+
// slice() copies; HEAPU8 is replaced wholesale when the
|
|
238
|
+
// module's memory grows, so a view would not survive.
|
|
239
|
+
const region = outPtr !== 0 && outLen > 0
|
|
240
|
+
? Module.HEAPU8.slice(outPtr, outPtr + outLen)
|
|
241
|
+
: new Uint8Array(0);
|
|
242
|
+
return { envelope, bytes: region };
|
|
243
|
+
} finally {
|
|
244
|
+
if (outPtr) Module._l4_free_blob(outPtr);
|
|
245
|
+
if (blobPtr) Module._free(blobPtr);
|
|
246
|
+
Module._free(outCell);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
|
|
130
250
|
/** Escape hatch for tests: the raw Emscripten module. */
|
|
131
251
|
_module: Module,
|
|
132
252
|
};
|