From 2ddca57f3fc9c79b4d41d46fdc2076a71ca30fc6 Mon Sep 17 00:00:00 2001
From: Virgil Dupras <hsoft@hardcoded.net>
Date: Tue, 10 Mar 2020 16:02:40 -0400
Subject: [PATCH] forth: add string and logic routines, as well as "RECURSE"

The goal was to be able to implement "(" in forth, but I realised that my
INTERPRET approach was wrong. Compiling the line beforehand is, after all,
not good. I'll have to change it again.
---
 apps/forth/dict.asm       | 115 +++++++++++++++++++++++++++++++++++++++++++---
 apps/forth/dictionary.txt |  13 ++++++
 apps/forth/main.asm       |   2 +-
 apps/forth/util.asm       |  96 +++++++++++++++++++++++++++++---------
 apps/lib/util.asm         |   2 +-
 5 files changed, 198 insertions(+), 30 deletions(-)

diff --git a/apps/forth/dict.asm b/apps/forth/dict.asm
index 99fdea3..cbff0e6 100644
--- a/apps/forth/dict.asm
+++ b/apps/forth/dict.asm
@@ -282,9 +282,7 @@ DEFINE:
 	; is lit
 	ldi
 	ldi
-	inc	hl \ inc hl
 	call	strcpyM
-	inc	hl		; byte after word termination
 	jr	.loop
 .notLIT:
 	; it's a word
@@ -309,7 +307,7 @@ DEFINE:
 	; a good old regular word. We have 2 bytes to copy. But before we do,
 	; let's check whether it's an EXIT. LDI doesn't affect Z, so we can
 	; make our jump later.
-	call	HLPointsEXIT
+	call	HLPointsEXITQUIT
 	ldi
 	ldi
 	jr	nz, .loop
@@ -487,10 +485,20 @@ FETCH:
 	push	hl
 	jp	exit
 
+; ( -- a )
+	.db "LIT@"
+	.fill 4
+	.dw FETCH
+LITFETCH:
+	.dw nativeWord
+	call	readLITTOS
+	push	hl
+	jp	exit
+
 ; ( a b -- b a )
 	.db "SWAP"
 	.fill 4
-	.dw FETCH
+	.dw LITFETCH
 SWAP:
 	.dw nativeWord
 	pop	hl
@@ -571,9 +579,36 @@ DIV:
 	push	bc
 	jp	exit
 
+; ( a1 a2 -- b )
+	.db "SCMP"
+	.fill 4
+	.dw DIV
+SCMP:
+	.dw nativeWord
+	pop	de
+	pop	hl
+	call	strcmp
+	call	flagsToBC
+	push	bc
+	jp	exit
+
+; ( n1 n2 -- f )
+	.db "CMP"
+	.fill 5
+	.dw SCMP
+CMP:
+	.dw nativeWord
+	pop	hl
+	pop	de
+	or	a	; clear carry
+	sbc	hl, de
+	call	flagsToBC
+	push	bc
+	jp	exit
+
 	.db "IF"
 	.fill 6
-	.dw DIV
+	.dw CMP
 IF:
 	.dw ifWord
 
@@ -589,13 +624,25 @@ ELSE:
 THEN:
 	.dw thenWord
 
+	.db "RECURSE"
+	.db 0
+	.dw THEN
+RECURSE:
+	.dw nativeWord
+	call	popRS
+	ld	l, (ix)
+	ld	h, (ix+1)
+	dec	hl \ dec hl
+	push	hl \ pop iy
+	jp	compiledWord
+
 ; End of native words
 
 ; ( a -- )
 ; @ .
 	.db "?"
 	.fill 7
-	.dw THEN
+	.dw RECURSE
 FETCHDOT:
 	.dw compiledWord
 	.dw FETCH
@@ -654,3 +701,59 @@ CONSTANT:
 	.dw DOES
 	.dw FETCH
 	.dw EXIT
+
+; ( f -- f )
+; IF 0 ELSE 1 THEN
+	.db "NOT"
+	.fill 5
+	.dw CONSTANT
+NOT:
+	.dw compiledWord
+	.dw IF
+	.dw NUMBER
+	.dw 0
+	.dw ELSE
+	.dw NUMBER
+	.dw 1
+	.dw THEN
+	.dw EXIT
+
+; ( n1 n2 -- f )
+; CMP NOT
+	.db "="
+	.fill 7
+	.dw NOT
+EQ:
+	.dw compiledWord
+	.dw CMP
+	.dw NOT
+	.dw EXIT
+
+; ( n1 n2 -- f )
+; CMP -1 =
+	.db "<"
+	.fill 7
+	.dw EQ
+LT:
+	.dw compiledWord
+	.dw CMP
+	.dw NUMBER
+	.dw -1
+	.dw EQ
+	.dw EXIT
+
+; ( n1 n2 -- f )
+; CMP 1 =
+	.db ">"
+	.fill 7
+	.dw LT
+GT:
+LATEST:
+	.dw compiledWord
+	.dw CMP
+	.dw NUMBER
+	.dw 1
+	.dw EQ
+	.dw EXIT
+
+;
diff --git a/apps/forth/dictionary.txt b/apps/forth/dictionary.txt
index c2562fb..1f38a65 100644
--- a/apps/forth/dictionary.txt
+++ b/apps/forth/dictionary.txt
@@ -43,6 +43,7 @@ IF          n --            Branch to ELSE or THEN if n is zero
 INTERPRET   --              Get a line from stdin, compile it in tmp memory,
                             then execute the compiled contents.
 QUIT        R:drop --       Return to interpreter promp immediately
