A dyn goes into every integer width range-checked, a char where its code point fits, and the trap names the width and what it found.

This commit is contained in:
Joseph Ferano 2026-09-26 12:27:40 +07:00
parent d5437c3f22
commit 9c47d3bacc
7 changed files with 191 additions and 52 deletions

View File

@ -30,11 +30,12 @@ dyn-unless-annotated design.
** DONE Dyn has a char, and dyn text counts characters ** DONE Dyn has a char, and dyn text counts characters
CLOSED: [2026-09-26] CLOSED: [2026-09-26]
Only a char literal, =at= on a text and =chars= make one, and it prints as its Only a char literal, =at= on a text and =chars= make one, and it prints as its
literal, bare too, a control character as \\uXXXX; into a typed i32 (the prelude's literal, bare too, a control character as \\uXXXX. Into any integer width it gives its
rune) or through (i32 c) it gives its code point. length, at and slice on dyn text count code point where that fits, into a byte only when ASCII; a dyn int into any width is
code points, a malformed byte counting as one U+FFFD. A non-ASCII literal defaults to range-checked, while a cast on either wraps as a typed cast does. length, at and slice
i32 and is refused where a u8 is wanted. Rules out char arithmetic, a typed code point on dyn text count code points, a malformed byte counting as one U+FFFD. A non-ASCII
turning into a char, and byte offsets on dyn text. literal defaults to i32 and is refused where a byte is wanted. Rules out char
arithmetic, a typed code point turning into a char, and byte offsets on dyn text.
** DONE Any typed container crosses into dyn as a view ** DONE Any typed container crosses into dyn as a view
CLOSED: [2026-09-26] CLOSED: [2026-09-26]

View File

