- `woc` now emits `.wob` that `wovm` runs: emit.ml lowers the typed, owner-annotated AST (scope-stack registers with a >64 WO-E401 diagnostic, Lua-style call windows, ICALL by slot, dedup const pool, drop maps, line tables, implicit terminators); disasm.ml backs `--dump-bc` goldens. - Ownership lowering consumes the four owner tables verbatim; RESIDUAL is the only source of borrow ops, coalesced per operand. Review caught the emitter consuming only 2 of owner.ml's 4 residual producers — an assignment-anchored aliasing violation ran to exit 0 instead of trapping; fixed, plus a backstop raising WO-E404 for any residual region left unconsumed. - Conformance harness `scripts/oop-e2e.sh` (`just oop-e2e`): four fixture kinds with exact outcomes — byte-exact stdout, one WO-E### anchored on `error CODE:`, numeric trap code, gc trace. 25 fixtures incl. pricing-demo logic, the ownership suite, and DB_STUB's parse-but-trap. `tests/` un-ignored so the corpus is actually tracked. - `woc build` produces a self-contained binary: wovm copy + appended image + 20-byte trailer, self-exec via /proc/self/exe. Verified relocated outside the repo, argless, and against adversarial trailer corruption. - Milestone 1's five spec criteria all MET (`just oop-accept`). Criterion 3 closed by WO-E405 — the entry must return `Int`, since program mode already says its return value is the exit code — which deletes the leak class without adding return-type metadata to the format. `gc/held-cycle` retired: an externally-held cycle is not expressible in a post-exit pump. - New spec: inferred GC + incremental per-shard tri-color mark-sweep, retiring `@gc` and reference counting. Story gains iterations 7b (that work) and 9b (`@table`, relations, compiler-checked query); `.dev/reference` gains a sparse System.Linq checkout. Priority: 5→6→7 (log-watcher) then 7b, 8, 9, 9b.
136 lines
6.2 KiB
Markdown
136 lines
6.2 KiB
Markdown
# Milestone-1 source surface the emitter lowers — normative reference
|
|
|
|
> What a `.wo` program may say and have `woc` produce bytecode for.
|
|
> The `.wob` format doc ([`00-wob-format.md`](00-wob-format.md)) names the
|
|
> BUILTIN *ids*; this names their **source spellings** and the handful of
|
|
> rules that have no other home. Landed with the emitter
|
|
> (`compiler/src/emit.ml`, plan 3 task 1). Diagnostics referenced here are
|
|
> catalogued in [`01-error-catalog.md`](01-error-catalog.md).
|
|
>
|
|
> Anything on this page is a contract for corpus fixtures and for every
|
|
> later sub-project's `.wo` code — not an emitter implementation detail.
|
|
|
|
## Builtins
|
|
|
|
Containers and runtime services are free functions, never methods. Each
|
|
maps to one `BUILTIN` id of the format doc.
|
|
|
|
| source | `.wob` builtin | arity | meaning |
|
|
| --- | --- | --- | --- |
|
|
| `now()` | `now` | 0 | wall-clock milliseconds (`Int`) |
|
|
| `print(t)` | `print` | 1 | a `Text`, newline-terminated |
|
|
| `print_int(n)` | `print_int` | 1 | an `Int`, newline-terminated |
|
|
| `words(t)` | `words` | 1 | whitespace token count of a `Text` |
|
|
| `multi_new()` | `multi_new` | 0 | a fresh `multi T` — see the destination rule below |
|
|
| `map_new()` | `map_new` | 0 | a fresh `map<K, V>` — see the destination rule below |
|
|
| `push(m, v)` | `multi_push` | 2 | append to a `multi` |
|
|
| `count(c)` | `count` | 1 | length of a `multi` or a `map` |
|
|
| `latest(m)` | `latest` | 1 | last element of a `multi` (traps `BOUNDS` when empty) |
|
|
| `get(c, k)` | `multi_get` / `map_get` | 2 | element by index, or value by key (a missing key traps `KEY`) |
|
|
| `set(m, k, v)` | `map_set` | 3 | insert or replace in a `map` |
|
|
| `has(m, k)` | `map_has` | 2 | `1`/`0` |
|
|
|
|
`get`, `set`, `push`, `count` and `has` resolve on the container they are
|
|
given, so one source name covers the `multi` and `map` ids the runtime
|
|
keeps apart.
|
|
|
|
**Sugar.** `c[i]` is exactly `get(c, i)` and `m[k] = v` is exactly
|
|
`set(m, k, v)`. There is no element *write* into a `multi` — v1 has
|
|
`multi_push` and `multi_get` and no element store — so `m[i] = v` on a
|
|
`multi` is `WO-E403`.
|
|
|
|
**Shadowing.** A user-declared free `fn` of the same name always wins. A
|
|
declared name is never silently replaced by a builtin.
|
|
|
|
**`push` and `@gc` elements.** `push(m, v)`'s value argument is never a
|
|
resolved callee parameter (`push` has no declared signature), so the
|
|
owner pass's ordinary Take-gated transfer never reaches it; a `@gc` value
|
|
pushed into a `multi` is special-cased in `owner.ml`'s `analyze_call`
|
|
(the value escapes into the container exactly like a ctor field, RC_INC
|
|
included) specifically so a `multi`-mediated `@gc` cycle can be built
|
|
and later collected (`tests/corpus/gc/`, plan 3 task 5). **`set(m, k, v)`
|
|
has no equivalent special case** — a `@gc` key or value handed to `set`
|
|
is not retained, so a `map<_, SomeGcClass>` (or a `@gc`-keyed map) built
|
|
this way will under-count its element's refcount and the collector will
|
|
free it while the map still points at it. Nothing in the corpus
|
|
exercises this yet; treat it as an open gap, not a proven-safe pattern,
|
|
until `set` gets the same fix `push` did.
|
|
|
|
## A fresh container needs a destination of declared type
|
|
|
|
`multi_new()` and `map_new()` carry their element (and key/value) kinds as
|
|
an instruction immediate, and those kinds **are** the container's drop
|
|
plan at runtime (`runtime/src/gc.c`). They cannot be guessed: assuming
|
|
`SCALAR` for a `multi Item` leaks every element, and for a
|
|
`map<Text, _>` leaks every key. Milestone-1 `let` has no container type
|
|
annotation — its optional annotation is a bare identifier — so a fresh
|
|
container must be created where its type is declared:
|
|
|
|
```wo
|
|
class Store {
|
|
items: multi Item
|
|
by_name: map<Text, Int>
|
|
}
|
|
|
|
fn main() {
|
|
let s = Store { items: multi_new(), by_name: map_new() } -- kinds from the fields
|
|
push(s.items, Item { n: 7 })
|
|
set(s.by_name, "seven", 7)
|
|
}
|
|
```
|
|
|
|
A bare `let m = map_new()` is `WO-E403`, reported at the creation site.
|
|
The same rule applies to a `take`/`mut` parameter of declared container
|
|
type, which is also a typed destination.
|
|
|
|
## Calls
|
|
|
|
- Argument count must match the callee's parameter count (`WO-E403`).
|
|
Nothing upstream checks arity — `types.ml` declares `WO-E203` and never
|
|
raises it — and a mismatched call reserves a register window the callee
|
|
does not read, which the loader rejects outright.
|
|
- A method is called as `receiver.method(args)`. When the receiver's
|
|
declared type is an **interface**, the call is dispatched by vtable
|
|
(`ICALL`); satisfaction is structural (same method name, same parameter
|
|
count), Go-style, with no `implements` keyword.
|
|
- `self` occupies the callee's `r0`, so a method's argument count is
|
|
`1 + parameters`.
|
|
|
|
## Program entry
|
|
|
|
The entry point is the **zero-argument free `fn main`**. A `main` that
|
|
takes parameters is not an entry (the format's own rule is a zero-argument
|
|
free fn), and `wovm` will report `module has no entry method`.
|
|
|
|
## Operators with no dedicated opcode
|
|
|
|
Lowered by the emitter, not added to the format:
|
|
|
|
| source | lowering |
|
|
| --- | --- |
|
|
| `a % b` | `a - (a / b) * b` — exact for the VM's truncating `DIV`, which traps on `0` and on `INT64_MIN / -1`, both correct for `%` too |
|
|
| `a != b` | `(a == b) == 0` |
|
|
| `a > b`, `a >= b` | `LT` / `LE` with the operands swapped |
|
|
| `a == b` on `Text` | `EQS` (content equality); `EQ` otherwise |
|
|
| `a .. b` | `CONCAT` — `+` is arithmetic only, never string addition |
|
|
|
|
## Not lowerable in milestone 1
|
|
|
|
Each is `WO-E403` at the offending site, never invented bytecode:
|
|
|
|
- an element write into a `multi` (no element-store instruction);
|
|
- `for` over a `map` (v1 exposes no key enumeration);
|
|
- a name that is neither a local, a parameter, `self`, a declared `fn`,
|
|
nor a builtin;
|
|
- a field or method on a type that is not a declared class — including a
|
|
class named only inside `multi T` / `ref T`, which `types.ml`'s
|
|
unknown-type check (`WO-E225`) does not look inside.
|
|
|
|
## `?T`
|
|
|
|
A nullable field stores exactly what `T` stores and spells nil as `0`.
|
|
The v1 format has no kind byte for it (field kinds run `0..5`; the loader
|
|
rejects `6`), and it needs none: every per-kind drop plan already ignores
|
|
a zero slot. `?T`'s field kind is therefore `T`'s. Note the consequence
|
|
for `@gc`: `?SomeGcClass` is a `GCREF` field like any other, so it
|
|
participates in refcounting and cycle detection normally.
|