about summary refs log tree commit diff stats
path: root/subx/010core.cc
blob: 59d631a8d29e3e758b86d722c391e3c9b8c06eab (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//:: simulated x86 registers

:(before "End Types")
enum {
  EAX,
  ECX,
  EDX,
  EBX,
  ESP,
  EBP,
  ESI,
  EDI,
  NUM_INT_REGISTERS,
};
union reg {
  int32_t i;
  uint32_t u;
};
:(before "End Globals")
reg R[NUM_INT_REGISTERS] = { {0} };
uint32_t EIP = 0;
:(before "End Reset")
bzero(R, sizeof(R));
EIP = 0;

//:: simulated flag registers; just a subset that we care about

:(before "End Globals")
bool OF=false, ZF=false, SF=false;
:(before "End Reset")
OF = ZF = SF = false;

//: how the flag registers are updated after each instruction

:(before "End Includes")
// beware: no side-effects in args
#define BINARY_ARITHMETIC_OP(op, arg1, arg2) { \
  /* arg1 and arg2 must be signed */ \
  int64_t tmp = arg1 op arg2; \
  arg1 = arg1 op arg2; \
  SF = (arg1 < 0); \
  ZF = (arg1 == 0); \
  OF = (arg1 != tmp); \
}

#define BINARY_BITWISE_OP(op, arg1, arg2) { \
  /* arg1 and arg2 must be unsigned */ \
  arg1 = arg1 op arg2; \
  SF = (arg1 >> 31); \
  ZF = (arg1 == 0); \
  OF = false; \
}

//:: simulated RAM

:(before "End Globals")
vector<uint8_t> Memory;
:(before "End Reset")
Memory.clear();

//:: core interpreter loop

:(scenario add_imm32_to_eax)
# opcode     modrm     sib       displacement      immediate
  05                                               0a 0b 0c 0d  # add EAX, 0x0d0c0b0a
+load: 05
+load: 0a
+load: 0b
+load: 0c
+load: 0d
+run: add to EAX immediate 0x0d0c0b0a
+reg: storing 0x0d0c0b0a in register EAX

:(code)
// helper for tests: load a program into memory from a textual representation
// of its bytes, and run it
void run(const string& text_bytes) {
  load_program(text_bytes);
  EIP = 1;  // preserve null pointer
  while (EIP < Memory.size())
    run_one_instruction();
}

void load_program(const string& text_bytes) {
  assert(Memory.empty());
  // initialize address 0
  Memory.push_back(0);
  // now, to read the hex bytes in ASCII, we'll use C's strtol
  // strtol needs a char*, so we grab the buffer backing the string object
  char* curr = const_cast<char*>(&text_bytes[0]);   // non-standard approach, but blessed by Herb Sutter (http://herbsutter.com/2008/04/07/cringe-not-vectors-are-guaranteed-to-be-contiguous/#comment-483)
  char* max = curr + strlen(curr);
  while (true) {
    if (curr >= max) return;
    // skip whitespace
    while (*curr == ' ' || *curr == '\n') ++curr;
    // skip comments
    if (*curr == '#') {
      while (*curr != '\n') {
        ++curr;
        if (curr >= max) return;
      }
      ++curr;
      if (curr >= max) return;
    }
    Memory.push_back(strtol(curr, &curr, /*hex*/16));
    trace(99, "load") << HEXBYTE << static_cast<unsigned int>(Memory.back()) << end();  // ugly that iostream doesn't print uint8_t as an integer
  }
}

// skeleton of how x86 instructions are decoded
void run_one_instruction() {
  uint8_t op=0, op2=0, op3=0;
  switch(op = next()) {
  // our first opcode
  case 0x05: {  // add EAX, imm32
    int32_t arg2 = imm32();
    trace(2, "run") << "add to EAX immediate 0x" << HEXWORD << arg2 << end();
    BINARY_ARITHMETIC_OP(+, R[EAX].i, arg2);
    trace(98, "reg") << "storing 0x" << HEXWORD << R[EAX].i << " in register EAX" << end();
    break;
  }
  // End Single-Byte Opcodes
  case 0x0f:
    switch(op2 = next()) {
    // End Two-Byte Opcodes Starting With 0f
    default:
      cerr << "unrecognized second opcode after 0f: " << std::hex << static_cast<int>(op2) << '\n';
      exit(1);
    }
    break;
  case 0xf3:
    switch(op2 = next()) {
    // End Two-Byte Opcodes Starting With f3
    case 0x0f:
      switch(op3 = next()) {
      // End Three-Byte Opcodes Starting With f3 0f
      default:
        cerr << "unrecognized third opcode after f3 0f: " << std::hex << static_cast<int>(op3) << '\n';
        exit(1);
      }
      break;
    default:
      cerr << "unrecognized second opcode after f3: " << std::hex << static_cast<int>(op2) << '\n';
      exit(1);
    }
    break;
  case 0xf4:  // hlt
    EIP = Memory.size();
    break;
  default:
    cerr << "unrecognized opcode: " << std::hex << static_cast<int>(op) << '\n';
    exit(1);
  }
}

