Every no-implicit-widening comment now says what is true instead

This commit is contained in:
Joseph Ferano 2026-09-20 18:33:15 +07:00
parent d0e33331b5
commit 3e4267f57c
6 changed files with 72 additions and 43 deletions

23
FIX.org
View File

@ -2555,11 +2555,18 @@ and the wrap width would silently follow the count's declared type — and the
emitter's poison mask is keyed to the value's width. A count wider than the emitter's poison mask is keyed to the value's width. A count wider than the
value is refused and says so. value is refused and says so.
** Const folding is unchanged ** Const folding is unchanged, and was never the thing it looked like
The ~defconst~ integer folder (lib/check.ml) folds literal arithmetic within The ~defconst~ integer folder (~const_int~, lib/check.ml) runs on the *AST*,
one type and does not walk through a ~Cast~ node. So a widened operand is not before anything has a type, and carries one ~int64~ per constant with no width
a folded constant: ~(defconst n (+ small-i32-const big-i64-const))~ compiles attached. So it already folded across widths and still does —
and computes at run time rather than folding, and an array length written that ~(defconst w i32 4)~ times ~(defconst h i64 5)~ has always been a constant 20,
way is refused as it was before. Kept as it is on purpose — the folder's job usable as an array length — and widening neither added a fold nor removed one.
is array lengths and it already covers the same-type arithmetic they are Measured, not assumed.
written with.
The one thing that did change is at the edges rather than in the folder: it
answers nothing for a ~Call~ whose operator is not one of the five arithmetic
names, and a written cast is such a call. So ~(* w (i64 h))~ was not a
constant and ~(* w h)~ is — which means dropping a cast that widening made
unnecessary can turn a run-time computation into an array length. That is
widening adding a program, the same as everywhere else, and needed no change
here.

View File

@ -806,7 +806,8 @@ fact without cutting anything in half. See "The browser is the third target" bel
**Three edits were made to sand.flan's own text** when it was ported, and they are language decisions rather than fixes: **Three edits were made to sand.flan's own text** when it was ported, and they are language decisions rather than fixes:
- `(defconst gravity 0.05)``(defconst gravity f32 0.05)`. An untyped float constant is `f64`, `velocity` is `[f32]`, - `(defconst gravity 0.05)``(defconst gravity f32 0.05)`. An untyped float constant is `f64`, `velocity` is `[f32]`,
and there is no implicit widening. and `f64` into `f32` is a narrowing — still written, and still written after implicit widening landed (FIX.org
2026-09-20), because widening is only the conversions that cannot change the number and this one can.
- `(defvar current-color u32)``i32`. It is an index into `colors`, and `(len colors)` is an `i32`. - `(defvar current-color u32)``i32`. It is an index into `colors`, and `(len colors)` is an `i32`.
- `(defn main [])` is unchanged — the short form, as plan.org says. - `(defn main [])` is unchanged — the short form, as plan.org says.
@ -3934,8 +3935,9 @@ compile-time constant*. Both of emit.ml's string emitters take the bytes and ign
constant either way and this one is a constant a global can hold. constant either way and this one is a constant a global can hold.
**Two spellings, not one form that changes type with its context.** Odin threads a `type_hint` everywhere and can **Two spellings, not one form that changes type with its context.** Odin threads a `type_hint` everywhere and can
afford `#load("p")` to mean a `string` here and a `[]u8` there. With structural equality, no implicit widening and no afford `#load("p")` to mean a `string` here and a `[]u8` there. With structural equality and no conversion between one
coercion anywhere, the same text meaning two types would be a wart, so `string` is written down when it is wanted. The container and another — implicit widening is numbers only — the same text meaning two types would be a wart, so
`string` is written down when it is wanted. The
site's expectation is a fallback only and nothing depends on it. site's expectation is a fallback only and nothing depends on it.
**The path is a literal and resolves relative to the file the form is written in.** Both are Odin's rules and for **The path is a literal and resolves relative to the file the form is written in.** Both are Odin's rules and for

View File

