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