diff --git a/.gitattributes b/.gitattributes index 351ffdb..04af040 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,3 +7,7 @@ *.d64 binary *.d71 binary *.d81 binary + +# Nova128 sources and fixtures are byte-exact test inputs (PETSCII conversion, +# validators anchored on line ends). Keep them LF on every platform. +newLang/** text=auto eol=lf diff --git a/newLang/cfg/compiler.cfg b/newLang/cfg/compiler.cfg new file mode 100644 index 0000000..5489761 --- /dev/null +++ b/newLang/cfg/compiler.cfg @@ -0,0 +1,54 @@ +# Nova128 compiler memory map (E01-S05). docs/memory-map.md explains each +# region and the evidence behind it. ld65 refuses any segment that overflows +# its area, so this layout is also the compiler's capacity check. +# +# Bank 0 (CR=$0E) Bank 1 (CR=$4E) +# $0000-$12FF system (BASIC/KERNAL) $0000-$1FFF hidden by common RAM +# $1300-$18FF COMMON (copied at init) $2000-$BFFF BANK1 compiler tables +# $1900-$19FF DISKBUF $C000-$FFFF system ROM (unmapped RAM) +# $1A00-$1AFF private zero page (P0) +# $1B00-$1BEF SCRATCH (common) +# $1BF0-$1BFF MAILBOX +# $1C01-$B7FF MAIN (stub, code, data) +# $B800-$BFFF SWSTACK +# $C000-$FFFF system ROM + +MEMORY { + # Logical zero page. With P0 -> $1A00 these are physical $1A02-$1AFF. + # $00/$01 are the 8502 port and are never allocated. + ZP: file = "", start = $0002, size = $00FE, type = rw, define = yes; + + LOADADDR: file = %O, start = $1BFF, size = $0002; + HEADER: file = %O, start = $1C01, size = $000C; + MAIN: file = %O, start = $1C0D, size = $9BF3, define = yes; # to $B7FF + + COMMONRAM: file = "", start = $1300, size = $0600, define = yes; # to $18FF + DISKBUF: file = "", start = $1900, size = $0100, define = yes; + SCRATCH: file = "", start = $1B00, size = $00F0, define = yes; + MAILBOX: file = "", start = $1BF0, size = $0010, define = yes; + SWSTACK: file = "", start = $B800, size = $0800, define = yes; # to $BFFF + + BANK1: file = "", start = $2000, size = $A000, define = yes; # to $BFFF +} + +SEGMENTS { + ZEROPAGE: load = ZP, type = zp, optional = yes; + LOADADDR: load = LOADADDR, type = ro; + EXEHDR: load = HEADER, type = ro; + STARTUP: load = MAIN, type = ro; # first: SYS 7181 enters at $1C0D + CODE: load = MAIN, type = ro; + RODATA: load = MAIN, type = ro, optional = yes; + DATA: load = MAIN, type = rw, optional = yes; + COMMON: load = MAIN, run = COMMONRAM, type = rw, define = yes; + BSS: load = MAIN, type = bss, optional = yes, define = yes; + DISKBUF: load = DISKBUF, type = bss, optional = yes; + SCRATCH: load = SCRATCH, type = bss, optional = yes; + MAILBOX: load = MAILBOX, type = bss, optional = yes; + SWSTACK: load = SWSTACK, type = bss, optional = yes; + TABLES: load = BANK1, type = bss, optional = yes, define = yes; +} + +SYMBOLS { + # Physical bank-0 page that P0 maps to logical zero page in compiler states. + __COMPILER_ZP_PAGE__: type = export, value = $1A; +} diff --git a/newLang/docs/architecture.md b/newLang/docs/architecture.md index af6b8c0..e1a04d0 100644 --- a/newLang/docs/architecture.md +++ b/newLang/docs/architecture.md @@ -7,9 +7,9 @@ version: 0.1-draft # Nova128 Compiler and Runtime Architecture -Status: approved version 1 baseline. Exact addresses are deliberately absent -until the platform-probe epic validates them in VICE and, when available, on -hardware. +Status: approved version 1 baseline. The compiler memory map is frozen by +E01-S05 (section 4.4, `docs/memory-map.md`). Generated-program addresses are +still deliberately absent until the E04 stories validate them. ## 1. System Boundary @@ -131,6 +131,26 @@ bank 0 and bank 1 compiler mappings, CHROUT, a drive-8 status read, and a device-not-present failure all work, with A/X/Y/C/ST preserved and no channels left open. +### 4.4 Frozen compiler memory map (E01-S05) + +The gate above is closed for the compiler. `cfg/compiler.cfg` is authoritative +and `docs/memory-map.md` explains it and gives the evidence. In summary: + +- **Compiler states:** CR `$0E` (bank 0) or `$4E` (bank 1), 8K bottom common + RAM, P0 relocated to bank 0 page `$1A`, IRQs masked. +- **Bank 0:** COMMON `$1300-$18FF` (copied at init), DISKBUF `$1900`, private + zero page `$1A00`, SCRATCH `$1B00-$1BEF`, MAILBOX `$1BF0-$1BFF`, MAIN + `$1C0D-$B7FF` (STARTUP first), SWSTACK `$B800-$BFFF`. +- **Bank 1:** compiler tables `$2000-$BFFF` (40 KB). +- **Zero page:** the compiler owns logical `$02-$FF` via P0; KERNAL calls run + on the system zero page. +- **NMI:** guarded by `mmu_nmi_guard` through `$0318`; RESTORE is ignored + during compilation. +- **Test mailbox:** 16 bytes at `$1BF0`, shared by platform tests, the + compiler, and generated programs. + +The generated-program memory map remains open (E04-S01/E04-S02). + ### 4.3 Display and clock Compiler text uses KERNAL-compatible character output on the display already @@ -337,8 +357,8 @@ Testing is layered: 2. Compiler fixtures for lexer, parser, symbols, types, class layout, interface conformance, and diagnostics. 3. Golden PRG bytes and disassembly for small generated routines. -4. End-to-end x128 runs that write a result signature to a fixed test mailbox - chosen during E01. +4. End-to-end x128 runs that write a result signature to the fixed test + mailbox at `$1BF0` (`src/common/mailbox.inc`, `docs/memory-map.md`). 5. Monitor scripts with MMU, stack, heap, and dispatch break/watch points. 6. Editor round-trip: Vim saves source, C128 compiler consumes it without host conversion, and Vim reopens unchanged source. @@ -363,7 +383,7 @@ features depend on it. ## Open Questions -- Exact bank layouts, zero-page reservations, and common-RAM window +- Generated-program memory map (the compiler map is frozen in section 4.4) - Final interface-result convention - Compiler table capacities and their user-visible resource limits - Whether to cache the complete source in banked RAM when it fits diff --git a/newLang/docs/decisions.md b/newLang/docs/decisions.md index f7971e0..2e9e06b 100644 --- a/newLang/docs/decisions.md +++ b/newLang/docs/decisions.md @@ -38,6 +38,7 @@ documented default. `Proposed` means a later change still needs approval. | D114 | Version 1 has no interface-to-class down-cast operation. | Programs express polymorphism through interfaces without runtime type reflection. | | D115 | Character literals use single quotes and contain exactly one PETSCII character or supported escape. | The `char` type has a direct, unambiguous literal form while `//` remains the comment syntax. | | D116 | Runtime faults report a stable code and the best available map-resolvable program address. | Rich stack traces are deferred; faults remain diagnosable within the runtime budget. | +| D117 | Freeze the compiler memory map in `cfg/compiler.cfg`: 8K bottom common RAM, COMMON at `$1300`, private zero page at bank 0 page `$1A` via P0, bank-1 tables at `$2000-$BFFF`, IRQs masked and NMIs guarded in compiler states, test mailbox at `$1BF0`. | Internal contract backed by E01-S05 x128 evidence (`docs/memory-map.md`); no user-visible behavior except that RESTORE is ignored while compiling. Generated-program maps remain open. | ## Proposed @@ -58,6 +59,8 @@ No proposed product changes are currently pending. ## Open Questions -1. What exact compiler and generated-program memory maps pass the E01 probes? -2. What compiler table capacities fit after the verified memory map is frozen? +1. What generated-program memory map should E04-S01/E04-S02 adopt? (The + compiler map is D117.) +2. What compiler table capacities fit in the 40 KB bank-1 window and 254-byte + zero page now that the compiler map is frozen? diff --git a/newLang/docs/memory-map.md b/newLang/docs/memory-map.md new file mode 100644 index 0000000..eefc8b8 --- /dev/null +++ b/newLang/docs/memory-map.md @@ -0,0 +1,126 @@ +# Nova128 Compiler Memory Map + +Story: E01-S05 +Status: frozen for the compiler (D117); verified in VICE x128 3.10 +Authoritative source: `cfg/compiler.cfg`. This document explains it. + +The generated-program memory map is **not** frozen here. It belongs to E04-S01 +and E04-S02, which must reuse the test mailbox below. + +## Mappings + +| State | CR | RCR (low bits) | P0 | P1 | IRQs | +|---|---|---|---|---|---| +| System | as BASIC left it (`$00`) | as BASIC left it | `$0000` | `$0001` | caller's | +| Compiler bank 0 | `$0E` | `%0110` 8K bottom common | bank 0 page `$1A` | `$0001` | masked | +| Compiler bank 1 | `$4E` | `%0110` 8K bottom common | bank 0 page `$1A` | `$0001` | masked | + +RCR bits 6–7 (VIC bank) are always carried over from the system value. +`src/compiler/mmu.s` is the only module that writes MMU registers. + +## Bank 0 + +| Range | Size | Name | Contents | +|---|---|---|---| +| `$0000-$12FF` | | system | BASIC 7, KERNAL, editor, screen. Off limits except `NMINV` (`$0318`), which the compiler saves and restores | +| `$1300-$18FF` | 1536 | COMMON | Copied from the PRG by `mmu_init`: MMU switches, KERNAL wrapper, NMI guard, and any code that runs while bank 1 is mapped | +| `$1900-$19FF` | 256 | DISKBUF | Disk transfer buffer, visible from both banks | +| `$1A00-$1AFF` | 256 | private ZP | Physical page behind logical zero page in compiler states. Not a linker area: nothing else may live here | +| `$1B00-$1BEF` | 240 | SCRATCH | Shared state needed from both banks | +| `$1BF0-$1BFF` | 16 | MAILBOX | Fixed test mailbox (below) | +| `$1C01-$1C0C` | 12 | header | BASIC 7 stub `10 SYS 7181` | +| `$1C0D-$B7FF` | 39923 | MAIN | `STARTUP` (always first, entry at `$1C0D`), CODE, RODATA, DATA, COMMON load image, BSS | +| `$B800-$BFFF` | 2048 | SWSTACK | Compiler software stack | +| `$C000-$FFFF` | | system ROM | KERNAL/editor in both compiler states | + +`$1300-$1BFF` is the C128's free application area: after boot x128 shows only +the power-on fill pattern there. All of `$0000-$1FFF` is bottom common RAM, so +COMMON, DISKBUF, private ZP, SCRATCH, and MAILBOX are visible from bank 1. + +## Bank 1 + +| Range | Size | Name | Contents | +|---|---|---|---| +| `$0000-$1FFF` | | hidden | Covered by bottom common RAM in compiler states | +| `$2000-$BFFF` | 40960 | BANK1 / `TABLES` | Names, symbols, class/interface/type records, pass workspaces | +| `$C000-$FFFF` | | unmapped | System ROM is visible here instead; BASIC string space at the top of bank 1 is untouched | + +Bank-1 data is reached only through `mmu.s` helpers (`mmu_peek1`/`mmu_poke1` +with the zero-page pointer `mmu_ptr`, and later block-copy routines), or by +code in COMMON. MAIN code never runs while bank 1 is mapped, because MAIN above +`$1FFF` is not visible then. + +BASIC 7 keeps variables in bank 1 from `$0400`. The compiler is started with +`RUN`, which clears variables, so overwriting `$2000-$BFFF` loses no BASIC +state. + +## Zero Page + +In compiler states P0 points at bank 0 page `$1A`, so the compiler owns the +whole logical zero page `$02-$FF` (linker area `ZP`, 254 bytes). `$00/$01` are +the 8502 port and are never allocated. Modules allocate zero page only through +the `ZEROPAGE` segment. Current users: `mmu_ptr` (2 bytes). + +System zero page stays intact. KERNAL calls switch P0 back to `$0000` for the +call. While P0 is relocated, the system zero page is still readable at +`$1A00-$1AFF` (the MMU swaps the two pages), e.g. `ST` at `$1A90`. + +## Interrupts + +- **IRQ:** masked in both compiler states. The ROM IRQ path (`$FF17`) jumps via + `$0314` into BASIC's handler, which uses system zero page and would corrupt + the private page. `kcall` enables IRQs only for the duration of a KERNAL call, + in system state. +- **NMI:** cannot be masked. The ROM entry `$FF05` uses no zero page: it pushes + A/X/Y/CR, sets CR=0, and jumps via `$0318`. `mmu_init` points `$0318` at + `mmu_nmi_guard` (in COMMON), which reads the CIA #2 ICR, counts the event, + and exits through `$FF33` (pull CR/Y/X/A, RTI). RESTORE is therefore ignored + while the compiler runs. `mmu_shutdown` restores `$0318`. + +## Test Mailbox + +Defined in `src/common/mailbox.inc`. Used by platform tests, the compiler, and +generated programs, and read by the harness after the program returns. + +| Address | Field | Meaning | +|---|---|---| +| `$1BF0` | magic 0 | `$4E` (`N`) once initialized | +| `$1BF1` | magic 1 | `$38` (`8`) | +| `$1BF2` | producer | `$01` platform test, `$02` compiler, `$03` generated program | +| `$1BF3` | status | `$01` running, `$A5` pass, `$FA` fail | +| `$1BF4-$1BF5` | code | Little-endian diagnostic, fault, or failed-check code; 0 on pass | +| `$1BF6-$1BF7` | address | Little-endian fault or completion address | +| `$1BF8-$1BFF` | data | Eight producer-defined bytes | + +Protocol: + +1. The producer clears all 16 bytes, writes the magic and producer, then sets + status `$01` before doing any work. +2. It writes code, address, and data, then writes the final status **last**. +3. The harness polls until the magic matches and status is `$A5` or `$FA`. A + status still `$01` at the timeout means the producer hung or crashed. A + missing magic means it never started. + +## Evidence + +`scripts/verify-map-skeleton.ps1` links `src/compiler/mmu.s` with +`tests/platform/map-skeleton.s` using `cfg/compiler.cfg`, then: + +1. Builds five variants that overflow ZP, COMMON, MAIN, SWSTACK, and TABLES by + one region's worth. ld65 rejects each in the expected memory area. +2. Checks fixed placements: STARTUP and `skel_entry` at `$1C0D`, MAILBOX at + `$1BF0`, COMMON within `$1300-$18FF`, TABLES within `$2000-$BFFF`. +3. Runs the skeleton in x128 and reads the mailbox: + `4E 38 01 A5 00 00 … 01 4E 1A C5`. All six checks pass: + +| Check | What it proves | +|---|---| +| ZP | A store to logical ZP lands at `$1Axx`; system ZP is unchanged | +| KCALL | CHROUT via `kcall_inline` from both states; private ZP and CR/P0 intact afterwards | +| BANK1 | First and last TABLES bytes hold bank-1 values; bank 0 at the same addresses unchanged | +| COMMON | DISKBUF/SCRATCH written in bank 0 read back in bank 1; mailbox written from bank 1 | +| NMI | A CIA #2 timer NMI taken in the bank-1 state returns with CR `$4E`, P0 `$1A`, sentinel `$C5`, and the same SP | +| RESTORE | After `mmu_shutdown`: CR/RCR/P0/P1/`$0318` and SP equal their entry values | + +Negative control: forcing P0L to `$00` in `mmu_bank0/1` makes the skeleton +report FAIL with code `$000F` (ZP, KCALL, BANK1, NMI). diff --git a/newLang/planning/claims/E01-S05.md b/newLang/planning/claims/E01-S05.md new file mode 100644 index 0000000..1d99fa9 --- /dev/null +++ b/newLang/planning/claims/E01-S05.md @@ -0,0 +1,92 @@ +--- +story: E01-S05 +owner: e01_memory_map +status: done +started: 2026-11-07 +depends-on: [E01-S02, E01-S03, E01-S04] +--- + +# E01-S05 Claim + +## Intended change + +Freeze the compiler memory map from E01 evidence: common-RAM placement, a +private compiler zero page via P0, bank-1 table space, and a fixed test +mailbox. Provide `cfg/compiler.cfg`, `src/common/c128.inc`, +`src/common/mailbox.inc`, and the production `src/compiler/mmu.s` (bank +switching, KERNAL wrapper, NMI guard), then prove them with a linked skeleton +in x128. + +## File area + +- `cfg/compiler.cfg` +- `src/common/c128.inc`, `src/common/mailbox.inc` +- `src/compiler/mmu.s` +- `tests/platform/map-skeleton.s` +- `scripts/build-map-skeleton.ps1`, `scripts/verify-map-skeleton.ps1` +- `docs/architecture.md` section 4, `docs/decisions.md` +- `planning/claims/E01-S05.md` +- generated files under `build/` + +## Acceptance evidence + +- [x] `docs/architecture.md` lists verified regions and reservations + (section 4.4; full table and evidence in `docs/memory-map.md`). +- [x] `cfg/compiler.cfg` can link a capacity-checking skeleton (normal link + passes; ZP, COMMON, MAIN, SWSTACK, and TABLES overflow builds are each + rejected by ld65 in the expected memory area). +- [x] `src/common/c128.inc` defines observed hardware/KERNAL constants, each + annotated with the document holding its evidence. +- [x] The test mailbox address and protocol are documented and exercised + (`$1BF0`, `src/common/mailbox.inc`; x128 run reads + `4E 38 01 A5 00 00 ...`). + +## Coordination notes + +Absorbs the E01-S03 open NMI risk: the skeleton takes a CIA #2 timer NMI while +bank 1 and the private zero page are active. + +## Work log + +- Read from x128 ROM: `$FFFA` -> `$FF05` (SEI, push A/X/Y/CR, CR=0, + `JMP ($0318)`); `$FF33` pops CR/Y/X/A and RTIs; `$FF17` IRQ path jumps via + `$0314` into BASIC, which uses system zero page. `$1300-$1BFF` holds only + the power-on fill pattern after boot. + +- First x128 run never touched the mailbox: `mmu.s` linked first, so + `mmu_init` sat at the `SYS 7181` target. Added a `STARTUP` segment placed + first and a build check that the entry label is at `$1C0D`. +- Memory-area and segment `define` symbols collided (`__DISKBUF_SIZE__`); + only the memory areas define symbols now. +- A CIA #2 timer NMI taken in the bank-1 state with the private zero page + returns cleanly through ROM `$FF05` -> `mmu_nmi_guard` -> `$FF33`, which + closes the E01-S03 NMI risk. +- Negative control: forcing P0L to `$00` in `mmu_bank0/1` gives mailbox + FAIL code `$000F` (ZP, KCALL, BANK1, NMI). Source restored afterwards. +- Found that the earlier branch switch/merge had rewritten all committed + `newLang/` text files with CRLF (`core.autocrlf=true`), breaking the grammar + validator. Added `newLang/** text=auto eol=lf` to `.gitattributes` and + converted the 79 files back to LF; they match the committed blobs again. + +## Completion + +- Tests run: `scripts/verify-map-skeleton.ps1` (includes five overflow + builds), mutation run, `tests/grammar/validate.ps1`, + `tests/disk/prepare-disk.tests.ps1`, `scripts/verify-minimal.ps1`, + `scripts/verify-display-clock.ps1`, `scripts/verify-mmu-probe.ps1`, + `scripts/verify-kernal-probe.ps1`. +- Results: skeleton PASS with code `$0000`; one NMI under CR `$4E` / P0 `$1A` + with sentinel `$C5` intact; screenshot shows both KCALL lines and + `NOVA128 MAP SKELETON OK`; all earlier regressions pass, and the disk + builder still produces SHA-256 `208EF297...`. +- Files changed: `cfg/compiler.cfg`, `src/common/c128.inc`, + `src/common/mailbox.inc`, `src/compiler/mmu.s`, + `tests/platform/map-skeleton.s`, `scripts/build-map-skeleton.ps1`, + `scripts/verify-map-skeleton.ps1`, `docs/memory-map.md`, + `docs/architecture.md`, `docs/decisions.md` (D117), this claim, and the + repository `.gitattributes`. +- Remaining risks: generated-program map still open (E04); bank-1 block-copy + routines not yet written (only byte peek/poke); RESTORE is ignored during + compilation (documented in D117); IRQs stay masked, so long compiler phases + freeze the jiffy clock and keyboard except during KERNAL calls; VICE-only + evidence. diff --git a/newLang/scripts/build-map-skeleton.ps1 b/newLang/scripts/build-map-skeleton.ps1 new file mode 100644 index 0000000..62a2456 --- /dev/null +++ b/newLang/scripts/build-map-skeleton.ps1 @@ -0,0 +1,105 @@ +# E01-S05: link the memory-map skeleton against cfg/compiler.cfg. +# build-map-skeleton.ps1 normal build, prints segment usage +# build-map-skeleton.ps1 -Overflow ZP build that must fail in ld65 +[CmdletBinding()] +param( + [ValidateSet("", "ZP", "COMMON", "MAIN", "SWSTACK", "TABLES")] + [string]$Overflow = "" +) + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +$build = Join-Path $project "build" +$config = Join-Path $project "cfg\compiler.cfg" +$sources = @( + (Join-Path $project "src\compiler\mmu.s"), + (Join-Path $project "tests\platform\map-skeleton.s") +) +$suffix = if ($Overflow) { "-overflow-" + $Overflow.ToLowerInvariant() } else { "" } +$stem = Join-Path $build ("map-skeleton" + $suffix) +$program = "$stem.prg" +$map = "$stem.map" +$labels = "$stem.lbl" + +$ca65 = (Get-Command ca65 -ErrorAction Stop).Source +$ld65 = (Get-Command ld65 -ErrorAction Stop).Source +New-Item -ItemType Directory -Force -Path $build | Out-Null +foreach ($path in @($program, $map, $labels)) { + if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Force } +} + +$objects = @() +foreach ($source in $sources) { + $object = Join-Path $build (([IO.Path]::GetFileNameWithoutExtension($source)) + $suffix + ".o") + $defines = @() + if ($Overflow) { $defines = @("-D", "OVERFLOW_$Overflow=1") } + & $ca65 --cpu 6502 @defines -o $object $source + if ($LASTEXITCODE -ne 0) { throw "ca65 failed on $source" } + $objects += $object +} + +$linkOutput = & $ld65 -C $config -o $program -m $map -Ln $labels @objects 2>&1 +$linkExit = $LASTEXITCODE +if ($Overflow) { + $text = ($linkOutput | Out-String) + if ($linkExit -eq 0) { throw "OVERFLOW_$Overflow linked; capacity check failed to fire" } + if ($text -notmatch "(?i)overflows memory area") { + throw "OVERFLOW_$Overflow failed for an unexpected reason: $text" + } + $area = [regex]::Match($text, "(?i)overflows memory area\W+(\w+)").Groups[1].Value + Write-Host ("overflow {0}: ld65 rejected it (memory area {1})" -f $Overflow, $area) + return +} +if ($linkExit -ne 0) { throw ("ld65 failed:`n" + ($linkOutput | Out-String)) } + +# Segment usage from the map file: name, start, end, size. +$segments = @{} +$mapText = Get-Content -Raw -LiteralPath $map +$block = [regex]::Match($mapText, "(?s)Segment list:.*?-{5,}\r?\n(.*?)\r?\n\r?\n").Groups[1].Value +foreach ($line in ($block -split "\r?\n")) { + $m = [regex]::Match($line, "^(\w+)\s+([0-9A-F]{6})\s+([0-9A-F]{6})\s+([0-9A-F]{6})") + if ($m.Success) { + $segments[$m.Groups[1].Value] = @{ + Start = [Convert]::ToInt32($m.Groups[2].Value, 16) + End = [Convert]::ToInt32($m.Groups[3].Value, 16) + Size = [Convert]::ToInt32($m.Groups[4].Value, 16) + } + } +} + +# Contract checks that ld65 cannot express: fixed placements. +function Require-Segment([string]$name, [int]$lo, [int]$hi) { + if (-not $segments.ContainsKey($name)) { throw "segment $name missing from map" } + $seg = $segments[$name] + if ($seg.Size -gt 0 -and ($seg.Start -lt $lo -or $seg.End -gt $hi)) { + throw ("segment {0} `${1:X4}-`${2:X4} outside `${3:X4}-`${4:X4}" -f + $name, $seg.Start, $seg.End, $lo, $hi) + } +} +Require-Segment "ZEROPAGE" 0x0002 0x00FF +Require-Segment "COMMON" 0x1300 0x18FF +Require-Segment "MAILBOX" 0x1BF0 0x1BFF +Require-Segment "STARTUP" 0x1C0D 0xB7FF +Require-Segment "CODE" 0x1C0D 0xB7FF +if ($segments["STARTUP"].Start -ne 0x1C0D) { throw "STARTUP does not begin at `$1C0D" } +$labelText = Get-Content -Raw -LiteralPath $labels +if ($labelText -notmatch "(?im)^al\s+001C0D\s+\.skel_entry\s*$") { + throw "skel_entry is not at `$1C0D (the SYS 7181 target)" +} +Require-Segment "SWSTACK" 0xB800 0xBFFF +Require-Segment "TABLES" 0x2000 0xBFFF +if ($segments["MAILBOX"].Start -ne 0x1BF0) { throw "mailbox is not at `$1BF0" } + +$bytes = [IO.File]::ReadAllBytes($program) +if ($bytes[0] -ne 0x01 -or $bytes[1] -ne 0x1C) { throw "load address is not `$1C01" } + +Write-Host ("built {0} ({1} bytes)" -f $program, $bytes.Length) +$limits = [ordered]@{ + ZEROPAGE = 0x00FE; COMMON = 0x0600; SWSTACK = 0x0800; TABLES = 0xA000 +} +foreach ($name in ($segments.Keys | Sort-Object { $segments[$_].Start })) { + $seg = $segments[$name] + $limit = if ($limits.Contains($name)) { " of {0}" -f $limits[$name] } else { "" } + Write-Host (" {0,-9} `${1:X4}-`${2:X4} {3,6} bytes{4}" -f + $name, $seg.Start, $seg.End, $seg.Size, $limit) +} diff --git a/newLang/scripts/verify-map-skeleton.ps1 b/newLang/scripts/verify-map-skeleton.ps1 new file mode 100644 index 0000000..011b06e --- /dev/null +++ b/newLang/scripts/verify-map-skeleton.ps1 @@ -0,0 +1,130 @@ +# E01-S05: prove cfg/compiler.cfg capacity checks, then run the map skeleton +# in VICE x128 and verify the fixed test mailbox at $1BF0. +[CmdletBinding()] +param( + [int]$RemotePort = 6513, + [int]$TimeoutSeconds = 30 +) + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +$build = Join-Path $project "build" +$program = Join-Path $build "map-skeleton.prg" +$screenshot = Join-Path $build "map-skeleton-vicii.png" +$x128 = "C:\Program Files\GTK3VICE-3.10-win64\bin\x128.exe" +$mailbox = 0x1BF0 + +# 1. Every bounded region must reject an overflow at link time. +foreach ($region in "ZP", "COMMON", "MAIN", "SWSTACK", "TABLES") { + & (Join-Path $PSScriptRoot "build-map-skeleton.ps1") -Overflow $region +} + +# 2. The real skeleton. +& (Join-Path $PSScriptRoot "build-map-skeleton.ps1") +if (-not (Test-Path -LiteralPath $x128)) { throw "x128.exe not found at $x128" } +if (Test-Path -LiteralPath $screenshot) { Remove-Item -LiteralPath $screenshot -Force } + +$arguments = @( + "-autostart", ('"' + $program + '"'), + "-autostart-warp", "-warp", "-sounddev", "dummy", + "-exitscreenshotvicii", ('"' + $screenshot + '"'), + "-remotemonitor", "-remotemonitoraddress", ("ip4://127.0.0.1:{0}" -f $RemotePort) +) -join ' ' +$psi = [Diagnostics.ProcessStartInfo]::new() +$psi.FileName = $x128 +$psi.Arguments = $arguments +$psi.UseShellExecute = $true +$psi.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden +$process = [Diagnostics.Process]::Start($psi) +$client = $null + +function Read-Available($stream) { + $buffer = New-Object byte[] 16384 + $text = "" + Start-Sleep -Milliseconds 300 + while ($stream.DataAvailable) { + $count = $stream.Read($buffer, 0, $buffer.Length) + if ($count -le 0) { break } + $text += [Text.Encoding]::ASCII.GetString($buffer, 0, $count) + Start-Sleep -Milliseconds 50 + } + return $text +} + +function Convert-Dump([string]$text) { + $memory = @{} + $rows = [regex]::Matches($text, "(?i)C:([0-9a-f]{4})((?:[ ]{1,2}[0-9a-f]{2}){1,16})") + foreach ($row in $rows) { + $address = [Convert]::ToInt32($row.Groups[1].Value, 16) + foreach ($hex in ($row.Groups[2].Value.Trim() -split '\s+')) { + $memory[$address] = [Convert]::ToInt32($hex, 16) + $address++ + } + } + return $memory +} + +try { + $deadline = [DateTime]::UtcNow.AddSeconds(10) + while ([DateTime]::UtcNow -lt $deadline) { + if ($process.HasExited) { throw "x128 exited before remote verification" } + try { + $client = [Net.Sockets.TcpClient]::new() + $client.Connect("127.0.0.1", $RemotePort) + break + } catch { + if ($client) { $client.Dispose(); $client = $null } + Start-Sleep -Milliseconds 200 + } + } + if (-not $client -or -not $client.Connected) { throw "could not connect to VICE remote monitor" } + + $stream = $client.GetStream() + $writer = [IO.StreamWriter]::new($stream, [Text.Encoding]::ASCII, 1024, $true) + $writer.NewLine = "`n" + $writer.AutoFlush = $true + + # Mailbox protocol: magic "N8", then status leaves RUNNING ($01). + $memory = @{} + $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) + while ($true) { + Start-Sleep -Seconds 1 + $writer.WriteLine(("m `${0:X4} `${1:X4}" -f $mailbox, ($mailbox + 15))) + $chunk = Read-Available $stream + $memory = Convert-Dump $chunk + $magic = ($memory[$mailbox] -eq 0x4E -and $memory[$mailbox + 1] -eq 0x38) + if ($magic -and $memory[$mailbox + 3] -in 0xA5, 0xFA) { break } + if ([DateTime]::UtcNow -ge $deadline) { + throw "mailbox never reached a final status; last response: $chunk" + } + $writer.WriteLine("x") + Read-Available $stream | Out-Null + } + $writer.WriteLine("x") + Start-Sleep -Seconds 2 # let the OK line print + $writer.WriteLine("m 0000 0000") + Read-Available $stream | Out-Null + + $bytes = 0..15 | ForEach-Object { "{0:X2}" -f $memory[$mailbox + $_] } + $code = $memory[$mailbox + 4] + 256 * $memory[$mailbox + 5] + $writer.WriteLine("quit") + $writer.Dispose(); $client.Dispose(); $client = $null + if (-not $process.WaitForExit(10000)) { throw "x128 did not exit after monitor quit" } + + Write-Host ("mailbox `$1BF0: {0}" -f ($bytes -join ' ')) + $checks = @(@(0x01, "ZP"), @(0x02, "KCALL"), @(0x04, "BANK1"), @(0x08, "NMI"), + @(0x10, "COMMON"), @(0x20, "RESTORE")) + $failed = @($checks | Where-Object { $code -band $_[0] } | ForEach-Object { $_[1] }) + if ($memory[$mailbox + 2] -ne 0x01) { throw "producer is not the platform skeleton" } + if ($memory[$mailbox + 3] -ne 0xA5 -or $code -ne 0) { + throw ("skeleton FAILED checks: {0} (code `${1:X4})" -f ($failed -join ', '), $code) + } + $nmis = $memory[$mailbox + 8] + if ($nmis -lt 1) { throw "no NMI was counted" } + Write-Host ("verified mailbox PASS, code `$0000, {0} NMI(s) under CR `${1:X2} / P0 page `${2:X2}, ZP sentinel `${3:X2}" -f + $nmis, $memory[$mailbox + 9], $memory[$mailbox + 10], $memory[$mailbox + 11]) + if (Test-Path -LiteralPath $screenshot) { Write-Host ("screenshot: {0}" -f $screenshot) } +} finally { + if ($client) { $client.Dispose() } + if ($process -and -not $process.HasExited) { $process.Kill(); $process.WaitForExit() } +} diff --git a/newLang/src/common/c128.inc b/newLang/src/common/c128.inc new file mode 100644 index 0000000..5f1b76f --- /dev/null +++ b/newLang/src/common/c128.inc @@ -0,0 +1,70 @@ +; c128.inc - C128 hardware and KERNAL constants used by Nova128. +; +; Every value here was exercised in VICE x128 3.10 by an E01 probe; the +; evidence column names the document. Add a constant only with evidence. + +.ifndef C128_INC +C128_INC = 1 + +; ------------------------------------------------------------------ MMU +MMU_CR = $FF00 ; configuration register, visible in all maps +MMU_RCR = $D506 ; RAM/common config (I/O must be mapped) +MMU_P0L = $D507 ; page-zero pointer, low commits (write H first) +MMU_P0H = $D508 +MMU_P1L = $D509 ; page-one pointer, low commits (write H first) +MMU_P1H = $D50A + +CR_BANK15 = $00 ; BASIC+KERNAL+I/O, RAM bank 0 (BASIC default) +CR_RAM0_KERNAL = $0E ; RAM bank 0 $0000-$BFFF, system ROM, I/O +CR_RAM1_KERNAL = $4E ; same with RAM bank 1 +CR_BANK1_BIT = $40 + +RCR_VIC_MASK = $C0 ; RCR bits 6-7: VIC bank, always preserved +RCR_COMMON_8K_BOTTOM = $06 ; 8K common $0000-$1FFF (bank 0) + ; docs/mmu-probe.md, docs/kernal-wrapper.md + +; ------------------------------------------------- interrupt entry (ROM) +VEC_NMI_HW = $FFFA ; -> $FF05 in system ROM +ROM_NMI_ENTRY = $FF05 ; SEI; push A,X,Y,CR; CR=0; JMP (NMINV) +ROM_IRQ_ENTRY = $FF17 ; push A,X,Y,CR; CR=0; JMP (CINV/CBINV) +ROM_INT_EXIT = $FF33 ; pull CR,Y,X,A; RTI +CINV = $0314 ; IRQ vector (BASIC's handler uses system ZP) +NMINV = $0318 ; NMI RAM vector + ; docs/memory-map.md (read from x128 ROM) + +; -------------------------------------------------------------- CIA #2 +CIA2_TALO = $DD04 +CIA2_TAHI = $DD05 +CIA2_ICR = $DD0D ; NMI source; read acknowledges +CIA2_CRA = $DD0E +CIA_ICR_SET = $80 +CIA_ICR_TA = $01 +CIA_CRA_START = $01 +CIA_CRA_ONESHOT = $08 +CIA_CRA_LOAD = $10 + +; ------------------------------------------------------ video and clock +MODE = $D7 ; bit 7: $80 = 80-column editor active +VIC_CLKRATE = $D030 ; bit 0: 2 MHz (VIC display off) + ; docs/platform-display-clock.md + +; --------------------------------------------------- KERNAL zero page +STATUS = $90 ; ST, as returned by READST +LDTND = $98 ; number of open logical files + +; ---------------------------------------------------------- KERNAL calls +SETBNK = $FF68 ; A = data bank, X = filename bank +READST = $FFB7 +SETLFS = $FFBA +SETNAM = $FFBD +OPEN = $FFC0 ; no-name OPEN sends nothing on the bus +CLOSE = $FFC3 +CHKIN = $FFC6 ; absent device: C=1, A=5, ST=$80 +CLRCHN = $FFCC +CHRIN = $FFCF +CHROUT = $FFD2 + ; docs/kernal-wrapper.md + +KERNAL_ERR_DEVICE_NOT_PRESENT = 5 + +.endif diff --git a/newLang/src/common/mailbox.inc b/newLang/src/common/mailbox.inc new file mode 100644 index 0000000..b848c47 --- /dev/null +++ b/newLang/src/common/mailbox.inc @@ -0,0 +1,31 @@ +; mailbox.inc - fixed test mailbox shared by the compiler, generated +; programs, and the x128 harness. Protocol: docs/memory-map.md. +; +; The mailbox is in bank 0 inside bottom common RAM, so it is readable and +; writable from every Nova128 mapping and from the VICE monitor after exit. + +.ifndef MAILBOX_INC +MAILBOX_INC = 1 + +MB_BASE = $1BF0 +MB_MAGIC0 = MB_BASE + 0 ; 'N' ($4E) once the mailbox is initialized +MB_MAGIC1 = MB_BASE + 1 ; '8' ($38) +MB_PRODUCER = MB_BASE + 2 ; who wrote it (MB_PROD_*) +MB_STATUS = MB_BASE + 3 ; MB_ST_* +MB_CODE = MB_BASE + 4 ; 2 bytes LE: diagnostic/fault/check code +MB_ADDR = MB_BASE + 6 ; 2 bytes LE: fault or check address +MB_DATA = MB_BASE + 8 ; 8 bytes producer-defined payload +MB_SIZE = 16 + +MB_MAGIC0_VALUE = $4E +MB_MAGIC1_VALUE = $38 + +MB_PROD_PLATFORM = $01 ; platform probe/skeleton +MB_PROD_COMPILER = $02 ; N128C +MB_PROD_PROGRAM = $03 ; generated Nova128 program + +MB_ST_RUNNING = $01 ; written first; still set => hung or crashed +MB_ST_PASS = $A5 +MB_ST_FAIL = $FA + +.endif diff --git a/newLang/src/compiler/mmu.s b/newLang/src/compiler/mmu.s new file mode 100644 index 0000000..db845f7 --- /dev/null +++ b/newLang/src/compiler/mmu.s @@ -0,0 +1,286 @@ +; mmu.s - the only Nova128 compiler module that changes the C128 MMU. +; +; Compiler mappings (docs/memory-map.md): +; bank-0 state: CR=$0E, RCR=8K bottom common, P0 -> private page +; bank-1 state: CR=$4E, otherwise identical +; system state: whatever BASIC had at entry (saved by mmu_init) +; +; Rules every caller relies on: +; - IRQs stay masked in both compiler states. The ROM IRQ path jumps into +; BASIC's handler, which uses system zero page; with P0 relocated it would +; corrupt the compiler's private zero page. +; - KERNAL routines run only through kcall/kcall_inline, which switch to the +; system state (including system zero page) and back. +; - NMIs (RESTORE, CIA #2) are acknowledged and ignored by mmu_nmi_guard until +; mmu_shutdown restores the system NMI vector. +; - Everything in the COMMON segment runs from bottom common RAM, so it can be +; called from either compiler state. mmu_init (CODE) runs in system state. +; +; P0H is always 0: E01-S02 showed that under bottom common the P0 bank bit is +; forced to bank 0 anyway, and the private page lives in bank 0. + +.setcpu "6502" +.include "../common/c128.inc" + +.import __COMMON_LOAD__, __COMMON_RUN__, __COMMON_SIZE__ +.import __COMPILER_ZP_PAGE__ + +.export mmu_init, mmu_bank0, mmu_bank1, mmu_system, mmu_shutdown +.export kcall, kcall_inline, kc_target +.export mmu_nmi_guard, mmu_nmi_count +.export mmu_peek1, mmu_poke1 +.exportzp mmu_ptr +.export mmu_sys_cr, mmu_sys_rcr, mmu_sys_p0l, mmu_sys_p0h +.export mmu_sys_p1l, mmu_sys_p1h, mmu_sys_nmi, mmu_comp_rcr + +.segment "ZEROPAGE" +mmu_ptr: .res 2 ; bank-1 access pointer (private page) + +; ------------------------------------------------------------------ CODE +.segment "CODE" + +; Call in system state with IRQs masked, before any other mmu routine. +; Copies the COMMON segment to its run address, records the system state, +; and installs the NMI guard. Clobbers A/X/Y. Leaves system state mapped. +mmu_init: + ldx #<__COMMON_SIZE__ ; whole pages first, then the remainder + ldy #>__COMMON_SIZE__ + lda #<__COMMON_LOAD__ + sta @src+1 + lda #>__COMMON_LOAD__ + sta @src+2 + lda #<__COMMON_RUN__ + sta @dst+1 + lda #>__COMMON_RUN__ + sta @dst+2 + cpy #$00 + beq @tail +@page: jsr @copy256 + inc @src+2 + inc @dst+2 + dey + bne @page +@tail: cpx #$00 + beq @copied + ldy #$00 +@last: jsr @one + iny + dex + bne @last + beq @copied +@copy256: + sty @savey + ldy #$00 +@full: jsr @one + iny + bne @full + ldy @savey + rts +@one: +@src: lda $FFFF,y ; patched +@dst: sta $FFFF,y ; patched + rts +@savey: .byte $00 +@copied: + lda MMU_CR + sta mmu_sys_cr + lda MMU_RCR + sta mmu_sys_rcr + and #RCR_VIC_MASK + ora #RCR_COMMON_8K_BOTTOM + sta mmu_comp_rcr + lda MMU_P0L + sta mmu_sys_p0l + lda MMU_P0H + sta mmu_sys_p0h + lda MMU_P1L + sta mmu_sys_p1l + lda MMU_P1H + sta mmu_sys_p1h + lda NMINV + sta mmu_sys_nmi + lda NMINV+1 + sta mmu_sys_nmi+1 + lda #$00 + sta mmu_nmi_count + lda #mmu_nmi_guard + sta NMINV+1 + rts + +; ---------------------------------------------------------------- COMMON +.segment "COMMON" + +; Enter a compiler state from any state. Clobbers A only; flags undefined. +; Order: common RAM first, so the code stays visible when CR selects bank 1. +mmu_bank0: + lda #CR_RAM0_KERNAL + .byte $2C ; BIT abs: skip the next LDA +mmu_bank1: + lda #CR_RAM1_KERNAL + pha + lda mmu_comp_rcr + sta MMU_RCR + lda #$00 + sta MMU_P0H ; high first, low commits + lda #<__COMPILER_ZP_PAGE__ + sta MMU_P0L + pla + sta MMU_CR + rts + +; Return to the saved system state, NMI guard still installed. Clobbers A. +; Order: CR first (system CR selects bank 0, where this code also lives). +mmu_system: + lda mmu_sys_cr + sta MMU_CR + lda mmu_sys_rcr + sta MMU_RCR + lda mmu_sys_p0h + sta MMU_P0H + lda mmu_sys_p0l + sta MMU_P0L + lda mmu_sys_p1h + sta MMU_P1H + lda mmu_sys_p1l + sta MMU_P1L + rts + +; Final exit: system state plus the system NMI vector. Clobbers A. +mmu_shutdown: + jsr mmu_system + lda mmu_sys_nmi + sta NMINV + lda mmu_sys_nmi+1 + sta NMINV+1 + rts + +; KERNAL wrapper (contract proven by E01-S03, extended with P0): +; in: kc_target = KERNAL entry; A/X/Y = routine arguments +; out: A/X/Y and N/V/Z/C as the routine returned them, caller's I flag, +; CR/RCR/P0 exactly as at the call +; Runs the routine in system state with IRQs enabled. +kcall: + php + sei + sta kc_a + pla + sta kc_caller_p + lda MMU_CR + sta kc_saved_cr + lda MMU_RCR + sta kc_saved_rcr + lda MMU_P0L + sta kc_saved_p0l + lda mmu_sys_cr ; bank 0 first: kcall lives in bank 0 + sta MMU_CR + lda mmu_sys_rcr + sta MMU_RCR + lda mmu_sys_p0h + sta MMU_P0H + lda mmu_sys_p0l ; system zero page for the KERNAL + sta MMU_P0L + cli + lda kc_a + jsr @jump + php + sei + sta kc_a + pla + sta kc_result_p + lda #$00 + sta MMU_P0H + lda kc_saved_p0l + sta MMU_P0L + lda kc_saved_rcr ; common before a possible bank-1 CR + sta MMU_RCR + lda kc_saved_cr + sta MMU_CR + lda kc_caller_p ; result flags with the caller's I flag + and #$04 + sta kc_i_bit + lda kc_result_p + and #$FB + ora kc_i_bit + pha + lda kc_a + plp + rts +@jump: jmp (kc_target) + +; Bank-1 byte access for code running in the bank-0 state. The pointer is in +; the private zero page, which is visible in both states. +; mmu_peek1: A = bank1[(mmu_ptr),Y]; N/Z reflect A +; mmu_poke1: bank1[(mmu_ptr),Y] = A +; Both preserve X and Y and return in the bank-0 state. +mmu_peek1: + jsr mmu_bank1 + lda (mmu_ptr),y + pha + jsr mmu_bank0 + pla + rts + +mmu_poke1: + pha + jsr mmu_bank1 + pla + sta (mmu_ptr),y + jmp mmu_bank0 + +; Compact form: JSR kcall_inline / .word target. Preserves A/X/Y in, then +; behaves exactly like kcall. +kcall_inline: + sta kc_inl_a + sty kc_inl_y + pla + sta @src+1 ; JSR pushed the address of its last byte + pla + sta @src+2 + ldy #1 +@src: lda $FFFF,y ; patched + sta kc_target-1,y + iny + cpy #3 + bne @src + lda @src+1 ; return past the inline word + clc + adc #2 + tay + lda @src+2 + adc #0 + pha + tya + pha + ldy kc_inl_y + lda kc_inl_a + jmp kcall + +; Reached from ROM $FF05 with CR=0 (I/O mapped) and A/X/Y/CR on the stack. +; Reading the CIA #2 ICR acknowledges a timer NMI; RESTORE needs no ack. +mmu_nmi_guard: + bit CIA2_ICR + inc mmu_nmi_count + jmp ROM_INT_EXIT + +kc_target: .word $0000 +.assert $FF, error, "kc_target must not straddle a page (JMP bug)" +kc_a: .byte $00 +kc_caller_p: .byte $00 +kc_result_p: .byte $00 +kc_i_bit: .byte $00 +kc_saved_cr: .byte $00 +kc_saved_rcr: .byte $00 +kc_saved_p0l: .byte $00 +kc_inl_a: .byte $00 +kc_inl_y: .byte $00 +mmu_nmi_count: .byte $00 +mmu_comp_rcr: .byte $00 +mmu_sys_cr: .byte $00 +mmu_sys_rcr: .byte $00 +mmu_sys_p0l: .byte $00 +mmu_sys_p0h: .byte $00 +mmu_sys_p1l: .byte $00 +mmu_sys_p1h: .byte $00 +mmu_sys_nmi: .word $0000 diff --git a/newLang/tests/platform/map-skeleton.s b/newLang/tests/platform/map-skeleton.s new file mode 100644 index 0000000..4782aac --- /dev/null +++ b/newLang/tests/platform/map-skeleton.s @@ -0,0 +1,429 @@ +; E01-S05: capacity-checking skeleton for cfg/compiler.cfg. +; +; Links the production src/compiler/mmu.s with the frozen memory map and +; proves, in x128, every region and rule the compiler will depend on. Results +; go to the fixed test mailbox (src/common/mailbox.inc): +; MB_STATUS $A5 pass / $FA fail +; MB_CODE 16-bit mask of FAILED checks (CHECK_*), 0 on pass +; MB_DATA+0 NMIs counted by mmu_nmi_guard +; MB_DATA+1 CR seen right after the NMI (expect $4E) +; MB_DATA+2 P0L seen right after the NMI (expect the private page) +; MB_DATA+3 private zero-page sentinel after the NMI (expect $C5) +; +; Assemble with -D OVERFLOW_=1 to prove ld65 rejects an overflow of +; ZP, COMMON, MAIN, SWSTACK, or TABLES. + +.setcpu "6502" +.include "../../src/common/c128.inc" +.include "../../src/common/mailbox.inc" + +.import mmu_init, mmu_bank0, mmu_bank1, mmu_system, mmu_shutdown +.import mmu_peek1, mmu_poke1, kcall_inline, mmu_nmi_count +.importzp mmu_ptr +.import __COMPILER_ZP_PAGE__ + +CHECK_ZP = $0001 ; private zero page isolated from system ZP +CHECK_KCALL = $0002 ; KERNAL calls from both states keep ZP/MMU +CHECK_BANK1 = $0004 ; first/last table bytes are bank-1 only +CHECK_NMI = $0008 ; NMI under bank 1 + private ZP is harmless +CHECK_COMMON = $0010 ; COMMON/DISKBUF/SCRATCH/MAILBOX shared +CHECK_RESTORE = $0020 ; exact system state after mmu_shutdown +CHECK_ALL = $003F + +ZP_SENTINEL = $C5 + +.segment "LOADADDR" + .word $1C01 + +.segment "EXEHDR" + .word basic_end + .word 10 + .byte $9E + .byte "7181" ; SYS $1C0D + .byte $00 +basic_end: + .word $0000 + +.segment "ZEROPAGE" +zp_probe: .res 1 +.ifdef OVERFLOW_ZP + .res $FE +.endif + +.segment "MAILBOX" +mailbox: .res MB_SIZE +.assert mailbox = MB_BASE, error, "MAILBOX segment must sit at MB_BASE" + +.segment "DISKBUF" +diskbuf: .res $100 + +.segment "SCRATCH" +scratch: .res 1 + +.segment "SWSTACK" +swstack_first: .res 1 + .res $0800 - 2 +swstack_last: .res 1 +.ifdef OVERFLOW_SWSTACK + .res 1 +.endif + +.segment "TABLES" +tables_first: .res 1 + .res $A000 - 2 +tables_last: .res 1 +.ifdef OVERFLOW_TABLES + .res 1 +.endif + +; --------------------------------------------------------------- STARTUP +.segment "STARTUP" +.export skel_entry +skel_entry: + php + sei + tsx + stx entry_sp + + ldx #MB_SIZE - 1 ; mailbox: initialized and running + lda #$00 +@clr: sta MB_BASE,x + dex + bpl @clr + lda #MB_MAGIC0_VALUE + sta MB_MAGIC0 + lda #MB_MAGIC1_VALUE + sta MB_MAGIC1 + lda #MB_PROD_PLATFORM + sta MB_PRODUCER + lda #MB_ST_RUNNING + sta MB_STATUS + lda #CHECK_ALL + sta fail_mask+1 + + lda MMU_CR ; system state for CHECK_RESTORE + sta entry_state+0 + ldx #4 +@save: lda MMU_RCR,x + sta entry_state+1,x + dex + bpl @save + lda NMINV + sta entry_state+6 + lda NMINV+1 + sta entry_state+7 + lda zp_probe ; system ZP byte at the probe's offset + sta sys_zp_before + + jsr mmu_init + jsr mmu_bank0 + + ; ------------------------------------------- private zero page + lda #ZP_SENTINEL + sta zp_probe ; logical ZP -> physical $1Axx + jsr mmu_system + lda zp_probe ; system ZP again + cmp sys_zp_before + bne @zp_done + lda __COMPILER_ZP_PAGE__ * 256 + zp_probe + cmp #ZP_SENTINEL + bne @zp_done + lda #tables_first + sta mmu_ptr+1 + ldy #$00 + lda #$5A + jsr mmu_poke1 + lda #tables_last + sta mmu_ptr+1 + lda #$A5 + jsr mmu_poke1 + jsr mmu_peek1 + cmp #$A5 + bne @b1_done + lda #tables_first + sta mmu_ptr+1 + jsr mmu_peek1 + cmp #$5A + bne @b1_done + lda tables_first + cmp b0_first + bne @b1_done + lda tables_last + cmp b0_last + bne @b1_done + jsr state_is_bank0 + bne @b1_done + lda #skel_done + sta MB_ADDR+1 + ldy #ok_msg - messages + lda fail_mask + ora fail_mask+1 + bne @fail + lda #MB_ST_PASS + bne @report +@fail: ldy #fail_msg - messages + lda #MB_ST_FAIL +@report: + sta MB_STATUS +skel_done: + plp +@print: lda messages,y + beq @end + sty print_y + jsr CHROUT + ldy print_y + iny + bne @print +@end: rts + +.segment "CODE" + +; Clear a passed check (A = low-byte mask; all checks fit in the low byte). +pass: + eor #$FF + and fail_mask + sta fail_mask + rts + +; Z=1 when the MMU shows the exact bank-0 compiler state. +state_is_bank0: + lda MMU_CR + cmp #CR_RAM0_KERNAL + bne @no + lda MMU_P0L + cmp #<__COMPILER_ZP_PAGE__ +@no: rts + +.ifdef OVERFLOW_MAIN + .res $9BF3 +.endif + +.segment "RODATA" +bank0_msg: + .byte "KCALL FROM BANK 0", $0D, $00 +messages: +ok_msg: + .byte "NOVA128 MAP SKELETON OK", $0D, $00 +fail_msg: + .byte "NOVA128 MAP SKELETON FAIL", $0D, $00 + +.segment "BSS" +entry_sp: .res 1 +entry_state: .res 8 ; CR, RCR, P0L, P0H, P1L, P1H, NMINV +sys_zp_before: .res 1 +fail_mask: .res 2 +b0_first: .res 1 +b0_last: .res 1 +print_y: .res 1 + +; ---------------------------------------------------------------- COMMON +; These routines run with bank 1 mapped, so they must be in common RAM. +.segment "COMMON" + +common_say_bank1: + lda #$00 + sta kc1_ok + jsr mmu_bank1 + ldy #$00 +@say: lda bank1_msg,y + beq @said + sty say_y + jsr kcall_inline + .word CHROUT + ldy say_y + iny + bne @say +@said: lda MMU_CR ; still bank 1 after the KERNAL calls? + cmp #CR_RAM1_KERNAL + bne @out + lda MMU_P0L + cmp #<__COMPILER_ZP_PAGE__ + bne @out + inc kc1_ok +@out: jmp mmu_bank0 + +common_shared_test: + lda #$00 + sta shared_ok + jsr mmu_bank1 + lda diskbuf + cmp #$3C + bne @out + lda scratch + cmp #$C3 + bne @out + lda #$7E + sta MB_DATA+7 + inc shared_ok +@out: jmp mmu_bank0 + +common_nmi_test: + tsx + stx entry_sp_inner + jsr mmu_bank1 + lda #$7F ; disable all CIA #2 sources, clear flags + sta CIA2_ICR + bit CIA2_ICR + lda #$40 + sta CIA2_TALO + lda #$00 + sta CIA2_TAHI + lda mmu_nmi_count + sta nmi_before + lda #CIA_ICR_SET | CIA_ICR_TA + sta CIA2_ICR + lda #CIA_CRA_START | CIA_CRA_ONESHOT | CIA_CRA_LOAD + sta CIA2_CRA + ldx #$00 ; wait up to ~65K iterations + ldy #$00 +@wait: lda mmu_nmi_count + cmp nmi_before + bne @hit + dex + bne @wait + dey + bne @wait +@hit: lda MMU_CR ; state right after the NMI returned + sta MB_DATA+1 + lda MMU_P0L + sta MB_DATA+2 + lda zp_probe + sta MB_DATA+3 + tsx ; same depth as at routine entry? + stx nmi_sp + lda #$7F + sta CIA2_ICR + lda #$00 + sta CIA2_CRA + bit CIA2_ICR + lda mmu_nmi_count + sec + sbc nmi_before + sta MB_DATA+0 + jmp mmu_bank0 + +bank1_msg: .byte "KCALL FROM BANK 1", $0D, $00 +kc1_ok: .byte $00 +say_y: .byte $00 +shared_ok: .byte $00 +nmi_before: .byte $00 +nmi_sp: .byte $00 +entry_sp_inner: .byte $00 + +.ifdef OVERFLOW_COMMON + .res $0600 +.endif