Claude and I Vibe Coded a Forth Interpreter

Topics relating to various Forth models on the 6502, 65816, and related microprocessors and microcontrollers.
Post Reply
Martin_H
Posts: 837
Joined: 08 Jan 2014

Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

My prompt was an ANS Forth for the 65816 with the following requirements:
* 32 KB RAM, 32 KB ROM with the Forth kernel in ROM.
* A serial UART used for the console.
* An ITC threading model.
* 16-bit cell size.

I chose the ITC threading model because if it worked, I planned to extend it with Garth's zero overheard Forth interrupt code.
I chose a 16-bit cell size because that's usually fine for embedded systems. I once used a 32 bit Forth on a microcontroller I swear I didn't use an integer larger than 4096.

At the end of the session Claude produced a zip file which I extracted, and the code didn't assemble and probably doesn't work. But I uploaded to GitHub anyway: https://github.com/Martin-H1/65816/tree/main/Forth816

A few things about the session impressed me:
* First, it produced a memory map, register conventions, and dictionary format that all seemed reasonable.
* Second, it claimed knowledge of Fig-Forth and was using it as a basis.
* Third, the assembly didn't look terrible and maybe a line-by-line code review could produce something useful.
* Fourth, at no point was I tempted to say, "Go home AI, you're drunk". Which has happened to me frequently with other LLM's I've tried.
Martin_H
Posts: 837
Joined: 08 Jan 2014

Re: Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

Interesting things so far:
* The code formatting is unusual. It's massively indented with spaces. I'm fixing that as I review it.
* It uses upper case letters for assembly mnemonics. That strikes me as old school, and I'm changing that.
* The comments are generally good. But I found one typo so far. It was a bit jarring because I wondered why here?
* It doesn't have a NEXT loop; it uses a macro to append NEXT to every dictionary entry. I've seen that in microcode, but not a FORTH system.
* It misses optimization to avoid sep and rep instructions. For example, I would replace this code segment:

Code: Select all

	; --- Set Data Bank to $00 ---
	sep #$20
	.a8
	lda #$00
	pha
	plb			; DB = $00
	rep #$20
	.a16
with this:

Code: Select all

	; --- Set Data Bank to $00 ---
	pea $0000
	plb			; pop extra zero off stack
	plb			; DB = $00
* It has inexplicable code like the following:

Code: Select all

	; --- User area: STATE = 0 (interpret) ---
	lda #UP_BASE + U_STATE
	sta SCRATCH0
	stz (SCRATCH0)
No stz indirect exists, but even if one did, why statically load a pointer and use an indirect? Why not do this:

Code: Select all

	; --- User area: STATE = 0 (interpret) ---
	stz UP_BASE + U_STATE
* Unless I'm missing something, even if the code is valid, the indirection is unneeded. For example:

Code: Select all

	; --- User area: DP = DICT_BASE ---
	lda #UP_BASE + U_DP
	sta SCRATCH0
	lda #DICT_BASE
	sta (SCRATCH0)
should be:

Code: Select all

	; --- User area: DP = DICT_BASE ---
	lda #DICT_BASE
	sta UP_BASE + U_DP
* Another typo which uses the low byte operator to load the CFA, storing it in scratch and jumping indirect.

Code: Select all

	; --- Jump to QUIT (outer interpreter) ---
	; Load CFA of QUIT and execute it
	lda #<QUIT_CFA		; CFA of QUIT word
	sta W
	lda (W)			; Fetch code pointer
	sta SCRATCH0
	jmp (SCRATCH0)
After fixing all of these issue forth.s assembles cleanly. It was 286 lines and took about two hours to review. I think it could take about 40 hours to review all the source. That's less time than writing it from scratch. So this could be a win.
Martin_H
Posts: 837
Joined: 08 Jan 2014

Re: Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

The file primitives.s is going to be a much bigger effort. It's 3344 lines long, and there's 75 errors when trying to assemble it. 10 of those errors are the use of the invalid op code BGE. I'm starting at line 1 and reviewing each function. So far, the basic stack manipulation looks reasonable. But there's weird code like this:

Code: Select all

