about summary refs log tree commit diff stats
path: root/baremetal/shell
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2021-02-09 22:54:09 -0800
committerKartik K. Agaram <vc@akkartik.com>2021-02-09 22:57:37 -0800
commit7d400217994c573153fda0e4a28542c75ace3997 (patch)
tree257e9a3603898c4b447dd007c6e06228c192d5f3 /baremetal/shell
parent1969febce5c3043de55b5e28da2f38ca08d17562 (diff)
downloadmu-7d400217994c573153fda0e4a28542c75ace3997.tar.gz
7708 - baremetal/shell: word data structure
Not everything here is tested, but enough that I'm starting to feel confident.

We see our first divergence with apps/tile. In apps/tile we render everything,
then go back and figure out where to position the cursor. This relies on
some low-level smarts and is also quite klunky and complex.

In baremetal/shell I plan to do something simpler: maintain a tree of objects
where each level knows which sub-object under it has the cursor. Now I
can pass in the cursor object to each object, and if it detects that it
has the cursor it can recursively figure out which sub-object has the cursor.
The bottom-most objects (grapheme stacks) draw the cursor as they render
themselves. Single-pass algorithm, draw the cursor as you render, no low-level
smarts needed.

But there's a divergence. What in apps/tile used to look like this, with
a cursor ␣ at the end of the word 'abc':

  abc␣def

..now looks like this:

  abc␣ def

..with an extra space.

This could cause some jarring 'dancing' as you move the cursor through a
list of words.
Diffstat (limited to 'baremetal/shell')
-rw-r--r--baremetal/shell/gap-buffer.mu89
-rw-r--r--baremetal/shell/word.mu698
2 files changed, 786 insertions, 1 deletions
diff --git a/baremetal/shell/gap-buffer.mu b/baremetal/shell/gap-buffer.mu
index 56aa9098..fa1b83a0 100644
--- a/baremetal/shell/gap-buffer.mu
+++ b/baremetal/shell/gap-buffer.mu
@@ -177,7 +177,7 @@ fn gap-left _self: (addr gap-buffer) -> _/eax: grapheme {
   return g
 }
 
