Pārlūkot izejas kodu

newLang: freeze the compiler memory map (E01-S05)

Add cfg/compiler.cfg, the production mmu.s, c128.inc, and the test mailbox,
proven by a capacity-checking skeleton in x128:

- 8K bottom common RAM, shared code at $1300, private zero page at bank 0
  page $1A via P0, bank-1 tables at $2000-$BFFF, test mailbox at $1BF0
- ld65 rejects overflows of the ZP, COMMON, MAIN, SWSTACK, and TABLES areas
- a CIA #2 timer NMI taken under bank 1 with the private zero page returns
  cleanly through the NMI guard (closes the E01-S03 NMI risk)
- decision D117 records the map; RESTORE is ignored while compiling

Also pin newLang/ to LF in .gitattributes. A branch switch under
core.autocrlf had rewritten the byte-exact fixtures with CRLF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
Daniel Covington pirms 6 dienas
vecāks
revīzija
7fb5c60fb7
12 mainītis faili ar 1358 papildinājumiem un 8 dzēšanām
  1. +4
    -0
      .gitattributes
  2. +54
    -0
      newLang/cfg/compiler.cfg
  3. +26
    -6
      newLang/docs/architecture.md
  4. +5
    -2
      newLang/docs/decisions.md
  5. +126
    -0
      newLang/docs/memory-map.md
  6. +92
    -0
      newLang/planning/claims/E01-S05.md
  7. +105
    -0
      newLang/scripts/build-map-skeleton.ps1
  8. +130
    -0
      newLang/scripts/verify-map-skeleton.ps1
  9. +70
    -0
      newLang/src/common/c128.inc
  10. +31
    -0
      newLang/src/common/mailbox.inc
  11. +286
    -0
      newLang/src/compiler/mmu.s
  12. +429
    -0
      newLang/tests/platform/map-skeleton.s

+ 4
- 0
.gitattributes Parādīt failu

@@ -7,3 +7,7 @@
*.d64 binary *.d64 binary
*.d71 binary *.d71 binary
*.d81 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

+ 54
- 0
newLang/cfg/compiler.cfg Parādīt failu

@@ -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;
}

+ 26
- 6
newLang/docs/architecture.md Parādīt failu

@@ -7,9 +7,9 @@ version: 0.1-draft


# Nova128 Compiler and Runtime Architecture # 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 ## 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 device-not-present failure all work, with A/X/Y/C/ST preserved and no channels
left open. 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 ### 4.3 Display and clock


Compiler text uses KERNAL-compatible character output on the display already 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 2. Compiler fixtures for lexer, parser, symbols, types, class layout, interface
conformance, and diagnostics. conformance, and diagnostics.
3. Golden PRG bytes and disassembly for small generated routines. 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. 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 6. Editor round-trip: Vim saves source, C128 compiler consumes it without host
conversion, and Vim reopens unchanged source. conversion, and Vim reopens unchanged source.
@@ -363,7 +383,7 @@ features depend on it.


## Open Questions ## 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 - Final interface-result convention
- Compiler table capacities and their user-visible resource limits - Compiler table capacities and their user-visible resource limits
- Whether to cache the complete source in banked RAM when it fits - Whether to cache the complete source in banked RAM when it fits


+ 5
- 2
newLang/docs/decisions.md Parādīt failu

@@ -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. | | 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. | | 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. | | 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 ## Proposed


@@ -58,6 +59,8 @@ No proposed product changes are currently pending.


## Open Questions ## 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?



+ 126
- 0
newLang/docs/memory-map.md Parādīt failu

@@ -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).

+ 92
- 0
newLang/planning/claims/E01-S05.md Parādīt failu

@@ -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.

+ 105
- 0
newLang/scripts/build-map-skeleton.ps1 Parādīt failu

@@ -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)
}

+ 130
- 0
newLang/scripts/verify-map-skeleton.ps1 Parādīt failu

@@ -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() }
}

+ 70
- 0
newLang/src/common/c128.inc Parādīt failu

@@ -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

+ 31
- 0
newLang/src/common/mailbox.inc Parādīt failu

@@ -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

+ 286
- 0
newLang/src/compiler/mmu.s Parādīt failu

@@ -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
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 <kc_target <> $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

+ 429
- 0
newLang/tests/platform/map-skeleton.s Parādīt failu

@@ -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_<REGION>=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 ; every check starts failed
sta fail_mask
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 #<CHECK_ZP
jsr pass
@zp_done:
jsr mmu_bank0

; ---------------------------------- KERNAL calls from both states
ldy #$00
@say0: lda bank0_msg,y
beq @said0
sty print_y
jsr kcall_inline
.word CHROUT
ldy print_y
iny
bne @say0
@said0: jsr common_say_bank1 ; same from the bank-1 state
lda zp_probe
cmp #ZP_SENTINEL
bne @kc_done
jsr state_is_bank0
bne @kc_done
lda kc1_ok
cmp #$01
bne @kc_done
lda #<CHECK_KCALL
jsr pass
@kc_done:

; ------------------------------------------- bank-1 table window
lda tables_first ; bank-0 bytes at the same addresses
sta b0_first
lda tables_last
sta b0_last
lda #<tables_first
sta mmu_ptr
lda #>tables_first
sta mmu_ptr+1
ldy #$00
lda #$5A
jsr mmu_poke1
lda #<tables_last
sta mmu_ptr
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
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 #<CHECK_BANK1
jsr pass
@b1_done:

; ----------------------------------------- shared common regions
lda #$3C
sta diskbuf
lda #$C3
sta scratch
jsr common_shared_test
lda shared_ok
cmp #$01
bne @sh_done
lda MB_DATA+7 ; written from the bank-1 state
cmp #$7E
bne @sh_done
lda #<CHECK_COMMON
jsr pass
@sh_done:
lda #$00
sta MB_DATA+7

; -------------------------------------- NMI under bank 1 + ZP
jsr common_nmi_test
lda MB_DATA+0
beq @nmi_done ; the NMI never arrived
lda MB_DATA+1
cmp #CR_RAM1_KERNAL
bne @nmi_done
lda MB_DATA+2
cmp #<__COMPILER_ZP_PAGE__
bne @nmi_done
lda MB_DATA+3
cmp #ZP_SENTINEL
bne @nmi_done
lda nmi_sp
cmp entry_sp_inner
bne @nmi_done
lda #<CHECK_NMI
jsr pass
@nmi_done:

; ------------------------------------------------- full restore
jsr mmu_shutdown
tsx
cpx entry_sp
bne @rs_done
lda MMU_CR
cmp entry_state+0
bne @rs_done
ldx #4
@rs: lda MMU_RCR,x
cmp entry_state+1,x
bne @rs_done
dex
bpl @rs
lda NMINV
cmp entry_state+6
bne @rs_done
lda NMINV+1
cmp entry_state+7
bne @rs_done
lda #<CHECK_RESTORE
jsr pass
@rs_done:

; ------------------------------------------------ report
lda fail_mask
sta MB_CODE
lda fail_mask+1
sta MB_CODE+1
lda #<skel_done
sta MB_ADDR
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

Notiek ielāde…
Atcelt
Saglabāt

Powered by TurnKey Linux.