@venn-lang/core 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 +303 -0
- package/dist/index.d.ts +3654 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13137 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
- package/src/ast/call-args.ts +15 -0
- package/src/ast/dotted-path.ts +22 -0
- package/src/ast/index.ts +7 -0
- package/src/ast/is-statement.ts +40 -0
- package/src/ast/split-call.ts +28 -0
- package/src/ast/walk.ts +6 -0
- package/src/codes/build-problem.ts +24 -0
- package/src/codes/catalog.ts +35 -0
- package/src/codes/code.types.ts +7 -0
- package/src/codes/index.ts +3 -0
- package/src/compile/compile.ts +169 -0
- package/src/compile/compile.types.ts +40 -0
- package/src/compile/index.ts +3 -0
- package/src/compile/lex-scope.ts +78 -0
- package/src/compile/nodes/binary.ts +56 -0
- package/src/compile/nodes/call.ts +50 -0
- package/src/compile/nodes/collection.ts +83 -0
- package/src/compile/nodes/const-lit.ts +30 -0
- package/src/compile/nodes/fast-binary.ts +71 -0
- package/src/compile/nodes/fn.ts +87 -0
- package/src/compile/nodes/index.ts +8 -0
- package/src/compile/nodes/literal.ts +86 -0
- package/src/compile/nodes/map-fields.ts +79 -0
- package/src/compile/nodes/member.ts +32 -0
- package/src/events/envelope.types.ts +17 -0
- package/src/events/event-data.types.ts +25 -0
- package/src/events/ids.types.ts +10 -0
- package/src/events/index.ts +5 -0
- package/src/events/run-plan.types.ts +18 -0
- package/src/events/status.types.ts +2 -0
- package/src/expand/accepted-kinds.ts +23 -0
- package/src/expand/deco/deco-decorator.ts +77 -0
- package/src/expand/deco/deco-env.ts +24 -0
- package/src/expand/deco/deco.types.ts +43 -0
- package/src/expand/deco/document-decos.ts +64 -0
- package/src/expand/deco/index.ts +7 -0
- package/src/expand/deco/read-signature.ts +57 -0
- package/src/expand/deco/run-body.ts +119 -0
- package/src/expand/decorate-callable.ts +49 -0
- package/src/expand/decorations.ts +41 -0
- package/src/expand/expand.ts +108 -0
- package/src/expand/expand.types.ts +78 -0
- package/src/expand/handles/around-verbs.ts +35 -0
- package/src/expand/handles/binding-verbs.ts +27 -0
- package/src/expand/handles/common-verbs.ts +40 -0
- package/src/expand/handles/fn-verbs.ts +58 -0
- package/src/expand/handles/handle.types.ts +33 -0
- package/src/expand/handles/index.ts +4 -0
- package/src/expand/handles/kind-of.ts +36 -0
- package/src/expand/handles/make-handle.ts +100 -0
- package/src/expand/handles/missing-verb.ts +27 -0
- package/src/expand/handles/runnable-verbs.ts +13 -0
- package/src/expand/handles/type-verbs.ts +56 -0
- package/src/expand/index.ts +40 -0
- package/src/expand/make-context.ts +69 -0
- package/src/expand/node-meta.ts +30 -0
- package/src/expand/node-span.ts +17 -0
- package/src/expand/swap-node.ts +28 -0
- package/src/expand/wrong-kind.ts +102 -0
- package/src/expr/cell.types.ts +25 -0
- package/src/expr/closure.ts +53 -0
- package/src/expr/closure.types.ts +31 -0
- package/src/expr/eval-env.types.ts +9 -0
- package/src/expr/evaluate.ts +18 -0
- package/src/expr/frame.ts +60 -0
- package/src/expr/index.ts +14 -0
- package/src/expr/invoke.ts +140 -0
- package/src/expr/member-value.ts +91 -0
- package/src/expr/methods/index.ts +65 -0
- package/src/expr/methods/list-grouping.ts +107 -0
- package/src/expr/methods/list-methods.ts +57 -0
- package/src/expr/methods/list-selection.ts +142 -0
- package/src/expr/methods/map-extras.ts +87 -0
- package/src/expr/methods/map-methods.ts +17 -0
- package/src/expr/methods/number-methods.ts +29 -0
- package/src/expr/methods/string-extras.ts +66 -0
- package/src/expr/methods/string-methods.ts +32 -0
- package/src/expr/methods/unit-methods.ts +54 -0
- package/src/expr/namespace.ts +20 -0
- package/src/expr/native.types.ts +40 -0
- package/src/expr/operators.ts +91 -0
- package/src/expr/pending.ts +47 -0
- package/src/expr/prelude.ts +75 -0
- package/src/expr/task.ts +71 -0
- package/src/format/format-text.ts +22 -0
- package/src/format/format.types.ts +18 -0
- package/src/format/from-settings.ts +40 -0
- package/src/format/index.ts +5 -0
- package/src/format/organize-header.ts +66 -0
- package/src/format/reindent.ts +35 -0
- package/src/generated/ast.ts +2383 -0
- package/src/generated/grammar.ts +5236 -0
- package/src/generated/module.ts +25 -0
- package/src/grammar/venn.langium +291 -0
- package/src/graph/graph.types.ts +20 -0
- package/src/graph/index.ts +2 -0
- package/src/graph/to-graph.ts +87 -0
- package/src/index.ts +93 -0
- package/src/interpolation/compile-template.ts +40 -0
- package/src/interpolation/index.ts +4 -0
- package/src/interpolation/interpolation.types.ts +18 -0
- package/src/interpolation/scan-interpolations.ts +47 -0
- package/src/interpolation/template.types.ts +19 -0
- package/src/lang/default-services.ts +14 -0
- package/src/lang/index.ts +3 -0
- package/src/lang/venn-lexer.ts +34 -0
- package/src/lang/venn-module.ts +51 -0
- package/src/module/index.ts +5 -0
- package/src/module/specifier.ts +21 -0
- package/src/parse/error-to-problem.ts +43 -0
- package/src/parse/index.ts +3 -0
- package/src/parse/parse-expression.ts +27 -0
- package/src/parse/parse-output.types.ts +8 -0
- package/src/parse/parse.ts +26 -0
- package/src/problem/build-diff.ts +116 -0
- package/src/problem/diff.types.ts +20 -0
- package/src/problem/format-value.ts +40 -0
- package/src/problem/index.ts +8 -0
- package/src/problem/problem-error.ts +15 -0
- package/src/problem/problem.types.ts +21 -0
- package/src/problem/related.types.ts +7 -0
- package/src/problem/severity.types.ts +2 -0
- package/src/problem/span.types.ts +8 -0
- package/src/typecheck/action-signature.ts +53 -0
- package/src/typecheck/builtin-types.ts +63 -0
- package/src/typecheck/builtins.ts +267 -0
- package/src/typecheck/catalog.types.ts +16 -0
- package/src/typecheck/check-deco.ts +174 -0
- package/src/typecheck/check-stmts.ts +151 -0
- package/src/typecheck/check-types.ts +232 -0
- package/src/typecheck/context.ts +31 -0
- package/src/typecheck/imported-types.ts +127 -0
- package/src/typecheck/index.ts +26 -0
- package/src/typecheck/infer.ts +416 -0
- package/src/typecheck/kind-types.ts +79 -0
- package/src/typecheck/member-docs.ts +211 -0
- package/src/typecheck/named-types.ts +57 -0
- package/src/typecheck/prelude-types.ts +152 -0
- package/src/typecheck/reshaped-fns.ts +58 -0
- package/src/typecheck/scheme.ts +75 -0
- package/src/typecheck/seed-params.ts +72 -0
- package/src/typecheck/seed-values.ts +64 -0
- package/src/typecheck/show.ts +64 -0
- package/src/typecheck/solidify.ts +60 -0
- package/src/typecheck/spec-to-type.ts +88 -0
- package/src/typecheck/type-env.ts +46 -0
- package/src/typecheck/type-ref.ts +62 -0
- package/src/typecheck/type.types.ts +168 -0
- package/src/typecheck/unify.ts +192 -0
- package/src/typecheck/unit-types.ts +49 -0
- package/src/units/combine.ts +97 -0
- package/src/units/index.ts +6 -0
- package/src/units/parse-instant.ts +12 -0
- package/src/units/parse-number.ts +48 -0
- package/src/units/unit-guards.ts +16 -0
- package/src/units/unit.types.ts +38 -0
- package/src/value/equals.ts +4 -0
- package/src/value/index.ts +4 -0
- package/src/value/is-numeric.ts +6 -0
- package/src/value/truthiness.ts +9 -0
- package/src/value/value.types.ts +12 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DefaultLexer, type LexerResult, type TokenizeOptions } from "langium";
|
|
2
|
+
|
|
3
|
+
/** A single lexed token; NL suppression keys off its token-type name. */
|
|
4
|
+
type Token = LexerResult["tokens"][number];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Makes `NL` (a newline or `;`) significant between statements, but suppresses
|
|
8
|
+
* it inside `( )` and `[ ]` so calls, arg lists and list literals may still
|
|
9
|
+
* span multiple physical lines. Blocks and maps (`{ }`) keep their newlines.
|
|
10
|
+
*/
|
|
11
|
+
export class VennLexer extends DefaultLexer {
|
|
12
|
+
override tokenize(text: string, options?: TokenizeOptions): LexerResult {
|
|
13
|
+
const result = super.tokenize(text, options);
|
|
14
|
+
result.tokens = suppressBracketedNewlines(result.tokens);
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function suppressBracketedNewlines(tokens: Token[]): Token[] {
|
|
20
|
+
const kept: Token[] = [];
|
|
21
|
+
let depth = 0;
|
|
22
|
+
for (const token of tokens) {
|
|
23
|
+
depth = Math.max(0, depth + bracketDelta(token.tokenType.name));
|
|
24
|
+
if (token.tokenType.name === "NL" && depth > 0) continue;
|
|
25
|
+
kept.push(token);
|
|
26
|
+
}
|
|
27
|
+
return kept;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function bracketDelta(name: string): number {
|
|
31
|
+
if (name === "(" || name === "[") return 1;
|
|
32
|
+
if (name === ")" || name === "]") return -1;
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDefaultCoreModule,
|
|
3
|
+
createDefaultSharedCoreModule,
|
|
4
|
+
EmptyFileSystem,
|
|
5
|
+
inject,
|
|
6
|
+
type LangiumCoreServices,
|
|
7
|
+
type LanguageMetaData,
|
|
8
|
+
type Module,
|
|
9
|
+
} from "langium";
|
|
10
|
+
import {
|
|
11
|
+
VennGeneratedModule,
|
|
12
|
+
VennGeneratedSharedModule,
|
|
13
|
+
VennLanguageMetaData,
|
|
14
|
+
} from "../generated/module.js";
|
|
15
|
+
import { VennLexer } from "./venn-lexer.js";
|
|
16
|
+
|
|
17
|
+
// The newline-aware lexer: `NL` is suppressed inside brackets.
|
|
18
|
+
const VennLexerModule: Module<LangiumCoreServices, { parser: { Lexer: VennLexer } }> = {
|
|
19
|
+
parser: { Lexer: (services) => new VennLexer(services) },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function modeModule(
|
|
23
|
+
mode: LanguageMetaData["mode"],
|
|
24
|
+
): Module<LangiumCoreServices, { LanguageMetaData: LanguageMetaData }> {
|
|
25
|
+
return { LanguageMetaData: () => ({ ...VennLanguageMetaData, mode }) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build the Langium core services on `EmptyFileSystem`, so no `node:*` is
|
|
30
|
+
* touched and this runs in both the CLI (Node) and the LSP (Web Worker).
|
|
31
|
+
*
|
|
32
|
+
* `production` skips Chevrotain's grammar validation, which dominates the cost
|
|
33
|
+
* of a first parse and only ever checks generated code against itself. That
|
|
34
|
+
* check belongs in a test, `venn-module.test.ts`, which is why this takes a
|
|
35
|
+
* mode at all.
|
|
36
|
+
*
|
|
37
|
+
* @param mode Langium language mode; `development` re-enables the validation.
|
|
38
|
+
*/
|
|
39
|
+
export function createVennServices(
|
|
40
|
+
mode: LanguageMetaData["mode"] = "production",
|
|
41
|
+
): LangiumCoreServices {
|
|
42
|
+
const shared = inject(createDefaultSharedCoreModule(EmptyFileSystem), VennGeneratedSharedModule);
|
|
43
|
+
const venn = inject(
|
|
44
|
+
createDefaultCoreModule({ shared }),
|
|
45
|
+
VennGeneratedModule,
|
|
46
|
+
VennLexerModule,
|
|
47
|
+
modeModule(mode),
|
|
48
|
+
);
|
|
49
|
+
shared.ServiceRegistry.register(venn);
|
|
50
|
+
return venn;
|
|
51
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** What a specifier written in an `import` names. */
|
|
2
|
+
export type SpecifierKind = "relative" | "alias" | "package";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which of the three a specifier is.
|
|
6
|
+
*
|
|
7
|
+
* The rule is Node's, so there is no `npm:` prefix and nothing new to learn:
|
|
8
|
+
* `./util.vn` is a file beside this one, `#shared/auth.vn` goes through
|
|
9
|
+
* `[paths]`, and a bare `zod` is an installed package. The shape of what is
|
|
10
|
+
* written already says which it is.
|
|
11
|
+
*/
|
|
12
|
+
export function specifierKind(spec: string): SpecifierKind {
|
|
13
|
+
if (spec.startsWith(".") || spec.startsWith("/")) return "relative";
|
|
14
|
+
if (spec.startsWith("#")) return "alias";
|
|
15
|
+
return "package";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Whether the specifier names an installed package rather than a file. */
|
|
19
|
+
export function isPackageSpecifier(spec: string): boolean {
|
|
20
|
+
return specifierKind(spec) === "package";
|
|
21
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { buildProblem, CODES } from "../codes/index.js";
|
|
2
|
+
import type { Problem, Span } from "../problem/index.js";
|
|
3
|
+
|
|
4
|
+
/** Structural view of a Chevrotain lexer error (avoids importing chevrotain). */
|
|
5
|
+
interface LexerError {
|
|
6
|
+
offset: number;
|
|
7
|
+
line?: number;
|
|
8
|
+
column?: number;
|
|
9
|
+
length: number;
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Structural view of a Chevrotain parser (recognition) error. */
|
|
14
|
+
interface RecognitionError {
|
|
15
|
+
token: { startOffset: number; startLine?: number; startColumn?: number; image: string };
|
|
16
|
+
message: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Map a lexer error to VN1001. */
|
|
20
|
+
export function lexerErrorToProblem(args: { error: LexerError; uri: string }): Problem {
|
|
21
|
+
const e = args.error;
|
|
22
|
+
const span: Span = {
|
|
23
|
+
uri: args.uri,
|
|
24
|
+
offset: e.offset,
|
|
25
|
+
length: e.length,
|
|
26
|
+
line: e.line ?? 1,
|
|
27
|
+
column: e.column ?? 1,
|
|
28
|
+
};
|
|
29
|
+
return buildProblem({ spec: CODES.VN1001_LEX, span, title: e.message });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Map a parser error to VN1002. */
|
|
33
|
+
export function parserErrorToProblem(args: { error: RecognitionError; uri: string }): Problem {
|
|
34
|
+
const t = args.error.token;
|
|
35
|
+
const span: Span = {
|
|
36
|
+
uri: args.uri,
|
|
37
|
+
offset: t.startOffset,
|
|
38
|
+
length: t.image?.length ?? 1,
|
|
39
|
+
line: t.startLine ?? 1,
|
|
40
|
+
column: t.startColumn ?? 1,
|
|
41
|
+
};
|
|
42
|
+
return buildProblem({ spec: CODES.VN1002_PARSE, span, title: args.error.message });
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ExpectStmt, Expr, FlowDecl, StepDecl } from "../generated/ast.js";
|
|
2
|
+
import { parse } from "./parse.js";
|
|
3
|
+
|
|
4
|
+
const PREFIX = 'flow "e" { step "e" { expect ';
|
|
5
|
+
const SUFFIX = " } }";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* How far {@link parseExpression} shifts CST offsets. Exported because the
|
|
9
|
+
* editor maps the parsed expression back onto the `${…}` it came from, and a
|
|
10
|
+
* hand-counted constant there would rot the moment this wrapper changes.
|
|
11
|
+
*/
|
|
12
|
+
export const EXPRESSION_OFFSET: number = PREFIX.length;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse a standalone expression by wrapping it in a minimal flow and extracting
|
|
16
|
+
* the `expect` subject. Used by string interpolation (`${…}`).
|
|
17
|
+
*
|
|
18
|
+
* @returns The expression, or `undefined` when the source does not parse.
|
|
19
|
+
*/
|
|
20
|
+
export function parseExpression(source: string): Expr | undefined {
|
|
21
|
+
const { ast, problems } = parse(`${PREFIX}${source}${SUFFIX}`);
|
|
22
|
+
if (problems.length > 0) return undefined;
|
|
23
|
+
const flow = ast.decls[0] as FlowDecl | undefined;
|
|
24
|
+
const step = flow?.body.stmts[0] as StepDecl | undefined;
|
|
25
|
+
const stmt = step?.body.stmts[0] as ExpectStmt | undefined;
|
|
26
|
+
return stmt?.subject;
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Document } from "../generated/ast.js";
|
|
2
|
+
import type { Problem } from "../problem/index.js";
|
|
3
|
+
|
|
4
|
+
/** The result of {@link parse}: a (possibly partial) AST plus VN1xxx problems. */
|
|
5
|
+
export interface ParseOutput {
|
|
6
|
+
ast: Document;
|
|
7
|
+
problems: Problem[];
|
|
8
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ParseResult } from "langium";
|
|
2
|
+
import type { Document } from "../generated/ast.js";
|
|
3
|
+
import { vennServices } from "../lang/index.js";
|
|
4
|
+
import type { Problem } from "../problem/index.js";
|
|
5
|
+
import { lexerErrorToProblem, parserErrorToProblem } from "./error-to-problem.js";
|
|
6
|
+
import type { ParseOutput } from "./parse-output.types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse `.vn` source into an AST plus VN1xxx problems. Synchronous and
|
|
10
|
+
* filesystem-free (Chevrotain error recovery keeps a partial AST on failure).
|
|
11
|
+
* It never throws: bad syntax comes back in `problems`.
|
|
12
|
+
*
|
|
13
|
+
* @param options.uri Source URI recorded on every span, for editors and reports.
|
|
14
|
+
*/
|
|
15
|
+
export function parse(text: string, options: { uri?: string } = {}): ParseOutput {
|
|
16
|
+
const uri = options.uri ?? "memory://inline.vn";
|
|
17
|
+
const result = vennServices().parser.LangiumParser.parse<Document>(text);
|
|
18
|
+
return { ast: result.value, problems: collectProblems({ result, uri }) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function collectProblems(args: { result: ParseResult<Document>; uri: string }): Problem[] {
|
|
22
|
+
const { result, uri } = args;
|
|
23
|
+
const lexical = result.lexerErrors.map((error) => lexerErrorToProblem({ error, uri }));
|
|
24
|
+
const syntactic = result.parserErrors.map((error) => parserErrorToProblem({ error, uri }));
|
|
25
|
+
return [...lexical, ...syntactic];
|
|
26
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Diff, DiffEntry } from "./diff.types.js";
|
|
2
|
+
import { clamp, formatValue } from "./format-value.js";
|
|
3
|
+
|
|
4
|
+
/** Past this many fields a wall of text stops being a diff. */
|
|
5
|
+
const MAX_ENTRIES = 40;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Past this the walk stops descending. A value that contains itself has no
|
|
9
|
+
* bottom, and a stack overflow inside a failure report is a crash, not a report.
|
|
10
|
+
*/
|
|
11
|
+
const MAX_DEPTH = 16;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Turn the two sides a producer compared into a {@link Diff}. Two structures are
|
|
15
|
+
* walked field by field, so the failure names the field that moved instead of
|
|
16
|
+
* printing the same rendering twice; anything else stays a plain pair.
|
|
17
|
+
*/
|
|
18
|
+
export function buildDiff(args: {
|
|
19
|
+
/** Heading for the field-by-field form. */
|
|
20
|
+
label: string;
|
|
21
|
+
expected: unknown;
|
|
22
|
+
actual: unknown;
|
|
23
|
+
/**
|
|
24
|
+
* Whether the two sides correspond position by position. Pass `false` for a
|
|
25
|
+
* comparison that does not, such as `contains`. See {@link walkable}.
|
|
26
|
+
*/
|
|
27
|
+
aligned?: boolean;
|
|
28
|
+
}): Diff {
|
|
29
|
+
const entries: DiffEntry[] = [];
|
|
30
|
+
const { expected, actual } = args;
|
|
31
|
+
if (walkable(args)) walk({ expected, actual, path: "", entries, depth: 0 });
|
|
32
|
+
if (entries.length === 0) return pair(expected, actual);
|
|
33
|
+
return { kind: "fields", label: args.label, entries };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A field-by-field walk claims the two sides correspond position by position.
|
|
38
|
+
* `equals` claims that; `contains` holds one needle against every item, and
|
|
39
|
+
* lining that needle up with the haystack would invent a comparison nobody
|
|
40
|
+
* made, such as `[0] expected 5, actual [1,2]`.
|
|
41
|
+
*/
|
|
42
|
+
function walkable(args: { expected: unknown; actual: unknown; aligned?: boolean }): boolean {
|
|
43
|
+
return args.aligned !== false && comparable(args.expected, args.actual);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pair(expected: unknown, actual: unknown): Diff {
|
|
47
|
+
return {
|
|
48
|
+
kind: "scalar",
|
|
49
|
+
expected: clamp(formatValue(expected)),
|
|
50
|
+
actual: clamp(formatValue(actual)),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Only two structures of the same shape are worth walking side by side. */
|
|
55
|
+
function comparable(expected: unknown, actual: unknown): boolean {
|
|
56
|
+
if (Array.isArray(expected) !== Array.isArray(actual)) return false;
|
|
57
|
+
return isStructure(expected) && isStructure(actual);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isStructure(value: unknown): boolean {
|
|
61
|
+
return typeof value === "object" && value !== null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function walk(args: {
|
|
65
|
+
expected: unknown;
|
|
66
|
+
actual: unknown;
|
|
67
|
+
path: string;
|
|
68
|
+
entries: DiffEntry[];
|
|
69
|
+
depth: number;
|
|
70
|
+
}): void {
|
|
71
|
+
const list = Array.isArray(args.expected);
|
|
72
|
+
for (const key of keysOf(args.expected, args.actual)) {
|
|
73
|
+
if (args.entries.length >= MAX_ENTRIES) return;
|
|
74
|
+
visit({ from: args, key, path: childPath({ parent: args.path, key, list }) });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* One field of the walk. It descends only where the two sides disagree: an
|
|
80
|
+
* untouched subtree is worth one line of context, not fifty.
|
|
81
|
+
*/
|
|
82
|
+
function visit(args: {
|
|
83
|
+
from: { expected: unknown; actual: unknown; entries: DiffEntry[]; depth: number };
|
|
84
|
+
key: string;
|
|
85
|
+
path: string;
|
|
86
|
+
}): void {
|
|
87
|
+
const expected = read(args.from.expected, args.key);
|
|
88
|
+
const actual = read(args.from.actual, args.key);
|
|
89
|
+
const { entries, depth } = args.from;
|
|
90
|
+
const entry = entryOf(args.path, expected, actual);
|
|
91
|
+
if (entry.same || depth >= MAX_DEPTH || !comparable(expected, actual)) entries.push(entry);
|
|
92
|
+
else walk({ expected, actual, path: args.path, entries, depth: depth + 1 });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function entryOf(path: string, expected: unknown, actual: unknown): DiffEntry {
|
|
96
|
+
const left = formatValue(expected);
|
|
97
|
+
const right = formatValue(actual);
|
|
98
|
+
return { path, expected: clamp(left), actual: clamp(right), same: left === right };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Every field either side has, in expected-first order. */
|
|
102
|
+
function keysOf(expected: unknown, actual: unknown): string[] {
|
|
103
|
+
const keys = Object.keys(expected as object);
|
|
104
|
+
for (const key of Object.keys(actual as object)) {
|
|
105
|
+
if (!keys.includes(key)) keys.push(key);
|
|
106
|
+
}
|
|
107
|
+
return keys;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function childPath(args: { parent: string; key: string; list: boolean }): string {
|
|
111
|
+
return args.list ? `${args.parent}[${args.key}]` : `${args.parent}.${args.key}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function read(value: unknown, key: string): unknown {
|
|
115
|
+
return (value as Record<string, unknown>)[key];
|
|
116
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One field of a structured comparison. Fields that agree are carried too: what
|
|
3
|
+
* makes a diff readable is the one line that moved standing among the ones that
|
|
4
|
+
* did not.
|
|
5
|
+
*/
|
|
6
|
+
export interface DiffEntry {
|
|
7
|
+
/** Where the field sits under the subject: `.status`, `[2]`, `.items[0].id`. */
|
|
8
|
+
path: string;
|
|
9
|
+
expected: string;
|
|
10
|
+
actual: string;
|
|
11
|
+
same: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Structured expected-vs-actual (§16), never a `toString`. */
|
|
15
|
+
export type Diff =
|
|
16
|
+
| { kind: "scalar"; expected: string; actual: string }
|
|
17
|
+
| { kind: "json"; path: string; expected: unknown; actual: unknown }
|
|
18
|
+
| { kind: "text"; expected: string; actual: string }
|
|
19
|
+
/** The two sides walked field by field; a list position counts as a field. */
|
|
20
|
+
| { kind: "fields"; label: string; entries: readonly DiffEntry[] };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Past this a value stops informing and starts scrolling. */
|
|
2
|
+
const LIMIT = 200;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One side of a diff, rendered the way the language shows values: strings quoted
|
|
6
|
+
* so `"1"` never reads as `1`, structures as compact JSON. Never `[object
|
|
7
|
+
* Object]`, because a failure that hides what it compared is not a report.
|
|
8
|
+
*/
|
|
9
|
+
export function formatValue(value: unknown): string {
|
|
10
|
+
if (value === undefined) return "absent";
|
|
11
|
+
if (value === null) return "null";
|
|
12
|
+
if (typeof value === "function") return "fn";
|
|
13
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
14
|
+
if (typeof value !== "object") return String(value);
|
|
15
|
+
return json(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Trim for display only. Sameness is decided on the untrimmed rendering. */
|
|
19
|
+
export function clamp(text: string): string {
|
|
20
|
+
return text.length <= LIMIT ? text : `${text.slice(0, LIMIT)}…`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function json(value: object): string {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.stringify(value) ?? shapeOf(value);
|
|
26
|
+
} catch {
|
|
27
|
+
// Cyclic, or a BigInt inside: say what it is rather than what it prints as.
|
|
28
|
+
return shapeOf(value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shapeOf(value: object): string {
|
|
33
|
+
if (Array.isArray(value)) return `a list of ${value.length} ${plural("item", value.length)}`;
|
|
34
|
+
const count = Object.keys(value).length;
|
|
35
|
+
return `a map with ${count} ${plural("field", count)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function plural(noun: string, count: number): string {
|
|
39
|
+
return count === 1 ? noun : `${noun}s`;
|
|
40
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { buildDiff } from "./build-diff.js";
|
|
2
|
+
export type { Diff, DiffEntry } from "./diff.types.js";
|
|
3
|
+
export { formatValue } from "./format-value.js";
|
|
4
|
+
export type { Problem } from "./problem.types.js";
|
|
5
|
+
export { ProblemError } from "./problem-error.js";
|
|
6
|
+
export type { RelatedInfo } from "./related.types.js";
|
|
7
|
+
export type { Severity } from "./severity.types.js";
|
|
8
|
+
export type { Span } from "./span.types.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Problem } from "./problem.types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Carries a {@link Problem} across a throw boundary at runtime. Catch it to
|
|
5
|
+
* recover the structured problem; `message` is only the title.
|
|
6
|
+
*/
|
|
7
|
+
export class ProblemError extends Error {
|
|
8
|
+
readonly problem: Problem;
|
|
9
|
+
|
|
10
|
+
constructor(problem: Problem) {
|
|
11
|
+
super(problem.title);
|
|
12
|
+
this.name = "ProblemError";
|
|
13
|
+
this.problem = problem;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Diff } from "./diff.types.js";
|
|
2
|
+
import type { RelatedInfo } from "./related.types.js";
|
|
3
|
+
import type { Severity } from "./severity.types.js";
|
|
4
|
+
import type { Span } from "./span.types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The single shape shared by compile diagnostics and runtime failures, so one
|
|
8
|
+
* renderer serves terminal, editor, and UI (§16). Optional fields are populated
|
|
9
|
+
* as each producer has the information.
|
|
10
|
+
*/
|
|
11
|
+
export interface Problem {
|
|
12
|
+
code: string;
|
|
13
|
+
severity: Severity;
|
|
14
|
+
title: string;
|
|
15
|
+
span: Span;
|
|
16
|
+
help?: string;
|
|
17
|
+
related?: RelatedInfo[];
|
|
18
|
+
diff?: Diff;
|
|
19
|
+
note?: string;
|
|
20
|
+
docs?: string;
|
|
21
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { splitCall } from "../ast/index.js";
|
|
2
|
+
import type { Expr, MapLit } from "../generated/ast.js";
|
|
3
|
+
import type { Infer } from "./infer.js";
|
|
4
|
+
import { inferExpr } from "./infer.js";
|
|
5
|
+
import type { FnType, Type } from "./type.types.js";
|
|
6
|
+
import { DYNAMIC } from "./type.types.js";
|
|
7
|
+
import type { TypeEnv } from "./type-env.js";
|
|
8
|
+
import { unify } from "./unify.js";
|
|
9
|
+
|
|
10
|
+
/** A verb call, however it was written: bound to a name, or standing alone. */
|
|
11
|
+
export interface CallSite {
|
|
12
|
+
target: string;
|
|
13
|
+
args: readonly Expr[];
|
|
14
|
+
opts?: MapLit;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* What a verb gives back, and what it makes of the arguments on the way.
|
|
19
|
+
*
|
|
20
|
+
* This is where a typed world meets an untyped one. With no signature the result
|
|
21
|
+
* is `dynamic` and nothing is checked, because the language has to keep working
|
|
22
|
+
* with plugins that never say a word about types. With one, the arguments are
|
|
23
|
+
* unified against it, which is how `http.on api req => …` learns that `req` is a
|
|
24
|
+
* request without anyone writing it down.
|
|
25
|
+
*/
|
|
26
|
+
export function callType(site: CallSite, env: TypeEnv, infer: Infer): Type {
|
|
27
|
+
const signature = infer.catalog?.signatureOf(site.target);
|
|
28
|
+
// The trailing `{ … }` is the options in both spellings, so it is told apart
|
|
29
|
+
// here too. Otherwise `http.get(url, { … })` would line the map up against the
|
|
30
|
+
// URL's parameter and the checker would disagree with the runtime.
|
|
31
|
+
const split = splitCall(site.args, signature?.params.length ?? 0);
|
|
32
|
+
const args = split.args.map((arg) => inferExpr(arg, env, infer));
|
|
33
|
+
const opts = site.opts ?? split.opts;
|
|
34
|
+
if (opts) inferExpr(opts, env, infer);
|
|
35
|
+
if (!signature) return DYNAMIC;
|
|
36
|
+
applyParams(signature, args);
|
|
37
|
+
return signature.result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Line the arguments up with the parameters, quietly.
|
|
42
|
+
*
|
|
43
|
+
* A mismatch here is not reported: the options map, the trailing arguments and
|
|
44
|
+
* the shorthands mean a verb can be called in more shapes than one signature
|
|
45
|
+
* describes. The purpose is to *inform* the argument types, a callback's
|
|
46
|
+
* parameters above all, not to police the call.
|
|
47
|
+
*/
|
|
48
|
+
function applyParams(signature: FnType, args: readonly Type[]): void {
|
|
49
|
+
const width = Math.min(signature.params.length, args.length);
|
|
50
|
+
for (let at = 0; at < width; at += 1) {
|
|
51
|
+
unify(args[at] as Type, signature.params[at] as Type);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** What a built-in type is, and how it reads when written. */
|
|
2
|
+
export interface BuiltinType {
|
|
3
|
+
/** One line, in the reader's terms rather than the compiler's. */
|
|
4
|
+
doc: string;
|
|
5
|
+
/** How it is written where a type goes. */
|
|
6
|
+
example: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The types the language brings with it, described once.
|
|
11
|
+
*
|
|
12
|
+
* These are the words a reader meets first, so the hover and the completion list
|
|
13
|
+
* read from here. A built-in that cannot explain itself is a built-in nobody
|
|
14
|
+
* discovers.
|
|
15
|
+
*/
|
|
16
|
+
export const BUILTIN_TYPES: Readonly<Record<string, BuiltinType>> = {
|
|
17
|
+
string: {
|
|
18
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Venn's own interpolation, quoted for a reader.
|
|
19
|
+
doc: "Text. Interpolates with `${…}` and carries the string methods.",
|
|
20
|
+
example: "name: string",
|
|
21
|
+
},
|
|
22
|
+
number: {
|
|
23
|
+
doc: "A number, whole or not. There is no separate integer type.",
|
|
24
|
+
example: "age: number",
|
|
25
|
+
},
|
|
26
|
+
bool: {
|
|
27
|
+
doc: "`true` or `false`, and nothing else — no truthiness, no coercion.",
|
|
28
|
+
example: "active: bool",
|
|
29
|
+
},
|
|
30
|
+
null: { doc: "The absence of a value. Written `null`.", example: "found: string | null" },
|
|
31
|
+
void: { doc: "What a verb answers with when it answers nothing.", example: "-> void" },
|
|
32
|
+
dynamic: {
|
|
33
|
+
doc: "A value whose shape nothing can know — a parsed response, a JSON field. Name a type and annotate a binding to give it one.",
|
|
34
|
+
example: "const price: Price = res.json",
|
|
35
|
+
},
|
|
36
|
+
list: { doc: "Many of one thing, in order.", example: "tags: list<string>" },
|
|
37
|
+
map: { doc: "Keys not known ahead of time, values all alike.", example: "headers: map<string>" },
|
|
38
|
+
duration: {
|
|
39
|
+
doc: "A length of time: `500ms`, `2s`, `1m`. Arithmetic keeps the unit.",
|
|
40
|
+
example: "timeout: duration",
|
|
41
|
+
},
|
|
42
|
+
size: {
|
|
43
|
+
doc: "A size in bytes: `4kb`, `2mb`. Arithmetic keeps the unit.",
|
|
44
|
+
example: "limit: size",
|
|
45
|
+
},
|
|
46
|
+
percent: {
|
|
47
|
+
doc: "A proportion: `5%`, `99.9%`. Reads back as `.ratio` or `.percent`.",
|
|
48
|
+
example: "rate: percent",
|
|
49
|
+
},
|
|
50
|
+
instant: {
|
|
51
|
+
doc: "A moment in time, written as an ISO date: `2026-07-27T12:00:00Z`.",
|
|
52
|
+
example: "at: instant",
|
|
53
|
+
},
|
|
54
|
+
never: {
|
|
55
|
+
doc: "What a verb that always fails answers with. No value has this type.",
|
|
56
|
+
example: "-> never",
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Whether a name is one of the language's own types. */
|
|
61
|
+
export function isBuiltinType(name: string): boolean {
|
|
62
|
+
return name in BUILTIN_TYPES;
|
|
63
|
+
}
|