Log In  
Follow
thisismypassword
[ :: Read More :: ]

Cart #zxspectrum-0 | 2023-10-13 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
4

How?

  • It's recommended to run it in Pico-8 with "-displays_x 2 -displays_y 2".
  • If you don't (say, if you run it in the BBS), you'd have to move the mouse around to see the whole window
  • In particular, move the mouse to the bottom-left corner to see the Spectrum prompt

  • You can use the keyboard to type stuff
  • The symbol key is Tab.
  • ZX Spectrum keyboard layout: http://slady.net/Sinclair-ZX-Spectrum-keyboard/layout/
  • ZX Spectrum user manual: https://worldofspectrum.org/ZXBasicManual/
  • Non-spectrum keys like punctuation/backspace automatically press the right shift/symbol combination

  • To load a tape:
  • First type 'j' for load.
  • Then type double-quotes (") twice. On the BBS or on non-standard keyboard layouts, you're better off pressing Tab+P to get double-quotes. (On BBS, '~' might also work - looks like an emscripten bug).
  • Now, press enter.
  • Only after that, drag a tape (in .tap or .tzx format) to Pico-8.
  • Reset Pico-8 if you want to drag another tape.

  • By default, the arrow keys move the cursor. You can instead have them use the Kempston joystick or the Sinclair joysticks in the pause menu.

What?

  • It's a ZX Spectrum emulator
  • It's quite slow. 2-5 times slower than the real thing.
  • I don't know if sound is broken or just too slow to be ok.
  • Tapes with copy protection won't work.

  • By default, tapes load super-quick if using the loading routines in the ROM. (Can be disabled in pause menu)
  • By default, the CPU is underclocked to maintain responsiveness (since pico-8 can't keep up). (Can be disabled in pause menu)

  • The included Spectrum ROM is copyrighted by Amstrad, who have allowed its use by emulators.

Wherefore?

  • The source code won't bite you if you read it. (But it won't lick you, either)

  • Some highlights from the source:
  • Using the pico-8 memory for the z80 registers, to get the "16 bit reg = two 8 bit regs" behaviour for free(-ish).
  • Using fillp + rectfill to draw each 4x4 block on the screen
  • Only drawing modified (or blinking) areas of the vram.
  • Pre-calculating the flag results of every single addition and add-with-carry (subtraction is easily derived from that)
P#135835 2023-10-13 11:49 ( Edited 2023-10-13 12:58)

[ :: Read More :: ]

Currently, there are two ways to access pico8 memory: peek/poke[2/4] and @/%/$

The @/%/$ operators look a bit weird but are faster and better overall.
However, I think their main drawback is that they don't replace the need for poke, causing code that uses them to necessarily be quite inconsistent, imbalanced and weird whenever it needs to combine peeks with pokes.

I wanted to check how hard it would be to add the ability to poke into @/%/$, so I created the following fork of z8lua that implements it:
https://github.com/thisismypassport/z8lua

Here's how it works:

-- simple poke
@0x1000 = 0x1

?@0x1000 -- outputs 1

-- use of $ as both peek and poke (copying 4 bytes of memory)
$0x8000 = $0

-- multiple assignment
@1,%2,$4 = 1,2,4

?@1 -- outputs 1
?%2 -- outputs 2
?$4 -- outputs 4

-- compound assignment - worked automatically
@1 += 0x80

?@1 -- outputs 129 (since previously was 1)

-- poke metatables
meta = setmetatable({}, {
  __peek=function(self) return "peek" end,
  __poke=function(self, value) print("poke "..value) end
})

@meta ..= " works"
-- prints: "poke peek works"

-- reason for case added in check_conflict, for reference
function f(a, b)
  @a, a = b, b
end

f(0x100, 7)
?@0x100 -- outputs 7

-- note: there is a bug in z8lua where @"1" doesn't work.
-- Since pico8 itself doesn't have this bug, I decided not to fix it - not for peek nor for poke.

Comments welcome.

P#134944 2023-09-27 03:53 ( Edited 2023-09-27 04:22)

[ :: Read More :: ]

Cart #resolution-1 | 2023-09-26 | Code ▽ | Embed ▽ | No License
3

Repuzzle hint & solution cart

("Resolution")

What's Repuzzle?

Sounds like you're here by mistake.

Head over to Repuzzle's page in the BBS and don't come back unless and until you're well and truly stuck.

What does this cart contain?

All hints and solutions for Repuzzle - revealable in gradual order.

Whether you need a small pointer for a level or five you're stuck on, or whether you want to see all the solutions - this cart has you covered.

Though it won't make getting to the solutions too pleasant!

Controls?

You can use the standard pico8 controls, or you can use the mouse (Mouse wheel and buttons only. The mouse wheel also works inside popups)

You can press Ctrl+C to copy everything on-screen.

FAQ

Why are the popups so annoying?

To discourage you from viewing the bigger hints & solutions.

Why is navigation so annoying? I keep pressing the wrong buttons.

Same - to discourage you from having too much fun navigating the solutions.

Why is the cart so ugly? white on gray?!

Yes. Same. Discourages you from looking at the hints for too long.

Any more questions?

P#133720 2023-08-30 04:19 ( Edited 2023-09-26 05:15)

[ :: Read More :: ]

I found some confusing behavior when trying to put multiple if/while shorthands on the same line, e.g.:

if (x1) if(x2) print('true')
print('anyway')

This prints 'anyway' only if x1 is true - whereas I would've expected it to print 'anyway', well - anyway.

It looks like line breaks only terminate a single if/while shorthand, causing the next line to be inside the other shorthand.

It used to be that this code was generating a runtime error, but now it just silently does something really unexpected instead.

P#131999 2023-07-15 08:10

[ :: Read More :: ]

Hi @zep, I found today that >><= and <<>= don't work in 0.2.5g (a regression, as they worked in the past for sure - didn't check which version broke them)

