Log In  

Cart #pico_repl-32 | 2022-09-16 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
37

What's this?

A REPL (Read-Eval-Print Loop) for pico-8, in pico-8!

Supports executing all lua & pico-8 statements. (if statements, for loops, functions - you name it)

While my code does its own parsing and execution of the statements, any functions you call are the real pico-8 functions.

Code can be typed in, pasted in, or dropped in from .lua files.

Alternatively, carts saved in .p8.rom format (save mycart.p8.rom in pico-8) can be dropped to the REPL to automatically run them.

What can I do with it?

Type expressions like 1+3/5 or sqrt(-2) to see the expected results. (Well - perhaps unexpected to some in the case of the sqrt)

Type statements or programs like the one below to do whatever you wish. (That one prints all global functions)

for k, v in pairs(_env) do
  if (type(v) == "function") ?k
end

Computations that produce lots of results are automatically paged, computations that do nothing indefinitely can be interrupted via escape (escape works in BBS or SPLORE only)

The special variable "_" is set to the value of the last expression you've executed.

The special variable "_env" is set to the globals table (it's similar to lua's/pico-8's _ENV - which is also supported but requires ctrl+L to type)

Pico-8's globals are not disturbed and are the same as in a blank Pico-8 cart.

You can type "\i=0" to disable all text that "interrupts" the code while its executing, letting the code use up the entire screen. This also disables output paging, but pressing escape to stop execution still works.

What's the point of this? Pico-8 has a REPL already!

The main point of it is that I made it, everything else is up to you.

But for example, you can easily take a look at whatever Pico version happens to be running in the BBS, and see how it differs from your own.

Can I use this to write Pico-8 carts?

No. All this allows you to do is to write and execute some code.

You can't edit sprites/sfx/etc/etc, or export, or import, etc etc etc.

Hopefully that's limited enough to avoid running afoul of any legal concerns.

What are the caveats?

As said, all evaluation other than execution of global functions is implemented by me and may have bugs (feel free to report those), or subtle inconsistencies with real pico8/lua behaviour.

No bugs or missing features are currently known.

How do I copy code into the interpreter?

Easy - just use Ctrl+V to paste and Ctrl+X/C to cut/copy.

Well - cut/copy is currently a bit janky on the BBS - you have to press it twice to get it on the clipboard.

You can also drag & drop a text file to the cart to copy its contents.

And what if I don't have any code?

You can take any cart, use pico-8 to convert it to .p8.rom format (e.g. save mycart.p8.rom) and drag & drop it into the cart to run it immediately.

The cart runs without resetting the system, so odd effects may occur if you run multiple carts without resetting (e.g. via \rst).

If you press escape, you can resume the cart via \cont (or restart - without reset - via '\run')

(Note: .p8 and .p8.png files are not supported - just .p8.rom)

Anything else of interest?

There are some special \-prefixed identifiers which act like special variables:

  • \interrupt (or \i) : set this to 0 to disable anything that might interfere with the running code (e.g. prompts while running code, override of print function, etc.) (default: 1)
  • \flip (or \f) : set this to 0 to disable auto-flipping. Requires \i to be 0 to have any effect. Useful for code that does its own flips but runs too slow under the REPL, resulting in flickering without this option. (default: 1)
  • \repl (or \r) : set this to 0 to disable REPL-specific features like automatic printing of results, '_' and '_env' (as an alias of _ENV). (default: 1)
  • \code (or \c) : a table of the most recently input code (item 1 is the previous code, item 2 is the one before it, and so on. item 0 is the current code). You can use this to store the code you've typed somewhere, e.g. printh(\c[1], "@clip")
  • \max_items (or \mi) : the maximal number of items shown when printing a table. -1 means show all. (default: 10)
  • \hex (or \h) : if true, numbers are printed in hex. (default: false)
  • \precise (or \pr) : if true, numbers are printed in hex if pico8 doesn't print them precisely in decimal. (default: false)
  • \colors (or \cl) : a table of the colors used, customization! color meaning: {output, errors, interrupts, prompt, input, input errors, comments, constants, keywords, punctuation, pico functions, cursor}

As well as some special \-prefixed functions:

  • \exec (or \x) : a function that takes a string, and executes the string as if it were written in the repl's command line. Can take an optional second parameter - the environment to execute in. Further parameters are passed as '...' to the executed code.
  • \eval (or \v) : a function that takes a string, and evaluates the string as a lua expression (with the full power of the repl), returning its result. Can take an optional second parameter - the environment to execute in. Further parameters are passed as '...' to the evaluated code.
  • \compile (or \cm) : a function that takes a string, and compiles it as if it were written in the repl's command line. It returns a function that receives a (required) environment argument and executes the code when called. If the returned function receives further parameters, they are passed as '...' to the compiled code.
  • \tostr (or \ts) : a function that converts a value to a string in the same way as the repl does.
  • \print (or \p) : a function that prints a value in the same way as the repl does.

And even some special \-prefixed commands (identifiers which perform an action when accessed):

  • \reset (or \rst) : completely resets the cart
  • \run : if _init/_draw/_update/etc were defined, runs them as if from the pico mainloop
  • \cont : continues a previous run - similar to \run, except _init is not called.

And some shortcuts:

  • Ctrl+X/C/V - cut/copy/paste.
  • Shift+Enter or Ctrl+B - insert a line-break.
  • Home/End or Ctrl+A/E - move cursor to start/end of line
  • Ctrl+Home/End - move cursor to start/end of input
  • Ctrl+Up/Down - like Up/Down, but only browses through input history.
  • Ctrl+L - enter/exit punycase mode, where holding shift allows typing lowercase/punycase instead of symbols.

Oh, and a puzzle game based on this interpreter can be found HERE.

Commented Source Code

Since the comments in the source code took too many characters and had to be stripped out, the original source code, with the comments, is available here:

Commented Source Code

Just in case the link goes down, it's also available here, though unfortunately without syntax highlighting:

------------------------
-- Prepare globals
------------------------

local g_ENV, my_ENV, globfuncs = _ENV, {}, {}
for k,v in pairs(_ENV) do
    my_ENV[k] = v
    if (type(v) == "function") globfuncs[k] = true
end

local _ENV = my_ENV -- with this, we segregate ourselves from the running code (all global accesses below use _ENV automagically)

g_enable_repl, g_last_value = true

------------------------
-- Utils
------------------------

-- is ch inside str? (if so, returns index)
function isoneof(ch, str)
    for i=1,#str do
        if (str[i] == ch) return i
    end
end

------------------------
-- Tokenize
------------------------

-- escape sequences in strings (e.g. \n -> new line)
local esc_keys, esc_values = split "a,b,f,n,r,t,v,\\,\",',\n,*,#,-,|,+,^", split "\a,\b,\f,\n,\r,\t,\v,\\,\",',\n,\*,\#,\-,\|,\+,\^"
local escapes = {}
for i=1,#esc_keys do escapes[esc_keys[i]] = esc_values[i] end

-- is ch a digit char?
function isdigit(ch)
    return ch >= '0' and ch <= '9'
end
-- is ch a valid identifier char?
function isalnum(ch)
    return ch >= 'A' and ch <= 'Z' or ch >= 'a' and ch <= 'z' or ch == '_' or ch >= '\x80' or isdigit(ch)
end

-- extarct string value from quoted string
-- returns value, end index
function dequote(str, i, strlen, quote, fail)
    local rawstr = ''
    while i <= strlen do
        local ch = str[i]
        if (ch == quote) break
        if ch == '\\' then -- handle escape sequences
            i += 1
            local esch = str[i]
            ch = escapes[esch] -- handle normal escapes
            -- hex escape (e.g. \xff)
            if esch == 'x' then
                esch = tonum('0x'..sub(str,i+1,i+2))
                if (esch) i += 2 else fail "bad hex escape"
                ch = chr(esch)
            -- decimal escape (e.g. \014)
            elseif isdigit(esch) then
                local start = i
                while isdigit(esch) and i < start + 3 do i += 1; esch = str[i] end
                i -= 1
                esch = tonum(sub(str,start,i))
                if (not esch or esch >= 256) fail "bad decimal escape"
                ch = chr(esch)
            -- ignore subsequent whitespace
            elseif esch == 'z' then
                repeat i += 1; esch = str[i] until not isoneof(esch, ' \r\t\f\v\n')
                if (esch == '') fail()
                ch = ''
                i -= 1
            elseif esch == '' then fail() ch='' end
            if (not ch) fail("bad escape: " .. esch) ch=''
        elseif ch == '\n' then
            fail "unterminated string"
            break
        end
        rawstr ..= ch
        i += 1
    end
    if (i > strlen) fail("unterminated string", true)
    return rawstr, i+1
end

-- extracts string value from long bracketed string (e.g. [[string]])
-- returns value, end index
function delongbracket(str, i, strlen, fail, strict)
    if str[i] == '[' then
        i += 1
        local eq_start = i
        while (str[i] == '=') i += 1
        local end_delim = ']' .. sub(str,eq_start,i-1) .. ']'
        local j = #end_delim

        if str[i] == '[' then
            i += 1
            if (str[i] == '\n') i += 1
            local start = i
            while (i <= strlen and sub(str,i,i+j-1) != end_delim) i += 1
            if (i >= strlen) fail()
            return sub(str,start,i-1), i+j
        end
    end
    if (strict) fail "invalid long brackets"
    return nil, i
end

-- converts a string into tokens.
--   if strict is set, errors are thrown if invalid, and comments are ignored
-- returns:
--   array of tokens
--   array of the line each token is found at (for if/while shorthand parsing only)
--   array of token start indices
--   array of token end indices
-- A token is:
--   false for invalid
--   true for comment (unless strict)
--   number for numeric literal
--   string for identifier, keyword, or punctuation
--   table for string literal (table contains a single string at position [1])
function tokenize(str, strict)
    local i, line, start = 1, 1
    local tokens, tlines, tstarts, tends, err = {}, {}, {}, {}

    local function fail(v, ok)
        if (strict) on_compile_fail(v, start)
        err = v and not ok
    end

    -- we support unindexable huge strings up to 64KB (at least as long as pico8 can handle them)
    -- we do this via the below hacks (though it doesn't handle huge tokens over 16KB...)
    local strlen = #str >= 0 and #str or 0x7fff
    while i <= strlen do
        if (i >= 0x4001 and strlen >= 0x7fff) str = sub(str, 0x4001); i -= 0x4000; strlen = #str >= 0 and #str or 0x7fff

        start = i
        local ch = str[i]
        local ws, token
        -- whitespace
        if isoneof(ch, ' \r\t\f\v\n') then
            i += 1; ws = true
            if (ch == '\n') line += 1
        -- comment
        elseif isoneof(ch, '-/') and str[i+1] == ch then
            i += 2
            if (ch == '-' and str[i] == '[') token, i = delongbracket(str, i, strlen, fail)
            if not token then
                while (i <= strlen and str[i] != '\n') i += 1
            end
            if (strict) ws = true else add(tokens, true)
        -- number
        elseif isdigit(ch) or (ch == '.' and isdigit(str[i+1])) then
            local digits, dot = "0123456789", true
            -- hex. number (0x...)
            if ch == '0' and isoneof(str[i+1], 'xX') then digits ..= "AaBbCcDdEeFf"; i += 2
            -- binary number (0b...)
            elseif ch == '0' and isoneof(str[i+1], 'bB') then digits = "01"; i += 2
            end
            while true do
                ch = str[i]
                if ch == '.' and dot then dot = false
                elseif not isoneof(ch, digits) then break end
                i += 1
            end
            token = sub(str,start,i-1)
            if (not tonum(token)) fail "bad number"; token="0"
            add(tokens, tonum(token))
        -- identifier
        elseif isalnum(ch) then
            while isalnum(str[i]) do i += 1 end
            add(tokens, sub(str,start,i-1))
        -- string
        elseif ch == "'" or ch == '"' then
            token, i = dequote(str, i+1, strlen, ch, fail)
            add(tokens, {token})
        -- long-bracket string
        elseif ch == '[' and isoneof(str[i+1], "=[") then
            token, i = delongbracket(str, i, strlen, fail, true)
            add(tokens, {token})
        -- punctuation
        else
            i += 1
            local ch2,ch3,ch4 = unpack(split(sub(str,i,i+2),""))
            if ch2 == ch and ch3 == ch and isoneof(ch,'.>') then
                i += 2
                if (ch4 == "=" and isoneof(ch,'>')) i += 1
            elseif ch2 == ch and ch3 != ch and isoneof(ch,'<>') and isoneof(ch3,'<>') then
                i += 2
                if (ch4 == "=") i += 1
            elseif ch2 == ch and isoneof(ch,'.:^<>') then
                i += 1
                if (ch3 == "=" and isoneof(ch,'.^<>')) i += 1
            elseif ch2 == '=' and isoneof(ch,'+-*/\\%^&|<>=~!') then i += 1
            elseif isoneof(ch,'+-*/\\%^&|<>=~#(){}[];,[email protected]$.:') then
            else fail("bad char: " .. ch) end
            add(tokens, sub(str,start,i-1))
        end
        if (not ws) add(tlines, line); add(tstarts, start); add(tends, i-1)
        if (err) tokens[#tokens], err = false, false
    end
    return tokens, tlines, tstarts, tends
end

------------------------
-- More Utils
------------------------

-- is obj inside table?
function isin(obj, tab)
    for i=1,#tab do
        if (tab[i] == obj) return i
    end
end

-- similar to unpack, except depack(pack(...)) is always ...
function depack(t)
    return unpack(t,1,t.n) -- (unpack defaults to t,1,#t instead)
end

-- copy a table
function copy(t)
    local ct = {}
    for k, v in next, t do ct[k] = v end
    return ct
end

------------------------
-- Parse & Eval
------------------------

-- General information:
-- As we parse lua's grammar, we build nodes, which are merely
-- functions that take e (an environment) as the first arg.
-- Parent nodes call their children nodes, thus forming a sort of tree.

-- An environment (e) is an array of scope tables
-- the scope table at index 0 contains top-level upvalues like _ENV
-- other scope tables contain locals defined within a local statement (*)
-- Thus, upvalues and locals are accessed the same way

-- Expression (expr) parsing returns a (node, setnode, tailcallnode) tuple.
-- node returns the expression's value
-- setnode returns a tuple of the table and key to use for the assignment (**)
-- tailcallnode returns a tuple of the function and args to use for a tail-call
-- setnode and/or tailcallnode are nil if assignment/call is not available

-- Note that functions called from within parse_expr instead return a
-- (node, is_prefix, setnode, tailcallnode) tuple, where is_prefix
-- says whether the node can be used as a prefix for calls/etc.

-- Statement (stmt) parsing returns a (node, is_end) tuple
-- node returns either:
--   nil to continue execution
--   true to break from loop
--   (0, label object) to goto the label object
--   table to return its depack() from the function
--   function to tail-call it as we return from the function
-- node may also be nil for empty statements
-- is_end is true if the statement must end the block

-- (*) We create a table per local statement, instead of per block
--     because by using goto, you can execute a local statement multiple
--     times without leaving a block, each time resulting in a different
--     local (that can be independently captured)

-- (**) It would be much simpler for setnode to do the assignment itself,
--      but it would prevent us from mimicking lua's observable left-to-right
--      evaluation behaviour,  where the assignment targets are evaluated
--      before the assignment values.

-- On that note, we generally mimic lua's observable left-to-right evaluation
-- behaviour, except that we do true left-to-right evaluation, while lua
-- usually evaluates locals (only!) right before the operation that uses them.
-- This difference can be observed if the local is captured by a closure,
--  e.g: local a=1; print(a + (function() a = 3; return 0 end)())

-- anyway:

-- identifiers to treat as keywords instead
local keywords = split "and,break,do,else,elseif,end,false,for,function,goto,if,in,local,nil,not,or,repeat,return,then,true,until,while"

keyword_map = {}
for kw in all(keywords) do keyword_map[kw] = true end

-- is token an assign op (e.g. +=)?
local function is_op_assign(token)
    return type(token) == "string" and token[-1] == '='
end

-- tokens that terminate a block
end_tokens = split 'end,else,elseif,until'

-- parses a string, returning a function
-- that receives a global environment (e.g. _ENV) and executes the code
function parse(str )
    -- tokenize the string first
    local tokens, tlines, tstarts = tokenize(str, true)
    -- ti: the token index we're at
    -- e_len: how many environments deep we are
    -- depth: how many blocks deep we are
    local ti, e_len, depth, func_e_len, loop_depth, func_depth = 1, 0, 0 , 0
    local parse_expr, parse_block
    -- gotos: array of functions to evaluate in order to finalize gotos
    -- locals: maps names of locals to the environment array index where
    --         they're defined
    -- labels: maps names of labels to label objects
    --
    -- both locals and labels use a metatable to simulate a sort-of stack
    -- where pushed maps inherit from all previous maps in the stack and
    -- can be easily popped.
    --
    -- endcb: specifies when to stop shorthand parsing
    local gotos, locals, labels, endcb = {}

    local function fail(err)
        on_compile_fail(err, tstarts[ti-1] or 1)
    end

    -- return a node that returns a constant
    local function const_node(value)
        return function() return value end
    end
    -- return a node that returns the value of a variable
    local function var_node(name)
        local e_i = locals[name]
        if e_i then return function(e) return e[e_i][name] end -- local/upvalue
        else e_i = locals._ENV return function(e) return e[e_i]._ENV[name] end -- global
        end
    end
    -- return a node that returns the values of the vararg arguments
    -- of the current function.
    local function vararg_node()
        local e_i = locals['...']
        if (not e_i or e_i != func_e_len) fail "unexpected '...'"
        return function(e) return depack(e[e_i]["..."]) end
    end
    -- return a setnode that allows assigning to the value of a variable
    local function assign_node(name)
        local e_i = locals[name]
        if e_i then return function(e) return e[e_i], name end -- local/upvalue
        else e_i = locals._ENV return function(e) return e[e_i]._ENV, name end -- global
        end
    end

    -- consume the next token, requiring it to be 'expect'
    local function require(expect)
        local token = tokens[ti]; ti += 1
        if (token == expect) return
        if (token == nil) fail()
        fail("expected: " .. expect)
    end

    -- consume the next token, requiring it to be an identifier
    -- returns the identifier
    local function require_ident(token)
        if (not token) token = tokens[ti]; ti += 1
        if (token == nil) fail()
        if (type(token) == 'string' and isalnum(token[1]) and not keyword_map[token]) return token
        if (type(token) == 'string') fail("invalid identifier: " .. token)
        fail "identifier expected"
    end

    -- if the next token is 'expect', consumes it and returns true
    local function accept(expect)
        if (tokens[ti] == expect) ti += 1; return true
    end

    -- push a new locals map to the locals 'stack'
    local function push_locals()
        locals = setmetatable({}, {__index=locals})
        e_len += 1
    end

    -- pop a locals map from the 'stack'
    local function pop_locals()
        locals = getmetatable(locals).__index
        e_len -= 1
    end

    -- evaluate an array of nodes, returning a pack of results
    -- the last node in the array may return an arbitrary number of results,
    -- all of which are packed.
    local function eval_nodes(e, nodes)
        local results = {}
        local n = #nodes
        for i=1,n-1 do
            results[i] = nodes[i](e)
        end
        if n > 0 then
            local values = pack(nodes[n](e))
            if values.n != 1 then
                for i=1,values.n do
                    results[n + i - 1] = values[i]
                end
                n += values.n - 1
            else
                results[n] = values[1]
            end
        end
        results.n = n
        return results
    end

    -- parses a comma-separated list of elements, each parsed via 'parser'
    local function parse_list(parser)
        local list = {}
        add(list, (parser()))
        while accept ',' do
            add(list, (parser()))
        end
        return list
    end

    -- parse a call expression
    --   node : call target node
    --   method : method to call for method call expression (e.g. a:b())
    --   arg : single argument node (e.g. for a"b" and a{b})
    -- returns (node, is_prefix (true), setnode (nil), tailcallnode)
    local function parse_call(node, method, arg)
        -- parse the arguments
        local args = {}
        if arg then
            add(args, arg)
        elseif not accept ')' then
            while true do
                add(args, (parse_expr()))
                if (accept ')') break
                require ','
            end
        end

        if method then
            return function(e)
                -- call method
                local obj = node(e)
                return obj[method](obj, depack(eval_nodes(e, args)))
            end, true, nil, function(e)
                -- return ingredients for a method tail-call
                local obj = node(e)
                return obj[method], pack(obj, depack(eval_nodes(e, args)))
            end
        else
            return function(e)
                -- call function
                return node(e)(depack(eval_nodes(e, args)))
            end, true, nil, function(e)
                -- return ingredients for a function tail-call
                return node(e), eval_nodes(e, args)
            end
        end
    end

    -- parse a table construction expression (e.g. {1,2,3})
    local function parse_table()
        -- key/value nodes
        local keys, values = {}, {}
        -- splat_i : either #keys if the last item in the table is array-style
        --   (and thus may fill multiple array values), or nil otherwise
        local index, splat_i = 1
        while not accept '}' do
            splat_i = nil

            local key, value
            -- e.g. [a]=b
            if accept '[' then
                key = parse_expr(); require ']'; require '='; value = parse_expr()
            -- e.g. a=b
            elseif tokens[ti+1] == '=' then
                key = const_node(require_ident()); require '='; value = parse_expr()
            -- e.g. b
            else
                key = const_node(index); value = parse_expr(); index += 1; splat_i = #keys + 1
            end

            add(keys, key); add(values, value)

            if (accept '}') break
            if (not accept ';') require ','
        end

        return function(e)
            -- constuct table
            -- note: exact behaviour of # may differ from natively created tables
            local table = {}
            for i=1,#keys do
                if i == splat_i then
                    -- set multiple table elements (e.g. {f()})
                    local key, value = keys[i](e), pack(values[i](e))
                    for j=1,value.n do
                        table[key + j - 1] = value[j]
                    end
                else
                    -- set table element
                    table[keys[i](e)] = values[i](e)
                end
            end
            return table
        end
    end

    -- parse a function expression or statement
    -- is_stmt : true if statement
    -- is_local: true if local function statement
    local function parse_function(is_stmt, is_local)

        -- has_self : function has implicit self arg
        -- setnode : for statements, how to assign the function to a variable
        local name, has_self, setnode

        if is_stmt then
            if is_local then
                -- local function statement
                push_locals()
                name = require_ident()
                locals[name] = e_len
                setnode = assign_node(name)

            else
                -- function statement
                name = {require_ident()}
                -- function name may include multiple .-seprated parts
                while (accept '.') add(name, require_ident())
                -- and may include a final :-separated part
                if (accept ':') add(name, require_ident()); has_self = true

                if #name == 1 then setnode = assign_node(name[1])
                else
                    local node = var_node(name[1])
                    for i=2,#name-1 do
                        local node_i = node -- capture
                        node = function(e) return node_i(e)[name[i]] end
                    end
                    setnode = function(e) return node(e), name[#name] end
                end

            end
        end

        -- parse function params
        local params, vararg = {}
        if (has_self) add(params, 'self')
        require "("
        if not accept ')' then
            while true do
                if (accept '...') vararg = true; else add(params, require_ident())
                if (accept ')') break
                require ','
                if (vararg) fail "unexpected param after '...'"
            end
        end

        -- add function params as locals
        push_locals()
        for param in all(params) do locals[param] = e_len end
        if (vararg) locals['...'] = e_len

        -- parse function's body
        local old_gotos, old_depth, old_e_len = gotos, func_depth, func_e_len
        gotos, func_depth, func_e_len = {}, depth + 1, e_len
        local body = parse_block()
        for g in all(gotos) do g() end -- handle gotos
        gotos, func_depth, func_e_len = old_gotos, old_depth, old_e_len
        require 'end'
        pop_locals()

        return function(e)
            if (is_local) add(e, {})

            -- create the function's environment
            -- note: this is a shallow copy of the environment array,
            --   not of the tables within.
            local func_e = copy(e)
            local expected_e_len = #func_e

            -- this is the actual function created
            local func = function(...)
                local args = pack(...) -- pack args

                -- normally, when a function exits, its environment
                -- ends up the same as it started, so it can be reused
                -- however, if the function didn't exit yet (e.g. recursion)
                -- we create a copy of the environment to use for this call
                local my_e = func_e
                if #my_e != expected_e_len then
                    local new_e = {}
                    for i=0, expected_e_len do new_e[i] = my_e[i] end
                    my_e = new_e
                end

                -- add scope for params 
                local scope = {}
                for i=1,#params do scope[params[i]] = args[i] end

                if (vararg) scope['...'] = pack(unpack(args, #params+1, args.n))

                -- evaluate function body
                add(my_e, scope)
                local retval = body(my_e)
                deli(my_e)

                -- return function result
                if retval then
                    if (type(retval) == "table") return depack(retval) -- return
                    return retval() -- tailcall
                end
            end

            -- assign or return the function
            if (is_stmt) local d,k = setnode(e); d[k] = func else return func
        end
    end

    -- parse a core expression, aka an expression without any suffixes
    -- returns (node, is_prefix, setnode, tailcallnode)
    local function parse_core()
        local token = tokens[ti]; ti += 1
        local arg
        if (token == nil) fail()
        -- nil constant
        if (token == "nil") return const_node()
        -- true constant
        if (token == "true") return const_node(true)
        -- false constant
        if (token == "false") return const_node(false)
        -- number constant
        if (type(token) == "number") return const_node(token)
        -- string constant
        if (type(token) == "table") return const_node(token[1])
        -- table
        if (token == "{") return parse_table()
        -- parentheses (this is NOT an no-op, unlike in most
        --   languages - as it forces the expression to return 1 result)
        if (token == "(") arg = parse_expr(); require ')'; return function(e) return (arg(e)) end, true
        -- unary ops
        if (token == "-") arg = parse_expr(11); return function(e) return -arg(e) end
        if (token == "~") arg = parse_expr(11); return function(e) return ~arg(e) end
        if (token == "not") arg = parse_expr(11); return function(e) return not arg(e) end
        if (token == "#") arg = parse_expr(11); return function(e) return #arg(e) end
        if (token == "@") arg = parse_expr(11); return function(e) return @arg(e) end
        if (token == "%") arg = parse_expr(11); return function(e) return %arg(e) end
        if (token == "$") arg = parse_expr(11); return function(e) return $arg(e) end
        -- function creation
        if (token == 'function') return parse_function()
        -- vararg
        if (token == "...") return vararg_node()
        -- special repl-specific commands
        if (token == "\\") arg = require_ident() return function() return cmd_exec(arg) end, true, function() return cmd_assign(arg) end
        -- identifiers
        if (require_ident(token)) return var_node(token), true, assign_node(token)
        fail("unexpected token: " .. token)
    end

    -- parse a binary operation expression
    local function parse_binary_op(token, prec, left, right_expr)
        local right
        if (token == "^" and prec <= 12) right = right_expr(12); return function(e) return left(e) ^ right(e) end
        if (token == "*" and prec < 10) right = right_expr(10); return function(e) return left(e) * right(e) end
        if (token == "/" and prec < 10) right = right_expr(10); return function(e) return left(e) / right(e) end
        if (token == "\\" and prec < 10) right = right_expr(10); return function(e) return left(e) \ right(e) end
        if (token == "%" and prec < 10) right = right_expr(10); return function(e) return left(e) % right(e) end
        if (token == "+" and prec < 9) right = right_expr(9); return function(e) return left(e) + right(e) end
        if (token == "-" and prec < 9) right = right_expr(9); return function(e) return left(e) - right(e) end
        if (token == ".." and prec <= 8) right = right_expr(8); return function(e) return left(e) .. right(e) end
        if (token == "<<" and prec < 7) right = right_expr(7); return function(e) return left(e) << right(e) end
        if (token == ">>" and prec < 7) right = right_expr(7); return function(e) return left(e) >> right(e) end
        if (token == ">>>" and prec < 7) right = right_expr(7); return function(e) return left(e) >>> right(e) end
        if (token == "<<>" and prec < 7) right = right_expr(7); return function(e) return left(e) <<> right(e) end
        if (token == ">><" and prec < 7) right = right_expr(7); return function(e) return left(e) >>< right(e) end
        if (token == "&" and prec < 6) right = right_expr(6); return function(e) return left(e) & right(e) end
        if ((token == "^^" or token == "~") and prec < 5) right = right_expr(5); return function(e) return left(e) ^^ right(e) end
        if (token == "|" and prec < 4) right = right_expr(4); return function(e) return left(e) | right(e) end
        if (token == "<" and prec < 3) right = right_expr(3); return function(e) return left(e) < right(e) end
        if (token == ">" and prec < 3) right = right_expr(3); return function(e) return left(e) > right(e) end
        if (token == "<=" and prec < 3) right = right_expr(3); return function(e) return left(e) <= right(e) end
        if (token == ">=" and prec < 3) right = right_expr(3); return function(e) return left(e) >= right(e) end
        if (token == "==" and prec < 3) right = right_expr(3); return function(e) return left(e) == right(e) end
        if ((token == "~=" or token == "!=") and prec < 3) right = right_expr(3); return function(e) return left(e) ~= right(e) end
        if (token == "and" and prec < 2) right = right_expr(2); return function(e) return left(e) and right(e) end
        if (token == "or" and prec < 1) right = right_expr(1); return function(e) return left(e) or right(e) end
    end

    -- given an expression, parses a suffix for this expression, if possible
    -- prec : precedence to not go beyond when parsing
    -- isprefix : true to allow calls/etc. (lua disallows it for certain
    --            expression unless parentheses are used, not sure why)
    -- returns (node, is_prefix, setnode, tailcallnode)
    local function parse_expr_more(prec, left, isprefix)
        local token = tokens[ti]; ti += 1
        local right, arg
        if isprefix then
            -- table index by name
            if (token == '.') right = require_ident(); return function(e) return left(e)[right] end, true, function(e) return left(e), right end
            -- table index
            if (token == '[') right = parse_expr(); require ']'; return function(e) return left(e)[right(e)] end, true, function(e) return left(e), right(e) end
            -- call
            if (token == "(") return parse_call(left)
            -- call with table or string argument
            if (token == "{" or type(token) == "table") ti -= 1; arg = parse_core(); return parse_call(left, nil, arg)
            -- method call
            if token == ":" then 
                right = require_ident();
                -- ... with table or string argument
                if (tokens[ti] == "{" or type(tokens[ti]) == "table") arg = parse_core(); return parse_call(left, right, arg)
                require '('; return parse_call(left, right)
            end
        end

        -- binary op
        local node = parse_binary_op(token, prec, left, parse_expr)
        if (not node) ti -= 1
        return node
    end

    -- parse an arbitrary expression
    -- prec : precedence to not go beyond when parsing
    -- returns (node, setnode, tailcallnode)
    parse_expr = function(prec)
        local node, isprefix, setnode, callnode = parse_core()
        while true do
            local newnode, newisprefix, newsetnode, newcallnode = parse_expr_more(prec or 0, node, isprefix)
            if (not newnode) break
            node, isprefix, setnode, callnode = newnode, newisprefix, newsetnode, newcallnode
        end
        return node, setnode, callnode
    end

    -- parse an assignment expression, returning its setnode
    local function parse_assign_expr()
        local _, assign_expr = parse_expr()
        if (not assign_expr) fail "cannot assign to value"
        return assign_expr
    end

    -- parse assignment statement
    local function parse_assign()
        local targets = parse_list(parse_assign_expr)
        require "="
        local sources = parse_list(parse_expr)

        if #targets == 1 and #sources == 1 then return function(e)
            -- single assignment (for performance)
            local d,k = targets[1](e); d[k] = sources[1](e)
        end else return function(e)
            -- multiple assignment (e.g. a,b=c,d)
            local dests, keys = {}, {}
            for i=1,#targets do local d,k = targets[i](e); add(dests,d) add(keys,k) end
            local values = eval_nodes(e, sources)
            -- assign from last to first, per observable lua behaviour
            for i=#targets,1,-1 do dests[i][keys[i]] = values[i] end
        end end
    end

    -- parse op-assignment statement (e.g. +=)
    -- receives the node and setnode of the assignment target
    -- this double evaluation of the assignment target is as per pico-8
    local function parse_op_assign(node, setnode)
        local token = tokens[ti]; ti += 1
        local op = sub(token,1,-2)
        local op_node = parse_binary_op(op, 0, node, function() return parse_expr() end) -- ignore precedence
        if (not op_node) fail "invalid compound assignment"
        return function(e) local d,k = setnode(e); d[k] = op_node(e) end
    end

    -- parse local statement
    local function parse_local()
        if accept 'function' then
            -- local function statement
            return parse_function(true, true)
        else
            local targets = parse_list(require_ident)
            local sources = accept '=' and parse_list(parse_expr) or {}

            push_locals()
            for i=1,#targets do locals[targets[i]] = e_len end

            if #targets == 1 and #sources == 1 then return function(e)
                -- single local (for performance)
                add(e, {[targets[1]] = sources[1](e)})
            end else return function(e)
                -- multiple locals
                local scope = {}
                local values = eval_nodes(e, sources)
                for i=1,#targets do scope[targets[i]] = values[i] end
                add(e, scope)
            end end
        end
    end

    -- set-up endcb for if/while shorthand parsing
    -- allows terminating the parsing of a block at the end of the line
    local function setup_endcb(allowed)
        local line = tlines[ti-1]
        endcb = function() return line != tlines[ti] end
        if (not allowed or endcb()) fail(ti <= #tokens and "bad shorthand" or nil)
    end

    -- parse an 'if' statement
    local function parse_ifstmt()
        local short = tokens[ti] == '('
        local cond = parse_expr()
        local then_b, else_b
        if accept 'then' then
            -- normal if statement
            then_b, else_b = parse_block()
            if accept 'else' then else_b = parse_block(); require "end" -- else
            elseif accept 'elseif' then else_b = parse_ifstmt() -- elseif
            else require "end" end
        else
            -- shorthand if
            setup_endcb(short)
            then_b = parse_block()
            if (not endcb() and accept 'else') else_b = parse_block() -- shorhand if/else
            endcb = nil
        end

        return function(e)
            -- execute the if
            if cond(e) then return then_b(e)
            elseif else_b then return else_b(e)
            end
        end
    end

    -- parse a loop block, updating loop_depth (for break purposes)
    local function parse_loop_block(...)
        local old_depth = loop_depth
        loop_depth = depth + 1
        local result = parse_block(...)
        loop_depth = old_depth
        return result
    end

    -- if retval denotes a break, do not propagate it further
    -- useful when returning from loop blocks
    local function handle_break(retval, label)
        if (retval == true) return -- break
        return retval, label
    end

    -- parse a 'while' block
    local function parse_while()
        local short = tokens[ti] == '('
        local cond = parse_expr()
        local body
        if accept 'do' then
            -- normal while statement
            body = parse_loop_block()
            require 'end'
        else
            -- shorthand while statement
            setup_endcb(short)
            body = parse_loop_block()
            endcb = nil
        end

        return function(e)
            -- execute the while
            while cond(e) do
                if (stat(1)>=1) yield_execute()
                local retval, label = body(e)
                if (retval) return handle_break(retval, label)
            end
        end
    end

    -- parse a repeat/until statement
    local function parse_repeat()
        -- note that the until part can reference
        -- locals declared inside the repeat body, thus
        -- we pop the locals/scopes ourselves
        local block_e_len = e_len
        local body = parse_loop_block(true)
        require 'until'
        local cond = parse_expr()
        while (e_len > block_e_len) pop_locals()

        return function(e)
            -- execute the repeat/until
            repeat
                if (stat(1)>=1) yield_execute()
                local retval, label = body(e)
                if (not retval) label = cond(e) -- reuse label as the end cond

                while (#e > block_e_len) deli(e) -- pop scopes ourselves
                if (retval) return handle_break(retval, label)
            until label -- actually the end cond
        end
    end

    -- parse a 'for' statement
    local function parse_for()
        if tokens[ti + 1] == '=' then
            -- numeric for statement
            local varb = require_ident()
            require '='
            local min = parse_expr()
            require ','
            local max = parse_expr()
            local step = accept ',' and parse_expr() or const_node(1)
            require 'do'

            -- push 'for' local, and parse the body
            push_locals()
            locals[varb] = e_len
            local body = parse_loop_block()
            require 'end'
            pop_locals()

            return function(e)
                -- execute the numeric 'for'
                for i=min(e),max(e),step(e) do
                    if (stat(1)>=1) yield_execute()
                    add(e, {[varb]=i})
                    local retval, label = body(e)
                    deli(e)
                    if (retval) return handle_break(retval, label)
                end
            end
        else
            -- generic 'for' block
            local targets = parse_list(require_ident)
            require "in"
            local sources = parse_list(parse_expr)
            require 'do'

            -- push 'for' locals, and parse the body
            push_locals()
            for target in all(targets) do locals[target] = e_len end

            local body = parse_loop_block()
            require 'end'
            pop_locals()

            return function(e)
                -- execute the generic 'for'
                -- (must synthesize it ourselves, as a generic for's
                --  number of vars is fixed)
                local exps = eval_nodes(e, sources)
                while true do
                    local scope = {}

                    local vars = {exps[1](exps[2], exps[3])}
                    if (vars[1] == nil) break
                    exps[3] = vars[1]
                    for i=1,#targets do scope[targets[i]] = vars[i] end

                    if (stat(1)>=1) yield_execute()
                    add(e, scope)
                    local retval, label = body(e)
                    deli(e)
                    if (retval) return handle_break(retval, label)
                end
            end
        end
    end

    -- parse a break statement
    local function parse_break()
        if (not loop_depth or func_depth and loop_depth < func_depth) fail "break outside of loop"
        return function() return true end
    end

    -- parse a return statement
    -- N.B. lua actually allows return (and vararg) in top-level
    --      this sort-of breaks repuzzle and is confusing/useless in pico,
    --      so we disallow it.
    local function parse_return()

        if tokens[ti] == ';' or isin(tokens[ti], end_tokens) or (endcb and endcb()) then
            -- return no values (represented by us as an empty pack)
            return function() return pack() end
        else
            local node, _, callnode = parse_expr()
            local nodes = {node}
            while (accept ',') add(nodes, (parse_expr()))

            if #nodes == 1 and callnode and func_depth then
                -- tail-call (aka jump into other function instead of returning)
                return function(e) local func, args = callnode(e);
                    if (stat(1)>=1) yield_execute()
                    return function() return func(depack(args)) end
                end
            else
                -- normal return
                return function(e) return eval_nodes(e, nodes) end
            end
        end
    end

    -- parse label statement
    local function parse_label(parent)
        local label = require_ident()
        require '::'
        if (labels[label] and labels[label].depth == depth) fail "label already defined"
        -- store label object
        labels[label] = {e_len=e_len, depth=depth, block=parent, i=#parent}
    end

    -- parse goto statement
    local function parse_goto()
        local label = require_ident()
        local labels_c, e_len_c, value = labels, e_len -- capture labels

        -- the label may be defined after the goto, so process the goto
        -- at function end
        add(gotos, function ()
            value = labels_c[label]
            if (not value) fail "label not found"
            if (func_depth and value.depth < func_depth) fail "goto outside of function"
            -- goto cannot enter a scope
            -- (empty statements at the end of a scope aren't considered a
            --  part of the scope for this purpose)
            local goto_e_len = labels_c[value.depth] or e_len_c
            if (value.e_len > goto_e_len and value.i < #value.block) fail "goto past local"
        end)

        return function()
            if (stat(1)>=1) yield_execute()
            return 0, value
        end
    end

    -- parse any statement
    local function parse_stmt(parent)
        local token = tokens[ti]; ti += 1
        -- empty semicolon
        if (token == ';') return
        -- do-end block
        if (token == 'do') local node = parse_block(); require 'end'; return node
        -- if
        if (token == 'if') return parse_ifstmt()
        -- while loop
        if (token == 'while') return parse_while()
        -- repeat/until loop
        if (token == 'repeat') return parse_repeat()
        -- for loop
        if (token == 'for') return parse_for()
        -- break
        if (token == 'break') return parse_break()
        -- return
        if (token == 'return') return parse_return(), true
        -- local
        if (token == 'local') return parse_local()
        -- goto
        if (token == 'goto') return parse_goto()
        -- label
        if (token == '::') return parse_label(parent)
        -- function
        if (token == 'function' and tokens[ti] != '(') return parse_function(true)
        -- print shorthand
        if token == '?' then
            local print_node, nodes = var_node 'print', parse_list(parse_expr);
            return function (e) print_node(e)(depack(eval_nodes(e, nodes))) end
        end

        -- handle assignments and expressions
        ti -= 1
        local start = ti -- allow reparse
        local node, setnode, callnode = parse_expr()

        -- assignment
        if accept ',' or accept '=' then
            ti = start; return parse_assign()
        -- op-assignment
        elseif is_op_assign(tokens[ti]) then
            return parse_op_assign(node, setnode)
        -- repl-specific print of top-level expression
        elseif depth <= 1 and g_enable_repl then
            return function (e)
                local results = pack(node(e))
                if (not (callnode and results.n == 0)) add(g_results, results)
                g_last_value = results[1]
            end
        -- regular expression statements (must be call)
        else
            if (not callnode) fail "statement has no effect"
            return function(e) node(e) end
        end
    end

    -- parse a block of statements
    -- keep_locals: true to let the caller exit the block themselves
    parse_block = function(keep_locals)
        -- push a new labels map in the labels 'stack'
        labels = setmetatable({}, {__index=labels})
        labels[depth] = e_len

        -- increase depth
        depth += 1
        local block_depth = depth
        local block_e_len = keep_locals and 0x7fff or e_len

        -- parse block statements
        local block = {}
        while ti <= #tokens and not isin(tokens[ti], end_tokens) and not (endcb and endcb()) do
            local  stmt, need_end =  parse_stmt(block)
            if (stmt) add(block, stmt) 
            if (need_end) accept ';'; break
        end

        -- pop any locals pushed inside the block
        while (e_len > block_e_len) pop_locals()
        depth -= 1
        labels = getmetatable(labels).__index

        return function (e)
            -- execute the block's statements
            local retval, label
            local i,n = 1,#block
            while i <= n do

                retval, label = block[i](e)
                if retval then
                    -- handle returns & breaks
                    if (type(retval) != "number") break
                    -- handle goto to parent block
                    if (label.depth != block_depth) break
                    -- handle goto to this block
                    i = label.i
                    while (#e > label.e_len) deli(e)
                    retval, label = nil
                end
                i += 1
            end
            while (#e > block_e_len) deli(e)
            return retval, label
        end
    end

    -- create top-level upvalues
    locals = g_enable_repl and {_ENV=0, _env=0, _=0} or {_ENV=0}
    locals['...'] = 0
    -- parse top-level block
    local root = parse_block()
    if (ti <= #tokens) fail "unexpected end"
    -- handle top-level gotos
    for g in all(gotos) do g() end

    return function(env, ...)
        -- create top-level scope
        local scope = g_enable_repl and {_ENV=env, _env=env, _=g_last_value} or {_ENV=env}
        scope['...'] = pack(...)

        -- execute

        local retval = root{[0]=scope}

        if (retval) return depack(retval)
    end
end

------------------------
-- Output
------------------------

g_show_max_items, g_hex_output, g_precise_output = 10, false, false

-- reverse mapping of escapes
local unescapes = {["\0"]="000",["\014"]="014",["\015"]="015"}
for k, v in pairs(escapes) do 
    if (not isoneof(k, "'\n")) unescapes[v] = k
end

-- create quoted string from a string value
function requote(str)
    local i = 1
    while i <= #str do
        local ch = str[i]
        local nch = unescapes[ch]
        if (nch) str = sub(str,1,i-1) .. '\\' .. nch .. sub(str,i+1); i += #nch
        i += 1
    end
    return '"' .. str .. '"'
end

-- is 'key' representable as an identifier?
function is_identifier(key)
    if (type(key) != 'string') return false
    if (keyword_map[key]) return false
    if (#key == 0 or isdigit(key[1])) return false
    for i=1,#key do
        if (not isalnum(key[i])) return false
    end
    return true
end

-- convert value as a string
-- (more featured than tostr)
function value_to_str(val, depth)
    local ty = type(val)
    -- nil
    if (ty == 'nil') then
        return 'nil'
    -- boolean
    elseif (ty == 'boolean') then
        return val and 'true' or 'false'
    -- number (optionally hex)
    elseif (ty == 'number') then
        if (not g_precise_output) return tostr(val, g_hex_output)
        local str = tostr(val)
        return tonum(str) == val and str or tostr(val,1)
    -- string (with quotes)
    elseif (ty == 'string') then
        return requote(val)
    -- table contents
    elseif (ty == 'table' and not depth) then
        local res = '{'
        local i = 0
        local prev = 0
        -- avoid pairs, as it uses metamethods
        for k,v in next, val do
            if (i == g_show_max_items) res ..= ',<...>' break
            if (i > 0) res ..= ','

            local vstr = value_to_str(v,1)
            if k == prev + 1 then res ..= vstr; prev = k
            elseif is_identifier(k) then res ..= k .. '=' .. vstr
            else res ..= '[' .. value_to_str(k,1) ..']=' .. vstr end
            i += 1
        end

        return res .. '}'
    -- other
    else
        return '<' .. tostr(ty) .. '>'
    end
end

-- convert more results into a string
function results_to_str(str, results)
    if (results == nil) return str -- no new results
    if (not str) str = ''

    local count = min(21,#results)
    for ir=1, count do
        if (#str > 0) str ..= '\n'

        local result = results[ir]
        if type(result) == 'table' then
            local line = ''
            for i=1,result.n do
                if (#line > 0) line ..= ', '
                line ..= value_to_str(result[i])
            end
            str ..= line
        else
            str ..= result
        end
    end

    local new_results = {}
    for i=count+1, #results do new_results[i - count] = results[i] end
    return str, new_results
end

------------------------
-- Console output
------------------------

poke(0x5f2d,1) -- enable keyboard
cls()

g_prompt = "> " -- currently must be valid token!
g_input, g_input_lines, g_input_start = "", 1, 0
g_cursor_pos, g_cursor_time = 1, 20
--lint: g_str_output, g_error_output
g_history, g_history_i = {''}, 1
--lint: g_interrupt, g_notice, g_notice_time
g_abort = false
g_num_output_lines, g_line = 0, 1

g_enable_interrupt, g_enable_autoflip = true, true
g_pal = split "7,4,3,5,6,8,5,12,14,7,11,5"

-- override print for better output
g_ENV.print = function(value, ...)
    if (pack(...).n != 0 or not g_enable_interrupt) return print(value, ...)

    add(g_results, tostr(value))
end

-- suppress pause (e.g. from p, etc.)
function unpause()
    poke(0x5f30,1)
end

-- an iterator over pressed keys
function get_keys()
    return function()
        if (stat(30)) return stat(31)
    end
end

-- walk over a string, calling a callback on its chars
function walk_str(str, cb)
    local i = 1
    local x, y = 0, 0
    if (not str) return i, x, y
    while i <= #str do
        local ch = str[i]
        local spch = ch >= '\x80'
        if (x >= (spch and 31 or 32)) y += 1; x = 0
        if (cb) cb(i,ch,x,y)

        if ch == '\n' then y += 1; x = 0
        else x += (spch and 2 or 1) end
        i += 1
    end
    return i, x, y
end

-- given string and index, return x,y at index
function str_i2xy(str, ci)
    local cx, cy = 0, 0
    local ei, ex, ey = walk_str(str, function(i,ch,x,y)
        if (ci == i) cx, cy = x, y
    end)
    if (ci >= ei) cx, cy = ex, ey
    if (ex > 0) ey += 1
    return cx, cy, ey
end

-- given string and x,y - return index at x,y
function str_xy2i(str, cx, cy)
    local ci = 1
    local found = false
    local ei, ex, ey = walk_str(str, function(i,ch,x,y)
        if (cy == y and cx == x and not found) ci = i; found = true
        if ((cy < y or cy == y and cx < x) and not found) ci = i - 1; found = true
    end)
    if (not found) ci = cy >= ey and ei or ei - 1
    if (ex > 0) ey += 1
    return ci, ey
end

-- print string at position, using color value or function
function str_print(str, xpos, ypos, color)
    if type(color) == "function" then
        walk_str(str, function(i,ch,x,y)
            print(ch, xpos + x*4, ypos + y*6, color(i))
        end)
    else
        print(str and "\^rw" .. str, xpos, ypos, color)
    end
end

-- print code, using syntax highlighting
function str_print_input(input, xpos, ypos)
    local tokens, _, tstarts, tends = tokenize(input) -- tlines not reliable!
    local ti = 1
    str_print(input, xpos, ypos, function(i)
        while ti <= #tends and tends[ti] < i do ti += 1 end

        local token
        if (ti <= #tends and tstarts[ti] <= i) token = tokens[ti]

        local c = g_pal[5]
        if token == false then c = g_pal[6] -- error
        elseif token == true then c = g_pal[7] -- comment
        elseif type(token) != 'string' or isin(token, {"nil","true","false"}) then c = g_pal[8]
        elseif keyword_map[token] then c = g_pal[9]
        elseif not isalnum(token[1]) then c = g_pal[10]
        elseif globfuncs[token] then c = g_pal[11] end

        return c
    end)
end

-- draw (messy...)
function _draw()
    local old_color = peek(0x5f25)
    local old_camx, old_camy = peek2(0x5f28), peek2(0x5f2a)
    camera()

    local function scroll(count)
        cursor(0,127)
        for _=1,count do
            rectfill(0,g_line*6,127,(g_line+1)*6-1,0)
            if g_line < 21 then
                g_line += 1
            else
                print ""
            end
        end
    end

    local function unscroll(count, minline)
        for _=1,count do
            if (g_line > minline) g_line -= 1
            rectfill(0,g_line*6,127,(g_line+1)*6-1,0)
        end
    end

    local function draw_cursor(x, y)
        for i=0,2 do
            local c = pget(x+i,y+5)
            pset(x+i,y+5,c==0 and g_pal[12] or 0)
        end
    end

    local function draw_input(cursor)
        local input = g_prompt .. g_input .. ' '
        local cx, cy, ilines = str_i2xy(input, #g_prompt + g_cursor_pos) -- ' ' is cursor placeholder

        if ilines > g_input_lines then
            scroll(ilines - g_input_lines)
        elseif ilines < g_input_lines then
            unscroll(g_input_lines - ilines, ilines)
        end
        g_input_lines = ilines

        g_input_start = mid(g_input_start, 0, max(g_input_lines - 21, 0))

        ::again::
        local sy = g_line - g_input_lines + g_input_start
        if (sy+cy < 0) g_input_start += 1; goto again
        if (sy+cy >= 21) g_input_start -= 1; goto again

        local y = sy*6
        rectfill(0,y,127,y+g_input_lines*6-1,0)
        if (g_input_lines>21) rectfill(0,126,127,127,0) -- clear partial line
        str_print_input(input,0,y)
        print(g_prompt,0,y,g_pal[4])

        if (g_cursor_time >= 10 and cursor != false and not g_interrupt) draw_cursor(cx*4, y + cy*6)
    end

    -- require pressing enter to view more results
    local function page_interrupt(page_olines)
        scroll(1)
        g_line -= 1
        print("[enter] ('esc' to abort)",0,g_line*6,g_pal[3])

        while true do
            flip(); unpause()
            for key in get_keys() do
                if (key == '\x1b') g_abort = true; g_str_output = ''; g_results = {}; return false
                if (key == '\r' or key == '\n') g_num_output_lines += page_olines; return true
            end
        end
    end

    ::again::
    local ostart, olines
    if g_results or g_str_output then
        ostart, olines = str_xy2i(g_str_output, 0, g_num_output_lines)
        if olines - g_num_output_lines <= 20 and g_results then -- add more output
            g_str_output, g_results = results_to_str(g_str_output, g_results)
            ostart, olines = str_xy2i(g_str_output, 0, g_num_output_lines)
            if (#g_results == 0 and not g_interrupt) g_results = nil
        end
    end

    if (not g_interrupt) camera()

    if (g_num_output_lines == 0 and not g_interrupt) draw_input(not g_str_output)

    if g_str_output then
        local output = sub(g_str_output, ostart)
        local page_olines = min(olines - g_num_output_lines, 20)

        scroll(page_olines)
        str_print(output,0,(g_line - page_olines)*6,g_pal[1])

        if page_olines < olines - g_num_output_lines then
            if (page_interrupt(page_olines)) goto again
        else
            local _, _, elines = str_i2xy(g_error_output, 0)
            scroll(elines)
            str_print(g_error_output,0,(g_line - elines)*6,g_pal[2])

            if g_interrupt then
                g_num_output_lines += page_olines
            else
                g_input, g_input_lines, g_input_start, g_cursor_pos, g_num_output_lines, g_str_output, g_error_output =
                    '', 0, 0, 1, 0
                draw_input()
            end
        end
    end

    if g_interrupt then
        scroll(1)
        g_line -= 1
        print(g_interrupt,0,g_line*6,g_pal[3])
    end

    if g_notice then
        scroll(1)
        g_line -= 1
        print(g_notice,0,g_line*6,g_pal[3])
        g_notice = nil
    end

    if g_notice_time then
        g_notice_time -= 1
        if (g_notice_time == 0) g_notice, g_notice_time = ''
    end

    g_cursor_time -= 1
    if (g_cursor_time == 0) g_cursor_time = 20

    color(old_color)
    camera(old_camx, old_camy)
    if (g_line <= 20) cursor(0, g_line * 6)
end

------------------------
--- Execution loop
------------------------

g_in_execute_yield, g_in_mainloop, g_from_flip = false, false, false
g_pending_keys = {}
--lint: g_results, g_error, g_error_idx

-- report compilation error
-- an error of nil means code is likely incomplete
function on_compile_fail(err, idx)
    g_error, g_error_idx = err, idx
    assert(false, err)
end

-- execute code
function execute_raw(line, env, ...)
    return parse(line)(env or g_ENV, ...)
end

-- evaluate code
function eval_raw(expr, env, ...)
    return execute_raw("return " .. expr, env, ...)
end

-- try parse code
function try_parse(line)
    local cc = cocreate(parse)
    ::_::
    local ok, result = coresume(cc, line)
    if (ok and not result) goto _ -- this shouldn't happen anymore, but does (pico bug?)
    if (not ok) result, g_error = g_error, false
    return ok, result
end

function pos_to_str(line, idx)
    local x, y = str_i2xy(line, idx)
    return "line " .. y+1 .. " col " .. x+1
end

-- execute code
function execute(line, complete)
    g_results, g_abort, g_error = {}, false, false
    g_in_execute_yield, g_in_mainloop, g_from_flip = false, false, false

    -- create a coroutine to allow the code to yield to us periodically
    local coro = cocreate(function () 
        local results = pack(execute_raw(line))
        if (results.n != 0) add(g_results, results)
    end)
    local _ok, error
    while true do
        _ok, error = coresume(coro)
        if (costatus(coro) == 'dead') break

        -- handle yields (due to yield/flip or periodic)
        if g_enable_interrupt and not g_in_mainloop then
            g_interrupt = "running, press 'esc' to abort"
            _draw(); flip()
            g_interrupt = nil
        else
            if (g_enable_autoflip and not g_in_mainloop and not g_from_flip) flip()
            if (not g_enable_autoflip and holdframe) holdframe()
            g_from_flip = false
        end

        for key in get_keys() do
            if key == '\x1b' then g_abort = true
            else add(g_pending_keys, key) end
        end

        -- abort execution if needed
        if (g_abort) error = 'computation aborted'; break
    end

    if g_error == nil then -- code is incomplete
        if (complete) error = "unexpected end of code" else error, g_results = nil
    end
    if (g_error) error, g_error = g_error .. "\nat " .. pos_to_str(line, g_error_idx)
    g_error_output = error
    g_pending_keys = {}
    return not error
end

-- called periodically during execution
yield_execute = function ()
    -- yield all the way back to us
    g_in_execute_yield = true
    yield()
    g_in_execute_yield = false
end

-- override flip to force a yield_execute
g_ENV.flip = function(...)
    local results = pack(flip(...))
    g_from_flip = true
    yield_execute()
    return depack(results)
end

-- override coresume to handle yield_execute in coroutines
g_ENV.coresume = function(co, ...)
    local results = pack(coresume(co, ...))
    -- propagate yields from yield_execute
    while g_in_execute_yield do
        yield()
        results = pack(coresume(co)) -- and resume
    end
    g_error = false -- discard inner compilation errors (via \x)
    return depack(results)
end

-- override stat so we can handle keys ourselves
g_ENV.stat = function(i, ...)
    if i == 30 then
        return #g_pending_keys > 0 or stat(i, ...)
    elseif i == 31 then
        if #g_pending_keys > 0 then
            return deli(g_pending_keys, 1)
        else
            local key = stat(i, ...)
            if (key == '\x1b') g_abort = true
            return key
        end
    else
        return stat(i, ...)
    end
end

-- simulate a mainloop.
-- NOTE:
--   real mainloop disables time/btnp updates, and also can't be recursed into/quit legally.
--   the below doesn't disable time/btnp updates at all - but that's not important enough for us.
function do_mainloop(env, continue)
    if not continue then
        if (_set_fps) _set_fps(env._update60 and 60 or 30)
        if (env._init) env._init()
    end
    g_in_mainloop = true
    while env._draw or env._update or env._update60 do
        -- if (_update_buttons) _update_buttons() -- this breaks btnp in its current form
        if (holdframe) holdframe()
        if env._update60 then env._update60() elseif env._update then env._update() end
        if (env._draw) env._draw()
        flip()
        g_from_flip = true
        yield_execute()
    end
    g_in_mainloop = false
end

------------------------
-- Cart decompression
------------------------

k_old_code_table = "\n 0123456789abcdefghijklmnopqrstuvwxyz!#%(){}[]<>+=/*:;.,~_"

-- Old code compression scheme - encodes offset+count for repeated code
function uncompress_code_old(comp)
    local code, i = "", 9
    while true do
        local ch = ord(comp, i); i += 1
        if ch == 0 then
            -- any pico8 char
            local ch2 = comp[i]; i += 1
            if (ch2 == '\0') break -- end
            code ..= ch2
        elseif ch <= 0x3b then
            -- quick char from table
            code ..= k_old_code_table[ch]
        else
            -- copy previous code
            local ch2 = ord(comp, i); i += 1
            local count = (ch2 >> 4) + 2 
            local offset = ((ch - 0x3c) << 4) + (ch2 & 0xf)
            for _=1,count do
                code ..= code[-offset]
            end
        end
    end
    return code
end

-- New code compression scheme - also uses move-to-front (mtf) and bit reading
function uncompress_code_new(comp)
    local code, i, shift, mtf = "", 9, 0, {}

    for idx=0,0xff do mtf[idx] = chr(idx) end

    local function getbit()
        local bit = (ord(comp, i) >> shift) & 1
        shift += 1
        if (shift == 8) i += 1; shift = 0
        return bit == 1
    end
    local function getbits(n)
        local value = 0
        for bit=0,n-1 do -- NOT fast
            value |= tonum(getbit()) << bit
        end
        return value
    end

    while true do
        if getbit() then
            -- literal char
            local nbits, idx = 4, 0
            while (getbit()) idx |= 1 << nbits; nbits += 1
            idx += getbits(nbits)

            local ch = mtf[idx]
            code ..= ch

            -- update mtf
            for j=idx,1,-1 do
                mtf[j] = mtf[j-1]
            end
            mtf[0] = ch
        else
            -- copy previous code (usually)
            local obits = getbit() and (getbit() and 5 or 10) or 15
            local offset = getbits(obits) + 1

            if offset == 1 and obits == 15 then
                break -- not an official way to recognize end, but works
            elseif offset == 1 and obits == 10 then
                -- raw block
                while true do
                    local ch = getbits(8)
                    if (ch == 0) break else code ..= chr(ch)
                end
            else
                local count = 3
                repeat
                    local part = getbits(3)
                    count += part
                until part != 7

                for _=1,count do
                    -- we assume 0x8000 isn't a valid offset (pico8 doesn't produce it)
                    code ..= code[-offset]
                end
            end
        end
    end
    return code
end

------------------------
-- Console input
------------------------

--lint: g_ideal_x, g_key_code
g_prev_paste = stat(4)
g_key_time, g_lower = 0, false

poke(0x5f5c,10,2) -- faster btnp

-- return if keyboard key is pressed, using btnp-like logic
function keyp(code)
    if stat(28,code) then
        if (code != g_key_code) g_key_code, g_key_time = code, 0
        return g_key_time == 0 or (g_key_time >= 10 and g_key_time % 2 == 0)
    elseif g_key_code == code then
        g_key_code = nil
    end
end

-- update console input
function _update()
    local input = false

    local function go_line(dy)
        local cx, cy, h = str_i2xy(g_prompt .. g_input, #g_prompt + g_cursor_pos)
        if (g_ideal_x) cx = g_ideal_x
        cy += dy
        if (not (cy >= 0 and cy < h)) return false
        g_cursor_pos = max(str_xy2i(g_prompt .. g_input, cx, cy) - #g_prompt, 1)
        g_ideal_x = cx
        g_cursor_time = 20 -- setting input clears ideal x
        return true
    end

    local function go_edge(dx)
        local cx, cy = str_i2xy(g_prompt .. g_input, #g_prompt + g_cursor_pos)
        cx = dx > 0 and 100 or 0
        g_cursor_pos = max(str_xy2i(g_prompt .. g_input, cx, cy) - #g_prompt, 1)
        input = true
    end

    local function go_history(di)
        g_history[g_history_i] = g_input
        g_history_i += di
        g_input = g_history[g_history_i]
        if di < 0 then
            g_cursor_pos = #g_input + 1
        else
            g_cursor_pos = max(str_xy2i(g_prompt .. g_input, 32, 0) - #g_prompt, 1) -- end of first line
            local ch = g_input[g_cursor_pos]
            if (ch != '' and ch != '\n') g_cursor_pos -= 1
        end
        input = true
    end

    local function push_history()
        if #g_input > 0 then
            if (#g_history > 50) del(g_history, g_history[1])
            g_history[#g_history] = g_input
            add(g_history, '')
            g_history_i = #g_history
            input = true
        end
    end

    local function delchar(offset)
        if (g_cursor_pos+offset > 0) then
            g_input = sub(g_input,1,g_cursor_pos+offset-1) .. sub(g_input,g_cursor_pos+offset+1)
            g_cursor_pos += offset
            input = true
        end
    end

    local function inschar(key)
        g_input = sub(g_input,1,g_cursor_pos-1) .. key .. sub(g_input,g_cursor_pos)
        g_cursor_pos += #key
        input = true
    end

    local ctrl = stat(28,224) or stat(28,228)
    local shift = stat(28,225) or stat(28,229)

    local keycode = -1
    if keyp(80) then -- left
        if (g_cursor_pos > 1) g_cursor_pos -= 1; input = true
    elseif keyp(79) then -- right
        if (g_cursor_pos <= #g_input) g_cursor_pos += 1; input = true
    elseif keyp(82) then -- up
        if ((ctrl or not go_line(-1)) and g_history_i > 1) go_history(-1)
    elseif keyp(81) then -- down
        if ((ctrl or not go_line(1)) and g_history_i < #g_history) go_history(1)
    else
        local key = stat(31)
        keycode = ord(key)

        if key == '\x1b' then -- escape
            if #g_input == 0 then extcmd "pause"
            else g_results, g_error_output = {}; push_history() end
        elseif key == '\r' or key == '\n' then -- enter
            if shift then
                inschar '\n'
            else
                execute(g_input) -- sets g_results/g_error_output
                if (not g_results) inschar '\n' else push_history()
            end
        elseif ctrl and keyp(40) then -- ctrl+enter
            execute(g_input, true); push_history()
        elseif key != '' and keycode >= 0x20 and keycode < 0x9a then -- ignore ctrl-junk
            if (g_lower and keycode >= 0x80) key = chr(keycode - 63)
            inschar(key)
        elseif keycode == 193 then -- ctrl+b
            inschar '\n'
        elseif keycode == 192 then -- ctrl+a
            go_edge(-1)
        elseif keycode == 196 then -- ctrl+e
            go_edge(1)
        elseif keycode == 203 then -- ctrl+l
            g_lower = not g_lower
            g_notice, g_notice_time = "shift now selects " .. (g_lower and "punycase" or "symbols"), 40
        elseif keyp(74) then -- home
            if (ctrl) g_cursor_pos = 1; input = true else go_edge(-1);
        elseif keyp(77) then -- end
            if (ctrl) g_cursor_pos = #g_input + 1; input = true else go_edge(1);        
        elseif keyp(42) then delchar(-1) -- backspace
        elseif keyp(76) then delchar(0) -- del
        end
    end

    local paste = stat(4)
    if (paste != g_prev_paste or keycode == 213) inschar(paste); g_prev_paste = paste -- ctrl+v

    if keycode == 194 or keycode == 215 then -- ctrl+x/c
        if g_input != '' and g_input != g_prev_paste then
            g_prev_paste = g_input; printh(g_input, "@clip");
            if (keycode == 215) g_input = ''; g_cursor_pos = 1;
            g_notice = "press again to put in clipboard"
        else
            g_notice = ''
        end
    end

    if stat(120) then -- file drop
        local str, count = ""
        repeat
            count = serial(0x800,0x5f80,0x80)
            str ..= chr(peek(0x5f80,count))
        until count == 0
        if (not load_cart(str)) inschar(str)
    end

    if (input) g_cursor_time, g_ideal_x = 20
    g_key_time += 1

    unpause()
end

------------------------
-- Main
------------------------

-- my own crummy mainloop, since time() does not seem to update if the regular mainloop goes "rogue" and flips.
function toplevel_main()
    while true do
        if (holdframe) holdframe()
        _update()
        _draw()
        flip()
    end
end

-- Self-test
-- (so I can more easily see if something got regressed in the future (esp. due to pico8 changes))

function selftest(i, cb)
    local ok, error = coresume(cocreate(cb))
    if not ok then
        printh("error #" .. i .. ": " .. error)
        print("error #" .. i .. "\npico8 broke something again,\nthis cart may not work.\npress any button to ignore")
        while (btnp() == 0) flip()
        cls()
    end
end

selftest(1, function() assert(pack(eval_raw "(function (...) return ... end)(1,2,nil,nil)" ).n == 4) end)
selftest(2, function() assert(eval_raw "function() local temp, temp2 = {max(1,3)}, -20;return temp[1] + temp2; end" () == -17) end)

-------------------------------------------------------
-- We're running out of tokens!
-- What to do? Well, we already have an interpreter above,
-- so we might as well as interpret the rest of our code!
--
-- But looking at code inside strings isn't fun, so I'm automatically moving
-- all the below code (after the count::stop) into the $$BELOW$$ string
-- when creating the cart.
-------------------------------------------------------

_ENV.g_ENV = g_ENV -- make g_ENV a global, so it can be accessed by below code
execute_raw("$$BELOW$$", _ENV)
--lint: count::stop

------------------------
-- Special \-commands
------------------------

-- execute a repl-specific command
function cmd_exec(name)
    if isin(name, {"i","interrupt"}) then
        return g_enable_interrupt
    elseif isin(name, {"f","flip"}) then
        return g_enable_autoflip
    elseif isin(name, {"r","repl"}) then
        return g_enable_repl
    elseif isin(name, {"mi","max_items"}) then
        return g_show_max_items
    elseif isin(name, {"h","hex"}) then
        return g_hex_output
    elseif isin(name, {"pr","precise"}) then
        return g_precise_output
    elseif isin(name, {"cl","colors"}) then
        return g_pal
    elseif isin(name, {"c","code"}) then
        local code = {[0]=g_input}
        for i=1,#g_history-1 do code[i] = g_history[#g_history-i] end
        return code
    elseif isin(name, {"cm","compile"}) then
        return function(str) return try_parse(str) end
    elseif isin(name, {"x","exec"}) then
        return function(str, env, ...) execute_raw(str, env, ...) end
    elseif isin(name, {"v","eval"}) then
        return function(str, env, ...) return eval_raw(str, env, ...) end
    elseif isin(name, {"p","print"}) then
        return function(str, ...) g_ENV.print(value_to_str(str), ...) end
    elseif isin(name, {"ts","tostr"}) then
        return function(str) return value_to_str(str) end
    elseif isin(name, {"rst","reset"}) then
        run() -- full pico8 reset
    elseif isin(name, {"run"}) then
        do_mainloop(g_ENV)
    elseif isin(name, {"cont"}) then
        do_mainloop(g_ENV, true)
    else
        assert(false, "unknown \\-command")
    end
end

-- assign to a repl-specific command
function cmd_assign(name)
    local function trueish(t)
        return (t and t != 0) and true or false
    end

    local func
    if isin(name, {"i","interrupt"}) then
        func = function(v) g_enable_interrupt = trueish(v) end
    elseif isin(name, {"f","flip"}) then
        func = function(v) g_enable_autoflip = trueish(v) end
    elseif isin(name, {"r","repl"}) then
        func = function(v) g_enable_repl = trueish(v) end
    elseif isin(name, {"mi","max_items"}) then
        func = function(v) g_show_max_items = tonum(v) or -1 end
    elseif isin(name, {"h","hex"}) then
        func = function(v) g_hex_output = trueish(v) end
    elseif isin(name, {"pr","precise"}) then
        func = function(v) g_precise_output = trueish(v) end
    elseif isin(name, {"cl","colors"}) then
        func = function(v) g_pal = v end
    else
        assert(false, "unknown \\-command assign")
    end

    -- do some trickery to allow calling func upon assignment
    -- (as we're expected to return the assignment target)
    local obj = {__newindex=function(t,k,v) func(v) end}
    return setmetatable(obj, obj), 0
end

------------------------
-- Misc.
------------------------

function load_cart(str)
    -- is this a full rom? (I'm assuming nobody will drop exactly-32kb text files here!)
    local code, full = sub(str, 0x4301)
    if #code == 0x3d00 then
        full = true
        poke(0, ord(str, 1, 0x4300)) -- load rom
    else
        code = str -- else, either tiny-rom or plaintext
    end

    local header = sub(code, 1, 4)
    if header == ":c:\0" then
        code = uncompress_code_old(code)
    elseif header == "\0pxa" then
        code = uncompress_code_new(code)
    elseif full then
        code = split(code, '\0')[1]
    else
        -- either plaintext or a tiny/uncompressed tiny-rom (indistinguishable)
        return
    end

    -- run in ideal execution environment
    g_enable_interrupt, g_enable_repl = false, false
    local ok = execute(code, true)
    g_enable_repl = true
    if (ok) execute("\\run") -- we need to call do_mainloop from within execute, this is the easiest way
    return true
end

toplevel_main()

Changelog

That's not a question.
I mean:

v32:

  • Support drag & drop of carts in .p8.rom format.
  • Added \cont to continue a previous \run
  • Fix btnp under \run
  • Support '//' comments

v31:

  • Support new Pico v0.2.5 '~' alias for '^^'
  • Added \pr to enable precise number printing
  • Allow return and ... at top-level
  • Allow setting ... when calling \x/\v/etc.

v30:

  • Further commented source code (result was too large, so it was placed in this post)
  • Added \ts and \p functions
  • Avoid calling flip twice per frame (Used to make some sense in an earlier pico version)

v29:

  • Syntax checking is now stricter, like in lua/pico-8
  • Fixed scope of repeat-until block to include the 'until' expression
  • Fixed code that combines a method call (':') with an unparenthesized argument
  • Fixed subtle issues with goto
  • Lots of other minor syntax fix-ups
  • Added Ctrl+L for typing punycase, and added _ENV
  • Changed \r=0 to also disable _ and _env (but not _ENV, which is a real language feature)
  • Assigning to _env no longer changes the environment - you need to assign to _ENV instead
  • Added \cm and optional env arg to \x and \v
  • Print compilation error positions

v28:

  • Minor bugfixies
  • Added "\r=0" to disable automatic printing of results

v27:

  • Support for delete, [ctrl+]home/end, shift+enter, and ctrl+up/down keys
  • Support for dropping text files to cart
  • Support printing of P8SCII strings

v26:

  • Hackfix for regression introduced in Pico v0.2.4 (enabled if needed)
  • Support for P8SCII escape codes

v25:

  • Proper support for _env reassignment/redeclaration, just for completion's sake. (_ENV reassignment since v29)

v24:

  • Fixed keyboard input. (stat(30) and stat(31))
  • Fixed output for code that prints something and then hangs for a while.
  • Added "\f=0" to disable auto-flip (also requires "\i=0")

v23:

  • Faster trailing-nil handling using new Pico8 APIs.
  • Self-test on startup. (Will give an error if there's a regression again)
  • Fixed \eval for tail calls

v22:

  • Fixed for regression introduced in Pico v0.2.1 (enabled if needed)

v21:

  • Updated for changed Pico v0.2.0f opcodes. (Now requires v0.2.0f and up)

v20:

  • Fixed for Pico v0.2.0e (no longer relying on pico8's coroutine bug)

v19:

  • Added new Pico v0.2.0d opcodes. (Now requires v0.2.0d and up)
  • Added v0.2.0 while shorthand

v18:

  • Added new Pico v0.2.0 opcodes. (Now requires Pico8 v0.2.0 and up)
  • Changed all @-prefixed identifiers/commands to be \-prefixed instead, now that pico8 stole @ from me.
  • Added \hex

v17:

  • Fixed editing of large inputs

v16:

  • Added \exec & \eval

v15:

  • Fully working trailing-nil handling. (Thanks to JWinslow23 & sparr for ideas in the discord)

v14:

  • Ctrl+A/E as a poor linuxman's home/end

v13:

  • More trailing-nil handling fixes.

v12:

  • Ctrl+B inserts a line break.
  • Added \reset & \run
  • Minor syntax fixies. (Around pico-8's questionably-parsed syntax)

v11:

  • Fix time()/t() not working correctly.

v10:

  • Supported copying/pasting in BBS. (Now that it's supported by pico)
  • Fixed syntax highlighting in long lines.
  • Fixed some... "security holes" in the repl. (__pairs, global overrides)

v9:

  • Syntax highlighting! (Customizable, too - see \cl)

v8:

  • Added \c to allow programmatically accessing code.
  • Added table printing ({1,2,3} instead of <table>), \mi
  • Some minor syntax fixies

v7:

  • Allow escape to bring up the pause menu when not used to cancel input.

v6:

  • Added paste support
  • 2 minor syntax bugfixes

v5:

  • Support goto and tailcalls
  • Fix recursion.

v4:

  • Fix expressions like 'type(nil)'

v3:

  • Support comments, bracket-string literals and all string literal escapes
  • Support dots/colons in function name

v2:

  • Added "\i=0" command to disable "interruptions" while executing code.
  • Fixed nasty glitch that would cause code to sometimes appear to be failing by spamming errors.
  • Fixed paging of 20*k+1 output lines

v1:

  • Original version, support for almost all pico-8/lua syntax.
P#71429 2019-12-26 23:09 ( Edited 2022-09-16 03:22)

This looks exceedingly complex but it clearly has one thing I really like. And that's a full calculator with parentheses ability.

This will definitely be one of my favorite carts as instrumental as it is. Gold star for you, @thisismypassword.

P#71439 2019-12-27 16:31 ( Edited 2019-12-27 16:31)

@xNoname - thanks for giving it a try!
I've tried inputting this cart myself and it seems to work like in the tweet, so perhaps indeed a typo has been made (any ideas on how to add copy/paste-type support in BBS, or something like it? Or even if I should?)

Looking at your animation - by the way - I've added a new "@i=0" command to the REPL cart to optionally disable the "Running, Press 'esc' to abort" text while a cart is executing. :)

P#71443 2019-12-27 22:43 ( Edited 2019-12-27 22:44)

Neat tool. Reminds me of a project I want to work on, implementing a minimal scripting language and multi-cart access and saving. The intent behind what I want to develop is an expansion to headchant's Pico Monsters. :3

P#71444 2019-12-28 00:10

Just updated it with goto and tailcalls too - they were easier to add to my "AST interpreter" than I thought they'd be.

EDIT: Aaaand I ended up adding pasting as well. (Plus something kinda sorta like pasting on the BBS, using - of course - the good old GPIO)

P#71459 2019-12-28 14:13 ( Edited 2019-12-28 15:20)
1

@thisismypassword, May I suggest you use CARTDATA() or CSTORE() to save complex commands so they do not need to be typed in manually again later ?

P#71463 2019-12-28 16:46

O.O

P#71485 2019-12-29 02:50

How do I get back to SPLORE?

P#71488 2019-12-29 07:06
1

@sugarflower - hmm - thinking about it, there's no reason I can't show the pause menu when there's nothing typed in and the user presses escape, so I updated the cart to do just that. Now you can go back to SPLORE! :)

P#71489 2019-12-29 10:43 ( Edited 2019-12-29 10:54)

@dw817 - I've added a "@c" table that allows programmatically accessing the code, so now you can store it wherever you wish. (Just don't chase the correct index too long)

EDIT: Also there now seems to be syntax highlighting for some reason.

P#71511 2019-12-29 23:47 ( Edited 2019-12-30 01:30)

great! :D

P#71756 2020-01-04 17:12

I happened to notice zep added copy/paste support in the BBS now, so I went ahead and made it work better with my cart. (copy's a bit janky (oh well), but paste works great!)

This means there's no more need for the gpio trick! (gpio trick retained for sentimental value)

EDIT: gpio trick later removed for token value

P#72230 2020-01-25 05:40 ( Edited 2022-02-26 14:24)

How does it work ?

@c["print 23"]
@c[0]="print 23"
P#72892 2020-02-08 00:55

@dw817 - @c[1] gives you the previously typed line, for example. (@c[2] gives you the line typed before that, for another).

P#72896 2020-02-08 01:09

Is there a way to do with a string like I listed ? That's what would really be useful.

runcmd["print 23"]

results, 23

P#72897 2020-02-08 01:11

@dw817 - ah, you want a way to execute/evaluate strings at runtime? Sure, that's easy enough - I've added it as @Exec and @eval.
E.g: @Exec("print(23)")

P#72900 2020-02-08 02:35

It's works here:

  @Exec("print 23")

But not here:

  @Exec("print (1+2)*3")

Result is:
Attempt To Call A NIL Value

P#72914 2020-02-08 17:39

@dw817 - you need to put parentheses around the argument you pass to print, of course - same as you would write directly in the repl or in a .p8 file.

So @Exec("print(23)") & @Exec("print((1+2)3)")*

I'm not entirely sure what you're trying to do, by the way - why pass a literal string to @Exec when you can just type the code directly in the repl? Or are you just trying it out?

P#72915 2020-02-08 17:59 ( Edited 2020-02-08 18:03)

Ah ... I see the problem. You can't use the PRINT command with parentheses, you must use ? command.

(1+2)*3 ... 9
        @Exec("(1+2)*3") ... 9
        @Exec"(1+2)*3" ... 9
        @Exec("?(1+2)*3") ... 9
        @Exec"?(1+2)*3" ... 9
        @Exec"print (1+2)*3" ... error
        @Exec("print (1+2)*3") ... error

Yep, I'm trying it out. Want to see if it's possible to calculate complex math entries as a string that is further converted to an @Exec command.

You realize if your cart here runs all Pico-8 commands correctly that with @Exec() you have made an interpreter more powerful than Pico-8 itself. Something definitely worth testing out ...

P#72923 2020-02-09 03:29 ( Edited 2020-02-09 03:49)

The problem is PRINT isn't a command - never was one in lua or pico-8. It's a function, so if you're going to call it - you must use the usual style of calling functions, which normally means using parentheses.

  @Exec"print((1+2)*3)" ... 9
  @Exec("print((1+2)*3)") ... 9

(Though yeah - as you found out, "?" is a command and can be used instead if you prefer)
Oh, and note you can also use @eval to return the computation's result instead of printing it:

print(@eval"(1+2)*3" + 10) ... 19
P#72929 2020-02-09 11:18 ( Edited 2020-02-09 11:19)

@thisismypassword

Not sure if you care about API dependence, but here's a non-PICO-8-specific way to test for empty args:

function empty_args(...)
  return tostr(...) == "[no value]"
end

(Obviously you don't need a whole function call, it's just there to illustrate usage.)


Edit: Actually... this isn't that great. A string arg of "[no value]" would result in apparent emptiness. Hrm.

It's too bad type() doesn't know about "no value".

Never mind.

P#73235 2020-02-19 17:27 ( Edited 2020-02-19 17:34)

@Felice - additionally, while that works on 0.1.12c - in 0.1.12d (on the BBS), print(tostr()) gives "[nil]" instead for some reason (*).

Yeah, I'm not too happy about the type(btn(...)) thing, but it's the best I (and several people on discord) could come up with.
Definitely interested in hearing other suggestions, though - thanks for them.

(*) _tostr still behaves like in 0.1.12c, interestingly, but that's an undocumented 0.1.12d-only function whose presence also seems like a pico-8 bug (setting it to nil breaks pico-8, and I heard it broke at least one person's existing cart)

P#73269 2020-02-20 02:35 ( Edited 2020-02-20 02:38)

@thisismypassword

I did think of one small thing, though I'm not sure it's really an improvement:

isbool={[true]=true,[false]=true}
 :
 :
if isbool[btn(...)] then
  (there are args)
end
P#73332 2020-02-22 04:02

@Felice - it's the btn that's the questionable part, not the type - so I don't see an improvement either.

By the way, if it wasn't for it not working in 0.1.12d, then your previous suggestion could be used reliably as follows:

function empty_args(...)
    return ... == nil and tostr(...) == "[no value]"
end

(The "... == nil" operates on just the first element of the ...)

But alas, undocumented behavior never changes the way you expect...

P#73347 2020-02-22 14:57 ( Edited 2020-02-22 15:05)

Yeah, I know. I just liked the idea of eliminating the type() function call and string compare, but that's my cycle-counting brain at work, which often leads me astray in PICO-8, since all kinda of things that should be slow are fast, and sometimes vice-versa. Table lookups are super fast, but I wouldn't be surprised if a string compare is too and/or type() is free.

P#73386 2020-02-23 17:54

@Felice - ah, thanks - I've added your optimization as part of the latest update.
(A single table lookup is indeed faster than a function call + compare)

EDIT: By the way, for readers from the future, all this exchange is moot nowadays - there are pack, unpack, and select functions that allow easy exact dealing with varargs.

P#74809 2020-04-16 07:54 ( Edited 2022-02-26 14:33)

Sadly, I had always hoped this was built into Pico, so you could shell out and use the command-line as a calculator.

P#75358 2020-04-25 15:43

I wish this made carts bc I can't find out how to make one

P#75999 2020-05-06 23:52

So i made one of @doczi_dominik's tutorial gifs and it worked

P#76182 2020-05-08 17:27

Looks like this cart was pretty much broken since Pico v0.2.1 came out - it added a new parameter to add(), which broke my code that was doing "add(table, func())" with func being a function that returns multiple values. (I was even careful not to rely on that in another part of my code, but unfortunately not in all of it)

Oh well - fixed now.

P#87535 2021-02-12 11:27

I cant actually type any parenthesi types besides normal ones (no brackets or curlies) because AltGr 7,8,9 and 0 (german keyboard) end up doing stuff connected to the gif recorder
what should i do?

P#87562 2021-02-12 23:22

this is really cool, i've been experiementing with trying to make a language in pico8 that'll run fast enough for me to write a full game in it. I didn't think anyone would do a full Lua interpretter!

P#101471 2021-12-04 22:04

You can find a puzzle game based on this interpreter here: LINK

(That also corresponds to the large v29 update, as I found lots of little bugs and things that parsed when they shouldn't while working on that - and fixed them here too, as the 2 carts are based on the same code)

P#109183 2022-03-26 02:32

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2022-12-03 05:47:12 | 0.258s | Q:65