Skip to content

Repository files navigation

Rune Language

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.

Quick Start

scripts/dev.sh
scripts/dev.sh --shell
scripts/dev.sh --vscode

Or 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 lsp

Build the local CLI used by the VSCode extension:

go build -o .bin/rune ./cmd/rune

CLI

rune 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.

Project Layout

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.

Self-hosting direction

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.

Language Snapshot

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"
}

Types

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
}

Type Inference

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.

Lambdas

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

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.

Go Codegen

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.

TypeScript Codegen

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.rn

REPL

Start an interactive session:

rune repl

The REPL evaluates expressions and definitions through the interpreter path, so it does not require a main function.

VSCode

The VSCode extension in vscode-rune/ provides:

  • TextMate highlighting
  • snippets
  • rune lsp integration
  • 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 --vscode

Inlay 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.

Examples

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.rn

examples/complex_type2.rn is a static type-inference example. It intentionally contains recursive functions that should be checked, not run.

Design Notes

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.

About

Rune is an expression-oriented language toolchain written in Go. It parses and checks Rune source, lowers it to a shared IR, and can interpret it, compile it to Go, or emit TypeScript for DOM-style programs.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages