The hybrid-boundary inversion is closed: a statically provable interface
violation now fails at COMPILE time instead of reaching wovm as an ICALL
that traps WO_T_BOUNDS at runtime.
- types.ml class_satisfies: the same rule emit.ml's `satisfies` builds
vtable rows from (instance method with matching name + parameter count
for every interface method; `static fn` never satisfies) — one rule, two
consumers, so the check and the vtable can never disagree.
- check_iface_boundary fires wherever a confidently class-typed value flows
into an interface-typed slot: call arguments against the callee's declared
parameters (free fns, methods off confident receivers, interface-method
sigs, statics — resolved exactly as confident_typ resolves returns),
annotated `let`s, and `return`s. Silent when underivable.
- The same per-argument pass extends the ?T boundary to CALL ARGUMENTS
(the previous slice covered stores/returns/operands): nil into a
non-nullable parameter is WO-E212, an unnarrowed ?T argument is WO-E211.
- tests/corpus/trap/unsatisfied-interface -> compile-fail/ with
fixture.code WO-E205, per the fixture's own standing instruction; its
header comment rewritten to the wired reality.
- The new arg checks caught a real mistyped signature in the sample:
log-watcher's rpc_error/rpc_result/call_tool declared `id: json.Value`
while every caller legitimately passes nil (JSON-RPC id-absent) — now
`?json.Value`; dispatch/call_tool/cron-row sites moved to the
bind-then-narrow idiom (including an `or`-guard narrowing:
`if spath == nil or spat == nil { return }`).
- Catalog: E205 gains its main-table row; the "owed gap" section is
rewritten as closed. Board known-gap struck through.
Verified: woc-test 540/0 + test_diag 14/0; oop-e2e 83/0 (fixture now
compile-fail, satisfying-class negative probe compiles clean); oop-accept
ALL MET; log-watcher 7/0; employee 8/0; gc-cycle clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .dev | ||
| .vscode | ||
| compiler | ||
| database/src | ||
| docs | ||
| runtime | ||
| scripts | ||
| tests/corpus | ||
| .gitignore | ||
| .gitmodules | ||
| CLAUDE.md | ||
| justfile | ||
| README.md | ||
| VERSION | ||
writeonce
A small compiled language with a database built in. You write .wo
files; one command turns them into a single native binary that carries its
own storage engine — a typed, WAL-durable, crash-recoverable database — with
no server to install, no ORM, and no query strings. Tables are just classes,
queries are written in the language and checked by the compiler, and the whole
program ships as one file that depends only on the system C library.
Status: early, honest. Everything documented on this page compiles and runs today and is exercised by the acceptance tests in this repository. Features that are planned but not yet available are listed separately under Roadmap — they are not described as if they work. Nothing here is API-stable yet.
Why writeonce
- The database is part of the language. A
classmarked@tableis a table. Its rows persist through a write-ahead log, survive a restart, and are reached by navigating typed relations — not by assembling SQL text. - Queries are compiled, not interpreted.
from e in Employee where e.salary > 90000 select elowers to bytecode loops over the engine. A mistyped field name is a compile error, not a runtime surprise. There is no SQL string anywhere in the shipped binary. - One binary, no runtime dependencies.
woc .produces a self-contained executable (~100 KB for the sample programs) that links only libc. Copy it to a server and run it. - Small on purpose. No FFI, no package manager, no framework. The standard library is a handful of OS modules. The language is designed to be read.
writeonce is not a web framework and does not (yet) serve HTTP, WebSockets,
or a UI. It is a systems language whose distinguishing feature is the embedded
database. If you have seen an older "writeonce" that served REST from cargo run, that was a separate, earlier runtime; this page documents the current
woc/wovm toolchain.
System requirements
To run a compiled writeonce program:
- Linux on x86-64. The produced binary is a native executable that links only
the system C library (
libc); nothing else is required at runtime.
To build programs from source (the toolchain), you need:
| Tool | Version tested | Purpose |
|---|---|---|
| OCaml | 4.14+ | builds woc, the compiler front end |
| dune | 3.14+ | OCaml build driver |
| A C11 compiler | gcc 13 / clang | builds wovm, the runtime VM |
| just | 1.x | task runner for the build/test recipes |
| make | any | drives the runtime build |
Other POSIX platforms (macOS, BSD) are untested. The toolchain itself has no network or package-download step — it builds entirely from the checked-in source.
Getting the toolchain
Two artifacts make up the toolchain:
woc— the compiler (OCaml). Reads.wosource, type-checks it, runs the ownership pass, and emits a.wobimage or a standalone binary.wovm— the runtime (C11). Loads a.wobimage and executes it. Whenwocbuilds a standalone binary, it embeds the image into a copy ofwovm.
Build both from the repository root:
just woc-build # builds compiler/_build/default/bin/woc
just wovm-build # builds runtime/wovm
# gate them (optional but recommended)
just woc-test # compiler unit + golden suites
just wovm-test # runtime unit suites, both dispatch flavors, ASan-clean
Your first program
A writeonce project is a directory with a wo.toml manifest and one or more
.wo files. Every program has an entry point:
-- hello/main.wo
fn main(args: multi Text) -> Int {
print("hello, writeonce");
return 0;
}
# hello/wo.toml
name = "hello"
version = "0.1.0"
[runtime]
wo = ">= 0.1"
Compile the directory into a single binary and run it:
woc hello/ # produces hello/target/hello
./hello/target/hello
# hello, writeonce
main returns an Int — that value is the process exit code. args is
the command-line arguments (the program name is not included).
The two build paths
# 1. standalone binary (what you ship): woc reads wo.toml, emits target/<name>
woc myproject/
# 2. image + VM (handy while developing): emit a .wob, run it with wovm
woc --emit myproject/ -o app.wob
wovm app.wob arg1 arg2
Both paths run the same program. The standalone binary is the release artifact; the image path lets you inspect or move the image around.
Language at a glance
writeonce is statically typed with a compile-time ownership model — every value has a known owner, memory is freed deterministically, and values that form cycles are collected by an inferred garbage collector (you never annotate GC- ness; the compiler infers it). The surface will look familiar:
- Types:
Int,Text,Bool, and userclasstypes.?Tmarks an optional (nullable) value;nilis the empty case. - Containers:
multi T(a growable list) andmap<K, V>. Literals:[],[a, b],{}. - Classes & records: classes with fields and methods,
static const/static fnmembers, module-scoped across files. - Control flow:
if/else,for x in xs,for k, v in m,switchexpressions, andtry { … } catch (e) { … }(also an expression form). - Strings: interpolation with
${expr}inside a"…"literal. - Functions: free functions and methods; arguments and returns are typed.
fn classify(n: Int) -> Text {
if n < 0 { return "negative"; }
return switch n {
case 0: "zero";
default: "positive";
};
}
Standard library
A compact set of OS modules, reached by their reserved names — no imports:
| Module | What it does |
|---|---|
fs |
exists, list, stat, read_all, read_at, append |
time |
sleep, now, local, iso |
env |
get, stopping (a cooperative shutdown flag) |
net |
TCP listen / accept / read / write / close (host + port) |
proc |
run a child process, capture stdout/stderr/exit |
json |
encode / decode (json.decode(t) as T yields ?T) |
These are deliberately minimal — the surface a real program needs, and no more.
The database
This is the point of the language. Declaring storage is declaring a class:
@table(name: "departments", index: [name])
class Department {
name: Text @unique
staff: backlink Employee.dept -- reverse relation, not a stored column
}
@table(name: "employees", index: [dept], index: [dept, salary])
class Employee {
name: Text
salary: Int
hired: Int
dept: ref Department -- foreign key: stored as the row id
}
@tablemakes a class persistent — named storage plus declared secondary indexes. Every instance youinsertis written to a write-ahead log before it is acknowledged, so an acked write survives a crash; on the next start the log is replayed.ref Tis a typed foreign key (a forward relation).backlink T.fis its inverse — a virtual field, no stored column, resolved by an index scan.@uniqueenforces uniqueness at insert/update; a violation is a catchable trap.- Foreign keys restrict deletes: deleting a row that another row still references traps rather than orphaning it.
Writing and reading data
Mutation is direct; queries are a comprehension the compiler lowers to engine operations:
-- insert (WAL-durable); @unique makes a re-insert trap, and try/catch it:
let eng = try insert Department { name: "Engineering" } catch (e) nil;
insert Employee { name: "Asha", salary: 9200000, hired: 1704067200000, dept: eng };
-- query: filter, order, limit, project — checked at compile time
for e in from s in Employee where s.salary > 8000000 order by s.salary desc select s {
print("${e.name} ${e.salary} (${e.dept.name})"); -- ref navigation
}
-- navigate a backlink (the department's staff), update through the result
for e in from s in dept.staff select s {
e.salary = e.salary + e.salary * 5 / 100; -- update-through-row
}
-- delete (restricted if still referenced)
let ok = try delete row catch (e) nil;
The query surface available today is from v in <table | relation> where … [order by k [desc]] [take n] select v | v.field, plus insert, delete, and
update-through-a-row. It is proven end to end by the employee sample, whose
data survives a process restart via log replay.
Project layout & the manifest
myproject/
├── wo.toml # manifest: name, version, [runtime], [build]
├── main.wo # entry point (fn main)
├── types.wo # your @table classes, other types
└── target/ # build output (the standalone binary lands here)
name = "myproject"
version = "0.1.0"
[runtime]
wo = ">= 0.1"
[build]
runtime = "../../../runtime/wovm" # path to the wovm the binary is built from
woc myproject/ compiles every .wo file under the directory as one program.
Programs that create tables read their data directory from the WO_DATA
environment variable at run time:
WO_DATA=./data ./target/myproject seed
WO_DATA=./data ./target/myproject report # a fresh process still sees the data
Worked examples
Two complete sample programs live in the repository and double as the language's acceptance tests:
-
docs/examples/employee/— departments and employees related byref/backlink,@unique, foreign-key restrict on delete, per-department reports, and persistence across a restart. Run it:just employee # compile + run every mode against a durable database -
docs/examples/log-watcher/— a long-running daemon that watches log files for silent death, using thefs/time/net/procstdlib. Run it:just log-watcher
Read either program's main.wo for idiomatic, working writeonce.
Roadmap
Planned, not yet available — listed so the shipped surface above stays honest. These exist as design iterations and/or work-in-progress branches, not as features you can use today:
- Query aggregates —
group … by … into gwithcount/avg/min/maxand projection records. (Today the same result is written by hand from the shipped primitives.) - HTTP service layer —
serviceblocks that route requests to methods. - Concurrency — a shard-actor runtime and green-threaded fibers.
- Cross-program database access — one program attaching to another's database over a local channel, with keypair authentication and per-client rights.
- Blue-green deployment — in-process recompile and atomic version switch.
- Compile-time metaprogramming —
@derive(Json/Csv/Eq/…)generated from a class's own metadata, no reflection.
Known current limits worth naming: net is TCP host+port only; proc.run has
no timeout or signal control; there is no stdin/stdout byte I/O and no FFI.
writeonce is a work in progress. Interfaces will change. If you build something with it, pin to a commit.