int and float are the machine types, not a second name for them

The author's exception to the foreign-spelling list: int is i32 and float
is f32, and nothing else on that list moves.

Spelled in Types.ikind_of_name and Types.fkind_of_name rather than as two
prelude defaliases, because Check.is_cast asks those two functions and never
the alias table — a prelude alias would have left (int x) with no reading
while (i32 x) had one. Both names join primitive_names for the same reason
one layer down: that list is what decides (vec-new int) and the three-element
(defvar x int).

Nothing reverses: ikind_name still says i32, so every message, signature,
inspector line and DWARF name shows the machine type whichever spelling was
written.

A defalias restating the builtin is the no-op it says it is; one pointing the
name anywhere else is refused, since the alias table is never consulted and
the declaration would otherwise mean i32 in silence.
This commit is contained in:
Joseph Ferano 2026-09-20 19:09:26 +07:00
parent 5ea6884d2c
commit 26242388b9
6 changed files with 318 additions and 15 deletions

86
FIX.org
View File

@ -2041,3 +2041,89 @@ A ~defn~ whose name is a builtin's is still not refused. The builtin still
wins every call and the definition is still unreachable; what changed is that wins every call and the definition is still unreachable; what changed is that
the arity message says so and notes the definition. Refusing the shadowing is the arity message says so and notes the definition. Refusing the shadowing is
a language decision and was left to the author. a language decision and was left to the author.
* ~int~ and ~float~ as builtin aliases, 2026-09-20
The author, on the foreign-spelling list the diagnostics pass had just
landed: "I think we can make an exception for int and float."
Exactly those two. ~int~ is ~i32~ and ~float~ is ~f32~; ~integer~, ~long~,
~double~, ~uint~, ~str~ and the rest keep the teaching refusal, and ~usize~
stays off the list entirely for the reason already recorded there — its width
depends on the target.
** Spelled as machine types, not as prelude aliases
Two implementations were on the table: two ~defalias~es in the prelude, or
two entries where ~i32~ and ~f32~ already resolve. The second, and the thing
that decides it is the cast.
~Check.is_cast~ asks ~Types.ikind_of_name~ and ~Types.fkind_of_name~ whether
a head names a primitive. It does not look in the alias table, and no user
alias is a cast head today. A prelude ~(defalias int i32)~ would therefore
have given ~(int x)~ no reading while ~(i32 x)~ had one — a spelling that
works in type position and nowhere else, which is a second-class name and not
what was asked for.
So: ~"i32" | "int" -> Some I32~ and ~"f32" | "float" -> Some F32~, plus both
names on ~Types.primitive_names~. That last is not decoration —
~type_named~ reads that list, and it is what decides whether ~(vec-new int)~
names an element type and whether a three-element ~(defvar x int)~ reads its
third element as a type. Without it the identity would have been
type-position-only again, one layer down.
Every other path is reached without learning the word: the resolver, the
parameter-vector pairing, the ~$t~ refusal, the near-miss candidates, the
~let~ binding-vector annotation hint. Nothing in ~emit~, ~x86~, ~js~ or the
runtime changed, or could have.
** What the user sees: ~i32~, always
~ikind_name~ and ~fkind_name~ are the only way back from a type to a name and
they have no ~int~ to give. So the erasure is total and in one direction: a
program may write ~int~ everywhere, and every error message, every eldoc
signature, every inspector line and every DWARF type name says ~i32~.
~(defn f [a int] int ...)~ reports as ~(Fn [i32] i32)~. A mismatch at a site
spelled ~int~ says "expected i32". Both pinned.
This is the same erasure a user ~defalias~ already has and is the honest
answer: the alias is a spelling, the type is the type.
** Redefinition: true ones are no-ops, false ones are refused
The author's own programs contain ~(defalias int i32)~, written before there
was a builtin. The rule is decided by the target, at registration:
- ~(defalias int i32)~ and ~(defalias float f32)~ — accepted, and nothing is
written to the alias table. The declaration is true, it is now redundant,
and deleting the line is a cleanup rather than a fix.
- Anything else — refused: "int is a builtin alias for i32 and cannot be
redefined as i64 — delete this defalias, or give the type another name".
The alternative was the ~arity~ precedent, where the builtin wins and a note
surfaces at the error the shadowing caused. It does not transfer: a
~(defalias int i64)~ has no later error site to hang a note on. ~resolve_name~
reaches ~ikind_of_name~ before the alias table, so the declaration would be
read as ~i32~ at every use and nothing would ever say so. Silence was the one
unacceptable answer; refusing costs a rename in the program that meant it.
Nothing was added for ~(defvar int 5)~ or ~(defn f [int x] ...)~. Identity
settles them: whatever those do with ~i32~ written in, they now do with
~int~, and both were already refusals.
** Widening
No table entry, because there is nothing to widen — ~int~ *is* ~i32~. Pinned
as identity instead: ~(+ intvar i64var)~ is refused with "expected i64, found
i32", the same message and the same spelling as ~(+ i32var i64var)~. Written
that way so it survives whatever the widening lane lands: it asserts that the
two spellings behave alike, not what either one does.
** Pinned
Type position (parameter and return), cast head, ~(Vec int)~ and
~(Map int float)~, struct fields, ~(defalias Row (Vec int))~, the
three-element ~defvar~ zeroed static, ~int~/~i32~ and ~float~/~f32~ passing
for each other across a call, the two erasure messages, the widening
identity, both no-op redefinitions, three redefinition refusals, and
~integer~, ~double~ and ~long~ still teaching. ~test/programs/int-float.flan~
runs the value half on both backends.
One existing row changed: the foreign-spelling pin in test_flan.ml used
~int~, which resolves now, and was moved to ~long~.

