From 92fbeb4fc907e0297b44d0a3cf637ee442619c82 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 13:59:32 +0700 Subject: [PATCH] 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. --- TODO.org | 7 ++++--- lib/check.ml | 14 +++++++++++--- lib/emit.ml | 1 + lib/inspect.ml | 2 +- lib/render.ml | 14 ++++++++++---- lib/session.ml | 4 ++++ runtime/flan_dyn.c | 16 ++++++++++++++-- runtime/flan_dyn.h | 2 ++ test/programs/dyn-char-spell.flan | 11 ++++++----- test/test_acceptance.ml | 26 ++++++++++++++++---------- 10 files changed, 69 insertions(+), 28 deletions(-) diff --git a/TODO.org b/TODO.org index 931f09a9..b6a99b6e 100644 --- a/TODO.org +++ b/TODO.org @@ -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 diff --git a/lib/check.ml b/lib/check.ml index 01c8dc8d..bc37a738 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -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 diff --git a/lib/emit.ml b/lib/emit.ml index 475b7e92..c0583628 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -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 diff --git a/lib/inspect.ml b/lib/inspect.ml index 878e7b77..4ffa65c8 100644 --- a/lib/inspect.ml +++ b/lib/inspect.ml @@ -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) } diff --git a/lib/render.ml b/lib/render.ml index e40d65bc..330ea77c 100644 --- a/lib/render.ml +++ b/lib/render.ml @@ -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. *) diff --git a/lib/session.ml b/lib/session.ml index fb8f54df..30f426b2 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -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 }) } diff --git a/runtime/flan_dyn.c b/runtime/flan_dyn.c index 981cdd08..f2309dda 100644 --- a/runtime/flan_dyn.c +++ b/runtime/flan_dyn.c @@ -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, "="); diff --git a/runtime/flan_dyn.h b/runtime/flan_dyn.h index 2b73f3ec..f48cdcc1 100644 --- a/runtime/flan_dyn.h +++ b/runtime/flan_dyn.h @@ -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); diff --git a/test/programs/dyn-char-spell.flan b/test/programs/dyn-char-spell.flan index c61cac62..d20d3c1f 100644 --- a/test/programs/dyn-char-spell.flan +++ b/test/programs/dyn-char-spell.flan @@ -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) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 9f073d98..d39974b0 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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 \