@onda-lang/wasm-compiler 0.7.5 → 0.8.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/src/index.js CHANGED
@@ -74,9 +74,17 @@ class OndaCompiler {
74
74
  this.frontend = frontend;
75
75
  this.compileTrustedMir = compileTrustedMir;
76
76
  this.lsp = null;
77
+ this.disposed = false;
78
+ }
79
+
80
+ assertActive() {
81
+ if (this.disposed) {
82
+ throw new OndaCompilerError("compiler was disposed");
83
+ }
77
84
  }
78
85
 
79
86
  async compileSource(source, options = {}) {
87
+ this.assertActive();
80
88
  if (typeof source !== "string") {
81
89
  throw configurationError("source must be a string");
82
90
  }
@@ -87,6 +95,7 @@ class OndaCompiler {
87
95
  source,
88
96
  compile.sampleRate,
89
97
  compile.blockSize,
98
+ compile.constantsJson,
90
99
  );
91
100
  } catch (error) {
92
101
  throw diagnosticsFromFrontend(error);
@@ -101,22 +110,29 @@ class OndaCompiler {
101
110
  return { artifact, sourceFiles, sourceGraph };
102
111
  }
103
112
 
104
- async compileWorkspace(workspace, options = {}) {
105
- if (!workspace || typeof workspace !== "object" || Array.isArray(workspace)) {
106
- throw configurationError("workspace must contain an entry and source map");
107
- }
108
- if (typeof workspace.entry !== "string" || workspace.entry.length === 0) {
109
- throw configurationError("workspace.entry must be a non-empty string");
110
- }
111
- if (!workspace.sources || typeof workspace.sources !== "object" || Array.isArray(workspace.sources)) {
112
- throw configurationError("workspace.sources must be an object of paths to source strings");
113
+ async inspectSourceConstants(source, options = {}) {
114
+ this.assertActive();
115
+ if (typeof source !== "string") {
116
+ throw configurationError("source must be a string");
113
117
  }
114
- for (const [path, source] of Object.entries(workspace.sources)) {
115
- if (typeof source !== "string") {
116
- throw configurationError(`workspace source '${path}' must be a string`);
117
- }
118
+ const compile = normalizeCompileConstInspectionOptions(options);
119
+ let encoded;
120
+ try {
121
+ encoded = this.frontend.inspect_source_compile_constants(
122
+ source,
123
+ compile.sampleRate,
124
+ compile.blockSize,
125
+ compile.constantsJson,
126
+ );
127
+ } catch (error) {
128
+ throw diagnosticsFromFrontend(error);
118
129
  }
130
+ return decodeCompileConstDescriptors(encoded);
131
+ }
119
132
 
133
+ async compileWorkspace(workspace, options = {}) {
134
+ this.assertActive();
135
+ workspace = normalizeWorkspace(workspace);
120
136
  const compile = normalizeCompileOptions(options);
121
137
  let frontendCompilation;
122
138
  try {
@@ -125,6 +141,7 @@ class OndaCompiler {
125
141
  JSON.stringify(workspace.sources),
126
142
  compile.sampleRate,
127
143
  compile.blockSize,
144
+ compile.constantsJson,
128
145
  );
129
146
  } catch (error) {
130
147
  throw diagnosticsFromFrontend(error);
@@ -139,7 +156,27 @@ class OndaCompiler {
139
156
  return { artifact, sourceFiles, sourceGraph };
140
157
  }
141
158
 
159
+ async inspectWorkspaceConstants(workspace, options = {}) {
160
+ this.assertActive();
161
+ workspace = normalizeWorkspace(workspace);
162
+ const compile = normalizeCompileConstInspectionOptions(options);
163
+ let encoded;
164
+ try {
165
+ encoded = this.frontend.inspect_source_workspace_compile_constants(
166
+ workspace.entry,
167
+ JSON.stringify(workspace.sources),
168
+ compile.sampleRate,
169
+ compile.blockSize,
170
+ compile.constantsJson,
171
+ );
172
+ } catch (error) {
173
+ throw diagnosticsFromFrontend(error);
174
+ }
175
+ return decodeCompileConstDescriptors(encoded);
176
+ }
177
+
142
178
  async compileProjectImage(imageBytes, options = {}) {
179
+ this.assertActive();
143
180
  const bytes = normalizeBytes(imageBytes, "project image");
144
181
  const compile = normalizeCompileOptions(options);
145
182
  let frontendCompilation;
@@ -148,6 +185,7 @@ class OndaCompiler {
148
185
  bytes,
149
186
  compile.sampleRate,
150
187
  compile.blockSize,
188
+ compile.constantsJson,
151
189
  );
152
190
  } catch (error) {
153
191
  throw diagnosticsFromFrontend(error);
@@ -162,7 +200,26 @@ class OndaCompiler {
162
200
  return { artifact, sourceFiles, sourceGraph };
163
201
  }
164
202
 
203
+ async inspectProjectImageConstants(imageBytes, options = {}) {
204
+ this.assertActive();
205
+ const bytes = normalizeBytes(imageBytes, "project image");
206
+ const compile = normalizeCompileConstInspectionOptions(options);
207
+ let encoded;
208
+ try {
209
+ encoded = this.frontend.inspect_project_image_compile_constants(
210
+ bytes,
211
+ compile.sampleRate,
212
+ compile.blockSize,
213
+ compile.constantsJson,
214
+ );
215
+ } catch (error) {
216
+ throw diagnosticsFromFrontend(error);
217
+ }
218
+ return decodeCompileConstDescriptors(encoded);
219
+ }
220
+
165
221
  async createProjectImage(sourceGraph, buffers = new Map()) {
222
+ this.assertActive();
166
223
  const graph = normalizeSourceGraph(sourceGraph);
167
224
  const builder = new this.frontend.WebProjectImageBuilder(JSON.stringify(graph));
168
225
  try {
@@ -182,6 +239,7 @@ class OndaCompiler {
182
239
  }
183
240
 
184
241
  async inspectProjectImage(imageBytes) {
242
+ this.assertActive();
185
243
  try {
186
244
  return normalizeProjectImageInfo(JSON.parse(this.frontend.inspect_project_image(
187
245
  normalizeBytes(imageBytes, "project image"),
@@ -192,6 +250,7 @@ class OndaCompiler {
192
250
  }
193
251
 
194
252
  async loadProjectFiles(files, projectFilePath = null) {
253
+ this.assertActive();
195
254
  const builder = new this.frontend.WebMaterializedProjectBuilder();
196
255
  try {
197
256
  if (projectFilePath !== null) {
@@ -213,6 +272,7 @@ class OndaCompiler {
213
272
  }
214
273
 
215
274
  async materializeProjectImage(imageBytes, assetFileNames = new Map()) {
275
+ this.assertActive();
216
276
  let plan;
217
277
  try {
218
278
  plan = this.frontend.materialize_project_image(
@@ -232,6 +292,7 @@ class OndaCompiler {
232
292
  }
233
293
 
234
294
  async encodeBufferAsset(binding) {
295
+ this.assertActive();
235
296
  const normalized = normalizeBufferBinding(binding);
236
297
  try {
237
298
  return this.frontend.encode_buffer_asset(
@@ -247,6 +308,7 @@ class OndaCompiler {
247
308
  }
248
309
 
249
310
  async decodeBufferAsset(bytes) {
311
+ this.assertActive();
250
312
  return this.#decodeBuffer(
251
313
  () => this.frontend.decode_buffer_asset(normalizeBytes(bytes, "buffer asset")),
252
314
  "failed to decode Onda buffer asset",
@@ -254,6 +316,7 @@ class OndaCompiler {
254
316
  }
255
317
 
256
318
  async decodeBufferFile(bytes, path = "buffer") {
319
+ this.assertActive();
257
320
  return this.#decodeBuffer(
258
321
  () => this.frontend.decode_buffer_file(
259
322
  normalizeBytes(bytes, "buffer file"),
@@ -284,6 +347,7 @@ class OndaCompiler {
284
347
  }
285
348
 
286
349
  async projectCapabilities() {
350
+ this.assertActive();
287
351
  return {
288
352
  imageFormatVersion: this.frontend.project_image_format_version(),
289
353
  bufferAssetFormatVersion: this.frontend.buffer_asset_format_version(),
@@ -292,6 +356,7 @@ class OndaCompiler {
292
356
  }
293
357
 
294
358
  async sendLspMessage(message) {
359
+ this.assertActive();
295
360
  if (!message || typeof message !== "object" || Array.isArray(message)) {
296
361
  throw new OndaCompilerError("LSP message must be a JSON-RPC object");
297
362
  }
@@ -308,18 +373,23 @@ class OndaCompiler {
308
373
  }
309
374
 
310
375
  async setLspAnalysisOptions(options = {}) {
311
- const compile = normalizeCompileOptions(options);
376
+ this.assertActive();
377
+ const analysis = normalizeLspAnalysisOptions(options);
312
378
  this.lsp ??= new this.frontend.OndaLsp();
313
379
  try {
314
- this.lsp.set_analysis_options(compile.sampleRate, compile.blockSize);
380
+ this.lsp.set_analysis_options(analysis.sampleRate, analysis.blockSize);
315
381
  } catch (cause) {
316
382
  throw new OndaCompilerError("failed to configure Onda LSP analysis", { cause });
317
383
  }
318
384
  }
319
385
 
320
386
  async dispose() {
387
+ if (this.disposed) return;
388
+ this.disposed = true;
321
389
  this.lsp?.free();
322
390
  this.lsp = null;
391
+ this.frontend = null;
392
+ this.compileTrustedMir = null;
323
393
  }
324
394
  }
325
395
 
@@ -328,6 +398,9 @@ class WorkerOndaCompiler {
328
398
  this.worker = worker;
329
399
  this.nextRequestId = 1;
330
400
  this.pending = new Map();
401
+ this.disposed = false;
402
+ this.terminated = false;
403
+ this.disposePromise = null;
331
404
  this.onMessage = (event) => this.handleMessage(event.data);
332
405
  this.onError = (event) => this.failAll(event.error ?? new Error(event.message));
333
406
  worker.addEventListener("message", this.onMessage);
@@ -342,14 +415,26 @@ class WorkerOndaCompiler {
342
415
  return this.request("compileSource", { source, options });
343
416
  }
344
417
 
418
+ inspectSourceConstants(source, options = {}) {
419
+ return this.request("inspectSourceConstants", { source, options });
420
+ }
421
+
345
422
  compileWorkspace(workspace, options = {}) {
346
423
  return this.request("compileWorkspace", { workspace, options });
347
424
  }
348
425
 
426
+ inspectWorkspaceConstants(workspace, options = {}) {
427
+ return this.request("inspectWorkspaceConstants", { workspace, options });
428
+ }
429
+
349
430
  compileProjectImage(imageBytes, options = {}) {
350
431
  return this.request("compileProjectImage", { imageBytes, options });
351
432
  }
352
433
 
434
+ inspectProjectImageConstants(imageBytes, options = {}) {
435
+ return this.request("inspectProjectImageConstants", { imageBytes, options });
436
+ }
437
+
353
438
  createProjectImage(sourceGraph, buffers = new Map()) {
354
439
  return this.request("createProjectImage", { sourceGraph, buffers });
355
440
  }
@@ -390,15 +475,21 @@ class WorkerOndaCompiler {
390
475
  return this.request("lspAnalysisOptions", { options });
391
476
  }
392
477
 
393
- async dispose() {
394
- try {
395
- await this.request("dispose");
396
- } finally {
478
+ dispose() {
479
+ if (this.disposePromise) return this.disposePromise;
480
+ if (this.disposed) return Promise.resolve();
481
+ const request = this.request("dispose");
482
+ this.disposed = true;
483
+ this.disposePromise = request.finally(() => {
397
484
  this.terminate(new OndaCompilerError("compiler worker was disposed"));
398
- }
485
+ });
486
+ return this.disposePromise;
399
487
  }
400
488
 
401
489
  terminate(error) {
490
+ if (this.terminated) return;
491
+ this.disposed = true;
492
+ this.terminated = true;
402
493
  this.worker.removeEventListener("message", this.onMessage);
403
494
  this.worker.removeEventListener("error", this.onError);
404
495
  this.worker.terminate();
@@ -406,6 +497,9 @@ class WorkerOndaCompiler {
406
497
  }
407
498
 
408
499
  request(type, fields = {}) {
500
+ if (this.disposed) {
501
+ return Promise.reject(new OndaCompilerError("compiler was disposed"));
502
+ }
409
503
  const requestId = this.nextRequestId;
410
504
  this.nextRequestId += 1;
411
505
  return new Promise((resolve, reject) => {
@@ -504,6 +598,48 @@ export function createDefaultImports() {
504
598
  }
505
599
 
506
600
  function normalizeCompileOptions(options) {
601
+ const compile = normalizeCompileInputOptions(options);
602
+ if (
603
+ options.codegen !== undefined
604
+ && (!options.codegen || typeof options.codegen !== "object" || Array.isArray(options.codegen))
605
+ ) {
606
+ throw configurationError("codegen options must be an object");
607
+ }
608
+ return {
609
+ ...compile,
610
+ codegen: options.codegen ?? {},
611
+ };
612
+ }
613
+
614
+ function normalizeCompileConstInspectionOptions(options) {
615
+ const compile = normalizeCompileInputOptions(options);
616
+ if (options.codegen !== undefined) {
617
+ throw configurationError("codegen options do not apply to compile constant inspection");
618
+ }
619
+ return compile;
620
+ }
621
+
622
+ function normalizeCompileInputOptions(options) {
623
+ return {
624
+ ...normalizeContextOptions(options),
625
+ constantsJson: JSON.stringify(normalizeCompileConstants(options.constants ?? {})),
626
+ };
627
+ }
628
+
629
+ function normalizeLspAnalysisOptions(options) {
630
+ const context = normalizeContextOptions(options);
631
+ if (options.constants !== undefined) {
632
+ throw configurationError(
633
+ "constants are compile-request inputs and cannot be set as LSP analysis options",
634
+ );
635
+ }
636
+ if (options.codegen !== undefined) {
637
+ throw configurationError("codegen options cannot be set as LSP analysis options");
638
+ }
639
+ return context;
640
+ }
641
+
642
+ function normalizeContextOptions(options) {
507
643
  if (!options || typeof options !== "object" || Array.isArray(options)) {
508
644
  throw configurationError("compiler options must be an object");
509
645
  }
@@ -515,13 +651,160 @@ function normalizeCompileOptions(options) {
515
651
  if (!Number.isInteger(blockSize) || blockSize <= 0 || blockSize > MAX_BLOCK_SIZE) {
516
652
  throw configurationError(`blockSize must be between 1 and ${MAX_BLOCK_SIZE} frames`);
517
653
  }
654
+ return { sampleRate, blockSize };
655
+ }
656
+
657
+ function normalizeWorkspace(workspace) {
658
+ if (!workspace || typeof workspace !== "object" || Array.isArray(workspace)) {
659
+ throw configurationError("workspace must contain an entry and source map");
660
+ }
661
+ if (typeof workspace.entry !== "string" || workspace.entry.length === 0) {
662
+ throw configurationError("workspace.entry must be a non-empty string");
663
+ }
664
+ if (!workspace.sources || typeof workspace.sources !== "object" || Array.isArray(workspace.sources)) {
665
+ throw configurationError("workspace.sources must be an object of paths to source strings");
666
+ }
667
+ for (const [path, source] of Object.entries(workspace.sources)) {
668
+ if (typeof source !== "string") {
669
+ throw configurationError(`workspace source '${path}' must be a string`);
670
+ }
671
+ }
672
+ return workspace;
673
+ }
674
+
675
+ function normalizeCompileConstants(constants) {
518
676
  if (
519
- options.codegen !== undefined
520
- && (!options.codegen || typeof options.codegen !== "object" || Array.isArray(options.codegen))
677
+ !constants
678
+ || typeof constants !== "object"
679
+ || Array.isArray(constants)
680
+ || ArrayBuffer.isView(constants)
521
681
  ) {
522
- throw configurationError("codegen options must be an object");
682
+ throw configurationError("constants must be an object or Map");
523
683
  }
524
- return { sampleRate, blockSize, codegen: options.codegen ?? {} };
684
+ const entries = constants instanceof Map
685
+ ? [...constants.entries()]
686
+ : Object.entries(constants);
687
+ return entries.map(([rawName, value]) => {
688
+ const name = String(rawName);
689
+ if (name.length === 0) {
690
+ throw configurationError("compile constant names must be non-empty");
691
+ }
692
+ if (typeof value === "boolean") {
693
+ return { name, element: "bool", array: false, values: [value] };
694
+ }
695
+ if (typeof value === "number") {
696
+ return {
697
+ name,
698
+ element: "number",
699
+ array: false,
700
+ values: [encodeCompileNumber(value)],
701
+ };
702
+ }
703
+ if (typeof value === "bigint") {
704
+ return { name, element: "i64", array: false, values: [value.toString()] };
705
+ }
706
+ if (value instanceof Uint8Array) {
707
+ const values = [...value];
708
+ if (values.some((item) => item !== 0 && item !== 1)) {
709
+ throw configurationError(
710
+ `boolean compile constant array '${name}' may contain only 0 or 1`,
711
+ );
712
+ }
713
+ return {
714
+ name,
715
+ element: "bool",
716
+ array: true,
717
+ values: values.map((item) => item !== 0),
718
+ };
719
+ }
720
+ if (value instanceof Int32Array) {
721
+ return { name, element: "i32", array: true, values: [...value] };
722
+ }
723
+ if (value instanceof BigInt64Array) {
724
+ return {
725
+ name,
726
+ element: "i64",
727
+ array: true,
728
+ values: [...value].map((item) => item.toString()),
729
+ };
730
+ }
731
+ if (value instanceof Float32Array) {
732
+ return {
733
+ name,
734
+ element: "f32",
735
+ array: true,
736
+ values: encodeCompileFloatArray(value),
737
+ };
738
+ }
739
+ if (value instanceof Float64Array) {
740
+ return {
741
+ name,
742
+ element: "f64",
743
+ array: true,
744
+ values: encodeCompileFloatArray(value),
745
+ };
746
+ }
747
+ if (Array.isArray(value) && value.every((item) => typeof item === "boolean")) {
748
+ return { name, element: "bool", array: true, values: value };
749
+ }
750
+ throw configurationError(
751
+ `compile constant '${name}' must be a boolean, number, bigint, or matching typed array`,
752
+ );
753
+ });
754
+ }
755
+
756
+ function encodeCompileFloatArray(values) {
757
+ return [...values].map(encodeCompileNumber);
758
+ }
759
+
760
+ function encodeCompileNumber(value) {
761
+ if (Object.is(value, -0)) return "-0";
762
+ if (Number.isNaN(value)) return "NaN";
763
+ if (value === Number.POSITIVE_INFINITY) return "Infinity";
764
+ if (value === Number.NEGATIVE_INFINITY) return "-Infinity";
765
+ return value;
766
+ }
767
+
768
+ function decodeCompileConstDescriptors(encoded) {
769
+ let descriptors;
770
+ try {
771
+ descriptors = JSON.parse(encoded);
772
+ } catch (cause) {
773
+ throw new OndaCompilerError("failed to decode compile constant descriptors", { cause });
774
+ }
775
+ if (!Array.isArray(descriptors)) {
776
+ throw new OndaCompilerError("frontend returned invalid compile constant descriptors");
777
+ }
778
+ return descriptors.map((descriptor) => ({
779
+ name: descriptor.name,
780
+ element: descriptor.element,
781
+ kind: descriptor.kind,
782
+ elementCount: descriptor.element_count,
783
+ value: decodeCompileConstValue(descriptor.element, descriptor.kind, descriptor.values),
784
+ }));
785
+ }
786
+
787
+ function decodeCompileConstValue(element, kind, values) {
788
+ const decoded = element === "i64"
789
+ ? values.map((value) => BigInt(value))
790
+ : element === "f32" || element === "f64"
791
+ ? values.map(decodeCompileFloat)
792
+ : values;
793
+ if (kind === "scalar") return decoded[0];
794
+ if (element === "bool") return Uint8Array.from(decoded, (value) => value ? 1 : 0);
795
+ if (element === "i32") return Int32Array.from(decoded);
796
+ if (element === "i64") return BigInt64Array.from(decoded);
797
+ if (element === "f32") return Float32Array.from(decoded);
798
+ if (element === "f64") return Float64Array.from(decoded);
799
+ throw new OndaCompilerError(`frontend returned unknown compile constant element '${element}'`);
800
+ }
801
+
802
+ function decodeCompileFloat(value) {
803
+ if (value === "-0") return -0;
804
+ if (value === "NaN") return Number.NaN;
805
+ if (value === "Infinity") return Number.POSITIVE_INFINITY;
806
+ if (value === "-Infinity") return Number.NEGATIVE_INFINITY;
807
+ return value;
525
808
  }
526
809
 
527
810
  function consumeFrontendCompilation(compilation) {
package/src/worker.js CHANGED
@@ -19,6 +19,14 @@ globalThis.addEventListener("message", async (event) => {
19
19
  respond(requestId, result, [result.artifact.wasm.buffer]);
20
20
  return;
21
21
  }
22
+ if (message.type === "inspectSourceConstants") {
23
+ const result = await (await compiler()).inspectSourceConstants(
24
+ message.source,
25
+ message.options,
26
+ );
27
+ respond(requestId, result, compileConstTransfers(result));
28
+ return;
29
+ }
22
30
  if (message.type === "compileWorkspace") {
23
31
  const result = await (await compiler()).compileWorkspace(
24
32
  message.workspace,
@@ -27,6 +35,14 @@ globalThis.addEventListener("message", async (event) => {
27
35
  respond(requestId, result, [result.artifact.wasm.buffer]);
28
36
  return;
29
37
  }
38
+ if (message.type === "inspectWorkspaceConstants") {
39
+ const result = await (await compiler()).inspectWorkspaceConstants(
40
+ message.workspace,
41
+ message.options,
42
+ );
43
+ respond(requestId, result, compileConstTransfers(result));
44
+ return;
45
+ }
30
46
  if (message.type === "compileProjectImage") {
31
47
  const result = await (await compiler()).compileProjectImage(
32
48
  message.imageBytes,
@@ -35,6 +51,14 @@ globalThis.addEventListener("message", async (event) => {
35
51
  respond(requestId, result, [result.artifact.wasm.buffer]);
36
52
  return;
37
53
  }
54
+ if (message.type === "inspectProjectImageConstants") {
55
+ const result = await (await compiler()).inspectProjectImageConstants(
56
+ message.imageBytes,
57
+ message.options,
58
+ );
59
+ respond(requestId, result, compileConstTransfers(result));
60
+ return;
61
+ }
38
62
  if (message.type === "createProjectImage") {
39
63
  const result = await (await compiler()).createProjectImage(
40
64
  message.sourceGraph,
@@ -123,3 +147,10 @@ function compiler(frontendWasm) {
123
147
  function respond(requestId, value, transfer = []) {
124
148
  globalThis.postMessage({ type: "result", requestId, value }, transfer);
125
149
  }
150
+
151
+ function compileConstTransfers(descriptors) {
152
+ return descriptors
153
+ .map((descriptor) => descriptor.value)
154
+ .filter(ArrayBuffer.isView)
155
+ .map((value) => value.buffer);
156
+ }