View File

@ -246,14 +246,25 @@ let one_edit a b =
value goes was not a mistyped struct. *) value goes was not a mistyped struct. *)
let nearest cands n = List.find_opt (fun c -> c <> n && one_edit n c) cands let nearest cands n = List.find_opt (fun c -> c <> n && one_edit n c) cands
(* What the last language called it. [int] is two edits from [i32] and so is (* What the last language called it. [long] is two edits from [i64] and so is
outside [one_edit]'s net, which is right two edits is a guess but the outside [one_edit]'s net, which is right two edits is a guess but the
name is not a guess at all: it is what C, Java, Go and Python spell the name is not a guess at all: it is what C, Java, Go and Python spell an
default integer, and somebody writing it here has not mistyped anything, integer, and somebody writing it here has not mistyped anything, they have
they have not yet learned that this language sizes its integers in the not yet learned that this language sizes its integers in the name. Without
name. Without this list [int] falls through to the lowercase arm of this list [long] falls through to the lowercase arm of [resolve_name] and
[resolve_name] and is reported as generic code over a type variable, which is reported as generic code over a type variable, which is a sentence about
is a sentence about a feature the reader was not reaching for. a feature the reader was not reaching for.
Two names that belong on this list by that reasoning are *not* on it:
[int] and [float]. They are builtin aliases as of 2026-09-20 [int] is
[i32] and [float] is [f32], spelled in [Types.ikind_of_name] and
[Types.fkind_of_name] so they resolve rather than teach, and a row for
either here would be dead code the next reader would trust. The exception
stops at those two, on the author's decision: the default integer and the
default float are the two spellings a program reaches for constantly, and
[integer], [long], [double], [str] and the rest keep teaching. So [integer]
still answers [i32] here even though [int] no longer does the sibling
spelling is still a name this language does not have.
Short on purpose, and only names with one honest answer. [char] is not Short on purpose, and only names with one honest answer. [char] is not
here: C's is a byte, Java's is a UTF-16 unit and Rust's is a scalar value, here: C's is a byte, Java's is a UTF-16 unit and Rust's is a scalar value,
@ -266,14 +277,13 @@ let nearest cands n = List.find_opt (fun c -> c <> n && one_edit n c) cands
that builds both. A name goes on this list when the answer does not depend that builds both. A name goes on this list when the answer does not depend
on anything. *) on anything. *)
let foreign_spelling = function let foreign_spelling = function
| "int" | "integer" -> Some "i32" | "integer" -> Some "i32"
| "uint" | "unsigned" -> Some "u32" | "uint" | "unsigned" -> Some "u32"
| "long" -> Some "i64" | "long" -> Some "i64"
| "ulong" -> Some "u64" | "ulong" -> Some "u64"
| "short" -> Some "i16" | "short" -> Some "i16"
| "ushort" -> Some "u16" | "ushort" -> Some "u16"
| "byte" -> Some "u8" | "byte" -> Some "u8"
| "float" -> Some "f32"
| "double" -> Some "f64" | "double" -> Some "f64"
| "boolean" -> Some "bool" | "boolean" -> Some "bool"
| "str" -> Some "string" | "str" -> Some "string"
@ -7718,6 +7728,34 @@ let collect env (decls : Ast.decl list) =
| Ast.Defunion (n, _) -> | Ast.Defunion (n, _) ->
Hashtbl.replace env.locs n d.Ast.dloc; Hashtbl.replace env.locs n d.Ast.dloc;
Hashtbl.replace env.unions n { Tast.sname = n; fields = [] } Hashtbl.replace env.unions n { Tast.sname = n; fields = [] }
(* [int] and [float] are builtin aliases (Types), and a program that
declared them itself which this one's author did, before they were
builtin must not quietly stop meaning what it says. The alias
table is never consulted for either name: [resolve_name] answers
from [ikind_of_name] first, so a [(defalias int i64)] left to
register would be read as [i32] at every use and nothing would ever
say so.
So the target decides. Spelled as the builtin's own type, the
declaration is true and is accepted as the no-op it is the old
program still compiles, and deleting the line is a cleanup rather
than a fix. Spelled as anything else it is refused, because the only
alternative is to silently mean something else. Nothing is written
to the table either way: the name resolves without it. *)
| Ast.Defalias (n, { Ast.t = Ast.Tname target; _ })
when n = "int" || n = "float" ->
let builtin = if n = "int" then "i32" else "f32" in
if target <> builtin then
Loc.failk "check/builtin-alias" d.Ast.dloc
"%s is a builtin alias for %s and cannot be redefined as %s — \
delete this defalias, or give the type another name"
n builtin target
| Ast.Defalias (n, _) when n = "int" || n = "float" ->
let builtin = if n = "int" then "i32" else "f32" in
Loc.failk "check/builtin-alias" d.Ast.dloc
"%s is a builtin alias for %s and cannot be redefined — delete \
this defalias, or give the type another name"
n builtin
| Ast.Defalias (n, t) -> Hashtbl.replace env.aliases n t | Ast.Defalias (n, t) -> Hashtbl.replace env.aliases n t
| _ -> ()) | _ -> ())
decls; decls;

