@venn-lang/core
v0.6.0
Published
The Venn compiler: grammar, parser, type checker, expression compiler, decorator expansion, and the Problem model.
Downloads
1,387
Maintainers
Readme
@venn-lang/core
The Venn compiler: grammar, parser, type checker, expression compiler, decorator expansion, and the
Problemmodel.
Everything that turns .vn text into something another package can run, check or draw lives here.
The kernel is deliberately small: it knows flows, steps, expressions, types and decorators, and it
knows nothing about HTTP, browsers or files. Protocols arrive as plugins built on
@venn-lang/sdk; the core only ever sees the tree they decorate.
@venn-lang/core never imports node:*. It is built with platform: "neutral" so the same bundle runs
in Node, in the CLI, and inside the Web Worker that hosts the language server. Its runtime
dependencies are Langium (parsing) and @venn-lang/types (the published type vocabulary).
Usage
import { checkTypes, parse, toGraph } from "@venn-lang/core";
const uri = "file:///hello.vn";
const source = `flow "Hello" {
step "Ping" {
http.get "https://example.com/health"
expect res.status == 200
}
}`;
const { ast, problems } = parse(source, { uri });
const checked = checkTypes(ast, { uri });
for (const problem of [...problems, ...checked.problems]) {
console.log(`${problem.code} ${problem.title} (${problem.span.line}:${problem.span.column})`);
}
const graph = toGraph(ast); // flow -> step -> action/expect, for the node editorparse is synchronous and touches no filesystem. Error recovery keeps a partial AST, so a file with
a syntax error still produces a tree the editor can work with.
Module map
| Module | Responsibility |
| --- | --- |
| grammar/ | venn.langium, the whole grammar of the language. The single source the parser is generated from. |
| generated/ | Langium output: the AST interfaces, the isX type guards, the grammar object and the services module. Regenerated by pnpm --filter @venn-lang/core langium:generate. |
| lang/ | The Langium services parse uses, plus VennLexer, which makes newlines significant between statements and suppresses them inside ( ) and [ ]. |
| parse/ | parse and parseExpression. Turns text into a Document plus VN1xxx problems. |
| ast/ | Small readers over the generated tree: walkAst, dottedPath, callArgs, splitCall, isRunnable. |
| problem/ | The Problem shape shared by compile diagnostics and runtime failures, with Span, Diff, Severity, ProblemError and buildDiff. |
| codes/ | CODES, the catalog of every VNxxxx the kernel can raise, and buildProblem. |
| expand/ | The expansion phase. Runs every @decorator over the tree once, before anything else reads it. |
| typecheck/ | Hindley-Milner inference with generics, the Type algebra, the plugin TypeCatalog, cross-file importedTypes, and the prose the editor shows on hover. |
| compile/ | Compiles each Expr into a Thunk, a closure over the environment. Memoised per node, so the tree is walked once and not once per evaluation. |
| expr/ | The evaluator surface: evaluate, closures and frames, invoke, memberValue, the built-in method tables, and the prelude values. |
| value/ | What a runtime value is, and the three questions asked of one: truthy, strictEquals, isNumeric. |
| units/ | Unit-typed values (300ms, 2mb, 50%, ISO instants) and the arithmetic that keeps units honest. |
| interpolation/ | ${…} placeholders, described once for both the evaluator and the editor. |
| format/ | formatText, shared by venn fmt and the language server so both agree on the result. |
| graph/ | toGraph: a pure AST transform into nodes and edges for the visual editor. |
| events/ | The event envelope the runner emits and the UI consumes. Types only, no runtime. |
| module/ | Which of the three kinds an import specifier is: relative, alias or package. |
API
Parsing
| Export | What it gives you |
| --- | --- |
| parse(text, { uri? }) | ParseOutput: the Document AST and the VN1xxx problems. |
| parseExpression(source) | An Expr for a standalone expression, or undefined when it is not one. |
| EXPRESSION_OFFSET | How far parseExpression shifts CST offsets, so a caller can map back onto the ${…} it came from. |
| vennServices() / createVennServices() | The Langium services, cached and fresh respectively. |
| VennLexer | The lexer that suppresses newlines inside brackets. |
| VennGeneratedModule, VennGeneratedSharedModule, VennLanguageMetaData | The generated Langium modules, for a host that builds its own services. |
Everything from generated/ast.ts is re-exported: Document, FlowDecl, StepDecl, ActionCall,
ExpectStmt, FnDecl, DecoDecl, Expr and the rest, each with its isX guard.
The AST readers exist because the grammar spells some things two ways:
| Export | What it answers |
| --- | --- |
| callArgs(call) | The arguments of a call as one list, whether it was written http.get "url" or conn.close(). |
| splitCall(args, takes) | Which trailing { … } is an options map and which is a real argument. |
| dottedPath(expr) | The dotted name an expression spells (http.get), or undefined when it is not a plain chain. |
| isRunnable(decl) | Whether a top-level node runs or merely defines. |
| walkAst(root) | Every descendant, depth first. |
Problems and codes
Every failure, at compile time or at run time, is a Problem: a stable code, a severity, a
one-line title in the user's vocabulary, a span, and optional help, related, diff, note
and docs. One shape means one renderer serves the terminal, the editor and the UI.
import { buildProblem, CODES, ProblemError } from "@venn-lang/core";
throw new ProblemError(
buildProblem({
spec: CODES.VN6001_ASSERTION_FAILED,
span: { uri, offset: 0, length: 0, line: 1, column: 1 },
title: "The response did not arrive before the deadline",
}),
);| Family | Covers |
| --- | --- |
| VN1xxx | Lexical and syntactic. |
| VN2xxx | Name resolution, imports, decorators, capability negotiation. |
| VN3xxx | Types and units. |
| VN4xxx | Concurrency and isolation. |
| VN5xxx | Lint. |
| VN6xxx | Assertions, at run time. |
| VN7xxx | Actions and protocols, at run time. |
| VN8xxx | Timeouts and resource limits. |
CODES holds the codes the kernel itself raises, from VN1001_LEX to VN8002_LOOP_LIMIT. Plugins
add their own in the same families. buildDiff turns two compared values into a structured Diff,
walking them field by field when they line up, so a failure names the field that moved instead of
printing two renderings side by side. formatValue renders a single value for a report.
Type checking
import { checkTypes, showType } from "@venn-lang/core";
const { problems, types, slots } = checkTypes(ast, { uri, catalog, decos, imports });checkTypes runs Hindley-Milner inference over the whole document. Functions are generalised, so one
fn id(x) => x serves every type; dynamic unifies with everything and never errors, so a plugin
result or a parsed HTTP response places no annotation burden on the pure parts of a file. It returns
the problems, every expression's inferred type keyed by node (for hover), and the expressions parsed
out of each string literal's ${…} slots.
CheckTypesOptions is how the checker learns about the world outside the file: catalog (what the
loaded plugins publish), decos (the pub decos the imports reach) and imports (what each
imported name turned out to be).
| Export | Purpose |
| --- | --- |
| Type, FnType, RecordType, UnionType, OpaqueType, LiteralType | The type algebra. |
| DYNAMIC, literal, opaque, union | Constructors for the ones a caller builds by hand. |
| prune | Follow a type variable to what it has been solved to. |
| showType, showTypes | Render types for a hover or a diagnostic. showTypes names variables across a group, so two unrelated parameters are not both called a. |
| TypeCatalog | The two questions the core asks about plugins: typeOf(name) and signatureOf(target). |
| specToType, ResolveRef | Read a published TypeSpec from @venn-lang/types into the checker's own type. |
| importedTypes, ImportedTypes | The types of the names a document imports, worked out from the files it names. Cycles answer with nothing rather than looping. |
| memberType, resolveMember | The type of xs.map, s.length, p.name. |
| createContext | Fresh per-check inference state, so two checks never share variable ids. |
| BUILTIN_TYPES, isBuiltinType | string, number, bool, duration, size, percent, instant and friends, each with a line of documentation and an example. |
| PRELUDE_SPECS, isPrelude, PreludeSpec, PreludeArg | The prelude, described once: types for the checker, prose for the editor. |
| MEMBER_DOCS, MemberDoc, memberKind | What each built-in member does, for completion and hover. |
| KIND_SPECS, KIND_TYPES | What a decorator target handle offers, published as types. |
Compiling and evaluating expressions
An expression is compiled once into a Thunk, a function of the environment. Everything the source
settles (which operator, which literal, which slot a name lives in) is decided at compile time.
import { evaluate, type EvalEnv } from "@venn-lang/core";
const env: EvalEnv = { lookup: (name) => (name === "res" ? { status: 200 } : undefined) };
evaluate(subject, env); // true, for `res.status == 200`| Export | Purpose |
| --- | --- |
| compileExpr(expr) | The compiled Thunk. Memoised per node. |
| closureOfDecl(decl, env) | A top-level fn as a callable value bound into an environment. |
| evaluate(expr, env) | Compile if needed, then run. The seam every caller uses. |
| EvalEnv | One method, lookup(name). Actions and matchers are not here: they belong to the runtime registry. |
| childEnv(parent, bindings) | A nested scope. |
| Cell, CellEnv, hasCells | Bindings addressed by cell, which is what lets a recursive fn capture itself. |
| invoke, invoke1, callClosure, isCallable | Calling a Venn callable, with fixed arities for the hot paths. |
| Closure, isClosure, NativeFn, nativeFn, isNativeFn | The two kinds of callable. |
| memberValue(receiver, member) | What .x means on any value: a map's own data first, then the built-in member tables. |
| namespaceValue, isNamespaceValue | A plugin namespace as a value, so fmt.table(rows) works inside any expression. |
| PRELUDE_VALUES | range, str, typeOf, pretty, spawn. |
| display(value), typeName(value) | How print renders a value, and what the language calls its type. |
Value (from value/) is the shape a runtime value can take, with truthy, strictEquals and
isNumeric alongside it. There is no coercion: "99.00" never equals 99.
Decorator expansion
expand is the phase between parsing and everything else. It is the only place a @name means
anything. The kernel does not know what @retry is; it knows that a name written with an @ is
looked up in a DecoratorSource, handed the node it sits on, and allowed to rewrite it. Decorators
written in TypeScript by a plugin and decorators written in Venn with deco go through the same
door.
import { expand } from "@venn-lang/core";
const { problems } = expand({ document: ast, decorators, uri, imported });Applied innermost first, so a decorator that rewrites a body finds a body its own decorators have
already finished with. An ExpandContext gives a decorator the node, its evaluated args, the same
arguments as syntax in written, the parent, and four verbs: replace, remove, meta and
reject.
| Export | Purpose |
| --- | --- |
| expand, ExpandResult, ExpandContext, DecoratorDefinition, DecoratorSource, DecoratedNode, NodeMeta | The mechanism and its contracts. |
| TargetKind, TARGET_KINDS, isTargetKind, kindOf | The words a deco uses about what it decorates: Fn, Flow, Step, Binding, Type, Node. kindOf is the one place that vocabulary meets the compiler's $type names. |
| makeHandle, handleSurface, TargetHandle, HandleSurface | The value a deco body holds for its target, and what it publishes. |
| readSignature, DecoSignature, SignatureResult, acceptedKinds, decoTarget | Reading what a deco decorates off the type of its first parameter. |
| decoDecorator, withDocumentDecos, ImportedDeco, DocumentDecoArgs, decoCannotCall, DecoBodyArgs | Turning a deco declaration, local or imported, into a definition expansion can run. |
| metaOf, readMeta, writeMeta | Facts a decorator leaves on a node for the runtime. Non-enumerable, so they never land in a serialised AST. |
| readDecorations, addDecoration, AROUND_KEYS, Decorations, decorateCallable | Where .wrap, .before and .after leave their closures, and how a callable picks them up. |
| swapNode, spanOf | Replacing a node in its container, and locating one. |
| wrongKind, wrongKindTitle, wrongTargetTitle, kindWords, nodeWord, everyKindWritten | The prose behind VN2014, phrased in the author's words rather than in node type names. |
Units
A number literal may carry a unit, and the unit survives arithmetic.
expect res.duration < 300ms
expect res.size <= 2mb
const rate = 99.9%parseNumber turns a NUMBER lexeme into a plain number or a UnitValue; parseInstant turns an
ISO-8601 lexeme into an Instant that keeps its source text. combine({ op, left, right }) does the
arithmetic and comparison: 300ms + 1s succeeds, 300ms + 2mb returns the mismatch behind VN3012.
isUnitValue and isInstant are the guards. Canonical bases are milliseconds, bytes, a ratio from 0
to 1, and epoch milliseconds.
Interpolation
scanInterpolations(text) locates every ${…} precisely enough to highlight, hover and jump to.
compileTemplate(text) splits a literal once into its constant chunks and its holes, each hole
carrying both its source and its parsed Expr. There is always exactly one more chunk than holes, so
rejoining them cannot drop text. Both are cached by text, because a .vn file holds a fixed set of
string literals.
Formatting
import { DEFAULT_FORMAT, formatOptionsFrom, formatText } from "@venn-lang/core";
formatText(source, { indentWidth: 4 });
formatText(source, formatOptionsFrom(manifest.format)); // the [format] table of venn.tomlformatText re-indents by bracket depth and, by default, moves every use above every import. It
never joins or splits lines, and it is idempotent. organizeHeader and reindent are the two steps
on their own.
Graph and events
toGraph(document) derives the node graph from the AST: a flow contains its steps, a step contains
its actions and expectations, and sequential edges run between siblings. Pure, with no runtime state.
The events/ module is types only: Envelope is the single contract between runner, host and UI,
carrying a monotonic seq, a timestamp, a RunId, a kind and its payload. EventKind is derived
from the keys of EventData, so adding an event is one edit. RunPlan is what the UI draws in grey
before anything executes.
Modules
specifierKind(spec) classifies an import specifier the way Node does: ./util.vn is relative,
#shared/auth.vn is an alias resolved through [paths], and a bare name is an installed package.
isPackageSpecifier is the common question asked of it.
Grammar
The grammar lives in src/grammar/venn.langium and is the fixed part of
the language. It knows flow, step, group, fragment, fn, deco, type, config, matrix,
control flow, lifecycle hooks, expect, and one rule that absorbs every protocol present and future:
ActionCall:
target=QualifiedName (called?='(' (call=ArgList)? ')')?
(args+=ActionArg)* (opts=MapLit)?;That is why http.get "url" { headers } needs no grammar change to exist. Whether a dotted target is
a plugin verb or a method on something in scope is decided by what the name resolves to, not by how
the call is written.
Statements are terminated by a newline or ;. The lexer drops those inside ( ) and [ ], so an
argument list or a list literal may still span lines, while blocks and maps keep their newlines.
Regenerate the parser after editing the grammar:
pnpm --filter @venn-lang/core langium:generate
pnpm --filter @venn-lang/core testSee also
@venn-lang/runtimewalks the checked document and executes it.@venn-lang/typesis the type vocabulary as plain data, shared with plugins.@venn-lang/lspreuses the parser, checker and formatter to serve editors.docs/venn-language.mdis the language specification;docs/type-system.mdcovers the types.
