@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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +303 -0
  3. package/dist/index.d.ts +3654 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +13137 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +61 -0
  8. package/src/ast/call-args.ts +15 -0
  9. package/src/ast/dotted-path.ts +22 -0
  10. package/src/ast/index.ts +7 -0
  11. package/src/ast/is-statement.ts +40 -0
  12. package/src/ast/split-call.ts +28 -0
  13. package/src/ast/walk.ts +6 -0
  14. package/src/codes/build-problem.ts +24 -0
  15. package/src/codes/catalog.ts +35 -0
  16. package/src/codes/code.types.ts +7 -0
  17. package/src/codes/index.ts +3 -0
  18. package/src/compile/compile.ts +169 -0
  19. package/src/compile/compile.types.ts +40 -0
  20. package/src/compile/index.ts +3 -0
  21. package/src/compile/lex-scope.ts +78 -0
  22. package/src/compile/nodes/binary.ts +56 -0
  23. package/src/compile/nodes/call.ts +50 -0
  24. package/src/compile/nodes/collection.ts +83 -0
  25. package/src/compile/nodes/const-lit.ts +30 -0
  26. package/src/compile/nodes/fast-binary.ts +71 -0
  27. package/src/compile/nodes/fn.ts +87 -0
  28. package/src/compile/nodes/index.ts +8 -0
  29. package/src/compile/nodes/literal.ts +86 -0
  30. package/src/compile/nodes/map-fields.ts +79 -0
  31. package/src/compile/nodes/member.ts +32 -0
  32. package/src/events/envelope.types.ts +17 -0
  33. package/src/events/event-data.types.ts +25 -0
  34. package/src/events/ids.types.ts +10 -0
  35. package/src/events/index.ts +5 -0
  36. package/src/events/run-plan.types.ts +18 -0
  37. package/src/events/status.types.ts +2 -0
  38. package/src/expand/accepted-kinds.ts +23 -0
  39. package/src/expand/deco/deco-decorator.ts +77 -0
  40. package/src/expand/deco/deco-env.ts +24 -0
  41. package/src/expand/deco/deco.types.ts +43 -0
  42. package/src/expand/deco/document-decos.ts +64 -0
  43. package/src/expand/deco/index.ts +7 -0
  44. package/src/expand/deco/read-signature.ts +57 -0
  45. package/src/expand/deco/run-body.ts +119 -0
  46. package/src/expand/decorate-callable.ts +49 -0
  47. package/src/expand/decorations.ts +41 -0
  48. package/src/expand/expand.ts +108 -0
  49. package/src/expand/expand.types.ts +78 -0
  50. package/src/expand/handles/around-verbs.ts +35 -0
  51. package/src/expand/handles/binding-verbs.ts +27 -0
  52. package/src/expand/handles/common-verbs.ts +40 -0
  53. package/src/expand/handles/fn-verbs.ts +58 -0
  54. package/src/expand/handles/handle.types.ts +33 -0
  55. package/src/expand/handles/index.ts +4 -0
  56. package/src/expand/handles/kind-of.ts +36 -0
  57. package/src/expand/handles/make-handle.ts +100 -0
  58. package/src/expand/handles/missing-verb.ts +27 -0
  59. package/src/expand/handles/runnable-verbs.ts +13 -0
  60. package/src/expand/handles/type-verbs.ts +56 -0
  61. package/src/expand/index.ts +40 -0
  62. package/src/expand/make-context.ts +69 -0
  63. package/src/expand/node-meta.ts +30 -0
  64. package/src/expand/node-span.ts +17 -0
  65. package/src/expand/swap-node.ts +28 -0
  66. package/src/expand/wrong-kind.ts +102 -0
  67. package/src/expr/cell.types.ts +25 -0
  68. package/src/expr/closure.ts +53 -0
  69. package/src/expr/closure.types.ts +31 -0
  70. package/src/expr/eval-env.types.ts +9 -0
  71. package/src/expr/evaluate.ts +18 -0
  72. package/src/expr/frame.ts +60 -0
  73. package/src/expr/index.ts +14 -0
  74. package/src/expr/invoke.ts +140 -0
  75. package/src/expr/member-value.ts +91 -0
  76. package/src/expr/methods/index.ts +65 -0
  77. package/src/expr/methods/list-grouping.ts +107 -0
  78. package/src/expr/methods/list-methods.ts +57 -0
  79. package/src/expr/methods/list-selection.ts +142 -0
  80. package/src/expr/methods/map-extras.ts +87 -0
  81. package/src/expr/methods/map-methods.ts +17 -0
  82. package/src/expr/methods/number-methods.ts +29 -0
  83. package/src/expr/methods/string-extras.ts +66 -0
  84. package/src/expr/methods/string-methods.ts +32 -0
  85. package/src/expr/methods/unit-methods.ts +54 -0
  86. package/src/expr/namespace.ts +20 -0
  87. package/src/expr/native.types.ts +40 -0
  88. package/src/expr/operators.ts +91 -0
  89. package/src/expr/pending.ts +47 -0
  90. package/src/expr/prelude.ts +75 -0
  91. package/src/expr/task.ts +71 -0
  92. package/src/format/format-text.ts +22 -0
  93. package/src/format/format.types.ts +18 -0
  94. package/src/format/from-settings.ts +40 -0
  95. package/src/format/index.ts +5 -0
  96. package/src/format/organize-header.ts +66 -0
  97. package/src/format/reindent.ts +35 -0
  98. package/src/generated/ast.ts +2383 -0
  99. package/src/generated/grammar.ts +5236 -0
  100. package/src/generated/module.ts +25 -0
  101. package/src/grammar/venn.langium +291 -0
  102. package/src/graph/graph.types.ts +20 -0
  103. package/src/graph/index.ts +2 -0
  104. package/src/graph/to-graph.ts +87 -0
  105. package/src/index.ts +93 -0
  106. package/src/interpolation/compile-template.ts +40 -0
  107. package/src/interpolation/index.ts +4 -0
  108. package/src/interpolation/interpolation.types.ts +18 -0
  109. package/src/interpolation/scan-interpolations.ts +47 -0
  110. package/src/interpolation/template.types.ts +19 -0
  111. package/src/lang/default-services.ts +14 -0
  112. package/src/lang/index.ts +3 -0
  113. package/src/lang/venn-lexer.ts +34 -0
  114. package/src/lang/venn-module.ts +51 -0
  115. package/src/module/index.ts +5 -0
  116. package/src/module/specifier.ts +21 -0
  117. package/src/parse/error-to-problem.ts +43 -0
  118. package/src/parse/index.ts +3 -0
  119. package/src/parse/parse-expression.ts +27 -0
  120. package/src/parse/parse-output.types.ts +8 -0
  121. package/src/parse/parse.ts +26 -0
  122. package/src/problem/build-diff.ts +116 -0
  123. package/src/problem/diff.types.ts +20 -0
  124. package/src/problem/format-value.ts +40 -0
  125. package/src/problem/index.ts +8 -0
  126. package/src/problem/problem-error.ts +15 -0
  127. package/src/problem/problem.types.ts +21 -0
  128. package/src/problem/related.types.ts +7 -0
  129. package/src/problem/severity.types.ts +2 -0
  130. package/src/problem/span.types.ts +8 -0
  131. package/src/typecheck/action-signature.ts +53 -0
  132. package/src/typecheck/builtin-types.ts +63 -0
  133. package/src/typecheck/builtins.ts +267 -0
  134. package/src/typecheck/catalog.types.ts +16 -0
  135. package/src/typecheck/check-deco.ts +174 -0
  136. package/src/typecheck/check-stmts.ts +151 -0
  137. package/src/typecheck/check-types.ts +232 -0
  138. package/src/typecheck/context.ts +31 -0
  139. package/src/typecheck/imported-types.ts +127 -0
  140. package/src/typecheck/index.ts +26 -0
  141. package/src/typecheck/infer.ts +416 -0
  142. package/src/typecheck/kind-types.ts +79 -0
  143. package/src/typecheck/member-docs.ts +211 -0
  144. package/src/typecheck/named-types.ts +57 -0
  145. package/src/typecheck/prelude-types.ts +152 -0
  146. package/src/typecheck/reshaped-fns.ts +58 -0
  147. package/src/typecheck/scheme.ts +75 -0
  148. package/src/typecheck/seed-params.ts +72 -0
  149. package/src/typecheck/seed-values.ts +64 -0
  150. package/src/typecheck/show.ts +64 -0
  151. package/src/typecheck/solidify.ts +60 -0
  152. package/src/typecheck/spec-to-type.ts +88 -0
  153. package/src/typecheck/type-env.ts +46 -0
  154. package/src/typecheck/type-ref.ts +62 -0
  155. package/src/typecheck/type.types.ts +168 -0
  156. package/src/typecheck/unify.ts +192 -0
  157. package/src/typecheck/unit-types.ts +49 -0
  158. package/src/units/combine.ts +97 -0
  159. package/src/units/index.ts +6 -0
  160. package/src/units/parse-instant.ts +12 -0
  161. package/src/units/parse-number.ts +48 -0
  162. package/src/units/unit-guards.ts +16 -0
  163. package/src/units/unit.types.ts +38 -0
  164. package/src/value/equals.ts +4 -0
  165. package/src/value/index.ts +4 -0
  166. package/src/value/is-numeric.ts +6 -0
  167. package/src/value/truthiness.ts +9 -0
  168. package/src/value/value.types.ts +12 -0
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@venn-lang/core",
3
+ "version": "0.1.0",
4
+ "description": "The Venn compiler: grammar, parser, type checker, expression compiler, decorator expansion, and the Problem model.",
5
+ "keywords": [
6
+ "venn",
7
+ "testing",
8
+ "e2e",
9
+ "compiler",
10
+ "parser",
11
+ "langium",
12
+ "dsl"
13
+ ],
14
+ "homepage": "https://github.com/venn-lang/venn/tree/main/packages/core#readme",
15
+ "bugs": "https://github.com/venn-lang/venn/issues",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/venn-lang/venn.git",
19
+ "directory": "packages/core"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Vinicius Borges",
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "exports": {
26
+ ".": {
27
+ "development": "./src/index.ts",
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "src",
36
+ "!src/**/*.test.ts",
37
+ "!src/**/*.suite.ts"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "langium": "^4.3.1",
44
+ "@venn-lang/contracts": "0.1.0",
45
+ "@venn-lang/types": "0.1.0"
46
+ },
47
+ "devDependencies": {
48
+ "@fast-check/vitest": "^0.4.1",
49
+ "langium-cli": "^4.3.0",
50
+ "tsdown": "^0.22.14",
51
+ "typescript": "^7.0.2",
52
+ "vitest": "^4.1.10"
53
+ },
54
+ "scripts": {
55
+ "langium:generate": "langium generate",
56
+ "prebuild": "pnpm run langium:generate",
57
+ "build": "tsdown",
58
+ "test": "vitest run",
59
+ "typecheck": "tsc --noEmit"
60
+ }
61
+ }
@@ -0,0 +1,15 @@
1
+ import type { ActionCall, Expr } from "../generated/ast.js";
2
+
3
+ /**
4
+ * The arguments of a call, however it was written.
5
+ *
6
+ * The language spells a call two ways, `conn.close()` with brackets and
7
+ * `http.get "url"` without, and the parser keeps them in different places on
8
+ * the node. They are the same call with the same arguments, so every reader
9
+ * downstream asks here and sees one list.
10
+ */
11
+ export function callArgs(call: ActionCall): readonly Expr[] {
12
+ const bracketed = call.call?.args ?? [];
13
+ if (bracketed.length === 0) return call.args;
14
+ return [...bracketed.map((arg) => arg.value), ...call.args];
15
+ }
@@ -0,0 +1,22 @@
1
+ import type { AstNode } from "langium";
2
+ import type { Expr } from "../generated/ast.js";
3
+ import { isMember, isRef } from "../generated/ast.js";
4
+
5
+ /**
6
+ * The dotted name an expression spells, such as `http.get` or
7
+ * `data.faker.uuid`.
8
+ *
9
+ * The parser cannot tell a namespace from a field: `res.status` and `http.get`
10
+ * are the same shape. Only a registry knows which is which, so the name is
11
+ * carried this far as text and asked about there.
12
+ *
13
+ * @returns The dotted name, or `undefined` when the expression is not a plain
14
+ * chain of identifiers.
15
+ */
16
+ export function dottedPath(expr: Expr | AstNode | undefined): string | undefined {
17
+ if (!expr) return undefined;
18
+ if (isRef(expr)) return expr.name;
19
+ if (!isMember(expr) || expr.optional) return undefined;
20
+ const receiver = dottedPath(expr.receiver);
21
+ return receiver === undefined ? undefined : `${receiver}.${expr.member}`;
22
+ }
@@ -0,0 +1,7 @@
1
+ export type { AstNode } from "langium";
2
+ export { callArgs } from "./call-args.js";
3
+ export { dottedPath } from "./dotted-path.js";
4
+ export { isRunnable } from "./is-statement.js";
5
+ export type { SplitCall } from "./split-call.js";
6
+ export { splitCall } from "./split-call.js";
7
+ export { walkAst } from "./walk.js";
@@ -0,0 +1,40 @@
1
+ import {
2
+ type Declaration,
3
+ isConfigDecl,
4
+ isDatasetDecl,
5
+ isDecoDecl,
6
+ isFactoryDecl,
7
+ isFlowDecl,
8
+ isFnDecl,
9
+ isFragmentDecl,
10
+ isLifecycleDecl,
11
+ isMatrixDecl,
12
+ isReportDecl,
13
+ isTypeDecl,
14
+ type Statement,
15
+ } from "../generated/ast.js";
16
+
17
+ /**
18
+ * Whether a top-level node runs, or merely defines.
19
+ *
20
+ * A `flow`, a `fn`, a `type` are definitions: they exist for something else to
21
+ * reach. Everything else at the top of a file is a statement, and statements
22
+ * run in the order they are written. That holds in a script, where the file is
23
+ * the program, and in a test file, so the same lines mean the same thing in
24
+ * both.
25
+ */
26
+ export function isRunnable(node: Declaration): node is Declaration & Statement {
27
+ return !(
28
+ isFlowDecl(node) ||
29
+ isFnDecl(node) ||
30
+ isFragmentDecl(node) ||
31
+ isTypeDecl(node) ||
32
+ isFactoryDecl(node) ||
33
+ isDatasetDecl(node) ||
34
+ isConfigDecl(node) ||
35
+ isMatrixDecl(node) ||
36
+ isReportDecl(node) ||
37
+ isDecoDecl(node) ||
38
+ isLifecycleDecl(node)
39
+ );
40
+ }
@@ -0,0 +1,28 @@
1
+ import type { Expr, MapLit } from "../generated/ast.js";
2
+ import { isMapLit } from "../generated/ast.js";
3
+
4
+ /** A call's arguments, told apart: what it takes, and what configures it. */
5
+ export interface SplitCall {
6
+ args: readonly Expr[];
7
+ opts?: MapLit;
8
+ }
9
+
10
+ /**
11
+ * Tell a verb's positional arguments from its options map.
12
+ *
13
+ * The language spells one call two ways, and the trailing `{ … }` means the
14
+ * same in both: `http.get "url" { headers }` and `http.get("url", { headers })`
15
+ * are the same request. Only the first spelling puts the map somewhere the
16
+ * parser can label, so the second is disambiguated here.
17
+ *
18
+ * A map beyond the declared positionals is the options; a map *within* them is
19
+ * an argument. `db.seed({ users: […] })` passes a map because seeding is what
20
+ * it does, and that must never be read as configuration.
21
+ *
22
+ * @param takes How many positional arguments the verb declares.
23
+ */
24
+ export function splitCall(args: readonly Expr[], takes: number): SplitCall {
25
+ const last = args[args.length - 1];
26
+ if (args.length <= takes || !last || !isMapLit(last)) return { args };
27
+ return { args: args.slice(0, -1), opts: last };
28
+ }
@@ -0,0 +1,6 @@
1
+ import { type AstNode, AstUtils } from "langium";
2
+
3
+ /** Every descendant of `root`, depth-first, for whole-document passes. */
4
+ export function walkAst(root: AstNode): AstNode[] {
5
+ return AstUtils.streamAllContents(root).toArray();
6
+ }
@@ -0,0 +1,24 @@
1
+ import type { Diff, Problem, RelatedInfo, Span } from "../problem/index.js";
2
+ import type { CodeSpec } from "./code.types.js";
3
+
4
+ /** Construct a {@link Problem} from a catalog spec plus location and message. */
5
+ export function buildProblem(args: {
6
+ spec: CodeSpec;
7
+ span: Span;
8
+ title: string;
9
+ help?: string;
10
+ related?: RelatedInfo[];
11
+ diff?: Diff;
12
+ note?: string;
13
+ }): Problem {
14
+ return {
15
+ code: args.spec.code,
16
+ severity: args.spec.severity,
17
+ title: args.title,
18
+ span: args.span,
19
+ help: args.help,
20
+ related: args.related,
21
+ diff: args.diff,
22
+ note: args.note,
23
+ };
24
+ }
@@ -0,0 +1,35 @@
1
+ import type { CodeSpec } from "./code.types.js";
2
+
3
+ /**
4
+ * Every VNxxxx the kernel itself can raise. Codes are stable, googlable, and
5
+ * documented; families follow the leading digit (1 lex/syntax … 8 timeout).
6
+ */
7
+ export const CODES = {
8
+ VN1001_LEX: { code: "VN1001", severity: "error" },
9
+ VN1002_PARSE: { code: "VN1002", severity: "error" },
10
+ VN2003_UNKNOWN_ACTION: { code: "VN2003", severity: "error" },
11
+ VN2004_UNKNOWN_MATCHER: { code: "VN2004", severity: "error" },
12
+ VN2005_UNKNOWN_FRAGMENT: { code: "VN2005", severity: "error" },
13
+ VN2006_UNKNOWN_ENV: { code: "VN2006", severity: "error" },
14
+ VN2007_NAMESPACE_NOT_IMPORTED: { code: "VN2007", severity: "error" },
15
+ VN2008_UNCALLED_ACTION: { code: "VN2008", severity: "error" },
16
+ VN2009_NOT_EXPORTED: { code: "VN2009", severity: "error" },
17
+ VN2013_UNKNOWN_DECORATOR: { code: "VN2013", severity: "error" },
18
+ VN2014_DECORATOR_TARGET: { code: "VN2014", severity: "error" },
19
+ VN2015_DECO_SIGNATURE: { code: "VN2015", severity: "error" },
20
+ VN2016_DECO_IMPURE: { code: "VN2016", severity: "error" },
21
+ VN2017_DECO_VERB: { code: "VN2017", severity: "error" },
22
+ VN3001_UNKNOWN_OPTION: { code: "VN3001", severity: "error" },
23
+ VN3010_TYPE_MISMATCH: { code: "VN3010", severity: "error" },
24
+ VN3012_UNIT_MISMATCH: { code: "VN3012", severity: "error" },
25
+ VN3013_NOT_CALLABLE: { code: "VN3013", severity: "error" },
26
+ VN3014_STILL_WAITING: { code: "VN3014", severity: "error" },
27
+ VN3015_NOT_A_LIST: { code: "VN3015", severity: "error" },
28
+ VN3016_NOT_A_NUMBER: { code: "VN3016", severity: "error" },
29
+ VN3017_DECO_ARGUMENTS: { code: "VN3017", severity: "error" },
30
+ VN5001_REMOVED_KEYWORD: { code: "VN5001", severity: "error" },
31
+ VN6001_ASSERTION_FAILED: { code: "VN6001", severity: "error" },
32
+ VN7001_ACTION_FAILED: { code: "VN7001", severity: "error" },
33
+ VN7004_HOOK_FAILED: { code: "VN7004", severity: "error" },
34
+ VN8002_LOOP_LIMIT: { code: "VN8002", severity: "error" },
35
+ } as const satisfies Record<string, CodeSpec>;
@@ -0,0 +1,7 @@
1
+ import type { Severity } from "../problem/index.js";
2
+
3
+ /** A catalog entry: a stable code plus its default severity. */
4
+ export interface CodeSpec {
5
+ code: string;
6
+ severity: Severity;
7
+ }
@@ -0,0 +1,3 @@
1
+ export { buildProblem } from "./build-problem.js";
2
+ export { CODES } from "./catalog.js";
3
+ export type { CodeSpec } from "./code.types.js";
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Expression compilation: a tree becomes a function of the environment.
3
+ *
4
+ * A program's tree is fixed once parsed, so every decision the source settles
5
+ * (which operator, which literal value, which slot a name lives in, how a
6
+ * string splits around its placeholders) is made once here instead of on every
7
+ * visit.
8
+ */
9
+
10
+ import type { Cell } from "../expr/cell.types.js";
11
+ import type { Closure } from "../expr/closure.types.js";
12
+ import type { EvalEnv } from "../expr/eval-env.types.js";
13
+ import type { Frame } from "../expr/frame.js";
14
+ import type { Expr, FnDecl } from "../generated/ast.js";
15
+ import type { Compile, Thunk } from "./compile.types.js";
16
+ import { freeSlot, type LexScope, rootScope } from "./lex-scope.js";
17
+ import {
18
+ closureIn,
19
+ compileBinary,
20
+ compileCall,
21
+ compileFnExpr,
22
+ compileIndex,
23
+ compileInstant,
24
+ compileList,
25
+ compileMap,
26
+ compileMember,
27
+ compileNumber,
28
+ compileString,
29
+ compileTernary,
30
+ compileUnary,
31
+ constant,
32
+ constThunk,
33
+ } from "./nodes/index.js";
34
+
35
+ const ROOT = rootScope();
36
+
37
+ /**
38
+ * Compile an expression into a function of the environment.
39
+ *
40
+ * Memoised per node, so asking twice for the same expression hands back the
41
+ * same thunk.
42
+ *
43
+ * @returns A thunk: give it an environment and it produces the value.
44
+ */
45
+ export function compileExpr(expr: Expr): Thunk {
46
+ return compileIn(expr, ROOT);
47
+ }
48
+
49
+ /**
50
+ * Build the function value for a top-level `fn name(…)`.
51
+ *
52
+ * The body is compiled against `env`, which is what lets a recursive
53
+ * declaration reach its own name: the cell is handed out before the binding
54
+ * that fills it exists.
55
+ *
56
+ * @returns The closure, ready to be bound under the declaration's name.
57
+ */
58
+ export function closureOfDecl(decl: FnDecl, env: EvalEnv): Closure {
59
+ return closureIn(decl, env, compileIn);
60
+ }
61
+
62
+ function compileIn(expr: Expr, scope: LexScope): Thunk {
63
+ const known = scope.cache.get(expr);
64
+ if (known) return known;
65
+ const thunk = dispatch(expr, scope);
66
+ scope.cache.set(expr, thunk);
67
+ return thunk;
68
+ }
69
+
70
+ const NOTHING: Thunk = () => undefined;
71
+
72
+ function dispatch(expr: Expr, scope: LexScope): Thunk {
73
+ switch (expr.$type) {
74
+ case "NumberLit":
75
+ return compileNumber(expr);
76
+ case "StringLit":
77
+ // A placeholder compiles in the scope that holds it, so `"${i}"` inside a
78
+ // function reads `i` as that function's slot rather than asking for it by
79
+ // name. Its parsed expression is shared by every literal with the same
80
+ // text, which is safe only because the compiled form is cached per scope:
81
+ // two scopes reach the same node and each keeps its own thunk for it.
82
+ return compileString(expr, (inner) => compileIn(inner, scope));
83
+ case "InstantLit":
84
+ return compileInstant(expr);
85
+ case "BoolLit":
86
+ return constant(expr.value === "true");
87
+ case "NullLit":
88
+ return constant(null);
89
+ case "Ref":
90
+ return compileRef(expr.name, scope);
91
+ case "FnExpr":
92
+ // A function made here captures this environment to reach the names
93
+ // around it, so there has to be one for it to capture.
94
+ scope.dynamic = true;
95
+ return compileFnExpr(expr, compileIn);
96
+ default:
97
+ return operation(expr, (inner) => compileIn(inner, scope));
98
+ }
99
+ }
100
+
101
+ function operation(expr: Expr, compile: Compile): Thunk {
102
+ switch (expr.$type) {
103
+ case "Member":
104
+ return compileMember(expr, compile);
105
+ case "FnExpr":
106
+ return compileFnExpr(expr, compileIn);
107
+ case "Call":
108
+ return compileCall(expr, compile);
109
+ case "Index":
110
+ return compileIndex(expr, compile);
111
+ case "Binary":
112
+ return compileBinary(expr, compile);
113
+ case "Unary":
114
+ return compileUnary(expr, compile);
115
+ case "Ternary":
116
+ return compileTernary(expr, compile);
117
+ case "ListLit":
118
+ return compileList(expr, compile);
119
+ case "MapLit":
120
+ return compileMap(expr, compile);
121
+ default:
122
+ // Not a node the grammar can produce: the only one left is the value a
123
+ // decorator's `.setValue` put here.
124
+ return constThunk(expr) ?? NOTHING;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Compile a name into the cheapest read available.
130
+ *
131
+ * A name the enclosing function binds is an index into the frame. Anything else
132
+ * (a top-level binding, a namespace, the prelude) is a cell the closure
133
+ * resolved when it was built, which is an index too.
134
+ *
135
+ * The lookup fallback stays for environments that hand out no cells: a function
136
+ * nested inside another reads its parent's frame, and an expression compiled at
137
+ * the root has no closure at all.
138
+ */
139
+ function compileRef(name: string, scope: LexScope): Thunk {
140
+ const slot = scope.names.indexOf(name);
141
+ // The one name a bare body binds is the value it was handed, unwrapped.
142
+ if (slot === 0 && scope.bare) return (env) => env;
143
+ if (slot !== -1) return slotThunk(slot);
144
+ // One closure owns this body, so the cell can be held right here: no frame to
145
+ // carry it, and no chain to walk.
146
+ const own = scope.cellOf?.(name);
147
+ if (own) return () => own.value;
148
+ const up = freeSlot(scope, name);
149
+ if (up === undefined) return (env) => env.lookup(name);
150
+ return (env) => {
151
+ const cells = (env as Frame).up;
152
+ return cells === undefined ? env.lookup(name) : (cells[up] as Cell).value;
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Reading one slot, written for the slot it reads.
158
+ *
159
+ * The first three are fields of the frame, so each gets a thunk naming that
160
+ * field outright: one property load, and the same one every time this thunk
161
+ * runs, which is what lets V8 settle the site.
162
+ */
163
+ function slotThunk(slot: number): Thunk {
164
+ if (slot === 0) return (env) => (env as Frame).s0;
165
+ if (slot === 1) return (env) => (env as Frame).s1;
166
+ if (slot === 2) return (env) => (env as Frame).s2;
167
+ const at = slot - 3;
168
+ return (env) => ((env as Frame).rest as unknown[])[at];
169
+ }
@@ -0,0 +1,40 @@
1
+ import type { EvalEnv } from "../expr/eval-env.types.js";
2
+ import type { Expr } from "../generated/ast.js";
3
+
4
+ /**
5
+ * A compiled expression: everything the source decided is already captured, so
6
+ * running it needs only the environment.
7
+ */
8
+ export type Thunk = (env: EvalEnv) => unknown;
9
+
10
+ /**
11
+ * Compile a sub-expression.
12
+ *
13
+ * Passed into each node's compiler rather than imported, so a compiler for one
14
+ * kind of node never depends on the dispatcher that reaches it.
15
+ */
16
+ export type Compile = (expr: Expr) => Thunk;
17
+
18
+ /** A function body, compiled: its local bindings in order, then its result. */
19
+ export interface CompiledBody {
20
+ /** Every name that gets a slot: the parameters first, then the locals. */
21
+ readonly names: readonly string[];
22
+ /** How many names spill past the frame's inline slots. Usually none. */
23
+ readonly extra: number;
24
+ /** Every name the body reads but does not bind, in cell order. */
25
+ readonly free: readonly string[];
26
+ /**
27
+ * The body binds one name, reads nothing else, and asks for nothing by text,
28
+ * so a call hands the argument straight to `result` with no frame between.
29
+ */
30
+ readonly bare: boolean;
31
+ readonly locals: readonly CompiledLocal[];
32
+ readonly result: Thunk;
33
+ }
34
+
35
+ /** One of the body's `let` bindings, compiled. Filled in declaration order. */
36
+ export interface CompiledLocal {
37
+ /** Which slot of the frame this local writes to. */
38
+ readonly slot: number;
39
+ readonly value: Thunk;
40
+ }
@@ -0,0 +1,3 @@
1
+ export { closureOfDecl, compileExpr } from "./compile.js";
2
+ export type { Compile, CompiledBody, CompiledLocal, Thunk } from "./compile.types.js";
3
+ export { constLit } from "./nodes/index.js";
@@ -0,0 +1,78 @@
1
+ import type { Cell } from "../expr/cell.types.js";
2
+ import type { Expr, FnBody, ParamList } from "../generated/ast.js";
3
+ import type { Thunk } from "./compile.types.js";
4
+
5
+ /**
6
+ * What the compiler knows by name at one point in the source: the parameters
7
+ * and locals of the function being compiled, in slot order.
8
+ *
9
+ * Only the *current* function. A name from an enclosing one is left dynamic:
10
+ * resolving across frames would mean walking a chain at run time, which is the
11
+ * cost slots exist to remove.
12
+ */
13
+ export interface LexScope {
14
+ readonly names: readonly string[];
15
+ /**
16
+ * Compiled nodes for this scope. Per scope, not global: a placeholder's
17
+ * expression is shared between every string literal with the same text, so a
18
+ * single cache would hand one scope's slot numbers to another.
19
+ */
20
+ readonly cache: WeakMap<Expr, Thunk>;
21
+ /**
22
+ * Names the body reads but does not bind, in the order they were first seen.
23
+ * Each becomes a cell the closure resolves once, so reading one costs an
24
+ * index rather than a walk up the environment chain.
25
+ *
26
+ * Absent at the root, where there is no closure to hold the cells and a
27
+ * shared list would grow across every program the process ever compiles.
28
+ */
29
+ readonly free?: string[];
30
+ /**
31
+ * This body binds one name and nothing else, so the value bound to it *is*
32
+ * the environment: there is no frame, and reading the parameter is reading
33
+ * the argument. Set optimistically and withdrawn by whoever compiled the body
34
+ * if it turns out to need an environment after all.
35
+ */
36
+ bare?: boolean;
37
+ /** The body asks for some name as text, so it needs a real environment. */
38
+ dynamic?: boolean;
39
+ /**
40
+ * The cell a free name resolves to, when this body belongs to exactly one
41
+ * closure and so may hold cells of its own. Absent for a `fn (…) => …`, whose
42
+ * compiled body is shared by every closure the expression produces.
43
+ */
44
+ cellOf?: (name: string) => Cell;
45
+ }
46
+
47
+ /** Outside any function: every name is looked up by asking the environment. */
48
+ export function rootScope(): LexScope {
49
+ return { names: [], cache: new WeakMap() };
50
+ }
51
+
52
+ /**
53
+ * The scope inside a function: its parameters, then its locals. A local that
54
+ * repeats a parameter's name takes that slot, the way an assignment would.
55
+ */
56
+ export function scopeOf(params: ParamList | undefined, body: FnBody): LexScope {
57
+ const names: string[] = (params?.params ?? []).map((param) => param.name);
58
+ for (const local of body.locals) {
59
+ if (!names.includes(local.name)) names.push(local.name);
60
+ }
61
+ // Worth trying without a frame: one name, and nothing bound after it.
62
+ const bare = names.length === 1 && body.locals.length === 0;
63
+ return { names, cache: new WeakMap(), free: [], bare };
64
+ }
65
+
66
+ /** Whether a body compiled optimistically can keep doing without a frame. */
67
+ export function stayedBare(scope: LexScope): boolean {
68
+ return Boolean(scope.bare) && !scope.dynamic && (scope.free?.length ?? 0) === 0;
69
+ }
70
+
71
+ /** Where a free name sits in this scope's cell list, adding it if new. */
72
+ export function freeSlot(scope: LexScope, name: string): number | undefined {
73
+ const free = scope.free;
74
+ if (!free) return undefined;
75
+ const at = free.indexOf(name);
76
+ if (at !== -1) return at;
77
+ return free.push(name) - 1;
78
+ }
@@ -0,0 +1,56 @@
1
+ import { applyBinary, negate } from "../../expr/operators.js";
2
+ import { isWaiting } from "../../expr/pending.js";
3
+ import type { Binary, Unary } from "../../generated/ast.js";
4
+ import { isNumeric, truthy } from "../../value/index.js";
5
+ import type { Compile, Thunk } from "../compile.types.js";
6
+ import { FAST_BINARY } from "./fast-binary.js";
7
+
8
+ /** A binary operation, with its operator resolved at compile time. */
9
+ export function compileBinary(expr: Binary, compile: Compile): Thunk {
10
+ const op = expr.operator;
11
+ if (op === "&&" || op === "||" || op === "??") return compileLogical(op, expr, compile);
12
+ const left = compile(expr.left);
13
+ const right = compile(expr.right);
14
+ // Two plain numbers take the short way, in a thunk written for this operator
15
+ // alone; anything else falls back to units.
16
+ const fast = FAST_BINARY[op];
17
+ if (fast) return fast(left, right);
18
+ return (env) => applyBinary(op, left(env), right(env));
19
+ }
20
+
21
+ function compileLogical(op: string, expr: Binary, compile: Compile): Thunk {
22
+ const left = compile(expr.left);
23
+ const right = compile(expr.right);
24
+ // The left side is evaluated once and kept: an action can sit in expression
25
+ // position, and running it twice would be a second request.
26
+ if (op === "&&")
27
+ return (env) => {
28
+ const a = left(env);
29
+ return truthy(a) ? right(env) : a;
30
+ };
31
+ if (op === "||")
32
+ return (env) => {
33
+ const a = left(env);
34
+ return truthy(a) ? a : right(env);
35
+ };
36
+ return (env) => left(env) ?? right(env);
37
+ }
38
+
39
+ /** `!x` and `-x`, taking the settled value straight through when it is one. */
40
+ export function compileUnary(expr: Unary, compile: Compile): Thunk {
41
+ const operand = compile(expr.operand);
42
+ if (expr.operator === "!") {
43
+ return (env) => {
44
+ const value = operand(env);
45
+ return isWaiting(value) ? value.then((v) => !truthy(v)) : !truthy(value);
46
+ };
47
+ }
48
+ return (env) => {
49
+ const value = operand(env);
50
+ return isWaiting(value) ? value.then(negated) : negated(value);
51
+ };
52
+ }
53
+
54
+ function negated(value: unknown): unknown {
55
+ return isNumeric(value) ? negate(value) : -Number(value);
56
+ }
@@ -0,0 +1,50 @@
1
+ import { invoke, invoke1 } from "../../expr/invoke.js";
2
+ import { isWaiting, whenAllReady, whenBothReady } from "../../expr/pending.js";
3
+ import type { Call } from "../../generated/ast.js";
4
+ import type { Compile, Thunk } from "../compile.types.js";
5
+
6
+ /**
7
+ * A call, with its argument list compiled.
8
+ *
9
+ * A call is the hottest thing a program does, so the common arities get their
10
+ * own thunk and build the argument array from a literal rather than with `map`.
11
+ */
12
+ export function compileCall(expr: Call, compile: Compile): Thunk {
13
+ const callee = compile(expr.callee);
14
+ const args = (expr.args?.args ?? []).map((arg) => compile(arg.value));
15
+ if (args.length === 0) return (env) => call(callee(env), []);
16
+ if (args.length === 1) return oneArg(callee, args[0] as Thunk);
17
+ if (args.length === 2) return twoArgs(callee, args[0] as Thunk, args[1] as Thunk);
18
+ return (env) =>
19
+ call(
20
+ callee(env),
21
+ args.map((arg) => arg(env)),
22
+ );
23
+ }
24
+
25
+ function oneArg(callee: Thunk, first: Thunk): Thunk {
26
+ return (env) => {
27
+ const fn = callee(env);
28
+ const arg = first(env);
29
+ // The overwhelmingly common shape: nothing is waiting, so the value goes
30
+ // straight in, with no list built to hold one thing.
31
+ if (!isWaiting(fn) && !isWaiting(arg)) return invoke1(fn, arg);
32
+ return call(fn, [arg]);
33
+ };
34
+ }
35
+
36
+ function twoArgs(callee: Thunk, first: Thunk, second: Thunk): Thunk {
37
+ return (env) => {
38
+ const fn = callee(env);
39
+ const a = first(env);
40
+ const b = second(env);
41
+ if (!isWaiting(fn) && !isWaiting(a) && !isWaiting(b)) return invoke(fn, [a, b]);
42
+ return call(fn, [a, b]);
43
+ };
44
+ }
45
+
46
+ /** Wait for whatever is not here yet: the function, or what it is given. */
47
+ function call(callee: unknown, values: unknown[]): unknown {
48
+ if (!isWaiting(callee)) return whenAllReady(values, (ready) => invoke(callee, ready));
49
+ return whenBothReady(callee, Promise.all(values), (fn, ready) => invoke(fn, ready as unknown[]));
50
+ }