;------------------------------------------------------------------------------
; DEPTH ( -- n ) number of items on parameter stack
;------------------------------------------------------------------------------
	HEADER "DEPTH", DEPTH_CFA, 0, TWOOVER_CFA
	CODEPTR DEPTH_CODE
.proc DEPTH_CODE
	.a16
	.i16
	txa
	eor #$FFFF		; Two's complement
	inc
	clc
	adc #$03FF		; PSP_INIT - result / 2
	lsr			; Divide by 2 (cells)
	dex
	dex
	sta 0,X
	NEXT
.endproc
It's like it forgot the subtract instruction or the PSP_INIT symbols exists but still understands these concepts. I think this code is clearer and SCRATCH0 is in the direct page:

Code: Select all

;------------------------------------------------------------------------------
; DEPTH ( -- n ) number of items on parameter stack
;------------------------------------------------------------------------------
	HEADER "DEPTH", DEPTH_CFA, 0, TWOOVER_CFA
	CODEPTR DEPTH_CODE
.proc DEPTH_CODE
	.a16
	.i16
	stx SCRATCH0
	lda #$03FF		; (PSP_INIT - result) / 2
	sec
	sbc SCRATCH0
	lsr			; Divide by 2 (cells)
	dex
	dex
	sta 0,X
	NEXT
.endproc
The pick code contains some do nothing sections and needs a rewrite. Interestingly it realized this partway through and started writing the correct code.

Code: Select all

;------------------------------------------------------------------------------
; PICK ( xu...x1 x0 u -- xu...x1 x0 xu )
;------------------------------------------------------------------------------
	HEADER "PICK", PICK_CFA, 0, DEPTH_CFA
	CODEPTR PICK_CODE
.proc PICK_CODE
	.a16
	.i16
	lda 0,X			; u
	inc			; u+1 (skip u itself)
	asl			; * 2 (cell size)
	tay			; offset
	lda 0,X			; re-use slot
	lda 0,Y			; Fetch xu  (X+offset)
	; Note: can't use X+Y directly, use explicit index
	; Actually: stack[u+1] = X + (u+1)*2
	; Recalculate properly:
	stx SCRATCH0
	lda 0,X			; u
	inc
	asl
	clc
	adc SCRATCH0		; X + (u+1)*2
	sta SCRATCH0
	lda (SCRATCH0)		; Fetch xu
	sta 0,X			; Replace u with xu
	NEXT
.endproc
This looks like the correct code:

Code: Select all

;------------------------------------------------------------------------------
; PICK ( xu...x1 x0 u -- xu...x1 x0 xu )
;------------------------------------------------------------------------------
	HEADER "PICK", PICK_CFA, 0, DEPTH_CFA
	CODEPTR PICK_CODE
.proc PICK_CODE
	.a16
	.i16
	stx SCRATCH0
	lda 0,X			; u
	inc			; u+1 (skip u itself)
	asl			; * 2 (cell size)
	clc
	adc SCRATCH0		; X + (u+1)*2
	sta SCRATCH0
	lda (SCRATCH0)		; Fetch xu
	sta 0,X			; Replace u with xu
	NEXT
.endproc
Martin_H
Posts: 837
Joined: 08 Jan 2014

Re: Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

I've officially hit the "go home Claude, you're drunk" function. The comments are comedy gold because they illustrate how far off its thinking is from what the actual effects on the register state. Moreover, Y contains the IP, and I never saw it pushed onto the return stack in NEXT.

Code: Select all

;------------------------------------------------------------------------------
; R@ ( -- a ) (R: a -- a)
;------------------------------------------------------------------------------
	HEADER "R@", RFETCH_CFA, 0, RFROM_CFA
	CODEPTR RFETCH_CODE
