--# Main
--------------------------------------------------------------
--Project: pimp my print
--Version: 0.2
--Author: toffer
--The MIT License (MIT)
--
--Copyright (c) 2014 toffer
--
--Permission is hereby granted, free of charge, to any person obtaining a copy
--of this software and associated documentation files (the "Software"), to deal
--in the Software without restriction, including without limitation the rights
--to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
--copies of the Software, and to permit persons to whom the Software is
--furnished to do so, subject to the following conditions:
--
--The above copyright notice and this permission notice shall be included in all
--copies or substantial portions of the Software.
--
--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
--OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
--SOFTWARE.
--[[ Usage
Import the lib as a dependency
Insert a breakpoint with --@
To step line by line across a function
or a lopp put it next to the function/loop declaration.
To avoid breakpoints in nested function/loop put --! in front of the
declaration/line. (see int setup function of this tab)
!! the monitor params must be enabled
Tap once to resume code execution ]]
-- example
function setup()
-- step on each line of the function
local function dostuff() --@
local x,y = 1,4
for i=1,2 do
x = x + i
y = y + x
end
-- avoid step this loop
for i=1,20 do --!
x = x + i
y = y + x
end
x = y
end
dostuff()
transforms = { rotation = 0, x = WIDTH/2, y = HEIGHT/2 }
tween(5, transforms, { rotation = 360 }, { loop = tween.loop.forever })
wastouched = false
end
function draw()
background(56, 43, 34, 45)
pushMatrix()
translate(transforms.x,transforms.y + math.sin(ElapsedTime) * 100)
rotate(transforms.rotation)
if wastouched then
fill(255, 0, 6, 255)
rect(0,0,100,100) --@ another breakpoint
else
fill(196, 153, 36, 255)
rect(0,0,100,100)
end
popMatrix()
wastouched = false
end
function touched(touch)
if touch.state == BEGAN then
wastouched = true --@
end
if touch.state == 1 then
transforms.x = touch.x
transforms.y = touch.y
end
end
--# pimp
-- pimp my print
local __G,debug = _G,debug
local threads,time,noop = {},0,function() end
local env,log,halt,monitor
local parsetokens
-- Register hooks
-- Base one, output source exerpt and local vars
local function breakpoint(token,pimp,...)
local info = pimp:getinfo()
local chunk,from,to,lines = pimp:prettysource(info,6,6)
clearOutput()
print(string.format("/ %s @%d,%d /",info.tab,from,to,info.name))
print(chunk)
parameter.watch("Locals")
Locals = table.concat(pimp:prettyvars(info),"\n")
return true
end
pimphook = pimphook or {}
pimphook["@"] = breakpoint
local pimp = pimp or {}
-- @internal Print hook
-- trigger hooks callback if a token is found from the first arg
-- if not, print as usual
local function hook(token,...)
local hook = pimphook[token]
if hook and hook(token,pimp,...) then pimp:yield() else log(token,...) end
end
-- Yield the current running thread.
-- Should not be directly called, rather use a positve return from a hook callback to
-- ensure all other operations (print,inspect...) appened before
function pimp:yield(...)
halt = true
coroutine.yield()
end
-- Pause the 'main' loop
-- If it was running, enter in inspect mode targeting the global env
function pimp:pause()
halt = not halt and true or false
if halt then pimp:inspect(env) end
end
function pimp:prettyvars(info)
local l = 1
while debug.getinfo(l,"f").func ~= hook do
l = l + 1
end
local scope = l+1
local i,locals,key,val = 0,{}
while true do
i = i + 1
key,val = debug.getlocal(scope,i)
if not key then
break end
if not string.match(key,"%(") then -- avoid for loops
if type(val) == "table" then
key = {key}
for k,v in pairs(val) do
table.insert(key," "..k.." : "..tostring(v))
end
table.insert(locals,table.concat(key,"\n"))
else
table.insert(locals,key.." : "..tostring(val))
end
end
end
return locals
end
-- Return the tab name and a chunk of the source code where the playhead currently halted
-- An arrow mark the current line
function pimp:prettysource(info,linesbefore,linesafter)
local cur = info.currentline
local prev,next = cur - linesbefore,cur + linesafter
local i,lines = 0,{}
for line in string.gmatch(info.source,"(.-)\n") do
i = i + 1
if i > next then break end
if i >= prev then
line = string.gsub(i == cur and ">"..line or line,"%s%s%s%s"," ")
-- line = string.gsub(line,"%s%s%s%s"," ")
if #line > 35 then line = string.sub(line,1,32).."…" end
table.insert(lines,line)
end
end
return table.concat(lines,"\n"),prev,next,lines
-- return string.gsub(table.concat(lines,"\n"),"print.-%)","")
end
-- Switch print monitoring on/off.
-- If on, all prints called with "special token" are submited to
-- their corresponding hooks callbacks.
function pimp:monitor(flag)
if flag ~= monitor then
monitor = flag
print = monitor and hook or pimp.print
saveProjectData("pimp_monitor",monitor)
end
end
-- Print remaping when not monitoring
pimp.print = function(token,...)
if not pimphook[token] then log(token,...) end
end
-- Return 'adjusted' debug info (as debug.info)
-- at the point where this method get called (assume this one get called from a hook)
-- @return table
-- source : (string) source file (tab)
-- func : (function) current function
-- name : (string) current function name
-- currentline : (int) current line
-- tab : (string) current tab name
-- level : (int) debug level. ie:start point for local vars
function pimp:getinfo()
local l = 1
while debug.getinfo(l,"f").func ~= hook do
l = l + 1
end
l = l + 1
local info = debug.getinfo(l)
info.level = l
if not info.name then
-- TODO: push fn refs in a table and check for others:keyboard..'
local fn = info.func
if fn == env.draw then info.name = "draw"
elseif fn == env.touched then info.name = "touched"
elseif fn == env.setup then info.name = "setup"
end
end
local src = info.source
info.source = string.gsub(string.sub(src,select(2,string.find(src,"%]%s")) + 1),"print.-%)","")
info.tab = string.match(src,"%[%[(.-)%]")
return info
end
local function stepin(fn,...)
-- @internal threads handling
-- @param fn function to carry
-- @param arguments to pass along in
if halt then
return end
local co = threads[fn]
local status = co and coroutine.status(co) or "dead"
if status == "dead" then
co = coroutine.create(fn)
threads[fn] = co
end
local succ,res = coroutine.resume(co,...)
if not succ then
error(res)
end
end
local function instep(fn)
-- Return flag thread ?
local co = threads[fn]
return co and coroutine.status(co) == "suspended" or false
end
-- Bootstrap
local osetup
local function setuphook()
debug.sethook()
-- init stuff
log = print
local mp = readProjectData("pimp_monitor")
if mp == nil then mp = true end
parameter.boolean("monitor",mp,
function(flag)
pimp:monitor(flag)
end)
env = setmetatable({},{ __index = _G })
env._G = env
-- if not pimpnotoken then
-- TODO: dependencies
local read = readProjectTab
for _,tab in ipairs(listProjectTabs()) do
if tab ~= "pimp" then
local chunk = "--[["..tab.."]] "..pimp:parsetokens(read(tab))
setfenv(loadstring(chunk),env)()
end
end
--[[
-- Build hooks table
for k,v in pairs(pimphook) do
pimphook[k] = v.yield end
]]
osetup = env.setup; draw = env.draw; touched = env.touched or noop
setfenv(osetup,env)
setfenv(draw,env)
setfenv(touched,env)
setfenv(tween.update,env)
-- end
local _draw = draw
local backmode
local function dosetup()
stepin(osetup)
if not halt then
backmode = backingMode() == 0 and
function()
local bm = backingMode()
if monitor then
if bm == 0 then
backingMode(1)
end
elseif bm == 1 and not halt then
backingMode(0)
end
end or noop
dosetup = noop
end return true
end
local _touched = touched
local touchstack,touches = {count = -1}
draw = function()
ElapsedTime = time
if dosetup() then
return end
if not halt then
time = time + DeltaTime
-- consume touch stack
if #touchstack > 0 then
for _,v in ipairs(touchstack) do
local touch = not instep(_touched) and table.remove(touchstack,1)
or nil
if type(touch) == "number" then -- draw reached
break end
stepin(_touched,touch)
if halt then break end
end
end
backmode() -- ? do we need that
stepin(_draw)
else
if touchstack.count > -1 then
local n = #touchstack
if n > 1 then
table.insert(touchstack,1)
end
end
end
end
touched = function(touch)
local state = touch.state
-- store touches that could be pushed into eval stack during
-- halt phase
if halt then
if touchstack.count > -1 then
table.insert(touchstack,touch)
touchstack.count = touchstack.count + 1 - state
else
if state == 0 then
halt = nil
end
end
else
if #touchstack == 0 then
stepin(_touched,touch)
if halt then
touchstack.count = 0
end
end
end
end
local _update = tween.update
tween.update = function(dt)
stepin(_update,dt)
end
time = ElapsedTime
end
if not debug.gethook() then
debug.sethook(function(e)
if setup and setup ~= setuphook then
osetup = setup
setup = setuphook
end
end,"r")
end
-- Tokenizer
local find,match,sub,insert = string.find,string.match,string.sub,table.insert
local getoken,nextline,parseblank,parsetable,parsebloc,parseline
local lines,line,cursor,chunk
getoken = function()
local pref,token,opts = match(line,"^(.-)%-%-([@!])(.-)$")
if pref then
if token == "@" then
local params = {'"@"'}
if opts and pimphook then
for key,hk in pairs(pimphook) do
local res = { match(opts,key) }
for _,v in ipairs(res) do
insert(params,'"'..v..'"')
end
end
end
return pref,";print("..table.concat(params,",")..")"
elseif token == "!" then
return pref,""
end
end
end
nextline = function()
local pos = cursor
cursor,line = select(2,find(chunk,"(.-)\n",pos))
if not cursor then -- last line
cursor,line = select(2,find(chunk,"([^\n]+)$",pos))
end
cursor = cursor + 1
local prefix,strhook = getoken()
if prefix then
line = prefix
return parseblank(strhook) or parsetable(strhook)
or parsebloc(strhook) or parseline(strhook)
end
end
parseblank = function(strhook)
if not find(line,"%S") then -- blank line
insert(lines,line)
return true
end
if find(line,"%-%-[^@%[%[]") then
local pref,com = match(line,"(.-)(%-%-.-)$")
if pref and match(pref,"%w") then
insert(lines,pref..strhook..com)
else
insert(lines,line)
end
return true
end
if find(line,"[[",1,true) then
while true do
insert(lines,line)
if find(line,"]]",1,true) then
break
end
nextline()
end
return true
end
end
parseline = function(strhook)
insert(lines,line..strhook)
return true
end
parsetable = function(strhook)
if match(line,"{") then
if match(line,"}") then -- one liner
insert(lines,line..strhook)
return true
end
insert(lines,line)
local nest = 0
while true do
if not nextline() then
if not parseblank(strhook) and not parsebloc(strhook) then
if match(line,"{") then
nest = nest + 1
end
if match(line,"}") then
if nest > 0 then
insert(lines,line)
nest = nest - 1
else
insert(lines,line..strhook)
break
end
else
insert(lines,line)
end
end
end
end
return true
end
end
parsebloc = function(strhook)
-- TODO repeat + better catch
if match(line,"if.-then")
or match(line,"for.-do")
or match(line,"while.-do")
or match(line,"function") then
-- on liner
if match(line,"end") then
local repl = match(line,"return") and "return" or "end"
line = string.gsub(line,repl,strhook.." "..repl)
insert(lines,line)
return true
end
-- insert first line
insert(lines,line)
local res
while true do
if not nextline() then
if match(line,"end") then
if not parsebloc(strhook) then
-- TODO: fancy format like:
-- for i=1,10 do
-- x = x + 1 end
insert(lines,line)
break
end
elseif match(line,"else") then
insert(lines,line)
else
if parseblank(strhook) or parsetable(strhook)
or parsebloc(strhook) or parseline(strhook) then end
end
end
end
return true
end
end
local function nextoken()
local nxt = select(2,find(chunk,"--@",cursor,true))
if nxt then
nxt = select(2,find(sub(chunk,cursor,nxt),".*\n"))
if nxt then
insert(lines,sub(chunk,cursor,cursor + nxt - 2))
cursor = cursor + nxt
end
nextline()
return true
end
local left = sub(chunk,cursor)
if #left > 0 then
insert(lines,left)
end
end
function pimp:parsetokens(src)
chunk,lines,cursor = src,{},1
local hastoken = false
while nextoken() do
hastoken = true
end
if hastoken then
src = table.concat(lines,"\n")
end
chunk,lines = nil,nil
return src,hastoken
end