uint8_t next(void) {
  if (EIP >= Memory.size()) return /*hlt*/0xf4;
  return Memory.at(EIP++);
}

// read a 32-bit immediate in little-endian order from the instruction stream
int32_t imm32(void) {
  int result = next();
  result |= (next()<<8);
  result |= (next()<<16);
  result |= (next()<<24);
  return result;
}

:(before "End Includes")
#include <iomanip>
#define HEXBYTE  std::hex << std::setw(2) << std::setfill('0')
#define HEXWORD  std::hex << std::setw(8) << std::setfill('0')
180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511
# Read a text file containing whitespace-separated pairs of ascii hex bytes
# from stdin, and convert them into binary bytes (octets) on stdout. Ignore
# comments between '#' and newline.
#
# To run (from the subx/ directory):
#   $ ./subx translate *.subx apps/hex.subx -o apps/hex
#   $ echo '80 81 82  # comment'  |./subx run apps/hex  |xxd -
# Expected output:
#   00000000: 8081 82
#
# Only hex bytes and comments are permitted. Outside of comments all words
# must be exactly 2 characters long and contain only characters [0-9a-f]. No
# uppercase hex.

== code
#   instruction                     effective address                                                   register    displacement    immediate
# . op          subop               mod             rm32          base        index         scale       r32
# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes

Entry:  # run tests if necessary, convert stdin if not

#?     # for debugging: run a single test
#?     e8/call test-convert-next-octet-aborts-on-single-hex-byte/disp32
#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
#?     eb/jump  $main:end/disp8

    # . prolog
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # - if argc > 1 and argv[1] == "test", then return run_tests()
    # . argc > 1
    81          7/subop/compare     1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0/disp8         1/imm32           # compare *EBP
    7e/jump-if-lesser-or-equal  $run-main/disp8
    # . argv[1] == "test"
    # . . push args
    68/push  "test"/imm32
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
    # . . call
    e8/call  kernel-string-equal?/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . check result
    3d/compare-EAX-and  1/imm32
    75/jump-if-not-equal  $run-main/disp8
    # . run-tests()
    e8/call  run-tests/disp32
    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
    eb/jump  $main:end/disp8
$run-main:
    # - otherwise convert stdin
    # var ed/EAX : exit-descriptor
    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
    89/copy                         3/mod/direct    0/rm32/EAX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EAX
    # configure ed to really exit()
    # . ed->target = 0
    c7          0/subop/copy        0/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm32           # copy to *EAX
    # return convert(Stdin, 1/stdout, 2/stderr, ed)
    # . . push args
    50/push-EAX/ed
    68/push  Stderr/imm32
    68/push  Stdout/imm32
    68/push  Stdin/imm32
    # . . call
    e8/call  convert/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
    # . syscall(exit, 0)
    bb/copy-to-EBX  0/imm32
$main:end:
    b8/copy-to-EAX  1/imm32/exit
    cd/syscall  0x80/imm8

# the main entry point
convert:  # in : (address buffered-file), out : (address buffered-file), err : (address buffered-file), ed : (address exit-descriptor) -> <void>
    # pseudocode:
    #   while true
    #     EAX = convert-next-octet(in, err, ed)
    #     if (EAX == Eof) break
    #     write-byte-buffered(out, AL)
    #   flush(out)
    #
    # . prolog
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # . save registers
    50/push-EAX
$convert:loop:
    # EAX = convert-next-octet(in, err, ed)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x14/disp8      .                 # push *(EBP+20)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
    # . . call
    e8/call  convert-next-octet/disp32
    # . . discard first 2 args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    # if (EAX == Eof) break
    3d/compare-EAX-and  0xffffffff/imm32/Eof
    74/jump-if-equal  $convert:loop-end/disp8
    # write-byte-buffered(out, AL)
    # . . push args
    50/push-EAX
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
    # . . call
    e8/call  write-byte-buffered/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # loop
    eb/jump  $convert:loop/disp8
$convert:loop-end:
    # flush(out)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
    # . . call
    e8/call  flush/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
$convert:end:
    # . restore registers
    58/pop-to-EAX
    # . epilog
    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
    5d/pop-to-EBP
    c3/return

