about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2021-02-11 20:59:39 -0800
committerKartik K. Agaram <vc@akkartik.com>2021-02-11 20:59:39 -0800
commitead6c01fa4f9291ecbac9e6a4567be87cb0e4a8b (patch)
treeacd3473e6c736ba8813e2c820904ddadf864d7cd
parentd1bcd1421f7a0e87105499a738bae3b3a5c92de9 (diff)
downloadmu-ead6c01fa4f9291ecbac9e6a4567be87cb0e4a8b.tar.gz
7724 - baremetal: rendering array values
-rw-r--r--baremetal/118parse-hex-int.subx897
-rw-r--r--baremetal/301array-equal.subx432
-rw-r--r--baremetal/311decimal-int.subx206
-rw-r--r--baremetal/400.mu4
-rw-r--r--baremetal/shell/value.mu105
5 files changed, 1644 insertions, 0 deletions
diff --git a/baremetal/118parse-hex-int.subx b/baremetal/118parse-hex-int.subx
new file mode 100644
index 00000000..153def3c
--- /dev/null
+++ b/baremetal/118parse-hex-int.subx
@@ -0,0 +1,897 @@
+# some utilities for converting numbers from hex
+# lowercase letters only for now
+
+== code
+#   instruction                     effective address                                                   register    displacement    immediate
+# . op          subop               mod             rm32          base        index         scale       r32
+# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
+
+is-hex-int?:  # in: (addr slice) -> result/eax: boolean
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # . save registers
+    51/push-ecx
+    52/push-edx
+    53/push-ebx
+    # ecx = s
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   8/disp8         .                 # copy *(ebp+8) to ecx
+    # edx = s->end
+    8b/copy                         1/mod/*+disp8   1/rm32/ecx    .           .             .           2/r32/edx   4/disp8         .                 # copy *(ecx+4) to edx
+    # var curr/ecx: (addr byte) = s->start
+    8b/copy                         0/mod/indirect  1/rm32/ecx    .           .             .           1/r32/ecx   .               .                 # copy *ecx to ecx
+    # if s is empty return false
+    b8/copy-to-eax  0/imm32/false
+    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
+    73/jump-if-addr>=  $is-hex-int?:end/disp8
+    # skip past leading '-'
+    # . if (*curr == '-') ++curr
+    31/xor                          3/mod/direct    3/rm32/ebx    .           .             .           3/r32/ebx   .               .                 # clear ebx
+    8a/copy-byte                    0/mod/indirect  1/rm32/ecx    .           .             .           3/r32/BL    .               .                 # copy byte at *ecx to BL
+    81          7/subop/compare     3/mod/direct    3/rm32/ebx    .           .             .           .           .               0x2d/imm32/-      # compare ebx
+    75/jump-if-!=  $is-hex-int?:initial-0/disp8
+    # . ++curr
+    41/increment-ecx
+    # skip past leading '0x'
+$is-hex-int?:initial-0:
+    # . if (*curr != '0') jump to loop
+    31/xor                          3/mod/direct    3/rm32/ebx    .           .             .           3/r32/ebx   .               .                 # clear ebx
+    8a/copy-byte                    0/mod/indirect  1/rm32/ecx    .           .             .           3/r32/BL    .               .                 # copy byte at *ecx to BL
+    81          7/subop/compare     3/mod/direct    3/rm32/ebx    .           .             .           .           .               0x30/imm32/0      # compare ebx
+    75/jump-if-!=  $is-hex-int?:loop/disp8
+    # . ++curr
+    41/increment-ecx
+$is-hex-int?:initial-0x:
+    # . if (curr >= in->end) return true
+    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
+    73/jump-if-addr>=  $is-hex-int?:true/disp8
+    # . if (*curr != 'x') jump to loop  # the previous '0' is still valid so doesn't need to be checked again
+    31/xor                          3/mod/direct    3/rm32/ebx    .           .             .           3/r32/ebx   .               .                 # clear ebx
+    8a/copy-byte                    0/mod/indirect  1/rm32/ecx    .           .             .           3/r32/BL    .               .                 # copy byte at *ecx to BL
+    81          7/subop/compare     3/mod/direct    3/rm32/ebx    .           .             .           .           .               0x78/imm32/x      # compare ebx
+    75/jump-if-!=  $is-hex-int?:loop/disp8
+    # . ++curr
+    41/increment-ecx
+$is-hex-int?:loop:
+    # if (curr >= in->end) return true
+    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
+    73/jump-if-addr>=  $is-hex-int?:true/disp8
+    # var eax: boolean = is-hex-digit?(*curr)
+    # . . push args
+    8a/copy-byte                    0/mod/indirect  1/rm32/ecx    .           .             .           0/r32/AL    .               .                 # copy byte at *ecx to AL
+    50/push-eax
+    # . . call
+    e8/call  is-hex-digit?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # if (eax == false) return false
+    3d/compare-eax-and  0/imm32/false
+    74/jump-if-=  $is-hex-int?:end/disp8
+    # ++curr
+    41/increment-ecx
+    # loop
+    eb/jump  $is-hex-int?:loop/disp8
+$is-hex-int?:true:
+    # return true
+    b8/copy-to-eax  1/imm32/true
+$is-hex-int?:end:
+    # . restore registers
+    5b/pop-to-ebx
+    5a/pop-to-edx
+    59/pop-to-ecx
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-is-hex-int:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # (eax..ecx) = "34"
+    b8/copy-to-eax  "34"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy *eax to ecx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  1/index/ecx   .           1/r32/ecx   4/disp8         .                 # copy eax+ecx+4 to ecx
+    05/add-to-eax  4/imm32
+    # var slice/ecx: slice = {eax, ecx}
+    51/push-ecx
+    50/push-eax
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = is-hex-int?(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  is-hex-int?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 1, msg)
+    # . . push args
+    68/push  "F - test-is-hex-int"/imm32
+    68/push  1/imm32/true
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-is-hex-int-handles-letters:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # (eax..ecx) = "34a"
+    b8/copy-to-eax  "34a"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy *eax to ecx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  1/index/ecx   .           1/r32/ecx   4/disp8         .                 # copy eax+ecx+4 to ecx
+    05/add-to-eax  4/imm32
+    # var slice/ecx: slice = {eax, ecx}
+    51/push-ecx
+    50/push-eax
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = is-hex-int?(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  is-hex-int?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 1, msg)
+    # . . push args
+    68/push  "F - test-is-hex-int-handles-letters"/imm32
+    68/push  1/imm32/true
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-is-hex-int-with-trailing-char:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # (eax..ecx) = "34q"
+    b8/copy-to-eax  "34q"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy *eax to ecx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  1/index/ecx   .           1/r32/ecx   4/disp8         .                 # copy eax+ecx+4 to ecx
+    05/add-to-eax  4/imm32
+    # var slice/ecx: slice = {eax, ecx}
+    51/push-ecx
+    50/push-eax
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = is-hex-int?(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  is-hex-int?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 0, msg)
+    # . . push args
+    68/push  "F - test-is-hex-int-with-trailing-char"/imm32
+    68/push  0/imm32/false
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-is-hex-int-with-leading-char:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # (eax..ecx) = "q34"
+    b8/copy-to-eax  "q34"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy *eax to ecx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  1/index/ecx   .           1/r32/ecx   4/disp8         .                 # copy eax+ecx+4 to ecx
+    05/add-to-eax  4/imm32
+    # var slice/ecx: slice = {eax, ecx}
+    51/push-ecx
+    50/push-eax
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = is-hex-int?(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  is-hex-int?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 0, msg)
+    # . . push args
+    68/push  "F - test-is-hex-int-with-leading-char"/imm32
+    68/push  0/imm32/false
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-is-hex-int-empty:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # var slice/ecx: slice = ""
+    68/push  0/imm32
+    68/push  0/imm32
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = is-hex-int?(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  is-hex-int?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 0, msg)
+    # . . push args
+    68/push  "F - test-is-hex-int-empty"/imm32
+    68/push  0/imm32/false
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-is-hex-int-handles-0x-prefix:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # (eax..ecx) = "0x3a"
+    b8/copy-to-eax  "0x3a"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy *eax to ecx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  1/index/ecx   .           1/r32/ecx   4/disp8         .                 # copy eax+ecx+4 to ecx
+    05/add-to-eax  4/imm32
+    # var slice/ecx: slice = {eax, ecx}
+    51/push-ecx
+    50/push-eax
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = is-hex-int?(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  is-hex-int?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 1, msg)
+    # . . push args
+    68/push  "F - test-is-hex-int-handles-0x-prefix"/imm32
+    68/push  1/imm32/true
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-is-hex-int-handles-negative:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # (eax..ecx) = "-34a"
+    b8/copy-to-eax  "-34a"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy *eax to ecx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  1/index/ecx   .           1/r32/ecx   4/disp8         .                 # copy eax+ecx+4 to ecx
+    05/add-to-eax  4/imm32
+    # var slice/ecx: slice = {eax, ecx}
+    51/push-ecx
+    50/push-eax
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = is-hex-int?(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  is-hex-int?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 1, msg)
+    # . . push args
+    68/push  "F - test-is-hex-int-handles-negative"/imm32
+    68/push  1/imm32/true
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-is-hex-int-handles-negative-0x-prefix:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # (eax..ecx) = "-0x3a"
+    b8/copy-to-eax  "-0x3a"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy *eax to ecx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  1/index/ecx   .           1/r32/ecx   4/disp8         .                 # copy eax+ecx+4 to ecx
+    05/add-to-eax  4/imm32
+    # var slice/ecx: slice = {eax, ecx}
+    51/push-ecx
+    50/push-eax
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = is-hex-int?(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  is-hex-int?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 1, msg)
+    # . . push args
+    68/push  "F - test-is-hex-int-handles-negative-0x-prefix"/imm32
+    68/push  1/imm32/true
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+parse-hex-int:  # in: (addr array byte) -> result/eax: int
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # . save registers
+    51/push-ecx
+    52/push-edx
+    # eax = in
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   8/disp8         .                 # copy *(ebp+8) to eax
+    # var curr/ecx: (addr byte) = &in->data
+    8d/copy-address                 1/mod/*+disp8   0/rm32/eax    .           .             .           1/r32/ecx   4/disp8         .                 # copy eax+4 to ecx
+    # var max/edx: (addr byte) = &in->data[in->size]
+    # . edx = in->size
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           2/r32/edx   .               .                 # copy *eax to edx
+    # . edx = in->data + in->size
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  2/index/edx   .           2/r32/edx   4/disp8         .                 # copy eax+edx+4 to edx
+    # return parse-hex-int-helper(curr, max)
+    # . . push args
+    52/push-edx
+    51/push-ecx
+    # . . call
+    e8/call  parse-hex-int-helper/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+$parse-hex-int:end:
+    # . restore registers
+    5a/pop-to-edx
+    59/pop-to-ecx
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+parse-hex-int-from-slice:  # in: (addr slice) -> result/eax: int
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # . save registers
+    51/push-ecx
+    52/push-edx
+    # ecx = in
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   8/disp8         .                 # copy *(ebp+8) to ecx
+    # var max/edx: (addr byte) = in->end
+    8b/copy                         1/mod/*+disp8   1/rm32/ecx    .           .             .           2/r32/edx   4/disp8         .                 # copy *(ecx+4) to edx
+    # var curr/ecx: (addr byte) = in->start
+    8b/copy                         0/mod/indirect  1/rm32/ecx    .           .             .           1/r32/ecx   .               .                 # copy *ecx to ecx
+    # return parse-hex-int-helper(curr, max)
+    # . . push args
+    52/push-edx
+    51/push-ecx
+    # . . call
+    e8/call  parse-hex-int-helper/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+$parse-hex-int-from-slice:end:
+    # . restore registers
+    5a/pop-to-edx
+    59/pop-to-ecx
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+parse-hex-int-helper:  # start: (addr byte), end: (addr byte) -> result/eax: int
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # . save registers
+    51/push-ecx
+    52/push-edx
+    53/push-ebx
+    56/push-esi
+    # var curr/ecx: (addr byte) = start
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   8/disp8         .                 # copy *(ebp+8) to ecx
+    # edx = max
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           2/r32/edx   0xc/disp8       .                 # copy *(ebp+12) to edx
+    # var result/ebx: int = 0
+    31/xor                          3/mod/direct    3/rm32/ebx    .           .             .           3/r32/ebx   .               .                 # clear ebx
+    # var negate?/esi: boolean = false
+    31/xor                          3/mod/direct    6/rm32/esi    .           .             .           6/r32/esi   .               .                 # clear esi
+$parse-hex-int-helper:negative:
+    # if (*curr == '-') ++curr, negate = true
+    31/xor                          3/mod/direct    0/rm32/eax    .           .             .           0/r32/eax   .               .                 # clear eax
+    8a/copy-byte                    0/mod/indirect  1/rm32/ecx    .           .             .           0/r32/AL    .               .                 # copy byte at *ecx to AL
+    3d/compare-eax-and  0x2d/imm32/-
+    75/jump-if-!=  $parse-hex-int-helper:initial-0/disp8
+    # . ++curr
+    41/increment-ecx
+    # . negate = true
+    be/copy-to-esi  1/imm32/true
+$parse-hex-int-helper:initial-0:
+    # skip past leading '0x'
+    # . if (*curr != '0') jump to loop
+    8a/copy-byte                    0/mod/indirect  1/rm32/ecx    .           .             .           0/r32/AL    .               .                 # copy byte at *ecx to AL
+    3d/compare-eax-and  0x30/imm32/0
+    75/jump-if-!=  $parse-hex-int-helper:loop/disp8
+    # . ++curr
+    41/increment-ecx
+$parse-hex-int-helper:initial-0x:
+    # . if (curr >= in->end) return result
+    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
+    73/jump-if-addr>=  $parse-hex-int-helper:end/disp8
+    # . if (*curr != 'x') jump to loop  # the previous '0' is still valid so doesn't need to be checked again
+    31/xor                          3/mod/direct    0/rm32/eax    .           .             .           0/r32/eax   .               .                 # clear eax
+    8a/copy-byte                    0/mod/indirect  1/rm32/ecx    .           .             .           0/r32/AL    .               .                 # copy byte at *ecx to AL
+    3d/compare-eax-and  0x78/imm32/x
+    75/jump-if-!=  $parse-hex-int-helper:loop/disp8
+    # . ++curr
+    41/increment-ecx
+$parse-hex-int-helper:loop:
+    # if (curr >= in->end) break
+    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
+    73/jump-if-addr>=  $parse-hex-int-helper:negate/disp8
+    # var eax: int = from-hex-char(*curr)
+    # . . copy arg to eax
+    8a/copy-byte                    0/mod/indirect  1/rm32/ecx    .           .             .           0/r32/AL    .               .                 # copy byte at *ecx to AL
+    # . . call
+    e8/call  from-hex-char/disp32
+    # result = result * 16 + eax
+    c1/shift    4/subop/left        3/mod/direct    3/rm32/ebx    .           .             .           .           .               4/imm8            # shift ebx left by 4 bits
+    01/add                          3/mod/direct    3/rm32/ebx    .           .             .           0/r32/eax   .               .                 # add eax to ebx
+    # ++curr
+    41/increment-ecx
+    # loop
+    eb/jump  $parse-hex-int-helper:loop/disp8
+$parse-hex-int-helper:negate:
+    # if (negate?) result = -result
+    81          7/subop/compare     3/mod/direct    6/rm32/esi    .           .             .           .           .               0/imm32/false     # compare esi
+    74/jump-if-=  $parse-hex-int-helper:end/disp8
+    f7          3/subop/negate      3/mod/direct    3/rm32/ebx    .           .             .           .           .               .                 # negate ebx
+$parse-hex-int-helper:end:
+    # return result
+    89/copy                         3/mod/direct    0/rm32/eax    .           .             .           3/r32/ebx   .               .                 # copy ebx to eax
+    # . restore registers
+    5e/pop-to-esi
+    5b/pop-to-ebx
+    5a/pop-to-edx
+    59/pop-to-ecx
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-hex-int-from-slice-single-digit:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # (eax..ecx) = "a"
+    b8/copy-to-eax  "a"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy *eax to ecx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  1/index/ecx   .           1/r32/ecx   4/disp8         .                 # copy eax+ecx+4 to ecx
+    05/add-to-eax  4/imm32
+    # var slice/ecx: slice = {eax, ecx}
+    51/push-ecx
+    50/push-eax
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = parse-hex-int-from-slice(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  parse-hex-int-from-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 0xa, msg)
+    # . . push args
+    68/push  "F - test-parse-hex-int-from-slice-single-digit"/imm32
+    68/push  0xa/imm32
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-hex-int-from-slice-multi-digit:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # (eax..ecx) = "34a"
+    b8/copy-to-eax  "34a"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy *eax to ecx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  1/index/ecx   .           1/r32/ecx   4/disp8         .                 # copy eax+ecx+4 to ecx
+    05/add-to-eax  4/imm32
+    # var slice/ecx: slice = {eax, ecx}
+    51/push-ecx
+    50/push-eax
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = parse-hex-int-from-slice(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  parse-hex-int-from-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 0x34a, msg)
+    # . . push args
+    68/push  "F - test-parse-hex-int-from-slice-multi-digit"/imm32
+    68/push  0x34a/imm32
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-hex-int-from-slice-0x-prefix:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # (eax..ecx) = "0x34"
+    b8/copy-to-eax  "0x34"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy *eax to ecx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  1/index/ecx   .           1/r32/ecx   4/disp8         .                 # copy eax+ecx+4 to ecx
+    05/add-to-eax  4/imm32
+    # var slice/ecx: slice = {eax, ecx}
+    51/push-ecx
+    50/push-eax
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = parse-hex-int-from-slice(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  parse-hex-int-from-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 0x34, msg)
+    # . . push args
+    68/push  "F - test-parse-hex-int-from-slice-0x-prefix"/imm32
+    68/push  0x34/imm32
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-hex-int-from-slice-zero:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # (eax..ecx) = "0"
+    b8/copy-to-eax  "0"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy *eax to ecx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  1/index/ecx   .           1/r32/ecx   4/disp8         .                 # copy eax+ecx+4 to ecx
+    05/add-to-eax  4/imm32
+    # var slice/ecx: slice = {eax, ecx}
+    51/push-ecx
+    50/push-eax
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = parse-hex-int-from-slice(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  parse-hex-int-from-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 0, msg)
+    # . . push args
+    68/push  "F - test-parse-hex-int-from-slice-zero"/imm32
+    68/push  0/imm32
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-hex-int-from-slice-0-prefix:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # (eax..ecx) = "03"
+    b8/copy-to-eax  "03"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy *eax to ecx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  1/index/ecx   .           1/r32/ecx   4/disp8         .                 # copy eax+ecx+4 to ecx
+    05/add-to-eax  4/imm32
+    # var slice/ecx: slice = {eax, ecx}
+    51/push-ecx
+    50/push-eax
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = parse-hex-int-from-slice(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  parse-hex-int-from-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 0x3, msg)
+    # . . push args
+    68/push  "F - test-parse-hex-int-from-slice-0-prefix"/imm32
+    68/push  0x3/imm32
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-hex-int-from-slice-negative:
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # (eax..ecx) = "-03"
+    b8/copy-to-eax  "-03"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # copy *eax to ecx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  1/index/ecx   .           1/r32/ecx   4/disp8         .                 # copy eax+ecx+4 to ecx
+    05/add-to-eax  4/imm32
+    # var slice/ecx: slice = {eax, ecx}
+    51/push-ecx
+    50/push-eax
+    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
+    # eax = parse-hex-int-from-slice(slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call  parse-hex-int-from-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, -3, msg)
+    # . . push args
+    68/push  "F - test-parse-hex-int-from-slice-negative"/imm32
+    68/push  -3/imm32
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+is-hex-digit?:  # c: byte -> result/eax: boolean
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # . save registers
+    51/push-ecx
+    # ecx = c
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   8/disp8         .                 # copy *(ebp+8) to ecx
+    # return false if c < '0'
+    81          7/subop/compare     3/mod/direct    1/rm32/ecx    .           .             .           .           .               0x30/imm32        # compare ecx
+    7c/jump-if-<  $is-hex-digit?:false/disp8
+    # return true if c <= '9'
+    81          7/subop/compare     3/mod/direct    1/rm32/ecx    .           .             .           .           .               0x39/imm32        # compare ecx
+    7e/jump-if-<=  $is-hex-digit?:true/disp8
+    # drop case
+    25/and-eax-with 0x5f/imm32
+    # return false if c > 'f'
+    81          7/subop/compare     3/mod/direct    1/rm32/ecx    .           .             .           .           .               0x66/imm32        # compare ecx
+    7f/jump-if->  $is-hex-digit?:false/disp8
+    # return true if c >= 'a'
+    81          7/subop/compare     3/mod/direct    1/rm32/ecx    .           .             .           .           .               0x61/imm32        # compare ecx
+    7d/jump-if->=  $is-hex-digit?:true/disp8
+    # otherwise return false
+$is-hex-digit?:false:
+    b8/copy-to-eax  0/imm32/false
+    eb/jump $is-hex-digit?:end/disp8
+$is-hex-digit?:true:
+    b8/copy-to-eax  1/imm32/true
+$is-hex-digit?:end:
+    # . restore registers
+    59/pop-to-ecx
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-hex-below-0:
+    # eax = is-hex-digit?(0x2f)
+    # . . push args
+    68/push  0x2f/imm32
+    # . . call
+    e8/call  is-hex-digit?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 0, msg)
+    # . . push args
+    68/push  "F - test-hex-below-0"/imm32
+    68/push  0/imm32/false
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    c3/return
+
+test-hex-0-to-9:
+    # eax = is-hex-digit?(0x30)
+    # . . push args
+    68/push  0x30/imm32
+    # . . call
+    e8/call  is-hex-digit?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 1, msg)
+    # . . push args
+    68/push  "F - test-hex-at-0"/imm32
+    68/push  1/imm32/true
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # eax = is-hex-digit?(0x39)
+    # . . push args
+    68/push  0x39/imm32
+    # . . call
+    e8/call  is-hex-digit?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 1, msg)
+    # . . push args
+    68/push  "F - test-hex-at-9"/imm32
+    68/push  1/imm32/true
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    c3/return
+
+test-hex-above-9-to-a:
+    # eax = is-hex-digit?(0x3a)
+    # . . push args
+    68/push  0x3a/imm32
+    # . . call
+    e8/call  is-hex-digit?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 0, msg)
+    # . . push args
+    68/push  "F - test-hex-above-9-to-a"/imm32
+    68/push  0/imm32/false
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    c3/return
+
+test-hex-a-to-f:
+    # eax = is-hex-digit?(0x61)
+    # . . push args
+    68/push  0x61/imm32
+    # . . call
+    e8/call  is-hex-digit?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 1, msg)
+    # . . push args
+    68/push  "F - test-hex-at-a"/imm32
+    68/push  1/imm32/true
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # eax = is-hex-digit?(0x66)
+    # . . push args
+    68/push  0x66/imm32
+    # . . call
+    e8/call  is-hex-digit?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 1, msg)
+    # . . push args
+    68/push  "F - test-hex-at-f"/imm32
+    68/push  1/imm32/true
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    c3/return
+
+test-hex-above-f:
+    # eax = is-hex-digit?(0x67)
+    # . . push args
+    68/push  0x67/imm32
+    # . . call
+    e8/call  is-hex-digit?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 0, msg)
+    # . . push args
+    68/push  "F - test-hex-above-f"/imm32
+    68/push  0/imm32/false
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    c3/return
+
+from-hex-char:  # in/eax: byte -> out/eax: nibble
+$from-hex-char:check0:
+    # if (eax < '0') goto abort
+    3d/compare-eax-with  0x30/imm32/0
+    7c/jump-if-<  $from-hex-char:abort/disp8
+$from-hex-char:check1:
+    # if (eax > 'f') goto abort
+    3d/compare-eax-with  0x66/imm32/f
+    7f/jump-if->  $from-hex-char:abort/disp8
+$from-hex-char:check2:
+    # if (eax > '9') goto next check
+    3d/compare-eax-with  0x39/imm32/9
+    7f/jump-if->  $from-hex-char:check3/disp8
+$from-hex-char:digit:
+    # return eax - '0'
+    2d/subtract-from-eax  0x30/imm32/0
+    c3/return
+$from-hex-char:check3:
+    # if (eax < 'a') goto abort
+    3d/compare-eax-with  0x61/imm32/a
+    7c/jump-if-<  $from-hex-char:abort/disp8
+$from-hex-char:letter:
+    # return eax - ('a'-10)
+    2d/subtract-from-eax  0x57/imm32/a-10
+    c3/return
+
+$from-hex-char:abort:
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 "invalid hex char" 3 0)  # 3=cyan
+    {
+      eb/jump loop/disp8
+    }
+    # never gets here
+
+# . . vim:nowrap:textwidth=0
diff --git a/baremetal/301array-equal.subx b/baremetal/301array-equal.subx
new file mode 100644
index 00000000..fd46ccb6
--- /dev/null
+++ b/baremetal/301array-equal.subx
@@ -0,0 +1,432 @@
+# Comparing arrays of numbers.
+
+== code
+
+array-equal?:  # a: (addr array int), b: (addr array int) -> result/eax: boolean
+    # pseudocode:
+    #   asize = a->size
+    #   if (asize != b->size) return false
+    #   i = 0
+    #   curra = a->data
+    #   currb = b->data
+    #   while i < asize
+    #     i1 = *curra
+    #     i2 = *currb
+    #     if (c1 != c2) return false
+    #     i+=4, curra+=4, currb+=4
+    #   return true
+    #
+    # registers:
+    #   i: ecx
+    #   asize: edx
+    #   curra: esi
+    #   currb: edi
+    #   i1: eax
+    #   i2: ebx
+    #
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # . save registers
+    51/push-ecx
+    52/push-edx
+    53/push-ebx
+    56/push-esi
+    57/push-edi
+    # esi = a
+    8b/-> *(ebp+8) 6/r32/esi
+    # edi = b
+    8b/-> *(ebp+0xc) 7/r32/edi
+    # var asize/edx: int = a->size
+    8b/-> *esi 2/r32/edx
+$array-equal?:sizes:
+    # if (asize != b->size) return false
+    39/compare *edi 2/r32/edx
+    75/jump-if-!= $array-equal?:false/disp8
+    # var curra/esi: (addr byte) = a->data
+    81 0/subop/add %esi 4/imm32
+    # var currb/edi: (addr byte) = b->data
+    81 0/subop/add %edi 4/imm32
+    # var i/ecx: int = 0
+    31/xor-with %ecx 1/r32/ecx
+    # var vala/eax: int
+    # var valb/ebx: int
+$array-equal?:loop:
+    # if (i >= asize) return true
+    39/compare %ecx 2/r32/edx
+    7d/jump-if->= $array-equal?:true/disp8
+    # var vala/eax: int = *curra
+    8b/-> *esi 0/r32/eax
+    # var valb/ebx: int = *currb
+    8b/-> *edi 3/r32/ebx
+    # if (vala != valb) return false
+    39/compare %eax 3/r32/ebx
+    75/jump-if-!= $array-equal?:false/disp8
+    # i += 4
+    81 0/subop/add %ecx 4/imm32
+    # currs += 4
+    81 0/subop/add %esi 4/imm32
+    # currb += 4
+    81 0/subop/add %edi 4/imm32
+    eb/jump $array-equal?:loop/disp8
+$array-equal?:true:
+    b8/copy-to-eax 1/imm32
+    eb/jump $array-equal?:end/disp8
+$array-equal?:false:
+    b8/copy-to-eax 0/imm32
+$array-equal?:end:
+    # . restore registers
+    5f/pop-to-edi
+    5e/pop-to-esi
+    5b/pop-to-ebx
+    5a/pop-to-edx
+    59/pop-to-ecx
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-compare-empty-with-empty-array:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # var ecx: (array _) = []
+    68/push 0/imm32/size
+    89/<- %ecx 4/r32/esp
+    # var edx: (array _) = []
+    68/push 0/imm32/size
+    89/<- %edx 4/r32/esp
+    #
+    (array-equal? %ecx %edx)  # => eax
+    (check-ints-equal %eax 1 "F - test-compare-empty-with-empty-array")
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-compare-empty-with-non-empty-array:  # also checks size-mismatch code path
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # var ecx: (array int) = [1]
+    68/push 1/imm32
+    68/push 4/imm32/size
+    89/<- %ecx 4/r32/esp
+    # var edx: (array int) = []
+    68/push 0/imm32/size
+    89/<- %edx 4/r32/esp
+    #
+    (array-equal? %ecx %edx)  # => eax
+    (check-ints-equal %eax 0 "F - test-compare-empty-with-non-empty-array")
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-compare-equal-arrays:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # var ecx: (array int) = [1, 2, 3]
+    68/push 3/imm32
+    68/push 2/imm32
+    68/push 1/imm32
+    68/push 0xc/imm32/size
+    89/<- %ecx 4/r32/esp
+    # var edx: (array int) = [1, 2, 3]
+    68/push 3/imm32
+    68/push 2/imm32
+    68/push 1/imm32
+    68/push 0xc/imm32/size
+    89/<- %edx 4/r32/esp
+    #
+    (array-equal? %ecx %edx)  # => eax
+    (check-ints-equal %eax 1 "F - test-compare-equal-arrays")
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-compare-inequal-arrays-equal-sizes:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # var ecx: (array int) = [1, 4, 3]
+    68/push 3/imm32
+    68/push 4/imm32
+    68/push 1/imm32
+    68/push 0xc/imm32/size
+    89/<- %ecx 4/r32/esp
+    # var edx: (array int) = [1, 2, 3]
+    68/push 3/imm32
+    68/push 2/imm32
+    68/push 1/imm32
+    68/push 0xc/imm32/size
+    89/<- %edx 4/r32/esp
+    #
+    (array-equal? %ecx %edx)  # => eax
+    (check-ints-equal %eax 0 "F - test-compare-inequal-arrays-equal-sizes")
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+_parse-array-of-ints:  # ad: (addr allocation-descriptor), s: (addr array byte), out: (addr handle array int)
+    # pseudocode
+    #   end = &s->data[s->size]
+    #   curr = s->data
+    #   size = 0
+    #   while true
+    #     if (curr >= end) break
+    #     curr = skip-chars-matching-in-slice(curr, end, ' ')
+    #     if (curr >= end) break
+    #     curr = skip-chars-not-matching-in-slice(curr, end, ' ')
+    #     ++size
+    #   allocate-array(ad, size*4, out)
+    #   var slice: slice = {s->data, 0}
+    #   curr = lookup(out)->data
+    #   while true
+    #     if (slice->start >= end) break
+    #     slice->start = skip-chars-matching-in-slice(slice->start, end, ' ')
+    #     if (slice->start >= end) break
+    #     slice->end = skip-chars-not-matching-in-slice(slice->start, end, ' ')
+    #     *curr = parse-hex-int-from-slice(slice)
+    #     curr += 4
+    #     slice->start = slice->end
+    #   return result
+    #
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # . save registers
+    50/push-eax
+    51/push-ecx
+    52/push-edx
+    53/push-ebx
+    56/push-esi
+    57/push-edi
+    # esi = s
+    8b/-> *(ebp+0xc) 6/r32/esi
+    # var curr/ecx: (addr byte) = s->data
+    8d/copy-address *(esi+4) 1/r32/ecx
+    # var end/edx: (addr byte) = &s->data[s->size]
+    # . edx = s->size
+    8b/-> *esi 2/r32/edx
+    # . edx += curr
+    01/add-to %edx 1/r32/ecx
+    # var size/ebx: int = 0
+    31/xor-with %ebx 3/r32/ebx
+$_parse-array-of-ints:loop1:
+    # if (curr >= end) break
+    39/compare %ecx 2/r32/edx
+    73/jump-if-addr>= $_parse-array-of-ints:break1/disp8
+    # curr = skip-chars-matching-in-slice(curr, end, ' ')
+    (skip-chars-matching-in-slice %ecx %edx 0x20)  # => eax
+    89/<- %ecx 0/r32/eax
+    # if (curr >= end) break
+    39/compare %ecx 2/r32/edx
+    73/jump-if-addr>= $_parse-array-of-ints:break1/disp8
+    # curr = skip-chars-not-matching-in-slice(curr, end, ' ')
+    (skip-chars-not-matching-in-slice %ecx %edx 0x20)  # => eax
+    89/<- %ecx 0/r32/eax
+    # size += 4
+    81 0/subop/add %ebx 4/imm32
+    eb/jump $_parse-array-of-ints:loop1/disp8
+$_parse-array-of-ints:break1:
+    (allocate-array *(ebp+8) %ebx *(ebp+0x10))
+$_parse-array-of-ints:pass2:
+    # var slice/edi: slice = {s->data, 0}
+    68/push 0/imm32/end
+    8d/copy-address *(esi+4) 7/r32/edi
+    57/push-edi
+    89/<- %edi 4/r32/esp
+    # curr = lookup(out)->data
+    8b/-> *(ebp+0x10) 0/r32/eax
+    (lookup *eax *(eax+4))  # => eax
+    8d/copy-address *(eax+4) 1/r32/ecx
+$_parse-array-of-ints:loop2:
+    # if (slice->start >= end) break
+    39/compare *edi 2/r32/edx
+    73/jump-if-addr>= $_parse-array-of-ints:end/disp8
+    # slice->start = skip-chars-matching-in-slice(slice->start, end, ' ')
+    (skip-chars-matching-in-slice *edi %edx 0x20)  # => eax
+    89/<- *edi 0/r32/eax
+    # if (slice->start >= end) break
+    39/compare *edi 2/r32/edx
+    73/jump-if-addr>= $_parse-array-of-ints:end/disp8
+    # slice->end = skip-chars-not-matching-in-slice(slice->start, end, ' ')
+    (skip-chars-not-matching-in-slice *edi %edx 0x20)  # => eax
+    89/<- *(edi+4) 0/r32/eax
+    # *curr = parse-hex-int-from-slice(slice)
+    (parse-hex-int-from-slice %edi)
+    89/<- *ecx 0/r32/eax
+    # curr += 4
+    81 0/subop/add %ecx 4/imm32
+    # slice->start = slice->end
+    8b/-> *(edi+4) 0/r32/eax
+    89/<- *edi 0/r32/eax
+    eb/jump $_parse-array-of-ints:loop2/disp8
+$_parse-array-of-ints:end:
+    # . reclaim locals
+    81 0/subop/add %esp 8/imm32
+    # . restore registers
+    5f/pop-to-edi
+    5e/pop-to-esi
+    5b/pop-to-ebx
+    5a/pop-to-edx
+    59/pop-to-ecx
+    58/pop-to-eax
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-array-of-ints:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # var h/esi: (handle array int)
+    68/push 0/imm32
+    68/push 0/imm32
+    89/<- %esi 4/r32/esp
+    # var ecx: (array int) = [1, 2, 3]
+    68/push 3/imm32
+    68/push 2/imm32
+    68/push 1/imm32
+    68/push 0xc/imm32/size
+    89/<- %ecx 4/r32/esp
+    #
+    (_parse-array-of-ints Heap "1 2 3" %esi)
+    (lookup *esi *(esi+4))  # => eax
+    (array-equal? %ecx %eax)  # => eax
+    (check-ints-equal %eax 1 "F - test-parse-array-of-ints")
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-array-of-ints-empty:
+    # - empty string = empty array
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # var h/esi: handle
+    68/push 0/imm32
+    68/push 0/imm32
+    89/<- %esi 4/r32/esp
+    #
+    (_parse-array-of-ints Heap "" %esi)
+    (lookup *esi *(esi+4))  # => eax
+    (check-ints-equal *eax 0 "F - test-parse-array-of-ints-empty")
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-array-of-ints-just-whitespace:
+    # - just whitespace = empty array
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # var h/esi: handle
+    68/push 0/imm32
+    68/push 0/imm32
+    89/<- %esi 4/r32/esp
+    #
+    (_parse-array-of-ints Heap Space %esi)
+    (lookup *esi *(esi+4))  # => eax
+    (check-ints-equal *eax 0 "F - test-parse-array-of-ints-just-whitespace")
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-array-of-ints-extra-whitespace:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # var h/esi: handle
+    68/push 0/imm32
+    68/push 0/imm32
+    89/<- %esi 4/r32/esp
+    # var ecx: (array int) = [1, 2, 3]
+    68/push 3/imm32
+    68/push 2/imm32
+    68/push 1/imm32
+    68/push 0xc/imm32/size
+    89/<- %ecx 4/r32/esp
+    #
+    (_parse-array-of-ints Heap " 1 2  3  " %esi)
+    (lookup *esi *(esi+4))  # => eax
+    (array-equal? %ecx %eax)  # => eax
+    (check-ints-equal %eax 1 "F - test-parse-array-of-ints-extra-whitespace")
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+parse-array-of-ints:  # s: (addr array byte), out: (addr handle array int)
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    #
+    (_parse-array-of-ints Heap *(ebp+8) *(ebp+0xc))
+$parse-array-of-ints:end:
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+# helper for later tests
+# compare an array with a string representation of an array literal
+check-array-equal:  # a: (addr array int), expected: (addr string), msg: (addr string)
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # . save registers
+    50/push-eax
+    56/push-esi
+    # var h/esi: handle
+    68/push 0/imm32
+    68/push 0/imm32
+    89/<- %esi 4/r32/esp
+    # var b/eax: (addr array int) = parse-array-of-ints(Heap, expected)
+    (parse-array-of-ints *(ebp+0xc) %esi)
+    (lookup *esi *(esi+4))  # => eax
+    #
+    (array-equal? *(ebp+8) %eax)
+    (check-ints-equal %eax 1 *(ebp+0x10))
+$check-array-equal:end:
+    # . restore registers
+    5e/pop-to-esi
+    58/pop-to-eax
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-check-array-equal:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # var ecx: (array int) = [1, 2, 3]
+    68/push 3/imm32
+    68/push 2/imm32
+    68/push 1/imm32
+    68/push 0xc/imm32/size
+    89/<- %ecx 4/r32/esp
+    #
+    (check-array-equal %ecx "1 2 3" "F - test-check-array-equal")
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+== data
+
+# length-prefixed string containing just a single space
+Space:  # (array byte)
+    # size: int
+    1/imm32
+    # data
+    20/space
diff --git a/baremetal/311decimal-int.subx b/baremetal/311decimal-int.subx
index 9434ce72..d0d8dde7 100644
--- a/baremetal/311decimal-int.subx
+++ b/baremetal/311decimal-int.subx
@@ -425,3 +425,209 @@ $test-decimal-size-multi-digit-negative:end:
     89/<- %esp 5/r32/ebp
     5d/pop-to-ebp
     c3/return
+
+_parse-array-of-decimal-ints:  # ad: (addr allocation-descriptor), s: (addr array byte), out: (addr handle array int)
+    # pseudocode
+    #   end = &s->data[s->size]
+    #   curr = s->data
+    #   size = 0
+    #   while true
+    #     if (curr >= end) break
+    #     curr = skip-chars-matching-in-slice(curr, end, ' ')
+    #     if (curr >= end) break
+    #     curr = skip-chars-not-matching-in-slice(curr, end, ' ')
+    #     ++size
+    #   allocate-array(ad, size*4, out)
+    #   var slice: slice = {s->data, 0}
+    #   curr = lookup(out)->data
+    #   while true
+    #     if (slice->start >= end) break
+    #     slice->start = skip-chars-matching-in-slice(slice->start, end, ' ')
+    #     if (slice->start >= end) break
+    #     slice->end = skip-chars-not-matching-in-slice(slice->start, end, ' ')
+    #     *curr = parse-hex-int-from-slice(slice)
+    #     curr += 4
+    #     slice->start = slice->end
+    #   return result
+    #
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # . save registers
+    50/push-eax
+    51/push-ecx
+    52/push-edx
+    53/push-ebx
+    56/push-esi
+    57/push-edi
+    # esi = s
+    8b/-> *(ebp+0xc) 6/r32/esi
+    # var curr/ecx: (addr byte) = s->data
+    8d/copy-address *(esi+4) 1/r32/ecx
+    # var end/edx: (addr byte) = &s->data[s->size]
+    # . edx = s->size
+    8b/-> *esi 2/r32/edx
+    # . edx += curr
+    01/add-to %edx 1/r32/ecx
+    # var size/ebx: int = 0
+    31/xor-with %ebx 3/r32/ebx
+$_parse-array-of-decimal-ints:loop1:
+    # if (curr >= end) break
+    39/compare %ecx 2/r32/edx
+    73/jump-if-addr>= $_parse-array-of-decimal-ints:break1/disp8
+    # curr = skip-chars-matching-in-slice(curr, end, ' ')
+    (skip-chars-matching-in-slice %ecx %edx 0x20)  # => eax
+    89/<- %ecx 0/r32/eax
+    # if (curr >= end) break
+    39/compare %ecx 2/r32/edx
+    73/jump-if-addr>= $_parse-array-of-decimal-ints:break1/disp8
+    # curr = skip-chars-not-matching-in-slice(curr, end, ' ')
+    (skip-chars-not-matching-in-slice %ecx %edx 0x20)  # => eax
+    89/<- %ecx 0/r32/eax
+    # size += 4
+    81 0/subop/add %ebx 4/imm32
+    eb/jump $_parse-array-of-decimal-ints:loop1/disp8
+$_parse-array-of-decimal-ints:break1:
+    (allocate-array *(ebp+8) %ebx *(ebp+0x10))
+$_parse-array-of-decimal-ints:pass2:
+    # var slice/edi: slice = {s->data, 0}
+    68/push 0/imm32/end
+    8d/copy-address *(esi+4) 7/r32/edi
+    57/push-edi
+    89/<- %edi 4/r32/esp
+    # curr = lookup(out)->data
+    8b/-> *(ebp+0x10) 0/r32/eax
+    (lookup *eax *(eax+4))  # => eax
+    8d/copy-address *(eax+4) 1/r32/ecx
+$_parse-array-of-decimal-ints:loop2:
+    # if (slice->start >= end) break
+    39/compare *edi 2/r32/edx
+    73/jump-if-addr>= $_parse-array-of-decimal-ints:end/disp8
+    # slice->start = skip-chars-matching-in-slice(slice->start, end, ' ')
+    (skip-chars-matching-in-slice *edi %edx 0x20)  # => eax
+    89/<- *edi 0/r32/eax
+    # if (slice->start >= end) break
+    39/compare *edi 2/r32/edx
+    73/jump-if-addr>= $_parse-array-of-decimal-ints:end/disp8
+    # slice->end = skip-chars-not-matching-in-slice(slice->start, end, ' ')
+    (skip-chars-not-matching-in-slice *edi %edx 0x20)  # => eax
+    89/<- *(edi+4) 0/r32/eax
+    # *curr = parse-hex-int-from-slice(slice)
+    (parse-decimal-int-from-slice %edi)
+    89/<- *ecx 0/r32/eax
+    # curr += 4
+    81 0/subop/add %ecx 4/imm32
+    # slice->start = slice->end
+    8b/-> *(edi+4) 0/r32/eax
+    89/<- *edi 0/r32/eax
+    eb/jump $_parse-array-of-decimal-ints:loop2/disp8
+$_parse-array-of-decimal-ints:end:
+    # . reclaim locals
+    81 0/subop/add %esp 8/imm32
+    # . restore registers
+    5f/pop-to-edi
+    5e/pop-to-esi
+    5b/pop-to-ebx
+    5a/pop-to-edx
+    59/pop-to-ecx
+    58/pop-to-eax
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-array-of-decimal-ints:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # var h/esi: (handle array int)
+    68/push 0/imm32
+    68/push 0/imm32
+    89/<- %esi 4/r32/esp
+    # var ecx: (array int) = [1, 2, 3]
+    68/push 3/imm32
+    68/push 2/imm32
+    68/push 1/imm32
+    68/push 0xc/imm32/size
+    89/<- %ecx 4/r32/esp
+    #
+    (_parse-array-of-decimal-ints Heap "1 2 3" %esi)
+    (lookup *esi *(esi+4))  # => eax
+    (array-equal? %ecx %eax)  # => eax
+    (check-ints-equal %eax 1 "F - test-parse-array-of-decimal-ints")
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-array-of-decimal-ints-empty:
+    # - empty string = empty array
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # var h/esi: handle
+    68/push 0/imm32
+    68/push 0/imm32
+    89/<- %esi 4/r32/esp
+    #
+    (_parse-array-of-decimal-ints Heap "" %esi)
+    (lookup *esi *(esi+4))  # => eax
+    (check-ints-equal *eax 0 "F - test-parse-array-of-decimal-ints-empty")
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-array-of-decimal-ints-just-whitespace:
+    # - just whitespace = empty array
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # var h/esi: handle
+    68/push 0/imm32
+    68/push 0/imm32
+    89/<- %esi 4/r32/esp
+    #
+    (_parse-array-of-decimal-ints Heap Space %esi)
+    (lookup *esi *(esi+4))  # => eax
+    (check-ints-equal *eax 0 "F - test-parse-array-of-decimal-ints-just-whitespace")
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-parse-array-of-decimal-ints-extra-whitespace:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # var h/esi: handle
+    68/push 0/imm32
+    68/push 0/imm32
+    89/<- %esi 4/r32/esp
+    # var ecx: (array int) = [1, 2, 3]
+    68/push 3/imm32
+    68/push 2/imm32
+    68/push 1/imm32
+    68/push 0xc/imm32/size
+    89/<- %ecx 4/r32/esp
+    #
+    (_parse-array-of-decimal-ints Heap " 1 2  3  " %esi)
+    (lookup *esi *(esi+4))  # => eax
+    (array-equal? %ecx %eax)  # => eax
+    (check-ints-equal %eax 1 "F - test-parse-array-of-decimal-ints-extra-whitespace")
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+parse-array-of-decimal-ints:  # s: (addr array byte), out: (addr handle array int)
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    #
+    (_parse-array-of-decimal-ints Heap *(ebp+8) *(ebp+0xc))
+$parse-array-of-decimal-ints:end:
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
diff --git a/baremetal/400.mu b/baremetal/400.mu
index 99195b2c..3a5e2174 100644
--- a/baremetal/400.mu
+++ b/baremetal/400.mu
@@ -67,5 +67,9 @@ sig stream-final s: (addr stream byte) -> _/eax: byte
 
 #sig copy-bytes src: (addr byte), dest: (addr byte), n: int
 sig copy-array-object src: (addr array _), dest-ah: (addr handle array _)
+sig array-equal? a: (addr array int), b: (addr array int) -> _/eax: boolean
+sig parse-array-of-ints s: (addr array byte), out: (addr handle array int)
+sig parse-array-of-decimal-ints s: (addr array byte), out: (addr handle array int)
+sig check-array-equal a: (addr array int), expected: (addr string), msg: (addr string)
 
 sig integer-divide a: int, b: int -> _/eax: int, _/edx: int
diff --git a/baremetal/shell/value.mu b/baremetal/shell/value.mu
index 64b1d772..4a93227e 100644
--- a/baremetal/shell/value.mu
+++ b/baremetal/shell/value.mu
@@ -6,6 +6,35 @@ type value {
   array-data: (handle array value)  # if type = 2
 }
 
+# top-level? is a hack just for numbers
+# we'll eventually need to return a y coordinate as well to render 2D values
+fn render-value screen: (addr screen), _val: (addr value), x: int, y: int, top-level?: boolean -> _/eax: int {
+  var val/esi: (addr value) <- copy _val
+  var val-type/ecx: (addr int) <- get val, type
+  compare *val-type, 1/string
+  {
+    break-if-!=
+    var val-ah/eax: (addr handle array byte) <- get val, text-data
+    var _val-string/eax: (addr array byte) <- lookup *val-ah
+    var val-string/ecx: (addr array byte) <- copy _val-string
+    var new-x/eax: int <- render-string screen, val-string, x, y
+    return new-x
+  }
+  compare *val-type, 2/array
+  {
+    break-if-!=
+    var val-ah/eax: (addr handle array value) <- get val, array-data
+    var _val-array/eax: (addr array value) <- lookup *val-ah
+    var val-array/edx: (addr array value) <- copy _val-array
+    var new-x/eax: int <- render-array screen, val-array, x, y
+    return new-x
+  }
+  # render ints by default for now
+  var val-num/eax: (addr float) <- get val, number-data
+  var new-x/eax: int <- render-number screen, *val-num, x, y, top-level?
+  return new-x
+}
+
 fn initialize-value-with-integer _self: (addr value), n: int {
   var self/esi: (addr value) <- copy _self
   var type/eax: (addr int) <- get self, type
@@ -127,3 +156,79 @@ fn test-render-string {
   check-screen-row screen, 0/y, "\"abc\"", "F - test-render-string"
   check-ints-equal new-x, 5, "F - test-render-string: result"
 }
+
+fn initialize-value-with-array-of-integers _self: (addr value), s: (addr array byte) {
+  # parse s into a temporary array of ints
+  var tmp-storage: (handle array int)
+  var tmp-ah/eax: (addr handle array int) <- address tmp-storage
+  parse-array-of-decimal-ints s, tmp-ah  # leak
+  var _tmp/eax: (addr array int ) <- lookup *tmp-ah
+  var tmp/esi: (addr array int ) <- copy _tmp
+  # load the array into values
+  var self/edi: (addr value) <- copy _self
+  var type/eax: (addr int) <- get self, type
+  copy-to *type, 2/string
+  var dest-array-ah/eax: (addr handle array value) <- get self, array-data
+  var len/ebx: int <- length tmp
+  populate dest-array-ah, len
+  var _dest-array/eax: (addr array value) <- lookup *dest-array-ah
+  var dest-array/edi: (addr array value) <- copy _dest-array
+  var i/eax: int <- copy 0
+  {
+    compare i, len
+    break-if->=
+    var src-addr/ecx: (addr int) <- index tmp, i
+    var src/ecx: int <- copy *src-addr
+    var src-f/xmm0: float <- convert src
+    var dest-offset/edx: (offset value) <- compute-offset dest-array, i
+    var dest-val/edx: (addr value) <- index dest-array, dest-offset
+    var dest/edx: (addr float) <- get dest-val, number-data
+    copy-to *dest, src-f
+    i <- increment
+    loop
+  }
+}
+
+fn render-array screen: (addr screen), _arr: (addr array value), x: int, y: int -> _/eax: int {
+  # don't surround in spaces
+  draw-code-point screen, 0x5b/open-bracket, x, y, 7/fg, 0/bg
+  increment x
+  var arr/esi: (addr array value) <- copy _arr
+  var max/ecx: int <- length arr
+  var i/edx: int <- copy 0
+  var new-x/eax: int <- copy x
+  {
+    compare i, max
+    break-if->=
+    {
+      compare i, 0
+      break-if-=
+      draw-code-point screen, 0x20/space, new-x, y, 7/fg, 0/bg
+      new-x <- increment
+    }
+    var off/ecx: (offset value) <- compute-offset arr, i
+    var x/ecx: (addr value) <- index arr, off
+    new-x <- render-value screen, x, new-x, y, 0/nested
+    i <- increment
+    loop
+  }
+  draw-code-point screen, 0x5d/close-bracket, new-x, y, 7/fg, 0/bg
+  new-x <- increment
+  return new-x
+}
+
+fn test-render-array {
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x20, 4
+  #
+  var val-storage: value
+  var val/eax: (addr value) <- address val-storage
+  initialize-value-with-array-of-integers val, "0 1 2"
+  var val-array-ah/eax: (addr handle array value) <- get val, array-data
+  var val-array/eax: (addr array value) <- lookup *val-array-ah
+  var new-x/eax: int <- render-array screen, val-array, 0/x, 0/y
+  check-screen-row screen, 0/y, "[0 1 2]", "F - test-render-array"
+  check-ints-equal new-x, 7, "F - test-render-array: result"
+}