From 8e5fcf54cc513993939282005cd5d1d62cedf4eb Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 18 Sep 2026 08:00:57 +0700 Subject: [PATCH] The two sweeps do not race a printing fixture against a clock dev-chatty.flan outlives the surveys' twenty seconds by design, and unlike dev-repl it prints while it does -- so the two backends stop at different lines and the diff reports on scheduling rather than on lowering. It joins dev-loop and dev-watch in the excluded-by-name list in both sweeps, with the distinction written down. rt_flush_out is guarded on __wasm__: the pipe it is careful about belongs to a merged flan dev, which is only ever a native host, and wasm32 need not answer for a descriptor mode its runtime may model differently. And three comments that went false with the _exit: the atexit registration in the merged entry point is no longer there for rt_die, which unlinks the socket for itself now, so both places that said so say what it is actually left covering. --- lib/dev.ml | 28 ++++++++++++++++------------ runtime/flan_rt.c | 16 +++++++++++++++- spike/js/survey.sh | 8 +++++--- spike/x86/survey.sh | 13 +++++++++++-- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/lib/dev.ml b/lib/dev.ml index addaabe..21ebc54 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -3146,15 +3146,19 @@ int flan_merged_program_state(void) { * message has already sent two investigations in this repository to the wrong * place. Gone is the honest state, and the client already has words for it. * - * [atexit] covers exit(3), which is the compiler thread's own way out and the - * way out of a program that somehow returns from main. It does NOT cover the - * two places a program dies where it stands: rt_die in flan_rt.c and die_now - * in flan_agent.c both take _exit, because the atexit chain and the ELF - * destructors want the loader lock a dlopening listener thread may be holding - * — and in this build that chain also holds OCaml's shutdown. Both of them - * therefore unlink this path by hand, which is why this function's body is - * written out three times in the tree rather than shared: it is four lines, and - * the alternative is a runtime that has to link against the daemon. */ + * [atexit] covers exit(3), and what is left taking exit(3) here is narrower + * than it was: the two places a program dies where it stands — rt_die in + * flan_rt.c and die_now in flan_agent.c — both take _exit, because the atexit + * chain and the ELF destructors want the loader lock a dlopening listener + * thread may be holding, and in this build that chain also holds OCaml's + * shutdown. Both therefore unlink this path by hand, so the same four lines + * exist in three places rather than one — sharing them would mean a runtime + * that links against the daemon, which is a worse trade than the repetition. + * + * What is left for this one is every exit(3) nobody planned — an OCaml fatal + * on the compiler thread most of all, since [Stdlib.exit] ends in the C one. + * Four lines on a path nobody means to take is the right price for a socket + * that never sits on disk refusing connects. */ static void flan_merged_unlink_sock(void) { const char *s = getenv("FLAN_DEV_SOCK"); if (s != NULL && *s != '\0') unlink(s); @@ -3234,9 +3238,9 @@ int main(int argc, char **argv) { * own shutdown to that chain. The hand-written [flan_merged_unlink_sock] that * used to sit under this function's _exit went with it for the same reason — * there is no way out of here any more to unlink on. The [atexit] - * registration above stays, because it is not for this path: it is for - * exit(3), which is what flan_rt.c's rt_die takes, and a merged daemon that - * dies through a trap must not leave a socket refusing connects behind it. */ + * registration above stays, and no longer because of the trap: rt_die takes + * _exit now and unlinks for itself, exactly as die_now does. What is left + * for it is written at [flan_merged_unlink_sock]. */ for (;;) { if (setjmp(program_return) == 0) { rc = flan_program_main(argc, argv); diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index a1190f5..75ac84d 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -394,7 +394,9 @@ void flan_escape_bytes(const uint8_t *p, int64_t n, flan_slice *out) { * redirected stdout is not, so without this the error appears above the output * that led to it. */ +#if !defined(__wasm__) #include +#endif #include /* That flush, made incapable of waiting. @@ -416,12 +418,24 @@ void flan_escape_bytes(const uint8_t *p, int64_t n, flan_slice *out) { * The return value is ignored deliberately, twice over: a failed fcntl leaves * the old blocking behaviour, which is what this code did before, and a flush * that reports EAGAIN has done as much as it is going to. There is nothing a - * dying process can do about either. */ + * dying process can do about either. + * + * Guarded on __wasm__, which this file otherwise does only for the valgrind + * client request below. The hazard is a pipe whose reader is + * the compiler thread of a merged `flan dev', which is a native host and only + * ever a native host — a wasm32 module has no compiler beside it and no such + * pipe. So the guard is not a portability apology: it says where the problem + * can exist, and keeps wasm32 from having to answer for a descriptor mode its + * runtime may model differently. */ +#if defined(__wasm__) +static void rt_flush_out(void) { (void)fflush(stdout); } +#else static void rt_flush_out(void) { int flags = fcntl(1, F_GETFL, 0); if (flags >= 0) (void)fcntl(1, F_SETFL, flags | O_NONBLOCK); (void)fflush(stdout); } +#endif /* [_exit] and not [exit], for the reason die_now gives in * vendor/agent/flan_agent.c and gives at length: this runs on the game thread, diff --git a/spike/js/survey.sh b/spike/js/survey.sh index b1b79b9..646c7fa 100755 --- a/spike/js/survey.sh +++ b/spike/js/survey.sh @@ -59,9 +59,11 @@ fi corpus=${SURVEY_CORPUS:-$root} out=$(mktemp -d); trap 'rm -rf "$out"' EXIT -# The two that run until something stops them, excluded by name for the reason -# the x86 sweep excludes them: a timeout cannot tell them from a hang. -forever="dev-loop dev-watch" +# The ones that run until something stops them, excluded by name for the reason +# the x86 sweep excludes them: a timeout cannot tell them from a hang -- and in +# dev-chatty's case cannot even give the two sides the same truncation, since +# it prints 4K a frame for as long as it is allowed to. +forever="dev-loop dev-watch dev-chatty" TIMEOUT=${TIMEOUT:-20} diff --git a/spike/x86/survey.sh b/spike/x86/survey.sh index d3affb0..9f2a0b8 100755 --- a/spike/x86/survey.sh +++ b/spike/x86/survey.sh @@ -61,10 +61,19 @@ corpus=${SURVEY_CORPUS:-$root} out=$(mktemp -d); trap 'rm -rf "$out"' EXIT -# The two that run until something stops them. Not a failure and not a match; +# The ones that run until something stops them. Not a failure and not a match; # they are excluded by name because a timeout cannot tell them apart from a # backend that hung. -forever="dev-loop dev-watch" +# +# dev-chatty is the third and is here for a sharper reason than the other two. +# It also outlives the timeout -- it is sized to outlast test_dev's checks and +# is killed with the connection -- but what makes it unusable here is that it +# *prints* while it does, 4K a frame. Two backends stopped by a clock stop at +# different lines, so the diff is a report about scheduling rather than about +# lowering, and it fails the alias every run. dev-repl outlives the timeout in +# the same way and is not listed, because it prints nothing and the two +# truncations are both empty. +forever="dev-loop dev-watch dev-chatty" TIMEOUT=${TIMEOUT:-20}