# read bytes from 'in' until a sequence of two lowercase hex (0-9, a-f) bytes
# skip spaces and newlines
# on '#' skip bytes until newline
# raise an error and abort on all other unexpected bytes
# return in EAX an _octet_ containing the binary value of the two hex characters
# return Eof on reaching end of file
convert-next-octet:  # in : (address buffered-file), err : (address buffered-file), ed : (address exit-descriptor) -> byte-or-Eof/EAX
    # pseudocode:
    #   EAX = scan-next-byte(in, err, ed)
    #   if (EAX == Eof) return
    #   ECX = from-hex-char(EAX)
    #   EAX = scan-next-byte(in, err, ed)
    #   if (EAX == Eof) error("partial byte found.")
    #   EAX = from-hex-char(EAX)
    #   EAX = (ECX << 4) | EAX
    #   return
    #
    # . prolog
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # . save registers
    51/push-ECX
    # EAX = scan-next-byte(in, err, ed)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
    # . . call
    e8/call  scan-next-byte/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    # if (EAX == Eof) return
    3d/compare-EAX-and  0xffffffff/imm32/Eof
    74/jump-if-equal  $convert-next-octet:end/disp8
    # EAX = from-hex-char(EAX)
    e8/call from-hex-char/disp32
    # ECX = EAX
    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy EAX to ECX
    # EAX = scan-next-byte(in, err, ed)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
    # . . call
    e8/call  scan-next-byte/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    # if (EAX == Eof) error(ed, err, "partial byte found.")
    3d/compare-EAX-and  0xffffffff/imm32/Eof
    75/jump-if-not-equal  $convert-next-octet:convert/disp8
    # . error-byte(ed, err, msg, '.')  # reusing error-byte to avoid creating _yet_ another helper
    # . . push args
    68/push  0x2e/imm32/period/dummy
    68/push  "convert-next-octet: partial byte found"/imm32
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
    # . . call
    e8/call  error-byte/disp32  # never returns
$convert-next-octet:convert:
    # EAX = from-hex-char(EAX)
    e8/call from-hex-char/disp32
    # EAX = (ECX << 4) | EAX
    # . ECX <<= 4
    c1/shift    4/subop/left        3/mod/direct    1/rm32/ECX    .           .             .           .           .               4/imm8            # shift ECX left by 4 bits
    # . EAX |= ECX
    09/or                           3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # EAX = bitwise OR with ECX
$convert-next-octet:end:
    # . restore registers
    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-convert-next-octet:
    # - check that the first two bytes of the input are assembled into the resulting octet
    # This test uses exit-descriptors. Use EBP for setting up local variables.
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # clear all streams
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-buffered-file/imm32
    05/add-to-EAX  4/imm32
    50/push-EAX
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-stream)
    # . . push args
    68/push  _test-error-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-error-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
    # initialize '_test-stream' to "abc"
    # . write(_test-stream, "abc")
    # . . push args
    68/push  "abc"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # initialize exit-descriptor 'ed' for the call to 'convert-next-octet' below
    # . var ed/ECX : exit-descriptor
    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
    # . tailor-exit-descriptor(ed, 12)
    # . . push args
    68/push  0xc/imm32/nbytes-of-args-for-convert-next-octet
    51/push-ECX/ed
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # EAX = convert-next-octet(_test-buffered-file, _test-error-buffered-file, ed)
    # . . push args
    51/push-ECX/ed
    68/push  _test-error-buffered-file/imm32
    68/push  _test-buffered-file/imm32
    # . . call
    e8/call  convert-next-octet/disp32
    # registers except ESP may be clobbered at this point
    # pop args to convert-next-octet
    # . . discard first 2 args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . . restore ed
    59/pop-to-ECX
    # check that convert-next-octet didn't abort
    # . check-ints-equal(ed->value, 0, msg)
    # . . push args
    68/push  "F - test-convert-next-octet: unexpected abort"/imm32
    68/push  0/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         .                 # push *(ECX+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
    # return if abort
    81          7/subop/compare     1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         0/imm32           # compare *(ECX+4)
    75/jump-if-not-equal  $test-convert-next-octet:end/disp8
    # check-ints-equal(EAX, 0xab, msg)
    # . . push args
    68/push  "F - test-convert-next-octet"/imm32
    68/push  0xab/imm32/ab
    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
$test-convert-next-octet:end:
    # . epilog
    # don't restore ESP from EBP; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    5d/pop-to-EBP
    c3/return

test-convert-next-octet-handles-Eof:
    # - check that reaching end of file returns Eof
    # This test uses exit-descriptors. Use EBP for setting up local variables.
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # clear all streams
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-buffered-file/imm32
    05/add-to-EAX  4/imm32
    50/push-EAX
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-stream)
    # . . push args
    68/push  _test-error-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-error-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
    # don't initialize '_test-stream'
    # initialize exit-descriptor 'ed' for the call to 'convert-next-octet' below
    # . var ed/ECX : exit-descriptor
    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
    # . tailor-exit-descriptor(ed, 12)
    # . . push args
    68/push  0xc/imm32/nbytes-of-args-for-convert-next-octet
    51/push-ECX/ed
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # EAX = convert-next-octet(_test-buffered-file, _test-error-buffered-file, ed)
    # . . push args
    51/push-ECX/ed
    68/push  _test-error-buffered-file/imm32
    68/push  _test-buffered-file/imm32
    # . . call
    e8/call  convert-next-octet/disp32
    # registers except ESP may be clobbered at this point
    # pop args to convert-next-octet
    # . . discard first 2 args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . . restore ed
    59/pop-to-ECX
    # check that convert-next-octet didn't abort
    # . check-ints-equal(ed->value, 0, msg)
    # . . push args
    68/push  "F - test-convert-next-octet: unexpected abort"/imm32
    68/push  0/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         .                 # push *(ECX+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
    # return if abort
    81          7/subop/compare     1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         0/imm32           # compare *(ECX+4)
    75/jump-if-not-equal  $test-convert-next-octet-handles-Eof:end/disp8
    # check-ints-equal(EAX, Eof, msg)
    # . . push args
    68/push  "F - test-convert-next-octet-handles-Eof"/imm32
    68/push  0xffffffff/imm32/Eof
    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
