Page 3 of 8

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Tue Mar 17, 2026 8:16 pm
by Martin_H
Three unit tests written and various issues corrected, so I will issue a new code quality report.

Code: Select all

filename,	size,	   score
compare.s,	 296 lines, A - fully unit tested.
forth.s,		216 lines, B - untested but looks good after revision/review
hal_mench.s,  171 lines, A - reused code from another project.
interpreter.s, 414 lines, B - untested but looks good after revision/review
io.s,			149 lines, A - fully unit tested.
macros.inc,	182 lines, A- - partially unit tested
math.s,		 387 lines, B - untested but looks good after revision/review
memory.s,	  214 lines, B - untested but looks good after revision/review
primitives.s, 316 lines, F - Unsalvageable code, needs a rewrite.
print.s,		151 lines, A - reused code from another project.
stack.s,		285 lines, A - full unit tested
stubs.s,		208 lines, F - Claude generated place holders no implementation
system.s,	  748 lines, D - Possibly salvageable code, needs work.
Working on the math.s unit test which is a lot of work. So I'm writing this to stay motivated. Also, apologies to our friends across the pond, as math.s should be maths.s. But I promise not to name any module aluminum.s.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Tue Mar 17, 2026 10:40 pm
by Martin_H
I missed this on the first pass through math.s. In several places Claude jsr's to another function. This would work if this was a subroutine threaded Forth, but this is an ITC Forth and routines end with the NEXT macro. So that jsr would never return.

Code: Select all

;------------------------------------------------------------------------------
; MOD ( n1 n2 -- rem )
;------------------------------------------------------------------------------
	HEADER "MOD", MOD_CFA, 0, SLASH_CFA
	CODEPTR MOD_CODE
PUBLIC MOD_CODE
	jsr SLASHMOD_CODE
	inx			; Stack: NOS=rem TOS=quot → DROP
	inx
	NEXT
ENDPUBLIC
The fix is simple enough I split SLASHMOD_CODE into a separate utility routine that all functions can jsr to. SLASHMOD now looks like this:

Code: Select all

;------------------------------------------------------------------------------
; /MOD ( n1 n2 -- rem quot ) signed division
;------------------------------------------------------------------------------
	HEADER "/MOD", SLASHMOD_CFA, 0, UMSLASHMOD_CFA
	CODEPTR SLASHMOD_CODE
PUBLIC SLASHMOD_CODE
	jsr SLASHMOD_IMPL
	NEXT
ENDPUBLIC
.proc SLASHMOD_IMPL
; actual code is here and end in an rts

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Wed Mar 18, 2026 6:09 pm
by Martin_H
Now that I have a respectable number of primitives working, I decided to start looking into chaining words together through the NEXT macro. But to do it within a unit test. Unfortunately, I spent a fair bit of time last night getting nowhere, so I instrumented the code, and initially I thought Claude botched the ITC model. Below is Claude's NEXT macro:

Code: Select all

;------------------------------------------------------------------------------
; NEXT - Inner interpreter dispatch
;
; Fetches the CFA at IP (Y), advances IP by 2, then jumps through
; the code pointer stored at that CFA.
;
; Register state on entry:
;   Y = IP (points to next CFA cell in current word body)
;   X = PSP
;   A = undefined (will be clobbered)
;
; Cycle cost: ~14 cycles (inlined)
;------------------------------------------------------------------------------
.macro  NEXT
        .a16
        LDA     0,Y             ; A = *IP (fetch CFA of next word)
        INY                     ; IP += 1
        INY                     ; IP += 2  (advance past this cell)
        STA     W               ; W = CFA
        LDA     (W)             ; A = *W (fetch code pointer)
        STA     SCRATCH0
	rts
	JMP     (SCRATCH0)      ; Jump to code
.endmacro
I was thinking that Y is the IP that contains the CFA of the next word, so the "lda 0,Y" is the first indirection, and C would contain the code address.