View File

@ -68,22 +68,47 @@ let bits = function
let bits_f = function F32 -> 32 | F64 -> 64 let bits_f = function F32 -> 32 | F64 -> 64
(* [int] and [float] are the two builtin aliases, and they are spelled here
rather than as prelude [defalias]es so that they are the machine type and
not a second name for it. The difference is visible at a cast: [Check]'s
[is_cast] asks these two functions whether a head names a primitive, and an
entry in the alias table is not consulted there so a prelude alias would
give [(int x)] no reading while [(i32 x)] had one. Named here, every path
that already accepts [i32] accepts [int] without learning the word.
Only these two. The rest of the foreign spellings [long], [double],
[uint], [str] stay refusals that teach the Flan name; see
[Check.foreign_spelling] for why the line is drawn where it is.
The mapping is one-way on purpose: [ikind_name] and [fkind_name] below
still answer [i32] and [f32], so every message, every DWARF name and every
inspector line the user sees says the machine type, whichever spelling the
source used. *)
let ikind_of_name = function let ikind_of_name = function
| "i8" -> Some I8 | "i16" -> Some I16 | "i32" -> Some I32 | "i64" -> Some I64 | "i8" -> Some I8 | "i16" -> Some I16 | "i32" | "int" -> Some I32
| "i64" -> Some I64
| "u8" -> Some U8 | "u16" -> Some U16 | "u32" -> Some U32 | "u64" -> Some U64 | "u8" -> Some U8 | "u16" -> Some U16 | "u32" -> Some U32 | "u64" -> Some U64
| _ -> None | _ -> None
let fkind_of_name = function let fkind_of_name = function
| "f32" -> Some F32 | "f64" -> Some F64 | _ -> None | "f32" | "float" -> Some F32 | "f64" -> Some F64 | _ -> None
(* Every name the resolver accepts as a primitive type. The list exists so a (* Every name the resolver accepts as a primitive type. The list exists so a
near-miss can be reported as the typo it is. [Unit] is on it because the near-miss can be reported as the typo it is. [Unit] is on it because the
resolver still answers to that name -- [Cimport] builds [Tname "Unit"] for resolver still answers to that name -- [Cimport] builds [Tname "Unit"] for
C's void, and never goes through the parser -- but nobody writes it: unit C's void, and never goes through the parser -- but nobody writes it: unit
is spelled [()] in source, and [Parse.texpr] refuses the word. *) is spelled [()] in source, and [Parse.texpr] refuses the word.
[int] and [float] are on it for the same reason they are in the two
functions above: the places that ask this list whether a [defvar]'s third
element is a type, whether [(vec-new int)] names an element type, whether a
[let] binding vector has an annotation wedged into it must answer the
same for [int] as for [i32], or the alias is a type-position-only spelling
and the identity is a half one. *)
let primitive_names = let primitive_names =
[ "i8"; "i16"; "i32"; "i64"; "u8"; "u16"; "u32"; "u64"; [ "i8"; "i16"; "i32"; "i64"; "u8"; "u16"; "u32"; "u64";
"f32"; "f64"; "bool"; "string"; "dyn"; "Unit"; "Never"; "Allocator" ] "f32"; "f64"; "bool"; "string"; "dyn"; "Unit"; "Never"; "Allocator";
"int"; "float" ]
let ikind_name k = let ikind_name k =
(if signed k then "i" else "u") ^ string_of_int (bits k) (if signed k then "i" else "u") ^ string_of_int (bits k)

