@live-codes/odin-wasm 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +202 -2
- package/THIRD-PARTY-NOTICES.md +41 -0
- package/assets/odin-src.pack.gz +0 -0
- package/assets/odin.js +2302 -0
- package/assets/odin.wasm +0 -0
- package/bin/copy-assets.mjs +47 -0
- package/dist/odin-wasm.global.js +6 -0
- package/package.json +49 -4
- package/scripts/build-iife.mjs +33 -0
- package/scripts/check-package.mjs +43 -0
- package/scripts/sync-assets.mjs +63 -0
- package/src/api.js +140 -0
- package/src/asset-receipts.js +13 -0
- package/src/assets.js +149 -0
- package/src/engine.js +220 -0
- package/src/host-js.js +112 -0
- package/src/index.js +9 -0
- package/src/index.node.js +9 -0
- package/src/pack.js +40 -0
- package/src/packaged.node.js +13 -0
- package/src/runtime.js +40 -0
- package/src/wasi-host.js +564 -0
package/src/engine.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// The compile → run engine. It has no opinion about where the compiler module
|
|
2
|
+
// and the standard library came from; `assets.js` reads them, this runs them.
|
|
3
|
+
//
|
|
4
|
+
// The compiler is a WASI command module, so every compile is a fresh instance
|
|
5
|
+
// over a fresh in-memory file system: the standard library, the user's file,
|
|
6
|
+
// and an `out/` to write into. The module it emits then runs on one of two
|
|
7
|
+
// hosts, depending on the target it was built for.
|
|
8
|
+
|
|
9
|
+
import { WasiFileSystem, WasiExit, createWasi, runWasi } from './wasi-host.js';
|
|
10
|
+
import { createJsHost, JsExit } from './host-js.js';
|
|
11
|
+
|
|
12
|
+
// Where the compiler looks for base/, core/ and vendor/.
|
|
13
|
+
const ODIN_ROOT = '/odin';
|
|
14
|
+
const PROGRAM = '/out/main.wasm';
|
|
15
|
+
|
|
16
|
+
/** The targets the compiler can emit, and the page can run. */
|
|
17
|
+
export const TARGETS = ['wasi_wasm32', 'js_wasm32'];
|
|
18
|
+
|
|
19
|
+
const encoder = new TextEncoder();
|
|
20
|
+
|
|
21
|
+
const capture = () => {
|
|
22
|
+
const stdoutParts = [];
|
|
23
|
+
const stderrParts = [];
|
|
24
|
+
const order = [];
|
|
25
|
+
return {
|
|
26
|
+
stdout: (text) => {
|
|
27
|
+
stdoutParts.push(text);
|
|
28
|
+
order.push(text);
|
|
29
|
+
},
|
|
30
|
+
stderr: (text) => {
|
|
31
|
+
stderrParts.push(text);
|
|
32
|
+
order.push(text);
|
|
33
|
+
},
|
|
34
|
+
stdoutText: () => stdoutParts.join(''),
|
|
35
|
+
stderrText: () => stderrParts.join(''),
|
|
36
|
+
outputText: () => order.join(''),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** The lines a caller reads as diagnostics, with the empty ones dropped. */
|
|
41
|
+
const diagnosticLines = (text) =>
|
|
42
|
+
text
|
|
43
|
+
.split('\n')
|
|
44
|
+
.map((line) => line.trimEnd())
|
|
45
|
+
.filter((line) => line.trim() !== '');
|
|
46
|
+
|
|
47
|
+
/** Why a compile produced no module, as the lines a caller reads. */
|
|
48
|
+
const compileFailure = (compiled) =>
|
|
49
|
+
compiled.crash
|
|
50
|
+
? [compiled.crash]
|
|
51
|
+
: compiled.diagnostics.length > 0
|
|
52
|
+
? compiled.diagnostics
|
|
53
|
+
: [`The compiler produced no module (exit code ${compiled.exitCode}).`];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {object} assets
|
|
57
|
+
* @param {WebAssembly.Module} assets.compilerModule - the Odin compiler
|
|
58
|
+
* @param {[string, Uint8Array][]} assets.libraryEntries - the packed standard library
|
|
59
|
+
*/
|
|
60
|
+
export function createEngine({ compilerModule, libraryEntries }) {
|
|
61
|
+
/** A fresh file system with the standard library under /odin. */
|
|
62
|
+
const libraryFs = () => {
|
|
63
|
+
const fs = new WasiFileSystem();
|
|
64
|
+
for (const [path, data] of libraryEntries) fs.writeFile(`${ODIN_ROOT}/${path}`, data);
|
|
65
|
+
// The compiler registers its collections (base, core, vendor) from
|
|
66
|
+
// directories and requires all three to exist; the pack ships no vendor
|
|
67
|
+
// packages, so the directory is empty.
|
|
68
|
+
fs.mkdirAll(`${ODIN_ROOT}/vendor`);
|
|
69
|
+
return fs;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const compile = ({ code, target, compileArgs, fileName }) => {
|
|
73
|
+
const fs = libraryFs();
|
|
74
|
+
fs.writeFile(`/src/${fileName}`, encoder.encode(code));
|
|
75
|
+
fs.mkdirAll('/out');
|
|
76
|
+
|
|
77
|
+
const compilerOut = [];
|
|
78
|
+
const compilerErr = [];
|
|
79
|
+
const args = [
|
|
80
|
+
'odin',
|
|
81
|
+
'build',
|
|
82
|
+
'/src',
|
|
83
|
+
`-out:${PROGRAM}`,
|
|
84
|
+
'-backend:wasm',
|
|
85
|
+
`-target:${target}`,
|
|
86
|
+
...compileArgs,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
let exitCode = 1;
|
|
90
|
+
let crash = '';
|
|
91
|
+
try {
|
|
92
|
+
exitCode = runWasi(compilerModule, createWasi(fs, {
|
|
93
|
+
args,
|
|
94
|
+
env: [`ODIN_ROOT=${ODIN_ROOT}`],
|
|
95
|
+
stdout: (text) => compilerOut.push(text),
|
|
96
|
+
stderr: (text) => compilerErr.push(text),
|
|
97
|
+
}));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
crash = error?.message ?? String(error);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
exitCode,
|
|
104
|
+
artifact: exitCode === 0 ? fs.readFile(PROGRAM) : null,
|
|
105
|
+
diagnostics: diagnosticLines(compilerOut.join('') + compilerErr.join('')),
|
|
106
|
+
crash,
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Compile only, returning the module for the caller to run.
|
|
112
|
+
*
|
|
113
|
+
* A page that needs the DOM has to instantiate the program on the main thread — a worker has no
|
|
114
|
+
* `document`, and Odin's JS runtime is written for the page — so the caller takes the module
|
|
115
|
+
* instead of an output string.
|
|
116
|
+
*
|
|
117
|
+
* @returns {{ok: boolean, wasm: Uint8Array|null, errors: string[], compileMs: number}}
|
|
118
|
+
*/
|
|
119
|
+
const compileOnly = ({ code, target, compileArgs = [], fileName = 'main.odin' }) => {
|
|
120
|
+
const started = now();
|
|
121
|
+
const compiled = compile({ code, target, compileArgs: [...compileArgs], fileName });
|
|
122
|
+
const compileMs = Math.round(now() - started);
|
|
123
|
+
if (compiled.artifact === null || compiled.artifact.length === 0) {
|
|
124
|
+
return { ok: false, wasm: null, errors: compileFailure(compiled), compileMs };
|
|
125
|
+
}
|
|
126
|
+
return { ok: true, wasm: compiled.artifact, errors: [], compileMs };
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const runWasiTarget = (artifact, { input, args }) => {
|
|
130
|
+
const captured = capture();
|
|
131
|
+
let exitCode = 0;
|
|
132
|
+
try {
|
|
133
|
+
exitCode = runWasi(new WebAssembly.Module(artifact.slice()), createWasi(new WasiFileSystem(), {
|
|
134
|
+
args: ['main.wasm', ...args],
|
|
135
|
+
stdin: input,
|
|
136
|
+
stdout: captured.stdout,
|
|
137
|
+
stderr: captured.stderr,
|
|
138
|
+
}));
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error instanceof WasiExit) exitCode = error.code;
|
|
141
|
+
else {
|
|
142
|
+
captured.stderr(`The program trapped: ${error?.message ?? error}\n`);
|
|
143
|
+
exitCode = 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { exitCode, ...capturedText(captured) };
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const runJsTarget = async (artifact, { input, args }) => {
|
|
150
|
+
const captured = capture();
|
|
151
|
+
const host = createJsHost({ input, args: ['main.wasm', ...args], stdout: captured.stdout, stderr: captured.stderr });
|
|
152
|
+
let exitCode = 0;
|
|
153
|
+
try {
|
|
154
|
+
const { instance } = await WebAssembly.instantiate(artifact.slice(), host.imports);
|
|
155
|
+
host.setMemory(instance.exports.memory);
|
|
156
|
+
if (instance.exports._start) instance.exports._start();
|
|
157
|
+
if (instance.exports._end) instance.exports._end();
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error instanceof JsExit) exitCode = error.code;
|
|
160
|
+
else {
|
|
161
|
+
captured.stderr(`The program trapped: ${error?.message ?? error}\n`);
|
|
162
|
+
exitCode = 1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { exitCode, ...capturedText(captured) };
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Compile and run one program.
|
|
170
|
+
*
|
|
171
|
+
* @param {object} params
|
|
172
|
+
* @param {string} params.code
|
|
173
|
+
* @param {string|Uint8Array} params.input
|
|
174
|
+
* @param {string} params.target
|
|
175
|
+
* @param {string[]} params.args
|
|
176
|
+
* @param {string[]} params.compileArgs
|
|
177
|
+
* @param {string} params.fileName
|
|
178
|
+
*/
|
|
179
|
+
const compileAndRun = async ({ code, input = '', target, args = [], compileArgs = [], fileName = 'main.odin' }) => {
|
|
180
|
+
const compileStarted = now();
|
|
181
|
+
const compiled = compile({ code, target, compileArgs: [...compileArgs], fileName });
|
|
182
|
+
const compileMs = Math.round(now() - compileStarted);
|
|
183
|
+
|
|
184
|
+
if (compiled.artifact === null || compiled.artifact.length === 0) {
|
|
185
|
+
return { stdout: '', stderr: '', output: '', errors: compileFailure(compiled), exitCode: null, compileMs, runMs: null };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const runStarted = now();
|
|
189
|
+
let ran;
|
|
190
|
+
try {
|
|
191
|
+
ran = target === 'js_wasm32'
|
|
192
|
+
? await runJsTarget(compiled.artifact, { input, args })
|
|
193
|
+
: runWasiTarget(compiled.artifact, { input, args });
|
|
194
|
+
} catch (error) {
|
|
195
|
+
ran = { exitCode: 1, stdout: '', stderr: `The program could not run: ${error?.message ?? error}\n`, output: '', };
|
|
196
|
+
ran.output = ran.stderr;
|
|
197
|
+
}
|
|
198
|
+
const runMs = Math.round(now() - runStarted);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
stdout: ran.stdout,
|
|
202
|
+
stderr: ran.stderr,
|
|
203
|
+
output: ran.output,
|
|
204
|
+
errors: [],
|
|
205
|
+
exitCode: ran.exitCode,
|
|
206
|
+
compileMs,
|
|
207
|
+
runMs,
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return { compileAndRun, compileOnly };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const capturedText = (captured) => ({
|
|
215
|
+
stdout: captured.stdoutText(),
|
|
216
|
+
stderr: captured.stderrText(),
|
|
217
|
+
output: captured.outputText(),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const now = () => (globalThis.performance?.now?.() ?? Date.now());
|
package/src/host-js.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// The host a `js_wasm32` program runs on, outside a page.
|
|
2
|
+
//
|
|
3
|
+
// A js_wasm32 module imports `odin_env` (the console, stdin, `evaluate`, the
|
|
4
|
+
// math intrinsics), `odin_dom` (elements and events), `webgl` and `webgl2`.
|
|
5
|
+
// Only `odin_env` can do anything without a DOM, so the other three are stubs
|
|
6
|
+
// that accept any call and return 0 — a program that reaches for the DOM runs
|
|
7
|
+
// and does nothing rather than failing to instantiate. A page that wants the
|
|
8
|
+
// DOM and the canvas supplies its own host; see the repository's page, which
|
|
9
|
+
// uses Odin's own `odin.js`.
|
|
10
|
+
|
|
11
|
+
/** Thrown by `trap`/`abort`, so an exit is not confused with a real crash. */
|
|
12
|
+
export class JsExit extends Error {
|
|
13
|
+
constructor(code) {
|
|
14
|
+
super(`The program exited (code ${code}).`);
|
|
15
|
+
this.name = 'JsExit';
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const encoder = new TextEncoder();
|
|
21
|
+
const decoder = new TextDecoder();
|
|
22
|
+
|
|
23
|
+
// Any import the host does not define resolves to a no-op returning 0, so a
|
|
24
|
+
// module written against a different set of host functions still instantiates.
|
|
25
|
+
const withFallback = (target) =>
|
|
26
|
+
new Proxy(target, {
|
|
27
|
+
get: (object, property) =>
|
|
28
|
+
typeof property === 'string' && !(property in object) ? () => 0 : object[property],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} options
|
|
33
|
+
* @param {string|Uint8Array} [options.input] - the program's stdin, handed out once
|
|
34
|
+
* @param {string[]} [options.args] - argv, including argv[0]
|
|
35
|
+
* @param {(text: string) => void} [options.stdout]
|
|
36
|
+
* @param {(text: string) => void} [options.stderr]
|
|
37
|
+
*/
|
|
38
|
+
export function createJsHost({ input = '', args = [], stdout = () => {}, stderr = () => {} } = {}) {
|
|
39
|
+
const inputBytes = typeof input === 'string' ? encoder.encode(input) : new Uint8Array(input);
|
|
40
|
+
let inputOffset = 0;
|
|
41
|
+
|
|
42
|
+
let memory = null;
|
|
43
|
+
const bytes = () => new Uint8Array(memory.buffer);
|
|
44
|
+
const readString = (ptr, len) => decoder.decode(bytes().subarray(ptr, ptr + len));
|
|
45
|
+
|
|
46
|
+
// Any call to a DOM or WebGL import is accepted and returns 0.
|
|
47
|
+
const inertNamespace = new Proxy(
|
|
48
|
+
{},
|
|
49
|
+
{ get: (_target, property) => (typeof property === 'string' ? () => 0 : undefined) }
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const odin_env = {
|
|
53
|
+
write(fd, ptr, len) {
|
|
54
|
+
const text = readString(ptr, len);
|
|
55
|
+
(fd === 2 ? stderr : stdout)(text);
|
|
56
|
+
},
|
|
57
|
+
// Standard input, for the standard library's js streams — see the
|
|
58
|
+
// repository's patches/core/os/file_js.odin, which is in the shipped pack.
|
|
59
|
+
read(fd, ptr, len) {
|
|
60
|
+
if (fd !== 0) return 0;
|
|
61
|
+
const count = Math.max(0, Math.min(len, inputBytes.length - inputOffset));
|
|
62
|
+
if (count > 0) {
|
|
63
|
+
bytes().set(inputBytes.subarray(inputOffset, inputOffset + count), ptr);
|
|
64
|
+
inputOffset += count;
|
|
65
|
+
}
|
|
66
|
+
return count;
|
|
67
|
+
},
|
|
68
|
+
trap() {
|
|
69
|
+
throw new JsExit(1);
|
|
70
|
+
},
|
|
71
|
+
abort() {
|
|
72
|
+
throw new JsExit(1);
|
|
73
|
+
},
|
|
74
|
+
alert() {},
|
|
75
|
+
// The one interop call that works without a DOM: it evaluates in the global
|
|
76
|
+
// scope, so a page that embeds this can draw through it.
|
|
77
|
+
evaluate(ptr, len) {
|
|
78
|
+
const source = readString(ptr, len);
|
|
79
|
+
(0, globalThis.eval)(source);
|
|
80
|
+
},
|
|
81
|
+
open() {},
|
|
82
|
+
time_now: () => BigInt(Date.now()),
|
|
83
|
+
tick_now: () => globalThis.performance?.now?.() ?? Date.now(),
|
|
84
|
+
time_sleep() {},
|
|
85
|
+
sqrt: Math.sqrt,
|
|
86
|
+
sin: Math.sin,
|
|
87
|
+
cos: Math.cos,
|
|
88
|
+
pow: Math.pow,
|
|
89
|
+
fmuladd: (x, y, z) => x * y + z,
|
|
90
|
+
ln: Math.log,
|
|
91
|
+
exp: Math.exp,
|
|
92
|
+
ldexp: (x, exponent) => x * 2 ** exponent,
|
|
93
|
+
rand_bytes(ptr, len) {
|
|
94
|
+
bytes().set(globalThis.crypto.getRandomValues(new Uint8Array(len)), ptr);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
imports: {
|
|
100
|
+
env: withFallback({}),
|
|
101
|
+
odin_env: withFallback(odin_env),
|
|
102
|
+
odin_dom: inertNamespace,
|
|
103
|
+
webgl: inertNamespace,
|
|
104
|
+
webgl2: inertNamespace,
|
|
105
|
+
},
|
|
106
|
+
/** The module exports its own memory, so it is set after instantiation. */
|
|
107
|
+
setMemory(mem) {
|
|
108
|
+
memory = mem;
|
|
109
|
+
},
|
|
110
|
+
args,
|
|
111
|
+
};
|
|
112
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The entry every environment gets unless something more specific matches, so
|
|
2
|
+
// it has to work without a filesystem: no `node:fs` here, and `baseUrl` is
|
|
3
|
+
// required because a browser cannot read a file that lives inside an npm package.
|
|
4
|
+
import { createApi } from './api.js';
|
|
5
|
+
|
|
6
|
+
const api = createApi({ packaged: null });
|
|
7
|
+
|
|
8
|
+
export const createCompiler = api.createCompiler;
|
|
9
|
+
export const { TARGETS } = api;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Node, where the assets that ship in this package can be read off disk, so
|
|
2
|
+
// `baseUrl` becomes optional and `createCompiler()` is enough on its own.
|
|
3
|
+
import { createApi } from './api.js';
|
|
4
|
+
import { packagedAssets } from './packaged.node.js';
|
|
5
|
+
|
|
6
|
+
const api = createApi({ packaged: packagedAssets });
|
|
7
|
+
|
|
8
|
+
export const createCompiler = api.createCompiler;
|
|
9
|
+
export const { TARGETS } = api;
|
package/src/pack.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The packed standard library: Odin's `base/` and `core/`, in the container the
|
|
2
|
+
// repository's packer writes.
|
|
3
|
+
//
|
|
4
|
+
// The compiler reads its standard library from files, so a page must give it
|
|
5
|
+
// one. A flat, self-describing container is smaller than a tar (no per-entry
|
|
6
|
+
// headers) and inflates with the one decompressor a browser and Node both have.
|
|
7
|
+
//
|
|
8
|
+
// Format (before gzip):
|
|
9
|
+
// "ODINPOC1" 8 bytes
|
|
10
|
+
// count u32 little-endian
|
|
11
|
+
// count × { pathLen:u32, path:utf8, dataLen:u32, data:bytes }
|
|
12
|
+
|
|
13
|
+
export const PACK_MAGIC = 'ODINPOC1';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {ArrayBuffer|Uint8Array} buffer - the inflated container
|
|
17
|
+
* @returns {[string, Uint8Array][]} `base/…`, `core/…` and their bytes
|
|
18
|
+
*/
|
|
19
|
+
export function unpackLibrary(buffer) {
|
|
20
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
21
|
+
if (new TextDecoder().decode(bytes.subarray(0, 8)) !== PACK_MAGIC) {
|
|
22
|
+
throw new Error(`The standard library pack is not a ${PACK_MAGIC} container.`);
|
|
23
|
+
}
|
|
24
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
25
|
+
const decoder = new TextDecoder();
|
|
26
|
+
const count = view.getUint32(8, true);
|
|
27
|
+
const entries = [];
|
|
28
|
+
let pos = 12;
|
|
29
|
+
for (let i = 0; i < count; i++) {
|
|
30
|
+
const pathLen = view.getUint32(pos, true);
|
|
31
|
+
pos += 4;
|
|
32
|
+
const path = decoder.decode(bytes.subarray(pos, pos + pathLen));
|
|
33
|
+
pos += pathLen;
|
|
34
|
+
const dataLen = view.getUint32(pos, true);
|
|
35
|
+
pos += 4;
|
|
36
|
+
entries.push([path, bytes.subarray(pos, pos + dataLen)]);
|
|
37
|
+
pos += dataLen;
|
|
38
|
+
}
|
|
39
|
+
return entries;
|
|
40
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The Node half of the packaged-asset source: reading the files that ship in
|
|
2
|
+
// this package.
|
|
3
|
+
//
|
|
4
|
+
// This module is only reachable through the `node` condition in package.json,
|
|
5
|
+
// so a browser bundle never includes it and never trips over `node:fs`.
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
|
|
8
|
+
export const PACKAGED_ROOT = new URL('../assets/', import.meta.url);
|
|
9
|
+
|
|
10
|
+
export const packagedAssets = {
|
|
11
|
+
root: PACKAGED_ROOT,
|
|
12
|
+
readFile: async (name) => new Uint8Array(await readFile(new URL(name, PACKAGED_ROOT))),
|
|
13
|
+
};
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The engines, one per asset source.
|
|
2
|
+
//
|
|
3
|
+
// A source is keyed by where its bytes came from, so creating two compilers
|
|
4
|
+
// against the same `baseUrl` — or the same package — compiles the compiler
|
|
5
|
+
// module and unpacks the standard library once. Every compiler holds a
|
|
6
|
+
// reference; the entry is dropped when the last one is disposed.
|
|
7
|
+
|
|
8
|
+
import { inflateGzip } from './assets.js';
|
|
9
|
+
import { createEngine } from './engine.js';
|
|
10
|
+
import { unpackLibrary } from './pack.js';
|
|
11
|
+
|
|
12
|
+
const engines = new Map();
|
|
13
|
+
|
|
14
|
+
async function loadEngine(source, onProgress) {
|
|
15
|
+
const report = (name) => (loaded, total) => onProgress?.({ name, loaded, total });
|
|
16
|
+
const wasmBytes = await source.readAsset('odin.wasm', report('odin.wasm'));
|
|
17
|
+
const packBytes = await source.readAsset('odin-src.pack.gz', report('odin-src.pack.gz'));
|
|
18
|
+
const compilerModule = await WebAssembly.compile(wasmBytes);
|
|
19
|
+
const libraryEntries = unpackLibrary(await inflateGzip(packBytes));
|
|
20
|
+
return createEngine({ compilerModule, libraryEntries });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @returns {{ promise: Promise<object>, refs: number, key: string }} */
|
|
24
|
+
export function acquireEngine(source, onProgress) {
|
|
25
|
+
let entry = engines.get(source.key);
|
|
26
|
+
if (entry === undefined) {
|
|
27
|
+
entry = { key: source.key, refs: 0, promise: loadEngine(source, onProgress) };
|
|
28
|
+
engines.set(source.key, entry);
|
|
29
|
+
// A failed load must not be cached, or every later attempt reports the same
|
|
30
|
+
// stale failure.
|
|
31
|
+
entry.promise.catch(() => engines.delete(source.key));
|
|
32
|
+
}
|
|
33
|
+
entry.refs += 1;
|
|
34
|
+
return entry;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function releaseEngine(entry) {
|
|
38
|
+
entry.refs -= 1;
|
|
39
|
+
if (entry.refs <= 0) engines.delete(entry.key);
|
|
40
|
+
}
|