print and println go variadic, with Clojure's spacing
Arguments print in order with a single space between each pair, println ending the line; (println) is the newline alone and (print) is nothing. The checker's arm renders each argument exactly as it did alone, so typed and dyn values mix in one call, one-argument sites are byte-identical, and an unprintable argument is still refused at its own span.
This commit is contained in:
parent
73ab213134
commit
2b91fd2146
23
FIX.org
23
FIX.org
@ -423,3 +423,26 @@ rename. typed-flan branch freezes the static language pre-dyn.
|
||||
|
||||
All of it dispatches after the x86-dyn lane lands. The struct dyn-field
|
||||
refusal (01e60fa) is the stopgap 2 lifts.
|
||||
|
||||
* println is variadic, 2026-09-20
|
||||
Author, dogfooding: "println should be variadic" — hit "println takes 1
|
||||
argument, given 2".
|
||||
|
||||
Semantics chosen: Clojure's. Every argument prints in order, a single space
|
||||
between each pair, println ends the line. (println) is the newline alone,
|
||||
(print) is nothing. Single-argument call sites are byte-identical to before —
|
||||
the space is a separator, never a trailer.
|
||||
|
||||
Mechanics: no prelude macro. print/println were never functions — they are
|
||||
the checker's structural walk (lib/check.ml, the "print" | "println" arm;
|
||||
the walk in lib/render.ml) — so the arm itself went variadic: each argument
|
||||
is checked and rendered exactly as it was alone, with a one-byte " " write
|
||||
interleaved. Typed and dyn arguments mix in one call because each gets its
|
||||
own printer and both sinks share stdio's buffer (flan_write_stdout and
|
||||
flan_dyn_print both go through stdout). One generic argument still defers
|
||||
the whole call to instantiation. Diagnostics stay on the argument: each is
|
||||
checked carrying its own loc and render.ml fails on the expression's loc,
|
||||
so an unprintable second argument underlines that argument, not the form —
|
||||
pinned in test_flan.ml. Output pinned by test/programs/println-variadic.flan
|
||||
and its acceptance row (LLVM), spacing exact, "|" markers so a leaked
|
||||
trailing space is a visible red.
|
||||
|
||||
48
lib/check.ml
48
lib/check.ml
@ -5175,12 +5175,18 @@ and named_call ctx ~want loc name args =
|
||||
cannot be told from the punctuation. The split is exactly top level vs
|
||||
nested, which is why it lives here and not in the walk. *)
|
||||
| "print" | "println" ->
|
||||
arity loc name 1 args;
|
||||
(* Variadic, with Clojure's spacing: every argument prints in order with a
|
||||
single space between each pair, and [println] ends the line. No arity
|
||||
check — (println) is the newline alone and (print) is nothing, which is
|
||||
also Clojure's answer. Each argument gets the same walk it would get
|
||||
alone, so one call mixes typed and dyn values freely, and a bad argument
|
||||
is refused at its own location: the arguments are checked one by one
|
||||
below, each carrying its own loc, and render.ml fails on the
|
||||
expression's loc rather than the form's. *)
|
||||
(* Printing is a read, not a move: the walk goes over the value and keeps
|
||||
nothing. Without this, (println v) would consume a Vec and every
|
||||
printing of one would be its last. *)
|
||||
let target = List.hd args in
|
||||
let a = check ctx target in
|
||||
let checked = List.map (fun target -> check ctx target) args in
|
||||
(* ── The allow-list, and what it takes to get on it ───────────────
|
||||
plan.org names [println] as the one compiler-provided exception — it
|
||||
"selects a structural printer at each concrete instantiation" — and
|
||||
@ -5223,8 +5229,10 @@ and named_call ctx ~want loc name args =
|
||||
|
||||
The node produced here is a unit no-op, thrown away with the rest of
|
||||
the abstract pass. The real printer is selected when the copy is
|
||||
checked with [t] concrete. *)
|
||||
if generic_ty a.Tast.ty then
|
||||
checked with [t] concrete. One generic argument defers the whole call:
|
||||
the printers for its neighbours would be re-selected at instantiation
|
||||
anyway, so building them here would be work thrown away twice. *)
|
||||
if List.exists (fun a -> generic_ty a.Tast.ty) checked then
|
||||
mk loc Types.Unit Tast.Unit
|
||||
else
|
||||
let bslice = Types.Slice (Types.Int Types.U8) in
|
||||
@ -5264,12 +5272,26 @@ and named_call ctx ~want loc name args =
|
||||
ptrs = None;
|
||||
alloc = (fun ty -> fresh_slot ctx ty) }
|
||||
in
|
||||
let parts =
|
||||
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 ]))) ]
|
||||
| _ -> Render.render rc 0 a
|
||||
in
|
||||
(* Built fresh per use rather than shared: nothing else in this file puts
|
||||
one node in two places of a tree, and a pass that hangs state off a
|
||||
node would be entitled to assume it appears once. *)
|
||||
let space () =
|
||||
write (mk loc bslice
|
||||
(Tast.Prim (Tast.Bytes, [ mk loc Types.String (Tast.Str " ") ])))
|
||||
in
|
||||
let parts =
|
||||
match checked with
|
||||
| [] -> []
|
||||
| first :: rest ->
|
||||
render_one first
|
||||
@ List.concat_map (fun a -> space () :: render_one a) rest
|
||||
in
|
||||
let nl =
|
||||
if String.equal name "println" then
|
||||
[ write
|
||||
@ -5913,12 +5935,14 @@ let builtins : (string * string * string) list =
|
||||
("write-stdout", "write-stdout [[u8]] ()",
|
||||
"Writes the bytes to standard output exactly as given: no newline and \
|
||||
no formatting.");
|
||||
("print", "print [T] ()",
|
||||
"The structural printer, selected for the argument's concrete type. A \
|
||||
string prints raw at the top level and quoted inside a structure, and \
|
||||
a Ptr or a Handle prints its address rather than being followed. \
|
||||
Printing is a read, so it does not consume the value.");
|
||||
("println", "println [T] ()", "print, with a newline after it.");
|
||||
("print", "print [T ...] ()",
|
||||
"The structural printer, selected for each argument's concrete type; \
|
||||
arguments print in order with a single space between them. A string \
|
||||
prints raw at the top level and quoted inside a structure, and a Ptr \
|
||||
or a Handle prints its address rather than being followed. Printing \
|
||||
is a read, so it does not consume the value.");
|
||||
("println", "println [T ...] ()",
|
||||
"print, with a newline after it — (println) alone is the newline.");
|
||||
("exit", "exit [i32] never",
|
||||
"Ends the process with this status. It has no value, so nothing written \
|
||||
after it runs.");
|
||||
|
||||
@ -19,8 +19,10 @@
|
||||
|
||||
No printing function is here at all any more. [print] and [println] are
|
||||
the whole printing surface, and neither is a function: both are
|
||||
compiler-provided and structural, a walk over the concrete type at the
|
||||
call site (check.ml, and the walk itself in render.ml). That is plan.org's
|
||||
compiler-provided and structural, a walk over each argument's concrete
|
||||
type at the call site (check.ml, and the walk itself in render.ml). Both
|
||||
are variadic with Clojure's spacing -- arguments in order, one space
|
||||
between each pair, [println] ending the line. That is plan.org's
|
||||
Milestone 5 item, and it needed none of the rest of milestone 5 -- there
|
||||
is nothing to dispatch on at run time and no user-supplied printer to
|
||||
choose between, so no type variables are involved. The earlier note here
|
||||
|
||||
33
test/programs/println-variadic.flan
Normal file
33
test/programs/println-variadic.flan
Normal file
@ -0,0 +1,33 @@
|
||||
;;;; print and println are variadic with Clojure's spacing: every argument
|
||||
;;;; prints in order, a single space between each pair, println ending the
|
||||
;;;; line. (println) alone is the newline; (print) alone is nothing.
|
||||
;;;;
|
||||
;;;; What the pins are for. The one-argument lines must be byte-identical to
|
||||
;;;; what they printed before the forms went variadic — no trailing space is
|
||||
;;;; the thing an eye cannot see in a diff, so every print line ends in a "|"
|
||||
;;;; marker that would move if a space leaked. And the arguments are typed and
|
||||
;;;; dyn in the same call: each gets the walk it would get alone, the typed
|
||||
;;;; ones selected at check time and the dyn ones handed to the runtime, so
|
||||
;;;; the interleaving on one line is what proves the two sinks share a buffer.
|
||||
|
||||
(defvar boxed dyn 21)
|
||||
(defvar dlabel dyn "mid")
|
||||
|
||||
(defn main [] ()
|
||||
;; println at every arity
|
||||
(println) ; bare newline
|
||||
(println "one") ; the old single-argument form, unchanged
|
||||
(println "a" "b")
|
||||
(println "x:" 5 true)
|
||||
;; mixed machine types in one call
|
||||
(println 1 2.5 "s" false)
|
||||
;; dyn and typed in the same call, dyn in the middle
|
||||
(println "pre" boxed "post")
|
||||
(println dlabel 7)
|
||||
;; print: zero args is nothing, and no separator ever trails
|
||||
(print)
|
||||
(print "|")
|
||||
(print "p")
|
||||
(print "|")
|
||||
(print "q" "r" 3)
|
||||
(println "|"))
|
||||
@ -3291,6 +3291,16 @@ level "1"
|
||||
there. 1005 is the defer, 6 is the value the restart produced. *)
|
||||
outputs "dyn: a defer on the transfer path" "programs/dyn-defer.flan"
|
||||
"1005\n6\n";
|
||||
(* print and println are variadic with Clojure's spacing — arguments in
|
||||
order, one space between each pair, println ending the line, (println)
|
||||
the newline alone and (print) nothing. The program's comment says what
|
||||
each line pins; the string here is the whole of the spacing rule, and
|
||||
the "|" markers are what make a leaked trailing space a visible red
|
||||
rather than an invisible byte. The dyn lines are typed and dyn
|
||||
arguments in one call, which is the two printers sharing one buffer. *)
|
||||
outputs "print and println are variadic"
|
||||
"programs/println-variadic.flan"
|
||||
"\none\na b\nx: 5 true\n1 2.5 s false\npre 21 post\nmid 7\n|p|q r 3|\n";
|
||||
outputs ~opt:"-O0" "dyn: a defer on the transfer path, -O0"
|
||||
"programs/dyn-defer.flan" "1005\n6\n";
|
||||
(* And through the other backend, where the defer and the roots meet in a
|
||||
|
||||
@ -2785,6 +2785,32 @@ let () =
|
||||
"(defn show [x $t] () {:where (equal? $t)} (println x))";
|
||||
accepts "and so is print"
|
||||
"(defn show [x $t] () {:where (equal? $t)} (print x))";
|
||||
(* One generic argument defers the whole call, neighbours included. *)
|
||||
accepts "a variadic println with a type variable among the arguments"
|
||||
"(defn show [x $t] () {:where (equal? $t)} (println \"x:\" x 1))";
|
||||
|
||||
(* Variadic print/println: any number of arguments, zero included. The
|
||||
acceptance program pins what comes out; these pin only what the checker
|
||||
admits, and — the part a program cannot show — where a refusal lands. *)
|
||||
accepts "println with no arguments" "(defn f [] () (println))";
|
||||
accepts "print with no arguments" "(defn f [] () (print))";
|
||||
accepts "println with three arguments of three types"
|
||||
"(defn f [] () (println \"x:\" 5 true))";
|
||||
(* An unprintable argument is refused at its own span, not the form's:
|
||||
each argument is checked carrying its own loc, and render.ml fails on
|
||||
the expression's loc. Column 43 is the [m], not the [(println]. *)
|
||||
(let name = "an unprintable argument is refused at the argument" in
|
||||
match checked "(defn f [m (Map i32 i32)] () (println \"x\" m))" with
|
||||
| _ ->
|
||||
incr failures;
|
||||
Printf.printf "FAIL %s: expected a type error\n" name
|
||||
| exception Loc.Error { Loc.dloc; dmsg; _ } ->
|
||||
let got = Loc.to_string dloc in
|
||||
if got <> "<test>:1:43" || not (contains dmsg "no printer for") then begin
|
||||
incr failures;
|
||||
Printf.printf "FAIL %s\n wanted: %s (no printer for)\n got: %s (%s)\n"
|
||||
name "<test>:1:43" got dmsg
|
||||
end);
|
||||
|
||||
(* A predicate a body relies on has to be carried by every signature between
|
||||
it and the call site, or the refusal moves into code the caller did not
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user