about summary refs log tree commit diff stats
path: root/subx
diff options
context:
space:
mode:
Diffstat (limited to 'subx')
-rw-r--r--subx/056trace.subx2
-rw-r--r--subx/058stream-equal.subx1
-rw-r--r--subx/064write-byte.subx2
-rw-r--r--subx/069allocate.subx2
-rw-r--r--subx/070new-stream.subx2
-rw-r--r--subx/072slice.subx4
-rw-r--r--subx/074print-int-decimal.subx2
-rw-r--r--subx/Readme.md36
-rwxr-xr-xsubx/apps/assortbin28457 -> 28463 bytes
-rwxr-xr-xsubx/apps/crenshaw2-1bin23301 -> 23307 bytes
-rwxr-xr-xsubx/apps/crenshaw2-1bbin23860 -> 23866 bytes
-rwxr-xr-xsubx/apps/dquotesbin25223 -> 34772 bytes
-rw-r--r--subx/apps/dquotes.subx1095
-rwxr-xr-xsubx/apps/factorialbin22217 -> 22223 bytes
-rw-r--r--subx/apps/factorial.subx2
-rwxr-xr-xsubx/apps/handlebin23023 -> 23082 bytes
-rw-r--r--subx/apps/handle.subx52
-rwxr-xr-xsubx/apps/hexbin26310 -> 26316 bytes
-rwxr-xr-xsubx/apps/packbin43371 -> 43377 bytes
-rw-r--r--subx/stats.md13
-rwxr-xr-xsubx/test_apps18
21 files changed, 966 insertions, 265 deletions
diff --git a/subx/056trace.subx b/subx/056trace.subx
index 71d72796..417c24ce 100644
--- a/subx/056trace.subx
+++ b/subx/056trace.subx
@@ -962,7 +962,7 @@ $_append-4:end:
 $_append-4:abort:
     # . _write(2/stderr, error)
     # . . push args
-    68/push  "stream overflow"/imm32
+    68/push  "stream overflow\n"/imm32
     68/push  2/imm32/stderr
     # . . call
     e8/call  _write/disp32
diff --git a/subx/058stream-equal.subx b/subx/058stream-equal.subx
index c65112cc..68296212 100644
--- a/subx/058stream-equal.subx
+++ b/subx/058stream-equal.subx
@@ -26,6 +26,7 @@ stream-data-equal?:  # f : (address stream), s : (address string) -> EAX : boole
     # EDI = s
     8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           7/r32/EDI   0xc/disp8       .                 # copy *(EBP+12) to EDI
     # if (f->write != s->length) return false
+$stream-data-equal?:compare-lengths:
     39/compare                      0/mod/indirect  7/rm32/EDI    .           .             .           0/r32/EAX   .               .                 # compare *EDI and EAX
     75/jump-if-not-equal  $stream-data-equal?:false/disp8
     # currs/EDI = s->data
diff --git a/subx/064write-byte.subx b/subx/064write-byte.subx
index 606cc7d3..057b9164 100644
--- a/subx/064write-byte.subx
+++ b/subx/064write-byte.subx
@@ -243,7 +243,7 @@ $append-byte:end:
 $append-byte:abort:
     # . _write(2/stderr, error)
     # . . push args
-    68/push  "append-byte: out of space"/imm32
+    68/push  "append-byte: out of space\n"/imm32
     68/push  2/imm32/stderr
     # . . call
     e8/call  _write/disp32
diff --git a/subx/069allocate.subx b/subx/069allocate.subx
index ac491ac3..e262b4d5 100644
--- a/subx/069allocate.subx
+++ b/subx/069allocate.subx
@@ -177,7 +177,7 @@ allocate-region:  # ad : (address allocation-descriptor), n : int -> new-ad : (a
 $allocate-region:abort:
     # . _write(2/stderr, error)
     # . . push args
-    68/push  "allocate-region: failed to allocate"/imm32
+    68/push  "allocate-region: failed to allocate\n"/imm32
     68/push  2/imm32/stderr
     # . . call
     e8/call  _write/disp32
diff --git a/subx/070new-stream.subx b/subx/070new-stream.subx
index 8a833581..b6934b1e 100644
--- a/subx/070new-stream.subx
+++ b/subx/070new-stream.subx
@@ -52,7 +52,7 @@ $new-stream:end:
 $new-stream:abort:
     # . _write(2/stderr, error)
     # . . push args
-    68/push  "new-stream: size too large"/imm32
+    68/push  "new-stream: size too large\n"/imm32
     68/push  2/imm32/stderr
     # . . call
     e8/call  _write/disp32
diff --git a/subx/072slice.subx b/subx/072slice.subx
index 3aff095d..c9a7fe4a 100644
--- a/subx/072slice.subx
+++ b/subx/072slice.subx
@@ -1007,7 +1007,7 @@ $slice-to-string:end:
 $slice-to-string:abort:
     # . _write(2/stderr, error)
     # . . push args
-    68/push  "slice-to-string: out of space"/imm32
+    68/push  "slice-to-string: out of space\n"/imm32
     68/push  2/imm32/stderr
     # . . call
     e8/call  _write/disp32
@@ -1107,4 +1107,4 @@ _test-slice-data-3:
     64/d
 _test-slice-data-4:
 
-# . _. vim:nowrap:textwidth=0
+# . . vim:nowrap:textwidth=0
diff --git a/subx/074print-int-decimal.subx b/subx/074print-int-decimal.subx
index 57daad01..f6ea490f 100644
--- a/subx/074print-int-decimal.subx
+++ b/subx/074print-int-decimal.subx
@@ -116,7 +116,7 @@ $print-int32-decimal:end:
 $print-int32-decimal:abort:
     # . _write(2/stderr, error)
     # . . push args
-    68/push  "print-int32-decimal: out of space"/imm32
+    68/push  "print-int32-decimal: out of space\n"/imm32
     68/push  2/imm32/stderr
     # . . call
     e8/call  _write/disp32