But and it pains me to say this. I was wrong and Claude was right. Y is the IP, but the IP points to a list of CFA's. So, the first indirection loads C with the CFA, and the second indirection loads the code address. My corrected unit testing stub looks like this:

Code: Select all

; CFA used to handle the NEXT at the end of code were testing.
CFA_LIST:
	.word RTS_CFA	
HEADER "RTS", RTS_CFA, 0, 0
CODEPTR RTS_CODE
PUBLIC  RTS_CODE
	PRINTLN rtsmsg
	ldy #CFA_LIST
	rts
ENDPUBLIC
rtsmsg:	.asciiz "In RTS handler"
This stub allows me to get a primitive ending in NEXT to issue an RTS back to the unit test, but I don't have to recompile the primitives, I just need to do an "ldy #CFA_LIST" on entry into the unit test. Previously I was putting an RTS at the top of NEXT and doing a recompile. This also allows me to test words that push items onto the return stack.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Wed Mar 18, 2026 8:45 pm
by Martin_H
My math unit test needs more test cases, for example signed test cases. But it already indicates the UM* and /MOD are incorrect.

Code: Select all

math test - enter!
+ test $255 + $1025 (expect 127A) = 127A
- test $1025 - $255 (expect 0DD0) = 0DD0
* test $1025 * $0014 (expect 42E4) = 42E4
UM* test $1025 * $255 High (expect 0025) = 0000
UM* test $1025 * $255 Low (expect A649) = A649
UM/MOD test $000927C0 / $0A (expect EA60) = EA60
UM/MOD test remainder (expect 0000) = 0000
/MOD test $8000 /MOD $0A (expect 0CCC) = 0000
/MOD test remainder (expect 0008) = 0000
NEGATE test $ffe0 (expect 0020) = 0020
NEGATE test $00e0 (expect FF20) = FF20
ABS test $ffe0 (expect 0020) = 0020
ABS test $00e0 (expect 00E0) = 00E0
MAX test $0032 $1032 (expect 1032) = 1032
MIN test $0032 $1032 (expect 0032) = 0032
1+ test $1032 (expect 1033) = 1033
1- test $0537 (expect 0536) = 0536
2* test $0537 (expect 0A6E) = 0A6E
2/ test $0537 (expect 029B) = 029B
math test - exit!

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Wed Mar 18, 2026 10:44 pm
by GARTHWILSON
Martin_H wrote:
But it already indicates the UM* and /MOD are incorrect.
In the head post, you say,
Quote:
* Second, it claimed knowledge of Fig-Forth and was using it as a basis.
and
Quote:
* 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.
figForth has bugs in the UM* and UM/MOD.  AI seems to only find stuff on the web and doesn't truly understand anything.  See my descriptions of these bugs, and fixes, at http://6502.org/source/integers/ummodfix/ummodfix.htm and viewtopic.php?t=689 .  The latter has optimizations offered by others here.  You'll probably find more things of interest in the Forth section of my links page, at http://wilsonminesco.com/links.html#Forth .

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Thu Mar 19, 2026 2:19 am
by Martin_H
GARTHWILSON wrote:
figForth has bugs in the UM* and UM/MOD.  AI seems to only find stuff on the web and doesn't truly understand anything.  See my descriptions of these bugs, and fixes, at http://6502.org/source/integers/ummodfix/ummodfix.htm and viewtopic.php?t=689 .  The latter has optimizations offered by others here.  You'll probably find more things of interest in the Forth section of my links page, at http://wilsonminesco.com/links.html#Forth .
Thanks Garth. I probably ran into this bug when I ported Fig-Forth to my 6502 SBC years ago but forgot about it.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Thu Mar 19, 2026 8:16 am
by leepivonka
There are some more word tests at https://github.com/leepivonka/FM/blob/main/test.fs

