A place in update, ++ and -- evaluates each of its subexpressions once, and (update place f args ...) stores (f old args ...) back into it
This commit is contained in:
parent
b8c81d94ee
commit
8cc63ed9f6
15
TODO.org
15
TODO.org
@ -1959,21 +1959,6 @@ CLOSED: [2026-09-25]
|
|||||||
=put= on an instance still checks a declared slot's type and still inserts an
|
=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.
|
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
|
** 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))=,
|
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
|
where =state= is a =defclass= instance with a =pause= slot. =(CFn [] ())= is
|
||||||
|
|||||||
@ -3887,7 +3887,7 @@ before this landed, so `macro-unless.flan` is a test written after the feature.
|
|||||||
### The line between a special form and a macro
|
### 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
|
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
|
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
|
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 ...)`.
|
and answers `(while :label (not test) body ...)`.
|
||||||
|
|||||||
67
lib/parse.ml
67
lib/parse.ml
@ -449,6 +449,14 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
|
|||||||
| [ target; value ] -> mk (Ast.Set (place target, expr value))
|
| [ target; value ] -> mk (Ast.Set (place target, expr value))
|
||||||
| _ -> fail f "set is (set place 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]) ───────────────────────
|
(* ── (vec-new [u8]) and (map-new string [u8]) ───────────────────────
|
||||||
The type positions of these two take a type expression. Whether this call
|
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
|
is the builtin at all is the checker's to know — a program may define its
|
||||||
@ -1252,6 +1260,65 @@ and place (f : Form.t) : Ast.place =
|
|||||||
(deref p), or a class slot (get inst :slot)"
|
(deref p), or a class slot (get inst :slot)"
|
||||||
(Form.to_string f)
|
(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 =
|
and arms f (items : Form.t list) : Ast.arm list =
|
||||||
let rec go = function
|
let rec go = function
|
||||||
| [] -> []
|
| [] -> []
|
||||||
|
|||||||
@ -2270,16 +2270,12 @@ let source = {flan|
|
|||||||
;; because a macro does not have a type at all; the expansion is checked at the
|
;; 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.
|
;; call site as if it had been written there.
|
||||||
;;
|
;;
|
||||||
;; **++ and -- read the place twice, and that is an accepted cost.** The
|
;; **++ and -- evaluate the place once.** Each index, key and pointer in the
|
||||||
;; expansion is (set PLACE (+ PLACE 1)), so PLACE is evaluated once to read
|
;; place is bound to a temp before the read, so (++ (at arr (next-index)))
|
||||||
;; and once to write. For a variable, a field or a deref that is free and
|
;; calls next-index once and reads and writes the same element — C's rule for
|
||||||
;; means nothing. For (at arr (next-index)) — an index with a side effect —
|
;; compound assignment. They are update with + and -, spelled as the form
|
||||||
;; it means next-index runs twice and the read and the write land on different
|
;; update~ that update itself expands into (a prelude macro may not call a
|
||||||
;; elements. That is not a bug to be fixed here: macros are non-hygienic by
|
;; macro); lib/parse.ml's [modify] is where the place is taken apart.
|
||||||
;; 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.
|
|
||||||
(defmacro inc [& args]
|
(defmacro inc [& args]
|
||||||
(if (!= (length args) 1)
|
(if (!= (length args) 1)
|
||||||
`(inc-takes-one-number)
|
`(inc-takes-one-number)
|
||||||
@ -2293,12 +2289,31 @@ let source = {flan|
|
|||||||
(defmacro ++ [& args]
|
(defmacro ++ [& args]
|
||||||
(if (!= (length args) 1)
|
(if (!= (length args) 1)
|
||||||
`(++-takes-one-place)
|
`(++-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]
|
(defmacro -- [& args]
|
||||||
(if (!= (length args) 1)
|
(if (!= (length args) 1)
|
||||||
`(---takes-one-place)
|
`(---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 ────────────────
|
;; ── into: a fused transformation, and not a transducer ────────────────
|
||||||
;;
|
;;
|
||||||
|
|||||||
@ -142,7 +142,7 @@ Each item: the proposal, then the reason in one line.
|
|||||||
index, field).
|
index, field).
|
||||||
- **`==` is `=`; `=` is assignment.** `x = v` reads `(set x v)`, `a[i] = v`
|
- **`==` 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
|
reads `(set (at a i) v)`, `p.x = v` reads `(set (.x p) v)`. `x += v` reads
|
||||||
`(set x (+ x v))`; like `++` today, the place is evaluated twice.
|
`(update x + v)`, which evaluates the place once, as `++` does.
|
||||||
- **A run of the same operator flattens** (variadics, section 3):
|
- **A run of the same operator flattens** (variadics, section 3):
|
||||||
`a + b + c` reads `(+ a b c)`, `a < b < c` reads `(< a b c)` (Flan's chain
|
`a + b + c` reads `(+ a b c)`, `a < b < c` reads `(< a b c)` (Flan's chain
|
||||||
semantics, `test/programs/chain.flan`). This keeps the converter round trip
|
semantics, `test/programs/chain.flan`). This keeps the converter round trip
|
||||||
|
|||||||
55
test/programs/update-place.flan
Normal file
55
test/programs/update-place.flan
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
;;;; update, ++ and -- evaluate every subexpression of their place once, as
|
||||||
|
;;;; C's compound assignment does. `calls` counts the index function: one call
|
||||||
|
;;;; per form, and the read and the write land on the same element.
|
||||||
|
|
||||||
|
(defonce calls i32 0)
|
||||||
|
|
||||||
|
(defn next-index [] i32
|
||||||
|
(set calls (+ calls 1))
|
||||||
|
(- calls 1))
|
||||||
|
|
||||||
|
(defstruct Body [velocity i32 hits [3 i32]])
|
||||||
|
|
||||||
|
(defn add [x i32 y i32] i32 (+ x y))
|
||||||
|
|
||||||
|
(defclass counter [n i32])
|
||||||
|
|
||||||
|
(defn which-slot [] dyn
|
||||||
|
(set calls (+ calls 1))
|
||||||
|
:n)
|
||||||
|
|
||||||
|
(defn main [] i32
|
||||||
|
(let [xs [10 20 30]
|
||||||
|
v (vec-new i32)
|
||||||
|
g (Body {.velocity 5})]
|
||||||
|
(push v 1) (push v 2) (push v 3)
|
||||||
|
;; next-index answers 0, then 1, then 2.
|
||||||
|
(++ (at xs (next-index)))
|
||||||
|
(-- (at v (next-index)))
|
||||||
|
(update (at xs (next-index)) * 3)
|
||||||
|
(println calls) ; 3
|
||||||
|
(println (at xs 0) (at xs 1) (at xs 2)) ; 11 20 90
|
||||||
|
(println (at v 0) (at v 1) (at v 2)) ; 1 1 3
|
||||||
|
;; A field, with a macro as the function, and with arguments after it.
|
||||||
|
(update (.velocity g) inc)
|
||||||
|
(update (.velocity g) add 10)
|
||||||
|
(println (.velocity g)) ; 16
|
||||||
|
;; A path through a field into an element: the struct is written in place,
|
||||||
|
;; not in a copy.
|
||||||
|
(set calls 0)
|
||||||
|
(update (at (.hits g) (next-index)) + 7)
|
||||||
|
(++ (at (.hits g) (next-index)))
|
||||||
|
(println calls) ; 2
|
||||||
|
(println (at (.hits g) 0) (at (.hits g) 1)) ; 7 1
|
||||||
|
;; Through a pointer.
|
||||||
|
(let [p (addr (.velocity g))]
|
||||||
|
(update (deref p) * 2)
|
||||||
|
(println (.velocity g))) ; 32
|
||||||
|
(free v))
|
||||||
|
;; A class slot, with the key computed once.
|
||||||
|
(let [c (counter 4)]
|
||||||
|
(set calls 0)
|
||||||
|
(++ (get c (which-slot)))
|
||||||
|
(update (get c (which-slot)) * 10)
|
||||||
|
(println calls (get c :n))) ; 2 50
|
||||||
|
0)
|
||||||
@ -432,6 +432,13 @@ let () =
|
|||||||
match_enum_out;
|
match_enum_out;
|
||||||
outputs ~dev:true "match over an enum, dev" "programs/match-enum.flan"
|
outputs ~dev:true "match over an enum, dev" "programs/match-enum.flan"
|
||||||
match_enum_out;
|
match_enum_out;
|
||||||
|
(* update, ++ and -- evaluate their place's subexpressions once: the
|
||||||
|
counts are the number of calls an index or a key function got. *)
|
||||||
|
let update_out = "3\n11 20 90\n1 1 3\n16\n2\n7 1\n32\n2 50\n" in
|
||||||
|
outputs "update evaluates its place once" "programs/update-place.flan"
|
||||||
|
update_out;
|
||||||
|
outputs ~x86:true "update evaluates its place once, --x86"
|
||||||
|
"programs/update-place.flan" update_out;
|
||||||
(* The count is [length] so that [len] is left to programs, and this is
|
(* The count is [length] so that [len] is left to programs, and this is
|
||||||
the claim that it really is one: a local holding a count, a
|
the claim that it really is one: a local holding a count, a
|
||||||
parameter, and a defn the program calls by its bare name, all of
|
parameter, and a defn the program calls by its bare name, all of
|
||||||
@ -4904,6 +4911,17 @@ level "1"
|
|||||||
"(defn main [] i32 (++) 0)" "++-takes-one-place";
|
"(defn main [] i32 (++) 0)" "++-takes-one-place";
|
||||||
macro_arity "-- with two arguments"
|
macro_arity "-- with two arguments"
|
||||||
"(defn main [] i32 (let [a 1 b 2] (-- a b)) 0)" "---takes-one-place";
|
"(defn main [] i32 (let [a 1 b 2] (-- a b)) 0)" "---takes-one-place";
|
||||||
|
macro_arity "update with no function"
|
||||||
|
"(defn main [] i32 (let [a 1] (update a)) 0)"
|
||||||
|
"update-takes-a-place-and-a-function";
|
||||||
|
(* A place update takes is a place set takes, and is refused the same way. *)
|
||||||
|
macro_arity "update of something that is not a place"
|
||||||
|
"(defn main [] i32 (update 5 inc) 0)" "5 is not assignable";
|
||||||
|
macro_arity "update of a parameter"
|
||||||
|
"(defn f [a i32] () (update a inc))" "a is a parameter";
|
||||||
|
macro_arity "update whose function answers the wrong type"
|
||||||
|
"(defn yes [x i32] bool true)\n(defn f [] () (let [a 1] (update a yes)))"
|
||||||
|
"expected i32";
|
||||||
(* unless keeps a guard, and it is now the narrower one: a body may be
|
(* unless keeps a guard, and it is now the narrower one: a body may be
|
||||||
missing, a test may not. *)
|
missing, a test may not. *)
|
||||||
macro_arity "unless with no test at all"
|
macro_arity "unless with no test at all"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user