@ -4185,38 +4185,34 @@ let mismatch_found : Types.t Found.t = Found.create 16
let unbox loc (want : Types.t) (e : Tast.expr) : Tast.expr = let unbox loc (want : Types.t) (e : Tast.expr) : Tast.expr =
let need sym ty = rt loc ty sym [ e ] in let need sym ty = rt loc ty sym [ e ] in
match want with match want with
| Types.Int Types.I64 -> need "flan_dyn_need_i64" dyn_i64
| Types.Float Types.F64 -> need "flan_dyn_need_f64" dyn_f64 | Types.Float Types.F64 -> need "flan_dyn_need_f64" dyn_f64
| Types.Bool -> | Types.Bool ->
(* The ABI answers an [int32_t]; [bool] is an [i1]. The narrowing is the (* The ABI answers an [int32_t]; [bool] is an [i1]. The narrowing is the
language's own cast and cannot fail — the runtime already decided the language's own cast and cannot fail — the runtime already decided the
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))
(* An int that fits, range-checked at run time at this site, or a char's (* Any integer width, checked at run time at this site (TODO.org, "Dyn
code point: an i32 is the prelude's rune. *) unless annotated"): an int in the width's range, or a char's code point
| Types.Int Types.I32 -> where it fits — ASCII only into a byte, since a byte past ASCII is not
rt loc want "flan_dyn_need_i32" [ e; here loc ] that char in UTF-8. The runtime answers an i64 it has already checked,
(* Every other width is refused rather than served by a need_i64 and a so the narrowing after it cannot lose a bit. *)
truncation. Narrowing is written or it does not happen — that survives | Types.Int k ->
widening becoming implicit (TODO.org, "Implicit numeric widening is let code =
legal; narrowing stays a hard error") untouched, and this is the match k with
boundary where it matters most: the value's type was *already* uncertain | Types.I8 -> 0L | Types.U8 -> 1L | Types.I16 -> 2L | Types.U16 -> 3L
here, so an annotation that quietly discarded the high bits would read as | Types.I32 -> 4L | Types.U32 -> 5L | Types.I64 -> 6L | Types.U64 -> 7L
a check and be the opposite of one. in
let n =
Nor does widening reach this arm from the other side. The box carries one rt loc dyn_i64 "flan_dyn_need_int"
integer width and one float width, so there is no narrower source here to [ e; mk loc (Types.Int Types.I32) (Tast.Int (code, Types.I32)); here loc ]
widen from — a u32 want is asking the i64 in the box to fit in half of in
itself, which is the refusal above and not a conversion the lattice has. if k = Types.I64 then n else mk loc want (Tast.Prim (Tast.Cast want, [ n ]))
The ABI grows a per-width entry point when there is a reason to; until (* An f32 is refused rather than served by a need_f64 and a rounding: the
then the spelling that works is an i64 and a written conversion after box carries one float width, and narrowing is written or it does not
it. *) happen (TODO.org, "Implicit numeric widening is legal; narrowing stays a
| Types.Int _ | Types.Float _ -> hard error"). *)
no_dyn_yet loc ~into:false want | Types.Float _ ->
(Printf.sprintf no_dyn_yet loc ~into:false want " — take it as f64 and convert"
" — take it as %s and convert"
(if Types.is_numeric want && (match want with Types.Float _ -> true | _ -> false)
then "f64" else "i64"))
| _ -> no_dyn_yet loc ~into:false want "" | _ -> no_dyn_yet loc ~into:false want ""
(* ── A numeric cast written on a dyn ───────────────────────────────── (* ── A numeric cast written on a dyn ─────────────────────────────────

View File

@ -5070,6 +5070,7 @@ declare i64 @flan_dyn_need_i64(i64)
declare double @flan_dyn_need_f64(i64) declare double @flan_dyn_need_f64(i64)
declare i32 @flan_dyn_need_bool(i64) declare i32 @flan_dyn_need_bool(i64)
declare i32 @flan_dyn_need_i32(i64, ptr, i64) declare i32 @flan_dyn_need_i32(i64, ptr, i64)
declare i64 @flan_dyn_need_int(i64, i32, ptr, i64)
declare i64 @flan_dyn_int_of(i64) declare i64 @flan_dyn_int_of(i64)
; A numeric cast written on a dyn answers which numeric tag the box holds; ; A numeric cast written on a dyn answers which numeric tag the box holds;
; check.ml's [cast_dyn] branches on it and each arm is an ordinary need plus ; check.ml's [cast_dyn] branches on it and each arm is an ordinary need plus

View File

@ -2691,31 +2691,70 @@ double flan_dyn_need_f64(flan_dyn v) {
static const char *an(const char *w); /* forward: "a" or "an" */ static const char *an(const char *w); /* forward: "a" or "an" */
/* A dyn into a typed i32: an int that fits, range-checked, or a char's code /* A dyn into a typed integer of width [kind] — 0..7 for i8 u8 i16 u16 i32
* point — an i32 is the prelude's rune. The sentence names what was found * u32 i64 u64, check.ml's [unbox] — answered as an i64 the caller narrows:
* and what would do, and no call: the site in front of it is the one that * an int in the width's range, or a char's code point where it fits. A byte
* failed. */ * takes only an ASCII char, because a byte past ASCII is not that char in
int32_t flan_dyn_need_i32(flan_dyn v, const uint8_t *loc, int64_t loclen) { * UTF-8. A dyn int is an i64, so a u64 takes 0 up to the largest i64. The
* sentence names what was found and what would do, and no call: the site in
* front of it is the one that failed. */
static const char *const int_names[8] = { "i8", "u8", "i16", "u16",
"i32", "u32", "i64", "u64" };
int64_t flan_dyn_need_int(flan_dyn v, int32_t kind, const uint8_t *loc,
int64_t loclen) {
static const int64_t lo[8] = { -128, 0, -32768, 0, INT32_MIN, 0, INT64_MIN, 0 };
static const int64_t hi[8] = { 127, 255, 32767, 65535, INT32_MAX,
4294967295LL, INT64_MAX, INT64_MAX };
int32_t t = flan_dyn_tag(v); int32_t t = flan_dyn_tag(v);
const char *name;
char sv[SAY_MAX]; char sv[SAY_MAX];
if (t == FLAN_DYN_TAG_CHAR) return (int32_t)dyn_payload(v); if (kind < 0 || kind > 7) kind = 6;
if (t == FLAN_DYN_TAG_INT) { name = int_names[kind];
int64_t x = dyn_int_value(v); if (t == FLAN_DYN_TAG_CHAR) {
if (x >= INT32_MIN && x <= INT32_MAX) return (int32_t)x; int64_t cp = (int64_t)dyn_payload(v);
flan_say(loc, loclen, int64_t top = kind <= 1 ? 127 : hi[kind];
"dyn: an i32 is wanted here, and the int %lld is outside an " char cs[16];
"i32's range", (long long)x); if (cp <= top) return cp;
char_spell((uint32_t)cp, cs);
if (kind <= 1)
flan_say(loc, loclen,
"dyn: %s %s is wanted here, and the char %s is more than one "
"byte in UTF-8. Take its code point as an i32",
an(name), name, cs);
else
flan_say(loc, loclen,
"dyn: %s %s is wanted here, and the char %s, code point %lld, "
"is outside %s %s's range", an(name), name, cs, (long long)cp,
an(name), name);
flan_trap((const uint8_t *)"DynRange", 8); flan_trap((const uint8_t *)"DynRange", 8);
} }
say(sv, SAY_MAX, v); if (t == FLAN_DYN_TAG_INT) {
int64_t x = dyn_int_value(v);
if (x >= lo[kind] && x <= hi[kind]) return x;
flan_say(loc, loclen,
"dyn: %s %s is wanted here, and the int %lld is outside %s %s's "
"range", an(name), name, (long long)x, an(name), name);
flan_trap((const uint8_t *)"DynRange", 8);
}
if (t == FLAN_DYN_TAG_NIL) sv[0] = '\0';
else say(sv, SAY_MAX, v);
flan_say(loc, loclen, flan_say(loc, loclen,
"dyn: an i32 is wanted here, and this is %s %s, %s. An i32 takes an " "dyn: %s %s is wanted here, and this is %s%s%s%s%s. %s %s takes an "
"int or a char's code point%s", "int or a char's code point%s%s%s",
an(tag_of(v)), tag_of(v), sv, an(name), name, t == FLAN_DYN_TAG_NIL ? "" : an(tag_of(v)),
t == FLAN_DYN_TAG_FLOAT ? "; convert a float with (i32 x)" : ""); t == FLAN_DYN_TAG_NIL ? "" : " ", tag_of(v), t == FLAN_DYN_TAG_NIL ? "" : ", ", sv,
name[0] == 'i' ? "An" : "A", name,
t == FLAN_DYN_TAG_FLOAT ? "; convert a float with (" : "",
t == FLAN_DYN_TAG_FLOAT ? name : "",
t == FLAN_DYN_TAG_FLOAT ? " x)" : "");
flan_trap((const uint8_t *)"DynType", 7); flan_trap((const uint8_t *)"DynType", 7);
} }
int32_t flan_dyn_need_i32(flan_dyn v, const uint8_t *loc, int64_t loclen) {
return (int32_t)flan_dyn_need_int(v, 4, loc, loclen);
}
/* The int arm of a numeric cast written on a dyn ([check.ml]'s [cast_dyn]), /* The int arm of a numeric cast written on a dyn ([check.ml]'s [cast_dyn]),
* reached once [flan_dyn_cast_kind] has said the box is not a float: an * reached once [flan_dyn_cast_kind] has said the box is not a float: an
* int's value, or a char's code point, so (i32 c) is the code point the * int's value, or a char's code point, so (i32 c) is the code point the

View File

@ -279,8 +279,12 @@ void flan_dyn_emit_watch(flan_dyn v);
int64_t flan_dyn_need_i64(flan_dyn v); int64_t flan_dyn_need_i64(flan_dyn v);
double flan_dyn_need_f64(flan_dyn v); double flan_dyn_need_f64(flan_dyn v);
uint8_t flan_dyn_need_bool(flan_dyn v); uint8_t flan_dyn_need_bool(flan_dyn v);
/* For a typed i32: an int in range, or a char's code point (the prelude's /* For a typed integer of width [kind], 0..7 for i8 u8 i16 u16 i32 u32 i64
* rune). Anything else, or an int out of range, traps at [loc]. */ * u64: an int in range, or a char's code point where it fits (ASCII only
* into a byte), as an i64 the caller narrows. Anything else traps at [loc].
* [flan_dyn_need_i32] is kind 4. */
int64_t flan_dyn_need_int(flan_dyn v, int32_t kind, const uint8_t *loc,
int64_t loclen);
int32_t flan_dyn_need_i32(flan_dyn v, const uint8_t *loc, int64_t loclen); int32_t flan_dyn_need_i32(flan_dyn v, const uint8_t *loc, int64_t loclen);
/* A numeric cast's int arm: an int's value or a char's code point. */ /* A numeric cast's int arm: an int's value or a char's code point. */
int64_t flan_dyn_int_of(flan_dyn v); int64_t flan_dyn_int_of(flan_dyn v);

View File

@ -0,0 +1,48 @@
;;;; A dyn into a typed integer of every width: an int in the width's range
;;;; passes, and so does a char's code point where it fits (ASCII only into a
;;;; byte). A cast on a dyn is the typed cast after an unbox, so it takes a
;;;; char too and wraps as a typed cast does. With an argument, one crossing
;;;; out of range traps at its call.
(defn to-i8 [x i8] i64 (i64 x))
(defn to-u8 [x u8] i64 (i64 x))
(defn to-i16 [x i16] i64 (i64 x))
(defn to-u16 [x u16] i64 (i64 x))
(defn to-i32 [x i32] i64 (i64 x))
(defn to-u32 [x u32] i64 (i64 x))
(defn to-i64 [x i64] i64 x)
(defn to-u64 [x u64] u64 x)
(defn d [x] dyn x)
(defn main [args [str]] i32
(do
;; the edges of each width, both ends, and a char at each
(println (to-i8 (d -128)) (to-i8 (d 127)) (to-i8 (d \a)))
(println (to-u8 (d 0)) (to-u8 (d 255)) (to-u8 (d \a)))
(println (to-i16 (d -32768)) (to-i16 (d 32767)) (to-i16 (d \é)))
(println (to-u16 (d 0)) (to-u16 (d 65535)) (to-u16 (d \日)))
(println (to-i32 (d -2147483648)) (to-i32 (d 2147483647)) (to-i32 (d \😀)))
(println (to-u32 (d 0)) (to-u32 (d 4294967295)) (to-u32 (d \😀)))
(println (to-i64 (d -9223372036854775807)) (to-i64 (d 9223372036854775807))
(to-i64 (d \😀)))
(println (to-u64 (d 0)) (to-u64 (d 9223372036854775807)) (to-u64 (d \😀)))
;; casts: a char at every width, and the typed cast's wrap
(println (i8 (d \a)) (u8 (d \a)) (i16 (d \a)) (u16 (d \a))
(i32 (d \a)) (u32 (d \a)) (i64 (d \a)) (u64 (d \a)))
(println (u32 (d -1)) (u32 (the i64 -1)) (u8 (d 256)) (u8 (the i64 256)))
(when (> (length args) 1)
(let [w (at args 1)]
(cond
(= w "u8-256") (println (to-u8 (d 256)))
(= w "u8-neg") (println (to-u8 (d -1)))
(= w "i8-128") (println (to-i8 (d 128)))
(= w "i16-big") (println (to-i16 (d 32768)))
(= w "u16-big") (println (to-u16 (d 65536)))
(= w "i32-big") (println (to-i32 (d 2147483648)))
(= w "u32-neg") (println (to-u32 (d -1)))
(= w "u64-neg") (println (to-u64 (d -1)))
(= w "u8-char") (println (to-u8 (d \é)))
(= w "u16-char") (println (to-u16 (d \😀)))
:else (println (to-i64 (d "x")))))))
0)

View File

@ -5360,6 +5360,56 @@ level "1"
outputs "dyn: chars" "programs/dyn-char.flan" dyn_char_out; outputs "dyn: chars" "programs/dyn-char.flan" dyn_char_out;
outputs ~opt:"-O0" "dyn: chars, -O0" "programs/dyn-char.flan" dyn_char_out; outputs ~opt:"-O0" "dyn: chars, -O0" "programs/dyn-char.flan" dyn_char_out;
outputs ~x86:true "dyn: chars, --x86" "programs/dyn-char.flan" dyn_char_out; outputs ~x86:true "dyn: chars, --x86" "programs/dyn-char.flan" dyn_char_out;
(* A dyn into every integer width: both edges pass, a char passes where
it fits, a cast takes a char and wraps as the typed cast beside it
does; then one trap per width past its range, a char too wide, and a
text, each at its own call. *)
let widths_out =
"-128 127 97\n0 255 97\n-32768 32767 233\n0 65535 26085\n\
-2147483648 2147483647 128512\n0 4294967295 128512\n\
-9223372036854775807 9223372036854775807 128512\n\
0 9223372036854775807 128512\n97 97 97 97 97 97 97 97\n\
4294967295 4294967295 0 0\n"
in
outputs "dyn: every integer width" "programs/dyn-int-widths.flan" widths_out;
outputs ~opt:"-O0" "dyn: every integer width, -O0"
"programs/dyn-int-widths.flan" widths_out;
outputs ~x86:true "dyn: every integer width, --x86"
"programs/dyn-int-widths.flan" widths_out;
List.iter
(fun x86 ->
let exe = compile ~x86 "programs/dyn-int-widths.flan" in
List.iter
(fun (arg, want) ->
let want = "programs/dyn-int-widths.flan:" ^ want in
let code, text = run exe (Some arg) in
if code <> 134 || not (contains text want) then begin
incr failures;
Printf.printf
"FAIL dyn: %s traps%s\n got: %S (exit %d)\n \
wanted: %S (exit 134)\n"
arg (if x86 then ", --x86" else "") text code want
end)
[ ("u8-256", "37:42: dyn: a u8 is wanted here, and the int 256 is \
outside a u8's range");
("u8-neg", "38:42: dyn: a u8 is wanted here, and the int -1 is \
outside a u8's range");
("i8-128", "39:42: dyn: an i8 is wanted here, and the int 128 is \
outside an i8's range");
("i16-big", "40:44: dyn: an i16 is wanted here, and the int 32768");
("u16-big", "41:44: dyn: a u16 is wanted here, and the int 65536");
("i32-big", "42:44: dyn: an i32 is wanted here, and the int \
2147483648");
("u32-neg", "43:44: dyn: a u32 is wanted here, and the int -1 is \
outside a u32's range");
("u64-neg", "44:44: dyn: a u64 is wanted here, and the int -1");
("u8-char", "45:43: dyn: a u8 is wanted here, and the char \\é is \
more than one byte in UTF-8");
("u16-char", "46:45: dyn: a u16 is wanted here, and the char \
\\😀, code point 128512, is outside a u16's range");
("x", "47:34: dyn: an i64 is wanted here, and this is a text, \
\"x\". An i64 takes an int or a char's code point") ])
[ false; true ];
(* Print, then read: each char dyn-char-spell.flan prints — every ASCII (* Print, then read: each char dyn-char-spell.flan prints — every ASCII
code point, the C1 controls, then four past them — reads back as the code point it was, code point, the C1 controls, then four past them — reads back as the code point it was,
and so does the compiler's own spelling of the same literal, which is and so does the compiler's own spelling of the same literal, which is
@ -5567,7 +5617,7 @@ level "1"
Matched on the half that carries the meaning rather than on the Matched on the half that carries the meaning rather than on the
whole sentence, so the row is about the trap being reached with whole sentence, so the row is about the trap being reached with
the right two things in hand and not about punctuation. *) the right two things in hand and not about punctuation. *)
|| not (contains text "float, and an int was wanted") || not (contains text "an i64 is wanted here, and this is a float, 1.5")
then begin then begin
incr failures; incr failures;
Printf.printf Printf.printf
@ -5615,7 +5665,7 @@ level "1"
in in
if code <> 134 if code <> 134
|| not (contains text nil_option_out) || not (contains text nil_option_out)
|| not (contains text "int was wanted") || not (contains text "an i64 is wanted here, and this is nil.")
then begin then begin
incr failures; incr failures;
Printf.printf Printf.printf