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