diff --git a/editor.asm b/editor.asm index affb35d..48a0a8b 100644 --- a/editor.asm +++ b/editor.asm @@ -555,10 +555,15 @@ main_loop: jsr do_settings_popover jmp main_loop @do_load: + jsr load_dirty_check ; unsaved changes? C=1 → cancelled + bcs @load_cancelled jsr do_load_file jmp main_loop @do_load_source: + jsr load_dirty_check + bcs @load_cancelled jsr do_load_source_file +@load_cancelled: jmp main_loop @do_save: jsr do_save_file @@ -660,26 +665,10 @@ move_begin: do_new_file: lda IS_DIRTY beq @clear - ; Prompt — reverse-video alert bar so it can't be mistaken for idle status. lda #new_dirty_text - jsr modal_draw_text -@wait: - jsr GETIN - beq @wait - cmp #PET_STOP - beq @cancel - cmp #'Y' - beq @save - cmp #'y' - beq @save - cmp #'N' - beq @clear - cmp #'n' - beq @clear - jmp @wait -@save: - jsr do_save_file ; if save is cancelled, still proceed to clear + jsr dirty_prompt ; C=1 → cancelled (STOP, or the save didn't happen) + bcs @done @clear: ; Reset gap buffer to empty lda #quit_dirty_text - jsr modal_draw_text + jmp dirty_prompt + +quit_dirty_text: + ; "SAVE BEFORE QUIT (Y/N)?" in screen codes + .byte $13,$01,$16,$05,$20,$02,$05,$06,$0F,$12,$05,$20,$11,$15,$09,$14 + .byte $20,$28,$19,$2F,$0E,$29,$3F, 0 + +; ============================================================================ +; load_dirty_check — F3/F6 guard: ask before a load discards unsaved changes. +; Returns C=0 to go ahead with the load, C=1 if the user cancelled. +; ============================================================================ + +load_dirty_check: + lda IS_DIRTY + beq @clean + lda #load_dirty_text + jmp dirty_prompt +@clean: + clc + rts + +load_dirty_text: + ; "SAVE BEFORE LOAD (Y/N)?" in screen codes + .byte $13,$01,$16,$05,$20,$02,$05,$06,$0F,$12,$05,$20,$0C,$0F,$01,$04 + .byte $20,$28,$19,$2F,$0E,$29,$3F, 0 + +; ============================================================================ +; dirty_prompt — "SAVE BEFORE xxx (Y/N)?" modal shared by new, load and quit. +; +; Entry: A/X = lo/hi of the prompt text (screen codes, modal_draw_text format). +; Y: calls do_save_file. Returns C=0 if the write succeeded; C=1 if the +; save was cancelled or failed (do_save_file leaves IS_DIRTY set then). +; N: returns C=0 — proceed, discarding the changes. +; STOP: returns C=1 — cancel; the idle status bar is restored via modal_exit. +; Clobbers: A, X, Y. +; ============================================================================ + +dirty_prompt: + jsr modal_draw_text ; reverse-video alert bar @wait: jsr GETIN beq @wait @@ -742,15 +766,15 @@ quit_dirty_prompt: cmp #'y' beq @save cmp #'N' - beq @nosave + beq @proceed cmp #'n' - beq @nosave + beq @proceed jmp @wait @save: - jsr do_save_file ; save — clears IS_DIRTY on success - clc - rts -@nosave: + jsr do_save_file ; clears IS_DIRTY only on a successful write + lda IS_DIRTY + bne @cancel ; save cancelled or failed: keep editing +@proceed: clc rts @cancel: @@ -758,11 +782,6 @@ quit_dirty_prompt: sec rts -quit_dirty_text: - ; "SAVE BEFORE QUIT (Y/N)?" in screen codes - .byte $13,$01,$16,$05,$20,$02,$05,$06,$0F,$12,$05,$20,$11,$15,$09,$14 - .byte $20,$28,$19,$2F,$0E,$29,$3F, 0 - ; ============================================================================ ; init_settings — write compiled-in defaults into the SETTING_* BSS bytes. ; Called on both cold and warm start before load_settings overlays from disk. diff --git a/loadsave.asm b/loadsave.asm index 8a9983a..5f61270 100644 --- a/loadsave.asm +++ b/loadsave.asm @@ -166,9 +166,12 @@ load_body: @name_done: ; ---- OPEN ---- + ; The Kernal registers the logical file before the serial OPEN can fail + ; (DEVICE NOT PRESENT etc.), so a failed OPEN must still CLOSE IO_LA or + ; every later load/save fails with FILE OPEN for the rest of the session. jsr OPEN bcc :+ - jmp @io_err + jmp @close_err : ; Check drive status after OPEN jsr READST @@ -459,7 +462,7 @@ do_save_file: ; ---- OPEN ---- jsr OPEN bcc :+ - jmp @io_err + jmp @close_err ; LFN is registered even when OPEN fails : jsr READST beq :+ diff --git a/modasm.asm b/modasm.asm index 06820a1..de129a7 100644 --- a/modasm.asm +++ b/modasm.asm @@ -1934,8 +1934,11 @@ emit_instruction: ; FIX (branch offset): compute signed relative offset as a proper 16-bit ; subtraction: offset = target - (PC + 2). ; PC still points to the branch opcode (not yet advanced). - ; The hi byte of the result must be $00 (forward) or $FF (backward); - ; anything else means the target is more than 128 bytes away. + ; The hi byte of the result must be $00 (forward) or $FF (backward), and + ; the lo byte's sign must agree with it: $00xx needs bit 7 of xx clear + ; (offset 0..+127), $FFxx needs it set (offset -128..-1). Checking the + ; hi byte alone accepted +128..+255 and -129..-256 and emitted an offset + ; that branched to the wrong place. ; ; Compute PC+2 into TMP2 lda ASM_PC_LO @@ -1951,13 +1954,18 @@ emit_instruction: sbc TMP2 sta TMP ; lo byte = branch offset to emit lda ASM_VAL_HI - sbc TMP2+1 ; hi byte: $00=forward in range, $FF=backward in range - beq @rel_ok + sbc TMP2+1 ; hi byte: $00=forward, $FF=backward + beq @rel_fwd cmp #$FF bne @rel_range_err + lda TMP ; backward: lo must be $80..$FF + bpl @rel_range_err ; $FF00..$FF7F = -256..-129 + bmi @rel_ok +@rel_fwd: + lda TMP ; forward: lo must be $00..$7F + bmi @rel_range_err ; $0080..$00FF = +128..+255 @rel_ok: - lda TMP - jsr emit_byte + jsr emit_byte ; A = offset lo byte bcs @err jmp @done @@ -2387,7 +2395,7 @@ open_output: jsr SETNAM jsr OPEN - bcs @err + bcs @err_close ; LFN is registered even when OPEN fails ldx #ASM_OUT_LA_VAL jsr CHKOUT @@ -2407,8 +2415,13 @@ open_output: rts @err_close: + ; The Kernal registers the logical file before the serial OPEN can fail + ; (DEVICE NOT PRESENT etc.): without this CLOSE every later assembly in + ; the session fails with FILE OPEN on LFN 4. The caller expects an + ; error to have been recorded, so record I/O ERROR here. lda #ASM_OUT_LA_VAL jsr CLOSE + jsr set_err_io @err: sec rts diff --git a/modren.asm b/modren.asm index 66e488c..a66d0ef 100644 --- a/modren.asm +++ b/modren.asm @@ -244,6 +244,17 @@ pass2: jsr setup_staging ; linearise source, set SRC_PTR bcs @error + ; The source is now linear in [staging_start..BUF_END): the editor's gap + ; no longer exists in it. advance_src (used by parse_line_number and + ; skip_to_next_cr) still warps SRC_PTR from REN_GAP to REN_GAP_END when + ; it lands on the old GAP_START — with a pre-gap larger than the gap, + ; the staging area straddles that address and the warp silently dropped + ; gap_size bytes of program mid-line. Make the warp a no-op from here on. + lda REN_GAP_END_LO + sta REN_GAP_LO + lda REN_GAP_END_HI + sta REN_GAP_HI + ; DST_PTR = work_buf lda REN_BUF_LO sta DST_PTR diff --git a/modules.asm b/modules.asm index 2812b88..2567ec5 100644 --- a/modules.asm +++ b/modules.asm @@ -99,7 +99,7 @@ MPOP_ITEM_ROW0 = (MPOP_TOP + 3) ; screen row of first item (title at TOP+1, MOD_LOAD_SAVE_LO = $0228 MOD_LOAD_SAVE_HI = $0229 ; Index of the most recently run module — lets mpop_show_error honor -; MODASM's rich-error flag ($C007) only when MODASM actually ran. +; MODASM's rich-error flag (ASM_ERR) only when MODASM actually ran. MOD_LAST_IDX = $022A MOD_IDX_MODASM = 0 @@ -837,6 +837,10 @@ run_sel_loaded: sta CURSOR_ROW sta CURSOR_COL jsr ensure_cursor_visible ; scroll/compute so display agrees with the gap + ; The module rewrote the buffer (renumber, tokenize...): the text no + ; longer matches what is on disk, so the new/load/quit prompts must fire. + lda #$FF + sta IS_DIRTY rts @check_reposition: @@ -856,6 +860,10 @@ run_sel_loaded: sta GAP_END+1 ; TOP_LINE intentionally not reset — ensure_cursor_visible (called by ; do_search_replace after we return) will scroll as needed. + ; MODSFR reports $04 after a replace as well as after a plain find, and + ; does not say which; a replace edits the buffer, so mark it dirty. + lda #$FF + sta IS_DIRTY rts @check_err: @@ -880,15 +888,15 @@ run_sel_load_err: ; ============================================================================ mpop_show_error: - ; If MODASM set ASM_ERR ($C007=$FF), show rich "ERR NNNN: MSGTEXT" display. + ; If MODASM set ASM_ERR (=$FF), show rich "ERR NNNN: MSGTEXT" display. ; Gate on "MODASM was the module that just ran": five other modules - ; LOAD at $C000, so $C007 is one of THEIR code bytes — a coincidental - ; $FF there (or a stale flag from an earlier MODASM failure) used to - ; display garbage as a rich assembler error for unrelated failures. + ; LOAD at MOD_LO_BASE, so ASM_ERR is one of THEIR code bytes — a + ; coincidental $FF there (or a stale flag from an earlier MODASM failure) + ; used to display garbage as a rich assembler error for unrelated failures. lda MOD_LAST_IDX cmp #MOD_IDX_MODASM bne @generic - lda $C007 ; ASM_ERR flag written by MODASM's set_err_common + lda ASM_ERR ; flag written by MODASM's set_err_common cmp #$FF beq asm_show_err ; MODASM error → rich display, includes wait @generic: @@ -916,8 +924,8 @@ mpop_show_error: ; ============================================================================ ; asm_show_err — rich MODASM error display. ; -; Reads ASM_ERR_LINE_LO/HI ($C008/$C009) and ASM_ERR_MSG ($C00A, screen codes, -; zero-terminated) written by MODASM's set_err_common, and formats: +; Reads ASM_ERR_LINE_LO/HI and ASM_ERR_MSG (screen codes, zero-terminated) +; written by MODASM's set_err_common, and formats: ; ; ERR NNNN: MSGTEXT ; @@ -932,9 +940,13 @@ mpop_show_error: asm_err_prefix_txt: .byte $05,$12,$12,$20 ; "ERR " -ASM_ERR_LINE_LO = $C008 -ASM_ERR_LINE_HI = $C009 -ASM_ERR_MSG = $C00A +; MODASM's error block sits at the start of its ASM_STATE block, which is +; MOD_LO_BASE (layout.inc): $C000 on the C64, $9000 on the C128. Offsets +; must match ASM_ERR / ASM_ERR_LINE_* / ASM_ERR_MSG in modasm.asm. +ASM_ERR = MOD_LO_BASE + $07 +ASM_ERR_LINE_LO = MOD_LO_BASE + $08 +ASM_ERR_LINE_HI = MOD_LO_BASE + $09 +ASM_ERR_MSG = MOD_LO_BASE + $0A asm_show_err: ; --- 1. Write "ERR " prefix --- @@ -953,9 +965,9 @@ asm_show_err: ; Load 16-bit line number into AN_CUR (reuse existing BSS scratch). ; an_emit_number writes via do_insert; we can't use it here. ; Instead: same repeated-subtraction algorithm, writing to STATUS_ROW,Y. - lda $C008 ; ASM_ERR_LINE_LO + lda ASM_ERR_LINE_LO sta AN_CUR - lda $C009 ; ASM_ERR_LINE_HI + lda ASM_ERR_LINE_HI sta AN_CUR+1 ; Reuse an_div_lo/an_div_hi table from editor.asm (same link unit). diff --git a/tests/test_include.py b/tests/test_include.py index c9f6676..01d5988 100755 --- a/tests/test_include.py +++ b/tests/test_include.py @@ -480,6 +480,26 @@ def chain(n): equivalence("mixed directives across files", src, files) print("ok: data + code directives across multiple files") + # 14. Relative branch range. A branch whose offset is +128..+255 or + # -129..-256 has a hi byte of $00/$FF like an in-range one, so the + # check must also look at the sign of the lo byte. + def nops(n): + return " NOP\n" * n + + # forward: BEQ over 120 NOPs = +120, fine; over 130 = +130, out of range + src = " *= $C000\n BEQ FAR\n" + nops(120) + "FAR: RTS\n" + m = assemble(src) + expect_output(m, bytes.fromhex("00c0f078") + b"\xea" * 120 + b"\x60", "BEQ +120") + m = assemble(" *= $C000\n BEQ FAR\n" + nops(130) + "FAR: RTS\n") + expect_error(m, "BRANCH RANGE", "BEQ +130", line=2) + # backward: RTS; 125 NOPs; BNE BACK = -128, fine; 130 NOPs = -133, out of range + src = " *= $C000\nBACK: RTS\n" + nops(125) + " BNE BACK\n" + m = assemble(src) + expect_output(m, bytes.fromhex("00c060") + b"\xea" * 125 + b"\xd0\x80", "BNE -128") + m = assemble(" *= $C000\nBACK: RTS\n" + nops(130) + " BNE BACK\n") + expect_error(m, "BRANCH RANGE", "BNE -133", line=133) + print("ok: relative branch range (lo-byte sign checked)") + print("\nall include tests passed")