@onda-lang/wasm-compiler 0.5.0-rc.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 Francesco Cameli
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 ADDED
@@ -0,0 +1,131 @@
1
+ # Onda WebAssembly compiler
2
+
3
+ `@onda-lang/wasm-compiler` compiles Onda source or an in-memory multi-file project to a complete,
4
+ self-contained WebAssembly processor artifact. It runs in modern browsers and Node.js and does not
5
+ require LLVM, a Wasm linker, Rust, or `wasm-pack` after installation.
6
+
7
+ ```js
8
+ import { createCompiler } from "@onda-lang/wasm-compiler";
9
+
10
+ const compiler = await createCompiler();
11
+ const artifact = await compiler.compileSource(source, {
12
+ sampleRate: 48_000,
13
+ blockSize: 128,
14
+ });
15
+
16
+ console.log(artifact.wasm, artifact.metadata);
17
+ ```
18
+
19
+ The package composes Onda's embedded Rust frontend with its Binaryen backend. The frontend emits
20
+ validated versioned MIR in memory; the backend lowers that trusted producer output to the generic
21
+ Onda WebAssembly processor ABI. The package verifies the MIR schema handshake during startup.
22
+
23
+ Release builds post-optimize the Rust frontend Wasm with the package's pinned Binaryen
24
+ `wasm-opt -O4`. The build records input/output sizes and the optimizer version in
25
+ `dist/build.json`, and fails if the pass does not reduce the shipped module. Generated DSP modules
26
+ are already optimized independently by the runtime Binaryen O4 pipeline.
27
+
28
+ ## Projects
29
+
30
+ Project compilation resolves imports and includes entirely from the supplied source map and the
31
+ embedded standard library:
32
+
33
+ ```js
34
+ const artifact = await compiler.compileProject({
35
+ entry: "main.onda",
36
+ sources: {
37
+ "main.onda": mainSource,
38
+ "dsp/filter.onda": filterSource,
39
+ },
40
+ }, {
41
+ sampleRate: 48_000,
42
+ blockSize: 128,
43
+ });
44
+ ```
45
+
46
+ Compilation failures throw `OndaCompileError`. Its `diagnostics` property contains structured
47
+ parse, semantic, MIR, configuration, or code-generation diagnostics.
48
+
49
+ ## Browser workers
50
+
51
+ Compilation is CPU-intensive. Use the built-in worker client in interactive browser applications:
52
+
53
+ ```js
54
+ const compiler = await createCompiler({ worker: true });
55
+ const artifact = await compiler.compileSource(source, options);
56
+ await compiler.dispose();
57
+ ```
58
+
59
+ Static hosts and bundlers may provide explicit `workerUrl` and `frontendWasm` URLs. The worker
60
+ receives the frontend URL during initialization, so versioned or content-hashed compiler assets do
61
+ not depend on the package's development directory layout.
62
+
63
+ In worker mode the page-side entry point stays lightweight; the Rust frontend Wasm, Binaryen, and
64
+ the MIR backend are loaded only inside the worker.
65
+
66
+ The package also exports `@onda-lang/wasm-compiler/worker` for hosts that want to own the worker
67
+ protocol directly.
68
+
69
+ ## Browser LSP
70
+
71
+ The frontend Wasm contains the same transport-neutral language server used by `onda lsp`. Worker
72
+ clients send ordinary JSON-RPC/LSP messages and receive every response or notification emitted for
73
+ that message:
74
+
75
+ ```js
76
+ await compiler.setLspAnalysisOptions({ sampleRate: 48_000, blockSize: 256 });
77
+
78
+ const [initialized] = await compiler.sendLspMessage({
79
+ jsonrpc: "2.0",
80
+ id: 1,
81
+ method: "initialize",
82
+ params: { processId: null, capabilities: {} },
83
+ });
84
+
85
+ await compiler.sendLspMessage({
86
+ jsonrpc: "2.0",
87
+ method: "textDocument/didOpen",
88
+ params: {
89
+ textDocument: {
90
+ uri: "file:///onda-project/main.onda",
91
+ languageId: "onda",
92
+ version: 1,
93
+ text: source,
94
+ },
95
+ },
96
+ });
97
+ ```
98
+
99
+ Open every virtual project file with `didOpen`; imports and includes resolve from those overlays and
100
+ the embedded standard library. Diagnostics, semantic tokens, completion, hover, definitions, and
101
+ document symbols use the native server implementation. The browser transport does not run MIR or a
102
+ backend until the host explicitly calls `compileSource` or `compileProject`.
103
+
104
+ ## CLI
105
+
106
+ The npm package installs `onda-wasm`:
107
+
108
+ ```sh
109
+ onda-wasm compile ./main.onda --root . --output ./dist/main.wasm
110
+ ```
111
+
112
+ The command recursively collects `.onda` files below `--root`, compiles the requested entry, and
113
+ writes an integrity-associated `main.wasm` plus `main.onda.json` descriptor. Use `--help` for code
114
+ generation and output options.
115
+
116
+ ## Artifact hosting
117
+
118
+ The compiler returns the generic processor artifact. Web Audio is optional; use
119
+ `@onda-lang/webaudio` when an `AudioWorklet` host is desired. Build-time users should publish only
120
+ the generated `.wasm` and `.onda.json`, not this compiler package or Binaryen.
121
+
122
+ The low-level `@onda-lang/binaryen-web` package remains available for advanced consumers that
123
+ already produce compatible Onda MIR.
124
+
125
+ ## Versioning
126
+
127
+ `[workspace.package].version` in the repository's top-level `Cargo.toml` is the only authored Onda
128
+ version. Run `scripts/sync-versions.sh` or `scripts/sync-versions.ps1` after changing it.
129
+ The underlying `scripts/sync-package-versions.mjs` updates the workspace-owned `Cargo.lock` entries,
130
+ discovers every `@onda-lang/*` package, and synchronizes its manifest and lockfile. Compiler builds
131
+ and release jobs run it automatically; `ONDA_VERSION` is generated into the packaged distribution.
@@ -0,0 +1,11 @@
1
+ # Third-party notices
2
+
3
+ The packaged compiler includes the following third-party components:
4
+
5
+ - Binaryen.js 130.0.0, licensed under Apache-2.0. The complete license is shipped as
6
+ `dist/licenses/BINARYEN-LICENSE`.
7
+ - The Onda WebAssembly math kernel uses `libm` 0.2.16, licensed under MIT. The complete license is
8
+ shipped as `dist/licenses/LIBM-LICENSE`.
9
+
10
+ The generated processor artifacts contain only the required closure of the math kernel. They do
11
+ not contain Binaryen itself.
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
4
+ import { dirname, extname, isAbsolute, parse, relative, resolve, sep } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ import {
8
+ OndaCompileError,
9
+ createCompiler,
10
+ createProcessorArtifactFiles,
11
+ } from "../src/index.js";
12
+ import { MAX_BLOCK_SIZE } from "../src/config.js";
13
+
14
+ const HELP = `Usage:
15
+ onda-wasm compile <input.onda> [options]
16
+
17
+ Options:
18
+ --root <directory> Project root used to collect .onda sources
19
+ --output, -o <file> Output Wasm path (default: <input>.wasm)
20
+ --meta-out <file> Output descriptor path (default: <output>.onda.json)
21
+ --sample-rate <number> Compile-time sample rate (default: 48000)
22
+ --block-size <integer> Compile-time block size (default: 128)
23
+ --optimize-level <0..4> Binaryen optimization level (default: 4)
24
+ --shrink-level <0..2> Binaryen shrink level (default: 0)
25
+ --fast-math Enable relaxed floating-point rewrites
26
+ --no-simd Disable WebAssembly SIMD code generation
27
+ --wat-out <file> Also write WebAssembly text format
28
+ --help, -h Show this help
29
+ --version, -V Show the Onda compiler version
30
+ `;
31
+
32
+ export async function main(argv = process.argv.slice(2)) {
33
+ if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
34
+ process.stdout.write(HELP);
35
+ return 0;
36
+ }
37
+ if (argv.includes("--version") || argv.includes("-V")) {
38
+ const manifest = JSON.parse(
39
+ await readFile(new URL("../package.json", import.meta.url), "utf8"),
40
+ );
41
+ process.stdout.write(`${manifest.version}\n`);
42
+ return 0;
43
+ }
44
+ if (argv[0] !== "compile") {
45
+ throw new Error(`unknown command '${argv[0]}'\n\n${HELP}`);
46
+ }
47
+
48
+ const parsed = parseCompileArguments(argv.slice(1));
49
+ const input = resolve(parsed.input);
50
+ const root = resolve(parsed.root ?? dirname(input));
51
+ const entryRelative = relative(root, input);
52
+ if (entryRelative === "" || entryRelative === ".") {
53
+ throw new Error("input must identify an .onda file below the project root");
54
+ }
55
+ if (entryRelative === ".." || entryRelative.startsWith(`..${sep}`) || isAbsolute(entryRelative)) {
56
+ throw new Error(`input '${input}' is outside project root '${root}'`);
57
+ }
58
+ if (extname(input) !== ".onda") {
59
+ throw new Error("input must have an .onda extension");
60
+ }
61
+
62
+ const sources = await collectSources(root);
63
+ const entry = portablePath(entryRelative);
64
+ if (!(entry in sources)) {
65
+ throw new Error(`input '${entry}' was not found while collecting project sources`);
66
+ }
67
+
68
+ const output = normalizeWasmOutput(parsed.output ?? resolve(dirname(input), parse(input).name));
69
+ const metadataOutput = resolve(
70
+ parsed.metaOut ?? output.slice(0, -".wasm".length) + ".onda.json",
71
+ );
72
+ const watOutput = parsed.watOut === undefined ? undefined : resolve(parsed.watOut);
73
+ const compiler = await createCompiler();
74
+ try {
75
+ const artifact = await compiler.compileProject({ entry, sources }, {
76
+ sampleRate: parsed.sampleRate,
77
+ blockSize: parsed.blockSize,
78
+ codegen: {
79
+ optimizeLevel: parsed.optimizeLevel,
80
+ shrinkLevel: parsed.shrinkLevel,
81
+ fastMath: parsed.fastMath,
82
+ simd: parsed.simd,
83
+ emitText: watOutput !== undefined,
84
+ },
85
+ });
86
+ const files = await createProcessorArtifactFiles(artifact, {
87
+ baseName: parse(output).name,
88
+ });
89
+ await mkdir(dirname(output), { recursive: true });
90
+ await mkdir(dirname(metadataOutput), { recursive: true });
91
+ if (watOutput !== undefined) await mkdir(dirname(watOutput), { recursive: true });
92
+ const writes = [
93
+ writeFile(output, files.wasm.bytes),
94
+ writeFile(metadataOutput, files.metadata.text),
95
+ ];
96
+ if (watOutput !== undefined) {
97
+ if (typeof artifact.wat !== "string") {
98
+ throw new Error("Binaryen did not return the requested WebAssembly text");
99
+ }
100
+ writes.push(writeFile(watOutput, artifact.wat));
101
+ }
102
+ await Promise.all(writes);
103
+ process.stdout.write(`Wrote WebAssembly: ${output}\n`);
104
+ process.stdout.write(`Wrote descriptor: ${metadataOutput}\n`);
105
+ if (watOutput !== undefined) process.stdout.write(`Wrote WebAssembly text: ${watOutput}\n`);
106
+ return 0;
107
+ } finally {
108
+ await compiler.dispose();
109
+ }
110
+ }
111
+
112
+ function parseCompileArguments(args) {
113
+ let input;
114
+ const result = {
115
+ root: undefined,
116
+ output: undefined,
117
+ metaOut: undefined,
118
+ sampleRate: 48_000,
119
+ blockSize: 128,
120
+ optimizeLevel: 4,
121
+ shrinkLevel: 0,
122
+ fastMath: false,
123
+ simd: true,
124
+ watOut: undefined,
125
+ };
126
+ for (let index = 0; index < args.length; index += 1) {
127
+ const argument = args[index];
128
+ if (!argument.startsWith("-")) {
129
+ if (input !== undefined) throw new Error(`unexpected argument '${argument}'`);
130
+ input = argument;
131
+ continue;
132
+ }
133
+ switch (argument) {
134
+ case "--root":
135
+ result.root = requiredValue(args, ++index, argument);
136
+ break;
137
+ case "--output":
138
+ case "-o":
139
+ result.output = requiredValue(args, ++index, argument);
140
+ break;
141
+ case "--meta-out":
142
+ result.metaOut = requiredValue(args, ++index, argument);
143
+ break;
144
+ case "--sample-rate":
145
+ result.sampleRate = numberValue(args, ++index, argument);
146
+ break;
147
+ case "--block-size":
148
+ result.blockSize = integerValue(args, ++index, argument, 1, MAX_BLOCK_SIZE);
149
+ break;
150
+ case "--optimize-level":
151
+ result.optimizeLevel = integerValue(args, ++index, argument, 0, 4);
152
+ break;
153
+ case "--shrink-level":
154
+ result.shrinkLevel = integerValue(args, ++index, argument, 0, 2);
155
+ break;
156
+ case "--fast-math":
157
+ result.fastMath = true;
158
+ break;
159
+ case "--no-simd":
160
+ result.simd = false;
161
+ break;
162
+ case "--wat-out":
163
+ result.watOut = requiredValue(args, ++index, argument);
164
+ break;
165
+ default:
166
+ throw new Error(`unknown option '${argument}'`);
167
+ }
168
+ }
169
+ if (input === undefined) throw new Error("compile requires an input .onda file");
170
+ return { input, ...result };
171
+ }
172
+
173
+ async function collectSources(root) {
174
+ const sources = Object.create(null);
175
+ await visit(root);
176
+ return sources;
177
+
178
+ async function visit(directory) {
179
+ const entries = await readdir(directory, { withFileTypes: true });
180
+ entries.sort((left, right) => left.name.localeCompare(right.name));
181
+ for (const entry of entries) {
182
+ if (entry.isDirectory()) {
183
+ if ([".git", "node_modules", "target"].includes(entry.name)) continue;
184
+ await visit(resolve(directory, entry.name));
185
+ } else if (entry.isFile() && extname(entry.name) === ".onda") {
186
+ const path = resolve(directory, entry.name);
187
+ sources[portablePath(relative(root, path))] = await readFile(path, "utf8");
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ function normalizeWasmOutput(output) {
194
+ const resolved = resolve(output);
195
+ return extname(resolved) === ".wasm" ? resolved : `${resolved}.wasm`;
196
+ }
197
+
198
+ function portablePath(path) {
199
+ return path.split(sep).join("/");
200
+ }
201
+
202
+ function requiredValue(args, index, option) {
203
+ const value = args[index];
204
+ if (value === undefined || value.startsWith("-")) {
205
+ throw new Error(`${option} requires a value`);
206
+ }
207
+ return value;
208
+ }
209
+
210
+ function numberValue(args, index, option) {
211
+ const value = Number(requiredValue(args, index, option));
212
+ if (!Number.isFinite(value) || value <= 0) {
213
+ throw new Error(`${option} requires a finite number greater than zero`);
214
+ }
215
+ return value;
216
+ }
217
+
218
+ function integerValue(args, index, option, minimum, maximum) {
219
+ const value = Number(requiredValue(args, index, option));
220
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
221
+ throw new Error(`${option} requires an integer from ${minimum} to ${maximum}`);
222
+ }
223
+ return value;
224
+ }
225
+
226
+ function printError(error) {
227
+ if (error instanceof OndaCompileError) {
228
+ for (const diagnostic of error.diagnostics) {
229
+ const location = diagnostic.file
230
+ ? `${diagnostic.file}:${diagnostic.line}:${diagnostic.column}: `
231
+ : "";
232
+ process.stderr.write(`${location}[${diagnostic.stage}] ${diagnostic.message}\n`);
233
+ }
234
+ return;
235
+ }
236
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
237
+ }
238
+
239
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
240
+ main().then(
241
+ (status) => {
242
+ process.exitCode = status;
243
+ },
244
+ (error) => {
245
+ printError(error);
246
+ process.exitCode = 1;
247
+ },
248
+ );
249
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@onda-lang/wasm-compiler",
3
+ "version": "0.5.0-rc.0",
4
+ "type": "module",
5
+ "description": "Onda source-to-WebAssembly compiler for browsers and Node.js",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/onda-lang/onda.git",
10
+ "directory": "packages/onda_wasm_compiler"
11
+ },
12
+ "homepage": "https://onda-lang.github.io/onda/",
13
+ "bugs": {
14
+ "url": "https://github.com/onda-lang/onda/issues"
15
+ },
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "types": "./src/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./src/index.d.ts",
23
+ "default": "./src/index.js"
24
+ },
25
+ "./artifact": {
26
+ "types": "./src/artifact.d.ts",
27
+ "default": "./src/artifact.js"
28
+ },
29
+ "./worker": {
30
+ "types": "./src/worker.d.ts",
31
+ "default": "./src/worker.js"
32
+ }
33
+ },
34
+ "imports": {
35
+ "#onda-frontend-loader": {
36
+ "node": "./src/frontend-node.js",
37
+ "default": "./src/frontend-browser.js"
38
+ }
39
+ },
40
+ "bin": {
41
+ "onda-wasm": "./bin/onda-wasm.js"
42
+ },
43
+ "files": [
44
+ "bin",
45
+ "dist",
46
+ "src",
47
+ "LICENSE",
48
+ "README.md",
49
+ "THIRD_PARTY_NOTICES.md"
50
+ ],
51
+ "scripts": {
52
+ "build": "node ./scripts/build.mjs",
53
+ "sync:versions": "node ../../scripts/sync-package-versions.mjs",
54
+ "check:versions": "node ../../scripts/sync-package-versions.mjs --check",
55
+ "test": "npm run build && npm run test:built",
56
+ "test:built": "node --test ./test/*.test.js",
57
+ "test:pack": "node ./scripts/test-package.mjs",
58
+ "test:pack:built": "node ./scripts/test-package.mjs --skip-build",
59
+ "prepack": "npm run build"
60
+ },
61
+ "dependencies": {
62
+ "@onda-lang/processor-abi": "0.5.0-rc.0",
63
+ "binaryen": "130.0.0"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public"
67
+ }
68
+ }
@@ -0,0 +1 @@
1
+ export * from "@onda-lang/processor-abi";
@@ -0,0 +1 @@
1
+ export * from "@onda-lang/processor-abi";
package/src/config.js ADDED
@@ -0,0 +1,2 @@
1
+ // Onda's MIR and processor ABI use signed i32 frame indices.
2
+ export const MAX_BLOCK_SIZE = 0x7fff_ffff;
@@ -0,0 +1,6 @@
1
+ export function defaultFrontendInput() {
2
+ return new URL(
3
+ "../dist/frontend/onda_compiler_web_bg.wasm",
4
+ import.meta.url,
5
+ );
6
+ }
@@ -0,0 +1,8 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ export function defaultFrontendInput() {
4
+ return readFile(new URL(
5
+ "../dist/frontend/onda_compiler_web_bg.wasm",
6
+ import.meta.url,
7
+ ));
8
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,119 @@
1
+ export const ONDA_VERSION: string;
2
+ export const MIR_SCHEMA_VERSION: number;
3
+ export {
4
+ OndaArtifactError,
5
+ PROCESSOR_ABI_VERSION,
6
+ PROCESSOR_ARTIFACT_FORMAT,
7
+ PROCESSOR_ARTIFACT_FORMAT_VERSION,
8
+ PROCESSOR_SNAPSHOT_FORMAT_VERSION,
9
+ createProcessorArtifactFiles,
10
+ loadProcessorArtifactFiles,
11
+ parseProcessorMetadata,
12
+ serializeProcessorMetadata,
13
+ validateProcessorArtifact,
14
+ validateProcessorMetadata,
15
+ validateProcessorModule,
16
+ } from "@onda-lang/processor-abi";
17
+ export type {
18
+ OndaProcessorArtifact,
19
+ OndaProcessorMetadata,
20
+ } from "@onda-lang/processor-abi";
21
+ import type { OndaProcessorArtifact } from "@onda-lang/processor-abi";
22
+
23
+ export interface OndaCompilerDiagnostic {
24
+ stage: string;
25
+ code: number;
26
+ message: string;
27
+ file: string | null;
28
+ line: number;
29
+ column: number;
30
+ end_line: number;
31
+ end_column: number;
32
+ trace: string[];
33
+ }
34
+
35
+ export interface OndaCodegenOptions {
36
+ optimize?: boolean;
37
+ optimizeLevel?: 0 | 1 | 2 | 3 | 4;
38
+ shrinkLevel?: 0 | 1 | 2;
39
+ fastMath?: boolean;
40
+ simd?: boolean;
41
+ allowInliningFunctionsWithLoops?: boolean;
42
+ emitText?: boolean;
43
+ }
44
+
45
+ export interface OndaCompileOptions {
46
+ sampleRate?: number;
47
+ blockSize?: number;
48
+ codegen?: OndaCodegenOptions;
49
+ }
50
+
51
+ export interface OndaProject {
52
+ entry: string;
53
+ sources: Record<string, string>;
54
+ }
55
+
56
+ export interface OndaCompilerInstance {
57
+ compileSource(
58
+ source: string,
59
+ options?: OndaCompileOptions,
60
+ ): Promise<OndaProcessorArtifact>;
61
+ compileProject(
62
+ project: OndaProject,
63
+ options?: OndaCompileOptions,
64
+ ): Promise<OndaProcessorArtifact>;
65
+ sendLspMessage(message: OndaLspMessage): Promise<OndaLspMessage[]>;
66
+ setLspAnalysisOptions(options?: OndaCompileOptions): Promise<void>;
67
+ dispose(): Promise<void>;
68
+ }
69
+
70
+ export interface OndaLspMessage {
71
+ jsonrpc: "2.0";
72
+ id?: string | number | null;
73
+ method?: string;
74
+ params?: unknown;
75
+ result?: unknown;
76
+ error?: { code: number; message: string; data?: unknown };
77
+ }
78
+
79
+ export interface DirectCompilerOptions {
80
+ worker?: false;
81
+ frontendWasm?: string | URL | ArrayBuffer | ArrayBufferView | WebAssembly.Module;
82
+ }
83
+
84
+ export interface OndaWorkerLike {
85
+ addEventListener(type: "message" | "error", listener: (event: any) => void): void;
86
+ removeEventListener(type: "message" | "error", listener: (event: any) => void): void;
87
+ postMessage(message: unknown): void;
88
+ terminate(): void;
89
+ }
90
+
91
+ export interface OndaWorkerConstructor {
92
+ new (
93
+ url: string | URL,
94
+ options: { type: "module"; name: string },
95
+ ): OndaWorkerLike;
96
+ }
97
+
98
+ export interface WorkerCompilerOptions {
99
+ worker: true;
100
+ workerUrl?: string | URL;
101
+ frontendWasm?: string | URL | ArrayBuffer | ArrayBufferView | WebAssembly.Module;
102
+ Worker?: OndaWorkerConstructor;
103
+ }
104
+
105
+ export class OndaCompilerError extends Error {
106
+ cause?: unknown;
107
+ }
108
+
109
+ export class OndaCompileError extends OndaCompilerError {
110
+ readonly diagnostics: OndaCompilerDiagnostic[];
111
+ }
112
+
113
+ export class OndaBinaryenError extends Error {}
114
+
115
+ export function createCompiler(
116
+ options?: DirectCompilerOptions | WorkerCompilerOptions,
117
+ ): Promise<OndaCompilerInstance>;
118
+
119
+ export function createDefaultImports(): Record<string, never>;
package/src/index.js ADDED
@@ -0,0 +1,384 @@
1
+ import {
2
+ OndaArtifactError,
3
+ PROCESSOR_ABI_VERSION,
4
+ PROCESSOR_ARTIFACT_FORMAT,
5
+ PROCESSOR_ARTIFACT_FORMAT_VERSION,
6
+ PROCESSOR_SNAPSHOT_FORMAT_VERSION,
7
+ createProcessorArtifactFiles,
8
+ loadProcessorArtifactFiles,
9
+ parseProcessorMetadata,
10
+ serializeProcessorMetadata,
11
+ validateProcessorArtifact,
12
+ validateProcessorMetadata,
13
+ validateProcessorModule,
14
+ } from "@onda-lang/processor-abi";
15
+ import { SUPPORTED_MIR_SCHEMA_VERSION } from "../dist/backend/constants.js";
16
+ import { OndaBinaryenError } from "../dist/backend/errors.js";
17
+ import { defaultFrontendInput } from "#onda-frontend-loader";
18
+ import { ONDA_VERSION } from "../dist/version.js";
19
+ import { MAX_BLOCK_SIZE } from "./config.js";
20
+
21
+ export const MIR_SCHEMA_VERSION = SUPPORTED_MIR_SCHEMA_VERSION;
22
+ export { ONDA_VERSION };
23
+
24
+ export {
25
+ OndaArtifactError,
26
+ OndaBinaryenError,
27
+ PROCESSOR_ABI_VERSION,
28
+ PROCESSOR_ARTIFACT_FORMAT,
29
+ PROCESSOR_ARTIFACT_FORMAT_VERSION,
30
+ PROCESSOR_SNAPSHOT_FORMAT_VERSION,
31
+ createProcessorArtifactFiles,
32
+ loadProcessorArtifactFiles,
33
+ parseProcessorMetadata,
34
+ serializeProcessorMetadata,
35
+ validateProcessorArtifact,
36
+ validateProcessorMetadata,
37
+ validateProcessorModule,
38
+ };
39
+
40
+ export class OndaCompilerError extends Error {
41
+ constructor(message, { cause } = {}) {
42
+ super(message);
43
+ this.name = "OndaCompilerError";
44
+ if (cause !== undefined) this.cause = cause;
45
+ }
46
+ }
47
+
48
+ export class OndaCompileError extends OndaCompilerError {
49
+ constructor(diagnostics, { cause } = {}) {
50
+ const normalized = normalizeDiagnostics(diagnostics);
51
+ const first = normalized[0];
52
+ const message = first
53
+ ? `${first.stage}: ${first.message}`
54
+ : "Onda compilation failed";
55
+ super(message, { cause });
56
+ this.name = "OndaCompileError";
57
+ this.diagnostics = normalized;
58
+ }
59
+ }
60
+
61
+ let toolchainInitialization;
62
+
63
+ class OndaCompiler {
64
+ constructor(frontend, compileTrustedMir) {
65
+ this.frontend = frontend;
66
+ this.compileTrustedMir = compileTrustedMir;
67
+ this.lsp = null;
68
+ }
69
+
70
+ async compileSource(source, options = {}) {
71
+ if (typeof source !== "string") {
72
+ throw configurationError("source must be a string");
73
+ }
74
+ const compile = normalizeCompileOptions(options);
75
+ let mir;
76
+ try {
77
+ mir = this.frontend.compile_to_mir_messagepack(
78
+ source,
79
+ compile.sampleRate,
80
+ compile.blockSize,
81
+ );
82
+ } catch (error) {
83
+ throw diagnosticsFromFrontend(error);
84
+ }
85
+ return compileMirTransport(mir, compile.codegen, this.compileTrustedMir);
86
+ }
87
+
88
+ async compileProject(project, options = {}) {
89
+ if (!project || typeof project !== "object" || Array.isArray(project)) {
90
+ throw configurationError("project must contain an entry and source map");
91
+ }
92
+ if (typeof project.entry !== "string" || project.entry.length === 0) {
93
+ throw configurationError("project.entry must be a non-empty string");
94
+ }
95
+ if (!project.sources || typeof project.sources !== "object" || Array.isArray(project.sources)) {
96
+ throw configurationError("project.sources must be an object of paths to source strings");
97
+ }
98
+ for (const [path, source] of Object.entries(project.sources)) {
99
+ if (typeof source !== "string") {
100
+ throw configurationError(`project source '${path}' must be a string`);
101
+ }
102
+ }
103
+
104
+ const compile = normalizeCompileOptions(options);
105
+ let mir;
106
+ try {
107
+ mir = this.frontend.compile_project_to_mir_messagepack(
108
+ project.entry,
109
+ JSON.stringify(project.sources),
110
+ compile.sampleRate,
111
+ compile.blockSize,
112
+ );
113
+ } catch (error) {
114
+ throw diagnosticsFromFrontend(error);
115
+ }
116
+ return compileMirTransport(mir, compile.codegen, this.compileTrustedMir);
117
+ }
118
+
119
+ async sendLspMessage(message) {
120
+ if (!message || typeof message !== "object" || Array.isArray(message)) {
121
+ throw new OndaCompilerError("LSP message must be a JSON-RPC object");
122
+ }
123
+ this.lsp ??= new this.frontend.OndaLsp();
124
+ try {
125
+ const responses = JSON.parse(this.lsp.handle_message(JSON.stringify(message)));
126
+ if (!Array.isArray(responses)) {
127
+ throw new Error("Onda LSP returned a non-array response batch");
128
+ }
129
+ return responses;
130
+ } catch (cause) {
131
+ throw new OndaCompilerError("Onda LSP failed to handle a message", { cause });
132
+ }
133
+ }
134
+
135
+ async setLspAnalysisOptions(options = {}) {
136
+ const compile = normalizeCompileOptions(options);
137
+ this.lsp ??= new this.frontend.OndaLsp();
138
+ try {
139
+ this.lsp.set_analysis_options(compile.sampleRate, compile.blockSize);
140
+ } catch (cause) {
141
+ throw new OndaCompilerError("failed to configure Onda LSP analysis", { cause });
142
+ }
143
+ }
144
+
145
+ async dispose() {
146
+ this.lsp?.free();
147
+ this.lsp = null;
148
+ }
149
+ }
150
+
151
+ class WorkerOndaCompiler {
152
+ constructor(worker) {
153
+ this.worker = worker;
154
+ this.nextRequestId = 1;
155
+ this.pending = new Map();
156
+ this.onMessage = (event) => this.handleMessage(event.data);
157
+ this.onError = (event) => this.failAll(event.error ?? new Error(event.message));
158
+ worker.addEventListener("message", this.onMessage);
159
+ worker.addEventListener("error", this.onError);
160
+ }
161
+
162
+ initialize(frontendWasm) {
163
+ return this.request("initialize", { frontendWasm });
164
+ }
165
+
166
+ compileSource(source, options = {}) {
167
+ return this.request("compileSource", { source, options });
168
+ }
169
+
170
+ compileProject(project, options = {}) {
171
+ return this.request("compileProject", { project, options });
172
+ }
173
+
174
+ sendLspMessage(message) {
175
+ return this.request("lspMessage", { message });
176
+ }
177
+
178
+ setLspAnalysisOptions(options = {}) {
179
+ return this.request("lspAnalysisOptions", { options });
180
+ }
181
+
182
+ async dispose() {
183
+ try {
184
+ await this.request("dispose");
185
+ } finally {
186
+ this.terminate(new OndaCompilerError("compiler worker was disposed"));
187
+ }
188
+ }
189
+
190
+ terminate(error) {
191
+ this.worker.removeEventListener("message", this.onMessage);
192
+ this.worker.removeEventListener("error", this.onError);
193
+ this.worker.terminate();
194
+ this.failAll(error);
195
+ }
196
+
197
+ request(type, fields = {}) {
198
+ const requestId = this.nextRequestId;
199
+ this.nextRequestId += 1;
200
+ return new Promise((resolve, reject) => {
201
+ this.pending.set(requestId, { resolve, reject });
202
+ this.worker.postMessage({ type, requestId, ...fields });
203
+ });
204
+ }
205
+
206
+ handleMessage(message) {
207
+ const pending = this.pending.get(message?.requestId);
208
+ if (!pending) return;
209
+ this.pending.delete(message.requestId);
210
+ if (message.type === "result") {
211
+ pending.resolve(message.value);
212
+ return;
213
+ }
214
+ const error = message.error?.diagnostics
215
+ ? new OndaCompileError(message.error.diagnostics)
216
+ : new OndaCompilerError(message.error?.message ?? "compiler worker failed");
217
+ if (message.error?.name) error.name = message.error.name;
218
+ if (message.error?.stack) error.stack = message.error.stack;
219
+ pending.reject(error);
220
+ }
221
+
222
+ failAll(error) {
223
+ for (const { reject } of this.pending.values()) reject(error);
224
+ this.pending.clear();
225
+ }
226
+ }
227
+
228
+ export async function createCompiler(options = {}) {
229
+ if (options.worker === true) {
230
+ const WorkerConstructor = options.Worker ?? globalThis.Worker;
231
+ if (typeof WorkerConstructor !== "function") {
232
+ throw new OndaCompilerError(
233
+ "worker compilation requires a browser Worker constructor",
234
+ );
235
+ }
236
+ const workerUrl = options.workerUrl ?? new URL("./worker.js", import.meta.url);
237
+ const compiler = new WorkerOndaCompiler(
238
+ new WorkerConstructor(workerUrl, {
239
+ type: "module",
240
+ name: "onda-wasm-compiler",
241
+ }),
242
+ );
243
+ try {
244
+ await compiler.initialize(options.frontendWasm);
245
+ return compiler;
246
+ } catch (error) {
247
+ compiler.terminate(
248
+ new OndaCompilerError("compiler worker initialization failed", { cause: error }),
249
+ );
250
+ throw error;
251
+ }
252
+ }
253
+
254
+ if (!toolchainInitialization) {
255
+ let initialization;
256
+ initialization = initializeToolchain(options.frontendWasm).catch((error) => {
257
+ if (toolchainInitialization === initialization) {
258
+ toolchainInitialization = undefined;
259
+ }
260
+ throw error;
261
+ });
262
+ toolchainInitialization = initialization;
263
+ }
264
+ const toolchain = await toolchainInitialization;
265
+ return new OndaCompiler(toolchain.frontend, toolchain.compileTrustedMir);
266
+ }
267
+
268
+ async function initializeToolchain(frontendWasm) {
269
+ const [frontend, backend] = await Promise.all([
270
+ import("../dist/frontend/onda_compiler_web.js"),
271
+ import("../dist/backend/index.js"),
272
+ ]);
273
+ const moduleOrPath = frontendWasm ?? await defaultFrontendInput();
274
+ try {
275
+ await frontend.default({ module_or_path: moduleOrPath });
276
+ } catch (cause) {
277
+ throw new OndaCompilerError("failed to initialize the Onda frontend Wasm", { cause });
278
+ }
279
+ const producerSchema = frontend.mir_schema_version();
280
+ if (producerSchema !== SUPPORTED_MIR_SCHEMA_VERSION) {
281
+ throw new OndaCompilerError(
282
+ `MIR schema mismatch: frontend produces ${producerSchema}, Binaryen backend supports ${SUPPORTED_MIR_SCHEMA_VERSION}`,
283
+ );
284
+ }
285
+ return { frontend, compileTrustedMir: backend.compileTrustedMir };
286
+ }
287
+
288
+ export function createDefaultImports() {
289
+ return {};
290
+ }
291
+
292
+ function normalizeCompileOptions(options) {
293
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
294
+ throw configurationError("compiler options must be an object");
295
+ }
296
+ const sampleRate = options.sampleRate ?? 48_000;
297
+ const blockSize = options.blockSize ?? 128;
298
+ if (typeof sampleRate !== "number" || !Number.isFinite(sampleRate) || sampleRate <= 0) {
299
+ throw configurationError("sampleRate must be finite and greater than zero");
300
+ }
301
+ if (!Number.isInteger(blockSize) || blockSize <= 0 || blockSize > MAX_BLOCK_SIZE) {
302
+ throw configurationError(`blockSize must be between 1 and ${MAX_BLOCK_SIZE} frames`);
303
+ }
304
+ if (
305
+ options.codegen !== undefined
306
+ && (!options.codegen || typeof options.codegen !== "object" || Array.isArray(options.codegen))
307
+ ) {
308
+ throw configurationError("codegen options must be an object");
309
+ }
310
+ return { sampleRate, blockSize, codegen: options.codegen ?? {} };
311
+ }
312
+
313
+ function compileMirTransport(mir, codegen, compileTrustedMir) {
314
+ try {
315
+ return compileTrustedMir(mir, codegen);
316
+ } catch (cause) {
317
+ if (cause instanceof OndaCompileError) throw cause;
318
+ throw new OndaCompileError([{
319
+ stage: "codegen",
320
+ code: 0,
321
+ message: cause instanceof Error ? cause.message : String(cause),
322
+ file: null,
323
+ line: 0,
324
+ column: 0,
325
+ end_line: 0,
326
+ end_column: 0,
327
+ trace: [],
328
+ }], { cause });
329
+ }
330
+ }
331
+
332
+ function diagnosticsFromFrontend(error) {
333
+ const encoded = typeof error === "string" ? error : error?.message;
334
+ if (typeof encoded === "string") {
335
+ try {
336
+ const diagnostics = JSON.parse(encoded);
337
+ if (Array.isArray(diagnostics)) {
338
+ return new OndaCompileError(diagnostics, { cause: error });
339
+ }
340
+ } catch {}
341
+ }
342
+ return new OndaCompileError([{
343
+ stage: "frontend",
344
+ code: 0,
345
+ message: encoded ?? String(error),
346
+ file: null,
347
+ line: 0,
348
+ column: 0,
349
+ end_line: 0,
350
+ end_column: 0,
351
+ trace: [],
352
+ }], { cause: error });
353
+ }
354
+
355
+ function configurationError(message) {
356
+ return new OndaCompileError([{
357
+ stage: "configuration",
358
+ code: 0,
359
+ message,
360
+ file: null,
361
+ line: 0,
362
+ column: 0,
363
+ end_line: 0,
364
+ end_column: 0,
365
+ trace: [],
366
+ }]);
367
+ }
368
+
369
+ function normalizeDiagnostics(diagnostics) {
370
+ if (!Array.isArray(diagnostics)) return [];
371
+ return diagnostics.map((diagnostic) => ({
372
+ stage: String(diagnostic?.stage ?? "unknown"),
373
+ code: Number.isInteger(diagnostic?.code) ? diagnostic.code : 0,
374
+ message: String(diagnostic?.message ?? "compilation failed"),
375
+ file: typeof diagnostic?.file === "string" ? diagnostic.file : null,
376
+ line: Number.isInteger(diagnostic?.line) ? diagnostic.line : 0,
377
+ column: Number.isInteger(diagnostic?.column) ? diagnostic.column : 0,
378
+ end_line: Number.isInteger(diagnostic?.end_line) ? diagnostic.end_line : 0,
379
+ end_column: Number.isInteger(diagnostic?.end_column) ? diagnostic.end_column : 0,
380
+ trace: Array.isArray(diagnostic?.trace)
381
+ ? diagnostic.trace.map((entry) => String(entry))
382
+ : [],
383
+ }));
384
+ }
@@ -0,0 +1 @@
1
+ export {};
package/src/worker.js ADDED
@@ -0,0 +1,68 @@
1
+ import { createCompiler } from "./index.js";
2
+
3
+ let compilerPromise;
4
+
5
+ globalThis.addEventListener("message", async (event) => {
6
+ const message = event.data ?? {};
7
+ const requestId = message.requestId;
8
+ try {
9
+ if (message.type === "initialize") {
10
+ await compiler(message.frontendWasm);
11
+ respond(requestId, null);
12
+ return;
13
+ }
14
+ if (message.type === "compileSource") {
15
+ const artifact = await (await compiler()).compileSource(
16
+ message.source,
17
+ message.options,
18
+ );
19
+ respond(requestId, artifact, [artifact.wasm.buffer]);
20
+ return;
21
+ }
22
+ if (message.type === "compileProject") {
23
+ const artifact = await (await compiler()).compileProject(
24
+ message.project,
25
+ message.options,
26
+ );
27
+ respond(requestId, artifact, [artifact.wasm.buffer]);
28
+ return;
29
+ }
30
+ if (message.type === "lspMessage") {
31
+ const responses = await (await compiler()).sendLspMessage(message.message);
32
+ respond(requestId, responses);
33
+ return;
34
+ }
35
+ if (message.type === "lspAnalysisOptions") {
36
+ await (await compiler()).setLspAnalysisOptions(message.options);
37
+ respond(requestId, null);
38
+ return;
39
+ }
40
+ if (message.type === "dispose") {
41
+ if (compilerPromise) await (await compilerPromise).dispose();
42
+ respond(requestId, null);
43
+ globalThis.close();
44
+ return;
45
+ }
46
+ throw new Error(`unknown compiler worker request '${String(message.type)}'`);
47
+ } catch (error) {
48
+ globalThis.postMessage({
49
+ type: "error",
50
+ requestId,
51
+ error: {
52
+ name: error?.name ?? "Error",
53
+ message: error?.message ?? String(error),
54
+ stack: error?.stack,
55
+ diagnostics: error?.diagnostics,
56
+ },
57
+ });
58
+ }
59
+ });
60
+
61
+ function compiler(frontendWasm) {
62
+ compilerPromise ??= createCompiler({ frontendWasm });
63
+ return compilerPromise;
64
+ }
65
+
66
+ function respond(requestId, value, transfer = []) {
67
+ globalThis.postMessage({ type: "result", requestId, value }, transfer);
68
+ }