Page 1 of 8
Claude and I Vibe Coded a Forth Interpreter
Posted: Thu Mar 12, 2026 5:47 pm
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.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Thu Mar 12, 2026 7:33 pm
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.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Fri Mar 13, 2026 12:18 am
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
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Fri Mar 13, 2026 2:38 am
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.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Fri Mar 13, 2026 1:51 pm
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
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Fri Mar 13, 2026 2:17 pm
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.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Fri Mar 13, 2026 3:42 pm
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.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Fri Mar 13, 2026 8:08 pm
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.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Sat Mar 14, 2026 2:03 am
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.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Sat Mar 14, 2026 3:37 am
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.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Sat Mar 14, 2026 4:13 am
by GARTHWILSON
On the '816, $FFFE,X will go into the next bank. I think you'll need LDA 0,X, INX, INX, CMP #0, BNE.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Sat Mar 14, 2026 2:35 pm
by Martin_H
On the '816, $FFFE,X will go into the next bank. I think you'll need LDA 0,X, INX, INX, CMP #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.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Sat Mar 14, 2026 4:55 pm
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.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Sat Mar 14, 2026 7:03 pm
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
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Sat Mar 14, 2026 7:48 pm
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.
; 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.