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,16 @@
1
+ // The config `npx spp-lang test` runs with when the project has no Playwright
2
+ // config of its own: every .spp file under where it was run. Everything it
3
+ // writes goes in that directory, not in here.
4
+ import path from 'node:path';
5
+ import { defineConfig } from '@playwright/test';
6
+ import { defineSppConfig } from '../generate.js';
7
+ const root = process.env.SPP_CWD ?? process.cwd();
8
+ export default defineConfig({
9
+ ...defineSppConfig({ root }),
10
+ outputDir: path.join(root, 'test-results'),
11
+ reporter: [['list'], ['html', { outputFolder: path.join(root, 'playwright-report'), open: 'never' }]],
12
+ use: {
13
+ baseURL: process.env.SPP_BASE_URL || undefined,
14
+ trace: 'retain-on-failure',
15
+ },
16
+ });
@@ -0,0 +1,14 @@
1
+ import { type PlaywrightTestArgs } from '@playwright/test';
2
+ import type { Project } from '../../config.ts';
3
+ import type { Scenario, Vocabulary } from '../../language/load.ts';
4
+ import type { BrowserContext } from './words.ts';
5
+ type Body = (args: PlaywrightTestArgs) => Promise<void>;
6
+ export declare function project(p: Project): {
7
+ test: import("@playwright/test").TestType<PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
8
+ scenario(file: string, line: number): Body;
9
+ mistake(file: string, line: number): Body;
10
+ define(file: string, line: number): Body;
11
+ broken(file: string): Body;
12
+ };
13
+ export declare function runScenario(scenario: Scenario, vocabulary: Vocabulary[], ctx: BrowserContext): Promise<void>;
14
+ export {};
@@ -0,0 +1,158 @@
1
+ // What the generated tests run (see generate.ts). Each generated file declares
2
+ // its scenarios as Playwright tests and hands each one a body from here, so a
3
+ // generated file says only which tests there are, and everything they do is
4
+ // read from the .spp files when they run.
5
+ //
6
+ // The project is loaded and checked once per worker, the first time a test
7
+ // needs it. A scenario with a mistake the checker can see fails straight away,
8
+ // without starting a browser, and so does a broken Define, as a test of its own.
9
+ //
10
+ // Each Gherkin step becomes a test.step whose location is the step's line in
11
+ // the .spp or Markdown file, so reports and failures point at the Gherkin, not
12
+ // at Spec++'s own code.
13
+ import { expect, test } from '@playwright/test';
14
+ import { check } from '../../language/check.js';
15
+ import { apply, Block, core, Env, run, SppError, Stack } from '../../language/cognate.js';
16
+ import { discover, LoadError, load } from '../../language/load.js';
17
+ import { keywordForms, keywords, StepBlock } from '../../language/steps.js';
18
+ import { leftovers } from '../../words/shared.js';
19
+ import { browser, settle } from './words.js';
20
+ // Tags that mean something to Playwright itself, applied as the test starts.
21
+ // @only is the generated file's, since it changes which tests are declared.
22
+ const MODIFIERS = {
23
+ '@skip': () => test.skip(),
24
+ '@fixme': () => test.fixme(),
25
+ '@fail': () => test.fail(),
26
+ '@slow': () => test.slow(),
27
+ };
28
+ const projects = new Map();
29
+ function loadProject(project) {
30
+ const key = JSON.stringify(project);
31
+ const cached = projects.get(key);
32
+ if (cached)
33
+ return cached;
34
+ const files = [];
35
+ const broken = new Map();
36
+ for (const file of discover(project.features, project.root, project.ignore)) {
37
+ try {
38
+ files.push(load(file));
39
+ }
40
+ catch (err) {
41
+ if (!(err instanceof LoadError))
42
+ throw err;
43
+ broken.set(file, err);
44
+ }
45
+ }
46
+ const scenarios = new Map();
47
+ for (const scenario of files.flatMap((f) => f.features).flatMap((f) => f.scenarios))
48
+ scenarios.set(where(scenario.file, scenario.line), scenario);
49
+ const loaded = {
50
+ files,
51
+ broken,
52
+ vocabulary: files.flatMap((f) => f.vocabulary),
53
+ report: check(files, { words: browser(core()), settle: leftovers, cwd: project.root }),
54
+ scenarios,
55
+ };
56
+ projects.set(key, loaded);
57
+ return loaded;
58
+ }
59
+ const where = (file, line) => `${file}:${line}`;
60
+ // The tests a generated file can declare, for a project. Called as the file
61
+ // loads, so a base URL from the command line applies to its tests.
62
+ export function project(p) {
63
+ if (process.env.SPP_BASE_URL)
64
+ test.use({ baseURL: process.env.SPP_BASE_URL });
65
+ const stale = (what) => new Error(`${what} isn't where the generated tests say. They're out of date: run Playwright again.`);
66
+ return {
67
+ test,
68
+ // The scenario on this line of this file.
69
+ scenario(file, line) {
70
+ return async ({ page }) => {
71
+ const loaded = loadProject(p);
72
+ const scenario = loaded.scenarios.get(where(file, line));
73
+ if (!scenario)
74
+ throw stale(`The scenario at ${where(file, line)}`);
75
+ for (const tag of scenario.tags)
76
+ MODIFIERS[tag]?.();
77
+ const problem = loaded.report.scenarios.get(scenario);
78
+ if (problem)
79
+ throw located(problem);
80
+ await runScenario(scenario, loaded.vocabulary, { page, expect });
81
+ };
82
+ },
83
+ // A scenario the checker found a mistake in, which fails with it, without
84
+ // asking for a page, so no browser starts.
85
+ mistake(file, line) {
86
+ return async () => {
87
+ const loaded = loadProject(p);
88
+ const scenario = loaded.scenarios.get(where(file, line));
89
+ const problem = scenario && loaded.report.scenarios.get(scenario);
90
+ if (!scenario || !problem)
91
+ throw stale(`The mistake in the scenario at ${where(file, line)}`);
92
+ for (const tag of scenario.tags)
93
+ MODIFIERS[tag]?.();
94
+ throw located(problem);
95
+ };
96
+ },
97
+ // The broken Define on this line, as a test that fails with its mistake.
98
+ define(file, line) {
99
+ return async () => {
100
+ const problem = loadProject(p).report.vocabulary.find((d) => d.file === file && d.line === line);
101
+ throw problem ? located(problem) : stale(`The broken Define at ${where(file, line)}`);
102
+ };
103
+ },
104
+ // A file that can't be read as Spec++, as a test that fails with why.
105
+ broken(file) {
106
+ return async () => {
107
+ const err = loadProject(p).broken.get(file);
108
+ if (!err)
109
+ throw stale(`The mistake in ${file}`);
110
+ const [first] = err.problems;
111
+ const message = err.problems.map((problem) => problem.message).join('\n');
112
+ throw located({ file, line: first?.line ?? 1, column: first?.column ?? 1, message });
113
+ };
114
+ },
115
+ };
116
+ }
117
+ export async function runScenario(scenario, vocabulary, ctx) {
118
+ const words = new Env(browser(core()));
119
+ for (const { program } of vocabulary)
120
+ await run(program, words, new Stack(), ctx);
121
+ // The scenario's own stack, where each keyword leaves its mode for the next.
122
+ const steps = keywords(settle);
123
+ const modes = new Stack();
124
+ for (const step of scenario.steps) {
125
+ const location = { file: step.file, line: step.line, column: step.column };
126
+ await test.step(`${step.keyword}${step.text}`, async () => {
127
+ const block = new StepBlock(new Block(step.program ?? [], words), step.argument);
128
+ try {
129
+ if (step.error !== undefined)
130
+ throw step.error;
131
+ modes.push(block);
132
+ await apply(step.keyword.trim(), keywordForms(steps, step.keywordType), modes, ctx);
133
+ }
134
+ catch (err) {
135
+ // A keyword that couldn't take what it needed is its own mistake.
136
+ if (!block.ran && err instanceof SppError && step.error === undefined)
137
+ err.pos = -step.keyword.length;
138
+ throw pointAt(err, step);
139
+ }
140
+ }, { location });
141
+ }
142
+ }
143
+ // Makes the error's top frame the offending word in the .spp file, which
144
+ // is where Playwright's reporters look for a code frame to show.
145
+ function pointAt(err, step) {
146
+ if (!(err instanceof Error))
147
+ return err;
148
+ const column = step.column + (err.pos ?? 0);
149
+ const frame = ` at ${step.file}:${step.line}:${column}`;
150
+ const [head] = (err.stack ?? '').split(/\n\s+at /);
151
+ err.stack = `${head || `${err.name}: ${err.message}`}\n${frame}`;
152
+ return err;
153
+ }
154
+ function located(d) {
155
+ const err = new SppError(d.message);
156
+ err.stack = `SppError: ${d.message}\n at ${d.file}:${d.line}:${d.column}`;
157
+ return err;
158
+ }
@@ -0,0 +1,11 @@
1
+ import type { expect, Locator, Page } from '@playwright/test';
2
+ import { Env } from '../../language/cognate.ts';
3
+ import type { Context } from '../../language/types.ts';
4
+ import type { Driver } from '../../words/driver.ts';
5
+ export interface BrowserContext extends Context {
6
+ page: Page;
7
+ expect: typeof expect;
8
+ }
9
+ export declare const playwright: Driver<BrowserContext, Locator>;
10
+ export declare const settle: (stack: import("../../language/cognate.ts").Stack, ctx: BrowserContext) => Promise<void>;
11
+ export declare function browser(parent?: Env | null): Env;
@@ -0,0 +1,129 @@
1
+ // The words that drive a browser: the shared words (words/shared.ts) on a
2
+ // Playwright driver, and the words only a browser has, like Visit.
3
+ //
4
+ // Playwright's getBy* locators implement Testing Library's queries, and its
5
+ // expect retries until a check passes, which is what the shared words ask of
6
+ // a driver.
7
+ import { Env } from '../../language/cognate.js';
8
+ import { called, formsFor, TextType } from '../../language/types.js';
9
+ import { actions, CheckType, checks, Expectation, escapeRegExp, NameType, nameOf, queries, say, settler, } from '../../words/shared.js';
10
+ // Every ARIA role that makes sense to query by name, as a word.
11
+ const ROLES = `alert alertdialog application article banner blockquote button caption cell checkbox
12
+ code columnheader combobox complementary contentinfo definition deletion dialog directory document
13
+ emphasis feed figure form generic grid gridcell group heading img insertion link list listbox
14
+ listitem log main marquee math meter menu menubar menuitem menuitemcheckbox menuitemradio navigation
15
+ none note option paragraph presentation progressbar radio radiogroup region row rowgroup rowheader
16
+ scrollbar search searchbox separator slider spinbutton status strong subscript superscript switch
17
+ tab table tablist tabpanel term textbox time timer toolbar tooltip tree treegrid treeitem`.split(/\s+/);
18
+ // ---------------------------------------------------------------- the driver
19
+ export const playwright = {
20
+ roles: ROLES,
21
+ queries: ['label', 'text', 'placeholder', 'alt', 'title', 'testId'],
22
+ actions: ['click', 'double-click', 'hover', 'focus', 'check', 'uncheck', 'clear', 'fill', 'type', 'select', 'press'],
23
+ states: ['visible', 'hidden', 'checked', 'enabled', 'disabled', 'focused', 'empty', 'value', 'text', 'containing'],
24
+ find({ page }, root, query) {
25
+ const r = root ?? page;
26
+ const { name } = query;
27
+ switch (query.by) {
28
+ case 'role':
29
+ return r.getByRole(query.role, { name, exact: true });
30
+ case 'label':
31
+ return r.getByLabel(name, { exact: true });
32
+ case 'text':
33
+ return query.loose ? r.getByText(name).first() : r.getByText(name, { exact: true });
34
+ case 'placeholder':
35
+ return r.getByPlaceholder(name, { exact: true });
36
+ case 'alt':
37
+ return r.getByAltText(name, { exact: true });
38
+ case 'title':
39
+ return r.getByTitle(name, { exact: true });
40
+ case 'testId':
41
+ return r.getByTestId(name);
42
+ }
43
+ },
44
+ async act(_, target, action) {
45
+ switch (action.do) {
46
+ case 'click':
47
+ return target.click();
48
+ case 'double-click':
49
+ return target.dblclick();
50
+ case 'hover':
51
+ return target.hover();
52
+ case 'focus':
53
+ return target.focus();
54
+ case 'check':
55
+ return target.check();
56
+ case 'uncheck':
57
+ return target.uncheck();
58
+ case 'clear':
59
+ return target.clear();
60
+ case 'fill':
61
+ return target.fill(action.text);
62
+ case 'type':
63
+ return target.pressSequentially(action.text);
64
+ case 'select':
65
+ await target.selectOption(action.text);
66
+ return;
67
+ case 'press':
68
+ return target.press(action.key);
69
+ }
70
+ },
71
+ press({ page }, key) {
72
+ return page.keyboard.press(key);
73
+ },
74
+ expect(ctx, target, state, not) {
75
+ const e = not ? ctx.expect(target).not : ctx.expect(target);
76
+ switch (state.is) {
77
+ case 'visible':
78
+ return e.toBeVisible();
79
+ case 'hidden':
80
+ return e.toBeHidden();
81
+ case 'checked':
82
+ return e.toBeChecked();
83
+ case 'enabled':
84
+ return e.toBeEnabled();
85
+ case 'disabled':
86
+ return e.toBeDisabled();
87
+ case 'focused':
88
+ return e.toBeFocused();
89
+ case 'empty':
90
+ return e.toBeEmpty();
91
+ case 'value':
92
+ return e.toHaveValue(state.text);
93
+ case 'text':
94
+ return e.toHaveText(state.text);
95
+ case 'containing':
96
+ return e.toContainText(state.text);
97
+ }
98
+ },
99
+ };
100
+ export const settle = settler(playwright);
101
+ // ---------------------------------------------------------------- vocabulary
102
+ const form = formsFor();
103
+ const Path = called(TextType, 'a "/path" or URL');
104
+ export function browser(parent = null) {
105
+ const env = queries(new Env(parent), playwright);
106
+ // navigation
107
+ env
108
+ .word('Visit', form([Path], (ctx, path) => ctx.page.goto(path)))
109
+ .word('Reload', form([], (ctx) => ctx.page.reload()))
110
+ .word('Back', form([], (ctx) => ctx.page.goBack()));
111
+ actions(env, playwright);
112
+ checks(env, playwright);
113
+ // checks on the page itself
114
+ return env
115
+ .word('At', form([Path], CheckType, (ctx, where) => new Expectation(`At ${JSON.stringify(where)}`, (not) => {
116
+ const e = ctx.expect(ctx.page);
117
+ return (not ? e.not : e).toHaveURL(urlPattern(where));
118
+ })))
119
+ .word('Titled', form([NameType], CheckType, (ctx, title) => new Expectation(`Titled ${say(title)}`, (not) => {
120
+ const e = ctx.expect(ctx.page);
121
+ return (not ? e.not : e).toHaveTitle(nameOf(title));
122
+ })));
123
+ }
124
+ // "/dashboard" means that path on whatever host the page is on.
125
+ function urlPattern(where) {
126
+ if (!where.startsWith('/'))
127
+ return where;
128
+ return new RegExp(`^[a-z]+://[^/]+${escapeRegExp(where)}(?:[?#].*)?$`);
129
+ }
@@ -0,0 +1,18 @@
1
+ export interface Project {
2
+ root: string;
3
+ features: string[];
4
+ ignore: string[];
5
+ outputDir: string;
6
+ }
7
+ interface Registry {
8
+ collecting: boolean;
9
+ projects: Project[];
10
+ }
11
+ export declare const registry: Registry;
12
+ export declare function findConfig(from: string): string | undefined;
13
+ export declare const everything: (root: string) => Project;
14
+ export declare function loadProjects(from?: string, { trusted }?: {
15
+ trusted?: boolean | undefined;
16
+ }): Promise<Project[]>;
17
+ export declare function contains(project: Project, file: string): boolean;
18
+ export {};
@@ -0,0 +1,63 @@
1
+ // Which .spp files make up a project, for the tools that don't run Playwright:
2
+ // the checker, the dictionary and the language server. Playwright's config
3
+ // says, through its defineSppConfig calls (backends/playwright/generate.ts).
4
+ //
5
+ // The config is found by looking up from the current directory. Loading it
6
+ // runs it, but only to collect what its defineSppConfig calls say: nothing is
7
+ // generated. Without a config, or one with no defineSppConfig in it, the
8
+ // project is every .spp file under the current directory.
9
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
10
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
11
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
12
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
13
+ });
14
+ }
15
+ return path;
16
+ };
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { pathToFileURL } from 'node:url';
20
+ const holder = globalThis;
21
+ holder.sppRegistry ??= { collecting: false, projects: [] };
22
+ export const registry = holder.sppRegistry;
23
+ const NAMES = ['ts', 'mts', 'cts', 'js', 'mjs', 'cjs'].map((ext) => `playwright.config.${ext}`);
24
+ export function findConfig(from) {
25
+ for (let dir = path.resolve(from);; dir = path.dirname(dir)) {
26
+ const file = NAMES.map((name) => path.join(dir, name)).find((f) => fs.existsSync(f));
27
+ if (file)
28
+ return file;
29
+ if (dir === path.dirname(dir))
30
+ return undefined;
31
+ }
32
+ }
33
+ // Every .spp file under a directory, when nothing says otherwise.
34
+ export const everything = (root) => ({
35
+ root: path.resolve(root),
36
+ features: [path.resolve(root)],
37
+ ignore: [],
38
+ outputDir: path.resolve(root, '.spp-tests'),
39
+ });
40
+ // The projects the config above a directory defines. Loading a config runs
41
+ // it, so pass trusted: false where the project's code mustn't run, and get
42
+ // the default instead.
43
+ export async function loadProjects(from = process.cwd(), { trusted = true } = {}) {
44
+ const file = trusted ? findConfig(from) : undefined;
45
+ if (!file)
46
+ return [everything(from)];
47
+ registry.collecting = true;
48
+ registry.projects = [];
49
+ try {
50
+ // A new URL each time, so a config that's changed is read again.
51
+ await import(__rewriteRelativeImportExtension(`${pathToFileURL(file).href}?spp=${Date.now()}`));
52
+ return registry.projects.length ? [...registry.projects] : [everything(path.dirname(file))];
53
+ }
54
+ finally {
55
+ registry.collecting = false;
56
+ registry.projects = [];
57
+ }
58
+ }
59
+ // Whether a file is one of a project's: under its features, and not ignored.
60
+ export function contains(project, file) {
61
+ const within = (dir) => file === dir || file.startsWith(dir + path.sep);
62
+ return project.features.some(within) && !project.ignore.some(within);
63
+ }
@@ -0,0 +1,22 @@
1
+ export type { SppConfig, SppTests } from './backends/playwright/generate.ts';
2
+ export { defineSppConfig, generate } from './backends/playwright/generate.ts';
3
+ export { runScenario } from './backends/playwright/runtime.ts';
4
+ export type { BrowserContext } from './backends/playwright/words.ts';
5
+ export { browser, playwright, settle } from './backends/playwright/words.ts';
6
+ export type { Project } from './config.ts';
7
+ export { contains, everything, findConfig, loadProjects } from './config.ts';
8
+ export type { CheckOptions, Diagnostic, Line, Place, Report, WordInfo } from './language/check.ts';
9
+ export { Analysis, analyze, check, format } from './language/check.ts';
10
+ export type { Node, Program, Statement, Token } from './language/cognate.ts';
11
+ export { Block, call, core, Env, lex, parse, run, SppError, Stack, Sym } from './language/cognate.ts';
12
+ export type { Feature, Scenario, SppFile, Step, Vocabulary } from './language/load.ts';
13
+ export { discover, LoadError, load, loadSource, selects } from './language/load.ts';
14
+ export type { Fence } from './language/markdown.ts';
15
+ export { fences, sppFences, sppLines } from './language/markdown.ts';
16
+ export type { CodeLine, Sections } from './language/sections.ts';
17
+ export { codeLines, sections, split } from './language/sections.ts';
18
+ export type { Context, Form, Type } from './language/types.ts';
19
+ export { AnyType, called, formsFor, NumberType, oneOf, signature, TableType, TextType, type } from './language/types.ts';
20
+ export { dictionary } from './tools/dictionary.ts';
21
+ export type { Action, Capabilities, Driver, Name, Query, QueryKind, State } from './words/driver.ts';
22
+ export { actions, CheckType, checks, ElementType, leftovers, MatcherType, NameType, PatternType, queries, ScopeType, settler, } from './words/shared.ts';
@@ -0,0 +1,12 @@
1
+ export { defineSppConfig, generate } from './backends/playwright/generate.js';
2
+ export { runScenario } from './backends/playwright/runtime.js';
3
+ export { browser, playwright, settle } from './backends/playwright/words.js';
4
+ export { contains, everything, findConfig, loadProjects } from './config.js';
5
+ export { Analysis, analyze, check, format } from './language/check.js';
6
+ export { Block, call, core, Env, lex, parse, run, SppError, Stack, Sym } from './language/cognate.js';
7
+ export { discover, LoadError, load, loadSource, selects } from './language/load.js';
8
+ export { fences, sppFences, sppLines } from './language/markdown.js';
9
+ export { codeLines, sections, split } from './language/sections.js';
10
+ export { AnyType, called, formsFor, NumberType, oneOf, signature, TableType, TextType, type } from './language/types.js';
11
+ export { dictionary } from './tools/dictionary.js';
12
+ export { actions, CheckType, checks, ElementType, leftovers, MatcherType, NameType, PatternType, queries, ScopeType, settler, } from './words/shared.js';
@@ -0,0 +1,178 @@
1
+ import type { Env, Node, Param, Program } from './cognate.ts';
2
+ import type { Scenario, SppFile, Step } from './load.ts';
3
+ import type { Form, Type } from './types.ts';
4
+ export interface Diagnostic {
5
+ file: string;
6
+ line: number;
7
+ column: number;
8
+ message: string;
9
+ define?: string;
10
+ }
11
+ export interface Report {
12
+ vocabulary: Diagnostic[];
13
+ scenarios: Map<Scenario, Diagnostic>;
14
+ }
15
+ export interface CheckOptions {
16
+ words: Env;
17
+ settle: (stack: {
18
+ name: string;
19
+ shown: string;
20
+ }[]) => string | null;
21
+ cwd?: string;
22
+ }
23
+ declare class Var {
24
+ members: readonly string[] | null;
25
+ phrase: string;
26
+ }
27
+ declare class BlockOf {
28
+ body: Program;
29
+ scope: Scope;
30
+ origin: Origin;
31
+ constructor(body: Program, scope: Scope, origin: Origin);
32
+ }
33
+ type Ty = Type<unknown> | Var | BlockOf;
34
+ interface Val {
35
+ ty: Ty;
36
+ shown: string;
37
+ }
38
+ interface Want {
39
+ members: readonly string[] | null;
40
+ phrase: string;
41
+ key: unknown;
42
+ }
43
+ interface Way {
44
+ inputs: Want[];
45
+ outputs: (args: Val[]) => Val[];
46
+ def?: Def;
47
+ }
48
+ interface Origin {
49
+ file: string;
50
+ at(pos: number): {
51
+ line: number;
52
+ column: number;
53
+ };
54
+ }
55
+ declare class Mistake extends Error {
56
+ pos?: number;
57
+ }
58
+ export interface Place {
59
+ file: string;
60
+ line: number;
61
+ column: number;
62
+ }
63
+ interface Def {
64
+ name: string;
65
+ block: BlockOf;
66
+ at: Place;
67
+ header: Param[];
68
+ params: {
69
+ name: string;
70
+ type: Type<unknown>;
71
+ }[];
72
+ state: 'unchecked' | 'checking' | 'done';
73
+ outputs?: Val[];
74
+ noun?: Type<unknown>;
75
+ error?: Diagnostic;
76
+ }
77
+ type Known = {
78
+ name: string;
79
+ } & ({
80
+ kind: 'forms';
81
+ forms: readonly Form[];
82
+ } | {
83
+ kind: 'define';
84
+ defs: Def[];
85
+ } | {
86
+ kind: 'value';
87
+ val: Val;
88
+ });
89
+ declare class Scope {
90
+ parent: Scope | null;
91
+ builtins: Env | null;
92
+ words: Map<string, Known>;
93
+ constructor(parent: Scope | null, builtins?: Env | null);
94
+ set(known: Known): void;
95
+ lookup(name: string): Known | undefined;
96
+ names(): Generator<string>;
97
+ all(): Known[];
98
+ }
99
+ declare class Sim {
100
+ open: boolean;
101
+ stack: Val[];
102
+ inputs: Var[];
103
+ constructor(open: boolean);
104
+ peek(i: number): Val | undefined;
105
+ fromCaller(want?: Want): Val;
106
+ }
107
+ export declare function check(files: SppFile[], options: CheckOptions): Report;
108
+ export interface WordInfo {
109
+ name: string;
110
+ kind: 'builtin' | 'define' | 'value';
111
+ signatures: string[];
112
+ outputs: (readonly string[] | null)[];
113
+ at?: Place;
114
+ places?: Place[];
115
+ }
116
+ export interface Line {
117
+ file: string;
118
+ line: number;
119
+ }
120
+ export declare class Analysis {
121
+ report: Report;
122
+ private checker;
123
+ constructor(checker: Checker);
124
+ words(at?: Step | Line): WordInfo[];
125
+ word(name: string, at?: Step | Line): WordInfo | undefined;
126
+ expected(left: string, at?: Step | Line): {
127
+ members: readonly string[] | null;
128
+ phrase: string;
129
+ } | null;
130
+ chosen(file: string, line: number, column: number): Place | undefined;
131
+ }
132
+ export declare function analyze(files: SppFile[], options: CheckOptions): Analysis;
133
+ declare class Checker {
134
+ options: CheckOptions;
135
+ cwd: string;
136
+ defs: Def[];
137
+ report: Report;
138
+ vocabulary: Scope;
139
+ stepScopes: Map<Step, Scope>;
140
+ lineScopes: Map<string, Scope>;
141
+ guessing: boolean;
142
+ chosen: Map<string, Def>;
143
+ constructor(options: CheckOptions);
144
+ check(files: SppFile[]): Report;
145
+ scenario(scenario: Scenario, vocabulary: Scope): Diagnostic | undefined;
146
+ settle(sim: Sim): void;
147
+ scopeFor(at?: Step | Line): Scope;
148
+ info(known: Known): WordInfo;
149
+ expected(left: string, scope: Scope): {
150
+ members: readonly string[] | null;
151
+ phrase: string;
152
+ } | null;
153
+ diagnose(err: unknown, origin: Origin, define?: string): Diagnostic;
154
+ run(program: Program, scope: Scope, sim: Sim, origin: Origin): void;
155
+ evaluate(node: Node, scope: Scope, sim: Sim, origin: Origin): void | Way;
156
+ params(def: Def): {
157
+ name: string;
158
+ type: Type<unknown>;
159
+ }[];
160
+ kindOf(name: string, scope: Scope): Type<unknown>;
161
+ nounOf(def: Def, outputs: Val[]): Type<unknown> | undefined;
162
+ overloads(vocabulary: Scope): void;
163
+ where(d: Place): string;
164
+ take(sim: Sim, word: string): Val;
165
+ formWay(name: string, form: Form): Way;
166
+ defineWays(defs: Def[]): Way[];
167
+ defineWay(def: Def): Way;
168
+ checkDefine(def: Def): void;
169
+ call(name: string, ways: Way[], sim: Sim): Way;
170
+ fit(way: Way, sim: Sim): {
171
+ binds: Map<Var, Want>;
172
+ fresh: number;
173
+ unsure: number;
174
+ } | null;
175
+ mismatch(name: string, ways: Way[], sim: Sim): Mistake;
176
+ }
177
+ export declare function format(d: Diagnostic, source: string, cwd?: string): string;
178
+ export {};