|
Warning: this is an htmlized version!
The original is here, and the conversion rules are here. |
-- repl.lua: a repl for Lua (new version).
-- This file:
-- http://angg.twu.net/dednat5/repl2.lua.html
-- http://angg.twu.net/dednat5/repl2.lua
-- (find-dn5 "repl2.lua")
--
-- Author: Eduardo Ochs <eduardoochs@gmail.com>
-- Version: 2011dec05
-- License: GPL3
--
-- REPLs are hard to implement! As I've tried to explain in my notes in
-- http://angg.twu.net/repl.html
-- (find-TH "repl")
-- the control flow of a REPL can be daunting...
--
-- The class "Repl", defined below, is an attempt to implement all the
-- ideas mentioned in those notes, plus a few more - e.g., different
-- prefixes - without using any tricks like throw/catch, gotos, or
-- tail cails; we just use Repl object with a "status" field, plus
-- several other fields for temporary data.
--
-- The logic of a REPL is, very roughly, this:
--
-- /--------------------\
-- | |
-- v |
-- R ---> E ---> P ---> L
--
-- which means: Try to [R]ead a command, possibly spanning several
-- lines; after reading it, try to [E]val it; if the eval was
-- successful, [P]rint the results, otherwise display the error; if
-- not abort has been requested, [L]oop.
--
-- The logic of [R]ead is roughly this: read a first line of input,
-- with prompt ">"; while what we've got is an incomplete command -
-- like "if foo() then bar() else", read more input, now with prompt
-- ">>", again testing for completeness after adding each line. But
-- there are several kinds of errors that we must handle, so here is
--
-- The full pseudocode
-- ===================
-- (Note that this is to be read while you follow the real code!)
--
-- "Read()" is this:
-- Readfirst(), and while not Incomplete() do Readanother(); end;
-- if not Compilationerror() then return true end.
-- If either "Readfirst()" or "Readanother" receive a "^C", a "^D"
-- or an "eof" in its input, return nil.
-- Note that:
-- 1) Compilationerror() may set the status to "compilation
-- error", and in this case Read() returns nil.
-- 2) If Read() returns true this means that we have something
-- to Eval().
-- 3) There are several cases in which Read() returns nil:
-- r.status = "eof" -> means: abort the REPL
-- r.status = "^D" -> means: abort the REPL
-- r.status = "compilation error" -> means: read more
-- r.status = "^C" -> means: read more
-- 4) Incomplete() runs r.f, r.err = loadstring(r.code) and
-- tests if r.err holds certain a certain type of error (that
-- means that the code ends prematurely). The values in r.f
-- and r.err are reused by Compilationerror() and Eval().
-- 5) Readfirst() runs Identify(), which detects which prefix is
-- being used and sets some variables (e.g. r.print)
-- accordingly.
-- "Eval()" is this:
-- Run r.f() with xpcall, using a simple error handler to display
-- a traceback in case of runtime errors; when a runtime error
-- occurs, set r.status to "runtime error" and return nil, and
-- when there are no runtime errors set r.fresults to a closure
-- that returns the same return values as r.f(), and return true.
-- Note that when Eval() returns true that means that we may have
-- something to print.
-- "Print()" is this:
-- Run r.print(r:fresults()). For some prefixes, like "=", r.print
-- is set to a function that prints the results; for other
-- prefixes, like "", r.print is set to nop, which do not print
-- the results.
-- "ReadEvalPrint()" is this:
-- r:Read() and r:Eval() and r:Print().
-- After running that, r.status can be one of: "eof", "^C", "^D",
-- "compilation error", "runtime error", or some other (garbage)
-- values, all meaning "ok". When the status is "eof" or "^D"
-- ReadEvalPrint() returns nil, in all other cases it returns
-- true. When ReadEvalPrint() returns true that means that we
-- should loop.
--
-- How did I develop that
-- ======================
-- I examined this sample interaction,
--
-- > r = Repl {}
-- > r:Repl()
-- L> = 2 +
-- LL> 3, 4
-- 5 4
-- L> ^D
-- >
--
-- and wrote down how the fields in the Repl object "r" should be
-- changed, and in which order, and by which function:
--
-- r.line = "= 2 +" <-- set by Readline
-- r.str = "= 2 +" <-- set by Readfirst
-- r.prefix = "=" <-- set by Identify
-- r.body = " 2 +" <-/
-- r.code = "return 2 +" <-/
-- r.print = <function print> <-/
-- r.status = "incomplete" <-- set by Incomplete
-- r.line = "3, 4" <-- set by Readline
-- r.str = "= 2 +\n 3, 4" <-- set by Readanother
-- r.body = " 2 +\n 3, 4" <-/
-- r.code = "return 2 +\n 3, 4" <-/
-- r.status = "complete" <-- set by Incomplete
-- r.f, r.err = loadstring("return 2 +\n 3, 4") <-- set by Eval
-- r.out = {true, 5, 4, n=3} <-/
-- r.results() = 5, 4 <-/
-- r.line = "^D" <-- set by Readline
-- r.status = "^D"
--
-- Then, starting from the sketchy data flow diagram above, I
-- discovered how should be the control flow, and wrote the code.
--
-- A note: I'm cheap, so I decided to support "^C" and "^D" only as
-- lines holding a LITERAL caret then an uppercase "C" or "D" - I
-- don't want to have to deal with real signals right now! 8-\
--
-- (find-es "lua5" "loadstring_and_eof")
-- (find-es "lua5" "traceback")
-- (find-luamanualw3m "#pdf-xpcall")
-- (find-luamanualw3m "#pdf-unpack")
-- (find-luamanualw3m "#pdf-select")
require "common" -- (find-dn5 "common.lua")
require "eoo" -- (find-dn5 "eoo.lua")
Repl = Class {
type = "Repl",
__index = {
Readline = function (r, prompt)
io.write(prompt)
r.line = io.read()
if r.line == nil then r.status = "eof"; return end
if r.line == "^C" then r.status = "^C"; return end
if r.line == "^D" then r.status = "^D"; return end
return true
end,
Identify = function (r)
local prefix, body = r.line:match("^(==?)(.*)$")
r.str = r.line
if prefix then
r.prefix = prefix
r.body = body
r.code = "return "..body
r.print = print
r.print = PP
else
r.prefix = ""
r.body = r.line
r.code = r.line
r.print = function (...) end
end
end,
Incomplete = function (r)
local pat = "<eof>.$"
r.f, r.err = loadstring(r.code)
if r.err and r.err:match(pat) then
r.status = "incomplete"
return true
end
end,
Compilationerror = function (r)
if r.err then
r.status = "compilation error"
print(r.err)
return true
end
end,
Readfirst = function (r)
if r:Readline("L> ") then
r:Identify()
return true
end
end,
Readanother = function (r)
if r:Readline("LL> ") then
r.str = r.str .."\n"..r.line
r.body = r.body.."\n"..r.line
r.code = r.code.."\n"..r.line
return true
end
end,
Read = function (r)
if not r:Readfirst() then return end
while r:Incomplete() do
if not r:Readanother() then return end
end
if r:Compilationerror() then return end
return true
end,
Eval = function (r)
local handler = function () print(debug.traceback()) end
local out = pack(xpcall(r.f, handler))
if not out[1] then r.status = "runtime error"; return end
r.fresults = function () return unpack(out, 2, out.n) end
return true
end,
Print = function (r)
r.print(r:fresults())
end,
ReadEvalPrint = function (r)
if r:Read() and r:Eval() then r:Print() end
if r.status == "^D" or r.status == "eof" then return nil end
return true
end,
Repl = function (r)
while r:ReadEvalPrint() do end
end,
},
}
repl = function () Repl{}:Repl() end
-- dump-to: tests
--[==[
* (eepitch-lua51)
* (eepitch-kill)
* (eepitch-lua51)
-- dofile "eoo.lua"
dofile "repl2.lua"
repl()
= 2, 3, 4
= 2, 3 +
4
= !!!
= (2 +
3 +
4)
= (2 +
3
+ 4)
foo(bar)
print(2, 3)
print(2,
^C
^C
^D
--]==]
-- Local Variables:
-- coding: raw-text-unix
-- ee-anchor-format: "«%s»"
-- End: