println prints a char as the character itself, and a char inside a value, in the inspector or at the REPL prints as its literal.

This commit is contained in:
Joseph Ferano 2026-09-26 13:59:32 +07:00
parent fd8620516c
commit 92fbeb4fc9
10 changed files with 69 additions and 28 deletions

View File

@ -33,13 +33,14 @@ Decision 127: a char literal is the number typed code wants there, and a =char=
an untyped array of char literals included; a =char= crosses into dyn as a dyn char, and an untyped array of char literals included; a =char= crosses into dyn as a dyn char, and
only a dyn char unboxes into one. No arithmetic: =(i32 c)= and =(char n)= convert, the only a dyn char unboxes into one. No arithmetic: =(i32 c)= and =(char n)= convert, the
latter checked. An untyped defconst of one is that literal where a number is wanted. latter checked. An untyped defconst of one is that literal where a number is wanted.
Printed as its literal, as dyn prints one. =runes-next= and =rune-at= give a char; the Printed as dyn prints one (129a). =runes-next= and =rune-at= give a char; the
UTF-8 codec (=decode-rune=, =encode-rune=) stays on i32. Rules out the f(\a) fork. UTF-8 codec (=decode-rune=, =encode-rune=) stays on i32. Rules out the f(\a) fork.
** 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. =println= prints it as the
literal, bare too, a control character as \\uXXXX. Into any integer width it gives its character itself (129a); inside a value, in the inspector and the REPL it is its
literal, 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 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 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 on dyn text count code points, a malformed byte counting as one U+FFFD. A non-ASCII

View File