-fn gap-index _self: (addr gap-buffer) -> _/eax: int {
+fn index-of-gap _self: (addr gap-buffer) -> _/eax: int {
   var self/eax: (addr gap-buffer) <- copy _self
   var left/eax: (addr grapheme-stack) <- get self, left
   var top-addr/eax: (addr int) <- get left, top
@@ -333,6 +333,93 @@ fn test-gap-buffer-equal-fails {
   check-not result, "F - test-gap-buffer-equal-fails"
 }
 
+fn gap-buffers-equal? self: (addr gap-buffer), g: (addr gap-buffer) -> _/eax: boolean {
+  var tmp/eax: int <- gap-buffer-length self
+  var len/ecx: int <- copy tmp
+  var leng/eax: int <- gap-buffer-length g
+  compare len, leng
+  {
+    break-if-=
+    return 0/false
+  }
+  var i/edx: int <- copy 0
+  {
+    compare i, len
+    break-if->=
+    {
+      var tmp/eax: grapheme <- gap-index self, i
+      var curr/ecx: grapheme <- copy tmp
+      var currg/eax: grapheme <- gap-index g, i
+      compare curr, currg
+      break-if-=
+      return 0/false
+    }
+    i <- increment
+    loop
+  }
+  return 1/true
+}
+
+fn gap-index _self: (addr gap-buffer), _n: int -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var n/ebx: int <- copy _n
+  # if n < left->length, index into left
+  var left/edi: (addr grapheme-stack) <- get self, left
+  var left-len-a/edx: (addr int) <- get left, top
+  compare n, *left-len-a
+  {
+    break-if->=
+    var data-ah/eax: (addr handle array grapheme) <- get left, data
+    var data/eax: (addr array grapheme) <- lookup *data-ah
+    var result/eax: (addr grapheme) <- index data, n
+    return *result
+  }
+  # shrink n
+  n <- subtract *left-len-a
+  # if n < right->length, index into right
+  var right/edi: (addr grapheme-stack) <- get self, right
+  var right-len-a/edx: (addr int) <- get right, top
+  compare n, *right-len-a
+  {
+    break-if->=
+    var data-ah/eax: (addr handle array grapheme) <- get right, data
+    var data/eax: (addr array grapheme) <- lookup *data-ah
+    var result/eax: (addr grapheme) <- index data, n
+    return *result
+  }
+  # error
+  abort "gap-index: out of bounds"
+  return 0
+}
+
+fn test-gap-buffers-equal? {
+  var _a: gap-buffer
+  var a/esi: (addr gap-buffer) <- address _a
+  initialize-gap-buffer-with a, "abc"
+  var _b: gap-buffer
+  var b/edi: (addr gap-buffer) <- address _b
+  initialize-gap-buffer-with b, "abc"
+  var _c: gap-buffer
+  var c/ebx: (addr gap-buffer) <- address _c
+  initialize-gap-buffer-with c, "ab"
+  var _d: gap-buffer
+  var d/edx: (addr gap-buffer) <- address _d
+  initialize-gap-buffer-with d, "abd"
+  #
+  var result/eax: boolean <- gap-buffers-equal? a, a
+  check result, "F - test-gap-buffers-equal? - reflexive"
+  result <- gap-buffers-equal? a, b
+  check result, "F - test-gap-buffers-equal? - equal"
+  # length not equal
+  result <- gap-buffers-equal? a, c
+  check-not result, "F - test-gap-buffers-equal? - not equal"
+  # contents not equal
+  result <- gap-buffers-equal? a, d
+  check-not result, "F - test-gap-buffers-equal? - not equal 2"
+  result <- gap-buffers-equal? d, a
+  check-not result, "F - test-gap-buffers-equal? - not equal 3"
+}
+
 fn copy-gap-buffer _src-ah: (addr handle gap-buffer), _dest-ah: (addr handle gap-buffer) {
   # obtain src-a, dest-a
   var src-ah/eax: (addr handle gap-buffer) <- copy _src-ah
diff --git a/baremetal/shell/word.mu b/baremetal/shell/word.mu
new file mode 100644
index 00000000..b6c73264
--- /dev/null
+++ b/baremetal/shell/word.mu
@@ -0,0 +1,698 @@
+type word {
+  scalar-data: (handle gap-buffer)
+  next: (handle word)
+  prev: (handle word)
+}
+
+fn initialize-word _self: (addr word) {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  allocate data-ah
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  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-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  allocate data-ah
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  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 move-word-contents _src-ah: (addr handle word), _dest-ah: (addr handle word) {
+  var dest-ah/eax: (addr handle word) <- copy _dest-ah
+  var _dest/eax: (addr word) <- lookup *dest-ah
+  var dest/edi: (addr word) <- copy _dest
+  var src-ah/eax: (addr handle word) <- copy _src-ah
+  var _src/eax: (addr word) <- lookup *src-ah
+  var src/esi: (addr word) <- copy _src
+  cursor-to-start src
+  var src-data-ah/eax: (addr handle gap-buffer) <- get src, scalar-data
+  var src-data/eax: (addr gap-buffer) <- lookup *src-data-ah
+  var src-stack/ecx: (addr grapheme-stack) <- get src-data, right
+  {
+    var done?/eax: boolean <- grapheme-stack-empty? src-stack
+    compare done?, 0/false
+    break-if-!=
+    var g/eax: grapheme <- pop-grapheme-stack src-stack
+#?     print-grapheme 0, g
+#?     print-string 0, "\n"
+    add-grapheme-to-word dest, g
+    loop
+  }
+}
+
+fn copy-word-contents-before-cursor _src-ah: (addr handle word), _dest-ah: (addr handle word) {
+  var dest-ah/eax: (addr handle word) <- copy _dest-ah
+  var _dest/eax: (addr word) <- lookup *dest-ah
+  var dest/edi: (addr word) <- copy _dest
+  var src-ah/eax: (addr handle word) <- copy _src-ah
+  var src/eax: (addr word) <- lookup *src-ah
+  var src-data-ah/eax: (addr handle gap-buffer) <- get src, scalar-data
+  var src-data/eax: (addr gap-buffer) <- lookup *src-data-ah
+  var src-stack/ecx: (addr grapheme-stack) <- get src-data, left
+  var src-stack-data-ah/eax: (addr handle array grapheme) <- get src-stack, data
+  var _src-stack-data/eax: (addr array grapheme) <- lookup *src-stack-data-ah
+  var src-stack-data/edx: (addr array grapheme) <- copy _src-stack-data
+  var top-addr/ecx: (addr int) <- get src-stack, top
+  var i/eax: int <- copy 0
+  {
+    compare i, *top-addr
+    break-if->=
+    var g/edx: (addr grapheme) <- index src-stack-data, i
+    add-grapheme-to-word dest, *g
+    i <- increment
+    loop
+  }
+}
+
+fn word-equal? _self: (addr word), s: (addr array byte) -> _/eax: boolean {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  var result/eax: boolean <- gap-buffer-equal? data, s
+  return result
+}
+
+fn words-equal? _self: (addr word), _w: (addr word) -> _/eax: boolean {
+  var self/eax: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var _data/eax: (addr gap-buffer) <- lookup *data-ah
+  var data/ecx: (addr gap-buffer) <- copy _data
+  var w/eax: (addr word) <- copy _w
+  var w-data-ah/eax: (addr handle gap-buffer) <- get w, scalar-data
+  var w-data/eax: (addr gap-buffer) <- lookup *w-data-ah
+  var result/eax: boolean <- gap-buffers-equal? data, w-data
+  return result
+}
+
+fn word-length _self: (addr word) -> _/eax: int {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  var result/eax: int <- gap-buffer-length data
+  return result
+}
+
+fn first-word _in: (addr handle word), out: (addr handle word) {
+  var curr-ah/esi: (addr handle word) <- copy _in
+  var curr/eax: (addr word) <- lookup *curr-ah
+  var prev/edi: (addr handle word) <- copy 0
+  {
+    prev <- get curr, prev
+    var curr/eax: (addr word) <- lookup *prev
+    compare curr, 0
+    break-if-=
+    copy-object prev, curr-ah
+    loop
+  }
+  copy-object curr-ah, out
+}
+
+fn final-word _in: (addr handle word), out: (addr handle word) {
+  var curr-h: (handle word)
+  var curr-ah/esi: (addr handle word) <- address curr-h
+  copy-object _in, curr-ah
+  var curr/eax: (addr word) <- copy 0
+  var next/edi: (addr handle word) <- copy 0
+  {
+    curr <- lookup *curr-ah
+    next <- get curr, next
+    curr <- lookup *next
+    compare curr, 0
+    break-if-=
+    copy-object next, curr-ah
+    loop
+  }
+  copy-object curr-ah, out  # modify 'out' right at the end, just in case it's same as 'in'
+}
+
+fn first-grapheme _self: (addr word) -> _/eax: grapheme {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  var result/eax: grapheme <- first-grapheme-in-gap-buffer data
+  return result
+}
+
+fn grapheme-before-cursor _self: (addr word) -> _/eax: grapheme {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  var result/eax: grapheme <- grapheme-before-cursor-in-gap-buffer data
+  return result
+}
+
+fn add-grapheme-to-word _self: (addr word), c: grapheme {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  add-grapheme-at-gap data, c
+}
+
+fn cursor-at-start? _self: (addr word) -> _/eax: boolean {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  var result/eax: boolean <- gap-at-start? data
+  return result
+}
+
+fn cursor-at-end? _self: (addr word) -> _/eax: boolean {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  var result/eax: boolean <- gap-at-end? data
+  return result
+}
+
+fn cursor-left _self: (addr word) {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  var dummy/eax: grapheme <- gap-left data
+}
+
+fn cursor-right _self: (addr word) {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  var dummy/eax: grapheme <- gap-right data
+}
+
+fn cursor-to-start _self: (addr word) {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  gap-to-start data
+}
+
+fn cursor-to-end _self: (addr word) {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  gap-to-end data
+}
+
+fn cursor-index _self: (addr word) -> _/eax: int {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  var result/eax: int <- index-of-gap data
+  return result
+}
+
+fn delete-before-cursor _self: (addr word) {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  delete-before-gap data
+}
+
+fn pop-after-cursor _self: (addr word) -> _/eax: grapheme {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  var result/eax: grapheme <- pop-after-gap data
+  return result
+}
+
+fn delete-next _self: (addr word) {
+  var self/esi: (addr word) <- copy _self
+  var next-ah/edi: (addr handle word) <- get self, next
+  var next/eax: (addr word) <- lookup *next-ah
+  compare next, 0
+  break-if-=
+  var next-next-ah/ecx: (addr handle word) <- get next, next
+  var self-ah/esi: (addr handle word) <- get next, prev
+  copy-object next-next-ah, next-ah
+  var new-next/eax: (addr word) <- lookup *next-next-ah
+  compare new-next, 0
+  break-if-=
+  var dest/eax: (addr handle word) <- get new-next, prev
+  copy-object self-ah, dest
+}
+
+fn render-word screen: (addr screen), _self: (addr word), x: int, y: int, render-cursor?: boolean -> _/eax: int {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  var result/eax: int <- render-gap-buffer screen, data, x, y, render-cursor?
+  return result
+}
+
+fn render-words-in-reverse screen: (addr screen), _words-ah: (addr handle word), x: int, y: int, cursor-word-addr: int -> _/eax: int {
+  var words-ah/eax: (addr handle word) <- copy _words-ah
+  var _words-a/eax: (addr word) <- lookup *words-ah
+  var words-a/ecx: (addr word) <- copy _words-a
+  compare words-a, 0
+  {
+    break-if-!=
+    return x
+  }
+  # recurse
+  var next-ah/eax: (addr handle word) <- get words-a, next
+  var next-x/eax: int <- render-words-in-reverse screen, next-ah, x, y, cursor-word-addr
+  # print
+  var render-cursor?/edx: boolean <- copy 0/false
+  {
+    compare cursor-word-addr, words-a
+    break-if-!=
+    render-cursor? <- copy 1/true
+  }
+  next-x <- render-word screen, words-a, next-x, y, render-cursor?
+  var space/ecx: grapheme <- copy 0x20/space
+  draw-grapheme screen, space, next-x, y, 3/fg=cyan, 0/bg
+  next-x <- increment
+  return next-x
+}
+
+fn test-render-words-in-reverse {
+  # words = [aaa, bbb, ccc, ddd]
+  var w-storage: (handle word)
+  var w-ah/esi: (addr handle word) <- address w-storage
+  allocate w-ah
+  var _w/eax: (addr word) <- lookup *w-ah
+  var w/ecx: (addr word) <- copy _w
+  initialize-word-with w, "aaa"
+  append-word-at-end-with w-ah, "bbb"
+  append-word-at-end-with w-ah, "ccc"
+  append-word-at-end-with w-ah, "ddd"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x20, 4
+  #
+  var cursor-word/eax: int <- copy w
+  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
+  check-screen-row screen, 0/y,                                   "ddd ccc bbb aaa  ", "F - test-render-words-in-reverse/0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "               | ", "F - test-render-words-in-reverse/0 cursor"
+  # - start moving cursor left through final word
+  cursor-left w
+  var cursor-word/eax: int <- copy w
+  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
+  check-screen-row screen, 0/y,                                   "ddd ccc bbb aaa  ", "F - test-render-words-in-reverse/1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "              |  ", "F - test-render-words-in-reverse/1 cursor"
+  #
+  cursor-left w
+  var cursor-word/eax: int <- copy w
+  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
+  check-screen-row screen, 0/y,                                   "ddd ccc bbb aaa  ", "F - test-render-words-in-reverse/2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "             |   ", "F - test-render-words-in-reverse/2 cursor"
+  #
+  cursor-left w
+  var cursor-word/eax: int <- copy w
+  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
+  check-screen-row screen, 0/y,                                   "ddd ccc bbb aaa  ", "F - test-render-words-in-reverse/3"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "            |    ", "F - test-render-words-in-reverse/3 cursor"
+  # further moves left within the word change nothing
+  cursor-left w
+  var cursor-word/eax: int <- copy w
+  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
+  check-screen-row screen, 0/y,                                   "ddd ccc bbb aaa  ", "F - test-render-words-in-reverse/3"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "            |    ", "F - test-render-words-in-reverse/3 cursor"
+  # - switch to next word
+  var w2-ah/eax: (addr handle word) <- get w, next
+  var _w/eax: (addr word) <- lookup *w2-ah
+  var w/ecx: (addr word) <- copy _w
+  var cursor-word/eax: int <- copy w
+  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
+  check-screen-row screen, 0/y,                                   "ddd ccc bbb  aaa  ", "F - test-render-words-in-reverse/4"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "           |      ", "F - test-render-words-in-reverse/4 cursor"
+  # now speed up a little
+  cursor-left w
+  cursor-left w
+  var cursor-word/eax: int <- copy w
+  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
+  check-screen-row screen, 0/y,                                   "ddd ccc bbb aaa  ", "F - test-render-words-in-reverse/5"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "         |       ", "F - test-render-words-in-reverse/5 cursor"
+  #
+  var w2-ah/eax: (addr handle word) <- get w, next
+  var _w/eax: (addr word) <- lookup *w2-ah
+  var w/ecx: (addr word) <- copy _w
+  cursor-left w
+  var cursor-word/eax: int <- copy w
+  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
+  check-screen-row screen, 0/y,                                   "ddd ccc bbb aaa  ", "F - test-render-words-in-reverse/6"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "      |          ", "F - test-render-words-in-reverse/6 cursor"
+}
+
+# Gotcha with some word operations: ensure dest-ah isn't in the middle of some
+# existing chain of words. There are two pointers to patch, and you'll forget
+# to do the other one.
+fn copy-words _src-ah: (addr handle word), _dest-ah: (addr handle word) {
+  var src-ah/eax: (addr handle word) <- copy _src-ah
+  var src-a/eax: (addr word) <- lookup *src-ah
+  compare src-a, 0
+  break-if-=
+  # copy
+  var dest-ah/edi: (addr handle word) <- copy _dest-ah
+  copy-word src-a, dest-ah
+  # recurse
+  var rest: (handle word)
+  var rest-ah/ecx: (addr handle word) <- address rest
+  var next-src-ah/esi: (addr handle word) <- get src-a, next
+  copy-words next-src-ah, rest-ah
+  chain-words dest-ah, rest-ah
+}
+
+fn copy-words-in-reverse _src-ah: (addr handle word), _dest-ah: (addr handle word) {
+  var src-ah/eax: (addr handle word) <- copy _src-ah
+  var _src-a/eax: (addr word) <- lookup *src-ah
+  var src-a/esi: (addr word) <- copy _src-a
+  compare src-a, 0
+  break-if-=
+  # recurse
+  var next-src-ah/ecx: (addr handle word) <- get src-a, next
+  var dest-ah/edi: (addr handle word) <- copy _dest-ah
+  copy-words-in-reverse next-src-ah, dest-ah
+  #
+  copy-word-at-end src-a, dest-ah
+}
+
+fn copy-word-at-end src: (addr word), _dest-ah: (addr handle word) {
+  var dest-ah/edi: (addr handle word) <- copy _dest-ah
+  # if dest is null, copy and return
+  var dest-a/eax: (addr word) <- lookup *dest-ah
+  compare dest-a, 0
+  {
+    break-if-!=
+    copy-word src, dest-ah
+    return
+  }
+  # copy current word
+  var new: (handle word)
+  var new-ah/ecx: (addr handle word) <- address new
+  copy-word src, new-ah
+  # append it at the end
+  var curr-ah/edi: (addr handle word) <- copy dest-ah
+  {
+    var curr-a/eax: (addr word) <- lookup *curr-ah  # curr-a guaranteed not to be null
+    var next-ah/ecx: (addr handle word) <- get curr-a, next
+    var next-a/eax: (addr word) <- lookup *next-ah
+    compare next-a, 0
+    break-if-=
+    curr-ah <- copy next-ah
+    loop
+  }
+  chain-words curr-ah, new-ah
+}
+
+fn append-word-at-end-with _dest-ah: (addr handle word), s: (addr array byte) {
+  var dest-ah/edi: (addr handle word) <- copy _dest-ah
+  # if dest is null, copy and return
+  var dest-a/eax: (addr word) <- lookup *dest-ah
+  compare dest-a, 0
+  {
+    break-if-!=
+    allocate-word-with dest-ah, s
+    return
+  }
+  # otherwise append at end
+  var curr-ah/edi: (addr handle word) <- copy dest-ah
+  {
+    var curr-a/eax: (addr word) <- lookup *curr-ah  # curr-a guaranteed not to be null
+    var next-ah/ecx: (addr handle word) <- get curr-a, next
+    var next-a/eax: (addr word) <- lookup *next-ah
+    compare next-a, 0
+    break-if-=
+    curr-ah <- copy next-ah
+    loop
+  }
+  append-word-with *curr-ah, s
+}
+
+fn copy-word _src-a: (addr word), _dest-ah: (addr handle word) {
+  var dest-ah/eax: (addr handle word) <- copy _dest-ah
+  allocate dest-ah
+  var _dest-a/eax: (addr word) <- lookup *dest-ah
+  var dest-a/eax: (addr word) <- copy _dest-a
+  initialize-word dest-a
+  var dest/edi: (addr handle gap-buffer) <- get dest-a, scalar-data
+  var src-a/eax: (addr word) <- copy _src-a
+  var src/eax: (addr handle gap-buffer) <- get src-a, scalar-data
+  copy-gap-buffer src, dest
+}
+
+# one implication of handles: append must take a handle
+fn append-word _self-ah: (addr handle word) {
+  var saved-self-storage: (handle word)
+  var saved-self/eax: (addr handle word) <- address saved-self-storage
+  copy-object _self-ah, saved-self
+#?   {
+#?     print-string 0, "self-ah is "
+#?     var foo/eax: int <- copy _self-ah
+#?     print-int32-hex 0, foo
+#?     print-string 0, "\n"
+#?   }
+  var self-ah/esi: (addr handle word) <- copy _self-ah
+  var _self/eax: (addr word) <- lookup *self-ah
+  var self/ebx: (addr word) <- copy _self
+#?   {
+#?     print-string 0, "0: self is "
+#?     var self-ah/eax: (addr handle word) <- copy _self-ah
+#?     var self/eax: (addr word) <- lookup *self-ah
+#?     var foo/eax: int <- copy self
+#?     print-int32-hex 0, foo
+#?     print-string 0, "\n"
+#?   }
+  # allocate new handle
+  var new: (handle word)
+  var new-ah/ecx: (addr handle word) <- address new
+  allocate new-ah
+  var new-addr/eax: (addr word) <- lookup new
+  initialize-word new-addr
+#?   {
+#?     print-string 0, "new is "
+#?     var foo/eax: int <- copy new-addr
+#?     print-int32-hex 0, foo
+#?     print-string 0, "\n"
+#?   }
+  # new->next = self->next
+  var src/esi: (addr handle word) <- get self, next
+#?   {
+#?     print-string 0, "src is "
+#?     var foo/eax: int <- copy src
+#?     print-int32-hex 0, foo
+#?     print-string 0, "\n"
+#?   }
+  var dest/edi: (addr handle word) <- get new-addr, next
+  copy-object src, dest
+  # new->next->prev = new
+  {
+    var next-addr/eax: (addr word) <- lookup *src
+    compare next-addr, 0
+    break-if-=
+#?     {
+#?       print-string 0, "next-addr is "
+#?       var foo/eax: int <- copy next-addr
+#?       print-int32-hex 0, foo
+#?       print-string 0, "\n"
+#?     }
+    dest <- get next-addr, prev
+#? #?     {
+#? #?       print-string 0, "self-ah is "
+#? #?       var foo/eax: int <- copy _self-ah
+#? #?       print-int32-hex 0, foo
+#? #?       print-string 0, "\n"
+#? #?       print-string 0, "2: self is "
+#? #?       var self-ah/eax: (addr handle word) <- copy _self-ah
+#? #?       var self/eax: (addr word) <- lookup *self-ah
+#? #?       var foo/eax: int <- copy self
+#? #?       print-int32-hex 0, foo
+#? #?       print-string 0, "\n"
+#? #?     }
+#?     {
+#?       print-string 0, "copying new to "
+#?       var foo/eax: int <- copy dest
+#?       print-int32-hex 0, foo
+#?       print-string 0, "\n"
+#?     }
+    copy-object new-ah, dest
+#?     {
+#?       print-string 0, "4: self is "
+#?       var self-ah/eax: (addr handle word) <- copy _self-ah
+#?       var self/eax: (addr word) <- lookup *self-ah
+#?       var foo/eax: int <- copy self
+#?       print-int32-hex 0, foo
+#?       print-string 0, "\n"
+#?     }
+  }
+  # new->prev = saved-self
+  dest <- get new-addr, prev
+#?   {
+#?     print-string 0, "copying "
+#?     var self-ah/esi: (addr handle word) <- copy _self-ah
+#?     var self/eax: (addr word) <- lookup *self-ah
+#?     var foo/eax: int <- copy self
+#?     print-int32-hex 0, foo
+#?     print-string 0, " to "
+#?     foo <- copy dest
+#?     print-int32-hex 0, foo
+#?     print-string 0, "\n"
+#?   }
+  var saved-self-ah/eax: (addr handle word) <- address saved-self-storage
+  copy-object saved-self-ah, dest
+  # self->next = new
+  dest <- get self, next
+  copy-object new-ah, dest
+}
+
+fn chain-words _self-ah: (addr handle word), _next: (addr handle word) {
+  var self-ah/esi: (addr handle word) <- copy _self-ah
+  var _self/eax: (addr word) <- lookup *self-ah
+  var self/ecx: (addr word) <- copy _self
+  var dest/edx: (addr handle word) <- get self, next
+  var next-ah/edi: (addr handle word) <- copy _next
+  copy-object next-ah, dest
+  var next/eax: (addr word) <- lookup *next-ah
+  compare next, 0
+  break-if-=
+  dest <- get next, prev
+  copy-object self-ah, dest
+}
+
+fn emit-word _self: (addr word), out: (addr stream byte) {
+  var self/esi: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  emit-gap-buffer data, out
+}
+
+fn word-is-decimal-integer? _self: (addr word) -> _/eax: boolean {
+  var self/eax: (addr word) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  var result/eax: boolean <- gap-buffer-is-decimal-integer? data
+  return result
+}
+
+fn word-exists? haystack: (addr word), needle: (addr word) -> _/eax: boolean {
+  # base case
+  compare haystack, 0
+  {
+    break-if-!=
+    return 0/false
+  }
+  # check current word
+  var found?/eax: boolean <- words-equal? haystack, needle
+  compare found?, 0/false
+  {
+    break-if-=
+    return 1/true
+  }
+  # recurse
+  var curr/eax: (addr word) <- copy haystack
+  var next-ah/eax: (addr handle word) <- get curr, next
+  var next/eax: (addr word) <- lookup *next-ah
+  var result/eax: boolean <- word-exists? next, needle
+  return result
+}
+
+fn test-word-exists? {
+  var needle-storage: word
+  var needle/esi: (addr word) <- address needle-storage
+  initialize-word-with needle, "abc"
+  var w-storage: (handle word)
+  var w-ah/edi: (addr handle word) <- address w-storage
+  allocate w-ah
+  var _w/eax: (addr word) <- lookup *w-ah
+  var w/ecx: (addr word) <- copy _w
+  initialize-word-with w, "aaa"
+  #
+  var result/eax: boolean <- word-exists? w, w
+  check result, "F - test-word-exists? reflexive"
+  result <- word-exists? w, needle
+  check-not result, "F - test-word-exists? 1"
+  append-word-at-end-with w-ah, "bbb"
+  result <- word-exists? w, needle
+  check-not result, "F - test-word-exists? 2"
+  append-word-at-end-with w-ah, "abc"
+  result <- word-exists? w, needle
+  check result, "F - test-word-exists? 3"
+  append-word-at-end-with w-ah, "ddd"
+  result <- word-exists? w, needle
+  check result, "F - test-word-exists? 4"
+}
+
+fn word-list-length words: (addr handle word) -> _/eax: int {
+  var curr-ah/esi: (addr handle word) <- copy words
+  var result/edi: int <- copy 0
+  {
+    var curr/eax: (addr word) <- lookup *curr-ah
+    compare curr, 0
+    break-if-=
+    {
+      var word-len/eax: int <- word-length curr
+      result <- add word-len
+      result <- add 1/inter-word-margin
+    }
+    curr-ah <- get curr, next
+    loop
+  }
+  return result
+}
+
+# out-ah already has a word allocated and initialized
+fn parse-words in: (addr array byte), out-ah: (addr handle word) {
+  var in-stream: (stream byte 0x100)
+  var in-stream-a/esi: (addr stream byte) <- address in-stream
+  write in-stream-a, in
+  var cursor-word-ah/ebx: (addr handle word) <- copy out-ah
+  $parse-words:loop: {
+    var done?/eax: boolean <- stream-empty? in-stream-a
+    compare done?, 0/false
+    break-if-!=
+    var _g/eax: grapheme <- read-grapheme in-stream-a
+    var g/ecx: grapheme <- copy _g
+    # if not space, insert
+    compare g, 0x20/space
+    {
+      break-if-=
+      var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
+      add-grapheme-to-word cursor-word, g
+      loop $parse-words:loop
+    }
+    # otherwise insert word after and move cursor to it
+    append-word cursor-word-ah
+    var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
+    cursor-to-start cursor-word  # reset cursor in each function
+    cursor-word-ah <- get cursor-word, next
+    loop
+  }
+}