Selaa lähdekoodia

newLang: add the PETSCII source reader (E02-S01)

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
Daniel Covington 6 päivää sitten
vanhempi
commit
5f16994c60
16 muutettua tiedostoa jossa 1434 lisäystä ja 20 poistoa
  1. +7
    -3
      newLang/cfg/compiler.cfg
  2. +6
    -4
      newLang/docs/architecture.md
  3. +3
    -2
      newLang/docs/decisions.md
  4. +8
    -6
      newLang/docs/memory-map.md
  5. +90
    -0
      newLang/planning/claims/E02-S01.md
  6. +2
    -2
      newLang/scripts/build-map-skeleton.ps1
  7. +117
    -0
      newLang/scripts/build-source-test.ps1
  8. +233
    -0
      newLang/scripts/lib/Nova128Test.psm1
  9. +64
    -0
      newLang/scripts/verify-source-test.ps1
  10. +12
    -0
      newLang/src/common/diskio.inc
  11. +20
    -0
      newLang/src/common/source.inc
  12. +168
    -0
      newLang/src/compiler/diskio.s
  13. +16
    -2
      newLang/src/compiler/mmu.s
  14. +341
    -0
      newLang/src/compiler/source.s
  15. +1
    -1
      newLang/tests/platform/map-skeleton.s
  16. +346
    -0
      newLang/tests/source/source-test.s

+ 7
- 3
newLang/cfg/compiler.cfg Näytä tiedosto

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



+ 6
- 4
newLang/docs/architecture.md Näytä tiedosto

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


+ 3
- 2
newLang/docs/decisions.md Näytä tiedosto

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


+ 8
- 6
newLang/docs/memory-map.md Näytä tiedosto

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


+ 90
- 0
newLang/planning/claims/E02-S01.md Näytä tiedosto

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

+ 2
- 2
newLang/scripts/build-map-skeleton.ps1 Näytä tiedosto

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


+ 117
- 0
newLang/scripts/build-source-test.ps1 Näytä tiedosto

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

+ 233
- 0
newLang/scripts/lib/Nova128Test.psm1 Näytä tiedosto

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

+ 64
- 0
newLang/scripts/verify-source-test.ps1 Näytä tiedosto

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

+ 12
- 0
newLang/src/common/diskio.inc Näytä tiedosto

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

+ 20
- 0
newLang/src/common/source.inc Näytä tiedosto

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

+ 168
- 0
newLang/src/compiler/diskio.s Näytä tiedosto

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

+ 16
- 2
newLang/src/compiler/mmu.s Näytä tiedosto

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


+ 341
- 0
newLang/src/compiler/source.s Näytä tiedosto

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

+ 1
- 1
newLang/tests/platform/map-skeleton.s Näytä tiedosto

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


+ 346
- 0
newLang/tests/source/source-test.s Näytä tiedosto

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

Loading…
Peruuta
Tallenna

Powered by TurnKey Linux.