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