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,137 @@
1
+ import type { Context, Form, Type } from './types.ts';
2
+ export declare class SppError extends Error {
3
+ pos?: number;
4
+ constructor(message: string, pos?: number);
5
+ }
6
+ type Located = {
7
+ pos: number;
8
+ };
9
+ export type Node = Located & ({
10
+ type: 'num';
11
+ value: number;
12
+ } | {
13
+ type: 'str';
14
+ value: string;
15
+ } | {
16
+ type: 'sym';
17
+ value: string;
18
+ } | {
19
+ type: 'word';
20
+ name: string;
21
+ } | {
22
+ type: 'define';
23
+ name: string;
24
+ params: Param[];
25
+ } | {
26
+ type: 'block';
27
+ body: Program;
28
+ });
29
+ export type Param = Located & {
30
+ kind: string;
31
+ name: string;
32
+ };
33
+ export type Statement = Node[];
34
+ export type Program = Statement[];
35
+ type Lexeme = {
36
+ type: 'num';
37
+ value: number;
38
+ } | {
39
+ type: 'str';
40
+ value: string;
41
+ } | {
42
+ type: 'sym';
43
+ value: string;
44
+ } | {
45
+ type: 'word';
46
+ name: string;
47
+ } | {
48
+ type: ':';
49
+ } | {
50
+ type: '(';
51
+ } | {
52
+ type: ')';
53
+ };
54
+ export type Token = Lexeme & Located & {
55
+ length: number;
56
+ line: number;
57
+ indent: number;
58
+ };
59
+ export declare function lex(src: string): Token[];
60
+ export declare function parse(src: string): Program;
61
+ export declare class Block {
62
+ body: Program;
63
+ env: Env;
64
+ constructor(body: Program, env: Env);
65
+ toString(): string;
66
+ }
67
+ export declare class Sym {
68
+ name: string;
69
+ constructor(name: string);
70
+ toString(): string;
71
+ }
72
+ export declare class Definition {
73
+ name: string;
74
+ block: Block;
75
+ header: Param[];
76
+ private resolved?;
77
+ constructor(name: string, block: Block, header: Param[]);
78
+ get params(): {
79
+ name: string;
80
+ type: Type;
81
+ }[];
82
+ }
83
+ export declare class Definitions {
84
+ list: Definition[];
85
+ constructor(first: Definition);
86
+ ordered(): Definition[];
87
+ }
88
+ export declare const specificity: (d: {
89
+ params: {
90
+ type: Type;
91
+ }[];
92
+ }) => number;
93
+ export declare class Kinded implements Describable {
94
+ kind: string;
95
+ value: unknown;
96
+ constructor(kind: string, value: unknown);
97
+ describe(): string;
98
+ }
99
+ export declare const unwrap: (v: unknown) => unknown;
100
+ export interface Describable {
101
+ describe(): string;
102
+ }
103
+ export declare function describe(v: unknown): string;
104
+ type Word = readonly Form[] | Definitions | {
105
+ value: unknown;
106
+ };
107
+ export declare class Env {
108
+ parent: Env | null;
109
+ words: Map<string, Word>;
110
+ spellings: Map<string, string>;
111
+ kinds: Map<string, Type<unknown>>;
112
+ constructor(parent?: Env | null);
113
+ word(name: string, ...forms: Form[]): this;
114
+ def(name: string, definition: Definition): this;
115
+ kind(t: Type): this;
116
+ kindOf(name: string): Type | undefined;
117
+ builtinKinds(): Generator<Type>;
118
+ bind(name: string, value: unknown): this;
119
+ private set;
120
+ lookup(name: string): Word | undefined;
121
+ forms(name: string): readonly Form[] | undefined;
122
+ names(): Generator<string>;
123
+ }
124
+ export declare class Stack extends Array<unknown> {
125
+ take<T>(word: string, t: Type<T>): T;
126
+ }
127
+ export declare function run(program: Program, env: Env, stack: Stack, ctx: Context): Promise<void>;
128
+ export declare function call(block: Block, stack: Stack, ctx: Context, args?: Record<string, unknown>): Promise<void>;
129
+ export declare function resolve(params: Param[], env: Env): {
130
+ name: string;
131
+ type: Type;
132
+ }[];
133
+ export declare function notAKind(name: string, builtins: Iterable<Type>): string;
134
+ export declare function apply(name: string, forms: readonly Form[], stack: Stack, ctx: Context): Promise<void>;
135
+ export declare function unknownWord(name: string, known: Iterable<string>): SppError;
136
+ export declare function core(): Env;
137
+ export {};
@@ -0,0 +1,570 @@
1
+ // A small Cognate-flavoured interpreter.
2
+ //
3
+ // Cognate reads like prose because only capitalised words are code: lowercase
4
+ // words are comments, and statements run right to left, so a verb written
5
+ // first runs last, after everything it acts on has been evaluated.
6
+ //
7
+ // Click the Button "Sign in"
8
+ //
9
+ // pushes "Sign in", Button turns it into a locator, Click clicks it. That is
10
+ // the whole trick Spec++ is built on.
11
+ //
12
+ // Lexical rules follow CognaC (cognate-lang/cognate, src/lexer.l), except:
13
+ //
14
+ // - Commas, colons and full stops standing on their own are prose
15
+ // punctuation and skipped, so "Then I See "Welcome"." still parses.
16
+ // - Each line is one statement; there are no semicolons.
17
+ // - Blocks are a colon ending a line plus an indented body, as in Gherkin,
18
+ // not parentheses. Define is a section keyword, like Gherkin's: its
19
+ // colon comes first, "Define: Add", and its body is indented under it.
20
+ // - Comments are Gherkin's: a line starting with #.
21
+ import { AnyType, article, formsFor, NumberType, signature, TableType, TextType, type } from './types.js';
22
+ export class SppError extends Error {
23
+ pos;
24
+ constructor(message, pos) {
25
+ super(message);
26
+ this.name = 'SppError';
27
+ this.pos = pos;
28
+ }
29
+ }
30
+ // ---------------------------------------------------------------- lexing
31
+ const IDENT = /[A-Z\-?!+/*><=^.][A-Za-z0-9\-?!'+/*>=<^.]*/y;
32
+ const INFORMAL = /[a-z][a-z0-9'\-?!.]*/y;
33
+ const NUMBER = /-?(?:[1-9][0-9]*|[0-9])(?:\.[0-9]+)?(?:e-?[0-9]+)?(?=[\s;,:]|$)/y;
34
+ const SYMBOL = /\\[A-Za-z0-9\-?!'+/*>=<^.]+/y;
35
+ const STRING = /"((?:\\[abfnrtv\\"]|[^\n"\\])*)"/y;
36
+ const PROSE = /[,:.]+(?=[\s;]|$)/y;
37
+ // a colon that opens a block is the last thing on its line
38
+ const LINE_END = /[ \t]*(?:\n|$)/y;
39
+ const ESCAPES = {
40
+ a: '\x07',
41
+ b: '\b',
42
+ f: '\f',
43
+ n: '\n',
44
+ r: '\r',
45
+ t: '\t',
46
+ v: '\v',
47
+ '\\': '\\',
48
+ '"': '"',
49
+ };
50
+ export function lex(src) {
51
+ const tokens = [];
52
+ let i = 0;
53
+ let line = 0;
54
+ let lineStart = 0;
55
+ const at = (re, from = i) => {
56
+ re.lastIndex = from;
57
+ return re.exec(src);
58
+ };
59
+ const push = (t, length) => {
60
+ const indent = src.slice(lineStart, i).search(/[^ \t]|$/);
61
+ tokens.push({ ...t, pos: i, length, line, indent });
62
+ };
63
+ while (i < src.length) {
64
+ const c = src[i];
65
+ if (c === '\n') {
66
+ i++;
67
+ line++;
68
+ lineStart = i;
69
+ continue;
70
+ }
71
+ if (/\s/.test(c)) {
72
+ i++;
73
+ continue;
74
+ }
75
+ // A comment, as in Gherkin: a line whose first character is #.
76
+ if (c === '#' && !src.slice(lineStart, i).trim()) {
77
+ const nl = src.indexOf('\n', i);
78
+ i = nl === -1 ? src.length : nl;
79
+ continue;
80
+ }
81
+ if (c === ';') {
82
+ // Silently skipping it would run "A; B" as one statement, right to left.
83
+ throw new SppError('One statement per line: put what comes after the ; on a line of its own', i);
84
+ }
85
+ if (c === '(' || c === ')') {
86
+ // Only a Define's header has them; the parser says so anywhere else.
87
+ push({ type: c }, 1);
88
+ i++;
89
+ continue;
90
+ }
91
+ const string = at(STRING);
92
+ if (string) {
93
+ push({ type: 'str', value: string[1].replace(/\\(.)/g, (_, e) => ESCAPES[e]) }, string[0].length);
94
+ i += string[0].length;
95
+ continue;
96
+ }
97
+ const number = at(NUMBER);
98
+ if (number) {
99
+ push({ type: 'num', value: Number(number[0]) }, number[0].length);
100
+ i += number[0].length;
101
+ continue;
102
+ }
103
+ const prose = at(PROSE);
104
+ if (prose) {
105
+ // Punctuation between words isn't code, unless it's the colon that
106
+ // ends a line; the parser decides whether that opens a block.
107
+ if (prose[0].endsWith(':') && at(LINE_END, i + prose[0].length))
108
+ push({ type: ':' }, prose[0].length);
109
+ i += prose[0].length;
110
+ continue;
111
+ }
112
+ const symbol = at(SYMBOL);
113
+ if (symbol) {
114
+ push({ type: 'sym', value: symbol[0].slice(1).toLowerCase() }, symbol[0].length);
115
+ i += symbol[0].length;
116
+ continue;
117
+ }
118
+ // a lowercase word: a comment
119
+ const informal = at(INFORMAL);
120
+ if (informal) {
121
+ i += informal[0].length;
122
+ continue;
123
+ }
124
+ const m = at(IDENT);
125
+ if (m) {
126
+ // "Sign-in," or "Visit." — let trailing prose punctuation go
127
+ const name = m[0].replace(/[,:.]+$/, '') || m[0];
128
+ push({ type: 'word', name }, name.length);
129
+ i += name.length;
130
+ // The colon in "Define: Add" is a keyword's, as in "Feature:", so it's
131
+ // kept for the parser wherever it is on the line.
132
+ if (/^define$/i.test(name) && src[i] === ':') {
133
+ push({ type: ':' }, 1);
134
+ i++;
135
+ }
136
+ continue;
137
+ }
138
+ if (c === '"')
139
+ throw new SppError('Unclosed string', i);
140
+ throw new SppError(`Unexpected character ${JSON.stringify(c)}`, i);
141
+ }
142
+ return tokens;
143
+ }
144
+ // A program is a list of statements, one per line; a statement is a list of
145
+ // nodes; a block node holds a program of its own. A line ending in a colon
146
+ // takes the more-indented lines after it as a block, placed at the end of
147
+ // that line.
148
+ export function parse(src) {
149
+ const lines = [];
150
+ for (const t of lex(src)) {
151
+ let last = lines.at(-1);
152
+ if (last?.number !== t.line) {
153
+ last = { number: t.line, indent: t.indent, tokens: [] };
154
+ lines.push(last);
155
+ }
156
+ last.tokens.push(t);
157
+ }
158
+ let i = 0;
159
+ function body(indent) {
160
+ const statements = [];
161
+ while (i < lines.length && lines[i].indent >= indent) {
162
+ const line = lines[i++];
163
+ if (line.indent > indent)
164
+ throw new SppError('Unexpected indentation', line.tokens[0].pos);
165
+ const last = line.tokens.at(-1);
166
+ const define = isDefine(line.tokens) ? line.tokens[1] : null;
167
+ const colon = define ?? (last?.type === ':' ? last : null);
168
+ const statement = statementOf(line.tokens);
169
+ // A trailing colon with nothing indented under it was just prose; a
170
+ // Define with nothing under it is a mistake.
171
+ const next = lines[i];
172
+ if (colon && next && next.indent > indent) {
173
+ statement.push({ type: 'block', body: body(next.indent), pos: colon.pos });
174
+ }
175
+ else if (define) {
176
+ throw new SppError(`Define: ${statement[0].type === 'define' ? statement[0].name : ''} needs an indented body under it`, line.tokens[0].pos);
177
+ }
178
+ if (statement.length)
179
+ statements.push(statement);
180
+ }
181
+ return statements;
182
+ }
183
+ const program = body(lines[0]?.indent ?? 0);
184
+ if (i < lines.length)
185
+ throw new SppError('Unexpected indentation', lines[i].tokens[0].pos);
186
+ return program;
187
+ }
188
+ // "Define: Add", a Define's header: a section keyword, like "Feature:".
189
+ const isDefine = (tokens) => tokens[0]?.type === 'word' && /^define$/i.test(tokens[0].name) && tokens[1]?.type === ':';
190
+ function statementOf(tokens) {
191
+ const first = tokens[0];
192
+ if (isDefine(tokens)) {
193
+ const name = tokens[2];
194
+ if (name?.type !== 'word')
195
+ throw new SppError('Define: needs a Capitalised name after it', first.pos);
196
+ return [{ type: 'define', name: name.name, params: paramsOf(tokens.slice(3)), pos: first.pos }];
197
+ }
198
+ const statement = [];
199
+ for (let n = 0; n < tokens.length; n++) {
200
+ const t = tokens[n];
201
+ if (t.type === ':')
202
+ continue;
203
+ if (t.type === '(' || t.type === ')') {
204
+ throw new SppError('( ) only name what a Define takes, as in "Define: Move Cart (Source)"; blocks are indented under a colon', t.pos);
205
+ }
206
+ if (t.type === 'word' && /^define$/i.test(t.name)) {
207
+ const next = tokens[n + 1];
208
+ if (n > 0 || next?.type === ':')
209
+ throw new SppError('Define: starts a line of its own, with its body indented under it', t.pos);
210
+ // The old way, "Define Add:".
211
+ throw new SppError(`Write Define: ${next?.type === 'word' ? next.name : 'Name'}, with the colon after Define, as in Feature:`, t.pos);
212
+ }
213
+ statement.push(t);
214
+ }
215
+ return statement;
216
+ }
217
+ // What a Define takes, after its name: each capitalised word is a kind, and
218
+ // a word in ( ) after one names it. Lowercase words are prose, as anywhere.
219
+ function paramsOf(tokens) {
220
+ const params = [];
221
+ const wrong = (t) => new SppError("After a Define's name come the kinds it takes, like Text or Element, each maybe named in ( )", t.pos);
222
+ for (let n = 0; n < tokens.length; n++) {
223
+ const t = tokens[n];
224
+ // a trailing colon, as in the old way, is prose
225
+ if (t.type === ':' && n === tokens.length - 1)
226
+ break;
227
+ if (t.type !== 'word')
228
+ throw wrong(t);
229
+ const param = { kind: t.name, name: t.name, pos: t.pos };
230
+ if (tokens[n + 1]?.type === '(') {
231
+ const [, name, close] = tokens.slice(n + 1, n + 4);
232
+ if (name?.type !== 'word' || close?.type !== ')') {
233
+ throw new SppError(`( ) after ${t.name} holds one Capitalised name for it, as in ${t.name} (Source)`, tokens[n + 1].pos);
234
+ }
235
+ param.name = name.name;
236
+ n += 3;
237
+ }
238
+ params.push(param);
239
+ }
240
+ return params;
241
+ }
242
+ // ---------------------------------------------------------------- values
243
+ export class Block {
244
+ body;
245
+ env;
246
+ constructor(body, env) {
247
+ this.body = body;
248
+ this.env = env;
249
+ }
250
+ toString() {
251
+ return 'a block';
252
+ }
253
+ }
254
+ export class Sym {
255
+ name;
256
+ constructor(name) {
257
+ this.name = name;
258
+ }
259
+ toString() {
260
+ return `\\${this.name}`;
261
+ }
262
+ }
263
+ // A Defined word: its body, and the kinds it takes, named, in reading order.
264
+ //
265
+ // Its header's kinds are looked up when it's first called, not when it's
266
+ // Defined, since a noun it takes may be Defined in a later file.
267
+ export class Definition {
268
+ name;
269
+ block;
270
+ header;
271
+ resolved;
272
+ constructor(name, block, header) {
273
+ this.name = name;
274
+ this.block = block;
275
+ this.header = header;
276
+ }
277
+ get params() {
278
+ this.resolved ??= resolve(this.header, this.block.env);
279
+ return this.resolved;
280
+ }
281
+ }
282
+ // Every Define of one word, most particular first: those that take more,
283
+ // then those whose kinds are narrower (a Cart before an Element, anything
284
+ // before Any). A call runs the first whose kinds are on the stack. The checker
285
+ // orders them the same way, and says when two could always both fit.
286
+ export class Definitions {
287
+ list;
288
+ constructor(first) {
289
+ this.list = [first];
290
+ }
291
+ ordered() {
292
+ return [...this.list].sort((a, b) => b.params.length - a.params.length || specificity(b) - specificity(a));
293
+ }
294
+ }
295
+ export const specificity = (d) => d.params.reduce((n, p) => n + (p.type.base ? 2 : p.type === AnyType ? 0 : 1), 0);
296
+ // What a noun leaves: a value, labelled with the noun's kind. Words that take
297
+ // the kind check the label; any other word gets the value itself.
298
+ export class Kinded {
299
+ kind;
300
+ value;
301
+ constructor(kind, value) {
302
+ this.kind = kind;
303
+ this.value = value;
304
+ }
305
+ describe() {
306
+ return describe(this.value);
307
+ }
308
+ }
309
+ export const unwrap = (v) => (v instanceof Kinded ? v.value : v);
310
+ // The kind a noun, a Define that leaves one value, makes of what it leaves.
311
+ // Only the checker knows what that value is, so here its base is Any.
312
+ function nounKind(name) {
313
+ return {
314
+ name,
315
+ phrase: article(name),
316
+ members: [name],
317
+ base: AnyType,
318
+ test: (v) => v instanceof Kinded && v.kind.toLowerCase() === name.toLowerCase(),
319
+ };
320
+ }
321
+ const isDescribable = (v) => typeof v === 'object' && v !== null && typeof v.describe === 'function';
322
+ export function describe(v) {
323
+ if (typeof v === 'string')
324
+ return JSON.stringify(v);
325
+ if (isDescribable(v))
326
+ return v.describe();
327
+ return String(v);
328
+ }
329
+ export class Env {
330
+ parent;
331
+ // Keyed in lowercase, since only a word's first letter has to be a capital.
332
+ words = new Map();
333
+ // How each word was written where it was made, for editors to offer.
334
+ spellings = new Map();
335
+ // The kinds a Define's header can name, keyed in lowercase.
336
+ kinds = new Map();
337
+ constructor(parent = null) {
338
+ this.parent = parent;
339
+ }
340
+ // A builtin, in one or more forms.
341
+ word(name, ...forms) {
342
+ return this.set(name, forms);
343
+ }
344
+ // A Defined word.
345
+ // Another Define of a word adds to it, here; one in an inner scope hides it.
346
+ def(name, definition) {
347
+ const here = this.words.get(name.toLowerCase());
348
+ if (!(here instanceof Definitions))
349
+ return this.set(name, new Definitions(definition));
350
+ here.list.push(definition);
351
+ return this;
352
+ }
353
+ // A kind a Define can take, like Text or Element. A noun makes one too.
354
+ kind(t) {
355
+ this.kinds.set(t.name.toLowerCase(), t);
356
+ return this;
357
+ }
358
+ kindOf(name) {
359
+ const key = name.toLowerCase();
360
+ for (let e = this; e; e = e.parent)
361
+ if (e.kinds.has(key))
362
+ return e.kinds.get(key);
363
+ const word = this.lookup(name);
364
+ if (word instanceof Definitions)
365
+ return nounKind(word.list[0].name);
366
+ }
367
+ // The built-in kinds, nearest first.
368
+ *builtinKinds() {
369
+ for (let e = this; e; e = e.parent)
370
+ yield* e.kinds.values();
371
+ }
372
+ // A value, like what a Define was given, under the name its header gives it.
373
+ bind(name, value) {
374
+ return this.set(name, { value });
375
+ }
376
+ set(name, word) {
377
+ this.words.set(name.toLowerCase(), word);
378
+ this.spellings.set(name.toLowerCase(), name);
379
+ return this;
380
+ }
381
+ lookup(name) {
382
+ const key = name.toLowerCase();
383
+ for (let e = this; e; e = e.parent)
384
+ if (e.words.has(key))
385
+ return e.words.get(key);
386
+ }
387
+ // A builtin's forms, or undefined if the word isn't a builtin.
388
+ forms(name) {
389
+ const word = this.lookup(name);
390
+ return Array.isArray(word) ? word : undefined;
391
+ }
392
+ // Every word in scope, as it was spelled.
393
+ *names() {
394
+ for (let e = this; e; e = e.parent)
395
+ yield* e.spellings.values();
396
+ }
397
+ }
398
+ // ---------------------------------------------------------------- stack
399
+ export class Stack extends Array {
400
+ // Pops one value of the given type, for Define, which is syntax, not a
401
+ // word; words say what they take in their forms instead.
402
+ take(word, t) {
403
+ if (!this.length)
404
+ throw new SppError(`${word} expected ${t.phrase}, but there was nothing left`);
405
+ const v = this.pop();
406
+ if (!t.test(v))
407
+ throw new SppError(`${word} expected ${t.phrase}, but got ${describe(v)}`);
408
+ return v;
409
+ }
410
+ }
411
+ // ---------------------------------------------------------------- evaluation
412
+ export async function run(program, env, stack, ctx) {
413
+ for (const statement of program) {
414
+ for (let n = statement.length - 1; n >= 0; n--) {
415
+ const node = statement[n];
416
+ try {
417
+ await evaluate(node, env, stack, ctx);
418
+ }
419
+ catch (err) {
420
+ // Outermost wins: an error deep inside a Defined word is reported at
421
+ // the word the step actually wrote.
422
+ if (err instanceof Object)
423
+ err.pos = node.pos;
424
+ throw err;
425
+ }
426
+ }
427
+ }
428
+ }
429
+ export async function call(block, stack, ctx, args = {}) {
430
+ const env = new Env(block.env);
431
+ for (const [name, value] of Object.entries(args))
432
+ env.bind(name, value);
433
+ await run(block.body, env, stack, ctx);
434
+ }
435
+ // A Definition as a form, so it's called like any word, taking what its
436
+ // header says off the stack. Its body runs on a stack of its own, so it can't
437
+ // reach into its caller's; what it leaves goes on its caller's.
438
+ function formOf(d, stack) {
439
+ return {
440
+ inputs: d.params.map((p) => p.type),
441
+ output: null,
442
+ impl: async (ctx, ...args) => {
443
+ const own = new Stack();
444
+ await call(d.block, own, ctx, Object.fromEntries(d.params.map((p, i) => [p.name, args[i]])));
445
+ stack.push(...(isNoun(own, d.block.env) ? [new Kinded(d.name, unwrap(own[0]))] : own));
446
+ },
447
+ };
448
+ }
449
+ // Whether a Define left what a noun leaves: one value, of a built-in kind
450
+ // other than Any. The checker decides the same, from types.
451
+ function isNoun(left, env) {
452
+ if (left.length !== 1)
453
+ return false;
454
+ return [...env.builtinKinds()].some((k) => k !== AnyType && k.test(unwrap(left[0])));
455
+ }
456
+ // The kinds a Define's header names, or the first that isn't one.
457
+ export function resolve(params, env) {
458
+ return params.map((p) => {
459
+ const t = env.kindOf(p.kind);
460
+ if (!t)
461
+ throw Object.assign(new SppError(notAKind(p.kind, env.builtinKinds())), { pos: p.pos });
462
+ return { name: p.name, type: t };
463
+ });
464
+ }
465
+ export function notAKind(name, builtins) {
466
+ const kinds = [...new Set([...builtins].map((t) => t.name))].join(', ');
467
+ return `${name} isn't a kind a Define can take. The kinds are ${kinds}, and nouns: Defines that leave one of those`;
468
+ }
469
+ // Whether a value is what a form's input takes, and what the form is given.
470
+ // A kind's input takes the labelled value; any other, the value itself.
471
+ const accepts = (t, v) => (t.base ? t.test(v) : t.test(unwrap(v)));
472
+ const given = (t, v) => (t.base ? v : unwrap(v));
473
+ const BlockType = type('Block', 'an indented block after a colon', (v) => v instanceof Block);
474
+ // Calls the first form whose inputs are on top of the stack. If none are,
475
+ // says what was wanted at the first place every form had given up.
476
+ export async function apply(name, forms, stack, ctx) {
477
+ const fits = (f) => f.inputs.every((t, i) => i < stack.length && accepts(t, stack[stack.length - 1 - i]));
478
+ const form = forms.find(fits);
479
+ if (!form)
480
+ throw mismatch(name, forms, stack);
481
+ const args = form.inputs.map((t) => given(t, stack.pop()));
482
+ const result = await form.impl(ctx, ...args);
483
+ if (!form.output)
484
+ return;
485
+ // A vocabulary bug, not a mistake in the .spp file.
486
+ if (!form.output.test(result))
487
+ throw new Error(`${signature(name, form)} left ${describe(result)}`);
488
+ stack.push(result);
489
+ }
490
+ function mismatch(name, forms, stack) {
491
+ let live = forms;
492
+ for (let i = 0;; i++) {
493
+ const here = live.filter((f) => f.inputs.length > i);
494
+ const v = stack[stack.length - 1 - i];
495
+ const ok = here.filter((f) => i < stack.length && accepts(f.inputs[i], v));
496
+ if (!ok.length) {
497
+ const wanted = [...new Set(here.map((f) => f.inputs[i].phrase))].join(', or ');
498
+ const got = i < stack.length ? `got ${describe(v)}` : 'there was nothing left';
499
+ return new SppError(`${name} expected ${wanted}, but ${got}`);
500
+ }
501
+ live = ok;
502
+ }
503
+ }
504
+ async function evaluate(node, env, stack, ctx) {
505
+ switch (node.type) {
506
+ case 'num':
507
+ case 'str':
508
+ return void stack.push(node.value);
509
+ case 'sym':
510
+ return void stack.push(new Sym(node.value));
511
+ case 'block':
512
+ return void stack.push(new Block(node.body, env));
513
+ case 'define':
514
+ return void env.def(node.name, new Definition(node.name, stack.take(`Define ${node.name}`, BlockType), node.params));
515
+ case 'word': {
516
+ const w = env.lookup(node.name);
517
+ if (w === undefined)
518
+ throw unknownWord(node.name, env.names());
519
+ if (w instanceof Definitions)
520
+ return apply(node.name, w.ordered().map((d) => formOf(d, stack)), stack, ctx);
521
+ if (Array.isArray(w))
522
+ return apply(node.name, w, stack, ctx);
523
+ return void stack.push(w.value);
524
+ }
525
+ }
526
+ }
527
+ export function unknownWord(name, known) {
528
+ const near = closest(name.toLowerCase(), new Set(known));
529
+ const guess = near ? `Did you mean ${near[0].toUpperCase() + near.slice(1)}? ` : '';
530
+ return new SppError(`Unknown word ${name}. ${guess}Capitalised words are code; if it's prose, write it in lowercase.`);
531
+ }
532
+ function closest(word, names) {
533
+ let best;
534
+ let bestD = Math.max(2, Math.floor(word.length / 3)) + 1;
535
+ for (const n of names) {
536
+ const d = distance(word, n.toLowerCase());
537
+ if (d < bestD) {
538
+ best = n;
539
+ bestD = d;
540
+ }
541
+ }
542
+ return best;
543
+ }
544
+ function distance(a, b) {
545
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
546
+ for (let i = 1; i <= a.length; i++) {
547
+ let prev = row[0];
548
+ row[0] = i;
549
+ for (let j = 1; j <= b.length; j++) {
550
+ const tmp = row[j];
551
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
552
+ prev = tmp;
553
+ }
554
+ }
555
+ return row[b.length];
556
+ }
557
+ // ---------------------------------------------------------------- core words
558
+ // Deliberately small: no conditionals, loops or arithmetic. A spec should read
559
+ // the same every time it runs, so there's nothing in it to branch on.
560
+ export function core() {
561
+ const form = formsFor();
562
+ return (new Env()
563
+ // "Given I Visit ..." — the one capital English can't do without.
564
+ .word('I', form([], () => { }))
565
+ .word('Print', form([AnyType], (ctx, v) => (ctx.print ?? console.log)(describe(v))))
566
+ .kind(TextType)
567
+ .kind(NumberType)
568
+ .kind(TableType)
569
+ .kind(AnyType));
570
+ }