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
|
All of it dispatches after the x86-dyn lane lands. The struct dyn-field
|
||||||
refusal (01e60fa) is the stopgap 2 lifts.
|
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
|
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. *)
|
nested, which is why it lives here and not in the walk. *)
|
||||||
| "print" | "println" ->
|
| "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
|
(* 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
|
nothing. Without this, (println v) would consume a Vec and every
|
||||||
printing of one would be its last. *)
|
printing of one would be its last. *)
|
||||||
let target = List.hd args in
|
let checked = List.map (fun target -> check ctx target) args in
|
||||||
let a = check ctx target in
|
|
||||||
(* ── The allow-list, and what it takes to get on it ───────────────
|
(* ── The allow-list, and what it takes to get on it ───────────────
|
||||||
plan.org names [println] as the one compiler-provided exception — it
|
plan.org names [println] as the one compiler-provided exception — it
|
||||||
"selects a structural printer at each concrete instantiation" — and
|
"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 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
|
the abstract pass. The real printer is selected when the copy is
|
||||||
checked with [t] concrete. *)
|
checked with [t] concrete. One generic argument defers the whole call:
|
||||||
if generic_ty a.Tast.ty then
|
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
|
mk loc Types.Unit Tast.Unit
|
||||||
else
|
else
|
||||||
let bslice = Types.Slice (Types.Int Types.U8) in
|
let bslice = Types.Slice (Types.Int Types.U8) in
|
||||||
@ -5264,12 +5272,26 @@ and named_call ctx ~want loc name args =
|
|||||||
ptrs = None;
|
ptrs = None;
|
||||||
alloc = (fun ty -> fresh_slot ctx ty) }
|
alloc = (fun ty -> fresh_slot ctx ty) }
|
||||||
in
|
in
|
||||||
let parts =
|
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 ]))) ]
|
||||||
| _ -> Render.render rc 0 a
|
| _ -> Render.render rc 0 a
|
||||||
in
|
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 =
|
let nl =
|
||||||
if String.equal name "println" then
|
if String.equal name "println" then
|
||||||
[ write
|
[ write
|
||||||
@ -5913,12 +5935,14 @@ let builtins : (string * string * string) list =
|
|||||||
("write-stdout", "write-stdout [[u8]] ()",
|
("write-stdout", "write-stdout [[u8]] ()",
|
||||||
"Writes the bytes to standard output exactly as given: no newline and \
|
"Writes the bytes to standard output exactly as given: no newline and \
|
||||||
no formatting.");
|
no formatting.");
|
||||||
("print", "print [T] ()",
|
("print", "print [T ...] ()",
|
||||||
"The structural printer, selected for the argument's concrete type. A \
|
"The structural printer, selected for each argument's concrete type; \
|
||||||
string prints raw at the top level and quoted inside a structure, and \
|
arguments print in order with a single space between them. A string \
|
||||||
a Ptr or a Handle prints its address rather than being followed. \
|
prints raw at the top level and quoted inside a structure, and a Ptr \
|
||||||
Printing is a read, so it does not consume the value.");
|
or a Handle prints its address rather than being followed. Printing \
|
||||||
("println", "println [T] ()", "print, with a newline after it.");
|
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",
|
("exit", "exit [i32] never",
|
||||||
"Ends the process with this status. It has no value, so nothing written \
|
"Ends the process with this status. It has no value, so nothing written \
|
||||||
after it runs.");
|
after it runs.");
|
||||||
|
|||||||
@ -19,8 +19,10 @@
|
|||||||
|
|
||||||
No printing function is here at all any more. [print] and [println] are
|
No printing function is here at all any more. [print] and [println] are
|
||||||
the whole printing surface, and neither is a function: both are
|
the whole printing surface, and neither is a function: both are
|
||||||
compiler-provided and structural, a walk over the concrete type at the
|
compiler-provided and structural, a walk over each argument's concrete
|
||||||
call site (check.ml, and the walk itself in render.ml). That is plan.org's
|
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
|
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
|
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
|
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. *)
|
there. 1005 is the defer, 6 is the value the restart produced. *)
|
||||||
outputs "dyn: a defer on the transfer path" "programs/dyn-defer.flan"
|
outputs "dyn: a defer on the transfer path" "programs/dyn-defer.flan"
|
||||||
"1005\n6\n";
|
"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"
|
outputs ~opt:"-O0" "dyn: a defer on the transfer path, -O0"
|
||||||
"programs/dyn-defer.flan" "1005\n6\n";
|
"programs/dyn-defer.flan" "1005\n6\n";
|
||||||
(* And through the other backend, where the defer and the roots meet in a
|
(* 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))";
|
"(defn show [x $t] () {:where (equal? $t)} (println x))";
|
||||||
accepts "and so is print"
|
accepts "and so is print"
|
||||||
"(defn show [x $t] () {:where (equal? $t)} (print x))";
|
"(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
|
(* 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
|
it and the call site, or the refusal moves into code the caller did not
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user