Chapa 1)


Repl3.lua: an eepitch-friendly REPL for Lua

Eepitch is explained here.
My main page about Lua is here.
   Announcements: eev, lua-l.


1. Introduction

Lua comes with two default repls: its "interactive mode" and debug.debug(). The interactive mode supports multi-line commands and the prefix trick, in which commands like "= exprs" are translated to "print(exprs)", and debug.debug() is as bare as possible: it doesn't support multi-line commands or the prefix trick, and it also doesn't print tracebacks on errors. Try:

print(2, 3+
=     2, 3+
f = function (a) return 10 + a end

You will get this:

Most of the other repls for Lua suppose that the users will run them in a terminal, and so they focus on features like editing and completion. One of the most popular is Rob Hoelz's lua-repl; I used it for a while, and I even mentioned it in the slides and in the article about Dednat6, but I've never been able to understand its code, so after some time I gave up using it and started to work on repls that wouldn't have any "black boxes", in this sense...

2. Tracebacks: Prosody, PrintFunction, find-luatb

One of the first things that I wanted in my repl was a better way of printing tracebacks. In mar/2022 I sent this e-mail to lua-l asking for reimplementations in Lua of debug.traceback(), and Matthew Wild pointed to the function _traceback in Prosody (notes); I rewrote that code as the class Prosody in Prosody1.lua, and after some time I rewrote that class completely to change its output; the result was PrintFunction1.lua. The screenshot at the left below compares their outputs - its test block is here - and the screenshot at the right shows what happens when we follow one of the `find-luatb's (an elisp hyperlink):

3. PrintFunction1.lua

Here are some technical details. The file PrintFunction1.lua defines only one class: PrintFunction, that implements several ways of printing a description of function - or, more precisely, several ways of printing the table returned by a debug.getinfo(o, "nSluf"), where o is either a function or an integer that represents the level of the desired stack frame. The default output format for objects of the class PrintFunction uses elisp hyperlinks when possible, like this,

(find-luatb "~/LUA/lua50init.lua 2324 2328 global run_repl3_now")

but the method :v() converts a PrintFunction object into a VTable object, that is printed like this:

{ "currentline"=2328,
  "func"=<function: 0x560939fc8770>,

4. Pretracebacks

A call to debug.traceback() returns a traceback - i.e. "a low-resolution picture of the current stack frames" - as a string. I will use the term "pretraceback" to refer to "a low-resolution picture of the current stack frames" before it is converted to a string.

When we are inside the repl defined in Repl3.lua, errors make the call stack be saved in an object of the class PreTraceback, and then that object is printed in the default way, that looks like this:

15 -> [ C ]  C function (unknown name)
14 -> [Lua] stdin line 1
13 -> (find-luatb "~/LUA/lua50init.lua 2324 2328 global run_repl3_now")
12 -> (find-luatb "~/LUA/Repl3.lua 143 145 method repl")
11 -> (find-luatb "~/LUA/Repl3.lua 136 140 method readevalprint")
10 -> (find-luatb "~/LUA/Repl3.lua 129 134 method evalprint")
9 -> (find-luatb "~/LUA/XPCall1.lua 97 97 method xpcall6")
8 -> (find-luatb "~/LUA/XPCall1.lua 75 85 method xb")
7 -> [ C ] global C function "xpcall"
6 -> [Lua] tail call
5 -> [Lua] [string "  print(2+nil)"] line 1
4 -> (find-luatb "~/LUA/XPCall1.lua 78 82")
3 -> (find-luatb "~/LUA/PreTraceback1.lua 156 157 field new")
2 -> (find-luatb "~/LUA/PreTraceback1.lua 110 113 field getframes")
1 -> (find-luatb "~/LUA/PreTraceback1.lua 95 96 field atlevel")
0 -> [ C ] field C function "getinfo"

If the global variable ptb was empty when that error happened, then that pretraceback will be saved there.

5. Testing code inside test blocks

Here is an interesting example. I wrote this section in 2024jan28, and you can try that version of the code here.

I'm not 100% happy with the current version of my class PrintFunction, and here's how I'm trying to make it better. I executed this test block - Repl-tests - in three parts delimited by dashed lines, and took a screenshot after the end of each part:

In the part between the dashed lines I redefined the method _shortsrc of the class PrintFunction, and then printed the same pretraceback as before - the one in the global variable ptb - again; this used the new _shortsrc, and so the output of "= ptb" was slightly different than before; in the second screenshot we can see that the output of the "= ptb[5]" near the top was this, that dropped the "p" of the "print" and used two lines,

>>> = ptb[5]      -- fix this
[Lua] rint(2+
 nil)         -- shows a traceback. Bad frames: 14,13,5. line 2

and after the redefinition of _shortsrc the stack frame 5 of ptb was shown in this way, that is much better:

5 -> [Lua] string print(2+... line 2

This fixed the stack frame 5. I was still not happy with the output for the stack frames 14 and 13, so I put this after the last dashed line:

= ptb[13]:v()
= ptb[14]:v()

these lines print the stack frames 14 and 13 in vertical/verbose mode, and at some point I will use them to see how to make more changes to _shortsrc, or to other methods in the class PrintFunction. Note that the experimental code - the new _shortsrc - is commented out... it is only executed - in a repl! - when I play with that test block.

6. Try it!

rm -Rfv /tmp/repl3/
mkdir   /tmp/repl3/
cd      /tmp/repl3/
cp -v ~/LUA/{lua50init,PreTraceback1,PrintFunction1,XPCall1,Repl3}.lua .
wget http://anggtwu.net/LUA/lua50init.lua
wget http://anggtwu.net/LUA/PreTraceback1.lua
wget http://anggtwu.net/LUA/PrintFunction1.lua
wget http://anggtwu.net/LUA/XPCall1.lua
wget http://anggtwu.net/LUA/Repl3.lua

 Make `find-repl3', LUA_INIT and LUA_PATH point to /tmp/repl3/:
 (code-c-d "repl3"   "/tmp/repl3/" :anchor)
 (setenv "LUA_INIT" "@/tmp/repl3/lua50init.lua")
 (setenv "LUA_PATH"  "/tmp/repl3/?.lua;;")

 If you know how to use test blocks, try this one (with M-e):
 (find-repl3 "Repl3.lua" "Repl-tests")

7. Trying an old version

Here's how to try the version mentioned in the section "Testing code inside test blocks":

rm -Rfv /tmp/show2-elpeg1/
mkdir   /tmp/show2-elpeg1/
cd      /tmp/show2-elpeg1/
git clone https://github.com/edrx/show2-elpeg1 .
git checkout --detach a814fe724
# (find-gitk "/tmp/show2-elpeg1/")

rm -Rfv /tmp/repl3/
mkdir   /tmp/repl3/
cd      /tmp/repl3/

cd      /tmp/show2-elpeg1/LUA/
cp -v {lua50init,PreTraceback1,PrintFunction1,XPCall1,Repl3}.lua /tmp/repl3/

cd      /tmp/repl3/

 Make `find-repl3', LUA_INIT and LUA_PATH point to /tmp/repl3/:
 (code-c-d "repl3"   "/tmp/repl3/" :anchor)
 (setenv "LUA_INIT" "@/tmp/repl3/lua50init.lua")
 (setenv "LUA_PATH"  "/tmp/repl3/?.lua;;")

 If you know how to use test blocks, try this one (with M-e):
 (find-repl3 "Repl3.lua" "Repl-tests")

8. Missing features

My first "serious" repl was Repl1.lua, included in emlua.

  • It had a way to capture all output from a block of Lua code, and return that as a string - that at some point later would be inserted in an Emacs buffer. This was implemented by the class WithFakePrint.
  • It had an interface in which instead of the repl reading input with io.read() we could send an input line to it using r:esend(line), and the repl would return its output as a string. That was implemented by the class EdrxEmacsRepl, and used by emlua-dostring, emlua-dostring+, esend, and eepitch-emlua.
  • My r:esend(line) was implemented in a very primitive way, with several subcases. I heard - maybe here? - that the Fennel repl uses coroutines instead of io.read()... it would be nice to have a similar trick with coroutines in my Repl3, but Fennel is not eepitch-friendly, and in all my attempts to learn it I progressed very slowly. Anyway, my notes on Fennel are here.

TODO: explain how to inspect the local variables and upvalues stored in each stack frame of a pretraceback!

If you think that this is interesting, please send me a "hi"!