$test-convert-next-octet-handles-Eof:end:
    # . epilog
    # don't restore ESP from EBP; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    5d/pop-to-EBP
    c3/return

test-convert-next-octet-aborts-on-single-hex-byte:
    # - check that a single unaccompanied hex byte aborts
    # This test uses exit-descriptors. Use EBP for setting up local variables.
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # clear all streams
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-buffered-file/imm32
    05/add-to-EAX  4/imm32
    50/push-EAX
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-stream)
    # . . push args
    68/push  _test-error-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-error-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
    # initialize '_test-stream' to "a"
    # . write(_test-stream, "a")
    # . . push args
    68/push  "a"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # initialize exit-descriptor 'ed' for the call to 'convert-next-octet' below
    # . var ed/ECX : exit-descriptor
    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
    # . tailor-exit-descriptor(ed, 12)
    # . . push args
    68/push  0xc/imm32/nbytes-of-args-for-convert-next-octet
    51/push-ECX/ed
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # EAX = convert-next-octet(_test-buffered-file, _test-error-buffered-file, ed)
    # . . push args
    51/push-ECX/ed
    68/push  _test-error-buffered-file/imm32
    68/push  _test-buffered-file/imm32
    # . . call
    e8/call  convert-next-octet/disp32
    # registers except ESP may be clobbered at this point
    # pop args to convert-next-octet
    # . . discard first 2 args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . . restore ed
    59/pop-to-ECX
    # check that convert-next-octet aborted
    # . check-ints-equal(ed->value, 2, msg)
    # . . push args
    68/push  "F - test-convert-next-octet-aborts-on-single-hex-byte: unexpected abort"/imm32
    68/push  2/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         .                 # push *(ECX+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
$test-convert-next-octet-aborts-on-single-hex-byte:end:
    # . epilog
    # don't restore ESP from EBP; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    5d/pop-to-EBP
    c3/return

# read whitespace until a hex byte, and return it
# return Eof if file ends without finding a hex byte
# on '#' skip all bytes until newline
# abort on any other byte
scan-next-byte:  # in : (address buffered-file), err : (address buffered-file), ed : (address exit-descriptor) -> byte-or-Eof/EAX
    # pseudocode:
    #   while true
    #     EAX = read-byte-buffered(in)
    #     if (EAX == Eof) return EAX
    #     if (is-hex-digit?(EAX)) return EAX
    #     if (EAX == ' ' or '\t' or '\n') continue
    #     if (EAX == '#') skip-until-newline(in)
    #     else error-byte(ed, err, "invalid byte: " EAX)
    #
    # . prolog
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # . save registers
$scan-next-byte:loop:
    # EAX = read-byte-buffered(in)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
    # . . call
    e8/call  read-byte-buffered/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # if (EAX == Eof) return EAX
    3d/compare-with-EAX  0xffffffff/imm32/Eof
    74/jump-if-equal  $scan-next-byte:end/disp8
    # if (is-hex-digit?(EAX)) return EAX
    # . save EAX for now
    50/push-EAX
    # . is-hex-digit?(EAX)
    # . . push args
    50/push-EAX
    # . . call
    e8/call  is-hex-digit?/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . compare with 'false'
    3d/compare-with-EAX  0/imm32
    # . restore EAX (does not affect flags)
    58/pop-to-EAX
    # . check whether to return
    75/jump-if-not-equal  $scan-next-byte:end/disp8
$scan-next-byte:check1:
    # if (EAX == ' ') continue
    3d/compare-EAX-and  0x20/imm32/space
    74/jump-if-equal  $scan-next-byte:loop/disp8
    # if (EAX == '\t') continue
    3d/compare-EAX-and  9/imm32/tab
    74/jump-if-equal  $scan-next-byte:loop/disp8
    # if (EAX == '\n') continue
    3d/compare-EAX-and  0xa/imm32/newline
    74/jump-if-equal  $scan-next-byte:loop/disp8