The Claude version of FORTH seems not know about colon words. It either ignores them, or converts them to machine code.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Thu Mar 19, 2026 3:16 pm
by Martin_H
Thanks for the link to the tests. Once I get the interpreter running unit testing within Forth is good direction.
leepivonka wrote:
The Claude version of FORTH seems not know about colon words. It either ignores them, or converts them to machine code.
The outer interpreter is probably a write off. I'm thinking about translating the Fig-Forth colon definitions, but no definitive plans yet. After I complete low level unit, I think WORDS and QUIT are where I need to start.

Update: I am puzzled by how Claude's interpreter is supposed to work.

Fig-Forth enters on the cold start vector, initializes variables, goes to ABORT, and then to QUIT.

Claude Forth enters on processor vectors, initializes variables, and goes to QUIT. ABORT is above it and it also goes to QUIT. QUIT is also a mess.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Fri Mar 20, 2026 6:20 pm
by Martin_H
I've decided that Claude and I need to have a conversation about the ABORT function, and why it doesn't make sense.

Q: What does the ABORT word do?

Claude: Looking at the code we wrote, ABORT is defined as a colon definition in primitives.s:
Honestly that stub is incomplete — it pushes PSP_INIT onto the stack but never actually resets the stack pointer. The ANS Forth standard definition of ABORT should:

1. Empty the parameter stack — reset PSP (X register) to its initial value
2. Empty the return stack — reset RSP (S register) to its initial value
3. Set STATE to 0 — drop back to interpret mode if compiling
4. Transfer control to QUIT — restart the outer interpreter loop
A correct implementation would be:

Code: Select all

