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
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.
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.
** 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 any integer width it gives its
Only a char literal, =at= on a text and =chars= make one. =println= prints it as the
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
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

View File

@ -5185,7 +5185,8 @@ let condition_desc ctx loc name =
ei64 = (fun x -> emit (to_bytes hctx loc Tast.I64ToBytes x));
eu64 = (fun x -> emit (to_bytes hctx loc Tast.U64ToBytes 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
let value =
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));
eu64 = (fun x -> write (conv Tast.U64ToBytes 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
let rc = render_ctx ctx emitter in
let c_top = emitter.Render.edyn in
let render_one a =
match a.Tast.ty with
| Types.String | Types.Slice (_, (Types.Int Types.U8)) ->
[ 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. *)
| t when is_string_ty t ->
[ 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 ]);
eu64 = (fun x -> unit_rt "flan_dev_watch_emit_u64" [ 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
(* 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

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_print(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_watch(i64)
; 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;
enums = c.enums;
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 });
bytechar = emit; epitaph = emit };
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
other four write to. *)
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
@ -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.Bool ->
[ unit_ (Tast.If (e, lit "true", lit "false")) ]
(* A char prints as a dyn char does, as its literal: the runtime's one
spelling, so the typed and dyn sides cannot drift apart. *)
(* A char inside a value, or in the inspector, prints as its literal, by
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 ->
[ c.emit.edyn
[ c.emit.enested
{ Tast.e = Tast.Prim (Tast.Rt "flan_dyn_from_char", [ cast (Types.Int Types.I32) e ]);
ty = Types.Dyn; loc } ]
(* 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
the walk hands the whole value to [c.emit.edyn], which names the runtime
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
arm above, and a [Var] never reaches a backend. So this names the fix
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
the reply's value like any other. *)
edyn =
(fun x ->
{ Tast.e = Tast.Prim (Tast.Rt "flan_dyn_emit_dev", [ x ]);
ty = Types.Unit; loc = x.Tast.loc });
enested =
(fun x ->
{ Tast.e = Tast.Prim (Tast.Rt "flan_dyn_emit_dev", [ x ]);
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);
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:
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);
return;
/* 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); }
/* 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,
int64_t 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
* 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_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,
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
;;;; 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.
;;;; line, each inside a vector, where a char prints as its literal. The test
;;;; 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
(let [v (vec-new u8)]
(dotimes [i 128] (push v (u8 i)))
(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))))
(dotimes [i (length c1)] (println [(at c1 i)])))
(free w))
(let [u (the dyn "é日😀")]
(dotimes [i (length u)] (println (at u i))))
(dotimes [i (length u)] (println [(at u i)])))
(free v))
0)

View File

@ -5518,8 +5518,8 @@ level "1"
code point in a typed i32. With an argument, a dyn int at the i32
traps at the call. *)
let dyn_char_out =
"\\I\n\\é\n\\日\n\\😀\n[\\a \\space \\( \\newline]\n:char\n\
6\n\\é\n\\日\n\\😀\n\\o\n日😀\n\
"I\né\n日\n😀\n[\\a \\space \\( \\newline]\n:char\n\
6\né\n日\n😀\no\n日😀\n\
[\\é \\日 \\😀 \\space \\o \\k]\n6\né日😀 ok\ntrue\n日\nok\n\
true\nfalse\nfalse\nfalse\ntrue\nfalse\ntrue\n2\n128512\n5\n97\n"
in
@ -5531,9 +5531,9 @@ level "1"
both ways. Then a dyn int at a char parameter and (char n) on a
surrogate, each trapping at its own form. *)
let char_out =
"\\a\n\\a\n\\é [\\é \\z \\space]\n233 233\n\
true true true true \\c \\q\n\\A \\😀 65 65 \\z\n\\B\n\
(some 1) (some 2) false\n2\ntrue true\n122\né日\n\\q\n[\\x \\y]\n\\y\n"
"a\na\né [\\é \\z \\space]\n233 233\n\
true true true true c q\nA 😀 65 65 z\nB\n\
(some 1) (some 2) false\n2\ntrue true\n122\né日\nq\n[\\x \\y]\ny\n"
in
outputs "char: a typed char" "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
like any other: characters counted, ASCII or not. *)
let string_char_out =
"ab 2 \\b\né日😀! 4 \\日 \\!\n日😀 [\\é \\日 \\😀 \\!]\n\
é日😀! 4 \\😀 true\n10 4\n"
"ab 2 b\né日😀! 4 日 !\n日😀 [\\é \\日 \\😀 \\!]\n\
é日😀! 4 😀 true\n10 4\n"
in
outputs "dyn: a String crossing counts chars"
"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
pin's stamp and the text's measure live in different header fields),
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"
pinned_out;
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\". 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 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,
and so does the compiler's own spelling of the same literal, which is
what flan convert writes. *)
@ -5676,7 +5677,12 @@ level "1"
let code, text = run exe None in
let lines = String.split_on_char '\n' text 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
incr failures;
Printf.printf "FAIL dyn chars read back as printed%s\n \