flan/test/programs/int-generic.flan
Joseph Ferano 5ad16d6815 A conversion asks the bound, not a type the variable does not have
(i32 (at xs i)) inside a body bounded integer? was refused with "i32
converts a number, found t". Arithmetic, comparison, min/max, the
bitwise fold and the shifts all ask the where clause; the conversions
were the family nobody had gone back to, and the cast block held three
arms of it.

The machine-type target asked Types.is_numeric of its operand and the
enum target asked Types.Int _, so a variable fell through both to the
refusal however it was bounded. The variable target had the opposite
defect: it asked the bound of the target and then took any generic
operand, so a second variable declared only ordered? passed the abstract
pass on the strength of a sentence about a different one. Nothing wrong
was ever emitted through it — ordered? admits numbers and enums and both
convert at the instantiation — which is exactly why it is worth closing:
the hole opens the day ordered? admits a type that does not.

The rule is the repo's own, applied to a set instead of a type: a
conversion is legal at a bounded variable exactly when it is legal at
every type the bound admits. A machine-type target needs numeric?, an
enum target needs integer? because numeric? admits the floats the
concrete arm refuses, and ordered?/equal?/hashable? admit nothing — the
last by what the predicate says rather than by the set it denotes, since
hashable? already admits strings and ordered? may.

Both float targets and the narrowing i32 stay legal: (f64 i64-x) rounds
above 2^53 and (i32 f64-x) truncates where the types are written, and a
generic that refused what its copies accept would be the fork the rule
forbids. FIX.org, 2026-09-21, has the account, and records what this
costs: no predicate now licenses a generic enum to integer conversion,
and enum? is the eventual answer.

The refusal says what the variable is known to be and what to write, in
the clause spelling unconstrained already uses: a body with no clause
gets the clause, a body that has one is told which predicate to add.
2026-09-21 10:01:18 +07:00

170 lines
6.2 KiB
Plaintext

