Printers for every shape a value can have
C-x C-e rendered the scalars and refused the rest, which made it a calculator
rather than a REPL. The renderer is now a compile-time walk over the type,
emitting a piece at a time: structs, nested structs, fixed arrays, slices,
options, enums by name, and pointers as their shape. A raylib Color comes back
through the FFI as (rl/Color {:r 17 :g 34 :b 51 :a 68}).
Piecewise emission is what makes composites possible at all - a struct is its
fields with punctuation between them, and concatenating that in generated IR
would need an allocator the language does not have.
u64 now renders, in C, with %llu. It used to refuse because i64->bytes is
signed and it would otherwise come back as -1, but refusing a whole struct
because one field is a u64 is much worse than adding a runtime entry point.
Strings are quoted and escaped in C for the same reason: unescaped content does
not round-trip and reads as a framing bug rather than as the value it is.
An enum renders as :name, recovered from the checker's table as a chain of
comparisons, since members are erased to i32 before the backend sees them; a
value outside the declared members falls through to its number, which is what
you would want to see. A pointer is rendered and never followed - it is the
only thing that could make the walk cycle, and dereferencing one a REPL was
handed is not a safe thing to do on someone's behalf.
Three bounds, easy to conflate. depth and span bound the walk, so sand's
[100 [100 u32]] grid does not unroll into ten thousand render sites. The output
is bounded once in the runtime, since a slice renders through a loop the
compiler cannot bound, and one place enforcing it means no renderer carries a
budget.
emit.ml's cast now treats an enum as the i32 it is. Nothing in the surface
language produces that - a keyword resolves against its enum and never widens -
but the renderer needs an enum's number when it falls outside the members.
This commit is contained in:
parent
44e199186e
commit
20fedd4ad8
45
NEXT.md
45
NEXT.md
@ -737,11 +737,46 @@ socket. The read is safe without a handshake because `flan_dev_result` bumps a
|
|||||||
generation counter last; the daemon waits for it to move rather than assuming
|
generation counter last; the daemon waits for it to move rather than assuming
|
||||||
the program has reached a frame boundary.
|
the program has reached a frame boundary.
|
||||||
|
|
||||||
What renders: the integers, the floats, `bool`, `string`, `[u8]`, `Unit`, and
|
The renderer is a **compile-time walk over the type**, emitting a piece at a
|
||||||
an enum (as its number — enum members are erased to `i32` before the backend
|
time through `flan_dev_emit`. Piecewise because a struct is its fields with
|
||||||
sees them). What refuses, by name: everything else, and **`u64` specifically**,
|
punctuation between them, and concatenating that in generated IR would need an
|
||||||
because `i64->bytes` is signed and anything past 2⁶³ would come back negative.
|
allocator the language does not have.
|
||||||
Refusing beats a number that is quietly wrong.
|
|
||||||
|
```
|
||||||
|
big 18446744073709551615
|
||||||
|
col :blue
|
||||||
|
(.pos b) (V {:x 1.5 :y 0})
|
||||||
|
b (Blob {:id 7 :name "sandy \"quoted\"" :pos (V {:x 1.5 :y 0}) :tags [ 0 42 0]})
|
||||||
|
(slice (.tags b) 0 3) [ 0 42 0]
|
||||||
|
(rl/get-color 0x11223344) (rl/Color {:r 17 :g 34 :b 51 :a 68})
|
||||||
|
sim/grid [ [ 0 0 0 0 0 0 0 0 ...] [ 0 ... ] ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Details that are decisions rather than formatting:
|
||||||
|
|
||||||
|
- **`u64` renders in C**, with `%llu`. The language's own `i64->bytes` is
|
||||||
|
signed, so it used to refuse rather than come back as `-1` — but refusing a
|
||||||
|
whole struct because one field is a `u64` is much worse, so the runtime got
|
||||||
|
an entry point instead.
|
||||||
|
- **Strings are quoted and escaped**, also in C. Unescaped content does not
|
||||||
|
round-trip and reads as a framing bug rather than as the value it is.
|
||||||
|
- **An enum renders as `:name`**, recovered from the checker's table as a chain
|
||||||
|
of comparisons, because members are erased to `i32` before the backend sees
|
||||||
|
them. A value outside the declared members falls through to its number, which
|
||||||
|
is exactly what you would want to see.
|
||||||
|
- **A pointer is never followed** — `<ptr>`. It is the only thing that could
|
||||||
|
make the walk cycle, and dereferencing one a REPL was handed is not a safe
|
||||||
|
thing to do on someone's behalf.
|
||||||
|
- **Three separate bounds**, easy to conflate. `depth` (4) and `span` (8) bound
|
||||||
|
the *walk*, so `[100 [100 u32]]` does not become ten thousand render sites in
|
||||||
|
one module. The *output* is bounded once in the runtime — `emit` truncates at
|
||||||
|
4K and `end` appends `...` — because a slice renders through a loop the
|
||||||
|
compiler cannot bound, and one place enforcing it means no renderer carries a
|
||||||
|
budget.
|
||||||
|
- A slice is the one case needing a runtime loop, and the slice goes into a
|
||||||
|
slot first so the expression it came from is not evaluated once per element.
|
||||||
|
|
||||||
|
What still refuses by name: `Map`, `Fn`, a type variable.
|
||||||
|
|
||||||
A caveat inherited from the language, not introduced here: `3.0` renders as
|
A caveat inherited from the language, not introduced here: `3.0` renders as
|
||||||
`3`, indistinguishable from the integer. `flan run calc-me.flan "1.5 * 2.0"`
|
`3`, indistinguishable from the integer. `flan run calc-me.flan "1.5 * 2.0"`
|
||||||
|
|||||||
10
lib/emit.ml
10
lib/emit.ml
@ -742,7 +742,15 @@ and shim_out f name (x : Tast.expr) =
|
|||||||
|
|
||||||
and cast f (x : Tast.expr) target =
|
and cast f (x : Tast.expr) target =
|
||||||
let v = value f x in
|
let v = value f x in
|
||||||
let src = x.Tast.ty in
|
(* An enum is an i32 at run time and its own type only in the checker, so a
|
||||||
|
cast involving one is a cast on that i32. Nothing in the surface language
|
||||||
|
produces this — a keyword resolves against the enum and never widens — but
|
||||||
|
the REPL's renderer needs an enum's number when it falls outside the
|
||||||
|
declared members. *)
|
||||||
|
let concrete (t : Types.t) =
|
||||||
|
match t with Types.Enum _ -> Types.Int Types.I32 | t -> t
|
||||||
|
in
|
||||||
|
let src = concrete x.Tast.ty and target = concrete target in
|
||||||
if Types.equal src target then v
|
if Types.equal src target then v
|
||||||
else
|
else
|
||||||
let op =
|
let op =
|
||||||
|
|||||||
243
lib/session.ml
243
lib/session.ml
@ -349,51 +349,204 @@ let eval ?(origin = "<eval>") t src : change =
|
|||||||
expression is wrapped in a function that has nowhere to be called from, and
|
expression is wrapped in a function that has nowhere to be called from, and
|
||||||
the module says "run this once". The agent does, at a frame boundary.
|
the module says "run this once". The agent does, at a frame boundary.
|
||||||
|
|
||||||
Getting the value back does not marshal anything. The compiler knows the
|
Getting the value back does not marshal anything. A Flan value carries no
|
||||||
expression's type, so the thunk renders it to bytes here, at compile time,
|
header, so nothing at run time could say what it is; the compiler knows the
|
||||||
and hands them to the runtime — which is the only thing that *can* work,
|
type and renders it *there*, in the thunk. That is the layout decision's
|
||||||
since a Flan value carries no header and nothing at run time could tell what
|
bill, paid here — and it is why the renderer is a compile-time walk over the
|
||||||
it is. That is the layout decision's bill, paid here.
|
type rather than a function in the runtime.
|
||||||
|
|
||||||
The rendering goes to [flan_dev_result], not to stdout: stdout belongs to
|
The rendering goes to [flan_dev_emit], a piece at a time, not to stdout.
|
||||||
the program, it is in the hot path for anything that prints, and a dev-only
|
Piecewise because a struct is its fields with punctuation between them and
|
||||||
feature must not put a branch in it. *)
|
concatenating that in generated IR would need an allocator the language does
|
||||||
|
not have; not stdout because stdout belongs to the program, is in the hot
|
||||||
|
path for anything that prints, and a dev-only feature must not put a branch
|
||||||
|
in it. *)
|
||||||
|
|
||||||
let result_sym = "flan/dev-result"
|
type emitter = { ename : string; ety : Types.t }
|
||||||
|
|
||||||
let result_extern : Tast.extern =
|
let emit_bytes = { ename = "flan/dev-emit"; ety = Types.Slice (Types.Int Types.U8) }
|
||||||
{ Tast.ename = result_sym; esym = "flan_dev_result";
|
let emit_str = { ename = "flan/dev-emit-str"; ety = Types.Slice (Types.Int Types.U8) }
|
||||||
eparams = [ Types.Slice (Types.Int Types.U8) ]; eret = Types.Unit }
|
let emit_i64 = { ename = "flan/dev-emit-i64"; ety = Types.Int Types.I64 }
|
||||||
|
let emit_u64 = { ename = "flan/dev-emit-u64"; ety = Types.Int Types.U64 }
|
||||||
|
let emit_f64 = { ename = "flan/dev-emit-f64"; ety = Types.Float Types.F64 }
|
||||||
|
|
||||||
(* The scalars, and nothing else yet. A struct, an (Option T) or a slice of
|
let externs : Tast.extern list =
|
||||||
structs needs a printer derived per type, which is real work; refusing by
|
let one e sym = { Tast.ename = e.ename; esym = sym; eparams = [ e.ety ];
|
||||||
name is the house rule, and a wrong rendering would be the silent kind. *)
|
eret = Types.Unit } in
|
||||||
let render (e : Tast.expr) : Tast.expr =
|
[ one emit_bytes "flan_dev_emit";
|
||||||
|
one emit_str "flan_dev_emit_str";
|
||||||
|
one emit_i64 "flan_dev_emit_i64";
|
||||||
|
one emit_u64 "flan_dev_emit_u64";
|
||||||
|
one emit_f64 "flan_dev_emit_f64";
|
||||||
|
{ Tast.ename = "flan/dev-begin"; esym = "flan_dev_result_begin";
|
||||||
|
eparams = []; eret = Types.Unit };
|
||||||
|
{ Tast.ename = "flan/dev-end"; esym = "flan_dev_result_end";
|
||||||
|
eparams = []; eret = Types.Unit } ]
|
||||||
|
|
||||||
|
(* Two separate limits, easily conflated. [depth] and [span] bound the *walk*,
|
||||||
|
so a big fixed array or a self-containing struct cannot turn one expression
|
||||||
|
into a module with ten thousand render sites in it. How much text actually
|
||||||
|
comes out is bounded in the runtime instead, once, for every renderer. *)
|
||||||
|
let max_depth = 4
|
||||||
|
let max_span = 8
|
||||||
|
|
||||||
|
type ctx = {
|
||||||
|
structs : Tast.structure list;
|
||||||
|
enums : (string * (string * int64) list) list;
|
||||||
|
(* Slots a rendered loop needs. The thunk's frame grows as the walk finds
|
||||||
|
slices in it. *)
|
||||||
|
mutable slots : Types.t list; (* reversed *)
|
||||||
|
mutable nslots : int;
|
||||||
|
}
|
||||||
|
|
||||||
|
let slot c ty =
|
||||||
|
let i = c.nslots in
|
||||||
|
c.nslots <- i + 1;
|
||||||
|
c.slots <- ty :: c.slots;
|
||||||
|
i
|
||||||
|
|
||||||
|
let rec render c depth (e : Tast.expr) : Tast.expr list =
|
||||||
let loc = e.Tast.loc in
|
let loc = e.Tast.loc in
|
||||||
let bytes = Types.Slice (Types.Int Types.U8) in
|
let unit_ e = { Tast.e; ty = Types.Unit; loc } in
|
||||||
|
let call em x = unit_ (Tast.Call (em.ename, [ x ])) in
|
||||||
let cast t x = { Tast.e = Tast.Prim (Tast.Cast t, [ x ]); ty = t; loc } in
|
let cast t x = { Tast.e = Tast.Prim (Tast.Cast t, [ x ]); ty = t; loc } in
|
||||||
let prim p x = { Tast.e = Tast.Prim (p, [ x ]); ty = bytes; loc } in
|
let bytes_of s =
|
||||||
let str s = { Tast.e = Tast.Str s; ty = Types.String; loc } in
|
{ Tast.e = Tast.Prim (Tast.Bytes, [ { Tast.e = Tast.Str s; ty = Types.String; loc } ]);
|
||||||
|
ty = Types.Slice (Types.Int Types.U8); loc }
|
||||||
|
in
|
||||||
|
let lit s = call emit_bytes (bytes_of s) in
|
||||||
|
let int64 n = { Tast.e = Tast.Int (n, Types.I64); ty = Types.Int Types.I64; loc } in
|
||||||
|
let i32 n =
|
||||||
|
{ Tast.e = Tast.Int (Int64.of_int n, Types.I32); ty = Types.Int Types.I32; loc }
|
||||||
|
in
|
||||||
|
let do_ xs = unit_ (Tast.Do xs) in
|
||||||
|
if depth > max_depth then [ lit "..." ]
|
||||||
|
else
|
||||||
match e.Tast.ty with
|
match e.Tast.ty with
|
||||||
| Types.Int Types.U64 ->
|
| Types.Int Types.U64 -> [ call emit_u64 e ]
|
||||||
(* i64->bytes is signed, so anything above 2^63 would render negative.
|
| Types.Int _ -> [ call emit_i64 (cast (Types.Int Types.I64) e) ]
|
||||||
Refusing beats a number that is quietly wrong. *)
|
| Types.Float _ -> [ call emit_f64 (cast (Types.Float Types.F64) e) ]
|
||||||
fail loc "no printer for u64 yet — its rendering would be signed"
|
|
||||||
| Types.Int _ -> prim Tast.I64ToBytes (cast (Types.Int Types.I64) e)
|
|
||||||
| Types.Enum _ ->
|
|
||||||
(* An enum is an i32 at run time and its members are not carried into the
|
|
||||||
backend, so this is the number and not the name. *)
|
|
||||||
prim Tast.I64ToBytes (cast (Types.Int Types.I64) e)
|
|
||||||
| Types.Float _ -> prim Tast.F64ToBytes (cast (Types.Float Types.F64) e)
|
|
||||||
| Types.Bool ->
|
| Types.Bool ->
|
||||||
{ Tast.e = Tast.If (e, prim Tast.Bytes (str "true"), prim Tast.Bytes (str "false"));
|
[ unit_ (Tast.If (e, lit "true", lit "false")) ]
|
||||||
ty = bytes; loc }
|
| Types.Unit -> [ lit "()" ]
|
||||||
| Types.String -> prim Tast.Bytes e
|
| Types.String ->
|
||||||
| Types.Slice (Types.Int Types.U8) -> e
|
[ call emit_str
|
||||||
| Types.Unit -> prim Tast.Bytes (str "()")
|
{ Tast.e = Tast.Prim (Tast.Bytes, [ e ]);
|
||||||
|
ty = Types.Slice (Types.Int Types.U8); loc } ]
|
||||||
|
(* Bytes are almost always text, and escaping makes the case where they are
|
||||||
|
not readable rather than a mess. *)
|
||||||
|
| Types.Slice (Types.Int Types.U8) -> [ call emit_str e ]
|
||||||
|
(* An enum's members are erased to i32 before the backend sees them, so the
|
||||||
|
name has to be recovered here, from the checker's table, as a chain of
|
||||||
|
comparisons. Falling through to the number is not a failure: a value
|
||||||
|
outside the declared members is exactly what you would want to see. *)
|
||||||
|
| Types.Enum n ->
|
||||||
|
let members = try List.assoc n c.enums with Not_found -> [] in
|
||||||
|
let number = call emit_i64 (cast (Types.Int Types.I64) e) in
|
||||||
|
List.fold_left
|
||||||
|
(fun otherwise (name, v) ->
|
||||||
|
let is =
|
||||||
|
{ Tast.e =
|
||||||
|
Tast.Prim (Tast.Eq,
|
||||||
|
[ cast (Types.Int Types.I64) e; int64 v ]);
|
||||||
|
ty = Types.Bool; loc }
|
||||||
|
in
|
||||||
|
unit_ (Tast.If (is, lit (":" ^ name), otherwise)))
|
||||||
|
number members
|
||||||
|
|> fun x -> [ x ]
|
||||||
|
(* A pointer is rendered as its shape and never followed: it is the only
|
||||||
|
thing that could make this walk cycle, and dereferencing one a REPL was
|
||||||
|
handed is not a safe thing to do on someone's behalf. *)
|
||||||
|
| Types.Ptr _ -> [ lit "<ptr>" ]
|
||||||
|
| Types.Option t ->
|
||||||
|
let tag = { Tast.e = Tast.Field (e, 0); ty = Types.Int Types.I8; loc } in
|
||||||
|
let some = { Tast.e = Tast.Field (e, 1); ty = t; loc } in
|
||||||
|
let is_some =
|
||||||
|
{ Tast.e =
|
||||||
|
Tast.Prim (Tast.Ne,
|
||||||
|
[ tag; { Tast.e = Tast.Int (0L, Types.I8);
|
||||||
|
ty = Types.Int Types.I8; loc } ]);
|
||||||
|
ty = Types.Bool; loc }
|
||||||
|
in
|
||||||
|
[ unit_
|
||||||
|
(Tast.If (is_some,
|
||||||
|
do_ ((lit "(some " :: render c (depth + 1) some) @ [ lit ")" ]),
|
||||||
|
lit "none")) ]
|
||||||
|
| Types.Named n ->
|
||||||
|
(match
|
||||||
|
List.find_opt (fun (s : Tast.structure) -> String.equal s.Tast.sname n)
|
||||||
|
c.structs
|
||||||
|
with
|
||||||
|
| None -> [ lit ("<" ^ n ^ ">") ]
|
||||||
|
| Some st ->
|
||||||
|
let fields = st.Tast.fields in
|
||||||
|
let shown = List.filteri (fun i _ -> i < max_span) fields in
|
||||||
|
let parts =
|
||||||
|
List.concat
|
||||||
|
(List.mapi
|
||||||
|
(fun i (f : Tast.field) ->
|
||||||
|
let v = { Tast.e = Tast.Field (e, i); ty = f.Tast.fty; loc } in
|
||||||
|
(if i = 0 then [] else [ lit " " ])
|
||||||
|
@ [ lit (":" ^ f.Tast.fname ^ " ") ]
|
||||||
|
@ render c (depth + 1) v)
|
||||||
|
shown)
|
||||||
|
in
|
||||||
|
[ do_ ((lit ("(" ^ n ^ " {") :: parts)
|
||||||
|
@ (if List.length fields > max_span then [ lit " ..." ] else [])
|
||||||
|
@ [ lit "})" ]) ])
|
||||||
|
(* A fixed array's length is in its type, so it unrolls — capped, because
|
||||||
|
sand's grid is [100 [100 u32]] and unrolling that is ten thousand render
|
||||||
|
sites in one module. *)
|
||||||
|
| Types.Array (n, t) ->
|
||||||
|
let shown = min (Int64.to_int n) max_span in
|
||||||
|
let parts =
|
||||||
|
List.concat
|
||||||
|
(List.init shown (fun i ->
|
||||||
|
let v =
|
||||||
|
{ Tast.e = Tast.Prim (Tast.At, [ e; i32 i ]); ty = t; loc }
|
||||||
|
in
|
||||||
|
lit " " :: render c (depth + 1) v))
|
||||||
|
in
|
||||||
|
[ do_ ((lit "[" :: parts)
|
||||||
|
@ (if Int64.to_int n > shown then [ lit " ..." ] else [])
|
||||||
|
@ [ lit "]" ]) ]
|
||||||
|
(* A slice's length is not known until it runs, so this is the one case
|
||||||
|
that needs a loop. The slice goes into a slot first: the expression it
|
||||||
|
came from must not be evaluated once per element. *)
|
||||||
|
| Types.Slice t ->
|
||||||
|
let sv = slot c e.Tast.ty and iv = slot c (Types.Int Types.I32) in
|
||||||
|
let local i ty = { Tast.e = Tast.Local i; ty; loc } in
|
||||||
|
let len =
|
||||||
|
{ Tast.e = Tast.Prim (Tast.Len, [ local sv e.Tast.ty ]);
|
||||||
|
ty = Types.Int Types.I32; loc }
|
||||||
|
in
|
||||||
|
let cond =
|
||||||
|
{ Tast.e = Tast.Prim (Tast.Lt, [ local iv (Types.Int Types.I32); len ]);
|
||||||
|
ty = Types.Bool; loc }
|
||||||
|
in
|
||||||
|
let elem =
|
||||||
|
{ Tast.e =
|
||||||
|
Tast.Prim (Tast.At, [ local sv e.Tast.ty; local iv (Types.Int Types.I32) ]);
|
||||||
|
ty = t; loc }
|
||||||
|
in
|
||||||
|
let step =
|
||||||
|
unit_
|
||||||
|
(Tast.Set
|
||||||
|
(Tast.Plocal iv,
|
||||||
|
{ Tast.e =
|
||||||
|
Tast.Prim (Tast.Add, [ local iv (Types.Int Types.I32); i32 1 ]);
|
||||||
|
ty = Types.Int Types.I32; loc }))
|
||||||
|
in
|
||||||
|
[ unit_
|
||||||
|
(Tast.Let
|
||||||
|
([ (sv, e); (iv, i32 0) ],
|
||||||
|
[ lit "[";
|
||||||
|
unit_
|
||||||
|
(Tast.While
|
||||||
|
(cond, (lit " " :: render c (depth + 1) elem) @ [ step ]));
|
||||||
|
lit "]" ])) ]
|
||||||
| t ->
|
| t ->
|
||||||
fail loc "no printer for %s yet — only the scalars, bool and strings render"
|
fail loc "no printer for %s" (Types.to_string t)
|
||||||
(Types.to_string t)
|
|
||||||
|
|
||||||
let eval_expr ?(origin = "<eval>") t src : change =
|
let eval_expr ?(origin = "<eval>") t src : change =
|
||||||
let form =
|
let form =
|
||||||
@ -402,16 +555,22 @@ let eval_expr ?(origin = "<eval>") t src : change =
|
|||||||
| [] -> fail Loc.unknown "nothing to evaluate"
|
| [] -> fail Loc.unknown "nothing to evaluate"
|
||||||
| _ :: f :: _ -> fail f.Form.loc "one expression at a time"
|
| _ :: f :: _ -> fail f.Form.loc "one expression at a time"
|
||||||
in
|
in
|
||||||
let checked, slots = Check.expression t.env (Parse.expr form) in
|
let checked, base = Check.expression t.env (Parse.expr form) in
|
||||||
|
let c =
|
||||||
|
{ structs = t.program.Tast.structs;
|
||||||
|
enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums [];
|
||||||
|
slots = []; nslots = Array.length base }
|
||||||
|
in
|
||||||
|
let loc = checked.Tast.loc in
|
||||||
|
let nullary n = { Tast.e = Tast.Call (n, []); ty = Types.Unit; loc } in
|
||||||
let body =
|
let body =
|
||||||
[ { Tast.e = Tast.Call (result_sym, [ render checked ]);
|
(nullary "flan/dev-begin" :: render c 0 checked) @ [ nullary "flan/dev-end" ]
|
||||||
ty = Types.Unit; loc = checked.Tast.loc } ]
|
|
||||||
in
|
in
|
||||||
t.thunks <- t.thunks + 1;
|
t.thunks <- t.thunks + 1;
|
||||||
let name = Printf.sprintf "eval/%d" t.thunks in
|
let name = Printf.sprintf "eval/%d" t.thunks in
|
||||||
let thunk : Tast.fn =
|
let thunk : Tast.fn =
|
||||||
{ Tast.name; params = []; slots; ret = Types.Unit; body;
|
{ Tast.name; params = []; ret = Types.Unit; body; floc = loc;
|
||||||
floc = checked.Tast.loc }
|
slots = Array.append base (Array.of_list (List.rev c.slots)) }
|
||||||
in
|
in
|
||||||
(* Built against the program but never spliced into it: an evaluation is not
|
(* Built against the program but never spliced into it: an evaluation is not
|
||||||
a declaration, and adding one would leave the session carrying an eval/N
|
a declaration, and adding one would leave the session carrying an eval/N
|
||||||
@ -419,7 +578,7 @@ let eval_expr ?(origin = "<eval>") t src : change =
|
|||||||
let program =
|
let program =
|
||||||
{ t.program with
|
{ t.program with
|
||||||
Tast.fns = t.program.Tast.fns @ [ thunk ];
|
Tast.fns = t.program.Tast.fns @ [ thunk ];
|
||||||
externs = t.program.Tast.externs @ [ result_extern ] }
|
externs = t.program.Tast.externs @ externs }
|
||||||
in
|
in
|
||||||
let ir =
|
let ir =
|
||||||
Emit.redefinition ~dev:true ~known:(known t) ~call:name program ~fns:[ name ]
|
Emit.redefinition ~dev:true ~known:(known t) ~call:name program ~fns:[ name ]
|
||||||
|
|||||||
@ -12,8 +12,8 @@
|
|||||||
*
|
*
|
||||||
* flan_dev_cell(name) the cell a new function lives in
|
* flan_dev_cell(name) the cell a new function lives in
|
||||||
* flan_dev_global(name, size, init) the storage a new global lives in
|
* flan_dev_global(name, size, init) the storage a new global lives in
|
||||||
* flan_dev_result(bytes, len) where an evaluated expression's rendering
|
* flan_dev_emit(...) where an evaluated expression's rendering
|
||||||
* goes, for the daemon to read back
|
* goes, piece by piece, to be read back
|
||||||
*
|
*
|
||||||
* Both are idempotent: the second module to mention a name gets what the first
|
* Both are idempotent: the second module to mention a name gets what the first
|
||||||
* one got. That is the whole point. Two modules that each define their own
|
* one got. That is the whole point. Two modules that each define their own
|
||||||
@ -106,10 +106,20 @@ void *flan_dev_global(const char *name, uint64_t size, const void *init) {
|
|||||||
|
|
||||||
/* ── The value of an evaluated expression ──────────────────────────── */
|
/* ── The value of an evaluated expression ──────────────────────────── */
|
||||||
|
|
||||||
/* C-x C-e compiles a thunk that renders one expression and calls this with the
|
/* C-x C-e compiles a thunk that renders one expression and emits it here, a
|
||||||
* text. It is not written to stdout: stdout belongs to the program, it is in
|
* piece at a time. It is not written to stdout: stdout belongs to the program,
|
||||||
* the hot path for anything that prints, and a dev-only feature must not put a
|
* it is in the hot path for anything that prints, and a dev-only feature must
|
||||||
* branch in it. The daemon reads this back over the agent's socket instead.
|
* not put a branch in it. The daemon reads this back over the agent's socket.
|
||||||
|
*
|
||||||
|
* Emitting piece by piece rather than returning one string is what makes a
|
||||||
|
* composite renderer possible at all — a struct is its fields with punctuation
|
||||||
|
* between them, and concatenating that in the generated IR would mean an
|
||||||
|
* allocator the language does not have.
|
||||||
|
*
|
||||||
|
* The output bound lives here and nowhere else. A slice of a million elements
|
||||||
|
* renders with a loop the compiler cannot bound, so [emit] truncates and
|
||||||
|
* [end] says so with an ellipsis. One place enforcing it means no renderer has
|
||||||
|
* to carry a budget.
|
||||||
*
|
*
|
||||||
* [generation] is what makes the read safe without a handshake. The thunk runs
|
* [generation] is what makes the read safe without a handshake. The thunk runs
|
||||||
* on the game thread at a frame boundary, whenever that happens to be; the
|
* on the game thread at a frame boundary, whenever that happens to be; the
|
||||||
@ -118,13 +128,86 @@ void *flan_dev_global(const char *name, uint64_t size, const void *init) {
|
|||||||
#define RESULT_MAX 4096
|
#define RESULT_MAX 4096
|
||||||
static char result[RESULT_MAX];
|
static char result[RESULT_MAX];
|
||||||
static size_t result_len;
|
static size_t result_len;
|
||||||
|
static int result_full;
|
||||||
static uint64_t generation;
|
static uint64_t generation;
|
||||||
|
|
||||||
void flan_dev_result(const uint8_t *bytes, int64_t len) {
|
void flan_dev_result_begin(void) {
|
||||||
|
result_len = 0;
|
||||||
|
result_full = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void flan_dev_emit(const uint8_t *bytes, int64_t len) {
|
||||||
size_t n = len < 0 ? 0 : (size_t)len;
|
size_t n = len < 0 ? 0 : (size_t)len;
|
||||||
if (n > RESULT_MAX) n = RESULT_MAX;
|
if (result_len + n > RESULT_MAX) {
|
||||||
memcpy(result, bytes, n);
|
n = RESULT_MAX - result_len;
|
||||||
result_len = n;
|
result_full = 1;
|
||||||
|
}
|
||||||
|
memcpy(result + result_len, bytes, n);
|
||||||
|
result_len += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void emit_cstr(const char *s) {
|
||||||
|
flan_dev_emit((const uint8_t *)s, (int64_t)strlen(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rendered in C so that u64 is not a lie: the language's own i64->bytes is
|
||||||
|
* signed, and anything past 2^63 would come back negative. */
|
||||||
|
void flan_dev_emit_u64(uint64_t x) {
|
||||||
|
char buf[32];
|
||||||
|
snprintf(buf, sizeof buf, "%llu", (unsigned long long)x);
|
||||||
|
emit_cstr(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void flan_dev_emit_i64(int64_t x) {
|
||||||
|
char buf[32];
|
||||||
|
snprintf(buf, sizeof buf, "%lld", (long long)x);
|
||||||
|
emit_cstr(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void flan_dev_emit_f64(double x) {
|
||||||
|
char buf[64];
|
||||||
|
snprintf(buf, sizeof buf, "%g", x);
|
||||||
|
emit_cstr(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quoted and escaped, in C, because doing it in the generated IR would be a
|
||||||
|
* loop per string and the language has no allocator to build the result in.
|
||||||
|
* A string whose content is not escaped does not round-trip and reads as a
|
||||||
|
* framing bug rather than as the value it is. */
|
||||||
|
void flan_dev_emit_str(const uint8_t *bytes, int64_t len) {
|
||||||
|
size_t n = len < 0 ? 0 : (size_t)len;
|
||||||
|
emit_cstr("\"");
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
unsigned char c = bytes[i];
|
||||||
|
switch (c) {
|
||||||
|
case '"': emit_cstr("\\\""); break;
|
||||||
|
case '\\': emit_cstr("\\\\"); break;
|
||||||
|
case '\n': emit_cstr("\\n"); break;
|
||||||
|
case '\t': emit_cstr("\\t"); break;
|
||||||
|
case '\r': emit_cstr("\\r"); break;
|
||||||
|
default:
|
||||||
|
if (c < 0x20) {
|
||||||
|
char buf[8];
|
||||||
|
snprintf(buf, sizeof buf, "\\x%02x", c);
|
||||||
|
emit_cstr(buf);
|
||||||
|
} else {
|
||||||
|
flan_dev_emit(&c, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit_cstr("\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
void flan_dev_result_end(void) {
|
||||||
|
if (result_full) {
|
||||||
|
/* Room is made for it rather than assumed: the buffer is full by
|
||||||
|
* definition when this fires. */
|
||||||
|
const char *ell = "...";
|
||||||
|
size_t k = strlen(ell);
|
||||||
|
if (result_len > RESULT_MAX - k) result_len = RESULT_MAX - k;
|
||||||
|
memcpy(result + result_len, ell, k);
|
||||||
|
result_len += k;
|
||||||
|
}
|
||||||
/* Last, so a reader that sees the new generation sees the whole value. */
|
/* Last, so a reader that sees the new generation sees the whole value. */
|
||||||
__atomic_store_n(&generation, generation + 1, __ATOMIC_RELEASE);
|
__atomic_store_n(&generation, generation + 1, __ATOMIC_RELEASE);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,6 @@
|
|||||||
(import agent "vendor:agent")
|
(import agent "vendor:agent")
|
||||||
|
|
||||||
(defvar ticks i64)
|
(defvar ticks i64)
|
||||||
(defconst step-by i64 3)
|
|
||||||
|
|
||||||
(defn step [] i64
|
(defn step [] i64
|
||||||
(set ticks (+ ticks 1))
|
(set ticks (+ ticks 1))
|
||||||
ticks)
|
ticks)
|
||||||
|
|||||||
38
test/programs/printers.flan
Normal file
38
test/programs/printers.flan
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
;;;; The fixture C-x C-e is tested against: one value of every shape the
|
||||||
|
;;;; renderer knows, held in globals so an expression has something real to
|
||||||
|
;;;; read out of a running process.
|
||||||
|
;;;;
|
||||||
|
;;;; It keeps running rather than counting reloads, because a thunk only runs
|
||||||
|
;;;; when the program next reaches a frame boundary. (agent/wait 5) is both the
|
||||||
|
;;;; poll and the pacing — 5ms of nothing, which is what a frame is when there
|
||||||
|
;;;; is no frame.
|
||||||
|
(import agent "vendor:agent")
|
||||||
|
|
||||||
|
(defstruct V [x f32 y f32])
|
||||||
|
(defstruct Blob [id i32 name string pos V tags [3 i32]])
|
||||||
|
(defenum Colour [red 0 green 1 blue 2])
|
||||||
|
|
||||||
|
(defvar ticks i64)
|
||||||
|
(defvar big u64)
|
||||||
|
(defvar b Blob)
|
||||||
|
(defvar arr [4 i32])
|
||||||
|
(defvar col Colour)
|
||||||
|
|
||||||
|
;;; Read by nothing on purpose: it is here to be *evaluated*, so that C-x C-e
|
||||||
|
;;; is tested against a constant living in the program's memory and not only
|
||||||
|
;;; against arithmetic the compiler could have done itself.
|
||||||
|
(defconst step-by i64 3)
|
||||||
|
|
||||||
|
(defn main [] i32
|
||||||
|
(agent/start "/tmp/flan-printers-fallback.sock")
|
||||||
|
(set big 0xFFFFFFFFFFFFFFFF)
|
||||||
|
(set (.id b) 7)
|
||||||
|
(set (.name b) "sandy \"quoted\"")
|
||||||
|
(set (.x (.pos b)) 1.5)
|
||||||
|
(set (at (.tags b) 1) 42)
|
||||||
|
(set (at arr 2) 9)
|
||||||
|
(set col :blue)
|
||||||
|
(dotimes [i 4000]
|
||||||
|
(agent/wait 5)
|
||||||
|
(set ticks (+ ticks 1)))
|
||||||
|
0)
|
||||||
@ -60,7 +60,7 @@ let () =
|
|||||||
let flan = "../bin/main.exe" in
|
let flan = "../bin/main.exe" in
|
||||||
let pid =
|
let pid =
|
||||||
Unix.create_process flan
|
Unix.create_process flan
|
||||||
[| flan; "dev"; "programs/dev-repl.flan"; "-s"; sock |]
|
[| flan; "dev"; "programs/printers.flan"; "-s"; sock |]
|
||||||
Unix.stdin fd Unix.stderr
|
Unix.stdin fd Unix.stderr
|
||||||
in
|
in
|
||||||
Unix.close fd;
|
Unix.close fd;
|
||||||
@ -82,9 +82,26 @@ let () =
|
|||||||
in
|
in
|
||||||
value "arithmetic" "(+ 1 2)" "3";
|
value "arithmetic" "(+ 1 2)" "3";
|
||||||
value "a comparison" "(< 1 2)" "true";
|
value "a comparison" "(< 1 2)" "true";
|
||||||
value "a string" "\"hi\"" "hi";
|
(* Quoted and escaped, in the runtime: a string whose content is not
|
||||||
(* A defconst: its value is in the program's rodata and this reads it. *)
|
escaped does not round-trip and reads as a framing bug. *)
|
||||||
|
value "a string" "\"hi\"" "\"hi\"";
|
||||||
|
value "an escaped string" "(.name b)" "\"sandy \\\"quoted\\\"\"";
|
||||||
|
(* A defconst: its value is in the program's memory and this reads it. *)
|
||||||
value "a constant" "step-by" "3";
|
value "a constant" "step-by" "3";
|
||||||
|
(* Rendered in C, because the language's own i64->bytes is signed and
|
||||||
|
this would otherwise come back as -1. *)
|
||||||
|
value "u64 at its maximum" "big" "18446744073709551615";
|
||||||
|
(* A struct, nested, with a fixed array inside it. *)
|
||||||
|
value "a struct" "(.pos b)" "(V {:x 1.5 :y 0})";
|
||||||
|
value "a nested struct" "b"
|
||||||
|
"(Blob {:id 7 :name \"sandy \\\"quoted\\\"\" :pos (V {:x 1.5 :y 0}) :tags [ 0 42 0]})";
|
||||||
|
value "a fixed array" "arr" "[ 0 0 9 0]";
|
||||||
|
(* A slice's length is not known until it runs, so this one renders
|
||||||
|
through a loop rather than by unrolling. *)
|
||||||
|
value "a slice" "(slice (.tags b) 0 3)" "[ 0 42 0]";
|
||||||
|
(* An enum's members are erased to i32 before the backend sees them, so
|
||||||
|
the name is recovered from the checker's table. *)
|
||||||
|
value "an enum" "col" ":blue";
|
||||||
|
|
||||||
(* The one that proves it ran inside the process: the program increments
|
(* The one that proves it ran inside the process: the program increments
|
||||||
[ticks] every frame, so two evaluations of it must disagree. A copy
|
[ticks] every frame, so two evaluations of it must disagree. A copy
|
||||||
@ -101,9 +118,6 @@ let () =
|
|||||||
program advancing" a b
|
program advancing" a b
|
||||||
| _ -> fail "ticks did not evaluate");
|
| _ -> fail "ticks did not evaluate");
|
||||||
|
|
||||||
(* Types with no printer derived yet refuse by name rather than render
|
|
||||||
something plausible and wrong. u64 is its own case: i64->bytes is
|
|
||||||
signed, so anything past 2^63 would come back negative. *)
|
|
||||||
let refuses name code reason =
|
let refuses name code reason =
|
||||||
let r = evals code in
|
let r = evals code in
|
||||||
match field r "message" with
|
match field r "message" with
|
||||||
@ -114,7 +128,6 @@ let () =
|
|||||||
| Some m -> fail "%s said %S, wanted it to mention %S" name m reason
|
| Some m -> fail "%s said %S, wanted it to mention %S" name m reason
|
||||||
| None -> fail "%s was accepted" name
|
| None -> fail "%s was accepted" name
|
||||||
in
|
in
|
||||||
refuses "u64" "rand-state" "no printer for u64";
|
|
||||||
refuses "a declaration" "(defvar nope i64)" "";
|
refuses "a declaration" "(defvar nope i64)" "";
|
||||||
refuses "an unknown name" "no-such-name" "unknown name";
|
refuses "an unknown name" "no-such-name" "unknown name";
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user