flan/test/test_repl.ml
Joseph Ferano 0f3b633449 The printed struct moves to the dot, with the reader that parses it
render.ml's output and emacs/flan-inspect.el's parser are the two ends of one
wire format, which is why the printer was left on the colon when the rest of
the corpus moved: shifting it alone would have broken inspection in the dev
loop without breaking a test that said so. They move together here.

The field list in the inspector is labelled with the dot too, which is the
spelling flan-inspect-step-expr already used to build `(.x b)' — the label and
the expression it stands for now read the same.

One case needed a guard the colon never did: `...' also begins with a dot and
is the renderer saying it stopped, not a field called `..'. A field name never
starts with a second dot, so one character of lookahead separates them.

The colon is not gone from the rendered grammar. An enum member is `:green' and
is a *value*, so the two are now told apart by the character alone, which is
the only thing that distinguishes them.

Also font lock, handed over with the same change: `:name' was the rule that
drew field labels, and with the colon belonging to keywords every label in the
corpus was left unfontified. `.name' is drawn as a constant, in both the places
it appears — the label in `{.x 1.0}' and the accessor in `(.x v)', which are
the same name.
2026-09-12 16:19:55 +07:00

159 lines
6.7 KiB
OCaml

(* C-x C-e: evaluating an expression inside a program that is running.
A different primitive from redefining a name, and the difference is the
whole test: there is no name to install a body into, so the expression is
compiled into a thunk with nowhere to be called from, the module says "run
this once", and the agent does — at a frame boundary, on the game thread.
Nothing is marshalled back. 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. That is the layout decision's bill, and it is why only the scalars
work so far.
The case that matters most is the same expression evaluated twice with
different answers: that is what says it read the live process's state rather
than a copy of it. *)
open Flan
(* The watchdog first: a hang is the one failure mode that reports
nothing at all. See watchdog.ml. *)
let () = Watchdog.arm ~seconds:600 "test_repl"
let failures = ref 0
let fail fmt = Printf.ksprintf (fun s -> incr failures; print_endline ("FAIL " ^ s)) fmt
let scratch = Filename.get_temp_dir_name ()
let tmp n = Filename.concat scratch ("flan-repl-" ^ n)
let rec await ?(ms = 8000) f =
if f () then true
else if ms <= 0 then false
else begin ignore (Unix.select [] [] [] 0.005); await ~ms:(ms - 5) f end
let rec connect ?(ms = 8000) path =
let s = Unix.socket Unix.PF_UNIX Unix.SOCK_STREAM 0 in
match Unix.connect s (Unix.ADDR_UNIX path) with
| () -> s
| exception Unix.Unix_error _ when ms > 0 ->
Unix.close s;
ignore (Unix.select [] [] [] 0.005);
connect ~ms:(ms - 5) path
let request fd sexp = Wire.send fd sexp; Wire.parse (Wire.recv fd)
let field r k = Wire.string_field r k
let status r = match field r "status" with Some s -> s | None -> "<none>"
let quote s =
let b = Buffer.create (String.length s + 8) in
Buffer.add_char b '"';
String.iter
(fun c ->
if c = '"' || c = '\\' then Buffer.add_char b '\\';
Buffer.add_char b c)
s;
Buffer.add_char b '"';
Buffer.contents b
let () =
match Sys.command "command -v clang > /dev/null 2>&1 && command -v llc > /dev/null 2>&1" with
| 0 ->
let sock = tmp "dev.sock" and out = tmp "prog.out" in
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ sock; out ];
let fd = Unix.openfile out [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
let flan = "../bin/main.exe" in
let pid =
Unix.create_process flan
[| flan; "dev"; "programs/printers.flan"; "-s"; sock |]
Unix.stdin fd Unix.stderr
in
Unix.close fd;
if not (await (fun () -> Sys.file_exists sock)) then fail "the daemon never listened"
else begin
let c = connect sock in
let evals code =
request c
(Printf.sprintf "(:op \"eval-expr\" :code %s :file \"/tmp/buf.flan\")"
(quote code))
in
let value name code expected =
let r = evals code in
match field r "value" with
| Some v when v = expected -> ()
| Some v -> fail "%s\n got: %S\n wanted: %S" name v expected
| None ->
fail "%s: %s" name (Option.value ~default:(status r) (field r "message"))
in
value "arithmetic" "(+ 1 2)" "3";
value "a comparison" "(< 1 2)" "true";
(* Quoted and escaped, in the runtime: a string whose content is not
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";
(* 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";
(* A Unit expression is almost always a call made for its effect, so it
has to be *evaluated* and then reported as (). Emitting the literal
without running it made the prompt answer while nothing happened. *)
value "a call made for its effect" "(println \"printed\")" "()";
(* The one that proves it ran inside the process: the program increments
[ticks] every frame, so two evaluations of it must disagree. A copy
of the program's state, or a value computed here, would not. *)
let read () =
match field (evals "ticks") "value" with
| Some v -> int_of_string_opt v
| None -> None
in
(match (read (), read ()) with
| Some a, Some b when b > a -> ()
| Some a, Some b ->
fail "ticks read %d then %d — the expression did not see the live \
program advancing" a b
| _ -> fail "ticks did not evaluate");
let refuses name code reason =
let r = evals code in
match field r "message" with
| Some m when
(let n = String.length reason and h = String.length m in
let rec go i = i + n <= h && (String.sub m i n = reason || go (i + 1)) in
go 0) -> ()
| Some m -> fail "%s said %S, wanted it to mention %S" name m reason
| None -> fail "%s was accepted" name
in
refuses "a declaration" "(defvar nope i64)" "";
refuses "an unknown name" "no-such-name" "unknown name";
(* And the session is untouched by all of it: an evaluation is not a
declaration, so nothing named eval/N accumulates in the program. *)
let r = request c "(:op \"describe\")" in
if status r <> "ok" then fail "describe after evaluating: %s" (status r);
ignore (request c "(:op \"close\")");
Unix.close c
end;
(try Unix.kill pid Sys.sigterm with Unix.Unix_error _ -> ());
(try ignore (Unix.waitpid [] pid) with Unix.Unix_error _ -> ());
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ sock; out ];
if !failures = 0 then print_endline "repl: all tests passed"
else begin
Printf.printf "\n%d failure(s)\n" !failures;
exit 1
end
| _ -> print_endline "repl: skipped (no clang or llc on PATH)"