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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -2
  3. package/dist/bin/spp-lang.d.ts +2 -0
  4. package/dist/bin/spp-lang.js +214 -0
  5. package/dist/src/backends/playwright/generate.d.ts +13 -0
  6. package/dist/src/backends/playwright/generate.js +238 -0
  7. package/dist/src/backends/playwright/runner/playwright.config.d.ts +2 -0
  8. package/dist/src/backends/playwright/runner/playwright.config.js +16 -0
  9. package/dist/src/backends/playwright/runtime.d.ts +14 -0
  10. package/dist/src/backends/playwright/runtime.js +158 -0
  11. package/dist/src/backends/playwright/words.d.ts +11 -0
  12. package/dist/src/backends/playwright/words.js +129 -0
  13. package/dist/src/config.d.ts +18 -0
  14. package/dist/src/config.js +63 -0
  15. package/dist/src/index.d.ts +22 -0
  16. package/dist/src/index.js +12 -0
  17. package/dist/src/language/check.d.ts +178 -0
  18. package/dist/src/language/check.js +709 -0
  19. package/dist/src/language/cognate.d.ts +137 -0
  20. package/dist/src/language/cognate.js +570 -0
  21. package/dist/src/language/load.d.ts +58 -0
  22. package/dist/src/language/load.js +200 -0
  23. package/dist/src/language/markdown.d.ts +9 -0
  24. package/dist/src/language/markdown.js +52 -0
  25. package/dist/src/language/sections.d.ts +19 -0
  26. package/dist/src/language/sections.js +119 -0
  27. package/dist/src/language/steps.d.ts +19 -0
  28. package/dist/src/language/steps.js +91 -0
  29. package/dist/src/language/types.d.ts +32 -0
  30. package/dist/src/language/types.js +51 -0
  31. package/dist/src/tools/dictionary.d.ts +3 -0
  32. package/dist/src/tools/dictionary.js +89 -0
  33. package/dist/src/tools/lsp.d.ts +1 -0
  34. package/dist/src/tools/lsp.js +479 -0
  35. package/dist/src/words/driver.d.ts +39 -0
  36. package/dist/src/words/driver.js +9 -0
  37. package/dist/src/words/shared.d.ts +56 -0
  38. package/dist/src/words/shared.js +272 -0
  39. 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
+ }