src_load reads a SEQ file from device 8 once into a bank-1 cache (D118). It enforces the 80-byte line, 599-line, and 18,999-byte limits as bytes arrive and reports stable codes with exact positions. src_rewind and src_next_line replay the cache one logical line at a time for every pass. diskio.s wraps the KERNAL file calls; channels close on every path. Verified in x128 with a 1581: eight fixtures, including the exact limits, each limit error, a missing file (DOS 62), and a missing final $0D, all match host-computed line counts, Fletcher-16 checksums, and positions. Adds a shared PowerShell test library for x128 scripts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
| @@ -4,8 +4,9 @@ | |||||
| # | # | ||||
| # Bank 0 (CR=$0E) Bank 1 (CR=$4E) | # Bank 0 (CR=$0E) Bank 1 (CR=$4E) | ||||
| # $0000-$12FF system (BASIC/KERNAL) $0000-$1FFF hidden by common RAM | # $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) | |||||
| # $1300-$18FF COMMON (copied at init) $2000-$6AFF SRCCACHE source cache | |||||
| # $1900-$19FF DISKBUF $6B00-$BFFF BANK1 compiler tables | |||||
| # $C000-$FFFF system ROM (unmapped RAM) | |||||
| # $1A00-$1AFF private zero page (P0) | # $1A00-$1AFF private zero page (P0) | ||||
| # $1B00-$1BEF SCRATCH (common) | # $1B00-$1BEF SCRATCH (common) | ||||
| # $1BF0-$1BFF MAILBOX | # $1BF0-$1BFF MAILBOX | ||||
| @@ -28,7 +29,9 @@ MEMORY { | |||||
| MAILBOX: file = "", start = $1BF0, size = $0010, define = yes; | MAILBOX: file = "", start = $1BF0, size = $0010, define = yes; | ||||
| SWSTACK: file = "", start = $B800, size = $0800, define = yes; # to $BFFF | SWSTACK: file = "", start = $B800, size = $0800, define = yes; # to $BFFF | ||||
| BANK1: file = "", start = $2000, size = $A000, define = yes; # to $BFFF | |||||
| # Whole-file source cache (D118): >= 18,999 bytes, page aligned. | |||||
| SRCCACHE: file = "", start = $2000, size = $4B00, define = yes; # to $6AFF | |||||
| BANK1: file = "", start = $6B00, size = $5500, define = yes; # to $BFFF | |||||
| } | } | ||||
| SEGMENTS { | SEGMENTS { | ||||
| @@ -45,6 +48,7 @@ SEGMENTS { | |||||
| SCRATCH: load = SCRATCH, type = bss, optional = yes; | SCRATCH: load = SCRATCH, type = bss, optional = yes; | ||||
| MAILBOX: load = MAILBOX, type = bss, optional = yes; | MAILBOX: load = MAILBOX, type = bss, optional = yes; | ||||
| SWSTACK: load = SWSTACK, type = bss, optional = yes; | SWSTACK: load = SWSTACK, type = bss, optional = yes; | ||||
| SOURCE: load = SRCCACHE, type = bss, optional = yes, define = yes; | |||||
| TABLES: load = BANK1, type = bss, optional = yes, define = yes; | TABLES: load = BANK1, type = bss, optional = yes, define = yes; | ||||
| } | } | ||||
| @@ -141,7 +141,8 @@ and `docs/memory-map.md` explains it and gives the evidence. In summary: | |||||
| - **Bank 0:** COMMON `$1300-$18FF` (copied at init), DISKBUF `$1900`, private | - **Bank 0:** COMMON `$1300-$18FF` (copied at init), DISKBUF `$1900`, private | ||||
| zero page `$1A00`, SCRATCH `$1B00-$1BEF`, MAILBOX `$1BF0-$1BFF`, MAIN | zero page `$1A00`, SCRATCH `$1B00-$1BEF`, MAILBOX `$1BF0-$1BFF`, MAIN | ||||
| `$1C0D-$B7FF` (STARTUP first), SWSTACK `$B800-$BFFF`. | `$1C0D-$B7FF` (STARTUP first), SWSTACK `$B800-$BFFF`. | ||||
| - **Bank 1:** compiler tables `$2000-$BFFF` (40 KB). | |||||
| - **Bank 1:** source cache `$2000-$6AFF` (D118), compiler tables | |||||
| `$6B00-$BFFF` (21,760 bytes). | |||||
| - **Zero page:** the compiler owns logical `$02-$FF` via P0; KERNAL calls run | - **Zero page:** the compiler owns logical `$02-$FF` via P0; KERNAL calls run | ||||
| on the system zero page. | on the system zero page. | ||||
| - **NMI:** guarded by `mmu_nmi_guard` through `$0318`; RESTORE is ignored | - **NMI:** guarded by `mmu_nmi_guard` through `$0318`; RESTORE is ignored | ||||
| @@ -195,8 +196,10 @@ pointer-rich abstract syntax tree. | |||||
| - write PRG and MAP output; | - write PRG and MAP output; | ||||
| - delete or invalidate partial output on failure. | - delete or invalidate partial output on failure. | ||||
| Repeated passes trade disk/source traversal time for bounded memory. The source | |||||
| stream should be rewindable without reopening unsafe shared emulator state. | |||||
| Repeated passes re-read the source, not the disk. `src_load` (`source.s`) | |||||
| reads the file once into the bank-1 cache and enforces the editor-profile | |||||
| limits. Each pass calls `src_rewind`, then `src_next_line` delivers one logical | |||||
| line at a time into a common-RAM buffer (D118, E02-S01). | |||||
| ## 6. Compiler Data Records | ## 6. Compiler Data Records | ||||
| @@ -386,6 +389,5 @@ features depend on it. | |||||
| - Generated-program memory map (the compiler map is frozen in section 4.4) | - 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 | |||||
| - Physical-hardware acceptance requirements | - Physical-hardware acceptance requirements | ||||
| @@ -39,6 +39,7 @@ documented default. `Proposed` means a later change still needs approval. | |||||
| | 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. | | | 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. | | ||||
| | D118 | The compiler loads the whole source file once into a bank-1 cache (`$2000-$6AFF`) and replays it for every pass, instead of re-reading the disk. | Rewind is exact by construction and avoids reading a slow disk three times. Compiler tables get the remaining 21,760 bytes of bank 1 (`$6B00-$BFFF`). Internal; no user-visible behavior. | | |||||
| ## Proposed | ## Proposed | ||||
| @@ -61,6 +62,6 @@ No proposed product changes are currently pending. | |||||
| 1. What generated-program memory map should E04-S01/E04-S02 adopt? (The | 1. What generated-program memory map should E04-S01/E04-S02 adopt? (The | ||||
| compiler map is D117.) | 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? | |||||
| 2. What compiler table capacities fit in the 21,760-byte bank-1 table window | |||||
| and 254-byte zero page (D117, D118)? | |||||
| @@ -1,7 +1,8 @@ | |||||
| # Nova128 Compiler Memory Map | # Nova128 Compiler Memory Map | ||||
| Story: E01-S05 | Story: E01-S05 | ||||
| Status: frozen for the compiler (D117); verified in VICE x128 3.10 | |||||
| Status: frozen for the compiler (D117, source cache D118); verified in VICE | |||||
| x128 3.10 | |||||
| Authoritative source: `cfg/compiler.cfg`. This document explains it. | Authoritative source: `cfg/compiler.cfg`. This document explains it. | ||||
| The generated-program memory map is **not** frozen here. It belongs to E04-S01 | The generated-program memory map is **not** frozen here. It belongs to E04-S01 | ||||
| @@ -42,12 +43,13 @@ COMMON, DISKBUF, private ZP, SCRATCH, and MAILBOX are visible from bank 1. | |||||
| | Range | Size | Name | Contents | | | Range | Size | Name | Contents | | ||||
| |---|---|---|---| | |---|---|---|---| | ||||
| | `$0000-$1FFF` | | hidden | Covered by bottom common RAM in compiler states | | | `$0000-$1FFF` | | hidden | Covered by bottom common RAM in compiler states | | ||||
| | `$2000-$BFFF` | 40960 | BANK1 / `TABLES` | Names, symbols, class/interface/type records, pass workspaces | | |||||
| | `$2000-$6AFF` | 19200 | SRCCACHE / `SOURCE` | Whole source file, loaded once by `src_load` (18,999-byte limit) | | |||||
| | `$6B00-$BFFF` | 21760 | 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 | | | `$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` | 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 | |||||
| and `mmu_copy_to1`, using the zero-page pointers `mmu_ptr`/`mmu_src`), or by | |||||
| code in COMMON such as `src_fetch_line`. MAIN code never runs while bank 1 is mapped, because MAIN above | |||||
| `$1FFF` is not visible then. | `$1FFF` is not visible then. | ||||
| BASIC 7 keeps variables in bank 1 from `$0400`. The compiler is started with | BASIC 7 keeps variables in bank 1 from `$0400`. The compiler is started with | ||||
| @@ -59,7 +61,7 @@ state. | |||||
| In compiler states P0 points at bank 0 page `$1A`, so the compiler owns the | 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 | 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 8502 port and are never allocated. Modules allocate zero page only through | ||||
| the `ZEROPAGE` segment. Current users: `mmu_ptr` (2 bytes). | |||||
| the `ZEROPAGE` segment. Current users: `mmu_ptr` and `mmu_src` (4 bytes). | |||||
| System zero page stays intact. KERNAL calls switch P0 back to `$0000` for the | 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 | call. While P0 is relocated, the system zero page is still readable at | ||||
| @@ -117,7 +119,7 @@ Protocol: | |||||
| |---|---| | |---|---| | ||||
| | ZP | A store to logical ZP lands at `$1Axx`; system ZP is unchanged | | | 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 | | | 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 | | |||||
| | BANK1 | First and last TABLES bytes (`$6B00`, `$BFFF`) 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 | | | 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 | | | 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 | | | RESTORE | After `mmu_shutdown`: CR/RCR/P0/P1/`$0318` and SP equal their entry values | | ||||
| @@ -0,0 +1,90 @@ | |||||
| --- | |||||
| story: E02-S01 | |||||
| owner: e02_source_reader | |||||
| status: done | |||||
| started: 2026-11-07 | |||||
| depends-on: [E01-S03, E01-S05] | |||||
| --- | |||||
| # E02-S01 Claim | |||||
| ## Intended change | |||||
| Add the compiler's source reader. `src_load` reads a PETSCII SEQ file from | |||||
| device 8 once into a bank-1 cache, enforcing the line, line-count, and file | |||||
| size limits with stable error codes and exact positions. `src_rewind` and | |||||
| `src_next_line` then replay the cached source identically for every pass, | |||||
| one logical line at a time. Disk access goes through new `diskio.s` KERNAL | |||||
| wrappers that always close their channels. | |||||
| ## File area | |||||
| - `src/compiler/source.s`, `src/compiler/diskio.s` | |||||
| - `src/common/source.inc` | |||||
| - `src/compiler/mmu.s` (bank-1 block copy only) | |||||
| - `cfg/compiler.cfg` (bank-1 source cache area) | |||||
| - `tests/platform/map-skeleton.s` (table-size constant only) | |||||
| - `tests/source/` | |||||
| - `scripts/build-source-test.ps1`, `scripts/verify-source-test.ps1`, | |||||
| `scripts/lib/` | |||||
| - `docs/memory-map.md`, `docs/architecture.md`, `docs/decisions.md` (D118) | |||||
| - `planning/claims/E02-S01.md` | |||||
| - generated files under `build/` | |||||
| ## Acceptance evidence | |||||
| - [x] Reads Vim-created SEQ source without host conversion: fixtures use | |||||
| Vim's exact save format (PETSCII, `$0D` lines, both letter ranges in | |||||
| `decls.n8`); the reader delivers the bytes unchanged, verified by a host | |||||
| Fletcher-16 match. A live Vim edit is E07-S03. | |||||
| - [x] Accepts `$0D` lines and a missing final `$0D`: `nofinal.n8` yields the | |||||
| same 50 lines and checksum as `decls.n8` with one byte fewer. | |||||
| - [x] Enforces line/file limits with stable errors: `edge.n8` (80-byte line, | |||||
| 599 lines, 18,999 bytes) loads; `longln.n8` -> code 3 at 3:81, | |||||
| `manyln.n8` -> code 4 at 600:1, `big.n8` -> code 2 at 500:38. | |||||
| - [x] Rewinds identically for all compiler passes: two replays per load give | |||||
| identical line counts and checksums, and `src_line_no` ends at the line | |||||
| count. | |||||
| - [x] Disk failure closes channels and leaves no partial output: `LDTND` is 0 | |||||
| after every load; after each failure `src_len` is 0 and `src_next_line` | |||||
| returns end of source at once; a later `decls.n8` load succeeds unchanged. | |||||
| ## Coordination notes | |||||
| Source error codes are reader return codes. E02-S04 maps them to stable `Exxx` | |||||
| diagnostics. A true Vim round trip (edit in x64sc, compile in x128) is | |||||
| E07-S03; this story proves the reader consumes Vim's exact byte format | |||||
| (`../vim/NOTES.md`, `vedit.c` save path) with no conversion. | |||||
| ## Work log | |||||
| - Confirmed Vim's save format in `../vim/NOTES.md` and `vedit.c` (raw | |||||
| buffer, `$0D` lines, final `$0D` appended, 80-column wrap on load). | |||||
| - Recorded D118: one disk read into a bank-1 cache (`$2000-$6AFF`), tables | |||||
| keep `$6B00-$BFFF` (21,760 bytes). Map skeleton re-verified with it. | |||||
| - Added `mmu_copy_to1` and `mmu_src` to `mmu.s`, `diskio.s` with a COMMON | |||||
| chunk reader run through `kcall`, `source.s`, and `diskio.inc`. | |||||
| - Added `scripts/lib/Nova128Test.psm1` (link, PETSCII conversion, disk | |||||
| images, remote monitor, mailbox polling). The monitor helper waits for the | |||||
| `(C:$xxxx)` prompt; a fixed delay returned empty replies with a 1581. | |||||
| - Negative control: allowing 81-byte lines makes the C128 report FAIL and the | |||||
| host flag `longln.n8` (code 0, position 0:0, 100 bytes kept). Restored. | |||||
| ## Completion | |||||
| - Tests run: `scripts/verify-source-test.ps1` (pass plus mutation run), | |||||
| `scripts/verify-map-skeleton.ps1`, `scripts/verify-kernal-probe.ps1`, | |||||
| `tests/grammar/validate.ps1`, `tests/disk/prepare-disk.tests.ps1`. | |||||
| - Results: all 8 fixture records match host expectations; screenshot shows | |||||
| each result code and `NOVA128 SOURCE TEST OK`; regressions pass. | |||||
| - Files changed: `src/compiler/source.s`, `src/compiler/diskio.s`, | |||||
| `src/compiler/mmu.s`, `src/common/source.inc`, `src/common/diskio.inc`, | |||||
| `cfg/compiler.cfg`, `tests/source/source-test.s`, | |||||
| `tests/platform/map-skeleton.s`, `scripts/build-source-test.ps1`, | |||||
| `scripts/verify-source-test.ps1`, `scripts/build-map-skeleton.ps1`, | |||||
| `scripts/lib/Nova128Test.psm1`, `docs/memory-map.md`, | |||||
| `docs/architecture.md`, `docs/decisions.md` (D118), this claim. | |||||
| - Remaining risks: device 8 is fixed; a read error part-way through a file | |||||
| (ST error other than EOI) is handled in code but not exercised, since VICE | |||||
| offers no simple way to inject one; only the 1581 (D81) was tested; error | |||||
| codes are reader codes until E02-S04 assigns user-facing `Exxx` IDs. | |||||
| @@ -87,7 +87,7 @@ if ($labelText -notmatch "(?im)^al\s+001C0D\s+\.skel_entry\s*$") { | |||||
| throw "skel_entry is not at `$1C0D (the SYS 7181 target)" | throw "skel_entry is not at `$1C0D (the SYS 7181 target)" | ||||
| } | } | ||||
| Require-Segment "SWSTACK" 0xB800 0xBFFF | Require-Segment "SWSTACK" 0xB800 0xBFFF | ||||
| Require-Segment "TABLES" 0x2000 0xBFFF | |||||
| Require-Segment "TABLES" 0x6B00 0xBFFF | |||||
| if ($segments["MAILBOX"].Start -ne 0x1BF0) { throw "mailbox is not at `$1BF0" } | if ($segments["MAILBOX"].Start -ne 0x1BF0) { throw "mailbox is not at `$1BF0" } | ||||
| $bytes = [IO.File]::ReadAllBytes($program) | $bytes = [IO.File]::ReadAllBytes($program) | ||||
| @@ -95,7 +95,7 @@ if ($bytes[0] -ne 0x01 -or $bytes[1] -ne 0x1C) { throw "load address is not `$1C | |||||
| Write-Host ("built {0} ({1} bytes)" -f $program, $bytes.Length) | Write-Host ("built {0} ({1} bytes)" -f $program, $bytes.Length) | ||||
| $limits = [ordered]@{ | $limits = [ordered]@{ | ||||
| ZEROPAGE = 0x00FE; COMMON = 0x0600; SWSTACK = 0x0800; TABLES = 0xA000 | |||||
| ZEROPAGE = 0x00FE; COMMON = 0x0600; SWSTACK = 0x0800; TABLES = 0x5500 | |||||
| } | } | ||||
| foreach ($name in ($segments.Keys | Sort-Object { $segments[$_].Start })) { | foreach ($name in ($segments.Keys | Sort-Object { $segments[$_].Start })) { | ||||
| $seg = $segments[$name] | $seg = $segments[$name] | ||||
| @@ -0,0 +1,117 @@ | |||||
| # E02-S01: build the source-reader test PRG and a fresh D81 holding it plus | |||||
| # generated PETSCII fixtures. Writes build/source-test/expected.json with the | |||||
| # results the host computes independently of the C128 code. | |||||
| [CmdletBinding()] | |||||
| param() | |||||
| $ErrorActionPreference = "Stop" | |||||
| $project = Split-Path $PSScriptRoot -Parent | |||||
| Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force | |||||
| $out = Join-Path $project "build\source-test" | |||||
| New-Item -ItemType Directory -Force -Path $out | Out-Null | |||||
| $link = Invoke-Nova128Link -Name "source-test" -BuildDir $out ` | |||||
| -Config (Join-Path $project "cfg\compiler.cfg") -Sources @( | |||||
| (Join-Path $project "src\compiler\mmu.s"), | |||||
| (Join-Path $project "src\compiler\diskio.s"), | |||||
| (Join-Path $project "src\compiler\source.s"), | |||||
| (Join-Path $project "tests\source\source-test.s")) | |||||
| if ((Get-Nova128Label $link.Labels "test_entry") -ne 0x1C0D) { throw "test_entry is not at `$1C0D" } | |||||
| # ---------------------------------------------------------------- fixtures | |||||
| function Get-Lines([byte[]]$bytes) { | |||||
| # Split exactly as src_next_line does: $0D ends a line; trailing bytes | |||||
| # without $0D form a last line. | |||||
| $lines = New-Object System.Collections.Generic.List[byte[]] | |||||
| $start = 0 | |||||
| for ($i = 0; $i -lt $bytes.Length; $i++) { | |||||
| if ($bytes[$i] -eq 13) { | |||||
| if ($i -gt $start) { $lines.Add([byte[]]$bytes[$start..($i - 1)]) } | |||||
| else { $lines.Add([byte[]]@()) } | |||||
| $start = $i + 1 | |||||
| } | |||||
| } | |||||
| if ($start -lt $bytes.Length) { $lines.Add([byte[]]$bytes[$start..($bytes.Length - 1)]) } | |||||
| return ,$lines | |||||
| } | |||||
| function Get-Fletcher([byte[]]$bytes) { | |||||
| $s1 = 0; $s2 = 0 | |||||
| foreach ($line in (Get-Lines $bytes)) { | |||||
| foreach ($b in (@($line) + 13)) { | |||||
| $s1 = ($s1 + $b) % 255 | |||||
| $s2 = ($s2 + $s1) % 255 | |||||
| } | |||||
| } | |||||
| return @($s1, $s2) | |||||
| } | |||||
| function New-Line([int]$length, [int]$seed) { | |||||
| # Mixed case so both PETSCII letter ranges appear. | |||||
| $alphabet = "abcdefghijKLMNOPQRST0123456789 =+()" | |||||
| $chars = for ($i = 0; $i -lt $length; $i++) { $alphabet[($i + $seed) % $alphabet.Length] } | |||||
| return (-join $chars) | |||||
| } | |||||
| $fixtures = [ordered]@{} | |||||
| $decls = ConvertTo-Nova128Petscii (Get-Content -Raw (Join-Path $project "tests\grammar\accepted\decls.n8")) | |||||
| if ($decls[-1] -ne 13) { throw "decls.n8 should end with a newline" } | |||||
| $fixtures["decls.n8"] = $decls | |||||
| $fixtures["nofinal.n8"] = [byte[]]$decls[0..($decls.Length - 2)] | |||||
| $edge = New-Object System.Text.StringBuilder | |||||
| [void]$edge.Append((New-Line 80 0) + "`n") # exactly the line limit | |||||
| for ($i = 0; $i -lt 598; $i++) { | |||||
| $len = if ($i -lt 380) { 31 } else { 30 } # totals exactly 18,999 bytes | |||||
| [void]$edge.Append((New-Line $len $i) + "`n") | |||||
| } | |||||
| $fixtures["edge.n8"] = ConvertTo-Nova128Petscii $edge.ToString() | |||||
| $long = "a = 1`nb = 2`n" + (New-Line 81 3) + "`nc = 3`n" | |||||
| $fixtures["longln.n8"] = ConvertTo-Nova128Petscii $long | |||||
| $many = (1..600 | ForEach-Object { "x = $_" }) -join "`n" | |||||
| $fixtures["manyln.n8"] = ConvertTo-Nova128Petscii ($many + "`n") | |||||
| $big = (1..500 | ForEach-Object { New-Line 37 $_ }) -join "`n" | |||||
| $fixtures["big.n8"] = ConvertTo-Nova128Petscii ($big + "`n") | |||||
| # Boundary sanity: these are the facts the expectations below rely on. | |||||
| $edgeLines = Get-Lines $fixtures["edge.n8"] | |||||
| if ($fixtures["edge.n8"].Length -ne 18999 -or $edgeLines.Count -ne 599 -or $edgeLines[0].Length -ne 80) { | |||||
| throw "edge.n8 is not at the exact limits" | |||||
| } | |||||
| if ($fixtures["big.n8"].Length -ne 19000) { throw "big.n8 is not 19,000 bytes" } | |||||
| $hasLow = $decls | Where-Object { $_ -ge 0x41 -and $_ -le 0x5A } | |||||
| $hasHigh = $decls | Where-Object { $_ -ge 0xC1 -and $_ -le 0xDA } | |||||
| if (-not $hasLow -or -not $hasHigh) { throw "decls.n8 lacks one PETSCII letter range" } | |||||
| $files = @(@{ Name = "source-test"; Path = $link.Program; Type = "prg" }) | |||||
| foreach ($name in $fixtures.Keys) { | |||||
| $path = Join-Path $out ($name -replace '\.n8$', '.pet') | |||||
| [IO.File]::WriteAllBytes($path, $fixtures[$name]) | |||||
| $files += @{ Name = $name; Path = $path; Type = "seq" } | |||||
| } | |||||
| $disk = Join-Path $out "source-test.d81" | |||||
| New-Nova128Disk -Path $disk -Files $files | |||||
| # ------------------------------------------------------------ expectations | |||||
| function Ok([string]$name) { | |||||
| $b = $fixtures[$name]; $f = Get-Fletcher $b | |||||
| return [ordered]@{ name = $name; code = 0; lines = (Get-Lines $b).Count; len = $b.Length; | |||||
| s1 = $f[0]; s2 = $f[1] } | |||||
| } | |||||
| $expected = @( | |||||
| (Ok "decls.n8"), | |||||
| (Ok "nofinal.n8"), | |||||
| (Ok "edge.n8"), | |||||
| [ordered]@{ name = "longln.n8"; code = 3; line = 3; col = 81 }, | |||||
| [ordered]@{ name = "manyln.n8"; code = 4; line = 600; col = 1 }, | |||||
| [ordered]@{ name = "big.n8"; code = 2; line = 500; col = 38 }, | |||||
| [ordered]@{ name = "missing.n8"; code = 1; dos = 62 }, | |||||
| (Ok "decls.n8") | |||||
| ) | |||||
| $expected | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $out "expected.json") | |||||
| Write-Host ("built {0}" -f $link.Program) | |||||
| Write-Host ("disk {0} ({1} fixtures)" -f $disk, $fixtures.Count) | |||||
| @@ -0,0 +1,233 @@ | |||||
| # Shared helpers for Nova128 build and x128 verification scripts. | |||||
| # Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force | |||||
| $script:ViceBin = "C:\Program Files\GTK3VICE-3.10-win64\bin" | |||||
| function Get-ViceTool([string]$Name) { | |||||
| $path = Join-Path $script:ViceBin "$Name.exe" | |||||
| if (-not (Test-Path -LiteralPath $path)) { throw "$Name.exe not found at $path" } | |||||
| return $path | |||||
| } | |||||
| # Assemble each source and link them with an ld65 config. | |||||
| # Returns @{ Program; Map; Labels }. | |||||
| function Invoke-Nova128Link { | |||||
| param( | |||||
| [Parameter(Mandatory)][string]$Name, | |||||
| [Parameter(Mandatory)][string[]]$Sources, | |||||
| [Parameter(Mandatory)][string]$Config, | |||||
| [Parameter(Mandatory)][string]$BuildDir, | |||||
| [string[]]$Defines = @() | |||||
| ) | |||||
| $ca65 = (Get-Command ca65 -ErrorAction Stop).Source | |||||
| $ld65 = (Get-Command ld65 -ErrorAction Stop).Source | |||||
| New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null | |||||
| $out = @{ | |||||
| Program = Join-Path $BuildDir "$Name.prg" | |||||
| Map = Join-Path $BuildDir "$Name.map" | |||||
| Labels = Join-Path $BuildDir "$Name.lbl" | |||||
| } | |||||
| foreach ($path in $out.Values) { | |||||
| if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Force } | |||||
| } | |||||
| $defineArgs = @() | |||||
| foreach ($d in $Defines) { $defineArgs += @("-D", $d) } | |||||
| $objects = @() | |||||
| foreach ($source in $Sources) { | |||||
| $object = Join-Path $BuildDir ("$Name-" + [IO.Path]::GetFileNameWithoutExtension($source) + ".o") | |||||
| & $ca65 --cpu 6502 @defineArgs -o $object $source | |||||
| if ($LASTEXITCODE -ne 0) { throw "ca65 failed on $source" } | |||||
| $objects += $object | |||||
| } | |||||
| $linkOutput = & $ld65 -C $Config -o $out.Program -m $out.Map -Ln $out.Labels @objects 2>&1 | |||||
| if ($LASTEXITCODE -ne 0) { throw ("ld65 failed:`n" + ($linkOutput | Out-String)) } | |||||
| return $out | |||||
| } | |||||
| function Get-Nova128Label { | |||||
| param([Parameter(Mandatory)][string]$Labels, [Parameter(Mandatory)][string]$Name) | |||||
| $text = Get-Content -Raw -LiteralPath $Labels | |||||
| $m = [regex]::Match($text, "(?im)^al\s+([0-9a-f]+)\s+\." + [regex]::Escape($Name) + "\s*$") | |||||
| if (-not $m.Success) { throw "label $Name not found in $Labels" } | |||||
| return [Convert]::ToInt32($m.Groups[1].Value, 16) | |||||
| } | |||||
| # ASCII text -> PETSCII SEQ bytes, using the same rules as Vim and | |||||
| # prepare-disk.ps1: a-z -> $41-$5A, A-Z -> $C1-$DA, CRLF/LF -> $0D. | |||||
| function ConvertTo-Nova128Petscii { | |||||
| param([Parameter(Mandatory)][AllowEmptyString()][string]$Text) | |||||
| $bytes = [Text.Encoding]::ASCII.GetBytes($Text) | |||||
| $out = New-Object System.Collections.Generic.List[byte] | |||||
| for ($i = 0; $i -lt $bytes.Length; $i++) { | |||||
| $v = $bytes[$i] | |||||
| if ($v -eq 13) { | |||||
| if ($i + 1 -lt $bytes.Length -and $bytes[$i + 1] -eq 10) { continue } | |||||
| $out.Add(13); continue | |||||
| } | |||||
| if ($v -eq 10) { $out.Add(13); continue } | |||||
| if ($v -ge 0x61 -and $v -le 0x7A) { $out.Add([byte]($v - 0x20)); continue } | |||||
| if ($v -ge 0x41 -and $v -le 0x5A) { $out.Add([byte]($v + 0x80)); continue } | |||||
| $out.Add($v) | |||||
| } | |||||
| return ,$out.ToArray() | |||||
| } | |||||
| # Create a fresh disk image and write files to it. | |||||
| # $Files: ordered list of @{ Name = "decls.n8"; Path = host file; Type = "seq"|"prg" } | |||||
| function New-Nova128Disk { | |||||
| param( | |||||
| [Parameter(Mandatory)][string]$Path, | |||||
| [Parameter(Mandatory)][object[]]$Files, | |||||
| [ValidateSet("d64", "d81")][string]$Type = "d81", | |||||
| [string]$Label = "nova128,n8" | |||||
| ) | |||||
| $c1541 = Get-ViceTool "c1541" | |||||
| if (Test-Path -LiteralPath $Path) { Remove-Item -LiteralPath $Path -Force } | |||||
| $c1541Args = @("-format", $Label, $Type, $Path) | |||||
| foreach ($f in $Files) { | |||||
| $suffix = if ($f.Type -eq "seq") { ",s" } else { "" } | |||||
| $c1541Args += @("-write", $f.Path, ($f.Name + $suffix)) | |||||
| } | |||||
| $output = & $c1541 @c1541Args 2>&1 | |||||
| if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $Path)) { | |||||
| throw ("c1541 failed:`n" + ($output | Out-String)) | |||||
| } | |||||
| $listing = (& $c1541 -attach $Path -list 2>&1) | Out-String | |||||
| foreach ($f in $Files) { | |||||
| if ($listing -notmatch ('"' + [regex]::Escape($f.Name) + '"\s+' + $f.Type)) { | |||||
| throw "$($f.Name) ($($f.Type)) missing from $Path" | |||||
| } | |||||
| } | |||||
| } | |||||
| # Start x128 with a remote monitor. Returns a session object. | |||||
| function Start-Nova128X128 { | |||||
| param( | |||||
| [Parameter(Mandatory)][string]$Autostart, | |||||
| [int]$Port = 6530, | |||||
| [string]$Drive8Image, | |||||
| [string]$Drive8Type = "1581", | |||||
| [string]$Screenshot | |||||
| ) | |||||
| $x128 = Get-ViceTool "x128" | |||||
| $argList = @() | |||||
| if ($Drive8Image) { | |||||
| $argList += @("-drive8type", $Drive8Type, "-drive8truedrive", "-8", ('"' + $Drive8Image + '"')) | |||||
| } | |||||
| $argList += @("-autostart", ('"' + $Autostart + '"'), "-autostart-warp", "-warp", | |||||
| "-sounddev", "dummy", "-remotemonitor", | |||||
| "-remotemonitoraddress", "ip4://127.0.0.1:$Port") | |||||
| if ($Screenshot) { | |||||
| if (Test-Path -LiteralPath $Screenshot) { Remove-Item -LiteralPath $Screenshot -Force } | |||||
| $argList += @("-exitscreenshotvicii", ('"' + $Screenshot + '"')) | |||||
| } | |||||
| $psi = [Diagnostics.ProcessStartInfo]::new() | |||||
| $psi.FileName = $x128 | |||||
| $psi.Arguments = $argList -join ' ' | |||||
| $psi.UseShellExecute = $true | |||||
| $psi.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden | |||||
| $process = [Diagnostics.Process]::Start($psi) | |||||
| $client = $null | |||||
| $deadline = [DateTime]::UtcNow.AddSeconds(10) | |||||
| while ([DateTime]::UtcNow -lt $deadline) { | |||||
| if ($process.HasExited) { throw "x128 exited before the remote monitor connected" } | |||||
| try { | |||||
| $client = [Net.Sockets.TcpClient]::new() | |||||
| $client.Connect("127.0.0.1", $Port) | |||||
| break | |||||
| } catch { | |||||
| if ($client) { $client.Dispose(); $client = $null } | |||||
| Start-Sleep -Milliseconds 200 | |||||
| } | |||||
| } | |||||
| if (-not $client) { $process.Kill(); throw "could not connect to the VICE remote monitor" } | |||||
| $stream = $client.GetStream() | |||||
| $writer = [IO.StreamWriter]::new($stream, [Text.Encoding]::ASCII, 1024, $true) | |||||
| $writer.NewLine = "`n" | |||||
| $writer.AutoFlush = $true | |||||
| return [pscustomobject]@{ Process = $process; Client = $client; Stream = $stream; Writer = $writer } | |||||
| } | |||||
| function Invoke-Nova128Monitor { | |||||
| param([Parameter(Mandatory)]$Session, [Parameter(Mandatory)][string]$Command, | |||||
| [int]$TimeoutMs = 5000) | |||||
| $Session.Writer.WriteLine($Command) | |||||
| $buffer = New-Object byte[] 65536 | |||||
| $text = "" | |||||
| # Every monitor reply ends with a prompt such as "(C:$1c0d) ". | |||||
| $deadline = [DateTime]::UtcNow.AddMilliseconds($TimeoutMs) | |||||
| while ([DateTime]::UtcNow -lt $deadline) { | |||||
| if ($Session.Stream.DataAvailable) { | |||||
| $n = $Session.Stream.Read($buffer, 0, $buffer.Length) | |||||
| if ($n -le 0) { break } | |||||
| $text += [Text.Encoding]::ASCII.GetString($buffer, 0, $n) | |||||
| if ($text -match '\(C:\$[0-9a-f]{4}\)\s*$') { break } | |||||
| } else { | |||||
| Start-Sleep -Milliseconds 50 | |||||
| } | |||||
| } | |||||
| return $text | |||||
| } | |||||
| # Read memory through the monitor. Returns byte[] (length = Count). | |||||
| function Read-Nova128Memory { | |||||
| param([Parameter(Mandatory)]$Session, [int]$Address, [int]$Count) | |||||
| $text = Invoke-Nova128Monitor $Session ("m `${0:X4} `${1:X4}" -f $Address, ($Address + $Count - 1)) | |||||
| $memory = @{} | |||||
| foreach ($row in [regex]::Matches($text, "(?i)C:([0-9a-f]{4})((?:[ ]{1,2}[0-9a-f]{2}){1,16})")) { | |||||
| $a = [Convert]::ToInt32($row.Groups[1].Value, 16) | |||||
| foreach ($hex in ($row.Groups[2].Value.Trim() -split '\s+')) { | |||||
| $memory[$a] = [Convert]::ToByte($hex, 16); $a++ | |||||
| } | |||||
| } | |||||
| $bytes = New-Object byte[] $Count | |||||
| for ($i = 0; $i -lt $Count; $i++) { | |||||
| if (-not $memory.ContainsKey($Address + $i)) { | |||||
| throw ("monitor did not return `${0:X4}; response: {1}" -f ($Address + $i), $text) | |||||
| } | |||||
| $bytes[$i] = $memory[$Address + $i] | |||||
| } | |||||
| return ,$bytes | |||||
| } | |||||
| # Poll the test mailbox ($1BF0) until it reports PASS or FAIL. Returns its | |||||
| # 16 bytes. Resumes emulation between polls. | |||||
| function Wait-Nova128Mailbox { | |||||
| param([Parameter(Mandatory)]$Session, [int]$TimeoutSeconds = 60) | |||||
| $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) | |||||
| while ($true) { | |||||
| Start-Sleep -Seconds 1 | |||||
| $mb = Read-Nova128Memory $Session 0x1BF0 16 | |||||
| if ($mb[0] -eq 0x4E -and $mb[1] -eq 0x38 -and ($mb[3] -eq 0xA5 -or $mb[3] -eq 0xFA)) { | |||||
| return ,$mb | |||||
| } | |||||
| if ([DateTime]::UtcNow -ge $deadline) { | |||||
| $state = if ($mb[0] -eq 0x4E) { "status `$" + $mb[3].ToString("X2") } else { "never initialized" } | |||||
| throw "mailbox did not finish within $TimeoutSeconds s ($state)" | |||||
| } | |||||
| Invoke-Nova128Monitor $Session "x" | Out-Null | |||||
| } | |||||
| } | |||||
| # Let the program finish printing, then quit x128 (writes any exit screenshot). | |||||
| function Stop-Nova128X128 { | |||||
| param([Parameter(Mandatory)]$Session, [int]$SettleSeconds = 2) | |||||
| try { | |||||
| if ($SettleSeconds -gt 0) { | |||||
| Invoke-Nova128Monitor $Session "x" | Out-Null | |||||
| Start-Sleep -Seconds $SettleSeconds | |||||
| Invoke-Nova128Monitor $Session "m 0000 0000" | Out-Null | |||||
| } | |||||
| $Session.Writer.WriteLine("quit") | |||||
| $Session.Writer.Dispose() | |||||
| $Session.Client.Dispose() | |||||
| if (-not $Session.Process.WaitForExit(10000)) { throw "x128 did not exit" } | |||||
| } finally { | |||||
| if (-not $Session.Process.HasExited) { $Session.Process.Kill(); $Session.Process.WaitForExit() } | |||||
| } | |||||
| } | |||||
| Export-ModuleMember -Function Get-ViceTool, Invoke-Nova128Link, Get-Nova128Label, | |||||
| ConvertTo-Nova128Petscii, New-Nova128Disk, Start-Nova128X128, Invoke-Nova128Monitor, | |||||
| Read-Nova128Memory, Wait-Nova128Mailbox, Stop-Nova128X128 | |||||
| @@ -0,0 +1,64 @@ | |||||
| # E02-S01: run the source-reader test in x128 (1581 true drive) and compare | |||||
| # every result record with the host-computed expectations. | |||||
| [CmdletBinding()] | |||||
| param([int]$Port = 6531, [int]$TimeoutSeconds = 180) | |||||
| $ErrorActionPreference = "Stop" | |||||
| $project = Split-Path $PSScriptRoot -Parent | |||||
| Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force | |||||
| & (Join-Path $PSScriptRoot "build-source-test.ps1") | |||||
| $out = Join-Path $project "build\source-test" | |||||
| $disk = Join-Path $out "source-test.d81" | |||||
| $screenshot = Join-Path $out "source-test-vicii.png" | |||||
| $expected = Get-Content -Raw (Join-Path $out "expected.json") | ConvertFrom-Json | |||||
| $session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port -Screenshot $screenshot | |||||
| try { | |||||
| $mb = Wait-Nova128Mailbox $session -TimeoutSeconds $TimeoutSeconds | |||||
| $count = $mb[8] | |||||
| $table = $mb[9] + 256 * $mb[10] | |||||
| if ($count -ne $expected.Count) { throw "program ran $count fixtures, host expects $($expected.Count)" } | |||||
| $raw = Read-Nova128Memory $session $table (16 * $count) | |||||
| } finally { | |||||
| Stop-Nova128X128 $session | |||||
| } | |||||
| $failures = @() | |||||
| for ($i = 0; $i -lt $count; $i++) { | |||||
| $r = $raw[(16 * $i)..(16 * $i + 15)] | |||||
| $e = $expected[$i] | |||||
| $got = [ordered]@{ | |||||
| code = $r[0]; dos = $r[1]; kernal = $r[2]; line = $r[3] + 256 * $r[4]; col = $r[5] | |||||
| ldtnd = $r[6]; lines = $r[7] + 256 * $r[8]; len = $r[9] + 256 * $r[10] | |||||
| s1 = $r[11]; s2 = $r[12]; flag = $r[13]; lastline = $r[14] + 256 * $r[15] | |||||
| } | |||||
| $problems = @() | |||||
| if ($got.code -ne $e.code) { $problems += "code $($got.code) expected $($e.code)" } | |||||
| if ($got.ldtnd -ne 0) { $problems += "$($got.ldtnd) file(s) left open" } | |||||
| if ($got.flag -ne 1) { | |||||
| $problems += $(if ($e.code -eq 0) { "passes differ" } else { "source delivered after failure" }) | |||||
| } | |||||
| if ($e.code -eq 0) { | |||||
| foreach ($k in "lines", "len", "s1", "s2") { | |||||
| if ($got[$k] -ne $e.$k) { $problems += "$k $($got[$k]) expected $($e.$k)" } | |||||
| } | |||||
| if ($got.lastline -ne $e.lines) { $problems += "src_line_no $($got.lastline) expected $($e.lines)" } | |||||
| $detail = "{0} lines, {1} bytes, fletcher {2:X2}{3:X2}" -f $got.lines, $got.len, $got.s1, $got.s2 | |||||
| } else { | |||||
| if ($got.len -ne 0) { $problems += "src_len $($got.len) after failure" } | |||||
| if ($null -ne $e.line -and ($got.line -ne $e.line -or $got.col -ne $e.col)) { | |||||
| $problems += "position $($got.line):$($got.col) expected $($e.line):$($e.col)" | |||||
| } | |||||
| if ($null -ne $e.dos -and $got.dos -ne $e.dos) { $problems += "DOS $($got.dos) expected $($e.dos)" } | |||||
| $detail = if ($e.code -eq 1) { "DOS {0}" -f $got.dos } else { "at {0}:{1}" -f $got.line, $got.col } | |||||
| } | |||||
| $status = if ($problems) { "FAIL" } else { "ok" } | |||||
| Write-Host (" {0,-11} code {1} {2,-36} {3}" -f $e.name, $got.code, $detail, $status) | |||||
| if ($problems) { $failures += "$($e.name): " + ($problems -join "; ") } | |||||
| } | |||||
| if ($mb[3] -ne 0xA5) { $failures += ("C128 reported FAIL ({0} fixture(s))" -f $mb[4]) } | |||||
| if ($failures) { throw ("source-reader verification failed:`n " + ($failures -join "`n ")) } | |||||
| Write-Host "verified source reader: $count fixtures match host expectations" | |||||
| Write-Host "screenshot: $screenshot" | |||||
| @@ -0,0 +1,12 @@ | |||||
| ; diskio.inc - logical file numbers and device used by src/compiler/diskio.s. | |||||
| .ifndef DISKIO_INC | |||||
| DISKIO_INC = 1 | |||||
| DIO_DEVICE = 8 | |||||
| DIO_LFN_CMD = 15 ; command/status channel: opened first, closed last | |||||
| DIO_LFN_IN = 2 ; source input | |||||
| DIO_SA_IN = 2 | |||||
| DIO_STATUS_MAX = 40 ; bytes kept from a drive status line | |||||
| .endif | |||||
| @@ -0,0 +1,20 @@ | |||||
| ; source.inc - source-reader limits and result codes (E02-S01). | |||||
| ; | |||||
| ; These are the reader's return codes. They are stable: E02-S04 maps each to | |||||
| ; a user-facing Exxx diagnostic without renumbering them. | |||||
| .ifndef SOURCE_INC | |||||
| SOURCE_INC = 1 | |||||
| SRC_MAX_LINE_BYTES = 80 ; logical line, excluding the $0D | |||||
| SRC_MAX_FILE_BYTES = 18999 ; whole file, including every $0D | |||||
| SRC_MAX_LINES = 599 | |||||
| SRC_EOL = $0D | |||||
| SRC_OK = 0 | |||||
| SRC_ERR_DISK = 1 ; src_kernal_err / src_dos_code say why | |||||
| SRC_ERR_FILE_TOO_LARGE = 2 ; position of byte 19,000 | |||||
| SRC_ERR_LINE_TOO_LONG = 3 ; position of the 81st byte of the line | |||||
| SRC_ERR_TOO_MANY_LINES = 4 ; line 600, column 1 | |||||
| .endif | |||||
| @@ -0,0 +1,168 @@ | |||||
| ; diskio.s - KERNAL file and command-channel wrappers for the compiler. | |||||
| ; | |||||
| ; All routines are called from the bank-0 compiler state and reach the KERNAL | |||||
| ; only through kcall_inline (mmu.s). They return C=1 on a KERNAL error with | |||||
| ; A = the KERNAL error number. Callers own the open/close ordering: open the | |||||
| ; command channel first, close it last (docs/architecture.md section 9). | |||||
| .setcpu "6502" | |||||
| .include "../common/c128.inc" | |||||
| .include "../common/diskio.inc" | |||||
| .import kcall_inline | |||||
| .export dio_open_cmd, dio_open_in, dio_chkin_in, dio_read_status | |||||
| .export dio_read_chunk, dio_clrchn, dio_close | |||||
| .export dio_count, dio_eof, dio_st, dio_dos_code, dio_status | |||||
| .import __DISKBUF_START__ | |||||
| ; ------------------------------------------------------------------ CODE | |||||
| .segment "CODE" | |||||
| ; OPEN 15,8,15 with no name. Nothing is sent on the bus until it is read. | |||||
| dio_open_cmd: | |||||
| jsr dio_setbank | |||||
| lda #DIO_LFN_CMD | |||||
| ldx #DIO_DEVICE | |||||
| ldy #15 | |||||
| jsr kcall_inline | |||||
| .word SETLFS | |||||
| lda #$00 | |||||
| jsr kcall_inline | |||||
| .word SETNAM | |||||
| jsr kcall_inline | |||||
| .word OPEN | |||||
| rts | |||||
| ; OPEN 2,8,2,name. In: A/X = name address (bank 0), Y = name length. | |||||
| dio_open_in: | |||||
| sta dio_tmp | |||||
| stx dio_tmp+1 | |||||
| sty dio_tmp+2 | |||||
| jsr dio_setbank | |||||
| lda dio_tmp+2 ; SETNAM: A=len, X/Y=address | |||||
| ldx dio_tmp | |||||
| ldy dio_tmp+1 | |||||
| jsr kcall_inline | |||||
| .word SETNAM | |||||
| lda #DIO_LFN_IN | |||||
| ldx #DIO_DEVICE | |||||
| ldy #DIO_SA_IN | |||||
| jsr kcall_inline | |||||
| .word SETLFS | |||||
| jsr kcall_inline | |||||
| .word OPEN | |||||
| rts | |||||
| dio_chkin_in: | |||||
| ldx #DIO_LFN_IN | |||||
| jsr kcall_inline | |||||
| .word CHKIN | |||||
| rts | |||||
| ; Read the drive status line from channel 15 into dio_status (CR ended) and | |||||
| ; its leading number into dio_dos_code. Leaves the default channels selected. | |||||
| dio_read_status: | |||||
| lda #$FF | |||||
| sta dio_dos_code | |||||
| ldx #DIO_LFN_CMD | |||||
| jsr kcall_inline | |||||
| .word CHKIN | |||||
| bcs @out | |||||
| ldx #$00 | |||||
| @read: stx dio_tmp | |||||
| jsr kcall_inline | |||||
| .word CHRIN | |||||
| ldx dio_tmp | |||||
| sta dio_status,x | |||||
| cmp #$0D | |||||
| beq @parse | |||||
| inx | |||||
| cpx #DIO_STATUS_MAX - 1 | |||||
| bcc @read | |||||
| lda #$0D | |||||
| sta dio_status,x | |||||
| @parse: lda dio_status ; two decimal digits -> binary | |||||
| sec | |||||
| sbc #'0' | |||||
| cmp #10 | |||||
| bcs @bad | |||||
| sta dio_tmp | |||||
| asl a ; *10 = *8 + *2 | |||||
| asl a | |||||
| adc dio_tmp | |||||
| asl a | |||||
| sta dio_tmp | |||||
| lda dio_status+1 | |||||
| sec | |||||
| sbc #'0' | |||||
| cmp #10 | |||||
| bcs @bad | |||||
| adc dio_tmp | |||||
| sta dio_dos_code | |||||
| @bad: jsr dio_clrchn | |||||
| clc | |||||
| @out: rts | |||||
| ; Read up to 256 bytes from the current input channel into DISKBUF. | |||||
| ; Out: dio_count (0 = 256), dio_eof ($40 at end of file), dio_st (ST with | |||||
| ; the EOI bit removed; non-zero means a read error). | |||||
| dio_read_chunk: | |||||
| jsr kcall_inline | |||||
| .word dio_chunk_sys | |||||
| rts | |||||
| dio_clrchn: | |||||
| jsr kcall_inline | |||||
| .word CLRCHN | |||||
| rts | |||||
| ; CLOSE the logical file in A. | |||||
| dio_close: | |||||
| jsr kcall_inline | |||||
| .word CLOSE | |||||
| rts | |||||
| dio_setbank: | |||||
| lda #$00 ; data and filename in bank 0 | |||||
| tax | |||||
| jsr kcall_inline | |||||
| .word SETBNK | |||||
| rts | |||||
| ; ---------------------------------------------------------------- COMMON | |||||
| ; Runs in the system state through kcall (system zero page, IRQs enabled), | |||||
| ; so it uses only absolute variables. | |||||
| .segment "COMMON" | |||||
| dio_chunk_sys: | |||||
| lda #$00 | |||||
| sta dio_count | |||||
| sta dio_eof | |||||
| sta dio_st | |||||
| @loop: jsr CHRIN | |||||
| ldx dio_count | |||||
| sta __DISKBUF_START__,x | |||||
| inc dio_count | |||||
| jsr READST | |||||
| beq @more | |||||
| tax | |||||
| and #$BF ; everything except EOI is an error | |||||
| sta dio_st | |||||
| txa | |||||
| and #$40 | |||||
| sta dio_eof | |||||
| rts | |||||
| @more: lda dio_count | |||||
| bne @loop ; stop after 256 bytes (count wraps to 0) | |||||
| rts | |||||
| dio_count: .byte $00 | |||||
| dio_eof: .byte $00 | |||||
| dio_st: .byte $00 | |||||
| ; ------------------------------------------------------------------- BSS | |||||
| .segment "BSS" | |||||
| dio_dos_code: .res 1 | |||||
| dio_tmp: .res 3 | |||||
| dio_status: .res DIO_STATUS_MAX | |||||
| @@ -28,13 +28,14 @@ | |||||
| .export mmu_init, mmu_bank0, mmu_bank1, mmu_system, mmu_shutdown | .export mmu_init, mmu_bank0, mmu_bank1, mmu_system, mmu_shutdown | ||||
| .export kcall, kcall_inline, kc_target | .export kcall, kcall_inline, kc_target | ||||
| .export mmu_nmi_guard, mmu_nmi_count | .export mmu_nmi_guard, mmu_nmi_count | ||||
| .export mmu_peek1, mmu_poke1 | |||||
| .exportzp mmu_ptr | |||||
| .export mmu_peek1, mmu_poke1, mmu_copy_to1 | |||||
| .exportzp mmu_ptr, mmu_src | |||||
| .export mmu_sys_cr, mmu_sys_rcr, mmu_sys_p0l, mmu_sys_p0h | .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 | .export mmu_sys_p1l, mmu_sys_p1h, mmu_sys_nmi, mmu_comp_rcr | ||||
| .segment "ZEROPAGE" | .segment "ZEROPAGE" | ||||
| mmu_ptr: .res 2 ; bank-1 access pointer (private page) | mmu_ptr: .res 2 ; bank-1 access pointer (private page) | ||||
| mmu_src: .res 2 ; common-RAM source for mmu_copy_to1 | |||||
| ; ------------------------------------------------------------------ CODE | ; ------------------------------------------------------------------ CODE | ||||
| .segment "CODE" | .segment "CODE" | ||||
| @@ -229,6 +230,19 @@ mmu_poke1: | |||||
| sta (mmu_ptr),y | sta (mmu_ptr),y | ||||
| jmp mmu_bank0 | jmp mmu_bank0 | ||||
| ; Copy X bytes (X=0 means 256) from (mmu_src) to bank1[(mmu_ptr)]. The | |||||
| ; source must be visible from bank 1: common RAM or $0000-$1FFF. Call from | |||||
| ; the bank-0 state; returns in it. Clobbers A/X/Y; pointers unchanged. | |||||
| mmu_copy_to1: | |||||
| jsr mmu_bank1 | |||||
| ldy #$00 | |||||
| @copy: lda (mmu_src),y | |||||
| sta (mmu_ptr),y | |||||
| iny | |||||
| dex | |||||
| bne @copy | |||||
| jmp mmu_bank0 | |||||
| ; Compact form: JSR kcall_inline / .word target. Preserves A/X/Y in, then | ; Compact form: JSR kcall_inline / .word target. Preserves A/X/Y in, then | ||||
| ; behaves exactly like kcall. | ; behaves exactly like kcall. | ||||
| kcall_inline: | kcall_inline: | ||||
| @@ -0,0 +1,341 @@ | |||||
| ; source.s - buffered PETSCII source reader (E02-S01). | |||||
| ; | |||||
| ; src_load reads one SEQ file from device 8 into the bank-1 source cache | |||||
| ; (SOURCE segment, D118), checking the editor-profile limits as bytes arrive. | |||||
| ; Every pass then calls src_rewind and src_next_line, which replay the cache | |||||
| ; one logical line at a time into src_line (common RAM). Bytes are not | |||||
| ; translated: PETSCII in, PETSCII out. Only the line terminator is consumed. | |||||
| ; | |||||
| ; Lines and columns are 1-based. A file that ends without $0D still has its | |||||
| ; last line. All routines run in the bank-0 compiler state. | |||||
| .setcpu "6502" | |||||
| .include "../common/source.inc" | |||||
| .include "../common/diskio.inc" | |||||
| .import mmu_copy_to1, mmu_bank0, mmu_bank1 | |||||
| .importzp mmu_ptr, mmu_src | |||||
| .import dio_open_cmd, dio_open_in, dio_chkin_in, dio_read_status | |||||
| .import dio_read_chunk, dio_clrchn, dio_close | |||||
| .import dio_count, dio_eof, dio_st, dio_dos_code | |||||
| .import __DISKBUF_START__ | |||||
| .export src_load, src_rewind, src_next_line | |||||
| .export src_line, src_line_len, src_line_no | |||||
| .export src_valid, src_len, src_lines | |||||
| .export src_err_line, src_err_col, src_kernal_err, src_dos_code | |||||
| ; ------------------------------------------------------------------ CODE | |||||
| .segment "CODE" | |||||
| ; Load a source file. | |||||
| ; in: A/X = filename address (bank 0), Y = filename length | |||||
| ; out: C=0, A=SRC_OK, src_len/src_lines set, src_valid=1 | |||||
| ; C=1, A=SRC_ERR_*, src_err_line/col (limit errors) or | |||||
| ; src_kernal_err/src_dos_code (disk errors); src_valid=0, src_len=0 | |||||
| ; Channels 2 and 15 are closed on every path. | |||||
| src_load: | |||||
| sta name_ptr | |||||
| stx name_ptr+1 | |||||
| sty name_len | |||||
| lda #$00 | |||||
| ldx #state_end - state_start - 1 | |||||
| @clr: sta state_start,x | |||||
| dex | |||||
| bpl @clr | |||||
| jsr open_source | |||||
| bcc @chunk | |||||
| cmp #$00 ; A=0: command channel never opened | |||||
| beq @fail_closed_disk | |||||
| lda #SRC_ERR_DISK | |||||
| bne @fail | |||||
| @chunk: jsr dio_read_chunk | |||||
| jsr validate_chunk | |||||
| bcs @limit_err | |||||
| lda #<__DISKBUF_START__ ; append the chunk to the bank-1 cache | |||||
| sta mmu_src | |||||
| lda #>__DISKBUF_START__ | |||||
| sta mmu_src+1 | |||||
| clc | |||||
| lda #<src_cache | |||||
| adc src_len | |||||
| sta mmu_ptr | |||||
| lda #>src_cache | |||||
| adc src_len+1 | |||||
| sta mmu_ptr+1 | |||||
| ldx dio_count | |||||
| jsr mmu_copy_to1 | |||||
| lda dio_count ; src_len += count (0 means 256) | |||||
| bne @add | |||||
| inc src_len+1 | |||||
| bne @added | |||||
| @add: clc | |||||
| adc src_len | |||||
| sta src_len | |||||
| bcc @added | |||||
| inc src_len+1 | |||||
| @added: lda dio_st | |||||
| bne @read_err | |||||
| lda dio_eof | |||||
| beq @chunk | |||||
| jsr close_all | |||||
| lda v_lines | |||||
| sta src_lines | |||||
| lda v_lines+1 | |||||
| sta src_lines+1 | |||||
| lda #$01 | |||||
| sta src_valid | |||||
| jsr src_rewind | |||||
| lda #SRC_OK | |||||
| clc | |||||
| rts | |||||
| @read_err: ; ST error mid-file: ask the drive why | |||||
| jsr dio_clrchn | |||||
| jsr dio_read_status | |||||
| lda dio_dos_code | |||||
| sta src_dos_code | |||||
| lda #SRC_ERR_DISK | |||||
| bne @fail | |||||
| @fail_closed_disk: | |||||
| lda #SRC_ERR_DISK | |||||
| bne @fail_closed | |||||
| @limit_err: ; A = SRC_ERR_* from validate_chunk | |||||
| @fail: pha | |||||
| jsr close_all | |||||
| pla | |||||
| @fail_closed: | |||||
| sta src_err | |||||
| lda #$00 ; no partial source survives a failure | |||||
| sta src_valid | |||||
| sta src_len | |||||
| sta src_len+1 | |||||
| sta src_lines | |||||
| sta src_lines+1 | |||||
| jsr src_rewind | |||||
| lda src_err | |||||
| sec | |||||
| rts | |||||
| ; Open channel 15, then the source on channel 2, and select it for input. | |||||
| ; C=0 on success. C=1 on failure with src_kernal_err/src_dos_code set and | |||||
| ; A=0 if channel 15 could not be opened (nothing to close), else A=1. | |||||
| open_source: | |||||
| jsr dio_open_cmd | |||||
| bcc @cmd_open | |||||
| sta src_kernal_err | |||||
| lda #DIO_LFN_CMD ; OPEN may have entered the file table | |||||
| jsr dio_close | |||||
| lda #$00 | |||||
| sec | |||||
| rts | |||||
| @cmd_open: | |||||
| lda name_ptr | |||||
| ldx name_ptr+1 | |||||
| ldy name_len | |||||
| jsr dio_open_in | |||||
| bcs @kernal | |||||
| jsr dio_read_status ; e.g. 62,FILE NOT FOUND | |||||
| bcs @kernal | |||||
| lda dio_dos_code | |||||
| sta src_dos_code | |||||
| cmp #20 | |||||
| bcs @failed | |||||
| jsr dio_chkin_in | |||||
| bcc @ok | |||||
| @kernal: | |||||
| sta src_kernal_err | |||||
| @failed: | |||||
| lda #$01 | |||||
| sec | |||||
| @ok: rts | |||||
| close_all: | |||||
| jsr dio_clrchn | |||||
| lda #DIO_LFN_IN | |||||
| jsr dio_close | |||||
| lda #DIO_LFN_CMD ; command channel last | |||||
| jmp dio_close | |||||
| ; Check every byte of the chunk in DISKBUF against the limits. | |||||
| ; C=1 with A=SRC_ERR_* and src_err_line/col on the first violation. | |||||
| validate_chunk: | |||||
| ldx #$00 | |||||
| @byte: lda v_col | |||||
| bne @in_line | |||||
| inc v_lines ; first byte of a new line | |||||
| bne :+ | |||||
| inc v_lines+1 | |||||
| : lda v_lines+1 | |||||
| cmp #>(SRC_MAX_LINES + 1) | |||||
| bcc @lines_ok | |||||
| bne @too_many | |||||
| lda v_lines | |||||
| cmp #<(SRC_MAX_LINES + 1) | |||||
| bcc @lines_ok | |||||
| @too_many: | |||||
| lda #$01 | |||||
| sta src_err_col | |||||
| lda #SRC_ERR_TOO_MANY_LINES | |||||
| bne @error | |||||
| @lines_ok: | |||||
| @in_line: | |||||
| inc v_total | |||||
| bne :+ | |||||
| inc v_total+1 | |||||
| : lda v_total+1 | |||||
| cmp #>(SRC_MAX_FILE_BYTES + 1) | |||||
| bcc @size_ok | |||||
| bne @too_big | |||||
| lda v_total | |||||
| cmp #<(SRC_MAX_FILE_BYTES + 1) | |||||
| bcc @size_ok | |||||
| @too_big: | |||||
| ldy v_col | |||||
| iny | |||||
| sty src_err_col | |||||
| lda #SRC_ERR_FILE_TOO_LARGE | |||||
| bne @error | |||||
| @size_ok: | |||||
| lda __DISKBUF_START__,x | |||||
| cmp #SRC_EOL | |||||
| bne @not_eol | |||||
| lda #$00 | |||||
| sta v_col | |||||
| beq @next | |||||
| @not_eol: | |||||
| inc v_col | |||||
| lda v_col | |||||
| cmp #SRC_MAX_LINE_BYTES + 1 | |||||
| bcc @next | |||||
| sta src_err_col | |||||
| lda #SRC_ERR_LINE_TOO_LONG | |||||
| bne @error | |||||
| @next: inx | |||||
| cpx dio_count ; dio_count 0 = 256: stops when X wraps | |||||
| bne @byte | |||||
| clc | |||||
| rts | |||||
| @error: pha | |||||
| lda v_lines | |||||
| sta src_err_line | |||||
| lda v_lines+1 | |||||
| sta src_err_line+1 | |||||
| pla | |||||
| sec | |||||
| rts | |||||
| ; Restart line delivery at the first line. Valid after any src_load. | |||||
| src_rewind: | |||||
| lda #$00 | |||||
| sta src_pos | |||||
| sta src_pos+1 | |||||
| sta src_line_no | |||||
| sta src_line_no+1 | |||||
| sta src_line_len | |||||
| rts | |||||
| ; Deliver the next logical line. | |||||
| ; out: C=0: src_line = bytes without $0D, src_line_len, src_line_no | |||||
| ; C=1: end of source (or no valid source loaded) | |||||
| src_next_line: | |||||
| lda src_valid | |||||
| beq @eof | |||||
| sec ; remaining = src_len - src_pos | |||||
| lda src_len | |||||
| sbc src_pos | |||||
| sta remaining | |||||
| lda src_len+1 | |||||
| sbc src_pos+1 | |||||
| sta remaining+1 | |||||
| ora remaining | |||||
| beq @eof | |||||
| lda remaining+1 ; fetch at most one line plus its $0D | |||||
| bne @cap | |||||
| lda remaining | |||||
| cmp #SRC_MAX_LINE_BYTES + 1 | |||||
| bcc @limit | |||||
| @cap: lda #SRC_MAX_LINE_BYTES + 1 | |||||
| @limit: sta fetch_limit | |||||
| clc | |||||
| lda #<src_cache | |||||
| adc src_pos | |||||
| sta mmu_ptr | |||||
| lda #>src_cache | |||||
| adc src_pos+1 | |||||
| sta mmu_ptr+1 | |||||
| jsr src_fetch_line ; Y = content length, X = bytes consumed | |||||
| sty src_line_len | |||||
| txa | |||||
| clc | |||||
| adc src_pos | |||||
| sta src_pos | |||||
| bcc :+ | |||||
| inc src_pos+1 | |||||
| : inc src_line_no | |||||
| bne :+ | |||||
| inc src_line_no+1 | |||||
| : clc | |||||
| rts | |||||
| @eof: sec | |||||
| rts | |||||
| ; ---------------------------------------------------------------- COMMON | |||||
| ; Runs with bank 1 mapped, so it lives in common RAM. | |||||
| .segment "COMMON" | |||||
| src_fetch_line: | |||||
| jsr mmu_bank1 | |||||
| ldy #$00 | |||||
| @copy: cpy fetch_limit | |||||
| beq @no_eol ; last line without a final $0D | |||||
| lda (mmu_ptr),y | |||||
| cmp #SRC_EOL | |||||
| beq @eol | |||||
| sta src_line,y | |||||
| iny | |||||
| bne @copy | |||||
| @eol: tya | |||||
| tax | |||||
| inx ; the $0D is consumed too | |||||
| jmp mmu_bank0 | |||||
| @no_eol: | |||||
| tya | |||||
| tax | |||||
| jmp mmu_bank0 | |||||
| fetch_limit: .byte $00 | |||||
| ; ---------------------------------------------------------------- SOURCE | |||||
| ; Bank-1 cache for the whole file. Never touched directly from bank 0. | |||||
| .segment "SOURCE" | |||||
| src_cache: .res SRC_MAX_FILE_BYTES | |||||
| ; --------------------------------------------------------------- SCRATCH | |||||
| .segment "SCRATCH" | |||||
| src_line: .res SRC_MAX_LINE_BYTES | |||||
| ; ------------------------------------------------------------------- BSS | |||||
| .segment "BSS" | |||||
| name_ptr: .res 2 | |||||
| name_len: .res 1 | |||||
| remaining: .res 2 | |||||
| src_line_len: .res 1 | |||||
| src_line_no: .res 2 | |||||
| src_pos: .res 2 | |||||
| state_start: ; cleared at the start of every src_load | |||||
| src_valid: .res 1 | |||||
| src_len: .res 2 | |||||
| src_lines: .res 2 | |||||
| src_err: .res 1 | |||||
| src_err_line: .res 2 | |||||
| src_err_col: .res 1 | |||||
| src_kernal_err: .res 1 | |||||
| src_dos_code: .res 1 | |||||
| v_col: .res 1 | |||||
| v_lines: .res 2 | |||||
| v_total: .res 2 | |||||
| state_end: | |||||
| @@ -70,7 +70,7 @@ swstack_last: .res 1 | |||||
| .segment "TABLES" | .segment "TABLES" | ||||
| tables_first: .res 1 | tables_first: .res 1 | ||||
| .res $A000 - 2 | |||||
| .res $5500 - 2 | |||||
| tables_last: .res 1 | tables_last: .res 1 | ||||
| .ifdef OVERFLOW_TABLES | .ifdef OVERFLOW_TABLES | ||||
| .res 1 | .res 1 | ||||
| @@ -0,0 +1,346 @@ | |||||
| ; E02-S01: source-reader test in x128. | |||||
| ; | |||||
| ; Loads each fixture from drive 8 with src_load. For a successful load it | |||||
| ; replays the source twice (src_rewind + src_next_line) and records the line | |||||
| ; count and a Fletcher-16 checksum of the delivered lines, each terminated by | |||||
| ; $0D. For a failed load it records the error and checks that no source | |||||
| ; survives. Every record is 16 bytes in `results`: | |||||
| ; +0 result code +1 DOS status code +2 KERNAL error | |||||
| ; +3 error line (2) +5 error column +6 LDTND after src_load | |||||
| ; +7 lines (2) +9 src_len (2) +11 Fletcher s1, +12 s2 | |||||
| ; +13 flag: OK -> both passes identical; error -> next_line gave EOF | |||||
| ; +14 src_line_no after the last line (2) | |||||
| ; Mailbox: PASS when every result code matches its expectation and every | |||||
| ; flag is 1; MB_DATA+0 = fixture count, MB_DATA+1/2 = address of results. | |||||
| ; The host checks every other field against values it computes itself. | |||||
| .setcpu "6502" | |||||
| .include "../../src/common/c128.inc" | |||||
| .include "../../src/common/mailbox.inc" | |||||
| .include "../../src/common/source.inc" | |||||
| .import mmu_init, mmu_bank0, mmu_shutdown, kcall_inline | |||||
| .import src_load, src_rewind, src_next_line | |||||
| .import src_line, src_line_len, src_line_no, src_len | |||||
| .import src_err_line, src_err_col, src_kernal_err, src_dos_code | |||||
| .import __COMPILER_ZP_PAGE__ | |||||
| REC_SIZE = 16 | |||||
| .segment "LOADADDR" | |||||
| .word $1C01 | |||||
| .segment "EXEHDR" | |||||
| .word basic_end | |||||
| .word 10 | |||||
| .byte $9E | |||||
| .byte "7181" | |||||
| .byte $00 | |||||
| basic_end: | |||||
| .word $0000 | |||||
| .segment "MAILBOX" | |||||
| .res MB_SIZE | |||||
| .segment "ZEROPAGE" | |||||
| fx_ptr: .res 2 ; current fixture table entry | |||||
| rec_ptr: .res 2 ; current result record | |||||
| .segment "STARTUP" | |||||
| .export test_entry, results | |||||
| test_entry: | |||||
| php | |||||
| sei | |||||
| ldx #MB_SIZE - 1 | |||||
| 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 | |||||
| jsr mmu_init | |||||
| jsr mmu_bank0 | |||||
| lda #$00 | |||||
| sta failures | |||||
| sta fx_index | |||||
| lda #<fixtures | |||||
| sta fx_ptr | |||||
| lda #>fixtures | |||||
| sta fx_ptr+1 | |||||
| lda #<results | |||||
| sta rec_ptr | |||||
| lda #>results | |||||
| sta rec_ptr+1 | |||||
| @next: ldy #0 ; entry: name lo, name hi, length, expected | |||||
| lda (fx_ptr),y | |||||
| sta name_lo | |||||
| iny | |||||
| lda (fx_ptr),y | |||||
| sta name_hi | |||||
| iny | |||||
| lda (fx_ptr),y | |||||
| sta name_len | |||||
| iny | |||||
| lda (fx_ptr),y | |||||
| sta expected | |||||
| jsr run_fixture | |||||
| clc | |||||
| lda fx_ptr | |||||
| adc #4 | |||||
| sta fx_ptr | |||||
| bcc :+ | |||||
| inc fx_ptr+1 | |||||
| : clc | |||||
| lda rec_ptr | |||||
| adc #REC_SIZE | |||||
| sta rec_ptr | |||||
| bcc :+ | |||||
| inc rec_ptr+1 | |||||
| : inc fx_index | |||||
| lda fx_index | |||||
| cmp #FIXTURE_COUNT | |||||
| bne @next | |||||
| jsr mmu_shutdown | |||||
| lda #FIXTURE_COUNT | |||||
| sta MB_DATA+0 | |||||
| lda #<results | |||||
| sta MB_DATA+1 | |||||
| lda #>results | |||||
| sta MB_DATA+2 | |||||
| lda failures | |||||
| sta MB_CODE | |||||
| ldy #ok_msg - messages | |||||
| lda failures | |||||
| bne @fail | |||||
| lda #MB_ST_PASS | |||||
| bne @report | |||||
| @fail: ldy #fail_msg - messages | |||||
| lda #MB_ST_FAIL | |||||
| @report: | |||||
| sta MB_STATUS | |||||
| plp | |||||
| @print: lda messages,y | |||||
| beq @end | |||||
| sty print_y | |||||
| jsr CHROUT | |||||
| ldy print_y | |||||
| iny | |||||
| bne @print | |||||
| @end: rts | |||||
| .segment "CODE" | |||||
| ; Load one fixture and fill its record (rec_ptr). | |||||
| run_fixture: | |||||
| ldx #REC_SIZE - 1 | |||||
| lda #$00 | |||||
| @zero: sta record,x | |||||
| dex | |||||
| bpl @zero | |||||
| jsr show_name | |||||
| lda name_lo | |||||
| ldx name_hi | |||||
| ldy name_len | |||||
| jsr src_load | |||||
| sta record+0 | |||||
| lda src_dos_code | |||||
| sta record+1 | |||||
| lda src_kernal_err | |||||
| sta record+2 | |||||
| lda src_err_line | |||||
| sta record+3 | |||||
| lda src_err_line+1 | |||||
| sta record+4 | |||||
| lda src_err_col | |||||
| sta record+5 | |||||
| lda __COMPILER_ZP_PAGE__ * 256 + LDTND ; system ZP via the swap | |||||
| sta record+6 | |||||
| lda src_len | |||||
| sta record+9 | |||||
| lda src_len+1 | |||||
| sta record+10 | |||||
| lda record+0 | |||||
| bne @failed | |||||
| jsr replay ; pass A | |||||
| lda count | |||||
| sta record+7 | |||||
| lda count+1 | |||||
| sta record+8 | |||||
| lda s1 | |||||
| sta record+11 | |||||
| lda s2 | |||||
| sta record+12 | |||||
| lda src_line_no | |||||
| sta record+14 | |||||
| lda src_line_no+1 | |||||
| sta record+15 | |||||
| jsr replay ; pass B must match exactly | |||||
| lda count | |||||
| cmp record+7 | |||||
| bne @check | |||||
| lda count+1 | |||||
| cmp record+8 | |||||
| bne @check | |||||
| lda s1 | |||||
| cmp record+11 | |||||
| bne @check | |||||
| lda s2 | |||||
| cmp record+12 | |||||
| bne @check | |||||
| inc record+13 | |||||
| bne @check | |||||
| @failed: | |||||
| jsr src_rewind ; nothing may be delivered after a failure | |||||
| jsr src_next_line | |||||
| bcc @check | |||||
| inc record+13 | |||||
| @check: lda record+0 | |||||
| cmp expected | |||||
| bne @bad | |||||
| lda record+13 | |||||
| cmp #$01 | |||||
| beq @store | |||||
| @bad: inc failures | |||||
| @store: ldy #REC_SIZE - 1 | |||||
| @copy: lda record,y | |||||
| sta (rec_ptr),y | |||||
| dey | |||||
| bpl @copy | |||||
| lda record+0 | |||||
| ora #'0' | |||||
| jsr kcall_inline | |||||
| .word CHROUT | |||||
| lda #$0D | |||||
| jsr kcall_inline | |||||
| .word CHROUT | |||||
| rts | |||||
| ; One pass over the source: count lines, Fletcher-16 over line bytes + $0D. | |||||
| replay: lda #$00 | |||||
| sta count | |||||
| sta count+1 | |||||
| sta s1 | |||||
| sta s2 | |||||
| jsr src_rewind | |||||
| @line: jsr src_next_line | |||||
| bcs @done | |||||
| inc count | |||||
| bne :+ | |||||
| inc count+1 | |||||
| : ldy #$00 | |||||
| @byte: cpy src_line_len | |||||
| beq @eol | |||||
| lda src_line,y | |||||
| jsr fletcher | |||||
| iny | |||||
| bne @byte | |||||
| @eol: lda #SRC_EOL | |||||
| jsr fletcher | |||||
| jmp @line | |||||
| @done: rts | |||||
| ; s1 = (s1 + A) mod 255; s2 = (s2 + s1) mod 255. Preserves Y. | |||||
| fletcher: | |||||
| clc | |||||
| adc s1 | |||||
| adc #$00 ; end-around carry: 256 = 1 (mod 255) | |||||
| cmp #$FF | |||||
| bne :+ | |||||
| lda #$00 | |||||
| : sta s1 | |||||
| clc | |||||
| adc s2 | |||||
| adc #$00 | |||||
| cmp #$FF | |||||
| bne :+ | |||||
| lda #$00 | |||||
| : sta s2 | |||||
| rts | |||||
| show_name: | |||||
| ldy #$00 | |||||
| @ch: cpy name_len | |||||
| beq @sp | |||||
| lda name_lo | |||||
| sta @rd+1 | |||||
| lda name_hi | |||||
| sta @rd+2 | |||||
| @rd: lda $FFFF,y ; patched | |||||
| sty print_y | |||||
| jsr kcall_inline | |||||
| .word CHROUT | |||||
| ldy print_y | |||||
| iny | |||||
| bne @ch | |||||
| @sp: lda #' ' | |||||
| jsr kcall_inline | |||||
| .word CHROUT | |||||
| rts | |||||
| .segment "RODATA" | |||||
| ; name address, name length, expected result code | |||||
| fixtures: | |||||
| .word f_decls | |||||
| .byte f_decls_end - f_decls, SRC_OK | |||||
| .word f_nofinal | |||||
| .byte f_nofinal_end - f_nofinal, SRC_OK | |||||
| .word f_edge | |||||
| .byte f_edge_end - f_edge, SRC_OK | |||||
| .word f_longln | |||||
| .byte f_longln_end - f_longln, SRC_ERR_LINE_TOO_LONG | |||||
| .word f_manyln | |||||
| .byte f_manyln_end - f_manyln, SRC_ERR_TOO_MANY_LINES | |||||
| .word f_big | |||||
| .byte f_big_end - f_big, SRC_ERR_FILE_TOO_LARGE | |||||
| .word f_missing | |||||
| .byte f_missing_end - f_missing, SRC_ERR_DISK | |||||
| .word f_decls | |||||
| .byte f_decls_end - f_decls, SRC_OK ; again, after failures | |||||
| FIXTURE_COUNT = (* - fixtures) / 4 | |||||
| ; Unshifted PETSCII letters ($41-$5A) are what c1541 stores for lowercase. | |||||
| f_decls: .byte "DECLS.N8" | |||||
| f_decls_end: | |||||
| f_nofinal: .byte "NOFINAL.N8" | |||||
| f_nofinal_end: | |||||
| f_edge: .byte "EDGE.N8" | |||||
| f_edge_end: | |||||
| f_longln: .byte "LONGLN.N8" | |||||
| f_longln_end: | |||||
| f_manyln: .byte "MANYLN.N8" | |||||
| f_manyln_end: | |||||
| f_big: .byte "BIG.N8" | |||||
| f_big_end: | |||||
| f_missing: .byte "MISSING.N8" | |||||
| f_missing_end: | |||||
| messages: | |||||
| ok_msg: .byte "NOVA128 SOURCE TEST OK", $0D, $00 | |||||
| fail_msg: .byte "NOVA128 SOURCE TEST FAIL", $0D, $00 | |||||
| .segment "BSS" | |||||
| results: .res REC_SIZE * FIXTURE_COUNT | |||||
| record: .res REC_SIZE | |||||
| failures: .res 1 | |||||
| fx_index: .res 1 | |||||
| name_lo: .res 1 | |||||
| name_hi: .res 1 | |||||
| name_len: .res 1 | |||||
| expected: .res 1 | |||||
| count: .res 2 | |||||
| s1: .res 1 | |||||
| s2: .res 1 | |||||
| print_y: .res 1 | |||||
Powered by TurnKey Linux.