check_8086.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  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. # String comparisons add branch sites without adding `CMP AX,CX' (their
  201. # value path tests with OR AX,AX, not a comparison), so the shape walk is
  202. # what finds them. 5*6 = thirty `if <string compare>' in t48, 6+3+4 = the
  203. # thirteen in t49; every one is a plain IF, so every declared nibble is 4.
  204. "t48_strcmp": [4] * 30,
  205. "t49_strcmp_lit": [4] * 13,
  206. # One `if (a + b) = 'helloworld'' -- a comparison whose LEFT operand is a
  207. # concatenation result, so the branch is new even though the shape is the
  208. # same plain IF as t48/t49.
  209. "t52_strcat_nest": [4],
  210. }
  211. BRANCH_WHY = {
  212. "t09_if": "three `if' statements",
  213. "t10_while": "two `while' loops",
  214. "t11_for": "a `for .. to' and a `for .. downto'",
  215. "t12_repeat": "two `repeat .. until' loops",
  216. "t15_label": "a single `if .. then goto'",
  217. "t22_case": "a `case' with two arms, both falling through to `end'",
  218. "t30_forloop": "a single `for .. to'",
  219. "t32_forexit": "a `for .. to' and an `if .. exit'",
  220. "t48_strcmp": "five blocks of six `if <string compare>'",
  221. "t49_strcmp_lit": "three blocks of `if <string literal compare>'",
  222. "t52_strcat_nest": "one `if (a + b) = 'helloworld'', a concatenation "
  223. "result compared",
  224. }
  225. def source_operators(path):
  226. """The relational operators of every `writeln (x OP y)' in source order."""
  227. with open(path) as fh:
  228. text = fh.read()
  229. return [m.group(1) for m in RE_WRITELN_OP.finditer(text)]
  230. def check_runtime_region(verbose):
  231. """A: the runtime's code region is pure code, so this is exhaustive."""
  232. blob, code_end, size = probe_runtime()
  233. if code_end > len(blob):
  234. return ["the probe says code ends at %d but only %d bytes were dumped"
  235. % (code_end, len(blob))]
  236. instrs, stopped = sweep(blob[:code_end])
  237. problems = []
  238. if stopped is not None:
  239. problems.append("the runtime's code region does not sweep clean: it "
  240. "stops at offset %04X, so this check cannot claim to "
  241. "have looked at everything" % stopped)
  242. bad = [(o, b) for (o, b, _) in instrs if b == 0x0F]
  243. for o, _ in bad:
  244. problems.append("runtime offset %04X is a 0F-prefixed opcode, which "
  245. "does not exist on an 8086" % o)
  246. if verbose:
  247. print("runtime code region 0..%d: %d instructions swept%s"
  248. % (code_end, len(instrs),
  249. "" if stopped is None else ", stopped at %04X" % stopped))
  250. return problems, dict(size=size, code_end=code_end,
  251. ninstr=len(instrs), n0f=len(bad))
  252. def program_code_region(img, rt_size):
  253. """(start, end) of the generated program's code region, both as IMAGE
  254. offsets, derived from the image rather than from a restated constant: the
  255. entry jump's displacement is the program's own answer for where the code
  256. begins, and hdrCS is `pc + LoadBias' with pc the end of the generated code,
  257. so hdrCS - LoadBias is where it stops.
  258. Where the header is gets two INDEPENDENT answers: rt_size is the runtime
  259. blob's size as the probe measured it, and comimage.find_header locates the
  260. header inside this file by its own self-consistency equation. They are
  261. measurements of two different artefacts, so their agreement is evidence,
  262. and a .COM whose two disagree has no code region this checker can state --
  263. guessing one of them would be the restated constant this function exists
  264. to avoid."""
  265. hdr_probe = comimage.ENT_SZ + rt_size
  266. hdr_file = comimage.find_header(img)
  267. if hdr_file is None or hdr_file != hdr_probe:
  268. return None
  269. if len(img) < hdr_file + comimage.HDR_SZ:
  270. return None
  271. start = comimage.entry_target(img)
  272. if start is None:
  273. return None
  274. start %= 0x10000
  275. hdr_cs = int.from_bytes(img[hdr_file + 2:hdr_file + 4], "little")
  276. end = hdr_cs - comimage.LOAD_BIAS
  277. if not (start <= end <= len(img)):
  278. return None
  279. return start, end
  280. def main(argv):
  281. verbose = "-v" in argv
  282. if not os.path.exists(COMTEST):
  283. print("FAIL: %s not built; run tests/run_com_tests.sh first" % COMTEST)
  284. return 1
  285. rt_problems, rt_info = check_runtime_region(verbose)
  286. problems = list(rt_problems)
  287. work = tempfile.mkdtemp(prefix="check8086.")
  288. try:
  289. fixtures = sorted(glob.glob(os.path.join(HERE, "fixtures", "*.pas")))
  290. paths = "\n".join(fixtures) + "\n"
  291. subprocess.run([COMTEST], input=paths.encode(), cwd=work,
  292. stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  293. val_nibbles = collections.Counter() # EmSetcc sites
  294. br_nibbles = collections.Counter() # EmJcc sites
  295. ncmp = nval = 0
  296. nlinked = 0
  297. ordered = 0
  298. ordered_cmps = 0
  299. pinned = []
  300. nbr_anchored = 0
  301. swept_fixtures = 0
  302. swept_bytes = 0
  303. missing = []
  304. for src in fixtures:
  305. name = os.path.basename(src)[:-4]
  306. com = os.path.join(work, name + ".COM")
  307. if not os.path.exists(com):
  308. continue # a fixture that errors by design
  309. with open(com, "rb") as fh:
  310. img = fh.read()
  311. region = program_code_region(img, rt_info["size"])
  312. if region is None:
  313. problems.append("%s: could not locate the code region "
  314. "(entry jump, program header and hdrCS do not "
  315. "agree)" % name)
  316. continue
  317. start, end = region
  318. code = img[start:end]
  319. nlinked += 1
  320. # A branch is found three ways -- anchored on the comparison
  321. # before it (B, C) and by its own shape (D) -- and the three
  322. # overlap, so they all go into one set of offsets and each site
  323. # is counted and validated once, at the end. Counting as we went
  324. # reported 28 branch sites when there are 13: the anchored walk
  325. # and the shape walk were both adding to the same histogram.
  326. branch_offs = set()
  327. # B: every CMP AX,CX -- the only shape EmCmpAxCx emits, and the
  328. # only thing that can precede either lowering. ParseCmp puts a
  329. # comparison VALUE there; the FOR test puts a BRANCH there.
  330. fixture_val_nibbles = []
  331. for off in find_all(code, CMP_AX_CX):
  332. ncmp += 1
  333. nxt = code[off + 2:off + 8]
  334. if is_value_shape(nxt):
  335. val_nibbles[nxt[3] & 0x0F] += 1
  336. fixture_val_nibbles.append(nxt[3] & 0x0F)
  337. nval += 1
  338. elif is_branch_shape(nxt):
  339. branch_offs.add(off + 2)
  340. nbr_anchored += 1
  341. else:
  342. problems.append(
  343. "%s+%04X: CMP AX,CX is followed by %s -- neither "
  344. "TP3 shape (MOV AX,1; Jcc +1; DEC AX, or Jcc +3; EJMP)"
  345. % (name, start + off,
  346. " ".join("%02X" % b for b in nxt) or "nothing"))
  347. # C: every CMP AX,0000 -- what IF, WHILE and REPEAT emit to test
  348. # a Boolean. The three-byte anchor matters: a bare 3D also matches
  349. # displacement and immediate bytes, and taking it as an opcode is
  350. # what produced two false alarms here (a 3D inside a CALL
  351. # displacement in t11_for, and one inside a string in t21_mixed).
  352. for off in find_all(code, CMP_AX_ZERO):
  353. nxt = code[off + 3:off + 6]
  354. if is_branch_shape(nxt):
  355. branch_offs.add(off + 3)
  356. nbr_anchored += 1
  357. else:
  358. problems.append(
  359. "%s+%04X: CMP AX,0000 is followed by %s -- the three "
  360. "EmCmpAxi (0) sites are IF/WHILE/REPEAT tests, so "
  361. "each must be Jcc +3; EJMP"
  362. % (name, start + off,
  363. " ".join("%02X" % b for b in nxt) or "nothing"))
  364. # D: the same branches again, found by their own SHAPE rather than
  365. # by the instruction before it. This is what covers the CASE arm,
  366. # whose EmCmpAxi carries a label rather than 0 and which no
  367. # comparison anchor can therefore find. Discovery only -- the
  368. # counting and the checking happen once, over branch_offs.
  369. for off in find_all(code, b"\xe9"):
  370. if off >= 2 and is_branch_shape(code[off - 2:off + 1]):
  371. branch_offs.add(off - 2)
  372. # E: each branch, counted under the nibble the COMPILER DECLARES
  373. # (the emitted one put back through the negation) and required to
  374. # be a condition Compiler.mod claims to use.
  375. for off in sorted(branch_offs):
  376. emitted = code[off] & 0x0F
  377. declared = negated(emitted)
  378. br_nibbles[declared] += 1
  379. if declared not in JCC_NIBBLES:
  380. problems.append(
  381. "%s+%04X: branch emits %Xh, which declares the "
  382. "condition %Xh -- not one of the conditions "
  383. "Compiler.mod declares for a branch (%s)"
  384. % (name, start + off, code[off], declared,
  385. ", ".join("%Xh" % k
  386. for k in sorted(JCC_NIBBLES))))
  387. # F: where the region sweeps clean -- no inline strings, so the
  388. # whole thing is code -- assert no 0F opcode over it as well. This
  389. # is extra coverage, not the backbone, and how much of the suite
  390. # it reached is printed rather than implied.
  391. instrs, stopped = sweep(code, comimage.LOAD_BIAS + start)
  392. if stopped is None:
  393. swept_fixtures += 1
  394. swept_bytes += len(code)
  395. for o, b, _ in instrs:
  396. if b == 0x0F:
  397. problems.append(
  398. "%s+%04X: 0F-prefixed opcode in swept code"
  399. % (name, start + o))
  400. if verbose:
  401. print("%-16s code %d..%d%s"
  402. % (name, start, end,
  403. "" if stopped is None
  404. else " (sweep stops at +%04X: string data)"
  405. % stopped))
  406. # H: WHICH branch condition each site means, per fixture. The
  407. # table is read off the .pas sources, not off the image -- that is
  408. # the whole point, since a table measured from the image would
  409. # agree with any behaviour including a wrong one.
  410. #
  411. # This closes a hole the mutations above MEASURED rather than
  412. # assumed. Clause E only rejects an emitted nibble that declares
  413. # a condition Compiler.mod does not claim for a branch, and the
  414. # inversion in EmJcc turns IF's declared 4 into an emitted 5 --
  415. # which is CASE's declared nibble, and IS claimed. So dropping
  416. # the inversion for the IF and CASE sites alone left this check
  417. # green (mutation M5) while every conditional in every program
  418. # took the wrong path. The FOR sites happen not to be blind that
  419. # way, because FOR declares C and F, whose negations D and E are
  420. # not declared for anything here -- so a whole-suite inversion is
  421. # caught by luck, and a partial one is not.
  422. #
  423. # It catches M5 because `declared' is computed from the EMITTED
  424. # byte by going back through the inversion: an IF that emitted
  425. # JccShort instead of JccShortInv reads back as declaring 5.
  426. got_br = sorted(negated(code[o] & 0x0F) for o in branch_offs)
  427. want_br = sorted(BRANCH_SITES.get(name, got_br))
  428. if got_br != want_br:
  429. problems.append(
  430. "%s: its %d branch sites declare %s, but reading the "
  431. "source says they are %s (%s)"
  432. % (name, len(got_br),
  433. " ".join("%Xh" % n for n in got_br),
  434. " ".join("%Xh" % n for n in want_br),
  435. BRANCH_WHY.get(name, "not a fixture with branches")))
  436. elif name in BRANCH_SITES:
  437. pinned.append(name)
  438. # G: for the one fixture whose operators are known from its
  439. # SOURCE, the emitted nibbles must match them IN ORDER. This is
  440. # what catches a swap, which the shape clauses above cannot: a
  441. # SETG where a SETGE belongs is still a perfectly good 8086 shape.
  442. if name == ORDER_FIXTURE:
  443. ops = source_operators(src)
  444. want = [OP_NIBBLE[o] for o in ops]
  445. if fixture_val_nibbles != want:
  446. problems.append(
  447. "%s: the %d comparisons emitted as %s, but the source "
  448. "asks in order for %s"
  449. % (name, len(fixture_val_nibbles),
  450. " ".join("%Xh" % n for n in fixture_val_nibbles),
  451. " ".join("%s=%Xh" % (o, n)
  452. for o, n in zip(ops, want))))
  453. else:
  454. ordered += 1
  455. ordered_cmps += len(want)
  456. finally:
  457. subprocess.run(["rm", "-rf", work])
  458. # D: the declared conditions must all be exercised, or the check above is
  459. # only as good as whatever the suite happened to use.
  460. for nib, op in sorted(SETCC_NIBBLES.items()):
  461. if val_nibbles[nib] == 0:
  462. missing.append("`%s' (SETcc %02Xh) is declared by ParseCmp but no "
  463. "fixture uses it as a comparison" % (op, nib | 0x90))
  464. for nib, where in sorted(JCC_NIBBLES.items()):
  465. if br_nibbles[nib] == 0:
  466. missing.append("the %s branch (Jcc nibble %Xh) is declared but no "
  467. "fixture emits it" % (where, nib))
  468. problems += missing
  469. print("8086 check: %d comparison sites, %d lowered to a Boolean value, "
  470. "%d lowered to a branch" % (ncmp, nval, nbr_anchored))
  471. print(" value conditions : %s"
  472. % " ".join("%s x%d" % (SETCC_NIBBLES.get(n, "?%X?" % n), c)
  473. for n, c in sorted(val_nibbles.items())))
  474. print(" branch conditions : %s"
  475. % " ".join("%s x%d" % (JCC_NIBBLES.get(n, "?%X?" % n), c)
  476. for n, c in sorted(br_nibbles.items())))
  477. print(" runtime: %d bytes, %d swept, %d 0F-prefixed"
  478. % (rt_info["size"], rt_info["ninstr"], rt_info["n0f"]))
  479. print(" program code: %d of %d fixtures swept end to end, "
  480. "%d bytes" % (swept_fixtures, nlinked, swept_bytes))
  481. print(" %s: %d comparisons matched against their source "
  482. "operators, in order" % (ORDER_FIXTURE, ordered_cmps))
  483. # H must have been reached for EVERY fixture it names. A table row for a
  484. # fixture that no longer links, or whose region cannot be located, would
  485. # otherwise sit there looking like coverage while testing nothing.
  486. for name in sorted(set(BRANCH_SITES) - set(pinned)):
  487. problems.append("%s: clause H expects %d branch sites, but the "
  488. "fixture contributed none -- the row is not being "
  489. "tested" % (name, len(BRANCH_SITES[name])))
  490. print(" clause H: %d of %d fixtures matched the branch "
  491. "conditions read off their source"
  492. % (len(pinned), len(BRANCH_SITES)))
  493. if ordered == 0:
  494. problems.append("%s: no comparison was matched against its source "
  495. "operator, so a swapped condition would go unnoticed"
  496. % ORDER_FIXTURE)
  497. if ncmp == 0:
  498. problems.append("no comparison sites were found at all, so nothing "
  499. "above was checked")
  500. if problems:
  501. print("FAIL: %d problem(s)" % len(problems))
  502. for p in problems:
  503. print(" - %s" % p)
  504. return 1
  505. print("PASS: no 0F-prefixed opcode in the runtime or in swept program "
  506. "code; every comparison is")
  507. print(" lowered to TP3's shape; all %d declared comparison "
  508. "conditions and all %d declared branch conditions are exercised"
  509. % (len(SETCC_NIBBLES), len(JCC_NIBBLES)))
  510. return 0
  511. if __name__ == "__main__":
  512. sys.exit(main(sys.argv))