From 3d75196121557ae4ed8eda9e5eb08fb376433778 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sat, 22 Aug 2026 21:29:47 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20iteration=2036=20tasks=202-3=20?= =?UTF-8?q?=E2=80=94=20grammar,=20checker,=20emit=20lowering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ast: unop Not, binop BAnd/BOr/BXor/Shl/Shr - parser: | ^ join additive, & << >> join multiplicative (Go rungs); not joins unary (Lua placement); ladder doc updated - compound assigns claim the dead +=/-= tokens plus *= /= %= — parse-time sugar via rewind-and-reparse, fresh ids by construction - types: bitwise Int-only both sides, not Bool-only (WO-E201 family); literal shift count outside 0..63 rejected as new WO-E223; confident-typ knows bitwise=Int, not=Bool - emit: op_band..op_shr 42..46; not lowers on existing EQ vs zero - woc-test 543/0 unchanged; scratch smoke parses/typechecks clean Co-Authored-By: Claude Fable 5 --- compiler/src/ast.ml | 20 +++++++++- compiler/src/dump.ml | 6 +++ compiler/src/emit.ml | 29 +++++++++++++++ compiler/src/parser.ml | 84 +++++++++++++++++++++++++++++++++++++----- compiler/src/types.ml | 77 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 206 insertions(+), 10 deletions(-) diff --git a/compiler/src/ast.ml b/compiler/src/ast.ml index 8195584..7e3ae82 100644 --- a/compiler/src/ast.ml +++ b/compiler/src/ast.ml @@ -146,7 +146,13 @@ type field = { spells out the token; it is the only unclaimed binary-shaped token left, and Lua's `..` is the same design (concat binds looser than `+`/`-`, tighter than comparison — this ladder's ordering). *) -type unop = Neg +(* iteration 36: `Not` is boolean negation, the keyword `not` (a word + like and/or, never `!`). Bool-only operand (types.ml), lowered on the + existing WOP_EQ against a zero constant — no new opcode, the same + doctrine And/Or's comment below records for the short-circuit pair. *) +type unop = + | Neg + | Not (* `And`/`Or` (haxe-parity Task 2): real keywords, spelled as words, not `&&`/`||` — the spec amendment's own wording. `Bool`-typed operands @@ -170,6 +176,18 @@ type binop = | Ge | And | Or + (* iteration 36: the five Int bitwise operators (story 36's settled + decisions). Precedence copies Go's C-trap fix: BAnd/Shl/Shr sit on + the multiplicative rung, BOr/BXor on the additive rung — both above + comparison, so `x & mask == 0` groups the AND first. Int-only + operands (types.ml); Shr is arithmetic (sign-extending); a count + outside 0..63 traps WO_T_SHIFT at run time and a literal count is + rejected at compile time. *) + | BAnd + | BOr + | BXor + | Shl + | Shr type expr = { id : int; diff --git a/compiler/src/dump.ml b/compiler/src/dump.ml index 60a4aa3..95722d8 100644 --- a/compiler/src/dump.ml +++ b/compiler/src/dump.ml @@ -211,6 +211,11 @@ let binop_str : Ast.binop -> string = function | Ast.Ge -> ">=" | Ast.And -> "and" | Ast.Or -> "or" + | Ast.BAnd -> "&" + | Ast.BOr -> "|" + | Ast.BXor -> "^" + | Ast.Shl -> "<<" + | Ast.Shr -> ">>" (* Raw token span shared by both DbStub renderings below: a statement- position DbStub (dump_stmt) and an expression-position one nested @@ -239,6 +244,7 @@ let rec expr_str (e : Ast.expr) : string = | Ast.Call (callee, args) -> Printf.sprintf "%s(%s)" (expr_str callee) (String.concat ", " (List.map expr_str args)) | Ast.Unary (Ast.Neg, operand) -> "-" ^ expr_str operand + | Ast.Unary (Ast.Not, operand) -> "not " ^ expr_str operand | Ast.Binary (op, l, r) -> Printf.sprintf "%s %s %s" (expr_str l) (binop_str op) (expr_str r) | Ast.Ctor (name, fields) -> Printf.sprintf "%s { %s }" name diff --git a/compiler/src/emit.ml b/compiler/src/emit.ml index 698062e..072b44c 100644 --- a/compiler/src/emit.ml +++ b/compiler/src/emit.ml @@ -186,6 +186,15 @@ let op_fneg = 38 let op_feq = 39 let op_flt = 40 let op_fle = 41 + +(* iteration 36: the Int bitwise ops (.wob v6). SHR is arithmetic + (sign-extending); a shift count outside 0..63 traps WO_T_SHIFT — + literal counts never get this far (types.ml rejects them). *) +let op_band = 42 +let op_bor = 43 +let op_bxor = 44 +let op_shl = 45 +let op_shr = 46 let op_concat = 8 let op_eq = 9 let op_lt = 10 @@ -1195,10 +1204,14 @@ let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option | _ -> None)) | _ -> None) | Unary (Neg, o) -> ty_of_expr p f o + | Unary (Not, _) -> Some (Scalar "Bool") | Binary (op, l, _) -> ( match op with | Concat -> Some (Scalar "Text") | Eq | Ne | Lt | Le | Gt | Ge | And | Or -> Some (Scalar "Bool") + (* iteration 36: bitwise is Int-only (types.ml enforces), so the + result is always Int — no Float twin to derive through `l`. *) + | BAnd | BOr | BXor | Shl | Shr -> Some (Scalar "Int") | Add | Sub | Mul | Div | Mod -> ( match ty_of_expr p f l with Some t -> Some t | None -> Some (Scalar "Int"))) | Ctor (cn, _) -> Some (Scalar cn) (* arc: a spawn's value is the typed actor address (a scalar word) *) @@ -1876,6 +1889,14 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e `-x` on an infinity gives the other infinity. Integer NEG on f64 bits would produce a different number entirely. *) put f (ins_abc (if is_float p f o then op_fneg else op_neg) dst b 0) + | Unary (Not, o) -> + (* iteration 36: no NOT opcode — Bool is 0/1, so `not x` is + `x == 0` on the existing EQ, the same lowering `!=` already + uses for its final flip. *) + let b = emit_operand p f v o in + let z = alloc_temp p f e.pos in + put f (ins_abx op_loadk z (check_bx p f e.pos "constant" (const_int p 0))); + put f (ins_abc op_eq dst b z) | Binary (op, l, r) -> emit_binary p f v ~dst op l r | Ctor (cn, fields) -> emit_ctor p f v ~dst e cn fields | Spawn (cn, fields) -> @@ -2154,6 +2175,14 @@ and emit_binary (p : pctx) (f : fstate) (v : views) ~(dst : int) (op : Ast.binop put f (ins_abc op_div q a b); put f (ins_abc op_mul q q b); put f (ins_abc op_sub dst a q) + (* iteration 36: Int-only (types.ml enforced), one instruction each — + no Float twin exists to select and no nil/text special case can + reach here. *) + | BAnd -> simple op_band + | BOr -> simple op_bor + | BXor -> simple op_bxor + | Shl -> simple op_shl + | Shr -> simple op_shr (* haxe-parity Task 3: compare-and-jump chain on the existing EQ/EQS/ JZ/JMP opcodes — no new opcode, per the brief. The subject is diff --git a/compiler/src/parser.ml b/compiler/src/parser.ml index 8c9c8e2..c1ecff0 100644 --- a/compiler/src/parser.ml +++ b/compiler/src/parser.ml @@ -591,13 +591,23 @@ let parse_sig_head (st : state) : sig_head = and and (haxe-parity Task 2) comparison == != < <= > >= concat .. - additive + - - multiplicative * / % - unary minus -x + additive + - | ^ (| ^ iteration 36) + multiplicative * / % & << >> (& << >> iteration 36) + unary -x not x (not iteration 36) postfix a.b a(b) a[b] primary literals, idents, `(expr)`, constructor literals, select-as-expression (DbStub) + Iteration 36 slots the five bitwise operators into the EXISTING + additive/multiplicative rungs exactly where Go's table puts them + (token.go's Precedence: | ^ with + -, & << >> with * / %) — above + comparison, so `x & mask == 0` groups the AND first (C parses that + the other way; Go fixed the trap, this grammar copies the fix), and + shifts bind tighter than `+` so `1 << 4 + 1` is `(1 << 4) + 1`. + `not` joins the unary level (Lua's placement): `not a == b` groups + `(not a) == b`. Expression-position `|` is Token.Pipe reused — the + union-declaration use parses in the type grammar, never here. + This ordering matches Lua's (concat binds looser than +/-, tighter than comparison) — see ast.ml's module doc for why `..`/Concat is this task's own addition, not a straight rt port. `and`/`or` sit @@ -867,9 +877,15 @@ and parse_additive (st : state) : Ast.expr = let continue_ = ref true in while !continue_ do match peek st with - | (Token.Plus | Token.Dash) as k -> + | (Token.Plus | Token.Dash | Token.Pipe | Token.Caret) as k -> let pos = peek_pos st in - let op = if k = Token.Plus then Ast.Add else Ast.Sub in + let op = + match k with + | Token.Plus -> Ast.Add + | Token.Dash -> Ast.Sub + | Token.Pipe -> Ast.BOr + | _ -> Ast.BXor + in let id = fresh_id st in ignore (advance st); let rhs = parse_multiplicative st in @@ -883,9 +899,17 @@ and parse_multiplicative (st : state) : Ast.expr = let continue_ = ref true in while !continue_ do match peek st with - | (Token.Star | Token.Slash | Token.Percent) as k -> + | (Token.Star | Token.Slash | Token.Percent | Token.Amp | Token.Shl | Token.Shr) as k -> let pos = peek_pos st in - let op = match k with Token.Star -> Ast.Mul | Token.Slash -> Ast.Div | _ -> Ast.Mod in + let op = + match k with + | Token.Star -> Ast.Mul + | Token.Slash -> Ast.Div + | Token.Percent -> Ast.Mod + | Token.Amp -> Ast.BAnd + | Token.Shl -> Ast.Shl + | _ -> Ast.Shr + in let id = fresh_id st in ignore (advance st); let rhs = parse_unary st in @@ -913,6 +937,12 @@ and parse_unary (st : state) : Ast.expr = ignore (advance st); let operand = parse_unary st in { Ast.id; pos; kind = Ast.Unary (Ast.Neg, operand) } + | Token.KwNot -> + let pos = peek_pos st in + let id = fresh_id st in + ignore (advance st); + let operand = parse_unary st in + { Ast.id; pos; kind = Ast.Unary (Ast.Not, operand) } | _ -> parse_as st (parse_postfix st) and parse_postfix (st : state) : Ast.expr = @@ -1485,6 +1515,7 @@ and parse_stmt (st : state) : Ast.stmt = | _ -> let pos = peek_pos st in let id = fresh_id st in + let start_tok = st.pos in let e = parse_expr st in if accept st Token.Eq then begin let value = parse_expr st in @@ -1492,8 +1523,43 @@ and parse_stmt (st : state) : Ast.stmt = { Ast.s_id = id; s_pos = pos; s_kind = Ast.Assign { target = e; value } } end else begin - end_of_stmt st; - { Ast.s_id = id; s_pos = pos; s_kind = Ast.ExprStmt e } + (* iteration 36: compound assigns, parse-time sugar — `x += e` IS + `x = x + e`, including an index expression evaluating twice, + exactly as the written-out form would (the story's documented + contract). The value's left operand is the SAME place parsed a + second time by rewinding st.pos to the statement start: no + expression rung consumes a compound token, so the re-parse + stops exactly where the first one did, and every re-parsed + node draws a fresh id — the owner/emit passes see two honest + reads, never one node in two roles. +=/-= existed as tokens + since haxe-parity Task 2 but no rule ever consumed them (the + dead-token defect story 36 records); this claims all five. *) + let compound_op = + match peek st with + | Token.PlusEq -> Some Ast.Add + | Token.MinusEq -> Some Ast.Sub + | Token.StarEq -> Some Ast.Mul + | Token.SlashEq -> Some Ast.Div + | Token.PercentEq -> Some Ast.Mod + | _ -> None + in + match compound_op with + | Some op -> + let op_pos = peek_pos st in + ignore (advance st); + let after_op = st.pos in + st.pos <- start_tok; + let lhs_again = parse_expr st in + st.pos <- after_op; + let rhs = parse_expr st in + end_of_stmt st; + let value = + { Ast.id = fresh_id st; pos = op_pos; kind = Ast.Binary (op, lhs_again, rhs) } + in + { Ast.s_id = id; s_pos = pos; s_kind = Ast.Assign { target = e; value } } + | None -> + end_of_stmt st; + { Ast.s_id = id; s_pos = pos; s_kind = Ast.ExprStmt e } end (* Saves/restores state.no_brace around an if/while condition or a diff --git a/compiler/src/types.ml b/compiler/src/types.ml index 7ea7754..c646f8d 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -461,6 +461,10 @@ let unused_use_code = Diag.warning_prefix ^ "202" (* WO-W202 *) let unknown_type_name_code = Diag.types_prefix ^ "25" (* WO-E225 *) +(* iteration 36: a LITERAL shift count outside 0..63 — rejected here so + the WO_T_SHIFT run-time trap only ever fires on variable counts. *) +let shift_count_code = Diag.types_prefix ^ "23" (* WO-E223 *) + (* haxe-parity Task 3, review fix (Critical 1). `default` is moved to the *end* of the lowering order regardless of where it sits in the source (Ast.switch_lowering_order) -- so a `case` arm written after @@ -1381,6 +1385,12 @@ let typecheck_program ~file ~(module_of : string -> string) this arm every interpolated value looked underivable and every check built on confident types silently skipped it. *) | Binary (Concat, _, _) -> Some (TScalar "Text") + (* iteration 36: bitwise is confidently Int and `not` confidently + Bool for the same reason the comparison arm above is Bool — the + operator's own meaning, not a guess (Int-only/Bool-only operands + are enforced in typecheck_expr's own arms). *) + | Binary ((BAnd | BOr | BXor | Shl | Shr), _, _) -> Some (TScalar "Int") + | Unary (Not, _) -> Some (TScalar "Bool") | Interp _ -> (* An interpolation always *produces* Text by construction (emit.ml decides, per-segment, whether the embedded value @@ -1729,6 +1739,25 @@ let typecheck_program ~file ~(module_of : string -> string) check_builtin_call ~file collector name e.pos args confident_types) | _ -> ()); { typ = TScalar "Int"; is_nil = false } + | Unary (Not, operand) -> + (* iteration 36: Bool-only, no truthiness — the same confident- + type contract the and/or arms below use, and the same WO-E201 + wording, so `not` reads as the third member of that family. *) + let res = typecheck_expr env cenv operand in + (match confident_typ cenv operand with + | None -> () + | Some t -> + (if is_nullable t || res.is_nil then e211 operand.pos (expr_label operand)); + if unwrap_nullable t <> TScalar "Bool" then + Diag.Collector.add collector + (Diag.error ~code:type_mismatch_code ~file ~line:operand.pos.line + ~col:operand.pos.col + ~message: + (Printf.sprintf + "`not` operand must be `Bool`, got `%s` -- no truthiness in this language" + (typ_label t)) + ())); + { typ = TScalar "Bool"; is_nil = false } | Unary (_, operand) -> typecheck_expr env cenv operand | Binary ((And | Or) as op, left, right) -> (* haxe-parity Task 2: `Bool`-typed operands only, no truthiness @@ -1857,6 +1886,54 @@ let typecheck_program ~file ~(module_of : string -> string) if op = Mod then mod_on_float cenv e.pos left right; { typ = (match confident_typ cenv left with Some t -> t | None -> TScalar "Int"); is_nil = false } + | Binary (((BAnd | BOr | BXor | Shl | Shr) as op), left, right) -> + (* iteration 36: bitwise is Int-only on BOTH sides — no Float + twin exists (nothing like FADD to fall back to), so a Float + or Text operand would lower to a garbage word operation with + no diagnostic. Reported off confident types, the same + stay-silent-when-underivable contract as every check above. *) + let lres = typecheck_expr env cenv left in + let rres = typecheck_expr env cenv right in + if is_nullable lres.typ || lres.is_nil then e211 left.pos (expr_label left); + if is_nullable rres.typ || rres.is_nil then e211 right.pos (expr_label right); + let opname = + match op with BAnd -> "&" | BOr -> "|" | BXor -> "^" | Shl -> "<<" | _ -> ">>" + in + let check_int_operand (operand : expr) = + match confident_typ cenv operand with + | None -> () + | Some t -> + if unwrap_nullable t <> TScalar "Int" then + Diag.Collector.add collector + (Diag.error ~code:type_mismatch_code ~file ~line:operand.pos.line + ~col:operand.pos.col + ~message: + (Printf.sprintf "`%s` operand must be `Int`, got `%s` -- bitwise is Int-only" + opname (typ_label t)) + ()) + in + check_int_operand left; + check_int_operand right; + (* a LITERAL count outside 0..63 can never be right — reject it + here (WO-E223) so the WO_T_SHIFT trap is variable-count-only. + A negative literal arrives as Unary(Neg, IntLit). *) + (match op with + | Shl | Shr -> + let out_of_range = + match right.kind with + | IntLit n -> n < 0 || n > 63 + | Unary (Neg, { kind = IntLit n; _ }) -> n > 0 + | _ -> false + in + if out_of_range then + Diag.Collector.add collector + (Diag.error ~code:shift_count_code ~file ~line:right.pos.line + ~col:right.pos.col + ~message: + (Printf.sprintf "shift count is out of range 0..63 for `%s`" opname) + ()) + | _ -> ()); + { typ = TScalar "Int"; is_nil = false } | Binary (((Lt | Le | Gt | Ge) as op), left, right) -> let lres = typecheck_expr env cenv left in let rres = typecheck_expr env cenv right in