Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions prims.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 35 additions & 12 deletions test/cli_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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)
Loading