writeonce/docs/guides/language-surface.md
shoney.arickathil 23550e7021 feat(lang+wo-html): raw text literals, component layer, MVC samples
- lexer: backtick raw text literal — content verbatim, no escape
  processing, common source margin removed at lex time; `${ }` raw and
  `{{ }}` auto-escaping holes
- `{{ e }}` desugars to `esc(${e})` in parser.ml — a Call on the `esc`
  in scope, so types/owner/emit/.wob/VM are untouched
- WO-E004 unterminated raw literal; WO-E005 newline inside "..." —
  closes a hole where a missing quote silently ate the rest of the file
- wo-html: `Component` interface, `render_all`, `Layout`, README
- framework: `ok_html` joins ok_text/ok_json in http/types.wo
- site + shop restructured to one-feature-one-module MVC (view +
  controller per directory, model at the root, bootstrap-only main)
- removed the filler `pad: Int` convention — verified unnecessary for
  plain classes, interface dispatch, containers and actors
- corrected recorded claims: gap #1 blocks neither the build nor the
  layout; a class crosses module lines, only a free fn is scoped
- docs/guides/language-surface.md — the full grammar inventory
- story 37 landed and moved to done/

Gates: oop-accept MET, oop-e2e 116/0, woc-test 556/0, site 11/0,
web-app 46/0, fibers 10/0, db-actor 8/0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 03:58:41 +02:00

244 lines
12 KiB
Markdown

