about summary refs log tree commit diff stats
path: root/linux/calls.subx
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2021-03-03 22:09:50 -0800
committerKartik K. Agaram <vc@akkartik.com>2021-03-03 22:21:03 -0800
commit71e4f3812982dba2efb471283d310224e8db363e (patch)
treeea111a1acb8b8845dbda39c0e1b4bac1d198143b /linux/calls.subx
parentc6b928be29ac8cdb4e4d6e1eaa20420ff03e5a4c (diff)
downloadmu-71e4f3812982dba2efb471283d310224e8db363e.tar.gz
7842 - new directory organization
Baremetal is now the default build target and therefore has its sources
at the top-level. Baremetal programs build using the phase-2 Mu toolchain
that requires a Linux kernel. This phase-2 codebase which used to be at
the top-level is now under the linux/ directory. Finally, the phase-2 toolchain,
while self-hosting, has a way to bootstrap from a C implementation, which
is now stored in linux/bootstrap. The bootstrap C implementation uses some
literate programming tools that are now in linux/bootstrap/tools.

So the whole thing has gotten inverted. Each directory should build one
artifact and include the main sources (along with standard library). Tools
used for building it are relegated to sub-directories, even though those
tools are often useful in their own right, and have had lots of interesting
programs written using them.

A couple of things have gotten dropped in this process:
  - I had old ways to run on just a Linux kernel, or with a Soso kernel.
    No more.
  - I had some old tooling for running a single test at the cursor. I haven't
    used that lately. Maybe I'll bring it back one day.

The reorg isn't done yet. Still to do:
  - redo documentation everywhere. All the README files, all other markdown,
    particularly vocabulary.md.
  - clean up how-to-run comments at the start of programs everywhere
  - rethink what to do with the html/ directory. Do we even want to keep
    supporting it?

