plano is an embeddable, schema-driven DSL runtime written in Go.
Current baseline:
- release:
v0.8.0 - public API generation:
v1 - artifact schema:
plano.artifact/v2
This repository currently contains a first usable implementation with:
- hand-written lexer and parser
- AST and diagnostics with compiler-provided quick-fix suggestions
- a Go workspace (
go.work) with separate core, CLI, and example modules - schema registration for forms and functions
- a Cobra-based CLI under
cmd/planofor core parsing, analysis, and embedded sample display - a public bind API for declaration and symbol collection
- a public check API for static type analysis
- a public HIR phase for typed compiler-internal lowering input
- a compiler that produces a typed document
- script-body execution with lexical scope, filtered loops, conditional expressions, membership expressions, and user-defined functions
- script control flow with
else if,break, andcontinue - expr-lang backed expression evaluation with host-registered variables and functions
- bounded parse and expr-lang program caches for repeated compile requests
- bundled example host DSLs under
examples/ - validated action registry for call statements
- glob imports via
** - a
Taskfile.ymlfor common local commands - unit tests for parsing, compilation, imports, script execution, and lowering
cmd/plano: CLI for parsing, checking, compiling, and displaying embedded.planosample filesfrontend/plano:ParseFileAPI for.planosource to ASTcompiler: structured compile API from source bytes, strings, or files to typed documentslsp: workspace analysis plus a basicgo.lsp.dev/protocolLSP server with hover, definition, diagnostics, folding ranges, code actions, and expr-lang host binding hintsschema: form specs, field specs, types, refs, and builtin scalar typesast: parser output nodesdiag: diagnostics model
Examples:
examples/builddsl: build graph loweringexamples/pipelinedsl: CI pipeline loweringexamples/servicedsl: service topology lowering
The example modules are documentation and host-integration examples. The CLI intentionally does not import them; it embeds a few sample .plano files for quick inspection.
The implementation also uses:
collectionxfor ordered compiler outputs, object values, and host-side IR structuresexpr-lang/exprfor opt-in dynamic expression evaluation throughexpr(...)mofor optional values in lowered IRlofor concise lowering transformsoopsfor internal error wrapping on loader and lowering boundaries
package main
import (
"context"
"github.com/arcgolabs/plano/compiler"
"github.com/arcgolabs/plano/schema"
)
func main() {
c := compiler.New(compiler.Options{})
_ = c.RegisterForm(schema.FormSpec{
Name: "workspace",
LabelKind: schema.LabelNone,
BodyMode: schema.BodyFieldOnly,
Fields: schema.Fields(
schema.FieldSpec{
Name: "name",
Type: schema.TypeString,
Required: true,
},
),
})
_, _ = c.CompileSource(context.Background(), "build.plano", []byte(`
workspace {
name = "demo"
}
`))
}You can also inspect the declaration-binding phase directly:
binding, diags := c.BindSource(context.Background(), "build.plano", src)
_ = binding
_ = diagsThe compiler also exposes string helpers when you already have in-memory source text:
result := c.CompileStringDetailed(ctx, "build.plano", src)
_ = result.DocumentHosts can expose additional variables and functions to expr-lang expressions:
_ = c.RegisterExprVar("branch", "main")
_ = c.RegisterExprFunc("slug", func(params ...any) (any, error) {
return strings.ReplaceAll(params[0].(string), "/", "-"), nil
}, func(string) string { return "" })Those values are available from expr(...) and expr_eval(...) in .plano scripts, together with current script locals and top-level constants.
Repeated expr-lang program compilation is cached by default. Set compiler.Options.ExprCacheEntries to a positive entry count to tune the bounded cache, or -1 to disable it.
And the static typecheck phase:
checks, diags := c.CheckSource(context.Background(), "build.plano", src)
_ = checks
_ = diagsAnd the typed HIR phase:
result := c.CompileSourceDetailed(ctx, "build.plano", src)
_ = result.HIRFor editor integrations, the lsp module can either analyze in-memory documents directly or expose a basic LSP server:
server := lsp.NewServer(lsp.ServerOptions{
Compiler: configuredCompiler,
})
_ = lsp.ServeStdio(context.Background(), lsp.ServerOptions{
Compiler: configuredCompiler,
})The LSP module also exposes folding ranges through Snapshot.FoldingRanges(...) and compiler-suggested quick fixes through Snapshot.CodeActions(...), with matching protocol handlers.
- Language draft: plano_language_definition.md
- Changelog: CHANGELOG.md
- Implementation status: docs/implementation_status.md
- Public API policy: docs/public_api.md
- Artifact schema: docs/artifact_schema.md
Build and run:
go run ./cmd/plano examples
go run ./cmd/plano examples build
go run ./cmd/plano version
go run ./cmd/plano parse ./cmd/plano/samples/basic.plano
go run ./cmd/plano bind ./cmd/plano/samples/basic.plano
go run ./cmd/plano check ./cmd/plano/samples/basic.plano
go run ./cmd/plano hir ./cmd/plano/samples/basic.plano
go run ./cmd/plano compile ./cmd/plano/samples/basic.plano
go run ./cmd/plano validate ./cmd/plano/samples/basic.plano
go run ./cmd/plano diag ./cmd/plano/samples/basic.plano
go run ./cmd/plano compile --format yaml --out ./document.yaml ./cmd/plano/samples/basic.planoparse prints AST JSON.
bind prints the declaration and symbol binding result JSON.
check prints the binding plus static typecheck result JSON.
hir prints the typed HIR JSON.
compile prints the typed document JSON.
validate checks whether the file compiles successfully.
diag prints diagnostics without failing the command on warnings.
examples lists embedded sample files, or prints one sample when passed a name.
version prints the release version, public API generation, and artifact schema generation.
Output controls:
--format json|yamlforparse,bind,check,hir, andcompile--format text|json|yamlforvalidateanddiag--out <path>to write command output to a file instead of stdout--stricton compiler-backed commands to fail on any diagnostics, not only errors
CLI releases:
- pushing a tag like
cmd/plano/v0.8.1runs thecli-releaseGitHub Actions workflow - the workflow builds cross-platform
planoarchives with GoReleaser and publishes them to a GitHub Release for that tag - workspace-local modules are resolved through
go.work; childgo.modfiles should not declare localgithub.com/arcgolabs/planodependencies
The repository also ships a small Taskfile.yml for common local workflows:
task fmt
task test
task lint
task bench
task bench:compiler
task bench:lsp
task work:sync
task examples
task sample SAMPLE=build
task parse FILE=./cmd/plano/samples/basic.plano FORMAT=yaml
task bind FILE=./cmd/plano/samples/basic.plano FORMAT=yaml
task check FILE=./cmd/plano/samples/basic.plano FORMAT=yaml
task hir FILE=./cmd/plano/samples/basic.plano FORMAT=yaml
task compile FILE=./cmd/plano/samples/basic.plano FORMAT=yaml OUT=./document.yamlThe repository now runs as a small Go workspace rather than a single module.
- Root module
github.com/arcgolabs/plano: compiler core, AST, diagnostics, schema, and frontend packages cmd/plano: standalone CLI modulelsp: standalone LSP helper moduleexamples/builddsl: example build DSL moduleexamples/pipelinedsl: example pipeline DSL moduleexamples/servicedsl: example service DSL module
That split gives us cleaner boundaries:
- core can evolve without dragging CLI/example dependencies into every consumer build
- example DSLs are now visibly host-side modules instead of looking like core packages
- future modules such as
lspor plugin/runtime adapters can be added without reshaping the core again
Workspace note:
- sibling workspace modules are resolved through
go.work - child
go.modfiles only declare external dependencies, not other local workspace modules
The implementation is still narrower than the full language draft, but the main compiler path is now usable:
- imports
- glob imports such as
import "tasks/**/*.plano" - top-level
const - top-level user-defined
fn - builtins such as
len,keys,values,range,get,slice,has,append,concat, andmerge - expr-lang backed
expr(...)andexpr_eval(...)calls with host-registered variables and functions - conditional expressions with
condition ? then : else - membership expressions with
item in listandkey in map - LSP completion and hover for expr-lang host variables and functions inside
expr(...)strings - static typechecking for expressions, fields, returns, and registered function/action signatures
- validated call statements through host-registered actions
- typed HIR output for stable lowering
- form declarations
- script-body execution with
let, local reassignment,if,else if, single- and dual-variablefor,for ... where, conditional expressions, membership expressions,break, andcontinue - field assignments, nested forms, and call statements
- expression evaluation with registered and user-defined functions
- lowering from HIR to sample IRs through documentation example modules under
examples/
Plugin packaging and richer module/runtime integration are still pending.
The repository is now past the "parser prototype" stage and behaves more like a real compiler core. The next useful iterations are mostly semantic and tooling work:
- keep extending collection and script ergonomics without collapsing host DSL boundaries into core
- tighten diagnostics, especially related spans and richer import/reference errors
- stabilize the HIR and example-lowering contracts before introducing a formal plugin packaging API
- add more real-world example DSL flows so language changes are exercised against multiple host shapes