From 9c47d3bacc245d1c98f486c0bfb2901a6f544f2f Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 12:27:40 +0700 Subject: [PATCH] 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. --- TODO.org | 11 ++--- lib/check.ml | 48 ++++++++++---------- lib/emit.ml | 1 + runtime/flan_dyn.c | 73 ++++++++++++++++++++++++------- runtime/flan_dyn.h | 8 +++- test/programs/dyn-int-widths.flan | 48 ++++++++++++++++++++ test/test_acceptance.ml | 54 ++++++++++++++++++++++- 7 files changed, 191 insertions(+), 52 deletions(-) create mode 100644 test/programs/dyn-int-widths.flan diff --git a/TODO.org b/TODO.org index 539a3fe2..560bda95 100644 --- a/TODO.org +++ b/TODO.org @@ -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] diff --git a/lib/check.ml b/lib/check.ml index 9e07dae7..3558f8f3 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -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 ───────────────────────────────── diff --git a/lib/emit.ml b/lib/emit.ml index 9636f84c..bef243f1 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -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 diff --git a/runtime/flan_dyn.c b/runtime/flan_dyn.c index 7fb3ee2f..1c76b1f7 100644 --- a/runtime/flan_dyn.c +++ b/runtime/flan_dyn.c @@ -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 diff --git a/runtime/flan_dyn.h b/runtime/flan_dyn.h index 372af22b..24a290f9 100644 --- a/runtime/flan_dyn.h +++ b/runtime/flan_dyn.h @@ -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); diff --git a/test/programs/dyn-int-widths.flan b/test/programs/dyn-int-widths.flan new file mode 100644 index 00000000..7faf69da --- /dev/null +++ b/test/programs/dyn-int-widths.flan @@ -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) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index bf2c3b08..604e43dc 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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