$scan-next-byte:check2:
    # if (EAX == '#') skip-until-newline(in)
    3d/compare-with-EAX  0x23/imm32
    75/jump-if-not-equal  $scan-next-byte:check3/disp8
    # . skip-until-newline(in)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
    # . . call
    e8/call  skip-until-newline/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    eb/jump  $scan-next-byte:loop/disp8
$scan-next-byte:check3:
    # otherwise error-byte(ed, err, msg, EAX)
    # . . push args
    50/push-EAX
    68/push  "scan-next-byte: invalid byte"/imm32
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
    # . . call
    e8/call  error-byte/disp32  # never returns
$scan-next-byte:end:
    # . restore registers
    # . epilog
    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
    5d/pop-to-EBP
    c3/return

test-scan-next-byte:
    # - check that the first byte of the input is returned
    # This test uses exit-descriptors. Use EBP for setting up local variables.
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # clear all streams
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-buffered-file/imm32
    05/add-to-EAX  4/imm32
    50/push-EAX
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-stream)
    # . . push args
    68/push  _test-error-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-error-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
    # initialize '_test-stream' to "abc"
    # . write(_test-stream, "abc")
    # . . push args
    68/push  "abc"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # initialize exit-descriptor 'ed' for the call to 'scan-next-byte' below
    # . var ed/ECX : exit-descriptor
    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
    # . tailor-exit-descriptor(ed, 12)
    # . . push args
    68/push  0xc/imm32/nbytes-of-args-for-scan-next-byte
    51/push-ECX/ed
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # EAX = scan-next-byte(_test-buffered-file, _test-error-buffered-file, ed)
    # . . push args
    51/push-ECX/ed
    68/push  _test-error-buffered-file/imm32
    68/push  _test-buffered-file/imm32
    # . . call
    e8/call  scan-next-byte/disp32
    # registers except ESP may be clobbered at this point
    # pop args to scan-next-byte
    # . . discard first 2 args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . . restore ed
    59/pop-to-ECX
    # check that scan-next-byte didn't abort
    # . check-ints-equal(ed->value, 0, msg)
    # . . push args
    68/push  "F - test-scan-next-byte: unexpected abort"/imm32
    68/push  0/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         .                 # push *(ECX+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
    # return if abort
    81          7/subop/compare     1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         0/imm32           # compare *(ECX+4)
    75/jump-if-not-equal  $test-scan-next-byte:end/disp8
    # check-ints-equal(EAX, 0x61/a, msg)
    # . . push args
    68/push  "F - test-scan-next-byte"/imm32
    68/push  0x61/imm32/a
    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
$test-scan-next-byte:end:
    # . epilog
    # don't restore ESP from EBP; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    5d/pop-to-EBP
    c3/return

test-scan-next-byte-skips-whitespace:
    # - check that the first byte after whitespace is returned
    # This test uses exit-descriptors. Use EBP for setting up local variables.
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # clear all streams
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-buffered-file/imm32
    05/add-to-EAX  4/imm32
    50/push-EAX
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-stream)
    # . . push args
    68/push  _test-error-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-error-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
    # initialize '_test-stream' to input with leading whitespace
    # . write(_test-stream, text)
    # . . push args
    68/push  "  abc"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # initialize exit-descriptor 'ed' for the call to 'scan-next-byte' below
    # . var ed/ECX : exit-descriptor
    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
    # . tailor-exit-descriptor(ed, 12)
    # . . push args
    68/push  0xc/imm32/nbytes-of-args-for-scan-next-byte
    51/push-ECX/ed
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # EAX = scan-next-byte(_test-buffered-file, _test-error-buffered-file, ed)
    # . . push args
    51/push-ECX/ed
    68/push  _test-error-buffered-file/imm32
    68/push  _test-buffered-file/imm32
    # . . call
    e8/call  scan-next-byte/disp32
    # registers except ESP may be clobbered at this point
    # pop args to scan-next-byte
    # . . discard first 2 args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . . restore ed
    59/pop-to-ECX
    # check that scan-next-byte didn't abort
    # . check-ints-equal(ed->value, 0, msg)
    # . . push args
    68/push  "F - test-scan-next-byte-skips-whitespace: unexpected abort"/imm32
    68/push  0/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         .                 # push *(ECX+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
    # return if abort
    81          7/subop/compare     1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         0/imm32           # compare *(ECX+4)
    75/jump-if-not-equal  $test-scan-next-byte-skips-whitespace:end/disp8
    # check-ints-equal(EAX, 0x61/a, msg)
    # . . push args
    68/push  "F - test-scan-next-byte-skips-whitespace"/imm32
    68/push  0x61/imm32/a
    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
$test-scan-next-byte-skips-whitespace:end:
    # . epilog
    # don't restore ESP from EBP; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    5d/pop-to-EBP
    c3/return

