@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,179 @@
1
+ // The diagnostic record every part of the toolchain speaks.
2
+ //
3
+ // One shape, produced in one place, consumed by both `8bs check` and the
4
+ // language server. That is the whole point: the error you see in the editor and
5
+ // the error CI fails on are the same object, not two implementations that drift.
6
+
7
+ /**
8
+ * @typedef {object} Diagnostic
9
+ * @property {string} code Stable identifier, e.g. "8BS1021".
10
+ * @property {string} message Human-readable text, no trailing period.
11
+ * @property {string} file Path or URI the diagnostic belongs to.
12
+ * @property {number} start Zero-based offset into the source text.
13
+ * @property {number} length Length of the offending span, in characters.
14
+ * @property {'error'|'warning'} severity
15
+ */
16
+
17
+ /**
18
+ * Diagnostic codes.
19
+ *
20
+ * 1000s are lexical and syntax problems — things findable without knowing what
21
+ * any name refers to. 2000s are resolution and type errors (resolution is
22
+ * implemented; type errors wait on a binder). 3000s are target limits: the
23
+ * construct is valid but the compiler cannot lower it yet, it is not available
24
+ * on the requested target, or the target refuses it as a hardware hazard.
25
+ */
26
+ export const Codes = {
27
+ UNTERMINATED_STRING: '8BS1002',
28
+ UNEXPECTED_CHARACTER: '8BS1003',
29
+ UNMATCHED_BRACKET: '8BS1004',
30
+ UNCLOSED_BRACKET: '8BS1005',
31
+ UNTERMINATED_BLOCK_COMMENT: '8BS1006',
32
+ UNTERMINATED_ASM_BLOCK: '8BS1007',
33
+ INVALID_NUMBER: '8BS1008',
34
+ // A decimal literal (`0.5`) used anywhere other than as the first argument
35
+ // to a duration clock call, `#frames(...)` — the only place the language
36
+ // has any float-shaped syntax. foldDurations() (packages/compiler/src/fold) consumes and
37
+ // removes every valid one before check() ever runs, so any that survive
38
+ // to be walked here were misplaced.
39
+ MISPLACED_DECIMAL_LITERAL: '8BS1009',
40
+ SYNTAX_ERROR: '8BS1101',
41
+ VALUE_OUT_OF_RANGE: '8BS1021',
42
+ // `#frames(...)`'s argument shape is wrong — not one integer-or-decimal
43
+ // literal followed by the bare unit word it is measured in (see
44
+ // foldDurations()). The unit is required: `#frames(30)` is this.
45
+ INVALID_DURATION_ARGUMENT: '8BS1022',
46
+ // A `#frames(...)` call folded to zero frames at the project's configured
47
+ // frameRate — always a bug, not a benign rounding nicety: it would wrap a
48
+ // countdown like `let ticks: utinyint = #frames(...); ... ticks = ticks - 1;`
49
+ // straight through 0 instead of ticking.
50
+ ZERO_DURATION: '8BS1023',
51
+ // A `#frames(...)` call didn't fold to an exact frame count at the
52
+ // project's configured frameRate — reported so a rate change (e.g. 60 to
53
+ // 50) that silently nudges a duration's real-world length is never
54
+ // invisible.
55
+ INEXACT_DURATION: '8BS1024',
56
+ // `#frames(...)`'s second argument names a unit the fold doesn't know —
57
+ // the only one so far is `seconds` (see the fold pass's DURATION_UNITS).
58
+ // Reported at the identifier itself, not the whole call.
59
+ UNKNOWN_DURATION_UNIT: '8BS1025',
60
+ // A string literal (or the text of a template) holds a character outside
61
+ // the portable set — space, `0`-`9`, `A`-`Z`, and `! , - . : ?` — the
62
+ // characters every target's character set can show (the NES ships its own
63
+ // font with exactly these; the Commodore machines are switched to their
64
+ // upper-case set). Reported where the string becomes program data
65
+ // (packages/compiler/src/ir), never for an import specifier.
66
+ UNPORTABLE_CHARACTER: '8BS1026',
67
+ // A string literal longer than 255 bytes: strings are length-prefixed
68
+ // with one byte, and no screen this compiles for has that many cells in
69
+ // a row anyway.
70
+ STRING_TOO_LONG: '8BS1027',
71
+ // A template string (`\`TICK ${ticks}\``) somewhere other than the second
72
+ // argument of a namespace's `print(cell, ...)` — the one place the
73
+ // compiler expands one — or with the wrong shape around it.
74
+ MISPLACED_TEMPLATE: '8BS1028',
75
+ // A `${...}` field the compiler cannot lay out: its width was not given
76
+ // and could not be taken from the expression's type (an imported name, a
77
+ // call across modules), or the value is not something a number field can
78
+ // show (signed, or wider than 16 bits).
79
+ UNPRINTABLE_FIELD: '8BS1029',
80
+ // `#name` — the compile-time spelling — naming a function the compiler
81
+ // doesn't evaluate (`#frames` is the only one), or a compile-time function
82
+ // used without being called (`#frames` on its own).
83
+ UNKNOWN_COMPILE_TIME_FUNCTION: '8BS1030',
84
+ // Assignment (or `++`/`--`) to a `const`: a compile-time constant, inlined
85
+ // wherever it is read, has no storage on the target to assign to. The
86
+ // checker reports a module's own consts; the linker, an imported one.
87
+ ASSIGN_TO_CONST: '8BS1031',
88
+ // `a[7]` on an `array<T, 4>`: a literal index at or past the array's
89
+ // length. Only a literal (or a const) can be checked at compile time; a
90
+ // runtime index is the program's own responsibility, as on the machine.
91
+ INDEX_OUT_OF_RANGE: '8BS1032',
92
+ // `[1, 2, 3]` for an `array<T, 4>`: an array initialiser has exactly as
93
+ // many elements as the type says — the length is part of the type, and
94
+ // the data is laid out at compile time, so nothing can pad or truncate.
95
+ ARRAY_SIZE_MISMATCH: '8BS1033',
96
+ // Names say which side of the compile-time rule they are on: a `const`
97
+ // is UPPER_SNAKE (`OPTION_COUNT`, `BorderColor.BLUE`), a variable starts
98
+ // with a lower-case letter. So a reader knows `LIMIT` is resolved by
99
+ // 8bitscript and `limit` is storage on the machine, without looking up
100
+ // the declaration.
101
+ NAME_CASE: '8BS1034',
102
+ // A call with more arguments than the function has parameters, or fewer
103
+ // than the parameters without a default. A default (`border: utinyint =
104
+ // BorderColor.BLACK`) is a compile-time value 8bitscript fills in at the
105
+ // call, so every call the machine sees is complete.
106
+ WRONG_ARGUMENT_COUNT: '8BS1035',
107
+ // `#system(...)` called with arguments: it takes none (see the fold pass).
108
+ SYSTEM_TAKES_NO_ARGUMENTS: '8BS1036',
109
+ // `#fact(...)` of a key the sheet does not have, or with no key at all
110
+ // (see the fold pass and fold/facts.mjs for the keys).
111
+ UNKNOWN_FACT: '8BS1037',
112
+ // `#fact(...)` in a build that knows its machine but was handed no
113
+ // hardware facts: the fold will not guess a sheet for a real build.
114
+ NO_HARDWARE_FACTS: '8BS1038',
115
+
116
+ UNRESOLVED_PACKAGE: '8BS2001',
117
+ NOT_AN_8BS_PACKAGE: '8BS2002',
118
+ MISSING_PACKAGE_ENTRY: '8BS2003',
119
+ UNRESOLVED_RELATIVE_IMPORT: '8BS2004',
120
+ NO_SUCH_EXPORT: '8BS2005',
121
+ DUPLICATE_BINDING: '8BS2006',
122
+ UNRESOLVED_NAME: '8BS2007',
123
+ MISSING_NATIVE_SOURCE: '8BS2008',
124
+ // A declaration or import named after a builtin — `waitFrame`
125
+ // (packages/compiler/src/ir). Compile-time functions (`#frames`) need no
126
+ // reservation: their `#` spelling is a different token from any name. A user binding by that name would otherwise
127
+ // be silently reinterpreted as the builtin rather than getting a clear
128
+ // diagnostic.
129
+ RESERVED_BUILTIN_NAME: '8BS2009',
130
+ // The entry module must export exactly one thing — a function taking no
131
+ // parameters — and that is the program: what a 6502 target's synthesised
132
+ // C `main` calls and what the web host's worker calls. Zero exports, a
133
+ // second export, an exported global or namespace, or a parameterised entry
134
+ // are all this diagnostic. Other modules (packages, libraries) may export
135
+ // whatever they like.
136
+ ENTRY_EXPORTS: '8BS2010',
137
+ // A package subpath — `@scope/name/thing` — that the package's
138
+ // `"8bitscript".exports` map has no entry for (packages/compiler/src/
139
+ // resolver). The package itself is fine; the import asks it for something
140
+ // it does not offer, which is a different failure from a missing entry
141
+ // file (8BS2003) or a package that is not 8BitScript at all (8BS2002).
142
+ NO_SUCH_SUBPATH: '8BS2011',
143
+
144
+ NOT_COMPILABLE: '8BS3001',
145
+ NOT_ON_THIS_TARGET: '8BS3002',
146
+ // Two of a build's hardware tags each have their own version of a file,
147
+ // and nothing says which wins (see the resolver's chooseVariant).
148
+ AMBIGUOUS_VARIANT: '8BS3004',
149
+ // A write the requested target's own documentation says can damage the
150
+ // machine — the PET's "killer poke" ($E842 with bit 5 set) is the one
151
+ // entry (packages/compiler/src/linker/hazards.mjs). Reported by the
152
+ // linker, which alone knows the machine and has every const inlined; so
153
+ // it is a build-time diagnostic, not one `8bs check` or the editor show.
154
+ HARDWARE_HAZARD: '8BS3003',
155
+ };
156
+
157
+ /** @returns {Diagnostic} */
158
+ export function diagnostic(code, message, file, start, length, severity = 'error') {
159
+ return { code, message, file, start, length, severity };
160
+ }
161
+
162
+ /**
163
+ * Convert an offset into 1-based line and column, for terminal output.
164
+ *
165
+ * @param {string} text
166
+ * @param {number} offset
167
+ * @returns {{ line: number, column: number }}
168
+ */
169
+ export function positionAt(text, offset) {
170
+ let line = 1;
171
+ let lastBreak = -1;
172
+ for (let i = 0; i < offset && i < text.length; i += 1) {
173
+ if (text[i] === '\n') {
174
+ line += 1;
175
+ lastBreak = i;
176
+ }
177
+ }
178
+ return { line, column: offset - lastBreak };
179
+ }
@@ -0,0 +1,209 @@
1
+ // The fact sheet's keys: what a build knows about the machine it is for.
2
+ //
3
+ // A fact is a number or a flag with one meaning on every machine, keyed the
4
+ // way the machine packages' hardware catalogs spell it (`video.columns`,
5
+ // `memory.banked` — see docs/packages.md, "the hardware catalog"). The
6
+ // compiler owns the *keys* and their types; the machine packages own the
7
+ // *values*: each catalog's top-level `facts` is the stock machine's sheet
8
+ // and each hardware value's `facts` is what choosing it changes, and the
9
+ // CLI merges them for a build (packages/cli/src/hardware.mjs,
10
+ // resolveHardware). `#fact(video.columns)` folds to the merged value, and
11
+ // `@8bitscript/system` gives every key a const (`Video.COLUMNS`) so a
12
+ // program never spells a key itself.
13
+ //
14
+ // Two rules the table enforces. A fact is never missing: every catalog
15
+ // declares every key that is `program: true` (the CLI's catalog test holds
16
+ // them to it), so a machine without hardware sprites says `video.sprites`
17
+ // 0 and a program's branch on it folds away rather than failing to
18
+ // compile. And a fact is the worst case that matters, not the brochure
19
+ // figure: `video.spritesPerLine` is the number that decides whether a
20
+ // scene works (8 on the NES, whose 64 per frame would mislead), and where
21
+ // a chip has a cycle budget instead of a count (the X16), the package
22
+ // states the sprite size the count assumes.
23
+ //
24
+ // `when` says when the fact is settled. `build`: fixed by the build — the
25
+ // machine and the hardware it was built for — so the const is the truth on
26
+ // every machine the binary runs on. `run`: hardware the machine may or
27
+ // may not have when the program runs (a REU, a mouse in a port, banked
28
+ // RAM), which one binary can detect and use; the const then means "this
29
+ // build may use it" — the hardware was chosen for the build, so the
30
+ // capability's detection code is compiled in — and whether it is really
31
+ // there is the capability's runtime answer. A build that never asked for
32
+ // the hardware carries no code for it. Which facts are `run` is the
33
+ // machine's business as much as the key's: the table marks what is
34
+ // detectable *somewhere*, and a package that cannot detect it on its
35
+ // machine says so in its notes.
36
+ //
37
+ // `program: false` keys are the CLI's, not a program's: `video.frameRate`
38
+ // is what a screenshot's frame count is timed against, and it is not on
39
+ // the sheet because the level machines (packages/backend-6502, FRAME_SYNC)
40
+ // detect NTSC or PAL at run time — one binary runs at both — so a
41
+ // compile-time refresh rate would be a fact that is sometimes wrong.
42
+
43
+ /**
44
+ * @typedef {object} Fact
45
+ * @property {'count'|'flag'} type a number (folds to an IntegerLiteral) or
46
+ * a yes/no (folds to a BooleanLiteral)
47
+ * @property {'build'|'run'} when see above
48
+ * @property {boolean} program on the program's sheet (`@8bitscript/system`)
49
+ * @property {string} doc one line, for hover and the editor's panel
50
+ */
51
+
52
+ const count = (when, doc, program = true) => ({ type: 'count', when, program, doc });
53
+ const flag = (when, doc, program = true) => ({ type: 'flag', when, program, doc });
54
+
55
+ /** Every fact key, in the order the sheet lists them. @type {Map<string, Fact>} */
56
+ export const FACTS = new Map([
57
+ // Video: the text grid and what the display is made of.
58
+ ['video.columns', count('build', 'Cells across the text grid the portable text draws on.')],
59
+ ['video.rows', count('build', 'Cells down the text grid.')],
60
+ ['video.cellWidth', count('build', 'Pixels across one cell.')],
61
+ ['video.cellHeight', count('build', 'Pixels down one cell.')],
62
+ ['video.palette', count('build', 'Colours the display can show at once.')],
63
+ ['video.cellColors', count('build', 'Colours one cell can hold in the text mode the grid uses.')],
64
+ ['video.colorPerCell', flag('build', 'A program can set one cell\'s colour without changing its neighbours\'.')],
65
+ ['video.glyphs', count('build', 'Characters a program can redefine at run time; 0 where the font is fixed.')],
66
+ ['video.blockWidth', count('build', 'Pseudo-pixels across one cell from the fixed font\'s block glyphs; 0 where there are none.')],
67
+ ['video.blockHeight', count('build', 'Pseudo-pixels down one cell from the block glyphs; 0 where there are none.')],
68
+ ['video.bitmap', flag('build', 'A pixel-addressable mode exists.')],
69
+ ['video.layers', count('build', 'Independent background layers.')],
70
+ ['video.scroll', flag('build', 'Hardware fine scroll exists.')],
71
+ ['video.sprites', count('build', 'Hardware sprites in total; 0 where moving objects are drawn in software.')],
72
+ ['video.spritesPerLine', count('build', 'Hardware sprites one scanline can show — the number that decides whether a scene works.')],
73
+ ['video.spriteWidth', count('build', 'Pixels across the largest hardware sprite.')],
74
+ ['video.spriteHeight', count('build', 'Pixels down the largest hardware sprite.')],
75
+ ['video.spriteColors', count('build', 'Colours one hardware sprite can hold, not counting transparent.')],
76
+ ['video.frameRate', count('build', 'Display refreshes a second, for timing a screenshot; not on the sheet, the machine may be NTSC or PAL at run time.', false)],
77
+ // Audio: the chip's shape.
78
+ ['audio.voices', count('build', 'Voices the sound hardware plays at once.')],
79
+ ['audio.noise', flag('build', 'A noise voice exists.')],
80
+ ['audio.envelope', flag('build', 'Hardware volume envelopes (ADSR or similar) exist.')],
81
+ ['audio.filter', flag('build', 'A hardware filter exists.')],
82
+ ['audio.pcm', flag('build', 'Sample playback exists.')],
83
+ ['audio.volume', flag('build', 'Each voice has its own volume.')],
84
+ ['audio.entropy', flag('build', 'A hardware random source exists.')],
85
+ // Input: the ports and what is plugged into them.
86
+ ['input.keyboard', flag('build', 'A keyboard the program can read.')],
87
+ ['input.joysticks', count('build', 'Joystick ports.')],
88
+ ['input.pads', count('build', 'Console-style controller ports.')],
89
+ ['input.mouse', flag('run', 'A mouse this build may use; whether one is plugged in is the capability\'s answer at run time.')],
90
+ ['input.paddles', flag('run', 'Paddles this build may use; whether they are plugged in is the capability\'s answer at run time.')],
91
+ // Storage.
92
+ ['storage.save', flag('build', 'Somewhere this build can persist bytes: disk, SD card, battery RAM.')],
93
+ ['storage.kib', count('build', 'KiB that place holds — the medium\'s usable capacity, not its image size; 0 where there is nowhere to save. Where the route is a host directory or a card whose size is the owner\'s, it is the smallest real medium that route stands for, so a program that fits the fact fits the hardware. KiB rather than bytes because a disk does not fit in the 16 bits an 8-bit machine counts in — the same reason memory.bankedKib is KiB.')],
94
+ // Memory.
95
+ ['memory.ram', count('build', 'Bytes of RAM the program is linked to use, code and data together.')],
96
+ ['memory.banked', flag('run', 'RAM beyond the CPU\'s window this build may use (a REU, an MMU bank, VERA-style banks); whether it is there is the capability\'s answer at run time.')],
97
+ ['memory.bankedKib', count('run', 'KiB of that banked RAM the build was fitted with.')],
98
+ ]);
99
+
100
+ /** The keys a program's sheet carries, in order. */
101
+ export const PROGRAM_FACTS = [...FACTS].filter(([, fact]) => fact.program).map(([key]) => key);
102
+
103
+ /**
104
+ * The name `@8bitscript/system` gives a key: `video.spritesPerLine` is
105
+ * `Video.SPRITES_PER_LINE`. The namespace is the key's first word
106
+ * capitalised; the const is the rest in upper snake case.
107
+ *
108
+ * @param {string} key
109
+ * @returns {{ namespace: string, name: string }}
110
+ */
111
+ export function factConstName(key) {
112
+ const [group, ...rest] = key.split('.');
113
+ const member = rest.join('.');
114
+ return {
115
+ namespace: group[0].toUpperCase() + group.slice(1),
116
+ name: member.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
117
+ };
118
+ }
119
+
120
+ /**
121
+ * The value a fact has when nothing set it: 0 or false — the placeholder
122
+ * the fold uses with no machine in hand, and the value a catalog test
123
+ * compares against when it insists a key was declared on purpose.
124
+ *
125
+ * @param {string} key
126
+ */
127
+ export function factPlaceholder(key) {
128
+ return FACTS.get(key)?.type === 'flag' ? false : 0;
129
+ }
130
+
131
+ /**
132
+ * Check a `requires` block against the table: what a program needs of the
133
+ * machine it is built for, before the machine is known.
134
+ *
135
+ * A count is a floor and a flag must be true — "at least this much RAM",
136
+ * "somewhere to save" — because that is the shape of every requirement a
137
+ * program actually has. A `when: 'run'` fact cannot be required: whether a
138
+ * mouse is plugged in is answered on the machine, not by the build, so a
139
+ * program that needs one asks the capability and says so itself.
140
+ *
141
+ * @param {object} requires
142
+ * @returns {string[]} the problems, in words; empty when clean
143
+ */
144
+ export function requiresProblems(requires) {
145
+ const problems = [];
146
+ for (const [key, need] of Object.entries(requires ?? {})) {
147
+ const fact = FACTS.get(key);
148
+ if (!fact) {
149
+ problems.push(`'${key}' is not a fact — the keys are ${PROGRAM_FACTS.join(', ')}`);
150
+ continue;
151
+ }
152
+ if (!fact.program) {
153
+ problems.push(`'${key}' is not on a program's sheet, so a program cannot require it`);
154
+ continue;
155
+ }
156
+ if (fact.when === 'run') {
157
+ problems.push(
158
+ `'${key}' is settled on the machine, not by the build, so it cannot be required — `
159
+ + 'ask the capability at run time instead',
160
+ );
161
+ continue;
162
+ }
163
+ if (fact.type === 'flag' && need !== true) {
164
+ problems.push(`'${key}' is a flag: require it with true, or leave it out — not ${JSON.stringify(need)}`);
165
+ }
166
+ if (fact.type === 'count' && !(Number.isInteger(need) && need > 0)) {
167
+ problems.push(`'${key}' is a count: require a whole number above zero, not ${JSON.stringify(need)}`);
168
+ }
169
+ }
170
+ return problems;
171
+ }
172
+
173
+ /**
174
+ * What a `requires` block asks for that a build's facts do not give.
175
+ *
176
+ * @param {object} requires already checked by requiresProblems
177
+ * @param {object} facts the merged sheet for one build
178
+ * @returns {{ key: string, need: number|boolean, have: number|boolean }[]} empty when the build is enough
179
+ */
180
+ export function unmetRequirements(requires, facts) {
181
+ const unmet = [];
182
+ for (const [key, need] of Object.entries(requires ?? {})) {
183
+ const have = facts?.[key] ?? factPlaceholder(key);
184
+ const met = FACTS.get(key)?.type === 'flag' ? have === true : have >= need;
185
+ if (!met) unmet.push({ key, need, have });
186
+ }
187
+ return unmet;
188
+ }
189
+
190
+ /**
191
+ * Check one facts object against the table: every key known, every value
192
+ * of its key's type. Returns the problems, in words; empty when clean.
193
+ *
194
+ * @param {object} facts
195
+ * @returns {string[]}
196
+ */
197
+ export function factProblems(facts) {
198
+ const problems = [];
199
+ for (const [key, value] of Object.entries(facts ?? {})) {
200
+ const fact = FACTS.get(key);
201
+ if (!fact) {
202
+ problems.push(`'${key}' is not a fact — the keys are ${[...FACTS.keys()].join(', ')}`);
203
+ continue;
204
+ }
205
+ if (fact.type === 'flag' && typeof value !== 'boolean') problems.push(`'${key}' is a flag: true or false, not ${JSON.stringify(value)}`);
206
+ if (fact.type === 'count' && !(Number.isInteger(value) && value >= 0)) problems.push(`'${key}' is a count: a whole number, not ${JSON.stringify(value)}`);
207
+ }
208
+ return problems;
209
+ }