diff --git a/newLang/docs/architecture.md b/newLang/docs/architecture.md index 1eaf7ef..7e9f220 100644 --- a/newLang/docs/architecture.md +++ b/newLang/docs/architecture.md @@ -8,8 +8,9 @@ version: 0.1-draft # Nova128 Compiler and Runtime Architecture Status: approved version 1 baseline. The compiler memory map is frozen by -E01-S05 (section 4.4, `docs/memory-map.md`). Generated-program addresses are -still deliberately absent until the E04 stories validate them. +E01-S05 (section 4.4, `docs/memory-map.md`). E04-S01 defines the generated +PRG/MAP container and low-memory startup; the higher generated-program map +(banking, heap, software stack) remains open until E04-S02. ## 1. System Boundary @@ -47,7 +48,10 @@ C64 mode or the Z80. | `NAME.MAP` | Symbols, addresses, class layouts, interface slots, and section sizes | Output replacement must be explicit. The compiler must not silently overwrite -an existing PRG after a failed compilation. +an existing PRG after a failed compilation. E04-S01 therefore refuses an +existing `NAME.PRG` or `NAME.MAP` with E202; the user erases the old output +before rebuilding. Files created by the current run are scratched on abort or +failure. ## 3. Repository Module Plan @@ -162,6 +166,26 @@ speed bit is cleared before returning to BASIC, and any uncertain mode falls back to 1 MHz. Direct VDC screen memory is display-only and is not compiler semantic storage. See `docs/platform-display-clock.md` for evidence. +### 2.3 Generated program container (E04-S01) + +The generated PRG begins with load address `$1C01`, the 12-byte BASIC 7 stub +`10 SYS 7181`, and machine entry `$1C0D`. Sections are emitted in ascending +address order; a gap before a section is filled with zero bytes. The entry +point and each section's start/size are recorded in `NAME.MAP` as +CR-terminated text: + +```text +NOVA128 MAP 1 +ENTRY 1C0D +SECTION STUB 1C01 000C +SECTION START 1C0D 004A +``` + +`src/runtime/startup.s` supplies the fixed stub and startup bytes. The startup +initializes the test mailbox, reports producer `$03` and PASS, and returns to +BASIC. The test fixture keeps its sections below `$4000`, where BASIC's mapping +still exposes RAM; banking, heap, and software-stack placement are E04-S02. + ## 5. Compiler Passes The compiler reparses a buffered/streamed source rather than retaining a large @@ -277,6 +301,14 @@ memory. ## 7. Generated Program ABI +### 7.0 Container and entry (E04-S01) + +- Load address `$1C01`; BASIC 7 stub `10 SYS 7181`; entry `$1C0D`. +- Sections are contiguous and ascending; gaps are zero-filled. +- `NAME.MAP` records `ENTRY` and every `SECTION name start size` in hex. +- The startup writes the mailbox with producer `$03`, then PASS, and returns + to BASIC. It does not yet install a runtime mapping or frames. + ### 7.1 Scalar representation - Multi-byte values are little-endian. diff --git a/newLang/docs/decisions.md b/newLang/docs/decisions.md index 5bce510..c13d5d3 100644 --- a/newLang/docs/decisions.md +++ b/newLang/docs/decisions.md @@ -54,6 +54,7 @@ documented default. `Proposed` means a later change still needs approval. | D125 | An array index may be an integer or a value assignable to word; a constant index is also checked against the declared length. | Spec 3.3 gives arrays but not the index type. Byte indexes widen; an out-of-range constant is E089 at compile time. Implemented by `check_index` in `parser.s`. | | D126 | Arrays are fields or locals only: never parameters, results, or whole values. A bare array is E088, and a parameter or result declaring one is E094. | Keeps array storage and copying out of the ABI (D108); indexing is the only way to read or write one. Implemented by `no_array_sig` and `ty_assign`. | | D127 | `return` with a value in a program is E151. A method that declares a result must end every path in `return`; an `if` satisfies this only when both branches do, a `while` never does, and a missing return is reported at `end` as E150. | Spec 7 permits `return` but does not say what a program return means; a program has no caller or result. Deterministic and observable, and lets E04 emit a single exit path. Implemented by `close_block` and `set_ret`. | +| D128 | The generated PRG container loads at `$1C01` with the same 12-byte BASIC 7 stub as the compiler and enters at `$1C0D`. Sections follow in ascending address order; gaps are zero-filled. `NAME.MAP` is CR-terminated text: `NOVA128 MAP 1`, `ENTRY hhhh`, then `SECTION name start size` in hex. Existing `NAME.PRG`/`NAME.MAP` is refused with E202 instead of overwritten; outputs created by the run are scratched on abort or failure. The higher generated-program map (banking, heap, software stack) remains E04-S02. | Gives E04-S01 a deterministic, testable container while preserving a previously successful build. Refusing an existing output is explicit; the user must erase it before recompiling. Implemented by `output.s`, `startup.s`, and `formats.inc`. | ## Deferred diff --git a/newLang/docs/diagnostics.md b/newLang/docs/diagnostics.md index 7535bcc..7fba465 100644 --- a/newLang/docs/diagnostics.md +++ b/newLang/docs/diagnostics.md @@ -117,6 +117,7 @@ After 20 errors the next one is replaced by E221 and compilation stops. |---|---|---| | E200 | `DSK_SOURCE_UNREADABLE` | CANNOT READ SOURCE; DOS % | | E201 | `DSK_ERR_FILE_UNWRITABLE` | CANNOT WRITE ERROR FILE; DOS % | +| E202 | `DSK_OUTPUT_EXISTS` | OUTPUT FILE ALREADY EXISTS | ## Resource limits (E220-E239) diff --git a/newLang/docs/memory-map.md b/newLang/docs/memory-map.md index 0a0555e..d49fc3f 100644 --- a/newLang/docs/memory-map.md +++ b/newLang/docs/memory-map.md @@ -5,8 +5,24 @@ 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 -and E04-S02, which must reuse the test mailbox below. +E04-S01 defines the generated PRG/MAP container below; the higher +generated-program map (banking, heap, software stack) still belongs to E04-S02. + +## Generated Program Container (E04-S01) + +| Element | Value | +|---|---| +| Load address | `$1C01` | +| BASIC 7 stub | 12 bytes, `10 SYS 7181` | +| Entry | `$1C0D` | +| Sections | ascending addresses, zero-filled gaps | +| MAP | `NOVA128 MAP 1`, `ENTRY hhhh`, `SECTION name start size` | +| Startup | clears mailbox, producer `$03`, status PASS, then `RTS` | + +The fixed startup is copied from `src/runtime/startup.s`; `output.s` refuses +existing `NAME.PRG`/`NAME.MAP` and scratches outputs it created on failure. +Test sections stay below `$4000` until E04-S02 freezes the runtime mapping. +The generated program still uses the test mailbox below. ## Mappings diff --git a/newLang/planning/claims/E04-S01.md b/newLang/planning/claims/E04-S01.md new file mode 100644 index 0000000..496db6d --- /dev/null +++ b/newLang/planning/claims/E04-S01.md @@ -0,0 +1,117 @@ +--- +story: E04-S01 +owner: e04_output +status: done +started: 2026-11-09 +depends-on: [E01-S05, E02-S04] +--- + +# E04-S01 Claim + +## Intended change + +Add the generated-program container and writer. `src/compiler/output.s` accepts +a source base name, writes a PRG with a valid load-address header and ordered +sections, records each section's start/size plus the entry point in a MAP file, +and emits the fixed BASIC 7 stub and startup path from +`src/runtime/startup.s`. The generated program initializes the test mailbox, +reports pass, and returns cleanly to BASIC. + +An existing `NAME.PRG` or `NAME.MAP` is refused with the new disk diagnostic +E202 instead of being overwritten. Outputs created by the current run are +scratched if emission fails or the caller aborts, so no partial final file is +left behind. + +## File area + +- `src/compiler/output.s` (new), `src/runtime/startup.s` (new) +- `src/common/formats.inc` (new), `src/common/diagcat.inc`, `docs/diagnostics.md` +- `tests/codegen/output-test.s` +- `scripts/build-output-test.ps1`, `scripts/verify-output-test.ps1` +- `docs/decisions.md`, `docs/architecture.md`, `docs/memory-map.md` +- `planning/claims/E04-S01.md` +- generated files under `build/` + +## Acceptance evidence + +- [x] Writes a valid load-address header and deterministic sections: the + generated `generate.prg` is 133 bytes, starts with `01 1C`, contains the + verified 12-byte `10 SYS 7181` stub at `$1C01`, 74 startup bytes at `$1C0D`, + zero padding through `$1C7F`, and `DE AD BE EF` at `$1C80`. +- [x] Emits a runnable startup path and clean termination: the extracted PRG + autostarts in x128, writes producer `$03`, status `$A5`, code 0, empty data, + and returns to BASIC. +- [x] Refuses/handles output-name collision according to the approved policy: + the pre-existing `collide.prg`/`collide.map` fixtures cause + `OUT_ERR_EXISTS`; both files remain byte-for-byte unchanged because nothing + is opened before the check. +- [x] Removes or marks partial output after any failure: the `order.prg` + section-order failure and the `abort.prg` partial-section run both call + `out_abort`; neither PRG nor MAP exists afterwards. +- [x] MAP records section start, size, and entry point: `generate.map` is the + exact CR-terminated sequence `NOVA128 MAP 1`, `ENTRY 1C0D`, + `SECTION STUB 1C01 000C`, `SECTION START 1C0D 004A`, + `SECTION PAD 1C80 0004`. + +## Coordination notes + +- E01-S05 and E02-S04 are `done`; no other claim overlapped this area. +- D128 records the provisional container and refusal policy. E04-S02 still + owns the higher generated-program map (banking, heap, software stack); + this story's fixture keeps sections below `$4000` where BASIC's mapping + exposes RAM. +- `output.s` returns `OUT_ERR_EXISTS`; E202 is in the stable catalog for the + compiler shell (a later story) to report. No shell integration was invented + here. +- Production and abort paths use the existing `diskio.s` wrappers; no module + except `mmu.s` touches the MMU. + +## Work log + +- Built the writer around a compact section-record table (name pointer, length, + start, size), a load-address header, and a MAP written after the PRG closes. +- First build failed because `out_rec_ptr` was in BSS; 6502 `(ptr),y` requires + zero page. Moved it to `ZEROPAGE`. +- Long branches in `out_begin` and `out_write_map` needed short-branch/`jmp` + trampolines. +- First x128 result was `OUT_ERR_ORDER`: the load-address header had advanced + `out_current` to `$1C03`, so the `$1C01` STUB section looked backward. The + writer now rewinds to `out_load` after the header. +- The next fixture exposed every PRG byte as `01`: `out_byte` loaded the section + flag into A before calling `out_put`. It now saves the byte first. +- Collision fixtures originally used PETSCII uppercase names while the writer + preserved the source's PETSCII lowercase names, so the writer legitimately + did not see them. Fixtures and host reads now use the same lowercase names. +- The abort fixture initially emitted a byte after `out_startup` had ended its + section, producing `OUT_ERR_STATE`; it now opens an explicit `X` section and + writes `$AA` before aborting. +- A stale parser-test x128 held port 6530 and caused empty/dropped monitor + replies. Verification identifies test-image processes and stops only those + before running. + +## Completion + +- Tests run: + - `scripts/build-output-test.ps1` + - `scripts/verify-output-test.ps1 -Port 6530 -RunPort 6531` + - `scripts/build-diag-test.ps1 -UpdateDocs` + - `scripts/verify-diag-test.ps1 -Port 6530` + - `scripts/verify-parser-test.ps1 -Port 6530` + - `git diff --check` +- Results: writer fixture PASS with results finish=0, collision=1, order=2, + abort=0; PRG and MAP bytes match; generated PRG autostarts with producer 3 + and PASS; collision fixtures unchanged; abort/order outputs absent. + Diagnostics: catalog 71 entries, docs in sync, 19 screen rows and ERR files + exact. Parser: all 41 fixtures pass, lowest expression SP `$A7`. +- Files changed: `src/compiler/output.s`, `src/runtime/startup.s`, + `src/common/formats.inc`, `src/common/diagcat.inc`, `docs/diagnostics.md`, + `tests/codegen/output-test.s`, `scripts/build-output-test.ps1`, + `scripts/verify-output-test.ps1`, `docs/decisions.md`, + `docs/architecture.md`, `docs/memory-map.md`, this claim. +- Remaining risks: an existing output must be erased before recompiling + (refusal policy, D128); a power loss during direct PRG writing can leave a + partial file because there is no atomic rename; at most 8 sections and + 12-character section names; the MAP has no symbols/class/interface entries + yet; the startup is only the mailbox/termination path and does not install + a runtime mapping; sections above `$3FFF` are E04-S02 work; `out_load` is + caller-controlled rather than validated against the fixed container. diff --git a/newLang/scripts/build-output-test.ps1 b/newLang/scripts/build-output-test.ps1 new file mode 100644 index 0000000..8f9b35a --- /dev/null +++ b/newLang/scripts/build-output-test.ps1 @@ -0,0 +1,29 @@ +# E04-S01: build the PRG/MAP writer test and a disk with collision fixtures. +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force + +$out = Join-Path $project "build\output-test" +$link = Invoke-Nova128Link -Name "output-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\output.s"), + (Join-Path $project "src\runtime\startup.s"), + (Join-Path $project "tests\codegen\output-test.s")) + +$oldPrg = Join-Path $out "old-collide.prg" +$oldMap = Join-Path $out "old-collide.map" +[IO.File]::WriteAllBytes($oldPrg, [byte[]](0x4B, 0x45, 0x45, 0x50)) # "KEEP" +[IO.File]::WriteAllBytes($oldMap, [byte[]](0x4B, 0x45, 0x45, 0x50, 0x4D, 0x41, 0x50)) # "KEEPMAP" + +New-Nova128Disk -Path (Join-Path $out "output-test.d81") -Files @( + @{ Name = "OUTPUTTEST"; Path = $link.Program; Type = "prg" }, + @{ Name = "collide.prg"; Path = $oldPrg; Type = "prg" }, + @{ Name = "collide.map"; Path = $oldMap; Type = "seq" }) + +Write-Host ("built {0}" -f $link.Program) +Write-Host ("disk {0}" -f (Join-Path $out "output-test.d81")) diff --git a/newLang/scripts/verify-output-test.ps1 b/newLang/scripts/verify-output-test.ps1 new file mode 100644 index 0000000..c023645 --- /dev/null +++ b/newLang/scripts/verify-output-test.ps1 @@ -0,0 +1,124 @@ +# E04-S01: run the PRG/MAP writer test in x128, inspect the disk artifacts, +# and autostart the generated PRG to prove the startup path and termination. +[CmdletBinding()] +param( + [int]$Port = 6530, + [int]$RunPort = 6531, + [int]$TimeoutSeconds = 90 +) + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +& (Join-Path $PSScriptRoot "build-output-test.ps1") + +$out = Join-Path $project "build\output-test" +$disk = Join-Path $out "output-test.d81" +$c1541 = Get-ViceTool "c1541" + +$session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port ` + -Screenshot (Join-Path $out "output-test-vicii.png") +try { + $mb = Wait-Nova128Mailbox $session -TimeoutSeconds $TimeoutSeconds +} finally { + Stop-Nova128X128 $session +} +Write-Host ('mailbox: ' + (($mb | ForEach-Object { $_.ToString('X2') }) -join ' ')) +Write-Host ('results: finish={0} collision={1} order={2} abort={3}' -f $mb[8], $mb[9], $mb[10], $mb[11]) + +$failures = @() +if ($mb[2] -ne 0x01) { $failures += "producer $($mb[2]) is not the platform test" } +if ($mb[3] -ne 0xA5) { $failures += "fixture status is not PASS" } +$want = @(0, 1, 2, 0) +for ($i = 0; $i -lt $want.Count; $i++) { + if ($mb[8 + $i] -ne $want[$i]) { + $failures += "result $i is $($mb[8 + $i]), expected $($want[$i])" + } +} +if ($mb[4] -ne 0 -or $mb[5] -ne 0) { + $failures += "code is $($mb[4] + 256 * $mb[5])" +} + +function Read-DiskFile([string]$Name, [string]$Dest, [bool]$Seq) { + if (Test-Path -LiteralPath $Dest) { Remove-Item -LiteralPath $Dest -Force } + $spec = if ($Seq) { "$Name,s" } else { $Name } + & $c1541 -attach $disk -read $spec $Dest 2>&1 | Out-Null + if (-not (Test-Path -LiteralPath $Dest)) { throw "could not read $Name from the test disk" } +} + +$prgPath = Join-Path $out "generate.prg" +$mapPath = Join-Path $out "generate.map" +$collidePrg = Join-Path $out "collide-check.prg" +$collideMap = Join-Path $out "collide-check.map" +Read-DiskFile "generate.prg" $prgPath $false +Read-DiskFile "generate.map" $mapPath $true +Read-DiskFile "collide.prg" $collidePrg $false +Read-DiskFile "collide.map" $collideMap $true + +$bytes = [IO.File]::ReadAllBytes($prgPath) +if ($bytes.Length -ne 133) { $failures += "GENERATE.PRG is $($bytes.Length) bytes, expected 133" } +if ($bytes[0] -ne 0x01 -or $bytes[1] -ne 0x1C) { + $failures += ("load address is ${0:X2}{1:X2}, expected 011C" -f $bytes[1], $bytes[0]) +} +$stub = [byte[]](0x0B, 0x1C, 0x0A, 0x00, 0x9E, 0x37, 0x31, 0x38, 0x31, 0x00, 0x00, 0x00) +for ($i = 0; $i -lt $stub.Length; $i++) { + if ($bytes[2 + $i] -ne $stub[$i]) { + $failures += ("BASIC stub byte {0} is {1:X2}, expected {2:X2}" -f $i, $bytes[2 + $i], $stub[$i]) + } +} +$padOffset = 2 + (0x1C80 - 0x1C01) +$pad = @(0xDE, 0xAD, 0xBE, 0xEF) +for ($i = 0; $i -lt $pad.Count; $i++) { + if ($bytes[$padOffset + $i] -ne $pad[$i]) { + $failures += ("PAD byte {0} is {1:X2}, expected {2:X2}" -f $i, $bytes[$padOffset + $i], $pad[$i]) + } +} +for ($i = 2 + 12 + 80; $i -lt $padOffset; $i++) { + if ($bytes[$i] -ne 0) { $failures += "gap byte at $i is not zero"; break } +} + +$mapLines = ([IO.File]::ReadAllText($mapPath)).TrimEnd("`r", "`n") -split "`r`n|`r|`n" +$expectedMap = @( + "NOVA128 MAP 1", + "ENTRY 1C0D", + "SECTION STUB 1C01 000C", + "SECTION START 1C0D 004A", + "SECTION PAD 1C80 0004") +if (($mapLines -join "`n") -ne ($expectedMap -join "`n")) { + $failures += ("GENERATE.MAP differs:`n got: " + ($mapLines -join "`n ") + + "`n want: " + ($expectedMap -join "`n ")) +} + +$keep = [Text.Encoding]::ASCII.GetString([IO.File]::ReadAllBytes($collidePrg)) +if ($keep -cne "KEEP") { $failures += "COLLIDE.PRG changed to '$keep'" } +$keepMap = [Text.Encoding]::ASCII.GetString([IO.File]::ReadAllBytes($collideMap)).TrimEnd("`r", "`n") +if ($keepMap -cne "KEEPMAP") { $failures += "COLLIDE.MAP changed to '$keepMap'" } + +$listing = (& $c1541 -attach $disk -list 2>&1) -join "`n" +foreach ($absent in @("abort.prg", "abort.map", "order.prg", "order.map")) { + if ($listing -match ('"' + [regex]::Escape($absent) + '"')) { + $failures += "$absent exists after abort/failure" + } +} + +if ($failures) { throw ("output verification failed:`n " + ($failures -join "`n ")) } + +# ---------------------------------------------------------- generated run +$run = Start-Nova128X128 -Autostart $prgPath -Port $RunPort +try { + $gmb = Wait-Nova128Mailbox $run -TimeoutSeconds $TimeoutSeconds +} finally { + Stop-Nova128X128 $run +} +if ($gmb[2] -ne 0x03) { throw "generated producer is $($gmb[2]), expected 3" } +if ($gmb[3] -ne 0xA5) { throw "generated program did not report PASS" } +if ($gmb[4] -ne 0 -or $gmb[5] -ne 0) { throw "generated code is $($gmb[4] + 256 * $gmb[5])" } +for ($i = 8; $i -lt 16; $i++) { + if ($gmb[$i] -ne 0) { throw "generated data byte $i is not zero" } +} + +Write-Host "verified PRG header, BASIC stub, PAD section, and MAP:" +$expectedMap | ForEach-Object { Write-Host " |$_" } +Write-Host "COLLIDE.PRG/MAP unchanged; ABORT/ORDER left no output" +Write-Host "generated PRG autostarted: producer 3, PASS, code 0" +Write-Host "PRG bytes: $($bytes.Length); screenshots not requested" diff --git a/newLang/src/common/diagcat.inc b/newLang/src/common/diagcat.inc index a1e940c..a38e4da 100644 --- a/newLang/src/common/diagcat.inc +++ b/newLang/src/common/diagcat.inc @@ -94,6 +94,7 @@ ; ------------------------------------------------------------------- disk DIAGNOSTIC DSK_SOURCE_UNREADABLE, 200, "CANNOT READ SOURCE; DOS %" DIAGNOSTIC DSK_ERR_FILE_UNWRITABLE, 201, "CANNOT WRITE ERROR FILE; DOS %" + DIAGNOSTIC DSK_OUTPUT_EXISTS, 202, "OUTPUT FILE ALREADY EXISTS" ; ------------------------------------------------------- resource limits DIAGNOSTIC RES_TOO_MANY_NAMES, 220, "MORE THAN 255 NAMES" diff --git a/newLang/src/common/formats.inc b/newLang/src/common/formats.inc new file mode 100644 index 0000000..5d13c90 --- /dev/null +++ b/newLang/src/common/formats.inc @@ -0,0 +1,27 @@ +; formats.inc - generated-program container constants (E04-S01). +; +; The container is deliberately small: a C128 BASIC 7 stub at $1C01, the +; machine entry at $1C0D, and contiguous sections loaded in ascending order. +; The higher generated-program map (banking, heap, software stack) is E04-S02. + +.ifndef FORMATS_INC +FORMATS_INC = 1 + +PRG_LOAD_ADDR = $1C01 +PRG_ENTRY_ADDR = $1C0D +PRG_STUB_SIZE = 12 + +OUT_MAX_SECTIONS = 8 +OUT_SECTION_NAME_MAX = 12 +OUT_RECORD_SIZE = 7 ; name ptr(2), name len(1), start(2), size(2) + +; out_begin/out_finish/out_abort result codes (out_err). +OUT_OK = 0 +OUT_ERR_EXISTS = 1 ; NAME.PRG or NAME.MAP already exists +OUT_ERR_ORDER = 2 ; section starts below the current address +OUT_ERR_SECTIONS = 3 ; more than OUT_MAX_SECTIONS +OUT_ERR_NAME = 4 ; base or section name too long +OUT_ERR_DISK = 5 ; KERNAL or DOS failure +OUT_ERR_STATE = 6 ; byte emitted outside a section + +.endif diff --git a/newLang/src/compiler/output.s b/newLang/src/compiler/output.s new file mode 100644 index 0000000..3aa6556 --- /dev/null +++ b/newLang/src/compiler/output.s @@ -0,0 +1,855 @@ +; output.s - generated PRG and MAP writer (E04-S01). +; +; Container (D128): +; PRG starts with the load address PRG_LOAD_ADDR, then ordered sections. +; A gap before a section is filled with zero bytes. The MAP is PETSCII text: +; +; NOVA128 MAP 1 +; ENTRY hhhh +; SECTION name start size +; +; API (all calls in the bank-0 compiler state): +; out_begin A/X = source name (bank 0), Y = length. Sets out_load and +; out_entry first. Refuses an existing NAME.PRG or NAME.MAP +; with OUT_ERR_EXISTS; no existing output is overwritten. +; out_startup Emits the BASIC 7 stub at $1C01 and the startup code at +; $1C0D from src/runtime/startup.s. +; out_section A/X = section name, Y = length; out_start = start address. +; out_byte A = byte; requires an open section. +; out_bytes A/X = data, Y = count (0-255). +; out_end_section +; out_finish Writes the MAP, closes both files, and returns C=0 on success. +; out_abort Closes and scratches only files created by this run. +; +; Every failure sets out_err and returns C=1; the caller must call out_abort. +; DOS/KERNAL errors are OUT_ERR_DISK; the detailed DOS code is not retained. + +.setcpu "6502" +.include "../common/c128.inc" +.include "../common/diskio.inc" +.include "../common/formats.inc" + +.import dio_open_cmd, dio_open_in, dio_open_out, dio_read_status +.import dio_chkout, dio_putc, dio_clrchn, dio_close, dio_command, dio_dos_code +.import gen_stub, gen_startup, gen_startup_size + +.export out_begin, out_startup, out_section, out_end_section +.export out_byte, out_bytes, out_finish, out_abort +.export out_load, out_entry, out_start, out_err + +.segment "ZEROPAGE" +out_dst: .res 2 +out_srcp: .res 2 +out_rec_ptr: .res 2 + +.segment "CODE" + +; Build one name from out_base plus the suffix at out_srcp (out_suffix_len). +; out_dst is advanced to the end; out_name_len is the total length. +build_name: + ldy #$00 +@base: cpy out_base_len + beq @suffix + lda out_base,y + sta (out_dst),y + iny + bne @base +@suffix: + sty out_tmp + clc + lda out_dst + adc out_tmp + sta out_dst + bcc :+ + inc out_dst+1 +: ldy #$00 +@sl: cpy out_suffix_len + beq @done + lda (out_srcp),y + sta (out_dst),y + iny + bne @sl +@done: lda out_base_len + clc + adc out_suffix_len + sta out_name_len + rts + +.macro MAKE_NAME dst, suffix, slen, lenvar + lda #dst + sta out_dst+1 + lda #suffix + sta out_srcp+1 + lda #slen + sta out_suffix_len + jsr build_name + lda out_name_len + sta lenvar +.endmacro + +; Append out_append_len bytes from out_srcp to out_dst. +append_bytes: + ldy #$00 +@loop: cpy out_append_len + beq @done + lda (out_srcp),y + sta (out_dst),y + iny + bne @loop +@done: tya + clc + adc out_dst + sta out_dst + bcc :+ + inc out_dst+1 +: rts + +; out_rec_ptr = out_sections + out_index * OUT_RECORD_SIZE. +set_rec_ptr: + lda #out_sections + sta out_rec_ptr+1 + ldx out_index + beq @done +@add: clc + lda out_rec_ptr + adc #OUT_RECORD_SIZE + sta out_rec_ptr + bcc :+ + inc out_rec_ptr+1 +: dex + bne @add +@done: rts + +; Build and send "S0:name" on the command channel. +out_scratch: + sta out_scratch_ptr + stx out_scratch_ptr+1 + sty out_scratch_len + lda #out_cmd + sta out_dst+1 + lda #scratch_prefix + sta out_srcp+1 + lda #3 + sta out_append_len + jsr append_bytes + lda out_scratch_ptr + sta out_srcp + lda out_scratch_ptr+1 + sta out_srcp+1 + lda out_scratch_len + sta out_append_len + jsr append_bytes + lda out_scratch_len + clc + adc #3 + sta out_cmd_len + lda #out_cmd + ldy out_cmd_len + jsr dio_command + bcs @disk + lda dio_dos_code + cmp #20 + bcc @ok + cmp #62 ; already absent is harmless + beq @ok +@disk: lda #OUT_ERR_DISK + sta out_err + sec + rts +@ok: clc + rts + +; Test A/X name, length Y. C=1 if it exists (out_err=0) or the drive failed +; (out_err=OUT_ERR_DISK); C=0 if it does not exist. +out_exists: + jsr dio_open_in + bcs @disk + jsr dio_read_status + bcs @disk + lda dio_dos_code + cmp #62 + beq @absent + cmp #20 + bcs @disk + lda #DIO_LFN_IN + jsr dio_close + lda #OUT_OK + sta out_err + sec + rts +@absent: + lda #DIO_LFN_IN + jsr dio_close + lda #OUT_OK + sta out_err + clc + rts +@disk: + lda #DIO_LFN_IN + jsr dio_close + lda #OUT_ERR_DISK + sta out_err + sec + rts + +out_begin: + sta out_srcp + stx out_srcp+1 + sty out_name_len + + lda #$00 + sta out_count + sta out_in_section + sta out_prg_created + sta out_map_created + sta out_cmd_open + sta out_prg_is_open + sta out_map_is_open + sta out_err + lda out_load + sta out_current + lda out_load+1 + sta out_current+1 + + lda out_name_len + sta out_base_len + cmp #4 + bcc @base_ready + ldy out_name_len + dey + lda (out_srcp),y + cmp #'8' + bne @base_ready + dey + lda (out_srcp),y + and #$7F + cmp #'N' + bne @base_ready + dey + lda (out_srcp),y + cmp #'.' + bne @base_ready + lda out_name_len + sec + sbc #3 + sta out_base_len +@base_ready: + lda out_base_len + bne @have_base + jmp @bad_name +@have_base: + cmp #13 + bcc @base_ok + jmp @bad_name +@base_ok: + ldy #$00 +@copy: cpy out_base_len + beq @copied + lda (out_srcp),y + sta out_base,y + iny + bne @copy +@copied: + MAKE_NAME out_prg_name, sfx_prg, 4, out_prg_len + MAKE_NAME out_map_name, sfx_map, 4, out_map_len + MAKE_NAME out_prg_open, sfx_prg_open, 8, out_prg_open_len + MAKE_NAME out_map_open, sfx_map_open, 8, out_map_open_len + MAKE_NAME out_prg_read, sfx_prg_read, 8, out_prg_read_len + MAKE_NAME out_map_read, sfx_map_read, 8, out_map_read_len + + jsr dio_open_cmd + bcc :+ + jmp @disk_fail +: + lda #1 + sta out_cmd_open + + lda #out_prg_read + ldy out_prg_read_len + jsr out_exists + bcc @check_map + lda out_err + bne @close_fail + jmp @exists +@check_map: + lda #out_map_read + ldy out_map_read_len + jsr out_exists + bcc @open_prg + lda out_err + bne @close_fail + jmp @exists + +@exists: + lda #OUT_ERR_EXISTS + sta out_err + jmp @close_fail + +@open_prg: + lda #1 + sta out_prg_is_open + sta out_prg_created + lda #out_prg_open + ldy out_prg_open_len + jsr dio_open_out + bcs @fail_created + jsr dio_read_status + bcs @fail_created + lda dio_dos_code + cmp #20 + bcs @fail_created + ldx #DIO_LFN_ERR + jsr dio_chkout + bcs @fail_created + lda out_load + jsr out_put + bcs @fail_created + lda out_load+1 + jsr out_put + bcs @fail_created + lda out_load ; the header is not part of section 0 + sta out_current + lda out_load+1 + sta out_current+1 + clc + rts + +@bad_name: + lda #OUT_ERR_NAME + sta out_err + sec + rts +@disk_fail: + lda #OUT_ERR_DISK + sta out_err + sec + rts +@close_fail: + jsr out_close_cmd + sec + rts +@fail_created: + lda #OUT_ERR_DISK + sta out_err + jsr out_cleanup + sec + rts + +out_startup: + lda #PRG_LOAD_ADDR + sta out_start+1 + lda #stub_name + ldy #4 + jsr out_section + bcs @out + lda #gen_stub + ldy #PRG_STUB_SIZE + jsr out_bytes + bcs @out + jsr out_end_section + + lda #PRG_ENTRY_ADDR + sta out_start+1 + lda #start_name + ldy #5 + jsr out_section + bcs @out + lda #gen_startup + ldy #out_map_open + ldy out_map_open_len + jsr dio_open_out + bcs @fail + jsr dio_read_status + bcs @fail + lda dio_dos_code + cmp #20 + bcs @fail + ldx #DIO_LFN_ERR + jsr dio_chkout + bcs @fail + jsr out_write_map + bcs @fail + jsr out_close_map + jsr out_close_cmd + lda #OUT_OK + sta out_err + lda #$00 + sta out_prg_created + sta out_map_created + clc + rts +@fail: + lda out_err + bne :+ + lda #OUT_ERR_DISK + sta out_err +: jsr out_cleanup + sec + rts + +out_abort: + lda #OUT_OK + sta out_err + jsr out_cleanup + lda out_err + beq @ok + sec + rts +@ok: clc + rts + +; Close created outputs and scratch only files this run created. +out_cleanup: + jsr out_close_prg + jsr out_close_map + lda out_cmd_open + bne @have_cmd + jsr dio_open_cmd + bcs @done + lda #1 + sta out_cmd_open +@have_cmd: + lda out_prg_created + beq @map + lda #out_prg_name + ldy out_prg_len + jsr out_scratch +@map: lda out_map_created + beq @close + lda #out_map_name + ldy out_map_len + jsr out_scratch +@close: + jsr out_close_cmd +@done: + lda #$00 + sta out_prg_created + sta out_map_created + rts + +out_close_prg: + lda out_prg_is_open + beq @ok + jsr dio_clrchn + lda #DIO_LFN_ERR + jsr dio_close + lda #$00 + sta out_prg_is_open +@ok: clc + rts + +out_close_map: + lda out_map_is_open + beq @ok + jsr dio_clrchn + lda #DIO_LFN_ERR + jsr dio_close + lda #$00 + sta out_map_is_open +@ok: clc + rts + +out_close_cmd: + lda out_cmd_open + beq @ok + lda #DIO_LFN_CMD + jsr dio_close + lda #$00 + sta out_cmd_open +@ok: clc + rts + +; ---------------------------------------------------------------- MAP text +out_write_map: + lda #map_header + ldy #map_header_len + jsr out_text + bcc :+ + jmp @out +: + lda #map_entry + ldy #map_entry_len + jsr out_text + bcc :+ + jmp @out +: + lda out_entry + sta out_num + lda out_entry+1 + sta out_num+1 + jsr out_hex4 + bcc :+ + jmp @out +: + jsr out_cr + bcc :+ + jmp @out +: + lda #$00 + sta out_index +@section: + lda out_index + cmp out_count + beq @done + jsr set_rec_ptr + lda #section_prefix + ldy #section_prefix_len + jsr out_text + bcs @out + ldy #0 + lda (out_rec_ptr),y + sta out_srcp + iny + lda (out_rec_ptr),y + sta out_srcp+1 + iny + lda (out_rec_ptr),y + sta out_text_len + ldy #0 +@name: cpy out_text_len + beq @named + lda (out_srcp),y + sty out_y + jsr out_char + bcs @out + ldy out_y + iny + bne @name +@named: + lda #' ' + jsr out_char + bcs @out + ldy #3 + lda (out_rec_ptr),y + sta out_num + iny + lda (out_rec_ptr),y + sta out_num+1 + jsr out_hex4 + bcs @out + lda #' ' + jsr out_char + bcs @out + ldy #5 + lda (out_rec_ptr),y + sta out_num + iny + lda (out_rec_ptr),y + sta out_num+1 + jsr out_hex4 + bcs @out + jsr out_cr + bcs @out + inc out_index + jmp @section +@done: clc +@out: rts + +; A/X = text, Y = length. +out_text: + sta out_srcp + stx out_srcp+1 + sty out_text_len + ldy #$00 +@loop: cpy out_text_len + beq @done + lda (out_srcp),y + sty out_y + jsr out_char + bcs @out + ldy out_y + iny + bne @loop +@done: clc +@out: rts + +out_cr: + lda #$0D + jmp out_char + +out_char: + sty out_y + jsr dio_putc + bcs @fail + ldy out_y + clc + rts +@fail: + lda #OUT_ERR_DISK + sta out_err + ldy out_y + sec + rts + +out_hex4: + lda out_num+1 + jsr out_hex_byte + bcs @out + lda out_num + jmp out_hex_byte +@out: rts + +out_hex_byte: + pha + lsr a + lsr a + lsr a + lsr a + tax + lda hex_digits,x + jsr out_char + bcs @fail + pla + and #$0F + tax + lda hex_digits,x + jmp out_char +@fail: pla + sec + rts + +.segment "RODATA" +sfx_prg: .byte ".PRG" +sfx_map: .byte ".MAP" +sfx_prg_open: .byte ".PRG,P,W" +sfx_map_open: .byte ".MAP,S,W" +sfx_prg_read: .byte ".PRG,P,R" +sfx_map_read: .byte ".MAP,S,R" +stub_name: .byte "STUB" +start_name: .byte "START" +scratch_prefix: .byte "S0:" +map_header: .byte "NOVA128 MAP 1", $0D +map_header_len = * - map_header +map_entry: .byte "ENTRY " +map_entry_len = * - map_entry +section_prefix: .byte "SECTION " +section_prefix_len = * - section_prefix +hex_digits: .byte "0123456789ABCDEF" + +.segment "BSS" +out_base: .res 12 +out_base_len: .res 1 +out_name_len: .res 1 +out_suffix_len: .res 1 +out_prg_name: .res 16 +out_prg_len: .res 1 +out_map_name: .res 16 +out_map_len: .res 1 +out_prg_open: .res 20 +out_prg_open_len: .res 1 +out_map_open: .res 20 +out_map_open_len: .res 1 +out_prg_read: .res 20 +out_prg_read_len: .res 1 +out_map_read: .res 20 +out_map_read_len: .res 1 +out_cmd: .res 48 +out_cmd_len: .res 1 +out_load: .res 2 +out_entry: .res 2 +out_start: .res 2 +out_current: .res 2 +out_count: .res 1 +out_index: .res 1 +out_in_section: .res 1 +out_prg_created: .res 1 +out_map_created: .res 1 +out_cmd_open: .res 1 +out_prg_is_open: .res 1 +out_map_is_open: .res 1 +out_err: .res 1 +out_sections: .res OUT_MAX_SECTIONS * OUT_RECORD_SIZE +out_tmp: .res 2 +out_tmp_len: .res 1 +out_scratch_ptr: .res 2 +out_scratch_len: .res 1 +out_append_len: .res 1 +out_byte_value: .res 1 +out_text_len: .res 1 +out_y: .res 1 +out_num: .res 2 diff --git a/newLang/src/runtime/startup.s b/newLang/src/runtime/startup.s new file mode 100644 index 0000000..610c2a7 --- /dev/null +++ b/newLang/src/runtime/startup.s @@ -0,0 +1,71 @@ +; startup.s - fixed generated-program entry (E04-S01). +; +; output.s copies these bytes into the generated PRG at PRG_LOAD_ADDR and +; PRG_ENTRY_ADDR. They are position-independent except for the fixed mailbox +; addresses, which are part of the platform contract. The stub is the same +; 12-byte BASIC 7 form used by the compiler itself: +; +; $1C01 0B 1C link to the end marker at $1C0B +; $1C03 0A 00 line 10 +; $1C05 9E SYS +; $1C06 "7181" $1C0D +; $1C0A 00 end of line +; $1C0B 00 00 end of program +; $1C0D startup code +; +; The startup has no caller-visible result and returns to BASIC. Later E04 +; stories replace the body with runtime initialization once frames and the +; generated-program memory map exist. + +.setcpu "6502" +.include "../common/mailbox.inc" +.include "../common/formats.inc" + +.segment "RODATA" + +.export gen_stub, gen_stub_size, gen_startup, gen_startup_size +gen_stub: + .byte $0B, $1C ; next BASIC line at $1C0B + .word 10 + .byte $9E ; SYS + .byte "7181" ; $1C0D + .byte $00 + .word $0000 ; end of BASIC program +gen_stub_end: + +gen_startup: + 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_PROGRAM + sta MB_PRODUCER + lda #MB_ST_RUNNING + sta MB_STATUS + lda #$00 + sta MB_CODE + sta MB_CODE+1 + sta MB_ADDR + sta MB_ADDR+1 + sta MB_DATA + sta MB_DATA+1 + sta MB_DATA+2 + sta MB_DATA+3 + sta MB_DATA+4 + sta MB_DATA+5 + sta MB_DATA+6 + sta MB_DATA+7 + lda #MB_ST_PASS + sta MB_STATUS + rts +gen_startup_end: + +gen_stub_size = gen_stub_end - gen_stub +gen_startup_size = gen_startup_end - gen_startup +.assert gen_stub_size = PRG_STUB_SIZE, error, "generated BASIC stub size changed" +.assert gen_startup_size <= 255, error, "startup must fit the byte-count API" diff --git a/newLang/tests/codegen/output-test.s b/newLang/tests/codegen/output-test.s new file mode 100644 index 0000000..e4aae3d --- /dev/null +++ b/newLang/tests/codegen/output-test.s @@ -0,0 +1,248 @@ +; E04-S01: PRG/MAP writer test in x128. +; +; The fixture runs in the compiler mapping and writes GENERATE.PRG/MAP with +; the fixed startup, a PAD section at $1C80, then checks: +; +0 out_finish result (0) +; +1 collision result (OUT_ERR_EXISTS) +; +2 section-order result (OUT_ERR_ORDER) +; +3 abort result (0) +; COLLIDE.PRG/MAP already exist on the test disk; the host verifies they are +; unchanged. ABORT and ORDER must leave no output behind. + +.setcpu "6502" +.include "../../src/common/c128.inc" +.include "../../src/common/mailbox.inc" +.include "../../src/common/formats.inc" + +.import mmu_init, mmu_bank0, mmu_shutdown +.import out_begin, out_startup, out_section, out_end_section +.import out_byte, out_bytes, out_finish, out_abort +.import out_load, out_entry, out_start, out_err + +.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 "STARTUP" +.export test_entry +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 + lda #$00 + sta fail_mask + sta MB_DATA+0 + sta MB_DATA+1 + sta MB_DATA+2 + sta MB_DATA+3 + + jsr mmu_init + jsr mmu_bank0 + + lda #PRG_LOAD_ADDR + sta out_load+1 + lda #PRG_ENTRY_ADDR + sta out_entry+1 + + ; ------------------------------------------------ successful output + lda #name_generate + ldy #name_generate_len + jsr out_begin + bcs @gen_fail + jsr out_startup + bcs @gen_fail + lda #$80 + sta out_start + lda #$1C + sta out_start+1 + lda #name_pad + ldy #3 + jsr out_section + bcs @gen_fail + lda #pad_bytes + ldy #4 + jsr out_bytes + bcs @gen_fail + jsr out_end_section + jsr out_finish + bcs @gen_fail + lda out_err + sta MB_DATA+0 + cmp #OUT_OK + beq @gen_done + lda #$01 + jsr fail + jmp @gen_done +@gen_fail: + lda out_err + sta MB_DATA+0 + lda #$01 + jsr fail + jsr out_abort +@gen_done: + + ; ---------------------------------------------------- collision + lda #name_collide + ldy #name_collide_len + jsr out_begin + bcc @coll_unexpected + lda out_err + sta MB_DATA+1 + cmp #OUT_ERR_EXISTS + beq @coll_ok + lda #$02 + jsr fail + jmp @coll_abort +@coll_unexpected: + lda #$02 + jsr fail + jmp @coll_abort +@coll_ok: +@coll_abort: + jsr out_abort +@coll_done: + + ; ------------------------------------------------ section order + lda #name_order + ldy #name_order_len + jsr out_begin + bcs @order_begin_fail + lda #$00 + sta out_start + lda #$1C + sta out_start+1 + lda #name_bad + ldy #3 + jsr out_section + bcc @order_unexpected + lda out_err + sta MB_DATA+2 + cmp #OUT_ERR_ORDER + beq @order_abort + lda #$04 + jsr fail + jmp @order_abort +@order_begin_fail: + lda out_err + sta MB_DATA+2 + lda #$04 + jsr fail + jmp @order_done +@order_unexpected: + lda #$04 + jsr fail + jmp @order_abort +@order_abort: + jsr out_abort +@order_done: + + ; -------------------------------------------------------- abort + lda #name_abort + ldy #name_abort_len + jsr out_begin + bcs @abort_fail + lda #$01 + sta out_start + lda #$1C + sta out_start+1 + lda #name_x + ldy #1 + jsr out_section + bcs @abort_fail + lda #$AA + jsr out_byte + bcs @abort_fail + jsr out_abort + bcs @abort_fail + lda out_err + sta MB_DATA+3 + cmp #OUT_OK + beq @abort_done +@abort_fail: + lda out_err + sta MB_DATA+3 + lda #$08 + jsr fail + jsr out_abort +@abort_done: + + jsr mmu_shutdown + lda fail_mask + sta MB_CODE + lda #$00 + sta MB_CODE+1 + lda #MB_ST_FAIL + ldx fail_mask + bne @status + lda #MB_ST_PASS +@status: + sta MB_STATUS + ldy #$00 +@print: lda message,y + beq @end + sty print_y + jsr CHROUT + ldy print_y + iny + bne @print +@end: rts + +.segment "CODE" +fail: + ora fail_mask + sta fail_mask + rts + +.segment "RODATA" +name_generate: .byte "GENERATE.N8" +name_generate_len = * - name_generate +name_collide: .byte "COLLIDE.N8" +name_collide_len = * - name_collide +name_order: .byte "ORDER.N8" +name_order_len = * - name_order +name_abort: .byte "ABORT.N8" +name_abort_len = * - name_abort +name_pad: .byte "PAD" +name_bad: .byte "BAD" +name_x: .byte "X" +pad_bytes: .byte $DE, $AD, $BE, $EF +message: .byte "NOVA128 OUTPUT TEST DONE", $0D, $00 + +.segment "BSS" +fail_mask: .res 1 +print_y: .res 1