Page 6 of 8

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Thu Mar 26, 2026 1:54 pm
by Martin_H
BigEd wrote:
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.
Yes, what I'm doing today is starting a new session, feeding the background information prompt in GitHub, and then giving Claude just enough information needed to work on the current problem. I also will ask for the smallest possible deliverable to keep Claude focused. This is also to avoid Claude going compute bound and exhausting my tokens.

Claude's generated DOTHEX_CODE didn't work, but I didn't bother debugging it because I have a working version in print.s which I used instead. So, I'm not sure how close Claude was. Because I have working output routines from another project, I won't rely on Claude for that code. I've already ported the compareTest.s to use the interpreters I/O routines.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Thu Mar 26, 2026 11:52 pm
by Martin_H
I removed my scaffolding code, and the unit tests use the interpreter's I/O routines. I updated prompt.txt and asked Claude a more constrained task. After loading the prompt, I asked it to fetch my source and implement the COMPARE function. The good news is the generate source assembles on the first try! I haven't tested it yet, but the dialog was interesting, so I am posting it here

Me: Fetch all the relevant source files and implement the compare function.
Claude's response:

Code: Select all

Let me fetch all the relevant source files first.
Good, I have everything I need. Now let me implement COMPARE. Let me carefully work through the design before writing code.

Design considerations:
Stack: ( addr1 u1 addr2 u2 -- n ) → consumes 4 cells, leaves 1
Returns 0 (equal), $FFFF (-1, str1 < str2), $0001 (1, str1 > str2)
Compare byte-by-byte; if equal up to MIN(u1,u2), shorter string is less
Must switch to 8-bit mode for byte fetches, like C@, MOVE, etc.
Y is the IP — must be saved with PHY/PLY before using Y as an index
Can't use X or Y directly in arithmetic/logic — need TXA/TYA
Use the JSR/helper pattern (like SLASHMOD) since we need Y as a loop counter
SCRATCH0 and SCRATCH1 are available ZP pointers; TMPA/TMPB for counters
Stack-relative locals for the two addresses, like MOVE_CODE uses

Stack layout on entry (X = PSP):
0,X = u2
2,X = addr2
4,X = u1
6,X = addr1
Plan:

Use JSR COMPARE_IMPL pattern so Y is free inside the helper
Pop all 4 args, find MIN(u1,u2) as loop count
Loop byte-by-byte comparing; on first difference return $FFFF or $0001
After loop, compare lengths: equal → 0, u1 < u2 → $FFFF, u1 > u2 → $0001
Push result, then NEXT
Claude emitted the code and it assembles, so I will unit test it now. But what I found interesting was this part of the plan:
"Use JSR COMPARE_IMPL pattern so Y is free inside the helper"
What's interesting is that pattern isn't used to free the Y register. It's used to allow another Forth primitive to JSR into an implementation to reuse code. The mathematics functions use this, but they also save Y and use stack locals. So Claude assumed that pattern was required to use that technique.

After I code review, I spotted my first bug. On entry to the code segment below C contains the desired result. But the PLA, PLA stomps on it, so the STA 0,X does work. The fix is PLY, PLY, PLY to preserve C.
Quote:
@store_result:
;----------------------------------------------------------
; Tear down hw stack locals, restore IP, push result.
;----------------------------------------------------------
PLA ; discard addr1
PLA ; discard addr2
PLY ; Restore IP

DEX
DEX
STA 0,X ; Push result onto parameter stack
Update: Good news, after fixing that bug COMPARE passed unit testing.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Fri Mar 27, 2026 11:33 am
by Martin_H
Martin_H wrote:
I removed my scaffolding code, and the unit tests use the interpreter's I/O routines. I updated prompt.txt and asked Claude a more constrained task. After loading the prompt, I asked it to fetch my source and implement the COMPARE function. The good news is the generate source assembles on the first try! I haven't tested it yet, but the dialog was interesting, so I am posting it here

Me: Fetch all the relevant source files and implement the compare function.
Claude's response:

Code: Select all

Let me fetch all the relevant source files first.
Good, I have everything I need. Now let me implement COMPARE. Let me carefully work through the design before writing code.

