diff --git a/FIX.org b/FIX.org index fb8b629..edb3e06 100644 --- a/FIX.org +++ b/FIX.org @@ -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 the arity message says so and notes the definition. Refusing the shadowing is 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~. diff --git a/lib/check.ml b/lib/check.ml index 86e44b9..59de7e5 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -246,14 +246,25 @@ let one_edit a b = value goes was not a mistyped struct. *) 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 - name is not a guess at all: it is what C, Java, Go and Python spell the - default integer, and somebody writing it here has not mistyped anything, - they have not yet learned that this language sizes its integers in the - name. Without this list [int] falls through to the lowercase arm of - [resolve_name] and is reported as generic code over a type variable, which - is a sentence about a feature the reader was not reaching for. + name is not a guess at all: it is what C, Java, Go and Python spell an + integer, and somebody writing it here has not mistyped anything, they have + not yet learned that this language sizes its integers in the name. Without + this list [long] falls through to the lowercase arm of [resolve_name] and + is reported as generic code over a type variable, which is a sentence about + 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 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 on anything. *) let foreign_spelling = function - | "int" | "integer" -> Some "i32" + | "integer" -> Some "i32" | "uint" | "unsigned" -> Some "u32" | "long" -> Some "i64" | "ulong" -> Some "u64" | "short" -> Some "i16" | "ushort" -> Some "u16" | "byte" -> Some "u8" - | "float" -> Some "f32" | "double" -> Some "f64" | "boolean" -> Some "bool" | "str" -> Some "string" @@ -7718,6 +7728,34 @@ let collect env (decls : Ast.decl list) = | Ast.Defunion (n, _) -> Hashtbl.replace env.locs n d.Ast.dloc; 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 | _ -> ()) decls; diff --git a/lib/types.ml b/lib/types.ml index 88c59cc..7c267a1 100644 --- a/lib/types.ml +++ b/lib/types.ml @@ -68,22 +68,47 @@ let bits = function 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 - | "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 | _ -> None 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 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 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 = [ "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 = (if signed k then "i" else "u") ^ string_of_int (bits k) diff --git a/test/programs/int-float.flan b/test/programs/int-float.flan new file mode 100644 index 0000000..6aeb8d2 --- /dev/null +++ b/test/programs/int-float.flan @@ -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) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 40293cc..5d8a0b3 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -1243,6 +1243,19 @@ let () = string_eq_out; outputs ~x86:true "string equality, --x86" "programs/string-eq.flan" 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 nil/false vs. everything else, Clojure's rule, on both backends; a typed scrutinee stays strictly bool, which is a checker test diff --git a/test/test_flan.ml b/test/test_flan.ml index b773aeb..b3b33e6 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -2245,9 +2245,12 @@ let () = rejects_check "a plain type typo keeps the short answer" "(defvar total i33) (defn f [] ())" ~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" - "(defvar total int) (defn f [] ())" - ~needle:"unknown type int — Flan spells it i32"; + "(defvar total long) (defn f [] ())" + ~needle:"unknown type long — Flan spells it i64"; rejects_check "a data case is not a type, and says what is" "(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 \ @@ -2521,6 +2524,81 @@ let () = "(defenum E [a 1]) (defstruct E [x i32])" ~needle:"defined twice"; rejects_check "an extern and a constant" "(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 poison return is a function that returns nothing at all. A literal count is rejected; a computed one is masked in [emit]. *)