flan/test/programs/chain.flan
Joseph Ferano 165685490e Comparisons take a run of operands: the orderings chain, != is all-pairs
(< a b c) was an arity error. The folding operators had taken two operands
or more since fold_left_prim went in; the six comparisons had not, and they
are the ones the game hit.

The orderings and = chain: (< a b c) is a below b and b below c, because the
left fold would compare a bool against a number. != does not — the author's
ruling is that (!= 1 2 1) should be false — so it asks about every pair,
Common Lisp's /=. Whether a sequence is increasing is a question about
neighbours; whether a set of values are all different is a question about the
set, and the pair chaining never looks at is the one that decides it.

Every operand is bound to a slot first, in source order, so an operand two
pairs name is evaluated once — the spelling a reader would write, (and (< a b)
(< b c)), evaluates b twice. The conjunction then stops at the first pair that
fails, with nothing observable riding on it: everything has already run.

At two operands both readings are one pair and neither goes through the n-ary
lowering, so every comparison there is emits what it always did. (< x) joins
(+) and (- x) as a refusal — it would be true whatever it was handed.
2026-09-21 11:03:39 +07:00

174 lines
7.9 KiB
Plaintext

;;;; Chained comparisons, and the property no checker test can assert: how
;;;; many times each operand runs.
;;;;
;;;; (< a b c) means a < b and b < c. The reading a compiler could otherwise
;;;; have picked, the left fold ((a < b) < c), compares a bool against a
;;;; number and nothing means it. The interesting half is the middle operand:
;;;; two links mention b, and the spelling a reader would write by hand,
;;;; (and (< a b) (< b c)), evaluates it twice. So every operand here comes
;;;; through [mark], which prints its tag, and the printed tags are what say
;;;; each operand ran once and in source order.
;;;;
;;;; The folding operators are marked the same way, because "left to right,
;;;; exactly once" is one claim and it is made about both families.
(defonce calls i32)
;; Prints its tag and answers its value. Every operand below is one of these,
;; so the tag line is a transcript of the evaluation.
(defn mark [tag string v i32] i32
(set calls (+ calls 1))
(print tag)
v)
(defn markf [tag string v f64] f64
(print tag)
v)
;; A generic chain, admitted by the predicate its signature carries. The same
;; three-link body as everything above, over a type the function does not know.
(defn between [a $t b $t c $t] bool {:where (ordered? $t)}
(< a b c))
;; Three unannotated parameters are three dyn ones, so this is the chain the
;; runtime compares rather than the machine.
(defn dyn-rising [a b c] bool
(< a b c))
;; And the all-pairs reading over dyn operands, which asks the runtime the
;; same question three times rather than twice.
(defn dyn-distinct [a b c] bool
(!= a b c))
;; Prints y when its two arguments agree and N when they do not. Two bools
;; cannot be compared with = — bool is not an ordered or an equatable type —
;; so the agreement between a chain and the spelling it stands for is said
;; with the operators bool does have.
(defn agree [a bool b bool] ()
(if (and (or a b) (not (and a b))) (print "N") (print "y")))
(defn line [b bool] ()
(print " -> ")
(print b)
(println ""))
(defn main [] i32
;; ── the chain itself ──────────────────────────────────────────────
(print (< 1 2 3)) (print " ") ; true
(print (< 1 5 3)) (print " ") ; false: the second link
(print (< 5 1 3)) (print " ") ; false: the first link
(print (< 1 2 3 4)) (print " ") ; true, four operands
(print (< 1 2 4 3)) (println "") ; false, four operands
;; Every member of the family, chained. The middle link is the false one in
;; each of the second column.
(print (<= 1 1 2)) (print " ") (print (<= 1 2 1)) (print " ")
(print (> 3 2 1)) (print " ") (print (> 3 1 2)) (print " ")
(print (>= 3 3 1)) (print " ") (print (>= 3 1 2)) (print " ")
(print (= 2 2 2)) (print " ") (print (= 2 3 2))
(println "")
;; != is the one that does not chain: it asks whether the operands are all
;; different, so the pair it looks at is every pair and not only the
;; neighbouring ones. (!= 1 2 1) has no two neighbours alike and is still
;; false, which is the whole of the difference.
(print (!= 1 2 3)) (print " ") ; true
(print (!= 1 1 2)) (print " ") ; false, the adjacent pair
(print (!= 1 2 1)) (print " ") ; false, the pair chaining misses
(print (!= 1 2 3 4)) (print " ") ; true, six pairs
(print (!= 1 2 3 1)) (print " ") ; false, first against last
(print (!= 1 2 3 2)) (println "") ; false, a pair in the middle
;; The chain and the spelling it stands for agree, everywhere both are
;; legal. Written out rather than trusted, because it is the whole claim.
(agree (< 1 2 3) (and (< 1 2) (< 2 3)))
(agree (< 1 5 3) (and (< 1 5) (< 5 3)))
(agree (< 5 1 3) (and (< 5 1) (< 1 3)))
(agree (<= 1 1 2) (and (<= 1 1) (<= 1 2)))
(agree (> 3 1 2) (and (> 3 1) (> 1 2)))
(agree (= 2 3 2) (and (= 2 3) (= 3 2)))
(agree (< 1 2 3 4) (and (< 1 2) (and (< 2 3) (< 3 4))))
;; != stands for the all-pairs spelling, not the adjacent one.
(agree (!= 1 2 1) (and (!= 1 2) (and (!= 2 1) (!= 1 1))))
(agree (!= 1 2 3) (and (!= 1 2) (and (!= 1 3) (!= 2 3))))
(println "")
;; ── one evaluation of each operand, in order ──────────────────────
;; The first link is true here, so the chain goes on. Three tags.
(set calls 0)
(line (< (mark "a" 1) (mark "b" 2) (mark "c" 3)))
;; abc -> true
;; The first link is *false* here, and the tags still say abc: stopping
;; early stops comparing, not running. b appears once although two links
;; name it, which is the bug the slots exist to prevent.
(line (< (mark "a" 9) (mark "b" 1) (mark "c" 5)))
;; abc -> false
;; Four operands, false in the middle, and every one of them runs.
(line (< (mark "a" 1) (mark "b" 2) (mark "c" 0) (mark "d" 9)))
;; abcd -> false
;; Ten marks across three chains is the count, and a middle operand
;; evaluated twice would make it eleven.
(print calls) (println "") ; 10
;; != makes the same promise over its larger set of pairs. All different,
;; six comparisons over four operands, four tags.
(set calls 0)
(line (!= (mark "a" 1) (mark "b" 2) (mark "c" 3) (mark "d" 4)))
;; abcd -> true
;; The *first* pair it looks at already says no, and the other three
;; operands run anyway: every operand is in its slot before any pair is
;; compared, so stopping early stops comparing and nothing else.
(line (!= (mark "a" 1) (mark "b" 1) (mark "c" 3) (mark "d" 4)))
;; abcd -> false
;; The pair chaining would never have looked at — first against last.
(line (!= (mark "a" 1) (mark "b" 2) (mark "c" 3) (mark "d" 1)))
;; abcd -> false
;; Twelve, and each of the three lines above contributed four. An operand
;; named by three pairs evaluated once per pair would say twenty-four.
(print calls) (println "") ; 12
;; The folding operators make the same promise.
(set calls 0)
(print (+ (mark "p" 1) (mark "q" 2) (mark "r" 3))) (println "") ; pqr6
(print (- (mark "p" 10) (mark "q" 3) (mark "r" 2))) (println "") ; pqr5
(print (* (mark "p" 2) (mark "q" 3) (mark "r" 4))) (println "") ; pqr24
(print (min (mark "p" 5) (mark "q" 2) (mark "r" 8))) (println "") ; pqr2
(print (max (mark "p" 5) (mark "q" 2) (mark "r" 8))) (println "") ; pqr8
(print calls) (println "") ; 15
;; ── the types a chain admits ──────────────────────────────────────
;; One width throughout: the first pair decides, the rest are checked
;; against it, and nothing widens implicitly at three operands any more
;; than it does at two.
(let [x (u8 1) y (u8 2) z (u8 3)]
(print (< x y z)) (print " ")) ; true
(let [x (i64 -5) y (i64 0) z (i64 5)]
(print (< x y z)) (print " ")) ; true
(print (< (markf "f" 1.0) (markf "g" 2.0) (markf "h" 3.0))) (println "")
;; true, and fgh printed first
;; Strings, which = and != admit and the orderings do not. They are the one
;; operand type here that is not a machine word, so they are what says the
;; slots a chain binds are slots of whatever type the operands have.
(print (= "a" "a" "a")) (print " ") ; true
(print (= "a" "b" "a")) (print " ") ; false
(print (!= "a" "b" "c")) (print " ") ; true
(print (!= "a" "b" "a")) (println "") ; false, the non-adjacent pair
;; The generic chain, at two types.
(print (between (i32 1) (i32 2) (i32 3))) (print " ") ; true
(print (between (f64 3.0) (f64 2.0) (f64 1.0))) (println "") ; false
;; And the dyn chain, where the runtime does the comparing.
(print (dyn-rising 1 2 3)) (print " ") ; true
(print (dyn-rising 1 5 3)) (print " ") ; false
(print (dyn-distinct 1 2 3)) (print " ") ; true
(print (dyn-distinct 1 2 1)) (println "") ; false, the non-adjacent pair
0)