spp-lang 0.0.0-stage → 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/README.md +170 -2
- package/dist/bin/spp-lang.d.ts +2 -0
- package/dist/bin/spp-lang.js +214 -0
- package/dist/src/backends/playwright/generate.d.ts +13 -0
- package/dist/src/backends/playwright/generate.js +238 -0
- package/dist/src/backends/playwright/runner/playwright.config.d.ts +2 -0
- package/dist/src/backends/playwright/runner/playwright.config.js +16 -0
- package/dist/src/backends/playwright/runtime.d.ts +14 -0
- package/dist/src/backends/playwright/runtime.js +158 -0
- package/dist/src/backends/playwright/words.d.ts +11 -0
- package/dist/src/backends/playwright/words.js +129 -0
- package/dist/src/config.d.ts +18 -0
- package/dist/src/config.js +63 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.js +12 -0
- package/dist/src/language/check.d.ts +178 -0
- package/dist/src/language/check.js +709 -0
- package/dist/src/language/cognate.d.ts +137 -0
- package/dist/src/language/cognate.js +570 -0
- package/dist/src/language/load.d.ts +58 -0
- package/dist/src/language/load.js +200 -0
- package/dist/src/language/markdown.d.ts +9 -0
- package/dist/src/language/markdown.js +52 -0
- package/dist/src/language/sections.d.ts +19 -0
- package/dist/src/language/sections.js +119 -0
- package/dist/src/language/steps.d.ts +19 -0
- package/dist/src/language/steps.js +91 -0
- package/dist/src/language/types.d.ts +32 -0
- package/dist/src/language/types.js +51 -0
- package/dist/src/tools/dictionary.d.ts +3 -0
- package/dist/src/tools/dictionary.js +89 -0
- package/dist/src/tools/lsp.d.ts +1 -0
- package/dist/src/tools/lsp.js +479 -0
- package/dist/src/words/driver.d.ts +39 -0
- package/dist/src/words/driver.js +9 -0
- package/dist/src/words/shared.d.ts +56 -0
- package/dist/src/words/shared.js +272 -0
- package/package.json +65 -4
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
// The checker: runs every step on a stack of types instead of values, so
|
|
2
|
+
// mistakes show up before a browser starts.
|
|
3
|
+
//
|
|
4
|
+
// It uses the forms the runtime uses, and picks between them the same way:
|
|
5
|
+
// the first whose inputs are on top of the stack. With no conditionals or
|
|
6
|
+
// loops, every step is straight-line code, so one pass is the whole story.
|
|
7
|
+
//
|
|
8
|
+
// A Define's header says what it takes: "Define: Add Text (Item)" takes Text,
|
|
9
|
+
// called Item in its body, so Add : Text ->. Its body is checked with those,
|
|
10
|
+
// and can't take anything else from its caller. What it leaves is worked out.
|
|
11
|
+
//
|
|
12
|
+
// A Define that leaves one thing of a built-in kind is a noun, and makes a
|
|
13
|
+
// kind of its own: "Define: Cart" leaving an Element makes Cart, a narrower
|
|
14
|
+
// Element, which other Defines' headers can take.
|
|
15
|
+
//
|
|
16
|
+
// A word can have several Defines, told apart by what they take. A use runs
|
|
17
|
+
// the first that fits, most particular first, as the runtime does
|
|
18
|
+
// (Definitions in cognate.ts). Two that could always both fit are a mistake
|
|
19
|
+
// in the Defines, so a use never has to guess.
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { notAKind, parse, SppError, specificity, unknownWord } from './cognate.js';
|
|
22
|
+
import { lineCol } from './load.js';
|
|
23
|
+
import { keywordForms, keywords, StepType } from './steps.js';
|
|
24
|
+
import { AnyType, article, NumberType, signature, TableType, TextType, type } from './types.js';
|
|
25
|
+
// ---------------------------------------------------------------- static values
|
|
26
|
+
// The keyword words, for their forms; the checker runs steps itself.
|
|
27
|
+
const KEYWORDS = keywords(async () => { });
|
|
28
|
+
const SymbolType = type('Symbol', 'a symbol', (v) => false);
|
|
29
|
+
// A value a Define's body takes from its caller. members is what it can
|
|
30
|
+
// still be, narrowed by each use; null until something uses it.
|
|
31
|
+
class Var {
|
|
32
|
+
members = null;
|
|
33
|
+
phrase = 'a value';
|
|
34
|
+
}
|
|
35
|
+
// A block, waiting for Define to take it.
|
|
36
|
+
class BlockOf {
|
|
37
|
+
body;
|
|
38
|
+
scope;
|
|
39
|
+
origin;
|
|
40
|
+
constructor(body, scope, origin) {
|
|
41
|
+
this.body = body;
|
|
42
|
+
this.scope = scope;
|
|
43
|
+
this.origin = origin;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const membersOf = (ty) => ty instanceof Var ? ty.members : ty instanceof BlockOf ? ['Block'] : ty.name === AnyType.name ? null : ty.members;
|
|
47
|
+
const nameOf = (ty) => membersOf(ty)?.join(' | ') ?? 'Any';
|
|
48
|
+
// A variable is shown as what it's been narrowed to so far.
|
|
49
|
+
const show = (v) => (v.ty instanceof Var ? v.ty.phrase : v.shown);
|
|
50
|
+
const stepOrigin = (step) => ({
|
|
51
|
+
file: step.file,
|
|
52
|
+
at: (pos) => ({ line: step.line, column: step.column + pos }),
|
|
53
|
+
});
|
|
54
|
+
const fileOrigin = (v) => ({ file: v.file, at: (pos) => lineCol(v.src, pos) });
|
|
55
|
+
class Mistake extends Error {
|
|
56
|
+
pos;
|
|
57
|
+
}
|
|
58
|
+
class Scope {
|
|
59
|
+
parent;
|
|
60
|
+
builtins;
|
|
61
|
+
words = new Map();
|
|
62
|
+
constructor(parent, builtins = null) {
|
|
63
|
+
this.parent = parent;
|
|
64
|
+
this.builtins = builtins;
|
|
65
|
+
}
|
|
66
|
+
set(known) {
|
|
67
|
+
this.words.set(known.name.toLowerCase(), known);
|
|
68
|
+
}
|
|
69
|
+
lookup(name) {
|
|
70
|
+
const key = name.toLowerCase();
|
|
71
|
+
for (let s = this; s; s = s.parent) {
|
|
72
|
+
const known = s.words.get(key);
|
|
73
|
+
if (known)
|
|
74
|
+
return known;
|
|
75
|
+
const forms = s.builtins?.forms(name);
|
|
76
|
+
if (s.builtins && forms)
|
|
77
|
+
return { name: s.builtins.spellings.get(key) ?? name, kind: 'forms', forms };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
*names() {
|
|
81
|
+
for (let s = this; s; s = s.parent) {
|
|
82
|
+
for (const known of s.words.values())
|
|
83
|
+
yield known.name;
|
|
84
|
+
if (s.builtins)
|
|
85
|
+
yield* s.builtins.names();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Everything visible from here, nearest first, each name once.
|
|
89
|
+
all() {
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
return [...this.names()].flatMap((name) => {
|
|
92
|
+
const key = name.toLowerCase();
|
|
93
|
+
if (seen.has(key))
|
|
94
|
+
return [];
|
|
95
|
+
seen.add(key);
|
|
96
|
+
const known = this.lookup(name);
|
|
97
|
+
return known ? [known] : [];
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// The stack a statement runs on. An open stack belongs to an editor's guess
|
|
102
|
+
// at what comes next: reaching below its bottom takes a value that isn't
|
|
103
|
+
// written yet, which becomes one of its inputs. Otherwise the stack is
|
|
104
|
+
// closed; there's nothing below.
|
|
105
|
+
class Sim {
|
|
106
|
+
open;
|
|
107
|
+
stack = [];
|
|
108
|
+
inputs = [];
|
|
109
|
+
constructor(open) {
|
|
110
|
+
this.open = open;
|
|
111
|
+
}
|
|
112
|
+
// What's i from the top, or undefined if that's below the bottom.
|
|
113
|
+
peek(i) {
|
|
114
|
+
return this.stack[this.stack.length - 1 - i];
|
|
115
|
+
}
|
|
116
|
+
// A new value from the caller, for a slot below the bottom.
|
|
117
|
+
fromCaller(want) {
|
|
118
|
+
const v = new Var();
|
|
119
|
+
if (want) {
|
|
120
|
+
v.members = want.members;
|
|
121
|
+
v.phrase = want.phrase;
|
|
122
|
+
}
|
|
123
|
+
this.inputs.push(v);
|
|
124
|
+
return { ty: v, shown: v.phrase };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------- checking
|
|
128
|
+
export function check(files, options) {
|
|
129
|
+
return analyze(files, options).report;
|
|
130
|
+
}
|
|
131
|
+
// A checked project, which can also be asked about words and types, for
|
|
132
|
+
// an editor. Where a step or a line is given, it's the scope that step, or
|
|
133
|
+
// that line of a Define's body, sees: in a body, what its header takes.
|
|
134
|
+
export class Analysis {
|
|
135
|
+
report;
|
|
136
|
+
checker;
|
|
137
|
+
constructor(checker) {
|
|
138
|
+
this.checker = checker;
|
|
139
|
+
this.report = checker.report;
|
|
140
|
+
}
|
|
141
|
+
words(at) {
|
|
142
|
+
return this.checker
|
|
143
|
+
.scopeFor(at)
|
|
144
|
+
.all()
|
|
145
|
+
.map((known) => this.checker.info(known));
|
|
146
|
+
}
|
|
147
|
+
word(name, at) {
|
|
148
|
+
const known = this.checker.scopeFor(at).lookup(name);
|
|
149
|
+
return known && this.checker.info(known);
|
|
150
|
+
}
|
|
151
|
+
// What a value would need to be, written after the code in left: in
|
|
152
|
+
// "Fill the Field "Email" with ", Fill needs its text. null when it
|
|
153
|
+
// could be anything, or left can't be read.
|
|
154
|
+
expected(left, at) {
|
|
155
|
+
return this.checker.expected(left, this.checker.scopeFor(at));
|
|
156
|
+
}
|
|
157
|
+
// Where the Define is that a use of a word, at this place, runs.
|
|
158
|
+
chosen(file, line, column) {
|
|
159
|
+
return this.checker.chosen.get(placeKey({ file, line, column }))?.at;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const placeKey = (p) => `${p.file}:${p.line}:${p.column}`;
|
|
163
|
+
export function analyze(files, options) {
|
|
164
|
+
const checker = new Checker(options);
|
|
165
|
+
checker.check(files);
|
|
166
|
+
return new Analysis(checker);
|
|
167
|
+
}
|
|
168
|
+
class Checker {
|
|
169
|
+
options;
|
|
170
|
+
cwd;
|
|
171
|
+
defs = [];
|
|
172
|
+
report = { vocabulary: [], scenarios: new Map() };
|
|
173
|
+
vocabulary;
|
|
174
|
+
// what each step, and each line of a Define, sees, for an editor
|
|
175
|
+
stepScopes = new Map();
|
|
176
|
+
lineScopes = new Map();
|
|
177
|
+
// answering an editor, which wants a best guess rather than a mistake
|
|
178
|
+
guessing = false;
|
|
179
|
+
// which Define each use of a Defined word runs, by where the use is
|
|
180
|
+
chosen = new Map();
|
|
181
|
+
constructor(options) {
|
|
182
|
+
this.options = options;
|
|
183
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
184
|
+
}
|
|
185
|
+
check(files) {
|
|
186
|
+
// Every file's Defines share one scope, as they do when the tests run.
|
|
187
|
+
const vocabulary = new Scope(new Scope(null, this.options.words));
|
|
188
|
+
this.vocabulary = vocabulary;
|
|
189
|
+
for (const v of files.flatMap((f) => f.vocabulary)) {
|
|
190
|
+
const origin = fileOrigin(v);
|
|
191
|
+
try {
|
|
192
|
+
const sim = new Sim(false);
|
|
193
|
+
this.run(v.program, vocabulary, sim, origin);
|
|
194
|
+
this.settle(sim);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
this.report.vocabulary.push(this.diagnose(err, origin));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Defines are checked once all of them are known, since a Define can use
|
|
201
|
+
// one from a later file. Unused ones are checked too.
|
|
202
|
+
for (const def of this.defs) {
|
|
203
|
+
this.checkDefine(def);
|
|
204
|
+
if (def.error)
|
|
205
|
+
this.report.vocabulary.push(def.error);
|
|
206
|
+
}
|
|
207
|
+
this.overloads(vocabulary);
|
|
208
|
+
for (const { features } of files) {
|
|
209
|
+
for (const scenario of features.flatMap((f) => f.scenarios)) {
|
|
210
|
+
const problem = this.scenario(scenario, vocabulary);
|
|
211
|
+
if (problem)
|
|
212
|
+
this.report.scenarios.set(scenario, problem);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return this.report;
|
|
216
|
+
}
|
|
217
|
+
scenario(scenario, vocabulary) {
|
|
218
|
+
const scope = new Scope(vocabulary);
|
|
219
|
+
// The scenario's own stack, where each keyword leaves its mode.
|
|
220
|
+
const modes = new Sim(false);
|
|
221
|
+
for (const step of scenario.steps) {
|
|
222
|
+
const origin = stepOrigin(step);
|
|
223
|
+
this.stepScopes.set(step, scope);
|
|
224
|
+
try {
|
|
225
|
+
if (step.error !== undefined)
|
|
226
|
+
throw step.error;
|
|
227
|
+
// Its keyword takes the step, and the mode above it if it needs one.
|
|
228
|
+
modes.stack.push({ ty: StepType, shown: 'the step' });
|
|
229
|
+
try {
|
|
230
|
+
const forms = keywordForms(KEYWORDS, step.keywordType);
|
|
231
|
+
this.call(step.keyword.trim(), forms.map((f) => this.formWay(step.keyword.trim(), f)), modes);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (err instanceof Mistake)
|
|
235
|
+
err.pos = -step.keyword.length;
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
const sim = new Sim(false);
|
|
239
|
+
if (typeof step.argument === 'string')
|
|
240
|
+
sim.stack.push({ ty: TextType, shown: JSON.stringify(step.argument) });
|
|
241
|
+
else if (step.argument)
|
|
242
|
+
sim.stack.push({ ty: TableType, shown: 'the table' });
|
|
243
|
+
this.run(step.program ?? [], scope, sim, origin);
|
|
244
|
+
this.settle(sim);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
return this.diagnose(err, origin);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
settle(sim) {
|
|
252
|
+
// A noun's value is settled as what it is underneath.
|
|
253
|
+
const problem = this.options.settle(sim.stack.map((v) => ({ name: nameOf(isType(v.ty) ? root(v.ty) : v.ty), shown: show(v) })));
|
|
254
|
+
if (problem)
|
|
255
|
+
throw new Mistake(problem);
|
|
256
|
+
}
|
|
257
|
+
// ---------------------------------------------------------------- for editors
|
|
258
|
+
scopeFor(at) {
|
|
259
|
+
const scope = at && ('keyword' in at ? this.stepScopes.get(at) : this.lineScopes.get(`${at.file}:${at.line}`));
|
|
260
|
+
return scope ?? this.vocabulary;
|
|
261
|
+
}
|
|
262
|
+
info(known) {
|
|
263
|
+
const { name } = known;
|
|
264
|
+
if (known.kind === 'forms') {
|
|
265
|
+
return {
|
|
266
|
+
name,
|
|
267
|
+
kind: 'builtin',
|
|
268
|
+
signatures: known.forms.map((f) => signature(name, f)),
|
|
269
|
+
outputs: known.forms.map((f) => (f.output ? membersOf(f.output) : [])),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (known.kind === 'value') {
|
|
273
|
+
return {
|
|
274
|
+
name,
|
|
275
|
+
kind: 'value',
|
|
276
|
+
signatures: [`${name} : ${nameOf(known.val.ty)}`],
|
|
277
|
+
outputs: [membersOf(known.val.ty)],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const signatures = [];
|
|
281
|
+
const outputs = [];
|
|
282
|
+
for (const def of known.defs) {
|
|
283
|
+
this.checkDefine(def);
|
|
284
|
+
if (!def.outputs) {
|
|
285
|
+
signatures.push(`${name} : (has a mistake)`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const inputs = def.params.map((p) => p.type.name + (p.name !== p.type.name ? ` (${p.name})` : '')).join(', ');
|
|
289
|
+
const leaves = def.noun ? def.noun.name : def.outputs.map((o) => nameOf(o.ty)).join(', ');
|
|
290
|
+
signatures.push(`${name} : ${inputs}${inputs ? ' ' : ''}->${leaves ? ` ${leaves}` : ''}`);
|
|
291
|
+
const last = def.outputs.at(-1);
|
|
292
|
+
// A noun leaves its kind, and is offered where what it narrows is wanted.
|
|
293
|
+
outputs.push(...(def.noun ? [def.noun.members, root(def.noun).members] : [last ? membersOf(last.ty) : []]));
|
|
294
|
+
}
|
|
295
|
+
const places = known.defs.map((d) => d.at);
|
|
296
|
+
return { name, kind: 'define', signatures, outputs, at: places[0], places };
|
|
297
|
+
}
|
|
298
|
+
expected(left, scope) {
|
|
299
|
+
// Whatever's written next runs before left does, so left finds it on
|
|
300
|
+
// top of the stack: the first thing left takes from below is it.
|
|
301
|
+
let program;
|
|
302
|
+
try {
|
|
303
|
+
program = parse(left);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const statement = program.at(-1);
|
|
309
|
+
if (!statement)
|
|
310
|
+
return null;
|
|
311
|
+
const sim = new Sim(true);
|
|
312
|
+
this.guessing = true;
|
|
313
|
+
try {
|
|
314
|
+
this.run([statement], new Scope(scope), sim, { file: '', at: () => ({ line: 0, column: 0 }) });
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
this.guessing = false;
|
|
321
|
+
}
|
|
322
|
+
const next = sim.inputs[0];
|
|
323
|
+
return next ? { members: next.members, phrase: next.phrase } : null;
|
|
324
|
+
}
|
|
325
|
+
diagnose(err, origin, define) {
|
|
326
|
+
if (!(err instanceof Error))
|
|
327
|
+
throw err;
|
|
328
|
+
// Anything but a mistake in the .spp file is a bug in Spec++.
|
|
329
|
+
if (!(err instanceof Mistake) && err.name !== 'SppError')
|
|
330
|
+
throw err;
|
|
331
|
+
const { line, column } = origin.at(err.pos ?? 0);
|
|
332
|
+
return { file: origin.file, line, column, message: err.message, define };
|
|
333
|
+
}
|
|
334
|
+
// Statements run in order, each right to left, as they do for real.
|
|
335
|
+
run(program, scope, sim, origin) {
|
|
336
|
+
for (const statement of program) {
|
|
337
|
+
for (let n = statement.length - 1; n >= 0; n--) {
|
|
338
|
+
const node = statement[n];
|
|
339
|
+
try {
|
|
340
|
+
this.evaluate(node, scope, sim, origin);
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
if (err instanceof Mistake && err.pos === undefined)
|
|
344
|
+
err.pos = node.pos;
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
evaluate(node, scope, sim, origin) {
|
|
351
|
+
switch (node.type) {
|
|
352
|
+
case 'str':
|
|
353
|
+
return void sim.stack.push({ ty: TextType, shown: JSON.stringify(node.value) });
|
|
354
|
+
case 'num':
|
|
355
|
+
return void sim.stack.push({ ty: NumberType, shown: String(node.value) });
|
|
356
|
+
case 'sym':
|
|
357
|
+
return void sim.stack.push({ ty: SymbolType, shown: `\\${node.value}` });
|
|
358
|
+
case 'block':
|
|
359
|
+
return void sim.stack.push({ ty: new BlockOf(node.body, scope, origin), shown: 'a block' });
|
|
360
|
+
case 'define': {
|
|
361
|
+
const block = this.take(sim, `Define ${node.name}`);
|
|
362
|
+
if (!(block.ty instanceof BlockOf)) {
|
|
363
|
+
throw new Mistake(`Define ${node.name} expected an indented block after a colon, but got ${block.shown}`);
|
|
364
|
+
}
|
|
365
|
+
// Its header is checked with its body, once every Define is known,
|
|
366
|
+
// since it may take a noun Defined later.
|
|
367
|
+
const def = {
|
|
368
|
+
name: node.name,
|
|
369
|
+
block: block.ty,
|
|
370
|
+
at: { file: origin.file, ...origin.at(node.pos) },
|
|
371
|
+
header: node.params,
|
|
372
|
+
params: [],
|
|
373
|
+
state: 'unchecked',
|
|
374
|
+
};
|
|
375
|
+
this.defs.push(def);
|
|
376
|
+
const here = scope.words.get(node.name.toLowerCase());
|
|
377
|
+
if (here?.kind === 'define')
|
|
378
|
+
return void here.defs.push(def);
|
|
379
|
+
return scope.set({ name: node.name, kind: 'define', defs: [def] });
|
|
380
|
+
}
|
|
381
|
+
case 'word': {
|
|
382
|
+
const known = scope.lookup(node.name);
|
|
383
|
+
if (!known)
|
|
384
|
+
throw new Mistake(unknownWord(node.name, scope.names()).message);
|
|
385
|
+
if (known.kind === 'value')
|
|
386
|
+
return void sim.stack.push(known.val);
|
|
387
|
+
if (known.kind === 'forms')
|
|
388
|
+
return this.call(node.name, known.forms.map((f) => this.formWay(node.name, f)), sim);
|
|
389
|
+
const way = this.call(node.name, this.defineWays(known.defs), sim);
|
|
390
|
+
if (way.def && !this.guessing)
|
|
391
|
+
this.chosen.set(placeKey({ file: origin.file, ...origin.at(node.pos) }), way.def);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// What a Define's header takes, checked.
|
|
397
|
+
params(def) {
|
|
398
|
+
const same = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
399
|
+
return def.header.map((p, i) => {
|
|
400
|
+
const at = (message) => Object.assign(new SppError(message), { pos: p.pos });
|
|
401
|
+
if (def.header.findIndex((q) => same(q.name, p.name)) < i) {
|
|
402
|
+
throw at(`Two things are called ${p.name}. Name them, as in ${p.kind} (Source) and ${p.kind} (Target)`);
|
|
403
|
+
}
|
|
404
|
+
// The name hides the word in the body. That's the point for a noun
|
|
405
|
+
// taken by its own name, a Cart called Cart; anything else is a mistake.
|
|
406
|
+
const hidden = def.block.scope.lookup(p.name);
|
|
407
|
+
if (hidden && !(hidden.kind === 'define' && same(p.name, p.kind))) {
|
|
408
|
+
throw at(same(p.name, p.kind)
|
|
409
|
+
? `${p.name} is already a word, so name this one, as in ${p.kind} (Item)`
|
|
410
|
+
: `${p.name} is already a word, so call this one something else`);
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
return { name: p.name, type: this.kindOf(p.kind, def.block.scope) };
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
if (err instanceof Mistake && err.pos === undefined)
|
|
417
|
+
err.pos = p.pos;
|
|
418
|
+
throw err;
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
// A kind a header names: a built-in one, or a noun's.
|
|
423
|
+
kindOf(name, scope) {
|
|
424
|
+
const builtin = this.options.words.kindOf(name);
|
|
425
|
+
if (builtin)
|
|
426
|
+
return builtin;
|
|
427
|
+
const known = scope.lookup(name);
|
|
428
|
+
if (known?.kind !== 'define')
|
|
429
|
+
throw new Mistake(notAKind(name, this.options.words.builtinKinds()));
|
|
430
|
+
if (known.defs.length > 1)
|
|
431
|
+
throw new Mistake(`${name} can't be a kind: a noun has one Define, and it has ${known.defs.length}`);
|
|
432
|
+
const [def] = known.defs;
|
|
433
|
+
this.checkDefine(def);
|
|
434
|
+
if (def.error)
|
|
435
|
+
throw new Mistake(`${def.name} can't be a kind: its Define has a mistake, at ${this.where(def.error)}`);
|
|
436
|
+
if (!def.noun) {
|
|
437
|
+
throw new Mistake(`${def.name} can't be a kind: only a noun can, a Define that leaves one thing, like something on the page`);
|
|
438
|
+
}
|
|
439
|
+
return def.noun;
|
|
440
|
+
}
|
|
441
|
+
// The kind a Define makes of what it leaves, if it's a noun: one thing, of
|
|
442
|
+
// a built-in kind other than Any. The runtime decides the same, from values.
|
|
443
|
+
nounOf(def, outputs) {
|
|
444
|
+
if (outputs.length !== 1 || !isType(outputs[0].ty))
|
|
445
|
+
return;
|
|
446
|
+
const have = root(outputs[0].ty).members;
|
|
447
|
+
const base = [...this.options.words.builtinKinds()].find((k) => k !== AnyType && have.length > 0 && have.every((m) => k.members.includes(m)));
|
|
448
|
+
if (!base)
|
|
449
|
+
return;
|
|
450
|
+
return { name: def.name, phrase: article(def.name), members: [def.name], base, test: (v) => false };
|
|
451
|
+
}
|
|
452
|
+
// Each word's Defines, two at a time: a noun can't share its name, since
|
|
453
|
+
// it's a kind, and two that could always both fit leave a use guessing.
|
|
454
|
+
overloads(vocabulary) {
|
|
455
|
+
for (const known of vocabulary.words.values()) {
|
|
456
|
+
if (known.kind !== 'define')
|
|
457
|
+
continue;
|
|
458
|
+
const defs = known.defs.filter((d) => !d.error);
|
|
459
|
+
for (const [j, b] of defs.entries()) {
|
|
460
|
+
const a = defs.slice(0, j).find((a) => a.noun || b.noun || clash(a, b));
|
|
461
|
+
if (!a)
|
|
462
|
+
continue;
|
|
463
|
+
const where = this.where(a.at);
|
|
464
|
+
const message = a.noun || b.noun
|
|
465
|
+
? `${b.name} is a noun, so it can only have one Define, and there's another at ${where}`
|
|
466
|
+
: clash(a, b) === 'same'
|
|
467
|
+
? `${b.name} takes the same things here as at ${where}, so there'd be no telling which one a step means`
|
|
468
|
+
: `${b.name} here and at ${where} could both take the same things, and neither takes narrower kinds, so there'd be no telling which one a step means`;
|
|
469
|
+
b.error = { ...b.at, message, define: b.name };
|
|
470
|
+
this.report.vocabulary.push(b.error);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
where(d) {
|
|
475
|
+
return `${path.relative(this.cwd, d.file)}:${d.line}:${d.column}`;
|
|
476
|
+
}
|
|
477
|
+
take(sim, word) {
|
|
478
|
+
const v = sim.stack.pop();
|
|
479
|
+
if (v)
|
|
480
|
+
return v;
|
|
481
|
+
if (sim.open)
|
|
482
|
+
return sim.fromCaller();
|
|
483
|
+
throw new Mistake(`${word} expected a value, but there was nothing left`);
|
|
484
|
+
}
|
|
485
|
+
formWay(name, form) {
|
|
486
|
+
return {
|
|
487
|
+
inputs: form.inputs.map((t) => ({ members: membersOf(t), phrase: t.phrase, key: t })),
|
|
488
|
+
outputs: (args) => (form.output ? [{ ty: form.output, shown: [name, ...args.map(show)].join(' ') }] : []),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
// A Define's way: what its header takes, and what its body leaves.
|
|
492
|
+
// A word's Defines, most particular first, as the runtime orders them.
|
|
493
|
+
defineWays(defs) {
|
|
494
|
+
return defs
|
|
495
|
+
.map((def) => ({ def, way: this.defineWay(def) }))
|
|
496
|
+
.sort((a, b) => b.way.inputs.length - a.way.inputs.length || specificity(b.def) - specificity(a.def))
|
|
497
|
+
.map(({ way }) => way);
|
|
498
|
+
}
|
|
499
|
+
defineWay(def) {
|
|
500
|
+
this.checkDefine(def);
|
|
501
|
+
if (def.error) {
|
|
502
|
+
throw new Mistake(`${def.name} can't be used: its Define has a mistake, at ${this.where(def.error)}: ${def.error.message}`);
|
|
503
|
+
}
|
|
504
|
+
const { outputs, noun } = def;
|
|
505
|
+
const inputs = def.params.map((p) => ({ members: membersOf(p.type), phrase: p.type.phrase, key: p }));
|
|
506
|
+
if (noun)
|
|
507
|
+
return { inputs, outputs: (args) => [{ ty: noun, shown: [def.name, ...args.map(show)].join(' ') }], def };
|
|
508
|
+
// A vocabulary bug: checkDefine says what a Define leaves.
|
|
509
|
+
if (!outputs)
|
|
510
|
+
throw new Error(`Checked ${def.name}'s Define, but it leaves nothing known`);
|
|
511
|
+
return { inputs, outputs: () => outputs, def };
|
|
512
|
+
}
|
|
513
|
+
// Runs a Define's body with what its header takes, by name, and nothing
|
|
514
|
+
// below that.
|
|
515
|
+
checkDefine(def) {
|
|
516
|
+
if (def.state === 'done')
|
|
517
|
+
return;
|
|
518
|
+
if (def.state === 'checking')
|
|
519
|
+
throw new Mistake(`${def.name} uses itself, so it would never finish`);
|
|
520
|
+
def.state = 'checking';
|
|
521
|
+
try {
|
|
522
|
+
def.params = this.params(def);
|
|
523
|
+
const scope = new Scope(def.block.scope);
|
|
524
|
+
for (const p of def.params)
|
|
525
|
+
scope.set({ name: p.name, kind: 'value', val: { ty: p.type, shown: p.name } });
|
|
526
|
+
// Its header and body see what the header takes.
|
|
527
|
+
const { origin } = def.block;
|
|
528
|
+
for (const line of [def.at.line, ...def.block.body.flatMap((s) => (s[0] ? [origin.at(s[0].pos).line] : []))]) {
|
|
529
|
+
this.lineScopes.set(`${origin.file}:${line}`, scope);
|
|
530
|
+
}
|
|
531
|
+
const sim = new Sim(false);
|
|
532
|
+
this.run(def.block.body, scope, sim, def.block.origin);
|
|
533
|
+
def.outputs = sim.stack;
|
|
534
|
+
def.noun = this.nounOf(def, sim.stack);
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
def.error = this.diagnose(err, def.block.origin, def.name);
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
def.state = 'done';
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Calls the way the runtime would pick: the first whose inputs are on top
|
|
544
|
+
// of the stack.
|
|
545
|
+
call(name, ways, sim) {
|
|
546
|
+
const fits = ways.flatMap((way) => {
|
|
547
|
+
const fit = this.fit(way, sim);
|
|
548
|
+
return fit ? [{ way, ...fit }] : [];
|
|
549
|
+
});
|
|
550
|
+
if (!fits.length)
|
|
551
|
+
throw this.mismatch(name, ways, sim);
|
|
552
|
+
// On an open stack, a form with an optional last input, like a query's
|
|
553
|
+
// Scope, would always fit by taking it from below, or by deciding a
|
|
554
|
+
// variable must be one. Unless it's known to be there, prefer the form
|
|
555
|
+
// without it.
|
|
556
|
+
const shorter = fits.filter((f) => !fits.some((g) => g !== f && g.unsure < f.unsure && isPrefix(g.way.inputs, f.way.inputs)));
|
|
557
|
+
let [chosen] = shorter;
|
|
558
|
+
const known = chosen.fresh === 0 && chosen.binds.size === 0;
|
|
559
|
+
if (shorter.length > 1 && !known) {
|
|
560
|
+
if (this.guessing) {
|
|
561
|
+
// For an editor's guess, any of them will do: each input may be any
|
|
562
|
+
// of what they'd take there.
|
|
563
|
+
const way = anyOf(shorter.map((f) => f.way));
|
|
564
|
+
const fit = this.fit(way, sim);
|
|
565
|
+
// A checker bug: each of them fits, so any of them together does.
|
|
566
|
+
if (!fit)
|
|
567
|
+
throw new Error(`${name}'s forms each fit, but not together`);
|
|
568
|
+
chosen = { way, ...fit };
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
const alternatives = [...new Set(shorter.map((f) => f.way.inputs.map((w) => w.phrase).join(' and ')))];
|
|
572
|
+
throw new Mistake(`Can't tell how ${name} is being used here: it could take ${alternatives.join(', or ')}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
for (const [v, want] of chosen.binds) {
|
|
576
|
+
v.members = want.members;
|
|
577
|
+
v.phrase = want.phrase;
|
|
578
|
+
}
|
|
579
|
+
const n = chosen.way.inputs.length;
|
|
580
|
+
const args = chosen.way.inputs.map((want, i) => sim.peek(i) ?? sim.fromCaller(want));
|
|
581
|
+
sim.stack.length = Math.max(0, sim.stack.length - n);
|
|
582
|
+
sim.stack.push(...chosen.way.outputs(args));
|
|
583
|
+
return chosen.way;
|
|
584
|
+
}
|
|
585
|
+
// Whether a way's inputs are on top of the stack, and what that would
|
|
586
|
+
// narrow the stack's variables to. fresh counts inputs taken from a caller;
|
|
587
|
+
// unsure counts those plus inputs that are variables.
|
|
588
|
+
fit(way, sim) {
|
|
589
|
+
const binds = new Map();
|
|
590
|
+
let fresh = 0;
|
|
591
|
+
let unsure = 0;
|
|
592
|
+
for (let i = 0; i < way.inputs.length; i++) {
|
|
593
|
+
const want = way.inputs[i];
|
|
594
|
+
const v = sim.peek(i);
|
|
595
|
+
if (!v) {
|
|
596
|
+
if (!sim.open)
|
|
597
|
+
return null;
|
|
598
|
+
fresh++;
|
|
599
|
+
unsure++;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (v.ty instanceof Var)
|
|
603
|
+
unsure++;
|
|
604
|
+
const wanted = want.members;
|
|
605
|
+
if (wanted === null)
|
|
606
|
+
continue;
|
|
607
|
+
if (v.ty instanceof Var) {
|
|
608
|
+
const now = binds.get(v.ty)?.members ?? v.ty.members;
|
|
609
|
+
const narrowed = now === null ? wanted : now.filter((m) => wanted.includes(m));
|
|
610
|
+
if (!narrowed.length)
|
|
611
|
+
return null;
|
|
612
|
+
binds.set(v.ty, { ...want, members: narrowed });
|
|
613
|
+
}
|
|
614
|
+
else if (!accepts(want, v))
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
return { binds, fresh, unsure };
|
|
618
|
+
}
|
|
619
|
+
// Says what was wanted at the first place every way had given up, as the
|
|
620
|
+
// runtime does.
|
|
621
|
+
mismatch(name, ways, sim) {
|
|
622
|
+
let live = ways;
|
|
623
|
+
for (let i = 0;; i++) {
|
|
624
|
+
const here = live.filter((w) => w.inputs.length > i);
|
|
625
|
+
const v = sim.peek(i);
|
|
626
|
+
const ok = v ? here.filter((w) => accepts(w.inputs[i], v)) : [];
|
|
627
|
+
if (!ok.length || !here.length) {
|
|
628
|
+
const wanted = [...new Set(here.map((w) => w.inputs[i].phrase))].join(', or ') || 'something else';
|
|
629
|
+
return new Mistake(`${name} expected ${wanted}, but ${v ? `got ${show(v)}` : 'there was nothing left'}`);
|
|
630
|
+
}
|
|
631
|
+
live = ok;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function accepts(want, v) {
|
|
636
|
+
const have = membersOf(v.ty);
|
|
637
|
+
const wanted = want.members;
|
|
638
|
+
if (wanted === null || have === null)
|
|
639
|
+
return true;
|
|
640
|
+
if (v.ty instanceof Var)
|
|
641
|
+
return have.some((m) => wanted.includes(m));
|
|
642
|
+
if (have.every((m) => wanted.includes(m)))
|
|
643
|
+
return true;
|
|
644
|
+
// A Cart is an Element too.
|
|
645
|
+
return isType(v.ty) && !!v.ty.base && accepts(want, { ...v, ty: v.ty.base });
|
|
646
|
+
}
|
|
647
|
+
const isType = (ty) => !(ty instanceof Var) && !(ty instanceof BlockOf);
|
|
648
|
+
// Whether two Defines of a word could always both fit: 'same' if they take
|
|
649
|
+
// the same kinds, 'ambiguous' if each is narrower somewhere. They can't both
|
|
650
|
+
// fit if they take different numbers of things, or if at some place neither
|
|
651
|
+
// kind is within the other; and if one is narrower throughout, it's chosen.
|
|
652
|
+
function clash(a, b) {
|
|
653
|
+
if (a.params.length !== b.params.length)
|
|
654
|
+
return null;
|
|
655
|
+
let aNarrower = false;
|
|
656
|
+
let bNarrower = false;
|
|
657
|
+
for (const [i, p] of a.params.entries()) {
|
|
658
|
+
const x = p.type;
|
|
659
|
+
const y = b.params[i].type;
|
|
660
|
+
const xInY = within(x, y);
|
|
661
|
+
const yInX = within(y, x);
|
|
662
|
+
if (xInY && yInX)
|
|
663
|
+
continue;
|
|
664
|
+
if (!xInY && !yInX)
|
|
665
|
+
return null;
|
|
666
|
+
if (xInY)
|
|
667
|
+
aNarrower = true;
|
|
668
|
+
else
|
|
669
|
+
bNarrower = true;
|
|
670
|
+
}
|
|
671
|
+
return !aNarrower && !bNarrower ? 'same' : aNarrower && bNarrower ? 'ambiguous' : null;
|
|
672
|
+
}
|
|
673
|
+
// Whether every value of kind x is one of kind y: a Cart is an Element, and
|
|
674
|
+
// everything is Any.
|
|
675
|
+
function within(x, y) {
|
|
676
|
+
if (y.name === AnyType.name || x.name.toLowerCase() === y.name.toLowerCase())
|
|
677
|
+
return true;
|
|
678
|
+
return !!x.base && within(x.base, y);
|
|
679
|
+
}
|
|
680
|
+
// What a noun's kind is underneath: Element, for a Cart.
|
|
681
|
+
const root = (t) => (t.base ? root(t.base) : t);
|
|
682
|
+
function anyOf(ways) {
|
|
683
|
+
const length = Math.max(...ways.map((w) => w.inputs.length));
|
|
684
|
+
const inputs = Array.from({ length }, (_, i) => {
|
|
685
|
+
const here = ways.flatMap((w) => (w.inputs[i] ? [w.inputs[i]] : []));
|
|
686
|
+
// Any value, if any of them takes any value.
|
|
687
|
+
const known = here.flatMap((w) => (w.members === null ? [] : [w.members]));
|
|
688
|
+
const members = known.length < here.length ? null : [...new Set(known.flat())];
|
|
689
|
+
return { members, phrase: [...new Set(here.map((w) => w.phrase))].join(', or '), key: undefined };
|
|
690
|
+
});
|
|
691
|
+
return { inputs, outputs: ways[0].outputs };
|
|
692
|
+
}
|
|
693
|
+
function isPrefix(short, long) {
|
|
694
|
+
return short.length < long.length && short.every((w, i) => w.key === long[i].key);
|
|
695
|
+
}
|
|
696
|
+
// ---------------------------------------------------------------- output
|
|
697
|
+
// A diagnostic as a person reads it: where, the line, and what's wrong.
|
|
698
|
+
export function format(d, source, cwd = process.cwd()) {
|
|
699
|
+
const text = source.split('\n')[d.line - 1] ?? '';
|
|
700
|
+
const indent = text.length - text.trimStart().length;
|
|
701
|
+
const caret = `${' '.repeat(Math.max(0, d.column - 1 - indent))}^`;
|
|
702
|
+
const where = d.define ? ` (in Define ${d.define})` : '';
|
|
703
|
+
return [
|
|
704
|
+
`${path.relative(cwd, d.file)}:${d.line}:${d.column}${where}`,
|
|
705
|
+
` ${text.trim()}`,
|
|
706
|
+
` ${caret}`,
|
|
707
|
+
` ${d.message}`,
|
|
708
|
+
].join('\n');
|
|
709
|
+
}
|