HEADER  "ABORT", ABORT_CFA, 0, BYE_CFA
CODEPTR ABORT_CODE
        .proc   ABORT_CODE
        .a16
        .i16
                ; Reset parameter stack
                LDX     #$03FF          ; PSP_INIT

                ; Reset return stack
                LDA     #$01FF          ; RSP_INIT
                TAS

                ; Set STATE = 0 (interpret)
                LDA     UP
                CLC
                ADC     #U_STATE
                STA     SCRATCH0
                STZ     (SCRATCH0)

                ; Jump to QUIT body directly
                ; (can't JSR — return stack was just wiped)
                LDA     #<QUIT_BODY     ; Load address of QUIT's body
                TAY                     ; IP = QUIT body
                NEXT
        .endproc
The key subtlety is that because ABORT wipes the return stack, it cannot use DOCOL/EXIT to call QUIT — there's no valid return address to push. Instead it directly loads QUIT_BODY into IP (Y) and dispatches with NEXT, effectively jumping into QUIT's body as if it were already executing.

A few things I find interesting about this:
1. It abandoned a colon definition for an assembly primitive.
2. Claude loves em dashes.
3. It's still using the invalid instruction stz (SCRATCH)
4. It's not using the ldy instruction!

I decided to ignore Claude and write the function as follows:

Code: Select all

;------------------------------------------------------------------------------
; ABORT ( -- ) The ANS Forth standard definition of ABORT should:
; 1. Empty the parameter stack — reset PSP (X register) to its initial value
; 2. Empty the return stack — reset RSP (S register) to its initial value
; 3. Set STATE to 0 — drop back to interpret mode if compiling
; 4. Transfer control to QUIT — restart the outer interpreter loop 
;------------------------------------------------------------------------------
HEADER  "ABORT", ABORT_CFA, 0, BYE_CFA
CODEPTR ABORT_CODE
.proc ABORT_CODE
	ldx PSP_SAVE		; Reset parameter stack to entry point values
	lda RSP_SAVE		; Reset return stack to entry point values
	tas

	lda UP
	sta SCRATCH0
	ldy #U_STATE
	lda #0000
	sta (SCRATCH0),Y	; Set STATE = 0 (interpret)

	ldy #QUIT_BODY		; IP = QUIT body
	NEXT
I have the corrected code to Claude and it

Claude: That's a clean solution — using Y as an offset into the user area via (SCRATCH0),Y indirect indexed addressing, which the 65816 fully supports. And reusing Y for the IP load right after is efficient since you're done with it as an index at that point. Let me find all the STZ ( occurrences in the files and fix them all consistently.

Perhaps I should have asked Claude to fix its other errors.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Fri Mar 20, 2026 9:42 pm
by Martin_H
Claude and I are discussing the broken WORD function, and I described how to use stack relative addressing.

Code: Select all

Let me study the current WORD_CODE stub carefully first so I understand exactly what it needs to do:
Good — now I have a clear picture of what it needs to do and how broken the current version is.
Let me rewrite it cleanly using stack-relative addressing.
Me: Thanks, I reviewed the code and it looks better.

Code: Select all

You're welcome! The stack-relative approach turned out really clean — using the hardware stack as a local variable frame is one of those 65816 features that's easy to overlook but makes a big difference for register-hungry routines like WORD.
I'm still not sure if this is going to produce anything useful. But at least Claude is merging my fixes in his code base. It would be better if we both used my Git hub.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Fri Mar 20, 2026 9:48 pm
by Martin_H
Me: I'm confused about FORTH_INIT. At the end it goes to QUIT_CFA. Should it go to ABORT_CFA which then goes to QUIT_BODY?

Code: Select all

You're right to question it. Let's think through what each does:

QUIT_CFA is a colon definition — it goes through DOCOL, which pushes the current IP onto the return stack before executing. But at startup there is no valid IP to push, so that's a problem.

ABORT_CODE is a machine code primitive that resets both stacks and jumps directly into QUIT_BODY via NEXT — no return stack involvement at all. That's exactly what you want at startup: clean slate, both stacks reset, straight into the interpreter loop.

So yes, FORTH_INIT should jump to ABORT_CODE rather than QUIT_CFA. Let me fix that:

Done. The flow is now:

FORTH_INIT → JMP ABORT_CODE
ABORT_CODE → resets PSP, RSP, STATE, >IN → LDY #<QUIT_BODY → NEXT
QUIT_BODY  → RSP_RESET, STATE=0, then REPL loop
Which is clean all the way through — no colon definitions involved until the interpreter is actually running.
Basically to understand what Claude has done wrong required a great deal of work on my part.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Sat Mar 21, 2026 1:03 am
by Martin_H
This is a funny one. Claude has been hallucinating a file called constants.inc. I pointed out a build error, and it responded with this:
Quote:
There's already a constants.inc include in forth.s but the file doesn't exist yet. That's the right solution — create constants.inc with all the shared constants, and include it from both forth.s and primitives.s. Let me also check what's needed from primitives.s:

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Sat Mar 21, 2026 1:53 am
by Martin_H
Claude likes BDD's technique of pointing the direct page register to the stack:

Code: Select all

Done. That TSC/TCD frame technique is elegant — it turns the hardware stack into a local variable frame with named offsets, which is exactly the same pattern we used in WORD_CODE. It would be very useful in other HAL functions too if they ever need more complex local state.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Sat Mar 21, 2026 2:50 am
by GARTHWILSON
In my '816 Forth, RESET puts the PFA of COLD in IP, then goes to NEXTCOLD ends with ABORT, and ABORT ends with QUITCOLD, ABORT, and QUIT are secondaries (ie, colon definitions), but they don't need to jump to NEXT at the end.  (RESET, a primitive, does though.)

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Sat Mar 21, 2026 4:48 am
by BigDumbDinosaur
Martin_H wrote:
Claude likes BDD's technique of pointing the direct page register to the stack...
However, what I do is slightly different:

Code: Select all

         rep #%00100000
         sec
         tsc                   ;SP —> .C
         sbc !#sfsiz           ;allocate SFSIZ bytes on stack
         tcs                   ;.C —> SP
         inc                   ;(SP + 1)
         tcd                   ;$00 in logical direct page

The way Claude is doing it will work, but will make address $00 appear at SP, which location is subject to being inadvertently overwritten.  The sequence I’ve shown results in $00 being at SP+1, which is safe.