Design considerations:
Stack: ( addr1 u1 addr2 u2 -- n ) → consumes 4 cells, leaves 1
Returns 0 (equal), $FFFF (-1, str1 < str2), $0001 (1, str1 > str2)
Compare byte-by-byte; if equal up to MIN(u1,u2), shorter string is less
Must switch to 8-bit mode for byte fetches, like C@, MOVE, etc.
Y is the IP — must be saved with PHY/PLY before using Y as an index
Can't use X or Y directly in arithmetic/logic — need TXA/TYA
Use the JSR/helper pattern (like SLASHMOD) since we need Y as a loop counter
SCRATCH0 and SCRATCH1 are available ZP pointers; TMPA/TMPB for counters
Stack-relative locals for the two addresses, like MOVE_CODE uses

Stack layout on entry (X = PSP):
0,X = u2
2,X = addr2
4,X = u1
6,X = addr1
Plan:

Use JSR COMPARE_IMPL pattern so Y is free inside the helper
Pop all 4 args, find MIN(u1,u2) as loop count
Loop byte-by-byte comparing; on first difference return $FFFF or $0001
After loop, compare lengths: equal → 0, u1 < u2 → $FFFF, u1 > u2 → $0001
Push result, then NEXT
Claude emitted the code and it assembles, so I will unit test it now. But what I found interesting was this part of the plan:
"Use JSR COMPARE_IMPL pattern so Y is free inside the helper"
What's interesting is that pattern isn't used to free the Y register. It's used to allow another Forth primitive to JSR into an implementation to reuse code. The mathematics functions use this, but they also save Y and use stack locals. So Claude assumed that pattern was required to use that technique.

After I reviewed the code, I spotted my first bug. On entry to the code segment below C contains the desired result. But the PLA, PLA stomps on it, so the STA 0,X does work. The fix is PLY, PLY, PLY to preserve C.
Quote:
@store_result:
;----------------------------------------------------------
; Tear down hw stack locals, restore IP, push result.
;----------------------------------------------------------
PLA ; discard addr1
PLA ; discard addr2
PLY ; Restore IP

DEX
DEX
STA 0,X ; Push result onto parameter stack
Update: Good news, after fixing that bug COMPARE passed unit testing.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Fri Mar 27, 2026 1:44 pm
by Martin_H
Me: The long-term goal is rewriting interpret as a colon definition. To enable that goal, please copy the do_number code from interpret to NUMBER_CODE routine. I will then unit test NUMBER_CODE. Do not modify interpret as that code will be reworked at a later time.

Claude (edited for brevity*):

Code: Select all

Looking at the source, NUMBER_CODE is currently a stub that calls do_number (the inline subroutine inside INTERPRET_CODE) but doesn't actually contain the number conversion logic itself. The goal is to make NUMBER_CODE self-contained so it can be unit tested independently. Let me re-read the relevant sections carefully before writing anything.
Check for uploaded files
No uploads, so I'll work from the fetched source.
Let me re-read the two relevant sections precisely.
The relevant sections are:
Current NUMBER_CODE stub (from the earlier fetch):
asm        HEADER  "NUMBER", NUMBER_CFA, 0, SQUOTE_CFA
        CODEPTR NUMBER_CODE
        PUBLIC  NUMBER_CODE
        .a16
        .i16
                ; ( addr -- n flag ) Convert counted string to number
                ; flag: TRUE if successful
                JSR     INTERPRET_CODE::do_number
                BCC     @ok
                ; Error
                ...
                STZ     0,X
                NEXT
@ok:            DEX
                DEX
                LDA     #$FFFF
                STA     0,X
                NEXT
        ENDPUBLIC
do_number lives as a local subroutine inside INTERPRET_CODE:: and has a known register exhaustion problem noted in comments.
The logic is sound but incomplete due to running out of ZP variables mid-way. Now I'll write the clean, self-contained NUMBER_CODE with do_number's logic
fully inlined and the register exhaustion issues resolved using the hw stack for locals — consistent with the patterns in this codebase.

Created a file, read a fileCreated a file, read a fileHere's a summary of what was done and the key decisions:
Structure
NUMBER_CODE now uses the same JSR NUMBER_IMPL / NEXT pattern as SLASHMOD and COMPARE, keeping the Forth word thin and the logic in a separately-exportable helper that your unit tests can call directly.
Fixing the register exhaustion from do_number
The original sketch ran out of ZP variables mid-loop because it was trying to hold the char pointer, BASE, source length, and accumulator all in ZP simultaneously. The fix is to push the advancing character pointer onto the hardware stack as a local (LOC_PTR = 1,S), the same technique used in MOVE_CODE and FILL_CODE. That frees enough ZP for everything else: TMPA (char count), TMPB (accumulator), SCRATCH0 (BASE), SCRATCH1 (sign flag).
Stack effect

Success: ( addr -- n TRUE ) — addr is replaced by the converted number
Failure: ( addr -- addr FALSE ) — addr is preserved so the caller (INTERPRET) can use it in an error message, matching the original do_number contract

Multiply
Uses the same repeated-addition loop as the original sketch, but correctly saves/restores Y around it with PHY/PLY since Y holds the IP at the Forth level (even though IP is already saved on the hw stack, a nested PHY here just adds another frame which is fine and self-consistent).
What to change in primitives.s
Replace the existing NUMBER_CODE / ENDPUBLIC block with this new one. The INTERPRET_CODE::do_number subroutine and all of INTERPRET_CODE remain completely untouched.
Me: Thanks, I will unit test this.

Code: Select all

Good luck with the testing! Come back when you have results and we can work through any failures together.
* Claude must be paid by the word because he can be long winded.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Fri Mar 27, 2026 2:01 pm
by Martin_H
After pasting the code, it fails to assemble due to illegal instructions.
LDA (LOC_PTR,S)
INC LOC_PTR,S
Even though my context prompt explicitly warned about these! I inform Claude and it states these warnings were in my summary prompt.

Me: After pasting the code, it fails to assemble due to illegal instructions. Do not regenerate the code as I will fix it. But these are the instructions:
LDA (LOC_PTR,S)
INC LOC_PTR,S

Claude:

Code: Select all

Good catches. Those are both illegal on the 65816:
LDA (LOC_PTR,S) — stack-relative indirect requires a Y index. The valid form is LDA (LOC_PTR,S),Y, so Y must be set to 0 before using it.
INC LOC_PTR,S — there is no INC for stack-relative addressing. The workaround is to load, increment in A, and store back:
LDA     LOC_PTR,S
INC     A
STA     LOC_PTR,S
These are the same constraints noted in the prompt summary — INC only supports direct page, absolute, and accumulator modes, and stack-relative indirect always requires the ,Y suffix.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Fri Mar 27, 2026 3:46 pm
by BigDumbDinosaur
Martin_H wrote:
After pasting the code, it fails to assemble due to illegal instructions...
Claude wrote:
Good catches. Those are both illegal on the 65816:
LDA (LOC_PTR,S) — stack-relative indirect requires a Y index. The valid form is LDA (LOC_PTR,S),Y, so Y must be set to 0 before using it.
I don’t get it.  If Claude knows that is bogus code—“Good catches”, why did he even offer it to you?

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Fri Mar 27, 2026 3:59 pm
by BigEd
It's the nature of LLMs. It's not a person.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Fri Mar 27, 2026 5:44 pm
by Martin_H
Here's Claude's original number parsing code which had problems assembling, but overall seemed structurally sound:

Code: Select all

        LOC_PTR = 1             ; hw stack offset for current char pointer
        ; saved IP at 3,S after PHY + PHA

                PHY                     ; Save IP

                ;----------------------------------------------------------
                ; Load address, read length byte, set up char pointer.
                ;----------------------------------------------------------
                LDA     0,X             ; addr (counted string)
                STA     SCRATCH0        ; SCRATCH0 = addr temporarily

                SEP     #$20            ; 8-bit for byte fetch
                .a8
                LDA     (SCRATCH0)      ; length byte
                REP     #$20
                .a16
                AND     #$00FF
                BEQ     @fail           ; Empty string -> fail

                STA     TMPA            ; TMPA = character count

                ; Advance pointer to first character (addr+1)
                LDA     SCRATCH0
                INC     A
                PHA                     ; LOC_PTR = addr+1; hw stack: [ptr][saved_IP]

                ;----------------------------------------------------------
                ; Fetch BASE from user area into SCRATCH0.
                ;----------------------------------------------------------
                LDA     UP
                CLC
                ADC     #U_BASE
                STA     SCRATCH0
                LDA     (SCRATCH0)      ; BASE
                STA     SCRATCH0        ; SCRATCH0 = BASE

                ;----------------------------------------------------------
                ; Check for leading '-'.
                ;----------------------------------------------------------
                STZ     SCRATCH1        ; SCRATCH1 = sign flag = positive

                SEP     #$20
                .a8
                LDA     (LOC_PTR,S)     ; Peek at first char (Y=0 implicit)
                REP     #$20
                .a16
                AND     #$00FF
                CMP     #'-'
                BNE     @digit_loop_init

                ; Leading minus: set sign, advance pointer, decrement count
                LDA     #$FFFF
                STA     SCRATCH1        ; sign = negative
                INC     LOC_PTR,S       ; advance char pointer
                DEC     TMPA            ; one fewer char to process
                BEQ     @fail_cleanup   ; '-' alone is not a valid number

                ;----------------------------------------------------------
                ; Digit conversion loop.
                ; TMPB accumulates the result: TMPB = TMPB * BASE + digit
                ; TMPA counts remaining characters.
                ; LOC_PTR,S points to the current character.
                ; SCRATCH0 holds BASE throughout.
                ;----------------------------------------------------------
@digit_loop_init:
                STZ     TMPB            ; result accumulator = 0

@digit_loop:
                ; Fetch current character
                SEP     #$20
                .a8
                LDA     (LOC_PTR,S)
                REP     #$20
                .a16
                AND     #$00FF

                ; Convert ASCII to digit value
                CMP     #'0'
                BCC     @fail_cleanup   ; < '0' -> invalid
                CMP     #'9' + 1
                BCC     @is_decimal
                CMP     #'A'
                BCC     @fail_cleanup   ; between '9' and 'A' -> invalid
                CMP     #'F' + 1
                BCS     @fail_cleanup   ; > 'F' -> invalid
                ; Hex letter A-F
                SEC
                SBC     #'A' - 10       ; A->10, B->11, ... F->15
                BRA     @check_base

@is_decimal:
                SEC
                SBC     #'0'            ; '0'->0 ... '9'->9

@check_base:
                ; Digit value is in A. Reject if >= BASE.
                CMP     SCRATCH0        ; digit - BASE
                BCS     @fail_cleanup   ; digit >= BASE -> invalid

                ; TMPB = TMPB * BASE + digit
                ; Multiply TMPB by BASE using repeated addition,
                ; same pattern as the original do_number sketch.
                ; Save digit on hw stack temporarily.
                PHA                     ; [digit][ptr][saved_IP]

                ; Multiply: use Y as loop counter (IP already saved)
                PHY                     ; [Y][digit][ptr][saved_IP]
                LDY     SCRATCH0        ; Y = BASE
                STZ     W               ; W = product accumulator = 0
@mul_loop:
                CPY     #0
                BEQ     @mul_done
                LDA     W
                CLC
                ADC     TMPB            ; product += TMPB (original value each iter)
                STA     W
                DEY
                BRA     @mul_loop
@mul_done:
                PLY                     ; Restore Y (which is IP, still saved on hw stack)

                ; W = TMPB * BASE; add digit
                PLA                     ; digit back into A
                CLC
                ADC     W
                STA     TMPB            ; TMPB = TMPB * BASE + digit

                ; Advance pointer and loop
                INC     LOC_PTR,S
                DEC     TMPA
                BNE     @digit_loop

                ;----------------------------------------------------------
                ; All digits processed successfully.
                ; Apply sign.
                ;----------------------------------------------------------
                LDA     SCRATCH1        ; sign flag
                BEQ     @positive
                ; Negate result
                LDA     TMPB
                EOR     #$FFFF
                INC     A
                STA     TMPB
@positive:
                ; Tear down hw stack local and restore IP
                PLA                     ; discard LOC_PTR
                PLY                     ; Restore IP

                ; Replace TOS (addr) with result, push TRUE flag
                LDA     TMPB
                STA     0,X             ; TOS = result
                DEX
                DEX
                LDA     #$FFFF          ; TRUE
                STA     0,X
                RTS

                ;----------------------------------------------------------
                ; Failure paths: leave original addr on stack, push FALSE.
                ;----------------------------------------------------------
@fail_cleanup:
                PLA                     ; discard LOC_PTR
@fail:
                PLY                     ; Restore IP
                ; addr is still at 0,X (untouched)
                DEX
                DEX
                STZ     0,X             ; FALSE
                RTS
In my background context prompt, I outlined the use of the direct page register. Unfortunately, Claude didn't use that and used of page zero scratch variables.
Here's my untested line for line rework using the direct page register with stack locals:

Code: Select all

        LOC_PRODUCT = 11        ; hw stack offset for product
        LOC_RESULT = 9         ; hw stack offset for result
        LOC_BASE = 7            ; hw stack offset for base value
        LOC_SIGN = 5            ; hw stack offset for sign value
        LOC_PTR = 3             ; hw stack offset for current char pointer
        LOC_COUNT = 1           ; hw stack offset for character count
        LOC_SIZE = LOC_COUNT+LOC_PRODUCT
                PHD                     ; Save DP
                PHY                     ; Save IP

                TSC                     ; Reserve space for stack locals
                SEC
                SBC     #LOC_SIZE
                TCS

                ;----------------------------------------------------------
                ; Fetch BASE using page zero pointer into LOC_BASE
                ;----------------------------------------------------------
                LDY     #U_BASE
                LDA     (UP),Y          ; BASE
                STA     LOC_BASE,S      ; LOC_BASE = BASE

                TSC                     ; Set DP to point to stack locals.
                TCD                     ; No page zero access until reset!

                STZ     LOC_SIGN        ; LOC_SIGN = 0, assume positive

                ;----------------------------------------------------------
                ; Load address, read length byte, set up char pointer.
                ;----------------------------------------------------------
                LDA     0,X             ; addr (counted string)
                STA     LOC_PTR
                SEP     #$20            ; 8-bit for byte fetch
                .a8
                LDA     (LOC_PTR)       ; length byte
                REP     #$20
                .a16
                AND     #$00FF
                BEQ     @fail_cleanup   ; Empty string -> fail
                STA     LOC_COUNT       ; LOC_COUNT,S = character count

                ; Advance pointer to first character (addr+1)
                INC     LOC_PTR

                ;----------------------------------------------------------
                ; Check for leading '-'.
                ;----------------------------------------------------------
                SEP     #$20
                .a8
                LDA     (LOC_PTR)       ; Peek at first char
                REP     #$20
                .a16
                AND     #$00FF
                CMP     #'-'
                BNE     @digit_loop_init

                ; Leading minus: set sign, advance pointer, decrement count
                LDA     #$FFFF
                STA     LOC_SIGN        ; sign = negative
                INC     LOC_PTR         ; advance char pointer
                DEC     LOC_COUNT       ; one fewer char to process
                BEQ     @fail_cleanup   ; '-' alone is not a valid number

                ;----------------------------------------------------------
                ; Digit conversion loop.
                ; TMPB accumulates the result: TMPB = TMPB * BASE + digit
                ; TMPA counts remaining characters.
                ; LOC_PTR,S points to the current character.
                ; SCRATCH0 holds BASE throughout.
                ;----------------------------------------------------------
@digit_loop_init:
                STZ     LOC_RESULT      ; accumulate digits in result

@digit_loop:
                ; Fetch current character
                SEP     #$20
                .a8
                LDA     (LOC_PTR)
                REP     #$20
                .a16
                AND     #$00FF

                ; Convert ASCII to digit value
                CMP     #'0'
                BCC     @fail_cleanup   ; < '0' -> invalid
                CMP     #'9' + 1
                BCC     @is_decimal
                CMP     #'A'
                BCC     @fail_cleanup   ; between '9' and 'A' -> invalid
                CMP     #'F' + 1
                BCS     @fail_cleanup   ; > 'F' -> invalid
                ; Hex letter A-F
                SEC
                SBC     #'A' - 10       ; A->10, B->11, ... F->15
                BRA     @check_base

@is_decimal:
                SEC
                SBC     #'0'            ; '0'->0 ... '9'->9

@check_base:
                ; Digit value is in A. Reject if >= BASE.
                CMP     LOC_BASE        ; digit - BASE
                BCS     @fail_cleanup   ; digit >= BASE -> invalid

                ; RESULT = RESULT * BASE + digit
                ; Multiply by using repeated addition
                PHA                     ; Save digit on hw stack temporarily.

                ; Multiply: use Y as loop counter (IP already saved)
                LDY     LOC_BASE
                STZ     LOC_PRODUCT     ; product accumulator = 0