@ -5185,7 +5185,8 @@ let condition_desc ctx loc name =
ei64 = (fun x -> emit (to_bytes hctx loc Tast.I64ToBytes x)); ei64 = (fun x -> emit (to_bytes hctx loc Tast.I64ToBytes x));
eu64 = (fun x -> emit (to_bytes hctx loc Tast.U64ToBytes x)); eu64 = (fun x -> emit (to_bytes hctx loc Tast.U64ToBytes x));
ef64 = (fun x -> emit (to_bytes hctx loc Tast.F64ToBytes x)); ef64 = (fun x -> emit (to_bytes hctx loc Tast.F64ToBytes x));
edyn = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_emit_msg", [ x ]))) } edyn = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_emit_msg", [ x ])));
enested = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_emit_msg", [ x ]))) }
in in
let value = let value =
mk loc ty (Tast.Deref (mk loc (Types.Ptr (Types.Mut, ty)) (Tast.Local pslot))) mk loc ty (Tast.Deref (mk loc (Types.Ptr (Types.Mut, ty)) (Tast.Local pslot)))
@ -14486,13 +14487,19 @@ and named_call ?(qualified = false) ctx ~want loc name args =
ei64 = (fun x -> write (conv Tast.I64ToBytes x)); ei64 = (fun x -> write (conv Tast.I64ToBytes x));
eu64 = (fun x -> write (conv Tast.U64ToBytes x)); eu64 = (fun x -> write (conv Tast.U64ToBytes x));
ef64 = (fun x -> write (conv Tast.F64ToBytes x)); ef64 = (fun x -> write (conv Tast.F64ToBytes x));
edyn = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_print_at", [ x; here loc ]))) } edyn = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_print_at", [ x; here loc ])));
enested = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_print_nested_at", [ x; here loc ]))) }
in in
let rc = render_ctx ctx emitter in let rc = render_ctx ctx emitter in
let c_top = emitter.Render.edyn in
let render_one a = let render_one a =
match a.Tast.ty with match a.Tast.ty with
| Types.String | Types.Slice (_, (Types.Int Types.U8)) -> | Types.String | Types.Slice (_, (Types.Int Types.U8)) ->
[ write (mk loc bslice (Tast.Prim (Tast.Bytes, [ a ]))) ] [ write (mk loc bslice (Tast.Prim (Tast.Bytes, [ a ]))) ]
(* A char at the top prints as the character itself (129a), through the
runtime's dyn printer so the two sides agree. *)
| Types.Char ->
[ c_top (box loc a) ]
(* A String prints as its text, raw at the top as a str does. *) (* A String prints as its text, raw at the top as a str does. *)
| t when is_string_ty t -> | t when is_string_ty t ->
[ write (mk loc bslice (Tast.Prim (Tast.Bytes, [ string_bytes ctx loc a ]))) ] [ write (mk loc bslice (Tast.Prim (Tast.Bytes, [ string_bytes ctx loc a ]))) ]
@ -14562,7 +14569,8 @@ and named_call ?(qualified = false) ctx ~want loc name args =
ei64 = (fun x -> unit_rt "flan_dev_watch_emit_i64" [ x ]); ei64 = (fun x -> unit_rt "flan_dev_watch_emit_i64" [ x ]);
eu64 = (fun x -> unit_rt "flan_dev_watch_emit_u64" [ x ]); eu64 = (fun x -> unit_rt "flan_dev_watch_emit_u64" [ x ]);
ef64 = (fun x -> unit_rt "flan_dev_watch_emit_f64" [ x ]); ef64 = (fun x -> unit_rt "flan_dev_watch_emit_f64" [ x ]);
edyn = (fun x -> unit_rt "flan_dyn_emit_watch" [ x ]) } edyn = (fun x -> unit_rt "flan_dyn_emit_watch" [ x ]);
enested = (fun x -> unit_rt "flan_dyn_emit_watch" [ x ]) }
in in
(* A place is read where it stands; anything else is bound to a slot of (* A place is read where it stands; anything else is bound to a slot of
this frame first, so the walk — which names its argument once per this frame first, so the walk — which names its argument once per

View File

@ -5122,6 +5122,7 @@ declare void @flan_dyn_set_at(i64, i64, i64, ptr, i64)
declare void @flan_dyn_push(i64, i64, ptr, i64) declare void @flan_dyn_push(i64, i64, ptr, i64)
declare void @flan_dyn_print(i64) declare void @flan_dyn_print(i64)
declare void @flan_dyn_print_at(i64, ptr, i64) declare void @flan_dyn_print_at(i64, ptr, i64)
declare void @flan_dyn_print_nested_at(i64, ptr, i64)
declare void @flan_dyn_emit_dev(i64) declare void @flan_dyn_emit_dev(i64)
declare void @flan_dyn_emit_watch(i64) declare void @flan_dyn_emit_watch(i64)
; The watch table, which (watch "name" v) renders into. flan_dev.c is linked ; The watch table, which (watch "name" v) renders into. flan_dev.c is linked

View File

@ -166,7 +166,7 @@ let refusal c (ty : Types.t) : string option =
{ Render.structs = c.structs; datas = c.datas; unions = c.unions; { Render.structs = c.structs; datas = c.datas; unions = c.unions;
enums = c.enums; enums = c.enums;
emit = { Render.ebytes = emit; estr = emit; ei64 = emit; eu64 = emit; emit = { Render.ebytes = emit; estr = emit; ei64 = emit; eu64 = emit;
ef64 = emit; edyn = emit }; ef64 = emit; edyn = emit; enested = emit };
ptrs = Some { Render.live = (fun _ -> { unit_ with ty = Types.Bool }); ptrs = Some { Render.live = (fun _ -> { unit_ with ty = Types.Bool });
bytechar = emit; epitaph = emit }; bytechar = emit; epitaph = emit };
alloc = (fun _ -> 0) } alloc = (fun _ -> 0) }

View File

@ -35,6 +35,10 @@ type emitter = {
runtime's to read — so the runtime renders it, into the same place the runtime's to read — so the runtime renders it, into the same place the
other four write to. *) other four write to. *)
edyn : Tast.expr -> Tast.expr; edyn : Tast.expr -> Tast.expr;
(* The same, for a dyn inside a larger value: a text quoted and a char as
its literal. The same as [edyn] on the inspecting side, which quotes at
the top too; [println]'s differs, since its top level is raw. *)
enested : Tast.expr -> Tast.expr;
} }
(* What a walk is allowed to do with a pointer, and it is exactly two (* What a walk is allowed to do with a pointer, and it is exactly two
@ -155,10 +159,12 @@ let rec render ?(refuse = print_refusal) c depth (e : Tast.expr) : Tast.expr lis
| Types.Float _ -> [ c.emit.ef64 (cast (Types.Float Types.F64) e) ] | Types.Float _ -> [ c.emit.ef64 (cast (Types.Float Types.F64) e) ]
| Types.Bool -> | Types.Bool ->
[ unit_ (Tast.If (e, lit "true", lit "false")) ] [ unit_ (Tast.If (e, lit "true", lit "false")) ]
(* A char prints as a dyn char does, as its literal: the runtime's one (* A char inside a value, or in the inspector, prints as its literal, by
spelling, so the typed and dyn sides cannot drift apart. *) the runtime's one spelling so the typed and dyn sides cannot drift
apart. [println]'s top level is the character itself, and check.ml's
print arm takes that case before the walk. *)
| Types.Char -> | Types.Char ->
[ c.emit.edyn [ c.emit.enested
{ Tast.e = Tast.Prim (Tast.Rt "flan_dyn_from_char", [ cast (Types.Int Types.I32) e ]); { Tast.e = Tast.Prim (Tast.Rt "flan_dyn_from_char", [ cast (Types.Int Types.I32) e ]);
ty = Types.Dyn; loc } ] ty = Types.Dyn; loc } ]
(* Evaluated *and then* reported. A Unit expression is almost always a call (* Evaluated *and then* reported. A Unit expression is almost always a call
@ -443,7 +449,7 @@ let rec render ?(refuse = print_refusal) c depth (e : Tast.expr) : Tast.expr lis
does not — so the printing belongs on the side that can see the tag, and does not — so the printing belongs on the side that can see the tag, and
the walk hands the whole value to [c.emit.edyn], which names the runtime the walk hands the whole value to [c.emit.edyn], which names the runtime
entry point that renders into this emitter's sink. *) entry point that renders into this emitter's sink. *)
| Types.Dyn -> [ c.emit.edyn e ] | Types.Dyn -> [ (if depth = 0 then c.emit.edyn else c.emit.enested) e ]
(* Reachable: [(println m)] on a Map. Everything else in [Types.t] has an (* Reachable: [(println m)] on a Map. Everything else in [Types.t] has an
arm above, and a [Var] never reaches a backend. So this names the fix arm above, and a [Var] never reaches a backend. So this names the fix
rather than only the refusal. *) rather than only the refusal. *)

View File

@ -1556,6 +1556,10 @@ let dev_emitter : Render.emitter =
(* Into the value buffer, not stdout: a dyn expression's value belongs in (* Into the value buffer, not stdout: a dyn expression's value belongs in
the reply's value like any other. *) the reply's value like any other. *)
edyn = edyn =
(fun x ->
{ Tast.e = Tast.Prim (Tast.Rt "flan_dyn_emit_dev", [ x ]);
ty = Types.Unit; loc = x.Tast.loc });
enested =
(fun x -> (fun x ->
{ Tast.e = Tast.Prim (Tast.Rt "flan_dyn_emit_dev", [ x ]); { Tast.e = Tast.Prim (Tast.Rt "flan_dyn_emit_dev", [ x ]);
ty = Types.Unit; loc = x.Tast.loc }) } ty = Types.Unit; loc = x.Tast.loc }) }

