about summary refs log tree commit diff stats
path: root/apps
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2021-07-16 08:09:42 -0700
committerKartik K. Agaram <vc@akkartik.com>2021-07-16 08:28:56 -0700
commit44d26b77c45668c9b0c99894a4294cec004361fe (patch)
tree68a5dcd4971873efd4ce184e9bf9a531c2161813 /apps
parentac45f097153afd3a89f43886e4124c5b2c26b98a (diff)
downloadmu-44d26b77c45668c9b0c99894a4294cec004361fe.tar.gz
.
Diffstat (limited to 'apps')
-rw-r--r--apps/boot0.hex344
-rw-r--r--apps/colors.mu242
-rw-r--r--apps/ex1.mu14
-rw-r--r--apps/ex10.mu42
-rw-r--r--apps/ex10.mu.176
-rw-r--r--apps/ex11.mu261
-rw-r--r--apps/ex12.mu28
-rw-r--r--apps/ex2.mu28
-rw-r--r--apps/ex3.mu31
-rw-r--r--apps/ex4.mu14
-rw-r--r--apps/ex5.mu16
-rw-r--r--apps/ex6.mu32
-rw-r--r--apps/ex7.mu46
-rw-r--r--apps/ex8.mu12
-rw-r--r--apps/ex9.mu51
-rw-r--r--apps/hest-life.mu1029
-rw-r--r--apps/img.mu1148
-rw-r--r--apps/life.mu252
-rw-r--r--apps/mandelbrot-fixed.mu262
-rw-r--r--apps/mandelbrot-silhouette.mu150
-rw-r--r--apps/mandelbrot.mu179
-rw-r--r--apps/rpn.mu151
22 files changed, 4508 insertions, 0 deletions
diff --git a/apps/boot0.hex b/apps/boot0.hex
new file mode 100644
index 00000000..929d2fd9
--- /dev/null
+++ b/apps/boot0.hex
@@ -0,0 +1,344 @@
+# A minimal bootable image that:
+#   - loads more sectors past the first boot sector (using BIOS primitives)
+#   - switches to 32-bit mode (giving up access to BIOS primitives)
+#   - sets up a keyboard handler to print '1' at the top-left of screen when '1' is typed
+#
+# When it's ready to accept keys, it prints 'H' to the top-left of the screen.
+#
+# If the initial load fails, it prints 'D' to the top-left of the screen and
+# halts.
+#
+# To convert to a disk image, first prepare a realistically sized disk image:
+#   dd if=/dev/zero of=code.img count=20160  # 512-byte sectors, so 10MB
+# Now fill in sectors:
+#   linux/bootstrap/bootstrap run linux/hex < apps/boot0.hex > boot.bin
+#   dd if=boot.bin of=code.img conv=notrunc
+# To run:
+#   qemu-system-i386 code.img
+# Or:
+#   bochs -f bochsrc  # bochsrc loads code.img
+#
+# Since we start out in 16-bit mode, we need instructions SubX doesn't
+# support.
+# This file contains just lowercase hex bytes and comments. Zero
+# error-checking. Make liberal use of:
+#   - comments documenting expected offsets
+#   - size checks on the emitted file (currently: 512 bytes)
+#   - xxd to eyeball that offsets contain expected bytes
+
+## 16-bit entry point
+
+# Upon reset, the IBM PC
+#   loads the first sector (512 bytes)
+#   from some bootable image (see the boot sector marker at the end of this file)
+#   to the address range [0x7c00, 0x7e00)
+
+# offset 00 (address 0x7c00):
+  # disable interrupts for this initialization
+  fa  # cli
+
+  # initialize segment registers
+  # this isn't always needed, but the recommendation is to not make assumptions
+  b8 00 00  # ax <- 0
+  8e d8  # ds <- ax
+  8e d0  # ss <- ax
+  8e c0  # es <- ax
+  8e e0  # fs <- ax
+  8e e8  # gs <- ax
+
+  # We don't read or write the stack before we get to 32-bit mode. No function
+  # calls, so we don't need to initialize the stack.
+
+# 0e:
+  # load more sectors from disk
+  b4 02  # ah <- 2  # read sectors from disk
+  # dl comes conveniently initialized at boot time with the index of the device being booted
+  b5 00  # ch <- 0  # cylinder 0
+  b6 00  # dh <- 0  # track 0
+  b1 02  # cl <- 2  # second sector, 1-based
+  b0 01  # al <- 1  # number of sectors to read
+  # address to write sectors to = es:bx = 0x7e00, contiguous with boot segment
+  bb 00 00  # bx <- 0
+  8e c3  # es <- bx
+  bb 00 7e  # bx <- 0x7e00
+  cd 13  # int 13h, BIOS disk service
+  0f 82 76 00  # jump-if-carry disk-error
+
+# 26:
+  # undo the A20 hack: https://en.wikipedia.org/wiki/A20_line
+  # this is from https://github.com/mit-pdos/xv6-public/blob/master/bootasm.S
+  # seta20.1:
+  e4 64  # al <- port 0x64
+  a8 02  # set zf if bit 1 (second-least significant) is not set
+  75 fa  # if zf not set, goto seta20.1 (-6)
+
+  b0 d1  # al <- 0xd1
+  e6 64  # port 0x64 <- al
+
+# 30:
+  # seta20.2:
+  e4 64  # al <- port 0x64
+  a8 02  # set zf if bit 1 (second-least significant) is not set
+  75 fa  # if zf not set, goto seta20.2 (-6)
+
+  b0 df  # al <- 0xdf
+  e6 64  # port 0x64 <- al
+
+# 3a:
+  # switch to 32-bit mode
+  0f 01 16  # lgdt 00/mod/indirect 010/subop 110/rm/use-disp16
+    80 7c  # *gdt_descriptor
+# 3f:
+  0f 20 c0  # eax <- cr0
+  66 83 c8 01  # eax <- or 0x1
+  0f 22 c0  # cr0 <- eax
+  ea c0 7c 08 00  # far jump to initialize_32bit_mode after setting cs to the record at offset 8 in the gdt (gdt_code)
+
+# padding
+# 4e:
+                                          00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+## GDT: 3 records of 8 bytes each
+
+# 60:
+# gdt_start:
+# gdt_null:  mandatory null descriptor
+  00 00 00 00 00 00 00 00
+# gdt_code:  (offset 8 from gdt_start)
+  ff ff  # limit[0:16]
+  00 00 00  # base[0:24]
+  9a  # 1/present 00/privilege 1/descriptor type = 1001b
+      # 1/code 0/conforming 1/readable 0/accessed = 1010b
+  cf  # 1/granularity 1/32-bit 0/64-bit-segment 0/AVL = 1100b
+      # limit[16:20] = 1111b
+  00  # base[24:32]
+# gdt_data:  (offset 16 from gdt_start)
+  ff ff  # limit[0:16]
+  00 00 00  # base[0:24]
+  92  # 1/present 00/privilege 1/descriptor type = 1001b
+      # 0/data 0/conforming 1/readable 0/accessed = 0010b
+  cf  # same as gdt_code
+  00  # base[24:32]
+# gdt_end:
+
+# padding
+# 78:
+                        00 00 00 00 00 00 00 00
+
+# 80:
+# gdt_descriptor:
+  17 00  # final index of gdt = gdt_end - gdt_start - 1
+  60 7c 00 00  # start = gdt_start
+
+# padding
+# 85:
+                  00 00 00 00 00 00 00 00 00 00
+
+# 90:
+# disk_error:
+  # print 'D' to top-left of screen to indicate disk error
+  # *0xb8000 <- 0x0f44
+  # bx <- 0xb800
+  bb 00 b8
+  # ds <- bx
+  8e db  # 11b/mod 011b/reg/ds 011b/rm/bx
+  # al <- 'D'
+  b0 44
+  # ah <- 0x0f  # white on black
+  b4 0f
+  # bx <- 0
+  bb 00 00
+  # *ds:bx <- ax
+  89 07  # 00b/mod/indirect 000b/reg/ax 111b/rm/bx
+
+e9 fb ff  # loop forever
+
+# padding
+# a1:
+   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+## 32-bit code from this point (still some instructions not in SubX)
+
+# c0:
+# initialize_32bit_mode:
+  66 b8 10 00  # ax <- offset 16 from gdt_start
+  8e d8  # ds <- ax
+  8e d0  # ss <- ax
+  8e c0  # es <- ax
+  8e e0  # fs <- ax
+  8e e8  # gs <- ax
+
+  # load interrupt handlers
+  0f 01 1d  # lidt 00/mod/indirect 011/subop 101/rm32/use-disp32
+    00 7f 00 00  # *idt_descriptor
+
+  # enable keyboard IRQ
+  b0 fd  # al <- 0xfd  # enable just IRQ1
+  e6 21  # port 0x21 <- al
+
+  # initialization is done; enable interrupts
+  fb
+  e9 21 00 00 00  # jump to 0x7d00
+
+# padding
+# df:
+                                             00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+## 'application' SubX code: print one character to top-left of screen
+
+# offset 100 (address 0x7d00):
+# Entry:
+  # eax <- *0x7ff4  # random address in second segment containing 'H'
+  8b  # copy rm32 to r32
+    05  # 00/mod/indirect 000/r32/eax 101/rm32/use-disp32
+    # disp32
+    f4 7f 00 00
+  # *0xb8000 <- eax
+  89  # copy r32 to rm32
+    05  # 00/mod/indirect 000/r32/eax 101/rm32/use-disp32
+    # disp32
+    00 80 0b 00
+
+e9 fb ff ff ff  # loop forever
+
+# padding
+# 111:
+   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+# 120:
+# null interrupt handler:
+  cf  # iret
+
+# padding
+# 121:
+   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+# 130:
+# keyboard interrupt handler:
+  # prologue
+  fa  # disable interrupts
+  60  # push all registers to stack
+  # acknowledge interrupt
+  b0 20  # al <- 0x20
+  e6 20  # port 0x20 <- al
+  # check output buffer of 8042 keyboard controller (https://web.archive.org/web/20040604041507/http://panda.cs.ndsu.nodak.edu/~achapwes/PICmicro/keyboard/atkeyboard.html)
+  e4 64  # al <- port 0x64
+  a8 01  # set zf if bit 0 (least significant) is not set
+  74 11  # if bit 0 is not set, skip to epilogue
+  # read keycode into eax
+  31 c0  # eax <- xor eax;  11/direct 000/r32/eax 000/rm32/eax
+  e4 60  # al <- port 0x60
+  # map key '1' to ascii; if eax == 2, eax = 0x31
+  3d 02 00 00 00  # compare eax with 0x02
+  75 0b  # if not equal, goto epilogue
+  b8 31 0f 00 00  # eax <- 0x0f31
+  # print eax to top-left of screen (*0xb8000)
+  89  # copy r32 to rm32
+    05  # 00/mod/indirect 000/r32/eax 101/rm32/use-disp32
+    # disp32
+    00 80 0b 00
+  # epilogue
+  61  # pop all registers
+  fb  # enable interrupts
+  cf  # iret
+
+# padding
+# 155
+               00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+# final 2 bytes of boot sector
+55 aa
+
+## sector 2
+# loaded by load_disk, not automatically on boot
+
+# offset 200 (address 0x7e00): interrupt descriptor table
+# 32 entries * 8 bytes each = 256 bytes (0x100)
+# idt_start:
+
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+
+# entry 8: clock
+  20 7d  # target[0:16] = null interrupt handler
+  08 00  # segment selector (gdt_code)
+  00  # unused
+  8e  # 1/p 00/dpl 0 1110/type/32-bit-interrupt-gate
+  00 00  # target[16:32]
+
+# entry 9: keyboard
+  30 7d  # target[0:16] = keyboard interrupt handler
+  08 00  # segment selector (gdt_code)
+  00  # unused
+  8e  # 1/p 00/dpl 0 1110/type/32-bit-interrupt-gate
+  00 00  # target[16:32]
+
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+# idt_end:
+
+# offset 300 (address 0x7f00):
+# idt_descriptor:
+  ff 00  # idt_end - idt_start - 1
+  00 7e 00 00  # start = idt_start
+
+# padding
+                  00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 48 0f 00 00 00 00 00 00 00 00 00 00  # spot the 'H' with attributes
+# offset 400 (address 0x8000)
+
+# vim:ft=conf
diff --git a/apps/colors.mu b/apps/colors.mu
new file mode 100644
index 00000000..78e58838
--- /dev/null
+++ b/apps/colors.mu
@@ -0,0 +1,242 @@
+# Return colors 'near' a given r/g/b value (expressed in hex)
+# If we did this rigorously we'd need to implement cosines. So we won't.
+#
+# To build:
+#   $ ./translate apps/colors.mu
+#
+# Example session:
+#   $ qemu-system-i386 code.img
+#   Enter 3 hex bytes for r, g, b (lowercase; no 0x prefix) separated by a single space> aa 0 aa
+#   5
+# This means only color 5 in the default palette is similar to #aa00aa.
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  var in-storage: (stream byte 0x10)
+  var in/esi: (addr stream byte) <- address in-storage
+  {
+    # print prompt
+    var x/eax: int <- draw-text-rightward screen, "Enter 3 hex bytes for r, g, b (lowercase; no 0x prefix) separated by a single space> ", 0x10/x, 0x80/xmax, 0x28/y, 3/fg/cyan, 0/bg
+    # read line from keyboard
+    clear-stream in
+    {
+      draw-cursor screen, 0x20/space
+      var key/eax: byte <- read-key keyboard
+      compare key, 0xa/newline
+      break-if-=
+      compare key, 0
+      loop-if-=
+      var key2/eax: int <- copy key
+      append-byte in, key2
+      var g/eax: grapheme <- copy key2
+      draw-grapheme-at-cursor screen, g, 0xf/fg, 0/bg
+      move-cursor-right 0
+      loop
+    }
+    clear-screen screen
+    # parse
+    var a/ecx: int <- copy 0
+    var b/edx: int <- copy 0
+    var c/ebx: int <- copy 0
+    # a, b, c = r, g, b
+    a, b, c <- parse in
+#?     set-cursor-position screen, 0x10/x, 0x1a/y
+#?     draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen screen, a, 7/fg, 0/bg
+#?     draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, " ", 7/fg, 0/bg
+#?     draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen screen, b, 7/fg, 0/bg
+#?     draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, " ", 7/fg, 0/bg
+#?     draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen screen, c, 7/fg, 0/bg
+    a, b, c <- hsl a, b, c
+    # return all colors in the same quadrant in h, s and l
+    print-nearby-colors screen, a, b, c
+    # another metric
+    var color/eax: int <- nearest-color-euclidean-hsl a, b, c
+    set-cursor-position screen, 0x10/x, 0x26/y
+    draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "nearest (euclidean, h/s/l): ", 0xf/fg, 0/bg
+    draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, color, 7/fg, 0/bg
+    draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, " ", 0xf/fg, 0/bg
+    draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "               ", 0/fg, color
+    #
+    loop
+  }
+}
+
+# read exactly 3 words in a single line
+# Each word consists of exactly 1 or 2 hex bytes. No hex prefix.
+fn parse in: (addr stream byte) -> _/ecx: int, _/edx: int, _/ebx: int {
+  # read first byte of r
+  var tmp/eax: byte <- read-byte in
+  {
+    var valid?/eax: boolean <- hex-digit? tmp
+    compare valid?, 0/false
+    break-if-!=
+    abort "invalid byte 0 of r"
+  }
+  tmp <- fast-hex-digit-value tmp
+  var r/ecx: int <- copy tmp
+#?   set-cursor-position 0/screen, 0x10/x, 0x10/y
+#?   draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, r, 7/fg, 0/bg
+  # read second byte of r
+  tmp <- read-byte in
+  {
+    {
+      var valid?/eax: boolean <- hex-digit? tmp
+      compare valid?, 0/false
+    }
+    break-if-=
+    r <- shift-left 4
+    tmp <- fast-hex-digit-value tmp
+#?     {
+#?       var foo/eax: int <- copy tmp
+#?       set-cursor-position 0/screen, 0x10/x, 0x11/y
+#?       draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, foo, 7/fg, 0/bg
+#?     }
+    r <- add tmp
+#?     {
+#?       set-cursor-position 0/screen, 0x10/x, 0x12/y
+#?       draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, r, 7/fg, 0/bg
+#?     }
+    tmp <- read-byte in  # skip space
+  }
+  # read first byte of g
+  var tmp/eax: byte <- read-byte in
+  {
+    var valid?/eax: boolean <- hex-digit? tmp
+    compare valid?, 0/false
+    break-if-!=
+    abort "invalid byte 0 of g"
+  }
+  tmp <- fast-hex-digit-value tmp
+  var g/edx: int <- copy tmp
+#?   set-cursor-position 0/screen, 0x10/x, 0x13/y
+#?   draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, g, 7/fg, 0/bg
+  # read second byte of g
+  tmp <- read-byte in
+  {
+    {
+      var valid?/eax: boolean <- hex-digit? tmp
+      compare valid?, 0/false
+    }
+    break-if-=
+    g <- shift-left 4
+    tmp <- fast-hex-digit-value tmp
+#?     {
+#?       var foo/eax: int <- copy tmp
+#?       set-cursor-position 0/screen, 0x10/x, 0x14/y
+#?       draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, foo, 7/fg, 0/bg
+#?     }
+    g <- add tmp
+#?     {
+#?       set-cursor-position 0/screen, 0x10/x, 0x15/y
+#?       draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, g, 7/fg, 0/bg
+#?     }
+    tmp <- read-byte in  # skip space
+  }
+  # read first byte of b
+  var tmp/eax: byte <- read-byte in
+  {
+    var valid?/eax: boolean <- hex-digit? tmp
+    compare valid?, 0/false
+    break-if-!=
+    abort "invalid byte 0 of b"
+  }
+  tmp <- fast-hex-digit-value tmp
+  var b/ebx: int <- copy tmp
+#?   set-cursor-position 0/screen, 0x10/x, 0x16/y
+#?   draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, b, 7/fg, 0/bg
+  # read second byte of b
+  {
+    {
+      var done?/eax: boolean <- stream-empty? in
+      compare done?, 0/false
+    }
+    break-if-!=
+    tmp <- read-byte in
+    {
+      var valid?/eax: boolean <- hex-digit? tmp
+      compare valid?, 0/false
+    }
+    break-if-=
+    b <- shift-left 4
+    tmp <- fast-hex-digit-value tmp
+#?     {
+#?       var foo/eax: int <- copy tmp
+#?       set-cursor-position 0/screen, 0x10/x, 0x17/y
+#?       draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, foo, 7/fg, 0/bg
+#?     }
+    b <- add tmp
+#?     {
+#?       set-cursor-position 0/screen, 0x10/x, 0x18/y
+#?       draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, b, 7/fg, 0/bg
+#?     }
+  }
+  return r, g, b
+}
+
+# no error checking
+fn fast-hex-digit-value in: byte -> _/eax: byte {
+  var result/eax: byte <- copy in
+  compare result, 0x39
+  {
+    break-if->
+    result <- subtract 0x30/0
+    return result
+  }
+  result <- subtract 0x61/a
+  result <- add 0xa/10
+  return result
+}
+
+fn print-nearby-colors screen: (addr screen), h: int, s: int, l: int {
+#?   set-cursor-position screen, 0x10/x, 0x1c/y
+#?   draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen screen, h, 7/fg, 0/bg
+#?   draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, " ", 7/fg, 0/bg
+#?   draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen screen, s, 7/fg, 0/bg
+#?   draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, " ", 7/fg, 0/bg
+#?   draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen screen, l, 7/fg, 0/bg
+  # save just top 2 bits of each, so that we narrow down to 1/64th of the volume
+  shift-right h, 6
+  shift-right s, 6
+  shift-right l, 6
+#?   set-cursor-position screen, 0x10/x, 0x1/y
+#?   draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen screen, h, 7/fg, 0/bg
+#?   draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, " ", 7/fg, 0/bg
+#?   draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen screen, s, 7/fg, 0/bg
+#?   draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, " ", 7/fg, 0/bg
+#?   draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen screen, l, 7/fg, 0/bg
+  var a/ecx: int <- copy 0
+  var b/edx: int <- copy 0
+  var c/ebx: int <- copy 0
+  var color/eax: int <- copy 0
+  var y/esi: int <- copy 2
+  {
+    compare color, 0x100
+    break-if->=
+    a, b, c <- color-rgb color
+    a, b, c <- hsl a, b, c
+    a <- shift-right 6
+    b <- shift-right 6
+    c <- shift-right 6
+    {
+      compare a, h
+      break-if-!=
+      compare b, s
+      break-if-!=
+      compare c, l
+      break-if-!=
+      set-cursor-position screen, 0x10/x, y
+      draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, color, 7/fg, 0/bg
+      set-cursor-position screen, 0x14/x, y
+      draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, " ", 7/fg, 0/bg
+      draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "               ", 0/fg, color
+#?       draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, " ", 7/fg, 0/bg
+#?       draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen screen, a, 7/fg, 0/bg
+#?       draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, " ", 7/fg, 0/bg
+#?       draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen screen, b, 7/fg, 0/bg
+#?       draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, " ", 7/fg, 0/bg
+#?       draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen screen, c, 7/fg, 0/bg
+      y <- increment
+    }
+    color <- increment
+    loop
+  }
+}
diff --git a/apps/ex1.mu b/apps/ex1.mu
new file mode 100644
index 00000000..a96bed07
--- /dev/null
+++ b/apps/ex1.mu
@@ -0,0 +1,14 @@
+# The simplest possible bare-metal program.
+#
+# To build a disk image:
+#   ./translate apps/ex1.mu        # emits code.img
+# To run:
+#   qemu-system-i386 code.img
+# Or:
+#   bochs -f bochsrc               # bochsrc loads code.img
+#
+# Expected output: blank screen with no errors
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  loop
+}
diff --git a/apps/ex10.mu b/apps/ex10.mu
new file mode 100644
index 00000000..05198738
--- /dev/null
+++ b/apps/ex10.mu
@@ -0,0 +1,42 @@
+# Demo of mouse support.
+#
+# To build a disk image:
+#   ./translate apps/ex10.mu       # emits code.img
+# To run:
+#   qemu-system-i386 code.img
+# Or:
+#   bochs -f bochsrc               # bochsrc loads code.img
+#
+# Expected output:
+#   Values between -256 and +255 as you move the mouse over the window.
+#   You might need to click on the window once.
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  # repeatedly print out mouse driver results if non-zero
+  $main:event-loop: {
+    var dx/eax: int <- copy 0
+    var dy/ecx: int <- copy 0
+    dx, dy <- read-mouse-event
+    {
+      compare dx, 0
+      break-if-!=
+      compare dy, 0
+      break-if-!=
+      loop $main:event-loop
+    }
+    {
+      var dummy1/eax: int <- copy 0
+      var dummy2/ecx: int <- copy 0
+      dummy1, dummy2 <- draw-text-wrapping-right-then-down-over-full-screen screen, "         ", 0/x, 0x10/y, 0x31/fg, 0/bg
+    }
+    {
+      var dummy/ecx: int <- copy 0
+      dx, dummy <- draw-int32-decimal-wrapping-right-then-down-over-full-screen screen, dx, 0/x, 0x10/y, 0x31/fg, 0/bg
+    }
+    {
+      var dummy/eax: int <- copy 0
+      dummy, dy <- draw-int32-decimal-wrapping-right-then-down-over-full-screen screen, dy, 5/x, 0x10/y, 0x31/fg, 0/bg
+    }
+    loop
+  }
+}
diff --git a/apps/ex10.mu. b/apps/ex10.mu.
new file mode 100644
index 00000000..35fea653
--- /dev/null
+++ b/apps/ex10.mu.
@@ -0,0 +1,176 @@
+# Demo of mouse support.
+#
+# To build a disk image:
+#   ./translate ex10.mu            # emits disk.img
+# To run:
+#   qemu-system-i386 disk.img
+# Or:
+#   bochs -f bochsrc               # bochsrc loads disk.img
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+#?   var x/esi: int <- copy 0x200
+#?   var y/edi: int <- copy 0x180
+#?   render-grid x, y
+  $main:event-loop: {
+    # read deltas from mouse
+    var dx/eax: int <- copy 0
+    var dy/ecx: int <- copy 0
+    dx, dy <- read-mouse-event
+    # loop if deltas are both 0
+    {
+      compare dx, 0
+      break-if-!=
+      compare dy, 0
+      break-if-!=
+      loop $main:event-loop
+    }
+    # render unclamped deltas
+#?     render-grid x, y
+    draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, dx, 7/fg, 0/bg
+    draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, " ", 7/fg, 0/bg
+    draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, dy, 7/fg, 0/bg
+    move-cursor-to-left-margin-of-next-line screen
+#?     {
+#?       var dummy1/eax: int <- copy 0
+#?       var dummy2/ecx: int <- copy 0
+#?       dummy1, dummy2 <- draw-text-wrapping-right-then-down-over-full-screen screen, "         ", 0/x, 0x10/y, 0x31/fg, 0/bg
+#?     }
+#?     {
+#?       var ephemeral-dx/eax: int <- copy dx
+#?       var dummy/ecx: int <- copy 0
+#?       ephemeral-dx, dummy <- draw-int32-decimal-wrapping-right-then-down-over-full-screen screen, ephemeral-dx, 0/x, 0x10/y, 0x31/fg, 0/bg
+#?     }
+#?     {
+#?       var dummy/eax: int <- copy 0
+#?       var ephemeral-dy/ecx: int <- copy dy
+#?       dummy, ephemeral-dy <- draw-int32-decimal-wrapping-right-then-down-over-full-screen screen, ephemeral-dy, 5/x, 0x10/y, 0x31/fg, 0/bg
+#?     }
+#?     # clamp deltas
+#?     $clamp-dx: {
+#?       compare dx, -0xa
+#?       {
+#?         break-if->
+#?         dx <- copy -0xa
+#?         break $clamp-dx
+#?       }
+#?       compare dx, 0xa
+#?       {
+#?         break-if-<
+#?         dx <- copy 0xa
+#?         break $clamp-dx
+#?       }
+#?       dx <- copy 0
+#?     }
+#?     $clamp-dy: {
+#?       compare dy, -0xa
+#?       {
+#?         break-if->
+#?         dy <- copy -0xa
+#?         break $clamp-dy
+#?       }
+#?       compare dy, 0xa
+#?       {
+#?         break-if-<
+#?         dy <- copy 0xa
+#?         break $clamp-dy
+#?       }
+#?       dy <- copy 0
+#?     }
+#?     # render clamped deltas
+#?     {
+#?       var dummy1/eax: int <- copy 0
+#?       var dummy2/ecx: int <- copy 0
+#?       dummy1, dummy2 <- draw-text-wrapping-right-then-down-over-full-screen screen, "         ", 0/x, 0x20/y, 0x31/fg, 0/bg
+#?     }
+#?     {
+#?       var save-dx/eax: int <- copy dx
+#?       var dummy/ecx: int <- copy 0
+#?       save-dx, dummy <- draw-int32-decimal-wrapping-right-then-down-over-full-screen screen, save-dx, 0/x, 0x20/y, 0x31/fg, 0/bg
+#?     }
+#?     {
+#?       var dummy/eax: int <- copy 0
+#?       var save-dy/ecx: int <- copy dy
+#?       dummy, save-dy <- draw-int32-decimal-wrapping-right-then-down-over-full-screen screen, save-dy, 5/x, 0x20/y, 0x31/fg, 0/bg
+#?     }
+#?     # loop if deltas are both 0
+#?     {
+#?       compare dx, 0
+#?       break-if-!=
+#?       compare dy, 0
+#?       break-if-!=
+#?       loop $main:event-loop
+#?     }
+#?     # accumulate deltas and clamp result within screen bounds
+#?     x <- add dx
+#?     compare x, 0
+#?     {
+#?       break-if->=
+#?       x <- copy 0
+#?     }
+#?     compare x, 0x400
+#?     {
+#?       break-if-<
+#?       x <- copy 0x3ff
+#?     }
+#?     y <- subtract dy  # mouse y coordinates are reverse compared to screen
+#?     compare y, 0
+#?     {
+#?       break-if->=
+#?       y <- copy 0
+#?     }
+#?     compare y, 0x300
+#?     {
+#?       break-if-<
+#?       y <- copy 0x2ff
+#?     }
+    loop
+  }
+}
+
+#? fn render-grid curr-x: int, curr-y: int {
+#?   and-with curr-x, 0xfffffffc
+#?   and-with curr-y, 0xfffffffc
+#?   var y/eax: int <- copy 0
+#?   {
+#?     compare y, 0x300/screen-height=768
+#?     break-if->=
+#?     var x/edx: int <- copy 0
+#?     {
+#?       compare x, 0x400/screen-width=1024
+#?       break-if->=
+#?       var color/ecx: int <- copy 0
+#?       # set color if either x or y is divisible by 4
+#?       var tmp/ebx: int <- copy y
+#?       tmp <- and 3
+#?       compare tmp, 0
+#?       {
+#?         break-if-!=
+#?         color <- copy 3
+#?       }
+#?       tmp <- copy x
+#?       tmp <- and 3
+#?       compare tmp, 0
+#?       {
+#?         break-if-!=
+#?         color <- copy 3
+#?       }
+#?       # highlight color if x and y match curr-x and curr-y (quantized)
+#?       {
+#?         var xq/edx: int <- copy x
+#?         xq <- and 0xfffffffc
+#?         var yq/eax: int <- copy y
+#?         yq <- and 0xfffffffc
+#?         compare xq, curr-x
+#?         break-if-!=
+#?         compare yq, curr-y
+#?         break-if-!=
+#?         color <- copy 0xc
+#?       }
+#?       pixel-on-real-screen x, y, color
+#?       x <- increment
+#?       loop
+#?     }
+#?     y <- increment
+#?     loop
+#?   }
+#? }
diff --git a/apps/ex11.mu b/apps/ex11.mu
new file mode 100644
index 00000000..6b967724
--- /dev/null
+++ b/apps/ex11.mu
@@ -0,0 +1,261 @@
+# Demo of an interactive app: controlling a Bezier curve on screen
+#
+# To build a disk image:
+#   ./translate apps/ex11.mu       # emits code.img
+# To run:
+#   qemu-system-i386 code.img
+# Or:
+#   bochs -f bochsrc               # bochsrc loads code.img
+#
+# Expected output: a spline with 3 control points. Use `Tab` to switch cursor
+# between control points, and arrow keys to move the control point at the
+# cursor.
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  var env-storage: environment
+  var env/esi: (addr environment) <- address env-storage
+  initialize-environment env, 0x200 0x20, 0x180 0x90, 0x180 0x160
+  {
+    render screen, env
+    edit keyboard, env
+    loop
+  }
+}
+
+type environment {
+  p0: (handle point)
+  p1: (handle point)
+  p2: (handle point)
+  cursor: (handle point)  # one of p0, p1 or p2
+}
+
+type point {
+  x: int
+  y: int
+}
+
+fn render screen: (addr screen), _self: (addr environment) {
+  clear-screen screen
+  var self/esi: (addr environment) <- copy _self
+  var tmp-ah/ecx: (addr handle point) <- get self, p0
+  var tmp/eax: (addr point) <- lookup *tmp-ah
+  var p0/ebx: (addr point) <- copy tmp
+  tmp-ah <- get self, p1
+  tmp <- lookup *tmp-ah
+  var p1/edx: (addr point) <- copy tmp
+  tmp-ah <- get self, p2
+  tmp <- lookup *tmp-ah
+  var p2/ecx: (addr point) <- copy tmp
+  # control lines
+  line    screen, p0, p1,                 7/color
+  line    screen, p1, p2,                 7/color
+  # curve above control lines
+  bezier  screen, p0, p1, p2,             0xc/color
+  # points above curve
+  disc    screen, p0,           3/radius, 7/color   0xf/border
+  disc    screen, p1,           3/radius, 7/color   0xf/border
+  disc    screen, p2,           3/radius, 7/color   0xf/border
+  # cursor last of all
+  var cursor-ah/eax: (addr handle point) <- get self, cursor
+  var cursor/eax: (addr point) <- lookup *cursor-ah
+  cursor screen, cursor, 0xa/side, 3/color
+}
+
+fn bezier screen: (addr screen), _p0: (addr point), _p1: (addr point), _p2: (addr point), color: int {
+  var p0/esi: (addr point) <- copy _p0
+  var x0/ecx: (addr int) <- get p0, x
+  var y0/edx: (addr int) <- get p0, y
+  var p1/esi: (addr point) <- copy _p1
+  var x1/ebx: (addr int) <- get p1, x
+  var y1/eax: (addr int) <- get p1, y
+  var p2/esi: (addr point) <- copy _p2
+  var x2/edi: (addr int) <- get p2, x
+  var y2/esi: (addr int) <- get p2, y
+  draw-monotonic-bezier screen, *x0 *y0, *x1 *y1, *x2 *y2, color
+}
+
+fn cursor screen: (addr screen), _p: (addr point), side: int, color: int {
+  var half-side/eax: int <- copy side
+  half-side <- shift-right 1
+  var p/esi: (addr point) <- copy _p
+  var x-a/ecx: (addr int) <- get p, x
+  var left-x/ecx: int <- copy *x-a
+  left-x <- subtract half-side
+  var y-a/edx: (addr int) <- get p, y
+  var top-y/edx: int <- copy *y-a
+  top-y <- subtract half-side
+  var max/eax: int <- copy left-x
+  max <- add side
+  draw-horizontal-line screen, top-y, left-x, max, color
+  max <- copy top-y
+  max <- add side
+  draw-vertical-line screen, left-x, top-y, max, color
+  var right-x/ebx: int <- copy left-x
+  right-x <- add side
+  draw-vertical-line screen, right-x, top-y, max, color
+  var bottom-y/edx: int <- copy top-y
+  bottom-y <- add side
+  draw-horizontal-line screen, bottom-y, left-x, right-x, color
+}
+
+fn edit keyboard: (addr keyboard), _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var key/eax: byte <- read-key keyboard
+  compare key, 0
+  loop-if-=
+  {
+    compare key, 9/tab
+    break-if-!=
+    toggle-cursor self
+    return
+  }
+  {
+    compare key, 0x80/left-arrow
+    break-if-!=
+    cursor-left self
+    return
+  }
+  {
+    compare key, 0x83/right-arrow
+    break-if-!=
+    cursor-right self
+    return
+  }
+  {
+    compare key, 0x81/down-arrow
+    break-if-!=
+    cursor-down self
+    return
+  }
+  {
+    compare key, 0x82/up-arrow
+    break-if-!=
+    cursor-up self
+    return
+  }
+}
+
+fn toggle-cursor _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var cursor-ah/edi: (addr handle point) <- get self, cursor
+  var p0-ah/ecx: (addr handle point) <- get self, p0
+  var p1-ah/edx: (addr handle point) <- get self, p1
+  var p2-ah/ebx: (addr handle point) <- get self, p2
+  {
+    var p0?/eax: boolean <- handle-equal? *p0-ah, *cursor-ah
+    compare p0?, 0/false
+    break-if-=
+    copy-object p1-ah, cursor-ah
+    return
+  }
+  {
+    var p1?/eax: boolean <- handle-equal? *p1-ah, *cursor-ah
+    compare p1?, 0/false
+    break-if-=
+    copy-object p2-ah, cursor-ah
+    return
+  }
+  {
+    var p2?/eax: boolean <- handle-equal? *p2-ah, *cursor-ah
+    compare p2?, 0/false
+    break-if-=
+    copy-object p0-ah, cursor-ah
+    return
+  }
+  abort "lost cursor"
+}
+
+fn cursor-left _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var cursor-ah/esi: (addr handle point) <- get self, cursor
+  var cursor/eax: (addr point) <- lookup *cursor-ah
+  var cursor-x/eax: (addr int) <- get cursor, x
+  compare *cursor-x, 0x20
+  {
+    break-if-<
+    subtract-from *cursor-x, 0x20
+  }
+}
+
+fn cursor-right _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var cursor-ah/esi: (addr handle point) <- get self, cursor
+  var cursor/eax: (addr point) <- lookup *cursor-ah
+  var cursor-x/eax: (addr int) <- get cursor, x
+  compare *cursor-x, 0x3f0
+  {
+    break-if->
+    add-to *cursor-x, 0x20
+  }
+}
+
+fn cursor-up _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var cursor-ah/esi: (addr handle point) <- get self, cursor
+  var cursor/eax: (addr point) <- lookup *cursor-ah
+  var cursor-y/eax: (addr int) <- get cursor, y
+  compare *cursor-y, 0x20
+  {
+    break-if-<
+    subtract-from *cursor-y, 0x20
+  }
+}
+
+fn cursor-down _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var cursor-ah/esi: (addr handle point) <- get self, cursor
+  var cursor/eax: (addr point) <- lookup *cursor-ah
+  var cursor-y/eax: (addr int) <- get cursor, y
+  compare *cursor-y, 0x2f0
+  {
+    break-if->
+    add-to *cursor-y, 0x20
+  }
+}
+
+fn line screen: (addr screen), _p0: (addr point), _p1: (addr point), color: int {
+  var p0/esi: (addr point) <- copy _p0
+  var x0/ecx: (addr int) <- get p0, x
+  var y0/edx: (addr int) <- get p0, y
+  var p1/esi: (addr point) <- copy _p1
+  var x1/ebx: (addr int) <- get p1, x
+  var y1/eax: (addr int) <- get p1, y
+  draw-line screen, *x0 *y0, *x1 *y1, color
+}
+
+fn disc screen: (addr screen), _p: (addr point), radius: int, color: int, border-color: int {
+  var p/esi: (addr point) <- copy _p
+  var x/ecx: (addr int) <- get p, x
+  var y/edx: (addr int) <- get p, y
+  draw-disc screen, *x *y, radius, color, border-color
+}
+
+fn initialize-environment _self: (addr environment), x0: int, y0: int, x1: int, y1: int, x2: int, y2: int {
+  var self/esi: (addr environment) <- copy _self
+  var p0-ah/eax: (addr handle point) <- get self, p0
+  allocate p0-ah
+  var p0/eax: (addr point) <- lookup *p0-ah
+  initialize-point p0, x0 y0
+  var p1-ah/eax: (addr handle point) <- get self, p1
+  allocate p1-ah
+  var p1/eax: (addr point) <- lookup *p1-ah
+  initialize-point p1, x1 y1
+  var p2-ah/eax: (addr handle point) <- get self, p2
+  allocate p2-ah
+  var p2/eax: (addr point) <- lookup *p2-ah
+  initialize-point p2, x2 y2
+  # cursor initially at p0
+  var cursor-ah/edi: (addr handle point) <- get self, cursor
+  var src-ah/esi: (addr handle point) <- get self, p0
+  copy-object src-ah, cursor-ah
+}
+
+fn initialize-point _p: (addr point), x: int, y: int {
+  var p/esi: (addr point) <- copy _p
+  var dest/eax: (addr int) <- get p, x
+  var src/ecx: int <- copy x
+  copy-to *dest, src
+  dest <- get p, y
+  src <- copy y
+  copy-to *dest, src
+}
diff --git a/apps/ex12.mu b/apps/ex12.mu
new file mode 100644
index 00000000..79c259b8
--- /dev/null
+++ b/apps/ex12.mu
@@ -0,0 +1,28 @@
+# Checking the timer.
+#
+# To build a disk image:
+#   ./translate apps/ex12.mu       # emits code.img
+# To run:
+#   qemu-system-i386 code.img
+# Or:
+#   bochs -f bochsrc               # bochsrc loads code.img
+#
+# Expected output: text with slowly updating colors
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  var fg/ecx: int <- copy 0
+  var prev-timer-counter/edx: int <- copy 0
+  {
+    var dummy/eax: int <- draw-text-rightward screen, "hello from baremetal Mu!", 0x10/x, 0x400/xmax, 0x10/y, fg, 0/bg
+    # wait for timer to bump
+    {
+      var curr-timer-counter/eax: int <- timer-counter
+      compare curr-timer-counter, prev-timer-counter
+      loop-if-=
+      prev-timer-counter <- copy curr-timer-counter
+    }
+    # switch color
+    fg <- increment
+    loop
+  }
+}
diff --git a/apps/ex2.mu b/apps/ex2.mu
new file mode 100644
index 00000000..be66d883
--- /dev/null
+++ b/apps/ex2.mu
@@ -0,0 +1,28 @@
+# Test out the video mode by filling in the screen with pixels.
+#
+# To build a disk image:
+#   ./translate apps/ex2.mu        # emits code.img
+# To run:
+#   qemu-system-i386 code.img
+# Or:
+#   bochs -f bochsrc               # bochsrc loads code.img
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  var y/eax: int <- copy 0
+  {
+    compare y, 0x300/screen-height=768
+    break-if->=
+    var x/edx: int <- copy 0
+    {
+      compare x, 0x400/screen-width=1024
+      break-if->=
+      var color/ecx: int <- copy x
+      color <- and 0xff
+      pixel screen x, y, color
+      x <- increment
+      loop
+    }
+    y <- increment
+    loop
+  }
+}
diff --git a/apps/ex3.mu b/apps/ex3.mu
new file mode 100644
index 00000000..155457c6
--- /dev/null
+++ b/apps/ex3.mu
@@ -0,0 +1,31 @@
+# Draw pixels in response to keyboard events, starting from the top-left
+# and in raster order.
+#
+# To build a disk image:
+#   ./translate apps/ex3.mu        # emits code.img
+# To run:
+#   qemu-system-i386 code.img
+# Or:
+#   bochs -f bochsrc               # bochsrc loads code.img
+#
+# Expected output: a new green pixel starting from the top left corner of the
+# screen every time you press a key (letter or digit)
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  var x/ecx: int <- copy 0
+  var y/edx: int <- copy 0
+  {
+    var key/eax: byte <- read-key keyboard
+    compare key, 0
+    loop-if-=  # busy wait
+    pixel-on-real-screen x, y, 0x31/green
+    x <- increment
+    compare x, 0x400/screen-width=1024
+    {
+      break-if-<
+      y <- increment
+      x <- copy 0
+    }
+    loop
+  }
+}
diff --git a/apps/ex4.mu b/apps/ex4.mu
new file mode 100644
index 00000000..9c3e28ee
--- /dev/null
+++ b/apps/ex4.mu
@@ -0,0 +1,14 @@
+# Draw a character using the built-in font (GNU unifont)
+#
+# To build a disk image:
+#   ./translate apps/ex4.mu        # emits code.img
+# To run:
+#   qemu-system-i386 code.img
+# Or:
+#   bochs -f bochsrc               # bochsrc loads code.img
+#
+# Expected output: letter 'A' in green near the top-left corner of screen
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  draw-code-point screen, 0x41/A, 2/row, 1/col, 0xa/fg, 0/bg
+}
diff --git a/apps/ex5.mu b/apps/ex5.mu
new file mode 100644
index 00000000..07e8bc1a
--- /dev/null
+++ b/apps/ex5.mu
@@ -0,0 +1,16 @@
+# Draw a single line of ASCII text using the built-in font (GNU unifont)
+# Also demonstrates bounds-checking _before_ drawing.
+#
+# To build a disk image:
+#   ./translate apps/ex5.mu        # emits code.img
+# To run:
+#   qemu-system-i386 code.img
+# Or:
+#   bochs -f bochsrc               # bochsrc loads code.img
+#
+# Expected output: text in green near the top-left corner of screen
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  var dummy/eax: int <- draw-text-rightward screen, "hello from baremetal Mu!", 0x10/x, 0x400/xmax, 0x10/y, 0xa/fg, 0/bg
+  dummy <- draw-text-rightward screen, "you shouldn't see this", 0x10/x, 0xa0/xmax, 0x30/y, 3/fg, 0/bg  # xmax is too narrow
+}
diff --git a/apps/ex6.mu b/apps/ex6.mu
new file mode 100644
index 00000000..fbad3c13
--- /dev/null
+++ b/apps/ex6.mu
@@ -0,0 +1,32 @@
+# Drawing ASCII text incrementally.
+#
+# To build a disk image:
+#   ./translate apps/ex6.mu        # emits code.img
+# To run:
+#   qemu-system-i386 code.img
+# Or:
+#   bochs -f bochsrc               # bochsrc loads code.img
+#
+# Expected output: a box and text that doesn't overflow it
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  # drawing text within a bounding box
+  draw-box-on-real-screen 0xf, 0x1f, 0x79, 0x51, 0x4
+  var x/eax: int <- copy 0x20
+  var y/ecx: int <- copy 0x20
+  x, y <- draw-text-wrapping-right-then-down screen, "hello ",     0x10/xmin, 0x20/ymin, 0x78/xmax, 0x50/ymax, x, y, 0xa/fg, 0/bg
+  x, y <- draw-text-wrapping-right-then-down screen, "from ",      0x10/xmin, 0x20/ymin, 0x78/xmax, 0x50/ymax, x, y, 0xa/fg, 0/bg
+  x, y <- draw-text-wrapping-right-then-down screen, "baremetal ", 0x10/xmin, 0x20/ymin, 0x78/xmax, 0x50/ymax, x, y, 0xa/fg, 0/bg
+  x, y <- draw-text-wrapping-right-then-down screen, "Mu!",        0x10/xmin, 0x20/ymin, 0x78/xmax, 0x50/ymax, x, y, 0xa/fg, 0/bg
+
+  # drawing at the cursor in multiple directions
+  draw-text-wrapping-down-then-right-from-cursor-over-full-screen screen, "abc", 0xa/fg, 0/bg
+  draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "def", 0xa/fg, 0/bg
+
+  # test drawing near the edge
+  x <- draw-text-rightward screen, "R", 0x7f/x, 0x80/xmax=screen-width, 0x18/y, 0xa/fg, 0/bg
+  draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "wrapped from R", 0xa/fg, 0/bg
+
+  x <- draw-text-downward screen, "D", 0x20/x, 0x2f/y, 0x30/ymax=screen-height, 0xa/fg, 0/bg
+  draw-text-wrapping-down-then-right-from-cursor-over-full-screen screen, "wrapped from D", 0xa/fg, 0/bg
+}
diff --git a/apps/ex7.mu b/apps/ex7.mu
new file mode 100644
index 00000000..bd0afd20
--- /dev/null
+++ b/apps/ex7.mu
@@ -0,0 +1,46 @@
+# Cursor-based motions.
+#
+# To build a disk image:
+#   ./translate apps/ex7.mu        # emits code.img
+# To run:
+#   qemu-system-i386 code.img
+# Or:
+#   bochs -f bochsrc               # bochsrc loads code.img
+#
+# Expected output: an interactive game a bit like "snakes". Try pressing h, j,
+# k, l.
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  var space/eax: grapheme <- copy 0x20
+  set-cursor-position screen, 0, 0
+  {
+    draw-cursor screen, space
+    var key/eax: byte <- read-key keyboard
+    {
+      compare key, 0x68/h
+      break-if-!=
+      draw-code-point-at-cursor screen, 0x2d/dash, 0x31/fg, 0/bg
+      move-cursor-left 0
+    }
+    {
+      compare key, 0x6a/j
+      break-if-!=
+      draw-code-point-at-cursor screen, 0x7c/vertical-bar, 0x31/fg, 0/bg
+      move-cursor-down 0
+    }
+    {
+      compare key, 0x6b/k
+      break-if-!=
+      draw-code-point-at-cursor screen, 0x7c/vertical-bar, 0x31/fg, 0/bg
+      move-cursor-up 0
+    }
+    {
+      compare key, 0x6c/l
+      break-if-!=
+      var g/eax: code-point <- copy 0x2d/dash
+      draw-code-point-at-cursor screen, 0x2d/dash, 0x31/fg, 0/bg
+      move-cursor-right 0
+    }
+    loop
+  }
+}
diff --git a/apps/ex8.mu b/apps/ex8.mu
new file mode 100644
index 00000000..c5e695ed
--- /dev/null
+++ b/apps/ex8.mu
@@ -0,0 +1,12 @@
+# Demo of floating-point support.
+#
+# To build a disk image:
+#   ./translate apps/ex8.mu        # emits code.img
+# To run:
+#   bochs -f bochsrc               # bochsrc loads code.img
+# Set a breakpoint at 0x7c00 and start stepping.
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  var n/eax: int <- copy 0
+  var result/xmm0: float <- convert n
+}
diff --git a/apps/ex9.mu b/apps/ex9.mu
new file mode 100644
index 00000000..30853c69
--- /dev/null
+++ b/apps/ex9.mu
@@ -0,0 +1,51 @@
+# Demo of reading and writing to disk.
+#
+# Steps for trying it out:
+#   1. Translate this example into a disk image code.img.
+#       ./translate apps/ex9.mu
+#   2. Build a second disk image data.img containing some text.
+#       dd if=/dev/zero of=data.img count=20160
+#       echo 'abc def ghi' |dd of=data.img conv=notrunc
+#   3. Familiarize yourself with how the data disk looks within xxd:
+#       xxd data.img |head
+#   4. Run in an emulator, either Qemu or Bochs.
+#       qemu-system-i386 -hda code.img -hdb data.img
+#       bochs -f bochsrc.2disks
+#   5. Exit the emulator.
+#   6. Notice that the data disk now contains the word count of the original text.
+#       xxd data.img |head
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  var text-storage: (stream byte 0x200)
+  var text/esi: (addr stream byte) <- address text-storage
+  load-sectors data-disk, 0/lba, 1/num-sectors, text
+
+  var word-count/eax: int <- word-count text
+
+  var result-storage: (stream byte 0x10)
+  var result/edi: (addr stream byte) <- address result-storage
+  write-int32-decimal result, word-count
+  store-sectors data-disk, 0/lba, 1/num-sectors, result
+}
+
+fn word-count in: (addr stream byte) -> _/eax: int {
+  var result/edi: int <- copy 0
+  {
+    var done?/eax: boolean <- stream-empty? in
+    compare done?, 0/false
+    break-if-!=
+    var g/eax: grapheme <- read-grapheme in
+    {
+      compare g, 0x20/space
+      break-if-!=
+      result <- increment
+    }
+    {
+      compare g, 0xa/newline
+      break-if-!=
+      result <- increment
+    }
+    loop
+  }
+  return result
+}
diff --git a/apps/hest-life.mu b/apps/hest-life.mu
new file mode 100644
index 00000000..62f2d945
--- /dev/null
+++ b/apps/hest-life.mu
@@ -0,0 +1,1029 @@
+# Conway's Game of Life in a Hestified way
+#   https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
+#   https://ivanish.ca/hest-podcast
+#
+# To build:
+#   $ ./translate apps/hest-life.mu
+# I run it on my 2.5GHz Linux laptop like this:
+#   $ qemu-system-i386 code.img
+#
+# If things seem too fast or too slow on your computer, adjust the loop bounds
+# in the function `linger` at the bottom. Its value will depend on how you
+# accelerate Qemu (`-accel help`). Mu will eventually get a clock to obviate
+# the need for this tuning.
+#
+# Keyboard shortcuts:
+#   space: pause/resume
+#   0: restart time
+#   l: start looping from 0 to curren time
+#   L: stop looping
+#   +: zoom in
+#   -: zoom out
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  var env-storage: environment
+  var env/esi: (addr environment) <- address env-storage
+  initialize-environment env
+  var second-buffer: screen
+  var second-screen/edi: (addr screen) <- address second-buffer
+  initialize-screen second-screen, 0x80, 0x30, 1/include-pixels
+  render second-screen, env
+  convert-graphemes-to-pixels second-screen
+  copy-pixels second-screen, screen
+  {
+    edit keyboard, env
+    var play?/eax: (addr boolean) <- get env, play?
+    compare *play?, 0/false
+    {
+      break-if-=
+      step env
+      clear-screen second-screen
+      render second-screen, env
+      convert-graphemes-to-pixels second-screen
+      copy-pixels second-screen, screen
+    }
+    linger env
+    loop
+  }
+}
+
+type environment {
+  data: (handle array handle array cell)
+  zoom: int  # 0 = 1024 px per cell; 5 = 4px per cell; each step adjusts by a factor of 4
+  tick: int
+  play?: boolean
+  loop: int  # if non-zero, return tick to 0 after this point
+}
+
+type cell {
+  curr: boolean
+  next: boolean
+}
+
+fn render screen: (addr screen), _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var zoom/eax: (addr int) <- get self, zoom
+  compare *zoom, 0
+  {
+    break-if-!=
+    clear-screen screen
+    render0 screen, self
+  }
+  compare *zoom, 1
+  {
+    break-if-!=
+    clear-screen screen
+    render1 screen, self
+  }
+  compare *zoom, 4
+  {
+    break-if-!=
+    render4 screen, self
+  }
+  # clock
+  var tick-a/eax: (addr int) <- get self, tick
+  set-cursor-position screen, 0x78/x, 0/y
+  draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, *tick-a, 7/fg 0/bg
+}
+
+# Lots of hardcoded constants for now.
+# TODO: split this up into a primitive to render a single cell and its
+# incoming edges (but not the neighboring nodes they emanate from)
+fn render0 screen: (addr screen), _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  # cell border
+  draw-vertical-line   screen,  0xc0/x, 0/ymin, 0x300/ymax, 0x16/color=dark-grey
+  draw-vertical-line   screen, 0x340/x, 0/ymin, 0x300/ymax, 0x16/color=dark-grey
+  draw-horizontal-line screen,  0x40/y, 0/xmin, 0x400/xmax, 0x16/color=dark-grey
+  draw-horizontal-line screen, 0x2c0/y, 0/xmin, 0x400/xmax, 0x16/color=dark-grey
+  # neighboring inputs, corners
+  var color/eax: int <- state-color self, 0x7f/cur-topleftx, 0x5f/cur-toplefty
+  draw-rect screen,  0x90/xmin   0x10/ymin,    0xb0/xmax   0x30/ymax,  color
+  color <- state-color self, 0x81/cur-toprightx, 0x5f/cur-toprighty
+  draw-rect screen, 0x350/xmin   0x10/ymin,   0x370/xmax   0x30/ymax,  color
+  color <- state-color self, 0x7f/cur-botleftx, 0x61/cur-botlefty
+  draw-rect screen,  0x90/xmin  0x2d0/ymin,    0xb0/xmax  0x2f0/ymax,  color
+  color <- state-color self, 0x81/cur-botrightx, 0x61/cur-botrighty
+  draw-rect screen, 0x350/xmin  0x2d0/ymin,   0x370/xmax  0x2f0/ymax,  color
+  # neighboring inputs, edges
+  color <- state-color self, 0x80/cur-topx, 0x5f/cur-topy
+  draw-rect screen, 0x1f0/xmin   0x10/ymin,   0x210/xmax   0x30/ymax,  color
+  color <- state-color self, 0x7f/cur-leftx, 0x60/cur-lefty
+  draw-rect screen,  0x90/xmin  0x170/ymin,    0xb0/xmax  0x190/ymax,  color
+  color <- state-color self, 0x80/cur-botx, 0x61/cur-boty
+  draw-rect screen, 0x1f0/xmin  0x2d0/ymin,   0x210/xmax  0x2f0/ymax,  color
+  color <- state-color self, 0x81/cur-rightx, 0x60/cur-righty
+  draw-rect screen, 0x350/xmin  0x170/ymin,   0x370/xmax  0x190/ymax,  color
+  # sum node
+  draw-rect screen, 0x170/xsmin 0x140/ysmin,  0x190/xsmax 0x160/ysmax, 0x40/color
+  set-cursor-position screen, 0x2d/scol, 0x13/srow
+  draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "+", 0xf/color, 0/bg
+  # conveyors from neighboring inputs to sum node
+  draw-monotonic-bezier screen,  0xa0/x0  0x20/y0,  0x100/x1 0x150/ys,  0x180/xs 0x150/ys,  4/color
+  draw-monotonic-bezier screen,  0xa0/x0 0x180/y0,   0xc0/x1 0x150/ys,  0x180/xs 0x150/ys,  4/color
+  draw-monotonic-bezier screen,  0xa0/x0 0x2e0/y0,  0x100/x1 0x150/ys,  0x180/xs 0x150/ys,  4/color
+  draw-monotonic-bezier screen, 0x200/x0  0x20/y0,  0x180/x1  0x90/y1,  0x180/xs 0x150/ys,  4/color
+  draw-monotonic-bezier screen, 0x200/x0 0x2e0/y0,  0x180/x1 0x200/y1,  0x180/xs 0x150/ys,  4/color
+  draw-monotonic-bezier screen, 0x360/x0  0x20/y0,  0x180/x1  0xc0/y1,  0x180/xs 0x150/ys,  4/color
+  draw-monotonic-bezier screen, 0x360/x0 0x180/y0,  0x35c/x1 0x150/ys,  0x180/xs 0x150/ys,  4/color
+  draw-monotonic-bezier screen, 0x360/x0 0x2e0/y0,  0x180/x1 0x200/y1,  0x180/xs 0x150/ys,  4/color
+  # filter node
+  draw-rect screen, 0x200/xfmin 0x1c0/yfmin, 0x220/xfmax 0x1e0/yfmax, 0x31/color
+  set-cursor-position screen, 0x40/fcol, 0x1b/frow
+  draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "?", 0xf/color, 0/bg
+  # conveyor from sum node to filter node
+  draw-line screen 0x180/xs, 0x150/ys, 0x210/xf, 0x1d0/yf, 0xa2/color
+  # cell outputs at corners
+  var color/eax: int <- state-color self, 0x80/curx, 0x60/cury
+  draw-rect screen,  0xd0/xmin  0x50/ymin,  0xf0/xmax  0x70/ymax, color
+  draw-rect screen, 0x310/xmin  0x50/ymin, 0x330/xmax  0x70/ymax, color
+  draw-rect screen,  0xd0/xmin 0x290/ymin,  0xf0/xmax 0x2b0/ymax, color
+  draw-rect screen, 0x310/xmin 0x290/ymin, 0x330/xmax 0x2b0/ymax, color
+  # cell outputs at edges
+  draw-rect screen, 0x1f0/xmin  0x50/ymin, 0x210/xmax  0x70/ymax, color
+  draw-rect screen,  0xd0/xmin 0x170/ymin,  0xf0/xmax 0x190/ymax, color
+  draw-rect screen, 0x1f0/xmin 0x290/ymin, 0x210/xmax 0x2b0/ymax, color
+  draw-rect screen, 0x310/xmin 0x170/ymin, 0x330/xmax 0x190/ymax, color
+  # conveyors from filter to outputs
+  draw-monotonic-bezier screen, 0x210/xf 0x1d0/yf,  0x1c0/x1  0x60/y1,  0xe0/x2   0x60/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x210/xf 0x1d0/yf,   0xe0/x1 0x1c0/y1,  0xe0/x2  0x180/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x210/xf 0x1d0/yf,  0x1c0/x1 0x2a0/y1,  0xe0/x2  0x2a0/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x210/xf 0x1d0/yf,  0x210/x1  0x60/y1, 0x200/x2   0x60/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x210/xf 0x1d0/yf,  0x210/x1 0x230/y1, 0x200/x2  0x2a0/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x210/xf 0x1d0/yf,  0x320/x1 0x120/y1, 0x320/x2   0x60/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x210/xf 0x1d0/yf,  0x320/x1 0x1c0/y1  0x320/x2  0x180/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x210/xf 0x1d0/yf,  0x320/x1 0x230/y1, 0x320/x2  0x2a0/y2,  0x2a/color
+  # time-variant portion: 16 repeating steps
+  var tick-a/eax: (addr int) <- get self, tick
+  var progress/eax: int <- copy *tick-a
+  progress <- and 0xf
+  # 7 time steps for getting inputs to sum
+  {
+    compare progress, 7
+    break-if->=
+    var u/xmm7: float <- convert progress
+    var six/eax: int <- copy 6
+    var six-f/xmm0: float <- convert six
+    u <- divide six-f
+    # points on conveyors from neighboring cells
+    draw-bezier-point screen, u,  0xa0/x0  0x20/y0, 0x100/x1 0x150/ys, 0x180/xs 0x150/ys, 7/color, 4/radius
+    draw-bezier-point screen, u,  0xa0/x0 0x180/y0,  0xc0/x1 0x150/ys, 0x180/xs 0x150/ys, 7/color, 4/radius
+    draw-bezier-point screen, u,  0xa0/x0 0x2e0/y0, 0x100/x1 0x150/ys, 0x180/xs 0x150/ys, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x200/x0  0x20/y0, 0x180/x1  0x90/y1, 0x180/xs 0x150/ys, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x200/x0 0x2e0/y0, 0x180/x1 0x200/y1, 0x180/xs 0x150/ys, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x360/x0  0x20/y0, 0x180/x1  0xc0/y1, 0x180/xs 0x150/ys, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x360/x0 0x180/y0, 0x35c/x1 0x150/ys, 0x180/xs 0x150/ys, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x360/x0 0x2e0/y0, 0x180/x1 0x200/y1, 0x180/xs 0x150/ys, 7/color, 4/radius
+    return
+  }
+  # two time steps for getting count to filter
+  progress <- subtract 7
+  {
+    compare progress, 2
+    break-if->=
+    progress <- increment  # (0, 1) => (1, 2)
+    var u/xmm7: float <- convert progress
+    var three/eax: int <- copy 3
+    var three-f/xmm0: float <- convert three
+    u <- divide three-f
+    draw-linear-point screen, u, 0x180/xs, 0x150/ys, 0x210/xf, 0x1d0/yf, 7/color, 4/radius
+    set-cursor-position screen, 0x3a/scol, 0x18/srow
+    var n/eax: int <- num-live-neighbors self, 0x80/curx, 0x60/cury
+    draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, n, 0xf/fg 0/bg
+    return
+  }
+  # final 7 time steps for updating output
+  progress <- subtract 2
+  # points on conveyors to outputs
+  var u/xmm7: float <- convert progress
+  var six/eax: int <- copy 6
+  var six-f/xmm0: float <- convert six
+  u <- divide six-f
+  draw-bezier-point screen, u, 0x210/xf 0x1d0/yf,  0x1c0/x1  0x60/y1,  0xe0/x2   0x60/y2, 7/color, 4/radius
+  draw-bezier-point screen, u, 0x210/xf 0x1d0/yf,   0xe0/x1 0x1c0/y1,  0xe0/x2  0x180/y2, 7/color, 4/radius
+  draw-bezier-point screen, u, 0x210/xf 0x1d0/yf,  0x1c0/x1 0x2a0/y1,  0xe0/x2  0x2a0/y2, 7/color, 4/radius
+  draw-bezier-point screen, u, 0x210/xf 0x1d0/yf,  0x210/xf  0x60/y1, 0x200/x2   0x60/y2, 7/color, 4/radius
+  draw-bezier-point screen, u, 0x210/xf 0x1d0/yf,  0x210/xf 0x230/y1, 0x200/x2  0x2a0/y2, 7/color, 4/radius
+  draw-bezier-point screen, u, 0x210/xf 0x1d0/yf,  0x320/x1 0x120/y1, 0x320/x2   0x60/y2, 7/color, 4/radius
+  draw-bezier-point screen, u, 0x210/xf 0x1d0/yf,  0x320/x1 0x1c0/y1, 0x320/x2  0x180/y2, 7/color, 4/radius
+  draw-bezier-point screen, u, 0x210/xf 0x1d0/yf,  0x320/x1 0x230/y1, 0x320/x2  0x2a0/y2, 7/color, 4/radius
+}
+
+fn render1 screen: (addr screen), _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  # cell borders
+  draw-vertical-line   screen,  0xe0/x, 0/ymin, 0x300/ymax, 0x16/color=dark-grey
+  draw-vertical-line   screen, 0x200/x, 0/ymin, 0x300/ymax, 0x16/color=dark-grey
+  draw-vertical-line   screen, 0x320/x, 0/ymin, 0x300/ymax, 0x16/color=dark-grey
+  draw-horizontal-line screen,  0x60/y, 0/xmin, 0x400/xmax, 0x16/color=dark-grey
+  draw-horizontal-line screen, 0x180/y, 0/xmin, 0x400/xmax, 0x16/color=dark-grey
+  draw-horizontal-line screen, 0x2a0/y, 0/xmin, 0x400/xmax, 0x16/color=dark-grey
+  # cell 0: outputs
+  var color/eax: int <- state-color self, 0x80/curx, 0x60/cury
+  draw-rect screen,  0xe8/xmin  0x68/ymin, 0x118/xmax   0x98/ymax, color
+  draw-rect screen,  0xe8/xmin  0xd0/ymin, 0x118/xmax  0x100/ymax, color
+  draw-rect screen,  0xe8/xmin 0x148/ymin, 0x118/xmax  0x178/ymax, color
+  draw-rect screen, 0x158/xmin  0x68/ymin, 0x188/xmax   0x98/ymax, color
+  draw-rect screen, 0x158/xmin 0x148/ymin, 0x188/xmax  0x178/ymax, color
+  draw-rect screen, 0x1c8/xmin  0x68/ymin, 0x1f8/xmax   0x98/ymax, color
+  draw-rect screen, 0x1c8/xmin  0xd0/ymin, 0x1f8/xmax  0x100/ymax, color
+  draw-rect screen, 0x1c8/xmin 0x148/ymin, 0x1f8/xmax  0x178/ymax, color
+  # cell 1: outputs
+  var color/eax: int <- state-color self, 0x81/curx, 0x60/cury
+  draw-rect screen, 0x208/xmin  0x68/ymin, 0x238/xmax   0x98/ymax, color
+  draw-rect screen, 0x208/xmin  0xd0/ymin, 0x238/xmax  0x100/ymax, color
+  draw-rect screen, 0x208/xmin 0x148/ymin, 0x238/xmax  0x178/ymax, color
+  draw-rect screen, 0x278/xmin  0x68/ymin, 0x2a8/xmax   0x98/ymax, color
+  draw-rect screen, 0x278/xmin 0x148/ymin, 0x2a8/xmax  0x178/ymax, color
+  draw-rect screen, 0x2e8/xmin  0x68/ymin, 0x318/xmax   0x98/ymax, color
+  draw-rect screen, 0x2e8/xmin  0xd0/ymin, 0x318/xmax  0x100/ymax, color
+  draw-rect screen, 0x2e8/xmin 0x148/ymin, 0x318/xmax  0x178/ymax, color
+  # cell 2: outputs
+  var color/eax: int <- state-color self, 0x80/curx, 0x61/cury
+  draw-rect screen,  0xe8/xmin 0x188/ymin, 0x118/xmax  0x1b8/ymax, color
+  draw-rect screen,  0xe8/xmin 0x1f0/ymin, 0x118/xmax  0x220/ymax, color
+  draw-rect screen,  0xe8/xmin 0x268/ymin, 0x118/xmax  0x298/ymax, color
+  draw-rect screen, 0x158/xmin 0x188/ymin, 0x188/xmax  0x1b8/ymax, color
+  draw-rect screen, 0x158/xmin 0x268/ymin, 0x188/xmax  0x298/ymax, color
+  draw-rect screen, 0x1c8/xmin 0x188/ymin, 0x1f8/xmax  0x1b8/ymax, color
+  draw-rect screen, 0x1c8/xmin 0x1f0/ymin, 0x1f8/xmax  0x220/ymax, color
+  draw-rect screen, 0x1c8/xmin 0x268/ymin, 0x1f8/xmax  0x298/ymax, color
+  # cell 3: outputs
+  var color/eax: int <- state-color self, 0x81/curx, 0x61/cury
+  draw-rect screen, 0x208/xmin 0x188/ymin, 0x238/xmax  0x1b8/ymax, color
+  draw-rect screen, 0x208/xmin 0x1f0/ymin, 0x238/xmax  0x220/ymax, color
+  draw-rect screen, 0x208/xmin 0x268/ymin, 0x238/xmax  0x298/ymax, color
+  draw-rect screen, 0x278/xmin 0x188/ymin, 0x2a8/xmax  0x1b8/ymax, color
+  draw-rect screen, 0x278/xmin 0x268/ymin, 0x2a8/xmax  0x298/ymax, color
+  draw-rect screen, 0x2e8/xmin 0x188/ymin, 0x318/xmax  0x1b8/ymax, color
+  draw-rect screen, 0x2e8/xmin 0x1f0/ymin, 0x318/xmax  0x220/ymax, color
+  draw-rect screen, 0x2e8/xmin 0x268/ymin, 0x318/xmax  0x298/ymax, color
+  # neighboring nodes
+  var color/eax: int <- state-color self, 0x7f/curx, 0x5f/cury
+  draw-rect screen,  0xa8/xmin  0x28/ymin,  0xd8/xmax   0x58/ymax, color
+  var color/eax: int <- state-color self, 0x80/curx, 0x5f/cury
+  draw-rect screen, 0x158/xmin  0x28/ymin, 0x188/xmax   0x58/ymax, color
+  draw-rect screen, 0x1c8/xmin  0x28/ymin, 0x1f8/xmax   0x58/ymax, color
+  var color/eax: int <- state-color self, 0x81/curx, 0x5f/cury
+  draw-rect screen, 0x208/xmin  0x28/ymin, 0x238/xmax   0x58/ymax, color
+  draw-rect screen, 0x278/xmin  0x28/ymin, 0x2a8/xmax   0x58/ymax, color
+  var color/eax: int <- state-color self, 0x82/curx, 0x5f/cury
+  draw-rect screen, 0x328/xmin  0x28/ymin, 0x358/xmax   0x58/ymax, color
+  var color/eax: int <- state-color self, 0x7f/curx, 0x60/cury
+  draw-rect screen,  0xa8/xmin  0xd0/ymin,  0xd8/xmax  0x100/ymax, color
+  draw-rect screen,  0xa8/xmin 0x148/ymin,  0xd8/xmax  0x178/ymax, color
+  var color/eax: int <- state-color self, 0x82/curx, 0x60/cury
+  draw-rect screen, 0x328/xmin  0xd0/ymin, 0x358/xmax  0x100/ymax, color
+  draw-rect screen, 0x328/xmin 0x148/ymin, 0x358/xmax  0x178/ymax, color
+  var color/eax: int <- state-color self, 0x7f/curx, 0x61/cury
+  draw-rect screen,  0xa8/xmin 0x188/ymin,  0xd8/xmax  0x1b8/ymax, color
+  draw-rect screen,  0xa8/xmin 0x1f0/ymin,  0xd8/xmax  0x220/ymax, color
+  var color/eax: int <- state-color self, 0x82/curx, 0x61/cury
+  draw-rect screen, 0x328/xmin 0x188/ymin, 0x358/xmax  0x1b8/ymax, color
+  draw-rect screen, 0x328/xmin 0x1f0/ymin, 0x358/xmax  0x220/ymax, color
+  var color/eax: int <- state-color self, 0x7f/curx, 0x62/cury
+  draw-rect screen,  0xa8/xmin 0x2a8/ymin,  0xd8/xmax  0x2d8/ymax, color
+  var color/eax: int <- state-color self, 0x80/curx, 0x62/cury
+  draw-rect screen, 0x158/xmin 0x2a8/ymin, 0x188/xmax  0x2d8/ymax, color
+  draw-rect screen, 0x1c8/xmin 0x2a8/ymin, 0x1f8/xmax  0x2d8/ymax, color
+  var color/eax: int <- state-color self, 0x81/curx, 0x62/cury
+  draw-rect screen, 0x208/xmin 0x2a8/ymin, 0x238/xmax  0x2d8/ymax, color
+  draw-rect screen, 0x278/xmin 0x2a8/ymin, 0x2a8/xmax  0x2d8/ymax, color
+  var color/eax: int <- state-color self, 0x82/curx, 0x62/cury
+  draw-rect screen, 0x328/xmin 0x2a8/ymin, 0x358/xmax  0x2d8/ymax, color
+  # cell 0: sum and filter nodes
+  draw-rect screen, 0x148/xsmin  0xc8/ysmin, 0x158/xsmax  0xd8/ysmax, 0x40/color
+  draw-rect screen, 0x180/xfmin  0xf8/yfmin, 0x190/xfmax 0x108/yfmax, 0x31/color
+  # cell 1: sum and filter nodes
+  draw-rect screen, 0x268/xsmin  0xc8/ysmin, 0x278/xsmax  0xd8/ysmax, 0x40/color
+  draw-rect screen, 0x2a0/xfmin  0xf8/yfmin, 0x2b0/xfmax 0x108/yfmax, 0x31/color
+  # cell 2: sum and filter nodes
+  draw-rect screen, 0x148/xsmin 0x1e8/ysmin, 0x158/xsmax 0x1f8/ysmax, 0x40/color
+  draw-rect screen, 0x180/xfmin 0x218/yfmin, 0x190/xfmax 0x228/yfmax, 0x31/color
+  # cell 3: sum and filter nodes
+  draw-rect screen, 0x268/xsmin 0x1e8/ysmin, 0x278/xsmax 0x1f8/ysmax, 0x40/color
+  draw-rect screen, 0x2a0/xfmin 0x218/yfmin, 0x2b0/xfmax 0x228/yfmax, 0x31/color
+  # neighbor counts
+  var n/eax: int <- num-live-neighbors self, 0x80/curx, 0x60/cury
+  set-cursor-position screen, 0x2d, 0xe
+  draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, n, 0xf/fg 0/bg
+  var n/eax: int <- num-live-neighbors self, 0x81/curx, 0x60/cury
+  set-cursor-position screen, 0x52, 0xe
+  draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, n, 0xf/fg 0/bg
+  var n/eax: int <- num-live-neighbors self, 0x80/curx, 0x61/cury
+  set-cursor-position screen, 0x2d, 0x20
+  draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, n, 0xf/fg 0/bg
+  var n/eax: int <- num-live-neighbors self, 0x81/curx, 0x61/cury
+  set-cursor-position screen, 0x52, 0x20
+  draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, n, 0xf/fg 0/bg
+  # cell 0: conveyors from neighboring inputs to sum node
+  draw-monotonic-bezier screen,  0xc0/x0  0x40/y0,  0x100/x1  0xd0/ys, 0x150/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen,  0xc0/x0  0xe8/y0,   0xc0/x1  0xd0/ys, 0x150/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen,  0xc0/x0 0x1a0/y0,   0xe0/x1  0xd0/ys, 0x150/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen, 0x170/x0  0x40/y0,  0x150/x1  0x80/y1, 0x150/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen, 0x170/x0 0x1a0/y0,  0x150/x1 0x1a0/y1, 0x150/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen, 0x220/x0  0x40/y0,  0x150/x1  0x80/y1, 0x150/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen, 0x220/x0  0xe8/y0,  0x220/x1  0xd0/y1, 0x150/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen, 0x220/x0 0x1a0/y0,  0x180/x1 0x1a0/y1, 0x150/xs  0xd0/ys,  4/color
+  # cell 0: conveyors from filter to outputs
+  draw-monotonic-bezier screen, 0x188/xf 0x100/yf,  0x160/x1  0x8c/y1, 0x100/x2  0x80/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x100/yf,  0x100/x1 0x100/y1, 0x100/x2  0xe8/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x100/yf,  0x100/x1 0x100/y1, 0x100/x2 0x160/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x100/yf,  0x188/x1  0x80/y1, 0x170/x2  0x80/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x100/yf,  0x188/x1 0x160/y1, 0x170/x2 0x160/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x100/yf,  0x1e0/x1 0x100/y1, 0x1e0/x2  0x80/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x100/yf,  0x1e0/x1 0x100/y1  0x1e0/x2  0xe8/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x100/yf,  0x1e0/x1 0x100/y1, 0x1e0/x2 0x160/y2,  0x2a/color
+  # cell 0: time-variant portion: 16 repeating steps
+  $render1:cell0: {
+    var tick-a/eax: (addr int) <- get self, tick
+    var progress/eax: int <- copy *tick-a
+    progress <- and 0xf
+    # cell 0: 7 time steps for getting inputs to sum
+    {
+      compare progress, 7
+      break-if->=
+      var u/xmm7: float <- convert progress
+      var six/eax: int <- copy 6
+      var six-f/xmm0: float <- convert six
+      u <- divide six-f
+      # points on conveyors from neighboring cells
+      draw-bezier-point screen, u,  0xc0/x0  0x40/y0, 0x100/x1  0xd0/ys, 0x150/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u,  0xc0/x0  0xe8/y0,  0xc0/x1  0xd0/ys, 0x150/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u,  0xc0/x0 0x1a0/y0,  0xe0/x1  0xd0/ys, 0x150/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x170/x0  0x40/y0, 0x150/x1  0x80/y1, 0x150/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x170/x0 0x1a0/y0, 0x150/x1 0x1a0/y1, 0x150/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x220/x0  0x40/y0, 0x150/x1  0x80/y1, 0x150/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x220/x0  0xe8/y0, 0x220/x1  0xd0/y1, 0x150/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x220/x0 0x1a0/y0, 0x180/x1 0x1a0/y1, 0x150/xs  0xd0/ys, 7/color, 4/radius
+      break $render1:cell0
+    }
+    # cell 0: two time steps for getting count to filter
+    progress <- subtract 7
+    {
+      compare progress, 2
+      break-if->=
+      break $render1:cell0
+    }
+    # cell 0: final 7 time steps for updating output
+    progress <- subtract 2
+    # cell 0: points on conveyors to outputs
+    var u/xmm7: float <- convert progress
+    var six/eax: int <- copy 6
+    var six-f/xmm0: float <- convert six
+    u <- divide six-f
+    draw-bezier-point screen, u, 0x188/xf 0x100/yf,  0x160/x1  0x8c/y1, 0x100/x2  0x80/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x100/yf,  0x100/x1 0x100/y1, 0x100/x2  0xe8/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x100/yf,  0x100/x1 0x100/y1, 0x100/x2 0x160/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x100/yf,  0x188/xf  0x80/y1, 0x170/x2  0x80/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x100/yf,  0x188/xf 0x160/y1, 0x170/x2 0x160/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x100/yf,  0x1e0/x1 0x100/y1, 0x1e0/x2  0x80/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x100/yf,  0x1e0/x1 0x100/y1, 0x1e0/x2  0xe8/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x100/yf,  0x1e0/x1 0x100/y1, 0x1e0/x2 0x160/y2, 7/color, 4/radius
+  }
+  # cell 1: conveyors from neighboring inputs to sum node
+  draw-monotonic-bezier screen, 0x1e0/x0  0x40/y0,  0x220/x1  0xd0/ys, 0x270/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen, 0x1e0/x0  0xe8/y0,  0x1e0/x1  0xd0/ys, 0x270/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen, 0x1e0/x0 0x1a0/y0,  0x200/x1  0xd0/ys, 0x270/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen, 0x290/x0  0x40/y0,  0x270/x1  0x80/y1, 0x270/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen, 0x290/x0 0x1a0/y0,  0x270/x1 0x1a0/y1, 0x270/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen, 0x340/x0  0x40/y0,  0x270/x1  0x80/y1, 0x270/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen, 0x340/x0  0xe8/y0,  0x340/x1  0xd0/y1, 0x270/xs  0xd0/ys,  4/color
+  draw-monotonic-bezier screen, 0x340/x0 0x1a0/y0,  0x2a0/x1 0x1a0/y1, 0x270/xs  0xd0/ys,  4/color
+  # cell 1: conveyors from filter to outputs
+  draw-monotonic-bezier screen, 0x2a8/xf 0x100/yf,  0x280/x1  0x8c/y1, 0x220/x2  0x80/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x100/yf,  0x220/x1 0x100/y1, 0x220/x2  0xe8/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x100/yf,  0x220/x1 0x100/y1, 0x220/x2 0x160/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x100/yf,  0x2a8/x1  0x80/y1, 0x290/x2  0x80/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x100/yf,  0x2a8/x1 0x160/y1, 0x290/x2 0x160/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x100/yf,  0x300/x1 0x100/y1, 0x300/x2  0x80/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x100/yf,  0x300/x1 0x100/y1  0x300/x2  0xe8/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x100/yf,  0x300/x1 0x100/y1, 0x300/x2 0x160/y2,  0x2a/color
+  # cell 1: time-variant portion: 16 repeating steps
+  $render1:cell1: {
+    var tick-a/eax: (addr int) <- get self, tick
+    var progress/eax: int <- copy *tick-a
+    progress <- and 0xf
+    # cell 1: 7 time steps for getting inputs to sum
+    {
+      compare progress, 7
+      break-if->=
+      var u/xmm7: float <- convert progress
+      var six/eax: int <- copy 6
+      var six-f/xmm0: float <- convert six
+      u <- divide six-f
+      # points on conveyors from neighboring cells
+      draw-bezier-point screen, u, 0x1e0/x0  0x40/y0, 0x220/x1  0xd0/ys, 0x270/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x1e0/x0  0xe8/y0, 0x1e0/x1  0xd0/ys, 0x270/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x1e0/x0 0x1a0/y0, 0x200/x1  0xd0/ys, 0x270/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x290/x0  0x40/y0, 0x270/x1  0x80/y1, 0x270/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x290/x0 0x1a0/y0, 0x270/x1 0x1a0/y1, 0x270/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x340/x0  0x40/y0, 0x270/x1  0x80/y1, 0x270/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x340/x0  0xe8/y0, 0x340/x1  0xd0/y1, 0x270/xs  0xd0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x340/x0 0x1a0/y0, 0x2a0/x1 0x1a0/y1, 0x270/xs  0xd0/ys, 7/color, 4/radius
+      break $render1:cell1
+    }
+    # cell 1: two time steps for getting count to filter
+    progress <- subtract 7
+    {
+      compare progress, 2
+      break-if->=
+      break $render1:cell1
+    }
+    # cell 1: final 7 time steps for updating output
+    progress <- subtract 2
+    # cell 1: points on conveyors to outputs
+    var u/xmm7: float <- convert progress
+    var six/eax: int <- copy 6
+    var six-f/xmm0: float <- convert six
+    u <- divide six-f
+    draw-bezier-point screen, u, 0x2a8/xf 0x100/yf,  0x280/x1  0x8c/y1, 0x220/x2  0x80/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x100/yf,  0x220/x1 0x100/y1, 0x220/x2  0xe8/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x100/yf,  0x220/x1 0x100/y1, 0x220/x2 0x160/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x100/yf,  0x2a8/xf  0x80/y1, 0x290/x2  0x80/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x100/yf,  0x2a8/xf 0x160/y1, 0x290/x2 0x160/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x100/yf,  0x300/x1 0x100/y1, 0x300/x2  0x80/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x100/yf,  0x300/x1 0x100/y1, 0x300/x2  0xe8/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x100/yf,  0x300/x1 0x100/y1, 0x300/x2 0x160/y2, 7/color, 4/radius
+  }
+  # cell 2: conveyors from neighboring inputs to sum node
+  draw-monotonic-bezier screen,  0xc0/x0 0x160/y0,  0x100/x1 0x1f0/ys, 0x150/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen,  0xc0/x0 0x208/y0,   0xc0/x1 0x1f0/ys, 0x150/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen,  0xc0/x0 0x2c0/y0,   0xe0/x1 0x1f0/ys, 0x150/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen, 0x170/x0 0x160/y0,  0x150/x1 0x1a0/y1, 0x150/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen, 0x170/x0 0x2c0/y0,  0x150/x1 0x2c0/y1, 0x150/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen, 0x220/x0 0x160/y0,  0x150/x1 0x1a0/y1, 0x150/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen, 0x220/x0 0x208/y0,  0x220/x1 0x1f0/y1, 0x150/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen, 0x220/x0 0x2c0/y0,  0x180/x1 0x2c0/y1, 0x150/xs 0x1f0/ys,  4/color
+  # cell 2: conveyors from filter to outputs
+  draw-monotonic-bezier screen, 0x188/xf 0x220/yf,  0x160/x1 0x1ac/y1, 0x100/x2 0x1a0/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x220/yf,  0x100/x1 0x220/y1, 0x100/x2 0x208/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x220/yf,  0x100/x1 0x220/y1, 0x100/x2 0x280/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x220/yf,  0x188/x1 0x1a0/y1, 0x170/x2 0x1a0/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x220/yf,  0x188/x1 0x280/y1, 0x170/x2 0x280/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x220/yf,  0x1e0/x1 0x220/y1, 0x1e0/x2 0x1a0/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x220/yf,  0x1e0/x1 0x220/y1  0x1e0/x2 0x208/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x188/xf 0x220/yf,  0x1e0/x1 0x220/y1, 0x1e0/x2 0x280/y2,  0x2a/color
+  # cell 2: time-variant portion: 16 repeating steps
+  $render1:cell2: {
+    var tick-a/eax: (addr int) <- get self, tick
+    var progress/eax: int <- copy *tick-a
+    progress <- and 0xf
+    # cell 2: 7 time steps for getting inputs to sum
+    {
+      compare progress, 7
+      break-if->=
+      var u/xmm7: float <- convert progress
+      var six/eax: int <- copy 6
+      var six-f/xmm0: float <- convert six
+      u <- divide six-f
+      # points on conveyors from neighboring cells
+      draw-bezier-point screen, u,  0xc0/x0 0x160/y0, 0x100/x1 0x1f0/ys, 0x150/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u,  0xc0/x0 0x208/y0,  0xc0/x1 0x1f0/ys, 0x150/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u,  0xc0/x0 0x2c0/y0,  0xe0/x1 0x1f0/ys, 0x150/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x170/x0 0x160/y0, 0x150/x1 0x1a0/y1, 0x150/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x170/x0 0x2c0/y0, 0x150/x1 0x2c0/y1, 0x150/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x220/x0 0x160/y0, 0x150/x1 0x1a0/y1, 0x150/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x220/x0 0x208/y0, 0x220/x1 0x1f0/y1, 0x150/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x220/x0 0x2c0/y0, 0x180/x1 0x2c0/y1, 0x150/xs 0x1f0/ys, 7/color, 4/radius
+      break $render1:cell2
+    }
+    # cell 2: two time steps for getting count to filter
+    progress <- subtract 7
+    {
+      compare progress, 2
+      break-if->=
+      break $render1:cell2
+    }
+    # cell 2: final 7 time steps for updating output
+    progress <- subtract 2
+    # cell 2: points on conveyors to outputs
+    var u/xmm7: float <- convert progress
+    var six/eax: int <- copy 6
+    var six-f/xmm0: float <- convert six
+    u <- divide six-f
+    draw-bezier-point screen, u, 0x188/xf 0x220/yf,  0x160/x1 0x1ac/y1, 0x100/x2 0x1a0/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x220/yf,  0x100/x1 0x220/y1, 0x100/x2 0x208/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x220/yf,  0x100/x1 0x220/y1, 0x100/x2 0x280/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x220/yf,  0x188/xf 0x1a0/y1, 0x170/x2 0x1a0/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x220/yf,  0x188/xf 0x280/y1, 0x170/x2 0x280/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x220/yf,  0x1e0/x1 0x220/y1, 0x1e0/x2 0x1a0/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x220/yf,  0x1e0/x1 0x220/y1, 0x1e0/x2 0x208/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x188/xf 0x220/yf,  0x1e0/x1 0x220/y1, 0x1e0/x2 0x280/y2, 7/color, 4/radius
+  }
+  # cell 3: conveyors from neighboring inputs to sum node
+  draw-monotonic-bezier screen, 0x1e0/x0 0x160/y0,  0x220/x1 0x1f0/ys, 0x270/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen, 0x1e0/x0 0x208/y0,  0x1e0/x1 0x1f0/ys, 0x270/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen, 0x1e0/x0 0x2c0/y0,  0x200/x1 0x1f0/ys, 0x270/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen, 0x290/x0 0x160/y0,  0x270/x1 0x1a0/y1, 0x270/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen, 0x290/x0 0x2c0/y0,  0x270/x1 0x2c0/y1, 0x270/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen, 0x340/x0 0x160/y0,  0x270/x1 0x1a0/y1, 0x270/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen, 0x340/x0 0x208/y0,  0x340/x1 0x1f0/y1, 0x270/xs 0x1f0/ys,  4/color
+  draw-monotonic-bezier screen, 0x340/x0 0x2c0/y0,  0x2a0/x1 0x2c0/y1, 0x270/xs 0x1f0/ys,  4/color
+  # cell 3: conveyors from filter to outputs
+  draw-monotonic-bezier screen, 0x2a8/xf 0x220/yf,  0x280/x1 0x1ac/y1, 0x220/x2 0x1a0/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x220/yf,  0x220/x1 0x220/y1, 0x220/x2 0x208/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x220/yf,  0x220/x1 0x220/y1, 0x220/x2 0x280/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x220/yf,  0x2a8/x1 0x1a0/y1, 0x290/x2 0x1a0/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x220/yf,  0x2a8/x1 0x280/y1, 0x290/x2 0x280/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x220/yf,  0x300/x1 0x220/y1, 0x300/x2 0x1a0/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x220/yf,  0x300/x1 0x220/y1  0x300/x2 0x208/y2,  0x2a/color
+  draw-monotonic-bezier screen, 0x2a8/xf 0x220/yf,  0x300/x1 0x220/y1, 0x300/x2 0x280/y2,  0x2a/color
+  # cell 3: time-variant portion: 16 repeating steps
+  $render1:cell3: {
+    var tick-a/eax: (addr int) <- get self, tick
+    var progress/eax: int <- copy *tick-a
+    progress <- and 0xf
+    # cell 3: 7 time steps for getting inputs to sum
+    {
+      compare progress, 7
+      break-if->=
+      var u/xmm7: float <- convert progress
+      var six/eax: int <- copy 6
+      var six-f/xmm0: float <- convert six
+      u <- divide six-f
+      # points on conveyors from neighboring cells
+      draw-bezier-point screen, u, 0x1e0/x0 0x160/y0, 0x220/x1 0x1f0/ys, 0x270/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x1e0/x0 0x208/y0, 0x1e0/x1 0x1f0/ys, 0x270/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x1e0/x0 0x2c0/y0, 0x200/x1 0x1f0/ys, 0x270/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x290/x0 0x160/y0, 0x270/x1 0x1a0/y1, 0x270/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x290/x0 0x2c0/y0, 0x270/x1 0x2c0/y1, 0x270/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x340/x0 0x160/y0, 0x270/x1 0x1a0/y1, 0x270/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x340/x0 0x208/y0, 0x340/x1 0x1f0/y1, 0x270/xs 0x1f0/ys, 7/color, 4/radius
+      draw-bezier-point screen, u, 0x340/x0 0x2c0/y0, 0x2a0/x1 0x2c0/y1, 0x270/xs 0x1f0/ys, 7/color, 4/radius
+      break $render1:cell3
+    }
+    # cell 3: two time steps for getting count to filter
+    progress <- subtract 7
+    {
+      compare progress, 2
+      break-if->=
+      break $render1:cell3
+    }
+    # cell 3: final 7 time steps for updating output
+    progress <- subtract 2
+    # cell 3: points on conveyors to outputs
+    var u/xmm7: float <- convert progress
+    var six/eax: int <- copy 6
+    var six-f/xmm0: float <- convert six
+    u <- divide six-f
+    draw-bezier-point screen, u, 0x2a8/xf 0x220/yf,  0x280/x1 0x1ac/y1, 0x220/x2 0x1a0/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x220/yf,  0x220/x1 0x220/y1, 0x220/x2 0x208/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x220/yf,  0x220/x1 0x220/y1, 0x220/x2 0x280/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x220/yf,  0x2a8/xf 0x1a0/y1, 0x290/x2 0x1a0/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x220/yf,  0x2a8/xf 0x280/y1, 0x290/x2 0x280/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x220/yf,  0x300/x1 0x220/y1, 0x300/x2 0x1a0/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x220/yf,  0x300/x1 0x220/y1, 0x300/x2 0x208/y2, 7/color, 4/radius
+    draw-bezier-point screen, u, 0x2a8/xf 0x220/yf,  0x300/x1 0x220/y1, 0x300/x2 0x280/y2, 7/color, 4/radius
+  }
+}
+
+fn draw-bezier-point screen: (addr screen), u: float, x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, color: int, radius: int {
+  var _cy/eax: int <- bezier-point u, y0, y1, y2
+  var cy/ecx: int <- copy _cy
+  var cx/eax: int <- bezier-point u, x0, x1, x2
+  draw-disc screen, cx, cy, radius, color, 0xf/border-color=white
+}
+
+fn draw-linear-point screen: (addr screen), u: float, x0: int, y0: int, x1: int, y1: int, color: int, radius: int {
+  var _cy/eax: int <- line-point u, y0, y1
+  var cy/ecx: int <- copy _cy
+  var cx/eax: int <- line-point u, x0, x1
+  draw-disc screen, cx, cy, radius, color, 0xf/border-color=white
+}
+
+fn edit keyboard: (addr keyboard), _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var key/eax: byte <- read-key keyboard
+  # space: play/pause
+  {
+    compare key, 0x20/space
+    break-if-!=
+    var play?/eax: (addr boolean) <- get self, play?
+    compare *play?, 0/false
+    {
+      break-if-=
+      copy-to *play?, 0/false
+      return
+    }
+    copy-to *play?, 1/true
+    return
+  }
+  # 0: back to start
+  {
+    compare key, 0x30/0
+    break-if-!=
+    clear-environment self
+    return
+  }
+  # l: loop from here to start
+  {
+    compare key, 0x6c/l
+    break-if-!=
+    var tick-a/eax: (addr int) <- get self, tick
+    var tick/eax: int <- copy *tick-a
+    var loop/ecx: (addr int) <- get self, loop
+    copy-to *loop, tick
+    return
+  }
+  # L: reset loop
+  {
+    compare key, 0x4c/L
+    break-if-!=
+    var loop/eax: (addr int) <- get self, loop
+    copy-to *loop, 0
+    return
+  }
+  # -: zoom out
+  {
+    compare key, 0x2d/-
+    break-if-!=
+    var zoom/eax: (addr int) <- get self, zoom
+    compare *zoom, 1
+    {
+      break-if-!=
+      copy-to *zoom, 4
+    }
+    compare *zoom, 0
+    {
+      break-if-!=
+      copy-to *zoom, 1
+    }
+    # set tick to a multiple of zoom
+    var tick-a/edx: (addr int) <- get self, tick
+    clear-lowest-bits tick-a, *zoom
+    return
+  }
+  # +: zoom in
+  {
+    compare key, 0x2b/+
+    break-if-!=
+    var zoom/eax: (addr int) <- get self, zoom
+    compare *zoom, 1
+    {
+      break-if-!=
+      copy-to *zoom, 0
+    }
+    compare *zoom, 4
+    {
+      break-if-!=
+      copy-to *zoom, 1
+    }
+    # set tick to a multiple of zoom
+    var tick-a/edx: (addr int) <- get self, tick
+    clear-lowest-bits tick-a, *zoom
+    return
+  }
+}
+
+fn step _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var tick-a/ecx: (addr int) <- get self, tick
+  var zoom/edx: (addr int) <- get self, zoom
+  compare *zoom, 0
+  {
+    break-if-!=
+    increment *tick-a
+  }
+  compare *zoom, 1
+  {
+    break-if-!=
+    # I wanted to speed up time, but that doesn't seem very usable.
+#?     add-to *tick-a, 2
+    increment *tick-a
+  }
+  compare *zoom, 4
+  {
+    break-if-!=
+    add-to *tick-a, 0x10
+  }
+  var tick/eax: int <- copy *tick-a
+  tick <- and 0xf
+  compare tick, 0
+  {
+    break-if-!=
+    step4 self
+  }
+  var loop-a/eax: (addr int) <- get self, loop
+  compare *loop-a, 0
+  {
+    break-if-=
+    var loop/eax: int <- copy *loop-a
+    compare *tick-a, loop
+    break-if-<
+    clear-environment self
+  }
+}
+
+fn initialize-environment _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var zoom/eax: (addr int) <- get self, zoom
+  copy-to *zoom, 0
+  var play?/eax: (addr boolean) <- get self, play?
+  copy-to *play?, 1/true
+  var data-ah/eax: (addr handle array handle array cell) <- get self, data
+  populate data-ah, 0x100
+  var data/eax: (addr array handle array cell) <- lookup *data-ah
+  var y/ecx: int <- copy 0
+  {
+    compare y, 0xc0
+    break-if->=
+    var dest-ah/eax: (addr handle array cell) <- index data, y
+    populate dest-ah, 0x100
+    y <- increment
+    loop
+  }
+  set self, 0x80, 0x5f, 1/alive
+  set self, 0x81, 0x5f, 1/alive
+  set self, 0x7f, 0x60, 1/alive
+  set self, 0x80, 0x60, 1/alive
+  set self, 0x80, 0x61, 1/alive
+  flush self
+}
+
+fn clear-environment _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var tick/eax: (addr int) <- get self, tick
+  copy-to *tick, 0
+  # don't touch zoom or play settings
+  var data-ah/eax: (addr handle array handle array cell) <- get self, data
+  var data/eax: (addr array handle array cell) <- lookup *data-ah
+  var y/ecx: int <- copy 0
+  {
+    compare y, 0xc0
+    break-if->=
+    var row-ah/eax: (addr handle array cell) <- index data, y
+    var row/eax: (addr array cell) <- lookup *row-ah
+    var x/edx: int <- copy 0
+    {
+      compare x, 0x100
+      break-if->=
+      var dest/eax: (addr cell) <- index row, x
+      clear-object dest
+      x <- increment
+      loop
+    }
+    y <- increment
+    loop
+  }
+  set self, 0x80, 0x5f, 1/alive
+  set self, 0x81, 0x5f, 1/alive
+  set self, 0x7f, 0x60, 1/alive
+  set self, 0x80, 0x60, 1/alive
+  set self, 0x80, 0x61, 1/alive
+  flush self
+}
+
+fn set _self: (addr environment), _x: int, _y: int, _val: boolean {
+  var self/esi: (addr environment) <- copy _self
+  var data-ah/eax: (addr handle array handle array cell) <- get self, data
+  var data/eax: (addr array handle array cell) <- lookup *data-ah
+  var y/ecx: int <- copy _y
+  var row-ah/eax: (addr handle array cell) <- index data, y
+  var row/eax: (addr array cell) <- lookup *row-ah
+  var x/ecx: int <- copy _x
+  var cell/eax: (addr cell) <- index row, x
+  var dest/eax: (addr boolean) <- get cell, next
+  var val/ecx: boolean <- copy _val
+  copy-to *dest, val
+}
+
+fn state _self: (addr environment), _x: int, _y: int -> _/eax: boolean {
+  var self/esi: (addr environment) <- copy _self
+  var x/ecx: int <- copy _x
+  var y/edx: int <- copy _y
+  # clip at the edge
+  compare x, 0
+  {
+    break-if->=
+    return 0/false
+  }
+  compare y, 0
+  {
+    break-if->=
+    return 0/false
+  }
+  compare x, 0x100/width
+  {
+    break-if-<
+    return 0/false
+  }
+  compare y, 0xc0/height
+  {
+    break-if-<
+    return 0/false
+  }
+  var data-ah/eax: (addr handle array handle array cell) <- get self, data
+  var data/eax: (addr array handle array cell) <- lookup *data-ah
+  var row-ah/eax: (addr handle array cell) <- index data, y
+  var row/eax: (addr array cell) <- lookup *row-ah
+  var cell/eax: (addr cell) <- index row, x
+  var src/eax: (addr boolean) <- get cell, curr
+  return *src
+}
+
+fn state-color _self: (addr environment), x: int, y: int -> _/eax: int {
+  var self/esi: (addr environment) <- copy _self
+  var color/ecx: int <- copy 0x1a/dead
+  {
+    var state/eax: boolean <- state self, x, y
+    compare state, 0/dead
+    break-if-=
+    color <- copy 0xf/alive
+  }
+  return color
+}
+
+fn flush  _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var data-ah/eax: (addr handle array handle array cell) <- get self, data
+  var _data/eax: (addr array handle array cell) <- lookup *data-ah
+  var data/esi: (addr array handle array cell) <- copy _data
+  var y/ecx: int <- copy 0
+  {
+    compare y, 0xc0/height
+    break-if->=
+    var row-ah/eax: (addr handle array cell) <- index data, y
+    var _row/eax: (addr array cell) <- lookup *row-ah
+    var row/ebx: (addr array cell) <- copy _row
+    var x/edx: int <- copy 0
+    {
+      compare x, 0x100/width
+      break-if->=
+      var cell-a/eax: (addr cell) <- index row, x
+      var curr-a/edi: (addr boolean) <- get cell-a, curr
+      var next-a/esi: (addr boolean) <- get cell-a, next
+      var val/eax: boolean <- copy *next-a
+      copy-to *curr-a, val
+      copy-to *next-a, 0/dead
+      x <- increment
+      loop
+    }
+    y <- increment
+    loop
+  }
+}
+
+fn render4 screen: (addr screen), _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var y/ecx: int <- copy 0
+  {
+    compare y, 0xc0/height
+    break-if->=
+    var x/edx: int <- copy 0
+    {
+      compare x, 0x100/width
+      break-if->=
+      var state/eax: boolean <- state self, x, y
+      compare state, 0/false
+      {
+        break-if-=
+        render4-cell screen, x, y, 0xf/alive
+      }
+      compare state, 0/false
+      {
+        break-if-!=
+        render4-cell screen, x, y, 0x1a/dead
+      }
+      x <- increment
+      loop
+    }
+    y <- increment
+    loop
+  }
+}
+
+fn render4-cell screen: (addr screen), x: int, y: int, color: int {
+  var xmin/eax: int <- copy x
+  xmin <- shift-left 2
+  var xmax/ecx: int <- copy xmin
+  xmax <- add 4
+  var ymin/edx: int <- copy y
+  ymin <- shift-left 2
+  var ymax/ebx: int <- copy ymin
+  ymax <- add 4
+  draw-rect screen, xmin ymin, xmax ymax, color
+}
+
+fn step4 _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var y/ecx: int <- copy 0
+  {
+    compare y, 0xc0/height
+    break-if->=
+    var x/edx: int <- copy 0
+    {
+      compare x, 0x100/width
+      break-if->=
+      var n/eax: int <- num-live-neighbors self, x, y
+      # if neighbors < 2, die of loneliness
+      {
+        compare n, 2
+        break-if->=
+        set self, x, y, 0/dead
+      }
+      # if neighbors > 3, die of overcrowding
+      {
+        compare n, 3
+        break-if-<=
+        set self, x, y, 0/dead
+      }
+      # if neighbors = 2, preserve state
+      {
+        compare n, 2
+        break-if-!=
+        var old-state/eax: boolean <- state self, x, y
+        set self, x, y, old-state
+      }
+      # if neighbors = 3, cell quickens to life
+      {
+        compare n, 3
+        break-if-!=
+        set self, x, y, 1/live
+      }
+      x <- increment
+      loop
+    }
+    y <- increment
+    loop
+  }
+  flush self
+}
+
+fn num-live-neighbors _self: (addr environment), x: int, y: int -> _/eax: int {
+  var self/esi: (addr environment) <- copy _self
+  var result/edi: int <- copy 0
+  # row above: zig
+  decrement y
+  decrement x
+  var s/eax: boolean <- state self, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  increment x
+  s <- state self, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  increment x
+  s <- state self, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  # curr row: zag
+  increment y
+  s <- state self, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  subtract-from x, 2
+  s <- state self, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  # row below: zig
+  increment y
+  s <- state self, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  increment x
+  s <- state self, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  increment x
+  s <- state self, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  return result
+}
+
+fn linger _self: (addr environment) {
+  var self/esi: (addr environment) <- copy _self
+  var i/ecx: int <- copy 0
+  {
+    compare i, 0x10000000  # Kartik's Linux with -enable-kvm
+#?     compare i, 0x8000000  # Kartik's Mac with -accel tcg
+    break-if->=
+    i <- increment
+    loop
+  }
+}
diff --git a/apps/img.mu b/apps/img.mu
new file mode 100644
index 00000000..65b773c5
--- /dev/null
+++ b/apps/img.mu
@@ -0,0 +1,1148 @@
+# load an image from disk and display it on screen
+#
+# To build:
+#   $ ./translate apps/img.mu                       # generates code.img
+# Load a pbm, pgm or ppm image (no more than 255 levels) in the data disk
+#   $ dd if=/dev/zero of=data.img count=20160
+#   $ dd if=x.pbm of=data.img conv=notrunc
+# or
+#   $ dd if=t.pgm of=data.img conv=notrunc
+# or
+#   $ dd if=snail.ppm of=data.img conv=notrunc
+# To run:
+#   $ qemu-system-i386 -hda code.img -hdb data.img
+
+type image {
+  type: int  # supported types:
+             #  1: portable bitmap (P1) - pixels 0 or 1
+             #  2: portable greymap (P2) - pixels 1-byte greyscale values
+             #  3: portable pixmap (P3) - pixels 3-byte rgb values
+  max: int
+  width: int
+  height: int
+  data: (handle array byte)
+}
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  var img-storage: image
+  var img/esi: (addr image) <- address img-storage
+  load-image img, data-disk
+  render-image screen, img, 0/x, 0/y, 0x300/width, 0x300/height
+}
+
+fn load-image self: (addr image), data-disk: (addr disk) {
+  # data-disk -> stream
+  var s-storage: (stream byte 0x200000)  # 512* 0x1000 sectors
+  var s/ebx: (addr stream byte) <- address s-storage
+  draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, "loading sectors from data disk", 3/fg, 0/bg
+  move-cursor-to-left-margin-of-next-line 0/screen
+  load-sectors data-disk, 0/lba, 0x100/sectors, s
+  load-sectors data-disk, 0x100/lba, 0x100/sectors, s
+  load-sectors data-disk, 0x200/lba, 0x100/sectors, s
+  load-sectors data-disk, 0x300/lba, 0x100/sectors, s
+  load-sectors data-disk, 0x400/lba, 0x100/sectors, s
+  load-sectors data-disk, 0x500/lba, 0x100/sectors, s
+  load-sectors data-disk, 0x600/lba, 0x100/sectors, s
+  load-sectors data-disk, 0x700/lba, 0x100/sectors, s
+  load-sectors data-disk, 0x800/lba, 0x100/sectors, s
+  load-sectors data-disk, 0x900/lba, 0x100/sectors, s
+  load-sectors data-disk, 0xa00/lba, 0x100/sectors, s
+  load-sectors data-disk, 0xb00/lba, 0x100/sectors, s
+  load-sectors data-disk, 0xc00/lba, 0x100/sectors, s
+  load-sectors data-disk, 0xd00/lba, 0x100/sectors, s
+  load-sectors data-disk, 0xe00/lba, 0x100/sectors, s
+  load-sectors data-disk, 0xf00/lba, 0x100/sectors, s
+  draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, "parsing", 3/fg, 0/bg
+  move-cursor-to-left-margin-of-next-line 0/screen
+  initialize-image self, s
+}
+
+fn initialize-image _self: (addr image), in: (addr stream byte) {
+  var self/esi: (addr image) <- copy _self
+  var mode-storage: slice
+  var mode/ecx: (addr slice) <- address mode-storage
+  next-word in, mode
+  {
+    var P1?/eax: boolean <- slice-equal? mode, "P1"
+    compare P1?, 0/false
+    break-if-=
+    var type-a/eax: (addr int) <- get self, type
+    copy-to *type-a, 1/ppm
+    initialize-image-from-pbm self, in
+    return
+  }
+  {
+    var P2?/eax: boolean <- slice-equal? mode, "P2"
+    compare P2?, 0/false
+    break-if-=
+    var type-a/eax: (addr int) <- get self, type
+    copy-to *type-a, 2/pgm
+    initialize-image-from-pgm self, in
+    return
+  }
+  {
+    var P3?/eax: boolean <- slice-equal? mode, "P3"
+    compare P3?, 0/false
+    break-if-=
+    var type-a/eax: (addr int) <- get self, type
+    copy-to *type-a, 3/ppm
+    initialize-image-from-ppm self, in
+    return
+  }
+  abort "initialize-image: unrecognized image type"
+}
+
+# dispatch to a few variants with mostly identical boilerplate
+fn render-image screen: (addr screen), _img: (addr image), xmin: int, ymin: int, width: int, height: int {
+  var img/esi: (addr image) <- copy _img
+  var type-a/eax: (addr int) <- get img, type
+  {
+    compare *type-a, 1/pbm
+    break-if-!=
+    render-pbm-image screen, img, xmin, ymin, width, height
+    return
+  }
+  {
+    compare *type-a, 2/pgm
+    break-if-!=
+    var img2-storage: image
+    var img2/edi: (addr image) <- address img2-storage
+    dither-pgm-unordered img, img2
+    render-raw-image screen, img2, xmin, ymin, width, height
+    return
+  }
+  {
+    compare *type-a, 3/ppm
+    break-if-!=
+    var img2-storage: image
+    var img2/edi: (addr image) <- address img2-storage
+    dither-ppm-unordered img, img2
+    render-raw-image screen, img2, xmin, ymin, width, height
+    return
+  }
+  abort "render-image: unrecognized image type"
+}
+
+## helpers
+
+# import a black-and-white ascii bitmap (each pixel is 0 or 1)
+fn initialize-image-from-pbm _self: (addr image), in: (addr stream byte) {
+  var self/esi: (addr image) <- copy _self
+  var curr-word-storage: slice
+  var curr-word/ecx: (addr slice) <- address curr-word-storage
+  # load width, height
+  next-word in, curr-word
+  var tmp/eax: int <- parse-decimal-int-from-slice curr-word
+  var width/edx: int <- copy tmp
+  next-word in, curr-word
+  tmp <- parse-decimal-int-from-slice curr-word
+  var height/ebx: int <- copy tmp
+  # save width, height
+  var dest/eax: (addr int) <- get self, width
+  copy-to *dest, width
+  dest <- get self, height
+  copy-to *dest, height
+  # initialize data
+  var capacity/edx: int <- copy width
+  capacity <- multiply height
+  var data-ah/edi: (addr handle array byte) <- get self, data
+  populate data-ah, capacity
+  var _data/eax: (addr array byte) <- lookup *data-ah
+  var data/edi: (addr array byte) <- copy _data
+  var i/ebx: int <- copy 0
+  {
+    compare i, capacity
+    break-if->=
+    next-word in, curr-word
+    var src/eax: int <- parse-decimal-int-from-slice curr-word
+    {
+      var dest/ecx: (addr byte) <- index data, i
+      copy-byte-to *dest, src
+    }
+    i <- increment
+    loop
+  }
+}
+
+# render a black-and-white ascii bitmap (each pixel is 0 or 1)
+fn render-pbm-image screen: (addr screen), _img: (addr image), xmin: int, ymin: int, width: int, height: int {
+  var img/esi: (addr image) <- copy _img
+  # yratio = height/img->height
+  var img-height-a/eax: (addr int) <- get img, height
+  var img-height/xmm0: float <- convert *img-height-a
+  var yratio/xmm1: float <- convert height
+  yratio <- divide img-height
+  # xratio = width/img->width
+  var img-width-a/eax: (addr int) <- get img, width
+  var img-width/ebx: int <- copy *img-width-a
+  var img-width-f/xmm0: float <- convert img-width
+  var xratio/xmm2: float <- convert width
+  xratio <- divide img-width-f
+  # esi = img->data
+  var img-data-ah/eax: (addr handle array byte) <- get img, data
+  var _img-data/eax: (addr array byte) <- lookup *img-data-ah
+  var img-data/esi: (addr array byte) <- copy _img-data
+  var len/edi: int <- length img-data
+  #
+  var one/eax: int <- copy 1
+  var one-f/xmm3: float <- convert one
+  var width-f/xmm4: float <- convert width
+  var height-f/xmm5: float <- convert height
+  var zero/eax: int <- copy 0
+  var zero-f/xmm0: float <- convert zero
+  var y/xmm6: float <- copy zero-f
+  {
+    compare y, height-f
+    break-if-float>=
+    var imgy-f/xmm5: float <- copy y
+    imgy-f <- divide yratio
+    var imgy/edx: int <- truncate imgy-f
+    var x/xmm7: float <- copy zero-f
+    {
+      compare x, width-f
+      break-if-float>=
+      var imgx-f/xmm5: float <- copy x
+      imgx-f <- divide xratio
+      var imgx/ecx: int <- truncate imgx-f
+      var idx/eax: int <- copy imgy
+      idx <- multiply img-width
+      idx <- add imgx
+      # error info in case we rounded wrong and 'index' will fail bounds-check
+      compare idx, len
+      {
+        break-if-<
+        set-cursor-position 0/screen, 0x20/x 0x20/y
+        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgx, 3/fg 0/bg
+        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgy, 4/fg 0/bg
+        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, idx, 5/fg 0/bg
+      }
+      var src-a/eax: (addr byte) <- index img-data, idx
+      var src/eax: byte <- copy-byte *src-a
+      var color-int/eax: int <- copy src
+      {
+        compare color-int, 0/black
+        break-if-=
+        color-int <- copy 0xf/white
+      }
+      var screenx/ecx: int <- convert x
+      screenx <- add xmin
+      var screeny/edx: int <- convert y
+      screeny <- add ymin
+      pixel screen, screenx, screeny, color-int
+      x <- add one-f
+      loop
+    }
+    y <- add one-f
+    loop
+  }
+}
+
+# import a greyscale ascii "greymap" (each pixel is a shade of grey from 0 to 255)
+fn initialize-image-from-pgm _self: (addr image), in: (addr stream byte) {
+  var self/esi: (addr image) <- copy _self
+  var curr-word-storage: slice
+  var curr-word/ecx: (addr slice) <- address curr-word-storage
+  # load width, height
+  next-word in, curr-word
+  var tmp/eax: int <- parse-decimal-int-from-slice curr-word
+  var width/edx: int <- copy tmp
+  next-word in, curr-word
+  tmp <- parse-decimal-int-from-slice curr-word
+  var height/ebx: int <- copy tmp
+  # check and save color levels
+  next-word in, curr-word
+  {
+    tmp <- parse-decimal-int-from-slice curr-word
+    compare tmp, 0xff
+    break-if-=
+    draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, "levels of grey is not 255; continuing and hoping for the best", 0x2b/fg 0/bg
+  }
+  var dest/edi: (addr int) <- get self, max
+  copy-to *dest, tmp
+  # save width, height
+  dest <- get self, width
+  copy-to *dest, width
+  dest <- get self, height
+  copy-to *dest, height
+  # initialize data
+  var capacity/edx: int <- copy width
+  capacity <- multiply height
+  var data-ah/edi: (addr handle array byte) <- get self, data
+  populate data-ah, capacity
+  var _data/eax: (addr array byte) <- lookup *data-ah
+  var data/edi: (addr array byte) <- copy _data
+  var i/ebx: int <- copy 0
+  {
+    compare i, capacity
+    break-if->=
+    next-word in, curr-word
+    var src/eax: int <- parse-decimal-int-from-slice curr-word
+    {
+      var dest/ecx: (addr byte) <- index data, i
+      copy-byte-to *dest, src
+    }
+    i <- increment
+    loop
+  }
+}
+
+# render a greyscale ascii "greymap" (each pixel is a shade of grey from 0 to 255) by quantizing the shades
+fn render-pgm-image screen: (addr screen), _img: (addr image), xmin: int, ymin: int, width: int, height: int {
+  var img/esi: (addr image) <- copy _img
+  # yratio = height/img->height
+  var img-height-a/eax: (addr int) <- get img, height
+  var img-height/xmm0: float <- convert *img-height-a
+  var yratio/xmm1: float <- convert height
+  yratio <- divide img-height
+  # xratio = width/img->width
+  var img-width-a/eax: (addr int) <- get img, width
+  var img-width/ebx: int <- copy *img-width-a
+  var img-width-f/xmm0: float <- convert img-width
+  var xratio/xmm2: float <- convert width
+  xratio <- divide img-width-f
+  # esi = img->data
+  var img-data-ah/eax: (addr handle array byte) <- get img, data
+  var _img-data/eax: (addr array byte) <- lookup *img-data-ah
+  var img-data/esi: (addr array byte) <- copy _img-data
+  var len/edi: int <- length img-data
+  #
+  var one/eax: int <- copy 1
+  var one-f/xmm3: float <- convert one
+  var width-f/xmm4: float <- convert width
+  var height-f/xmm5: float <- convert height
+  var zero/eax: int <- copy 0
+  var zero-f/xmm0: float <- convert zero
+  var y/xmm6: float <- copy zero-f
+  {
+    compare y, height-f
+    break-if-float>=
+    var imgy-f/xmm5: float <- copy y
+    imgy-f <- divide yratio
+    var imgy/edx: int <- truncate imgy-f
+    var x/xmm7: float <- copy zero-f
+    {
+      compare x, width-f
+      break-if-float>=
+      var imgx-f/xmm5: float <- copy x
+      imgx-f <- divide xratio
+      var imgx/ecx: int <- truncate imgx-f
+      var idx/eax: int <- copy imgy
+      idx <- multiply img-width
+      idx <- add imgx
+      # error info in case we rounded wrong and 'index' will fail bounds-check
+      compare idx, len
+      {
+        break-if-<
+        set-cursor-position 0/screen, 0x20/x 0x20/y
+        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgx, 3/fg 0/bg
+        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgy, 4/fg 0/bg
+        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, idx, 5/fg 0/bg
+      }
+      var src-a/eax: (addr byte) <- index img-data, idx
+      var src/eax: byte <- copy-byte *src-a
+      var color-int/eax: int <- nearest-grey src
+      var screenx/ecx: int <- convert x
+      screenx <- add xmin
+      var screeny/edx: int <- convert y
+      screeny <- add ymin
+      pixel screen, screenx, screeny, color-int
+      x <- add one-f
+      loop
+    }
+    y <- add one-f
+    loop
+  }
+}
+
+fn nearest-grey level-255: byte -> _/eax: int {
+  var result/eax: int <- copy level-255
+  result <- shift-right 4
+  result <- add 0x10
+  return result
+}
+
+fn dither-pgm-unordered-monochrome _src: (addr image), _dest: (addr image) {
+  var src/esi: (addr image) <- copy _src
+  var dest/edi: (addr image) <- copy _dest
+  # copy 'width'
+  var src-width-a/eax: (addr int) <- get src, width
+  var tmp/eax: int <- copy *src-width-a
+  var src-width: int
+  copy-to src-width, tmp
+  {
+    var dest-width-a/edx: (addr int) <- get dest, width
+    copy-to *dest-width-a, tmp
+  }
+  # copy 'height'
+  var src-height-a/eax: (addr int) <- get src, height
+  var tmp/eax: int <- copy *src-height-a
+  var src-height: int
+  copy-to src-height, tmp
+  {
+    var dest-height-a/ecx: (addr int) <- get dest, height
+    copy-to *dest-height-a, tmp
+  }
+  # transform 'data'
+  var capacity/ebx: int <- copy src-width
+  capacity <- multiply src-height
+  var dest/edi: (addr image) <- copy _dest
+  var dest-data-ah/eax: (addr handle array byte) <- get dest, data
+  populate dest-data-ah, capacity
+  var _dest-data/eax: (addr array byte) <- lookup *dest-data-ah
+  var dest-data/edi: (addr array byte) <- copy _dest-data
+  # needs a buffer to temporarily hold more than 256 levels of precision
+  var errors-storage: (array int 0xc0000)
+  var errors/ebx: (addr array int) <- address errors-storage
+  var src-data-ah/eax: (addr handle array byte) <- get src, data
+  var _src-data/eax: (addr array byte) <- lookup *src-data-ah
+  var src-data/esi: (addr array byte) <- copy _src-data
+  var y/edx: int <- copy 0
+  {
+    compare y, src-height
+    break-if->=
+    var x/ecx: int <- copy 0
+    {
+      compare x, src-width
+      break-if->=
+      var curr/eax: byte <- _read-pgm-buffer src-data, x, y, src-width
+      var curr-int/eax: int <- copy curr
+      curr-int <- shift-left 0x10  # we have 32 bits; we'll use 16 bits for the fraction and leave 8 for unanticipated overflow
+      var error/esi: int <- _read-dithering-error errors, x, y, src-width
+      error <- add curr-int
+      $_dither-pgm-unordered-monochrome:update-error: {
+        compare error, 0x800000
+        {
+          break-if->=
+          _write-raw-buffer dest-data, x, y, src-width, 0/black
+          break $_dither-pgm-unordered-monochrome:update-error
+        }
+        _write-raw-buffer dest-data, x, y, src-width, 1/white
+        error <- subtract 0xff0000
+      }
+      _diffuse-dithering-error-floyd-steinberg errors, x, y, src-width, src-height, error
+      x <- increment
+      loop
+    }
+    move-cursor-to-left-margin-of-next-line 0/screen
+    y <- increment
+    loop
+  }
+}
+
+fn dither-pgm-unordered _src: (addr image), _dest: (addr image) {
+  var src/esi: (addr image) <- copy _src
+  var dest/edi: (addr image) <- copy _dest
+  # copy 'width'
+  var src-width-a/eax: (addr int) <- get src, width
+  var tmp/eax: int <- copy *src-width-a
+  var src-width: int
+  copy-to src-width, tmp
+  {
+    var dest-width-a/edx: (addr int) <- get dest, width
+    copy-to *dest-width-a, tmp
+  }
+  # copy 'height'
+  var src-height-a/eax: (addr int) <- get src, height
+  var tmp/eax: int <- copy *src-height-a
+  var src-height: int
+  copy-to src-height, tmp
+  {
+    var dest-height-a/ecx: (addr int) <- get dest, height
+    copy-to *dest-height-a, tmp
+  }
+  # compute scaling factor 255/max
+  var target-scale/eax: int <- copy 0xff
+  var scale-f/xmm7: float <- convert target-scale
+  var src-max-a/eax: (addr int) <- get src, max
+  var tmp-f/xmm0: float <- convert *src-max-a
+  scale-f <- divide tmp-f
+  # transform 'data'
+  var capacity/ebx: int <- copy src-width
+  capacity <- multiply src-height
+  var dest/edi: (addr image) <- copy _dest
+  var dest-data-ah/eax: (addr handle array byte) <- get dest, data
+  populate dest-data-ah, capacity
+  var _dest-data/eax: (addr array byte) <- lookup *dest-data-ah
+  var dest-data/edi: (addr array byte) <- copy _dest-data
+  # needs a buffer to temporarily hold more than 256 levels of precision
+  var errors-storage: (array int 0xc0000)
+  var errors/ebx: (addr array int) <- address errors-storage
+  var src-data-ah/eax: (addr handle array byte) <- get src, data
+  var _src-data/eax: (addr array byte) <- lookup *src-data-ah
+  var src-data/esi: (addr array byte) <- copy _src-data
+  var y/edx: int <- copy 0
+  {
+    compare y, src-height
+    break-if->=
+    var x/ecx: int <- copy 0
+    {
+      compare x, src-width
+      break-if->=
+      var initial-color/eax: byte <- _read-pgm-buffer src-data, x, y, src-width
+      # . scale to 255 levels
+      var initial-color-int/eax: int <- copy initial-color
+      var initial-color-f/xmm0: float <- convert initial-color-int
+      initial-color-f <- multiply scale-f
+      initial-color-int <- convert initial-color-f
+      var error/esi: int <- _read-dithering-error errors, x, y, src-width
+      # error += (initial-color << 16)
+      {
+        var tmp/eax: int <- copy initial-color-int
+        tmp <- shift-left 0x10  # we have 32 bits; we'll use 16 bits for the fraction and leave 8 for unanticipated overflow
+        error <- add tmp
+      }
+      # nearest-color = nearest(error >> 16)
+      var nearest-color/eax: int <- copy error
+      nearest-color <- shift-right-signed 0x10
+      {
+        compare nearest-color, 0
+        break-if->=
+        nearest-color <- copy 0
+      }
+      {
+        compare nearest-color, 0xf0
+        break-if-<=
+        nearest-color <- copy 0xf0
+      }
+      # . truncate last 4 bits
+      nearest-color <- and 0xf0
+      # error -= (nearest-color << 16)
+      {
+        var tmp/eax: int <- copy nearest-color
+        tmp <- shift-left 0x10
+        error <- subtract tmp
+      }
+      # color-index = (nearest-color >> 4 + 16)
+      var color-index/eax: int <- copy nearest-color
+      color-index <- shift-right 4
+      color-index <- add 0x10
+      var color-index-byte/eax: byte <- copy-byte color-index
+      _write-raw-buffer dest-data, x, y, src-width, color-index-byte
+      _diffuse-dithering-error-floyd-steinberg errors, x, y, src-width, src-height, error
+      x <- increment
+      loop
+    }
+    y <- increment
+    loop
+  }
+}
+
+# Use Floyd-Steinberg algorithm for diffusing error at x, y in a 2D grid of
+# dimensions (width, height)
+#
+# https://tannerhelland.com/2012/12/28/dithering-eleven-algorithms-source-code.html
+#
+# Error is currently a fixed-point number with 16-bit fraction. But
+# interestingly this function doesn't care about that.
+fn _diffuse-dithering-error-floyd-steinberg errors: (addr array int), x: int, y: int, width: int, height: int, error: int {
+  {
+    compare error, 0
+    break-if-!=
+    return
+  }
+  var width-1/esi: int <- copy width
+  width-1 <- decrement
+  var height-1/edi: int <- copy height
+  height-1 <- decrement
+  # delta = error/16
+#?   show-errors errors, width, height, x, y
+  var delta/ecx: int <- copy error
+  delta <- shift-right-signed 4
+  # In Floyd-Steinberg, each pixel X transmits its errors to surrounding
+  # pixels in the following proportion:
+  #           X     7/16
+  #     3/16  5/16  1/16
+  var x/edx: int <- copy x
+  {
+    compare x, width-1
+    break-if->=
+    var tmp/eax: int <- copy 7
+    tmp <- multiply delta
+    var xright/edx: int <- copy x
+    xright <- increment
+    _accumulate-dithering-error errors, xright, y, width, tmp
+  }
+  var y/ebx: int <- copy y
+  {
+    compare y, height-1
+    break-if-<
+    return
+  }
+  var ybelow: int
+  copy-to ybelow, y
+  increment ybelow
+  {
+    compare x, 0
+    break-if-<=
+    var tmp/eax: int <- copy 3
+    tmp <- multiply delta
+    var xleft/edx: int <- copy x
+    xleft <- decrement
+    _accumulate-dithering-error errors, xleft, ybelow, width, tmp
+  }
+  {
+    var tmp/eax: int <- copy 5
+    tmp <- multiply delta
+    _accumulate-dithering-error errors, x, ybelow, width, tmp
+  }
+  {
+    compare x, width-1
+    break-if->=
+    var xright/edx: int <- copy x
+    xright <- increment
+    _accumulate-dithering-error errors, xright, ybelow, width, delta
+  }
+#?   show-errors errors, width, height, x, y
+}
+
+fn _accumulate-dithering-error errors: (addr array int), x: int, y: int, width: int, error: int {
+  var curr/esi: int <- _read-dithering-error errors, x, y, width
+  curr <- add error
+  _write-dithering-error errors, x, y, width, curr
+}
+
+fn _read-dithering-error _errors: (addr array int), x: int, y: int, width: int -> _/esi: int {
+  var errors/esi: (addr array int) <- copy _errors
+  var idx/ecx: int <- copy y
+  idx <- multiply width
+  idx <- add x
+  var result-a/eax: (addr int) <- index errors, idx
+  return *result-a
+}
+
+fn _write-dithering-error _errors: (addr array int), x: int, y: int, width: int, val: int {
+  var errors/esi: (addr array int) <- copy _errors
+  var idx/ecx: int <- copy y
+  idx <- multiply width
+  idx <- add x
+#?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, idx, 7/fg 0/bg
+#?   move-cursor-to-left-margin-of-next-line 0/screen
+  var src/eax: int <- copy val
+  var dest-a/edi: (addr int) <- index errors, idx
+  copy-to *dest-a, src
+}
+
+fn _read-pgm-buffer _buf: (addr array byte), x: int, y: int, width: int -> _/eax: byte {
+  var buf/esi: (addr array byte) <- copy _buf
+  var idx/ecx: int <- copy y
+  idx <- multiply width
+  idx <- add x
+  var result-a/eax: (addr byte) <- index buf, idx
+  var result/eax: byte <- copy-byte *result-a
+  return result
+}
+
+fn _write-raw-buffer _buf: (addr array byte), x: int, y: int, width: int, val: byte {
+  var buf/esi: (addr array byte) <- copy _buf
+  var idx/ecx: int <- copy y
+  idx <- multiply width
+  idx <- add x
+  var src/eax: byte <- copy val
+  var dest-a/edi: (addr byte) <- index buf, idx
+  copy-byte-to *dest-a, src
+}
+
+# some debugging helpers
+fn show-errors errors: (addr array int), width: int, height: int, x: int, y: int {
+  compare y, 1
+  {
+    break-if-=
+    return
+  }
+  compare x, 0
+  {
+    break-if-=
+    return
+  }
+  var y/edx: int <- copy 0
+  {
+    compare y, height
+    break-if->=
+    var x/ecx: int <- copy 0
+    {
+      compare x, width
+      break-if->=
+      var error/esi: int <- _read-dithering-error errors, x, y, width
+      psd "e", error, 5/fg, x, y
+      x <- increment
+      loop
+    }
+    move-cursor-to-left-margin-of-next-line 0/screen
+    y <- increment
+    loop
+  }
+}
+
+fn psd s: (addr array byte), d: int, fg: int, x: int, y: int {
+  {
+    compare y, 0x18
+    break-if->=
+    return
+  }
+  {
+    compare y, 0x1c
+    break-if-<=
+    return
+  }
+  {
+    compare x, 0x40
+    break-if->=
+    return
+  }
+#?   {
+#?     compare x, 0x48
+#?     break-if-<=
+#?     return
+#?   }
+  draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, s, 7/fg 0/bg
+  draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, d, fg 0/bg
+}
+
+fn psx s: (addr array byte), d: int, fg: int, x: int, y: int {
+#?   {
+#?     compare y, 0x60
+#?     break-if->=
+#?     return
+#?   }
+#?   {
+#?     compare y, 0x6c
+#?     break-if-<=
+#?     return
+#?   }
+  {
+    compare x, 0x20
+    break-if->=
+    return
+  }
+#?   {
+#?     compare x, 0x6c
+#?     break-if-<=
+#?     return
+#?   }
+  draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, s, 7/fg 0/bg
+  draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, d, fg 0/bg
+}
+
+# import a color ascii "pixmap" (each pixel consists of 3 shades of r/g/b from 0 to 255)
+fn initialize-image-from-ppm _self: (addr image), in: (addr stream byte) {
+  var self/esi: (addr image) <- copy _self
+  var curr-word-storage: slice
+  var curr-word/ecx: (addr slice) <- address curr-word-storage
+  # load width, height
+  next-word in, curr-word
+  var tmp/eax: int <- parse-decimal-int-from-slice curr-word
+  var width/edx: int <- copy tmp
+  next-word in, curr-word
+  tmp <- parse-decimal-int-from-slice curr-word
+  var height/ebx: int <- copy tmp
+  next-word in, curr-word
+  # check color levels
+  {
+    tmp <- parse-decimal-int-from-slice curr-word
+    compare tmp, 0xff
+    break-if-=
+    abort "initialize-image-from-ppm: supports exactly 255 levels per rgb channel"
+  }
+  var dest/edi: (addr int) <- get self, max
+  copy-to *dest, tmp
+  # save width, height
+  dest <- get self, width
+  copy-to *dest, width
+  dest <- get self, height
+  copy-to *dest, height
+  # initialize data
+  var capacity/edx: int <- copy width
+  capacity <- multiply height
+  # . multiply by 3 for the r/g/b channels
+  var tmp/eax: int <- copy capacity
+  tmp <- shift-left 1
+  capacity <- add tmp
+  #
+  var data-ah/edi: (addr handle array byte) <- get self, data
+  populate data-ah, capacity
+  var _data/eax: (addr array byte) <- lookup *data-ah
+  var data/edi: (addr array byte) <- copy _data
+  var i/ebx: int <- copy 0
+  {
+    compare i, capacity
+    break-if->=
+    next-word in, curr-word
+    var src/eax: int <- parse-decimal-int-from-slice curr-word
+    {
+      var dest/ecx: (addr byte) <- index data, i
+      copy-byte-to *dest, src
+    }
+    i <- increment
+    loop
+  }
+}
+
+# import a color ascii "pixmap" (each pixel consists of 3 shades of r/g/b from 0 to 255)
+fn render-ppm-image screen: (addr screen), _img: (addr image), xmin: int, ymin: int, width: int, height: int {
+  var img/esi: (addr image) <- copy _img
+  # yratio = height/img->height
+  var img-height-a/eax: (addr int) <- get img, height
+  var img-height/xmm0: float <- convert *img-height-a
+  var yratio/xmm1: float <- convert height
+  yratio <- divide img-height
+  # xratio = width/img->width
+  var img-width-a/eax: (addr int) <- get img, width
+  var img-width/ebx: int <- copy *img-width-a
+  var img-width-f/xmm0: float <- convert img-width
+  var xratio/xmm2: float <- convert width
+  xratio <- divide img-width-f
+  # esi = img->data
+  var img-data-ah/eax: (addr handle array byte) <- get img, data
+  var _img-data/eax: (addr array byte) <- lookup *img-data-ah
+  var img-data/esi: (addr array byte) <- copy _img-data
+  var len/edi: int <- length img-data
+  #
+  var one/eax: int <- copy 1
+  var one-f/xmm3: float <- convert one
+  var width-f/xmm4: float <- convert width
+  var height-f/xmm5: float <- convert height
+  var zero/eax: int <- copy 0
+  var zero-f/xmm0: float <- convert zero
+  var y/xmm6: float <- copy zero-f
+  {
+    compare y, height-f
+    break-if-float>=
+    var imgy-f/xmm5: float <- copy y
+    imgy-f <- divide yratio
+    var imgy/edx: int <- truncate imgy-f
+    var x/xmm7: float <- copy zero-f
+    {
+      compare x, width-f
+      break-if-float>=
+      var imgx-f/xmm5: float <- copy x
+      imgx-f <- divide xratio
+      var imgx/ecx: int <- truncate imgx-f
+      var idx/eax: int <- copy imgy
+      idx <- multiply img-width
+      idx <- add imgx
+      # . multiply by 3 for the r/g/b channels
+      {
+        var tmp/ecx: int <- copy idx
+        tmp <- shift-left 1
+        idx <- add tmp
+      }
+      # error info in case we rounded wrong and 'index' will fail bounds-check
+      compare idx, len
+      {
+        break-if-<
+        set-cursor-position 0/screen, 0x20/x 0x20/y
+        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgx, 3/fg 0/bg
+        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgy, 4/fg 0/bg
+        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, idx, 5/fg 0/bg
+      }
+      # r channel
+      var r: int
+      {
+        var src-a/eax: (addr byte) <- index img-data, idx
+        var src/eax: byte <- copy-byte *src-a
+        copy-to r, src
+      }
+      idx <- increment
+      # g channel
+      var g: int
+      {
+        var src-a/eax: (addr byte) <- index img-data, idx
+        var src/eax: byte <- copy-byte *src-a
+        copy-to g, src
+      }
+      idx <- increment
+      # b channel
+      var b: int
+      {
+        var src-a/eax: (addr byte) <- index img-data, idx
+        var src/eax: byte <- copy-byte *src-a
+        copy-to b, src
+      }
+      idx <- increment
+      # plot nearest color
+      var color/eax: int <- nearest-color-euclidean r, g, b
+      var screenx/ecx: int <- convert x
+      screenx <- add xmin
+      var screeny/edx: int <- convert y
+      screeny <- add ymin
+      pixel screen, screenx, screeny, color
+      x <- add one-f
+      loop
+    }
+    y <- add one-f
+    loop
+  }
+}
+
+fn dither-ppm-unordered _src: (addr image), _dest: (addr image) {
+  var src/esi: (addr image) <- copy _src
+  var dest/edi: (addr image) <- copy _dest
+  # copy 'width'
+  var src-width-a/eax: (addr int) <- get src, width
+  var tmp/eax: int <- copy *src-width-a
+  var src-width: int
+  copy-to src-width, tmp
+  {
+    var dest-width-a/edx: (addr int) <- get dest, width
+    copy-to *dest-width-a, tmp
+  }
+  # copy 'height'
+  var src-height-a/eax: (addr int) <- get src, height
+  var tmp/eax: int <- copy *src-height-a
+  var src-height: int
+  copy-to src-height, tmp
+  {
+    var dest-height-a/ecx: (addr int) <- get dest, height
+    copy-to *dest-height-a, tmp
+  }
+  # compute scaling factor 255/max
+  var target-scale/eax: int <- copy 0xff
+  var scale-f/xmm7: float <- convert target-scale
+  var src-max-a/eax: (addr int) <- get src, max
+  var tmp-f/xmm0: float <- convert *src-max-a
+  scale-f <- divide tmp-f
+  # allocate 'data'
+  var capacity/ebx: int <- copy src-width
+  capacity <- multiply src-height
+  var dest/edi: (addr image) <- copy _dest
+  var dest-data-ah/eax: (addr handle array byte) <- get dest, data
+  populate dest-data-ah, capacity
+  var _dest-data/eax: (addr array byte) <- lookup *dest-data-ah
+  var dest-data/edi: (addr array byte) <- copy _dest-data
+  # error buffers per r/g/b channel
+  var red-errors-storage: (array int 0xc0000)
+  var tmp/eax: (addr array int) <- address red-errors-storage
+  var red-errors: (addr array int)
+  copy-to red-errors, tmp
+  var green-errors-storage: (array int 0xc0000)
+  var tmp/eax: (addr array int) <- address green-errors-storage
+  var green-errors: (addr array int)
+  copy-to green-errors, tmp
+  var blue-errors-storage: (array int 0xc0000)
+  var tmp/eax: (addr array int) <- address blue-errors-storage
+  var blue-errors: (addr array int)
+  copy-to blue-errors, tmp
+  # transform 'data'
+  var src-data-ah/eax: (addr handle array byte) <- get src, data
+  var _src-data/eax: (addr array byte) <- lookup *src-data-ah
+  var src-data/esi: (addr array byte) <- copy _src-data
+  var y/edx: int <- copy 0
+  {
+    compare y, src-height
+    break-if->=
+    var x/ecx: int <- copy 0
+    {
+      compare x, src-width
+      break-if->=
+      # - update errors and compute color levels for current pixel in each channel
+      # update red-error with current image pixel
+      var red-error: int
+      {
+        var tmp/esi: int <- _read-dithering-error red-errors, x, y, src-width
+        copy-to red-error, tmp
+      }
+      {
+        var tmp/eax: int <- _ppm-error src-data, x, y, src-width, 0/red, scale-f
+        add-to red-error, tmp
+      }
+      # recompute red channel for current pixel
+      var red-level: int
+      {
+        var tmp/eax: int <- _error-to-ppm-channel red-error
+        copy-to red-level, tmp
+      }
+      # update green-error with current image pixel
+      var green-error: int
+      {
+        var tmp/esi: int <- _read-dithering-error green-errors, x, y, src-width
+        copy-to green-error, tmp
+      }
+      {
+        var tmp/eax: int <- _ppm-error src-data, x, y, src-width, 1/green, scale-f
+        add-to green-error, tmp
+      }
+      # recompute green channel for current pixel
+      var green-level: int
+      {
+        var tmp/eax: int <- _error-to-ppm-channel green-error
+        copy-to green-level, tmp
+      }
+      # update blue-error with current image pixel
+      var blue-error: int
+      {
+        var tmp/esi: int <- _read-dithering-error blue-errors, x, y, src-width
+        copy-to blue-error, tmp
+      }
+      {
+        var tmp/eax: int <- _ppm-error src-data, x, y, src-width, 2/blue, scale-f
+        add-to blue-error, tmp
+      }
+      # recompute blue channel for current pixel
+      var blue-level: int
+      {
+        var tmp/eax: int <- _error-to-ppm-channel blue-error
+        copy-to blue-level, tmp
+      }
+      # - figure out the nearest color
+#?       {
+#?         compare red-level, 0x80
+#?         break-if->
+#?         draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, red-level, 4/fg 0/bg
+#?         draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, green-level, 2/fg 0/bg
+#?         draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, blue-level, 9/fg 0/bg
+#?       }
+      var nearest-color-index/eax: int <- nearest-color-euclidean red-level, green-level, blue-level
+      {
+        var nearest-color-index-byte/eax: byte <- copy-byte nearest-color-index
+        _write-raw-buffer dest-data, x, y, src-width, nearest-color-index-byte
+      }
+      # - diffuse errors
+      var red-level: int
+      var green-level: int
+      var blue-level: int
+      {
+        var tmp-red-level/ecx: int <- copy 0
+        var tmp-green-level/edx: int <- copy 0
+        var tmp-blue-level/ebx: int <- copy 0
+        tmp-red-level, tmp-green-level, tmp-blue-level <- color-rgb nearest-color-index
+        copy-to red-level, tmp-red-level
+        copy-to green-level, tmp-green-level
+        copy-to blue-level, tmp-blue-level
+      }
+      # update red-error
+      var red-level-error/eax: int <- copy red-level
+      red-level-error <- shift-left 0x10
+      subtract-from red-error, red-level-error
+      _diffuse-dithering-error-floyd-steinberg red-errors, x, y, src-width, src-height, red-error
+      # update green-error
+      var green-level-error/eax: int <- copy green-level
+      green-level-error <- shift-left 0x10
+      subtract-from green-error, green-level-error
+      _diffuse-dithering-error-floyd-steinberg green-errors, x, y, src-width, src-height, green-error
+      # update blue-error
+      var blue-level-error/eax: int <- copy blue-level
+      blue-level-error <- shift-left 0x10
+      subtract-from blue-error, blue-level-error
+      _diffuse-dithering-error-floyd-steinberg blue-errors, x, y, src-width, src-height, blue-error
+      #
+      x <- increment
+      loop
+    }
+    y <- increment
+    loop
+  }
+}
+
+# convert a single channel for a single image pixel to error space
+fn _ppm-error buf: (addr array byte), x: int, y: int, width: int, channel: int, _scale-f: float -> _/eax: int {
+  # current image pixel
+  var initial-level/eax: byte <- _read-ppm-buffer buf, x, y, width, channel
+  # scale to 255 levels
+  var initial-level-int/eax: int <- copy initial-level
+  var initial-level-f/xmm0: float <- convert initial-level-int
+  var scale-f/xmm1: float <- copy _scale-f
+  initial-level-f <- multiply scale-f
+  initial-level-int <- convert initial-level-f
+  # switch to fixed-point with 16 bits of precision
+  initial-level-int <- shift-left 0x10
+  return initial-level-int
+}
+
+fn _error-to-ppm-channel error: int -> _/eax: int {
+  # clamp(error >> 16)
+  var result/esi: int <- copy error
+  result <- shift-right-signed 0x10
+  {
+    compare result, 0
+    break-if->=
+    result <- copy 0
+  }
+  {
+    compare result, 0xff
+    break-if-<=
+    result <- copy 0xff
+  }
+  return result
+}
+
+# read from a buffer containing alternating bytes from r/g/b channels
+fn _read-ppm-buffer _buf: (addr array byte), x: int, y: int, width: int, channel: int -> _/eax: byte {
+  var buf/esi: (addr array byte) <- copy _buf
+  var idx/ecx: int <- copy y
+  idx <- multiply width
+  idx <- add x
+  var byte-idx/edx: int <- copy 3
+  byte-idx <- multiply idx
+  byte-idx <- add channel
+  var result-a/eax: (addr byte) <- index buf, byte-idx
+  var result/eax: byte <- copy-byte *result-a
+  return result
+}
+
+# each byte in the image data is a color of the current palette
+fn render-raw-image screen: (addr screen), _img: (addr image), xmin: int, ymin: int, width: int, height: int {
+  var img/esi: (addr image) <- copy _img
+  # yratio = height/img->height
+  var img-height-a/eax: (addr int) <- get img, height
+  var img-height/xmm0: float <- convert *img-height-a
+  var yratio/xmm1: float <- convert height
+  yratio <- divide img-height
+  # xratio = width/img->width
+  var img-width-a/eax: (addr int) <- get img, width
+  var img-width/ebx: int <- copy *img-width-a
+  var img-width-f/xmm0: float <- convert img-width
+  var xratio/xmm2: float <- convert width
+  xratio <- divide img-width-f
+  # esi = img->data
+  var img-data-ah/eax: (addr handle array byte) <- get img, data
+  var _img-data/eax: (addr array byte) <- lookup *img-data-ah
+  var img-data/esi: (addr array byte) <- copy _img-data
+  var len/edi: int <- length img-data
+  #
+  var one/eax: int <- copy 1
+  var one-f/xmm3: float <- convert one
+  var width-f/xmm4: float <- convert width
+  var height-f/xmm5: float <- convert height
+  var zero/eax: int <- copy 0
+  var zero-f/xmm0: float <- convert zero
+  var y/xmm6: float <- copy zero-f
+  {
+    compare y, height-f
+    break-if-float>=
+    var imgy-f/xmm5: float <- copy y
+    imgy-f <- divide yratio
+    var imgy/edx: int <- truncate imgy-f
+    var x/xmm7: float <- copy zero-f
+    {
+      compare x, width-f
+      break-if-float>=
+      var imgx-f/xmm5: float <- copy x
+      imgx-f <- divide xratio
+      var imgx/ecx: int <- truncate imgx-f
+      var idx/eax: int <- copy imgy
+      idx <- multiply img-width
+      idx <- add imgx
+      # error info in case we rounded wrong and 'index' will fail bounds-check
+      compare idx, len
+      {
+        break-if-<
+        set-cursor-position 0/screen, 0x20/x 0x20/y
+        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgx, 3/fg 0/bg
+        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgy, 4/fg 0/bg
+        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, idx, 5/fg 0/bg
+      }
+      var color-a/eax: (addr byte) <- index img-data, idx
+      var color/eax: byte <- copy-byte *color-a
+      var color-int/eax: int <- copy color
+      var screenx/ecx: int <- convert x
+      screenx <- add xmin
+      var screeny/edx: int <- convert y
+      screeny <- add ymin
+      pixel screen, screenx, screeny, color-int
+      x <- add one-f
+      loop
+    }
+    y <- add one-f
+    loop
+  }
+}
diff --git a/apps/life.mu b/apps/life.mu
new file mode 100644
index 00000000..a65347bf
--- /dev/null
+++ b/apps/life.mu
@@ -0,0 +1,252 @@
+# Conway's Game of Life
+#
+# To build:
+#   $ ./translate apps/life.mu
+# To run:
+#   $ qemu-system-i386 code.img
+
+fn state _grid: (addr array boolean), x: int, y: int -> _/eax: boolean {
+  # clip at the edge
+  compare x, 0
+  {
+    break-if->=
+    return 0/false
+  }
+  compare y, 0
+  {
+    break-if->=
+    return 0/false
+  }
+  compare x, 0x100/width
+  {
+    break-if-<
+    return 0/false
+  }
+  compare y, 0xc0/height
+  {
+    break-if-<
+    return 0/false
+  }
+  var idx/eax: int <- copy y
+  idx <- shift-left 8/log2width
+  idx <- add x
+  var grid/esi: (addr array boolean) <- copy _grid
+  var result/eax: (addr boolean) <- index grid, idx
+  return *result
+}
+
+fn set-state _grid: (addr array boolean), x: int, y: int, val: boolean {
+  # don't bother checking bounds
+  var idx/eax: int <- copy y
+  idx <- shift-left 8/log2width
+  idx <- add x
+  var grid/esi: (addr array boolean) <- copy _grid
+  var result/eax: (addr boolean) <- index grid, idx
+  var src/ecx: boolean <- copy val
+  copy-to *result, src
+}
+
+fn num-live-neighbors grid: (addr array boolean), x: int, y: int -> _/eax: int {
+  var result/edi: int <- copy 0
+  # row above: zig
+  decrement y
+  decrement x
+  var s/eax: boolean <- state grid, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  increment x
+  s <- state grid, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  increment x
+  s <- state grid, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  # curr row: zag
+  increment y
+  s <- state grid, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  subtract-from x, 2
+  s <- state grid, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  # row below: zig
+  increment y
+  s <- state grid, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  increment x
+  s <- state grid, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  increment x
+  s <- state grid, x, y
+  {
+    compare s, 0/false
+    break-if-=
+    result <- increment
+  }
+  return result
+}
+
+fn step old-grid: (addr array boolean), new-grid: (addr array boolean) {
+  var y/ecx: int <- copy 0
+  {
+    compare y, 0xc0/height
+    break-if->=
+    var x/edx: int <- copy 0
+    {
+      compare x, 0x100/width
+      break-if->=
+      var n/eax: int <- num-live-neighbors old-grid, x, y
+      # if neighbors < 2, die of loneliness
+      {
+        compare n, 2
+        break-if->=
+        set-state new-grid, x, y, 0/dead
+      }
+      # if neighbors > 3, die of overcrowding
+      {
+        compare n, 3
+        break-if-<=
+        set-state new-grid, x, y, 0/dead
+      }
+      # if neighbors = 2, preserve state
+      {
+        compare n, 2
+        break-if-!=
+        var old-state/eax: boolean <- state old-grid, x, y
+        set-state new-grid, x, y, old-state
+      }
+      # if neighbors = 3, cell quickens to life
+      {
+        compare n, 3
+        break-if-!=
+        set-state new-grid, x, y, 1/live
+      }
+      x <- increment
+      loop
+    }
+    y <- increment
+    loop
+  }
+}
+
+# color a square of size 'side' starting at x*side, y*side
+fn render-square _x: int, _y: int, color: int {
+  var y/edx: int <- copy _y
+  y <- shift-left 2/log2side
+  var side/ebx: int <- copy 1
+  side <- shift-left 2/log2side
+  var ymax/ecx: int <- copy y
+  ymax <- add side
+  {
+    compare y, ymax
+    break-if->=
+    {
+      var x/eax: int <- copy _x
+      x <- shift-left 2/log2side
+      var xmax/ecx: int <- copy x
+      xmax <- add side
+      {
+        compare x, xmax
+        break-if->=
+        pixel-on-real-screen x, y, color
+        x <- increment
+        loop
+      }
+    }
+    y <- increment
+    loop
+  }
+}
+
+fn render grid: (addr array boolean) {
+  var y/ecx: int <- copy 0
+  {
+    compare y, 0xc0/height
+    break-if->=
+    var x/edx: int <- copy 0
+    {
+      compare x, 0x100/width
+      break-if->=
+      var state/eax: boolean <- state grid, x, y
+      compare state, 0/false
+      {
+        break-if-=
+        render-square x, y, 3/cyan
+      }
+      compare state, 0/false
+      {
+        break-if-!=
+        render-square x, y, 0/black
+      }
+      x <- increment
+      loop
+    }
+    y <- increment
+    loop
+  }
+}
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+#?   # allocate on the stack
+#?   var grid1-storage: (array boolean 0xc000)  # width * height
+#?   var grid1/esi: (addr array boolean) <- address grid1-storage
+#?   var grid2-storage: (array boolean 0xc000)  # width * height
+#?   var grid2/edi: (addr array boolean) <- address grid2-storage
+  # allocate on the heap
+  var grid1-storage: (handle array boolean)
+  var grid1-ah/eax: (addr handle array boolean) <- address grid1-storage
+  populate grid1-ah, 0xc000  # width * height
+  var _grid1/eax: (addr array boolean) <- lookup *grid1-ah
+  var grid1/esi: (addr array boolean) <- copy _grid1
+  var grid2-storage: (handle array boolean)
+  var grid2-ah/eax: (addr handle array boolean) <- address grid2-storage
+  populate grid2-ah, 0xc000  # width * height
+  var _grid2/eax: (addr array boolean) <- lookup *grid2-ah
+  var grid2/edi: (addr array boolean) <- copy _grid2
+  # initialize grid1
+  set-state grid1, 0x80, 0x5f, 1/live
+  set-state grid1, 0x81, 0x5f, 1/live
+  set-state grid1, 0x7f, 0x60, 1/live
+  set-state grid1, 0x80, 0x60, 1/live
+  set-state grid1, 0x80, 0x61, 1/live
+  # render grid1
+  render grid1
+  {
+    var key/eax: byte <- read-key keyboard
+    compare key, 0
+#?     loop-if-=  # press key to step
+    break-if-!=  # press key to quit  # comment this out to run under bochs; I'm not sure why there's a newline in the keyboard buffer
+    # iter: grid1 -> grid2
+    step grid1, grid2
+    render grid2
+    # iter: grid2 -> grid1
+    step grid2, grid1
+    render grid1
+    loop
+  }
+}
diff --git a/apps/mandelbrot-fixed.mu b/apps/mandelbrot-fixed.mu
new file mode 100644
index 00000000..fc33aae1
--- /dev/null
+++ b/apps/mandelbrot-fixed.mu
@@ -0,0 +1,262 @@
+# Mandelbrot set using fixed-point numbers.
+#
+# Install:
+#   $ git clone https://github.com/akkartik/mu
+#   $ cd mu
+# Build on Linux:
+#   $ ./translate apps/mandelbrot-fixed.mu
+# Build on other platforms (slow):
+#   $ ./translate_emulated apps/mandelbrot-fixed.mu
+# Run:
+#   $ qemu-system-i386 code.img
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  # Initially the viewport is centered at 0, 0 in the scene.
+  var scene-cx-f: int
+  var scene-cy-f: int
+  # Initially the viewport shows a section of the scene 4 units wide.
+  var scene-width-f: int
+  copy-to scene-width-f, 0x400/4
+  {
+    mandelbrot screen scene-cx-f, scene-cy-f, scene-width-f
+    # move at an angle slowly towards the edge
+    var adj-f/eax: int <- multiply-fixed scene-width-f, 0x12/0.07
+    subtract-from scene-cx-f, adj-f
+    add-to scene-cy-f, adj-f
+    # slowly shrink the scene width to zoom in
+    var tmp-f/eax: int <- multiply-fixed scene-width-f, 0x80/0.5
+    copy-to scene-width-f, tmp-f
+    loop
+  }
+}
+
+# Since they still look like int types, we'll append a '-f' suffix to variable
+# names to designate fixed-point numbers.
+
+fn int-to-fixed in: int -> _/eax: int {
+  var result-f/eax: int <- copy in
+  result-f <- shift-left 8/fixed-precision
+  {
+    break-if-not-overflow
+    abort "int-to-fixed: overflow"
+  }
+  return result-f
+}
+
+fn fixed-to-int in-f: int -> _/eax: int {
+  var result/eax: int <- copy in-f
+  result <- shift-right-signed 8/fixed-precision
+  return result
+}
+
+# The process of throwing bits away always adjusts a number towards -infinity.
+fn test-fixed-conversion {
+  # 0
+  var f/eax: int <- int-to-fixed 0
+  var result/eax: int <- fixed-to-int f
+  check-ints-equal result, 0, "F - test-fixed-conversion - 0"
+  # 1
+  var f/eax: int <- int-to-fixed 1
+  var result/eax: int <- fixed-to-int f
+  check-ints-equal result, 1, "F - test-fixed-conversion - 1"
+  # -1
+  var f/eax: int <- int-to-fixed -1
+  var result/eax: int <- fixed-to-int f
+  check-ints-equal result, -1, "F - test-fixed-conversion - -1"
+  # 0.5 = 1/2
+  var f/eax: int <- int-to-fixed 1
+  f <- shift-right-signed 1
+  var result/eax: int <- fixed-to-int f
+  check-ints-equal result, 0, "F - test-fixed-conversion - 0.5"
+  # -0.5 = -1/2
+  var f/eax: int <- int-to-fixed -1
+  f <- shift-right-signed 1
+  var result/eax: int <- fixed-to-int f
+  check-ints-equal result, -1, "F - test-fixed-conversion - -0.5"
+  # 1.5 = 3/2
+  var f/eax: int <- int-to-fixed 3
+  f <- shift-right-signed 1
+  var result/eax: int <- fixed-to-int f
+  check-ints-equal result, 1, "F - test-fixed-conversion - 1.5"
+  # -1.5 = -3/2
+  var f/eax: int <- int-to-fixed -3
+  f <- shift-right-signed 1
+  var result/eax: int <- fixed-to-int f
+  check-ints-equal result, -2, "F - test-fixed-conversion - -1.5"
+  # 1.25 = 5/4
+  var f/eax: int <- int-to-fixed 5
+  f <- shift-right-signed 2
+  var result/eax: int <- fixed-to-int f
+  check-ints-equal result, 1, "F - test-fixed-conversion - 1.25"
+  # -1.25 = -5/4
+  var f/eax: int <- int-to-fixed -5
+  f <- shift-right-signed 2
+  var result/eax: int <- fixed-to-int f
+  check-ints-equal result, -2, "F - test-fixed-conversion - -1.25"
+}
+
+# special routines for multiplying and dividing fixed-point numbers
+
+fn multiply-fixed a-f: int, b-f: int -> _/eax: int {
+  var result/eax: int <- copy a-f
+  result <- multiply b-f
+  {
+    break-if-not-overflow
+    abort "multiply-fixed: overflow"
+  }
+  result <- shift-right-signed 8/fixed-precision
+  return result
+}
+
+fn divide-fixed a-f: int, b-f: int -> _/eax: int {
+  var result-f/eax: int <- copy a-f
+  result-f <- shift-left 8/fixed-precision
+  {
+    break-if-not-overflow
+    abort "divide-fixed: overflow"
+  }
+  var dummy-remainder/edx: int <- copy 0
+  result-f, dummy-remainder <- integer-divide result-f, b-f
+  return result-f
+}
+
+# multiplying or dividing by an integer can use existing instructions.
+
+# adding and subtracting two fixed-point numbers can use existing instructions.
+
+fn mandelbrot screen: (addr screen), scene-cx-f: int, scene-cy-f: int, scene-width-f: int {
+  var a/eax: int <- copy 0
+  var b/ecx: int <- copy 0
+  a, b <- screen-size screen
+  var width/esi: int <- copy a
+  width <- shift-left 3/log2-font-width
+  var height/edi: int <- copy b
+  height <- shift-left 4/log2-font-height
+  var y/ecx: int <- copy 0
+  {
+    compare y, height
+    break-if->=
+    var imaginary-f/ebx: int <- viewport-to-imaginary-f y, width, height, scene-cy-f, scene-width-f
+    var x/eax: int <- copy 0
+    {
+      compare x, width
+      break-if->=
+      var real-f/edx: int <- viewport-to-real-f x, width, scene-cx-f, scene-width-f
+      var iterations/esi: int <- mandelbrot-iterations-for-point real-f, imaginary-f, 0x400/max
+      iterations <- shift-right 3
+      var color/edx: int <- copy 0
+      {
+        var dummy/eax: int <- copy 0
+        dummy, color <- integer-divide iterations, 0x18/24/size-of-cycle-0
+        color <- add 0x20/cycle-0
+      }
+      pixel screen, x, y, color
+      x <- increment
+      loop
+    }
+    y <- increment
+    loop
+  }
+}
+
+fn mandelbrot-iterations-for-point real-f: int, imaginary-f: int, max: int -> _/esi: int {
+  var x-f/esi: int <- copy 0
+  var y-f/edi: int <- copy 0
+  var iterations/ecx: int <- copy 0
+  {
+    var done?/eax: boolean <- mandelbrot-done? x-f, y-f
+    compare done?, 0/false
+    break-if-!=
+    compare iterations, max
+    break-if->=
+    var x2-f/edx: int <- mandelbrot-x x-f, y-f, real-f
+    var y2-f/ebx: int <- mandelbrot-y x-f, y-f, imaginary-f
+    x-f <- copy x2-f
+    y-f <- copy y2-f
+    iterations <- increment
+    loop
+  }
+  return iterations
+}
+
+fn mandelbrot-done? x-f: int, y-f: int -> _/eax: boolean {
+  # x*x + y*y > 4
+  var tmp-f/eax: int <- multiply-fixed x-f, x-f
+  var result-f/ecx: int <- copy tmp-f
+  tmp-f <- multiply-fixed y-f, y-f
+  result-f <- add tmp-f
+  compare result-f, 0x400/4
+  {
+    break-if->
+    return 0/false
+  }
+  return 1/true
+}
+
+fn mandelbrot-x x-f: int, y-f: int, real-f: int -> _/edx: int {
+  # x*x - y*y + real
+  var tmp-f/eax: int <- multiply-fixed x-f, x-f
+  var result-f/ecx: int <- copy tmp-f
+  tmp-f <- multiply-fixed y-f, y-f
+  result-f <- subtract tmp-f
+  result-f <- add real-f
+  return result-f
+}
+
+fn mandelbrot-y x-f: int, y-f: int, imaginary-f: int -> _/ebx: int {
+  # 2*x*y + imaginary
+  var result-f/eax: int <- copy x-f
+  result-f <- shift-left 1/log2
+  result-f <- multiply-fixed result-f, y-f
+  result-f <- add imaginary-f
+  return result-f
+}
+
+# Scale (x, y) pixel coordinates to a complex plane where the viewport width
+# ranges from -2 to +2. Viewport height just follows the viewport's aspect
+# ratio.
+
+fn viewport-to-real-f x: int, width: int, scene-cx-f: int, scene-width-f: int -> _/edx: int {
+  # 0 in the viewport       goes to scene-cx - scene-width/2 
+  # width in the viewport   goes to scene-cx + scene-width/2
+  # Therefore:
+  # x in the viewport       goes to (scene-cx - scene-width/2) + x*scene-width/width
+  # At most two numbers being multiplied before a divide, so no risk of overflow.
+  var result-f/eax: int <- int-to-fixed x
+  result-f <- multiply-fixed result-f, scene-width-f
+  var width-f/ecx: int <- copy width
+  width-f <- shift-left 8/fixed-precision
+  result-f <- divide-fixed result-f, width-f
+  result-f <- add scene-cx-f
+  var half-scene-width-f/ecx: int <- copy scene-width-f
+  half-scene-width-f <- shift-right 1
+  result-f <- subtract half-scene-width-f
+  return result-f
+}
+
+fn viewport-to-imaginary-f y: int, width: int, height: int, scene-cy-f: int, scene-width-f: int -> _/ebx: int {
+  # 0 in the viewport       goes to scene-cy - scene-width/2*height/width
+  # height in the viewport  goes to scene-cy + scene-width/2*height/width
+  # Therefore:
+  # y in the viewport       goes to (scene-cy - scene-width/2*height/width) + y*scene-width/width
+  #  scene-cy - scene-width/width * (height/2 + y)
+  # At most two numbers being multiplied before a divide, so no risk of overflow.
+  var result-f/eax: int <- int-to-fixed y
+  result-f <- multiply-fixed result-f, scene-width-f
+  var width-f/ecx: int <- copy width
+  width-f <- shift-left 8/fixed-precision
+  result-f <- divide-fixed result-f, width-f
+  result-f <- add scene-cy-f
+  var second-term-f/edx: int <- copy 0
+  {
+    var _second-term-f/eax: int <- copy scene-width-f
+    _second-term-f <- shift-right 1
+    var height-f/ebx: int <- copy height
+    height-f <- shift-left 8/fixed-precision
+    _second-term-f <- multiply-fixed _second-term-f, height-f
+    _second-term-f <- divide-fixed _second-term-f, width-f
+    second-term-f <- copy _second-term-f
+  }
+  result-f <- subtract second-term-f
+  return result-f
+}
diff --git a/apps/mandelbrot-silhouette.mu b/apps/mandelbrot-silhouette.mu
new file mode 100644
index 00000000..0d9a137c
--- /dev/null
+++ b/apps/mandelbrot-silhouette.mu
@@ -0,0 +1,150 @@
+# Mandelbrot set
+#
+# Install:
+#   $ git clone https://github.com/akkartik/mu
+#   $ cd mu
+# Build on Linux:
+#   $ ./translate apps/mandelbrot.mu
+# Build on other platforms (slow):
+#   $ ./translate_emulated apps/mandelbrot.mu
+# Run:
+#   $ qemu-system-i386 code.img
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  mandelbrot screen
+}
+
+fn mandelbrot screen: (addr screen) {
+  var a/eax: int <- copy 0
+  var b/ecx: int <- copy 0
+  a, b <- screen-size screen
+  var width/esi: int <- copy a
+  width <- shift-left 3/log2-font-width
+  var height/edi: int <- copy b
+  height <- shift-left 4/log2-font-height
+  var y/ecx: int <- copy 0
+  {
+    compare y, height
+    break-if->=
+    var imaginary/xmm1: float <- viewport-to-imaginary y, width, height
+    var x/edx: int <- copy 0
+    {
+      compare x, width
+      break-if->=
+      var real/xmm0: float <- viewport-to-real x, width
+      var iterations/eax: int <- mandelbrot-iterations-for-point real, imaginary, 0x400/max
+      compare iterations, 0x400/max
+      {
+        break-if->=
+        pixel screen, x, y, 0xf/white
+      }
+      compare iterations, 0x400/max
+      {
+        break-if-<
+        pixel screen, x, y, 0/black
+      }
+      x <- increment
+      loop
+    }
+    y <- increment
+    loop
+  }
+}
+
+fn mandelbrot-iterations-for-point real: float, imaginary: float, max: int -> _/eax: int {
+  var zero: float
+  var x/xmm0: float <- copy zero
+  var y/xmm1: float <- copy zero
+  var iterations/ecx: int <- copy 0
+  {
+    var done?/eax: boolean <- mandelbrot-done? x, y
+    compare done?, 0/false
+    break-if-!=
+    compare iterations, max
+    break-if->=
+    var newx/xmm2: float <- mandelbrot-x x, y, real
+    var newy/xmm3: float <- mandelbrot-y x, y, imaginary
+    x <- copy newx
+    y <- copy newy
+    iterations <- increment
+    loop
+  }
+  return iterations
+}
+
+fn mandelbrot-done? x: float, y: float -> _/eax: boolean {
+  # x*x + y*y > 4
+  var x2/xmm0: float <- copy x
+  x2 <- multiply x
+  var y2/xmm1: float <- copy y
+  y2 <- multiply y
+  var sum/xmm0: float <- copy x2
+  sum <- add y2
+  var four/eax: int <- copy 4
+  var four-f/xmm1: float <- convert four
+  compare sum, four-f
+  {
+    break-if-float>
+    return 0/false
+  }
+  return 1/true
+}
+
+fn mandelbrot-x x: float, y: float, real: float -> _/xmm2: float {
+  # x*x - y*y + real
+  var x2/xmm0: float <- copy x
+  x2 <- multiply x
+  var y2/xmm1: float <- copy y
+  y2 <- multiply y
+  var result/xmm0: float <- copy x2
+  result <- subtract y2
+  result <- add real
+  return result
+}
+
+fn mandelbrot-y x: float, y: float, imaginary: float -> _/xmm3: float {
+  # 2*x*y + imaginary
+  var two/eax: int <- copy 2
+  var result/xmm0: float <- convert two
+  result <- multiply x
+  result <- multiply y
+  result <- add imaginary
+  return result
+}
+
+# Scale (x, y) pixel coordinates to a complex plane where the viewport width
+# ranges from -2 to +2. Viewport height just follows the viewport's aspect
+# ratio.
+
+fn viewport-to-real x: int, width: int -> _/xmm0: float {
+  # (x - width/2)*4/width
+  var result/xmm0: float <- convert x
+  var width-f/xmm1: float <- convert width
+  var two/eax: int <- copy 2
+  var two-f/xmm2: float <- convert two
+  var half-width-f/xmm2: float <- reciprocal two-f
+  half-width-f <- multiply width-f
+  result <- subtract half-width-f
+  var four/eax: int <- copy 4
+  var four-f/xmm2: float <- convert four
+  result <- multiply four-f
+  result <- divide width-f
+  return result
+}
+
+fn viewport-to-imaginary y: int, width: int, height: int -> _/xmm1: float {
+  # (y - height/2)*4/width
+  var result/xmm0: float <- convert y
+  var height-f/xmm1: float <- convert height
+  var half-height-f/xmm1: float <- copy height-f
+  var two/eax: int <- copy 2
+  var two-f/xmm2: float <- convert two
+  half-height-f <- divide two-f
+  result <- subtract half-height-f
+  var four/eax: int <- copy 4
+  var four-f/xmm1: float <- convert four
+  result <- multiply four-f
+  var width-f/xmm1: float <- convert width
+  result <- divide width-f
+  return result
+}
diff --git a/apps/mandelbrot.mu b/apps/mandelbrot.mu
new file mode 100644
index 00000000..218fd4fa
--- /dev/null
+++ b/apps/mandelbrot.mu
@@ -0,0 +1,179 @@
+# Mandelbrot set
+#
+# Install:
+#   $ git clone https://github.com/akkartik/mu
+#   $ cd mu
+# Build on Linux:
+#   $ ./translate apps/mandelbrot.mu
+# Build on other platforms (slow):
+#   $ ./translate_emulated apps/mandelbrot.mu
+# Run:
+#   $ qemu-system-i386 code.img
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  # Initially the viewport is centered at 0, 0 in the scene.
+  var zero: float
+  var scene-cx/xmm1: float <- copy zero
+  var scene-cy/xmm2: float <- copy zero
+  # Initially the viewport shows a section of the scene 4 units wide.
+  # scene-width-scale = 0.5
+  var scene-width-scale: float
+  var dest/eax: (addr float) <- address scene-width-scale
+  fill-in-rational dest, 1, 2
+  # scene-width = 4
+  var four: float
+  var dest/eax: (addr float) <- address four
+  fill-in-rational dest, 4, 1
+  var scene-width/xmm3: float <- copy four
+  {
+    mandelbrot screen scene-cx, scene-cy, scene-width
+    # move the center some % of the current screen-width
+    var adj/xmm0: float <- rational 2, 0x1c/28
+    adj <- multiply scene-width
+    scene-cx <- subtract adj
+    scene-cy <- add adj
+    # slowly shrink the scene width to zoom in
+    scene-width <- multiply scene-width-scale
+    loop
+  }
+}
+
+fn mandelbrot screen: (addr screen), scene-cx: float, scene-cy: float, scene-width: float {
+  var a/eax: int <- copy 0
+  var b/ecx: int <- copy 0
+  a, b <- screen-size screen
+  var width/esi: int <- copy a
+  width <- shift-left 3/log2-font-width
+  var height/edi: int <- copy b
+  height <- shift-left 4/log2-font-height
+  var y/ecx: int <- copy 0
+  {
+    compare y, height
+    break-if->=
+    var imaginary/xmm1: float <- viewport-to-imaginary y, width, height, scene-cy, scene-width
+    var x/ebx: int <- copy 0
+    {
+      compare x, width
+      break-if->=
+      var real/xmm0: float <- viewport-to-real x, width, scene-cx, scene-width
+      var iterations/eax: int <- mandelbrot-iterations-for-point real, imaginary, 0x400/max
+      iterations <- shift-right 3
+      var color/edx: int <- copy 0
+      iterations, color <- integer-divide iterations, 0x18/24/size-of-cycle-0
+      color <- add 0x20/cycle-0
+      pixel screen, x, y, color
+      x <- increment
+      loop
+    }
+    y <- increment
+    loop
+  }
+}
+
+fn mandelbrot-iterations-for-point real: float, imaginary: float, max: int -> _/eax: int {
+  var zero: float
+  var x/xmm0: float <- copy zero
+  var y/xmm1: float <- copy zero
+  var iterations/ecx: int <- copy 0
+  {
+    var done?/eax: boolean <- mandelbrot-done? x, y
+    compare done?, 0/false
+    break-if-!=
+    compare iterations, max
+    break-if->=
+    var newx/xmm2: float <- mandelbrot-x x, y, real
+    var newy/xmm3: float <- mandelbrot-y x, y, imaginary
+    x <- copy newx
+    y <- copy newy
+    iterations <- increment
+    loop
+  }
+  return iterations
+}
+
+fn mandelbrot-done? x: float, y: float -> _/eax: boolean {
+  # x*x + y*y > 4
+  var x2/xmm0: float <- copy x
+  x2 <- multiply x
+  var y2/xmm1: float <- copy y
+  y2 <- multiply y
+  var sum/xmm0: float <- copy x2
+  sum <- add y2
+  var four/eax: int <- copy 4
+  var four-f/xmm1: float <- convert four
+  compare sum, four-f
+  {
+    break-if-float>
+    return 0/false
+  }
+  return 1/true
+}
+
+fn mandelbrot-x x: float, y: float, real: float -> _/xmm2: float {
+  # x*x - y*y + real
+  var x2/xmm0: float <- copy x
+  x2 <- multiply x
+  var y2/xmm1: float <- copy y
+  y2 <- multiply y
+  var result/xmm0: float <- copy x2
+  result <- subtract y2
+  result <- add real
+  return result
+}
+
+fn mandelbrot-y x: float, y: float, imaginary: float -> _/xmm3: float {
+  # 2*x*y + imaginary
+  var two/eax: int <- copy 2
+  var result/xmm0: float <- convert two
+  result <- multiply x
+  result <- multiply y
+  result <- add imaginary
+  return result
+}
+
+# Scale (x, y) pixel coordinates to a complex plane where the viewport width
+# ranges from -2 to +2. Viewport height just follows the viewport's aspect
+# ratio.
+
+fn viewport-to-real x: int, width: int, scene-cx: float, scene-width: float -> _/xmm0: float {
+  # 0 in the viewport       goes to scene-cx - scene-width/2 
+  # width in the viewport   goes to scene-cx + scene-width/2
+  # Therefore:
+  # x in the viewport       goes to (scene-cx - scene-width/2) + x*scene-width/width
+  # At most two numbers being multiplied before a divide, so no risk of overflow.
+  var result/xmm0: float <- convert x
+  result <- multiply scene-width
+  var width-f/xmm1: float <- convert width
+  result <- divide width-f
+  result <- add scene-cx
+  var two/eax: int <- copy 2
+  var two-f/xmm2: float <- convert two
+  var half-scene-width/xmm1: float <- copy scene-width
+  half-scene-width <- divide two-f
+  result <- subtract half-scene-width
+  return result
+}
+
+fn viewport-to-imaginary y: int, width: int, height: int, scene-cy: float, scene-width: float -> _/xmm1: float {
+  # 0 in the viewport       goes to scene-cy - scene-width/2*height/width
+  # height in the viewport  goes to scene-cy + scene-width/2*height/width
+  # Therefore:
+  # y in the viewport       goes to (scene-cy - scene-width/2*height/width) + y*scene-width/width
+  #  scene-cy - scene-width/width * (height/2 + y)
+  # At most two numbers being multiplied before a divide, so no risk of overflow.
+  var result/xmm0: float <- convert y
+  result <- multiply scene-width
+  var width-f/xmm1: float <- convert width
+  result <- divide width-f
+  result <- add scene-cy
+  var two/eax: int <- copy 2
+  var two-f/xmm2: float <- convert two
+  var second-term/xmm1: float <- copy scene-width
+  second-term <- divide two-f
+  var height-f/xmm2: float <- convert height
+  second-term <- multiply height-f
+  var width-f/xmm2: float <- convert width
+  second-term <- divide width-f
+  result <- subtract second-term
+  return result
+}
diff --git a/apps/rpn.mu b/apps/rpn.mu
new file mode 100644
index 00000000..1f432365
--- /dev/null
+++ b/apps/rpn.mu
@@ -0,0 +1,151 @@
+# Integer arithmetic using postfix notation
+#
+# Limitations:
+#   Division not implemented yet.
+#
+# To build:
+#   $ ./translate apps/rpn.mu
+#
+# Example session:
+#   $ qemu-system-i386 code.img
+#   > 4
+#   4
+#   > 5 3 -
+#   2
+#
+# Error handling is non-existent. This is just a prototype.
+
+fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
+  var in-storage: (stream byte 0x80)
+  var in/esi: (addr stream byte) <- address in-storage
+  var y/ecx: int <- copy 0
+  var space/edx: grapheme <- copy 0x20
+  # read-eval-print loop
+  {
+    # print prompt
+    var x/eax: int <- draw-text-rightward screen, "> ", 0/x, 0x80/xmax, y, 3/fg/cyan, 0/bg
+    # read line from keyboard
+    clear-stream in
+    {
+      draw-cursor screen, space
+      var key/eax: byte <- read-key keyboard
+      compare key, 0xa/newline
+      break-if-=
+      compare key, 0
+      loop-if-=
+      var key2/eax: int <- copy key
+      append-byte in, key2
+      var g/eax: grapheme <- copy key2
+      draw-grapheme-at-cursor screen, g, 0xf/fg, 0/bg
+      move-cursor-right 0
+      loop
+    }
+    # clear cursor
+    draw-grapheme-at-cursor screen, space, 3/fg/never-used, 0/bg
+    # parse and eval
+    var out/eax: int <- simplify in
+    # print
+    y <- increment
+    out, y <- draw-int32-decimal-wrapping-right-then-down screen, out, 0/xmin, y, 0x80/xmax, 0x30/ymax, 0/x, y, 7/fg, 0/bg
+    # newline
+    y <- increment
+    #
+    loop
+  }
+}
+
+type int-stack {
+  data: (handle array int)
+  top: int
+}
+
+fn simplify in: (addr stream byte) -> _/eax: int {
+  var word-storage: slice
+  var word/ecx: (addr slice) <- address word-storage
+  var stack-storage: int-stack
+  var stack/esi: (addr int-stack) <- address stack-storage
+  initialize-int-stack stack, 0x10
+  $simplify:word-loop: {
+    next-word in, word
+    var done?/eax: boolean <- slice-empty? word
+    compare done?, 0
+    break-if-!=
+    # if word is an operator, perform it
+    {
+      var is-add?/eax: boolean <- slice-equal? word, "+"
+      compare is-add?, 0
+      break-if-=
+      var _b/eax: int <- pop-int-stack stack
+      var b/edx: int <- copy _b
+      var a/eax: int <- pop-int-stack stack
+      a <- add b
+      push-int-stack stack, a
+      loop $simplify:word-loop
+    }
+    {
+      var is-sub?/eax: boolean <- slice-equal? word, "-"
+      compare is-sub?, 0
+      break-if-=
+      var _b/eax: int <- pop-int-stack stack
+      var b/edx: int <- copy _b
+      var a/eax: int <- pop-int-stack stack
+      a <- subtract b
+      push-int-stack stack, a
+      loop $simplify:word-loop
+    }
+    {
+      var is-mul?/eax: boolean <- slice-equal? word, "*"
+      compare is-mul?, 0
+      break-if-=
+      var _b/eax: int <- pop-int-stack stack
+      var b/edx: int <- copy _b
+      var a/eax: int <- pop-int-stack stack
+      a <- multiply b
+      push-int-stack stack, a
+      loop $simplify:word-loop
+    }
+    # otherwise it's an int
+    var n/eax: int <- parse-decimal-int-from-slice word
+    push-int-stack stack, n
+    loop
+  }
+  var result/eax: int <- pop-int-stack stack
+  return result
+}
+
+fn initialize-int-stack _self: (addr int-stack), n: int {
+  var self/esi: (addr int-stack) <- copy _self
+  var d/edi: (addr handle array int) <- get self, data
+  populate d, n
+  var top/eax: (addr int) <- get self, top
+  copy-to *top, 0
+}
+
+fn push-int-stack _self: (addr int-stack), _val: int {
+  var self/esi: (addr int-stack) <- copy _self
+  var top-addr/ecx: (addr int) <- get self, top
+  var data-ah/edx: (addr handle array int) <- get self, data
+  var data/eax: (addr array int) <- lookup *data-ah
+  var top/edx: int <- copy *top-addr
+  var dest-addr/edx: (addr int) <- index data, top
+  var val/eax: int <- copy _val
+  copy-to *dest-addr, val
+  add-to *top-addr, 1
+}
+
+fn pop-int-stack _self: (addr int-stack) -> _/eax: int {
+  var self/esi: (addr int-stack) <- copy _self
+  var top-addr/ecx: (addr int) <- get self, top
+  {
+    compare *top-addr, 0
+    break-if->
+    return 0
+  }
+  subtract-from *top-addr, 1
+  var data-ah/edx: (addr handle array int) <- get self, data
+  var data/eax: (addr array int) <- lookup *data-ah
+  var top/edx: int <- copy *top-addr
+  var result-addr/eax: (addr int) <- index data, top
+  var val/eax: int <- copy *result-addr
+  return val
+}