about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKartik Agaram <vc@akkartik.com>2018-11-23 00:28:16 -0800
committerKartik Agaram <vc@akkartik.com>2018-11-23 00:42:30 -0800
commitd373c008b3ef54845175db7b8e8319d750a5b7bf (patch)
tree08daa8370d16d82770f3b72580c8e8a254cafde5
parent886630e9376d0f8198117aac3c0d83938bb2c1c6 (diff)
downloadmu-d373c008b3ef54845175db7b8e8319d750a5b7bf.tar.gz
4763 - back to the 'trivial' crenshaw2-1 compiler
This time I've ported (and test-driven) 'GetChar' and 'GetNum'. The new
tests bring together our new testable interfaces for read() and exit().
-rwxr-xr-xsubx/apps/crenshaw2-1bin4723 -> 5716 bytes
-rw-r--r--subx/apps/crenshaw2-1.subx424
-rwxr-xr-xsubx/test_apps4
3 files changed, 426 insertions, 2 deletions
diff --git a/subx/apps/crenshaw2-1 b/subx/apps/crenshaw2-1
index 8a4edcb7..2f8be36a 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 69a42885..817543e4 100644
--- a/subx/apps/crenshaw2-1.subx
+++ b/subx/apps/crenshaw2-1.subx
@@ -17,9 +17,11 @@
 #   $ echo $?
 #   3
 #
-# Stdin must contain just a single hex number. Other input will print an error:
+# 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 the usual naming conventions.
 
 == code
 # instruction                     effective address                                                   operand     displacement    immediate
@@ -27,6 +29,27 @@
 # 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:
+  # 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
+  8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           1/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
@@ -44,12 +67,300 @@
   81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/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
 
+# Read a sequence of digits into 'out'. Abort if there are none, or if there
+# are too many to fit in 'out'.
+# 'Look' must be initialized with the current byte.
+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 Look > 0xff
+  #       write(err, "Error: tried to write more than one byte\n")
+  #       stop(ed, 1)
+  #     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 Look > 0xff error
+  8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Look/disp32     .                 # copy *Look to EAX
+  3d/compare-EAX  0xff/imm32
+  7e/jump-if-lesser-or-equal  $get-num:loop-stage2/disp8
+    # error(ed, err, "get-num: tried to write too large a value into a single byte")  # TODO: show value being printed
+      # push args
+  68/push  "get-num: tried to write too large a value into a single byte"/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:
+  # 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-stage3/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-stage3:
+  # 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
+  75/jump-if-not-equal  $get-num:loop/disp8
+$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
+  ## 1. initialize 'in'
+  # 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
+  # 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
+  ## 2. 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
+  ## 1. initialize 'in'
+  # 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
+  # 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
+  ## 2. 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
+
 ## helpers
 
-expected:  # ed : (address exit-descriptor), f : fd or (address stream), s : (address array byte)
+# 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
@@ -97,4 +408,113 @@ expected:  # ed : (address exit-descriptor), f : fd or (address stream), s : (ad
   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 337b6866..75bdc6b5 100755
--- a/subx/test_apps
+++ b/subx/test_apps
@@ -145,9 +145,13 @@ CFLAGS=-g ./subx translate *.subx apps/crenshaw2-1.subx  -o apps/crenshaw2-1
 [ "$1" != record ]  &&  git diff --quiet apps/crenshaw2-1
 CFLAGS=-g ./subx run apps/crenshaw2-1 2>crenshaw2-1.out  ||  true
 test "`cat crenshaw2-1.out`" = 'Error: integer expected'
+CFLAGS=-g ./subx run apps/crenshaw2-1 test
+echo
 test `uname` = 'Linux'  &&  {
   apps/crenshaw2-1 2>crenshaw2-1.out  ||  true
   test "`cat crenshaw2-1.out`" = 'Error: integer expected'
+  apps/crenshaw2-1 test
+  echo
 }
 
 exit 0