rt_exec.py 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997
  1. #!/usr/bin/env python3
  2. """rt_exec.py -- call every 8086 runtime entry with a known argument, on qemu,
  3. and check what it does.
  4. The runtime (shell/Runtime.mod) is hand-assembled 8086, so "it built" says
  5. nothing about it. This says it *runs*. It used to run it on Unicorn, and
  6. Unicorn 2.1.4 mis-decodes 16-bit ModRM memory operands, so it failed 33 of 33
  7. and every one of those failures was the emulator's. A wrong oracle is worse
  8. than none: it cannot tell "my codegen is broken" from "the machine is broken".
  9. So the machine is now qemu-system-i386, driven through the same boot sector the
  10. .COM fixtures use (tests/exec/bootcom.s), and the expectations are unchanged
  11. except where they were demonstrably wrong -- see EXPECTATION FIXES below.
  12. .pas-free: RtProbe (base = RT_BASE) -> blob + entry offsets
  13. rt_exec.py -> 16-64 byte case record
  14. exec/rtdrv.s (assembled) -> a whole .COM image
  15. bootcom.s + qemu -> one framed record per case
  16. rt_exec.py -> parsed, compared
  17. tests/rt_exec.py --probe # the standalone runtime dump (base 0)
  18. tests/rt_exec.py --list # the case names
  19. tests/rt_exec.py --show # print what came out, assert nothing
  20. EXPECTATION FIXES -- two expectations were wrong, and both were wrong in the
  21. direction that made the HARNESS the suspect:
  22. * rdchar stores ONE byte (Runtime.EmitRdChar: `StDiDl' = MOV [DI], DL), so
  23. dumping a two-byte word and comparing it against ord(c) could never pass.
  24. The check was broken, the entry was right. The case now reports two bytes
  25. and expects the character followed by the 0xEE poison that is still there.
  26. * rdint at end of input does NOT store anything. TP3's xrdint returns to
  27. rnerr without touching the variable, and Runtime.EmitRdInt says so in its
  28. own comment ("^Z before any digit reaches rnend ... returns WITHOUT
  29. touching the variable"). The old expectation said the variable became 0.
  30. It stays at the poison, and that is the faithful answer.
  31. Everything else is the original expectation verbatim. The two `rdln' halves of
  32. the six rdint cases are merged into the same case as the value they follow: a
  33. readln that printed anything would change the same compared string, so the
  34. assertion is unchanged - one qemu boot per check instead of two.
  35. """
  36. import math
  37. import os
  38. import re
  39. import struct
  40. import subprocess
  41. import sys
  42. import tempfile
  43. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  44. from run_com_exec import (GM2, QEMU, build_boot_sector, build_floppy, # noqa: E402
  45. run_qemu, visible_output)
  46. HERE = os.path.dirname(os.path.abspath(__file__))
  47. SHELL = os.path.dirname(HERE)
  48. # --------------------------------------------------------------- the layout
  49. # RT_BASE is restated from exec/rtdrv.s on purpose: the driver needs the blob at
  50. # a known offset and Python needs to know where the driver thinks it is, so the
  51. # two are written down and then CHECKED against each other (see build_image).
  52. # A restated constant that is never compared is a constant waiting to lie.
  53. RT_BASE = 0x0190
  54. HDR = 0x4000
  55. DLEN = 32
  56. DATA = 0x5000
  57. DSHIFT = 0x2EF0
  58. STORE = DSHIFT+0x2140
  59. WSTR = DSHIFT+0x2150
  60. WSTR_EMPTY = DSHIFT+0x2158
  61. CASE = DSHIFT+0x2160
  62. CASE_SZ = 64
  63. RDSTR_DST = DSHIFT+0x21b0
  64. SST_HELLO = DSHIFT+0x21c0
  65. SST_LONG = DSHIFT+0x21d0
  66. SST_EMPTY = DSHIFT+0x21e0
  67. SST_DST = DSHIFT+0x21f0
  68. SST_CAT = DSHIFT+0x2200
  69. SST_CAT0 = DSHIFT+0x2210
  70. SST_WORLD = DSHIFT+0x2220
  71. AXOUT = DSHIFT+0x21a6 # the driver stores the last call's AX here
  72. FMTDEST = DSHIFT+0x2430 # a 64-byte string for the real_fmt cases
  73. ASCSRC = DSHIFT+0x2470 # the ASCIIZ strings for real_val
  74. FILEBLK = DSHIFT+0x2a00 # the 132-byte file control block
  75. REALDST = 0x0FA8
  76. # A .COM is loaded at CS:0100, and every address in the image is an image
  77. # offset. The load bias is therefore the difference between the two, and
  78. # putting it in one place here stops it being applied -- or forgotten -- in each
  79. # field. exec/rtdrv.s has its own .set of it, for the same reason it has its
  80. # own .set of the record offsets: two independent statements that are then
  81. # checked against each other by the fact that any disagreement stops the cases
  82. # from passing.
  83. LOAD_BIAS = 0x0100
  84. # The 8086 has a fixed 16-byte-case-record area in rtdrv.s, and the harness
  85. # asserts this window is still zeros. If the driver outgrows its space it
  86. # starts eating the runtime it is testing, which would be silent; this makes it
  87. # loud.
  88. DRIVER_TOP = 0x0180
  89. # The four framing bytes exec/rtdrv.s wraps each case's report in.
  90. SOH, STX, ETX, EOT = 0x01, 0x02, 0x03, 0x04
  91. # Entry selector order, as Runtime.def declares it. The names are used only to
  92. # be looked up in RtProbe's output, so renaming an entry in Runtime.mod makes
  93. # the lookup fail loudly instead of silently testing offset 0.
  94. ENTS = ["initmem", "progend", "stackchk", "wrint", "wrchar", "wrbool",
  95. "wrreal", "wrln", "rdint", "rdchar", "rdbool", "rdln", "halt",
  96. "wrtinl", "rdbyte", "wrstr", "rdstr", "strstore", "cmpstr", "strcat",
  97. "wrintw", "real_from_int", "real_add", "real_sub", "real_mul",
  98. "real_div", "real_cmp", "real_int", "real_fmt", "real_val",
  99. "real_write", "set_op", "set_cmp", "set_copy",
  100. "file_assign", "file_reset", "file_rewrite", "file_close",
  101. "ioresult", "io_store", "heap_new", "file_eof", "file_readln",
  102. "int_fmt", "val_int", "str_copy", "val_real", "real_abs",
  103. "real_sqrt", "real_ln", "real_exp", "real_sin", "real_cos",
  104. "real_arctan", "str_delete", "str_insert",
  105. "wrcharw", "wrboolw", "wrstrw", "memcopy"]
  106. # A call slot's flags, as rtdrv.s reads them.
  107. F_STACK = 1 # the argument goes on the stack, popped by the caller
  108. F_AX = 2 # ...or in AX instead (initmem's convention)
  109. F_INL = 4 # wrtinl: the argument is at the return address, not on the stack
  110. F_STACK2 = 8 # two stack words: +6 pushed first (lands at [BP+6]), +2 second
  111. # (at [BP+4]) -- strstore's dest/src order
  112. F_CX = 16 # CX := the word at +8, the maximum length rdstr and strstore
  113. # both read from the register
  114. F_STACK3 = 32 # three stack words: +8 first, then +6, then +2, so +2 is
  115. # [BP+4], +6 is [BP+6] and +8 is [BP+8]
  116. F_STACK4 = 64 # four stack words: +10, +8, +6, +2 -> [BP+10]..[BP+4]
  117. # The byte patterns check_bp_contract looks for, and the rule they express:
  118. # "an entry that borrows BP opens with PUSH BP; MOV BP,SP and pops it again
  119. # before it returns" -- so an entry containing 8B EC anywhere must start with
  120. # 55 8B EC and must contain a 5D. See that function: the first half of that
  121. # rule is exact and the second is a screen, and the difference matters.
  122. PUSH_BP_MOV_BP_SP = b"\x55\x8b\xec" # PUSH BP ; MOV BP,SP
  123. MOV_BP_SP = b"\x8b\xec" # MOV BP,SP
  124. POP_BP = b"\x5d" # POP BP
  125. # --------------------------------------------------------------- RtProbe
  126. def ensure_rtprobe():
  127. """Build tests/RtProbe.mod, return its path.
  128. Timestamp-checked, for the reason run_com_exec.ensure_comtest documents:
  129. a probe older than the runtime it reports on is worse than no probe, and
  130. the failure it produces -- a blob with the wrong addresses in it -- is
  131. silent at the byte level and catastrophic at the behaviour level.
  132. """
  133. exe = os.path.join(HERE, "rtprobe")
  134. newer = ["Runtime.mod", "Runtime.def", "Runtime.o", "Posix.c",
  135. os.path.join("tests", "RtProbe.mod")]
  136. if os.path.exists(exe):
  137. t = os.path.getmtime(exe)
  138. if all(os.path.exists(os.path.join(SHELL, f))
  139. and os.path.getmtime(os.path.join(SHELL, f)) <= t
  140. for f in newer):
  141. return exe
  142. r = subprocess.run([GM2, "-fiso", "-o", exe, "tests/RtProbe.mod",
  143. "Runtime.o", "Posix.o"],
  144. capture_output=True, cwd=SHELL)
  145. if r.returncode != 0:
  146. sys.exit("building RtProbe failed:\n" + r.stderr.decode(errors="replace"))
  147. return exe
  148. def load_runtime(base):
  149. """Run RtProbe at `base`.
  150. Return (blob, {name: image-absolute offset}, code_end). `code_end` is the
  151. blob-relative offset where the code stops and the data area starts, and it
  152. is what bounds the last entry's bytes: without it the last entry's "does
  153. it contain MOV SP,BP" question would be answered partly by the data area,
  154. and a string or an entry-table word that happens to contain the pattern
  155. would turn a missing epilogue into a pass.
  156. """
  157. out = subprocess.run([ensure_rtprobe()], input=b"%d\n" % base,
  158. capture_output=True, check=True).stdout.decode(
  159. errors="replace")
  160. # RtProbe ends its lines CR LF, as a program of this vintage would. Every
  161. # pattern below is `$'-anchored, and Python's `$' in MULTILINE stops before
  162. # "\n" only -- so without this the first regex fails and the report blames
  163. # the probe for it.
  164. out = out.replace("\r\n", "\n")
  165. m = re.search(r"^base=(\d+)$", out, re.M)
  166. if m is None or int(m.group(1)) != base:
  167. sys.exit("RtProbe did not confirm base=%d:\n%s" % (base, out[:400]))
  168. size = int(re.search(r"^(\d+) bytes", out, re.M).group(1))
  169. cm = re.search(r"^code ends at (\d+)$", out, re.M)
  170. if cm is None:
  171. sys.exit("RtProbe did not say where the code ends:\n%s" % out[:400])
  172. code_end = int(cm.group(1))
  173. if not 0 < code_end <= size:
  174. sys.exit("RtProbe says the code ends at %d, which is not inside a "
  175. "%d byte blob" % (code_end, size))
  176. entries = {nm: int(v) for v, nm in
  177. re.findall(r"entry \d+ = (\d+)\s+\((\w+)\)", out)}
  178. missing = [e for e in ENTS if e not in entries]
  179. if missing:
  180. sys.exit("RtProbe did not report entries: %s" % ", ".join(missing))
  181. blob = bytearray()
  182. for line in out.splitlines():
  183. h = re.match(r"^[0-9A-F]{8} ((?:[0-9A-F]{2} )+)$", line)
  184. if h:
  185. blob += bytes.fromhex(h.group(1).replace(" ", ""))
  186. if len(blob) != size:
  187. sys.exit("parsed %d bytes of runtime, RtProbe says %d"
  188. % (len(blob), size))
  189. for nm, off in entries.items():
  190. if not base <= off < base + code_end:
  191. sys.exit("entry %s is at %d, outside the code area %d..%d -- the "
  192. "entry table and the blob disagree"
  193. % (nm, off, base, base + code_end))
  194. return bytes(blob), entries, code_end
  195. def probe():
  196. """The standalone dump run_all.sh's `runtime probe' step wants: base 0."""
  197. raw = subprocess.run([ensure_rtprobe()], input=b"0\n", capture_output=True,
  198. check=True).stdout
  199. sys.stdout.buffer.write(raw)
  200. return 0
  201. def check_bp_contract(blob, entries, base, code_end):
  202. """Every entry must hand BP back.
  203. The driver keeps its cursor into the case record in BP, because an entry is
  204. allowed to destroy every register except BP and SP. Two entries did not
  205. honour that: wrchar and wrbool borrowed BP to reach their argument -- [SP]
  206. is unencodable in 16-bit mode -- and returned without saving it. One
  207. multi-call case caught it, and only because it made four calls in a row; a
  208. new entry that borrowed BP the same way would be caught by nothing.
  209. So the rule is checked here, over the built blob, before any case runs.
  210. The rule is the runtime's own convention, in two halves, and the halves are
  211. NOT equally strong -- which is worth writing down rather than discovering:
  212. * "an entry that borrows BP must START with 55 8B EC" is EXACT. The
  213. prologue is the first thing the entry does, so a positional test
  214. cannot be fooled by anything.
  215. * "...and must contain a 5D somewhere" is a SCREEN, not a proof. 5D is
  216. POP BP, but it is also a perfectly ordinary displacement byte, and
  217. this code does not disassemble. Requiring the POP to be contiguous
  218. with something else does not rescue it either: wrbool legitimately
  219. closes its frame with POP BP after the INT 21h, and a rule that
  220. insisted on 89 EC 5D would have failed a correct entry -- a false
  221. alarm, which is worse here than a miss because it teaches a reader to
  222. distrust the check. So the honest position is: a push with no pop is
  223. CAUGHT unless some displacement in the entry happens to be 5Dh, and
  224. the argument displacements in this runtime are 2 and 4, so nothing can
  225. collide today. tests/nonvacuity.sh removes the POP as well as the
  226. PUSH and requires each to turn this red, so the screen is at least
  227. witnessed rather than assumed.
  228. Counting 55s and 5Ds to check frame BALANCE would be worse than either:
  229. 55 occurs as an immediate and 5D as a displacement, so the answer would
  230. come out wrong without the check ever looking wrong. For the same reason
  231. each entry's bytes stop at the next entry, or at `code_end` for the last
  232. one: running on into the data area would let a string satisfy either test.
  233. The counts it returns are the point as much as the verdict. A scan that
  234. matches nothing is indistinguishable from a scan that passes, and that has
  235. happened here before -- so zero borrowers is a failure, not a pass.
  236. Entries are counted by OFFSET, not by name: `progend' and `halt' are one
  237. entry with two names, and counting them twice would make the total here
  238. disagree with the number of things that can actually be wrong.
  239. """
  240. offs = sorted(set(entries.values()))
  241. scanned = borrowed = 0
  242. bad = []
  243. for i, a in enumerate(offs):
  244. lo = a - base
  245. hi = (offs[i + 1] - base) if i + 1 < len(offs) else code_end
  246. code = bytes(blob[lo:hi])
  247. scanned += 1
  248. if MOV_BP_SP not in code:
  249. continue
  250. borrowed += 1
  251. # The two halves are reported separately, because they are separate
  252. # faults with separate fixes and "it does not hand BP back" does not
  253. # say which of them happened.
  254. if not code.startswith(PUSH_BP_MOV_BP_SP):
  255. why = "borrows BP but does not open with 55 8B EC, so the "\
  256. "caller's BP is destroyed"
  257. elif POP_BP not in code:
  258. why = "pushes BP but never pops it, so the caller gets a "\
  259. "cursor two bytes off"
  260. else:
  261. continue
  262. names = sorted(n for n, v in entries.items() if v == a)
  263. bad.append("%s -- %s (image 0x%04X: %s)"
  264. % (",".join(names), why, a, code[:12].hex(" ")))
  265. if bad:
  266. sys.exit("these entries do not hand BP back, so a caller that keeps a "
  267. "cursor in BP -- which this driver does, and which is the whole "
  268. "reason BP is the one register an entry may keep -- loses it:\n"
  269. " %s" % "\n ".join(bad))
  270. if borrowed == 0:
  271. sys.exit("no entry borrows BP, so this check examined %d entries and "
  272. "matched nothing: it is not looking at what it claims to "
  273. "look at" % scanned)
  274. return scanned, borrowed
  275. # ------------------------------------------------------------------ the case
  276. def case_bytes(c, entries):
  277. """One CASE_SZ byte case record, exactly as exec/rtdrv.s reads it.
  278. The record is a second, independent statement of the same layout rtdrv.s
  279. carries in its .set block. The cases only pass if the two agree on every
  280. offset, which is the whole reason for writing it twice: a driver that
  281. silently read the wrong field would still boot, still run, and still print.
  282. """
  283. if len(c["in"]) > 8:
  284. sys.exit("case %s supplies %d input bytes; the record holds 8"
  285. % (c["name"], len(c["in"])))
  286. b = bytearray(CASE_SZ)
  287. struct.pack_into("<H", b, 0, c["dump"]) # OFF_DUMPLEN
  288. struct.pack_into("<H", b, 2, c["dumpadr"]) # OFF_DUMPADR
  289. b[4] = len(c["in"]) # OFF_INLEN
  290. b[5:5 + len(c["in"])] = c["in"] # OFF_INBUF
  291. struct.pack_into("<H", b, 13, c["index"]) # OFF_CASE (word)
  292. calls = c["calls"]
  293. if len(calls) > 4:
  294. sys.exit("case %s has %d calls; the record holds 4"
  295. % (c["name"], len(calls)))
  296. b[15] = len(calls) # OFF_NCALLS (byte)
  297. for i, (ent, arg, mode, arg2, cx, arg3) in enumerate(calls):
  298. o = 16 + 12 * i # SLOT0, SLOT_SZ = 12
  299. struct.pack_into("<H", b, o, entries[ent])
  300. struct.pack_into("<H", b, o + 2, arg & 0xFFFF)
  301. b[o + 4] = mode
  302. struct.pack_into("<H", b, o + 6, arg2 & 0xFFFF)
  303. struct.pack_into("<H", b, o + 8, cx & 0xFFFF)
  304. struct.pack_into("<H", b, o + 10, arg3 & 0xFFFF)
  305. return bytes(b)
  306. def build_image(blob, rec):
  307. """Assemble exec/rtdrv.s into a complete .COM, and check it is the one
  308. we think it is.
  309. The three assertions are the point of this function. An image that is
  310. subtly not the image we intended would still boot, still run, and still
  311. print something -- the failure would look like a runtime bug.
  312. """
  313. if RT_BASE + len(blob) > HDR:
  314. sys.exit("the runtime is %d bytes: at RT_BASE=%04X it runs into the "
  315. "test scaffolding at %04X. Move the .org's in rtdrv.s."
  316. % (len(blob), RT_BASE, HDR))
  317. with tempfile.TemporaryDirectory() as td:
  318. open(os.path.join(td, "rtblob.bin"), "wb").write(blob)
  319. open(os.path.join(td, "rtcase.bin"), "wb").write(rec)
  320. src = os.path.join(HERE, "exec", "rtdrv.s")
  321. obj, raw = os.path.join(td, "r.o"), os.path.join(td, "r.bin")
  322. r = subprocess.run(["as", "--32", "-I", td, "-o", obj, src],
  323. capture_output=True)
  324. if r.returncode != 0:
  325. sys.exit("as failed on rtdrv.s:\n"
  326. + r.stderr.decode(errors="replace"))
  327. r = subprocess.run(["objcopy", "-O", "binary", obj, raw],
  328. capture_output=True)
  329. if r.returncode != 0:
  330. sys.exit("objcopy failed:\n" + r.stderr.decode(errors="replace"))
  331. img = open(raw, "rb").read()
  332. # E9 00 00: a near jump to offset 3 from offset 0, so the displacement is
  333. # 3 - (0 + 3) = 0. The driver is the first thing after the jump, which is
  334. # what makes the rel16 zero and therefore useless as a check on the target
  335. # -- the check that matters is the one below, that the blob landed where the
  336. # driver thinks it did.
  337. if img[0:3] != b"\xe9\x00\x00":
  338. sys.exit("rtdrv.s does not open with a 3-byte near JMP over itself: %s"
  339. % img[0:3].hex())
  340. if img[RT_BASE:RT_BASE + len(blob)] != blob:
  341. sys.exit("the blob is not at RT_BASE=%04X in the assembled image -- "
  342. "rtdrv.s and rt_exec.py disagree about the layout"
  343. % RT_BASE)
  344. gap = img[DRIVER_TOP:RT_BASE]
  345. if gap != b"\x00" * len(gap):
  346. sys.exit("the driver has outgrown its space: rtdrv.s no longer fits "
  347. "under %04X, so it is overwriting the runtime" % DRIVER_TOP)
  348. return img
  349. def parse_record(raw, want_index):
  350. """Split the serial output into (case index, printed, dumped) or raise.
  351. Nothing here is lenient. A driver that printed nothing, a truncated
  352. record, or a record for the wrong case are all failures with a reason, not
  353. a comparison against an empty string that happens to pass.
  354. """
  355. if not raw or raw[0] != SOH:
  356. raise AssertionError("no record header: the driver printed %r" % raw[:64])
  357. if len(raw) < 7 or raw[5] != STX:
  358. raise AssertionError("malformed record header %r" % raw[:16])
  359. idx = int(raw[1:5], 16)
  360. if idx != want_index:
  361. raise AssertionError("record is for case %04X, expected %04X"
  362. % (idx, want_index))
  363. if raw[-1] != EOT:
  364. raise AssertionError("record was never closed (EOT): the last call did "
  365. "not return, or the machine halted early")
  366. end = raw.index(ETX, 6)
  367. if end < 0:
  368. raise AssertionError("record has no ETX: the calls printed nothing and "
  369. "the driver never got past them")
  370. printed, dump = raw[6:end], raw[end + 1:-1]
  371. if len(dump) % 2 or not re.fullmatch(b"[0-9A-F]*", dump):
  372. raise AssertionError("the memory report %r is not hex" % dump)
  373. return idx, printed, bytes.fromhex(dump.decode())
  374. # ----------------------------------------------------------------- the cases
  375. def build_cases():
  376. """Every check, in the order the names are printed.
  377. Each case is a list of calls plus two expectations: the bytes the entry(s)
  378. printed, and the bytes that must be at `dumpadr' afterwards. `dump=0' means
  379. the case asserts nothing about memory, which is itself an assertion: the
  380. report is empty, so a case cannot quietly acquire a memory check it did not
  381. ask for.
  382. The addresses handed to an entry as its argument are MEMORY addresses, not
  383. image offsets -- that is the whole of LoadBias, and it is the difference
  384. between a read entry storing where the harness looks and storing one page
  385. lower where nothing checks it. V below is "the memory address of STORE",
  386. which is what every read case wants.
  387. """
  388. cs = []
  389. V = STORE + LOAD_BIAS
  390. def add(name, calls, out=b"", stdin=b"", dump=0, dumpbytes=b"",
  391. dumpadr=STORE, note=""):
  392. cs.append({"name": name, "calls": calls, "out": out, "in": stdin,
  393. "dump": dump, "dumpbytes": dumpbytes, "dumpadr": dumpadr,
  394. "note": note, "index": len(cs)})
  395. def s(ent, arg):
  396. return (ent, arg, F_STACK, 0, 0, 0)
  397. def x(ent, arg):
  398. return (ent, arg, F_AX, 0, 0, 0)
  399. def n(ent):
  400. return (ent, 0, 0, 0, 0, 0)
  401. def i(ent):
  402. return (ent, 0, F_INL, 0, 0, 0)
  403. def s2(ent, arg, arg2):
  404. """Two stack words: arg2 is pushed first (it lands at [BP+6]), arg
  405. second (at [BP+4]). real_from_int(src, dst) takes its source first."""
  406. return (ent, arg, F_STACK | F_STACK2, arg2, 0, 0)
  407. def s3(ent, a, b, c):
  408. """Three stack words: c is pushed first (landing at [BP+8]), then b
  409. ([BP+6]), then a ([BP+4])."""
  410. return (ent, a, F_STACK | F_STACK3, b, c, 0)
  411. def s4(ent, a, b, c, d):
  412. """Four stack words, d first: d=[BP+10], c=[BP+8], b=[BP+6],
  413. a=[BP+4]."""
  414. return (ent, a, F_STACK | F_STACK4, b, c, d)
  415. def s2cx(ent, arg, arg2, cx):
  416. """Two stack words AND CX: arg2 is pushed first (it lands at [BP+6]),
  417. arg second (at [BP+4]), and CX carries the maximum length. strstore is
  418. the only entry that wants all three."""
  419. return (ent, arg, F_STACK | F_STACK2 | F_CX, arg2, cx, 0)
  420. def scx(ent, arg, cx):
  421. """One stack word and CX, for rdstr: dest at [BP+4], limit in CX."""
  422. return (ent, arg, F_STACK | F_CX, 0, cx, 0)
  423. # initmem is handed the header in AX -- and the header's MEMORY address, not
  424. # its image offset. Given the image offset it reads the two words it wants
  425. # out of blank space, finds both zero, and clears nothing at all: a silent
  426. # no-op, which is the reason this case poisons the data area to AAH first.
  427. add("initmem zeroes globals",
  428. [x("initmem", HDR + LOAD_BIAS)], dump=DLEN, dumpadr=DATA,
  429. dumpbytes=b"\x00" * DLEN,
  430. note="the data area is poisoned to AA first, so a zero is a result")
  431. add("file_assign copies the name",
  432. [s2("file_assign", FILEBLK + LOAD_BIAS, SST_HELLO + LOAD_BIAS)],
  433. dump=10, dumpbytes=b"\xff\xff\x00\x00hello\x00",
  434. dumpadr=FILEBLK,
  435. note="handle=FFFF, mode=0, then the NUL-terminated name")
  436. for v, why in [(0, "the one value where a signed/unsigned slip is invisible"),
  437. (1, ""), (-1, ""), (7, ""), (10, ""), (42, ""),
  438. (100, ""), (12345, ""), (-32768, "the sign bit"), (32767, "")]:
  439. add("wrint(%d)" % v, [s("wrint", v & 0xFFFF)],
  440. out=str(v).encode(), note=why)
  441. # --- the 6-byte MBF real: integer -> real ---------------------------
  442. # The source word is passed in the case record's input bytes (CASE+5),
  443. # and the destination at STORE; both are memory addresses, as always.
  444. def mbf(v):
  445. """TP3's measured 6-byte Real for an integer (CALC-PORT.md)."""
  446. if v == 0:
  447. return bytes(6)
  448. neg = v < 0
  449. a = abs(v)
  450. k = a.bit_length() - 1
  451. e = 129 + k
  452. n = (a - (1 << k)) << (39 - k)
  453. b = bytearray([e]) + bytearray((n >> (8 * i)) & 0xFF
  454. for i in range(5))
  455. if neg:
  456. b[5] |= 0x80
  457. return bytes(b)
  458. def mbf_float(x):
  459. """The measured 6-byte MBF nearest to a fractional value."""
  460. if x == 0:
  461. return bytes(6)
  462. neg = x < 0
  463. a = abs(x)
  464. k = math.floor(math.log2(a))
  465. e = 129 + k
  466. n = int(round((a / (2.0 ** k) - 1.0) * (1 << 39)))
  467. if n >= (1 << 39):
  468. n = 0
  469. e += 1
  470. b = bytearray([e]) + bytearray((n >> (8 * i)) & 0xFF for i in range(5))
  471. if neg:
  472. b[5] |= 0x80
  473. return bytes(b)
  474. SRC = CASE + 5 + LOAD_BIAS
  475. for v in (0, 1, 2, 3, 5, 7, 42, 1000, 32767, -1, -2, -42, -32768):
  476. add("real_from_int(%d)" % v,
  477. [s2("real_from_int", STORE + LOAD_BIAS, SRC)],
  478. stdin=struct.pack("<h", v),
  479. dump=6, dumpbytes=mbf(v), dumpadr=STORE,
  480. note="the measured exponent bias is 129")
  481. # --- MBF addition and subtraction -----------------------------------
  482. # The driver preloads 12-byte blocks (a then b); real_add/real_sub take
  483. # the block's address, read both operands, and overwrite a with the result.
  484. BLOCKS = [(1, 2), (3, 5), (5, 3), (1, 1), (1000, 1), (-3, 2),
  485. (42, 1000), (32767, 1), (0, 5), (-3, -2),
  486. (6, 2), (10, 2), (15, 3), (1000, 2), (-6, 2), (6, -2),
  487. (-6, -2), (0, 5), (5, 2), (42, 6)]
  488. for bi, (a, b) in enumerate(BLOCKS):
  489. blk = DSHIFT+0x2230 + 16 * bi
  490. add("real_add(%d,%d)" % (a, b),
  491. [s("real_add", blk + LOAD_BIAS)],
  492. dump=6, dumpbytes=mbf(a + b), dumpadr=blk,
  493. note="the measured MBF bytes, the reference's readd")
  494. add("real_sub(%d,%d)" % (a, b),
  495. [s("real_sub", blk + LOAD_BIAS)],
  496. dump=6, dumpbytes=mbf(a - b), dumpadr=blk,
  497. note="the measured MBF bytes, the reference's resub")
  498. add("real_mul(%d,%d)" % (a, b),
  499. [s("real_mul", blk + LOAD_BIAS)],
  500. dump=6, dumpbytes=mbf(a * b), dumpadr=blk,
  501. note="the measured MBF bytes, the reference's remult")
  502. if b != 0 and a % b == 0:
  503. q = mbf(a // b)
  504. elif a == 5 and b == 2:
  505. q = bytes.fromhex("820000000020") # 2.5
  506. else:
  507. q = None
  508. if q is not None:
  509. add("real_div(%d,%d)" % (a, b),
  510. [s("real_div", blk + LOAD_BIAS)],
  511. dump=6, dumpbytes=q, dumpadr=blk,
  512. note="the measured MBF bytes, the reference's rediv")
  513. # --- recmp: AX := -1/0/+1 for a<b, a=b, a>b ------------------------
  514. # The driver stores every call's AX at AXOUT, so these cases can read the
  515. # result. The operand block is the same one the arithmetic cases use.
  516. seen = set()
  517. for bi, (a, b) in enumerate(BLOCKS):
  518. if (a, b) in seen:
  519. continue
  520. seen.add((a, b))
  521. blk = DSHIFT+0x2230 + 16 * bi
  522. want = (a > b) - (a < b)
  523. add("real_cmp(%d,%d)" % (a, b),
  524. [s("real_cmp", blk + LOAD_BIAS)],
  525. dump=2, dumpbytes=struct.pack("<H", want & 0xFFFF), dumpadr=AXOUT,
  526. note="the reference's recmp, returned as AX = -1/0/+1")
  527. # Fractional blocks, compared through the exponent/mantissa path.
  528. FRAC = [(3.7, 3.7), (3.9, 3.7), (-3.7, -3.9), (0.5, 0.25),
  529. (-0.5, 0.5), (12345.75, 12345.5), (2.5, 0.0), (-2.5, 0.0),
  530. (0.99, 0.0), (1.9, 0.0)]
  531. for fi, (a, b) in enumerate(FRAC):
  532. blk = DSHIFT+0x2370 + 16 * fi
  533. want = (a > b) - (a < b)
  534. add("real_cmp(%g,%g)" % (a, b),
  535. [s("real_cmp", blk + LOAD_BIAS)],
  536. dump=2, dumpbytes=struct.pack("<H", want & 0xFFFF), dumpadr=AXOUT,
  537. note="fractional magnitudes: the mantissa decides")
  538. add("real_cmp(2^40,0)",
  539. [s("real_cmp", DSHIFT+0x2410 + LOAD_BIAS)],
  540. dump=2, dumpbytes=struct.pack("<h", 1), dumpadr=AXOUT,
  541. note="the exponent alone decides")
  542. # --- reint: Int(real), in place --------------------------------------
  543. # a is the real; the block's b is unused. The expected bytes are the
  544. # measured MBF of trunc(a), because reint returns a REAL, not an integer
  545. # (TPSRC's xint pushes the six-byte result back).
  546. for bi, (a, b) in enumerate(BLOCKS):
  547. if a in (0, 1, -3, 32767):
  548. blk = DSHIFT+0x2230 + 16 * bi
  549. add("real_int(%d)" % a,
  550. [s("real_int", blk + LOAD_BIAS)],
  551. dump=6, dumpbytes=mbf(math.trunc(a)), dumpadr=blk,
  552. note="the integer part as a real, the reference's reint")
  553. for fi, (a, b) in enumerate(FRAC):
  554. blk = DSHIFT+0x2370 + 16 * fi
  555. add("real_int(%g)" % a,
  556. [s("real_int", blk + LOAD_BIAS)],
  557. dump=6, dumpbytes=mbf(math.trunc(a)), dumpadr=blk,
  558. note="the fraction is masked away; the sign and exponent stay")
  559. add("real_int(2^40)",
  560. [s("real_int", DSHIFT+0x2410 + LOAD_BIAS)],
  561. dump=6, dumpbytes=bytes.fromhex("A90000000000"), dumpadr=DSHIFT+0x2410,
  562. note="exponent >= 0A8h is already integral and returns unchanged")
  563. # --- fmtreal: real_fmt writes a length-prefixed, left-padded field -----
  564. # CX = (decimals << 8) | width, [BP+4] = real, [BP+6] = destination.
  565. def fmt(name, blk, width, dec, text):
  566. add(name,
  567. [s2cx("real_fmt", blk + LOAD_BIAS, FMTDEST + LOAD_BIAS,
  568. (dec << 8) | width)],
  569. dump=1 + len(text), dumpbytes=bytes([len(text)]) + text,
  570. dumpadr=FMTDEST,
  571. note="the reference's fmtreal; dest[0] is the field length")
  572. add("real_fmt(from int 1)",
  573. [s2("real_from_int", STORE + LOAD_BIAS, SRC),
  574. s2cx("real_fmt", STORE + LOAD_BIAS, FMTDEST + LOAD_BIAS, 0)],
  575. stdin=struct.pack("<h", 1), dump=2, dumpbytes=bytes([1]) + b"1",
  576. dumpadr=FMTDEST, note="a known-good 1.0 from real_from_int")
  577. fmt("real_fmt(1:0:0)", DSHIFT+0x2230, 0, 0, b"1")
  578. fmt("real_fmt(0:0:0)", DSHIFT+0x22b0, 0, 0, b"0")
  579. fmt("real_fmt(3.7:0:2)", DSHIFT+0x2370, 0, 2, b"3.70")
  580. fmt("real_fmt(12345.75:12:2)", DSHIFT+0x23c0, 12, 2, b" 12345.75")
  581. fmt("real_fmt(0.5:6:3)", DSHIFT+0x23a0, 6, 3, b" 0.500")
  582. fmt("real_fmt(-3.7:0:1)", DSHIFT+0x2390, 0, 1, b"-3.7")
  583. fmt("real_fmt(0:5:2)", DSHIFT+0x22b0, 5, 2, b" 0.00")
  584. fmt("real_fmt(2.5:0:0)", DSHIFT+0x23d0, 0, 0, b"3")
  585. fmt("real_fmt(3.7:10:25)", DSHIFT+0x2370, 10, 25, b"3.7000E+00")
  586. # --- ascreal: real_val parses an ASCIIZ string ------------------------
  587. # A dump of AXOUT shows the returned offset, or FFFFh on a hard error.
  588. ASC_STRINGS = [b"3.5\0", b"-2.25\0", b"1E3\0", b"0.5\0",
  589. b"12345.75\0", b"3.5x\0", b"1E\0", b"1E40\0",
  590. b"\0", b".5\0", b"1.2.3\0", b"35\0", b"5E-1\0", b"5.\0", b"5.0\0", b"12345\0", b"1234575\0", b"1234.5\0", b"1000\0", b"123\0", b"1234\0", b"12340\0", b"12341\0", b"12341E0\0", b"12345E0\0"]
  591. WANT_OFF = [3, 5, 3, 3, 8, 3, 0xFFFF, 0xFFFF, 0, 2, 0xFFFF, 2, 4, 2, 3, 5, 7, 6, 4, 3, 4, 5, 5, 7, 7]
  592. off = 0
  593. for txt, want in zip(ASC_STRINGS, WANT_OFF):
  594. add("real_val(%r) offset" % txt[:-1],
  595. [s2("real_val", ASCSRC + off + LOAD_BIAS, STORE + LOAD_BIAS)],
  596. dump=2, dumpbytes=struct.pack("<H", want & 0xFFFF), dumpadr=AXOUT,
  597. note="AX = the first unparsed character, FFFFh on error")
  598. off += len(txt)
  599. # The parsed VALUE. On a hard error nothing is stored, and STORE is
  600. # poisoned with EEEE (its first two bytes are all the driver writes), so
  601. # those cases also prove the destination was left alone.
  602. VAL = [3.5, -2.25, 1000.0, 0.5, 12345.75, 3.5, None, None, 0.0, 0.5,
  603. None, 35.0, 0.5, 5.0, 5.0, 12345.0, 1234575.0, 1234.5,
  604. 1000.0, 123.0, 1234.0, 12340.0, 12341.0, 12341.0, 12345.0]
  605. off = 0
  606. for txt, v in zip(ASC_STRINGS, VAL):
  607. wantv = (b"\xEE\xEE" + bytes(4)) if v is None else mbf_float(v)
  608. add("real_val(%r) value" % txt[:-1],
  609. [s2("real_val", ASCSRC + off + LOAD_BIAS, STORE + LOAD_BIAS)],
  610. dump=6, dumpbytes=wantv, dumpadr=STORE,
  611. note="the packed MBF, or the untouched EEEE poison on error")
  612. off += len(txt)
  613. for bi, (a, b) in [(0, (12340, 5)), (1, (12340, 1))]:
  614. blk = DSHIFT+0x2500 + 16 * bi
  615. add("real_add(%d,%d)" % (a, b), [s("real_add", blk + LOAD_BIAS)],
  616. dump=6, dumpbytes=mbf(a + b), dumpadr=blk,
  617. note="a large value plus a small digit: the add core's alignment")
  618. for bi, a in [(2, 12341), (3, 12345), (4, 12340)]:
  619. blk = DSHIFT+0x2500 + 16 * bi
  620. add("real_mul(%d,1)" % a, [s("real_mul", blk + LOAD_BIAS)],
  621. dump=6, dumpbytes=mbf(a), dumpadr=blk,
  622. note="multiplying by 1.0 must be exact")
  623. # --- real_write: [BP+4]=decimals, [BP+6]=width, [BP+8]=real ----------
  624. def wr(name, blk, width, dec, text):
  625. add(name, [s3("real_write", dec, width, blk + LOAD_BIAS)], out=text,
  626. note="fmtreal's field, padded and printed by the entry itself")
  627. wr("real_write(3.7:0:2)", DSHIFT+0x2370, 0, 2, b"3.70")
  628. wr("real_write(3.7:10:2)", DSHIFT+0x2370, 10, 2, b" 3.70")
  629. wr("real_write(12345.75:12:2)", DSHIFT+0x23c0, 12, 2, b" 12345.75")
  630. wr("real_write(0.5:0:3)", DSHIFT+0x23a0, 0, 3, b"0.500")
  631. wr("real_write(-3.7:0:1)", DSHIFT+0x2390, 0, 1, b"-3.7")
  632. wr("real_write(2.5:0:0)", DSHIFT+0x23d0, 0, 0, b"3")
  633. wr("real_write(42:0:0)", DSHIFT+0x2290, 0, 0, b"42")
  634. wr("real_write(3.7:18:FFFF)", DSHIFT+0x2370, 18, 0xFFFF,
  635. b" 3.7000000000E+00")
  636. # --- set algebra ----------------------------------------------------
  637. # set_op: [BP+4]=(op<<8)|size, [BP+6]=dest, [BP+8]=right, [BP+10]=left.
  638. # 0 = intersection, 1 = union, 2 = difference.
  639. def setop(name, op, size, dest, right, left, want):
  640. add(name, [s4("set_op", (op << 8) | size, dest + LOAD_BIAS,
  641. right + LOAD_BIAS, left + LOAD_BIAS)],
  642. dump=size, dumpbytes=want, dumpadr=dest,
  643. note="the loops in EmitSetOps, exact bytes")
  644. setop("set_op /", 0, 1, DSHIFT+0x25b0, DSHIFT+0x2590, DSHIFT+0x2580, b"\x08")
  645. setop("set_op +", 1, 1, DSHIFT+0x25b0, DSHIFT+0x2590, DSHIFT+0x2580, b"\x1A")
  646. setop("set_op -", 2, 1, DSHIFT+0x25b0, DSHIFT+0x2590, DSHIFT+0x2580, b"\x02")
  647. setop("set_op / 2 bytes", 0, 2, DSHIFT+0x25e0, DSHIFT+0x25d0, DSHIFT+0x25c0, b"\x08\x03")
  648. # set_cmp: [BP+4]=size, [BP+6]=right, [BP+8]=left; AX = equal bit 0,
  649. # strict subset bit 1, strict superset bit 2.
  650. def setcmp(name, size, left, right, mask):
  651. add(name, [s3("set_cmp", size, right + LOAD_BIAS, left + LOAD_BIAS)],
  652. dump=2, dumpbytes=struct.pack("<H", mask), dumpadr=AXOUT,
  653. note="the reference mask: 1 equal, 2 subset, 4 superset")
  654. setcmp("set_cmp size 0", 0, DSHIFT+0x2580, DSHIFT+0x2590, 1)
  655. setcmp("set_cmp A=A", 1, DSHIFT+0x2580, DSHIFT+0x2580, 1)
  656. setcmp("set_cmp A~B", 1, DSHIFT+0x2580, DSHIFT+0x2590, 0)
  657. setcmp("set_cmp C<A", 1, DSHIFT+0x25a0, DSHIFT+0x2580, 2)
  658. setcmp("set_cmp A>C", 1, DSHIFT+0x2580, DSHIFT+0x25a0, 4)
  659. setcmp("set_cmp 2-byte incomparable", 2, DSHIFT+0x25c0, DSHIFT+0x25d0, 0)
  660. add("set_copy 2 bytes",
  661. [s3("set_copy", 2, DSHIFT+0x25c0 + LOAD_BIAS, DSHIFT+0x25e0 + LOAD_BIAS)],
  662. dump=2, dumpbytes=b"\x0A\x0F", dumpadr=DSHIFT+0x25e0,
  663. note="[BP+4]=size, [BP+6]=src, [BP+8]=dest")
  664. add("wrchar('A')", [s("wrchar", ord("A"))], out=b"A")
  665. add("wrchar('!')", [s("wrchar", ord("!"))], out=b"!")
  666. add("wrbool(0)", [s("wrbool", 0)], out=b"FALSE")
  667. add("wrbool(1)", [s("wrbool", 1)], out=b"TRUE")
  668. add("wrbool(2)", [s("wrbool", 2)], out=b"TRUE",
  669. note="any non-zero is true, as TP3's xwrb does")
  670. add("wrln", [n("wrln")], out=b"\r\n")
  671. add("stackchk returns", [n("stackchk")], out=b"",
  672. note="no output: the assertion is that control came back at all")
  673. add("writeln(42) writeln TRUE in one program",
  674. [s("wrint", 42), s("wrchar", ord(" ")), s("wrbool", 1), n("wrln")],
  675. out=b"42 TRUE\r\n",
  676. note="four calls back to back, so no register leaks between them")
  677. # rdint: the value, and then the rdln that must eat the rest of the line.
  678. for text, want in [(b" 42abc", 42), (b"-17 x", -17), (b"+5", 5),
  679. (b"0", 0), (b" 007", 7), (b"1234", 1234)]:
  680. add("rdint(%r) then rdln" % text,
  681. [s("rdint", V), n("rdln")], out=b"", stdin=text, dump=2,
  682. dumpbytes=struct.pack("<H", want & 0xFFFF),
  683. note="the delimiter must be left for rdln, which prints nothing")
  684. for c in "Q7":
  685. add("rdchar(%r)" % c, [s("rdchar", V)], out=b"", stdin=c.encode(),
  686. dump=2, dumpbytes=bytes([ord(c), 0xEE]),
  687. note="rdchar stores ONE byte, so the poison is still in byte 2")
  688. # rdbyte: the same reader as rdint, entered through its own frame, and the
  689. # whole point is the WIDTH of the store. A poison byte sits just past the
  690. # variable, so the word store rdint used would show up here as byte 2
  691. # turning into the high byte of the value instead of staying EE. -1 is
  692. # the case where the two disagree most visibly: the value is FFFF, so a
  693. # word store writes FF twice.
  694. for text, want in [(b"200", 200), (b"-1", 0xFF), (b"7", 7), (b"0", 0)]:
  695. add("rdbyte(%r) then rdln" % text,
  696. [s("rdbyte", V), n("rdln")], out=b"", stdin=text, dump=2,
  697. dumpbytes=bytes([want & 0xFF, 0xEE]),
  698. note="rdbyte stores ONE byte, so the poison is still in byte 2")
  699. for c, want in [("T", 1), ("y", 1), ("1", 1), ("F", 0), ("n", 0), ("0", 0)]:
  700. add("rdbool(%r)" % c, [s("rdbool", V)], out=b"", stdin=c.encode(),
  701. dump=2, dumpbytes=struct.pack("<H", want & 0xFFFF))
  702. add("rdint at end of input leaves the variable alone",
  703. [s("rdint", V), n("rdln")], out=b"", stdin=b"", dump=2,
  704. dumpbytes=b"\xee\xee",
  705. note="TP3's xrdint returns to rnerr without storing; the poison stays")
  706. add("rdbyte at end of input leaves the variable alone",
  707. [s("rdbyte", V), n("rdln")], out=b"", stdin=b"", dump=2,
  708. dumpbytes=b"\xee\xee",
  709. note="rdscan returns CX=2 on the ^Z path, so rdbyte stores nothing")
  710. add("wrtinl reads its length and characters from the return address",
  711. [i("wrtinl")], out=b"hello",
  712. note="the one entry whose argument is not a stack word")
  713. # wrstr is the other way round from wrtinl: its argument IS a stack word,
  714. # but a word that has to be a POINTER, so the string's bytes live in memory
  715. # instead of after the call. WSTR (seeded in rtdrv.s, restated here) is a
  716. # length byte and five characters - the same bytes wrtinl reads from the
  717. # code stream - so the two cases must print the same word. The argument
  718. # carries LOAD_BIAS because a runtime entry dereferences a MEMORY address,
  719. # not an image offset.
  720. add("wrstr prints the length byte's worth of characters",
  721. [s("wrstr", WSTR + LOAD_BIAS)], out=b"hello",
  722. note="the entry dereferences the stack word; the bytes are in memory")
  723. add("wrstr prints nothing for an empty string",
  724. [s("wrstr", WSTR_EMPTY + LOAD_BIAS)], out=b"",
  725. note="a length byte of 0 must print no characters at all")
  726. # --- rdstr: the entry behind readln(s)/read(s). Its argument is a stack
  727. # word (the destination address), it walks it with SI, and CX is the limit.
  728. # The destination is pre-filled with 0xEE in rtdrv.s, so every byte past the
  729. # stored length is a checked claim that the entry did not write it.
  730. add("rdstr reads a line into a string",
  731. [scx("rdstr", RDSTR_DST + LOAD_BIAS, 10)], out=b"", stdin=b"hello\n",
  732. dump=11, dumpbytes=bytes([5]) + b"hello" + b"\xee" * 5,
  733. dumpadr=RDSTR_DST,
  734. note="length byte then the characters; nothing past the length is written")
  735. add("rdstr truncates a line longer than CX",
  736. [scx("rdstr", RDSTR_DST + LOAD_BIAS, 3)], out=b"", stdin=b"abcdef\n",
  737. dump=5, dumpbytes=bytes([3]) + b"abc" + b"\xee",
  738. dumpadr=RDSTR_DST,
  739. note="CX is the limit; the rest of the line is left unread, not stored")
  740. add("rdstr stores length 0 for an empty line",
  741. [scx("rdstr", RDSTR_DST + LOAD_BIAS, 10)], out=b"", stdin=b"\n",
  742. dump=3, dumpbytes=bytes([0]) + b"\xee\xee", dumpadr=RDSTR_DST,
  743. note="the terminator is the first character, so only dest[0] is written")
  744. add("rdstr at end of input stores length 0",
  745. [scx("rdstr", RDSTR_DST + LOAD_BIAS, 10)], out=b"", stdin=b"",
  746. dump=3, dumpbytes=bytes([0]) + b"\xee\xee", dumpadr=RDSTR_DST,
  747. note="getch returns ^Z at end of input, a terminator like CR and LF")
  748. # --- strstore: `s := t' for two string variables. dest at [BP+4], source
  749. # at [BP+6], maximum in CX. The count is min(source length, CX), the source
  750. # is never written, and the destination tail past the new length is left
  751. # exactly as it was.
  752. add("strstore copies a whole string",
  753. [s2cx("strstore", SST_DST + LOAD_BIAS, SST_HELLO + LOAD_BIAS, 20)],
  754. out=b"", dump=8, dumpbytes=bytes([5]) + b"hello" + b"\xee" * 2,
  755. dumpadr=SST_DST,
  756. note="dest length and characters; the tail past them is still EE")
  757. add("strstore truncates to the destination maximum",
  758. [s2cx("strstore", SST_DST + LOAD_BIAS, SST_LONG + LOAD_BIAS, 4)],
  759. out=b"", dump=8, dumpbytes=bytes([4]) + b"abcd" + b"\xee" * 3,
  760. dumpadr=SST_DST,
  761. note="count := min(source length, CX); the source itself is not shortened")
  762. add("strstore copies an empty string",
  763. [s2cx("strstore", SST_DST + LOAD_BIAS, SST_EMPTY + LOAD_BIAS, 10)],
  764. out=b"", dump=4, dumpbytes=bytes([0]) + b"\xee" * 3, dumpadr=SST_DST,
  765. note="CX 0 must copy nothing -- LOOP would otherwise run 65536 times")
  766. add("strstore leaves the source alone",
  767. [s2cx("strstore", SST_DST + LOAD_BIAS, SST_LONG + LOAD_BIAS, 4)],
  768. out=b"", dump=9, dumpbytes=bytes([8]) + b"abcdefgh", dumpadr=SST_LONG,
  769. note="TP3 shortens a stack copy of the source; this entry writes none")
  770. # --- cmpstr: string compare, AX := -1 / 0 / +1 for left <, =, > right.
  771. # left is at [BP+6] and right at [BP+4] (the same order s2cx gives which
  772. # string goes where), so this compares 'hello' against 'abcdefgh'.
  773. #
  774. # The sign is NOT checked here, and cannot be: a case record dumps MEMORY,
  775. # and the driver does not capture AX after a call. What this case can say
  776. # is that the entry runs to RET without touching an operand - it is
  777. # read-only - and check_bp_contract separately proves it hands BP back. The
  778. # SIGN is pinned by t48_strcmp and t49_strcmp_lit, which run whole compiled
  779. # programs on qemu and print all six relations as booleans; a program is a
  780. # better observer of a return value than a record with no field for one.
  781. add("cmpstr compares 'hello' with 'abcdefgh' and writes neither",
  782. [s2cx("cmpstr", SST_LONG + LOAD_BIAS, SST_HELLO + LOAD_BIAS, 20)],
  783. out=b"", dump=6, dumpbytes=bytes([5]) + b"hello", dumpadr=SST_HELLO,
  784. note="the dump is the LEFT operand ([BP+6]); it must come back unchanged")
  785. # --- strcat: s := s + t. The same argument shape as strstore -- source
  786. # pushed first, destination at [BP+4], maximum in CX -- but the DESTINATION
  787. # is READ as well as written: dest[0] is the existing length, dest[0]+src[0]
  788. # is the new one, and the characters are copied to dest[1]+dest[0]. That is
  789. # why the destination is seeded: strstore may ignore its destination, but
  790. # 0xEE here would read as a length of 238 and raise the overflow error.
  791. #
  792. # The overflow EXIT cannot be a case: a record dumps memory after the calls
  793. # return, and the error path leaves through INT 21h AH=4C, so the record is
  794. # never closed. t55_strcat_overflow runs a whole compiled program and
  795. # checks the exit code instead - see run_com_exec.py and the .exit file.
  796. add("strcat appends the source to the destination",
  797. [s2cx("strcat", SST_CAT + LOAD_BIAS, SST_WORLD + LOAD_BIAS, 20)],
  798. out=b"", dump=11, dumpbytes=bytes([10]) + b"helloworld",
  799. dumpadr=SST_CAT,
  800. note="'hello' + 'world': the new length is the SUM of the two")
  801. add("strcat appends to an empty destination",
  802. [s2cx("strcat", SST_CAT0 + LOAD_BIAS, SST_WORLD + LOAD_BIAS, 20)],
  803. out=b"", dump=6, dumpbytes=bytes([5]) + b"world", dumpadr=SST_CAT0,
  804. note="a zero destination length is the ordinary case, not a special one")
  805. add("strcat of an empty source leaves the destination alone",
  806. [s2cx("strcat", SST_CAT + LOAD_BIAS, SST_EMPTY + LOAD_BIAS, 20)],
  807. out=b"", dump=8, dumpbytes=bytes([5]) + b"hello" + b"\xee" * 2,
  808. dumpadr=SST_CAT,
  809. note="a zero SOURCE length takes JCXZ; a LOOP would run 65536 times")
  810. add("strcat at the destination maximum does not overflow",
  811. [s2cx("strcat", SST_CAT + LOAD_BIAS, SST_WORLD + LOAD_BIAS, 10)],
  812. out=b"", dump=11, dumpbytes=bytes([10]) + b"helloworld",
  813. dumpadr=SST_CAT,
  814. note="sum == CX is allowed; only sum > CX is the error")
  815. return cs
  816. def run_case(c, blob, entries, boot):
  817. rec = case_bytes(c, entries)
  818. img = build_image(blob, rec)
  819. with tempfile.TemporaryDirectory() as td:
  820. floppy = os.path.join(td, "f.img")
  821. open(floppy, "wb").write(build_floppy(boot, img, c["in"], load_seg=0, inlen_off=0x7000, inbuf_off=0x7004))
  822. return run_qemu(floppy)
  823. def main(argv):
  824. if "--probe" in argv:
  825. return probe()
  826. cases = build_cases()
  827. if "--list" in argv:
  828. for c in cases:
  829. print(c["name"])
  830. return 0
  831. show = "--show" in argv
  832. only = [a for a in argv if not a.startswith("-")]
  833. blob, entries, code_end = load_runtime(RT_BASE)
  834. boot = build_boot_sector(load_seg=0, inlen=0x7000, copy_bytes=0x7000, scratch=0x1000)
  835. # Before any machine is started: the contract the driver depends on.
  836. nscan, nborrow = check_bp_contract(blob, entries, RT_BASE, code_end)
  837. print(" %-52s PASS (%d entries scanned, %d borrow BP and hand it "
  838. "back)" % ("every entry preserves BP", nscan, nborrow))
  839. npass = 1
  840. nfail = 0
  841. for c in cases:
  842. if only and not any(o in c["name"] for o in only):
  843. continue
  844. # A marker byte inside an expectation would make the framing
  845. # ambiguous, so it is refused rather than mis-parsed.
  846. for b in (SOH, STX, ETX, EOT):
  847. if bytes([b]) in c["out"]:
  848. sys.exit("case %s: its expected output contains the framing "
  849. "byte %02X, which would be indistinguishable from the "
  850. "driver's own markers" % (c["name"], b))
  851. raw, rc, note = run_case(c, blob, entries, boot)
  852. if show:
  853. print(c["name"], "->", repr(visible_output(raw)))
  854. continue
  855. try:
  856. _, printed, dumped = parse_record(raw, c["index"])
  857. except AssertionError as e:
  858. print(" %-52s FAIL %s" % (c["name"], e))
  859. nfail += 1
  860. continue
  861. if printed != c["out"]:
  862. print(" %-52s FAIL printed %s, want %s"
  863. % (c["name"], visible_output(printed),
  864. visible_output(c["out"])))
  865. nfail += 1
  866. continue
  867. if dumped != c["dumpbytes"]:
  868. print(" %-52s FAIL +%04X = %s, want %s"
  869. % (c["name"], c["dumpadr"], dumped.hex() or "-",
  870. c["dumpbytes"].hex() or "-"))
  871. nfail += 1
  872. continue
  873. print(" %-52s PASS%s" % (c["name"],
  874. (" (" + c["note"] + ")") if c["note"] else ""))
  875. npass += 1
  876. if show:
  877. return 0
  878. print("\nruntime entries: %d passed, %d failed (of %d)"
  879. % (npass, nfail, npass + nfail))
  880. return 1 if nfail else 0
  881. if __name__ == "__main__":
  882. sys.exit(main(sys.argv[1:]))