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: