diff --git a/prims.lua b/prims.lua index 91e8f9f..98495a5 100644 --- a/prims.lua +++ b/prims.lua @@ -737,7 +737,36 @@ function P.install_native_stdlib() return orig_variable(x) end + -- pr (issue #22): the canonical kernel `pr` (klambda/writer.kl) gates ALL + -- output on *hush*: + -- (defun pr (S ST) (if (value *hush*) S (...write S to ST...))) + -- That silences writes to ANY stream under -q/*hush*, so `pr` to a FILE + -- stream produced a zero-byte file — a divergence from shen-cl/shen-go/ + -- ShenScript, which write to files regardless of *hush*. *hush* is meant to + -- suppress only the interactive/echo output that goes to standard output. + -- This native override consults *hush* ONLY when the target is the standard + -- output stream (*stoutput*); writes to any other stream always occur. The + -- actual write delegates to the original compiled-KL pr (with *hush* cleared) + -- so the write path stays byte-identical. + local orig_pr = F["pr"] + local function pr(s, st) + if GLOBALS["*hush*"] and st == GLOBALS["*stoutput*"] then return s end + if GLOBALS["*hush*"] then + -- non-stdout stream under *hush*: write unconditionally. Temporarily + -- clear *hush* so the original kernel pr takes its write branch, then + -- restore it so we don't perturb global state. + local saved = GLOBALS["*hush*"] + GLOBALS["*hush*"] = false + local ok, res = pcall(orig_pr, s, st) + GLOBALS["*hush*"] = saved + if not ok then error(res, 0) end + return res + end + return orig_pr(s, st) + end + local function install(name, fn, arity) F[name] = fn; FA[fn] = arity end + install("pr", pr, 2) install("variable?", variable_q, 1) install("fail", fail, 0) install("shen.parse-failure", parse_failure, 0) diff --git a/test/cli_spec.lua b/test/cli_spec.lua index 9bc7a4e..83c39f3 100644 --- a/test/cli_spec.lua +++ b/test/cli_spec.lua @@ -11,10 +11,10 @@ -- * the REPL survives adversarial input (apply-non-function) and keeps going; -- * an adversarial `-e` exits nonzero with a clean error (no Lua traceback); -- * unknown option exits 2; --- * the *hush*/-q divergence: on shen-lua, `-q` SILENCES pr to file streams, --- producing a ZERO-BYTE file, whereas without -q the file gets the payload. --- (This is the documented cross-impl divergence vs shen-cl/shen-go/ShenScript, --- which route pr to files regardless of *hush*.) +-- * pr-to-file under -q/*hush* (issue #22): -q sets *hush*, but *hush* only +-- suppresses writes to STANDARD OUTPUT. A `pr` to a FILE stream writes the +-- payload regardless of *hush*, matching shen-cl/shen-go/ShenScript. (This +-- used to diverge: -q produced a zero-byte file; fixed in #22.) -- -- Every subprocess is wrapped in `timeout` when available, so an EOF-loop -- regression FAILS (nonzero/empty output) rather than HANGS the whole suite. @@ -176,21 +176,22 @@ do end -- --------------------------------------------------------------------------- --- THE -q / *hush* DIVERGENCE. On shen-lua, -q sets *hush*, which SILENCES pr --- to file streams -> a ZERO-BYTE file. Without -q the file gets "payload". --- This intentionally differs from shen-cl/shen-go/ShenScript, which write the --- payload regardless of *hush*. Lock in shen-lua's documented behavior. +-- pr-to-file under -q / *hush* (issue #22 regression). -q sets *hush*, but +-- *hush* must suppress only STANDARD-OUTPUT writes — a `pr` to a FILE stream +-- writes the payload regardless of *hush*, matching shen-cl/shen-go/ShenScript. +-- (Pre-#22 this produced a ZERO-BYTE file.) -- --------------------------------------------------------------------------- do local pq = os.tmpname() local expr = '(let S (open "' .. pq .. '" out) (do (pr "payload" S) (close S)))' local _, codeq = run(SHEN .. " -q -e " .. sh_quote(expr)) check(codeq == 0, "-q pr-to-file exits 0") - -- read the file size - local sizeq = 0 + -- read the file back + local contentq = "" local hf = io.open(pq, "rb") - if hf then local d = hf:read("*a") or ""; sizeq = #d; hf:close() end - check(sizeq == 0, "-q SILENCES pr to file (zero-byte file) — documented divergence") + if hf then contentq = hf:read("*a") or ""; hf:close() end + check(contentq == "payload", + "issue #22: -q (*hush*) does NOT silence pr to a file stream — payload is written") os.remove(pq) -- without -q, the same write produces the payload @@ -205,5 +206,27 @@ do os.remove(pn) end +-- --------------------------------------------------------------------------- +-- issue #22 (other half): *hush* STILL silences standard-output writes. With +-- -q, a `pr` to (stoutput) must produce no stdout — only the file path above +-- is exempted, not the gate itself. +-- --------------------------------------------------------------------------- +do + -- `pr` returns its string argument, and `-e` echoes the expression's value, + -- so the pr text would appear in stdout via the echo regardless of *hush*. + -- Return a DISTINCT value (99) so the only way the marker can appear is the + -- pr write itself — which must be silenced under -q. + local marker = "NOISE_22_MARKER" + local expr = '(do (pr "' .. marker .. '" (stoutput)) 99)' + local outq, codeq = run(SHEN .. " -q -e " .. sh_quote(expr)) + check(codeq == 0, "-q pr-to-stdout exits 0") + check(outq:find(marker, 1, true) == nil, + "issue #22: -q (*hush*) still silences pr to standard output") + -- and without -q the marker IS written to stdout (sanity: the gate exists) + local outn = run(SHEN .. " -e " .. sh_quote(expr)) + check(outn:find(marker, 1, true) ~= nil, + "without -q, pr to standard output is written") +end + io.write(string.format("cli_spec: %d pass, %d fail\n", npass, nfail)) os.exit(nfail == 0 and 0 or 1)