From 517758684892ac9a7fd9cc51da2216530edbf22c Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 06:22:25 +0700 Subject: [PATCH 01/16] A number or character literal bound by let or loop takes its type from its uses in the function --- TODO.org | 3 + lib/check.ml | 385 ++++++++++++++++++++++++++++-- test/programs/literal-locals.flan | 59 +++++ test/test_acceptance.ml | 8 + test/test_flan.ml | 15 ++ 5 files changed, 450 insertions(+), 20 deletions(-) create mode 100644 test/programs/literal-locals.flan diff --git a/TODO.org b/TODO.org index 1cf331ff..dcb65469 100644 --- a/TODO.org +++ b/TODO.org @@ -33,6 +33,9 @@ disagree are refused with a request for an annotation. Vector, map and text lite are dyn unless something typed wants them. A typed value is boxed where it goes into dyn, and a dyn unboxed (checked) where typed code needs it; typed beside dyn in an operator gives dyn. Dyn integers stay i64 and dyn floats f64. +Local inference is in (check.ml [lit_session]). The f32 default and dyn text and vector +literals wait on the author's answers to the phase-1 measurements; =FLAN_LIT=f32,dyn= in +check.ml is the measuring switch, to be deleted with them. ** DONE Dynamic-first, and the dyn half of the language CLOSED: [2026-09-20] An unannotated parameter or return is =dyn=: a NaN-boxed value over a mark-sweep diff --git a/lib/check.ml b/lib/check.ml index db1fd732..077a371c 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -70,6 +70,10 @@ type binding = { that the field is already in hand. [None] everywhere else, and a refusal with [None] says exactly what it said before. *) bwhat : string option; + (* The literal a [let] or [loop] bound this name to, when its type is read + off the uses ([lit_session]). The initialiser's node, by identity, is the + key: two expansions of one macro are two nodes. *) + blit : Ast.expr option; } (* A class slot's type: what a value stored into it is checked against. A @@ -725,11 +729,53 @@ type lentry = | Lrecur of (int * Types.t) list | Lbarrier of string +(* Local inference for a number or character literal bound by a [let] or a + [loop]: [(let [t 0.0] ... (set t (+ t x)))] makes [t] x's type. The uses + are read by checking the form once with the literal locals at their current + guess and every hook below recording instead of refusing, then undoing that + check; the guesses are solved, and the form is checked for real. A guess + that moved is checked again, at most [lit_rounds] times, so a local fed by + another settles. One session per function context, opened by its outermost + such [let], so a lambda or a generic's copy is inferred on its own and a + literal's type never depends on another function. + + What a use says about the local: + - [Up t]: the local flows into a [t] — a parameter, a return, a field, an + index. The local has to widen into [t]. + - [Down t]: a [t] is [set] into it, or passed to [recur] for it. [t] has to + widen into the local. + - [Hint t]: an operator's other operand, which meets it at either. + A [set] of one such local into another links the two, and a group of + linked locals takes one type. *) +type lit_con = Up | Down | Hint + +type lit_session = { + (* The type each literal local is checked at, by its initialiser's node. *) + mutable decided : (Ast.expr * Types.t) list; + (* On during the recording check and off for the real one. *) + mutable recording : bool; + mutable cons : (Ast.expr * (lit_con * Types.t * Loc.t)) list; + mutable links : (Ast.expr * Ast.expr) list; + (* Each literal local the recording check bound, with its name. *) + mutable seen : (Ast.expr * string) list; +} + +(* Nonzero while any recording check runs: the refusal memos ([arm_failed], + [if_failed], [truthy_failed]) are not written then, since a refusal made + at a guessed type must not be replayed at the decided one. *) +let lit_recording = ref 0 + +(* Set around the one check of an operator's operand that is a literal local, + so its use is recorded as a [Hint] and not an [Up]. *) +let lit_hint = ref false + (* Per-function state. Slots are never reused, so [slots] is also the frame size — the interpreter allocates one array of this length per call. *) type ctx = { env : env; ret : Types.t; + (* The literal-inference session of this function, while one is open. *) + mutable lits : lit_session option; mutable slots : int; (* The type of each slot, newest first. A backend needs it to size the frame — nothing else records it, since the IR refers to slots by index. *) @@ -910,7 +956,7 @@ let render_ctx ctx (emit : Render.emitter) : Render.ctx = variables properly means emitting a [!DILexicalBlock] per [Let] and moving the [llvm.dbg.declare]s out of the entry block to the binding sites, which needs block structure this IR does not carry. *) -let bind ctx ?what name bty ~assignable = +let bind ctx ?what ?lit name bty ~assignable = let taken n = List.exists (fun s -> s = Some n) ctx.slot_names in let name' = if not (taken name) then name @@ -924,7 +970,7 @@ let bind ctx ?what name bty ~assignable = let slot = fresh_slot ~name:name' ctx bty in (* [ctx.scope] keeps the *source* name: the suffix is a debug-info artifact and resolving [v] must still find the innermost binding. *) - ctx.scope <- (name, { slot; bty; assignable; bwhat = what }) :: ctx.scope; + ctx.scope <- (name, { slot; bty; assignable; bwhat = what; blit = lit }) :: ctx.scope; slot let lookup ctx name = List.assoc_opt name ctx.scope @@ -956,7 +1002,7 @@ let rec capture ctx loc name = a [let] inside the body restores what it displaced, and the copy's binding goes with it. One field, not two: the environment is keyed by the source name. *) - | Some (outer, slot) -> Some { slot; bty = outer.bty; assignable = false; bwhat = None } + | Some (outer, slot) -> Some { slot; bty = outer.bty; assignable = false; bwhat = None; blit = None } | None -> let from_parent () = (* Not a local of the body directly around this one, so ask whether that @@ -977,7 +1023,7 @@ let rec capture ctx loc name = | Some _, Some (outer : binding) -> let slot = bind ctx name outer.bty ~assignable:false in ctx.caught <- ctx.caught @ [ (name, (outer, slot)) ]; - Some { slot; bty = outer.bty; assignable = false; bwhat = None } + Some { slot; bty = outer.bty; assignable = false; bwhat = None; blit = None } | _ -> None (* The one thing [capture] does not answer for. A captured name is a copy, so @@ -997,7 +1043,7 @@ and peek_outer ctx name = if ctx.outer_what = None then None else match List.assoc_opt name ctx.caught with - | Some ((b : binding), slot) -> Some { slot; bty = b.bty; assignable = false; bwhat = None } + | Some ((b : binding), slot) -> Some { slot; bty = b.bty; assignable = false; bwhat = None; blit = None } | None -> match List.assoc_opt name ctx.outer with | Some b -> Some b @@ -3585,6 +3631,114 @@ let restart_sig tys = let dyn_i64 = Types.Int Types.I64 let dyn_f64 = Types.Float Types.F64 +(* PROTOTYPE switch for measuring the literal rules; removed before merge. *) +let lit_mode = try Sys.getenv "FLAN_LIT" with Not_found -> "" +let lit_has m = List.mem m (String.split_on_char ',' lit_mode) +let float_default () = if lit_has "f32" then Types.F32 else Types.F64 + +(* ── Literal locals ([lit_session]) ────────────────────────────────── *) + +(* The literal a [let] or [loop] initialiser is, when its type is to be read + off the uses: a number or a character, negated or not. A bool has one type + and a wide literal one (u64), so neither has anything to infer. *) +let lit_kind (e : Ast.expr) = + match e.Ast.e with + | Ast.Int _ -> Some `Int + | Ast.Byte _ -> Some `Char + | Ast.Float _ -> Some `Float + | Ast.Call ({ Ast.e = Ast.Var "-"; _ }, [ { Ast.e = Ast.Int _; _ } ]) -> Some `Int + | Ast.Call ({ Ast.e = Ast.Var "-"; _ }, [ { Ast.e = Ast.Float _; _ } ]) -> Some `Float + | _ -> None + +(* What the literal is with no use to say otherwise. *) +let lit_default = function + | `Int -> Types.Int Types.I32 + | `Char -> Types.Int Types.U8 + | `Float -> Types.Float (float_default ()) + +(* The types a use can give it: any number for an integer or a character, + since an untyped integer constant is usable where a float is wanted, and + only a float for a float. A type variable is admitted and left to + [int_literal] to judge against its bound. Anything else — dyn, a struct — + says nothing about the literal's type; the local keeps its guess and the + use is checked as it always was. *) +let lit_admits kind (t : Types.t) = + match kind, t with + | (`Int | `Char), (Types.Int _ | Types.Float _ | Types.Var _) -> true + | `Float, (Types.Float _ | Types.Var _) -> true + | _ -> false + +let lit_rounds = 3 + +(* Every literal local the recording check bound, with the type the uses + decide for it, and the first pair of uses that no one type satisfies. *) +let lit_solve (s : lit_session) = + let keys = + List.fold_left + (fun acc (k, n) -> if List.exists (fun (k', _) -> k' == k) acc then acc else (k, n) :: acc) + [] s.seen + in + (* The linked group of [k]: a [set] of one literal local into another. *) + let group k = + let rec go seen = function + | [] -> seen + | k :: rest when List.memq k seen -> go seen rest + | k :: rest -> + let next = + List.filter_map + (fun (a, b) -> if a == k then Some b else if b == k then Some a else None) + s.links + in + go (k :: seen) (next @ rest) + in + go [] [ k ] + in + let widens a b = Types.equal a b || Types.widens_to ~from:a ~into:b in + List.map + (fun (k, name) -> + let members = List.filter (fun m -> List.exists (fun (k', _) -> k' == m) keys) (group k) in + let kind = + if List.exists (fun m -> lit_kind m = Some `Float) members then `Float + else Option.value (lit_kind k) ~default:`Int + in + let cons = + List.filter_map + (fun (k', c) -> if List.memq k' members then Some c else None) + s.cons + |> List.filter (fun (_, t, _) -> lit_admits kind t) + |> List.rev + in + let pick c = List.filter_map (fun (c', t, l) -> if c' = c then Some (t, l) else None) cons in + let ups = pick Up and downs = pick Down and hints = pick Hint in + let res = + match ups with + | (u0, l0) :: _ -> + (match List.find_opt (fun (u, _) -> List.for_all (fun (u', _) -> widens u u') ups) ups with + | None -> + let (u1, l1) = + List.find (fun (u, _) -> not (widens u u0 || widens u0 u)) ups + in + Error ((u0, l0), (u1, l1)) + | Some (c, lc) -> + (match List.find_opt (fun (d, _) -> not (widens d c)) downs with + | None -> Ok c + | Some (d, ld) -> Error ((c, lc), (d, ld)))) + | [] -> + (match downs @ hints with + | [] -> Ok (lit_default kind) + | (t0, l0) :: rest -> + let rec fold (t, l) = function + | [] -> Ok t + | (t', l') :: rest -> + (match Types.join t t' with + | Some j -> fold ((j, if Types.equal j t then l else l')) rest + | None -> Error ((t, l), (t', l'))) + in + fold (t0, l0) rest) + in + (k, name, res)) + keys + (* Converting to whatever width the other side of the boundary wants, with a [Cast] and not a silent reinterpretation. The name is for the direction it was written for: runtime/flan_dyn.h boxes integers as [i64] and floats as @@ -4763,7 +4917,7 @@ let with_recovery env ~on f = end let invented_ctx env ret = - { env; ret; slots = 0; slot_tys = []; slot_names = []; scope = []; + { env; ret; lits = None; slots = 0; slot_tys = []; slot_names = []; scope = []; defers = []; defer_slot = None; outer = []; outer_what = None; caught = []; place_ok = false; envslot = None; parent = None; in_frames = None; loops = []; tail = false; in_defer = false; defer_ok = false; defer_block = "a nested form"; owner = "" } @@ -5711,9 +5865,10 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = | Some other when other <> Types.Never -> Loc.failk literal_at_want loc "expected %s, found the float literal %g" (tyname loc other) x - | _ -> Types.F64 + | _ -> float_default () in mk loc (Types.Float k) (Tast.Float (x, k)) + | Ast.Str s when want = None && lit_has "dyn" -> box loc (mk loc Types.String (Tast.Str s)) | Ast.Str s -> expect ctx loc ~want (mk loc Types.String (Tast.Str s)) | Ast.Kw k -> (* Two keywords in one spelling, told apart by the expectation. Where an @@ -5962,6 +6117,11 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = let v = check ctx ~want:Types.Dyn v in expect ctx loc ~want (rt loc Types.Unit "flan_dyn_slot_set" [ target; k; v; here loc ]) + | Ast.Set ((Ast.Pvar n as p), v) when lit_recorded ctx n <> None -> + let key = Option.get (lit_recorded ctx n) in + let p, pty = check_place ctx loc p in + let v = lit_down ctx key pty v in + expect ctx loc ~want (mk loc Types.Unit (Tast.Set (p, v))) | Ast.Set (p, v) -> let p, pty = check_place ctx loc p in let v = check ctx ~want:pty v in @@ -5984,6 +6144,8 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = fixed-array literal they always were. *) | Ast.Arr items when want = Some Types.Dyn -> dyn_vec ctx loc (map_lr (fun x -> check ctx ~want:Types.Dyn x) items) + | Ast.Arr items when want = None && lit_has "dyn" -> + dyn_vec ctx loc (map_lr (fun x -> check ctx ~want:Types.Dyn x) items) | Ast.Arr items -> check_arr ctx ~want loc items (* (array 4 rl/Vector2). Parse already assembled the whole array type, so there is nothing to infer: resolve it and hand back its all-bytes-zero @@ -6338,6 +6500,16 @@ and var ctx ?(qualified = false) loc ~want name = defn to pass that" builtin_prefix name name builtin_prefix name | _ -> match lookup ctx name with + (* A literal local while its uses are being recorded: the use is noted + and read at the type it asks for, so the recording check goes on past + a use its guess would have refused. That check is thrown away. *) + | Some ({ blit = Some key; _ } as b) + when (match ctx.lits, want with + | Some s, Some t -> s.recording && lit_admits (Option.value (lit_kind key) ~default:`Int) t + | _ -> false) -> + let s = Option.get ctx.lits and t = Option.get want in + s.cons <- (key, ((if !lit_hint then Hint else Up), t, loc)) :: s.cons; + mk loc t (Tast.Local b.slot) | Some b -> expect ctx loc ~want (mk loc b.bty (Tast.Local b.slot)) (* A local of the enclosing function, in a body that was lifted out of it: @@ -7050,12 +7222,140 @@ and defer_counter_zero slot loc = (* [defer_ok] says whether *this* let has the function's extent. If it does, so does every form in its body, including a nested let — which is why the flag is handed to the body rather than consumed here. *) +(* The literal local [n] names, while its uses are being recorded. *) +and lit_recorded ctx n = + match ctx.lits, lookup ctx n with + | Some s, Some { blit = Some key; _ } when s.recording -> Some key + | _ -> None + +(* [v] stored into the literal local [key] (a [set] or a [recur]), while + recording: checked on its own terms, so its type is what it brings rather + than the guess. Another literal local links the two; a float literal says + only that it is a float. *) +and lit_down ctx key pty (v : Ast.expr) = + let s = Option.get ctx.lits in + let other = + match v.Ast.e with + | Ast.Var m -> (match lookup ctx m with Some { blit = Some k; _ } -> Some k | _ -> None) + | _ -> None + in + match other with + | Some k -> s.links <- (key, k) :: s.links; check ctx v + | None -> + match lit_kind v with + | Some `Float -> + s.cons <- (key, (Hint, Types.Float (float_default ()), v.Ast.loc)) :: s.cons; + check ctx v + (* An integer literal fits wherever its value does; one past i32 says + the local is at least an i64. *) + | Some _ -> + (match v.Ast.e with + | Ast.Int n when Int64.compare n (Int64.of_int32 Int32.max_int) > 0 + || Int64.compare n (Int64.of_int32 Int32.min_int) < 0 -> + s.cons <- (key, (Hint, Types.Int Types.I64, v.Ast.loc)) :: s.cons; + check ctx ~want:(Types.Int Types.I64) v + | _ -> check ctx ~want:pty v) + | None -> + (match trial ctx (fun () -> check ctx v) with + | Ok e -> + s.cons <- (key, (Down, e.Tast.ty, v.Ast.loc)) :: s.cons; + e + | Error _ -> check ctx ~want:pty v) + +(* The type a literal initialiser is checked at while a session is open — + its current guess, or the decision — noting it as seen while recording. + [None] for anything that is not a literal, or with no session open. *) +and lit_local ctx name (e : Ast.expr) = + match ctx.lits, lit_kind e with + | Some s, Some kind -> + if s.recording then s.seen <- (e, name) :: s.seen; + Some (match List.assq_opt e s.decided with Some t -> t | None -> lit_default kind) + | _ -> None + +(* [run] is a [let] or a [loop] with some of [inits] literals, and the + outermost such form of this function: the session opens here. See + [lit_session]. *) +and with_lits : 'a. ctx -> Loc.t -> Ast.expr list -> (unit -> 'a) -> 'a = + fun ctx loc inits run -> + if ctx.lits <> None || not (List.exists (fun e -> lit_kind e <> None) inits) + then run () + else begin + let s = { decided = []; recording = false; cons = []; links = []; seen = [] } in + ctx.lits <- Some s; + Fun.protect ~finally:(fun () -> ctx.lits <- None) @@ fun () -> + let undo = Loc.diag ~kind:"check/lit-undo" loc "undone" in + let rec round n = + s.cons <- []; s.links <- []; s.seen <- []; + s.recording <- true; + incr lit_recording; + Fun.protect + ~finally:(fun () -> decr lit_recording; s.recording <- false) + (fun () -> ignore (trial ctx (fun () -> ignore (run ()); raise (Loc.Error undo)))); + let solved = lit_solve s in + let guess k = + match List.assq_opt k s.decided with + | Some t -> t + | None -> lit_default (Option.value (lit_kind k) ~default:`Int) + in + let decided = + List.map (fun (k, _, r) -> (k, match r with Ok t -> t | Error _ -> guess k)) solved + in + let moved = List.exists (fun (k, t) -> not (Types.equal t (guess k))) decided in + s.decided <- decided; + if moved && n < lit_rounds then round (n + 1) + else + match List.find_opt (fun (_, _, r) -> Result.is_error r) solved with + | Some (k, name, Error ((t1, l1), (t2, l2))) -> lit_conflict k name t1 l1 t2 l2 + | _ -> () + in + round 1; + if lit_has "log" then + List.iter + (fun (k, t) -> + let d = lit_default (Option.value (lit_kind k) ~default:`Int) in + if not (Types.equal t d) then + Printf.eprintf "LITINF %s:%d:%d %s -> %s\n" k.Ast.loc.Loc.file + k.Ast.loc.Loc.line k.Ast.loc.Loc.col (tyname loc d) (tyname loc t)) + s.decided; + run () + end + +(* Two uses of a literal local that no one type satisfies. *) +and lit_conflict (k : Ast.expr) name t1 l1 t2 l2 = + let lit = + match k.Ast.e with + | Ast.Int n -> Int64.to_string n + | Ast.Float x -> Printf.sprintf "%g" x + | Ast.Byte b -> Printf.sprintf "\\%c" (Char.chr b) + | Ast.Call (_, [ { Ast.e = Ast.Int n; _ } ]) -> Int64.to_string (Int64.neg n) + | Ast.Call (_, [ { Ast.e = Ast.Float x; _ } ]) -> Printf.sprintf "%g" (-.x) + | _ -> "..." + in + let lit = if String.length lit > 0 && lit.[0] <> '-' && not (String.contains lit '.') && lit_kind k = Some `Float then lit ^ ".0" else lit in + let fix = + if fln_source k.Ast.loc then Printf.sprintf "let %s: %s = %s" name (tyname l1 t1) lit + else Printf.sprintf "(%s %s)" (tyname l1 t1) lit + in + Loc.failk "check/literal-uses" k.Ast.loc + ~notes:[ Loc.note l1 (Printf.sprintf "%s is used as %s here" name (tyname l1 t1)); + Loc.note l2 (Printf.sprintf "and as %s here" (tyname l2 t2)) ] + "%s is used as %s and as %s, and %s can have only one type. Write the \ + one it should have: %s" + name (tyname l1 t1) (tyname l2 t2) lit fix + and check_let ctx ?(tail = false) ?want ?(defer_ok = false) loc bs body = + with_lits ctx loc + (List.filter_map + (fun (b : Ast.binding) -> if b.Ast.bty = None then Some b.Ast.bval else None) + bs) + @@ fun () -> scoped ctx (fun () -> let bs = map_lr (fun (b : Ast.binding) -> let want = Option.map (resolve ctx.env) b.Ast.bty in + let lit = if b.Ast.bty = None then lit_local ctx b.Ast.bname b.Ast.bval else None in + let want = match lit with Some t -> Some t | None -> want in let v = check ctx ?want b.Ast.bval in (match v.Tast.ty with (* A refused initialiser, already reported: the name is bound to @@ -7066,7 +7366,10 @@ and check_let ctx ?(tail = false) ?want ?(defer_ok = false) loc bs body = b.Ast.bname (tyname loc v.Tast.ty) | _ -> ()); (* Locals are assignable places; parameters are not. *) - let slot = bind ctx b.Ast.bname v.Tast.ty ~assignable:true in + let slot = + bind ctx b.Ast.bname v.Tast.ty ~assignable:true + ?lit:(Option.map (fun _ -> b.Ast.bval) lit) + in (slot, v)) bs in @@ -7281,6 +7584,7 @@ and check_dotimes ctx ~want loc label name (b : Ast.bounds) body = below and every [continue] a [recur] mints count from the same stack [emit] indexes. *) and check_loop ctx ?want loc bs body = + with_lits ctx loc (List.map snd bs) @@ fun () -> scoped ctx (fun () -> (* Each initial value is evaluated once, before the loop, exactly as a [let]'s is and as [dotimes]'s bound is — and bound before the next is @@ -7288,8 +7592,9 @@ and check_loop ctx ?want loc bs body = name. *) let binds = map_lr - (fun (n, v) -> - let v = check ctx v in + (fun (n, v0) -> + let lit = lit_local ctx n v0 in + let v = check ctx ?want:lit v0 in (match v.Tast.ty with (* A refused initialiser, already reported: the name is bound to the poison so that what follows is still checked. *) @@ -7298,7 +7603,8 @@ and check_loop ctx ?want loc bs body = fail v.Tast.loc "%s would be bound to %s, which is not a value" n (tyname loc v.Tast.ty) | _ -> ()); - (bind ctx n v.Tast.ty ~assignable:true, v)) + (bind ctx n v.Tast.ty ~assignable:true + ?lit:(Option.map (fun _ -> v0) lit), v)) bs in let names = List.map (fun (slot, v) -> (slot, v.Tast.ty)) binds in @@ -7371,7 +7677,18 @@ and check_recur ctx ~tail loc args = if want <> got then fail loc "this loop binds %d name%s and this recur passes %d" want (if want = 1 then "" else "s") got; - let vals = List.map2 (fun a (_, ty) -> check ctx ~want:ty a) args names in + let vals = + List.map2 + (fun a (slot, ty) -> + match + List.find_opt (fun (_, (b : binding)) -> b.slot = slot) ctx.scope + with + | Some (_, { blit = Some key; _ }) + when (match ctx.lits with Some s -> s.recording | None -> false) -> + lit_down ctx key ty a + | _ -> check ctx ~want:ty a) + args names + in (* Every name is rebound at once. The new values go into temporaries first, so that (recur y x) swaps rather than writing y over x and then reading it back — the same reason Clojure's recur is simultaneous. *) @@ -7486,7 +7803,7 @@ and check_truthy ctx c = (fun () -> try check_truthy_once ctx c with Loc.Error d as ex -> - truthy_failed := (c, scope, ctx.ret, d) :: !truthy_failed; + if !lit_recording = 0 then truthy_failed := (c, scope, ctx.ret, d) :: !truthy_failed; raise ex) and check_truthy_once ctx c = @@ -7564,7 +7881,7 @@ and check_if ctx ?(tail = false) ?want loc c t e = (fun () -> try check_if_once ctx ~tail ?want loc c t e with Loc.Error d as ex -> - Hashtbl.add if_failed c.Ast.loc (c, (scope, ctx.ret), want, d); + if !lit_recording = 0 then Hashtbl.add if_failed c.Ast.loc (c, (scope, ctx.ret), want, d); raise ex) and check_if_once ctx ~tail ?want loc c t e = @@ -7653,7 +7970,7 @@ and check_if_once ctx ~tail ?want loc c t e = with | Ok v -> Ok v | Error d -> - Hashtbl.add arm_failed e.Ast.loc (e, key, t.Tast.ty, d); + if !lit_recording = 0 then Hashtbl.add arm_failed e.Ast.loc (e, key, t.Tast.ty, d); Error d in let meet v = @@ -7843,7 +8160,7 @@ and generic_ctor ctx ~want loc name given = (* A literal's own type, the one it has with nothing expected of it. *) let literal_type (a : Ast.expr) = match a.Ast.e with - | Ast.Float _ -> Types.Float Types.F64 + | Ast.Float _ -> Types.Float (float_default ()) | Ast.UInt _ -> Types.Int Types.U64 | Ast.Byte _ -> Types.Int Types.U8 | _ -> Types.Int Types.I32 @@ -9241,7 +9558,7 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms = (match trial ctx (at (Some w)) with | Ok b -> Ok b | Error d -> - Hashtbl.add arm_failed head.Ast.loc + if !lit_recording = 0 then Hashtbl.add arm_failed head.Ast.loc (head, (ctx.scope, ctx.ret), w, d); Error d) in @@ -14144,7 +14461,7 @@ and trial ctx f = Only [Loc.Error] is caught. A timeout or a stack overflow is not a refusal to reconsider, and silently continuing past one would turn a resource failure into a wrong answer. *) - let[@warning "+9"] { env = _; ret = _; slots; slot_tys; slot_names; scope; + let[@warning "+9"] { env = _; ret = _; lits = _; slots; slot_tys; slot_names; scope; defers; defer_slot; defer_ok; defer_block; outer = _; outer_what; caught; place_ok; envslot; parent = _; in_frames; loops; tail; in_defer; @@ -14207,7 +14524,7 @@ and trial_at ctx (y : Ast.expr) (w : Types.t) = (match trial ctx (fun () -> check ctx ~want:w y) with | Ok b -> Ok b | Error d -> - Hashtbl.add arm_failed y.Ast.loc (y, (ctx.scope, ctx.ret), w, d); + if !lit_recording = 0 then Hashtbl.add arm_failed y.Ast.loc (y, (ctx.scope, ctx.ret), w, d); Error d) and binary ctx ?(dyn_ok = false) ?(join = true) name loc ~want args = @@ -14227,7 +14544,35 @@ and binary ctx ?(dyn_ok = false) ?(join = true) name loc ~want args = let needs_want (f : Ast.expr) = is_literal f || (match f.Ast.e with Ast.Kw _ -> true | _ -> false) in - if y_decides then begin + (* While a literal local's uses are recorded, the operand beside it + decides and the local is recorded as meeting it ([lit_session]). *) + let lv (f : Ast.expr) = + match f.Ast.e with Ast.Var n -> lit_recorded ctx n <> None | _ -> false + in + let hinted f = + lit_hint := true; + Fun.protect ~finally:(fun () -> lit_hint := false) f + in + let float_lit (f : Ast.expr) = lit_kind f = Some `Float in + if lv x && float_lit y then begin + let a = hinted (fun () -> check ctx ~want:(Types.Float (float_default ())) x) in + a, check ctx ~want:a.Tast.ty y + end + else if lv y && float_lit x then begin + let b = hinted (fun () -> check ctx ~want:(Types.Float (float_default ())) y) in + check ctx ~want:b.Tast.ty x, b + end + else if lv x && not (lv y) && not (needs_want y) then begin + let b = check ctx ?want y in + let a = hinted (fun () -> check ctx ~want:b.Tast.ty x) in + a, b + end + else if lv y && not (lv x) && not (needs_want x) then begin + let a = check ctx ?want x in + let b = hinted (fun () -> check ctx ~want:a.Tast.ty y) in + a, b + end + else if y_decides then begin let b = check ctx ?want y in let a = check ctx ~want:b.Tast.ty x in a, b diff --git a/test/programs/literal-locals.flan b/test/programs/literal-locals.flan new file mode 100644 index 00000000..c9386d34 --- /dev/null +++ b/test/programs/literal-locals.flan @@ -0,0 +1,59 @@ +;;;; A number literal bound by let or loop takes its type from its uses in +;;;; the function. Each line's expected output is beside it. + +;; A set of an i64 sum makes the accumulator an i64. +(defn total [xs [i64]] i64 + (let [t 0] + (dotimes [i (length xs)] + (set t (+ t (at xs i)))) + t)) + +;; The operand beside it: an f64 accumulator from a float literal. +(defn mean [xs [f64]] f64 + (let [s 0.0] + (dotimes [i (length xs)] + (set s (+ s (at xs i)))) + (/ s (f64 (length xs))))) + +;; A counter compared with an i64 bound counts past i32. +(defn count-to [n i64] i64 + (let [i 0] + (while (< i n) + (set i (+ i 1000000000))) + i)) + +;; A set of one literal local into another links them: b holds a value past +;; i32, so a is an i64 too. +(defn linked [] i64 + (let [a 0 b 0] + (set b 3000000000) + (set a b) + a)) + +;; recur rebinds a loop's names the way set does. +(defn sum-to [n i64] i64 + (loop [i 0 acc 0] + (if (< i n) (recur (+ i 1) (+ acc 1000000000)) acc))) + +;; Inside a generic body the literal takes the type variable. +(defn sum-of [xs [$t]] $t {:where (numeric? $t)} + (let [acc 0] + (dotimes [i (length xs)] + (set acc (+ acc (at xs i)))) + acc)) + +(defn main [] i32 + (let [xs (the [3 i64] [3000000000 4 5]) + fs (the [2 f64] [0.5 0.25]) + gs (the [2 u8] [200 50])] + (println (total (slice xs 0 3))) ; 3000000009 + (println (mean (slice fs 0 2))) ; 0.375 + (println (count-to 5000000000)) ; 5000000000 + (println (linked)) ; 3000000000 + (println (sum-to 3)) ; 3000000000 + (println (sum-of (slice xs 0 3))) ; 3000000009 + (println (sum-of (slice fs 0 2)))) ; 0.75 + ;; Nothing says otherwise: an i32 and an f64. + (let [n 7 f 1.5] + (println n f)) ; 7 1.5 + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 6d83f46c..9fc012c8 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -389,6 +389,14 @@ let () = outputs "value semantics" "programs/values.flan" values_out; outputs "machine surface" "programs/machine.flan" machine_out; outputs "unit main exits 0" "programs/unit-main.flan" "ok\n"; + let literal_locals_out = + "3000000009\n0.375\n5000000000\n3000000000\n3000000000\n3000000009\n\ + 0.75\n7 1.5\n" + in + outputs "literal locals take their uses' type" "programs/literal-locals.flan" + literal_locals_out; + outputs ~x86:true "literal locals take their uses' type, --x86" + "programs/literal-locals.flan" literal_locals_out; (* Comparisons over three operands and more. The lines that carry the whole claim are the tag transcripts: [abc -> false] is a chain whose *first* link already decided the answer and whose middle operand — diff --git a/test/test_flan.ml b/test/test_flan.ml index 1f3d72cb..70221049 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -1086,6 +1086,21 @@ let () = two [infers] above still hold — and this is the position that had no way to say it. *) infers "array constructor" "(array 4 f32)" "[4 f32]"; + (* A literal bound by a let takes its type from its uses in the function, + and two uses no one type satisfies are refused with the annotation. *) + accepts "a literal local takes the type set into it" + "(defn f [x i64] i64 (let [t 0] (set t (+ t x)) t))"; + accepts "a literal local takes an operand's type" + "(defn f [x f64] f64 (let [s 0.0] (set s (+ s x)) s))"; + accepts "recur rebinds a literal local at the type it brings" + "(defn f [n i64] i64 (loop [i 0 acc 0] (if (< i n) (recur (+ i 1) (+ acc n)) acc)))"; + accepts "a set links two literal locals" + "(defn f [] i64 (let [a 0 b 0] (set b 3000000000) (set a b) a))"; + rejects_check "two uses of a literal local disagree" + ~needle:"x is used as u32 and as i32, and 0 can have only one type. \ + Write the one it should have: (u32 0)" + "(defn u [x u32] u32 x) (defn i [x i32] i32 x) \ + (defn f [] i32 (let [x 0] (u x) (i x)) 0)"; infers "array of a struct" "(array 2 i32)" "[2 i32]"; infers "array of an array" "(array 2 [3 u8])" "[2 [3 u8]]"; (* (array-fill [r c] v): the same type at any rank, with the element type From 0a1f5a3483dffa47f79fd0a704153be85dd8478e Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 06:26:24 +0700 Subject: [PATCH 02/16] The literal-conflict fix spells a whole float with its point, and the measuring switch says what it is for --- lib/check.ml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/check.ml b/lib/check.ml index 077a371c..169a113a 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -3631,7 +3631,10 @@ let restart_sig tys = let dyn_i64 = Types.Int Types.I64 let dyn_f64 = Types.Float Types.F64 -(* PROTOTYPE switch for measuring the literal rules; removed before merge. *) +(* The switch the literal rules of TODO.org's "Dyn unless annotated" were + measured with: [f32] makes an unconstrained float literal f32, [dyn] makes + an unwanted text or vector literal dyn, [log] prints each literal local + inference moved off its default. Deleted once those rules are decided. *) let lit_mode = try Sys.getenv "FLAN_LIT" with Not_found -> "" let lit_has m = List.mem m (String.split_on_char ',' lit_mode) let float_default () = if lit_has "f32" then Types.F32 else Types.F64 @@ -7331,7 +7334,10 @@ and lit_conflict (k : Ast.expr) name t1 l1 t2 l2 = | Ast.Call (_, [ { Ast.e = Ast.Float x; _ } ]) -> Printf.sprintf "%g" (-.x) | _ -> "..." in - let lit = if String.length lit > 0 && lit.[0] <> '-' && not (String.contains lit '.') && lit_kind k = Some `Float then lit ^ ".0" else lit in + (* %g drops the point from a whole float; put it back so the fix reads as + a float literal. 1e+20 already does. *) + let whole = lit <> "" && String.for_all (fun c -> (c >= '0' && c <= '9') || c = '-') lit in + let lit = if whole && lit_kind k = Some `Float then lit ^ ".0" else lit in let fix = if fln_source k.Ast.loc then Printf.sprintf "let %s: %s = %s" name (tyname l1 t1) lit else Printf.sprintf "(%s %s)" (tyname l1 t1) lit From 5f1b9b9e7e66fd3af30654c9824fe6747a0d6775 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 09:49:51 +0700 Subject: [PATCH 03/16] An unconstrained float literal is an f32, and an integer literal meeting a float literal meets it at the float --- TODO.org | 7 +- emacs/test-flan.el | 4 +- lib/check.ml | 126 ++++++++++++++++++----- test/programs/x86-p12-handler-value.flan | 2 +- test/test_acceptance.ml | 6 +- test/test_dev.ml | 4 +- test/test_flan.ml | 27 +++-- test/test_session.ml | 18 ++-- 8 files changed, 140 insertions(+), 54 deletions(-) diff --git a/TODO.org b/TODO.org index b1169f74..3bd9439b 100644 --- a/TODO.org +++ b/TODO.org @@ -33,9 +33,10 @@ disagree are refused with a request for an annotation. Vector, map and text lite are dyn unless something typed wants them. A typed value is boxed where it goes into dyn, and a dyn unboxed (checked) where typed code needs it; typed beside dyn in an operator gives dyn. Dyn integers stay i64 and dyn floats f64. -Local inference is in (check.ml [lit_session]). The f32 default and dyn text and vector -literals wait on the author's answers to the phase-1 measurements; =FLAN_LIT=f32,dyn= in -check.ml is the measuring switch, to be deleted with them. +Done: local inference (check.ml [lit_session]), the f32 float default, an integer and a +float literal meeting at the float. Waiting: text and vector literals dyn by default, on +dyn text to str and dyn vec to slice conversion (a lane after views); =FLAN_LIT=dyn= +measures it, and under it a let-bound one some typed use wants already stays typed. ** DONE Dynamic-first, and the dyn half of the language CLOSED: [2026-09-20] An unannotated parameter or return is =dyn=: a NaN-boxed value over a mark-sweep diff --git a/emacs/test-flan.el b/emacs/test-flan.el index 7712e455..39fd3406 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -1891,9 +1891,9 @@ already rely on it — so nothing here is a stand-in for the real thing." (test-flan--check "a generic's IR is shown copy by copy" (and (string-match-p "\\`; LLVM IR for selection-sort, one copy" text) (string-match-p "^;; selection-sort at \\$t = i32$" text) - (string-match-p "^;; selection-sort at \\$t = f64$" text) + (string-match-p "^;; selection-sort at \\$t = f32$" text) (string-match-p "^define .*flan\\.selection-sort-i32" text) - (string-match-p "^define .*flan\\.selection-sort-f64" text))) + (string-match-p "^define .*flan\\.selection-sort-f32" text))) (test-flan--check "each copy names the file the generic was sent from" (string-match-p (regexp-quote file) text))))) diff --git a/lib/check.ml b/lib/check.ml index 62929a34..6ecc2894 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -769,6 +769,15 @@ let lit_recording = ref 0 so its use is recorded as a [Hint] and not an [Up]. *) let lit_hint = ref false +(* Text and bracket literals keep their typed reading while this is set (the + [dyn] switch only): a [defconst]'s value, and a let-bound one some typed + use wants. *) +let typed_literals = ref false +let with_typed_literals f = + let was = !typed_literals in + typed_literals := true; + Fun.protect ~finally:(fun () -> typed_literals := was) f + (* Per-function state. Slots are never reused, so [slots] is also the frame size — the interpreter allocates one array of this length per call. *) type ctx = { @@ -3631,13 +3640,15 @@ let restart_sig tys = let dyn_i64 = Types.Int Types.I64 let dyn_f64 = Types.Float Types.F64 -(* The switch the literal rules of TODO.org's "Dyn unless annotated" were - measured with: [f32] makes an unconstrained float literal f32, [dyn] makes - an unwanted text or vector literal dyn, [log] prints each literal local - inference moved off its default. Deleted once those rules are decided. *) +(* The switch the unfinished half of TODO.org's "Dyn unless annotated" is + measured with: [dyn] makes an unwanted text or bracket literal dyn (a + let-bound one stays typed when a use wants it, [lit_session]), and [log] + prints each literal local inference moved off its default. Deleted when + that half lands. *) let lit_mode = try Sys.getenv "FLAN_LIT" with Not_found -> "" let lit_has m = List.mem m (String.split_on_char ',' lit_mode) -let float_default () = if lit_has "f32" then Types.F32 else Types.F64 +(* An unconstrained float literal is an f32, as an integer one is an i32. *) +let float_default () = Types.F32 (* ── Literal locals ([lit_session]) ────────────────────────────────── *) @@ -3651,13 +3662,18 @@ let lit_kind (e : Ast.expr) = | Ast.Float _ -> Some `Float | Ast.Call ({ Ast.e = Ast.Var "-"; _ }, [ { Ast.e = Ast.Int _; _ } ]) -> Some `Int | Ast.Call ({ Ast.e = Ast.Var "-"; _ }, [ { Ast.e = Ast.Float _; _ } ]) -> Some `Float + | Ast.Str _ | Ast.Arr (_ :: _) when lit_has "dyn" -> Some `Box | _ -> None (* What the literal is with no use to say otherwise. *) +(* A text or bracket literal ([`Box]) is dyn, or with a typed use its typed + reading; [Types.Unit] stands for "typed" in [decided], since the typed + reading is the literal's own and not one a use names. *) let lit_default = function | `Int -> Types.Int Types.I32 | `Char -> Types.Int Types.U8 | `Float -> Types.Float (float_default ()) + | `Box -> Types.Dyn (* The types a use can give it: any number for an integer or a character, since an untyped integer constant is usable where a float is wanted, and @@ -3669,6 +3685,7 @@ let lit_admits kind (t : Types.t) = match kind, t with | (`Int | `Char), (Types.Int _ | Types.Float _ | Types.Var _) -> true | `Float, (Types.Float _ | Types.Var _) -> true + | `Box, t -> not (Types.equal t Types.Dyn) | _ -> false let lit_rounds = 3 @@ -3701,7 +3718,8 @@ let lit_solve (s : lit_session) = (fun (k, name) -> let members = List.filter (fun m -> List.exists (fun (k', _) -> k' == m) keys) (group k) in let kind = - if List.exists (fun m -> lit_kind m = Some `Float) members then `Float + if lit_kind k = Some `Box then `Box + else if List.exists (fun m -> lit_kind m = Some `Float) members then `Float else Option.value (lit_kind k) ~default:`Int in let cons = @@ -3712,6 +3730,7 @@ let lit_solve (s : lit_session) = |> List.rev in let pick c = List.filter_map (fun (c', t, l) -> if c' = c then Some (t, l) else None) cons in + if kind = `Box then (k, name, Ok (if cons = [] then Types.Dyn else Types.Unit)) else let ups = pick Up and downs = pick Down and hints = pick Hint in let res = match ups with @@ -4861,6 +4880,16 @@ let arm_join (a : Types.t) (b : Types.t) = (match a, b with | Types.Dyn, _ | _, Types.Dyn -> Some Types.Dyn | _ -> None) +(* The type two untyped literals meet at: the wider of their own types, and + an integer beside a float at the float — [(if c 1 2.5)] is an f32, though + i32 does not widen into f32, because the 1 was never an i32 to lose. Only + for literals: a typed i32 beside a float literal still has to be converted. *) +let literal_meet (a : Types.t) (b : Types.t) = + match Types.join a b, a, b with + | Some j, _, _ -> Some j + | None, Types.Int _, Types.Float _ -> Some b + | None, Types.Float _, Types.Int _ -> Some a + | None, _, _ -> None let infer_seen : (Types.t * Loc.t * bool) list ref = ref [] (* What a refused subexpression stands as while recovering. [Zero] of [Never] is a value nothing else builds, so it is recognisable; see [check]. *) @@ -5870,8 +5899,14 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = (tyname loc other) x | _ -> float_default () in + (* Past f32's largest value the literal would be infinity, silently. *) + if k = Types.F32 && Float.is_finite x && Float.abs x > 3.4028234663852886e38 then + Loc.failk literal_at_want loc + "%g does not fit in f32, whose largest value is about 3.4e38. Write \ + %s" + x (if fln_source loc then Printf.sprintf "f64(%g)" x else Printf.sprintf "(f64 %g)" x); mk loc (Types.Float k) (Tast.Float (x, k)) - | Ast.Str s when want = None && lit_has "dyn" -> box loc (mk loc Types.String (Tast.Str s)) + | Ast.Str s when want = None && lit_has "dyn" && not !typed_literals -> box loc (mk loc Types.String (Tast.Str s)) | Ast.Str s -> expect ctx loc ~want (mk loc Types.String (Tast.Str s)) | Ast.Kw k -> (* Two keywords in one spelling, told apart by the expectation. Where an @@ -6171,7 +6206,7 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = fixed-array literal they always were. *) | Ast.Arr items when want = Some Types.Dyn -> dyn_vec ctx loc (map_lr (fun x -> check ctx ~want:Types.Dyn x) items) - | Ast.Arr items when want = None && lit_has "dyn" -> + | Ast.Arr items when want = None && lit_has "dyn" && not !typed_literals -> dyn_vec ctx loc (map_lr (fun x -> check ctx ~want:Types.Dyn x) items) | Ast.Arr items -> check_arr ctx ~want loc items (* (array 4 rl/Vector2). Parse already assembled the whole array type, so @@ -6536,7 +6571,8 @@ and var ctx ?(qualified = false) loc ~want name = | _ -> false) -> let s = Option.get ctx.lits and t = Option.get want in s.cons <- (key, ((if !lit_hint then Hint else Up), t, loc)) :: s.cons; - mk loc t (Tast.Local b.slot) + if lit_kind key = Some `Box then expect ctx loc ~want (mk loc b.bty (Tast.Local b.slot)) + else mk loc t (Tast.Local b.slot) | Some b -> expect ctx loc ~want (mk loc b.bty (Tast.Local b.slot)) (* A local of the enclosing function, in a body that was lifted out of it: @@ -7249,6 +7285,17 @@ and defer_counter_zero slot loc = (* [defer_ok] says whether *this* let has the function's extent. If it does, so does every form in its body, including a nested let — which is why the flag is handed to the body rather than consumed here. *) +(* A text or bracket literal local used where only its typed reading works — + sliced, its address taken, cloned, destructured: a typed use, recorded. *) +and lit_typed_use ctx (e : Ast.expr) = + match e.Ast.e, ctx.lits with + | Ast.Var n, Some s when s.recording -> + (match lookup ctx n with + | Some { blit = Some key; _ } when lit_kind key = Some `Box -> + s.cons <- (key, (Up, Types.Unit, e.Ast.loc)) :: s.cons + | _ -> ()) + | _ -> () + (* The literal local [n] names, while its uses are being recorded. *) and lit_recorded ctx n = match ctx.lits, lookup ctx n with @@ -7299,6 +7346,12 @@ and lit_local ctx name (e : Ast.expr) = Some (match List.assq_opt e s.decided with Some t -> t | None -> lit_default kind) | _ -> None +(* A literal local's initialiser, at the type [lit_local] gave it: [Unit] is + a text or bracket literal's typed reading ([lit_default]). *) +and lit_init ctx t (e : Ast.expr) = + if Types.equal t Types.Unit then with_typed_literals (fun () -> check ctx e) + else check ctx ~want:t e + (* [run] is a [let] or a [loop] with some of [inits] literals, and the outermost such form of this function: the session opens here. See [lit_session]. *) @@ -7385,8 +7438,11 @@ and check_let ctx ?(tail = false) ?want ?(defer_ok = false) loc bs body = (fun (b : Ast.binding) -> let want = Option.map (resolve ctx.env) b.Ast.bty in let lit = if b.Ast.bty = None then lit_local ctx b.Ast.bname b.Ast.bval else None in - let want = match lit with Some t -> Some t | None -> want in - let v = check ctx ?want b.Ast.bval in + let v = + match lit with + | Some t -> lit_init ctx t b.Ast.bval + | None -> check ctx ?want b.Ast.bval + in (match v.Tast.ty with (* A refused initialiser, already reported: the name is bound to the poison so that what follows is still checked. *) @@ -7624,7 +7680,9 @@ and check_loop ctx ?want loc bs body = map_lr (fun (n, v0) -> let lit = lit_local ctx n v0 in - let v = check ctx ?want:lit v0 in + let v = + match lit with Some t -> lit_init ctx t v0 | None -> check ctx v0 + in (match v.Tast.ty with (* A refused initialiser, already reported: the name is bound to the poison so that what follows is still checked. *) @@ -8129,7 +8187,7 @@ and literal_join ctx (a : Ast.expr) (b : Ast.expr) = | _ -> probe ctx x.Ast.loc (fun () -> (check ctx x).Tast.ty) in match own a, own b with - | Some x, Some y -> Types.join x y + | Some x, Some y -> literal_meet x y | _ -> None (* Whether a name would reach a callee if it were called — a global function, a @@ -8239,7 +8297,7 @@ and generic_ctor ctx ~want loc name given = (match List.assoc_opt v !subst with | None -> subst := (v, t) :: !subst; lit_only := v :: !lit_only | Some b -> - (match Types.join b t with + (match literal_meet b t with | Some j -> subst := (v, j) :: List.remove_assoc v !subst | None -> fail a.Ast.loc "%s's .%s is %s here, and this is %s" @@ -8707,12 +8765,13 @@ and arr_elem_type ctx (items : Ast.expr list) : Types.t option = List.filter (fun t -> t <> Types.Never) (List.map snd typed) in let lit_tys = List.filter_map natural lits in - let join_all = function + let join_with meet = function | [] -> None | t :: ts -> List.fold_left - (fun acc t -> Option.bind acc (fun a -> Types.join a t)) (Some t) ts + (fun acc t -> Option.bind acc (fun a -> meet a t)) (Some t) ts in + let join_all = join_with Types.join in let mixed_dyn = List.mem Types.Dyn tys && (List.exists (fun t -> t <> Types.Dyn) tys || lits <> []) @@ -8728,7 +8787,13 @@ and arr_elem_type ctx (items : Ast.expr list) : Types.t option = List.fold_left (fun acc i -> if fits t i then acc - else Option.bind acc (fun a -> Option.bind (natural i) (Types.join a))) + else + Option.bind acc (fun a -> + match Option.bind (natural i) (Types.join a), i.Ast.e with + (* A float literal is any float: beside an i32 it takes the + f64 the i32 widens into, not its own f32. *) + | None, Ast.Float _ -> Types.join a (Types.Float Types.F64) + | j, _ -> j)) (Some t) lits in (match t' with @@ -8737,7 +8802,7 @@ and arr_elem_type ctx (items : Ast.expr list) : Types.t option = in let candidates = if tys <> [] then [ join_all tys ] - else join_all lit_tys :: List.map Option.some lit_tys + else join_with literal_meet lit_tys :: List.map Option.some lit_tys in if mixed_dyn then None else if tys = [] && lits = [] then @@ -9511,7 +9576,7 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms = List.fold_left (fun acc x -> Option.bind acc (fun a -> - Option.bind (literal_join ctx first x) (Types.join a))) + Option.bind (literal_join ctx first x) (literal_meet a))) (literal_join ctx first first) rest in (match j with @@ -11568,6 +11633,7 @@ and named_call ?(qualified = false) ctx ~want loc name args = { Ast.e = Ast.Int i; _ }; { Ast.e = Ast.Int n; _ }; { Ast.e = Ast.Int exact; _ } ] -> let plural k = if Int64.equal k 1L then "" else "s" in + lit_typed_use ctx target; let target = check ctx target in (match target.Tast.ty with | Types.Array (m, elem) -> @@ -12049,6 +12115,7 @@ and named_call ?(qualified = false) ctx ~want loc name args = | "clone" -> (match args with | target :: rest when List.length rest <= 1 -> + lit_typed_use ctx target; (* Checked once, then dispatched on what it turned out to be: checking it inside a guard as well would allocate the target's slots twice and evaluate whatever it was written as twice. *) @@ -12795,6 +12862,7 @@ and named_call ?(qualified = false) ctx ~want loc name args = "slice is (slice a), (slice a lo) or (slice a lo hi) — given %d \ arguments" (List.length args) | target :: bounds -> + lit_typed_use ctx target; let target = check_target ctx target in let ty = target.Tast.ty in match ty with @@ -12998,6 +13066,7 @@ and named_call ?(qualified = false) ctx ~want loc name args = | "addr" -> arity ctx loc name 1 args; let a = List.hd args in + lit_typed_use ctx a; (match place_of_expr a with | None -> fail a.Ast.loc @@ -13506,6 +13575,11 @@ and named_call ?(qualified = false) ctx ~want loc name args = when Int64.compare n (-2147483648L) < 0 || Int64.compare n 2147483647L > 0 -> Some target | Ast.UInt _, (Types.Int _ | Types.Float _) -> Some target + (* A float literal likewise: (f64 0.1) is the f64 nearest 0.1 and not + the f32 one widened, and (u64 1.8e19) converts the f64 it says. *) + | _, Types.Float _ when lit_kind (List.hd args) = Some `Float -> Some target + | _, Types.Int _ when lit_kind (List.hd args) = Some `Float -> + Some (Types.Float Types.F64) | _ -> None in let a = check ctx ?want (List.hd args) in @@ -15269,7 +15343,9 @@ let check_parents env = that still does not check once no progress is left has a real error, so the last round is run without swallowing it. *) let settle_consts env consts = - let infer (_, v) = (check (invented_ctx env Types.Unit) v).Tast.ty in + let infer (_, v) = + with_typed_literals (fun () -> (check (invented_ctx env Types.Unit) v).Tast.ty) + in let pending = ref consts in let rec settle () = let left = @@ -16309,12 +16385,12 @@ and read_return env (fn : Ast.fn) params = | (t0, l0, _) :: _ -> (* Where no join exists the first typed exit's type is the one checked against, so the refusal is the one [if] gives its else arm. *) - let meet = function + let meet ?(join = arm_join) = function | [] -> None | ((t, l, _) :: _) as xs -> let j = List.fold_left - (fun acc (u, _, _) -> Option.bind acc (fun a -> arm_join a u)) + (fun acc (u, _, _) -> Option.bind acc (fun a -> join a u)) (Some t) xs in let at = @@ -16330,7 +16406,9 @@ and read_return env (fn : Ast.fn) params = let decided = match meet (List.filter (fun (_, _, lit) -> not lit) arrive) with | Some d -> d - | None -> Option.value (meet arrive) ~default:(t0, l0) + | None -> + (* Every exit a literal: they meet as two literals do. *) + Option.value (meet ~join:literal_meet arrive) ~default:(t0, l0) in ignore (attempt (fst decided)); decided @@ -16905,7 +16983,7 @@ let check_global env (d : Ast.decl) : Tast.global option = ~pattern:(match v.Ast.e with Ast.Int _ -> false | _ -> true) v.Ast.loc kind k, kind); ty; loc = d.Ast.dloc } - | _ -> check (ctx ()) ~want:ty v + | _ -> with_typed_literals (fun () -> check (ctx ()) ~want:ty v) in no_union_const env d.Ast.dloc n ginit; (* After the union's own refusal, so a computed union member keeps the diff --git a/test/programs/x86-p12-handler-value.flan b/test/programs/x86-p12-handler-value.flan index faa843a2..2bd60423 100644 --- a/test/programs/x86-p12-handler-value.flan +++ b/test/programs/x86-p12-handler-value.flan @@ -88,7 +88,7 @@ (handler-bind [(Oops [c] (invoke-restart 'use-zero))] (println (deferred))) ; 9 (println trace) ; 101 — the defer ran - (handler-bind [(Oops [c] (invoke-restart 'use-value 2.5))] + (handler-bind [(Oops [c] (invoke-restart 'use-value (f64 2.5)))] (println (floating))) ; 5 (handler-bind [(Oops [c] (invoke-restart 'use-value "supplied"))] (println (spelled))) ; supplied diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index a0d42819..98661f13 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -3614,7 +3614,7 @@ let () = and each read used to make the call again. *) let generic_struct_out = "60 3 4\nfalse 7.5 3\n(some 3.5) (some 2.5) 1\n2 1 2.5 1.5\n\ - (Pair i32 {.a 2 .b 1}) (Pair f64 {.a 1 .b 2.5})\n6\n\ + (Pair i32 {.a 2 .b 1}) (Pair f32 {.a 1 .b 2.5})\n6\n\ 0 1 2\n3 2\n6\n6 3\n(some 34) none 15\n" in outputs "generic structs" "programs/generic-struct.flan" generic_struct_out; @@ -3791,9 +3791,9 @@ let () = requirement the author wrote down. What is asserted is that it names the type passed and the predicate it failed, and not the body. *) refuses "a generic over maps, instantiated at a key that cannot be hashed" - "programs/generic-map-reject.flan" "f64 is not hashable?"; + "programs/generic-map-reject.flan" "f32 is not hashable?"; refuses "and it names the type the call site asked for" - "programs/generic-map-reject.flan" "at $t = f64"; + "programs/generic-map-reject.flan" "at $t = f32"; (* This one is refused either way, so the guard is about *which* refusal: with sand.flan out of date the file stops at the import and the needle diff --git a/test/test_dev.ml b/test/test_dev.ml index be21fb4e..dc32caec 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -3841,7 +3841,7 @@ let () = "(defn use-sort [] ()\n (let [a [3 1 2]] (selection-sort (slice a 0 3)))\n \ (let [b [3.0 1.0]] (selection-sort (slice b 0 2))))" in - if status r <> "ok" then fail "%s: calling the generic at f64: %s" backend (said r) + if status r <> "ok" then fail "%s: calling the generic at f32: %s" backend (said r) else begin let r = ask () in if status r <> "ok" then @@ -3852,7 +3852,7 @@ let () = let types f = Option.value ~default:"" (Wire.string_field f "types") in - if types a <> "$t = f64" || types b <> "$t = i32" then + if types a <> "$t = f32" || types b <> "$t = i32" then fail "%s: the copies are not headed by their types: %s, %s" backend (types a) (types b); List.iter diff --git a/test/test_flan.ml b/test/test_flan.ml index d8189f64..631c562a 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -1071,7 +1071,7 @@ let def_reading name src gname ~ty = let () = (* ── Literal defaulting and inference ──────────────────────────── *) infers "int defaults to i32" "42" "i32"; - infers "float defaults to f64" "0.5" "f64"; + infers "float defaults to f32" "0.5" "f32"; infers "byte is u8" "\\space" "u8"; infers "string" "\"hi\"" "str"; infers "bool" "true" "bool"; @@ -1096,6 +1096,13 @@ let () = "(defn f [n i64] i64 (loop [i 0 acc 0] (if (< i n) (recur (+ i 1) (+ acc n)) acc)))"; accepts "a set links two literal locals" "(defn f [] i64 (let [a 0 b 0] (set b 3000000000) (set a b) a))"; + rejects_check "a float literal past f32's range is refused with the f64 spelling" + ~needle:"1e+39 does not fit in f32, whose largest value is about 3.4e38. \ + Write (f64 1e+39)" + "(defn f [] f32 1e39)"; + infers "a float literal converted is built at the target" "(f64 0.1)" "f64"; + infers "an f32 range literal beside others makes the array f64" + "[2.5 1e300]" "[2 f64]"; rejects_check "two uses of a literal local disagree" ~needle:"x is used as u32 and as i32, and 0 can have only one type. \ Write the one it should have: (u32 0)" @@ -1108,7 +1115,7 @@ let () = not a zero, which is what lets it be a defonce's initialiser — see programs/array-fill.flan for what it puts in the elements. *) infers "array-fill, rank 1" "(array-fill [5] 7)" "[5 i32]"; - infers "array-fill, rank 2" "(array-fill [2 3] 0.5)" "[2 [3 f64]]"; + infers "array-fill, rank 2" "(array-fill [2 3] 0.5)" "[2 [3 f32]]"; infers "array-fill, rank 3" "(array-fill [2 3 4] true)" "[2 [3 [4 bool]]]"; (* A zero dimension is a legal array with no elements, and the fill loop runs no passes over it. *) @@ -1129,7 +1136,7 @@ let () = (* An untyped integer constant is usable where a float is wanted, as in Odin; the reverse is not. *) - infers "int literal into a float" "(+ 1 0.5)" "f64"; + infers "int literal into a float" "(+ 1 0.5)" "f32"; rejects_check "float literal into an int" "(defn f [] i32 (+ 1 0.5))" ~needle:"expected i32"; @@ -3828,7 +3835,7 @@ let () = rejects_check "an inline generator's body has to answer the element type" "(defonce grid [2 [3 u8]] (array-gen [2 3] (fn [i j] 1.5)))\n\ (defn f [] i32 0)" - ~needle:"expected u8, found f64"; + ~needle:"expected u8, found f32"; rejects_check "an inline generator takes one argument per dimension too" "(defn f [] i32 (let [a (array-gen [2] (fn [i j] i))] 0))" ~needle:"this array-gen has 1 dimension, so its generator is called with \ @@ -6718,7 +6725,7 @@ let () = "(defn bump [x $t] $t {:where (integer? $t)} (+ x 300))"; (* A float at integer?, refused at the call that asked, naming the bound. *) rejects_check "a float does not instantiate an integer?-bounded variable" - ~needle:"f64 is not integer?" + ~needle:"f32 is not integer?" "(defn bump [x $t] $t {:where (integer? $t)} (+ x 1))\n\ (defn main [] () (println (bump 1.5)))"; (* And dyn is refused by the bound too — the clause's own refusal, the more @@ -7676,7 +7683,7 @@ let () = (* ── An array literal with nothing outside it naming a type ────── *) infers "a literal takes the other elements' type" "[(f32 1.0) 2.5]" "[2 f32]"; infers "numbers meet at the wider" "[(u8 1) 256]" "[2 i32]"; - infers "an int and a float literal meet at f64" "[1 2.5]" "[2 f64]"; + infers "an int and a float literal meet at the float" "[1 2.5]" "[2 f32]"; infers "a wide literal makes the array u64" "[1 18446744073709551615]" "[2 u64]"; infers "None takes the other element's Option" "[None (Some 1)]" "[2 (Option i32)]"; infers "a number and a string are a dyn vector" "[10 \"Hi\"]" "dyn"; @@ -7726,10 +7733,10 @@ let () = check "max-value at an unbounded type variable names the bound and only it" (contains d.Loc.dmsg "write {:where (numeric? $t)}" && not (contains d.Loc.dmsg "Fn"))); - infers "two literal if arms meet at the wider" "(if true 1 2.5)" "f64"; + infers "two literal if arms meet at the float" "(if true 1 2.5)" "f32"; infers "two integer if arms stay i32" "(if true 1 2)" "i32"; - infers "two literal match arms meet at the wider" - "(match (Some 1) (Some v) 1 None 2.5)" "f64"; + infers "two literal match arms meet at the float" + "(match (Some 1) (Some v) 1 None 2.5)" "f32"; accepts "max-value at a type variable the bound admits" "(defn f [x $t] $t {:where (integer? $t)} (max-value t))"; rejects_check "max-value at a type that is not a number names the bound" @@ -7846,7 +7853,7 @@ let () = reads_as "an inferred return follows a call to another" ("(defn g [x f64] _ (f x))\n(defn f [x f64] _ (* x 2.0))" ^ main) "g" "g [f64] f64"; reads_as "a literal return takes the other exit's type" - ("(defn f [c bool] _ (when c (return 1)) 2.5)" ^ main) "f" "f [bool] f64"; + ("(defn f [c bool] _ (when c (return 1)) 2.5)" ^ main) "f" "f [bool] f32"; reads_as "a literal return takes a parameter's type" ("(defn f [x i64] _ (when (< x 0) (return 0)) x)" ^ main) "f" "f [i64] i64"; reads_as "a literal return takes an f32" diff --git a/test/test_session.ml b/test/test_session.ml index 328158fd..503dd716 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -212,7 +212,7 @@ let () = "(defn scale [x i64] f64 (f64 (* x 2))) (defn other [] f64 (same 2.5))" with | c -> - if not (List.mem "same-f64" c.Session.fns) then + if not (List.mem "same-f32" c.Session.fns) then fail "a copy a tolerated caller asked for was not generated again: %s" (String.concat " " c.Session.fns); if List.map (fun (x : Session.stale) -> x.Session.caller) c.Session.stale @@ -434,7 +434,7 @@ let () = not: the recovery half of the same claim. *) match Session.eval_expr xt "(println (pick (slice [1.5 0.5] 0 2)))" with | e -> - if not (has e.Session.ir "pick-f64") then + if not (has e.Session.ir "pick-f32") then fail "the expression after a refused one carried no copy" | exception Loc.Error { Loc.dmsg = m; _ } -> fail "the session was poisoned by a bad expression: %s" m); @@ -1509,7 +1509,7 @@ let () = if not (List.mem want c.Session.fns) then fail "redefining a generic did not install %s; it installed %s" want (String.concat " " c.Session.fns)) - [ "hold-i32"; "hold-f64" ]; + [ "hold-i32"; "hold-f32" ]; (* And only its own copies: [put-at] did not change, and its copies are reached through their cells, so reinstalling them would be work with no effect. *) @@ -1530,7 +1530,7 @@ let () = if not (List.mem want c.Session.fns) then fail "redefining a called generic did not install %s; it \ installed %s" want (String.concat " " c.Session.fns)) - [ "put-at-i32"; "put-at-f64" ] + [ "put-at-i32"; "put-at-f32" ] | exception Loc.Error { Loc.dmsg = m; _ } -> fail "redefining a generic: %s" m); @@ -1561,9 +1561,9 @@ let () = fail "a second redefinition of a generic installed nothing"); (* 3. A redefinition that needs a copy the process was never built with. The - fixture never calls [pick] at f64, so [pick-f64] exists in no program + fixture never calls [pick] at f32, so [pick-f32] exists in no program anywhere; redefining the *caller* to ask for it has to build and install - it. Nothing in the form names [pick-f64] — it is found by being an + it. Nothing in the form names [pick-f32] — it is found by being an instantiation the host lacks. *) (match Session.eval (gen ()) @@ -1572,9 +1572,9 @@ let () = (set counter (+ counter (i64 (pick (slice fs 0 3)))))))" with | c -> - if not (List.mem "pick-f64" c.Session.fns) then + if not (List.mem "pick-f32" c.Session.fns) then fail "a redefinition needing a new instantiation did not install \ - pick-f64; it installed %s" (String.concat " " c.Session.fns) + pick-f32; it installed %s" (String.concat " " c.Session.fns) | exception Loc.Error { Loc.dmsg = m; _ } -> fail "a redefinition needing a new instantiation: %s" m); @@ -1620,7 +1620,7 @@ let () = (let t = gen () in match Session.eval_expr t "(println (pick (slice [1.5 0.5] 0 2)))" with | e -> - if not (has e.Session.ir "pick-f64") then + if not (has e.Session.ir "pick-f32") then fail "an expression that instantiated a generic did not carry the copy" | exception Loc.Error { Loc.dmsg = m; _ } -> fail "an expression that instantiates a generic: %s" m); From 23663c47731209df1063cadb13562e76a6f0a488 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 10:45:46 +0700 Subject: [PATCH 04/16] An unconstrained float literal is an f64, literal locals settle by merging the locals that feed each other, and a restart refused for a number type writes the conversion --- TODO.org | 10 +- emacs/test-flan.el | 4 +- lib/check.ml | 611 +++++++++++++++-------- runtime/flan_rt.c | 75 ++- test/programs/literal-locals.flan | 19 + test/programs/restarts.flan | 6 + test/programs/x86-p12-handler-value.flan | 2 +- test/test_acceptance.ml | 10 +- test/test_dev.ml | 4 +- test/test_flan.ml | 34 +- test/test_session.ml | 18 +- 11 files changed, 545 insertions(+), 248 deletions(-) diff --git a/TODO.org b/TODO.org index 1f983138..3aea9494 100644 --- a/TODO.org +++ b/TODO.org @@ -31,13 +31,17 @@ crossing, a dyn big int for u64, and any check in a release build. ** NEXT Dyn unless annotated Decided 2026-09-26, replacing the plain rule: number, bool and char literals are typed, their type inferred from their uses inside the function (never across functions); an -unconstrained integer literal is int (i32) and a float literal float (f32); uses that +unconstrained integer literal is int (i32) and a float literal f64 (decision 121); uses that disagree are refused with a request for an annotation. Vector, map and text literals are dyn unless something typed wants them. A typed value is boxed where it goes into dyn, and a dyn unboxed (checked) where typed code needs it; typed beside dyn in an operator gives dyn. Dyn integers stay i64 and dyn floats f64. -Done: local inference (check.ml [lit_session]), the f32 float default, an integer and a -float literal meeting at the float. Waiting: text and vector literals dyn by default, on +Decision 121: f64 and not f32, because f32 locals lost precision silently — 0.1 summed a +million times printed 100958. A float literal is f32 only where inference finds typed code +wanting f32 (a parameter, field, return or operand). A literal local fed only by dyn takes +the dyn width, i64 or f64. +Done: local inference (check.ml [lit_session]), an integer and a float literal meeting +at the float. Waiting: text and vector literals dyn by default, on dyn text to str and dyn vec to slice conversion (a lane after views); =FLAN_LIT=dyn= measures it, and under it a let-bound one some typed use wants already stays typed. ** DONE Dynamic-first, and the dyn half of the language diff --git a/emacs/test-flan.el b/emacs/test-flan.el index 39fd3406..7712e455 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -1891,9 +1891,9 @@ already rely on it — so nothing here is a stand-in for the real thing." (test-flan--check "a generic's IR is shown copy by copy" (and (string-match-p "\\`; LLVM IR for selection-sort, one copy" text) (string-match-p "^;; selection-sort at \\$t = i32$" text) - (string-match-p "^;; selection-sort at \\$t = f32$" text) + (string-match-p "^;; selection-sort at \\$t = f64$" text) (string-match-p "^define .*flan\\.selection-sort-i32" text) - (string-match-p "^define .*flan\\.selection-sort-f32" text))) + (string-match-p "^define .*flan\\.selection-sort-f64" text))) (test-flan--check "each copy names the file the generic was sent from" (string-match-p (regexp-quote file) text))))) diff --git a/lib/check.ml b/lib/check.ml index 9b5c0747..4f009b3d 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -746,13 +746,14 @@ type lentry = | Lbarrier of string (* Local inference for a number or character literal bound by a [let] or a - [loop]: [(let [t 0.0] ... (set t (+ t x)))] makes [t] x's type. The uses - are read by checking the form once with the literal locals at their current - guess and every hook below recording instead of refusing, then undoing that - check; the guesses are solved, and the form is checked for real. A guess - that moved is checked again, at most [lit_rounds] times, so a local fed by - another settles. One session per function context, opened by its outermost - such [let], so a lambda or a generic's copy is inferred on its own and a + [loop]: [(let [t 0.0] ... (set t (+ t x)))] makes [t] x's type. The form + is checked with every literal local at its current guess while each use + records what it says about the local. When the uses agree with the + guesses that check is the answer; otherwise it is undone, the guesses are + solved and it is checked again. Locals that feed one another are merged + into one group (union-find), so a chain of any length settles in one more + round. One session per function context, opened by its outermost such + [let], so a lambda or a generic's copy is inferred on its own and a literal's type never depends on another function. What a use says about the local: @@ -760,20 +761,35 @@ type lentry = index. The local has to widen into [t]. - [Down t]: a [t] is [set] into it, or passed to [recur] for it. [t] has to widen into the local. - - [Hint t]: an operator's other operand, which meets it at either. - A [set] of one such local into another links the two, and a group of - linked locals takes one type. *) + - [Hint t]: an operator's other operand, which meets it at either; or a + dyn it meets, as the dyn width ([Types.Dyn] here, i64 or f64 in [solve]). + A [set] of arithmetic over such locals and literals into another merges + them, as does an operator between two of them. *) type lit_con = Up | Down | Hint +(* Initialiser nodes by identity: two expansions of one macro are two nodes + and may print alike. *) +module Phys = Hashtbl.Make (struct + type t = Ast.expr + let equal = ( == ) + let hash = Hashtbl.hash +end) + type lit_session = { - (* The type each literal local is checked at, by its initialiser's node. *) - mutable decided : (Ast.expr * Types.t) list; - (* On during the recording check and off for the real one. *) + (* The type each literal local is checked at. Kept across rounds. *) + decided : Types.t Phys.t; + (* On while a round checks; off for a final check after one that failed. *) mutable recording : bool; - mutable cons : (Ast.expr * (lit_con * Types.t * Loc.t)) list; - mutable links : (Ast.expr * Ast.expr) list; - (* Each literal local the recording check bound, with its name. *) - mutable seen : (Ast.expr * string) list; + (* This round's locals, numbered as they are bound, and their names. *) + ids : int Phys.t; + mutable keys : (Ast.expr * string) list; + mutable count : int; + (* Union-find over the numbers, and each one's uses. *) + parent : (int, int) Hashtbl.t; + cons : (int, lit_con * Types.t * Loc.t) Hashtbl.t; + (* A use its guess could not serve was read at the type it asked for, so + this round's check is not a program and is thrown away. *) + mutable dirty : bool; } (* Nonzero while any recording check runs: the refusal memos ([arm_failed], @@ -781,9 +797,15 @@ type lit_session = { at a guessed type must not be replayed at the decided one. *) let lit_recording = ref 0 -(* Set around the one check of an operator's operand that is a literal local, - so its use is recorded as a [Hint] and not an [Up]. *) -let lit_hint = ref false +(* The operands of the operators being checked that are literal locals, by + location: a want reaching one is the other operand's type, a [Hint] and not + an [Up], and a refusal there is the operator's to handle. *) +let lit_operand_locs : Loc.t list ref = ref [] + +(* Set while arithmetic over literal locals is checked at the type of the + local it is stored into ([lit_down]): the locals in it are merged with that + one, so the guess it is checked at says nothing about them. *) +let lit_quiet = ref false (* Text and bracket literals keep their typed reading while this is set (the [dyn] switch only): a [defconst]'s value, and a let-bound one some typed @@ -3663,8 +3685,9 @@ let dyn_f64 = Types.Float Types.F64 that half lands. *) let lit_mode = try Sys.getenv "FLAN_LIT" with Not_found -> "" let lit_has m = List.mem m (String.split_on_char ',' lit_mode) -(* An unconstrained float literal is an f32, as an integer one is an i32. *) -let float_default () = Types.F32 +(* An unconstrained float literal is an f64 (decision 121); it is an f32 only + where typed code wants one. *) +let float_default () = Types.F64 (* ── Literal locals ([lit_session]) ────────────────────────────────── *) @@ -3685,7 +3708,7 @@ let lit_kind (e : Ast.expr) = (* A text or bracket literal ([`Box]) is dyn, or with a typed use its typed reading; [Types.Unit] stands for "typed" in [decided], since the typed reading is the literal's own and not one a use names. *) -let lit_default = function +let lit_default (_ : Ast.expr) = function | `Int -> Types.Int Types.I32 | `Char -> Types.Int Types.U8 | `Float -> Types.Float (float_default ()) @@ -3704,78 +3727,121 @@ let lit_admits kind (t : Types.t) = | `Box, t -> not (Types.equal t Types.Dyn) | _ -> false -let lit_rounds = 3 +(* The rounds a session may take before its last guesses are checked as + they stand. Merging makes two the usual count; the bound only stops a + pathological program from looping. *) +let lit_rounds = 8 -(* Every literal local the recording check bound, with the type the uses - decide for it, and the first pair of uses that no one type satisfies. *) -let lit_solve (s : lit_session) = - let keys = - List.fold_left - (fun acc (k, n) -> if List.exists (fun (k', _) -> k' == k) acc then acc else (k, n) :: acc) - [] s.seen - in - (* The linked group of [k]: a [set] of one literal local into another. *) - let group k = - let rec go seen = function - | [] -> seen - | k :: rest when List.memq k seen -> go seen rest - | k :: rest -> - let next = - List.filter_map - (fun (a, b) -> if a == k then Some b else if b == k then Some a else None) - s.links - in - go (k :: seen) (next @ rest) +let lit_id (s : lit_session) (key : Ast.expr) = Phys.find_opt s.ids key + +let rec lit_root (s : lit_session) i = + match Hashtbl.find_opt s.parent i with + | Some p when p <> i -> + let r = lit_root s p in + Hashtbl.replace s.parent i r; + r + | _ -> i + +let lit_add (s : lit_session) key c = + match lit_id s key with Some i -> Hashtbl.add s.cons i c | None -> () + +let lit_union (s : lit_session) a b = + match lit_id s a, lit_id s b with + | Some i, Some j -> + let ri = lit_root s i and rj = lit_root s j in + if ri <> rj then Hashtbl.replace s.parent (max ri rj) (min ri rj) + | _ -> () + +(* What earlier sessions decided, while an outermost one is open. An outer + session that checks its form again checks every lambda inside it again, + and each of those opens a session of its own; starting that one from its + last answer makes it settle in one round, where starting from the default + made nested lambdas cost a factor per level. Keyed by the node and the + type variables' bindings, since a generic's body is one node checked at + several types. Only ever a first guess: a wrong one costs a round. *) +let lit_depth = ref 0 +let lit_memo : ((string * Types.t) list * Types.t) list Phys.t = Phys.create 64 + +let lit_guess ~subst (s : lit_session) key kind = + match Phys.find_opt s.decided key with + | Some t -> t + | None -> + let same (sb, _) = + List.equal (fun (a, t) (b, u) -> String.equal a b && Types.equal t u) sb subst in - go [] [ k ] - in + match Option.bind (Phys.find_opt lit_memo key) (List.find_opt same) with + | Some (_, t) -> t + | None -> lit_default key kind + +(* Each literal local this round bound, with the type its group's uses decide + or the first pair of uses no one type satisfies. Linear in locals and + uses. *) +let lit_solve (s : lit_session) = + let keys = Array.of_list (List.rev s.keys) in + let n = Array.length keys in + let root = Array.init n (lit_root s) in + let members = Array.make n [] in + for i = n - 1 downto 0 do members.(root.(i)) <- i :: members.(root.(i)) done; let widens a b = Types.equal a b || Types.widens_to ~from:a ~into:b in - List.map - (fun (k, name) -> - let members = List.filter (fun m -> List.exists (fun (k', _) -> k' == m) keys) (group k) in - let kind = - if lit_kind k = Some `Box then `Box - else if List.exists (fun m -> lit_kind m = Some `Float) members then `Float - else Option.value (lit_kind k) ~default:`Int - in - let cons = - List.filter_map - (fun (k', c) -> if List.memq k' members then Some c else None) - s.cons - |> List.filter (fun (_, t, _) -> lit_admits kind t) - |> List.rev - in - let pick c = List.filter_map (fun (c', t, l) -> if c' = c then Some (t, l) else None) cons in - if kind = `Box then (k, name, Ok (if cons = [] then Types.Dyn else Types.Unit)) else - let ups = pick Up and downs = pick Down and hints = pick Hint in - let res = - match ups with - | (u0, l0) :: _ -> - (match List.find_opt (fun (u, _) -> List.for_all (fun (u', _) -> widens u u') ups) ups with - | None -> - let (u1, l1) = - List.find (fun (u, _) -> not (widens u u0 || widens u0 u)) ups - in - Error ((u0, l0), (u1, l1)) - | Some (c, lc) -> - (match List.find_opt (fun (d, _) -> not (widens d c)) downs with - | None -> Ok c - | Some (d, ld) -> Error ((c, lc), (d, ld)))) - | [] -> - (match downs @ hints with - | [] -> Ok (lit_default kind) - | (t0, l0) :: rest -> - let rec fold (t, l) = function - | [] -> Ok t - | (t', l') :: rest -> - (match Types.join t t' with - | Some j -> fold ((j, if Types.equal j t then l else l')) rest - | None -> Error ((t, l), (t', l'))) - in - fold (t0, l0) rest) - in - (k, name, res)) - keys + let result = Array.make n (Ok Types.Unit) in + Array.iteri + (fun r ms -> + if ms <> [] then begin + let kinds = List.map (fun m -> Option.value (lit_kind (fst keys.(m))) ~default:`Int) ms in + let kind = + if List.mem `Box kinds then `Box + else if List.mem `Float kinds then `Float + else if List.mem `Int kinds then `Int + else `Char + in + let dyn_width = + match kind with `Float -> Types.Float Types.F64 | _ -> Types.Int Types.I64 + in + let cons = + List.concat_map (fun m -> List.rev (Hashtbl.find_all s.cons m)) ms + |> List.filter_map (fun (c, t, l) -> + if kind <> `Box && Types.equal t Types.Dyn then Some (Hint, dyn_width, l) + else if lit_admits kind t then Some (c, t, l) + else None) + in + let pick c = List.filter_map (fun (c', t, l) -> if c' = c then Some (t, l) else None) cons in + let res = + if kind = `Box then Ok (if cons = [] then Types.Dyn else Types.Unit) + else + let ups = pick Up and downs = pick Down and hints = pick Hint in + match ups with + | (u0, l0) :: _ -> + (match List.find_opt (fun (u, _) -> List.for_all (fun (u', _) -> widens u u') ups) ups with + | None -> + let (u1, l1) = List.find (fun (u, _) -> not (widens u u0 || widens u0 u)) ups in + Error ((u0, l0), (u1, l1)) + | Some (c, lc) -> + (match List.find_opt (fun (d, _) -> not (widens d c)) downs with + | None -> Ok c + | Some (d, ld) -> Error ((c, lc), (d, ld)))) + | [] -> + (match downs @ hints with + | [] -> + (* No use names a type: the widest of the members' own. *) + Ok (List.fold_left + (fun acc m -> + let t = lit_default (fst keys.(m)) kind in + match Types.join acc t with Some j -> j | None -> acc) + (lit_default (fst keys.(r)) kind) ms) + | (t0, l0) :: rest -> + let rec fold (t, l) = function + | [] -> Ok t + | (t', l') :: rest -> + (match Types.join t t' with + | Some j -> fold ((j, if Types.equal j t then l else l')) rest + | None -> Error ((t, l), (t', l'))) + in + fold (t0, l0) rest) + in + List.iter (fun m -> result.(m) <- res) ms + end) + members; + Array.to_list (Array.mapi (fun i (k, name) -> (k, name, result.(i))) keys) (* Converting to whatever width the other side of the boundary wants, with a [Cast] and not a silent reinterpretation. The name is for the direction it @@ -5915,12 +5981,17 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = (tyname loc other) x | _ -> float_default () in - (* Past f32's largest value the literal would be infinity, silently. *) - if k = Types.F32 && Float.is_finite x && Float.abs x > 3.4028234663852886e38 then - Loc.failk literal_at_want loc - "%g does not fit in f32, whose largest value is about 3.4e38. Write \ - %s" - x (if fln_source loc then Printf.sprintf "f64(%g)" x else Printf.sprintf "(f64 %g)" x); + (* Where f32 is wanted, a literal past its range would be infinity or 0, + silently. *) + (if k = Types.F32 && Float.is_finite x && x <> 0.0 then + let f = Int32.float_of_bits (Int32.bits_of_float x) in + if Float.is_integer f && f = 0.0 then + Loc.failk literal_at_want loc + "%g is too small for f32, which rounds it to 0 — the smallest \ + f32 above 0 is about 1.4e-45" x + else if not (Float.is_finite f) then + Loc.failk literal_at_want loc + "%g does not fit in f32, whose largest value is about 3.4e38" x); mk loc (Types.Float k) (Tast.Float (x, k)) | Ast.Str s when want = None && lit_has "dyn" && not !typed_literals -> box loc (mk loc Types.String (Tast.Str s)) | Ast.Str s -> expect ctx loc ~want (mk loc Types.String (Tast.Str s)) @@ -6331,6 +6402,7 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = if ctx.in_defer then fail loc "invoke-restart is not allowed inside a defer"; + let written = args in let args = map_lr (fun a -> check ctx a) args in List.iter (fun (a : Tast.expr) -> @@ -6342,6 +6414,22 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = | _ -> ()) args; let sg = restart_sig (List.map (fun (a : Tast.expr) -> a.Tast.ty) args) in + (* What the run-time refusal needs to write its fix, after the signature + and a 0x1f: the syntax (i indented, p parenthesised), then each + argument as written, or x. Only the message reads past the 0x1f; the + comparison is on [type_id sg]. *) + let said = + let spell (a : Ast.expr) = + match a.Ast.e with + | Ast.Float x -> + let t = Printf.sprintf "%g" x in + if String.exists (fun c -> c = '.' || c = 'e' || c = 'n' || c = 'i') t + then t else t ^ ".0" + | _ -> spell_arg "x" a + in + String.concat "\x1f" + (sg :: (if fln_source loc then "i" else "p") :: List.map spell written) + in (* Evaluated into slots first, so that an argument which transfers on its own is guarded before this form aims the channel, and so that a call written in an argument is on the ordinary walk rather than hidden @@ -6356,7 +6444,7 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = in let invoke = mk loc Types.Never - (Tast.InvokeRestart (type_id name, name, locals, sg, type_id sg, loc)) + (Tast.InvokeRestart (type_id name, name, locals, said, type_id sg, loc)) in expect ctx loc ~want (if binds = [] then invoke @@ -6578,17 +6666,25 @@ and var ctx ?(qualified = false) loc ~want name = defn to pass that" builtin_prefix name name builtin_prefix name | _ -> match lookup ctx name with - (* A literal local while its uses are being recorded: the use is noted - and read at the type it asks for, so the recording check goes on past - a use its guess would have refused. That check is thrown away. *) + (* A literal local while its uses are being recorded: the use is noted. + One its guess cannot serve is read at the type it asks for, so the + check goes on to the uses after it, and the round is marked to be + thrown away. A dyn want says the dyn width ([lit_solve]). *) | Some ({ blit = Some key; _ } as b) when (match ctx.lits, want with - | Some s, Some t -> s.recording && lit_admits (Option.value (lit_kind key) ~default:`Int) t + | Some s, Some t -> + s.recording && not !lit_quiet + && (Types.equal t Types.Dyn + || lit_admits (Option.value (lit_kind key) ~default:`Int) t) | _ -> false) -> let s = Option.get ctx.lits and t = Option.get want in - s.cons <- (key, ((if !lit_hint then Hint else Up), t, loc)) :: s.cons; - if lit_kind key = Some `Box then expect ctx loc ~want (mk loc b.bty (Tast.Local b.slot)) - else mk loc t (Tast.Local b.slot) + let operand = List.memq loc !lit_operand_locs in + let c = if operand || Types.equal t Types.Dyn then Hint else Up in + lit_add s key (c, t, loc); + (try expect ctx loc ~want (mk loc b.bty (Tast.Local b.slot)) + with Loc.Error _ when lit_kind key <> Some `Box && not operand -> + s.dirty <- true; + mk loc t (Tast.Local b.slot)) | Some b -> expect ctx loc ~want (mk loc b.bty (Tast.Local b.slot)) (* A local of the enclosing function, in a body that was lifted out of it: @@ -7308,7 +7404,7 @@ and lit_typed_use ctx (e : Ast.expr) = | Ast.Var n, Some s when s.recording -> (match lookup ctx n with | Some { blit = Some key; _ } when lit_kind key = Some `Box -> - s.cons <- (key, (Up, Types.Unit, e.Ast.loc)) :: s.cons + lit_add s key (Up, Types.Unit, e.Ast.loc) | _ -> ()) | _ -> () @@ -7318,48 +7414,79 @@ and lit_recorded ctx n = | Some s, Some { blit = Some key; _ } when s.recording -> Some key | _ -> None +(* [v] read as arithmetic: the literal locals among its operands, and the + operands that are something else (a call, an index, a typed name). Number + literals are neither. The result's type is the join of all of them, so the + locals are merged with whatever [v] is stored into and the others are + what it brings. *) +and lit_parts ctx (v : Ast.expr) = + let rec go (v : Ast.expr) (vars, others) = + match v.Ast.e with + | Ast.Var n -> + (match lookup ctx n with + | Some { blit = Some k; _ } when lit_kind k <> Some `Box -> (k :: vars, others) + | _ -> (vars, v :: others)) + | Ast.Int _ | Ast.Float _ | Ast.Byte _ -> (vars, others) + | Ast.Call ({ Ast.e = Ast.Var ("+" | "-" | "*" | "/" | "%" | "min" | "max"); _ }, args) + when args <> [] -> + List.fold_left (fun acc a -> go a acc) (vars, others) args + | _ -> (vars, v :: others) + in + go v ([], []) + +(* A float literal, or an integer one past i32, anywhere in [v]'s arithmetic. *) +and lit_wide_literals (v : Ast.expr) = + let rec go (v : Ast.expr) = + match v.Ast.e with + | Ast.Float _ -> [ (Types.Float (float_default ()), v.Ast.loc) ] + | Ast.Int n when Int64.compare n (Int64.of_int32 Int32.max_int) > 0 + || Int64.compare n (Int64.of_int32 Int32.min_int) < 0 -> + [ (Types.Int Types.I64, v.Ast.loc) ] + | Ast.Call ({ Ast.e = Ast.Var ("+" | "-" | "*" | "/" | "%" | "min" | "max"); _ }, args) -> + List.concat_map go args + | _ -> [] + in + go v + (* [v] stored into the literal local [key] (a [set] or a [recur]), while - recording: checked on its own terms, so its type is what it brings rather - than the guess. Another literal local links the two; a float literal says - only that it is a float. *) + recording: the literal locals it is arithmetic over are merged with [key], + and each other operand says, on its own terms, what it brings ([Down]) — + never the guess the round happens to have. Then [v] is checked at [key]'s + guess, quietly, since a want there is the guess and says nothing. *) and lit_down ctx key pty (v : Ast.expr) = let s = Option.get ctx.lits in - let other = - match v.Ast.e with - | Ast.Var m -> (match lookup ctx m with Some { blit = Some k; _ } -> Some k | _ -> None) - | _ -> None + let quietly f = + let was = !lit_quiet in + lit_quiet := true; + Fun.protect ~finally:(fun () -> lit_quiet := was) f in - match other with - | Some k -> s.links <- (key, k) :: s.links; check ctx v - | None -> - match lit_kind v with - | Some `Float -> - s.cons <- (key, (Hint, Types.Float (float_default ()), v.Ast.loc)) :: s.cons; - check ctx v - (* An integer literal fits wherever its value does; one past i32 says - the local is at least an i64. *) - | Some _ -> - (match v.Ast.e with - | Ast.Int n when Int64.compare n (Int64.of_int32 Int32.max_int) > 0 - || Int64.compare n (Int64.of_int32 Int32.min_int) < 0 -> - s.cons <- (key, (Hint, Types.Int Types.I64, v.Ast.loc)) :: s.cons; - check ctx ~want:(Types.Int Types.I64) v - | _ -> check ctx ~want:pty v) - | None -> - (match trial ctx (fun () -> check ctx v) with - | Ok e -> - s.cons <- (key, (Down, e.Tast.ty, v.Ast.loc)) :: s.cons; - e - | Error _ -> check ctx ~want:pty v) + let vars, others = lit_parts ctx v in + List.iter (lit_union s key) vars; + List.iter (fun (t, l) -> lit_add s key (Hint, t, l)) (lit_wide_literals v); + List.iter + (fun (o : Ast.expr) -> + match trial ctx (fun () -> check ctx o) with + | Ok e -> lit_add s key (Down, e.Tast.ty, o.Ast.loc) + | Error _ -> ()) + others; + match trial ctx (fun () -> quietly (fun () -> check ctx ~want:pty v)) with + | Ok e -> e + | Error _ -> + s.dirty <- true; + quietly (fun () -> check ctx v) (* The type a literal initialiser is checked at while a session is open — - its current guess, or the decision — noting it as seen while recording. + its current guess, or the decision — numbering it while recording. [None] for anything that is not a literal, or with no session open. *) and lit_local ctx name (e : Ast.expr) = match ctx.lits, lit_kind e with | Some s, Some kind -> - if s.recording then s.seen <- (e, name) :: s.seen; - Some (match List.assq_opt e s.decided with Some t -> t | None -> lit_default kind) + if s.recording && not (Phys.mem s.ids e) then begin + Phys.replace s.ids e s.count; + s.count <- s.count + 1; + s.keys <- (e, name) :: s.keys + end; + Some (lit_guess ~subst:ctx.env.subst s e kind) | _ -> None (* A literal local's initialiser, at the type [lit_local] gave it: [Unit] is @@ -7376,44 +7503,100 @@ and with_lits : 'a. ctx -> Loc.t -> Ast.expr list -> (unit -> 'a) -> 'a = if ctx.lits <> None || not (List.exists (fun e -> lit_kind e <> None) inits) then run () else begin - let s = { decided = []; recording = false; cons = []; links = []; seen = [] } in + let s = { decided = Phys.create 16; recording = false; ids = Phys.create 16; + keys = []; count = 0; parent = Hashtbl.create 16; + cons = Hashtbl.create 16; dirty = false } in ctx.lits <- Some s; - Fun.protect ~finally:(fun () -> ctx.lits <- None) @@ fun () -> - let undo = Loc.diag ~kind:"check/lit-undo" loc "undone" in + incr lit_depth; + let remember () = + let subst = ctx.env.subst in + List.iter + (fun (k, _) -> + let kind = Option.value (lit_kind k) ~default:`Int in + let t = lit_guess ~subst s k kind in + let others = + Option.value (Phys.find_opt lit_memo k) ~default:[] + |> List.filter (fun (sb, _) -> sb != subst) + in + Phys.replace lit_memo k ((subst, t) :: others)) + s.keys + in + Fun.protect + ~finally:(fun () -> + ctx.lits <- None; + decr lit_depth; + if !lit_depth = 0 then Phys.reset lit_memo) + @@ fun () -> + let unsettled = Loc.diag ~kind:"check/lit-unsettled" loc "unsettled" in + (* The decisions the uses recorded so far make, and whether any moved. *) + let settle () = + let solved = lit_solve s in + let moved = ref false in + List.iter + (fun (k, _, r) -> + match r with + | Ok t -> + let kind = Option.value (lit_kind k) ~default:`Int in + if not (Types.equal t (lit_guess ~subst:ctx.env.subst s k kind)) then begin + moved := true; + Phys.replace s.decided k t + end + | Error _ -> ()) + solved; + (solved, !moved) + in + let conflict solved = + List.find_map + (fun (k, name, r) -> match r with Error e -> Some (k, name, e) | Ok _ -> None) + solved + in + let log () = + if lit_has "log" then + Phys.iter + (fun k t -> + let d = lit_default k (Option.value (lit_kind k) ~default:`Int) in + if not (Types.equal t d) then + Printf.eprintf "LITINF %s:%d:%d %s -> %s\n" k.Ast.loc.Loc.file + k.Ast.loc.Loc.line k.Ast.loc.Loc.col (tyname loc d) (tyname loc t)) + s.decided + in let rec round n = - s.cons <- []; s.links <- []; s.seen <- []; + Phys.reset s.ids; s.keys <- []; s.count <- 0; + Hashtbl.reset s.parent; Hashtbl.reset s.cons; s.dirty <- false; s.recording <- true; incr lit_recording; - Fun.protect - ~finally:(fun () -> decr lit_recording; s.recording <- false) - (fun () -> ignore (trial ctx (fun () -> ignore (run ()); raise (Loc.Error undo)))); - let solved = lit_solve s in - let guess k = - match List.assq_opt k s.decided with - | Some t -> t - | None -> lit_default (Option.value (lit_kind k) ~default:`Int) + (* A ref, because [trial] is monomorphic inside this recursive group. *) + let answer = ref None in + let outcome = + Fun.protect + ~finally:(fun () -> decr lit_recording; s.recording <- false) + (fun () -> + trial ctx (fun () -> + let r = run () in + let solved, moved = settle () in + (* The guesses held: this check is the answer. *) + if moved || s.dirty || conflict solved <> None then + raise (Loc.Error unsettled); + answer := Some r; + poison loc)) in - let decided = - List.map (fun (k, _, r) -> (k, match r with Ok t -> t | Error _ -> guess k)) solved - in - let moved = List.exists (fun (k, t) -> not (Types.equal t (guess k))) decided in - s.decided <- decided; - if moved && n < lit_rounds then round (n + 1) - else - match List.find_opt (fun (_, _, r) -> Result.is_error r) solved with - | Some (k, name, Error ((t1, l1), (t2, l2))) -> lit_conflict k name t1 l1 t2 l2 - | _ -> () + match outcome, !answer with + | Ok _, Some r -> log (); remember (); r + | _ -> + let solved, moved = settle () in + (match conflict solved with + | Some (k, name, ((t1, l1), (t2, l2))) -> lit_conflict k name t1 l1 t2 l2 + | None -> ()); + if moved && n < lit_rounds then round (n + 1) + else begin + (* Nothing left to learn: checked for real, so a refusal is the + ordinary one and a whole-file check goes on past it. *) + log (); + remember (); + run () + end in - round 1; - if lit_has "log" then - List.iter - (fun (k, t) -> - let d = lit_default (Option.value (lit_kind k) ~default:`Int) in - if not (Types.equal t d) then - Printf.eprintf "LITINF %s:%d:%d %s -> %s\n" k.Ast.loc.Loc.file - k.Ast.loc.Loc.line k.Ast.loc.Loc.col (tyname loc d) (tyname loc t)) - s.decided; - run () + round 1 end (* Two uses of a literal local that no one type satisfies. *) @@ -8804,12 +8987,7 @@ and arr_elem_type ctx (items : Ast.expr list) : Types.t option = (fun acc i -> if fits t i then acc else - Option.bind acc (fun a -> - match Option.bind (natural i) (Types.join a), i.Ast.e with - (* A float literal is any float: beside an i32 it takes the - f64 the i32 widens into, not its own f32. *) - | None, Ast.Float _ -> Types.join a (Types.Float Types.F64) - | j, _ -> j)) + Option.bind acc (fun a -> Option.bind (natural i) (Types.join a))) (Some t) lits in (match t' with @@ -14678,7 +14856,53 @@ and trial_at ctx (y : Ast.expr) (w : Types.t) = and binary ctx ?(dyn_ok = false) ?(join = true) name loc ~want args = match args with - | [ x; y ] -> + | [ x; y ] -> lit_operands ctx x y (fun () -> binary_pair ctx ~dyn_ok ~join loc ~want x y) + | _ -> fail loc "%s takes two arguments" name + +(* An operator's two operands, while literal locals' uses are recorded: one + beside a literal local says the type it meets it at ([Hint]), and two of + them are merged. Checked exactly as ever, so a round whose guesses hold is + the program. *) +and lit_operands ctx (x : Ast.expr) (y : Ast.expr) f = + let key (e : Ast.expr) = + match e.Ast.e with Ast.Var n -> lit_recorded ctx n | _ -> None + in + match ctx.lits, key x, key y with + | Some s, kx, ky when kx <> None || ky <> None -> + let float_lit (e : Ast.expr) = lit_kind e = Some `Float in + (* Before the check, which refuses a float literal beside an integer + guess. *) + (match kx, ky with + | Some k, _ when float_lit y -> lit_add s k (Hint, Types.Float (float_default ()), y.Ast.loc) + | _, Some k when float_lit x -> lit_add s k (Hint, Types.Float (float_default ()), x.Ast.loc) + | _ -> ()); + let saved = !lit_operand_locs in + lit_operand_locs := x.Ast.loc :: y.Ast.loc :: saved; + let a, b = + try Fun.protect ~finally:(fun () -> lit_operand_locs := saved) f + with Loc.Error _ as ex -> + (* Refused at the guess, as (+ acc x) is over an i32 guess and an + i64 x: what the other operand is on its own terms is the use. *) + let own (k, (other : Ast.expr)) = + match trial ctx (fun () -> check ctx other) with + | Ok e -> lit_add s k (Hint, e.Tast.ty, other.Ast.loc) + | Error _ -> () + in + (match kx, ky with + | Some k, None -> own (k, y) + | None, Some k -> own (k, x) + | _ -> ()); + raise ex + in + (match kx, ky with + | Some k1, Some k2 -> lit_union s k1 k2 + | Some k, None -> lit_add s k (Hint, b.Tast.ty, y.Ast.loc) + | None, Some k -> lit_add s k (Hint, a.Tast.ty, x.Ast.loc) + | None, None -> ()); + a, b + | _ -> f () + +and binary_pair ctx ~dyn_ok ~join loc ~want (x : Ast.expr) (y : Ast.expr) = let y_decides = (is_literal x && not (is_literal y)) || (match x.Ast.e, y.Ast.e with @@ -14693,35 +14917,7 @@ and binary ctx ?(dyn_ok = false) ?(join = true) name loc ~want args = let needs_want (f : Ast.expr) = is_literal f || (match f.Ast.e with Ast.Kw _ -> true | _ -> false) in - (* While a literal local's uses are recorded, the operand beside it - decides and the local is recorded as meeting it ([lit_session]). *) - let lv (f : Ast.expr) = - match f.Ast.e with Ast.Var n -> lit_recorded ctx n <> None | _ -> false - in - let hinted f = - lit_hint := true; - Fun.protect ~finally:(fun () -> lit_hint := false) f - in - let float_lit (f : Ast.expr) = lit_kind f = Some `Float in - if lv x && float_lit y then begin - let a = hinted (fun () -> check ctx ~want:(Types.Float (float_default ())) x) in - a, check ctx ~want:a.Tast.ty y - end - else if lv y && float_lit x then begin - let b = hinted (fun () -> check ctx ~want:(Types.Float (float_default ())) y) in - check ctx ~want:b.Tast.ty x, b - end - else if lv x && not (lv y) && not (needs_want y) then begin - let b = check ctx ?want y in - let a = hinted (fun () -> check ctx ~want:b.Tast.ty x) in - a, b - end - else if lv y && not (lv x) && not (needs_want x) then begin - let a = check ctx ?want x in - let b = hinted (fun () -> check ctx ~want:a.Tast.ty y) in - a, b - end - else if y_decides then begin + if y_decides then begin let b = check ctx ?want y in let a = check ctx ~want:b.Tast.ty x in a, b @@ -14817,7 +15013,6 @@ and binary ctx ?(dyn_ok = false) ?(join = true) name loc ~want args = | _ -> raise (Loc.Error d)) | _ -> raise (Loc.Error d) end - | _ -> fail loc "%s takes two arguments" name (* ── The builtins, said out loud ─────────────────────────────────────── A name, a signature and one line, for every name [named_call] and [var] diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index b841fec7..c48f6f39 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -1125,13 +1125,82 @@ _Noreturn void flan_restart_fail(const uint8_t *loc, int64_t loclen, * dynamic stack, so the invoke site cannot see what it will find, and the * frame cannot see who will find it. What each end knows is its own parameter * list, so the message is both of them side by side. */ +/* The top-level items of a signature "(a b c)", where an item may itself be + * bracketed: "(Ptr i32)", "[3 f64]". Up to [max]; answers how many. */ +static int sig_items(const uint8_t *s, int64_t n, const uint8_t **at, + int64_t *len, int max) { + int count = 0, depth = 0; + int64_t start = -1; + for (int64_t i = 1; i + 1 < n; i++) { + uint8_t c = s[i]; + if (c == ' ' && depth == 0) { + if (start >= 0 && count < max) { at[count] = s + start; len[count] = i - start; count++; } + start = -1; + continue; + } + if (start < 0) start = i; + if (c == '(' || c == '[') depth++; + else if (c == ')' || c == ']') depth--; + } + if (start >= 0 && count < max) { at[count] = s + start; len[count] = n - 1 - start; count++; } + return count; +} + +static int is_number_type(const uint8_t *s, int64_t n) { + static const char *names[] = { "i8", "i16", "i32", "i64", "u8", "u16", + "u32", "u64", "f32", "f64" }; + for (size_t k = 0; k < sizeof names / sizeof names[0]; k++) + if ((int64_t)strlen(names[k]) == n && memcmp(names[k], s, (size_t)n) == 0) + return 1; + return 0; +} + +/* [got] is the invoke site's signature, then after each 0x1f: the syntax + * (i or p) and every argument as written. Where the two signatures differ + * only in which number type an argument is, the fix is that argument + * converted: (f64 2.5), or f64(2.5) in the indented syntax. */ _Noreturn void flan_restart_args_fail(const uint8_t *loc, int64_t loclen, const uint8_t *name, int64_t namelen, const uint8_t *want, int64_t wantlen, const uint8_t *got, int64_t gotlen) { - flan_say(loc, loclen, "restart %.*s takes %.*s, given %.*s", (int)namelen, - (const char *)name, (int)wantlen, (const char *)want, (int)gotlen, - (const char *)got); + enum { MAX = 16 }; + const uint8_t *part[MAX + 2]; + int64_t plen[MAX + 2]; + int parts = 0; + int64_t start = 0; + for (int64_t i = 0; i <= gotlen && parts < MAX + 2; i++) + if (i == gotlen || got[i] == 0x1f) { + part[parts] = got + start; plen[parts] = i - start; parts++; + start = i + 1; + } + const uint8_t *w[MAX], *g[MAX]; + int64_t wl[MAX], gl[MAX]; + int nw = sig_items(want, wantlen, w, wl, MAX); + int ng = sig_items(part[0], plen[0], g, gl, MAX); + char fix[512]; + size_t used = 0; + fix[0] = 0; + int ok = parts >= 2 && nw == ng && ng == parts - 2 && ng > 0; + for (int k = 0; ok && k < ng; k++) { + if (wl[k] == gl[k] && memcmp(w[k], g[k], (size_t)wl[k]) == 0) continue; + if (!is_number_type(w[k], wl[k]) || !is_number_type(g[k], gl[k])) { ok = 0; break; } + int indented = plen[1] == 1 && part[1][0] == 'i'; + int wrote = indented + ? snprintf(fix + used, sizeof fix - used, "%s%.*s(%.*s)", used ? ", " : "", + (int)wl[k], (const char *)w[k], (int)plen[k + 2], (const char *)part[k + 2]) + : snprintf(fix + used, sizeof fix - used, "%s(%.*s %.*s)", used ? ", " : "", + (int)wl[k], (const char *)w[k], (int)plen[k + 2], (const char *)part[k + 2]); + if (wrote < 0 || (size_t)wrote >= sizeof fix - used) { ok = 0; break; } + used += (size_t)wrote; + } + if (ok && used > 0) + flan_say(loc, loclen, "restart %.*s takes %.*s, given %.*s. Write %s", + (int)namelen, (const char *)name, (int)wantlen, (const char *)want, + (int)plen[0], (const char *)part[0], fix); + else + flan_say(loc, loclen, "restart %.*s takes %.*s, given %.*s", (int)namelen, + (const char *)name, (int)wantlen, (const char *)want, (int)plen[0], + (const char *)part[0]); rt_trap((const uint8_t *)"RestartArity", 12); } diff --git a/test/programs/literal-locals.flan b/test/programs/literal-locals.flan index c9386d34..69bef99a 100644 --- a/test/programs/literal-locals.flan +++ b/test/programs/literal-locals.flan @@ -42,6 +42,21 @@ (set acc (+ acc (at xs i)))) acc)) +;; A chain of sets settles however long it is. +(defn chained [x i64] i64 + (let [a0 0 a1 0 a2 0 a3 0 a4 0 a5 0] + (set a0 x) (set a1 (+ a0 1)) (set a2 (+ a1 1)) (set a3 (+ a2 1)) + (set a4 (+ a3 1)) (set a5 (+ a4 1)) + a5)) + +;; A dyn number is an i64 or an f64, and so is a literal local it feeds. +(defn boxed [x] dyn x) +(defn from-dyn [] () + (let [d (boxed 0.1) s 0.0 n 0] + (set s (+ s d)) + (set n (+ n (boxed 5000000000))) + (println s n))) + (defn main [] i32 (let [xs (the [3 i64] [3000000000 4 5]) fs (the [2 f64] [0.5 0.25]) @@ -53,6 +68,10 @@ (println (sum-to 3)) ; 3000000000 (println (sum-of (slice xs 0 3))) ; 3000000009 (println (sum-of (slice fs 0 2)))) ; 0.75 + (println (chained 3000000000)) ; 3000000005 + (from-dyn) ; 0.1 5000000000 + (let [x 0.1] + (println (= (boxed x) (boxed 0.1)))) ; true ;; Nothing says otherwise: an i32 and an f64. (let [n 7 f 1.5] (println n f)) ; 7 1.5 diff --git a/test/programs/restarts.flan b/test/programs/restarts.flan index 961c079f..b8adc4a5 100644 --- a/test/programs/restarts.flan +++ b/test/programs/restarts.flan @@ -101,6 +101,11 @@ (handler-bind [(AssetMissing [c] (invoke-restart 'use-value 21))] (shadowed n))) +;;; A number of another type: the refusal writes the conversion. +(defn widened [n i32] i32 + (handler-bind [(AssetMissing [c] (let [big (i64 7)] (invoke-restart 'use-value big)))] + (supplied n))) + (defn main [args [str]] i32 ;; One argument selects a trap; none runs the table's case. (if (> (length args) 1) @@ -110,6 +115,7 @@ (= k 2) (print (mistyped 91)) (= k 3) (print (overfull 92)) (= k 4) (print (mislaid 93)) + (= k 5) (print (widened 94)) :else (println "?")) (return 0))) diff --git a/test/programs/x86-p12-handler-value.flan b/test/programs/x86-p12-handler-value.flan index 2bd60423..faa843a2 100644 --- a/test/programs/x86-p12-handler-value.flan +++ b/test/programs/x86-p12-handler-value.flan @@ -88,7 +88,7 @@ (handler-bind [(Oops [c] (invoke-restart 'use-zero))] (println (deferred))) ; 9 (println trace) ; 101 — the defer ran - (handler-bind [(Oops [c] (invoke-restart 'use-value (f64 2.5)))] + (handler-bind [(Oops [c] (invoke-restart 'use-value 2.5))] (println (floating))) ; 5 (handler-bind [(Oops [c] (invoke-restart 'use-value "supplied"))] (println (spelled))) ; supplied diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 79e3d68b..311499d3 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -391,7 +391,7 @@ let () = outputs "unit main exits 0" "programs/unit-main.flan" "ok\n"; let literal_locals_out = "3000000009\n0.375\n5000000000\n3000000000\n3000000000\n3000000009\n\ - 0.75\n7 1.5\n" + 0.75\n3000000005\n0.1 5000000000\ntrue\n7 1.5\n" in outputs "literal locals take their uses' type" "programs/literal-locals.flan" literal_locals_out; @@ -1400,6 +1400,8 @@ let () = have taken them is not consulted. *) refuses "a shadowing clause of the same name and a different signature" "4" "restart use-value takes (str), given (i32)"; + refuses "a number of another type is refused with its conversion" "5" + "restart use-value takes (i32), given (i64). Write (i32 big)"; (try Sys.remove exe with Sys_error _ -> ()) in restart_mismatch (); @@ -3614,7 +3616,7 @@ let () = and each read used to make the call again. *) let generic_struct_out = "60 3 4\nfalse 7.5 3\n(some 3.5) (some 2.5) 1\n2 1 2.5 1.5\n\ - (Pair i32 {.a 2 .b 1}) (Pair f32 {.a 1 .b 2.5})\n6\n\ + (Pair i32 {.a 2 .b 1}) (Pair f64 {.a 1 .b 2.5})\n6\n\ 0 1 2\n3 2\n6\n6 3\n(some 34) none 15\n" in outputs "generic structs" "programs/generic-struct.flan" generic_struct_out; @@ -3791,9 +3793,9 @@ let () = requirement the author wrote down. What is asserted is that it names the type passed and the predicate it failed, and not the body. *) refuses "a generic over maps, instantiated at a key that cannot be hashed" - "programs/generic-map-reject.flan" "f32 is not hashable?"; + "programs/generic-map-reject.flan" "f64 is not hashable?"; refuses "and it names the type the call site asked for" - "programs/generic-map-reject.flan" "at $t = f32"; + "programs/generic-map-reject.flan" "at $t = f64"; (* This one is refused either way, so the guard is about *which* refusal: with sand.flan out of date the file stops at the import and the needle diff --git a/test/test_dev.ml b/test/test_dev.ml index dc32caec..be21fb4e 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -3841,7 +3841,7 @@ let () = "(defn use-sort [] ()\n (let [a [3 1 2]] (selection-sort (slice a 0 3)))\n \ (let [b [3.0 1.0]] (selection-sort (slice b 0 2))))" in - if status r <> "ok" then fail "%s: calling the generic at f32: %s" backend (said r) + if status r <> "ok" then fail "%s: calling the generic at f64: %s" backend (said r) else begin let r = ask () in if status r <> "ok" then @@ -3852,7 +3852,7 @@ let () = let types f = Option.value ~default:"" (Wire.string_field f "types") in - if types a <> "$t = f32" || types b <> "$t = i32" then + if types a <> "$t = f64" || types b <> "$t = i32" then fail "%s: the copies are not headed by their types: %s, %s" backend (types a) (types b); List.iter diff --git a/test/test_flan.ml b/test/test_flan.ml index cdeb8130..4e7e2beb 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -1071,7 +1071,7 @@ let def_reading name src gname ~ty = let () = (* ── Literal defaulting and inference ──────────────────────────── *) infers "int defaults to i32" "42" "i32"; - infers "float defaults to f32" "0.5" "f32"; + infers "float defaults to f64" "0.5" "f64"; infers "byte is u8" "\\space" "u8"; infers "string" "\"hi\"" "str"; infers "bool" "true" "bool"; @@ -1096,13 +1096,15 @@ let () = "(defn f [n i64] i64 (loop [i 0 acc 0] (if (< i n) (recur (+ i 1) (+ acc n)) acc)))"; accepts "a set links two literal locals" "(defn f [] i64 (let [a 0 b 0] (set b 3000000000) (set a b) a))"; - rejects_check "a float literal past f32's range is refused with the f64 spelling" - ~needle:"1e+39 does not fit in f32, whose largest value is about 3.4e38. \ - Write (f64 1e+39)" + accepts "a float literal local takes the f32 typed code wants" + "(defn f [x f32] f32 (let [s 0.0] (set s (+ s x)) s))"; + rejects_check "a literal past f32's range where f32 is wanted" + ~needle:"1e+39 does not fit in f32, whose largest value is about 3.4e38" "(defn f [] f32 1e39)"; - infers "a float literal converted is built at the target" "(f64 0.1)" "f64"; - infers "an f32 range literal beside others makes the array f64" - "[2.5 1e300]" "[2 f64]"; + rejects_check "a literal f32 rounds to 0 where f32 is wanted" + ~needle:"1e-50 is too small for f32, which rounds it to 0" + "(defn f [] f32 1e-50)"; + infers "a literal past f32's range is an f64 like any other" "(+ 1.0 1e300)" "f64"; rejects_check "two uses of a literal local disagree" ~needle:"x is used as u32 and as i32, and 0 can have only one type. \ Write the one it should have: (u32 0)" @@ -1115,7 +1117,7 @@ let () = not a zero, which is what lets it be a defonce's initialiser — see programs/array-fill.flan for what it puts in the elements. *) infers "array-fill, rank 1" "(array-fill [5] 7)" "[5 i32]"; - infers "array-fill, rank 2" "(array-fill [2 3] 0.5)" "[2 [3 f32]]"; + infers "array-fill, rank 2" "(array-fill [2 3] 0.5)" "[2 [3 f64]]"; infers "array-fill, rank 3" "(array-fill [2 3 4] true)" "[2 [3 [4 bool]]]"; (* A zero dimension is a legal array with no elements, and the fill loop runs no passes over it. *) @@ -1136,7 +1138,7 @@ let () = (* An untyped integer constant is usable where a float is wanted, as in Odin; the reverse is not. *) - infers "int literal into a float" "(+ 1 0.5)" "f32"; + infers "int literal into a float" "(+ 1 0.5)" "f64"; rejects_check "float literal into an int" "(defn f [] i32 (+ 1 0.5))" ~needle:"expected i32"; @@ -3809,7 +3811,7 @@ let () = rejects_check "an inline generator's body has to answer the element type" "(defonce grid [2 [3 u8]] (array-gen [2 3] (fn [i j] 1.5)))\n\ (defn f [] i32 0)" - ~needle:"expected u8, found f32"; + ~needle:"expected u8, found f64"; rejects_check "an inline generator takes one argument per dimension too" "(defn f [] i32 (let [a (array-gen [2] (fn [i j] i))] 0))" ~needle:"this array-gen has 1 dimension, so its generator is called with \ @@ -6699,7 +6701,7 @@ let () = "(defn bump [x $t] $t {:where (integer? $t)} (+ x 300))"; (* A float at integer?, refused at the call that asked, naming the bound. *) rejects_check "a float does not instantiate an integer?-bounded variable" - ~needle:"f32 is not integer?" + ~needle:"f64 is not integer?" "(defn bump [x $t] $t {:where (integer? $t)} (+ x 1))\n\ (defn main [] () (println (bump 1.5)))"; (* And dyn is refused by the bound too — the clause's own refusal, the more @@ -7657,7 +7659,7 @@ let () = (* ── An array literal with nothing outside it naming a type ────── *) infers "a literal takes the other elements' type" "[(f32 1.0) 2.5]" "[2 f32]"; infers "numbers meet at the wider" "[(u8 1) 256]" "[2 i32]"; - infers "an int and a float literal meet at the float" "[1 2.5]" "[2 f32]"; + infers "an int and a float literal meet at f64" "[1 2.5]" "[2 f64]"; infers "a wide literal makes the array u64" "[1 18446744073709551615]" "[2 u64]"; infers "None takes the other element's Option" "[None (Some 1)]" "[2 (Option i32)]"; infers "a number and a string are a dyn vector" "[10 \"Hi\"]" "dyn"; @@ -7711,10 +7713,10 @@ let () = check "max-value at an unbounded type variable names the bound and only it" (contains d.Loc.dmsg "write {:where (numeric? $t)}" && not (contains d.Loc.dmsg "Fn"))); - infers "two literal if arms meet at the float" "(if true 1 2.5)" "f32"; + infers "two literal if arms meet at the wider" "(if true 1 2.5)" "f64"; infers "two integer if arms stay i32" "(if true 1 2)" "i32"; - infers "two literal match arms meet at the float" - "(match (Some 1) (Some v) 1 None 2.5)" "f32"; + infers "two literal match arms meet at the wider" + "(match (Some 1) (Some v) 1 None 2.5)" "f64"; accepts "max-value at a type variable the bound admits" "(defn f [x $t] $t {:where (integer? $t)} (max-value t))"; rejects_check "max-value at a type that is not a number names the bound" @@ -7831,7 +7833,7 @@ let () = reads_as "an inferred return follows a call to another" ("(defn g [x f64] _ (f x))\n(defn f [x f64] _ (* x 2.0))" ^ main) "g" "g [f64] f64"; reads_as "a literal return takes the other exit's type" - ("(defn f [c bool] _ (when c (return 1)) 2.5)" ^ main) "f" "f [bool] f32"; + ("(defn f [c bool] _ (when c (return 1)) 2.5)" ^ main) "f" "f [bool] f64"; reads_as "a literal return takes a parameter's type" ("(defn f [x i64] _ (when (< x 0) (return 0)) x)" ^ main) "f" "f [i64] i64"; reads_as "a literal return takes an f32" diff --git a/test/test_session.ml b/test/test_session.ml index 503dd716..328158fd 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -212,7 +212,7 @@ let () = "(defn scale [x i64] f64 (f64 (* x 2))) (defn other [] f64 (same 2.5))" with | c -> - if not (List.mem "same-f32" c.Session.fns) then + if not (List.mem "same-f64" c.Session.fns) then fail "a copy a tolerated caller asked for was not generated again: %s" (String.concat " " c.Session.fns); if List.map (fun (x : Session.stale) -> x.Session.caller) c.Session.stale @@ -434,7 +434,7 @@ let () = not: the recovery half of the same claim. *) match Session.eval_expr xt "(println (pick (slice [1.5 0.5] 0 2)))" with | e -> - if not (has e.Session.ir "pick-f32") then + if not (has e.Session.ir "pick-f64") then fail "the expression after a refused one carried no copy" | exception Loc.Error { Loc.dmsg = m; _ } -> fail "the session was poisoned by a bad expression: %s" m); @@ -1509,7 +1509,7 @@ let () = if not (List.mem want c.Session.fns) then fail "redefining a generic did not install %s; it installed %s" want (String.concat " " c.Session.fns)) - [ "hold-i32"; "hold-f32" ]; + [ "hold-i32"; "hold-f64" ]; (* And only its own copies: [put-at] did not change, and its copies are reached through their cells, so reinstalling them would be work with no effect. *) @@ -1530,7 +1530,7 @@ let () = if not (List.mem want c.Session.fns) then fail "redefining a called generic did not install %s; it \ installed %s" want (String.concat " " c.Session.fns)) - [ "put-at-i32"; "put-at-f32" ] + [ "put-at-i32"; "put-at-f64" ] | exception Loc.Error { Loc.dmsg = m; _ } -> fail "redefining a generic: %s" m); @@ -1561,9 +1561,9 @@ let () = fail "a second redefinition of a generic installed nothing"); (* 3. A redefinition that needs a copy the process was never built with. The - fixture never calls [pick] at f32, so [pick-f32] exists in no program + fixture never calls [pick] at f64, so [pick-f64] exists in no program anywhere; redefining the *caller* to ask for it has to build and install - it. Nothing in the form names [pick-f32] — it is found by being an + it. Nothing in the form names [pick-f64] — it is found by being an instantiation the host lacks. *) (match Session.eval (gen ()) @@ -1572,9 +1572,9 @@ let () = (set counter (+ counter (i64 (pick (slice fs 0 3)))))))" with | c -> - if not (List.mem "pick-f32" c.Session.fns) then + if not (List.mem "pick-f64" c.Session.fns) then fail "a redefinition needing a new instantiation did not install \ - pick-f32; it installed %s" (String.concat " " c.Session.fns) + pick-f64; it installed %s" (String.concat " " c.Session.fns) | exception Loc.Error { Loc.dmsg = m; _ } -> fail "a redefinition needing a new instantiation: %s" m); @@ -1620,7 +1620,7 @@ let () = (let t = gen () in match Session.eval_expr t "(println (pick (slice [1.5 0.5] 0 2)))" with | e -> - if not (has e.Session.ir "pick-f32") then + if not (has e.Session.ir "pick-f64") then fail "an expression that instantiated a generic did not carry the copy" | exception Loc.Error { Loc.dmsg = m; _ } -> fail "an expression that instantiates a generic: %s" m); From 473bf0a2b72f265977312dce26b7767d07a067dd Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 10:49:54 +0700 Subject: [PATCH 05/16] The bit operators are && || ^^ ~~ in .fln at Python's precedence, take integers only on typed and dyn values, and popcount, leading-zeros, trailing-zeros and the rotations exist on both backends. --- TODO.org | 8 ++ emacs/flan-fln-mode.el | 3 +- emacs/flan-mode.el | 4 +- emacs/test-flan-fln.el | 15 +++ lib/check.ml | 206 +++++++++++++++++++++++++++++------- lib/emit.ml | 59 +++++++++++ lib/indent_printer.ml | 99 +++++++++-------- lib/indent_reader.ml | 81 ++++++++------ lib/js.ml | 6 ++ lib/reader.ml | 3 + lib/tast.ml | 4 + lib/x86.ml | 87 ++++++++++++++- runtime/flan_dyn.c | 112 ++++++++++++++++++++ runtime/flan_dyn.h | 14 +++ spec-syntax.md | 12 ++- test/programs/bits-dyn.flan | 51 +++++++++ test/programs/bits.flan | 46 ++++++++ test/test_acceptance.ml | 75 +++++++++++++ test/test_flan.ml | 42 ++++++-- test/test_syntax.ml | 31 +++++- 20 files changed, 830 insertions(+), 128 deletions(-) create mode 100644 test/programs/bits-dyn.flan create mode 100644 test/programs/bits.flan diff --git a/TODO.org b/TODO.org index 08a02ae2..3803e3b7 100644 --- a/TODO.org +++ b/TODO.org @@ -128,6 +128,14 @@ expansion that defines a macro re-runs the expander, in a build and in a session no =,',x=, since =quote= takes a symbol, and a macro defined by an expansion is not exported from a package. docs/BUILT.md, "Quasiquote runs before the walk". +** DONE Bit operators are && || ^^ ~~ in .fln (decision 123) +CLOSED: [2026-09-26] +Tighter than a comparison, looser than a shift, =&&= then =^^= then =||= +(Python and Rust), so =x && mask == 0= tests the masked bits. Integers only, a +bool refused toward =and=/=or=/=not=; a dyn shift count outside 0..63 traps. +=~~= is one token, so a nested .fln unquote is =~(~x)=; the paren reader keeps +=~~x= as unquote twice and reads =^^= as a name. Rules out C's precedence. + ** DONE A form the prelude relies on is built in; a form only programs use is a macro CLOSED: [2026-09-25] =cond=, =when= and =dotimes= are special forms in parse.ml; =inc=, =++=, =into=, diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el index 96e0c01c..2217f941 100644 --- a/emacs/flan-fln-mode.el +++ b/emacs/flan-fln-mode.el @@ -77,7 +77,8 @@ fine here. Brackets and strings are still paired." ;; that starts with one, or follows a line that ends with one, continues the ;; line above. (defconst flan-fln--binops - '("or" "and" "==" "!=" "<" "<=" ">" ">=" "<<" ">>" "+" "-" "*" "/" "%")) + '("or" "and" "==" "!=" "<" "<=" ">" ">=" "||" "^^" "&&" "<<" ">>" "+" "-" "*" + "/" "%")) (defconst flan-fln--binop-re (regexp-opt flan-fln--binops)) diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index 4e0d3b30..5c49844c 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -154,7 +154,9 @@ face says.") (defconst flan--builtins '(;; arithmetic, comparison, bits "+" "-" "*" "/" "%" "=" "!=" "<" "<=" ">" ">=" "not" - "bit-and" "bit-or" "bit-xor" "<<" ">>" "min" "max" + "bit-and" "bit-or" "bit-xor" "bit-not" "&&" "||" "^^" "<<" ">>" + "rotate-left" "rotate-right" "popcount" "leading-zeros" "trailing-zeros" + "min" "max" ;; the fill patterns "zeroed" "filled" "dead-beef" ;; allocators diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el index 1a69fbd1..5c8185ac 100644 --- a/emacs/test-flan-fln.el +++ b/emacs/test-flan-fln.el @@ -188,6 +188,21 @@ fn step() -> () (test-flan-fln--is "and not the start of the body" (test-flan-fln--thing 'flan-fln-body) "grid[r, c] = 1")) +;; The bit operators continue a line as the other spaced operators do. +;; Not through `test-flan-fln--in', whose `|' marks point and would eat one +;; half of `||'. +(dolist (op '("&&" "||" "^^")) + (with-temp-buffer + (insert "x = a " op "\n b\ny = a\n " op " b\n") + (flan-fln-mode) + (goto-char (point-min)) + (forward-line 1) + (test-flan--check (concat "a line after a trailing " op " continues it") + (flan-fln--continuation-p (point))) + (forward-line 2) + (test-flan--check (concat "a line starting with " op " continues") + (flan-fln--continuation-p (point))))) + (test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "velocity[row, col] = 0.0") (test-flan-fln--is "a top-level form ends before trailing comment lines" (test-flan-fln--thing 'flan-fln-toplevel) diff --git a/lib/check.ml b/lib/check.ml index aa7423a1..4b3590f3 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -677,12 +677,13 @@ let spell_arg stand_for (a : Ast.expr) = (* Operators other languages spell differently, each mapped to the Flan builtin that computes the same thing. Only exact equivalents: [mod] is left - out because Clojure's is floored and [%] is not. *) + out because Clojure's is floored and [%] is not. [&&] and [||] are not + here: they are the bit operators, and a bool reaching one is told which + logical operator it wanted there. *) let operator_aliases = [ ("not=", ("!=", "Not-equal")); ("=/=", ("!=", "Not-equal")); ("/=", ("!=", "Not-equal")); ("<>", ("!=", "Not-equal")); ("==", ("=", "Equality")); ("===", ("=", "Equality")); - ("&&", ("and", "Logical and")); ("||", ("or", "Logical or")); ("!", ("not", "Logical not")) ] (* The fix, as the sentence that ends the refusal. The reader's call is @@ -10086,7 +10087,9 @@ and not_numeric name what (a : Tast.expr) = | _ -> false in let where = a.Tast.loc in - if text then + if a.Tast.ty = Types.Bool && String.equal what "integers" then + bool_bits where name + else if text then fail where "%s takes %s, and this is %s — there is no %s on text. The prelude \ concatenates with concat and join" @@ -10206,14 +10209,16 @@ and dyn_fold ctx ~want loc name first rest = | "+" -> "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) + | _ -> dyn_bits_sym name in + (* A bitwise fold takes integers on both sides, and the typed side of a + mixed pair can be asked now rather than at run time. *) + let bitwise = not (List.mem name [ "+"; "-"; "*"; "/"; "%" ]) in + if bitwise then + List.iter + (fun (v : Tast.expr) -> + if v.Tast.ty <> Types.Dyn then bits_operand ctx v.Tast.loc name v) + first; (* The site travels with the operands. A dyn arithmetic trap is this language's type error, and until now it printed with no file, no line and no column — [here loc] is the same string literal [cast_dyn] hands the @@ -10224,12 +10229,79 @@ and dyn_fold ctx ~want loc name first rest = | [ 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 + let operand arg = + if bitwise then begin + let v = check ctx arg in + if v.Tast.ty <> Types.Dyn then bits_operand ctx v.Tast.loc name v; + v + end + else check ctx ~want:Types.Dyn arg in + let acc = List.fold_left (fun acc arg -> apply acc (operand arg)) acc rest in expect ctx loc ~want acc +(* The runtime's entry point for each bit operation on a dyn int. *) +and dyn_bits_sym name = + match name with + | "bit-and" -> "flan_dyn_bitand" | "bit-or" -> "flan_dyn_bitor" + | "bit-xor" -> "flan_dyn_bitxor" | "bit-not" -> "flan_dyn_bitnot" + | "<<" -> "flan_dyn_shl" | ">>" -> "flan_dyn_shr" + | "rotate-left" -> "flan_dyn_rotl" | "rotate-right" -> "flan_dyn_rotr" + | "popcount" -> "flan_dyn_popcount" | "leading-zeros" -> "flan_dyn_clz" + | "trailing-zeros" -> "flan_dyn_ctz" + | _ -> invalid_arg ("dyn_bits_sym " ^ name) + +(* An operand of a bit operation, once it is known not to be dyn: an integer, + or a type variable the where clause bounds by [integer?]. A bool is the + likeliest thing to arrive here — [a && b] is logical and in C — so it is + answered with the operator that does what was meant. *) +and bits_operand ctx loc name (v : Tast.expr) = + match v.Tast.ty with + | Types.Int _ -> () + | t when generic_ty t -> unconstrained ctx.env loc name ~needs:"integer?" t + | Types.Bool -> bool_bits v.Tast.loc name + | other -> fail loc "%s takes integers, found %s" name (tyname loc other) + +(* A pair with a bool in it is usually refused before [bits_operand] sees it, + as a mismatch between the bool and the other operand. When the pair is + refused, each operand is checked on its own terms, and a bool among them is + the refusal given. The compile is already failing, so the second check + costs nothing that matters. *) +and bool_first : 'a. ctx -> string -> Ast.expr list -> (unit -> 'a) -> 'a = + fun ctx name args k -> + try k () with + | Loc.Error _ as e -> + List.iter + (fun a -> + match check ctx a with + | v when v.Tast.ty = Types.Bool -> bool_bits v.Tast.loc name + | _ -> () + | exception Loc.Error _ -> ()) + (List.filteri (fun i _ -> i < 2) args); + raise e + +and bool_bits loc name = + let fln = fln_source loc in + let shown = + if not fln then name + else match name with + | "bit-and" -> "&&" | "bit-or" -> "||" | "bit-xor" -> "^^" + | "bit-not" -> "~~" | n -> n + in + let logic = + match name with + | "bit-and" -> Some (if fln then "a and b" else "(and a b)") + | "bit-or" -> Some (if fln then "a or b" else "(or a b)") + | "bit-xor" -> Some (if fln then "a != b" else "(!= a b)") + | "bit-not" -> Some (if fln then "not a" else "(not a)") + | _ -> None + in + Loc.failk "check/bits-of-bool" loc + "%s works on the bits of an integer, and this is a bool. %s" shown + (match logic with + | Some l -> Printf.sprintf "For true and false, write %s" l + | None -> "True and false are combined with and, or and not") + (* A comparison over three operands or more asks about more than one pair, and every operand is bound to a slot before any pair is looked at. That is what makes "left to right, exactly once" true of the lowering and not only of @@ -10989,8 +11061,32 @@ and named_call ?(qualified = false) ctx ~want loc name args = | _ -> Tast.BitXor in fold_arity loc name args; - fold_left_prim ctx ~want loc name p ~needs:"integer?" Types.is_integer - "integers" args + bool_first ctx name args (fun () -> + fold_left_prim ctx ~want loc name p ~needs:"integer?" Types.is_integer + "integers" args) + (* The .fln operators, which the indented reader already spells as the words + above; a form built some other way may still carry them. [~qualified] + skips the shadowing arm, because a program that means its own [&&] has + been answered by that arm already under this name. *) + | "&&" | "||" | "^^" | "~~" -> + let canon = match name with + | "&&" -> "bit-and" | "||" -> "bit-or" | "^^" -> "bit-xor" + | _ -> "bit-not" + in + named_call ~qualified:true ctx ~want loc canon args + | "bit-not" | "popcount" | "leading-zeros" | "trailing-zeros" -> + arity ctx loc name 1 args; + let v = check ctx ?want:(numeric_want want) (List.hd args) in + if v.Tast.ty = Types.Dyn then + expect ctx loc ~want (rt loc Types.Dyn (dyn_bits_sym name) [ v; here loc ]) + else begin + bits_operand ctx loc name v; + let p = match name with + | "bit-not" -> Tast.BitNot | "popcount" -> Tast.Popcount + | "leading-zeros" -> Tast.Clz | _ -> Tast.Ctz + in + prim p v.Tast.ty [ v ] + end (* The shifts stay at two, and not only because a shift chain reads badly: each count would be checked against the same width below, so (<< x 30 30) would pass two legal shifts and still shift the value away entirely. @@ -11002,22 +11098,33 @@ and named_call ?(qualified = false) ctx ~want loc name args = type and the width the shift wraps at would be taken from a number that is only saying how far, and the range check just below, along with [emit]'s mask, is keyed to the *value's* width. A count wider than the value is - refused and is told to write the cast. *) - | "<<" | ">>" -> - let p = if String.equal name "<<" then Tast.Shl else Tast.Shr in + refused and is told to write the cast. + + The rotations share the rule and not the range check: a rotation by the + width is the value unchanged, so every count means something and is taken + modulo the width. *) + | "<<" | ">>" | "rotate-left" | "rotate-right" -> + let p = match name with + | "<<" -> Tast.Shl | ">>" -> Tast.Shr | "rotate-left" -> Tast.Rotl + | _ -> Tast.Rotr + in arity ctx loc name 2 args; - let a, b = binary ctx ~join:false name loc ~want:(numeric_want want) args in - (match a.Tast.ty with - | Types.Int _ -> () - (* A type variable under {:where (integer? $t)}: every type the bound - admits has a width to shift within, so the abstract pass lets the - body through and each instantiation meets the concrete checks below - at its own width. Anything weaker — [numeric?] included — is refused - here, at the definition, because a shift at f32 means nothing. *) - | t when generic_ty t -> - unconstrained ctx.env loc name ~needs:"integer?" t - | other -> fail loc "%s takes integers, found %s" name - (tyname loc other)); + let a, b = + bool_first ctx name args (fun () -> + binary ctx ~dyn_ok:true ~join:false name loc + ~want:(numeric_want want) args) + in + if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then begin + (* The typed side of a mixed pair still has to be an integer: the dyn + half is asked at run time, and this half can be asked now. *) + List.iter + (fun (v : Tast.expr) -> + if v.Tast.ty <> Types.Dyn then bits_operand ctx v.Tast.loc name v) + [ a; b ]; + expect ctx loc ~want + (rt loc Types.Dyn (dyn_bits_sym name) [ box loc a; box loc b; here loc ]) + end else begin + bits_operand ctx loc name a; (* A shift by the operand's own width or more is poison in LLVM, which at -O2 turns the whole function into an undefined value rather than into a wrong number. A literal count is rejected here — that is the typo — and @@ -11032,6 +11139,7 @@ and named_call ?(qualified = false) ctx ~want loc name args = (tyname loc a.Tast.ty) (Types.bits k) | _ -> ()); prim p a.Tast.ty [ a; b ] + end (* (min a b) and (max a b) evaluate each operand once — hence the slots — because a min over two calls must not call either of them twice. @@ -14457,19 +14565,41 @@ let builtins : (string * string * string) list = ("not", "not [bool] bool", "Negates a bool. Nothing else in this language is a truth value."); ("bit-and", "bit-and [int ...] int", - "Bitwise and, folded left. Integers only; operands of different widths \ - meet at the wider one, the way + does."); - ("bit-or", "bit-or [int ...] int", "Bitwise or, folded left over integers."); + "Bitwise and, folded left; a && b in a .fln file. Integers only; operands \ + of different widths meet at the wider one, the way + does."); + ("bit-or", "bit-or [int ...] int", + "Bitwise or, folded left over integers; a || b in a .fln file."); ("bit-xor", "bit-xor [int ...] int", - "Bitwise exclusive or, folded left over integers."); + "Bitwise exclusive or, folded left over integers; a ^^ b in a .fln file."); + ("bit-not", "bit-not [int] int", + "Every bit of an integer flipped; ~~a in a .fln file."); + ("&&", "&& [int ...] int", "bit-and, by its .fln spelling."); + ("||", "|| [int ...] int", "bit-or, by its .fln spelling."); + ("^^", "^^ [int ...] int", "bit-xor, by its .fln spelling."); + ("~~", "~~ [int] int", "bit-not, by its .fln spelling."); ("<<", "<< [int int] int", "Left shift. The value's type decides — a narrower count widens to it, a \ wider one is refused — and a literal count at or past the value's width \ - is refused too, because LLVM calls that poison."); + is refused too. On a dyn int, a count outside 0 to 63 traps."); (">>", ">> [int int] int", - "Right shift. The value's type decides and the count widens to it, never \ - the reverse; a literal count at or past the width is refused, as it is \ - for <<."); + "Right shift, arithmetic on a signed type and logical on an unsigned one. \ + The value's type decides and the count widens to it; a literal count at \ + or past the width is refused, as it is for <<."); + ("rotate-left", "rotate-left [int int] int", + "The bits of the value moved left by the count, the ones that fall off \ + the top coming back in at the bottom. The count is taken modulo the \ + width."); + ("rotate-right", "rotate-right [int int] int", + "The bits of the value moved right by the count, wrapping round to the \ + top. The count is taken modulo the width."); + ("popcount", "popcount [int] int", + "How many bits of the integer are set. The answer has the operand's \ + type."); + ("leading-zeros", "leading-zeros [int] int", + "How many zero bits come before the highest set bit, counted within the \ + operand's width: the width itself for 0."); + ("trailing-zeros", "trailing-zeros [int] int", + "How many zero bits come after the lowest set bit: the width for 0."); ("min", "min [ordered? ...] ordered?", "The smallest of two or more operands, each of them evaluated exactly \ once however many there are. Two widths meet at the wider: (min i8-x \ diff --git a/lib/emit.ml b/lib/emit.ml index 97240c6e..5efc9a84 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -1478,6 +1478,7 @@ let settled_prim (p : Tast.prim) = | Tast.Add | Tast.Sub | Tast.Mul | Tast.Eq | Tast.Ne | Tast.Lt | Tast.Le | Tast.Gt | Tast.Ge | Tast.Not | Tast.BitAnd | Tast.BitOr | Tast.BitXor | Tast.Shl | Tast.Shr + | Tast.BitNot | Tast.Popcount | Tast.Clz | Tast.Ctz | Tast.Rotl | Tast.Rotr (* Questions about a value's shape, answered from the layout tables. *) | Tast.Len | Tast.SizeOf _ | Tast.AlignOf _ | Tast.AddrOf -> true (* Everything else reaches C, signals, or both: an index and a slice are @@ -3873,6 +3874,33 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) = let t = fresh f in ins f "%s = xor i1 %s, true" t a; t + | Tast.BitNot, [ x ] -> + let a = value f x in + let t = fresh f in + ins f "%s = xor %s %s, -1" t (ll x.Tast.ty) a; + t + (* [i1 false] says a zero operand is defined — the width — rather than + poison, which is the language's answer for 0. *) + | (Tast.Popcount | Tast.Clz | Tast.Ctz), [ x ] -> + let a = value f x in + let ty = ll x.Tast.ty in + let t = fresh f in + (match p with + | Tast.Popcount -> ins f "%s = call %s @llvm.ctpop.%s(%s %s)" t ty ty ty a + | Tast.Clz -> + ins f "%s = call %s @llvm.ctlz.%s(%s %s, i1 false)" t ty ty ty a + | _ -> ins f "%s = call %s @llvm.cttz.%s(%s %s, i1 false)" t ty ty ty a); + t + (* A funnel shift of a value with itself is a rotation, and the funnel + shifts take their count modulo the width, which is the rotation's rule. *) + | (Tast.Rotl | Tast.Rotr), [ x; y ] -> + let a = value f x in + let b = value f y in + let ty = ll x.Tast.ty in + let t = fresh f in + ins f "%s = call %s @llvm.%s.%s(%s %s, %s %s, %s %s)" t ty + (if p = Tast.Rotl then "fshl" else "fshr") ty ty a ty a ty b; + t | Tast.Len, [ x ] -> (match x.Tast.ty with | Types.Array (n, _) -> Int64.to_string n @@ -4931,6 +4959,26 @@ let header = {|; Generated by flan. The layout is C's: no object headers anywher declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1 immarg) declare i32 @llvm.bswap.i32(i32) +declare i8 @llvm.ctpop.i8(i8) +declare i8 @llvm.ctlz.i8(i8, i1 immarg) +declare i8 @llvm.cttz.i8(i8, i1 immarg) +declare i8 @llvm.fshl.i8(i8, i8, i8) +declare i8 @llvm.fshr.i8(i8, i8, i8) +declare i16 @llvm.ctpop.i16(i16) +declare i16 @llvm.ctlz.i16(i16, i1 immarg) +declare i16 @llvm.cttz.i16(i16, i1 immarg) +declare i16 @llvm.fshl.i16(i16, i16, i16) +declare i16 @llvm.fshr.i16(i16, i16, i16) +declare i32 @llvm.ctpop.i32(i32) +declare i32 @llvm.ctlz.i32(i32, i1 immarg) +declare i32 @llvm.cttz.i32(i32, i1 immarg) +declare i32 @llvm.fshl.i32(i32, i32, i32) +declare i32 @llvm.fshr.i32(i32, i32, i32) +declare i64 @llvm.ctpop.i64(i64) +declare i64 @llvm.ctlz.i64(i64, i1 immarg) +declare i64 @llvm.cttz.i64(i64, i1 immarg) +declare i64 @llvm.fshl.i64(i64, i64, i64) +declare i64 @llvm.fshr.i64(i64, i64, i64) declare ptr @llvm.frameaddress.p0(i32 immarg) declare void @flan_rt_init(i32, ptr) declare void @flan_argv(ptr) @@ -5034,6 +5082,17 @@ declare i64 @flan_dyn_mul(i64, i64, ptr, i64) declare i64 @flan_dyn_div(i64, i64, ptr, i64) declare i64 @flan_dyn_rem(i64, i64, ptr, i64) declare i64 @flan_dyn_neg(i64, ptr, i64) +declare i64 @flan_dyn_bitand(i64, i64, ptr, i64) +declare i64 @flan_dyn_bitor(i64, i64, ptr, i64) +declare i64 @flan_dyn_bitxor(i64, i64, ptr, i64) +declare i64 @flan_dyn_bitnot(i64, ptr, i64) +declare i64 @flan_dyn_shl(i64, i64, ptr, i64) +declare i64 @flan_dyn_shr(i64, i64, ptr, i64) +declare i64 @flan_dyn_rotl(i64, i64, ptr, i64) +declare i64 @flan_dyn_rotr(i64, i64, ptr, i64) +declare i64 @flan_dyn_popcount(i64, ptr, i64) +declare i64 @flan_dyn_clz(i64, ptr, i64) +declare i64 @flan_dyn_ctz(i64, ptr, i64) declare i64 @flan_dyn_lt(i64, i64, ptr, i64) declare i64 @flan_dyn_le(i64, i64, ptr, i64) declare i64 @flan_dyn_gt(i64, i64, ptr, i64) diff --git a/lib/indent_printer.ml b/lib/indent_printer.ml index a86235e1..e3fd42cb 100644 --- a/lib/indent_printer.ml +++ b/lib/indent_printer.ml @@ -296,36 +296,42 @@ let flatten (f : Form.t) (rest : Form.t list) = (* ── Expressions ───────────────────────────────────────────────────── *) -(* Text and syntactic level, the same scale [Indent_reader] reads: 10 an atom - or bracket, 9 a postfix chain, 8 a unary minus, 1-7 binary, 3 [not], 0 a - one-line [if] or a lambda. *) +(* Text and syntactic level, the same scale [Indent_reader] reads: 13 an atom + or bracket, 12 a postfix chain, 11 a prefix [-] or [~~], 1-10 binary, 3 + [not], 0 a one-line [if] or a lambda. *) +(* The operator a head prints as: [=] is [==], and the bit words are the + operators the reader turns into them. *) +let infix_op = function + | "=" -> "==" | "bit-and" -> "&&" | "bit-or" -> "||" | "bit-xor" -> "^^" + | s -> s + let rec expr (f : Form.t) : string * int = match f.v with | Form.Sym s when !hole && s = hole_sym -> (s, 0) | Form.Sym s -> sym f s | Form.Kw k -> - if kw_ok k then (":" ^ k, 10) else unprintable f "a keyword with no spelling" + if kw_ok k then (":" ^ k, 13) else unprintable f "a keyword with no spelling" | Form.Int i -> let t = Option.value (!spelling f) ~default:(Int64.to_string i) in - (t, if t.[0] = '-' then 8 else 10) - | Form.UInt (_, s) -> (s, 10) + (t, if t.[0] = '-' then 11 else 13) + | Form.UInt (_, s) -> (s, 13) | Form.Float x -> let s = Option.value (!spelling f) ~default:(Form.float_repr x) in if not (Reader.is_digit s.[0] || (s.[0] = '-' && String.length s > 1 && Reader.is_digit s.[1])) then unprintable f "a float with no literal"; - (s, if s.[0] = '-' then 8 else 10) - | Form.Str s -> ("\"" ^ Form.escape s ^ "\"", 10) - | Form.Byte b -> (Form.byte_repr b, 10) - | Form.Vec xs -> ("[" ^ vec_text xs ^ "]", 10) - | Form.Map xs -> ("{" ^ map_text xs ^ "}", 10) - | Form.List [] -> ("()", 10) + (s, if s.[0] = '-' then 11 else 13) + | Form.Str s -> ("\"" ^ Form.escape s ^ "\"", 13) + | Form.Byte b -> (Form.byte_repr b, 13) + | Form.Vec xs -> ("[" ^ vec_text xs ^ "]", 13) + | Form.Map xs -> ("{" ^ map_text xs ^ "}", 13) + | Form.List [] -> ("()", 13) | Form.List (h :: args) -> in_quasi f (fun () -> list f h args) and sym f s = if s = "==" then unprintable f "the name == (it reads as =)" - else if R.is_op_word s || s = "if" then (paren s, 10) - else if name_ok s then (s, 10) + else if R.is_op_word s || s = "if" then (paren s, 13) + else if name_ok s then (s, 13) else unprintable f (Printf.sprintf "the name %s" s) and at lvl f = @@ -347,16 +353,16 @@ and commas xs = String.concat ", " (comma_items xs) as soon as one element has an operator in it. *) and vec_text xs = let ts = List.map expr xs in - if List.for_all (fun (_, l) -> l >= 8) ts then String.concat " " (List.map fst ts) + if List.for_all (fun (_, l) -> l >= 11) ts then String.concat " " (List.map fst ts) else String.concat ", " (List.map (fun (t, _) -> t) ts) and map_text xs = let ts = List.map expr xs in - if List.for_all (fun (_, l) -> l >= 8) ts then String.concat " " (List.map fst ts) + if List.for_all (fun (_, l) -> l >= 11) ts then String.concat " " (List.map fst ts) else let rec pairs = function | (k, kl) :: (v, _) :: rest -> - ((if kl < 8 then paren k else k) ^ " " ^ v) :: pairs rest + ((if kl < 11 then paren k else k) ^ " " ^ v) :: pairs rest | [ (k, _) ] -> [ k ] | [] -> [] in @@ -367,22 +373,26 @@ and head_text (h : Form.t) = | Form.Sym "==" -> unprintable h "the name ==" | Form.Sym s when R.is_op_word s -> s | Form.Sym s -> fst (sym h s) - | _ -> at 9 h + | _ -> at 12 h and list f h args = - let call () = (head_text h ^ "(" ^ commas args ^ ")", 9) in + let call () = (head_text h ^ "(" ^ commas args ^ ")", 12) in match h.v, args with - | Form.Sym "quote", [ x ] -> ("'" ^ Form.to_source x, 10) - | Form.Sym "unquote", [ x ] -> ("~" ^ at 10 x, 10) - | Form.Sym "unquote-splicing", [ x ] -> ("~@" ^ at 10 x, 10) + | Form.Sym "quote", [ x ] -> ("'" ^ Form.to_source x, 13) + (* [~~] is bit-not, so an unquote of anything that starts with [~] is + parenthesised: [~(~x)]. *) + | Form.Sym "unquote", [ x ] -> + let t = at 13 x in + ((if t <> "" && t.[0] = '~' then "~(" ^ t ^ ")" else "~" ^ t), 13) + | Form.Sym "unquote-splicing", [ x ] -> ("~@" ^ at 13 x, 13) | Form.Sym s, _ :: _ :: _ - when (R.is_binop s || s = "=") && s <> "==" && not (s = "!=" && List.length args > 2) -> - let op = if s = "=" then "==" else s in + when R.is_binop (infix_op s) && s <> "==" && not (s = "!=" && List.length args > 2) -> + let op = infix_op s in let lvl = Option.get (R.binop_level op) in let first = List.hd args and rest = List.tl args in let ft, fl = expr first in let same = match first.v with - | Form.List (h' :: _ :: _ :: _) -> is_sym s h' || lvl = 4 + | Form.List (h' :: _ :: _ :: _) -> (match h'.v with Form.Sym s' -> infix_op s' = op | _ -> false) || lvl = 4 | _ -> false in let ft = if fl < lvl || (fl = lvl && same) then paren ft else ft in @@ -398,18 +408,19 @@ and list f h args = (ft :: List.map (fun x -> and_in_or x (at (lvl + 1) x)) rest), lvl) | Form.Sym "-", [ x ] -> let t, l = expr x in - if l >= 9 && t <> "" && R.is_neg_char t.[0] then ("-" ^ t, 8) - else ("-(" ^ at 0 x ^ ")", 9) + if l >= 12 && t <> "" && R.is_neg_char t.[0] then ("-" ^ t, 11) + else ("-(" ^ at 0 x ^ ")", 12) | Form.Sym "not", [ x ] -> ("not " ^ at 3 x, 3) + | Form.Sym ("bit-not" | "~~"), [ x ] -> ("~~" ^ at 11 x, 11) (* [and] or [or] of one value is that value. *) | Form.Sym ("and" | "or"), [ x ] when !quasi = 0 -> expr x - | Form.Sym "at", t :: (_ :: _ as idx) -> (at 9 t ^ "[" ^ commas idx ^ "]", 9) + | Form.Sym "at", t :: (_ :: _ as idx) -> (at 12 t ^ "[" ^ commas idx ^ "]", 12) | Form.Sym s, [ t ] when String.length s > 1 && s.[0] = '.' && name_ok s && not (String.contains (String.sub s 1 (String.length s - 1)) '.') -> let tt, tl = expr t in let glued = - tl >= 9 + tl >= 12 && (match t.v with | Form.Byte _ -> false | Form.Sym x -> name_ok x && not (String.contains x '.') && not (R.capitalised x) @@ -421,9 +432,9 @@ and list f h args = let c = tt.[String.length tt - 1] in c = ')' || c = ']' || c = '}' || c = '"') in - if glued then (tt ^ s, 9) else call () + if glued then (tt ^ s, 12) else call () | Form.Sym s, [ ({ v = Form.Map _; _ } as m) ] when name_ok s && R.capitalised s -> - (s ^ fst (expr m), 9) + (s ^ fst (expr m), 12) | Form.Sym "the", _ when (match typed_lambda f with Some (_, [ _ ]) -> true | _ -> false) -> (match typed_lambda f with | Some (head, [ body ]) -> (head ^ " => " ^ unit_text body, 0) @@ -451,7 +462,7 @@ and inline_text ?(lvl = 0) (f : Form.t) = | Form.List [ { v = Form.Sym "set"; _ }; t; v ] -> assign_text ~lvl t v | Form.List [ { v = Form.Sym "update"; _ }; t; { v = Form.Sym (("+" | "-" | "*" | "/") as op); _ }; w ] when not (R.simple_place t) -> - at 9 t ^ " " ^ op ^ "= " ^ at (max lvl 1) w + at 12 t ^ " " ^ op ^ "= " ^ at (max lvl 1) w | _ -> at lvl f (* A body after [=]: [()] there reads as [(do)]. *) @@ -463,7 +474,7 @@ and unit_text (f : Form.t) = (* [t = v], or [t += w] when [v] is [(+ t w)]. *) and assign_text ?(lvl = 0) t v = - let tt = at 9 t in + let tt = at 12 t in match v.v with | Form.List [ { v = Form.Sym (("+" | "-" | "*" | "/") as op); _ }; a; w ] when same a t && R.simple_place t -> @@ -480,7 +491,7 @@ and typed_lambda (f : Form.t) = match t.v with | Form.List [ { v = Form.Sym (("Fn" | "CFn") as h); _ }; { v = Form.Vec ps; _ }; r ] -> h ^ "(" ^ String.concat ", " (List.map tyt ps) ^ ") -> " ^ tyt r - | _ -> at 9 t + | _ -> at 12 t in match f.v with | Form.List [ { v = Form.Sym "the"; _ }; @@ -499,7 +510,7 @@ let rec ty (f : Form.t) = match f.v with | Form.List [ { v = Form.Sym (("Fn" | "CFn") as h); _ }; { v = Form.Vec ps; _ }; r ] -> h ^ "(" ^ commas ps ^ ") -> " ^ ty r - | _ -> at 9 f + | _ -> at 12 f (* A [defn]'s parameter type the reader could not mistake for a name: a primitive, a capitalised or [$] name, or a bracket. [[x y]] with a @@ -739,7 +750,9 @@ and wrapped n prefix (f : Form.t) = match f.v with | Form.List (h :: (_ :: _ as args)) when (match h.v with | Form.Sym ("at" | "quote" | "unquote" | "unquote-splicing") -> false - | Form.Sym s -> not (R.is_op_word s) && not (String.length s > 1 && s.[0] = '.') + | Form.Sym s -> + not (R.is_op_word (infix_op s)) && s <> "bit-not" + && not (String.length s > 1 && s.[0] = '.') | _ -> false) -> let open_ = prefix ^ head_text h ^ "(" in let col = n + String.length open_ in @@ -771,7 +784,7 @@ and wrapped n prefix (f : Form.t) = let open_ = prefix ^ "[" in let col = n + String.length open_ in let ts = List.map expr xs in - let sep = if List.for_all (fun (_, l) -> l >= 8) ts then "" else "," in + let sep = if List.for_all (fun (_, l) -> l >= 11) ts then "" else "," in let rec go line acc = function | [] -> List.rev ((line ^ "]") :: acc) | (t, _) :: rest -> @@ -972,9 +985,9 @@ and sugar n (f : Form.t) : string list option = Some [ i ^ guard (inline_text f) ] | Form.List [ { v = Form.Sym "set"; _ }; t; v ] -> let line = i ^ guard (assign_text t v) in - if String.length line <= width && lambda_value n (guard (at 9 t)) v = None + if String.length line <= width && lambda_value n (guard (at 12 t)) v = None then Some [ line ] - else Some (value_lines n (guard (at 9 t)) v) + else Some (value_lines n (guard (at 12 t)) v) | Form.List [ { v = Form.Sym "if"; _ }; c; a; b ] -> let simple (x : Form.t) = match x.v with @@ -1068,7 +1081,7 @@ and sugar n (f : Form.t) : string list option = :: List.concat_map (fun ((pat : Form.t), body) -> List.mapi (fun k l -> if k = 0 then Source_text.tag pat.loc.Loc.line l else l) @@ - let pt = at 8 pat in + let pt = at 11 pat in let line = ind (n + 2) ^ pt ^ " -> " ^ inline_text body in match body.v with | Form.List ({ v = Form.Sym "do"; _ } :: _ :: _ :: _) -> @@ -1274,7 +1287,7 @@ and sugar n (f : Form.t) : string list option = Some ("(" ^ fst (expr p0) ^ ": " ^ ty key ^ String.concat "" (List.map (fun p -> ", " ^ fst (expr p)) rest) ^ ")") | (Form.Kw _ | Form.Str _ | Form.Int _ | Form.Sym _), _ -> - Some ("(" ^ commas ps ^ ") when " ^ at 9 key) + Some ("(" ^ commas ps ^ ") when " ^ at 12 key) | _ -> None in Option.map (fun h -> fn_like n f (i ^ "method " ^ name ^ h) body) head @@ -1339,7 +1352,7 @@ and handler_clauses n cls = match c.v with | Form.List (t :: { v = Form.Vec [ { v = Form.Sym v; _ } ]; _ } :: (_ :: _ as b)) when def_name v -> - Some ((ind n ^ "on " ^ at 9 t ^ "(" ^ v ^ ")") :: block (n + 2) b) + Some ((ind n ^ "on " ^ at 12 t ^ "(" ^ v ^ ")") :: block (n + 2) b) | _ -> None in let cs = List.map clause cls in @@ -1354,7 +1367,7 @@ and let_lines n prs body = | Form.Sym x, Form.List [ { v = Form.Sym "the"; _ }; ty_; w ] when def_name x && typed_lambda v = None -> ("let " ^ x ^ ": " ^ ty ty_, w) - | _ -> ("let " ^ guard (at 8 t), v) + | _ -> ("let " ^ guard (at 11 t), v) in (* Each binding line carries its own source line, so a comment written after a binding stays on it. *) diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index b703d7f4..5c004229 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -23,6 +23,7 @@ type tok = | COMMA | COLON (* x: T, and the trailing : of a call's block *) | UNQ | SPLICE (* ~ and ~@ *) + | BNOT (* ~~, bit-not; a nested unquote is ~(~x) *) | NEG (* the - glued to the front of a name *) | NEWLINE | INDENT | DEDENT | EOF @@ -36,7 +37,7 @@ let show = function | ATOM v -> Form.to_source (Form.make v Loc.unknown) | DATUM f -> Form.to_source f | LP -> "(" | RP -> ")" | LB -> "[" | RB -> "]" | LC -> "{" | RC -> "}" - | COMMA -> "," | COLON -> ":" | UNQ -> "~" | SPLICE -> "~@" | NEG -> "-" + | COMMA -> "," | COLON -> ":" | UNQ -> "~" | SPLICE -> "~@" | BNOT -> "~~" | NEG -> "-" | NEWLINE -> "the end of the line" | INDENT -> "an indented line" | DEDENT -> "the end of the block" @@ -45,17 +46,23 @@ let show = function (* ── Names ─────────────────────────────────────────────────────────── *) (* Binary operators and their levels, low to high (spec §2 "Precedence"). - [not] sits at 3 and unary minus at 8; neither is binary. *) + [not] sits at 3 and the prefix [-] and [~~] at 11; neither is binary. The + bit operators sit between the comparisons and the shifts, Python's and + Rust's order, so [x && mask == 0] is [(x && mask) == 0]. *) let binops = [ ("or", 1); ("and", 2); ("==", 4); ("!=", 4); ("<", 4); ("<=", 4); (">", 4); (">=", 4); - ("<<", 5); (">>", 5); ("+", 6); ("-", 6); ("*", 7); ("/", 7); ("%", 7) ] + ("||", 5); ("^^", 6); ("&&", 7); + ("<<", 8); (">>", 8); ("+", 9); ("-", 9); ("*", 10); ("/", 10); ("%", 10) ] let binop_level s = List.assoc_opt s binops let is_binop s = binop_level s <> None -(* [==] is Flan's [=]; every other operator is its own name. *) -let op_sym = function "==" -> "=" | s -> s +(* [==] is Flan's [=], and the bit operators are the words the Lisp side + writes; every other operator is its own name. *) +let op_sym = function + | "==" -> "=" | "&&" -> "bit-and" | "||" -> "bit-or" | "^^" -> "bit-xor" + | s -> s (* Words that are operators rather than names wherever a value is read. Alone before a comma or a closer they are the symbol itself, [reduce(+, 0, xs)]; @@ -185,7 +192,11 @@ let lex ?(line = 1) ?(col = 1) ~file src : token list = indented block, or quasiquote(x) on one line" | '~' -> Reader.advance st; - if Reader.peek st = '@' then begin + if Reader.peek st = '~' then begin + Reader.advance st; + emit BNOT (Loc.upto l0 (Reader.here st)) + end + else if Reader.peek st = '@' then begin Reader.advance st; emit SPLICE (Loc.upto l0 (Reader.here st)) end @@ -521,7 +532,7 @@ let where_ p = | _ -> t.loc let starts_value = function - | NAME _ | KW _ | ATOM _ | DATUM _ | LP | LB | LC | UNQ | SPLICE | NEG -> true + | NAME _ | KW _ | ATOM _ | DATUM _ | LP | LB | LC | UNQ | SPLICE | BNOT | NEG -> true | _ -> false let ends_value = function @@ -654,16 +665,16 @@ let refuse_ws ?(brace = false) loc e = (if brace then "entries" else "elements") (if brace then "{.x a + 1, .y 2}" else "[a - 1, b]") -(* Expressions come back with their syntactic level: 10 an atom or a bracket, - 9 a postfix chain, 8 a unary minus, 1-7 a binary operator's level, 3 a - [not], 0 a one-line [if] or a lambda. Anything under 8 is "compound": it - has an operator at its top, so it cannot sit in a list separated only by - whitespace. *) +(* Expressions come back with their syntactic level: 13 an atom or a bracket, + 12 a postfix chain, 11 a prefix [-] or [~~], 1-10 a binary operator's + level, 3 a [not], 0 a one-line [if] or a lambda. Anything under 11 is + "compound": it has an operator at its top, so it cannot sit in a list + separated only by whitespace. *) let rec expr p : Form.t * int = binary p 1 and binary p lvl : Form.t * int = if lvl = 3 then not_ p - else if lvl > 7 then unary p + else if lvl > 10 then unary p else let l0 = (peek p).loc in let ((first, _) as fst_) = binary p (lvl + 1) in @@ -727,7 +738,11 @@ and unary p = | NEG -> ignore (advance p); let x, _ = postfix p in - (mk p t.loc (Form.List [ sym t.loc "-"; x ]), 8) + (mk p t.loc (Form.List [ sym t.loc "-"; x ]), 11) + | BNOT -> + ignore (advance p); + let x, _ = unary p in + (mk p t.loc (Form.List [ sym t.loc "bit-not"; x ]), 11) | _ -> postfix p and postfix p = @@ -740,18 +755,18 @@ and postfix p = | LP -> ignore (advance p); let args = items p RP t.loc ~what:"arguments" in - loop (mk p l0 (Form.List (f :: args)), 9) + loop (mk p l0 (Form.List (f :: args)), 12) | LB -> ignore (advance p); let idx = items p RB t.loc ~what:"indices" ~head:(text_of f) in - loop (mk p l0 (Form.List (sym t.loc "at" :: f :: idx)), 9) + loop (mk p l0 (Form.List (sym t.loc "at" :: f :: idx)), 12) | NAME s when String.length s > 1 && s.[0] = '.' -> ignore (advance p); - loop (mk p l0 (Form.List [ sym t.loc s; f ]), 9) + loop (mk p l0 (Form.List [ sym t.loc s; f ]), 12) | LC -> ignore (advance p); let m = map_items p t.loc in - loop (mk p l0 (Form.List [ f; Form.make (Form.Map m) (span p t.loc) ]), 9) + loop (mk p l0 (Form.List [ f; Form.make (Form.Map m) (span p t.loc) ]), 12) | _ -> fp in loop (primary p) @@ -768,7 +783,7 @@ and primary p : Form.t * int = else if is_op_word s then begin if glued_lp || ends_value nxt.tok then begin ignore (advance p); - (sym l0 (op_sym s), 10) + (sym l0 (op_sym s), 13) end else failk "operator-operand" l0 @@ -780,18 +795,18 @@ and primary p : Form.t * int = else begin ignore (advance p); check_name t s; - (sym l0 s, 10) + (sym l0 s, 13) end - | KW k -> ignore (advance p); (Form.make (Form.Kw k) l0, 10) + | KW k -> ignore (advance p); (Form.make (Form.Kw k) l0, 13) | ATOM v -> ignore (advance p); - (Form.make v l0, if negative_literal t.tok then 8 else 10) - | DATUM f -> ignore (advance p); (f, 10) + (Form.make v l0, if negative_literal t.tok then 11 else 13) + | DATUM f -> ignore (advance p); (f, 13) | LP -> ignore (advance p); if (peek p).tok = RP then begin ignore (advance p); - (mk p l0 (Form.List []), 10) + (mk p l0 (Form.List []), 13) end else let e, _ = expr p in @@ -804,21 +819,21 @@ and primary p : Form.t * int = Several values in a list are written in brackets, [a, b]; \ arguments go glued to a name, f(a, b)" | _ -> stray p ~after:(text_of e)); - (e, 10) + (e, 13) | LB -> ignore (advance p); let xs = vec_items p l0 in - (mk p l0 (Form.Vec xs), 10) + (mk p l0 (Form.Vec xs), 13) | LC -> ignore (advance p); let xs = map_items p l0 in - (mk p l0 (Form.Map xs), 10) + (mk p l0 (Form.Map xs), 13) | UNQ | SPLICE -> ignore (advance p); let x, _ = primary p in let name = if t.tok = UNQ then "unquote" else "unquote-splicing" in - (mk p l0 (Form.List [ sym l0 name; x ]), 10) - | NEG -> unary p + (mk p l0 (Form.List [ sym l0 name; x ]), 13) + | NEG | BNOT -> unary p | tk -> failk "expected-value" (where_ p) "expected a value here, and found %s" (show tk) @@ -919,7 +934,7 @@ and fn_expr p = is what follows. *) | tk when names && n.loc.Loc.line > rp.loc.Loc.eline && starts_value tk -> lambda_arrow n.loc (header ()) - | _ -> (mk p t.loc (Form.List (sym t.loc "fn" :: args)), 9) + | _ -> (mk p t.loc (Form.List (sym t.loc "fn" :: args)), 12) (* What follows a lambda's [=>]: a value on the line, or the indented block under it. [header] is the lambda's header as written, for a message. *) @@ -1063,7 +1078,7 @@ and vec_items p open_loc = | EOF -> unclosed p '[' open_loc | _ -> let e, lvl = expr p in - if lvl < 8 && prev_ws then refuse_ws t.loc e; + if lvl < 11 && prev_ws then refuse_ws t.loc e; (match (peek p).tok with | COMMA -> if !spaces then mixed (peek p).loc; @@ -1072,7 +1087,7 @@ and vec_items p open_loc = | RB -> ignore (advance p); List.rev (e :: acc) | EOF -> unclosed p '[' open_loc | tk when starts_value tk && (peek p).sp -> - if lvl < 8 then refuse_ws t.loc e; + if lvl < 11 then refuse_ws t.loc e; if !commas then mixed (peek p).loc; spaces := true; go (e :: acc) true @@ -1103,7 +1118,7 @@ and map_items p open_loc = and no %s between it and the value" n (if tk = COLON then "colon" else "= sign") | tk when starts_value tk && (peek p).sp -> - if lvl < 8 then refuse_ws ~brace:true t.loc e; + if lvl < 11 then refuse_ws ~brace:true t.loc e; go (e :: acc) | _ -> stray p ~after:(text_of e)) in diff --git a/lib/js.ml b/lib/js.ml index 43d3af0c..7ab9f151 100644 --- a/lib/js.ml +++ b/lib/js.ml @@ -889,6 +889,12 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) = | Tast.Not, [ x ] -> Printf.sprintf "(!%s)" (value f x) | (Tast.BitAnd | Tast.BitOr | Tast.BitXor | Tast.Shl | Tast.Shr), [ x; y ] -> bitwise f e p x y + | Tast.BitNot, [ x ] -> ( + match x.Tast.ty with + | Types.Int k -> norm k (Printf.sprintf "~(%s)" (value f x)) + | t -> at loc "a bitwise operation on %s" (Types.to_string t)) + | (Tast.Popcount | Tast.Clz | Tast.Ctz | Tast.Rotl | Tast.Rotr), _ -> + at loc "the bit counts and rotations are not in the JS dialect" | Tast.Len, [ x ] -> ( match x.Tast.ty with | Types.Array (n, _) -> Printf.sprintf "%Ld" n diff --git a/lib/reader.ml b/lib/reader.ml index 40890424..6e50b465 100644 --- a/lib/reader.ml +++ b/lib/reader.ml @@ -209,6 +209,9 @@ let rec read_form st = advance st; if peek st = '@' then (advance st; read_wrapped st loc "unquote-splicing") else read_wrapped st loc "unquote" + (* [^^] is bit-xor's other name, and metadata on a form that starts with + [^] would mean nothing, so the two cannot collide. *) + | '^' when peek2 st = '^' -> read_symbol_or_keyword st | '^' -> Loc.failk "reader/metadata" loc "metadata (^) is not supported yet" diff --git a/lib/tast.ml b/lib/tast.ml index be09a3a5..e297c882 100644 --- a/lib/tast.ml +++ b/lib/tast.ml @@ -23,6 +23,10 @@ type prim = (* bitwise, integers only. [Shr] is arithmetic on a signed type and logical on an unsigned one, which is what the operand's own kind already says. *) | BitAnd | BitOr | BitXor | Shl | Shr + (* One operand each, and the answer has the operand's type. [Clz] and [Ctz] + answer the width for zero. [Rotl] and [Rotr] take the count modulo the + width, so no count is out of range. *) + | BitNot | Popcount | Clz | Ctz | Rotl | Rotr (* containers: fixed arrays and slices only at milestone 2 *) | Len | At | Slice (* (slice-from p n): a [T] made out of a (Ptr T) and a length the caller diff --git a/lib/x86.ml b/lib/x86.ml index 8a512401..78119d26 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -386,6 +386,32 @@ let shift_cl b ~ext ~dst = rex b ~w:true ~r:0 ~x:0 ~m:dst; u8 b 0xd3; modrm_r b let shl_cl b ~dst = shift_cl b ~ext:4 ~dst let shr_cl b ~dst = shift_cl b ~ext:5 ~dst let sar_cl b ~dst = shift_cl b ~ext:7 ~dst +let shift_imm b ~ext ~dst n = + rex b ~w:true ~r:0 ~x:0 ~m:dst; u8 b 0xc1; modrm_r b ~r:ext ~m:dst; u8 b n + +(* bsf (0xbc) and bsr (0xbd): the index of the lowest or highest set bit, with + ZF set and the destination undefined when the source is zero. Both are in + every x86-64 CPU, which tzcnt, lzcnt and popcnt are not. *) +let bitscan b ~op ~dst ~src = + rex b ~w:true ~r:dst ~x:0 ~m:src; u8 b 0x0f; u8 b op; modrm_r b ~r:dst ~m:src + +let cmovz_rr b ~dst ~src = + rex b ~w:true ~r:dst ~x:0 ~m:src; u8 b 0x0f; u8 b 0x44; modrm_r b ~r:dst ~m:src + +(* rol (ext 0) and ror (ext 1) by cl at the operand's own width, unlike the + shifts above: a rotation at 64 bits of a value that is 8 wide would bring + the wrong bits round. The hardware masks cl to 5 bits (6 at 64) and then + rotates modulo the width, which is the language's rule for every width. *) +let rot_cl b ~ext ~bits ~dst = + match bits with + | 8 -> + rex ~force:(dst >= 4) b ~w:false ~r:0 ~x:0 ~m:dst; u8 b 0xd2; + modrm_r b ~r:ext ~m:dst + | 16 -> + u8 b 0x66; rex b ~w:false ~r:0 ~x:0 ~m:dst; u8 b 0xd3; + modrm_r b ~r:ext ~m:dst + | 32 -> rex b ~w:false ~r:0 ~x:0 ~m:dst; u8 b 0xd3; modrm_r b ~r:ext ~m:dst + | _ -> rex b ~w:true ~r:0 ~x:0 ~m:dst; u8 b 0xd3; modrm_r b ~r:ext ~m:dst let setcc b ~cc ~dst = rex ~force:(dst >= 4) b ~w:false ~r:0 ~x:0 ~m:dst; @@ -3402,7 +3428,66 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) dst = end; movzx8 f.b ~dst:rax ~src:rax; store_loc f ~reg:rax dst Types.Bool - | Tast.Not, [ a ] -> + (* Zero-extended to 64 bits first, so a negative i8 counts eight bits and + not sixty-four. Every sequence below is baseline x86-64, the target LLVM + is given too: popcount is the SWAR sum LLVM writes for [ctpop] without + popcnt, and the two scans answer the width for zero through a cmov on the + flag bsr and bsf set. *) + | (Tast.Popcount | Tast.Clz | Tast.Ctz), [ a ] -> + let la = eval f a in + let w = + match a.Tast.ty with + | Types.Int k -> Types.bits k + | t -> unsupported "a bit count of %s" (Types.to_string t) + in + load_int f.b ~dst:rax ~mm:(lmem f la ~scratch:r11) ~size:(w / 8) + ~signed:false; + (match p with + | Tast.Popcount -> + mov_rr f.b ~dst:rcx ~src:rax; + shift_imm f.b ~ext:5 ~dst:rcx 1; + imm_into f ~reg:rdx 0x5555555555555555L; + and_rr f.b ~dst:rcx ~src:rdx; + sub_rr f.b ~dst:rax ~src:rcx; + imm_into f ~reg:rdx 0x3333333333333333L; + mov_rr f.b ~dst:rcx ~src:rax; + and_rr f.b ~dst:rcx ~src:rdx; + shift_imm f.b ~ext:5 ~dst:rax 2; + and_rr f.b ~dst:rax ~src:rdx; + add_rr f.b ~dst:rax ~src:rcx; + mov_rr f.b ~dst:rcx ~src:rax; + shift_imm f.b ~ext:5 ~dst:rcx 4; + add_rr f.b ~dst:rax ~src:rcx; + imm_into f ~reg:rdx 0x0f0f0f0f0f0f0f0fL; + and_rr f.b ~dst:rax ~src:rdx; + imm_into f ~reg:rdx 0x0101010101010101L; + imul_rr f.b ~dst:rax ~src:rdx; + shift_imm f.b ~ext:5 ~dst:rax 56 + | Tast.Clz -> + bitscan f.b ~op:0xbd ~dst:rcx ~src:rax; + imm_into f ~reg:rdx (-1L); + cmovz_rr f.b ~dst:rcx ~src:rdx; + imm_into f ~reg:rax (Int64.of_int (w - 1)); + sub_rr f.b ~dst:rax ~src:rcx + | _ -> + bitscan f.b ~op:0xbc ~dst:rcx ~src:rax; + imm_into f ~reg:rdx (Int64.of_int w); + cmovz_rr f.b ~dst:rcx ~src:rdx; + mov_rr f.b ~dst:rax ~src:rcx); + store_loc f ~reg:rax dst t + | (Tast.Rotl | Tast.Rotr), [ a; b ] -> + let la = eval f a in + let lb = eval f b in + let w = + match a.Tast.ty with + | Types.Int k -> Types.bits k + | t -> unsupported "a rotation of %s" (Types.to_string t) + in + load_loc f ~reg:rax la a.Tast.ty; + load_loc f ~reg:rcx lb b.Tast.ty; + rot_cl f.b ~ext:(if p = Tast.Rotl then 0 else 1) ~bits:w ~dst:rax; + store_loc f ~reg:rax dst t + | (Tast.Not | Tast.BitNot), [ a ] -> let la = eval f a in if Types.equal a.Tast.ty Types.Bool then begin load_loc f ~reg:rax la Types.Bool; diff --git a/runtime/flan_dyn.c b/runtime/flan_dyn.c index 8c2a24e6..4f4f17cf 100644 --- a/runtime/flan_dyn.c +++ b/runtime/flan_dyn.c @@ -2785,6 +2785,118 @@ flan_dyn flan_dyn_rem(flan_dyn a, flan_dyn b, const uint8_t *loc, return arith(loc, loclen, "%", a, b); } +/* ── Bits ────────────────────────────────────────────────────────────── + * + * Ints only: a float has no bits a program means, and a bool is most likely + * a reach for logical and from C, so its trap says which operator that is. + * Every answer is the one typed i64 code gives for the same operands. A shift + * count outside 0..63 traps rather than being masked as typed code masks it: + * there is no width here to have been chosen, and a count out of range is a + * mistake the value cannot show. */ + +#define BITS_INT "it takes integers" +#define BITS_BOOL "it takes integers; true and false are combined with and, or and not" + +static int is_int(flan_dyn v) { return flan_dyn_tag(v) == FLAN_DYN_TAG_INT; } +static int is_bool(flan_dyn v) { return flan_dyn_tag(v) == FLAN_DYN_TAG_BOOL; } + +static void want_ints(const uint8_t *loc, int64_t loclen, const char *op, + flan_dyn a, flan_dyn b) { + if (!is_int(a) || !is_int(b)) + trap2(loc, loclen, TYPE_TRAP, op, + is_bool(a) || is_bool(b) ? BITS_BOOL : BITS_INT, a, b); +} + +static void want_int(const uint8_t *loc, int64_t loclen, const char *op, + flan_dyn a) { + if (!is_int(a)) + trap1(loc, loclen, TYPE_TRAP, op, is_bool(a) ? BITS_BOOL : BITS_INT, a); +} + +flan_dyn flan_dyn_bitand(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + want_ints(loc, loclen, "bit-and", a, b); + return flan_dyn_from_i64(dyn_int_value(a) & dyn_int_value(b)); +} +flan_dyn flan_dyn_bitor(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + want_ints(loc, loclen, "bit-or", a, b); + return flan_dyn_from_i64(dyn_int_value(a) | dyn_int_value(b)); +} +flan_dyn flan_dyn_bitxor(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + want_ints(loc, loclen, "bit-xor", a, b); + return flan_dyn_from_i64(dyn_int_value(a) ^ dyn_int_value(b)); +} +flan_dyn flan_dyn_bitnot(flan_dyn a, const uint8_t *loc, int64_t loclen) { + want_int(loc, loclen, "bit-not", a); + return flan_dyn_from_i64(~dyn_int_value(a)); +} + +static uint64_t shift_count(const uint8_t *loc, int64_t loclen, const char *op, + flan_dyn a, flan_dyn b) { + int64_t n; + want_ints(loc, loclen, op, a, b); + n = dyn_int_value(b); + if (n < 0 || n > 63) + trap2(loc, loclen, ARITH_TRAP, op, + "the count is outside 0 to 63, the bits an int has", a, b); + return (uint64_t)n; +} + +flan_dyn flan_dyn_shl(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + uint64_t n = shift_count(loc, loclen, "<<", a, b); + return flan_dyn_from_i64((int64_t)((uint64_t)dyn_int_value(a) << n)); +} +/* Arithmetic, as >> on a typed i64 is. */ +flan_dyn flan_dyn_shr(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + uint64_t n = shift_count(loc, loclen, ">>", a, b); + int64_t x = dyn_int_value(a); + /* >> on a negative int64_t is implementation-defined in C before C23; + * the complement trick is arithmetic on every compiler. */ + if (x < 0) return flan_dyn_from_i64(~(int64_t)(~(uint64_t)x >> n)); + return flan_dyn_from_i64((int64_t)((uint64_t)x >> n)); +} + +static uint64_t rot(uint64_t x, uint64_t n, int left) { + n &= 63; + if (n == 0) return x; + return left ? (x << n) | (x >> (64 - n)) : (x >> n) | (x << (64 - n)); +} + +flan_dyn flan_dyn_rotl(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + want_ints(loc, loclen, "rotate-left", a, b); + return flan_dyn_from_i64((int64_t)rot((uint64_t)dyn_int_value(a), + (uint64_t)dyn_int_value(b), 1)); +} +flan_dyn flan_dyn_rotr(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + want_ints(loc, loclen, "rotate-right", a, b); + return flan_dyn_from_i64((int64_t)rot((uint64_t)dyn_int_value(a), + (uint64_t)dyn_int_value(b), 0)); +} + +flan_dyn flan_dyn_popcount(flan_dyn a, const uint8_t *loc, int64_t loclen) { + want_int(loc, loclen, "popcount", a); + return flan_dyn_from_i64(__builtin_popcountll((uint64_t)dyn_int_value(a))); +} +/* 64 for zero, which the builtins leave undefined. */ +flan_dyn flan_dyn_clz(flan_dyn a, const uint8_t *loc, int64_t loclen) { + uint64_t x; + want_int(loc, loclen, "leading-zeros", a); + x = (uint64_t)dyn_int_value(a); + return flan_dyn_from_i64(x == 0 ? 64 : __builtin_clzll(x)); +} +flan_dyn flan_dyn_ctz(flan_dyn a, const uint8_t *loc, int64_t loclen) { + uint64_t x; + want_int(loc, loclen, "trailing-zeros", a); + x = (uint64_t)dyn_int_value(a); + return flan_dyn_from_i64(x == 0 ? 64 : __builtin_ctzll(x)); +} + /* ── Ordering ────────────────────────────────────────────────────────── * * Numbers against numbers, text against text, and nothing else. Text orders diff --git a/runtime/flan_dyn.h b/runtime/flan_dyn.h index 5e6a118b..73e1b16d 100644 --- a/runtime/flan_dyn.h +++ b/runtime/flan_dyn.h @@ -205,6 +205,20 @@ flan_dyn flan_dyn_div(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen flan_dyn flan_dyn_rem(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); flan_dyn flan_dyn_neg(flan_dyn a, const uint8_t *loc, int64_t loclen); +/* The bit operations, on ints only. A shift count outside 0..63 traps, where + * typed code masks it; a rotation takes its count modulo 64. */ +flan_dyn flan_dyn_bitand(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_bitor(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_bitxor(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_bitnot(flan_dyn a, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_shl(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_shr(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_rotl(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_rotr(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_popcount(flan_dyn a, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_clz(flan_dyn a, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_ctz(flan_dyn a, const uint8_t *loc, int64_t loclen); + /* Answer a bool dyn. Numbers compare as numbers and text compares bytewise; * a mixture of the two, or anything else, traps. */ flan_dyn flan_dyn_lt(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); diff --git a/spec-syntax.md b/spec-syntax.md index bda24f0a..754753b9 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -155,9 +155,15 @@ Each item: the proposal, then the reason in one line. ### Expressions - **Precedence**, low to high: `or` < `and` < `not` < comparisons - (`== != < <= > >=`) < `<< >>` < `+ -` < `* / %` < unary `-` < postfix (call, - index, field). **Built.** Mixing comparison operators in one chain, - `a < b <= c`, is refused. An operator glued to `(` is always a call. + (`== != < <= > >=`) < `||` < `^^` < `&&` < `<< >>` < `+ -` < `* / %` < + prefix `-` and `~~` < postfix (call, index, field). **Built.** Mixing + comparison operators in one chain, `a < b <= c`, is refused. An operator + glued to `(` is always a call. The bit operators sit where Python and Rust + put them, so `x && mask == 0` is `(x && mask) == 0`. +- **The bit operators** are `a && b`, `a || b`, `a ^^ b` and `~~a`, reading + `(bit-and a b)`, `(bit-or a b)`, `(bit-xor a b)` and `(bit-not a)`. They take + integers; `and`, `or` and `not` are the logical ones. `~~` is one token, so a + nested unquote is written `~(~x)`. **Built.** - **`==` is `=`; `=` is assignment.** `x = v` reads `(set x v)`, `a[i] = v` reads `(set (at a i) v)`, `p.x = v` reads `(set (.x p) v)`. `x += v` reads `(set x (+ x v))` where every part of the place is a name or a literal, and diff --git a/test/programs/bits-dyn.flan b/test/programs/bits-dyn.flan new file mode 100644 index 00000000..9dddbaa2 --- /dev/null +++ b/test/programs/bits-dyn.flan @@ -0,0 +1,51 @@ +;;;; The bit operators on dyn ints, beside the same operations on typed i64: +;;;; each line prints the dyn answer, the typed one, and whether they agree. +;;;; A dyn int is an i64, so the two must be the same number for every count +;;;; in 0..63. With an argument, the program traps instead: 1 a shift count +;;;; out of range, 2 a bool operand, 3 a float operand, 4 a negative count to >>. + +(defn dyn-ops [a b n] dyn + [(bit-and a b) (bit-or a b) (bit-xor a b) (bit-not a) (<< a n) (>> a n) + (rotate-left a n) (rotate-right a n) (popcount a) (leading-zeros a) + (trailing-zeros a) (bit-xor a b n) (&& a b) (|| a b)]) + +(defn typed-ops [a i64 b i64 n i64] [14 i64] + [(bit-and a b) (bit-or a b) (bit-xor a b) (bit-not a) (<< a n) (>> a n) + (rotate-left a n) (rotate-right a n) (popcount a) (leading-zeros a) + (trailing-zeros a) (bit-xor a b n) (&& a b) (|| a b)]) + +(defn compare [a i64 b i64 n i64] () + (let [d (dyn-ops a b n) + t (typed-ops a b n)] + (dotimes [i 14] + (print (at d i) " ") + (when (!= (i64 (at d i)) (at t i)) + (print "DIFFER at " i " typed " (at t i) " "))) + (println))) + +;; A typed operand beside a dyn one makes the whole operation dyn. +(defonce mask i32 255) +(defn mixed [x] dyn (bit-and x mask)) + +(defn shift [a n] dyn (<< a n)) +(defn sar [a n] dyn (>> a n)) +(defn band [a b] dyn (bit-and a b)) + +(defn main [args [str]] i32 + (let [k (if (> (length args) 1) (bytes->i64 (bytes-view (at args 1))) 0)] + (cond + (= k 0) + (do + (compare 0 0 0) + (compare -1 12345 63) + (compare -9000000000000000000 1234567890123 13) + (compare 9223372036854775807 -9223372036854775807 1) + (compare 281474976710656 -281474976710657 47) + (compare 1 3 62) + (println (mixed 4660) (mixed -1)) + (println (leading-zeros 0) (trailing-zeros 0) (popcount -1)) + 0) + (= k 1) (do (println "before") (println (shift 1 64)) 0) + (= k 2) (do (println "before") (println (band true 1)) 0) + (= k 3) (do (println "before") (println (band 1.5 1)) 0) + :else (do (println "before") (println (sar 1 -1)) 0)))) diff --git a/test/programs/bits.flan b/test/programs/bits.flan new file mode 100644 index 00000000..5d5ab743 --- /dev/null +++ b/test/programs/bits.flan @@ -0,0 +1,46 @@ +;;;; The bit operators at every integer width, typed. Every operand is a +;;;; global or a parameter, so -O2 folds nothing and the x86 backend lowers +;;;; each one; the three builds must print the same lines. One generic body +;;;; covers the widths, which also walks the integer? bound. + +(defn bits [a $t b $t n $t] () + {:where (integer? $t)} + (println (bit-and a b) (bit-or a b) (bit-xor a b) (bit-not a) + (bit-and a b n) (&& a b) (|| a b) (^^ a b)) + (println (<< a n) (>> a n) (rotate-left a n) (rotate-right a n)) + (println (popcount a) (leading-zeros a) (trailing-zeros a) + (popcount b) (leading-zeros b) (trailing-zeros b))) + +(defonce i8a i8 -100) (defonce i8b i8 45) (defonce i8n i8 3) +(defonce i16a i16 -30000) (defonce i16b i16 12345) (defonce i16n i16 5) +(defonce i32a i32 -2000000000) (defonce i32b i32 123456789) (defonce i32n i32 7) +(defonce i64a i64 -9000000000000000000) (defonce i64b i64 1234567890123) (defonce i64n i64 13) +(defonce u8a u8 200) (defonce u8b u8 45) (defonce u8n u8 3) +(defonce u16a u16 60000) (defonce u16b u16 12345) (defonce u16n u16 5) +(defonce u32a u32 4000000000) (defonce u32b u32 123456789) (defonce u32n u32 7) +(defonce u64a u64 18000000000000000000) (defonce u64b u64 1234567890123) (defonce u64n u64 13) + +;; Zero, for the counts: leading-zeros and trailing-zeros answer the width. +(defonce z8 i8 0) (defonce z16 u16 0) (defonce z32 i32 0) (defonce z64 u64 0) +;; A rotation count past the width, and a negative one: both modulo the width. +(defonce big32 i32 35) (defonce neg32 i32 -1) (defonce big8 u8 11) + +(defn main [] i32 + (bits i8a i8b i8n) + (bits i16a i16b i16n) + (bits i32a i32b i32n) + (bits i64a i64b i64n) + (bits u8a u8b u8n) + (bits u16a u16b u16n) + (bits u32a u32b u32n) + (bits u64a u64b u64n) + (println (leading-zeros z8) (trailing-zeros z8) + (leading-zeros z16) (trailing-zeros z16) + (leading-zeros z32) (trailing-zeros z32) + (leading-zeros z64) (trailing-zeros z64) (popcount z64)) + (println (rotate-left i32b big32) (rotate-right i32b big32) + (rotate-left i32b neg32) (rotate-left u8a big8) + (rotate-right u8a big8)) + ;; A narrower count widens to the value's type. + (println (<< i64b u8n) (rotate-left u64b u8n)) + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index ff0185a4..c86ce46e 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -669,6 +669,81 @@ let () = outputs "unary minus" "programs/negate.flan" neg_out; outputs ~opt:"-O0" "unary minus, -O0" "programs/negate.flan" neg_out; outputs ~x86:true "unary minus, x86" "programs/negate.flan" neg_out; + (* The bit operators at every width, and on dyn ints beside typed i64: + both backends and both optimisation levels print the same numbers. *) + let bits_out = + "12 -67 -79 99 0 12 -67 -79\n\ + -32 -13 -28 -109\n\ + 4 0 2 4 2 0\n\ + 16 -17671 -17687 29999 0 16 -17671 -17687\n\ + 23040 -938 23057 -31658\n\ + 6 0 4 6 2 0\n\ + 4869120 -1881412331 -1886281451 1999999999 0 4869120 -1881412331 -1886281451\n\ + 1698037760 -15625000 1698037828 17929432\n\ + 10 0 10 16 5 0\n\ + 1164229214208 -8999999929661324085 -9000001093890538293 8999999999999999999 0 1164229214208 -8999999929661324085 -9000001093890538293\n\ + 3636062617077809152 -1098632812500000 3636062617077813347 1153167001185248\n\ + 25 0 18 23 23 0\n\ + 8 237 229 55 0 8 237 229\n\ + 64 25 70 25\n\ + 3 0 3 4 2 0\n\ + 8224 64121 55897 5535 0 8224 64121 55897\n\ + 19456 1875 19485 1875\n\ + 7 0 5 6 2 0\n\ + 105580544 4017876245 3912295701 294967295 0 105580544 4017876245 3912295701\n\ + 898891776 31250000 898891895 31250000\n\ + 13 0 11 16 5 0\n\ + 5386010624 18000001229181879499 18000001223795868875 446744073709551615 0 5386010624 18000001229181879499 18000001223795868875\n\ + 11174618839553933312 2197265625000000 11174618839553941305 2197265625000000\n\ + 22 0 19 23 23 0\n\ + 8 8 16 16 32 32 64 64 0\n\ + 987654312 -1595180638 -2085755254 70 25\n\ + 9876543120984 9876543120984\n\ + " in + outputs "bit operators" "programs/bits.flan" bits_out; + outputs ~opt:"-O0" "bit operators, -O0" "programs/bits.flan" bits_out; + outputs ~x86:true "bit operators, x86" "programs/bits.flan" bits_out; + let bits_dyn_out = + "0 0 0 -1 0 0 0 0 0 64 64 0 0 0 \n\ + 12345 -1 -12346 0 -9223372036854775808 -1 -1 -1 64 0 0 -12295 12345 -1 \n\ + 1164229214208 -8999999929661324085 -9000001093890538293 8999999999999999999 3636062617077809152 -1098632812500000 3636062617077813347 1153167001185248 25 0 18 -9000001093890538298 1164229214208 -8999999929661324085 \n\ + 1 -1 -2 -9223372036854775808 -2 4611686018427387903 -2 -4611686018427387905 63 1 0 -1 1 -1 \n\ + 0 -1 -1 -281474976710657 0 2 2147483648 2 1 15 48 -48 0 -1 \n\ + 1 3 2 -2 4611686018427387904 0 4611686018427387904 4 1 63 0 60 1 3 \n\ + 52 255\n\ + 32 32 32\n\ + " in + outputs "dyn bit operators" "programs/bits-dyn.flan" bits_dyn_out; + outputs ~opt:"-O0" "dyn bit operators, -O0" "programs/bits-dyn.flan" + bits_dyn_out; + outputs ~x86:true "dyn bit operators, x86" "programs/bits-dyn.flan" + bits_dyn_out; + (* A dyn bit operation traps at its own site: a shift count out of range + either way, a bool, a float. *) + let bits_traps ?opt ?x86 () = + let exe = compile ?opt ?x86 "programs/bits-dyn.flan" in + let traps arg reason = + let code, text = run exe (Some arg) in + if code <> 134 || not (contains text "before\n") + || not (contains text "programs/bits-dyn.flan:") + || not (contains text reason) + then begin + incr failures; + Printf.printf + "FAIL dyn bit trap %s\n got: %S (exit %d)\n wanted: %S (exit 134)\n" + arg text code reason + end + in + traps "1" "dyn <<: int and int, and the count is outside 0 to 63"; + traps "2" "dyn bit-and: bool and int, and it takes integers; true and \ + false are combined with and, or and not"; + traps "3" "dyn bit-and: float and int, and it takes integers —"; + traps "4" "dyn >>: int and int, and the count is outside 0 to 63"; + (try Sys.remove exe with Sys_error _ -> ()) + in + bits_traps (); + bits_traps ~opt:"-O0" (); + bits_traps ~x86:true (); (* A literal arm takes the other arm's type. *) let arm_out = "4000000\n9000000000\n5000000000\n7\n9000000000\n3\n9000000000\n2.5\n" in diff --git a/test/test_flan.ml b/test/test_flan.ml index 6b853e5f..a4990919 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -1375,6 +1375,36 @@ let () = ~needle:"bit-or takes two arguments or more, given 0"; rejects_check "one operand is not a min" "(defn f [] i32 (min 7))" ~needle:"min takes two arguments or more, given 1"; + (* The bit operators take integers, and a bool is answered with the logical + operator a C programmer meant. *) + infers "bit-not keeps its operand's type" "(bit-not (u16 5))" "u16"; + infers "popcount keeps its operand's type" "(popcount (i64 5))" "i64"; + infers "a rotation takes the value's type" "(rotate-left (u8 5) (u8 1))" "u8"; + infers "&& is bit-and" "(&& 6 3)" "i32"; + infers "a bit operation with a dyn operand is dyn" + "(bit-and (the dyn 6) (i32 3))" "dyn"; + infers "a shift with a dyn operand is dyn" "(<< (the dyn 1) 3)" "dyn"; + rejects_check "bit-and over bools points at and" + "(defn f [a bool b bool] bool (= (bit-and a b) 0))" + ~needle:"bit-and works on the bits of an integer, and this is a bool. \ + For true and false, write (and a b)"; + rejects_check "bit-not over a bool points at not" + "(defn f [a bool] i32 (bit-not a) 0)" ~needle:"write (not a)"; + rejects_check "bit-xor over bools points at !=" + "(defn f [a bool b bool] i32 (bit-xor a b) 0)" ~needle:"write (!= a b)"; + rejects_check "a typed bool beside a dyn is refused before it runs" + "(defn f [a bool d dyn] dyn (bit-or a d))" ~needle:"write (or a b)"; + rejects_check "a shift of a bool" + "(defn f [a bool] i32 (<< a 1) 0)" + ~needle:"combined with and, or and not"; + rejects_check "a bool beside an integer" + "(defn f [x i32 flag bool] i32 (bit-and x flag))" ~needle:"write (and a b)"; + rejects_check "a bool beside a literal" + "(defn f [flag bool] i32 (bit-or flag 1))" ~needle:"write (or a b)"; + rejects_check "popcount of a float" + "(defn f [a f64] f64 (popcount a))" ~needle:"popcount takes integers, found f64"; + rejects_check "a rotation's count does not widen the value" + "(defn f [a u8 n i32] u8 (rotate-left a n))" ~needle:"i32"; (* ── Chained comparisons ───────────────────────────────────────── *) (* (< a b c) is a < b and b < c. The left fold — ((a < b) < c) — would be @@ -2749,18 +2779,16 @@ let () = "(defn f [a i32 b i32] bool (=/= a b))" ~needle:"Write (!= a b)"; rejects_check "== names =" "(defn f [a i32 b i32] bool (== a b))" ~needle:"Write (= a b)"; - rejects_check "&& names and" - "(defn f [a bool b bool] bool (&& a b))" ~needle:"Write (and a b)"; + rejects_check "&& over bools names and" + "(defn f [a bool b bool] bool (&& a b))" ~needle:"write (and a b)"; accepts "and that call compiles" "(defn f [a bool b bool] bool (and a b))"; - rejects_check "|| names or" - "(defn f [a bool b bool] bool (|| a b))" ~needle:"Write (or a b)"; + rejects_check "|| over bools names or" + "(defn f [a bool b bool] bool (|| a b))" ~needle:"write (or a b)"; rejects_check "! names not" "(defn f [a bool] bool (! a))" ~needle:"Write (not a)"; rejects_check "a ! at an arity not does not take gets not's shape" "(defn f [a bool b bool] bool (! a b))" ~needle:"called as (not x)"; - rejects_check "a bare && is written back as the and that compiles" - "(defn f [] bool (&&))" ~needle:"Write (and)"; - accepts "and it does" "(defn f [] bool (and))"; + accepts "a bare and compiles" "(defn f [] bool (and))"; accepts "a program's own not= is its own" "(defn not= [a i32 b i32] bool (!= a b)) \ (defn f [a i32 b i32] bool (not= a b))"; diff --git a/test/test_syntax.ml b/test/test_syntax.ml index a623b0a4..a054e6e5 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -111,6 +111,10 @@ let canon (f : Form.t) : Form.t = let rec go env (f : Form.t) = let v = match f.v with + (* The Lisp side may write the bit operators' .fln spellings, which read + back as their words: the same builtin by two names. *) + | Form.Sym ("&&" | "||" | "^^" as s) -> + Form.Sym (match s with "&&" -> "bit-and" | "||" -> "bit-or" | _ -> "bit-xor") | Form.Sym s -> Form.Sym (look env s) (* Quoted data keeps its names: renaming them would hide a printer that renamed them too. *) @@ -524,6 +528,21 @@ let () = reads "chain" "x = a < b < c" "(set x (< a b c))"; reads "left to right" "x = a - b + c" "(set x (+ (- a b) c))"; reads "precedence" "x = a or b and not c == d" "(set x (or a (and b (not (= c d)))))"; + (* The bit operators: tighter than a comparison, looser than a shift, and + among themselves && then ^^ then ||. *) + reads "bit and under a comparison" "x = a && mask == 0" + "(set x (= (bit-and a mask) 0))"; + reads "bit operator order" "x = a || b ^^ c && d << 2 + 1" + "(set x (bit-or a (bit-xor b (bit-and c (<< d (+ 2 1))))))"; + reads "bit operators left to right" "x = a && b && c || d" + "(set x (bit-or (bit-and a b c) d))"; + reads "bit-not" "x = ~~a && ~~f(b)" "(set x (bit-and (bit-not a) (bit-not (f b))))"; + reads "bit-not of a negation" "x = ~~-a" "(set x (bit-not (- a)))"; + reads "bit operator values" "x = reduce(^^, 0, xs)" "(set x (reduce bit-xor 0 xs))"; + reads "bit-not in a spaced vector" "x = [~~a b]" "(set x [(bit-not a) b])"; + reads "a nested unquote" "quote\n f(~(~x))" "(quasiquote (f (unquote (unquote x))))"; + reads "bit-not in a template" "quote\n f(~~x, ~(~~y))" + "(quasiquote (f (bit-not x) (unquote (bit-not y))))"; refuses "not-equal chain" "x = a != b != c" "indent/chained-not-equal" "!=(a, b, c)"; reads "not-equal call" "x = !=(a, b, c)" "(set x (!= a b c))"; refuses "mixed comparison" "x = a < b <= c" "indent/mixed-comparison" "and"; @@ -909,7 +928,17 @@ let () = fail "%s: read back %s from %S" name (describe_diff forms back) text | exception e -> fail "%s: its text is refused: %s\n%s" name (diag_text e) text in - round "a one-line lambda" "(defn f [] () (h (fn [a] (+ a 1)) 2))" "= h(fn(a) => a + 1, 2)"; + round "bit operators print infix" + "(defn f [a i32 m i32] bool (= (bit-and a (bit-not m)) (bit-or (bit-xor a 1) (<< m 2))))" + "a && ~~m == a ^^ 1 || m << 2"; + round "bit operators parenthesise against precedence" + "(defn f [a i32 b i32 c i32] i32 (bit-and (bit-or a b) (+ c 1) (bit-not (bit-xor a b))))" + "= (a || b) && c + 1 && ~~(a ^^ b)"; + round "a nested unquote prints with parentheses" + "(defmacro m [x] `(defmacro n [] `(g ~~x ~(bit-not x))))" "~(~x)"; + prints "the Lisp spellings print as the operators" + "(defn f [a i32 b i32] i32 (^^ (&& a b) (|| a b)))" "a && b ^^ (a || b)"; + round "a one-line lambda""(defn f [] () (h (fn [a] (+ a 1)) 2))" "= h(fn(a) => a + 1, 2)"; round "a block lambda as a call's last argument" "(defn f [] () (sort-by xs (fn [a b] (g a) (< a b))))" " sort-by(xs, fn(a, b) =>\n g(a)\n a < b)"; From 5712d4bdc07e8d048aa797a0c2cb9b9bdb0dc614 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 11:04:32 +0700 Subject: [PATCH 06/16] A bool operand of a bit operator is refused with its and, or or not hint in a whole-file check, whichever operand it is. --- lib/check.ml | 55 +++++++++++++++++++--------------- test/test_flan.ml | 75 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 91 insertions(+), 39 deletions(-) diff --git a/lib/check.ml b/lib/check.ml index 4b3590f3..bf61092b 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -10262,23 +10262,32 @@ and bits_operand ctx loc name (v : Tast.expr) = | Types.Bool -> bool_bits v.Tast.loc name | other -> fail loc "%s takes integers, found %s" name (tyname loc other) -(* A pair with a bool in it is usually refused before [bits_operand] sees it, - as a mismatch between the bool and the other operand. When the pair is - refused, each operand is checked on its own terms, and a bool among them is - the refusal given. The compile is already failing, so the second check - costs nothing that matters. *) -and bool_first : 'a. ctx -> string -> Ast.expr list -> (unit -> 'a) -> 'a = - fun ctx name args k -> - try k () with - | Loc.Error _ as e -> - List.iter - (fun a -> - match check ctx a with - | v when v.Tast.ty = Types.Bool -> bool_bits v.Tast.loc name - | _ -> () - | exception Loc.Error _ -> ()) - (List.filteri (fun i _ -> i < 2) args); - raise e +(* A bool operand is refused before the operands are joined, and not left to + [bits_operand]: the join sees a bool beside an integer as a plain mismatch, + "expected i32, found bool", which says nothing of [and]. Only the operands + that are plainly bools are asked about, because asking is a second check + and these are free of side effects: a comparison or a [not], a name, a + field of one. Any other bool still arrives at [bits_operand]. *) +and bool_operands ctx name (args : Ast.expr list) = + let rec plain (a : Ast.expr) = + match a.Ast.e with + | Ast.Var _ -> true + | Ast.Field (t, _) -> plain t + | _ -> false + in + let is_bool (a : Ast.expr) = + match a.Ast.e with + | Ast.Call ({ Ast.e = Ast.Var h; _ }, _) + when List.mem h [ "="; "!="; "<"; "<="; ">"; ">="; "not" ] + && not (shadows_builtin ctx a.Ast.loc h) -> + true + | _ when plain a -> + (match speculate ctx.env (fun () -> check ctx a) with + | v -> v.Tast.ty = Types.Bool + | exception Loc.Error _ -> false) + | _ -> false + in + List.iter (fun a -> if is_bool a then bool_bits a.Ast.loc name) args and bool_bits loc name = let fln = fln_source loc in @@ -11061,9 +11070,9 @@ and named_call ?(qualified = false) ctx ~want loc name args = | _ -> Tast.BitXor in fold_arity loc name args; - bool_first ctx name args (fun () -> - fold_left_prim ctx ~want loc name p ~needs:"integer?" Types.is_integer - "integers" args) + bool_operands ctx name args; + fold_left_prim ctx ~want loc name p ~needs:"integer?" Types.is_integer + "integers" args (* The .fln operators, which the indented reader already spells as the words above; a form built some other way may still carry them. [~qualified] skips the shadowing arm, because a program that means its own [&&] has @@ -11076,6 +11085,7 @@ and named_call ?(qualified = false) ctx ~want loc name args = named_call ~qualified:true ctx ~want loc canon args | "bit-not" | "popcount" | "leading-zeros" | "trailing-zeros" -> arity ctx loc name 1 args; + bool_operands ctx name args; let v = check ctx ?want:(numeric_want want) (List.hd args) in if v.Tast.ty = Types.Dyn then expect ctx loc ~want (rt loc Types.Dyn (dyn_bits_sym name) [ v; here loc ]) @@ -11109,10 +11119,9 @@ and named_call ?(qualified = false) ctx ~want loc name args = | _ -> Tast.Rotr in arity ctx loc name 2 args; + bool_operands ctx name args; let a, b = - bool_first ctx name args (fun () -> - binary ctx ~dyn_ok:true ~join:false name loc - ~want:(numeric_want want) args) + binary ctx ~dyn_ok:true ~join:false name loc ~want:(numeric_want want) args in if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then begin (* The typed side of a mixed pair still has to be an integer: the dyn diff --git a/test/test_flan.ml b/test/test_flan.ml index a4990919..cc745989 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -1384,23 +1384,66 @@ let () = infers "a bit operation with a dyn operand is dyn" "(bit-and (the dyn 6) (i32 3))" "dyn"; infers "a shift with a dyn operand is dyn" "(<< (the dyn 1) 3)" "dyn"; - rejects_check "bit-and over bools points at and" + (* Through the whole-file check [flan check] and the editor run, which + records a refusal and goes on rather than raising at the first: a hint + that only a raised refusal could give would be lost there. [.fln] text is + checked from a file, since the operator's spelling follows the syntax. *) + let refuses_all name ?(fln = false) src needle = + let diags = + match + if fln then begin + let path = Test_support.tmp "bits-" (string_of_int (Hashtbl.hash name) ^ ".fln") in + Out_channel.with_open_bin path (fun oc -> output_string oc src); + let r = snd (Front.checked ~all:true path) in + Sys.remove path; r + end + else Check.program_all (Parse.program_all (read src)) + with + | _ -> [] + | exception Loc.Errors ds -> List.map (fun (d : Loc.diag) -> d.Loc.dmsg) ds + | exception Loc.Error d -> [ d.Loc.dmsg ] + in + if not (List.exists (fun m -> contains m needle) diags) then begin + incr failures; + Printf.printf "FAIL %s\n wanted: %s\n got: %s\n" name needle + (String.concat " | " diags) + end + in + refuses_all "bit-and over bools points at and" "(defn f [a bool b bool] bool (= (bit-and a b) 0))" - ~needle:"bit-and works on the bits of an integer, and this is a bool. \ - For true and false, write (and a b)"; - rejects_check "bit-not over a bool points at not" - "(defn f [a bool] i32 (bit-not a) 0)" ~needle:"write (not a)"; - rejects_check "bit-xor over bools points at !=" - "(defn f [a bool b bool] i32 (bit-xor a b) 0)" ~needle:"write (!= a b)"; - rejects_check "a typed bool beside a dyn is refused before it runs" - "(defn f [a bool d dyn] dyn (bit-or a d))" ~needle:"write (or a b)"; - rejects_check "a shift of a bool" - "(defn f [a bool] i32 (<< a 1) 0)" - ~needle:"combined with and, or and not"; - rejects_check "a bool beside an integer" - "(defn f [x i32 flag bool] i32 (bit-and x flag))" ~needle:"write (and a b)"; - rejects_check "a bool beside a literal" - "(defn f [flag bool] i32 (bit-or flag 1))" ~needle:"write (or a b)"; + "bit-and works on the bits of an integer, and this is a bool. \ + For true and false, write (and a b)"; + refuses_all "bit-not over a bool points at not" + "(defn f [a bool] i32 (bit-not a) 0)" "write (not a)"; + refuses_all "bit-xor over bools points at !=" + "(defn f [a bool b bool] i32 (bit-xor a b) 0)" "write (!= a b)"; + refuses_all "a typed bool beside a dyn is refused before it runs" + "(defn f [a bool d dyn] dyn (bit-or a d))" "write (or a b)"; + refuses_all "a shift of a bool" + "(defn f [a bool] i32 (<< a 1) 0)" "combined with and, or and not"; + refuses_all "a bool shift count" "(defn f [flag bool] i32 (<< 1 flag))" + "combined with and, or and not"; + refuses_all "a bool beside an integer, an integer wanted" + "(defn f [flag bool x i32] i32 (bit-and flag x))" "write (and a b)"; + refuses_all "a bool beside a literal, an integer wanted" + "(defn f [flag bool] i32 (bit-or flag 1))" "write (or a b)"; + refuses_all "a literal beside a bool" "(defn f [flag bool] i32 (bit-or 1 flag))" + "write (or a b)"; + refuses_all "a bool third" "(defn f [x i32 flag bool] i32 (bit-and x x flag))" + "write (and a b)"; + refuses_all "a comparison as an operand" + "(defn f [x i32 y i32] i32 (bit-and x (= x y)))" "write (and a b)"; + refuses_all "a bool field" "(defstruct S [on bool]) (defn f [s S] i32 (bit-or 1 (.on s)))" + "write (or a b)"; + refuses_all ~fln:true "&& in .fln, the bool first" + "fn f(flag: bool, x: i32) -> i32\n flag && x\n" "For true and false, write a and b"; + refuses_all ~fln:true "&& in .fln, a literal first" + "fn f(flag: bool) -> i32\n 1 && flag\n" "write a and b"; + refuses_all ~fln:true "&& in .fln, the bool last of three" + "fn f(flag: bool, x: i32) -> i32\n x && x && flag\n" "write a and b"; + refuses_all ~fln:true "~~ in .fln" "fn f(a: bool) -> i32\n ~~a\n" "~~ works on the bits"; + refuses_all ~fln:true "^^ in .fln" "fn f(a: bool, b: bool) -> bool\n a ^^ b == 0\n" + "write a != b"; rejects_check "popcount of a float" "(defn f [a f64] f64 (popcount a))" ~needle:"popcount takes integers, found f64"; rejects_check "a rotation's count does not widen the value" From c7fb0e54224d681566addea7cb3b28667e0f6f6f Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 11:14:09 +0700 Subject: [PATCH 07/16] Literal locals fed through do, let and if arms share one type without extra rounds, a chain past the rounds names the local to annotate, a restart's conversion fix shows the argument as written, and a name's lookup is answered from the last one under the same scope --- lib/check.ml | 121 ++++++++++++++++++++++++------ runtime/flan_rt.c | 8 +- test/programs/literal-locals.flan | 10 ++- test/programs/restarts.flan | 5 ++ test/test_acceptance.ml | 2 + test/test_flan.ml | 23 ++++++ 6 files changed, 143 insertions(+), 26 deletions(-) diff --git a/lib/check.ml b/lib/check.ml index 4f009b3d..84d38976 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -1020,7 +1020,21 @@ let bind ctx ?what ?lit name bty ~assignable = ctx.scope <- (name, { slot; bty; assignable; bwhat = what; blit = lit }) :: ctx.scope; slot -let lookup ctx name = List.assoc_opt name ctx.scope +(* The last answer for each name, with the scope it was read from. A body is + checked under one scope for long stretches and names the same local + several times per form, so compared by identity this answers most + lookups without walking a scope that, in a long [let], holds thousands of + names. *) +let lookup_cache : (string, (string * binding) list * binding option) Hashtbl.t = + Hashtbl.create 64 + +let lookup ctx name = + match Hashtbl.find_opt lookup_cache name with + | Some (sc, r) when sc == ctx.scope -> r + | _ -> + let r = List.assoc_opt name ctx.scope in + Hashtbl.replace lookup_cache name (ctx.scope, r); + r (* Capture, spec-memory.md's case 2: a body lifted into a function of its own — an [fn] literal or a handler clause — naming a local of the function it was @@ -6089,11 +6103,11 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = integer type, since the real one is answered again per copy. A local of the same name shadows it. *) | Ast.Var name - when (not (List.mem_assoc name ctx.scope)) - && (List.mem name ctx.env.lenvars - || (match List.assoc_opt name ctx.env.subst with - | Some (Types.Len _) -> true - | _ -> false)) -> + when (List.mem name ctx.env.lenvars + || (match List.assoc_opt name ctx.env.subst with + | Some (Types.Len _) -> true + | _ -> false)) + && lookup ctx name = None -> let n = match List.assoc_opt name ctx.env.subst with | Some (Types.Len n) -> n @@ -6425,7 +6439,16 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = let t = Printf.sprintf "%g" x in if String.exists (fun c -> c = '.' || c = 'e' || c = 'n' || c = 'i') t then t else t ^ ".0" - | _ -> spell_arg "x" a + | _ -> + (* The argument as written, when it is on one line of a file the + checker can read; otherwise an ellipsis. *) + let l = a.Ast.loc in + match Loc.source_line l with + | Some line + when l.Loc.macro = None && l.Loc.eline = l.Loc.line && l.Loc.col >= 1 + && l.Loc.ecol > l.Loc.col && l.Loc.ecol - 1 <= String.length line -> + String.sub line (l.Loc.col - 1) (l.Loc.ecol - l.Loc.col) + | _ -> spell_arg "\u{2026}" a in String.concat "\x1f" (sg :: (if fln_source loc then "i" else "p") :: List.map spell written) @@ -7420,19 +7443,37 @@ and lit_recorded ctx n = locals are merged with whatever [v] is stored into and the others are what it brings. *) and lit_parts ctx (v : Ast.expr) = - let rec go (v : Ast.expr) (vars, others) = + (* [env]: names a [let] inside [v] binds, with what they were bound to, so + (let [t a0] t) passes a0 through as (do a0) and an if's arms do. *) + let rec go env (v : Ast.expr) (vars, others) = match v.Ast.e with | Ast.Var n -> - (match lookup ctx n with - | Some { blit = Some k; _ } when lit_kind k <> Some `Box -> (k :: vars, others) - | _ -> (vars, v :: others)) + (match List.assoc_opt n env with + | Some (Some (vs, os)) -> (vs @ vars, os @ others) + | Some None -> (vars, v :: others) + | None -> + match lookup ctx n with + | Some { blit = Some k; _ } when lit_kind k <> Some `Box -> (k :: vars, others) + | _ -> (vars, v :: others)) | Ast.Int _ | Ast.Float _ | Ast.Byte _ -> (vars, others) | Ast.Call ({ Ast.e = Ast.Var ("+" | "-" | "*" | "/" | "%" | "min" | "max"); _ }, args) when args <> [] -> - List.fold_left (fun acc a -> go a acc) (vars, others) args + List.fold_left (fun acc a -> go env a acc) (vars, others) args + | Ast.Do (_ :: _ as xs) -> go env (List.nth xs (List.length xs - 1)) (vars, others) + | Ast.Let (bs, (_ :: _ as xs)) -> + let env = + List.fold_left + (fun env (b : Ast.binding) -> + (b.Ast.bname, + if b.Ast.bty = None then Some (go env b.Ast.bval ([], [])) else None) + :: env) + env bs + in + go env (List.nth xs (List.length xs - 1)) (vars, others) + | Ast.If (_, t, Some e) -> go env e (go env t (vars, others)) | _ -> (vars, v :: others) in - go v ([], []) + go [] v ([], []) (* A float literal, or an integer one past i32, anywhere in [v]'s arithmetic. *) and lit_wide_literals (v : Ast.expr) = @@ -7528,22 +7569,23 @@ and with_lits : 'a. ctx -> Loc.t -> Ast.expr list -> (unit -> 'a) -> 'a = if !lit_depth = 0 then Phys.reset lit_memo) @@ fun () -> let unsettled = Loc.diag ~kind:"check/lit-unsettled" loc "unsettled" in - (* The decisions the uses recorded so far make, and whether any moved. *) + (* The decisions the uses recorded so far make, written into + [s.decided], and the locals whose decision moved. *) let settle () = let solved = lit_solve s in - let moved = ref false in + let moved = ref [] in List.iter - (fun (k, _, r) -> + (fun (k, name, r) -> match r with | Ok t -> let kind = Option.value (lit_kind k) ~default:`Int in if not (Types.equal t (lit_guess ~subst:ctx.env.subst s k kind)) then begin - moved := true; + moved := (k, name, t) :: !moved; Phys.replace s.decided k t end | Error _ -> ()) solved; - (solved, !moved) + (solved, List.rev !moved) in let conflict solved = List.find_map @@ -7567,6 +7609,9 @@ and with_lits : 'a. ctx -> Loc.t -> Ast.expr list -> (unit -> 'a) -> 'a = incr lit_recording; (* A ref, because [trial] is monomorphic inside this recursive group. *) let answer = ref None in + (* What the settle inside the trial found, which has already written + its decisions: settling again outside would see nothing move. *) + let settled = ref None in let outcome = Fun.protect ~finally:(fun () -> decr lit_recording; s.recording <- false) @@ -7574,8 +7619,9 @@ and with_lits : 'a. ctx -> Loc.t -> Ast.expr list -> (unit -> 'a) -> 'a = trial ctx (fun () -> let r = run () in let solved, moved = settle () in + settled := Some (solved, moved); (* The guesses held: this check is the answer. *) - if moved || s.dirty || conflict solved <> None then + if moved <> [] || s.dirty || conflict solved <> None then raise (Loc.Error unsettled); answer := Some r; poison loc)) @@ -7583,11 +7629,19 @@ and with_lits : 'a. ctx -> Loc.t -> Ast.expr list -> (unit -> 'a) -> 'a = match outcome, !answer with | Ok _, Some r -> log (); remember (); r | _ -> - let solved, moved = settle () in + let solved, moved = + match !settled with Some sm -> sm | None -> settle () + in (match conflict solved with | Some (k, name, ((t1, l1), (t2, l2))) -> lit_conflict k name t1 l1 t2 l2 | None -> ()); - if moved && n < lit_rounds then round (n + 1) + if moved <> [] && n < lit_rounds then round (n + 1) + else if moved <> [] then begin + (* Still moving: a local fed through more calls than the rounds + follow. It is the local that needs its type written. *) + let k, name, t = List.hd moved in + lit_unsettled k name t + end else begin (* Nothing left to learn: checked for real, so a refusal is the ordinary one and a whole-file check goes on past it. *) @@ -7599,6 +7653,31 @@ and with_lits : 'a. ctx -> Loc.t -> Ast.expr list -> (unit -> 'a) -> 'a = round 1 end +(* A literal local whose uses kept changing its type past [lit_rounds]. *) +and lit_unsettled (k : Ast.expr) name t = + let lit = lit_spelling k in + let fix = + if fln_source k.Ast.loc then Printf.sprintf "let %s: %s = %s" name (tyname k.Ast.loc t) lit + else Printf.sprintf "(%s %s)" (tyname k.Ast.loc t) lit + in + Loc.failk "check/literal-unsettled" k.Ast.loc + "the type of %s depends on too long a chain of the values stored into it \ + to be read off them. Write the type it should have: %s" + name fix + +and lit_spelling (k : Ast.expr) = + let lit = + match k.Ast.e with + | Ast.Int n -> Int64.to_string n + | Ast.Float x -> Printf.sprintf "%g" x + | Ast.Byte b -> Printf.sprintf "\\%c" (Char.chr b) + | Ast.Call (_, [ { Ast.e = Ast.Int n; _ } ]) -> Int64.to_string (Int64.neg n) + | Ast.Call (_, [ { Ast.e = Ast.Float x; _ } ]) -> Printf.sprintf "%g" (-.x) + | _ -> "..." + in + let whole = lit <> "" && String.for_all (fun c -> (c >= '0' && c <= '9') || c = '-') lit in + if whole && lit_kind k = Some `Float then lit ^ ".0" else lit + (* Two uses of a literal local that no one type satisfies. *) and lit_conflict (k : Ast.expr) name t1 l1 t2 l2 = let lit = diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index c48f6f39..57c35edf 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -1193,10 +1193,14 @@ _Noreturn void flan_restart_args_fail(const uint8_t *loc, int64_t loclen, if (wrote < 0 || (size_t)wrote >= sizeof fix - used) { ok = 0; break; } used += (size_t)wrote; } + /* An argument the compiler could not spell is an ellipsis, and a fix with + * a hole in it is a conversion to make, not code to paste. */ + int holed = strstr(fix, "\xe2\x80\xa6") != NULL; if (ok && used > 0) - flan_say(loc, loclen, "restart %.*s takes %.*s, given %.*s. Write %s", + flan_say(loc, loclen, "restart %.*s takes %.*s, given %.*s. %s %s", (int)namelen, (const char *)name, (int)wantlen, (const char *)want, - (int)plen[0], (const char *)part[0], fix); + (int)plen[0], (const char *)part[0], + holed ? "Convert the argument with" : "Write", fix); else flan_say(loc, loclen, "restart %.*s takes %.*s, given %.*s", (int)namelen, (const char *)name, (int)wantlen, (const char *)want, (int)plen[0], diff --git a/test/programs/literal-locals.flan b/test/programs/literal-locals.flan index 69bef99a..71aefc48 100644 --- a/test/programs/literal-locals.flan +++ b/test/programs/literal-locals.flan @@ -30,10 +30,14 @@ (set a b) a)) -;; recur rebinds a loop's names the way set does. +;; Two locals fed from each other: i is counted against an i64, and acc +;; sums a literal past i32. (defn sum-to [n i64] i64 - (loop [i 0 acc 0] - (if (< i n) (recur (+ i 1) (+ acc 1000000000)) acc))) + (let [i 0 acc 0] + (while (< i n) + (set acc (+ acc 1000000000)) + (set i (+ i 1))) + acc)) ;; Inside a generic body the literal takes the type variable. (defn sum-of [xs [$t]] $t {:where (numeric? $t)} diff --git a/test/programs/restarts.flan b/test/programs/restarts.flan index b8adc4a5..cc799dc5 100644 --- a/test/programs/restarts.flan +++ b/test/programs/restarts.flan @@ -106,6 +106,10 @@ (handler-bind [(AssetMissing [c] (let [big (i64 7)] (invoke-restart 'use-value big)))] (supplied n))) +(defn doubled [n i32] i32 + (handler-bind [(AssetMissing [c] (let [big (i64 7)] (invoke-restart 'use-value (* big 2))))] + (supplied n))) + (defn main [args [str]] i32 ;; One argument selects a trap; none runs the table's case. (if (> (length args) 1) @@ -116,6 +120,7 @@ (= k 3) (print (overfull 92)) (= k 4) (print (mislaid 93)) (= k 5) (print (widened 94)) + (= k 6) (print (doubled 95)) :else (println "?")) (return 0))) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 311499d3..ea7a24e6 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -1402,6 +1402,8 @@ let () = "restart use-value takes (str), given (i32)"; refuses "a number of another type is refused with its conversion" "5" "restart use-value takes (i32), given (i64). Write (i32 big)"; + refuses "and the argument as it was written when it is an expression" "6" + "restart use-value takes (i32), given (i64). Write (i32 (* big 2))"; (try Sys.remove exe with Sys_error _ -> ()) in restart_mismatch (); diff --git a/test/test_flan.ml b/test/test_flan.ml index 4e7e2beb..e3b9fa9a 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -1105,6 +1105,29 @@ let () = ~needle:"1e-50 is too small for f32, which rounds it to 0" "(defn f [] f32 1e-50)"; infers "a literal past f32's range is an f64 like any other" "(+ 1.0 1e300)" "f64"; + (* Chains through a second round, and through do, let and if arms that + merge without one. *) + accepts "a literal local fed through do, however long the chain" + "(defn f [x i64] i64 (let [a0 0 a1 0 a2 0 a3 0 a4 0 a5 0 a6 0 a7 0 a8 0 a9 0 a10 0 \ + a11 0 a12 0] (set a0 x) (set a1 (do a0)) (set a2 (do a1)) (set a3 (do a2)) \ + (set a4 (do a3)) (set a5 (do a4)) (set a6 (do a5)) (set a7 (do a6)) \ + (set a8 (do a7)) (set a9 (do a8)) (set a10 (do a9)) (set a11 (do a10)) \ + (set a12 (do a11)) a12))"; + accepts "a literal local fed through a let" + "(defn f [x i64] i64 (let [a0 0 a1 0 a2 0] (set a0 x) (set a1 (let [t a0] t)) \ + (set a2 (let [t a1] t)) a2))"; + accepts "a literal local fed through both arms of an if" + "(defn f [x i64] i64 (let [a0 0 a1 0] (set a0 x) (set a1 (if true a0 a0)) a1))"; + accepts "a literal local fed through a generic call settles in rounds" + "(defn same [x $t] $t x) (defn f [x i64] i64 (let [a0 0 a1 0 a2 0] (set a0 x) \ + (set a1 (same a0)) (set a2 (same a1)) a2))"; + rejects_check "a chain the rounds cannot follow names the local to annotate" + ~needle:"the type of a7 depends on too long a chain of the values stored into \ + it to be read off them. Write the type it should have: (i64 0)" + "(defn same [x $t] $t x) (defn f [x i64] i64 (let [a0 0 a1 0 a2 0 a3 0 a4 0 a5 0 \ + a6 0 a7 0 a8 0 a9 0 a10 0] (set a0 x) (set a1 (same a0)) (set a2 (same a1)) \ + (set a3 (same a2)) (set a4 (same a3)) (set a5 (same a4)) (set a6 (same a5)) \ + (set a7 (same a6)) (set a8 (same a7)) (set a9 (same a8)) (set a10 (same a9)) a10))"; rejects_check "two uses of a literal local disagree" ~needle:"x is used as u32 and as i32, and 0 can have only one type. \ Write the one it should have: (u32 0)" From bcdb6250a97af245a2b8422bebdc1f57b31f0c18 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 11:46:32 +0700 Subject: [PATCH 08/16] A bit operator's operand of any shape that answers a bool gets the and, or or not hint, found by a check that is thrown away. --- lib/check.ml | 49 +++++++++++++++++++++++++++++++---------------- test/test_flan.ml | 12 ++++++++++++ 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/lib/check.ml b/lib/check.ml index bf61092b..27004a6e 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -606,6 +606,9 @@ let builtin_names : string list ref = ref [] twenty thousand calls. Filled beside the list. *) let builtin_set : (string, unit) Hashtbl.t = Hashtbl.create 128 +(* Set while [bool_operands] asks an operand its type; see there. *) +let probing = ref false + (* ── builtin/, the reserved qualifier ────────────────────────────────── [builtin/length] is the builtin [length], whatever else the program has decided [length] means. It is the way out of the dead end shadowing used to leave: a @@ -10264,28 +10267,40 @@ and bits_operand ctx loc name (v : Tast.expr) = (* A bool operand is refused before the operands are joined, and not left to [bits_operand]: the join sees a bool beside an integer as a plain mismatch, - "expected i32, found bool", which says nothing of [and]. Only the operands - that are plainly bools are asked about, because asking is a second check - and these are free of side effects: a comparison or a [not], a name, a - field of one. Any other bool still arrives at [bits_operand]. *) + "expected i32, found bool", which says nothing of [and]. Each operand's own + type is asked in a trial that is always abandoned, so the check leaves no + trace — no slot, no lifted lambda, no recorded refusal — and the real check + below is the only one that counts. A literal is never a bool, and a call to + an arithmetic or bit operator answers a number or a dyn, so neither is + asked. Nor is anything asked while a probe is running: the probe wants a + type, and asking again inside it would check a nest of these once per + level for every level above it, which doubles with each level. *) and bool_operands ctx name (args : Ast.expr list) = - let rec plain (a : Ast.expr) = + if not !probing then + let never_bool (a : Ast.expr) = match a.Ast.e with - | Ast.Var _ -> true - | Ast.Field (t, _) -> plain t + | Ast.Int _ | Ast.UInt _ | Ast.Float _ | Ast.Byte _ | Ast.Str _ | Ast.Kw _ -> + true + | Ast.Call ({ Ast.e = Ast.Var h; _ }, _) -> + List.mem h + [ "+"; "-"; "*"; "/"; "%"; "bit-and"; "bit-or"; "bit-xor"; "bit-not"; + "&&"; "||"; "^^"; "~~"; "<<"; ">>"; "rotate-left"; "rotate-right"; + "popcount"; "leading-zeros"; "trailing-zeros" ] + && not (shadows_builtin ctx a.Ast.loc h) | _ -> false in let is_bool (a : Ast.expr) = - match a.Ast.e with - | Ast.Call ({ Ast.e = Ast.Var h; _ }, _) - when List.mem h [ "="; "!="; "<"; "<="; ">"; ">="; "not" ] - && not (shadows_builtin ctx a.Ast.loc h) -> - true - | _ when plain a -> - (match speculate ctx.env (fun () -> check ctx a) with - | v -> v.Tast.ty = Types.Bool - | exception Loc.Error _ -> false) - | _ -> false + (not (never_bool a)) + && + let ty = ref None in + probing := true; + Fun.protect ~finally:(fun () -> probing := false) (fun () -> + ignore + (trial ctx (fun () -> + let v = check ctx a in + ty := Some v.Tast.ty; + Loc.failk "check/probe" a.Ast.loc "abandoned"))); + !ty = Some Types.Bool in List.iter (fun a -> if is_bool a then bool_bits a.Ast.loc name) args diff --git a/test/test_flan.ml b/test/test_flan.ml index cc745989..cc6485b4 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -1435,6 +1435,18 @@ let () = "(defn f [x i32 y i32] i32 (bit-and x (= x y)))" "write (and a b)"; refuses_all "a bool field" "(defstruct S [on bool]) (defn f [s S] i32 (bit-or 1 (.on s)))" "write (or a b)"; + refuses_all "a call that answers a bool" + "(defn p? [x i32] bool (> x 0)) (defn f [x i32] i32 (bit-and x (p? x)))" + "write (and a b)"; + refuses_all "an if whose type is bool" + "(defn f [x i32 c bool] i32 (bit-or 1 (if c true false)))" "write (or a b)"; + refuses_all "a dyn function's typed bool result" + "(defn p? [x] bool (> x 0)) (defn f [d dyn] i32 (<< 1 (p? d)))" + "combined with and, or and not"; + refuses_all "a bool inside a nest of bit operations" + "(defn p? [x i32] bool (> x 0)) \ + (defn f [x i32] i32 (bit-and x (bit-or x (bit-xor x (p? x)))))" + "write (!= a b)"; refuses_all ~fln:true "&& in .fln, the bool first" "fn f(flag: bool, x: i32) -> i32\n flag && x\n" "For true and false, write a and b"; refuses_all ~fln:true "&& in .fln, a literal first" From 6d8ced2ba33ebfc2a7671103ef62bc92a6e87c48 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 11:48:01 +0700 Subject: [PATCH 09/16] A dyn text becomes a str as its own bytes, a dyn view becomes its own storage, and a dyn vec or map becomes a checked copy in a [const T], a fixed array or a struct, while a writable [T] is never a copy. --- TODO.org | 6 + lib/check.ml | 59 +++- lib/emit.ml | 1 + runtime/flan_dyn.c | 446 +++++++++++++++++++++++++++++- runtime/flan_dyn.h | 12 + runtime/flan_rt.c | 34 +++ test/dyn_ops.c | 26 ++ test/programs/dyn-into-typed.flan | 115 ++++++++ test/test_acceptance.ml | 61 ++++ test/test_flan.ml | 13 +- 10 files changed, 767 insertions(+), 6 deletions(-) create mode 100644 test/programs/dyn-into-typed.flan diff --git a/TODO.org b/TODO.org index 6a35158f..fe0c4a6b 100644 --- a/TODO.org +++ b/TODO.org @@ -28,6 +28,12 @@ class's float slot's rule), and a u64 above the largest i64 traps when read. A v storage its own dyn global's initialiser built is refused. Rules out copying at the crossing, a dyn big int for u64, and any check in a release build. +** DONE A dyn value crosses into a str, a slice, an array or a struct +CLOSED: [2026-09-26] +A writable [T] is never a copy: a plain dyn vec into one traps and names [const T], so a +write can never miss the vec. A str from a text is its own bytes, live at least until the +next free-temp. Rules out copy-in/copy-out at a call, and rooting the text in the crossing's frame. + ** NEXT Dyn unless annotated Decided 2026-09-26, replacing the plain rule: number, bool and char literals are typed, their type inferred from their uses inside the function (never across functions); an diff --git a/lib/check.ml b/lib/check.ml index aa7423a1..c649db42 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -3639,8 +3639,11 @@ let no_dyn_yet loc ~into t extra = dyn value cannot be read out of or written into without a meaning nobody has decided. A str is read as a copy and never written, since a dyn text is a collector pointer and typed storage is never scanned; a - [const] slice is refused because a dyn view can be written through. *) -let rec view_desc structs (t : Types.t) : (string, Types.t) result = + [const] slice is refused because a dyn view can be written through. One + is described only [~into] a written type ([into_typed]), as [c]. *) +let rec view_desc ?(into = false) structs (t : Types.t) + : (string, Types.t) result = + let view_desc = view_desc ~into in let ( let* ) = Result.bind in match t with | Types.Int k -> @@ -3655,6 +3658,8 @@ let rec view_desc structs (t : Types.t) : (string, Types.t) result = let* d = view_desc structs e in Ok (Printf.sprintf "a%Ld;%s" n d) | Types.Slice (Types.Mut, e) -> let* d = view_desc structs e in Ok ("s" ^ d) + | Types.Slice (Types.Const, e) when into -> + let* d = view_desc structs e in Ok ("c" ^ d) | Types.Vec e -> let* d = view_desc structs e in Ok ("v" ^ d) | Types.Named n -> (match Hashtbl.find_opt structs n with @@ -4215,6 +4220,48 @@ let unbox loc (want : Types.t) (e : Tast.expr) : Tast.expr = then "f64" else "i64")) | _ -> no_dyn_yet loc ~into:false want "" +(* A dyn where a str, a slice, a fixed array or a struct was written: + [flan_dyn_need_as] with the written type's descriptor, answering through + a slot of that type. The runtime decides, since only it can see what the + dyn holds: a text is a str's own bytes, a view of the wanted elements is + their own storage, a vec or a map is a checked copy, and a [T] that can be + written through is never a copy — a write through one would not reach the + dyn vec, so the answer would differ with the vec typed or dyn. Its comment + in runtime/flan_dyn.c says where the copies live and how long a str from a + text lasts. + + A fixed array or a struct that holds a Vec is refused: from a view it + would be a second copy of an owning header. *) +let into_typed ctx loc (want : Types.t) (got : Tast.expr) : Tast.expr = + let structs = ctx.env.structs in + let rec owns (t : Types.t) = + match t with + | Types.Vec _ -> true + | Types.Array (_, e) -> owns e + | Types.Named n -> + (match Hashtbl.find_opt structs n with + | Some st -> List.exists (fun (f : Tast.field) -> owns f.Tast.fty) st.Tast.fields + | None -> false) + | _ -> false + in + match view_desc ~into:true structs want with + | Error inner -> + no_dyn_yet loc ~into:false want + (if Types.equal inner want then "" + else Printf.sprintf " — a dyn value has no %s to become" (tyname loc inner)) + | Ok _ when owns want -> + no_dyn_yet loc ~into:false want " — it holds a Vec, which owns its storage" + | Ok d -> + let ds = fresh_slot ctx Types.Dyn and out = fresh_slot ctx want in + let local s ty = mk loc ty (Tast.Local s) in + mk loc want + (Tast.Let + ([ (ds, got); (out, mk loc want (Tast.Zero want)) ], + [ rt loc Types.Unit "flan_dyn_need_as" + [ local ds Types.Dyn; mk loc Types.String (Tast.Str d); + addr_of loc (local out want); here loc ]; + local out want ])) + (* ── A numeric cast written on a dyn ───────────────────────────────── * TODO.org, "A numeric cast opens a dyn box". @@ -4581,6 +4628,14 @@ let expect ctx loc ~want (got : Tast.expr) = or to dyn itself; wrap the type in Option, or keep the value dyn" (tyname loc w) | _, Types.Dyn when Types.fits ~expected:w ~actual:Types.Dyn -> got + | (Types.String | Types.Slice _ | Types.Array _), Types.Dyn -> + let opened = into_typed ctx loc w got in + Opened.replace opened_by_want opened got; + opened + | Types.Named n, Types.Dyn when Hashtbl.mem ctx.env.structs n -> + let opened = into_typed ctx loc w got in + Opened.replace opened_by_want opened got; + opened | _, Types.Dyn -> let opened = unbox loc w got in Opened.replace opened_by_want opened got; diff --git a/lib/emit.ml b/lib/emit.ml index 97240c6e..a40ef2f2 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -5079,6 +5079,7 @@ declare i64 @flan_dyn_need_not_nil(i64) declare i32 @flan_dyn_truthy(i64) declare i64 @flan_dyn_view_slice(ptr, i64, ptr, i64, i32) declare i64 @flan_dyn_view_at(ptr, i64, ptr, i64, i32, i32) +declare void @flan_dyn_need_as(i64, ptr, i64, ptr, ptr, i64) declare void @flan_dyn_root_push(ptr) declare void @flan_dyn_root_push_desc(ptr, ptr) declare ptr @flan_dyn_env_new(i64, ptr) diff --git a/runtime/flan_dyn.c b/runtime/flan_dyn.c index 00dced35..8807deb9 100644 --- a/runtime/flan_dyn.c +++ b/runtime/flan_dyn.c @@ -1525,9 +1525,71 @@ static void mark_desc(char *base, const flan_desc *d) { } } +/* ── Texts a str was taken from ──────────────────────────────────────── + * + * A dyn text crossing into a str ([flan_dyn_need_as]) is not copied: the str + * is the text's own bytes, which never move, since this collector never + * moves anything. What could happen is a free — a str in a typed local, + * field or Vec is not a root, and typed storage is never scanned — so the + * crossing pins the text here, keyed by the temp arena's stamp, and every + * collection marks the pins whose stamp is still live. The str then lasts + * as long as the text is reachable from dyn or until the next free-temp, + * whichever is later: the lifetime i64->bytes's text already has, and text + * kept longer is cloned, as there. + * + * Rooting the text in the crossing's frame would be shorter than that and + * wrong for a str returned or stored, and a pin for good would keep every + * text a frame loop ever crossed. A copy into the temp arena would be sound + * too; this is the same lifetime without the copy. */ +const void *flan_temp_stamp(uint64_t *inc, uint64_t *epoch); +int32_t flan_temp_stamp_live(const void *a, uint64_t inc, uint64_t epoch); + +typedef struct text_pin { + flan_obj *o; + const void *a; + uint64_t inc, epoch; +} text_pin; + +static text_pin *pins; +static int64_t pins_n, pins_cap; + +static void pins_prune(void) { + int64_t i, k = 0; + for (i = 0; i < pins_n; i++) + if (flan_temp_stamp_live(pins[i].a, pins[i].inc, pins[i].epoch)) + pins[k++] = pins[i]; + pins_n = k; +} + +static void pin_text(flan_obj *o) { + uint64_t inc, epoch; + const void *a = flan_temp_stamp(&inc, &epoch); + text_pin *last = pins_n > 0 ? &pins[pins_n - 1] : NULL; + if (last != NULL && last->o == o && last->a == a && last->inc == inc + && last->epoch == epoch) + return; + if (pins_n == pins_cap) { + pins_prune(); + if (pins_n == pins_cap) { + int64_t cap = pins_cap ? pins_cap * 2 : 64; + text_pin *p = (text_pin *)realloc(pins, (size_t)cap * sizeof *p); + if (p == NULL) trap_oom(NULL, 0, cap * (int64_t)sizeof *p); + pins = p; + pins_cap = cap; + } + } + pins[pins_n].o = o; + pins[pins_n].a = a; + pins[pins_n].inc = inc; + pins[pins_n].epoch = epoch; + pins_n++; +} + static void gc_mark_all(void) { int64_t i; unsigned k; + pins_prune(); + for (i = 0; i < pins_n; i++) mark_push(pins[i].o); for (i = 0; i < roots_n; i++) { const flan_desc *d = roots[i].desc; if (d == NULL) mark_value(*(flan_dyn *)roots[i].base); @@ -3106,6 +3168,9 @@ static inline int is_map(flan_dyn v) { * t str (read as a copy; never written from here) * a;T a fixed [n T] * sT a slice [T] + * cT a [const T]: only in what a crossing *into* a + * written type wants ([flan_dyn_need_as]); no view + * is ever of one * vT a (Vec T) * {Name;f1;T1f2;T2} a struct, its fields in declaration order * @@ -3136,7 +3201,7 @@ static const uint8_t *desc_name_end(const uint8_t *d) { static const uint8_t *desc_skip(const uint8_t *d) { switch (*d) { case 'a': d++; desc_int(&d); return desc_skip(d); - case 's': case 'v': return desc_skip(d + 1); + case 's': case 'c': case 'v': return desc_skip(d + 1); case '{': d = desc_name_end(d + 1); while (*d != '}') d = desc_skip(desc_name_end(d)); @@ -3152,7 +3217,7 @@ static void desc_lay(const uint8_t *d, int64_t *size, int64_t *align) { case 'b': case 'B': case '?': *size = 1; *align = 1; return; case 'h': case 'H': *size = 2; *align = 2; return; case 'i': case 'I': case 'f': *size = 4; *align = 4; return; - case 't': case 's': *size = 16; *align = 8; return; + case 't': case 's': case 'c': *size = 16; *align = 8; return; case 'v': *size = 40; *align = 8; return; case 'a': { int64_t n, s, a; @@ -3271,6 +3336,10 @@ static void desc_spell(const uint8_t *d, char *buf, size_t cap) { desc_spell(d + 1, inner, sizeof inner); snprintf(buf, cap, "[%s]", inner); return; + case 'c': + desc_spell(d + 1, inner, sizeof inner); + snprintf(buf, cap, "[const %s]", inner); + return; case 'v': desc_spell(d + 1, inner, sizeof inner); snprintf(buf, cap, "(Vec %s)", inner); @@ -3916,6 +3985,379 @@ flan_dyn flan_dyn_view_flat(void *data, int64_t len, int32_t elem) { return view_make(data, len, old_elem_desc(elem), VIEW_FLAT, 0); } +/* ── Dyn into a written type ─────────────────────────────────────────── + * + * The reverse of a view: a dyn value reaching typed code that wrote a str, a + * slice, a fixed array or a struct (lib/check.ml, [into_typed]). [want] is + * the written type's descriptor, the prefix code above, with [c] for a + * [const T]; [out] is where the typed value goes — a str's or slice's two + * words, or the array's or struct's bytes. Four answers, in order: + * + * a text its own bytes, for a str or a [const u8], pinned + * ([pin_text]) and never copied; + * a view of typed storage whose element type is the one wanted: that + * storage, checked for staleness in a dev build, never copied; + * a vec, map copied and unboxed element by element, each checked, for a + * [const T], a fixed array or a struct — a [const T]'s block + * in the temp arena ([flan_temp_block]); + * anything else traps, naming the element and what it is. + * + * A [T] that can be written through is never a copy. A write through a copy + * would not reach the dyn vec, and the same program would then answer + * differently with the vec typed or dyn; so a plain dyn vec, or a view of + * other elements, into a [T] traps and names [const T]. A fixed array and a + * struct are values, copied on the typed side as well, so a copy there + * changes nothing. */ + +void *flan_temp_block(int64_t bytes, int64_t align, int64_t elem, + const char *type, int64_t typelen); + +typedef struct into_site { + const uint8_t *loc; + int64_t loclen; + char op[140]; /* "into [const i64]" */ + char where[192]; /* "element 2", "field :x of element 2"; "" */ +} into_site; + +static _Noreturn void into_trap(into_site *s, const char *trap, + const char *fmt, ...) { + char msg[640]; + va_list ap; + va_start(ap, fmt); + vsnprintf(msg, sizeof msg, fmt, ap); + va_end(ap); + flan_say(s->loc, s->loclen, "dyn %s: %s", s->op, msg); + dyn_trap((const uint8_t *)trap, (int64_t)strlen(trap)); +} + +static const char *into_who(into_site *s) { + return s->where[0] ? s->where : "this"; +} + +/* "element 2 is a text, "a", and an i64 is wanted there". A view is checked + * for staleness before it is rendered, since rendering reads it. */ +static _Noreturn void into_wrong(into_site *s, flan_dyn x, const char *why) { + char sx[SAY_MAX]; + const char *t = tag_of(x); + if (dyn_boxed(x) && dyn_box(x) == BOX_OBJ && dyn_obj(x) != NULL + && dyn_obj(x)->kind == OBJ_VIEW) + view_guard_check(s->loc, s->loclen, s->op, dyn_obj(x)); + if (flan_dyn_tag(x) == FLAN_DYN_TAG_NIL) + into_trap(s, "DynType", "%s is nil, and %s", into_who(s), why); + say(sx, SAY_MAX, x); + into_trap(s, "DynType", "%s is %s %s, %s, and %s", into_who(s), an(t), t, + sx, why); +} + +/* "an i64 is wanted there". */ +static void into_wanted(char *buf, size_t cap, const uint8_t *d) { + char ty[128]; + desc_spell(d, ty, sizeof ty); + snprintf(buf, cap, "%s %s is wanted there", an(ty), ty); +} + +/* Whether a view's descriptor [a] starts with the whole of the code [b]. + * The code is prefix-free, so a view's (inside a longer one) matches exactly + * when these bytes do. */ +static int desc_same(const uint8_t *a, const uint8_t *b) { + size_t n = (size_t)(desc_skip(b) - b); + return memcmp(a, b, n) == 0; +} + +/* A vec's length and element [i], a plain one's word or a view's element + * boxed. [o] is a vec-tagged object whose guard has been checked. */ +static int64_t into_len(into_site *s, flan_obj *o) { + return o->kind == OBJ_VIEW ? view_len(s->loc, s->loclen, s->op, o) : o->len; +} + +static flan_dyn into_at(into_site *s, flan_obj *o, int64_t i) { + if (o->kind == OBJ_VIEW) + return view_read(s->loc, s->loclen, s->op, o, o->u.view.desc, + view_elem_at(o, i)); + return o->u.v.items[i]; +} + +/* A view object, or NULL; checked for staleness when it is one. */ +static flan_obj *into_view(into_site *s, flan_dyn x) { + flan_obj *o; + if (!dyn_boxed(x) || dyn_box(x) != BOX_OBJ) return NULL; + o = dyn_obj(x); + if (o == NULL || o->kind != OBJ_VIEW) return NULL; + view_guard_check(s->loc, s->loclen, s->op, o); + return o; +} + +/* Steps [s->where] into a part of what it names; [into_leave] steps back. */ +static void into_enter(into_site *s, const char *fmt, ...) { + char part[96], rest[192]; + va_list ap; + va_start(ap, fmt); + vsnprintf(part, sizeof part, fmt, ap); + va_end(ap); + memcpy(rest, s->where, sizeof rest); + if (rest[0] == '\0') snprintf(s->where, sizeof s->where, "%s", part); + else snprintf(s->where, sizeof s->where, "%s of %s", part, rest); +} + +static void into_leave(into_site *s, const char *saved) { + memcpy(s->where, saved, sizeof s->where); +} + +static void into_put(into_site *s, const uint8_t *d, flan_dyn x, uint8_t *p); +static void into_slice(into_site *s, const uint8_t *d, flan_dyn x, + uint8_t *p); + +/* A text's bytes as a str's two words, pinned. */ +static void into_text(flan_dyn x, uint8_t *p) { + flan_obj *o = dyn_obj(x); + const uint8_t *b = obj_text_bytes(o); + pin_text(o); + memcpy(p, &b, 8); + memcpy(p + 8, &o->len, 8); +} + +/* [n] elements of [e] from the vec-tagged [o] into [p]. */ +static void into_elems(into_site *s, const uint8_t *e, flan_obj *o, int64_t n, + uint8_t *p) { + int64_t i, sz = desc_size(e); + char saved[192]; + memcpy(saved, s->where, sizeof saved); + for (i = 0; i < n; i++) { + flan_dyn x; + into_enter(s, "element %lld", (long long)i); + x = into_at(s, o, i); + into_put(s, e, x, p + i * sz); + into_leave(s, saved); + } +} + +static void into_put(into_site *s, const uint8_t *d, flan_dyn x, uint8_t *p) { + char why[256], ty[128]; + int64_t lo, hi; + if (int_range(*d, &lo, &hi)) { + int64_t n; + if (flan_dyn_tag(x) != FLAN_DYN_TAG_INT) { + into_wanted(why, sizeof why, d); + into_wrong(s, x, why); + } + n = dyn_int_value(x); + if (n < lo || n > hi) { + desc_spell(d, ty, sizeof ty); + if (*d == 'L') + into_trap(s, "DynRange", "%s is %lld, and a u64 holds no negative " + "number", into_who(s), (long long)n); + into_trap(s, "DynRange", "%s is %lld, and %s %s holds %lld to %lld", + into_who(s), (long long)n, an(ty), ty, (long long)lo, + (long long)hi); + } + switch (*d) { + case 'b': case 'B': { uint8_t b = (uint8_t)n; memcpy(p, &b, 1); return; } + case 'h': case 'H': { uint16_t h = (uint16_t)n; memcpy(p, &h, 2); return; } + case 'i': case 'I': { uint32_t w = (uint32_t)n; memcpy(p, &w, 4); return; } + default: memcpy(p, &n, 8); return; + } + } + switch (*d) { + case 'f': case 'd': { + double f = 0; + /* An int goes into a float when the float holds it exactly: a view's + element write and a class's float slot take the same rule. */ + if (flan_dyn_tag(x) == FLAN_DYN_TAG_INT) { + int64_t n = dyn_int_value(x); + f = *d == 'f' ? (double)(float)n : (double)n; + if (!(f >= -9223372036854775808.0 && f < 9223372036854775808.0) + || (int64_t)f != n) + into_trap(s, "DynRange", "%s is %lld, which has no exact %s. Write " + "it as a float, as in %lld.0", into_who(s), (long long)n, + *d == 'f' ? "f32" : "f64", (long long)n); + } else if (flan_dyn_tag(x) == FLAN_DYN_TAG_FLOAT) + f = dyn_num_value(x); + else { + into_wanted(why, sizeof why, d); + into_wrong(s, x, why); + } + if (*d == 'f') { float g = (float)f; memcpy(p, &g, 4); } + else memcpy(p, &f, 8); + return; + } + case '?': + if (flan_dyn_tag(x) != FLAN_DYN_TAG_BOOL) { + into_wanted(why, sizeof why, d); + into_wrong(s, x, why); + } + *p = dyn_payload(x) ? 1 : 0; + return; + case 't': + if (!is_text(x)) + into_wrong(s, x, "a str is wanted there, which only a text becomes"); + into_text(x, p); + return; + case 'a': { + const uint8_t *e = d + 1; + int64_t n = desc_int(&e), len; + flan_obj *o; + if (!is_vec(x)) { + desc_spell(d, ty, sizeof ty); + snprintf(why, sizeof why, "%s %s is made from a vec", an(ty), ty); + into_wrong(s, x, why); + } + o = into_view(s, x); + if (o == NULL) o = dyn_obj(x); + len = into_len(s, o); + if (len != n) { + desc_spell(d, ty, sizeof ty); + into_trap(s, "DynRange", "%s has %lld element%s, and %s %s holds " + "exactly %lld", s->where[0] ? s->where : "this vec", + (long long)len, len == 1 ? "" : "s", an(ty), ty, + (long long)n); + } + /* A view of the same elements is the same bytes: an array is a value, + copied on the typed side too. */ + if (o->kind == OBJ_VIEW && desc_same(o->u.view.desc, e)) { + if (n > 0) memcpy(p, view_base(o), (size_t)(n * desc_size(e))); + return; + } + into_elems(s, e, o, n, p); + return; + } + case '{': { + const uint8_t *at, *name, *fty; + int64_t off, namelen, foff; + flan_obj *o; + char saved[192]; + if (!is_map(x)) { + desc_spell(d, ty, sizeof ty); + snprintf(why, sizeof why, "%s %s is made from a map", an(ty), ty); + into_wrong(s, x, why); + } + o = into_view(s, x); + if (o != NULL && desc_same(o->u.view.desc, d)) { + memcpy(p, o->u.view.base, (size_t)desc_size(d)); + return; + } + memcpy(saved, s->where, sizeof saved); + at = desc_fields(d, &off); + while (desc_next(&at, &off, &name, &namelen, &foff, &fty)) { + flan_dyn k = flan_dyn_kw(name, namelen), v; + if (!flan_dyn_truthy(flan_dyn_map_contains_at(x, k, s->loc, + s->loclen))) { + desc_spell(d, ty, sizeof ty); + into_trap(s, "DynType", "%s has no :%.*s, and %s %s needs every " + "field", s->where[0] ? s->where : "this map", + (int)namelen, (const char *)name, an(ty), ty); + } + v = flan_dyn_get(x, k, s->loc, s->loclen); + into_enter(s, "field :%.*s", (int)namelen, (const char *)name); + into_put(s, fty, v, p + foff); + into_leave(s, saved); + } + return; + } + case 's': case 'c': + into_slice(s, d, x, p); + return; + default: + desc_spell(d, ty, sizeof ty); + snprintf(why, sizeof why, "%s %s is not made from a dyn value", an(ty), ty); + into_wrong(s, x, why); + } +} + +/* The copy a [const T] reads, of a plain vec or a view of other elements. */ +static void into_copy(into_site *s, const uint8_t *e, flan_obj *o, + uint8_t *out) { + static const char scalars[] = "bBhHiIlLfd?t"; + static const char *const words[] = { "i8", "u8", "i16", "u16", "i32", "u32", + "i64", "u64", "f32", "f64", "bool", + "str" }; + const char *w = *e != '\0' ? strchr(scalars, *e) : NULL; + /* The registry keeps the name by pointer, so it is static text. */ + const char *type = w != NULL ? words[w - scalars] : "element"; + int64_t n = into_len(s, o), size, align; + uint8_t *block = NULL; + desc_lay(e, &size, &align); + if (n > 0 && size > 0) { + block = (uint8_t *)flan_temp_block(n * size, align, size, type, + (int64_t)strlen(type)); + if (block == NULL) trap_oom(s->loc, s->loclen, n * size); + memset(block, 0, (size_t)(n * size)); + into_elems(s, e, o, n, block); + } + memcpy(out, &block, 8); + memcpy(out + 8, &n, 8); +} + +/* A [T] or a [const T], at the top or inside an array or a struct. A text + * is a [const u8]'s bytes, a view of the very elements wanted is its own + * storage, and a [const T] of anything else vec-shaped is a copy; a [T] + * never is, since a write through it would not reach the vec. */ +static void into_slice(into_site *s, const uint8_t *d, flan_dyn x, + uint8_t *p) { + const uint8_t *e = d + 1; + int mut = *d == 's'; + const char *who = s->where[0] ? s->where : "this vec"; + char ty[128], ety[128], why[256]; + flan_obj *o; + desc_spell(d, ty, sizeof ty); + desc_spell(e, ety, sizeof ety); + if (is_text(x) && *e == 'B') { + if (mut) + into_wrong(s, x, "a text is read-only, so it becomes a str or a " + "[const u8] and never a [u8]"); + into_text(x, p); + return; + } + if (!is_vec(x)) { + snprintf(why, sizeof why, "only a vec becomes %s %s", an(ty), ty); + into_wrong(s, x, why); + } + o = into_view(s, x); + if (o != NULL && desc_same(o->u.view.desc, e)) { + void *b = view_base(o); + int64_t n = view_len(s->loc, s->loclen, s->op, o); + memcpy(p, &b, 8); + memcpy(p + 8, &n, 8); + return; + } + if (mut) { + char have[128]; + if (o != NULL) { + desc_spell(o->u.view.desc, have, sizeof have); + into_trap(s, "DynType", "%s is a view of %s elements, so %s %s of it " + "would be a copy, and a write through the copy would never " + "reach the vec. Take it as [const %s], which reads a copy", + who, have, an(ty), ty, ety); + } + into_trap(s, "DynType", "%s is a dyn vec, so %s %s of it would be a " + "copy, and a write through the copy would never reach the vec. " + "Take it as [const %s], which reads a copy", who, an(ty), ty, + ety); + } + into_copy(s, e, o != NULL ? o : dyn_obj(x), p); +} + +void flan_dyn_need_as(flan_dyn v, const uint8_t *want, int64_t wantlen, + void *out, const uint8_t *loc, int64_t loclen) { + into_site s; + char ty[128]; + uint8_t *p = (uint8_t *)out; + (void)wantlen; + s.loc = loc; + s.loclen = loclen; + s.where[0] = '\0'; + desc_spell(want, ty, sizeof ty); + snprintf(s.op, sizeof s.op, "into %s", ty); + switch (*want) { + case 't': + if (!is_text(v)) into_wrong(&s, v, "only a text becomes a str"); + into_text(v, p); + return; + default: + into_put(&s, want, v, p); + return; + } +} + static flan_dyn len_walk(flan_dyn v) { if (is_text(v)) return flan_dyn_from_i64(dyn_obj(v)->len); /* A map's length is its slot count, so a stale instance would answer the diff --git a/runtime/flan_dyn.h b/runtime/flan_dyn.h index 01a29f12..b18c353f 100644 --- a/runtime/flan_dyn.h +++ b/runtime/flan_dyn.h @@ -348,6 +348,18 @@ flan_dyn flan_dyn_view_slice(void *data, int64_t len, const uint8_t *desc, flan_dyn flan_dyn_view_at(void *addr, int64_t len, const uint8_t *desc, int64_t desclen, int32_t shape, int32_t here); +/* The other direction: a dyn value where typed code wrote a str, a [T], a + * [const T], a fixed [n T] or a struct. [want] is that type's descriptor + * (the code above, and 'c' before a [const T]'s element); [out] receives the + * str's or slice's two words, or the array's or struct's bytes. A text + * becomes a str or a [const u8] as its own bytes, kept from the collector + * until the temp arena's next free-all; a view of the wanted elements + * becomes its own storage; a vec or a map becomes a checked copy — for a + * [const T] in the temp arena — and a [T] is never a copy. Anything else + * traps at [loc], naming the element and what it holds. */ +void flan_dyn_need_as(flan_dyn v, const uint8_t *want, int64_t wantlen, + void *out, const uint8_t *loc, int64_t loclen); + /* print, =, length and has-key? with the site they were written at: a view * that traps inside one names it. */ void flan_dyn_print_at(flan_dyn v, const uint8_t *loc, int64_t loclen); diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index b841fec7..59cd0d2a 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -2593,6 +2593,40 @@ int8_t flan_f64_temp(double x, flan_slice *out) { return flan_temp_text(render_f64, &x, out); } +/* For flan_dyn.c's crossing of a dyn value into a written type + * ([flan_dyn_need_as]). A dyn vec copied into a [const T] or a text's bytes + * lent to a str live exactly as long as i64->bytes's text does: until the + * temp arena's next free-all. [flan_temp_block] is the copy's block, noted + * in a dev build's registry like any temp slice so a read after free-temp + * traps or reads poison; NULL only when malloc itself failed. The stamp is + * the arena and the incarnation and epoch it is at now, and a stamp is live + * while that arena is still at both — a free-temp, the agent's wipe, the end + * of a scratch evaluation or a destroy ends it. An arena record is never + * freed (see [flan_retired]), so an old stamp is always safe to read. */ +void *flan_temp_block(int64_t bytes, int64_t align, int64_t elem, + const char *type, int64_t typelen) { + flan_allocator *a = flan_context_temp(); + void *q; + if (a == NULL || bytes <= 0) return NULL; + q = a->proc(a, FLAN_ALLOC_ALLOC, NULL, 0, bytes, align); + if (q != NULL) flan_dev_reg_note_sliced(q, bytes, elem, type, typelen, a); + return q; +} + +const void *flan_temp_stamp(uint64_t *inc, uint64_t *epoch) { + flan_allocator *a = flan_context_temp(); + *inc = a ? a->incarnation : 0; + *epoch = a ? a->epoch : 0; + return a; +} + +int32_t flan_temp_stamp_live(const void *p, uint64_t inc, uint64_t epoch) { + const flan_allocator *a = (const flan_allocator *)p; + /* No temp arena could be made: the pin is kept for good. */ + if (a == NULL) return 1; + return a->incarnation == inc && a->epoch == epoch; +} + /* The allocator's identity, for the condition's :allocator field. The pointer * is the identity — the same thing the epoch hangs off. */ int64_t flan_alloc_id(flan_allocator *a) { return (int64_t)(intptr_t)a; } diff --git a/test/dyn_ops.c b/test/dyn_ops.c index 7a0c1a33..04c8efd5 100644 --- a/test/dyn_ops.c +++ b/test/dyn_ops.c @@ -1386,6 +1386,31 @@ static void walk_hook(const uint8_t *name, int64_t namelen) { longjmp(walk_out, 1); } +/* flan_dyn_need_as: a text is a str's own bytes, a view of the same + * elements its own storage, and a plain vec a checked copy. */ +static void into(void) { + static int64_t a[3] = { 1, 2, 3 }; + struct { const uint8_t *p; int64_t n; } s; + int64_t arr[2]; + flan_dyn t, v, w; + flan_gc_init(); + t = flan_dyn_from_bytes((const uint8_t *)"abc", 3); + flan_dyn_root_push(&t); + flan_dyn_need_as(t, (const uint8_t *)"t", 1, &s, NULL, 0); + check(s.n == 3 && memcmp(s.p, "abc", 3) == 0, "into str"); + v = flan_dyn_view_slice(a, 3, (const uint8_t *)"l", 1, 0); + flan_dyn_root_push(&v); + flan_dyn_need_as(v, (const uint8_t *)"sl", 2, &s, NULL, 0); + check((const void *)s.p == (const void *)a && s.n == 3, "into [i64] view"); + w = flan_dyn_vec_new(); + flan_dyn_root_push(&w); + flan_dyn_push(w, flan_dyn_from_i64(7), NULL, 0); + flan_dyn_push(w, flan_dyn_from_i64(8), NULL, 0); + flan_dyn_need_as(w, (const uint8_t *)"a2;l", 4, arr, NULL, 0); + check(arr[0] == 7 && arr[1] == 8, "into [2 i64] copy"); + flan_dyn_root_pop(3); +} + static void walkreset(void) { static uint64_t big[1] = { UINT64_MAX }; flan_dyn v, w, m; @@ -1416,6 +1441,7 @@ int main(int argc, char **argv) { } if (strcmp(argv[1], "ops") == 0) { ops(); + into(); printf(failures == 0 ? "ops ok\n" : "ops failed\n"); return failures == 0 ? 0 : 1; } diff --git a/test/programs/dyn-into-typed.flan b/test/programs/dyn-into-typed.flan new file mode 100644 index 00000000..22db7401 --- /dev/null +++ b/test/programs/dyn-into-typed.flan @@ -0,0 +1,115 @@ +;;;; A dyn value where typed code wrote a str, a slice, a fixed array or a +;;;; struct. Mode 0 is the survey; the others are one trap each, since a trap +;;;; ends the process. test_acceptance.ml runs it on both backends and under +;;;; --dev, where mode 8 (a view of a returned call's local) traps as well. + +(declare gc-collect [] () "flan_gc_collect") +(declare gc-count [] i64 "flan_gc_count") + +(defstruct Point [x f64 y i32]) +(defstruct Named [name str id u32]) +(defclass pt [x y]) + +;; Unannotated parameters and returns are dyn. +(defn keep [d] dyn d) +(defn text-of [d] dyn (slice d 0 5)) + +(defn shout [s str] () (println s)) +(defn keep-str [s str] str s) +;; The text crosses in this frame, whose dyn slots are gone once it returns. +(defn fetch [] str (keep-str (text-of (keep "pinned text")))) +(defn sum [xs [const i64]] i64 + (let [t (i64 0)] + (dotimes [i (length xs)] (set t (+ t (at xs i)))) + t)) +(defn bump [xs [i64]] () + (dotimes [i (length xs)] (set (at xs i) (+ (at xs i) 100)))) +(defn sumf [xs [const f64]] f64 + (let [t 0.0] + (dotimes [i (length xs)] (set t (+ t (at xs i)))) + t)) +(defn bytes-sum [xs [const u8]] i64 + (let [t (i64 0)] + (dotimes [i (length xs)] (set t (+ t (i64 (at xs i))))) + t)) +(defn names [xs [const str]] () + (dotimes [i (length xs)] (println (at xs i)))) +(defn words [xs [const [const u8]]] i64 (length (at xs 1))) +(defn triple [a [3 i64]] i64 (+ (at a 0) (at a 2))) +(defn grid [g [2 [2 i32]]] i32 (at g 1 0)) +(defn px [p Point] f64 (+ (.x p) (f64 (.y p)))) +(defn named [n Named] () (println (.name n) (.id n))) +(defn f64s [xs [f64]] () (println (length xs))) +(defn raw [xs [u8]] () (println (length xs))) + +;; A view of a local, kept past the call that owns the local. +(defonce held dyn nil) +(defn leak-local [] () + (let [a [(i64 1) 2 3]] + (set held (keep a)))) + +(defn main [args [str]] i32 + (let [n (i32 (bytes->i64 (bytes-view (at args 1))))] + (cond + (= n 0) + (do + ;; A text becomes a str. + (shout (keep "hello")) + (println (length (keep-str (keep "four")))) + ;; A str kept past its crossing, the text reachable only through + ;; the pin, while collections run and the heap is refilled. + (let [s (fetch)] + (gc-collect) + (dotimes [i 20000] (text-of (keep "XXXXXXXXXX"))) + (gc-collect) + (dotimes [i 20000] (text-of (keep "XXXXXXXXXX"))) + (println s)) + ;; A pin ends at free-temp: a thousand crossed texts are collected. + (free-temp) + (gc-collect) + (let [before (gc-count)] + (dotimes [i 1000] (keep-str (text-of (keep "abcdefgh")))) + (free-temp) + (gc-collect) + (println (< (- (gc-count) before) 100))) + ;; A view of typed storage comes back as that storage. + (let [a [(i64 1) 2 3]] + (bump (keep a)) + (println a) + (println (sum (keep a)))) + (let [v (vec-new i64)] + (push v 5) (push v 6) + (bump (keep v)) + (println (at v 0) (at v 1)) + (free v)) + ;; A plain dyn vec is a checked copy for a [const T]. + (println (sum (the dyn [1 2 3 4]))) + (println (sumf (the dyn [1 2.5]))) + (println (bytes-sum (the dyn [1 2 255]))) + (println (bytes-sum (keep "AB"))) + (names (the dyn ["ada" "bo"])) + (println (words (the dyn ["x" "yyy"]))) + ;; A view of other elements too. + (let [b [(i32 7) 8]] + (println (sum (keep b)))) + ;; A fixed array and a struct are values: a copy either way. + (println (triple (the dyn [10 20 30]))) + (let [a [(i64 4) 5 6]] (println (triple (keep a)))) + (println (grid (the dyn [[1 2] [3 4]]))) + (println (px (the dyn {:x 1.5 :y 2}))) + (println (px (pt 2.5 3))) + (let [p (Point {.x 0.25 .y 4})] (println (px (keep p)))) + (named (the dyn {:name "cy" :id 9})) + 0) + (= n 1) (do (println (sum (the dyn [1 "a" 3]))) 0) + (= n 2) (do (bump (the dyn [1 2])) 0) + (= n 3) (do (println (sum (keep "abc"))) 0) + (= n 4) (do (println (bytes-sum (the dyn [1 300]))) 0) + (= n 5) (do (println (triple (the dyn [1 2]))) 0) + (= n 6) (do (println (px (the dyn {:x 1.5}))) 0) + (= n 7) (do (shout (keep 5)) 0) + (= n 8) (do (leak-local) (bump held) 0) + (= n 9) (do (raw (keep "abc")) 0) + (= n 10) (let [a [(f32 1) 2]] (f64s (keep a)) 0) + (= n 11) (do (println (px (the dyn {:x 1.5 :y "no"}))) 0) + :else 1))) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index bb0d75f7..d666a42e 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -6023,6 +6023,67 @@ level "1" dyn_view_any ~dev:true (); dyn_view_any ~dev:true ~x86:true (); + (* ── A dyn value into a written type ─────────────────────────────── + programs/dyn-into-typed.flan: a text into a str, kept past the frame + it crossed in while collections run; a view back into its own + storage, written through; a dyn vec, map or instance copied into a + [const T], a fixed array or a struct (mode 0); then one trap per mode, + each naming the element and what it holds. Mode 8, a view of a + returned call's local, traps under --dev only. *) + let into_out = + "hello\n4\npinne\ntrue\n[101 102 103]\n306\n105 106\n10\n3.5\n258\n\ + 131\nada\nbo\n3\n15\n40\n10\n3\n3.5\n5.5\n4.25\ncy 9\n" + and into_traps = + [ ("1", "dyn-into-typed.flan:104:33: dyn into [const i64]: element 1 \ + is a text, \"a\", and an i64 is wanted there"); + ("2", "dyn into [i64]: this vec is a dyn vec, so a [i64] of it would \ + be a copy, and a write through the copy would never reach the \ + vec. Take it as [const i64], which reads a copy"); + ("3", "this is a text, \"abc\", and only a vec becomes a [const i64]"); + ("4", "element 1 is 300, and a u8 holds 0 to 255"); + ("5", "this vec has 2 elements, and a [3 i64] holds exactly 3"); + ("6", "dyn into Point: this map has no :y, and a Point needs every \ + field"); + ("7", "dyn into str: this is an int, 5, and only a text becomes a str"); + ("9", "a text is read-only, so it becomes a str or a [const u8] and \ + never a [u8]"); + ("10", "this vec is a view of f32 elements"); + ("11", "field :y is a text, \"no\", and an i32 is wanted there") ] + and into_stale = + [ ("8", "dyn into [i64]: this view points into a local of leak-local, \ + and that call has returned") ] + in + let dyn_into ?opt ?(x86 = false) ?(dev = false) () = + let exe = compile ?opt ~x86 ~dev "programs/dyn-into-typed.flan" in + let name what = + "dyn: into a written type" ^ what + ^ (match opt with Some o -> ", " ^ o | None -> "") + ^ (if x86 then ", --x86" else "") ^ (if dev then ", --dev" else "") + in + let code, text = run exe (Some "0") in + if code <> 0 || text <> into_out then begin + incr failures; + Printf.printf "FAIL %s\n got: %S (exit %d)\n wanted: %S\n" + (name "") text code into_out + end; + List.iter + (fun (mode, needle) -> + let code, text = run exe (Some mode) in + if code <> 134 || not (contains text needle) then begin + incr failures; + Printf.printf + "FAIL %s\n got: %S (exit %d)\n wanted a trap \ + saying %S\n" (name (", mode " ^ mode)) text code needle + end) + (into_traps @ if dev then into_stale else []); + (try Sys.remove exe with Sys_error _ -> ()) + in + dyn_into (); + dyn_into ~opt:"-O0" (); + dyn_into ~x86:true (); + dyn_into ~dev:true (); + dyn_into ~dev:true ~x86:true (); + (* The root count, which is the part of this feature the runs above cannot check — and the reason has outlived the stub it was first written about. flan_dyn.c's trigger has a one-megabyte floor, and not one diff --git a/test/test_flan.ml b/test/test_flan.ml index 6b853e5f..0d60369d 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -7713,9 +7713,18 @@ let () = rejects_check "the refuses a dyn and names the cast" "(defn f [x dyn] i32 (the i32 x))" ~needle:"write (i32 x) to convert it"; accepts "the cast that refusal names compiles" "(defn f [x dyn] i32 (i32 x))"; - rejects_check "the refuses a dyn at a type a dyn does not become" + rejects_check "the refuses a dyn at str, which a dyn becomes where passed" "(defn f [x dyn] str (the str x))" - ~needle:"dyn — str does not cross into a written type yet"; + ~needle:"a dyn becomes a str where a str is passed"; + accepts "a dyn becomes a slice, an array and a struct where passed" + "(defstruct P [x i32]) (defn f [a [const i64] b [2 f32] c P s str] () ()) \ + (defn g [d dyn] () (f d d d d))"; + rejects_check "a dyn does not become an array of Vecs" + "(defn f [d dyn] [2 (Vec i64)] d)" + ~needle:"it holds a Vec, which owns its storage"; + rejects_check "a dyn does not become a slice of dyn" + "(defn f [d dyn] [const dyn] d)" + ~needle:"[const dyn] does not cross into a written type yet"; rejects_check "the refuses a dyn at bool, which a dyn becomes where passed" "(defn f [x dyn] bool (the bool x))" ~needle:"a dyn becomes a bool where a bool is passed"; From ca9e28860a665716cdcd20cd74e7e29557ca7e59 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 11:52:13 +0700 Subject: [PATCH 10/16] A dyn value into a written type has a checker row that compiles. --- test/test_flan.ml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_flan.ml b/test/test_flan.ml index 0d60369d..6ea5cb04 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -7717,8 +7717,8 @@ let () = "(defn f [x dyn] str (the str x))" ~needle:"a dyn becomes a str where a str is passed"; accepts "a dyn becomes a slice, an array and a struct where passed" - "(defstruct P [x i32]) (defn f [a [const i64] b [2 f32] c P s str] () ()) \ - (defn g [d dyn] () (f d d d d))"; + "(defstruct P [x i32]) (defn f [a [const i64] b [2 f32] c P s str] i64 (length a)) \ + (defn g [d dyn] i64 (f d d d d))"; rejects_check "a dyn does not become an array of Vecs" "(defn f [d dyn] [2 (Vec i64)] d)" ~needle:"it holds a Vec, which owns its storage"; From 4fb9d3b3fe0c04c29dbe1287c81886030fe1ee41 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 12:05:30 +0700 Subject: [PATCH 11/16] A wide fold of let operands checks slowly, recorded. --- TODO.org | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.org b/TODO.org index 1b677505..71b78b90 100644 --- a/TODO.org +++ b/TODO.org @@ -738,6 +738,9 @@ One spelling for one operation; != stays, and not= is refused with a suggestion of !=. * Checker +** TODO Checking a wide fold of let operands is slow +A 2000-operand (bit-and (let …) …) takes 32 s to check (37 s before the bit operators); +2000 plain names take 0.03 s. Something per operand is quadratic or worse. ** DONE A dyn value takes .field and [:key] CLOSED: [2026-09-26] From 78e25dfa48ba4e4307481bb42bbf1d7660832094 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 12:09:25 +0700 Subject: [PATCH 12/16] A str from dyn text is pinned once per temp stamp and is a poisoned temp copy in a dev build, a float past f32's range and a map key a struct lacks trap, and a struct's refusal names what the value is. --- runtime/flan_dyn.c | 179 ++++++++++++++++++++++++------ test/programs/dyn-into-typed.flan | 23 ++++ test/test_acceptance.ml | 22 +++- 3 files changed, 185 insertions(+), 39 deletions(-) diff --git a/runtime/flan_dyn.c b/runtime/flan_dyn.c index 375be79a..83ac74d8 100644 --- a/runtime/flan_dyn.c +++ b/runtime/flan_dyn.c @@ -1540,56 +1540,97 @@ static void mark_desc(char *base, const flan_desc *d) { * Rooting the text in the crossing's frame would be shorter than that and * wrong for a str returned or stored, and a pin for good would keep every * text a frame loop ever crossed. A copy into the temp arena would be sound - * too; this is the same lifetime without the copy. */ + * too; this is the same lifetime without the copy, and a dev build takes the + * copy instead ([into_text]) so that a str kept past free-temp reads the + * arena's poison, as i64->bytes's text does, rather than whatever text the + * allocator put there next. + * + * A text is pinned once per stamp, however often it crosses: the stamp's + * number is written into the text's [gen], which nothing else reads for a + * text, so a program that never calls free-temp and passes the same texts + * in a loop keeps a flat list. Distinct texts cost one pointer each here, + * and each keeps its own object alive until the stamp ends — heavier than + * i64->bytes's few bytes of arena, since the object has a header. The pins + * sit in runs, one per stamp, so an entry is a pointer and a run's stamp is + * kept once. */ const void *flan_temp_stamp(uint64_t *inc, uint64_t *epoch); int32_t flan_temp_stamp_live(const void *a, uint64_t inc, uint64_t epoch); -typedef struct text_pin { - flan_obj *o; +typedef struct pin_run { const void *a; uint64_t inc, epoch; -} text_pin; + int64_t start; /* its first entry in [pins] */ +} pin_run; -static text_pin *pins; -static int64_t pins_n, pins_cap; +static flan_obj **pins; +static int64_t pins_n, pins_cap; +static pin_run *runs; +static int64_t runs_n, runs_cap; +/* The number the newest run's texts carry in [gen]; never 0, which is what + a text is made with. A wrap would take four billion stamps, and costs one + duplicate entry when it lands on an old text's number. */ +static uint32_t pin_gen; + +static int64_t pins_count(void) { return pins_n; } static void pins_prune(void) { - int64_t i, k = 0; - for (i = 0; i < pins_n; i++) - if (flan_temp_stamp_live(pins[i].a, pins[i].inc, pins[i].epoch)) - pins[k++] = pins[i]; + int64_t r, k = 0, rk = 0; + for (r = 0; r < runs_n; r++) { + int64_t lo = runs[r].start; + int64_t hi = r + 1 < runs_n ? runs[r + 1].start : pins_n; + if (!flan_temp_stamp_live(runs[r].a, runs[r].inc, runs[r].epoch)) continue; + memmove(pins + k, pins + lo, (size_t)(hi - lo) * sizeof *pins); + runs[rk] = runs[r]; + runs[rk].start = k; + k += hi - lo; + rk++; + } pins_n = k; + runs_n = rk; +} + +static void *pins_grow(void *p, int64_t *cap, size_t each) { + int64_t c = *cap ? *cap * 2 : 64; + void *q = realloc(p, (size_t)c * each); + if (q == NULL) trap_oom(NULL, 0, c * (int64_t)each); + *cap = c; + return q; } static void pin_text(flan_obj *o) { uint64_t inc, epoch; const void *a = flan_temp_stamp(&inc, &epoch); - text_pin *last = pins_n > 0 ? &pins[pins_n - 1] : NULL; - if (last != NULL && last->o == o && last->a == a && last->inc == inc - && last->epoch == epoch) + pin_run *last = runs_n > 0 ? &runs[runs_n - 1] : NULL; + if (last == NULL || last->a != a || last->inc != inc + || last->epoch != epoch) { + if (runs_n == runs_cap) { + pins_prune(); + if (runs_n == runs_cap) runs = pins_grow(runs, &runs_cap, sizeof *runs); + } + runs[runs_n].a = a; + runs[runs_n].inc = inc; + runs[runs_n].epoch = epoch; + runs[runs_n].start = pins_n; + runs_n++; + if (++pin_gen == 0) pin_gen = 1; + } else if (o->gen == pin_gen) return; if (pins_n == pins_cap) { pins_prune(); - if (pins_n == pins_cap) { - int64_t cap = pins_cap ? pins_cap * 2 : 64; - text_pin *p = (text_pin *)realloc(pins, (size_t)cap * sizeof *p); - if (p == NULL) trap_oom(NULL, 0, cap * (int64_t)sizeof *p); - pins = p; - pins_cap = cap; - } + if (pins_n == pins_cap) pins = pins_grow(pins, &pins_cap, sizeof *pins); } - pins[pins_n].o = o; - pins[pins_n].a = a; - pins[pins_n].inc = inc; - pins[pins_n].epoch = epoch; - pins_n++; + o->gen = pin_gen; + pins[pins_n++] = o; } +/* How many texts are pinned now, for a test that the list stays flat. */ +int64_t flan_dyn_pin_count(void) { return pins_count(); } + static void gc_mark_all(void) { int64_t i; unsigned k; pins_prune(); - for (i = 0; i < pins_n; i++) mark_push(pins[i].o); + for (i = 0; i < pins_n; i++) mark_push(pins[i]); for (i = 0; i < roots_n; i++) { const flan_desc *d = roots[i].desc; if (d == NULL) mark_value(*(flan_dyn *)roots[i].base); @@ -4219,10 +4260,23 @@ static void into_put(into_site *s, const uint8_t *d, flan_dyn x, uint8_t *p); static void into_slice(into_site *s, const uint8_t *d, flan_dyn x, uint8_t *p); -/* A text's bytes as a str's two words, pinned. */ +/* A text's bytes as a str's two words, pinned — or, in a dev build, copied + * into the temp arena, whose registry note and poison catch a str kept past + * free-temp (see [pin_text]). */ static void into_text(flan_dyn x, uint8_t *p) { flan_obj *o = dyn_obj(x); const uint8_t *b = obj_text_bytes(o); + if (flan_dev_reg_enabled()) { + uint8_t *q = NULL; + if (o->len > 0) { + q = (uint8_t *)flan_temp_block(o->len, 1, 1, "u8", 2); + if (q == NULL) trap_oom(NULL, 0, o->len); + memcpy(q, b, (size_t)o->len); + } + memcpy(p, &q, 8); + memcpy(p + 8, &o->len, 8); + return; + } pin_text(o); memcpy(p, &b, 8); memcpy(p + 8, &o->len, 8); @@ -4282,9 +4336,18 @@ static void into_put(into_site *s, const uint8_t *d, flan_dyn x, uint8_t *p) { into_trap(s, "DynRange", "%s is %lld, which has no exact %s. Write " "it as a float, as in %lld.0", into_who(s), (long long)n, *d == 'f' ? "f32" : "f64", (long long)n); - } else if (flan_dyn_tag(x) == FLAN_DYN_TAG_FLOAT) + } else if (flan_dyn_tag(x) == FLAN_DYN_TAG_FLOAT) { f = dyn_num_value(x); - else { + /* A finite float past f32's range would become inf, which is a change + of value and not a rounding: refused, as an int out of range is. */ + if (*d == 'f' && f == f && f - f == 0 && (f > 3.4028234663852886e38 + || f < -3.4028234663852886e38)) { + char sx[SAY_MAX]; + say(sx, SAY_MAX, x); + into_trap(s, "DynRange", "%s is %s, and an f32 holds -3.4028235e38 " + "to 3.4028235e38", into_who(s), sx); + } + } else { into_wanted(why, sizeof why, d); into_wrong(s, x, why); } @@ -4348,16 +4411,60 @@ static void into_put(into_site *s, const uint8_t *d, flan_dyn x, uint8_t *p) { return; } memcpy(saved, s->where, sizeof saved); + /* What the value is called in a sentence: its place inside the whole, + else what it is — a struct's view, a class's instance, a map. */ + { + char what[160]; + flan_obj *m = dyn_obj(x); + int64_t i, n; + if (s->where[0]) snprintf(what, sizeof what, "%s", s->where); + else if (o != NULL) { + const char *nm; + int64_t nl; + view_struct_name(o, &nm, &nl); + snprintf(what, sizeof what, "this %.*s", (int)nl, nm); + } else if (m->u.v.klass != NULL) { + kw_entry *k = m->u.v.klass; + snprintf(what, sizeof what, "this %.*s", (int)k->len, + (const char *)kw_bytes(k)); + } else + snprintf(what, sizeof what, "this map"); + at = desc_fields(d, &off); + while (desc_next(&at, &off, &name, &namelen, &foff, &fty)) + if (!flan_dyn_truthy(flan_dyn_map_contains_at( + x, flan_dyn_kw(name, namelen), s->loc, s->loclen))) { + desc_spell(d, ty, sizeof ty); + into_trap(s, "DynType", "%s has no :%.*s, and %s %s needs every " + "field", what, (int)namelen, (const char *)name, an(ty), + ty); + } + /* A key the struct has no field for is refused, as a class refuses a + slot it does not declare: dropping it would lose what was written. */ + if (o == NULL) class_sync(m); + n = o != NULL ? view_nfields(o) : m->len; + for (i = 0; i < n; i++) { + flan_dyn k = o != NULL ? view_field_key(o, i) : m->u.v.items[2 * i]; + int64_t foff2; + if (desc_field(d, k, &foff2) == NULL) { + char sk[SAY_MAX]; + int64_t off2; + const uint8_t *at2; + say(sk, SAY_MAX, k); + desc_spell(d, ty, sizeof ty); + said_len = 0; + said_add("dyn %s: %s has %s, and %s %s has no field %s. Its fields " + "are", s->op, what, sk, an(ty), ty, sk); + at2 = desc_fields(d, &off2); + while (desc_next(&at2, &off2, &name, &namelen, &foff, &fty)) + said_add(" :%.*s", (int)namelen, (const char *)name); + flan_say(s->loc, s->loclen, "%s", said_buf); + dyn_trap((const uint8_t *)"DynType", 7); + } + } + } at = desc_fields(d, &off); while (desc_next(&at, &off, &name, &namelen, &foff, &fty)) { flan_dyn k = flan_dyn_kw(name, namelen), v; - if (!flan_dyn_truthy(flan_dyn_map_contains_at(x, k, s->loc, - s->loclen))) { - desc_spell(d, ty, sizeof ty); - into_trap(s, "DynType", "%s has no :%.*s, and %s %s needs every " - "field", s->where[0] ? s->where : "this map", - (int)namelen, (const char *)name, an(ty), ty); - } v = flan_dyn_get(x, k, s->loc, s->loclen); into_enter(s, "field :%.*s", (int)namelen, (const char *)name); into_put(s, fty, v, p + foff); diff --git a/test/programs/dyn-into-typed.flan b/test/programs/dyn-into-typed.flan index 22db7401..cc70fe4d 100644 --- a/test/programs/dyn-into-typed.flan +++ b/test/programs/dyn-into-typed.flan @@ -42,6 +42,15 @@ (defn f64s [xs [f64]] () (println (length xs))) (defn raw [xs [u8]] () (println (length xs))) +(declare pin-count [] i64 "flan_dyn_pin_count") +(defn slen [s str] i64 (length s)) +(defn f32s [xs [const f32]] () (println (length xs))) +(defclass pq [x]) + +;; A str kept in a global past free-temp: a dev build's copy is poisoned. +(defonce kept str "") +(defn stash-str [] () (set kept (keep-str (keep "kept text")))) + ;; A view of a local, kept past the call that owns the local. (defonce held dyn nil) (defn leak-local [] () @@ -72,6 +81,10 @@ (free-temp) (gc-collect) (println (< (- (gc-count) before) 100))) + ;; The same texts crossing again and again are pinned once each. + (let [t1 (keep "one") t2 (keep "two") n (i64 0)] + (dotimes [i 100000] (set n (+ n (slen t1) (slen t2)))) + (println n (< (pin-count) 10))) ;; A view of typed storage comes back as that storage. (let [a [(i64 1) 2 3]] (bump (keep a)) @@ -112,4 +125,14 @@ (= n 9) (do (raw (keep "abc")) 0) (= n 10) (let [a [(f32 1) 2]] (f64s (keep a)) 0) (= n 11) (do (println (px (the dyn {:x 1.5 :y "no"}))) 0) + (= n 12) (do (f32s (the dyn [1.5 1e300])) 0) + (= n 13) (do (println (px (pq 1.5))) 0) + (= n 14) (do (println (px (the dyn {:x 1.5 :y 2 :z 3}))) 0) + (= n 15) + (do (stash-str) + (free-temp) + (gc-collect) + (dotimes [i 1000] (text-of (keep "XXXXXXXXXX"))) + (println (at kept 0)) + 0) :else 1))) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 629ffb34..d5591101 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -6106,10 +6106,10 @@ level "1" each naming the element and what it holds. Mode 8, a view of a returned call's local, traps under --dev only. *) let into_out = - "hello\n4\npinne\ntrue\n[101 102 103]\n306\n105 106\n10\n3.5\n258\n\ + "hello\n4\npinne\ntrue\n600000 true\n[101 102 103]\n306\n105 106\n10\n3.5\n258\n\ 131\nada\nbo\n3\n15\n40\n10\n3\n3.5\n5.5\n4.25\ncy 9\n" and into_traps = - [ ("1", "dyn-into-typed.flan:104:33: dyn into [const i64]: element 1 \ + [ ("1", "dyn-into-typed.flan:117:33: dyn into [const i64]: element 1 \ is a text, \"a\", and an i64 is wanted there"); ("2", "dyn into [i64]: this vec is a dyn vec, so a [i64] of it would \ be a copy, and a write through the copy would never reach the \ @@ -6123,7 +6123,13 @@ level "1" ("9", "a text is read-only, so it becomes a str or a [const u8] and \ never a [u8]"); ("10", "this vec is a view of f32 elements"); - ("11", "field :y is a text, \"no\", and an i32 is wanted there") ] + ("11", "field :y is a text, \"no\", and an i32 is wanted there"); + ("12", "element 1 is 1e+300, and an f32 holds -3.4028235e38 to \ + 3.4028235e38"); + ("13", "dyn into Point: this pq has no :y, and a Point needs every \ + field"); + ("14", "dyn into Point: this map has :z, and a Point has no field \ + :z. Its fields are :x :y") ] and into_stale = [ ("8", "dyn into [i64]: this view points into a local of leak-local, \ and that call has returned") ] @@ -6151,6 +6157,16 @@ level "1" saying %S\n" (name (", mode " ^ mode)) text code needle end) (into_traps @ if dev then into_stale else []); + (* A str from a text kept in a global past free-temp: a dev build's + copy reads the arena's poison (0xEF), never another text's bytes. *) + if dev then begin + let code, text = run exe (Some "15") in + if code <> 0 || text <> "239\n" then begin + incr failures; + Printf.printf "FAIL %s\n got: %S (exit %d)\n wanted: %S\n" + (name ", a str kept past free-temp") text code "239\n" + end + end; (try Sys.remove exe with Sys_error _ -> ()) in dyn_into (); From b1cba196b99cac66644fb5497d89b594da33525b Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 12:19:03 +0700 Subject: [PATCH 13/16] A .fln comparison chain may mix < with <=, or > with >=, reading as the and of its tests with every operand evaluated once, left to right, as a < b < c is, and the printers write that and back as the chain. --- TODO.org | 4 + lib/indent_printer.ml | 103 ++++++++++++++++++- lib/indent_reader.ml | 130 +++++++++++++++++++++--- lib/paren_printer.ml | 53 ++++++++++ spec-syntax.md | 15 ++- test/syntax/chain/macro.fln | 16 +++ test/syntax/handwritten/chain-mixed.fln | 76 ++++++++++++++ test/syntax/handwritten/chain-mixed.out | 12 +++ test/test_syntax.ml | 53 +++++++++- 9 files changed, 441 insertions(+), 21 deletions(-) create mode 100644 test/syntax/chain/macro.fln create mode 100644 test/syntax/handwritten/chain-mixed.fln create mode 100644 test/syntax/handwritten/chain-mixed.out diff --git a/TODO.org b/TODO.org index 71b78b90..0a4e2a1a 100644 --- a/TODO.org +++ b/TODO.org @@ -696,6 +696,10 @@ consecutive lets this way. Rules out ~loop~/~recur~ anywhere the .fln reader reads, ~quote~ included; loops are ~while~/~until~/~dotimes~/~for~. The Lisp syntax and its macros' expansions keep them. +** DONE A .fln chain may mix < with <=, or > with >= (decision 124) +~0 <= r < rows~ is the ~and~ of its tests; like ~(< a b c)~ every operand runs once, left to right, +with no short-circuit. Direction changes and ~==~/~!=~ in a mix stay refused. + ** TODO Hard-coded code in messages is still paren syntax in a .fln file Types follow the code's syntax now (=Types.spell=). Hints written into a message's text — =(Ptr %s)=, =(clone v)=, =(the T x)= in most of =check.ml= and =parse.ml=, the diff --git a/lib/indent_printer.ml b/lib/indent_printer.ml index a41982bc..2a841df3 100644 --- a/lib/indent_printer.ml +++ b/lib/indent_printer.ml @@ -173,6 +173,92 @@ let rec pat_names (t : Form.t) : string list option = let binds n t = match pat_names t with Some ns -> List.mem n ns | None -> false +(* [f] as a comparison chain that mixes < with <=, or > with >=: its operands + and operators, when the reader would read the chain back as [f] itself. + The candidate is rebuilt by the reader's own [cmp_chain] and compared up to + the names its [let]s bind, so an [and] of tests that only looks like a + chain, or a [let] the reader would not have made, prints as it is. *) +let chain_of (f : Form.t) = + let rec eq env (a : Form.t) (b : Form.t) = + match a.v, b.v with + | Form.Sym x, Form.Sym y -> + (match List.assoc_opt x env with + | Some y' -> y = y' + | None -> x = y && not (List.exists (fun (_, y') -> y' = y) env)) + | Form.List ({ v = Form.Sym "let"; _ } :: { v = Form.Vec bx; _ } :: xs), + Form.List ({ v = Form.Sym "let"; _ } :: { v = Form.Vec by; _ } :: ys) -> + let rec binds env bx by = + match bx, by with + | ({ Form.v = Form.Sym tx; _ }) :: vx :: bx', ({ Form.v = Form.Sym ty; _ }) :: vy :: by' -> + if eq env vx vy then binds ((tx, ty) :: env) bx' by' else None + | [], [] -> Some env + | _ -> None + in + (match binds env bx by with + | Some env -> List.length xs = List.length ys && List.for_all2 (eq env) xs ys + | None -> false) + | Form.List xs, Form.List ys | Form.Vec xs, Form.Vec ys | Form.Map xs, Form.Map ys -> + List.length xs = List.length ys && List.for_all2 (eq env) xs ys + | x, y -> x = y + in + let subst env (x : Form.t) = + match x.v with + | Form.Sym s -> Option.value (List.assoc_opt s env) ~default:x + | _ -> x + in + (* The tests, left to right, with each bound name replaced by its value. *) + let rec tests env (f : Form.t) = + match f.v with + | Form.List [ { v = Form.Sym "let"; _ }; { v = Form.Vec bs; _ }; body ] -> + let rec binds env = function + | ({ Form.v = Form.Sym t; _ }) :: v :: rest -> binds ((t, subst env v) :: env) rest + | [] -> Some env + | _ -> None + in + Option.bind (binds env bs) (fun env -> tests env body) + | Form.List ({ v = Form.Sym "and"; _ } :: (_ :: _ :: _ as cs)) -> + List.fold_left + (fun acc c -> Option.bind acc (fun l -> Option.map (( @ ) l) (tests env c))) + (Some []) cs + | Form.List [ { v = Form.Sym op; _ }; a; b ] when R.cmp_dir op <> None -> + Some [ (op, subst env a, subst env b) ] + | _ -> None + in + let rec linked = function + | (_, _, b) :: ((_, a, _) :: _ as rest) -> eq [] b a && linked rest + | _ -> true + in + (* In a template the paren text spells a [~cmp] name as the unquoted call + that makes it, [~(Form.Sym {.s "~cmp1"})]: read it as the name. *) + let rec unwrap (x : Form.t) = + match x.v with + | Form.List [ { v = Form.Sym "unquote"; _ }; + { v = Form.List [ { v = Form.Sym "Form.Sym"; _ }; + { v = Form.Map [ { v = Form.Sym ".s"; _ }; + { v = Form.Str n; _ } ]; _ } ]; _ } ] + when String.length n > 4 && String.sub n 0 4 = "~cmp" -> { x with v = Form.Sym n } + | Form.List l -> { x with v = Form.List (List.map unwrap l) } + | Form.Vec l -> { x with v = Form.Vec (List.map unwrap l) } + | _ -> x + in + match f.v with + | Form.List ({ v = Form.Sym ("and" | "let"); _ } :: _) -> + let f = unwrap f in + (match tests [] f with + | Some (((op1, x0, _) :: _ :: _) as ts) + when linked ts + && List.for_all (fun (op, _, _) -> R.cmp_dir op = R.cmp_dir op1) ts + && List.exists (fun (op, _, _) -> op <> op1) ts -> + let xs = x0 :: List.map (fun (_, _, b) -> b) ts in + let ops = List.map (fun (op, _, _) -> op) ts in + let n = ref 0 in + let fresh () = incr n; Printf.sprintf "~cmp%d" !n in + if eq [] f (R.cmp_chain ~fresh f.loc xs ops) then Some (xs, ops) else None + | _ -> None) + | _ -> None + +let is_chain f = chain_of f <> None + (* Whether [f] mentions [n]: the name, or a field path or qualified name starting with it. Any occurrence counts, a quoted one or one under an unquote included. A macro whose expansion names a variable its call does @@ -266,7 +352,7 @@ let rename_let n n' (bs : Form.t list) (body : Form.t list) = let flatten (f : Form.t) (rest : Form.t list) = match f.v with | Form.List (({ v = Form.Sym "let"; _ } as h) :: ({ v = Form.Vec bs; _ } as bv) :: (_ :: _ as body)) - when rest <> [] && bs <> [] && List.length bs mod 2 = 0 -> + when rest <> [] && bs <> [] && List.length bs mod 2 = 0 && not (is_chain f) -> Option.bind (all pat_names (List.filteri (fun i _ -> i mod 2 = 0) bs)) (fun names -> @@ -326,6 +412,13 @@ let rec expr (f : Form.t) : string * int = | Form.Vec xs -> ("[" ^ vec_text xs ^ "]", 13) | Form.Map xs -> ("{" ^ map_text xs ^ "}", 13) | Form.List [] -> ("()", 13) + | Form.List _ when is_chain f -> + let xs, ops = Option.get (chain_of f) in + let lvl = Option.get (R.binop_level (List.hd ops)) in + let ts = List.map (at (lvl + 1)) xs in + (List.hd ts + ^ String.concat "" (List.map2 (fun op t -> " " ^ op ^ " " ^ t) ops (List.tl ts)), + lvl) | Form.List (h :: args) -> in_quasi f (fun () -> list f h args) and sym f s = @@ -605,6 +698,7 @@ let body_guess (h : Form.t) args = lists goes in the block. *) let stmt_like (a : Form.t) = match a.v with + | Form.List _ when is_chain a -> false | Form.List ({ v = Form.Sym h; _ } :: _) -> List.mem h [ "let"; "set"; "when"; "unless"; "cond"; "while"; "until"; "dotimes"; "match"; "handler-case"; @@ -641,7 +735,8 @@ let body_guess (h : Form.t) args = let let_sugar (f : Form.t) = match f.v with - | Form.List ({ v = Form.Sym "let"; _ } :: { v = Form.Vec bs; _ } :: _ :: _) -> + | Form.List ({ v = Form.Sym "let"; _ } :: { v = Form.Vec bs; _ } :: _ :: _) + when not (is_chain f) -> (match pairs bs with None | Some [] -> false | Some _ -> true) | _ -> false @@ -940,6 +1035,7 @@ and value_lines n prefix (v : Form.t) = if n + String.length inline <= width then [ ind n ^ inline ] else match v.v with + | _ when is_chain v -> [ ind n ^ inline ] | Form.List ({ v = Form.Sym h; _ } :: _) when not (List.mem h sugar_heads || h = "fn" || h = "if") -> wrapped n (prefix ^ " = ") v @@ -956,7 +1052,8 @@ and label_of = function and sugar n (f : Form.t) : string list option = let i = ind n in match f.v with - | Form.List ({ v = Form.Sym "let"; _ } :: { v = Form.Vec bs; _ } :: (_ :: _ as body)) -> + | Form.List ({ v = Form.Sym "let"; _ } :: { v = Form.Vec bs; _ } :: (_ :: _ as body)) + when not (is_chain f) -> (match pairs bs with | None | Some [] -> None | Some prs -> Some (let_lines n prs body)) diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index 2980ec47..c036d4ec 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -100,6 +100,49 @@ let compound (at : Loc.t) op (e : Form.t) (v : Form.t) span = Form.List [ Form.make (Form.Sym "update") at; e; Form.make (Form.Sym op) at; v ] +(* A comparison chain that mixes [<] with [<=], or [>] with [>=], is the + [and] of its neighbouring pairs: [0 <= r < rows] is + [(and (<= 0 r) (< r rows))]. It is evaluated as [(< a b c)] is: every + operand once, left to right, before any test, with no short-circuit. When + an operand is more than a name or a literal, every operand but a literal + is bound first, in order, to a fresh [~cmp] name, which no reader can + produce: a name too, since a call to its right may change it. The printer + rebuilds a candidate with this same function and prints the chain only + when the two agree. *) +let cmp_dir = function + | "<" | "<=" -> Some `Up + | ">" | ">=" -> Some `Down + | _ -> None + +let cmp_chain ~fresh (l : Loc.t) (xs : Form.t list) (ops : string list) = + let mkf v = Form.make v l in + let s x = mkf (Form.Sym x) in + let literal (x : Form.t) = + match x.v with + | Form.Int _ | Form.UInt _ | Form.Float _ | Form.Str _ | Form.Byte _ + | Form.Kw _ | Form.Sym ("true" | "false" | "nil") -> true + | _ -> false + in + let simple (x : Form.t) = literal x || (match x.v with Form.Sym _ -> true | _ -> false) in + let keep = if List.for_all simple xs then simple else literal in + let bound = + List.map (fun x -> if keep x then (None, x) else + let t = s (fresh ()) in (Some (t, x), t)) xs + in + let refs = List.map snd bound in + let rec tests = function + | a :: (b :: _ as rest), op :: ops -> mkf (Form.List [ s op; a; b ]) :: tests (rest, ops) + | _ -> [] + in + let body = mkf (Form.List (s "and" :: tests (refs, ops))) in + match List.concat_map (function (Some (t, x), _) -> [ t; x ] | _ -> []) bound with + | [] -> body + | bs -> mkf (Form.List [ s "let"; mkf (Form.Vec bs); body ]) + +(* The reader's fresh names for [cmp_chain], counted per [read_all]. *) +let cmp_n = ref 0 +let cmp_fresh () = incr cmp_n; Printf.sprintf "~cmp%d" !cmp_n + (* A [-] glued to one of these starts a negation: [-x] is [(- x)]. Anything else keeps the Lisp reading, so [--], [->] and [-=] stay names. *) let is_neg_char c = @@ -676,6 +719,30 @@ let no_loop loc word = break leaves the loop early, and continue goes on to the next round." word +(* A refused chain written out as the [and] of all its tests. A middle + operand that is more than a name or a literal is named by a [let] first, + so the rewrite does not run it twice. *) +let and_rewrite (xs : Form.t list) ops = + let n = List.length xs in + let lets = ref [] in + let texts = + List.mapi + (fun i (x : Form.t) -> + let plain = match x.v with Form.List _ | Form.Vec _ | Form.Map _ -> false | _ -> true in + if plain || i = 0 || i = n - 1 then text_of x + else begin + let m = if !lets = [] then "mid" else Printf.sprintf "mid%d" (List.length !lets + 1) in + lets := Printf.sprintf " let %s = %s\n" m (text_of x) :: !lets; + m + end) + xs + in + let rec tests = function + | a :: (b :: _ as rest), op :: ops -> Printf.sprintf "%s %s %s" a op b :: tests (rest, ops) + | _ -> [] + in + String.concat "" (List.rev !lets) ^ " " ^ String.concat " and " (tests (texts, ops)) + (* Expressions come back with their syntactic level: 13 an atom or a bracket, 12 a postfix chain, 11 a prefix [-] or [~~], 1-10 a binary operator's level, 3 a [not], 0 a one-line [if] or a lambda. Anything under 11 is @@ -706,31 +773,62 @@ and binary p lvl : Form.t * int = binop_level s = Some lvl && not ((peek_at p 1).tok = LP && not (peek_at p 1).sp) in + let operator s = + let ot = advance p in + if not (ot.sp && (peek p).sp) then + failk "unspaced-operator" ot.loc + "%s is an operator here, and a binary operator has a space on each \ + side: a %s b. Without them a-b is one name" + s s; + let rhs, _ = binary p (lvl + 1) in + (ot, rhs) + in let rec run op operands = match (peek p).tok with | NAME s when binary_here s -> - let ot = advance p in - if not (ot.sp && (peek p).sp) then - failk "unspaced-operator" ot.loc - "%s is an operator here, and a binary operator has a space on each \ - side: a %s b. Without them a-b is one name" - s s; - let rhs, _ = binary p (lvl + 1) in + let _, rhs = operator s in if s = op then run op (rhs :: operands) - else begin - if lvl = 4 then - failk "mixed-comparison" ot.loc - "%s follows %s in one chain, and a chain compares with one \ - operator. Join the tests with and, or parenthesise one side" - s op; + else let folded, _ = close op operands in run s [ rhs; folded ] - end | _ -> close op operands in + (* A comparison chain is read whole, then judged: one operator throughout + is the variadic call, one direction is [cmp_chain], anything else is + refused at the first operator that breaks it. *) + let rec chain acc = + match (peek p).tok with + | NAME s when binary_here s -> + let ot, rhs = operator s in + chain ((s, ot, rhs) :: acc) + | _ -> List.rev acc + in + let comparison () = + let links = chain [] in + let ops = List.map (fun (s, _, _) -> s) links in + let xs = first :: List.map (fun (_, _, x) -> x) links in + let op1 = List.hd ops in + if List.for_all (( = ) op1) ops then close op1 (List.rev xs) + else + let d = cmp_dir op1 in + Array.iteri + (fun i (op, (ot : token), _) -> + if i > 0 && (d = None || cmp_dir op <> d) then begin + let prev, _, _ = List.nth links (i - 1) in + failk "mixed-comparison" ot.loc + "%s follows %s in one chain. A chain may repeat one operator, \ + or mix < with <=, or > with >=, as in 0 <= i < n. Write this \ + one as tests joined with and:\n\n%s" + op prev (and_rewrite xs ops) + end) + (Array.of_list links); + (cmp_chain ~fresh:cmp_fresh (span p l0) xs ops, lvl) + in (* [run] folds a different operator at the same level into the left operand, so the first operator here only starts the first run. *) match (peek p).tok with + | NAME s when binary_here s && (cmp_dir s <> None || s = "==" || s = "!=") -> + comparison () | NAME s when binary_here s -> run s [ first ] | _ -> fst_ @@ -2295,12 +2393,14 @@ let read_all ?(line = 1) ?col ?indent ?(global_let = true) ~file src = let snippet = col <> None in let col = Option.value col ~default:1 in let saved = !source in + let saved_n = !cmp_n in + cmp_n := 0; (* The quoted text is indexed by the buffer's lines, so a snippet that starts on line 40 is padded to start there. *) source := (file, Array.of_list (String.split_on_char '\n' (String.make (line - 1) '\n' ^ String.make (col - 1) ' ' ^ src))); - Fun.protect ~finally:(fun () -> source := saved) (fun () -> + Fun.protect ~finally:(fun () -> source := saved; cmp_n := saved_n) (fun () -> let toks = layout ~snippet ~base:col ?indent (lex ~line ~col ~file src) in let s = { p = { toks; i = 0; closed = -1 }; lets = [] } in (* At the top level, a [let] is a global, [(def x dyn v)]: a let there has diff --git a/lib/paren_printer.ml b/lib/paren_printer.ml index fd32ae83..7e73b39b 100644 --- a/lib/paren_printer.ml +++ b/lib/paren_printer.ml @@ -315,7 +315,60 @@ let rec layout ?(inside = fun _ -> false) spell col (f : Form.t) : string list = | _ -> [ one ] (** A whole file, with [source]'s comments and spellings when given. *) +(* A .fln comparison chain binds its operands to [~cmp] names, which paren + text cannot spell ([~] opens an unquote). Outside a template each gets a + name that nothing in its top-level form uses, so no reference there is + captured. Inside one a plain name would capture the caller's variable of + that name, and the paren syntax has no auto-gensym, so the name is made + where it lands: [~(Form.Sym {.s "~cmp1"})], a name no caller can write. *) +let readable_temps (f : Form.t) = + let is_temp s = String.length s > 4 && String.sub s 0 4 = "~cmp" in + let rec syms acc (f : Form.t) = + match f.v with + | Form.Sym s -> s :: acc + | Form.List l | Form.Vec l | Form.Map l -> List.fold_left syms acc l + | _ -> acc + in + let all = syms [] f in + let temps = + List.fold_left + (fun acc s -> if is_temp s && not (List.mem s acc) then s :: acc else acc) + [] (List.rev all) + |> List.rev + in + if temps = [] then f + else + let taken = ref all in + let rec pick i = + let n = if i = 1 then "mid" else Printf.sprintf "mid%d" i in + if List.mem n !taken then pick (i + 1) else (taken := n :: !taken; n) + in + let names = List.map (fun t -> (t, pick 1)) temps in + let rec go depth (f : Form.t) = + let sub l = List.map (go depth) l in + match f.v with + | Form.Sym s when is_temp s && depth > 0 -> + let m v = Form.make v f.loc in + m (Form.List + [ m (Form.Sym "unquote"); + m (Form.List [ m (Form.Sym "Form.Sym"); + m (Form.Map [ m (Form.Sym ".s"); m (Form.Str s) ]) ]) ]) + | Form.Sym s -> + (match List.assoc_opt s names with Some n -> { f with v = Form.Sym n } | None -> f) + | Form.List [ ({ v = Form.Sym "quasiquote"; _ } as h); x ] -> + { f with v = Form.List [ h; go (depth + 1) x ] } + | Form.List [ ({ v = Form.Sym ("unquote" | "unquote-splicing"); _ } as h); x ] + when depth > 0 -> + { f with v = Form.List [ h; go (depth - 1) x ] } + | Form.List l -> { f with v = Form.List (sub l) } + | Form.Vec l -> { f with v = Form.Vec (sub l) } + | Form.Map l -> { f with v = Form.Map (sub l) } + | _ -> f + in + go 0 f + let program ?source (fs : Form.t list) : string = + let fs = List.map readable_temps fs in let spell = match source with Some src -> Source_text.spelling src | None -> fun _ -> None in diff --git a/spec-syntax.md b/spec-syntax.md index 0f9cf874..0a1f11a8 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -156,14 +156,25 @@ Each item: the proposal, then the reason in one line. - **Precedence**, low to high: `or` < `and` < `not` < comparisons (`== != < <= > >=`) < `||` < `^^` < `&&` < `<< >>` < `+ -` < `* / %` < - prefix `-` and `~~` < postfix (call, index, field). **Built.** Mixing - comparison operators in one chain, `a < b <= c`, is refused. An operator + prefix `-` and `~~` < postfix (call, index, field). **Built.** An operator glued to `(` is always a call. The bit operators sit where Python and Rust put them, so `x && mask == 0` is `(x && mask) == 0`. - **The bit operators** are `a && b`, `a || b`, `a ^^ b` and `~~a`, reading `(bit-and a b)`, `(bit-or a b)`, `(bit-xor a b)` and `(bit-not a)`. They take integers; `and`, `or` and `not` are the logical ones. `~~` is one token, so a nested unquote is written `~(~x)`. **Built.** +- **A comparison chain may mix `<` with `<=`, or `>` with `>=`** (decision + 124). `0 <= r < rows` reads `(and (<= 0 r) (< r rows))`. It is evaluated as + `a < b < c` is: every operand once, left to right, before any test, with no + short-circuit. When an operand is more than a name or a literal, every + operand but a literal is bound first, in order, to a fresh name, so a name + is read before a call to its right runs: `a < f(x) <= b` reads + `(let [~cmp1 a ~cmp2 (f x) ~cmp3 b] (and (< ~cmp1 ~cmp2) (<= ~cmp2 ~cmp3)))`. + `flan convert` to parens names them `mid`, `mid2`, ..., or, inside a + template, `~(Form.Sym {.s "~cmp1"})`, which no caller can capture. A chain that + changes direction, `a < b > c`, or mixes in `==` or `!=`, is refused with the + whole chain rewritten as `and`, a middle call named by a `let` first. The + printer writes such an `and` back as the chain. **Built.** - **`==` is `=`; `=` is assignment.** `x = v` reads `(set x v)`, `a[i] = v` reads `(set (at a i) v)`, `p.x = v` reads `(set (.x p) v)`. `x += v` reads `(set x (+ x v))` where every part of the place is a name or a literal, and diff --git a/test/syntax/chain/macro.fln b/test/syntax/chain/macro.fln new file mode 100644 index 00000000..10d35552 --- /dev/null +++ b/test/syntax/chain/macro.fln @@ -0,0 +1,16 @@ +;;;; A chain in a macro's template, converted to parens: the names it binds +;;;; must not capture the caller's, which here are the ones the converter +;;;; would otherwise pick. + +defmacro(between, [lo x hi]): + quote + ~lo <= ~x < ~hi + +fn main() -> i32 + let mid = 1 + let mid2 = 2 + let mid3 = 3 + println(between(mid, 5, mid)) + println(between(0, mid2, mid3)) + println(between(mid3, mid2, mid)) + 0 diff --git a/test/syntax/handwritten/chain-mixed.fln b/test/syntax/handwritten/chain-mixed.fln new file mode 100644 index 00000000..10963b10 --- /dev/null +++ b/test/syntax/handwritten/chain-mixed.fln @@ -0,0 +1,76 @@ +;;;; A comparison chain that mixes < with <=, or > with >=, is the and of its +;;;; neighbouring tests, evaluated as a < b < c is. Every operand below comes +;;;; through mark, which prints its tag, so each tag line is a transcript: +;;;; each operand runs exactly once, in source order, even after a false test. + +once calls = 0 + +fn mark(tag: str, v: i32) -> i32 + calls += 1 + print(tag) + v + +fn line(b: bool) -> () + print(" -> ") + println(b) + +; A name is read where it stands, before a call to its right changes it. +once level = 0 + +fn raise() -> i32 + level = 10 + 5 + +fn dyn-mark(tag, v) + print(tag) + v + +fn in-grid(r: i32, rows: i32) -> bool = 0 <= r < rows + +fn dyn-between(lo, x, hi) = lo <= x < hi + +fn main() -> i32 + print(in-grid(0, 3)) + print(" ") + print(in-grid(2, 3)) + print(" ") + print(in-grid(3, 3)) + print(" ") + print(in-grid(-1, 3)) + println("") + let a = 1 + let b = 2 + let c = 2 + let d = 5 + print(a < b <= c < d) + print(" ") + print(a < b <= c < 2) + print(" ") + print(d >= c > 1) + print(" ") + print(d >= c > 2) + println("") + + ; A middle operand that is a call runs once although two tests name it. + line(mark("a", 1) < mark("b", 2) <= mark("c", 2)) + line(mark("a", 1) < mark("b", 2) <= mark("c", 1)) + ; The first test is false, and every operand still runs. + line(mark("a", 3) < mark("b", 2) <= mark("c", 5)) + line(mark("a", 1) <= mark("b", 2) < mark("c", 3) <= mark("d", 3)) + line(mark("a", 9) >= mark("b", 5) > mark("c", 7) >= mark("d", 0)) + println(calls) + + ; The same over dyn operands. + print(dyn-between(0, 0, 3)) + print(" ") + print(dyn-between(0, 3, 3)) + print(" ") + print(dyn-between(1.5, 2, 2.5)) + println("") + line(dyn-mark("p", 1) < dyn-mark("q", 2) <= dyn-mark("r", 2)) + line(dyn-mark("p", 5) < dyn-mark("q", 2) <= dyn-mark("r", 9)) + print(level < raise() <= 7) + level = 0 + print(" ") + println(<(level, raise(), 7)) + 0 diff --git a/test/syntax/handwritten/chain-mixed.out b/test/syntax/handwritten/chain-mixed.out new file mode 100644 index 00000000..11bb5298 --- /dev/null +++ b/test/syntax/handwritten/chain-mixed.out @@ -0,0 +1,12 @@ +true true false false +true false true false +abc -> true +abc -> false +abc -> false +abcd -> true +abcd -> false +17 +true false true +pqr -> true +pqr -> false +true true diff --git a/test/test_syntax.ml b/test/test_syntax.ml index 370153d0..b4f80c11 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -549,7 +549,25 @@ let () = "(quasiquote (f (bit-not x) (unquote (bit-not y))))"; refuses "not-equal chain" "x = a != b != c" "indent/chained-not-equal" "!=(a, b, c)"; reads "not-equal call" "x = !=(a, b, c)" "(set x (!= a b c))"; - refuses "mixed comparison" "x = a < b <= c" "indent/mixed-comparison" "and"; + (* One direction mixes; each operand that is a call is bound once, in + order, before any test. *) + reads "mixed chain" "x = 0 <= r < rows" "(set x (and (<= 0 r) (< r rows)))"; + reads "mixed chain of four" "x = a < b <= c < d" + "(set x (and (< a b) (<= b c) (< c d)))"; + reads "mixed chain downward" "x = x >= y > 0" "(set x (and (>= x y) (> y 0)))"; + (* A name is bound too once any operand is, so it is read in its turn. *) + reads "mixed chain over a call" "x = a < f(b) <= c" + "(set x (let [~cmp1 a ~cmp2 (f b) ~cmp3 c] (and (< ~cmp1 ~cmp2) (<= ~cmp2 ~cmp3))))"; + reads "mixed chain over two calls" "x = 0 < f() <= g() < h()" + "(set x (let [~cmp1 (f) ~cmp2 (g) ~cmp3 (h)] (and (< 0 ~cmp1) (<= ~cmp1 ~cmp2) (< ~cmp2 ~cmp3))))"; + refuses "chain that turns around" "x = a < b > c" "indent/mixed-comparison" + "a < b and b > c"; + refuses "== in a chain" "x = a == b < c" "indent/mixed-comparison" "a == b and b < c"; + refuses "== after a chain" "x = x < 1 <= 2 == true" "indent/mixed-comparison" + "x < 1 and 1 <= 2 and 2 == true"; + refuses "a refused chain's middle call is named once" "x = a < f(b) <= g(c) > d" + "indent/mixed-comparison" + "let mid = f(b)\n let mid2 = g(c)\n a < mid and mid <= mid2 and mid2 > d"; (* Statements. *) reads "lets merge" "fn f() -> i32\n let a = 1\n let b = 2\n a + b" "(defn f [] i32 (let [a 1 b 2] (+ a b)))"; @@ -946,6 +964,23 @@ let () = fail "%s: read back %s from %S" name (describe_diff forms back) text | exception e -> fail "%s: its text is refused: %s\n%s" name (diag_text e) text in + round "a mixed chain" "(defn f [r i32 n i32] bool (and (<= 0 r) (< r n)))" "= 0 <= r < n"; + round "an and of one operator stays an and" + "(defn f [r i32 n i32] bool (and (< 0 r) (< r n)))" "= 0 < r and r < n"; + round "an and whose middles differ stays an and" + "(defn f [r i32 n i32] bool (and (<= 0 r) (< n 9)))" "= 0 <= r and n < 9"; + round "a let the reader would not make stays a let" + "(defn f [a i32] bool (let [m (g)] (and (<= a m) (< m (h)))))" " let m = g()"; + back "a chain's middle call gets a name paren text can spell" + "fn f(a, b) -> bool = a < g() <= b" + "(let [mid a mid2 (g) mid3 b] (and (< mid mid2) (<= mid2 mid3)))"; + (* And the paren text prints as the chain again, up to the names. *) + let src = "(defn f [a i32 b i32] bool (let [mid a mid2 (g) mid3 b] (and (< mid mid2) (<= mid2 mid3))))" in + prints "a mixed chain over a call comes back a chain" src " a < g() <= b"; + (let forms = Reader.read_all ~file:"

" src in + let back = Indent_reader.read_all ~file:"

" (Indent_printer.program ~source:src forms) in + if not (same_forms (List.map norm forms) (List.map norm back)) then + fail "a mixed chain over a call: read back %s" (describe_diff forms back)); round "bit operators print infix" "(defn f [a i32 m i32] bool (= (bit-and a (bit-not m)) (bit-or (bit-xor a 1) (<< m 2))))" "a && ~~m == a ^^ 1 || m << 2"; @@ -1447,6 +1482,22 @@ let () = [ "syntax/flat/shadows.flan"; "syntax/flat/macros.flan"; "syntax/flat/capture.flan" ]; run_both "syntax/mixed/main.flan" "12\n12\n0\n55\n"; run_both "syntax/mixed/main.fln" "25\n7\nfar\n3\n"; + (* A chain in a template: its names made where the macro expands in the + paren text, and the chain printed back as one. *) + let src = In_channel.with_open_bin "syntax/chain/macro.fln" In_channel.input_all in + let want = "false\ntrue\nfalse\n" in + run_both "syntax/chain/macro.fln" want; + let forms = Indent_reader.read_all ~file:"syntax/chain/macro.fln" src in + let paren = Paren_printer.program ~source:src forms in + if not (Test_support.contains paren "~(Form.Sym {.s \"~cmp1\"}) ~lo") then + fail "a template's chain in parens: %s" paren; + let flan = Filename.concat scratch (Printf.sprintf "chain-macro-%d.flan" (Unix.getpid ())) in + Out_channel.with_open_bin flan (fun oc -> output_string oc paren); + run_both flan want; + let back = Indent_printer.program ~source:paren (Reader.read_all ~file:flan paren) in + (try Sys.remove flan with Sys_error _ -> ()); + if not (Test_support.contains back "~lo <= ~x < ~hi") then + fail "a template's chain back from parens: %s" back; (* Return types read off the body, in both spellings of [_]. *) List.iter (fun p -> run_both p "3\n2.5\n1.5\n2.5\nyes 0\n4\n0 5\n2\n1\n") From 0b568185695820afa4dc592ba291d61fedf94450 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 12:23:17 +0700 Subject: [PATCH 14/16] Dev bookkeeping per temp allocation grows without free-temp, recorded. --- TODO.org | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TODO.org b/TODO.org index 1725ed97..5a4856fc 100644 --- a/TODO.org +++ b/TODO.org @@ -1548,6 +1548,10 @@ are a dyn vector, except numbers with no common type, which are refused. Rules out the first element typing the rest. * Dev loop +** TODO --dev bookkeeping per temp allocation grows without free-temp +Under --dev each temp allocation (i64->bytes, dyn text crossing into str) costs about +340 bytes of registry notes until free-temp; a loop passing dyn text as str 4M times +without free-temp reaches 2.7 GB. Release stays flat. A CLI that never frees temp hits it. ** WAIT A _ caller whose type follows a redefined callee Its signature changes in the session but its body is not recompiled, so every call From 56b254d81c6c860a2ced735e800a2616c2bbc77c Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 12:33:00 +0700 Subject: [PATCH 15/16] A typed char is queued behind the dyn char and literal inference lanes. --- TODO.org | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/TODO.org b/TODO.org index 5a4856fc..8c1abc3d 100644 --- a/TODO.org +++ b/TODO.org @@ -10,6 +10,12 @@ pointing at it. A CANCELLED entry carries the one-line reason, because an idea rejected without a record is an idea that gets re-proposed. * Language surface +** NEXT A typed char +Decided 2026-09-26 (127): =char= is a typed code point. A char literal is typed by local +inference like a number literal: u8 or i32 where typed code wants a number (a literal +above 127 is refused as a u8), =char= otherwise; a =char= crossing into dyn stays a char. +Rules out the fork where =f(\a)= printed =\a= and =let c = \a= then =f(c)= printed 97. +Waits on the dyn char lane and the literal inference lane. ** NEXT if let Decided 2026-09-26 (126), Rust's spelling: =if let Some(g) = left= plus a block tests the pattern and binds =g= in that block only; =elif=/=else= follow as for =if=. Any From 473a9528b96dca07f938ddb747af7b91ef7a6496 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 12:47:34 +0700 Subject: [PATCH 16/16] A lane runs only the tests its change touches, and one tester runs the full suite after a batch of merges. --- CLAUDE.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2383974f..19e056e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,10 +23,12 @@ saying what is now true, not what was done. ## Tests -`dune test --root .` must be green before a lane reports; grep its output for -FAIL, since the exit code alone has lied. `@checks` (`@page`, `@x86`, `@cells`), -`@sanitize` and `@valgrind` are slow and run once between batches of lanes, with -the author's permission, never inside a lane. ASan misses uninitialised stack +A lane never runs the full `dune test`: it builds with `-j 2` and runs only the +programs and test executables its change touches, one at a time, and lists them in +its report. After about five lanes merge, one tester agent runs `dune test --root .` +on master and fixes what broke; grep its output for FAIL, since the exit code alone +has lied. `@checks` (`@page`, `@x86`, `@cells`), `@sanitize` and `@valgrind` are +slow and run only with the author's permission. ASan misses uninitialised stack reads; `@valgrind` catches them. ## Evidence