test-scan-next-byte-skips-comment:
    # - check that the first byte after a comment (and newline) is returned
    # This test uses exit-descriptors. Use EBP for setting up local variables.
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # clear all streams
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-buffered-file/imm32
    05/add-to-EAX  4/imm32
    50/push-EAX
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-stream)
    # . . push args
    68/push  _test-error-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-error-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
    # initialize '_test-stream' to input with leading comment
    # . write(_test-stream, comment)
    # . . push args
    68/push  "#x\n"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . write(_test-stream, real text)
    # . . push args
    68/push  "ab"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # initialize exit-descriptor 'ed' for the call to 'scan-next-byte' below
    # . var ed/ECX : exit-descriptor
    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
    # . tailor-exit-descriptor(ed, 12)
    # . . push args
    68/push  0xc/imm32/nbytes-of-args-for-scan-next-byte
    51/push-ECX/ed
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # EAX = scan-next-byte(_test-buffered-file, _test-error-buffered-file, ed)
    # . . push args
    51/push-ECX/ed
    68/push  _test-error-buffered-file/imm32
    68/push  _test-buffered-file/imm32
    # . . call
    e8/call  scan-next-byte/disp32
    # registers except ESP may be clobbered at this point
    # pop args to scan-next-byte
    # . . discard first 2 args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . . restore ed
    59/pop-to-ECX
    # check that scan-next-byte didn't abort
    # . check-ints-equal(ed->value, 0, msg)
    # . . push args
    68/push  "F - test-scan-next-byte-skips-comment: unexpected abort"/imm32
    68/push  0/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         .                 # push *(ECX+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
    # return if abort
    81          7/subop/compare     1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         0/imm32           # compare *(ECX+4)
    75/jump-if-not-equal  $test-scan-next-byte-skips-comment:end/disp8
    # check-ints-equal(EAX, 0x61/a, msg)
    # . . push args
    68/push  "F - test-scan-next-byte-skips-comment"/imm32
    68/push  0x61/imm32/a
    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
$test-scan-next-byte-skips-comment:end:
    # . epilog
    # don't restore ESP from EBP; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    5d/pop-to-EBP
    c3/return

test-scan-next-byte-skips-comment-and-whitespace:
    # - check that the first byte after a comment and any further whitespace is returned
    # This test uses exit-descriptors. Use EBP for setting up local variables.
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # clear all streams
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-buffered-file/imm32
    05/add-to-EAX  4/imm32
    50/push-EAX
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-stream)
    # . . push args
    68/push  _test-error-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-error-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
    # initialize '_test-stream' to input with leading comment and more whitespace after newline
    # . write(_test-stream, comment)
    # . . push args
    68/push  "#x\n"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . write(_test-stream, real text)
    # . . push args
    68/push  " ab"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # initialize exit-descriptor 'ed' for the call to 'scan-next-byte' below
    # . var ed/ECX : exit-descriptor
    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
    # . tailor-exit-descriptor(ed, 12)
    # . . push args
    68/push  0xc/imm32/nbytes-of-args-for-scan-next-byte
    51/push-ECX/ed
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # EAX = scan-next-byte(_test-buffered-file, _test-error-buffered-file, ed)
    # . . push args
    51/push-ECX/ed
    68/push  _test-error-buffered-file/imm32
    68/push  _test-buffered-file/imm32
    # . . call
    e8/call  scan-next-byte/disp32
    # registers except ESP may be clobbered at this point
    # pop args to scan-next-byte
    # . . discard first 2 args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . . restore ed
    59/pop-to-ECX
    # check that scan-next-byte didn't abort
    # . check-ints-equal(ed->value, 0, msg)
    # . . push args
    68/push  "F - test-scan-next-byte-skips-comment-and-whitespace: unexpected abort"/imm32
    68/push  0/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         .                 # push *(ECX+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
    # return if abort
    81          7/subop/compare     1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         0/imm32           # compare *(ECX+4)
    75/jump-if-not-equal  $test-scan-next-byte-skips-comment-and-whitespace:end/disp8
    # check-ints-equal(EAX, 0x61/a, msg)
    # . . push args
    68/push  "F - test-scan-next-byte-skips-comment-and-whitespace"/imm32
    68/push  0x61/imm32/a
    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
$test-scan-next-byte-skips-comment-and-whitespace:end:
    # . epilog
    # don't restore ESP from EBP; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    5d/pop-to-EBP
    c3/return

