Skip to content

Fix seven data-loss and miscompile bugs from code review - #9

Merged
DNSGeek merged 7 commits into
mainfrom
claude/zealous-poitras-a6e52d
Oct 17, 2026
Merged

DNSGeek merged 7 commits into
mainfrom
claude/zealous-poitras-a6e52d

Conversation

@DNSGeek

@DNSGeek DNSGeek commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Summary

Seven confirmed bugs from the recent code review, one commit each. The C64 build is behaviour-identical apart from the fixes; the C128 editor image still links below $4000.

Editor (data loss)

  • do_new_file / quit_dirty_prompt: a cancelled or failed save no longer clears the buffer or quits. do_save_file clears IS_DIRTY only after a successful write, so a still-dirty buffer is treated as cancel.
  • F3/F6 load now prompts "SAVE BEFORE LOAD (Y/N)?" when there are unsaved changes, as README and the editor manual already promised. The Y/N/STOP wait shared by new, load and quit is factored into one dirty_prompt routine.
  • run_module_by_index sets IS_DIRTY on status $02 (buffer replaced, e.g. Renumber) and $04 (gap repositioned, e.g. Search/Replace), so module edits trigger the save prompts.

MODREN (silent truncation)

  • Pass 2 walks the linearised staging text, but advance_src still warped SRC_PTR across the editor's old gap. With pre-gap text larger than the gap, gap_size bytes of program were dropped mid-line. After setup_staging, REN_GAP is pointed at REN_GAP_END so the warp is a no-op.

I/O (session wedge)

  • loadsave.asm load and save, and modasm.asm open_output: a failed OPEN now goes through CLRCHN + CLOSE. The Kernal registers the LFN before the serial open can fail, so one DEVICE NOT PRESENT used to leave the file open and every later load/save/assemble failed with FILE OPEN. open_output also records I/O ERROR, which its caller already assumed.

MODASM (miscompile)

  • Relative-branch range check now also tests the lo byte's sign, so +128..+255 and -129..-256 report BRANCH RANGE instead of emitting a wrapped offset.

C128

  • mpop_show_error / asm_show_err read MODASM's error block via MOD_LO_BASE + $07..$0A instead of literal $C007..$C00A. On the C128 that state is at $9000 and $C000 is ROM, so assembler errors degraded to the generic message. Unchanged on the C64.

Verification

  • bash make_petproject.sh: both targets build and link.
  • python3 tests/test_include.py: all 15 checks pass, including the new branch-range regression test (BEQ/BNE over 130 NOPs must fail, +120 / -128 must assemble). The unfixed assembler accepted the +130 case and emitted offset $82.
  • MODREN fix reproduced on py65 with a nearly full buffer whose old gap start falls inside a line number: unfixed output 10 A / 20 B / 340 D ..., fixed output intact.
  • C64 .prg size deltas: editor +35, modasm +11, modren +12 bytes; all other modules byte-identical.

Noticed, not changed

  • MODSFR returns $04 for a plain find as well as a replace, so a find-only now also marks the buffer dirty (a spurious save prompt, no data loss). A distinct "edited" status from MODSFR would fix it.
  • open_output's no-filename exit still sets no error message.
  • A cancelled or failed save from any prompt leaves the gap compacted at the end of the text (cursor moves to end), as before.

🤖 Generated with Claude Code

DNSGeek and others added 7 commits October 17, 2026 11:18
do_new_file and quit_dirty_prompt called do_save_file on Y and then went
ahead and cleared the buffer / quit even when the save prompt was
cancelled or the write failed.  do_save_file clears IS_DIRTY only after a
successful write, so test it afterwards and take the existing cancel path
when the buffer is still dirty.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
README.md and docs/editor_manual.md promise a save prompt before a load
discards unsaved changes, but the F3/F6 dispatch went straight into the
filename prompt.  Factor the Y/N/STOP wait that new-file and quit used
into one dirty_prompt routine and guard both load keys with it
("SAVE BEFORE LOAD (Y/N)?").  A cancelled or failed save counts as
cancel, as in the previous commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
run_module_by_index accepted status $02 (buffer replaced, e.g. Renumber)
and $04 (gap repositioned, e.g. Search/Replace) without touching
IS_DIRTY, so those edits never triggered the save-before-new/load/quit
prompts and could be lost silently.  Set IS_DIRTY on both paths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mpop_show_error and asm_show_err read ASM_ERR / ASM_ERR_LINE_LO/HI /
ASM_ERR_MSG at hard-coded $C007..$C00A.  On the C128 the module state
block is at MOD_LO_BASE = $9000 and $C000 is ROM, so assembler errors
collapsed to the generic MODULE ERROR message.  Derive the addresses from
MOD_LO_BASE (layout.inc), which is $C000 on the C64 so that build is
unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
setup_staging linearises the program into [BUF_END - content, BUF_END),
but advance_src (used by parse_line_number and skip_to_next_cr) still
jumped SRC_PTR from the editor's old GAP_START to GAP_END.  When the
pre-gap text is larger than the gap, the staging area straddles the old
GAP_START, so pass 2 skipped gap_size bytes of program while parsing a
line number and silently truncated the renumbered text.  After
setup_staging succeeds, point REN_GAP at REN_GAP_END so the warp is a
no-op for the rest of pass 2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Kernal registers the logical file in its tables before the serial
OPEN can fail, so a DEVICE NOT PRESENT (or any other OPEN error) left
LFN 2 open for the rest of the session and every later load or save
failed with FILE OPEN.  Route OPEN failure through the existing
CLRCHN + CLOSE path in both do_load_file's body and do_save_file.

modasm's open_output had the same leak on LFN 4; it now closes the file
and records I/O ERROR, which the caller already assumed had happened.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
emit_instruction only tested that target-(PC+2) had a hi byte of $00 or
$FF, so forward branches of +128..+255 and backward branches of
-129..-256 were accepted and assembled with a wrapped offset that jumps
to the wrong address.  Require bit 7 of the lo byte to be clear for $00
and set for $FF, else BRANCH RANGE.

tests/test_include.py gains a regression test: BEQ over 130 NOPs and BNE
back over 130 NOPs must fail with BRANCH RANGE, while +120 and -128
assemble with the expected bytes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@DNSGeek
DNSGeek merged commit 0a64cd2 into main Oct 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant