Runtime.mod 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180
  1. IMPLEMENTATION MODULE Runtime ;
  2. (* 8086 runtime library, assembled byte by byte. Each little emitter below is
  3. one 8086 instruction; the ModR/M byte is spelled out in the comment so the
  4. encoding can be checked by hand against an 8086 table. This is the same
  5. approach the compiler itself takes in Compiler.mod (Ebyte/Eword/EmCall),
  6. so the library needs no external assembler and no binary artifact in the
  7. tree - RT_Build assembles it every run, which is why the entry offsets are
  8. known exactly and are not guesses.
  9. Memory model: a .COM image, so CS = DS = ES = SS = 0 and the whole thing
  10. lives in one 64K segment. The runtime occupies image offsets
  11. RT_Build's base .. base + RT_Size - 1, the generated program follows, and
  12. the program's globals follow at that + 1000H.
  13. The base is a parameter rather than an assumption, and that is not
  14. decoration. The image begins with a three-byte JMP - a .COM is entered at
  15. file offset 0, and without it a .COM built by this compiler starts by
  16. executing initmem - so the runtime does NOT sit at image offset 0. Every
  17. address the runtime bakes into its own code (its data block, and the entry
  18. offsets it hands back) therefore has to be biased by that base. Two places
  19. do it, and only two: FixUp for the data block, and RT_Entry for the
  20. entries. The emitted BYTES are identical for any base, because both are
  21. computed after assembly; tests/check_runtime.py and tests/runtime.golden
  22. pass base 0 and are unaffected.
  23. Calling conventions, matching Compiler.IoCall:
  24. WrInt/WrChar/WrBool/WrReal one 16-bit value on the stack (caller pops)
  25. RdInt/RdChar/RdBool one address on the stack (caller pops)
  26. WrLn/RdLn/StackChk nothing
  27. InitMem AX = offset of the program header word block
  28. ProgEnd/Halt nothing; exits with code 0
  29. All entries preserve BP and SP, and every register except the documented
  30. result, so they can be called from the middle of an expression. *)
  31. FROM SYSTEM IMPORT BYTE ;
  32. CONST
  33. MaxRt = 4096 ;
  34. MaxLbl = 64 ;
  35. MaxFix = 400 ;
  36. MaxNm = 15 ;
  37. (* Offsets inside the runtime's own data block. These must agree with the
  38. bytes EmitData emits, and EmitData ASSERTS that they do - because the
  39. only other way to find out is to run a program, and a D_ constant that
  40. is one too high does not crash: the $ that terminates the string it
  41. points at is simply somewhere else, and a $-string read from the wrong
  42. address runs on until it happens to find a 24h. wrln did exactly that
  43. and printed a screenful of memory, which is what found this. D_TRUE was
  44. the only one that was right, and the golden (instructions only) could
  45. not see any of it. *)
  46. D_NUM = 0 ; (* 8 bytes, decimal conversion scratch *)
  47. D_TRUE = 8 ; (* "TRUE$" - 5 bytes, 8..12 *)
  48. D_FALSE = 13 ; (* "FALSE$" - 6 bytes, 13..18 *)
  49. D_CRLF = 19 ; (* CR LF '$' - 3 bytes, 19..21 *)
  50. D_REAL = 22 ; (* "?REAL?" - 6 bytes, 22..27; reals are not formatted yet *)
  51. D_PUSH = 28 ; (* 2 bytes, 28..29: the one-character pushback. The high
  52. byte is 1 whenever a character is pending, so the
  53. word is 0 exactly when the slot is empty - a NUL in
  54. the input would otherwise be indistinguishable from
  55. "nothing pushed back". Byte 30 is unused. *)
  56. D_END = 31 ; (* the block is padded to here *)
  57. (* THE LOAD BIAS. Everything above is an offset into the image as this
  58. compiler numbers it: the entry JMP is at image 0. But a DOS .COM is
  59. NOT loaded at that offset - DOS loads it at CS:0100, because 0000..00FF
  60. is the PSP - so image offset K lives at CS:(K + 0100h), and since a
  61. .COM has CS = DS, every address baked into the image as a literal has
  62. to carry that +0100h.
  63. Getting this wrong is the single most confusing failure this compiler
  64. can have, because nothing crashes. The entry JMP is *relative*, so it
  65. still lands on the prologue; every CALL is relative, so every call
  66. still lands on the right runtime entry; initmem still runs and still
  67. zeroes the data area. The program therefore starts, runs, and prints
  68. its first field correctly - and then a $ that is 0100h too low resolves
  69. to code instead of to data, so the string walk runs on through the
  70. runtime's own instructions until it happens to hit a 24h. That is
  71. exactly what writeln('hi') did: `hi' out of wrtin's inline data, then
  72. 249 bytes of runtime machine code, stopping only at the '$' inside
  73. "TRUE$". See tests/fixtures/t02_writeln.out.
  74. The bias is a CONSTANT, not a variable to be relocated at load time,
  75. because the 8086 has no way to relocode an image in place and no way
  76. to set a segment register to a sub-paragraph boundary. It is the same
  77. +0100h the original's own output has: TPSRC7 opendest copies the
  78. generated image to 100h and runs it there, so the original's addresses
  79. are 0100h-relative too. The difference is only in how the bytes are
  80. *numbered* - ours are 0-based in the file, the original's are
  81. segment-relative - and this constant is where that difference stops.
  82. The constant itself is declared in Runtime.def, because that is the only
  83. declaration site a compiler can import - an implementation module does not
  84. re-declare what its definition module already declared. *)
  85. TYPE
  86. LblRec = RECORD
  87. nm : ARRAY [0..MaxNm] OF CHAR ;
  88. off : CARDINAL ;
  89. END ;
  90. FixRec = RECORD
  91. kind : CARDINAL ; (* 0 = rel8, 1 = rel16, 2 = data address *)
  92. place : CARDINAL ; (* offset of the displacement/address field *)
  93. nm : ARRAY [0..MaxNm] OF CHAR ;
  94. val : CARDINAL ;
  95. END ;
  96. VAR
  97. rt : ARRAY [0..MaxRt - 1] OF BYTE ;
  98. rpos : CARDINAL ;
  99. lbl : ARRAY [0..MaxLbl - 1] OF LblRec ;
  100. ltop : CARDINAL ;
  101. fix : ARRAY [0..MaxFix - 1] OF FixRec ;
  102. nfix : CARDINAL ;
  103. dataAt : CARDINAL ;
  104. rtBase : CARDINAL ;
  105. built : BOOLEAN ;
  106. entNm : ARRAY [0..15] OF ARRAY [0..MaxNm] OF CHAR ;
  107. (* ---------------------------------------------------------------- *)
  108. (* name helpers *)
  109. (* ---------------------------------------------------------------- *)
  110. PROCEDURE StrEq (a, b : ARRAY OF CHAR ) : BOOLEAN ;
  111. VAR i : CARDINAL ;
  112. BEGIN
  113. i := 0 ;
  114. WHILE (i <= HIGH (a)) AND (i <= HIGH (b)) DO
  115. IF a [i] # b [i] THEN
  116. RETURN FALSE
  117. END ;
  118. INC (i)
  119. END ;
  120. RETURN TRUE
  121. END StrEq ;
  122. PROCEDURE SetStr (VAR dst : ARRAY OF CHAR ; src : ARRAY OF CHAR ) ;
  123. VAR i : CARDINAL ;
  124. BEGIN
  125. i := 0 ;
  126. WHILE (i <= HIGH (dst)) AND (i <= HIGH (src)) DO
  127. dst [i] := src [i] ;
  128. INC (i)
  129. END ;
  130. IF i <= HIGH (dst) THEN
  131. dst [i] := 0C
  132. END
  133. END SetStr ;
  134. (* ---------------------------------------------------------------- *)
  135. (* primitive emitters *)
  136. (* ---------------------------------------------------------------- *)
  137. PROCEDURE B (b : CARDINAL ) ;
  138. (* emit exactly ONE byte. A two-byte opcode must be written as two B calls -
  139. B masks to 100H, so B (8BE4H) would silently emit just E4. *)
  140. BEGIN
  141. IF rpos >= MaxRt THEN
  142. RETURN (* blob is oversized: drop the byte *)
  143. END ;
  144. rt [rpos] := VAL (BYTE, b MOD 100H) ;
  145. INC (rpos)
  146. END B ;
  147. PROCEDURE W (w : CARDINAL ) ;
  148. BEGIN
  149. B (w MOD 100H) ;
  150. B ((w DIV 100H) MOD 100H) (* little endian *)
  151. END W ;
  152. PROCEDURE M (nm : ARRAY OF CHAR ) ;
  153. (* mark: nm is the current offset *)
  154. BEGIN
  155. IF ltop < MaxLbl THEN
  156. SetStr (lbl [ltop].nm, nm) ;
  157. lbl [ltop].off := rpos ;
  158. INC (ltop)
  159. END
  160. END M ;
  161. PROCEDURE AddFix (kind, place : CARDINAL ; nm : ARRAY OF CHAR ; val : CARDINAL ) ;
  162. BEGIN
  163. IF nfix < MaxFix THEN
  164. fix [nfix].kind := kind ;
  165. fix [nfix].place := place ;
  166. fix [nfix].val := val ;
  167. SetStr (fix [nfix].nm, nm) ;
  168. INC (nfix)
  169. END
  170. END AddFix ;
  171. PROCEDURE LblOff (nm : ARRAY OF CHAR ) : CARDINAL ;
  172. VAR i : CARDINAL ;
  173. BEGIN
  174. i := 0 ;
  175. WHILE i < ltop DO
  176. IF StrEq (lbl [i].nm, nm) THEN
  177. RETURN lbl [i].off
  178. END ;
  179. INC (i)
  180. END ;
  181. RETURN 0
  182. END LblOff ;
  183. PROCEDURE J8 (nm : ARRAY OF CHAR ) ;
  184. BEGIN
  185. B (0EBH) ; (* JMP rel8 *)
  186. AddFix (0, rpos, nm, 0) ;
  187. B (0)
  188. END J8 ;
  189. PROCEDURE C8 (nm : ARRAY OF CHAR ) ;
  190. BEGIN
  191. B (0E8H) ; (* CALL rel16 *)
  192. AddFix (1, rpos, nm, 0) ;
  193. W (0)
  194. END C8 ;
  195. PROCEDURE Jcc (code : CARDINAL ; nm : ARRAY OF CHAR ) ;
  196. BEGIN
  197. B (code) ; (* Jcc rel8 *)
  198. AddFix (0, rpos, nm, 0) ;
  199. B (0)
  200. END Jcc ;
  201. PROCEDURE Dd (delta : CARDINAL ) ;
  202. (* emit a 16-bit address into the runtime's data block; the value is only
  203. known once the data block has been placed, so it is a fixup *)
  204. BEGIN
  205. AddFix (2, rpos, "", delta) ;
  206. W (0)
  207. END Dd ;
  208. (* --- 8-bit / 16-bit register and memory forms, one instruction each --- *)
  209. (* THE 16-BIT ModR/M EFFECTIVE-ADDRESS TABLE, MEASURED NOT REMEMBERED.
  210. Every address form below was confirmed by EXECUTING it on a real 8086
  211. (qemu-system-i386) with a probe that stores a marker through the candidate
  212. encoding and then reports which physical address received it; see
  213. tests/probe/modrm19.s. The mod=11 column is not an address at all -- it
  214. names a register -- so it is confirmed instead by asking GNU as to
  215. ENCODE the eight register moves and checking FCML's decode of the result
  216. (tests/probe/modrm11.py, which also carries the hand-checking anchors).
  217. Do not "fix" any of these from memory. The version that comes to mind is
  218. wrong in exactly the cells called out below, and every one of those
  219. mistakes shipped as code that decoded cleanly:
  220. r/m mod=00 mod=01 mod=10 mod=11
  221. 000 [BX+SI] [BX+SI]+disp [BX+SI]+disp AX
  222. 001 [BX+DI] [BX+DI]+disp [BX+DI]+disp CX
  223. 010 [BP+SI] [BP+SI]+disp [BP+SI]+disp DX
  224. 011 [BP+DI] [BP+DI]+disp [BP+DI]+disp BX
  225. 100 [SI] [SI]+disp [SI]+disp SP
  226. 101 [DI] [DI]+disp [DI]+disp BP
  227. 110 disp16 [BP]+disp [BP]+disp SI
  228. 111 [BX] [BX]+disp [BX]+disp DI
  229. "disp" is disp8 for mod=01 and disp16 for mod=10.
  230. In mod=11 BOTH fields name a register and BOTH use the same list,
  231. AX CX DX BX SP BP SI DI -- the reg field and the r/m field do not
  232. differ, and there is no second BX at code 7. The memorable version
  233. drops AX off the front and invents a duplicate BX at the end, which
  234. shifts every code down by one; that is precisely how MovSiBx and
  235. CmpSiBx came to be written "89 DC" and "39 DC", which are MOV SP,BX
  236. and CMP SP,BX. Note what the shifted table gets right by luck: SP is
  237. at 100 either way, so the mistake is invisible until you check a
  238. register below it. rm=100 is SP, and SI is rm=110, not rm=100.
  239. And reg/rm swap direction with the opcode, which is the other trap:
  240. 88 /r MOV r/m8,r8 89 /r MOV r/m16,r16 reg is the SOURCE
  241. 8A /r MOV r8,r/m8 8B /r MOV r16,r/m16 reg is the TARGET
  242. So ModRM C6 names "DH and AL" either way, but 88 C6 is DH:=AL while
  243. 8A C6 is AL:=DH. The 8-bit register list is also its own, and it is not
  244. the word list:
  245. 000 001 010 011 100 101 110 111 = AL CL DL BL AH CH DH BH
  246. The two lists agree at every code except 100, where the byte form is AH
  247. and the word form is SP. Reading the same ModRM byte as the other size
  248. silently swaps AH for SP, and 88 C2 is DL:=AL while 88 17 is [BX]<-DL.
  249. The word-form list, and the four anchors nobody writes by hand:
  250. 83 C4 08 ADD SP, 8 rm=100 -> SP
  251. 83 C6 02 ADD SI, 2 rm=110 -> SI
  252. 8B EC MOV BP, SP reg=101 rm=100
  253. 8B E5 MOV SP, BP reg=100 rm=101
  254. Asking "which register is rm=100?" and answering SI is the single most
  255. common error in this file's history.
  256. Concrete recipes for opcode 8r / 9r (r/m = rm, 16-bit form):
  257. mod=00 rm=110 -> 06 <disp16> the only direct form
  258. mod=00 rm=111 -> 07 = [BX] 2 bytes
  259. mod=00 rm=101 -> 05 = [DI] 2 bytes
  260. mod=01 rm=110 -> 46 disp8 = [BP]+disp8
  261. mod=01 rm=111 -> 47 disp8 = [BX]+disp8
  262. mod=01 rm=101 -> 45 disp8 = [DI]+disp8
  263. There is no [SP] form in 16-bit mode: SIB bytes are 386-only. Anything
  264. wanting the top of the stack has to go through BP. *)
  265. PROCEDURE PushBp ; BEGIN B (55H) END PushBp ;
  266. PROCEDURE PopBp ; BEGIN B (5DH) END PopBp ;
  267. PROCEDURE MovBpSp ; BEGIN B (8BH) ; B (0ECH) END MovBpSp ; (* 8B EC: MOV BP,SP *)
  268. PROCEDURE MovSpBp ; BEGIN B (89H) ; B (0ECH) END MovSpBp ; (* 89 EC: MOV SP,BP *)
  269. PROCEDURE LeaveR ; BEGIN B (0C9H) END LeaveR ;
  270. PROCEDURE RetR ; BEGIN B (0C3H) END RetR ;
  271. PROCEDURE Int21 ; BEGIN B (0CDH) ; B (21H) END Int21 ;
  272. PROCEDURE PushDs ; BEGIN B (1EH) END PushDs ;
  273. PROCEDURE PopEs ; BEGIN B (7H) END PopEs ;
  274. PROCEDURE PushAx ; BEGIN B (50H) END PushAx ;
  275. PROCEDURE PopAx ; BEGIN B (58H) END PopAx ;
  276. PROCEDURE PushBx ; BEGIN B (53H) END PushBx ;
  277. PROCEDURE PopBx ; BEGIN B (5BH) END PopBx ;
  278. PROCEDURE PushCx ; BEGIN B (51H) END PushCx ;
  279. PROCEDURE PopCx ; BEGIN B (59H) END PopCx ;
  280. PROCEDURE PushDx ; BEGIN B (52H) END PushDx ;
  281. PROCEDURE PopDx ; BEGIN B (5AH) END PopDx ;
  282. PROCEDURE PushDi ; BEGIN B (57H) END PushDi ;
  283. PROCEDURE PopDi ; BEGIN B (5FH) END PopDi ;
  284. PROCEDURE XorAxAx ; BEGIN B (31H) ; B (0C0H) END XorAxAx ; (* 11 000 000 *)
  285. PROCEDURE XorCxCx ; BEGIN B (31H) ; B (0C9H) END XorCxCx ; (* 11 001 001 *)
  286. PROCEDURE XorDxDx ; BEGIN B (31H) ; B (0D2H) END XorDxDx ; (* 11 010 010 *)
  287. PROCEDURE XorDiDi ; BEGIN B (31H) ; B (0FFH) END XorDiDi ; (* 11 111 111 *)
  288. PROCEDURE IncCx ; BEGIN B (41H) END IncCx ;
  289. PROCEDURE IncSi ; BEGIN B (46H) END IncSi ;
  290. PROCEDURE DecSi ; BEGIN B (4EH) END DecSi ;
  291. PROCEDURE AddDi2 ; BEGIN B (83H) ; B (0C7H) ; B (2) END AddDi2 ; (* 11 000 111 *)
  292. PROCEDURE CmpAl (v : CARDINAL ) ; BEGIN B (3CH) ; B (v) END CmpAl ;
  293. PROCEDURE CmpAx0 ; BEGIN B (83H) ; B (0F8H) ; B (0) END CmpAx0 ;
  294. PROCEDURE CmpCx0 ; BEGIN B (83H) ; B (0F9H) ; B (0) END CmpCx0 ;
  295. PROCEDURE CmpArg2W0 ;
  296. BEGIN
  297. B (8BH) ; B (0ECH) ; (* MOV BP,SP *)
  298. B (83H) ; B (7EH) ; B (2) ; B (0) ; (* CMP WORD [BP+2],0 *)
  299. B (89H) ; B (0ECH) (* MOV SP,BP *)
  300. END CmpArg2W0 ;
  301. PROCEDURE CmpArg4W0 ;
  302. (* CMP WORD PTR [BP+4],0 - the same comparison, in the frame where the entry
  303. pushed BP first.
  304. The 2 and the 4 are the whole point of having two names. [SP] is not
  305. encodable in 16-bit mode, so an entry that takes one 16-bit argument has to
  306. borrow BP to reach it, and how far up the argument then sits depends on
  307. whether BP was saved first: 2 bytes without the PUSH, 4 with it. A single
  308. emitter taking the displacement as a parameter is the obvious way to write
  309. this and the wrong one -- a parameter is invisible to tests/audit_helpers.py,
  310. which reads the NAME and compares it against the decode, so `CmpArgW0 (2)'
  311. and `CmpArgW0 (4)' would differ by one byte with nothing in the codebase
  312. able to tell which was meant. In the name, the audit pins it: a helper
  313. called CmpArg4W0 that emitted 83 7E 02 00 would fail the run.
  314. Note the sandwich: MOV BP,SP before and MOV SP,BP after, and no POP BP, so
  315. the two forms are the ONLY difference. (Was 83 7C 24 00 00, which decodes
  316. as CMP WORD [SI+24h],0.) The imm8 of the 83 form is sign-extended to 16
  317. bits, so 0 really is a 16-bit zero. *)
  318. BEGIN
  319. B (8BH) ; B (0ECH) ; (* MOV BP,SP *)
  320. B (83H) ; B (7EH) ; B (4) ; B (0) ; (* CMP WORD [BP+4],0 *)
  321. B (89H) ; B (0ECH) (* MOV SP,BP *)
  322. END CmpArg4W0 ;
  323. PROCEDURE CmpSiBx ; BEGIN B (39H) ; B (0DEH) END CmpSiBx ; (* 39 DE: CMP SI,BX *)
  324. PROCEDURE CmpCxDx ; BEGIN B (39H) ; B (0D1H) END CmpCxDx ; (* 11 010 001 *)
  325. PROCEDURE CmpDiCx ; BEGIN B (39H) ; B (0CFH) END CmpDiCx ; (* 11 001 111 *)
  326. PROCEDURE CmpBx0 ; BEGIN B (83H) ; B (0FBH) ; B (0) END CmpBx0 ;
  327. PROCEDURE CmpCxV (v : CARDINAL ) ; BEGIN B (83H) ; B (0F9H) ; B (v) END CmpCxV ;
  328. PROCEDURE AddDl (v : CARDINAL ) ; BEGIN B (80H) ; B (0C2H) ; B (v) END AddDl ;
  329. PROCEDURE SubAl (v : CARDINAL ) ; BEGIN B (2CH) ; B (v) END SubAl ;
  330. PROCEDURE NegAx ; BEGIN B (0F7H) ; B (0D8H) END NegAx ;
  331. PROCEDURE NegDi ; BEGIN B (0F7H) ; B (0DFH) END NegDi ;
  332. PROCEDURE DivCx ; BEGIN B (0F7H) ; B (0F1H) END DivCx ; (* 11 110 001 *)
  333. PROCEDURE MulBx ; BEGIN B (0F7H) ; B (0E3H) END MulBx ; (* 11 100 011 *)
  334. PROCEDURE MovAh (v : CARDINAL ) ; BEGIN B (0B4H) ; B (v) END MovAh ;
  335. PROCEDURE MovDl (v : CARDINAL ) ; BEGIN B (0B2H) ; B (v) END MovDl ;
  336. (* The runtime's own data block, addressed by absolute image address. Four
  337. emitters cover it, and the difference between them is the whole subject of
  338. the mistakes recorded below, so they are named systematically:
  339. Mov<reg>Vx MOV reg, Vx -- reg := the ADDRESS (BB / BA + Dd)
  340. Ld<reg>Vx MOV reg, [Vx] -- reg := the CONTENTS (8B 1E + Dd)
  341. StVx<reg> MOV [Vx], reg -- [Vx] := reg (89 1E + Dd)
  342. "Vx" is the operand word for "a 16-bit absolute address into the data
  343. block", spelled with a V precisely so it cannot be confused with a base
  344. register: on the 8086 there is no [BX] memory form, so the address has to
  345. go through mod=00 / rm=110, and rm=111 is [BX+SI] - a different, perfectly
  346. decodable instruction. tests/audit_helpers.py checks all four.
  347. The two traps here, both of which happened:
  348. - Dd versus W. Dd turns a D_ offset into a FIXUP, so the address can be
  349. placed only once the data block has been given its final position in the
  350. image. W emits the number as it stands. The two produce the SAME
  351. instruction, and getch once used MovBxImm where it wanted the address, so
  352. `MOV BX,D_PUSH' came out as `MOV BX,001Ch' - the raw offset 1Ch, inside
  353. the entry JMP. Non-zero, so the pushback slot was never examined, getch
  354. served a byte of the runtime's own code forever, and readln hung. Hence
  355. the Imm suffix: the raw-immediate emitters are named for what they do.
  356. - address versus contents. LdBxVx and MovBxVx are both `8B`/`BB` + Dd to
  357. the eye and completely different instructions. getch's first cut used
  358. MovBxVx where it wanted the contents, so it tested the ADDRESS for zero,
  359. found 02AFh every time, and returned the low byte of memory 02AFh - 0,
  360. the value it had just stored there - instead of asking DOS. *)
  361. PROCEDURE MovBxImm (v : CARDINAL ) ; BEGIN B (0BBH) ; W (v) END MovBxImm ;
  362. PROCEDURE MovCxImm (v : CARDINAL ) ; BEGIN B (0B9H) ; W (v) END MovCxImm ;
  363. PROCEDURE MovDxImm (v : CARDINAL ) ; BEGIN B (0BAH) ; W (v) END MovDxImm ;
  364. PROCEDURE MovBxVx (delta : CARDINAL ) ; BEGIN B (0BBH) ; Dd (delta) END MovBxVx ;
  365. PROCEDURE MovDxVx (delta : CARDINAL ) ; BEGIN B (0BAH) ; Dd (delta) END MovDxVx ;
  366. PROCEDURE LdBxVx (delta : CARDINAL ) ;
  367. (* 8B 1E lo hi: BX := WORD PTR [Vx] - mod=00 / rm=110, the only 16-bit form
  368. that can carry a bare absolute address. *)
  369. BEGIN
  370. B (8BH) ; B (01EH) ; Dd (delta)
  371. END LdBxVx ;
  372. PROCEDURE MovSiAx ; BEGIN B (8BH) ; B (0F0H) END MovSiAx ; (* 8B F0: MOV SI,AX *)
  373. PROCEDURE MovAxDi ; BEGIN B (8BH) ; B (0C7H) END MovAxDi ; (* 11 000 111 *)
  374. PROCEDURE MovCxSi6 ; BEGIN B (8BH) ; B (4CH) ; B (6) END MovCxSi6 ;
  375. PROCEDURE MovDxSi2 ; BEGIN B (8BH) ; B (54H) ; B (2) END MovDxSi2 ;
  376. (* The program header is a block of words laid out by the compiler:
  377. +0 hdrFlag 1 = image is valid
  378. +2 hdrCS end of the generated code, in bytes
  379. +4 hdrDS base of the data area <- initmem wants these two
  380. +6 hdrHeap end of the data area <-
  381. +8 hdrMax max open files
  382. initmem must read the words the COMPILER WRITES, and those are +4 (hdrDS,
  383. the data base) and +6 (hdrHeap, the data end). It used to read +8, which
  384. is hdrMax -- and the compiler patches that to 0 -- so CX came out as 0,
  385. "cmp cx,dx / jbe im_done" fired immediately, and initmem silently zeroed
  386. nothing at all. A 3-byte instruction, so no entry offset moved either
  387. way; only the header offset in the byte changed. Nothing caught it
  388. because the loop was well formed - it just did nothing. The cross-check
  389. that would have: tests/run_com_tests.sh now asserts the runtime's SI-relative
  390. reads against the same header offsets it verifies the words at. *)
  391. PROCEDURE MovDxSi4 ; BEGIN B (8BH) ; B (54H) ; B (4) END MovDxSi4 ;
  392. PROCEDURE MovAxBp4 ; BEGIN B (8BH) ; B (46H) ; B (4) END MovAxBp4 ; (* AX:=[BP+4] *)
  393. PROCEDURE MovBxBp4 ; BEGIN B (8BH) ; B (5EH) ; B (4) END MovBxBp4 ; (* BX:=[BP+4] *)
  394. PROCEDURE MovAlDh ; BEGIN B (8AH) ; B (0C6H) END MovAlDh ; (* 8A C6: AL:=DH *)
  395. PROCEDURE MovDlSi ; BEGIN B (8AH) ; B (14H) END MovDlSi ;
  396. PROCEDURE MovDlArg2 ; BEGIN B (8AH) ; B (56H) ; B (2) END MovDlArg2 ; (* DL:=[BP+2] *)
  397. (* The AL twins of the three above. INT 21h AH=02h displays the character in
  398. AL, not DL, so every character this runtime writes has to arrive in AL.
  399. DL is the natural register for the digit scratch in wrint and for a stack
  400. argument in wrchar, and both of them were loading DL and then calling
  401. AH=02h - which printed whatever happened to be left in AL. For wrint that
  402. was the QUOTIENT's low byte from the preceding DIV, so writeln(1) printed a
  403. NUL and writeln(2) printed a NUL, and a two-digit number printed its
  404. quotient instead of itself. A wrong register is not a wrong encoding: the
  405. golden bytes were right, `mov dl,[si]' is a perfectly good instruction, and
  406. every byte-level check stayed green. Only running it found this. *)
  407. PROCEDURE MovAlD (v : CARDINAL ) ; BEGIN B (0B0H) ; B (v) END MovAlD ; (* MOV AL,imm8 *)
  408. PROCEDURE MovAlSi ; BEGIN B (8AH) ; B (04H) END MovAlSi ; (* 8A 04: AL:=[SI] *)
  409. PROCEDURE MovAlArg2 ; BEGIN B (8AH) ; B (46H) ; B (2) END MovAlArg2 ; (* AL:=[BP+2] *)
  410. PROCEDURE MovAlArg4 ; BEGIN B (8AH) ; B (46H) ; B (4) END MovAlArg4 ; (* AL:=[BP+4] *)
  411. PROCEDURE MovAlDl ; BEGIN B (8AH) ; B (0C2H) END MovAlDl ; (* 8A C2: AL:=DL *)
  412. PROCEDURE StDiAx ; BEGIN B (89H) ; B (5H) END StDiAx ; (* 89 05: [DI]:=AX *)
  413. PROCEDURE StDiAl ; BEGIN B (88H) ; B (5H) END StDiAl ; (* 88 05: [DI]:=AL *)
  414. PROCEDURE StSiDl ; BEGIN B (88H) ; B (14H) END StSiDl ; (* 88 14: [SI]:=DL *)
  415. PROCEDURE StDiDl ; BEGIN B (88H) ; B (15H) END StDiDl ; (* 88 15: [DI]:=DL *)
  416. PROCEDURE StDiCx ; BEGIN B (89H) ; B (0DH) END StDiCx ; (* 89 0D: [DI]:=CX *)
  417. PROCEDURE MovDiBp4 ; BEGIN B (8BH) ; B (7EH) ; B (4) END MovDiBp4 ; (* DI:=[BP+4] *)
  418. (* There is NO "store through BX" instruction on the 8086, and writing one
  419. anyway is the single most expensive mistake this file has produced, so the
  420. reasoning is recorded rather than left in the probe history.
  421. In 16-bit addressing the r/m column is a LOCATION, not a register list: for
  422. mod=00, rm=000..101 are [BX+SI] [BX+DI] [BP+SI] [BP+DI] [SI] [DI], and
  423. rm=110 is the only one that means "a direct displacement". rm=111 is
  424. [BX+SI], NOT [BX]. So `89 1D' - the ModR/M the first version of this used
  425. for "MOV [BX],CX" - is mod=00 reg=BX r/m=DI, i.e. MOV [DI],BX: the two
  426. operands swapped AND [BX] inexpressible. GNU as and objdump in -m i8086
  427. agree, and the bytes are perfectly well formed, which is why nothing
  428. objected. What the program saw was its own address being written over the
  429. interrupt vector table, and the variable it was asked to fill left holding
  430. whatever the loader put there: readln(n) then printed 0, and readln(c)
  431. printed a NUL. Correct by comparison: the caller puts the address in BX,
  432. and the store has to go through DI or SI, so the sequence is
  433. MOV DI,[BP+4] / MOV [DI],<value> - see StoreThrough below. *)
  434. PROCEDURE MovDlAl ; BEGIN B (88H) ; B (0C2H) END MovDlAl ;
  435. PROCEDURE XchgAxDi ; BEGIN B (87H) ; B (0C7H) END XchgAxDi ; (* 87 C7: AX<->DI *)
  436. PROCEDURE AddAxDi ; BEGIN B (03H) ; B (0C7H) END AddAxDi ; (* 03 C7: AX:=AX+DI *)
  437. PROCEDURE MovSiBx ; BEGIN B (89H) ; B (0DEH) END MovSiBx ; (* 89 DE: MOV SI,BX *)
  438. PROCEDURE MovDiDx ; BEGIN B (89H) ; B (0D7H) END MovDiDx ;
  439. PROCEDURE MovDiAx ; BEGIN B (89H) ; B (0C7H) END MovDiAx ;
  440. PROCEDURE AddDiAx ; BEGIN B (1H) ; B (0C7H) END AddDiAx ;
  441. PROCEDURE IncBx ; BEGIN B (43H) END IncBx ;
  442. PROCEDURE LdAlBx ; BEGIN B (8AH) ; B (07H) END LdAlBx ; (* 8A 07: AL:=[BX] *)
  443. PROCEDURE MovAlBl ; BEGIN B (8AH) ; B (0C3H) END MovAlBl ; (* 8A C3: AL:=BL *)
  444. (* 8A 07 and 8A C3 are one letter apart and do opposite things: the
  445. first reads the byte AT the pointer in BX, the second takes the low
  446. byte OF BX. getch's pushback path wanted the second and used the
  447. first, so it read memory 010Ah - the address of the character - and
  448. returned whatever was there. Hence the Ld/Mov split. *)
  449. PROCEDURE MovClBx ; BEGIN B (8AH) ; B (0FH) END MovClBx ; (* 8A 0F: CL:=[BX] *)
  450. PROCEDURE XorBxBx ; BEGIN B (31H) ; B (0DBH) END XorBxBx ; (* 31 DB: BX:=0 *)
  451. PROCEDURE MovBlDl ; BEGIN B (8AH) ; B (0DAH) END MovBlDl ; (* 8A DA: BL:=DL *)
  452. PROCEDURE StVxBx (delta : CARDINAL ) ;
  453. (* 89 1E lo hi: MOV [Vx],BX - the mirror of LdBxVx above, and it must use the
  454. same mod=00 / rm=110 and the same Dd fixup kind, or one of the two
  455. addresses a different place. *)
  456. BEGIN
  457. B (89H) ; B (01EH) ; Dd (delta)
  458. END StVxBx ;
  459. PROCEDURE JmpBx ; BEGIN B (0FFH) ; B (0E3H) END JmpBx ; (* FF E3: JMP BX *)
  460. (* JE 74 JNE 75 JB 72 JBE 76 JGE 7D *)
  461. PROCEDURE Je8 (nm : ARRAY OF CHAR ) ; BEGIN Jcc (74H, nm) END Je8 ;
  462. PROCEDURE Jne8 (nm : ARRAY OF CHAR ) ; BEGIN Jcc (75H, nm) END Jne8 ;
  463. PROCEDURE Jb8 (nm : ARRAY OF CHAR ) ; BEGIN Jcc (72H, nm) END Jb8 ;
  464. PROCEDURE Jbe8 (nm : ARRAY OF CHAR ) ; BEGIN Jcc (76H, nm) END Jbe8 ;
  465. PROCEDURE Jge8 (nm : ARRAY OF CHAR ) ; BEGIN Jcc (7DH, nm) END Jge8 ;
  466. PROCEDURE Ja8 (nm : ARRAY OF CHAR ) ; BEGIN Jcc (77H, nm) END Ja8 ;
  467. PROCEDURE Jcxz8 (nm : ARRAY OF CHAR ) ; BEGIN Jcc (0E3H, nm) END Jcxz8 ;
  468. PROCEDURE Loop8 (nm : ARRAY OF CHAR ) ; BEGIN Jcc (0E2H, nm) END Loop8 ;
  469. (* ---------------------------------------------------------------- *)
  470. (* the entries *)
  471. (* ---------------------------------------------------------------- *)
  472. PROCEDURE EmitInitMem ;
  473. (* AX = offset of the program header. The header holds, at +4 the base of
  474. the program's data area and at +8 its end, so the globals can be zeroed -
  475. Pascal leaves them undefined, TP3's runtime clears them. Also makes
  476. ES = DS so that any string instruction in the library would work. *)
  477. BEGIN
  478. M ("initmem") ;
  479. MovSiAx ; (* SI = AX = the header offset the caller passed *)
  480. MovDxSi4 ; (* DX = [SI+4] = hdrDS = data base *)
  481. MovCxSi6 ; (* CX = [SI+6] = hdrHeap = data end *)
  482. CmpCxDx ;
  483. Jbe8 ("im_done") ;
  484. MovDiDx ; (* DI = data base *)
  485. M ("im_zero") ;
  486. XorAxAx ; (* AX = 0: the value written into every global.
  487. The caller passes the header offset in AX, so
  488. without this the "zeroing" loop would write the
  489. header offset into all of them. *)
  490. StDiAx ;
  491. AddDi2 ;
  492. CmpDiCx ;
  493. Jb8 ("im_zero") ;
  494. M ("im_done") ;
  495. PushDs ; PopEs ;
  496. RetR
  497. END EmitInitMem ;
  498. PROCEDURE EmitEnd ;
  499. (* progend and halt are the same code: the compiler already zeroes AX for
  500. progend, and a HALT argument is discarded at compile time, so both leave
  501. with exit code 0. *)
  502. BEGIN
  503. M ("progend") ;
  504. M ("halt") ;
  505. XorAxAx ;
  506. MovAh (4CH) ;
  507. Int21 ;
  508. RetR
  509. END EmitEnd ;
  510. PROCEDURE EmitStackChk ;
  511. (* called from every procedure prologue. Range and stack checking are not
  512. compiled in yet, so this must do nothing at all - in particular it must
  513. not touch a register, because the call site is in the middle of a
  514. partially evaluated expression. *)
  515. BEGIN
  516. M ("stackchk") ;
  517. RetR
  518. END EmitStackChk ;
  519. PROCEDURE EmitGetCh ;
  520. (* AL = next character, 1Ah at end of input. INT 21h AH=08h reads without
  521. echoing, so a redirected stdin behaves the same as a keyboard.
  522. This is TP3's `getbyte' (TPSRC4:94), not a bare INT 21h. getbyte serves a
  523. buffered character from the file record without advancing the buffer
  524. pointer when the "char pre-read" flag ($02) is set, and readnum sets that
  525. flag on the character that ENDS a number - the terminating blank is *not*
  526. consumed. xreadln is then called and it is the xreadln that consumes the
  527. newline.
  528. This matters because `readln(n)' is emitted as xrdint followed by xreadln
  529. (TPSRC8 prdrdln emits xreadln whenever rdlnflg is set). A getch that
  530. always consumed its character left xreadln starting on the *following*
  531. line, so `readln(n); readln(c)' read the char from line 3 of the input.
  532. One byte of pushback reproduces the flag. *)
  533. BEGIN
  534. M ("getch") ;
  535. LdBxVx (D_PUSH) ; (* BX := the slot's contents *)
  536. CmpBx0 ;
  537. Je8 ("gc_dos") ;
  538. MovAlBl ; (* AL := the pending character, i.e. the
  539. low byte OF BX - not the byte at [BX] *)
  540. XorBxBx ;
  541. StVxBx (D_PUSH) ; (* and empty the slot *)
  542. RetR ;
  543. M ("gc_dos") ;
  544. MovAh (08H) ;
  545. Int21 ;
  546. RetR
  547. END EmitGetCh ;
  548. PROCEDURE EmitUnGetCh ;
  549. (* put AL back, so the next getch returns it again. TP3 does this by
  550. rewinding the buffer pointer; a one-byte slot is the same thing. *)
  551. BEGIN
  552. M ("ungetch") ;
  553. MovDlAl ;
  554. MovBxImm (1) ; (* BH := 1 marks the slot occupied, so
  555. a pushed-back NUL is still a
  556. pushed-back NUL *)
  557. MovBlDl ;
  558. StVxBx (D_PUSH) ;
  559. RetR
  560. END EmitUnGetCh ;
  561. PROCEDURE EmitWrInt ;
  562. (* one signed 16-bit value on the stack. Div CX gives the remainder in DX,
  563. which is turned into a digit and stored backwards from the end of the
  564. scratch area, then printed forwards. *)
  565. BEGIN
  566. M ("wrint") ;
  567. PushBp ; MovBpSp ;
  568. MovAxBp4 ;
  569. CmpAx0 ;
  570. Jge8 ("wi_pos") ;
  571. PushAx ;
  572. MovAlD (ORD ("-")) ; MovAh (2) ; Int21 ;
  573. PopAx ;
  574. NegAx ;
  575. M ("wi_pos") ;
  576. MovCxImm (10) ;
  577. MovBxVx (D_NUM + 8) ; (* BX = one past the last digit *)
  578. MovSiBx ;
  579. M ("wi_dig") ;
  580. XorDxDx ;
  581. DivCx ;
  582. AddDl (ORD ("0")) ;
  583. DecSi ;
  584. StSiDl ;
  585. CmpAx0 ;
  586. Jne8 ("wi_dig") ;
  587. M ("wi_out") ;
  588. CmpSiBx ;
  589. Je8 ("wi_done") ;
  590. MovAlSi ; (* the digit: AH=02h wants it in AL *)
  591. MovAh (02H) ; Int21 ;
  592. IncSi ;
  593. J8 ("wi_out") ;
  594. M ("wi_done") ;
  595. MovSpBp ; PopBp ; RetR
  596. END EmitWrInt ;
  597. PROCEDURE EmitWrChar ;
  598. (* the low byte of the one 16-bit argument, which sits above the return
  599. address. There is no [SP] addressing in 16-bit mode, so BP stands in for
  600. the stack pointer -- and it has to be SAVED, because BP is the one register
  601. a caller is entitled to expect back: the test driver keeps its cursor into
  602. the case record there, so an entry that borrows BP without returning it
  603. sends the next call to a garbage address. That is not hypothetical: wrchar
  604. and wrbool both did it, and "writeln(42) writeln TRUE in one program" hung
  605. the machine with the record header printed twice, because the driver had
  606. been sent back to the top of its own loop. Load BP, use it, hand it back.
  607. MovAlArg4, not MovAlArg2: BP is pushed first, so the argument is four bytes
  608. up, not two. AL, not DL: see the AL twins above. *)
  609. BEGIN
  610. M ("wrchar") ;
  611. PushBp ; MovBpSp ;
  612. MovAlArg4 ;
  613. MovSpBp ; PopBp ;
  614. MovAh (02H) ;
  615. Int21 ;
  616. RetR
  617. END EmitWrChar ;
  618. PROCEDURE EmitWrInl ;
  619. (* Write an inline string literal - TP3 TPSRC4 "xwrtinl".
  620. The compiler emits
  621. CALL wrtinl <length byte> <character>...
  622. so the return address on the stack points at the length byte that follows
  623. the call. POP BX takes that address, CX picks up the length, and the
  624. routine finishes with JMP BX - returning to just past the last character.
  625. That is the whole trick: the literal is self-delimiting, so it needs no
  626. terminator, no length table and no space in the data segment, and it costs
  627. the code stream only the characters themselves (TPSRC10 "estring" emits
  628. exactly <length byte><chars> for the same reason).
  629. Consequently this entry has NO stack argument, unlike WrInt/WrChar: the
  630. return address has already been consumed by the POP. *)
  631. BEGIN
  632. M ("wrtinl") ;
  633. PopBx ; (* BX := address of the length byte *)
  634. XorCxCx ;
  635. MovClBx ; (* CX := length *)
  636. IncBx ; (* BX -> first character *)
  637. MovAh (02H) ; (* INT 21h/02h: put character, AL *)
  638. Jcxz8 ("wn_end") ; (* empty string -> nothing to do *)
  639. M ("wn_loop") ;
  640. LdAlBx ; (* AL := next character *)
  641. Int21 ; (* (preserves every register but AL) *)
  642. IncBx ;
  643. Loop8 ("wn_loop") ;
  644. M ("wn_end") ;
  645. JmpBx (* resume past the string; no RET here,
  646. the return address is already gone *)
  647. END EmitWrInl ;
  648. PROCEDURE EmitWrStr ;
  649. (* Write a string VARIABLE - TP3 TPSRC4 "xwrtstr", minus the field-width
  650. formatting this compiler does not compile yet.
  651. The compiler emits
  652. <PUSH the string's address> from EmPushVarAddr
  653. CALL wrstr
  654. so the address is the one 16-bit argument on the stack, read through BP
  655. exactly as WrChar reads its value (there is no [SP] form). The first byte
  656. at that address is the current LENGTH and the characters follow it, which
  657. is the layout TPSRC8's `string' type gives a variable: TPSRC8:83 does
  658. `INC BX / MOV varsize,BX' for `string [n]', so n+1 bytes, length byte
  659. first.
  660. The loop is wrtinl's, pointed at the caller's bytes instead of at the
  661. bytes behind the call: CL := [BX] is the length, BX moves to the first
  662. character, and one INT 21h/02h prints each one until CX runs out. A
  663. length of 0 prints nothing, so an empty string is silent. *)
  664. BEGIN
  665. M ("wrstr") ;
  666. PushBp ; MovBpSp ;
  667. PushBx ; PushCx ; PushAx ;
  668. MovBxBp4 ; (* BX := the caller's string address *)
  669. XorCxCx ;
  670. MovClBx ; (* CX := the length byte at [BX] *)
  671. IncBx ; (* BX -> the first character *)
  672. MovAh (02H) ; (* INT 21h/02h: put the character in AL *)
  673. Jcxz8 ("ws_end") ; (* empty string -> nothing to do *)
  674. M ("ws_loop") ;
  675. LdAlBx ; (* AL := next character *)
  676. Int21 ; (* preserves every register but AL *)
  677. IncBx ;
  678. Loop8 ("ws_loop") ;
  679. M ("ws_end") ;
  680. PopAx ; PopCx ; PopBx ;
  681. MovSpBp ; PopBp ;
  682. RetR
  683. END EmitWrStr ;
  684. PROCEDURE EmitWrBool ;
  685. (* 4, not 2, for the reason given on CmpArg4W0: BP is pushed before it is
  686. borrowed, so the argument is four bytes up. POP BP before the RET, and
  687. note that CmpArg4W0 has already handed SP back, so the pop lands on the
  688. saved BP and not on the argument. *)
  689. BEGIN
  690. M ("wrbool") ;
  691. PushBp ;
  692. CmpArg4W0 ;
  693. Jne8 ("wb_t") ;
  694. MovDxVx (D_FALSE) ;
  695. J8 ("wb_o") ;
  696. M ("wb_t") ;
  697. MovDxVx (D_TRUE) ;
  698. M ("wb_o") ;
  699. MovAh (09H) ;
  700. Int21 ;
  701. PopBp ;
  702. RetR
  703. END EmitWrBool ;
  704. PROCEDURE EmitWrLn ;
  705. BEGIN
  706. M ("wrln") ;
  707. MovDxVx (D_CRLF) ;
  708. MovAh (09H) ;
  709. Int21 ;
  710. RetR
  711. END EmitWrLn ;
  712. PROCEDURE EmitWrReal ;
  713. (* the 6-byte real is on the stack but is not formatted: the compiler does
  714. not yet load real operands into a form the runtime could read. A visible
  715. marker beats printing the mantissa as an integer. *)
  716. BEGIN
  717. M ("wrreal") ;
  718. MovDxVx (D_REAL) ;
  719. MovAh (09H) ;
  720. Int21 ;
  721. RetR
  722. END EmitWrReal ;
  723. PROCEDURE EmitRdInt ;
  724. (* Read a number into a 16-bit variable: the caller's address is on the
  725. stack. The scan itself is EmitRdScan, shared with rdbyte; an entry owns
  726. its frame, its address load and its store, nothing else. *)
  727. BEGIN
  728. M ("rdint") ;
  729. PushBp ; MovBpSp ;
  730. PushAx ; PushBx ; PushCx ; PushDx ; PushDi ;
  731. C8 ("rdscan") ;
  732. CmpCxV (2) ;
  733. Je8 ("ri_skip") ; (* nothing entered: leave the variable
  734. alone, as TP3's xrdint at rdierr does *)
  735. MovDiBp4 ; (* ...the caller's address into DI... *)
  736. StDiAx ; (* ...and store the word. MovDiBp4 and
  737. not a [BX] form: there is none; see
  738. the note on the store emitters. *)
  739. M ("ri_skip") ;
  740. PopDi ; PopDx ; PopCx ; PopBx ; PopAx ;
  741. MovSpBp ; PopBp ; RetR
  742. END EmitRdInt ;
  743. PROCEDURE EmitRdScan ;
  744. (* The number reader, called by rdint (word store) and by rdbyte (byte
  745. store). A transcription of TP3 TPSRC4 xrdint/readnum, minus the store:
  746. rnspace: getbyte ; ^Z ? -> rnend
  747. consume ; <= 20h ? -> rnspace
  748. rndig: [buf]=char ; getbyte ; <= 20h ? -> rnend (NOT consumed)
  749. consume ; -> rndig
  750. rnend: buf[0] := 0
  751. It builds NO frame of its own and touches neither BP nor SP, so it is
  752. called from the caller's frame and that frame's [BP+4] is still the
  753. caller's argument when it returns. It clobbers AX BX CX DX DI, which the
  754. caller has already saved.
  755. Returns AX = the value, and CX = 0 positive, 1 negative, 2 nothing read.
  756. CX = 2 is set HERE, on the ^Z path: it is the state both callers test to
  757. decide not to store, and before this split the check existed only as a
  758. jump to the exit, so the state itself was never produced. AX is
  759. meaningless when CX = 2.
  760. Two behaviours are load-bearing and neither is obvious:
  761. - the character that ENDS the scan is left pending, because readnum tests
  762. it *before* clearing getbyte's pre-read flag. xreadln, which the
  763. compiler emits next for a readln, is what consumes the newline. See the
  764. note on getch.
  765. - ^Z before any digit returns without a value, and the caller then returns
  766. WITHOUT touching the variable: TP3's xrdint reaches rdierr and RETs.
  767. Sign in CX, value in DI, value in AX on the way out. *)
  768. BEGIN
  769. M ("rdscan") ;
  770. M ("ri_lead") ;
  771. C8 ("getch") ;
  772. CmpAl (1AH) ; (* ^Z: end of input *)
  773. Je8 ("ri_none") ;
  774. CmpAl (20H) ; (* every control char and the space.
  775. 20H, not 20: readnum skips
  776. everything <= #$20, and a literal
  777. that READS like hex but is
  778. decimal compares AL against 14h,
  779. so a leading space is never
  780. skipped and the scan ends with
  781. nothing read. Every other
  782. literal in this module is
  783. explicit for the same reason. *)
  784. Jbe8 ("ri_lead") ;
  785. XorCxCx ;
  786. CmpAl (ORD ("-")) ;
  787. Jne8 ("ri_nos") ;
  788. IncCx ;
  789. C8 ("getch") ;
  790. J8 ("ri_dig0") ;
  791. M ("ri_nos") ;
  792. CmpAl (ORD ("+")) ;
  793. Jne8 ("ri_dig0") ;
  794. C8 ("getch") ;
  795. M ("ri_dig0") ;
  796. XorDiDi ;
  797. M ("ri_dig") ;
  798. CmpAl (ORD ("0")) ;
  799. Jb8 ("ri_done") ;
  800. CmpAl (ORD ("9")) ;
  801. Ja8 ("ri_done") ;
  802. SubAl (ORD ("0")) ;
  803. MovAh (0) ; (* AX := the digit, 0..9 *)
  804. XchgAxDi ; (* AX := the value so far, DI := the digit.
  805. The digit has to survive MUL, and the 8-bit
  806. reg field of 8A/88 has only AL/CL/DL/BL/AH/
  807. CH/DH/BH - there is no SI or DI byte to park
  808. it in. MUL r/m16 writes DX, so the previous
  809. version's "keep the digit across the multiply"
  810. comment was describing a register the multiply
  811. owns. Every read of an integer therefore
  812. returned 0: the digit was computed, moved to
  813. DH, and multiplied away one instruction later. *)
  814. MovBxImm (10) ;
  815. MulBx ; (* DX:AX := value * 10 *)
  816. AddAxDi ; (* AX := low word + digit. A carry out of bit 15
  817. is dropped, which is what a 16-bit INTEGER
  818. does anyway - the high word of the product is
  819. never stored. *)
  820. MovDiAx ;
  821. C8 ("getch") ;
  822. J8 ("ri_dig") ;
  823. M ("ri_done") ;
  824. C8 ("ungetch") ; (* the delimiter stays pending *)
  825. CmpCx0 ;
  826. Je8 ("ri_pos") ;
  827. NegDi ;
  828. M ("ri_pos") ;
  829. MovAxDi ; (* the parsed value out of DI and into AX,
  830. where both callers store it from *)
  831. RetR ;
  832. M ("ri_none") ; (* ^Z first: no value, and CX says so *)
  833. MovCxImm (2) ;
  834. RetR
  835. END EmitRdScan ;
  836. PROCEDURE EmitRdByte ;
  837. (* Read a number into a BYTE variable: the same scan, a one-byte store.
  838. TPSRC8 prdnum dispatches on the variable's size - `CMP varsize,#$01 /
  839. JA prdemit' keeps xrdint for anything wider and falls through to
  840. `MOV AX,#xrdbyte' for a one-byte variable - and TPSRC4's two entries
  841. differ in exactly one store: xrdbyte is `CLC', xrdint is `STC', and rdi1
  842. stores AX or AL on `JNB rdibyt'.
  843. The flag form is not transcribed. It would put the STC at the head of the
  844. rdint entry, and the BP contract in tests/rt_exec.py requires an entry
  845. that borrows BP to *start* with `55 8B EC' and to hand BP back from inside
  846. its own slice - which is what an entry owning its own frame does. Two
  847. frames and two stores around one scan is the same idea, with that
  848. constraint respected.
  849. The store is the whole reason this entry exists: rdint writes two bytes,
  850. and the second one is the byte that FOLLOWS the variable, so
  851. `readln (b)' was reading into b and into whatever the compiler had put
  852. next to it. *)
  853. BEGIN
  854. M ("rdbyte") ;
  855. PushBp ; MovBpSp ;
  856. PushAx ; PushBx ; PushCx ; PushDx ; PushDi ;
  857. C8 ("rdscan") ;
  858. CmpCxV (2) ;
  859. Je8 ("rby_skip") ; (* nothing entered: leave it alone *)
  860. MovDiBp4 ; (* the caller's address into DI *)
  861. StDiAl ; (* ONE byte - 88 05, AL not AX *)
  862. M ("rby_skip") ;
  863. PopDi ; PopDx ; PopCx ; PopBx ; PopAx ;
  864. MovSpBp ; PopBp ; RetR
  865. END EmitRdByte ;
  866. PROCEDURE EmitRdChar ;
  867. BEGIN
  868. M ("rdchar") ;
  869. PushBp ; MovBpSp ;
  870. PushAx ; PushBx ; PushDi ;
  871. C8 ("getch") ;
  872. MovDlAl ;
  873. MovDiBp4 ; (* no [BX] form: the address goes in DI *)
  874. StDiDl ;
  875. PopDi ; PopBx ; PopAx ;
  876. MovSpBp ; PopBp ; RetR
  877. END EmitRdChar ;
  878. PROCEDURE EmitRdBool ;
  879. (* one character, classified the way TP3 does: T/t/Y/y/1 true, anything else
  880. false. *)
  881. BEGIN
  882. M ("rdbool") ;
  883. PushBp ; MovBpSp ;
  884. PushAx ; PushBx ; PushCx ; PushDi ;
  885. C8 ("getch") ;
  886. XorCxCx ;
  887. CmpAl (ORD ("T")) ; Je8 ("rb_t") ;
  888. CmpAl (ORD ("t")) ; Je8 ("rb_t") ;
  889. CmpAl (ORD ("Y")) ; Je8 ("rb_t") ;
  890. CmpAl (ORD ("y")) ; Je8 ("rb_t") ;
  891. CmpAl (ORD ("1")) ; Je8 ("rb_t") ;
  892. J8 ("rb_s") ;
  893. M ("rb_t") ;
  894. IncCx ;
  895. M ("rb_s") ;
  896. MovDiBp4 ; (* DI = the caller's address, because there is
  897. no [BX] to store through *)
  898. StDiCx ;
  899. PopDi ; PopCx ; PopBx ; PopAx ;
  900. MovSpBp ; PopBp ; RetR
  901. END EmitRdBool ;
  902. PROCEDURE EmitRdLn ;
  903. (* discard the rest of the line, including the terminator *)
  904. BEGIN
  905. M ("rdln") ;
  906. PushAx ;
  907. M ("rl_loop") ;
  908. C8 ("getch") ;
  909. CmpAl (0DH) ; Je8 ("rl_e") ;
  910. CmpAl (0AH) ; Je8 ("rl_e") ;
  911. CmpAl (1AH) ; Je8 ("rl_e") ; (* ^Z: end of input *)
  912. J8 ("rl_loop") ;
  913. M ("rl_e") ;
  914. PopAx ;
  915. RetR
  916. END EmitRdLn ;
  917. PROCEDURE Str5 (s : ARRAY OF CHAR ; n : CARDINAL) ;
  918. (* emit n characters, so the strings below are visibly the same length as the
  919. D_ constants claim and an off-by-one in either place is a compile-time
  920. mismatch rather than a wrong pointer nobody notices *)
  921. VAR i : CARDINAL ;
  922. BEGIN
  923. i := 0 ;
  924. WHILE i < n DO
  925. B (ORD (s [i])) ;
  926. INC (i)
  927. END
  928. END Str5 ;
  929. PROCEDURE EmitData ;
  930. (* The runtime's data block. The D_ constants name the offsets in it and are
  931. ASSERTED against what is emitted here, three ways: the starting offset of
  932. each string, the offset just past it, and the block's total length. A D_
  933. constant is a 16-bit immediate inside a MOV, so a wrong one is a
  934. perfectly well-formed instruction that reads the wrong memory - see the
  935. note on the D_ block. *)
  936. BEGIN
  937. dataAt := rpos ;
  938. (* Each D_ is checked BEFORE the string it names is emitted, because it
  939. names that string's START. *)
  940. IF dataAt + D_NUM # rpos THEN
  941. HALT
  942. END ;
  943. (* 8 bytes of scratch, never read before written *)
  944. B (0) ; B (0) ; B (0) ; B (0) ; B (0) ; B (0) ; B (0) ; B (0) ;
  945. IF dataAt + D_TRUE # rpos THEN
  946. HALT
  947. END ;
  948. Str5 ("TRUE$", 5) ;
  949. IF dataAt + D_FALSE # rpos THEN
  950. HALT
  951. END ;
  952. Str5 ("FALSE$", 6) ;
  953. IF dataAt + D_CRLF # rpos THEN
  954. HALT
  955. END ;
  956. B (13) ; B (10) ; B (ORD ("$")) ;
  957. IF dataAt + D_REAL # rpos THEN
  958. HALT
  959. END ;
  960. Str5 ("?REAL?", 6) ;
  961. IF dataAt + D_PUSH # rpos THEN
  962. HALT
  963. END ;
  964. B (0) ; B (0) ; (* the pushback slot, empty to begin with *)
  965. WHILE rpos < dataAt + D_END DO
  966. B (0)
  967. END ;
  968. (* the total, so D_END is checked too and not just used as a pad target *)
  969. IF rpos - dataAt # D_END THEN
  970. HALT
  971. END
  972. END EmitData ;
  973. PROCEDURE FixUp ;
  974. VAR i, t, rel : CARDINAL ;
  975. BEGIN
  976. i := 0 ;
  977. WHILE i < nfix DO
  978. IF fix [i].kind = 2 THEN
  979. (* A data address, and the ONE kind that is absolute rather than
  980. relative: rtBase says where the blob sits in the image, val says
  981. where inside the blob, and LoadBias says where the image itself
  982. sits once DOS has loaded it. All three are needed; omitting any
  983. one produces a readable-looking address into the wrong bytes. *)
  984. t := (dataAt + rtBase + fix [i].val + LoadBias) MOD 10000H ;
  985. rt [fix [i].place] := VAL (BYTE, t MOD 100H) ;
  986. rt [fix [i].place + 1] := VAL (BYTE, (t DIV 100H) MOD 100H)
  987. ELSE
  988. t := LblOff (fix [i].nm) ;
  989. IF fix [i].kind = 0 THEN
  990. (* rel8 is measured from the end of the instruction, i.e. one
  991. byte past the displacement field *)
  992. rel := (t + 100H - (fix [i].place + 1)) MOD 100H ;
  993. rt [fix [i].place] := VAL (BYTE, rel)
  994. ELSE
  995. rel := (t + 10000H - (fix [i].place + 2)) MOD 10000H ;
  996. rt [fix [i].place] := VAL (BYTE, rel MOD 100H) ;
  997. rt [fix [i].place + 1] := VAL (BYTE, (rel DIV 100H) MOD 100H)
  998. END
  999. END ;
  1000. INC (i)
  1001. END
  1002. END FixUp ;
  1003. (* ---------------------------------------------------------------- *)
  1004. (* public interface *)
  1005. (* ---------------------------------------------------------------- *)
  1006. PROCEDURE RT_Build (base : CARDINAL) ;
  1007. BEGIN
  1008. IF built THEN
  1009. RETURN
  1010. END ;
  1011. rtBase := base ; (* must precede every Emit*, they read it *)
  1012. rpos := 0 ; ltop := 0 ; nfix := 0 ; dataAt := 0 ;
  1013. EmitInitMem ;
  1014. EmitEnd ;
  1015. EmitStackChk ;
  1016. EmitWrInt ; EmitWrChar ; EmitWrBool ; EmitWrReal ; EmitWrLn ;
  1017. EmitWrInl ; EmitWrStr ;
  1018. EmitRdInt ; EmitRdScan ; EmitRdByte ;
  1019. EmitRdChar ; EmitRdBool ; EmitRdLn ;
  1020. EmitGetCh ; EmitUnGetCh ;
  1021. EmitData ;
  1022. FixUp ;
  1023. SetStr (entNm [0], "initmem") ;
  1024. SetStr (entNm [1], "progend") ;
  1025. SetStr (entNm [2], "stackchk") ;
  1026. SetStr (entNm [3], "wrint") ;
  1027. SetStr (entNm [4], "wrchar") ;
  1028. SetStr (entNm [5], "wrbool") ;
  1029. SetStr (entNm [6], "wrreal") ;
  1030. SetStr (entNm [7], "wrln") ;
  1031. SetStr (entNm [8], "rdint") ;
  1032. SetStr (entNm [9], "rdchar") ;
  1033. SetStr (entNm [10], "rdbool") ;
  1034. SetStr (entNm [11], "rdln") ;
  1035. SetStr (entNm [12], "halt") ;
  1036. SetStr (entNm [13], "wrtinl") ;
  1037. SetStr (entNm [14], "rdbyte") ;
  1038. SetStr (entNm [15], "wrstr") ;
  1039. built := TRUE
  1040. END RT_Build ;
  1041. PROCEDURE RT_Size () : CARDINAL ;
  1042. BEGIN
  1043. IF NOT built THEN
  1044. RT_Build (0)
  1045. END ;
  1046. RETURN rpos
  1047. END RT_Size ;
  1048. PROCEDURE RT_Byte (i : CARDINAL ) : BYTE ;
  1049. BEGIN
  1050. IF NOT built THEN
  1051. RT_Build (0)
  1052. END ;
  1053. IF i >= rpos THEN
  1054. RETURN 0
  1055. END ;
  1056. RETURN rt [i]
  1057. END RT_Byte ;
  1058. PROCEDURE RT_Entry (i : CARDINAL ) : CARDINAL ;
  1059. BEGIN
  1060. IF NOT built THEN
  1061. RT_Build (0)
  1062. END ;
  1063. IF i > 15 THEN
  1064. RETURN 0
  1065. END ;
  1066. RETURN rtBase + LblOff (entNm [i])
  1067. END RT_Entry ;
  1068. PROCEDURE RT_CodeEnd () : CARDINAL ;
  1069. BEGIN
  1070. IF NOT built THEN
  1071. RT_Build (0)
  1072. END ;
  1073. RETURN dataAt
  1074. END RT_CodeEnd ;
  1075. END Runtime.