@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.
@@ -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
+ }