Merge branch 'println' into dev-loop
nth removed, and the REPL's structural walk lifted into a println that shares it. The example of the narrowing-index rule used nth, which no longer exists.
This commit is contained in:
commit
2474397d30
4
MY-NOTES.org
Normal file
4
MY-NOTES.org
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
* Additional things
|
||||||
|
** Why do we have prints dedicted per type? That's terrible, what do we need to have proper (println) work for everything?
|
||||||
|
** The current std is suffering from the same problem, functions should be generic? Why is it like this?
|
||||||
|
** Why do we have =at= and =nth= if they're the same? Pick one, maybe =at=. For all STD functions make sure we don't have redundancy, even if it matches clojure or lisp
|
||||||
2
NEXT.md
2
NEXT.md
@ -620,7 +620,7 @@ Most of these are edges the language keeps and you should know about. Two — th
|
|||||||
both found by review after milestone 4 — were bugs that reached LLVM or ran wrong, and are **fixed**; each says so. They
|
both found by review after milestone 4 — were bugs that reached LLVM or ran wrong, and are **fixed**; each says so. They
|
||||||
stay written down because each one is now a rule the checker enforces, and a later change could quietly drop it.
|
stay written down because each one is now a rule the checker enforces, and a later change could quietly drop it.
|
||||||
|
|
||||||
- **An index converts from a narrower integer and never from a wider one.** `(nth colors current-color)` with a `u32`
|
- **An index converts from a narrower integer and never from a wider one.** `(at colors current-color)` with a `u32`
|
||||||
index works — anything above 2³¹ truncates to a negative `i32` and the unsigned bounds check rejects it. An `i64` index
|
index works — anything above 2³¹ truncates to a negative `i32` and the unsigned bounds check rejects it. An `i64` index
|
||||||
is refused with the reason: 2³²+5 truncates to 5 and would read the wrong element with no trap at all.
|
is refused with the reason: 2³²+5 truncates to 5 and would read the wrong element with no trap at all.
|
||||||
- **There is one top-level namespace, and `check.ml` now enforces it.** The environment's tables are per-kind — structs,
|
- **There is one top-level namespace, and `check.ml` now enforces it.** The environment's tables are per-kind — structs,
|
||||||
|
|||||||
@ -120,6 +120,6 @@
|
|||||||
(defn main [args [string]] i32
|
(defn main [args [string]] i32
|
||||||
(if (< (len args) 2)
|
(if (< (len args) 2)
|
||||||
(do (print-line "usage: calc-me \"1 + 2 * 3\"") 1)
|
(do (print-line "usage: calc-me \"1 + 2 * 3\"") 1)
|
||||||
(match (evaluate (bytes (nth args 1)))
|
(match (evaluate (bytes (at args 1)))
|
||||||
(Some v) (do (print-f64 v) (print-line "") 0)
|
(Some v) (do (print-f64 v) (print-line "") 0)
|
||||||
None (do (print-line "calc-me: cannot parse") 1))))
|
None (do (print-line "calc-me: cannot parse") 1))))
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
(defconst flan--special
|
(defconst flan--special
|
||||||
'("let" "if" "do" "while" "until" "dotimes" "loop" "match" "set" "return"
|
'("let" "if" "do" "while" "until" "dotimes" "loop" "match" "set" "return"
|
||||||
"defer" "some" "none" "try" "zeroed" "uninit" "slice" "at" "len" "addr"
|
"defer" "some" "none" "try" "zeroed" "uninit" "slice" "at" "len" "addr"
|
||||||
"bytes" "cast" "true" "false" "nil")
|
"bytes" "cast" "true" "false" "nil" "print" "println")
|
||||||
"Forms with meaning to the checker.")
|
"Forms with meaning to the checker.")
|
||||||
|
|
||||||
(defvar flan-font-lock-keywords
|
(defvar flan-font-lock-keywords
|
||||||
|
|||||||
55
lib/check.ml
55
lib/check.ml
@ -1128,7 +1128,7 @@ and named_call ctx ~want loc name args =
|
|||||||
| other -> fail loc "len takes an array, a slice or a string, found %s"
|
| other -> fail loc "len takes an array, a slice or a string, found %s"
|
||||||
(Types.to_string other));
|
(Types.to_string other));
|
||||||
prim Tast.Len index_ty [ a ]
|
prim Tast.Len index_ty [ a ]
|
||||||
| "at" | "nth" ->
|
| "at" ->
|
||||||
(match args with
|
(match args with
|
||||||
| target :: idx when idx <> [] ->
|
| target :: idx when idx <> [] ->
|
||||||
let target = check ctx target in
|
let target = check ctx target in
|
||||||
@ -1214,6 +1214,59 @@ and named_call ctx ~want loc name args =
|
|||||||
| "write-stdout" ->
|
| "write-stdout" ->
|
||||||
arity loc name 1 args;
|
arity loc name 1 args;
|
||||||
prim Tast.WriteStdout Types.Unit [ byte_slice ctx (List.hd args) ]
|
prim Tast.WriteStdout Types.Unit [ byte_slice ctx (List.hd args) ]
|
||||||
|
|
||||||
|
(* (println x) and (print x): the structural printer, selected on the type
|
||||||
|
the argument checked to. plan.org, Milestone 5 — "compiler-provided,
|
||||||
|
per concrete type". That is not overloading and needs no type variables:
|
||||||
|
there is no dispatch at run time and no user-supplied printer to pick
|
||||||
|
between. The walk itself is render.ml, shared with the REPL, which is what
|
||||||
|
stops the two from drifting apart.
|
||||||
|
|
||||||
|
[min]/[max]/[zeroed] above dispatch on the resolved argument type the same
|
||||||
|
way. The slots the slice arm needs come out of the frame of whatever
|
||||||
|
function this call is written in, via [fresh_slot] — allocated once per
|
||||||
|
call site, at check time, not once per iteration of a loop around it.
|
||||||
|
|
||||||
|
A string prints raw here and quoted inside a structure. Those are not in
|
||||||
|
conflict: (println "hello") has to print hello or it is useless, and
|
||||||
|
(println b) where b has a string field has to quote it or the field
|
||||||
|
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;
|
||||||
|
let a = check ctx (List.hd args) in
|
||||||
|
let bslice = Types.Slice (Types.Int Types.U8) in
|
||||||
|
let write x = mk loc Types.Unit (Tast.Prim (Tast.WriteStdout, [ x ])) in
|
||||||
|
let conv pr x = mk loc bslice (Tast.Prim (pr, [ x ])) in
|
||||||
|
let emitter : Render.emitter =
|
||||||
|
{ Render.ebytes = write;
|
||||||
|
estr = (fun x -> write (conv Tast.EscapeBytes x));
|
||||||
|
ei64 = (fun x -> write (conv Tast.I64ToBytes x));
|
||||||
|
eu64 = (fun x -> write (conv Tast.U64ToBytes x));
|
||||||
|
ef64 = (fun x -> write (conv Tast.F64ToBytes x)) }
|
||||||
|
in
|
||||||
|
let rc =
|
||||||
|
{ Render.structs =
|
||||||
|
Hashtbl.fold (fun _ v acc -> v :: acc) ctx.env.structs [];
|
||||||
|
enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) ctx.env.enums [];
|
||||||
|
emit = emitter;
|
||||||
|
alloc = (fun ty -> fresh_slot ctx ty) }
|
||||||
|
in
|
||||||
|
let parts =
|
||||||
|
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
|
||||||
|
let nl =
|
||||||
|
if String.equal name "println" then
|
||||||
|
[ write
|
||||||
|
(mk loc bslice
|
||||||
|
(Tast.Prim (Tast.Bytes, [ mk loc Types.String (Tast.Str "\n") ])))
|
||||||
|
]
|
||||||
|
else []
|
||||||
|
in
|
||||||
|
expect loc ~want (mk loc Types.Unit (Tast.Do (parts @ nl)))
|
||||||
| "exit" ->
|
| "exit" ->
|
||||||
arity loc name 1 args;
|
arity loc name 1 args;
|
||||||
prim Tast.Exit Types.Never [ check ctx ~want:index_ty (List.hd args) ]
|
prim Tast.Exit Types.Never [ check ctx ~want:index_ty (List.hd args) ]
|
||||||
|
|||||||
23
lib/emit.ml
23
lib/emit.ml
@ -668,12 +668,17 @@ and addr f (e : Tast.expr) : string =
|
|||||||
|
|
||||||
and field_addr f (target : Tast.expr) i =
|
and field_addr f (target : Tast.expr) i =
|
||||||
let base = addr f target in
|
let base = addr f target in
|
||||||
let sn = match target.Tast.ty with
|
(* An Option is { i8, T } and has no declared name to gep through, so its
|
||||||
| Types.Named n -> n
|
layout is spelled out instead. Nothing in the surface language reaches a
|
||||||
|
field of one -- [match] and [some] are how an Option is opened -- but the
|
||||||
|
structural printer does, to read the tag without unwrapping a None. *)
|
||||||
|
let sty = match target.Tast.ty with
|
||||||
|
| Types.Named n -> sname n
|
||||||
|
| Types.Option _ as t -> ll t
|
||||||
| t -> failwith ("field of " ^ Types.to_string t)
|
| t -> failwith ("field of " ^ Types.to_string t)
|
||||||
in
|
in
|
||||||
let p = fresh f in
|
let p = fresh f in
|
||||||
ins f "%s = getelementptr inbounds %s, ptr %s, i32 0, i32 %d" p (sname sn) base i;
|
ins f "%s = getelementptr inbounds %s, ptr %s, i32 0, i32 %d" p sty base i;
|
||||||
p
|
p
|
||||||
|
|
||||||
(* One index per dimension, so [(at grid row col)] is two geps. Indices are
|
(* One index per dimension, so [(at grid row col)] is two geps. Indices are
|
||||||
@ -1207,6 +1212,8 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
|
|||||||
| Tast.BytesToI64, [ x ] -> shim_in f "@flan_bytes_to_i64" "i64" x
|
| Tast.BytesToI64, [ x ] -> shim_in f "@flan_bytes_to_i64" "i64" x
|
||||||
| Tast.F64ToBytes, [ x ] -> shim_out f "@flan_f64_to_bytes" x
|
| Tast.F64ToBytes, [ x ] -> shim_out f "@flan_f64_to_bytes" x
|
||||||
| Tast.I64ToBytes, [ x ] -> shim_out f "@flan_i64_to_bytes" x
|
| Tast.I64ToBytes, [ x ] -> shim_out f "@flan_i64_to_bytes" x
|
||||||
|
| Tast.U64ToBytes, [ x ] -> shim_out f "@flan_u64_to_bytes" x
|
||||||
|
| Tast.EscapeBytes, [ x ] -> shim_in_out f "@flan_escape_bytes" x
|
||||||
| Tast.WriteStdout, [ x ] ->
|
| Tast.WriteStdout, [ x ] ->
|
||||||
let p, n = explode f x in
|
let p, n = explode f x in
|
||||||
ins f "call void @flan_write_stdout(ptr %s, i64 %s)" p n;
|
ins f "call void @flan_write_stdout(ptr %s, i64 %s)" p n;
|
||||||
@ -1244,6 +1251,14 @@ and shim_out f name (x : Tast.expr) =
|
|||||||
ins f "call void %s(%s %s, ptr %s)" name (ll x.Tast.ty) v tmp;
|
ins f "call void %s(%s %s, ptr %s)" name (ll x.Tast.ty) v tmp;
|
||||||
load f tmp (Types.Slice (Types.Int Types.U8))
|
load f tmp (Types.Slice (Types.Int Types.U8))
|
||||||
|
|
||||||
|
(* Slice in, slice out: [shim_in] returns a scalar and [shim_out] takes one, so
|
||||||
|
a shim that transforms bytes into bytes is neither. *)
|
||||||
|
and shim_in_out f name (x : Tast.expr) =
|
||||||
|
let p, n = explode f x in
|
||||||
|
let tmp = alloca f (Types.Slice (Types.Int Types.U8)) in
|
||||||
|
ins f "call void %s(ptr %s, i64 %s, ptr %s)" name p n tmp;
|
||||||
|
load f tmp (Types.Slice (Types.Int Types.U8))
|
||||||
|
|
||||||
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
|
||||||
(* An enum is an i32 at run time and its own type only in the checker, so a
|
(* An enum is an i32 at run time and its own type only in the checker, so a
|
||||||
@ -1509,6 +1524,8 @@ declare double @flan_bytes_to_f64(ptr, i64)
|
|||||||
declare i64 @flan_bytes_to_i64(ptr, i64)
|
declare i64 @flan_bytes_to_i64(ptr, i64)
|
||||||
declare void @flan_f64_to_bytes(double, ptr)
|
declare void @flan_f64_to_bytes(double, ptr)
|
||||||
declare void @flan_i64_to_bytes(i64, ptr)
|
declare void @flan_i64_to_bytes(i64, ptr)
|
||||||
|
declare void @flan_u64_to_bytes(i64, ptr)
|
||||||
|
declare void @flan_escape_bytes(ptr, i64, ptr)
|
||||||
declare void @flan_handler_push(ptr)
|
declare void @flan_handler_push(ptr)
|
||||||
declare void @flan_handler_pop(ptr)
|
declare void @flan_handler_pop(ptr)
|
||||||
declare void @flan_signal(i32, ptr, ptr)
|
declare void @flan_signal(i32, ptr, ptr)
|
||||||
|
|||||||
@ -9,9 +9,20 @@
|
|||||||
loader yet; at milestone 3 it becomes an ordinary [core:] package and this
|
loader yet; at milestone 3 it becomes an ordinary [core:] package and this
|
||||||
module goes away. The acceptance programs may call anything defined here.
|
module goes away. The acceptance programs may call anything defined here.
|
||||||
|
|
||||||
No overloading: [print-f64] and [print-str] name the type, because
|
[println] is not here and is not a function: it is compiler-provided and
|
||||||
compile-time overloading before the checker is stable is how a small
|
structural, a walk over the concrete type at the call site (check.ml, and
|
||||||
language stops being one. A single [println] is milestone 5. *)
|
the walk itself in render.ml). 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 said a single [println] had
|
||||||
|
to wait for generics; it did not.
|
||||||
|
|
||||||
|
The [print-*] functions stay, and not as compatibility. They print without
|
||||||
|
a newline and name their type at the call site, which is what a loop that
|
||||||
|
prints elements separated by spaces wants -- see [show] in
|
||||||
|
test/programs/slices.flan. [println] cannot express that, and [print] is
|
||||||
|
structural where these are not: [(print-str s)] is the raw bytes, whereas
|
||||||
|
[(print s)] is the same walk [println] uses. *)
|
||||||
|
|
||||||
let source = {flan|
|
let source = {flan|
|
||||||
(defn print-bytes [b [u8]]
|
(defn print-bytes [b [u8]]
|
||||||
|
|||||||
201
lib/render.ml
Normal file
201
lib/render.ml
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
(** The structural printer: a compile-time walk over a [Tast] type that emits
|
||||||
|
the calls which print a value of it.
|
||||||
|
|
||||||
|
It lives apart from its two callers because there are two, and they differ
|
||||||
|
in exactly one thing: where the pieces go. The REPL sends them to
|
||||||
|
[flan_dev_emit] ([session.ml]); [println] sends them to stdout
|
||||||
|
([check.ml]). Everything else — which arm a type takes, how an enum
|
||||||
|
recovers its member names, the depth and span caps — has to be the same in
|
||||||
|
both, and the way to make it the same is to have one copy.
|
||||||
|
|
||||||
|
Why the walk is at compile time at all: a Flan value carries no header, so
|
||||||
|
nothing at run time could say what it is. The compiler knows the type and
|
||||||
|
renders it there. And why it emits piecewise rather than building a string:
|
||||||
|
a struct is its fields with punctuation between them, and concatenating
|
||||||
|
that would need an allocator the language does not have.
|
||||||
|
|
||||||
|
The emitter is five functions rather than five names because the two sides
|
||||||
|
are not both extern calls. The REPL's are ([flan_dev_emit_i64] takes an
|
||||||
|
i64); stdout's compose a conversion with a write — [(write-stdout
|
||||||
|
(i64->bytes x))] — and a name alone cannot say that. *)
|
||||||
|
|
||||||
|
(* Each takes a value of the type its field is named for and returns a Unit
|
||||||
|
expression that prints it. [estr] is handed a [u8] slice and is expected to
|
||||||
|
quote and escape it: it is the *nested* string case, the one inside a struct
|
||||||
|
or an array, where an unquoted run of bytes could not be told from the
|
||||||
|
punctuation around it. A caller that wants a string printed raw does not go
|
||||||
|
through the walk at all. *)
|
||||||
|
type emitter = {
|
||||||
|
ebytes : Tast.expr -> Tast.expr; (* [u8], verbatim: punctuation and literals *)
|
||||||
|
estr : Tast.expr -> Tast.expr; (* [u8], quoted and escaped *)
|
||||||
|
ei64 : Tast.expr -> Tast.expr;
|
||||||
|
eu64 : Tast.expr -> Tast.expr;
|
||||||
|
ef64 : Tast.expr -> Tast.expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ctx = {
|
||||||
|
structs : Tast.structure list;
|
||||||
|
enums : (string * (string * int64) list) list;
|
||||||
|
emit : emitter;
|
||||||
|
(* A slot in the *caller's* frame. Only the slice arm needs one, and it needs
|
||||||
|
two: the slice itself, so the expression it came from is evaluated once
|
||||||
|
rather than once per element, and the loop counter. Who owns the frame
|
||||||
|
differs — the REPL's is a thunk it is building, [println]'s is the user
|
||||||
|
function being checked — so allocating one is the caller's to do. *)
|
||||||
|
alloc : Types.t -> int;
|
||||||
|
}
|
||||||
|
|
||||||
|
(* 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
|
||||||
|
|
||||||
|
let fail = Loc.fail
|
||||||
|
|
||||||
|
let rec render c depth (e : Tast.expr) : Tast.expr list =
|
||||||
|
let loc = e.Tast.loc in
|
||||||
|
let unit_ e = { Tast.e; ty = Types.Unit; loc } in
|
||||||
|
let cast t x = { Tast.e = Tast.Prim (Tast.Cast t, [ x ]); ty = t; loc } in
|
||||||
|
let bytes_of s =
|
||||||
|
{ 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 = c.emit.ebytes (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
|
||||||
|
| Types.Int Types.U64 -> [ c.emit.eu64 e ]
|
||||||
|
| Types.Int _ -> [ c.emit.ei64 (cast (Types.Int Types.I64) e) ]
|
||||||
|
| Types.Float _ -> [ c.emit.ef64 (cast (Types.Float Types.F64) e) ]
|
||||||
|
| Types.Bool ->
|
||||||
|
[ unit_ (Tast.If (e, lit "true", lit "false")) ]
|
||||||
|
(* Evaluated *and then* reported. A Unit expression is almost always a call
|
||||||
|
made for its effect — (print-line "x") is the REPL's most ordinary
|
||||||
|
input — so emitting the literal without running it would make the prompt
|
||||||
|
answer () while nothing happened. *)
|
||||||
|
| Types.Unit -> [ e; lit "()" ]
|
||||||
|
| Types.String ->
|
||||||
|
[ c.emit.estr
|
||||||
|
{ 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) -> [ c.emit.estr 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 = c.emit.ei64 (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 = c.alloc e.Tast.ty and iv = c.alloc (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 ->
|
||||||
|
fail loc "no printer for %s" (Types.to_string t)
|
||||||
196
lib/session.ml
196
lib/session.ml
@ -398,174 +398,18 @@ let externs : Tast.extern list =
|
|||||||
{ Tast.ename = "flan/dev-end"; esym = "flan_dev_result_end";
|
{ Tast.ename = "flan/dev-end"; esym = "flan_dev_result_end";
|
||||||
eparams = []; eret = Types.Unit } ]
|
eparams = []; eret = Types.Unit } ]
|
||||||
|
|
||||||
(* Two separate limits, easily conflated. [depth] and [span] bound the *walk*,
|
(* The REPL's emitter. Each piece is one extern call: the dev runtime already
|
||||||
so a big fixed array or a self-containing struct cannot turn one expression
|
has a renderer per scalar, and [flan_dev_emit_str] already quotes and
|
||||||
into a module with ten thousand render sites in it. How much text actually
|
escapes. See render.ml for what the five are and why they are functions. *)
|
||||||
comes out is bounded in the runtime instead, once, for every renderer. *)
|
let dev_emitter : Render.emitter =
|
||||||
let max_depth = 4
|
let call em (x : Tast.expr) : Tast.expr =
|
||||||
let max_span = 8
|
{ Tast.e = Tast.Call (em.ename, [ x ]); ty = Types.Unit; loc = x.Tast.loc }
|
||||||
|
|
||||||
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 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 bytes_of s =
|
|
||||||
{ Tast.e = Tast.Prim (Tast.Bytes, [ { Tast.e = Tast.Str s; ty = Types.String; loc } ]);
|
|
||||||
ty = Types.Slice (Types.Int Types.U8); loc }
|
|
||||||
in
|
in
|
||||||
let lit s = call emit_bytes (bytes_of s) in
|
{ Render.ebytes = call emit_bytes;
|
||||||
let int64 n = { Tast.e = Tast.Int (n, Types.I64); ty = Types.Int Types.I64; loc } in
|
estr = call emit_str;
|
||||||
let i32 n =
|
ei64 = call emit_i64;
|
||||||
{ Tast.e = Tast.Int (Int64.of_int n, Types.I32); ty = Types.Int Types.I32; loc }
|
eu64 = call emit_u64;
|
||||||
in
|
ef64 = call emit_f64 }
|
||||||
let do_ xs = unit_ (Tast.Do xs) in
|
|
||||||
if depth > max_depth then [ lit "..." ]
|
|
||||||
else
|
|
||||||
match e.Tast.ty with
|
|
||||||
| Types.Int Types.U64 -> [ call emit_u64 e ]
|
|
||||||
| Types.Int _ -> [ call emit_i64 (cast (Types.Int Types.I64) e) ]
|
|
||||||
| Types.Float _ -> [ call emit_f64 (cast (Types.Float Types.F64) e) ]
|
|
||||||
| Types.Bool ->
|
|
||||||
[ unit_ (Tast.If (e, lit "true", lit "false")) ]
|
|
||||||
(* Evaluated *and then* reported. A Unit expression is almost always a call
|
|
||||||
made for its effect — (print-line "x") is the REPL's most ordinary
|
|
||||||
input — so emitting the literal without running it would make the prompt
|
|
||||||
answer () while nothing happened. *)
|
|
||||||
| Types.Unit -> [ e; lit "()" ]
|
|
||||||
| Types.String ->
|
|
||||||
[ call emit_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 ->
|
|
||||||
fail loc "no printer for %s" (Types.to_string t)
|
|
||||||
|
|
||||||
let eval_expr ?(origin = "<eval>") t src : change =
|
let eval_expr ?(origin = "<eval>") t src : change =
|
||||||
let form =
|
let form =
|
||||||
@ -575,21 +419,31 @@ let eval_expr ?(origin = "<eval>") t src : change =
|
|||||||
| _ :: f :: _ -> fail f.Form.loc "one expression at a time"
|
| _ :: f :: _ -> fail f.Form.loc "one expression at a time"
|
||||||
in
|
in
|
||||||
let checked, base = Check.expression t.env (Parse.expr form) in
|
let checked, base = Check.expression t.env (Parse.expr form) in
|
||||||
|
(* The thunk's frame starts at whatever [Check.expression] needed and grows
|
||||||
|
as the walk finds slices in it, so the slots the renderer asks for are
|
||||||
|
appended past [base] and collected here to size the frame below. *)
|
||||||
|
let extra = ref [] and nslots = ref (Array.length base) in
|
||||||
let c =
|
let c =
|
||||||
{ structs = t.program.Tast.structs;
|
{ Render.structs = t.program.Tast.structs;
|
||||||
enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums [];
|
enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums [];
|
||||||
slots = []; nslots = Array.length base }
|
emit = dev_emitter;
|
||||||
|
alloc = (fun ty ->
|
||||||
|
let i = !nslots in
|
||||||
|
incr nslots;
|
||||||
|
extra := ty :: !extra;
|
||||||
|
i) }
|
||||||
in
|
in
|
||||||
let loc = checked.Tast.loc in
|
let loc = checked.Tast.loc in
|
||||||
let nullary n = { Tast.e = Tast.Call (n, []); ty = Types.Unit; loc } in
|
let nullary n = { Tast.e = Tast.Call (n, []); ty = Types.Unit; loc } in
|
||||||
let body =
|
let body =
|
||||||
(nullary "flan/dev-begin" :: render c 0 checked) @ [ nullary "flan/dev-end" ]
|
(nullary "flan/dev-begin" :: Render.render c 0 checked)
|
||||||
|
@ [ nullary "flan/dev-end" ]
|
||||||
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 = []; ret = Types.Unit; body; fdefers = []; fparent = None; floc = loc;
|
{ Tast.name; params = []; ret = Types.Unit; body; fdefers = []; fparent = None; floc = loc;
|
||||||
slots = Array.append base (Array.of_list (List.rev c.slots)) }
|
slots = Array.append base (Array.of_list (List.rev !extra)) }
|
||||||
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
|
||||||
|
|||||||
@ -28,6 +28,10 @@ type prim =
|
|||||||
*text*: bytes->f64 parses "12.5", f64->bytes renders it — that is what
|
*text*: bytes->f64 parses "12.5", f64->bytes renders it — that is what
|
||||||
calc-me's tokenizer and the prelude's printers each need. *)
|
calc-me's tokenizer and the prelude's printers each need. *)
|
||||||
| Bytes | BytesToF64 | BytesToI64 | F64ToBytes | I64ToBytes
|
| Bytes | BytesToF64 | BytesToI64 | F64ToBytes | I64ToBytes
|
||||||
|
(* No surface name: the structural printer is the only thing that builds
|
||||||
|
these. U64ToBytes because u64 is not i64 with a flag, EscapeBytes for a
|
||||||
|
string nested inside a printed structure. *)
|
||||||
|
| U64ToBytes | EscapeBytes
|
||||||
| WriteStdout | Exit | Argv
|
| WriteStdout | Exit | Argv
|
||||||
| Cast of Types.t
|
| Cast of Types.t
|
||||||
|
|
||||||
|
|||||||
14
plan.org
14
plan.org
@ -104,7 +104,7 @@ world.
|
|||||||
is type-directed: ~(defvar enemies (Map string Enemy) (map-new))~. ~get~
|
is type-directed: ~(defvar enemies (Map string Enemy) (map-new))~. ~get~
|
||||||
returns ~(Option V)~; ~put~ is the `Unit`-returning upsert. See
|
returns ~(Option V)~; ~put~ is the `Unit`-returning upsert. See
|
||||||
spec-memory.md for the deferred move-aware operations.
|
spec-memory.md for the deferred move-aware operations.
|
||||||
- Operations: ~get~, ~put~, ~remove~, ~push~, ~pop~, ~nth~, ~len~, ~update~.
|
- Operations: ~get~, ~put~, ~remove~, ~push~, ~pop~, ~at~, ~len~, ~update~.
|
||||||
Copying is explicit: ~(clone m)~, and owning containers move rather than copy on
|
Copying is explicit: ~(clone m)~, and owning containers move rather than copy on
|
||||||
assignment. No ~!~ convention — nothing is immutable, so it
|
assignment. No ~!~ convention — nothing is immutable, so it
|
||||||
would carry no information. No ~assoc~; it only existed as the copy-returning form.
|
would carry no information. No ~assoc~; it only existed as the copy-returning form.
|
||||||
@ -381,10 +381,14 @@ wasm32 target cheap, because a primitive is the only thing implemented twice.
|
|||||||
| arithmetic, comparison, casts | per machine type |
|
| arithmetic, comparison, casts | per machine type |
|
||||||
|
|
||||||
Printing is *not* a primitive. ~print-str~, ~print-f64~ and friends are Flan
|
Printing is *not* a primitive. ~print-str~, ~print-f64~ and friends are Flan
|
||||||
functions over ~write-stdout~. At milestone 5, compiler-provided ~println~ emits
|
functions over ~write-stdout~. ~println~ and ~print~ are compiler-provided: the
|
||||||
or selects a structural printer for every concrete type, including generic
|
checker walks the concrete type at the call site and emits the printer for it
|
||||||
instantiations. This is intentionally not user-defined overload resolution:
|
(lib/render.ml, shared with the REPL's ~C-x C-e~). This is intentionally not
|
||||||
ordinary values remain untagged, while ~any~ and ~Error~ carry the metadata their
|
user-defined overload resolution — ordinary values remain untagged, and there
|
||||||
|
is nothing to dispatch on at run time — which is why it did not have to wait
|
||||||
|
for generics as this line once said it would. Generic instantiations are
|
||||||
|
concrete types by the time the checker sees them, so they need nothing further;
|
||||||
|
~any~ and ~Error~ are the remaining case, and they carry the metadata their
|
||||||
dynamic printers need.
|
dynamic printers need.
|
||||||
|
|
||||||
*Entry point.* ~(defn main [args [string]] i32)~. Both the parameter and the
|
*Entry point.* ~(defn main [args [string]] i32)~. Both the parameter and the
|
||||||
|
|||||||
@ -191,6 +191,67 @@ void flan_i64_to_bytes(int64_t x, flan_slice *out) {
|
|||||||
out->len = n < 0 ? 0 : (int64_t)n;
|
out->len = n < 0 ? 0 : (int64_t)n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* u64 is not i64 with a flag: 0xFFFFFFFFFFFFFFFF is 18446744073709551615 and
|
||||||
|
* not -1, and routing it through the signed printer is the only way println
|
||||||
|
* could disagree with the REPL about a value both can hold. Hence a second
|
||||||
|
* shim rather than a cast at the call site. */
|
||||||
|
void flan_u64_to_bytes(uint64_t x, flan_slice *out) {
|
||||||
|
int n = snprintf(scratch, SCRATCH, "%llu", (unsigned long long)x);
|
||||||
|
out->ptr = (const uint8_t *)scratch;
|
||||||
|
out->len = n < 0 ? 0 : (int64_t)n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A string *inside* a printed structure, quoted and escaped, so that the run
|
||||||
|
* of bytes can be told from the punctuation around it — (S {:name "a b"}) has
|
||||||
|
* two fields if the quotes are missing and one if they are there.
|
||||||
|
*
|
||||||
|
* This is the same escape table as flan_dev_emit_str in flan_dev.c, and
|
||||||
|
* deliberately so: the REPL and println must not disagree about what a struct
|
||||||
|
* looks like. It cannot be the *same function* because the dev one streams
|
||||||
|
* into the result buffer and this one has to hand back a slice; if either
|
||||||
|
* table changes, change both.
|
||||||
|
*
|
||||||
|
* Its own buffer, not `scratch`: escaping is the one conversion whose output
|
||||||
|
* is not a bounded handful of characters. Over-long input is truncated with an
|
||||||
|
* ellipsis rather than silently cut, because a value that prints as a shorter
|
||||||
|
* value is the failure nobody notices. */
|
||||||
|
#define ESCAPE_MAX 1024
|
||||||
|
static char escaped[ESCAPE_MAX];
|
||||||
|
|
||||||
|
void flan_escape_bytes(const uint8_t *p, int64_t n, flan_slice *out) {
|
||||||
|
size_t len = n < 0 ? 0 : (size_t)n;
|
||||||
|
size_t w = 0;
|
||||||
|
int cut = 0;
|
||||||
|
/* The guard reserves 9 bytes, and all 9 are spoken for: 4 for the longest
|
||||||
|
* single escape (\xNN), 3 for the ellipsis, 1 for the closing quote, 1
|
||||||
|
* spare. So the loop never writes a partial escape and the three writes
|
||||||
|
* after it never need a bound of their own. Swept over every length to 1300
|
||||||
|
* against \x01, '"', '\\' and 'a': the worst output is 1021 of 1024. If the
|
||||||
|
* escape table ever grows a longer form, this 9 grows with it. */
|
||||||
|
escaped[w++] = '"';
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
if (w + 5 + 4 >= ESCAPE_MAX) { cut = 1; break; }
|
||||||
|
unsigned char c = p[i];
|
||||||
|
switch (c) {
|
||||||
|
case '"': escaped[w++] = '\\'; escaped[w++] = '"'; break;
|
||||||
|
case '\\': escaped[w++] = '\\'; escaped[w++] = '\\'; break;
|
||||||
|
case '\n': escaped[w++] = '\\'; escaped[w++] = 'n'; break;
|
||||||
|
case '\t': escaped[w++] = '\\'; escaped[w++] = 't'; break;
|
||||||
|
case '\r': escaped[w++] = '\\'; escaped[w++] = 'r'; break;
|
||||||
|
default:
|
||||||
|
if (c < 0x20) {
|
||||||
|
w += (size_t)snprintf(escaped + w, 5, "\\x%02x", c);
|
||||||
|
} else {
|
||||||
|
escaped[w++] = (char)c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cut) { escaped[w++] = '.'; escaped[w++] = '.'; escaped[w++] = '.'; }
|
||||||
|
escaped[w++] = '"';
|
||||||
|
out->ptr = (const uint8_t *)escaped;
|
||||||
|
out->len = (int64_t)w;
|
||||||
|
}
|
||||||
|
|
||||||
/* Bounds failures. The emitted code branches here and then falls off the end
|
/* Bounds failures. The emitted code branches here and then falls off the end
|
||||||
* with `unreachable`, so these must not return — the same explicit shape as
|
* with `unreachable`, so these must not return — the same explicit shape as
|
||||||
* every other non-local exit, which is what keeps wasm32 free of unwinding.
|
* every other non-local exit, which is what keeps wasm32 free of unwinding.
|
||||||
|
|||||||
@ -82,7 +82,7 @@
|
|||||||
(>= c 0) (< c (- cols 1))
|
(>= c 0) (< c (- cols 1))
|
||||||
(empty-at? r c)
|
(empty-at? r c)
|
||||||
(< (rand-f32) 0.5))
|
(< (rand-f32) 0.5))
|
||||||
(set (at grid r c) (nth colors current-color))
|
(set (at grid r c) (at colors current-color))
|
||||||
(set (at velocity r c) 1.0)))))))
|
(set (at velocity r c) 1.0)))))))
|
||||||
|
|
||||||
(defn move-grain [from-row i32 from-col i32
|
(defn move-grain [from-row i32 from-col i32
|
||||||
@ -198,7 +198,7 @@
|
|||||||
rl/white)
|
rl/white)
|
||||||
(rl/draw-texture brush 20 50 rl/white)
|
(rl/draw-texture brush 20 50 rl/white)
|
||||||
(rl/draw-texture-v brush (rl/Vector2 {:x 44.0 :y 50.0})
|
(rl/draw-texture-v brush (rl/Vector2 {:x 44.0 :y 50.0})
|
||||||
(rl/get-color (nth colors current-color)))
|
(rl/get-color (at colors current-color)))
|
||||||
(rl/draw-texture-ex brush (rl/Vector2 {:x 72.0 :y 46.0}) 0.0 2.0 rl/white)))
|
(rl/draw-texture-ex brush (rl/Vector2 {:x 72.0 :y 46.0}) 0.0 2.0 rl/white)))
|
||||||
;; The mirrored one beside them, scaled up so the flip is visible rather
|
;; The mirrored one beside them, scaled up so the flip is visible rather
|
||||||
;; than eight pixels wide. If the two badges look the same, either the flip
|
;; than eight pixels wide. If the two badges look the same, either the flip
|
||||||
@ -449,7 +449,7 @@
|
|||||||
;; and does not.
|
;; and does not.
|
||||||
(defn draw-world-cursor []
|
(defn draw-world-cursor []
|
||||||
(let [p (rl/get-screen-to-world-2d (rl/get-mouse-position) view)
|
(let [p (rl/get-screen-to-world-2d (rl/get-mouse-position) view)
|
||||||
tint (rl/get-color (nth colors current-color))
|
tint (rl/get-color (at colors current-color))
|
||||||
x (.x p)
|
x (.x p)
|
||||||
y (.y p)
|
y (.y p)
|
||||||
r (f32 (* brush-size cell-size))]
|
r (f32 (* brush-size cell-size))]
|
||||||
@ -528,7 +528,7 @@
|
|||||||
sh (- (rl/get-screen-height) 40)]
|
sh (- (rl/get-screen-height) 40)]
|
||||||
(dotimes [i (len colors)]
|
(dotimes [i (len colors)]
|
||||||
(let [cx (- sw (* (- (len colors) (+ i 1)) 46))
|
(let [cx (- sw (* (- (len colors) (+ i 1)) 46))
|
||||||
c (rl/get-color (nth colors i))]
|
c (rl/get-color (at colors i))]
|
||||||
(rl/draw-circle cx sh (f32 16.0) c)
|
(rl/draw-circle cx sh (f32 16.0) c)
|
||||||
(when (= i current-color)
|
(when (= i current-color)
|
||||||
(rl/draw-circle-lines cx sh (f32 22.0) rl/white))))
|
(rl/draw-circle-lines cx sh (f32 22.0) rl/white))))
|
||||||
|
|||||||
@ -58,7 +58,7 @@
|
|||||||
;; this frame (spec-memory.md, non-escaping fn).
|
;; this frame (spec-memory.md, non-escaping fn).
|
||||||
(defn largest [xs [a] gt (Fn [a a] bool)] (Option a)
|
(defn largest [xs [a] gt (Fn [a a] bool)] (Option a)
|
||||||
(if (> (len xs) 0)
|
(if (> (len xs) 0)
|
||||||
(Some (reduce (fn [x y] (if (gt x y) x y)) (nth xs 0) xs))
|
(Some (reduce (fn [x y] (if (gt x y) x y)) (at xs 0) xs))
|
||||||
None))
|
None))
|
||||||
|
|
||||||
;; (largest hps >) — `>` at i32 is an ordinary function value
|
;; (largest hps >) — `>` at i32 is an ordinary function value
|
||||||
|
|||||||
11
test/programs/nth-gone.flan
Normal file
11
test/programs/nth-gone.flan
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
;;;; nth was an alias of at, and an asymmetric one: the checker accepted it as
|
||||||
|
;;;; a read, but parse.ml and place_of_expr both match only [at], so
|
||||||
|
;;;; (set (nth a i) x) and (addr (nth a i)) were refused while the [at] forms
|
||||||
|
;;;; worked. Two names documented as identical that disagree about writing are
|
||||||
|
;;;; worse than one name, so nth is gone and this pins the removal: it must
|
||||||
|
;;;; fail as an unknown name, not quietly resolve to at again.
|
||||||
|
|
||||||
|
(defvar a [4 i32])
|
||||||
|
|
||||||
|
(defn main [] i32
|
||||||
|
(nth a 0))
|
||||||
124
test/programs/println.flan
Normal file
124
test/programs/println.flan
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
;;;; println, one row per arm of the structural printer.
|
||||||
|
;;;;
|
||||||
|
;;;; The walk in render.ml is shared with the REPL, but until now its only
|
||||||
|
;;;; coverage was the REPL tests -- and those run a dev build, where the pieces
|
||||||
|
;;;; go to flan_dev_emit. println is the same walk with the other emitter, in
|
||||||
|
;;;; an ordinary build, so every arm needs saying again here: the two emitters
|
||||||
|
;;;; are the one thing the shared module does *not* make identical.
|
||||||
|
;;;;
|
||||||
|
;;;; Run at -O0 as well as -O2. The slice arm emits a loop over slots taken
|
||||||
|
;;;; from the enclosing function's frame, and mem2reg is exactly the pass that
|
||||||
|
;;;; would launder a slot mistake into working code.
|
||||||
|
|
||||||
|
(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])
|
||||||
|
|
||||||
|
;; Five deep, so the walk hits max_depth (4) and prints "..." rather than
|
||||||
|
;; descending forever. A self-containing struct is the case this cap exists
|
||||||
|
;; for; five nested ones are the same shape without needing a pointer.
|
||||||
|
(defstruct D5 [n i32])
|
||||||
|
(defstruct D4 [d D5])
|
||||||
|
(defstruct D3 [d D4])
|
||||||
|
(defstruct D2 [d D3])
|
||||||
|
(defstruct D1 [d D2])
|
||||||
|
|
||||||
|
(defvar b Blob)
|
||||||
|
(defvar col Colour)
|
||||||
|
(defvar big u64)
|
||||||
|
(defvar small u64)
|
||||||
|
(defvar arr [4 i32])
|
||||||
|
(defvar wide [10 i32])
|
||||||
|
(defvar deep D1)
|
||||||
|
(defvar n i32)
|
||||||
|
(defstruct Long [s string])
|
||||||
|
(defvar long-one Long)
|
||||||
|
|
||||||
|
(defn nothing [] )
|
||||||
|
|
||||||
|
(defn find-it [s [i32] k i32] (Option i32)
|
||||||
|
(dotimes [i (len s)]
|
||||||
|
(when (= (at s i) k) (return (Some i))))
|
||||||
|
None)
|
||||||
|
|
||||||
|
(defn main [] i32
|
||||||
|
;; A string at top level prints raw. Anywhere else it is quoted, which the
|
||||||
|
;; Blob row below shows -- the two are the same value printed two ways on
|
||||||
|
;; purpose, and that difference is the thing most likely to be "fixed".
|
||||||
|
(println "plain string")
|
||||||
|
(println (bytes "plain bytes"))
|
||||||
|
|
||||||
|
(println 42)
|
||||||
|
(println -7)
|
||||||
|
(set small 5)
|
||||||
|
(println small)
|
||||||
|
;; The u64 row. Through the signed printer this reads -1, which is the one
|
||||||
|
;; way println could disagree with the REPL about a value both can hold.
|
||||||
|
(set big 0xFFFFFFFFFFFFFFFF)
|
||||||
|
(println big)
|
||||||
|
|
||||||
|
(println 3.5)
|
||||||
|
(println -0.25)
|
||||||
|
|
||||||
|
(println true)
|
||||||
|
(println false)
|
||||||
|
|
||||||
|
;; Unit is evaluated and *then* reported: a Unit expression is a call made
|
||||||
|
;; for its effect, so emitting () without running it would be a lie.
|
||||||
|
(println (nothing))
|
||||||
|
|
||||||
|
(set col :green)
|
||||||
|
(println col)
|
||||||
|
;; red is 0, which is also the zero value, so this row says the chain of
|
||||||
|
;; comparisons reaches the *first* member and does not fall through to the
|
||||||
|
;; number it is erased to.
|
||||||
|
(set col :red)
|
||||||
|
(println col)
|
||||||
|
|
||||||
|
;; A pointer is its shape and is never followed -- the only thing that could
|
||||||
|
;; make this walk cycle.
|
||||||
|
(set n 3)
|
||||||
|
(println (addr n))
|
||||||
|
|
||||||
|
(println (find-it (slice wide 0 10) 0))
|
||||||
|
(println (find-it (slice wide 0 10) 99))
|
||||||
|
|
||||||
|
(set (.id b) 7)
|
||||||
|
(set (.name b) "sandy \"quoted\"")
|
||||||
|
(set (.x (.pos b)) 1.5)
|
||||||
|
(set (.y (.pos b)) -2.0)
|
||||||
|
(set (at (.tags b) 1) 42)
|
||||||
|
(println b)
|
||||||
|
|
||||||
|
(set (at arr 2) 9)
|
||||||
|
(println arr)
|
||||||
|
|
||||||
|
;; Ten elements against a span cap of eight: eight, then "...".
|
||||||
|
(set (at wide 9) 1)
|
||||||
|
(println wide)
|
||||||
|
|
||||||
|
;; A slice's length is not known until it runs, so this is the arm that
|
||||||
|
;; emits a loop rather than unrolling.
|
||||||
|
(println (slice arr 1 4))
|
||||||
|
|
||||||
|
(set (.n (.d (.d (.d (.d deep))))) 5)
|
||||||
|
(println deep)
|
||||||
|
|
||||||
|
;; Two slice printlns in one function, and one inside a loop: the slots the
|
||||||
|
;; loop needs are allocated per *call site* at check time, not per iteration.
|
||||||
|
(dotimes [i 2]
|
||||||
|
(println (slice arr 0 2)))
|
||||||
|
(println (slice arr 2 4))
|
||||||
|
|
||||||
|
;; A nested string longer than the escape buffer. Escaping is the one
|
||||||
|
;; conversion whose output is not a bounded handful of characters, so it
|
||||||
|
;; truncates -- with an ellipsis inside the quotes, because a value that
|
||||||
|
;; prints as a shorter value is the failure nobody notices.
|
||||||
|
(set (.s long-one) "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
|
||||||
|
(println long-one)
|
||||||
|
|
||||||
|
;; print is println without the newline.
|
||||||
|
(print "a")
|
||||||
|
(print "b")
|
||||||
|
(println "c")
|
||||||
|
0)
|
||||||
@ -127,6 +127,39 @@ let () =
|
|||||||
in
|
in
|
||||||
outputs "slice algorithms" "programs/slices.flan" slices_out;
|
outputs "slice algorithms" "programs/slices.flan" slices_out;
|
||||||
outputs ~opt:"-O0" "slice algorithms, -O0" "programs/slices.flan" slices_out;
|
outputs ~opt:"-O0" "slice algorithms, -O0" "programs/slices.flan" slices_out;
|
||||||
|
(* println, one row per arm of render.ml's walk. The walk is shared with
|
||||||
|
the REPL, but its only coverage was the REPL tests -- a dev build,
|
||||||
|
emitting to flan_dev_emit. This is the same walk with the other emitter
|
||||||
|
in an ordinary build, which is a path nothing else takes.
|
||||||
|
|
||||||
|
Three of these rows are load-bearing beyond "it prints something". The
|
||||||
|
u64 reads 18446744073709551615 and not -1, which is what a second shim
|
||||||
|
exists for. The Blob's name is quoted and escaped while the two strings
|
||||||
|
at the top are raw, which is the top-level/nested split and the thing
|
||||||
|
most likely to be "tidied up" into being wrong. And the Option rows are
|
||||||
|
an arm that never ran until now: a field of an Option had no gep in
|
||||||
|
emit.ml, so the REPL would have failed on one too.
|
||||||
|
|
||||||
|
At -O0 as well -- the slice arm allocates slots in the enclosing
|
||||||
|
function's frame and emits a loop, and mem2reg is the pass that would
|
||||||
|
hide a mistake in either. *)
|
||||||
|
let println_out =
|
||||||
|
"plain string\nplain bytes\n42\n-7\n5\n18446744073709551615\n3.5\n\
|
||||||
|
-0.25\ntrue\nfalse\n()\n:green\n:red\n<ptr>\n(some 0)\nnone\n\
|
||||||
|
(Blob {:id 7 :name \"sandy \\\"quoted\\\"\" :pos (V {:x 1.5 :y -2}) :tags [ 0 42 0]})\n\
|
||||||
|
[ 0 0 9 0]\n[ 0 0 0 0 0 0 0 0 ...]\n[ 0 9 0]\n\
|
||||||
|
(D1 {:d (D2 {:d (D3 {:d (D4 {:d (D5 {:n ...})})})})})\n[ 0 0]\n\
|
||||||
|
[ 0 0]\n[ 9 0]\n"
|
||||||
|
(* The escape buffer is 1024 and the input is 1100 x's, so this is the
|
||||||
|
truncation: the ellipsis goes *inside* the quotes, and the count is
|
||||||
|
spelled out rather than pasted so that a change to the buffer or to
|
||||||
|
the reserve shows up here as a number and not as a wall of x. *)
|
||||||
|
^ "(Long {:s \"" ^ String.make 1014 'x' ^ "...\"})\n"
|
||||||
|
^ "abc\n"
|
||||||
|
in
|
||||||
|
outputs "println, every arm" "programs/println.flan" println_out;
|
||||||
|
outputs ~opt:"-O0" "println, every arm, -O0" "programs/println.flan"
|
||||||
|
println_out;
|
||||||
(* The byte predicates, parse-i64, and the two number helpers. The refused
|
(* The byte predicates, parse-i64, and the two number helpers. The refused
|
||||||
parse-i64 cases are every shape strtoll answers 0 for — "", "abc",
|
parse-i64 cases are every shape strtoll answers 0 for — "", "abc",
|
||||||
"12x", "-", " 1" — so a None there is the whole reason the function is
|
"12x", "-", " 1" — so a None there is the whole reason the function is
|
||||||
@ -620,6 +653,11 @@ let () =
|
|||||||
"one directory is one set of names";
|
"one directory is one set of names";
|
||||||
refuses "two mains in one program" "programs/pkg-two-mains.flan"
|
refuses "two mains in one program" "programs/pkg-two-mains.flan"
|
||||||
"main is defined twice";
|
"main is defined twice";
|
||||||
|
(* nth is gone, not renamed: it has to fail as a name nobody defined. If it
|
||||||
|
were ever re-added as an alias of [at] it would have to be a place too,
|
||||||
|
and this row is what says so. *)
|
||||||
|
refuses "nth is not a name" "programs/nth-gone.flan"
|
||||||
|
"unknown function nth";
|
||||||
|
|
||||||
(* ── wasm32 (NEXT.md, deferred item 6) ──────────────────────────────
|
(* ── wasm32 (NEXT.md, deferred item 6) ──────────────────────────────
|
||||||
The second target, and the reason sand-headless imports no raylib. What
|
The second target, and the reason sand-headless imports no raylib. What
|
||||||
|
|||||||
@ -643,8 +643,9 @@ second
|
|||||||
|
|
||||||
<h2 id="arrays">Arrays and slices</h2>
|
<h2 id="arrays">Arrays and slices</h2>
|
||||||
|
|
||||||
<p><code>at</code> and <code>nth</code> are the same operation and take any number of
|
<p><code>at</code> indexes a fixed array or a slice, and takes any number of
|
||||||
indices, so <code>(at grid r c)</code> indexes a two-dimensional fixed array directly.
|
indices, so <code>(at grid r c)</code> indexes a two-dimensional fixed array directly.
|
||||||
|
It is a place: <code>(set (at grid r c) v)</code> and <code>(addr (at grid r c))</code> both work.
|
||||||
<code>len</code> works on a fixed array, a slice or a string.
|
<code>len</code> works on a fixed array, a slice or a string.
|
||||||
<code>(slice s lo hi)</code> takes a half-open range and never copies.</p>
|
<code>(slice s lo hi)</code> takes a half-open range and never copies.</p>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user