From 92ef74c5c9b95c7c3e82e7f6832192109b6fb82f Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Wed, 11 Nov 2026 08:24:30 -0500 Subject: [PATCH] Give generated programs a private zero page (D157). Generated programs kept their state in system zero page $C0-$FE, which on the C128 is KERNAL and screen-editor state while CHROUT runs: the print runtime's pointer and length sat on TCOLOR/RVS/QTSW ($F2-$F4), and the KERNAL IRQ clears its index at $C0 (CAS1). HELLO.PRG printed "HELLO FROM NOVA1ELLO FROM NOVA128" in reverse video, which 8f8cf00 had hidden by masking bit 7 in the check. The ENTRY stub now masks IRQs, installs CR $0E (D129), points P0 at page $1A, sets the frame base, calls CODE and START, then restores P0, CR and flags. prgprint switches P0 back to $0000 only around each CHROUT, inlined to stay position independent. The driver test no longer masks reverse video. The larger runtime exposed an 8-bit overflow in the driver's CODE base (ENTRY + START + PRINT now exceeds 255 bytes, which surfaced as DISK ERROR); the driver now passes the 16-bit address it already computes. Full regression: 25 of 26 suites pass; parser fails only on baseline hashes that predate 221e0c5. --- newLang/docs/architecture.md | 5 ++- newLang/docs/decisions.md | 3 +- newLang/docs/memory-map.md | 13 +++++++ newLang/scripts/verify-driver-test.ps1 | 7 ++-- newLang/src/common/formats.inc | 2 + newLang/src/compiler/main.s | 53 +++++++++++++++----------- newLang/src/runtime/prgprint.s | 42 +++++++++++++++----- 7 files changed, 88 insertions(+), 37 deletions(-) diff --git a/newLang/docs/architecture.md b/newLang/docs/architecture.md index b56064a..cd860e7 100644 --- a/newLang/docs/architecture.md +++ b/newLang/docs/architecture.md @@ -65,8 +65,9 @@ The on-machine driver (`src/compiler/main.s`, E07-S05) turns those pieces into `sz_plan_code` so absolute targets match the loaded image, and writes STUB, ENTRY, START, PRINT and CODE through `output.s`. The image is `$1C01` stub, `$1C0D` entry, the runtime startup, the position-independent print runtime, -then CODE; the entry sets the D129 frame base, calls the body, runs the -startup mailbox and restores BASIC's frame pointer. The MAP records all five +then CODE; the entry masks IRQs, installs CR `$0E` and the program's private +zero page (P0 at page `$1A`), sets the D129 frame base, calls the body, runs +the startup mailbox, and restores P0, CR and flags (D157). The MAP records all five sections. The driver prints `SOURCE`, `OUTPUT` and the diagnostic count, keeps the previous output byte-identical when a compile fails or the output already exists, and reports emitter gaps rather than emitting a pointer outside the diff --git a/newLang/docs/decisions.md b/newLang/docs/decisions.md index dcaaa79..e9855de 100644 --- a/newLang/docs/decisions.md +++ b/newLang/docs/decisions.md @@ -90,7 +90,8 @@ documented default. `Proposed` means a later change still needs approval. | D154 | E07-S05 adds the on-machine driver `src/compiler/main.s`, built by `scripts/build-driver.ps1` into `build/n128c.prg` with the whole compiler, `output.s` and `src/runtime/startup.s`. The driver prompts `SOURCE:` through KERNAL `CHRIN`, accepts up to 15 characters, folds PETSCII lowercase to uppercase, loads with `src_load`, initialises `diag_init`, sets `em_on`, and plans the code base as `PRG_ENTRY_ADDR + gen_startup_size` with `sz_plan_code` before `parse_file_base`, so absolute targets match the PRG. It then writes STUB, START and CODE with `out_begin`/`out_startup`/`out_section`/`out_bytes`/`out_finish` and prints `SOURCE name`, `OUTPUT name`, `n ERRORS`. The three `em_pr_*` cells are explicitly zeroed because the generated image does not carry `src/runtime/print.s` yet: a `print` source fails with E227 instead of emitting a call into the compiler's own copy of the routine. Refusals print `OUTPUT EXISTS`, `NAME TOO LONG`, `DISK ERROR`, `LOAD FAILED`, `COMPILE FAILED, n ERRORS` or `NO CODE GENERATED`, and `out_abort` scratches only files the run created. `scripts/launch-compiler.ps1` gains `-Keys`/`-KeyDelay` so a test can type the source name, and `scripts/verify-driver-test.ps1` drives the summary, success, syntax-failure, output-collision and generated-program runs on a fresh D81. | The driver is wiring around contracts that already existed, so it adds no new compiler phase: the only new decisions are the fixed image layout (CODE follows the startup at `$1C57` for the current startup size), the name limit, and keeping print honest rather than emitting a pointer into the compiler. A clean code-only source now produces a runnable container (`COUNTER.PRG`, 263 bytes, CODE at `$1C57`, 95-byte MAP) whose generated program reports `MB_ST_PASS` in a second x128 session; a syntax error writes `BAD.ERR` (`E024`, then `E227`) and leaves an earlier `COUNTER.PRG` byte-identical, as does a refused second compile. Text `print` stays a known gap until the generated-program print runtime is emitted into the container. | -| D155 | Generated programs carry their own print runtime. `src/runtime/prgprint.s` is position independent (relative branches, no self-addressed stores, only the fixed `CHROUT` address) and is copied verbatim into a `PRINT` section between `START` and `CODE`. It works in four zero-page bytes (`$C0`, `$F2-$F4`) that a generated `ENTRY` stub saves before calling the body and restores before returning to BASIC; the stub also sets the D129 frame base to `$BF80`. `em_print` now emits `JSR` to the value in `em_pr_text`/`em_pr_byte`/`em_pr_cr` instead of a compiler trampoline, and the driver sets those cells to `PRG_ENTRY_ADDR + gen_startup_size` plus each routine's offset before parsing. The code base is planned as entry plus startup plus runtime size. The one captured string literal is copied into a `DATA` section after CODE and the driver rewrites the emitted `LDA #gen_text` pair to that copy, so the PRG never references compiler memory. | Closes the image gap E07-S05 hit: `print` compiles to a call inside the PRG, `COUNTER.MAP` gains a `PRINT` section, and `hello.n8` displays its string from a standalone PRG. Emitted sizes are unchanged (the `JSR` is still three bytes), so existing lengths and in-place recorder fixtures hold; the old trampolines remain for fixtures that point the cells at their own recorders. | +| D155 | Generated programs carry their own print runtime. `src/runtime/prgprint.s` is position independent (relative branches, no self-addressed stores, only the fixed `CHROUT` address) and is copied verbatim into a `PRINT` section between `START` and `CODE`. It works in four zero-page bytes (`$C0`, `$F2-$F4`) that a generated `ENTRY` stub saves before calling the body and restores before returning to BASIC; the stub also sets the D129 frame base to `$BF80`. *(Zero-page placement superseded by D157: these bytes now live in the program's private P0 page and are no longer saved.)* `em_print` now emits `JSR` to the value in `em_pr_text`/`em_pr_byte`/`em_pr_cr` instead of a compiler trampoline, and the driver sets those cells to `PRG_ENTRY_ADDR + gen_startup_size` plus each routine's offset before parsing. The code base is planned as entry plus startup plus runtime size. The one captured string literal is copied into a `DATA` section after CODE and the driver rewrites the emitted `LDA #gen_text` pair to that copy, so the PRG never references compiler memory. | Closes the image gap E07-S05 hit: `print` compiles to a call inside the PRG, `COUNTER.MAP` gains a `PRINT` section, and `hello.n8` displays its string from a standalone PRG. Emitted sizes are unchanged (the `JSR` is still three bytes), so existing lengths and in-place recorder fixtures hold; the old trampolines remain for fixtures that point the cells at their own recorders. | +| D157 | Generated programs own a private zero page. The `ENTRY` stub does `PHP`/`SEI`, saves the entry CR and installs CR `$0E` (D129), and points P0 at bank 0 page `$1A` (`PRG_ZP_PAGE`, the compiler's own private page, idle once BASIC runs the PRG); it sets the frame base, calls CODE then START, then restores P0 to `$0000`, the CR and the flags. Every zero-page cell a generated program uses (frame ABI, `MB_SELF`, `EM_LP`/`EM_TMP`, `IF_RESULT`, for-end/step values, the print runtime's `$C0`/`$F2-$F4`) is therefore private. `prgprint.s` switches P0 back to `$0000` only around each `CHROUT` (inlined, because a JSR to a shared helper is not position independent) and re-masks IRQs before restoring P0. `ENTRY` is 49 bytes and no longer saves system bytes. The driver passes the 16-bit CODE address to pass 3 instead of an 8-bit offset sum. Supersedes the zero-page part of D155 and the "only free zero page" premise of D153. | D153/D155 placed generated-program state in system zero page `$C0-$FE`, which on the C128 is KERNAL and screen-editor state while CHROUT runs: `$F2-$F4` are TCOLOR/RVS/QTSW, `$D0-$F9` hold the editor's line pointers, cursor and flags, and the KERNAL IRQ clears `$C0` (CAS1). HELLO.PRG printed `HELLO FROM NOVA1ELLO FROM NOVA128` in reverse video. A private P0 page removes the whole class of overlap with one stub change and costs a few cycles per character; it mirrors the compiler's own P0 + KERNAL-wrapper pattern (D117). Risks: an NMI other than RESTORE that uses zero page would see the private page while P0 is relocated (the ROM NMI entry itself uses none); IRQs stay masked for the whole program, so a future input runtime must enable them only with P0 at `$0000`. Approved by the user on 2026-11-11. | ## Deferred diff --git a/newLang/docs/memory-map.md b/newLang/docs/memory-map.md index 9aa3f0b..8fe2392 100644 --- a/newLang/docs/memory-map.md +++ b/newLang/docs/memory-map.md @@ -21,6 +21,8 @@ later stories. | Sections | ascending addresses, zero-filled gaps | | MAP | `NOVA128 MAP 1`, `ENTRY hhhh`, `SECTION name start size`, `CLASS name address size destroy count ifdir`, `METHOD name address` | | Startup | clears mailbox, producer `$03`, status PASS, then `RTS` | +| Entry stub (D157) | 49 bytes: `PHP`/`SEI`, save CR and install CR `$0E`, P0 -> page `$1A`, frame base `$BF80`, `JSR` CODE, `JSR` START, P0 -> `$0000`, restore CR, `PLP`, `RTS` | +| Section order | `STUB`, `ENTRY`, `START`, `PRINT` (`prgprint.s`), `CODE`, `DATA` | The fixed startup is copied from `src/runtime/startup.s`; `output.s` refuses existing `NAME.PRG`/`NAME.MAP` and scratches outputs it created on failure. @@ -280,6 +282,17 @@ variables and buffers (and anything else above `$3FFF`) before it calls `mmu_shutdown`. Two test programs read wrong values this way before the rule was written down. +### Generated programs (D157) + +A generated program runs with P0 at bank 0 page `$1A` (`PRG_ZP_PAGE`) from its +`ENTRY` stub until it returns, with IRQs masked throughout. Every zero-page +address it uses (`$C0-$FE`: print runtime, `IF_RESULT`, for values, `EM_LP`, +`EM_TMP`, `MB_SELF`, `SP`/`FP`) is physically `$1Axx`, so the KERNAL and screen +editor state in system zero page is never touched. On the C128 that state +includes `$F2-$F4` (TCOLOR, RVS, QTSW), `$D0-$F9` (editor), and `$C0` (CAS1, +cleared by the KERNAL IRQ). `prgprint.s` sets P0 to `$0000` only for each +`CHROUT` call and re-masks IRQs before relocating it again. + ## Interrupts - **IRQ:** masked in both compiler states. The ROM IRQ path (`$FF17`) jumps via diff --git a/newLang/scripts/verify-driver-test.ps1 b/newLang/scripts/verify-driver-test.ps1 index 504e146..4aed7d0 100644 --- a/newLang/scripts/verify-driver-test.ps1 +++ b/newLang/scripts/verify-driver-test.ps1 @@ -77,9 +77,10 @@ function Get-ScreenText { $ram = Read-Nova128Memory $Session 0x0400 1000 $rows = for ($r = 0; $r -lt 25; $r++) { $chars = for ($c = 0; $c -lt 40; $c++) { - # The C128 40-column display stores the printed bytes with bit 7 - # set ($88 for H, $A0 for space), so mask it before decoding. - $code = $ram[$r * 40 + $c] -band 0x7F + # Do not mask bit 7: a set bit is reverse video, which means the + # program left the editor's RVS flag ($F3) on (D157). Such cells + # decode to '?' so the literal search fails. + $code = $ram[$r * 40 + $c] if ($code -le 0x1F) { [char]($code + 0x40) } elseif ($code -le 0x3F) { [char]$code } else { '?' } } -join $chars diff --git a/newLang/src/common/formats.inc b/newLang/src/common/formats.inc index f0f20d4..09d2561 100644 --- a/newLang/src/common/formats.inc +++ b/newLang/src/common/formats.inc @@ -11,6 +11,8 @@ FORMATS_INC = 1 PRG_LOAD_ADDR = $1C01 PRG_ENTRY_ADDR = $1C0D PRG_STUB_SIZE = 12 +PRG_ZP_PAGE = $1A ; P0 page of generated programs (D157) +PRG_CR = $0E ; D129 generated-program mapping OUT_MAX_SECTIONS = 8 OUT_SECTION_NAME_MAX = 12 diff --git a/newLang/src/compiler/main.s b/newLang/src/compiler/main.s index 9fc1f4c..9acd837 100644 --- a/newLang/src/compiler/main.s +++ b/newLang/src/compiler/main.s @@ -29,7 +29,7 @@ .include "../common/source.inc" NAME_MAX = 15 -ENTRY_SIZE = 65 +ENTRY_SIZE = 49 ; prefix + two JSRs + suffix (asserted) .import mmu_init, mmu_bank0, mmu_shutdown, kcall_inline .import src_load @@ -39,7 +39,7 @@ ENTRY_SIZE = 65 .import em_on, em_bad, em_pr_text, em_pr_byte, em_pr_cr .import gen_text, gen_text_len .import cg_code, cg_len -.import sz_plan_code, sz_code_base +.import sz_code_base .import out_begin, out_section, out_end_section, out_bytes, out_byte .import out_finish, out_abort, out_err .import out_load, out_entry, out_start @@ -162,12 +162,10 @@ driver_entry: sta diag_screen sta em_on - lda #ENTRY_SIZE ; code follows entry, startup, runtime - clc - adc #entry_prefix - ldy #34 + ldy #<(entry_prefix_end - entry_prefix) jsr out_bytes bcs @fail lda drv_code @@ -395,7 +394,7 @@ emit_entry: bcs @fail lda #entry_suffix - ldy #25 + ldy #<(entry_suffix_end - entry_suffix) jmp out_bytes @fail: rts @@ -532,16 +531,24 @@ sec_entry: .byte "ENTRY" sec_start: .byte "START" sec_print: .byte "PRINT" sec_data: .byte "DATA" -; Save the working zero page the print runtime uses, then set the D129 -; frame base ($BF80). Push order: $C0, $F2, $F3, $F4, $FD, $FE. -entry_prefix: .byte $AD, $C0, $00, $48, $AD, $F2, $00, $48 - .byte $AD, $F3, $00, $48, $AD, $F4, $00, $48 - .byte $AD, $FD, $00, $48, $AD, $FE, $00, $48 - .byte $A9, $80, $8D, $FD, $00, $A9, $BF, $8D, $FE, $00 -; Restore in reverse order, then return to BASIC. -entry_suffix: .byte $68, $8D, $FE, $00, $68, $8D, $FD, $00 - .byte $68, $8D, $F4, $00, $68, $8D, $F3, $00 - .byte $68, $8D, $F2, $00, $68, $8D, $C0, $00, $60 +; D157 generated entry. Mask IRQs, install the D129 mapping (CR $0E), give +; the program its own zero page by pointing P0 at page PRG_ZP_PAGE, and set the +; frame base ($BF80). The program never touches system zero page, so BASIC's +; and the screen editor's bytes need no save; prgprint switches P0 back to +; $0000 only around each CHROUT. Then CODE, START (emit_entry adds the JSRs). +entry_prefix: .byte $08, $78 ; PHP, SEI + .byte $AD, MMU_CR, $48 ; LDA CR, PHA + .byte $A9, PRG_CR, $8D, MMU_CR + .byte $A9, $00, $8D, MMU_P0H ; P0H first + .byte $A9, PRG_ZP_PAGE, $8D, MMU_P0L + .byte $A9, $80, $85, $FD, $A9, $BF, $85, $FE +entry_prefix_end: +; P0 back to $0000, restore the entry CR and flags, return to BASIC. +entry_suffix: .byte $A9, $00, $8D, MMU_P0H + .byte $8D, MMU_P0L + .byte $68, $8D, MMU_CR ; PLA, STA CR + .byte $28, $60 ; PLP, RTS +entry_suffix_end: banner: .byte "NOVA128 COMPILER", $0D, "SOURCE: " banner_end: src_label: .byte "SOURCE " @@ -589,3 +596,5 @@ drv_code: .res 2 drv_text: .res 2 drv_tmp_lo: .res 1 drv_tmp_hi: .res 1 + +.assert ENTRY_SIZE = (entry_prefix_end - entry_prefix) + 6 + (entry_suffix_end - entry_suffix), error, "ENTRY_SIZE does not match the entry stub" diff --git a/newLang/src/runtime/prgprint.s b/newLang/src/runtime/prgprint.s index 8566bf2..cedbcd7 100644 --- a/newLang/src/runtime/prgprint.s +++ b/newLang/src/runtime/prgprint.s @@ -2,14 +2,19 @@ ; ; output.s writes these bytes into the generated image ahead of CODE, so they ; must be position independent: no references to compiler data or to their own -; addresses, only relative branches and the fixed KERNAL CHROUT address. +; addresses, only relative branches, the fixed KERNAL CHROUT address and the +; MMU P0 registers. ; -; Working storage is four zero-page bytes that the generated entry stub saves -; and restores around the program body: +; Working storage is four bytes of the program's private zero page (D157: the +; entry stub points P0 at page PRG_ZP_PAGE with IRQs masked): ; $F2-$F3 text pointer ; $F4 text length ; $C0 text index / byte value scratch -; None of them belongs to the D129 frame ABI ($FB-$FE) or the emitter cells. +; None of them belongs to the D129 frame ABI ($FB-$FE). CHROUT needs the system +; zero page (on the C128 $F2-$F4 are the editor's TCOLOR/RVS/QTSW, and the IRQ +; clears $C0), so every call goes through SYS_CHROUT, which switches P0 back +; to $0000 for the call only. Each use is inlined: a JSR to a shared helper +; would not be position independent. ; ; Contract: ; pp_text A/X = address, Y = length (0..255); prints the bytes, clobbers A. @@ -19,11 +24,28 @@ .setcpu "6502" .include "../common/c128.inc" +.include "../common/formats.inc" PP_SRC = $F2 PP_LEN = $F4 PP_IDX = $C0 +; A = character. P0 -> $0000, CHROUT, re-mask IRQs, P0 -> PRG_ZP_PAGE. +; Clobbers A; X and Y are whatever CHROUT leaves. +.macro SYS_CHROUT + pha + lda #$00 + sta MMU_P0H ; high first, low commits + sta MMU_P0L + pla + jsr CHROUT + sei + lda #$00 + sta MMU_P0H + lda #PRG_ZP_PAGE + sta MMU_P0L +.endmacro + .export pp_start, pp_size .export pp_off_text, pp_off_byte, pp_off_cr @@ -40,7 +62,7 @@ pp_text: sta PP_IDX @loop: ldy PP_IDX lda (PP_SRC),y - jsr CHROUT + SYS_CHROUT inc PP_IDX lda PP_IDX cmp PP_LEN @@ -63,7 +85,7 @@ pp_byte: cpx #'0' beq @tens txa - jsr CHROUT + SYS_CHROUT @tens: ldx #'0' @ten_loop: lda PP_SRC @@ -77,14 +99,16 @@ pp_byte: cpx #'0' beq @units txa - jsr CHROUT + SYS_CHROUT @units: lda PP_SRC clc adc #'0' - jmp CHROUT + SYS_CHROUT + rts pp_cr: lda #$0D - jmp CHROUT + SYS_CHROUT + rts pp_end: pp_size = pp_end - pp_start