check_8086.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. #!/usr/bin/env python3
  2. """check_8086.py -- require that the emitted image contains no opcode the 8086
  3. lacks, and that what replaced the two illegal ones is TP3's shape.
  4. The bug
  5. -------
  6. `EmJcc` emitted `0F 8x rel16' and `EmSetcc` emitted `0F 9x' (SETcc). Both are
  7. 386-and-later: on an 8086 the byte `0F' is not an opcode prefix at all, so
  8. every conditional branch and every comparison *value* in every compiled program
  9. was an illegal instruction on the machine TP3 targets.
  10. Nothing in the build could see it, and each thing that might have failed is
  11. worth naming:
  12. * it compiled, because the compiler only ever writes bytes;
  13. * FCML decoded it happily, because FCML's -m16 mode is 386 -- and FCML is
  14. this project's independent disassembler, so the one tool that could have
  15. objected was the one tool guaranteed to agree;
  16. * the .COM linked and its layout checked, because `0F 84 lo hi' is a
  17. perfectly well-formed 4-byte displacement field;
  18. * the runtime golden did not move, because the runtime emits no 0F;
  19. * and all 30 fixtures ran to the right answers under qemu-system-i386,
  20. whose lowest CPU model is 486. There is no `-cpu 8086'.
  21. That last one is why this file exists rather than a one-line change to the
  22. harness. The gap was invisible to the oracle, not absent.
  23. Why the two regions are treated differently
  24. ------------------------------------------
  25. The runtime's code region (bytes 0..code-end of the runtime blob) is pure
  26. code, so it can be swept exhaustively and the sweep must complete. That is a
  27. hard guarantee: no 0F-prefixed instruction anywhere in the runtime.
  28. The generated program's code region is NOT pure code -- inline string literals
  29. are emitted into it, after the code, and a linear sweep desynchronises on them
  30. and then reports an undefined opcode in the middle of a string. t09_if is the
  31. demonstration: the sweep decodes 16 real instructions and then dies at the
  32. bytes `FE E9 0D 00', which are ASCII text, not code. So a sweep of the
  33. program region cannot answer "is this 8086-legal", and a check that believed it
  34. would be worse than no check.
  35. What is sound instead is to name the SITES rather than the boundaries. Both
  36. illegal opcodes were emitted in answer to exactly one thing -- the result of a
  37. comparison -- and a comparison is always introduced by one of two sequences
  38. this compiler emits and nothing else emits:
  39. 3B C1 EmCmpAxCx, CMP AX,CX
  40. 3D lo hi EmCmpAxi, CMP AX,imm16
  41. Every one of the seven EmJcc call sites and the single EmSetcc call site sits
  42. immediately after one of those, which is checked by reading each site rather
  43. than assumed (see the site table in Compiler.mod). So this check asserts:
  44. A. the runtime's code region sweeps clean and holds no 0F-prefixed opcode;
  45. B. every `3B C1` in every fixture's code region is followed by one of exactly
  46. two 8086-legal shapes --
  47. B8 01 00 7X 01 48 a Boolean VALUE: MOV AX,1 ; Jcc +1 ; DEC AX
  48. 7X 03 E9 a BRANCH: Jcc +3 ; EJMP
  49. which are EmSetcc and EmJcc respectively;
  50. C. every `3D lo hi' is followed by the BRANCH shape, because all five
  51. EmCmpAxi sites are IF/WHILE/REPEAT/CASE tests;
  52. D. every condition nibble the compiler's two tables declare must appear at
  53. least once across the suite.
  54. D is what stops the check being vacuous, and it is why t33_cmpops exists: when
  55. this was first written, measuring the emitted nibbles showed `=', `<>' and
  56. `<=' were never used in a comparison anywhere in the suite. A check that only
  57. requires "some condition was lowered" would have been satisfied by the three
  58. that were covered.
  59. The shapes in B and C are hand-derived from the original compiler, not read
  60. back out of this compiler's output:
  61. TPSRC8 246-295 IF / WHILE / REPEAT are each
  62. MOV AL,brnchop ; MOV AH,#$03 ; CALL eword
  63. PUSH pc ; CALL ejump
  64. i.e. a SHORT Jcc of displacement 3 stepping over a 3-byte
  65. EJMP. brnchop is the condition's own opcode, so the short
  66. jump is taken straight to the target.
  67. TPSRC9 412-424 flgbool turns a comparison's flags into a value with
  68. MOV AX,#0001 ; <Jcc> +1 ; DEC AX
  69. AX stays 1 because the DEC was stepped over.
  70. Both are `short Jcc ; one byte ; something`, which is why the displacement is
  71. 3 in one case and 1 in the other and why both are two instructions and a byte.
  72. Usage: check_8086.py [-v] (from shell/)
  73. """
  74. import collections
  75. import glob
  76. import os
  77. import re
  78. import subprocess
  79. import sys
  80. import tempfile
  81. HERE = os.path.dirname(os.path.abspath(__file__))
  82. SHELL = os.path.dirname(HERE)
  83. sys.path.insert(0, HERE)
  84. import disasm16 # noqa: E402
  85. # The image layout: where the header is, how big it is, and the load bias this
  86. # file used to carry on its own account. See tests/check_comimage.py, which
  87. # asserts that every reader of a linked image gets these from one place.
  88. import comimage # noqa: E402
  89. COMTEST = os.path.join(SHELL, "comtest")
  90. # Declared by Compiler.mod, restated here rather than asked of the code under
  91. # test, and cross-checked against what the suite emits. These are the low
  92. # nibbles of the `0F 9x' SETcc opcodes ParseCmp passes to EmSetcc:
  93. # = 94H <> 95H < 9CH > 9FH >= 9DH <= 9EH
  94. # EmSetcc's job is to answer "is this comparison true", so its Jcc is the
  95. # comparison's OWN opcode and these keys are what appears in the image.
  96. SETCC_NIBBLES = {0x4: "=", 0x5: "<>", 0xC: "<", 0xD: ">=", 0xE: "<=",
  97. 0xF: ">"}
  98. # ... and of the `0F 8x' Jcc opcodes the seven EmJcc sites pass:
  99. # IF 84H REPEAT 84H CASE 85H FOR 8CH (downto) / 8FH (to)
  100. # EmJcc JUMPS TO the target while these are "taken when the condition is
  101. # false" (IF's JZ is patched to the ELSE, so it must fire when the test
  102. # failed), and the Jcc-over-EJMP shape steps over the EJMP when it is TAKEN.
  103. # Those two things are opposite, so the byte in the image is the negation of
  104. # the nibble declared here: the Jcc code's low bit IS the negation bit, and
  105. # negating a condition is `n XOR 1' (JE/JNE are 74h/75h). Hence the XOR below,
  106. # and hence clause E is stated on the emitted byte rather than on these keys.
  107. JCC_NIBBLES = {0x4: "IF / REPEAT", 0x5: "CASE", 0xC: "FOR downto",
  108. 0xF: "FOR to"}
  109. def negated(nib):
  110. """The Jcc nibble the image will carry for a site that declares `nib'."""
  111. return nib ^ 1
  112. CMP_AX_CX = b"\x3b\xc1" # EmCmpAxCx
  113. CMP_AX_ZERO = b"\x3d\x00\x00" # EmCmpAxi (0)
  114. def probe_runtime():
  115. """(blob, code_end) from the existing rt_exec probe, so this check does
  116. not restate Runtime.RT_Size or where the code stops."""
  117. out = subprocess.run([sys.executable, os.path.join(HERE, "rt_exec.py"),
  118. "--probe"], capture_output=True, text=True,
  119. cwd=SHELL)
  120. if out.returncode != 0:
  121. sys.stderr.write(out.stdout + out.stderr)
  122. raise SystemExit("FAIL: rt_exec.py --probe failed")
  123. size = code_end = None
  124. for line in out.stdout.splitlines():
  125. if line.endswith("bytes") and size is None:
  126. size = int(line.split()[0])
  127. if line.startswith("code ends at"):
  128. code_end = int(line.split()[3])
  129. if size is None or code_end is None:
  130. raise SystemExit("FAIL: could not read the runtime size from the probe")
  131. # the probe prints a hex dump of the blob; rebuild it from the .COM-free
  132. # dump lines so this check needs no second source of the runtime bytes
  133. blob = bytearray()
  134. for line in out.stdout.splitlines():
  135. parts = line.split()
  136. # one offset word then 16 two-digit hex bytes
  137. if len(parts) == 17 and all(len(p) == 2 for p in parts[1:]):
  138. try:
  139. blob += bytes(int(p, 16) for p in parts[1:])
  140. except ValueError:
  141. pass
  142. return bytes(blob), code_end, size
  143. def sweep(code, base=0):
  144. """Linear sweep. Returns (instructions, offset_it_stopped_at_or_None).
  145. An instruction is (offset, opcode_byte, length)."""
  146. out = []
  147. pc = 0
  148. while pc < len(code):
  149. text, length = disasm16.decode(code[pc:], base + pc)
  150. if length == 0:
  151. return out, pc
  152. out.append((pc, code[pc], length))
  153. pc += length
  154. return out, None
  155. def find_all(hay, needle, start=0):
  156. i = start
  157. while True:
  158. i = hay.find(needle, i)
  159. if i < 0:
  160. return
  161. yield i
  162. i += 1
  163. def is_value_shape(nxt):
  164. """B8 01 00 7X 01 48 -- EmSetcc: MOV AX,#0001 ; Jcc +1 ; DEC AX"""
  165. return (len(nxt) >= 6 and nxt[0] == 0xB8 and nxt[1] == 0x01
  166. and nxt[2] == 0x00 and 0x70 <= nxt[3] <= 0x7F
  167. and nxt[4] == 0x01 and nxt[5] == 0x48)
  168. def is_branch_shape(nxt):
  169. """7X 03 E9 -- EmJcc: Jcc +3, stepping over a 3-byte EJMP"""
  170. return (len(nxt) >= 3 and 0x70 <= nxt[0] <= 0x7F
  171. and nxt[1] == 0x03 and nxt[2] == 0xE9)
  172. # Pascal relational operator -> the condition nibble the 8086 short Jcc must
  173. # carry for that operator to be answered correctly. `=' is JE (74h), and so
  174. # on down the 70h..7Fh table. Restated here, and checked against what
  175. # Compiler.mod's ParseCmp table passes to EmSetcc, so the two cannot drift.
  176. OP_NIBBLE = {"=": 0x4, "<>": 0x5, "<": 0xC, "<=": 0xE, ">": 0xF, ">=": 0xD}
  177. # The fixture whose SOURCE ORDER of operators is compared against the order
  178. # the compiler emitted them in. This is the clause that catches a swap: the
  179. # clauses above only ask "is this an 8086 shape", and a shape with the wrong
  180. # nibble is still a shape. It has to be a fixture whose every comparison is a
  181. # value (so every one is an EmSetcc site, in source order) and which uses all
  182. # six operators -- hence t33_cmpops, which exists partly for this.
  183. ORDER_FIXTURE = "t33_cmpops"
  184. RE_WRITELN_OP = re.compile(
  185. r"writeln\s*\(\s*\w+\s*(=|<>|<=|>=|<|>)\s*\w+\s*\)")
  186. # H: for each fixture that HAS a branch, the multiset of conditions its branch
  187. # sites declare, read off the .pas source by hand, with the reading spelled
  188. # out in BRANCH_WHY so a later reader can check the reasoning rather than
  189. # trust it. Declared nibbles, i.e. before EmJcc's inversion, so 4 = IF/WHILE/
  190. # REPEAT, 5 = CASE, C = FOR downto, F = FOR to.
  191. BRANCH_SITES = {
  192. "t09_if": [4, 4, 4], # three `if ... then ... else'
  193. "t10_while": [4, 4], # two `while ... do'
  194. "t11_for": [15, 12], # one `for .. to' (F), one `for .. downto' (C)
  195. "t12_repeat": [4, 4], # two `repeat .. until'
  196. "t15_label": [4], # one `if x < 5 then goto 1'
  197. "t22_case": [5, 5], # `case x of' with two arms, both fall to end
  198. "t30_forloop": [15], # one `for .. to'
  199. "t32_forexit": [15, 4], # one `for .. to' plus one `if .. exit'
  200. }
  201. BRANCH_WHY = {
  202. "t09_if": "three `if' statements",
  203. "t10_while": "two `while' loops",
  204. "t11_for": "a `for .. to' and a `for .. downto'",
  205. "t12_repeat": "two `repeat .. until' loops",
  206. "t15_label": "a single `if .. then goto'",
  207. "t22_case": "a `case' with two arms, both falling through to `end'",
  208. "t30_forloop": "a single `for .. to'",
  209. "t32_forexit": "a `for .. to' and an `if .. exit'",
  210. }
  211. def source_operators(path):
  212. """The relational operators of every `writeln (x OP y)' in source order."""
  213. with open(path) as fh:
  214. text = fh.read()
  215. return [m.group(1) for m in RE_WRITELN_OP.finditer(text)]
  216. def check_runtime_region(verbose):
  217. """A: the runtime's code region is pure code, so this is exhaustive."""
  218. blob, code_end, size = probe_runtime()
  219. if code_end > len(blob):
  220. return ["the probe says code ends at %d but only %d bytes were dumped"
  221. % (code_end, len(blob))]
  222. instrs, stopped = sweep(blob[:code_end])
  223. problems = []
  224. if stopped is not None:
  225. problems.append("the runtime's code region does not sweep clean: it "
  226. "stops at offset %04X, so this check cannot claim to "
  227. "have looked at everything" % stopped)
  228. bad = [(o, b) for (o, b, _) in instrs if b == 0x0F]
  229. for o, _ in bad:
  230. problems.append("runtime offset %04X is a 0F-prefixed opcode, which "
  231. "does not exist on an 8086" % o)
  232. if verbose:
  233. print("runtime code region 0..%d: %d instructions swept%s"
  234. % (code_end, len(instrs),
  235. "" if stopped is None else ", stopped at %04X" % stopped))
  236. return problems, dict(size=size, code_end=code_end,
  237. ninstr=len(instrs), n0f=len(bad))
  238. def program_code_region(img, rt_size):
  239. """(start, end) of the generated program's code region, both as IMAGE
  240. offsets, derived from the image rather than from a restated constant: the
  241. entry jump's displacement is the program's own answer for where the code
  242. begins, and hdrCS is `pc + LoadBias' with pc the end of the generated code,
  243. so hdrCS - LoadBias is where it stops.
  244. Where the header is gets two INDEPENDENT answers: rt_size is the runtime
  245. blob's size as the probe measured it, and comimage.find_header locates the
  246. header inside this file by its own self-consistency equation. They are
  247. measurements of two different artefacts, so their agreement is evidence,
  248. and a .COM whose two disagree has no code region this checker can state --
  249. guessing one of them would be the restated constant this function exists
  250. to avoid."""
  251. hdr_probe = comimage.ENT_SZ + rt_size
  252. hdr_file = comimage.find_header(img)
  253. if hdr_file is None or hdr_file != hdr_probe:
  254. return None
  255. if len(img) < hdr_file + comimage.HDR_SZ:
  256. return None
  257. start = comimage.entry_target(img)
  258. if start is None:
  259. return None
  260. start %= 0x10000
  261. hdr_cs = int.from_bytes(img[hdr_file + 2:hdr_file + 4], "little")
  262. end = hdr_cs - comimage.LOAD_BIAS
  263. if not (start <= end <= len(img)):
  264. return None
  265. return start, end
  266. def main(argv):
  267. verbose = "-v" in argv
  268. if not os.path.exists(COMTEST):
  269. print("FAIL: %s not built; run tests/run_com_tests.sh first" % COMTEST)
  270. return 1
  271. rt_problems, rt_info = check_runtime_region(verbose)
  272. problems = list(rt_problems)
  273. work = tempfile.mkdtemp(prefix="check8086.")
  274. try:
  275. fixtures = sorted(glob.glob(os.path.join(HERE, "fixtures", "*.pas")))
  276. paths = "\n".join(fixtures) + "\n"
  277. subprocess.run([COMTEST], input=paths.encode(), cwd=work,
  278. stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  279. val_nibbles = collections.Counter() # EmSetcc sites
  280. br_nibbles = collections.Counter() # EmJcc sites
  281. ncmp = nval = 0
  282. nlinked = 0
  283. ordered = 0
  284. ordered_cmps = 0
  285. pinned = []
  286. nbr_anchored = 0
  287. swept_fixtures = 0
  288. swept_bytes = 0
  289. missing = []
  290. for src in fixtures:
  291. name = os.path.basename(src)[:-4]
  292. com = os.path.join(work, name + ".COM")
  293. if not os.path.exists(com):
  294. continue # a fixture that errors by design
  295. with open(com, "rb") as fh:
  296. img = fh.read()
  297. region = program_code_region(img, rt_info["size"])
  298. if region is None:
  299. problems.append("%s: could not locate the code region "
  300. "(entry jump, program header and hdrCS do not "
  301. "agree)" % name)
  302. continue
  303. start, end = region
  304. code = img[start:end]
  305. nlinked += 1
  306. # A branch is found three ways -- anchored on the comparison
  307. # before it (B, C) and by its own shape (D) -- and the three
  308. # overlap, so they all go into one set of offsets and each site
  309. # is counted and validated once, at the end. Counting as we went
  310. # reported 28 branch sites when there are 13: the anchored walk
  311. # and the shape walk were both adding to the same histogram.
  312. branch_offs = set()
  313. # B: every CMP AX,CX -- the only shape EmCmpAxCx emits, and the
  314. # only thing that can precede either lowering. ParseCmp puts a
  315. # comparison VALUE there; the FOR test puts a BRANCH there.
  316. fixture_val_nibbles = []
  317. for off in find_all(code, CMP_AX_CX):
  318. ncmp += 1
  319. nxt = code[off + 2:off + 8]
  320. if is_value_shape(nxt):
  321. val_nibbles[nxt[3] & 0x0F] += 1
  322. fixture_val_nibbles.append(nxt[3] & 0x0F)
  323. nval += 1
  324. elif is_branch_shape(nxt):
  325. branch_offs.add(off + 2)
  326. nbr_anchored += 1
  327. else:
  328. problems.append(
  329. "%s+%04X: CMP AX,CX is followed by %s -- neither "
  330. "TP3 shape (MOV AX,1; Jcc +1; DEC AX, or Jcc +3; EJMP)"
  331. % (name, start + off,
  332. " ".join("%02X" % b for b in nxt) or "nothing"))
  333. # C: every CMP AX,0000 -- what IF, WHILE and REPEAT emit to test
  334. # a Boolean. The three-byte anchor matters: a bare 3D also matches
  335. # displacement and immediate bytes, and taking it as an opcode is
  336. # what produced two false alarms here (a 3D inside a CALL
  337. # displacement in t11_for, and one inside a string in t21_mixed).
  338. for off in find_all(code, CMP_AX_ZERO):
  339. nxt = code[off + 3:off + 6]
  340. if is_branch_shape(nxt):
  341. branch_offs.add(off + 3)
  342. nbr_anchored += 1
  343. else:
  344. problems.append(
  345. "%s+%04X: CMP AX,0000 is followed by %s -- the three "
  346. "EmCmpAxi (0) sites are IF/WHILE/REPEAT tests, so "
  347. "each must be Jcc +3; EJMP"
  348. % (name, start + off,
  349. " ".join("%02X" % b for b in nxt) or "nothing"))
  350. # D: the same branches again, found by their own SHAPE rather than
  351. # by the instruction before it. This is what covers the CASE arm,
  352. # whose EmCmpAxi carries a label rather than 0 and which no
  353. # comparison anchor can therefore find. Discovery only -- the
  354. # counting and the checking happen once, over branch_offs.
  355. for off in find_all(code, b"\xe9"):
  356. if off >= 2 and is_branch_shape(code[off - 2:off + 1]):
  357. branch_offs.add(off - 2)
  358. # E: each branch, counted under the nibble the COMPILER DECLARES
  359. # (the emitted one put back through the negation) and required to
  360. # be a condition Compiler.mod claims to use.
  361. for off in sorted(branch_offs):
  362. emitted = code[off] & 0x0F
  363. declared = negated(emitted)
  364. br_nibbles[declared] += 1
  365. if declared not in JCC_NIBBLES:
  366. problems.append(
  367. "%s+%04X: branch emits %Xh, which declares the "
  368. "condition %Xh -- not one of the conditions "
  369. "Compiler.mod declares for a branch (%s)"
  370. % (name, start + off, code[off], declared,
  371. ", ".join("%Xh" % k
  372. for k in sorted(JCC_NIBBLES))))
  373. # F: where the region sweeps clean -- no inline strings, so the
  374. # whole thing is code -- assert no 0F opcode over it as well. This
  375. # is extra coverage, not the backbone, and how much of the suite
  376. # it reached is printed rather than implied.
  377. instrs, stopped = sweep(code, comimage.LOAD_BIAS + start)
  378. if stopped is None:
  379. swept_fixtures += 1
  380. swept_bytes += len(code)
  381. for o, b, _ in instrs:
  382. if b == 0x0F:
  383. problems.append(
  384. "%s+%04X: 0F-prefixed opcode in swept code"
  385. % (name, start + o))
  386. if verbose:
  387. print("%-16s code %d..%d%s"
  388. % (name, start, end,
  389. "" if stopped is None
  390. else " (sweep stops at +%04X: string data)"
  391. % stopped))
  392. # H: WHICH branch condition each site means, per fixture. The
  393. # table is read off the .pas sources, not off the image -- that is
  394. # the whole point, since a table measured from the image would
  395. # agree with any behaviour including a wrong one.
  396. #
  397. # This closes a hole the mutations above MEASURED rather than
  398. # assumed. Clause E only rejects an emitted nibble that declares
  399. # a condition Compiler.mod does not claim for a branch, and the
  400. # inversion in EmJcc turns IF's declared 4 into an emitted 5 --
  401. # which is CASE's declared nibble, and IS claimed. So dropping
  402. # the inversion for the IF and CASE sites alone left this check
  403. # green (mutation M5) while every conditional in every program
  404. # took the wrong path. The FOR sites happen not to be blind that
  405. # way, because FOR declares C and F, whose negations D and E are
  406. # not declared for anything here -- so a whole-suite inversion is
  407. # caught by luck, and a partial one is not.
  408. #
  409. # It catches M5 because `declared' is computed from the EMITTED
  410. # byte by going back through the inversion: an IF that emitted
  411. # JccShort instead of JccShortInv reads back as declaring 5.
  412. got_br = sorted(negated(code[o] & 0x0F) for o in branch_offs)
  413. want_br = sorted(BRANCH_SITES.get(name, got_br))
  414. if got_br != want_br:
  415. problems.append(
  416. "%s: its %d branch sites declare %s, but reading the "
  417. "source says they are %s (%s)"
  418. % (name, len(got_br),
  419. " ".join("%Xh" % n for n in got_br),
  420. " ".join("%Xh" % n for n in want_br),
  421. BRANCH_WHY.get(name, "not a fixture with branches")))
  422. elif name in BRANCH_SITES:
  423. pinned.append(name)
  424. # G: for the one fixture whose operators are known from its
  425. # SOURCE, the emitted nibbles must match them IN ORDER. This is
  426. # what catches a swap, which the shape clauses above cannot: a
  427. # SETG where a SETGE belongs is still a perfectly good 8086 shape.
  428. if name == ORDER_FIXTURE:
  429. ops = source_operators(src)
  430. want = [OP_NIBBLE[o] for o in ops]
  431. if fixture_val_nibbles != want:
  432. problems.append(
  433. "%s: the %d comparisons emitted as %s, but the source "
  434. "asks in order for %s"
  435. % (name, len(fixture_val_nibbles),
  436. " ".join("%Xh" % n for n in fixture_val_nibbles),
  437. " ".join("%s=%Xh" % (o, n)
  438. for o, n in zip(ops, want))))
  439. else:
  440. ordered += 1
  441. ordered_cmps += len(want)
  442. finally:
  443. subprocess.run(["rm", "-rf", work])
  444. # D: the declared conditions must all be exercised, or the check above is
  445. # only as good as whatever the suite happened to use.
  446. for nib, op in sorted(SETCC_NIBBLES.items()):
  447. if val_nibbles[nib] == 0:
  448. missing.append("`%s' (SETcc %02Xh) is declared by ParseCmp but no "
  449. "fixture uses it as a comparison" % (op, nib | 0x90))
  450. for nib, where in sorted(JCC_NIBBLES.items()):
  451. if br_nibbles[nib] == 0:
  452. missing.append("the %s branch (Jcc nibble %Xh) is declared but no "
  453. "fixture emits it" % (where, nib))
  454. problems += missing
  455. print("8086 check: %d comparison sites, %d lowered to a Boolean value, "
  456. "%d lowered to a branch" % (ncmp, nval, nbr_anchored))
  457. print(" value conditions : %s"
  458. % " ".join("%s x%d" % (SETCC_NIBBLES.get(n, "?%X?" % n), c)
  459. for n, c in sorted(val_nibbles.items())))
  460. print(" branch conditions : %s"
  461. % " ".join("%s x%d" % (JCC_NIBBLES.get(n, "?%X?" % n), c)
  462. for n, c in sorted(br_nibbles.items())))
  463. print(" runtime: %d bytes, %d swept, %d 0F-prefixed"
  464. % (rt_info["size"], rt_info["ninstr"], rt_info["n0f"]))
  465. print(" program code: %d of %d fixtures swept end to end, "
  466. "%d bytes" % (swept_fixtures, nlinked, swept_bytes))
  467. print(" %s: %d comparisons matched against their source "
  468. "operators, in order" % (ORDER_FIXTURE, ordered_cmps))
  469. # H must have been reached for EVERY fixture it names. A table row for a
  470. # fixture that no longer links, or whose region cannot be located, would
  471. # otherwise sit there looking like coverage while testing nothing.
  472. for name in sorted(set(BRANCH_SITES) - set(pinned)):
  473. problems.append("%s: clause H expects %d branch sites, but the "
  474. "fixture contributed none -- the row is not being "
  475. "tested" % (name, len(BRANCH_SITES[name])))
  476. print(" clause H: %d of %d fixtures matched the branch "
  477. "conditions read off their source"
  478. % (len(pinned), len(BRANCH_SITES)))
  479. if ordered == 0:
  480. problems.append("%s: no comparison was matched against its source "
  481. "operator, so a swapped condition would go unnoticed"
  482. % ORDER_FIXTURE)
  483. if ncmp == 0:
  484. problems.append("no comparison sites were found at all, so nothing "
  485. "above was checked")
  486. if problems:
  487. print("FAIL: %d problem(s)" % len(problems))
  488. for p in problems:
  489. print(" - %s" % p)
  490. return 1
  491. print("PASS: no 0F-prefixed opcode in the runtime or in swept program "
  492. "code; every comparison is")
  493. print(" lowered to TP3's shape; all %d declared comparison "
  494. "conditions and all %d declared branch conditions are exercised"
  495. % (len(SETCC_NIBBLES), len(JCC_NIBBLES)))
  496. return 0
  497. if __name__ == "__main__":
  498. sys.exit(main(sys.argv))