瀏覽代碼

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 6 天之前
父節點
當前提交
7fb5c60fb7
共有 12 個檔案被更改,包括 1358 行新增 和 8 行删除
  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 查看文件

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

+ 54
- 0
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;
}

+ 26
- 6
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


+ 5
- 2
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?


+ 126
- 0
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).

+ 92
- 0
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.

+ 105
- 0
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)
}

+ 130
- 0
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() }
}

+ 70
- 0
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

+ 31
- 0
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

+ 286
- 0
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
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 查看文件

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

Loading…
取消
儲存

Powered by TurnKey Linux.