View File

@ -0,0 +1,63 @@
;;;; The two builtin aliases: int is i32 and float is f32.
;;;;
;;;; Not a prelude defalias -- an entry in Types.ikind_of_name and
;;;; Types.fkind_of_name, so the name IS the machine type rather than a second
;;;; name for it. What that buys is what this program exercises: every
;;;; position that takes i32 takes int, the cast head included, and nothing
;;;; downstream of the checker ever hears the word.
;;;;
;;;; The rest of the foreign spellings -- long, double, integer, str -- are
;;;; still refusals that teach the Flan name, which a program cannot show
;;;; because it would not compile; that half is in test_flan.ml.
(defstruct Point [x int y float])
;;; A type alias over one: the alias machinery sees a resolved i32, exactly as
;;; if (Vec i32) had been written.
(defalias Row (Vec int))
;;; A zeroed static, from the three-element defvar whose third element is read
;;; as a type and not as a value.
(defvar total int)
;;; Both spellings in one signature, to make the point that they are the same
;;; two types and not a parallel pair.
(defn mix [a int b i32 c float d f32] int
(+ a b (int c) (int d)))
(defn main [] i32
;; The cast head, which is the position a prelude alias could not have
;; reached: Check.is_cast asks Types.ikind_of_name and Types.fkind_of_name
;; whether the head names a primitive, and never looks in the alias table.
(let [n (int 7)
f (float 2.5)
back (int f) ; f32 -> i32, truncating
wide (i64 n)]
(println n) ; 7
(println f) ; 2.5
(println back) ; 2
(println wide)) ; 7
;; Generic type arguments: the element type of a Vec and both halves of a
;; Map, named with the alias.
(let [v (vec-new int)
m (map-new string int)]
(push v 10)
(push v 20)
(put m "k" 30)
(println (at v 0)) ; 10
(println (at v 1)) ; 20
(println (match (get m "k") (Some n) n None -1))) ; 30
;; Struct fields, written and read.
(let [p (Point {.x 3 .y 1.5})]
(println (.x p)) ; 3
(println (.y p))) ; 1.5
;; The zeroed global, before and after.
(println total) ; 0
(set total 41)
(println (+ total 1)) ; 42
(println (mix 1 2 3.5 4.5)) ; 10
0)

