flan/test/programs/into.flan
Joseph Ferano ca14394e0f into fuses at compile time because it is a macro, not a transducer
(into xs (vec-new i32) (map double) (filter even?)). The function name is
syntax and never a value, so (map double) is (double x) written into the
loop body: no intermediate collection, no closure, no generics, nothing
to inline. Transducers would compose at run time and Rust's iterators
need monomorphisation; a macro needs neither. into.flan counts the pulls,
which is the assertion a unit test cannot make.

The destination is in the form because the destination is the allocation,
and that is what makes spec-memory.md's explicit-allocator rule true by
construction rather than by convention. Which also settles the open
question: reductions do not share the form. A seed is not an allocation,
so (into xs 0 (map cost) (sum)) would be a second form wearing the same
spelling, and the destination would stop being honest about what it is.

A source that is already a name is used as it is, not bound. A (Vec T) is
move-only, so binding it would take the caller's ownership for something
that only reads; a fixed array would be copied once per into. A source
that is anything else is still bound once, which is what a call needs.
The wart is that an owning temporary there leaks, because the binding has
a name the caller cannot reach — a call in that position should borrow,
and drop is what would close it.

All four of the prelude's macro limits bit and none blocked anything. The
three refusals are names nothing defines, which is the only error
facility a macro has. into-wrap is a defn using only special forms, so
Macro.reduce does not drop it, and it is the first thing in the prelude
written as a loop/recur.
2026-09-13 09:27:20 +07:00

82 lines
3.0 KiB
Plaintext

;;;; into — a fused transformation, and not a transducer.
;;;;
;;;; What is asserted here is the thing a unit test cannot see: the loop that
;;;; comes out is one loop, and the values it produces are the ones the chain
;;;; describes in the order it was written.
;;;;
;;;; 1. The order of the transforms is the order of the stages. (filter even?)
;;;; before (map double) is not the same program as after it, and both are
;;;; here with different answers.
;;;; 2. There is no intermediate collection. `pulls` counts every call to the
;;;; transform functions: one pass over the source is one call per element
;;;; per stage it reaches, and a chain that built a Vec per stage would pull
;;;; a different number.
;;;; 3. A source that is already a name is only read. `src` is a (Vec i32),
;;;; which is move-only, and it is still alive and freeable afterwards.
;;;; 4. A source that is a call is evaluated once, not once per element.
(defvar pulls i32 0)
(defn double [x i32] i32
(set pulls (+ pulls 1))
(* x 2))
(defn even? [x i32] bool
(set pulls (+ pulls 1))
(= (% x 2) 0))
(defvar builds i32 0)
;; A source that is a call. It must be made once, however many elements come
;; out of it. It borrows rather than allocating, which is the shape a call in
;; this position wants: the macro binds the value to a name the caller cannot
;; see, so an owning temporary here would be a leak nobody can reach.
(defn source [xs [i32]] [i32]
(set builds (+ builds 1))
(slice xs 0 (len xs)))
(defn show [v [i32]] ()
(dotimes [i (len v)] (print (at v i)) (print " "))
(println ""))
(defn main [] i32
;; No transforms: a copy into the destination named in the form.
(let [xs [7 8 9]
v (into xs (vec-new i32))]
(show (as-slice v)) ; 7 8 9
(free v))
;; map then filter.
(let [xs [1 2 3 4 5 6]
v (into xs (vec-new i32) (map double) (filter even?))]
(show (as-slice v)) ; 2 4 6 8 10 12
(free v))
;; filter then map, over the same source: a different answer, because the
;; stages are in the order they were written.
(let [xs [1 2 3 4 5 6]
v (into xs (vec-new i32) (filter even?) (map double))]
(show (as-slice v)) ; 4 8 12
(free v))
;; One pass and no intermediate collection. The two chains above pulled
;; 6 doubles + 6 evens, then 6 evens + 3 doubles: 21.
(print pulls) (println "") ; 21
;; A name as the source is read, not moved: src is still alive here.
(let [src (into [3 1 2] (vec-new i32))
v (into src (vec-new i32) (map double))]
(show (as-slice v)) ; 6 2 4
(show (as-slice src)) ; 3 1 2
(free v)
(free src))
;; A source that is a call is bound once, so it is made once however many
;; elements come out of it.
(let [xs [1 2 3 4]
v (into (source (slice xs 0 4)) (vec-new i32) (filter even?))]
(show (as-slice v)) ; 2 4
(free v))
(print builds) (println "") ; 1
0)