test-scan-next-byte-skips-whitespace-and-comment:
    # - check that the first byte after any whitespace and comments is returned
    # This test uses exit-descriptors. Use EBP for setting up local variables.
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # clear all streams
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-buffered-file/imm32
    05/add-to-EAX  4/imm32
    50/push-EAX
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-stream)
    # . . push args
    68/push  _test-error-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-error-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
    # initialize '_test-stream' to input with leading whitespace and comment
    # . write(_test-stream, comment)
    # . . push args
    68/push  " #x\n"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . write(_test-stream, real text)
    # . . push args
    68/push  "ab"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # initialize exit-descriptor 'ed' for the call to 'scan-next-byte' below
    # . var ed/ECX : exit-descriptor
    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
    # . tailor-exit-descriptor(ed, 12)
    # . . push args
    68/push  0xc/imm32/nbytes-of-args-for-scan-next-byte
    51/push-ECX/ed
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # EAX = scan-next-byte(_test-buffered-file, _test-error-buffered-file, ed)
    # . . push args
    51/push-ECX/ed
    68/push  _test-error-buffered-file/imm32
    68/push  _test-buffered-file/imm32
    # . . call
    e8/call  scan-next-byte/disp32
    # registers except ESP may be clobbered at this point
    # pop args to scan-next-byte
    # . . discard first 2 args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . . restore ed
    59/pop-to-ECX
    # check that scan-next-byte didn't abort
    # . check-ints-equal(ed->value, 0, msg)
    # . . push args
    68/push  "F - test-scan-next-byte-skips-whitespace-and-comment: unexpected abort"/imm32
    68/push  0/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         .                 # push *(ECX+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
    # return if abort
    81          7/subop/compare     1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         0/imm32           # compare *(ECX+4)
    75/jump-if-not-equal  $test-scan-next-byte-skips-whitespace-and-comment:end/disp8
    # check-ints-equal(EAX, 0x61/a, msg)
    # . . push args
    68/push  "F - test-scan-next-byte-skips-whitespace-and-comment"/imm32
    68/push  0x61/imm32/a
    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
$test-scan-next-byte-skips-whitespace-and-comment:end:
    # . epilog
    # don't restore ESP from EBP; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    5d/pop-to-EBP
    c3/return

test-scan-next-byte-reads-final-byte:
    # - check that the final byte in input is returned
    # This test uses exit-descriptors. Use EBP for setting up local variables.
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # clear all streams
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-buffered-file/imm32
    05/add-to-EAX  4/imm32
    50/push-EAX
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-stream)
    # . . push args
    68/push  _test-error-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-error-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
    # initialize '_test-stream' to input with single character
    # . write(_test-stream, character)
    # . . push args
    68/push  "a"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # initialize exit-descriptor 'ed' for the call to 'scan-next-byte' below
    # . var ed/ECX : exit-descriptor
    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
    # . tailor-exit-descriptor(ed, 12)
    # . . push args
    68/push  0xc/imm32/nbytes-of-args-for-scan-next-byte
    51/push-ECX/ed
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # EAX = scan-next-byte(_test-buffered-file, _test-error-buffered-file, ed)
    # . . push args
    51/push-ECX/ed
    68/push  _test-error-buffered-file/imm32
    68/push  _test-buffered-file/imm32
    # . . call
    e8/call  scan-next-byte/disp32
    # registers except ESP may be clobbered at this point
    # pop args to scan-next-byte
    # . . discard first 2 args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . . restore ed
    59/pop-to-ECX
    # check that scan-next-byte didn't abort
    # . check-ints-equal(ed->value, 0, msg)
    # . . push args
    68/push  "F - test-scan-next-byte-reads-final-byte: unexpected abort"/imm32
    68/push  0/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         .                 # push *(ECX+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
    # return if abort
    81          7/subop/compare     1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         0/imm32           # compare *(ECX+4)
    75/jump-if-not-equal  $test-scan-next-byte-reads-final-byte:end/disp8
    # check-ints-equal(EAX, 0x61/a, msg)
    # . . push args
    68/push  "F - test-scan-next-byte-reads-final-byte"/imm32
    68/push  0x61/imm32/a
    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
$test-scan-next-byte-reads-final-byte:end:
    # . epilog
    # don't restore ESP from EBP; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    5d/pop-to-EBP
    c3/return

test-scan-next-byte-handles-Eof:
    # - check that the right sentinel value is returned when there's no data remaining to be read
    # This test uses exit-descriptors. Use EBP for setting up local variables.
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # clear all streams
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-buffered-file/imm32
    05/add-to-EAX  4/imm32
    50/push-EAX
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-stream)
    # . . push args
    68/push  _test-error-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-error-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
    # leave '_test-stream' empty
    # initialize exit-descriptor 'ed' for the call to 'scan-next-byte' below
    # . var ed/ECX : exit-descriptor
    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
    # . tailor-exit-descriptor(ed, 12)
    # . . push args
    68/push  0xc/imm32/nbytes-of-args-for-scan-next-byte
    51/push-ECX/ed
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # EAX = scan-next-byte(_test-buffered-file, _test-error-buffered-file, ed)
    # . . push args
    51/push-ECX/ed
    68/push  _test-error-buffered-file/imm32
    68/push  _test-buffered-file/imm32
    # . . call
    e8/call  scan-next-byte/disp32
    # registers except ESP may be clobbered at this point
    # pop args to scan-next-byte
    # . . discard first 2 args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . . restore ed
    59/pop-to-ECX
    # check that scan-next-byte didn't abort
    # . check-ints-equal(ed->value, 0, msg)
    # . . push args
    68/push  "F - test-scan-next-byte-handles-Eof: unexpected abort"/imm32
    68/push  0/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         .                 # push *(ECX+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
    # return if abort
    81          7/subop/compare     1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         0/imm32           # compare *(ECX+4)
    75/jump-if-not-equal  $test-scan-next-byte-handles-Eof:end/disp8
    # check-ints-equal(EAX, Eof, msg)
    # . . push args
    68/push  "F - test-scan-next-byte-handles-Eof"/imm32
    68/push  0xffffffff/imm32/Eof
    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
