feat(compiler): let-type annotations, container literals, statics, pub(read)
Driving goal: compile docs/examples/log-watcher (1285 lines of .wo).
Diagnostics on that program: 481 -> 379; parse errors 85 -> 18.
tests/corpus via scripts/oop-e2e.sh stays 71 checks / 0 failures.
- ast.ml: `Let.ty` is a full `field_ty` (was a bare name), so `multi Text`,
`map<K, V>` and `?T` annotate a local; new `ListLit`/`MapLit` expressions;
`method_decl.is_static`; `field.pub_read`
- parser.ml: let annotations go through parse_field_ty; `[]`/`[a, b]` and
`{}` literals in expression position; `static const`/`static fn` in class
bodies (one token of lookahead — `static` stays an identifier);
`pub(read) field: T`; a `;` ends a statement on its own, so one-line
guard bodies (`{ skip(...); return; }`) parse
- emit.ml: list/map literals lower to multi_new/map_new + one multi_push per
element, element kinds from the destination's declared type (WO-E403 with
no typed destination, same rule multi_new already had); `static fn` gets a
receiverless method record (arg_cnt = params) and lowers `Cls.fn(args)`
through emit_direct with no `self`; a static can satisfy no interface
- types.ml: a written `let` annotation is now the authority for the binding's
type (the only thing that types `[]`/`{}`/`nil`); `static_method_of` +
static-call return types; method_info.is_static is wired from the AST
- owner.ml: container literals are fresh owned values, elements read in place
(inherits push()'s open borrowed-element gap, noted in the code)
- plan doc: `Spec:` header line so planboard's lint accepts the plan
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e966f54ecf
commit
2a98186259
7 changed files with 280 additions and 30 deletions
|
|
@ -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<K, V>`. 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<Text, Text> = {}` 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
|
||||
|
|
|
|||
|
|
@ -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) ]
|
||||
|
|
|
|||
|
|
@ -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<K, V>` — 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue