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.
Claude and I Vibe Coded a Forth Interpreter
Re: Claude and I Vibe Coded a Forth Interpreter
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.
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
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:
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.
Update: Good news, after fixing that bug COMPARE passed unit testing.
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
"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
;----------------------------------------------------------
; 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
Re: Claude and I Vibe Coded a Forth Interpreter
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:
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.
Update: Good news, after fixing that bug COMPARE passed unit testing.
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
"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
;----------------------------------------------------------
; 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
Re: Claude and I Vibe Coded a Forth Interpreter
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*):
Me: Thanks, I will unit test this.
* Claude must be paid by the word because he can be long winded.
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.
Code: Select all
Good luck with the testing! Come back when you have results and we can work through any failures together.
Last edited by Martin_H on Fri Mar 27, 2026 2:03 pm, edited 2 times in total.
Re: Claude and I Vibe Coded a Forth Interpreter
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:
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.
- BigDumbDinosaur
- Posts: 9425
- Joined: 28 May 2009
- Location: Midwestern USA (JB Pritzker’s dystopia)
- Contact:
Re: Claude and I Vibe Coded a Forth Interpreter
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.
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.
x86? We ain't got no x86. We don't NEED no stinking x86!
Re: Claude and I Vibe Coded a Forth Interpreter
It's the nature of LLMs. It's not a person.
Re: Claude and I Vibe Coded a Forth Interpreter
Here's Claude's original number parsing code which had problems assembling, but overall seemed structurally sound:
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:
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.
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
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
Re: Claude and I Vibe Coded a Forth Interpreter
Martin_H wrote:
* Claude must be paid by the word because he can be long winded.
Neil
- BigDumbDinosaur
- Posts: 9425
- Joined: 28 May 2009
- Location: Midwestern USA (JB Pritzker’s dystopia)
- Contact:
Re: Claude and I Vibe Coded a Forth Interpreter
BigEd wrote:
It's the nature of LLMs. It's not a person.
I should have noted that my question was rhetorical—I know why Claude offers wrong answers.
x86? We ain't got no x86. We don't NEED no stinking x86!
- BigDumbDinosaur
- Posts: 9425
- Joined: 28 May 2009
- Location: Midwestern USA (JB Pritzker’s dystopia)
- Contact:
Re: Claude and I Vibe Coded a Forth Interpreter
barnacle wrote:
Martin_H wrote:
* Claude must be paid by the word because he can be long winded.
x86? We ain't got no x86. We don't NEED no stinking x86!
Re: Claude and I Vibe Coded a Forth Interpreter
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
I think just had a rude awakening about changing the direct page register. This code doesn't produce the correct results:
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"
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
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
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:
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!
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."
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!
- BigDumbDinosaur
- Posts: 9425
- Joined: 28 May 2009
- Location: Midwestern USA (JB Pritzker’s dystopia)
- Contact:
Re: Claude and I Vibe Coded a Forth Interpreter
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.
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+2Although 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
x86? We ain't got no x86. We don't NEED no stinking x86!