A lean, compiled language for CLI tools and services. Memory safety
comes from dynamic ownership/borrow checking — no tracing GC, no static
borrow checker; concurrency is structured (scope, spawn, channels);
everything compiles ahead-of-time to native binaries through one
toolchain binary.
The dynamic checking is not yet unconditional — two documented gaps
remain (docs/semantics/ownership.md): assigning a scope-backed slice
to an outer struct field is not checked (access after the scope closes
reads freed storage), and ordinary own dereferences compile to raw C
access without per-dereference dangling checks.
Status: v0.2.0 released; the package/module system is on main.
The compiler works end to end — programs in examples/ compile to
native binaries today, including an HTTP CRUD + SQLite service and a
GTK4 todo app — and the toolchain's own lexer, parser, checker, and C
emitter are written in oxl and compile themselves (see
docs/self-hosting.md).
docs/specs/2026-11-03-language-design.md— approved design (the spec everything traces to)docs/semantics/— ownership, concurrency, and compiler-interface contractsdocs/specs/— milestone plans and per-task statusdocs/prototype/report.md— de-risking benchmarks (state-machine frames ~96 B/task; ownership checks ≈ free)
curl -fsSL https://raw.githubusercontent.com/oxidean/oxl/main/scripts/install.sh | shThe installer drops a dispatch shim at ~/.oxl/bin/oxl and the latest
toolchain under ~/.oxl/versions/<tag>/. Put the shim on PATH
(the installer prints the line):
export PATH="$HOME/.oxl/bin:$PATH"
oxl version # → oxl vX.Y.Z (language 0.3)The shim manages multiple toolchains side by side and auto-detects the
one a project needs — $OXL_TOOLCHAIN, a .oxl-version file,
toolchain vX.Y.Z in the nearest oxl.mod, else the default:
oxl toolchain list # installed versions
oxl toolchain install v0.9.0 # fetch another release
oxl toolchain use v0.9.0 # set the default
oxl toolchain use --local v0.9.0 # pin a directory (.oxl-version)Full details: docs/install.md. Prebuilt tarballs
currently ship for Linux x86_64; other supported platforms build from
source below.
Requires: a Rust toolchain (cargo) for the seed compiler, a C compiler
(cc), and pthreads. Programs that use the sqlite3 shims also need
sqlite3 dev headers (-lsqlite3 is linked only when the emitted C calls
it). Supported platforms: Linux (x86_64), macOS (aarch64), and Windows
(x86_64, via MinGW gcc or LLVM clang — the runtime's oxl_* veneer in
runtime/rt.h maps threads/sync to SRWLOCK+CONDITION_VARIABLE and
sockets to WinSock2).
cd oxl && cargo build --release && cd ..
# binary: oxl/target/release/oxl — put it on PATH for the commands below
export PATH="$PWD/oxl/target/release:$PATH"oxl build/run invoke a C compiler for the final link; set
LANG_CC to override (e.g. LANG_CC=clang-18), else the driver
probes cc → gcc → clang (MSVC's cl can't take the emitted C —
_Thread_local, __sync builtins). LANG_LDFLAGS appends extra link
inputs (FFI shim objects, libraries — see examples/gui-todo/build.sh).
Programs that touch SQLite link -lsqlite3; where no dev package
exists (stock Windows) LANG_SQLITE_SRC points at an amalgamation
sqlite3.c to compile instead. tests/run.sh honors the same
overrides.
oxl build file.oxl [-o out] # compile to a native binary
oxl run file.oxl [args...] # compile + run
oxl test file.oxl # run all fn test_*() in a scope each
oxl fmt [-w|--check] file.oxl # canonical AST pretty-print (comments kept)
oxl doc file.oxl # /// comments → markdown
oxl get <path>@<query> # fetch + pin a dependency (needs git)
oxl mod init <path> # write a skeleton oxl.mod
oxl mod tidy # sync requires/oxl.sum with the imports
oxl mod verify # re-hash the cache against oxl.sum
oxl mod list # the MVS-selected module build list
oxl list -m all # same build list (alias)
oxl lsp # stdio LSP: diagnostics, hover, goto-definitionA complete program — a tiny HTTP service built on std, compiled by
oxl run to a native binary that answers GET /hello/<who>:
package main
import "std/encoding/json"
import "std/net/http"
import "std/strings"
struct App { name: str }
fn greet(path: str) -> str {
end := len(path)
q := find(path, "?")
if q >= 0 {
end = q
}
who := substr(path, len("/hello/"), end)
who = strings.trim(who)
if who == "" {
who = "world"
}
return "hello, " + who
}
fn (a App) route(method: str, path: str, body: str) -> http.Resp {
if method == "GET" && strings.startswith(path, "/hello/") {
r := json.jobj()
json.jadd(r, "message", json.jstr(greet(path)))
json.jadd(r, "from", json.jstr(a.name))
return http.Resp{status: 200, body: json.tojson(r) + "\n", ctype: "application/json"}
}
return http.Resp{status: 404, body: "{\"error\":\"not found\"}\n", ctype: "application/json"}
}
fn main() {
http.serve(8080, App{name: "oxl"})
}
Types int float bool byte str cstr err chan + user structs, enum
tagged unions with match, slices []T, function values/closures, and
methods fn (r T) m; own T / ref T / mut T handles with runtime
borrow checks and compile-time move tracking; [v, err] returns with
dropped-error-as-compile-error; defer; extern fn C FFI;
scope/spawn/go + spawnBlocking structured concurrency on a
worker pool + blocking pool (suspend-capable functions compile to
stackless state machines — tasks are ~96 B frames, not threads; see
docs/semantics/concurrency.md); buffered channels mkchan(n) and
select multi-channel wait; break/continue; interface op
constraints; generics fn f[T any]/fn g[T cmp.Ordered] monomorphized
per call site; import "std/strings" modules with as aliases and
.{name, ...} selection; top-level let globals
(process-wide state, initialized before main); builtin surface: print,
itoa, string ops, mkbuf/poke*, argc/argv, readfile/
writefile, exit, mkshared/sget/sset/scas/sadd,
now/now_ms/usleep, sqlite3 shims, assert.
Packages are directories of .oxl files that share a package clause;
package main marks an executable. A project opts into module mode by
placing an oxl.mod manifest at its root:
module github.com/me/app
oxl 0.3
require github.com/oxidean/widget v1.4.2
replace github.com/oxidean/widget => ../widget
Directives: module (this module's path), oxl (minimum toolchain
version), require (a dependency at a version, also in a parenthesized
block), replace (pin a dependency to a local directory), and
toolchain (toolchain pin — parsed by the language, consumed by the
version-manager shim). The two lookups root differently: the language
walks up from the entry file to the nearest oxl.mod (without one,
only std/ imports resolve), while the version-manager shim picks the
pin walking up from the current working directory — invoking oxl from
outside the project tree can resolve a different pin or the default.
Import resolution runs one longest-prefix competition across every
candidate tree: std/... (the toolchain's shipped standard library),
this module's path (resolved under the manifest's directory), used
workspace trees (below), replace directories, and required modules in
the module cache ($OXLMODCACHE or ~/.oxl/mod). Longest matching
path wins; an exact tie goes to the earlier source, and a used module
shadows a require on the same path. Anything no tree provides is
cannot find package.
Packages under internal/ may only be imported by code beneath the
internal directory's parent. Import cycles are a compile error and
print the cycle chain. And an import the file never references is a
compile error too — unused imports do not build.
An oxl.work file at a project root stitches several local modules
into one build:
oxlwork 0.3
use ./
use ./extra
Each use line names a module directory relative to the work file, and
the file is discovered by walking up from the entry the same way
oxl.mod is (file mode — no oxl.mod found — ignores workspaces
entirely). Imports under a used module's path resolve to the local
tree ahead of replace and the cache, and an exact-path tie goes to
the use tree — the require line still documents the published floor
for consumers outside the workspace. used modules need no cache slots
and no oxl.sum entries: oxl mod tidy keeps the require without
fetching and oxl mod verify has nothing to check.
This repo is itself a workspace: packages outside std/ live in the
extra/ module (module github.com/oxidean/oxl/extra), wired through
the root oxl.work. extra/testutil is the first citizen.
oxl get <path>@<query> fetches a module through git (a bare clone
plus git archive of the resolved commit — canonical tree bytes, never
a checkout) into the module cache, upserts the require line in
oxl.mod, and pins both h1: hashes in oxl.sum. Queries: @latest
(highest semver tag), @vX.Y.Z (a tag), or a branch/SHA (recorded as a
pseudo-version, v0.0.0-<timestamp>-<sha>). The cache is
content-addressed (<cache>/<escaped-path>@<version>/, uppercase
letters !-escaped) and sealed read-only; every build verifies a cached
module against oxl.sum — a tampered or missing entry fails with
checksum mismatch / missing oxl.sum entry diagnostics. replace
directories bypass cache and sums by design. Private repos work through
git's own auth (ssh agent, credential helpers); git is a toolchain
dependency for get only.
oxl mod keeps oxl.mod/oxl.sum in sync so neither is edited by
hand: oxl mod init <path> writes a skeleton manifest (refusing to
overwrite), and oxl mod tidy walks the import graph — an import no
require provides is resolved to its containing module and fetched
(longest prefix wins, then progressively shorter ones; a required
module lacking the package is re-probed at @latest), modules only
reached through other modules land as // indirect requires at the
MVS-selected version, and requires nothing imports are dropped — the
companion to unused imports being compile errors. Rewrites are
surgical: comments, replaces, and untouched lines survive verbatim.
oxl mod verify re-hashes every required module's cache slot against
oxl.sum; oxl mod list prints the build list like oxl list -m all. git is needed only when tidy actually fetches — a manifest
already in sync tidies offline.
| Path | What it proves |
|---|---|
examples/hello.oxl |
hello world end-to-end |
examples/features.oxl |
structs, own/mut, [v,err], FFI, scope/spawn/channels |
examples/cli/main.oxl |
CLI pilot: argv, string ops, error paths |
examples/http-crud-sqlite/main.oxl |
the flagship pilot: HTTP server + SQLite + per-connection tasks |
examples/http-hello/main.oxl |
the minimal std/net/http service shown above |
examples/smoke_test.oxl |
oxl test scoped test harness |
tests/stdlib_test.oxl |
stdlib + break/continue coverage |
tests/stdmods_test.oxl |
std/encoding/json, std/sync, std/time end-to-end |
tests/io_test.oxl |
std/io, std/bytes, std/bufio, std/strconv, std/fmt end-to-end |
tests/util_test.oxl |
std/path+filepath, cmp/sort/slices/maps, math+rand, unicode/utf8 |
tests/flaglog_test.oxl |
std/flag registration/usage/parse + std/log level/line shape |
tests/enc_test.oxl |
std/encoding/{base64,hex,binary,csv} RFC vectors + round-trips |
tests/hash_test.oxl, tests/crypto_test.oxl |
std/hash one-shots + crypto/{sha256,md5,hmac} published vectors |
tests/net_test.oxl, tests/httpclient_test.oxl |
std/net loopback dial/listen + net/http client get/post |
examples/gui-todo/ |
GTK todo app over extern fn FFI (see docs/gui-pilot.md) |
bootstrap/cmd/lex |
self-host: the language's lexer written in the language |
bootstrap/parser/ |
self-host: the language's parser written in the language — parses itself, zero skips |
bootstrap/check/ |
self-host: the typechecker written in the language — checks itself clean |
bootstrap/cmd/codegen |
self-host: the C emitter written in the language — compiles itself to byte-identical C |
Try it:
oxl run examples/http-crud-sqlite/main.oxl &
curl -d 'widget' localhost:8080/items # → {"created":"widget"}
curl localhost:8080/items # → {"items":[{"id":1,"name":"widget"}]}oxl/ the seed compiler (Rust): lexer → parser → check → codegen → C → cc
runtime/ rt.c/rt.h — arenas, ObjHdr checks, scopes/traps, executor, channels
docs/ design doc, semantics annex, prototype report
std/ the standard library, written in the language (strings, testing, http/net, json, csv, base64, hex, binary, sync, time, os, errors, io, bytes, bufio, fmt, strconv, path, filepath, sort, slices, maps, cmp, math, rand, unicode/utf8, flag, log, net, net/http, hash, crypto/{sha256,md5,hmac}, collections, regexp, container/{heap,list,ring})
bootstrap/ self-host components — lexlib, parser, check libraries + cmd/ drivers (docs/self-hosting.md)
extra/ the extras module (`github.com/oxidean/oxl/extra`) — packages outside std, resolved via the root oxl.work workspace
oxl.mod this repo's module manifest (`module oxl`)
oxl.work workspace file — `use ./` + `use ./extra`
examples/ pilot programs (see above)
tests/ tests/run.sh end-to-end suite + tests/diag/ checker-diagnostic corpus (runs in CI)
scripts/ commit-check.sh — commit grammar + no-AI-attribution gate
prototype/ de-risking benchmarks
docs/ product docs, standards, semantics, and the spec archive
.ox-agent/ ox-agent runtime — work items, skills, workflows, pipelines
.agents/skills materialized agent skills (agentcanon layout — see AGENTS.md)
Planning flows through ox-agent work items (ox-agent start); design
exploration runs through the staged skills (brainstorm → specify →
multi-stage-plan → implementation → verify → ship).