writeonce/docs/superpowers/plans/2026-08-22-operator-parity.md
shoney.arickathil c0b0dbb846 docs: audit all markdown against the code, fix findings, flatten status folders
- README: shipped concurrency/HTTP/WebSockets sat in the roadmap as "not yet
  available"; "no package manager" contradicted [deps]; the deps example
  would not have compiled (the key IS the module name)
- runtime/README: leads with wovm, wo-rt.c demoted to a historical section;
  dropped 2 nonexistent recipes, crates/rt, @gc refcounting, 13 suites -> 18
- employee + log-watcher READMEs claimed "does not compile"; both are gates
- error catalog: +10 emitted codes incl WO-E250, the only diagnostic the
  shipped query surface raises; recorded why the sweep rotted
- language-surface: group-by parses, then the typechecker refuses it
- 00-code-review + 00-link-audit re-run; history kept, not rewritten
- 48 dead Rust-era exploration links de-linked rather than re-pointed (their
  prose names the retired plan by number); successor map -> discarded.md
- 08-project-structure: compiler/plan/ never existed; corpus has 9 dirs, 5 empty
- releasing.md: dropped a --draft step the workflow never had
- new docs/00-doc-audit.md: findings + disposition, incl one row where the
  audit was wrong and the doc it accused was right
- status folders removed: 34 stories flat, status only in frontmatter; 252
  links recomputed from resolved paths; board/board-views/structure retaught
- story 24 -> in-progress, since frontmatter is now the only truth
- new iteration 38: fs mutation verbs + net.connect, the two capability
  families no iteration owned
- new iteration 39: gofiber/fiber v3.5.0 parity study. The ledger called
  CSRF/sessions unblocked by iteration 34's HMAC, but the runtime has no
  source of randomness at all
- linkcheck skips .dev/.superpowers: 0 broken paths, 0 bad anchors

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 19:20:22 +02:00