# The writeonce language surface — everything a `.wo` file may contain
Derived from the front end as it stands on 2026-08-24, by reading
`compiler/src/lexer.ml`, `parser.ml`, `ast.ml` and `types.ml` — not a
spec. Where this disagrees with the compiler, the compiler is right.
The normative companions are
[`08-builtin-surface.md`](../plan/oop-vm/08-builtin-surface.md) (what the
runtime offers) and [`01-error-catalog.md`](../plan/oop-vm/01-error-catalog.md)
(every diagnostic). The reasoning under the front end is
[`compiler/src/CODE-LOGIC.md`](../../compiler/src/CODE-LOGIC.md).
Read this as the answer to "what can I write?" — the last section is the
matching answer to "what will the compiler refuse?", which is just as
much part of the surface.
Every form listed below was compiled and run against `woc`/`wovm` while
writing this page, not read off the parser and hoped for.
---
## 1. Lexical
| thing | form | notes |
| --- | --- | --- |
| comment | `-- to end of line` | the only comment form; no block comment |
| identifier | `[A-Za-z_][A-Za-z0-9_-]*` | **internal dashes are legal** — `a-b` is ONE identifier, so binary minus after an identifier needs spaces (`a - b`) |
| integer | `42`, `0xFF`, `0b1011`, `1_000_000` | `_` only BETWEEN digits; hex/binary accumulate in 63-bit OCaml int, so a full-width `0xFFFF…FFFF` is out of reach (`-1` spells all-ones) |
| float | `1.5`, `2e9`, `1.0e-3` | a bare digit run stays `Int`; only a fraction or exponent makes a `Float` |
| text | `"..."` or `'...'` | escapes `\n \t \r \0 \\ \" \'`; anything else after `\` is that literal character. A raw newline inside is **WO-E005** |
| interpolation | `"${expr}"` | desugars at parse time to a `..` chain; `\$` is a literal `$`, and a lone `$` not followed by `{` is literal |
| raw text literal | `` `...` `` | iteration 37 — content verbatim (NO escape processing), newlines are content, common source margin removed at compile time. Holes: `${e}` raw, `{{ e }}` HTML-escaped |
| booleans / nil | `true`, `false`, `nil` | |
| newline | significant | terminates a statement (or `;`). A line ending in `..` continues on the next — the one newline suppression |
| build flags | `#if name` / `#else` / `#end` | token-level filter, flag NAMES only (no expressions); nesting allowed; set with `woc -D name` |
**Keywords** (35): `type class interface fn let mut take return if else
while for in true false use spawn using pub break continue do const and
or not inline switch case default typedef try catch nil as INSERT
SELECT`.
Deliberately NOT keywords, so they lex as ordinary identifiers: `self`,
`me`, `subscribe`, `receive`, lowercase `insert`/`select`, and every
query clause word (`from`, `where`, `group`, `by`, `into`, `order`,
`desc`, `take`, `select`) plus every type constructor word (`ref`,
`multi`, `map`, `backlink`, `actor`).
## 2. File and module level
A directory is a module; `pub` is the export line. A file may contain,
in any order:
| declaration | form |
| --- | --- |
| import | `use fs` (reserved stdlib namespace) or `use shared/util` (project-relative path) |
| import + extension methods | `using shared/textutil` — the module's `pub` free fns whose first parameter matches a receiver become callable as methods on it (compile-time rewrite) |
| class | `[pub] class Name { fields, methods, consts }` |
| plain type | `[pub] type Name { ... }` — identical field grammar to `class`; methods parse for real in both |
| structural record | `typedef Name = { field: T, ... }` — fields only, and two records of the same SHAPE are the same type |
| tagged union | `type Name = A \| B \| C(x: Int, ...)` — bare variants lower to integer tags, payload variants to records. Construction is call-style and POSITIONAL: `C(1, 2)`, never `C { x: 1 }` |
| interface | `[pub] interface Name { fn sig(...) -> T }` — signatures only, no fields, no bodies. Satisfaction is STRUCTURAL |
| free function | `[pub] fn name(params) -> T { ... }` |
| constant | `const NAME = <literal>` — substituted by the parser before typecheck; a local of the same name shadows it |
The program entry is the free `fn main`, zero-argument or `fn
main(args: multi Text) -> Int`.
### Annotations
| annotation | where | effect |
| --- | --- | --- |
| `@table(name: "…", index: [a], index: [b, c])` | on a `class`/`type` | the class IS a WAL-backed table |
| `@unique` | on a field | uniqueness constraint |
| `@gc` | on a class | **rejected** — GC-ness is inferred, never declared |
Unknown annotation names parse and are ignored; argument lists on field
annotations are consumed and discarded.
## 3. Types
| kind | spelling |
| --- | --- |
| scalars | `Int`, `Float`, `Bool`, `Text`, `Bytes`, `Timestamp`, `Id` |
| nullable | `?T` — legal on any of the above and on heap shapes |
| list | `multi T` |
| map | `map<K, V>` |
| row reference | `ref C` |
| reverse relation | `backlink C.field` — the computed inverse of a `ref` |
| actor address | `actor M` — M is the message type, inferred from the class's `receive` |
| declared types | any `class` / `type` / `typedef` / union name |
| stdlib types | `json.Value`, `net.Conn` |
| predeclared records | `Stat`, `TimeParts`, `Proc`, `Error` — no source declares them; field ORDER is the contract with the C runtime |
### Fields and parameters
- Field: `name: T`, optionally `= <default>` and/or `@ann`.
- `pub(read) name: T` — readable outside the declaring class, writable
only inside it.
- Parameter conventions: **borrow is the default**; `mut x: T` for a
mutable borrow, `take x: T` to move ownership in. These apply to
NAMED parameters only.
- **`self` is implicit** — never written in the parameter list, and
always writable inside its own class's methods (`self.total += 1`
needs no annotation). `self` is an ordinary identifier, not a keyword.
- Methods may be `static fn`; class-level constants may be `static
const` or bare `const`.
## 4. Statements
| statement | form |
| --- | --- |
| binding | `let x = e`, `let x: T = e` |
| assignment | `x = e`, `obj.f = e`, `m[k] = e` |
| compound assignment | `x += e`, `-=`, `*=`, `/=`, `%=` — parse-time sugar for the written-out form (there are no bitwise compound assigns) |
| conditional | `if c { } else if c { } else { }` |
| while | `while c { }` |
| do-while | `do { } while c` — body always runs once |
| for | `for x in <multi \| query>`, `for k, v in <map>` |
| loop control | `break`, `continue` — owned values alive in the body are dropped at the jump site |
| return | `return` / `return e` |
| database write | `insert C { f: v, ... }`, `delete e` |
| expression | any expression in statement position |
## 5. Expressions
| form | spelling |
| --- | --- |
| literals | int, float, text, raw text, bool, `nil` |
| container literals | `[]`, `[a, b, c]` (a `multi`), `{}` (an empty `map`) — a fresh container needs a destination of declared type |
| constructor | `C { field: v, ... }` |
| access | `x.f`, `c[i]` (a `multi` — out of range traps), `m[k]` (a `map` — a missing key is **nil**) |
| call | `f(a, b)`, `x.m(a)`, `mod.member(a)` |
| unary | `-e`, `not e` |
| binary | see the ladder below |
| checked conversion | `e as T` — its ONE meaning is decoding JSON text, yielding `?T`. There is no reinterpret cast |
| actor spawn | `spawn C { fields }` → an `actor M` address |
| trapping guard | `try <expr> catch (e) <expr-or-block>` — an EXPRESSION; `e` binds the `Error` record `{ code, line, method, msg }` |
| multi-way choice | `switch e { case A: ...; default: ...; }` — an expression. Arms match VALUES; `case a, b:` fires for either. A union payload binds positionally: `case Rect(w, h): w * h`. `default` is required UNLESS the subject is a union whose variants are all covered |
| query | `from … select …`, see below |
| interpolation | inside `"…"` and `` `…` `` |
### Operator precedence, loosest to tightest
1. `or`
2. `and`
3. comparison — `== != < <= > >=`
4. concatenation — `..`
5. additive — `+ -` **and `|` `^`**
6. multiplicative — `* / %` **and `&` `<<` `>>`**
7. unary — `-`, `not`
8. `as` conversion — binds to a postfix expression, so tighter than unary
9. postfix — call, index, field
Bitwise operators do not get their own tiers: `|`/`^` ride the additive
rung and `&`/`<<`/`>>` the multiplicative one (Go's arrangement). They
are Int-only on BOTH sides — there is no Float twin — and a literal
shift count outside `0..63` is a compile error.
`+` is arithmetic ONLY, never string addition. `and`/`or` are
short-circuit, `Bool`-typed operands only — there is no truthiness, and
no `&&`/`||`/`!`/`~` anywhere in the language.
## 6. Queries (language-integrated, never SQL text)
```
from <var> in <source>
where <expr> -- zero or more
group <expr> by <key> into <gvar>
order by <expr> [desc]
take <expr>
select <expr>
```
The source is either a table class (`from p in Product`) or a
navigation (`from s in dept.staff` — a `backlink` or a `multi`). Present
today: from / where / order / take / select plus group-by aggregation.
Joins are not in the slice. A query is an expression and is also what
`for x in <query>` iterates.
## 7. Concurrency
- `spawn C { fields }` constructs the actor's state (fields MOVE in) and
starts it; the value is an `actor M` address.
- `send(addr, msg)` — fire and forget; the message MOVES to the runtime.
- `call(addr, msg) -> R` — a send that parks the calling fiber until the
receive returns. Every `receive` program-wide must agree on `R`, and
`R` must be a copyable scalar. A dead callee traps, never hangs.
- A class becomes an actor by declaring `fn receive(msg: M)` — `receive`
is an ordinary identifier, not a keyword.
- Blocking stdlib calls park the fiber. There is no `async`, no `await`,
and no user-visible thread.
## 8. Builtins and the stdlib
**Free builtins** (a user-declared `fn` of the same name always wins):
`print`, `print_err`, `print_int`, `now`, `words`, `len`, `count`,
`byte_at`, `char_of`, `substr`, `trim`, `to_lower`, `starts_with`,
`ends_with`, `index_of`, `last_index_of`, `split`, `split_ws`, `join`,
`parse_int`, `int_to_text`, `multi_new`, `map_new`, `push`, `get`,
`set`, `has`, `remove`, `latest`, `pop`, `shift`, `slice`, `sort`,
`reverse`, `key_at`, `val_at`, `send`, `call`, `sha1`, `sha256`,
`hmac_sha256`, and the Float/Bytes bridges `float`, `trunc`,
`parse_float`, `float_to_text`, `float_cmp`, `bytes_len`, `bytes_at`,
`bytes_slice`, `bytes_eq`, `bytes_concat`, `bytes_of_text`,
`text_of_bytes`, `base64_encode`, `base64_decode`.
**Reserved module namespaces**, each resolving to builtins:
| module | covers |
| --- | --- |
| `fs` | `exists`, `list`, `stat`, `read_all`, `read_at`, `append` |
| `time` | `now`, `sleep`, `local`, `iso`, `ticks` |
| `env` | `get`, `stopping` (the SIGTERM/SIGINT latch) |
| `net` | `listen`, `listen_unix`, `accept`, `accept_dl`, `read`, `read_dl`, `write`, `write_dl`, `peer`, `close` |
| `proc` | `run` |
| `json` | `encode`, `decode` (paired with `as T`) |
A failing syscall traps `IO` with errno's message; **absence is never a
trap** — a missing path or an unset variable is nil.
## 9. What the language deliberately does NOT have
This list is doctrine, not a backlog. Each was considered and rejected.
- **Closures and function values.** Capture is a field on a class. This
is why there is no dependency injection and no callback API anywhere.
- **`&&`, `||`, `!`, `~`.** Word operators only (`and`, `or`, `not`);
complement is `-1 ^ x`.
- **Inheritance.** Interfaces are structural; there is no `extends`.
- **`inline fn`.** The keyword exists solely to produce a clear
rejection — optimization is the compiler's job.
- **A reinterpret cast.** `as` decodes JSON and nothing else.
- **Varargs, and generics beyond the built-in containers.**
- **Truthiness.** A condition must be `Bool`.
- **A block comment, and a `{{`/`${`/backtick escape inside a raw
literal.** Those three are written by concatenating an ordinary
`"..."` string with `..` — one greppable door.
- **`async`/`await`.** Fibers park; the shard runs someone else.
- **A runtime template engine.** Markup is a compile-time literal or it
does not exist.
- **Non-empty map literals** (`{ k: v }` in expression position) —
indistinguishable from a constructor literal without lookahead nothing
else needs.
- **A full-width 64-bit integer literal**, and **joins in queries** —
both real limits rather than doctrine.