@8bitscript/compiler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/index.mjs +91 -0
- package/package.json +29 -0
- package/src/ast/index.mjs +90 -0
- package/src/checker/index.mjs +294 -0
- package/src/diagnostics/index.mjs +179 -0
- package/src/fold/facts.mjs +209 -0
- package/src/fold/index.mjs +412 -0
- package/src/intellisense/index.mjs +558 -0
- package/src/ir/index.mjs +1422 -0
- package/src/lexer/index.mjs +383 -0
- package/src/linker/hazards.mjs +121 -0
- package/src/linker/index.mjs +1075 -0
- package/src/parser/index.mjs +795 -0
- package/src/resolver/index.mjs +534 -0
- package/src/templates/index.mjs +278 -0
- package/src/types/index.mjs +116 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
// Built-in hover and completion.
|
|
2
|
+
//
|
|
3
|
+
// The repository has no binder yet, so there is no symbol table to resolve a
|
|
4
|
+
// user's own variables or functions against. What *can* be answered honestly
|
|
5
|
+
// today is "what does this piece of built-in syntax mean" — a primitive type,
|
|
6
|
+
// `volatile`, `ptr`, `array`, `asm6502`, `@address`, `memory.read`/
|
|
7
|
+
// `memory.write`, `string`, `#frames(...)`, its `seconds` unit, `waitFrame()` — because the compiler
|
|
8
|
+
// already knows all of it statically, independent of any particular program.
|
|
9
|
+
//
|
|
10
|
+
// This module is that answer, expressed as a small position-based API
|
|
11
|
+
// (`getHoverInfo`, `getCompletions`) that an editor-protocol layer can call
|
|
12
|
+
// without knowing anything about 8BitScript itself. When a binder exists, the
|
|
13
|
+
// same two functions grow to cover user-defined names; nothing about this
|
|
14
|
+
// shape is a dead end.
|
|
15
|
+
import { tokenize, TokenKind } from '../lexer/index.mjs';
|
|
16
|
+
import { PRIMITIVE_INTEGER_TYPES, resolveIntegerType } from '../types/index.mjs';
|
|
17
|
+
import { DURATION_CLOCKS, DURATION_UNITS, SYSTEMS } from '../fold/index.mjs';
|
|
18
|
+
import { FACTS } from '../fold/facts.mjs';
|
|
19
|
+
|
|
20
|
+
/** Insert thousands separators without touching locale/ICU: `-8388608` -> `-8,388,608`. */
|
|
21
|
+
function formatNumber(n) {
|
|
22
|
+
const sign = n < 0 ? '-' : '';
|
|
23
|
+
const digits = Math.abs(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
24
|
+
return sign + digits;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hover markdown for one spelling of an integer type.
|
|
29
|
+
*
|
|
30
|
+
* The wording depends on *which* spelling was hovered: the canonical name
|
|
31
|
+
* (`utinyint`) explains the type and names its low-level alias; the alias
|
|
32
|
+
* (`u8`) leads by pointing back to the canonical name it stands for.
|
|
33
|
+
*
|
|
34
|
+
* @param {import('../types/index.mjs').IntegerType} type
|
|
35
|
+
* @param {string} spelling
|
|
36
|
+
*/
|
|
37
|
+
function integerHoverMarkdown(type, spelling) {
|
|
38
|
+
const size = `${type.bytes} byte${type.bytes === 1 ? '' : 's'} / ${type.bits} bits`;
|
|
39
|
+
const range = `${formatNumber(type.min)} through ${formatNumber(type.max)}`;
|
|
40
|
+
|
|
41
|
+
if (spelling === type.legacyAlias) {
|
|
42
|
+
return [
|
|
43
|
+
`**${spelling}**`,
|
|
44
|
+
'',
|
|
45
|
+
`Low-level alias for ${type.canonicalName}.`,
|
|
46
|
+
'',
|
|
47
|
+
[
|
|
48
|
+
type.summary,
|
|
49
|
+
`Size: ${size}`,
|
|
50
|
+
`Range: ${range}`,
|
|
51
|
+
].join(' \n'),
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return [
|
|
56
|
+
`**${spelling}**`,
|
|
57
|
+
'',
|
|
58
|
+
type.summary,
|
|
59
|
+
'',
|
|
60
|
+
[
|
|
61
|
+
`Size: ${size}`,
|
|
62
|
+
`Range: ${range}`,
|
|
63
|
+
`Low-level alias: ${type.legacyAlias}`,
|
|
64
|
+
].join(' \n'),
|
|
65
|
+
].join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Documentation for the non-integer built-ins, in 8BitScript's own terms. */
|
|
69
|
+
const CONSTRUCT_DOCS = {
|
|
70
|
+
string: {
|
|
71
|
+
summary: 'Constant text in the program image, as a parameter type.',
|
|
72
|
+
markdown: [
|
|
73
|
+
'**string**',
|
|
74
|
+
'',
|
|
75
|
+
'Text that lives in the program image — ROM on a cartridge, the `.prg` on a Commodore, a data segment on the web — written as a literal: `"TICK"`. A `string` parameter receives one; `s.length` is its byte count and `s[i]` its i-th character (ASCII), so a loop can put it on screen one cell at a time.',
|
|
76
|
+
'',
|
|
77
|
+
'Only the portable character set is allowed — space, `0`-`9`, `A`-`Z`, and `! , - . : ?` (upper case only), the characters every target can show — and at most 255 of them. A backtick string with `${...}` fields, `\`TICK ${ticks:1}\``, is a template: `text.print(cell, ...)` lays it out at compile time into one `print` per run of text and one `printNumber(cell, value, width)` per field.',
|
|
78
|
+
'',
|
|
79
|
+
'`const Label: string = "..."` names constant text. `let name: string<8>` is text that changes: 8 characters of RAM behind a length byte — the shape a literal has, so it goes wherever a `string` goes. `name = "..."` or `name = other` copies at runtime, cut to the capacity (a literal that does not fit is a diagnostic); `name.length` and `name[i]` read it. There is no concatenation.',
|
|
80
|
+
].join('\n'),
|
|
81
|
+
},
|
|
82
|
+
volatile: {
|
|
83
|
+
summary: 'Value that may change outside normal program flow.',
|
|
84
|
+
markdown: [
|
|
85
|
+
'**volatile<T>**',
|
|
86
|
+
'',
|
|
87
|
+
'Marks a value whose contents may change outside normal program execution.',
|
|
88
|
+
'',
|
|
89
|
+
'The compiler must preserve reads and writes rather than assuming the value stays unchanged.',
|
|
90
|
+
'',
|
|
91
|
+
'Common uses include memory-mapped hardware registers and values modified by interrupts.',
|
|
92
|
+
'',
|
|
93
|
+
'Most ordinary variables do not need `volatile`.',
|
|
94
|
+
].join('\n'),
|
|
95
|
+
},
|
|
96
|
+
ptr: {
|
|
97
|
+
summary: 'Pointer to a memory location holding a value of type T.',
|
|
98
|
+
markdown: [
|
|
99
|
+
'**ptr<T>**',
|
|
100
|
+
'',
|
|
101
|
+
'A pointer to a memory location containing a value of type T, for explicit low-level memory access.',
|
|
102
|
+
].join('\n'),
|
|
103
|
+
},
|
|
104
|
+
array: {
|
|
105
|
+
summary: 'Fixed-size array of N values of type T.',
|
|
106
|
+
markdown: [
|
|
107
|
+
'**array<T, N>**',
|
|
108
|
+
'',
|
|
109
|
+
'A fixed-size array of N values of type T, read and written one element at a time: `a[i]`, `a[i] = v`. `a.length` is N, a number 8bitscript fills in.',
|
|
110
|
+
'',
|
|
111
|
+
'`let a: array<T, N>` is N values in RAM (zero until written, or `= [..]`); `const Table: array<T, N> = [..]` is N values of data in the program, never in RAM; `@address(0x0400) let screenRam: array<T, N>` is N cells of hardware. N is a literal or a `const`, and the size is part of the type, so memory usage is predictable — no hidden allocation or resizing.',
|
|
112
|
+
'',
|
|
113
|
+
'A function takes one the same way — `function pick(t: array<u8, 4>, i: u8)` — and the call passes the array by name: the address of its first element, with nothing copied and no length travelling alongside it, since `t.length` is folded from the type. An array parameter is read-only and has no default.',
|
|
114
|
+
].join('\n'),
|
|
115
|
+
},
|
|
116
|
+
asm6502: {
|
|
117
|
+
summary: 'Embeds raw 6502 assembly directly.',
|
|
118
|
+
markdown: [
|
|
119
|
+
'**asm6502**',
|
|
120
|
+
'',
|
|
121
|
+
'Embeds raw 6502 assembly directly in an 8BitScript program.',
|
|
122
|
+
'',
|
|
123
|
+
'Use it when direct machine-level control is required. The block is passed through untouched — 8BitScript does not parse or check the assembly inside it.',
|
|
124
|
+
].join('\n'),
|
|
125
|
+
},
|
|
126
|
+
address: {
|
|
127
|
+
summary: 'Binds a declaration to a specific memory address.',
|
|
128
|
+
markdown: [
|
|
129
|
+
'**@address(location)**',
|
|
130
|
+
'',
|
|
131
|
+
'Binds a declaration to a specific memory address.',
|
|
132
|
+
'',
|
|
133
|
+
'Commonly used for memory-mapped hardware registers, where a variable\'s storage is a fixed location rather than one the compiler assigns.',
|
|
134
|
+
].join('\n'),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** `#frames(...)`: the compile-time duration builtin (packages/compiler/src/fold). */
|
|
139
|
+
const FRAMES_DOC = [
|
|
140
|
+
'**#frames(n, unit)**',
|
|
141
|
+
'',
|
|
142
|
+
'Compile-time duration, as a frame count. `n` is an integer or decimal literal — `#frames(1, seconds)`, `#frames(0.5, seconds)` — never a variable or expression. `unit` says what `n` is measured in and is required; the only unit so far is `seconds`.',
|
|
143
|
+
'',
|
|
144
|
+
'Folds at compile time to however many frames — `waitFrame()` calls — that much time takes at this project\'s configured `frameRate` (`8bs.config.ts`, default 60) — `#frames(0.5, seconds)` becomes `30` at the default rate, `25` at a configured 50. Always a plain integer once compiled: no runtime division, no floating point.',
|
|
145
|
+
'',
|
|
146
|
+
'The `#` says 8bitscript evaluates this before any target toolchain runs; a plain `name(...)` always runs on the machine. Nothing is reserved: `#frames` is its own token, and the unit word is only a unit in this argument position.',
|
|
147
|
+
].join('\n');
|
|
148
|
+
|
|
149
|
+
/** `#system()`: the machine this build is for (packages/compiler/src/fold). */
|
|
150
|
+
const SYSTEM_DOC = [
|
|
151
|
+
'**#system()**',
|
|
152
|
+
'',
|
|
153
|
+
'Compile-time: the machine this build is for, as a number. Compare it with the names `@8bitscript/system` exports — `if (#system() == System.NES) { ... }` — and the other machines\' branches fold away in the generated code. Takes no arguments: the build already knows which machine.',
|
|
154
|
+
'',
|
|
155
|
+
`The machines, in the order \`8bs build --target\` lists them: ${[...SYSTEMS.keys()].map((name) => `\`${name}\``).join(', ')}. With no machine in hand — \`8bs check\`, this editor — the call is valid and target-dependent, like a \`.<machine>.8bs\` file.`,
|
|
156
|
+
'',
|
|
157
|
+
'Prefer a fact to the name where one exists: `text.COLUMNS` is right on a machine this list has never heard of; `#system() == System.PET` says nothing about a C128 in 80 columns.',
|
|
158
|
+
].join('\n');
|
|
159
|
+
|
|
160
|
+
const FACT_DOC = [
|
|
161
|
+
'**#fact(...)**',
|
|
162
|
+
'',
|
|
163
|
+
'Compile-time: one fact about the machine this build is for — a number or a yes/no from its hardware fact sheet, written as words: `#fact(video.columns)`, `#fact(memory.banked)`. The value is the machine package\'s catalog entry for the stock machine, changed by whatever hardware the build was fitted with (`--profile`, `--hardware`), so the branch for hardware this build lacks folds away.',
|
|
164
|
+
'',
|
|
165
|
+
'A program rarely writes this itself: `@8bitscript/system` gives every fact a name — `Video.COLUMNS`, `Audio.VOICES`, `Input.KEYBOARD`, `Memory.RAM` — and reads it this way. A fact marked *run time* there means "this build may use it"; whether the hardware is really there is the capability\'s answer on the machine.',
|
|
166
|
+
'',
|
|
167
|
+
`The keys: ${[...FACTS].filter(([, f]) => f.program).map(([key]) => `\`${key}\``).join(', ')}. With no machine in hand — \`8bs check\`, this editor — every fact is its placeholder (0 or false) and target-dependent, like \`#system()\`.`,
|
|
168
|
+
].join('\n');
|
|
169
|
+
|
|
170
|
+
/** One fact key's hover, inside a `#fact(...)`. */
|
|
171
|
+
const factKeyDoc = (key) => {
|
|
172
|
+
const fact = FACTS.get(key);
|
|
173
|
+
return [
|
|
174
|
+
`**${key}**`,
|
|
175
|
+
'',
|
|
176
|
+
`${fact.doc} A ${fact.type === 'flag' ? 'yes/no' : 'count'}, settled ${fact.when === 'run' ? 'at run time: the const says this build may use it, and the capability says whether it is there' : 'by the build'}.${fact.program ? '' : ' Not on the program\'s sheet: the CLI reads it.'}`,
|
|
177
|
+
'',
|
|
178
|
+
'Only a key inside `#fact(...)`; anywhere else these words are ordinary names a program is free to declare.',
|
|
179
|
+
].join('\n');
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** Every `#name` the compiler evaluates, with what completion says about it. */
|
|
183
|
+
const COMPILE_TIME_DOCS = {
|
|
184
|
+
// `insert` is what goes into the buffer after a `#` the user has typed;
|
|
185
|
+
// when the lexer already made a `#name` token the whole token is
|
|
186
|
+
// replaced, by the label itself unless `insert` adds something to it.
|
|
187
|
+
frames: { detail: 'Compile-time duration, as a frame count.', documentation: FRAMES_DOC, insert: 'frames' },
|
|
188
|
+
system: { detail: 'Compile-time: the machine this build is for.', documentation: SYSTEM_DOC, insert: 'system()' },
|
|
189
|
+
fact: { detail: 'Compile-time: one fact about the machine this build is for.', documentation: FACT_DOC, insert: 'fact' },
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/** The units a `#frames(...)` duration can be written in, keyed as DURATION_UNITS is. */
|
|
193
|
+
const UNIT_DOCS = {};
|
|
194
|
+
|
|
195
|
+
/** `seconds`: the (only) unit a `#frames(...)` duration can be written in. */
|
|
196
|
+
UNIT_DOCS.seconds = [
|
|
197
|
+
'**seconds**',
|
|
198
|
+
'',
|
|
199
|
+
'A unit for `#frames(...)`: `#frames(0.5, seconds)` is half a second, counted in logical frames — `waitFrame()` calls — at this project\'s configured `frameRate` (`8bs.config.ts`, default 60). The unit is required, so the call always says what its literal is measured in.',
|
|
200
|
+
'',
|
|
201
|
+
'Only a unit in the second argument to `#frames(...)`; anywhere else, `seconds` is an ordinary name a program is free to declare.',
|
|
202
|
+
].join('\n');
|
|
203
|
+
|
|
204
|
+
/** `waitFrame()`: block until the next logical frame (packages/compiler/src/ir). */
|
|
205
|
+
const WAITFRAME_DOC = [
|
|
206
|
+
'**waitFrame()**',
|
|
207
|
+
'',
|
|
208
|
+
'Blocks until the next logical frame, then returns. Call it once per pass through your main loop — `while (true) { waitFrame(); ... }` — the way an 8-bit program waits for vertical blank (cc65\'s `waitvsync()`).',
|
|
209
|
+
'',
|
|
210
|
+
'Frames arrive at this project\'s configured `frameRate` (`8bs.config.ts`, default 60) on every target, whatever the real hardware refreshes at — on the 6502 machines it waits on the video chip\'s own vertical blank, on the web it waits on the page\'s frame clock. Pair it with `#frames(...)` to count time: `#frames(0.5, seconds)` is how many `waitFrame()` calls make half a second.',
|
|
211
|
+
'',
|
|
212
|
+
'Takes no arguments and returns nothing. Reserved: a variable, function, parameter, or import named `waitFrame` is a compile error.',
|
|
213
|
+
].join('\n');
|
|
214
|
+
|
|
215
|
+
/** `memory.read`/`memory.write`: the one namespace the compiler recognises itself. */
|
|
216
|
+
const MEMORY_DOCS = {
|
|
217
|
+
write: [
|
|
218
|
+
'**memory.write(address, value)**',
|
|
219
|
+
'',
|
|
220
|
+
'Writes one byte directly to the target machine\'s address space.',
|
|
221
|
+
'',
|
|
222
|
+
'This is the low-level equivalent of `POKE` on Commodore BASIC systems.',
|
|
223
|
+
'',
|
|
224
|
+
'Prefer a machine API such as `screen` when one exists for what you are trying to do.',
|
|
225
|
+
].join('\n'),
|
|
226
|
+
read: [
|
|
227
|
+
'**memory.read(address)**',
|
|
228
|
+
'',
|
|
229
|
+
'Reads one byte directly from the target machine\'s address space.',
|
|
230
|
+
'',
|
|
231
|
+
'This is the low-level equivalent of `PEEK` on Commodore BASIC systems.',
|
|
232
|
+
'',
|
|
233
|
+
'Prefer a machine API such as `screen` when one exists for what you are trying to do.',
|
|
234
|
+
].join('\n'),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const TYPE_CONSTRUCTOR_NAMES = ['array', 'ptr', 'volatile'];
|
|
238
|
+
|
|
239
|
+
/** The index of the token covering `offset` (a cursor right after a word still hits it), or -1. */
|
|
240
|
+
function tokenIndexAt(tokens, offset) {
|
|
241
|
+
return tokens.findIndex((t) => offset >= t.start && offset <= t.start + t.length);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Built-in hover information for the construct at `offset` in `text`.
|
|
246
|
+
*
|
|
247
|
+
* Recognises primitive integer types (canonical spellings like `utinyint` and
|
|
248
|
+
* `int`, or their low-level `u8`/`i32`-style aliases),
|
|
249
|
+
* `volatile`/`ptr`/`array`, `asm6502`, `@address`, the `memory.read`/
|
|
250
|
+
* `memory.write` intrinsic, and the `#frames(...)` (with its `seconds` unit),
|
|
251
|
+
* `#system()`, and `waitFrame()` builtins — every built-in this milestone documents. Anything else,
|
|
252
|
+
* including a user's own identifiers or namespace, returns `null`: there is
|
|
253
|
+
* no binder yet to say what they mean.
|
|
254
|
+
*
|
|
255
|
+
* @param {string} text
|
|
256
|
+
* @param {number} offset
|
|
257
|
+
* @returns {{ start: number, length: number, markdown: string } | null}
|
|
258
|
+
*/
|
|
259
|
+
export function getHoverInfo(text, offset) {
|
|
260
|
+
const { tokens } = tokenize(text);
|
|
261
|
+
return hoverAt(tokens, offset, text);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function hoverAt(tokens, offset, text) {
|
|
265
|
+
const index = tokenIndexAt(tokens, offset);
|
|
266
|
+
if (index === -1) return null;
|
|
267
|
+
const token = tokens[index];
|
|
268
|
+
|
|
269
|
+
// Inside a template string, a `${...}` field is ordinary source: re-lex
|
|
270
|
+
// the field the parser's way (offsets shifted back into the file) and
|
|
271
|
+
// answer for the token under the cursor there — `#frames`, its unit, a
|
|
272
|
+
// type in a future cast — as if it stood outside the string.
|
|
273
|
+
if (token.kind === TokenKind.Template) {
|
|
274
|
+
const field = token.parts.find((p) => p.kind === 'field' && offset >= p.sourceStart && offset <= p.sourceEnd);
|
|
275
|
+
if (!field) return null;
|
|
276
|
+
const inner = tokenize(text.slice(field.sourceStart, field.sourceEnd)).tokens;
|
|
277
|
+
for (const t of inner) t.start += field.sourceStart;
|
|
278
|
+
return hoverAt(inner, offset, text);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (token.kind === TokenKind.Type) {
|
|
282
|
+
const integer = resolveIntegerType(token.text);
|
|
283
|
+
if (integer) {
|
|
284
|
+
return { start: token.start, length: token.length, markdown: integerHoverMarkdown(integer, token.text) };
|
|
285
|
+
}
|
|
286
|
+
const construct = CONSTRUCT_DOCS[token.text];
|
|
287
|
+
if (construct) return { start: token.start, length: token.length, markdown: construct.markdown };
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (token.kind === TokenKind.Keyword && token.text === 'asm6502') {
|
|
292
|
+
return { start: token.start, length: token.length, markdown: CONSTRUCT_DOCS.asm6502.markdown };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (token.kind === TokenKind.Decorator && token.text.slice(1) === 'address') {
|
|
296
|
+
return { start: token.start, length: token.length, markdown: CONSTRUCT_DOCS.address.markdown };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (token.kind === TokenKind.Identifier && (token.text === 'read' || token.text === 'write')) {
|
|
300
|
+
const dot = tokens[index - 1];
|
|
301
|
+
const object = tokens[index - 2];
|
|
302
|
+
if (dot?.text === '.' && object?.kind === TokenKind.Identifier && object.text === 'memory') {
|
|
303
|
+
return { start: token.start, length: token.length, markdown: MEMORY_DOCS[token.text] };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// `#frames` is its own token kind, so any occurrence is the compile-time
|
|
308
|
+
// function; `waitFrame` is reserved (see checker/index.mjs's
|
|
309
|
+
// RESERVED_BUILTIN_NAMES), so unlike memory.read/write there is no
|
|
310
|
+
// namespace to require — any bare occurrence means the builtin.
|
|
311
|
+
if (token.kind === TokenKind.CompileTime && DURATION_CLOCKS.has(token.text.slice(1))) {
|
|
312
|
+
return { start: token.start, length: token.length, markdown: FRAMES_DOC };
|
|
313
|
+
}
|
|
314
|
+
if (token.kind === TokenKind.CompileTime && token.text === '#system') {
|
|
315
|
+
return { start: token.start, length: token.length, markdown: SYSTEM_DOC };
|
|
316
|
+
}
|
|
317
|
+
if (token.kind === TokenKind.CompileTime && token.text === '#fact') {
|
|
318
|
+
return { start: token.start, length: token.length, markdown: FACT_DOC };
|
|
319
|
+
}
|
|
320
|
+
// A fact key's words are not reserved either — `video.columns` only
|
|
321
|
+
// means the fact inside `#fact(...)` — so the hover finds the whole key
|
|
322
|
+
// the hovered word is part of, and only claims it there.
|
|
323
|
+
if (token.kind === TokenKind.Identifier) {
|
|
324
|
+
const key = factKeyAt(tokens, index);
|
|
325
|
+
if (key) return { start: key.start, length: key.length, markdown: factKeyDoc(key.text) };
|
|
326
|
+
}
|
|
327
|
+
// The unit word is *not* reserved — it only means the unit in the second
|
|
328
|
+
// argument slot of a clock call, `#frames(0.5, seconds)`, so the hover has
|
|
329
|
+
// to check it is actually in that slot before claiming so.
|
|
330
|
+
if (token.kind === TokenKind.Identifier && DURATION_UNITS.has(token.text) && isDurationUnitSlot(tokens, index)) {
|
|
331
|
+
return { start: token.start, length: token.length, markdown: UNIT_DOCS[token.text] };
|
|
332
|
+
}
|
|
333
|
+
if (token.kind === TokenKind.Identifier && token.text === 'waitFrame') {
|
|
334
|
+
return { start: token.start, length: token.length, markdown: WAITFRAME_DOC };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Is the identifier at `index` the unit argument of a duration clock call —
|
|
342
|
+
* the `seconds` in `#frames(0.5, seconds)`? Matches the exact shape the fold
|
|
343
|
+
* accepts: `#<clock> ( <number> , <unit>`.
|
|
344
|
+
*/
|
|
345
|
+
/**
|
|
346
|
+
* The fact key the identifier at `index` belongs to — `video.columns` for
|
|
347
|
+
* either word — when it sits inside a `#fact(...)` call and is a key the
|
|
348
|
+
* compiler knows; null otherwise.
|
|
349
|
+
*
|
|
350
|
+
* @returns {{ text: string, start: number, length: number } | null}
|
|
351
|
+
*/
|
|
352
|
+
function factKeyAt(tokens, index) {
|
|
353
|
+
let first = index;
|
|
354
|
+
while (tokens[first - 1]?.text === '.' && tokens[first - 2]?.kind === TokenKind.Identifier) first -= 2;
|
|
355
|
+
let last = index;
|
|
356
|
+
while (tokens[last + 1]?.text === '.' && tokens[last + 2]?.kind === TokenKind.Identifier) last += 2;
|
|
357
|
+
const open = tokens[first - 1];
|
|
358
|
+
const callee = tokens[first - 2];
|
|
359
|
+
const close = tokens[last + 1];
|
|
360
|
+
if (open?.text !== '(' || close?.text !== ')' || callee?.kind !== TokenKind.CompileTime || callee.text !== '#fact') return null;
|
|
361
|
+
const text = tokens.slice(first, last + 1).map((t) => t.text).join('');
|
|
362
|
+
if (!FACTS.has(text)) return null;
|
|
363
|
+
return { text, start: tokens[first].start, length: tokens[last].start + tokens[last].length - tokens[first].start };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isDurationUnitSlot(tokens, index) {
|
|
367
|
+
const [callee, open, literal, comma] = [tokens[index - 4], tokens[index - 3], tokens[index - 2], tokens[index - 1]];
|
|
368
|
+
return comma?.text === ','
|
|
369
|
+
&& literal?.kind === TokenKind.Number
|
|
370
|
+
&& open?.text === '('
|
|
371
|
+
&& callee?.kind === TokenKind.CompileTime
|
|
372
|
+
&& DURATION_CLOCKS.has(callee.text.slice(1));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The token index the cursor is asking *about*: a word the cursor is still
|
|
377
|
+
* inside or at the end of is the thing being typed, not context, so step
|
|
378
|
+
* back to whatever precedes it.
|
|
379
|
+
*/
|
|
380
|
+
function contextIndex(tokens, offset) {
|
|
381
|
+
const before = tokens.filter((t) => t.start < offset);
|
|
382
|
+
let i = before.length - 1;
|
|
383
|
+
const current = before[i];
|
|
384
|
+
if (
|
|
385
|
+
current
|
|
386
|
+
&& current.start + current.length >= offset
|
|
387
|
+
&& [TokenKind.Identifier, TokenKind.Type, TokenKind.Keyword, TokenKind.CompileTime].includes(current.kind)
|
|
388
|
+
) {
|
|
389
|
+
i -= 1;
|
|
390
|
+
}
|
|
391
|
+
return { before, i };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Is `offset` a position where a type name belongs?
|
|
396
|
+
*
|
|
397
|
+
* Only two shapes introduce a type in this grammar: a `:` annotation (`let x:`,
|
|
398
|
+
* a parameter, a return type) and a type argument after a type constructor
|
|
399
|
+
* (`ptr<`, `array<`, `volatile<`). Both are checked by token, not regex, so
|
|
400
|
+
* `x < 5` does not get mistaken for `ptr<u8>`.
|
|
401
|
+
*/
|
|
402
|
+
function isTypePosition(tokens, offset) {
|
|
403
|
+
const { before, i } = contextIndex(tokens, offset);
|
|
404
|
+
const context = before[i];
|
|
405
|
+
if (!context) return false;
|
|
406
|
+
if (context.text === ':') return true;
|
|
407
|
+
if (context.text === '<') return before[i - 1]?.kind === TokenKind.Type;
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Is `offset` inside the `#name` spelling — either a `#` just typed (which
|
|
413
|
+
* is not a token on its own: the lexer only makes one when an identifier
|
|
414
|
+
* character follows) or a `#name` being typed?
|
|
415
|
+
*/
|
|
416
|
+
function compileTimePosition(tokens, offset, text) {
|
|
417
|
+
const at = tokens.find((t) => t.kind === TokenKind.CompileTime
|
|
418
|
+
&& offset > t.start && offset <= t.start + t.length);
|
|
419
|
+
if (at) return { replacing: true };
|
|
420
|
+
return text[offset - 1] === '#' ? { replacing: false } : null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Is `offset` the unit argument of a compile-time clock call — the
|
|
425
|
+
* `seconds` in `#frames(0.5, |)`? The same shape isDurationUnitSlot()
|
|
426
|
+
* recognises for hover, one token earlier.
|
|
427
|
+
*/
|
|
428
|
+
/**
|
|
429
|
+
* Is `offset` the key argument of a `#fact(...)` call — `#fact(|)`, or a
|
|
430
|
+
* key already partly typed, `#fact(vid|)` / `#fact(video.col|)`?
|
|
431
|
+
*/
|
|
432
|
+
function isFactKeyPosition(tokens, offset) {
|
|
433
|
+
const { before, i } = contextIndex(tokens, offset);
|
|
434
|
+
let j = i;
|
|
435
|
+
// Step back over a partly typed dotted key: the word being typed is
|
|
436
|
+
// already behind `i`, so what is left is `video.` or `video`, or nothing.
|
|
437
|
+
if (before[j]?.text === '.') j -= 1;
|
|
438
|
+
while (before[j]?.kind === TokenKind.Identifier) {
|
|
439
|
+
if (before[j - 1]?.text === '.') { j -= 2; continue; }
|
|
440
|
+
j -= 1;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
return before[j]?.text === '(' && before[j - 1]?.kind === TokenKind.CompileTime && before[j - 1].text === '#fact';
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function isDurationUnitPosition(tokens, offset) {
|
|
447
|
+
const { before, i } = contextIndex(tokens, offset);
|
|
448
|
+
const [callee, open, literal, comma] = [before[i - 3], before[i - 2], before[i - 1], before[i]];
|
|
449
|
+
return comma?.text === ','
|
|
450
|
+
&& literal?.kind === TokenKind.Number
|
|
451
|
+
&& open?.text === '('
|
|
452
|
+
&& callee?.kind === TokenKind.CompileTime
|
|
453
|
+
&& DURATION_CLOCKS.has(callee.text.slice(1));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Built-in completion items available at `offset` in `text`.
|
|
458
|
+
*
|
|
459
|
+
* Built-ins only — the type names where a type can appear, the compile-time
|
|
460
|
+
* functions after a `#`, and the unit words inside a `#frames(...)` call.
|
|
461
|
+
* No project-wide or member completion: that needs the binder this
|
|
462
|
+
* milestone deliberately does not add. Inside a template string, a
|
|
463
|
+
* `${...}` field is ordinary source and gets the same answers it would
|
|
464
|
+
* outside one.
|
|
465
|
+
*
|
|
466
|
+
* @param {string} text
|
|
467
|
+
* @param {number} offset
|
|
468
|
+
* @returns {{ label: string, kind: 'type'|'function'|'constant', sortRank: number,
|
|
469
|
+
* detail: string, documentation: string, insertText?: string }[]}
|
|
470
|
+
*/
|
|
471
|
+
export function getCompletions(text, offset) {
|
|
472
|
+
const { tokens } = tokenize(text);
|
|
473
|
+
return completionsAt(tokens, offset, text);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function completionsAt(tokens, offset, text) {
|
|
477
|
+
const index = tokenIndexAt(tokens, offset);
|
|
478
|
+
const token = tokens[index];
|
|
479
|
+
if (token?.kind === TokenKind.Template) {
|
|
480
|
+
const field = token.parts.find((p) => p.kind === 'field'
|
|
481
|
+
&& offset >= p.sourceStart && offset <= p.sourceEnd);
|
|
482
|
+
if (!field) return [];
|
|
483
|
+
const inner = tokenize(text.slice(field.sourceStart, field.sourceEnd)).tokens;
|
|
484
|
+
for (const t of inner) t.start += field.sourceStart;
|
|
485
|
+
return completionsAt(inner, offset, text);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const compileTime = compileTimePosition(tokens, offset, text);
|
|
489
|
+
if (compileTime) {
|
|
490
|
+
return Object.entries(COMPILE_TIME_DOCS).map(([name, doc]) => ({
|
|
491
|
+
label: `#${name}`,
|
|
492
|
+
kind: 'function',
|
|
493
|
+
sortRank: 0,
|
|
494
|
+
detail: doc.detail,
|
|
495
|
+
documentation: doc.documentation,
|
|
496
|
+
// The `#` is already in the buffer unless the lexer made a token of
|
|
497
|
+
// it, in which case the whole `#name` is what gets replaced — by the
|
|
498
|
+
// label, unless the insertion adds the call's parentheses.
|
|
499
|
+
...(compileTime.replacing
|
|
500
|
+
? (doc.insert === name ? {} : { insertText: `#${doc.insert}` })
|
|
501
|
+
: { insertText: doc.insert }),
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (isFactKeyPosition(tokens, offset)) {
|
|
506
|
+
return [...FACTS].filter(([, fact]) => fact.program).map(([key, fact]) => ({
|
|
507
|
+
label: key,
|
|
508
|
+
kind: 'constant',
|
|
509
|
+
sortRank: 0,
|
|
510
|
+
detail: fact.doc,
|
|
511
|
+
documentation: factKeyDoc(key),
|
|
512
|
+
}));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (isDurationUnitPosition(tokens, offset)) {
|
|
516
|
+
return [...DURATION_UNITS.keys()].map((name) => ({
|
|
517
|
+
label: name,
|
|
518
|
+
kind: 'constant',
|
|
519
|
+
sortRank: 0,
|
|
520
|
+
detail: 'A unit a #frames(...) duration can be written in.',
|
|
521
|
+
documentation: UNIT_DOCS[name],
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (!isTypePosition(tokens, offset)) return [];
|
|
526
|
+
|
|
527
|
+
const items = [];
|
|
528
|
+
|
|
529
|
+
for (const type of PRIMITIVE_INTEGER_TYPES) {
|
|
530
|
+
items.push({
|
|
531
|
+
label: type.canonicalName,
|
|
532
|
+
kind: 'type',
|
|
533
|
+
sortRank: 0,
|
|
534
|
+
detail: `${type.summary} (${type.min}..${type.max})`,
|
|
535
|
+
documentation: integerHoverMarkdown(type, type.canonicalName),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
for (const name of ['string', ...TYPE_CONSTRUCTOR_NAMES]) {
|
|
539
|
+
items.push({
|
|
540
|
+
label: name,
|
|
541
|
+
kind: 'type',
|
|
542
|
+
sortRank: 0,
|
|
543
|
+
detail: CONSTRUCT_DOCS[name].summary,
|
|
544
|
+
documentation: CONSTRUCT_DOCS[name].markdown,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
for (const type of PRIMITIVE_INTEGER_TYPES) {
|
|
548
|
+
items.push({
|
|
549
|
+
label: type.legacyAlias,
|
|
550
|
+
kind: 'type',
|
|
551
|
+
sortRank: 1,
|
|
552
|
+
detail: `Low-level alias for ${type.canonicalName}`,
|
|
553
|
+
documentation: integerHoverMarkdown(type, type.legacyAlias),
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return items;
|
|
558
|
+
}
|