In spite of these shortcomings, all the scripts at the top-level, linux/
and linux/bootstrap are working. The names of the scripts also feel reasonable.
This is a good milestone to take stock at.
Diffstat (limited to 'linux/calls.subx')
-rw-r--r--linux/calls.subx1876
1 files changed, 1876 insertions, 0 deletions
diff --git a/linux/calls.subx b/linux/calls.subx
new file mode 100644
index 00000000..903ae0f2
--- /dev/null
+++ b/linux/calls.subx
@@ -0,0 +1,1876 @@
+# Function calls in a single line.
+#
+# To run (on Linux):
+#   $ ./translate_subx init.linux [012]*.subx apps/subx-params.subx apps/calls.subx
+#   $ mv a.elf apps/calls
+#
+# Example 1:
+#   $ echo '(foo %eax)'                         |  apps/calls
+#   # . (foo %eax)                                      # output has comments
+#   ff 6/subop/push %eax                                # push
+#   e8/call foo/disp32                                  # call
+#   81 0/subop/add %esp 4/imm32                         # undo push
+#
+# Example 2:
+#   $ echo '(foo Var1 *(eax + 4) "blah")'       |  apps/calls
+#   # . (foo Var1 *(eax + 4) "blah")
+#   68/push "blah"/imm32
+#   ff 6/subop/push *(eax + 4)                          # push args in..
+#   68/push Var1/imm32                                  # ..reverse order
+#   e8/call foo/disp32
+#   81 0/subop/add %esp 0xc/imm32                       # undo pushes
+#
+# Calls always begin with '(' as the first non-whitespace on a line.
+
+== code
+
+Entry:  # run tests if necessary, convert stdin if not
+    # . prologue
+    89/<- %ebp 4/r32/esp
+
+    # initialize heap
+    # . Heap = new-segment(Heap-size)
+    # . . push args
+    68/push Heap/imm32
+    ff 6/subop/push *Heap-size
+    # . . call
+    e8/call new-segment/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+
+    # - if argc > 1 and argv[1] == "test", then return run_tests()
+    # if (argc <= 1) goto run-main
+    81 7/subop/compare *ebp 1/imm32
+    7e/jump-if-<= $subx-calls-main:interactive/disp8
+    # if (!kernel-string-equal?(argv[1], "test")) goto run-main
+    # . eax = kernel-string-equal?(argv[1], "test")
+    # . . push args
+    68/push "test"/imm32
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call kernel-string-equal?/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . if (eax == false) goto run-main
+    3d/compare-eax-and 0/imm32/false
+    74/jump-if-= $subx-calls-main:interactive/disp8
+    # run-tests()
+    e8/call run-tests/disp32
+    # syscall(exit, *Num-test-failures)
+    8b/-> *Num-test-failures 3/r32/ebx
+    eb/jump $subx-calls-main:end/disp8
+$subx-calls-main:interactive:
+    # - otherwise convert stdin
+    # subx-calls(Stdin, Stdout)
+    # . . push args
+    68/push Stdout/imm32
+    68/push Stdin/imm32
+    # . . call
+    e8/call subx-calls/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # syscall(exit, 0)
+    bb/copy-to-ebx 0/imm32
+$subx-calls-main:end:
+    e8/call syscall_exit/disp32
+
+subx-calls:  # in: (addr buffered-file), out: (addr buffered-file)
+    # pseudocode:
+    #   var line: (stream byte 512)
+    #   var words: (stream slice 16)  # at most function name and 15 args
+    #   while true
+    #     clear-stream(line)
+    #     read-line-buffered(in, line)
+    #     if (line->write == 0) break                           # end of file
+    #     skip-chars-matching(line, ' ')
+    #     if line->data[line->read] != '('
+    #       write-stream-data(out, line)
+    #       continue
+    #     # emit comment
+    #     write-buffered(out, "# . ")
+    #     write-stream-data(out, line)
+    #     # emit code
+    #     ++line->read to skip '('
+    #     clear-stream(words)
+    #     words = parse-line(line)
+    #     emit-call(out, words)
+    #   flush(out)
+    #
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # . save registers
+    50/push-eax
+    51/push-ecx
+    52/push-edx
+    56/push-esi
+    # var line/esi: (stream byte 512)
+    81 5/subop/subtract %esp 0x200/imm32
+    68/push 0x200/imm32/length
+    68/push 0/imm32/read
+    68/push 0/imm32/write
+    89/<- %esi 4/r32/esp
+    # var words/edx: (stream slice 128)  # 16 rows * 8 bytes/row
+    81 5/subop/subtract %esp 0x80/imm32
+    68/push 0x80/imm32/length
+    68/push 0/imm32/read
+    68/push 0/imm32/write
+    89/<- %edx 4/r32/esp
+$subx-calls:loop:
+    # clear-stream(line)
+    # . . push args
+    56/push-esi
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # read-line-buffered(in, line)
+    # . . push args
+    56/push-esi
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call read-line-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+$subx-calls:check0:
+    # if (line->write == 0) break
+    81 7/subop/compare *esi 0/imm32
+    0f 84/jump-if-= $subx-calls:break/disp32
+    # skip-chars-matching(line, ' ')
+    # . . push args
+    68/push 0x20/imm32/space
+    56/push-esi
+    # . . call
+    e8/call skip-chars-matching/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # if (line->data[line->read] == '(') goto convert-call
+    # . ecx = line->read
+    8b/-> *(esi+4) 1/r32/ecx
+    # . eax = line->data[line->read]
+    31/xor-with %eax 0/r32/eax
+    8a/copy-byte *(esi+ecx+0xc) 0/r32/AL
+    # . if (eax == '(') goto convert-call
+    3d/compare-eax-and 0x28/imm32/open-paren
+    74/jump-if-= $subx-calls:convert-call/disp8
+$subx-calls:pass-through:
+    # write-stream-data(out, line)
+    # . . push args
+    56/push-esi
+    ff 6/subop/push *(ebp+0xc)
+    # . . call
+    e8/call write-stream-data/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # continue
+    eb/jump $subx-calls:loop/disp8
+$subx-calls:convert-call:
+    # - emit comment
+    # write-buffered(out, "# . ")
+    # . . push args
+    68/push "# . "/imm32
+    ff 6/subop/push *(ebp+0xc)
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # write-stream-data(out, line)
+    # . . push args
+    56/push-esi
+    ff 6/subop/push *(ebp+0xc)
+    # . . call
+    e8/call write-stream-data/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # - emit code
+    # ++line->read to skip '('
+    ff 0/subop/increment *(esi+4)
+    # clear-stream(words)
+    # . . push args
+    52/push-edx
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # words = parse-line(line)
+    # . . push args
+    52/push-edx
+    56/push-esi
+    # . . call
+    e8/call parse-line/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # emit-call(out, words)
+    # . . push args
+    52/push-edx
+    ff 6/subop/push *(ebp+0xc)
+    # . . call
+    e8/call emit-call/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # loop
+    e9/jump $subx-calls:loop/disp32
+$subx-calls:break:
+    # flush(out)
+    # . . push args
+    ff 6/subop/push *(ebp+0xc)
+    # . . call
+    e8/call flush/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+$subx-calls:end:
+    # . reclaim locals
+    81 0/subop/add %esp 0x298/imm32  # 0x20c + 0x8c
+    # . restore registers
+    5e/pop-to-esi
+    5a/pop-to-edx
+    59/pop-to-ecx
+    58/pop-to-eax
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+parse-line:  # line: (addr stream byte), words: (addr stream slice)
+    # pseudocode:
+    #   var word-slice: slice
+    #   while true
+    #     word-slice = next-word-string-or-expression-without-metadata(line)
+    #     if slice-empty?(word-slice)
+    #       break                                 # end of line
+    #     write-int(words, word-slice->start)
+    #     write-int(words, word-slice->end)
+    #
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # . save registers
+    51/push-ecx
+    # var word-slice/ecx: slice
+    68/push 0/imm32/end
+    68/push 0/imm32/start
+    89/<- %ecx 4/r32/esp
+$parse-line:loop:
+    # word-slice = next-word-string-or-expression-without-metadata(line)
+    # . . push args
+    51/push-ecx
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call next-word-string-or-expression-without-metadata/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+$parse-line:check1:
+    # if (slice-empty?(word-slice)) break
+    # . eax = slice-empty?(word-slice)
+    # . . push args
+    51/push-ecx
+    # . . call
+    e8/call slice-empty?/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . if (eax != false) break
+    3d/compare-eax-and 0/imm32/false
+    0f 85/jump-if-!= $parse-line:end/disp32
+#?     # dump word-slice {{{
+#?     # . write(2/stderr, "w: ")
+#?     # . . push args
+#?     68/push "w: "/imm32
+#?     68/push 2/imm32/stderr
+#?     # . . call
+#?     e8/call write/disp32
+#?     # . . discard args
+#?     81 0/subop/add %esp 8/imm32
+#?     # . clear-stream($Stderr->buffer)
+#?     # . . push args
+#?     68/push  $Stderr->buffer/imm32
+#?     # . . call
+#?     e8/call clear-stream/disp32
+#?     # . . discard args
+#?     81 0/subop/add %esp 4/imm32
+#?     # . write-slice-buffered(Stderr, word-slice)
+#?     # . . push args
+#?     51/push-ecx
+#?     68/push Stderr/imm32
+#?     # . . call
+#?     e8/call write-slice-buffered/disp32
+#?     # . . discard args
+#?     81 0/subop/add %esp 8/imm32
+#?     # . flush(Stderr)
+#?     # . . push args
+#?     68/push Stderr/imm32
+#?     # . . call
+#?     e8/call flush/disp32
+#?     # . . discard args
+#?     81 0/subop/add %esp 4/imm32
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push "$\n"/imm32
+#?     68/push 2/imm32/stderr
+#?     # . . call
+#?     e8/call write/disp32
+#?     # . . discard args
+#?     81 0/subop/add %esp 8/imm32
+#?     # }}}
+$parse-line:write-word:
+    # write-int(words, word-slice->start)
+    # . . push args
+    ff 6/subop/push *ecx
+    ff 6/subop/push *(ebp+0xc)
+    # . . call
+    e8/call write-int/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # write-int(words, word-slice->end)
+    # . . push args
+    ff 6/subop/push *(ecx+4)
+    ff 6/subop/push *(ebp+0xc)
+    # . . call
+    e8/call write-int/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # loop
+    e9/jump $parse-line:loop/disp32
+$parse-line:end:
+    # . reclaim locals
+    81 0/subop/add %esp 8/imm32
+    # . restore registers
+    59/pop-to-ecx
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+emit-call:  # out: (addr buffered-file), words: (addr stream slice)
+    # pseudocode:
+    #   if (words->write < 8) abort
+    #   curr = &words->data[words->write-8]
+    #   min = words->data
+    #   # emit pushes
+    #   while true
+    #     if (curr <= min) break
+    #     if slice-starts-with?(curr, "%x")    # floating-point register
+    #       write-buffered(out, "81 5/subop/subtract %esp 4/imm32\n")
+    #       write-buffered(out, "f3 0f 11/<- *esp ")
+    #       write-byte-buffered(out, *(curr->start+4))
+    #       write-buffered(out, "/x32\n")
+    #     else if *curr->start in '%' '*'
+    #       write-buffered(out, "ff 6/subop/push ")
+    #       write-slice-buffered(out, curr)
+    #       write-buffered(out, "\n")
+    #     else
+    #       write-buffered(out, "68/push ")
+    #       write-slice-buffered(out, curr)
+    #       write-buffered(out, "/imm32\n")
+    #     curr -= 8
+    #   # emit call
+    #   write-buffered(out, "e8/call ")
+    #   write-slice-buffered(out, curr)
+    #   write-buffered(out, "/disp32\n")
+    #   # emit pops
+    #   write-buffered(out, "81 0/subop/add %esp ")
+    #   write-int32-hex-buffered(out, words->write >> 1 - 4)
+    #   write-buffered(out, "/imm32\n")
+    #
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # . save registers
+    50/push-eax
+    51/push-ecx
+    52/push-edx
+    56/push-esi
+    # esi = words
+    8b/-> *(ebp+0xc) 6/r32/esi
+    # if (words->write < 8) abort
+    # . ecx = words->write - 8
+    8b/-> *esi 1/r32/ecx
+    81 5/subop/subtract %ecx 8/imm32
+    0f 8c/jump-if-< $emit-call:error1/disp32
+    # var curr/ecx: (addr slice) = &words->data[words->write-8]
+    8d/copy-address *(esi+ecx+0xc) 1/r32/ecx
+    # var min/edx: (addr byte) = words->data
+    8d/copy-address *(esi+0xc) 2/r32/edx
+    # - emit pushes
+$emit-call:push-loop:
+    # if (curr <= min) break
+    39/compare %ecx 2/r32/edx
+    0f 8e/jump-if-<= $emit-call:call-instruction/disp32
+    # if !slice-starts-with?(curr, "%x") goto next check
+    # . eax = slice-starts-with?(curr, "%x")
+    # . . push args
+    68/push "%x"/imm32
+    51/push-ecx
+    # . . call
+    e8/call slice-starts-with?/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . if (eax != 0) goto next check
+    3d/compare-eax-and 0/imm32/false
+    74/jump-if-= $emit-call:push-int/disp8
+    # emit floating-point push
+    # . write-buffered(out, "81 5/subop/subtract %esp 4/imm32\n")
+    # . . push args
+    68/push "81 5/subop/subtract %esp 4/imm32\n"/imm32
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-buffered(out, "f3 0f 11/<- *esp ")
+    # . . push args
+    68/push "f3 0f 11/<- *esp "/imm32
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-byte-buffered(out, *(curr->start+4))
+    # . . eax = *(curr->start+4)
+    8b/-> *ecx 0/r32/eax
+    8a/copy-byte *(eax+4) 0/r32/eax
+    81 4/subop/and %eax 0xff/imm32
+    # . . push args
+    50/push-eax
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-byte-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-buffered(out, "/x32\n")
+    68/push "/x32\n"/imm32
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . continue
+    e9/jump $emit-call:next-push/disp32
+$emit-call:push-int:
+    # if (*curr->start in '%' '*') goto push-rm32
+    # . var start/eax: (addr byte) = curr->start
+    8b/-> *ecx 0/r32/eax
+    # . var c/eax: byte = *eax
+    8b/-> *eax 0/r32/eax
+    81 4/subop/and %eax 0xff/imm32
+    # . if (c == '%') goto push-rm32
+    3d/compare-eax-and 0x25/imm32/percent
+    74/jump-if-= $emit-call:push-rm32/disp8
+    # . if (c == '*') goto push-rm32
+    3d/compare-eax-and 0x2a/imm32/asterisk
+    74/jump-if-= $emit-call:push-rm32/disp8
+$emit-call:push-imm32:
+    # write-buffered(out, "68/push ")
+    # . . push args
+    68/push "68/push "/imm32
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # write-slice-buffered(out, curr)
+    # . . push args
+    51/push-ecx
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-slice-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # write-buffered(out, "/imm32\n")
+    68/push "/imm32\n"/imm32
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # continue
+    eb/jump $emit-call:next-push/disp8
+$emit-call:push-rm32:
+    # write-buffered(out, "ff 6/subop/push ")
+    # . . push args
+    68/push "ff 6/subop/push "/imm32
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # write-slice-buffered(out, curr)
+    # . . push args
+    51/push-ecx
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-slice-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # write-buffered(out, "\n")
+    68/push Newline/imm32
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+$emit-call:next-push:
+    # curr -= 8
+    81 5/subop/subtract %ecx 8/imm32
+    # loop
+    e9/jump $emit-call:push-loop/disp32
+$emit-call:call-instruction:
+    # write-buffered(out, "e8/call ")
+    68/push "e8/call "/imm32
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # write-slice-buffered(out, curr)
+    # . . push args
+    51/push-ecx
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-slice-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # write-buffered(out, "/disp32\n")
+    68/push "/disp32\n"/imm32
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+$emit-call:pop-instruction:
+    # write-buffered(out, "81 0/subop/add %esp ")
+    68/push "81 0/subop/add %esp "/imm32
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # write-int32-hex-buffered(out, words->write >> 1 - 4)
+    # . . push args
+    8b/-> *esi 0/r32/eax
+    c1/shift 7/subop/arith-right %eax 1/imm8
+    2d/subtract-from-eax 4/imm32
+    50/push-eax
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-int32-hex-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # write-buffered(out, "/imm32\n")
+    68/push "/imm32\n"/imm32
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+$emit-call:end:
+    # . restore registers
+    5e/pop-to-esi
+    5a/pop-to-edx
+    59/pop-to-ecx
+    58/pop-to-eax
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+$emit-call:error1:
+    # print(stderr, "error: calls.subx: '()' is not a valid call")
+    # . write-buffered(Stderr, "error: calls.subx: '()' is not a valid call")
+    # . . push args
+    68/push "error: calls.subx: '()' is not a valid call"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . flush(Stderr)
+    # . . push args
+    68/push Stderr/imm32
+    # . . call
+    e8/call flush/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . syscall(exit, 1)
+    bb/copy-to-ebx 1/imm32
+    e8/call syscall_exit/disp32
+    # never gets here
+
+test-subx-calls-passes-most-lines-through:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . clear-stream($_test-input-buffered-file->buffer)
+    # . . push args
+    68/push  $_test-input-buffered-file->buffer/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . 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 %esp 4/imm32
+    # . clear-stream($_test-output-buffered-file->buffer)
+    # . . push args
+    68/push  $_test-output-buffered-file->buffer/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . write(_test-input-stream, "== abcd 0x1\n")
+    # . . push args
+    68/push "== abcd 0x1\n"/imm32
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call write/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # subx-calls(_test-input-buffered-file, _test-output-buffered-file)
+    # . . push args
+    68/push _test-output-buffered-file/imm32
+    68/push _test-input-buffered-file/imm32
+    # . . call
+    e8/call subx-calls/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # check that the line just passed through
+    # . flush(_test-output-buffered-file)
+    # . . push args
+    68/push _test-output-buffered-file/imm32
+    # . . call
+    e8/call flush/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . check-stream-equal(_test-output-stream, "== abcd 0x1\n", msg)
+    # . . push args
+    68/push "F - test-subx-calls-passes-most-lines-through"/imm32
+    68/push "== abcd 0x1\n"/imm32
+    68/push _test-output-stream/imm32
+    # . . call
+    e8/call check-stream-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-subx-calls-processes-calls:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . clear-stream($_test-input-buffered-file->buffer)
+    # . . push args
+    68/push  $_test-input-buffered-file->buffer/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . 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 %esp 4/imm32
+    # . clear-stream($_test-output-buffered-file->buffer)
+    # . . push args
+    68/push  $_test-output-buffered-file->buffer/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . write(_test-input-stream, "(foo %eax)\n")
+    # . . push args
+    68/push "(foo %eax)\n"/imm32
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call write/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # subx-calls(_test-input-buffered-file, _test-output-buffered-file)
+    # . . push args
+    68/push _test-output-buffered-file/imm32
+    68/push _test-input-buffered-file/imm32
+    # . . call
+    e8/call subx-calls/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # check that the line just passed through
+    # . flush(_test-output-buffered-file)
+    # . . push args
+    68/push _test-output-buffered-file/imm32
+    # . . call
+    e8/call flush/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+#?     # dump _test-output-stream {{{
+#?     # . write(2/stderr, "^")
+#?     # . . push args
+#?     68/push "^"/imm32
+#?     68/push 2/imm32/stderr
+#?     # . . call
+#?     e8/call write/disp32
+#?     # . . discard args
+#?     81 0/subop/add %esp 8/imm32
+#?     # . write-stream(2/stderr, _test-output-stream)
+#?     # . . push args
+#?     68/push _test-output-stream/imm32
+#?     68/push 2/imm32/stderr
+#?     # . . call
+#?     e8/call write-stream/disp32
+#?     # . . discard args
+#?     81 0/subop/add %esp 8/imm32
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push "$\n"/imm32
+#?     68/push 2/imm32/stderr
+#?     # . . call
+#?     e8/call write/disp32
+#?     # . . discard args
+#?     81 0/subop/add %esp 8/imm32
+#?     # . rewind-stream(_test-output-stream)
+#?     # . . push args
+#?     68/push _test-output-stream/imm32
+#?     # . . call
+#?     e8/call rewind-stream/disp32
+#?     # . . discard args
+#?     81 0/subop/add %esp 4/imm32
+#?     # }}}
+    # . check-next-stream-line-equal(_test-output-stream, "# . (foo %eax)", msg)
+    # . . push args
+    68/push "F - test-subx-calls-processes-calls: comment"/imm32
+    68/push "# . (foo %eax)"/imm32
+    68/push _test-output-stream/imm32
+    # . . call
+    e8/call check-next-stream-line-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . check-next-stream-line-equal(_test-output-stream, "ff 6/subop/push %eax", msg)
+    # . . push args
+    68/push "F - test-subx-calls-processes-calls: arg 0"/imm32
+    68/push "ff 6/subop/push %eax"/imm32
+    68/push _test-output-stream/imm32
+    # . . call
+    e8/call check-next-stream-line-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . check-next-stream-line-equal(_test-output-stream, "e8/call foo/disp32", msg)
+    # . . push args
+    68/push "F - test-subx-calls-processes-calls: call"/imm32
+    68/push "e8/call foo/disp32"/imm32
+    68/push _test-output-stream/imm32
+    # . . call
+    e8/call check-next-stream-line-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . check-next-stream-line-equal(_test-output-stream, "81 0/subop/add %esp 4/imm32", msg)
+    # . . push args
+    68/push "F - test-subx-calls-processes-calls: pops"/imm32
+    68/push "81 0/subop/add %esp 0x00000004/imm32"/imm32
+    68/push _test-output-stream/imm32
+    # . . call
+    e8/call check-next-stream-line-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+next-word-string-or-expression-without-metadata:  # line: (addr stream byte), out: (addr slice)
+    # pseudocode:
+    #   skip-chars-matching(line, ' ')
+    #   if line->read >= line->write              # end of line
+    #     out = {0, 0}
+    #     return
+    #   out->start = &line->data[line->read]
+    #   if line->data[line->read] == '#'          # comment
+    #     out->end = &line->data[line->write]     # skip to end of line
+    #     return
+    #   if line->data[line->read] == '"'          # string literal
+    #     skip-string(line)
+    #     out->end = &line->data[line->read]      # no metadata
+    #     return
+    #   if line->data[line->read] == '*'          # expression
+    #     if line->data[line->read + 1] == ' '
+    #       abort
+    #     if line->data[line->read + 1] == '('
+    #       skip-until-close-paren(line)
+    #       if (line->data[line->read] != ')'
+    #         abort
+    #       ++line->read to skip ')'
+    #     out->end = &line->data[line->read]
+    #     return
+    #   if line->data[line->read] == '('
+    #     abort
+    #   if line->data[line->read] == ')'
+    #     ++line->read to skip ')'
+    #     # make sure there's nothing else of importance
+    #     if line->read >= line->write
+    #       out = {0, 0}
+    #       return
+    #     if line->data[line->read] != ' '
+    #       abort
+    #     skip-chars-matching-whitespace(line)
+    #     if line->read >= line->write
+    #       out = {0, 0}
+    #       return
+    #     if line->data[line->read] == '#'        # only thing permitted after ')' is a comment
+    #       out = {0, 0}
+    #       return
+    #     abort
+    #   # default case: read a word -- but no metadata
+    #   while true
+    #     if line->read >= line->write
+    #       break
+    #     if line->data[line->read] == ' '
+    #       break
+    #     if line->data[line->read] == ')'
+    #       break
+    #     if line->data[line->read] == '/'
+    #       abort
+    #     ++line->read
+    #   out->end = &line->data[line->read]
+    #
+    # registers:
+    #   ecx: often line->read
+    #   eax: often line->data[line->read]
+    #
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # . save registers
+    50/push-eax
+    51/push-ecx
+    56/push-esi
+    57/push-edi
+    # esi = line
+    8b/-> *(ebp+8) 6/r32/esi
+    # edi = out
+    8b/-> *(ebp+0xc) 7/r32/edi
+    # skip-chars-matching(line, ' ')
+    # . . push args
+    68/push 0x20/imm32/space
+    ff 6/subop/push *(ebp+8)
+    # . . call
+    e8/call skip-chars-matching/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+$next-word-string-or-expression-without-metadata:check0:
+    # if (line->read >= line->write) abort because we didn't encounter a final ')'
+    # . ecx = line->read
+    8b/-> *(esi+4) 1/r32/ecx
+    # . if (ecx >= line->write) abort
+    3b/compare<- *esi 1/r32/ecx
+    0f 8d/jump-if->= $next-word-string-or-expression-without-metadata:error0/disp32
+$next-word-string-or-expression-without-metadata:check-for-comment:
+    # out->start = &line->data[line->read]
+    8d/copy-address *(esi+ecx+0xc) 0/r32/eax
+    89/<- *edi 0/r32/eax
+    # if (line->data[line->read] != '#') goto next check
+    # . var eax: byte = line->data[line->read]
+    31/xor-with %eax 0/r32/eax
+    8a/copy-byte *(esi+ecx+0xc) 0/r32/AL
+    # . if (eax != '#') goto next check
+    3d/compare-eax-and 0x23/imm32/pound
+    75/jump-if-!= $next-word-string-or-expression-without-metadata:check-for-string-literal/disp8
+$next-word-string-or-expression-without-metadata:comment:
+    # out->end = &line->data[line->write]
+    8b/-> *esi 0/r32/eax
+    8d/copy-address *(esi+eax+0xc) 0/r32/eax
+    89/<- *(edi+4) 0/r32/eax
+    # line->read = line->write  # skip rest of line
+    8b/-> *esi 0/r32/eax
+    89/<- *(esi+4) 0/r32/eax
+    # return
+    e9/jump $next-word-string-or-expression-without-metadata:end/disp32
+$next-word-string-or-expression-without-metadata:check-for-string-literal:
+    # if (line->data[line->read] != '"') goto next check
+    3d/compare-eax-and 0x22/imm32/dquote
+    75/jump-if-!= $next-word-string-or-expression-without-metadata:check-for-expression/disp8
+$next-word-string-or-expression-without-metadata:string-literal:
+    # skip-string(line)
+    # . . push args
+    56/push-esi
+    # . . call
+    e8/call skip-string/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # out->end = &line->data[line->read]
+    8b/-> *(esi+4) 1/r32/ecx
+    8d/copy-address *(esi+ecx+0xc) 0/r32/eax
+    89/<- *(edi+4) 0/r32/eax
+    # return
+    e9/jump $next-word-string-or-expression-without-metadata:end/disp32
+$next-word-string-or-expression-without-metadata:check-for-expression:
+    # if (line->data[line->read] != '*') goto next check
+    3d/compare-eax-and 0x2a/imm32/asterisk
+    75/jump-if-!= $next-word-string-or-expression-without-metadata:check-for-open-paren/disp8
+    # if (line->data[line->read + 1] == ' ') goto error1
+    8a/copy-byte *(esi+ecx+0xd) 0/r32/AL
+    3d/compare-eax-and 0x20/imm32/space
+    0f 84/jump-if-= $next-word-string-or-expression-without-metadata:error1/disp32
+    # if (line->data[line->read + 1] != '(') goto regular-word
+    3d/compare-eax-and 0x28/imm32/open-paren
+    0f 85/jump-if-!= $next-word-string-or-expression-without-metadata:regular-word-without-metadata/disp32
+$next-word-string-or-expression-without-metadata:paren:
+    # skip-until-close-paren(line)
+    # . . push args
+    56/push-esi
+    # . . call
+    e8/call skip-until-close-paren/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # if (line->data[line->read] != ')') goto error2
+    # . eax = line->data[line->read]
+    8b/-> *(esi+4) 1/r32/ecx
+    8a/copy-byte *(esi+ecx+0xc) 0/r32/AL
+    # . if (eax != ')') goto error2
+    3d/compare-eax-and 0x29/imm32/close-paren
+    0f 85/jump-if-!= $next-word-string-or-expression-without-metadata:error2/disp32
+    # ++line->read to skip ')'
+    ff 0/subop/increment *(esi+4)
+    # out->end = &line->data[line->read]
+    8b/-> *(esi+4) 1/r32/ecx
+    8d/copy-address *(esi+ecx+0xc) 0/r32/eax
+    89/<- *(edi+4) 0/r32/eax
+    # return
+    e9/jump $next-word-string-or-expression-without-metadata:end/disp32
+$next-word-string-or-expression-without-metadata:check-for-open-paren:
+    # if (line->data[line->read] == '(') abort
+    3d/compare-eax-and 0x28/imm32/open-paren
+    0f 84/jump-if-= $next-word-string-or-expression-without-metadata:error4a/disp32
+$next-word-string-or-expression-without-metadata:check-for-end-of-call:
+    # if (line->data[line->read] != ')') goto next check
+    3d/compare-eax-and 0x29/imm32/close-paren
+    75/jump-if-!= $next-word-string-or-expression-without-metadata:regular-word-without-metadata/disp8
+    # ++line->read to skip ')'
+    ff 0/subop/increment *(esi+4)
+    # - error checking: make sure there's nothing else of importance on the line
+    # if (line->read >= line->write) return out = {0, 0}
+    # . ecx = line->read
+    8b/-> *(esi+4) 1/r32/ecx
+    # . if (ecx >= line->write) return {0, 0}
+    3b/compare<- *esi 1/r32/ecx
+    0f 8d/jump-if->= $next-word-string-or-expression-without-metadata:return-eol/disp32
+    # if (line->data[line->read] == '/') goto error3
+    # . eax = line->data[line->read]
+    8a/copy-byte *(esi+ecx+0xc) 0/r32/AL
+    # . if (eax == '/') goto error3
+    3d/compare-eax-and 0x2f/imm32/slash
+    0f 84/jump-if-= $next-word-string-or-expression-without-metadata:error3/disp32
+    # skip-chars-matching-whitespace(line)
+    # . . push args
+    56/push-esi
+    # . . call
+    e8/call skip-chars-matching-whitespace/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # if (line->read >= line->write) return out = {0, 0}
+    # . ecx = line->read
+    8b/-> *(esi+4) 1/r32/ecx
+    # . if (ecx >= line->write) return {0, 0}
+    3b/compare<- *esi 1/r32/ecx
+    0f 8d/jump-if->= $next-word-string-or-expression-without-metadata:return-eol/disp32
+    # if (line->data[line->read] == '#') return out = {0, 0}
+    # . eax = line->data[line->read]
+    8b/-> *(esi+4) 1/r32/ecx
+    8a/copy-byte *(esi+ecx+0xc) 0/r32/AL
+    # . if (eax == '#') return out = {0, 0}
+    3d/compare-eax-and 0x23/imm32/pound
+    74/jump-if-= $next-word-string-or-expression-without-metadata:return-eol/disp8
+    # otherwise goto error4
+    e9/jump $next-word-string-or-expression-without-metadata:error4/disp32
+$next-word-string-or-expression-without-metadata:regular-word-without-metadata:
+    # if (line->read >= line->write) break
+    # . ecx = line->read
+    8b/-> *(esi+4) 1/r32/ecx
+    # . if (ecx >= line->write) break
+    3b/compare<- *esi 1/r32/ecx
+    7d/jump-if->= $next-word-string-or-expression-without-metadata:regular-word-break/disp8
+    # if (line->data[line->read] == ' ') break
+    # . eax = line->data[line->read]
+    8b/-> *(esi+4) 1/r32/ecx
+    8a/copy-byte *(esi+ecx+0xc) 0/r32/AL
+    # . if (eax == ' ') break
+    3d/compare-eax-and 0x20/imm32/space
+    74/jump-if-= $next-word-string-or-expression-without-metadata:regular-word-break/disp8
+    # if (line->data[line->read] == ')') break
+    3d/compare-eax-and 0x29/imm32/close-paren
+    0f 84/jump-if-= $next-word-string-or-expression-without-metadata:regular-word-break/disp32
+    # if (line->data[line->read] == '/') goto error5
+    3d/compare-eax-and 0x2f/imm32/slash
+    0f 84/jump-if-= $next-word-string-or-expression-without-metadata:error5/disp32
+    # ++line->read
+    ff 0/subop/increment *(esi+4)
+    # loop
+    e9/jump $next-word-string-or-expression-without-metadata:regular-word-without-metadata/disp32
+$next-word-string-or-expression-without-metadata:regular-word-break:
+    # out->end = &line->data[line->read]
+    8b/-> *(esi+4) 1/r32/ecx
+    8d/copy-address *(esi+ecx+0xc) 0/r32/eax
+    89/<- *(edi+4) 0/r32/eax
+    eb/jump $next-word-string-or-expression-without-metadata:end/disp8
+$next-word-string-or-expression-without-metadata:return-eol:
+    # return out = {0, 0}
+    c7 0/subop/copy *edi 0/imm32
+    c7 0/subop/copy *(edi+4) 0/imm32
+$next-word-string-or-expression-without-metadata:end:
+    # . restore registers
+    5f/pop-to-edi
+    5e/pop-to-esi
+    59/pop-to-ecx
+    58/pop-to-eax
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+$next-word-string-or-expression-without-metadata:error0:
+    # print(stderr, "error: missing final ')' in '" line "'")
+    # . write-buffered(Stderr, "error: missing final ')' in '")
+    # . . push args
+    68/push "error: missing final ')' in '"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-stream-data(Stderr, line)
+    # . . push args
+    56/push-esi
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-stream-data/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-buffered(Stderr, "'")
+    # . . push args
+    68/push "'"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . flush(Stderr)
+    # . . push args
+    68/push Stderr/imm32
+    # . . call
+    e8/call flush/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . syscall(exit, 1)
+    bb/copy-to-ebx 1/imm32
+    e8/call  syscall_exit/disp32
+    # never gets here
+
+$next-word-string-or-expression-without-metadata:error1:
+    # print(stderr, "error: no space allowed after '*' in '" line "'")
+    # . write-buffered(Stderr, "error: no space allowed after '*' in '")
+    # . . push args
+    68/push "error: no space allowed after '*' in '"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-stream-data(Stderr, line)
+    # . . push args
+    56/push-esi
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-stream-data/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-buffered(Stderr, "'")
+    # . . push args
+    68/push "'"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . flush(Stderr)
+    # . . push args
+    68/push Stderr/imm32
+    # . . call
+    e8/call flush/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . syscall(exit, 1)
+    bb/copy-to-ebx 1/imm32
+    e8/call syscall_exit/disp32
+    # never gets here
+
+$next-word-string-or-expression-without-metadata:error2:
+    # print(stderr, "error: *(...) expression must be all on a single line in '" line "'")
+    # . write-buffered(Stderr, "error: *(...) expression must be all on a single line in '")
+    # . . push args
+    68/push "error: *(...) expression must be all on a single line in '"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-stream-data(Stderr, line)
+    # . . push args
+    56/push-esi
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-stream-data/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-buffered(Stderr, "'")
+    # . . push args
+    68/push "'"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . flush(Stderr)
+    # . . push args
+    68/push Stderr/imm32
+    # . . call
+    e8/call flush/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . syscall(exit, 1)
+    bb/copy-to-ebx 1/imm32
+    e8/call syscall_exit/disp32
+    # never gets here
+
+$next-word-string-or-expression-without-metadata:error3:
+    # print(stderr, "error: no metadata after calls; just use a comment (in '" line "')")
+    # . write-buffered(Stderr, "error: no metadata after calls; just use a comment (in '")
+    # . . push args
+    68/push "error: no metadata after calls; just use a comment (in '"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-stream-data(Stderr, line)
+    # . . push args
+    56/push-esi
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-stream-data/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-buffered(Stderr, "')")
+    # . . push args
+    68/push "')"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . flush(Stderr)
+    # . . push args
+    68/push Stderr/imm32
+    # . . call
+    e8/call flush/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . syscall(exit, 1)
+    bb/copy-to-ebx 1/imm32
+    e8/call syscall_exit/disp32
+    # never gets here
+
+$next-word-string-or-expression-without-metadata:error4a:
+    # print(stderr, "error: unexpected '(' within call in '" line "'")
+    # . write-buffered(Stderr, "error: unexpected '(' within call in '")
+    # . . push args
+    68/push "error: unexpected '(' within call in '"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-stream-data(Stderr, line)
+    # . . push args
+    56/push-esi
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-stream-data/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-buffered(Stderr, "'\n")
+    # . . push args
+    68/push "'\n"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . flush(Stderr)
+    # . . push args
+    68/push Stderr/imm32
+    # . . call
+    e8/call flush/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . syscall(exit, 1)
+    bb/copy-to-ebx 1/imm32
+    e8/call syscall_exit/disp32
+    # never gets here
+
+$next-word-string-or-expression-without-metadata:error4:
+    # print(stderr, "error: unexpected text after end of call in '" line "'")
+    # . write-buffered(Stderr, "error: unexpected text after end of call in '")
+    # . . push args
+    68/push "error: unexpected text after end of call in '"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-stream-data(Stderr, line)
+    # . . push args
+    56/push-esi
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-stream-data/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-buffered(Stderr, "'")
+    # . . push args
+    68/push "'"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . flush(Stderr)
+    # . . push args
+    68/push Stderr/imm32
+    # . . call
+    e8/call flush/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . syscall(exit, 1)
+    bb/copy-to-ebx 1/imm32
+    e8/call syscall_exit/disp32
+    # never gets here
+
+$next-word-string-or-expression-without-metadata:error5:
+    # print(stderr, "error: no metadata anywhere in calls (in '" line "')")
+    # . write-buffered(Stderr, "error: no metadata anywhere in calls (in '")
+    # . . push args
+    68/push "error: no metadata anywhere in calls (in '"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-stream-data(Stderr, line)
+    # . . push args
+    56/push-esi
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-stream-data/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . write-buffered(Stderr, "')")
+    # . . push args
+    68/push "')"/imm32
+    68/push Stderr/imm32
+    # . . call
+    e8/call write-buffered/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # . flush(Stderr)
+    # . . push args
+    68/push Stderr/imm32
+    # . . call
+    e8/call flush/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # . syscall(exit, 1)
+    bb/copy-to-ebx 1/imm32
+    e8/call syscall_exit/disp32
+    # never gets here
+
+test-next-word-string-or-expression-without-metadata:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # var slice/ecx: slice
+    68/push 0/imm32/end
+    68/push 0/imm32/start
+    89/<- %ecx 4/r32/esp
+    # write(_test-input-stream, "  ab")
+    # . . push args
+    68/push "  ab"/imm32
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call write/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
+    # . . push args
+    51/push-ecx
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call next-word-string-or-expression-without-metadata/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # check-ints-equal(_test-input-stream->read, 4, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata/updates-stream-read-correctly"/imm32
+    68/push 4/imm32
+    b8/copy-to-eax _test-input-stream/imm32
+    ff 6/subop/push *(eax+4)
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # check-ints-equal(slice->start - _test-input-stream->data, 2, msg)
+    # . check-ints-equal(slice->start - _test-input-stream, 14, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata: start"/imm32
+    68/push 0xe/imm32
+    # . . push slice->start - _test-input-stream
+    8b/-> *ecx 0/r32/eax
+    81 5/subop/subtract %eax _test-input-stream/imm32
+    50/push-eax
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # check-ints-equal(slice->end - _test-input-stream->data, 4, msg)
+    # . check-ints-equal(slice->end - _test-input-stream, 16, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata: end"/imm32
+    68/push 0x10/imm32
+    # . . push slice->end - _test-input-stream
+    8b/-> *(ecx+4) 0/r32/eax
+    81 5/subop/subtract %eax _test-input-stream/imm32
+    50/push-eax
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-next-word-string-or-expression-without-metadata-returns-whole-comment:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # var slice/ecx: slice
+    68/push 0/imm32/end
+    68/push 0/imm32/start
+    89/<- %ecx 4/r32/esp
+    # write(_test-input-stream, "  # a")
+    # . . push args
+    68/push "  # a"/imm32
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call write/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
+    # . . push args
+    51/push-ecx
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call next-word-string-or-expression-without-metadata/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # check-ints-equal(_test-input-stream->read, 5, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-returns-whole-comment/updates-stream-read-correctly"/imm32
+    68/push 5/imm32
+    b8/copy-to-eax _test-input-stream/imm32
+    ff 6/subop/push *(eax+4)
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # check-ints-equal(slice->start - _test-input-stream->data, 2, msg)
+    # . check-ints-equal(slice->start - _test-input-stream, 14, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-returns-whole-comment: start"/imm32
+    68/push 0xe/imm32
+    # . . push slice->start - _test-input-stream
+    8b/-> *ecx 0/r32/eax
+    81 5/subop/subtract %eax _test-input-stream/imm32
+    50/push-eax
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # check-ints-equal(slice->end - _test-input-stream->data, 5, msg)
+    # . check-ints-equal(slice->end - _test-input-stream, 17, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-returns-whole-comment: end"/imm32
+    68/push 0x11/imm32
+    # . . push slice->end - _test-input-stream
+    8b/-> *(ecx+4) 0/r32/eax
+    81 5/subop/subtract %eax _test-input-stream/imm32
+    50/push-eax
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-next-word-string-or-expression-without-metadata-returns-string-literal:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # var slice/ecx: slice
+    68/push 0/imm32/end
+    68/push 0/imm32/start
+    89/<- %ecx 4/r32/esp
+    # write(_test-input-stream, " \"a b\" ")
+    # . . push args
+    68/push " \"a b\" "/imm32
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call write/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
+    # . . push args
+    51/push-ecx
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call next-word-string-or-expression-without-metadata/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # check-ints-equal(slice->start - _test-input-stream->data, 1, msg)
+    # . check-ints-equal(slice->start - _test-input-stream, 13, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-returns-string-literal: start"/imm32
+    68/push 0xd/imm32
+    # . . push slice->start - _test-input-stream
+    8b/-> *ecx 0/r32/eax
+    81 5/subop/subtract %eax _test-input-stream/imm32
+    50/push-eax
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # check-ints-equal(slice->end - _test-input-stream->data, 6, msg)
+    # . check-ints-equal(slice->end - _test-input-stream, 18, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-returns-string-literal: end"/imm32
+    68/push 0x12/imm32
+    # . . push slice->end - _test-input-stream
+    8b/-> *(ecx+4) 0/r32/eax
+    81 5/subop/subtract %eax _test-input-stream/imm32
+    50/push-eax
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-next-word-string-or-expression-without-metadata-returns-string-with-escapes:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # var slice/ecx: slice
+    68/push 0/imm32/end
+    68/push 0/imm32/start
+    89/<- %ecx 4/r32/esp
+    # write(_test-input-stream, " \"a\\\"b\"")
+    # . . push args
+    68/push " \"a\\\"b\""/imm32
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call write/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
+    # . . push args
+    51/push-ecx
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call next-word-string-or-expression-without-metadata/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # check-ints-equal(slice->start - _test-input-stream->data, 1, msg)
+    # . check-ints-equal(slice->start - _test-input-stream, 13, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-returns-string-with-escapes: start"/imm32
+    68/push 0xd/imm32
+    # . . push slice->start - _test-input-stream
+    8b/-> *ecx 0/r32/eax
+    81 5/subop/subtract %eax _test-input-stream/imm32
+    50/push-eax
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # check-ints-equal(slice->end - _test-input-stream->data, 7, msg)
+    # . check-ints-equal(slice->end - _test-input-stream, 19, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-returns-string-with-escapes: end"/imm32
+    68/push 0x13/imm32
+    # . . push slice->end - _test-input-stream
+    8b/-> *(ecx+4) 0/r32/eax
+    81 5/subop/subtract %eax _test-input-stream/imm32
+    50/push-eax
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-next-word-string-or-expression-without-metadata-returns-whole-expression:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # var slice/ecx: slice
+    68/push 0/imm32/end
+    68/push 0/imm32/start
+    89/<- %ecx 4/r32/esp
+    # write(_test-input-stream, " *(a b) ")
+    # . . push args
+    68/push " *(a b) "/imm32
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call write/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
+    # . . push args
+    51/push-ecx
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call next-word-string-or-expression-without-metadata/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # check-ints-equal(slice->start - _test-input-stream->data, 1, msg)
+    # . check-ints-equal(slice->start - _test-input-stream, 13, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-returns-whole-expression: start"/imm32
+    68/push 0xd/imm32
+    # . . push slice->start - _test-input-stream
+    8b/-> *ecx 0/r32/eax
+    81 5/subop/subtract %eax _test-input-stream/imm32
+    50/push-eax
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # check-ints-equal(slice->end - _test-input-stream->data, 7, msg)
+    # . check-ints-equal(slice->end - _test-input-stream, 19, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-returns-whole-expression: end"/imm32
+    68/push 0x13/imm32
+    # . . push slice->end - _test-input-stream
+    8b/-> *(ecx+4) 0/r32/eax
+    81 5/subop/subtract %eax _test-input-stream/imm32
+    50/push-eax
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-next-word-string-or-expression-without-metadata-returns-eol-on-trailing-close-paren:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # var slice/ecx: slice
+    68/push 0/imm32/end
+    68/push 0/imm32/start
+    89/<- %ecx 4/r32/esp
+    # write(_test-input-stream, " ) ")
+    # . . push args
+    68/push " ) "/imm32
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call write/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
+    # . . push args
+    51/push-ecx
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call next-word-string-or-expression-without-metadata/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # check-ints-equal(slice->start, 0, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-returns-eol-on-trailing-close-paren: start"/imm32
+    68/push 0/imm32
+    ff 6/subop/push *ecx
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # check-ints-equal(slice->end, 0, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-returns-eol-on-trailing-close-paren: end"/imm32
+    68/push 0/imm32
+    ff 6/subop/push *(ecx+4)
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-next-word-string-or-expression-without-metadata-handles-comment-after-trailing-close-paren:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # var slice/ecx: slice
+    68/push 0/imm32/end
+    68/push 0/imm32/start
+    89/<- %ecx 4/r32/esp
+    # write(_test-input-stream, " ) # abc ")
+    # . . push args
+    68/push " ) # abc "/imm32
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call write/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
+    # . . push args
+    51/push-ecx
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call next-word-string-or-expression-without-metadata/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # check-ints-equal(slice->start, 0, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-handles-comment-after-trailing-close-paren: start"/imm32
+    68/push 0/imm32
+    ff 6/subop/push *ecx
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # check-ints-equal(slice->end, 0, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-handles-comment-after-trailing-close-paren: end"/imm32
+    68/push 0/imm32
+    ff 6/subop/push *(ecx+4)
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-next-word-string-or-expression-without-metadata-handles-newline-after-trailing-close-paren:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # var slice/ecx: slice
+    68/push 0/imm32/end
+    68/push 0/imm32/start
+    89/<- %ecx 4/r32/esp
+    # write(_test-input-stream, " )\n")
+    # . . push args
+    68/push " )\n"/imm32
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call write/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
+    # . . push args
+    51/push-ecx
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call next-word-string-or-expression-without-metadata/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # check-ints-equal(slice->start, 0, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-handles-newline-after-trailing-close-paren: start"/imm32
+    68/push 0/imm32
+    ff 6/subop/push *ecx
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # check-ints-equal(slice->end, 0, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-handles-newline-after-trailing-close-paren: end"/imm32
+    68/push 0/imm32
+    ff 6/subop/push *(ecx+4)
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return
+
+test-next-word-string-or-expression-without-metadata-stops-at-close-paren:
+    # . prologue
+    55/push-ebp
+    89/<- %ebp 4/r32/esp
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call clear-stream/disp32
+    # . . discard args
+    81 0/subop/add %esp 4/imm32
+    # var slice/ecx: slice
+    68/push 0/imm32/end
+    68/push 0/imm32/start
+    89/<- %ecx 4/r32/esp
+    # write(_test-input-stream, " abc) # def")
+    # . . push args
+    68/push " abc) # def"/imm32
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call write/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
+    # . . push args
+    51/push-ecx
+    68/push _test-input-stream/imm32
+    # . . call
+    e8/call next-word-string-or-expression-without-metadata/disp32
+    # . . discard args
+    81 0/subop/add %esp 8/imm32
+    # check-ints-equal(slice->start - _test-input-stream->data, 1, msg)
+    # . check-ints-equal(slice->start - _test-input-stream, 13, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-stops-at-close-paren: start"/imm32
+    68/push 0xd/imm32
+    # . . push slice->start - _test-input-stream
+    8b/-> *ecx 0/r32/eax
+    81 5/subop/subtract %eax _test-input-stream/imm32
+    50/push-eax
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # check-ints-equal(slice->end - _test-input-stream->data, 4, msg)
+    # . check-ints-equal(slice->end - _test-input-stream, 16, msg)
+    # . . push args
+    68/push "F - test-next-word-string-or-expression-without-metadata-stops-at-close-paren: end"/imm32
+    68/push 0x10/imm32
+    # . . push slice->end - _test-input-stream
+    8b/-> *(ecx+4) 0/r32/eax
+    81 5/subop/subtract %eax _test-input-stream/imm32
+    50/push-eax
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81 0/subop/add %esp 0xc/imm32
+    # . epilogue
+    89/<- %esp 5/r32/ebp
+    5d/pop-to-ebp
+    c3/return