+RECURSE     R:I -- R:I-2    Run the current word again.
 THEN        --              Does nothing. Serves as a branching merker for IF
                             and ELSE.
 
@@ -66,6 +67,18 @@ HERE        -- a            Push HERE's address
 *           a b -- c        a * b -> c
 /           a b -- c        a / b -> c
 
+*** Logic ***
+=           n1 n2 -- f      Push true if n1 == n2
+<           n1 n2 -- f      Push true if n1 < n2
+>           n1 n2 -- f      Push true if n1 > n2
+CMP         n1 n2 -- n      Compare n1 and n2 and set n to -1, 0, or 1.
+                            n=0: a1=a2. n=1: a1>a2. n=-1: a1<a2.
+NOT         f -- f          Push the logical opposite of f
+
+*** Strings ***
+LIT@ x      -- a            Read folloing LIT and push its addr to a
+S=          a1 a2 -- n      Compare strings a1 and a2. See CMP
+
 *** I/O ***
 .           n --            Print n in its decimal form
 EMIT        c --            Spit char c to stdout
diff --git a/apps/forth/main.asm b/apps/forth/main.asm
index 3042579..ab8cb61 100644
--- a/apps/forth/main.asm
+++ b/apps/forth/main.asm
@@ -27,7 +27,7 @@ forthMain:
 	; we check for stack underflow.
 	push	af \ push af \ push af
 	ld	(INITIAL_SP), sp
-	ld	hl, CONSTANT		; last entry in hardcoded dict
+	ld	hl, LATEST
 	ld	(CURRENT), hl
 	ld	hl, FORTH_RAMEND
 	ld	(HERE), hl
diff --git a/apps/forth/util.asm b/apps/forth/util.asm
index 03282e9..18c7e50 100644
--- a/apps/forth/util.asm
+++ b/apps/forth/util.asm
@@ -69,10 +69,14 @@ HLPointsLIT:
 	pop	de
 	ret
 
-HLPointsEXIT:
+HLPointsEXITQUIT:
 	push	de
 	ld	de, EXIT
 	call	HLPointsDE
+	jr	z, .end
+	ld	de, QUIT
+	call	HLPointsDE
+.end:
 	pop	de
 	ret
 
@@ -98,21 +102,22 @@ compSkip:
 	inc	hl \ inc hl
 	ret
 
+; ***readLIT***
 ; The goal of this routine is to read a string literal following the currently
 ; executed words. For example, CREATE and DEFINE need this. Things are a little
 ; twisted, so bear with me while I explain how it works.
 ;
 ; When we call this routine, everything has been compiled. We're on an atom and
 ; we're executing it. Now, we're looking for a string literal or a word-with-a
-; name that follows our readCompWord caller. We could think that this word is
-; right there on RS' TOS, but no! You have to account for words wrapping the
-; caller. For example, "VARIABLE" calls "CREATE". If you call "VARIABLE foo",
-; if CREATE looks at what follows in RS' TOS, it will only find the "2" in
-; "CREATE 2 ALLOT".
+; name that follows our readLIT caller. We could think that this word is
+; right there on RS' TOS, but not always! You have to account for words wrapping
+; the caller. For example, "VARIABLE" calls "CREATE". If you call
+; "VARIABLE foo", if CREATE looks at what follows in RS' TOS, it will only find
+; the "2" in "CREATE 2 ALLOT".
 ;
-; Therefore, we actually need to check in RS' *bottom of stack* for our answer.
-; If that atom is a LIT, we're good. We make HL point to it and advance IP to
-; byte following null-termination.
+; In this case, we actually need to check in RS' *bottom of stack* for our
+; answer. If that atom is a LIT, we're good. We make HL point to it and advance
+; IP to byte following null-termination.
 ;
 ; If it isn't, things get interesting: If it's a word reference, then it's
 ; not an invalid literal. For example, one could want to redefine an existing
@@ -123,31 +128,48 @@ compSkip:
 ; second word in our dict. We don't accept EXIT because it's the termination
 ; word. Yeah, it means that ";" can't be overridden...
 ; If name can't be read, we abort