261 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Iteration 36 — operator parity (`not`, bitwise, hex literals, compound assigns): implementation plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use
> superpowers:subagent-driven-development (recommended) or
> superpowers:executing-plans to implement this plan task-by-task.
> Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** the language gains boolean `not`, the five Int bitwise
operators (`&` `|` `^` `<<` `>>`), hex/binary/underscore integer
literals, and working compound assigns (`+=` `-=` `*=` `/=` `%=`) —
full stack, `.wob` v6.
**Architecture:** front-to-back in dependency order — tokens/lexer,
then AST/parser, then the type checker, then emit + VM opcodes, then
the corpus/gates closeout. Every semantic decision is already settled
in the story ("Settled decisions", 2026-08-22): `>>` arithmetic,
out-of-range shift count traps (literal counts rejected at compile
time), `not` at the unary level beside minus.
**Tech stack:** OCaml compiler (`compiler/src/`), C runtime/VM
(`runtime/src/`), dune + just gates.
**Spec:** the story IS the spec —
`docs/stories/language-runtime-database/36-operator-parity.md`
(story-19 deviation precedent: decisions recorded normatively there,
reasoning-under-the-code lands in CODE-LOGIC.md as part of this plan).
## Global constraints
- Plans in this repo carry no code blocks — each step names the exact
file/anchor and describes the change; the executor writes the code.
- Every pre-existing fixture must lex, parse, and run byte-identically
(the iteration-19 discipline). Any golden that changes is a defect in
the change, not a bless-and-move-on.
- Precedence copies Go (`.dev/reference/go/src/go/token/token.go:266`):
`&` `<<` `>>` join the multiplicative rung, `|` `^` join the additive
rung — no new rungs, both above comparison.
- Bitwise is Int-only. No Float, Bool, Text, Bytes operands — the
existing operand-type diagnostics reject them; `numeric_world`
(`types.ml:193`) is not touched.
- Run gates via just recipes only (`just woc-test`, `just wovm-test`,
`just oop-accept`, `just web-app`, `just deps-accept`).
- Branch off master first (never commit on master); bullet-style commit
messages, ≤25 lines, no push.
---
## Task 1 — lexer and tokens: the new surface exists as tokens
**Files:** modify `compiler/src/token.ml` (kind list ~134-150),
`compiler/src/lexer.ml` (keyword table ~131-147, operator switch
~500-570, digit scanner ~386-430), `compiler/src/dump.ml` (kind labels
~103).
**Interfaces produced:** tokens `Amp`, `Caret`, `Shl`, `Shr`,
`StarEq`, `SlashEq`, `PercentEq`, keyword `KwNot`; hex (`0x`), binary
(`0b`), and underscore-separated integer literals arriving as ordinary
`Token.Int`.
- [ ] Add the seven operator tokens and `KwNot` to `token.ml`'s kind
list, each with the one-line comment style the file already uses;
add `not` to the lexer keyword table beside `and`/`or`
(`lexer.ml:144-145`) — grep the corpus and samples first to confirm
`not` is used nowhere as an identifier (the discipline every keyword
above it followed).
- [ ] Extend the operator switch: new `&` and `^` cases (single-char,
today they fall to the error branch); `*`, `/`, `%` gain a peek for
`=` (the `+`/`+=` two-char pattern at `lexer.ml:543-549` is the
template); `<` gains a peek for a second `<` (order: `=` first so
`<=` stays `LtEq`, then `<`), `>` symmetrically. No `<<=`/`>>=` —
bitwise compound assigns are out of scope per the story.
- [ ] Extend the digit scanner: after an initial `0`, a peek for `x`
or `b` switches to hex/binary accumulation (case-insensitive hex
digits; at least one digit required, else the existing malformed-
number path); `_` is skipped between digits in every integer form
(leading/trailing/doubled `_` falls out as an error naturally).
Accumulation wraps in i64 like the VM's own arithmetic (the ADD
doctrine, `wob.h:194`). The bare-decimal and float paths stay
byte-identical — the iteration-19 comment at `lexer.ml:387` explains
exactly which branch keeps them safe.
- [ ] Add dump labels for the eight new kinds beside PLUSEQ/MINUSEQ
(`dump.ml:103`).
- [ ] Verify: `just woc-test` — every existing golden unchanged (new
tokens are unreachable from old source). Commit.
## Task 2 — AST and parser: the grammar accepts what the tokens spell
**Files:** modify `compiler/src/ast.ml` (unop at :149, binop at :158),
`compiler/src/parser.ml` (ladder doc ~594, additive/multiplicative
~860-905, unary at :908, assign statement at :1489, an id-freshening
clone helper beside `subst_expr` ~2068).
**Interfaces produced:** `unop` gains `Not`; `binop` gains `BAnd`,
`BOr`, `BXor`, `Shl`, `Shr`; the assign statement accepts all five
compound forms and desugars them at parse time.
- [ ] Extend `ast.ml`: `Not` in `unop`, the five bitwise constructors
in `binop`, with a doc comment recording the Go-rung decision and
pointing at the story's settled-decisions section.
- [ ] Parser rungs: `parse_multiplicative` also accepts `Amp`/`Shl`/
`Shr`, the additive function also accepts `Pipe`/`Caret`, mapping to
the new constructors. Update the ladder doc comment (~594) to show
the new members of each rung and the two grouping consequences
(`x & mask == 0` groups the AND first; shifts bind tighter than
`+`). The declaration-position use of `Pipe` (union types) parses in
a different grammar and is untouched — confirm by the goldens.
- [ ] `parse_unary` (:908) gains `KwNot` beside `Dash`, producing
`Unary (Not, operand)` — the Lua placement the story settled:
`not a == b` groups `(not a) == b`.
- [ ] Compound assigns at the assign site (:1489): after a place
expression, `PlusEq`/`MinusEq`/`StarEq`/`SlashEq`/`PercentEq` build
the same `Ast.Assign` whose value is a `Binary` of the matching
arithmetic op over a CLONE of the target and the parsed right-hand
side. The clone must freshen every node id (`fresh_id`) — write the
small recursive helper beside `subst_expr`, which is the precedent
for rebuilding expressions. Semantics are therefore exactly the
written-out form (`x += e` ≡ `x = x + e`), including an index
expression evaluating twice — that equivalence is the documented
contract, pinned by a fixture in Task 5.
- [ ] Verify: `just woc-test` unchanged; hand-compile a scratch file
with each new form and confirm the AST dump groups per the ladder
doc. Commit.
## Task 3 — type checker: Int-only bitwise, Bool-only not, literal shift counts
**Files:** modify `compiler/src/types.ml` (the `Binary` operand
tables — the And/Or Bool-wiring near :868/:1196 and the arithmetic
table that owns `Add` are the anchors), `compiler/src/diag.ml` if the
new code needs registering.
**Interfaces produced:** `BAnd`/`BOr`/`BXor`/`Shl`/`Shr` check as
Int × Int → Int; `Not` checks as Bool → Bool; a literal shift count
outside 0..63 is a compile error.
- [ ] Wire the five bitwise ops into the same checker table `Add`
lives in, Int-only on both sides (no Float twin — nothing like
FADD exists for them, and `numeric_world` stays untouched). The
wrong-operand diagnostic is the existing operator-type family the
corpus fixture `compile-fail/lang-and-non-bool-operand` demonstrates
for and/or — same shape, Int spelled where Bool was.
- [ ] Wire `Not` as Bool → Bool through the same path `And`/`Or`
operands use.
- [ ] Literal shift counts: when a `Shl`/`Shr` right operand is an
integer literal outside 0..63, emit the iteration's one NEW
diagnostic at the operand's position — "shift count N is out of
range 0..63". Take the next free code in the WO-E2 family (grep
diag.ml/types.ml for the current highest; WO-E205 is the last known
at `types.ml:1509`) and register it wherever that family is listed.
- [ ] Verify: `just woc-test`; scratch files confirm each rejection
fires at the right position and each accepted form types as Int/Bool.
Commit.
## Task 4 — emit and runtime: five opcodes, one trap kind, `.wob` v6
**Files:** modify `runtime/src/wob.h` (opcode enum after `WOP_FLE = 41`,
trap enum after `WO_T_FK = 11` at :186, `WOB_VERSION` at :16),
`runtime/src/vm.c` (dispatch table ~1058 and the arithmetic label
block), `runtime/src/loader.c` (the per-opcode validation switch,
~534), `compiler/src/emit.ml` (the `Binary` lowering that maps `Add`
to `WOP_ADD`, and the `Unary` site), `compiler/src/disasm.ml` (opcode
names), plus the runtime's own disassembler if `runtime/src` carries
one (grep for where FADD got its name).
**Interfaces produced:** `WOP_BAND = 42`, `WOP_BOR = 43`,
`WOP_BXOR = 44`, `WOP_SHL = 45`, `WOP_SHR = 46`; trap kind
`WO_T_SHIFT = 12`; `WOB_VERSION` 5 → 6.
- [ ] Add the five opcodes to `wob.h` with the house comment style: A
B C register forms on i64; SHL/SHR document the trap contract (count
outside 0..63 traps WO_T_SHIFT) and that SHR is arithmetic
(sign-extending) per the story. Bump `WOB_VERSION` to 6 and extend
its comment with a one-line v6 entry (the v5 comment is the
template). Add `WO_T_SHIFT` to the trap enum with its message wired
wherever the other trap kinds name theirs.
- [ ] VM: five new labels in the computed-goto table (~1058) and
cases in the ISO-switch flavor (both dispatch flavors are gated —
the wovm-test comment in the justfile says why). AND/OR/XOR are
single-expression cases beside ADD; SHL/SHR range-check the count
register first and trap WO_T_SHIFT, then shift (SHR on the signed
value — C's signed right shift is arithmetic on every platform the
runtime supports, but write it via the explicit sign-preserving
idiom the codebase prefers if one exists; check how DIV guards
INT64_MIN for the local style).
- [ ] Loader: the validation switch (~534) accepts the five new
opcodes with three-register operand checking, same arm shape as ADD.
- [ ] Emit: extend the `Binary` lowering table (find where `Add`
becomes `WOP_ADD` — `Ast.` names may be opened, grep for `Concat`'s
lowering) with the five new mappings. Lower `Unary Not` with NO new
opcode: Bool is 0/1, so `not x` is the existing WOP_EQ against a
zero constant (the And/Or comment at `ast.ml:151` records the same
no-new-opcode doctrine for the short-circuit pair). Compound assigns
need nothing here — they died in the parser.
- [ ] Disasm: names for the five opcodes in `compiler/src/disasm.ml`
and the runtime twin if it exists.
- [ ] Verify: `just woc-build && just wovm-build`, then `just
woc-test` and `just wovm-test` (the ASan+UBSan gate, both dispatch
flavors). A scratch program exercising every operator, a
variable-count in-range shift, and a caught out-of-range shift (try/
catch over the trap) prints the expected values. Commit.
## Task 5 — corpus, gates, docs: pin everything, close out
**Files:** create fixtures under `tests/corpus/run/`,
`tests/corpus/compile-fail/`, `tests/corpus/trap/`; modify
`docs/plan/oop-vm/00-wob-format.md` (v6 section),
`compiler/src/CODE-LOGIC.md` and `runtime/src/CODE-LOGIC.md`
(reasoning-under-the-code), the story file (move refine → done, both
frontmatter and folder in the same change), `docs/stories/00-status.md`
(standup entry).
- [ ] Run fixtures (the `run/arithmetic` fixture is the shape
template): one covering all five bitwise operators including the two
grouping pins (`x & mask == 0`, a shift mixed with `+`); one for
hex/binary/underscore literals proving value-identity with decimal
twins; one for `not` (plain, chained with and/or, and the
`not a == b` grouping pin); one for compound assigns on a let, a
field, and an index place, including the desugar-equivalence pin (an
index expression with a visible side effect running twice, exactly
as the written-out form would).
- [ ] The consumer proof: a fixture computing HMAC's ipad/opad XOR
step in pure `.wo` — `byte_at` over a key Text, `^` with the 0x36
and 0x5c pad constants — matching the RFC 2104 test-vector
intermediate. This is the story's acceptance criterion that
iteration 34 is demonstrably unblocked.
- [ ] Compile-fail fixtures: `not` on an Int operand; a bitwise
operator on Bool and on Float; a literal shift count of 64 and of a
negative literal (the new diagnostic, both sides of the range).
- [ ] Trap fixture: a variable-count shift receiving 64 at run time
traps WO_T_SHIFT — uncaught surface first, then a try/catch arm
reading the error code, whichever shape `tests/corpus/trap/`'s
existing fixtures use.
- [ ] Dead-token defect closure: confirm the pre-change WO-E101 on
`x += 1` is gone by the compound-assign run fixture existing at all;
note the closure in the story's landing blockquote.
- [ ] Gates, all of them: `just woc-test`, `just wovm-test`,
`just oop-accept`, `just web-app`, `just deps-accept` — every count
at or above its story-recorded level, zero failures.
- [ ] Docs closeout: v6 section in `00-wob-format.md` (five opcodes,
trap kind, version gate — the v5 section is the template);
CODE-LOGIC.md on both sides for the decisions that live in code
(arithmetic SHR, trap-not-mask, `not` as EQ-zero, parse-time
compound-assign desugar and its double-eval contract); story file
set to `status: done` with a landing blockquote in the
story-15/16 voice; standup entry in `00-status.md` answering the six
questions (reference project: `.dev/reference/go`).
- [ ] Commit.
## Self-review notes
- Story coverage: goals 1-5 map to Tasks 1-4; every acceptance
criterion has a Task-5 fixture or gate; the settled decisions are
restated at their implementation sites so no executor re-litigates
them.
- Type consistency: token names (Amp/Caret/Shl/Shr/StarEq/SlashEq/
PercentEq/KwNot), AST names (Not, BAnd/BOr/BXor/Shl/Shr), opcode
names/ids (WOP_BAND..WOP_SHR = 42..46), and the trap kind
(WO_T_SHIFT = 12) are spelled identically in every task that touches
them.
- Known allowance: Task 3's diagnostic code is "next free in the
WO-E2 family" rather than a fixed number — the family's registry is
the source of truth and hard-coding here would rot.