View File

@ -1243,6 +1243,19 @@ let () =
string_eq_out; string_eq_out;
outputs ~x86:true "string equality, --x86" "programs/string-eq.flan" outputs ~x86:true "string equality, --x86" "programs/string-eq.flan"
string_eq_out; string_eq_out;
(* The two builtin aliases: int is i32, float is f32. Both backends,
because the claim is identity with the machine type and the two
backends are where a spelling that only half resolved would show up as
a different number rather than as a refusal. The refusals that go with
the feature -- a redefining defalias, and [long] still teaching -- are
checker tests in test_flan.ml, since neither compiles. *)
let int_float_out =
"7\n2.5\n2\n7\n10\n20\n30\n3\n1.5\n0\n42\n10\n"
in
outputs "int and float, the builtin aliases" "programs/int-float.flan"
int_float_out;
outputs ~x86:true "int and float, --x86" "programs/int-float.flan"
int_float_out;
(* dyn if: truthiness -- M2 queue item 7. A dyn scrutinee is tested for (* dyn if: truthiness -- M2 queue item 7. A dyn scrutinee is tested for
nil/false vs. everything else, Clojure's rule, on both backends; a nil/false vs. everything else, Clojure's rule, on both backends; a
typed scrutinee stays strictly bool, which is a checker test typed scrutinee stays strictly bool, which is a checker test

View File

@ -2245,9 +2245,12 @@ let () =
rejects_check "a plain type typo keeps the short answer" rejects_check "a plain type typo keeps the short answer"
"(defvar total i33) (defn f [] ())" "(defvar total i33) (defn f [] ())"
~needle:"unknown type i33 — did you mean i32?"; ~needle:"unknown type i33 — did you mean i32?";
(* [int] used to be this row. It resolves now — it is a builtin alias for
[i32] so the spelling that still teaches has to be one of the ones the
exception did not cover. *)
rejects_check "and another language's spelling is answered by name" rejects_check "and another language's spelling is answered by name"
"(defvar total int) (defn f [] ())" "(defvar total long) (defn f [] ())"
~needle:"unknown type int — Flan spells it i32"; ~needle:"unknown type long — Flan spells it i64";
rejects_check "a data case is not a type, and says what is" rejects_check "a data case is not a type, and says what is"
"(defdata Shape [(Circle [r f64])]) (defvar g Circle) (defn f [] ())" "(defdata Shape [(Circle [r f64])]) (defvar g Circle) (defn f [] ())"
~needle:"Circle is a case of the data type Shape, and a case is not a \ ~needle:"Circle is a case of the data type Shape, and a case is not a \
@ -2521,6 +2524,81 @@ let () =
"(defenum E [a 1]) (defstruct E [x i32])" ~needle:"defined twice"; "(defenum E [a 1]) (defstruct E [x i32])" ~needle:"defined twice";
rejects_check "an extern and a constant" rejects_check "an extern and a constant"
"(declare cw [] \"flan_cw\") (defconst cw 1)" ~needle:"defined twice"; "(declare cw [] \"flan_cw\") (defconst cw 1)" ~needle:"defined twice";
(* ── The two builtin aliases ────────────────────────────────────────
[int] is [i32] and [float] is [f32], as of 2026-09-20, and they are the
whole of the exception: every other foreign spelling still teaches. They
are entries in [Types.ikind_of_name] and [Types.fkind_of_name] rather
than prelude [defalias]es, so the claim under test is identity, not
resolution the rows below are the positions where a merely-resolving
name and the machine type would part company.
The corpus half is test/programs/int-float.flan, which runs on both
backends; what cannot be a program is here, because it does not
compile. *)
accepts "int in a return type and a parameter"
"(defn f [a int] int a)";
accepts "float likewise"
"(defn f [a float] float a)";
(* The position a prelude alias could not have reached: [is_cast] asks the
two [*kind_of_name] functions and never the alias table. *)
accepts "int and float as cast heads"
"(defn f [] int (int (float 1)))";
accepts "int as a generic type argument"
"(defn f [] i64 (let [v (vec-new int) m (map-new int float)] 0))";
accepts "int as a struct field, and float beside it"
"(defstruct P [x int y float]) (defn f [] int (let [p (P {.x 1 .y 2.0})] (.x p)))";
(* The three-element defvar, whose third element is read as a type: a
zeroed static and not a dyn global holding a value called [int]. *)
accepts "a defvar whose type is int" "(defvar g int) (defn f [] int g)";
accepts "and one whose type is float" "(defvar g float) (defn f [] float g)";
accepts "a user alias over int" "(defalias Row (Vec int)) (defn f [] i64 0)";
(* Identity, stated where identity is the only thing that could make it
pass: the two spellings meet as one type with no conversion between
them. *)
accepts "int and i32 are one type"
"(defn g [x i32] i32 x) (defn f [a int] i32 (g a))";
accepts "float and f32 are one type"
"(defn g [x f32] f32 x) (defn f [a float] f32 (g a))";
(* Erasure: the compiler answers in the machine type's name whichever
spelling the source used, which is what the inspector and DWARF show
too [ikind_name] and [fkind_name] have no [int] to give back. *)
rejects_check "a type error under int names i32"
"(defn f [] int 1.5)" ~needle:"expected i32";
rejects_check "and one under float names f32"
"(defn f [] float (f64 1.0))" ~needle:"expected f32";
(* Widening needs no entry for [int] because [int] *is* [i32]: the mixed
arithmetic that i32 refuses, int refuses identically and by the same
message. Pinned as identity rather than as a widening rule, so it says
the same thing whatever the widening table grows into. *)
rejects_check "int mixes with i64 exactly as i32 does"
"(defvar a int) (defvar b i64) (defn f [] i64 (+ a b))"
~needle:"expected i64, found i32";
(* A program that declared the alias itself — which this one's author did,
before it was builtin. True as written, it is the no-op it says it is;
pointed anywhere else it is refused, because the alias table is never
consulted for the name and the declaration would silently mean i32. *)
accepts "a defalias restating the builtin is a no-op"
"(defalias int i32) (defn f [] int 1)";
accepts "and so is the float one"
"(defalias float f32) (defn f [] float 1.0)";
rejects_check "a defalias redefining int is refused"
"(defalias int i64) (defn f [] int 1)"
~needle:"int is a builtin alias for i32 and cannot be redefined as i64";
rejects_check "and so is one redefining float"
"(defalias float f64) (defn f [] float 1.0)"
~needle:"float is a builtin alias for f32 and cannot be redefined as f64";
rejects_check "a defalias pointing int at a compound type"
"(defalias int (Vec i32)) (defn f [] i32 1)"
~needle:"int is a builtin alias for i32 and cannot be redefined";
(* The exception stops at two names. [integer] is [int]'s own sibling and
still teaches, which is the sharpest statement of where the line is. *)
rejects_check "integer still teaches"
"(defn f [] integer 1)" ~needle:"unknown type integer — Flan spells it i32";
rejects_check "double still teaches"
"(defn f [] double 1.0)" ~needle:"unknown type double — Flan spells it f64";
rejects_check "long still teaches"
"(defn f [] long 1)" ~needle:"unknown type long — Flan spells it i64";
(* A shift by the operand's own width or more is poison in LLVM, and at -O2 (* A shift by the operand's own width or more is poison in LLVM, and at -O2
a poison return is a function that returns nothing at all. A literal count a poison return is a function that returns nothing at all. A literal count
is rejected; a computed one is masked in [emit]. *) is rejected; a computed one is masked in [emit]. *)