about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--apps/tile/environment.mu181
-rw-r--r--apps/tile/gap-buffer.mu256
-rw-r--r--apps/tile/int-stack.mu49
-rw-r--r--apps/tile/rpn.mu94
-rw-r--r--apps/tile/word.mu95
5 files changed, 583 insertions, 92 deletions
diff --git a/apps/tile/environment.mu b/apps/tile/environment.mu
index bd2a1814..7814676a 100644
--- a/apps/tile/environment.mu
+++ b/apps/tile/environment.mu
@@ -1,26 +1,14 @@
 type environment {
   screen: (handle screen)
-  buf: gap-buffer
-  cursor-row: int
-  cursor-col: int
+  cursor-word: (handle word)
 }
 
 fn initialize-environment _env: (addr environment) {
   var env/esi: (addr environment) <- copy _env
-  var screen-ah/edi: (addr handle screen) <- get env, screen
-  var _screen/eax: (addr screen) <- lookup *screen-ah
-  var screen/edi: (addr screen) <- copy _screen
-  {
-    var cursor-col/eax: (addr int) <- get env, cursor-col
-    copy-to *cursor-col, 3
-  }
-  {
-    var cursor-row/eax: (addr int) <- get env, cursor-row
-    copy-to *cursor-row, 3
-  }
-  # buf
-  var gap/eax: (addr gap-buffer) <- get env, buf
-  initialize-gap-buffer gap
+  var cursor-word-ah/eax: (addr handle word) <- get env, cursor-word
+  allocate cursor-word-ah
+  var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
+  initialize-word cursor-word
 }
 
 fn initialize-environment-with-fake-screen _self: (addr environment), nrows: int, ncols: int {
@@ -34,13 +22,23 @@ fn initialize-environment-with-fake-screen _self: (addr environment), nrows: int
 
 fn render-loop _self: (addr environment) {
   var self/esi: (addr environment) <- copy _self
-  render self
+  # initial render
+  {
+    var screen-ah/edi: (addr handle screen) <- get self, screen
+    var screen/eax: (addr screen) <- lookup *screen-ah
+    move-cursor screen, 3, 3
+  }
   #
   $interactive:loop: {
     var key/eax: grapheme <- read-key-from-real-keyboard
     compare key, 0x71  # 'q'
     break-if-=
     process self, key
+    var max-depth/eax: int <- compute-max-depth self
+    print-string-to-real-screen "ZZ: "
+    print-int32-decimal-to-real-screen max-depth
+    print-string-to-real-screen "\n"
+#?     render self, max-depth
     loop
   }
 }
@@ -48,54 +46,48 @@ fn render-loop _self: (addr environment) {
 fn process _self: (addr environment), key: grapheme {
 $process:body: {
     var self/esi: (addr environment) <- copy _self
-    var screen-ah/edi: (addr handle screen) <- get self, screen
-    var _screen/eax: (addr screen) <- lookup *screen-ah
-    var screen/edi: (addr screen) <- copy _screen
-    var buf/ebx: (addr gap-buffer) <- get self, buf
+    var cursor-word-ah/eax: (addr handle word) <- get self, cursor-word
+    var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
+    var cursor-word/ecx: (addr word) <- copy _cursor-word
     compare key, 0x445b1b  # left-arrow
     {
       break-if-!=
-      var char-skipped/eax: grapheme <- gap-left buf
-      compare char-skipped, -1
-      {
-        break-if-=
-        var cursor-row/eax: (addr int) <- get self, cursor-row
-        var cursor-col/ecx: (addr int) <- get self, cursor-col
-        decrement *cursor-col
-        move-cursor screen, *cursor-row, *cursor-col
-      }
+      # TODO:
+      #   gap-left cursor-word
+      # or
+      #   cursor-word = cursor-word->prev
+      #   gap-to-end cursor-word
       break $process:body
     }
     compare key, 0x435b1b  # right-arrow
     {
       break-if-!=
-      var char-skipped/eax: grapheme <- gap-right buf
-      compare char-skipped, -1
-      {
-        break-if-=
-        var cursor-row/eax: (addr int) <- get self, cursor-row
-        var cursor-col/ecx: (addr int) <- get self, cursor-col
-        increment *cursor-col
-        move-cursor screen, *cursor-row, *cursor-col
-      }
+      # TODO:
+      #   gap-right cursor-word
+      # or
+      #   cursor-word = cursor-word->next
+      #   gap-to-start cursor-word
+      break $process:body
+    }
+    compare key, 0x20  # space
+    {
+      break-if-!=
+      # TODO: new word
       break $process:body
     }
-    var g/ecx: grapheme <- copy key
+    var g/edx: grapheme <- copy key
     var print?/eax: boolean <- real-grapheme? key
     {
       compare print?, 0  # false
       break-if-=
-      add-grapheme-at-gap buf, g
-      var cursor-col/eax: (addr int) <- get self, cursor-col
-      increment *cursor-col
-      render self
+      add-grapheme-to-word cursor-word, g
       break $process:body
     }
     # silently ignore other hotkeys
 }
 }
 
-fn render _env: (addr environment) {
+fn render _env: (addr environment), max-depth: int {
   var env/esi: (addr environment) <- copy _env
   var screen-ah/edi: (addr handle screen) <- get env, screen
   var _screen/eax: (addr screen) <- lookup *screen-ah
@@ -103,13 +95,92 @@ fn render _env: (addr environment) {
   # prepare screen
   clear-screen screen
   move-cursor screen, 3, 3
-  # render input area
-  var buf/ecx: (addr gap-buffer) <- get env, buf
-  render-gap-buffer screen, buf
-#?   # render stacks
-#?   render-all-stacks screen
-  # update cursor
-  var cursor-row/eax: (addr int) <- get env, cursor-row
-  var cursor-col/ecx: (addr int) <- get env, cursor-col
-  move-cursor screen, *cursor-row, *cursor-col
+  # cursor-word
+  var cursor-word-ah/esi: (addr handle word) <- get env, cursor-word
+  var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
+  var cursor-word/ebx: (addr word) <- copy _cursor-word
+  # curr-word
+  var curr-word/eax: (addr word) <- first-word cursor-word
+  # first-word
+  var first-word: (addr word)
+  copy-to first-word, curr-word
+  # cursor-col
+  var cursor-col: int
+  var cursor-col-a: (addr int)
+  var tmp/ecx: (addr int) <- address cursor-col
+  copy-to cursor-col-a, tmp
+  # curr-col
+  var curr-col/ecx: int <- copy 3
+  {
+    compare curr-word, 0
+    break-if-=
+    move-cursor screen, 3, curr-col
+    print-word screen, curr-word
+    curr-col <- render-stack screen, first-word, curr-word, max-depth, curr-col, cursor-word, cursor-col-a
+    var next-word-ah/edx: (addr handle word) <- get curr-word, next
+    curr-word <- lookup *next-word-ah
+    loop
+  }
+  move-cursor screen, 3, *cursor-col-a
+}
+
+# Render the stack result from interpreting first-world to final-word (inclusive)
+# with the bottom-left corner at botleft-row, botleft-col.
+#
+# Outputs:
+# - Return the farthest column written.
+# - If final-word is same as cursor-word, do some additional computation to set
+#   cursor-col-a.
+fn render-stack screen: (addr screen), first-word: (addr word), final-word: (addr word), botleft-row: int, botleft-col: int, cursor-word: (addr word), cursor-col-a: (addr int) -> right-col/ecx: int {
+  print-word screen, first-word
+}
+
+# We could be a little faster by not using 'first-word' (since max is commutative),
+# but this way the code follows the pattern of 'render'. Let's see if that's a net win.
+fn compute-max-depth _env: (addr environment) -> result/eax: int {
+  var env/esi: (addr environment) <- copy _env
+  # cursor-word
+  var cursor-word-ah/esi: (addr handle word) <- get env, cursor-word
+  var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
+  {
+    var foo/eax: int <- copy cursor-word
+    print-string-to-real-screen "cursor-word: "
+    print-int32-hex-to-real-screen foo
+    print-string-to-real-screen "\n"
+  }
+  # curr-word
+  var curr-word/eax: (addr word) <- first-word cursor-word
+  {
+    var foo/eax: int <- copy curr-word
+    print-string-to-real-screen "curr-word: "
+    print-int32-hex-to-real-screen foo
+    print-string-to-real-screen "\n"
+  }
+  # first-word
+  var first-word: (addr word)
+  copy-to first-word, curr-word
+  #
+  var out/ebx: int <- copy 0
+  {
+    compare curr-word, 0
+    break-if-=
+    {
+      var a/eax: int <- copy first-word
+      print-string-to-real-screen "outside max-stack-depth: "
+      print-int32-hex-to-real-screen a
+      print-string-to-real-screen "\n"
+    }
+    var curr-max-depth/edi: int <- max-stack-depth first-word, curr-word
+    compare curr-max-depth, out
+    {
+      break-if-<=
+      out <- copy curr-max-depth
+    }
+    var next-word-ah/edx: (addr handle word) <- get curr-word, next
+    curr-word <- lookup *next-word-ah
+    loop
+  }
+  print-int32-decimal-to-real-screen out
+  print-string-to-real-screen "\n"
+  result <- copy out
 }
diff --git a/apps/tile/gap-buffer.mu b/apps/tile/gap-buffer.mu
index 9d6b7161..1503690b 100644
--- a/apps/tile/gap-buffer.mu
+++ b/apps/tile/gap-buffer.mu
@@ -11,6 +11,22 @@ fn initialize-gap-buffer _self: (addr gap-buffer) {
   initialize-grapheme-stack right, 0x10
 }
 
+# just for tests
+fn initialize-gap-buffer-with self: (addr gap-buffer), s: (addr array byte) {
+  initialize-gap-buffer self
+  var stream-storage: (stream byte 0x10)
+  var stream/ecx: (addr stream byte) <- address stream-storage
+  write stream, s
+  {
+    var done?/eax: boolean <- stream-empty? stream
+    compare done?, 0  # false
+    break-if-!=
+    var g/eax: grapheme <- read-grapheme stream
+    add-grapheme-at-gap self, g
+    loop
+  }
+}
+
 fn render-gap-buffer screen: (addr screen), _gap: (addr gap-buffer) {
   var gap/esi: (addr gap-buffer) <- copy _gap
   var left/eax: (addr grapheme-stack) <- get gap, left
@@ -30,49 +46,26 @@ fn gap-buffer-length _gap: (addr gap-buffer) -> result/eax: int {
   result <- add left-length
 }
 
-# dump stack to screen from bottom to top
-# don't move the cursor or anything
-fn render-stack-from-bottom _self: (addr grapheme-stack), screen: (addr screen) {
-  var self/esi: (addr grapheme-stack) <- copy _self
-  var data-ah/edi: (addr handle array grapheme) <- get self, data
-  var _data/eax: (addr array grapheme) <- lookup *data-ah
-  var data/edi: (addr array grapheme) <- copy _data
-  var top-addr/ecx: (addr int) <- get self, top
-  var i/eax: int <- copy 0
-  {
-    compare i, *top-addr
-    break-if->=
-    var g/edx: (addr grapheme) <- index data, i
-    print-grapheme screen, *g
-    i <- increment
-    loop
-  }
+fn add-grapheme-at-gap _self: (addr gap-buffer), g: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var left/eax: (addr grapheme-stack) <- get self, left
+  push-grapheme-stack left, g
 }
 
-# dump stack to screen from top to bottom
-# don't move the cursor or anything
-fn render-stack-from-top _self: (addr grapheme-stack), screen: (addr screen) {
-  var self/esi: (addr grapheme-stack) <- copy _self
-  var data-ah/edi: (addr handle array grapheme) <- get self, data
-  var _data/eax: (addr array grapheme) <- lookup *data-ah
-  var data/edi: (addr array grapheme) <- copy _data
-  var top-addr/ecx: (addr int) <- get self, top
-  var i/eax: int <- copy *top-addr
-  i <- decrement
+fn gap-to-start self: (addr gap-buffer) {
   {
-    compare i, 0
-    break-if-<
-    var g/edx: (addr grapheme) <- index data, i
-    print-grapheme screen, *g
-    i <- decrement
-    loop
+    var curr/eax: grapheme <- gap-left self
+    compare curr, -1
+    loop-if-!=
   }
 }
 
-fn add-grapheme-at-gap _self: (addr gap-buffer), g: grapheme {
-  var self/esi: (addr gap-buffer) <- copy _self
-  var left/eax: (addr grapheme-stack) <- get self, left
-  push-grapheme-stack left, g
+fn gap-to-end self: (addr gap-buffer) {
+  {
+    var curr/eax: grapheme <- gap-right self
+    compare curr, -1
+    loop-if-!=
+  }
 }
 
 fn gap-right _self: (addr gap-buffer) -> result/eax: grapheme {
@@ -112,6 +105,79 @@ $gap-left:body: {
 }
 }
 
+fn gap-buffer-equal? _self: (addr gap-buffer), s: (addr array byte) -> result/eax: boolean {
+$gap-buffer-equal?:body: {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # complication: graphemes may be multiple bytes
+  # so don't rely on length
+  # instead turn the expected result into a stream and arrange to read from it in order
+  var stream-storage: (stream byte 0x10)
+  var expected-stream/ecx: (addr stream byte) <- address stream-storage
+  write expected-stream, s
+  # compare left
+  var left/edx: (addr grapheme-stack) <- get self, left
+  result <- prefix-match? left, expected-stream
+  compare result, 0  # false
+  break-if-= $gap-buffer-equal?:body
+  # compare right
+  var right/edx: (addr grapheme-stack) <- get self, right
+  result <- suffix-match? right, expected-stream
+  compare result, 0  # false
+  break-if-= $gap-buffer-equal?:body
+  # ensure there's nothing left over
+  result <- stream-empty? expected-stream
+}
+}
+
+fn test-gap-buffer-equal-from-end? {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer g
+  #
+  var c/eax: grapheme <- copy 0x61  # 'a'
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  # gap is at end (right is empty)
+  var _result/eax: boolean <- gap-buffer-equal? g, "aaa"
+  var result/eax: int <- copy _result
+  check-ints-equal result, 1, "F - test-gap-buffer-equal-from-end?"
+}
+
+fn test-gap-buffer-equal-from-middle? {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer g
+  #
+  var c/eax: grapheme <- copy 0x61  # 'a'
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  var dummy/eax: grapheme <- gap-left g
+  # gap is in the middle
+  var _result/eax: boolean <- gap-buffer-equal? g, "aaa"
+  var result/eax: int <- copy _result
+  check-ints-equal result, 1, "F - test-gap-buffer-equal-from-middle?"
+}
+
+fn test-gap-buffer-equal-from-start? {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer g
+  #
+  var c/eax: grapheme <- copy 0x61  # 'a'
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  var dummy/eax: grapheme <- gap-left g
+  dummy <- gap-left g
+  dummy <- gap-left g
+  # gap is at the start
+  var _result/eax: boolean <- gap-buffer-equal? g, "aaa"
+  var result/eax: int <- copy _result
+  check-ints-equal result, 1, "F - test-gap-buffer-equal-from-start?"
+}
+
 type grapheme-stack {
   data: (handle array grapheme)
   top: int
@@ -125,6 +191,20 @@ fn initialize-grapheme-stack _self: (addr grapheme-stack), n: int {
   copy-to *top, 0
 }
 
+fn grapheme-stack-empty? _self: (addr grapheme-stack) -> result/eax: boolean {
+$grapheme-stack-empty?:body: {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var top/eax: (addr int) <- get self, top
+  compare *top, 0
+  {
+    break-if-=
+    result <- copy 1  # false
+    break $grapheme-stack-empty?:body
+  }
+  result <- copy 0  # false
+}
+}
+
 fn push-grapheme-stack _self: (addr grapheme-stack), _val: grapheme {
   var self/esi: (addr grapheme-stack) <- copy _self
   var top-addr/ecx: (addr int) <- get self, top
@@ -155,3 +235,105 @@ $pop-grapheme-stack:body: {
   val <- copy *result-addr
 }
 }
+
+# dump stack to screen from bottom to top
+# don't move the cursor or anything
+fn render-stack-from-bottom _self: (addr grapheme-stack), screen: (addr screen) {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/eax: int <- copy 0
+  {
+    compare i, *top-addr
+    break-if->=
+    var g/edx: (addr grapheme) <- index data, i
+    print-grapheme screen, *g
+    i <- increment
+    loop
+  }
+}
+
+# dump stack to screen from top to bottom
+# don't move the cursor or anything
+fn render-stack-from-top _self: (addr grapheme-stack), screen: (addr screen) {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/eax: int <- copy *top-addr
+  i <- decrement
+  {
+    compare i, 0
+    break-if-<
+    var g/edx: (addr grapheme) <- index data, i
+    print-grapheme screen, *g
+    i <- decrement
+    loop
+  }
+}
+
+# compare from bottom
+# beware: modifies 'stream', which must be disposed of after a false result
+fn prefix-match? _self: (addr grapheme-stack), s: (addr stream byte) -> result/eax: boolean {
+$prefix-match?:body: {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/ebx: int <- copy 0
+  {
+    compare i, *top-addr
+    break-if->=
+    # if curr != expected, return false
+    {
+      var curr-a/edx: (addr grapheme) <- index data, i
+      var expected/eax: grapheme <- read-grapheme s
+      {
+        compare expected, *curr-a
+        break-if-=
+        result <- copy 0  # false
+        break $prefix-match?:body
+      }
+    }
+    i <- increment
+    loop
+  }
+  result <- copy 1   # true
+}
+}
+
+# compare from bottom
+# beware: modifies 'stream', which must be disposed of after a false result
+fn suffix-match? _self: (addr grapheme-stack), s: (addr stream byte) -> result/eax: boolean {
+$suffix-match?:body: {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/eax: (addr int) <- get self, top
+  var i/ebx: int <- copy *top-addr
+  i <- decrement
+  {
+    compare i, 0
+    break-if-<
+    {
+      var curr-a/edx: (addr grapheme) <- index data, i
+      var expected/eax: grapheme <- read-grapheme s
+      # if curr != expected, return false
+      {
+        compare expected, *curr-a
+        break-if-=
+        result <- copy 0  # false
+        break $suffix-match?:body
+      }
+    }
+    i <- decrement
+    loop
+  }
+  result <- copy 1   # true
+}
+}
diff --git a/apps/tile/int-stack.mu b/apps/tile/int-stack.mu
new file mode 100644
index 00000000..5e7a2223
--- /dev/null
+++ b/apps/tile/int-stack.mu
@@ -0,0 +1,49 @@
+type int-stack {
+  data: (handle array int)
+  top: int
+}
+
+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 clear-int-stack _self: (addr int-stack) {
+  var self/esi: (addr int-stack) <- copy _self
+  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) -> val/eax: int {
+$pop-int-stack:body: {
+  var self/esi: (addr int-stack) <- copy _self
+  var top-addr/ecx: (addr int) <- get self, top
+  {
+    compare *top-addr, 0
+    break-if->
+    val <- copy 0
+    break $pop-int-stack:body
+  }
+  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
+  val <- copy *result-addr
+}
+}
diff --git a/apps/tile/rpn.mu b/apps/tile/rpn.mu
new file mode 100644
index 00000000..a70d9fd5
--- /dev/null
+++ b/apps/tile/rpn.mu
@@ -0,0 +1,94 @@
+fn simplify in: (addr stream byte), out: (addr int-stack) {
+  var word-storage: slice
+  var word/ecx: (addr slice) <- address word-storage
+  clear-int-stack out
+  $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 out
+      var b/edx: int <- copy _b
+      var a/eax: int <- pop-int-stack out
+      a <- add b
+      push-int-stack out, 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 out
+      var b/edx: int <- copy _b
+      var a/eax: int <- pop-int-stack out
+      a <- subtract b
+      push-int-stack out, 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 out
+      var b/edx: int <- copy _b
+      var a/eax: int <- pop-int-stack out
+      a <- multiply b
+      push-int-stack out, a
+      loop $simplify:word-loop
+    }
+    # otherwise it's an int
+    var n/eax: int <- parse-decimal-int-from-slice word
+    push-int-stack out, n
+    loop
+  }
+}
+
+# Copy of 'simplify' that just tracks the maximum stack depth needed
+# Doesn't actually need to simulate the stack, since every word has a predictable effect.
+fn max-stack-depth first-word: (addr word), final-word: (addr word) -> result/edi: int {
+  var a/eax: int <- copy first-word
+  print-string-to-real-screen "inside max-stack-depth: "
+  print-int32-hex-to-real-screen a
+  print-string-to-real-screen "\n"
+  var curr-word/esi: (addr word) <- copy first-word
+  var curr-depth/ecx: int <- copy 0
+  result <- copy 0
+  $max-stack-depth:word-loop: {
+    # handle operators
+    {
+      var is-add?/eax: boolean <- word-equal? curr-word, "+"
+      compare is-add?, 0
+      break-if-=
+      curr-depth <- decrement
+      loop $max-stack-depth:word-loop
+    }
+    {
+      var is-sub?/eax: boolean <- word-equal? curr-word, "-"
+      compare is-sub?, 0
+      break-if-=
+      curr-depth <- decrement
+      loop $max-stack-depth:word-loop
+    }
+    {
+      var is-mul?/eax: boolean <- word-equal? curr-word, "*"
+      compare is-mul?, 0
+      break-if-=
+      curr-depth <- decrement
+      loop $max-stack-depth:word-loop
+    }
+    # otherwise it's an int (do we need error-checking?)
+    curr-depth <- increment
+    # update max depth if necessary
+    {
+      compare curr-depth, result
+      break-if-<=
+      result <- copy curr-depth
+    }
+    loop
+  }
+}
diff --git a/apps/tile/word.mu b/apps/tile/word.mu
new file mode 100644
index 00000000..c87a2242
--- /dev/null
+++ b/apps/tile/word.mu
@@ -0,0 +1,95 @@
+type word {
+  data: gap-buffer
+  next: (handle word)
+  prev: (handle word)
+}
+
+fn initialize-word _self: (addr word) {
+  var self/esi: (addr word) <- copy _self
+  var data/eax: (addr gap-buffer) <- get self, data
+  initialize-gap-buffer data
+}
+
+## some helpers for creating words. mostly for tests
+
+fn initialize-word-with _self: (addr word), s: (addr array byte) {
+  var self/esi: (addr word) <- copy _self
+  var data/eax: (addr gap-buffer) <- get self, data
+  initialize-gap-buffer-with data, s
+}
+
+fn allocate-word-with _out: (addr handle word), s: (addr array byte) {
+  var out/eax: (addr handle word) <- copy _out
+  allocate out
+  var out-addr/eax: (addr word) <- lookup *out
+  initialize-word-with out-addr, s
+}
+
+# just for tests for now
+# TODO: handle existing next
+# one implication of handles: append must take a handle
+fn append-word-with self-h: (handle word), s: (addr array byte) {
+  var self/eax: (addr word) <- lookup self-h
+  var next-ah/eax: (addr handle word) <- get self, next
+  allocate-word-with next-ah, s
+  var next/eax: (addr word) <- lookup *next-ah
+  var prev-ah/eax: (addr handle word) <- get next, prev
+  copy-handle self-h, prev-ah
+}
+
+# just for tests for now
+# TODO: handle existing prev
+fn prepend-word-with self-h: (handle word), s: (addr array byte) {
+  var self/eax: (addr word) <- lookup self-h
+  var prev-ah/eax: (addr handle word) <- get self, prev
+  allocate-word-with prev-ah, s
+  var prev/eax: (addr word) <- lookup *prev-ah
+  var next-ah/eax: (addr handle word) <- get prev, next
+  copy-handle self-h, next-ah
+}
+
+## real primitives
+
+fn word-equal? _self: (addr word), s: (addr array byte) -> result/eax: boolean {
+  var self/esi: (addr word) <- copy _self
+  var data/eax: (addr gap-buffer) <- get self, data
+  result <- gap-buffer-equal? data, s
+}
+
+fn first-word _self: (addr word) -> result/eax: (addr word) {
+  var self/esi: (addr word) <- copy _self
+  var out/edi: (addr word) <- copy self
+  var prev/esi: (addr handle word) <- get self, prev
+  {
+    var curr/eax: (addr word) <- lookup *prev
+    compare curr, 0
+    break-if-=
+    out <- copy curr
+    prev <- get curr, prev
+    loop
+  }
+  result <- copy out
+}
+
+fn final-word _self: (addr word), out: (addr handle word) {
+  var self/esi: (addr word) <- copy _self
+  var next/esi: (addr handle word) <- get self, next
+  {
+    copy-object next, out
+    var curr/eax: (addr word) <- lookup *next
+    compare curr, 0
+    loop-if-!=
+  }
+}
+
+fn add-grapheme-to-word _self: (addr word), c: grapheme {
+  var self/esi: (addr word) <- copy _self
+  var data/eax: (addr gap-buffer) <- get self, data
+  add-grapheme-at-gap data, c
+}
+
+fn print-word screen: (addr screen), _self: (addr word) {
+  var self/esi: (addr word) <- copy _self
+  var data/eax: (addr gap-buffer) <- get self, data
+  render-gap-buffer screen, data
+}