diff --git a/.gitmodules b/.gitmodules index 3d38b21..abe6910 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "reference/writeonce-api"] path = reference/writeonce-api url = https://github.com/shoneyJ/writeonce-api +[submodule "docs/examples/site"] + path = docs/examples/site + url = git@github.com:shoneyJ/writeonce-site.git diff --git a/docs/examples/site b/docs/examples/site new file mode 160000 index 0000000..81ef9b9 --- /dev/null +++ b/docs/examples/site @@ -0,0 +1 @@ +Subproject commit 81ef9b9ca00c7941dd37d3d3e3e4de6af849a5b4 diff --git a/docs/examples/site/CODE-LOGIC.md b/docs/examples/site/CODE-LOGIC.md deleted file mode 100644 index 22f5dbe..0000000 --- a/docs/examples/site/CODE-LOGIC.md +++ /dev/null @@ -1,108 +0,0 @@ -# site — how it is put together - -Written 2026-08-23 with the sample's landing; restructured 2026-08-25 -onto the program template's MVC layout (`docs/examples/shop`), so the two -samples now read the same way. - -## The layout - -| file | layer | what it owns | -| --- | --- | --- | -| `types.wo` | MODEL | the `Chapter` `@table`, the `ChapterLink` projection, `Chapters.links()`, and `seed_if_empty()` | -| `content.wo` | MODEL (content) | the ten chapter bodies as fragment-returning functions, plus `seed_chapters()` | -| `layout/app.wo` | VIEW (chrome) | `AppShell` — the component that fills writeonce-view's `Layout` — the two named widths, and `html_error` | -| `layout/header.wo`, `layout/footer.wo` | VIEW (chrome) | the shared nav bar (brand = mark + wordmark) and footer | -| `layout/logo.wo` | VIEW (chrome) | the mark as inline SVG, plus the `
` links | -| `install/view.wo`, `install/controller.wo` | VIEW + CONTROLLER | `/install` — the toolchain guide. Static copy, so `InstallPage` has no fields | -| `packages/view.wo`, `packages/controller.wo` | VIEW + CONTROLLER | `/packages` and `/packages/:name` — the catalogue, its cards, and per-package usage | -| `favicon/controller.wo` | CONTROLLER | `/favicon.svg` — builds its own `Resp` (image/svg+xml) | -| `home/view.wo` | VIEW | `HomePage` and the homepage's code showcase | -| `home/controller.wo` | CONTROLLER | `Home` — the `/` handler | -| `chapter/view.wo` | VIEW | `ChapterNav`, `ChapterPage` | -| `chapter/controller.wo` | CONTROLLER | `ShowChapter` — the `/ch/:slug` handler | -| `admin/controller.wo` | CONTROLLER | `AdminEdit` — bearer-gated edit, answers a redirect (no view: it redirects) | -| `health/controller.wo` | CONTROLLER | `Health` — the liveness probe (no view: it answers text) | -| `main.wo` | BOOTSTRAP | seed, routes, serve. Nothing else | -| `wo.toml` | — | the two `[deps]`: `framework` (serving) and `html` (markup) | - -One feature = one directory = one module, holding that feature's view -and its controller together. A module sees its own declarations plus -what it `use`s, so `home/` reaching the chapter nav has to say `use -chapter`. - -The model stays at the root and is reachable from everywhere: a CLASS -crosses module lines without being exported, and only a free `fn` is -module-scoped (`WO-E210`). That single rule explains the whole layout — -`Chapter` and `ChapterLink` are classes, so the feature modules just -name them; the shared query would have been a free fn, so it is a -`static fn` on `Chapters` instead. (`pub` cannot prefix an `@table` -class — recorded gap #1 — but nothing needs it to.) - -## Decisions that are not obvious from the code - -- **Chapters are rows, not constants.** `seed_if_empty()` inserts them - only when the table answers empty, so a WAL restart keeps admin edits - instead of reseeding over them — the sample's own proof of chapter 6's - claim. The seed bodies are BUILT with writeonce-view's builders at boot; after - that the table is the truth and the builders are never consulted again. -- **The seam is enforced by where the query sits.** `Chapters.links()` - lives with the MODEL and hands the view a `multi ChapterLink` — - a projection, not a cursor. No component in this sample touches the - database, which is what lets `ChapterNav` be the same component on the - homepage and on every chapter page, differing only by `current`. -- **`HomePage` and `ChapterPage` hold a `Component`, not chapter data.** - The nav arrives as an already-built child component in a slot, so - neither page knows what a chapter is. That is content projection — - Angular's `.wo files and one entry: a free " .. "function named main. It returns the process exit code. There is no " .. "runtime to install separately and no build pipeline — woc build produces " .. "ONE self-contained binary with the VM and your bytecode inside.");
- b = b .. code_block("fn main() -> Int {\n print(\"hello, writeonce\");\n return 0;\n}");
- b = b .. el("p", "leading-relaxed mt-4",
- "Run it: woc build . -o hello && ./hello. " .. "Statements end with ;, blocks use braces, comments start with --.");
- return b;
-}
-
-fn ch_values() -> Text {
- let b = el("p", "leading-relaxed mb-4",
- "let binds a value; the type is inferred. Scalars: Int (64-bit), " .. "Float, Bool, Text (bytes, binary-safe). Text " .. "interpolates with \${...} and concatenates with ... " .. "Integer literals speak hex and binary, and the full bitwise set is here: " .. "& | ^ << >> — grouped Go-style, so a mask compare needs no parentheses.");
- b = b .. code_block("let port = 8080;\nlet pi = 3.14159;\nlet name = \"writeonce\";\nlet msg = \"listening on \${port}\";\n\nlet flags = 0b1010_0001;\nlet high = flags & 0xF0; -- bitwise AND, then == compares\nlet shifted = 1 << 12; -- 4096\nif flags & 0x80 != 0 {\n print(\"top bit set\"); -- groups (flags & 0x80) != 0\n}");
- return b;
-}
-
-fn ch_containers() -> Text {
- let b = el("p", "leading-relaxed mb-4",
- "Two containers: multi T (a growable list) and map<K, V>. " .. "A map read m[k] answers nil when the key is absent — the everyday idiom " .. "for optional lookups like HTTP headers. for .. in walks both.");
- b = b .. code_block("let langs: multi Text = [\"c\", \"ocaml\", \"writeonce\"];\npush(langs, \"more\");\nprint(\"count \${len(langs)}\");\n\nlet ages: mapimplements). This is how porch, the web framework, takes handlers.");
- b = b .. code_block("interface Handler {\n fn handle(req: Req) -> Resp\n}\n\nclass Hello {\n greeting: Text\n fn handle(req: Req) -> Resp {\n return ok_text(\"\${self.greeting}, \${req.path}\");\n }\n}\n\n-- any class with a matching handle() satisfies Handler\napp.get(\"/hello\", Hello { greeting: \"hi\" });");
- return b;
-}
-
-fn ch_optionals() -> Text {
- let b = el("p", "leading-relaxed mb-4",
- "?T is a value or nil, and the compiler forces the check before use. " .. "Failures are TRAPS: named, catchable, never silent. try ... catch (e) is " .. "an expression; e carries code, line, method and message. Anything " .. "uncaught ends the program with the same structured report.");
- b = b .. code_block("let n = parse_int(\"42x\"); -- ?Int\nif n == nil {\n print(\"not a number\");\n}\n\nlet r = try fs.read_all(\"/etc/missing\", 4096) catch (e) e.msg;\nprint(r); -- the file's bytes, or \"No such file or directory\"\n\nlet d = 0;\nlet q = try 10 / d catch (e) 0 - 1; -- DIV0 is a trap, caught here");
- return b;
-}
-
-fn ch_tables() -> Text {
- let b = el("p", "leading-relaxed mb-4",
- "The database is IN the language. @table makes a class a table; " .. "insert writes a row; queries are first-class expressions; an UPDATE is a " .. "plain field assignment on a query result. With WO_DATA=<dir> every " .. "commit is WAL-durable before it is acknowledged and replays on restart — this very " .. "site stores these chapters that way, and the admin form's edits survive a kill.");
- b = b .. code_block("@table(name: \"notes\", index: [tag])\nclass Note {\n tag: Text @unique\n val: Int\n}\n\ninsert Note { tag: \"first\", val: 1 };\n\nfor n in from x in Note where x.val > 0 order by x.tag select x {\n print(\"\${n.tag} = \${n.val}\");\n}\n\nlet hits = from x in Note where x.tag == \"first\" take 1 select x;\nif len(hits) == 1 {\n hits[0].val = 2; -- an update: assign through the row\n}");
- return b;
-}
-
-fn ch_storage() -> Text {
- let b = el("p", "leading-relaxed mb-4",
- "Two optional @table keys decide where a table's rows LIVE. Both default " .. "to today's behaviour, so every table above keeps working untouched. " .. "durable: false keeps a table entirely in RAM — a full table in-process, " .. "same indexes, same @unique, same queries — that is simply empty after a " .. "restart. That is what a session store or a rate-limit counter wants: losing it on " .. "reboot is correct, and paying to make it durable is waste.");
- b = b .. code_block("@table(name: \"sessions\", durable: false)\nclass Session {\n sid: Text @unique\n hits: Int\n}\n\n@table(name: \"catalog\", resident: keys, index: [sku])\nclass Product {\n sku: Text @unique\n price: Int\n blurb: Text\n}");
- b = b .. el("p", "leading-relaxed mt-4 mb-4",
- "resident: keys is the other direction: only the KEY stays in memory. The " .. "row body lives in the write-ahead log and is read back on demand, so a table can be " .. "far larger than RAM without a page-fault cliff. Measured on a product catalogue, the " .. "resident set is 2.55× smaller than the same table fully resident. " .. "Such a table has nowhere to keep its rows without a log, so declaring it and starting " .. "without WO_DATA is REFUSED at startup rather than silently ignored.");
- b = b .. el("p", "leading-relaxed mb-4",
- "Reads and writes are unchanged — the storage mode is not a different API:");
- b = b .. code_block("let hits = from x in Product where x.sku == \"a-1\" take 1 select x;\nif len(hits) == 1 {\n hits[0].price = 1299; -- an ordinary assignment\n}");
- b = b .. el("p", "leading-relaxed mt-4",
- "Underneath, that assignment appends a delta — which field changed, plus a " .. "back-pointer to the row's previous record — instead of rewriting the whole row. A read " .. "walks that chain backward, newest wins. Left alone the chain would grow forever on a " .. "hot row, so it is BOUNDED: past 16 links an update writes a full row image and the " .. "chain restarts at zero. A row updated a million times still costs a bounded number of " .. "record reads, on read and on replay alike.");
- return b;
-}
-
-fn ch_actors() -> Text {
- let b = el("p", "leading-relaxed mb-4",
- "Concurrency is actors on fibers: spawn makes an actor from a class with a " .. "receive method, send delivers one message at a time, and " .. "ownership MOVES with the message — no locks, no shared mutable state, no data races " .. "by construction. Blocking calls park the fiber; the shard serves others meanwhile. " .. "One VM per core by default; mailboxes are bounded (a full one is a catchable trap).");
- b = b .. code_block("class Counter {\n total: Int\n fn receive(msg: Tick) {\n self.total = self.total + msg.n;\n print(\"total \${self.total}\");\n }\n}\n\nclass Tick {\n n: Int\n}\n\nfn main() -> Int {\n let c: actor Tick = spawn Counter { total: 0 };\n send(c, Tick { n: 1 });\n send(c, Tick { n: 2 });\n time.sleep(50); -- parks this fiber; the actor runs\n return 0;\n}");
- return b;
-}
-
-fn ch_deps() -> Text {
- let b = el("p", "leading-relaxed mb-4",
- "Dependencies are git repositories pinned in wo.toml; wo.lock " .. "records the exact revision, and locked builds work offline. The [deps] KEY names the " .. "module you use. This site has two: porch, the writeonce web framework, and the writeonce-view " .. "library that rendered the page you are reading.");
- b = b .. code_block(`
- [deps]
- porch = { git = "https://github.com/shoneyj/porch", rev = "v0.1.0" }
- view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" }`);
- b = b .. code_block(`
- use porch
- use porch/http
- use view
-
- -- html's builders + tailwind-style utilities, zero JS, no build step:
- let body = el("h1", "text-3xl font-bold", "Hello");
- return ok_html(page("Hello", body));`);
- return b;
-}
-
-fn ch_serving() -> Text {
- let b = el("p", "leading-relaxed mb-4",
- "The whole stack of this site: routes with :param captures, a middleware " .. "chain, handler classes, @table persistence, and server-rendered HTML — " .. "one binary behind a proxy. This is the site's own main, abbreviated:");
- b = b .. code_block("fn main(args: multi Text) -> Int {\n seed_if_empty();\n let app = App { middleware: [], routes: [] };\n app.use_mw(Mw { m: Logging {} });\n app.get(\"/\", Home {});\n app.get(\"/ch/:slug\", ShowChapter {});\n app.post(\"/admin/ch/:slug\", AdminEdit { token: token });\n return app.serve(\"127.0.0.1\", port);\n}");
- b = b .. el("p", "leading-relaxed mt-4",
- "The admin route checks its bearer token in the handler — mechanism lives in the " .. "framework (bearer_token, constant-time ct_eq), POLICY stays " .. "in the app. Try editing this chapter: " .. "curl -X POST -H \"authorization: Bearer ...\" -d \"title=...&body=...\" /admin/ch/serving.");
- return b;
-}
-
--- One seed row per chapter: (ord, slug, title, body-builder above).
-pub fn seed_chapters() {
- insert Chapter { slug: "hello", ord: 1, title: "Hello, writeonce", body: ch_hello() };
- insert Chapter { slug: "values", ord: 2, title: "Values, Text and bitwise", body: ch_values() };
- insert Chapter { slug: "containers", ord: 3, title: "multi and map", body: ch_containers() };
- insert Chapter { slug: "classes", ord: 4, title: "Classes and interfaces", body: ch_classes() };
- insert Chapter { slug: "optionals", ord: 5, title: "Optionals and traps", body: ch_optionals() };
- insert Chapter { slug: "tables", ord: 6, title: "@table: the built-in database", body: ch_tables() };
- insert Chapter { slug: "storage", ord: 7, title: "Storage modes: durable and resident", body: ch_storage() };
- insert Chapter { slug: "actors", ord: 8, title: "Actors and fibers", body: ch_actors() };
- insert Chapter { slug: "deps", ord: 9, title: "Dependencies", body: ch_deps() };
- insert Chapter { slug: "serving", ord: 10, title: "Serving the web (this site)", body: ch_serving() };
-}
diff --git a/docs/examples/site/favicon/controller.wo b/docs/examples/site/favicon/controller.wo
deleted file mode 100644
index 18b8dc6..0000000
--- a/docs/examples/site/favicon/controller.wo
+++ /dev/null
@@ -1,13 +0,0 @@
--- favicon/controller.wo — GET /favicon.svg. The one route that answers
--- something other than HTML or text, so it builds its own Resp.
-use porch/http
-use layout
-
-pub class Favicon {
- fn handle(req: Req) -> Resp {
- let h: mapTwo native binaries — woc,
- the compiler, and wovm, the runtime VM. Both depend only on
- the system C library. There is no package manager to install, no
- language runtime to install on the machines you deploy to, and no
- build toolchain beyond these two files.
One tarball, two binaries. Take it from - the GitHub release, or from this site as a mirror — they are the same - bytes, and the checksum below proves it:
-- Download 0.1.0 (linux-amd64) - Mirror -
-Verify it before you extract it. The - expected digest is served beside the archive at - .sha256:
`, - code_block("curl -O https://writeonce.de/dl/writeonce-0.1.0-linux-amd64.tar.gz\ncurl -O https://writeonce.de/dl/writeonce-0.1.0-linux-amd64.tar.gz.sha256\nsha256sum -c writeonce-0.1.0-linux-amd64.tar.gz.sha256")); - - let s2 = section("2. Extract it", - `Extract into /usr/local,
- replacing any previous install. Run this as root, or through
- sudo:
Add one line to your
- $HOME/.profile (or /etc/profile for every user
- on the box), then restart your shell:
Both should print the same version.
- woc finds wovm beside itself, so a tarball
- install needs no further configuration.
A project is a directory with a
- wo.toml manifest and one or more .wo files.
- Nothing else — no lockfile to create by hand, no scaffolding step.
woc <dir> emits ONE
- standalone binary at target/<name>. Copy that file to a
- server and run it — the VM is inside it, and so is the database.
Dependencies are git repositories pinned
- by revision. The [deps] KEY is the module name your code
- uses. woc writes a wo.lock with the
- exact revision, and a locked build works offline. See
- Packages for what is available.
The tarball installs to
- /usr/local/writeonce: bin/woc,
- bin/wovm, and a VERSION file. To point
- woc at a different VM, set $WO_RUNTIME or add a
- [build] runtime = "..." key to wo.toml. A
- manifest may also require a minimum toolchain with
- [runtime] wo = ">= 0.1"; woc refuses to build
- a project that needs a newer toolchain than itself.
woc imports symbols up to GLIBC_2.35 and wovm up to GLIBC_2.34, read off the published v0.1.0 release. That covers Ubuntu 22.04+, Debian 12+ and Fedora 36+. On RHEL 9 (2.34) the runtime works but the compiler does not — build elsewhere and copy the binary over, which is the point of a self-contained executable.Check yours with
- ldd --version. If it is older than 2.35, build from source
- until a wider-compatibility release exists. The floor is set by the
- machine that BUILT the release, not by the language — the CI runner is
- pinned to Ubuntu 22.04 to keep it this low.
use porch.",
- git: "https://github.com/shoneyj/porch",
- rev: "v0.1.0",
- what: `
- App — the route table and the middleware chains; app.get/post/delete_, use_mw, use_after, mount, serve.StaticFiles — serve a directory over a wildcard route. Traversal is refused, not normalised; max_bytes is a hard ceiling. This site's /dl downloads run through it.Handler, Middleware, After — structural interfaces. A handler is a CLASS; its fields are the closure this language does not have.Req/Resp plus builders: ok_text, ok_html, ok_json, created_json, not_found, bad_request, unauthorized, conflict, redirect.bearer_token, basic_credentials, constant-time ct_eq. Which routes are gated stays your policy.form_values, multipart_parts, content negotiation, ETags, security headers, CORS, WebSocket frames.esc() — escapes & < > ". A {{ }} hole in a raw text literal compiles to a call to it, so display data is escaped by construction. (Written with entities here: inside a raw literal, a real {{ would BE a hole.)Component — a class with fields and fn render() -> Text, satisfied structurally. multi Component holds children directly; render_all renders them in order.Layout — content projection: title, head, nav, content and footer as pre-rendered slots.el, link, card, nav_bar, code_block, form_post, text_input, text_area, submit_btn, btn_link.tw_css() + page() — one hand-written utility sheet, inlined, so a page is one self-contained response. No CDN, no build step, no JS.{{ self.summary }}
-A package is a git repository with a
- wo.toml that says kind = "library". There is no
- registry and no publish step: you depend on a URL and a revision, and
- wo.lock pins exactly what you built against. These are the
- libraries this site itself is built on.
{{ self.summary }}
-Add it to your wo.toml. The
- KEY is the module name — that is what use imports.