culebra Yuji Hirose
winget install --id=yhirose.culebra -e Programming language with a bytecode VM, JIT, and AOT compiler
winget install --id=yhirose.culebra -e Programming language with a bytecode VM, JIT, and AOT compiler
> Status: pre-1.0 and under active development — APIs and syntax may change.
A dynamically-typed cross-platform scripting language with three backends: a bytecode VM, an LLVM JIT, and an ahead-of-time build that emits a standalone binary. Write scripts, CLI tools, machine learning, desktop apps, and games.
The stdlib, test runner, linter, formatter, debugger and docs are all in that one executable. Nothing else to install! Or try it in the browser first on the Playground.
Paste this into a terminal — it downloads culebra, writes an example to hello.cul, and runs that same source
file three ways: as a script, JIT-compiled, and built into a standalone binary.
curl -fsSL https://raw.githubusercontent.com/yhirose/culebra/master/install.sh | sh
culebra --version
cat > hello.cul <<'EOF'
let people = [
{ name: 'Taro', greeting: 'Konnichiwa' },
{ name: 'John', greeting: 'Hello' },
{ name: 'Ada', greeting: 'Bonjour' },
]
for p in people.sorted_by(|p| p.name) {
println("{p.greeting}, {p.name}!")
}
EOF
culebra hello.cul # the bytecode VM (default)
culebra --jit hello.cul # JIT
culebra build hello.cul -o hello && ./hello # AOT: compile once, ship the binary
cat hello.cul | culebra - # stdin (curl ... | culebra - also works)
Optional — this one also touches your editor's config
(VSCode/Vim/Neovim) and writes AGENTS.md/CLAUDE.md into the
current directory, so it isn't bundled into the block above:
culebra init
culebra init installs syntax highlighting and the debug adapter for
whichever of VSCode, Vim, or Neovim it finds on this machine, and adds
coding-agent instructions to AGENTS.md (or
CLAUDE.md/.github/copilot-instructions.md if one already exists) —
reopen hello.cul afterward and the highlighting is already on, and
Claude Code or another agent reading AGENTS.md already knows the
conventions.
For Linux (x86-64), swap the first line for culebra-linux-x64.tar.gz;
Windows and a permanent, system-wide install are below.
| Platform | Download |
|---|---|
| macOS (Apple Silicon) | culebra-macos-arm64.tar.gz |
| Linux (x86-64) | culebra-linux-x64.tar.gz |
| Windows (x86-64) | culebra-windows-x64.zip |
These always point at the newest release; culebra --version reports
which one you have. Extracting from the command line also avoids the
quarantine flag macOS puts on anything unpacked through Finder — the
binaries are unsigned. To put culebra on PATH permanently instead
of just for this shell:
sudo mv culebra-*/culebra /usr/local/bin/
Or skip the manual steps — install.sh downloads the right archive for
your OS/arch, verifies its checksum, and installs it to /usr/local/bin
(macOS Apple Silicon and Linux x86-64 only; use CULEBRA_INSTALL_DIR to
change the destination):
curl -fsSL https://raw.githubusercontent.com/yhirose/culebra/master/install.sh | sh
macOS also installs through a package manager:
brew install yhirose/culebra/culebra
Windows and Linux have no package manager entry yet — use the download above.
Checksums and every release's notes are on the releases page.
culebra build emits a single executable —
nothing to install alongside it, no runtime dependencies.Fast cold start, single-file programs, and a stdlib that is already in scope add up to a runtime that doesn't punish an agent for invoking it every turn.
import lines for the stdlib. JSON, Http, Regex, PEG,
FS, Tensor, SQLite, Search, and the rest are bound before the program runs.culebra build turns the script the agent just
wrote into a binary it can hand back to the user.A bare x = 1 is an immutable binding — reassigning it is an error;
write mut x = 1 for one you intend to change. No declaration keyword
for the common case.
The whole standard library is bound before the program runs — no package manager, no lockfile:
.env, UUID, and SQLite — the amalgamation is compiled
in, so there is no system library to install.Parallel, shared buffers, and
Ctrl+C delivered as a channel message.Term — color, cursor control, the alternate screen,
and key/mouse input for TUIs, downsampled to whatever the terminal
supports (and silent under NO_COLOR).Scene — a retained-mode 3D renderer for procedural geometry
with physically based lighting. Experimental: opt-in
(-DCULEBRA_ENABLE_SCENE=ON) and not in the released binaries. It
builds on all three platforms; the window is exercised on macOS and
Linux.Tensor is a language primitive, not a library. Arithmetic is
operators over a lazy graph that Tensor.eval materializes; matmul is
.dot(). Ops run on the CPU (AVX2 / NEON kernels, Accelerate on
macOS) or the GPU (Metal on macOS, CUDA where nvcc built it in),
picked per op by size unless a Tensor.use_*() call pins one.
x = Tensor.from([[1.0, 2.0], [3.0, 4.0]])
y = x.dot(x.transpose())
Tensor.eval(y) # [[5.0, 11.0], [11.0, 25.0]]
Dir.embedded(name) hands a directory of files to your program with no
code change across backends: the VM and JIT read it live
from disk — edit a file, rerun — and culebra build bakes every byte
into the executable, so the shipped binary needs nothing alongside it.
It is one kind of Dir, read the same way as a directory on disk
(Dir.disk) or files held in memory (Dir.memory).
let assets = Dir.embedded("dist") # index.html, favicon.ico, ...
println(assets.exists("index.html")) # => true
println(assets.exists("favicon.ico")) # => true
A desktop GUI in web tech: a local HTTP server supplies the UI, the
OS's own WebView engine displays it, and culebra build ships the
whole thing — server, routes, and embedded assets — as one binary.
Desktop.run({
title: "Hello from culebra",
assets: Dir.embedded("dist"), # index.html, favicon.ico, ...
routes: fn (srv) {
srv.get("/api/hello", fn (req) {
"hi from the embedded server"
})
},
})
An immediate-mode 2D framebuffer — draw, present, poll input,
repeat — with sprites and text; sound is the Audio namespace (tones,
samples, music, synthesised PCM), which needs no window. It opens a real window on
macOS, Linux and Windows; a run that declares itself headless does the
same pixel work and displays nothing, which is how the tests and a
displayless server run it.
Canvas.run(160, 160, fn () {
Canvas.clear(Canvas.rgba(20, 24, 40))
Canvas.rect(20, 76, 8, 8, Canvas.rgba(220, 60, 60))
false # stop after one frame
})
if guards.enum Shape { Circle(Float), Rect(Float, Float) },
generic where you need it, matched by variant or by enum.(3, 4) is immutable and hashable; {1, 2, 3}
is an insertion-ordered set of unique values."head={head}, rest={tail.size()}", with
format specs, triple-quoted blocks that dedent, and re"…" regex
literals.x.f(y) ≡ f(x, y)).Iterable / Iterator; user-defined traits
with default methods.class form. Both supported, same encapsulation
semantics.fn whose body contains yield returns an
Iterator; yield from delegates to any iterable.effect fn / perform / handle … with, with
multi-shot resume — one mechanism covering generators, exceptions,
dependency injection, and backtracking search.@expr before a fn or class wraps the declared
value before it is bound.1 + 1 # => 2 is an executable
assertion under culebra test.assert_eq prints a diff.async/await. Isolate.spawn runs a
closure on its own thread with its own heap, values cross by copy,
and Channel carries them — so two isolates cannot race on one
object. SharedBuffer (fixed-layout, zero-copy) and Shared
(immutable, by reference) opt out of the copy where it matters.Every subcommand below is part of the executable that runs a program.
| Command | What it does |
|---|---|
culebra test [paths...] | run tests and the doctests written in comments |
culebra lint [paths...] | report static problems without running the program |
culebra fmt [paths...] | reformat source to the canonical style |
culebra dap | speak the Debug Adapter Protocol over stdio (VSCode / Vim / Zed) |
culebra lsp | serve diagnostics, hover and formatting to an editor over stdio |
culebra docs [topic] | read and search the embedded reference docs |
culebra init | set up this directory's AI agent instructions and this machine's editors |
culebra build -o | compile ahead-of-time into a standalone executable |
culebra wrap | build an extended binary exposing your own C++ classes |
Details are in docs/tooling.md.
culebra build compiles a .cul source ahead-of-time into a
self-contained executable. No LLVM at runtime; tree-shaking drops
the ~500 runtime helpers a program doesn't reference. Tensor-free
programs also drop the Accelerate / Metal framework dependency.
culebra build path/to/script.cul -o ./out
./out
The link step runs the host's C++ compiler on macOS (Xcode's Command
Line Tools) and Linux (cc); on Windows the binary carries its own
linker, and culebra toolchain install fetches the mingw libraries it
links against. Everything else is inside the binary. Cross-compile is
also supported. Host requirements and binary sizes per feature axis are
in docs/deployment.md.
The bytecode VM embeds into a C++23 host: header-only, no LLVM, no culebra library to build or install. C++ and the script call each other, so a script can be what the host is configured — or extended — with.
#include
int main() {
culebra::vm::Embed embed; // stdlib installed, traits registered
std::vector readings{18.5, 41.2, 79.8}; // the host's own data
size_t call_count = 0;
// The argument and return types come from the signature; the host state
// the body reads and writes is an ordinary capture.
embed.define("temperature", [&](int64_t sensor) {
call_count++;
return readings.at(sensor);
}, {"sensor"});
embed.eval(R"(
config = {mut limit: 40.0}
fn too_hot(sensor) { temperature(sensor) > config.limit }
too_hot(0)
)");
// The session outlives the run, so the host reads what the script left,
// calls back into it, and writes into its values — an Object is a
// reference, not a copy.
auto config = embed.global("config");
auto limit = config["limit"].as(); // 40.0
auto hot = embed.call("too_hot", 1).as(); // true: 41.2 > 40.0
config.set("limit", 50.0);
hot = embed.call("too_hot", 1).as(); // false, same script
// call_count is 3 by now: the script's own call, and the two above.
}
Build it with the flags the engine itself reports:
c++ $(culebra embed-flags --cflags) host.cpp $(culebra embed-flags --libs) -o host
A failure in script code arrives as culebra::CulebraError, with the
kind, message and position the script's own catch sees.
docs/deployment.md
has the rest: the types define converts, the rest of the Value surface
over a script's Objects and Arrays, C++ classes through
culebra wrap,
the JIT path, threading, and interrupts.
async/await. Stack
traces stay readable, the debugger sees every frame, and library
authors don't write a colored version of every function. Targeted
at services up to a few-thousand-connection ceiling — the shape
SQLite, Redis, and most line-of-business backends already use. The
reasoning is written out in
docs/essays/concurrency.md.weak. An object with a
drop property has it called when the last reference goes away —
including for a reference cycle a scope leaves behind, so a
back-reference to a parent needs no weak annotation. Reference
counting drives the timing; a tracing collector backs it up for
cycles. The reasoning is written out in
docs/essays/memory.md.x = 1
creates an immutable binding — reassigning it is an error. Use mut
for a variable you intend to change (mut x = 1; x = 2), so mut
marks exactly what changes; let is an optional, explicit marker for
the immutable form. The default binding is both safe and ceremony-free,
and mutation is visible where it happens.x.f(...) doubles as the resolution path
for free functions over user types.import. Namespaces are bound before the
program runs, so a one-file script has no import block. Splitting
across files uses top-level import / export, which keeps the
dependency graph known at parse time — what AOT bundling and
tree-shaking need.Culebra targets three forms of performance: fast startup, small binaries, and BLAS-class numerics.
benchmarks/mnist/
and benchmarks/microgpt/.docs/handbook.md
/ 日本語docs/language.md
/ 日本語docs/stdlib.md
/ 日本語culebra test / lint / fmt / dap /
lsp / docs):
docs/tooling.md
/ 日本語culebra build / culebra wrap):
docs/deployment.md
/ 日本語docs/guides/ — currently
parsing with PEG and
building a desktop appdocs/quick-guide.md
/ 日本語CLAUDE.md, .github/copilot-instructions.md or
AGENTS.md (culebra docs agent):
docs/agent.md
/ 日本語docs/essays/Building from source and running the tests: CONTRIBUTING.md.