Home

Awesome

(parens-8)

A tiny Lisp for your pico-8 carts

Overview

Parens-8 is a tool for bypassing the pico-8 token limit. It takes up 5% of the allowed 8192 tokens, and gives you practically infinite code space in return: store extra code in strings or cart ROM, load it during init, run it like regular Lua code.

Parens-8 is designed for maximum interoperability with Lua. Functions, tables, values and coroutines can be passed and used seamlessly between Lua and parens-8. Think of parens-8 as Lua semantics with Lisp syntax.

local a, b, c = parens8("1 2 3")
?(b == 2) -- true

local myfunction = parens8"(fn(x) (print x))"
myfunction(42) -- prints 42

-- use the lua multiline string syntax: [[]]
parens8[[
     (set foo 256)
     (set bar (fn (a b)
          (when a
               (print b)
               (print "a was false or nil")
          )
     ))
]]

bar(foo, "hello") -- prints "hello"
bar(false, "goodbye") -- prints "a was false or nil"

Parens-8 comes with four base builtins:

Note that "foo" is translated into (quote foo) by the parser. You can also create Lua arrays and nil this way: (quote (1 2 3)), (quote)

While it's possible to write an entire game in parens-8, it's best to keep most of your code as plain Lua. Use parens-8 for code that you know is stable, and where performance isn't critical. We'll elaborate on performance and use cases further ahead.

Parens-8 versions

Parens-8 comes in three different versions:

v1 is an interpreter that evaluates the AST as it runs the code. This makes it the lightest version in terms of token usage.

v2 is substantially faster than v1: it compiles the AST into closures that perform only the required computations. Think of it as a bytecode compiler that compiles to Lua closures instead of a portable binary.

v3 takes the compiler one step further and compiles scope lookup into proper locals, upvalues, and globals, instead of performing scope lookup at runtime.

If you're unsure of which version to pick, use v3. It has the best overall performance and the most features, at the cost of slightly heavier token cost. v3 is the version that most closely reproduces the semantics of Lua.

Performance

For the following Lua and parens-8 snippets:

function fun(cond, a, b, c)
     if cond then
          return pack(a, b, c)
     end
end
(set fun (fn (cond a b c)
     (when cond (pack a b c))
))

Running the fun implementations for each language gives the following.

languagecyclescycles / nativenative / cycles
native lua291100%
parens-8 v31164.025%
parens-8 v21936.615%
parens-8 v133011.38.7%

For more details, see this document.

Extensions

While designed as a lightweight runtime for offloading code to strings and ROM, parens-8 has extensions to turn it into a fully featured programming language.

; comments! (disabled by default)

; operators
(set fib (fn (x)
     (when (< x 2)
          x
          (+ (fib (- x 1))
             (fib (- x 2)))
     )
))

; table constructors
(set mytable (table (a 1) (b 2) (c 3) 4 5 6))

; loops
(for ((k v) (pairs mytable))
     (print (.. k (.. ": " (.. v (.. " -> " (fib v)))))))

; field access, let, seq
(set mytable.b 42)
(let ((print print) (value 256)) (env mytable (seq
     (print b)     ; 42
     (set b value)
     (print b)     ; 256
)))
(print b)          ; nil
(print mytable.b)  ; 256

; variadics
(set add_all (fn (head ...)
     (when head (+ head (add_all ...)) 0)
))
(print (add_all 1 2 3 4 5)) ; 15

Extensions can be found in v*/builtin/. Parens-8 v3 with all builtin extensions enabled is 941 tokens. The field and variadics syntax extensions are enabled separately by including v3/parens8_field.lua (547 tokens) or v3/parens8_variadics.lua (524 tokens) instead of v3/parens8.lua (495 tokens).

Remember: it's unlikely you will need all extensions. Pick a few ones you know will be useful for your use case, and make sure you get as much mileage as you can out of them. About half of parens-8 v3 is written in parens-8, with only the four core builtins, no extensions.

ROM utilities

If (when) you run out of chars in your cart's code, you can store more code in the ROM of other carts. This is easily done via the utilities found in parens-8/rom-utils/:

The function write_module takes care of all of the above, with multiple parens-8 snippets to be saved to ROM, and copies the load/run lua code into a .p8l file:

write_module("game_logic.p8l", 0x0, "game_logic.p8",
[[(print "hello, I'm a piece of code.")]],
[[(print "hello, I'm *another* piece of code.")]])

Running the above code will create the file game_logic.p8l with these contents:

parens8[[
(parens8 (readrom 0x0000 0x0024 "game_logic.p8"))
(parens8 (readrom 0x0024 0x002c "game_logic.p8"))
]]

The workflow for writing parens-8 code when you don't have enough chars left in your main cart becomes:

For an example of this in action, check this pico-8 cart.

Limitations

Troubleshooting errors is somewhat challenging, as the language itself makes no attempt at diagnostics. Debugging compiled parens-8 (v2 and v3) is slightly easier, as you can at least tell if something is a syntax error or a runtime error.

You can add some debugging facilities by adding assertions in the call code of parens-8:

-- in parens-8 v3, line 59:
return args and function(frame)
     assert(fun(frame), op .. " was nil")
     return fun(frame)(args(frame))
end or function(frame)
     assert(fun(frame), op .. " was nil")
     return fun(frame)()
end

Just remember to revert those changes when you're done debugging.

' and " can't be escaped in parens-8 strings, but you can use either as quotes:

(print "hello, here's a single quote")
(print 'sure... a single "quote", I think we call this an apostrophe')
(print "don't make fun of me, you can't even say 'can't'")
(print "y'all know the `..` operator exists, right?")

In parens-8 v1 and v2 (fixed in v3!) variables with nil values become invisible, that is:

x = "oops"
parens8[[
((fn (x) (id
     (print x)
     (set x (quote))
     (print x)
)) 42)
]] -- prints "42", then "oops"

This pitfall is extremely easy to run into accidentally, and can be hard to troubleshoot. v3 fixes this issue completely.

Misc

As mentioned above, parens-8 v3 offers optional support for variadics with the ... syntax. Because of the way upvalues work in v3, parameter packs can also be captured, something native Lua can't do!

(set store (fn (...)
     (fn () ...)
))

All versions of parens-8 support the same behavior as Lua for multiple return values. So even without the variadics support, functions like id, select, pack and unpack can and should be leveraged to your advantage.

Most Lisp flavors support some sort of seq or progn builtin for executing sequences of statements. Parens-8 does offer an optional seq builtin if you find yourself writing a lot of imperative code, but the identity function function id(...) return ... end and select are reasonable substitutes if you would rather save on tokens, though seq is significantly faster. id is also useful for returning multiple values:

(set print_foo_then_print_bar
     (fn () (id (print foo)
                (print bar))))
(set swap (fn (a b) (id b a)))

Parens-8, like Lua, supports tail call elimination. This can be leveraged if you plan on foregoing flow extensions:

(set loop (fn (i) (loop (+ i 1)
     (print (.. (stat 0) (.. " " i)))
)))
(loop 1)

Parens-8 v3 offers this pattern as the loop builtin for the price of 1 (one) token. It's the poor man's while loop.

There's a code highlighter. I might make a parens-8 code editor in pico-8 with tools for saving to ROM and such? who knows. It's there. you can try it.

Acknowledgements