Sakura Lang

Quickstart

Build the Sakura Lang compiler from source, then check, test and run a first program.

Pre-alpha: build from source

The compiler runs the core language and substantial M3 functionality on JavaScript, including contracts, std handlers, permissions, scaffolding, LSP and MCP. No release or installer is published. See the roadmap for remaining M3 gates and planned M4/M5 work.

Install from source

You need rustup and Node.js (sakura run and sakura test execute the generated JavaScript with node; CI uses Node 26). The repository pins its Rust toolchain in compiler/rust-toolchain.toml, so rustup fetches the right version on first build.

git clone https://github.com/icantcodefyi/sakura-lang
cd sakura-lang/compiler
cargo install --locked --path crates/sakura_cli
sakura --version
sakura 0.0.0

The standard library is embedded in the binary, so sakura works from any directory. To try it without installing, run cargo run -p sakura_cli -- <command> inside compiler/.

Run an example

compiler/examples/ holds the design's example programs as projects. All twelve pass the configured run/test/check modes listed on the roadmap:

sakura run compiler/examples/counter
sakura test compiler/examples/typed_errors
1. milk
2. eggs
1. bread

counter is a user-defined effect whose handler keeps state; typed_errors recovers from typed failures with try/catch. The examples README records each mode; agent_loop and secrets currently have check-only coverage.

Create a project

Run sakura new cli hello to scaffold a project (lib, agent, and api templates also exist), or write the two files below by hand. A project is a sakura.toml manifest and a src/ directory, and one file is one module.

hello/sakura.toml
[package]
name = "hello"
version = "0.1.0"
edition = "2026"

[permissions]

[permissions] is the ceiling on what the program may do (design §6). This program only prints, so it is empty. sakura run enforces startup grants and passes the table as SAKURA_PERMISSIONS for host-handler enforcement. Missing required root authority exits with 75.

hello/src/main.sakura
/// Build a greeting for a person.
fn greet(name: String) -> String { "Hello, {name}" }

pub fn main() -> Unit ! {Console} { Console.print(greet("world")) }

test "greets by name" { expect_eq(greet("Ada"), "Hello, Ada") }

property "always starts with Hello"(name: String) {
  expect(String.starts_with(greet(name), "Hello, "))
}

greet has no !, so it is pure. main declares Console, the only effect the program performs. This is the canonical layout: sakura fmt keeps a short body on its signature's line.

Check, test and run

Run these from hello/. With no path, each command uses src/.

sakura check
no problems

sakura check parses, resolves names, checks types and effects, and checks that every match is exhaustive. It generates no code, and it accepts typed holes.

sakura test
test greets by name ... ok
test always starts with Hello ... ok

2 passed, 0 failed, 0 skipped (seed 0)

sakura test runs every test and property block under deterministic handlers (a fixed clock, a seeded random source, captured console output) with the network denied. --seed 7 changes the property inputs, --filter greets runs matching tests only, and --json writes one record per test.

sakura run
Hello, world

sakura run checks the program, compiles it to JavaScript under target/sakura/js/, and runs main with node. Arguments after -- go to the program. The exit code is the program's: 0 for success, 1 when main fails with an uncaught typed error, 70 for a defect such as integer overflow, with one JSON defect record on stderr.

sakura fmt rewrites files in the one canonical format; sakura fmt --check changes nothing and exits 1 if a file is not formatted.

Check it, as an agent would

Suppose greet starts printing without declaring it:

hello/src/main.sakura
/// Build a greeting for a person.
fn greet(name: String) -> String {
  Console.print("greeting {name}")
  "Hello, {name}"
}

pub fn main() -> Unit ! {Console} { Console.print(greet("world")) }
error[V3001]: function `greet` performs `Console`, which its signature does not allow (allowed: `{}`)
  ┌─ src/main.sakura:2:4
  │
2 │ fn greet(name: String) -> String {
  │    ^^^^^
3 │   Console.print("greeting {name}")
  │   -------------------------------- `Console.print` performs `Console` here
  │
  = expected: {}
  =    found: {Console}
  = help: Add `Console` to the signature
  = help: Handle the effect locally with `with`
  = see `sakura explain V3001`

1 error

sakura check --json writes the same error as JSON Lines, one record per line, followed by a summary record. The diagnostic record, pretty-printed with hashes shortened:

{
  "schema_version": "1.0",
  "kind": "diagnostic",
  "id": "d:e8f9a0e1d729d3d1",
  "code": "V3001",
  "severity": "error",
  "phase": "effect",
  "message": "function `greet` performs `Console`, which its signature does not allow (allowed: `{}`)",
  "snapshot": "sha256:9470da24…",
  "symbol_id": null,
  "spans": [
    { "uri": "file:///…/hello/src/main.sakura", "content_hash": "sha256:f1ee9f10…",
      "start_byte": 38, "end_byte": 43, "role": "primary" },
    { "uri": "file:///…/hello/src/main.sakura", "content_hash": "sha256:f1ee9f10…",
      "start_byte": 72, "end_byte": 104, "role": "cause",
      "label": "`Console.print` performs `Console` here" }
  ],
  "related_ids": [],
  "expected": { "render": "{}", "row": { "labels": [], "tail": null } },
  "actual": { "render": "{Console}", "row": { "labels": [{ "effect": "std::Console", "args": [] }], "tail": null } },
  "obligation": null,
  "counterexample": null,
  "explanation_uri": "sakura://explain/V3001",
  "fixes": [
    { "id": "f:e8f9a0e1d729d3d1.0", "title": "Add `Console` to the signature",
      "applicability": "suggested", "impact": "interface_change",
      "edits": [{ "span": { "uri": "file:///…/hello/src/main.sakura", "content_hash": "sha256:f1ee9f10…",
                            "start_byte": 67, "end_byte": 67, "role": "primary" },
                  "replacement": " ! {Console}" }] },
    { "id": "f:e8f9a0e1d729d3d1.1", "title": "Handle the effect locally with `with`",
      "applicability": "incomplete", "impact": "local",
      "edits": [/* wraps the body in `with ?handler { … }` */] }
  ]
}

Spans are zero-based, half-open UTF-8 byte offsets, and an edit applies only while content_hash still matches. The first fix changes the function's interface, so it is suggested, never machine_applicable: an agent must not widen a signature silently. sakura explain V3001 prints the rationale with a wrong and a fixed program. See Diagnostics for the format.

A typed hole is how an agent asks the compiler what fits:

fn unfinished(x: Int) -> Int { ?result }
hole ?result: expected Int, effects {}; locals [x: Int]; candidates [x, Int.abs(?i), Int.clamp(?i, ?lo, ?hi), …]

sakura check accepts holes; sakura build and sakura run reject them with V2050. See Typed holes.

Commands

CommandToday
sakura check [--json] [paths]Parse, resolve, type and effect check, exhaustiveness. Holes allowed
sakura build [paths]Check, then generate JavaScript. Holes rejected (V2050)
sakura run [path] [-- args]Build and run main with node
sakura test [--json] [--seed N] [--filter text] [--update-snapshots]Run tests and properties with deterministic handlers
sakura fmt [--check] [paths]Canonical format. There are no options
sakura explain V3001Rationale, causes and repairs for a diagnostic code
sakura promptPrint the language guide for agents (llms.txt)

Also available: sakura new, sakura init, sakura fix, sakura check --frozen-specs all [--since rev], sakura lsp, and sakura mcp serve. sakura doc and trust-site sakura audit are M3 completion work. Package commands remain planned; verification/WASM are M4 and native is M5.

Next

Start the Language Tour.

Edit on GitHub

On this page