View File

@ -855,9 +855,12 @@ static void render(dyn_sink w, flan_dyn v, int depth, int nested) {
else emit_n(w, obj_text_bytes(o), o->len); else emit_n(w, obj_text_bytes(o), o->len);
return; return;
} }
/* A char prints as the literal that reads back as it, at every depth. */ /* A char at the top of a println is the character itself, and anywhere a
* reader must tell it apart — inside a value, in the inspector — the
* literal that reads back as it: Clojure's println against its pr. */
case FLAN_DYN_TAG_CHAR: case FLAN_DYN_TAG_CHAR:
char_spell((uint32_t)dyn_payload(v), buf); if (nested) char_spell((uint32_t)dyn_payload(v), buf);
else buf[utf8_encode((uint32_t)dyn_payload(v), (uint8_t *)buf)] = '\0';
emit(w, buf); emit(w, buf);
return; return;
/* A keyword prints with its colon, bare, at every depth: :a is its own /* A keyword prints with its colon, bare, at every depth: :a is its own
@ -4384,6 +4387,15 @@ void flan_dyn_print_at(flan_dyn v, const uint8_t *loc, int64_t loclen) {
void flan_dyn_print(flan_dyn v) { flan_dyn_print_at(v, NULL, 0); } void flan_dyn_print(flan_dyn v) { flan_dyn_print_at(v, NULL, 0); }
/* The same onto stdout, as a value inside a larger one prints: a text
* quoted and a char as its literal. println's walk calls it for a dyn or a
* char inside a typed struct, array or slice. */
void flan_dyn_print_nested_at(flan_dyn v, const uint8_t *loc, int64_t loclen) {
walk_site was = walk_enter(loc, loclen, "print");
render(flan_write_stdout, v, 0, 1);
walk_leave(was);
}
flan_dyn flan_dyn_eq_at(flan_dyn a, flan_dyn b, const uint8_t *loc, flan_dyn flan_dyn_eq_at(flan_dyn a, flan_dyn b, const uint8_t *loc,
int64_t loclen) { int64_t loclen) {
walk_site was = walk_enter(loc, loclen, "="); walk_site was = walk_enter(loc, loclen, "=");

View File

@ -399,6 +399,8 @@ void flan_dyn_need_as(flan_dyn v, const uint8_t *want, int64_t wantlen,
/* print, =, length and has-key? with the site they were written at: a view /* print, =, length and has-key? with the site they were written at: a view
* that traps inside one names it. */ * that traps inside one names it. */
void flan_dyn_print_at(flan_dyn v, const uint8_t *loc, int64_t loclen); void flan_dyn_print_at(flan_dyn v, const uint8_t *loc, int64_t loclen);
void flan_dyn_print_nested_at(flan_dyn v, const uint8_t *loc,
int64_t loclen);
flan_dyn flan_dyn_eq_at(flan_dyn a, flan_dyn b, const uint8_t *loc, flan_dyn flan_dyn_eq_at(flan_dyn a, flan_dyn b, const uint8_t *loc,
int64_t loclen); int64_t loclen);
flan_dyn flan_dyn_len_at(flan_dyn v, const uint8_t *loc, int64_t loclen); flan_dyn flan_dyn_len_at(flan_dyn v, const uint8_t *loc, int64_t loclen);

View File

@ -1,19 +1,20 @@
;;;; Every ASCII code point, the C1 controls, and a few past them, 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, each inside a vector, where a char prints as its literal. The test
;;;; code point, so what a char prints as is what reads as it. ;;;; reads each back with the reader and wants the same code point, so what a
;;;; char prints as there is what reads as it.
(defn main [] i32 (defn main [] i32
(let [v (vec-new u8)] (let [v (vec-new u8)]
(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 ;; the C1 controls, U+0080 to U+009F, and U+00A0, each C2 then one byte
(let [w (vec-new u8)] (let [w (vec-new u8)]
(dotimes [i 33] (push w (u8 0xC2)) (push w (u8 (+ 0x80 i)))) (dotimes [i 33] (push w (u8 0xC2)) (push w (u8 (+ 0x80 i))))
(let [c1 (the dyn (str (slice w)))] (let [c1 (the dyn (str (slice w)))]
(dotimes [i (length c1)] (println (at c1 i)))) (dotimes [i (length c1)] (println [(at c1 i)])))
(free w)) (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))
0) 0)

View File

@ -5518,8 +5518,8 @@ level "1"
code point in a typed i32. With an argument, a dyn int at the i32 code point in a typed i32. With an argument, a dyn int at the i32
traps at the call. *) traps at the call. *)
let dyn_char_out = let dyn_char_out =
"\\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😀\no\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\n5\n97\n" true\nfalse\nfalse\nfalse\ntrue\nfalse\ntrue\n2\n128512\n5\n97\n"
in in
@ -5531,9 +5531,9 @@ level "1"
both ways. Then a dyn int at a char parameter and (char n) on a both ways. Then a dyn int at a char parameter and (char n) on a
surrogate, each trapping at its own form. *) surrogate, each trapping at its own form. *)
let char_out = let char_out =
"\\a\n\\a\n\\é [\\é \\z \\space]\n233 233\n\ "a\na\né [\\é \\z \\space]\n233 233\n\
true true true true \\c \\q\n\\A \\😀 65 65 \\z\n\\B\n\ true true true true c q\nA 😀 65 65 z\nB\n\
(some 1) (some 2) false\n2\ntrue true\n122\né日\n\\q\n[\\x \\y]\n\\y\n" (some 1) (some 2) false\n2\ntrue true\n122\né日\nq\n[\\x \\y]\ny\n"
in in
outputs "char: a typed char" "programs/char.flan" char_out; outputs "char: a typed char" "programs/char.flan" char_out;
outputs ~opt:"-O0" "char: a typed char, -O0" "programs/char.flan" char_out; outputs ~opt:"-O0" "char: a typed char, -O0" "programs/char.flan" char_out;
@ -5560,8 +5560,8 @@ level "1"
(* A String, and a str made from one, cross into dyn as text measured (* A String, and a str made from one, cross into dyn as text measured
like any other: characters counted, ASCII or not. *) like any other: characters counted, ASCII or not. *)
let string_char_out = let string_char_out =
"ab 2 \\b\né日😀! 4 \\日 \\!\n日😀 [\\é \\日 \\😀 \\!]\n\ "ab 2 b\né日😀! 4 日 !\n日😀 [\\é \\日 \\😀 \\!]\n\
é日😀! 4 \\😀 true\n10 4\n" é日😀! 4 😀 true\n10 4\n"
in in
outputs "dyn: a String crossing counts chars" outputs "dyn: a String crossing counts chars"
"programs/dyn-char-string.flan" string_char_out; "programs/dyn-char-string.flan" string_char_out;
@ -5572,7 +5572,7 @@ level "1"
(* A text pinned by crossing into a str keeps counting characters (the (* A text pinned by crossing into a str keeps counting characters (the
pin's stamp and the text's measure live in different header fields), pin's stamp and the text's measure live in different header fields),
and a char writes through a view of typed storage. *) and a char writes through a view of typed storage. *)
let pinned_out = "9\n9\n9\n3\n\\日\né日\n122 26085 3\n97 2\n" in let pinned_out = "9\n9\n9\n3\n日\né日\n122 26085 3\n97 2\n" in
outputs "dyn: a pinned text counts chars" "programs/dyn-char-pinned.flan" outputs "dyn: a pinned text counts chars" "programs/dyn-char-pinned.flan"
pinned_out; pinned_out;
outputs ~opt:"-O0" "dyn: a pinned text counts chars, -O0" outputs ~opt:"-O0" "dyn: a pinned text counts chars, -O0"
@ -5648,7 +5648,8 @@ level "1"
("x", "52:34: dyn: an i64 is wanted here, and this is a text, \ ("x", "52:34: dyn: an i64 is wanted here, and this is a text, \
\"x\". An i64 takes an int or a char's code point") ]) \"x\". An i64 takes an int or a char's code point") ])
[ false; true ]; [ false; true ];
(* Print, then read: each char dyn-char-spell.flan prints — every ASCII (* Print, then read: each char dyn-char-spell.flan prints inside a vector,
where a char shows its literal — 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
what flan convert writes. *) what flan convert writes. *)
@ -5676,7 +5677,12 @@ level "1"
let code, text = run exe None in let code, text = run exe None in
let lines = String.split_on_char '\n' text in let lines = String.split_on_char '\n' text in
let lines = List.filteri (fun i _ -> i < List.length spelled) lines in let lines = List.filteri (fun i _ -> i < List.length spelled) lines in
let got = List.map read_char lines in let unwrap l =
let n = String.length l in
if n >= 2 && l.[0] = '[' && l.[n - 1] = ']' then String.sub l 1 (n - 2)
else l
in
let got = List.map (fun l -> read_char (unwrap l)) lines in
if code <> 0 || got <> List.map Option.some spelled then begin if code <> 0 || got <> List.map Option.some spelled then begin
incr failures; incr failures;
Printf.printf "FAIL dyn chars read back as printed%s\n \ Printf.printf "FAIL dyn chars read back as printed%s\n \