-readCompWord:
-	; In all cases, we want RS' BOS in HL. Let's get it now.
-	ld	hl, (RS_ADDR)
+;
+; BOS vs TOS: What we cover so far is the "CREATE" and friends cases, where we
+; want to read BOS. There are, however, cases where we want to read TOS, that is
+; that we want to read the LIT right next to our atom. Example: "(". When
+; processing comments, we are at compile time and want to read words from BOS,
+; yes), however, in "("'s definition, there's "LIT@ )", which means "fetch LIT
+; next to me and push this to stack". This LIT we want to fetch is *not* from
+; BOS, it's from TOS.
+;
+; This is why we have readLITBOS and readLITTOS. readLIT uses HL and DE and is
+; not used directly.
+
+; Given a RS stack pointer HL, read LIT next to it (or abort) and set HL to
+; point to its associated string. Set DE to there the RS stack pointer should
+; point next.
+readLIT:
 	call	HLPointsLIT
 	jr	nz, .notLIT
 	; RS BOS is a LIT, make HL point to string, then skip this RS compword.
 	inc	hl \ inc hl	; HL now points to string itself
-	push	hl		; --> lvl 1, our result
+	; HL has our its final value
+	ld	d, h
+	ld	e, l
 	call	strskip
 	inc	hl		; byte after word termination
-	ld	(RS_ADDR), hl
-	pop	hl		; <-- lvl 1, our result
+	ex	de, hl
 	ret
 .notLIT:
 	; Alright, not a literal, but is it a word? If it's not a number, then
 	; it's a word.
 	call	HLPointsNUMBER
 	jr	z, .notWord
+	call	HLPointsEXITQUIT
+	jr	z, .notWord
 	; Not a number, then it's a word. Copy word to pad and point to it.
+	push	hl		; --> lvl 1. we need it to set DE later
 	call	intoHL
 	or	a		; clear carry
 	ld	de, CODELINK_OFFSET
 	sbc	hl, de
 	; That's our return value
-	push	hl		; --> lvl 1
+	push	hl		; --> lvl 2
 	; HL now points to word offset, let'd copy it to pad
 	ex	de, hl
 	call	pad
@@ -157,10 +179,10 @@ readCompWord:
 	; null-terminate
 	xor	a
 	ld	(de), a
-	; Advance RS' BOS by 2
-	ld	hl, RS_ADDR
-	inc	(hl) \ inc (hl)
-	pop	hl		; <-- lvl 1
+	pop	hl		; <-- lvl 2
+	pop	de		; <-- lvl 1
+	; Advance IP by 2
+	inc	de \ inc de
 	ret
 .notWord:
 	ld	hl, .msg
@@ -169,6 +191,24 @@ readCompWord:
 .msg:
 	.db "word expected", 0
 
+readLITBOS:
+	push	de
+	ld	hl, (RS_ADDR)
+	call	readLIT
+	ld	(RS_ADDR), de
+	pop	de
+	ret
+
+readLITTOS:
+	push	de
+	ld	l, (ix)
+	ld	h, (ix+1)
+	call	readLIT
+	ld	(ix), e
+	ld	(ix+1), d
+	pop	de
+	ret
+
 ; For DE being a wordref, move DE to the previous wordref.
 ; Z is set if DE point to 0 (no entry). NZ if not.
 prev:
@@ -238,7 +278,7 @@ compile:
 	; When encountering an undefined word during compilation, we spit a
 	; reference to litWord, followed by the null-terminated word.
 	; This way, if a preceding word expect a string literal, it will read it
-	; by calling readCompWord, and if it doesn't, the routine will be
+	; by calling readLIT, and if it doesn't, the routine will be
 	; called, triggering an abort.
 	ld	hl, LIT
 	call	wrCompHL
@@ -256,7 +296,7 @@ compile:
 ; Spit name + prev in (HERE) and adjust (HERE) and (CURRENT)
 ; HL points to new (HERE)
 entryhead:
-	call	readCompWord
+	call	readLITBOS
 	ld	de, (HERE)
 	call	strcpy
 	ex	de, hl		; (HERE) now in HL
@@ -291,3 +331,15 @@ HLPointsIMMED:
 	inc	hl
 	pop	hl
 	ret
+
+; Checks flags Z and C and sets BC to 0 if Z, 1 if C and -1 otherwise
+flagsToBC:
+	ld	bc, 0
+	ret	z	; equal
+	inc	bc
+	ret	c	; >
+	; <
+	dec	bc
+	dec	bc
+	ret
+
diff --git a/apps/lib/util.asm b/apps/lib/util.asm
index 386f990..613b860 100644
--- a/apps/lib/util.asm
+++ b/apps/lib/util.asm
@@ -52,7 +52,7 @@ strcpy:
 	ret
 
 ; Compares strings pointed to by HL and DE until one of them hits its null char.
-; If equal, Z is set. If not equal, Z is reset.
+; If equal, Z is set. If not equal, Z is reset. C is set if HL > DE
 strcmp:
 	push	hl
 	push	de