diff --git a/newLang/cfg/compiler.cfg b/newLang/cfg/compiler.cfg index 5489761..b573252 100644 --- a/newLang/cfg/compiler.cfg +++ b/newLang/cfg/compiler.cfg @@ -4,8 +4,9 @@ # # 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) +# $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) # $1B00-$1BEF SCRATCH (common) # $1BF0-$1BFF MAILBOX @@ -28,7 +29,9 @@ MEMORY { 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 + # 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 { @@ -45,6 +48,7 @@ SEGMENTS { SCRATCH: load = SCRATCH, type = bss, optional = yes; MAILBOX: load = MAILBOX, 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; } diff --git a/newLang/docs/architecture.md b/newLang/docs/architecture.md index e1a04d0..f00a3e8 100644 --- a/newLang/docs/architecture.md +++ b/newLang/docs/architecture.md @@ -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 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). +- **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 on the system zero page. - **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; - 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 @@ -386,6 +389,5 @@ features depend on it. - 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 - Physical-hardware acceptance requirements diff --git a/newLang/docs/decisions.md b/newLang/docs/decisions.md index 2e9e06b..191cae6 100644 --- a/newLang/docs/decisions.md +++ b/newLang/docs/decisions.md @@ -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. | | 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. | +| 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 @@ -61,6 +62,6 @@ No proposed product changes are currently pending. 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? +2. What compiler table capacities fit in the 21,760-byte bank-1 table window + and 254-byte zero page (D117, D118)? diff --git a/newLang/docs/memory-map.md b/newLang/docs/memory-map.md index eefc8b8..e20d3e5 100644 --- a/newLang/docs/memory-map.md +++ b/newLang/docs/memory-map.md @@ -1,7 +1,8 @@ # Nova128 Compiler Memory Map 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. 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 | |---|---|---|---| | `$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 | 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. 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 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). +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 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 | | 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 | | 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 | diff --git a/newLang/planning/claims/E02-S01.md b/newLang/planning/claims/E02-S01.md new file mode 100644 index 0000000..6e5448d --- /dev/null +++ b/newLang/planning/claims/E02-S01.md @@ -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. diff --git a/newLang/scripts/build-map-skeleton.ps1 b/newLang/scripts/build-map-skeleton.ps1 index 62a2456..aad1856 100644 --- a/newLang/scripts/build-map-skeleton.ps1 +++ b/newLang/scripts/build-map-skeleton.ps1 @@ -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)" } 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" } $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) $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 })) { $seg = $segments[$name] diff --git a/newLang/scripts/build-source-test.ps1 b/newLang/scripts/build-source-test.ps1 new file mode 100644 index 0000000..041eae4 --- /dev/null +++ b/newLang/scripts/build-source-test.ps1 @@ -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) diff --git a/newLang/scripts/lib/Nova128Test.psm1 b/newLang/scripts/lib/Nova128Test.psm1 new file mode 100644 index 0000000..ed84765 --- /dev/null +++ b/newLang/scripts/lib/Nova128Test.psm1 @@ -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 diff --git a/newLang/scripts/verify-source-test.ps1 b/newLang/scripts/verify-source-test.ps1 new file mode 100644 index 0000000..0474a61 --- /dev/null +++ b/newLang/scripts/verify-source-test.ps1 @@ -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" diff --git a/newLang/src/common/diskio.inc b/newLang/src/common/diskio.inc new file mode 100644 index 0000000..5b4e647 --- /dev/null +++ b/newLang/src/common/diskio.inc @@ -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 diff --git a/newLang/src/common/source.inc b/newLang/src/common/source.inc new file mode 100644 index 0000000..f8ff6ac --- /dev/null +++ b/newLang/src/common/source.inc @@ -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 diff --git a/newLang/src/compiler/diskio.s b/newLang/src/compiler/diskio.s new file mode 100644 index 0000000..0a418c8 --- /dev/null +++ b/newLang/src/compiler/diskio.s @@ -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 diff --git a/newLang/src/compiler/mmu.s b/newLang/src/compiler/mmu.s index db845f7..ede2cca 100644 --- a/newLang/src/compiler/mmu.s +++ b/newLang/src/compiler/mmu.s @@ -28,13 +28,14 @@ .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_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_p1l, mmu_sys_p1h, mmu_sys_nmi, mmu_comp_rcr .segment "ZEROPAGE" mmu_ptr: .res 2 ; bank-1 access pointer (private page) +mmu_src: .res 2 ; common-RAM source for mmu_copy_to1 ; ------------------------------------------------------------------ CODE .segment "CODE" @@ -229,6 +230,19 @@ mmu_poke1: sta (mmu_ptr),y 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 ; behaves exactly like kcall. kcall_inline: diff --git a/newLang/src/compiler/source.s b/newLang/src/compiler/source.s new file mode 100644 index 0000000..482a75d --- /dev/null +++ b/newLang/src/compiler/source.s @@ -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+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+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: diff --git a/newLang/tests/platform/map-skeleton.s b/newLang/tests/platform/map-skeleton.s index 4782aac..bd27ae5 100644 --- a/newLang/tests/platform/map-skeleton.s +++ b/newLang/tests/platform/map-skeleton.s @@ -70,7 +70,7 @@ swstack_last: .res 1 .segment "TABLES" tables_first: .res 1 - .res $A000 - 2 + .res $5500 - 2 tables_last: .res 1 .ifdef OVERFLOW_TABLES .res 1 diff --git a/newLang/tests/source/source-test.s b/newLang/tests/source/source-test.s new file mode 100644 index 0000000..49f5604 --- /dev/null +++ b/newLang/tests/source/source-test.s @@ -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+1 + 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+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