Documentation
¶
Overview ¶
Package lang — file-scope directives for runner definitions.
@lang.runner and @lang.define are compiler.Directive extensions, like @include and @assert. They run at preprocess time (line-based), before the parser sees the expression grammar.
@lang.runner is the file-scope form of what runners/*.nflow ships as data: it lets a flow declare a language inline. @lang.define is the runtime-only form (no install recipe) — the data model is shared.
Package lang ships inline polyglot execution through the `lang` bundle, using directives and bare keyword aliases such as py, rs, go, and ts.
Each tag compiles to a single LangBlock AST node. The runtime materializes the source to a temp file, resolves the interpreter, and delegates execution to extensions/external via the executor seam (see exec.go). No new subprocess engine lives in this package.
Typical use:
@require lang
{ "x": 2, "y": 3 } -> py"import json, sys; v=json.load(sys.stdin); print(json.dumps({'value': v['x'] + v['y']}))"
py {
import sys, json
data = json.load(sys.stdin)
print(json.dumps({"len": len(data)}))
}
The bundle is registered for @require but is not mounted in the default native bundle set. Add @require lang to a flow that wants the keywords.
Package lang — parser integration.
Index ¶
- Constants
- Variables
- func Bundle(_ map[string]string) compiler.Bundle
- func Install(id, version string) (cmd string, args []string, err error)
- func LoadRunners() error
- func Register(l Language)
- func RegisterDefine(d Define) error
- func Resolve(lang *Language, version string) (cmd string, args []string, err error)
- func WithAutoInstallDisabled(ctx context.Context) context.Context
- type Alias
- type Check
- type Define
- type Ensure
- type ExecError
- type LangBlock
- type Language
- type NotInstalledError
- type OSRecipe
- type Preflight
- type Report
- type RunnerSpec
- type UnknownLanguageError
- type UnknownTagError
Constants ¶
const ID = "lang"
ID is the bundle identifier. Short, stable, never an import path. Two extensions with the same ID panic at compiler.Register.
Variables ¶
var DefineDirective = compiler.Directive{
Name: "lang.define",
Example: `@lang.define "gleam" { "id": "gleam", "ext": ".gleam", "find": ["gleam", "run"] }`,
Handler: handleDefineDirective,
}
DefineDirective is the @lang.define directive. Same data model, but intended for runtime-only languages (no install recipe). The body format is identical to @lang.runner.
var ErrNotFound = errors.New("interpreter not found")
ErrNotFound is the sentinel from lookPathCmd. Tests assert against this rather than a string match.
var Registry = map[string]*Language{}
Registry is the process-global table of known languages. Populated at init() by LoadRunners (from embedded runners/*.nflow) and at parse time by RegisterDefine (define.go) when @lang.define is used.
Keyed by (ID, Version) — versioned variants of the same language (python "", python "2", python "3") are distinct entries. The unversioned Lookup(id, "") returns the default variant.
Two languages with the same (ID, Version) panic — silent override would let a stale registration win and produce confusing runtime behavior.
var RunnerDirective = compiler.Directive{
Name: "lang.runner",
Example: `@lang.runner "python" { "id": "python", "ext": ".py", "find": ["python3"] }`,
Handler: handleRunnerDirective,
}
RunnerDirective is the @lang.runner directive. It parses the runner spec from the directive body and registers it via registerSpec (the same path LoadRunners uses for embedded runner files).
Syntax (file-scope, one per line until the closing }):
@lang.runner "python" {
"id": "python",
"ext": ".py",
"aliases": ["py", "python"],
"find": ["python3", "python"],
"stdin": "json",
"stdout": "json",
"install": {
"windows": ["winget", "install", "Python.Python.3.12"],
"darwin": ["brew", "install", "python@3.12"]
}
}
The body is strict JSON, as in embedded runners/*.nflow files. Use an optional `_comment` field for notes. The same ParseRunner handles both.
Functions ¶
func Bundle ¶
Bundle wires the language tag keywords, the @lang.runner and @lang.define directives, and the embedded runner source modules.
It returns the contract documented in extensions/README.md: ID, Libraries, Directives, Primaries, Fixtures, SourceModules.
- Primaries: contributed by OnPreprocess from the registered aliases. Adding an alias to a runner file makes it available without a Go edit.
- Directives: @lang.runner and @lang.define for inline declarations. See lang_directive.go.
- SourceModules: embedded runners/*.nflow, mirroring the nexssp/ai pattern. Consumers can @include runner files as data.
Feature logic lives in sibling files; this file does only wiring.
func Install ¶
Install is the package-level entry point used by `nflow lang install <id>`. Wraps installEnsure so callers don't construct the type.
func LoadRunners ¶
func LoadRunners() error
LoadRunners parses every runners/*.nflow file from the embed and registers each as a Language. Called once at init(); idempotent across reloads (duplicate registration panics — that's the project rule for builtins).
One bad runner file does NOT kill the binary: registerSpec's panic is recovered and returned as an error, so the caller sees "file X failed because Y" rather than a process crash. This is the resilience rule — a malformed data file is user input, not a reason to panic the host.
func Register ¶
func Register(l Language)
Register adds a language to the Registry. Duplicate (ID, Version) panic, matching the project rule: "collisions panic; missing contracts fail loudly."
func RegisterDefine ¶
RegisterDefine adds a user-defined language to the Registry.
Called by the @lang.define directive. Go hosts may also register a language directly before compiling flows.
Returns an error rather than panicking: user-defined languages fail gracefully, while builtins (which the developer shipped intentionally) panic on duplicate registration via Register.
func Resolve ¶
Resolve is the single entry point for "give me a runnable command."
Tries the language's Select (find-only). On miss, calls ensureHook, which installs and re-resolves by default or returns a NotInstalledError if the runner has no recipe or installation fails. Context-scoped internal callers may disable installation without changing this production default.
Preflight, runLangBlock, and the CLI all go through here. There is no other call site for lang.Select — Resolve is the gate.
func WithAutoInstallDisabled ¶ added in v0.48.0
WithAutoInstallDisabled marks a context so language preflight and runtime resolution report missing interpreters without invoking package managers. Self-test uses this to avoid changing the host environment.
Types ¶
type Alias ¶
type Alias struct {
Tag string // "py2", "python3", "golang"
ID string // canonical language ID, e.g. "python"
Version string // "", "2", "3"
}
Alias maps a tag (e.g. "py") to a (Language.ID, Version) pair. The runtime never sees aliases; resolveAlias is the only exit point.
Aliases are derived from runner files at init() via registerAlias — the table is built, not hand-maintained. The TestAliasInvariants test asserts the derived table matches what the runner files declare.
type Check ¶
type Check struct {
ID string
Version string
Tag string // first alias that resolved to this (ID, Version), for hints
Cmd string // resolved command, "" if missing
Err error // non-nil if missing
}
Check is one language examined during preflight.
type Define ¶
type Define struct {
ID string // required, e.g. "gleam"
Version string // optional, e.g. "3"
Ext string // required, including dot, e.g. ".gleam"
Aliases []string // optional, e.g. ["gleam", "beam"]
Find []string // required, e.g. ["gleam", "run"]
Stdin string // "json" (default) | "text" | "raw"
Stdout string // "json" (default) | "text" | "raw"
FileTemplate string // optional JSON-in/JSON-out source for .file :autocreate
Install OSRecipe // optional, per-OS install commands
}
Define is the in-flow @lang.define directive's data model.
It lets users register a language the bundle doesn't ship built-in, without forking the repo or writing a runner file. Example DSL:
@lang.define "gleam" {
"id": "gleam",
"ext": ".gleam",
"aliases": ["gleam"],
"find": ["gleam", "run"],
"stdin": "json",
"stdout": "text"
}
{ "value": "x" } -> gleam"..."
This is the runtime equivalent of a runners/*.nflow file — same fields, same RunnerSpec underneath. Definitions update the process-wide registry and remain registered for the process lifetime; each (ID, Version) and alias must be declared only once.
type Ensure ¶
type Ensure interface {
Ensure(lang *Language, version string) (cmd string, args []string, err error)
}
Ensure is the auto-install seam.
Resolve calls Ensure when a language's Select (find-only) fails. The default installEnsure runs the runner's per-OS recipe and re-resolves the interpreter; tests or hosts can replace the hook when needed.
Pure Go: no nexssp/ensure, no Node.js. Recipes call winget/brew/apt/npm directly. See install.go.
type ExecError ¶
ExecError is the type returned when the interpreter subprocess fails. Stderr is a string (not []byte) — matches external.ExecResult.Stderr and avoids the []byte(string) copy.
type LangBlock ¶
type LangBlock struct {
ID string // "python" — canonical, never the tag
Version string // "", "2", "3"
Source string // source code text
Tag string // source keyword, including ".file" when using a script file
SourceFile string // absolute path for a file-backed script; empty for inline source
// Pos is the source position supplied by the compiler parser.
Pos any
}
LangBlock is the AST node produced by language keywords such as py, rs, and go.
All aliases (py, python, python3) resolve to a LangBlock with the same ID and a Version reflecting the alias. The runtime never sees the tag — by the time we reach materialize/exec, the canonical ID is the only thing in scope.
func ParseFileTag ¶
ParseFileTag creates a language block that runs an existing source file. The caller resolves the path relative to the containing .nflow file and supplies the file contents so preflight can inspect dependency metadata.
func ParseTag ¶
ParseTag converts a keyword like "py" or "python3" into a LangBlock ready for the runtime. The parser hook reads the keyword and source literal/block, then calls this function.
ParseTag fails fast on one condition, surfaced to the user as a compile-time error rather than a runtime surprise:
- Unknown keyword (not in the alias table).
Interpreter presence is NOT checked here — that's preflight's job. ParseTag produces the AST node; the compiler calls Preflight.Run on the collected blocks before any runtime invocation. This separation means a flow with py and rs where rust is missing reports both checks in one pass, not whichever keyword happened to parse first.
The caller is expected to wrap the returned error in compiler.SourceError(pos, ...) before it leaves the parser. We don't do that here because compiler.SourceError's signature varies across versions and we don't want this file coupled to it.
type Language ¶
type Language struct {
// ID is the canonical language identifier, e.g. "python".
ID string
// Ext is the file extension used when materializing source to disk,
// including the leading dot, e.g. ".py".
Ext string
// Select resolves a version string to a runnable command. Select is
// FIND-ONLY: it never installs. Resolve() is the gate that calls
// Ensure on miss. Preflight uses Select (not Resolve) to detect missing
// interpreters without triggering install.
Select func(version string) (cmd string, args []string, err error)
// Encode serializes the incoming stream value to bytes for stdin.
// JSON is the default; shell/raw languages use text.
Encode func(v any) ([]byte, error)
// Decode parses stdout back into a typed value the next atom sees.
// Takes string (not []byte) to avoid the []byte(string) copy from
// external.ExecResult.Stdout. JSON is the default; shell/raw return string.
Decode func(s string) (any, error)
// contains filtered or unexported fields
}
Language is the runtime view of a runner spec. One per (language, version). Tags are gone by here — runner file aliases are applied at parse time, and from this struct onward every code path speaks the canonical ID ("python"), never the tag ("py").
One struct, every language. New languages add a runners/<id>.nflow file, zero Go changes. See runners.go.
type NotInstalledError ¶
type NotInstalledError struct {
ID, Version string
Cause error // optional: why an auto-install attempt failed
}
NotInstalledError is the unified "interpreter missing" error. Preflight groups these into a single Report; the CLI prints the report and aborts.
func (*NotInstalledError) Error ¶
func (e *NotInstalledError) Error() string
func (*NotInstalledError) Unwrap ¶
func (e *NotInstalledError) Unwrap() error
type OSRecipe ¶
type OSRecipe struct {
Windows []string `json:"windows,omitempty"`
Darwin []string `json:"darwin,omitempty"`
Linux []string `json:"linux,omitempty"`
}
OSRecipe holds per-OS install commands. Each entry is a full command line; the first element is the binary, the rest are args.
type Preflight ¶
type Preflight struct {
// Installer, if non-nil, is invoked for each Missing entry before
// the Report is returned. Use this to enable auto-install: preflight
// still reports what was missing, but installs first. Default nil
// (no auto-install — the safe default).
Installer Ensure
}
Preflight is the pre-run gate. Before any LangBlock executes, the compiler calls Preflight with every LangBlock in the flow. Preflight resolves each (find-only — no install), groups missing interpreters, and returns a single Report. If the Report has any Missing entries, the flow does not run.
Why find-only here: preflight is a CHECK, not an install trigger. Hosts may explicitly call lang.Install or provide an Ensure implementation. Preflight surfaces what's missing; it doesn't fix it by default.
Why a single Report: a flow with py, rs, go, and ts that's missing rust, typescript, AND python3 should print one consolidated list, not fail on the first one and force the user to iterate.
func (Preflight) Run ¶
Run executes the preflight pass over the given blocks. Dedupes by (ID, Version) so a flow that calls py ten times only checks once.
blocks is the list of LangBlock AST nodes extracted from the flow by the compiler's AST validator, which calls Preflight.Run before any runtime invocation.
type Report ¶
type Report struct {
// Checked is every (ID, Version) pair examined, deduped. Includes
// both present and missing.
Checked []Check
// Missing is the subset of Checked where the interpreter was not
// found and no successful install happened. Length 0 = ready.
Missing []Check
}
Report is the result of a preflight pass. Zero Missing entries means "ready to run." One or more means "abort and show this to the user."
func (Report) Error ¶
Error renders the report as a multi-line message suitable for compiler.SourceError or CLI stderr. Satisfies the error interface so callers can `return preflight.Report{...}` directly.
func (Report) SelfTestSkipReason ¶
SelfTestSkipReason lets the repository self-test runner skip a language fixture when an interpreter is absent, while still failing on an unknown language or a broken preflight contract.
type RunnerSpec ¶
type RunnerSpec struct {
ID string `json:"id"`
Version string `json:"version,omitempty"`
Ext string `json:"ext"`
Aliases []string `json:"aliases,omitempty"`
Find []string `json:"find"`
Stdin string `json:"stdin,omitempty"`
Stdout string `json:"stdout,omitempty"`
FileTemplate string `json:"file_template,omitempty"`
Install OSRecipe `json:"install,omitzero"`
Deps string `json:"deps,omitempty"`
DepsMarker string `json:"deps_marker,omitempty"`
}
RunnerSpec is the data model parsed from a @lang.runner block in a .nflow file. One RunnerSpec per (language, version). The Go side is generic — adding a language means shipping a new runners/<id>.nflow file, zero Go changes.
File format (one block per file, multiple files for versioned variants):
@lang.runner "python" {
ext: ".py"
aliases: ["py", "python"]
find: ["python3", "python"]
stdin: "json"
stdout: "json"
file_template: "..." // optional source scaffold for :autocreate
install: {
windows: ["winget", "install", "Python.Python.3.12"],
darwin: ["brew", "install", "python@3.12"]
}
}
@lang.runner "python" "3" {
"id": "python",
"version": "3",
"ext": ".py",
"aliases": ["py3", "python3"],
"find": ["python3", "py -3"],
"stdin": "json",
"stdout": "json"
}
Fields:
- ID, Version: the canonical identity. Version "" means "default".
- Ext: file extension for materialized source, including the dot.
- Aliases: short keywords (py, python) that resolve to this spec.
- Find: candidate command lines tried in order. Each entry is a space-split command + args; the first that resolves on PATH wins.
- Stdin, Stdout: "json" (default) or "text".
- FileTemplate: optional JSON-in/JSON-out starter source used by `<alias>.file "path" :autocreate` when the target does not exist.
- Install: optional per-OS recipes for auto-install. Omit or leave the OS entry empty to skip auto-install for that platform.
- Deps, DepsMarker: optional command template and source marker for per-script dependency tools such as uv; {file} is passed as one arg.
func ParseRunner ¶
func ParseRunner(src string) (*RunnerSpec, error)
ParseRunner extracts the JSON object from a runner .nflow file and parses it into a RunnerSpec.
The file format is: an optional `@lang.runner "id" ["version"]` header line, followed by a strict JSON object. The JSON object carries all fields including "id" and "version". The header is for human/IDE readability only — it's ignored by the parser, which trusts the JSON.
This uses encoding/json directly (battle-tested by every Go program in existence). No hand-rolled brace counter, no comment stripper, no trailing-comma fixer. If a runner file needs documentation, it uses a "_comment" field inside the JSON — same convention as package.json.
The parser finds the first '{' in the source and decodes from there via json.NewDecoder, which handles all JSON syntax correctly including nested objects and strings containing braces.
func (RunnerSpec) Decode ¶
func (s RunnerSpec) Decode() (func(string) (any, error), error)
Decode resolves the stdout codec name to a function.
func (RunnerSpec) Encode ¶
func (s RunnerSpec) Encode() (func(any) ([]byte, error), error)
Encode resolves the stdin codec name to a function.
func (RunnerSpec) FindCmd ¶
func (s RunnerSpec) FindCmd() (cmd string, args []string, err error)
FindCmd tries each entry in Find via lookPathCmd. The first candidate that resolves to a usable interpreter wins; on Windows, py.exe is probed with its selected version so an empty launcher is treated as missing.
Each Find entry is split on whitespace; the first token is the binary, the rest are pre-args (e.g. "py -3" → cmd="py", args=["-3"]). The materialized source path is appended later by the runtime.
type UnknownLanguageError ¶
type UnknownLanguageError struct {
ID, Version string
}
UnknownLanguageError is returned by Lookup and ParseTag when the alias resolves to an (ID, Version) pair the Registry has no entry for. It surfaces as a compile-time SourceError via the parser hook.
func (*UnknownLanguageError) Error ¶
func (e *UnknownLanguageError) Error() string
type UnknownTagError ¶
type UnknownTagError struct{ Tag string }
UnknownTagError is returned by ParseTag when the keyword is not in the alias table. The user sees this wrapped in a SourceError at the keyword.
func (*UnknownTagError) Error ¶
func (e *UnknownTagError) Error() string