Page 5 of 8
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Mon Mar 23, 2026 7:32 pm
by BigDumbDinosaur
Maybe you could edit down a transcript of the most productive or decisive parts of the conversation and paste that in at the beginning of the next session? Maybe there's a better way... context is everything!
Probably would work. I’m getting the impression that Claude is saving some state somewhere so an interrupted “conversation” can carry on.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Mon Mar 23, 2026 9:54 pm
by Martin_H
I tried the /compact command and Claude said it didn't understand it. Which is odd because multiple websites said it should work
Maybe you could edit down a transcript of the most productive or decisive parts of the conversation and paste that in at the beginning of the next session? Maybe there's a better way... context is everything!
I asked Claude for suggestions, and it said the following:
Code: Select all
If you're concerned about context length, the best approach is to start a new conversation and paste in the current versions of the files you're actively working on. Since all the source files are saved as outputs, you can download them and re-upload whichever ones are relevant to the next task.
The key files at this point are:
forth.s
primitives.s
hal.s
macros.inc
dictionary.inc
constants.inc
forth.cfg
build.sh
I asked if I could upload a zip and it doesn't support that. But Claude does support loading from a GitHub URL. So that's the plan for the future.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Tue Mar 24, 2026 1:14 am
by Martin_H
I read the entire chat log, summarized it, and stored in GitHub like it is part of the source code. I can then paste a raw URL and let Claude read the whole thing. Here's the text:
Code: Select all
This is a summary of our previous session writing an ITC threaded ANS Forth (ANSI 1994) for the 65816 CPU. Since the 65816 is natively 16-bit, a 16-bit cell size is used for speed and simplicity.
The first target board is a single board computer has with the w65c265 with 32KB RAM and 32 KB ROM. The Forth kernel will be located in the ROM, and the user will interact using the chip's UART serial interface. The memory map of the system is:
$0000 - $00FF Direct Page (Forth VM registers, scratch)
$0100 - $01FF Hardware stack (Return Stack)
$0200 - $03FF Parameter Stack (grows downward)
$0400 - $04FF User Area
+$00 BASE numeric base (default 10)
+$02 STATE 0=interpret, 1=compile
+$04 DP dictionary pointer
+$06 LATEST latest definition link
+$08 TIB terminal input buffer address
+$0A >IN parse offset
+$0C SOURCE-LEN current source length
+$0E HANDLER exception handler
$0500-$05FF Terminal Input Buffer (TIB)
$0600-$7EFF RAM Dictionary (user definitions grow upward)
$8000 - $FFFF ROM (kernel primitives, core word set)
The Forth is written using the CA65 assembler with the following register allocations:
X = Parameter Stack Pointer (PSP)
Y = Instruction Pointer (IP)
A = Working register
S = Return Stack Pointer (RSP) - use the hardware stack!
D = Direct Page base for Forth internals
DB = Data Bank for dictionary
The current source files are located in GitHub and can be accessed with these URLs:
https://raw.githubusercontent.com/Martin-H1/65816/refs/heads/main/Forth816/build.sh
https://raw.githubusercontent.com/Martin-H1/65816/refs/heads/main/Forth816/macros.inc
https://raw.githubusercontent.com/Martin-H1/65816/refs/heads/main/Forth816/constants.inc
https://raw.githubusercontent.com/Martin-H1/65816/refs/heads/main/Forth816/forth.cfg
https://raw.githubusercontent.com/Martin-H1/65816/refs/heads/main/Forth816/README.md
https://raw.githubusercontent.com/Martin-H1/65816/refs/heads/main/Forth816/primitives.s
https://raw.githubusercontent.com/Martin-H1/65816/refs/heads/main/Forth816/dictionary.inc
https://raw.githubusercontent.com/Martin-H1/65816/refs/heads/main/Forth816/hal.s
https://raw.githubusercontent.com/Martin-H1/65816/refs/heads/main/Forth816/forth.s
In the source code you will notice the use of the macros PUBLIC and ENDPUBLIC. They are important as that allows the functions to be called by unit tests. The unit test are written in 65816 assembly and I don't have a testing framework.
During our previous session we ran into a few issues with invalid 65816 op codes, CA65 syntax, and 65816 best programming practices. I've retained a summary of some of those issues:
The valid 65816 branch instructions for future reference:
Condition Unsigned Signed
= BEQ BEQ
≠ BNE BNE
< BCC BMI (with overflow check)
≥ BCS BPL (with overflow check)
> reverse operands + BCC reverse operands + BMI
≤ reverse operands + BCS reverse operands + BPL
The 65816 CMP only supports immediate, direct page, absolute, and indexed memory operands - never a register directly. For comparing against Y or X you need to either use CPY/CPX with a memory or immediate value, or transfer with TYA/TXA first.
On the 65816, arithmetic and logic instructions (ADC, SBC, AND, ORA, EOR, etc.) only operate on memory operands or immediate values - never directly on X or Y registers. To use X or Y as a source you always need TXA/TYA first, or store to a zero page location and use that.
The CA65 asssembler doesn't support escaping the " character in string constants. This caused problems using the HEADER macro for words with " in the name (e.g. ABORT"). To resolve this we skipped the HEADER macro for these words and wrote header bytes manually, including the quote character as a numeric byte value $22.
Word defintions end in NEXT, so a word definition can't JSR directly to another word definition. That's beecause the JSR return. When this is required, this is worked around by refactoring the word code into a helper function. SLASHMOD_CODE and SLASHMOD_IMPL are an example of this pattern.
The 65816 supports stack relative addressing which is used to create stack local variables within a procedure. This reduces the need for direct page space, and eases register starvation issues. A restriction is that the only indirect stack local addressing mode is indexed indirect addressing using the Y register like this: "lda (OFFSET,S),Y". This technique is used in the WORD_CODE procedure.
However, the 65816 supports moving the direct page pointer to the procedure's stack frame. This is useful for using direct page addressing modes with stack locals. This technique is used in the hal_cputs function.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Tue Mar 24, 2026 2:29 pm
by Martin_H
I asked Claude to proofread my prompt summary, and it came back with a few things I missed. However, my session context is now so big that simply proofreading exhausted my tokens until 3:00pm. Hopefully a new session and the summary will let me continue.
Code: Select all
Here are additional 65816 gotchas:
STZ only supports direct page and absolute addressing — no indirect modes. Use LDA #0 / STA (addr) instead
INC, DEC, LDY, LDX don't support stack-relative addressing — only LDA/STA do
JSR indirect doesn't exist — use JMP indirect instead (safe since Forth words end in NEXT anyway)
BGE, BLE, BLT, BGT don't exist — see the branch table in your summary
PEA $0000 / PLB / PLB is a cleaner way to set the Data Bank Register than SEP / LDA #0 / PHA / PLB
PHY/PLY around loops:
Any primitive that uses LDY #n or TAY as a loop counter must save and restore Y with PHY/PLY since Y holds the Forth instruction pointer (IP).
BEQ/BNE after INX/INX:
INX/INX clobbers the zero flag, so you can't test the just-popped value with BEQ/BNE directly after. Save to a zero page variable or use CMP #0 after the pops.
TODO items:
STAR_CODE, UMSTAR_CODE, UMSLASHMOD_CODE, SLASHMOD_CODE, and MOD_CODE have known failures on edge cases and need replacing with verified implementations.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Tue Mar 24, 2026 4:08 pm
by BigDumbDinosaur
Code: Select all
Here are additional 65816 gotchas:
STZ only supports direct page and absolute addressing — no indirect modes. Use LDA #0 / STA (addr) instead
Nothing unique about that to the 65C816.
Code: Select all
INC, DEC, LDY, LDX don't support stack-relative addressing — only LDA/STA do
In my experience, that hasn’t gotten in the way. I actually don’t use stack-relative all that much—most of my functions point DP to the stack.
Code: Select all
JSR indirect doesn't exist — use JMP indirect instead (safe since Forth words end in NEXT anyway)
Closest you can get to an indirect JSR is by setting .X to $00 and using JSR (<targ>,X), or by writing a macro that ends up using JMP (<targ>). The problem with the latter is <targ> must be in bank $00.
Code: Select all
BGE, BLE, BLT, BGT don't exist — see the branch table in your summary
Nothing peculiar to the 816 as well, and can be synthesized in macros.
Code: Select all
PEA $0000 / PLB / PLB is a cleaner way to set the Data Bank Register than SEP / LDA #0 / PHA / PLB
That is true only if DB is to be set to $00. If PEA # is to be used to set DB to something other than $00, the target bank has to be in the MSB, since that is what will be grabbed by the second pull.
In practice, I generally recommend not going down the DB rabbit hole. In my programs, the initialization includes the PHK - PLB sequence to point DB to the execution bank, and that is it. Run-time variables are stored in the space (BSS) above the program text and static data, or on direct page if warranted. That way, I can use the relatively quick absolute addressing modes for most run-time data and indirect-long addressing for bank-agnostic data accesses. Since most of my programs are built up from a series of function calls, direct-page usage in the mainline code is minimal—functions requiring direct-page addressing point DP to the stack.
Code: Select all
PHY/PLY around loops:
Any primitive that uses LDY #n or TAY as a loop counter must save and restore Y with PHY/PLY since Y holds the Forth instruction pointer (IP).
BEQ/BNE after INX/INX:
INX/INX clobbers the zero flag, so you can't test the just-popped value with BEQ/BNE directly after. Save to a zero page variable or use CMP #0 after the pops.
Again, nothing peculiar to the 816.
I strongly suspect Claude doesn’t “understand” the full extent of what the 65C816 can do in native mode. 
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Tue Mar 24, 2026 5:22 pm
by BigEd
You seem to be confused, BDD. The text isn't a critique, it's a description. Seems reasonable to me that a software tool needs a description of the problem space.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Tue Mar 24, 2026 8:28 pm
by Martin_H
While I was waiting for Claude, I wrote the WORD function and unit tested it.
After 3:00 I gave Claude the link to my context prompt and it worked to bring it up to speed again. My directive was to write the "find" word. Unfortunately, I think the product of its output was substandard and requires significant rework. Among Claude's mistakes and bad behaviors:
* Edits in unrelated functions which either requires significant unit testing or selective merging of Claude's edits into a pristine copy.
* Removing descriptive comments, included TODO comments.
* Creating new direct page variables rather than using stack locals as explicitly directed! This one really ticks me off.
Given how LLM's work it's understandable its changes are all over the file. LLMs read text and transform it into an internal numeric representation. After the text prediction process the internal numeric representation is then transformed back into human readable text. But this transformation process can be lossy. It probably didn't happen in the previous session because the session context was so large that it avoided losses.
So where to go with this? I will take a look at the code Claude generated, but I'm fairly close to calling this experiment a failure.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Tue Mar 24, 2026 11:43 pm
by Martin_H
After reviewing the changes, I am ready to render a verdict:
* It took extensive manual review, bug fixes, and testing to get this far.
* Automated regression tests are needed to keep Claude in line.
* Claude won't be able to transition from assembly primitives to colon words.
* Claude ignores coding directions, even when given clear examples.
* Claude's recent edits are not useable, and the file wide edits were damaging.
While an interesting experiment, and I am amazed it works as well as it does. I don't see Claude as viable for this task. I certainly don't see it as worth paying for.
I may continue on this project without Claude, or I may consign this code to the bit bucket. Honestly, if I had done a line-by-line of Fig-Forth to 65816 assembly I might have finished by now.
For example, several times I pointed out that code like this:
Code: Select all
; --- Initialize User Pointer ---
LDA #UP_BASE
STA UP
; --- User area: BASE = 10 ---
LDA #UP_BASE + U_BASE
STA SCRATCH0
LDA #10
STA (SCRATCH0)
; --- User area: STATE = 0 (interpret) ---
; STZ (indirect) not supported - use STA (UP),Y
LDA #UP_BASE
STA SCRATCH0
LDA #0
LDY #U_STATE
STA (SCRATCH0),Y ; STATE = 0
; --- User area: DP = DICT_BASE ---
LDA #UP_BASE + U_DP
STA SCRATCH0
LDA #DICT_BASE
STA (SCRATCH0)
; --- User area: LATEST = last ROM word ---
LDA #UP_BASE + U_LATEST
STA SCRATCH0
LDA #LAST_WORD ; Defined at end of dictionary.s
STA (SCRATCH0)
; --- User area: TIB = TIB_BASE ---
LDA #UP_BASE + U_TIB
STA SCRATCH0
LDA #TIB_BASE
STA (SCRATCH0)
; --- User area: >IN = 0 and SOURCE-LEN = 0 ---
LDA #UP_BASE
STA SCRATCH0
LDA #0
LDY #U_TOIN
STA (SCRATCH0),Y ; >IN = 0
LDY #U_SOURCELEN
STA (SCRATCH0),Y ; SOURCE-LEN = 0
Should be written using the (UP),Y addressing mode. This is basic 6502 programming and there should be plenty of examples online.
Code: Select all
; --- Initialize User Pointer ---
LDA #UP_BASE
STA UP
; --- User area: BASE = 10 ---
LDY #U_BASE
LDA #10
STA (UP),Y
; --- User area: STATE = 0 (interpret) ---
; STZ (indirect) not supported - use STA (UP),Y
LDY #U_STATE
LDA #0
STA (UP),Y ; STATE = 0
; --- User area: DP = DICT_BASE ---
LDY #U_DP
LDA #DICT_BASE
STA (UP),Y
; --- User area: LATEST = last ROM word ---
LDY #U_LATEST
LDA #LAST_WORD ; Defined at end of dictionary.s
STA (UP),Y
; --- User area: TIB = TIB_BASE ---
LDY #U_TIB
LDA #TIB_BASE
STA (UP),Y
; --- User area: >IN = 0 and SOURCE-LEN = 0 ---
LDY #U_TOIN
LDA #0
STA (UP),Y ; >IN = 0
LDY #U_SOURCELEN
STA (UP),Y ; SOURCE-LEN = 0
Honestly since it's initialization code absolute addressing could be used. But that's at least better.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Wed Mar 25, 2026 4:30 am
by leepivonka
Thank you for writing about your adventures with Claude! It's been interesting following along.
I assume Claude has no memory of previous sessions with you, except for the session logs you play back to it at the start of the next session. So to improve Claude's 65816 programming, one needs to play a programming course into it at the start of every session?
If you want to continue with this FORTH & would like a starter, take a look at
https://github.com/leepivonka/FM . It started with the early Claude results you posted to GitHub but has been extensively modified. Maybe this is competition to the Claude version!
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Wed Mar 25, 2026 1:57 pm
by Martin_H
Thank you for writing about your adventures with Claude! It's been interesting following along.
I assume Claude has no memory of previous sessions with you, except for the session logs you play back to it at the start of the next session. So to improve Claude's 65816 programming, one needs to play a programming course into it at the start of every session?
If you want to continue with this FORTH & would like a starter, take a look at
https://github.com/leepivonka/FM . It started with the early Claude results you posted to GitHub but has been extensively modified. Maybe this is competition to the Claude version!
I'm glad you enjoyed it. Thanks for the link, I'll take a look. I already like your idea of adding a link to the Forth standard for each word.
This is yet another example of Claude's coding issues.
Code: Select all
; Return stack: TOS=index NOS=limit NOS2=saved_IP
; Pop index, copy, push back
PLA ; index
STA SCRATCH0
PHA ; Push back
LDA SCRATCH0
The STA and LDA being pointless was of cycles. Or Claude's allergy to using the Y register.
Code: Select all
LDA UP ; Get UP and add DP offset
CLC
ADC #U_DP
STA SCRATCH0
LDA (SCRATCH0) ; Fetch DP indirect
CLC
ADC 0,X ; Advance to DP + n
STA (SCRATCH0) ; Store new DP
INX ; Drop n
INX
Instead of this more compact and clearer code.
Code: Select all
PHY
LDY #U_DP
LDA (UP),Y ; Fetch DP indirect
CLC
ADC 0,X ; Advance to DP + n
STA (UP),Y ; Store new DP
PLY
INX ; Drop n
INX
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Wed Mar 25, 2026 6:27 pm
by Dr Jefyll
Thank you for writing about your adventures with Claude! It's been interesting following along.
I too am grateful for you sharing your experience, Martin.
In many cases Claude's performance was perplexing or outright disappointing. But the impression I take away has some surprises of a positive nature.
Like you, I was impressed when he -- uh, it! -- "asked good clarifying questions." To me, this hints at more than simple regurgitation of patterns learned by rote. Still no Einstein, of course, but it reminded me not to be too dismissive of what has been accomplished so far.
(The "clarifying questions" remark was in another thread, in
this post.)
-- Jeff
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Wed Mar 25, 2026 6:48 pm
by BigDumbDinosaur
After reviewing the changes, I am ready to render a verdict...While an interesting experiment, and I am amazed it works as well as it does. I don't see Claude as viable for this task. I certainly don't see it as worth paying for.
Makes me wonder if Claude would offer higher-quality results if you did pay for his...er...its services.
In any case, it was an interesting experiment. If nothing else, it has demonstrated that a reasonably skilled, assembly-language programmer is still smarter than a data center full of blade servers. 
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Wed Mar 25, 2026 7:23 pm
by BigEd
I gather that the paid versions of these things are very much better. In that line of thinking, it's impressive that a free version was so helpful.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Thu Mar 26, 2026 3:30 am
by Martin_H
Thanks for all the interest.
I've thought about forking over the money for one month just to see if it's better. If I continue to use Claude on this project, I think I need a different methodology. Having it work on a whole module is too risky and consumes too many Claude tokens. Instead, I would have it read the code from GitHub and prompt it to print the changes to a single function. I would review the proposed changes and merge them myself. I would also try to push it into creating colon definitions which is where the project needs to go.
In the meantime, I feel like there's too much working code to waste it. So, I took a slightly different approach today. I debugged the routines DOT_CODE and DOTS_CODE. Claude's helper function print_udec was a lost cause. While it had the right idea, it wasn't worth debugging, as I have a similar function in my print.s module. So, I copied it into primitives.s and extended it to either base 10 or 16.
Now that I have those functions working, I will change my unit test to use them instead of the print.s helper functions. The goal is to reduce the scaffold code and rely more on code in the interpreter itself. Eventually transitioning to colon definitions in the unit tests.
Re: Claude and I Vibe Coded a Forth Interpreter
Posted: Thu Mar 26, 2026 7:50 am
by BigEd
One tactic I've heard about is careful management of contexts: once an LLM has thrashed around for a bit, the very history of that session affects future performance. So, starting a new session and seeding it with the good bits from the one which went bad, and stopping short where it didn't work, and at that point using some different, careful, explicit prompt, should lead the new session in a better direction.
Much like maintaining several active branches in source control, I'm sure this takes skill and care and concentration.
I get the impression that these new tools need new skills - they don't need no skill.