A non-ASCII char literal is refused where a byte is wanted, a dyn int or char goes into an i32 range-checked, a cast on a dyn char gives its code point, and C1 controls print as \uXXXX.

This commit is contained in:
Joseph Ferano 2026-09-26 12:15:04 +07:00
parent 8a698076eb
commit d5437c3f22
11 changed files with 159 additions and 55 deletions

View File

@ -30,11 +30,11 @@ 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; it goes into typed code only as an i32 (the prelude's rune), and a literal, bare too, a control character as \\uXXXX; into a typed i32 (the prelude's
dyn int there traps rather than converts. length, at and slice on dyn text count code rune) or through (i32 c) it gives its code point. length, at and slice on dyn text count
points, a malformed byte counting as one U+FFFD. A non-ASCII literal defaults to i32. code points, a malformed byte counting as one U+FFFD. A non-ASCII literal defaults to
Rules out char arithmetic, a typed code point turning into a char, and byte offsets on i32 and is refused where a u8 is wanted. Rules out char arithmetic, a typed code point
dyn text. 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

@ -4192,11 +4192,10 @@ let unbox loc (want : Types.t) (e : Tast.expr) : Tast.expr =
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 i32 is the prelude's rune, so it takes a dyn char's code point, and (* An int that fits, range-checked at run time at this site, or a char's
only a char: an int in the box is refused at run time, at this site, for code point: an i32 is the prelude's rune. *)
the reason the arm below refuses one at compile time. *)
| Types.Int Types.I32 -> | Types.Int Types.I32 ->
rt loc want "flan_dyn_need_char" [ e; here loc ] rt loc want "flan_dyn_need_i32" [ e; here loc ]
(* 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. Narrowing is written or it does not happen — that survives truncation. Narrowing is written or it does not happen — that survives
widening becoming implicit (TODO.org, "Implicit numeric widening is widening becoming implicit (TODO.org, "Implicit numeric widening is
@ -4284,7 +4283,7 @@ let cast_dyn ctx loc (target : Types.t) (got : Tast.expr) : Tast.expr =
[ mk loc target [ mk loc target
(Tast.If (is_float, (Tast.If (is_float,
arm "flan_dyn_need_f64" dyn_f64, arm "flan_dyn_need_f64" dyn_f64,
arm "flan_dyn_need_i64" dyn_i64)) ])) arm "flan_dyn_int_of" dyn_i64)) ]))
(* nil is written [nil] and nothing else produces it, so this is the whole of (* nil is written [nil] and nothing else produces it, so this is the whole of
"the checker can see a nil reaching here" — a name, not a dataflow fact. "the checker can see a nil reaching here" — a name, not a dataflow fact.
@ -5692,6 +5691,29 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr =
| Ast.Byte b when want = Some Types.Dyn -> | Ast.Byte b when want = Some Types.Dyn ->
rt loc Types.Dyn "flan_dyn_from_char" rt loc Types.Dyn "flan_dyn_from_char"
[ mk loc (Types.Int Types.I32) (Tast.Int (Int64.of_int b, Types.I32)) ] [ mk loc (Types.Int Types.I32) (Tast.Int (Int64.of_int b, Types.I32)) ]
(* A char literal is a code point, and a byte type holds one only when it
is ASCII: \é as a u8 would be 0xE9, which is not é in UTF-8 and never
equals a byte of it. Refused by the char's name, not its number. *)
| Ast.Byte b
when (match want with
| Some (Types.Int (Types.U8 | Types.I8)) -> b > 127
| Some (Types.Int Types.I16) -> b > 32767
| Some (Types.Int Types.U16) -> b > 65535
| _ -> false) ->
let t = tyname loc (Option.get want) in
let c = Form.byte_repr b in
if (match want with
| Some (Types.Int (Types.U8 | Types.I8)) -> true
| _ -> false)
then
Loc.failk literal_at_want loc
"%s is %d bytes in UTF-8, not one, so it is not a %s. Write the str \
\"%s\" for its bytes, or take its code point as an i32"
c (String.length (Form.utf8 b)) t (Form.utf8 b)
else
Loc.failk literal_at_want loc
"%s is code point %d, which does not fit in a %s. Take its code point \
as an i32" c b t
(* A non-ASCII literal is a code point, which a u8 cannot hold as itself: (* A non-ASCII literal is a code point, which a u8 cannot hold as itself:
its default is the prelude's rune, an i32. *) its default is the prelude's rune, an i32. *)
| Ast.Byte b -> | Ast.Byte b ->
@ -13216,7 +13238,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
at the target instead, so (u64 2935910691) and (i64 5000000000) are the at the target instead, so (u64 2935910691) and (i64 5000000000) are the
constants they say. One that fits i32 keeps the default and the cast, constants they say. One that fits i32 keeps the default and the cast,
which is what (u32 -1) has always meant. *) which is what (u32 -1) has always meant. *)
let want = let operand_want =
match (List.hd args).Ast.e, target with match (List.hd args).Ast.e, target with
| Ast.Int n, (Types.Int _ | Types.Float _) | Ast.Int n, (Types.Int _ | Types.Float _)
when Int64.compare n (-2147483648L) < 0 when Int64.compare n (-2147483648L) < 0
@ -13224,7 +13246,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
| Ast.UInt _, (Types.Int _ | Types.Float _) -> Some target | Ast.UInt _, (Types.Int _ | Types.Float _) -> Some target
| _ -> None | _ -> None
in in
let a = check ctx ?want (List.hd args) in let a = check ctx ?want:operand_want (List.hd args) in
(match a.Tast.ty with (match a.Tast.ty with
| Types.Enum _ -> () | Types.Enum _ -> ()
(* A dyn opens here — [cast_dyn], TODO.org, "A numeric cast opens a dyn (* A dyn opens here — [cast_dyn], TODO.org, "A numeric cast opens a dyn
@ -13242,7 +13264,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
~what:"a number or an enum" ~is:"a number" v ~what:"a number or an enum" ~is:"a number" v
| t -> fail loc "%s converts a number, found %s" name (tyname loc t)); | t -> fail loc "%s converts a number, found %s" name (tyname loc t));
(match a.Tast.ty with (match a.Tast.ty with
| Types.Dyn -> cast_dyn ctx loc target a | Types.Dyn -> expect ctx loc ~want (cast_dyn ctx loc target a)
| _ -> prim (Tast.Cast target) target [ a ]) | _ -> prim (Tast.Cast target) target [ a ])
(* ── ordinary calls ────────────────────────────────────────────── *) (* ── ordinary calls ────────────────────────────────────────────── *)

View File

@ -5069,7 +5069,8 @@ declare ptr @flan_dev_literal(ptr, i64)
declare i64 @flan_dyn_need_i64(i64) 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_char(i64, ptr, i64) declare i32 @flan_dyn_need_i32(i64, ptr, 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
; the ordinary cast. The two slices are the site's location and the target's ; the ordinary cast. The two slices are the site's location and the target's

View File

@ -36,7 +36,7 @@ let utf8 b =
(* A char's spelling, the one [Reader.read_byte] reads back as the same code (* A char's spelling, the one [Reader.read_byte] reads back as the same code
point: a name where the reader has one, Clojure's \uXXXX for every other point: a name where the reader has one, Clojure's \uXXXX for every other
control character and DEL, and the character itself otherwise. control character (C0, DEL and C1), and the character itself otherwise.
runtime/flan_dyn.c's [char_spell] writes the same table. *) runtime/flan_dyn.c's [char_spell] writes the same table. *)
let byte_repr b = let byte_repr b =
match b with match b with
@ -47,7 +47,7 @@ let byte_repr b =
| 0 -> "\\nul" | 0 -> "\\nul"
| 8 -> "\\backspace" | 8 -> "\\backspace"
| 12 -> "\\formfeed" | 12 -> "\\formfeed"
| b when b < 32 || b = 127 -> Printf.sprintf "\\u%04X" b | b when b < 32 || (b >= 127 && b <= 0x9F) -> Printf.sprintf "\\u%04X" b
| b when b < 127 -> Printf.sprintf "\\%c" (Char.chr b) | b when b < 127 -> Printf.sprintf "\\%c" (Char.chr b)
| b -> "\\" ^ utf8 b | b -> "\\" ^ utf8 b

View File

@ -741,7 +741,7 @@ static int is_scalar(int64_t cp) {
/* A char's spelling, as lib/reader.ml's [read_byte] reads it back and /* A char's spelling, as lib/reader.ml's [read_byte] reads it back and
* lib/form.ml's [byte_repr] writes it: a name where there is one, \uXXXX * lib/form.ml's [byte_repr] writes it: a name where there is one, \uXXXX
* for every other control character and DEL, the character itself after the * for every other control character (C0, DEL and C1), the character itself after the
* backslash otherwise. */ * backslash otherwise. */
static void char_spell(uint32_t cp, char buf[16]) { static void char_spell(uint32_t cp, char buf[16]) {
uint8_t u[4]; uint8_t u[4];
@ -756,7 +756,10 @@ static void char_spell(uint32_t cp, char buf[16]) {
case 12: strcpy(buf, "\\formfeed"); return; case 12: strcpy(buf, "\\formfeed"); return;
default: break; default: break;
} }
if (cp < 32 || cp == 127) { snprintf(buf, 16, "\\u%04X", (unsigned)cp); return; } if (cp < 32 || (cp >= 127 && cp <= 0x9F)) {
snprintf(buf, 16, "\\u%04X", (unsigned)cp);
return;
}
n = utf8_encode(cp, u); n = utf8_encode(cp, u);
buf[0] = '\\'; buf[0] = '\\';
for (i = 0; i < n; i++) buf[1 + i] = (char)u[i]; for (i = 0; i < n; i++) buf[1 + i] = (char)u[i];
@ -2686,17 +2689,40 @@ double flan_dyn_need_f64(flan_dyn v) {
return dyn_num_value(v); return dyn_num_value(v);
} }
/* A char into typed code is its code point, an i32 — the prelude's rune. Only static const char *an(const char *w); /* forward: "a" or "an" */
* a char: an int is not one, and a typed i32 is taken from an int by writing
* the conversion, (i32 x). */ /* A dyn into a typed i32: an int that fits, range-checked, or a char's code
int32_t flan_dyn_need_char(flan_dyn v, const uint8_t *loc, int64_t loclen) { * point — an i32 is the prelude's rune. The sentence names what was found
if (flan_dyn_tag(v) != FLAN_DYN_TAG_CHAR) * and what would do, and no call: the site in front of it is the one that
trap1(loc, loclen, TYPE_TRAP, "i32", * failed. */
flan_dyn_tag(v) == FLAN_DYN_TAG_INT int32_t flan_dyn_need_i32(flan_dyn v, const uint8_t *loc, int64_t loclen) {
? "an i32 is taken from a char, and from an int only by (i32 x)" int32_t t = flan_dyn_tag(v);
: "an i32 is taken from a char, its code point", char sv[SAY_MAX];
v); if (t == FLAN_DYN_TAG_CHAR) return (int32_t)dyn_payload(v);
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);
flan_trap((const uint8_t *)"DynRange", 8);
}
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)" : "");
flan_trap((const uint8_t *)"DynType", 7);
}
/* 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
* implicit crossing into an i32 gives. */
int64_t flan_dyn_int_of(flan_dyn v) {
if (flan_dyn_tag(v) == FLAN_DYN_TAG_CHAR) return (int64_t)dyn_payload(v);
return flan_dyn_need_i64(v);
} }
uint8_t flan_dyn_need_bool(flan_dyn v) { uint8_t flan_dyn_need_bool(flan_dyn v) {
@ -2788,6 +2814,19 @@ int32_t flan_dyn_cast_kind(flan_dyn v, const uint8_t *loc, int64_t loc_len,
const uint8_t *target, int64_t target_len, const uint8_t *target, int64_t target_len,
int32_t want_float) { int32_t want_float) {
int32_t tag = flan_dyn_tag(v); int32_t tag = flan_dyn_tag(v);
/* A char casts as its code point, an int: the arm after this reads it
through [flan_dyn_int_of], so (i32 c) is what passing c to an i32 is. */
if (tag == FLAN_DYN_TAG_CHAR) {
if (want_float && site_first_time(loc, loc_len)) {
fflush(stdout);
fprintf(stderr,
"%.*s: (%.*s x) found a dyn holding a char, and converted its "
"code point to %.*s — warned once for this site\n",
(int)loc_len, (const char *)loc, (int)target_len,
(const char *)target, (int)target_len, (const char *)target);
}
return 0;
}
if (tag != FLAN_DYN_TAG_INT && tag != FLAN_DYN_TAG_FLOAT) { if (tag != FLAN_DYN_TAG_INT && tag != FLAN_DYN_TAG_FLOAT) {
/* [trap1] takes the operation as a C string and the target is a Flan /* [trap1] takes the operation as a C string and the target is a Flan
* slice, so it is copied out. Every cast name is two or three bytes; the * slice, so it is copied out. Every cast name is two or three bytes; the

View File

@ -279,9 +279,11 @@ 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);
/* A char's code point, for a typed i32: the prelude's rune. Any other tag /* For a typed i32: an int in range, or a char's code point (the prelude's
* traps at [loc], an int included. */ * rune). Anything else, or an int out of range, traps at [loc]. */
int32_t flan_dyn_need_char(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. */
int64_t flan_dyn_int_of(flan_dyn v);
/* A numeric cast written on a dyn — [(f64 d)], [(u32 d)] — TODO.org, /* A numeric cast written on a dyn — [(f64 d)], [(u32 d)] — TODO.org,
* "A numeric cast opens a dyn box". Unlike the parameter boundary above this * "A numeric cast opens a dyn box". Unlike the parameter boundary above this

View File

@ -141,13 +141,17 @@ static void ops(void) {
check(flan_dyn_tag(flan_dyn_vec_new()) == FLAN_DYN_TAG_VEC, "tag vec"); check(flan_dyn_tag(flan_dyn_vec_new()) == FLAN_DYN_TAG_VEC, "tag vec");
check(flan_dyn_tag(flan_dyn_from_char(0x65E5)) == FLAN_DYN_TAG_CHAR, "tag char"); check(flan_dyn_tag(flan_dyn_from_char(0x65E5)) == FLAN_DYN_TAG_CHAR, "tag char");
check(strcmp(flan_dyn_tag_name(FLAN_DYN_TAG_CHAR), "char") == 0, "word char"); check(strcmp(flan_dyn_tag_name(FLAN_DYN_TAG_CHAR), "char") == 0, "word char");
check(flan_dyn_need_char(flan_dyn_from_char(0x1F600), NULL, 0) == 0x1F600, check(flan_dyn_need_i32(flan_dyn_from_char(0x1F600), NULL, 0) == 0x1F600,
"need-char answers the code point"); "need-i32 answers a char's code point");
check(flan_dyn_need_i32(flan_dyn_from_i64(-7), NULL, 0) == -7,
"need-i32 answers an int that fits");
check(flan_dyn_int_of(flan_dyn_from_char('a')) == 97,
"a cast's int arm reads a char's code point");
{ {
/* "é日😀" is 2 + 3 + 4 bytes and three chars, and chars/text round-trip. */ /* "é日😀" is 2 + 3 + 4 bytes and three chars, and chars/text round-trip. */
flan_dyn t = text("\xC3\xA9\xE6\x97\xA5\xF0\x9F\x98\x80"); flan_dyn t = text("\xC3\xA9\xE6\x97\xA5\xF0\x9F\x98\x80");
check(num(flan_dyn_len(t)) == 3, "len counts chars"); check(num(flan_dyn_len(t)) == 3, "len counts chars");
check(flan_dyn_need_char(FDYN_at(t, flan_dyn_from_i64(1)), NULL, 0) == 0x65E5, check(flan_dyn_need_i32(FDYN_at(t, flan_dyn_from_i64(1)), NULL, 0) == 0x65E5,
"at answers a char"); "at answers a char");
check(truth(flan_dyn_eq(flan_dyn_text(flan_dyn_chars(t, NULL, 0), NULL, 0), t)), check(truth(flan_dyn_eq(flan_dyn_text(flan_dyn_chars(t, NULL, 0), NULL, 0), t)),
"text undoes chars"); "text undoes chars");
@ -267,14 +271,14 @@ static void ops(void) {
check(!truth(flan_dyn_eq(a, c)), "= text sees the last byte"); check(!truth(flan_dyn_eq(a, c)), "= text sees the last byte");
check(truth(flan_dyn_eq(a, a)), "= text against itself"); check(truth(flan_dyn_eq(a, a)), "= text against itself");
check(num(flan_dyn_len(a)) == 5, "len text"); check(num(flan_dyn_len(a)) == 5, "len text");
check(flan_dyn_need_char(FDYN_at(a, flan_dyn_from_i64(0)), NULL, 0) == 'h', "at text"); check(flan_dyn_need_i32(FDYN_at(a, flan_dyn_from_i64(0)), NULL, 0) == 'h', "at text");
check(flan_dyn_need_char(FDYN_at(a, flan_dyn_from_i64(4)), NULL, 0) == 'o', "at text last"); check(flan_dyn_need_i32(FDYN_at(a, flan_dyn_from_i64(4)), NULL, 0) == 'o', "at text last");
{ {
/* Embedded NUL, because a length-prefixed text is the claim and strlen is /* Embedded NUL, because a length-prefixed text is the claim and strlen is
how that claim gets quietly broken. */ how that claim gets quietly broken. */
flan_dyn z = flan_dyn_from_bytes((const uint8_t *)"a\0b", 3); flan_dyn z = flan_dyn_from_bytes((const uint8_t *)"a\0b", 3);
check(num(flan_dyn_len(z)) == 3, "len counts past a NUL"); check(num(flan_dyn_len(z)) == 3, "len counts past a NUL");
check(flan_dyn_need_char(FDYN_at(z, flan_dyn_from_i64(2)), NULL, 0) == 'b', "at past a NUL"); check(flan_dyn_need_i32(FDYN_at(z, flan_dyn_from_i64(2)), NULL, 0) == 'b', "at past a NUL");
check(!truth(flan_dyn_eq(z, text("a"))), "= does not stop at a NUL"); check(!truth(flan_dyn_eq(z, text("a"))), "= does not stop at a NUL");
} }
{ {

View File

@ -1,4 +1,4 @@
;;;; Every ASCII code point, and a few past it, as dyn chars printed one per ;;;; Every ASCII code point, the C1 controls, and a few past them, as dyn chars printed one per
;;;; line. The test reads each line back with the reader and wants the same ;;;; line. The test reads each line back with the reader and wants the same
;;;; code point, so what a char prints as is what reads as it. ;;;; code point, so what a char prints as is what reads as it.
@ -7,6 +7,12 @@
(dotimes [i 128] (push v (u8 i))) (dotimes [i 128] (push v (u8 i)))
(let [t (the dyn (str (slice v)))] (let [t (the dyn (str (slice v)))]
(dotimes [i (length t)] (println (at t i)))) (dotimes [i (length t)] (println (at t i))))
;; the C1 controls, U+0080 to U+009F, and U+00A0, each C2 then one byte
(let [w (vec-new u8)]
(dotimes [i 33] (push w (u8 0xC2)) (push w (u8 (+ 0x80 i))))
(let [c1 (the dyn (str (slice w)))]
(dotimes [i (length c1)] (println (at c1 i))))
(free w))
(let [u (the dyn "é日😀")] (let [u (the dyn "é日😀")]
(dotimes [i (length u)] (println (at u i)))) (dotimes [i (length u)] (println (at u i))))
(free v)) (free v))

View File

@ -1,7 +1,7 @@
;;;; Dyn chars: a char literal that ends up dyn is a char and prints as its ;;;; Dyn chars: a char literal that ends up dyn is a char and prints as its
;;;; literal; a dyn text counts characters, not bytes; chars and text convert ;;;; literal; a dyn text counts characters, not bytes; chars and text convert
;;;; between a text and a vec of chars. With an argument, a dyn int handed to an ;;;; between a text and a vec of chars. With an argument, a dyn text handed to
;;;; i32 — a code point — traps at the call. ;;;; an i32 traps at the call, and with "big" an int past an i32's range does.
(defn show [x] () (println x)) (defn show [x] () (println x))
(defn code-point [c i32] i32 c) (defn code-point [c i32] i32 c)
@ -41,6 +41,11 @@
(show (get {\é 1 \日 2} (at t 1))) (show (get {\é 1 \日 2} (at t 1)))
;; into typed code: the code point ;; into typed code: the code point
(show (code-point (at t 2))) (show (code-point (at t 2)))
;; an int that fits goes in too, and a cast agrees with the crossing
(show (code-point (the dyn 5)))
(show (i32 (the dyn \a)))
(when (> (length args) 1) (when (> (length args) 1)
(show (code-point (the dyn 5))))) (if (= (at args 1) "big")
(show (code-point (the dyn 5000000000)))
(show (code-point (the dyn "x"))))))
0) 0)

View File

@ -5355,16 +5355,19 @@ level "1"
"\\I\n\\é\n\\日\n\\😀\n[\\a \\space \\( \\newline]\n:char\n\ "\\I\n\\é\n\\日\n\\😀\n[\\a \\space \\( \\newline]\n:char\n\
6\n\\é\n\\日\n\\😀\n\\o\n日😀\n\ 6\n\\é\n\\日\n\\😀\n\\o\n日😀\n\
[\\é \\日 \\😀 \\space \\o \\k]\n6\né日😀 ok\ntrue\n日\nok\n\ [\\é \\日 \\😀 \\space \\o \\k]\n6\né日😀 ok\ntrue\n日\nok\n\
true\nfalse\nfalse\nfalse\ntrue\nfalse\ntrue\n2\n128512\n" true\nfalse\nfalse\nfalse\ntrue\nfalse\ntrue\n2\n128512\n5\n97\n"
in in
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;
(* 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, then three past it — 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
what flan convert writes. *) what flan convert writes. *)
let spelled = List.init 128 Fun.id @ [ 0xE9; 0x65E5; 0x1F600 ] in let spelled =
List.init 128 Fun.id @ List.init 32 (fun i -> 0x80 + i)
@ [ 0xA0; 0xE9; 0x65E5; 0x1F600 ]
in
let read_char s = let read_char s =
match Reader.read_all ~file:"<char>" s with match Reader.read_all ~file:"<char>" s with
| [ { Form.v = Form.Byte b; _ } ] -> Some b | [ { Form.v = Form.Byte b; _ } ] -> Some b
@ -5396,16 +5399,22 @@ level "1"
List.iter List.iter
(fun x86 -> (fun x86 ->
let exe = compile ~x86 "programs/dyn-char.flan" in let exe = compile ~x86 "programs/dyn-char.flan" in
let code, text = run exe (Some "x") in List.iter
let want = "programs/dyn-char.flan:45:25: dyn i32: int, and an i32 \ (fun (arg, want) ->
is taken from a char" in let code, text = run exe (Some arg) in
if code <> 134 || not (contains text want) then begin if code <> 134 || not (contains text want) then begin
incr failures; incr failures;
Printf.printf Printf.printf
"FAIL dyn: a dyn int at an i32 traps%s\n got: %S (exit \ "FAIL dyn: a dyn %s at an i32 traps%s\n got: %S \
%d)\n wanted: %S (exit 134)\n" (exit %d)\n wanted: %S (exit 134)\n"
(if x86 then ", --x86" else "") text code want arg (if x86 then ", --x86" else "") text code want
end) end)
[ ("x", "programs/dyn-char.flan:50:27: dyn: an i32 is wanted \
here, and this is a text, \"x\". An i32 takes an int or \
a char's code point");
("big", "programs/dyn-char.flan:49:27: dyn: an i32 is wanted \
here, and the int 5000000000 is outside an i32's \
range") ])
[ false; true ]; [ false; true ];
(* (watch "name" v) with nothing arming the table: a struct, an array, a (* (watch "name" v) with nothing arming the table: a struct, an array, a
slice, a dyn map and a string all compile against flan_dev.c's watch slice, a dyn map and a string all compile against flan_dev.c's watch

View File

@ -3014,6 +3014,22 @@ let () =
accepts "the fix a class-named-kind refusal offers compiles" accepts "the fix a class-named-kind refusal offers compiles"
"(defclass map-value [a])\n\ "(defclass map-value [a])\n\
(defn main [] i32 (if (= (type-of (map-value 1)) :map-value) 0 1))"; (defn main [] i32 (if (= (type-of (map-value 1)) :map-value) 0 1))";
(* A char literal past ASCII is not a byte: it is refused by its name at a
u8, whichever operand of = it is, and pushing it into bytes too. *)
rejects_check "a non-ASCII char is not a u8"
"(defn main [] i32 (let [b (the u8 1)] (if (= b \\é) 1 0)))"
~needle:"\\é is 2 bytes in UTF-8, not one, so it is not a u8";
rejects_check "and not on the left of = either"
"(defn main [] i32 (let [b (the u8 1)] (if (= \\日 b) 1 0)))"
~needle:"\\日 is 3 bytes in UTF-8";
rejects_check "nor pushed into bytes"
"(defn main [] i32 (let [v (vec-new u8)] (push v \\é) 0))"
~needle:"Write the str \"é\" for its bytes";
rejects_check "a code point past a u16"
"(defn main [] i32 (let [b (the u16 1)] (if (= b \\😀) 1 0)))"
~needle:"\\😀 is code point 128512, which does not fit in a u16";
accepts "an ASCII char is a u8"
"(defn main [] i32 (let [b (the u8 97)] (if (= b \\a) 0 1)))";
rejects_check "type-of takes one argument" rejects_check "type-of takes one argument"
"(defn main [] i32 (let [k (type-of 1 2)] 0))" ~needle:"type-of"; "(defn main [] i32 (let [k (type-of 1 2)] 0))" ~needle:"type-of";
(* The constructor is an ordinary function, so its arity is the ordinary (* The constructor is an ordinary function, so its arity is the ordinary