	;;;	---------------------------------------------------------------
	;;;	SUBLEQ for CP/M. The word size is 16 bits, and the program
	;;;	is given 16 Kwords (32 KB) of memory. (If the system doesn't
	;;;	have enough, the program will not run.)
	;;;	I/O is via the console; since it cannot normally be redirected,
	;;;	CR/LF translation is on by default. It can be turned off with
	;;;	the 'R' switch.
	;;;	---------------------------------------------------------------
	;;;	CP/M system calls
getch:	equ	1h
putch:	equ	2h
puts:	equ	9h
fopen:	equ	0Fh
fread:	equ	14h
	;;;	RAM locations
fcb1:	equ	5ch	; FCB 1 (automatically preloaded with 1st file name)
fcb2:	equ	6ch	; FCB 2 (we're abusing this one for the switch)
dma:	equ	80h	; default DMA is located at 80h
bdos:	equ	5h 	; CP/M entry point
memtop:	equ	6h	; First reserved memory address (below this is ours)
	;;;	Constants
CR:	equ	13	; CR and LF
LF:	equ	10
EOF:	equ	26	; EOF marker (as we don't have exact filesizes)
MSTART:	equ	2048	; Reserve 2K of memory for this program + the stack
MSIZE:	equ	32768	; Reserve 32K of memory (16Kwords) for the SUBLEQ code
PB:	equ	0C6h	; PUSH B opcode. 
	org	100h
	;;;	-- Memory initialization --------------------------------------
	;;;	The fastest way to zero out a whole bunch of memory on the 8080
	;;;	is to push zeroes onto the stack. Since we need to do 32K,
	;;;	and it's slow already to begin with, let's do it that way.
	lxi	d,MSTART+MSIZE	; Top address we need
	lhld	memtop		; See if we even have enough memory
	call	cmp16		; Compare the two
	xchg			; Put top address in HL
	lxi	d,emem		; Memory error message
	jnc	die		; If there isn't enough memory, stop.
	sphl			; Set the stack pointer to the top of memory
	lxi	b,0 		; 2 zero bytes to push
	xra	a		; Zero out A.
	;;;	Each PUSH pushes 2 zeroes. 256 * 64 * 2 = 32768 zeroes. 
	;;;	In the interests of "speedy" (ha!) execution, let's unroll this
	;;;	loop a bit. In the interest of the reader, let's not write out
	;;;	64 lines of "PUSH B". 'PB' is set to the opcode for PUSH B, and
	;;;	4*16=64. This costs some memory, but since we're basically
	;;;	assuming a baller >48K system anyway to run any non-trivial
	;;;	SUBLEQ code (ha!), we can spare the 64 bytes. 
memini:	db	PB,PB,PB,PB, PB,PB,PB,PB, PB,PB,PB,PB, PB,PB,PB,PB
	db	PB,PB,PB,PB, PB,PB,PB,PB, PB,PB,PB,PB, PB,PB,PB,PB
	db	PB,PB,PB,PB, PB,PB,PB,PB, PB,PB,PB,PB, PB,PB,PB,PB	
	db	PB,PB,PB,PB, PB,PB,PB,PB, PB,PB,PB,PB, PB,PB,PB,PB
	inr	a		; This will loop around 256 times
	jnz	memini
	push	b
	;;;	This conveniently leaves SP pointing just below SUBLEQ memory.
	;;;	-- Check the raw switch ---------------------------------------
	;;;	CP/M conveniently parses the command line for us, under the
	;;;	assumption that there are two whitespace-separated filenames,
	;;;	which are also automatically made uppercase.
	;;;	We only have to see if the second filename starts with 'R'.
	lda	fcb2+1 		; Filename starts at offset 1 in the FCB
	cpi	'R'		; Is it 'R'?
	jnz	readfl		; If not, go read the file (in FCB1).
	lxi	h,chiraw	; If so, rewrite the jumps to use the raw fns
	shld	chin+1
	lxi	h,choraw
	shld	chout+1
	;;;	-- Parse the input file ---------------------------------------
	;;;	The input file should consist of signed integers written in
	;;;	decimal, separated by whitespace. (For simplicity, we'll call
	;;;	all control characters whitespace). CP/M can only read files
	;;;	128 bytes at a time, so we'll process it 128 bytes at a time
	;;;	as well.
readfl:	lda	fcb1+1		; See if a file was given
	cpi	' '		; If not, the filename will be empty (spaces)
	lxi	d,eusage	; Print the usage string if that is the case
	jz	die 
	mvi	c,fopen		; Otherwise, try to open the file.
	lxi	d,fcb1
	call	bdos
	inr	a		; FF is returned on error
	lxi	d,efile		; Print 'file error' and stop.
	jz	die
	;;;	Start parsing 16-bit numbers
	lxi	h,MSTART	; Start of SUBLEQ memory
	push	h		; Keep that on the stack
skipws:	call	fgetc		; Get character from file
	jc	rddone		; If EOF, we're done
	cpi	' '+1 		; Is it whitespace?
	jc	skipws		; Then get next character
rdnum:	lxi	h,0		; H = accumulator to store the number
	mov	b,h		; Set B if number should be negative.
	cpi	'-'		; Did we read a minus sign?
	jnz	rddgt		; If not, then this should be a digit.
	inr	b		; But if so, set B,
	call	fgetc		; and get the next character.
	jc	rddone
rddgt:	sui	'0'		; Make ASCII digit
	cpi	10 		; Which should now be less than 10
	jnc	fmterr		; Otherwise, print an error and stop
	mov	d,h		; Set HL=HL*10
	mov	e,l		; DE = HL 
	dad	h		; HL *= 2
	dad	h		; HL *= 4
	dad	d 		; HL *= 5
	dad	h 		; HL *= 10
	mvi	d,0		; Add in the digit
	mov	e,a
	dad 	d
	call	fgetc		; Get next character
	jc	rdeof		; EOF while reading number
	cpi	' '+1 		; Is it whitespace?
	jnc	rddgt		; If not, then it should be the next digit
	xchg			; If so, write the number to SUBLEQ memory
	pop	h 		; Number in DE and pointer in HL
	call	wrnum		; Write the number
	push	h		; Put the pointer back
	jmp	skipws		; Then skip to next number and parse it
rdeof:	xchg			; EOF, but we still have a number to write
	pop	h		; Number in DE and pointer in HL
	call	wrnum		; Write the number
	push	h
rddone:	pop	h		; We're done, discard pointer
	;;;	-- Run the SUBLEQ code ----------------------------------------
	lxi	h,MSTART	; Initialize IP
	;;; 	At the start of step, HL = IP (in system memory)
step:	mov	e,m		; Load A into DE
	inx	h
	mov	d,m
	inx	h
	mov	c,m		; Load B into BC
	inx	h
	mov	b,m
	inx	h
	mov	a,e		; Check if A=-1
	ana	d
	inr	a
	jz 	sbin		; If so, read input
	mov	a,b		; Otherwise, check if B=-1
	ana	c
	inr	a
	jz	sbout		; If so, write output
	;;;	Perform the SUBLEQ instruction
	push	h		; Store the IP (-2) on the stack
	mov	a,d		; Obtain [A] (set DE=[DE])
	ani	3Fh		; Make sure address is in 16K words
	mov	d,a
	lxi	h,MSTART	; Add to start address twice
	dad	d		; (SUBLEQ addresses words, we're addressing
	dad 	d		; bytes)
	mov	e,m		; Load low byte
	inx	h
	mov	d,m		; Load high byte
	mov	a,b		; Obtain [B] (set BC=[BC])
	ani	3Fh		; This adress should also be in the 16K words
	mov	b,a
	lxi	h,MSTART	; Add to start address twice, again
	dad	b
	dad	b
	mov	c,m		; Load low byte
	inx	h
	mov	b,m		; Load high byte
	mov	a,c		; BC (B) -= DE (A)
	sub	e		; Subtract low bytes
	mov	c,a
	mov	a,b		; Subtract high bytes
	sbb	d
	mov	b,a
	mov	m,b		; HL is still pointing to the high byte of [B]
	dcx	h
	mov	m,c		; Store the low byte back too
	pop	h		; Restore IP
	ral			; Check sign bit of [B] (which is still in A)
	jc	sujmp		; If set, it's negative, and we need to jump
	rar
	ora	c		; If we're still here, it wasn't set. OR with
	jz	sujmp		; low bit, if zero then we also need to jump
	inx	h 		; We don't need to jump, so we should ignore C;
	inx 	h		; increment the IP to advance past it.
	jmp	step		; Next step
sujmp:	mov	c,m		; We do need to jump, load BC=C
	inx	h
	mov	a,m		; High byte into A
	ral			; See if it is negative
	jc	quit		; If so, stop
	rar
	ani	3Fh		; Don't jump outside the address space
	mov	b,a		; High byte into B
	lxi	h,MSTART	; Calculate new IP
	dad	b
	dad	b
	jmp	step		; Do next step
	;;;	Input: A=-1
sbin:	inx	h		; Advance IP past C
	inx	h
	xchg			; IP in DE
	mov	a,b		; Calculate address for BC (B)
	ani	3Fh
	mov	b,a
	lxi	h,MSTART	
	dad	b
	dad	b
	call	chin		; Read character
	mov	m,a		; Store in low byte
	inx	h
	mvi	m,0 		; Store zero in high byte
	xchg			; IP back in HL
	jmp	step		; Next step
	;;;	Output: B=-1
sbout:	inx	h		; Advance IP past C
	inx	h
	xchg			; IP in DE and A in HL
	mov	a,h		; Calculate address for A
	ani	3Fh
	mov	h,a
	dad	h
	lxi	b,MSTART
	dad	b
	mov	a,m		; Retrieve low byte (character)
	call	chout		; Write character
	xchg			; IP back in HL
	jmp	step		; Next step 
quit:	rst	0
	;;;	-- Write number to SUBLEQ memory ------------------------------
	;;;	Assuming: DE holds the number, B=1 if number should be negated,
	;;;	HL holds the pointer to SUBLEQ memory.
wrnum:	dcr	b		; Should the number be negated?
	jnz	wrpos		; If not, just write it
	dcx	d		; Otherwise, negate it: decrement,
	mov	a,e		; Then complement low byte,
	cma
	mov	e,a
	mov	a,d		; Then complement high byte
	cma
	mov	d,a		; And then write it
wrpos:	mov	m,e		; Write low byte
	inx	h		; Advance pointer
	mov	m,d		; Write high byte
	inx	h		; Advance pointer
	ret 
	;;;	-- Read file byte by byte -------------------------------------
	;;;	The next byte from the file in FCB1 is returned in A, and all
	;;;	other registers are preserved. When 128 bytes have been read,
	;;;	the next record is loaded automatically. Carry set on EOF.
fgetc:	push	h		; Keep HL registers
	lda	fgptr		; Where are we in the record?
	ana	a		 
	jz	nxtrec		; If at 0 (rollover), load new record.
frecc:	mvi	h,0		; HL = A
	mov	l,a 		
	inr	a		; Next A
	sta	fgptr 		; Write A back
	mov	a,m		; Retrieve byte
	pop	h		; Restore HL registers
	cpi	EOF		; Is it EOF?
	rnz			; If not, we're done (ANA clears carry)
	stc			; But otherwise, set carry
	ret
nxtrec:	push	d		; Keep the other registers too
	push	b
	mvi	c,fread		; Read record from file
	lxi	d,fcb1
	call	bdos
	dcr	a		; A=1 on EOF
	jz	fgeof	
	inr	a		; A<>0 = error
	lxi	d,efile
	jnz	die
	mvi	a,80h		; If we're still here, record read correctly
	sta	fgptr		; Set pointer back to beginning of DMA.
	pop	b		; Restore B and D 
	pop	d
	jmp	frecc		; Get first character from the record.
fgeof:	stc			; On EOF (no more records), set carry 
	jmp	resbdh		; And restore the registers 
fgptr:	db 	0		; Pointer (80h-FFh) into DMA area. Reload on 0.
	;;;	-- Compare DE to HL -------------------------------------------
cmp16:	mov	a,d		; Compare high bytes
	cmp	h
	rnz			; If they are not equal, we know the ordering
	mov	a,e		; If they are equal, compare lower bytes
	cmp 	l
	ret 
	;;;	-- Register-preserving I/O routines ---------------------------
chin:	jmp	chitr		; These are rewritten to jump to the raw I/O
chout:	jmp	chotr		; instructions to turn translation off.
	;;;	-- Read character into A with translation ---------------------
chitr:	call	chiraw		; Get raw character
	cpi	CR		; Is it CR? 
	rnz			; If not, return character unchanged
	mvi	a,LF		; Otherwise, return LF (terminal sends only CR)
	ret
	;;;	-- Read character into A. -------------------------------------
chiraw:	push	h		; Save all registers except A
	push	d
	push	b
	mvi 	c,getch		; Get character from terminal
	call	bdos		; Character ends up in A
	jmp	resbdh		; Restore registers afterwards
	;;;	-- Write character in A to terminal with translation ----------
chotr:	cpi	LF		; Is it LF?
	jnz	choraw		; If not, just print it
	mvi	a,CR		; Otherwise, print a CR first,
	call	choraw
	mvi	a,LF		; And then a LF. (fall through)
	;;;	-- Write character in A to terminal ---------------------------
choraw:	push	h		; Store all registers
	push	d
	push	b
	push	psw		
	mvi	c,putch		; Write character to terminal
	mov	e,a
	call	bdos
	;;;	-- Restore registers ------------------------------------------
restor:	pop	psw		; Restore all registers
resbdh:	pop	b		; Restore B D H
	pop	d
	pop	h
	ret
	;;;	-- Make parse error message and stop --------------------------
	;;;	A should hold the offending character _after_ '0' has already
	;;; 	been subtracted.
fmterr:	adi	'0'		; Undo subtraction of ASCII 0
	lxi	h,eiloc		; Write the characters in the error message
	mov	m,a
	inx	h
	mvi	b,4		; Max. 4 more characters
fmtelp:	call	fgetc		; Get next character
	jc	fmtdne		; If EOF, stop
	mov	m,a		; If not, store the character
	inx	h		; Advance pointer
	dcr	b		; Should we do more characters?
	jnz	fmtelp		; If so, go get another
fmtdne:	lxi	d,einv		; Print 'invalid integer' error message.
	;;;	-- Print an error message and stop ----------------------------
die:	mvi	c,puts
	call	bdos
	rst	0
	;;;	-- Error messages ---------------------------------------------
eusage:	db	'SUBLEQ <file> [R]: Run the SUBLEQ program in <file>.$'
efile:	db	'File error$'	
emem:	db	'Memory error$'
einv:	db	'Invalid integer: '
eiloc:	db	'     $'