E.g:

a=3
a<<>=5
print(a) -- 3 now
P#130293 2023-05-29 20:56 ( Edited 2023-05-29 21:18)

[ :: Read More :: ]

There seems to have been a change in the virtual cpu cost of +=

Previously, in 0.2.4b, both x=x+y and x+=y cost 1 cycles.
Now, in 0.2.5g, x=x+y costs 1 cycle while x+=y costs 2 cycles.
(Where x and y are locals)

The same happens with other operators that cost 1 cycle, e.g. -=/- and &=/&

This feels like a bug since I wouldn't except x+=y to be costlier than x=x+y

Below code shows the perf. difference.

function testme_calib(name, func, calibrate_func, ...)
  -- based on https://www.lexaloffle.com/bbs/?pid=60198#p
  local n = 1024

  -- calibrate
  flip()
  local unused -- i am not sure why this helps give better results, but it does, so.

  local x,t=stat(1),stat(2)
  for i=1,n do
    calibrate_func(...)
  end
  local y,u=stat(1),stat(2)

  -- measure
  for i=1,n do
    func(...)
  end
  local z,v=stat(1),stat(2)

  -- report
  local function c(t0,t1,t2) return(t0+t2-2*t1)*128/n*256/60*256*2 end -- *2 for 0.2.x

  local s=name.." :"
  local lc=c(x-t,y-u,z-v)
  if (lc != 0) s..=" lua="..lc
  local sc=c(t,u,v)
  if (sc != 0) s..=" sys="..sc

  printh(s)
  print(s)
end

function testme(name, func, ...)
  return testme_calib(name, func, function() end, ...)
end

testme("+", function(x,y) x=x+y end, 1, 2)
testme("+=", function(x,y) x+=y end, 1, 2)

P#126995 2023-03-11 20:30 ( Edited 2023-03-11 20:31)

[ :: Read More :: ]

Previously, gonengazit found some infinite token exploits here and zep fixed them, but looks like they're not all gone yet - I stumbled upon one that still happens:

x=[[[[]]
 put your infinite tokens here...
--]]

Seems like it happens because pico8 supports recursive block comments, and uses the same code to also parse long strings recursively.

But lua then parses the long string non-recursively and executes the code that pico8 thought was part of the string.

Pico8 probably should only parse block comments recursively, not long strings.

P#122430 2022-12-15 00:11 ( Edited 2022-12-15 00:12)

[ :: Read More :: ]

I've created a cart minification & linting tool in python - Shrinko8:

https://github.com/thisismypassport/shrinko8

If you don't want to download anything, or want to use a UI - you can use the webapp here:
https://thisismypassport.github.io/shrinko8/

Otherwise, see the github link for details on how to download & use the tool.

