7.3 KiB
writeonce-pl — the .wo language at a glance
writeonce is a declarative full-stack programming language. You write .wo files; the wo toolchain compiles them into a single binary that owns its database, serves REST, and pushes live subscriptions. The one-line pitch: Go + Postgres + net/http + Phoenix LiveView, folded into one language and one binary.
It is declarative by default — programs are built from type, class, service, policy, and on <event> declarations. A type declares a data shape; a class is its behavior-bearing sibling — the same fields plus fn methods with a self receiver. There is no inheritance: no extends, no overriding, no virtual dispatch — composition via ref/multi, Go-style. From either declaration the compiler derives the database schema, HTTP endpoints, triggers, and client SDKs. The imperative parts (queries, transactions, method bodies) use a hybrid SQL + Cypher sublanguage.
The three pillars
| Pillar | Language construct | Status |
|---|---|---|
| Live subscriptions | LIVE prefix on queries; subscribe in a service's expose list. Deltas push on every commit over WebSocket — no polling. |
Stage 3 — /api/<type>/live is a 501 stub today |
| Front-end development | ##ui blocks compile to server-rendered HTML plus a small vanilla-JS client runtime. No Node, no React. |
Phase 6 — design-only (spec) |
| Database DML | Hybrid SQL + Cypher + document paths: $name parameters everywhere, cross-paradigm RETURNING col AS alias, one BEGIN … COMMIT block syntax. |
Query layer prototyped in C++ (prototypes/wo-db/); schema layer shipped in Stage 2 |
A complete program
One file is a full application — a database table, six REST endpoints, and a live subscription:
-- article.wo
type Article {
id: Id
title: Text
body: Markdown
author: Text
created_at: Timestamp = now()
service rest "/api/articles"
expose list, get, create, update, delete, subscribe
}
$ wo run
[wo] listening on :8080
All three pillars in ~10 lines: the type fields are the DML schema, expose subscribe is the live subscription, and (from Phase 6 on) a ##ui block alongside it renders the screen.
Implementation status
| Stage | What | Status |
|---|---|---|
| 1 | wo run <dir> discovers .wo files |
✅ shipped |
| 2 | parser + in-memory engine + REST CRUD | ✅ shipped — cargo run --bin wo -- run docs/examples/blog |
| 3 | LIVE subscriptions over WebSocket | pending (501 stub) |
| 4+ | transactional fn, policies, triggers, ##ui, WAL, codegen |
design-only |
Class model status: class declarations parse and serve REST CRUD today (plan 13a, shipped); method execution (13b), live pricing push (13c), and the MVC UI (plan 14) follow. Demo: examples/pricing/; master plan: plan 13.
The runtime itself targets zero external dependencies — all I/O driven directly by Linux kernel primitives (epoll, inotify, sendfile, …); see the kernel-primitive catalogue.
Where to read next
runtime/wo-language.md— the full user-facing language overview: toolchain, runtime stdlib, client SDKsruntime/database.md— the 7-phase engineering series behind the languageruntime/database/02-wo-language.md— the two-layer language spec (schema layer + query layer)examples/blog/README.md— the canonical worked example../prototypes/wo-db/README.md— the C++ prototype of the query-layer engine
Understanding the basics
Every layer of writeonce ultimately reduces to one primitive operation: ask the kernel for memory, store a value in it, read it back. Walking that operation up the abstraction ladder shows what the .wo syntax is actually hiding.
Level 0 — assembly: the kernel gives you a page
; x86-64 Linux — map one anonymous page, store 42 in it
mov rax, 9 ; syscall number: mmap
xor rdi, rdi ; addr = NULL (kernel picks)
mov rsi, 4096 ; len = one page
mov rdx, 3 ; prot = PROT_READ | PROT_WRITE
mov r10, 0x22 ; flags = MAP_PRIVATE | MAP_ANONYMOUS
mov r8, -1 ; fd = none
xor r9, r9 ; off = 0
syscall ; rax now holds the page address
mov qword [rax], 42 ; store the value
mov rbx, [rax] ; read it back
There is no "variable" — only an address the kernel handed back and a mov into it.
Level 1 — C: the libc wrapper names the address
long *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
*p = 42; /* store */
long v = *p; /* read */
Same syscall, same page — C just gives the address a typed name and lets the compiler emit the movs. (malloc is one more layer: a userland allocator carving up pages obtained exactly this way.)
Level 2 — .wo: the value gets a type, a lifetime, and durability
type Counter {
name: Text @unique
value: Int = 0
}
main {
insert Counter { name: "visits" }; -- allocate + store
update Counter{ name == "visits" }.value += 1;
let c = select Counter{ name == "visits" };
print(c.value); -- read back
}
The insert is still, underneath, "obtain memory, write bytes at an offset" — but the declaration has been folded into the language: the type decides the layout, the engine owns the allocation (in-RAM rows over mmap-backed segments), and Phase 3+ adds what raw memory never had — ACID transactions, WAL durability, and LIVE subscribers notified on every store.
This is the whole design in miniature: the runtime is written in Rust against raw kernel primitives (level 0–1, see plan/exploration/linux/00-linux.md and the assembly stance), so that the .wo author never has to leave level 2.