flan/test/programs/destructure.flan
Joseph Ferano 26c53e0a19 Every defn in the tree states its return type, and Unit is written ()
The mechanical half, ahead of the parser change that needs it. tools/unit-return.py
fills the empty slot with () and rewrites Unit as () wherever a type is spelled --
(Fn [i32] Unit), (Map i32 Unit), a return type written out.

Deciding whether a defn already had a return type is the whole difficulty, and
the script does it the way parse.ml did: is_type_form is transcribed rather than
improved, because being identical to the parser it replaces is what makes the
sweep meaning-preserving. It is re-runnable, so the lanes that branched before
this can have the same pass at merge:

    python3 tools/unit-return.py .
    python3 tools/unit-return.py --in-strings test/test_flan.ml test/test_acceptance.ml \
        test/test_session.ml emacs/test-flan-dev.el emacs/test-flan-mode.el
    python3 tools/unit-return.py --raw-ml lib/prelude.ml
    python3 tools/unit-return.py --in-html web/index.html

-v logs every defn it saw and what it decided, which is how a sweep of 440 sites
gets reviewed at all. Embedded modes pool a file's type declarations across all
its fragments, because a snippet split across concatenation -- decls ^ "(defn f
[s [u8]] Cursor ...)" -- cannot see the names the other half declared; pooled
names count only in bare-symbol position, for the same reason the prelude's do.
A fragment that cuts off mid-form is skipped rather than guessed at. Five sites
in test_flan.ml still needed a hand, and they are in this commit.

Two things ride along because the sweep needs them: parse.ml reads a lone () as
the return type of a function with no body, which was not a shape the old
optional slot could produce; and the map refusals name () rather than Unit, since
that is now the spelling a caller wrote.
2026-09-12 23:06:40 +07:00

109 lines
3.9 KiB
Plaintext

;;;; Destructuring in a let: Clojure's binding forms over Flan's shapes.
;;;;
;;;; A struct stands in for Clojure's map, so {:keys [x y]} and {inner :field}
;;;; read fields off one; a fixed array stands in for its sequence, so [a b]
;;;; and [a b & rest] read elements out of one. None of it is a new form: it
;;;; all desugars in parse.ml into the Let, (.field x), at and slice that were
;;;; already there, which is why this program is the test that it works — the
;;;; typed IR has nothing in it a pattern could be hiding in.
;;;;
;;;; The case that matters most here is `calls`. A pattern binds several names
;;;; from one value, and that value is bound to a temporary *first*, so a
;;;; pattern over a call calls it once. Delete the temporary and every name
;;;; re-evaluates the initialiser: this program prints the call count, so that
;;;; mistake changes the output instead of hiding in it.
(defstruct Point [x i32 y i32])
(defstruct Line [a Point b Point])
(defvar calls i32)
(defn make-point [] Point
(set calls (+ calls 1))
(Point {.x 3 .y 4}))
(defn show2 [label string a i32 b i32] ()
(print label)
(print " ")
(print a)
(print " ")
(print b)
(println ""))
(defn main [] i32
;; :keys, the common case: one name per field, spelled as the field is.
(let [{:keys [x y]} (Point {.x 1 .y 2})]
(show2 "keys" x y))
;; The pair form, which is what renames and what nests — a :keys entry is a
;; field name and never a pattern.
(let [{a .x b .y} (Point {.x 10 .y 20})]
(show2 "pairs" a b))
(let [l (Line {.a (Point {.x 5 .y 6}) .b (Point {.x 7 .y 8})})]
(let [{{:keys [x y]} .b} l]
(show2 "nested" x y))
;; A pattern may shadow the very name it destructures, because the value is
;; read into a temporary before any of the names are bound.
(let [{l .a} l]
(show2 "shadow" (.x l) (.y l))))
;; A later binding sees an earlier pattern's names, as in any let.
(let [{:keys [x]} (Point {.x 100 .y 0})
doubled (* x 2)]
(show2 "sequential" x doubled))
;; A fixed array names every element. The count is checked against the type,
;; so [a b] over a [3 i32] is a compile error and not a silent prefix.
(let [xs [11 22 33]
[a b c] xs]
(print "array ")
(print a) (print " ")
(print b) (print " ")
(print c) (println ""))
;; & rest is the tail as a slice, which is an ordinary (slice xs n (len xs))
;; over a local — nothing new, and nothing that outlives the array.
(let [xs [1 2 3 4 5]
[head & tail] xs]
(print "rest ")
(print head) (print " ")
(print (len tail)) (print " ")
(print (at tail 0)) (print " ")
(print (at tail 3)) (println ""))
;; The tail may be empty: naming every element and then asking for the rest
;; is a zero-length slice, not an error.
(let [xs [9 8]
[p q & rest] xs]
(print "empty-tail ")
(print (+ p q)) (print " ")
(print (len rest)) (println ""))
;; Patterns nest through each other: a struct inside an array.
(let [ps [(Point {.x 1 .y 2}) (Point {.x 3 .y 4})]
[{:keys [x]} {y .y}] ps]
(show2 "nested-in-array" x y))
;; A tail of something wider than a machine word. The corpus slices arrays of
;; i32, u8 and f32 and nothing else, so this is the one place the desugared
;; (slice xs n (len xs)) has to get a struct's stride right rather than a
;; scalar's.
(let [ps [(Point {.x 1 .y 2}) (Point {.x 3 .y 4}) (Point {.x 5 .y 6})]
[first & others] ps]
(print "struct-tail ")
(print (.x first)) (print " ")
(print (len others)) (print " ")
(print (.y (at others 0))) (print " ")
(print (.x (at others 1))) (println ""))
;; Evaluate-once. Two patterns, two calls, four names — one call per pattern.
;; Without the temporary each of the four names would call it again: 4, not 2.
(let [{:keys [x y]} (make-point)
{a .x b .y} (make-point)]
(print "calls ")
(print calls) (print " ")
(print (+ x (+ y (+ a b))))
(println ""))
0)