writeonce/docs/writeonce-pl.md

7.3 KiB
Raw Blame History

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.

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.