Features

  • It can do aggressive minification to reduce the token count, the character count, and the compressed size of a cart, giving meaningfully better results than other known tools like p8tool or GEM.

  • (E.g. in one example, a 81,416 char cart went to 31,213 chars with shrinko8, 35,563 chars with p8tool (though p8tool didn't run on it without having to do some hacks), and 40,813 chars with GEM.)

  • It can do linting, aka reporting of issues that may indicate bugs in your code, such as undefined, unused and duplicate local variables.

  • It supports all pico8 syntax and globals, including modern ones (as of the time of writing, but I do plan to update it when needed).

  • It also supports creating a p8.png file directly, and can actually compress carts better than Pico-8 would.

  • It also now supports declaring constants and replacing constant expressions with their value during minification - including even removing 'if false' blocks - see readme for more info.

Usage

Using the web-app is hopefully self-explanatory.

Example of using the command line: (first creates p8, second creates p8.png, third minifies less but works without changes for any cart)

python shrinko8.py path-to-input.p8 path-to-output.p8 --lint --count --minify
python shrinko8.py path-to-input.p8 path-to-output.p8.png --lint --count --minify
python shrinko8.py path-to-input.p8 path-to-output.p8.png --lint --count --minify-safe-only

More information is in the readme

I've decided to share this tool since people were having trouble using p8tool on their modern carts.

Comments, issues, and contribution are welcome.

Thanks to pancelor for many bug reports, suggestions, and the tool's name.

P#114635 2022-07-22 14:38 ( Edited 2024-04-14 21:26)

[ :: Read More :: ]

While pico8-lua itself can handle even 0x7fff arguments being passed to a function (easy to see via unpack, for example), peek and poke seem to only handle up to 0x2000.

E.g. peek(0x8000, 0x2001) returns 0x2000 values instead of 0x2001
And poke(0x8000, <0x2001 values>) only pokes addresses up to 0x9fff

P#113712 2022-06-27 06:41

[ :: Read More :: ]

Ever since 0.2.4c came to the BBS, copy/paste seems to cause a crash/hang:

  • stat(4) when there is something in the clipboard hangs
  • printh(<..>, "@clip") hangs
P#109803 2022-04-06 21:20 ( Edited 2022-04-06 21:20)

[ :: Read More :: ]

There used to be an issue where coroutines would yield unexpectedly if they took to long to execute.
This was fixed a while back, but I see that for coroutines that themselves run inside coroutines - this still happens.

In the below code, in the 'coroincoro' case, nil is yielded instead of 1, showing that the yield happened sometimes before the "real" yield call.

sleep=function() for i=0,100,0.00002 do end yield(1) end

function test(name, func, ...)
    local coro=cocreate(func, ...)
    local ok,result=coresume(coro)
    print(name..": "..tostr(ok)..", "..tostr(result))
end

test("coroutine", sleep)

local cc = cocreate(function()
    test("coroincoro", sleep)
end)
coresume(cc)
P#109790 2022-04-06 16:46

[ :: Read More :: ]

I noticed some nits about the way tokens are computed:

  • A few assignment operators ( ..= ^= >><= >><= ) seem to cost 2 tokens instead of 1 (all other assignment operators cost 1)

  • There seems to be logic that checks the token before '-' to determine if the '-' is a unary operator or not, but it doesn't seem to check newer operators (like &, |, etc.) and thus -1 is considered 2 tokens in things like "a & -1". (Even though it's 1 token in "a + -1")
P#109319 2022-03-28 15:40 ( Edited 2022-03-28 15:42)

[ :: Read More :: ]

Cart #repuzzle-13 | 2023-08-26 | Code ▽ | Embed ▽ | No License
25

Repuzzle - a Pico-8 Coding Puzzle Game

Insert, replace, delete, and move characters to win over 22 levels, ranging from the simple to the tricky.

Explanation of the basic controls is provided in the game.

Tips:

  • The pico-8 manual and pico-8 wiki can be useful. (both for this game and in general)
  • If you're stuck on one level, try another. The different "sections" each go roughly from easy to hard, with a lot of variance in-between.
  • If you're really really stuck, the hints can help.

Full list of controls:

Browsing:

  • Arrow keys to move between characters
  • Ctrl + Arrow keys to move between words
  • Page Up/Down to move between pages
  • Home/End to move to start/end of line
  • Ctrl + Home/End to move to start/end of code
  • You can also scroll via Mouse Wheel and move to a character via left-click

Switching Mode:

  • Tab to select the next editing mode
  • Ctrl+1/2/3 to select insert/replace/move mode directly (can also use F1/F2/F3 in BBS only)
  • Clicking on a mode with the mouse will select it as well

Editing:

  • Type to insert characters in insert mode
  • Type to replace characters in replace mode
  • Backspace/delete to delete characters in insert and replace modes
  • Backspace/delete to mark characters for moving in move mode
  • Type to move marked characters in move mode
  • Shift + Backspace/delete to undo changes (in all modes)
  • You can type lowercase, uppercase (with Shift), digits/punctuation, and line breaks (Enter)
  • Ctrl + V to paste code from the clipboard (in insert mode only)

Running:

  • Ctrl + Enter to run the code (you can also use Ctrl + B or 'run' in the pause menu)
  • Escape to abort the run
  • Upon error or abort, Tab (or Ctrl+1..6 or left-click) to browse the stack trace
  • If the code did any drawing or printing, you'll also see a 'see output screen' option - browse to it via Tab(/etc) to see the output

Misc.:

  • Ctrl + C to copy all code to the clipboard
  • Escape to access the pause menu (run; restart level; back to menu; see hint)

Uses sprites from:

Misc notes:

  • Some non-english keyboard layouts currently don't work too well with the devkit keyboard in pico-8. Try switching to an english layout if you're having trouble with your own (or try reporting a pico8 bug)
  • This uses the same Pico-8 interpreter as my Pico-8 REPL/Interpreter, available HERE

Changelog

  • Tweak levels 11 & 12 to avoid simple solution.
  • Improve hints on levels 20 & 22
  • Fix holding backspace key being unreliable on large levels.
  • Added mouse support & paste support
  • Made the deleted-due-to-move indicator pink

More Tips

  • If you're really really really truly really stuck, there is a hint & solution cart HERE.
P#109128 2022-03-25 01:18 ( Edited 2023-09-26 05:46)

[ :: Read More :: ]

This issue reproduces for me on multiple browsers (including chrome), and for luchak (on chrome 98).

Warning: this bug deletes all BBS saves - not just the ones of the carts used to reproduce it.

Steps to reproduce:

  1. Load some cart with a save in it, verify that the save works. I used https://www.lexaloffle.com/bbs/?pid=94408#p (you can save/restore via menu)
  2. Open new tab with some other cart. I used https://www.lexaloffle.com/bbs/?pid=108109#p
  3. In this new tab, press play and immediately switch to the old tab
  4. Refresh the old tab and check if your saves are still there. They will not be.
  5. If you want to see your saves again, switch to the new tab and let it load. If you don't care, delete the new tab - your saves are now gone forever.

It looks like when a game is first loading, it deletes the local storage (well, indexed db). Then, a bit later, it restores it.

P#108475 2022-03-12 04:04

[ :: Read More :: ]

It seems when the index parameters are out of bounds, sub now returns the last character in the string, instead of "", which breaks string parsing code.

As an example of incorrect result, sub("a",2,2) returns "a", whereas it's supposed to (and used to) return "".

P#101424 2021-12-04 07:51

[ :: Read More :: ]

New API pack() returns a table with an "n" field of always 0?

?pack(1,2).n -- 0 (expected: 2)
?pack(1,2,nil,3,nil).n -- 0 (expected: 5)

table.pack works as expected in lua.

P#76239 2020-05-09 15:53

[ :: Read More :: ]

I've just seen from the Pico 8 wiki that there's a new while shorthand:
while (cond) actions

And yeah - it works.
But it's not documented anywhere in the manual - not in the shorthand section and not even in the changelog!

P#75025 2020-04-20 06:39

[ :: Read More :: ]

(Applies to verison 0.1.12d rcsd)

  1. Lua's 'tostring' function is exposed now. It converts things to strings differently than tostr, including printing a table's address in memory.

  2. After doing "install_demos()" or "install_games()", something seems to go majorly wrong with pico-8. All nils become... nulls?!
    You can see this by trying to print a non-existing global/table value which would normally print [nil], but now prints [(null)].
    What is the type of a null? It's a null, too! (No, not the string "null" - but a null object itself)
    (It's clearly not a legal lua value)

You can see the above oddities via my https://www.lexaloffle.com/bbs/?tid=36381 cart, if you'd like.

P#71448 2019-12-28 02:32 ( Edited 2019-12-28 09:11)

[ :: Read More :: ]

Cart #pico_repl-35 | 2023-08-25 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
44

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.

Where is the 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 and ch >= '0' and ch <= '9'
end
-- is ch a valid identifier char?
function isalnum(ch)
    return ch and (ch >= 'A' and ch <= 'Z' or ch >= 'a' and ch <= 'z' or isoneof(ch, '_\x1e\x1f') 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 (not esch) fail()
                ch = ''
                i -= 1
            elseif not 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)
    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
    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)
            if (not token) fail "invalid long brackets"
            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,'+-*/\\%^&|<>=~#(){}[];,?@$.:') 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

    -- return whether we're at the end of a statement
    local function at_stmt_end()
        return isin(tokens[ti], end_tokens) or (endcb and endcb(ti))
    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
    -- the extra 'v' argument is used only by op-assignment statements
    local function parse_binary_op(token, prec, left, right_expr)
        local right
        if (token == "^" and prec <= 12) right = right_expr(12); return function(e,v) return left(e,v) ^ right(e) end
        if (token == "*" and prec < 10) right = right_expr(10); return function(e,v) return left(e,v) * right(e) end
        if (token == "/" and prec < 10) right = right_expr(10); return function(e,v) return left(e,v) / right(e) end
        if (token == "\\" and prec < 10) right = right_expr(10); return function(e,v) return left(e,v) \ right(e) end
        if (token == "%" and prec < 10) right = right_expr(10); return function(e,v) return left(e,v) % right(e) end
        if (token == "+" and prec < 9) right = right_expr(9); return function(e,v) return left(e,v) + right(e) end
        if (token == "-" and prec < 9) right = right_expr(9); return function(e,v) return left(e,v) - right(e) end
        if (token == ".." and prec <= 8) right = right_expr(8); return function(e,v) return left(e,v) .. right(e) end
        if (token == "<<" and prec < 7) right = right_expr(7); return function(e,v) return left(e,v) << right(e) end
        if (token == ">>" and prec < 7) right = right_expr(7); return function(e,v) return left(e,v) >> right(e) end
        if (token == ">>>" and prec < 7) right = right_expr(7); return function(e,v) return left(e,v) >>> right(e) end
        if (token == "<<>" and prec < 7) right = right_expr(7); return function(e,v) return left(e,v) <<> right(e) end
        if (token == ">><" and prec < 7) right = right_expr(7); return function(e,v) return left(e,v) >>< right(e) end
        if (token == "&" and prec < 6) right = right_expr(6); return function(e,v) return left(e,v) & right(e) end
        if ((token == "^^" or token == "~") and prec < 5) right = right_expr(5); return function(e,v) return left(e,v) ^^ right(e) end
        if (token == "|" and prec < 4) right = right_expr(4); return function(e,v) return left(e,v) | right(e) end
        if (token == "<" and prec < 3) right = right_expr(3); return function(e,v) return left(e,v) < right(e) end
        if (token == ">" and prec < 3) right = right_expr(3); return function(e,v) return left(e,v) > right(e) end
        if (token == "<=" and prec < 3) right = right_expr(3); return function(e,v) return left(e,v) <= right(e) end
        if (token == ">=" and prec < 3) right = right_expr(3); return function(e,v) return left(e,v) >= right(e) end
        if (token == "==" and prec < 3) right = right_expr(3); return function(e,v) return left(e,v) == right(e) end
        if ((token == "~=" or token == "!=") and prec < 3) right = right_expr(3); return function(e,v) return left(e,v) ~= right(e) end
        if (token == "and" and prec < 2) right = right_expr(2); return function(e,v) return left(e,v) and right(e) end
        if (token == "or" and prec < 1) right = right_expr(1); return function(e,v) return left(e,v) 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 setnode of the assignment target, and uses it to both get and set the value 
    -- (this is to ensure the node is evaluated only once)
    local function parse_op_assign(setnode)
        local token = tokens[ti]; ti += 1
        local op = sub(token,1,-2)
        local node = function(e, v) return v end -- parse_binary_op propagates the value as an extra arg to us
        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, d[k]) 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

    -- start if/while shorthand parsing
    -- allows terminating the parsing of a block at the end of the line
    local function start_shorthand(allowed)
        local line = tlines[ti - 1]
        local prev_endcb = endcb
        endcb = function(i) return line != tlines[i] end
        if (not allowed or endcb(ti)) fail(ti <= #tokens and "unterminated shorthand" or nil)
        return prev_endcb
    end

    -- end shorthand parsing, and verify we haven't exceeded the line
    local function end_shorthand(prev_endcb)
        if (endcb(ti-1)) fail("unterminated shorthand")
        endcb = prev_endcb
    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
            local prev = start_shorthand(short)
            then_b = parse_block()
            if (not endcb(ti) and accept 'else') else_b = parse_block() -- shorhand if/else
            end_shorthand(prev)
        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
            local prev = start_shorthand(short)
            body = parse_loop_block()
            end_shorthand(prev)
        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) outside of functions.
    --      this kinda completely breaks repuzzle, so the repl code in it disallows it.
    local function parse_return()

        if tokens[ti] == ';' or at_stmt_end() 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(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 at_stmt_end() 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

    ::restart::
    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 restart
        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,
        __index=function() return cmd_exec(name) end, -- op-assign needs this
    }
    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.

