From 3e68089cde2a4da430af755013349537369fcb05 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 19 Sep 2026 05:47:49 +0700 Subject: [PATCH 1/7] A parameter with no type is dyn, decided where the type names are all known The type itself, the ABI its operations call into, and the one decision the feature could not avoid: (defn f [x y]) is one parameter or two, and which one depends on whether y names a type. Parse does not decide it. That lookup is the one its defn comment records being removed for being wrong twice in one day -- the set of type names is incomplete at parse time by construction, and macros generating definitions is what widened the failure. So the vector is carried undecided, as Ast.pitems, and paired in Check, after every file is loaded, every macro expanded and every header imported. The set is complete there. It is not complete across time, and the comment says so: a defstruct written later changes a signature with no edit to the function. The return slot stays mandatory and dyn is written out in it. The ambiguity there has no syntactic resolution at all -- a capitalised head in a list is both a type application and a struct literal -- so the third state the parameters needed does not exist for the return type, and ret = None goes on meaning Unit. What the feature costs, and what is taken back: a slot with no type used to be a syntax error, so a mistyped type now reads as an extra parameter with no diagnostic. A name within one edit of a type's gets the resolver's own did-you-mean, and an unknown capitalised name is reported as the unknown type it is -- not one parameter in the corpus is capitalised. A lowercase name resembling no type is the feature working, and is the residual. The x86 backend refuses dyn by name; both callers already name --llvm, and the daemon takes that backend by default, so this is the first thing a user of dyn sees. The JS dialect refuses it too, for the opposite reason -- every value there is already dynamic and what is missing is only the lowering. runtime/flan_dyn.h is the fixed ABI. flan_dyn_stub.c stands in until the real collector lands and says in its header that it verifies nothing about roots. --- lib/ast.ml | 20 +++ lib/check.ml | 146 +++++++++++++++++- lib/cimport.ml | 2 +- lib/emit.ml | 15 ++ lib/js.ml | 10 ++ lib/load.ml | 43 +++++- lib/parse.ml | 75 +++++++++- lib/types.ml | 12 ++ lib/x86.ml | 17 +++ raylib-imported | Bin 0 -> 80192 bytes runtime/flan_dyn.h | 110 ++++++++++++++ runtime/flan_dyn_stub.c | 317 ++++++++++++++++++++++++++++++++++++++++ test/test_flan.ml | 31 +++- 13 files changed, 783 insertions(+), 15 deletions(-) create mode 100755 raylib-imported create mode 100644 runtime/flan_dyn.h create mode 100644 runtime/flan_dyn_stub.c diff --git a/lib/ast.ml b/lib/ast.ml index e80f8ae..76e941b 100644 --- a/lib/ast.ml +++ b/lib/ast.ml @@ -106,6 +106,19 @@ and rclause = restart clause's parameters are one, and a clause is part of an expression. *) and field = { fname : string; fty : texpr; floc : Loc.t } +(* One slot of a [defn]'s parameter vector, before it is known whether the slot + is a name or a type. [(defn f [x y] ...)] is two dyn parameters if [y] is + not a type and one parameter [x : y] if it is, and the parser cannot tell: + the type names are not all known until macros have run and every file has + been loaded. So the vector is carried undecided and paired in [Check], where + the set is complete. See the argument in Parse beside the [defn] case. *) +and pitem = + (* A bare symbol: either a parameter's name or a type's. *) + | Pname of string * Loc.t + (* Anything that cannot be a parameter name — [(Ptr T)], [[T]], [[n T]], [()] + — and so is a type whatever the environment says. *) + | Ptype of texpr + (* Two unwrap operators, because they are two different things — plan.org. *) and unwrap = Usome | Utry @@ -136,6 +149,13 @@ type pred = { pname : string; pvar : string; ploc : Loc.t } type fn = { name : string; params : field list; + (* [Some items] means the parameter vector has not been paired yet: it was + written by a [defn], where a slot with no type means [dyn], and [Check] + fills [params] from it before anything reads them. [None] is every other + way a signature is built — [declare], the shim, the C importer — where + every parameter's type was written out and the pairing was never in + doubt. Nothing downstream of [Check.pair_params] sees [Some]. *) + praw : pitem list option; ret : texpr option; (* None means (); only declare omits it *) (* The [{:where ...}] map at the head of the body, already unpacked. Empty for every function that has none, which is every function that is not diff --git a/lib/check.ml b/lib/check.ml index 9d7b1df..73706d7 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -707,6 +707,14 @@ and resolve_name env ~seen loc n = match n with | "bool" -> Types.Bool | "string" -> Types.String + (* Lowercase and concrete, which the rule three screens down says is a + type variable. It is spelled this way because it is a primitive and + every other primitive is lowercase — [dyn] beside [i64] and [bool] + reads as one of them, [Dyn] beside [Vec] and [Option] reads as a + container over something. The type-variable rule is reached by a + [when] guard below and this arm is before it, so the spelling costs + nothing but the note. *) + | "dyn" -> Types.Dyn | "Unit" -> Types.Unit | "Never" -> Types.Never (* A builtin opaque type, the way [string] is a builtin ptr+len. There is @@ -752,6 +760,127 @@ and array_len env loc = function fail loc "%s is not a compile-time integer constant, so it cannot be \ an array length" n) +(* ── Pairing a defn's parameter vector ────────────────────────────────── + + [(defn f [x y] ...)] is one parameter [x] of type [y] if [y] names a type, + and two parameters of type [dyn] if it does not. Parse could not tell — the + long argument is beside its [defn] case — so it handed over the slots + undecided and this is where they are paired, with every type name in hand: + every file loaded, every macro expanded, every C header imported. + + The walk is left to right and takes two slots or one. A name followed by + something that is a type takes two and is annotated; a name followed by + another name that is not a type, or by nothing, takes one and is [dyn]. That + is the whole rule, and it reads the way the vector reads. + + A name that *is* a type name is refused rather than paired. [(defn f [i64 x] + ...)] has no good reading: taken as written it is a parameter called [i64], + which shadows nothing but confuses everything, and the likelier intent is a + pair written backwards. Refusing here costs a rename in the one program that + meant it and closes the one place where this rule could still hand somebody + a signature they did not write. *) +let is_type_name env n = + Types.ikind_of_name n <> None + || Types.fkind_of_name n <> None + || List.mem n [ "bool"; "string"; "dyn"; "Unit"; "Never"; "Allocator" ] + || Hashtbl.mem env.aliases n + || Hashtbl.mem env.structs n + || Hashtbl.mem env.datas n + || Hashtbl.mem env.unions n + || Hashtbl.mem env.enums n + (* A type variable: [$t] in a signature is generics' binding site, and a + slot holding one is a type however few of them there are. *) + || (n <> "" && n.[0] = '$') + +(* Before a bare symbol is allowed to become an unannotated parameter, the two + ways it is more likely to be a type that went wrong. + + This is the cost dynamic-by-default puts on the parameter vector, and it is + worth naming plainly: a slot with no type used to be a syntax error, and now + it is a [dyn] parameter. So [(defn f [x f65] ())] — a typo for [f64] — no + longer reads as a mistyped type. It reads as two parameters, one of them + called [f65], and the function silently takes an argument nobody meant to + give it. An arity that changes because of a typo, with no diagnostic, is the + failure class Parse's [defn] comment calls the worst available, and the + feature reintroduces it in a new place. + + Two rules take most of it back. A name within one edit of a type's name is + the typo it looks like, and is refused with the same "did you mean" the + resolver gives — the near-miss table is already there and is exactly the + right question. And a capitalised name is a type by the convention the whole + corpus keeps: not one parameter in the language is capitalised, while [Form], + [Cursor], [Vector2] and the rest appear in these vectors constantly. So an + unknown capitalised name is an unknown *type*, reported as one, rather than + a parameter nobody would have spelled that way. + + What is left uncovered is a lowercase name that resembles no type: [(defn f + [x widget] ())] is two dyn parameters and there is no evidence in the text + that it was meant to be one. That case is the feature working as specified, + and it is the residual the parent owns. *) +let dyn_param_or_typo env n loc = + match near_miss env n with + | Some m -> + Loc.failk "check/unknown-type" loc + "unknown type %s — did you mean %s? A parameter with no type is dyn, so \ + this would otherwise be read as a second parameter called %s" + n m n + | None -> + if n <> "" && n.[0] = Char.uppercase_ascii n.[0] + && n.[0] <> Char.lowercase_ascii n.[0] + then + Loc.failk "check/unknown-type" loc + "unknown type %s. A capitalised name in a parameter vector is a type — \ + a parameter with no type is dyn, and parameters are lowercase" + n + +let pair_params env (items : Ast.pitem list) : Ast.field list = + let dyn loc = { Ast.t = Ast.Tname "dyn"; tloc = loc } in + let rec go = function + | [] -> [] + | Ast.Ptype t :: _ -> + Loc.failk "check/parameter-name-expected" t.Ast.tloc + "a parameter's name was expected here, and this is a type. \ + Parameters are [name Type ...], and a name with no type is dyn" + | Ast.Pname (n, loc) :: rest when is_type_name env n -> + ignore rest; + Loc.failk "check/parameter-named-type" loc + "%s names a type, so it cannot also be this parameter's name. If the \ + pair was written backwards it is [name %s]; otherwise rename the \ + parameter" n n + | Ast.Pname (n, loc) :: Ast.Ptype t :: rest -> + { Ast.fname = n; fty = t; floc = loc } :: go rest + | Ast.Pname (n, loc) :: Ast.Pname (t, tloc) :: rest when is_type_name env t -> + { Ast.fname = n; fty = { Ast.t = Ast.Tname t; tloc }; floc = loc } :: go rest + (* The slot after this one is not a type, so this one is a parameter with + no type written — unless the slot after it only *looks* unlike a type + because it was mistyped, which is what the check is for. The next slot + is the one interrogated, not this one: this one is a name either way. *) + | Ast.Pname (n, loc) :: (Ast.Pname (t, tloc) :: _ as rest) -> + dyn_param_or_typo env t tloc; + { Ast.fname = n; fty = dyn loc; floc = loc } :: go rest + | Ast.Pname (n, loc) :: rest -> + { Ast.fname = n; fty = dyn loc; floc = loc } :: go rest + in + go items + +(* Every [defn] in the program, with its parameter vector paired. Run as a pass + of its own, after the type names are registered and before any signature is + resolved, so that nothing downstream ever sees an unpaired one. *) +let pair_decls env (decls : Ast.decl list) : Ast.decl list = + let fn (f : Ast.fn) = + match f.Ast.praw with + | None -> f + | Some items -> { f with Ast.params = pair_params env items; praw = None } + in + List.map + (fun (d : Ast.decl) -> + match d.Ast.d with + | Ast.Defn f -> { d with Ast.d = Ast.Defn (fn f) } + | Ast.Declare (f, c) -> { d with Ast.d = Ast.Declare (fn f, c) } + | Ast.DeclareC (f, c) -> { d with Ast.d = Ast.DeclareC (fn f, c) } + | _ -> d) + decls + (* ── Generics: the four operations monomorphisation needs ─────────────── Naming a variable, binding one from an argument, substituting the binding back in, and spelling the result as a symbol. Everything else about the @@ -5539,6 +5668,13 @@ let collect env (decls : Ast.decl list) = Hashtbl.replace env.locs n d.Ast.dloc | _ -> ()) decls; + (* Every type name is registered by here — structs, data types and unions by + the names-first pass, aliases with them, enums by the pass just above — so + this is the first point at which a [defn]'s parameter vector can be paired. + It is done before the signature loop below rather than inside it, because a + signature may name a type declared further down and pairing must not depend + on the order the file was written in. *) + let decls = pair_decls env decls in List.iter (fun (d : Ast.decl) -> let loc = d.Ast.dloc in @@ -5742,7 +5878,13 @@ let collect env (decls : Ast.decl list) = if progressed && left <> [] then settle () in settle (); - List.iter (fun c -> ignore (infer c)) !pending + List.iter (fun c -> ignore (infer c)) !pending; + (* The paired declarations, handed back so that pass two checks the bodies of + the same functions whose signatures this pass registered. Pairing needs the + type names, which only this pass has; every pass after it needs the result, + and a [defn] still carrying an unpaired vector would check as a function of + no parameters at all. *) + decls (* A type that contains itself by value has no finite size. [(Ptr T)] and a slice are indirections and break the cycle; a fixed array does not, because @@ -6460,7 +6602,7 @@ let build_program ~keep_going (decls : Ast.decl list) : Tast.program * env = time it runs every signature is sound, so a body that fails to check cannot make the next body fail — which is what makes a declaration a resync point that needs no resynchronising. *) - collect env decls; + let decls = collect env decls in check_finite env; check_union_members env; let s = Loc.sink ~on:keep_going in diff --git a/lib/cimport.ml b/lib/cimport.ml index bc63572..e4dad44 100644 --- a/lib/cimport.ml +++ b/lib/cimport.ml @@ -813,7 +813,7 @@ let of_dump ~env ~taken ~bound_syms ~config (d : dump) : imported = decls := { Ast.d = Ast.DeclareC - ({ Ast.name = flan; params; ret; fwhere = []; fbody = []; nloc = f.cloc }, + ({ Ast.name = flan; params; praw = None; ret; fwhere = []; fbody = []; nloc = f.cloc }, f.csym); dloc = f.cloc } :: !decls) diff --git a/lib/emit.ml b/lib/emit.ml index bc82da5..8cbc0f9 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -125,6 +125,13 @@ let rec ll (t : Types.t) = and a copy in the IR are the right number of bytes. *) | Types.Map _ -> "%map" | Types.Option e -> Printf.sprintf "{ i8, %s }" (ll e) + (* One word, and [i64] rather than a pointer type: runtime/flan_dyn.h says + [typedef uint64_t flan_dyn], and the IR agreeing with that typedef is the + whole of what keeps the two sides linkable. Nothing here ever loads + through it — a dyn word is only ever passed to a flan_dyn_* call — so the + integer spelling costs no casts and keeps the emitter honest about not + knowing whether the bits are a pointer. *) + | Types.Dyn -> "i64" | Types.Var _ -> (* The checker rejects it by name — nothing reaches here. *) failwith ("no layout for " ^ Types.to_string t) @@ -318,6 +325,7 @@ let rec lay m (t : Types.t) : int * int = match Hashtbl.find_opt m.unions n with | Some u -> union_lay m u | None -> failwith ("no layout for struct " ^ n)) + | Types.Dyn -> 8, 8 | Types.Var _ -> failwith ("no layout for " ^ Types.to_string t) (* Size, alignment, and the offset of every member. *) @@ -543,6 +551,13 @@ let rec dty m d (t : Types.t) : int = "!DIDerivedType(tag: DW_TAG_pointer_type, name: \"%s\", \ baseType: null, size: 64)" (Types.to_string t)) + (* An unsigned word, which is what the typedef says it is. Telling lldb + it is a pointer would be a guess about the encoding the compiler has + deliberately not made, and telling it nothing would leave [p x] on a + dyn local with no answer at all. A raw word is the true and useful + reading: it prints, and the person reading it can hand it to the + runtime's own printer. *) + | Types.Dyn -> basic "dyn" 64 "DW_ATE_unsigned" | Types.Var _ -> failwith ("no debug type for " ^ Types.to_string t) in diff --git a/lib/js.ml b/lib/js.ml index c2c8922..018977b 100644 --- a/lib/js.ml +++ b/lib/js.ml @@ -235,6 +235,16 @@ let rec refuse_ty loc (t : Types.t) = "(Map K V) is not in the JS dialect yet — Odin's open-addressed map is a \ type-erased runtime over raw bytes and the JS answer is a Map keyed by \ a structural key, which is its own lane" + (* The irony is not lost: JavaScript is the one target where a dyn value + needs no boxing at all, because every value there is already one. What is + missing is not the representation but the lowering — dyn ops are calls + into runtime/flan_dyn.h, and this dialect has no such runtime. It is a + lane, not a difficulty. *) + | Types.Dyn -> + at loc + "dyn is not in the JS dialect yet — every JavaScript value is already \ + dynamic, so this is a matter of lowering the dyn operations onto the \ + host's own, and that work has not been done" | Types.Var n -> at loc "a type variable (%s) reached the backend, which cannot happen" n diff --git a/lib/load.ml b/lib/load.ml index e95b073..aadf66a 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -302,6 +302,20 @@ and rename_place owned alias bound (p : Ast.place) : Ast.place = let rename_field owned alias (f : Ast.field) : Ast.field = { f with Ast.fty = rename_texpr owned alias f.Ast.fty } +(* An unpaired parameter vector, qualified. The slots are still undecided here + — [Check] is what pairs them — so a bare symbol might be a parameter's name + or a type's, and this cannot tell. It does not have to: [owned] holds the + package's *declared* names, a parameter's name is not one of them, and a + parameter named after a type of the same package is refused outright when + the vector is paired. So qualifying every owned name and leaving every other + alone is right for both readings, and stays right because that refusal is + what keeps the two sets apart. *) +let rename_pitem owned alias (p : Ast.pitem) : Ast.pitem = + match p with + | Ast.Pname (n, loc) when List.mem n owned -> Ast.Pname (qualify alias n, loc) + | Ast.Pname _ -> p + | Ast.Ptype t -> Ast.Ptype (rename_texpr owned alias t) + let qualify_decl owned alias (d : Ast.decl) : Ast.decl = let loc = d.Ast.dloc in let k = @@ -346,11 +360,26 @@ let qualify_decl owned alias (d : Ast.decl) : Ast.decl = | other -> other)) | Ast.Defn fn -> let params = List.map (rename_field owned alias) fn.Ast.params in - let bound = List.map (fun (p : Ast.field) -> p.Ast.fname) fn.Ast.params in + let praw = Option.map (List.map (rename_pitem owned alias)) fn.Ast.praw in + (* The names a body may shadow. With the vector still unpaired, every + bare symbol in it is a candidate — an owned one is a type and is + dropped, because it is a name the body should go on qualifying, and + what is left is the parameter names and at worst a type of some other + package, which no body of this one refers to as a value. *) + let bound = + match praw with + | Some items -> + List.filter_map + (function + | Ast.Pname (n, _) when not (List.mem n owned) -> Some n + | _ -> None) + items + | None -> List.map (fun (p : Ast.field) -> p.Ast.fname) fn.Ast.params + in Ast.Defn { fn with Ast.name = qualify alias fn.Ast.name; - params; + params; praw; ret = Option.map (rename_texpr owned alias) fn.Ast.ret; fbody = List.map (rename_expr owned alias bound) fn.Ast.fbody } | Ast.Package _ -> Ast.Package alias @@ -634,6 +663,16 @@ let decl_uses acc (d : Ast.decl) = let field (f : Ast.field) = texpr_uses acc f.Ast.fty in let fn (f : Ast.fn) = List.iter field f.Ast.params; + (* An unpaired vector names types too, and a bare symbol in it may be one. + Every such symbol is recorded as a use: a parameter's name recorded here + resolves to nothing and costs nothing, where a type's name left out + would drop a real dependency and the import would not be loaded. Over- + recording is the safe direction for a dependency set. *) + Option.iter + (List.iter (function + | Ast.Pname (n, loc) -> acc := (n, loc) :: !acc + | Ast.Ptype t -> texpr_uses acc t)) + f.Ast.praw; Option.iter (texpr_uses acc) f.Ast.ret; List.iter (expr_uses acc) f.Ast.fbody in diff --git a/lib/parse.ml b/lib/parse.ml index b1007c8..ab9d9d1 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -111,6 +111,29 @@ let rec fields (f : Form.t) (items : Form.t list) : Ast.field list = Loc.fail odd.loc "field %s has no type — these come in name/type pairs" (Form.to_string odd) +(* A [defn]'s parameter vector, left undecided — the long argument is beside + the [defn] case. A bare symbol could be either half of a pair and is carried + as one; everything else is a type by its shape alone, and is resolved now so + that a malformed type is still reported at the character that is wrong. + + [fields] applies [no_pattern] to the name half of each pair, and this cannot: + which half a slot is has not been decided. A map is the one shape that can be + settled here anyway — braces are not a type in any position ([texpr] refuses + them), so a map in this vector is a destructuring pattern and nothing else, + and it gets the sentence that says so rather than a complaint about map type + syntax. A bracket cannot be settled the same way, because [[a b]] is a + pattern in a name slot and a slice type in a type slot; one written in a name + slot comes back from [Check] as "a parameter's name was expected here", which + is true and is as close as this can get. *) +and pitems (items : Form.t list) : Ast.pitem list = + List.map + (fun (it : Form.t) -> + match it.v with + | Sym s -> Ast.Pname (s, it.loc) + | Map _ -> no_pattern it; assert false + | _ -> Ast.Ptype (texpr it)) + items + (* ── The constraint map at the head of a defn body ────────────────────── [(defn sort [s [$t]] () {:where (ordered? $t)} body ...)]. Clojure's [{:pre [...] :post [...]}] is the precedent and the reason it is a map @@ -914,7 +937,47 @@ let rec decl (f : Form.t) : Ast.decl = Mandatory removes the guess: nothing is consulted, [()] is what a function that returns nothing writes, and a mistyped type is a mistyped type -- [(defn f [] f65 0.0)] reaches the resolver's near-miss check and comes back - as *did you mean f64*, where it used to come back as an unknown name. *) + as *did you mean f64*, where it used to come back as an unknown name. + + The return slot stays mandatory now that parameters may be left + unannotated, and it is worth saying why the two do not move together. + Dynamic-by-default means a *parameter* with no type is [dyn]; the return + type could have been given the same rule, and was not, because the + ambiguity there has no syntactic resolution at all. [(defn f [] (Rune + {.code 65}) (bar))] is the case above: a capitalised head in a list is a + type application and also a struct literal -- see [Struct] in [expr] -- + and no rule separates them, so an optional return slot is a coin toss + between a type and the first form of a body. A parameter vector has no + such case: every slot in it is a name or a type and never an expression. + So [dyn] is written out in the return position, which costs one token and + keeps a decision this file paid for twice in one day. + + ── The parameter vector ────────────────────────────────────────────── + + [(defn f [x y])] is one parameter [x] of type [y], or two parameters [x] + and [y] of type [dyn], and which one it is depends on whether [y] names a + type. That is the lookup this comment's first half says was removed for + being brittle, and it is being asked for again -- so it is not done here. + The vector is carried undecided, as [Ast.pitem]s, and paired in [Check], + where the set of type names is complete. + + The move is not cosmetic. What the old rule got wrong was consulting a set + that was not finished being built: it ran per-file, at parse time, before + macros had generated their definitions, and macros generating definitions + is exactly what widened the failure. By the time [Check] pairs the vector, + every file is loaded, every macro has expanded and every C header has been + imported, so the set is not a guess about what might be a type -- it is + the types. That is strictly more than the parser could ever know, and it + is the whole of the argument for the placement. + + What deferring does not buy is immunity. The set is complete at a point in + time and not across time: [(defn f [x y] ...)] is two dyn parameters until + somebody writes [(defstruct y ...)] or imports a header that declares one, + and then it is one parameter of type [y], with no edit to [f]. The + signature changes under it. That residual is real, it is the dictated + rule's and not this file's, and [Session.compatible] is where it is felt -- + a redefinition that changes a signature is refused there, and this is a + way for a signature to change with nothing redefined. *) | List ({ v = Sym "defn"; _ } :: args) -> (match args with | n :: { v = Vec ps; _ } :: ret :: body -> @@ -930,7 +993,7 @@ let rec decl (f : Form.t) : Ast.decl = function that returns nothing writes ()" msg in let fwhere, body = constraints body in - mk (Ast.Defn { Ast.name = sym n; params = fields f ps; + mk (Ast.Defn { Ast.name = sym n; params = []; praw = Some (pitems ps); ret = Some rty; fwhere; fbody = body_of body; nloc = n.loc }) | _ -> @@ -961,10 +1024,10 @@ let rec decl (f : Form.t) : Ast.decl = | { v = Str csym; _ } :: rest -> (match List.rev rest with | [ n; { v = Form.Vec ps; _ } ] -> - mk (mkd { Ast.name = sym n; params = fields f ps; + mk (mkd { Ast.name = sym n; params = fields f ps; praw = None; ret = None; fwhere = []; fbody = []; nloc = n.loc } csym) | [ n; { v = Form.Vec ps; _ }; r ] -> - mk (mkd { Ast.name = sym n; params = fields f ps; + mk (mkd { Ast.name = sym n; params = fields f ps; praw = None; ret = Some (texpr r); fwhere = []; fbody = []; nloc = n.loc } csym) | _ -> fail f "%s" usage) @@ -1140,6 +1203,10 @@ let rec decl (f : Form.t) : Ast.decl = params = [ { Ast.fname = sym p; fty = { Ast.t = Ast.Tslice form_t; tloc = p.loc }; floc = p.loc } ]; + (* Written out, not deferred: a macro takes [[Form]] and + returns a [Form], and neither half of that is the user's to + leave off. *) + praw = None; ret = Some form_t; fwhere = []; fbody = body_of body; nloc = n.loc }) | _ :: { v = Form.Vec ps; _ } :: body when body <> [] -> diff --git a/lib/types.ml b/lib/types.ml index 6c93489..f8f218a 100644 --- a/lib/types.ml +++ b/lib/types.ml @@ -47,6 +47,17 @@ type t = | Option of t (* (Option T) *) | Fn of t list * t (* (Fn [T ...] R) *) | Var of string (* a type variable — milestone 5 *) + (* [dyn]: one machine word whose contents the runtime knows and this module + does not. It is a written type — [(defvar x dyn 5)] boxes the 5 — and it + is also what an unannotated [defn] parameter means, which is why it is a + case here and not a Named type the prelude declares: the checker has to + recognise it to choose the boxing and the dyn op lowering, and a name in a + table cannot be matched on. + + Nothing about the representation is stated here on purpose. The word is + opaque to the compiler — runtime/flan_dyn.h owns which bits are a tag — + so that milestone 2 can change the encoding without touching Emit. *) + | Dyn let signed = function | I8 | I16 | I32 | I64 -> true @@ -121,6 +132,7 @@ let rec to_string = function Printf.sprintf "(Fn [%s] %s)" (String.concat " " (List.map to_string ps)) (to_string r) | Var n -> n + | Dyn -> "dyn" let is_numeric = function Int _ | Float _ -> true | _ -> false diff --git a/lib/x86.ml b/lib/x86.ml index 008ed21..9053482 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -475,6 +475,23 @@ let is_agg (t : Types.t) = | Types.Unit | Types.Never -> false | Types.String | Types.Slice _ | Types.Array _ | Types.Map _ | Types.Vec _ | Types.Option _ | Types.Named _ -> true + (* Refused by name rather than classified. A dyn word is one machine word and + would classify trivially — it is not the representation that is missing, + it is every operation on it, which is a call into runtime/flan_dyn.h that + this backend does not emit. Saying "a dyn value" here rather than letting + it through to fail at the first [+] means the reader is told the one true + thing about their program instead of something about an opcode. + + The sentence naming [--llvm] is not written here on purpose: both callers + add it, and each says it differently for a good reason — Session because + the daemon takes this backend by default and the reader chose a program + rather than a code generator, and main.ml only when [--x86] was not typed + out. Repeating it here would say it twice to the one reader and to the + wrong one. *) + | Types.Dyn -> + unsupported + "a dyn value. Every operation on one is a call into the dynamic runtime, \ + and this backend emits none of them" | Types.Var v -> unsupported "type variable %s" v let is_void (t : Types.t) = match t with Types.Unit | Types.Never -> true | _ -> false diff --git a/raylib-imported b/raylib-imported new file mode 100755 index 0000000000000000000000000000000000000000..ab898b253cf712dfcbbb6e8388a0ee748e539c31 GIT binary patch literal 80192 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 literal 0 HcmV?d00001 diff --git a/runtime/flan_dyn.h b/runtime/flan_dyn.h new file mode 100644 index 0000000..4dd5c38 --- /dev/null +++ b/runtime/flan_dyn.h @@ -0,0 +1,110 @@ +/* flan_dyn — the dynamic-value ABI. + * + * Milestone 1 of dynamic-by-default. A [flan_dyn] is one machine word, and + * every operation the compiler cannot type statically becomes a call to one of + * the functions below. The compiler emits these declarations from Emit; this + * header is the same contract written for C, and the two are diffed rather + * than trusted to agree. + * + * The word is opaque. Nothing outside the runtime may read a tag out of it, + * because which bits carry the tag is the runtime's business and milestone 2 + * moves them: the compiler only ever passes words it was given back to the + * functions here. That is what lets boxing change representation without a + * recompile of the emitter. + * + * Every function takes and returns scalars, for the reason flan_rt.c gives: + * nothing returns a struct by value, so the emitted .ll never has to agree + * with a platform's struct-return ABI. + */ + +#ifndef FLAN_DYN_H +#define FLAN_DYN_H + +#include + +typedef uint64_t flan_dyn; + +/* ── Construction ────────────────────────────────────────────────────── + * + * The typed-to-dyn direction. Integer literals in dyn context box as i64: + * there is one integer width behind a dyn value, so the defaulting question + * that a wider set of boxes would raise does not arise. + * + * [flan_dyn_nil] is the absent value, and is what an [if] with no else branch + * answers in dyn context. It is not Unit — Unit does not box, because a value + * of the zero-sized type carries nothing a dyn word could hold. */ +flan_dyn flan_dyn_nil(void); +flan_dyn flan_dyn_from_i64(int64_t v); +flan_dyn flan_dyn_from_f64(double v); +flan_dyn flan_dyn_from_bool(int32_t v); + +/* A string, as ptr+len — the shape [T] and string already have in Emit.ll. + * The runtime copies: the bytes behind a Flan string may be a literal in + * rodata or a slice of a buffer the program goes on to write. */ +flan_dyn flan_dyn_from_bytes(const uint8_t *ptr, int64_t len); + +/* The heterogeneous vector. In milestone 1 this is the runtime's own object + * rather than a Flan (Vec T) that happens to hold dyn words, which is why + * push/at/len on it go through the dyn ops below: the compiler knows only + * that it holds a dyn. */ +flan_dyn flan_dyn_vec_new(void); + +/* ── Operations ──────────────────────────────────────────────────────── + * + * Arithmetic dispatches on what the two words actually hold and traps through + * flan_trap_hook when they do not agree. The message is the runtime's: it is + * the side that knows which pair of types arrived, and a message assembled by + * the compiler could only name the static types, which are dyn and dyn. */ +flan_dyn flan_dyn_add(flan_dyn a, flan_dyn b); +flan_dyn flan_dyn_sub(flan_dyn a, flan_dyn b); +flan_dyn flan_dyn_mul(flan_dyn a, flan_dyn b); +flan_dyn flan_dyn_div(flan_dyn a, flan_dyn b); +flan_dyn flan_dyn_rem(flan_dyn a, flan_dyn b); + +/* The orderings answer a dyn holding a bool, not a C int: the result of an + * operation on dyn operands is a dyn, so that a comparison can be pushed into + * a heterogeneous vector like anything else. Where the compiler needs an i1 to + * branch on it follows with flan_dyn_need_bool. */ +flan_dyn flan_dyn_lt(flan_dyn a, flan_dyn b); +flan_dyn flan_dyn_le(flan_dyn a, flan_dyn b); +flan_dyn flan_dyn_gt(flan_dyn a, flan_dyn b); +flan_dyn flan_dyn_ge(flan_dyn a, flan_dyn b); + +/* Structural, and never traps. Two values of unrelated types are not an error + * to compare — they are unequal. This is the one op where a type mismatch has + * an answer instead of a trap, and = is the operator most likely to meet a + * heterogeneous container. */ +flan_dyn flan_dyn_eq(flan_dyn a, flan_dyn b); + +flan_dyn flan_dyn_len(flan_dyn v); +flan_dyn flan_dyn_at(flan_dyn v, flan_dyn i); +void flan_dyn_set_at(flan_dyn v, flan_dyn i, flan_dyn x); +void flan_dyn_push(flan_dyn v, flan_dyn x); +void flan_dyn_print(flan_dyn v); + +/* ── Extraction ──────────────────────────────────────────────────────── + * + * The dyn-to-typed direction, and the only one: a dyn reaches a typed slot + * through an annotation the programmer wrote — a typed parameter, a typed + * binding — and never by inference. A mismatch traps; the runtime owns the + * message for the reason given above. */ +int64_t flan_dyn_need_i64(flan_dyn v); +double flan_dyn_need_f64(flan_dyn v); +int32_t flan_dyn_need_bool(flan_dyn v); + +/* ── Roots ───────────────────────────────────────────────────────────── + * + * The collector is precise, so it has to be told where the live dyn words on + * the machine stack are. A dyn local or temporary that lives across a call or + * an allocation is pushed as a root at its binding and popped at scope exit, + * one [flan_dyn_root_pop] per scope carrying the count the scope pushed. + * + * The address is registered, not the word: the slot is written again while it + * is rooted, and a collection in between has to see the current value. */ +void flan_dyn_root_push(flan_dyn *slot); +void flan_dyn_root_pop(int64_t n); + +/* Called once from main before any other function here. */ +void flan_gc_init(void); + +#endif /* FLAN_DYN_H */ diff --git a/runtime/flan_dyn_stub.c b/runtime/flan_dyn_stub.c new file mode 100644 index 0000000..ae0d469 --- /dev/null +++ b/runtime/flan_dyn_stub.c @@ -0,0 +1,317 @@ +/* flan_dyn_stub — a standing-in implementation of the flan_dyn.h ABI. + * + * THE MERGE REPLACES THIS FILE WITH runtime/flan_dyn.c. It exists so that the + * compiler side of dynamic-by-default can be built and run against the fixed + * ABI before the real runtime lands; the real one is being written in parallel + * against the same header, and flan_dyn.h is the contract the two are diffed + * against. + * + * What it is not: it mallocs and never frees, it collects nothing, and + * flan_dyn_root_push / flan_dyn_root_pop record their arguments and do nothing + * with them. That last point matters for anyone reading a passing test here — + * root emission is *not* exercised by this file. A program with entirely wrong + * root discipline passes every test that runs against this stub. The check + * that does bite is the one over the emitted IR, counting pushes against pops + * per function; see the acceptance tests. + * + * The representation is the simplest thing that satisfies the header's rule + * that the word is opaque: every value is a pointer to a heap cell, including + * the small ones. The real runtime will not do this. + */ + +#include +#include +#include +#include +#include + +#include "flan_dyn.h" + +/* flan_rt.c's own [rt_trap] is static, so this mirrors it rather than calling + * it: print the sentence, offer the name to the dev daemon's hook, and leave + * with flan_rt's exit code so that a dyn trap is indistinguishable from any + * other trap to whoever is watching. The hook is flan_rt.c's global, and a + * program links both files. */ +extern void (*flan_trap_hook)(const uint8_t *name, int64_t namelen); + +static _Noreturn void dyn_trap(const char *name, const char *sentence) { + fflush(stdout); + fprintf(stderr, "%s\n", sentence); + fflush(stderr); + if (flan_trap_hook != NULL) + flan_trap_hook((const uint8_t *)name, (int64_t)strlen(name)); + _exit(134); +} + +enum tag { T_NIL, T_I64, T_F64, T_BOOL, T_STR, T_VEC }; + +typedef struct cell { + enum tag tag; + union { + int64_t i; + double f; + int32_t b; + struct { uint8_t *ptr; int64_t len; } s; + struct { struct cell **items; int64_t len, cap; } v; + } u; +} cell; + +static cell *alloc(enum tag t) { + cell *c = calloc(1, sizeof *c); + if (c == NULL) dyn_trap("OutOfMemory", "the dyn runtime could not allocate"); + c->tag = t; + return c; +} + +static cell *as(flan_dyn d) { return (cell *)(uintptr_t)d; } +static flan_dyn word(cell *c) { return (flan_dyn)(uintptr_t)c; } + +/* ── Construction ──────────────────────────────────────────────────── */ + +flan_dyn flan_dyn_nil(void) { return word(alloc(T_NIL)); } + +flan_dyn flan_dyn_from_i64(int64_t v) { + cell *c = alloc(T_I64); c->u.i = v; return word(c); +} + +flan_dyn flan_dyn_from_f64(double v) { + cell *c = alloc(T_F64); c->u.f = v; return word(c); +} + +flan_dyn flan_dyn_from_bool(int32_t v) { + cell *c = alloc(T_BOOL); c->u.b = (v != 0); return word(c); +} + +flan_dyn flan_dyn_from_bytes(const uint8_t *ptr, int64_t len) { + cell *c = alloc(T_STR); + c->u.s.ptr = malloc((size_t)len + 1); + if (c->u.s.ptr == NULL) dyn_trap("OutOfMemory", "the dyn runtime could not allocate"); + if (len > 0) memcpy(c->u.s.ptr, ptr, (size_t)len); + c->u.s.ptr[len] = 0; + c->u.s.len = len; + return word(c); +} + +flan_dyn flan_dyn_vec_new(void) { + cell *c = alloc(T_VEC); + c->u.v.cap = 8; + c->u.v.items = calloc((size_t)c->u.v.cap, sizeof(cell *)); + if (c->u.v.items == NULL) dyn_trap("OutOfMemory", "the dyn runtime could not allocate"); + return word(c); +} + +/* ── Arithmetic ────────────────────────────────────────────────────── */ + +/* Two numbers promote to f64 when either is one, which is the rule a reader + * expects of a dynamic language and is not the rule the typed language uses. + * The typed language has no implicit widening at all; here there is no + * annotation to have been written, so refusing would leave (+ 1 2.5) with no + * spelling that works. */ +static int numeric(cell *c) { return c->tag == T_I64 || c->tag == T_F64; } +static double as_f(cell *c) { return c->tag == T_I64 ? (double)c->u.i : c->u.f; } + +static flan_dyn arith(flan_dyn a, flan_dyn b, char op) { + cell *x = as(a), *y = as(b); + if (!numeric(x) || !numeric(y)) dyn_trap("DynArithType", "this arithmetic needs two numbers, and one of the two values is not one"); + if (x->tag == T_I64 && y->tag == T_I64) { + int64_t p = x->u.i, q = y->u.i, r = 0; + switch (op) { + case '+': r = p + q; break; + case '-': r = p - q; break; + case '*': r = p * q; break; + case '/': if (q == 0) dyn_trap("DivideByZero", "division by zero"); r = p / q; break; + case '%': if (q == 0) dyn_trap("DivideByZero", "division by zero"); r = p % q; break; + } + return flan_dyn_from_i64(r); + } + { + double p = as_f(x), q = as_f(y), r = 0; + switch (op) { + case '+': r = p + q; break; + case '-': r = p - q; break; + case '*': r = p * q; break; + case '/': r = p / q; break; + /* fmod without math.h, to keep the stub's link line as short as the + * real runtime's is meant to be. */ + case '%': r = p - q * (double)(int64_t)(p / q); break; + } + return flan_dyn_from_f64(r); + } +} + +flan_dyn flan_dyn_add(flan_dyn a, flan_dyn b) { return arith(a, b, '+'); } +flan_dyn flan_dyn_sub(flan_dyn a, flan_dyn b) { return arith(a, b, '-'); } +flan_dyn flan_dyn_mul(flan_dyn a, flan_dyn b) { return arith(a, b, '*'); } +flan_dyn flan_dyn_div(flan_dyn a, flan_dyn b) { return arith(a, b, '/'); } +flan_dyn flan_dyn_rem(flan_dyn a, flan_dyn b) { return arith(a, b, '%'); } + +/* ── Ordering and equality ─────────────────────────────────────────── */ + +static int cmp(flan_dyn a, flan_dyn b) { + cell *x = as(a), *y = as(b); + if (x->tag == T_STR && y->tag == T_STR) { + int64_t n = x->u.s.len < y->u.s.len ? x->u.s.len : y->u.s.len; + int r = memcmp(x->u.s.ptr, y->u.s.ptr, (size_t)n); + if (r != 0) return r < 0 ? -1 : 1; + return x->u.s.len == y->u.s.len ? 0 : (x->u.s.len < y->u.s.len ? -1 : 1); + } + if (!numeric(x) || !numeric(y)) dyn_trap("DynCompareType", "these two values have no ordering between them"); + if (x->tag == T_I64 && y->tag == T_I64) + return x->u.i == y->u.i ? 0 : (x->u.i < y->u.i ? -1 : 1); + { + double p = as_f(x), q = as_f(y); + return p == q ? 0 : (p < q ? -1 : 1); + } +} + +flan_dyn flan_dyn_lt(flan_dyn a, flan_dyn b) { return flan_dyn_from_bool(cmp(a, b) < 0); } +flan_dyn flan_dyn_le(flan_dyn a, flan_dyn b) { return flan_dyn_from_bool(cmp(a, b) <= 0); } +flan_dyn flan_dyn_gt(flan_dyn a, flan_dyn b) { return flan_dyn_from_bool(cmp(a, b) > 0); } +flan_dyn flan_dyn_ge(flan_dyn a, flan_dyn b) { return flan_dyn_from_bool(cmp(a, b) >= 0); } + +/* Structural, and never traps — the header's one exception. */ +static int eq(cell *x, cell *y) { + if (numeric(x) && numeric(y)) { + if (x->tag == T_I64 && y->tag == T_I64) return x->u.i == y->u.i; + return as_f(x) == as_f(y); + } + if (x->tag != y->tag) return 0; + switch (x->tag) { + case T_NIL: return 1; + case T_BOOL: return x->u.b == y->u.b; + case T_STR: return x->u.s.len == y->u.s.len + && memcmp(x->u.s.ptr, y->u.s.ptr, (size_t)x->u.s.len) == 0; + case T_VEC: { + if (x->u.v.len != y->u.v.len) return 0; + for (int64_t i = 0; i < x->u.v.len; i++) + if (!eq(x->u.v.items[i], y->u.v.items[i])) return 0; + return 1; + } + default: return 0; + } +} + +flan_dyn flan_dyn_eq(flan_dyn a, flan_dyn b) { + return flan_dyn_from_bool(eq(as(a), as(b))); +} + +/* ── Containers ────────────────────────────────────────────────────── */ + +static cell *need_vec(flan_dyn v) { + cell *c = as(v); + if (c->tag != T_VEC) dyn_trap("DynNotAVec", "this value is not a vector, so it has no elements"); + return c; +} + +static int64_t need_index(flan_dyn i) { + cell *c = as(i); + if (c->tag != T_I64) dyn_trap("DynIndexType", "an index must be an integer"); + return c->u.i; +} + +flan_dyn flan_dyn_len(flan_dyn v) { + cell *c = as(v); + if (c->tag == T_STR) return flan_dyn_from_i64(c->u.s.len); + return flan_dyn_from_i64(need_vec(v)->u.v.len); +} + +flan_dyn flan_dyn_at(flan_dyn v, flan_dyn i) { + cell *c = need_vec(v); + int64_t k = need_index(i); + if (k < 0 || k >= c->u.v.len) dyn_trap("Bounds", "index out of bounds"); + return word(c->u.v.items[k]); +} + +void flan_dyn_set_at(flan_dyn v, flan_dyn i, flan_dyn x) { + cell *c = need_vec(v); + int64_t k = need_index(i); + if (k < 0 || k >= c->u.v.len) dyn_trap("Bounds", "index out of bounds"); + c->u.v.items[k] = as(x); +} + +void flan_dyn_push(flan_dyn v, flan_dyn x) { + cell *c = need_vec(v); + if (c->u.v.len == c->u.v.cap) { + int64_t cap = c->u.v.cap * 2; + cell **items = realloc(c->u.v.items, (size_t)cap * sizeof(cell *)); + if (items == NULL) dyn_trap("OutOfMemory", "the dyn runtime could not allocate"); + c->u.v.items = items; + c->u.v.cap = cap; + } + c->u.v.items[c->u.v.len++] = as(x); +} + +static void print_cell(cell *c) { + switch (c->tag) { + case T_NIL: fputs("nil", stdout); break; + case T_I64: printf("%lld", (long long)c->u.i); break; + /* %g, so that a whole-numbered f64 does not print as an i64 would and + * the two remain distinguishable in a test's expected output. */ + case T_F64: printf("%g", c->u.f); break; + case T_BOOL: fputs(c->u.b ? "true" : "false", stdout); break; + case T_STR: printf("%.*s", (int)c->u.s.len, (const char *)c->u.s.ptr); break; + case T_VEC: + fputc('[', stdout); + for (int64_t i = 0; i < c->u.v.len; i++) { + if (i > 0) fputc(' ', stdout); + print_cell(c->u.v.items[i]); + } + fputc(']', stdout); + break; + } +} + +void flan_dyn_print(flan_dyn v) { print_cell(as(v)); } + +/* ── Extraction ────────────────────────────────────────────────────── */ + +int64_t flan_dyn_need_i64(flan_dyn v) { + cell *c = as(v); + if (c->tag != T_I64) dyn_trap("DynExpectedI64", "this value was required to be an i64 and is not"); + return c->u.i; +} + +double flan_dyn_need_f64(flan_dyn v) { + cell *c = as(v); + /* An i64 satisfies an f64 slot, because a dyn integer literal is an i64 by + * the header's rule and (defvar x f64 (f 1)) would otherwise be unwritable + * for any f returning dyn. The reverse is not true: f64 to i64 loses. */ + if (c->tag == T_I64) return (double)c->u.i; + if (c->tag != T_F64) dyn_trap("DynExpectedF64", "this value was required to be an f64 and is not"); + return c->u.f; +} + +int32_t flan_dyn_need_bool(flan_dyn v) { + cell *c = as(v); + if (c->tag != T_BOOL) dyn_trap("DynExpectedBool", "this value was required to be a bool and is not"); + return c->u.b; +} + +/* ── Roots ───────────────────────────────────────────────────────────── + * + * Recorded and otherwise ignored. The shadow stack is kept, and its depth + * checked against the pops, only so that a badly unbalanced emission fails + * loudly here rather than silently: an over-pop is a compiler bug worth + * dying on even in a stub that collects nothing. Under-pushing is invisible, + * and stays invisible until the real collector lands. */ + +static flan_dyn **roots = NULL; +static int64_t roots_len = 0, roots_cap = 0; + +void flan_dyn_root_push(flan_dyn *slot) { + if (roots_len == roots_cap) { + int64_t cap = roots_cap == 0 ? 64 : roots_cap * 2; + flan_dyn **r = realloc(roots, (size_t)cap * sizeof(flan_dyn *)); + if (r == NULL) dyn_trap("OutOfMemory", "the dyn runtime could not allocate"); + roots = r; + roots_cap = cap; + } + roots[roots_len++] = slot; +} + +void flan_dyn_root_pop(int64_t n) { + if (n < 0 || n > roots_len) dyn_trap("DynRootUnderflow", "the dyn root stack was popped further than it was pushed - a compiler bug"); + roots_len -= n; +} + +void flan_gc_init(void) { /* nothing to initialise: this stub never collects */ } diff --git a/test/test_flan.ml b/test/test_flan.ml index 5d46e1e..817e0bd 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -399,9 +399,15 @@ let () = | Arr [ _; _; _ ] -> () | _ -> check "array literal" false); (* ── Types: brackets mean different things by position ─────────── *) + (* Read out of [praw], not [params]: a defn's parameter vector is carried + undecided until Check pairs it, so the parser no longer fills [params] at + all. Every type spelled here is one the parser still resolves on sight — + brackets and lists are types by their shape whatever the environment says + — so [Ptype] is the shape under test and a [Pname] here would mean the + spelling stopped being recognised as a type. *) let ty src = match parse_decl (Printf.sprintf "(defn f [x %s] ())" src) with - | { d = Defn { params = [ { fty; _ } ]; _ }; _ } -> fty.t + | { d = Defn { praw = Some [ Pname ("x", _); Ptype t ]; _ }; _ } -> t.t | _ -> failwith "bad type test" in (match ty "[u8]" with Tslice _ -> () | _ -> check "[T] is a slice" false); @@ -421,7 +427,7 @@ let () = (* ── Declarations ──────────────────────────────────────────────── *) (match (parse_decl "(defn f [x i32] bool x)").d with - | Defn { ret = Some _; params = [ _ ]; fbody = [ _ ]; _ } -> () + | Defn { ret = Some _; praw = Some [ _; _ ]; fbody = [ _ ]; _ } -> () | _ -> check "defn with return type" false); (* () is the unit return type, and the body is what follows it. *) (match (parse_decl "(defn f [x i32] () (g x))").d with @@ -840,9 +846,22 @@ let () = rejects_check "a mistyped struct" "(defstruct Cursor [x i32]) (defn f [c Curser] ())" ~needle:"did you mean Cursor?"; - (* Nothing close: the type-variable rule still applies, and still names the - milestone. *) - rejects_check "a real type variable" "(defn f [x t] ())" + (* [(defn f [x t] ())] used to be one parameter of an unimplemented generic + type and is now two parameters of type dyn — a lowercase name resembling + no type is a parameter, which is the whole of dynamic-by-default. The + milestone-5 reading is still reachable, by writing the type variable with + the sigil the signature binds it with. *) + (match checked "(defn f [x t] ())" with + | p -> + (match List.find_opt (fun (f : Tast.fn) -> f.Tast.name = "f") p.Tast.fns with + | Some { Tast.params = [ Types.Dyn; Types.Dyn ]; _ } -> () + | _ -> check "an unannotated pair is two dyn parameters" false) + | exception _ -> check "an unannotated pair is two dyn parameters" false); + (* A bare lowercase name is still an unimplemented type variable everywhere a + type is the only thing a slot can hold. A defn's parameter vector stopped + being such a place — a slot there may be a parameter instead — so the rule + is exercised where it still decides, at a field. *) + rejects_check "a real type variable" "(defstruct Holder [x elem])" ~needle:"milestone 5"; rejects_check "an unknown concrete type" "(defn f [x Widget] ())" ~needle:"unknown type Widget"; @@ -1327,7 +1346,7 @@ let () = defn's body that just answers one says nothing about them. *) rejects_check "an fn with nothing to say what it takes" "(defn f [] () (fn [x] x))" ~needle:"nothing here says what this fn"; - rejects_check "type variables are milestone 5" "(defn f [x a] ())" + rejects_check "type variables are milestone 5" "(defn f [] a 0)" ~needle:"milestone 5"; (* The other half: a name in value position now *works*, and the arity is checked against the function it names. *) From c8091bdbf963028844906d1e8b4db4e4f7d836fa Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 19 Sep 2026 05:55:48 +0700 Subject: [PATCH 2/7] One unannotated add, answering 5 to the integers and 3.75 to the floats The boundary and the operators, which are the two halves of dyn being a type rather than a word the checker tolerates. Typed to dyn is implicit and dyn to typed is not, and the asymmetry is the design: boxing loses nothing and can happen wherever a dyn is wanted, while unboxing can fail at run time on a value the compiler cannot inspect, so it happens only where somebody wrote a type. Both go through expect, because expect is already the one place a wanted type meets a produced one, and every annotating site already calls it. Literals take their width from the dyn, not from the default. (defvar x dyn 5) holds an i64 five: the ABI carries one integer width, so the defaulting question never arises, and the literal is built at i64 rather than boxed after defaulting to i32 -- which also means 3000000000 is a dyn integer. An operator with one dyn operand is the runtime's. binary has already checked the second operand against the first, so a mixed pair arrives with the typed side boxed and the fold only has to call flan_dyn_add instead of adding. The comparisons answer bool and not a dyn holding one, because a comparison is almost always the test of an if; a program that wants it as a value boxes it again for free at that boundary. = and != never trap -- two values of unrelated types are unequal, not an error -- and the orderings do. Types.equal had no Dyn case, so dyn was equal to nothing including itself. print hands the whole value to the runtime rather than walking it: every other arm of the structural printer exists because a Flan value carries no header and only the compiler knows what it is, and a dyn is the exact reverse. The compiler carries the dyn runtime the way it already carries flan_rt.c, with the header pasted in front of the stub so there is one self-contained translation unit and one contract. --- lib/build.ml | 6 +- lib/check.ml | 210 +++++++++++++++++++++++++++++++++++ lib/dev.ml | 3 +- lib/dune | 13 ++- lib/emit.ml | 30 +++++ lib/render.ml | 13 +++ lib/types.ml | 6 +- runtime/flan_dyn_stub.c | 11 +- test/programs/dyn-basic.flan | 15 +++ 9 files changed, 301 insertions(+), 6 deletions(-) create mode 100644 test/programs/dyn-basic.flan diff --git a/lib/build.ml b/lib/build.ml index 4cf82ad..82259a5 100644 --- a/lib/build.ml +++ b/lib/build.ml @@ -848,7 +848,8 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = []) cache key via [compile_c]'s [opt]/[tflags] digest — see [cflags]. *) let objs = cc ~warn:runtime_warnings Runtime_src.source "flan_rt.c" - :: [ cc ~warn:runtime_warnings Runtime_src.dev_source "flan_dev.c" ] + :: cc ~warn:runtime_warnings Runtime_src.dev_source "flan_dev.c" + :: [ cc ~warn:runtime_warnings Runtime_src.dyn_source "flan_dyn.c" ] (* wasi-libc's entry point, which is not [main]. See [wasm_main_source]. Not the browser's: emscripten's start code calls [main] under that name, so the .ll's @main is already the entry point and the shim would be a @@ -1102,7 +1103,8 @@ let macro_module ?(opts = default) ?(csrcs = []) ?(lflags = []) ~macros let cc ?(warn = []) src name = compile_c ~opts ~tflags ~warn ~src ~name () in let objs = cc ~warn:runtime_warnings Runtime_src.source "flan_rt.c" - :: [ cc ~warn:runtime_warnings Runtime_src.dev_source "flan_dev.c" ] + :: cc ~warn:runtime_warnings Runtime_src.dev_source "flan_dev.c" + :: [ cc ~warn:runtime_warnings Runtime_src.dyn_source "flan_dyn.c" ] @ (match p.Tast.cshim with | [] -> [] | parts -> diff --git a/lib/check.ml b/lib/check.ml index 73706d7..9a8deda 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -1364,10 +1364,131 @@ let type_id name = let restart_sig tys = "(" ^ String.concat " " (List.map Types.to_string tys) ^ ")" +(* ── The dyn boundary ─────────────────────────────────────────────────── + + Typed to dyn is implicit and dyn to typed is not. That asymmetry is the + whole of the design and it is worth saying why it is not arbitrary. + + Boxing loses nothing: the value goes in and the runtime records what it was. + It can happen anywhere a dyn is wanted without a reader being surprised, + because nothing about the program's meaning turns on it. Unboxing can fail, + at run time, on a value the compiler cannot inspect -- so it happens only + where somebody *wrote a type*: a typed parameter, a typed binding, a typed + field. Those are the places a reader already understands as a claim about + what a value is, and a claim that can be wrong is exactly what a trap is + for. Nowhere else does the compiler decide a dyn is an i64 on its own. + + Both directions go through [expect], because [expect] is already the one + place a wanted type meets a produced one. Every site that annotates -- and + only those sites -- calls it with [~want]. + + Milestone 1 boxes the scalars and refuses everything else by name. A typed + container crossing into dyn is the interesting refusal: [(Vec i64)] has a + representation the dyn runtime does not know how to walk, and heterogeneity + at milestone 1 is served by the runtime's own vector behind + [flan_dyn_vec_new] instead. That is a "not yet" and says so. *) + +let dyn_i64 = Types.Int Types.I64 +let dyn_f64 = Types.Float Types.F64 + +(* Widening to the one width the ABI carries. runtime/flan_dyn.h boxes integers + as [i64] and floats as [f64] and offers no other width, which is the + language's "dyn integers are i64" written where it is enforced. The cast is + explicit in the tree rather than left to the backend: a [Cast] is what the + language's own conversions emit, and a widening one loses nothing. *) +let widen loc (want : Types.t) (e : Tast.expr) = + if Types.equal want e.Tast.ty then e + else mk loc want (Tast.Prim (Tast.Cast want, [ e ])) + +let unboxable t = + match t with + | Types.Int Types.I64 | Types.Float Types.F64 | Types.Bool -> true + | _ -> false + +(* The sentence a refusal at this boundary gives. It names the type and says + which direction failed, because "expected dyn, found (Vec i64)" would read + as a type error the programmer could fix by writing something else, and + there is nothing else to write -- the feature is not there yet. *) +let no_dyn_yet loc ~into t extra = + Loc.failk "check/dyn-not-yet" loc + "%s does not cross into %s yet%s" + (Types.to_string t) (if into then "dyn" else "a written type") extra + +let box loc (e : Tast.expr) : Tast.expr = + let dyn sym args = rt loc Types.Dyn sym args in + match e.Tast.ty with + | Types.Dyn -> e + | Types.Int _ -> dyn "flan_dyn_from_i64" [ widen loc dyn_i64 e ] + | Types.Float _ -> dyn "flan_dyn_from_f64" [ widen loc dyn_f64 e ] + (* The ABI takes an [int32_t], because a C signature that says [_Bool] is a + width argument nobody wants to have. *) + | Types.Bool -> dyn "flan_dyn_from_bool" [ widen loc (Types.Int Types.I32) e ] + (* A string is ptr+len and arrives as two arguments, the way every other + (ptr, len) entry point in the runtime takes one. The runtime copies: the + bytes may be a literal or a slice of a buffer the program goes on to + write. *) + | Types.String -> dyn "flan_dyn_from_bytes" [ e ] + (* Unit does not box. A value of the zero-sized type carries nothing for a + dyn word to hold, and [nil] -- which is what an [if] with no else answers + in dyn context -- is a different thing with a different constructor. The + two get confused if unit is allowed to become one. *) + | Types.Unit -> + Loc.failk "check/dyn-unit" loc + "() does not box into dyn — a value of the zero-sized type carries \ + nothing a dyn could hold. The absent dyn value is nil, which is what an \ + if with no else branch answers here" + | Types.Never -> e + | Types.Vec _ | Types.Map _ | Types.Slice _ | Types.Array _ -> + no_dyn_yet loc ~into:true e.Tast.ty + ". The dyn container at this milestone is the runtime's own, from \ + (vec-new dyn); a typed container has a representation the dyn runtime \ + cannot walk" + | Types.Named _ | Types.Enum _ | Types.Option _ | Types.Ptr _ + | Types.Alloc | Types.Fn _ | Types.Var _ -> + no_dyn_yet loc ~into:true e.Tast.ty "" + +let unbox loc (want : Types.t) (e : Tast.expr) : Tast.expr = + let need sym ty = rt loc ty sym [ e ] in + match want with + | Types.Int Types.I64 -> need "flan_dyn_need_i64" dyn_i64 + | Types.Float Types.F64 -> need "flan_dyn_need_f64" dyn_f64 + | Types.Bool -> + (* The ABI answers an [int32_t]; [bool] is an [i1]. The narrowing is the + language's own cast and cannot fail — the runtime already decided the + value was a bool, so what comes back is 0 or 1. *) + widen loc Types.Bool (need "flan_dyn_need_bool" (Types.Int Types.I32)) + (* Every other width is refused rather than served by a need_i64 and a + truncation. This language has no implicit narrowing anywhere, and putting + one at the boundary where a value's type was *already* uncertain is the + worst place in the program to start: the annotation would read as a check + and would be a silent discard of the high bits. The ABI grows a per-width + entry point when there is a reason to; until then the spelling that works + is an i64 and an explicit conversion after it. *) + | Types.Int _ | Types.Float _ -> + no_dyn_yet loc ~into:false want + (Printf.sprintf + " — the dyn runtime carries integers as i64 and floats as f64, so \ + take it as %s and convert" + (if Types.is_numeric want && (match want with Types.Float _ -> true | _ -> false) + then "f64" else "i64")) + | _ -> no_dyn_yet loc ~into:false want "" + let expect loc ~want (got : Tast.expr) = match want with | None -> got | Some w -> + (* The boundary, and the only implicit conversion in the language. It runs + before [fits] rather than instead of it: what comes back is an ordinary + expression of the wanted type, and if the coercion did not produce one + the usual message is still the one that reports it. *) + let got = + match w, got.Tast.ty with + | Types.Dyn, Types.Dyn -> got + | Types.Dyn, _ -> box loc got + | _, Types.Dyn when Types.fits ~expected:w ~actual:Types.Dyn -> got + | _, Types.Dyn -> unbox loc w got + | _ -> got + in if Types.fits ~expected:w ~actual:got.Tast.ty then got else fail loc "expected %s, found %s" (Types.to_string w) @@ -1698,6 +1819,12 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr = match e.Ast.e with | Ast.Int n -> int_literal loc ~want n | Ast.Byte b -> int_literal loc ~want ~default:Types.U8 (Int64.of_int b) + (* The float literal's own dyn case, for the reason the integer's has one: + the ABI carries one width and the literal is built at it. f64 is already + what an unconstrained float literal defaults to, so this only has to stop + the "expected dyn, found the float literal" arm below from firing. *) + | Ast.Float x when want = Some Types.Dyn -> + box loc (mk loc dyn_f64 (Tast.Float (x, Types.F64))) | Ast.Float x -> let k = match want with @@ -1932,6 +2059,16 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr = and int_literal loc ~want ?(default = Types.I32) n = match want with | Some (Types.Int k) -> mk loc (Types.Int k) (Tast.Int (in_range loc k n, k)) + (* A literal in dyn position takes i64 and not the i32 an unconstrained one + defaults to. This is where "dyn integers are i64" stops being a statement + about the ABI and becomes one about the language: [(defvar x dyn 5)] holds + an i64 five, and the defaulting question a wider set of boxes would raise + never arises because there is only the one box. Handled here rather than + left to [expect] so the literal is *built* at the right width — the range + check below is the one that matters, and 3000000000 is a dyn integer even + though it is not an i32. *) + | Some Types.Dyn -> + box loc (mk loc dyn_i64 (Tast.Int (in_range loc Types.I64 n, Types.I64))) (* An untyped integer constant is usable where a float is wanted, as in Odin. A float literal is never usable where an integer is wanted. *) | Some (Types.Float k) -> @@ -3221,6 +3358,14 @@ and fold_left_prim ctx ~want loc name p ok what args = match args with x :: y :: rest -> x, y, rest | _ -> assert false in let a, b = binary ctx name loc ~want:(numeric_want want) [ x; y ] in + (* One dyn operand makes the whole fold dyn. [binary] has already checked the + second against the first, so a mixed pair arrives with the typed side + boxed — [(+ x 1)] over a dyn [x] checked the literal at dyn and got an i64 + five in a box. What is left is to fold with the runtime's operator instead + of the machine's. *) + if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then + dyn_fold ctx ~want loc name [ a; b ] rest + else begin unconstrained ctx.env loc name ~needs:"numeric?" a.Tast.ty; (* Past [unconstrained] a variable here is one the [where] clause admitted, so the concrete predicate below has nothing to say about it — it is @@ -3236,6 +3381,37 @@ and fold_left_prim ctx ~want loc name p ok what args = rest in expect loc ~want acc + end + +(* The dyn lowering of a fold: one call per operator application, left to + right, each taking and answering a dyn word. The typed side of a mixed pair + is boxed on the way in — [box] is the identity on something already dyn, so + this needs no case analysis of its own. *) +and dyn_fold ctx ~want loc name first rest = + let sym = + match name with + | "+" -> "flan_dyn_add" | "-" -> "flan_dyn_sub" + | "*" -> "flan_dyn_mul" | "/" -> "flan_dyn_div" + | "%" -> "flan_dyn_rem" + | _ -> + (* Bitwise and shift operators land here if they ever admit a dyn + operand. They do not: the runtime carries no bitwise entry points, + and an integer operation on a value that might be a float is not + something to guess at. *) + no_dyn_yet loc ~into:false Types.Dyn + (Printf.sprintf " — %s has no dyn form" name) + in + let apply acc b = rt loc Types.Dyn sym [ acc; box loc b ] in + let acc = + match first with + | [ a; b ] -> apply (box loc a) b + | _ -> assert false + in + let acc = + List.fold_left (fun acc arg -> apply acc (check ctx ~want:Types.Dyn arg)) + acc rest + in + expect loc ~want acc (* ── Allocation failure, spec-memory.md ──────────────────────────────── No allocating operation returns an error and none can fail silently. When @@ -3520,10 +3696,14 @@ and named_call ctx ~want loc name args = | "%" -> arity loc name 2 args; let a, b = binary ctx name loc ~want:(numeric_want want) args in + if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then + dyn_fold ctx ~want loc name [ a; b ] [] + else begin unconstrained ctx.env loc name ~needs:"numeric?" a.Tast.ty; if not (Types.is_numeric a.Tast.ty || generic_ty a.Tast.ty) then fail loc "%s takes numbers, found %s" name (Types.to_string a.Tast.ty); prim Tast.Rem a.Tast.ty [ a; b ] + end | "=" | "!=" | "<" | "<=" | ">" | ">=" -> let p = match name with | "=" -> Tast.Eq | "!=" -> Tast.Ne | "<" -> Tast.Lt @@ -3531,6 +3711,35 @@ and named_call ctx ~want loc name args = in arity loc name 2 args; let a, b = binary ctx name loc ~want:None args in + (* A comparison with a dyn operand answers a *bool*, not a dyn, even though + the runtime's own entry point answers a dyn holding one. The reason is + where the result goes: a comparison is overwhelmingly the test of an + [if] or a [while], and those want an i1. So the need_bool is applied + here, once, and a program that really wants the comparison as a dyn + value boxes it again on the way into wherever it is going — which [box] + does for free at that boundary. + + [=] and [!=] are the pair that never traps: the runtime compares + structurally and answers false for values of unrelated types, because + two things being unalike is the answer to "are these equal", not an + error. The orderings do trap, and rightly — there is no true answer to + whether a string is less than a vector. *) + if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then begin + let sym = + match name with + | "=" | "!=" -> "flan_dyn_eq" + | "<" -> "flan_dyn_lt" | "<=" -> "flan_dyn_le" + | ">" -> "flan_dyn_gt" | _ -> "flan_dyn_ge" + in + let cmp = unbox loc Types.Bool (rt loc Types.Dyn sym [ box loc a; box loc b ]) in + (* [!=] has no entry point of its own: there is one structural equality + and the negation is an [i1] flip the backend folds away. *) + let r = + if String.equal name "!=" then mk loc Types.Bool (Tast.Prim (Tast.Not, [ cmp ])) + else cmp + in + expect loc ~want r + end else begin (* [=] and [!=] admit one type [<] does not: a handle, which is a pair of numbers in one word and where "the same entity" is the question the type exists to answer. Ordering handles would order a slot index, which @@ -3548,6 +3757,7 @@ and named_call ctx ~want loc name args = "%s compares machine numbers; %s has no built-in comparison \ (plan.org, Types)" name (Types.to_string a.Tast.ty); prim p Types.Bool [ a; b ] + end | "not" -> arity loc name 1 args; prim Tast.Not Types.Bool [ check ctx ~want:Types.Bool (List.hd args) ] diff --git a/lib/dev.ml b/lib/dev.ml index 1cee547..749f603 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -4001,7 +4001,8 @@ let merged_executable ~opts ~csrcs ~lflags ~pnames (p : Tast.program) ~out ~ll = let cc src name = compile_c ~opts ~tflags ~src ~name () in let objs = (cc Runtime_src.source "flan_rt.c" - :: [ cc Runtime_src.dev_source "flan_dev.c" ]) + :: cc Runtime_src.dev_source "flan_dev.c" + :: [ cc Runtime_src.dyn_source "flan_dyn.c" ]) @ (match p.Tast.cshim with | [] -> [] | parts -> diff --git a/lib/dune b/lib/dune index 266f3ae..9747a5c 100644 --- a/lib/dune +++ b/lib/dune @@ -26,11 +26,19 @@ ; so there is only ever one copy to edit. flan_dev.c goes into a dev build ; only — it is the run-time name lookup a REPL needs and a release build has ; no use for. +; [dyn_source] is runtime/flan_dyn_stub.c with runtime/flan_dyn.h pasted in +; front of it, because the generated module is one string and the stub includes +; the header by name. THE MERGE REPLACES THE STUB WITH runtime/flan_dyn.c and +; this rule keeps its shape — the header stays the contract both sides are +; diffed against, and concatenating it here is what makes the compiler carry a +; self-contained translation unit the way it already carries flan_rt.c. (rule (target runtime_src.ml) (deps %{workspace_root}/runtime/flan_rt.c - %{workspace_root}/runtime/flan_dev.c) + %{workspace_root}/runtime/flan_dev.c + %{workspace_root}/runtime/flan_dyn.h + %{workspace_root}/runtime/flan_dyn_stub.c) (action (with-stdout-to runtime_src.ml @@ -39,4 +47,7 @@ (cat %{workspace_root}/runtime/flan_rt.c) (echo "|c}\n\nlet dev_source = {c|\n") (cat %{workspace_root}/runtime/flan_dev.c) + (echo "|c}\n\nlet dyn_source = {c|\n") + (cat %{workspace_root}/runtime/flan_dyn.h) + (cat %{workspace_root}/runtime/flan_dyn_stub.c) (echo "|c}\n"))))) diff --git a/lib/emit.ml b/lib/emit.ml index 8cbc0f9..fcd3cdf 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -2833,6 +2833,36 @@ declare i64 @flan_alloc_fail_align() declare i64 @flan_alloc_fail_id() declare i64 @flan_alloc_budget(ptr) declare void @flan_alloc_set_budget(ptr, i64) +; The dynamic runtime, runtime/flan_dyn.h. A flan_dyn is one machine word and +; is spelled i64 here because the typedef says uint64_t; nothing in this file +; ever looks inside one, so every operation on a dyn value is one of these. +declare i64 @flan_dyn_nil() +declare i64 @flan_dyn_from_i64(i64) +declare i64 @flan_dyn_from_f64(double) +declare i64 @flan_dyn_from_bool(i32) +declare i64 @flan_dyn_from_bytes(ptr, i64) +declare i64 @flan_dyn_vec_new() +declare i64 @flan_dyn_add(i64, i64) +declare i64 @flan_dyn_sub(i64, i64) +declare i64 @flan_dyn_mul(i64, i64) +declare i64 @flan_dyn_div(i64, i64) +declare i64 @flan_dyn_rem(i64, i64) +declare i64 @flan_dyn_lt(i64, i64) +declare i64 @flan_dyn_le(i64, i64) +declare i64 @flan_dyn_gt(i64, i64) +declare i64 @flan_dyn_ge(i64, i64) +declare i64 @flan_dyn_eq(i64, i64) +declare i64 @flan_dyn_len(i64) +declare i64 @flan_dyn_at(i64, i64) +declare void @flan_dyn_set_at(i64, i64, i64) +declare void @flan_dyn_push(i64, i64) +declare void @flan_dyn_print(i64) +declare i64 @flan_dyn_need_i64(i64) +declare double @flan_dyn_need_f64(i64) +declare i32 @flan_dyn_need_bool(i64) +declare void @flan_dyn_root_push(ptr) +declare void @flan_dyn_root_pop(i64) +declare void @flan_gc_init() declare void @flan_dev_reg_enable() declare void @flan_dev_reg_note_vec(ptr, i64, ptr, i64) declare void @flan_dev_reg_note_map(ptr, i64, i64, ptr, i64) diff --git a/lib/render.ml b/lib/render.ml index ae5a47d..c0022a3 100644 --- a/lib/render.ml +++ b/lib/render.ml @@ -352,5 +352,18 @@ let rec render c depth (e : Tast.expr) : Tast.expr list = (Tast.While (cond, lit " " :: render c (depth + 1) elem, [ step ])); lit "]" ])) ] + (* The one type this walk does not walk. Every other arm is here because a + Flan value carries no header and only the compiler knows what it is; a + dyn value is the exact opposite — the runtime knows and the compiler + does not — so the printing belongs on the side that can see the tag, and + the walk hands the whole value over. + + The cost is that it writes to stdout itself rather than through + [c.emit], so a dyn printed at the REPL arrives on the program's output + and not in the REPL's buffer. Fixing that means an emit-shaped dyn + printer in the runtime — a second entry point taking the sink — and it + is not milestone 1's. *) + | Types.Dyn -> + [ unit_ (Tast.Prim (Tast.Rt "flan_dyn_print", [ e ])) ] | t -> fail loc "no printer for %s" (Types.to_string t) diff --git a/lib/types.ml b/lib/types.ml index f8f218a..18c2ba2 100644 --- a/lib/types.ml +++ b/lib/types.ml @@ -97,7 +97,11 @@ let rec equal a b = match a, b with | Int x, Int y -> x = y | Float x, Float y -> x = y - | Bool, Bool | String, String | Unit, Unit | Never, Never -> true + (* [Dyn] is equal to itself and to nothing else. Two dyn values may hold + different things at run time, which is the point of the type and is not + this function's question: this is identity of *static* types, and there is + one dyn type the way there is one string type. *) + | Bool, Bool | String, String | Unit, Unit | Never, Never | Dyn, Dyn -> true | Named x, Named y | Enum x, Enum y -> String.equal x y | Slice x, Slice y -> equal x y | Array (n, x), Array (m, y) -> Int64.equal n m && equal x y diff --git a/runtime/flan_dyn_stub.c b/runtime/flan_dyn_stub.c index ae0d469..779f49f 100644 --- a/runtime/flan_dyn_stub.c +++ b/runtime/flan_dyn_stub.c @@ -25,7 +25,16 @@ #include #include -#include "flan_dyn.h" +/* The compiler carries this file as one string with flan_dyn.h pasted in front + * of it (lib/dune), and in that form there is no header on disk to find. The + * probe keeps the file compilable both ways: standalone against the real + * header, and concatenated, where the declarations are already above. The + * header's own include guard makes the two agree. */ +#if defined(__has_include) +# if __has_include("flan_dyn.h") +# include "flan_dyn.h" +# endif +#endif /* flan_rt.c's own [rt_trap] is static, so this mirrors it rather than calling * it: print the sentence, offer the name to the dev daemon's hook, and leave diff --git a/test/programs/dyn-basic.flan b/test/programs/dyn-basic.flan new file mode 100644 index 0000000..bbce010 --- /dev/null +++ b/test/programs/dyn-basic.flan @@ -0,0 +1,15 @@ +;;;; An unannotated defn, called at two different types. +;;;; +;;;; [(defn add [x y] dyn (+ x y))] states no type for either parameter, so both +;;;; are dyn, and the + in the body is the dyn one: a call into the runtime that +;;;; decides on what the two words actually hold. The same function serves the +;;;; integer call and the float call, which is the whole of what the feature +;;;; buys and is not something the typed language could express at all. + +(defn add [x y] dyn (+ x y)) + +(defn main [] () + (print (add 2 3)) + (print "\n") + (print (add 1.5 2.25)) + (print "\n")) From de792fe14107fb839e695d3942ce3e23493e63cb Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 19 Sep 2026 06:06:44 +0700 Subject: [PATCH 3/7] A container holding an integer, a float, a string and a boolean at once (vec-new dyn) is not a (Vec dyn). At milestone 1 the heterogeneous container is the dyn runtime's own object and its type is dyn like everything else the runtime hands back, which is what lets push, at and len on it be the dyn operations instead of a type-erased Vec over eight-byte elements. It takes no allocator, and the refusal says why: the storage has to be storage the collector already knows about, where a Flan Vec's block would hold roots inside memory the collector does not own. len answers an i32 and at answers a dyn. The asymmetry is deliberate -- a length is what an index loop compares against, and handing back a boxed number would make (< i (len xs)) a dyn comparison and two allocations an iteration. The operand-order bug, which the first test could not see because both its operands were dyn: (+ n x) over a typed n and a dyn x threaded i64 into the second check, expect did what an annotation site had asked for and unboxed, and the result was a machine add of a value the runtime was never asked about -- the program trapping on a float instead of promoting it, with nothing in the source to say why. (+ x n) boxed correctly, so it was visible in one operand order only. binary now takes dyn_ok from the operators that have a dyn lowering and checks both operands on their own terms, which is safe exactly when neither needs an expectation to check -- a literal still takes the other's type, and a keyword still gets one, since :lo has no meaning without it. Cast had no bool arms, so the bool boundary failed to emit; reachability hid it, because the program that used it dropped the function. dyn does not cross to C: it is one word and would have passed as an integer, and C has no way to ask what the word means. A condition may not carry one either, nor hold one in a field -- a payload crosses a handler boundary and has to stay rooted across the transfer, which is the collector's question and milestone 2's. --- lib/check.ml | 144 ++++++++++++++++++++++++++++++++++--- lib/emit.ml | 11 +++ lib/types.ml | 2 +- mix | Bin 0 -> 83160 bytes test/programs/dyn-vec.flan | 24 +++++++ 5 files changed, 170 insertions(+), 11 deletions(-) create mode 100755 mix create mode 100644 test/programs/dyn-vec.flan diff --git a/lib/check.ml b/lib/check.ml index 9a8deda..50be294 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -1972,12 +1972,38 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr = let name = match c.Tast.ty with | Types.Named n -> n + (* Its own arm ahead of the general one, because "a condition is a + struct, not dyn" would read as a rule about shape when the answer is a + milestone. A condition crosses a handler boundary as a pointer to a + frame that is still alive, and a dyn payload has to stay rooted across + that transfer — which is the collector's question, not this one's, and + it is milestone 2's. *) + | Types.Dyn -> + no_dyn_yet c.Tast.loc ~into:false Types.Dyn + " — a condition crosses a handler boundary and a dyn payload has to \ + stay rooted across the transfer, which is milestone 2" | t -> fail c.Tast.loc "a condition is a struct, not %s — matching is by type and there is \ no condition hierarchy" (Types.to_string t) in + (* And the same refusal for a condition that merely *holds* one. The + payload is what crosses, so a dyn field is the dyn payload the note + above is about, whatever the struct around it is called. *) + (match Hashtbl.find_opt ctx.env.structs name with + | Some (s : Tast.structure) -> + List.iter + (fun (f : Tast.field) -> + if f.Tast.fty = Types.Dyn then + no_dyn_yet c.Tast.loc ~into:false Types.Dyn + (Printf.sprintf + " — the field %s of the condition %s is one, and a payload \ + has to stay rooted across a handler transfer, which is \ + milestone 2" + f.Tast.fname name)) + s.Tast.fields + | None -> ()); (* §1 and §2. [signal] is Unit whatever it finds; [error] is Never, because the only way past it is a handler that transfers — one that returns normally has not answered it, and the program stops. *) @@ -3357,12 +3383,10 @@ and fold_left_prim ctx ~want loc name p ok what args = let x, y, rest = match args with x :: y :: rest -> x, y, rest | _ -> assert false in - let a, b = binary ctx name loc ~want:(numeric_want want) [ x; y ] in - (* One dyn operand makes the whole fold dyn. [binary] has already checked the - second against the first, so a mixed pair arrives with the typed side - boxed — [(+ x 1)] over a dyn [x] checked the literal at dyn and got an i64 - five in a box. What is left is to fold with the runtime's operator instead - of the machine's. *) + let a, b = binary ctx ~dyn_ok:true name loc ~want:(numeric_want want) [ x; y ] in + (* One dyn operand makes the whole fold dyn, whichever side it is on. The + typed side is boxed by [dyn_fold]; a literal was already built at dyn by + [binary], so [(+ x 1)] over a dyn x folds an i64 one. *) if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then dyn_fold ctx ~want loc name [ a; b ] rest else begin @@ -3695,7 +3719,7 @@ and named_call ctx ~want loc name args = nobody writes on purpose. *) | "%" -> arity loc name 2 args; - let a, b = binary ctx name loc ~want:(numeric_want want) args in + let a, b = binary ctx ~dyn_ok:true name loc ~want:(numeric_want want) args in if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then dyn_fold ctx ~want loc name [ a; b ] [] else begin @@ -3710,7 +3734,7 @@ and named_call ctx ~want loc name args = | "<=" -> Tast.Le | ">" -> Tast.Gt | _ -> Tast.Ge in arity loc name 2 args; - let a, b = binary ctx name loc ~want:None args in + let a, b = binary ctx ~dyn_ok:true name loc ~want:None args in (* A comparison with a dyn operand answers a *bool*, not a dyn, even though the runtime's own entry point answers a dyn holding one. The reason is where the result goes: a comparison is overwhelmingly the test of an @@ -4056,6 +4080,25 @@ and named_call ctx ~want loc name args = out. *) | "vec-new" -> let elem, args = vec_new_elem ctx ~want loc args in + (* [(vec-new dyn)] is not a [(Vec dyn)]. At milestone 1 the heterogeneous + container is the dyn runtime's own object, and its type is [dyn] like + everything else the runtime hands back — which is what lets [push], [at] + and [len] on it go through the dyn operations rather than through a + type-erased Vec over eight-byte elements. + + The two could be made to coincide later, and the reason not to now is + the collector: a Flan Vec's storage comes from an allocator the program + named, and the words in it would be roots the collector has to find + inside a block it does not own. The runtime's own vector is storage the + collector already knows about. *) + if elem = Types.Dyn then begin + if args <> [] then + fail loc + "(vec-new dyn) takes no allocator — the dyn container's storage is \ + the dyn runtime's, which is what lets the collector find the values \ + inside it"; + expect loc ~want (rt loc Types.Dyn "flan_dyn_vec_new" []) + end else begin let a = allocator_arg ctx loc args in let v = fresh_slot ctx (Types.Vec elem) in let attempt = @@ -4073,12 +4116,23 @@ and named_call ctx ~want loc name args = region_check ctx.env loc (mk loc (Types.Vec elem) (Tast.Local v)) (mk loc (Types.Vec elem) (Tast.Local v)) ]))) + end (* Unit, not a Result and not an ignorable error code: see [alloc_guard]. *) | "push" -> arity loc name 2 args; (match args with | [ target; x ] -> let target = check ctx target in + (* A push into a dyn container is a call and nothing else: no allocation + guard, no restart, no region check. The dyn runtime owns the storage + and answers a failure to grow it on its own terms — the guard and the + retry restart exist for an allocator the *program* named, and here + there is none to name. *) + if target.Tast.ty = Types.Dyn then + expect loc ~want + (rt loc Types.Unit "flan_dyn_push" + [ target; check ctx ~want:Types.Dyn x ]) + else begin let elem = vec_elem loc "push" target.Tast.ty in let x = check ctx ~want:elem x in (* The element is bound before the loop so that a [retry] re-attempts @@ -4101,6 +4155,7 @@ and named_call ctx ~want loc name args = (with_note loc (alloc_guard ctx loc attempt) (reg_note loc "flan_dev_reg_note_vec" target [ size_of loc elem ] elem)) ]))) + end | _ -> assert false) | "reserve" -> arity loc name 2 args; @@ -4824,6 +4879,14 @@ and named_call ctx ~want loc name args = | Types.Map _ -> let n = rt loc (Types.Int Types.I64) "flan_map_len" [ a; here loc ] in expect loc ~want (mk loc index_ty (Tast.Prim (Tast.Cast index_ty, [ n ]))) + (* A dyn length is an i32 like every other length here, not a dyn holding + one. [len] is what an index loop compares against, and handing back a + boxed number would make [(< i (len xs))] a dyn comparison and a pair of + allocations per iteration. The runtime answers a dyn; it is unboxed at + once and narrowed the way the Vec's i64 above is. *) + | Types.Dyn -> + let n = unbox loc (Types.Int Types.I64) (rt loc Types.Dyn "flan_dyn_len" [ a ]) in + expect loc ~want (mk loc index_ty (Tast.Prim (Tast.Cast index_ty, [ n ]))) | other -> fail loc "len takes an array, a slice, a string, a Vec or a Map, found %s" @@ -4836,6 +4899,19 @@ and named_call ctx ~want loc name args = | Types.Vec _ -> let p, elem = vec_at ctx loc target idx in expect loc ~want (mk loc elem (Tast.Deref p)) + (* One index, because a dyn container is one dimension: the nested + [(at grid r c)] spelling walks a type the compiler can see through, + and here it cannot. [(at (at g r) c)] is the spelling that works and + is what the refusal names. *) + | Types.Dyn -> + (match idx with + | [ i ] -> + expect loc ~want + (rt loc Types.Dyn "flan_dyn_at" [ target; check ctx ~want:Types.Dyn i ]) + | _ -> + fail loc + "(at ...) over a dyn takes one index — the compiler cannot see \ + the shape of a dyn container, so write (at (at x i) j)") | _ -> let idx, ty = indexed ctx target idx in prim Tast.At ty (target :: idx)) @@ -5466,7 +5542,7 @@ and numeric_want want = widening, so one side has to decide it. Check the side that carries the most information first: a non-literal over a literal, and a float literal over an integer one, since an integer constant converts to a float and not back. *) -and binary ctx name loc ~want args = +and binary ctx ?(dyn_ok = false) name loc ~want args = match args with | [ x; y ] -> let y_decides = @@ -5475,11 +5551,46 @@ and binary ctx name loc ~want args = | (Ast.Int _ | Ast.Byte _), Ast.Float _ -> true | _ -> false) in + (* A form that cannot be checked without being told what is wanted. A + literal takes its width from the expectation, and a keyword has no + meaning at all without one — [:lo] resolves against the enum the site + expects and there is no keyword type to fall back on. Everything else + checks on its own terms. *) + let needs_want (f : Ast.expr) = + is_literal f || (match f.Ast.e with Ast.Kw _ -> true | _ -> false) + in if y_decides then begin let b = check ctx ?want y in let a = check ctx ~want:b.Tast.ty x in a, b - end else begin + end + (* [dyn_ok] is set by the operators that have a dyn lowering, and it exists + to stop the second operand being coerced to the first's type before + anybody has asked whether the pair is a dyn one. + + Without it [(+ n x)] over an [i64] n and a dyn x threads [i64] into the + second check, [expect] does what an annotation site asked for and + unboxes, and the result is a *machine* add of a value the runtime was + never asked about: the program traps on a float instead of promoting, + and nothing in the source says why. The mirror image [(+ x n)] boxed + correctly, so the bug was visible only in one operand order. + + Both sides are checked on their own terms here and the caller decides. + That is safe exactly when neither operand needs an expectation, which is + what [needs_want] settles — a literal still gets the first operand's + type, so [(+ x 1)] over a dyn x goes on building an i64 one. *) + else if dyn_ok && not (needs_want y) then begin + let a = check ctx ?want x in + let b = check ctx y in + (* Nothing dyn about this pair after all, so it is put back the way the + typed path built it. Re-checking only when the types actually differ + keeps the common case to one check of each operand. *) + if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn + || Types.equal a.Tast.ty b.Tast.ty + then a, b + else a, check ctx ~want:a.Tast.ty y + end + else begin let a = check ctx ?want x in let b = check ctx ~want:a.Tast.ty y in a, b @@ -5921,6 +6032,19 @@ let collect env (decls : Ast.decl list) = | Types.Int _ | Types.Float _ | Types.Bool | Types.Ptr _ | Types.Enum _ | Types.Unit -> () | Types.String | Types.Slice _ when what = "a parameter" -> () + (* Its own arm, because the general advice below is wrong for it and + dangerously so. A dyn is one machine word and would cross without + complaint — [(Ptr dyn)] is not the fix and there is nothing for a + shim to read: what the C side would receive is a word whose + meaning only the dyn runtime knows, and C has no way to ask. + Refused by name rather than let through as an integer. *) + | Types.Dyn -> + fail loc + "%s of %s is dyn, which does not cross to C. A dyn is one word \ + and would pass as an integer, but what the word means is the \ + dyn runtime's and there is nothing on the C side that can ask \ + — take the value at a written type and pass that" + what fn.Ast.name | _ -> fail loc "%s of %s is %s, which cannot cross to C directly — pass \ diff --git a/lib/emit.ml b/lib/emit.ml index fcd3cdf..6d4264f 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -2343,6 +2343,17 @@ and cast f ~guard (x : Tast.expr) target = runtime answers a pointer or NULL and the Option is built in the checker, so the null test is one integer compare on the address. *) | Types.Ptr _, Types.Int Types.I64 -> "ptrtoint" + (* Not written in the surface language either — this language has no + conversion between bool and a number, and deliberately. The dyn + boundary needs both halves: runtime/flan_dyn.h takes and answers a + bool as an [int32_t], because a C signature saying [_Bool] is a width + question nobody wants, and [bool] is an [i1] here. + + The truncation is safe in the one direction it runs: what comes back + from [flan_dyn_need_bool] is 0 or 1, because the runtime has already + decided the value was a bool, so the discarded bits are zero. *) + | Types.Bool, Types.Int _ -> "zext" + | Types.Int _, Types.Bool -> "trunc" | _ -> failwith "unsupported cast" in if op = "bitcast" then v diff --git a/lib/types.ml b/lib/types.ml index 18c2ba2..a780fa2 100644 --- a/lib/types.ml +++ b/lib/types.ml @@ -83,7 +83,7 @@ let fkind_of_name = function is spelled [()] in source, and [Parse.texpr] refuses the word. *) let primitive_names = [ "i8"; "i16"; "i32"; "i64"; "u8"; "u16"; "u32"; "u64"; - "f32"; "f64"; "bool"; "string"; "Unit"; "Never"; "Allocator" ] + "f32"; "f64"; "bool"; "string"; "dyn"; "Unit"; "Never"; "Allocator" ] let ikind_name k = (if signed k then "i" else "u") ^ string_of_int (bits k) diff --git a/mix b/mix new file mode 100755 index 0000000000000000000000000000000000000000..362c28db85e76526692f34348dad2bbe5e95bf3d GIT binary patch literal 83160 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 literal 0 HcmV?d00001 diff --git a/test/programs/dyn-vec.flan b/test/programs/dyn-vec.flan new file mode 100644 index 0000000..6d74b5a --- /dev/null +++ b/test/programs/dyn-vec.flan @@ -0,0 +1,24 @@ +;;;; A container holding four different types at once. +;;;; +;;;; (vec-new dyn) is not a (Vec dyn) — it is the dyn runtime's own vector, and +;;;; its type is dyn like everything else the runtime hands back. That is what +;;;; lets push, at and len on it be the dyn operations rather than a +;;;; type-erased Vec over eight-byte elements, and it is why no allocator is +;;;; named: the storage is the collector's to walk. + +(defn main [] () + (let [xs (vec-new dyn)] + (push xs 1) + (push xs 2.5) + (push xs "three") + (push xs true) + (print (len xs)) + (print "\n") + (print xs) + (print "\n") + ;; Read back out one at a time, to show that at answers a dyn and that the + ;; four of them are still four different things. + (dotimes [i (len xs)] + (print (at xs i)) + (print " ")) + (print "\n"))) From 9f2f0b1635961b8a1e24524d96dd438b525d5189 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 19 Sep 2026 06:12:41 +0700 Subject: [PATCH 4/7] Roots, pushed where the addresses are stable and popped where the frame leaves A precise collector has to be told where the live dyn words are, and the shadow stack next door is the precedent for where that goes: set up in the entry block, undone in ret, which is the one funnel all five exits pass through -- the tail, both returns, the none arm of (some x), and the landing block a handled condition unwinds through. A pop written only on the normal path would leave a frame's roots on the stack after every handled error. It differs from the shadow stack in two ways, and both are forced. It is not gated on dev: a backtrace is a convenience and a collector that cannot find its roots frees live values. And it is a count rather than a saved head pointer, because the ABI offers root_pop(n) and no way to read the stack's height -- so the number has to be known before the body is emitted, since ret runs during emission and a tally accumulated as roots were discovered would be short at every early return. dyn_roots works it out up front by walking the same nodes the emission will visit, the slots are minted from that count at entry, and dyn_tmp only hands them out. The pushes and the pops balance by construction rather than by two walks agreeing. Every dyn-producing call is spilled into a rooted slot the moment it exists. An SSA value is invisible to a collector that finds roots by address, and the next allocation could be the one that frees what it holds. Rooting all of them rather than only those that outlive a call is conservative and is the only thing available here: this file has no liveness and no lexical scope, the checker having resolved both into flat slot indices long before. The cost is a stack slot and a store per dyn value at every optimisation level, because a rooted alloca has its address escape and mem2reg cannot promote it. That is the price of an address-registration ABI rather than stack maps. A function with no dyn emits nothing at all -- no push, no pop, not a pop of zero -- which is what makes an annotated program's IR identical to what it was before any of this existed. Globals are rooted in main, before the startup function that fills them and before any other push, because every pop takes the top of the stack and these are the ones that must never be at the top. They are never popped, which is what a global's extent means. A dyn global needed no new machinery otherwise: a call is not a constant, so it is a computed global, and that already existed. --- lib/emit.ml | 192 +++++++++++++++++++++++++++++++++- test/programs/dyn-global.flan | 21 ++++ 2 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 test/programs/dyn-global.flan diff --git a/lib/emit.ml b/lib/emit.ml index 6d4264f..55b98b5 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -600,6 +600,30 @@ type f = { what makes the pop happen on the transfer path as well as the normal one. [None] in a release build, where there is no frame at all. *) mutable frame : string option; + (* How many dyn roots this function pushed at entry, and so how many one + [flan_dyn_root_pop] at each exit takes off. Zero for every function with + no dyn in it, which is every function in every program written so far — + and zero means *nothing is emitted at all*, neither push nor pop nor a + pop of zero. That is what keeps a fully annotated program's IR byte for + byte what it was before dyn existed, which is the thing [--no-gc] + promises and is tested for. + + It is a count and not a saved depth because the ABI offers + [flan_dyn_root_pop(n)] and no way to read the stack's height; it can be a + count, rather than needing one, because the number is a static property of + the function that [dyn_roots] works out before a line of the body is + emitted. That matters: [ret] runs *during* emission, and a count + accumulated as roots were discovered would be short at every early + return. *) + mutable droots : int; + (* The root slots' addresses, in push order: the dyn slots first and then one + per dyn-producing runtime call, minted by [dyn_tmp] as the body is + emitted. Both kinds are entry-block allocas, so the addresses are good for + the function's whole extent — which is why rooting is per function here + and not per scope. A slot that is not live any more holds a value the + collector keeps one cycle longer than it must, and that is the safe + direction to be wrong in. *) + mutable droot_ns : string list; (* Where a dev build records each slot's address, so that a stopped frame's locals can be read. [None] in a release build and in a function with no named slot at all. Only *named* slots are recorded: a slot the compiler @@ -666,10 +690,67 @@ let label f name = this frame, so a pop written only on the normal path leaves a dead frame on the stack after every handled error, and the next backtrace is a lie. Same lesson [emit_with_alloc] learned about the context allocator. *) +(* How many dyn roots a function will push, worked out before any of it is + emitted. One per dyn slot — a parameter or a local of that type — and one per + runtime call that answers a dyn, because the word a call hands back is live + from the moment it exists and the next allocation may be the one that + collects it. + + Rooting every dyn-producing call, rather than only the ones whose value + outlives a call, is conservative and is the only thing available: this file + has no liveness and no lexical scope, both of which the checker resolved + away into flat slot indices long before anything got here. The cost is real + and is the cost of a precise collector with an address-registration ABI + rather than stack maps — a rooted alloca has its address escape through + [flan_dyn_root_push], so mem2reg cannot promote it, and every dyn value + becomes a stack slot with a store at every optimisation level. + + A count rather than a running tally for the reason [droots] gives: [ret] is + reached while the body is still being emitted. *) +let dyn_roots (fn : Tast.fn) = + let slots = + Array.fold_left + (fun acc t -> if t = Types.Dyn then acc + 1 else acc) 0 fn.Tast.slots + in + let temps = ref 0 in + let count (e : Tast.expr) = + match e.Tast.e with + | Tast.Prim (Tast.Rt _, _) when e.Tast.ty = Types.Dyn -> incr temps + | _ -> () + in + List.iter (Tast.walk count) fn.Tast.body; + slots + !temps + +(* The next pre-made root slot for a dyn temporary. They are all minted, zeroed + and pushed in the entry block before a line of the body is emitted, and this + only hands them out — which is what makes the pushes and the pops balance by + construction rather than by the body being walked the same way twice. + + [dyn_roots] counts the same nodes the emission visits, so the supply runs + out only if those two disagree. If it ever does, the fallback is an ordinary + unrooted slot: one temporary the collector cannot see is a bug to find, + where a root stack that pops more than it pushed is memory corruption. *) +let dyn_tmp f = + match f.droot_ns with + | n :: rest -> f.droot_ns <- rest; n + | [] -> + let name = Printf.sprintf "%%dx%d" f.n in + f.n <- f.n + 1; + Buffer.add_string f.allocas (Printf.sprintf " %s = alloca i64\n" name); + name + let ret f v = (match f.frame with | Some prev -> ins f "store ptr %s, ptr @flan_frame_head" prev | None -> ()); + (* The pop, on every path out, for exactly the reason the shadow stack's is + here: a condition handled further out unwinds through the landing block, + and a pop written only on the normal path would leave this function's + roots on the stack after every handled error. The [unreachable] + terminators emit none, and are right not to — each of them dies inside C + and the process does not come back. *) + if f.droots > 0 then + ins f "call void @flan_dyn_root_pop(i64 %d)" f.droots; term f "ret %s %s" (ll f.ret) v (* The store that says "this slot is bound now". Emitted at each binding of a @@ -2262,6 +2343,18 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) = let t = fresh f in ins f "%s = call %s @%s(%s)" t (ll e.Tast.ty) sym args'; if signals then guard f; + (* A dyn word is spilled into a rooted slot the instant it exists. It is + an SSA value otherwise, and an SSA value is invisible to a collector + that finds its roots by address — the next allocation could be the one + that frees what this is holding. [dyn_roots] counted this call, so the + slot below is one the entry block has already pushed. + + The value carries on being used as a register: the store is what the + collector reads, and reading it back would only make the IR longer. *) + if e.Tast.ty = Types.Dyn then begin + let slot = dyn_tmp f in + ins f "store i64 %s, ptr %s" t slot + end; t end | Tast.SizeOf t, [] -> Printf.sprintf "%d" (fst (lay f.md t)) @@ -2431,6 +2524,7 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) = pads = []; loops = []; unwind = "unwind"; unwound = false; defers = fn.Tast.fdefers; frame = None; slotv = None; snames = fn.Tast.snames; + droots = 0; droot_ns = []; dsub; dline = (if fn.Tast.floc.Loc.line = 0 then 1 else fn.Tast.floc.Loc.line); dloc = ""; @@ -2449,6 +2543,54 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) = Buffer.add_string f.allocas (Printf.sprintf " store %s %%p%d, ptr %s\n" (ll ty) i f.slots.(i))) fn.Tast.params; + (* The dyn roots, and this is not gated on [m.dev]: the shadow stack below is + a debugging convenience and a release build does without it, while a + collector that cannot find its roots is a collector that frees live + values. Every build pays this, and only a function that has a dyn in it + pays anything — [dyn_roots] is zero otherwise and not a line is emitted, + which is what makes an annotated program's IR identical with and without + --no-gc. + + The dyn *slots* are already allocas from the loop above, so they are + pushed where they are; the temporaries need slots of their own, and they + are minted here, in order, so that [dyn_tmp] only has to hand them out. + Zeroed because the push happens at entry and the call that fills one may + be inside a branch that never runs — runtime/flan_dyn.h says a rooted slot + holding 0 is not a value. *) + let nroots = dyn_roots fn in + if nroots > 0 then begin + let nparams = List.length fn.Tast.params in + let pushed = ref [] in + Array.iteri + (fun i t -> + if t = Types.Dyn then begin + (* A parameter's slot was filled from [%pN] a few lines above and + must not be zeroed over the top of it. Every other slot holds + whatever the stack held until its binding runs, and the binding + may be inside a branch that does not. *) + if i >= nparams then + Buffer.add_string f.allocas + (Printf.sprintf " store i64 0, ptr %s\n" f.slots.(i)); + pushed := f.slots.(i) :: !pushed + end) + fn.Tast.slots; + let ntemps = nroots - List.length !pushed in + let temps = + List.init ntemps (fun i -> + let name = Printf.sprintf "%%dr%d" i in + Buffer.add_string f.allocas (Printf.sprintf " %s = alloca i64\n" name); + Buffer.add_string f.allocas + (Printf.sprintf " store i64 0, ptr %s\n" name); + name) + in + List.iter + (fun n -> + Buffer.add_string f.allocas + (Printf.sprintf " call void @flan_dyn_root_push(ptr %s)\n" n)) + (List.rev !pushed @ temps); + f.droots <- nroots; + f.droot_ns <- temps + end; (* The shadow stack's push, in the entry block, and the pop is at every [ret] (see [ret]). plan.org has had *Frames: shadow stack* in the dev column since the beginning; this is it, and it is dev-only, so a shipped @@ -2935,14 +3077,53 @@ declare i64 @flan_file_fail_reason() declare i8 @flan_slurp_into(ptr, ptr, i64, i64, ptr, i64) |} +(* Whether the program has a dyn in it anywhere, which is the one question + [main] asks before calling [flan_gc_init]. Asked of the whole program rather + than assumed, so that a program with no dyn emits no call and its [main] is + byte for byte the [main] it was before any of this existed. + + Every shape a dyn can take is one of these: a global of that type, a + signature that mentions it, a slot that holds one, or an expression that + produces one. *) +let uses_dyn (p : Tast.program) = + let found = ref false in + let note t = if t = Types.Dyn then found := true in + List.iter (fun (g : Tast.global) -> note g.Tast.gty) p.Tast.globals; + List.iter + (fun (fn : Tast.fn) -> + List.iter note fn.Tast.params; + note fn.Tast.ret; + Array.iter note fn.Tast.slots; + List.iter (Tast.walk (fun (e : Tast.expr) -> note e.Tast.ty)) fn.Tast.body) + p.Tast.fns; + !found + (* C's main, adapting to whichever of the four shapes Flan's main has: argv and the i32 status are each optional (plan.org, Milestone-2 primitives). *) -let emit_main m ?(startup = false) (fn : Tast.fn) = +let emit_main m ?(startup = false) ?(gc = false) ?(dyn_globals = []) (fn : Tast.fn) = let b = Buffer.create 256 in Buffer.add_string b (Printf.sprintf "\ndefine i32 @main(i32 %%argc, ptr %%argv)%s {\nentry:\n" (attrs m)); Buffer.add_string b " call void @flan_rt_init(i32 %argc, ptr %argv)\n"; + (* Immediately after the host runtime and before anything that could box: a + dyn global's initialiser runs in the startup function below, and the very + first thing it does is allocate. *) + if gc then Buffer.add_string b " call void @flan_gc_init()\n"; + (* The dyn globals, rooted here and never popped, which is the whole of what + a global's extent means. They go on the stack *before* the startup + function runs, because that function is what fills them and its first + allocation may be the one that collects — and before any of it pushes a + root of its own, because every pop in the program takes the top of the + stack and these are the ones that must never be at the top. + + Zero is what a global holds until its initialiser has run: BSS gives that + for free, and runtime/flan_dyn.h says a rooted slot holding 0 is not a + value. *) + List.iter + (fun g -> Buffer.add_string b + (Printf.sprintf " call void @flan_dyn_root_push(ptr %s)\n" (gname g))) + dyn_globals; (* The program's own end of the transfer channel. Nothing can be transferring when [main] returns: a restart is found by name on the restart stack, and an [invoke-restart] that finds none fails at the invoke site rather than @@ -3247,7 +3428,14 @@ let program ?(checks = true) ?(dev = false) ?(debug = false) ?(pnames = []) p.Tast.fns; let startup = emit_startup m ~hidden p.Tast.globals in (match List.find_opt (fun (f : Tast.fn) -> f.Tast.name = "main") p.Tast.fns with - | Some fn -> emit_main m ~startup fn + | Some fn -> + emit_main m ~startup ~gc:(uses_dyn p) + ~dyn_globals: + (List.filter_map + (fun (g : Tast.global) -> + if g.Tast.gty = Types.Dyn then Some g.Tast.gname else None) + p.Tast.globals) + fn (* A program with no [main] is linked into a C host that brings its own entry point, and then nothing calls the startup function — which is why the constant image is a constant image on both backends and not a diff --git a/test/programs/dyn-global.flan b/test/programs/dyn-global.flan new file mode 100644 index 0000000..e16f16d --- /dev/null +++ b/test/programs/dyn-global.flan @@ -0,0 +1,21 @@ +;;;; A dyn global, which is the case that needs the startup function. +;;;; +;;;; A dyn value is made by a call into the runtime, and a call is not a +;;;; constant, so the initialiser cannot be a constant image the way a typed +;;;; global's is. It runs in flan..init-globals, which main calls after +;;;; flan_gc_init and before anything the programmer wrote — the same machinery +;;;; the computed globals already use, which is the point: a dyn global is a +;;;; computed global and needed no new mechanism. + +(defvar counter dyn 0) +(defvar label dyn "start") + +(defn bump [] () + (set counter (+ counter 1))) + +(defn main [] () + (print counter) (print " ") (print label) (print "\n") + (bump) + (bump) + (set label "done") + (print counter) (print " ") (print label) (print "\n")) From 7c7586ebc666790842f12127a3b6b4b791786436 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 19 Sep 2026 06:33:28 +0700 Subject: [PATCH 5/7] --no-gc is a pass, not a flag the emitter can see The promise is that this program carries no collector, and the way to keep it is to refuse every dyn rather than to emit a different program: a dyn value is one the runtime allocates and the collector owns, and there is no smaller version to fall back to. So it runs between checking and emission, answers unit or raises, and hands the very same program on. Emit has no field to branch on and is told nothing. That is what makes the byte-identity claim true rather than approximate, and it is tested by compiling three annotated programs twice and comparing the text. A field, a mode, or a comment that mentioned the flag would break it on something incidental, a long way from anything to do with dyn. Every site is named, the way the global cycle refusal names the whole ring: a reader who has to annotate their program wants the list, not the first one and then another compile. Globals and signatures as well as body values -- the two files it is tested against report nine sites each, and the floors are set under that so an added line does not fail the test and a pass that named one site and stopped would. The four programs run at -O2 and -O0. dyn-boundary is asserted on its exit status as well as its output, because the boundary is only interesting in that it can fail and a test that showed it working would be testing the easy half. The x86 survey skips them by name: a REFUSED there means a node that backend has stopped lowering, which is a regression, and this is the opposite -- a lane that has not started. Take a name off llvmonly when the lowering arrives and the survey will say whether it works. 128 match, 0 differ, 0 refused. Checked while writing these: a dyn function with an early return pops its roots on both paths, and one with a defer pops on the transfer path too. --- bin/main.ml | 25 +++++- lib/check.ml | 67 ++++++++++++++++ spike/x86/survey.sh | 10 +++ test/programs/dyn-boundary.flan | 42 ++++++++++ test/test_acceptance.ml | 132 ++++++++++++++++++++++++++++++++ test/test_flan.ml | 47 ++++++++++++ 6 files changed, 319 insertions(+), 4 deletions(-) create mode 100644 test/programs/dyn-boundary.flan diff --git a/bin/main.ml b/bin/main.ml index 032fe0e..1b3193e 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -195,9 +195,17 @@ let llvm_flag = "--llvm" assembler is worth measuring rather than asserting. *) let no_annotate_flag = "--no-annotate" +(* "This program carries no collector", and the way it is kept is a refusal + rather than a different lowering: every dyn left in the program is named, + with its location, and nothing downstream is told the flag was given. See + [Check.no_gc], which is a pass between checking and emission and answers + unit — an annotated program's output is byte for byte what it is without + the flag, and that is the property the flag is worth having for. *) +let no_gc_flag = "--no-gc" + let flags = [ no_checks_flag; dev_flag; debug_flag; sanitize_flag; two_process_flag; - x86_flag; llvm_flag; no_annotate_flag ] + x86_flag; llvm_flag; no_annotate_flag; no_gc_flag ] (* Which backend a command got, from the two flags and the default it would have taken. One function because there is one rule, and the only thing that @@ -616,8 +624,12 @@ let () = with_errors path (fun () -> let l = load path in let pnames = if debug then param_names l else [] in - Flan.Check.program_all l.decls - |> Flan.Emit.program ~checks ~dev ~debug ~pnames ~sanitize + let p = Flan.Check.program_all l.decls in + (* Between checking and emission, and it hands the very same program + on: the flag is a question asked of what was checked, never a + parameter of what is emitted. *) + if List.mem no_gc_flag args then Flan.Check.no_gc p; + Flan.Emit.program ~checks ~dev ~debug ~pnames ~sanitize p |> print_string)) files | _ :: "build" :: path :: rest -> @@ -654,12 +666,17 @@ let () = prerr_endline "usage: flan build [-o out] [-O0|-O1|-O2|-O3] \ [--no-bounds-checks] \ - [--dev] [--debug] [--sanitize] [--target=wasm32-wasi|web|js]"; + [--dev] [--debug] [--sanitize] [--no-gc] [--target=wasm32-wasi|web|js]"; exit 2 in with_errors path (fun () -> let l = load path in let p = Flan.Check.program_all l.decls in + (* Before reachability rather than after: a dyn in a function nothing + calls is still a dyn somebody wrote, and a refusal that depended on + what [main] happened to reach would come and go as the program was + edited elsewhere. *) + if List.mem no_gc_flag rest then Flan.Check.no_gc p; (* The link follows the program, not the import list: a package nothing reachable calls into contributes no C and no linker argument, and its functions are not emitted either. That is what lets one file import diff --git a/lib/check.ml b/lib/check.ml index 50be294..a8bf82a 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -7080,3 +7080,70 @@ let expression env (e : Ast.expr) : let t = check ctx e in (t, Array.of_list (List.rev ctx.slot_tys), Array.of_list (List.rev ctx.slot_names)) + +(* ── --no-gc ──────────────────────────────────────────────────────────── + + The flag that says this program is to be compiled with no collector in it, + and the way to keep that promise is to refuse every dyn rather than to emit + a different program. A dyn value is a value the runtime allocates and the + collector owns; there is no smaller version of it to fall back to, and + quietly leaking instead would be a memory model nobody asked for. + + So this is a pass and not a flag. It runs between [Check] and [Emit], it + answers unit or it refuses, and nothing downstream of it is told the flag + exists — which is what makes a fully annotated program's output byte for + byte identical with the flag and without it. Emit has no [no_gc] field to + branch on, and that is deliberate: a field would be one more thing that + could change a comment, a name or an ordering, and the identity is worth + more than the branch would ever buy. + + Every site is named, the way the global cycle refusal names the whole ring + rather than one member of it. A reader who has to annotate their program + wants the list, not the first one and then another compile. *) + +let dyn_sites (p : Tast.program) : Loc.diag list = + let found = ref [] in + let add loc what = found := (loc, what) :: !found in + List.iter + (fun (g : Tast.global) -> + if g.Tast.gty = Types.Dyn then + add g.Tast.ginit.Tast.loc (Printf.sprintf "the global %s" g.Tast.gname)) + p.Tast.globals; + List.iter + (fun (fn : Tast.fn) -> + List.iteri + (fun i t -> + if t = Types.Dyn then + add fn.Tast.floc + (Printf.sprintf "parameter %d of %s" (i + 1) fn.Tast.name)) + fn.Tast.params; + if fn.Tast.ret = Types.Dyn then + add fn.Tast.floc (Printf.sprintf "the return type of %s" fn.Tast.name); + (* The body's own dyn values, which are the ones a signature does not + show: a let bound to a boxed literal, a (vec-new dyn) deep inside an + expression. Reported at the node, because that is the character to + change. *) + List.iter + (Tast.walk + (fun (e : Tast.expr) -> + match e.Tast.e with + | Tast.Prim (Tast.Rt sym, _) + when e.Tast.ty = Types.Dyn + && String.length sym > 8 + && String.sub sym 0 8 = "flan_dyn" -> + add e.Tast.loc (Printf.sprintf "this value in %s" fn.Tast.name) + | _ -> ())) + fn.Tast.body) + p.Tast.fns; + List.rev_map + (fun (loc, what) -> + Loc.diag ~kind:"check/no-gc" loc + (Printf.sprintf + "%s is dyn, and --no-gc says this program carries no collector. A \ + dyn value is one the runtime allocates and the collector owns, so \ + there is nothing smaller to compile it to — write the type" + what)) + !found + +let no_gc (p : Tast.program) = + match dyn_sites p with [] -> () | ds -> raise (Loc.Errors ds) diff --git a/spike/x86/survey.sh b/spike/x86/survey.sh index 85ac83d..919ae80 100755 --- a/spike/x86/survey.sh +++ b/spike/x86/survey.sh @@ -78,6 +78,15 @@ out=$(mktemp -d); trap 'rm -rf "$out"' EXIT # truncations are both empty. forever="dev-loop dev-watch dev-chatty" +# The dyn programs, which this backend refuses by name and is meant to: every +# operation on a dyn value is a call into the dynamic runtime and x86.ml emits +# none of them. They are listed rather than left to be counted as refusals +# because a REFUSED here means "a node this backend has stopped lowering", +# which is a regression, and this is the opposite -- a lane that has not +# started. Take a name off this list when the backend grows the lowering, and +# the survey will say whether it works. +llvmonly="dyn-basic dyn-vec dyn-global dyn-boundary" + TIMEOUT=${TIMEOUT:-20} # Extra flags, given to *both* sides. SURVEY_FLAGS=--dev is the one that has a @@ -101,6 +110,7 @@ for src in "$corpus"/test/programs/*.flan "$corpus"/spike/x86/*.flan \ [ $want = 1 ] || continue fi case " $forever " in *" $name "*) skip+=("$name:runs-forever"); continue;; esac + case " $llvmonly " in *" $name "*) skip+=("$name:dyn-is-llvm-only"); continue;; esac # LLVM first. A program that does not compile at all, or has no main, is not # this backend's business -- the frontend refused it either way. diff --git a/test/programs/dyn-boundary.flan b/test/programs/dyn-boundary.flan new file mode 100644 index 0000000..9556c0e --- /dev/null +++ b/test/programs/dyn-boundary.flan @@ -0,0 +1,42 @@ +;;;; The boundary in both directions, and the trap when a claim is wrong. +;;;; +;;;; Typed to dyn is implicit: take-dyn is called with an i64 and the boxing is +;;;; written nowhere. Dyn to typed is not: take-i64's parameter says i64, and +;;;; that annotation is the whole of why the unboxing is allowed to happen — +;;;; and the whole of why it may fail, which the last line of main proves by +;;;; handing it a float. +;;;; +;;;; A let carries no type in this language, so the annotation sites a dyn can +;;;; be unboxed at are the ones that do: a parameter, a return type, and a +;;;; global's declared type. All three are here. + +(defvar seven i64 7) +(defvar boxed dyn 21) +;; The other direction at a global: a dyn initialiser meeting a written type. +(defvar unboxed i64 boxed) + +(defn take-dyn [d dyn] dyn + (+ d 100)) + +(defn take-i64 [n i64] i64 + (* n 2)) + +(defn identity-dyn [d] dyn d) + +;; A dyn value answered at a written return type, which is the third site. +(defn as-i64 [d] i64 d) + +(defn main [] () + ;; Typed in: the i64 is boxed at the call with nothing written. + (print (take-dyn seven)) + (print "\n") + ;; Dyn out: the parameter is typed, so the word is unboxed at the call. + (print (take-i64 boxed)) + (print "\n") + (print unboxed) + (print "\n") + (print (as-i64 (identity-dyn 5))) + (print "\n") + ;; And the claim that is wrong. The runtime owns the message. + (print (take-i64 (identity-dyn 1.5))) + (print "\n")) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 78aab49..69f2e82 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -2967,6 +2967,138 @@ level "1" outputs ~opt:"-O0" "unions, -O0" "programs/unions.flan" unions_out; outputs ~dev:true "unions, dev" "programs/unions.flan" unions_out; + (* ── dyn, milestone 1 ──────────────────────────────────────────── + Four programs, and between them every claim the feature makes that can + be run rather than argued. + + [dyn-basic] is the one the feature exists for: a defn that annotates + nothing, called at two types, answering correctly to both. Nothing the + typed language can express does that. + + [dyn-vec] is the heterogeneous container, which is where a dynamic + language stops being a convenience and starts being a different data + model — four types in one vector, read back out one at a time. + + [dyn-global] is the case that needed the startup function: a call is not + a constant, so a dyn global is a computed global, and it turned out to + need no new machinery at all. + + [dyn-boundary] is the one with a trap in it, and it is asserted on its + exit status and its message: the boundary is only interesting because it + can fail, and a test that only showed it working would be testing the + easy half. It is at -O2 and -O0 like the rest, because the unboxing is a + call whose result feeds a machine instruction and that is exactly the + shape the optimiser could launder away. + + They are LLVM-only, and the [@x86] survey skips them by name — see + [llvmonly] in spike/x86/survey.sh. Not compiled by the dev backend, so + not run as dev builds either. *) + let dyn_basic_out = "5\n3.75\n" in + outputs "dyn: an unannotated defn at two types" + "programs/dyn-basic.flan" dyn_basic_out; + outputs ~opt:"-O0" "dyn: an unannotated defn at two types, -O0" + "programs/dyn-basic.flan" dyn_basic_out; + let dyn_vec_out = "4\n[1 2.5 three true]\n1 2.5 three true \n" in + outputs "dyn: a heterogeneous vector" + "programs/dyn-vec.flan" dyn_vec_out; + outputs ~opt:"-O0" "dyn: a heterogeneous vector, -O0" + "programs/dyn-vec.flan" dyn_vec_out; + let dyn_global_out = "0 start\n2 done\n" in + outputs "dyn: a global" "programs/dyn-global.flan" dyn_global_out; + outputs ~opt:"-O0" "dyn: a global, -O0" + "programs/dyn-global.flan" dyn_global_out; + + (* The boundary, both directions, and then the claim that is wrong. The + first four lines are the conversions; the trap is the fifth, and the + runtime owns its wording — the compiler could only have said that two + dyns did not agree, which is what they are for. *) + let dyn_boundary ?opt () = + let exe = compile ?opt "programs/dyn-boundary.flan" in + let code, text = run exe None in + let want = "107\n42\n21\n5\n" in + let name = + "dyn: the boundary both ways, and the trap" + ^ (match opt with Some o -> ", " ^ o | None -> "") + in + if code <> 134 + || not (contains text want) + || not (contains text "required to be an i64") + then begin + incr failures; + Printf.printf + "FAIL %s\n got: %S (exit %d)\n wanted: %S then a trap \ + (exit 134)\n" + name text code want + end; + (try Sys.remove exe with Sys_error _ -> ()) + in + dyn_boundary (); + dyn_boundary ~opt:"-O0" (); + + (* ── --no-gc ───────────────────────────────────────────────────── + The flag is a pass between checking and emission that answers unit or + refuses, and these are its two halves. + + Every dyn is named. Not the first one and then another compile: a reader + who has to annotate their program wants the list, which is why the pass + collects and raises [Loc.Errors] the way the global cycle refusal does. + Asserted on the count as well as on the text, because "it refused" would + pass just as well if it named one site and stopped. *) + let no_gc_sites path least = + let l = Load.program ~file:path (Reader.read_file path) in + let p = Check.program_all l.Load.decls in + match Check.no_gc p with + | () -> + incr failures; + Printf.printf "FAIL --no-gc on %s: it was accepted\n" path + | exception Loc.Errors ds -> + if List.length ds < least then begin + incr failures; + Printf.printf + "FAIL --no-gc on %s: named %d sites, wanted at least %d\n" + path (List.length ds) least + end; + List.iter + (fun (d : Loc.diag) -> + if not (contains d.Loc.dmsg "carries no collector") then begin + incr failures; + Printf.printf "FAIL --no-gc on %s: said %S\n" path d.Loc.dmsg + end) + ds + in + (* The vec file reports nine: a vec-new, four boxed pushes, a len, an at + and the dotimes bound. The floor is under that rather than equal to it + so an added line does not fail the test, and well over one so that a + pass which named the first site and stopped would. *) + no_gc_sites "programs/dyn-vec.flan" 8; + (* The global file reports nine too, and the point of it is the mix: two + [the global ...] sites and two [the return type of ...] ones, which a + walk over function bodies alone would never have found. *) + no_gc_sites "programs/dyn-global.flan" 6; + + (* The other half, and the reason the flag is a pass and not a parameter of + [Emit]: a program with nothing to refuse compiles to the same bytes with + the flag and without it. If [--no-gc] were ever plumbed into the emitter + — a field, a mode, a comment that mentioned it — this is what would + start failing, and it would fail on something incidental rather than on + anything to do with dyn. *) + let identical path = + let l = Load.program ~file:path (Reader.read_file path) in + let p = Check.program_all l.Load.decls in + let a = Emit.program p in + Check.no_gc p; + let b = Emit.program p in + if a <> b then begin + incr failures; + Printf.printf + "FAIL --no-gc changed the IR of %s (%d bytes vs %d)\n" + path (String.length a) (String.length b) + end + in + identical "programs/algorithms.flan"; + identical "programs/conditions.flan"; + identical "programs/unions.flan"; + (* The refusals, each by name. The first is the diagnostics bug NEXT.md listed and this lane fixed: a case name written as if it were a struct reported "unknown struct A", because nothing in the environment could diff --git a/test/test_flan.ml b/test/test_flan.ml index 817e0bd..cd0041d 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -866,6 +866,53 @@ let () = rejects_check "an unknown concrete type" "(defn f [x Widget] ())" ~needle:"unknown type Widget"; + (* ── dyn, and what it does not do yet ──────────────────────────── *) + + (* The pairing rule's own refusal. A name that is also a type's has no good + reading — taken as written it is a parameter called [i64] — and the + likelier intent is a pair the wrong way round, which the message names. *) + rejects_check "a parameter named after a type" "(defn f [i64 x] ())" + ~needle:"cannot also be this parameter's name"; + + (* The three "not yet" refusals, each by name and each for its own reason. + + A typed container does not box: [(Vec i64)] has a representation the dyn + runtime cannot walk, and the heterogeneous container at this milestone is + the runtime's own from [(vec-new dyn)]. *) + rejects_check "a typed container boxed into dyn" + "(defn take [d dyn] i32 1)\n\ + (defn main [] i32 (take [1 2 3]))" + ~needle:"does not cross into dyn yet"; + (* A condition crosses a handler boundary as a pointer to a live frame, and + a dyn payload has to stay rooted across that transfer — the collector's + question, and milestone 2's. *) + rejects_check "a dyn in a condition's payload" + "(defstruct Boom [what dyn])\n\ + (defn main [] () (signal (Boom {.what 1})))" + ~needle:"milestone 2"; + (* And the C boundary, which is the one that would otherwise pass silently: + a dyn is one word and would cross as an integer, and nothing on the other + side can ask what the word means. *) + rejects_check "a dyn crossing to C" + "(declare c-take [d dyn] () \"c_take\")" + ~needle:"does not cross to C"; + + (* The x86 backend refuses dyn by name, and the sentence has to be good: the + dev daemon takes that backend by default, so this is the first thing a + user of dyn sees. Neither half of the message names [--llvm] — Session and + main.ml each add that, differently and for their own reasons — so what is + pinned here is the half this file owns. *) + (match + X86.program ~checks:true + (Check.program_all + (program "(defn add [x y] dyn (+ x y))\n\ + (defn main [] () (print (add 1 2)))")) + with + | _ -> check "the x86 backend refuses dyn" false + | exception X86.Unsupported m -> + check "the x86 backend refuses dyn by name" + (contains m "a dyn value" && contains m "dynamic runtime")); + (* ── Static bounds ─────────────────────────────────────────────── *) (* A literal index into a fixed array is known now, so it is an error now rather than a trap later; everything else is the emitted bounds check's From 4aa24c0e33450e98877a422474a7823139409492 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 19 Sep 2026 06:36:23 +0700 Subject: [PATCH 6/7] The zero-root question is the integrator's, and the handoff says which asks are open Two decisions in this lane went against the brief and both are written down where the next reader will meet them: the return slot stayed mandatory, so the third state ret = None was to grow does not exist and neither does the fallout listed for load.ml, shim.ml and cimport.ml; and the parameter rule is resolved in Check rather than in parse.ml, because cimport passes C type names through verbatim and POSIX's lowercase stat and timespec are writable in parameter position, which is what makes a syntactic rule unsound rather than merely awkward. The open ABI point is in flan_dyn.h beside the root functions rather than only in the handoff, because the header is what the two sides diff. A rooted slot holding 0 is not a value: the compiler zeroes every root at entry because the push happens before the code that fills it and possibly for a branch that never runs, and 0 is the only pattern it can write without knowing the encoding. If the real runtime NaN-boxes and integer zero is the zero word then this is wrong and both sides change together. Session.compatible needed nothing: it compares with Types.equal over the parameters and the return, and dyn is equal to itself and to nothing else. Both directions are pinned anyway, because this is the one place "changes signature" covers a change the source does not spell out -- a parameter can become dyn, or stop being dyn, by a type being declared elsewhere in the program. @x86 128 match 0 differ 0 refused, @sanitize clean, dune test green. --- docs/handoffs/HANDOFF-dyn-m1.md | 124 ++++++++++++++++++++++++++++++++ runtime/flan_dyn.h | 24 ++++++- test/test_session.ml | 14 ++++ 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 docs/handoffs/HANDOFF-dyn-m1.md diff --git a/docs/handoffs/HANDOFF-dyn-m1.md b/docs/handoffs/HANDOFF-dyn-m1.md new file mode 100644 index 0000000..d9063a7 --- /dev/null +++ b/docs/handoffs/HANDOFF-dyn-m1.md @@ -0,0 +1,124 @@ +# dyn, milestone 1 — what was decided and what is left + +The compiler half of dynamic-by-default. The runtime half is a sibling's, built +in parallel against `runtime/flan_dyn.h`, which is the fixed ABI and the thing +the two copies are diffed against. + +## Two decisions that differ from the brief + +**The return slot stays mandatory; `dyn` is written out in it.** The brief +expected `ret = None` to grow a third state meaning "unannotated", and listed +mechanical fallout in `load.ml`, `shim.ml` and `cimport.ml`. That fallout does +not exist, because the change was not made. The reason is in `parse.ml` beside +the `defn` case: an optional return slot has *no* syntactic resolution, since a +capitalised head in a list is both a type application and a struct literal — +`(defn f [] (Rune {.code 65}) (bar))` is the misparse that removed the old +optional slot, and it would come straight back. A parameter vector has no such +case, because every slot in it is a name or a type and never an expression. So +`ret = None` still means Unit and only `declare` and the shim produce it. One +token in the return position buys a decision the file paid for twice in one day. + +**The parameter rule is resolved in `Check`, not in `parse.ml`.** The brief +asked for "a known type name is a type, anything else is another dyn param", +written where `parse.ml` argues its other misparse-closing decisions. That +lookup is exactly the one `parse.ml:904` records being removed for being wrong +twice in one day, and at parse time the set of type names is incomplete *by +construction* — macros generate definitions, packages are loaded later, C +headers are imported later. `cimport.ml` decides it: `named env n = tname n` +passes C type names through verbatim, so POSIX's `stat` and `timespec` are +lowercase Flan type names writable in parameter position, and no syntactic rule +("capitalised is a type") can be made sound. + +So the vector is carried undecided as `Ast.pitem`s and paired in +`Check.pair_params`, after every file is loaded, every macro expanded and every +header imported. The argument is written at `parse.ml`'s `defn` case as asked. + +## The residual the parent owns + +The set of type names is complete at a point in time and **not across time**. +`(defn f [x y] ...)` is two dyn parameters until somebody writes +`(defstruct y ...)` — or imports a header that declares one — and then it is one +parameter of type `y`, with no edit to `f`. The signature changes underneath it, +and arity changes with it. + +`Session.compatible` is where that is felt: it compares with `Types.equal` over +parameters and return, so a redefinition that changes dyn-ness is refused like +any other signature change (this falls out; it is pinned in `test_session.ml`). +But the *first* definition after such an edit is the one that changes, and +nothing warns. + +## What the feature costs, and what was taken back + +A parameter slot with no type used to be a syntax error. It is now a `dyn` +parameter, so **a mistyped type silently becomes an extra parameter** — the +arity changes with no diagnostic, which is the failure class `parse.ml` calls +the worst available. Two rules take most of it back, in `dyn_param_or_typo`: + +- a name within one edit of a type's name gets the resolver's own "did you + mean", and +- an unknown **capitalised** name is reported as an unknown type. Not one + parameter in the corpus is capitalised, while `Form`, `Cursor` and `Vector2` + appear in these vectors constantly. + +What is left uncovered is a lowercase name resembling no type: `(defn f [x +widget] ())` is two dyn parameters and nothing in the text says otherwise. That +is the feature working as specified. + +**Sharp edge of the near-miss rule.** `near_miss` treats any two single-char +names as one edit apart, and it compares against every struct name in scope. So +a `(defstruct D ...)` anywhere in the program makes `(defn f [a d] ...)` a +refusal rather than two dyn parameters. The message is actionable — write the +type, or rename — but it is a refusal a user will meet without having done +anything wrong. + +## Open ABI point for the integrator + +**A rooted slot holding 0 is not a value, and the collector must skip it.** +This is written into `runtime/flan_dyn.h` beside the root functions, and it is +the one thing in that header decided by one side alone. Roots are pushed in the +function's entry block, before the code that fills them has run and possibly for +a branch that never runs, so the compiler zeroes every root slot and must mean +something by it — and 0 is the only pattern it can write without knowing the +encoding. + +If the real runtime NaN-boxes and integer zero is the zero word, this is wrong +and the two sides need a different sentinel. Do not fix it on one side. + +## Not in milestone 1, each refused by name with a location + +- a typed container boxing into dyn (`(Vec i64)` → dyn): "not yet"; the + heterogeneous container is the runtime's own from `(vec-new dyn)` +- a dyn in a condition's payload, or in a field of one: milestone 2 — a payload + crosses a handler boundary and must stay rooted across the transfer +- a dyn crossing to C through `declare`/`declare-c`: it is one word and would + have passed as an integer with nothing on the other side able to ask what it + means. This one was **not** in the brief and is the dangerous one, because the + general "cannot cross to C" arm would have caught it with advice (`pass (Ptr + T)`) that is wrong for dyn. +- integer widths other than i64 and floats other than f64 unboxing from dyn: + the ABI carries one of each, and a `need_i64` plus a truncation would put an + implicit narrowing at the one boundary where the value's type was already + uncertain +- the x86 dev backend, and the JS dialect, refuse dyn entirely + +## Roots: what is and is not verified + +Every dyn slot and every dyn-producing runtime call is rooted, pushed in the +entry block and popped at every `ret` — which is the funnel all five exits pass +through, the transfer landing block included. Pushes and pops balance **by +construction**: `dyn_roots` counts before emission, the slots are minted from +that count, and `dyn_tmp` only hands them out. + +**The stub verifies none of this.** `flan_dyn_stub.c` mallocs and never frees, +so a program with entirely wrong root discipline passes every test that runs +against it. What is checked instead is the IR: an early-return function pops on +both paths, and a function with a defer pops on the transfer path. When the real +collector lands, that is the area to re-examine first. + +Cost: a rooted alloca has its address escape through `flan_dyn_root_push`, so +mem2reg cannot promote it. Every dyn local and every dyn temporary is a real +stack slot with a real store, at every optimisation level. That is inherent to a +precise collector with an address-registration ABI rather than stack maps. + +A function with no dyn emits nothing — no push, no pop, not a `pop(0)` — which +is what makes `--no-gc` byte-identity hold. diff --git a/runtime/flan_dyn.h b/runtime/flan_dyn.h index 4dd5c38..27c4454 100644 --- a/runtime/flan_dyn.h +++ b/runtime/flan_dyn.h @@ -100,7 +100,29 @@ int32_t flan_dyn_need_bool(flan_dyn v); * one [flan_dyn_root_pop] per scope carrying the count the scope pushed. * * The address is registered, not the word: the slot is written again while it - * is rooted, and a collection in between has to see the current value. */ + * is rooted, and a collection in between has to see the current value. + * + * ── OPEN POINT FOR THE INTEGRATOR ────────────────────────────────────── + * + * A ROOTED SLOT HOLDING 0 IS NOT A VALUE, AND THE COLLECTOR MUST SKIP IT. + * + * This is a constraint the compiler puts on the encoding, and it is the one + * thing in this header that was decided by one side alone. The reason it is + * forced: roots are pushed in the function's entry block, before the code that + * fills them has run, and a slot may belong to a branch that never runs at + * all. So the compiler zeroes every root slot at entry and has to mean + * something by it, and 0 is the only bit pattern it can write without knowing + * how values are encoded. Globals get the same treatment for free, from BSS. + * + * If the real runtime's encoding makes 0 a legitimate value — a NaN-boxing + * scheme where integer zero is the zero word is the obvious way this breaks — + * then this is wrong and the two sides need a different empty sentinel, which + * is a change to this header that both make together. Do not resolve it by + * changing one side. + * + * The root stack is strictly LIFO and the pops say how many, because there is + * no way to read its depth. Globals are pushed once, in main, before anything + * else and never popped. */ void flan_dyn_root_push(flan_dyn *slot); void flan_dyn_root_pop(int64_t n); diff --git a/test/test_session.ml b/test/test_session.ml index 8cc38fb..e32b55e 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -51,6 +51,20 @@ let () = refuses "a changed arity" "(defn outer [a i64 b i64] i64 (bump))" "changes signature"; + (* Dyn-ness is part of a signature like anything else, and this falls out of + [compatible] rather than being added to it: the comparison is + [Types.equal] over the parameters and the return, and dyn is equal to + itself and to nothing else. Pinned anyway, because it is the one place the + word "signature" covers a change the source does not spell out — the + return type here went from [i64] to [dyn] by being written differently, + and a parameter can change the same way by a *type* being declared + elsewhere in the program. *) + refuses "a return type that became dyn" + "(defn outer [] dyn (bump))" + "changes signature"; + refuses "a parameter that became dyn" + "(defn outer [x] i64 (bump))" + "changes signature"; (* The storage exists and has a shape: reusing it reads at the wrong offsets, and replacing it discards the state the reload exists to preserve. *) refuses "a retyped global" From bd981ff87a0c3cdb3e3627e9224825af7de2b0aa Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 19 Sep 2026 06:40:53 +0700 Subject: [PATCH 7/7] Four dyn values in a defer the collector had never been told about A defer is in the typed IR twice -- spliced into the body for the normal path, and again in fdefers for the path a transfer leaves through -- so a dyn temporary inside one is emitted twice. dyn_roots counted only the body's, and the second copy went into slots nothing had rooted. Nothing failed, and that is the whole reason this is worth a commit of its own. dyn_tmp falls back to a plain slot rather than unbalancing the stack, so the pushes and the pops still matched, the program ran and printed the right answer, and the values were simply invisible. Against a stub that never collects there is no symptom to find -- no leak, no crash, no wrong number. It would have become a symptom the week the real collector landed, in a defer reached only on a handled condition, which is close to the worst place to start looking. What found it was the IR: a rooted slot is spelled %dr and the fallback %dx, and the assertion is that no dyn program in the corpus emits one of the latter. That is now a test over all five dyn programs, and it is the only check in the lane that can see a missing root while there is still nothing to lose one by. When the collector arrives it is the thing to extend rather than replace. Also checked, both clean: flan dev --llvm builds and runs a dyn program, which is the route the x86 refusal sends people to and would have been a link error in the worst possible place; and the daemon's own refusal already names the flag. --- docs/handoffs/HANDOFF-dyn-m1.md | 20 +++++++++++++++--- lib/emit.ml | 9 ++++++++ spike/x86/survey.sh | 2 +- test/programs/dyn-defer.flan | 28 +++++++++++++++++++++++++ test/test_acceptance.ml | 37 +++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 test/programs/dyn-defer.flan diff --git a/docs/handoffs/HANDOFF-dyn-m1.md b/docs/handoffs/HANDOFF-dyn-m1.md index d9063a7..5220e97 100644 --- a/docs/handoffs/HANDOFF-dyn-m1.md +++ b/docs/handoffs/HANDOFF-dyn-m1.md @@ -111,9 +111,23 @@ that count, and `dyn_tmp` only hands them out. **The stub verifies none of this.** `flan_dyn_stub.c` mallocs and never frees, so a program with entirely wrong root discipline passes every test that runs -against it. What is checked instead is the IR: an early-return function pops on -both paths, and a function with a defer pops on the transfer path. When the real -collector lands, that is the area to re-examine first. +against it. What is checked instead is the IR, and that check earned its keep — +it found a real hole. A defer appears twice in the typed IR, spliced into `body` +for the normal path and again in `fdefers` for the path a transfer leaves +through, so a dyn temporary inside one is emitted twice; `dyn_roots` counted +only the body's, and the second copy went into slots the collector had never +been told about. + +Nothing failed, which is the point. `dyn_tmp` falls back to a plain unrooted +slot rather than unbalancing the stack, so the pushes and the pops still +matched, the program ran and printed the right answer, and four dyn values were +simply invisible. Under a stub that never collects there is no symptom at all. + +The assertion that caught it is in `test_acceptance.ml`: a rooted slot is +spelled `%dr` and the fallback `%dx`, and no dyn program in the corpus may emit +the latter. When the real collector lands, that is the check to extend rather +than replace — it is the only one that can see a missing root before there is a +collector to lose one by. Cost: a rooted alloca has its address escape through `flan_dyn_root_push`, so mem2reg cannot promote it. Every dyn local and every dyn temporary is a real diff --git a/lib/emit.ml b/lib/emit.ml index 55b98b5..0a704ba 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -719,6 +719,15 @@ let dyn_roots (fn : Tast.fn) = | _ -> () in List.iter (Tast.walk count) fn.Tast.body; + (* And the transfer path's copy of the defers, which is a second list of the + same expressions and is emitted as well — so it mints a second set of + temporaries, and counting only [body] left every one of them in a slot the + collector never heard of. The fallback in [dyn_tmp] meant that was silent: + the pushes and the pops still balanced, and four dyn values in a defer + reached on a handled condition were simply invisible. Found by emitting + one and counting [%dx] in the IR, which is the only thing that can see it + while the runtime is a stub that never collects. *) + List.iter (Tast.walk count) fn.Tast.fdefers; slots + !temps (* The next pre-made root slot for a dyn temporary. They are all minted, zeroed diff --git a/spike/x86/survey.sh b/spike/x86/survey.sh index 919ae80..1b2cc7d 100755 --- a/spike/x86/survey.sh +++ b/spike/x86/survey.sh @@ -85,7 +85,7 @@ forever="dev-loop dev-watch dev-chatty" # which is a regression, and this is the opposite -- a lane that has not # started. Take a name off this list when the backend grows the lowering, and # the survey will say whether it works. -llvmonly="dyn-basic dyn-vec dyn-global dyn-boundary" +llvmonly="dyn-basic dyn-vec dyn-global dyn-boundary dyn-defer" TIMEOUT=${TIMEOUT:-20} diff --git a/test/programs/dyn-defer.flan b/test/programs/dyn-defer.flan new file mode 100644 index 0000000..af4e495 --- /dev/null +++ b/test/programs/dyn-defer.flan @@ -0,0 +1,28 @@ +;;;; A dyn value produced inside a defer, on a function a transfer leaves +;;;; through rather than returns from. +;;;; +;;;; This is here for the root count and not for the arithmetic. A defer appears +;;;; twice in the typed IR — spliced into the body for the normal path, and +;;;; again in fdefers for the path a handled condition unwinds along — so the +;;;; emitter produces two copies of every dyn temporary inside one. Counting +;;;; only the body left the second copy's temporaries in slots the collector had +;;;; never been told about: the pushes and the pops still balanced, so nothing +;;;; failed, and the values were simply invisible. +;;;; +;;;; Nothing the stub does can show that, because it never collects. What shows +;;;; it is the emitted IR — a fallback slot is spelled %dx and a rooted one %dr, +;;;; and the fix is the absence of the former. + +(defstruct Boom [n i64]) + +(defn inner [x] dyn + (defer (print (+ x 1000)) (print "\n")) + (restart-case + (error (Boom {.n 1})) + (give [] 0)) + (+ x 1)) + +(defn main [] () + (handler-bind [(Boom [b] (invoke-restart 'give))] + (print (inner 5)) + (print "\n"))) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 69f2e82..b697342 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -3035,6 +3035,43 @@ level "1" dyn_boundary (); dyn_boundary ~opt:"-O0" (); + (* The root count, which is the one part of this feature no run can check: + the stub never collects, so a program whose roots are entirely wrong + passes every test above. What can be checked is the IR, and this is the + assertion that found a real hole — a defer appears twice in the typed IR, + spliced into the body for the normal path and again in [fdefers] for the + path a transfer leaves through, so a dyn temporary inside one is emitted + twice. Counting only the body left the second copy unrooted, silently: + the pushes and the pops balanced because [dyn_tmp] falls back to a plain + slot rather than unbalancing them, and four dyn values were invisible. + + [%dr] is a rooted slot and [%dx] is the fallback, so the claim is that + the emitted IR contains none of the latter. It is worth stating as a + property of the whole corpus and not only of this file: any dyn program + that mints one has a temporary the collector cannot see. *) + let no_fallback_slots path = + let l = Load.program ~file:path (Reader.read_file path) in + let ir = Emit.program (Check.program_all l.Load.decls) in + if contains ir "%dx" then begin + incr failures; + Printf.printf + "FAIL %s emits an unrooted dyn temporary (%%dx) — dyn_roots counted \ + fewer than the emission minted\n" + path + end + in + List.iter no_fallback_slots + [ "programs/dyn-basic.flan"; "programs/dyn-vec.flan"; + "programs/dyn-global.flan"; "programs/dyn-boundary.flan"; + "programs/dyn-defer.flan" ]; + (* And that the defer program still runs and still runs its defer: the + count being right is not much use if the transfer path broke getting + there. 1005 is the defer, 6 is the value the restart produced. *) + outputs "dyn: a defer on the transfer path" "programs/dyn-defer.flan" + "1005\n6\n"; + outputs ~opt:"-O0" "dyn: a defer on the transfer path, -O0" + "programs/dyn-defer.flan" "1005\n6\n"; + (* ── --no-gc ───────────────────────────────────────────────────── The flag is a pass between checking and emission that answers unit or refuses, and these are its two halves.