@8bitscript/compiler 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/index.mjs +91 -0
- package/package.json +29 -0
- package/src/ast/index.mjs +90 -0
- package/src/checker/index.mjs +294 -0
- package/src/diagnostics/index.mjs +179 -0
- package/src/fold/facts.mjs +209 -0
- package/src/fold/index.mjs +412 -0
- package/src/intellisense/index.mjs +558 -0
- package/src/ir/index.mjs +1422 -0
- package/src/lexer/index.mjs +383 -0
- package/src/linker/hazards.mjs +121 -0
- package/src/linker/index.mjs +1075 -0
- package/src/parser/index.mjs +795 -0
- package/src/resolver/index.mjs +534 -0
- package/src/templates/index.mjs +278 -0
- package/src/types/index.mjs +116 -0
package/src/ir/index.mjs
ADDED
|
@@ -0,0 +1,1422 @@
|
|
|
1
|
+
// The IR, and the lowering from AST to it.
|
|
2
|
+
//
|
|
3
|
+
// This is the common language between the front end and every backend: the
|
|
4
|
+
// 6502 backend and the web backend both consume exactly this, which is what
|
|
5
|
+
// keeps them from each re-deriving the language from the AST.
|
|
6
|
+
//
|
|
7
|
+
// The IR is *structured* — statements contain statements, expressions are
|
|
8
|
+
// trees — rather than the linear load/store sketch in the design notes. Both
|
|
9
|
+
// current backends want structure (WebAssembly has no goto at all, and C needs
|
|
10
|
+
// none for these shapes), so flattening to a linear form today would only mean
|
|
11
|
+
// rebuilding the structure in each backend. A linear form earns its place when
|
|
12
|
+
// a register allocator does.
|
|
13
|
+
//
|
|
14
|
+
// THE ONE RULE OF LOWERING: it is exhaustive-with-error. Every AST node either
|
|
15
|
+
// has a lowering rule or produces a diagnostic naming the construct. Nothing is
|
|
16
|
+
// silently dropped, ever — a program using an uncompilable feature fails with
|
|
17
|
+
// a message, not with a .prg missing half its logic.
|
|
18
|
+
//
|
|
19
|
+
// And, like every layer before it, IT NEVER THROWS. `analyze()` runs this on
|
|
20
|
+
// every keystroke (see the package entry point), so a half-typed statement —
|
|
21
|
+
// `clearCell =` with nothing after it, `namespace` with no name — is normal
|
|
22
|
+
// input, not an exceptional one. The parser reports the syntax error and
|
|
23
|
+
// leaves a hole in the tree; a hole lowers to nothing, quietly, because the
|
|
24
|
+
// person has already been told about it.
|
|
25
|
+
import { Codes, diagnostic } from '../diagnostics/index.mjs';
|
|
26
|
+
import { NodeType } from '../ast/index.mjs';
|
|
27
|
+
import { resolveIntegerType } from '../types/index.mjs';
|
|
28
|
+
import {
|
|
29
|
+
resolveScalarType, scanDeclaredTypes, parameterTypes, layoutTemplate, isTemplateCall, misplacedTemplate, resolveArrayType, typeForCount, MAX_STRING_LENGTH,
|
|
30
|
+
} from '../templates/index.mjs';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} IrProgram
|
|
34
|
+
* @property {IrImport[]} imports Unresolved until the linker consumes them.
|
|
35
|
+
* @property {IrGlobal[]} globals
|
|
36
|
+
* @property {IrFunction[]} functions
|
|
37
|
+
* @property {{ name: string, type: string, value: number, exported: boolean }[]} consts
|
|
38
|
+
* Top-level `const` declarations: compile-time constants the linker
|
|
39
|
+
* inlines at every reference. They never reach a backend.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// ---- strings ------------------------------------------------------------------
|
|
43
|
+
//
|
|
44
|
+
// A string literal is constant program data: its bytes, length-prefixed
|
|
45
|
+
// (one byte, so at most 255 of them), in the program image — ROM on a
|
|
46
|
+
// cartridge, the .prg on a Commodore, a data segment on the web. A `string`
|
|
47
|
+
// value at runtime is a pointer to that. Lowering keeps a per-module table
|
|
48
|
+
// of them (`ir.strings`, deduplicated by content) and a literal lowers to
|
|
49
|
+
// `{ kind: 'string', index }`; the linker merges the tables and rebases the
|
|
50
|
+
// indices, and each backend emits the table in its own spelling.
|
|
51
|
+
//
|
|
52
|
+
// The portable character set and the 255-byte limit are the checker's
|
|
53
|
+
// rules (checker/index.mjs, UNPORTABLE_CHARACTER / STRING_TOO_LONG), so
|
|
54
|
+
// they reach the editor; check() always runs before lower(), so a string
|
|
55
|
+
// that gets here is one the checker accepted.
|
|
56
|
+
|
|
57
|
+
class Lowering {
|
|
58
|
+
constructor(file) {
|
|
59
|
+
this.file = file;
|
|
60
|
+
this.diagnostics = [];
|
|
61
|
+
this.imports = [];
|
|
62
|
+
this.globals = [];
|
|
63
|
+
this.functions = [];
|
|
64
|
+
this.namespaces = [];
|
|
65
|
+
this.strings = [];
|
|
66
|
+
this.consts = [];
|
|
67
|
+
// This module's own top-level consts, name to value, scanned in
|
|
68
|
+
// program() before anything is lowered. A reference to one becomes the
|
|
69
|
+
// value right here — so it reaches the literal-only positions a number
|
|
70
|
+
// is allowed in (an `@address`, a `memory.write` argument's range
|
|
71
|
+
// check, a global's initialiser) exactly as a literal would. An
|
|
72
|
+
// *imported* const is not in this map: only the linker knows the other
|
|
73
|
+
// module, so those are resolved there.
|
|
74
|
+
this.ownConsts = new Map();
|
|
75
|
+
// What lowering can know about names without a binder: this module's
|
|
76
|
+
// own globals' and functions' declared types (scanned in program())
|
|
77
|
+
// and the current function's parameters — what sizes a `${...}` field
|
|
78
|
+
// from its expression when the width is left off (see templates/).
|
|
79
|
+
this.globalTypes = new Map();
|
|
80
|
+
this.functionTypes = new Map();
|
|
81
|
+
this.functionArity = new Map();
|
|
82
|
+
this.currentParams = new Map();
|
|
83
|
+
this.currentArrayParams = new Map();
|
|
84
|
+
// This module's own arrays, name to { type, length, constant }: what
|
|
85
|
+
// `a[i]` and `a.length` mean here. An imported array is resolved by the
|
|
86
|
+
// linker, which fills in the element type it cannot know from here.
|
|
87
|
+
this.arrays = new Map();
|
|
88
|
+
// This module's own string consts, name to string-table slot, and its
|
|
89
|
+
// `string<N>` variables, name to capacity N.
|
|
90
|
+
this.ownStrings = new Map();
|
|
91
|
+
this.stringBuffers = new Map();
|
|
92
|
+
// Own consts whose value only the linker can see (see global()): a
|
|
93
|
+
// reference to one is a `ref`, not a declared name of this module.
|
|
94
|
+
this.pendingConsts = new Set();
|
|
95
|
+
// Every top-level const by name, whatever its value turns out to be.
|
|
96
|
+
this.constNames = new Set();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The name scope a template field's type is inferred in. */
|
|
100
|
+
get typeScope() {
|
|
101
|
+
return {
|
|
102
|
+
paramTypes: this.currentParams, globalTypes: this.globalTypes,
|
|
103
|
+
functionTypes: this.functionTypes, arrayTypes: this.arrays,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fail(node, message) {
|
|
108
|
+
this.diagnostics.push(
|
|
109
|
+
diagnostic(Codes.NOT_COMPILABLE, message, this.file, node.start, node.length),
|
|
110
|
+
);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
program(ast) {
|
|
115
|
+
({
|
|
116
|
+
globalTypes: this.globalTypes, functionTypes: this.functionTypes,
|
|
117
|
+
arrayTypes: this.arrays, functionArity: this.functionArity,
|
|
118
|
+
} = scanDeclaredTypes(ast));
|
|
119
|
+
// In source order, so a const may be written in terms of one above it
|
|
120
|
+
// (`const WIDE: u8 = CELL * 2;` is not folded — only a name or a
|
|
121
|
+
// literal, the same narrowness the checker's own literal rule has).
|
|
122
|
+
for (const node of ast.body) {
|
|
123
|
+
if (node.type !== NodeType.VariableDeclaration || node.kind !== 'const' || !node.name) continue;
|
|
124
|
+
this.constNames.add(node.name.name);
|
|
125
|
+
const value = this.constInitialiser(node.initializer);
|
|
126
|
+
if (value !== null) this.ownConsts.set(node.name.name, value);
|
|
127
|
+
}
|
|
128
|
+
for (const node of ast.body) {
|
|
129
|
+
switch (node.type) {
|
|
130
|
+
case NodeType.VariableDeclaration:
|
|
131
|
+
this.global(node);
|
|
132
|
+
break;
|
|
133
|
+
case NodeType.FunctionDeclaration:
|
|
134
|
+
this.function(node);
|
|
135
|
+
break;
|
|
136
|
+
case NodeType.NamespaceDeclaration:
|
|
137
|
+
this.namespaceDeclaration(node);
|
|
138
|
+
break;
|
|
139
|
+
case NodeType.ImportDeclaration:
|
|
140
|
+
this.import(node);
|
|
141
|
+
break;
|
|
142
|
+
default:
|
|
143
|
+
this.fail(node, `a top-level ${node.type} is not compilable yet`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
imports: this.imports,
|
|
148
|
+
globals: this.globals,
|
|
149
|
+
functions: this.functions,
|
|
150
|
+
namespaces: this.namespaces,
|
|
151
|
+
strings: this.strings,
|
|
152
|
+
consts: this.consts,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The value of a top-level const's initialiser: a literal, a negated
|
|
158
|
+
* literal, or a const declared above it. Null for anything else — an
|
|
159
|
+
* imported name (the linker's job) or an expression (not folded).
|
|
160
|
+
*/
|
|
161
|
+
constInitialiser(node) {
|
|
162
|
+
if (!node) return null;
|
|
163
|
+
if (node.type === NodeType.IntegerLiteral) return node.value;
|
|
164
|
+
if (node.type === NodeType.BooleanLiteral) return node.value ? 1 : 0;
|
|
165
|
+
if (
|
|
166
|
+
node.type === NodeType.UnaryExpression
|
|
167
|
+
&& (node.operator === '-' || node.operator === '+')
|
|
168
|
+
&& node.argument?.type === NodeType.IntegerLiteral
|
|
169
|
+
) {
|
|
170
|
+
return node.operator === '-' ? -node.argument.value : node.argument.value;
|
|
171
|
+
}
|
|
172
|
+
if (node.type === NodeType.Identifier) return this.ownConsts.get(node.name) ?? null;
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A string literal (or a template's text run) as constant data: the IR
|
|
178
|
+
* expression naming its slot in this module's string table.
|
|
179
|
+
*/
|
|
180
|
+
stringConstant(node, value) {
|
|
181
|
+
let index = this.strings.findIndex((s) => s.text === value);
|
|
182
|
+
if (index === -1) {
|
|
183
|
+
index = this.strings.length;
|
|
184
|
+
this.strings.push({ text: value, bytes: [...value].map((ch) => ch.charCodeAt(0) & 0xFF) });
|
|
185
|
+
}
|
|
186
|
+
return { kind: 'string', index, start: node.start, length: node.length };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Is `name` a string here: a `string` parameter, a string const, or a `string<N>` variable? */
|
|
190
|
+
isStringParameter(name) {
|
|
191
|
+
if (this.currentParams.has(name)) return this.currentParams.get(name) === 'string';
|
|
192
|
+
return this.ownStrings.has(name) || this.stringBuffers.has(name);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The lowered expression for a string by name (see isStringParameter). */
|
|
196
|
+
stringRef(node) {
|
|
197
|
+
if (!this.currentParams.has(node.name) && this.ownStrings.has(node.name)) {
|
|
198
|
+
return { kind: 'string', index: this.ownStrings.get(node.name), start: node.start, length: node.length };
|
|
199
|
+
}
|
|
200
|
+
return { kind: 'ref', name: node.name, start: node.start, length: node.length };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* `text.print(cell, \`TICK ${ticks % 10:1} OPTION ${option}\`)`: a template
|
|
205
|
+
* is laid out here, at compile time (templates/index.mjs does the layout;
|
|
206
|
+
* the checker runs the same layout so its diagnostics reach the editor),
|
|
207
|
+
* into the calls a person would have written by hand — one
|
|
208
|
+
* `print(cell + offset, "TICK ")` per run of text and one
|
|
209
|
+
* `printNumber(cell + offset, value, width)` per field, each at the cell
|
|
210
|
+
* the runs before it add up to. Nothing formats at runtime: the
|
|
211
|
+
* generated code is the hand-written version.
|
|
212
|
+
*
|
|
213
|
+
* It is a protocol on the callee's namespace, not a builtin: any
|
|
214
|
+
* namespace exporting `print(cell, s: string)` and
|
|
215
|
+
* `printNumber(cell, value, width)` accepts a template as `print`'s
|
|
216
|
+
* second argument, and the linker resolves those two names the way it
|
|
217
|
+
* resolves any namespace call.
|
|
218
|
+
*/
|
|
219
|
+
templateCall(node) {
|
|
220
|
+
const template = node.args[node.args.length - 1];
|
|
221
|
+
if (!isTemplateCall(node)) {
|
|
222
|
+
const namespace = node.callee.type === NodeType.MemberExpression && node.callee.object.type === NodeType.Identifier
|
|
223
|
+
? node.callee.object.name : null;
|
|
224
|
+
this.diagnostics.push(misplacedTemplate(template, this.file, namespace));
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
const namespace = node.callee.object.name;
|
|
228
|
+
const { pieces, diagnostics } = layoutTemplate(template, this.typeScope, this.file, this.source);
|
|
229
|
+
this.diagnostics.push(...diagnostics);
|
|
230
|
+
if (diagnostics.length) return null;
|
|
231
|
+
|
|
232
|
+
const cell = this.expression(node.args[0]);
|
|
233
|
+
if (!cell) return null;
|
|
234
|
+
// Each call gets its own copy of the cell expression: the linker renames
|
|
235
|
+
// references in place, and one node shared between calls would be
|
|
236
|
+
// renamed twice.
|
|
237
|
+
const cellAt = (offset) => {
|
|
238
|
+
if (cell.kind === 'const') return { kind: 'const', value: cell.value + offset };
|
|
239
|
+
if (offset === 0) return structuredClone(cell);
|
|
240
|
+
return { kind: 'binop', operator: '+', left: structuredClone(cell), right: { kind: 'const', value: offset } };
|
|
241
|
+
};
|
|
242
|
+
const call = (member, args) => ({
|
|
243
|
+
kind: 'namespaceCall', namespace, member, args, start: node.start, length: node.length,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const body = [];
|
|
247
|
+
for (const piece of pieces) {
|
|
248
|
+
if (piece.kind === 'text') {
|
|
249
|
+
const s = this.stringConstant(piece.node, piece.node.value);
|
|
250
|
+
body.push(call('print', [cellAt(piece.offset), s]));
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const value = this.expression(piece.node.expression);
|
|
254
|
+
if (!value) return null;
|
|
255
|
+
body.push(call('printNumber', [cellAt(piece.offset), value, { kind: 'const', value: piece.width }]));
|
|
256
|
+
}
|
|
257
|
+
return body.length === 1 ? body[0] : { kind: 'block', body };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* `namespace screen { function setBorderColor(...) {...} const BLUE = 6; }`.
|
|
263
|
+
*
|
|
264
|
+
* A namespace has no runtime representation at all: a function member
|
|
265
|
+
* lowers to an ordinary IR function under a mangled name (`screen_
|
|
266
|
+
* setBorderColor`), and a const member is recorded as a plain number,
|
|
267
|
+
* never emitted as storage. `screen.setBorderColor(...)` and `Color.Blue`
|
|
268
|
+
* are resolved back to these by the linker, once it knows how `screen` (or
|
|
269
|
+
* an import of it) was actually named in the calling module — lowering
|
|
270
|
+
* itself only needs to record what this module's own namespaces contain.
|
|
271
|
+
*/
|
|
272
|
+
namespaceDeclaration(node) {
|
|
273
|
+
if (!node.name) return; // `namespace` with no name yet; the parser said so
|
|
274
|
+
const name = node.name.name;
|
|
275
|
+
const functions = new Map();
|
|
276
|
+
const consts = new Map();
|
|
277
|
+
|
|
278
|
+
for (const member of node.members) {
|
|
279
|
+
if (!member) continue;
|
|
280
|
+
if (member.type === NodeType.FunctionDeclaration) {
|
|
281
|
+
const memberName = member.name?.name ?? 'anonymous';
|
|
282
|
+
const mangled = `${name}_${memberName}`;
|
|
283
|
+
this.function(member, { mangledName: mangled });
|
|
284
|
+
functions.set(memberName, mangled);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (member.type === NodeType.VariableDeclaration) {
|
|
288
|
+
if (member.kind !== 'const') {
|
|
289
|
+
this.fail(member, 'a namespace member value must be declared with const, not let');
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const typeName = member.typeAnnotation?.name;
|
|
293
|
+
const resolved = typeName && resolveScalarType(typeName);
|
|
294
|
+
if (!resolved || resolved === 'void') {
|
|
295
|
+
this.fail(member, `a namespace const of type ${typeName ?? '(none)'} is not compilable yet`);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
// The same initialisers a module-level const takes: a literal, or a
|
|
299
|
+
// const — this module's own (a value by now), or one only the
|
|
300
|
+
// linker can see (an imported const, `Other.MEMBER`), recorded
|
|
301
|
+
// pending for it to resolve, range-check, and inline. A name this
|
|
302
|
+
// module declares as storage is refused here: it is not a const.
|
|
303
|
+
const init = member.initializer && this.expression(member.initializer);
|
|
304
|
+
if (init?.kind === 'const') {
|
|
305
|
+
consts.set(member.name.name, init.value);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (init?.kind === 'ref' && this.declares(init.name)) {
|
|
309
|
+
this.fail(member.initializer, `'${init.name}' is not a const, so it cannot initialise a namespace const: an initialiser is a literal or a const`);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (init?.kind === 'ref' || init?.kind === 'namespaceConst') {
|
|
313
|
+
consts.set(member.name.name, { pending: init, type: resolved });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
this.fail(member, 'a namespace const is initialised by a literal or a const');
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
this.fail(member, `a ${member.type} is not compilable inside a namespace yet`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
this.namespaces.push({
|
|
323
|
+
name, exported: node.exported ?? false, functions, consts,
|
|
324
|
+
start: node.name.start, length: node.name.length,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// An import lowers to a record, not to code: the linker resolves it against
|
|
329
|
+
// the other modules in the graph. IR with a non-empty `imports` is not a
|
|
330
|
+
// complete program yet, and both backends refuse it rather than dropping it.
|
|
331
|
+
import(node) {
|
|
332
|
+
if (!node.source) {
|
|
333
|
+
// The parser already reported the malformed import; a lowering record
|
|
334
|
+
// without a source module would be meaningless.
|
|
335
|
+
return this.fail(node, 'an import without a module specifier is not compilable');
|
|
336
|
+
}
|
|
337
|
+
this.imports.push({
|
|
338
|
+
source: node.source.value,
|
|
339
|
+
specifiers: (node.specifiers ?? []).map((spec) => ({
|
|
340
|
+
imported: spec.imported ?? spec.name,
|
|
341
|
+
local: spec.name,
|
|
342
|
+
start: spec.start,
|
|
343
|
+
length: spec.length,
|
|
344
|
+
})),
|
|
345
|
+
start: node.start,
|
|
346
|
+
length: node.length,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
global(node) {
|
|
351
|
+
if (!node.name) return null; // the parser reported the missing name
|
|
352
|
+
const annotation = node.typeAnnotation;
|
|
353
|
+
if (!annotation) {
|
|
354
|
+
return this.fail(node, 'a global needs an explicit type to be compilable');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (annotation.name === 'array') return this.arrayGlobal(node);
|
|
358
|
+
if (annotation.name === 'string') return this.stringGlobal(node);
|
|
359
|
+
|
|
360
|
+
// Whatever spelling the programmer used — `u8` or `utinyint` — normalises
|
|
361
|
+
// to the same canonical id every backend keys its codegen tables by.
|
|
362
|
+
// `utinyint` and `u8` are one type from here on, never two.
|
|
363
|
+
let type = annotation.name;
|
|
364
|
+
let isVolatile = false;
|
|
365
|
+
let resolved;
|
|
366
|
+
if (type === 'volatile') {
|
|
367
|
+
const inner = annotation.typeArguments?.[0];
|
|
368
|
+
resolved = inner && resolveIntegerType(inner.name);
|
|
369
|
+
if (!resolved) {
|
|
370
|
+
return this.fail(node, 'volatile<T> needs an integer T to be compilable');
|
|
371
|
+
}
|
|
372
|
+
isVolatile = true;
|
|
373
|
+
} else {
|
|
374
|
+
resolved = resolveIntegerType(type);
|
|
375
|
+
}
|
|
376
|
+
if (!resolved && type !== 'bool') {
|
|
377
|
+
return this.fail(node, `a global of type ${annotation.name} is not compilable yet`);
|
|
378
|
+
}
|
|
379
|
+
type = resolved ? resolved.canonicalName : 'bool';
|
|
380
|
+
|
|
381
|
+
let address = null;
|
|
382
|
+
for (const decorator of node.decorators ?? []) {
|
|
383
|
+
if (decorator.name === 'address') {
|
|
384
|
+
const argument = decorator.args?.[0];
|
|
385
|
+
// A const is a compile-time value, so it names an address as well
|
|
386
|
+
// as a literal does — `@address(Vic.BorderColor)`. An imported one
|
|
387
|
+
// cannot be seen from here; the message says so rather than
|
|
388
|
+
// pretending the decorator only ever takes digits.
|
|
389
|
+
const value = argument && this.constInitialiser(argument);
|
|
390
|
+
if (value === null || value === undefined) {
|
|
391
|
+
return this.fail(
|
|
392
|
+
decorator,
|
|
393
|
+
'@address needs one integer literal, or a const declared in this module',
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
address = value;
|
|
397
|
+
} else {
|
|
398
|
+
return this.fail(decorator, `the @${decorator.name} decorator is not compilable yet`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let init = null;
|
|
403
|
+
if (node.initializer) {
|
|
404
|
+
init = this.expression(node.initializer);
|
|
405
|
+
// A bare name that is not an own const (those became values in
|
|
406
|
+
// expression()) is either an imported const — left for the linker,
|
|
407
|
+
// the only layer that can see the other module — or a name this
|
|
408
|
+
// module declares, which is not a compile-time value and is refused
|
|
409
|
+
// here and now.
|
|
410
|
+
if (init && init.kind === 'ref') {
|
|
411
|
+
if (this.declares(init.name)) {
|
|
412
|
+
return this.fail(
|
|
413
|
+
node.initializer,
|
|
414
|
+
`'${init.name}' is not a const, so it cannot initialise a global: an initialiser is a literal or a const`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
} else if (init && init.kind === 'namespaceConst') {
|
|
418
|
+
// `BorderColor.BLUE`: a const the linker resolves; left pending
|
|
419
|
+
// the same way an imported const is.
|
|
420
|
+
} else if (init && init.kind !== 'const') {
|
|
421
|
+
return this.fail(node.initializer, 'a global initialiser must be a literal or a const to be compilable yet');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (address !== null && init) {
|
|
425
|
+
return this.fail(node, 'an @address global maps hardware and cannot have an initialiser');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// `const` is a compile-time constant — one of the three spellings that
|
|
429
|
+
// say "8bitscript resolves this" (a literal, a `const`, `#name(...)`).
|
|
430
|
+
// It is recorded, never stored: the linker replaces every reference
|
|
431
|
+
// with the value, so no backend ever sees it. `#frames(...)` has
|
|
432
|
+
// already folded, so `const HALF: utinyint = #frames(0.5, seconds)` is
|
|
433
|
+
// a literal by now.
|
|
434
|
+
if (node.kind === 'const') {
|
|
435
|
+
if (isVolatile || address !== null) {
|
|
436
|
+
return this.fail(node, 'a const is a compile-time value; it cannot be volatile or mapped with @address');
|
|
437
|
+
}
|
|
438
|
+
if (!init) {
|
|
439
|
+
return this.fail(node, 'a const needs a literal initialiser: it has no storage to assign later');
|
|
440
|
+
}
|
|
441
|
+
// `const HIGHLIGHT: utinyint = TextColor.YELLOW`, or `= Imported`: a
|
|
442
|
+
// value only the linker can see, so this const is recorded pending
|
|
443
|
+
// and is not in `ownConsts` — a reference to it stays a `ref` for
|
|
444
|
+
// the linker to inline, like a reference to an imported const.
|
|
445
|
+
const pending = init.kind === 'const' ? null : init;
|
|
446
|
+
if (pending) this.pendingConsts.add(node.name.name);
|
|
447
|
+
this.consts.push({
|
|
448
|
+
name: node.name.name, type, value: pending ? null : init.value, ...(pending ? { pending } : {}),
|
|
449
|
+
exported: node.exported ?? false,
|
|
450
|
+
start: node.name.start, length: node.name.length,
|
|
451
|
+
});
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
this.globals.push({
|
|
456
|
+
name: node.name.name, type, volatile: isVolatile, address,
|
|
457
|
+
init: init ? (init.kind === 'const' ? init.value : init) : 0,
|
|
458
|
+
exported: node.exported ?? false,
|
|
459
|
+
// The name's span, so the linker's entry-export rule can point at it.
|
|
460
|
+
start: node.name.start, length: node.name.length,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* `const LABEL: string = "READY";` — a name for constant text: the
|
|
466
|
+
* literal's slot in the string table, inlined wherever the name is read,
|
|
467
|
+
* like a number const is. `let name: string<8>;` — text that changes: N
|
|
468
|
+
* characters of RAM behind a length byte, the same length-prefixed shape
|
|
469
|
+
* a literal has, so it goes wherever a `string` goes. It starts empty,
|
|
470
|
+
* or `= "HI"` (which must fit); `name = "..."` / `name = other` copies at
|
|
471
|
+
* runtime, cut to N; `name.length` and `name[i]` read it.
|
|
472
|
+
*/
|
|
473
|
+
stringGlobal(node) {
|
|
474
|
+
const annotation = node.typeAnnotation;
|
|
475
|
+
const isConst = node.kind === 'const';
|
|
476
|
+
if (node.decorators?.length) {
|
|
477
|
+
return this.fail(node.decorators[0], 'a string is not mapped with @address: it is text in the program or in RAM');
|
|
478
|
+
}
|
|
479
|
+
if (isConst) {
|
|
480
|
+
if (annotation.typeArguments?.length) {
|
|
481
|
+
return this.fail(annotation, 'a const string is written `const NAME: string = "..."`: its length is the literal\'s');
|
|
482
|
+
}
|
|
483
|
+
if (node.initializer?.type !== NodeType.StringLiteral) {
|
|
484
|
+
return this.fail(node.initializer ?? node, 'a const string needs a string literal: const NAME: string = "..."');
|
|
485
|
+
}
|
|
486
|
+
const slot = this.stringConstant(node.initializer, node.initializer.value);
|
|
487
|
+
this.ownStrings.set(node.name.name, slot.index);
|
|
488
|
+
this.consts.push({
|
|
489
|
+
name: node.name.name, type: 'string', string: slot.index,
|
|
490
|
+
exported: node.exported ?? false, start: node.name.start, length: node.name.length,
|
|
491
|
+
});
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
const size = annotation.typeArguments?.[0];
|
|
495
|
+
const capacity = size?.type === NodeType.IntegerLiteral ? size.value
|
|
496
|
+
: size?.type === NodeType.TypeReference && !size.typeArguments?.length ? (this.ownConsts.get(size.name) ?? null)
|
|
497
|
+
: null;
|
|
498
|
+
if (capacity === null || annotation.typeArguments.length !== 1) {
|
|
499
|
+
return this.fail(annotation, 'a string variable needs a capacity: let name: string<N>, N an integer literal or a const declared in this module');
|
|
500
|
+
}
|
|
501
|
+
if (!Number.isInteger(capacity) || capacity < 1 || capacity > MAX_STRING_LENGTH) {
|
|
502
|
+
return this.fail(size, `a string capacity is 1..${MAX_STRING_LENGTH}, not ${capacity}`);
|
|
503
|
+
}
|
|
504
|
+
const init = new Array(capacity + 1).fill(0);
|
|
505
|
+
if (node.initializer) {
|
|
506
|
+
if (node.initializer.type !== NodeType.StringLiteral) {
|
|
507
|
+
return this.fail(node.initializer, 'a string variable starts as a string literal, or empty');
|
|
508
|
+
}
|
|
509
|
+
const bytes = [...node.initializer.value].map((ch) => ch.charCodeAt(0) & 0xFF);
|
|
510
|
+
if (bytes.length > capacity) {
|
|
511
|
+
this.diagnostics.push(diagnostic(
|
|
512
|
+
Codes.STRING_TOO_LONG,
|
|
513
|
+
`"${node.initializer.value}" is ${bytes.length} characters and does not fit in string<${capacity}>`,
|
|
514
|
+
this.file, node.initializer.start, node.initializer.length,
|
|
515
|
+
));
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
init[0] = bytes.length;
|
|
519
|
+
bytes.forEach((b, i) => { init[1 + i] = b; });
|
|
520
|
+
}
|
|
521
|
+
this.stringBuffers.set(node.name.name, capacity);
|
|
522
|
+
this.globals.push({
|
|
523
|
+
name: node.name.name, type: 'utinyint', array: capacity + 1, stringCapacity: capacity,
|
|
524
|
+
constant: false, volatile: false, address: null, init,
|
|
525
|
+
exported: node.exported ?? false, start: node.name.start, length: node.name.length,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* `name = "..."` or `name = other` on a `string<N>`: a runtime copy, cut
|
|
531
|
+
* to N. A literal is checked against N here; another string (a
|
|
532
|
+
* parameter, a const, another variable) is cut when it is longer.
|
|
533
|
+
*/
|
|
534
|
+
stringAssignment(node, capacity) {
|
|
535
|
+
if (node.operator !== '=') {
|
|
536
|
+
return this.fail(node, 'a string is assigned whole (s = ...); there is no string arithmetic');
|
|
537
|
+
}
|
|
538
|
+
if (node.right?.type === NodeType.StringLiteral && node.right.value.length > capacity) {
|
|
539
|
+
this.diagnostics.push(diagnostic(
|
|
540
|
+
Codes.STRING_TOO_LONG,
|
|
541
|
+
`"${node.right.value}" is ${node.right.value.length} characters and does not fit in string<${capacity}>`,
|
|
542
|
+
this.file, node.right.start, node.right.length,
|
|
543
|
+
));
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
const source = this.expression(node.right);
|
|
547
|
+
if (!source) return null;
|
|
548
|
+
if (!this.isStringValue(source)) {
|
|
549
|
+
return this.fail(node.right, `'${node.left.name}' is a string<${capacity}>: it is assigned a string — a literal, a const, a parameter, or another string variable`);
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
kind: 'stringCopy', target: { kind: 'ref', name: node.left.name, start: node.left.start, length: node.left.length },
|
|
553
|
+
source, capacity, start: node.left.start, length: node.left.length,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Is this lowered expression a string, as far as this module can tell? */
|
|
558
|
+
isStringValue(expr) {
|
|
559
|
+
if (expr.kind === 'string') return true;
|
|
560
|
+
if (expr.kind !== 'ref') return false;
|
|
561
|
+
// A parameter, a string variable, or — unknown here — an import the
|
|
562
|
+
// linker will check.
|
|
563
|
+
return this.currentParams.get(expr.name) === 'string'
|
|
564
|
+
|| this.stringBuffers.has(expr.name)
|
|
565
|
+
|| !(this.currentParams.has(expr.name) || this.globalTypes.has(expr.name)
|
|
566
|
+
|| this.functionTypes.has(expr.name) || this.arrays.has(expr.name) || this.ownConsts.has(expr.name));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* `let hp: array<utinyint, 4>;` — N elements of T in RAM, zero unless
|
|
571
|
+
* written `= [..]`; `const TABLE: array<utinyint, 3> = [1, 2, 3];` — N
|
|
572
|
+
* elements of T as data in the program, never in RAM; `@address(0x0400)
|
|
573
|
+
* let screenRam: array<utinyint, 1000>;` — N cells of hardware, a name
|
|
574
|
+
* for a fixed location as a scalar `@address` is. The length is part of
|
|
575
|
+
* the type and every element is a compile-time value, so the whole
|
|
576
|
+
* layout is decided here: `a.length` is a number, an initialiser has
|
|
577
|
+
* exactly N elements, and a literal index past the end is a diagnostic.
|
|
578
|
+
*
|
|
579
|
+
* Unlike a scalar `const`, a const array is not inlined — it has an
|
|
580
|
+
* address, because `TABLE[i]` with a runtime `i` needs one — so it is a
|
|
581
|
+
* global with `constant: true`, and each backend places it read-only.
|
|
582
|
+
*/
|
|
583
|
+
arrayGlobal(node) {
|
|
584
|
+
const resolved = resolveArrayType(node.typeAnnotation, this.ownConsts);
|
|
585
|
+
if (resolved.error) return this.fail(node.typeAnnotation, resolved.error);
|
|
586
|
+
const { type, length } = resolved;
|
|
587
|
+
const constant = node.kind === 'const';
|
|
588
|
+
|
|
589
|
+
let address = null;
|
|
590
|
+
for (const decorator of node.decorators ?? []) {
|
|
591
|
+
if (decorator.name !== 'address') {
|
|
592
|
+
return this.fail(decorator, `the @${decorator.name} decorator is not compilable yet`);
|
|
593
|
+
}
|
|
594
|
+
const value = decorator.args?.[0] && this.constInitialiser(decorator.args[0]);
|
|
595
|
+
if (value === null || value === undefined) {
|
|
596
|
+
return this.fail(decorator, '@address needs one integer literal, or a const declared in this module');
|
|
597
|
+
}
|
|
598
|
+
address = value;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
let init = null;
|
|
602
|
+
if (node.initializer) {
|
|
603
|
+
if (address !== null) {
|
|
604
|
+
return this.fail(node, 'an @address array maps hardware and cannot have an initialiser');
|
|
605
|
+
}
|
|
606
|
+
if (node.initializer.type !== NodeType.ArrayLiteral) {
|
|
607
|
+
return this.fail(node.initializer, 'an array initialiser is written [v, v, ...]: one compile-time value per element');
|
|
608
|
+
}
|
|
609
|
+
const { elements } = node.initializer;
|
|
610
|
+
if (elements.length !== length) {
|
|
611
|
+
this.diagnostics.push(diagnostic(
|
|
612
|
+
Codes.ARRAY_SIZE_MISMATCH,
|
|
613
|
+
`'${node.name.name}' is an array<${type}, ${length}>, so its initialiser needs ${length} element${length === 1 ? '' : 's'}, not ${elements.length}`,
|
|
614
|
+
this.file, node.initializer.start, node.initializer.length,
|
|
615
|
+
));
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
init = [];
|
|
619
|
+
const { min, max } = type === 'bool' ? { min: 0, max: 1 } : resolveIntegerType(type);
|
|
620
|
+
for (const element of elements) {
|
|
621
|
+
const value = this.expression(element);
|
|
622
|
+
if (!value) return null;
|
|
623
|
+
// `BorderColor.BLUE`, or an imported const: a compile-time value
|
|
624
|
+
// only the linker can see. Left as is for it to fill in.
|
|
625
|
+
if (value.kind === 'namespaceConst' || (value.kind === 'ref' && !this.declares(value.name))) {
|
|
626
|
+
init.push(value);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (value.kind !== 'const') {
|
|
630
|
+
return this.fail(element, 'an array element is a literal or a const: the data is laid out at compile time');
|
|
631
|
+
}
|
|
632
|
+
if (value.value < min || value.value > max) {
|
|
633
|
+
this.diagnostics.push(diagnostic(
|
|
634
|
+
Codes.VALUE_OUT_OF_RANGE, `${value.value} does not fit in ${type} (${min}..${max})`,
|
|
635
|
+
this.file, element.start, element.length,
|
|
636
|
+
));
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
init.push(value.value);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (constant && address !== null) {
|
|
643
|
+
return this.fail(node, 'a const array is data in the program; it cannot be mapped with @address');
|
|
644
|
+
}
|
|
645
|
+
if (constant && !init) {
|
|
646
|
+
return this.fail(node, 'a const array needs its values: const NAME: array<T, N> = [...]');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
this.globals.push({
|
|
650
|
+
name: node.name.name, type, array: length, constant, volatile: false, address, init,
|
|
651
|
+
exported: node.exported ?? false,
|
|
652
|
+
start: node.name.start, length: node.name.length,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* What an `a` in `a[i]` / `a.length` / `a[i] = v` names, as far as this
|
|
658
|
+
* module can tell: its own array (with everything known), a name it
|
|
659
|
+
* declares that is not an array (refused), or a name it does not declare
|
|
660
|
+
* — an import, left to the linker as a `ref` with `elementType: null`.
|
|
661
|
+
*/
|
|
662
|
+
arrayReference(object, what) {
|
|
663
|
+
if (object?.type !== NodeType.Identifier) {
|
|
664
|
+
return this.fail(object ?? { start: 0, length: 0 }, `${what} is only compilable on an array by name`);
|
|
665
|
+
}
|
|
666
|
+
const name = object.name;
|
|
667
|
+
const ref = { kind: 'ref', name, start: object.start, length: object.length };
|
|
668
|
+
// An array parameter is the array it was handed, with its element type
|
|
669
|
+
// and length known from its own type — so `t[i]` range-checks against
|
|
670
|
+
// the declared length and `t.length` folds, exactly as for a global.
|
|
671
|
+
if (this.currentArrayParams?.has(name)) {
|
|
672
|
+
const p = this.currentArrayParams.get(name);
|
|
673
|
+
return { ref, array: { type: p.elementType, length: p.length } };
|
|
674
|
+
}
|
|
675
|
+
if (this.currentParams.has(name)) {
|
|
676
|
+
return this.fail(object, `'${name}' is a parameter or local, not an array: ${what} needs an array`);
|
|
677
|
+
}
|
|
678
|
+
if (this.arrays.has(name)) return { ref, array: this.arrays.get(name) };
|
|
679
|
+
if (this.declares(name) || this.ownConsts.has(name) || this.pendingConsts.has(name)) {
|
|
680
|
+
return this.fail(object, `'${name}' is not an array: ${what} needs an array<T, N>`);
|
|
681
|
+
}
|
|
682
|
+
return { ref, array: null };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* A parameter's default, lowered: a number, or a pending expression the
|
|
687
|
+
* linker resolves (`BorderColor.BLACK`, an imported const). Null, with
|
|
688
|
+
* a diagnostic, for anything that is not a compile-time value.
|
|
689
|
+
*/
|
|
690
|
+
parameterDefault(p, type) {
|
|
691
|
+
if (type === 'string') {
|
|
692
|
+
const value = this.expression(p.defaultValue);
|
|
693
|
+
if (!value) return null;
|
|
694
|
+
if (value.kind !== 'string') return this.fail(p.defaultValue, 'a string parameter\'s default is a string literal or a string const');
|
|
695
|
+
return value;
|
|
696
|
+
}
|
|
697
|
+
const value = this.expression(p.defaultValue);
|
|
698
|
+
if (!value) return null;
|
|
699
|
+
if (value.kind === 'namespaceConst' || (value.kind === 'ref' && !this.declares(value.name) && !this.currentParams.has(value.name))) {
|
|
700
|
+
return value;
|
|
701
|
+
}
|
|
702
|
+
if (value.kind !== 'const') {
|
|
703
|
+
return this.fail(p.defaultValue, 'a parameter default is a literal or a const: it is filled in at compile time');
|
|
704
|
+
}
|
|
705
|
+
const { min, max } = type === 'bool' ? { min: 0, max: 1 } : resolveIntegerType(type);
|
|
706
|
+
if (value.value < min || value.value > max) {
|
|
707
|
+
this.diagnostics.push(diagnostic(
|
|
708
|
+
Codes.VALUE_OUT_OF_RANGE, `${value.value} does not fit in ${type} (${min}..${max})`,
|
|
709
|
+
this.file, p.defaultValue.start, p.defaultValue.length,
|
|
710
|
+
));
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
return value;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* The argument count of a call to one of this module's own functions,
|
|
718
|
+
* against its parameters: too many, or fewer than those without a
|
|
719
|
+
* default, is a diagnostic here so the editor sees it. Missing arguments
|
|
720
|
+
* are filled in by the linker, which resolves every default.
|
|
721
|
+
*/
|
|
722
|
+
checkArity(node, name, count) {
|
|
723
|
+
const arity = this.functionArity.get(name);
|
|
724
|
+
if (!arity || this.currentParams.has(name)) return true;
|
|
725
|
+
if (count > arity.max || count < arity.min) {
|
|
726
|
+
const takes = arity.min === arity.max ? `${arity.max}` : `${arity.min} to ${arity.max}`;
|
|
727
|
+
this.diagnostics.push(diagnostic(
|
|
728
|
+
Codes.WRONG_ARGUMENT_COUNT,
|
|
729
|
+
`'${name}' takes ${takes} argument${arity.max === 1 ? '' : 's'}, not ${count}`,
|
|
730
|
+
this.file, node.start, node.length,
|
|
731
|
+
));
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** Does this module declare `name` as something other than a const — a global, function, array, or string? */
|
|
738
|
+
declares(name) {
|
|
739
|
+
if (this.constNames.has(name)) return false;
|
|
740
|
+
return this.globalTypes.has(name) || this.functionTypes.has(name) || this.arrays.has(name) || this.stringBuffers.has(name);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** Index range check for an own array with a literal index; true when it passed. */
|
|
744
|
+
indexInRange(indexNode, index, array) {
|
|
745
|
+
if (!array || index.kind !== 'const') return true;
|
|
746
|
+
if (index.value < 0 || index.value >= array.length) {
|
|
747
|
+
this.diagnostics.push(diagnostic(
|
|
748
|
+
Codes.INDEX_OUT_OF_RANGE,
|
|
749
|
+
`index ${index.value} is outside an array<${array.type}, ${array.length}>: elements are 0..${array.length - 1}`,
|
|
750
|
+
this.file, indexNode.start, indexNode.length,
|
|
751
|
+
));
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** `a[i]` as an IR read. */
|
|
758
|
+
indexRead(node) {
|
|
759
|
+
const target = this.arrayReference(node.object, 'indexing');
|
|
760
|
+
if (!target) return null;
|
|
761
|
+
const index = this.expression(node.index);
|
|
762
|
+
if (!index || !this.indexInRange(node.index, index, target.array)) return null;
|
|
763
|
+
return { kind: 'index', array: target.ref, index, elementType: target.array?.type ?? null };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function(node, { mangledName } = {}) {
|
|
767
|
+
const params = [];
|
|
768
|
+
for (const p of node.params) {
|
|
769
|
+
const typeName = p.typeAnnotation?.name;
|
|
770
|
+
// `t: array<utinyint, 4>` — the array itself, passed by reference.
|
|
771
|
+
// The length is part of the type, so `t.length` is a constant inside
|
|
772
|
+
// the callee and costs nothing at run time; the caller passes the
|
|
773
|
+
// array's address and nothing is copied. Read-only for now: an
|
|
774
|
+
// element is read (`t[i]`), never assigned through.
|
|
775
|
+
if (typeName === 'array') {
|
|
776
|
+
const resolved = resolveArrayType(p.typeAnnotation, this.ownConsts);
|
|
777
|
+
if (resolved.error) return this.fail(p.typeAnnotation, resolved.error);
|
|
778
|
+
if (p.defaultValue) {
|
|
779
|
+
return this.fail(p, 'an array parameter has no default: an array is passed, never filled in');
|
|
780
|
+
}
|
|
781
|
+
if (params.some((q) => q.default !== undefined)) {
|
|
782
|
+
return this.fail(p, `'${p.name.name}' needs a default: every parameter after one with a default has one`);
|
|
783
|
+
}
|
|
784
|
+
params.push({
|
|
785
|
+
name: p.name.name, type: 'array', elementType: resolved.type, length: resolved.length,
|
|
786
|
+
});
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const type = typeName && resolveScalarType(typeName, { allowString: true });
|
|
790
|
+
if (!type || type === 'void') {
|
|
791
|
+
return this.fail(p, `a parameter of type ${typeName ?? '(none)'} is not compilable yet`);
|
|
792
|
+
}
|
|
793
|
+
const param = { name: p.name.name, type };
|
|
794
|
+
// A default is a compile-time value — a literal, a const, or a name
|
|
795
|
+
// only the linker can see — filled in at each call that leaves the
|
|
796
|
+
// argument off. Once one parameter has a default, the rest must too,
|
|
797
|
+
// since arguments are matched by position.
|
|
798
|
+
if (p.defaultValue) {
|
|
799
|
+
const value = this.parameterDefault(p, type);
|
|
800
|
+
if (value === null) return null;
|
|
801
|
+
param.default = value;
|
|
802
|
+
} else if (params.some((q) => q.default !== undefined)) {
|
|
803
|
+
return this.fail(p, `'${p.name.name}' needs a default: every parameter after one with a default has one`);
|
|
804
|
+
}
|
|
805
|
+
params.push(param);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const returnTypeName = node.returnType?.name ?? 'void';
|
|
809
|
+
const returnType = resolveScalarType(returnTypeName, { allowVoid: true });
|
|
810
|
+
if (!returnType) {
|
|
811
|
+
return this.fail(node, `a return type of ${returnTypeName} is not compilable yet`);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Threaded through statement lowering so a `return` deep inside an
|
|
815
|
+
// `if`/`while` can be checked against the function it actually belongs
|
|
816
|
+
// to, without passing the type down every recursive call by hand.
|
|
817
|
+
const outerReturnType = this.currentReturnType;
|
|
818
|
+
const outerParams = this.currentParams;
|
|
819
|
+
const outerArrayParams = this.currentArrayParams;
|
|
820
|
+
this.currentReturnType = returnType;
|
|
821
|
+
this.currentParams = parameterTypes(node);
|
|
822
|
+
// Array parameters are kept apart from the scalar ones: `parameterTypes`
|
|
823
|
+
// feeds type inference, which reasons about integers, and an array is
|
|
824
|
+
// not one. What the body needs from them is the element type and the
|
|
825
|
+
// length, so `t[i]` range-checks and `t.length` folds.
|
|
826
|
+
this.currentArrayParams = new Map(
|
|
827
|
+
params.filter((q) => q.type === 'array').map((q) => [q.name, q]),
|
|
828
|
+
);
|
|
829
|
+
// A parameter is declared in the body's block: `let x` over a
|
|
830
|
+
// parameter x is a redeclaration, as it is in C and AssemblyScript.
|
|
831
|
+
const outerBlock = this.blockNames;
|
|
832
|
+
this.blockNames = new Set([...this.currentParams.keys(), ...this.currentArrayParams.keys()]);
|
|
833
|
+
const body = this.functionBody(node.body);
|
|
834
|
+
this.blockNames = outerBlock;
|
|
835
|
+
this.currentReturnType = outerReturnType;
|
|
836
|
+
this.currentParams = outerParams;
|
|
837
|
+
this.currentArrayParams = outerArrayParams;
|
|
838
|
+
|
|
839
|
+
this.functions.push({
|
|
840
|
+
name: mangledName ?? (node.name?.name ?? 'anonymous'),
|
|
841
|
+
// A namespace member is only ever reached through the namespace
|
|
842
|
+
// (`screen.setBorderColor`, never `import { screen_setBorderColor }`);
|
|
843
|
+
// its mangled name is never itself an exportable top-level binding.
|
|
844
|
+
exported: mangledName ? false : node.exported,
|
|
845
|
+
params,
|
|
846
|
+
returnType,
|
|
847
|
+
body,
|
|
848
|
+
// The name's span, so the linker's entry-export rule can point at it.
|
|
849
|
+
start: node.name?.start ?? node.start,
|
|
850
|
+
length: node.name?.length ?? node.length,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/** A function's body: its own scope, but the block the parameters are declared in. */
|
|
855
|
+
functionBody(node) {
|
|
856
|
+
const outer = this.currentParams;
|
|
857
|
+
this.currentParams = new Map(outer);
|
|
858
|
+
try {
|
|
859
|
+
const out = [];
|
|
860
|
+
for (const statement of node?.body ?? []) {
|
|
861
|
+
const lowered = this.statement(statement);
|
|
862
|
+
if (lowered) out.push(lowered);
|
|
863
|
+
}
|
|
864
|
+
return out;
|
|
865
|
+
} finally {
|
|
866
|
+
this.currentParams = outer;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
block(node) {
|
|
871
|
+
return this.scoped(() => {
|
|
872
|
+
const out = [];
|
|
873
|
+
for (const statement of node?.body ?? []) {
|
|
874
|
+
const lowered = this.statement(statement);
|
|
875
|
+
if (lowered) out.push(lowered);
|
|
876
|
+
}
|
|
877
|
+
return out;
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Run `fn` with a copy of the current names-in-scope, so a local declared
|
|
883
|
+
* inside a block is not visible after it — block scoping, the same rule
|
|
884
|
+
* C and AssemblyScript apply to what the backends emit.
|
|
885
|
+
*/
|
|
886
|
+
scoped(fn) {
|
|
887
|
+
const outer = this.currentParams;
|
|
888
|
+
const outerBlock = this.blockNames;
|
|
889
|
+
this.currentParams = new Map(outer);
|
|
890
|
+
this.blockNames = new Set();
|
|
891
|
+
try {
|
|
892
|
+
return fn();
|
|
893
|
+
} finally {
|
|
894
|
+
this.currentParams = outer;
|
|
895
|
+
this.blockNames = outerBlock;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* `let i: utinyint = 0;` inside a function: a local, storage that exists
|
|
901
|
+
* while the function runs — the target's own stack or registers, as its
|
|
902
|
+
* compiler sees fit. An initialiser is an expression (it runs); left off,
|
|
903
|
+
* the local starts at 0 like a global does. A `const` here is refused:
|
|
904
|
+
* a const is a compile-time value and lives at the top level.
|
|
905
|
+
*/
|
|
906
|
+
local(node) {
|
|
907
|
+
if (!node.name) return null; // the parser reported the missing name
|
|
908
|
+
if (node.kind === 'const') {
|
|
909
|
+
return this.fail(node, 'a const is a compile-time value declared at the top level of a module, not inside a function');
|
|
910
|
+
}
|
|
911
|
+
const annotation = node.typeAnnotation;
|
|
912
|
+
if (!annotation) return this.fail(node, 'a local needs an explicit type to be compilable');
|
|
913
|
+
if (annotation.name === 'array') {
|
|
914
|
+
return this.fail(annotation, 'a local array is not compilable yet: declare the array at the top level');
|
|
915
|
+
}
|
|
916
|
+
const type = resolveScalarType(annotation.name);
|
|
917
|
+
if (!type || annotation.typeArguments?.length) {
|
|
918
|
+
return this.fail(annotation, `a local of type ${annotation.name} is not compilable yet: an integer or bool`);
|
|
919
|
+
}
|
|
920
|
+
for (const decorator of node.decorators ?? []) {
|
|
921
|
+
return this.fail(decorator, `@${decorator.name} maps hardware; it belongs on a top-level declaration`);
|
|
922
|
+
}
|
|
923
|
+
// One declaration per name per block — the rule C and AssemblyScript
|
|
924
|
+
// both enforce on what the backends emit, reported here instead.
|
|
925
|
+
if (this.blockNames?.has(node.name.name)) {
|
|
926
|
+
return this.fail(node.name, `'${node.name.name}' is already declared in this block`);
|
|
927
|
+
}
|
|
928
|
+
const init = node.initializer ? this.expression(node.initializer) : { kind: 'const', value: 0 };
|
|
929
|
+
if (!init) return null;
|
|
930
|
+
if (init.kind === 'string') {
|
|
931
|
+
return this.fail(node.initializer, `a string cannot initialise a ${type}: a string lives in a string<N> or a const`);
|
|
932
|
+
}
|
|
933
|
+
// In scope from here on: shadows a global, const, or array of the name.
|
|
934
|
+
this.currentParams.set(node.name.name, type);
|
|
935
|
+
this.blockNames?.add(node.name.name);
|
|
936
|
+
return { kind: 'local', name: node.name.name, type, init, start: node.name.start, length: node.name.length };
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* `for (let i: u8 = 0; i < 4; i++) { ... }`. Emitted as the target's own
|
|
941
|
+
* `for` — not unrolled into a `while` — so `continue` still runs the
|
|
942
|
+
* update, as it does everywhere else the syntax is used. The initialiser
|
|
943
|
+
* is a local declaration or a statement; the update a statement; any of
|
|
944
|
+
* the three may be left off.
|
|
945
|
+
*/
|
|
946
|
+
forStatement(node) {
|
|
947
|
+
return this.scoped(() => {
|
|
948
|
+
let init = null;
|
|
949
|
+
if (node.init) {
|
|
950
|
+
init = node.init.type === NodeType.VariableDeclaration
|
|
951
|
+
? this.local(node.init)
|
|
952
|
+
: this.expressionAsStatement(node.init, 'a for initialiser');
|
|
953
|
+
if (!init) return null;
|
|
954
|
+
}
|
|
955
|
+
const test = node.test ? this.expression(node.test) : null;
|
|
956
|
+
if (node.test && !test) return null;
|
|
957
|
+
let update = null;
|
|
958
|
+
if (node.update) {
|
|
959
|
+
update = this.expressionAsStatement(node.update, 'a for update');
|
|
960
|
+
if (!update) return null;
|
|
961
|
+
}
|
|
962
|
+
const body = this.blockOrStatement(node.body);
|
|
963
|
+
return { kind: 'for', init, test, update, body };
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/** An expression in statement position: an assignment, `++`/`--`, or a call. */
|
|
968
|
+
expressionAsStatement(e, what) {
|
|
969
|
+
if (!e?.type) return null;
|
|
970
|
+
if (e.type === NodeType.AssignmentExpression) return this.assignment(e);
|
|
971
|
+
if (e.type === NodeType.UpdateExpression) return this.update(e);
|
|
972
|
+
if (e.type === NodeType.CallExpression) return this.callExpression(e);
|
|
973
|
+
return this.fail(e, `${what} is an assignment, ++/--, or a call, not a ${e.type}`);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
statement(node) {
|
|
977
|
+
// A hole the parser left behind: it has already reported the syntax
|
|
978
|
+
// error, and there is nothing here to lower or to say twice.
|
|
979
|
+
if (!node?.type) return null;
|
|
980
|
+
switch (node.type) {
|
|
981
|
+
case NodeType.ExpressionStatement: {
|
|
982
|
+
const e = node.expression;
|
|
983
|
+
if (e.type === NodeType.AssignmentExpression) return this.assignment(e);
|
|
984
|
+
if (e.type === NodeType.UpdateExpression) return this.update(e);
|
|
985
|
+
if (e.type === NodeType.CallExpression) {
|
|
986
|
+
// A template string as the last argument is laid out into calls
|
|
987
|
+
// here — a statement, since it expands to several.
|
|
988
|
+
if (e.args[e.args.length - 1]?.type === NodeType.TemplateLiteral) return this.templateCall(e);
|
|
989
|
+
return this.callExpression(e);
|
|
990
|
+
}
|
|
991
|
+
return this.fail(node, `a bare ${e.type} statement is not compilable yet`);
|
|
992
|
+
}
|
|
993
|
+
case NodeType.VariableDeclaration:
|
|
994
|
+
return this.local(node);
|
|
995
|
+
case NodeType.ForStatement:
|
|
996
|
+
return this.forStatement(node);
|
|
997
|
+
case NodeType.IfStatement: {
|
|
998
|
+
const test = this.expression(node.test);
|
|
999
|
+
const then = node.consequent ? this.blockOrStatement(node.consequent) : [];
|
|
1000
|
+
const otherwise = node.alternate ? this.blockOrStatement(node.alternate) : null;
|
|
1001
|
+
return test ? { kind: 'if', test, then, else: otherwise } : null;
|
|
1002
|
+
}
|
|
1003
|
+
case NodeType.WhileStatement: {
|
|
1004
|
+
const test = this.expression(node.test);
|
|
1005
|
+
const body = this.blockOrStatement(node.body);
|
|
1006
|
+
return test ? { kind: 'while', test, body } : null;
|
|
1007
|
+
}
|
|
1008
|
+
case NodeType.ReturnStatement: {
|
|
1009
|
+
if (node.argument) {
|
|
1010
|
+
if (this.currentReturnType === 'void') {
|
|
1011
|
+
return this.fail(node, 'a function declared to return void cannot return a value');
|
|
1012
|
+
}
|
|
1013
|
+
const value = this.expression(node.argument);
|
|
1014
|
+
return value ? { kind: 'return', value } : null;
|
|
1015
|
+
}
|
|
1016
|
+
if (this.currentReturnType && this.currentReturnType !== 'void') {
|
|
1017
|
+
return this.fail(node, `this function must return a value of type ${this.currentReturnType}`);
|
|
1018
|
+
}
|
|
1019
|
+
return { kind: 'return', value: null };
|
|
1020
|
+
}
|
|
1021
|
+
case NodeType.BreakStatement:
|
|
1022
|
+
return { kind: 'break' };
|
|
1023
|
+
case NodeType.ContinueStatement:
|
|
1024
|
+
return { kind: 'continue' };
|
|
1025
|
+
case NodeType.AsmBlock:
|
|
1026
|
+
return { kind: 'asm', text: node.body.slice(1, -1) };
|
|
1027
|
+
case NodeType.BlockStatement:
|
|
1028
|
+
return { kind: 'block', body: this.block(node) };
|
|
1029
|
+
default:
|
|
1030
|
+
return this.fail(node, `a ${node.type} statement is not compilable yet`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// A call can be a statement (its result, if any, discarded) or, now that
|
|
1035
|
+
// functions may return a value, a subexpression — `expression()` below
|
|
1036
|
+
// routes CallExpression here too.
|
|
1037
|
+
//
|
|
1038
|
+
// `memory.read`/`memory.write` are a compiler-owned intrinsic, not a
|
|
1039
|
+
// namespace a module declares: raw memory access has to exist before any
|
|
1040
|
+
// library can be written in terms of it. Every other `object.member(...)`
|
|
1041
|
+
// callee is a namespace-qualified call — `screen.setBorderColor(...)` —
|
|
1042
|
+
// and lowering cannot know yet whether `screen` names a real namespace,
|
|
1043
|
+
// still less which module it came from: that needs the import graph, which
|
|
1044
|
+
// only the linker has. So this only records *what* was asked for; the
|
|
1045
|
+
// linker turns a resolved one into a plain `call` and reports an
|
|
1046
|
+
// unresolved one, exactly as it already does for a bare name.
|
|
1047
|
+
callExpression(node) {
|
|
1048
|
+
const callee = node.callee;
|
|
1049
|
+
if (!callee?.type) return null; // half-typed; the parser reported it
|
|
1050
|
+
if (callee.type === NodeType.MemberExpression && callee.object.type === NodeType.Identifier) {
|
|
1051
|
+
if (callee.object.name === 'memory') {
|
|
1052
|
+
return this.memoryIntrinsic(node, callee);
|
|
1053
|
+
}
|
|
1054
|
+
const args = [];
|
|
1055
|
+
for (const argument of node.args) {
|
|
1056
|
+
const lowered = this.expression(argument);
|
|
1057
|
+
if (!lowered) return null;
|
|
1058
|
+
args.push(lowered);
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
kind: 'namespaceCall',
|
|
1062
|
+
namespace: callee.object.name,
|
|
1063
|
+
member: callee.property.name,
|
|
1064
|
+
args,
|
|
1065
|
+
start: node.start,
|
|
1066
|
+
length: node.length,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
if (callee.type !== NodeType.Identifier) {
|
|
1070
|
+
return this.fail(node, 'a call through member access is not compilable yet');
|
|
1071
|
+
}
|
|
1072
|
+
// `waitFrame()` — block until the next logical frame. A builtin with its
|
|
1073
|
+
// own IR kind rather than a call to a function that exists somewhere:
|
|
1074
|
+
// every backend emits it differently (the 6502 backend as its frame-sync
|
|
1075
|
+
// runtime, the web backend as a host import), and it takes no arguments.
|
|
1076
|
+
// The name is reserved (checker/index.mjs's RESERVED_BUILTIN_NAMES), so
|
|
1077
|
+
// nothing a user declares can be what this refers to.
|
|
1078
|
+
if (callee.name === 'waitFrame') {
|
|
1079
|
+
if (node.args.length !== 0) {
|
|
1080
|
+
return this.fail(node, 'waitFrame() takes no arguments');
|
|
1081
|
+
}
|
|
1082
|
+
return { kind: 'waitFrame', start: node.start, length: node.length };
|
|
1083
|
+
}
|
|
1084
|
+
if (!this.checkArity(node, callee.name, node.args.length)) return null;
|
|
1085
|
+
const args = [];
|
|
1086
|
+
for (const argument of node.args) {
|
|
1087
|
+
// An array is handed over by name — `pick(STARTS, i)` — and this is
|
|
1088
|
+
// the one place its bare name is a value: the address of its first
|
|
1089
|
+
// element, nothing copied. Everywhere else a bare array name is an
|
|
1090
|
+
// error, because an array is otherwise used one element at a time.
|
|
1091
|
+
if (argument.type === NodeType.Identifier
|
|
1092
|
+
&& !this.currentParams.has(argument.name)
|
|
1093
|
+
&& (this.arrays.has(argument.name) || this.currentArrayParams.has(argument.name))) {
|
|
1094
|
+
args.push({ kind: 'ref', name: argument.name, start: argument.start, length: argument.length });
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
const lowered = this.expression(argument);
|
|
1098
|
+
if (!lowered) return null;
|
|
1099
|
+
args.push(lowered);
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
kind: 'call',
|
|
1103
|
+
name: callee.name,
|
|
1104
|
+
args,
|
|
1105
|
+
start: callee.start,
|
|
1106
|
+
length: callee.length,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
memoryIntrinsic(node, callee) {
|
|
1111
|
+
const member = callee.property.name;
|
|
1112
|
+
if (member === 'write') {
|
|
1113
|
+
if (node.args.length !== 2) {
|
|
1114
|
+
return this.fail(node, 'memory.write needs exactly two arguments: (address, value)');
|
|
1115
|
+
}
|
|
1116
|
+
const address = this.memoryArgument(node.args[0], 'usmallint');
|
|
1117
|
+
const value = this.memoryArgument(node.args[1], 'utinyint');
|
|
1118
|
+
if (!address || !value) return null;
|
|
1119
|
+
return { kind: 'memoryWrite', address, value, start: node.start, length: node.length };
|
|
1120
|
+
}
|
|
1121
|
+
if (member === 'read') {
|
|
1122
|
+
if (node.args.length !== 1) {
|
|
1123
|
+
return this.fail(node, 'memory.read needs exactly one argument: (address)');
|
|
1124
|
+
}
|
|
1125
|
+
const address = this.memoryArgument(node.args[0], 'usmallint');
|
|
1126
|
+
if (!address) return null;
|
|
1127
|
+
return { kind: 'memoryRead', address, start: node.start, length: node.length };
|
|
1128
|
+
}
|
|
1129
|
+
return this.fail(node, `memory.${member} is not compilable yet: only read and write exist`);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Lower one `memory.read`/`memory.write` argument, range-checking it
|
|
1134
|
+
* against `typeName` when it is a literal — the same rule `let x: T = n`
|
|
1135
|
+
* gets, extended to the one built-in call whose parameter types the
|
|
1136
|
+
* compiler knows without a binder.
|
|
1137
|
+
*/
|
|
1138
|
+
memoryArgument(node, typeName) {
|
|
1139
|
+
const value = this.expression(node);
|
|
1140
|
+
if (!value) return null;
|
|
1141
|
+
if (value.kind === 'const') {
|
|
1142
|
+
const { min, max } = resolveIntegerType(typeName);
|
|
1143
|
+
if (value.value < min || value.value > max) {
|
|
1144
|
+
this.diagnostics.push(diagnostic(
|
|
1145
|
+
Codes.VALUE_OUT_OF_RANGE,
|
|
1146
|
+
`${value.value} does not fit in ${typeName} (${min}..${max})`,
|
|
1147
|
+
this.file, node.start, node.length,
|
|
1148
|
+
));
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return value;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
blockOrStatement(node) {
|
|
1156
|
+
if (!node?.type) return []; // `while (x)` with no body typed yet
|
|
1157
|
+
if (node.type === NodeType.BlockStatement) return this.block(node);
|
|
1158
|
+
return this.scoped(() => {
|
|
1159
|
+
const lowered = this.statement(node);
|
|
1160
|
+
return lowered ? [lowered] : [];
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
assignment(node) {
|
|
1165
|
+
if (!node.left?.type) return null; // half-typed; the parser reported it
|
|
1166
|
+
if (node.left.type === NodeType.IndexExpression) {
|
|
1167
|
+
return this.indexStore(node.left, node.operator.slice(0, -1) || null, node.right);
|
|
1168
|
+
}
|
|
1169
|
+
if (node.left.type !== NodeType.Identifier) {
|
|
1170
|
+
return this.fail(node.left, `assigning to a ${node.left.type} is not compilable yet`);
|
|
1171
|
+
}
|
|
1172
|
+
if (this.arrays.has(node.left.name) && !this.currentParams.has(node.left.name)) {
|
|
1173
|
+
return this.fail(node.left, `'${node.left.name}' is an array: it is written one element at a time, ${node.left.name}[i] = ...`);
|
|
1174
|
+
}
|
|
1175
|
+
if (!this.currentParams.has(node.left.name) && this.stringBuffers.has(node.left.name)) {
|
|
1176
|
+
return this.stringAssignment(node, this.stringBuffers.get(node.left.name));
|
|
1177
|
+
}
|
|
1178
|
+
if (!this.currentParams.has(node.left.name) && this.ownStrings.has(node.left.name)) {
|
|
1179
|
+
this.diagnostics.push(diagnostic(
|
|
1180
|
+
Codes.ASSIGN_TO_CONST,
|
|
1181
|
+
`'${node.left.name}' is a const — a compile-time value with no storage — and cannot be assigned`,
|
|
1182
|
+
this.file, node.left.start, node.left.length,
|
|
1183
|
+
));
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
let value = this.expression(node.right);
|
|
1187
|
+
if (!value) return null;
|
|
1188
|
+
// A string into something this module knows is a number (a local, a
|
|
1189
|
+
// parameter, an own global); an imported target is the linker's to judge.
|
|
1190
|
+
if (value.kind === 'string' && (this.currentParams.has(node.left.name) || this.declares(node.left.name))) {
|
|
1191
|
+
return this.fail(node.right, `'${node.left.name}' is not a string: a string is assigned to a string<N>`);
|
|
1192
|
+
}
|
|
1193
|
+
if (node.operator !== '=') {
|
|
1194
|
+
// `x += e` is `x = x + e`; the operator minus its trailing `=`.
|
|
1195
|
+
value = {
|
|
1196
|
+
kind: 'binop',
|
|
1197
|
+
operator: node.operator.slice(0, -1),
|
|
1198
|
+
left: { kind: 'ref', name: node.left.name, start: node.left.start, length: node.left.length },
|
|
1199
|
+
right: value,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
kind: 'assign', target: node.left.name, value,
|
|
1204
|
+
start: node.left.start, length: node.left.length,
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* `a[i] = v`, `a[i] += v`, `a[i]++`: one element written. A const
|
|
1210
|
+
* array's elements are data in the program, and the checker has already
|
|
1211
|
+
* said so for this module's own; the linker says so for an imported one.
|
|
1212
|
+
* `operator` is the binary operator of a compound assignment (or null),
|
|
1213
|
+
* and `right` its right-hand side (or null for `++`/`--`, which is `+ 1`).
|
|
1214
|
+
*/
|
|
1215
|
+
indexStore(left, operator, right) {
|
|
1216
|
+
if (left.object?.type === NodeType.Identifier && this.isStringParameter(left.object.name)) {
|
|
1217
|
+
return this.fail(left, `a string is assigned whole (${left.object.name} = "..."), not one character at a time`);
|
|
1218
|
+
}
|
|
1219
|
+
const target = this.arrayReference(left.object, 'assigning to an element');
|
|
1220
|
+
if (!target) return null;
|
|
1221
|
+
if (target.array?.constant) {
|
|
1222
|
+
this.diagnostics.push(diagnostic(
|
|
1223
|
+
Codes.ASSIGN_TO_CONST,
|
|
1224
|
+
`'${left.object.name}' is a const array — data in the program, not RAM — and cannot be assigned to`,
|
|
1225
|
+
this.file, left.object.start, left.object.length,
|
|
1226
|
+
));
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
const index = this.expression(left.index);
|
|
1230
|
+
if (!index || !this.indexInRange(left.index, index, target.array)) return null;
|
|
1231
|
+
let value = right ? this.expression(right) : { kind: 'const', value: 1 };
|
|
1232
|
+
if (!value) return null;
|
|
1233
|
+
if (operator) {
|
|
1234
|
+
// `a[i] += e` is `a[i] = a[i] + e`; the read gets its own copies of the
|
|
1235
|
+
// array and index nodes, since the linker renames in place.
|
|
1236
|
+
value = {
|
|
1237
|
+
kind: 'binop', operator,
|
|
1238
|
+
left: { kind: 'index', array: structuredClone(target.ref), index: structuredClone(index), elementType: target.array?.type ?? null },
|
|
1239
|
+
right: value,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
return {
|
|
1243
|
+
kind: 'storeIndex', array: target.ref, index, value, elementType: target.array?.type ?? null,
|
|
1244
|
+
start: left.object.start, length: left.object.length,
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
update(node) {
|
|
1249
|
+
if (!node.argument?.type) return null; // half-typed; the parser reported it
|
|
1250
|
+
if (node.argument.type === NodeType.IndexExpression) {
|
|
1251
|
+
return this.indexStore(node.argument, node.operator === '++' ? '+' : '-', null);
|
|
1252
|
+
}
|
|
1253
|
+
if (node.argument.type !== NodeType.Identifier) {
|
|
1254
|
+
return this.fail(node.argument, `updating a ${node.argument.type} is not compilable yet`);
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
kind: 'assign',
|
|
1258
|
+
target: node.argument.name,
|
|
1259
|
+
start: node.argument.start,
|
|
1260
|
+
length: node.argument.length,
|
|
1261
|
+
value: {
|
|
1262
|
+
kind: 'binop',
|
|
1263
|
+
operator: node.operator === '++' ? '+' : '-',
|
|
1264
|
+
left: { kind: 'ref', name: node.argument.name, start: node.argument.start, length: node.argument.length },
|
|
1265
|
+
right: { kind: 'const', value: 1 },
|
|
1266
|
+
},
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
expression(node) {
|
|
1271
|
+
if (!node?.type) return null; // a hole the parser already reported
|
|
1272
|
+
switch (node.type) {
|
|
1273
|
+
case NodeType.IntegerLiteral:
|
|
1274
|
+
return { kind: 'const', value: node.value };
|
|
1275
|
+
case NodeType.BooleanLiteral:
|
|
1276
|
+
return { kind: 'const', value: node.value ? 1 : 0 };
|
|
1277
|
+
case NodeType.StringLiteral:
|
|
1278
|
+
return this.stringConstant(node, node.value);
|
|
1279
|
+
case NodeType.TemplateLiteral:
|
|
1280
|
+
this.diagnostics.push(misplacedTemplate(node, this.file));
|
|
1281
|
+
return null;
|
|
1282
|
+
case NodeType.IndexExpression: {
|
|
1283
|
+
// `s[i]`: the i-th byte of a string parameter; otherwise `a[i]`, an
|
|
1284
|
+
// element of an array.
|
|
1285
|
+
if (node.object?.type === NodeType.Identifier && this.isStringParameter(node.object.name)) {
|
|
1286
|
+
const index = this.expression(node.index);
|
|
1287
|
+
if (!index) return null;
|
|
1288
|
+
return { kind: 'stringByte', string: this.stringRef(node.object), index };
|
|
1289
|
+
}
|
|
1290
|
+
return this.indexRead(node);
|
|
1291
|
+
}
|
|
1292
|
+
case NodeType.ArrayLiteral:
|
|
1293
|
+
return this.fail(node, 'an array literal only initialises an array<T, N> declaration');
|
|
1294
|
+
case NodeType.Identifier:
|
|
1295
|
+
// A const declared in this module is its value, here and now — but
|
|
1296
|
+
// a parameter of the same name shadows it, ordinary lexical scoping.
|
|
1297
|
+
if (!this.currentParams.has(node.name) && this.ownConsts.has(node.name)) {
|
|
1298
|
+
return { kind: 'const', value: this.ownConsts.get(node.name) };
|
|
1299
|
+
}
|
|
1300
|
+
if (!this.currentParams.has(node.name) && this.ownStrings.has(node.name)) {
|
|
1301
|
+
return { kind: 'string', index: this.ownStrings.get(node.name), start: node.start, length: node.length };
|
|
1302
|
+
}
|
|
1303
|
+
// An array is used one element at a time; its bare name is not a
|
|
1304
|
+
// value (there is no array assignment or array argument yet).
|
|
1305
|
+
if (!this.currentParams.has(node.name) && this.arrays.has(node.name)) {
|
|
1306
|
+
return this.fail(node, `'${node.name}' is an array: read an element (${node.name}[i]) or its .length`);
|
|
1307
|
+
}
|
|
1308
|
+
if (this.currentArrayParams?.has(node.name)) {
|
|
1309
|
+
return this.fail(node, `'${node.name}' is an array parameter: read an element (${node.name}[i]) or its .length`);
|
|
1310
|
+
}
|
|
1311
|
+
// The span rides along so the linker can point a diagnostic at the
|
|
1312
|
+
// exact reference when a name resolves to nothing.
|
|
1313
|
+
return { kind: 'ref', name: node.name, start: node.start, length: node.length };
|
|
1314
|
+
case NodeType.BinaryExpression: {
|
|
1315
|
+
const left = this.expression(node.left);
|
|
1316
|
+
const right = this.expression(node.right);
|
|
1317
|
+
return left && right
|
|
1318
|
+
? { kind: 'binop', operator: node.operator, left, right }
|
|
1319
|
+
: null;
|
|
1320
|
+
}
|
|
1321
|
+
case NodeType.UnaryExpression: {
|
|
1322
|
+
const argument = this.expression(node.argument);
|
|
1323
|
+
if (!argument) return null;
|
|
1324
|
+
// A signed literal is written `-128`, so a negated constant has to
|
|
1325
|
+
// *be* a constant — otherwise the narrowest thing a `tinyint` can
|
|
1326
|
+
// hold could not initialise one. Only `-`/`+`, whose meaning on a
|
|
1327
|
+
// number needs no width to know; `~` and `!` are left to the target.
|
|
1328
|
+
if (argument.kind === 'const' && (node.operator === '-' || node.operator === '+')) {
|
|
1329
|
+
return { kind: 'const', value: node.operator === '-' ? -argument.value : argument.value };
|
|
1330
|
+
}
|
|
1331
|
+
return { kind: 'unop', operator: node.operator, argument };
|
|
1332
|
+
}
|
|
1333
|
+
case NodeType.CallExpression: {
|
|
1334
|
+
const call = this.callExpression(node);
|
|
1335
|
+
if (!call) return null;
|
|
1336
|
+
if (call.kind === 'memoryWrite') {
|
|
1337
|
+
return this.fail(node, 'memory.write does not return a value and cannot be used as an expression');
|
|
1338
|
+
}
|
|
1339
|
+
if (call.kind === 'waitFrame') {
|
|
1340
|
+
return this.fail(node, 'waitFrame() does not return a value and cannot be used as an expression');
|
|
1341
|
+
}
|
|
1342
|
+
return call;
|
|
1343
|
+
}
|
|
1344
|
+
case NodeType.MemberExpression: {
|
|
1345
|
+
if (node.object.type !== NodeType.Identifier) {
|
|
1346
|
+
return this.fail(node, 'a member expression is not compilable yet');
|
|
1347
|
+
}
|
|
1348
|
+
// `s.length`: how many bytes a string parameter holds.
|
|
1349
|
+
if (this.isStringParameter(node.object.name)) {
|
|
1350
|
+
if (node.property.name !== 'length') {
|
|
1351
|
+
return this.fail(node, `a string has no '${node.property.name}'; it has .length and s[i]`);
|
|
1352
|
+
}
|
|
1353
|
+
return { kind: 'stringLength', string: this.stringRef(node.object) };
|
|
1354
|
+
}
|
|
1355
|
+
// `t.length` on an array parameter: the length is part of the
|
|
1356
|
+
// parameter's type, so it is a constant here and nothing about it
|
|
1357
|
+
// reaches the machine.
|
|
1358
|
+
if (this.currentArrayParams.has(node.object.name)) {
|
|
1359
|
+
if (node.property.name !== 'length') {
|
|
1360
|
+
return this.fail(node, `an array has no '${node.property.name}'; it has .length and a[i]`);
|
|
1361
|
+
}
|
|
1362
|
+
return { kind: 'const', value: this.currentArrayParams.get(node.object.name).length };
|
|
1363
|
+
}
|
|
1364
|
+
// `a.length` on this module's own array: a number, right here. On a
|
|
1365
|
+
// name this module does not declare it is left as a namespace const
|
|
1366
|
+
// — the linker turns it back into a length if the name resolves to
|
|
1367
|
+
// an imported array instead of a namespace.
|
|
1368
|
+
if (!this.currentParams.has(node.object.name) && this.arrays.has(node.object.name)) {
|
|
1369
|
+
if (node.property.name !== 'length') {
|
|
1370
|
+
return this.fail(node, `an array has no '${node.property.name}'; it has .length and a[i]`);
|
|
1371
|
+
}
|
|
1372
|
+
return { kind: 'const', value: this.arrays.get(node.object.name).length };
|
|
1373
|
+
}
|
|
1374
|
+
// `BorderColor.BLUE`: a namespace const used as a value, not a call.
|
|
1375
|
+
// Resolved by the linker the same way a namespace-qualified call is —
|
|
1376
|
+
// lowering only records which namespace and member were named.
|
|
1377
|
+
return {
|
|
1378
|
+
kind: 'namespaceConst',
|
|
1379
|
+
namespace: node.object.name,
|
|
1380
|
+
member: node.property.name,
|
|
1381
|
+
start: node.start,
|
|
1382
|
+
length: node.length,
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
default:
|
|
1386
|
+
return this.fail(node, `a ${node.type} expression is not compilable yet`);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Lower a parsed program to IR.
|
|
1393
|
+
*
|
|
1394
|
+
* @param {object} ast
|
|
1395
|
+
* @param {string} file
|
|
1396
|
+
* @returns {{ ir: IrProgram, diagnostics: object[] }}
|
|
1397
|
+
*/
|
|
1398
|
+
export function lower(ast, file = '<unknown>', source = null) {
|
|
1399
|
+
const lowering = new Lowering(file);
|
|
1400
|
+
lowering.source = source;
|
|
1401
|
+
const ir = lowering.program(ast);
|
|
1402
|
+
return { ir, diagnostics: lowering.diagnostics };
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* The program's entry function, by output name, or null when the IR has none.
|
|
1407
|
+
*
|
|
1408
|
+
* Linked IR records it as `ir.entry` (the linker enforces exactly one
|
|
1409
|
+
* exported, parameterless function in the entry module). IR straight from
|
|
1410
|
+
* `lower()` — a single module, as the backends' own tests use — has no
|
|
1411
|
+
* linker to have decided, so the same rule is applied here after the fact:
|
|
1412
|
+
* the sole exported function, if there is exactly one. Both backends go
|
|
1413
|
+
* through this so "which function is the program" is decided in one place.
|
|
1414
|
+
*
|
|
1415
|
+
* @param {IrProgram & { entry?: string }} ir
|
|
1416
|
+
* @returns {string|null}
|
|
1417
|
+
*/
|
|
1418
|
+
export function entryOf(ir) {
|
|
1419
|
+
if (ir.entry) return ir.entry;
|
|
1420
|
+
const exported = ir.functions.filter((fn) => fn.exported);
|
|
1421
|
+
return exported.length === 1 ? exported[0].name : null;
|
|
1422
|
+
}
|