nonvacuity.sh 136 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119
  1. #!/bin/sh
  2. # The layout cases below rewrite tests/comimage.py and restore it within the
  3. # same second. A .pyc names the source by (mtime, size), and two versions of
  4. # this file can have both in common, so Python would keep executing the
  5. # MUTATED module after the restore. That made later cases report the mutation
  6. # as if it were the baseline. Do not write bytecode here.
  7. PYTHONDONTWRITEBYTECODE=1
  8. export PYTHONDONTWRITEBYTECODE
  9. # nonvacuity.sh -- prove the runtime checks can actually fail.
  10. #
  11. # A test that has never been seen red is not a test. This script breaks the
  12. # runtime on purpose, once per check, and asserts that the check goes red and
  13. # says something useful about the breakage. Then it restores the source and
  14. # asserts everything is green again.
  15. #
  16. # Each mutation below is a real bug that was in this file at some point, not an
  17. # invented one. That is the point: these are the mistakes we actually make
  18. # with 16-bit ModRM, so these are the ones the checks have to catch.
  19. #
  20. # audit_helpers.py name-versus-decode: catches a wrong ModRM that still
  21. # decodes cleanly, and both halves of a name that admits
  22. # an operand at all - `34 02' where the name promises
  23. # `34 01', and `35 01' where the row's opcode gate admits
  24. # only 34h. Both rows are new, and a row nobody has seen
  25. # reject anything accepts whatever it is shown.
  26. # audit_helpers.py coverage: catches a helper that has silently
  27. # dropped OUT of the audit, which is a green report about
  28. # a subject nobody looked at
  29. # run_com_tests.sh the .COM layout: catches a header that cannot be
  30. # located, a runtime size that disagrees with the image,
  31. # and an entry jump that starts in the wrong place
  32. # check_runtime.py golden: catches the same thing in the built
  33. # image
  34. # check_runtime.py decode sweep: catches a wrong instruction LENGTH
  35. # check_runtime.py branch targets: catches a wrong fixup
  36. # check_runtime.py entry goldens: catches a broken prologue
  37. # probe/modrm11.py the mod=11 table: catches the ModRM column itself
  38. # going wrong, which no amount of decoding will show
  39. # check_framedisp.py the BP disp rule: catches a displacement that reads
  40. # a different address than the symbol table named
  41. # tests/comimage.py the image layout: catches a sweep region built from a
  42. # literal instead of from two independent readings of the
  43. # file - the old RT_SZ went on reporting PASS while its
  44. # region began inside the runtime - and the layout itself
  45. # being written down a second time, now that one file
  46. # defines it and all three readers import it
  47. # rt_exec.py the BP contract: catches an entry that borrows BP to
  48. # reach its argument and does not hand it back. The bytes
  49. # are well formed, the golden is satisfied, the audit says
  50. # every helper emits what its name says, and the machine
  51. # triple-faults on the second call - so nothing short of
  52. # running it, or of stating the register contract
  53. # explicitly, can see it.
  54. # rt_exec.py the string entries: catches rdstr storing its first
  55. # character where the length byte belongs (a missing IncSi
  56. # puts `hello' back as 05 65 6c 6c 6f), and strstore
  57. # running LOOP with CX = 0 when the source is empty - 65536
  58. # iterations that wrap the segment and walk through the
  59. # driver's own case record. Both are same-size,
  60. # matrix-green slips that only a byte dump can see.
  61. # run_com_exec.py the frame rule: catches a local STRING packed with the
  62. # INTEGER rule, so its characters grow up over the saved BP
  63. # and the return address; readln into such a local then
  64. # runs the string's own bytes as an address. The fix moves
  65. # no well-formed instruction, so only execution - or the
  66. # size it pins in the compile matrix - can tell.
  67. # run_com_exec.py the string arms: catches the destination limit loaded
  68. # into AX instead of CX, the assignment pushes swapped so
  69. # the destination is read as its own source, and a string
  70. # read dispatched to the integer reader. Every one is the
  71. # right shape reading the wrong thing, and the MovCxi
  72. # helper is COMP in the audit, so no byte check can see it.
  73. # check_8086.py the 8086 opcodes: catches a conditional branch or a
  74. # SETcc emitted as `0F 8x'/`0F 9x', which are 386-only.
  75. # Nothing else here can: qemu-system-i386's lowest CPU
  76. # model is 486, where both are ordinary instructions, and
  77. # FCML's -m16 mode is a 386 too. Clause H additionally
  78. # catches the branch polarity being inverted, which is
  79. # still a legal opcode and therefore invisible to every
  80. # shape-based check.
  81. # run_com_exec.py behaviour: catches a*b that emits an ADD, `>'
  82. # and `>=' swapped, REPEAT..UNTIL that stops after one
  83. # pass, two procedures whose parameters collide, a left
  84. # operand overwritten by the right one's code (SaveLeft),
  85. # and a boolean `not' lowered as the integer one. Every
  86. # one of them sat in a fixture that COMPILED and was
  87. # never RUN, with every byte-level check green.
  88. # run_compile_tests.sh refusal: catches a boolean READ slipping
  89. # through to the runtime. TP3 refuses it with error 66
  90. # (TPSRC8 prderrtp; its runtime has no xrdbool), and
  91. # this compiler used to accept it and let rdbool store
  92. # a WORD into the one-byte variable. The mutation is
  93. # the one arm of IoCall's type dispatch that refuses it,
  94. # and only the COMPILE MATRIX can see a refusal: an ERR
  95. # fixture is never executed, so nothing that runs
  96. # programs on it can fail the check.
  97. # run_exec86.py behaviour (Exec86): catches the interpreter's OWN
  98. # reading of the machine: JE inverted in Exec86's Cond,
  99. # which swaps the two arms of every `=' in every program
  100. # while the emitted bytes, the sizes and every
  101. # byte-level check stay green. The image is fine; only
  102. # the thing reading it is wrong, so nothing that looks at
  103. # the image can fail this one.
  104. # runtest.py the R key: catches CmdRun poking nothing into
  105. # the interpreter at all, which faults on the first step
  106. # and prints "interpreter fault" instead of the guest's
  107. # answer. It is the only case here that needs a rebuilt
  108. # SHELL rather than a rebuilt compiler or runtime.
  109. #
  110. # The mod=11 cases do not need a rebuild -- they read the probe sources
  111. # directly -- so they are cheap, and they are the ones that matter most: the
  112. # table they guard is the one thing in this project that was wrong in the
  113. # documentation while the code was right, and a table that is wrong in the
  114. # code produces bytes that decode perfectly.
  115. #
  116. # Usage: tests/nonvacuity.sh (from shell/; leaves Runtime.mod restored)
  117. set -u
  118. cd "$(dirname "$0")/.." || exit 1
  119. GM2=/home/eric/bin/Modula2/Gm2/bin/gm2
  120. SAVED=../tmp/nonvacuity.Runtime.mod
  121. PROBE=../tmp/nonvacuity.rtprobe
  122. DUMP=../tmp/nonvacuity.dump
  123. # Exec86.mod is UNTRACKED, so unlike Runtime.mod git cannot put a botched
  124. # mutation back: the copy taken here is the only correct one in existence.
  125. # Shell.mod is tracked but is mutated by the R-key case below, and a trap that
  126. # restored only the sources would leave tpshell BUILT FROM the mutation - so
  127. # the trap rebuilds too. All three copies live in the project's own tmp/ and
  128. # are taken here, before the first mutation, rather than beside each case.
  129. #
  130. # comimage.py and comtest.py are the image layout and one of its three readers;
  131. # both are mutated by cases further down, and neither is recoverable from git
  132. # once the mutation is staged over - so they join the trap here for the same
  133. # reason, and an interrupted run cannot leave the layout half-written.
  134. #
  135. # Compiler.mod is the file mutated by the most cases, and it used to be saved
  136. # BESIDE the first case that needed it - the BP displacement section halfway
  137. # down this file, which is well AFTER this trap is set. An interrupted run
  138. # therefore left Compiler.mod half-mutated, with `git status` showing a source
  139. # that no case had finished with. It joins the copies above, taken before the
  140. # first mutation and covered by the same trap.
  141. #
  142. # tests/probe/modrm11.{py,s} are the last two: the mod=11 table section mutates
  143. # them and puts them back after each case, so a run that died between the two
  144. # left them edited. They move here for the same reason. restore_all copies
  145. # them directly instead of calling restore_probe, which is defined halfway down
  146. # this file: a trap firing before that definition would abort the rest of the
  147. # restore with "command not found".
  148. mkdir -p ../tmp
  149. SAVED_E=../tmp/nonvacuity.Exec86.mod
  150. SAVED_SH=../tmp/nonvacuity.Shell.mod
  151. SAVED_CI=../tmp/nonvacuity.comimage.py
  152. SAVED_T=../tmp/nonvacuity.comtest.py
  153. SAVED_C=../tmp/nonvacuity.Compiler.mod
  154. SAVED_CS=../tmp/nonvacuity.CompilerSource.mod
  155. SAVED_CY=../tmp/nonvacuity.CompilerSymbols.mod
  156. SAVED_CE=../tmp/nonvacuity.CompilerEmit.mod
  157. SAVED_PY=../tmp/nonvacuity.modrm11.py
  158. SAVED_S=../tmp/nonvacuity.modrm11.s
  159. cp Runtime.mod "$SAVED" || exit 1
  160. cp Exec86.mod "$SAVED_E" || exit 1
  161. cp Shell.mod "$SAVED_SH" || exit 1
  162. cp tests/comimage.py "$SAVED_CI" || exit 1
  163. cp tests/comtest.py "$SAVED_T" || exit 1
  164. cp Compiler.mod "$SAVED_C" || exit 1
  165. cp CompilerSource.mod "$SAVED_CS" || exit 1
  166. cp CompilerSymbols.mod "$SAVED_CY" || exit 1
  167. cp CompilerEmit.mod "$SAVED_CE" || exit 1
  168. cp tests/probe/modrm11.py "$SAVED_PY" || exit 1
  169. cp tests/probe/modrm11.s "$SAVED_S" || exit 1
  170. restore_all () {
  171. cp "$SAVED" Runtime.mod
  172. cp "$SAVED_E" Exec86.mod
  173. cp "$SAVED_SH" Shell.mod
  174. cp "$SAVED_CI" tests/comimage.py
  175. cp "$SAVED_T" tests/comtest.py
  176. cp "$SAVED_C" Compiler.mod
  177. cp "$SAVED_CS" CompilerSource.mod
  178. cp "$SAVED_CY" CompilerSymbols.mod
  179. cp "$SAVED_CE" CompilerEmit.mod
  180. cp "$SAVED_PY" tests/probe/modrm11.py
  181. cp "$SAVED_S" tests/probe/modrm11.s
  182. "$GM2" -fiso -c CompilerShared.mod >/dev/null 2>&1
  183. "$GM2" -fiso -c CompilerUtil.mod >/dev/null 2>&1
  184. "$GM2" -fiso -c CompilerSource.mod >/dev/null 2>&1
  185. "$GM2" -fiso -c CompilerSymbols.mod >/dev/null 2>&1
  186. "$GM2" -fiso -c CompilerLex.mod >/dev/null 2>&1
  187. "$GM2" -fiso -c CompilerEmit.mod >/dev/null 2>&1
  188. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1
  189. "$GM2" -fiso -c Exec86.mod >/dev/null 2>&1
  190. # The shell is rebuilt as well: a test that runs against a binary built
  191. # from a half-restored tree is reporting on the mutation, not on the code.
  192. # make rebuilds Compiler.o out of the Compiler.mod restored just above.
  193. make >/dev/null 2>&1
  194. }
  195. trap restore_all EXIT
  196. #
  197. # An unhandled signal does NOT run an EXIT trap: bash dies on the spot. That is
  198. # what the comment above promises ("an interrupted run cannot leave the layout
  199. # half-written") and what this file did not actually do. A handler that only
  200. # `exit's is enough, because `exit' from a trap DOES fire the EXIT trap -- and
  201. # it has to exit, or the run would go on using a tree it has just put back.
  202. #
  203. # Measured on bash 5.2: TERM and HUP fire these and restore everything, so
  204. # `kill' and a session teardown are covered. INT is trapped too, but in the
  205. # usual case it never reaches this script: a foreground child that dies of
  206. # SIGINT makes bash treat the signal as the CHILD's and continue the script
  207. # instead. So Ctrl-C does not abort this run at all -- it kills the current
  208. # case's child and the next case starts -- and SIGTERM is what actually stops
  209. # it. 129/130/143 = 128+SIGHUP/INT/TERM.
  210. trap 'exit 129' HUP
  211. trap 'exit 130' INT
  212. trap 'exit 143' TERM
  213. restore_compiler () {
  214. cp "$SAVED_C" Compiler.mod
  215. cp "$SAVED_CS" CompilerSource.mod
  216. cp "$SAVED_CY" CompilerSymbols.mod
  217. cp "$SAVED_CE" CompilerEmit.mod
  218. }
  219. pass=0
  220. fail=0
  221. # mutate <file> <sed-expr> -- apply a deliberate breakage and INSIST it landed.
  222. #
  223. # Four cases in this file were already dead when first run, all the same way:
  224. # the helper they name had been renamed or reformatted since the case was
  225. # written, the sed matched nothing, the source was unchanged, and the check
  226. # correctly passed - so the harness reported "NOT NON-VACUOUS" and, worse, a
  227. # reader skimming the output could take "the check still passed" for a passing
  228. # test. A case that cannot fire is worse than no case: it is a claim of
  229. # coverage that was never tested.
  230. #
  231. # So the mutation is verified, not assumed. If the file is byte-identical
  232. # afterwards, that is reported as a FAILURE of the harness, naming the sed, and
  233. # the case is not run - because running it would only produce a meaningless
  234. # green. The message says what to do (fix the sed) rather than what it found.
  235. mutate () {
  236. mf=$1
  237. msed=$2
  238. cp "$mf" ../tmp/nonvacuity.mut.bak
  239. sed -i "$msed" "$mf"
  240. if cmp -s "$mf" ../tmp/nonvacuity.mut.bak; then
  241. echo " BROKEN CASE: the mutation did not change $mf"
  242. echo " sed: $msed"
  243. echo " the named code has probably been renamed or reformatted -"
  244. echo " fix this case, it is asserting nothing"
  245. fail=$((fail + 1))
  246. return 1
  247. fi
  248. return 0
  249. }
  250. # rebuild <label> -- re-emit the runtime and dump it
  251. rebuild () {
  252. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1 || return 1
  253. "$GM2" -fiso -o "$PROBE" tests/RtProbe.mod Runtime.o Posix.o \
  254. >/dev/null 2>&1 || return 1
  255. "$PROBE" > "$DUMP" || return 1
  256. return 0
  257. }
  258. # expect_red <label> <pattern> <checker-cmd...>
  259. # <pattern> is a grep the failure output must match, so a check cannot
  260. # "pass" by failing for some unrelated reason.
  261. expect_red () {
  262. label=$1
  263. want=$2
  264. shift 2
  265. if out=$("$@" 2>&1); then
  266. echo "NOT NON-VACUOUS: $label -- the check still passed"
  267. fail=$((fail + 1))
  268. elif ! printf '%s\n' "$out" | grep -qi "$want"; then
  269. echo "WRONG FAILURE: $label -- went red, but not for the stated reason"
  270. printf '%s\n' "$out" | sed 's/^/ /'
  271. fail=$((fail + 1))
  272. else
  273. echo " ok: $label"
  274. printf '%s\n' "$out" | grep -im1 "$want" | sed 's/^/ /'
  275. pass=$((pass + 1))
  276. fi
  277. }
  278. echo "== each mutation must turn the named check red"
  279. echo
  280. # --- 1. name-versus-decode -------------------------------------------
  281. # MovSiBx was `89 DC`, which is MOV SP,BX. Two bytes either way, decodes
  282. # cleanly, and no structural check can see it.
  283. cp "$SAVED" Runtime.mod
  284. mutate Runtime.mod 's|B (0DEH) END MovSiBx|B (0DCH) END MovSiBx|'
  285. expect_red "audit_helpers catches MovSiBx emitting MOV SP,BX" \
  286. "MovSiBx" python3 tests/audit_helpers.py
  287. # CmpSiBx had the identical mistake, which is how you know a single fix is
  288. # not enough -- the same misreading was written twice.
  289. cp "$SAVED" Runtime.mod
  290. mutate Runtime.mod 's|B (39H) ; B (0DEH) END CmpSiBx|B (39H) ; B (0DCH) END CmpSiBx|'
  291. expect_red "audit_helpers catches CmpSiBx emitting CMP SP,BX" \
  292. "CmpSiBx" python3 tests/audit_helpers.py
  293. # --- 2. golden, and entry goldens ------------------------------------
  294. # MovDlAl was `88 C0` = MOV AL,AL instead of MOV DL,AL. This is the case that
  295. # motivated runtime.golden: the sweep stayed in sync, every branch target
  296. # stayed on a boundary, no entry's first bytes moved, and the size did not
  297. # change. The target helper was MovAlDh when this case was written, which is
  298. # the fourth way a case here can rot - see the note on `mutate` below.
  299. cp "$SAVED" Runtime.mod
  300. mutate Runtime.mod 's|PROCEDURE MovDlAl ; BEGIN B (88H) ; B (0C2H)|PROCEDURE MovDlAl ; BEGIN B (88H) ; B (0C0H)|'
  301. rebuild
  302. expect_red "runtime.golden catches MOV AL,AL" \
  303. "mov al,al" python3 tests/check_runtime.py "$DUMP"
  304. # initmem opened with the mis-emitted MovSiAx, so its entry golden was the
  305. # thing that noticed the prologue was a no-op.
  306. cp "$SAVED" Runtime.mod
  307. mutate Runtime.mod 's|B (0F0H) END MovSiAx|B (0C0H) END MovSiAx|'
  308. rebuild
  309. expect_red "check_runtime catches a broken initmem prologue" \
  310. "mov ax,ax" python3 tests/check_runtime.py "$DUMP"
  311. # --- 3. decode sweep / length ----------------------------------------
  312. # StDiDl was `88 97` = [BX+disp16],DL: mod=10, so the instruction needs a
  313. # disp16 it was not given, and the sweep loses sync two bytes later.
  314. cp "$SAVED" Runtime.mod
  315. mutate Runtime.mod 's|PROCEDURE StDiDl ; BEGIN B (88H) ; B (15H)|PROCEDURE StDiDl ; BEGIN B (88H) ; B (97H)|'
  316. rebuild
  317. expect_red "decode sweep catches a mod=10 byte move with no displacement" \
  318. "mov byte ptr \[bx+5b5fh\],dl" python3 tests/check_runtime.py "$DUMP"
  319. # --- 4. branch targets ------------------------------------------------
  320. # FixUp measures a rel8 from the end of the instruction, one byte past the
  321. # displacement field. Drop the +1 and every short branch lands one byte into
  322. # its target, which for a 3-byte instruction means the middle of it. The
  323. # bytes themselves are all perfectly well formed -- only the fixups are
  324. # wrong -- so this is the one failure mode the golden cannot be expected to
  325. # catch on its own.
  326. cp "$SAVED" Runtime.mod
  327. mutate Runtime.mod 's|rel := (t + 100H - (fix \[i\].place + 1)) MOD 100H|rel := (t + 100H - fix [i].place) MOD 100H|'
  328. rebuild
  329. expect_red "branch check catches rel8 fixups measured from the wrong byte" \
  330. "not an instruction boundary" \
  331. python3 tests/check_runtime.py "$DUMP"
  332. echo
  333. echo "== everything restored and green again"
  334. cp "$SAVED" Runtime.mod
  335. if rebuild; then
  336. if python3 tests/audit_helpers.py >/dev/null 2>&1 &&
  337. python3 tests/check_runtime.py "$DUMP" >/dev/null 2>&1; then
  338. echo " ok: both checks pass on the restored source"
  339. pass=$((pass + 1))
  340. else
  341. echo "NOT RESTORED: a check is red after restoring Runtime.mod"
  342. fail=$((fail + 1))
  343. fi
  344. else
  345. echo "NOT RESTORED: the runtime would not rebuild"
  346. fail=$((fail + 1))
  347. fi
  348. echo
  349. echo "== the mod=11 table (probe/modrm11.py)"
  350. # These mutate the probe's own sources, not the runtime, so there is no
  351. # rebuild in the loop. SAVED_PY / SAVED_S are taken at the top of this file,
  352. # beside the EXIT trap, so an interrupted run restores them too; each case
  353. # still puts them back here so the next one starts from the real table.
  354. M11="python3 tests/probe/modrm11.py"
  355. restore_probe () {
  356. cp "$SAVED_PY" tests/probe/modrm11.py
  357. cp "$SAVED_S" tests/probe/modrm11.s
  358. }
  359. # 1. one cell of the table moved
  360. mutate tests/probe/modrm11.py 's|"Si", "Di"\]$|"Bp", "Di"]|'
  361. expect_red "anchor pins a moved table cell" \
  362. "anchor ADD SI, 2" $M11
  363. restore_probe
  364. # 2. the table this project actually shipped: AX dropped off the front and a
  365. # duplicate BX invented at the end, which shifts every code down by one
  366. mutate tests/probe/modrm11.py 's|^REG = .*$|REG = ["Cx", "Dx", "Bx", "Sp", "Bp", "Si", "Di", "Bx"]|'
  367. expect_red "the table shifted by one (AX dropped, BX duplicated)" \
  368. "anchor MOV SP, BP" $M11
  369. restore_probe
  370. # 3. the .s edited to contradict the table. This is the case that shows why
  371. # the hard-coded EXPECT bytes exist: the assembler encodes the new claim
  372. # correctly, so comparing the .s against `as` alone can never fail here.
  373. mutate tests/probe/modrm11.s 's|movw %sp, %di # reg 100|movw %bp, %di # reg 100|'
  374. expect_red "probe source edited away from the recorded bytes" \
  375. "expected 89 E7" $M11
  376. restore_probe
  377. # 4. the 8-bit list edited, which is a different table from the word one
  378. mutate tests/probe/modrm11.s 's|movb %al, %dl # 88 C2 -> DL := AL|movb %al, %bl # was DL|'
  379. expect_red "the 8-bit register list edited" \
  380. "expected 88 C2" $M11
  381. restore_probe
  382. # 5. an anchor's recorded byte corrupted, so the anchor can no longer
  383. # corroborate itself
  384. mutate tests/probe/modrm11.py 's|"8B EC", "8B E5"|"8B ED", "8B E5"|'
  385. expect_red "anchor byte no longer matches the emitted code" \
  386. "expected 8B ED" $M11
  387. restore_probe
  388. if $M11 >/dev/null 2>&1; then
  389. echo " ok: modrm11.py passes on the restored probe sources"
  390. pass=$((pass + 1))
  391. else
  392. echo "NOT RESTORED: modrm11.py is red after restoring its sources"
  393. $M11 2>&1 | sed 's/^/ /'
  394. fail=$((fail + 1))
  395. fi
  396. echo
  397. echo "== the BP displacement rule (check_framedisp.py)"
  398. # This one is about Compiler.mod rather than the runtime, and it needs the
  399. # whole toolchain rebuilt (comtest, not rtprobe), so it gets its own rebuild.
  400. # SAVED_C itself is taken at the top of this file, beside the EXIT trap, so an
  401. # interrupted run restores Compiler.mod as well.
  402. rebuild_compiler () {
  403. $GM2 -fiso -c CompilerShared.mod >/dev/null 2>&1 || return 1
  404. $GM2 -fiso -c CompilerUtil.mod >/dev/null 2>&1 || return 1
  405. $GM2 -fiso -c CompilerSource.mod >/dev/null 2>&1 || return 1
  406. $GM2 -fiso -c CompilerSymbols.mod >/dev/null 2>&1 || return 1
  407. $GM2 -fiso -c CompilerLex.mod >/dev/null 2>&1 || return 1
  408. $GM2 -fiso -c CompilerEmit.mod >/dev/null 2>&1 || return 1
  409. $GM2 -fiso -c Compiler.mod >/dev/null 2>&1 || return 1
  410. $GM2 -fiso -fgen-module-list=tests/ct.lst -o /dev/null \
  411. tests/ComTest.mod TextBuf.o Posix.o CompilerShared.o CompilerUtil.o CompilerSource.o CompilerSymbols.o CompilerLex.o CompilerEmit.o Compiler.o Runtime.o Linker.o \
  412. >/dev/null 2>&1
  413. $GM2 -fiso -fuse-list=tests/ct.lst -o comtest \
  414. tests/ComTest.mod TextBuf.o Posix.o CompilerShared.o CompilerUtil.o CompilerSource.o CompilerSymbols.o CompilerLex.o CompilerEmit.o Compiler.o Runtime.o Linker.o \
  415. >/dev/null 2>&1 || return 1
  416. return 0
  417. }
  418. # 1. the original bug: `off MOD 100H`, always disp8. Restores the code that
  419. # was there before EmBpDisp existed. t28's [BP+128] read becomes [BP-128],
  420. # which is the failure this whole check is named after.
  421. #
  422. # regf is KEPT. It is the other half of EmBpDisp's job - the ModR/M reg
  423. # field, which the DI forms (regf 7) need - and it was added after this
  424. # case was written. Hard-coding 46H here again would break the DI forms at
  425. # the same time and turn one mutation into two faults; the check would
  426. # still go red, but for a reason the label does not name.
  427. python3 - "$SAVED_C" <<'PYEOF'
  428. import sys
  429. p = 'CompilerEmit.mod'
  430. s = open(p).read()
  431. old = """ IF off <= 127 THEN
  432. Ebyte (VAL (BYTE, 40H + regf * 8 + 6H)) ; Ebyte (VAL (BYTE, off))
  433. ELSE
  434. Ebyte (VAL (BYTE, 80H + regf * 8 + 6H)) ; Eword (off)
  435. END
  436. END EmBpDisp ;"""
  437. new = """ Ebyte (VAL (BYTE, 40H + regf * 8 + 6H)) ; Ebyte (VAL (BYTE, off MOD 100H))
  438. END EmBpDisp ;"""
  439. assert old in s, "EmBpDisp body not found -- update this mutation"
  440. open(p, 'w').write(s.replace(old, new))
  441. PYEOF
  442. if rebuild_compiler; then
  443. expect_red "displacement truncation reads a different address" \
  444. "no 8B access at \[BP+128\]" python3 tests/check_framedisp.py
  445. else
  446. echo " FAIL: the compiler would not rebuild with the truncation"
  447. fail=$((fail + 1))
  448. fi
  449. restore_compiler
  450. # 2. the other half of the rule: always use the 4-byte form, ignoring the
  451. # <= 127 case. This is over-cautious rather than wrong, so the checker must
  452. # still be happy -- which is worth asserting, because a check that only
  453. # ever fails on a smaller encoding is a check that pins one answer instead
  454. # of the rule.
  455. python3 - <<'PYEOF'
  456. p = 'CompilerEmit.mod'
  457. s = open(p).read()
  458. old = """ IF off <= 127 THEN
  459. Ebyte (VAL (BYTE, 40H + regf * 8 + 6H)) ; Ebyte (VAL (BYTE, off))
  460. ELSE
  461. Ebyte (VAL (BYTE, 80H + regf * 8 + 6H)) ; Eword (off)
  462. END"""
  463. new = """ Ebyte (VAL (BYTE, 80H + regf * 8 + 6H)) ; Eword (off)"""
  464. assert old in s, "EmBpDisp branch not found -- update this mutation"
  465. open(p, 'w').write(s.replace(old, new))
  466. PYEOF
  467. if rebuild_compiler; then
  468. if python3 tests/check_framedisp.py >/dev/null 2>&1; then
  469. echo " ok: always-disp16 is accepted, so the check pins the rule"
  470. echo " and not one particular encoding"
  471. pass=$((pass + 1))
  472. else
  473. echo " FAIL: check_framedisp rejects a safe, over-long encoding"
  474. python3 tests/check_framedisp.py 2>&1 | sed 's/^/ /'
  475. fail=$((fail + 1))
  476. fi
  477. else
  478. echo " FAIL: the compiler would not rebuild with always-disp16"
  479. fail=$((fail + 1))
  480. fi
  481. restore_compiler
  482. if rebuild_compiler; then
  483. if python3 tests/check_framedisp.py >/dev/null 2>&1; then
  484. echo " ok: check_framedisp passes on the restored source"
  485. pass=$((pass + 1))
  486. else
  487. echo "NOT RESTORED: check_framedisp is red after restoring Compiler.mod"
  488. python3 tests/check_framedisp.py 2>&1 | sed 's/^/ /'
  489. fail=$((fail + 1))
  490. fi
  491. else
  492. echo "NOT RESTORED: the compiler would not rebuild"
  493. fail=$((fail + 1))
  494. fi
  495. # --- the region check_framedisp sweeps, and the one file it comes from -----
  496. echo
  497. echo "== the image layout, measured once (tests/comimage.py)"
  498. # check_framedisp.py decodes instructions over a REGION of the image, and a
  499. # wrong region does not stop it: the byte patterns it searches for are in the
  500. # image wherever they happen to be, and a decode that starts mid-instruction
  501. # reached the same offsets anyway. That is how RT_SZ = 391 - a literal that had
  502. # drifted from the runtime it described - went on reporting PASS while its sweep
  503. # began inside the runtime, part way through an instruction. Nothing in the
  504. # check could see where its own region began.
  505. #
  506. # So the region is two independent readings of the file - where the entry jump
  507. # says execution starts, and where the program header says the runtime ends -
  508. # and the two must AGREE or the region is undetermined. Both readings, and the
  509. # layout itself, live in tests/comimage.py, which is also where comtest.py and
  510. # the independent checker in run_com_tests.sh now get the copy each of them
  511. # used to keep: a duplicated constant that has silently drifted is not an
  512. # independent check, it is a second source of truth that lies, and it lies in
  513. # the direction of looking like the code under test is broken.
  514. #
  515. # None of this needs a rebuild - comimage.py is Python the checkers import -
  516. # so these cases are cheap, and they turn the READERS red one at a time.
  517. # 1. entry_target answers with the literal the region used to be built from.
  518. # The two readings now disagree, and there is no measured region to sweep.
  519. python3 - <<'PYEOF'
  520. p = 'tests/comimage.py'
  521. s = open(p).read()
  522. old = ' return ENT_SZ + int.from_bytes(d[1:ENT_SZ], "little", signed=True)'
  523. new = ' return ENT_SZ + 391 # the literal RT_SZ was'
  524. assert s.count(old) == 1, "entry_target's return not found -- update this mutation"
  525. open(p, 'w').write(s.replace(old, new))
  526. PYEOF
  527. expect_red "a sweep region that is not where execution starts is rejected" \
  528. "neither region is swept" python3 tests/check_framedisp.py
  529. cp "$SAVED_CI" tests/comimage.py
  530. # 2 and 3. The header's own equation, one byte out, so the header cannot be
  531. # located at all - the case where a checker that fell back on a remembered
  532. # offset would report a confident number instead. Two readers, two
  533. # different sentences: each must say in its own words that it does not know
  534. # where the program's code begins.
  535. python3 - <<'PYEOF'
  536. p = 'tests/comimage.py'
  537. s = open(p).read()
  538. old = ' if w(HDR_DS_WORD) != DATA_IMAGE + LOAD_BIAS: # hdrDS'
  539. new = ' if w(HDR_DS_WORD) != DATA_IMAGE + LOAD_BIAS + 1: # hdrDS'
  540. assert s.count(old) == 1, "the hdrDS equation not found -- update this mutation"
  541. open(p, 'w').write(s.replace(old, new))
  542. PYEOF
  543. expect_red "check_framedisp reports a header it cannot locate" \
  544. "no program header found" python3 tests/check_framedisp.py
  545. expect_red "check_8086 reports the same unlocatable header" \
  546. "could not locate the code region" python3 tests/check_8086.py
  547. cp "$SAVED_CI" tests/comimage.py
  548. # 4-7. The single copy itself, now that there is one. A second find_header,
  549. # a second HDR_SZ, a caller that reaches the helper through another
  550. # consumer instead of through comimage, and - the case that exists
  551. # because of rt_exec.py's "matched nothing" lesson - the definition the
  552. # audit counts. A rule that matches nothing looks exactly like a rule
  553. # that passes, so a deleted definition must be a red report rather than a
  554. # silent green one.
  555. python3 - <<'PYEOF'
  556. p = 'tests/comtest.py'
  557. s = open(p).read()
  558. old = 'def check_com(path, expected_src_len):'
  559. new = ('def find_header(d):\n'
  560. ' return None # a second copy of the helper\n'
  561. '\n\n'
  562. 'def check_com(path, expected_src_len):')
  563. assert s.count(old) == 1, "check_com not found -- update this mutation"
  564. open(p, 'w').write(s.replace(old, new))
  565. PYEOF
  566. expect_red "the audit reports a find_header copied out of comimage" \
  567. "comtest.py defines its own find_header" python3 tests/check_comimage.py
  568. cp "$SAVED_T" tests/comtest.py
  569. python3 - <<'PYEOF'
  570. p = 'tests/comtest.py'
  571. s = open(p).read()
  572. old = 'from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, DATA_IMAGE, HEAD,'
  573. new = ('HDR_SZ = 16 # a second copy of the layout\n'
  574. 'from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, DATA_IMAGE, HEAD,')
  575. assert s.count(old) == 1, "the comimage import not found -- update this mutation"
  576. open(p, 'w').write(s.replace(old, new))
  577. PYEOF
  578. expect_red "the audit reports a layout constant defined elsewhere" \
  579. "comtest.py defines its own HDR_SZ" python3 tests/check_comimage.py
  580. cp "$SAVED_T" tests/comtest.py
  581. python3 - <<'PYEOF'
  582. p = 'tests/comimage.py'
  583. s = open(p).read()
  584. old = 'def find_header(d):'
  585. new = 'def _find_header(d): # the definition is gone'
  586. assert s.count(old) == 1, "def find_header not found -- update this mutation"
  587. open(p, 'w').write(s.replace(old, new))
  588. PYEOF
  589. expect_red "the audit counts its own subject rather than matching nothing" \
  590. "defines find_header 0 times" python3 tests/check_comimage.py
  591. cp "$SAVED_CI" tests/comimage.py
  592. # 7. The import dropped while the CALLS stay - the shape a refactor makes when
  593. # one reader starts taking the helper from another reader instead. It would
  594. # NameError the moment comtest ran, but the audit says so first, and in the
  595. # vocabulary of the layout rather than in the vocabulary of Python: the
  596. # failure this is guarding is "two sources of truth", not "undefined name".
  597. python3 - <<'PYEOF'
  598. p = 'tests/comtest.py'
  599. s = open(p).read()
  600. old = ('from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, DATA_IMAGE, HEAD,\n'
  601. ' HDR_DS_WORD, HDR_HEAP_WORD, find_header)')
  602. new = '# MUTATION: the shared import is gone, but the calls remain\n'
  603. assert s.count(old) == 1, "the comimage import not found -- update this mutation"
  604. open(p, 'w').write(s.replace(old, new))
  605. PYEOF
  606. expect_red "the audit reports a caller that does not take it from comimage" \
  607. "comtest.py calls find_header without importing it from comimage" \
  608. python3 tests/check_comimage.py
  609. cp "$SAVED_T" tests/comtest.py
  610. # Green again, on all three readers at once: the case above mutated the one
  611. # file all of them import, so restoring it has to satisfy each of them, not
  612. # just the one that was last run.
  613. for c in tests/check_framedisp.py tests/check_comimage.py tests/check_8086.py; do
  614. if python3 "$c" >/dev/null 2>&1; then
  615. echo " ok: $c passes on the restored sources"
  616. pass=$((pass + 1))
  617. else
  618. echo "NOT RESTORED: $c is red after restoring comimage.py and comtest.py"
  619. python3 "$c" 2>&1 | tail -3 | sed 's/^/ /'
  620. fail=$((fail + 1))
  621. fi
  622. done
  623. # --- 3. the operator bugs the nine dead fixtures were hiding ------------
  624. echo
  625. echo "== the bugs the never-executed fixtures were hiding"
  626. echo
  627. # A different KIND of case from everything above. The others break the code
  628. # and assert a byte-level check notices; these break the code and assert a
  629. # BEHAVIOURAL check notices, which is the only kind that could have found them.
  630. # Each of the original four shipped with a green compile matrix, a passing .COM
  631. # layout check, a passing golden and a passing emitter audit:
  632. #
  633. # OpMul = 1 `a * b` emitted ADD AX,CX. `*' and `+' both numbered
  634. # their operator 1, and BinOpEmit cannot see which
  635. # precedence level called it, so every multiplication
  636. # dispatched to the addition. The constant-folding arm was
  637. # correct, which is why `n * n' with n a CONST was right and
  638. # `a * a' with a a variable was not -- and t08_const is the
  639. # only fixture that ever multiplied.
  640. # 9Dh / 9FH `>' got SETGE and `>=' got SETG: swapped, one letter
  641. # apart in the mnemonic. Only a==b could see it.
  642. # JNZ -> body REPEAT..UNTIL looped back while the condition was TRUE,
  643. # which is WHILE, so the body ran once and stopped.
  644. #
  645. # They are mutated back to the original defect and run_com_exec.py must go red
  646. # on the exact fixture that pins the behaviour. The fourth (HideLocals) is a
  647. # scoping bug rather than an operator bug; it was hiding in the same place.
  648. #
  649. # Two joined them when the operand-lifetime and `not' fixes landed, each run
  650. # red by hand before it was written down here, and each with the same property
  651. # as the four above -- green everywhere except execution:
  652. #
  653. # SaveLeft's park `(p > q) or (q > p)' evaluated `(q > p) or (q > p)'. A
  654. # kind-2 value exists only in AX, and the right operand's
  655. # code is emitted before the two are ever brought together,
  656. # so without the push the left one is simply gone.
  657. # neglevel's split `not' on a boolean emitted the INTEGER `not', which left
  658. # 0FFFEh where a boolean belongs, and wrbool reads anything
  659. # non-zero as TRUE -- so `not (a = a)' answered TRUE and so
  660. # did every other `not'.
  661. mutate_file () {
  662. mf=$1
  663. saved=$2
  664. msed=$3
  665. cp "$saved" ../tmp/nonvacuity.mut2.bak
  666. sed -i "$msed" "$mf"
  667. if cmp -s "$mf" ../tmp/nonvacuity.mut2.bak; then
  668. echo " BROKEN CASE: the mutation did not change $mf"
  669. echo " sed: $msed"
  670. echo " the named code has probably been renamed or reformatted -"
  671. echo " fix this case, it is asserting nothing"
  672. fail=$((fail + 1))
  673. return 1
  674. fi
  675. return 0
  676. }
  677. mutate_compiler () { mutate_file Compiler.mod "$SAVED_C" "$1" ; }
  678. mutate_emit () { mutate_file CompilerEmit.mod "$SAVED_CE" "$1" ; }
  679. mutate_symbols () { mutate_file CompilerSymbols.mod "$SAVED_CY" "$1" ; }
  680. restore_compiler () {
  681. cp "$SAVED_C" Compiler.mod
  682. cp "$SAVED_CS" CompilerSource.mod
  683. cp "$SAVED_CY" CompilerSymbols.mod
  684. cp "$SAVED_CE" CompilerEmit.mod
  685. }
  686. # The SETcc swap and the HideLocals removal are done in python rather than
  687. # with sed: both need to match source text containing `*` and `(` in a way that
  688. # is tedious and fragile as a regex, and a case whose only failure mode is a
  689. # malformed sed is a case that silently asserts nothing.
  690. #
  691. # And a python helper fails in a way sed does not: a syntax error in the helper
  692. # is a non-zero exit, `if mutate_foo; then` is simply false, and the case is
  693. # SKIPPED -- with no failure counted and nothing on stdout but whatever python
  694. # printed. That is how the SETcc case spent its first run: an apostrophe in an
  695. # assert message ("the `>' arm") closed the string early, python died, the
  696. # compiler was never broken, and the suite still reported 0 failed. A skipped
  697. # case and a passing case look the same in the total. So each helper below
  698. # fails LOUDLY: a non-zero exit from python is reported as a BROKEN CASE and
  699. # counted, never swallowed.
  700. #
  701. # Each one also counts its targets before replacing. `assert s != before' only
  702. # says the file changed; with two edits it would pass if just one of them
  703. # landed, and with two identical HideLocals call sites it would happily delete
  704. # the wrong one -- still a changed file, still a working compiler, still green
  705. # for the wrong reason.
  706. mutate_cc_swap () {
  707. if python3 - <<'PYX'
  708. p = 'CompilerEmit.mod'
  709. s = open(p).read()
  710. GT = 'EmSetcc (9FH) ; (* > SETG *)' # the greater-than arm
  711. GE = 'EmSetcc (9DH) ; (* >= SETGE *)' # the greater-equal arm
  712. assert s.count(GT) == 1, 'expected 1 greater-than arm, found %d' % s.count(GT)
  713. assert s.count(GE) == 1, 'expected 1 greater-equal arm, found %d' % s.count(GE)
  714. s = s.replace(GT, GT.replace('9FH', '9DH'))
  715. s = s.replace(GE, GE.replace('9DH', '9FH'))
  716. open(p, 'w').write(s)
  717. PYX
  718. then
  719. return 0
  720. fi
  721. echo " BROKEN CASE: the SETcc swap did not apply"
  722. echo " the two EmSetcc arms are probably renamed or reformatted -"
  723. echo " fix this case, it is asserting nothing"
  724. fail=$((fail + 1))
  725. return 1
  726. }
  727. mutate_no_hidelocals () {
  728. if python3 - <<'PYX'
  729. p = 'Compiler.mod'
  730. s = open(p).read()
  731. # Two HideLocals calls exist. Only the body-exit one may go: deleting the
  732. # FORWARD one instead would still change the file, still rebuild, and still
  733. # leave t13_proc compiling, so the case would go green for the wrong reason.
  734. HL = ' HideLocals (nestMark) ; (* parameters and locals stop here *)\n'
  735. assert s.count(HL) == 1, 'expected 1 body-exit HideLocals, found %d' % s.count(HL)
  736. open(p, 'w').write(s.replace(HL, ''))
  737. PYX
  738. then
  739. return 0
  740. fi
  741. echo " BROKEN CASE: HideLocals was not removed"
  742. echo " the call or its comment has probably been reformatted -"
  743. echo " fix this case, it is asserting nothing"
  744. fail=$((fail + 1))
  745. return 1
  746. }
  747. restore_compiler
  748. if mutate_compiler 's|^ op := OpMul ; DropCh| op := OpAdd ; DropCh|'; then
  749. if rebuild_compiler; then
  750. expect_red "execution catches '*' emitting an ADD (t08_const, n*n)" \
  751. "t08_const" python3 tests/run_com_exec.py t08_const
  752. # t08 only ever multiplied two CONSTANTS, which is the one path that was
  753. # never wrong, because BinOpEmit folds it. So the case above is close
  754. # to vacuous: it proves the mutation changed the binary, not that the
  755. # emitted multiply is covered. The check that matters needs a
  756. # VARIABLE operand, and no shipped fixture has one -- which is why the
  757. # bug survived at all. So this writes a throwaway fixture that
  758. # multiplies a variable, runs it, and asserts the multiply is right.
  759. # The fixture is deleted afterwards; it is here to close the coverage
  760. # hole, not to become a permanent test (that is what a real fixture
  761. # with a `*' in it would be for).
  762. cat > tests/fixtures/zzmul.pas <<'ZZEOF'
  763. program zzmul;
  764. var a : integer ;
  765. begin
  766. a := 7 ;
  767. writeln (a * 6)
  768. end.
  769. ZZEOF
  770. printf '42\r\n' > tests/fixtures/zzmul.out
  771. expect_red "execution catches '*' on a VARIABLE (the unfolded path)" \
  772. "zzmul" python3 tests/run_com_exec.py zzmul
  773. rm -f tests/fixtures/zzmul.pas tests/fixtures/zzmul.out
  774. else
  775. echo " FAIL: the compiler would not rebuild with OpAdd for '*'"
  776. fail=$((fail + 1))
  777. fi
  778. fi
  779. restore_compiler
  780. restore_compiler
  781. if mutate_cc_swap; then
  782. if rebuild_compiler; then
  783. expect_red "execution catches '>' and '>=' swapped (t09_if)" \
  784. "t09_if" python3 tests/run_com_exec.py t09_if
  785. else
  786. echo " FAIL: the compiler would not rebuild with the SETcc swap"
  787. fail=$((fail + 1))
  788. fi
  789. fi
  790. restore_compiler
  791. restore_compiler
  792. if mutate_compiler 's| DropC (EmJcc (84H, L1)) ; (\* JZ -> body again \*)| zj := EmJcc (85H, L1) ;|'; then
  793. if rebuild_compiler; then
  794. expect_red "execution catches REPEAT..UNTIL exiting after one pass (t12)" \
  795. "t12_repeat" python3 tests/run_com_exec.py t12_repeat
  796. else
  797. echo " FAIL: the compiler would not rebuild with the JNZ repeat"
  798. fail=$((fail + 1))
  799. fi
  800. fi
  801. restore_compiler
  802. # The fourth: sibling procedures shared one parameter namespace, because a
  803. # finished procedure's symbols were left at a level Search still accepts.
  804. # Removing HideLocals puts two procedures' `a : integer' back in collision.
  805. restore_compiler
  806. if mutate_no_hidelocals; then
  807. if rebuild_compiler; then
  808. expect_red "a duplicate parameter in two procedures is a compile error again" \
  809. "ERROR 41" python3 tests/run_com_exec.py t13_proc
  810. else
  811. echo " FAIL: the compiler would not rebuild without HideLocals"
  812. fail=$((fail + 1))
  813. fi
  814. else
  815. echo " FAIL: could not remove HideLocals to test the scoping fix"
  816. fail=$((fail + 1))
  817. fi
  818. # M6: the LEFT operand, parked for the right one. kind 2 means "this value
  819. # exists in AX and nowhere else", and the right-hand operand's code is emitted
  820. # between recognizing the operator and using both operands - so without
  821. # SaveLeft's push the left value is overwritten before it is ever read, and
  822. # LoadPair's kind-4 shape can never trigger. `(p > q) or (q > p)' then
  823. # evaluates `(q > p) or (q > p)'. Every byte, every size and the whole compile
  824. # matrix stay green: nothing is malformed, the value is simply the wrong one.
  825. restore_compiler
  826. if python3 - <<'PYX'
  827. p = 'Compiler.mod'
  828. s = open(p).read()
  829. # Both guards are matched, and each must be unique. `IF left.kind = 2 THEN'
  830. # reads like it can only be SaveLeft's, but LoadAtom and friends also branch
  831. # on kind, so an unasserted replace could disable one of THEM instead and the
  832. # case would still go red - for a reason the label does not name.
  833. #
  834. # The kind-5 ("a string element whose address is in DI") guard did not exist
  835. # when this case was written; it arrived with `s[i]`. Making only the kind-2
  836. # park unreachable would leave that one alive, so the mutation would no longer
  837. # be "SaveLeft parks nothing" - it would be a half-disabled SaveLeft, and the
  838. # case would be testing something narrower than it says.
  839. g2 = ' ELSIF left.kind = 2 THEN\n'
  840. g5 = ' ELSIF left.kind = 5 THEN\n'
  841. assert s.count(g2) == 1, 'the kind-2 guard found %d times -- update this mutation' % s.count(g2)
  842. assert s.count(g5) == 1, 'the kind-5 guard found %d times -- update this mutation' % s.count(g5)
  843. s = s.replace(g2, ' ELSIF left.kind = 99 THEN\n')
  844. s = s.replace(g5, ' ELSIF left.kind = 99 THEN\n')
  845. open(p, 'w').write(s)
  846. PYX
  847. then
  848. if rebuild_compiler; then
  849. expect_red "execution catches a left operand clobbered by the right one" \
  850. "t34_arith" python3 tests/run_com_exec.py t34_arith
  851. else
  852. echo " FAIL: the compiler would not rebuild without SaveLeft's push"
  853. fail=$((fail + 1))
  854. fi
  855. else
  856. echo " BROKEN CASE: the SaveLeft mutation did not apply"
  857. fail=$((fail + 1))
  858. fi
  859. restore_compiler
  860. # M7: the type split in TPSRC9's neglevel, wrong half. A boolean NOT lowered
  861. # as the INTEGER one leaves 0FFFEh/0FFFFh, and wrbool tests [BP+4] <> 0 - so
  862. # `not (a = a)' answers TRUE, and so does every other `not'. One instruction,
  863. # same length, same sizes, and the compile matrix cannot see it because both
  864. # halves emit well-formed code for a type the parser already accepted.
  865. restore_compiler
  866. if python3 - <<'PYX'
  867. p = 'Compiler.mod'
  868. s = open(p).read()
  869. old = """ IF r.cls = TBool THEN
  870. LoadAtom (r) ;
  871. EmXorAl01 () ;"""
  872. new = """ IF r.cls = TBool THEN
  873. LoadAtom (r) ;
  874. EmNotAx () ; (* MUTATION: integer NOT on a boolean *)"""
  875. assert s.count(old) == 1, 'the TBool arm of ParseNeg found %d times -- update this mutation' % s.count(old)
  876. open(p, 'w').write(s.replace(old, new))
  877. PYX
  878. then
  879. if rebuild_compiler; then
  880. expect_red "execution catches a boolean NOT lowered as an integer one" \
  881. "t35_not" python3 tests/run_com_exec.py t35_not
  882. else
  883. echo " FAIL: the compiler would not rebuild with EmNotAx for a boolean"
  884. fail=$((fail + 1))
  885. fi
  886. else
  887. echo " BROKEN CASE: the neglevel type-split mutation did not apply"
  888. fail=$((fail + 1))
  889. fi
  890. restore_compiler
  891. # Bug 36, argument order: the fix has FOUR independent halves - three parsers
  892. # that push each argument the moment it is parsed instead of after the whole
  893. # list, and the callee that reads its parameters backwards from BP+4 - and
  894. # t36_argclobber is the one fixture that tells them apart. Before these four
  895. # cases existed, its only proof was the pre-fix run quoted in the commit
  896. # message, which is a fact about the past rather than a check: it cannot be
  897. # re-derived, and nothing stops the four halves from drifting apart.
  898. #
  899. # Each revert below was run to a red BEFORE it was written here, and the four
  900. # fail on four DIFFERENT rows, which is what makes them four cases and not one
  901. # wearing four hats: the callee revert turns `5 3 7' into `7 3 5' (every
  902. # argument lands on its neighbour's parameter), the deferred ParseCallArgs
  903. # zeroes the rows built from computed arguments, ParseCall changes the
  904. # function call's result (19 -> 10) and IoCall changes the last write
  905. # (`FALSE 1' -> `TRUE 544', the first argument printed as the second
  906. # argument's value). A revert that left t36 green is reported by expect_red
  907. # as NOT NON-VACUOUS; a revert that did not land is a BROKEN CASE.
  908. # revert_procedure <name> -- put <name> back the way it was before bug 36 was
  909. # fixed, spliced out of the tree the fix landed on top of. The old code's
  910. # source of truth is the old code: quoting a copy of it in this file is a copy
  911. # that can drift from the tag it claims to reproduce, and a revert that
  912. # reproduces nothing is a case asserting nothing.
  913. revert_procedure () {
  914. python3 - "$1" <<'PYX'
  915. import subprocess, sys
  916. name = sys.argv[1]
  917. cur = open('Compiler.mod').read()
  918. r = subprocess.run(['git', 'show', 'v-TP3-IMAGELAYOUT:shell/Compiler.mod'],
  919. capture_output=True)
  920. assert r.returncode == 0, 'cannot read the pre-fix tree: %s' % r.stderr.decode()[:200]
  921. pre = r.stdout.decode()
  922. def span(text, where):
  923. # The DEFINITION, not the FORWARD declaration a few screens earlier: the
  924. # anchor runs to the end of the line, and a FORWARD line ends `FORWARD ;'
  925. # instead of nothing.
  926. a, b = 'PROCEDURE %s (idx : CARDINAL) ;\n' % name, 'END %s ;' % name
  927. na, nb = text.count(a), text.count(b)
  928. assert na == 1 and nb == 1, \
  929. '%s: %d definitions, %d ends in %s -- update this case' % (name, na, nb, where)
  930. i = text.index(a)
  931. j = text.index(b, i) + len(b)
  932. return text[i:j], i, j
  933. new, i, j = span(cur, 'Compiler.mod')
  934. old, _, _ = span(pre, 'the pre-fix tree')
  935. assert old != new, 'the pre-fix %s is identical to the current one' % name
  936. open('Compiler.mod', 'w').write(cur[:i] + old + cur[j:])
  937. PYX
  938. }
  939. # The callee half: no remap, so parameters are read in declaration order from
  940. # BP+4 while the arguments are pushed in parse order - the FIRST argument ends
  941. # up in the LAST parameter's slot, and one-argument calls (all of
  942. # Runtime.mod's own cases) cannot tell the two layouts apart.
  943. restore_compiler
  944. if python3 - <<'PYX'
  945. p = 'Compiler.mod'
  946. s = open(p).read()
  947. old = """ i := nestMark ;
  948. WHILE i < symTop DO
  949. symtab [i].off := 8 + (parmOff - 4) - symtab [i].off
  950. - ParamSlotBytes (symtab [i]) ;
  951. INC (i)
  952. END ;
  953. """
  954. assert s.count(old) == 1, 'the ProcFunc parameter remap found %d times -- update this mutation' % s.count(old)
  955. open(p, 'w').write(s.replace(old, ''))
  956. PYX
  957. then
  958. if rebuild_compiler; then
  959. expect_red "t36 catches the callee laid out in declaration order (no remap)" \
  960. "t36_argclobber" python3 tests/run_com_exec.py t36_argclobber
  961. else
  962. echo " FAIL: the compiler would not rebuild without the parameter remap"
  963. fail=$((fail + 1))
  964. fi
  965. else
  966. echo " BROKEN CASE: the ProcFunc remap mutation did not apply"
  967. fail=$((fail + 1))
  968. fi
  969. restore_compiler
  970. # The caller half, site 1 of 3: a call used as a statement. The old code
  971. # read the whole list into args[] and pushed it backwards afterwards, so a
  972. # computed argument (which exists only in AX) was gone before the push loop
  973. # reached it.
  974. restore_compiler
  975. if revert_procedure ParseCallArgs; then
  976. if rebuild_compiler; then
  977. expect_red "t36 catches ParseCallArgs pushing after the whole list" \
  978. "t36_argclobber" python3 tests/run_com_exec.py t36_argclobber
  979. else
  980. echo " FAIL: the compiler would not rebuild with ParseCallArgs deferred"
  981. fail=$((fail + 1))
  982. fi
  983. else
  984. echo " BROKEN CASE: the ParseCallArgs revert did not apply"
  985. fail=$((fail + 1))
  986. fi
  987. restore_compiler
  988. # Site 2 of 3: the same call reached from ParseAtom, when a FUNCTION is
  989. # called inside an expression. A separate site, because it is a separate
  990. # procedure - and t36's `19' row is the only thing that notices this one.
  991. restore_compiler
  992. if revert_procedure ParseCall; then
  993. if rebuild_compiler; then
  994. expect_red "t36 catches ParseCall pushing after the whole list" \
  995. "t36_argclobber" python3 tests/run_com_exec.py t36_argclobber
  996. else
  997. echo " FAIL: the compiler would not rebuild with ParseCall deferred"
  998. fail=$((fail + 1))
  999. fi
  1000. else
  1001. echo " BROKEN CASE: the ParseCall revert did not apply"
  1002. fail=$((fail + 1))
  1003. fi
  1004. restore_compiler
  1005. # Site 3 of 3: write/writeln/read/readln, where the argument list is also
  1006. # where an inline string literal gets its call. Reverting this procedure
  1007. # also reverts the per-argument dispatch inside it (the BYTE read landed
  1008. # here), which is harmless for t36 and is why the splice is the whole
  1009. # procedure rather than the push alone: half a revert is a state the tree
  1010. # was never in.
  1011. restore_compiler
  1012. if revert_procedure IoCall; then
  1013. if rebuild_compiler; then
  1014. expect_red "t36 catches IoCall printing the last argument's value" \
  1015. "t36_argclobber" python3 tests/run_com_exec.py t36_argclobber
  1016. else
  1017. echo " FAIL: the compiler would not rebuild with IoCall's loops split"
  1018. fail=$((fail + 1))
  1019. fi
  1020. else
  1021. echo " BROKEN CASE: the IoCall revert did not apply"
  1022. fail=$((fail + 1))
  1023. fi
  1024. restore_compiler
  1025. # The boolean read (t38): TP3 refuses `readln (b)' with error 66 - prderrtp
  1026. # rejects the whole class and the runtime has no xrdbool - and this compiler
  1027. # used to accept it and dispatch to rdbool, which stores a WORD into the
  1028. # one-byte variable, its second byte landing on the variable AFTER b (t37's
  1029. # overflow, unobserved only because no fixture read a boolean). The fix is
  1030. # one arm of IoCall's type dispatch, so the mutation restores exactly that
  1031. # arm. The check is the COMPILE MATRIX rather than any run: an ERR fixture
  1032. # is never executed, so nothing that runs programs on it can see a refusal.
  1033. restore_compiler
  1034. if python3 - <<'PYX'
  1035. p = 'Compiler.mod'
  1036. s = open(p).read()
  1037. old = """ ELSIF acls = TBool THEN
  1038. (* TP3 refuses boolean I/O: TPSRC8 prderrtp is error 66,
  1039. "I/O not allowed here", and the runtime has no xrdbool
  1040. at all. rdbool stores a WORD, so one byte of an
  1041. accepted read would land on the variable AFTER b - the
  1042. exact overflow t37_readbyte pins for a byte. TP3's
  1043. answer to the whole class is the error; so is ours. *)
  1044. Err (EIoHere) ;
  1045. RETURN"""
  1046. assert s.count(old) == 1, 'the boolean-read error arm found %d times -- update this mutation' % s.count(old)
  1047. rep = """ ELSIF acls = TBool THEN
  1048. ent := TU_RdBool"""
  1049. open(p, 'w').write(s.replace(old, rep))
  1050. PYX
  1051. then
  1052. if rebuild_compiler; then
  1053. expect_red "t38 red when a boolean read dispatches to TU_RdBool" \
  1054. "t38_readbool" tests/run_compile_tests.sh
  1055. else
  1056. echo " FAIL: the compiler would not rebuild with the boolean read accepted"
  1057. fail=$((fail + 1))
  1058. fi
  1059. else
  1060. echo " BROKEN CASE: the boolean-read mutation did not apply"
  1061. fail=$((fail + 1))
  1062. fi
  1063. restore_compiler
  1064. # The string variable (t39). A STRING is the first declared type bigger than
  1065. # a word that this compiler accepts, and it is accepted in two places: DefVar
  1066. # exempts TString from its `size > 2' catch-all so `var s : string [10]' can
  1067. # exist at all, and the two arms below USE it once it does. The three
  1068. # mutations are the three places the feature lives, each restored to the
  1069. # exact pre-fix state (all three used to be ENoLib, or never existed):
  1070. #
  1071. # 1. the DEPARSE of assignment: before the fix, `s := 'hello'' fell out of
  1072. # Assignment's size check. Removing the string arm puts it back there.
  1073. # 2. the WRITE: before the fix, `writeln (s)' was refused by IoCall's
  1074. # type dispatch. Restoring the refusal arm is removing the feature.
  1075. # 3. the RUNTIME: wrstr's print loop is only as good as its length byte,
  1076. # and only rt_exec.py can see a wrstr that reads no length at all.
  1077. # 1. Assignment's string arm is gone, so `s := 'hello'' trips the old
  1078. # `size > 2' refusal again. The arm is spliced between its guard and the
  1079. # `size > 2' check that follows it, so a comment inside cannot break the
  1080. # match - but the GUARD is matched by text, and it has to be the whole
  1081. # guard. It grew an `AND (r.cls = TString)' when `s[i] := c' made a
  1082. # TString destination reach a second arm, and ParseSub grew a look-alike
  1083. # `IF symtab [r.idx].cls = TString THEN' for `s[i]'. The shorter anchor
  1084. # therefore now finds ParseSub's, some 800 lines earlier, and the splice
  1085. # below deletes everything between the two - which does not test the
  1086. # refusal, it tests whether the compiler still compiles. Hence the assert
  1087. # and the spelled-out guard.
  1088. restore_compiler
  1089. if python3 - <<'PYX'
  1090. p = 'Compiler.mod'
  1091. s = open(p).read()
  1092. g = ' IF (r.cls = TString) AND (VarSize (r) > 1) THEN\n'
  1093. assert s.count(g) == 1, 'the string-assignment guard found %d times' % s.count(g)
  1094. a = s.index(g)
  1095. m = ' RETURN\n END ;\n IF (r.kind = 5) OR (VarSize (r) > 2) THEN\n'
  1096. i = s.index(m, a)
  1097. new = g + ' Err (ENoLib) ; (* MUTATION: the arm is refused *)\n RETURN\n'
  1098. open(p, 'w').write(s[:a] + new + s[i + len(' RETURN\n'):])
  1099. PYX
  1100. then
  1101. if rebuild_compiler; then
  1102. expect_red "t39 red when the string-assignment arm is refused" \
  1103. "t39_strvar" tests/run_compile_tests.sh
  1104. else
  1105. echo " FAIL: the compiler would not rebuild with the string assignment refused"
  1106. fail=$((fail + 1))
  1107. fi
  1108. else
  1109. echo " BROKEN CASE: the string-assignment mutation did not apply"
  1110. fail=$((fail + 1))
  1111. fi
  1112. restore_compiler
  1113. # 2. The write arm of IoCall, restored to its pre-fix refusal.
  1114. restore_compiler
  1115. if python3 - <<'PYX'
  1116. p = 'Compiler.mod'
  1117. s = open(p).read()
  1118. old = """ IF arg.kind = 5 THEN
  1119. EmPushDi ()
  1120. ELSIF symtab [arg.idx].isVar THEN
  1121. (* VAR string: the slot holds the address of the
  1122. caller's characters; push that, not the slot. *)
  1123. LoadSymVar (arg.idx, symtab [arg.idx].off, 2) ;
  1124. EmPushAx ()
  1125. ELSE
  1126. EmPushSymAddr (arg.idx, (symtab [arg.idx].off + arg.boff) MOD 10000H)
  1127. END ;
  1128. ent := TU_WrStr"""
  1129. assert s.count(old) == 1, 'the string-write arm found %d times -- update this mutation' % s.count(old)
  1130. rep = """ Err (ENoLib) ;
  1131. RETURN"""
  1132. open(p, 'w').write(s.replace(old, rep))
  1133. PYX
  1134. then
  1135. if rebuild_compiler; then
  1136. expect_red "t39 red when a string write is refused again (ENoLib)" \
  1137. "t39_strvar" tests/run_compile_tests.sh
  1138. else
  1139. echo " FAIL: the compiler would not rebuild with the string write refused"
  1140. fail=$((fail + 1))
  1141. fi
  1142. else
  1143. echo " BROKEN CASE: the string-write mutation did not apply"
  1144. fail=$((fail + 1))
  1145. fi
  1146. restore_compiler
  1147. # 3. The runtime half: wrstr never reads the length byte, so every string -
  1148. # "hello" and the empty one alike - prints nothing. The compiler, the
  1149. # golden and the audit all stay green; only executing the entry finds it.
  1150. cp "$SAVED" Runtime.mod
  1151. if python3 - <<'PYX'
  1152. p = 'Runtime.mod'
  1153. s = open(p).read()
  1154. old = """ MovBxBp4 ; (* BX := the caller's string address *)
  1155. XorCxCx ;
  1156. MovClBx ; (* CX := the length byte at [BX] *)"""
  1157. assert s.count(old) == 1, 'the wrstr length load found %d times -- update this mutation' % s.count(old)
  1158. rep = """ MovBxBp4 ; (* BX := the caller's string address *)
  1159. XorCxCx ; (* CX is never loaded: prints nothing *)"""
  1160. open(p, 'w').write(s.replace(old, rep))
  1161. PYX
  1162. then
  1163. if rebuild; then
  1164. expect_red "the runtime cases catch a wrstr that reads no length" \
  1165. "wrstr" python3 tests/rt_exec.py
  1166. else
  1167. echo " FAIL: the runtime would not rebuild with the length load dropped"
  1168. fail=$((fail + 1))
  1169. fi
  1170. else
  1171. echo " BROKEN CASE: the wrstr length-load mutation did not apply"
  1172. fail=$((fail + 1))
  1173. fi
  1174. cp "$SAVED" Runtime.mod
  1175. # Re-emit the object: the `rebuild` above compiled Runtime.o from the MUTATED
  1176. # source, and the restored-compiler check next links that object - without
  1177. # this recompile it would report on the mutation, not on the restored code.
  1178. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1 || {
  1179. echo " FAIL: Runtime.mod would not recompile after the restore"
  1180. fail=$((fail + 1))
  1181. }
  1182. if rebuild_compiler; then
  1183. if python3 tests/run_com_exec.py >/dev/null 2>&1; then
  1184. # No count: the matrix grows every time a fixture is added, and a
  1185. # number here would be right only until the next one.
  1186. echo " ok: every executed fixture passes on the restored compiler"
  1187. pass=$((pass + 1))
  1188. else
  1189. echo "NOT RESTORED: run_com_exec.py is red after restoring Compiler.mod"
  1190. python3 tests/run_com_exec.py 2>&1 | grep -i fail | head -3 | sed 's/^/ /'
  1191. fail=$((fail + 1))
  1192. fi
  1193. else
  1194. echo "NOT RESTORED: the compiler would not rebuild"
  1195. fail=$((fail + 1))
  1196. fi
  1197. echo
  1198. echo "== the string READ and ASSIGNMENT entries (t40, t41)"
  1199. # t39 above gave a STRING variable a frame and printed it. This milestone puts
  1200. # one on the RECEIVING end of the two operations that move data into a string
  1201. # and between two strings - readln(s)/read(s), and s := t - and each one is
  1202. # three things at once: a compiler arm that chooses a runtime entry, the
  1203. # convention that carries the limit, and the entry itself. The cases below
  1204. # break each of the six in turn. Every mutation is a fault, or a plausible
  1205. # neighbour of one, that was run to a red BY HAND before it was written here.
  1206. #
  1207. # The frame rule is the odd one out: it is not a new entry at all, but a
  1208. # pre-existing compiler bug that only this milestone could reach. A local
  1209. # STRING was packed with the INTEGER rule - the symbol took the pre-decrement
  1210. # locFree, so a string[5] local sat at BP-2 and its characters grew UP into
  1211. # BP+3, over the saved BP at [BP] and the return address at [BP+2]. `readln(s)'
  1212. # into such a local stored 'hello' over its own return address, and t40 then ran
  1213. # the string's own bytes as an address: Exec86 said `fault at IP=0x6F6C', which
  1214. # is 'l','o'. The fix packs locals top-anchored, so the first INTEGER (size 2)
  1215. # is still at -2 - byte for byte what the old rule produced - while a string[5]
  1216. # (size 6) goes to -6..-1, inside the frame. t39's size (217) and t40's
  1217. # behaviour are the two independent things that pin it.
  1218. # --- the two runtime entries ---------------------------------------------
  1219. # rdstr stored the first character where the length byte belongs, because the
  1220. # IncSi that steps SI past dest[0] was missing. The length byte then
  1221. # overwrote it: `hello' read back as 05 65 6c 6c 6f. Same length, same sizes,
  1222. # and the whole compile matrix stays green - only rt_exec.py's byte dump can
  1223. # see it.
  1224. cp "$SAVED" Runtime.mod
  1225. if python3 - <<'PYX'
  1226. p = 'Runtime.mod'
  1227. lines = open(p).read().split('\n')
  1228. out, i, n = [], 0, 0
  1229. while i < len(lines):
  1230. if lines[i].startswith(' IncSi ;') and 'dest + 1' in lines[i]:
  1231. out.append(' (* MUTATION: the IncSi that puts chars at dest+1 is gone *)')
  1232. i += 1
  1233. while i < len(lines) and not lines[i].rstrip().endswith('*)'):
  1234. i += 1
  1235. i += 1
  1236. n += 1
  1237. continue
  1238. out.append(lines[i])
  1239. i += 1
  1240. assert n == 1, 'the rdstr IncSi found %d times -- update this mutation' % n
  1241. open(p, 'w').write('\n'.join(out))
  1242. PYX
  1243. then
  1244. if rebuild; then
  1245. expect_red "rt_exec catches rdstr storing its first character at dest[0]" \
  1246. "rdstr reads a line into a string" python3 tests/rt_exec.py
  1247. else
  1248. echo " FAIL: the runtime would not rebuild with rdstr's IncSi removed"
  1249. fail=$((fail + 1))
  1250. fi
  1251. else
  1252. echo " BROKEN CASE: the rdstr IncSi mutation did not apply"
  1253. fail=$((fail + 1))
  1254. fi
  1255. cp "$SAVED" Runtime.mod
  1256. # strstore's empty-source guard. A source of length 0 sets CX := 0, and LOOP
  1257. # with CX = 0 runs 65536 times rather than zero - so `s := ''' copied 64K,
  1258. # wrapped the segment, and walked through the driver's own case record. That is
  1259. # why the pattern below matches the CASE, not a byte count: rt_exec.py reports
  1260. # this one as a malformed record, which is the honest shape of "the entry wrote
  1261. # where it had no business writing".
  1262. if python3 - <<'PYX'
  1263. p = 'Runtime.mod'
  1264. lines = open(p).read().split('\n')
  1265. out, i, n = [], 0, 0
  1266. while i < len(lines):
  1267. if lines[i].startswith(' Jcxz8 ("ss_done")'):
  1268. out.append(' (* MUTATION: the empty-source guard is gone *)')
  1269. i += 1
  1270. while i < len(lines) and not lines[i].rstrip().endswith('*)'):
  1271. i += 1
  1272. i += 1
  1273. n += 1
  1274. continue
  1275. out.append(lines[i])
  1276. i += 1
  1277. assert n == 1, 'the strstore Jcxz8 found %d times -- update this mutation' % n
  1278. open(p, 'w').write('\n'.join(out))
  1279. PYX
  1280. then
  1281. if rebuild; then
  1282. expect_red "rt_exec catches a strstore that runs LOOP with CX=0" \
  1283. "strstore copies an empty string" python3 tests/rt_exec.py
  1284. else
  1285. echo " FAIL: the runtime would not rebuild with the empty-source guard removed"
  1286. fail=$((fail + 1))
  1287. fi
  1288. else
  1289. echo " BROKEN CASE: the strstore empty-source guard mutation did not apply"
  1290. fail=$((fail + 1))
  1291. fi
  1292. cp "$SAVED" Runtime.mod
  1293. # Re-emit the object: `rebuild` above compiled Runtime.o from the MUTATED
  1294. # source, and the compiler rebuild below links that object - without this
  1295. # recompile the restored-compiler check at the end would report on the mutation.
  1296. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1 || {
  1297. echo " FAIL: Runtime.mod would not recompile after the restore"
  1298. fail=$((fail + 1))
  1299. }
  1300. # --- the frame rule -------------------------------------------------------
  1301. # The pre-fix layout, exactly: the symbol took locFree itself instead of its
  1302. # top, and the frame began at 0FFFEh. For INTEGER locals that is byte-for-byte
  1303. # the same -2, -4, ... the new rule produces, which is why the old rule survived
  1304. # so long unnoticed; for a STRING it puts characters where the return address
  1305. # is. Two independent readers go red together, so both are asserted: the
  1306. # compile matrix pins the LAYOUT (t39's code size drops 217 -> 215), and t40
  1307. # pins that the program RUNS (with the overlap its readln never sees the line
  1308. # terminator and the machine hangs).
  1309. restore_compiler
  1310. if python3 - <<'PYX'
  1311. p = 'Compiler.mod'
  1312. s = open(p).read()
  1313. a = '(locFree - size + 1) MOD 10000H'
  1314. assert s.count(a) == 2, 'the top-anchored base found %d times -- update this mutation' % s.count(a)
  1315. s = s.replace(a, 'locFree') # the old pre-decrement base
  1316. b = 'locFree := 0FFFFH ;'
  1317. assert s.count(b) == 2, 'the frame top found %d times -- update this mutation' % s.count(b)
  1318. s = s.replace(b, 'locFree := 0FFFEH ;') # the old starting point
  1319. open(p, 'w').write(s)
  1320. PYX
  1321. then
  1322. if rebuild_compiler; then
  1323. expect_red "the compile matrix catches a local string packed over the frame" \
  1324. "expected: OK 222" tests/run_compile_tests.sh
  1325. expect_red "t40 red when readln into a local string overwrites its return address" \
  1326. "never halted" python3 tests/run_com_exec.py t40_readln
  1327. else
  1328. echo " FAIL: the compiler would not rebuild with the old frame layout"
  1329. fail=$((fail + 1))
  1330. fi
  1331. else
  1332. echo " BROKEN CASE: the frame-layout revert did not apply"
  1333. fail=$((fail + 1))
  1334. fi
  1335. restore_compiler
  1336. # --- the MBF integer conversion's exponent base ---------------------------
  1337. # intreal starts with exponent 90h for a value whose top byte is set; one less
  1338. # and every real_from_int result carries the wrong exponent. rt_exec compares
  1339. # the six emitted bytes against CALC-PORT.md's measured table, so this is a
  1340. # behavioural case, not a byte-golden one.
  1341. cp "$SAVED" Runtime.mod
  1342. if mutate Runtime.mod 's| MovAxImm (0090H)| MovAxImm (008FH)|'; then
  1343. if "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1; then
  1344. expect_red "real_from_int red when the exponent base is 8Fh" \
  1345. "real_from_int" python3 tests/rt_exec.py
  1346. else
  1347. echo " FAIL: the runtime would not rebuild with the wrong base"
  1348. fail=$((fail + 1))
  1349. fi
  1350. else
  1351. echo " BROKEN CASE: the MBF exponent-base mutation did not apply"
  1352. fail=$((fail + 1))
  1353. fi
  1354. cp "$SAVED" Runtime.mod
  1355. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1
  1356. # --- the MBF sign is stored, not XORed into DH --------------------------
  1357. # XorDataDh is `30 36' (memory destination). Encoding it as `32 36' makes it
  1358. # XOR DH with the saved sign instead, which leaves the result with the
  1359. # operand's fraction bits (the bug found while transcribing readd). The
  1360. # real_add/real_sub cases compare exact bytes.
  1361. cp "$SAVED" Runtime.mod
  1362. if mutate Runtime.mod 's|BEGIN B (30H) ; B (36H) ; Dd (delta) END XorDataDh|BEGIN B (32H) ; B (36H) ; Dd (delta) END XorDataDh|'; then
  1363. if "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1; then
  1364. expect_red "real_add red when the sign is XORed into DH, not memory" \
  1365. "real_add" python3 tests/rt_exec.py
  1366. else
  1367. echo " FAIL: the runtime would not rebuild with the XOR direction wrong"
  1368. fail=$((fail + 1))
  1369. fi
  1370. else
  1371. echo " BROKEN CASE: the MBF XOR-direction mutation did not apply"
  1372. fail=$((fail + 1))
  1373. fi
  1374. cp "$SAVED" Runtime.mod
  1375. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1
  1376. # --- JccNear jumps when the condition HOLDS ------------------------------
  1377. # The near form is an INVERSE Jcc over an E9. Putting the condition back
  1378. # unchanged makes remult's `b = 0 ?' test jump when b is NON-zero, so every
  1379. # product becomes zero. The rt_exec real_mul cases compare exact bytes.
  1380. cp "$SAVED" Runtime.mod
  1381. if python3 - <<'PYX'
  1382. p = 'Runtime.mod'
  1383. s = open(p).read()
  1384. old = """ IF (code MOD 2) = 0 THEN
  1385. B (code + 1)
  1386. ELSE
  1387. B (code - 1)
  1388. END ;"""
  1389. assert s.count(old) == 1, 'the JccNear inverse found %d times' % s.count(old)
  1390. open(p, 'w').write(s.replace(old, ' B (code) ;'))
  1391. PYX
  1392. then
  1393. if "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1; then
  1394. expect_red "real_mul red when JccNear uses the raw condition" \
  1395. "real_mul" python3 tests/rt_exec.py
  1396. else
  1397. echo " FAIL: the runtime would not rebuild with the raw JccNear"
  1398. fail=$((fail + 1))
  1399. fi
  1400. else
  1401. echo " BROKEN CASE: the JccNear-inverse mutation did not apply"
  1402. fail=$((fail + 1))
  1403. fi
  1404. cp "$SAVED" Runtime.mod
  1405. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1
  1406. # --- rediv's first subtraction is SUB, not SBB ---------------------------
  1407. # TPSRC's `SUB.B AH,remul21a' ignores the carry; `SBB' would subtract the
  1408. # carry-out of the shifted numerator as well. In the ordinary compare path
  1409. # CF is 0, so the two agree and a casual transcription passes most divisions;
  1410. # only 42/6 - the one quotient below 1, where the loop subtracts on equality -
  1411. # comes out two ulps low. Encoding the AH subtraction as SBB must turn the
  1412. # exact-byte real_div cases red.
  1413. if mutate Runtime.mod 's|BEGIN B (2AH) ; B (26H) ; Dd (delta) END SubAhData|BEGIN B (1AH) ; B (26H) ; Dd (delta) END SubAhData|'; then
  1414. if "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1; then
  1415. expect_red "real_div red when the mantissa subtraction uses SBB instead of SUB" \
  1416. "real_div" python3 tests/rt_exec.py
  1417. else
  1418. echo " FAIL: the runtime would not rebuild with SubAhData as SBB"
  1419. fail=$((fail + 1))
  1420. fi
  1421. fi
  1422. cp "$SAVED" Runtime.mod
  1423. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1
  1424. # --- recmp compares b's SIGN, not its middle word -----------------------
  1425. # The different-sign path is decided by RCL on a's high word, but WHICH pair is
  1426. # different is decided by XOR DX,DI. XORing with SI (b's middle mantissa word)
  1427. # still sees a's sign, so two negatives are taken as different signs: -3.7 and
  1428. # -3.9 then compare backwards. The fractional real_cmp cases catch it.
  1429. if mutate Runtime.mod 's|BEGIN B (31H) ; B (0FAH) END XorDxDi|BEGIN B (31H) ; B (0F2H) END XorDxDi|'; then
  1430. if "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1; then
  1431. expect_red "real_cmp red when the sign XOR uses SI instead of DI" \
  1432. "real_cmp" python3 tests/rt_exec.py
  1433. else
  1434. echo " FAIL: the runtime would not rebuild with the XorDxDi mutation"
  1435. fail=$((fail + 1))
  1436. fi
  1437. fi
  1438. cp "$SAVED" Runtime.mod
  1439. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1
  1440. # --- reint must not skip the mask for ordinary large reals ---------------
  1441. # 0A8h is 2^39, above which every representable value is already integral.
  1442. # Lowering it to 088h (256) makes 12345.75 come back unchanged instead of
  1443. # 12345: the fraction survives, and the exact-byte real_int case says so.
  1444. if mutate Runtime.mod 's|CmpAl (0A8H)|CmpAl (088H)|'; then
  1445. if "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1; then
  1446. expect_red "real_int red when the integral short-circuit starts too low" \
  1447. "real_int" python3 tests/rt_exec.py
  1448. else
  1449. echo " FAIL: the runtime would not rebuild with the CmpAl mutation"
  1450. fail=$((fail + 1))
  1451. fi
  1452. fi
  1453. cp "$SAVED" Runtime.mod
  1454. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1
  1455. # --- fmtreal reads the real through DI, not BX ---------------------------
  1456. # LdBxDiOff is `8B 5D off' = MOV BX,[DI+off]. 8B 5F is [BX+off], and the
  1457. # formatter would then read its own card scratch as the real's mantissa. The
  1458. # exact-output real_fmt cases catch it.
  1459. if mutate Runtime.mod 's|BEGIN B (8BH) ; B (5DH) ; B (off MOD 256) END LdBxDiOff|BEGIN B (8BH) ; B (5FH) ; B (off MOD 256) END LdBxDiOff|'; then
  1460. if "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1; then
  1461. expect_red "real_fmt red when the real is loaded through BX, not DI" \
  1462. "real_fmt" python3 tests/rt_exec.py
  1463. else
  1464. echo " FAIL: the runtime would not rebuild with the LdBxDiOff mutation"
  1465. fail=$((fail + 1))
  1466. fi
  1467. fi
  1468. cp "$SAVED" Runtime.mod
  1469. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1
  1470. # --- ascreal's digit add is an ADD, not a SUB ----------------------------
  1471. # rd_core is the post-testexp core; its caller must set resign. The parser
  1472. # adds each digit, so resign must be 0. 8000h makes it a subtraction with the
  1473. # sign XORed into the second operand: 12341 comes out 12339-ish, while the
  1474. # offset tests (which do not look at the value) still pass.
  1475. if python3 - <<'PYX'
  1476. p = 'Runtime.mod'
  1477. s = open(p).read()
  1478. old = """ PopAx ; PopBx ; PopDx ;
  1479. StDataImm (D_RSIGN, 0) ; (* readd, not resub: resign := 0 *)"""
  1480. assert s.count(old) == 1, 'the parser resign found %d times' % s.count(old)
  1481. open(p, 'w').write(s.replace(old, """ PopAx ; PopBx ; PopDx ;
  1482. StDataImm (D_RSIGN, 8000H) ;"""))
  1483. PYX
  1484. then
  1485. if "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1; then
  1486. expect_red "real_val value red when the digit add uses resign=8000h" \
  1487. "real_val" python3 tests/rt_exec.py
  1488. else
  1489. echo " FAIL: the runtime would not rebuild with the parser resign mutation"
  1490. fail=$((fail + 1))
  1491. fi
  1492. else
  1493. echo " BROKEN CASE: the parser resign mutation did not apply"
  1494. fail=$((fail + 1))
  1495. fi
  1496. cp "$SAVED" Runtime.mod
  1497. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1
  1498. # --- the compiler's arms --------------------------------------------------
  1499. # The limit both entries read is MOV CX,imm16. MOV AX,imm16 is one opcode away,
  1500. # the same length, and the audit's MovCxi row is COMP - "no single sequence to
  1501. # disassemble" - so nothing in the byte-level checks can tell them apart. Only
  1502. # execution can: t41's `s := t' truncates to 4 under CX and does not under AX.
  1503. # (t40 passes either way, because its CX happens to survive large enough for the
  1504. # five characters it reads - exactly the kind of luck a behavioural case is for.)
  1505. restore_compiler
  1506. if mutate_emit 's| Ebyte (0B9H) ; Eword (imm)| Ebyte (0B8H) ; Eword (imm)|'; then
  1507. if rebuild_compiler; then
  1508. expect_red "t41 red when the destination limit is loaded into AX, not CX" \
  1509. "t41_strassign" python3 tests/run_com_exec.py t41_strassign
  1510. else
  1511. echo " FAIL: the compiler would not rebuild with MovCxi emitting MOV AX"
  1512. fail=$((fail + 1))
  1513. fi
  1514. fi
  1515. restore_compiler
  1516. # Assignment pushes the source first, then the destination, so the source sits
  1517. # at [BP+6] and the destination at [BP+4]. Swapped, strstore takes the
  1518. # destination as its own source - t41's `worl' rows become copies of what was
  1519. # already in the destination, which looks like it works and prints the wrong
  1520. # bytes.
  1521. restore_compiler
  1522. if python3 - <<'PYX'
  1523. p = 'Compiler.mod'
  1524. s = open(p).read()
  1525. src = (' IF src.kind = 1 THEN\n'
  1526. ' EmPushSymAddr (src.idx, (symtab [src.idx].off + src.boff) MOD 10000H)\n'
  1527. ' ELSIF src.kind = 5 THEN\n'
  1528. ' (* A string element reached through DI, including a pointer\n'
  1529. ' dereference: Buffer^[i]. Its address is already in DI. *)\n'
  1530. ' EmPushDi ()\n'
  1531. ' ELSE\n'
  1532. ' Err (ENoLib) ; (* a computed string is not a value *)\n'
  1533. ' RETURN\n'
  1534. ' END ;\n'
  1535. ' EmPushSymAddr (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H) ;')
  1536. dst = (' IF src.kind = 1 THEN\n'
  1537. ' EmPushSymAddr (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H)\n'
  1538. ' ELSIF src.kind = 5 THEN\n'
  1539. ' (* A string element reached through DI, including a pointer\n'
  1540. ' dereference: Buffer^[i]. Its address is already in DI. *)\n'
  1541. ' EmPushDi ()\n'
  1542. ' ELSE\n'
  1543. ' Err (ENoLib) ; (* a computed string is not a value *)\n'
  1544. ' RETURN\n'
  1545. ' END ;\n'
  1546. ' EmPushSymAddr (src.idx, (symtab [src.idx].off + src.boff) MOD 10000H) ;')
  1547. assert s.count(src) == 1, 'the assignment pushes found %d times -- update this mutation' % s.count(src)
  1548. open(p, 'w').write(s.replace(src, dst))
  1549. PYX
  1550. then
  1551. if rebuild_compiler; then
  1552. expect_red "t41 red when the assignment pushes are swapped" \
  1553. "t41_strassign" python3 tests/run_com_exec.py t41_strassign
  1554. else
  1555. echo " FAIL: the compiler would not rebuild with the pushes swapped"
  1556. fail=$((fail + 1))
  1557. fi
  1558. else
  1559. echo " BROKEN CASE: the assignment push-order mutation did not apply"
  1560. fail=$((fail + 1))
  1561. fi
  1562. restore_compiler
  1563. # The read dispatch's string arm. TU_RdStr and TU_RdInt sit next to each other
  1564. # in the same table and both take one address; TU_RdInt parses the line as a
  1565. # number, which stores nothing on `world', so readln(s) leaves s empty and the
  1566. # program prints a blank line. A refusal would at least say so; this arm looks
  1567. # like it worked and answers with the wrong bytes.
  1568. restore_compiler
  1569. if python3 - <<'PYX'
  1570. p = 'Compiler.mod'
  1571. s = open(p).read()
  1572. old = (' EmMovCxi ((VarSize (arg) - 1) MOD 10000H) ;\n'
  1573. ' ent := TU_RdStr')
  1574. assert s.count(old) == 1, 'the string-read arm found %d times -- update this mutation' % s.count(old)
  1575. open(p, 'w').write(s.replace(old, old.replace('TU_RdStr', 'TU_RdInt')))
  1576. PYX
  1577. then
  1578. if rebuild_compiler; then
  1579. expect_red "t40 red when a string read dispatches to TU_RdInt" \
  1580. "t40_readln" python3 tests/run_com_exec.py t40_readln
  1581. else
  1582. echo " FAIL: the compiler would not rebuild with the read dispatch on TU_RdInt"
  1583. fail=$((fail + 1))
  1584. fi
  1585. else
  1586. echo " BROKEN CASE: the string-read dispatch mutation did not apply"
  1587. fail=$((fail + 1))
  1588. fi
  1589. restore_compiler
  1590. # --- the include pre-pass -------------------------------------------------
  1591. # ExpandIncludes is the only thing that turns {$I name} into the named file;
  1592. # forced off, it is exactly the old bug - the directive is a comment and the
  1593. # include's own symbols do not exist. t56_include's `FromInclude' lives only in
  1594. # the .inc, so the compile matrix demands OK 56 4 and gets ERR 41 instead. That
  1595. # pins the fixture to the include ACTUALLY happening, not merely to the buffer
  1596. # being well formed; t57/t58 pin the two refusals (90 and 96) separately.
  1597. restore_compiler
  1598. if python3 - <<'PYX'
  1599. p = 'CompilerSource.mod'
  1600. s = open(p).read()
  1601. old = 'isInc := TRUE'
  1602. assert s.count(old) == 1, 'the include flag found %d times -- update this mutation' % s.count(old)
  1603. open(p, 'w').write(s.replace(old, 'isInc := FALSE'))
  1604. PYX
  1605. then
  1606. if rebuild_compiler; then
  1607. expect_red "t56 red when the include is swallowed, not expanded" \
  1608. "expected: OK 61 5" tests/run_compile_tests.sh
  1609. else
  1610. echo " FAIL: the compiler would not rebuild with the include forced off"
  1611. fail=$((fail + 1))
  1612. fi
  1613. else
  1614. echo " BROKEN CASE: the include-flag mutation did not apply"
  1615. fail=$((fail + 1))
  1616. fi
  1617. restore_compiler
  1618. # --- typed constants, and the caret/decimal character constants -----------
  1619. # A scalar typed constant's whole point is the CLASS its declaration gives the
  1620. # symbol: the value folds to an immediate either way, so the code size does not
  1621. # move when the class is dropped - `cls := typCls' -> `cls := cls' turns
  1622. # t59's `write (FXMax)' from the char writer to the integer one and the program
  1623. # prints 71 for 'G'. The compile matrix stays GREEN (same OK 65 4); only
  1624. # execution can see it, which is why this case runs the .COM and not the matrix.
  1625. restore_compiler
  1626. if python3 - <<'PYX'
  1627. p = 'Compiler.mod'
  1628. s = open(p).read()
  1629. old = ' cls := typCls'
  1630. assert s.count(old) == 1, 'the declared-type assignment found %d times -- update this mutation' % s.count(old)
  1631. open(p, 'w').write(s.replace(old, ' cls := cls'))
  1632. PYX
  1633. then
  1634. if rebuild_compiler; then
  1635. expect_red "t59 red when a typed constant's declared class is thrown away" \
  1636. "t59_typedconst" python3 tests/run_com_exec.py t59_typedconst
  1637. else
  1638. echo " FAIL: the compiler would not rebuild with the typed-const class dropped"
  1639. fail=$((fail + 1))
  1640. fi
  1641. else
  1642. echo " BROKEN CASE: the typed-const class mutation did not apply"
  1643. fail=$((fail + 1))
  1644. fi
  1645. restore_compiler
  1646. # ^M and #27 are CHARACTER constants, and ParseAtom must accept them as
  1647. # literals - RdConst alone only helps in a declaration. Drop them from the
  1648. # literal test and `write (^M)' is a syntax error, so the MATRIX goes red here
  1649. # (unlike the class case above, this one is visible at compile time).
  1650. restore_compiler
  1651. if python3 - <<'PYX'
  1652. p = 'Compiler.mod'
  1653. s = open(p).read()
  1654. old = " OR (CurCh () = '^') OR (CurCh () = '#') THEN"
  1655. assert s.count(old) == 1, 'the caret/decimal literal test found %d times -- update this mutation' % s.count(old)
  1656. open(p, 'w').write(s.replace(old, ' THEN'))
  1657. PYX
  1658. then
  1659. if rebuild_compiler; then
  1660. expect_red "t60 red when ^M / #nn are not character literals" \
  1661. "expected: OK 80 5" tests/run_compile_tests.sh
  1662. else
  1663. echo " FAIL: the compiler would not rebuild with the caret literal test removed"
  1664. fail=$((fail + 1))
  1665. fi
  1666. else
  1667. echo " BROKEN CASE: the caret-literal mutation did not apply"
  1668. fail=$((fail + 1))
  1669. fi
  1670. restore_compiler
  1671. # --- the set membership bit -----------------------------------------------
  1672. # Compile-time `in' reads a byte out of the emitted set and tests one bit. Flip
  1673. # the test and every answer is wrong while the sizes are untouched, so the
  1674. # compile matrix stays GREEN (same OK 183 4) and only execution sees it - the
  1675. # same shape as the typed-constant class case above, and the reason t61 has a
  1676. # .out rather than just a row.
  1677. restore_compiler
  1678. if python3 - <<'PYX'
  1679. p = 'Compiler.mod'
  1680. s = open(p).read()
  1681. old = 'isIn := (o DIV m) MOD 2 # 0'
  1682. assert s.count(old) == 1, 'the set membership bit test found %d times -- update this mutation' % s.count(old)
  1683. open(p, 'w').write(s.replace(old, 'isIn := (o DIV m) MOD 2 = 0'))
  1684. PYX
  1685. then
  1686. if rebuild_compiler; then
  1687. expect_red "t61 red when the set membership bit test is inverted" \
  1688. "t61_setconst" python3 tests/run_com_exec.py t61_setconst
  1689. else
  1690. echo " FAIL: the compiler would not rebuild with the set bit test inverted"
  1691. fail=$((fail + 1))
  1692. fi
  1693. else
  1694. echo " BROKEN CASE: the set-bit mutation did not apply"
  1695. fail=$((fail + 1))
  1696. fi
  1697. restore_compiler
  1698. # --- an enumerator's ordinal ----------------------------------------------
  1699. # Every enumerator must get its own ordinal (0,1,2,..). Give them all 0 and
  1700. # every `a = SomeEnumerator' is TRUE, while the sizes do not move: the compile
  1701. # matrix stays green (OK 187 5) and only execution sees it.
  1702. restore_compiler
  1703. if python3 - <<'PYX'
  1704. p = 'Compiler.mod'
  1705. s = open(p).read()
  1706. old = 'KConst, TScalar, 1, 1, 0, hi + 1, FALSE'
  1707. assert s.count(old) == 1, 'the enumerator ordinal found %d times -- update this mutation' % s.count(old)
  1708. open(p, 'w').write(s.replace(old, 'KConst, TScalar, 1, 1, 0, 0, FALSE'))
  1709. PYX
  1710. then
  1711. if rebuild_compiler; then
  1712. expect_red "t62 red when every enumerator gets ordinal 0" \
  1713. "t62_enum" python3 tests/run_com_exec.py t62_enum
  1714. else
  1715. echo " FAIL: the compiler would not rebuild with the enumerator ordinals flattened"
  1716. fail=$((fail + 1))
  1717. fi
  1718. else
  1719. echo " BROKEN CASE: the enumerator-ordinal mutation did not apply"
  1720. fail=$((fail + 1))
  1721. fi
  1722. restore_compiler
  1723. # --- subrange sizing ------------------------------------------------------
  1724. # TP3 sizes a subrange by its bounds: one byte when both are inside 0..255.
  1725. # Force two bytes and CellRec grows from 80 to 82, so the compile matrix's
  1726. # `data=84' becomes `data=86' - this one is visible at compile time, unlike the
  1727. # set/class cases above.
  1728. restore_compiler
  1729. if python3 - <<'PYX'
  1730. p = 'Compiler.mod'
  1731. s = open(p).read()
  1732. old = 'IF (lo >= 0) AND (hi <= 255) AND (lo <= hi) THEN\n size := 1'
  1733. assert s.count(old) == 1, 'the subrange size test found %d times -- update this mutation' % s.count(old)
  1734. open(p, 'w').write(s.replace(old, old.replace('size := 1', 'size := 2')))
  1735. PYX
  1736. then
  1737. if rebuild_compiler; then
  1738. expect_red "t63 red when a small subrange is sized two bytes" \
  1739. "expected: OK 62 85" tests/run_compile_tests.sh
  1740. else
  1741. echo " FAIL: the compiler would not rebuild with subranges forced to two bytes"
  1742. fail=$((fail + 1))
  1743. fi
  1744. else
  1745. echo " BROKEN CASE: the subrange-size mutation did not apply"
  1746. fail=$((fail + 1))
  1747. fi
  1748. restore_compiler
  1749. # --- an element's width ---------------------------------------------------
  1750. # A constant array subscript must hand the ELEMENT's size to the store, or the
  1751. # array's own size is used and `v[1] := 11' writes the whole array. Drop the
  1752. # width and the compile matrix stays green (same OK 101 16) while execution
  1753. # sees 0s and a corrupted neighbour - the shape the width exists to prevent.
  1754. restore_compiler
  1755. if python3 - <<'PYX'
  1756. p = 'Compiler.mod'
  1757. s = open(p).read()
  1758. # The element width is set once after the subscript list, for constant and
  1759. # run-time indices alike; dropping it makes the whole array's width win.
  1760. old = 'r.sz := arrs [ai].elem'
  1761. new = 'r.sz := 0'
  1762. assert s.count(old) == 1, 'the element width assignment found %d times -- update this mutation' % s.count(old)
  1763. open(p, 'w').write(s.replace(old, new))
  1764. PYX
  1765. then
  1766. if rebuild_compiler; then
  1767. expect_red "t65 red when a subscripted element keeps the array's width" \
  1768. "t65_array_access" python3 tests/run_com_exec.py t65_array_access
  1769. else
  1770. echo " FAIL: the compiler would not rebuild with the element width dropped"
  1771. fail=$((fail + 1))
  1772. fi
  1773. else
  1774. echo " BROKEN CASE: the element-width mutation did not apply"
  1775. fail=$((fail + 1))
  1776. fi
  1777. restore_compiler
  1778. # --- the file variable's size ---------------------------------------------
  1779. # Our file control block is FileVarSize bytes; shrink it and every file
  1780. # variable's slot moves, so t66's `data=269' becomes `data=265'. Visible at
  1781. # compile time, so the matrix carries it.
  1782. restore_compiler
  1783. if python3 - <<'PYX'
  1784. p = 'Compiler.mod'
  1785. s = open(p).read()
  1786. old = 'FileVarSize = 132 ;'
  1787. assert s.count(old) == 1, 'the file variable size found %d times -- update this mutation' % s.count(old)
  1788. open(p, 'w').write(s.replace(old, 'FileVarSize = 130 ;'))
  1789. PYX
  1790. then
  1791. if rebuild_compiler; then
  1792. expect_red "t66 red when the file variable is the wrong size" \
  1793. "expected: OK 55 269" tests/run_compile_tests.sh
  1794. else
  1795. echo " FAIL: the compiler would not rebuild with the file size changed"
  1796. fail=$((fail + 1))
  1797. fi
  1798. else
  1799. echo " BROKEN CASE: the file-size mutation did not apply"
  1800. fail=$((fail + 1))
  1801. fi
  1802. restore_compiler
  1803. # --- a string parameter's slot --------------------------------------------
  1804. # A string VALUE parameter's slot is its size (n+1 bytes). Collapse SlotSize
  1805. # to 2 for it and BOTH sides agree on 2 - so the compiler still compiles and
  1806. # the matrix stays green - but the callee's "string" is just a length byte and
  1807. # garbage; t67 prints something other than `hello'.
  1808. restore_compiler
  1809. if python3 - <<'PYX'
  1810. p = 'Compiler.mod'
  1811. s = open(p).read()
  1812. old = ' RETURN size\nEND SlotSize ;'
  1813. assert s.count(old) == 1, 'the SlotSize fallthrough found %d times -- update this mutation' % s.count(old)
  1814. open(p, 'w').write(s.replace(old, ' RETURN 2\nEND SlotSize ;'))
  1815. PYX
  1816. then
  1817. if rebuild_compiler; then
  1818. expect_red "t67 red when a string parameter's slot collapses to a word" \
  1819. "t67_strparam" python3 tests/run_com_exec.py t67_strparam
  1820. else
  1821. echo " FAIL: the compiler would not rebuild with SlotSize collapsed"
  1822. fail=$((fail + 1))
  1823. fi
  1824. else
  1825. echo " BROKEN CASE: the string-slot mutation did not apply"
  1826. fail=$((fail + 1))
  1827. fi
  1828. restore_compiler
  1829. # --- a string builtin is registered ---------------------------------------
  1830. # LENGTH has to be in the symbol table, or the name is simply unknown. Remove
  1831. # its registration and t68 stops compiling (ERR 41), so the compile matrix
  1832. # carries this one.
  1833. restore_compiler
  1834. if python3 - <<'PYX'
  1835. p = 'Compiler.mod'
  1836. s = open(p).read()
  1837. old = (' i := NewSym ("LENGTH" , KBuiltin, BI_Length , 0, 0, 0, 0, FALSE) ;\n'
  1838. ' symtab [i].defnd := TRUE ;\n'
  1839. ' symtab [i].goPos := 0 ; (* inline: the length byte, no entry *)\n')
  1840. assert s.count(old) == 1, 'the LENGTH registration found %d times -- update this mutation' % s.count(old)
  1841. open(p, 'w').write(s.replace(old, ''))
  1842. PYX
  1843. then
  1844. if rebuild_compiler; then
  1845. expect_red "t68 red when LENGTH is not registered" \
  1846. "expected: OK 127 26" tests/run_compile_tests.sh
  1847. else
  1848. echo " FAIL: the compiler would not rebuild without the LENGTH builtin"
  1849. fail=$((fail + 1))
  1850. fi
  1851. else
  1852. echo " BROKEN CASE: the LENGTH-registration mutation did not apply"
  1853. fail=$((fail + 1))
  1854. fi
  1855. restore_compiler
  1856. # --- a CRT builtin is registered ------------------------------------------
  1857. # ClrScr must be in the symbol table for t69 to compile. Remove it and the
  1858. # fixture stops at ERR 41 against its expected OK 102 4.
  1859. restore_compiler
  1860. if python3 - <<'PYX'
  1861. p = 'Compiler.mod'
  1862. s = open(p).read()
  1863. old = ' i := NewSym ("CLRSCR" , KBuiltin, BI_ClrScr , 0, 0, 0, 0, FALSE) ;\n symtab [i].defnd := TRUE ;\n'
  1864. assert s.count(old) == 1, 'the CLRSCR registration found %d times -- update this mutation' % s.count(old)
  1865. open(p, 'w').write(s.replace(old, ''))
  1866. PYX
  1867. then
  1868. if rebuild_compiler; then
  1869. expect_red "t69 red when CLRSCR is not registered" \
  1870. "expected: OK 158 5" tests/run_compile_tests.sh
  1871. else
  1872. echo " FAIL: the compiler would not rebuild without the CLRSCR builtin"
  1873. fail=$((fail + 1))
  1874. fi
  1875. else
  1876. echo " BROKEN CASE: the CLRSCR-registration mutation did not apply"
  1877. fail=$((fail + 1))
  1878. fi
  1879. restore_compiler
  1880. # --- KBD is registered ----------------------------------------------------
  1881. # Without the predefined KBD file, `Read(Kbd, Ch)' is an unknown identifier and
  1882. # t70 stops compiling against its expected OK 55 5.
  1883. restore_compiler
  1884. if python3 - <<'PYX'
  1885. p = 'Compiler.mod'
  1886. s = open(p).read()
  1887. old = (' i := NewSym ("KBD", KVar, TText, FileVarSize, 1, 0, 0, FALSE) ;\n'
  1888. ' symtab [i].isKbd := TRUE ;\n')
  1889. assert s.count(old) == 1, 'the KBD registration found %d times -- update this mutation' % s.count(old)
  1890. open(p, 'w').write(s.replace(old, ''))
  1891. PYX
  1892. then
  1893. if rebuild_compiler; then
  1894. expect_red "t70 red when KBD is not registered" \
  1895. "expected: OK 60 6" tests/run_compile_tests.sh
  1896. else
  1897. echo " FAIL: the compiler would not rebuild without the KBD file"
  1898. fail=$((fail + 1))
  1899. fi
  1900. else
  1901. echo " BROKEN CASE: the KBD-registration mutation did not apply"
  1902. fail=$((fail + 1))
  1903. fi
  1904. restore_compiler
  1905. # --- KEYPRESSED is registered ---------------------------------------------
  1906. # It is a FUNCTION, not a statement builtin, so it is registered like LENGTH
  1907. # and parsed in ParseAtom. Remove it and t72 stops at ERR 41.
  1908. restore_compiler
  1909. if python3 - <<'PYX'
  1910. p = 'Compiler.mod'
  1911. s = open(p).read()
  1912. old = (' i := NewSym ("KEYPRESSED", KBuiltin, BI_KeyPressed, 0, 0, 0, 0, FALSE) ;\n'
  1913. ' symtab [i].defnd := TRUE ;\n')
  1914. assert s.count(old) == 1, 'the KEYPRESSED registration found %d times -- update this mutation' % s.count(old)
  1915. open(p, 'w').write(s.replace(old, ''))
  1916. PYX
  1917. then
  1918. if rebuild_compiler; then
  1919. expect_red "t72 red when KEYPRESSED is not registered" "expected: OK 138 6" tests/run_compile_tests.sh
  1920. else
  1921. echo " FAIL: the compiler would not rebuild without the KEYPRESSED builtin"
  1922. fail=$((fail + 1))
  1923. fi
  1924. else
  1925. echo " BROKEN CASE: the KEYPRESSED-registration mutation did not apply"
  1926. fail=$((fail + 1))
  1927. fi
  1928. restore_compiler
  1929. # --- KeyPressed masks AL, not AX ------------------------------------------
  1930. # AH is still 0Bh after INT 21h, so `AND AL,1' leaves AX = 0B00h and the
  1931. # boolean test `CMP AX,0' calls the NO-KEY answer TRUE. bootcom's handler
  1932. # happens to IRET the whole AX and returns AH=0, so qemu cannot see this;
  1933. # only the Exec86 oracle can, which is why t72 runs under both.
  1934. restore_compiler
  1935. if mutate_compiler 's| Ebyte (25H) ; Ebyte (01H) ; Ebyte (00H)| Ebyte (24H) ; Ebyte (01H)|'; then
  1936. if rebuild_compiler; then
  1937. expect_red "t72 red when KeyPressed masks AL instead of AX" "more" python3 tests/run_exec86.py t72_keypress
  1938. else
  1939. echo " FAIL: the compiler would not rebuild with AND AL,1"
  1940. fail=$((fail + 1))
  1941. fi
  1942. else
  1943. echo " BROKEN CASE: the AND AX,1 mutation did not apply"
  1944. fail=$((fail + 1))
  1945. fi
  1946. restore_compiler
  1947. # --- a parameter may shadow a global --------------------------------------
  1948. # DupTest used to reject any name Search could see, so CALC's
  1949. # `IBMCh(var Ch: Char)' collided with the global `Ch'. Make the level test
  1950. # unconditional and t73 is ERR 41 at the parameter name again.
  1951. restore_compiler
  1952. if mutate_symbols 's| IF symtab \[i\].level = lexnest THEN| IF TRUE THEN|'; then
  1953. if rebuild_compiler; then
  1954. expect_red "t73 red when a parameter may not shadow a global" "expected: OK 340 8" tests/run_compile_tests.sh
  1955. else
  1956. echo " FAIL: the compiler would not rebuild with the old DupTest"
  1957. fail=$((fail + 1))
  1958. fi
  1959. else
  1960. echo " BROKEN CASE: the DupTest level-test mutation did not apply"
  1961. fail=$((fail + 1))
  1962. fi
  1963. restore_compiler
  1964. # --- a VAR parameter is not a value parameter -----------------------------
  1965. # Record the formal as a value parameter again (the exact old behaviour):
  1966. # the caller pushes a copy and IBMCh/Bump update that copy. This compiles and
  1967. # runs, and prints H,H,41,a - which is why it is the execution, not the
  1968. # compile, that must catch it.
  1969. restore_compiler
  1970. if mutate_compiler 's| symtab \[i\].isVar := groupVar ;| symtab \[i\].isVar := FALSE ;|'; then
  1971. if rebuild_compiler; then
  1972. expect_red "t73 red when a VAR parameter is recorded as value" "FAIL" python3 tests/run_com_exec.py t73_varparam
  1973. else
  1974. echo " FAIL: the compiler would not rebuild with VAR as value"
  1975. fail=$((fail + 1))
  1976. fi
  1977. else
  1978. echo " BROKEN CASE: the VAR-as-value mutation did not apply"
  1979. fail=$((fail + 1))
  1980. fi
  1981. restore_compiler
  1982. # --- a CASE range is a range ----------------------------------------------
  1983. # The old emitter compared the selector with lo and then with hi, both
  1984. # demanding equality, so `1..5' matched nothing. Invert only the high test:
  1985. # i=5 in t74's `1..5' arm then falls through to ELSE, and the .out says
  1986. # `range'. This is an execution failure, not a compile failure - the range
  1987. # compiled and ran, and answered wrongly.
  1988. restore_compiler
  1989. if mutate_compiler 's|EmJcc (8FH, 0)|EmJcc (84H, 0)|'; then
  1990. if rebuild_compiler; then
  1991. expect_red "t74 red when a CASE range requires equality" "FAIL" python3 tests/run_com_exec.py t74_caselist
  1992. else
  1993. echo " FAIL: the compiler would not rebuild with the broken range test"
  1994. fail=$((fail + 1))
  1995. fi
  1996. else
  1997. echo " BROKEN CASE: the CASE-range mutation did not apply"
  1998. fail=$((fail + 1))
  1999. fi
  2000. restore_compiler
  2001. # --- an array subscript scales by the element size ------------------------
  2002. # Skip the multiply: for an Integer array a[i] is then base + (i - lo) BYTES,
  2003. # so a[2] reads the second byte of a[1]. Compiles, runs, wrong - an execution
  2004. # failure, which is what t75 is executed for.
  2005. restore_compiler
  2006. if mutate_compiler 's| IF st # 1 THEN| IF FALSE THEN|'; then
  2007. if rebuild_compiler; then
  2008. expect_red "t75 red when an array subscript is not scaled" "FAIL" python3 tests/run_com_exec.py t75_arrvar
  2009. else
  2010. echo " FAIL: the compiler would not rebuild without the element scale"
  2011. fail=$((fail + 1))
  2012. fi
  2013. else
  2014. echo " BROKEN CASE: the element-scale mutation did not apply"
  2015. fail=$((fail + 1))
  2016. fi
  2017. restore_compiler
  2018. # --- a field width reaches the width-aware writer -------------------------
  2019. # Call wrint instead of wrintw. The caller still pushes the value and the
  2020. # width and still cleans up four bytes, so wrint reads the WIDTH as its value
  2021. # and the stack stays balanced: t76 prints the widths, not the numbers.
  2022. restore_compiler
  2023. if mutate_compiler 's|ent := TU_WrIntW|ent := TU_WrInt|'; then
  2024. if rebuild_compiler; then
  2025. expect_red "t76 red when a width calls the plain integer writer" "FAIL" python3 tests/run_com_exec.py t76_width
  2026. else
  2027. echo " FAIL: the compiler would not rebuild with the plain writer"
  2028. fail=$((fail + 1))
  2029. fi
  2030. else
  2031. echo " BROKEN CASE: the width-entry mutation did not apply"
  2032. fail=$((fail + 1))
  2033. fi
  2034. restore_compiler
  2035. # --- a 2-D subscript uses the row stride ----------------------------------
  2036. # Let every dimension use the ELEMENT size: `g[i,j]' then adds (i-lo0)*elem
  2037. # and (j-lo1)*elem instead of scaling the first by the second dimension's
  2038. # count. 1-D is unchanged, so t75 stays green; t77's 2-D element moves.
  2039. restore_compiler
  2040. if mutate_compiler 's| k := nd + 1 ;| k := arrs \[ai\].n ;|'; then
  2041. if rebuild_compiler; then
  2042. expect_red "t77 red when the 2-D row stride is dropped" "FAIL" python3 tests/run_com_exec.py t77_with
  2043. else
  2044. echo " FAIL: the compiler would not rebuild without the row stride"
  2045. fail=$((fail + 1))
  2046. fi
  2047. else
  2048. echo " BROKEN CASE: the row-stride mutation did not apply"
  2049. fail=$((fail + 1))
  2050. fi
  2051. restore_compiler
  2052. # --- a WITH over an element keeps the element's offset ---------------------
  2053. # `with g[1,2]' must remember boff (2,3's predecessor); dropping it makes the
  2054. # body address g[1,1]. t77 sets g[1,2].a := 7 and reads it back.
  2055. restore_compiler
  2056. if mutate_compiler 's| withBoff \[withN\] := t.boff| withBoff \[withN\] := 0|'; then
  2057. if rebuild_compiler; then
  2058. expect_red "t77 red when WITH forgets the element offset" "FAIL" python3 tests/run_com_exec.py t77_with
  2059. else
  2060. echo " FAIL: the compiler would not rebuild without the WITH offset"
  2061. fail=$((fail + 1))
  2062. fi
  2063. else
  2064. echo " BROKEN CASE: the WITH-offset mutation did not apply"
  2065. fail=$((fail + 1))
  2066. fi
  2067. restore_compiler
  2068. # --- a set field's constructor is sized by the TARGET ---------------------
  2069. # Write the default 32-byte set-of-Char length instead of the field's one
  2070. # byte: the store runs past Status into N and the neighbours. t78 reads N
  2071. # back, so the overrun is visible.
  2072. restore_compiler
  2073. if mutate_compiler 's| WHILE k < r.setsize DO| WHILE k < 32 DO|'; then
  2074. if rebuild_compiler; then
  2075. expect_red "t78 red when a set field stores 32 bytes, not its size" "FAIL" python3 tests/run_com_exec.py t78_setfield
  2076. else
  2077. echo " FAIL: the compiler would not rebuild with the 32-byte store"
  2078. fail=$((fail + 1))
  2079. fi
  2080. else
  2081. echo " BROKEN CASE: the set-size mutation did not apply"
  2082. fail=$((fail + 1))
  2083. fi
  2084. restore_compiler
  2085. # --- a field's set base offset is the ARRAY/RECORD's ----------------------
  2086. # `in' reads setoff + boff. Forgetting the symbol's base (setoff := 0) makes
  2087. # sh[2,3].Status read absolute address 15 instead of sh+15.
  2088. restore_compiler
  2089. if mutate_compiler 's| r.setoff := symtab \[r.idx\].off ;| r.setoff := 0 ;|'; then
  2090. if rebuild_compiler; then
  2091. expect_red "t78 red when a field's set base offset is dropped" "FAIL" python3 tests/run_com_exec.py t78_setfield
  2092. else
  2093. echo " FAIL: the compiler would not rebuild without the set base"
  2094. fail=$((fail + 1))
  2095. fi
  2096. else
  2097. echo " BROKEN CASE: the set-base mutation did not apply"
  2098. fail=$((fail + 1))
  2099. fi
  2100. restore_compiler
  2101. # --- a short literal sets the SOURCE length ---------------------------------
  2102. # Skip the min(source length, capacity): n stays at the field's capacity and
  2103. # the copy reads past the literal into the string pool, so 'abc' prints five
  2104. # characters. t79's first line is 'abc'.
  2105. restore_compiler
  2106. if mutate_compiler 's| IF strLen \[src.strx\] < n THEN| IF FALSE THEN|'; then
  2107. if rebuild_compiler; then
  2108. expect_red "t79 red when a short string literal keeps the capacity" "FAIL" python3 tests/run_com_exec.py t79_strfield
  2109. else
  2110. echo " FAIL: the compiler would not rebuild without the length min"
  2111. fail=$((fail + 1))
  2112. fi
  2113. else
  2114. echo " BROKEN CASE: the literal-length mutation did not apply"
  2115. fail=$((fail + 1))
  2116. fi
  2117. restore_compiler
  2118. # --- a DI-addressed string field stores its length --------------------------
  2119. # Store a zero length byte: the characters go to the field but every reader
  2120. # sees an empty string. t79 writes 'xy' through WITH and prints it back.
  2121. restore_compiler
  2122. if mutate_compiler 's| EmStDiImm (0, n) ;| EmStDiImm (0, 0) ;|'; then
  2123. if rebuild_compiler; then
  2124. expect_red "t79 red when a string field's length byte is zero" "FAIL" python3 tests/run_com_exec.py t79_strfield
  2125. else
  2126. echo " FAIL: the compiler would not rebuild with a zero length"
  2127. fail=$((fail + 1))
  2128. fi
  2129. else
  2130. echo " BROKEN CASE: the string length-byte mutation did not apply"
  2131. fail=$((fail + 1))
  2132. fi
  2133. restore_compiler
  2134. # --- the Real exponent bias is 129, not 128 -------------------------------
  2135. # byte 0 is E + 129 in CALC-PORT.md's measured table. One less and every
  2136. # constant's exponent is wrong; the byte check reads the emitted code.
  2137. restore_compiler
  2138. if mutate_compiler 's| e := 129 + k ;| e := 128 + k ;|'; then
  2139. if rebuild_compiler; then
  2140. if ./tests/run_compile_tests.sh --build-only >/dev/null 2>&1; then
  2141. expect_red "the Real bytes red when the exponent bias is 128" "FAIL" python3 tests/check_realconst.py
  2142. else
  2143. echo " FAIL: compiletest would not rebuild with the wrong bias"
  2144. fail=$((fail + 1))
  2145. fi
  2146. else
  2147. echo " FAIL: the compiler would not rebuild with the wrong bias"
  2148. fail=$((fail + 1))
  2149. fi
  2150. else
  2151. echo " BROKEN CASE: the Real-bias mutation did not apply"
  2152. fail=$((fail + 1))
  2153. fi
  2154. restore_compiler
  2155. # --- the fraction is (a - 2^k), not a ------------------------------------
  2156. # Dropping the implicit 1 from the mantissa: 1.0 becomes 2.0, 3.0's bytes are
  2157. # wrong, and the measured table no longer appears.
  2158. restore_compiler
  2159. if mutate_compiler 's| n := a - p ;| n := a ;|'; then
  2160. if rebuild_compiler; then
  2161. if ./tests/run_compile_tests.sh --build-only >/dev/null 2>&1; then
  2162. expect_red "the Real bytes red when the implicit 1 is missing" "FAIL" python3 tests/check_realconst.py
  2163. else
  2164. echo " FAIL: compiletest would not rebuild without the implicit 1"
  2165. fail=$((fail + 1))
  2166. fi
  2167. else
  2168. echo " FAIL: the compiler would not rebuild without the implicit 1"
  2169. fail=$((fail + 1))
  2170. fi
  2171. else
  2172. echo " BROKEN CASE: the implicit-1 mutation did not apply"
  2173. fail=$((fail + 1))
  2174. fi
  2175. restore_compiler
  2176. # --- UpCase subtracts 32, not 33 ------------------------------------------
  2177. # The run-time fold; 'a' becomes '@'. t81 prints A for 'a'.
  2178. restore_compiler
  2179. if mutate_compiler 's| EmSubAlImm (32) ;| EmSubAlImm (33) ;|'; then
  2180. if rebuild_compiler; then
  2181. expect_red "t81 red when UpCase subtracts 33" "FAIL" python3 tests/run_com_exec.py t81_upcase
  2182. else
  2183. echo " FAIL: the compiler would not rebuild with a 33 fold"
  2184. fail=$((fail + 1))
  2185. fi
  2186. else
  2187. echo " BROKEN CASE: the UpCase-fold mutation did not apply"
  2188. fail=$((fail + 1))
  2189. fi
  2190. restore_compiler
  2191. # --- a set membership indexes bytes with DIV 8 ----------------------------
  2192. # Two SHRs instead of three divide by 4: 'Y' (89) picks byte 22 of a 32-byte
  2193. # literal, whose bits are clear. t82 expects the first line to be 'yes'.
  2194. restore_compiler
  2195. if mutate_compiler 's| EmShrAx1 () ; EmShrAx1 () ; EmShrAx1 () ;| EmShrAx1 () ; EmShrAx1 () ;|'; then
  2196. if rebuild_compiler; then
  2197. expect_red "t82 red when set membership divides by 4, not 8" "FAIL" python3 tests/run_com_exec.py t82_setin
  2198. else
  2199. echo " FAIL: the compiler would not rebuild with two shifts"
  2200. fail=$((fail + 1))
  2201. fi
  2202. else
  2203. echo " BROKEN CASE: the set-index shift mutation did not apply"
  2204. fail=$((fail + 1))
  2205. fi
  2206. restore_compiler
  2207. # --- real output goes through real_write, not the placeholder ----------
  2208. # TU_WrReal is still the "?REAL?" marker; the compiler's new real arm must
  2209. # select TU_RealWrite. t84 prints a real field with variable width/decimals.
  2210. restore_compiler
  2211. if mutate_compiler 's| ent := TU_RealWrite ;| ent := TU_WrReal ;|'; then
  2212. if rebuild_compiler; then
  2213. expect_red "t84 red when real output falls back to the placeholder" \
  2214. "t84_realwrite" python3 tests/run_com_exec.py t84_realwrite
  2215. else
  2216. echo " FAIL: the compiler would not rebuild with the placeholder"
  2217. fail=$((fail + 1))
  2218. fi
  2219. fi
  2220. restore_compiler
  2221. # --- a DI-addressed string field is an address, not a scalar ------------
  2222. # `if A[I].Contents <> '' then' must push DI; the old EmPushStrAddr refused
  2223. # kind 5 (ENoLib) and SaveLeft would try to load the whole string.
  2224. restore_compiler
  2225. if python3 - <<'PYX'
  2226. p = 'Compiler.mod'
  2227. s = open(p).read()
  2228. old = """ IF r.kind = 5 THEN
  2229. (* a string element/field computed through DI - `Sheet[X,Y].Contents'"""
  2230. assert s.count(old) == 1, 'the kind-5 string branch found %d times' % s.count(old)
  2231. open(p, 'w').write(s.replace(old, """ IF r.kind = 4 THEN
  2232. (* a string element/field computed through DI - `Sheet[X,Y].Contents'"""))
  2233. PYX
  2234. then
  2235. if rebuild_compiler; then
  2236. expect_red "t83 red when a string field address is refused" \
  2237. "t83_strfield_cmp" python3 tests/run_com_exec.py t83_strfield_cmp
  2238. else
  2239. echo " FAIL: the compiler would not rebuild without the kind-5 push"
  2240. fail=$((fail + 1))
  2241. fi
  2242. else
  2243. echo " BROKEN CASE: the kind-5 string-field mutation did not apply"
  2244. fail=$((fail + 1))
  2245. fi
  2246. restore_compiler
  2247. # --- real_write pads from the WIDTH, not from the length ----------------
  2248. # The pad is width - length; using the length for both makes it zero, so every
  2249. # right-justified field loses its leading spaces. rt_exec checks exact output.
  2250. cp "$SAVED" Runtime.mod
  2251. if mutate Runtime.mod 's| MovAxData (D_FMTW) ;| MovAxData (D_FMTLEN) ;|'; then
  2252. if "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1; then
  2253. expect_red "real_write red when the pad uses the length" \
  2254. "real_write" python3 tests/rt_exec.py
  2255. else
  2256. echo " FAIL: the runtime would not rebuild with the pad mutation"
  2257. fail=$((fail + 1))
  2258. fi
  2259. fi
  2260. cp "$SAVED" Runtime.mod
  2261. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1
  2262. # --- set_op's intersection really ANDs the two bytes ---------------------
  2263. # 20 D0 is AND AL,DL; 20 C2 is AND DL,AL and leaves AL (the left byte), which
  2264. # is exactly the bug this rung fixed. The exact-byte set_op cases catch it.
  2265. cp "$SAVED" Runtime.mod
  2266. if mutate Runtime.mod 's|PROCEDURE AndAlDl ; BEGIN B (20H) ; B (0D0H)|PROCEDURE AndAlDl ; BEGIN B (20H) ; B (0C2H)|'; then
  2267. if "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1; then
  2268. expect_red "set_op red when intersection uses the wrong AND operand" \
  2269. "set_op" python3 tests/rt_exec.py
  2270. else
  2271. echo " FAIL: the runtime would not rebuild with the AND mutation"
  2272. fail=$((fail + 1))
  2273. fi
  2274. fi
  2275. cp "$SAVED" Runtime.mod
  2276. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1
  2277. # --- set union is OR, not intersection -----------------------------------
  2278. # t85's `union' line is `Status := Status + [C]'; choosing intersection there
  2279. # loses C (and the later difference line too). This pins the compiler's
  2280. # operator-to-runtime mapping, not just the runtime core.
  2281. restore_compiler
  2282. if mutate_compiler 's| SetBinEmit (1, left, right, r)| SetBinEmit (0, left, right, r)|'; then
  2283. if rebuild_compiler; then
  2284. expect_red "t85 red when set addition becomes intersection" \
  2285. "t85_setops" python3 tests/run_com_exec.py t85_setops
  2286. else
  2287. echo " FAIL: the compiler would not rebuild with set + as *"
  2288. fail=$((fail + 1))
  2289. fi
  2290. fi
  2291. restore_compiler
  2292. # --- Succ increments, Pred decrements ------------------------------------
  2293. restore_compiler
  2294. if python3 - <<'PYX'
  2295. p = 'Compiler.mod'
  2296. s = open(p).read()
  2297. old = """ IF symtab [idx].cls = BI_Succ THEN
  2298. EmIncAx ()
  2299. ELSE
  2300. EmDecAx ()"""
  2301. assert s.count(old) == 1, 'the Succ branch found %d times' % s.count(old)
  2302. open(p, 'w').write(s.replace(old, """ IF symtab [idx].cls = BI_Succ THEN
  2303. EmDecAx ()
  2304. ELSE
  2305. EmDecAx ()"""))
  2306. PYX
  2307. then
  2308. if rebuild_compiler; then
  2309. expect_red "t86 red when Succ decrements" \
  2310. "t86_succ" python3 tests/run_com_exec.py t86_succ
  2311. else
  2312. echo " FAIL: the compiler would not rebuild with Succ as Pred"
  2313. fail=$((fail + 1))
  2314. fi
  2315. else
  2316. echo " BROKEN CASE: the Succ mutation did not apply"
  2317. fail=$((fail + 1))
  2318. fi
  2319. restore_compiler
  2320. if rebuild_compiler; then
  2321. if python3 tests/run_com_exec.py >/dev/null 2>&1; then
  2322. echo " ok: every executed fixture passes on the restored sources"
  2323. pass=$((pass + 1))
  2324. else
  2325. echo "NOT RESTORED: run_com_exec.py is red after restoring the sources"
  2326. python3 tests/run_com_exec.py 2>&1 | grep -i fail | head -3 | sed 's/^/ /'
  2327. fail=$((fail + 1))
  2328. fi
  2329. else
  2330. echo "NOT RESTORED: the compiler would not rebuild"
  2331. fail=$((fail + 1))
  2332. fi
  2333. echo
  2334. echo "== the second execution oracle (run_exec86.py)"
  2335. # Everything above runs the image under qemu-system-i386. This one runs the
  2336. # SAME image inside shell/Exec86.mod, this project's own in-process 8086
  2337. # interpreter, and requires its output to agree with BOTH the hand-derived .out
  2338. # and qemu, byte for byte. Two execution checks that could only ever agree
  2339. # with each other would be one check: the point of this one is that it was
  2340. # written against the 8086's own reference rather than against qemu, whose
  2341. # lowest CPU model is a 486 and whose `0F 84' is an ordinary JZ.
  2342. #
  2343. # So the only mutation worth writing here is one qemu cannot make at all - the
  2344. # interpreter's own reading of the machine. Cond is that reading: nibble 4 is
  2345. # JE, and inverting it swaps the two arms of every `=' in every program. The
  2346. # emitted bytes, the sizes and the compile matrix are untouched, because
  2347. # nothing is emitted here - the fault is in who interprets it.
  2348. #
  2349. # Exec86.mod is recompiled explicitly rather than left to the harness:
  2350. # ensure_exec86run() notices the source changed and RELINKS, but does not
  2351. # recompile it, so a mutated source with a stale object would link the good
  2352. # interpreter straight back in and stay green - which is the trap its own
  2353. # docstring records, and the reason this case compiles first.
  2354. #
  2355. # SAVED_E itself was taken at the top of this file, next to the EXIT trap that
  2356. # puts it back.
  2357. if python3 - <<'PYX'
  2358. p = 'Exec86.mod'
  2359. s = open(p).read()
  2360. old = "| 4H : r := ZF"
  2361. new = "| 4H : r := NOT ZF (* MUTATION: JE inverted *)"
  2362. assert s.count(old) == 1, 'the JE arm of Cond found %d times -- update this mutation' % s.count(old)
  2363. open(p, 'w').write(s.replace(old, new))
  2364. PYX
  2365. then
  2366. if $GM2 -fiso -c Exec86.mod >/dev/null 2>&1; then
  2367. expect_red "the interpreter's own oracle catches JE inverted" \
  2368. "t33_cmpops" python3 tests/run_exec86.py t33_cmpops
  2369. else
  2370. echo " FAIL: Exec86.mod would not compile with JE inverted"
  2371. fail=$((fail + 1))
  2372. fi
  2373. else
  2374. echo " BROKEN CASE: the Cond JE mutation did not apply"
  2375. fail=$((fail + 1))
  2376. fi
  2377. cp "$SAVED_E" Exec86.mod
  2378. # And the check on the RESTORED interpreter, because a green above could also
  2379. # come from a mutation that never took and an object left mutated by a run
  2380. # that died in between.
  2381. if $GM2 -fiso -c Exec86.mod >/dev/null 2>&1; then
  2382. if python3 tests/run_exec86.py >/dev/null 2>&1; then
  2383. echo " ok: run_exec86 green on the restored interpreter"
  2384. pass=$((pass + 1))
  2385. else
  2386. echo "NOT RESTORED: run_exec86.py is red after restoring Exec86.mod"
  2387. python3 tests/run_exec86.py 2>&1 | grep -i "fail" | head -3 | sed 's/^/ /'
  2388. fail=$((fail + 1))
  2389. fi
  2390. else
  2391. echo "NOT RESTORED: Exec86.mod would not recompile"
  2392. fail=$((fail + 1))
  2393. fi
  2394. echo
  2395. echo "== the R key: compile, poke, run, report (runtest.py)"
  2396. # The only check in the project that exercises CmdRun. Everything above looks
  2397. # at bytes, or at what qemu says the image does; this one drives the shell
  2398. # through a pty the way a person would, presses R, and compares the GUEST's
  2399. # output against the fixture's hand-derived .out - so it fails on things no
  2400. # byte check can see and no file records either, because R writes no file at
  2401. # all (which is itself asserted).
  2402. #
  2403. # The mutation is the poke loop's count. With n = 0 nothing is copied into
  2404. # the interpreter, loadHi stays where Clear86 left it - 0100h - and Run86
  2405. # faults on its very first step, "execution left the loaded image", before the
  2406. # guest has executed a single instruction. That is fast and deterministic, which
  2407. # matters: the obvious alternative (poking the image somewhere else) leaves the
  2408. # machine executing zeros and buys a step-limit timeout instead of a finding.
  2409. cp "$SAVED_SH" Shell.mod
  2410. if python3 - <<'PYX'
  2411. p = 'Shell.mod'
  2412. s = open(p).read()
  2413. old = " n := LinkSize () ;"
  2414. new = " n := 0 ; (* MUTATION: nothing is poked *)"
  2415. assert s.count(old) == 1, 'the poke count in CmdRun found %d times -- update this mutation' % s.count(old)
  2416. open(p, 'w').write(s.replace(old, new))
  2417. PYX
  2418. then
  2419. if make > ../tmp/nonvacuity.make.log 2>&1; then
  2420. expect_red "the R check catches an image that was never poked" \
  2421. "AH=4Ch exit" python3 tests/runtest.py
  2422. else
  2423. echo " FAIL: the shell would not rebuild with the poke loop neutered"
  2424. tail -5 ../tmp/nonvacuity.make.log | sed 's/^/ /'
  2425. fail=$((fail + 1))
  2426. fi
  2427. else
  2428. echo " BROKEN CASE: the CmdRun poke mutation did not apply"
  2429. fail=$((fail + 1))
  2430. fi
  2431. cp "$SAVED_SH" Shell.mod
  2432. if make > ../tmp/nonvacuity.make.log 2>&1; then
  2433. if python3 tests/runtest.py >/dev/null 2>&1; then
  2434. echo " ok: runtest green on the restored shell"
  2435. pass=$((pass + 1))
  2436. else
  2437. echo "NOT RESTORED: runtest.py is red after restoring Shell.mod"
  2438. python3 tests/runtest.py 2>&1 | grep -i "fail" | head -3 | sed 's/^/ /'
  2439. fail=$((fail + 1))
  2440. fi
  2441. else
  2442. echo "NOT RESTORED: the shell would not rebuild"
  2443. tail -5 ../tmp/nonvacuity.make.log | sed 's/^/ /'
  2444. fail=$((fail + 1))
  2445. fi
  2446. echo
  2447. echo "== 8086 legality of the conditional lowering (check_8086.py)"
  2448. # This whole section exists because of a fault that every other check in the
  2449. # project was blind to, and being blunt about WHY is the point of listing it.
  2450. #
  2451. # EmJcc and EmSetcc emitted `0F 8x rel16' and `0F 9x rel8'. `0F' is a
  2452. # 386-and-later opcode prefix; the 8086 has none. So EVERY conditional branch
  2453. # and EVERY comparison in EVERY compiled program was an illegal instruction on
  2454. # the machine this compiler targets. And all of the following were green while
  2455. # it was: the compile matrix, the .COM layout checker, the runtime golden, the
  2456. # emitter audit, and the whole execution suite answering correctly under qemu.
  2457. #
  2458. # Two reasons, and the second is the one worth keeping:
  2459. # 1. qemu-system-i386 has no 8086 model. Its lowest is 486, where `0F 84' is
  2460. # a perfectly ordinary JZ. So the execution oracle CANNOT see this class
  2461. # of fault, ever.
  2462. # 2. FCML is this project's INDEPENDENT disassembler, and FCML's -m16 mode is
  2463. # a 386. The one tool whose job is to say "this is not a real instruction"
  2464. # was architecturally guaranteed to agree with the bug.
  2465. #
  2466. # So a check was needed that asks the question nothing else was asking. The
  2467. # five mutations below are the mutations that check has to catch, and each was
  2468. # run by hand and confirmed red BEFORE this section existed.
  2469. #
  2470. # M4 and M5 are the interesting pair. M4 is a fault this project actually
  2471. # introduced while fixing the above: EmJcc jumps TO its target, but the 8086
  2472. # shape steps OVER a 3-byte EJMP, so the naive port inverts every conditional
  2473. # in every program. It was caught by execution, not by any byte check. M5 is
  2474. # the partial version -- the inversion dropped for the IF and CASE sites only,
  2475. # kept for FOR -- and MEASURING that is what produced clause H, because the
  2476. # check was blind to M5 as first written: IF declares nibble 4, CASE declares
  2477. # 5, they negate into each other, and both are declared, so nothing complained
  2478. # while every conditional in every program took the wrong path. FOR declares
  2479. # C and F, whose negations D and E are declared for nothing at all, so the
  2480. # whole-suite version was caught by luck.
  2481. #
  2482. # Note these need `rebuild_compiler' only to refresh comtest; check_8086.py
  2483. # relinks the fixtures itself. Getting that wrong is how this section's first
  2484. # draft reported a mutation as VACUOUS: the build was skipped, a stale comtest
  2485. # ran the GOOD compiler, and the check passed. A helper that exits non-zero
  2486. # makes `if mutate_x; then' false and the case is silently skipped, which looks
  2487. # exactly like a pass -- so rebuild_compiler is checked, not assumed.
  2488. # M1: the original defect, restored verbatim.
  2489. restore_compiler
  2490. if python3 - <<'PYX'
  2491. p = 'CompilerEmit.mod'
  2492. s = open(p).read()
  2493. old = """ Ebyte (JccShortInv (cc)) ; (* Jcc_s, taken when cc does NOT hold *)
  2494. Ebyte (03H) ; (* rel8: step over the 3-byte EJMP *)
  2495. RETURN EmJmpNear (target) (* target = 0 => forward, see above *)"""
  2496. new = """ Ebyte (0FH) ; Ebyte (cc) ; Eword (0) ;
  2497. IF target = 0 THEN
  2498. RETURN nPatch
  2499. END ;
  2500. RETURN 0"""
  2501. assert s.count(old) == 1, 'EmJcc body found %d times -- update this mutation' % s.count(old)
  2502. open(p, 'w').write(s.replace(old, new))
  2503. PYX
  2504. then
  2505. if rebuild_compiler; then
  2506. expect_red "check_8086 catches a 0F 8x branch (the original bug)" \
  2507. "0F 84" python3 tests/check_8086.py
  2508. else
  2509. echo " FAIL: the compiler would not rebuild with the 0F branch"
  2510. fail=$((fail + 1))
  2511. fi
  2512. else
  2513. echo " BROKEN CASE: the 0F branch mutation did not apply"
  2514. fail=$((fail + 1))
  2515. fi
  2516. restore_compiler
  2517. # M2: the other original defect, SETcc plus the MOV AH,0 it needed.
  2518. restore_compiler
  2519. if python3 - <<'PYX'
  2520. p = 'CompilerEmit.mod'
  2521. s = open(p).read()
  2522. old = """ Ebyte (0B8H) ; Eword (1) ; (* MOV AX,#0001 *)
  2523. Ebyte (JccShort (cc)) ; (* taken when the comparison HOLDS *)
  2524. Ebyte (01H) ; (* rel8: step over the DEC AX *)
  2525. Ebyte (48H) (* DEC AX *)"""
  2526. new = """ Ebyte (0FH) ; Ebyte (cc) ; Ebyte (0C0H) ;
  2527. EmMovAh0 ()"""
  2528. assert s.count(old) == 1, 'EmSetcc body found %d times -- update this mutation' % s.count(old)
  2529. open(p, 'w').write(s.replace(old, new))
  2530. PYX
  2531. then
  2532. if rebuild_compiler; then
  2533. expect_red "check_8086 catches a 0F 9x SETcc (the original bug)" \
  2534. "0F 9F C0" python3 tests/check_8086.py
  2535. else
  2536. echo " FAIL: the compiler would not rebuild with the 0F SETcc"
  2537. fail=$((fail + 1))
  2538. fi
  2539. else
  2540. echo " BROKEN CASE: the 0F SETcc mutation did not apply"
  2541. fail=$((fail + 1))
  2542. fi
  2543. restore_compiler
  2544. # M4: the polarity inversion, everywhere. Note the expected text is clause H's,
  2545. # not a shape complaint: every byte here is a legal 8086 shape, which is the
  2546. # entire reason a separate clause had to be written.
  2547. restore_compiler
  2548. if python3 - <<'PYX'
  2549. p = 'CompilerEmit.mod'
  2550. s = open(p).read()
  2551. old = ' Ebyte (JccShortInv (cc)) ; (* Jcc_s, taken when cc does NOT hold *)'
  2552. new = ' Ebyte (JccShort (cc)) ; (* MUTATION: inversion dropped *)'
  2553. assert s.count(old) == 1, 'EmJcc Ebyte found %d times -- update this mutation' % s.count(old)
  2554. open(p, 'w').write(s.replace(old, new))
  2555. PYX
  2556. then
  2557. if rebuild_compiler; then
  2558. expect_red "clause H catches the branch polarity inverted everywhere" \
  2559. "but reading the source says" python3 tests/check_8086.py
  2560. else
  2561. echo " FAIL: the compiler would not rebuild with the inversion dropped"
  2562. fail=$((fail + 1))
  2563. fi
  2564. else
  2565. echo " BROKEN CASE: the polarity mutation did not apply"
  2566. fail=$((fail + 1))
  2567. fi
  2568. restore_compiler
  2569. # M5: the same inversion dropped for the IF and CASE sites ONLY. This one is
  2570. # the reason clause H exists: it was run by hand and the check stayed GREEN,
  2571. # and FOR's C/F nibbles were the only reason the whole-suite version (M4)
  2572. # happened to be caught.
  2573. restore_compiler
  2574. if python3 - <<'PYX'
  2575. p = 'CompilerEmit.mod'
  2576. s = open(p).read()
  2577. old = ' Ebyte (JccShortInv (cc)) ; (* Jcc_s, taken when cc does NOT hold *)'
  2578. new = """ IF (cc = 84H) OR (cc = 85H) THEN
  2579. Ebyte (JccShort (cc)) (* MUTATION: no inversion here *)
  2580. ELSE
  2581. Ebyte (JccShortInv (cc))
  2582. END ;"""
  2583. assert s.count(old) == 1, 'EmJcc Ebyte found %d times -- update this mutation' % s.count(old)
  2584. open(p, 'w').write(s.replace(old, new))
  2585. PYX
  2586. then
  2587. if rebuild_compiler; then
  2588. expect_red "clause H catches the inversion dropped for IF and CASE only" \
  2589. "t22_case" python3 tests/check_8086.py
  2590. else
  2591. echo " FAIL: the compiler would not rebuild with the partial inversion"
  2592. fail=$((fail + 1))
  2593. fi
  2594. else
  2595. echo " BROKEN CASE: the partial-inversion mutation did not apply"
  2596. fail=$((fail + 1))
  2597. fi
  2598. restore_compiler
  2599. # And the check on the RESTORED compiler, so a green above cannot come from a
  2600. # mutation that failed to take and left the real defect in place.
  2601. if rebuild_compiler; then
  2602. if python3 tests/check_8086.py >/dev/null 2>&1; then
  2603. echo " ok: check_8086 green on the restored compiler"
  2604. pass=$((pass + 1))
  2605. else
  2606. echo "NOT RESTORED: check_8086.py is red after restoring Compiler.mod"
  2607. python3 tests/check_8086.py 2>&1 | grep -m3 -- ' - ' | sed 's/^/ /'
  2608. fail=$((fail + 1))
  2609. fi
  2610. else
  2611. echo "NOT RESTORED: the compiler would not rebuild"
  2612. fail=$((fail + 1))
  2613. fi
  2614. echo
  2615. echo "== the emitter-name audit of Compiler.mod (audit_helpers.py)"
  2616. # These need no rebuild: the audit reads the SOURCE, not the built object, so
  2617. # they are the cheapest cases here and they cover the module the audit used
  2618. # not to look at at all. That is the point of the section: the audit reported
  2619. # "every helper agrees with its name" for a module it had never examined, and
  2620. # EmXchgAxCx was `93` (XCHG BX,AX) under a name that says XCHG AX,CX for the
  2621. # whole life of the project. Two of these five are for faults that were real.
  2622. SAVED_C2=../tmp/nonvacuity.Compiler.mod.2
  2623. SAVED_CE2=../tmp/nonvacuity.CompilerEmit.mod.2
  2624. SAVED_R2=../tmp/nonvacuity.Runtime.mod.2
  2625. cp Compiler.mod "$SAVED_C2" || exit 1
  2626. cp CompilerEmit.mod "$SAVED_CE2" || exit 1
  2627. cp Runtime.mod "$SAVED_R2" || exit 1
  2628. restore_audit_sources () {
  2629. cp "$SAVED_C2" Compiler.mod
  2630. cp "$SAVED_CE2" CompilerEmit.mod
  2631. cp "$SAVED_R2" Runtime.mod
  2632. }
  2633. AUD="python3 tests/audit_helpers.py"
  2634. # 1. THE fault. 91h is XCHG AX,CX; 93h is XCHG BX,AX. Both are one byte, so
  2635. # the compile matrix never moved and the byte counts never moved.
  2636. cp "$SAVED_C2" Compiler.mod
  2637. mutate CompilerEmit.mod 's|^ Ebyte (91H)$| Ebyte (93H)|'
  2638. expect_red "audit catches XchgAxCx emitting XCHG BX,AX" \
  2639. "exchanges Ax and Bx" $AUD
  2640. restore_audit_sources
  2641. # 2. the coverage check itself. A parameter list that find_helpers does not
  2642. # accept is exactly how the real emitter was missed, and the inventory is
  2643. # scanned separately on purpose so this can be caught. Without the
  2644. # independent scan this case is silent, because both lists would come from
  2645. # the same parser and agree that the helper does not exist.
  2646. cp "$SAVED_C2" Compiler.mod
  2647. mutate CompilerEmit.mod 's|^PROCEDURE EmXchgAxCx () ;$|PROCEDURE EmXchgAxCx (why : CARDINAL) ;|'
  2648. expect_red "audit reports an emitter it cannot reach, rather than skipping it" \
  2649. "never examined it" $AUD
  2650. restore_audit_sources
  2651. # 3. EmXchgAxDx was named EmMoveAxDx, which said MOV where the bytes say XCHG.
  2652. # 93h here is XCHG AX,BX - one letter away, the exact class of mistake the
  2653. # name is supposed to make impossible.
  2654. cp "$SAVED_C2" Compiler.mod
  2655. mutate Compiler.mod 's|^ Ebyte (92H)$| Ebyte (93H)|'
  2656. expect_red "audit catches XchgAxDx emitting XCHG BX,AX" \
  2657. "XchgAxDx" $AUD
  2658. restore_audit_sources
  2659. # 4. CmpArgW0's [BP+2] written as the 386 SIB form, which decodes on a 8086 as
  2660. # [SI+24h]. A real bug: the runtime was clearing the wrong memory.
  2661. cp "$SAVED_R2" Runtime.mod
  2662. mutate Runtime.mod 's| B (83H) ; B (7EH) ; B (2) ; B (0) ;| B (83H) ; B (7CH) ; B (24) ; B (0) ; B (0) ;|'
  2663. expect_red "audit catches the [SI+24h] encoding of [BP+2]" \
  2664. "memory base is 'si" $AUD
  2665. restore_audit_sources
  2666. # 5. MovAxSp is POP then PUSH, because MOV AX,[SP] does not exist on an 8086.
  2667. # Dropping the POP leaves the stack one word short - a fault in the shape,
  2668. # not in a byte value.
  2669. cp "$SAVED_CE2" CompilerEmit.mod
  2670. python3 - <<'PYEOF'
  2671. p='CompilerEmit.mod'; s=open(p).read()
  2672. a=" Ebyte (58H) ; (* POP AX *)\n"
  2673. assert s.count(a)==1, "EmMovAxSp POP line not found -- update this mutation"
  2674. open(p,'w').write(s.replace(a, ""))
  2675. PYEOF
  2676. expect_red "audit catches MovAxSp with its POP missing" \
  2677. "MovAxSp" $AUD
  2678. restore_audit_sources
  2679. # 6. The two-instruction shape: IDIV is CWD then IDIV, and dropping the CWD
  2680. # leaves an un-sign-extended dividend in DX:AX. Both are still present as
  2681. # a two-step spec, so a missing step has to be visible.
  2682. cp "$SAVED_C2" Compiler.mod
  2683. mutate CompilerEmit.mod 's| Ebyte (99H) ; Ebyte (0F7H) ; Ebyte (0F9H)| Ebyte (0F7H) ; Ebyte (0F9H)|'
  2684. expect_red "audit catches IDiv without the CWD that extends the dividend" \
  2685. "IDivAxCx" $AUD
  2686. restore_audit_sources
  2687. # 7. The byte under EmXorAl01's name, and the row in PATTERNS that was added
  2688. # to admit it. `34 01' is XOR AL,#01 - the boolean NOT, and the one emitter
  2689. # in Compiler.mod with no ModRM byte at all. A grammar row nobody has ever
  2690. # seen reject anything cannot be trusted to accept only the truth, so here
  2691. # is the rejection: `34 02' is the same length, decodes just as cleanly, and
  2692. # `not x' would flip bit 1 instead of bit 0.
  2693. cp "$SAVED_C2" Compiler.mod
  2694. mutate CompilerEmit.mod 's|^ Ebyte (34H) ; Ebyte (01H)| Ebyte (34H) ; Ebyte (02H)|'
  2695. expect_red "audit catches EmXorAl01 emitting xor al,#2 under a name saying #1" \
  2696. "XorAl01" $AUD
  2697. restore_audit_sources
  2698. # 7b. The other half of that row: the opcode gate. 34h is XOR AL,#imm and
  2699. # 35h is XOR AX,#imm - a word, not a byte, under a name that says AL. The
  2700. # row is pinned to {034h} on purpose, and a pin that has never been asked
  2701. # to hold is a pin. The report is the one thing the row above cannot
  2702. # produce: with no reading at all, the helper falls out of the grammar.
  2703. cp "$SAVED_C2" Compiler.mod
  2704. mutate CompilerEmit.mod 's|^ Ebyte (34H) ; Ebyte (01H)| Ebyte (35H) ; Ebyte (01H)|'
  2705. expect_red "the opcode gate rejects XOR AX under an XOR AL name" \
  2706. "no name pattern accepts it" $AUD
  2707. restore_audit_sources
  2708. if $AUD >/dev/null 2>&1; then
  2709. echo " ok: the audit passes on both restored sources"
  2710. pass=$((pass + 1))
  2711. else
  2712. echo "NOT RESTORED: the audit is red after restoring the sources"
  2713. $AUD 2>&1 | sed 's/^/ /'
  2714. fail=$((fail + 1))
  2715. fi
  2716. echo
  2717. echo "== the BP contract rt_exec.py checks before it starts a machine"
  2718. # wrchar and wrbool both borrowed BP to reach their argument -- [SP] is not
  2719. # encodable in 16-bit mode -- and neither saved it. The driver's cursor into
  2720. # the case record lives in BP precisely because BP is the one register an entry
  2721. # may keep, so "wrchar borrowed it and did not give it back" sent the second
  2722. # call to a garbage address, the machine triple-faulted, and the run printed the
  2723. # record header twice and hung. Both the golden and the audit call that shape
  2724. # CORRECT: the bytes are well formed, every branch is on a boundary, the size
  2725. # is unchanged, and the decode says exactly what it says. So the rule is now
  2726. # checked directly, and these two cases are what make that check more than a
  2727. # claim.
  2728. #
  2729. # rt_exec.py needs the whole runtime rebuilt and then boots 36 machines, so this
  2730. # section is the slow one. The baseline comes first and is asserted: a case
  2731. # that mutates a red tree proves nothing.
  2732. SAVED_R3=../tmp/nonvacuity.Runtime.mod.3
  2733. cp Runtime.mod "$SAVED_R3" || exit 1
  2734. if rebuild; then
  2735. if python3 tests/rt_exec.py >/dev/null 2>&1; then
  2736. echo " ok: baseline - rt_exec.py passes on the unmutated runtime"
  2737. pass=$((pass + 1))
  2738. else
  2739. echo " FAIL: the baseline is already red, so the cases below prove"
  2740. echo " nothing - fix the baseline before reading them"
  2741. python3 tests/rt_exec.py 2>&1 | tail -3 | sed 's/^/ /'
  2742. fail=$((fail + 1))
  2743. fi
  2744. else
  2745. echo " FAIL: could not rebuild the runtime for the baseline"
  2746. fail=$((fail + 1))
  2747. fi
  2748. # 1. THE fault: drop the PUSH, exactly as EmitWrChar was written. This is the
  2749. # shape the behavioural case found, so the byte-level check must find it too
  2750. # -- a check that only the expensive test can trip is a check that has not
  2751. # been made to earn its place.
  2752. cp "$SAVED_R3" Runtime.mod
  2753. python3 - <<'PYEOF'
  2754. p = 'Runtime.mod'
  2755. s = open(p).read()
  2756. a = ' M ("wrchar") ;\n PushBp ; MovBpSp ;\n'
  2757. assert s.count(a) == 1, "EmitWrChar prologue not found exactly once"
  2758. open(p, 'w').write(s.replace(a, ' M ("wrchar") ;\n MovBpSp ;\n'))
  2759. PYEOF
  2760. if rebuild; then
  2761. expect_red "the BP contract catches wrchar borrowing BP unsaved" \
  2762. "borrows BP but does not open with" python3 tests/rt_exec.py
  2763. else
  2764. echo " FAIL: the runtime would not rebuild with the PUSH removed"
  2765. fail=$((fail + 1))
  2766. fi
  2767. cp "$SAVED_R3" Runtime.mod
  2768. # 2. The mirror image: push it and never pop it. A different one-instruction
  2769. # omission with the same consequence for a caller, and the reason the rule
  2770. # asks for the 5D and not just the 55.
  2771. cp "$SAVED_R3" Runtime.mod
  2772. mutate Runtime.mod 's|^ MovSpBp ; PopBp ;$| MovSpBp ;|'
  2773. if rebuild; then
  2774. expect_red "the BP contract catches a BP that is pushed and never popped" \
  2775. "pushes BP but never pops it" python3 tests/rt_exec.py
  2776. else
  2777. echo " FAIL: the runtime would not rebuild with the POP removed"
  2778. fail=$((fail + 1))
  2779. fi
  2780. cp "$SAVED_R3" Runtime.mod
  2781. # 3. The scan's other silent failure: a rule that matches nothing looks exactly
  2782. # like a rule that passes. The pattern the check looks for is changed to one
  2783. # the blob does not contain -- MOV BP,DI, which the runtime has no reason to
  2784. # emit -- so the check has examined thirteen entries and matched nothing and
  2785. # MUST say so rather than report a clean sweep. This is the general shape of
  2786. # the fault this project keeps making: a check whose SUBJECT has drifted
  2787. # reports a confident answer about the wrong thing.
  2788. cp "$SAVED_R3" Runtime.mod
  2789. SAVED_X=../tmp/nonvacuity.rt_exec.py
  2790. cp tests/rt_exec.py "$SAVED_X" || exit 1
  2791. mutate tests/rt_exec.py 's|^MOV_BP_SP = b"\\x8b\\xec" .*$|MOV_BP_SP = b"\\x8b\\xed" # MOV BP,DI: never emitted|' \
  2792. expect_red "a check that matched nothing is a failure, not a pass" \
  2793. "matched nothing" python3 tests/rt_exec.py
  2794. cp "$SAVED_X" tests/rt_exec.py
  2795. if rebuild; then
  2796. if python3 tests/rt_exec.py >/dev/null 2>&1; then
  2797. echo " ok: rt_exec.py passes on the restored source"
  2798. pass=$((pass + 1))
  2799. else
  2800. echo "NOT RESTORED: rt_exec.py is red after restoring Runtime.mod"
  2801. python3 tests/rt_exec.py 2>&1 | tail -3 | sed 's/^/ /'
  2802. fail=$((fail + 1))
  2803. fi
  2804. else
  2805. echo "NOT RESTORED: the runtime would not rebuild"
  2806. fail=$((fail + 1))
  2807. fi
  2808. # 4. The GOLDEN and the entry goldens, which are the other half of the same
  2809. # rule. rt_exec.py states the contract; check_runtime.py pins the bytes.
  2810. # They are separate mechanisms and the case below is what shows the golden
  2811. # one works on its own -- a re-baseline of runtime.golden would otherwise
  2812. # have absorbed the new prologues silently, and the next person to bless it
  2813. # would have no way to know the PUSH and POP were ever missing.
  2814. cp "$SAVED_R3" Runtime.mod
  2815. python3 - <<'PYEOF'
  2816. p = 'Runtime.mod'
  2817. s = open(p).read()
  2818. a = ' M ("wrchar") ;\n PushBp ; MovBpSp ;\n MovAlArg4 ;\n MovSpBp ; PopBp ;\n'
  2819. assert s.count(a) == 1, "EmitWrChar frame not found exactly once"
  2820. open(p, 'w').write(s.replace(a, ' M ("wrchar") ;\n MovBpSp ;\n MovAlArg2 ;\n MovSpBp ;\n'))
  2821. PYEOF
  2822. if rebuild; then
  2823. expect_red "the entry golden catches wrchar without its PUSH BP" \
  2824. "entry wrchar starts 8B EC" python3 tests/check_runtime.py "$DUMP"
  2825. else
  2826. echo " FAIL: the runtime would not rebuild with wrchar's frame removed"
  2827. fail=$((fail + 1))
  2828. fi
  2829. cp "$SAVED_R3" Runtime.mod
  2830. echo
  2831. echo "== the .COM layout check, and the runtime size it now measures"
  2832. # The checker used to RESTATE the runtime's size as a literal. It had drifted
  2833. # from the runtime it described, so the header was read out of the code stream
  2834. # and EVERY linked fixture "failed" on it - a whole file of "failures" that were
  2835. # not findings at all. A duplicated constant that has drifted does not fail
  2836. # loudly; it re-reports the same falsehood, in which the real failures hide.
  2837. # The size is now MEASURED from the image.
  2838. #
  2839. # These cases corrupt a real emitted .COM and require the checker to notice.
  2840. # They need the images, so they are built once and copied; the checker has a
  2841. # --check-only mode for exactly this, because its scratch directory is normally
  2842. # deleted on exit and a check that has only ever seen the truth is not a check.
  2843. KEEPDIR=../tmp/nonvacuity.com
  2844. rm -rf "$KEEPDIR"
  2845. TP_COM_KEEP=1 tests/run_com_tests.sh >../tmp/nonvacuity.com.log 2>&1
  2846. KEEP=$(sed -n 's/^TP_COM_KEEP=1: images left in //p' \
  2847. ../tmp/nonvacuity.com.log | tail -1)
  2848. if [ -z "$KEEP" ] || [ ! -d "$KEEP" ]; then
  2849. echo " FAIL: could not obtain emitted .COM images for the layout cases"
  2850. fail=$((fail + 1))
  2851. else
  2852. COMCHK="tests/run_com_tests.sh --check-only"
  2853. # 0. The baseline. Every case below is a claim that a specific assertion
  2854. # turns red, and none of them means anything if the copies of untouched
  2855. # images already fail. (The stale RT_SZ produced exactly that: a whole
  2856. # file of "failures" that were not findings.) So this is asserted first,
  2857. # and a failure here is reported as a broken baseline rather than a red
  2858. # test.
  2859. #
  2860. # The baseline is also WHERE THE HEADER OFFSET COMES FROM. Cases 1 and 2
  2861. # used to carry it as the literal 435 and 439, which were ENT_SZ + the
  2862. # runtime size at the time -- 432. Two 4-byte changes to the runtime
  2863. # later, both cases were editing the wrong bytes: 435 had become four
  2864. # bytes inside the runtime itself, so case 1 had stopped testing "the
  2865. # header cannot be found" and had started testing "the checker also
  2866. # notices you scribbled on the code", and case 2 had become a no-op
  2867. # that wrote the header where it already was. Both still went red, for
  2868. # reasons the messages did not name, which is the whole problem: a
  2869. # hard-coded offset is a claim about the world that expires silently.
  2870. #
  2871. # So the offset is read back out of the checker's own report on the
  2872. # untouched images, and the expected numbers below are ARITHMETIC ON IT.
  2873. # If the runtime grows again, these cases still test what they say.
  2874. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  2875. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  2876. BASE_OUT=$($COMCHK "$KEEPDIR" 2>&1)
  2877. if [ $? -eq 0 ]; then
  2878. echo " ok: baseline - untouched copies of the real images all pass"
  2879. pass=$((pass + 1))
  2880. else
  2881. echo " FAIL: the baseline is already red, so the cases below prove"
  2882. echo " nothing - fix the baseline before reading them"
  2883. printf '%s\n' "$BASE_OUT" | grep FAIL | head -3 | sed 's/^/ /'
  2884. fail=$((fail + 1))
  2885. fi
  2886. BASE_RT=$(printf '%s\n' "$BASE_OUT" \
  2887. | sed -n 's/.*measured runtime size: \([0-9][0-9]*\) bytes.*/\1/p' \
  2888. | head -1)
  2889. if [ -z "$BASE_RT" ]; then
  2890. echo " FAIL: the checker did not report the runtime size it measured,"
  2891. echo " so the cases below cannot find the header to edit"
  2892. fail=$((fail + 1))
  2893. BASE_RT=0
  2894. fi
  2895. # The layout constants are ENT_SZ 3 and HDR_SZ 16 (tests/comimage.py), so the
  2896. # header sits at hdrOff = 3 + rtSz and the entry jump's displacement must be
  2897. # (hdrOff + 16) - 3. The wanted number is therefore computed from the header
  2898. # OFFSET, not from rtSz: the two differ by 3, and getting that backwards
  2899. # produces a plausible-looking expectation three bytes out, which is how
  2900. # this case came to expect "want 452" and then be reported as not proving
  2901. # what it said.
  2902. BASE_OFF=$((3 + BASE_RT))
  2903. # Case 2 claims a runtime four bytes LONGER, so the header - and with it the
  2904. # demanded jump target - moves four bytes further on.
  2905. SHIFTED_WANT=$((BASE_OFF + 4 + 16 - 3))
  2906. echo " (measured runtime size $BASE_RT, header at image offset $BASE_OFF)"
  2907. # 1. Break hdrDS so it no longer ties the header to its own offset. The
  2908. # header must become UNFINDABLE and be reported as such - a checker that
  2909. # fell back to a remembered offset would report a confident number here,
  2910. # which is the failure mode the measurement was introduced to remove.
  2911. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  2912. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  2913. python3 - "$KEEPDIR/t01_minimal.COM" "$BASE_OFF" <<'PYEOF'
  2914. import sys
  2915. p, off = sys.argv[1], int(sys.argv[2])
  2916. d = bytearray(open(p, 'rb').read())
  2917. d[off + 4:off + 6] = (0x1234).to_bytes(2, 'little') # hdrDS, no longer self-consistent
  2918. open(p, 'wb').write(bytes(d))
  2919. PYEOF
  2920. expect_red "a header that cannot be located is reported, not assumed" \
  2921. "no program header found" $COMCHK "$KEEPDIR"
  2922. # 2. The entry jump is now the locator: it names the first instruction,
  2923. # and the header sits HDR_SZ bytes before it. Moving the jump four
  2924. # bytes off means no header is named, and the check must say so rather
  2925. # than fall back on a remembered runtime size.
  2926. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  2927. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  2928. python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
  2929. import sys
  2930. p = sys.argv[1]
  2931. d = bytearray(open(p, 'rb').read())
  2932. rel = int.from_bytes(d[1:3], 'little')
  2933. d[1:3] = ((rel + 4) & 0xFFFF).to_bytes(2, 'little')
  2934. open(p, 'wb').write(bytes(d))
  2935. PYEOF
  2936. expect_red "the entry jump must name the header it follows" \
  2937. "no program header found" $COMCHK "$KEEPDIR"
  2938. # 3. The entry jump's opcode. One byte, and the only assertion in the
  2939. # project that can see where execution STARTS.
  2940. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  2941. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  2942. python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
  2943. import sys
  2944. p = sys.argv[1]
  2945. d = bytearray(open(p, 'rb').read())
  2946. d[0] = 0xEA
  2947. open(p, 'wb').write(bytes(d))
  2948. PYEOF
  2949. expect_red "the entry jump must be E9, not a near JMP" \
  2950. "no program header found" $COMCHK "$KEEPDIR"
  2951. # 4. The entry jump's target, moved one instruction earlier. A .COM that
  2952. # lands in the middle of the prologue runs, prints something and exits
  2953. # cleanly, so no size or structure check can see this.
  2954. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  2955. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  2956. python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
  2957. import sys
  2958. p = sys.argv[1]
  2959. d = bytearray(open(p, 'rb').read())
  2960. d[1:3] = (100).to_bytes(2, 'little')
  2961. open(p, 'wb').write(bytes(d))
  2962. PYEOF
  2963. expect_red "the entry jump must land on the first instruction" \
  2964. "no program header found" $COMCHK "$KEEPDIR"
  2965. rm -rf "$KEEPDIR"
  2966. fi
  2967. echo
  2968. echo "non-vacuity: $pass ok, $fail failed"
  2969. [ "$fail" -eq 0 ]