;;;; integer?, end to end: the bound numeric? was one type too wide for.
;;;;
;;;; Three families in here, in order. The collapsed abs — one written body
;;;; under {:where (integer? $t)} where abs-i32 and abs-i64 used to be, pinned
;;;; at six widths, at both signed minimums (the answer is itself, because the
;;;; negation wraps — what every two's-complement abs does), and beside the
;;;; libm float pair it deliberately does not shadow: (abs-f64 -0.0) is 0
;;;; because fabs clears the sign bit, which no integer body spells. Then the
;;;; operations only integer? admits in a generic body — bit-and, bit-or,
;;;; bit-xor, the shifts, and % — at several widths each. Then the join:
;;;; mixed widths at one $t resolve to the wider type in either argument
;;;; order (FIX.org 2026-09-20), so both orders print the same number from
;;;; the same copy.
(defonce i32min i32 -2147483648)
(defonce i64min i64 -9223372036854775808)
;; The low n bits, which needs a shift, a bit-and and the literal 1 — every
;; one of them admitted by integer? and none by anything weaker.
(defn low-bits [x $t n $t] $t
{:where (integer? $t)}
(bit-and x (- (<< 1 n) 1)))
;; Truncated %, the semantics everywhere in the language, in a generic body.
(defn even? [x $t] bool
{:where (integer? $t)}
(= (% x 2) 0))
;; xor and or, and the shift right.
(defn toggle [x $t m $t] $t
{:where (integer? $t)}
(bit-xor x m))
(defn with-flag [x $t f $t] $t
{:where (integer? $t)}
(bit-or x f))
(defn halve [x $t] $t
{:where (integer? $t)}
(>> x 1))
;; The untyped literal at a bounded variable: admitted under integer? by the
;; same arm that admits it under numeric?, ranged per copy.
(defn plus-300 [x $t] $t
{:where (integer? $t)}
(+ x 300))
;; The join family. eq2? is the pair the refusal used to be pinned on.
(defn eq2? [a $t b $t] bool
{:where (equal? $t)}
(= a b))
(defn tri [a $t b $t c $t] $t
{:where (numeric? $t)}
(+ a (+ b c)))
;; ── Conversions under a bound ──────────────────────────────────────────
;; A cast asks whether its operand is a number, and inside a generic body no
;; type is there to answer. The bound answers instead, for every copy at
;; once: integer? admits only integer kinds and every one of them converts,
;; so the body is checked once here and the conversion is the ordinary one in
;; each copy. This is the shape the bug report was written against — a sum
;; that narrows each element to the i32 it accumulates in.
(defn total [xs [$t]] i32
{:where (integer? $t)}
(let [acc 0]
(dotimes [i (len xs)] (set acc (+ acc (i32 (at xs i)))))
acc))
;; The same rule widening. i64 → f64 rounds above 2^53 and i32 → f64 does
;; not, and neither is refused when the type is written out, so neither is
;; refused under the bound: a conversion is not a claim that the value
;; survives, it is the claim that the operand is a number.
(defn mean [xs [$t]] f64
{:where (integer? $t)}
(let [sum 0.0]
(dotimes [i (len xs)] (set sum (+ sum (f64 (at xs i)))))
(/ sum (f64 (len xs)))))
;; numeric? is the weaker bound and narrowing is legal under it too, because
;; it is legal at every type it admits: (i32 x) on a written f64 truncates
;; towards zero, and that is what the f32 copy of this does.
(defn truncate [x $t] i32
{:where (numeric? $t)}
(i32 x))
;; The other direction, which is where the two bounds part company. An enum
;; is an i32 and a float has no enum reading, so this one needs integer?
;; exactly — numeric? would admit an f64 copy the concrete arm refuses.
(defenum Step [back -1 stay 0 forward 1])
(defn step-of [n $t] Step
{:where (integer? $t)}
(Step n))
(defn main [] ()
;; abs, one body, six widths.
(println (abs (i8 -7)))
(println (abs -7))
(println (abs (i64 -7)))
(println (abs (u8 7)))
(println (abs (u32 7)))
(println (abs (u64 7)))
;; The signed minimums answer themselves: the negation wraps, and saturating
;; quietly would be the wrong answer this file exists to refuse.
(println (abs i32min))
(println (abs i64min))
;; The float abs stays libm's: a sign-bit clear, so -0.0 comes back 0.
(println (abs-f64 -0.0))
(println (abs-f32 -0.0))
(println (abs-f64 -1.5))
(println (abs-f32 -2.5))
;; The integer?-only operations, per width.
(println (low-bits 255 3))
(println (low-bits (u16 65535) (u16 4)))
(println (low-bits (i64 1023) (i64 5)))
(println (even? 4))
(println (even? (u8 3)))
(println (even? (i64 -2)))
(println (toggle (u8 255) (u8 15)))
(println (with-flag 8 1))
(println (halve (u64 10)))
(println (halve (i64 -4)))
(println (plus-300 1))
(println (plus-300 (i64 1)))
;; The join: both orders, one copy, one answer.
(let [a (i8 3)
b (i64 3)]
(println (eq2? a b))
(println (eq2? b a)))
(let [x (u32 1)
y (i32 2)
z (i64 3)]
;; u32 and i32 meet at no type of their own; all three meet at the i64,
;; wherever it stands in the argument list.
(println (tri x y z))
(println (tri z y x)))
;; A literal beside a wider variable joins too: 4 arrives as an i32 and the
;; copy is i64's.
(let [w (i64 38)]
(println (tri w 3 1)))
;; And the one direction a container-bound variable does admit: the slice
;; fixed $t at i32 exactly, and a narrower scalar widens *into* that — the
;; same conversion a monomorphic i32 parameter would apply. (The reverse,
;; an i64 scalar against this slice, stays refused; the checker pins it.)
(let [ns [5 3 9 1]]
(match (index-of (slice ns 0 4) (i16 9))
(Some i) (println i)
_ (println -1)))
;; The conversions, generic and concrete side by side. Each pair is one
;; conversion written twice — once at a bounded variable and once at the
;; type a copy is made at — and the two answer the same number, which is
;; the whole claim the bound makes.
(let [small [5 3 9 1]
wide [(i64 5) (i64 3) (i64 9) (i64 1)]]
(println (total (slice small 0 4)))
(println (total (slice wide 0 4)))
(println (mean (slice small 0 4)))
(println (mean (slice wide 0 4))))
(println (truncate (i64 9)))
(println (truncate 2.75))
(println (truncate -2.75))
(println (i32 2.75))
(println (step-of 1))
(println (step-of (u8 0)))
(println (i32 (step-of (i64 -1)))))