@onda-lang/processor-abi 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 +21 -0
- package/README.md +18 -0
- package/package.json +34 -0
- package/src/index.d.ts +192 -0
- package/src/index.js +767 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) onda contributors
|
|
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,18 @@
|
|
|
1
|
+
# Onda processor ABI helpers
|
|
2
|
+
|
|
3
|
+
`@onda-lang/processor-abi` is the small, compiler-free JavaScript contract for Onda processor
|
|
4
|
+
artifacts. It validates current-format descriptors and complete core-WebAssembly modules, creates and
|
|
5
|
+
loads integrity-associated `.wasm`/`.onda.json` pairs, and provides the shared TypeScript surface
|
|
6
|
+
used by `@onda-lang/wasm-compiler`, `@onda-lang/binaryen-web`, and `@onda-lang/webaudio`.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import {
|
|
10
|
+
loadProcessorArtifactFiles,
|
|
11
|
+
validateProcessorArtifact,
|
|
12
|
+
} from "@onda-lang/processor-abi";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This package does not compile Onda source or host Web Audio. It exists so loaders and hosts can
|
|
16
|
+
validate artifacts without installing the compiler or duplicating the ABI schema.
|
|
17
|
+
The detailed TypeScript records mirror `onda_processor_abi::ProcessorDescriptor`; both packages
|
|
18
|
+
validate the same checked-in conformance fixture.
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onda-lang/processor-abi",
|
|
3
|
+
"version": "0.5.0-rc.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Shared Onda processor artifact schema, validation, and integrity helpers",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/onda-lang/onda.git",
|
|
10
|
+
"directory": "packages/onda_processor_abi"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://onda-lang.github.io/onda/",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/onda-lang/onda/issues"
|
|
15
|
+
},
|
|
16
|
+
"types": "./src/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./src/index.d.ts",
|
|
20
|
+
"default": "./src/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node --test"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
export const PROCESSOR_ARTIFACT_FORMAT: "onda-processor";
|
|
2
|
+
// Synchronized from format-versions.json; do not edit these copies directly.
|
|
3
|
+
export const PROCESSOR_ARTIFACT_FORMAT_VERSION: 1;
|
|
4
|
+
export const PROCESSOR_ABI_VERSION: 1;
|
|
5
|
+
export const PROCESSOR_SNAPSHOT_FORMAT_VERSION: 1;
|
|
6
|
+
|
|
7
|
+
export type OndaScalarType = "f32" | "f64" | "i32" | "i64" | "bool";
|
|
8
|
+
export type OndaArtifactKind = "webassembly_module" | "relocatable_object";
|
|
9
|
+
|
|
10
|
+
export interface OndaTargetInfo {
|
|
11
|
+
triple: string;
|
|
12
|
+
cpu: string;
|
|
13
|
+
features: string;
|
|
14
|
+
reloc_model: string;
|
|
15
|
+
code_model: string;
|
|
16
|
+
opt_level: string;
|
|
17
|
+
abi_name: string | null;
|
|
18
|
+
data_layout: string;
|
|
19
|
+
pointer_width_bits: number;
|
|
20
|
+
byte_order: "little_endian" | "big_endian";
|
|
21
|
+
pointer_model: "native_address" | "linear_memory_offset";
|
|
22
|
+
calling_convention: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type OndaIntegrationProfile =
|
|
26
|
+
| { kind: "native_relocatable_object"; symbol_visibility: string }
|
|
27
|
+
| {
|
|
28
|
+
kind: "webassembly_relocatable_object";
|
|
29
|
+
symbol_visibility: string;
|
|
30
|
+
no_entry: boolean;
|
|
31
|
+
export_memory: boolean;
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
kind: "core_webassembly_module";
|
|
35
|
+
imports: string[];
|
|
36
|
+
memory_export: string;
|
|
37
|
+
heap_base_export: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface OndaIoMetadata {
|
|
41
|
+
name: string;
|
|
42
|
+
type_repr: string;
|
|
43
|
+
scalar: OndaScalarType;
|
|
44
|
+
array_len: number;
|
|
45
|
+
element_size_bytes: number;
|
|
46
|
+
slot_offset: number;
|
|
47
|
+
byte_offset: number | null;
|
|
48
|
+
state_byte_offset: number | null;
|
|
49
|
+
byte_size: number;
|
|
50
|
+
default_reprs: string[] | null;
|
|
51
|
+
range_min_repr: string | null;
|
|
52
|
+
range_max_repr: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface OndaBufferMetadata {
|
|
56
|
+
name: string;
|
|
57
|
+
type_repr: string;
|
|
58
|
+
scalar: OndaScalarType;
|
|
59
|
+
element_size_bytes: number;
|
|
60
|
+
channels: "mono" | "static" | "dynamic";
|
|
61
|
+
static_channels: number | null;
|
|
62
|
+
access: "read_only" | "read_write";
|
|
63
|
+
may_write: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface OndaEventParamMetadata {
|
|
67
|
+
name: string;
|
|
68
|
+
type_repr: string;
|
|
69
|
+
scalar: OndaScalarType;
|
|
70
|
+
array_len: number;
|
|
71
|
+
is_slice: boolean;
|
|
72
|
+
byte_offset: number | null;
|
|
73
|
+
byte_size: number | null;
|
|
74
|
+
element_size_bytes: number;
|
|
75
|
+
has_default: boolean;
|
|
76
|
+
default_reprs: string[] | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface OndaEventMetadata {
|
|
80
|
+
name: string;
|
|
81
|
+
export: string;
|
|
82
|
+
payload_size_bytes: number | null;
|
|
83
|
+
payload_min_size_bytes: number;
|
|
84
|
+
has_dynamic_payload: boolean;
|
|
85
|
+
params: OndaEventParamMetadata[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface OndaStateMetadata {
|
|
89
|
+
name: string;
|
|
90
|
+
type_repr: string;
|
|
91
|
+
scalar: OndaScalarType;
|
|
92
|
+
array_len: number;
|
|
93
|
+
element_size_bytes: number;
|
|
94
|
+
packed_snapshot_byte_offset: number;
|
|
95
|
+
physical_state_byte_offset: number;
|
|
96
|
+
byte_size: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface OndaProcessorMetadata {
|
|
100
|
+
format: "onda-processor";
|
|
101
|
+
format_version: 1;
|
|
102
|
+
artifact_kind: OndaArtifactKind;
|
|
103
|
+
abi_version: 1;
|
|
104
|
+
backend: string;
|
|
105
|
+
mir_schema_version: number;
|
|
106
|
+
target: OndaTargetInfo;
|
|
107
|
+
integration: {
|
|
108
|
+
required_symbols: string[];
|
|
109
|
+
one_processor_per_artifact: true;
|
|
110
|
+
profile: OndaIntegrationProfile;
|
|
111
|
+
};
|
|
112
|
+
compile: { sample_rate: number; block_size: number; fast_math: boolean };
|
|
113
|
+
exports: {
|
|
114
|
+
memory?: string;
|
|
115
|
+
heap_base?: string;
|
|
116
|
+
init: string;
|
|
117
|
+
process: string;
|
|
118
|
+
events: string[];
|
|
119
|
+
};
|
|
120
|
+
runtime: {
|
|
121
|
+
state_size_bytes: number;
|
|
122
|
+
state_align_bytes: number;
|
|
123
|
+
param_size_bytes: number;
|
|
124
|
+
param_align_bytes: number;
|
|
125
|
+
state_initialization: "zeroed";
|
|
126
|
+
snapshot_size_bytes: number;
|
|
127
|
+
snapshot_format_version: 1;
|
|
128
|
+
snapshot_byte_order: "little_endian";
|
|
129
|
+
snapshot_restore_base: "post_init_physical_state_image";
|
|
130
|
+
requires_full_blocks: boolean;
|
|
131
|
+
};
|
|
132
|
+
metadata: {
|
|
133
|
+
inputs: OndaIoMetadata[];
|
|
134
|
+
outputs: OndaIoMetadata[];
|
|
135
|
+
control_outputs: OndaIoMetadata[];
|
|
136
|
+
params: OndaIoMetadata[];
|
|
137
|
+
buffers: OndaBufferMetadata[];
|
|
138
|
+
events: OndaEventMetadata[];
|
|
139
|
+
states: OndaStateMetadata[];
|
|
140
|
+
};
|
|
141
|
+
required_features?: string[];
|
|
142
|
+
optimization?: {
|
|
143
|
+
enabled: boolean;
|
|
144
|
+
level: number;
|
|
145
|
+
shrink_level: number;
|
|
146
|
+
fast_math: boolean;
|
|
147
|
+
simd: boolean;
|
|
148
|
+
inline_functions_with_loops: boolean;
|
|
149
|
+
};
|
|
150
|
+
integrity?: { algorithm: string; wasm: string };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface OndaProcessorArtifact {
|
|
154
|
+
wasm: Uint8Array | ArrayBuffer | ArrayBufferView;
|
|
155
|
+
metadata: OndaProcessorMetadata;
|
|
156
|
+
wat?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export class OndaArtifactError extends Error {}
|
|
160
|
+
export function validateProcessorMetadata(
|
|
161
|
+
metadata: object,
|
|
162
|
+
expectedKind?: OndaArtifactKind | null,
|
|
163
|
+
): OndaProcessorMetadata;
|
|
164
|
+
export function validateProcessorArtifact(
|
|
165
|
+
artifact: OndaProcessorArtifact,
|
|
166
|
+
options?: { inspectModule?: boolean },
|
|
167
|
+
): { wasm: Uint8Array; metadata: OndaProcessorMetadata };
|
|
168
|
+
export function validateProcessorModule(
|
|
169
|
+
module: WebAssembly.Module,
|
|
170
|
+
metadata: object,
|
|
171
|
+
): { module: WebAssembly.Module; metadata: OndaProcessorMetadata };
|
|
172
|
+
export function serializeProcessorMetadata(metadata: object, space?: number): string;
|
|
173
|
+
export function parseProcessorMetadata(
|
|
174
|
+
input: string | object,
|
|
175
|
+
expectedKind?: OndaArtifactKind | null,
|
|
176
|
+
): OndaProcessorMetadata;
|
|
177
|
+
export function createProcessorArtifactFiles(
|
|
178
|
+
artifact: OndaProcessorArtifact,
|
|
179
|
+
options?: { baseName?: string },
|
|
180
|
+
): Promise<{
|
|
181
|
+
wasm: { name: string; mediaType: "application/wasm"; bytes: Uint8Array };
|
|
182
|
+
metadata: {
|
|
183
|
+
name: string;
|
|
184
|
+
mediaType: "application/json";
|
|
185
|
+
text: string;
|
|
186
|
+
value: OndaProcessorMetadata;
|
|
187
|
+
};
|
|
188
|
+
}>;
|
|
189
|
+
export function loadProcessorArtifactFiles(
|
|
190
|
+
wasm: Uint8Array | ArrayBuffer | ArrayBufferView,
|
|
191
|
+
metadata: string | object,
|
|
192
|
+
): Promise<{ wasm: Uint8Array; metadata: OndaProcessorMetadata }>;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
export const PROCESSOR_ARTIFACT_FORMAT = "onda-processor";
|
|
2
|
+
// Synchronized from format-versions.json; do not edit these copies directly.
|
|
3
|
+
export const PROCESSOR_ARTIFACT_FORMAT_VERSION = 1;
|
|
4
|
+
export const PROCESSOR_ABI_VERSION = 1;
|
|
5
|
+
export const PROCESSOR_SNAPSHOT_FORMAT_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export class OndaArtifactError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "OndaArtifactError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validateProcessorMetadata(metadata, expectedKind = null) {
|
|
15
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
16
|
+
throw new OndaArtifactError("processor metadata must be an object");
|
|
17
|
+
}
|
|
18
|
+
if (metadata.format !== PROCESSOR_ARTIFACT_FORMAT) {
|
|
19
|
+
throw new OndaArtifactError(
|
|
20
|
+
`unsupported processor metadata format '${String(metadata.format)}'`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (metadata.format_version !== PROCESSOR_ARTIFACT_FORMAT_VERSION) {
|
|
24
|
+
throw new OndaArtifactError(
|
|
25
|
+
`unsupported processor metadata version ${String(metadata.format_version)}; expected ${PROCESSOR_ARTIFACT_FORMAT_VERSION}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (metadata.abi_version !== PROCESSOR_ABI_VERSION) {
|
|
29
|
+
throw new OndaArtifactError(
|
|
30
|
+
`unsupported processor ABI version ${String(metadata.abi_version)}; expected ${PROCESSOR_ABI_VERSION}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (
|
|
34
|
+
metadata.artifact_kind !== "webassembly_module"
|
|
35
|
+
&& metadata.artifact_kind !== "relocatable_object"
|
|
36
|
+
) {
|
|
37
|
+
throw new OndaArtifactError(
|
|
38
|
+
`unsupported processor artifact kind '${String(metadata.artifact_kind)}'`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (expectedKind !== null && metadata.artifact_kind !== expectedKind) {
|
|
42
|
+
throw new OndaArtifactError(
|
|
43
|
+
`expected processor artifact kind '${expectedKind}', got '${metadata.artifact_kind}'`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
requireInteger(metadata.mir_schema_version, "mir_schema_version", 1);
|
|
47
|
+
requireString(metadata.backend, "backend");
|
|
48
|
+
requireString(metadata.target?.triple, "target.triple");
|
|
49
|
+
requireString(metadata.target?.cpu, "target.cpu");
|
|
50
|
+
requireString(metadata.target?.features, "target.features", true);
|
|
51
|
+
requireString(metadata.target?.reloc_model, "target.reloc_model");
|
|
52
|
+
requireString(metadata.target?.code_model, "target.code_model");
|
|
53
|
+
requireString(metadata.target?.opt_level, "target.opt_level");
|
|
54
|
+
if (metadata.target?.abi_name !== null) {
|
|
55
|
+
requireString(metadata.target?.abi_name, "target.abi_name");
|
|
56
|
+
}
|
|
57
|
+
requireString(metadata.target?.data_layout, "target.data_layout");
|
|
58
|
+
requireInteger(metadata.target?.pointer_width_bits, "target.pointer_width_bits", 1);
|
|
59
|
+
if (
|
|
60
|
+
metadata.target?.byte_order !== "little_endian"
|
|
61
|
+
&& metadata.target?.byte_order !== "big_endian"
|
|
62
|
+
) {
|
|
63
|
+
throw new OndaArtifactError("target.byte_order must name a supported byte order");
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
metadata.target?.pointer_model !== "native_address"
|
|
67
|
+
&& metadata.target?.pointer_model !== "linear_memory_offset"
|
|
68
|
+
) {
|
|
69
|
+
throw new OndaArtifactError("target.pointer_model must name a supported pointer model");
|
|
70
|
+
}
|
|
71
|
+
requireString(metadata.target?.calling_convention, "target.calling_convention");
|
|
72
|
+
requirePositiveFinite(metadata.compile?.sample_rate, "compile.sample_rate");
|
|
73
|
+
requireInteger(metadata.compile?.block_size, "compile.block_size", 1);
|
|
74
|
+
requireBoolean(metadata.compile?.fast_math, "compile.fast_math");
|
|
75
|
+
requireInteger(metadata.runtime?.state_size_bytes, "runtime.state_size_bytes", 0);
|
|
76
|
+
requireInteger(metadata.runtime?.state_align_bytes, "runtime.state_align_bytes", 1);
|
|
77
|
+
requireInteger(metadata.runtime?.param_size_bytes, "runtime.param_size_bytes", 0);
|
|
78
|
+
requireInteger(metadata.runtime?.param_align_bytes, "runtime.param_align_bytes", 1);
|
|
79
|
+
requireInteger(
|
|
80
|
+
metadata.runtime?.snapshot_size_bytes,
|
|
81
|
+
"runtime.snapshot_size_bytes",
|
|
82
|
+
0,
|
|
83
|
+
);
|
|
84
|
+
requireInteger(
|
|
85
|
+
metadata.runtime?.snapshot_format_version,
|
|
86
|
+
"runtime.snapshot_format_version",
|
|
87
|
+
1,
|
|
88
|
+
);
|
|
89
|
+
if (
|
|
90
|
+
metadata.runtime.snapshot_format_version !== PROCESSOR_SNAPSHOT_FORMAT_VERSION
|
|
91
|
+
) {
|
|
92
|
+
throw new OndaArtifactError(
|
|
93
|
+
`unsupported processor snapshot version ${String(metadata.runtime.snapshot_format_version)}; expected ${PROCESSOR_SNAPSHOT_FORMAT_VERSION}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
requireLiteral(metadata.runtime?.state_initialization, "runtime.state_initialization", "zeroed");
|
|
97
|
+
requireLiteral(metadata.runtime?.snapshot_byte_order, "runtime.snapshot_byte_order", "little_endian");
|
|
98
|
+
requireLiteral(
|
|
99
|
+
metadata.runtime?.snapshot_restore_base,
|
|
100
|
+
"runtime.snapshot_restore_base",
|
|
101
|
+
"post_init_physical_state_image",
|
|
102
|
+
);
|
|
103
|
+
requireBoolean(metadata.runtime?.requires_full_blocks, "runtime.requires_full_blocks");
|
|
104
|
+
requireString(metadata.exports?.init, "exports.init");
|
|
105
|
+
requireString(metadata.exports?.process, "exports.process");
|
|
106
|
+
if (!Array.isArray(metadata.exports?.events)) {
|
|
107
|
+
throw new OndaArtifactError("exports.events must be an array");
|
|
108
|
+
}
|
|
109
|
+
for (const name of metadata.exports.events) {
|
|
110
|
+
requireString(name, "exports.events[]");
|
|
111
|
+
}
|
|
112
|
+
if (!Array.isArray(metadata.integration?.required_symbols)) {
|
|
113
|
+
throw new OndaArtifactError("integration.required_symbols must be an array");
|
|
114
|
+
}
|
|
115
|
+
for (const name of metadata.integration.required_symbols) {
|
|
116
|
+
requireString(name, "integration.required_symbols[]");
|
|
117
|
+
}
|
|
118
|
+
const requiredSymbols = new Set(metadata.integration.required_symbols);
|
|
119
|
+
for (const name of [
|
|
120
|
+
metadata.exports.init,
|
|
121
|
+
metadata.exports.process,
|
|
122
|
+
...metadata.exports.events,
|
|
123
|
+
]) {
|
|
124
|
+
if (!requiredSymbols.has(name)) {
|
|
125
|
+
throw new OndaArtifactError(
|
|
126
|
+
`integration.required_symbols is missing executable export '${name}'`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (metadata.integration?.one_processor_per_artifact !== true) {
|
|
131
|
+
throw new OndaArtifactError(
|
|
132
|
+
`integration.one_processor_per_artifact must be true for ABI version ${PROCESSOR_ABI_VERSION}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const profileKind = metadata.integration?.profile?.kind;
|
|
136
|
+
const expectedProfileKinds = metadata.artifact_kind === "webassembly_module"
|
|
137
|
+
? ["core_webassembly_module"]
|
|
138
|
+
: ["native_relocatable_object", "webassembly_relocatable_object"];
|
|
139
|
+
if (!expectedProfileKinds.includes(profileKind)) {
|
|
140
|
+
throw new OndaArtifactError(
|
|
141
|
+
`integration profile '${String(profileKind)}' is incompatible with artifact kind '${metadata.artifact_kind}'`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (profileKind === "core_webassembly_module") {
|
|
145
|
+
requireLiteral(metadata.target.byte_order, "target.byte_order", "little_endian");
|
|
146
|
+
requireString(metadata.exports?.memory, "exports.memory");
|
|
147
|
+
requireString(metadata.exports?.heap_base, "exports.heap_base");
|
|
148
|
+
requireString(metadata.integration.profile?.memory_export, "integration.profile.memory_export");
|
|
149
|
+
requireString(metadata.integration.profile?.heap_base_export, "integration.profile.heap_base_export");
|
|
150
|
+
if (
|
|
151
|
+
metadata.integration.profile.memory_export !== metadata.exports.memory
|
|
152
|
+
|| metadata.integration.profile.heap_base_export !== metadata.exports.heap_base
|
|
153
|
+
) {
|
|
154
|
+
throw new OndaArtifactError(
|
|
155
|
+
"core WebAssembly profile exports do not match the processor export table",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
for (const name of [metadata.exports.memory, metadata.exports.heap_base]) {
|
|
159
|
+
if (!requiredSymbols.has(name)) {
|
|
160
|
+
throw new OndaArtifactError(
|
|
161
|
+
`integration.required_symbols is missing runtime export '${name}'`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!Array.isArray(metadata.integration.profile.imports)) {
|
|
166
|
+
throw new OndaArtifactError("integration.profile.imports must be an array");
|
|
167
|
+
}
|
|
168
|
+
for (const name of metadata.integration.profile.imports) {
|
|
169
|
+
requireString(name, "integration.profile.imports[]");
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
requireString(
|
|
173
|
+
metadata.integration.profile?.symbol_visibility,
|
|
174
|
+
"integration.profile.symbol_visibility",
|
|
175
|
+
);
|
|
176
|
+
if (profileKind === "webassembly_relocatable_object") {
|
|
177
|
+
requireBoolean(metadata.integration.profile?.no_entry, "integration.profile.no_entry");
|
|
178
|
+
requireBoolean(
|
|
179
|
+
metadata.integration.profile?.export_memory,
|
|
180
|
+
"integration.profile.export_memory",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!metadata.metadata || typeof metadata.metadata !== "object") {
|
|
185
|
+
throw new OndaArtifactError("metadata payload must be an object");
|
|
186
|
+
}
|
|
187
|
+
for (const field of [
|
|
188
|
+
"states",
|
|
189
|
+
"inputs",
|
|
190
|
+
"outputs",
|
|
191
|
+
"control_outputs",
|
|
192
|
+
"params",
|
|
193
|
+
"buffers",
|
|
194
|
+
"events",
|
|
195
|
+
]) {
|
|
196
|
+
if (!Array.isArray(metadata.metadata[field])) {
|
|
197
|
+
throw new OndaArtifactError(`metadata.${field} must be an array`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const field of ["inputs", "outputs", "control_outputs", "params"]) {
|
|
201
|
+
metadata.metadata[field].forEach((entry, index) =>
|
|
202
|
+
validateIoMetadata(entry, `metadata.${field}[${index}]`)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
metadata.metadata.buffers.forEach((entry, index) =>
|
|
206
|
+
validateBufferMetadata(entry, `metadata.buffers[${index}]`)
|
|
207
|
+
);
|
|
208
|
+
metadata.metadata.events.forEach((entry, index) =>
|
|
209
|
+
validateEventMetadata(entry, `metadata.events[${index}]`)
|
|
210
|
+
);
|
|
211
|
+
const describedEventExports = metadata.metadata.events.map((event) => event.export);
|
|
212
|
+
if (
|
|
213
|
+
describedEventExports.length !== metadata.exports.events.length
|
|
214
|
+
|| describedEventExports.some((name, index) => name !== metadata.exports.events[index])
|
|
215
|
+
) {
|
|
216
|
+
throw new OndaArtifactError(
|
|
217
|
+
"metadata.events exports must match exports.events in declaration order",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
metadata.metadata.states.forEach((entry, index) =>
|
|
221
|
+
validateStateMetadata(entry, `metadata.states[${index}]`)
|
|
222
|
+
);
|
|
223
|
+
validateRuntimeLayouts(metadata);
|
|
224
|
+
if (metadata.required_features !== undefined) {
|
|
225
|
+
requireStringArray(metadata.required_features, "required_features");
|
|
226
|
+
}
|
|
227
|
+
if (metadata.optimization !== undefined) {
|
|
228
|
+
requireBoolean(metadata.optimization?.enabled, "optimization.enabled");
|
|
229
|
+
requireInteger(metadata.optimization?.level, "optimization.level", 0);
|
|
230
|
+
requireInteger(metadata.optimization?.shrink_level, "optimization.shrink_level", 0);
|
|
231
|
+
requireBoolean(metadata.optimization?.fast_math, "optimization.fast_math");
|
|
232
|
+
requireBoolean(metadata.optimization?.simd, "optimization.simd");
|
|
233
|
+
requireBoolean(
|
|
234
|
+
metadata.optimization?.inline_functions_with_loops,
|
|
235
|
+
"optimization.inline_functions_with_loops",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (metadata.integrity !== undefined) {
|
|
239
|
+
requireString(metadata.integrity?.algorithm, "integrity.algorithm");
|
|
240
|
+
requireString(metadata.integrity?.wasm, "integrity.wasm");
|
|
241
|
+
}
|
|
242
|
+
return metadata;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function validateIoMetadata(value, path) {
|
|
246
|
+
requireString(value?.name, `${path}.name`);
|
|
247
|
+
requireString(value?.type_repr, `${path}.type_repr`);
|
|
248
|
+
requireScalar(value?.scalar, `${path}.scalar`);
|
|
249
|
+
requireInteger(value?.array_len, `${path}.array_len`, 1);
|
|
250
|
+
requireInteger(value?.element_size_bytes, `${path}.element_size_bytes`, 1);
|
|
251
|
+
requireInteger(value?.slot_offset, `${path}.slot_offset`, 0);
|
|
252
|
+
requireNullableInteger(value?.byte_offset, `${path}.byte_offset`, 0);
|
|
253
|
+
requireNullableInteger(value?.state_byte_offset, `${path}.state_byte_offset`, 0);
|
|
254
|
+
requireInteger(value?.byte_size, `${path}.byte_size`, 1);
|
|
255
|
+
requireNullableStringArray(value?.default_reprs, `${path}.default_reprs`);
|
|
256
|
+
requireNullableString(value?.range_min_repr, `${path}.range_min_repr`);
|
|
257
|
+
requireNullableString(value?.range_max_repr, `${path}.range_max_repr`);
|
|
258
|
+
requireScalarLayout(value, path);
|
|
259
|
+
if (value.default_reprs !== null && value.default_reprs.length !== value.array_len) {
|
|
260
|
+
throw new OndaArtifactError(`${path}.default_reprs must contain one value per element`);
|
|
261
|
+
}
|
|
262
|
+
if ((value.range_min_repr === null) !== (value.range_max_repr === null)) {
|
|
263
|
+
throw new OndaArtifactError(`${path} range bounds must either both be present or both be null`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function validateBufferMetadata(value, path) {
|
|
268
|
+
requireString(value?.name, `${path}.name`);
|
|
269
|
+
requireString(value?.type_repr, `${path}.type_repr`);
|
|
270
|
+
requireScalar(value?.scalar, `${path}.scalar`);
|
|
271
|
+
requireInteger(value?.element_size_bytes, `${path}.element_size_bytes`, 1);
|
|
272
|
+
if (!["mono", "static", "dynamic"].includes(value?.channels)) {
|
|
273
|
+
throw new OndaArtifactError(`${path}.channels has an unsupported value`);
|
|
274
|
+
}
|
|
275
|
+
requireNullableInteger(value?.static_channels, `${path}.static_channels`, 1);
|
|
276
|
+
requireScalarElementSize(value, path);
|
|
277
|
+
if (
|
|
278
|
+
(value.channels === "mono" && value.static_channels !== 1)
|
|
279
|
+
|| (value.channels === "static" && value.static_channels === null)
|
|
280
|
+
|| (value.channels === "dynamic" && value.static_channels !== null)
|
|
281
|
+
) {
|
|
282
|
+
throw new OndaArtifactError(`${path}.static_channels is inconsistent with channels`);
|
|
283
|
+
}
|
|
284
|
+
if (!["read_only", "read_write"].includes(value?.access)) {
|
|
285
|
+
throw new OndaArtifactError(`${path}.access has an unsupported value`);
|
|
286
|
+
}
|
|
287
|
+
requireBoolean(value?.may_write, `${path}.may_write`);
|
|
288
|
+
if (value.may_write !== (value.access === "read_write")) {
|
|
289
|
+
throw new OndaArtifactError(`${path}.may_write must reflect the declared access capability`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function validateEventMetadata(value, path) {
|
|
294
|
+
requireString(value?.name, `${path}.name`);
|
|
295
|
+
requireString(value?.export, `${path}.export`);
|
|
296
|
+
requireNullableInteger(value?.payload_size_bytes, `${path}.payload_size_bytes`, 0);
|
|
297
|
+
requireInteger(value?.payload_min_size_bytes, `${path}.payload_min_size_bytes`, 0);
|
|
298
|
+
requireBoolean(value?.has_dynamic_payload, `${path}.has_dynamic_payload`);
|
|
299
|
+
if (!Array.isArray(value?.params)) {
|
|
300
|
+
throw new OndaArtifactError(`${path}.params must be an array`);
|
|
301
|
+
}
|
|
302
|
+
let minimumSize = 0;
|
|
303
|
+
let hasDynamicParam = false;
|
|
304
|
+
value.params.forEach((param, index) => {
|
|
305
|
+
const paramPath = `${path}.params[${index}]`;
|
|
306
|
+
requireString(param?.name, `${paramPath}.name`);
|
|
307
|
+
requireString(param?.type_repr, `${paramPath}.type_repr`);
|
|
308
|
+
requireScalar(param?.scalar, `${paramPath}.scalar`);
|
|
309
|
+
requireInteger(param?.array_len, `${paramPath}.array_len`, 0);
|
|
310
|
+
requireBoolean(param?.is_slice, `${paramPath}.is_slice`);
|
|
311
|
+
requireNullableInteger(param?.byte_offset, `${paramPath}.byte_offset`, 0);
|
|
312
|
+
requireNullableInteger(param?.byte_size, `${paramPath}.byte_size`, 0);
|
|
313
|
+
requireInteger(param?.element_size_bytes, `${paramPath}.element_size_bytes`, 1);
|
|
314
|
+
requireBoolean(param?.has_default, `${paramPath}.has_default`);
|
|
315
|
+
requireNullableStringArray(param?.default_reprs, `${paramPath}.default_reprs`);
|
|
316
|
+
requireScalarElementSize(param, paramPath);
|
|
317
|
+
if (hasDynamicParam ? param.byte_offset !== null : param.byte_offset !== minimumSize) {
|
|
318
|
+
throw new OndaArtifactError(`${paramPath}.byte_offset is inconsistent with event layout`);
|
|
319
|
+
}
|
|
320
|
+
if (param.is_slice) {
|
|
321
|
+
if (param.array_len !== 0 || param.byte_size !== null || param.has_default) {
|
|
322
|
+
throw new OndaArtifactError(`${paramPath} has an invalid slice descriptor`);
|
|
323
|
+
}
|
|
324
|
+
minimumSize += 4;
|
|
325
|
+
hasDynamicParam = true;
|
|
326
|
+
} else {
|
|
327
|
+
if (param.array_len < 1 || param.byte_size !== param.element_size_bytes * param.array_len) {
|
|
328
|
+
throw new OndaArtifactError(`${paramPath} has an invalid fixed-size descriptor`);
|
|
329
|
+
}
|
|
330
|
+
minimumSize += param.byte_size;
|
|
331
|
+
}
|
|
332
|
+
if (param.has_default !== (param.default_reprs !== null)) {
|
|
333
|
+
throw new OndaArtifactError(`${paramPath}.has_default must reflect default_reprs`);
|
|
334
|
+
}
|
|
335
|
+
if (param.default_reprs !== null && param.default_reprs.length !== param.array_len) {
|
|
336
|
+
throw new OndaArtifactError(`${paramPath}.default_reprs must contain one value per element`);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
if (value.payload_min_size_bytes !== minimumSize) {
|
|
340
|
+
throw new OndaArtifactError(`${path}.payload_min_size_bytes is inconsistent with params`);
|
|
341
|
+
}
|
|
342
|
+
if (value.has_dynamic_payload !== hasDynamicParam) {
|
|
343
|
+
throw new OndaArtifactError(`${path}.has_dynamic_payload is inconsistent with params`);
|
|
344
|
+
}
|
|
345
|
+
if (
|
|
346
|
+
hasDynamicParam
|
|
347
|
+
? value.payload_size_bytes !== null
|
|
348
|
+
: value.payload_size_bytes !== minimumSize
|
|
349
|
+
) {
|
|
350
|
+
throw new OndaArtifactError(`${path}.payload_size_bytes is inconsistent with params`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function validateStateMetadata(value, path) {
|
|
355
|
+
requireString(value?.name, `${path}.name`);
|
|
356
|
+
requireString(value?.type_repr, `${path}.type_repr`);
|
|
357
|
+
requireScalar(value?.scalar, `${path}.scalar`);
|
|
358
|
+
requireInteger(value?.array_len, `${path}.array_len`, 1);
|
|
359
|
+
requireInteger(value?.element_size_bytes, `${path}.element_size_bytes`, 1);
|
|
360
|
+
requireInteger(
|
|
361
|
+
value?.packed_snapshot_byte_offset,
|
|
362
|
+
`${path}.packed_snapshot_byte_offset`,
|
|
363
|
+
0,
|
|
364
|
+
);
|
|
365
|
+
requireInteger(
|
|
366
|
+
value?.physical_state_byte_offset,
|
|
367
|
+
`${path}.physical_state_byte_offset`,
|
|
368
|
+
0,
|
|
369
|
+
);
|
|
370
|
+
requireInteger(value?.byte_size, `${path}.byte_size`, 1);
|
|
371
|
+
requireScalarLayout(value, path);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function validateRuntimeLayouts(descriptor) {
|
|
375
|
+
const { metadata, runtime } = descriptor;
|
|
376
|
+
for (const field of ["inputs", "outputs", "control_outputs", "params"]) {
|
|
377
|
+
validateSequentialSlots(metadata[field], `metadata.${field}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (const field of ["inputs", "outputs"]) {
|
|
381
|
+
validateLogicalByteOffsets(metadata[field], `metadata.${field}`);
|
|
382
|
+
metadata[field].forEach((entry, index) => {
|
|
383
|
+
if (entry.state_byte_offset !== null) {
|
|
384
|
+
throw new OndaArtifactError(
|
|
385
|
+
`metadata.${field}[${index}].state_byte_offset must be null`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const paramRegions = metadata.params.map((entry, index) => {
|
|
392
|
+
if (entry.byte_offset === null) {
|
|
393
|
+
throw new OndaArtifactError(`metadata.params[${index}].byte_offset must not be null`);
|
|
394
|
+
}
|
|
395
|
+
if (entry.state_byte_offset !== null) {
|
|
396
|
+
throw new OndaArtifactError(
|
|
397
|
+
`metadata.params[${index}].state_byte_offset must be null`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return storageRegion(
|
|
401
|
+
entry.byte_offset,
|
|
402
|
+
entry.byte_size,
|
|
403
|
+
`metadata.params[${index}]`,
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
validateNonOverlappingRegions(
|
|
407
|
+
paramRegions,
|
|
408
|
+
runtime.param_size_bytes,
|
|
409
|
+
"runtime parameter storage",
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
validateLogicalByteOffsets(
|
|
413
|
+
metadata.control_outputs,
|
|
414
|
+
"metadata.control_outputs",
|
|
415
|
+
);
|
|
416
|
+
const physicalStateRegions = metadata.states.map((entry, index) =>
|
|
417
|
+
storageRegion(
|
|
418
|
+
entry.physical_state_byte_offset,
|
|
419
|
+
entry.byte_size,
|
|
420
|
+
`metadata.states[${index}]`,
|
|
421
|
+
)
|
|
422
|
+
);
|
|
423
|
+
for (const [index, entry] of metadata.control_outputs.entries()) {
|
|
424
|
+
if (entry.state_byte_offset === null) {
|
|
425
|
+
throw new OndaArtifactError(
|
|
426
|
+
`metadata.control_outputs[${index}].state_byte_offset must not be null`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
physicalStateRegions.push(storageRegion(
|
|
430
|
+
entry.state_byte_offset,
|
|
431
|
+
entry.byte_size,
|
|
432
|
+
`metadata.control_outputs[${index}]`,
|
|
433
|
+
));
|
|
434
|
+
}
|
|
435
|
+
validateNonOverlappingRegions(
|
|
436
|
+
physicalStateRegions,
|
|
437
|
+
runtime.state_size_bytes,
|
|
438
|
+
"runtime physical-state storage",
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
let packedOffset = 0;
|
|
442
|
+
for (const [index, entry] of metadata.states.entries()) {
|
|
443
|
+
if (entry.packed_snapshot_byte_offset !== packedOffset) {
|
|
444
|
+
throw new OndaArtifactError(
|
|
445
|
+
`metadata.states[${index}].packed_snapshot_byte_offset must be ${packedOffset}`,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
packedOffset = checkedLayoutEnd(
|
|
449
|
+
packedOffset,
|
|
450
|
+
entry.byte_size,
|
|
451
|
+
`metadata.states[${index}] packed snapshot`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
if (packedOffset !== runtime.snapshot_size_bytes) {
|
|
455
|
+
throw new OndaArtifactError(
|
|
456
|
+
`metadata.states describe ${packedOffset} snapshot bytes; runtime.snapshot_size_bytes is ${runtime.snapshot_size_bytes}`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function validateSequentialSlots(entries, path) {
|
|
462
|
+
let expected = 0;
|
|
463
|
+
for (const [index, entry] of entries.entries()) {
|
|
464
|
+
if (entry.slot_offset !== expected) {
|
|
465
|
+
throw new OndaArtifactError(`${path}[${index}].slot_offset must be ${expected}`);
|
|
466
|
+
}
|
|
467
|
+
expected = checkedLayoutEnd(expected, entry.array_len, `${path}[${index}] slots`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function validateLogicalByteOffsets(entries, path) {
|
|
472
|
+
let expected = 0;
|
|
473
|
+
for (const [index, entry] of entries.entries()) {
|
|
474
|
+
if (entry.byte_offset !== null && entry.byte_offset !== expected) {
|
|
475
|
+
throw new OndaArtifactError(`${path}[${index}].byte_offset must be ${expected} or null`);
|
|
476
|
+
}
|
|
477
|
+
expected = checkedLayoutEnd(expected, entry.byte_size, `${path}[${index}] bytes`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function storageRegion(offset, size, path) {
|
|
482
|
+
return {
|
|
483
|
+
offset,
|
|
484
|
+
end: checkedLayoutEnd(offset, size, path),
|
|
485
|
+
path,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function validateNonOverlappingRegions(regions, regionSize, description) {
|
|
490
|
+
const sorted = [...regions].sort((lhs, rhs) => lhs.offset - rhs.offset);
|
|
491
|
+
let previous = null;
|
|
492
|
+
for (const region of sorted) {
|
|
493
|
+
if (region.end > regionSize) {
|
|
494
|
+
throw new OndaArtifactError(
|
|
495
|
+
`${region.path} exceeds ${description} size ${regionSize}`,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
if (previous !== null && region.offset < previous.end) {
|
|
499
|
+
throw new OndaArtifactError(
|
|
500
|
+
`${region.path} overlaps ${previous.path} in ${description}`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
previous = region;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function checkedLayoutEnd(offset, size, path) {
|
|
508
|
+
const end = offset + size;
|
|
509
|
+
if (!Number.isSafeInteger(end)) {
|
|
510
|
+
throw new OndaArtifactError(`${path} byte extent exceeds the safe integer range`);
|
|
511
|
+
}
|
|
512
|
+
return end;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function validateProcessorArtifact(
|
|
516
|
+
artifact,
|
|
517
|
+
{ inspectModule = true } = {},
|
|
518
|
+
) {
|
|
519
|
+
if (!artifact || typeof artifact !== "object") {
|
|
520
|
+
throw new OndaArtifactError("processor artifact must be an object");
|
|
521
|
+
}
|
|
522
|
+
const wasm = asUint8Array(artifact.wasm);
|
|
523
|
+
const metadata = validateProcessorMetadata(
|
|
524
|
+
artifact.metadata,
|
|
525
|
+
"webassembly_module",
|
|
526
|
+
);
|
|
527
|
+
if (!WebAssembly.validate(wasm)) {
|
|
528
|
+
throw new OndaArtifactError("processor artifact is not valid WebAssembly");
|
|
529
|
+
}
|
|
530
|
+
if (!inspectModule) return { wasm, metadata };
|
|
531
|
+
const module = new WebAssembly.Module(wasm);
|
|
532
|
+
validateProcessorModule(module, metadata);
|
|
533
|
+
return { wasm, metadata };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function validateProcessorModule(module, metadataInput) {
|
|
537
|
+
let imports;
|
|
538
|
+
let moduleExports;
|
|
539
|
+
try {
|
|
540
|
+
imports = WebAssembly.Module.imports(module);
|
|
541
|
+
moduleExports = WebAssembly.Module.exports(module);
|
|
542
|
+
} catch {
|
|
543
|
+
throw new OndaArtifactError("processor module must be a WebAssembly.Module");
|
|
544
|
+
}
|
|
545
|
+
const metadata = validateProcessorMetadata(
|
|
546
|
+
metadataInput,
|
|
547
|
+
"webassembly_module",
|
|
548
|
+
);
|
|
549
|
+
if (imports.length !== 0) {
|
|
550
|
+
throw new OndaArtifactError(
|
|
551
|
+
`processor artifact has unexpected imports: ${imports.map((entry) => `${entry.module}.${entry.name}`).join(", ")}`,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
const exports = new Map(
|
|
555
|
+
moduleExports.map((entry) => [entry.name, entry.kind]),
|
|
556
|
+
);
|
|
557
|
+
for (const name of metadata.integration.required_symbols) {
|
|
558
|
+
if (!exports.has(name)) {
|
|
559
|
+
throw new OndaArtifactError(
|
|
560
|
+
`processor artifact is missing required export '${name}'`,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const expectedKinds = new Map([
|
|
565
|
+
[metadata.exports.memory, "memory"],
|
|
566
|
+
[metadata.exports.heap_base, "global"],
|
|
567
|
+
[metadata.exports.init, "function"],
|
|
568
|
+
[metadata.exports.process, "function"],
|
|
569
|
+
...metadata.exports.events.map((name) => [name, "function"]),
|
|
570
|
+
]);
|
|
571
|
+
for (const [name, expectedKind] of expectedKinds) {
|
|
572
|
+
const actualKind = exports.get(name);
|
|
573
|
+
if (actualKind !== expectedKind) {
|
|
574
|
+
throw new OndaArtifactError(
|
|
575
|
+
`processor export '${name}' must be a ${expectedKind}, got ${String(actualKind)}`,
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return { module, metadata };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function serializeProcessorMetadata(metadata, space = 2) {
|
|
583
|
+
validateProcessorMetadata(metadata);
|
|
584
|
+
return `${JSON.stringify(metadata, null, space)}\n`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export function parseProcessorMetadata(input, expectedKind = null) {
|
|
588
|
+
let metadata;
|
|
589
|
+
try {
|
|
590
|
+
metadata = typeof input === "string" ? JSON.parse(input) : input;
|
|
591
|
+
} catch (error) {
|
|
592
|
+
throw new OndaArtifactError(`invalid processor metadata JSON: ${error.message}`);
|
|
593
|
+
}
|
|
594
|
+
return validateProcessorMetadata(metadata, expectedKind);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export async function createProcessorArtifactFiles(
|
|
598
|
+
artifact,
|
|
599
|
+
{ baseName = "processor" } = {},
|
|
600
|
+
) {
|
|
601
|
+
const { wasm, metadata } = validateProcessorArtifact(artifact);
|
|
602
|
+
const digest = await sha256Hex(wasm);
|
|
603
|
+
const serializedMetadata = {
|
|
604
|
+
...metadata,
|
|
605
|
+
integrity: {
|
|
606
|
+
algorithm: "sha256",
|
|
607
|
+
wasm: digest,
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
const safeBaseName = sanitizeBaseName(baseName);
|
|
611
|
+
return {
|
|
612
|
+
wasm: {
|
|
613
|
+
name: `${safeBaseName}.wasm`,
|
|
614
|
+
mediaType: "application/wasm",
|
|
615
|
+
bytes: wasm.slice(),
|
|
616
|
+
},
|
|
617
|
+
metadata: {
|
|
618
|
+
name: `${safeBaseName}.onda.json`,
|
|
619
|
+
mediaType: "application/json",
|
|
620
|
+
text: serializeProcessorMetadata(serializedMetadata),
|
|
621
|
+
value: serializedMetadata,
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export async function loadProcessorArtifactFiles(
|
|
627
|
+
wasmInput,
|
|
628
|
+
metadataInput,
|
|
629
|
+
) {
|
|
630
|
+
const metadata = parseProcessorMetadata(metadataInput, "webassembly_module");
|
|
631
|
+
const artifact = validateProcessorArtifact({ wasm: wasmInput, metadata });
|
|
632
|
+
const expectedDigest = metadata.integrity?.wasm;
|
|
633
|
+
if (
|
|
634
|
+
metadata.integrity?.algorithm !== "sha256"
|
|
635
|
+
|| typeof expectedDigest !== "string"
|
|
636
|
+
|| !/^[0-9a-f]{64}$/.test(expectedDigest)
|
|
637
|
+
) {
|
|
638
|
+
throw new OndaArtifactError(
|
|
639
|
+
"processor descriptor is missing a valid SHA-256 Wasm integrity digest",
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
const actualDigest = await sha256Hex(artifact.wasm);
|
|
643
|
+
if (actualDigest !== expectedDigest) {
|
|
644
|
+
throw new OndaArtifactError(
|
|
645
|
+
`processor Wasm integrity mismatch; expected ${expectedDigest}, got ${actualDigest}`,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
return artifact;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function asUint8Array(value) {
|
|
652
|
+
if (value instanceof Uint8Array) return value;
|
|
653
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
654
|
+
if (ArrayBuffer.isView(value)) {
|
|
655
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
656
|
+
}
|
|
657
|
+
throw new OndaArtifactError("processor artifact wasm must be bytes");
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function requireString(value, path, allowEmpty = false) {
|
|
661
|
+
if (typeof value !== "string" || (!allowEmpty && value.length === 0)) {
|
|
662
|
+
throw new OndaArtifactError(
|
|
663
|
+
`${path} must be ${allowEmpty ? "a string" : "a non-empty string"}`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function requireLiteral(value, path, expected) {
|
|
669
|
+
if (value !== expected) {
|
|
670
|
+
throw new OndaArtifactError(
|
|
671
|
+
`${path} must be '${expected}' for processor ABI version ${PROCESSOR_ABI_VERSION}`,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function requireBoolean(value, path) {
|
|
677
|
+
if (typeof value !== "boolean") {
|
|
678
|
+
throw new OndaArtifactError(`${path} must be a boolean`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function requireNullableInteger(value, path, minimum) {
|
|
683
|
+
if (value === undefined) {
|
|
684
|
+
throw new OndaArtifactError(`${path} must be present`);
|
|
685
|
+
}
|
|
686
|
+
if (value !== null) requireInteger(value, path, minimum);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function requireNullableString(value, path) {
|
|
690
|
+
if (value === undefined) {
|
|
691
|
+
throw new OndaArtifactError(`${path} must be present`);
|
|
692
|
+
}
|
|
693
|
+
if (value !== null) requireString(value, path, true);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function requireStringArray(value, path) {
|
|
697
|
+
if (!Array.isArray(value)) {
|
|
698
|
+
throw new OndaArtifactError(`${path} must be an array`);
|
|
699
|
+
}
|
|
700
|
+
for (const entry of value) requireString(entry, `${path}[]`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function requireNullableStringArray(value, path) {
|
|
704
|
+
if (value === undefined) {
|
|
705
|
+
throw new OndaArtifactError(`${path} must be present`);
|
|
706
|
+
}
|
|
707
|
+
if (value !== null) requireStringArray(value, path);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function requireScalarElementSize(value, path) {
|
|
711
|
+
const expected = value.scalar === "bool" ? 1
|
|
712
|
+
: value.scalar === "f32" || value.scalar === "i32" ? 4
|
|
713
|
+
: 8;
|
|
714
|
+
if (value.element_size_bytes !== expected) {
|
|
715
|
+
throw new OndaArtifactError(`${path}.element_size_bytes does not match scalar`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function requireScalarLayout(value, path) {
|
|
720
|
+
requireScalarElementSize(value, path);
|
|
721
|
+
if (value.byte_size !== value.element_size_bytes * value.array_len) {
|
|
722
|
+
throw new OndaArtifactError(`${path}.byte_size does not match its scalar array shape`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function requireScalar(value, path) {
|
|
727
|
+
if (!["f32", "f64", "i32", "i64", "bool"].includes(value)) {
|
|
728
|
+
throw new OndaArtifactError(`${path} must name a MIR scalar type`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function requireInteger(value, path, minimum) {
|
|
733
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
734
|
+
throw new OndaArtifactError(
|
|
735
|
+
`${path} must be a safe integer greater than or equal to ${minimum}`,
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function requirePositiveFinite(value, path) {
|
|
741
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
742
|
+
throw new OndaArtifactError(`${path} must be a positive finite number`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function sanitizeBaseName(value) {
|
|
747
|
+
if (typeof value !== "string") {
|
|
748
|
+
throw new OndaArtifactError("artifact baseName must be a string");
|
|
749
|
+
}
|
|
750
|
+
const result = value.trim().replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
751
|
+
.replace(/^-+|-+$/g, "");
|
|
752
|
+
if (!result || result === "." || result === "..") {
|
|
753
|
+
throw new OndaArtifactError("artifact baseName has no usable characters");
|
|
754
|
+
}
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async function sha256Hex(bytes) {
|
|
759
|
+
const subtle = globalThis.crypto?.subtle;
|
|
760
|
+
if (!subtle) {
|
|
761
|
+
throw new OndaArtifactError(
|
|
762
|
+
"Web Crypto is required to create an integrity-checked processor artifact",
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
const digest = new Uint8Array(await subtle.digest("SHA-256", bytes));
|
|
766
|
+
return [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
767
|
+
}
|