Page 5 of 8

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Mon Mar 23, 2026 7:32 pm
by BigDumbDinosaur
BigEd wrote:
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
BigEd wrote:
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
Martin_H wrote:

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.
Quote:

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.
Quote:

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.
Quote:

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.
Quote:

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.

Quote:

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.  :D

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
leepivonka wrote:
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
leepivonka wrote:
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
Martin_H wrote:
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.  :D

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.