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
CLOSED: [2026-09-26]
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
rune) or through (i32 c) it gives its code point. length, at and slice on dyn text count
code points, a malformed byte counting as one U+FFFD. A non-ASCII literal defaults to
i32 and is refused where a u8 is wanted. Rules out char arithmetic, a typed code point
turning into a char, and byte offsets on dyn text.
literal, bare too, a control character as \\uXXXX. Into any integer width it gives its
code point where that fits, into a byte only when ASCII; a dyn int into any width is
range-checked, while a cast on either wraps as a typed cast does. length, at and slice
on dyn text count code points, a malformed byte counting as one U+FFFD. A non-ASCII
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
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 need sym ty = rt loc ty sym [ e ] in
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.Bool ->
(* 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
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))
(* An int that fits, range-checked at run time at this site, or a char's
code point: an i32 is the prelude's rune. *)
| Types.Int Types.I32 ->
rt loc want "flan_dyn_need_i32" [ e; here loc ]
(* Every other width is refused rather than served by a need_i64 and a
truncation. Narrowing is written or it does not happen — that survives
widening becoming implicit (TODO.org, "Implicit numeric widening is
legal; narrowing stays a hard error") untouched, and this is the
boundary where it matters most: the value's type was *already* uncertain
here, so an annotation that quietly discarded the high bits would read as
a check and be the opposite of one.
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 _ ->
no_dyn_yet loc ~into:false want
(Printf.sprintf
" — take it as %s and convert"
(if Types.is_numeric want && (match want with Types.Float _ -> true | _ -> false)
then "f64" else "i64"))
(* Any integer width, checked at run time at this site (TODO.org, "Dyn
unless annotated"): an int in the width's range, or a char's code point
where it fits — ASCII only into a byte, since a byte past ASCII is not
that char in UTF-8. The runtime answers an i64 it has already checked,
so the narrowing after it cannot lose a bit. *)
| Types.Int k ->
let code =
match k with
| Types.I8 -> 0L | Types.U8 -> 1L | Types.I16 -> 2L | Types.U16 -> 3L
| Types.I32 -> 4L | Types.U32 -> 5L | Types.I64 -> 6L | Types.U64 -> 7L
in
let n =
rt loc dyn_i64 "flan_dyn_need_int"
[ e; mk loc (Types.Int Types.I32) (Tast.Int (code, Types.I32)); here loc ]
in
if k = Types.I64 then n else mk loc want (Tast.Prim (Tast.Cast want, [ n ]))
(* An f32 is refused rather than served by a need_f64 and a rounding: the
box carries one float width, and narrowing is written or it does not
happen (TODO.org, "Implicit numeric widening is legal; narrowing stays a
hard error"). *)
| Types.Float _ ->
no_dyn_yet loc ~into:false want " — take it as f64 and convert"
| _ -> no_dyn_yet loc ~into:false want ""
(* ── 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 i32 @flan_dyn_need_bool(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)
; 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

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" */
/* A dyn into a typed i32: an int that fits, range-checked, or a char's code
* point — an i32 is the prelude's rune. The sentence names what was found
* and what would do, and no call: the site in front of it is the one that
* failed. */
int32_t flan_dyn_need_i32(flan_dyn v, const uint8_t *loc, int64_t loclen) {
/* A dyn into a typed integer of width [kind] — 0..7 for i8 u8 i16 u16 i32
* u32 i64 u64, check.ml's [unbox] — answered as an i64 the caller narrows:
* an int in the width's range, or a char's code point where it fits. A byte
* takes only an ASCII char, because a byte past ASCII is not that char in
* 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);
const char *name;
char sv[SAY_MAX];
if (t == FLAN_DYN_TAG_CHAR) return (int32_t)dyn_payload(v);
if (t == FLAN_DYN_TAG_INT) {
int64_t x = dyn_int_value(v);
if (x >= INT32_MIN && x <= INT32_MAX) return (int32_t)x;
flan_say(loc, loclen,
"dyn: an i32 is wanted here, and the int %lld is outside an "
"i32's range", (long long)x);
if (kind < 0 || kind > 7) kind = 6;
name = int_names[kind];
if (t == FLAN_DYN_TAG_CHAR) {
int64_t cp = (int64_t)dyn_payload(v);
int64_t top = kind <= 1 ? 127 : hi[kind];
char cs[16];
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);
}
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,
"dyn: an i32 is wanted here, and this is %s %s, %s. An i32 takes an "
"int or a char's code point%s",
an(tag_of(v)), tag_of(v), sv,
t == FLAN_DYN_TAG_FLOAT ? "; convert a float with (i32 x)" : "");
"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%s%s",
an(name), name, t == FLAN_DYN_TAG_NIL ? "" : an(tag_of(v)),
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);
}
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]),
* 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

View File

@ -279,8 +279,12 @@ void flan_dyn_emit_watch(flan_dyn v);
int64_t flan_dyn_need_i64(flan_dyn v);
double flan_dyn_need_f64(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
* rune). Anything else, or an int out of range, traps at [loc]. */
/* For a typed integer of width [kind], 0..7 for i8 u8 i16 u16 i32 u32 i64
* 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);
/* 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);

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 ~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;
(* 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
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
@ -5567,7 +5617,7 @@ level "1"
Matched on the half that carries the meaning rather than on the
whole sentence, so the row is about the trap being reached with
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
incr failures;
Printf.printf
@ -5615,7 +5665,7 @@ level "1"
in
if code <> 134
|| 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
incr failures;
Printf.printf