ffi/readline: blocking readline() + add_history(), nil on EOF

Phase 0 binding per PHASE0.md §9. M.readline(prompt) returns the line
as a Lua string (the C buffer is freed via libc free immediately after
ffi.string copies it) or nil on EOF. M.add_history skips empty lines.

Loader handles the case where libreadline-dev's unversioned
`libreadline.so` symlink isn't installed — falls through to
`readline.so.8` (current Debian/Arch ALARM) and `.so.7` (older)
before giving up. This trips on noether-the-LXD: only the runtime
package is present.

Smoke (stdin from heredoc, two lines + EOF):

  p1> hello world  -> "hello world"
  p2> second line  -> "second line"
  p3>              -> nil (EOF)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 11:44:38 +00:00
parent fd63dff65e
commit c9116c9bbf
+37 -2
View File
@@ -1,5 +1,5 @@
-- ffi/readline.lua — GNU readline binding.
-- Phase 0: readline + add_history + free. Phase 1: custom key bindings.
-- Phase 0: readline + add_history + EOF handling. Phase 1: custom key bindings.
-- See docs/PHASE0.md §9.
local ffi = require("ffi")
@@ -10,6 +10,41 @@ void add_history(const char *line);
void free(void *ptr);
]]
-- libreadline-dev (which ships the unversioned `libreadline.so` symlink) is
-- not assumed to be installed on the runtime host; fall back to versioned
-- sonames so a base Debian/Arch with just libreadline runtime works.
local function load_readline()
local errs = {}
for _, name in ipairs({"readline", "readline.so.8", "readline.so.7"}) do
local ok, lib = pcall(ffi.load, name)
if ok then return lib end
errs[#errs+1] = name .. ": " .. tostring(lib)
end
error("libreadline not loadable: " .. table.concat(errs, "; "))
end
local rl = load_readline()
local C = ffi.C
local M = {}
-- Phase 0 stubs; wired with the REPL implementation.
-- Read one line of input.
-- Returns:
-- string : the line (no trailing newline)
-- nil : EOF (Ctrl-D on empty line)
function M.readline(prompt)
local cstr = rl.readline(prompt)
if cstr == nil then return nil end
local s = ffi.string(cstr)
C.free(cstr)
return s
end
-- Append a non-empty line to readline's in-memory history.
function M.add_history(line)
if line and #line > 0 then
rl.add_history(line)
end
end
return M