about summary refs log tree commit diff stats
path: root/subx
diff options
context:
space:
mode:
authorKartik Agaram <vc@akkartik.com>2018-11-24 23:20:54 -0800
committerKartik Agaram <vc@akkartik.com>2018-11-24 23:26:14 -0800
commit33fdc60b168a04ca26b4924ecde2adb7c8059a13 (patch)
tree22238a4ef7b598a51aed1426ca89db00e6ab43f5 /subx
parent31ff94214bb9ae2f60b10bb9fa5ea468b141f752 (diff)
downloadmu-33fdc60b168a04ca26b4924ecde2adb7c8059a13.tar.gz
4775
Start with an exactly corresponding version to Crenshaw 2-1: single-digit
numbers. The only change: we assume the number is in hex.

The next version now supports multi-digit hex numbers.
Diffstat (limited to 'subx')
-rwxr-xr-xsubx/apps/crenshaw2-1bin7048 -> 6489 bytes
-rw-r--r--subx/apps/crenshaw2-1.subx224
-rwxr-xr-xsubx/apps/crenshaw2-1bbin0 -> 7048 bytes
-rw-r--r--subx/apps/crenshaw2-1b.subx836
-rwxr-xr-xsubx/test_apps10
5 files changed, 857 insertions, 213 deletions
diff --git a/subx/apps/crenshaw2-1 b/subx/apps/crenshaw2-1
index 1396ede0..16829e6a 100755
--- a/subx/apps/crenshaw2-1
+++ b/subx/apps/crenshaw2-1
Binary files differdiff --git a/subx/apps/crenshaw2-1.subx b/subx/apps/crenshaw2-1.subx
index 3abf7365..62e2cb89 100644
--- a/subx/apps/crenshaw2-1.subx
+++ b/subx/apps/crenshaw2-1.subx
@@ -186,23 +186,18 @@ compile:  # in : fd or (address stream), out : fd or (address stream), err : fd
   5d/pop-to-EBP
   c3/return
 
-# Read a sequence of digits into 'out'. Abort if there are none, or if there
-# are too many to fit in 'out'.
-# Input comes from the global variable 'Look' (first byte) and the argument
-# 'in' (rest).
+# Read a single digit into 'out'. Abort if there are none, or if there is no space in 'out'.
+# Input comes from the global variable 'Look', and we leave the next byte from
+# 'in' into it on exit.
 get-num:  # in : (address buffered-file), out : (address stream), err : fd or (address stream), ed : (address exit-descriptor) -> <void>
   # pseudocode:
   #   if !is-digit?(Look) expected(ed, err, "integer")
-  #   do
-  #     if out.write >= out.length
-  #       write(err, "Error: too many digits in number\n")
-  #       stop(ed, 1)
-  #     out.data[out.write] = LSB(Look)
-  #     ++out.write
-  #     Look = get-char(in)
-  #   while is-digit?(Look)
-  # This is complicated because I don't want to hard-code the error strategy in
-  # a general helper like write-byte. Maybe I should just create a local helper.
+  #   if out.write >= out.length
+  #     write(err, "Error: too many digits in number\n")
+  #     stop(ed, 1)
+  #   out.data[out.write] = LSB(Look)
+  #   ++out.write
+  #   Look = get-char(in)
   #
   # within the loop we'll try to keep things in registers:
   #   ESI : in
@@ -254,10 +249,9 @@ $get-num:main:
   8b/copy                         0/mod/indirect  7/rm32/EDI    .           .             .           1/r32/ECX   .               .                 # copy *EDI to ECX
     # EDX = out->length
   8b/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           2/r32/EDX   8/disp8         .                 # copy *(EDI+8) to EDX
-$get-num:loop:
   # if out->write >= out->length error
   3b/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare EDX with ECX
-  7d/jump-if-lesser  $get-num:loop-stage2/disp8
+  7d/jump-if-lesser  $get-num:stage2/disp8
     # error(ed, err, "get-num: too many digits in number")  # TODO: show full number
       # push args
   68/push  "get-num: too many digits in number"/imm32
@@ -267,7 +261,7 @@ $get-num:loop:
   e8/call  error/disp32  # never returns
       # discard args
   81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-$get-num:loop-stage2:
+$get-num:stage2:
   # out->data[out->write] = LSB(Look)
   8d/copy-address                 1/mod/*+disp8   4/rm32/sib    7/base/EDI  1/index/ECX   .           3/r32/EBX   0xc/disp8       .                 # copy EDI+ECX+12 to EBX
   8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Look/disp32     .                 # copy *Look to EAX
@@ -281,16 +275,6 @@ $get-num:loop-stage2:
   e8/call  get-char/disp32
     # discard args
   81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-  # EAX = is-digit?(Look)
-    # push args
-  ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Look/disp32     .                 # push *Look
-    # call
-  e8/call  is-digit?/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-  # if EAX loop
-  3d/compare-EAX  0/imm32
-  0f 85/jump-if-not-equal  $get-num:loop/disp16
 $get-num:loop-end:
   # persist necessary variables from registers
   89/copy                         0/mod/indirect  7/rm32/EDI    .           .             .           1/r32/ECX   .               .                 # copy ECX to *EDI
@@ -486,192 +470,6 @@ test-get-num-aborts-on-non-digit-in-Look:
   5d/pop-to-EBP
   c3/return
 
-test-get-num-reads-multiple-digits:
-  ## check that get-num returns all initial digits until it encounters a non-digit
-  # This test uses exit-descriptors. Use EBP for setting up local variables.
-  55/push-EBP
-  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-  ## clear all streams
-  # clear-stream(_test-stream)
-    # push args
-  68/push  _test-stream/imm32
-    # call
-  e8/call  clear-stream/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-  # clear-stream(_test-buffered-file+4)
-    # push args
-  b8/copy-to-EAX  _test-buffered-file/imm32
-  05/add-to-EAX  4/imm32
-  50/push-EAX
-    # call
-  e8/call  clear-stream/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-  # clear-stream(_test-output-stream)
-    # push args
-  68/push  _test-output-stream/imm32
-    # call
-  e8/call  clear-stream/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-  # clear-stream(_test-error-stream)
-    # push args
-  68/push  _test-error-stream/imm32
-    # call
-  e8/call  clear-stream/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-  ## initialize 'in'
-  # write(_test-stream, "3456 x")
-    # push args
-  68/push  "3456"/imm32
-  68/push  _test-stream/imm32
-    # call
-  e8/call  write/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-  ## initialize exit-descriptor 'ed'
-  # allocate on stack
-  # var ed/EAX : (address exit-descriptor)
-  81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
-  8d/copy-address                 0/mod/indirect  4/rm32/sib    4/base/ESP  4/index/none  .           0/r32/EAX   .               .                 # copy ESP to EAX
-  # size the exit-descriptor for the call to get-num below
-  # tailor-exit-descriptor(ed, 16)
-    # push args
-  68/push  0x10/imm32/nbytes-of-args-for-get-num
-  50/push-EAX/ed
-    # call
-  e8/call  tailor-exit-descriptor/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-  ## prime the pump
-  # get-char(_test-buffered-file)
-    # push args
-  68/push  _test-buffered-file/imm32
-    # call
-  e8/call  get-char/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-  ## get-num(in, out, err, ed)
-    # push args
-  50/push-EAX/ed
-  68/push  _test-error-stream/imm32
-  68/push  _test-output-stream/imm32
-  68/push  _test-buffered-file/imm32
-    # call
-  e8/call  get-num/disp32
-  ## registers except ESP may be clobbered at this point
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
-  # check-ints-equal(*_test-output-stream.data, '3456')
-    # push args
-  68/push  "F - test-get-num-reads-multiple-digits"/imm32
-  68/push  0x36353433/imm32
-  b8/copy-to-EAX  _test-output-stream/imm32
-  ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           0xc/disp8       .                 # push *(EAX+12)
-    # call
-  e8/call  check-ints-equal/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-  # reclaim locals
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-  5d/pop-to-EBP
-  c3/return
-
-test-get-num-reads-multiple-digits-followed-by-nondigit:
-  ## check that get-num returns all initial digits until it encounters a non-digit
-  # This test uses exit-descriptors. Use EBP for setting up local variables.
-  55/push-EBP
-  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-  ## clear all streams
-  # clear-stream(_test-stream)
-    # push args
-  68/push  _test-stream/imm32
-    # call
-  e8/call  clear-stream/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-  # clear-stream(_test-buffered-file+4)
-    # push args
-  b8/copy-to-EAX  _test-buffered-file/imm32
-  05/add-to-EAX  4/imm32
-  50/push-EAX
-    # call
-  e8/call  clear-stream/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-  # clear-stream(_test-output-stream)
-    # push args
-  68/push  _test-output-stream/imm32
-    # call
-  e8/call  clear-stream/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-  # clear-stream(_test-error-stream)
-    # push args
-  68/push  _test-error-stream/imm32
-    # call
-  e8/call  clear-stream/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-  ## initialize 'in'
-  # write(_test-stream, "3456 x")
-    # push args
-  68/push  "3456 x"/imm32
-  68/push  _test-stream/imm32
-    # call
-  e8/call  write/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-  ## initialize exit-descriptor 'ed'
-  # allocate on stack
-  # var ed/EAX : (address exit-descriptor)
-  81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
-  8d/copy-address                 0/mod/indirect  4/rm32/sib    4/base/ESP  4/index/none  .           0/r32/EAX   .               .                 # copy ESP to EAX
-  # size the exit-descriptor for the call to get-num below
-  # tailor-exit-descriptor(ed, 16)
-    # push args
-  68/push  0x10/imm32/nbytes-of-args-for-get-num
-  50/push-EAX/ed
-    # call
-  e8/call  tailor-exit-descriptor/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-  ## prime the pump
-  # get-char(_test-buffered-file)
-    # push args
-  68/push  _test-buffered-file/imm32
-    # call
-  e8/call  get-char/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-  ## get-num(in, out, err, ed)
-    # push args
-  50/push-EAX/ed
-  68/push  _test-error-stream/imm32
-  68/push  _test-output-stream/imm32
-  68/push  _test-buffered-file/imm32
-    # call
-  e8/call  get-num/disp32
-  ## registers except ESP may be clobbered at this point
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
-  # check-ints-equal(*_test-output-stream.data, '3456')
-    # push args
-  68/push  "F - test-get-num-reads-multiple-digits-followed-by-nondigit"/imm32
-  68/push  0x36353433/imm32
-  b8/copy-to-EAX  _test-output-stream/imm32
-  ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           0xc/disp8       .                 # push *(EAX+12)
-    # call
-  e8/call  check-ints-equal/disp32
-    # discard args
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-  # reclaim locals
-  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-  5d/pop-to-EBP
-  c3/return
-
 ## helpers
 
 # write(f, "Error: "+s+" expected\n") then stop(ed, 1)
diff --git a/subx/apps/crenshaw2-1b b/subx/apps/crenshaw2-1b
new file mode 100755
index 00000000..1396ede0
--- /dev/null
+++ b/subx/apps/crenshaw2-1b
Binary files differdiff --git a/subx/apps/crenshaw2-1b.subx b/subx/apps/crenshaw2-1b.subx
new file mode 100644
index 00000000..ef6996ee
--- /dev/null
+++ b/subx/apps/crenshaw2-1b.subx
@@ -0,0 +1,836 @@
+## Port of https://github.com/akkartik/crenshaw/blob/master/tutor2.1.pas
+# Corresponds to the section "single digits" in https://compilers.iecc.com/crenshaw/tutor2.txt
+# except that we support numbers of multiple digits.
+#
+# To run (from the subx directory):
+#   $ ./subx translate *.subx apps/crenshaw2-1.subx -o crenshaw2-1
+#   $ echo '1a'  |./subx run apps/crenshaw2-1
+# Expected output (not working yet):
+#   # syscall(exit, 1a)
+#   bb/copy-to-EBX  3/imm32
+#   b8/copy-to-EAX  1/imm32/exit
+#   cd/syscall  0x80/imm8
+#
+# To run the generated output:
+#   $ echo '1a'  |./subx run apps/crenshaw2-1 > z1.subx
+#   $ ./subx translate z1.subx -o z1
+#   $ ./subx run z1
+#   $ echo $?
+#   26  # 0x1a in decimal
+#
+# Stdin must contain just a single hex digit. Other input will print an error:
+#   $ echo 'xyz'  |./subx run apps/crenshaw2-1
+#   Error: integer expected
+#
+# Names in this file sometimes follow Crenshaw's original rather than my usual
+# naming conventions.
+
+== code
+# instruction                     effective address                                                   operand     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
+
+# main: run tests if necessary, call 'compile' if not
+  # prolog
+  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+  # if (argc > 1)
+  81          7/subop/compare     1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0/disp8         1/imm32           # compare *EBP
+  7e/jump-if-lesser-or-equal  $run-main/disp8
+  # and if (argv[1] == "test")
+    # push args
+  68/push  "test"/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0x8/disp8       .                 # push *(EBP+8)
+    # call
+  e8/call  kernel-string-equal/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check result
+  3d/compare-EAX  1/imm32
+  75/jump-if-not-equal  $run-main/disp8
+  # then return run-tests()
+  e8/call  run-tests/disp32
+#?   e8/call test-get-num-reads-multiple-digits/disp32
+  8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
+  eb/jump  $main:end/disp8
+$run-main:
+  # allocate space for an exit-descriptor
+    # var ed/EAX : (address exit-descriptor)
+  81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
+  8d/copy-address                 0/mod/indirect  4/rm32/sib    4/base/ESP  4/index/none  .           0/r32/EAX   .               .                 # copy ESP to EAX
+    # clear ed->target (so we really exit)
+  c7/copy                         0/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm32           # copy to *EAX
+  # compile(Stdin, 1/stdout, 2/stderr, ed)
+    # push args
+  50/push-EAX/ed
+  68/push  2/imm32/stderr
+  68/push  1/imm32/stdout
+  68/push  Stdin/imm32
+    # call
+  e8/call  compile/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+  # syscall(exit, 0)
+  bb/copy-to-EBX  0/imm32
+$main:end:
+  b8/copy-to-EAX  1/imm32/exit
+  cd/syscall  0x80/imm8
+
+# the main entry point
+compile:  # in : fd or (address stream), out : fd or (address stream), err : fd or (address stream), ed : (address exit-descriptor) -> <void>
+  # prolog
+  55/push-EBP
+  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+  # save registers
+  50/push-EAX
+  51/push-ECX
+  # Look = get-char(in)
+    # push args
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           8/disp8      .                    # push *(EBP+8)
+    # call
+  e8/call  get-char/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # var num/ECX : (address stream) on the stack
+  # Numbers can be 32 bits or 8 hex bytes long. One of them will be in 'Look', so we need space for 7 bytes.
+  # We won't add more, so that we can get overflow-handling for free.
+  # Add 12 bytes for 'read', 'write' and 'length' fields, for a total of 19 bytes, or 0x13 in hex.
+  81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x13/imm32        # subtract from ESP
+  8d/copy-address                 0/mod/indirect  4/rm32/sib    4/base/ESP  4/index/none  .           1/r32/ECX   .               .                 # copy ESP to ECX
+  # num->length = 7
+  c7/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           .           8/disp8         7/imm32           # copy to *(ECX+8)
+  # clear-stream(num)
+    # push args
+  51/push-ECX
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # get-num(in, num, err, ed)
+    # push args
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0x14/disp8      .                 # push *(EBP+20)
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0x10/disp8      .                 # push *(EBP+16)
+  51/push-ECX/num
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           8/disp8      .                    # push *(EBP+8)
+    # call
+  e8/call  get-num/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+  # EAX = write(_test-stream, "Ab")
+    # push args
+  68/push  "Ab"/imm32
+  68/push  _test-stream/imm32
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # EAX = write(out, "bb/copy-to-EBX  ")
+    # push args
+  68/push  "bb/copy-to-EBX  "/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # write-stream(out, num)
+    # push args
+  51/push-ECX/num
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # write(out, Newline)
+    # push args
+  68/push  Newline/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # EAX = write(out, "b8/copy-to-EAX  1/imm32/exit")
+    # push args
+  68/push  "b8/copy-to-EAX  1/imm32/exit"/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # EAX = write(out, Newline)
+    # push args
+  68/push  Newline/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # EAX = write(out, "cd/syscall  0x80/imm8")
+    # push args
+  68/push  "cd/syscall  0x80/imm8"/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # EAX = write(out, Newline)
+    # push args
+  68/push  Newline/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # restore registers
+  59/pop-to-ECX
+  58/pop-to-EAX
+  # epilog
+  89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+  5d/pop-to-EBP
+  c3/return
+
+# Read a sequence of digits into 'out'. Abort if there are none, or if there is
+# no space in 'out'.
+# Input comes from the global variable 'Look' (first byte) and the argument
+# 'in' (rest). We leave the next byte from 'in' into 'Look' on exit.
+get-num:  # in : (address buffered-file), out : (address stream), err : fd or (address stream), ed : (address exit-descriptor) -> <void>
+  # pseudocode:
+  #   if !is-digit?(Look) expected(ed, err, "integer")
+  #   do
+  #     if out.write >= out.length
+  #       write(err, "Error: too many digits in number\n")
+  #       stop(ed, 1)
+  #     out.data[out.write] = LSB(Look)
+  #     ++out.write
+  #     Look = get-char(in)
+  #   while is-digit?(Look)
+  # This is complicated because I don't want to hard-code the error strategy in
+  # a general helper like write-byte. Maybe I should just create a local helper.
+  #
+  # within the loop we'll try to keep things in registers:
+  #   ESI : in
+  #   EDI : out
+  #   EAX : temp
+  #   ECX : out->write
+  #   EDX : out->length
+  #   EBX : temp2
+  # We can't allocate Look to a register because it gets written implicitly in
+  # get-char in each iteration of the loop. (Thereby demonstrating that it's
+  # not the right interface for us. But we'll keep it just to follow Crenshaw.)
+  #
+  # prolog
+  55/push-EBP
+  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+  # EAX = is-digit?(Look)
+    # push args
+  ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Look/disp32     .                 # push *Look
+    # call
+  e8/call  is-digit?/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # if EAX == 0 error
+  3d/compare-EAX  0/imm32
+  75/jump-if-not-equal  $get-num:main/disp8
+    # expected(ed, err, "integer")
+      # push args
+  68/push  "integer"/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0x10/disp8      .                 # push *(EBP+16)
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0x14/disp8      .                 # push *(EBP+20)
+      # call
+  e8/call  expected/disp32  # never returns
+      # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$get-num:main:
+  # save registers
+  50/push-EAX
+  51/push-ECX
+  52/push-EDX
+  53/push-EBX
+  56/push-ESI
+  57/push-EDI
+  # read necessary variables to registers
+    # ESI = in
+  8b/copy                         1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
+    # EDI = out
+  8b/copy                         1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           7/r32/EDI   0xc/disp8       .                 # copy *(EBP+12) to EDI
+    # ECX = out->write
+  8b/copy                         0/mod/indirect  7/rm32/EDI    .           .             .           1/r32/ECX   .               .                 # copy *EDI to ECX
+    # EDX = out->length
+  8b/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           2/r32/EDX   8/disp8         .                 # copy *(EDI+8) to EDX
+$get-num:loop:
+  # if out->write >= out->length error
+  3b/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare EDX with ECX
+  7d/jump-if-lesser  $get-num:loop-stage2/disp8
+    # error(ed, err, "get-num: too many digits in number")  # TODO: show full number
+      # push args
+  68/push  "get-num: too many digits in number"/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0x10/disp8      .                 # push *(EBP+16)
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0x14/disp8      .                 # push *(EBP+20)
+      # call
+  e8/call  error/disp32  # never returns
+      # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$get-num:loop-stage2:
+  # out->data[out->write] = LSB(Look)
+  8d/copy-address                 1/mod/*+disp8   4/rm32/sib    7/base/EDI  1/index/ECX   .           3/r32/EBX   0xc/disp8       .                 # copy EDI+ECX+12 to EBX
+  8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Look/disp32     .                 # copy *Look to EAX
+  88/copy-byte                    0/mod/indirect  3/rm32/EBX    .           .             .           0/r32/AL    .               .                 # copy byte at AL to *EBX
+  # ++out->write
+  41/increment-ECX
+  # Look = get-char(in)
+    # push args
+  56/push-ESI
+    # call
+  e8/call  get-char/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # EAX = is-digit?(Look)
+    # push args
+  ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Look/disp32     .                 # push *Look
+    # call
+  e8/call  is-digit?/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # if EAX loop
+  3d/compare-EAX  0/imm32
+  0f 85/jump-if-not-equal  $get-num:loop/disp16
+$get-num:loop-end:
+  # persist necessary variables from registers
+  89/copy                         0/mod/indirect  7/rm32/EDI    .           .             .           1/r32/ECX   .               .                 # copy ECX to *EDI
+  # 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
+  # epilog
+  89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+  5d/pop-to-EBP
+  c3/return
+
+test-get-num-reads-single-digit:
+  ## check that get-num returns first character if it's a digit
+  # This test uses exit-descriptors. Use EBP for setting up local variables.
+  55/push-EBP
+  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+  ## clear all streams
+  # clear-stream(_test-stream)
+    # push args
+  68/push  _test-stream/imm32
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # clear-stream(_test-buffered-file+4)
+    # push args
+  b8/copy-to-EAX  _test-buffered-file/imm32
+  05/add-to-EAX  4/imm32
+  50/push-EAX
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # clear-stream(_test-output-stream)
+    # push args
+  68/push  _test-output-stream/imm32
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # clear-stream(_test-error-stream)
+    # push args
+  68/push  _test-error-stream/imm32
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  ## initialize 'in'
+  # write(_test-stream, "3")
+    # push args
+  68/push  "3"/imm32
+  68/push  _test-stream/imm32
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  ## initialize exit-descriptor 'ed'
+  # allocate on stack
+  # var ed/EAX : (address exit-descriptor)
+  81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
+  8d/copy-address                 0/mod/indirect  4/rm32/sib    4/base/ESP  4/index/none  .           0/r32/EAX   .               .                 # copy ESP to EAX
+  # size the exit-descriptor for the call to get-num below
+  # tailor-exit-descriptor(ed, 16)
+    # push args
+  68/push  0x10/imm32/nbytes-of-args-for-get-num
+  50/push-EAX/ed
+    # call
+  e8/call  tailor-exit-descriptor/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  ## prime the pump
+  # get-char(_test-buffered-file)
+    # push args
+  68/push  _test-buffered-file/imm32
+    # call
+  e8/call  get-char/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  ## get-num(in, out, err, ed)
+    # push args
+  50/push-EAX/ed
+  68/push  _test-error-stream/imm32
+  68/push  _test-output-stream/imm32
+  68/push  _test-buffered-file/imm32
+    # call
+  e8/call  get-num/disp32
+  ## registers except ESP may be clobbered at this point
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+  # check-ints-equal(*_test-output-stream.data, '3')
+    # push args
+  68/push  "F - test-get-num-reads-single-digit"/imm32
+  68/push  0x33/imm32
+  b8/copy-to-EAX  _test-output-stream/imm32
+  ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           0xc/disp8       .                 # push *(EAX+12)
+    # call
+  e8/call  check-ints-equal/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+  # reclaim locals
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  5d/pop-to-EBP
+  c3/return
+
+test-get-num-aborts-on-non-digit-in-Look:
+  ## check that get-num returns first character if it's a digit
+  # This test uses exit-descriptors. Use EBP for setting up local variables.
+  55/push-EBP
+  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+  ## clear all streams
+  # clear-stream(_test-stream)
+    # push args
+  68/push  _test-stream/imm32
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # clear-stream(_test-buffered-file+4)
+    # push args
+  b8/copy-to-EAX  _test-buffered-file/imm32
+  05/add-to-EAX  4/imm32
+  50/push-EAX
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # clear-stream(_test-output-stream)
+    # push args
+  68/push  _test-output-stream/imm32
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # clear-stream(_test-error-stream)
+    # push args
+  68/push  _test-error-stream/imm32
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  ## initialize 'in'
+  # write(_test-stream, "3")
+    # push args
+  68/push  "3"/imm32
+  68/push  _test-stream/imm32
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  ## initialize exit-descriptor 'ed'
+  # allocate on stack
+  # var ed/EAX : (address exit-descriptor)
+  81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
+  8d/copy-address                 0/mod/indirect  4/rm32/sib    4/base/ESP  4/index/none  .           0/r32/EAX   .               .                 # copy ESP to EAX
+  # size the exit-descriptor for the call to get-num below
+  # tailor-exit-descriptor(ed, 16)
+    # push args
+  68/push  0x10/imm32/nbytes-of-args-for-get-num
+  50/push-EAX/ed
+    # call
+  e8/call  tailor-exit-descriptor/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  ## don't initialize Look
+  ## get-num(in, out, err, ed)
+    # push args
+  50/push-EAX/ed
+  68/push  _test-error-stream/imm32
+  68/push  _test-output-stream/imm32
+  68/push  _test-buffered-file/imm32
+    # call
+  e8/call  get-num/disp32
+  ## registers except ESP may be clobbered at this point
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+  ## check that get-num tried to call exit(1)
+  # check-ints-equal(ed->value, 2, msg)  # i.e. stop was called with value 1
+    # push args
+  68/push  "F - test-get-num-aborts-on-non-digit-in-Look"/imm32
+  68/push  2/imm32
+    # push ed->value
+  ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           4/disp8         .                 # push *(EAX+4)
+    # call
+  e8/call  check-ints-equal/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+  # reclaim locals
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  5d/pop-to-EBP
+  c3/return
+
+test-get-num-reads-multiple-digits:
+  ## check that get-num returns all initial digits until it encounters a non-digit
+  # This test uses exit-descriptors. Use EBP for setting up local variables.
+  55/push-EBP
+  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+  ## clear all streams
+  # clear-stream(_test-stream)
+    # push args
+  68/push  _test-stream/imm32
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # clear-stream(_test-buffered-file+4)
+    # push args
+  b8/copy-to-EAX  _test-buffered-file/imm32
+  05/add-to-EAX  4/imm32
+  50/push-EAX
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # clear-stream(_test-output-stream)
+    # push args
+  68/push  _test-output-stream/imm32
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # clear-stream(_test-error-stream)
+    # push args
+  68/push  _test-error-stream/imm32
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  ## initialize 'in'
+  # write(_test-stream, "3456 x")
+    # push args
+  68/push  "3456"/imm32
+  68/push  _test-stream/imm32
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  ## initialize exit-descriptor 'ed'
+  # allocate on stack
+  # var ed/EAX : (address exit-descriptor)
+  81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
+  8d/copy-address                 0/mod/indirect  4/rm32/sib    4/base/ESP  4/index/none  .           0/r32/EAX   .               .                 # copy ESP to EAX
+  # size the exit-descriptor for the call to get-num below
+  # tailor-exit-descriptor(ed, 16)
+    # push args
+  68/push  0x10/imm32/nbytes-of-args-for-get-num
+  50/push-EAX/ed
+    # call
+  e8/call  tailor-exit-descriptor/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  ## prime the pump
+  # get-char(_test-buffered-file)
+    # push args
+  68/push  _test-buffered-file/imm32
+    # call
+  e8/call  get-char/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  ## get-num(in, out, err, ed)
+    # push args
+  50/push-EAX/ed
+  68/push  _test-error-stream/imm32
+  68/push  _test-output-stream/imm32
+  68/push  _test-buffered-file/imm32
+    # call
+  e8/call  get-num/disp32
+  ## registers except ESP may be clobbered at this point
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+  # check-ints-equal(*_test-output-stream.data, '3456')
+    # push args
+  68/push  "F - test-get-num-reads-multiple-digits"/imm32
+  68/push  0x36353433/imm32
+  b8/copy-to-EAX  _test-output-stream/imm32
+  ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           0xc/disp8       .                 # push *(EAX+12)
+    # call
+  e8/call  check-ints-equal/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+  # reclaim locals
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  5d/pop-to-EBP
+  c3/return
+
+test-get-num-reads-multiple-digits-followed-by-nondigit:
+  ## check that get-num returns all initial digits until it encounters a non-digit
+  # This test uses exit-descriptors. Use EBP for setting up local variables.
+  55/push-EBP
+  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+  ## clear all streams
+  # clear-stream(_test-stream)
+    # push args
+  68/push  _test-stream/imm32
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # clear-stream(_test-buffered-file+4)
+    # push args
+  b8/copy-to-EAX  _test-buffered-file/imm32
+  05/add-to-EAX  4/imm32
+  50/push-EAX
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # clear-stream(_test-output-stream)
+    # push args
+  68/push  _test-output-stream/imm32
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # clear-stream(_test-error-stream)
+    # push args
+  68/push  _test-error-stream/imm32
+    # call
+  e8/call  clear-stream/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  ## initialize 'in'
+  # write(_test-stream, "3456 x")
+    # push args
+  68/push  "3456 x"/imm32
+  68/push  _test-stream/imm32
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  ## initialize exit-descriptor 'ed'
+  # allocate on stack
+  # var ed/EAX : (address exit-descriptor)
+  81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
+  8d/copy-address                 0/mod/indirect  4/rm32/sib    4/base/ESP  4/index/none  .           0/r32/EAX   .               .                 # copy ESP to EAX
+  # size the exit-descriptor for the call to get-num below
+  # tailor-exit-descriptor(ed, 16)
+    # push args
+  68/push  0x10/imm32/nbytes-of-args-for-get-num
+  50/push-EAX/ed
+    # call
+  e8/call  tailor-exit-descriptor/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  ## prime the pump
+  # get-char(_test-buffered-file)
+    # push args
+  68/push  _test-buffered-file/imm32
+    # call
+  e8/call  get-char/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  ## get-num(in, out, err, ed)
+    # push args
+  50/push-EAX/ed
+  68/push  _test-error-stream/imm32
+  68/push  _test-output-stream/imm32
+  68/push  _test-buffered-file/imm32
+    # call
+  e8/call  get-num/disp32
+  ## registers except ESP may be clobbered at this point
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+  # check-ints-equal(*_test-output-stream.data, '3456')
+    # push args
+  68/push  "F - test-get-num-reads-multiple-digits-followed-by-nondigit"/imm32
+  68/push  0x36353433/imm32
+  b8/copy-to-EAX  _test-output-stream/imm32
+  ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           0xc/disp8       .                 # push *(EAX+12)
+    # call
+  e8/call  check-ints-equal/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+  # reclaim locals
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  5d/pop-to-EBP
+  c3/return
+
+## helpers
+
+# write(f, "Error: "+s+" expected\n") then stop(ed, 1)
+expected:  # ed : (address exit-descriptor), f : fd or (address stream), s : (address array byte) -> <void>
+  # prolog
+  55/push-EBP
+  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+  # write(f, "Error: ")
+    # push args
+  68/push  "Error: "/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # write(f, s)
+    # push args
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0x10/disp8      .                 # push *(EBP+16)
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # write(f, " expected")
+    # push args
+  68/push  " expected"/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # write(f, Newline)
+    # push args
+  68/push  Newline/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # stop(ed, 1)
+    # push args
+  68/push  1/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           8/disp8         .                 # push *(EBP+8)
+    # call
+  e8/call  stop/disp32
+  ## should never get past this point
+  # epilog
+  89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+  5d/pop-to-EBP
+  c3/return
+
+# write(f, "Error: "+s+"\n") then stop(ed, 1)
+error:  # ed : (address exit-descriptor), f : fd or (address stream), s : (address array byte) -> <void>
+  # prolog
+  55/push-EBP
+  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+  # write(f, "Error: ")
+    # push args
+  68/push  "Error: "/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # write(f, s)
+    # push args
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0x10/disp8      .                 # push *(EBP+16)
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # write(f, Newline)
+    # push args
+  68/push  Newline/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0xc/disp8       .                 # push *(EBP+12)
+    # call
+  e8/call  write/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+  # stop(ed, 1)
+    # push args
+  68/push  1/imm32
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           8/disp8         .                 # push *(EBP+8)
+    # call
+  e8/call  stop/disp32
+  ## should never get past this point
+  # epilog
+  89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+  5d/pop-to-EBP
+  c3/return
+
+# read a byte from 'f', and store it in 'Look'
+get-char:  # f : (address buffered-file) -> <void>
+  # prolog
+  55/push-EBP
+  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+  # save registers
+  50/push-EAX
+  # read-byte(f)
+    # push args
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0x8/disp8       .                 # push *(EBP+8)
+    # call
+  e8/call  read-byte/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # save EAX to Look
+  89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Look/disp32     .                 # copy EAX to *Look
+  # restore registers
+  58/pop-to-EAX
+  # epilog
+  89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+  5d/pop-to-EBP
+  c3/return
+
+is-digit?:  # c : int -> bool/EAX
+  # prolog
+  55/push-EBP
+  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+  # EAX = false
+  b8/copy-to-EAX  0/imm32
+  # if c < '0' return false
+  81          7/subop/compare     1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0x8/disp8       0x30/imm32        # compare *(EBP+8)
+  7c/jump-if-lesser  $is-digit?:end/disp8
+  # if c > '9' return false
+  81          7/subop/compare     1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           0x8/disp8       0x39/imm32        # compare *(EBP+8)
+  7f/jump-if-greater  $is-digit?:end/disp8
+  # otherwise return true
+  b8/copy-to-EAX  1/imm32
+$is-digit?:end:
+  # epilog
+  89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+  5d/pop-to-EBP
+  c3/return
+
+== data
+
+Look:
+  00 00 00 00
+
+_test-output-stream:
+  # current write index
+  00 00 00 00
+  # current read index
+  00 00 00 00
+  # length (= 8)
+  08 00 00 00
+  # data
+  00 00 00 00 00 00 00 00  # 8 bytes
+
+_test-error-stream:
+  # current write index
+  00 00 00 00
+  # current read index
+  00 00 00 00
+  # length (= 8)
+  08 00 00 00
+  # data
+  00 00 00 00 00 00 00 00  # 8 bytes
+
+# vim:nowrap:textwidth=0
diff --git a/subx/test_apps b/subx/test_apps
index 8c97c521..2bb7adec 100755
--- a/subx/test_apps
+++ b/subx/test_apps
@@ -150,4 +150,14 @@ test `uname` = 'Linux'  &&  {
   echo
 }
 
+echo crenshaw2-1b
+CFLAGS=-g ./subx translate *.subx apps/crenshaw2-1b.subx  -o apps/crenshaw2-1b
+[ "$1" != record ]  &&  git diff --quiet apps/crenshaw2-1b
+CFLAGS=-g ./subx run apps/crenshaw2-1b test
+echo
+test `uname` = 'Linux'  &&  {
+  apps/crenshaw2-1b test
+  echo
+}
+
 exit 0