Uh - I mean:

v35:

  • Updated subtleties of identifiers to match those of Pico8 (well, allow ゛ and ゜)
  • Remove support for nested comments (as Pico-8 did)

v34:

  • Updated subtleties of shorthands to match those of the latest Pico version
  • Support nested comments (Pico-8 oddity)

v33:

  • Updated due to breaking change in Pico v0.2.5d

v32:

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

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 2023-08-25 19:14)

[ :: Read More :: ]

Cart #fairchild001-11 | 2022-02-23 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
24

Update 1 - full speed & better sound.
Update 2 - fix missing mouse cursor.
Update 3 - fix speed decrease in Pico8 0.2.0
Update 4 - support cart/bios drag&drop and function keys.

What is this?

An emulator for the Fairchild Channel F, the first console to use programmable ROMs and a microprocessor.
The console was released in 1975, so don't expect much in the way of graphics, gameplay, or usability.
See Wikipedia for more info.

And what's that game running?

That's a homebrewed (i.e. - NOT original) game made by Blackbird and e5frog. You can find it here.
Any sound glitches are due to the emulator, not the game, by the way. (See below)

It pushes the capabilities of the console to its limits, so its quality is considerably higher than that of the other games for the console.
It's also anachronistic - the original Pac-Man wasn't released until 1980.

