@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
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// The lexer: raw text in, tokens out.
|
|
2
|
+
//
|
|
3
|
+
// First layer of the pipeline described in docs/compiler.md. It is deliberately
|
|
4
|
+
// the only layer that exists today, because it is the one that pays off before
|
|
5
|
+
// a parser does: it finds unterminated strings, stray characters, and unbalanced
|
|
6
|
+
// brackets, which is already enough to put real errors under the cursor.
|
|
7
|
+
//
|
|
8
|
+
// Every token carries its offset and length. Diagnostics are built from those,
|
|
9
|
+
// so a position is never recomputed by guesswork later.
|
|
10
|
+
import { Codes, diagnostic } from '../diagnostics/index.mjs';
|
|
11
|
+
import { INTEGER_TYPE_NAMES } from '../types/index.mjs';
|
|
12
|
+
|
|
13
|
+
export const TokenKind = {
|
|
14
|
+
Comment: 'comment',
|
|
15
|
+
String: 'string',
|
|
16
|
+
// A backtick string with `${...}` fields — `TICK ${ticks} OPTION ${option}`.
|
|
17
|
+
// One token for the whole thing; `parts` records where its literal text
|
|
18
|
+
// and its field sources sit, and the parser re-lexes each field.
|
|
19
|
+
Template: 'template',
|
|
20
|
+
Number: 'number',
|
|
21
|
+
Identifier: 'identifier',
|
|
22
|
+
Keyword: 'keyword',
|
|
23
|
+
Type: 'type',
|
|
24
|
+
Decorator: 'decorator',
|
|
25
|
+
// `#frames` — a function the compiler evaluates, never the target. The
|
|
26
|
+
// `#` is the one spelling that says "8bitscript resolves this before any
|
|
27
|
+
// target toolchain runs"; a plain `name(...)` always runs on the machine.
|
|
28
|
+
CompileTime: 'compileTime',
|
|
29
|
+
AsmBlock: 'asm',
|
|
30
|
+
Punctuation: 'punctuation',
|
|
31
|
+
Operator: 'operator',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const KEYWORDS = new Set([
|
|
35
|
+
'let', 'const', 'function', 'return', 'export', 'import', 'from', 'as',
|
|
36
|
+
'if', 'else', 'while', 'for', 'do', 'break', 'continue',
|
|
37
|
+
'switch', 'case', 'default', 'true', 'false', 'asm6502', 'namespace',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// Every primitive integer spelling comes from the shared registry — the
|
|
41
|
+
// canonical names (`utinyint`, `int`, ...) and the low-level aliases (`u8`,
|
|
42
|
+
// `i32`, ...) — so this set can't drift out of sync with the checker, the
|
|
43
|
+
// backends, or hover/completion.
|
|
44
|
+
export const TYPE_NAMES = new Set([
|
|
45
|
+
...INTEGER_TYPE_NAMES,
|
|
46
|
+
'bool', 'void', 'string', 'ptr', 'array', 'volatile',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const BRACKET_PAIRS = { ')': '(', ']': '[', '}': '{' };
|
|
50
|
+
const OPEN_BRACKETS = new Set(['(', '[', '{']);
|
|
51
|
+
|
|
52
|
+
// Operators, longest first, matched by maximal munch against this list only.
|
|
53
|
+
// Greedily globbing operator characters is how `x=-1` ends up lexed as the
|
|
54
|
+
// non-operator `=-`; matching real operators cannot produce a token that no
|
|
55
|
+
// rule of the language recognises.
|
|
56
|
+
const OPERATORS = [
|
|
57
|
+
'<<=', '>>=',
|
|
58
|
+
'==', '!=', '<=', '>=', '&&', '||', '<<', '>>', '++', '--',
|
|
59
|
+
'+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=',
|
|
60
|
+
'+', '-', '*', '/', '%', '&', '|', '^', '~', '!', '<', '>', '=',
|
|
61
|
+
'?', ':', ';', ',', '.',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const DIGITS_FOR_RADIX = { 2: /[01_]/, 10: /[0-9_]/, 16: /[0-9a-fA-F_]/ };
|
|
65
|
+
|
|
66
|
+
// `$` is not an identifier character: it introduces hex literals ($900F). A
|
|
67
|
+
// language aimed at this hardware gives the assembly spelling priority.
|
|
68
|
+
const isIdentStart = (c) => /[A-Za-z_]/.test(c);
|
|
69
|
+
const isIdentPart = (c) => /[A-Za-z0-9_]/.test(c);
|
|
70
|
+
const isDigit = (c) => c >= '0' && c <= '9';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Tokenize a source file.
|
|
74
|
+
*
|
|
75
|
+
* Always returns both tokens and diagnostics: lexing never throws, because an
|
|
76
|
+
* editor asks for tokens on every keystroke and half-typed source is the normal
|
|
77
|
+
* case, not an exceptional one.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} text
|
|
80
|
+
* @param {string} file
|
|
81
|
+
* @returns {{ tokens: object[], diagnostics: object[] }}
|
|
82
|
+
*/
|
|
83
|
+
export function tokenize(text, file = '<unknown>') {
|
|
84
|
+
const tokens = [];
|
|
85
|
+
const diagnostics = [];
|
|
86
|
+
const brackets = [];
|
|
87
|
+
let i = 0;
|
|
88
|
+
|
|
89
|
+
const push = (kind, start, end, extra = {}) =>
|
|
90
|
+
tokens.push({ kind, start, length: end - start, text: text.slice(start, end), ...extra });
|
|
91
|
+
|
|
92
|
+
while (i < text.length) {
|
|
93
|
+
const c = text[i];
|
|
94
|
+
|
|
95
|
+
if (c === ' ' || c === '\t' || c === '\r' || c === '\n') {
|
|
96
|
+
i += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Comments, both spellings.
|
|
101
|
+
if (c === '/' && text[i + 1] === '/') {
|
|
102
|
+
const start = i;
|
|
103
|
+
while (i < text.length && text[i] !== '\n') i += 1;
|
|
104
|
+
push(TokenKind.Comment, start, i);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (c === '/' && text[i + 1] === '*') {
|
|
108
|
+
const start = i;
|
|
109
|
+
i += 2;
|
|
110
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i += 1;
|
|
111
|
+
if (i >= text.length) {
|
|
112
|
+
diagnostics.push(
|
|
113
|
+
diagnostic(
|
|
114
|
+
Codes.UNTERMINATED_BLOCK_COMMENT,
|
|
115
|
+
'unterminated block comment',
|
|
116
|
+
file, start, text.length - start,
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
push(TokenKind.Comment, start, text.length);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
i += 2;
|
|
123
|
+
push(TokenKind.Comment, start, i);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Strings. A newline ends the search: an unterminated string should report
|
|
128
|
+
// on its own line rather than swallowing the rest of the file.
|
|
129
|
+
if (c === '"' || c === "'") {
|
|
130
|
+
const start = i;
|
|
131
|
+
const quote = c;
|
|
132
|
+
i += 1;
|
|
133
|
+
let closed = false;
|
|
134
|
+
while (i < text.length) {
|
|
135
|
+
if (text[i] === '\\') { i += 2; continue; }
|
|
136
|
+
if (text[i] === quote) { i += 1; closed = true; break; }
|
|
137
|
+
if (text[i] === '\n') break;
|
|
138
|
+
i += 1;
|
|
139
|
+
}
|
|
140
|
+
if (!closed) {
|
|
141
|
+
diagnostics.push(
|
|
142
|
+
diagnostic(
|
|
143
|
+
Codes.UNTERMINATED_STRING,
|
|
144
|
+
'unterminated string literal',
|
|
145
|
+
file, start, i - start,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
push(TokenKind.String, start, i, closed ? {} : { unterminated: true });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Template strings: `TICK ${ticks} OPTION ${option}`. Lexed as one token
|
|
154
|
+
// so the parser sees a single literal, with `parts` marking each run of
|
|
155
|
+
// text and each `${...}` field's source span (the field's own tokens are
|
|
156
|
+
// produced by the parser re-lexing that span, offsets intact). Braces
|
|
157
|
+
// nest inside a field so a future `{`-bearing expression still ends at
|
|
158
|
+
// the right `}`. Single-line, like the other strings.
|
|
159
|
+
if (c === '`') {
|
|
160
|
+
const start = i;
|
|
161
|
+
i += 1;
|
|
162
|
+
const parts = [];
|
|
163
|
+
let textStart = i;
|
|
164
|
+
let closed = false;
|
|
165
|
+
const flushText = (end) => {
|
|
166
|
+
if (end > textStart) parts.push({ kind: 'text', start: textStart, end });
|
|
167
|
+
};
|
|
168
|
+
while (i < text.length) {
|
|
169
|
+
if (text[i] === '\\') { i += 2; continue; }
|
|
170
|
+
if (text[i] === '`') { flushText(i); i += 1; closed = true; break; }
|
|
171
|
+
if (text[i] === '\n') break;
|
|
172
|
+
if (text[i] === '$' && text[i + 1] === '{') {
|
|
173
|
+
flushText(i);
|
|
174
|
+
const fieldStart = i;
|
|
175
|
+
i += 2;
|
|
176
|
+
const sourceStart = i;
|
|
177
|
+
let depth = 1;
|
|
178
|
+
while (i < text.length && text[i] !== '\n') {
|
|
179
|
+
if (text[i] === '{') depth += 1;
|
|
180
|
+
else if (text[i] === '}') { depth -= 1; if (depth === 0) break; }
|
|
181
|
+
i += 1;
|
|
182
|
+
}
|
|
183
|
+
if (depth !== 0) {
|
|
184
|
+
diagnostics.push(diagnostic(
|
|
185
|
+
Codes.UNTERMINATED_STRING, "unterminated '${' field in template string", file, fieldStart, i - fieldStart,
|
|
186
|
+
));
|
|
187
|
+
// Recorded anyway: a field being typed has no `}` yet, and
|
|
188
|
+
// hover and completion inside it are exactly what a person
|
|
189
|
+
// wants at that moment (half-typed source is the normal input,
|
|
190
|
+
// see this file's header).
|
|
191
|
+
parts.push({ kind: 'field', start: fieldStart, end: i, sourceStart, sourceEnd: i });
|
|
192
|
+
closed = true; // one diagnostic, not this plus "unterminated template"
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
parts.push({ kind: 'field', start: fieldStart, end: i + 1, sourceStart, sourceEnd: i });
|
|
196
|
+
i += 1;
|
|
197
|
+
textStart = i;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
i += 1;
|
|
201
|
+
}
|
|
202
|
+
if (!closed) {
|
|
203
|
+
diagnostics.push(diagnostic(
|
|
204
|
+
Codes.UNTERMINATED_STRING, 'unterminated template string', file, start, i - start,
|
|
205
|
+
));
|
|
206
|
+
}
|
|
207
|
+
push(TokenKind.Template, start, i, { parts });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Numbers, in the C spellings and the assembly spellings.
|
|
212
|
+
//
|
|
213
|
+
// `%` is also the modulo operator, so `%101` is a binary literal only where
|
|
214
|
+
// a value is expected: after an identifier, a literal, or a closing bracket
|
|
215
|
+
// the `%` in `x%2` has to be modulo. `$` has no such conflict.
|
|
216
|
+
const prev = tokens[tokens.length - 1];
|
|
217
|
+
const prevIsOperand = prev && (
|
|
218
|
+
prev.kind === TokenKind.Identifier ||
|
|
219
|
+
prev.kind === TokenKind.Number ||
|
|
220
|
+
prev.kind === TokenKind.String ||
|
|
221
|
+
prev.kind === TokenKind.Template ||
|
|
222
|
+
prev.kind === TokenKind.Type ||
|
|
223
|
+
[')', ']'].includes(prev.text)
|
|
224
|
+
);
|
|
225
|
+
const startsBinaryLiteral = c === '%' && /[01]/.test(text[i + 1] ?? '') && !prevIsOperand;
|
|
226
|
+
const startsHexLiteral = c === '$' && /[0-9a-fA-F]/.test(text[i + 1] ?? '');
|
|
227
|
+
|
|
228
|
+
if (isDigit(c) || startsHexLiteral || startsBinaryLiteral) {
|
|
229
|
+
const start = i;
|
|
230
|
+
let radix = 10;
|
|
231
|
+
if (c === '$') { radix = 16; i += 1; }
|
|
232
|
+
else if (c === '%') { radix = 2; i += 1; }
|
|
233
|
+
else if (c === '0' && /[xX]/.test(text[i + 1] ?? '')) { radix = 16; i += 2; }
|
|
234
|
+
else if (c === '0' && /[bB]/.test(text[i + 1] ?? '')) { radix = 2; i += 2; }
|
|
235
|
+
const digitsStart = i;
|
|
236
|
+
const digitPattern = DIGITS_FOR_RADIX[radix];
|
|
237
|
+
while (i < text.length && digitPattern.test(text[i])) i += 1;
|
|
238
|
+
const digits = text.slice(digitsStart, i).replace(/_/g, '');
|
|
239
|
+
if (digits === '') {
|
|
240
|
+
// `0x` with nothing after it. Reported here so the value can never be
|
|
241
|
+
// a silent NaN travelling through the checker.
|
|
242
|
+
diagnostics.push(
|
|
243
|
+
diagnostic(
|
|
244
|
+
Codes.INVALID_NUMBER,
|
|
245
|
+
`invalid number literal '${text.slice(start, i)}'`,
|
|
246
|
+
file, start, i - start,
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
push(TokenKind.Number, start, i, { value: 0, radix });
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
// A decimal fraction — `0.5` — radix 10 only, and only when a digit
|
|
253
|
+
// actually follows the `.`: a bare `1.` stays `1` then the `.`
|
|
254
|
+
// operator (unchanged), and `array<u8, 16>`-style code elsewhere in
|
|
255
|
+
// the grammar never wants a Number token to swallow a trailing `.`.
|
|
256
|
+
// Recorded as an exact numerator/denominator pair, never as a
|
|
257
|
+
// floating-point value used for arithmetic — the only thing that ever
|
|
258
|
+
// reads `isDecimal`/`numerator`/`denominator` is the `#frames(...)`
|
|
259
|
+
// compile-time fold (packages/compiler/src/fold), which works in
|
|
260
|
+
// exact integers throughout; `value` here is cosmetic only (kept for
|
|
261
|
+
// uniformity with plain-integer Number tokens).
|
|
262
|
+
if (radix === 10 && text[i] === '.' && isDigit(text[i + 1] ?? '')) {
|
|
263
|
+
i += 1; // the '.'
|
|
264
|
+
const fracStart = i;
|
|
265
|
+
while (i < text.length && digitPattern.test(text[i])) i += 1;
|
|
266
|
+
const fracDigits = text.slice(fracStart, i).replace(/_/g, '');
|
|
267
|
+
push(TokenKind.Number, start, i, {
|
|
268
|
+
value: Number.parseFloat(text.slice(start, i)),
|
|
269
|
+
radix,
|
|
270
|
+
isDecimal: true,
|
|
271
|
+
numerator: Number.parseInt(digits + fracDigits, 10),
|
|
272
|
+
denominator: 10 ** fracDigits.length,
|
|
273
|
+
});
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
push(TokenKind.Number, start, i, { value: Number.parseInt(digits, radix), radix });
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// `#frames(...)` — a compile-time function (see TokenKind.CompileTime).
|
|
282
|
+
if (c === '#' && isIdentStart(text[i + 1] ?? '')) {
|
|
283
|
+
const start = i;
|
|
284
|
+
i += 1;
|
|
285
|
+
while (i < text.length && isIdentPart(text[i])) i += 1;
|
|
286
|
+
push(TokenKind.CompileTime, start, i);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// `@address(0x900F)` and friends.
|
|
291
|
+
if (c === '@' && isIdentStart(text[i + 1] ?? '')) {
|
|
292
|
+
const start = i;
|
|
293
|
+
i += 1;
|
|
294
|
+
while (i < text.length && isIdentPart(text[i])) i += 1;
|
|
295
|
+
push(TokenKind.Decorator, start, i);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (isIdentStart(c)) {
|
|
300
|
+
const start = i;
|
|
301
|
+
while (i < text.length && isIdentPart(text[i])) i += 1;
|
|
302
|
+
const word = text.slice(start, i);
|
|
303
|
+
const kind = KEYWORDS.has(word)
|
|
304
|
+
? TokenKind.Keyword
|
|
305
|
+
: TYPE_NAMES.has(word)
|
|
306
|
+
? TokenKind.Type
|
|
307
|
+
: TokenKind.Identifier;
|
|
308
|
+
push(kind, start, i);
|
|
309
|
+
|
|
310
|
+
// The body of an `asm6502 { ... }` block is 6502 assembly, not
|
|
311
|
+
// 8BitScript. Lexing it as 8BitScript is simply wrong — `lda #$06` would
|
|
312
|
+
// report `#` as an unexpected character — so the whole block is taken as
|
|
313
|
+
// one opaque token and handed to the backend untouched.
|
|
314
|
+
if (word === 'asm6502') {
|
|
315
|
+
let j = i;
|
|
316
|
+
while (j < text.length && /\s/.test(text[j])) j += 1;
|
|
317
|
+
if (text[j] === '{') {
|
|
318
|
+
const bodyStart = j;
|
|
319
|
+
let depth = 0;
|
|
320
|
+
while (j < text.length) {
|
|
321
|
+
if (text[j] === '{') depth += 1;
|
|
322
|
+
else if (text[j] === '}') {
|
|
323
|
+
depth -= 1;
|
|
324
|
+
if (depth === 0) { j += 1; break; }
|
|
325
|
+
}
|
|
326
|
+
j += 1;
|
|
327
|
+
}
|
|
328
|
+
if (depth !== 0) {
|
|
329
|
+
diagnostics.push(
|
|
330
|
+
diagnostic(
|
|
331
|
+
Codes.UNTERMINATED_ASM_BLOCK,
|
|
332
|
+
'unterminated asm6502 block',
|
|
333
|
+
file, bodyStart, text.length - bodyStart,
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
push(TokenKind.AsmBlock, bodyStart, j);
|
|
338
|
+
i = j;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (OPEN_BRACKETS.has(c)) {
|
|
345
|
+
brackets.push({ char: c, offset: i });
|
|
346
|
+
push(TokenKind.Punctuation, i, i + 1);
|
|
347
|
+
i += 1;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (c in BRACKET_PAIRS) {
|
|
351
|
+
const top = brackets.pop();
|
|
352
|
+
if (!top || top.char !== BRACKET_PAIRS[c]) {
|
|
353
|
+
diagnostics.push(
|
|
354
|
+
diagnostic(Codes.UNMATCHED_BRACKET, `unmatched '${c}'`, file, i, 1),
|
|
355
|
+
);
|
|
356
|
+
if (top) brackets.push(top);
|
|
357
|
+
}
|
|
358
|
+
push(TokenKind.Punctuation, i, i + 1);
|
|
359
|
+
i += 1;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const operator = OPERATORS.find((op) => text.startsWith(op, i));
|
|
364
|
+
if (operator) {
|
|
365
|
+
push(TokenKind.Operator, i, i + operator.length);
|
|
366
|
+
i += operator.length;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
diagnostics.push(
|
|
371
|
+
diagnostic(Codes.UNEXPECTED_CHARACTER, `unexpected character '${c}'`, file, i, 1),
|
|
372
|
+
);
|
|
373
|
+
i += 1;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const open of brackets) {
|
|
377
|
+
diagnostics.push(
|
|
378
|
+
diagnostic(Codes.UNCLOSED_BRACKET, `unclosed '${open.char}'`, file, open.offset, 1),
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return { tokens, diagnostics };
|
|
383
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Hardware hazards: writes a target documents as able to damage the machine.
|
|
2
|
+
//
|
|
3
|
+
// The root AGENTS.md warns against turning one game's trivia into a compiler
|
|
4
|
+
// diagnostic. This table is the deliberate exception, and its bar is high: a
|
|
5
|
+
// write goes here only when the target's own documentation says it can
|
|
6
|
+
// destroy hardware — not crash the program, not garble the screen, destroy
|
|
7
|
+
// hardware. There is one entry today. The PET's "killer poke" (POKE 59458,62
|
|
8
|
+
// — `$E842`, the 6522 VIA's data-direction register B) makes port bit PB5
|
|
9
|
+
// an output. PB5 is the vertical-retrace *input*; on the CRTC models (the
|
|
10
|
+
// 12-inch 4032, 8032 and later) it is wired to the CRTC's vertical sync,
|
|
11
|
+
// which also drives the monitor, and driving that line drags its level down
|
|
12
|
+
// until the monitor's vertical deflection misbehaves and, in time, the
|
|
13
|
+
// flyback fails. See packages/pet/AGENTS.md ("Hazards").
|
|
14
|
+
//
|
|
15
|
+
// The rule is narrow on purpose: the register has legitimate uses (its
|
|
16
|
+
// other bits steer the cassette motor and IEEE-488 lines), so a write whose
|
|
17
|
+
// value 8bitscript can see at compile time — a literal or a const — with bit
|
|
18
|
+
// 5 clear is allowed, and only a write it cannot prove safe is refused: a
|
|
19
|
+
// constant with bit 5 set, or a runtime value. Reads are always fine.
|
|
20
|
+
//
|
|
21
|
+
// The check runs in the linker, not the checker, because it needs two
|
|
22
|
+
// things only the linker has: the machine being built for, and every const
|
|
23
|
+
// already inlined (so `memory.write(VIA_DDRB, 62)` with `VIA_DDRB` imported
|
|
24
|
+
// from another module is the same write as the literal). `8bs check` and the
|
|
25
|
+
// editor analyse files without a machine, so this class of diagnostic is a
|
|
26
|
+
// build-time one; docs/compiler.md and docs/language-server.md say so.
|
|
27
|
+
|
|
28
|
+
import { Codes, diagnostic } from '../diagnostics/index.mjs';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Per machine: the addresses whose writes are checked. `forbiddenBits` is
|
|
32
|
+
* the mask a compile-time value must have clear to be allowed; `name` and
|
|
33
|
+
* `why` are the words of the message.
|
|
34
|
+
*/
|
|
35
|
+
export const HARDWARE_HAZARDS = {
|
|
36
|
+
pet: [
|
|
37
|
+
{
|
|
38
|
+
address: 0xE842,
|
|
39
|
+
forbiddenBits: 0x20,
|
|
40
|
+
name: 'the VIA\'s data-direction register B ($E842, POKE 59458)',
|
|
41
|
+
why: 'a value with bit 5 set makes PB5 an output — the PET "killer poke", which drives the monitor\'s '
|
|
42
|
+
+ 'vertical sync on the CRTC models (4032, 8032 and later) and can destroy a 12-inch PET display. '
|
|
43
|
+
+ 'Only a compile-time value with bit 5 clear may be written here; see packages/pet/AGENTS.md',
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const hex = (n) => `$${n.toString(16).toUpperCase().padStart(4, '0')}`;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Walk the linked program's functions for writes to a hazardous address on
|
|
52
|
+
* `machine`, reporting each as `8BS3003` in the file it was written in.
|
|
53
|
+
*
|
|
54
|
+
* Three ways to write an address are covered: `memory.write(address, v)`
|
|
55
|
+
* with a compile-time address; assignment to a scalar global declared
|
|
56
|
+
* `@address(...)` there; and an element store into an `@address` array that
|
|
57
|
+
* covers it (a compile-time index that lands on it, or a runtime index,
|
|
58
|
+
* which cannot be proved to miss). Everything else — a runtime address to
|
|
59
|
+
* `memory.write`, say — is the program's own responsibility, as on the
|
|
60
|
+
* machine.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} ir the linked IR (globals carry output names and addresses)
|
|
63
|
+
* @param {string|undefined} machine
|
|
64
|
+
* @param {Map<object, string>} fileOf each function to the file it came from
|
|
65
|
+
* @param {object[]} diagnostics
|
|
66
|
+
*/
|
|
67
|
+
export function checkHardwareHazards(ir, machine, fileOf, diagnostics) {
|
|
68
|
+
const hazards = machine ? HARDWARE_HAZARDS[machine] : undefined;
|
|
69
|
+
if (!hazards) return;
|
|
70
|
+
|
|
71
|
+
// The globals that map a hazardous address, by output name: scalars at
|
|
72
|
+
// it exactly, arrays that span it (with the element offset that is it).
|
|
73
|
+
const scalars = new Map();
|
|
74
|
+
const arrays = new Map();
|
|
75
|
+
for (const g of ir.globals) {
|
|
76
|
+
if (g.address === null || g.address === undefined) continue;
|
|
77
|
+
for (const hazard of hazards) {
|
|
78
|
+
if (!g.array && g.address === hazard.address) scalars.set(g.name, hazard);
|
|
79
|
+
if (g.array && g.address <= hazard.address && hazard.address < g.address + g.array) {
|
|
80
|
+
arrays.set(g.name, { hazard, offset: hazard.address - g.address });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const safe = (value, hazard) => value?.kind === 'const' && (value.value & hazard.forbiddenBits) === 0;
|
|
86
|
+
const report = (node, file, hazard, how) => diagnostics.push(diagnostic(
|
|
87
|
+
Codes.HARDWARE_HAZARD,
|
|
88
|
+
`${how} ${hazard.name} on the ${machine}: ${hazard.why}`,
|
|
89
|
+
file, node.start ?? 0, node.length ?? 0,
|
|
90
|
+
));
|
|
91
|
+
const describe = (value) => (value?.kind === 'const' ? `writing ${value.value} to` : 'writing a runtime value to');
|
|
92
|
+
|
|
93
|
+
const visit = (s, file) => {
|
|
94
|
+
if (s.kind === 'memoryWrite' && s.address?.kind === 'const') {
|
|
95
|
+
const hazard = hazards.find((h) => h.address === s.address.value);
|
|
96
|
+
if (hazard && !safe(s.value, hazard)) report(s, file, hazard, describe(s.value));
|
|
97
|
+
} else if (s.kind === 'assign' && scalars.has(s.target)) {
|
|
98
|
+
const hazard = scalars.get(s.target);
|
|
99
|
+
if (!safe(s.value, hazard)) report(s, file, hazard, describe(s.value));
|
|
100
|
+
} else if (s.kind === 'storeIndex' && arrays.has(s.array?.name)) {
|
|
101
|
+
const { hazard, offset } = arrays.get(s.array.name);
|
|
102
|
+
const index = s.index;
|
|
103
|
+
if (index?.kind === 'const' && index.value !== offset) return;
|
|
104
|
+
if (!safe(s.value, hazard)) {
|
|
105
|
+
const how = index?.kind === 'const'
|
|
106
|
+
? describe(s.value)
|
|
107
|
+
: `an element store with a runtime index cannot be proved to miss ${hex(hazard.address)}, so this counts as writing to`;
|
|
108
|
+
report(s, file, hazard, how);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const walk = (body, file) => {
|
|
113
|
+
for (const s of body) {
|
|
114
|
+
visit(s, file);
|
|
115
|
+
if (s.kind === 'if') { walk(s.then, file); if (s.else) walk(s.else, file); }
|
|
116
|
+
else if (s.kind === 'while' || s.kind === 'block') walk(s.body, file);
|
|
117
|
+
else if (s.kind === 'for') { if (s.init) visit(s.init, file); if (s.update) visit(s.update, file); walk(s.body, file); }
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
for (const fn of ir.functions) walk(fn.body, fileOf.get(fn) ?? '');
|
|
121
|
+
}
|