.proc RFETCH_CODE
	.a16
	.i16
	; Peek at return stack without popping
	; Hardware stack: S+1,S+2 = saved IP (from DOCOL)
	; S+3,S+4 = top of return stack (>R'd value)
	tsx			; X = S (clobbers PSP!)
	; We need to save/restore X (PSP)
	; Use SCRATCH0 as temp PSP save
	stx SCRATCH0		; Save RSP in SCRATCH0
	; Saved IP is at RSP+1,RSP+2 (pushed by DOCOL)
	; R@ value is at RSP+3,RSP+4 (pushed by >R)
	lda 3,X			; Fetch R@ value
	; Restore PSP
	ldx SCRATCH0
	; Now X is RSP again - we need PSP back!
	; This approach is tricky - use alternative:
	; Pop, copy, push back
	pla			; Pop saved IP
	sta SCRATCH1
	pla			; Pop R@ value
	sta SCRATCH0
	pha			; Push R@ back
	lda SCRATCH1
	pha			; Push IP back
	lda SCRATCH0		; R@ value
	dex
	dex
	sta 0,X
	NEXT
.endproc
I think this is what it should be doing:

Code: Select all

;------------------------------------------------------------------------------
; R@ ( -- a ) (R: a -- a)
;------------------------------------------------------------------------------
	HEADER "R@", RFETCH_CFA, 0, RFROM_CFA
	CODEPTR RFETCH_CODE
.proc RFETCH_CODE
	.a16
	.i16
	; Officially this function peeks at return stack without popping.
	; Unofficially it's easier to pop and push back.
	pla			; Pop R@ value
	pha			; Push R@ back
	dex
	dex
	sta 0,X			; Push onto parameter stack
	NEXT
.endproc
After four hours work, I'm on line 332 out of 3315. About 10% done which aligns with my 40 hours estimate.
Martin_H
Posts: 837
Joined: 08 Jan 2014

Re: Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

The addition and subtraction functions look pretty good, but the multiplication and division functions have problems. Specifically, trashing the Y register which contains the IP. The fix is a simple phy and ply as seen below. I also think this code will have problems with signed values, but I will tackle that later. I think UM*. UM/MOD, and /MOD will need significant testing for this reason.

Code: Select all

;------------------------------------------------------------------------------
; * ( a b -- a*b ) 16x16 -> 16 (low word)
;------------------------------------------------------------------------------
	HEADER "*", STAR_CFA, 0, MINUS_CFA
	CODEPTR STAR_CODE
.proc STAR_CODE
	.a16
	.i16
	lda 0,X			; b (multiplier)
	sta TMPA
	lda 2,X			; a (multiplicand)
	inx
	inx			; Drop b slot
	stz 0,X			; Clear result
	phy			; save IP
	ldy #16			; 16 bit iterations
@loop:
	lsr TMPA		; Shift multiplier right
	bcc @skip
	clc
	adc 0,X			; Accumulate shifted multiplicand
	sta 0,X
@skip:
	asl			; Shift multiplicand left
	dey
	bne @loop
	;; TOS now contains the final result
	ply			; restore IP for NEXT call
	NEXT
.endproc
Martin_H
Posts: 837
Joined: 08 Jan 2014

Re: Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

I became curious why Claude kept repeating the HEADER, CODEPTR, and .proc (see below) rather than wrapping them in a macro.

Code: Select all

	HEADER "/MOD", SLASHMOD_CFA, 0, UMSLASHMOD_CFA
	CODEPTR SLASHMOD_CODE
.proc   SLASHMOD_CODE
So, I looked in macros.inc and found this at the bottom:

Code: Select all

;------------------------------------------------------------------------------
; PRIM - Full primitive word definition shorthand
;
; Creates header + code pointer, then you write the body inline.
; Ends scope with .endproc.
;
; Usage:
;   PRIM "DUP", DUP_CFA, 0, LAST_CFA
;   .proc DUP_CODE
;       ... machine code ...
;       NEXT
;   .endproc
;------------------------------------------------------------------------------

; (No combined macro for PRIM+body since ca65 can't open a .proc in a macro)
; Use HEADER + CODEPTR + .proc separately - see primitives.s for pattern.
Interesting, as that was a goal, but Claude concluded it wasn't allowed. However, I have put a .proc in a macro, so Claude was mistaken.
Martin_H
Posts: 837
Joined: 08 Jan 2014

Re: Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

I have uncovered seven instances of the dreaded BLT instruction. Another Claude hallucination, as bacon, lettuce, and tomato sandwiches aren't supported by any 65xx family CPU. This required a minor re-work of both the signed min and max functions. Fortunately, the signed comparison functions look pretty good.

So far, the various fetch and store functions are good, except store double cell which was inefficient as it incremented and decremented the pointer. I reworked it to use the pointer, then increment and use it again. Using the Y register might even be better, but I would need to preserve its contents.

Code: Select all

;------------------------------------------------------------------------------
; 2! ( d addr -- ) store double cell
;------------------------------------------------------------------------------
	HEADER "2!", TWOSTORE_CFA, 0, TWOFETCH_CFA
	CODEPTR TWOSTORE_CODE
.proc TWOSTORE_CODE
	.a16
	.i16
	lda 0,X			; pop addr to scratch pointer
	sta SCRATCH0
	inx
	inx
	lda 0,X			; pop low → addr
	sta (SCRATCH0)
	inx
	inx
	lda SCRATCH0		; increment pointer by a cell
	clc
	adc #2
	sta SCRATCH0
	lda 0,X			; pop high → addr+2
	sta (SCRATCH0)
	inx
	inx
	NEXT
.endproc
I'm 1/3 done with primitives.s and I've been at this about 13 hours. I'm keeping pace, but I was hoping to pick up speed.
Martin_H
Posts: 837
Joined: 08 Jan 2014

Re: Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

Both MOVE, FILL, and TYPE had code like the following:

Code: Select all

                LDA     0,X             ; u (byte count)
                INX
                INX
                BEQ     @done          ; Zero count = no-op
This is supposed to pop TOS and exit if zero. But the problem of course is that INX also sets the zero flag. This means the code won't work as written. It also leaves two arguments on the data stack. Even worse they both trash Y which is the IP and that means NEXT will go off into the sunset. The corrected move is as follows:

Code: Select all

;------------------------------------------------------------------------------
; MOVE ( src dst u -- ) copy u bytes from src to dst
;------------------------------------------------------------------------------
	HEADER "MOVE", MOVE_CFA, 0, TWOSTORE_CFA
	CODEPTR MOVE_CODE
.proc MOVE_CODE
	.a16
	.i16
	lda 0,X		; pop u (byte count) to TMPA
	sta TMPA
	inx
	inx
	lda 0,X		; pop dst to scratch1 ptr
	sta SCRATCH1
	inx
	inx
	lda 0,X		; pop src to scratch0 ptr
	sta SCRATCH0
	inx
	inx
	phy			; save IP
	ldy #0		; Byte-by-byte copy (MVN could be used)
	lda TMPA		; Zero count = no-op
	beq @done
@loop:	sep #$20
        .a8
	lda (SCRATCH0),Y
	sta (SCRATCH1),Y
	rep #$20
        .a16
	iny
	dec TMPA
	bne @loop
@done:	ply		; restore IP
	NEXT
.endproc
I'm not the world's greatest 65816 coder, and I keep finding erroneous code. It's better than Gemini or ChatGPT, but there's no way Claude lives up to Anthropic's claims that it's better than most human programmers. There's also no way a novice could take this code and compile and run it. I have ported several Forth's to various hardware, so I have seen other ITC implementations. So, I know what to look for.
Martin_H
Posts: 837
Joined: 08 Jan 2014

Re: Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

I reviewed the console I/O. Overall, they look like they would work, but Claude in-lined the 6551 code and repeated it in multiple functions. I need to create a HAL and move initialization, vectors, and console I/O into it.

I reviewed the inner interpreter words: exit, execute, lit, branch, 0branch, zbranch, do, loop, +loop, unloop, i, and j. I compared them to Fig-Forth and they look reasonable, but I can't be sure until I review the compiler. 0branch had the same pop and test bug as move.

Code: Select all

	lda 0,X			; pop and evaluate flag
	inx
	inx
	bne @no_branch	; Non-zero = no branch
What's wrong is that inx clears the Z flag set by the load, so the branch is never taken. The code should be as follows:

Code: Select all

	inx				; pop and evaluate flag
	inx
	lda -2,X
	bne @no_branch	; Non-zero = no branch
It's interesting that Claude created PUSH and POP macros and never used them. Had it done so, the fix to the pop and test bug could be made in one place. I should probably use them, but I'm trying to make minimal changes since I'm just desk checking the code. In any event I did update the macro.
Martin_H
Posts: 837
Joined: 08 Jan 2014

Re: Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

Unfortunately, the -2 offset won't work here. In Fig-Forth I saw it done using $FE,X in zero page, but on the 65816 the parameter stack is on page 3, and since X is now a 16-bit register I don't think that works here.

Here's another "Go home Claude, you're drunk" moment:

Code: Select all

	pha
	lda (1,S)           ; peek
	pla
	sta TMPB
	lda (TMPB)
	cmp    Y               ; source length vs Y
                BLE     @copy_done      ; Y >= len
It's doesn't assemble, and I don't even know what it's trying to do.
User avatar
GARTHWILSON
Forum Moderator
Posts: 8773
Joined: 30 Aug 2002
Location: Southern California
Contact:

Re: Claude and I Vibe Coded a Forth Interpreter

Post by GARTHWILSON »

On the '816, $FFFE,X will go into the next bank.  I think you'll need LDA 0,XINXINXCMP #0, BNE.
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?
Martin_H
Posts: 837
Joined: 08 Jan 2014

Re: Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

GARTHWILSON wrote:
On the '816, $FFFE,X will go into the next bank.  I think you'll need LDA 0,XINXINXCMP #0, BNE.
Thanks. Right now, I have only been working in bank zero for my '816 code. I have to remind myself the other banks exist. I used the cmp #0 approach as it's the most straightforward.

Right now, I'm puzzling over the code below. It starts off reasonably, but gets weird, and then seems to work in the end.

Code: Select all

.proc COMMA_CODE
	.a16
	.i16
	lda UP			; Get UP, add DP offset, and load DP
	clc
	adc #U_DP
	sta SCRATCH0
	lda (SCRATCH0)		; Save DP → SCRATCH1
	sta SCRATCH1
	lda 0,X			; Pop val off parameter stack
	inx
	inx
	sta (SCRATCH1)		; Store indirect through DP
	lda SCRATCH1		; DP += 2
	clc
	adc #2
	lda UP			; This slams the previous add?
	clc
	adc #U_DP
	sta SCRATCH0		; This is already in SCRATCH0
	lda SCRATCH1
	clc
	adc #2
	sta (SCRATCH0)		; Okay this might work
	NEXT
.endproc
The same weirdness is in C, primitive as well.
Martin_H
Posts: 837
Joined: 08 Jan 2014

Re: Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

Here's a good example from the COUNT primitive that works but it is inefficient.

Code: Select all

	sta SCRATCH1        ; Save length
	lda 0,X
	inc                         ; addr+1
	sta 0,X                   ; Replace addr with addr+1
	dex
	dex
	lda SCRATCH1
	sta 0,X                   ; Push length
	NEXT
The sta SCRATCH1, followed by the lda, inc, sta, and lda SCRATCH1 doesn't take advantage of the INC opcode's ability to operate on memory. Claude is treating the 65816 like a load/store RISC architecture. It's probably an artifact of other assembly languages in its training data. I replaced it with:

Code: Select all

	inc 0,X			; addr+1 on TOS
	dex
	dex
	sta 0,X			; Push length
	NEXT
I'm on line 1183 out of 3286, while I'm 1/3 complete, I've lost track of the hours spent on this module. I think I'm still going at the same pace. But the WORD routine is next and it looks like it need a lot of TLC. Here's some of the comments:

Code: Select all

	cmp    SCRATCH1       ; Hmm, SCRATCH1 is now overwritten
	; This is getting complex - use Y as index into TIB
	; Restart with cleaner approach using Y as index

and later
	; Hmm W is our dest pointer... clobbered.
	; This whole approach is too register-starved.
	; Real implementations use dedicated ZP vars for parser state.
Martin_H
Posts: 837
Joined: 08 Jan 2014

Re: Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

I've concluded that WORD can't be salvaged, it will be easier to write it from scratch. The problem is that many times Claude realized that its approach wouldn't work, so it started again, but it's unclear how far it was backtracking. I'll copy it below as it provides insight into how Claude works. It emits code but can't revise what its written.

I looked at Fig-Forth and its WORD implementation is written as a list of words, so I can't steal and rewrite that code.

Code: Select all

.proc   WORD_CODE
.a16
.i16
	lda 0,X             ; delimiter char
	inx
	inx
	sta SCRATCH1        ; Save delimiter

	; Get >IN and SOURCE
	lda UP
	clc
	adc #U_TOIN
	sta SCRATCH0
	lda (SCRATCH0)      ; >IN offset
	sta TMPA
	lda UP
	clc
	adc #U_TIB
	sta SCRATCH0
	lda (SCRATCH0)      ; TIB base
	sta TMPB

	; Get HERE as destination
	lda UP
	clc
	adc #U_DP
	sta SCRATCH0
	lda (SCRATCH0)
	sta SCRATCH0        ; HERE

	; Skip leading delimiters
	lda UP
	clc
	adc #U_SOURCELEN
	sta W
	lda (W)             ; source length
	sta W              ; reuse W as end counter

@skip_delim:
	lda TMPA            ; >IN
	cmp    W              ; >= source length?
                BGE     @empty
	; Fetch char at TIB+>IN
	pha
	lda TMPB
	clc
	adc TMPA
	sta SCRATCH1
	; Actually fetch byte:
	sep    #$20
        .a8
	lda (SCRATCH1)
	rep    #$20
        .a16
	and    #$00FF
	sta TMPA            ; Temp: current char
	pla    	; >IN
	; Compare with delimiter
	cmp    SCRATCH1       ; Hmm, SCRATCH1 is now overwritten
	; This is getting complex - use Y as index into TIB
	; Restart with cleaner approach using Y as index
	bra    @word_clean

@empty:         ; Return empty counted string at HERE
	lda SCRATCH0
	dex
	dex
	sta 0,X
	sep    #$20
        .a8
	lda #0
	sta (SCRATCH0)
	rep    #$20
        .a16
	NEXT

@word_clean:
	; Cleaner implementation using Y as TIB index
	; TMPB = TIB base, W = source length
	; SCRATCH0 = HERE (destination)
	; SCRATCH1 = delimiter

	; Reload delimiter
	lda UP
	clc
	adc #U_TOIN
	sta TMPA
	lda (TMPA)          ; >IN
	tay     	; Y = >IN

	lda UP
	clc
	adc #U_SOURCELEN
	sta TMPA
	lda (TMPA)          ; source len → TMPA (via scratch)
	sta TMPA

	; Skip delimiters
@skip2:
                CPY     TMPA
                BGE     @eoi
	; Fetch TIB[Y]
	lda TMPB
	sta SCRATCH1
	sep    #$20
        .a8
	lda (SCRATCH1),Y
	rep    #$20
        .a16
	and    #$00FF
	cmp    0,X             ; Compare with delimiter (still on pstack? no...)
	; Actually delimiter was popped - save it differently
	; Use SCRATCH1 for delimiter value
	; This requires refactor - store delim earlier
	; For now use a simple approach: delimiter in A during compare
	; We stored it in original SCRATCH1 before - but it's been clobbered
	; Let's use the return stack to hold delimiter cleanly
	pha     	; Save current char
	pla
	; Delimiter was in original 0,X (stack) - already consumed
	; Use fixed approach: re-read from dedicated temp
	; TMPB=TIB, TMPA=srclen, SCRATCH0=HERE
	; Delimiter needs its own home - use SCRATCH1

	; Skip the rest of this complex inline approach
	; and use a subroutine
	bra    @use_subroutine

@eoi:
	; Return HERE with empty word
	lda SCRATCH0
	dex
	dex
	sta 0,X
	NEXT

@use_subroutine:
	; Restore Y and call helper
	lda UP
	clc
	adc #U_TOIN
	sta TMPA
	lda (TMPA)
	tay
	jsr    word_helper
	NEXT

        ; Out-of-line helper for WORD to keep NEXT reachable
word_helper:
.a16
.i16
	; On entry:
	;   Y    = >IN (current parse position)
	;   TMPB = TIB base address
	;   TMPA = source length
	;   SCRATCH0 = HERE (output buffer)
	;   SCRATCH1 = delimiter char

	; Skip leading delimiters
@skip:          CPY     TMPA
                BGE     @at_end
	lda TMPB
	sta W
	sep    #$20
        .a8
	lda (W),Y
	rep    #$20
        .a16
	and    #$00FF
	cmp    SCRATCH1
	bne    @found_start
	iny
	bra    @skip

@found_start:
	; Copy word chars to HERE+1
	; SCRATCH0 = count byte address, start storing at SCRATCH0+1
	sta TMPA            ; Reuse TMPA as end-of-source? No...
	; Save source length elsewhere
	pha     	; Save first char
	lda UP
	clc
	adc #U_SOURCELEN
	sta W
	lda (W)
	sta TMPA            ; TMPA = source length again
	pla

	; X reg = PSP but we need an index - use dedicated counter
	lda SCRATCH0
	inc               ; Point past count byte
	sta W               ; W = destination pointer
	stz   SCRATCH1        ; Reuse SCRATCH1 as char count (0)
	; Save delimiter back...
	; This is getting deeply nested - use a pure byte loop with fixed regs:
	; Y = source index, W = dest ptr, SCRATCH1 = count, TMPB = TIB base
	; TMPA = source length, SCRATCH0 = HERE

	; Store delimiter in zero-page temp before overwriting SCRATCH1
	; We already have it: it was on parameter stack (consumed)
	; Re-read it from where EMIT_CODE left it... it's gone.
	; Simplest fix: stash delimiter at the very start in TMPA before
	; it gets clobbered (TMPA was only used for >IN and source length).
	; Accept limitation: word_helper needs delimiter passed differently.
	; For now, use space ($20) as hardcoded delimiter as a working default.
	lda #$20            ; Fallback: space delimiter
	sta SCRATCH1        ; Stash delimiter

	stz   TMPA            ; char count = 0
@copy:
	; Check source exhausted
	lda UP
	clc
	adc #U_SOURCELEN
	pha
	lda (1,S)           ; peek
	pla
	sta TMPB
	lda (TMPB)
	cmp    Y               ; source length vs Y
                BLE     @copy_done      ; Y >= len

	; Fetch char
	lda TMPB
	sta W
	; Hmm W is our dest pointer... clobbered.
	; This whole approach is too register-starved.
	; Real implementations use dedicated ZP vars for parser state.
	bra    @copy_done

@copy_done:
	; Store count byte
	sep    #$20
        .a8
	lda TMPA
	sta (SCRATCH0)      ; Store length at HERE
	rep    #$20
        .a16
	; Update >IN
	lda UP
	clc
	adc #U_TOIN
	sta W
                TYA
	sta (W)
	; Push HERE
	lda SCRATCH0
	dex
	dex
	sta 0,X
                RTS

@at_end:
	; Empty word
	sep    #$20
        .a8
	lda #0
	sta (SCRATCH0)
	rep    #$20
        .a16
	lda SCRATCH0
	dex
	dex
	sta 0,X
                RTS
.endproc
Martin_H
Posts: 837
Joined: 08 Jan 2014

Re: Claude and I Vibe Coded a Forth Interpreter

Post by Martin_H »

The code is starting to become progressively more broken. For example:
* jsr's to code that doesn't exist:

Code: Select all

C:\Users\mheer\Documents\git\65816\Forth816>grep print_udec *
primitives.s:   jsr    print_udec
primitives.s:print_udec:
primitives.s:   jsr DOT_CODE::print_udec
primitives.s:   jsr    DOT_CODE::print_udec

C:\Users\mheer\Documents\git\65816\Forth816>grep do_number *
primitives.s:   jsr    do_number
primitives.s:do_number:
primitives.s:   jsr INTERPRET_CODE::do_number
I rescued Claude here because I have print functions from my previous project. So, I created a print module and added them to this project.

In INTERPRET it wants to call word, so it does a jsr to a subroutine that contains most of the broken WORD code.
Quote:
; Push space delimiter and call WORD
dex
dex
lda #$20 ; Space
sta 0,X
; Manually inline simplified WORD:
; scan past spaces, copy word to HERE
jsr do_parse_word ; Returns addr on stack via SCRATCH0
It's like it never recovered from WORD and doesn't know what to do.
Post Reply