@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,278 @@
1
+ // Template layout: `text.print(cell, \`TICK ${ticks % 10:1} OPTION ${option}\`)`
2
+ // split into the pieces a person would have written by hand — one run of
3
+ // text and one number field at a time, each at the cell the pieces before
4
+ // it add up to.
5
+ //
6
+ // One implementation, two callers. The checker (packages/compiler/src/
7
+ // checker) runs it so a template's shape and field-width problems reach
8
+ // the editor — `analyze()` never lowers, and the editor only sees what
9
+ // `analyze()` reports. Lowering (packages/compiler/src/ir) runs it again
10
+ // to build the calls, and refuses on the same problems so a direct
11
+ // `lower()` can never silently drop a template. The linker deduplicates
12
+ // the two reports of one problem by code and span.
13
+ //
14
+ // What both need to know about names, without a binder: the types of the
15
+ // current module's own globals and functions (scanDeclaredTypes) and of
16
+ // the enclosing function's parameters. That is enough to size a field from
17
+ // its expression — `utinyint` is three digits, `usmallint` five — and
18
+ // honest about the rest: an imported name, a namespace member, or a call
19
+ // across modules has no visible type, and such a field needs its width
20
+ // written (`${x:3}`) rather than guessed.
21
+ import { Codes, diagnostic } from '../diagnostics/index.mjs';
22
+ import { NodeType } from '../ast/index.mjs';
23
+ import { resolveIntegerType } from '../types/index.mjs';
24
+
25
+ /** A string holds at most this many characters (one length byte). */
26
+ export const MAX_STRING_LENGTH = 255;
27
+
28
+ /** `void` / `bool` / `string` / an integer type, or null when `name` is none of them. */
29
+ export function resolveScalarType(name, { allowVoid = false, allowString = false } = {}) {
30
+ if (name === 'void') return allowVoid ? 'void' : null;
31
+ if (name === 'string') return allowString ? 'string' : null;
32
+ if (name === 'bool') return 'bool';
33
+ const resolved = resolveIntegerType(name);
34
+ return resolved ? resolved.canonicalName : null;
35
+ }
36
+
37
+ /** Elements an array may hold at most: an index has to fit the machine's address arithmetic. */
38
+ export const MAX_ARRAY_LENGTH = 65535;
39
+
40
+ /**
41
+ * `array<T, N>` resolved: the element type and the length, or a reason it
42
+ * cannot be. `N` is a literal or a const in `consts` (name to value) — a
43
+ * const is resolved by 8bitscript, so it can size a type as well as a
44
+ * literal can.
45
+ *
46
+ * @returns {{ type: string, length: number } | { error: string }}
47
+ */
48
+ export function resolveArrayType(annotation, consts = new Map()) {
49
+ const [element, size] = annotation?.typeArguments ?? [];
50
+ const type = element?.name && resolveScalarType(element.name);
51
+ if (!element || !type || element.typeArguments?.length) {
52
+ return { error: 'array<T, N> needs an integer or bool element type T' };
53
+ }
54
+ let length = null;
55
+ if (size?.type === NodeType.IntegerLiteral) length = size.value;
56
+ else if (size?.type === NodeType.TypeReference && !size.typeArguments?.length && consts.has(size.name)) {
57
+ length = consts.get(size.name);
58
+ }
59
+ if (length === null) {
60
+ return { error: 'array<T, N> needs a length N: an integer literal, or a const declared in this module' };
61
+ }
62
+ if (!Number.isInteger(length) || length < 1 || length > MAX_ARRAY_LENGTH) {
63
+ return { error: `an array length must be 1..${MAX_ARRAY_LENGTH}, not ${length}` };
64
+ }
65
+ return { type, length };
66
+ }
67
+
68
+ /** The type a `.length` folds to: the smallest unsigned type the number fits. */
69
+ export function typeForCount(n) {
70
+ return n <= 255 ? 'utinyint' : 'usmallint';
71
+ }
72
+
73
+ /** Decimal digits the widest value of an unsigned type needs: 255 -> 3, 65535 -> 5. */
74
+ export function digitsFor(typeName) {
75
+ const type = resolveIntegerType(typeName);
76
+ if (!type || type.signed || type.bits > 16) return null;
77
+ return String(type.max).length;
78
+ }
79
+
80
+ /** Widest of two integer types, for the result of arithmetic on them. */
81
+ function widerOf(a, b) {
82
+ const ta = resolveIntegerType(a);
83
+ const tb = resolveIntegerType(b);
84
+ if (!ta || !tb) return null;
85
+ return ta.bits >= tb.bits ? ta.canonicalName : tb.canonicalName;
86
+ }
87
+
88
+ const COMPARISON_OPERATORS = new Set(['==', '!=', '<', '>', '<=', '>=', '&&', '||']);
89
+
90
+ /**
91
+ * The declared types of a module's top-level globals, consts, and
92
+ * functions (return types), keyed by name — what a field expression's
93
+ * type can be read from without a binder. Arrays are listed apart, with
94
+ * their element type, length, and whether they are `const` (data in the
95
+ * program) or `let` (RAM): `a[i]` has the element type, `a.length` is a
96
+ * number, and `a` on its own is neither.
97
+ *
98
+ * @returns {{ globalTypes: Map<string,string>, functionTypes: Map<string,string>,
99
+ * arrayTypes: Map<string,{ type: string, length: number, constant: boolean }>,
100
+ * functionArity: Map<string,{ min: number, max: number }> }}
101
+ */
102
+ export function scanDeclaredTypes(ast) {
103
+ const globalTypes = new Map();
104
+ const functionTypes = new Map();
105
+ const arrayTypes = new Map();
106
+ // How many arguments each own function takes: `max` is its parameters,
107
+ // `min` those without a default.
108
+ const functionArity = new Map();
109
+ // Literal consts, in source order, so `array<u8, COUNT>` can be sized.
110
+ const consts = new Map();
111
+ for (const node of ast?.body ?? []) {
112
+ if (node.type === NodeType.VariableDeclaration && node.name && node.typeAnnotation) {
113
+ if (node.kind === 'const' && node.initializer?.type === NodeType.IntegerLiteral) {
114
+ consts.set(node.name.name, node.initializer.value);
115
+ }
116
+ if (node.typeAnnotation.name === 'array') {
117
+ const resolved = resolveArrayType(node.typeAnnotation, consts);
118
+ if (!resolved.error) arrayTypes.set(node.name.name, { ...resolved, constant: node.kind === 'const' });
119
+ continue;
120
+ }
121
+ if (node.typeAnnotation.name === 'string') {
122
+ // A string const or a string<N>: `.length` is a byte either way.
123
+ globalTypes.set(node.name.name, 'string');
124
+ continue;
125
+ }
126
+ const type = resolveScalarType(
127
+ node.typeAnnotation.name === 'volatile'
128
+ ? (node.typeAnnotation.typeArguments?.[0]?.name ?? '')
129
+ : node.typeAnnotation.name,
130
+ );
131
+ if (type) globalTypes.set(node.name.name, type);
132
+ }
133
+ if (node.type === NodeType.FunctionDeclaration && node.name) {
134
+ const type = resolveScalarType(node.returnType?.name ?? 'void', { allowVoid: true });
135
+ if (type) functionTypes.set(node.name.name, type);
136
+ const params = node.params ?? [];
137
+ functionArity.set(node.name.name, { max: params.length, min: params.filter((p) => !p.defaultValue).length });
138
+ }
139
+ }
140
+ return { globalTypes, functionTypes, arrayTypes, functionArity };
141
+ }
142
+
143
+ /**
144
+ * The parameter types of a function declaration, keyed by name — the
145
+ * innermost scope a field expression can name.
146
+ */
147
+ export function parameterTypes(fn) {
148
+ return new Map((fn.params ?? []).flatMap((p) => {
149
+ const type = p.typeAnnotation?.name && resolveScalarType(p.typeAnnotation.name, { allowString: true });
150
+ return type ? [[p.name.name, type]] : [];
151
+ }));
152
+ }
153
+
154
+ /**
155
+ * The static type of an expression, as far as a module can tell on its
156
+ * own: literals, the enclosing function's parameters, the module's own
157
+ * globals and functions, and arithmetic over those. Null for anything it
158
+ * cannot see.
159
+ *
160
+ * @param {{ paramTypes: Map, globalTypes: Map, functionTypes: Map, arrayTypes?: Map }} scope
161
+ */
162
+ export function inferType(node, scope) {
163
+ const arrayOf = (object) => (object?.type === NodeType.Identifier && !scope.paramTypes.has(object.name)
164
+ ? (scope.arrayTypes?.get(object.name) ?? null)
165
+ : null);
166
+ switch (node.type) {
167
+ case NodeType.IntegerLiteral:
168
+ if (node.value <= 255) return 'utinyint';
169
+ if (node.value <= 65535) return 'usmallint';
170
+ return 'uint';
171
+ case NodeType.BooleanLiteral:
172
+ return 'bool';
173
+ case NodeType.Identifier:
174
+ return scope.paramTypes.get(node.name) ?? scope.globalTypes.get(node.name) ?? null;
175
+ case NodeType.BinaryExpression: {
176
+ if (COMPARISON_OPERATORS.has(node.operator)) return 'bool';
177
+ return widerOf(inferType(node.left, scope), inferType(node.right, scope));
178
+ }
179
+ case NodeType.UnaryExpression:
180
+ return node.operator === '!' ? 'bool' : inferType(node.argument, scope);
181
+ case NodeType.IndexExpression:
182
+ return arrayOf(node.object)?.type ?? null;
183
+ case NodeType.MemberExpression: {
184
+ if (node.property?.name !== 'length') return null;
185
+ const array = arrayOf(node.object);
186
+ if (array) return typeForCount(array.length);
187
+ // `s.length` on a string — a parameter, const, or string<N> — is a byte.
188
+ return node.object?.type === NodeType.Identifier && inferType(node.object, scope) === 'string' ? 'utinyint' : null;
189
+ }
190
+ case NodeType.CallExpression:
191
+ return node.callee.type === NodeType.Identifier && !node.callee.compileTime
192
+ ? (scope.functionTypes.get(node.callee.name) ?? null)
193
+ : null;
194
+ default:
195
+ return null;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Is this call the one shape a template may appear in —
201
+ * `namespace.print(cell, \`...\`)`, exactly two arguments, the second a
202
+ * template? (Whether it is in statement position is the caller's to
203
+ * know: the checker tracks it while walking, lowering only reaches
204
+ * templateCall() from a statement.)
205
+ */
206
+ export function isTemplateCall(node) {
207
+ return node?.type === NodeType.CallExpression
208
+ && node.callee?.type === NodeType.MemberExpression
209
+ && node.callee.object?.type === NodeType.Identifier
210
+ && node.callee.property?.name === 'print'
211
+ && node.args.length === 2
212
+ && node.args[1]?.type === NodeType.TemplateLiteral;
213
+ }
214
+
215
+ /** The MISPLACED_TEMPLATE diagnostic for a template anywhere else. */
216
+ export function misplacedTemplate(template, file, namespace = null) {
217
+ return diagnostic(
218
+ Codes.MISPLACED_TEMPLATE,
219
+ namespace
220
+ ? `a template string is only valid as the second argument of ${namespace}.print(cell, ...)`
221
+ : "a template string is only valid as the second argument of a namespace's print(cell, ...)",
222
+ file, template.start, template.length,
223
+ );
224
+ }
225
+
226
+ /**
227
+ * Lay a template out. Returns the pieces in order, each with the cell
228
+ * offset it starts at, or the diagnostics that stopped it.
229
+ *
230
+ * @param {object} template TemplateLiteral node.
231
+ * @param {{ paramTypes: Map, globalTypes: Map, functionTypes: Map }} scope
232
+ * @param {string} file
233
+ * @param {string|null} source The file's text, so a diagnostic can quote a field back.
234
+ * @returns {{ pieces: ({ kind: 'text', offset: number, node: object }
235
+ * | { kind: 'field', offset: number, width: number, node: object })[], diagnostics: object[] }}
236
+ */
237
+ export function layoutTemplate(template, scope, file, source = null) {
238
+ const pieces = [];
239
+ const diagnostics = [];
240
+ let offset = 0;
241
+ for (const part of template.parts) {
242
+ if (part.type === NodeType.TemplateText) {
243
+ pieces.push({ kind: 'text', offset, node: part });
244
+ offset += part.value.length;
245
+ continue;
246
+ }
247
+ let width;
248
+ if (part.width) {
249
+ width = part.width.value;
250
+ if (width < 1 || width > MAX_STRING_LENGTH) {
251
+ diagnostics.push(diagnostic(
252
+ Codes.UNPRINTABLE_FIELD, `a field width must be 1..${MAX_STRING_LENGTH}, not ${width}`,
253
+ file, part.width.start, part.width.length,
254
+ ));
255
+ continue;
256
+ }
257
+ } else {
258
+ const type = inferType(part.expression, scope);
259
+ width = type ? digitsFor(type) : null;
260
+ if (width === null) {
261
+ const quoted = source
262
+ ? source.slice(part.expression.start, part.expression.start + part.expression.length)
263
+ : '...';
264
+ diagnostics.push(diagnostic(
265
+ Codes.UNPRINTABLE_FIELD,
266
+ type
267
+ ? `a ${type} cannot be a number field: fields show unsigned values up to 16 bits (utinyint, usmallint)`
268
+ : `this field's width cannot be taken from its expression here; say it: \${${quoted}:3}`,
269
+ file, part.start, part.length,
270
+ ));
271
+ continue;
272
+ }
273
+ }
274
+ pieces.push({ kind: 'field', offset, width, node: part });
275
+ offset += width;
276
+ }
277
+ return { pieces, diagnostics };
278
+ }
@@ -0,0 +1,116 @@
1
+ // The primitive integer type registry.
2
+ //
3
+ // One authoritative description of `utinyint`/`u8` and its seven siblings, so
4
+ // nothing downstream keeps its own copy. Before this module existed, the
5
+ // lexer's type-name set, the checker's range table, and each backend's width
6
+ // map were four hand-written lists that had to be kept in lockstep by hand.
7
+ // Now they all read this one.
8
+ //
9
+ // Naming is MySQL-inspired on purpose: `tinyint`/`smallint`/`mediumint`/`int`
10
+ // (and their `u`-prefixed unsigned counterparts) read as storage sizes to
11
+ // someone who has never seen `i8`/`u32`-style abbreviations, which is exactly
12
+ // the audience this is for. `i8`, `u8`, and so on remain as low-level aliases
13
+ // — the systems-programming spelling stays available, it just is not what the
14
+ // language leads with. `bigint`/`ubigint` are reserved for a future 64-bit
15
+ // type and deliberately not recognised anywhere yet.
16
+ //
17
+ // The canonical name IS the internal id: the IR and both backends key their
18
+ // own data by `canonicalName` (`utinyint`, not `u8`). `utinyint` and `u8` are
19
+ // never two types that happen to agree; they resolve to the same descriptor
20
+ // before anything downstream ever sees them.
21
+
22
+ /**
23
+ * @typedef {object} IntegerType
24
+ * @property {string} canonicalName Preferred, human-readable spelling (e.g. "utinyint") —
25
+ * also the id every compiler stage stores internally.
26
+ * @property {string} legacyAlias The short systems-programming spelling (e.g. "u8").
27
+ * @property {string[]} aliases Every spelling other than the canonical one.
28
+ * @property {boolean} signed
29
+ * @property {number} bits
30
+ * @property {number} bytes
31
+ * @property {number} min Inclusive.
32
+ * @property {number} max Inclusive.
33
+ * @property {string} summary One sentence, e.g. "Unsigned 1-byte integer."
34
+ */
35
+
36
+ /** Signed base name by bit width; the unsigned spelling is always `u` + this. */
37
+ const WIDTH_NAME = { 8: 'tinyint', 16: 'smallint', 24: 'mediumint', 32: 'int' };
38
+
39
+ function makeIntegerType(bits, signed) {
40
+ const range = 2 ** bits;
41
+ const bytes = bits / 8;
42
+ const base = WIDTH_NAME[bits];
43
+ const canonicalName = signed ? base : `u${base}`;
44
+ const legacyAlias = `${signed ? 'i' : 'u'}${bits}`;
45
+ return {
46
+ canonicalName,
47
+ legacyAlias,
48
+ aliases: [legacyAlias],
49
+ signed,
50
+ bits,
51
+ bytes,
52
+ min: signed ? -(range / 2) : 0,
53
+ max: signed ? range / 2 - 1 : range - 1,
54
+ summary: `${signed ? 'Signed' : 'Unsigned'} ${bytes}-byte integer.`,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Every canonical primitive integer type, narrowest first and signed before
60
+ * unsigned within a width — the order completion offers them in:
61
+ * tinyint, utinyint, smallint, usmallint, mediumint, umediumint, int, uint.
62
+ */
63
+ export const PRIMITIVE_INTEGER_TYPES = [8, 16, 24, 32].flatMap(
64
+ (bits) => [makeIntegerType(bits, true), makeIntegerType(bits, false)],
65
+ );
66
+
67
+ const BY_SPELLING = new Map();
68
+ for (const type of PRIMITIVE_INTEGER_TYPES) {
69
+ BY_SPELLING.set(type.canonicalName, type);
70
+ for (const alias of type.aliases) BY_SPELLING.set(alias, type);
71
+ }
72
+
73
+ /**
74
+ * Bytes one value of a type takes in the target's memory: what an array's
75
+ * or a variable's RAM comes to. The 24-bit types widen to 4 bytes, since
76
+ * neither backend has a 3-byte integer (see their NATIVE_WIDTH tables);
77
+ * `bool` is a byte; a `string` is a pointer, 2 bytes on a 6502 (the web's
78
+ * 4-byte `usize` is not RAM a program counts).
79
+ *
80
+ * @param {string} name A canonical type name or alias, `bool`, or `string`.
81
+ * @returns {number}
82
+ */
83
+ export function storageBytes(name) {
84
+ if (name === 'bool') return 1;
85
+ if (name === 'string') return 2;
86
+ const type = BY_SPELLING.get(name);
87
+ if (!type) return 0;
88
+ return type.bits === 24 ? 4 : type.bits / 8;
89
+ }
90
+
91
+ /** Every spelling a primitive integer type can be written with, canonical or alias. */
92
+ export const INTEGER_TYPE_NAMES = [...BY_SPELLING.keys()];
93
+
94
+ /**
95
+ * Resolve any spelling — canonical (`utinyint`) or legacy alias (`u8`) — to
96
+ * its descriptor. Two different spellings of the same type return the same
97
+ * object, which is what "not a separate type internally" means in practice.
98
+ *
99
+ * @param {string} name
100
+ * @returns {IntegerType | undefined}
101
+ */
102
+ export function resolveIntegerType(name) {
103
+ return BY_SPELLING.get(name);
104
+ }
105
+
106
+ /**
107
+ * Inclusive `[min, max]` ranges, keyed by every recognised spelling. Kept for
108
+ * callers that only ever needed bounds — the checker's original shape, before
109
+ * it had a reason to ask for anything else.
110
+ */
111
+ export const INTEGER_RANGES = Object.fromEntries(
112
+ INTEGER_TYPE_NAMES.map((name) => {
113
+ const type = resolveIntegerType(name);
114
+ return [name, [type.min, type.max]];
115
+ }),
116
+ );