@8bitscript/backend-web 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +28 -0
- package/src/index.mjs +360 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 8BitScript 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/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@8bitscript/backend-web",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Internal: lowers IR to generated AssemblyScript and drives asc to a .wasm.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=26"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"assemblyscript": "0.28.20",
|
|
12
|
+
"@8bitscript/compiler": "0.1.0"
|
|
13
|
+
},
|
|
14
|
+
"main": "./src/index.mjs",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.mjs",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "node --test"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// The web backend: IR in, .wasm out.
|
|
2
|
+
//
|
|
3
|
+
// It emits AssemblyScript and hands it to asc. AssemblyScript's sized integer
|
|
4
|
+
// types match the machine types one-to-one, which is most of why it is the web
|
|
5
|
+
// target's language: `u8` means the same wrapped byte in both worlds. The one
|
|
6
|
+
// wrinkle is that AssemblyScript widens integer arithmetic to i32, so every
|
|
7
|
+
// store narrows back explicitly — `x = <u8>(x + 1)` — which is exactly the
|
|
8
|
+
// wrap-at-assignment semantics the design specifies for this target.
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { dirname } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { PRIMITIVE_INTEGER_TYPES, entryOf } from '@8bitscript/compiler';
|
|
15
|
+
|
|
16
|
+
// AssemblyScript has no 24-bit integer either, so mediumint/umediumint widen
|
|
17
|
+
// the same way they do for the 6502 backend. Bits/signedness come from the
|
|
18
|
+
// compiler's shared type registry rather than a second hand-written table.
|
|
19
|
+
const NATIVE_WIDTH = { 8: 8, 16: 16, 24: 32, 32: 32 };
|
|
20
|
+
const AS_TYPE = Object.fromEntries(
|
|
21
|
+
PRIMITIVE_INTEGER_TYPES.map((t) => [t.canonicalName, `${t.signed ? 'i' : 'u'}${NATIVE_WIDTH[t.bits]}`]),
|
|
22
|
+
);
|
|
23
|
+
AS_TYPE.bool = 'bool';
|
|
24
|
+
// A string value is a pointer into linear memory at its constant,
|
|
25
|
+
// length-prefixed bytes — a static data segment asc lays out from
|
|
26
|
+
// STRING_DATA_BASE (see buildWasm), above the screen agreement in
|
|
27
|
+
// @8bitscript/web so a `clearScreen()` can never write over a label.
|
|
28
|
+
AS_TYPE.string = 'usize';
|
|
29
|
+
// An array parameter is the address of the array's first element, the same
|
|
30
|
+
// way a string parameter is: linear memory, indexed by elementAddress().
|
|
31
|
+
// The element type and the length live in the callee's own signature, so
|
|
32
|
+
// neither travels with the call.
|
|
33
|
+
AS_TYPE.array = 'usize';
|
|
34
|
+
// Bytes one element of each type takes in linear memory: the stride of an
|
|
35
|
+
// array, and what `memory.data(size)` reserves for a `let` array.
|
|
36
|
+
const AS_SIZE = Object.fromEntries(
|
|
37
|
+
PRIMITIVE_INTEGER_TYPES.map((t) => [t.canonicalName, NATIVE_WIDTH[t.bits] / 8]),
|
|
38
|
+
);
|
|
39
|
+
AS_SIZE.bool = 1;
|
|
40
|
+
|
|
41
|
+
/** The address of element `index` of the array `expr.array`, as AssemblyScript. */
|
|
42
|
+
function elementAddress(expr, signatures) {
|
|
43
|
+
const size = AS_SIZE[expr.elementType];
|
|
44
|
+
const index = emitExpression(expr.index, signatures);
|
|
45
|
+
return `<usize>(${emitExpression(expr.array, signatures)} + <usize>(${index})${size === 1 ? '' : ` * ${size}`})`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stringName = (index) => `__8bs_str_${index}`;
|
|
49
|
+
|
|
50
|
+
// Where asc places static data (`--memoryBase`): 0xE000, the top 8KB of
|
|
51
|
+
// the 64KB page — where a Commodore's ROM sits — so constant data never
|
|
52
|
+
// overlaps the character screen at the bottom (@8bitscript/web's
|
|
53
|
+
// WebRegisters agreement ends at 2003) nor anything a program is likely to
|
|
54
|
+
// memory.write() itself.
|
|
55
|
+
export const STRING_DATA_BASE = 0xE000;
|
|
56
|
+
|
|
57
|
+
// `memory.read`/`memory.write` map straight onto AssemblyScript's own
|
|
58
|
+
// linear-memory intrinsics: a byte at a runtime offset is exactly what
|
|
59
|
+
// `load<u8>`/`store<u8>` are for. This makes raw memory access target-
|
|
60
|
+
// symmetric — real hardware on native, a flat 64KB buffer standing in for it
|
|
61
|
+
// on the web, per the `--initialMemory 1` reservation in `buildWasm` below.
|
|
62
|
+
// `signatures` maps every function's name to its parameters' AssemblyScript
|
|
63
|
+
// types, so a call's arguments narrow to what the callee declared — the
|
|
64
|
+
// same wrap-at-assignment rule a store gets, since AssemblyScript widens
|
|
65
|
+
// arithmetic to i32 and refuses to pass a widened value to a `u8` on its
|
|
66
|
+
// own.
|
|
67
|
+
function emitCall(call, signatures) {
|
|
68
|
+
const paramTypes = signatures.get(call.name) ?? [];
|
|
69
|
+
const args = call.args.map((arg, i) => {
|
|
70
|
+
const emitted = emitExpression(arg, signatures);
|
|
71
|
+
return paramTypes[i] ? `<${paramTypes[i]}>${emitted}` : emitted;
|
|
72
|
+
});
|
|
73
|
+
return `${call.name}(${args.join(', ')})`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function emitExpression(expr, signatures) {
|
|
77
|
+
switch (expr.kind) {
|
|
78
|
+
case 'const': return String(expr.value);
|
|
79
|
+
case 'ref': return expr.name;
|
|
80
|
+
case 'binop':
|
|
81
|
+
return `(${emitExpression(expr.left, signatures)} ${expr.operator} ${emitExpression(expr.right, signatures)})`;
|
|
82
|
+
case 'unop':
|
|
83
|
+
return `(${expr.operator}${emitExpression(expr.argument, signatures)})`;
|
|
84
|
+
case 'call':
|
|
85
|
+
return emitCall(expr, signatures);
|
|
86
|
+
case 'memoryRead':
|
|
87
|
+
return `load<u8>(${emitExpression(expr.address, signatures)})`;
|
|
88
|
+
case 'string':
|
|
89
|
+
return stringName(expr.index);
|
|
90
|
+
case 'stringLength':
|
|
91
|
+
// Byte 0 is the length; the characters follow.
|
|
92
|
+
return `load<u8>(${emitExpression(expr.string, signatures)})`;
|
|
93
|
+
case 'stringByte':
|
|
94
|
+
return `load<u8>(<usize>(${emitExpression(expr.string, signatures)} + 1 + ${emitExpression(expr.index, signatures)}))`;
|
|
95
|
+
case 'index':
|
|
96
|
+
return `load<${AS_TYPE[expr.elementType]}>(${elementAddress(expr, signatures)})`;
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`backend-web: unknown IR expression '${expr.kind}'`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function emitStatement(statement, indent, types, returnType, signatures) {
|
|
103
|
+
const pad = ' '.repeat(indent);
|
|
104
|
+
switch (statement.kind) {
|
|
105
|
+
case 'assign': {
|
|
106
|
+
const type = types.get(statement.target) ?? 'i32';
|
|
107
|
+
return `${pad}${statement.target} = <${type}>${emitExpression(statement.value, signatures)};\n`;
|
|
108
|
+
}
|
|
109
|
+
case 'local': {
|
|
110
|
+
const type = AS_TYPE[statement.type];
|
|
111
|
+
return `${pad}let ${statement.name}: ${type} = <${type}>${emitExpression(statement.init, signatures)};\n`;
|
|
112
|
+
}
|
|
113
|
+
case 'stringCopy':
|
|
114
|
+
return `${pad}__8bs_string_copy(${emitExpression(statement.target, signatures)}, ${emitExpression(statement.source, signatures)}, ${statement.capacity});\n`;
|
|
115
|
+
case 'for': {
|
|
116
|
+
const clause = (s) => (s ? emitStatement(s, 0, types, returnType, signatures).trim().replace(/;$/, '') : '');
|
|
117
|
+
let out = `${pad}for (${clause(statement.init)}; ${statement.test ? emitExpression(statement.test, signatures) : ''}; ${clause(statement.update)}) {\n`;
|
|
118
|
+
out += statement.body.map((s) => emitStatement(s, indent + 1, types, returnType, signatures)).join('');
|
|
119
|
+
return `${out}${pad}}\n`;
|
|
120
|
+
}
|
|
121
|
+
case 'storeIndex': {
|
|
122
|
+
// The same narrowing a store to a scalar gets, to the element's width.
|
|
123
|
+
const type = AS_TYPE[statement.elementType];
|
|
124
|
+
return `${pad}store<${type}>(${elementAddress(statement, signatures)}, <${type}>${emitExpression(statement.value, signatures)});\n`;
|
|
125
|
+
}
|
|
126
|
+
case 'call':
|
|
127
|
+
return `${pad}${emitCall(statement, signatures)};\n`;
|
|
128
|
+
case 'waitFrame':
|
|
129
|
+
// The host import declared at the top of the module (see
|
|
130
|
+
// emitAssemblyScript) — the page's worker blocks it on the frame clock,
|
|
131
|
+
// a headless host counts it. Not something the wasm can do alone.
|
|
132
|
+
return `${pad}waitFrame();\n`;
|
|
133
|
+
case 'memoryWrite':
|
|
134
|
+
return `${pad}store<u8>(${emitExpression(statement.address, signatures)}, ${emitExpression(statement.value, signatures)});\n`;
|
|
135
|
+
case 'memoryRead':
|
|
136
|
+
// Only reachable as a bare statement; the byte read is discarded.
|
|
137
|
+
return `${pad}${emitExpression(statement, signatures)};\n`;
|
|
138
|
+
case 'if': {
|
|
139
|
+
let out = `${pad}if (${emitExpression(statement.test, signatures)}) {\n`;
|
|
140
|
+
out += statement.then.map((s) => emitStatement(s, indent + 1, types, returnType, signatures)).join('');
|
|
141
|
+
if (statement.else) {
|
|
142
|
+
out += `${pad}} else {\n`;
|
|
143
|
+
out += statement.else.map((s) => emitStatement(s, indent + 1, types, returnType, signatures)).join('');
|
|
144
|
+
}
|
|
145
|
+
return `${out}${pad}}\n`;
|
|
146
|
+
}
|
|
147
|
+
case 'while': {
|
|
148
|
+
let out = `${pad}while (${emitExpression(statement.test, signatures)}) {\n`;
|
|
149
|
+
out += statement.body.map((s) => emitStatement(s, indent + 1, types, returnType, signatures)).join('');
|
|
150
|
+
return `${out}${pad}}\n`;
|
|
151
|
+
}
|
|
152
|
+
case 'block': {
|
|
153
|
+
let out = `${pad}{\n`;
|
|
154
|
+
out += statement.body.map((s) => emitStatement(s, indent + 1, types, returnType, signatures)).join('');
|
|
155
|
+
return `${out}${pad}}\n`;
|
|
156
|
+
}
|
|
157
|
+
case 'return':
|
|
158
|
+
// Same wrap-at-assignment narrowing a store gets: AS widens arithmetic
|
|
159
|
+
// to i32, so a returned expression is cast back to the declared width.
|
|
160
|
+
return statement.value
|
|
161
|
+
? `${pad}return <${AS_TYPE[returnType]}>${emitExpression(statement.value, signatures)};\n`
|
|
162
|
+
: `${pad}return;\n`;
|
|
163
|
+
case 'break': return `${pad}break;\n`;
|
|
164
|
+
case 'continue': return `${pad}continue;\n`;
|
|
165
|
+
case 'asm':
|
|
166
|
+
throw Object.assign(
|
|
167
|
+
new Error('asm6502 blocks are 6502 code and cannot run on the web target'),
|
|
168
|
+
{ targetLimitation: true },
|
|
169
|
+
);
|
|
170
|
+
default:
|
|
171
|
+
throw new Error(`backend-web: unknown IR statement '${statement.kind}'`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Every statement in every function, nested ones included. */
|
|
176
|
+
function forEachStatement(functions, visit) {
|
|
177
|
+
const walkBody = (body) => {
|
|
178
|
+
for (const s of body) {
|
|
179
|
+
visit(s);
|
|
180
|
+
if (s.kind === 'if') { walkBody(s.then); if (s.else) walkBody(s.else); }
|
|
181
|
+
else if (s.kind === 'while' || s.kind === 'block') walkBody(s.body);
|
|
182
|
+
else if (s.kind === 'for') { if (s.init) visit(s.init); if (s.update) visit(s.update); walkBody(s.body); }
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
for (const fn of functions) walkBody(fn.body);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generate the AssemblyScript module for an IR program.
|
|
190
|
+
*
|
|
191
|
+
* `usesWaitFrame` tells buildWasm() to build with shared memory: the page
|
|
192
|
+
* runs a waitFrame() program in a worker that blocks on the frame clock,
|
|
193
|
+
* and paints its memory from the main thread.
|
|
194
|
+
*
|
|
195
|
+
* @returns {{ ok: true, source: string, usesWaitFrame: boolean } | { ok: false, error: string }}
|
|
196
|
+
*/
|
|
197
|
+
export function emitAssemblyScript(ir) {
|
|
198
|
+
// An array's name is its address (a `usize`), never assigned as a whole,
|
|
199
|
+
// so it has no entry here; a store into it narrows to the element type.
|
|
200
|
+
const types = new Map(ir.globals.filter((g) => !g.array).map((g) => [g.name, AS_TYPE[g.type]]));
|
|
201
|
+
const signatures = new Map(ir.functions.map((fn) => [fn.name, fn.params.map((p) => AS_TYPE[p.type])]));
|
|
202
|
+
let out = '// Generated by 8bs. Do not edit: the source of truth is the .8bs file.\n';
|
|
203
|
+
|
|
204
|
+
let usesWaitFrame = false;
|
|
205
|
+
let usesStringCopy = false;
|
|
206
|
+
forEachStatement(ir.functions, (s) => {
|
|
207
|
+
if (s.kind === 'waitFrame') usesWaitFrame = true;
|
|
208
|
+
if (s.kind === 'stringCopy') usesStringCopy = true;
|
|
209
|
+
});
|
|
210
|
+
if (usesWaitFrame) {
|
|
211
|
+
// A host import: the wasm cannot wait on its own (a browser needs the
|
|
212
|
+
// thread back to paint), so "block until the next logical frame" is the
|
|
213
|
+
// one thing the host supplies — see packages/cli/src/web-runtime.mjs
|
|
214
|
+
// (worker + Atomics.wait) and packages/cli/src/wasm-host.mjs (a counter).
|
|
215
|
+
out += '// @ts-ignore: decorator\n';
|
|
216
|
+
out += '@external("env", "waitFrame")\n';
|
|
217
|
+
out += 'declare function waitFrame(): void;\n';
|
|
218
|
+
}
|
|
219
|
+
out += '\n';
|
|
220
|
+
|
|
221
|
+
for (const [index, s] of (ir.strings ?? []).entries()) {
|
|
222
|
+
out += `const ${stringName(index)}: usize = memory.data<u8>([${[s.bytes.length, ...s.bytes].join(', ')}]); // "${s.text}"\n`;
|
|
223
|
+
}
|
|
224
|
+
if (ir.strings?.length) out += '\n';
|
|
225
|
+
if (usesStringCopy) {
|
|
226
|
+
// `name = other` on a string<N>: length byte, then the characters, cut
|
|
227
|
+
// to the capacity — the same helper the 6502 backend emits, in
|
|
228
|
+
// AssemblyScript.
|
|
229
|
+
out += 'function __8bs_string_copy(dst: usize, src: usize, capacity: u8): void {\n';
|
|
230
|
+
out += ' let n: u8 = load<u8>(src);\n';
|
|
231
|
+
out += ' if (n > capacity) n = capacity;\n';
|
|
232
|
+
out += ' store<u8>(dst, n);\n';
|
|
233
|
+
out += ' for (let i: u8 = 0; i < n; i++) store<u8>(dst + 1 + i, load<u8>(src + 1 + i));\n';
|
|
234
|
+
out += '}\n\n';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const g of ir.globals) {
|
|
238
|
+
if (g.array) {
|
|
239
|
+
// An array is a static chunk of linear memory, laid out by asc above
|
|
240
|
+
// STRING_DATA_BASE with the string constants: `const` with its
|
|
241
|
+
// values, `let` zeroed (or with its values), and an `@address` array
|
|
242
|
+
// N cells from that offset in the 64KB page — the web target has no
|
|
243
|
+
// hardware there, but the memory exists, so a screen-RAM-shaped array
|
|
244
|
+
// over the @8bitscript/web agreement is as real as on a Commodore.
|
|
245
|
+
const type = AS_TYPE[g.type];
|
|
246
|
+
const data = g.address !== null ? String(g.address)
|
|
247
|
+
: g.init ? `memory.data<${type}>([${g.init.join(', ')}])`
|
|
248
|
+
: `memory.data(${g.array * AS_SIZE[g.type]})`;
|
|
249
|
+
out += `export const ${g.name}: usize = ${data};\n`;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (g.address !== null) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
error: `'${g.name}' is mapped to hardware address 0x${g.address.toString(16)} with @address; ` +
|
|
256
|
+
'there is no such hardware on the web target',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// Exported so a host can observe the program's state; wasm mutable-global
|
|
260
|
+
// exports are exactly this use case.
|
|
261
|
+
out += `export let ${g.name}: ${AS_TYPE[g.type]} = ${g.init};\n`;
|
|
262
|
+
}
|
|
263
|
+
out += '\n';
|
|
264
|
+
|
|
265
|
+
// Only the entry is a wasm export — the artifact mirrors the language rule
|
|
266
|
+
// (the entry module's one export is the program), and a host finds the
|
|
267
|
+
// program as "the exported function" rather than by a magic name.
|
|
268
|
+
const entry = entryOf(ir);
|
|
269
|
+
try {
|
|
270
|
+
for (const fn of ir.functions) {
|
|
271
|
+
const params = fn.params.map((p) => `${p.name}: ${AS_TYPE[p.type]}`).join(', ');
|
|
272
|
+
const returnType = fn.returnType === 'void' ? 'void' : AS_TYPE[fn.returnType];
|
|
273
|
+
// A parameter is assignable like a global (`value = value / 10`), and
|
|
274
|
+
// narrows back to its own width the same way; it shadows a same-named
|
|
275
|
+
// global inside its function, as the linker's scoping already says.
|
|
276
|
+
// Locals too: a store to one narrows to its declared width. Names
|
|
277
|
+
// are function-wide here (a local shadows a global of the name in
|
|
278
|
+
// every store after its declaration, which the linker's scoping
|
|
279
|
+
// already guarantees is the only place it is named).
|
|
280
|
+
const locals = [];
|
|
281
|
+
forEachStatement([fn], (s) => { if (s.kind === 'local') locals.push([s.name, AS_TYPE[s.type]]); });
|
|
282
|
+
const fnTypes = fn.params.length || locals.length
|
|
283
|
+
? new Map([...types, ...fn.params.map((p) => [p.name, AS_TYPE[p.type]]), ...locals])
|
|
284
|
+
: types;
|
|
285
|
+
out += `${fn.name === entry ? 'export ' : ''}function ${fn.name}(${params}): ${returnType} {\n`;
|
|
286
|
+
out += fn.body.map((s) => emitStatement(s, 1, fnTypes, fn.returnType, signatures)).join('');
|
|
287
|
+
out += '}\n\n';
|
|
288
|
+
}
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (error.targetLimitation) return { ok: false, error: error.message };
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
return { ok: true, source: out, usesWaitFrame };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** The asc binary, from this package's own dependencies. */
|
|
297
|
+
function findAsc() {
|
|
298
|
+
const require = createRequire(import.meta.url);
|
|
299
|
+
return require.resolve('assemblyscript/bin/asc.js');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Compile IR to a .wasm via asc.
|
|
304
|
+
*
|
|
305
|
+
* @param {object} ir
|
|
306
|
+
* @param {{ outFile: string }} options
|
|
307
|
+
* @returns {Promise<{ ok: boolean, asFile?: string, error?: string }>}
|
|
308
|
+
*/
|
|
309
|
+
export async function buildWasm(ir, { outFile }) {
|
|
310
|
+
if (ir.imports?.length) {
|
|
311
|
+
// Unresolved imports mean the caller skipped the linker. Refusing here is
|
|
312
|
+
// what keeps a lower→backend shortcut from silently dropping modules.
|
|
313
|
+
return { ok: false, error: 'the IR still has unresolved imports: link() it before the backend' };
|
|
314
|
+
}
|
|
315
|
+
const emitted = emitAssemblyScript(ir);
|
|
316
|
+
if (!emitted.ok) return { ok: false, error: emitted.error };
|
|
317
|
+
|
|
318
|
+
const asFile = outFile.replace(/\.wasm$/, '.ts');
|
|
319
|
+
await mkdir(dirname(outFile), { recursive: true });
|
|
320
|
+
await writeFile(asFile, emitted.source, 'utf8');
|
|
321
|
+
|
|
322
|
+
return new Promise((resolvePromise) => {
|
|
323
|
+
const child = spawn(
|
|
324
|
+
process.execPath,
|
|
325
|
+
[
|
|
326
|
+
findAsc(), asFile, '-o', outFile, '-O3', '--runtime', 'stub',
|
|
327
|
+
// Static data (string constants) starts here — see STRING_DATA_BASE.
|
|
328
|
+
'--memoryBase', String(STRING_DATA_BASE),
|
|
329
|
+
// One page (64KB) reserved unconditionally, matching the 6502's own
|
|
330
|
+
// 16-bit address space — what memory.read/write address on the web
|
|
331
|
+
// target when a program uses them.
|
|
332
|
+
'--initialMemory', '1',
|
|
333
|
+
// A waitFrame() program's memory is shared: it runs in a worker that
|
|
334
|
+
// blocks on the frame clock while the page paints the same bytes
|
|
335
|
+
// from the main thread. Shared memory needs a maximum, and atomics
|
|
336
|
+
// are behind the threads feature flag. Memory stays exported either
|
|
337
|
+
// way, so every host reads `instance.exports.memory` regardless.
|
|
338
|
+
...(emitted.usesWaitFrame
|
|
339
|
+
? ['--maximumMemory', '1', '--sharedMemory', '--enable', 'threads']
|
|
340
|
+
: []),
|
|
341
|
+
],
|
|
342
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] },
|
|
343
|
+
);
|
|
344
|
+
let stderr = '';
|
|
345
|
+
child.stderr.on('data', (d) => { stderr += d; });
|
|
346
|
+
child.on('close', (code) => {
|
|
347
|
+
if (code === 0) { resolvePromise({ ok: true, asFile }); return; }
|
|
348
|
+
// Static data — arrays, string<N> variables, string constants — is
|
|
349
|
+
// laid out from STRING_DATA_BASE to the end of the one 64KB page, so
|
|
350
|
+
// there is room for about 8KB of it. asc reports going past the end
|
|
351
|
+
// as needing a second page; that is the limit, in the program's terms.
|
|
352
|
+
const overPage = /requires at least '\d+' pages of maximum memory/.test(stderr);
|
|
353
|
+
const error = overPage
|
|
354
|
+
? `this program's arrays, strings, and string variables need more than the ${65536 - STRING_DATA_BASE} bytes of static data the web target has `
|
|
355
|
+
+ `(the top of its one 64KB page, from 0x${STRING_DATA_BASE.toString(16).toUpperCase()}). Smaller arrays bring it down.\nasc failed:\n${stderr}`
|
|
356
|
+
: `asc failed:\n${stderr}`;
|
|
357
|
+
resolvePromise({ ok: false, asFile, error });
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
}
|