Adding malleability to any LÖVE app

Here is a 100-line LÖVE app that provides a bare-bones editing environment for creating LÖVE apps. It serves as a template for bolting on malleability to any pre-existing LÖVE app.

Background

Making software malleable requires a core mechanism of a read-eval-print-loop or a REPL, basically some way to make changes to the running program.

Traditionally, a REPL reads code from a 1D stream of text, runs it, and writes the results to a 1D stream of text. Typically this is a non-graphical experience, and it doesn’t translate well to LÖVE. Instead of the usual sequence of instructions, LÖVE programs are atomized into various handlers that get called by an invisible event loop. Instead of interactively accumulating sequences of instructions, it feels more natural to interactively reload the contents of an editor that might contain definitions for multiple handlers like:

Prior attempts

Until recently, I was aware of two ways to build a REPL in LÖVE:

  1. Evaluate a dynamic string using Lua’s load(). lines.love basically uses this approach. It suffers from the drawback that any errors crash the system and you have to restart. Worse, since lines.love implicitly runs the code on startup, errors persist across restarts. You have to go outside the program to fix things, a non-malleable experience that will stymie a lot more people.

  2. Wrap the load() calls within xpcall() for error recovery. This works, but scripts can no longer call love.draw directly. You have to instead define a new convention. Edit say app.draw, app.mousepressed, etc. and then call each within a xpcall() in love.draw, love.mousepressed, etc. respectively. And code in the editor now must respect this convention. My Freewheeling Apps have so far used this approach.

However, I just discovered a much cleaner approach that lets you forget about the editor and build naive, idiomatic LÖVE apps within the editor.

A new way

Here is the LÖVE app again. The basic idea is to redefine the main event loop to wrap calls to all the love.* handlers in xpcall() with a custom error handler. The error handler restores the love.* handlers to what the REPL needs. As a result, you get a chimerical app that can shape-shift to include whatever scripts you type into the editor, and shape-shift back to the editor at any event of your choosing, or on any error.

Code in the editor can also call out to any libraries included in the app. The membrane between app and scripts is porous.

You can see the raw code in this gist.

Caveats

You can do almost anything LÖVE allows, with one exception: redefining the main event loop at love.run. Any errors there will require restarting the app.

While the app can recover from errors, it doesn’t protect you against infinite loops. It’s just a proof of concept.

In addition to the 100 lines in main.lua, the app includes a standard immediate-mode GUI library for the text editor widget. But I made no changes there, and you could swap it out for something else without breaking the core idea.

Try it out

Here’s a simple script to try typing into the app:

N = 1
function love.draw()
  love.graphics.setColor(1,0,1)
  love.graphics.print(tostring(N), 100,100)
end

function love.update(dt)
  -- burn, baby, burn!
end

function love.mousepressed(x,y, button)
  N = N+1
end

function love.keypressed(key)
  error('back to IDE')
end

Click on the mouse to increment the number on screen. Press any key to go back to the IDE and see how error messages are displayed.



3 Likes

Seems like a nice approach! :smile:

I really need to sit down and try this stuff out properly…!

1 Like

It feels like you’ve found your LÖVE-based stack to be quite well-suited to a variety of different tasks, so that’s great to see.

1 Like