From c7fdf9aa3a71636398a7d22774991a724c7984cb Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Sat, 7 Nov 2026 20:03:44 -0500 Subject: [PATCH] newLang: add the diagnostic catalog and ERR writer (E02-S04) diagcat.inc is the single catalog: one line per diagnostic with a stable number in a per-category range. Included by errors.inc it gives E_* constants; included by diagnostics.s it gives the message table. docs/diagnostics.md is generated from it. The build rejects out-of-range or duplicate numbers and any grammar .expect symbol missing from it. diagnostics.s prints a two-line, 40-column-safe screen form and appends full lines to NAME.ERR. NAME.ERR is scratched at the start and created only when needed. After 20 errors, E221 replaces the next one and the caller must stop. diskio.s gains output-file and DOS-command wrappers. Verified in x128: screen RAM rows, diag.err and limit.err read back from the D81, stale-file scratching, an unwritable ERR name (DOS 33 -> E201 once), and no open files after any phase. A counting mutation is caught. Co-Authored-By: Claude Opus 5.5 --- newLang/docs/architecture.md | 13 +- newLang/docs/diagnostics.md | 94 +++++ newLang/planning/claims/E02-S04.md | 77 ++++ newLang/scripts/build-diag-test.ps1 | 145 +++++++ newLang/scripts/verify-diag-test.ps1 | 85 +++++ newLang/src/common/c128.inc | 1 + newLang/src/common/diagcat.inc | 68 ++++ newLang/src/common/diskio.inc | 2 + newLang/src/common/errors.inc | 16 + newLang/src/compiler/diagnostics.s | 539 +++++++++++++++++++++++++++ newLang/src/compiler/diskio.s | 56 +++ newLang/tests/diag/diag-test.s | 199 ++++++++++ newLang/tests/grammar/README.md | 6 +- 13 files changed, 1297 insertions(+), 4 deletions(-) create mode 100644 newLang/docs/diagnostics.md create mode 100644 newLang/planning/claims/E02-S04.md create mode 100644 newLang/scripts/build-diag-test.ps1 create mode 100644 newLang/scripts/verify-diag-test.ps1 create mode 100644 newLang/src/common/diagcat.inc create mode 100644 newLang/src/common/errors.inc create mode 100644 newLang/src/compiler/diagnostics.s create mode 100644 newLang/tests/diag/diag-test.s diff --git a/newLang/docs/architecture.md b/newLang/docs/architecture.md index f00a3e8..fd9eb83 100644 --- a/newLang/docs/architecture.md +++ b/newLang/docs/architecture.md @@ -344,8 +344,17 @@ fresh image per run. ## 11. Diagnostics and Faults Compile-time diagnostics use stable IDs such as `E014`. Runtime faults use a -separate stable namespace such as `R003`. Numeric assignments live in -`src/common/errors.inc` once story E02-S03 freezes the catalog. +separate stable namespace such as `R003`. The compile-time catalog is +`src/common/diagcat.inc` (one line per diagnostic, numbered by category +range); `errors.inc` turns it into `E_*` constants and `diagnostics.s` into +messages. `docs/diagnostics.md` is generated from it (E02-S04). + +`diagnostics.s` prints a two-line screen form (header, message clipped to 39 +characters) and appends one full line per diagnostic to `NAME.ERR`. The ERR +file is scratched when compilation starts and created only on the first +diagnostic. After 20 errors the next is replaced by E221 and the caller must +stop. If `NAME.ERR` cannot be written, E201 is shown once and reporting +continues on screen. Compiler errors include source name, line, column, and compact message. Runtime faults always include a stable fault code and the best available program address diff --git a/newLang/docs/diagnostics.md b/newLang/docs/diagnostics.md new file mode 100644 index 0000000..452df76 --- /dev/null +++ b/newLang/docs/diagnostics.md @@ -0,0 +1,94 @@ +# Nova128 Diagnostics + +Generated from `src/common/diagcat.inc` by `scripts/build-diag-test.ps1 -UpdateDocs`. +Do not edit by hand. Numbers are stable: never renumber or reuse one. + +Each `%` is replaced by an argument such as a name or type. On screen a +diagnostic is two lines, the header and the message clipped to 39 characters: + +```text +E010 TEST.N8 3 11 +CHARACTER LITERAL NEEDS ONE CHARACTER +``` + +`NAME.ERR` holds one full line per diagnostic: `E010 TEST.N8 3 11 CHARACTER ...`. +It is scratched when compilation starts and created only if a diagnostic occurs. +After 20 errors the next one is replaced by E221 and compilation stops. + +## Source and lexical (E001-E019) + +| Code | Symbol | Message | +|---|---|---| +| E001 | `SRC_LINE_TOO_LONG` | LINE LONGER THAN 80 CHARACTERS | +| E002 | `SRC_FILE_TOO_LARGE` | SOURCE LARGER THAN 18999 BYTES | +| E003 | `SRC_TOO_MANY_LINES` | MORE THAN 599 LINES | +| E004 | `LEX_INVALID_CHAR` | INVALID CHARACTER | +| E005 | `LEX_IDENT_TOO_LONG` | NAME LONGER THAN 16 CHARACTERS | +| E006 | `LEX_NUMBER_RANGE` | NUMBER LARGER THAN 65535 | +| E007 | `LEX_BAD_NUMBER` | MALFORMED NUMBER | +| E008 | `LEX_UNTERMINATED_STRING` | STRING NOT CLOSED ON THIS LINE | +| E009 | `LEX_BAD_ESCAPE` | UNKNOWN ESCAPE SEQUENCE | +| E010 | `LEX_BAD_CHAR_LITERAL` | CHARACTER LITERAL NEEDS ONE CHARACTER | + +## Syntax (E020-E059) + +| Code | Symbol | Message | +|---|---|---| +| E020 | `SYN_EXPECTED_IDENTIFIER` | NAME EXPECTED | +| E021 | `SYN_EXPECTED_EOL` | END OF LINE EXPECTED | +| E022 | `SYN_UNEXPECTED_EOL` | LINE ENDS TOO EARLY | +| E023 | `SYN_EXPECTED_RPAREN` | ')' EXPECTED | +| E024 | `SYN_EXPECTED_END_PROGRAM` | END PROGRAM EXPECTED | +| E025 | `SYN_EXPECTED_END_METHOD` | END METHOD EXPECTED | +| E026 | `SYN_EXPECTED_END_IF` | END IF EXPECTED | +| E027 | `SYN_EXPECTED_END_CLASS` | END CLASS EXPECTED | +| E028 | `SYN_EXPECTED_INTERFACE_NAME` | INTERFACE NAME EXPECTED | +| E029 | `SYN_INTERFACE_METHOD_BODY` | INTERFACE METHODS HAVE NO BODY | +| E030 | `SYN_UNEXPECTED_TOKEN` | % IS NOT ALLOWED HERE | + +## Names and declarations (E060-E079) + +| Code | Symbol | Message | +|---|---|---| +| E060 | `SEM_DUPLICATE_NAME` | % IS ALREADY DECLARED | +| E061 | `SEM_UNKNOWN_NAME` | % IS NOT DECLARED | + +## Types (E080-E119) + +| Code | Symbol | Message | +|---|---|---| +| E080 | `TYP_MISMATCH` | % EXPECTED; % FOUND | +| E081 | `TYP_CONDITION_NOT_BOOLEAN` | CONDITION MUST BE BOOLEAN | + +## Access (E120-E129) + +| Code | Symbol | Message | +|---|---|---| +| E120 | `ACC_PRIVATE_MEMBER` | % IS PRIVATE | + +## Interfaces (E130-E149) + +| Code | Symbol | Message | +|---|---|---| +| E130 | `IFC_METHOD_MISSING` | % IS NOT IMPLEMENTED | +| E131 | `IFC_SIGNATURE_MISMATCH` | % DIFFERS FROM INTERFACE | + +## Methods and returns (E150-E169) + +| Code | Symbol | Message | +|---|---|---| +| E150 | `SEM_MISSING_RETURN` | MISSING RETURN VALUE | + +## Disk (E200-E219) + +| Code | Symbol | Message | +|---|---|---| +| E200 | `DSK_SOURCE_UNREADABLE` | CANNOT READ SOURCE; DOS % | +| E201 | `DSK_ERR_FILE_UNWRITABLE` | CANNOT WRITE ERROR FILE; DOS % | + +## Resource limits (E220-E239) + +| Code | Symbol | Message | +|---|---|---| +| E220 | `RES_TOO_MANY_NAMES` | MORE THAN 255 NAMES | +| E221 | `RES_ERROR_LIMIT` | TOO MANY ERRORS; COMPILATION STOPPED | diff --git a/newLang/planning/claims/E02-S04.md b/newLang/planning/claims/E02-S04.md new file mode 100644 index 0000000..4f4e886 --- /dev/null +++ b/newLang/planning/claims/E02-S04.md @@ -0,0 +1,77 @@ +--- +story: E02-S04 +owner: e02_diagnostics +status: done +started: 2026-11-07 +depends-on: [E01-S03, E00-S02] +--- + +# E02-S04 Claim + +## Intended change + +Add the stable diagnostic catalog (`src/common/diagcat.inc`, one line per +diagnostic, included as constants and as messages), `src/common/errors.inc`, +and `src/compiler/diagnostics.s`, which prints the 40-column screen form and +writes `NAME.ERR`, with a default limit of 20 errors. Extend `diskio.s` with +output-file and DOS-command wrappers. + +## File area + +- `src/common/diagcat.inc`, `src/common/errors.inc`, `src/common/diskio.inc` +- `src/compiler/diagnostics.s`, `src/compiler/diskio.s` +- `tests/diag/` +- `scripts/build-diag-test.ps1`, `scripts/verify-diag-test.ps1`, + `scripts/lib/Nova128Test.psm1` +- `docs/diagnostics.md` (generated from the catalog), `docs/architecture.md` +- `planning/claims/E02-S04.md` +- generated files under `build/` + +## Acceptance evidence + +- [x] Lexical, syntax, type, access, interface, disk, and resource categories + have stable IDs: 33 diagnostics in `src/common/diagcat.inc` across nine + category ranges; the build rejects out-of-range, duplicate, or reused + numbers and any grammar `.expect` symbol missing from the catalog. +- [x] Screen form fits 40 columns for representative diagnostics: x128 screen + RAM shows each diagnostic as a header row and a message row, all at most + 39 characters, including the longest message, a clipped two-argument + message, and the widest header (`E023 DIAG.N8 65535 255`). +- [x] ERR output contains source, line, column, code, and message: + `diag.err` read back from the D81 equals the expected six lines exactly; + a stale `diag.err` was replaced and a stale `clean.err` was scratched when + no diagnostics occurred. +- [x] Error limiting defaults to 20 and terminates cleanly: 25 reports give + carry 0 x20 then 1 x5; `limit.err` holds 20 errors plus E221; no files are + left open after any phase. An unwritable ERR name (DOS 33) shows E201 once + and leaves no file. A mutation that stops counting is caught. + +## Coordination notes + +The grammar fixtures' symbolic `.expect` names become catalog symbols +unchanged. Later stories append diagnostics within their category range. + +## Work log + +- The first x128 run showed garbage before every message: `find_message` + clobbered X, the formatter's buffer index. Fixed; found by dumping screen + RAM and the format buffer. +- `c1541 -read` needs `name,s` for SEQ files. +- The ERR width check uses spec limits (15-character source name, line <= 599, + column <= 81), so the longest ERR line is 77 characters. + +## Completion + +- Tests run: `scripts/verify-diag-test.ps1` (pass plus counting mutation). +- Results: result block, 19 screen rows, `diag.err`, `limit.err`, and the + directory all match expectations. +- Files changed: `src/common/diagcat.inc`, `src/common/errors.inc`, + `src/common/diskio.inc`, `src/common/c128.inc` (CHKOUT), + `src/compiler/diagnostics.s`, `src/compiler/diskio.s`, `tests/diag/`, + `scripts/build-diag-test.ps1`, `scripts/verify-diag-test.ps1`, + `docs/diagnostics.md`, `docs/architecture.md`, + `tests/grammar/README.md`, this claim. +- Remaining risks: later stories will append diagnostics, and their wording + should be reviewed by the user; the 80-column display path was not + exercised here (E01-S04 covers KERNAL output in both modes); a disk that + fills up while NAME.ERR is being written is not tested. diff --git a/newLang/scripts/build-diag-test.ps1 b/newLang/scripts/build-diag-test.ps1 new file mode 100644 index 0000000..4728073 --- /dev/null +++ b/newLang/scripts/build-diag-test.ps1 @@ -0,0 +1,145 @@ +# E02-S04: check the diagnostic catalog, keep docs/diagnostics.md in sync with +# it, build the diagnostics test PRG, and prepare a D81 with stale ERR files. +# -UpdateDocs rewrites docs/diagnostics.md from src/common/diagcat.inc. +[CmdletBinding()] +param([switch]$UpdateDocs) + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +$out = Join-Path $project "build\diag-test" +New-Item -ItemType Directory -Force -Path $out | Out-Null + +# ------------------------------------------------------------ the catalog +$catalogPath = Join-Path $project "src\common\diagcat.inc" +$catalog = @() +foreach ($line in Get-Content -LiteralPath $catalogPath) { + $m = [regex]::Match($line, '^\s*DIAGNOSTIC\s+(\w+),\s*(\d+),\s*"(.*)"\s*$') + if ($m.Success) { + $catalog += [pscustomobject]@{ Symbol = $m.Groups[1].Value; Code = [int]$m.Groups[2].Value + Text = $m.Groups[3].Value } + } +} +$ranges = @{ + SRC = @(, @(1, 19)); LEX = @(, @(1, 19)); SYN = @(, @(20, 59)); SEM = @(@(60, 79), @(150, 169)) + TYP = @(, @(80, 119)); ACC = @(, @(120, 129)); IFC = @(, @(130, 149)); DSK = @(, @(200, 219)) + RES = @(, @(220, 239)) +} +$categories = [ordered]@{ + "Source and lexical" = @(1, 19); "Syntax" = @(20, 59); "Names and declarations" = @(60, 79) + "Types" = @(80, 119); "Access" = @(120, 129); "Interfaces" = @(130, 149) + "Methods and returns" = @(150, 169); "Disk" = @(200, 219); "Resource limits" = @(220, 239) +} +$problems = @() +foreach ($d in $catalog) { + $prefix = $d.Symbol.Split('_')[0] + if (-not $ranges.ContainsKey($prefix)) { $problems += "$($d.Symbol): unknown category prefix" } + elseif (-not ($ranges[$prefix] | Where-Object { $d.Code -ge $_[0] -and $d.Code -le $_[1] })) { + $problems += "$($d.Symbol) = $($d.Code) is outside the $prefix range" + } + if ($d.Code -lt 1 -or $d.Code -gt 255) { $problems += "$($d.Symbol): code must be 1..255" } + $argCount = ([regex]::Matches($d.Text, '%')).Count + if ($argCount -gt 2) { $problems += "$($d.Symbol): more than two arguments" } + if ($argCount -eq 0 -and $d.Text.Length -gt 39) { $problems += "$($d.Symbol): message longer than 39" } + if ($d.Text -cmatch '[a-z]') { $problems += "$($d.Symbol): use uppercase ASCII (PETSCII unshifted)" } + # ERR line <= 80 (Vim's width). Spec limits: name <= 15 ("12345678901 + # 2.N8"), line <= 599, column <= 81, so "Ennn NAME LLL CC " <= 28. + if ((28 + $d.Text.Length - $argCount + 16 * $argCount) -gt 80) { $problems += "$($d.Symbol): ERR line can exceed 80" } +} +$dupes = $catalog | Group-Object Code | Where-Object Count -gt 1 +foreach ($g in $dupes) { $problems += "code $($g.Name) used by $(($g.Group.Symbol) -join ', ')" } +$dupes = $catalog | Group-Object Symbol | Where-Object Count -gt 1 +foreach ($g in $dupes) { $problems += "symbol $($g.Name) declared twice" } +foreach ($e in Get-ChildItem (Join-Path $project "tests\grammar\rejected") -Filter *.expect) { + $sym = [regex]::Match((Get-Content -Raw $e.FullName), '(?m)^diagnostic: (\w+)').Groups[1].Value + if (-not ($catalog | Where-Object Symbol -eq $sym)) { $problems += "$($e.Name): $sym is not in the catalog" } +} +if ($problems) { throw ("diagnostic catalog problems:`n " + ($problems -join "`n ")) } + +# ------------------------------------------------- docs/diagnostics.md +$doc = New-Object System.Text.StringBuilder +[void]$doc.AppendLine("# Nova128 Diagnostics") +[void]$doc.AppendLine("") +[void]$doc.AppendLine("Generated from ``src/common/diagcat.inc`` by ``scripts/build-diag-test.ps1 -UpdateDocs``.") +[void]$doc.AppendLine("Do not edit by hand. Numbers are stable: never renumber or reuse one.") +[void]$doc.AppendLine("") +[void]$doc.AppendLine("Each ``%`` is replaced by an argument such as a name or type. On screen a") +[void]$doc.AppendLine("diagnostic is two lines, the header and the message clipped to 39 characters:") +[void]$doc.AppendLine("") +[void]$doc.AppendLine('```text') +[void]$doc.AppendLine("E010 TEST.N8 3 11") +[void]$doc.AppendLine("CHARACTER LITERAL NEEDS ONE CHARACTER") +[void]$doc.AppendLine('```') +[void]$doc.AppendLine("") +[void]$doc.AppendLine("``NAME.ERR`` holds one full line per diagnostic: ``E010 TEST.N8 3 11 CHARACTER ...``.") +[void]$doc.AppendLine("It is scratched when compilation starts and created only if a diagnostic occurs.") +[void]$doc.AppendLine("After 20 errors the next one is replaced by E221 and compilation stops.") +foreach ($c in $categories.Keys) { + $lo = $categories[$c][0]; $hi = $categories[$c][1] + [void]$doc.AppendLine("") + [void]$doc.AppendLine(("## {0} (E{1:D3}-E{2:D3})" -f $c, $lo, $hi)) + [void]$doc.AppendLine("") + [void]$doc.AppendLine("| Code | Symbol | Message |") + [void]$doc.AppendLine("|---|---|---|") + foreach ($d in ($catalog | Where-Object { $_.Code -ge $lo -and $_.Code -le $hi } | Sort-Object Code)) { + [void]$doc.AppendLine(("| E{0:D3} | ``{1}`` | {2} |" -f $d.Code, $d.Symbol, $d.Text.Replace('|', '\|'))) + } +} +$docPath = Join-Path $project "docs\diagnostics.md" +$docText = $doc.ToString().Replace("`r`n", "`n") +if ($UpdateDocs) { [IO.File]::WriteAllText($docPath, $docText) } +elseif (-not (Test-Path $docPath) -or [IO.File]::ReadAllText($docPath) -ne $docText) { + throw "docs/diagnostics.md is out of date; run build-diag-test.ps1 -UpdateDocs and review" +} + +# -------------------------------------------------------- expectations +function Format-Diag([string]$name, [int]$line, [int]$col, [string]$symbol, [string[]]$arguments = @()) { + $d = $catalog | Where-Object Symbol -eq $symbol + $msg = $d.Text # each % takes the next argument + foreach ($a in $arguments) { $idx = $msg.IndexOf('%'); $msg = $msg.Substring(0, $idx) + $a + $msg.Substring($idx + 1) } + $header = "E{0:D3} {1} {2} {3}" -f $d.Code, $name, $line, $col + return [pscustomobject]@{ Header = $header; Message = $msg; ErrLine = "$header $msg" } +} +$phaseA = @( + (Format-Diag "DIAG.N8" 3 11 "LEX_BAD_CHAR_LITERAL"), + (Format-Diag "DIAG.N8" 599 80 "SYN_EXPECTED_END_PROGRAM"), + (Format-Diag "DIAG.N8" 12 5 "SEM_DUPLICATE_NAME" @("COUNTER")), + (Format-Diag "DIAG.N8" 40 17 "TYP_MISMATCH" @("INTEGER", "VERYLONGCLASSNAM")), + (Format-Diag "DIAG.N8" 1 1 "DSK_SOURCE_UNREADABLE" @("62")), + (Format-Diag "DIAG.N8" 65535 255 "SYN_EXPECTED_RPAREN")) +$phaseD = @( + (Format-Diag "BAD*.N8" 7 3 "LEX_INVALID_CHAR"), + (Format-Diag "BAD*.N8" 7 3 "DSK_ERR_FILE_UNWRITABLE" @("##")), # ## = DOS code, checked by pattern + (Format-Diag "BAD*.N8" 8 4 "LEX_INVALID_CHAR")) +$limit = @(1..20 | ForEach-Object { (Format-Diag "LIMIT.N8" $_ 1 "LEX_INVALID_CHAR").ErrLine }) +$limit += (Format-Diag "LIMIT.N8" 21 1 "RES_ERROR_LIMIT").ErrLine +$screen = @() +foreach ($d in ($phaseA + $phaseD)) { + $screen += $d.Header + $screen += $(if ($d.Message.Length -gt 39) { $d.Message.Substring(0, 39) } else { $d.Message }) +} +$screen += "NOVA128 DIAG TEST OK" +[ordered]@{ + diag = @($phaseA | ForEach-Object ErrLine) + limit = $limit + screen = $screen +} | ConvertTo-Json -Depth 3 | Set-Content (Join-Path $out "expected.json") + +# ------------------------------------------------------------- program +$link = Invoke-Nova128Link -Name "diag-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\diagnostics.s"), + (Join-Path $project "tests\diag\diag-test.s")) +if ((Get-Nova128Label $link.Labels "test_entry") -ne 0x1C0D) { throw "test_entry is not at `$1C0D" } + +$stale = Join-Path $out "stale.pet" +[IO.File]::WriteAllBytes($stale, [byte[]](0x53, 0x54, 0x41, 0x4C, 0x45, 0x0D)) # "STALE" +New-Nova128Disk -Path (Join-Path $out "diag-test.d81") -Files @( + @{ Name = "diag-test"; Path = $link.Program; Type = "prg" }, + @{ Name = "diag.err"; Path = $stale; Type = "seq" }, + @{ Name = "limit.err"; Path = $stale; Type = "seq" }, + @{ Name = "clean.err"; Path = $stale; Type = "seq" }) +Write-Host ("catalog: {0} diagnostics in range; docs/diagnostics.md in sync" -f $catalog.Count) +Write-Host ("built {0}" -f $link.Program) diff --git a/newLang/scripts/verify-diag-test.ps1 b/newLang/scripts/verify-diag-test.ps1 new file mode 100644 index 0000000..2495a40 --- /dev/null +++ b/newLang/scripts/verify-diag-test.ps1 @@ -0,0 +1,85 @@ +# E02-S04: run the diagnostics test in x128, then check the result block, the +# 40-column screen, and the ERR files left on the disk image. +[CmdletBinding()] +param([int]$Port = 6534, [int]$TimeoutSeconds = 180) + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +& (Join-Path $PSScriptRoot "build-diag-test.ps1") + +$out = Join-Path $project "build\diag-test" +$disk = Join-Path $out "diag-test.d81" +$expected = Get-Content -Raw (Join-Path $out "expected.json") | ConvertFrom-Json +$failures = @() + +$session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port ` + -Screenshot (Join-Path $out "diag-test-vicii.png") +try { + $mb = Wait-Nova128Mailbox $session -TimeoutSeconds $TimeoutSeconds + $results = Read-Nova128Memory $session ($mb[8] + 256 * $mb[9]) 34 + Invoke-Nova128Monitor $session "x" | Out-Null + Start-Sleep -Seconds 2 # let the OK line print + $screenRam = Read-Nova128Memory $session 0x0400 1000 +} finally { + Stop-Nova128X128 $session -SettleSeconds 0 +} + +# ------------------------------------------------------------ results +$want = [ordered]@{ "phase A count" = @(0, 6); "phase D count" = @(1, 2); "phase D file state (2=failed)" = @(2, 2) + "phase B count" = @(3, 20); "phase C count" = @(4, 0) } +foreach ($k in $want.Keys) { + if ($results[$want[$k][0]] -ne $want[$k][1]) { $failures += "${k}: $($results[$want[$k][0]]) expected $($want[$k][1])" } +} +for ($i = 5; $i -le 8; $i++) { if ($results[$i] -ne 0) { $failures += "files left open after phase $($i - 4): $($results[$i])" } } +$carries = $results[9..33] +$wantCarries = @(0) * 20 + @(1) * 5 +if (($carries -join ',') -ne ($wantCarries -join ',')) { + $failures += "phase B carries $($carries -join '') expected $($wantCarries -join '')" +} + +# ------------------------------------------------------------- screen +$rows = for ($r = 0; $r -lt 25; $r++) { + $chars = for ($c = 0; $c -lt 40; $c++) { + $code = $screenRam[$r * 40 + $c] + if ($code -le 0x1F) { [char]($code + 0x40) } elseif ($code -le 0x3F) { [char]$code } else { '?' } + } + (-join $chars).TrimEnd() +} +for ($r = 0; $r -lt $expected.screen.Count; $r++) { + $pattern = '^' + [regex]::Escape($expected.screen[$r]).Replace('\#\#', '\d\d').Replace('##', '\d\d') + '$' + if ($rows[$r] -notmatch $pattern) { $failures += "screen row ${r}: '$($rows[$r])' expected '$($expected.screen[$r])'" } +} +$wide = @($rows | Where-Object { $_.Length -gt 39 }) +if ($wide) { $failures += "screen rows longer than 39: $($wide -join ' | ')" } + +# ---------------------------------------------------------- ERR files +$c1541 = Get-ViceTool "c1541" +$listing = (& $c1541 -attach $disk -list 2>&1) | Out-String +function Read-SeqText([string]$name) { + $dest = Join-Path $out ($name.Replace('*', '_') + ".bin") + if (Test-Path $dest) { Remove-Item $dest -Force } + & $c1541 -attach $disk -read "$name,s" $dest 2>&1 | Out-Null # SEQ needs ,s + if (-not (Test-Path $dest)) { return $null } + $bytes = [IO.File]::ReadAllBytes($dest) + return @(([Text.Encoding]::ASCII.GetString($bytes).TrimEnd("`r")) -split "`r") +} +$diagLines = Read-SeqText "diag.err" +if ($null -eq $diagLines) { $failures += "diag.err missing" } +elseif (($diagLines -join "`n") -cne ($expected.diag -join "`n")) { + $failures += "diag.err differs:`n got: " + ($diagLines -join "`n ") + "`n want: " + ($expected.diag -join "`n ") +} +$limitLines = Read-SeqText "limit.err" +if ($null -eq $limitLines) { $failures += "limit.err missing" } +elseif (($limitLines -join "`n") -cne ($expected.limit -join "`n")) { + $failures += "limit.err differs ($($limitLines.Count) lines): first '$($limitLines[0])', last '$($limitLines[-1])'" +} +if ($listing -match '"clean\.err"') { $failures += "stale clean.err was not scratched" } +if ($listing -match '"bad') { $failures += "a bad*.err file was created" } + +if ($failures) { throw ("diagnostics verification failed:`n " + ($failures -join "`n ")) } +Write-Host "screen (rows 0-$($expected.screen.Count - 1)):" +$rows[0..($expected.screen.Count - 1)] | ForEach-Object { Write-Host " |$_" } +Write-Host ("diag.err: {0} lines, limit.err: {1} lines (20 + limit), clean.err scratched, no bad*.err" -f + $diagLines.Count, $limitLines.Count) +Write-Host "verified diagnostics: results, screen, and ERR files match" diff --git a/newLang/src/common/c128.inc b/newLang/src/common/c128.inc index 5f1b76f..c7728b3 100644 --- a/newLang/src/common/c128.inc +++ b/newLang/src/common/c128.inc @@ -60,6 +60,7 @@ SETNAM = $FFBD OPEN = $FFC0 ; no-name OPEN sends nothing on the bus CLOSE = $FFC3 CHKIN = $FFC6 ; absent device: C=1, A=5, ST=$80 +CHKOUT = $FFC9 CLRCHN = $FFCC CHRIN = $FFCF CHROUT = $FFD2 diff --git a/newLang/src/common/diagcat.inc b/newLang/src/common/diagcat.inc new file mode 100644 index 0000000..2d242f4 --- /dev/null +++ b/newLang/src/common/diagcat.inc @@ -0,0 +1,68 @@ +; diagcat.inc - THE Nova128 diagnostic catalog (E02-S04). +; +; One line per diagnostic: DIAGNOSTIC symbol, number, "message" +; The including file defines the DIAGNOSTIC macro: errors.inc turns each line +; into a constant E_; diagnostics.s turns it into the message table. +; docs/diagnostics.md is generated from this file by build-diag-test.ps1. +; +; Rules (checked by scripts/build-diag-test.ps1): +; - Numbers are stable. Never renumber or reuse; append within the range. +; - Ranges: 1-19 source and lexical 20-59 syntax +; 60-79 names and declarations 80-119 types +; 120-129 access 130-149 interfaces +; 150-169 methods and returns 200-219 disk +; 220-239 resource limits +; - Messages are uppercase ASCII (PETSCII unshifted letters), at most 39 +; characters with no arguments. Each % is replaced by an argument (at most +; two, each at most 16 characters); the screen clips the result to 39. + +; ---------------------------------------------------- source and lexical + DIAGNOSTIC SRC_LINE_TOO_LONG, 1, "LINE LONGER THAN 80 CHARACTERS" + DIAGNOSTIC SRC_FILE_TOO_LARGE, 2, "SOURCE LARGER THAN 18999 BYTES" + DIAGNOSTIC SRC_TOO_MANY_LINES, 3, "MORE THAN 599 LINES" + DIAGNOSTIC LEX_INVALID_CHAR, 4, "INVALID CHARACTER" + DIAGNOSTIC LEX_IDENT_TOO_LONG, 5, "NAME LONGER THAN 16 CHARACTERS" + DIAGNOSTIC LEX_NUMBER_RANGE, 6, "NUMBER LARGER THAN 65535" + DIAGNOSTIC LEX_BAD_NUMBER, 7, "MALFORMED NUMBER" + DIAGNOSTIC LEX_UNTERMINATED_STRING, 8, "STRING NOT CLOSED ON THIS LINE" + DIAGNOSTIC LEX_BAD_ESCAPE, 9, "UNKNOWN ESCAPE SEQUENCE" + DIAGNOSTIC LEX_BAD_CHAR_LITERAL, 10, "CHARACTER LITERAL NEEDS ONE CHARACTER" + +; ----------------------------------------------------------------- syntax + DIAGNOSTIC SYN_EXPECTED_IDENTIFIER, 20, "NAME EXPECTED" + DIAGNOSTIC SYN_EXPECTED_EOL, 21, "END OF LINE EXPECTED" + DIAGNOSTIC SYN_UNEXPECTED_EOL, 22, "LINE ENDS TOO EARLY" + DIAGNOSTIC SYN_EXPECTED_RPAREN, 23, "')' EXPECTED" + DIAGNOSTIC SYN_EXPECTED_END_PROGRAM, 24, "END PROGRAM EXPECTED" + DIAGNOSTIC SYN_EXPECTED_END_METHOD, 25, "END METHOD EXPECTED" + DIAGNOSTIC SYN_EXPECTED_END_IF, 26, "END IF EXPECTED" + DIAGNOSTIC SYN_EXPECTED_END_CLASS, 27, "END CLASS EXPECTED" + DIAGNOSTIC SYN_EXPECTED_INTERFACE_NAME, 28, "INTERFACE NAME EXPECTED" + DIAGNOSTIC SYN_INTERFACE_METHOD_BODY, 29, "INTERFACE METHODS HAVE NO BODY" + DIAGNOSTIC SYN_UNEXPECTED_TOKEN, 30, "% IS NOT ALLOWED HERE" + +; -------------------------------------------- names and declarations + DIAGNOSTIC SEM_DUPLICATE_NAME, 60, "% IS ALREADY DECLARED" + DIAGNOSTIC SEM_UNKNOWN_NAME, 61, "% IS NOT DECLARED" + +; ------------------------------------------------------------------ types + DIAGNOSTIC TYP_MISMATCH, 80, "% EXPECTED; % FOUND" + DIAGNOSTIC TYP_CONDITION_NOT_BOOLEAN, 81, "CONDITION MUST BE BOOLEAN" + +; ----------------------------------------------------------------- access + DIAGNOSTIC ACC_PRIVATE_MEMBER, 120, "% IS PRIVATE" + +; ------------------------------------------------------------- interfaces + DIAGNOSTIC IFC_METHOD_MISSING, 130, "% IS NOT IMPLEMENTED" + DIAGNOSTIC IFC_SIGNATURE_MISMATCH, 131, "% DIFFERS FROM INTERFACE" + +; ---------------------------------------------------- methods and returns + DIAGNOSTIC SEM_MISSING_RETURN, 150, "MISSING RETURN VALUE" + +; ------------------------------------------------------------------- disk + DIAGNOSTIC DSK_SOURCE_UNREADABLE, 200, "CANNOT READ SOURCE; DOS %" + DIAGNOSTIC DSK_ERR_FILE_UNWRITABLE, 201, "CANNOT WRITE ERROR FILE; DOS %" + +; ------------------------------------------------------- resource limits + DIAGNOSTIC RES_TOO_MANY_NAMES, 220, "MORE THAN 255 NAMES" + DIAGNOSTIC RES_ERROR_LIMIT, 221, "TOO MANY ERRORS; COMPILATION STOPPED" diff --git a/newLang/src/common/diskio.inc b/newLang/src/common/diskio.inc index 5b4e647..522f17f 100644 --- a/newLang/src/common/diskio.inc +++ b/newLang/src/common/diskio.inc @@ -7,6 +7,8 @@ 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_LFN_ERR = 4 ; NAME.ERR output +DIO_SA_ERR = 4 DIO_STATUS_MAX = 40 ; bytes kept from a drive status line .endif diff --git a/newLang/src/common/errors.inc b/newLang/src/common/errors.inc new file mode 100644 index 0000000..0ea17da --- /dev/null +++ b/newLang/src/common/errors.inc @@ -0,0 +1,16 @@ +; errors.inc - diagnostic numbers as E_ constants (E02-S04). +; The catalog itself is src/common/diagcat.inc. + +.ifndef ERRORS_INC +ERRORS_INC = 1 + +.macro DIAGNOSTIC sym, number, text + .ident(.concat("E_", .string(sym))) = number +.endmacro +.include "diagcat.inc" +.delmacro DIAGNOSTIC + +DIAG_LIMIT_DEFAULT = 20 ; errors before compilation stops +DIAG_SCREEN_WIDTH = 39 ; longest screen line (no editor wrap) + +.endif diff --git a/newLang/src/compiler/diagnostics.s b/newLang/src/compiler/diagnostics.s new file mode 100644 index 0000000..c8916f4 --- /dev/null +++ b/newLang/src/compiler/diagnostics.s @@ -0,0 +1,539 @@ +; diagnostics.s - diagnostic reporting and the ERR writer (E02-S04). +; +; Usage (bank-0 compiler state): +; diag_init A/X = source name (bank 0), Y = length. Scratches a stale +; NAME.ERR (".N8" replaced by ".ERR"); resets the count. +; diag_line/diag_col, optional diag_arg1/diag_arg2 (address + length), +; then diag_report with A = E_* number. +; C=0: reported, keep going. C=1: the error limit was reached; the +; limit diagnostic has been reported and the caller must stop. +; diag_finish closes NAME.ERR; A = number of errors reported. +; +; Screen form (40 columns, no line ever longer than 39 characters): +; E010 TEST.N8 3 11 +; CHARACTER LITERAL NEEDS ONE CHARACTER +; ERR form, one line per diagnostic, $0D terminated: +; E010 TEST.N8 3 11 CHARACTER LITERAL NEEDS ONE CHARACTER +; NAME.ERR is created only when the first diagnostic is reported. If it cannot +; be written, DSK_ERR_FILE_UNWRITABLE is shown once on screen and reporting +; continues on screen only. + +.setcpu "6502" +.include "../common/c128.inc" +.include "../common/diskio.inc" +.include "../common/errors.inc" + +.import kcall_inline +.import dio_open_cmd, dio_open_out, dio_read_status, dio_chkout, dio_putc +.import dio_clrchn, dio_close, dio_command, dio_dos_code + +.export diag_init, diag_report, diag_finish +.export diag_line, diag_col, diag_arg1, diag_arg1_len, diag_arg2, diag_arg2_len +.export diag_count, diag_limit, diag_screen, diag_file_state, diag_buf + +DIAG_BUF_MAX = 96 +NAME_MAX = 16 + +FILE_NONE = 0 ; NAME.ERR not opened yet +FILE_OPEN = 1 +FILE_FAILED = 2 + +; ------------------------------------------------------------------ CODE +.segment "CODE" + +diag_init: + sta @src+1 + stx @src+2 + cpy #NAME_MAX + 1 + bcc :+ + ldy #NAME_MAX +: sty name_len + ldy #$00 +@copy: cpy name_len + beq @copied +@src: lda $FFFF,y ; patched + sta name_text,y + iny + bne @copy +@copied: + lda #$00 + sta diag_count + sta diag_stopped + sta diag_file_state + sta diag_arg1_len + sta diag_arg2_len + lda #DIAG_LIMIT_DEFAULT + sta diag_limit + lda #$01 + sta diag_screen + + ; base = name without a trailing ".N8" (either letter range) + lda name_len + sta base_len + cmp #3 + bcc @names + tay + lda name_text-1,y + cmp #'8' + bne @names + lda name_text-2,y + and #$7F + cmp #'N' + bne @names + lda name_text-3,y + cmp #'.' + bne @names + dey + dey + dey + sty base_len +@names: ldx #$00 ; scratch_cmd = "S0:" base ".ERR" + ldy #$00 +: lda scratch_prefix,y + sta scratch_cmd,x + inx + iny + cpy #3 + bne :- + jsr append_base + stx scratch_len + ldx #$00 ; open_name = base ".ERR,S,W" + jsr append_base_to_open + stx open_len + + jsr dio_open_cmd ; remove any stale NAME.ERR + bcs @done + lda #scratch_cmd + ldy scratch_len + jsr dio_command + lda #DIO_LFN_CMD + jsr dio_close +@done: rts + +; Append base + ".ERR" to scratch_cmd at X. +append_base: + ldy #$00 +: cpy base_len + beq :+ + lda name_text,y + sta scratch_cmd,x + inx + iny + bne :- +: ldy #$00 +: lda err_suffix,y + sta scratch_cmd,x + inx + iny + cpy #4 + bne :- + rts + +append_base_to_open: + ldy #$00 +: cpy base_len + beq :+ + lda name_text,y + sta open_name,x + inx + iny + bne :- +: ldy #$00 +: lda err_suffix,y + sta open_name,x + inx + iny + cpy #8 + bne :- + rts + +diag_report: + ldx diag_stopped + beq :+ + sec ; already stopped: report nothing more + rts +: ldx diag_count + cpx diag_limit + bcc @count + lda #E_RES_ERROR_LIMIT ; the (limit+1)th error stops compilation + sta diag_code + lda #$00 + sta diag_arg1_len + sta diag_arg2_len + lda #$01 + sta diag_stopped + jsr emit + sec + rts +@count: sta diag_code + inc diag_count + jsr emit + clc + rts + +; Report to screen and file, then clear the arguments. +emit: jsr format + lda diag_screen + beq :+ + jsr to_screen +: jsr to_file + lda #$00 + sta diag_arg1_len + sta diag_arg2_len + rts + +diag_finish: + lda diag_file_state + cmp #FILE_OPEN + bne @done + jsr dio_clrchn + lda #DIO_LFN_ERR + jsr dio_close + lda #DIO_LFN_CMD ; command channel last + jsr dio_close + lda #FILE_NONE + sta diag_file_state +@done: lda diag_count + rts + +; --------------------------------------------------------------- format +; diag_buf = "Ennn NAME line col message"; hdr_len, msg_start, buf_len. +format: ldx #$00 + lda #'E' + jsr put + lda diag_code ; three digits, zero padded + ldy #'0' - 1 +: iny + sec + sbc #100 + bcs :- + adc #100 + pha + tya + jsr put + pla + ldy #'0' - 1 +: iny + sec + sbc #10 + bcs :- + adc #10 + '0' + pha + tya + jsr put + pla + jsr put + lda #' ' + jsr put + ldy #$00 +: cpy name_len + beq :+ + lda name_text,y + jsr put + iny + bne :- +: lda #' ' + jsr put + lda diag_line + ldy diag_line+1 + jsr put_decimal + lda #' ' + jsr put + lda diag_col + ldy #$00 + jsr put_decimal + stx hdr_len + lda #' ' + jsr put + stx msg_start + + jsr find_message ; template address -> fmt_tpl + lda #$00 + sta arg_index + ldy #$00 +fmt_loop: + sty tpl_y +fmt_tpl: + lda $FFFF,y ; patched by find_message + beq fmt_done + cmp #'%' + bne fmt_plain + jsr put_arg + jmp fmt_next +fmt_plain: + jsr put +fmt_next: + ldy tpl_y + iny + bne fmt_loop +fmt_done: + stx buf_len + rts + +; X = index; store A unless the buffer is full. +put: cpx #DIAG_BUF_MAX + bcs :+ + sta diag_buf,x + inx +: rts + +; Append argument 1 or 2 (in order of the % markers). +put_arg: + lda arg_index ; 0 = first %, otherwise the second + inc arg_index + cmp #$00 + bne @two + lda diag_arg1 + sta @src+1 + lda diag_arg1+1 + sta @src+2 + lda diag_arg1_len + jmp @copy +@two: lda diag_arg2 + sta @src+1 + lda diag_arg2+1 + sta @src+2 + lda diag_arg2_len +@copy: sta arg_len + ldy #$00 +: cpy arg_len + beq @done +@src: lda $FFFF,y ; patched + jsr put + iny + bne :- +@done: rts + +; Append the unsigned 16-bit value A (low) / Y (high) in decimal. +put_decimal: + sta num + sty num+1 + lda #$00 + sta num_started + ldy #4 ; 10000, 1000, 100, 10, 1 +@digit: lda #'0' + sta num_digit +@sub: lda num + sec + sbc pow_lo,y + sta num_t + lda num+1 + sbc pow_hi,y + bcc @emit + sta num+1 + lda num_t + sta num + inc num_digit + bne @sub +@emit: lda num_digit + cpy #$00 ; always print the units digit + beq @out + cmp #'0' + bne @out + lda num_started + beq @skip + lda num_digit +@out: sta num_started + jsr put +@skip: dey + bpl @digit + rts + +; Point the fmt_tpl load in format at the template for diag_code. +; Preserves X: it is format's buffer index. +find_message: + ldy #$00 +@scan: lda catalog,y + beq @unknown + cmp diag_code + beq @found + iny + iny + iny + bne @scan +@unknown: + lda #unknown_text + sta fmt_tpl+2 + rts +@found: lda catalog+1,y + sta fmt_tpl+1 + lda catalog+2,y + sta fmt_tpl+2 + rts + +; ------------------------------------------------------------- outputs +to_screen: + ldy #$00 +: cpy hdr_len + beq :+ + lda diag_buf,y + jsr screen_putc + iny + bne :- +: lda #$0D + jsr screen_putc + lda msg_start ; message clipped to DIAG_SCREEN_WIDTH + clc + adc #DIAG_SCREEN_WIDTH + sta clip_end + ldy msg_start +: cpy buf_len + beq :+ + cpy clip_end + beq :+ + lda diag_buf,y + jsr screen_putc + iny + bne :- +: lda #$0D + jmp screen_putc + +screen_putc: + sty out_y + jsr kcall_inline + .word CHROUT + ldy out_y + rts + +to_file: + lda diag_file_state + cmp #FILE_FAILED + beq @done + cmp #FILE_OPEN + beq @write + jsr open_err_file + bcs @done +@write: ldx #DIO_LFN_ERR + jsr dio_chkout + bcs @done + ldy #$00 +: cpy buf_len + beq :+ + lda diag_buf,y + sty out_y + jsr dio_putc + ldy out_y + iny + bne :- +: lda #$0D + jsr dio_putc + jsr dio_clrchn +@done: rts + +; Open the command channel, then NAME.ERR. On failure close both, mark the +; file failed, and show DSK_ERR_FILE_UNWRITABLE once on screen. C=1 on failure. +open_err_file: + jsr dio_open_cmd + bcs @fail_cmd + lda #open_name + ldy open_len + jsr dio_open_out + bcs @fail + jsr dio_read_status + lda dio_dos_code + cmp #20 + bcs @fail + lda #FILE_OPEN + sta diag_file_state + clc + rts +@fail: lda #DIO_LFN_ERR + jsr dio_close +@fail_cmd: + lda #DIO_LFN_CMD + jsr dio_close + lda #FILE_FAILED + sta diag_file_state + lda dio_dos_code ; screen-only notice; the report that + ; triggered it is already on screen ; argument: DOS code as two digits + ldy #$00 + jsr dos_digits + lda #E_DSK_ERR_FILE_UNWRITABLE + sta diag_code + lda #dos_text + sta diag_arg1+1 + lda #2 + sta diag_arg1_len + lda #$00 + sta diag_arg2_len + jsr format + jsr to_screen + sec + rts + +dos_digits: + ldy #'0' - 1 +: iny + sec + sbc #10 + bcs :- + adc #10 + '0' + sty dos_text + sta dos_text+1 + rts + +.segment "RODATA" +pow_lo: .byte <1, <10, <100, <1000, <10000 +pow_hi: .byte >1, >10, >100, >1000, >10000 +scratch_prefix: .byte "S0:" +err_suffix: .byte ".ERR,S,W" +unknown_text: .byte "INTERNAL ERROR", 0 + +; code, template address; 0 ends the table. +.macro DIAGNOSTIC sym, number, text + .local t + .byte number + .word t + .pushseg + .segment "RODATA" +t: .byte text, 0 + .popseg +.endmacro +.segment "DATA" +catalog: +.include "../common/diagcat.inc" + .byte 0 +.delmacro DIAGNOSTIC +.assert * - catalog < 256, error, "catalog table must stay below 256 bytes" + +; ------------------------------------------------------------------- BSS +.segment "BSS" +diag_line: .res 2 +diag_col: .res 1 +diag_arg1: .res 2 +diag_arg1_len: .res 1 +diag_arg2: .res 2 +diag_arg2_len: .res 1 +diag_count: .res 1 +diag_limit: .res 1 +diag_screen: .res 1 +diag_file_state: .res 1 +diag_stopped: .res 1 +name_len: .res 1 +name_text: .res NAME_MAX +base_len: .res 1 +scratch_len: .res 1 +scratch_cmd: .res 3 + NAME_MAX + 4 +open_len: .res 1 +open_name: .res NAME_MAX + 8 +tpl_y: .res 1 +arg_index: .res 1 +arg_len: .res 1 +num: .res 2 +num_t: .res 1 +num_digit: .res 1 +num_started: .res 1 +clip_end: .res 1 +out_y: .res 1 +dos_text: .res 2 +diag_code: .res 1 +hdr_len: .res 1 +msg_start: .res 1 +buf_len: .res 1 +diag_buf: .res DIAG_BUF_MAX diff --git a/newLang/src/compiler/diskio.s b/newLang/src/compiler/diskio.s index 0a418c8..d49f583 100644 --- a/newLang/src/compiler/diskio.s +++ b/newLang/src/compiler/diskio.s @@ -14,6 +14,7 @@ .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 +.export dio_open_out, dio_chkout, dio_putc, dio_command .import __DISKBUF_START__ @@ -124,6 +125,61 @@ dio_close: .word CLOSE rts +; OPEN DIO_LFN_ERR,8,DIO_SA_ERR,name for writing. In: A/X = name (bank 0, +; including ",S,W"), Y = length. Check dio_read_status afterwards. +dio_open_out: + sta dio_tmp + stx dio_tmp+1 + sty dio_tmp+2 + jsr dio_setbank + lda dio_tmp+2 + ldx dio_tmp + ldy dio_tmp+1 + jsr kcall_inline + .word SETNAM + lda #DIO_LFN_ERR + ldx #DIO_DEVICE + ldy #DIO_SA_ERR + jsr kcall_inline + .word SETLFS + jsr kcall_inline + .word OPEN + rts + +; Select logical file X for output. C=1 with A = KERNAL error on failure. +dio_chkout: + jsr kcall_inline + .word CHKOUT + rts + +; Write A to the current output channel. +dio_putc: + jsr kcall_inline + .word CHROUT + rts + +; Send a DOS command (A/X = text, Y = length) on the open command channel, +; then read the reply into dio_status / dio_dos_code. +dio_command: + sta @src+1 + stx @src+2 + sty dio_tmp+2 + ldx #DIO_LFN_CMD + jsr dio_chkout + bcs @out + ldy #$00 +@byte: cpy dio_tmp+2 + beq @sent +@src: lda $FFFF,y ; patched + sty dio_tmp + jsr dio_putc + ldy dio_tmp + iny + bne @byte +@sent: jsr dio_clrchn ; UNLISTEN executes the command + jmp dio_read_status +@out: rts + dio_setbank: lda #$00 ; data and filename in bank 0 tax diff --git a/newLang/tests/diag/diag-test.s b/newLang/tests/diag/diag-test.s new file mode 100644 index 0000000..7bf85dc --- /dev/null +++ b/newLang/tests/diag/diag-test.s @@ -0,0 +1,199 @@ +; E02-S04: diagnostics and ERR-writer test in x128. +; +; Phase A DIAG.N8 six representative diagnostics on a cleared screen +; Phase D BAD*.N8 ERR name the drive refuses: report, screen-only notice, +; second report on screen only +; Phase B LIMIT.N8 25 errors, screen off: 20 reported, then the limit +; Phase C CLEAN.N8 no errors: the stale CLEAN.ERR is scratched, none made +; Results (address in MB_DATA+0/1): +; +0 A count +1 D count +2 D file state +3 B count +4 C count +; +5..+8 LDTND after A, D, B, C +; +9..+33 carry returned by each of B's 25 diag_report calls +; Mailbox PASS means the program ran to the end; the host checks the rest. + +.setcpu "6502" +.include "../../src/common/c128.inc" +.include "../../src/common/mailbox.inc" +.include "../../src/common/errors.inc" + +.import mmu_init, mmu_bank0, mmu_shutdown, kcall_inline +.import diag_init, diag_report, diag_finish, diag_line, diag_col +.import diag_arg1, diag_arg1_len, diag_arg2, diag_arg2_len +.import diag_screen, diag_file_state +.import __COMPILER_ZP_PAGE__ + +.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 + +.macro REPORT line, col, code + lda #<(line) + sta diag_line + lda #>(line) + sta diag_line+1 + lda #col + sta diag_col + lda #code + jsr diag_report +.endmacro + +.macro ARG1 text, len + lda #text + sta diag_arg1+1 + lda #len + sta diag_arg1_len +.endmacro + +.macro ARG2 text, len + lda #text + sta diag_arg2+1 + lda #len + sta diag_arg2_len +.endmacro + +.macro INIT name, len + lda #name + ldy #len + jsr diag_init +.endmacro + +.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 #$93 ; clear screen: the host reads it back + jsr kcall_inline + .word CHROUT + + ; ------------------------------------------------------ phase A + INIT n_diag, 7 + REPORT 3, 11, E_LEX_BAD_CHAR_LITERAL + REPORT 599, 80, E_SYN_EXPECTED_END_PROGRAM + ARG1 a_counter, 7 + REPORT 12, 5, E_SEM_DUPLICATE_NAME + ARG1 a_integer, 7 + ARG2 a_long, 16 + REPORT 40, 17, E_TYP_MISMATCH + ARG1 a_dos, 2 + REPORT 1, 1, E_DSK_SOURCE_UNREADABLE + REPORT 65535, 255, E_SYN_EXPECTED_RPAREN + jsr diag_finish + sta results+0 + jsr open_files + sta results+5 + + ; ------------------------------------------------------ phase D + INIT n_bad, 7 + REPORT 7, 3, E_LEX_INVALID_CHAR + REPORT 8, 4, E_LEX_INVALID_CHAR + lda diag_file_state + sta results+2 + jsr diag_finish + sta results+1 + jsr open_files + sta results+6 + + ; ------------------------------------------------------ phase B + INIT n_limit, 8 + lda #$00 + sta diag_screen + ldx #$00 +@limit: stx b_index + inx ; line = 1..25 + stx diag_line + lda #$00 + sta diag_line+1 + lda #1 + sta diag_col + lda #E_LEX_INVALID_CHAR + jsr diag_report + lda #$00 + rol a ; carry -> 0/1 + ldx b_index + sta results+9,x + inx + cpx #25 + bne @limit + jsr diag_finish + sta results+3 + jsr open_files + sta results+7 + + ; ------------------------------------------------------ phase C + INIT n_clean, 8 + jsr diag_finish + sta results+4 + jsr open_files + sta results+8 + + jsr mmu_shutdown + lda #results + sta MB_DATA+1 + lda #MB_ST_PASS + sta MB_STATUS + plp + ldy #$00 +@print: lda ok_msg,y + beq @end + sty b_index + jsr CHROUT + ldy b_index + iny + bne @print +@end: rts + +.segment "CODE" +; A = number of open logical files (system zero page via the P0 swap). +open_files: + lda __COMPILER_ZP_PAGE__ * 256 + LDTND + rts + +.segment "RODATA" +n_diag: .byte "DIAG.N8" +n_bad: .byte "BAD*.N8" +n_limit: .byte "LIMIT.N8" +n_clean: .byte "CLEAN.N8" +a_counter: .byte "COUNTER" +a_integer: .byte "INTEGER" +a_long: .byte "VERYLONGCLASSNAM" +a_dos: .byte "62" +ok_msg: .byte "NOVA128 DIAG TEST OK", $0D, $00 + +.segment "BSS" +results: .res 34 +b_index: .res 1 diff --git a/newLang/tests/grammar/README.md b/newLang/tests/grammar/README.md index 47a313f..125e31b 100644 --- a/newLang/tests/grammar/README.md +++ b/newLang/tests/grammar/README.md @@ -20,8 +20,10 @@ The validator enforces the Vim editor profile for every `.n8` fixture: - one symbolic diagnostic expectation for every rejected fixture; - no diagnostic sidecars for accepted fixtures. -`.expect` files deliberately use symbolic diagnostic names. Story E02-S04 will -assign stable numeric codes without requiring grammar fixtures to guess them. +`.expect` files deliberately use symbolic diagnostic names. E02-S04 assigned +their stable numbers in `src/common/diagcat.inc` (listed in +`docs/diagnostics.md`); `scripts/build-diag-test.ps1` fails if an `.expect` +name is missing from the catalog. ## Accepted Coverage