A dyn value an expression answers is the reply's value, not the program's output
This commit is contained in:
parent
cf028e23ea
commit
aa5052e401
10
TODO.org
10
TODO.org
@ -1591,10 +1591,12 @@ The park used to drain the agent ring only when something had asked it to poll,
|
||||
and a plain redefine does not, so every generation ran one re-run later than
|
||||
whoever pressed the key expected. The drain happens in front of the exit now.
|
||||
|
||||
** TODO A dyn value from eval-expr never reaches the reply's value field
|
||||
It renders to the program's own stdout and arrives on a later reply's output
|
||||
instead. Where a dyn expression's value should surface is a question about the
|
||||
editor protocol.
|
||||
** DONE A dyn value from eval-expr never reaches the reply's value field
|
||||
CLOSED: [2026-09-25]
|
||||
The renderer's emitter has a dyn entry: =println= keeps =flan_dyn_print= to
|
||||
stdout, and the REPL's renders through =flan_dyn_emit_dev= into the value
|
||||
buffer, so a dyn answer is the reply's =:value= on both backends. A dyn text is
|
||||
quoted there as a typed string is. Rules out a dyn value arriving on =:output=.
|
||||
|
||||
** DONE Memory diagnostics on demand
|
||||
CLOSED: [2026-09-20]
|
||||
|
||||
@ -8618,7 +8618,8 @@ and named_call ?(qualified = false) ctx ~want loc name args =
|
||||
(Tast.Prim (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)) }
|
||||
ef64 = (fun x -> write (conv Tast.F64ToBytes x));
|
||||
edyn = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_print", [ x ]))) }
|
||||
in
|
||||
let rc =
|
||||
{ Render.structs =
|
||||
|
||||
@ -4025,6 +4025,8 @@ declare i64 @flan_dyn_at(i64, i64)
|
||||
declare void @flan_dyn_set_at(i64, i64, i64)
|
||||
declare void @flan_dyn_push(i64, i64)
|
||||
declare void @flan_dyn_print(i64)
|
||||
declare void @flan_dyn_emit_dev(i64)
|
||||
declare void @flan_dyn_emit_watch(i64)
|
||||
declare i64 @flan_dyn_need_i64(i64)
|
||||
declare double @flan_dyn_need_f64(i64)
|
||||
declare i32 @flan_dyn_need_bool(i64)
|
||||
|
||||
@ -31,6 +31,10 @@ type emitter = {
|
||||
ei64 : Tast.expr -> Tast.expr;
|
||||
eu64 : Tast.expr -> Tast.expr;
|
||||
ef64 : Tast.expr -> Tast.expr;
|
||||
(* A dyn value, whole. The walk cannot take one apart — the tag is the
|
||||
runtime's to read — so the runtime renders it, into the same place the
|
||||
other four write to. *)
|
||||
edyn : Tast.expr -> Tast.expr;
|
||||
}
|
||||
|
||||
(* What a walk is allowed to do with a pointer, and it is exactly two
|
||||
@ -380,15 +384,9 @@ let rec render c depth (e : Tast.expr) : Tast.expr list =
|
||||
Flan value carries no header and only the compiler knows what it is; a
|
||||
dyn value is the exact opposite — the runtime knows and the compiler
|
||||
does not — so the printing belongs on the side that can see the tag, and
|
||||
the walk hands the whole value over.
|
||||
|
||||
The cost is that it writes to stdout itself rather than through
|
||||
[c.emit], so a dyn printed at the REPL arrives on the program's output
|
||||
and not in the REPL's buffer. Fixing that means an emit-shaped dyn
|
||||
printer in the runtime — a second entry point taking the sink — and it
|
||||
is not milestone 1's. *)
|
||||
| Types.Dyn ->
|
||||
[ unit_ (Tast.Prim (Tast.Rt "flan_dyn_print", [ e ])) ]
|
||||
the walk hands the whole value to [c.emit.edyn], which names the runtime
|
||||
entry point that renders into this emitter's sink. *)
|
||||
| Types.Dyn -> [ c.emit.edyn e ]
|
||||
(* Reachable: [(println m)] on a Map. Everything else in [Types.t] has an
|
||||
arm above, and a [Var] never reaches a backend. So this names the fix
|
||||
rather than only the refusal. *)
|
||||
|
||||
@ -1140,7 +1140,13 @@ let dev_emitter : Render.emitter =
|
||||
estr = call emit_str;
|
||||
ei64 = call emit_i64;
|
||||
eu64 = call emit_u64;
|
||||
ef64 = call emit_f64 }
|
||||
ef64 = call emit_f64;
|
||||
(* Into the value buffer, not stdout: a dyn expression's value belongs in
|
||||
the reply's value like any other. *)
|
||||
edyn =
|
||||
(fun x ->
|
||||
{ Tast.e = Tast.Prim (Tast.Rt "flan_dyn_emit_dev", [ x ]);
|
||||
ty = Types.Unit; loc = x.Tast.loc }) }
|
||||
|
||||
(* And what the REPL may do with a pointer, which [println] may not. See
|
||||
render.ml's [pointers] for why the two sides differ. *)
|
||||
|
||||
@ -48,6 +48,13 @@
|
||||
* copies honest. */
|
||||
|
||||
void flan_write_stdout(const uint8_t *p, int64_t n);
|
||||
/* The two sinks in flan_dev.c a dyn value can be rendered into instead of
|
||||
* stdout: an evaluated expression's value, and a watch slot. flan_dev.c is
|
||||
* linked into every build, so these resolve whether or not the build is a dev
|
||||
* one; the reference runs this way round so that flan_dev.c names nothing in
|
||||
* this file and the dyn runtime stays droppable at the file level. */
|
||||
void flan_dev_emit(const uint8_t *bytes, int64_t len);
|
||||
void flan_dev_watch_emit(const uint8_t *bytes, int64_t len);
|
||||
|
||||
/* The one non-local exit a dyn operation can take. flan_rt.c's [rt_trap] is
|
||||
* static, and re-implementing what it does — the break-loop hook, the flush,
|
||||
@ -462,7 +469,8 @@ static inline const char *tag_of(flan_dyn v) {
|
||||
* One walk, two callers. [flan_dyn_print] writes to stdout through
|
||||
* [flan_write_stdout], so a dyn print and a typed print interleave correctly
|
||||
* in the one buffer; a trap message renders into a small buffer and puts the
|
||||
* values in the sentence.
|
||||
* values in the sentence. [flan_dyn_emit_dev] and [flan_dyn_emit_watch] are
|
||||
* the print walk aimed at flan_dev.c's buffers instead of stdout.
|
||||
*
|
||||
* What it renders, per tag, is what typed [print] renders for the
|
||||
* corresponding type — captured from a running program rather than read off
|
||||
@ -495,11 +503,17 @@ static inline const char *tag_of(flan_dyn v) {
|
||||
|
||||
#define PRINT_DEPTH 16
|
||||
|
||||
static void emit(const char *s) {
|
||||
flan_write_stdout((const uint8_t *)s, (int64_t)strlen(s));
|
||||
/* Where the rendering goes. stdout for [print], or one of flan_dev.c's
|
||||
* buffers when the value is an evaluated expression's or a watched one: a
|
||||
* value written to stdout arrives on the program's output rather than as the
|
||||
* value the editor asked for. */
|
||||
typedef void (*dyn_sink)(const uint8_t *p, int64_t n);
|
||||
|
||||
static void emit(dyn_sink w, const char *s) {
|
||||
w((const uint8_t *)s, (int64_t)strlen(s));
|
||||
}
|
||||
|
||||
static void emit_n(const uint8_t *p, int64_t n) { flan_write_stdout(p, n); }
|
||||
static void emit_n(dyn_sink w, const uint8_t *p, int64_t n) { w(p, n); }
|
||||
|
||||
/* A text inside a structure, quoted and escaped. The same table as
|
||||
* flan_rt.c's [flan_escape_char], which is where the typed side's printers —
|
||||
@ -514,28 +528,28 @@ static void emit_n(const uint8_t *p, int64_t n) { flan_write_stdout(p, n); }
|
||||
* shares with the other side is the part that must not drift. If that table
|
||||
* changes, change this one. Streamed rather than built, so there is no buffer
|
||||
* to overrun and no length to cap. */
|
||||
static void emit_escaped(const uint8_t *p, int64_t n) {
|
||||
static void emit_escaped(dyn_sink w, const uint8_t *p, int64_t n) {
|
||||
int64_t i;
|
||||
emit("\"");
|
||||
emit(w, "\"");
|
||||
for (i = 0; i < n; i++) {
|
||||
unsigned char c = p[i];
|
||||
switch (c) {
|
||||
case '"': emit("\\\""); break;
|
||||
case '\\': emit("\\\\"); break;
|
||||
case '\n': emit("\\n"); break;
|
||||
case '\t': emit("\\t"); break;
|
||||
case '\r': emit("\\r"); break;
|
||||
case '"': emit(w, "\\\""); break;
|
||||
case '\\': emit(w, "\\\\"); break;
|
||||
case '\n': emit(w, "\\n"); break;
|
||||
case '\t': emit(w, "\\t"); break;
|
||||
case '\r': emit(w, "\\r"); break;
|
||||
default:
|
||||
if (c < 0x20) {
|
||||
char b[5];
|
||||
snprintf(b, sizeof b, "\\x%02x", c);
|
||||
emit(b);
|
||||
emit(w, b);
|
||||
} else {
|
||||
emit_n(&c, 1);
|
||||
emit_n(w, &c, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
emit("\"");
|
||||
emit(w, "\"");
|
||||
}
|
||||
|
||||
static int64_t dyn_int_value(flan_dyn v); /* forward: both int shapes */
|
||||
@ -554,20 +568,20 @@ static int64_t view_elem_size(int32_t elem);
|
||||
static int64_t vecish_len(flan_obj *o);
|
||||
static flan_dyn vecish_at(flan_obj *o, int64_t i);
|
||||
|
||||
static void render(flan_dyn v, int depth, int nested) {
|
||||
static void render(dyn_sink w, flan_dyn v, int depth, int nested) {
|
||||
char buf[64];
|
||||
int32_t t = flan_dyn_tag(v);
|
||||
if (depth > PRINT_DEPTH) { emit("..."); return; }
|
||||
if (depth > PRINT_DEPTH) { emit(w, "..."); return; }
|
||||
switch (t) {
|
||||
case FLAN_DYN_TAG_NIL:
|
||||
emit("nil");
|
||||
emit(w, "nil");
|
||||
return;
|
||||
case FLAN_DYN_TAG_BOOL:
|
||||
emit(dyn_payload(v) ? "true" : "false");
|
||||
emit(w, dyn_payload(v) ? "true" : "false");
|
||||
return;
|
||||
case FLAN_DYN_TAG_INT:
|
||||
snprintf(buf, sizeof buf, "%lld", (long long)dyn_int_value(v));
|
||||
emit(buf);
|
||||
emit(w, buf);
|
||||
return;
|
||||
case FLAN_DYN_TAG_FLOAT: {
|
||||
double d;
|
||||
@ -576,21 +590,21 @@ static void render(flan_dyn v, int depth, int nested) {
|
||||
* the comparison flan_rt.c and the prelude both use. */
|
||||
if (d != d) snprintf(buf, sizeof buf, "nan");
|
||||
else snprintf(buf, sizeof buf, "%g", d);
|
||||
emit(buf);
|
||||
emit(w, buf);
|
||||
return;
|
||||
}
|
||||
case FLAN_DYN_TAG_TEXT: {
|
||||
flan_obj *o = dyn_obj(v);
|
||||
if (nested) emit_escaped(obj_text_bytes(o), o->len);
|
||||
else emit_n(obj_text_bytes(o), o->len);
|
||||
if (nested) emit_escaped(w, obj_text_bytes(o), o->len);
|
||||
else emit_n(w, obj_text_bytes(o), o->len);
|
||||
return;
|
||||
}
|
||||
/* A keyword prints with its colon, bare, at every depth: :a is its own
|
||||
* spelling the way true is, and quoting it would make it a text. */
|
||||
case FLAN_DYN_TAG_KEYWORD: {
|
||||
kw_entry *k = dyn_kw(v);
|
||||
emit(":");
|
||||
emit_n(kw_bytes(k), k->len);
|
||||
emit(w, ":");
|
||||
emit_n(w, kw_bytes(k), k->len);
|
||||
return;
|
||||
}
|
||||
/* The map prints in edn's shape with the vec's spacing: a space before
|
||||
@ -604,40 +618,48 @@ static void render(flan_dyn v, int depth, int nested) {
|
||||
* for a record: #point{ :x 1 :y 2}. The tag is not an entry, so it is
|
||||
* written here or it is not written at all. */
|
||||
if (o->u.v.klass != NULL) {
|
||||
emit("#");
|
||||
emit_n(kw_bytes(o->u.v.klass), o->u.v.klass->len);
|
||||
emit(w, "#");
|
||||
emit_n(w, kw_bytes(o->u.v.klass), o->u.v.klass->len);
|
||||
}
|
||||
emit("{");
|
||||
emit(w, "{");
|
||||
for (i = 0; i < o->len; i++) {
|
||||
emit(" ");
|
||||
render(o->u.v.items[i * 2], depth + 1, 1);
|
||||
emit(" ");
|
||||
render(o->u.v.items[i * 2 + 1], depth + 1, 1);
|
||||
emit(w, " ");
|
||||
render(w, o->u.v.items[i * 2], depth + 1, 1);
|
||||
emit(w, " ");
|
||||
render(w, o->u.v.items[i * 2 + 1], depth + 1, 1);
|
||||
}
|
||||
emit("}");
|
||||
emit(w, "}");
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
flan_obj *o = dyn_obj(v);
|
||||
int64_t i, n = o->kind == OBJ_VIEW ? view_len("print", o) : o->len;
|
||||
emit("[");
|
||||
emit(w, "[");
|
||||
for (i = 0; i < n; i++) {
|
||||
emit(" ");
|
||||
emit(w, " ");
|
||||
if (o->kind == OBJ_VIEW)
|
||||
render(view_box(o->u.view.elem,
|
||||
render(w, view_box(o->u.view.elem,
|
||||
(const uint8_t *)view_base(o)
|
||||
+ i * view_elem_size(o->u.view.elem)),
|
||||
depth + 1, 1);
|
||||
else
|
||||
render(o->u.v.items[i], depth + 1, 1);
|
||||
render(w, o->u.v.items[i], depth + 1, 1);
|
||||
}
|
||||
emit("]");
|
||||
emit(w, "]");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void flan_dyn_print(flan_dyn v) { render(v, 0, 0); }
|
||||
void flan_dyn_print(flan_dyn v) { render(flan_write_stdout, v, 0, 0); }
|
||||
|
||||
/* The same rendering into an evaluated expression's value, and into the watch
|
||||
* slot [flan_dev_watch_begin] opened. lib/render.ml's dyn arm calls these on
|
||||
* the inspecting side and [flan_dyn_print] on [println]'s. A text is quoted
|
||||
* even at the top, because the typed side's renderer quotes a string there:
|
||||
* the value "5" and the value 5 must not read alike. */
|
||||
void flan_dyn_emit_dev(flan_dyn v) { render(flan_dev_emit, v, 0, 1); }
|
||||
void flan_dyn_emit_watch(flan_dyn v) { render(flan_dev_watch_emit, v, 0, 1); }
|
||||
|
||||
/* The same walk into a buffer, for a trap's sentence. Bounded and truncated
|
||||
* rather than allocating: a trap is the one moment when allocating would be a
|
||||
|
||||
@ -186,6 +186,10 @@ flan_dyn flan_dyn_map_contains(flan_dyn m, flan_dyn k);
|
||||
/* Structural, and per type it renders what typed [print] renders. Never
|
||||
* traps: every tag has a rendering, including nil. */
|
||||
void flan_dyn_print(flan_dyn v);
|
||||
/* The same rendering, into flan_dev.c's evaluated-value buffer and into the
|
||||
* open watch slot rather than stdout. A text is quoted at the top as well. */
|
||||
void flan_dyn_emit_dev(flan_dyn v);
|
||||
void flan_dyn_emit_watch(flan_dyn v);
|
||||
|
||||
/* ── The typed boundary ────────────────────────────────────────────────
|
||||
*
|
||||
|
||||
@ -5860,12 +5860,11 @@ let () =
|
||||
| Some { Form.v = Form.Sym "t"; _ } -> true
|
||||
| _ -> false
|
||||
in
|
||||
(* What the expression answered, wherever it came back: a dyn value
|
||||
is rendered into the reply's output rather than into [:value],
|
||||
and which of the two carries it is not what is under test. *)
|
||||
(* A dyn value is rendered into the reply's [:value], as a typed
|
||||
one is, on both backends, and a dyn text is quoted there as a
|
||||
typed string is. *)
|
||||
let answer r =
|
||||
Option.value ~default:"" (Wire.string_field r "value")
|
||||
^ Option.value ~default:"" (Wire.string_field r "output")
|
||||
in
|
||||
let read () =
|
||||
answer
|
||||
@ -5884,7 +5883,7 @@ let () =
|
||||
if not (await ~ms:20000 parked) then
|
||||
fail "the dyn-global program (--%s) never parked" backend
|
||||
else begin
|
||||
if not (contains_sub (read ()) "kept") then
|
||||
if read () <> "\"kept\"" then
|
||||
fail "--%s: the global was not readable before any thunk ran: %S"
|
||||
backend (read ());
|
||||
for cycle = 1 to 3 do
|
||||
@ -6076,13 +6075,11 @@ let () =
|
||||
if not (printed "counter 45") then
|
||||
fail "the defonce beside the edited def lost its value: %S"
|
||||
(Buffer.contents output);
|
||||
(* Read back rather than only printed, [counter]'s reason. A dyn
|
||||
renders through the program's printer — its reply carries the text
|
||||
in [:output] and an empty [:value] — so the cast is what turns the
|
||||
answer into a value the reply can hold. *)
|
||||
(* Read back rather than only printed, [counter]'s reason. [c] is a
|
||||
dyn, and its value arrives in [:value] like a typed one's. *)
|
||||
(let r =
|
||||
request c
|
||||
"(:op \"eval-expr\" :code \"(i64 c)\" :file \"programs/dev-rerun.flan\")"
|
||||
"(:op \"eval-expr\" :code \"c\" :file \"programs/dev-rerun.flan\")"
|
||||
in
|
||||
match Wire.string_field r "value" with
|
||||
| Some "10" -> ()
|
||||
@ -6525,13 +6522,9 @@ let () =
|
||||
"(:op \"eval-expr\" :code %S :file \"programs/dev-class.flan\")"
|
||||
code)
|
||||
in
|
||||
(* Every answer is compared inside the expression rather than read out
|
||||
of it. A generic answers a dyn, and a dyn value is rendered to the
|
||||
program's own stdout rather than into the reply's :value — it does
|
||||
reach a later reply's :output, which is how the dyn-global rows
|
||||
below read one, but the flush is the next reply's and not this one's.
|
||||
Asking the running program whether the answer is 12 puts a typed
|
||||
value in :value and takes the timing out of the test.
|
||||
(* Most answers are compared inside the expression, which keeps each
|
||||
row about dispatch rather than about rendering; the row after the
|
||||
first ask reads a dyn answer straight out of :value.
|
||||
|
||||
The first ask is retried: the agent's thread is let go only after the
|
||||
socket is bound, so an early ask is a race with the startup and not a
|
||||
@ -6546,6 +6539,12 @@ let () =
|
||||
else begin
|
||||
if !answered <> "1" then
|
||||
fail "the method the program was built with answered %S" !answered;
|
||||
(* A generic answers a dyn, and its value is in this reply's :value
|
||||
and not on the program's stdout. *)
|
||||
(let r = ask "(area (point 3 4))" in
|
||||
if value r <> "12" then
|
||||
fail "a dyn answer did not reach :value (%S, output %S)" (value r)
|
||||
(Option.value ~default:"" (Wire.string_field r "output")));
|
||||
(* A class is on [defs] as a class, with its slots and where it is
|
||||
written, and its constructor is not listed a second time as a fn. *)
|
||||
(let r = request c "(:op \"defs\")" in
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user