@mul_loop:
                LDA     LOC_PRODUCT
                CLC
                ADC     LOC_RESULT      ; product += LOC_RESULT
                STA     LOC_PRODUCT
                DEY
                BNE     @mul_loop

                ; PRODUCT = T * BASE; add digit
                PLA                     ; digit back into A
                CLC
                ADC     LOC_PRODUCT
                STA     LOC_RESULT

                ; Advance pointer and loop
                INC     LOC_PTR
                DEC     LOC_COUNT
                BNE     @digit_loop

                ;----------------------------------------------------------
                ; All digits processed successfully.
                ; Apply sign.
                ;----------------------------------------------------------
                LDA     LOC_SIGN        ; sign flag
                BEQ     @positive
                ; Negate result
                LDA     LOC_RESULT
                EOR     #$FFFF
                INC     A
                STA     LOC_RESULT
@positive:
                ; Replace TOS (addr) with result, push TRUE flag
                LDA     LOC_RESULT
                STA     0,X             ; TOS = result
                DEX
                DEX
                LDA     #$FFFF          ; TRUE
                STA     0,X
                BRA     @return

                ;----------------------------------------------------------
                ; Failure paths: leave original addr on stack, push FALSE.
                ;----------------------------------------------------------
@fail_cleanup:
                ; addr is still at 0,X (untouched), append status
                DEX
                DEX
                STZ     0,X             ; FALSE

@return:        ; Tear down hw stack local and restore IP
                TSC                     ; Drop locals
                CLC
                ADC    #LOC_SIZE
                TCS
                PLY                     ; Restore IP
                PLD                     ; Restore DP
                RTS
So much clearer. After testing I plan to upload this to Claude and ask it to summarize how they differ, and how I can alter my prompt to incorporate that knowledge.

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Sat Mar 28, 2026 4:04 am
by barnacle
Martin_H wrote:
* Claude must be paid by the word because he can be long winded.
It's been trained on MBA presentations. "Tell 'em what you're going to tell 'em, then tell 'em, then tell 'em what you told 'em. Three different ways."

Neil

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Sat Mar 28, 2026 6:03 am
by BigDumbDinosaur
BigEd wrote:
It's the nature of LLMs. It's not a person.
Yes, I know that.  Despite what naive users might think of services such as ChatGPT and Claude, there is no sentience involved.  It’s merely a stupid machine regurgitating information scraped from myriad websites.

I should have noted that my question was rhetorical—I know why Claude offers wrong answers.  :D

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Sat Mar 28, 2026 6:10 am
by BigDumbDinosaur
barnacle wrote:
Martin_H wrote:
* Claude must be paid by the word because he can be long winded.
It's been trained on MBA presentations.  "Tell 'em what you're going to tell 'em, then tell 'em, then tell 'em what you told 'em. Three different ways."
You can see that principle in action on Youtube, in which AI-generated videos about World War II history present a bunch of malarkey and repeat it ad infinitum...or is it ad nauseam?

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Sat Mar 28, 2026 7:25 am
by BigEd
On the subject of verbosity, I gather that - perhaps mostly at the start of a question - it's good to say more, because more tokens as input means more computation applied to make the output. This might or might not be valid information!

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Sat Mar 28, 2026 1:36 pm
by Martin_H
I think just had a rude awakening about changing the direct page register. This code doesn't produce the correct results:

Code: Select all

                PHD                     ; Save DP
                TSC
                TCD                     ; No page zero access until return!
		; addr is still at 0,X (untouched), set status FALSE
                LDA     #0              ; FALSE
                STA     0,X             ; Push status code
                ; Tear down hw stack locals, restore IP and DP
                PLD                     ; Restore DP
                RTS
But if I move the "STA 0,X" to after the PLD instruction it produces the correct results. I believe the reason is that "STA 0,X" has the value of the DP register added to the zero offset. It took hours of subtractive coding to produce this minimal example and it's the only conclusion I can reach.