$test-scan-next-byte-handles-Eof:end:
    # . epilog
    # don't restore ESP from EBP; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    5d/pop-to-EBP
    c3/return

test-scan-next-byte-aborts-on-invalid-byte:
    # - check that the a bad byte immediately aborts
    # This test uses exit-descriptors. Use EBP for setting up local variables.
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # clear all streams
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-buffered-file/imm32
    05/add-to-EAX  4/imm32
    50/push-EAX
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-stream)
    # . . push args
    68/push  _test-error-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-error-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-error-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
    # initialize '_test-stream' to "x"
    # . write(_test-stream, "x")
    # . . push args
    68/push  "x"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # initialize exit-descriptor 'ed' for the call to 'scan-next-byte' below
    # . var ed/ECX : exit-descriptor
    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
    # . tailor-exit-descriptor(ed, 12)
    # . . push args
    68/push  0xc/imm32/nbytes-of-args-for-scan-next-byte
    51/push-ECX/ed
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # EAX = scan-next-byte(_test-buffered-file, _test-error-buffered-file, ed)
    # . . push args
    51/push-ECX/ed
    68/push  _test-error-buffered-file/imm32
    68/push  _test-buffered-file/imm32
    # . . call
    e8/call  scan-next-byte/disp32
    # registers except ESP may be clobbered at this point
    # pop args to scan-next-byte
    # . . discard first 2 args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . . restore ed
    59/pop-to-ECX
    # check that scan-next-byte aborted
    # . check-ints-equal(ed->value, 2, msg)
    # . . push args
    68/push  "F - test-scan-next-byte-aborts-on-invalid-byte"/imm32
    68/push  2/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         .                 # push *(ECX+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
$test-scan-next-byte-aborts-on-invalid-byte:end:
    # . epilog
    # don't restore ESP from EBP; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    5d/pop-to-EBP
    c3/return

skip-until-newline:  # in : (address buffered-file) -> <void>
    # pseudocode:
    #   push EAX
    #   while true
    #     EAX = read-byte-buffered(in)
    #     if (EAX == Eof) break
    #     if (EAX == 0x0a) break
    #   pop EAX
    # . prolog
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # . save registers
    50/push-EAX
$skip-until-newline:loop:
    # . EAX = read-byte-buffered(in)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
    # . . call
    e8/call  read-byte-buffered/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . if (EAX == Eof) break
    3d/compare-EAX-and  0xffffffff/imm32/Eof
    74/jump-if-equal  $skip-until-newline:end/disp8
    # . if (EAX != 0xa/newline) loop
    3d/compare-EAX-and  0xa/imm32/newline
    75/jump-if-not-equal  $skip-until-newline:loop/disp8
$skip-until-newline:end:
    # . restore registers
    58/pop-to-EAX
    # . epilog
    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
    5d/pop-to-EBP
    c3/return

test-skip-until-newline:
    # - check that the read pointer points after the newline
    # setup
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . clear-stream(_test-buffered-file+4)
    # . . push args
    b8/copy-to-EAX  _test-buffered-file/imm32
    05/add-to-EAX  4/imm32
    50/push-EAX
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # initialize '_test-stream' to "abc\nde"
    # . write(_test-stream, "abc")
    # . . push args
    68/push  "abc\n"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . write(_test-stream, "de")
    # . . push args
    68/push  "de"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # skip-until-newline(_test-buffered-file)
    # . . push args
    68/push  _test-buffered-file/imm32
    # . . call
    e8/call  skip-until-newline/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # check-ints-equal(_test-buffered-file->read, 4, msg)
    # . . push args
    68/push  "F - test-skip-until-newline"/imm32
    68/push  4/imm32
    b8/copy-to-EAX  _test-buffered-file/imm32
    ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           8/disp8         .                 # push *(EAX+8)
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    # . end
    c3/return

== data

_test-error-stream:
    # current write index
    0/imm32
    # current read index
    0/imm32
    # line
    0x80/imm32  # 128 bytes
    # data (8 lines x 16 bytes/line)
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

# a test buffered file for _test-error-stream
_test-error-buffered-file:
    # file descriptor or (address stream)
    _test-error-stream/imm32
    # current write index
    0/imm32
    # current read index
    0/imm32
    # length
    6/imm32
    # data
    00 00 00 00 00 00  # 6 bytes

# . . vim:nowrap:textwidth=0