diff --git a/TODO.org b/TODO.org
index 867e46b1..72d9a41b 100644
--- a/TODO.org
+++ b/TODO.org
@@ -630,12 +630,6 @@ generic binding — saying =$= marks a
type variable and naming the bare spelling. A =defn= parameter was already
refused, as a type in a name slot.
-** NEXT A container parameter the function grows is warned at
-Decided 2026-09-25: Odin's behaviour stays — a Vec or Map passed by value is a
-copy of its header, so growth inside the callee does not reach the caller. A
-parameter the function grows (push, put, reserve, anything that can reallocate)
-gets a warning at the parameter suggesting (Ptr ...).
-
** CANCELLED not= as a spelling of !=
CLOSED: [2026-09-25]
One spelling for one operation; != stays, and not= is refused with a suggestion
@@ -680,14 +674,6 @@ A machine-type target needs =numeric?=; an enum target needs =integer?=;
by what it claims, not by the set it happens to denote this week — which is why
=ordered?= is refused even though every type it admits today converts.
-** NEXT There is now no generic enum to integer conversion
-Decided 2026-09-25: build =enum?= as described.
-Recorded as a loss. The one spelling that worked did so by not asking about the
-operand at all, so removing it was still right. =enum?= is the eventual answer —
-it would entail =ordered?= and =equal?= and not =numeric?=, so the cast rule
-becomes a disjunction and the refusal has to name whichever the reader meant. Each
-part of that is a decision and the author has not been asked.
-
** DONE The Ptr and union arms of the fill boundary are relaxable
CLOSED: [2026-09-25]
A =Ptr= may be byte-filled, and an untagged union is filled over its whole
@@ -856,34 +842,12 @@ CLOSED: [2026-09-20]
typed conditions stay strict =bool=. =and= and =or= hand back the operand that
decided them, Clojure's rule, through a desugaring that evaluates each test once.
-** NEXT A bool arm and a dyn arm joining as dyn
-Decided 2026-09-25: they join as =dyn=, the =bool= boxed — Clojure's rule, so =(or false (box "s"))= answers ="s"=.
-With both arms of a desugared =and=/=or= holding real values, a non-bool =dyn= on
-the losing side meets the strict =bool= boundary and traps —
-=(or false (box "s"))= is the case. Whether a =bool= arm and a =dyn= arm should
-join as =dyn= is the author's call and is not settled.
-
-** NEXT A truthiness failure re-runs the whole failing subtree
-Decided 2026-09-25: fix it without changing any message — the retry reuses what the first pass settled for each subtree (memoised by node), so nested =not= is linear. Test with a deep nest that must fail fast and with the existing message tests unchanged.
-The retry exists to keep a refused literal's message unchanged and re-runs the
-subtree rather than the leaf, which is exponential in nested =not= depth on a
-program that does not type-check. Moot for anything that compiles; only the
-daemon's half-typed recompiles could feel it. A cheaper retry was tried and
-shelved because it changes which literal gets the nicer message.
-
** DONE and's last operand gets a misdirected caret
CLOSED: [2026-09-25]
Already fixed by 3672da2, which blames the arm that is not a compiler temp; the
caret is on the last operand and =test/test_flan.ml= asserts its column. Rules
out relabelling the else arm, a bool sentinel, and inverting the condition.
-** NEXT Signature pairing's cold-rebuild edge
-Decided 2026-09-25: the type takes precedence, as today. The warning is at the parameter site: where a name in a parameter vector is read as a program-declared type but could also have been read as a parameter name, the parameter vector gets a warning naming the type and where it is declared.
-Whether a parameter vector reads as one annotated parameter or two dyn ones
-depends on what type names exist, so adding a type can silently re-pair an
-existing signature between compiles. A changed-pairing warning was proposed and
-not queued.
-
** DONE A typed container crosses into dyn as a view, and only from permanent storage
CLOSED: [2026-09-20]
The descriptor is pointer, length and element type — a slice plus the piece a
@@ -1006,13 +970,10 @@ at all, which is what the diagnosis predicted. A =map= that *changes* the elemen
type is the one shape that did not come with them: one copy per ordered pair of
types rather than per type.
-** NEXT CFn in a struct or a fixed array
-Decided 2026-09-25: allowed. A call through a null =CFn= is a named runtime condition on both backends, and parks in a dev build.
-A zeroed function value is a null pointer, so a function value is refused in any
-position zero-initialisation would conjure one — =CFn= included. An =(Option
-(CFn ...))= field is already legal. A table of function pointers is exactly what
-=CFn= is for, and the objection is about zero-initialisation rather than about
-capture.
+** DONE CFn in a struct or a fixed array
+CLOSED: [2026-09-25]
+A zeroed =CFn= is admitted everywhere and a call through a null one signals
+=NullCall= before its arguments run. =(Fn ...)= stays refused in those positions.
** DONE Structural compatibility is identical layout
Same fields, same types, same order, so structural compatibility is "the same
@@ -1655,12 +1616,6 @@ a defcustom.
The agent keeps the condition pointer beside its name and a verb hands it back, so
the editor can render the condition's own fields rather than only its class.
-** NEXT The type identity of a local is not qualified
-Decided 2026-09-25: a local's type prints package-qualified in the break buffer and the inspector, as a field's and a condition's already do.
-Settled for conditions and for structs, because =Load= qualifies every declaration
-at import. Still open for locals, where the debug information gives a bare name and
-nothing qualifies it.
-
** NEXT The render-thunk-per-inspection design
Decided 2026-09-25: the inspector reads a value through the type layouts the compiler records, with no compile per inspection, which lets it hold a value.
An inspection still compiles a thunk per request. A redesign rather than a
@@ -1930,21 +1885,6 @@ CLOSED: [2026-09-25]
=put= on an instance still checks a declared slot's type and still inserts an
undeclared key; only =set= refuses one, since a slot it writes has to exist.
-** NEXT update: change a place by applying a function to it
-Decided 2026-09-25: every place evaluates each of its subexpressions once, C's compound-assignment rule, which also fixes =++= and =--=; =update= is built on that. Rules out refusing side effects in a place.
-=(set (.velocity g) (inc (.velocity g)))= names the place twice. Clojure's
-=update= would be a macro over the same two steps, for a struct field and a
-class slot alike.
-
-Blocked on the double-evaluation question, which =++=, =--= and any
-compound assignment share: =(update (at grid (next-index) c) inc)= evaluates
-=(next-index)= twice, and a place with a side effect is then wrong rather than
-slow. Either places get a general single-evaluation rule — bind every
-subexpression of a place to a temp once, which is what C's compound assignment
-does — or the language says a place must be side-effect free and refuses
-otherwise. The first is the real fix and it is a change to how every place
-lowers, not to one macro.
-
** TODO A session eval reported (CFn [] ()) does not cross into dyn yet
At =sand.flan:46:20=, the =:pause= in =(when (get state :pause) (return))=,
where =state= is a =defclass= instance with a =pause= slot. =(CFn [] ())= is
diff --git a/docs/BUILT.md b/docs/BUILT.md
index 2a4071b0..5a8a18b8 100644
--- a/docs/BUILT.md
+++ b/docs/BUILT.md
@@ -3890,7 +3890,7 @@ before this landed, so `macro-unless.flan` is a test written after the feature.
### The line between a special form and a macro
A form the prelude itself relies on is built into the parser: `cond`, `when` and `dotimes`. A form only programs use
-is a prelude macro: `inc`, `++`, `into`, `unless`, `until` and `comment`. The reason is `Macro.reduce`: a prelude
+is a prelude macro: `inc`, `++`, `update`, `into`, `unless`, `until` and `comment`. The reason is `Macro.reduce`: a prelude
function that calls a macro is left out of the module that runs macros, so a form the prelude's own functions use
cannot be a macro without taking those functions away from every macro body. `until` peels an optional leading label
and answers `(while :label (not test) body ...)`.
@@ -4339,8 +4339,9 @@ implemented.
refusal's witness now runs. The escape refusal that replaced it is gone too; see "Escape: only an escaping
closure's environment is the collector's".*
- **An `fn` with nothing to say what it takes** (`fn-no-type.flan`), above.
-- **A position that would zero one** (`fn-in-struct.flan`): a struct field, a global, a fixed array's element,
- `(zeroed)`. ZII fills an omitted field with all-bytes-zero, and **a zeroed function value is a null pointer, which
+- **A position that would zero an `(Fn ...)`** (`fn-in-struct.flan`): a struct field, a global, a fixed array's element,
+ `(zeroed)`. A `(CFn ...)` is admitted in all four: every call through one tests for null and signals `NullCall`
+ (`fn-cfn-table.flan`). ZII fills an omitted field with all-bytes-zero, and **a zeroed function value is a null pointer, which
is the one kind of zero that is not a value the type can have** — every other type's zero is one: `0`, `false`, an
empty slice, `None`, a union's first case. A parameter, a return type and a `let` binding are not on the list
because none of them is ever conjured, and an `(Option (Fn ...))` is not either, because a `None`'s tag is what
diff --git a/lib/check.ml b/lib/check.ml
index f0c7d623..aede389e 100644
--- a/lib/check.ml
+++ b/lib/check.ml
@@ -891,18 +891,24 @@ let no_such_rand name =
Odin's [where] clause is the same shape ([core/slice/slice.odin:289] is
[where intrinsics.type_is_ordered(T)]) with forty-one predicates against
- these five. There is no [copyable?] any more and no Odin counterpart
+ these six. There is no [copyable?] any more and no Odin counterpart
either: Odin has no move semantics, and since the repeal neither does this
language, so [$T] never has to answer the question.
- [integer?] is the narrowest of the five and exists because [numeric?] was
+ [integer?] is the narrowest numeric bound and exists because [numeric?] was
one type too wide for a family of bodies: an integer body under [numeric?]
is instantiated at f32 and f64 too, and (if (< x 0) (- 0 x) x) at -0.0 is
the wrong abs while %, the bitwise operators and the shifts have no float
meaning at all. A function that can be generalized should not need a
variant per numeric type, and [integer?] is what lets the integer-only
- ones say exactly what they need. *)
-let predicate_names = [ "ordered?"; "equal?"; "hashable?"; "numeric?"; "integer?" ]
+ ones say exactly what they need.
+
+ [enum?] admits exactly the enums. It entails [ordered?] and [equal?] and
+ not [numeric?]: an enum compares, and it converts to a number, but it is
+ not one — no arithmetic, no literal. It is what licenses the generic
+ enum-to-number conversion, beside [numeric?]. *)
+let predicate_names =
+ [ "ordered?"; "equal?"; "hashable?"; "numeric?"; "integer?"; "enum?" ]
(* ── What a type owns, transitively ────────────────────────────────────
The one structural ownership question that survived the repeal, because it
@@ -1041,6 +1047,7 @@ let pred_holds p (t : Types.t) =
| "hashable?" -> Types.keyable t
| "numeric?" -> Types.is_numeric t
| "integer?" -> Types.is_integer t
+ | "enum?" -> (match t with Types.Enum _ -> true | _ -> false)
| _ -> false
(* What one declared predicate *also* gives you. These are entailments over
@@ -1053,8 +1060,8 @@ let pred_holds p (t : Types.t) =
let pred_entails ~declared ~wanted =
String.equal declared wanted
|| match wanted, declared with
- | "ordered?", ("numeric?" | "integer?") -> true
- | "equal?", ("numeric?" | "ordered?" | "integer?") -> true
+ | "ordered?", ("numeric?" | "integer?" | "enum?") -> true
+ | "equal?", ("numeric?" | "ordered?" | "integer?" | "enum?") -> true
(* Every integer type is a number, so [integer?] gives a body everything
[numeric?] does — the arithmetic, the written 0, the untyped integer
literal — on top of the operations only it admits. The reverse is
@@ -1145,12 +1152,19 @@ let fn_sig (t : Types.t) =
let callable_ty t = fn_sig t <> None
+(* A (CFn ...) is not on the list: it is one code address, and every call
+ through one tests for null and signals NullCall (see [Emit.null_check]), so
+ a zeroed one is an empty slot rather than a crash. That is what lets a table
+ of function pointers be a struct or a fixed array. An (Fn ...) stays
+ refused: a call through one is not tested, and (Option (Fn ...)) is the
+ field that holds one. *)
let rec no_zeroed_fn loc what (t : Types.t) =
match t with
- | Types.Fn _ | Types.CFn _ ->
+ | Types.Fn _ ->
fail loc
"%s cannot be %s — it would be zeroed, and a zeroed function value is a \
- null pointer. Pass it as a parameter, or hold it in a let"
+ null pointer. Pass it as a parameter, hold it in a let, or store a \
+ (CFn ...) if it captures nothing"
what (Types.to_string t)
| Types.Array (_, e) -> no_zeroed_fn loc what e
| _ -> ()
@@ -1635,9 +1649,37 @@ let dyn_param_or_typo env n loc =
parameters are lowercase"
n
-let pair_params ?(also = fun _ -> false) env (items : Ast.pitem list)
- : Ast.field list =
- let is_type_name env n = is_type_name env n || also n in
+(* The pairings a parameter vector owes to a type the program declares under
+ a name that is also a legal parameter name — a lowercase one, since a
+ capitalised name is refused as a parameter. [(defn f [p point] ...)] is one
+ parameter while [point] is a type and two dyn ones the moment it is not,
+ so adding or removing the type re-pairs the signature with no edit to it.
+ The type still wins; this is the warning at the parameter, filled by
+ [pair_decls] and printed by [build_program] with the other warnings. *)
+let pairing_warnings : Loc.diag list ref = ref []
+
+let pair_params ?(also = fun _ -> false) ?(declared = fun _ -> None)
+ ?(hide = fun _ -> false) env (items : Ast.pitem list) : Ast.field list =
+ let is_type_name env n = (is_type_name env n && not (hide n)) || also n in
+ let warn_pairing n t tloc =
+ let bare =
+ match String.rindex_opt t '/' with
+ | Some i -> String.sub t (i + 1) (String.length t - i - 1)
+ | None -> t
+ in
+ match declared t with
+ | Some (what, (at : Loc.t))
+ when bare <> "" && bare.[0] >= 'a' && bare.[0] <= 'z' ->
+ pairing_warnings :=
+ Loc.diag ~kind:"check/parameter-reads-a-type" tloc
+ (Printf.sprintf
+ "[%s %s] is one parameter %s of type %s, the %s declared at %s, \
+ and not two dyn parameters. If two were meant, give the second \
+ a name no type has"
+ n t n t what (Loc.to_string at))
+ :: !pairing_warnings
+ | _ -> ()
+ in
let dyn loc = { Ast.t = Ast.Tname "dyn"; tloc = loc } in
let rec go = function
| [] -> []
@@ -1653,6 +1695,7 @@ let pair_params ?(also = fun _ -> false) env (items : Ast.pitem list)
| Ast.Pname (n, loc) :: Ast.Ptype t :: rest ->
{ Ast.fname = n; fty = t; floc = loc } :: go rest
| Ast.Pname (n, loc) :: Ast.Pname (t, tloc) :: rest when is_type_name env t ->
+ warn_pairing n t tloc;
{ Ast.fname = n; fty = { Ast.t = Ast.Tname t; tloc }; floc = loc } :: go rest
(* The slot after this one is not a type, so this one is a parameter with
no type written — unless the slot after it only *looks* unlike a type
@@ -1782,13 +1825,42 @@ let pair_decls env (decls : Ast.decl list) : Ast.decl list =
match d.Ast.d with Ast.Defclass (n, _) -> Some n | _ -> None)
decls
in
- let fn (f : Ast.fn) =
+ (* The types the program declares, with what kind and where, for
+ [pair_params]'s warning. The prelude's are left out: its names are the
+ language's, not a declaration the reader made. *)
+ let types = Hashtbl.create 16 in
+ List.iter
+ (fun (d : Ast.decl) ->
+ let add n what =
+ if d.Ast.dloc.Loc.file <> Prelude.file
+ && not (List.mem n Types.primitive_names) then
+ Hashtbl.replace types n (what, d.Ast.dloc)
+ in
+ match d.Ast.d with
+ | Ast.Defstruct (n, _, _) -> add n "struct"
+ | Ast.Defenum (n, _) -> add n "enum"
+ | Ast.Defalias (n, _) -> add n "alias"
+ | Ast.Defdata (n, _) -> add n "data type"
+ | Ast.Defunion (n, _) -> add n "union"
+ | _ -> ())
+ decls;
+ pairing_warnings := [];
+ (* A prelude signature is paired against the prelude's types alone: a
+ program's type named [t] must not turn the prelude's parameter [t] into
+ a type. *)
+ let fn ~prelude (f : Ast.fn) =
match f.Ast.praw with
| None -> f
- | Some items -> { f with Ast.params = pair_params env items; praw = None }
+ | Some items ->
+ let hide n = prelude && Hashtbl.mem types n in
+ { f with
+ Ast.params =
+ pair_params ~hide ~declared:(Hashtbl.find_opt types) env items;
+ praw = None }
in
List.map
(fun (d : Ast.decl) ->
+ let prelude = String.equal d.Ast.dloc.Loc.file Prelude.file in
match d.Ast.d with
(* A class's slot vector is paired here and nowhere earlier, for the
reason a [defn]'s is, and its constructor is written from the
@@ -1802,9 +1874,9 @@ let pair_decls env (decls : Ast.decl list) : Ast.decl list =
(f.Ast.fname, slot_of env ~classes n f.Ast.fname f.Ast.fty))
slots);
Classes.constructor n slots d.Ast.dloc
- | Ast.Defn f -> { d with Ast.d = Ast.Defn (fn f) }
- | Ast.Declare (f, c) -> { d with Ast.d = Ast.Declare (fn f, c) }
- | Ast.DeclareC (f, c) -> { d with Ast.d = Ast.DeclareC (fn f, c) }
+ | Ast.Defn f -> { d with Ast.d = Ast.Defn (fn ~prelude f) }
+ | Ast.Declare (f, c) -> { d with Ast.d = Ast.Declare (fn ~prelude f, c) }
+ | Ast.DeclareC (f, c) -> { d with Ast.d = Ast.DeclareC (fn ~prelude f, c) }
| _ -> d)
decls
@@ -2447,6 +2519,49 @@ and const_steps ro (ty : Types.t) n =
let const_copy env (e : Types.t) =
if owning env e || holds_dyn env e then None else Some "(clone v)"
+(* Whether (clone x) accepts a value of this type — the same arms the clone
+ builtin takes: a Vec or a Map whose elements own nothing, or a slice whose
+ elements neither own storage nor hold a dyn. *)
+let clone_accepts env (t : Types.t) =
+ match t with
+ | Types.Vec _ | Types.Map _ -> not (region_only env t)
+ | Types.Slice (_, e) -> not (owning env e || holds_dyn env e)
+ | _ -> false
+
+(* The end of clone's refusal for a container of owning elements. Pushing
+ the elements themselves into a second container would copy their headers
+ and share their blocks, so the advice is a copy of each element where
+ clone takes one, and otherwise that there is no copy to make. *)
+let insert_copies env (t : Types.t) =
+ let elem =
+ match t with
+ | Types.Vec e | Types.Slice (_, e) | Types.Map (_, e) -> Some e
+ | _ -> None
+ in
+ match elem with
+ | Some e when clone_accepts env e ->
+ "Build a second container and push a (clone x) of each element into it"
+ | Some e ->
+ Printf.sprintf
+ "Nothing copies what a %s owns either, so read the elements where they \
+ are"
+ (Types.to_string e)
+ | None -> "Build a second container and insert into it"
+
+(* A call written back out as source, for a fix that has to repeat what the
+ reader wrote: names, integers and calls of those. Anything else is [None]
+ and the caller says the fix in words. *)
+let rec spell_form (a : Ast.expr) =
+ match a.Ast.e with
+ | Ast.Var v -> Some v
+ | Ast.Int n -> Some (Int64.to_string n)
+ | Ast.UInt (_, s) -> Some s
+ | Ast.Call (f, args) ->
+ let parts = List.map spell_form (f :: args) in
+ if List.mem None parts then None
+ else Some ("(" ^ String.concat " " (List.filter_map Fun.id parts) ^ ")")
+ | _ -> None
+
(* A store through a read-only view: a [[const T]] or a (Ptr const T). *)
let refuse_const_place env loc (view : Types.t) =
match view with
@@ -4227,6 +4342,101 @@ let tracked_call loc env name (tr : Shim.track) ret (args : Tast.expr list) =
(* Every expression goes through here, and [check_value] is the one that
knows the forms. What this adds is [refuse_owned_copy], asked of whatever
came back unless the form was checked as the target of a place. *)
+
+(* The conditions [check_truthy] has refused, each with the body it was
+ checked in and its diagnostic, for as long as the outermost call is on the
+ stack — see [check_truthy]. Physical identity on both, since a generic's
+ body is the same syntax checked again at another type. *)
+let truthy_failed :
+ (Ast.expr * (string * binding) list * Types.t * Loc.diag) list ref = ref []
+
+(* Whether two scopes bind the same names at the same types, which is what a
+ refusal under them can depend on — the slots are fresh on every pass. A
+ memo keyed on less would replay a refusal after a retry changed a type. *)
+let same_scope (a : (string * binding) list) (b : (string * binding) list) =
+ a == b
+ || List.equal
+ (fun (n, (x : binding)) (m, (y : binding)) ->
+ String.equal n m && Types.equal x.bty y.bty)
+ a b
+let truthy_depth = ref 0
+
+(* The same for an [if], keyed on its condition and the expectation:
+ an [if] whose else arm is tried on its own terms first (see [check_if])
+ would otherwise be re-checked, refused, by the trial of every [if] above
+ it — the square of a refused or/and chain's length. *)
+let if_failed :
+ (Loc.t,
+ Ast.expr * ((string * binding) list * Types.t) * Types.t option * Loc.diag)
+ Hashtbl.t =
+ Hashtbl.create 16
+let if_depth = ref 0
+
+
+(* A Vec or a Map parameter is a copy of the caller's header — Odin's rule —
+ so growing it reallocates a block only this function's copy points at, and
+ the caller's container never sees the elements. The function being checked
+ and its container parameters, by slot, and the warnings found so far, one
+ per parameter, printed by [build_program]. A stack because a generic's copy
+ is checked from inside the body that called it. *)
+let grow_params : (ctx * (int * Ast.field) list) list ref = ref []
+let grow_warnings : Loc.diag list ref = ref []
+
+let note_grown ctx op loc (target : Tast.expr) =
+ (* The parameter the container is reached from, through struct fields
+ taken by value — a field of a parameter is in the parameter's copy too —
+ and the path written back out. A [Deref] ends the walk: through a
+ pointer the caller's own storage is what grows. *)
+ let rec root (e : Tast.expr) =
+ match e.Tast.e with
+ | Tast.Local s -> Some (s, fun p -> p)
+ | Tast.Field (inner, i) ->
+ (match inner.Tast.ty with
+ | Types.Named n ->
+ (match Hashtbl.find_opt ctx.env.structs n with
+ | Some st when i < List.length st.Tast.fields ->
+ let f = (List.nth st.Tast.fields i).Tast.fname in
+ Option.map
+ (fun (s, path) -> (s, fun p -> Printf.sprintf "(.%s %s)" f (path p)))
+ (root inner)
+ | _ -> None)
+ | _ -> None)
+ | _ -> None
+ in
+ match target.Tast.ty, root target, !grow_params with
+ | ((Types.Vec _ | Types.Map _) as t), Some (s, path), (c, ps) :: _ when c == ctx ->
+ (match List.assoc_opt s ps with
+ | Some (p : Ast.field)
+ when not
+ (List.exists
+ (fun (d : Loc.diag) -> d.Loc.dloc = p.Ast.floc)
+ !grow_warnings) ->
+ let ts = Types.to_string t in
+ let msg =
+ match target.Tast.e with
+ | Tast.Local _ ->
+ Printf.sprintf
+ "%s is a %s passed by value, a copy of the caller's header, so \
+ the %s at %s grows this function's copy and the caller's \
+ container never sees it. Take it as (Ptr %s) and write (%s \
+ (deref %s) ...), and each caller passes (addr c) for its \
+ container c"
+ p.Ast.fname ts op (Loc.to_string loc) ts op p.Ast.fname
+ | _ ->
+ let pt = Types.to_string (List.nth ctx.slot_tys (ctx.slots - 1 - s)) in
+ Printf.sprintf
+ "%s is a %s passed by value, a copy of the caller's, so the %s \
+ at %s grows %s in this function's copy and the caller's never \
+ sees it. Take it as (Ptr %s), where %s reaches the caller's own, \
+ and each caller passes (addr c) for its %s c"
+ p.Ast.fname pt op (Loc.to_string loc) (path p.Ast.fname) pt
+ (path p.Ast.fname) pt
+ in
+ grow_warnings :=
+ Loc.diag ~kind:"check/grown-parameter" p.Ast.floc msg :: !grow_warnings
+ | _ -> ())
+ | _ -> ()
+
(* Recovery, when [env.recovering] is on: a subexpression that is refused is
recorded and stands as a [poison] of type [Never], which fits any want, so
checking carries on around it and every error in a body is reported. What
@@ -6089,12 +6299,12 @@ and check_recur ctx ~tail loc args =
literals gets the nicer message, not just the speed. The cost that
buys is real: nested [not] on a program that does not type-check re-runs
this whole function once per level of nesting inside the level above it,
- which is exponential in how deep the nesting goes — moot for a program
- that compiles, since neither retry ever fires, and moot for ordinary
- nesting depths, but visible within a second or so around twenty levels
- of a [not] wrapped in a [not] wrapped in .... The dev daemon is the one
- caller that could feel this, recompiling a half-typed form on every
- edit; nobody has hit it in practice and it is not fixed here.
+ which would be exponential in how deep the nesting goes. What keeps it
+ linear is [truthy_failed]: a condition this function has already refused,
+ in the same body, is refused again with the same diagnostic rather than
+ re-checked, so a retry re-walks its subtree once and stops at the first
+ condition below it that was settled. The message is the one the first
+ pass produced, so no message changes.
Keywords are a separate, deliberate loss rather than a bug: a bare
[:kw] used to be checked here with [want:Types.Bool] from the start, so
@@ -6109,6 +6319,30 @@ and check_recur ctx ~tail loc args =
test_flan.ml pins the new answer down so it is not lost again by
accident. *)
and check_truthy ctx c =
+ (* Only where a refusal is raised: while recovering, one is recorded where
+ it happens and the check goes on, so a replayed one would hide others. *)
+ if ctx.env.recovering && ctx.env.speculating = 0 then check_truthy_once ctx c
+ else
+ match
+ List.find_opt
+ (fun (n, sc, r, _) -> n == c && r == ctx.ret && same_scope sc ctx.scope)
+ !truthy_failed
+ with
+ | Some (_, _, _, d) -> raise (Loc.Error d)
+ | None ->
+ let scope = ctx.scope in
+ incr truthy_depth;
+ Fun.protect
+ ~finally:(fun () ->
+ decr truthy_depth;
+ if !truthy_depth = 0 then truthy_failed := [])
+ (fun () ->
+ try check_truthy_once ctx c
+ with Loc.Error d as ex ->
+ truthy_failed := (c, scope, ctx.ret, d) :: !truthy_failed;
+ raise ex)
+
+and check_truthy_once ctx c =
let loc = c.Ast.loc in
(* Speculative, because a refusal here is answered by asking again at
[bool]; and that second ask is guarded, because its refusal is re-worded
@@ -6163,6 +6397,30 @@ and check_truthy ctx c =
| exception Loc.Error _ -> check ctx ~want:Types.Bool c
and check_if ctx ?(tail = false) ?want loc c t e =
+ if ctx.env.recovering && ctx.env.speculating = 0 then
+ check_if_once ctx ~tail ?want loc c t e
+ else
+ match
+ List.find_opt
+ (fun (n, (sc, r), w, _) ->
+ n == c && r == ctx.ret && w = want && same_scope sc ctx.scope)
+ (Hashtbl.find_all if_failed c.Ast.loc)
+ with
+ | Some (_, _, _, d) -> raise (Loc.Error d)
+ | None ->
+ let scope = ctx.scope in
+ incr if_depth;
+ Fun.protect
+ ~finally:(fun () ->
+ decr if_depth;
+ if !if_depth = 0 then Hashtbl.reset if_failed)
+ (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);
+ raise ex)
+
+and check_if_once ctx ~tail ?want loc c t e =
let c = check_truthy ctx c in
(* Both arms are the tail, and a one-armed [if] counts: [(when c (recur ...))]
is how nearly every loop is written, and the branch is still the last
@@ -6206,6 +6464,34 @@ and check_if ctx ?(tail = false) ?want loc c t e =
want = None
&& (match t.Tast.ty with Types.Slice _ | Types.Ptr _ -> true | _ -> false)
in
+ (* A bool arm and a dyn arm meet at dyn, the bool boxed — Clojure's rule,
+ so (or false (box "s")) answers "s" rather than unboxing the string at
+ bool and trapping. The other order already met at dyn, the then arm
+ deciding. So after a bool then arm the else arm is checked on its own
+ terms first, since checking it at bool is what unboxes it, and kept
+ when it is a bool or a dyn. Anything else is abandoned and checked at
+ bool as before, for that path's messages. A chain whose arms all fit
+ is checked once; a refused one re-checks each level below the refusal
+ once more, the square of its depth. *)
+ let own_else =
+ if want = None && t.Tast.ty = Types.Bool then
+ match
+ trial ctx (fun () ->
+ let v = branch ctx (fun () -> in_tail (fun () -> check ctx e)) in
+ match v.Tast.ty with
+ | Types.Bool | Types.Dyn | Types.Never -> v
+ | _ -> raise (Loc.Error (Loc.diag v.Tast.loc "not bool or dyn")))
+ with
+ | Ok v -> Some v
+ | Error _ -> None
+ else None
+ in
+ let t =
+ match own_else with
+ | Some v when v.Tast.ty = Types.Dyn ->
+ expect ctx t.Tast.loc ~want:(Some Types.Dyn) t
+ | _ -> t
+ in
let ewant =
match want with
| Some _ -> want
@@ -6227,6 +6513,9 @@ and check_if ctx ?(tail = false) ?want loc c t e =
location; and with an expectation in hand both arms are checked against
it rather than against each other, so nothing here runs. *)
let e =
+ match own_else with
+ | Some v -> v
+ | None ->
let reworded = want = None && and_sentinel e in
match
branch ctx (fun () ->
@@ -7972,9 +8261,16 @@ and not_numeric name what (a : Tast.expr) =
so that the family says it one way: a body with no clause is given the
whole clause, and a body that already has one is told which predicate to
add rather than a clause that would drop the ones it has. *)
-and cast_operand ctx loc name ~needs ~what ~is v =
- if declares ctx.env.tvpreds v needs then ()
+and cast_operand ctx loc name ~needs ?also ~what ~is v =
+ if declares ctx.env.tvpreds v needs
+ || (match also with
+ | Some (p, _) -> declares ctx.env.tvpreds v p
+ | None -> false)
+ then ()
else
+ (* A conversion two bounds license is refused naming both, since which one
+ the reader meant is theirs to say. *)
+ let is = match also with Some (_, is') -> is ^ " or " ^ is' | None -> is in
let declared =
List.filter_map
(fun (p : Ast.pred) ->
@@ -7990,10 +8286,19 @@ and cast_operand ctx loc name ~needs ~what ~is v =
(String.concat " and " ps) is
in
let fix =
+ let alt clause =
+ match also with
+ | Some (p, is') ->
+ Printf.sprintf ", or %s for %s"
+ (Printf.sprintf clause p v) is'
+ | None -> ""
+ in
if ctx.env.tvpreds = [] then
- Printf.sprintf "write {:where (%s $%s)} at the head of the body"
- needs v
- else Printf.sprintf "add (%s $%s) to the where clause" needs v
+ Printf.sprintf "write {:where (%s $%s)} at the head of the body%s"
+ needs v (alt "{:where (%s $%s)}")
+ else
+ Printf.sprintf "add (%s $%s) to the where clause%s" needs v
+ (alt "(%s $%s)")
in
Loc.failk "check/unconstrained-type-variable" loc
"%s converts %s. %s — %s" name what known fix
@@ -8288,14 +8593,20 @@ and file_guard ctx loc ~path_slot ~op mk_steps =
missing annotation for a program that had written one. One list, read by
both callers, so the next kind of type added cannot be added to one of
them. *)
+(* A global value that a bare name in a type position would reach instead of
+ a type. A type variable in scope is not shadowed by one: the prelude's
+ generics write [(vec-new t)], and a program's [(defonce t ...)] must not
+ change what the prelude means. *)
+and global_value ctx n =
+ Hashtbl.mem ctx.env.globals n && not (tyvar_in_scope ctx.env n)
+
(* An argument written as a type: a type expression, or a bare name that is a
type and not a local or a global of the same spelling. *)
and type_arg ctx (a : Ast.expr) =
type_of_expr a <> None
|| (match a.Ast.e with
| Ast.Var n ->
- lookup ctx n = None && (not (Hashtbl.mem ctx.env.globals n))
- && type_named ctx n
+ lookup ctx n = None && not (global_value ctx n) && type_named ctx n
| _ -> false)
and type_named ctx n =
@@ -8328,9 +8639,7 @@ and vec_new_elem ctx ~want loc args =
| a :: rest when type_of_expr a <> None ->
Some (resolve ctx.env (Option.get (type_of_expr a)), rest)
| { Ast.e = Ast.Var n; _ } :: rest
- when lookup ctx n = None
- && (not (Hashtbl.mem ctx.env.globals n))
- && type_named ctx n ->
+ when lookup ctx n = None && not (global_value ctx n) && type_named ctx n ->
Some (resolve_name ctx.env ~seen:[] loc n, rest)
| _ -> None
in
@@ -8393,9 +8702,7 @@ and map_kv loc what (t : Types.t) =
says half of a type and half is not a type. *)
and map_new_types ctx ~want loc args =
let is_type n =
- lookup ctx n = None
- && (not (Hashtbl.mem ctx.env.globals n))
- && type_named ctx n
+ lookup ctx n = None && not (global_value ctx n) && type_named ctx n
in
(* A type position holds a bare name or a type expression Parse has read
as one, as [vec-new]'s does. *)
@@ -9324,6 +9631,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
[ target; check ctx ~want:Types.Dyn x; here loc ])
else begin
let elem = vec_elem loc "push" target.Tast.ty in
+ note_grown ctx "push" loc target;
let x = check ctx ~want:elem x in
(* The element is bound before the loop so that a [retry] re-attempts
the allocation and not the expression that produced the value. *)
@@ -9355,6 +9663,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
let target = check_target ctx target in
refuse_const_change ctx loc target;
let n = check ctx ~want:index_ty n in
+ note_grown ctx "reserve" loc target;
let n64 =
mk loc (Types.Int Types.I64) (Tast.Prim (Tast.Cast (Types.Int Types.I64), [ n ]))
in
@@ -9482,6 +9791,57 @@ and named_call ?(qualified = false) ctx ~want loc name args =
"free takes a Vec, a Map, or a slice (bytes s) or (clone xs) made — \
found %s"
(Types.to_string other))
+ (* Emitted by the prelude's [into] when no (map f) is in the chain, so that
+ every element pushed is a source element as it stands. A push copies an
+ element's header, and for an element that owns storage the copy and the
+ source then share one block: growing an element through either side
+ reallocates it and frees the block the other still points at. That is a
+ use after free the program never wrote, under a name that promised a
+ copy, so it is refused here. A bare (push w (at v 0)) is not: it copies a
+ header in plain sight, the Odin contract every container follows.
+ Arguments are the source, then the destination and the transforms as
+ written — those two only to be spelled back in the fix, never checked. *)
+ | "into-copies-elements" ->
+ (match args with
+ | src :: dst :: transforms ->
+ let s = check ctx src in
+ let elem =
+ match s.Tast.ty with
+ | Types.Vec e | Types.Slice (_, e) | Types.Array (_, e) -> Some e
+ | _ -> None
+ in
+ (match elem with
+ | Some e when owning ctx.env e ->
+ let v = spell_arg "v" src in
+ let et = Types.to_string e in
+ let fix =
+ if clone_accepts ctx.env e then
+ match spell_form dst, List.map spell_form transforms with
+ | Some d, ts when not (List.mem None ts) ->
+ Printf.sprintf
+ "Add (map clone) to the chain, which copies what each \
+ element owns: (into %s)"
+ (String.concat " "
+ ((v :: d :: List.filter_map Fun.id ts) @ [ "(map clone)" ]))
+ | _ ->
+ "Add (map clone) to the chain, which copies what each element \
+ owns"
+ else
+ Printf.sprintf
+ "Nothing copies what a %s owns, so no copy of %s can stand on \
+ its own: read the elements where they are, or build each new \
+ element and push that"
+ et v
+ in
+ Loc.failk "check/into-shares-elements" src.Ast.loc
+ "into copies each element of %s as it stands, and an element of \
+ %s is a %s, which owns storage — the copy would share each \
+ element's block with %s, and growing either one frees the block \
+ the other points at. %s"
+ v v et v fix
+ | _ -> ());
+ expect ctx loc ~want (mk loc Types.Unit Tast.Unit)
+ | _ -> expect ctx loc ~want (mk loc Types.Unit Tast.Unit))
(* (clone v) uses the current allocator, (clone v a) names one. A deep,
independent copy: spec-memory.md's "copying is always explicit". *)
| "clone" ->
@@ -9517,9 +9877,9 @@ and named_call ?(qualified = false) ctx ~want loc name args =
| (Types.Vec _ | Types.Map _) when region_only ctx.env target.Tast.ty ->
fail loc
"%s cannot be cloned — its elements own storage, and nothing here \
- can walk one to copy what it owns. Build a second container and \
- insert into it"
+ can walk one to copy what it owns. %s"
(Types.to_string target.Tast.ty)
+ (insert_copies ctx.env target.Tast.ty)
(* A slice's elements, copied into a block from the allocator and
answered as a slice over it — what (bytes s) does for a string's
bytes, and the same lowering. The same refusal as a Vec's, for the
@@ -9527,9 +9887,9 @@ and named_call ?(qualified = false) ctx ~want loc name args =
| Types.Slice (_, elem) when owning ctx.env elem ->
fail loc
"%s cannot be cloned — its elements own storage, and nothing here \
- can walk one to copy what it owns. Build a container and insert \
- into it"
+ can walk one to copy what it owns. %s"
(Types.to_string target.Tast.ty)
+ (insert_copies ctx.env target.Tast.ty)
(* The copy is a block from an allocator, which the collector does not
walk, so a dyn in it would be a root nothing marks. *)
| Types.Slice (_, elem) when holds_dyn ctx.env elem ->
@@ -9639,6 +9999,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
check ctx ~want:Types.Dyn v; here loc ])
else begin
let kt, vt = map_kv loc "put" target.Tast.ty in
+ note_grown ctx "put" loc target;
let k = check ctx ~want:kt k in
let v = check ctx ~want:vt v in
(* Deferred: the arguments are checked — so a move here is still a move
@@ -10871,8 +11232,8 @@ and named_call ?(qualified = false) ctx ~want loc name args =
[ordered?] admits a type that does not, and that day is why the
question is asked of the predicate and not of the set it denotes. *)
| Types.Var v ->
- cast_operand ctx loc name ~needs:"numeric?" ~what:"a number"
- ~is:"a number" v
+ cast_operand ctx loc name ~needs:"numeric?" ~also:("enum?", "an enum")
+ ~what:"a number or an enum" ~is:"a number" v
| t -> fail loc "%s converts a number, found %s" name (Types.to_string t));
prim (Tast.Cast target) target [ a ]
| _ when is_cast name && List.length args = 1 ->
@@ -10903,8 +11264,8 @@ and named_call ?(qualified = false) ctx ~want loc name args =
machine type, so what is in question is only the operand, and the
[where] clause is what answers it. *)
| Types.Var v ->
- cast_operand ctx loc name ~needs:"numeric?" ~what:"a number"
- ~is:"a number" v
+ cast_operand ctx loc name ~needs:"numeric?" ~also:("enum?", "an enum")
+ ~what:"a number or an enum" ~is:"a number" v
| t -> fail loc "%s converts a number, found %s" name (Types.to_string t));
(match a.Tast.ty with
| Types.Dyn -> cast_dyn ctx loc target a
@@ -10956,6 +11317,14 @@ and ordinary_call ctx ~want loc name args =
match capture ctx loc name with
| Some b -> call_value ctx ~want loc (mk loc b.bty (Tast.Local b.slot)) args
| None -> assert false)
+ (* A global holding a function value — a (CFn ...) table entry's cousin,
+ since a global is one of the zeroed positions a CFn may sit in. Called
+ by its name the way a local one is. *)
+ | _ when (match Hashtbl.find_opt ctx.env.globals name with
+ | Some (ty, _) -> callable_ty ty
+ | None -> false) ->
+ let ty, _ = Hashtbl.find ctx.env.globals name in
+ call_value ctx ~want loc (mk loc ty (Tast.Global name)) args
| _ when Hashtbl.mem ctx.env.gsigs name ->
private_ref ctx loc name;
let vars, params, ret = Hashtbl.find ctx.env.gsigs name in
@@ -12100,6 +12469,10 @@ let builtins : (string * string * string) list =
its allocator's free-all. Refused for elements that own \
storage: a bytewise copy would alias the original's blocks under a \
name promising otherwise.");
+ ("into-copies-elements", "into-copies-elements [src dst transform...] ()",
+ "What into writes when its chain has no (map f). Refuses a source whose \
+ elements own storage, because pushing them as they stand would share \
+ their blocks. Not meant to be written by hand.");
(* (Map K V) *)
("map-new", "map-new [K? V? Allocator?] (Map K V)",
@@ -12993,6 +13366,55 @@ let check_union_members env =
(* ── Declarations: pass 2, check bodies ────────────────────────────── *)
+(* The names a body hands back or stores into: every name mentioned in a value
+ it answers — its last form's tails, a [return]'s value — and the name at
+ the root of every [set] place. A parameter among them is not warned at for
+ growing: the grown copy goes back to the caller, or the copy is the
+ function's own business. *)
+let escaping_names ~returns (body : Ast.expr list) : string list =
+ let names = ref [] in
+ let rec mentions (e : Ast.expr) =
+ (match e.Ast.e with Ast.Var n -> names := n :: !names | _ -> ());
+ ignore (Ast.map_children (fun x -> mentions x; x) e)
+ in
+ let rec tails (e : Ast.expr) =
+ match e.Ast.e with
+ | Ast.Do es | Ast.Let (_, es) ->
+ (match List.rev es with x :: _ -> tails x | [] -> ())
+ | Ast.If (_, a, b) -> tails a; Option.iter tails b
+ | Ast.Match (_, arms) ->
+ List.iter
+ (fun (a : Ast.arm) ->
+ match List.rev a.Ast.body with x :: _ -> tails x | [] -> ())
+ arms
+ (* A value that is the parameter, a field of it, or a literal built with
+ it. A call's result is its callee's business, and a unit form — the
+ push itself, last in a function that returns nothing — answers
+ nothing. *)
+ | Ast.Var _ | Ast.Field _ | Ast.Struct _ | Ast.Bare _ | Ast.Arr _
+ | Ast.MapLit _ -> mentions e
+ | _ -> ()
+ in
+ let rec root (e : Ast.expr) =
+ match e.Ast.e with
+ | Ast.Var n -> names := n :: !names
+ | Ast.Field (x, _) -> root x
+ | Ast.Call ({ Ast.e = Ast.Var ("at" | "deref"); _ }, x :: _) -> root x
+ | _ -> ()
+ in
+ let rec walk (e : Ast.expr) =
+ (match e.Ast.e with
+ | Ast.Return (Some x) -> tails x
+ | Ast.Set (Ast.Pvar n, _) -> names := n :: !names
+ | Ast.Set ((Ast.Pfield (x, _) | Ast.Pindex (x, _) | Ast.Pderef x
+ | Ast.Pslot (x, _)), _) -> root x
+ | _ -> ());
+ ignore (Ast.map_children (fun x -> walk x; x) e)
+ in
+ List.iter walk body;
+ if returns then (match List.rev body with x :: _ -> tails x | [] -> ());
+ !names
+
let rec check_fn env (fn : Ast.fn) : Tast.fn =
let params, ret = Hashtbl.find env.fns fn.Ast.name in
let ctx = { (invented_ctx env ret) with owner = fn.Ast.name } in
@@ -13015,6 +13437,41 @@ let rec check_fn env (fn : Ast.fn) : Tast.fn =
end;
ignore (bind ctx p.Ast.fname ty ~assignable:false))
fn.Ast.params params;
+ let grow_before = !grow_warnings in
+ let grow_saved = !grow_params in
+ grow_params :=
+ ( ctx,
+ List.filter_map
+ (fun (p : Ast.field) ->
+ Option.map (fun b -> (b.slot, p)) (List.assoc_opt p.Ast.fname ctx.scope))
+ fn.Ast.params )
+ :: grow_saved;
+ let escaping =
+ lazy
+ (let names =
+ escaping_names ~returns:(not (Types.equal ret Types.Unit)) fn.Ast.fbody
+ in
+ List.filter_map
+ (fun (p : Ast.field) ->
+ if List.mem p.Ast.fname names then Some p.Ast.floc else None)
+ fn.Ast.params)
+ in
+ Fun.protect
+ ~finally:(fun () ->
+ grow_params := grow_saved;
+ let added =
+ List.filteri
+ (fun i _ -> i < List.length !grow_warnings - List.length grow_before)
+ !grow_warnings
+ in
+ if added <> [] then
+ grow_warnings :=
+ List.filter
+ (fun (d : Loc.diag) ->
+ not (List.mem d.Loc.dloc (Lazy.force escaping)))
+ added
+ @ grow_before)
+ @@ fun () ->
let body =
match fn.Ast.fbody with
| [] ->
@@ -14135,30 +14592,37 @@ let shown_name n =
else n
let shadow_prelude (prelude : Ast.decl list) (decls : Ast.decl list) =
- let fn_name (d : Ast.decl) =
+ (* A value name, and whether it is a function's. A program's global takes
+ a prelude function's name over as a program's function does: both are
+ names a call or a read reaches, and the prelude's own uses keep the
+ prelude's. *)
+ let value_name (d : Ast.decl) =
match d.Ast.d with
- | Ast.Defn fn | Ast.Declare (fn, _) | Ast.DeclareC (fn, _) -> Some fn.Ast.name
+ | Ast.Defn fn | Ast.Declare (fn, _) | Ast.DeclareC (fn, _) ->
+ Some (fn.Ast.name, true)
+ | Ast.Defvar (n, _, _, _) | Ast.Defconst (n, _, _) -> Some (n, false)
| _ -> None
in
- let theirs = List.filter_map fn_name prelude in
+ let theirs = List.map fst (List.filter_map value_name prelude) in
let taken =
List.filter_map
(fun (d : Ast.decl) ->
- match fn_name d with
- | Some n when List.mem n theirs -> Some (n, d.Ast.dloc)
+ match value_name d with
+ | Some (n, f) when List.mem n theirs -> Some (n, d.Ast.dloc, f)
| _ -> None)
decls
in
let warnings =
List.map
- (fun (n, at) ->
+ (fun (n, at, f) ->
Loc.diag ~kind:"check/shadows-prelude" at
(Printf.sprintf
- "%s shadows the prelude's %s — every call in this file now \
+ "%s shadows the prelude's %s — every %s in this file now \
reaches your definition"
- n n))
+ n n (if f then "call" else "use")))
taken
in
+ let taken = List.map (fun (n, at, _) -> (n, at)) taken in
let prelude, decls =
List.fold_left
(fun (prelude, decls) (n, (at : Loc.t)) ->
@@ -14265,7 +14729,14 @@ let build_program ~keep_going ?tolerate (decls : Ast.decl list) :
time it runs every signature is sound, so a body that fails to check
cannot make the next body fail — which is what makes a declaration a
resync point that needs no resynchronising. *)
+ grow_warnings := [];
let decls = collect env decls in
+ if !print_warnings then
+ List.iter
+ (fun (d : Loc.diag) ->
+ prerr_endline
+ (Loc.entry ~mark:'~' ~label:"warning: " d.Loc.dloc d.Loc.dmsg))
+ (List.rev !pairing_warnings);
check_finite env;
check_union_members env;
let s = Loc.sink ~on:keep_going in
@@ -14319,6 +14790,12 @@ let build_program ~keep_going ?tolerate (decls : Ast.decl list) :
| _ -> None)
decls
in
+ if !print_warnings then
+ List.iter
+ (fun (d : Loc.diag) ->
+ prerr_endline
+ (Loc.entry ~mark:'~' ~label:"warning: " d.Loc.dloc d.Loc.dmsg))
+ (List.rev !grow_warnings);
Loc.finish s;
(* The handler clauses lifted out along the way. They are ordinary functions
from here down; nothing in the backend knows they were written inside
diff --git a/lib/emit.ml b/lib/emit.ml
index 4752a78d..640078e6 100644
--- a/lib/emit.ml
+++ b/lib/emit.ml
@@ -3177,6 +3177,9 @@ and call_ptr ?at f ret callee args =
code, Some ("ptr " ^ env)
| _ -> c, None
in
+ (match callee.Tast.ty, at with
+ | Types.CFn _, Some loc -> null_check f loc callee.Tast.ty code
+ | _ -> ());
let vs = map_lr (fun (a : Tast.expr) ->
let v = value f a in Printf.sprintf "%s %s" (ll a.Tast.ty) v) args in
Option.iter (mark_call f) at;
@@ -3184,6 +3187,19 @@ and call_ptr ?at f ret callee args =
if at <> None then clear_call f;
r
+(* A (CFn ...) may be a zeroed field, array element or global, and a zeroed
+ one is a null address. Tested before the arguments are evaluated, so a
+ call that is not going to be made runs none of them — the x86 backend
+ tests at the same point. [flan_null_call] signals [NullCall] and returns
+ only when something transferred, the shape of a bounds failure. *)
+and null_check f loc ty code =
+ let ok = fresh f in
+ ins f "%s = icmp ne ptr %s, null" ok code;
+ signal_block f loc ~guard:(fun () -> guard f) ok (fun id n ->
+ let tys = fst (fi_bytes f.md (Types.to_string ty ^ "\000")) in
+ ins f "call void @flan_null_call(ptr %s, i64 %d, ptr %s, ptr %s)"
+ id n tys xfer_param)
+
(* The code address behind one of the three [fnref]s, which is the same string
whether it is wanted as a bare [(Ptr ())] or as the first word of a
function value.
@@ -4925,6 +4941,9 @@ declare void @flan_arith_error(ptr, i64, i32, i64, i64, ptr) cold
; as C strings, then the cell and the channel. Signals StaleCall; returns when
; something answered.
declare void @flan_stale_call(ptr, ptr, ptr, ptr, ptr) cold
+; A call through a (CFn ...) holding null: the site, the value's type as a C
+; string and the channel. Signals NullCall; returns when something answered.
+declare void @flan_null_call(ptr, i64, ptr, ptr) cold
declare ptr @flan_context_allocator()
declare ptr @flan_context_use(ptr, i64)
declare void @flan_context_value(ptr)
diff --git a/lib/indent_printer.ml b/lib/indent_printer.ml
index eb3bfd38..3b7b2512 100644
--- a/lib/indent_printer.ml
+++ b/lib/indent_printer.ml
@@ -200,13 +200,17 @@ and inline_text ?(lvl = 0) (f : Form.t) =
w ^ " :" ^ k
| Form.List [ { v = Form.Sym "return"; _ }; v ] -> "return " ^ at (max lvl 1) v
| 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 lvl f
(* [t = v], or [t += w] when [v] is [(+ t w)]. *)
and assign_text ?(lvl = 0) t v =
let tt = at 9 t in
match v.v with
- | Form.List [ { v = Form.Sym (("+" | "-" | "*" | "/") as op); _ }; a; w ] when same a t ->
+ | Form.List [ { v = Form.Sym (("+" | "-" | "*" | "/") as op); _ }; a; w ]
+ when same a t && R.simple_place t ->
tt ^ " " ^ op ^ "= " ^ at (max lvl 1) w
| _ -> tt ^ " = " ^ at (max lvl 1) v
@@ -325,7 +329,7 @@ let body_split (h : Form.t) args =
let sugar_heads =
[ "let"; "set"; "if"; "when"; "cond"; "while"; "until"; "dotimes"; "match";
"handler-case"; "handler-bind"; "restart-case"; "return"; "defer"; "do";
- "quasiquote" ]
+ "quasiquote"; "update" ]
let rec block n (fs : Form.t list) : string list =
let rec go = function
@@ -441,6 +445,9 @@ and sugar n ~last (f : Form.t) : string list option =
(match pairs bs with
| None | Some [] -> None
| Some prs -> Some (let_lines n ~last prs body))
+ | Form.List [ { v = Form.Sym "update"; _ }; t; { v = Form.Sym ("+" | "-" | "*" | "/"); _ }; _ ]
+ when not (R.simple_place t) ->
+ 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 then Some [ line ]
diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml
index a5ea3a81..aefc217b 100644
--- a/lib/indent_reader.ml
+++ b/lib/indent_reader.ml
@@ -64,6 +64,35 @@ let is_op_word s = is_binop s || s = "not" || s = "="
let assign_ops = [ ("+=", "+"); ("-=", "-"); ("*=", "*"); ("/=", "/") ]
+(* A place whose parts are all names and literals reads the same however
+ often it is evaluated, so [x += v] over one is [(set x (+ x v))], the form
+ the Lisp side writes. Any other place — an index that is a call — reads
+ [(update p + v)], which evaluates each part of the place once. The printer
+ asks the same question, so the round trip is exact either way. *)
+let rec simple_place (f : Form.t) =
+ let atom (x : Form.t) =
+ match x.v with
+ | Form.Sym _ | Form.Int _ | Form.Kw _ | Form.Byte _ -> true
+ | _ -> false
+ in
+ match f.v with
+ | Form.Sym _ -> true
+ | Form.List [ { v = Form.Sym h; _ }; t ]
+ when (String.length h > 1 && h.[0] = '.') || h = "deref" ->
+ simple_place t
+ | Form.List ({ v = Form.Sym "at"; _ } :: t :: (_ :: _ as idx)) ->
+ simple_place t && List.for_all atom idx
+ | _ -> false
+
+let compound (at : Loc.t) op (e : Form.t) (v : Form.t) span =
+ if simple_place e then
+ Form.List
+ [ Form.make (Form.Sym "set") at; e;
+ Form.make (Form.List [ Form.make (Form.Sym op) at; e; v ]) span ]
+ else
+ Form.List
+ [ Form.make (Form.Sym "update") at; e; Form.make (Form.Sym op) at; v ]
+
(* 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 =
@@ -713,11 +742,7 @@ and inline_stmt p : Form.t =
| NAME op when List.mem_assoc op assign_ops ->
let eq = advance p in
let v, _ = expr p in
- mk p t.loc
- (Form.List
- [ sym eq.loc "set"; e;
- Form.make (Form.List [ sym eq.loc (List.assoc op assign_ops); e; v ])
- (span p e.loc) ])
+ mk p t.loc (compound eq.loc (List.assoc op assign_ops) e v (span p e.loc))
| _ -> e
(* [fn(a, b) = body] is a lambda; [fn(...)] followed by anything else is the
@@ -1114,10 +1139,7 @@ and expr_stmt (s : st) : Form.t =
let eq = advance p in
let v = value_line s ~after:(text_of e ^ " " ^ op) in
let o = List.assoc op assign_ops in
- mk p t0.loc
- (Form.List
- [ sym eq.loc "set"; e;
- Form.make (Form.List [ sym eq.loc o; e; v ]) (span p e.loc) ])
+ mk p t0.loc (compound eq.loc o e v (span p e.loc))
| COLON ->
let before = (last p).tok in
let c = advance p in
diff --git a/lib/load.ml b/lib/load.ml
index 70bb75cb..301babc1 100644
--- a/lib/load.ml
+++ b/lib/load.ml
@@ -1618,6 +1618,7 @@ let program ?(parse = Parse.program) ~file (forms : Form.t list) : t =
in
let decls =
Parse.with_imported ~decls:(imported.decls @ !Parse.imported_decls)
+ ~fns:!Parse.shadowing_fns
(macro_union imported.macros !Parse.imported_macros)
(fun () -> parse forms)
in
diff --git a/lib/macro.ml b/lib/macro.ml
index ae2577e7..1e570d0f 100644
--- a/lib/macro.ml
+++ b/lib/macro.ml
@@ -585,10 +585,39 @@ let with_module (l : loaded) (f : unit -> 'a) : 'a =
left in them, so a second pass only reaches the calls to the new names. A
macro that defines a macro whose expansion defines another costs one pass
per level, and the fuel is the same bound [settle] uses. *)
+(* The names these forms define as functions. A program's [defn] shadows a
+ macro of the same name — the prelude's [clamp] or [update], or an
+ imported one — as it shadows a prelude function: every call in the file
+ reaches the definition, and [Check.shadow_prelude] says so. So such a
+ name is not expanded here. A [defmacro] is a [defn] too once parsed, but
+ not yet: [macro_name] is what finds those, and they are left alone. *)
+let defined_fns (forms : Form.t list) =
+ List.filter_map
+ (fun (f : Form.t) ->
+ match f.Form.v with
+ | Form.List ({ Form.v = Form.Sym ("defn" | "defn-"); _ }
+ :: { Form.v = Form.Sym n; _ } :: _) -> Some n
+ | _ -> None)
+ forms
+
let rec program_n left (forms : Form.t list) : Form.t list =
match loaded_for forms with
| None -> forms
| Some l ->
+ (* Never the prelude's own forms: they are parsed inside a session's
+ evaluation too, and their calls are to their own macros. *)
+ let prelude =
+ match forms with
+ | f :: _ -> String.equal f.Form.loc.Loc.file Prelude.file
+ | [] -> false
+ in
+ let shadowed =
+ if prelude then [] else defined_fns forms @ !Parse.shadowing_fns
+ in
+ let l =
+ if shadowed = [] then l
+ else { l with fns = List.filter (fun (n, _) -> not (List.mem n shadowed)) l.fns }
+ in
let before = macros_in forms in
let out = with_module l (fun () -> List.map (expand_form l) forms) in
let fresh = List.filter (fun n -> not (List.mem n before)) (macros_in out) in
diff --git a/lib/parse.ml b/lib/parse.ml
index e5190994..f148fe2e 100644
--- a/lib/parse.ml
+++ b/lib/parse.ml
@@ -30,6 +30,32 @@ let no_sigil (f : Form.t) =
let dname (f : Form.t) = no_sigil f; sym f
+(* A global's name. One spelled like a built-in type would stand where the
+ type is written — [(vec-new u8)] — and change what that means, in the
+ program and in the prelude alike, so it is refused where it is declared. *)
+let gname (f : Form.t) =
+ (match f.v with
+ | Sym s when List.mem s Types.primitive_names ->
+ Loc.failk "parse/global-named-type" f.loc
+ "%s is a type, so it cannot also name a global — (vec-new %s) would \
+ not know which was meant. Name it %s-value, or any name that is not \
+ a type"
+ s s s
+ | _ -> ());
+ dname f
+
+(* A type's name. A built-in type's is taken: a second [u8] would stand for
+ one or the other wherever a type is written, the prelude's included. *)
+let tname (f : Form.t) =
+ (match f.v with
+ | Sym s when List.mem s Types.primitive_names ->
+ Loc.failk "parse/type-named-builtin" f.loc
+ "%s is a built-in type, so it cannot be declared again. Give the new \
+ type a name of its own"
+ s
+ | _ -> ());
+ dname f
+
(* Names for the temporaries this file mints — the value is bound once and
everything that needs it reads *that*, so a destructuring pattern over a
call calls it once and a short-circuit operand is evaluated once. [~] is a
@@ -449,6 +475,14 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
| [ target; value ] -> mk (Ast.Set (place target, expr value))
| _ -> fail f "set is (set place value)")
+ (* What [update], [++] and [--] expand into: (update~ PLACE g NEW), where
+ NEW is written over the name [g]. The name has a [~] in it so no program
+ can write it; only a prelude macro builds one. See [modify]. *)
+ | Sym "update~" ->
+ (match args with
+ | [ target; { v = Sym g; _ }; value ] -> modify f target g value
+ | _ -> fail f "internal: update~ is (update~ place name value) — a compiler bug")
+
(* ── (vec-new [u8]) and (map-new string [u8]) ───────────────────────
The type positions of these two take a type expression. Whether this call
is the builtin at all is the checker's to know — a program may define its
@@ -1174,17 +1208,10 @@ and cond f (args : Form.t list) : Ast.expr =
[f.loc] would blame the enclosing (and ...) for whichever operand is
actually wrong.
- What answering the operand costs, for both forms alike: the two arms are
- now both real values, so mixing a dyn operand with a typed bool one makes
- check_if unify them, and the then arm decides. A non-bool dyn value on
- the losing side then meets the strict bool boundary at run time —
- (or false (box "s")) and (and (box nil) some-bool) both trap, verified on
- this tree. Each form used to be safe in exactly one of those directions,
- because the sentinel it answered was a bool literal that boxed to fit
- whatever the real branch was; neither is now, and they are at least
- symmetric about it. Making bool and dyn arms join as dyn is a check_if
- question, noted in TODO.org, "A bool arm and a dyn arm joining as dyn",
- and not decided here.
+ The two arms are both real values, so mixing a dyn operand with a typed
+ bool one makes check_if unify them: a bool arm and a dyn arm meet at dyn
+ with the bool boxed, whichever side each is on, so (or false (box "s"))
+ answers "s" and (and (box nil) some-bool) answers nil.
One known wart, measured rather than guessed, and left alone deliberately.
In a want-free position — [(println (and true true (vec-new i32)))] — the
@@ -1252,6 +1279,65 @@ and place (f : Form.t) : Ast.place =
(deref p), or a class slot (get inst :slot)"
(Form.to_string f)
+(* A read-modify-write of a place, with every subexpression of the place
+ evaluated once — C's rule for compound assignment. (update (at grid (next)
+ c) inc) calls [next] once, and the read and the write land on the same
+ element.
+
+ Each index, key and pointer is bound to a temp first, outermost and
+ leftmost first. The container a field or an index is taken from is not: it
+ has to stay a path to the storage, since a temp would be a copy of a struct
+ or an array and the write would land in the copy. Only a path made of
+ names, fields, indexes and derefs stays one; anything else in container
+ position — a call answering a Vec, say — is a value, and is bound like an
+ index. Then [g] is bound to the place's current value, and [value], written
+ over [g], is stored back through the same path. *)
+and modify f target g value : Ast.expr =
+ let mk e = { Ast.e; loc = f.loc } in
+ let binds = ref [] in
+ let temp (e : Ast.expr) =
+ match e.Ast.e with
+ | Ast.Int _ | Ast.UInt _ | Ast.Float _ | Ast.Byte _ | Ast.Str _ | Ast.Kw _ -> e
+ | _ ->
+ let t = fresh_temp "place" in
+ binds := { Ast.bname = t; bty = None; bval = e; bloc = e.Ast.loc } :: !binds;
+ { e with Ast.e = Ast.Var t }
+ in
+ let rec path (e : Ast.expr) =
+ match e.Ast.e with
+ | Ast.Var _ -> e
+ | Ast.Field (t, n) -> { e with Ast.e = Ast.Field (path t, n) }
+ | Ast.Call (({ Ast.e = Ast.Var "at"; _ } as h), t :: idx) when idx <> [] ->
+ let t = path t in
+ { e with Ast.e = Ast.Call (h, t :: List.map temp idx) }
+ | Ast.Call (({ Ast.e = Ast.Var "deref"; _ } as h), [ p ]) ->
+ { e with Ast.e = Ast.Call (h, [ temp p ]) }
+ | _ -> temp e
+ in
+ let p =
+ match place target with
+ | Ast.Pvar _ as p -> p
+ | Ast.Pfield (t, n) -> Ast.Pfield (path t, n)
+ | Ast.Pindex (t, idx) ->
+ let t = path t in
+ Ast.Pindex (t, List.map temp idx)
+ | Ast.Pderef p -> Ast.Pderef (temp p)
+ | Ast.Pslot (t, k) ->
+ let t = temp t in
+ Ast.Pslot (t, temp k)
+ in
+ let at e = { Ast.e; loc = target.loc } in
+ let read =
+ match p with
+ | Ast.Pvar n -> at (Ast.Var n)
+ | Ast.Pfield (t, n) -> at (Ast.Field (t, n))
+ | Ast.Pindex (t, idx) -> at (Ast.Call (at (Ast.Var "at"), t :: idx))
+ | Ast.Pderef p -> at (Ast.Call (at (Ast.Var "deref"), [ p ]))
+ | Ast.Pslot (t, k) -> at (Ast.Call (at (Ast.Var "get"), [ t; k ]))
+ in
+ let old = { Ast.bname = g; bty = None; bval = read; bloc = target.loc } in
+ mk (Ast.Let (List.rev (old :: !binds), [ mk (Ast.Set (p, expr value)) ]))
+
and arms f (items : Form.t list) : Ast.arm list =
let rec go = function
| [] -> []
@@ -1343,7 +1429,13 @@ let rec decl (f : Form.t) : Ast.decl =
| List ({ v = Sym "defalias"; _ } :: args) ->
(match args with
- | [ n; t ] -> mk (Ast.Defalias (dname n, texpr t))
+ (* int and float restate a builtin alias, which the checker takes up;
+ any other built-in type's name is refused as tname refuses it. *)
+ | [ n; t ] ->
+ let name =
+ match n.v with Sym ("int" | "float") -> dname n | _ -> tname n
+ in
+ mk (Ast.Defalias (name, texpr t))
| _ -> fail f "defalias is (defalias Name Type)")
(* A parent comes before the fields, where Common Lisp's define-condition
@@ -1353,16 +1445,16 @@ let rec decl (f : Form.t) : Ast.decl =
| List ({ v = Sym "defstruct"; _ } :: args) ->
(match args with
| [ n; { v = Vec fs; _ } ] ->
- mk (Ast.Defstruct (dname n, fields f fs, None))
+ mk (Ast.Defstruct (tname n, fields f fs, None))
| [ n; { v = Kw "parent"; _ }; p; { v = Vec fs; _ } ] when fs <> [] ->
- mk (Ast.Defstruct (dname n, fields f fs, Some (texpr p)))
+ mk (Ast.Defstruct (tname n, fields f fs, Some (texpr p)))
(* An empty field vector is the same category as none. *)
| [ n; { v = Kw "parent"; _ }; p ] | [ n; { v = Kw "parent"; _ }; p; { v = Vec []; _ } ] ->
let str name =
{ Ast.fname = name; fty = { Ast.t = Ast.Tname "string"; tloc = f.loc };
floc = f.loc }
in
- mk (Ast.Defstruct (dname n, [ str "name"; str "message" ], Some (texpr p)))
+ mk (Ast.Defstruct (tname n, [ str "name"; str "message" ], Some (texpr p)))
| _ ->
fail f
"defstruct is (defstruct Name [field Type ...]), or with a parent \
@@ -1370,7 +1462,7 @@ let rec decl (f : Form.t) : Ast.decl =
| List ({ v = Sym "defdata"; _ } :: args) ->
(match args with
- | [ n; { v = Vec vs; _ } ] -> mk (Ast.Defdata (dname n, List.map variant vs))
+ | [ n; { v = Vec vs; _ } ] -> mk (Ast.Defdata (tname n, List.map variant vs))
| _ -> fail f "defdata is (defdata Name [(Case [field Type ...]) ...])")
(* C's union: one storage, as many ways of reading it as there are members.
@@ -1413,7 +1505,7 @@ let rec decl (f : Form.t) : Ast.decl =
[member Type ...]). This reads as a tagged sum — write \
(defdata Name [(Case [field Type ...]) ...])")
ms;
- mk (Ast.Defunion (dname n, fields f ms))
+ mk (Ast.Defunion (tname n, fields f ms))
| _ -> fail f "defunion is (defunion Name [member Type ...])")
(* The slot after the parameters is unconditionally the return type. It used
@@ -1538,7 +1630,7 @@ let rec decl (f : Form.t) : Ast.decl =
List.iter
(fun (s : Form.t) -> match s.v with Sym _ -> no_sigil s | _ -> ())
slots;
- mk (Ast.Defclass (dname n, pitems slots))
+ mk (Ast.Defclass (tname n, pitems slots))
| _ -> fail f "defclass is (defclass Name [slot Type ...])")
| List ({ v = Sym ("defgeneric" | "defmulti" as which); _ } :: args) ->
@@ -1631,7 +1723,7 @@ let rec decl (f : Form.t) : Ast.decl =
| List ({ v = Sym "defenum"; _ } :: args) ->
(match args with
| [ n; { v = Form.Vec ms; _ } ] ->
- let ename = dname n in
+ let ename = tname n in
(* An enum member is an [i32] at run time. [Shim] lowers the type to
int32_t for C's benefit and [Check] builds every member as a
[Tast.Int (v, I32)] -- but the reader hands this pass an [int64], so
@@ -1781,11 +1873,11 @@ let rec decl (f : Form.t) : Ast.decl =
(match args with
| [ n; t ] ->
let ty, init = defvar3 t in
- mk (Ast.Defvar (dname n, Some ty, init, kind))
+ mk (Ast.Defvar (gname n, Some ty, init, kind))
| [ n; t; { v = Sym "uninit"; _ } ] ->
- mk (Ast.Defvar (dname n, Some (texpr t), Ast.Uninit, kind))
+ mk (Ast.Defvar (gname n, Some (texpr t), Ast.Uninit, kind))
| [ n; t; v ] ->
- mk (Ast.Defvar (dname n, Some (texpr t), Ast.Init (expr v), kind))
+ mk (Ast.Defvar (gname n, Some (texpr t), Ast.Init (expr v), kind))
| _ ->
fail f
"%s is (%s name Type value?) or (%s name value) — a third element \
@@ -1824,8 +1916,8 @@ let rec decl (f : Form.t) : Ast.decl =
| List ({ v = Sym "defconst"; _ } :: args) ->
(match args with
- | [ n; v ] -> mk (Ast.Defconst (dname n, None, expr v))
- | [ n; t; v ] -> mk (Ast.Defconst (dname n, Some (texpr t), expr v))
+ | [ n; v ] -> mk (Ast.Defconst (gname n, None, expr v))
+ | [ n; t; v ] -> mk (Ast.Defconst (gname n, Some (texpr t), expr v))
| _ -> fail f "defconst is (defconst name Type? value)")
(* A macro is an ordinary function, and this is where it becomes one:
@@ -2002,15 +2094,25 @@ let expansion_macros : Form.t list ref = ref []
written twice, in the file where the two copies could disagree silently. *)
let imported_decls : Ast.decl list ref = ref []
-let with_imported ?(decls = []) (ms : Form.t list) (f : unit -> 'a) : 'a =
+(* Functions a session already holds, by name. A program's [defn] shadows a
+ macro of its name, and a session's form is expanded alone, long after the
+ [defn] that shadows — so the session says which names those are, as it
+ says which macros it has. *)
+let shadowing_fns : string list ref = ref []
+
+let with_imported ?(decls = []) ?(fns = []) (ms : Form.t list)
+ (f : unit -> 'a) : 'a =
let saved = !imported_macros in
let saved_decls = !imported_decls in
+ let saved_fns = !shadowing_fns in
imported_macros := ms;
imported_decls := decls;
+ shadowing_fns := fns;
Fun.protect
~finally:(fun () ->
imported_macros := saved;
- imported_decls := saved_decls)
+ imported_decls := saved_decls;
+ shadowing_fns := saved_fns)
f
(* Two entry points and not one function with a flag, and the reason is the
diff --git a/lib/prelude.ml b/lib/prelude.ml
index 85c022b4..5cc0ea01 100644
--- a/lib/prelude.ml
+++ b/lib/prelude.ml
@@ -197,6 +197,17 @@ let source = {flan|
;; nothing a handler supplies makes the old arguments fit the new body.
(defstruct StaleCall :parent Error [callee string compiled string current string])
+;; A call through a (CFn ...) that holds no function. A CFn may be a struct
+;; field, a fixed array's element or a global, and each of those starts out
+;; zeroed, which for a function value is no address at all. Every call
+;; through one tests first and signals this instead of jumping to nothing.
+;; `type` is the value's type as written, "(CFn [i32] i32)".
+;;
+;; Signalled from the runtime — flan_null_call in runtime/flan_rt.c — so
+;; **this field is a C struct that has to agree with this one**. No restart is
+;; established at the call, BoundsError's decision for BoundsError's reason.
+(defstruct NullCall :parent Error [type string])
+
;; What a generic function signals when no method answers. `generic` is the
;; name written at the defgeneric or defmulti, and `value` is what the
;; dispatch actually produced -- the class of the first argument for a
@@ -2299,16 +2310,12 @@ let source = {flan|
;; because a macro does not have a type at all; the expansion is checked at the
;; call site as if it had been written there.
;;
-;; **++ and -- read the place twice, and that is an accepted cost.** The
-;; expansion is (set PLACE (+ PLACE 1)), so PLACE is evaluated once to read
-;; and once to write. For a variable, a field or a deref that is free and
-;; means nothing. For (at arr (next-index)) — an index with a side effect —
-;; it means next-index runs twice and the read and the write land on different
-;; elements. That is not a bug to be fixed here: macros are non-hygienic by
-;; decision (plan.org, open decision 2), a macro cannot bind a temporary for
-;; the *place* without a reference type it does not have, and
-;; rl/with-drawing and rl/with-mode-2d already take the same trade on their
-;; arguments. Write the index out first if it does anything.
+;; **++ and -- evaluate the place once.** Each index, key and pointer in the
+;; place is bound to a temp before the read, so (++ (at arr (next-index)))
+;; calls next-index once and reads and writes the same element — C's rule for
+;; compound assignment. They are update with + and -, spelled as the form
+;; update~ that update itself expands into (a prelude macro may not call a
+;; macro); lib/parse.ml's [modify] is where the place is taken apart.
(defmacro inc [& args]
(if (!= (length args) 1)
`(inc-takes-one-number)
@@ -2322,12 +2329,31 @@ let source = {flan|
(defmacro ++ [& args]
(if (!= (length args) 1)
`(++-takes-one-place)
- `(set ~(at args 0) (+ ~(at args 0) 1))))
+ (let [g (gensym)]
+ `(~(Form.Sym {.s "update~"}) ~(at args 0) ~g (+ ~g 1)))))
(defmacro -- [& args]
(if (!= (length args) 1)
`(---takes-one-place)
- `(set ~(at args 0) (- ~(at args 0) 1))))
+ (let [g (gensym)]
+ `(~(Form.Sym {.s "update~"}) ~(at args 0) ~g (- ~g 1)))))
+
+;; ── update: change a place by applying a function to it ────────────────
+;;
+;; (update (.velocity g) inc)
+;; (update (at grid r c) + 10)
+;;
+;; (update place f args ...) stores (f old args ...) back into the place, where
+;; old is what the place held. f is written as the head of a call, so it may be
+;; a function, an operator or a macro such as inc. Every place set takes is a
+;; place here too — a name, a field, an element, a deref, a class slot — and
+;; the place is evaluated once, as ++ says above. It answers what set answers.
+(defmacro update [& args]
+ (if (< (length args) 2)
+ `(update-takes-a-place-and-a-function)
+ (let [g (gensym)]
+ `(~(Form.Sym {.s "update~"}) ~(at args 0) ~g
+ (~(at args 1) ~g ~@(form-rest args 2))))))
;; ── into: a fused transformation, and not a transducer ────────────────
;;
@@ -2400,6 +2426,15 @@ let source = {flan|
(form-sym? head "filter") (recur (- k 1) `(when (~f ~x) ~body))
:else `(into-transform-is-map-or-filter ~t))))))))
+;; Whether any transform in the chain is a (map f).
+(defn into-maps? [ts [Form]] bool
+ (loop [k 0]
+ (cond
+ (= k (length ts)) false
+ (let [items (form-items (at ts k))]
+ (and (> (length items) 0) (form-sym? (at items 0) "map"))) true
+ :else (recur (+ k 1)))))
+
;; The items of a list form, and the empty slice for anything else — a
;; non-list transform falls into the arity complaint above rather than needing
;; a case of its own.
@@ -2452,10 +2487,19 @@ let source = {flan|
named? (form-is-sym? from)
src (if named? from (gensym))
bind (if named? (form-nil) (form-pair src from))
+ ;; With no (map f) in the chain every element pushed is a source
+ ;; element as it stands, which copies only its header: the checker
+ ;; refuses that for an element that owns storage. The destination and
+ ;; the transforms ride along unevaluated, to be written back in the fix.
+ shares (if (into-maps? (form-rest args 2))
+ (form-nil)
+ (form-cons `(into-copies-elements ~src ~(at args 1) ~@(form-rest args 2))
+ (form-nil)))
dst (gensym)
x (gensym)
i (gensym)]
`(let [~dst ~(at args 1) ~@bind]
+ ~@shares
(dotimes [~i (length ~src)]
(let [~x (at ~src ~i)]
~(into-wrap (form-rest args 2) dst x)))
diff --git a/lib/session.ml b/lib/session.ml
index 508f3edb..d29fcc9e 100644
--- a/lib/session.ml
+++ b/lib/session.ml
@@ -800,6 +800,18 @@ let rehost t =
t.built <- record_built env p p.Tast.fns SM.empty;
t.live <- SM.empty
+(* The session's own functions whose names a macro also has — see
+ [Parse.shadowing_fns]. A [defmacro] is a [defn] once parsed, so the
+ session's macros are taken back out. *)
+let shadowing_fns t origin =
+ let macros = List.filter_map Macro.macro_name (macros_for t origin) in
+ List.filter_map
+ (fun (d : Ast.decl) ->
+ match d.Ast.d with
+ | Ast.Defn fn when not (List.mem fn.Ast.name macros) -> Some fn.Ast.name
+ | _ -> None)
+ t.decls
+
(* [forms], when given, are [src] already read — [pruned] runs this over a
file a form fewer each round and has no text for the subset. [base] is the
file an [(import ...)] in them is resolved against, the session's own when
@@ -812,7 +824,7 @@ let eval ?(origin = " What makes that liveable is a where clause, written as a Clojure-style
map at the head of the body — {:where (ordered? $t)}, or a vector when
there is more than one: {:where [(ordered? $t) (hashable? $u)]}. There
-are five predicates, and each gates builtins the compiler already has:
| Predicate | What it admits |
|---|---|
integer? | bit-and bit-or bit-xor << >> — every integer type, no float |
numeric? | + - * / %, and a cast (t x) |
enum? | a cast to a number, (i32 x) — every enum type |
ordered? | < <= > >= min max |
equal? | = and != |
hashable? | the variable as a Map key — (map-new t V), get, put, has-key? |