@ -2026,12 +2026,19 @@ let unbox loc (want : Types.t) (e : Tast.expr) : Tast.expr =
value was a bool, so what comes back is 0 or 1. *) value was a bool, so what comes back is 0 or 1. *)
widen loc Types.Bool (need "flan_dyn_need_bool" (Types.Int Types.I32)) widen loc Types.Bool (need "flan_dyn_need_bool" (Types.Int Types.I32))
(* Every other width is refused rather than served by a need_i64 and a (* Every other width is refused rather than served by a need_i64 and a
truncation. This language has no implicit narrowing anywhere, and putting truncation. Narrowing is written or it does not happen that survives
one at the boundary where a value's type was *already* uncertain is the widening becoming implicit (FIX.org 2026-09-20) untouched, and this is the
worst place in the program to start: the annotation would read as a check boundary where it matters most: the value's type was *already* uncertain
and would be a silent discard of the high bits. The ABI grows a per-width here, so an annotation that quietly discarded the high bits would read as
entry point when there is a reason to; until then the spelling that works a check and be the opposite of one.
is an i64 and an explicit conversion after it. *)
Nor does widening reach this arm from the other side. The box carries one
integer width and one float width, so there is no narrower source here to
widen from a u32 want is asking the i64 in the box to fit in half of
itself, which is the refusal above and not a conversion the lattice has.
The ABI grows a per-width entry point when there is a reason to; until
then the spelling that works is an i64 and a written conversion after
it. *)
| Types.Int _ | Types.Float _ -> | Types.Int _ | Types.Float _ ->
no_dyn_yet loc ~into:false want no_dyn_yet loc ~into:false want
(Printf.sprintf (Printf.sprintf
@ -6520,9 +6527,11 @@ and named_call ctx ~want loc name args =
let as_bytes () = mk loc (Types.Slice (Types.Int Types.U8)) (Tast.Str data) in let as_bytes () = mk loc (Types.Slice (Types.Int Types.U8)) (Tast.Str data) in
(* Two spellings rather than one that changes type with its context. (* Two spellings rather than one that changes type with its context.
Odin threads a type_hint everywhere and can afford (embed "p") to Odin threads a type_hint everywhere and can afford (embed "p") to
mean a string here and a []u8 there; with structural equality and no mean a string here and a []u8 there; with structural equality and a
implicit widening anywhere, the same text meaning two types would be container that never converts to another container -- implicit
a wart. [want] is a fallback only, and nothing depends on it. *) widening is numbers only, FIX.org 2026-09-20 -- the same text meaning
two types would be a wart. [want] is a fallback only, and nothing
depends on it. *)
(match args with (match args with
| [ _; { Ast.e = Ast.Var "string"; _ } ] -> | [ _; { Ast.e = Ast.Var "string"; _ } ] ->
expect ctx loc ~want (as_string ()) expect ctx loc ~want (as_string ())
@ -7638,8 +7647,9 @@ and binary ctx ?(dyn_ok = false) ?(join = true) name loc ~want args =
let builtins : (string * string * string) list = let builtins : (string * string * string) list =
[ (* arithmetic and comparison *) [ (* arithmetic and comparison *)
("+", "+ [numeric? ...] numeric?", ("+", "+ [numeric? ...] numeric?",
"Sum, folded left over two or more operands that share one numeric \ "Sum, folded left over two or more operands. Two operands of different \
type nothing widens implicitly."); numeric types meet at the wider one when that cannot lose i32 and i64 \
add at i64 and i32 with u32 has no such type and is refused.");
("-", "- [numeric? ...] numeric?", ("-", "- [numeric? ...] numeric?",
"Difference, folded left: (- a b c) is ((a - b) - c)."); "Difference, folded left: (- a b c) is ((a - b) - c).");
("*", "* [numeric? ...] numeric?", ("*", "* [numeric? ...] numeric?",
@ -7664,20 +7674,23 @@ let builtins : (string * string * string) list =
("not", "not [bool] bool", ("not", "not [bool] bool",
"Negates a bool. Nothing else in this language is a truth value."); "Negates a bool. Nothing else in this language is a truth value.");
("bit-and", "bit-and [int ...] int", ("bit-and", "bit-and [int ...] int",
"Bitwise and, folded left. Integers only, and every operand has the \ "Bitwise and, folded left. Integers only; operands of different widths \
same width."); meet at the wider one, the way + does.");
("bit-or", "bit-or [int ...] int", "Bitwise or, folded left over integers."); ("bit-or", "bit-or [int ...] int", "Bitwise or, folded left over integers.");
("bit-xor", "bit-xor [int ...] int", ("bit-xor", "bit-xor [int ...] int",
"Bitwise exclusive or, folded left over integers."); "Bitwise exclusive or, folded left over integers.");
("<<", "<< [int int] int", ("<<", "<< [int int] int",
"Left shift. The count has the shifted value's own type, and a literal \ "Left shift. The value's type decides — a narrower count widens to it, a \
count at or past its width is refused LLVM calls that poison."); wider one is refused and a literal count at or past the value's width \
is refused too, because LLVM calls that poison.");
(">>", ">> [int int] int", (">>", ">> [int int] int",
"Right shift, by a count of the value's own type; a literal count at or \ "Right shift. The value's type decides and the count widens to it, never \
past the width is refused, as it is for <<."); the reverse; a literal count at or past the width is refused, as it is \
for <<.");
("min", "min [ordered? ...] ordered?", ("min", "min [ordered? ...] ordered?",
"The smallest of two or more operands, each of them evaluated exactly \ "The smallest of two or more operands, each of them evaluated exactly \
once however many there are."); once however many there are. Two widths meet at the wider: (min i8-x \
i16-y) is an i16.");
("max", "max [ordered? ...] ordered?", ("max", "max [ordered? ...] ordered?",
"The largest of two or more operands, each evaluated exactly once."); "The largest of two or more operands, each evaluated exactly once.");
("zeroed", "zeroed [] T", ("zeroed", "zeroed [] T",

View File

@ -31,12 +31,14 @@
is strictly the better call for every one of them. [print] is the same walk is strictly the better call for every one of them. [print] is the same walk
as [println] without the trailing newline, so it covers the no-newline case as [println] without the trailing newline, so it covers the no-newline case
that was the family's remaining excuse (see [show] in that was the family's remaining excuse (see [show] in
test/programs/slices.flan). And because this language has no implicit test/programs/slices.flan). And [(print-i64 x)] forced an explicit
widening, [(print-i64 x)] forced an explicit [(i64 x)] at every site; [(i64 x)] at every site, where [(print x)] takes the value as it is. That
[(print x)] takes the value as it is. That is not only shorter: the cast is not only shorter: the cast through the signed printer turned a [u64]
through the signed printer turned a [u64] above 2^63 into a negative above 2^63 into a negative number, where [print] routes it through
number, where [print] routes it through [flan_u64_to_bytes] and prints what [flan_u64_to_bytes] and prints what it actually holds. Implicit widening
it actually holds. *) (FIX.org 2026-09-20) would have removed the cast at a [u8] or an [i32] site
on its own, but not at that one -- a [u64] widens into nothing at all, and
the printer it was being forced through was the wrong one. *)
let source = {flan| let source = {flan|
;; The condition every allocating operation signals when the allocator cannot ;; The condition every allocating operation signals when the allocator cannot
@ -470,17 +472,20 @@ let source = {flan|
;; sum is the one shape a type variable cannot express, and it is worth being ;; sum is the one shape a type variable cannot express, and it is worth being
;; precise about why rather than leaving two near-identical functions looking ;; precise about why rather than leaving two near-identical functions looking
;; like an oversight. Each of these *widens*: sum-i32 accumulates in i64 and ;; like an oversight. Each of these *widens*: sum-i32 accumulates in i64 and
;; sum-f32 in f64, with an explicit cast per element, because there is no ;; sum-f32 in f64, because summing a screenful into the element's own type is
;; implicit widening anywhere in the language and summing a screenful into the ;; how a total silently wraps or absorbs. The per-element casts no longer have
;; element's own type is how a total silently wraps or absorbs. "The wider ;; to be written to say so an i32 widens into an i64 by itself, FIX.org
;; 2026-09-20 and they stay because what these two functions exist to show is
;; that the accumulator is a different type from the element. "The wider
;; type $t accumulates into" is a function from types to types — an associated ;; type $t accumulates into" is a function from types to types — an associated
;; type, or a constraint system of a kind {:where} is not and a generic sum ;; type, or a constraint system of a kind {:where} is not and a generic sum
;; that took its accumulator and its + as parameters would be reduce, which is ;; that took its accumulator and its + as parameters would be reduce, which is
;; above. ;; above.
;; Accumulates in i64 and each element is widened explicitly there is no ;; Accumulates in i64, because summing a screenful of i32 into an i32 is how a
;; implicit widening anywhere in the language, and summing a screenful of i32 ;; total silently wraps. The per-element (i64 ...) would happen on its own now;
;; into an i32 is how a total silently wraps. ;; it is written to keep the accumulator's type visible at the line that feeds
;; it.
(defn sum-i32 [s [i32]] i64 (defn sum-i32 [s [i32]] i64
(let [t (i64 0)] (let [t (i64 0)]
(dotimes [i (len s)] (dotimes [i (len s)]

View File

@ -23,8 +23,9 @@
(print (string a))) ; hello from a (print (string a))) ; hello from a
;; `string` is the second spelling, not a different meaning for the same ;; `string` is the second spelling, not a different meaning for the same
;; text. With structural equality and no implicit widening, one form that ;; text. With structural equality and nothing that converts one container
;; changes type with its context would be a wart. ;; into another -- implicit widening is numbers only -- one form that changes
;; type with its context would be a wart.
(println (embed "assets/b.bin" string)) ; BBB (println (embed "assets/b.bin" string)) ; BBB
;; Byte-exact, including bytes no text encoding would survive: emit.ml's ;; Byte-exact, including bytes no text encoding would survive: emit.ml's

1
web-files-out.txt Normal file
View File

@ -0,0 +1 @@
state