@live-codes/odin-wasm 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hatem Hosny
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,181 @@
1
- # Temporary Holding Version
1
+ # @live-codes/odin-wasm
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Run **Odin** in a browser with no server, through one API — on the Odin compiler compiled to
4
+ WebAssembly. The assets ship inside the package, so there is no toolchain to install.
5
+
6
+ ```js
7
+ import { createCompiler } from '@live-codes/odin-wasm';
8
+
9
+ const compiler = await createCompiler();
10
+
11
+ const { stdout, stderr, output, errors, exitCode } = await compiler.run(`
12
+ package main
13
+
14
+ import "core:fmt"
15
+
16
+ main :: proc() {
17
+ fmt.println("Hellope!")
18
+ }
19
+ `);
20
+
21
+ console.log(output); // "Hellope!\n" - stdout and stderr in the order the program wrote them
22
+ console.log(exitCode); // 0
23
+ ```
24
+
25
+ That is Node, where the packaged assets can be read off disk. **In a browser there is no filesystem**,
26
+ so a page has to be given a URL — copy the assets into whatever serves your page and pass it:
27
+
28
+ ```bash
29
+ npx --package @live-codes/odin-wasm odin-wasm-copy-assets public/odin
30
+ ```
31
+
32
+ ```js
33
+ const compiler = await createCompiler({ baseUrl: new URL('/odin/', location.href) });
34
+ ```
35
+
36
+ ## How it works
37
+
38
+ The compiler is a WASI command module, built by
39
+ [wodin](https://github.com/karl-zylinski/wodin) — the fork of Odin with a **direct WebAssembly
40
+ backend**, which emits wasm with no LLVM and no linker, and is what makes a browser-sized compiler
41
+ possible. The standard library is a single gzipped container the package reads into an in-memory file
42
+ system; the compiler never touches disk.
43
+
44
+ ```
45
+ your Odin source
46
+ → odin.wasm the compiler (a WASI module) → a wasm module
47
+ → run it wasi_wasm32: a WASI host, in this package
48
+ js_wasm32: the browser's DOM/canvas, through Odin's JS runtime → stdout / page
49
+ ```
50
+
51
+ ## API
52
+
53
+ ### `createCompiler(options)`
54
+
55
+ Returns a promise for a compiler. The heavy part — the compiler module and the packed standard
56
+ library, about 6 MB — is fetched here, so a bad `baseUrl` fails at this point rather than at the first
57
+ `run`. Compilers created against the same assets share one engine.
58
+
59
+ | Option | Meaning |
60
+ | --- | --- |
61
+ | `baseUrl` | Where the assets are served from. **Optional in Node**, where omitting it uses the files the package ships; required anywhere else, and absolute http(s) except in a browser, where it may be relative to the page. |
62
+ | `target` | `wasi_wasm32` (default) or `js_wasm32`. See [Targets](#targets). |
63
+ | `args` | Default program argv. |
64
+ | `compileArgs` | Extra compiler flags. |
65
+ | `fileName` | The name the source is compiled under. |
66
+ | `onProgress` | `({ name, loaded, total }) => {}` while the assets download. |
67
+
68
+ ### `compiler.run(code, input, runOptions?)`
69
+
70
+ Compiles and runs. `input` is stdin as a string or `Uint8Array`, handed to the program once.
71
+ `runOptions` may override `target`, `args`, `compileArgs` and `fileName` for that run.
72
+
73
+ | Field | Meaning |
74
+ | --- | --- |
75
+ | `stdout` | Everything the program wrote to fd 1. |
76
+ | `stderr` | Everything it wrote to fd 2. |
77
+ | `output` | Both, in the order the program wrote them. |
78
+ | `errors` | The compiler's diagnostics, one string per line. **Empty when the program compiled.** |
79
+ | `exitCode` | The program's exit status, or `null` if it never ran because it did not compile. |
80
+ | `compileMs` | Wall clock for the compile. |
81
+ | `runMs` | Wall clock for the run, or `null` if it did not run. |
82
+
83
+ `errors` is empty on success so that `errors.length` is a reliable failure test. On failure it holds
84
+ what the compiler said, and if the compiler said nothing it holds the underlying error's message, so a
85
+ failure is never silent.
86
+
87
+ ### `compiler.dispose()`
88
+
89
+ Drops this compiler's hold on the shared engine. Further runs throw.
90
+
91
+ ### `TARGETS`
92
+
93
+ The two targets, for building a picker: `['wasi_wasm32', 'js_wasm32']`.
94
+
95
+ ## Targets
96
+
97
+ | target | runs on | what it reaches |
98
+ | --- | --- | --- |
99
+ | `wasi_wasm32` (default) | a WASI host in this package | `core:fmt`, **stdin**, `os.args`, exit codes — and it works in Node |
100
+ | `js_wasm32` | the Odin JS runtime, which a page provides | `core:fmt`, **stdin**, the DOM and a `<canvas>` |
101
+
102
+ `js_wasm32` is the browser target: a page runs it with Odin's own `odin.js` so the program can reach
103
+ the DOM and a canvas. Outside a page there is no DOM, so this package gives it a host that supports
104
+ the console, stdin and `js.evaluate` and stubs the rest — enough to run and test a console program,
105
+ and to draw through `js.evaluate` when a page embeds it.
106
+
107
+ Standard input works on **both**, which is not a given: upstream's `core:os` is deliberately inert on
108
+ the js target (a compile-time `#panic("core:os is unsupported on js/wasm")`), so the standard library
109
+ that ships here carries two small patches that give it a real `os.stdin` over the host's
110
+ `odin_env.read`. See the repository's `FINDINGS.md`.
111
+
112
+ ## Loading it without a bundler
113
+
114
+ `dist/odin-wasm.global.js` is a **minified IIFE bundle** — one classic script, 16.5 KB — for anywhere
115
+ an ES module cannot go: a classic (non-module) worker, a plain `<script>`, a CDN URL handed to
116
+ `importScripts()`. It sets `self.odinWasm` to the same two exports (`createCompiler`, `TARGETS`).
117
+
118
+ ```js
119
+ // worker.js - a classic worker: no { type: 'module' }, no imports
120
+ importScripts('odin-wasm.global.js');
121
+
122
+ const compiler = await self.odinWasm.createCompiler({ baseUrl: '/odin/' });
123
+ const { stdout, exitCode } = await compiler.run('package main\n\nimport "core:fmt"\n\nmain :: proc() { fmt.println("hi") }\n');
124
+ ```
125
+
126
+ It is reachable as `@live-codes/odin-wasm/iife`, and it is committed rather than built on install, so
127
+ a consumer never needs esbuild. Rebuild it with `npm run build:iife`.
128
+
129
+ ## Where the assets come from
130
+
131
+ ```
132
+ assets/odin.wasm 3.6 MB the compiler (a WASI module, built with wasi-sdk)
133
+ assets/odin-src.pack.gz 2.4 MB Odin's base/ and core/, gzipped
134
+ ```
135
+
136
+ Three ways to reach them:
137
+
138
+ - **In Node, nothing.** Omit `baseUrl` and the package reads those files directly.
139
+ - **In a browser, one command.** `odin-wasm-copy-assets <dir>` writes them into a directory you
140
+ already serve, plus an `asset-receipts.json` describing its own bytes.
141
+ - **Or point `baseUrl` at a host you already have** — a CDN, an S3 bucket — serving those two paths.
142
+
143
+ Every asset is checked against a pinned SHA-256 receipt (`src/asset-receipts.js`) before it is used,
144
+ so a truncated download or a host serving something else is an error rather than a wasm
145
+ instantiation failure a step later.
146
+
147
+ The pack is Odin's standard library with `core/rexcode` (the machine-code library, tens of MB of
148
+ binaries) and other-target assembly left out. The `vendor` collection is **not** shipped: the compiler
149
+ requires the directory to exist, so it is present and empty, but a program that imports a `vendor:`
150
+ package will not find it.
151
+
152
+ ## What does not work
153
+
154
+ | | why |
155
+ | --- | --- |
156
+ | **`vendor:` packages** (`vendor:raylib`, …) | not shipped: 91 MB, mostly DirectX/SDL and C libraries with prebuilt wasm objects |
157
+ | **a DOM off the page** | `js_wasm32` needs one; in Node only its console, stdin and `js.evaluate` are real |
158
+ | **`js_wasm32` exit codes** | Odin's js runtime exits by trapping, so a program's status is reported as 1 rather than the number it passed |
159
+ | **threads, files, sockets** | WASI preview 1 here is single-threaded, and a program is given an empty filesystem |
160
+ | **a runaway program** | the compiler and the program both run synchronously; there is no cancellation |
161
+
162
+ ## Development
163
+
164
+ ```bash
165
+ npm test # 17 checks: both targets, stdin, args, exit codes, diagnostics, the bundle, the receipts
166
+ npm run check-package # what `prepack` runs: every asset hashes to its receipt and the bundle is present
167
+ npm run sync-assets # copy ../../assets into this package and re-pin the receipts
168
+ npm run build:iife # rebuild dist/ (needs esbuild; `npm install --include=dev`)
169
+ ```
170
+
171
+ `src/wasi-host.js` is the same WASI host as the demo repository's `public/wasi.js`, carried here so
172
+ the package stands alone; the two are kept in step by hand.
173
+
174
+ The tests run against the packaged assets with no server. `test/iife.test.js` loads the committed
175
+ bundle in a `vm` context and checks its shape, so the shipped file cannot rot unnoticed.
176
+
177
+ ## License
178
+
179
+ **MIT.** The compiler module and the packed standard library are Odin's, under its **zlib** license —
180
+ see [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md), which also records the two standard-library
181
+ files this package ships modified.
@@ -0,0 +1,41 @@
1
+ # Third-party notices
2
+
3
+ This package's own code is MIT (see [LICENSE](LICENSE)). What it ships and runs is other people's, and
4
+ this file records it.
5
+
6
+ ## The assets in `assets/`
7
+
8
+ Built, not written here; both come from a checkout of
9
+ [**wodin**](https://github.com/karl-zylinski/wodin), the fork of
10
+ [**Odin**](https://github.com/odin-lang/Odin) that adds a direct WebAssembly backend.
11
+
12
+ - **`assets/odin.wasm`** — the Odin compiler, compiled to `wasm32-wasip1`. Odin is under the **zlib**
13
+ license, and the module bundles what Odin's own sources do:
14
+ - **libtommath** — public domain.
15
+ - **utf8proc** — MIT.
16
+ - **ucg** — MIT.
17
+ - **`assets/odin-src.pack.gz`** — Odin's standard library (`base/` and `core/`), packed. Same
18
+ repository, same zlib license.
19
+
20
+ ### Modified standard-library sources
21
+
22
+ The pack is **not** a pristine copy of Odin's standard library. Two files replace the checkout's
23
+ copies so the `js_wasm32` target can have standard input; the modified versions are reproduced in the
24
+ repository this package is developed in, under `patches/`:
25
+
26
+ - `core/os/wasm.odin` — upstream's compile-time `#panic("core:os is unsupported on js/wasm")` is removed.
27
+ - `core/os/file_js.odin` — upstream's inert stubs are kept, but `os.stdin`, `os.stdout` and
28
+ `os.stderr` are given a real implementation over a host `odin_env.read` / `odin_env.write`.
29
+
30
+ Both are Odin source, under Odin's zlib license; the modifications are the package author's.
31
+
32
+ ## Toolchain used to build the compiler
33
+
34
+ - **wasi-sdk** (clang/lld and the WASI sysroot, including wasi-libc) — Apache-2.0 WITH
35
+ LLVM-exception; wasi-libc is separately MIT and Apache-2.0 WITH LLVM-exception.
36
+
37
+ ## Runtime
38
+
39
+ Nothing else is bundled. The WASI host, the js-target host, the pack reader and the IIFE bundle are
40
+ this package's own code, and the API uses only standard globals (`WebAssembly`, `fetch`,
41
+ `DecompressionStream`, `crypto.subtle`, `TextDecoder`) — there are no runtime dependencies.
Binary file
Binary file
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // Copies the assets that ship inside this package into a directory you serve.
3
+ //
4
+ // This is the browser story: a page cannot read a file inside node_modules, so
5
+ // the assets have to be published by whatever serves the page. One command drops
6
+ // a complete, self-describing copy into your public directory; point `baseUrl`
7
+ // at it and nothing else has to be hosted.
8
+ import { cp, mkdir, readdir, writeFile } from 'node:fs/promises';
9
+ import { resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { ASSET_RECEIPTS } from '../src/asset-receipts.js';
12
+
13
+ const ASSETS = fileURLToPath(new URL('../assets/', import.meta.url));
14
+
15
+ const USAGE = `Copy the assets that ship with this package into a directory you serve.
16
+
17
+ odin-wasm-copy-assets [directory]
18
+
19
+ directory where to write them (default: ./odin)
20
+
21
+ --print-path print the packaged assets directory and exit
22
+ --help print this
23
+ `;
24
+
25
+ const args = process.argv.slice(2);
26
+ if (args.includes('--help') || args.includes('-h')) {
27
+ console.log(USAGE);
28
+ process.exit(0);
29
+ }
30
+ if (args.includes('--print-path')) {
31
+ console.log(ASSETS);
32
+ process.exit(0);
33
+ }
34
+
35
+ const target = resolve(args.find((arg) => !arg.startsWith('-')) ?? 'odin');
36
+
37
+ await mkdir(target, { recursive: true });
38
+ for (const entry of await readdir(ASSETS, { withFileTypes: true })) {
39
+ await cp(resolve(ASSETS, entry.name), resolve(target, entry.name), { recursive: true, force: true });
40
+ }
41
+
42
+ // The copy carries the receipts for its own bytes, so whoever serves it can check it.
43
+ await writeFile(resolve(target, 'asset-receipts.json'), `${JSON.stringify(ASSET_RECEIPTS, null, 2)}\n`);
44
+
45
+ console.log(`Assets copied to ${target}`);
46
+ console.log('Serve that directory and pass its URL as baseUrl, for example:');
47
+ console.log(" await createCompiler({ baseUrl: new URL('/odin/', location.href) });");
@@ -0,0 +1,6 @@
1
+ var odinWasm=(()=>{var W=Object.defineProperty;var pt=Object.getOwnPropertyDescriptor;var gt=Object.getOwnPropertyNames;var St=Object.prototype.hasOwnProperty;var mt=(t,e)=>{for(var n in e)W(t,n,{get:e[n],enumerable:!0})},wt=(t,e,n,c)=>{if(e&&typeof e=="object"||typeof e=="function")for(let f of gt(e))!St.call(t,f)&&f!==n&&W(t,f,{get:()=>e[f],enumerable:!(c=pt(e,f))||c.enumerable});return t};var _t=t=>wt(W({},"__esModule",{value:!0}),t);var Yt={};mt(Yt,{TARGETS:()=>Lt,createCompiler:()=>Bt});var V=Object.freeze({"odin.wasm":Object.freeze({bytes:3763439,sha256:"5044e383aa2415927cd518a4d3e6fdbcc6c99154ac90d212e154f18c092deef3"}),"odin-src.pack.gz":Object.freeze({bytes:2502169,sha256:"567b1c0c1b50d3e557d119927ade3373dda91364acae3302f2cf01950a8dd8f9"})});var zt=Object.freeze(Object.keys(V)),Ut="baseUrl is required here. The assets that ship in this package can only be read where there is a filesystem, and a browser cannot reach a file inside an npm package \u2014 copy them somewhere your page can fetch with `npx --package @live-codes/odin-wasm odin-wasm-copy-assets <dir>` and pass that directory as baseUrl.";function tt(t,e){if(t.baseUrl!=null&&t.baseUrl!=="")return yt(t);if(!e)throw new Error(Ut);return At(e)}function yt(t){let e=Ct(t.baseUrl);return{kind:"hosted",key:`hosted\0${e}`,description:e,baseUrl:e,async readAsset(n,c){let f=new URL(n,e),E=await fetch(f);if(!E.ok)throw new Error(`Failed to load the runtime asset ${f}: ${E.status}`);return et(n,await Tt(E,c))}}}function At(t){return{kind:"packaged",key:`packaged\0${t.root.href}`,description:`the assets packaged with this library (${t.root.href})`,async readAsset(e){let n;try{n=await t.readFile(e)}catch(c){throw new Error(`Failed to read the packaged asset ${e} from ${t.root.href}: ${c.message}`,{cause:c})}return et(e,n)}}}function Ct(t){let e;try{e=t instanceof URL?t:new URL(t,globalThis.document?.baseURI)}catch(n){throw new Error(`baseUrl must be an absolute http(s) URL, or relative to the page in a browser: ${n.message}`,{cause:n})}if(e.protocol!=="http:"&&e.protocol!=="https:")throw new Error(`baseUrl must be an absolute http(s) URL, or relative to the page in a browser: got "${t}"`);return e.href.endsWith("/")?e.href:`${e.href}/`}async function Tt(t,e){if(!e||!t.body)return new Uint8Array(await t.arrayBuffer());let n=Number(t.headers.get("content-length"))||0,c=t.body.getReader(),f=[],E=0;for(e(0,n);;){let{done:d,value:S}=await c.read();if(d)break;f.push(S),E+=S.length,e(E,n)}let w=new Uint8Array(E),g=0;for(let d of f)w.set(d,g),g+=d.length;return w}async function et(t,e){let n=V[t];if(!n)throw new Error(`No pinned receipt for the runtime asset ${t}`);if(e.byteLength!==n.bytes)throw new Error(`The runtime asset ${t} is ${e.byteLength} bytes, expected ${n.bytes}`);let c=await bt(e);if(c!==n.sha256)throw new Error(`The runtime asset ${t} failed SHA-256 verification: expected ${n.sha256}, got ${c}`);return e}async function bt(t){let e=globalThis.crypto?.subtle;if(!e)throw new Error("Verifying the runtime assets needs crypto.subtle: a secure context in the browser, or Node 20 and later.");let n=await e.digest("SHA-256",t);return[...new Uint8Array(n)].map(c=>c.toString(16).padStart(2,"0")).join("")}async function rt(t){if(typeof DecompressionStream!="function")throw new Error("Inflating the standard library pack needs DecompressionStream");let e=new Blob([t]).stream().pipeThrough(new DecompressionStream("gzip"));return new Uint8Array(await new Response(e).arrayBuffer())}var N=class extends Error{constructor(e){super(`WASI process exited with code ${e}`),this.code=e}},O=t=>{let e=[];for(let n of String(t).split("/"))if(!(n===""||n===".")){if(n===".."){e.pop();continue}e.push(n)}return e.join("/")},H=t=>{let e=t.lastIndexOf("/");return e<0?"":t.slice(0,e)},Y=new TextEncoder,xt=new TextDecoder,$=class{constructor(){this.files=new Map,this.dirs=new Set([""])}mkdirAll(e){let n=O(e)===""?[]:O(e).split("/"),c="";for(let f of n)c=c===""?f:`${c}/${f}`,this.dirs.add(c)}writeFile(e,n){let c=O(e);this.mkdirAll(H(c));let f=n instanceof Uint8Array?n:new Uint8Array(n);this.files.set(c,{data:f,size:f.length})}readFile(e){let n=this.files.get(O(e));return n?n.data.subarray(0,n.size):null}exists(e){let n=O(e);return this.files.has(n)||this.dirs.has(n)}listDir(e){let n=O(e),c=n===""?"":`${n}/`,f=[];for(let E of this.dirs)E!==""&&E.startsWith(c)&&E.indexOf("/",c.length)<0&&E!==n&&f.push({name:E.slice(c.length),isDir:!0});for(let E of this.files.keys())E.startsWith(c)&&E.indexOf("/",c.length)<0&&f.push({name:E.slice(c.length),isDir:!1});return f.sort((E,w)=>E.name<w.name?-1:E.name>w.name?1:0),f}},kt=t=>t instanceof Uint8Array?t:new Uint8Array(t),X=(t,e={})=>{let n=e.args??[],c=e.env??[],f=e.stdout??(()=>{}),E=e.stderr??(()=>{}),w=kt(Y.encode(e.stdin??"")),g=0,d=new Map,S=4;d.set(0,{kind:"stdin"}),d.set(1,{kind:"stdout"}),d.set(2,{kind:"stderr"}),d.set(3,{kind:"dir",path:""});let _=["","",""],l=null,h=()=>new Uint8Array(l.buffer),u=()=>new DataView(l.buffer),y=(r,o)=>xt.decode(h().subarray(r,r+o)),k=(r,o)=>{let s=_[r]+o,i=s.lastIndexOf(`
2
+ `);i>=0&&((r===1?f:E)(s.slice(0,i+1)),s=s.slice(i+1)),_[r]=s},R=()=>{for(let r of[1,2])_[r]!==""&&((r===1?f:E)(_[r]),_[r]="")},A=(r,o,s)=>{let i=u();i.setBigUint64(r+0,0n,!0),i.setBigUint64(r+8,0n,!0),i.setUint8(r+16,o),i.setBigUint64(r+24,1n,!0),i.setBigUint64(r+32,BigInt(s),!0),i.setBigUint64(r+40,0n,!0),i.setBigUint64(r+48,0n,!0),i.setBigUint64(r+56,0n,!0)},z=(r,o)=>{if(t.dirs.has(r))return A(o,3,0),0;let s=t.files.get(r);return s?(A(o,4,s.size),0):44},D=(r,o)=>{if(o<=r.data.length)return;let s=Math.max(r.data.length*2,4096);for(;s<o;)s*=2;let i=new Uint8Array(s);i.set(r.data.subarray(0,r.size)),r.data=i},G=(r,o,s,i)=>{let a=u(),p=h(),m=0;for(let C=0;C<s;C++){let T=a.getUint32(o+C*8,!0),U=a.getUint32(o+C*8+4,!0),b=Math.max(0,Math.min(U,r.size-i));if(p.set(r.data.subarray(i,i+b),T),i+=b,m+=b,b<U)break}return m},Q=(r,o,s,i)=>{let a=u(),p=h(),m=0;for(let C=0;C<s;C++){let T=a.getUint32(o+C*8,!0),U=a.getUint32(o+C*8+4,!0);D(r,i+U),r.data.set(p.subarray(T,T+U),i),i+=U,r.size=Math.max(r.size,i),m+=U}return m},Z=(r,o,s)=>{let i=u(),a=h(),p=s;for(let m=0;m<r.length;m++){let C=Y.encode(r[m]);i.setUint32(o+m*4,p,!0),a.set(C,p),a[p+C.length]=0,p+=C.length+1}return 0},L=(r,o,s)=>{let i=d.get(r);if(!i||i.kind!=="dir")return null;let a=y(o,s);return O(a.startsWith("/")?a:`${i.path}/${a}`)};return{imports:{args_sizes_get(r,o){let s=u();return s.setUint32(r,n.length,!0),s.setUint32(o,n.reduce((i,a)=>i+Y.encode(a).length+1,0),!0),0},args_get(r,o){return Z(n,r,o)},environ_sizes_get(r,o){let s=u();return s.setUint32(r,c.length,!0),s.setUint32(o,c.reduce((i,a)=>i+Y.encode(a).length+1,0),!0),0},environ_get(r,o){return Z(c,r,o)},clock_res_get(r,o){return u().setBigUint64(o,1000n,!0),0},clock_time_get(r,o,s){let i=r===0?BigInt(Date.now())*1000000n:BigInt(Math.round((globalThis.performance?.now()??Date.now())*1e3))*1000n;return u().setBigUint64(s,i,!0),0},fd_close(r){return d.has(r)?(r>3&&d.delete(r),0):8},fd_fdstat_get(r,o){let s=d.get(r);if(!s)return 8;let i=u(),a=s.kind==="dir"?3:s.kind==="file"?4:2;return i.setUint8(o,a),i.setUint16(o+2,0,!0),i.setBigUint64(o+8,0xffffffffffffffffn,!0),i.setBigUint64(o+16,0xffffffffffffffffn,!0),0},fd_filestat_get(r,o){let s=d.get(r);return s?(s.kind==="dir"?A(o,3,0):s.kind==="file"?A(o,4,s.file.size):A(o,2,0),0):8},fd_filestat_set_size(r,o){let s=d.get(r);if(!s||s.kind!=="file")return 8;let i=Number(o);return D(s.file,i),i>s.file.size&&s.file.data.fill(0,s.file.size,i),s.file.size=i,0},fd_prestat_get(r,o){if(r!==3)return 8;let s=u();return s.setUint8(o,0),s.setUint32(o+4,1,!0),0},fd_prestat_dir_name(r,o,s){return r!==3?8:(s>=1&&(h()[o]=47),0)},fd_read(r,o,s,i){let a=d.get(r);if(!a)return 8;if(a.kind==="stdin"){let m=G({data:w,size:w.length},o,s,g);return g+=m,u().setUint32(i,m,!0),0}if(a.kind!=="file")return 8;let p=G(a.file,o,s,a.offset);return a.offset+=p,u().setUint32(i,p,!0),0},fd_pread(r,o,s,i,a){let p=d.get(r);if(!p||p.kind!=="file")return 8;let m=G(p.file,o,s,Number(i));return u().setUint32(a,m,!0),0},fd_write(r,o,s,i){let a=d.get(r);if(!a)return 8;if(a.kind==="stdout"||a.kind==="stderr"){let m=u(),C=0;for(let T=0;T<s;T++){let U=m.getUint32(o+T*8,!0),b=m.getUint32(o+T*8+4,!0);k(r,y(U,b)),C+=b}return m.setUint32(i,C,!0),0}if(a.kind!=="file")return 8;let p=Q(a.file,o,s,a.offset);return a.offset+=p,u().setUint32(i,p,!0),0},fd_pwrite(r,o,s,i,a){let p=d.get(r);if(!p||p.kind!=="file")return 8;let m=Q(p.file,o,s,Number(i));return u().setUint32(a,m,!0),0},fd_seek(r,o,s,i){let a=d.get(r);if(!a||a.kind!=="file")return 8;let p=Number(o),m=0;if(s===1)m=a.offset;else if(s===2)m=a.file.size;else if(s!==0)return 28;return m+p<0?28:(a.offset=m+p,u().setBigUint64(i,BigInt(a.offset),!0),0)},fd_readdir(r,o,s,i,a){let p=d.get(r);if(!p||p.kind!=="dir")return 8;p.entries||(p.entries=t.listDir(p.path));let m=u(),C=h(),T=0;for(let U=Number(i);U<p.entries.length;U++){let b=p.entries[U],x=Y.encode(b.name),F=new Uint8Array(24+x.length),I=new DataView(F.buffer);I.setBigUint64(0,BigInt(U+1),!0),I.setBigUint64(8,BigInt(U+1),!0),I.setUint32(16,x.length,!0),I.setUint8(20,b.isDir?3:4),F.set(x,24);let j=Math.min(F.length,s-T);if(C.set(F.subarray(0,j),o+T),T+=j,j<F.length)break}return m.setUint32(a,T,!0),0},path_create_directory(r,o,s){let i=L(r,o,s);return i===null?8:t.exists(i)?20:t.dirs.has(H(i))?(t.dirs.add(i),0):44},path_filestat_get(r,o,s,i,a){let p=L(r,s,i);return p===null?8:z(p,a)},path_open(r,o,s,i,a,p,m,C,T){let U=L(r,s,i);if(U===null)return 8;let b=u();if(t.dirs.has(U)){if(a&4&&a&1)return 20;let I=S++;return d.set(I,{kind:"dir",path:U}),b.setUint32(T,I,!0),0}if(a&2)return t.files.has(U)?54:44;let x=t.files.get(U);if(x){if(a&4&&a&1)return 20;a&8&&(x.data=new Uint8Array(0),x.size=0)}else{if(!(a&1)||!t.dirs.has(H(U)))return 44;x={data:new Uint8Array(0),size:0},t.files.set(U,x)}let F=S++;return d.set(F,{kind:"file",path:U,file:x,offset:C&1?x.size:0}),b.setUint32(T,F,!0),0},path_remove_directory(r,o,s){let i=L(r,o,s);return i===null?8:t.dirs.has(i)?t.listDir(i).length>0?55:(t.dirs.delete(i),0):t.files.has(i)?54:44},path_unlink_file(r,o,s){let i=L(r,o,s);return i===null?8:t.files.has(i)?(t.files.delete(i),0):t.dirs.has(i)?31:44},fd_sync(){return 0},fd_datasync(){return 0},fd_advise(){return 0},fd_tell(r,o){let s=d.get(r);return!s||s.kind!=="file"?8:(u().setBigUint64(o,BigInt(s.offset),!0),0)},fd_renumber(r,o){return!d.has(r)||!d.has(o)?8:(d.set(o,d.get(r)),d.delete(r),0)},fd_allocate:()=>52,fd_fdstat_set_flags:()=>52,fd_fdstat_set_rights:()=>52,fd_filestat_set_times:()=>52,path_filestat_set_times:()=>52,path_link:()=>52,path_readlink:()=>52,path_rename:()=>52,path_symlink:()=>52,proc_raise:()=>52,sock_accept:()=>52,sock_recv:()=>52,sock_send:()=>52,sock_shutdown:()=>52,proc_exit(r){throw new N(r)},random_get(r,o){return globalThis.crypto.getRandomValues(h().subarray(r,r+o)),0},sched_yield(){return 0},poll_oneoff(){return 52}},setMemory(r){l=r},flush:R}},q=(t,e)=>{let n=new WebAssembly.Instance(t,{wasi_snapshot_preview1:e.imports});e.setMemory(n.exports.memory);let c=0;try{n.exports._start()}catch(f){if(f instanceof N)c=f.code;else throw e.flush(),f}return e.flush(),c};var B=class extends Error{constructor(e){super(`The program exited (code ${e}).`),this.name="JsExit",this.code=e}},Ft=new TextEncoder,It=new TextDecoder,nt=t=>new Proxy(t,{get:(e,n)=>typeof n=="string"&&!(n in e)?()=>0:e[n]});function st({input:t="",args:e=[],stdout:n=()=>{},stderr:c=()=>{}}={}){let f=typeof t=="string"?Ft.encode(t):new Uint8Array(t),E=0,w=null,g=()=>new Uint8Array(w.buffer),d=(l,h)=>It.decode(g().subarray(l,l+h)),S=new Proxy({},{get:(l,h)=>typeof h=="string"?()=>0:void 0}),_={write(l,h,u){let y=d(h,u);(l===2?c:n)(y)},read(l,h,u){if(l!==0)return 0;let y=Math.max(0,Math.min(u,f.length-E));return y>0&&(g().set(f.subarray(E,E+y),h),E+=y),y},trap(){throw new B(1)},abort(){throw new B(1)},alert(){},evaluate(l,h){let u=d(l,h);(0,globalThis.eval)(u)},open(){},time_now:()=>BigInt(Date.now()),tick_now:()=>globalThis.performance?.now?.()??Date.now(),time_sleep(){},sqrt:Math.sqrt,sin:Math.sin,cos:Math.cos,pow:Math.pow,fmuladd:(l,h,u)=>l*h+u,ln:Math.log,exp:Math.exp,ldexp:(l,h)=>l*2**h,rand_bytes(l,h){g().set(globalThis.crypto.getRandomValues(new Uint8Array(h)),l)}};return{imports:{env:nt({}),odin_env:nt(_),odin_dom:S,webgl:S,webgl2:S},setMemory(l){w=l},args:e}}var J="/odin",it="/out/main.wasm",v=["wasi_wasm32","js_wasm32"],Ot=new TextEncoder,ot=()=>{let t=[],e=[],n=[];return{stdout:c=>{t.push(c),n.push(c)},stderr:c=>{e.push(c),n.push(c)},stdoutText:()=>t.join(""),stderrText:()=>e.join(""),outputText:()=>n.join("")}},Rt=t=>t.split(`
3
+ `).map(e=>e.trimEnd()).filter(e=>e.trim()!=="");function ct({compilerModule:t,libraryEntries:e}){let n=()=>{let g=new $;for(let[d,S]of e)g.writeFile(`${J}/${d}`,S);return g.mkdirAll(`${J}/vendor`),g},c=({code:g,target:d,compileArgs:S,fileName:_})=>{let l=n();l.writeFile(`/src/${_}`,Ot.encode(g)),l.mkdirAll("/out");let h=[],u=[],y=["odin","build","/src",`-out:${it}`,"-backend:wasm",`-target:${d}`,...S],k=1,R="";try{k=q(t,X(l,{args:y,env:[`ODIN_ROOT=${J}`],stdout:A=>h.push(A),stderr:A=>u.push(A)}))}catch(A){R=A?.message??String(A)}return{exitCode:k,artifact:k===0?l.readFile(it):null,diagnostics:Rt(h.join("")+u.join("")),crash:R}},f=(g,{input:d,args:S})=>{let _=ot(),l=0;try{l=q(new WebAssembly.Module(g.slice()),X(new $,{args:["main.wasm",...S],stdin:d,stdout:_.stdout,stderr:_.stderr}))}catch(h){h instanceof N?l=h.code:(_.stderr(`The program trapped: ${h?.message??h}
4
+ `),l=1)}return{exitCode:l,...at(_)}},E=async(g,{input:d,args:S})=>{let _=ot(),l=st({input:d,args:["main.wasm",...S],stdout:_.stdout,stderr:_.stderr}),h=0;try{let{instance:u}=await WebAssembly.instantiate(g.slice(),l.imports);l.setMemory(u.exports.memory),u.exports._start&&u.exports._start(),u.exports._end&&u.exports._end()}catch(u){u instanceof B?h=u.code:(_.stderr(`The program trapped: ${u?.message??u}
5
+ `),h=1)}return{exitCode:h,...at(_)}};return{compileAndRun:async({code:g,input:d="",target:S,args:_=[],compileArgs:l=[],fileName:h="main.odin"})=>{let u=M(),y=c({code:g,target:S,compileArgs:[...l],fileName:h}),k=Math.round(M()-u);if(y.artifact===null||y.artifact.length===0)return{stdout:"",stderr:"",output:"",errors:y.crash?[y.crash]:y.diagnostics.length>0?y.diagnostics:[`The compiler produced no module (exit code ${y.exitCode}).`],exitCode:null,compileMs:k,runMs:null};let R=M(),A;try{A=S==="js_wasm32"?await E(y.artifact,{input:d,args:_}):f(y.artifact,{input:d,args:_})}catch(D){A={exitCode:1,stdout:"",stderr:`The program could not run: ${D?.message??D}
6
+ `,output:""},A.output=A.stderr}let z=Math.round(M()-R);return{stdout:A.stdout,stderr:A.stderr,output:A.output,errors:[],exitCode:A.exitCode,compileMs:k,runMs:z}}}}var at=t=>({stdout:t.stdoutText(),stderr:t.stderrText(),output:t.outputText()}),M=()=>globalThis.performance?.now?.()??Date.now();var dt="ODINPOC1";function ut(t){let e=t instanceof Uint8Array?t:new Uint8Array(t);if(new TextDecoder().decode(e.subarray(0,8))!==dt)throw new Error(`The standard library pack is not a ${dt} container.`);let n=new DataView(e.buffer,e.byteOffset,e.byteLength),c=new TextDecoder,f=n.getUint32(8,!0),E=[],w=12;for(let g=0;g<f;g++){let d=n.getUint32(w,!0);w+=4;let S=c.decode(e.subarray(w,w+d));w+=d;let _=n.getUint32(w,!0);w+=4,E.push([S,e.subarray(w,w+_)]),w+=_}return E}var P=new Map;async function Dt(t,e){let n=g=>(d,S)=>e?.({name:g,loaded:d,total:S}),c=await t.readAsset("odin.wasm",n("odin.wasm")),f=await t.readAsset("odin-src.pack.gz",n("odin-src.pack.gz")),E=await WebAssembly.compile(c),w=ut(await rt(f));return ct({compilerModule:E,libraryEntries:w})}function lt(t,e){let n=P.get(t.key);return n===void 0&&(n={key:t.key,refs:0,promise:Dt(t,e)},P.set(t.key,n),n.promise.catch(()=>P.delete(t.key))),n.refs+=1,n}function K(t){t.refs-=1,t.refs<=0&&P.delete(t.key)}var Nt={wasi:"wasi_wasm32","wasi-wasm32":"wasi_wasm32",js:"js_wasm32","js-wasm32":"js_wasm32"};function ft(t,e){let n=t??e;if(n==null)return"wasi_wasm32";let c=Nt[n]??n;if(!v.includes(c))throw new Error(`Unknown target "${n}". Expected one of ${v.join(", ")}.`);return c}function Et({packaged:t}){async function e(n={}){let c=ft(n.target),f=tt(n,t),E=lt(f,n.onProgress),w;try{w=await E.promise}catch(S){throw K(E),S}let g={target:c,args:n.args??[],compileArgs:n.compileArgs??[],fileName:n.fileName??"main.odin"},d=!1;return{get target(){return g.target},get targets(){return[...v]},async run(S,_,l={}){if(d)throw new Error("This compiler has been disposed.");if(typeof S!="string")throw new Error("run() needs the program source as its first argument.");return w.compileAndRun({code:S,input:_??"",target:ft(l.target,g.target),args:l.args??g.args,compileArgs:[...g.compileArgs,...l.compileArgs??[]],fileName:l.fileName??g.fileName})},dispose(){d||(d=!0,K(E))}}}return{createCompiler:e,TARGETS:v}}var ht=Et({packaged:null}),Bt=ht.createCompiler,{TARGETS:Lt}=ht;return _t(Yt);})();
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "@live-codes/odin-wasm",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Run Odin in the browser with one API, on the Odin compiler compiled to WebAssembly",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "node": "./src/index.node.js",
13
+ "browser": "./src/index.js",
14
+ "default": "./src/index.js"
15
+ },
16
+ "./iife": "./dist/odin-wasm.global.js"
17
+ },
18
+ "bin": {
19
+ "odin-wasm-copy-assets": "./bin/copy-assets.mjs"
20
+ },
21
+ "files": [
22
+ "src",
23
+ "bin",
24
+ "scripts",
25
+ "dist",
26
+ "assets",
27
+ "THIRD-PARTY-NOTICES.md"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "scripts": {
33
+ "sync-assets": "node scripts/sync-assets.mjs",
34
+ "build:iife": "node scripts/build-iife.mjs",
35
+ "check-package": "node scripts/check-package.mjs",
36
+ "prepack": "node scripts/check-package.mjs",
37
+ "check": "node --check src/api.js && node --check src/assets.js && node --check src/asset-receipts.js && node --check src/engine.js && node --check src/host-js.js && node --check src/pack.js && node --check src/runtime.js && node --check src/wasi-host.js && node --check src/index.js && node --check src/index.node.js && node --check src/packaged.node.js && node --check bin/copy-assets.mjs && node --check scripts/sync-assets.mjs && node --check scripts/build-iife.mjs && node --check scripts/check-package.mjs",
38
+ "test": "node --test \"test/*.test.js\""
39
+ },
40
+ "devDependencies": {
41
+ "esbuild": "^0.25.0"
42
+ },
43
+ "keywords": [
44
+ "odin",
45
+ "wasm",
46
+ "webassembly",
47
+ "compiler",
48
+ "playground",
49
+ "livecodes"
50
+ ]
51
+ }
@@ -0,0 +1,33 @@
1
+ // Builds the IIFE bundle: one classic script, for workers and pages that cannot
2
+ // use ES modules.
3
+ //
4
+ // npm run build:iife
5
+ //
6
+ // The output is committed, because a classic worker can only `importScripts()`
7
+ // a URL and a consumer should not need a bundler to get one. It is built from
8
+ // the browser entry, so it never pulls in the Node-only packaged-assets code.
9
+ import { build } from 'esbuild';
10
+ import { statSync } from 'node:fs';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const BUNDLE = {
14
+ entry: '../src/index.js',
15
+ outfile: '../dist/odin-wasm.global.js',
16
+ globalName: 'odinWasm',
17
+ banner: `/*! @live-codes/odin-wasm - MIT. IIFE build, sets self.odinWasm.
18
+ * importScripts('odin-wasm.global.js') then self.odinWasm.createCompiler({ baseUrl }).
19
+ * The compiler assets it fetches are Odin's, under the zlib license. */`
20
+ };
21
+
22
+ const out = fileURLToPath(new URL(BUNDLE.outfile, import.meta.url));
23
+ await build({
24
+ entryPoints: [fileURLToPath(new URL(BUNDLE.entry, import.meta.url))],
25
+ outfile: out,
26
+ bundle: true,
27
+ format: 'iife',
28
+ globalName: BUNDLE.globalName,
29
+ minify: true,
30
+ platform: 'browser',
31
+ target: 'es2022'
32
+ });
33
+ console.log(`${BUNDLE.outfile.replace('../', '')} ${(statSync(out).size / 1024).toFixed(1)} KB (minified)`);
@@ -0,0 +1,43 @@
1
+ // Refuses to pack a package that cannot work.
2
+ //
3
+ // npm run check-package (also the `prepack` hook, so `npm publish` runs it)
4
+ //
5
+ // The assets are committed, so they can go stale or be deleted without anything in the tests
6
+ // noticing — the tests read whatever is there. This checks that every asset the receipts name is
7
+ // present and hashes to its receipt, and that the built bundle exists, before anything is published.
8
+ import { createHash } from 'node:crypto';
9
+ import { existsSync } from 'node:fs';
10
+ import { readFile } from 'node:fs/promises';
11
+ import { ASSET_RECEIPTS } from '../src/asset-receipts.js';
12
+
13
+ const ASSETS = new URL('../assets/', import.meta.url);
14
+ const BUNDLE = new URL('../dist/odin-wasm.global.js', import.meta.url);
15
+
16
+ const problems = [];
17
+
18
+ for (const [name, receipt] of Object.entries(ASSET_RECEIPTS)) {
19
+ const url = new URL(name, ASSETS);
20
+ if (!existsSync(url)) {
21
+ problems.push(`missing asset: assets/${name}`);
22
+ continue;
23
+ }
24
+ const bytes = await readFile(url);
25
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
26
+ if (bytes.length !== receipt.bytes || sha256 !== receipt.sha256) {
27
+ problems.push(`assets/${name} does not match its receipt (run \`npm run sync-assets\`)`);
28
+ }
29
+ }
30
+
31
+ if (!existsSync(BUNDLE)) {
32
+ problems.push('missing dist/odin-wasm.global.js (run `npm run build:iife`)');
33
+ }
34
+
35
+ if (problems.length > 0) {
36
+ console.error('Refusing to pack:');
37
+ for (const problem of problems) console.error(` ${problem}`);
38
+ process.exit(1);
39
+ }
40
+
41
+ console.log(
42
+ `Ready to pack: ${Object.keys(ASSET_RECEIPTS).length} assets verified, the bundle is present.`
43
+ );
@@ -0,0 +1,55 @@
1
+ // Copies the assets the repository builds into this package, and rewrites
2
+ // `src/asset-receipts.js` to pin them.
3
+ //
4
+ // npm run sync-assets (from this package)
5
+ // npm run assets:package (from the repository root, which builds first)
6
+ //
7
+ // The repository's `scripts/build-assets.sh` builds `assets/odin.wasm` and
8
+ // `assets/odin-src.pack.gz` at the root; a package has to carry its own copy, so
9
+ // this is the one step between them.
10
+ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
11
+ import { createHash } from 'node:crypto';
12
+
13
+ const ROOT_ASSETS = new URL('../../../assets/', import.meta.url);
14
+ const PACKAGE_ASSETS = new URL('../assets/', import.meta.url);
15
+ const RECEIPTS = new URL('../src/asset-receipts.js', import.meta.url);
16
+
17
+ // Loaded in this order, and the order is the load order.
18
+ const ASSET_NAMES = ['odin.wasm', 'odin-src.pack.gz'];
19
+
20
+ await mkdir(PACKAGE_ASSETS, { recursive: true });
21
+
22
+ const receipts = [];
23
+ for (const name of ASSET_NAMES) {
24
+ await copyFile(new URL(name, ROOT_ASSETS), new URL(name, PACKAGE_ASSETS));
25
+ const bytes = await readFile(new URL(name, PACKAGE_ASSETS));
26
+ receipts.push({
27
+ name,
28
+ bytes: bytes.length,
29
+ sha256: createHash('sha256').update(bytes).digest('hex'),
30
+ });
31
+ }
32
+
33
+ const entries = receipts
34
+ .map(({ name, bytes, sha256 }) => `\t'${name}': Object.freeze({ bytes: ${bytes}, sha256: '${sha256}' })`)
35
+ .join(',\n');
36
+
37
+ const body = `// Receipts for the assets this package ships in \`assets/\`, keyed by their path under it.
38
+ //
39
+ // Generated by scripts/sync-assets.mjs — do not edit by hand.
40
+ //
41
+ // Every read is checked against one of these before the bytes are used, so a truncated download or
42
+ // a host serving something else is an error here rather than a wasm instantiation failure a step
43
+ // later.
44
+
45
+ export const ASSET_RECEIPTS = Object.freeze({
46
+ ${entries}
47
+ });
48
+ `;
49
+
50
+ await writeFile(RECEIPTS, body);
51
+
52
+ for (const { name, bytes } of receipts) {
53
+ console.log(` ${name} ${bytes} bytes`);
54
+ }
55
+ console.log(`Wrote assets/ and src/asset-receipts.js`);
package/src/api.js ADDED
@@ -0,0 +1,117 @@
1
+ import { resolveAssetSource } from './assets.js';
2
+ import { TARGETS } from './engine.js';
3
+ import { acquireEngine, releaseEngine } from './runtime.js';
4
+
5
+ // One implementation, two entry points: `index.js` for anything without a
6
+ // filesystem, and `index.node.js` for Node, which can also read the assets that
7
+ // ship in this package.
8
+
9
+ const TARGET_ALIASES = {
10
+ wasi: 'wasi_wasm32',
11
+ 'wasi-wasm32': 'wasi_wasm32',
12
+ js: 'js_wasm32',
13
+ 'js-wasm32': 'js_wasm32',
14
+ };
15
+
16
+ function resolveTarget(value, fallback) {
17
+ const target = value ?? fallback;
18
+ if (target == null) return 'wasi_wasm32';
19
+ const resolved = TARGET_ALIASES[target] ?? target;
20
+ if (!TARGETS.includes(resolved)) {
21
+ throw new Error(`Unknown target "${target}". Expected one of ${TARGETS.join(', ')}.`);
22
+ }
23
+ return resolved;
24
+ }
25
+
26
+ export function createApi({ packaged }) {
27
+ /**
28
+ * Create an Odin compiler.
29
+ *
30
+ * The heavy part — the compiler module and the packed standard library — is
31
+ * fetched here, so a bad `baseUrl` fails at this point rather than at the
32
+ * first run. Sources that resolve to the same assets share one engine, so two
33
+ * compilers against the same `baseUrl` load the bytes once.
34
+ *
35
+ * @param {object} [options]
36
+ * @param {string} [options.baseUrl] - where the assets are served from. In
37
+ * Node this may be omitted to use the files that ship in this package;
38
+ * anywhere else it is required.
39
+ * @param {'wasi_wasm32'|'js_wasm32'} [options.target] - what the program is
40
+ * compiled for. `wasi_wasm32` (the default) runs anywhere; `js_wasm32` is
41
+ * the browser target, and outside a page only its console output works.
42
+ * @param {string[]} [options.args] - default program argv.
43
+ * @param {string[]} [options.compileArgs] - extra compiler flags.
44
+ * @param {string} [options.fileName] - the name the source is compiled under.
45
+ * @param {(progress: {name: string, loaded: number, total: number}) => void} [options.onProgress]
46
+ */
47
+ async function createCompiler(options = {}) {
48
+ const target = resolveTarget(options.target);
49
+ const source = resolveAssetSource(options, packaged);
50
+ const entry = acquireEngine(source, options.onProgress);
51
+
52
+ let engine;
53
+ try {
54
+ engine = await entry.promise;
55
+ } catch (error) {
56
+ releaseEngine(entry);
57
+ throw error;
58
+ }
59
+
60
+ const defaults = {
61
+ target,
62
+ args: options.args ?? [],
63
+ compileArgs: options.compileArgs ?? [],
64
+ fileName: options.fileName ?? 'main.odin',
65
+ };
66
+ let disposed = false;
67
+
68
+ return {
69
+ /** The target this compiler builds for, by default. */
70
+ get target() {
71
+ return defaults.target;
72
+ },
73
+
74
+ /** Every target the package can run, for building a picker. */
75
+ get targets() {
76
+ return [...TARGETS];
77
+ },
78
+
79
+ /**
80
+ * Compile and run a program.
81
+ *
82
+ * @param {string} code - the Odin source.
83
+ * @param {string|Uint8Array} [input] - stdin, handed to the program once.
84
+ * @param {object} [runOptions] - per-run `target`, `args`, `compileArgs`,
85
+ * `fileName`.
86
+ * @returns {Promise<{stdout: string, stderr: string, output: string,
87
+ * errors: string[], exitCode: number|null, compileMs: number,
88
+ * runMs: number|null}>} `output` is stdout and stderr in the order the
89
+ * program wrote them; `errors` holds the compiler's diagnostics and is
90
+ * empty when it compiled; `exitCode` is null when the program never ran.
91
+ */
92
+ async run(code, input, runOptions = {}) {
93
+ if (disposed) throw new Error('This compiler has been disposed.');
94
+ if (typeof code !== 'string') {
95
+ throw new Error('run() needs the program source as its first argument.');
96
+ }
97
+ return engine.compileAndRun({
98
+ code,
99
+ input: input ?? '',
100
+ target: resolveTarget(runOptions.target, defaults.target),
101
+ args: runOptions.args ?? defaults.args,
102
+ compileArgs: [...defaults.compileArgs, ...(runOptions.compileArgs ?? [])],
103
+ fileName: runOptions.fileName ?? defaults.fileName,
104
+ });
105
+ },
106
+
107
+ /** Drop this compiler's hold on the shared engine. Further runs throw. */
108
+ dispose() {
109
+ if (disposed) return;
110
+ disposed = true;
111
+ releaseEngine(entry);
112
+ },
113
+ };
114
+ }
115
+
116
+ return { createCompiler, TARGETS };
117
+ }