feat: iteration 36 task 5 — operators sample, docs closeout
- docs/examples/operators: manual-test workload (ok/FAIL lines per expression; trap mode proves WO_T_SHIFT); NO test fixtures by developer directive — acceptance is the manual pass - 00-wob-format.md: v6 section (opcodes 42-46, T_SHIFT, header v6) - CODE-LOGIC.md both sides: precedence-into-existing-rungs, Lua not, rewind-and-reparse compound assigns, trap-not-mask, 63-bit hex limit - story 36 refine -> in-progress with landing blockquote; board updated - gates: woc-test 543/0, wovm-test ASan, oop-accept ALL MET, deps-accept 8/0, web-app 26/0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c2c9b240f1
commit
ed6bfeea3d
8 changed files with 229 additions and 4 deletions
|
|
@ -260,3 +260,38 @@ a keyword, and visibility is name resolution at compile time.
|
|||
- **Exit-code bands stay split**: WO-E108 is a diagnostic through the normal
|
||||
collector path (exit 1); WO-E106/E107/E109 are manifest errors printed
|
||||
directly (exit 2).
|
||||
|
||||
## Operator parity (iteration 36 — `.wob` v6)
|
||||
|
||||
- **Precedence went INTO existing rungs, not new ones.** `|`/`^` joined
|
||||
`parse_additive`, `&`/`<<`/`>>` joined `parse_multiplicative` — exactly
|
||||
Go's table (`token.go` Precedence), which exists to fix C's trap:
|
||||
`x & mask == 0` groups the AND first here. The ladder doc in `parser.ml`
|
||||
carries the worked examples.
|
||||
- **`not` is a keyword at the unary level (Lua placement).** `not a == b`
|
||||
groups `(not a) == b`. Chosen over Python's looser placement because the
|
||||
grammar's ordering is already anchored to Lua by name and because
|
||||
Bool-only typing turns almost every misread into a compile error. It
|
||||
lowers on the existing EQ against a zero constant — no new opcode, the
|
||||
same doctrine as and/or's JZ lowering.
|
||||
- **Compound assigns are parse-time sugar via rewind-and-reparse.**
|
||||
`x += e` IS `x = x + e`, the documented contract — including an index
|
||||
expression evaluating twice, exactly as the written-out form would. The
|
||||
parser re-parses the place by resetting `st.pos` (no expression rung
|
||||
consumes a compound token, so the second parse stops where the first
|
||||
did); every re-parsed node draws a fresh id, so owner/emit see two
|
||||
honest reads, never one node in two roles. `+=`/`-=` had been lexed
|
||||
since haxe-parity Task 2 but no rule consumed them — dead tokens,
|
||||
`x += 1` died as a generic WO-E101 until this iteration.
|
||||
- **Bitwise is Int-only on BOTH sides (WO-E201 family)** — no F-twin
|
||||
exists, so a Float operand would have become a garbage word operation
|
||||
with no diagnostic. A LITERAL shift count outside 0..63 is WO-E223 at
|
||||
the operand's position (a negative literal arrives as
|
||||
`Unary(Neg, IntLit)` — both shapes are caught); a variable count is the
|
||||
VM's WO_T_SHIFT.
|
||||
- **Hex/binary literals accumulate in OCaml's native int (63-bit).** A
|
||||
full-width 64-bit literal like `0xFFFFFFFFFFFFFFFF` is out of reach —
|
||||
all-ones is spelled `-1` (and complement is `-1 ^ x`; there is no `~`).
|
||||
The `0x`/`0b` prefix commits only when a real base digit follows, so
|
||||
`0xg` stays `Int 0` + `Ident` — a parse error at its own position, no
|
||||
new lexer diagnostic. `_` separators are consumed only BETWEEN digits.
|
||||
|
|
|
|||
|
|
@ -193,7 +193,8 @@ that sequences its tasks. Read one, approve, then the next starts.
|
|||
|
||||
| Track | Item | Where |
|
||||
| -------- | --------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| Language | nothing active — the framework v1-polish slice landed 2026-08-20 (branch framework-v1, awaiting merge); next per the order: brainstorm 20/21's forks | [order](#implementation-order-re-sequenced-2026-08-20--code-review-pass) |
|
||||
| Language | 🔄 [iteration 36 — operator parity](stories/language-runtime-database/in-progress/36-operator-parity.md): `not`, bitwise `& \| ^ << >>`, hex/binary/`_` literals, compound assigns — CODE LANDED 2026-08-22 (branch operator-parity, `.wob` v6, all gates green; reference project `.dev/reference/go` drove the design). Awaiting the developer's MANUAL pass on `docs/examples/operators/` (no test fixtures by directive); unblocks story 34's pure-`.wo` HMAC question | [plan](superpowers/plans/2026-08-22-operator-parity.md) |
|
||||
| Language | the framework v1-polish slice landed 2026-08-20 (branch framework-v1, awaiting merge); next per the order: brainstorm 20/21's forks | [order](#implementation-order-re-sequenced-2026-08-20--code-review-pass) |
|
||||
|
||||
Off-goal work is parked; the goal (2026-08-20) is the web framework as a
|
||||
polished micro-framework v1 — iteration 17 (library kind + `internal/`) is
|
||||
|
|
|
|||
42
docs/examples/operators/README.md
Normal file
42
docs/examples/operators/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# operators — iteration 36's manual-test workload
|
||||
|
||||
Exercises everything iteration 36 added to the language: boolean `not`,
|
||||
the five Int bitwise operators (`&` `|` `^` `<<` `>>`), hex/binary/
|
||||
underscore integer literals, and the five compound assigns
|
||||
(`+=` `-=` `*=` `/=` `%=`).
|
||||
|
||||
## Run it
|
||||
|
||||
```
|
||||
woc . # from this directory
|
||||
./target/operators
|
||||
```
|
||||
|
||||
Every line prints `ok <label> = <value>` — each label states the exact
|
||||
expression and the expected result, so a manual test is reading the
|
||||
lines and spotting any `FAIL`. The sections, in order:
|
||||
|
||||
1. **Literals** — `0xFF`, `0b1010_1010`, `1_000_000` against their
|
||||
decimal twins.
|
||||
2. **The five operators** — including `-16 >> 2 = -4` (`>>` is
|
||||
ARITHMETIC: the sign bit extends, the story's settled decision 1)
|
||||
and complement spelled `-1 ^ x` (no `~` in this language).
|
||||
3. **Precedence pins** — Go's C-trap fix: `&`/`<<`/`>>` bind with
|
||||
`*`, `|`/`^` bind with `+`, all above comparison, so
|
||||
`x & mask == 0` groups the AND first and `1 << 4 + 1` is 17, not 32.
|
||||
4. **`not`** — a word like `and`/`or`, Lua's unary placement:
|
||||
`not a == b` groups `(not a) == b`.
|
||||
5. **Compound assigns** — one value threaded through all five.
|
||||
6. **The consumer proof** — HMAC's ipad/opad step (story 34's blocker):
|
||||
`byte_at(key, i) ^ 0x36` / `^ 0x5C` in pure `.wo`.
|
||||
|
||||
## The trap
|
||||
|
||||
```
|
||||
./target/operators trap
|
||||
```
|
||||
|
||||
must die with `trap 12 in shift_by ...: shift count out of range 0..63`
|
||||
(exit 1). A shift count outside 0..63 is a RUN-TIME trap only when the
|
||||
count is a variable — a literal out-of-range count never compiles
|
||||
(WO-E223, try changing `1 << 6` to `1 << 64`).
|
||||
74
docs/examples/operators/main.wo
Normal file
74
docs/examples/operators/main.wo
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
-- Operator parity — iteration 36's manual-test workload. Every section
|
||||
-- prints `label expected actual`: eyeball that the two numbers agree.
|
||||
-- Build & run: woc . && ./target/operators
|
||||
-- Trap demo: ./target/operators trap (shift count 64 at run time
|
||||
-- must die with `trap 12 ... shift count out of range`)
|
||||
|
||||
fn expect(label: Text, expected: Int, actual: Int) {
|
||||
if expected == actual {
|
||||
print("ok ${label} = ${actual}")
|
||||
} else {
|
||||
print("FAIL ${label} expected ${expected} got ${actual}")
|
||||
}
|
||||
}
|
||||
|
||||
fn shift_by(n: Int) -> Int {
|
||||
-- variable count: the compiler cannot see 64 here, so an out-of-range
|
||||
-- value reaches the VM and traps WO_T_SHIFT (the DIV0 precedent)
|
||||
return 1 << n
|
||||
}
|
||||
|
||||
fn main(args: multi Text) -> Int {
|
||||
if len(args) >= 1 and args[0] == "trap" {
|
||||
print("about to shift by 64 — expect trap 12, not a number:")
|
||||
print_int(shift_by(64))
|
||||
return 1
|
||||
}
|
||||
|
||||
-- literals: hex, binary, underscore separators
|
||||
expect("0xFF", 255, 0xFF)
|
||||
expect("0b1010_1010", 170, 0b1010_1010)
|
||||
expect("1_000_000", 1000000, 1_000_000)
|
||||
|
||||
-- the five bitwise operators, Int-only
|
||||
expect("12 & 10", 8, 12 & 10)
|
||||
expect("12 | 3", 15, 12 | 3)
|
||||
expect("12 ^ 10", 6, 12 ^ 10)
|
||||
expect("1 << 6", 64, 1 << 6)
|
||||
expect("-16 >> 2 (arithmetic)", -4, -16 >> 2)
|
||||
expect("complement -1 ^ 0xF0", -241, -1 ^ 0xF0)
|
||||
|
||||
-- precedence: Go's C-trap fix — & << >> bind with * / %, | ^ with + -
|
||||
expect("1 << 4 + 1 groups (1<<4)+1", 17, 1 << 4 + 1)
|
||||
expect("2 | 1 * 4 groups 2|(1*4)", 6, 2 | 1 * 4)
|
||||
if 0b1010_1010 & 0xFF == 0 {
|
||||
print("FAIL grouping: & lost to ==")
|
||||
} else {
|
||||
print("ok x & mask == 0 groups (x & mask) == 0")
|
||||
}
|
||||
|
||||
-- not: boolean negation, a word like and/or
|
||||
if not false { print("ok not false") }
|
||||
if not (1 > 2) and not false or false { print("ok not chains with and/or") }
|
||||
-- Lua placement: not binds tighter than ==
|
||||
if (not true) == false { print("ok not a == b groups (not a) == b") }
|
||||
|
||||
-- compound assigns (+= -= existed as dead tokens; all five live now)
|
||||
let acc: Int = 10
|
||||
acc += 5
|
||||
acc -= 1
|
||||
acc *= 3
|
||||
acc /= 2
|
||||
acc %= 7
|
||||
expect("10 +=5 -=1 *=3 /=2 %=7", 0, acc)
|
||||
|
||||
-- the consumer that motivated the iteration (story 34): HMAC's
|
||||
-- ipad/opad step is byte ^ constant — RFC 2104's pads, on the byte
|
||||
-- values of "key"
|
||||
let k: Text = "key"
|
||||
expect("'k' ^ 0x36 (ipad)", 93, byte_at(k, 0) ^ 0x36)
|
||||
expect("'k' ^ 0x5c (opad)", 55, byte_at(k, 0) ^ 0x5C)
|
||||
|
||||
print("done — compare every line above by eye; any FAIL is a defect")
|
||||
return 0
|
||||
}
|
||||
9
docs/examples/operators/wo.toml
Normal file
9
docs/examples/operators/wo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
name = "operators"
|
||||
version = "0.1.0"
|
||||
description = "Operator-parity showcase — iteration 36's manual-test workload: not, bitwise & | ^ << >>, hex/binary literals, compound assigns"
|
||||
|
||||
[runtime]
|
||||
wo = ">= 0.1"
|
||||
|
||||
[build]
|
||||
runtime = "../../../runtime/wovm"
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
All integers little-endian; offsets are absolute file offsets.
|
||||
|
||||
**Header (44 bytes):** magic `"WOB1"`, version 5 (iteration 19; see "v5: Float and Bytes" below), then offset/count u32 pairs for the constant pool, class table, interface section, and method table, then a u32 entry-method index (all-ones = none).
|
||||
**Header (44 bytes):** magic `"WOB1"`, version 6 (iteration 36; see "v6: the Int bitwise set" below — v5 was iteration 19's "v5: Float and Bytes"), then offset/count u32 pairs for the constant pool, class table, interface section, and method table, then a u32 entry-method index (all-ones = none).
|
||||
|
||||
**Constant pool** — sequential entries: one tag byte; tag 0 = i64 follows; tag 1 = text (u32 length + bytes, no NUL); tag 2 = f64 as its IEEE 754 bit pattern in an LE u64 (v5). There is no Bytes tag: Bytes has no literal form.
|
||||
|
||||
|
|
@ -55,6 +55,7 @@ The metadata exists for exactly one reason: `json.encode`/`json.decode` are runt
|
|||
| 33 | ENDTRY | pop the innermost catch frame — the try region completed without trapping |
|
||||
| 34–38 | FADD/FSUB/FMUL/FDIV/FNEG | f64 arithmetic on the register's bits (v5). **None of these trap**: IEEE 754 quiet semantics, so `x/0.0` is ±Inf and `0.0/0.0` is NaN. FNEG flips the sign bit, so `-0.0` is reachable |
|
||||
| 39–41 | FEQ/FLT/FLE | f64 IEEE compares, result 0/1 — so any comparison involving NaN is 0, and `0.0 == -0.0` is 1. Not a total order; indexes and order-by use the `float_cmp` builtin instead |
|
||||
| 42–46 | BAND/BOR/BXOR/SHL/SHR | i64 bitwise (v6, iteration 36). Int-only — woc refuses Float/Bool/Text operands, so no F-twin exists. SHL shifts the unsigned word (wrapping, like ADD); **SHR is arithmetic** (the sign bit extends). A shift count outside 0..63 traps T_SHIFT — never the hardware's silent count%64; a *literal* out-of-range count is rejected at compile time (WO-E223), so the trap only ever fires on variable counts |
|
||||
|
||||
**try/catch (Task 5).** A trap raised while a catch frame is live unwinds every frame *above* the catching one exactly as an uncaught trap does (drop maps run, registers null), then releases what the try region owned in the catching frame — the difference between the drop entry at the trapping instruction and the one at the handler pc — and resumes at the handler instead of leaving the VM. A frame that returns takes its still-open catch frames with it, so a `return` out of a try region cannot leave a handler pointing at a dead window. With no catch frame live, a trap behaves byte-for-byte as it did before v2. The catch arm's error record is an ordinary compiler-allocated object filled by the `err_fill` builtin (field order: 0 code, 1 line, 2 method, 3 msg).
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ The metadata exists for exactly one reason: `json.encode`/`json.decode` are runt
|
|||
|
||||
`EQS` accepts a nil operand for the same reason: two `?Text` values compare with it, and the answer is "both absent is equal, one absent is not". A non-nil operand must still be a real Text.
|
||||
|
||||
**Trap codes:** DIV0, BORROW, STACK, OOM, DB, BOUNDS, KEY, EXPLICIT, IO (a syscall the source cannot prevent said no — errno's message rides in the error record).
|
||||
**Trap codes:** DIV0, BORROW, STACK, OOM, DB, BOUNDS, KEY, EXPLICIT, IO (a syscall the source cannot prevent said no — errno's message rides in the error record), UNIQUE, FK, SHIFT (v6 — a variable shift count outside 0..63).
|
||||
|
||||
## v5: Float and Bytes (iteration 19)
|
||||
|
||||
|
|
@ -138,6 +139,34 @@ fraction there still fails the decode whole). A non-finite Float encodes as
|
|||
invalid JSON. A Bytes field crosses as a base64 string, matching
|
||||
`base64_encode`'s alphabet exactly.
|
||||
|
||||
## v6: the Int bitwise set (iteration 36)
|
||||
|
||||
The version bump is the same contract v5 set: an image is rejected in both
|
||||
age directions, because an older runtime meeting opcode 42 would bail on
|
||||
"unknown opcode" only after the loader trusted the rest of the header.
|
||||
|
||||
**What v6 adds** — nothing but opcodes and one trap kind: no new constant
|
||||
tag, field kind, or section.
|
||||
|
||||
- Opcodes `42–46` — `BAND`/`BOR`/`BXOR`/`SHL`/`SHR`, three-register i64
|
||||
forms (table above). Int-only by the checker, so unlike v5 there is no
|
||||
parallel F-set and no mode bit.
|
||||
- Trap kind `T_SHIFT` (12) — a variable shift count outside 0..63. The
|
||||
DIV0 precedent, deliberately NOT x86's silent count-mod-64 (`x << 64`
|
||||
must never quietly equal `x`) and NOT Go's saturate-to-0/-1 (spec
|
||||
surface serving generic-width code this language does not have).
|
||||
A literal count is rejected at compile time as WO-E223, so the trap
|
||||
is reachable only through a count computed at run time.
|
||||
- `>>` is **arithmetic** — the sign bit extends, Go's own choice for a
|
||||
signed integer, and this language's one Int is signed 64-bit. Byte-mask
|
||||
code (crypto, base64) never notices: its values keep the sign bit clear,
|
||||
where arithmetic and logical shifts agree bit for bit.
|
||||
- Source-side companions that need no format space: hex/binary/underscore
|
||||
Int literals (a literal is pool bits by the time it reaches the image),
|
||||
boolean `not` (lowered on the existing EQ against a zero constant, the
|
||||
same no-new-opcode doctrine as and/or's JZ lowering), and the compound
|
||||
assigns (parse-time sugar — `x += e` IS `x = x + e`, dead by emit time).
|
||||
|
||||
## Enum payload variants (haxe-parity compiler Task 4)
|
||||
|
||||
`.wob` v1 is unchanged — no new section, no new header field, no version
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
iteration: "36"
|
||||
status: refine
|
||||
status: in-progress
|
||||
---
|
||||
|
||||
# Iteration 36 — operator parity: `not`, bitwise, hex literals, compound assigns
|
||||
|
|
@ -30,6 +30,22 @@ status: refine
|
|||
> (5 tasks: lexer/tokens; AST/parser incl. compound-assign desugar;
|
||||
> type checker incl. literal shift-count rejection; emit + VM opcodes
|
||||
> `.wob` v6; corpus/gates/docs closeout).
|
||||
>
|
||||
> **CODE LANDED 2026-08-22** (branch `operator-parity`), full stack in
|
||||
> one day: tokens/lexer (incl. hex `0x` / binary `0b` / `_` separators),
|
||||
> Go-rung parsing, WO-E201 Int-only/Bool-only checking, WO-E223 literal
|
||||
> shift rejection, opcodes `WOP_BAND..WOP_SHR` 42-46 + `WO_T_SHIFT` as
|
||||
> `.wob` v6. The dead-token defect is closed: `x += 1` parses (all five
|
||||
> compound assigns, parse-time sugar — the desugar-equivalence contract
|
||||
> incl. double index-eval is in `compiler/src/CODE-LOGIC.md`). Gates:
|
||||
> `woc-test` 543/0, `wovm-test` ASan both flavors, `oop-accept` ALL MET,
|
||||
> `deps-accept` 8/0, `web-app` 26/0 — all unchanged. **Deviation by
|
||||
> developer directive: NO test fixtures were written** — acceptance is
|
||||
> MANUAL via the new sample `docs/examples/operators/` (`woc .`, run,
|
||||
> read the ok/FAIL lines; `trap` mode proves WO_T_SHIFT). The corpus
|
||||
> pins this plan called for (grouping, arithmetic `>>`, ipad/opad,
|
||||
> trap) live in that sample instead. Status stays in-progress until the
|
||||
> developer's manual pass; the AC below reads as written pre-deviation.
|
||||
|
||||
## Why this iteration exists
|
||||
|
||||
|
|
@ -181,3 +181,22 @@ layout.
|
|||
word and a Bytes as the same length-prefixed blob a Text uses, so replay is
|
||||
bit-exact for NaN, ±Inf, and `-0.0`. `test_wal`'s `test_float_bytes_replay`
|
||||
asserts on bits for exactly that reason.
|
||||
|
||||
## The Int bitwise set (iteration 36 — `.wob` v6)
|
||||
|
||||
- **One shared case-body text serves both dispatch flavors** — the new
|
||||
CASE blocks sit in the Int neighborhood after LE, so `-DWO_ISO_C`
|
||||
cannot rot (same discipline as every opcode before them).
|
||||
- **SHL shifts the unsigned register word** (wrapping, like ADD — a
|
||||
signed left-shift overflow would be UB); **SHR casts to int64_t
|
||||
first**, so it is ARITHMETIC — gcc/clang define signed `>>` as
|
||||
sign-extending, and those are the only compilers this runtime targets.
|
||||
- **The count check is a trap, not a mask.** x86 masks the count mod 64,
|
||||
which would make `x << 64 == x` silently; Go saturates to 0/-1, spec
|
||||
surface for generic-width code this VM does not have. WO_T_SHIFT (12)
|
||||
follows the DIV0 precedent instead: named, catchable, honest. It can
|
||||
only fire on a count computed at run time — woc rejects literal
|
||||
out-of-range counts as WO-E223.
|
||||
- **The loader validates the five opcodes as plain three-register forms**
|
||||
— the count is a register, not an immediate, so there is nothing to
|
||||
range-check at load time.
|
||||
|
|
|
|||
Loading…
Reference in a new issue