diff --git a/compiler/src/parser.ml b/compiler/src/parser.ml index bfcde81..1974441 100644 --- a/compiler/src/parser.ml +++ b/compiler/src/parser.ml @@ -127,6 +127,29 @@ let fail (st : state) (site : Ast.pos) (code : string) (message : string) : 'a = let syntax_code = Diag.parsing_prefix ^ "01" (* WO-E101: generic syntax error *) let table_code = Diag.parsing_prefix ^ "02" (* WO-E102: invalid @table(...) configuration *) let gc_removed_code = Diag.parsing_prefix ^ "04" (* WO-E104: `@gc` — GC-ness is inferred *) +let doctrine_reject_code = Diag.parsing_prefix ^ "05" (* WO-E105: rejected Haxe keyword *) + +(* The systems-track spec's reject rows (Part 1 verdict table), each with its + doctrine reason — these words never parse as ordinary identifiers, so a + Haxe habit fails loudly at its own position instead of misparsing into a + generic syntax error (or, worst, compiling clean: `super.f()` used to). *) +let doctrine_reject_reason (w : string) : string option = + match w with + | "extends" | "implements" | "super" | "override" -> + Some "no inheritance, ever — is-a is a tagged union, has-a is composition, polymorphism is structural interfaces (principle 4)" + | "cast" -> + Some "no unsafe casts — conversions are typed; `as` exists only in the json.decode target position" + | "Dynamic" | "untyped" -> + Some "static typing all the way to the register — typed `json.decode … as T -> ?T` covers the real use (principle 13)" + | "macro" -> Some "macros kill the fast-compile promise — codegen belongs to tooling" + | "extern" -> Some "one FFI hole voids the whole memory-safety story — capabilities are audited typed builtins (principle 10)" + | "operator" -> Some "one name, one signature — no operator overloading" + | _ -> None + +let reject_doctrine_word (st : state) (pos : Ast.pos) (w : string) (reason : string) : unit = + Diag.Collector.add st.collector + (Diag.error ~code:doctrine_reject_code ~file:st.file ~line:pos.Ast.line ~col:pos.Ast.col + ~message:(Printf.sprintf "`%s` is rejected: %s" w reason) ()) (* haxe-parity Task 2: the haxe keyword verdict table's `inline` row — "adopt (values): const compile-time values; inline *functions* @@ -1130,6 +1153,16 @@ and parse_query_expr (st : state) : Ast.expr = and parse_primary (st : state) : Ast.expr = match peek st with + | Token.Ident w when Option.is_some (doctrine_reject_reason w) -> + let pos = peek_pos st in + let id = fresh_id st in + (match doctrine_reject_reason w with + | Some r -> reject_doctrine_word st pos w r + | None -> ()); + ignore (advance st); + (* recovery value so downstream parsing continues; the error above is + already fatal to the compile *) + { Ast.id; pos; kind = Ast.IntLit 0 } | _ when is_query_trigger st -> parse_query_expr st | Token.Ident "delete" when (match (tok_at st (st.pos + 1)).kind with | Token.Newline | Token.Semicolon | Token.Eof -> false | _ -> true) -> @@ -1591,6 +1624,18 @@ let parse_class_or_type ?(pub = false) (st : state) (ann : type_annotations) : A let is_class = peek st = Token.KwClass in if is_class then ignore (advance st) else expect st Token.KwType "`type` or `class`"; let name = expect_ident st "type/class name" in + (* reject rows at their most habitual site: `class B extends A` *) + (match peek st with + | Token.Ident (("extends" | "implements") as w) -> + let wpos = peek_pos st in + (match doctrine_reject_reason w with + | Some r -> reject_doctrine_word st wpos w r + | None -> ()); + (* recovery: skip to the '{' so the body still parses *) + while (match peek st with Token.LBrace | Token.Eof -> false | _ -> true) do + ignore (advance st) + done + | _ -> ()); expect st Token.LBrace "'{'"; let id = fresh_id st in let fields = ref [] in @@ -1873,6 +1918,14 @@ let parse_program (st : state) : Ast.program = | Token.KwType | Token.KwClass -> decls := Ast.Class (parse_class_or_type st ann) :: !decls | _ -> unexpected st "`type` or `class` after annotation") + | Token.Ident w when Option.is_some (doctrine_reject_reason w) -> + (* `macro fn …` / `extern fn …` at top level: the doctrine + reason, not a generic syntax error *) + let wpos = peek_pos st in + (match doctrine_reject_reason w with + | Some r -> reject_doctrine_word st wpos w r + | None -> ()); + raise Parse_error | _ -> unexpected st "a top-level declaration (type/class/interface/fn/@annotation)" with Parse_error -> sync_to_next_top_level st) end diff --git a/compiler/src/types.ml b/compiler/src/types.ml index 8d43ddf..94931f4 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -614,6 +614,14 @@ let rec scalar_name_of (ft : field_ty) : string option = class declared later in the same file is not a false positive. *) let check_field_types ~file (syms : symbols) (collector : Diag.Collector.t) (prog : program) : unit = + let unknown_type_msg (name : string) : string = + match name with + | "Dynamic" | "untyped" -> + Printf.sprintf + "`%s` is rejected: static typing all the way to the register — typed `json.decode … as T -> ?T` covers the real use (principle 13)" + name + | _ -> Printf.sprintf "unknown type `%s`" name + in List.iter (function | Ast.Class c -> List.iter (fun (f : Ast.field) -> @@ -622,7 +630,7 @@ let check_field_types ~file (syms : symbols) (collector : Diag.Collector.t) Diag.Collector.add collector (Diag.error ~code:unknown_type_name_code ~file ~line:f.pos.line ~col:f.pos.col - ~message:(Printf.sprintf "unknown type `%s`" name) ()) + ~message:(unknown_type_msg name) ()) | _ -> () ) c.fields | Ast.Union u -> @@ -636,7 +644,7 @@ let check_field_types ~file (syms : symbols) (collector : Diag.Collector.t) Diag.Collector.add collector (Diag.error ~code:unknown_type_name_code ~file ~line:v.v_pos.line ~col:v.v_pos.col - ~message:(Printf.sprintf "unknown type `%s`" name) ()) + ~message:(unknown_type_msg name) ()) | _ -> () ) v.v_fields ) u.variants diff --git a/docs/00-status.md b/docs/00-status.md index 389d1d1..a62a671 100644 --- a/docs/00-status.md +++ b/docs/00-status.md @@ -83,8 +83,9 @@ Story slice: [`docs/stories/language-runtime-database/07-logwatcher-proof.md`](s - Iteration 5's *strictness* half — **`?T` forced handling landed 2026-08-18** (WO-E211/212/213 + local narrowing; the samples were updated to the - bind-then-narrow idiom and stay green). Still open: `pub(read)` write - enforcement, `using`, `#if`, reject rows. Plan 8 stays open for those. + bind-then-narrow idiom and stay green), **reject rows landed 2026-08-18** + (WO-E105 doctrine diagnostics — `super.f()` used to compile clean). Still + open: `pub(read)` write enforcement, `using`, `#if`. Plan 8 stays open. - Everything `@gc`: iteration 7b, `set`'s `@gc` retention gap, iteration 4's `gc/held-cycle` leak. The sample declares **no `@gc` class** — 35 classes, none with the gc flag, 0 `RC_INC`/`RC_DEC` against 78 `DROP`s — so none of it @@ -112,7 +113,7 @@ that sequences its tasks. Read one, approve, then the next starts. | 2 | [VM core (`wovm`)](stories/language-runtime-database/02-vm-core.md) | ✅ | | 3 | [Compiler front (`woc`)](stories/language-runtime-database/03-compiler-front.md) | ✅ (known gaps below) | | 4 | [Single binary end-to-end](stories/language-runtime-database/04-single-binary-e2e.md) | ✅ (known gaps below) | -| 5 | [Language surface](stories/language-runtime-database/05-language-surface.md) | 🔄 grammar done; **`?T` forced handling ✅ 2026-08-18** (WO-E211/212/213 + narrowing); `pub(read)`/`using`/`#if`/reject rows still ⏸ | +| 5 | [Language surface](stories/language-runtime-database/05-language-surface.md) | 🔄 grammar done; **`?T` forced handling ✅ + reject rows ✅ + WO-E205 ✅ (2026-08-18)**; `pub(read)`/`using`/`#if` still ⏸ | | 6 | [Program mode + stdlib](stories/language-runtime-database/06-program-mode-stdlib.md) | ✅ (the surface log-watcher uses) | | 7 | [log-watcher proof](stories/language-runtime-database/07-logwatcher-proof.md) | 🔄 **runs; executable in progress** | | 7b | [Inferred GC + mark-sweep](stories/language-runtime-database/07b-inferred-gc-mark-sweep.md) | ✅ **landed 2026-08-18** — `@gc` gone (WO-E104), GC-ness inferred, RC replaced by incremental mark-sweep, `.wob` v4; supersedes iteration 2's RC memory model | diff --git a/docs/plan/compiler/2026-08-01-haxe-parity-language.md b/docs/plan/compiler/2026-08-01-haxe-parity-language.md index 6cf83a9..78cae74 100644 --- a/docs/plan/compiler/2026-08-01-haxe-parity-language.md +++ b/docs/plan/compiler/2026-08-01-haxe-parity-language.md @@ -1,6 +1,6 @@ # Haxe-Parity Language Adoptions Implementation Plan -> **Status: ⏸ on hold behind [the executable plan](2026-08-14-logwatcher-executable.md)** (story iteration 5) — the grammar half of this plan is what let the driving workload compile, and the rest of it makes the language *refuse* more rather than making that program *run*, so it waits. Original status follows. — Tasks 1–4 ✅ shipped and review-verified: modules (`use`/`pub`), language surface (`and`/`or`, `${}` interpolation, `const`, break/continue, do-while), switch expressions, typedef records + enum payload variants. Task 5 (try/catch over the trap system) ✅ shipped 2026-08-14 — VM catch frames, expression and block catch arms, the `{code, line, method, msg}` record. Task 6 (`?T`) ✅ complete 2026-08-18 (branch `nullable-enforcement`): forced handling enforced — WO-E211/E212/E213 emit, locals narrow via `!= nil` guards / diverging early-return / `and`-chains / `while`; field places bind to a local first. Boxed scalar cells were superseded by `WO_NIL_SCALAR` before this task ran. Task 7 🔶 half: `static` members and `pub(read)` syntax landed, `using` and the `pub(read)` write check did not. Task 8 (`#if` + reject rows) ⬜. Board: [00-status.md](../../00-status.md) +> **Status: ⏸ on hold behind [the executable plan](2026-08-14-logwatcher-executable.md)** (story iteration 5) — the grammar half of this plan is what let the driving workload compile, and the rest of it makes the language *refuse* more rather than making that program *run*, so it waits. Original status follows. — Tasks 1–4 ✅ shipped and review-verified: modules (`use`/`pub`), language surface (`and`/`or`, `${}` interpolation, `const`, break/continue, do-while), switch expressions, typedef records + enum payload variants. Task 5 (try/catch over the trap system) ✅ shipped 2026-08-14 — VM catch frames, expression and block catch arms, the `{code, line, method, msg}` record. Task 6 (`?T`) ✅ complete 2026-08-18 (branch `nullable-enforcement`): forced handling enforced — WO-E211/E212/E213 emit, locals narrow via `!= nil` guards / diverging early-return / `and`-chains / `while`; field places bind to a local first. Boxed scalar cells were superseded by `WO_NIL_SCALAR` before this task ran. Task 7 🔶 half: `static` members and `pub(read)` syntax landed, `using` and the `pub(read)` write check did not. Task 8: reject rows ✅ 2026-08-18 (WO-E105 doctrine diagnostics at class headers, expression heads, top-level heads; `Dynamic`/`untyped` type names via WO-E225's doctrine message; corpus `compile-fail/reject-*`); `#if` build flags still ⬜. Board: [00-status.md](../../00-status.md) > **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. > diff --git a/docs/plan/oop-vm/01-error-catalog.md b/docs/plan/oop-vm/01-error-catalog.md index eb1e1c5..851dbd0 100644 --- a/docs/plan/oop-vm/01-error-catalog.md +++ b/docs/plan/oop-vm/01-error-catalog.md @@ -34,6 +34,7 @@ half of the story ("moved here" / "borrowed here" / etc.). | WO-E101 | generic syntax error: an unexpected token where the grammar expected something else, including running off the end of the file inside an unclosed block/type/interface body. Declaration-level recovery syncs to the next top-level keyword so one bad declaration yields one diagnostic, not a cascade. | `expected ')' or ',', got NEWLINE` | | WO-E102 | an invalid `@table(...)` configuration: `name` given twice, an `index` with no columns, or an argument key other than `name`/`index`. | `@table(name: ...) given twice` | | WO-E103 | haxe-parity Task 2. `inline fn ...` — the haxe keyword verdict table's own reject half of the `inline` row (`const` values are the adopted half). The whole declaration is discarded by the usual top-level recovery, same as any other bad declaration. | `` `inline fn` is rejected — optimization is the compiler's job `` | +| WO-E105 | iteration 5 strictness (2026-08-18). A rejected Haxe keyword used where it would otherwise misparse — or, worst, compile clean (`return super.f()` used to): `extends`/`implements` after a class name, and `extends`/`implements`/`super`/`override`/`cast`/`Dynamic`/`untyped`/`macro`/`extern`/`operator` as an expression head or a top-level declaration head. Each cites the systems-track verdict table's doctrine reason. (`Dynamic`/`untyped` as a *type name* fire WO-E225 with the same doctrine message.) | `` `extends` is rejected: no inheritance, ever — is-a is a tagged union, has-a is composition, polymorphism is structural interfaces (principle 4) `` | | WO-E104 | iteration 7b. `@gc` on a class — the annotation is gone: GC-ness is inferred (structural cycles + demand promotion; `woc --dump-gc`). The annotation is skipped for recovery and the class classifies by inference. | `` `@gc` is not a valid annotation: GC-ness is inferred by the compiler (run `woc --dump-gc`). Remove it. `` | ## WO-E2xx / WO-W2xx — types (Task 6, `compiler/src/types.ml`; WO-E214 Task 8, `compiler/bin/main.ml`; WO-E215 plan 3 Task 2, `compiler/src/types.ml`; WO-E210/E216–E218/W202 haxe-parity Task 1, `compiler/src/types.ml`; WO-E201 haxe-parity Task 2, `compiler/src/types.ml`; WO-E208 haxe-parity Task 3, `compiler/src/types.ml`; WO-E203 haxe-parity Task 4, `compiler/src/types.ml`) diff --git a/tests/corpus/compile-fail/reject-cast/fixture.code b/tests/corpus/compile-fail/reject-cast/fixture.code new file mode 100644 index 0000000..163e168 --- /dev/null +++ b/tests/corpus/compile-fail/reject-cast/fixture.code @@ -0,0 +1 @@ +WO-E105 diff --git a/tests/corpus/compile-fail/reject-cast/fixture.wo b/tests/corpus/compile-fail/reject-cast/fixture.wo new file mode 100644 index 0000000..ac723c7 --- /dev/null +++ b/tests/corpus/compile-fail/reject-cast/fixture.wo @@ -0,0 +1,6 @@ +-- Doctrine reject (WO-E105): no unsafe casts — `as` exists only in the +-- json.decode target position. +fn main() -> Int { + let x = cast 3; + return 0 +} diff --git a/tests/corpus/compile-fail/reject-dynamic/fixture.code b/tests/corpus/compile-fail/reject-dynamic/fixture.code new file mode 100644 index 0000000..5cf1644 --- /dev/null +++ b/tests/corpus/compile-fail/reject-dynamic/fixture.code @@ -0,0 +1 @@ +WO-E225 diff --git a/tests/corpus/compile-fail/reject-dynamic/fixture.wo b/tests/corpus/compile-fail/reject-dynamic/fixture.wo new file mode 100644 index 0000000..2516ee6 --- /dev/null +++ b/tests/corpus/compile-fail/reject-dynamic/fixture.wo @@ -0,0 +1,9 @@ +-- Doctrine reject: `Dynamic` as a type name — static typing to the register +-- (principle 13). Fires as WO-E225 with the doctrine-citing message. +class C { + v: Dynamic +} + +fn main() -> Int { + return 0 +} diff --git a/tests/corpus/compile-fail/reject-inheritance/fixture.code b/tests/corpus/compile-fail/reject-inheritance/fixture.code new file mode 100644 index 0000000..163e168 --- /dev/null +++ b/tests/corpus/compile-fail/reject-inheritance/fixture.code @@ -0,0 +1 @@ +WO-E105 diff --git a/tests/corpus/compile-fail/reject-inheritance/fixture.wo b/tests/corpus/compile-fail/reject-inheritance/fixture.wo new file mode 100644 index 0000000..5cbeb3f --- /dev/null +++ b/tests/corpus/compile-fail/reject-inheritance/fixture.wo @@ -0,0 +1,14 @@ +-- Doctrine reject (WO-E105): no inheritance, ever. `extends` fails at its +-- own position with the doctrine reason (principle 4), never a generic +-- syntax error. `super`/`override`/`implements` share the row. +class A { + n: Int +} + +class B extends A { + m: Int +} + +fn main() -> Int { + return 0 +}