Update: It took forever but I found that you can force absolute mode using this syntax: "STA a:0,X ; Push status code"

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Sat Mar 28, 2026 4:05 pm
by Martin_H
Me: I completed unit testing and reworked the code to use stack locals rather than page zero addressing. I also pointed the direct page register to the stack to allow direct page addressing on the stack locals. I would like to paste here and have you review it. Afterwards could you make suggestions on how to change my initial prompt to produce code more like this.

Claude: Please go ahead and paste the code — I'd like to see the full implementation before commenting. Once I've reviewed it I'll suggest prompt improvements.

Me: pastes code:

Claude:

Code: Select all

This is a clean implementation. A few observations:
What's better than my version:
* Moving DP to the stack frame is elegant — it turns all the locals into direct page accesses, giving you clean LDA LOC_BASE instead of LDA LOC_BASE,S everywhere, and makes INC/DEC on locals legal since direct page supports those modes
* LOC_PRODUCT as a stack local eliminates the need for W as a scratch register during multiply
* DEC LOC_SIGN from zero to $FFFF is a nice idiom for setting the negative flag — avoids a load/store of a literal
* The single @return path with BRA from the success case is tidier than my duplicated stack adjustment code
* Using a:0,X for the absolute forced addressing on the PSP writes is correct and important — with DP now pointing at the stack frame, bare 0,X could be misinterpreted

One thing to verify:
* The multiply loop does BASE iterations of product += RESULT. If BASE is 0 the loop runs 65536 times. That's an edge case that probably can't happen in practice since BASE is always set to a valid value, but worth noting.

Prompt improvements for next time:
* Specify the DP technique explicitly: "Use PHD/TCD to point the direct page register at the stack frame so locals can use direct page addressing modes including INC, DEC, and indirect (loc)."
* List forbidden ZP variables: "Do not use SCRATCH0, SCRATCH1, TMPA, TMPB, or W — all temporaries must live in the stack frame."
* Specify the addressing override for PSP: "Since DP is relocated, all parameter stack accesses via X must use absolute forced addressing, e.g. LDA a:0,X."
* Describe the frame setup pattern: "Reserve stack space with TSC / SEC / SBC #SIZE / TCS before TCD, and save/restore PHD/PLD around the frame."
* Specify the single return path pattern: "Use a single @return label with BRA from success and fall-through from failure, with stack teardown, PLY, and PLD done once."
Me: thanks

Claude: You're welcome! The DP-pointing-to-stack-frame technique is a genuinely useful pattern — worth documenting well in the prompt so future generated code uses it consistently from the start. Good luck with the rest of the Forth816 project!

Re: Claude and I Vibe Coded a Forth Interpreter

Posted: Sat Mar 28, 2026 6:12 pm
by BigDumbDinosaur
Martin_H wrote:
I think just had a rude awakening about changing the direct page register...the reason is that "STA 0,X" has the value of the DP register added to the zero offset.
Yep!  All direct page accesses involve adding whatever is in DP to the address, even when DP is set to $0000.

Something you should note when relocating DP to the stack is merely copying SP to DP creates the potential for an off-by-one bug, viz...

Code: Select all

         rep #%00100000        ;16-bit accumulator
         tsc                   ;SP —> .C
         tcd                   ;.C —> DP (SP —> DP)
         lda 0                 ;off-by-one error...
;
;        ————————————————————————————————————————————————————
;        LDA 0 will load from SP & SP+1, resulting in the LSB
;        being undefined content, while the MSB will (should)
;        be defined content.
;        ————————————————————————————————————————————————————

The “proper” method is...

Code: Select all

         rep #%00100000        ;16-bit accumulator
         tsc                   ;SP —> .C
         inc                   ;effectively SP+1
         tcd                   ;.C —> DP (SP+1 —> DP)
         lda 0                 ;loads from SP+1 & SP+2

Although relocating direct page to the stack will almost always result in DP not starting on a page boundary, the one-cycle access penalty that will result is a minimal price to pay for the flexibility that is gained.

Claude wrote:
* DEC LOC_SIGN from zero to $FFFF is a nice idiom for setting the negative flag — avoids a load/store of a literal
Don’t forget you can set/clear any of the flags with SEP/REP, which is non-destructive and completes in three Ø2 cycles.  DEC on a direct-page location requires between five and seven cycles, depending on accumulator width and DP alignment.