diff --git a/compiler/src/ast.ml b/compiler/src/ast.ml index c39f8d4..a164cd0 100644 --- a/compiler/src/ast.ml +++ b/compiler/src/ast.ml @@ -109,6 +109,12 @@ type field = { rt's own field-annotation handling; nothing downstream needs those arguments in Task 4. *) annotations : string list; + (* haxe-parity Task 7: `pub(read) name: T` — the field's value is + readable from outside the declaring class, but writable only from + inside it (Haxe's `(default, null)` property pattern). Reads need no + check at all; the write side is types.ml's, at every assignment + whose target is a field of another class's instance. *) + pub_read : bool; } (* ---- expressions (Task 5) ------------------------------------------ @@ -228,6 +234,19 @@ and expr_kind = uses either, so neither is grammar here (YAGNI, recorded in the task report). *) | Switch of expr * switch_arm list + (* Container literals, the driving workload's own spelling for a fresh + container: `[]` / `[a, b, c]` for a `multi T`, `{}` for an empty + `map`. They lower to exactly what `multi_new()`/`map_new()` + already lower to (the element kinds come from the destination's + declared type — docs/plan/oop-vm/08-builtin-surface.md's + "a fresh container needs a destination of declared type"), plus one + `push` per element for a non-empty list. A literal with no typed + destination is WO-E403, the same as a bare `let m = map_new()`. + Non-empty map literals are not grammar: the workload has none, and + `{ k: v }` in expression position cannot be told from a constructor + literal without lookahead nothing else needs. *) + | ListLit of expr list + | MapLit (* ---- statements (Task 5) --------------------------------------------- @@ -257,7 +276,11 @@ and stmt = { and stmt_kind = | Let of { name : string; - ty : string option; + (* The full annotation grammar, not just a bare name: the driving + workload writes `let rest: multi Text = []`, `let headers: + map = {}` and `let port: ?Int = nil`, all of which + parse_field_ty already understood for fields and parameters. *) + ty : field_ty option; value : expr; } | Assign of { @@ -383,6 +406,13 @@ type method_decl = { passes `pub = false` for a class body's own methods — this field is meaningful only when the surrounding decl is `Fn`. *) pub : bool; + (* haxe-parity Task 7: `static fn` on a class — no instance, no `self`, + called as `Flock.held(path)`. Lowered as an ordinary method record + with no receiver slot (emit.ml), so its `arg_cnt` counts parameters + only, and it can never satisfy an interface method (nothing to + dispatch on). Always false for a free `fn` and for an interface + signature. *) + is_static : bool; } (* `@table(name: "...", index: [a, b], index: [c])` — optional storage diff --git a/compiler/src/dump.ml b/compiler/src/dump.ml index 723772f..8ddaaa7 100644 --- a/compiler/src/dump.ml +++ b/compiler/src/dump.ml @@ -220,6 +220,8 @@ let rec expr_str (e : Ast.expr) : string = (List.map (fun (fname, fval) -> Printf.sprintf "%s: %s" fname (expr_str fval)) fields)) | Ast.DbStub toks -> Printf.sprintf "DB_STUB(%s)" (dbstub_tokens_str toks) | Ast.Interp inner -> Printf.sprintf "INTERP(%s)" (expr_str inner) + | Ast.ListLit items -> Printf.sprintf "[%s]" (String.concat ", " (List.map expr_str items)) + | Ast.MapLit -> "{}" (* haxe-parity Task 3: arm bodies are `stmt list`, not one `expr` — no golden AST/bc fixture pins a switch (direct assertions instead, see runner.ml, same convention haxe-parity Task 2 used), so this is a @@ -249,7 +251,7 @@ let rec dump_stmt (s : Ast.stmt) : string list = in match s.Ast.s_kind with | Ast.Let { name; ty; value } -> - let ty_part = match ty with None -> "" | Some t -> ": " ^ t in + let ty_part = match ty with None -> "" | Some t -> ": " ^ field_ty_str t in [ Printf.sprintf "%s LET %s%s = %s" (pos_str s.Ast.s_pos) name ty_part (expr_str value) ] | Ast.Assign { target; value } -> [ Printf.sprintf "%s ASSIGN %s = %s" (pos_str s.Ast.s_pos) (expr_str target) (expr_str value) ] diff --git a/compiler/src/emit.ml b/compiler/src/emit.ml index 97e8696..790aeaf 100644 --- a/compiler/src/emit.ml +++ b/compiler/src/emit.ml @@ -783,6 +783,12 @@ let class_method (p : pctx) (cname : string) (m : string) : Types.method_info op | Some (c : Types.class_info) -> List.find_opt (fun (mi : Types.method_info) -> mi.Types.name = m) c.Types.methods +(* haxe-parity Task 7: the `static fn` behind `Flock.held(path)`. Kept + separate from class_method so an instance method is never callable + through a class name, and a static never through an instance. *) +let static_method (p : pctx) (cname : string) (m : string) : Types.method_info option = + match class_method p cname m with Some mi when mi.Types.is_static -> Some mi | _ -> None + let iface_method (p : pctx) (iname : string) (m : string) : (int * Types.method_sig_info) option = match SM.find_opt iname p.p_iface_id with | None -> None @@ -856,6 +862,11 @@ let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option | IntLit _ -> Some (Scalar "Int") | StrLit _ -> Some (Scalar "Text") | BoolLit _ -> Some (Scalar "Bool") + (* Same rule as owner.ml's expr_ty: a non-empty list literal knows its + element type; an empty `[]`/`{}` is contextual on its destination. *) + | ListLit (first :: _) -> ( + match ty_of_expr p f first with Some (Scalar n) -> Some (Multi n) | _ -> None) + | ListLit [] | MapLit -> None | Ident n -> ( match List.assoc_opt n f.f_env with | Some (_, t) -> Some t @@ -922,6 +933,11 @@ let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option receiver is always caught by the `Some bt` arm above, so locals/params still shadow a same-named alias here too. *) match base.kind with + (* haxe-parity Task 7: a static call's base names a class, which is + never a value — checked before the `use`-alias reading, since a + class name and a module alias are both bare Idents here. *) + | Ident cls_name when static_method p cls_name mname <> None -> ( + match static_method p cls_name mname with Some mi -> mi.Types.ret | None -> None) | Ident alias -> ( match use_edge_for p ~file:f.f_file alias with | Some u when u.Types.ue_is_stdlib -> None (* nothing to infer a return type from yet *) @@ -1207,6 +1223,53 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e | IntLit n -> put f (ins_abx op_loadk dst (check_bx p f e.pos "constant" (const_int p n))) | BoolLit b -> put f (ins_abx op_loadk dst (check_bx p f e.pos "constant" (const_int p (if b then 1 else 0)))) | StrLit s -> put f (ins_abx op_loadk dst (check_bx p f e.pos "constant" (const_text p s))) + (* Container literals lower to exactly what `multi_new()`/`map_new()` + lower to — the element kinds are the destination's, never guessed + (docs/plan/oop-vm/08-builtin-surface.md) — plus one `multi_push` per + element, in source order. *) + | ListLit items -> ( + match container_imm p expected false with + | None -> + err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos + ~message: + "a list literal needs a destination of declared type `multi T` — its element kind is \ + the container's drop plan and cannot be guessed"; + put f (ins_abx op_loadk dst (const_int p 0)) + | Some imm -> + sync_mask p f v e.id; + put f (ins_abc op_builtin dst imm b_multi_new); + let elem_ty = + match expected with + | Some t -> ( match unwrap t with Multi en -> Some (Scalar en) | _ -> None) + | None -> None + in + let outer = f.f_temp in + if f.f_temp <= dst then f.f_temp <- dst + 1; + List.iter + (fun (item : Ast.expr) -> + let save = f.f_temp in + let base = alloc_temps p f e.pos 2 in + put f (ins_abc op_move base dst 0); + (match elem_ty with + | Some et -> emit_expr p f v ~dst:(base + 1) ~expected:et item + | None -> emit_expr p f v ~dst:(base + 1) item); + sync_mask p f v e.id; + f.f_cur_line <- item.pos.line; + put f (ins_abc op_builtin base base b_multi_push); + f.f_temp <- save) + items; + f.f_temp <- outer) + | MapLit -> ( + match container_imm p expected true with + | None -> + err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos + ~message: + "an empty map literal needs a destination of declared type `map` — its key and \ + value kinds are the container's drop plan and cannot be guessed"; + put f (ins_abx op_loadk dst (const_int p 0)) + | Some imm -> + sync_mask p f v e.id; + put f (ins_abc op_builtin dst imm b_map_new)) | Ident n -> ( match lookup_local f n with | Some (r, _) -> if r <> dst then put f (ins_abc op_move dst r 0) @@ -1936,6 +1999,14 @@ and emit_call (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : As match unwrap bt with | Scalar cn -> ( match class_method p cn mname with + | Some mi when mi.Types.is_static -> + (* haxe-parity Task 7: a static has no receiver — reaching it + through an instance is an error, not an implicit `Cls.` *) + err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos + ~message: + (Printf.sprintf "`%s` is a `static fn` — call it as `%s.%s(...)`, not on an instance" + mname cn mname); + put f (ins_abx op_loadk dst (const_int p 0)) | Some mi -> emit_direct p f v ~dst e ~key:(cn ^ "." ^ mname) ~recv:(Some base) ~params:mi.Types.params args @@ -1978,6 +2049,16 @@ and emit_call (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : As once qualification disambiguates them, not two spellings of whichever one the flat merge happened to keep. *) match base.kind with + (* haxe-parity Task 7: `Flock.held(path)` — the base names a class, + so there is no receiver to pass and the window holds parameters + only (`recv:None`, exactly like a free fn). Checked before the + `use`-alias reading: both are bare Idents at this point. *) + | Ident cls_name when static_method p cls_name mname <> None -> ( + match static_method p cls_name mname with + | Some mi -> + emit_direct p f v ~dst e ~key:(cls_name ^ "." ^ mname) ~recv:None ~params:mi.Types.params + args + | None -> ()) | Ident alias -> ( match use_edge_for p ~file:f.f_file alias with | Some u when u.Types.ue_is_stdlib -> @@ -2272,7 +2353,7 @@ and emit_stmt (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) : unit = f.f_cur_line <- s.s_pos.line; match s.s_kind with | Let { name; ty; value } -> - let declared = match ty with Some t -> Some (Scalar t) | None -> None in + let declared = ty in let vty = match declared with | Some t -> t @@ -2855,6 +2936,10 @@ let satisfies (p : pctx) (cid : int) (ir : ifacerec) : int list option = if not (List.mem mname cr.cr_methods) then None else match class_method p cr.cr_name mname with + (* A `static fn` has no receiver to dispatch on, so it can never + satisfy an interface method however well its name and arity + line up (haxe-parity Task 7). *) + | Some mi when mi.Types.is_static -> None | Some mi when List.length mi.Types.params = nparams -> ( match SM.find_opt (cr.cr_name ^ "." ^ mname) p.p_method_id with | Some midx -> go (midx :: acc) tl @@ -2985,11 +3070,18 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string) let key = c.name ^ "." ^ m.name in if not (SM.mem key !method_id) then begin method_id := SM.add key !nmethods !method_id; + (* haxe-parity Task 7: a `static fn` has no receiver, so + its window holds parameters only and its body binds + no `self` (self_class = None below) — otherwise it is + an ordinary method record, keyed `Class.name` like + any other. *) methods := - { mr_name = m.name; mr_class = Some cid; mr_argc = 1 + List.length m.params; + { mr_name = m.name; mr_class = Some cid; + mr_argc = (if m.is_static then 0 else 1) + List.length m.params; mr_regc = 1; mr_code = [||]; mr_lines = []; mr_drops = [] } :: !methods; - bodies := (u, Some (cid, c.name), m, !nmethods) :: !bodies; + bodies := + (u, (if m.is_static then None else Some (cid, c.name)), m, !nmethods) :: !bodies; incr nmethods end) c.methods diff --git a/compiler/src/owner.ml b/compiler/src/owner.ml index 037d419..5b21ad8 100644 --- a/compiler/src/owner.ml +++ b/compiler/src/owner.ml @@ -503,6 +503,14 @@ let rec expr_ty (ctx : ctx) (e : Ast.expr) : Ast.field_ty option = | IntLit _ -> Some (Scalar "Int") | StrLit _ -> Some (Scalar "Text") | BoolLit _ -> Some (Scalar "Bool") + (* A non-empty list literal knows its element type, so an unannotated + `let names = ["a", "b"]` is still classified Owned and dropped. An + empty `[]`/`{}` is contextual: only the destination's declared type + says what it holds, so this stays None and the annotation (or the + field/parameter it is built into) decides. *) + | ListLit (first :: _) -> ( + match expr_ty ctx first with Some (Scalar n) -> Some (Multi n) | _ -> None) + | ListLit [] | MapLit -> None | Ident n -> ( match find_local ctx n with | Some l -> Some l.l_ty @@ -1043,6 +1051,18 @@ let rec read_expr (ctx : ctx) (e : Ast.expr) : unit = read_expr ctx a; read_expr ctx b | Interp inner -> read_expr ctx inner + (* A list literal reads each element and hands it to the fresh + container, exactly as `push(m, v)` does — so it inherits `push`'s own + open gap, recorded in docs/plan/oop-vm/08-builtin-surface.md: an + element that is a *borrowed* non-constant Text (or any borrowed + owned value) is stored by pointer while the container's declared + element kind makes it the container's to free. The workload's own + literals are string constants (never freed) plus borrowed params + handed straight to a stdlib call, so nothing reachable today hits + it; it is a runtime-semantics gap to close with `push`, not a + literal-specific one. *) + | ListLit items -> List.iter (read_expr ctx) items + | MapLit -> () | DbStub _ -> (* trap-capable: the frame needs its drop map here *) record_drop ctx ~node:e.id ~pos:e.pos ~kind:DLiveMask ~items:(mask_items (live_holders ctx)) @@ -1542,12 +1562,13 @@ and analyze_block (ctx : ctx) ?(pre = []) ~node ~pos ~label (body : Ast.stmt lis List.iter (analyze_stmt ctx) body; pop_scope ctx -and analyze_let (ctx : ctx) (s : Ast.stmt) (name : string) (ty : string option) (value : Ast.expr) +and analyze_let (ctx : ctx) (s : Ast.stmt) (name : string) (ty : Ast.field_ty option) + (value : Ast.expr) : unit = read_expr ctx value; let vty = match ty with - | Some tn -> Scalar tn + | Some t -> t | None -> ( match expr_ty ctx value with Some t -> t | None -> Scalar "Int") in let cls = oclass_of ctx vty in diff --git a/compiler/src/parser.ml b/compiler/src/parser.ml index f06a4b3..057cbc4 100644 --- a/compiler/src/parser.ml +++ b/compiler/src/parser.ml @@ -448,7 +448,7 @@ let parse_default_expr (st : state) : Ast.default_expr = consume. Every pre-existing call site passes nothing and keeps the class-body behavior byte-identical (a comma there is still the same "unexpected" error as before). *) -let parse_field ?(comma_ends = false) (st : state) : Ast.field = +let parse_field ?(comma_ends = false) ?(pub_read = false) (st : state) : Ast.field = let pos = peek_pos st in let name = expect_field_name st "field name" in expect st Token.Colon "':'"; @@ -470,7 +470,8 @@ let parse_field ?(comma_ends = false) (st : state) : Ast.field = | Token.Newline | Token.RBrace | Token.Eof -> continue_ := false | _ -> unexpected st "an annotation, '=', or end of field" done; - { Ast.id = fresh_id st; pos; name; ty; default = !default; annotations = List.rev !annotations } + { Ast.id = fresh_id st; pos; name; ty; default = !default; annotations = List.rev !annotations; + pub_read } (* ---- param / signature parsing ------------------------------------------ *) @@ -571,12 +572,19 @@ let parse_sig_head (st : state) : sig_head = them, and requiring a terminator after it would wrongly reject `} else {` on one line, the normal style for chained if/else. *) +(* A `;` terminates the statement by itself — whatever follows on the same + line is the next statement, which is how the driving workload writes a + short guard body (`{ skip(res, ...); return; }`, `{ i = i + 1; + continue; }`). Requiring a newline *after* the semicolon (the earlier + rule) made one-line blocks a syntax error. With no `;`, a newline (or + the enclosing block's close) is still what ends the statement. *) let end_of_stmt (st : state) : unit = - ignore (accept st Token.Semicolon); - match peek st with - | Token.Newline -> ignore (advance st) - | Token.RBrace | Token.Eof -> () - | _ -> unexpected st "end of statement (newline or ';')" + if accept st Token.Semicolon then () + else + match peek st with + | Token.Newline -> ignore (advance st) + | Token.RBrace | Token.Eof -> () + | _ -> unexpected st "end of statement (newline or ';')" (* ---- statement-level recovery ------------------------------------------- @@ -998,6 +1006,34 @@ and parse_primary (st : state) : Ast.expr = let e = with_no_brace st false (fun () -> parse_expr st) in expect st Token.RParen "')'"; e + | Token.LBracket -> + (* `[]` / `[a, b, c]` — a fresh `multi`. Newlines inside the brackets + are insignificant (the workload writes single-line literals, but a + long one must be allowed to wrap like a call's argument list). *) + let pos = peek_pos st in + let id = fresh_id st in + ignore (advance st); + let items = ref [] in + skip_newlines st; + while peek st <> Token.RBracket && not (at_end st) do + items := with_no_brace st false (fun () -> parse_expr st) :: !items; + skip_newlines st; + if accept st Token.Comma then skip_newlines st + done; + expect st Token.RBracket "']' to close the list literal"; + { Ast.id; pos; kind = Ast.ListLit (List.rev !items) } + | Token.LBrace when not st.no_brace -> + (* `{}` — a fresh empty `map`. Only the empty form: see ast.ml's + MapLit doc comment for why `{ k: v }` is not grammar. *) + let pos = peek_pos st in + let id = fresh_id st in + ignore (advance st); + skip_newlines st; + if peek st <> Token.RBrace then + fail st (peek_pos st) syntax_code + "only the empty map literal `{}` is an expression — build entries with `set(m, k, v)`"; + ignore (advance st); + { Ast.id; pos; kind = Ast.MapLit } | Token.Ident _ when looks_like_ctor st -> parse_ctor_literal st | Token.Ident s -> let pos = peek_pos st in @@ -1092,7 +1128,7 @@ and parse_let_stmt (st : state) : Ast.stmt = ignore (advance st); (* 'let' *) let name = expect_ident st "let-binding name" in - let ty = if accept st Token.Colon then Some (expect_ident st "let-binding type") else None in + let ty = if accept st Token.Colon then Some (parse_field_ty st) else None in expect st Token.Eq "'=' in let binding"; let value = parse_expr st in end_of_stmt st; @@ -1227,10 +1263,11 @@ and parse_expr_no_brace (st : state) : Ast.expr = with_no_brace st true (fun () visibility is a different, not-yet-designed question — see ast.ml's method_decl.pub doc comment), so this default is what keeps every existing call site's behavior byte-identical. *) -let parse_method ?(pub = false) (st : state) : Ast.method_decl = +let parse_method ?(pub = false) ?(is_static = false) (st : state) : Ast.method_decl = let h = parse_sig_head st in let body = parse_block st in - { Ast.id = h.s_id; pos = h.s_pos; name = h.s_name; params = h.s_params; ret = h.s_ret; body; pub } + { Ast.id = h.s_id; pos = h.s_pos; name = h.s_name; params = h.s_params; ret = h.s_ret; body; pub; + is_static } (* A free top-level function is grammatically identical to a class method (signature + brace-delimited body span) — Task 6's brief @@ -1383,13 +1420,32 @@ let parse_class_or_type ?(pub = false) (st : state) (ann : type_annotations) : A | Token.Eof -> fail st (peek_pos st) syntax_code "unexpected end of input inside type/class body" | Token.KwFn -> methods := parse_method st :: !methods - (* bare `const` only — `static const` (Task 7's `static`) is not - recognized here at all: `static` lexes as a plain Ident, matches - none of this loop's arms (not looks_like_field: the next token is - `const`, not a Colon), and falls through to the same clean - "expected a field, method, or ..." error every other unrecognized - class-body construct gets — no half-swallow, per the brief. *) | Token.KwConst -> consts := parse_const_decl st :: !consts + (* haxe-parity Task 7: `static`. It is not a keyword (it lexes as a + plain Ident, so `static` stays a usable identifier elsewhere) — + one token of lookahead separates the marker from a field named + `static`, whose next token is a Colon. `static const` is a + class-scoped constant: the same substitution a bare class `const` + already gets, so both spellings land in the same list. `static fn` + carries the marker into the method record. *) + | Token.Ident "static" when (tok_at st (st.pos + 1)).kind = Token.KwConst -> + ignore (advance st); + consts := parse_const_decl st :: !consts + | Token.Ident "static" when (tok_at st (st.pos + 1)).kind = Token.KwFn -> + ignore (advance st); + methods := parse_method ~is_static:true st :: !methods + (* haxe-parity Task 7: `pub(read) field: T` — read-public, write- + private. `pub` with no `(read)` is not class-member grammar (member + visibility beyond this one accessor form is undesigned), so the + error names what is accepted. *) + | Token.KwPub -> + ignore (advance st); + expect st Token.LParen "'(' after `pub` in a class body — the only member form is `pub(read)`"; + (match peek st with + | Token.Ident "read" -> ignore (advance st) + | _ -> unexpected st "`read` — the only accessor form is `pub(read)`"); + expect st Token.RParen "')'"; + fields := parse_field ~pub_read:true st :: !fields (* looks_like_field MUST be checked before is_sync_ident: `on`/ `service`/`policy` are plain Idents here (Task 3 deliberately kept them as usable identifiers, unlike rt where they're real keywords @@ -1672,6 +1728,8 @@ let rec subst_expr (consts : Ast.expr StringMap.t) (bound : StringSet.t) (e : As | Ast.Ctor (cn, fields) -> { e with Ast.kind = Ast.Ctor (cn, List.map (fun (n, v) -> (n, subst_expr consts bound v)) fields) } | Ast.Interp inner -> { e with Ast.kind = Ast.Interp (subst_expr consts bound inner) } + | Ast.ListLit items -> { e with Ast.kind = Ast.ListLit (List.map (subst_expr consts bound) items) } + | Ast.MapLit -> e | Ast.Switch (subject, arms) -> { e with Ast.kind = diff --git a/compiler/src/types.ml b/compiler/src/types.ml index d50e54e..029aee5 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -147,6 +147,16 @@ let find_variant_in (unions : union_info StringMap.t) (name : string) : let find_variant (syms : symbols) (name : string) : (union_info * variant_info) option = find_variant_in syms.unions name +(* haxe-parity Task 7: the static member behind `Flock.held(path)` — a + qualified name whose head is a class, not a value. Instance methods are + deliberately excluded: `Cls.method(...)` on a non-static method is not + a call with an implicit receiver, it is an error, and returning None + here is what lets the caller say so. *) +let static_method_of (syms : symbols) (cls_name : string) (m_name : string) : method_info option = + match StringMap.find_opt cls_name syms.classes with + | Some cls -> List.find_opt (fun (m : method_info) -> m.name = m_name && m.is_static) cls.methods + | None -> None + (* Builtin scalars *) let builtin_scalars = ["Int"; "Bool"; "Text"; "Timestamp"; "Id"] @@ -332,7 +342,7 @@ let collect_declarations ~file (prog : program) (collector : Diag.Collector.t) : ret = m.ret; body = m.body; mutates = false; - is_static = false; + is_static = m.is_static; id = m.id; pos = m.pos; } ) (c.methods : Ast.method_decl list) in @@ -828,6 +838,12 @@ let typecheck_program ~file ~(module_of : string -> string) | IntLit _ -> Some (TScalar "Int") | StrLit _ -> Some (TScalar "Text") | BoolLit _ -> Some (TScalar "Bool") + (* A non-empty list literal is confident about its element type; an + empty `[]`/`{}` is contextual on its destination, exactly like + `multi_new()`/`map_new()` above. *) + | ListLit (first :: _) -> ( + match confident_typ cenv first with Some t -> Some (TMulti t) | None -> None) + | ListLit [] | MapLit -> None | Ident name -> StringMap.find_opt name cenv | Field (base, field_name) -> ( match confident_typ cenv base with @@ -893,7 +909,18 @@ let typecheck_program ~file ~(module_of : string -> string) match s.ret with Some ft -> Some (resolve_field_ty ft) | None -> Some TVoid) | None -> None) | None -> None)) - | _ -> None) + | _ -> ( + (* haxe-parity Task 7: a static call (`Flock.held(path)`). + The base names a class, so it has no confident *value* + type above — only this shape reaches here with a + resolvable member. *) + match base.kind with + | Ident cls_name -> ( + match static_method_of syms cls_name mname with + | Some m -> ( + match m.ret with Some ft -> Some (resolve_field_ty ft) | None -> Some TVoid) + | None -> None) + | _ -> None)) | _ -> None) | Ctor (class_name, _fields) -> (* `Ctor`'s class name is never a placeholder -- unlike @@ -1133,6 +1160,15 @@ let typecheck_program ~file ~(module_of : string -> string) { typ = TScalar "Int"; is_nil = false }) | DbStub _ -> { typ = TVoid; is_nil = false } | Switch (subject, arms) -> typecheck_switch ~want_value:true env cenv subject arms + | ListLit items -> + let elem_types = List.map (fun i -> (typecheck_expr env cenv i).typ) items in + (* Same contextual answer the builtin container constructors give + when the destination is what decides (see confident_typ): an + empty literal has no element type to report. *) + (match elem_types with + | t :: _ -> { typ = TMulti t; is_nil = false } + | [] -> { typ = TScalar "Int"; is_nil = false }) + | MapLit -> { typ = TScalar "Int"; is_nil = false } (* haxe-parity Task 3: the one deriver behind both `Switch` call sites -- `typecheck_expr`'s own case above (every "the value is used" @@ -1506,14 +1542,21 @@ let typecheck_program ~file ~(module_of : string -> string) and typecheck_stmt ((env, cenv) : typ StringMap.t * typ StringMap.t) (s : stmt) : typ StringMap.t * typ StringMap.t = match s.s_kind with - | Let { name; ty = _ty; value } -> + | Let { name; ty; value } -> let val_res = typecheck_expr env cenv value in + (* A written annotation is the authority — it is the only thing + that types a contextual value (`[]`, `{}`, `nil`), and for + everything else it is what the author declared the binding to + be. Only an unannotated `let` falls back to inference. *) + let declared = Option.map resolve_field_ty ty in + let bound_typ = match declared with Some t -> t | None -> val_res.typ in let new_cenv = - match confident_typ cenv value with - | Some t -> StringMap.add name t cenv - | None -> StringMap.remove name cenv + match (declared, confident_typ cenv value) with + | Some t, _ -> StringMap.add name t cenv + | None, Some t -> StringMap.add name t cenv + | None, None -> StringMap.remove name cenv in - (StringMap.add name val_res.typ env, new_cenv) + (StringMap.add name bound_typ env, new_cenv) | Assign { target; value } -> let _ = typecheck_expr env cenv target in let _ = typecheck_expr env cenv value in @@ -1753,6 +1796,8 @@ and walk_expr (bound : StringSet.t) (visit : StringSet.t -> expr -> unit) (e : e walk_expr bound visit r | Ctor (_, fields) -> List.iter (fun (_, v) -> walk_expr bound visit v) fields | Interp inner -> walk_expr bound visit inner + | ListLit items -> List.iter (walk_expr bound visit) items + | MapLit -> () | DbStub _ -> () | Switch (subject, arms) -> walk_expr bound visit subject; 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 bd81418..2c8d9aa 100644 --- a/docs/plan/compiler/2026-08-01-haxe-parity-language.md +++ b/docs/plan/compiler/2026-08-01-haxe-parity-language.md @@ -6,6 +6,8 @@ > > **Style rule (user convention):** concept, reason, and required behavior in words only; the executor writes the code. +**Spec:** [`docs/superpowers/specs/2026-08-01-systems-track-design.md`](../../superpowers/specs/2026-08-01-systems-track-design.md) (Part 1 verdict table, normative), amended by [`docs/superpowers/specs/2026-08-10-logwatcher-gap-closure-design.md`](../../superpowers/specs/2026-08-10-logwatcher-gap-closure-design.md). + **Goal:** Plan 8 — implement every **adopt** row of the systems-track spec's Haxe keyword verdict table: the language grows boolean operators (`and`/`or`), switch expressions, records, optionals, try/catch, enum payloads, static members, using-extensions, modules, `pub(read)`, build flags, and interpolation — with the reject rows enforced as diagnostics. (`abstract` was an adopt row until 2026-08-10; it is now a reject row. `is` was cut the same day — 0 uses in the driving workload, parked post-iteration-12 — emptying this plan's old Task 7, which is deleted rather than deferred.) **Architecture:** Plan 8 of the roadmap. Depends on OOP plans 1–3 (woc + wovm + corpus). Overwhelmingly compiler work in `compiler/src/`; the VM changes are exactly three, called out in their tasks: catch frames (try/catch), variant objects (enum payloads), and boxed optionals for scalars. Everything else lowers onto existing opcodes. The spec's verdict table (`docs/superpowers/specs/2026-08-01-systems-track-design.md` Part 1) is normative — this plan sequences it.