Log In  

A tool to help with print debugging based on the Python package IceCream. There's already a Lua version of IceCream but it won't work with Pico-8/Picotron as far as I know. I've called it db for debug rather than ic but it's very similar in concept.

You can download the code here or copy/paste below:

db.lua

----------------------------------------------------------------------------
-- print debugging based on IceCream for python
-- pico-8 lua version by jason delaat
do
   local ignore = {}
   local lookup = _ENV
   for k,_ in pairs(_ENV) do
      ignore[k] = true
   end
   local function format_arg(value, env)
      for k,v in pairs(lookup) do
         if v == value and not ignore[k] then
            return 'db: '..k..'='..tostr(v)
         end
      end
      return 'db: '..tostr(value)
   end

   local db_meta = {
      __call=function(self, value, log)
         if db.display and log then
            print(log)
         elseif db.display then
            print(format_arg(value))
         end
         return value
      end
   }
   db = {
      display = true,
      local_env = function(t)
         lookup = setmetatable(t or {}, {__index=_ENV})
         return lookup
      end,
      reset_env = function()
         lookup = _ENV
      end,
      wrap = function(f)
         local fn = sub(split(format_arg(f), '=')[1], 5)
         _ENV[fn] = function(...)
            local log = 'db: '..fn..'('
            local result = f(...)
            for a in all({...}) do
               log ..= tostr(a)..','
            end
            log = sub(log, 1, -2)
            log ..= ') --> '..tostr(result)
            return result, log
         end
      end
   }
   setmetatable(db, db_meta)
end
----------------------------------------------------------------------------

Summary

summary-table.png

Displaying variables

The most basic thing you can do with db is just display values and variables. When displaying variables with db it displays both the variable name and its value.

x = 1
db(2)
db(x)

Output

db: 2
db: x=1
  • Note: Duplicate values
    db creates its output by searching the global environment by key/value pairs looking for a matching value and then displaying it with the corresponding key. This mostly works. But if more than one variable has the same value db may log the wrong one. For example:

    x = 1
    y = 1
    db(x)

    Because the ordering of keys in a table isn't guaranteed this will sometimes (correctly) display db: x=1 but will also sometimes (incorrectly) display db: y=1. db automatically ignores variable/function names defined in the global environment before the debug.lua code—which includes all built-ins—to try to minimize these collisions.

Inline debug output

db returns its input so you can debug variables where they're actually used.

x = 1
y = 2
z = db(x) + db(y)
db(z)

Output

db: x=1
db: y=2
db: z=3

User defined types

Since db is essentially just a fancy print statement it should work properly with any object as long as it has a __tostring meta-method defined.

do
   local vector_meta = {
      __tostring=function(_ENV)
         return 'vec('..x..','..y..')'
      end
   }

   function vector(x, y)
      return setmetatable({x=x, y=y}, vector_meta)
   end
end

v = vector(1, 2)
db(v)

Output

db: v=vec(1,2)

Function calls

Since functions return values, by default db displays a function call as a bare value:

function plus(a, b)
   return a + b
end

db(plus(1, 2))

Output

db: 3

You can use db.wrap to enable functions to output their name, input parameters and output value, like so:

function plus(a, b)
   return a + b
end

db.wrap(plus)
db(plus(1, 2))

Output

db: plus(1,2) --> 3

Selective debug output

The db.display property can be used to turn debug output on and off wherever you want throughout your code. This lets you leave debug statements in place if you're not sure you're done with them yet but only display those parts you're interested in at the moment.

x = 1
y = 2
function plus(a, b)
   return db(x) + db(y)
end

function minus(a, b)
   return db(x) - db(y)
end

db.wrap(plus)
db.wrap(minus)

db.display = false
p = db(plus(x, y))

db.display = true
m = db(minus(x, y))

db(p)
db(m)

Output

db: x=1
db: y=1
db: minus(1,2) --> -1
db: p=3
db: m=-1

In the above example the debug output for plus is not displayed. However, db.display doesn't prevent the plus function from being called or returning a value: You can see via db(p) that p has the correct value.

Debugging functions

The Problem

Consider the following example:

x = 1
y = 2

function plus(a, b)
   local c = 'who am i?'
   db(c)
   return db(a) + db(b)
end

db(plus(x, y))

Output

db: who am i?
db: x=1
db: y=2
db: 3

First notice that the output of the function call is logged simply as db: 3 because the function hasn't been wrapped with db.wrap. But more importantly, db(c) is displaying the string but not the variable name while db(a) and db(b) are displaying as variables x and y respectively. That's becuase a, b and c are all local variables and Lua doesn't give us direct access to those in the same way as globals. Standard Lua has debug tools which can get a hold of those values but we don't have access to those here in Pico-land.

The Solution

If you want/need to debug values within a function use db.local_env and db.reset_env like so:

x = 1
y = 2

function plus(a, b)
   --local _ENV = db.local_env()    -- can be called with no arguments
   local _ENV = db.local_env({a=a, b=b})  -- note the 'local' keyword!
   c = 'who am i?'                       -- note the *lack* of 'local'
   db(c)
   return db(a) + db(b), db.reset_env()
end

db.wrap(plus)
db(plus(x, y))

db(c)
db(x)

Output

db: c=who am i?
db: a=1
db: b=2
db: plus(1,2) --> 3
db: [nil]
db: x=1

db.local_env creates a temporary enviroment and tells db to use that environment for building its output. db.reset_env just tells db to go back to using the global environment. Including the call db.reset_env() as a second return value let's us reset the environment "after" returning from the function.

  • Note: The local keyword
    Importantly _ENV must be defined as local here or you'll overwrite the global environment. Equally importantly, local variables should not be defined with the local keyword or they'll go back to outputting just values without names. These variables are still local though. As you can see in the output db(c), when called outside the function, displays db: [nil]. When you're done debugging you'll probably want to remove that _ENV: be sure to add local where necessary or all your function local variables will suddenly be globals and you'll have whole new errors to hunt down!

Providing a table to db.local_env is optional. It's mainly included to ensure that input parameters display correctly: Since input parameters are actually defined before our temporary environment, they don't exist in it unless we specifically put them there. You can still use those values but they won't display the variable name when logged.

function no_param(a)
   local _ENV = db.local_env()
   return db(a), db.reset_env()
end

no_param(1)

Output

db: 1
P#144207 2024-03-23 12:55


[Please log in to post a comment]