c-tier streaming: re-add the trailing newline emcc strips before postMessage

Emscripten's Module.print fires once per line and hands the loader
just the line content — no trailing newline; the caller is expected
to add it back when joining (outBuf.join('\n') does that for the
final return). Streaming path was forwarding the line straight to
onChunk without the newline, so successive chunks concatenated
horizontally in the playground panel — the loop

  (let loop ((i 0))
    (cond ((= (modulo i 10000) 0) (display \"tick \") (display i) (newline)))
    (loop (+ i 1)))

was running fine on c tier (each (newline) ended a printf line) but
arriving in the UI as 'tick 0tick 10000tick 20000...' with no
breaks.

Loader's print/printErr callbacks now pass `line + \"\\n\"` to
currentOnChunk. Final buffered join (which already adds the \\n)
is untouched, so the post-eval output string keeps the same shape.
Pyodide _StreamingStdout sees Python's raw write() bytes including
the \\n already; asm's emit_chunk fires AFTER output_len was
incremented past the \\n, so the slice [flush_start, output_len)
includes it. Neither needed a change.
This commit is contained in:
russell@unturf.com 2026-06-15 05:38:03 -04:00
parent d14c0eb5e4
commit cf7ff2219f
No known key found for this signature in database
4 changed files with 56 additions and 9 deletions

View file

@ -19,6 +19,14 @@ async function _bootstrap() {
// through immediately so the worker can postMessage a chunk to
// the UI as each line emerges (vs the user staring at a blank
// panel for 18 seconds during the heavy bend call).
//
// Emscripten's print convention is "one call per line WITHOUT
// the trailing newline" — the caller is expected to add a \n
// when reassembling. Our outBuf join adds it back, but the
// streaming path needs it explicitly or successive chunks
// concatenate horizontally in the output panel (saw with
// (let loop ((i 0)) ... (display i) (newline) ... (loop ...))
// — every tick ended up on the same line).
let outBuf = [];
let errBuf = [];
let currentOnChunk = null;
@ -26,11 +34,11 @@ async function _bootstrap() {
locateFile: (p) => wasmDir + p,
print: (line) => {
outBuf.push(line);
if (currentOnChunk) currentOnChunk(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
printErr: (line) => {
errBuf.push(line);
if (currentOnChunk) currentOnChunk(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
});

View file

@ -19,6 +19,14 @@ async function _bootstrap() {
// through immediately so the worker can postMessage a chunk to
// the UI as each line emerges (vs the user staring at a blank
// panel for 18 seconds during the heavy bend call).
//
// Emscripten's print convention is "one call per line WITHOUT
// the trailing newline" — the caller is expected to add a \n
// when reassembling. Our outBuf join adds it back, but the
// streaming path needs it explicitly or successive chunks
// concatenate horizontally in the output panel (saw with
// (let loop ((i 0)) ... (display i) (newline) ... (loop ...))
// — every tick ended up on the same line).
let outBuf = [];
let errBuf = [];
let currentOnChunk = null;
@ -26,11 +34,11 @@ async function _bootstrap() {
locateFile: (p) => wasmDir + p,
print: (line) => {
outBuf.push(line);
if (currentOnChunk) currentOnChunk(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
printErr: (line) => {
errBuf.push(line);
if (currentOnChunk) currentOnChunk(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
});

View file

@ -19,6 +19,14 @@ async function _bootstrap() {
// through immediately so the worker can postMessage a chunk to
// the UI as each line emerges (vs the user staring at a blank
// panel for 18 seconds during the heavy bend call).
//
// Emscripten's print convention is "one call per line WITHOUT
// the trailing newline" — the caller is expected to add a \n
// when reassembling. Our outBuf join adds it back, but the
// streaming path needs it explicitly or successive chunks
// concatenate horizontally in the output panel (saw with
// (let loop ((i 0)) ... (display i) (newline) ... (loop ...))
// — every tick ended up on the same line).
let outBuf = [];
let errBuf = [];
let currentOnChunk = null;
@ -26,11 +34,11 @@ async function _bootstrap() {
locateFile: (p) => wasmDir + p,
print: (line) => {
outBuf.push(line);
if (currentOnChunk) currentOnChunk(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
printErr: (line) => {
errBuf.push(line);
if (currentOnChunk) currentOnChunk(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
});

View file

@ -13,12 +13,33 @@ async function _bootstrap() {
const wasmDir = new URL("./", import.meta.url).href;
const { default: createLumbdaC } = await import(/* @vite-ignore */ factoryURL);
// Streaming output: every printf in the wasm fires Module.print
// synchronously. We buffer normally for the final return, but if
// the current eval registered an onChunk callback we also call
// through immediately so the worker can postMessage a chunk to
// the UI as each line emerges (vs the user staring at a blank
// panel for 18 seconds during the heavy bend call).
//
// Emscripten's print convention is "one call per line WITHOUT
// the trailing newline" — the caller is expected to add a \n
// when reassembling. Our outBuf join adds it back, but the
// streaming path needs it explicitly or successive chunks
// concatenate horizontally in the output panel (saw with
// (let loop ((i 0)) ... (display i) (newline) ... (loop ...))
// — every tick ended up on the same line).
let outBuf = [];
let errBuf = [];
let currentOnChunk = null;
const module = await createLumbdaC({
locateFile: (p) => wasmDir + p,
print: (line) => outBuf.push(line),
printErr: (line) => errBuf.push(line),
print: (line) => {
outBuf.push(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
printErr: (line) => {
errBuf.push(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
});
const _init = module.cwrap("lumbda_wasm_init", null, []);
@ -56,10 +77,12 @@ async function _bootstrap() {
const path = `/tmp/${name}.portal`;
module.FS.writeFile(path, blob);
},
async evalLisp(src) {
async evalLisp(src, onChunk) {
outBuf = [];
errBuf = [];
currentOnChunk = onChunk || null;
const errPtr = _eval(src);
currentOnChunk = null;
let errMsg = "";
if (errPtr) {
errMsg = module.UTF8ToString(errPtr);