And how do I play other games?

In order to play other games, you first need the console's two BIOS files and the ROM for the other game you want to play. (Having these is up to you and they cannot be shared)

If you have those, simply drag & drop them one-by-one into pico-8. (E.g. drop each of the bios files, then drop the cart).
If you want to run the game included in the bios, drop only the 2 bios files, then press the reset button.

By the way, a BIOS is needed since the "BIOS" included with the emulator is a homebrewed one which is only good enough for running a few games which don't use it much.

If you wish to copy the BIOS and ROM to the cart yourself - e.g. via cstore, they should be copied sequentially: BIOS SL31253 or SL90025 to location 0x0, BIOS SL31254 to location 0x400, and the cart to location 0x800. (Just like the Fairchild's own memory layout)

What are the controls?

In "1P" mode (default), the controls are:

  • arrow keys = move
  • Z = push
  • S/F = rotate left/right
  • E/D = push/pull
  • left shift = hold to control player 2 instead of player 1

In "2P" mode, the controls are:

  • arrow keys = move
  • Z + arrow keys = rotate/push/pull
  • S/F/E/D = player 2 move
  • left shift + S/F/E/D = player 2 rotate/push/pull

In both modes, additional controls are:

  • X + left/down/up/right = press console key 1/2/3/4. (can also use the mouse, or F1 through F4)
  • X + Z = switch between 1P/2P modes. (can also use the mouse or F5; only affects the controls)
  • (to reset, can use the mouse or F12)

What's working well?

All functionality is working and most likely bug-free, so all original and homebrew games work well, including the carts with extra memory.
(Exception: no support for the homebrewed multicart, which is too large for pico8 anyway)

Thanks to some optimizations, games now usually run at full speed.

What's not working well?

Sound has occasional glitches (either due to pico8's sound limitations or my implementation).

Keys sometime need to be pressed for a few moments to work in some games. This might be due to the original console's controls being slow to press.

What's the point of this?

Proving that emulators for non-trivial systems are possible to do in pico-8.
And just for the fun of creating and/or playing an emulator!

P#64568 2019-05-18 21:25 ( Edited 2022-02-26 16:41)

View Older Posts