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 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 <file.rn> Parse and type-check a Rune program
rune fmt <file.rn> Format a Rune source file
rune run <file.rn> Compile and run a Rune program
rune build <file.rn> Compile a Rune program to an executable
rune ts <file.rn> Compile a Rune program to TypeScript
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
vscode-rune/ VSCode extension
tree-sitter-rune/ Future Tree-sitter grammar scaffold
Rune functions are mappings from parameters to expressions:
add(a: Int, b: Int) -> Int => a + b
main() => {
@io.println(add(1, 2))
}
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
Bool
Void
HTMLElement
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
}
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/go
core/io
core/json
core/map
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 are prefixed with __ in generated Go to avoid
collisions with Go keywords and runtime names. Rune main becomes __main,
with a small Go main wrapper as 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.
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, or Tree-sitter parser. Those can come after the language core and LSP are stable.