Rune is an expression-oriented language toolchain written in Go. The current implementation parses Rune source, checks types, lowers to IR, and can either interpret, compile to Go, or emit TypeScript.
Rune source
-> lexer/parser
-> AST
-> semantic/type check
-> IR
-> interpreter, Go codegen, or TypeScript codegen
-> go run / go build
The fastest path for Rune is still: build a practical language that compiles to Go and reuses Go's runtime, GC, module system, and cross compilation.
scripts/dev.sh
scripts/dev.sh --shell
scripts/dev.sh --vscodeOr run the toolchain directly:
go run ./cmd/rune check examples/fib.rn
go run ./cmd/rune check core
go run ./cmd/rune fmt examples/fib.rn
go run ./cmd/rune run examples/fib.rn
go run ./cmd/rune build -o /tmp/rune-fib examples/fib.rn
go run ./cmd/rune ts examples/counter.rn
go run ./cmd/rune repl
go run ./cmd/rune lspBuild the local CLI used by the VSCode extension:
go build -o .bin/rune ./cmd/runerune check <path> Parse and type-check a file or directory
rune fmt <path> Format a file or directory (alias: format)
rune run <path> Run a file, or a directory with one main
rune build <file.rn> Compile a Rune program to an executable
rune ts <file.rn> Compile a Rune program to TypeScript
rune dts <file.rn> Emit TypeScript declarations for a Rune module
rune repl Start the Rune REPL
rune lsp Start the Rune language server
rune lsp serves over stdio by default and also accepts --stdio.
cmd/rune/ CLI entrypoint
internal/lexer/ Lexer
internal/parser/ Parser
internal/ast/ AST
internal/checker/ Type checking and inference
internal/ir/ Shared IR
internal/interpreter/ IR interpreter
internal/codegen/go/ Rune -> Go backend
internal/codegen/typescript/
Rune -> TypeScript backend
internal/format/ Formatter
internal/lsp/ Language server
internal/repl/ REPL
core/ Rune core library stubs
examples/ Runnable and type-checking examples
selfhost/ Rune implementations of bootstrap components
vscode-rune/ VSCode extension
tree-sitter-rune/ Future Tree-sitter grammar scaffold
Rune files can import other Rune files with Dart-like file imports:
@"./helper.rn"
main() => @io.println(helper())
Imported Rune declarations are private by default. Add + before a helper
declaration in helper.rn when callers in other files should use it:
+ helper() -> Int => 42
The self-hosted lexer, parser, IR, interpreter, CLI parser, and compiler emitter
live under selfhost/; see examples/lexer_bootstrap.rn,
examples/parser_bootstrap.rn, examples/compiler_bootstrap.rn, and the
tests/*_bootstrap.rn files for the current bootstrap entry points.
The self-hosted CLI parser and compiler are the preferred implementation path.
cmd/rune currently remains the Go bootstrap host and dispatch layer while
self-hosted command execution is migrated incrementally. Generated
selfhost_cli_gen.go and selfhost_compiler_gen.go are checked into
cmd/rune/ so that bootstrap changes stay reproducible. The Go implementation
continues to support bootstrapping and executable integration during this
migration. The LSP remains Go-hosted for now and is a planned later migration,
after the CLI and compiler execution paths are self-hosted.
Rune functions are mappings from parameters to expressions:
add(a: Int, b: Int) -> Int => a + b
main() => {
@io.println(add(1, 2))
}
Top-level declarations and enum members are private by default. Prefix a
function, type, or enum member with + to make it visible to other Rune
files. Struct and object fields and methods are public by default; prefix a
member with - when it should stay private.
Blocks return their final expression:
sum(a: Int, b: Int) => {
result := a + b
result
}
Pattern bodies are supported for single-parameter functions:
fib(n: Int) -> Int => {
0 => 0
1 => 1
_ => fib(n - 1) + fib(n - 2)
}
Match expressions use the same pattern syntax:
value {
true => "yes"
false => "no"
}
The built-in scalar types are:
Int
String
Char
Bool
Void
HTMLElement
WebComponent
Struct types use symbolic object syntax:
User: {
id: Int
name: String
age: Int
}
main() => {
user := User {
id: 1,
name: "oboard",
age: 22,
}
@io.println(user.name)
}
Methods live inside type declarations. Inside a method, .field means
this.field:
User: {
age: Int
isAdult() -> Bool => .age >= 18
}
Enum types use the same declaration shape with integer members:
Status: {
Completed = 0
Fail = 1
}
main() => {
status := Status.Completed
@io.println(status == Status.Fail)
}
Anonymous records are expressions:
obj := {
name: "Alice",
age: 30,
nextAge() => .age + 1
greetText() => "Hello, " + .name
}
Rune uses a Hindley-Milner-style inference direction with closed anonymous records. There is intentionally no row polymorphism: two anonymous record types only unify when their fields match as closed types.
Function values can still be refined by call sites. If a function value only
uses x.a and is called with { b, z, a }, the call site can refine the
parameter to the full argument shape.
Named struct arguments are preserved:
Return: {
b: Int
z: Bool
a: Int
}
fun(flag) => {
(flag {
true => (x: Return) => {
k: x.a + 1,
}
false => (y: Return) => {
k: y.b + 1,
}
})({
b: 2,
z: false,
a: 1,
}).k
}
Here x and y are typed as Return; the anonymous argument is accepted
because it structurally satisfies Return, and Go codegen emits a Return
literal.
Lambda parameters must be parenthesized:
arr.map((value) => value * 2)
This is invalid:
arr.map(value => value * 2)
Lambda parameter annotations are supported:
(value: Int) => value + 1
(user: User) => user.name
Core library calls are declaration-driven. The checker and backend load stubs
from core/<module>/<module>.rn; compiler-invented module calls are rejected.
Currently included modules:
core/array
core/bytes
core/buffer
core/compress
core/fs
core/go
core/io
core/iter
core/json
core/map
core/net
core/path
core/process
core/reader
core/set
core/stringbuffer
core/writer
I/O:
@io.print(value)
@io.println(value)
@io.printf(format, value)
Arrays:
main() => {
arr := [1, 2, 3]
arr.push(4)
arr.each((value) => @io.println(value))
doubled := arr.map((value) => value * 2)
@io.println(doubled[0])
}
JSON:
json := @json.stringify({
name: "Rune"
version: 1
})
Inline Go FFI:
@go.import("fmt")
isAdult(age: Int) -> Bool => @go.expr("$age >= 18")
main() => {
name := "oboard"
@go.stmt("fmt.Println($name)")
@io.println(isAdult(22))
}
FFI strings can reference Rune identifiers with $name; the Go backend expands
them after identifier mangling.
Rune-defined identifiers preserve their source spelling in generated Go. Target
keywords receive a trailing underscore (for example, type becomes type_),
and Rune main becomes main_ so a small Go main wrapper remains the
process entrypoint.
Anonymous records are emitted as Go struct literals. Named structs remain named Go structs. Function values are emitted as Go function values.
The TypeScript backend emits DOM-oriented TypeScript from the shared IR. It
does not support @go FFI. XML elements are Rune expressions and embedded
{expr} children become text nodes unless the expression returns
HTMLElement. A function declared as returning WebComponent can return an
XML literal; the TypeScript backend emits a CustomElementConstructor, and
matching XML tags create registered custom elements.
render() -> HTMLElement => {
$count := 0
<div>
<p>Count: {$count}</p>
<button @click={$count++}>Click Me</button>
</div>
}
Compile it with:
rune ts examples/counter.rnStart an interactive session:
rune replThe REPL evaluates expressions and definitions through the interpreter path, so
it does not require a main function.
The VSCode extension in vscode-rune/ provides:
- TextMate highlighting
- snippets
rune lspintegration- diagnostics
- hover
- completion
- go to definition
- rename
- document symbols
- formatting
- inlay hints for inferred function and lambda types
- Run / Debug code lenses for
main
For local development:
scripts/dev.sh --vscodeInlay hints show inferred parameter and return types, including anonymous lambda types:
fun(flag) => {
(flag {
true => (x) => {
k: x.a + 1,
}
false => (y) => {
k: y.b + 1,
}
})({
b: 2,
z: false,
a: 1,
}).k
}
The editor can display flag: Bool, x: { ... }, and return hints inline.
rune run examples/fib.rn
rune run examples/array.rn
rune run examples/anonymous_object.rn
rune run examples/complex_type.rn
rune run examples/ffi.rnexamples/complex_type2.rn is a static type-inference example. It intentionally
contains recursive functions that should be checked, not run.
Rune currently prioritizes:
- expression-first syntax
- deterministic parsing
- strong editor support
- shared IR for interpreter and compiler
- Go backend pragmatism
- closed record types without row polymorphism
Rune intentionally does not yet implement LLVM, a custom VM, GC, JIT, package
manager, package imports, nominal interfaces, or classes. Rune traits use
structural matching and do not require explicit impl declarations.