@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 8BitScript contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @8bitscript/compiler — the public surface other packages consume.
|
|
2
|
+
//
|
|
3
|
+
// The dependency direction is one-way and load-bearing: the CLI and the
|
|
4
|
+
// language server both depend on this package, and this package knows nothing
|
|
5
|
+
// about either of them, or about any editor. Analysis lives here so that
|
|
6
|
+
// `8bs check`, the editor, and CI all report the same errors from the same
|
|
7
|
+
// implementation.
|
|
8
|
+
import { tokenize } from './src/lexer/index.mjs';
|
|
9
|
+
import { parse } from './src/parser/index.mjs';
|
|
10
|
+
import { check } from './src/checker/index.mjs';
|
|
11
|
+
import { foldCompileTime } from './src/fold/index.mjs';
|
|
12
|
+
import { lower } from './src/ir/index.mjs';
|
|
13
|
+
import { resolveImports } from './src/resolver/index.mjs';
|
|
14
|
+
|
|
15
|
+
export { tokenize, TokenKind, KEYWORDS, TYPE_NAMES } from './src/lexer/index.mjs';
|
|
16
|
+
export { parse } from './src/parser/index.mjs';
|
|
17
|
+
export { NodeType, walk } from './src/ast/index.mjs';
|
|
18
|
+
export { check } from './src/checker/index.mjs';
|
|
19
|
+
export { foldCompileTime, DURATION_CLOCKS, DURATION_UNITS, SYSTEMS } from './src/fold/index.mjs';
|
|
20
|
+
export {
|
|
21
|
+
FACTS, PROGRAM_FACTS, factConstName, factPlaceholder, factProblems,
|
|
22
|
+
requiresProblems, unmetRequirements,
|
|
23
|
+
} from './src/fold/facts.mjs';
|
|
24
|
+
export { lower, entryOf } from './src/ir/index.mjs';
|
|
25
|
+
export { link, memoryOf } from './src/linker/index.mjs';
|
|
26
|
+
export {
|
|
27
|
+
MACHINES, findImports, isVariantPath, resolveImports, resolveSpecifier, variantOf, tagsOf,
|
|
28
|
+
} from './src/resolver/index.mjs';
|
|
29
|
+
export { Codes, diagnostic, positionAt } from './src/diagnostics/index.mjs';
|
|
30
|
+
export {
|
|
31
|
+
PRIMITIVE_INTEGER_TYPES,
|
|
32
|
+
INTEGER_TYPE_NAMES,
|
|
33
|
+
INTEGER_RANGES,
|
|
34
|
+
resolveIntegerType,
|
|
35
|
+
storageBytes,
|
|
36
|
+
} from './src/types/index.mjs';
|
|
37
|
+
export { getHoverInfo, getCompletions } from './src/intellisense/index.mjs';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Analyse one source file and return every diagnostic it produces.
|
|
41
|
+
*
|
|
42
|
+
* This is the single entry point for "what is wrong with this file". It never
|
|
43
|
+
* throws: source is assumed to be mid-edit, and a dependency on disk is assumed
|
|
44
|
+
* to be possibly broken.
|
|
45
|
+
*
|
|
46
|
+
* Import resolution is opt-in because it is the only part that touches the
|
|
47
|
+
* filesystem, and it needs a real absolute path to resolve against. Callers
|
|
48
|
+
* working with an unsaved buffer leave it off.
|
|
49
|
+
*
|
|
50
|
+
* Lowering runs too, and its diagnostics are part of the answer: "this
|
|
51
|
+
* construct is not compilable yet" is exactly what someone needs to see
|
|
52
|
+
* while typing, not at the end of a build. It is a pure AST-to-IR pass with
|
|
53
|
+
* no filesystem in it, so the only thing it cannot report on its own is
|
|
54
|
+
* anything that needs the other modules — an unresolved name, a namespace
|
|
55
|
+
* member that doesn't exist — which the linker reports at build time.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} text
|
|
58
|
+
* @param {string} file
|
|
59
|
+
* @param {{ resolveImports?: boolean, frameRate?: number, machine?: string, facts?: object }} [options]
|
|
60
|
+
* `machine` is the target when one is known; without it `#system()` and
|
|
61
|
+
* `#fact(...)` fold to placeholders and are valid-but-target-dependent,
|
|
62
|
+
* as a `.<machine>.8bs` import is. `facts` is the machine's hardware
|
|
63
|
+
* fact sheet, wanted whenever `machine` is given and a fact is read. `frameRate` (default 60) is the project's logical frame rate — see
|
|
64
|
+
* 8bs.config.ts — that every `#frames(...)` call folds against, mirroring
|
|
65
|
+
* link()'s option of the same name so `8bs check`/the editor and a real
|
|
66
|
+
* build agree on what a duration means.
|
|
67
|
+
* @returns {object[]} diagnostics, in source order
|
|
68
|
+
*/
|
|
69
|
+
export function analyze(text, file = '<unknown>', options = {}) {
|
|
70
|
+
const { tokens, diagnostics: lexical } = tokenize(text, file);
|
|
71
|
+
const { ast, diagnostics: syntax } = parse(tokens, text, file);
|
|
72
|
+
|
|
73
|
+
// Folding runs before check(), same ordering as the linker: a
|
|
74
|
+
// #frames(...) call needs to already be a plain IntegerLiteral by the
|
|
75
|
+
// time the width-fit rule walks the tree.
|
|
76
|
+
const folding = foldCompileTime(ast, file, { frameRate: options.frameRate, machine: options.machine, facts: options.facts });
|
|
77
|
+
const all = [...lexical, ...syntax, ...folding, ...check(ast, file, text)];
|
|
78
|
+
// A few rules — the template layout above all — are deliberately run by
|
|
79
|
+
// both check() and lower(), so that `check()` alone is a complete
|
|
80
|
+
// AST-level answer and `lower()` alone can never drop a construct
|
|
81
|
+
// silently. One problem is still reported once (the linker dedupes the
|
|
82
|
+
// same way for the same reason).
|
|
83
|
+
const seen = new Set(all.map((d) => `${d.code}@${d.start}+${d.length}`));
|
|
84
|
+
for (const d of lower(ast, file, text).diagnostics) {
|
|
85
|
+
if (!seen.has(`${d.code}@${d.start}+${d.length}`)) all.push(d);
|
|
86
|
+
}
|
|
87
|
+
// Import resolution stays on tokens rather than the AST, deliberately: a
|
|
88
|
+
// syntax error on line 30 should not stop line 1's import from being checked.
|
|
89
|
+
if (options.resolveImports) all.push(...resolveImports(tokens, file));
|
|
90
|
+
return all.sort((a, b) => a.start - b.start);
|
|
91
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@8bitscript/compiler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Internal: the 8BitScript front end — lexer, checker, and diagnostics.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=26"
|
|
9
|
+
},
|
|
10
|
+
"main": "./index.mjs",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./index.mjs",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"index.mjs",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@8bitscript/text": "0.1.0",
|
|
21
|
+
"@8bitscript/vic20": "0.1.0"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// AST node shapes.
|
|
2
|
+
//
|
|
3
|
+
// Every node carries `start` and `length`, the same span a diagnostic uses.
|
|
4
|
+
// That is not incidental: positions recorded at parse time are what let a
|
|
5
|
+
// checker error land on the exact characters that caused it, in the terminal
|
|
6
|
+
// and under the cursor, without anything downstream re-deriving a location.
|
|
7
|
+
//
|
|
8
|
+
// Nodes are plain objects. There is no class hierarchy and no visitor
|
|
9
|
+
// framework: a `type` string and a switch is enough at this size, and it keeps
|
|
10
|
+
// the tree trivially serialisable for tests.
|
|
11
|
+
|
|
12
|
+
export const NodeType = {
|
|
13
|
+
Program: 'Program',
|
|
14
|
+
|
|
15
|
+
ImportDeclaration: 'ImportDeclaration',
|
|
16
|
+
VariableDeclaration: 'VariableDeclaration',
|
|
17
|
+
FunctionDeclaration: 'FunctionDeclaration',
|
|
18
|
+
NamespaceDeclaration: 'NamespaceDeclaration',
|
|
19
|
+
Parameter: 'Parameter',
|
|
20
|
+
|
|
21
|
+
BlockStatement: 'BlockStatement',
|
|
22
|
+
IfStatement: 'IfStatement',
|
|
23
|
+
WhileStatement: 'WhileStatement',
|
|
24
|
+
ForStatement: 'ForStatement',
|
|
25
|
+
ReturnStatement: 'ReturnStatement',
|
|
26
|
+
BreakStatement: 'BreakStatement',
|
|
27
|
+
ContinueStatement: 'ContinueStatement',
|
|
28
|
+
ExpressionStatement: 'ExpressionStatement',
|
|
29
|
+
AsmBlock: 'AsmBlock',
|
|
30
|
+
|
|
31
|
+
Identifier: 'Identifier',
|
|
32
|
+
IntegerLiteral: 'IntegerLiteral',
|
|
33
|
+
// A decimal fraction (`0.5`) — legal only as the first argument to the
|
|
34
|
+
// `#frames(...)` compile-time duration builtin (packages/compiler/src/
|
|
35
|
+
// fold), which consumes and removes every valid one before anything else
|
|
36
|
+
// sees the tree; one surviving to `check()` means it was used somewhere
|
|
37
|
+
// else, which is always a diagnostic (the language has no other float
|
|
38
|
+
// semantics).
|
|
39
|
+
DecimalLiteral: 'DecimalLiteral',
|
|
40
|
+
BooleanLiteral: 'BooleanLiteral',
|
|
41
|
+
StringLiteral: 'StringLiteral',
|
|
42
|
+
// A backtick string with `${...}` fields: `TICK ${ticks % 10:1}`. `parts`
|
|
43
|
+
// is an ordered array of TemplateText and TemplateField nodes — real
|
|
44
|
+
// nodes, so walk() reaches every field expression and the fold pass and
|
|
45
|
+
// checker see them like any other expression.
|
|
46
|
+
TemplateLiteral: 'TemplateLiteral',
|
|
47
|
+
TemplateText: 'TemplateText',
|
|
48
|
+
// `${expression}` or `${expression:width}` — `width` an IntegerLiteral or
|
|
49
|
+
// null when the field left it to the expression's type.
|
|
50
|
+
TemplateField: 'TemplateField',
|
|
51
|
+
// `[1, 2, 3]`: the initialiser of an `array<T, N>`. Elements are
|
|
52
|
+
// expressions in the tree (so hover and the fold pass reach them), but
|
|
53
|
+
// lowering only accepts compile-time values there — the data is laid
|
|
54
|
+
// out before the program runs.
|
|
55
|
+
ArrayLiteral: 'ArrayLiteral',
|
|
56
|
+
|
|
57
|
+
AssignmentExpression: 'AssignmentExpression',
|
|
58
|
+
BinaryExpression: 'BinaryExpression',
|
|
59
|
+
UnaryExpression: 'UnaryExpression',
|
|
60
|
+
UpdateExpression: 'UpdateExpression',
|
|
61
|
+
CallExpression: 'CallExpression',
|
|
62
|
+
MemberExpression: 'MemberExpression',
|
|
63
|
+
IndexExpression: 'IndexExpression',
|
|
64
|
+
|
|
65
|
+
TypeReference: 'TypeReference',
|
|
66
|
+
Decorator: 'Decorator',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Build a node with its span. */
|
|
70
|
+
export function node(type, start, end, props = {}) {
|
|
71
|
+
return { type, start, length: end - start, ...props };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Walk every node in a tree, parents before children.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} root
|
|
78
|
+
* @param {(node: object, parent: object|null) => void} visit
|
|
79
|
+
*/
|
|
80
|
+
export function walk(root, visit, parent = null) {
|
|
81
|
+
if (!root || typeof root.type !== 'string') return;
|
|
82
|
+
visit(root, parent);
|
|
83
|
+
for (const value of Object.values(root)) {
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
for (const item of value) walk(item, visit, root);
|
|
86
|
+
} else if (value && typeof value === 'object' && typeof value.type === 'string') {
|
|
87
|
+
walk(value, visit, root);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// The checker.
|
|
2
|
+
//
|
|
3
|
+
// Four rules so far: an integer literal has to fit the type it is assigned
|
|
4
|
+
// to; the runtime builtin's name — `waitFrame` (packages/compiler/src/ir)
|
|
5
|
+
// — is reserved; a string that becomes program data (a literal, or the
|
|
6
|
+
// text of a template) holds only the portable character set, at most 255
|
|
7
|
+
// of them; a template string sits where the compiler can lay it out
|
|
8
|
+
// (`namespace.print(cell, \`...\`)` as a statement) with every field
|
|
9
|
+
// sized — the same layout lowering performs (packages/compiler/src/
|
|
10
|
+
// templates), run here so its diagnostics reach the editor; and a
|
|
11
|
+
// top-level `const` — a compile-time constant with no storage — is never
|
|
12
|
+
// assigned to.
|
|
13
|
+
//
|
|
14
|
+
// This used to pattern-match a fixed token shape because there was no tree to
|
|
15
|
+
// walk. It now runs on the AST, which is what the parser bought: the rule finds
|
|
16
|
+
// declarations anywhere — inside a function body, inside a `for` initialiser,
|
|
17
|
+
// on an exported declaration — rather than only at the one shape a token scan
|
|
18
|
+
// could recognise. The diagnostic code, the message, and the span are
|
|
19
|
+
// unchanged, because a rule moving to a better home should not look different
|
|
20
|
+
// to the person reading the error.
|
|
21
|
+
//
|
|
22
|
+
// Still deliberately narrow: the initialiser must be a literal, optionally
|
|
23
|
+
// negated. `let x: u8 = 200 + 100` is not folded, because constant folding
|
|
24
|
+
// belongs after a binder that knows what names mean.
|
|
25
|
+
import { Codes, diagnostic } from '../diagnostics/index.mjs';
|
|
26
|
+
import { NodeType, walk } from '../ast/index.mjs';
|
|
27
|
+
import { resolveIntegerType } from '../types/index.mjs';
|
|
28
|
+
import {
|
|
29
|
+
scanDeclaredTypes, parameterTypes, layoutTemplate, isTemplateCall, misplacedTemplate, resolveScalarType,
|
|
30
|
+
} from '../templates/index.mjs';
|
|
31
|
+
|
|
32
|
+
// The one runtime builtin that is a bare name: `waitFrame()` lowers to its
|
|
33
|
+
// own IR kind that every backend emits in its own way
|
|
34
|
+
// (packages/compiler/src/ir). It is closer to a keyword than to an ordinary
|
|
35
|
+
// name, but implemented as a reserved identifier rather than a grammar
|
|
36
|
+
// keyword — a call's *shape* is an ordinary call, and keywords are not
|
|
37
|
+
// valid callees. Reserving the name here is what keeps a user's own
|
|
38
|
+
// `waitFrame` from being silently reinterpreted as the builtin instead of
|
|
39
|
+
// getting a clear diagnostic.
|
|
40
|
+
//
|
|
41
|
+
// Compile-time functions (`#frames(...)`, packages/compiler/src/fold) need
|
|
42
|
+
// no reservation: their `#` spelling is its own token, so `let frames`
|
|
43
|
+
// never collides. Nor does the unit word (`seconds` in
|
|
44
|
+
// `#frames(0.5, seconds)`): that argument slot can never hold a variable,
|
|
45
|
+
// so the fold recognises the word by spelling in place and
|
|
46
|
+
// `let seconds: uint` anywhere else stays an ordinary declaration.
|
|
47
|
+
const RESERVED_BUILTIN_NAMES = new Map([
|
|
48
|
+
['waitFrame', 'the built-in frame wait, waitFrame()'],
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// What every target's character set can show: the NES ships its own font
|
|
52
|
+
// (packages/nes/native/6502/font.s) with exactly these glyphs, and the
|
|
53
|
+
// Commodore machines are switched to their upper-case set by putChar.
|
|
54
|
+
// Anything else is a diagnostic here — not silently blanked on one machine
|
|
55
|
+
// and shown on another. Import specifiers are StringLiteral nodes too and
|
|
56
|
+
// are skipped: a module path is not screen text.
|
|
57
|
+
const PORTABLE_CHARACTERS = /^[ 0-9A-Z!,\-.:?]*$/;
|
|
58
|
+
const MAX_STRING_LENGTH = 255;
|
|
59
|
+
|
|
60
|
+
function checkScreenText(n, file, diagnostics) {
|
|
61
|
+
if (n.unterminated) return; // the lexer reported it; whatever follows the quote is not the text
|
|
62
|
+
const value = n.value ?? '';
|
|
63
|
+
if (!PORTABLE_CHARACTERS.test(value)) {
|
|
64
|
+
const bad = [...value].find((ch) => !PORTABLE_CHARACTERS.test(ch));
|
|
65
|
+
const hint = /[a-z]/.test(bad) ? ' — upper case only' : '';
|
|
66
|
+
diagnostics.push(diagnostic(
|
|
67
|
+
Codes.UNPORTABLE_CHARACTER,
|
|
68
|
+
`'${bad}' is not in the portable character set (space, 0-9, A-Z, and ! , - . : ?)${hint}`,
|
|
69
|
+
file, n.start, n.length,
|
|
70
|
+
));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (value.length > MAX_STRING_LENGTH) {
|
|
74
|
+
diagnostics.push(diagnostic(
|
|
75
|
+
Codes.STRING_TOO_LONG,
|
|
76
|
+
`a string holds at most ${MAX_STRING_LENGTH} characters; this one is ${value.length}`,
|
|
77
|
+
file, n.start, n.length,
|
|
78
|
+
));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function reservedNameDiagnostic(nameNode, file) {
|
|
83
|
+
return diagnostic(
|
|
84
|
+
Codes.RESERVED_BUILTIN_NAME,
|
|
85
|
+
`'${nameNode.name}' is reserved for ${RESERVED_BUILTIN_NAMES.get(nameNode.name)}`,
|
|
86
|
+
file, nameNode.start, nameNode.length,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The constant value of an initialiser, or null when it is not a plain literal.
|
|
92
|
+
*
|
|
93
|
+
* @returns {{ value: number, node: object } | null}
|
|
94
|
+
*/
|
|
95
|
+
function literalValue(expression) {
|
|
96
|
+
if (!expression) return null;
|
|
97
|
+
if (expression.type === NodeType.IntegerLiteral) {
|
|
98
|
+
return { value: expression.value, node: expression };
|
|
99
|
+
}
|
|
100
|
+
if (
|
|
101
|
+
expression.type === NodeType.UnaryExpression &&
|
|
102
|
+
(expression.operator === '-' || expression.operator === '+') &&
|
|
103
|
+
expression.argument?.type === NodeType.IntegerLiteral
|
|
104
|
+
) {
|
|
105
|
+
const magnitude = expression.argument.value;
|
|
106
|
+
return {
|
|
107
|
+
value: expression.operator === '-' ? -magnitude : magnitude,
|
|
108
|
+
node: expression,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A const is UPPER_SNAKE; a variable starts with a lower-case letter. The
|
|
115
|
+
// spelling is the compile-time signal: `LIMIT` is resolved by 8bitscript,
|
|
116
|
+
// `limit` is storage on the machine. Namespace members count — `const`
|
|
117
|
+
// inside `namespace BorderColor` is `BLUE` — and so does a local.
|
|
118
|
+
const CONST_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
119
|
+
const VARIABLE_NAME = /^[a-z_]/;
|
|
120
|
+
|
|
121
|
+
/** `OptionCount` -> `OPTION_COUNT`, `borders` -> `BORDERS`: the spelling a const should have. */
|
|
122
|
+
function constSpelling(name) {
|
|
123
|
+
return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function checkNameCase(n, file, diagnostics) {
|
|
127
|
+
const name = n.name.name;
|
|
128
|
+
if (n.kind === 'const' && !CONST_NAME.test(name)) {
|
|
129
|
+
diagnostics.push(diagnostic(
|
|
130
|
+
Codes.NAME_CASE,
|
|
131
|
+
`a const is written in upper case, so a reader knows it is resolved by 8bitscript: ${constSpelling(name)}`,
|
|
132
|
+
file, n.name.start, n.name.length,
|
|
133
|
+
));
|
|
134
|
+
} else if (n.kind === 'let' && !VARIABLE_NAME.test(name)) {
|
|
135
|
+
diagnostics.push(diagnostic(
|
|
136
|
+
Codes.NAME_CASE,
|
|
137
|
+
`a variable starts with a lower-case letter; an upper-case name is a const`,
|
|
138
|
+
file, n.name.start, n.name.length,
|
|
139
|
+
));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The rules that need a function's scope. Templates: each one must be the
|
|
145
|
+
* second argument of a `namespace.print` call in statement position, and
|
|
146
|
+
* each of its fields must have a width the layout can determine — a field
|
|
147
|
+
* expression is typed in the scope of the parameters around it, and a
|
|
148
|
+
* template outside any function (a global initialiser) has nowhere to be
|
|
149
|
+
* laid out at all. Consts: this module's top-level `const`s are never
|
|
150
|
+
* assigned to or `++`/`--`ed, unless a parameter of the same name shadows
|
|
151
|
+
* one, which is ordinary lexical scoping.
|
|
152
|
+
*/
|
|
153
|
+
function checkFunctions(ast, file, source, diagnostics) {
|
|
154
|
+
const declared = scanDeclaredTypes(ast);
|
|
155
|
+
const consts = new Set(ast.body
|
|
156
|
+
.filter((n) => n.type === NodeType.VariableDeclaration && n.kind === 'const' && n.name)
|
|
157
|
+
.map((n) => n.name.name));
|
|
158
|
+
const constArrays = new Set([...declared.arrayTypes].filter(([, a]) => a.constant).map(([name]) => name));
|
|
159
|
+
// `HALF = 2` on a const, or `Table[0] = 2` on a const array (whose
|
|
160
|
+
// elements are data in the program). Either way the name is the span.
|
|
161
|
+
const assignedConst = (target, params) => {
|
|
162
|
+
const name = target?.type === NodeType.IndexExpression ? target.object : target;
|
|
163
|
+
if (name?.type !== NodeType.Identifier || params.has(name.name)) return null;
|
|
164
|
+
if (target.type === NodeType.IndexExpression) return constArrays.has(name.name) ? name : null;
|
|
165
|
+
return consts.has(name.name) ? name : null;
|
|
166
|
+
};
|
|
167
|
+
const visitFunction = (fn) => {
|
|
168
|
+
if (!fn.body) return; // a function whose body is still being typed
|
|
169
|
+
// Locals count with parameters here — function-scoped, an approximation
|
|
170
|
+
// of the block scoping lowering applies, enough to know a name is not
|
|
171
|
+
// the const or array it shadows and what type a field of it has.
|
|
172
|
+
const paramTypes = parameterTypes(fn);
|
|
173
|
+
walk(fn.body, (n) => {
|
|
174
|
+
if (n.type !== NodeType.VariableDeclaration || !n.name || n.kind !== 'let') return;
|
|
175
|
+
const type = n.typeAnnotation?.name && resolveScalarType(n.typeAnnotation.name);
|
|
176
|
+
if (type && !n.typeAnnotation.typeArguments?.length) paramTypes.set(n.name.name, type);
|
|
177
|
+
});
|
|
178
|
+
const scope = { ...declared, paramTypes };
|
|
179
|
+
// Parents are visited before children, so a template reached through
|
|
180
|
+
// its statement is claimed here before walk() descends to it.
|
|
181
|
+
const placed = new Set();
|
|
182
|
+
walk(fn.body, (n) => {
|
|
183
|
+
if (n.type === NodeType.ExpressionStatement && isTemplateCall(n.expression)) {
|
|
184
|
+
const template = n.expression.args[1];
|
|
185
|
+
placed.add(template);
|
|
186
|
+
diagnostics.push(...layoutTemplate(template, scope, file, source).diagnostics);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (n.type === NodeType.TemplateLiteral && !placed.has(n)) {
|
|
190
|
+
diagnostics.push(misplacedTemplate(n, file));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const target = n.type === NodeType.AssignmentExpression ? n.left
|
|
194
|
+
: n.type === NodeType.UpdateExpression ? n.argument : null;
|
|
195
|
+
const name = target && assignedConst(target, paramTypes);
|
|
196
|
+
if (name) {
|
|
197
|
+
diagnostics.push(diagnostic(
|
|
198
|
+
Codes.ASSIGN_TO_CONST,
|
|
199
|
+
target.type === NodeType.IndexExpression
|
|
200
|
+
? `'${name.name}' is a const array — data in the program, not RAM — and cannot be assigned to`
|
|
201
|
+
: `'${name.name}' is a const — a compile-time value with no storage — and cannot be assigned`,
|
|
202
|
+
file, name.start, name.length,
|
|
203
|
+
));
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
for (const node of ast.body) {
|
|
208
|
+
if (node.type === NodeType.FunctionDeclaration) visitFunction(node);
|
|
209
|
+
if (node.type === NodeType.NamespaceDeclaration) {
|
|
210
|
+
for (const member of node.members ?? []) {
|
|
211
|
+
if (member?.type === NodeType.FunctionDeclaration) visitFunction(member);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (node.type === NodeType.VariableDeclaration) {
|
|
215
|
+
walk(node.initializer, (n) => {
|
|
216
|
+
if (n.type === NodeType.TemplateLiteral) diagnostics.push(misplacedTemplate(n, file));
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {object} ast Program node from the parser.
|
|
224
|
+
* @param {string} file
|
|
225
|
+
* @param {string|null} [source] The file's text, so a diagnostic can quote code back.
|
|
226
|
+
* @returns {object[]} diagnostics
|
|
227
|
+
*/
|
|
228
|
+
export function check(ast, file = '<unknown>', source = null) {
|
|
229
|
+
const diagnostics = [];
|
|
230
|
+
if (!ast) return diagnostics;
|
|
231
|
+
|
|
232
|
+
checkFunctions(ast, file, source, diagnostics);
|
|
233
|
+
|
|
234
|
+
walk(ast, (n, parent) => {
|
|
235
|
+
if (
|
|
236
|
+
(n.type === NodeType.StringLiteral && parent?.type !== NodeType.ImportDeclaration)
|
|
237
|
+
|| n.type === NodeType.TemplateText
|
|
238
|
+
) {
|
|
239
|
+
checkScreenText(n, file, diagnostics);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (n.type === NodeType.ImportDeclaration) {
|
|
244
|
+
for (const specifier of n.specifiers) {
|
|
245
|
+
if (RESERVED_BUILTIN_NAMES.has(specifier.name)) {
|
|
246
|
+
diagnostics.push(reservedNameDiagnostic(specifier, file));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (n.type === NodeType.FunctionDeclaration || n.type === NodeType.Parameter) {
|
|
253
|
+
if (n.name && RESERVED_BUILTIN_NAMES.has(n.name.name)) {
|
|
254
|
+
diagnostics.push(reservedNameDiagnostic(n.name, file));
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (n.type !== NodeType.VariableDeclaration) return;
|
|
260
|
+
|
|
261
|
+
if (n.name && RESERVED_BUILTIN_NAMES.has(n.name.name)) {
|
|
262
|
+
diagnostics.push(reservedNameDiagnostic(n.name, file));
|
|
263
|
+
}
|
|
264
|
+
if (n.name) checkNameCase(n, file, diagnostics);
|
|
265
|
+
|
|
266
|
+
const typeName = n.typeAnnotation?.name;
|
|
267
|
+
// A type constructor such as ptr<u8> has type arguments and is not itself
|
|
268
|
+
// an integer, so it never resolves against the registry below.
|
|
269
|
+
if (n.typeAnnotation?.typeArguments?.length) return;
|
|
270
|
+
const resolved = typeName && resolveIntegerType(typeName);
|
|
271
|
+
if (!resolved) return;
|
|
272
|
+
|
|
273
|
+
const literal = literalValue(n.initializer);
|
|
274
|
+
if (!literal) return;
|
|
275
|
+
|
|
276
|
+
// The message keeps whatever the programmer actually wrote (`u8` or
|
|
277
|
+
// `utinyint`) even though both resolve to the same type: a diagnostic
|
|
278
|
+
// should point at the reader's own words, not a canonicalised rewrite.
|
|
279
|
+
const { min, max } = resolved;
|
|
280
|
+
if (literal.value < min || literal.value > max) {
|
|
281
|
+
diagnostics.push(
|
|
282
|
+
diagnostic(
|
|
283
|
+
Codes.VALUE_OUT_OF_RANGE,
|
|
284
|
+
`${literal.value} does not fit in ${typeName} (${min}..${max})`,
|
|
285
|
+
file,
|
|
286
|
+
literal.node.start,
|
|
287
|
+
literal.node.length,
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return diagnostics;
|
|
294
|
+
}
|