diff --git a/subx/Readme.md b/subx/Readme.md
index cab1a503..cb00eae0 100644
--- a/subx/Readme.md
+++ b/subx/Readme.md
@@ -44,8 +44,10 @@ Emulated runs generate a trace that permits [time-travel debugging](https://gith
   $ ./subx --debug translate examples/factorial.subx -o examples/factorial
   saving address->label information to 'labels'
   saving address->source information to 'source_lines'
+
   $ ./subx --debug --trace run examples/factorial
   saving trace to 'last_run'
+
   $ ../browse_trace/browse_trace last_run  # text-mode debugger UI
   ```
 
@@ -101,10 +103,11 @@ a few registers:
 * Six general-purpose 32-bit registers: EAX, EBX, ECX, EDX, ESI and EDI
 * Two additional 32-bit registers: ESP and EBP (I suggest you only use these to
   manage the call stack.)
-* Three 1-bit _flag_ registers for conditional branching:
+* Four 1-bit _flag_ registers for conditional branching:
   - zero/equal flag ZF
   - sign flag SF
   - overflow flag OF
+  - carry flag CF
 
 SubX programs consist of instructions like `89/copy`, `01/add`, `3d/compare`
 and `52/push-ECX` which modify these registers as well as a byte-addressable
@@ -152,19 +155,19 @@ _addressing mode_. This is a 2-bit argument that can take 4 possible values,
 and it determines what other arguments are required, and how to interpret
 them.
 
-* If `/mod` is `3`: the operand is the register described by the 3-bit `/rm32`
-  argument similarly to `/r32` above.
+* If `/mod` is `3`: the operand is in the register described by the 3-bit
+  `/rm32` argument similarly to `/r32` above.
 
-* If `/mod` is `0`: the operand is the address provided in the register
+* If `/mod` is `0`: the operand is in the address provided in the register
   described by `/rm32`. That's `*rm32` in C syntax.
 
-* If `/mod` is `1`: the operand is the address provided by adding the register
-  in `/rm32` with the (1-byte) displacement. That's `*(rm32 + disp8)` in C
-  syntax.
+* If `/mod` is `1`: the operand is in the address provided by adding the
+  register in `/rm32` with the (1-byte) displacement. That's `*(rm32 + disp8)`
+  in C syntax.
 
-* If `/mod` is `2`: the operand is the address provided by adding the register
-  in `/rm32` with the (4-byte) displacement. That's `*(/rm32 + disp32)` in C
-  syntax.
+* If `/mod` is `2`: the operand is in the address provided by adding the
+  register in `/rm32` with the (4-byte) displacement. That's `*(/rm32 +
+  disp32)` in C syntax.
 
 In the last three cases, one exception occurs when the `/rm32` argument
 contains `4`. Rather than encoding register `ESP`, it means the address is
@@ -215,9 +218,10 @@ This program sums the first 10 natural numbers. By convention I use horizontal
 tabstops to help read instructions, dots to help follow the long lines,
 comments before groups of instructions to describe their high-level purpose,
 and comments at the end of complex instructions to state the low-level
-operation they perform. Numbers are always in hexadecimal (base 16); the '0x'
-prefix is optional, and I tend to include it as a reminder when numbers look
-like decimal numbers or words.
+operation they perform. Numbers are always in hexadecimal (base 16) and must
+start with a digit ('0'..'9'); use the '0x' prefix when a number starts with a
+letter ('a'..'f'). I tend to also include it as a reminder when numbers look
+like decimal numbers.
 
 Try running this example now:
 
@@ -337,9 +341,9 @@ runnable on a Linux system running on Intel x86 processors, either 32- or
 
   1. [Converting ascii hex bytes to binary.](http://akkartik.github.io/mu/html/subx/apps/hex.subx.html) (✓)
   2. [Packing bitfields for x86 instructions into bytes.](http://akkartik.github.io/mu/html/subx/apps/pack.subx.html) (✓)
-  3. [Combining segments with the same name.](apps/assort.subx) (✓)
-  4. Support for string literals. (10% complete)
-  5. Replacing addresses with labels.
+  3. [Combining segments with the same name.](http://akkartik.github.io/mu/html/subx/apps/assort.subx.html) (✓)
+  4. [Support for string literals.](http://akkartik.github.io/mu/html/subx/apps/dquotes.subx.html) (✓)
+  5. [Replacing addresses with labels.](https://github.com/akkartik/mu/pull/34) (10% complete)
 
 * Testable, dependency-injected vocabulary of primitives
   - Streams: `read()`, `write()`. (✓)
diff --git a/subx/apps/assort b/subx/apps/assort
index 37585b8c..9d27fd9a 100755
--- a/subx/apps/assort
+++ b/subx/apps/assort
Binary files differdiff --git a/subx/apps/crenshaw2-1 b/subx/apps/crenshaw2-1
index 7caeb9af..159ed81a 100755
--- a/subx/apps/crenshaw2-1
+++ b/subx/apps/crenshaw2-1
Binary files differdiff --git a/subx/apps/crenshaw2-1b b/subx/apps/crenshaw2-1b
index c065c1f5..f4e02da6 100755
--- a/subx/apps/crenshaw2-1b
+++ b/subx/apps/crenshaw2-1b
Binary files differdiff --git a/subx/apps/dquotes b/subx/apps/dquotes
index 4f7a6bec..6774b8df 100755
--- a/subx/apps/dquotes
+++ b/subx/apps/dquotes
Binary files differdiff --git a/subx/apps/dquotes.subx b/subx/apps/dquotes.subx
index b9c06c16..64742599 100644
--- a/subx/apps/dquotes.subx
+++ b/subx/apps/dquotes.subx
@@ -90,7 +90,7 @@ convert:  # in : (address buffered-file), out : (address buffered-file) -> <void
     #     read-line-buffered(in, line)
     #     if (line->write == 0) break               # end of file
     #     while true
-    #       var word-slice = next-word(line)
+    #       var word-slice = next-word-or-string(line)
     #       if slice-empty?(word-slice)             # end of line
     #         break
     #       if slice-starts-with?(word-slice, "#")  # comment
@@ -165,12 +165,12 @@ $convert:check0:
     81          7/subop/compare     0/mod/indirect  1/rm32/ECX    .           .             .           .           .               0/imm32           # compare *ECX
     0f 84/jump-if-equal  $convert:break/disp32
 $convert:word-loop:
-    # next-word(line, word-slice)
+    # next-word-or-string(line, word-slice)
     # . . push args
     52/push-EDX
     51/push-ECX
     # . . call
-    e8/call  next-word/disp32
+    e8/call  next-word-or-string/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 $convert:check1:
@@ -731,39 +731,39 @@ test-convert-processes-string-literals:
     # called. We just want to make sure instructions using string literals
     # switch to a string variable with the right value.
     # (Modifying string literals completely off the radar for now.)
-    # dump output {{{
-    # . write(2/stderr, "result: ^")
-    # . . push args
-    68/push  "result: ^"/imm32
-    68/push  2/imm32/stderr
-    # . . call
-    e8/call  write/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # . 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # . 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # . 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # }}}
+#?     # dump output {{{
+#?     # . write(2/stderr, "result: ^")
+#?     # . . push args
+#?     68/push  "result: ^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # }}}
     # . check-next-stream-line-equal(_test-output-stream, "== code 0x1 ", msg)
     # . . push args
     68/push  "F - test-convert-processes-string-literals/0"/imm32
@@ -791,10 +791,10 @@ test-convert-processes-string-literals:
     e8/call  check-next-stream-line-equal/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . check-next-stream-line-equal(_test-output-stream, "== data ", msg)
+    # . check-next-stream-line-equal(_test-output-stream, "== data", msg)
     # . . push args
     68/push  "F - test-convert-processes-string-literals/3"/imm32
-    68/push  "== data "/imm32
+    68/push  "== data"/imm32
     68/push  _test-output-stream/imm32
     # . . call
     e8/call  check-next-stream-line-equal/disp32
@@ -844,15 +844,15 @@ test-convert-processes-string-literals:
 # generate the data segment contents byte by byte for a given slice
 emit-string-literal-data:  # out : (address stream), word : (address slice)
     # pseudocode
-    #   var len = word->end - word->start - 2  # ignore the double-quotes
-    #   append-int32-hex(out, len)
-    #   write(out, "/imm32")
+    #   len = string-length-at-start-of-slice(word->start, word->end)
+    #   print(out, "#{len}/imm32 ")
     #   curr = word->start
     #   ++curr  # skip '"'
     #   while true
     #     if (curr >= word->end) break
     #     c = *curr
     #     if (c == '"') break
+    #     if (c == '\') ++curr, c = *curr
     #     append-byte-hex(out, c)
     #     if c is alphanumeric:
     #       write(out, "/")
@@ -872,16 +872,21 @@ emit-string-literal-data:  # out : (address stream), word : (address slice)
     8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   0xc/disp8       .                 # copy *(EBP+12) to ESI
     # curr/EDX = word->start
     8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           2/r32/EDX   .               .                 # copy *ESI to EDX
+    # max/ESI = word->end
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           6/r32/ESI   4/disp8         .                 # copy *(ESI+4) to ESI
 $emit-string-literal-data:emit-length:
-    # TODO: handle metadata here
+    # len/EAX = string-length-at-start-of-slice(word->start, word->end)
+    # . . push args
+    56/push-ESI
+    52/push-EDX
+    # . . call
+    e8/call  string-length-at-start-of-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
     # print(out, "#{len}/imm32 ")
-    # . len/ECX = word->end - word->start - 2
-    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(ESI+4) to ECX
-    29/subtract                     3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # subtract EDX from ECX
-    81          5/subop/subtract    3/mod/direct    1/rm32/ECX    .           .             .           .           .               2/imm32           # subtract from ECX
     # . print-int32(out, len)
     # . . push args
-    51/push-ECX
+    50/push-EAX
     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
     # . . call
     e8/call  print-int32/disp32
@@ -898,9 +903,7 @@ $emit-string-literal-data:emit-length:
 $emit-string-literal-data:loop-init:
     # ++curr  # skip initial '"'
     42/increment-EDX
-    # max/ESI = word->end
-    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           6/r32/ESI   4/disp8         .                 # copy *(ESI+4) to ESI
-    # ECX = 0
+    # c/ECX = 0
     31/xor                          3/mod/direct    1/rm32/ECX    .           .             .           1/r32/ECX   .               .                 # clear ECX
 $emit-string-literal-data:loop:
     # if (curr >= max) break
@@ -911,6 +914,17 @@ $emit-string-literal-data:loop:
     # if (ECX == '"') break
     81          7/subop/compare     3/mod/direct    1/rm32/ECX    .           .             .           .           .               0x22/imm32/dquote # compare ECX
     74/jump-if-equal  $emit-string-literal-data:end/disp8
+    # if (ECX == '\') ++curr, ECX = *curr
+    81          7/subop/compare     3/mod/direct    1/rm32/ECX    .           .             .           .           .               0x5c/imm32/backslash  # compare ECX
+    75/jump-if-not-equal  $emit-string-literal-data:emit/disp8
+    # . ++curr
+    42/increment-EDX
+    # . if (curr >= max) break
+    39/compare                      3/mod/direct    2/rm32/EDX    .           .             .           6/r32/ESI   .               .                 # compare EDX with ESI
+    7d/jump-if-greater-or-equal  $emit-string-literal-data:end/disp8
+    # . CL = *curr
+    8a/copy-byte                    0/mod/indirect  2/rm32/EDX    .           .             .           1/r32/CL    .               .                 # copy byte at *EDX to CL
+$emit-string-literal-data:emit:
     # append-byte-hex(out, CL)
     # . . push args
     51/push-ECX
@@ -1017,8 +1031,8 @@ test-emit-string-literal-data:
     e8/call  clear-stream/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = '"abc"'
-    68/push  _test-slice-abc-end/imm32
+    # var slice/ECX = '"abc"/d'
+    68/push  _test-slice-abc-metadata-end/imm32
     68/push  _test-slice-abc/imm32
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # emit-string-literal-data(_test-output-stream, slice)
@@ -1251,7 +1265,7 @@ test-emit-string-literal-data-handles-escape-sequences:
     # . check-stream-equal(_test-output-stream, "3/imm32 61/a 22 62/b ", msg)
     # . . push args
     68/push  "F - test-emit-string-literal-data-handles-escape-sequences"/imm32
-    68/push  "3/imm32 61/a 22 62/b "/imm32
+    68/push  "0x00000003/imm32 61/a 22 62/b "/imm32
     68/push  _test-output-stream/imm32
     # . . call
     e8/call  check-stream-equal/disp32
@@ -1264,6 +1278,21 @@ test-emit-string-literal-data-handles-escape-sequences:
 
 # emit everything from a word except the initial datum
 emit-metadata:  # out : (address buffered-file), word : (address slice)
+    # pseudocode
+    #   var slice = {0, word->end}
+    #   curr = word->start
+    #   if *curr == '"'
+    #     curr = skip-string-in-slice(curr, word->end)
+    #   else
+    #     while true
+    #       if curr == word->end
+    #         return
+    #       if *curr == '/'
+    #         break
+    #       ++curr
+    #   slice->curr = curr
+    #   write-slice-buffered(out, slice)
+    #
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -1271,59 +1300,66 @@ emit-metadata:  # out : (address buffered-file), word : (address slice)
     50/push-EAX
     51/push-ECX
     52/push-EDX
-
-    # PSEUDOCODE
-    # ECX = (char *) word->start
-    # while true:
-    #   if ECX == word->end: return
-    #   if *(ECX++) == '/': break
-    # write-slice-buffered(out, {ECX, word->end})
-
-    # ECX = word
-    8b/copy/word                    1/mod/*+disp8   5/rm32/EBP    .           .             .           1/r32/ECX   0xc/disp8       .                 # copy *(EBP+12) to ECX
-    # EDX = word->end
-    8b/copy/word->end               1/mod/*+disp8   1/rm32/ECX    .           .             .           2/r32/EDX   4/disp8         .                 # copy *(ECX+4) to EDX
-    # ECX = word->start
-    8b/copy/word->start             0/mod/indirect  1/rm32/ECX    .           .             .           1/r32/ECX   .               .                 # copy *ECX to ECX
-
-    # clear out EAX
-    b8/copy-to-EAX 0/imm32
-    # while *start != '/':
-$skip-datum-loop:
-    # . start == end?
-    39/compare-ECX-and              3/mod/direct    2/rm32/EDX    .           .             .           1/r32/ECX   .               .                 # EDX == ECX
-    # . if so, return from function (it's only datum, or empty)
-    74/jump-if-equal  $emit-metadata:end/disp8
-
-    # . start++
-    41/increment-ECX                                                                                                                                  # ECX++
-
-    # . EAX = *start
-    8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
-
-    # . EAX != '/'?
-    3d/compare-EAX-and  0x2f/imm32
-    # . if so, continue looping
-    75/jump-if-not-equal  $skip-datum-loop/disp8
-    # end
-
-    # write-slice-buffered(out, &{start, end})
-    # . push end
+    53/push-EBX
+    56/push-ESI
+    # ESI = word
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   0xc/disp8       .                 # copy *(EBP+12) to ESI
+    # curr/ECX = word->start
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # copy *ESI to ECX
+    # end/EDX = word->end
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           2/r32/EDX   4/disp8         .                 # copy *(ESI+4) to EDX
+    # var slice/EBX = {0, end}
+    52/push-EDX
+    68/push  0/imm32
+    89/copy                         3/mod/direct    3/rm32/EBX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBX
+    # EAX = 0
+    b8/copy-to-EAX  0/imm32
+$emit-metadata:check-for-string-literal:
+    # -  if (*curr == '"') curr = skip-string-in-slice(curr, end)
+    8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/AL    .               .                 # copy byte at *ECX to AL
+    3d/compare-EAX-and  0x22/imm32/dquote
+    75/jump-if-not-equal  $emit-metadata:skip-datum-loop/disp8
+$emit-metadata:skip-string-literal:
+    # . EAX = skip-string-in-slice(curr, end)
+    # . . push args
     52/push-EDX
-    # . push start
     51/push-ECX
-    # . push &{start, end}
-    54/push-ESP
-
-    # . push out
+    # . . call
+    e8/call  skip-string-in-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . curr = EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy EAX to ECX
+    eb/jump  $emit-metadata:emit/disp8
+$emit-metadata:skip-datum-loop:
+    # - otherwise scan for '/'
+    # if (curr == end) return
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX and EDX
+    74/jump-if-equal  $emit-metadata:end/disp8
+    # if (*curr == '/') break
+    8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/AL    .               .                 # copy byte at *ECX to AL
+    3d/compare-EAX-and  0x2f/imm32/slash
+    74/jump-if-equal  $emit-metadata:emit/disp8
+    # ++curr
+    41/increment-ECX
+    eb/jump  $emit-metadata:skip-datum-loop/disp8
+$emit-metadata:emit:
+    # slice->curr = ECX
+    89/copy                         0/mod/indirect  3/rm32/EBX    .           .             .           1/r32/ECX   .               .                 # copy ECX to *EBX
+    # write-slice-buffered(out, slice)
+    # . . push args
+    53/push-EBX
     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
-
+    # . . call
     e8/call  write-slice-buffered/disp32
-    # . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           0x10/imm32      .                 # add 16 to ESP
-
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           8/imm32      .                    # add to ESP
 $emit-metadata:end:
+    # . reclaim locals
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           8/imm32      .                    # add to ESP
     # . restore registers
+    5e/pop-to-ESI
+    5b/pop-to-EBX
     5a/pop-to-EDX
     59/pop-to-ECX
     58/pop-to-EAX
@@ -1494,9 +1530,150 @@ test-emit-metadata-multiple:
     5d/pop-to-EBP
     c3/return
 
+test-emit-metadata-when-no-datum:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . 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-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-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
+    # var slice/ECX = "/abc"
+    b8/copy-to-EAX  "/abc"/imm32
+    # . push end/ECX
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    51/push-ECX
+    # . push curr/EAX
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . save stack pointer
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # emit-metadata(_test-output-buffered-file, slice)
+    # . . push args
+    51/push-ECX
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit-metadata/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-stream-equal(_test-output-stream, "/abc", msg)  # nothing skipped
+    # . . push args
+    68/push  "F - test-emit-metadata-when-no-datum"/imm32
+    68/push  "/abc"/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-emit-metadata-in-string-literal:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . 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-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-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
+    # var slice/ECX = "\"abc/def\"/ghi"
+    68/push  _test-slice-literal-string-with-metadata-end/imm32
+    68/push  _test-slice-literal-string/imm32/start
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # emit-metadata(_test-output-buffered-file, slice)
+    # . . push args
+    51/push-ECX
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit-metadata/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # dump output {{{
+#?     # . write(2/stderr, "result: ^")
+#?     # . . push args
+#?     68/push  "result: ^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # check-stream-equal(_test-output-stream, "/ghi", msg)  # important that there's no leading space
+    # . . push args
+    68/push  "F - test-emit-metadata-in-string-literal"/imm32
+    68/push  "/ghi"/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
 # (re)compute the bounds of the next word in the line
 # return empty string on reaching end of file
-next-word:  # line : (address stream byte), out : (address slice)
+next-word-or-string:  # line : (address stream byte), out : (address slice)
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -1517,18 +1694,18 @@ next-word:  # line : (address stream byte), out : (address slice)
     e8/call  skip-chars-matching/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-$next-word:check0:
+$next-word-or-string:check0:
     # if (line->read >= line->write) clear out and return
     # . EAX = line->read
     8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ESI+4) to EAX
     # . if (EAX < line->write) goto next check
     3b/compare                      0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # compare EAX with *ESI
-    7c/jump-if-lesser  $next-word:check-for-comment/disp8
+    7c/jump-if-lesser  $next-word-or-string:check-for-comment/disp8
     # . return out = {0, 0}
     c7          0/subop/copy        0/mod/direct    7/rm32/EDI    .           .             .           .           .               0/imm32           # copy to *EDI
     c7          0/subop/copy        1/mod/*+disp8   7/rm32/EDI    .           .             .           .           4/disp8         0/imm32           # copy to *(EDI+4)
-    eb/jump  $next-word:end/disp8
-$next-word:check-for-comment:
+    eb/jump  $next-word-or-string:end/disp8
+$next-word-or-string:check-for-comment:
     # out->start = &line->data[line->read]
     8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(ESI+4) to ECX
     8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+ECX+12 to EAX
@@ -1539,8 +1716,8 @@ $next-word:check-for-comment:
     8a/copy-byte                    1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/AL    0xc/disp8       .                 # copy byte at *(ESI+ECX+12) to AL
     # . compare
     3d/compare-EAX-and  0x23/imm32/pound
-    75/jump-if-not-equal  $next-word:check-for-string-literal/disp8
-$next-word:comment:
+    75/jump-if-not-equal  $next-word-or-string:check-for-string-literal/disp8
+$next-word-or-string:comment:
     # out->end = &line->data[line->write]
     8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # copy *ESI to EAX
     8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  0/index/EAX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+EAX+12 to EAX
@@ -1549,32 +1726,26 @@ $next-word:comment:
     8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # copy *ESI to EAX
     89/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(ESI+4)
     # return
-    eb/jump  $next-word:end/disp8
-$next-word:check-for-string-literal:
+    eb/jump  $next-word-or-string:end/disp8
+$next-word-or-string:check-for-string-literal:
     # if line->data[line->read] == '"'
     # . EAX = line->data[line->read]
     31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
     8a/copy-byte                    1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/AL    0xc/disp8       .                 # copy byte at *(ESI+ECX+12) to AL
     # . compare
     3d/compare-EAX-and  0x22/imm32/dquote
-    75/jump-if-not-equal  $next-word:regular-word/disp8
-$next-word:string-literal:
-    # ++line->read  # skip '"'
-    # . persist line->read
-    89/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy ECX to *(ESI+4)
-    # . ++line->read
-    ff          0/subop/increment   1/mod/*+disp8   6/rm32/ESI    .           .             .           .           4/disp8         .                 # increment *(ESI+4)
-    # parse-string(line, out)
+    75/jump-if-not-equal  $next-word-or-string:regular-word/disp8
+$next-word-or-string:string-literal:
+    # skip-string(line)
     # . . push args
-    57/push-EDI
     56/push-ESI
     # . . call
-    e8/call  parse-string/disp32
+    e8/call  skip-string/disp32
     # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
     # fall through
-$next-word:regular-word:
-    # otherwise skip-chars-not-matching-whitespace(line)  # including trailing newline
+$next-word-or-string:regular-word:
+    # skip-chars-not-matching-whitespace(line)  # including trailing newline
     # . . push args
     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
     # . . call
@@ -1585,7 +1756,7 @@ $next-word:regular-word:
     8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(ESI+4) to ECX
     8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+ECX+12 to EAX
     89/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(EDI+4)
-$next-word:end:
+$next-word-or-string:end:
     # . restore registers
     5f/pop-to-EDI
     5e/pop-to-ESI
@@ -1596,81 +1767,7 @@ $next-word:end:
     5d/pop-to-EBP
     c3/return
 
-parse-string:  # line : (address stream byte), out : (address slice)
-    # pseudocode:
-    #   ESI = line
-    #   curr/ECX = line->data[line->read]
-    #   max/EDX = line->data[line->write]
-    #   while curr >= max
-    #     if (*curr == '"') ++curr, break
-    #     if (*curr == '\\') curr+=2, continue
-    #     ++curr
-    #   line->read = curr - line->data
-    #   out->end = curr
-    #
-    # . 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
-    52/push-EDX
-    56/push-ESI
-    # ESI = line
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
-    # curr/ECX = &table->data[table->read]
-    # . ECX = table->read
-    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(ESI+4) to ECX
-    # . ECX = table->data + ECX
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           1/r32/ECX   0xc/disp8       .                 # copy ESI+ECX+12 to ECX
-    # max/EDX = &table->data[table->write]
-    # . EDX = table->write
-    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           2/r32/EDX   .               .                 # copy *ESI to EDX
-    # . EDX = table->data + EDX
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  2/index/EDX   .           2/r32/EDX   0xc/disp8       .                 # copy ESI+EDX+12 to EDX
-    # clear EAX
-    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
-$parse-string:loop:
-    # if (curr >= max) break
-    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
-    7d/jump-if-greater-or-equal  $parse-string:break/disp8
-    # c/EAX = *curr
-    8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/AL    .               .                 # copy byte at *ECX to AL
-$parse-string:check1:
-    # if (c == '"') break  # rely on caller to skip trailing non-whitespace
-    3d/compare-EAX-and  0x22/imm32/dquote
-    74/jump-if-equal  $parse-string:break/disp8
-$parse-string:check2:
-    # if (c == '\\') ++curr
-    3d/compare-EAX-and  0x5c/imm32/backslash
-    75/jump-if-not-equal  $parse-string:continue/disp8
-    # . ++curr
-    41/increment-ECX
-$parse-string:continue:
-    # ++curr
-    41/increment-ECX
-    # loop
-    eb/jump  $parse-string:loop/disp8
-$parse-string:break:
-    # out->end = curr
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           0/r32/EAX   0xc/disp8       .                 # copy *(EBP+12) to EAX
-    89/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           1/r32/ECX   4/disp8         .                 # copy ECX to *(EAX+4)
-    # line->read = curr - line - 12
-    29/subtract                     3/mod/direct    1/rm32/ECX    .           .             .           6/r32/ESI   .               .                 # subtract ESI from ECX
-    81          5/subop/subtract    3/mod/direct    1/rm32/ECX    .           .             .           .           .               0xc/imm32         # subtract from ECX
-    89/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy ECX to *(ESI+4)
-$parse-string:end:
-    # . restore registers
-    5e/pop-to-ESI
-    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-next-word:
+test-next-word-or-string:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -1694,17 +1791,17 @@ test-next-word:
     e8/call  write/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # next-word(_test-input-stream, slice)
+    # next-word-or-string(_test-input-stream, slice)
     # . . push args
     51/push-ECX
     68/push  _test-input-stream/imm32
     # . . call
-    e8/call  next-word/disp32
+    e8/call  next-word-or-string/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
     # check-ints-equal(_test-input-stream->read, 4, msg)
     # . . push args
-    68/push  "F - test-next-word/updates-stream-read-correctly"/imm32
+    68/push  "F - test-next-word-or-string/updates-stream-read-correctly"/imm32
     68/push  4/imm32
     b8/copy-to-EAX  _test-input-stream/imm32
     ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           4/disp8         .                 # push *(EAX+4)
@@ -1715,7 +1812,7 @@ test-next-word:
     # 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: start"/imm32
+    68/push  "F - test-next-word-or-string: start"/imm32
     68/push  0xe/imm32
     # . . push slice->start - _test-input-stream
     8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
@@ -1728,7 +1825,7 @@ test-next-word:
     # 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: end"/imm32
+    68/push  "F - test-next-word-or-string: end"/imm32
     68/push  0x10/imm32
     # . . push slice->end - _test-input-stream
     8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
@@ -1743,7 +1840,7 @@ test-next-word:
     5d/pop-to-EBP
     c3/return
 
-test-next-word-returns-whole-comment:
+test-next-word-or-string-returns-whole-comment:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -1767,17 +1864,17 @@ test-next-word-returns-whole-comment:
     e8/call  write/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # next-word(_test-input-stream, slice)
+    # next-word-or-string(_test-input-stream, slice)
     # . . push args
     51/push-ECX
     68/push  _test-input-stream/imm32
     # . . call
-    e8/call  next-word/disp32
+    e8/call  next-word-or-string/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
     # check-ints-equal(_test-input-stream->read, 5, msg)
     # . . push args
-    68/push  "F - test-next-word-returns-whole-comment/updates-stream-read-correctly"/imm32
+    68/push  "F - test-next-word-or-string-returns-whole-comment/updates-stream-read-correctly"/imm32
     68/push  5/imm32
     b8/copy-to-EAX  _test-input-stream/imm32
     ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           4/disp8         .                 # push *(EAX+4)
@@ -1788,7 +1885,7 @@ test-next-word-returns-whole-comment:
     # 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-returns-whole-comment: start"/imm32
+    68/push  "F - test-next-word-or-string-returns-whole-comment: start"/imm32
     68/push  0xe/imm32
     # . . push slice->start - _test-input-stream
     8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
@@ -1801,7 +1898,7 @@ test-next-word-returns-whole-comment:
     # 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-returns-whole-comment: end"/imm32
+    68/push  "F - test-next-word-or-string-returns-whole-comment: end"/imm32
     68/push  0x11/imm32
     # . . push slice->end - _test-input-stream
     8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
@@ -1816,7 +1913,7 @@ test-next-word-returns-whole-comment:
     5d/pop-to-EBP
     c3/return
 
-test-next-word-returns-empty-string-on-eof:
+test-next-word-or-string-returns-empty-string-on-eof:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -1833,17 +1930,17 @@ test-next-word-returns-empty-string-on-eof:
     68/push  0/imm32/start
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # write nothing to _test-input-stream
-    # next-word(_test-input-stream, slice)
+    # next-word-or-string(_test-input-stream, slice)
     # . . push args
     51/push-ECX
     68/push  _test-input-stream/imm32
     # . . call
-    e8/call  next-word/disp32
+    e8/call  next-word-or-string/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
     # check-ints-equal(slice->end - slice->start, 0, msg)
     # . . push args
-    68/push  "F - test-next-word-returns-empty-string-on-eof"/imm32
+    68/push  "F - test-next-word-or-string-returns-empty-string-on-eof"/imm32
     68/push  0/imm32
     # . . push slice->end - slice->start
     8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
@@ -1858,7 +1955,7 @@ test-next-word-returns-empty-string-on-eof:
     5d/pop-to-EBP
     c3/return
 
-test-next-word-returns-whole-string:
+test-next-word-or-string-returns-whole-string:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -1882,18 +1979,18 @@ test-next-word-returns-whole-string:
     e8/call  write/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # next-word(_test-input-stream, slice)
+    # next-word-or-string(_test-input-stream, slice)
     # . . push args
     51/push-ECX
     68/push  _test-input-stream/imm32
     # . . call
-    e8/call  next-word/disp32
+    e8/call  next-word-or-string/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
     # 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-returns-whole-string: start"/imm32
+    68/push  "F - test-next-word-or-string-returns-whole-string: start"/imm32
     68/push  0xd/imm32
     # . . push slice->start - _test-input-stream
     8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
@@ -1906,7 +2003,7 @@ test-next-word-returns-whole-string:
     # check-ints-equal(slice->end - _test-input-stream->data, 12, msg)
     # . check-ints-equal(slice->end - _test-input-stream, 24, msg)
     # . . push args
-    68/push  "F - test-next-word-returns-whole-string: end"/imm32
+    68/push  "F - test-next-word-or-string-returns-whole-string: end"/imm32
     68/push  0x18/imm32
     # . . push slice->end - _test-input-stream
     8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
@@ -1921,7 +2018,7 @@ test-next-word-returns-whole-string:
     5d/pop-to-EBP
     c3/return
 
-test-next-word-returns-string-with-escapes:
+test-next-word-or-string-returns-string-with-escapes:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -1945,18 +2042,18 @@ test-next-word-returns-string-with-escapes:
     e8/call  write/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # next-word(_test-input-stream, slice)
+    # next-word-or-string(_test-input-stream, slice)
     # . . push args
     51/push-ECX
     68/push  _test-input-stream/imm32
     # . . call
-    e8/call  next-word/disp32
+    e8/call  next-word-or-string/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
     # 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-returns-string-with-escapes: start"/imm32
+    68/push  "F - test-next-word-or-string-returns-string-with-escapes: start"/imm32
     68/push  0xd/imm32
     # . . push slice->start - _test-input-stream
     8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
@@ -1969,7 +2066,7 @@ test-next-word-returns-string-with-escapes:
     # check-ints-equal(slice->end - _test-input-stream->data, 9, msg)
     # . check-ints-equal(slice->end - _test-input-stream, 21, msg)
     # . . push args
-    68/push  "F - test-next-word-returns-string-with-escapes: end"/imm32
+    68/push  "F - test-next-word-or-string-returns-string-with-escapes: end"/imm32
     68/push  0x15/imm32
     # . . push slice->end - _test-input-stream
     8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
@@ -1984,6 +2081,546 @@ test-next-word-returns-string-with-escapes:
     5d/pop-to-EBP
     c3/return
 
+# update line->read to end of string literal surrounded by double quotes
+# line->read must start out at a double-quote
+skip-string:  # line : (address stream)
+    # . 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
+    52/push-EDX
+    # ECX = line
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         1/r32/ECX   8/disp8         .                 # copy *(EBP+8) to ECX
+    # EAX = skip-string-in-slice(&line->data[line->read], &line->data[line->write])
+    # . . push &line->data[line->write]
+    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .                         2/r32/EDX   8/disp8         .                 # copy *(ECX+8) to EDX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    1/base/ECX  2/index/EDX   .           2/r32/EDX   0xc/disp8       .                 # copy ECX+EDX+12 to EDX
+    52/push-EDX
+    # . . push &line->data[line->read]
+    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .                         2/r32/EDX   4/disp8         .                 # copy *(ECX+4) to EDX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    1/base/ECX  2/index/EDX   .           2/r32/EDX   0xc/disp8       .                 # copy ECX+EDX+12 to EDX
+    52/push-EDX
+    # . . call
+    e8/call  skip-string-in-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # line->read = EAX - line->data
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EAX
+    2d/subtract-from-EAX  0xc/imm32
+    89/copy                         1/mod/*+disp8   1/rm32/ECX    .           .                         0/r32/EAX   4/disp8         .                 # copy EAX to *(ECX+4)
+$skip-string:end:
+    # . restore registers
+    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-skip-string:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . write(_test-input-stream, "\"abc\" def")
+    # .                   indices:  0123 45
+    # . . push args
+    68/push  "\"abc\" def"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # precondition: line->read == 0
+    # . . push args
+    68/push  "F - test-skip-string/precondition"/imm32
+    68/push  0/imm32
+    b8/copy-to-EAX  _test-input-stream/imm32
+    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
+    # skip-string(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  skip-string/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(line->read, 5, msg)
+    # . . push args
+    68/push  "F - test-skip-string"/imm32
+    68/push  5/imm32
+    b8/copy-to-EAX  _test-input-stream/imm32
+    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
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-skip-string-ignores-spaces:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . write(_test-input-stream, "\"a b\"/yz")
+    # .                   indices:  0123 45
+    # . . push args
+    68/push  "\"a b\"/yz"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # precondition: line->read == 0
+    # . . push args
+    68/push  "F - test-skip-string-ignores-spaces/precondition"/imm32
+    68/push  0/imm32
+    b8/copy-to-EAX  _test-input-stream/imm32
+    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
+    # skip-string(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  skip-string/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(line->read, 5, msg)
+    # . . push args
+    68/push  "F - test-skip-string-ignores-spaces"/imm32
+    68/push  5/imm32
+    b8/copy-to-EAX  _test-input-stream/imm32
+    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
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-skip-string-ignores-escapes:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . write(_test-input-stream, "\"a\\\"b\"/yz")
+    # .                   indices:  01 2 34 56
+    # . . push args
+    68/push  "\"a\\\"b\"/yz"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # precondition: line->read == 0
+    # . . push args
+    68/push  "F - test-skip-string-ignores-escapes/precondition"/imm32
+    68/push  0/imm32
+    b8/copy-to-EAX  _test-input-stream/imm32
+    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
+    # skip-string(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  skip-string/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(line->read, 6, msg)
+    # . . push args
+    68/push  "F - test-skip-string-ignores-escapes"/imm32
+    68/push  6/imm32
+    b8/copy-to-EAX  _test-input-stream/imm32
+    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
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-skip-string-works-from-mid-stream:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # 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         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . write(_test-input-stream, "0 \"a\\\"b\"/yz")
+    # .                   indices:  01 2 34 56
+    # . . push args
+    68/push  "0 \"a\\\"b\"/yz"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # precondition: line->read == 2
+    c7          0/subop/copy        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           4/disp8         2/imm32           # copy to *(EAX+4)
+    # skip-string(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  skip-string/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(line->read, 8, msg)
+    # . . push args
+    68/push  "F - test-skip-string-works-from-mid-stream"/imm32
+    68/push  8/imm32
+    b8/copy-to-EAX  _test-input-stream/imm32
+    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
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+skip-string-in-slice:  # curr : (address byte), end : (address byte) -> new_curr/EAX
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    52/push-EDX
+    53/push-EBX
+    # ECX = curr
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         1/r32/ECX   8/disp8         .                 # copy *(EBP+8) to ECX
+    # EDX = end
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         2/r32/EDX   0xc/disp8         .               # copy *(EBP+12) to EDX
+    # EAX = 0
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+    # skip initial dquote
+    41/increment-ECX
+$skip-string-in-slice:loop:
+    # if (curr >= end) return curr
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
+    73/jump-if-greater-unsigned-or-equal  $skip-string-in-slice:return-curr/disp8
+    # AL = *curr
+    8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/AL    .               .                 # copy byte at *ECX to AL
+$skip-string-in-slice:dquote:
+    # if (EAX == '"') break
+    3d/compare-EAX-and  0x22/imm32/double-quote
+    74/jump-if-equal  $skip-string-in-slice:break/disp8
+$skip-string-in-slice:check-for-escape:
+    # if (EAX == '\') escape next char
+    3d/compare-EAX-and  0x5c/imm32/backslash
+    75/jump-if-not-equal  $skip-string-in-slice:continue/disp8
+$skip-string-in-slice:escape:
+    41/increment-ECX
+$skip-string-in-slice:continue:
+    # ++curr
+    41/increment-ECX
+    eb/jump  $skip-string-in-slice:loop/disp8
+$skip-string-in-slice:break:
+    # skip final dquote
+    41/increment-ECX
+$skip-string-in-slice:return-curr:
+    # return curr
+    89/copy                         3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy ECX to EAX
+$skip-string-in-slice:end:
+    # . restore registers
+    5b/pop-to-EBX
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-skip-string-in-slice:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup: (EAX..ECX) = "\"abc\" def"
+    b8/copy-to-EAX  "\"abc\" def"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # EAX = skip-string-in-slice(EAX, ECX)
+    # . . push args
+    51/push-ECX
+    50/push-EAX
+    # . . call
+    e8/call  skip-string-in-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(ECX-EAX, 4, msg)  # number of chars remaining after the string literal
+    # . . push args
+    68/push  "F - test-skip-string-in-slice"/imm32
+    68/push  4/imm32
+    # . . push ECX-EAX
+    29/subtract                     3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # subtract EAX from ECX
+    51/push-ECX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-skip-string-in-slice-ignores-spaces:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup: (EAX..ECX) = "\"a b\"/yz"
+    b8/copy-to-EAX  "\"a b\"/yz"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # EAX = skip-string-in-slice(EAX, ECX)
+    # . . push args
+    51/push-ECX
+    50/push-EAX
+    # . . call
+    e8/call  skip-string-in-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(ECX-EAX, 3, msg)  # number of chars remaining after the string literal
+    # . . push args
+    68/push  "F - test-skip-string-in-slice-ignores-spaces"/imm32
+    68/push  3/imm32
+    # . . push ECX-EAX
+    29/subtract                     3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # subtract EAX from ECX
+    51/push-ECX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-skip-string-in-slice-ignores-escapes:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup: (EAX..ECX) = "\"a\\\"b\"/yz"
+    b8/copy-to-EAX  "\"a\\\"b\"/yz"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # EAX = skip-string-in-slice(EAX, ECX)
+    # . . push args
+    51/push-ECX
+    50/push-EAX
+    # . . call
+    e8/call  skip-string-in-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(ECX-EAX, 3, msg)  # number of chars remaining after the string literal
+    # . . push args
+    68/push  "F - test-skip-string-in-slice-ignores-escapes"/imm32
+    68/push  3/imm32
+    # . . push ECX-EAX
+    29/subtract                     3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # subtract EAX from ECX
+    51/push-ECX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-skip-string-in-slice-stops-at-end:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup: (EAX..ECX) = "\"abc"  # unbalanced dquote
+    b8/copy-to-EAX  "\"abc"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # EAX = skip-string-in-slice(EAX, ECX)
+    # . . push args
+    51/push-ECX
+    50/push-EAX
+    # . . call
+    e8/call  skip-string-in-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(ECX-EAX, 0, msg)  # skipped to end of slice
+    # . . push args
+    68/push  "F - test-skip-string-in-slice-stops-at-end"/imm32
+    68/push  0/imm32
+    # . . push ECX-EAX
+    29/subtract                     3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # subtract EAX from ECX
+    51/push-ECX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+string-length-at-start-of-slice:  # curr : (address byte), end : (address byte) -> length/EAX
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    52/push-EDX
+    53/push-EBX
+    # ECX = curr
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         1/r32/ECX   8/disp8         .                 # copy *(EBP+8) to ECX
+    # EDX = end
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         2/r32/EDX   0xc/disp8         .               # copy *(EBP+12) to EDX
+    # length/EAX = 0
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+    # EBX = 0
+    31/xor                          3/mod/direct    3/rm32/EBX    .           .             .           3/r32/EBX   .               .                 # clear EBX
+    # skip initial dquote
+    41/increment-ECX
+$string-length-at-start-of-slice:loop:
+    # if (curr >= end) return length
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
+    73/jump-if-greater-unsigned-or-equal  $string-length-at-start-of-slice:end/disp8
+    # BL = *curr
+    8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           3/r32/BL    .               .                 # copy byte at *ECX to BL
+$string-length-at-start-of-slice:dquote:
+    # if (EBX == '"') break
+    81          7/subop/compare     3/mod/direct    3/rm32/EBX    .           .             .           .           .               0x22/imm32/dquote # compare EBX
+    74/jump-if-equal  $string-length-at-start-of-slice:end/disp8
+$string-length-at-start-of-slice:check-for-escape:
+    # if (EBX == '\') escape next char
+    81          7/subop/compare     3/mod/direct    3/rm32/EBX    .           .             .           .           .               0x5c/imm32/backslash # compare EBX
+    75/jump-if-not-equal  $string-length-at-start-of-slice:continue/disp8
+$string-length-at-start-of-slice:escape:
+    # increment curr but not result
+    41/increment-ECX
+$string-length-at-start-of-slice:continue:
+    # ++result
+    40/increment-EAX
+    # ++curr
+    41/increment-ECX
+    eb/jump  $string-length-at-start-of-slice:loop/disp8
+$string-length-at-start-of-slice:end:
+    # . restore registers
+    5b/pop-to-EBX
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-string-length-at-start-of-slice:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup: (EAX..ECX) = "\"abc\" def"
+    b8/copy-to-EAX  "\"abc\" def"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # EAX = string-length-at-start-of-slice(EAX, ECX)
+    # . . push args
+    51/push-ECX
+    50/push-EAX
+    # . . call
+    e8/call  string-length-at-start-of-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 3, msg)
+    # . . push args
+    68/push  "F - test-string-length-at-start-of-slice"/imm32
+    68/push  3/imm32
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-string-length-at-start-of-slice-escaped:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup: (EAX..ECX) = "\"ab\\c\" def"
+    b8/copy-to-EAX  "\"ab\\c\" def"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # EAX = string-length-at-start-of-slice(EAX, ECX)
+    # . . push args
+    51/push-ECX
+    50/push-EAX
+    # . . call
+    e8/call  string-length-at-start-of-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 3, msg)
+    # . . push args
+    68/push  "F - test-string-length-at-start-of-slice-escaped"/imm32
+    68/push  3/imm32
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
 == data
 
 Segment-size:
@@ -2009,6 +2646,8 @@ Slash:
 _test-slice-abc:
   22/dquote 61/a 62/b 63/c 22/dquote  # "abc"
 _test-slice-abc-end:
+  2f/slash 64/d
+_test-slice-abc-metadata-end:
 
 _test-slice-empty-string-literal:
   22/dquote 22/dquote  # ""
@@ -2031,4 +2670,14 @@ _test-slice-word-end:
   2f/slash 67/g 68/h 69/i  # /ghi
 _test-slice-word-end2:
 
+# "abc/def"/ghi
+_test-slice-literal-string:
+  22/dquote
+  61/a 62/b 63/c  # abc
+  2f/slash 64/d 65/e 66/f  # /def
+  22/dquote
+_test-slice-literal-string-end:
+  2f/slash 67/g 68/h 69/i  # /ghi
+_test-slice-literal-string-with-metadata-end:
+
 # . . vim:nowrap:textwidth=0
diff --git a/subx/apps/factorial b/subx/apps/factorial
index 16b54308..0f913091 100755
--- a/subx/apps/factorial
+++ b/subx/apps/factorial
Binary files differdiff --git a/subx/apps/factorial.subx b/subx/apps/factorial.subx
index 98efc6fa..e4b7a057 100644
--- a/subx/apps/factorial.subx
+++ b/subx/apps/factorial.subx
@@ -50,8 +50,8 @@ Entry:  # run tests if necessary, compute `factorial(5)` if not
     e8/call  run-tests/disp32
     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Num-test-failures/disp32          # copy *Num-test-failures to EAX
     eb/jump  $main:end/disp8  # where EAX will get copied to EBX
-    # - otherwise return factorial(5)
 $run-main:
+    # - otherwise return factorial(5)
     # . . push args
     68/push  5/imm32
     # . . call
diff --git a/subx/apps/handle b/subx/apps/handle
index 154e3725..c7110e42 100755
--- a/subx/apps/handle
+++ b/subx/apps/handle
Binary files differdiff --git a/subx/apps/handle.subx b/subx/apps/handle.subx
index 0ed12067..97a6c622 100644
--- a/subx/apps/handle.subx
+++ b/subx/apps/handle.subx
@@ -15,10 +15,11 @@
 # To run (from the subx directory):
 #   $ ./subx translate *.subx apps/handle.subx -o apps/handle
 #   $ ./subx run apps/handle
-# Expected result is a hard abort:
-#   ........lookup failed
-# (This file is a prototype, so the tests in this file aren't real tests. Don't
-# expect to run anything in the same process after they've completed.)
+# Expected result is a successful lookup followed by a hard abort:
+#   lookup succeeded
+#   lookup failed
+# (This file is a prototype. The 'tests' in it aren't real; failures are
+# expected.)
 
 == code
 #   instruction                     effective address                                                   register    displacement    immediate
@@ -212,25 +213,44 @@ lookup:  # h : (handle T) -> EAX : (address T)
     # EAX = handle
     8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           0/r32/EAX   8/disp8         .                 # copy *(EBP+8) to EAX
     # - inline {
+    # push handle->alloc_id
+    ff          6/subop/push        0/mod/indirect  0/rm32/EAX    .           .             .           .           .               .                 # push *EAX
+    # EAX = handle->address (payload)
+    8b/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           .           4/disp8         .                 # copy *(EAX+4) to EAX
     # push handle->address
-    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         .                 # push *(EAX+4)
-    # EAX = handle->alloc_id
+    50/push-EAX
+    # EAX = payload->alloc_id
     8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           .           .               .                 # copy *EAX to EAX
-    # if (EAX != *ESP) abort
-    39/compare                      0/mod/indirect  4/rm32/sib    4/base/ESP  4/index/none  .           0/r32/EAX   .               .                 # compare *ESP and EAX
-    75/jump-if-not-equal  $lookup:fail/disp8
-    # return ESP+4
+    # if (EAX != handle->alloc_id) abort
+    39/compare                      1/mod/*+disp8   4/rm32/sib    4/base/ESP  4/index/none  .           0/r32/EAX   4/disp8         .                 # compare *(ESP+4) and EAX
+    75/jump-if-not-equal  $lookup:abort/disp8
+    # EAX = pop handle->address
     58/pop-to-EAX
+    # discard handle->alloc_id
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # add 4
     05/add-to-EAX  4/imm32
     # - }
+    # - alternative consuming a second register {
+#?     # ECX = handle->alloc_id
+#?     8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+#?     # EAX = handle->address (payload)
+#?     8b/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(EAX+4) to EAX
+#?     # if (ECX != *EAX) abort
+#?     39/compare                      0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # compare *EAX and ECX
+#?     75/jump-if-not-equal  $lookup:abort/disp8
+#?     # add 4 to EAX
+#?     05/add-to-EAX  4/imm32
+    # - }
     # . epilog
     89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
     5d/pop-to-EBP
     c3/return
-$lookup:fail:
+
+$lookup:abort:
     # . _write(2/stderr, msg)
     # . . push args
-    68/push  "lookup failed"/imm32
+    68/push  "lookup failed\n"/imm32
     68/push  2/imm32/stderr
     # . . call
     e8/call  _write/disp32
@@ -294,6 +314,14 @@ test-lookup-success:
     # clean up
     # . *Next-alloc-id = 1
     c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .     Next-alloc-id/disp32  1/imm32           # copy to *Next-alloc-id
+    # write(2/stderr, "lookup succeeded\n")
+    # . . push args
+    68/push  "lookup succeeded\n"/imm32
+    68/push  2/imm32/stderr
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
     # . restore registers
     5a/pop-to-EDX
     59/pop-to-ECX
diff --git a/subx/apps/hex b/subx/apps/hex
index b23b8b86..5d663c6d 100755
--- a/subx/apps/hex
+++ b/subx/apps/hex
Binary files differdiff --git a/subx/apps/pack b/subx/apps/pack
index dfc71dc4..ad55fb75 100755
--- a/subx/apps/pack
+++ b/subx/apps/pack
Binary files differdiff --git a/subx/stats.md b/subx/stats.md
index 847c4b9a..a53d4963 100644
--- a/subx/stats.md
+++ b/subx/stats.md
@@ -1,16 +1,21 @@
                           Initial   -tests/whitespace/comments
 ## Lines in source
+standard library           7712     1814
 apps/crenshaw2-1b.subx      798      176
 apps/crenshaw2-1.subx       601      180
 apps/factorial.subx         107       28
 apps/handle.subx            361       58
-apps/hex.subx              1535      133
-apps/pack.subx             1667      241
+apps/hex.subx              1511      144
+apps/pack.subx             7348     1054
+apps/assort.subx           1318      284
+apps/dquotes.subx          2694      497
 
 ## Bytes in executable
 crenshaw2-1               17612     4112
 crenshaw2-1b              18171     4140
 factorial                 16530     3488
 handle                    17323     3582
-hex                       20591     3866
-pack                      20762     4054
+hex                       22684     4909
+pack                      37316     7825
+assort                    22506     5342
+dquotes                   27186     5849
diff --git a/subx/test_apps b/subx/test_apps
index 3001940f..a6e78ee3 100755
--- a/subx/test_apps
+++ b/subx/test_apps
@@ -131,9 +131,13 @@ test `uname` = 'Linux'  &&  examples/ex12
 echo handle
 ./subx translate 0*.subx apps/handle.subx  -o apps/handle
 [ "$1" != record ]  &&  git diff --exit-code apps/handle
-./subx run apps/handle 2>&1  |grep -q 'lookup failed'
+./subx run apps/handle > handle.out 2>&1  ||  true
+grep -q 'lookup succeeded' handle.out  ||  { echo "missing success test"; exit 1; }
+grep -q 'lookup failed' handle.out  ||  { echo "missing failure test"; exit 1; }
 test `uname` = 'Linux'  &&  {
-  apps/handle test 2>&1  |grep -q 'lookup failed'
+  apps/handle > handle.out 2>&1  ||  true
+  grep -q 'lookup succeeded' handle.out  ||  { echo "missing success test"; exit 1; }
+  grep -q 'lookup failed' handle.out  ||  { echo "missing failure test"; exit 1; }
 }
 
 echo factorial
@@ -200,4 +204,14 @@ test `uname` = 'Linux'  &&  {
   echo
 }
 
+echo dquotes
+./subx translate 0*.subx apps/subx-common.subx apps/dquotes.subx  -o apps/dquotes
+[ "$1" != record ]  &&  git diff --exit-code apps/dquotes
+./subx run apps/dquotes test
+echo
+test `uname` = 'Linux'  &&  {
+  apps/dquotes test
+  echo
+}
+
 exit 0