parser.s runs two passes over the cached source. Pass 1 parses interfaces, classes (implements lists, fields, method headers), and the one program, records top-level types in a bank-1 table, tracks method/program/if/while/ for blocks, checks duplicate locals, and reports syntax, ordering, and duplicate errors with line-level recovery. Pass 2 reports type references that name no declared type, so forward references are legal. Verified in x128 against 24 fixtures through the real ERR files: 7/7 accepted, 10/12 rejected grammar fixtures at their sidecar positions (line_cont and multi_stmt are statement-level, pending E03-S03), and five parser fixtures matching their complete diagnostic lists. A pass-order mutation is caught. D121 (proposed): method names may be keywords, as spec 5.1's `method print()` requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
| @@ -167,6 +167,13 @@ semantic storage. See `docs/platform-display-clock.md` for evidence. | |||||
| The compiler reparses a buffered/streamed source rather than retaining a large | The compiler reparses a buffered/streamed source rather than retaining a large | ||||
| pointer-rich abstract syntax tree. | pointer-rich abstract syntax tree. | ||||
| E03-S01 implements the declaration-level part of this as `parse_file` | |||||
| (`parser.s`): pass 1 parses declarations and method headers, records | |||||
| top-level types, tracks block structure, and reports syntax, ordering, and | |||||
| duplicate errors; pass 2 replays the source and reports type references that | |||||
| name no declared interface or class. Statement bodies are skimmed until | |||||
| E03-S03. | |||||
| ### Pass 1 — declarations and layout | ### Pass 1 — declarations and layout | ||||
| - tokenize the complete source; | - tokenize the complete source; | ||||
| @@ -47,6 +47,7 @@ documented default. `Proposed` means a later change still needs approval. | |||||
| |---|---|---| | |---|---|---| | ||||
| | D119 | The string and character escapes `\n` and `\r` both produce PETSCII `$0D`. | Commodore output has one line-end character, so both escapes start a new line. Implemented as `ESC_N_VALUE`/`ESC_R_VALUE` in `src/common/tokens.inc`; changing them later affects only those constants and the lexer golden files. | | | D119 | The string and character escapes `\n` and `\r` both produce PETSCII `$0D`. | Commodore output has one line-end character, so both escapes start a new line. Implemented as `ESC_N_VALUE`/`ESC_R_VALUE` in `src/common/tokens.inc`; changing them later affects only those constants and the lexer golden files. | | ||||
| | D120 | A program may use at most 255 distinct identifiers (names are 1-byte IDs). | Covers well beyond a 599-line program's typical vocabulary at 5,164 bytes of bank 1. Exceeding it is a resource diagnostic; raising it means 2-byte name IDs in every later record. | | | D120 | A program may use at most 255 distinct identifiers (names are 1-byte IDs). | Covers well beyond a 599-line program's typical vocabulary at 5,164 bytes of bank 1. Exceeding it is a resource diagnostic; raising it means 2-byte name IDs in every later record. | | ||||
| | D121 | A method name may be any identifier or reserved word, as in spec 5.1's `method print()`. | Spec 5.1 and the accepted fixtures declare `method print()` although `print` is a statement keyword. The parser accepts a keyword after `method`; E03-S03 must accept the same member names after `.`. | | |||||
| ## Deferred | ## Deferred | ||||
| @@ -45,6 +45,13 @@ After 20 errors the next one is replaced by E221 and compilation stops. | |||||
| | E028 | `SYN_EXPECTED_INTERFACE_NAME` | INTERFACE NAME EXPECTED | | | E028 | `SYN_EXPECTED_INTERFACE_NAME` | INTERFACE NAME EXPECTED | | ||||
| | E029 | `SYN_INTERFACE_METHOD_BODY` | INTERFACE METHODS HAVE NO BODY | | | E029 | `SYN_INTERFACE_METHOD_BODY` | INTERFACE METHODS HAVE NO BODY | | ||||
| | E030 | `SYN_UNEXPECTED_TOKEN` | % IS NOT ALLOWED HERE | | | E030 | `SYN_UNEXPECTED_TOKEN` | % IS NOT ALLOWED HERE | | ||||
| | E031 | `SYN_EXPECTED_PROGRAM` | PROGRAM DECLARATION EXPECTED | | |||||
| | E032 | `SYN_EXPECTED_END_INTERFACE` | END INTERFACE EXPECTED | | |||||
| | E033 | `SYN_EXPECTED_LPAREN` | '(' EXPECTED | | |||||
| | E034 | `SYN_EXPECTED_AS` | AS EXPECTED | | |||||
| | E035 | `SYN_EXPECTED_TYPE` | TYPE EXPECTED | | |||||
| | E036 | `SYN_EXPECTED_END_WHILE` | END WHILE EXPECTED | | |||||
| | E037 | `SYN_EXPECTED_END_FOR` | END FOR EXPECTED | | |||||
| ## Names and declarations (E060-E079) | ## Names and declarations (E060-E079) | ||||
| @@ -92,3 +99,4 @@ After 20 errors the next one is replaced by E221 and compilation stops. | |||||
| |---|---|---| | |---|---|---| | ||||
| | E220 | `RES_TOO_MANY_NAMES` | MORE THAN 255 NAMES | | | E220 | `RES_TOO_MANY_NAMES` | MORE THAN 255 NAMES | | ||||
| | E221 | `RES_ERROR_LIMIT` | TOO MANY ERRORS; COMPILATION STOPPED | | | E221 | `RES_ERROR_LIMIT` | TOO MANY ERRORS; COMPILATION STOPPED | | ||||
| | E222 | `RES_NESTING_TOO_DEEP` | BLOCKS NESTED TOO DEEPLY | | |||||
| @@ -51,6 +51,10 @@ COMMON, DISKBUF, private ZP, SCRATCH, and MAILBOX are visible from bank 1. | |||||
| | Owner | Bytes | Contents | | | Owner | Bytes | Contents | | ||||
| |---|---|---| | |---|---|---| | ||||
| | `names.s` | 5,164 | 64 hash-chain heads + 255 name records of 20 bytes (D120) | | | `names.s` | 5,164 | 64 hash-chain heads + 255 name records of 20 bytes (D120) | | ||||
| | `parser.s` | 768 | Top-level type table by name ID: kind, declaration line (E03-S01) | | |||||
| BSS is not zeroed at load: every table must be cleared by its owner before | |||||
| use (`names_init`, `parser.s` `tt_clear`). | |||||
| | `$C000-$FFFF` | | unmapped | System ROM is visible here instead; BASIC string space at the top of bank 1 is untouched | | | `$C000-$FFFF` | | unmapped | System ROM is visible here instead; BASIC string space at the top of bank 1 is untouched | | ||||
| Bank-1 data is reached only through `mmu.s` helpers (`mmu_peek1`/`mmu_poke1` | Bank-1 data is reached only through `mmu.s` helpers (`mmu_peek1`/`mmu_poke1` | ||||
| @@ -73,6 +77,12 @@ System zero page stays intact. KERNAL calls switch P0 back to `$0000` for the | |||||
| call. While P0 is relocated, the system zero page is still readable at | call. While P0 is relocated, the system zero page is still readable at | ||||
| `$1A00-$1AFF` (the MMU swaps the two pages), e.g. `ST` at `$1A90`. | `$1A00-$1AFF` (the MMU swaps the two pages), e.g. `ST` at `$1A90`. | ||||
| After `mmu_shutdown` the CPU is back in BASIC's mapping, where bank-0 RAM at | |||||
| `$4000-$BFFF` is hidden by BASIC ROM. A program must finish reading its MAIN | |||||
| variables and buffers (and anything else above `$3FFF`) before it calls | |||||
| `mmu_shutdown`. Two test programs read wrong values this way before the rule | |||||
| was written down. | |||||
| ## Interrupts | ## Interrupts | ||||
| - **IRQ:** masked in both compiler states. The ROM IRQ path (`$FF17`) jumps via | - **IRQ:** masked in both compiler states. The ROM IRQ path (`$FF17`) jumps via | ||||
| @@ -0,0 +1,88 @@ | |||||
| --- | |||||
| story: E03-S01 | |||||
| owner: e03_toplevel_parser | |||||
| status: done | |||||
| started: 2026-11-07 | |||||
| depends-on: [E02-S02, E02-S03, E02-S04] | |||||
| --- | |||||
| # E03-S01 Claim | |||||
| ## Intended change | |||||
| Add `src/compiler/parser.s`: a two-pass declaration-level parser. Pass 1 | |||||
| parses interfaces, classes (with implements lists, fields, and method | |||||
| headers), and exactly one program, records top-level types in a bank-1 | |||||
| table, tracks block structure inside method and program bodies, and reports | |||||
| syntax and duplicate-name errors with recovery. Pass 2 replays the source | |||||
| and reports type references that name no declared interface or class, so | |||||
| forward references work. | |||||
| ## File area | |||||
| - `src/compiler/parser.s`, `src/common/parser.inc` | |||||
| - `src/common/diagcat.inc` (appended entries), `docs/diagnostics.md` | |||||
| - `tests/parser/` | |||||
| - `scripts/build-parser-test.ps1`, `scripts/verify-parser-test.ps1` | |||||
| - `docs/architecture.md`, `docs/memory-map.md` | |||||
| - `planning/claims/E03-S01.md` | |||||
| - generated files under `build/` | |||||
| ## Acceptance evidence | |||||
| - [x] Parses interfaces, classes, implements lists, and exactly one program: | |||||
| all 7 accepted fixtures give no diagnostics and the type table matches | |||||
| every interface/class/program declaration and line; `noprog.n8` reports | |||||
| PROGRAM EXPECTED; a second or late declaration is an ordering error. | |||||
| - [x] Supports forward top-level type references: `forward.n8` and | |||||
| `decls.n8` use types before declaring them and pass; `unknown.n8` reports | |||||
| exactly its four truly unknown names. Swapping the pass order (checking | |||||
| references first) fails 6 accepted fixtures, so pass 2 is what makes | |||||
| forward references work. | |||||
| - [x] Rejects duplicate names and invalid ordering with recovery to later | |||||
| lines: `order.n8` (late class, late duplicate interface) and `recover.n8` | |||||
| (five independent errors) match their full expected diagnostic lists | |||||
| exactly; `eofclass.n8` shows end-of-file errors are not cascaded. | |||||
| - [x] Accepted/rejected fixtures from E00-S02 pass: 7/7 accepted; 10/12 | |||||
| rejected report their sidecar diagnostic at the sidecar line (and column | |||||
| where given). `line_cont.n8` and `multi_stmt.n8` need expression and | |||||
| statement parsing, which E03-S03's own criteria cover; the test lists them | |||||
| as pending and confirms they raise no false diagnostics meanwhile. | |||||
| ## Coordination notes | |||||
| - Statement and expression bodies are skimmed line by line here (block | |||||
| keywords tracked). `line_cont.n8` and `multi_stmt.n8` need the statement | |||||
| and expression grammar that E03-S03 owns ("Rejects multiple statements per | |||||
| line and cross-line expressions"); the test lists them as pending E03-S03 | |||||
| and checks that they raise no false diagnostics meanwhile. | |||||
| - Member-name uniqueness and access rules are E03-S02. Parameters and `var` | |||||
| locals already share one duplicate-checked scope here because | |||||
| `dup_decl.n8` needs it. | |||||
| ## Work log | |||||
| - Appended diagnostics E031-E037 and E222 to the catalog; docs regenerated. | |||||
| - First x128 runs found: the bank-1 type table was never cleared (power-on | |||||
| bytes looked like declarations); the test's PASS branch fell through when | |||||
| `ok_msg - messages` was 0; and `method print()` was rejected because | |||||
| `print` is a keyword. The last is a spec conflict, resolved by D121 | |||||
| (proposed). | |||||
| - Verifier fix: PowerShell unrolls a one-element array returned from a | |||||
| function, so single-line ERR files read as one string. | |||||
| ## Completion | |||||
| - Tests run: `scripts/verify-parser-test.ps1` (pass plus pass-order mutation), | |||||
| `scripts/build-diag-test.ps1` catalog check. | |||||
| - Results: 24 fixtures as expected (2 pending E03-S03); mutation caught. | |||||
| - Files changed: `src/compiler/parser.s`, `src/common/parser.inc`, | |||||
| `src/common/diagcat.inc`, `docs/diagnostics.md`, `tests/parser/`, | |||||
| `scripts/build-parser-test.ps1`, `scripts/verify-parser-test.ps1`, | |||||
| `docs/architecture.md`, `docs/memory-map.md`, `docs/decisions.md` (D121), | |||||
| this claim. | |||||
| - Remaining risks: D121 awaits approval; pass-2 diagnostics come after all | |||||
| pass-1 diagnostics in NAME.ERR (not sorted by line); `implements` accepts a | |||||
| class name until interface conformance (E03-S06); statement lines are only | |||||
| skimmed, so a block keyword inside a skimmed expression is not possible | |||||
| today but must stay so when E03-S03 lands. | |||||
| @@ -0,0 +1,61 @@ | |||||
| # E03-S01: build the parser test PRG and a fresh D81 holding every grammar and | |||||
| # parser fixture. The fixture order must match tests/parser/parser-test.s. | |||||
| [CmdletBinding()] | |||||
| param() | |||||
| $ErrorActionPreference = "Stop" | |||||
| $project = Split-Path $PSScriptRoot -Parent | |||||
| Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force | |||||
| $out = Join-Path $project "build\parser-test" | |||||
| New-Item -ItemType Directory -Force -Path $out | Out-Null | |||||
| $g = Join-Path $project "tests\grammar" | |||||
| $p = Join-Path $project "tests\parser" | |||||
| # disk name -> @(source, group, expect file) | |||||
| $script:ParserFixtures = [ordered]@{ | |||||
| "minimal.n8" = @("$g\accepted\minimal.n8", "accepted", $null) | |||||
| "decls.n8" = @("$g\accepted\decls.n8", "accepted", $null) | |||||
| "arrays.n8" = @("$g\accepted\arrays.n8", "accepted", $null) | |||||
| "exprs.n8" = @("$g\accepted\exprs.n8", "accepted", $null) | |||||
| "forward.n8" = @("$g\accepted\forward.n8", "accepted", $null) | |||||
| "nulls.n8" = @("$g\accepted\nulls.n8", "accepted", $null) | |||||
| "stmts.n8" = @("$g\accepted\stmts.n8", "accepted", $null) | |||||
| "badchar.n8" = @("$g\rejected\bad_char.n8", "rejected", "$g\rejected\bad_char.expect") | |||||
| "badclass.n8" = @("$g\rejected\bad_class.n8", "rejected", "$g\rejected\bad_class.expect") | |||||
| "badiface.n8" = @("$g\rejected\bad_iface.n8", "rejected", "$g\rejected\bad_iface.expect") | |||||
| "badmethod.n8" = @("$g\rejected\bad_method.n8", "rejected", "$g\rejected\bad_method.expect") | |||||
| "dupdecl.n8" = @("$g\rejected\dup_decl.n8", "rejected", "$g\rejected\dup_decl.expect") | |||||
| "ifacebody.n8" = @("$g\rejected\iface_body.n8", "rejected", "$g\rejected\iface_body.expect") | |||||
| "linecont.n8" = @("$g\rejected\line_cont.n8", "pending", "$g\rejected\line_cont.expect") | |||||
| "missclass.n8" = @("$g\rejected\miss_class.n8", "rejected", "$g\rejected\miss_class.expect") | |||||
| "missend.n8" = @("$g\rejected\miss_end.n8", "rejected", "$g\rejected\miss_end.expect") | |||||
| "missif.n8" = @("$g\rejected\miss_if.n8", "rejected", "$g\rejected\miss_if.expect") | |||||
| "missmeth.n8" = @("$g\rejected\miss_meth.n8", "rejected", "$g\rejected\miss_meth.expect") | |||||
| "multistmt.n8" = @("$g\rejected\multi_stmt.n8", "pending", "$g\rejected\multi_stmt.expect") | |||||
| "order.n8" = @("$p\order.n8", "exact", "$p\order.expect") | |||||
| "unknown.n8" = @("$p\unknown.n8", "exact", "$p\unknown.expect") | |||||
| "recover.n8" = @("$p\recover.n8", "exact", "$p\recover.expect") | |||||
| "noprog.n8" = @("$p\noprog.n8", "exact", "$p\noprog.expect") | |||||
| "eofclass.n8" = @("$p\eofclass.n8", "exact", "$p\eofclass.expect") | |||||
| } | |||||
| $link = Invoke-Nova128Link -Name "parser-test" -BuildDir $out ` | |||||
| -Config (Join-Path $project "cfg\compiler.cfg") -Sources @( | |||||
| (Join-Path $project "src\compiler\mmu.s"), | |||||
| (Join-Path $project "src\compiler\diskio.s"), | |||||
| (Join-Path $project "src\compiler\source.s"), | |||||
| (Join-Path $project "src\compiler\lexer.s"), | |||||
| (Join-Path $project "src\compiler\names.s"), | |||||
| (Join-Path $project "src\compiler\diagnostics.s"), | |||||
| (Join-Path $project "src\compiler\parser.s"), | |||||
| (Join-Path $project "tests\parser\parser-test.s")) | |||||
| if ((Get-Nova128Label $link.Labels "test_entry") -ne 0x1C0D) { throw "test_entry is not at `$1C0D" } | |||||
| $files = @(@{ Name = "parser-test"; Path = $link.Program; Type = "prg" }) | |||||
| foreach ($name in $ParserFixtures.Keys) { | |||||
| $pet = Join-Path $out ($name -replace '\.n8$', '.pet') | |||||
| [IO.File]::WriteAllBytes($pet, (ConvertTo-Nova128Petscii (Get-Content -Raw -LiteralPath $ParserFixtures[$name][0]))) | |||||
| $files += @{ Name = $name; Path = $pet; Type = "seq" } | |||||
| } | |||||
| New-Nova128Disk -Path (Join-Path $out "parser-test.d81") -Files $files | |||||
| Write-Host ("built {0}; {1} fixtures on parser-test.d81" -f $link.Program, $ParserFixtures.Count) | |||||
| @@ -0,0 +1,122 @@ | |||||
| # E03-S01: run the parser over every fixture in x128 and check the ERR files | |||||
| # and the recorded top-level types against the fixtures' expectations. | |||||
| [CmdletBinding()] | |||||
| param([int]$Port = 6535, [int]$TimeoutSeconds = 300) | |||||
| $ErrorActionPreference = "Stop" | |||||
| $project = Split-Path $PSScriptRoot -Parent | |||||
| Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force | |||||
| . (Join-Path $PSScriptRoot "build-parser-test.ps1") # defines $ParserFixtures | |||||
| $out = Join-Path $project "build\parser-test" | |||||
| $disk = Join-Path $out "parser-test.d81" | |||||
| # Catalog: symbol -> number. | |||||
| $codes = @{} | |||||
| foreach ($line in Get-Content (Join-Path $project "src\common\diagcat.inc")) { | |||||
| $m = [regex]::Match($line, '^\s*DIAGNOSTIC\s+(\w+),\s*(\d+),') | |||||
| if ($m.Success) { $codes[$m.Groups[1].Value] = [int]$m.Groups[2].Value } | |||||
| } | |||||
| $session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port ` | |||||
| -Screenshot (Join-Path $out "parser-test-vicii.png") | |||||
| try { | |||||
| $mb = Wait-Nova128Mailbox $session -TimeoutSeconds $TimeoutSeconds | |||||
| $dump = Read-Nova128Memory $session ($mb[8] + 256 * $mb[9]) ($mb[10] + 256 * $mb[11]) | |||||
| } finally { | |||||
| Stop-Nova128X128 $session | |||||
| } | |||||
| if ($mb[3] -ne 0xA5) { throw ("C128 reported FAIL (code `${0:X2})" -f $mb[4]) } | |||||
| # ------------------------------------------------------------ decode dump | |||||
| $records = @(); $p = 0 | |||||
| while ($dump[$p] -ne 0xFE) { | |||||
| if ($dump[$p] -ne 0xFF) { throw "dump out of sync at $p" } | |||||
| $r = [pscustomobject]@{ Index = $dump[$p + 1]; Load = $dump[$p + 2]; Count = $dump[$p + 3]; Types = @() } | |||||
| $p += 4 | |||||
| while ($dump[$p] -ne 0xFD) { | |||||
| $len = $dump[$p + 3] | |||||
| $spell = -join ($dump[($p + 4)..($p + 3 + $len)] | ForEach-Object { | |||||
| if ($_ -ge 0x41 -and $_ -le 0x5A) { [char]($_ + 0x20) } elseif ($_ -ge 0xC1 -and $_ -le 0xDA) { [char]($_ - 0x80) } else { [char]$_ } }) | |||||
| $r.Types += ("{0} {1} {2}" -f @("", "interface", "class", "program")[$dump[$p]], $spell, ($dump[$p + 1] + 256 * $dump[$p + 2])) | |||||
| $p += 4 + $len | |||||
| } | |||||
| $p++ | |||||
| $records += $r | |||||
| } | |||||
| $c1541 = Get-ViceTool "c1541" | |||||
| $listing = (& $c1541 -attach $disk -list 2>&1) | Out-String | |||||
| function Get-ErrLines([string]$base) { | |||||
| $dest = Join-Path $out "$base.err.bin" | |||||
| if (Test-Path $dest) { Remove-Item $dest -Force } | |||||
| if ($listing -notmatch ('"' + [regex]::Escape("$base.err") + '"')) { return $null } | |||||
| & $c1541 -attach $disk -read "$base.err,s" $dest 2>&1 | Out-Null | |||||
| # The leading comma keeps a one-line file an array (PowerShell unrolls it). | |||||
| return ,@(([Text.Encoding]::ASCII.GetString([IO.File]::ReadAllBytes($dest)).TrimEnd("`r")) -split "`r") | |||||
| } | |||||
| function Parse-Err([string]$line) { | |||||
| $m = [regex]::Match($line, '^E(\d{3}) \S+ (\d+) (\d+) ') | |||||
| return [pscustomobject]@{ Code = [int]$m.Groups[1].Value; Line = [int]$m.Groups[2].Value; Col = [int]$m.Groups[3].Value } | |||||
| } | |||||
| # ---------------------------------------------------------------- check | |||||
| $failures = @(); $i = 0; $pending = @() | |||||
| foreach ($name in $ParserFixtures.Keys) { | |||||
| $src, $group, $expect = $ParserFixtures[$name] | |||||
| $base = $name -replace '\.n8$', '' | |||||
| $r = $records[$i]; $i++ | |||||
| $errLines = Get-ErrLines $base | |||||
| $problems = @() | |||||
| if ($r.Load -ne 0) { $problems += "src_load returned $($r.Load)" } | |||||
| switch ($group) { | |||||
| "accepted" { | |||||
| if ($r.Count -ne 0) { $problems += "$($r.Count) diagnostic(s): $($errLines -join ' | ')" } | |||||
| if ($null -ne $errLines) { $problems += "an ERR file exists" } | |||||
| $want = @() | |||||
| $n = 0 | |||||
| foreach ($line in Get-Content -LiteralPath $src) { | |||||
| $n++ | |||||
| $m = [regex]::Match($line, '^\s*(interface|class|program)\s+(\w+)') | |||||
| if ($m.Success) { $want += ("{0} {1} {2}" -f $m.Groups[1].Value, $m.Groups[2].Value, $n) } | |||||
| } | |||||
| if ((($r.Types | Sort-Object) -join ';') -ne (($want | Sort-Object) -join ';')) { | |||||
| $problems += "types [$($r.Types -join '; ')] expected [$($want -join '; ')]" | |||||
| } | |||||
| $detail = "{0} types" -f $r.Types.Count | |||||
| } | |||||
| "rejected" { | |||||
| $text = Get-Content -Raw -LiteralPath $expect | |||||
| $sym = [regex]::Match($text, '(?m)^diagnostic: (\w+)').Groups[1].Value | |||||
| $wantLine = [int][regex]::Match($text, '(?m)^line: (\d+)').Groups[1].Value | |||||
| $colMatch = [regex]::Match($text, '(?m)^column: (\d+)') | |||||
| if ($null -eq $errLines) { $problems += "no ERR file" } | |||||
| else { | |||||
| $first = Parse-Err $errLines[0] | |||||
| if ($first.Code -ne $codes[$sym] -or $first.Line -ne $wantLine -or | |||||
| ($colMatch.Success -and $first.Col -ne [int]$colMatch.Groups[1].Value)) { | |||||
| $problems += "first diagnostic '$($errLines[0])' expected $sym (E$($codes[$sym])) at line $wantLine" | |||||
| } | |||||
| } | |||||
| $detail = if ($errLines) { $errLines[0] } else { "" } | |||||
| } | |||||
| "pending" { | |||||
| if ($r.Count -ne 0) { $problems += "false diagnostic(s) before E03-S03: $($errLines -join ' | ')" } | |||||
| $pending += $name | |||||
| $detail = "pending E03-S03 (statement grammar); no false errors" | |||||
| } | |||||
| "exact" { | |||||
| $want = @(Get-Content -LiteralPath $expect | Where-Object { $_ -and -not $_.StartsWith("//") } | ForEach-Object { | |||||
| $f = $_ -split ' '; "{0} {1} {2}" -f $codes[$f[0]], $f[1], $f[2] }) | |||||
| $got = @($errLines | Where-Object { $_ } | ForEach-Object { $e = Parse-Err $_; "{0} {1} {2}" -f $e.Code, $e.Line, $e.Col }) | |||||
| if (($got -join ';') -ne ($want -join ';')) { | |||||
| $problems += "diagnostics [$($got -join '; ')] expected [$($want -join '; ')]" | |||||
| } | |||||
| $detail = "{0} diagnostics" -f $got.Count | |||||
| } | |||||
| } | |||||
| $status = if ($problems) { "FAIL" } else { "ok" } | |||||
| Write-Host (" {0,-13} {1,-9} {2} {3}" -f $name, $group, $status, $detail) | |||||
| if ($problems) { $failures += "${name}: " + ($problems -join "; ") } | |||||
| } | |||||
| if ($failures) { throw ("parser verification failed:`n " + ($failures -join "`n ")) } | |||||
| Write-Host ("verified parser: {0} fixtures ({1} pending E03-S03)" -f $ParserFixtures.Count, $pending.Count) | |||||
| @@ -40,6 +40,13 @@ | |||||
| DIAGNOSTIC SYN_EXPECTED_INTERFACE_NAME, 28, "INTERFACE NAME EXPECTED" | DIAGNOSTIC SYN_EXPECTED_INTERFACE_NAME, 28, "INTERFACE NAME EXPECTED" | ||||
| DIAGNOSTIC SYN_INTERFACE_METHOD_BODY, 29, "INTERFACE METHODS HAVE NO BODY" | DIAGNOSTIC SYN_INTERFACE_METHOD_BODY, 29, "INTERFACE METHODS HAVE NO BODY" | ||||
| DIAGNOSTIC SYN_UNEXPECTED_TOKEN, 30, "% IS NOT ALLOWED HERE" | DIAGNOSTIC SYN_UNEXPECTED_TOKEN, 30, "% IS NOT ALLOWED HERE" | ||||
| DIAGNOSTIC SYN_EXPECTED_PROGRAM, 31, "PROGRAM DECLARATION EXPECTED" | |||||
| DIAGNOSTIC SYN_EXPECTED_END_INTERFACE, 32, "END INTERFACE EXPECTED" | |||||
| DIAGNOSTIC SYN_EXPECTED_LPAREN, 33, "'(' EXPECTED" | |||||
| DIAGNOSTIC SYN_EXPECTED_AS, 34, "AS EXPECTED" | |||||
| DIAGNOSTIC SYN_EXPECTED_TYPE, 35, "TYPE EXPECTED" | |||||
| DIAGNOSTIC SYN_EXPECTED_END_WHILE, 36, "END WHILE EXPECTED" | |||||
| DIAGNOSTIC SYN_EXPECTED_END_FOR, 37, "END FOR EXPECTED" | |||||
| ; -------------------------------------------- names and declarations | ; -------------------------------------------- names and declarations | ||||
| DIAGNOSTIC SEM_DUPLICATE_NAME, 60, "% IS ALREADY DECLARED" | DIAGNOSTIC SEM_DUPLICATE_NAME, 60, "% IS ALREADY DECLARED" | ||||
| @@ -66,3 +73,4 @@ | |||||
| ; ------------------------------------------------------- resource limits | ; ------------------------------------------------------- resource limits | ||||
| DIAGNOSTIC RES_TOO_MANY_NAMES, 220, "MORE THAN 255 NAMES" | DIAGNOSTIC RES_TOO_MANY_NAMES, 220, "MORE THAN 255 NAMES" | ||||
| DIAGNOSTIC RES_ERROR_LIMIT, 221, "TOO MANY ERRORS; COMPILATION STOPPED" | DIAGNOSTIC RES_ERROR_LIMIT, 221, "TOO MANY ERRORS; COMPILATION STOPPED" | ||||
| DIAGNOSTIC RES_NESTING_TOO_DEEP, 222, "BLOCKS NESTED TOO DEEPLY" | |||||
| @@ -0,0 +1,13 @@ | |||||
| ; parser.inc - top-level type table and parser limits (E03-S01). | |||||
| .ifndef PARSER_INC | |||||
| PARSER_INC = 1 | |||||
| TYPE_NONE = 0 | |||||
| TYPE_INTERFACE = 1 | |||||
| TYPE_CLASS = 2 | |||||
| TYPE_PROGRAM = 3 | |||||
| PARSE_MAX_DEPTH = 16 ; nested method/program/if/while/for blocks | |||||
| .endif | |||||
| @@ -0,0 +1,808 @@ | |||||
| ; parser.s - top-level declaration parser (E03-S01). | |||||
| ; | |||||
| ; parse_file runs two passes over the loaded source (call after src_load, | |||||
| ; diag_init, and names_init): | |||||
| ; pass 1 parses interfaces, classes (implements lists, fields, method | |||||
| ; headers), and the one program; records each top-level type in | |||||
| ; the bank-1 type table; reports syntax, ordering, and duplicate | |||||
| ; errors. | |||||
| ; pass 2 replays the source and reports only type references that do | |||||
| ; not name a declared interface or class (forward references are | |||||
| ; therefore legal). | |||||
| ; Method and program bodies are parsed line by line: `var` declarations are | |||||
| ; parsed and duplicate-checked; if/while/for/end blocks are tracked; other | |||||
| ; statement lines are skimmed until E03-S03 parses them. | |||||
| ; | |||||
| ; Recovery: an error skips the rest of its line. A mismatched `end X` | |||||
| ; reports the innermost missing end and closes blocks up to X. A top-level | |||||
| ; keyword or end of file inside a declaration reports the missing end once. | |||||
| ; Errors at end of file are reported at (last line + 1, column 1). | |||||
| ; If diag_report hits the error limit, parse_file returns at once. | |||||
| .setcpu "6502" | |||||
| .include "../common/tokens.inc" | |||||
| .include "../common/errors.inc" | |||||
| .include "../common/parser.inc" | |||||
| .import lex_reset, lex_next | |||||
| .import tok_kind, tok_line, tok_col, tok_len, tok_err | |||||
| .import src_line | |||||
| .import name_intern, name_declare | |||||
| .import diag_report, diag_line, diag_col, diag_arg1, diag_arg1_len | |||||
| .import mmu_peek1, mmu_poke1 | |||||
| .importzp mmu_ptr | |||||
| .export parse_file, type_kind, type_line_lo, type_line_hi | |||||
| .macro IFTOK kind, target | |||||
| lda tok_kind | |||||
| cmp #kind | |||||
| beq target | |||||
| .endmacro | |||||
| ; ------------------------------------------------------------------ CODE | |||||
| .segment "CODE" | |||||
| parse_file: | |||||
| tsx | |||||
| stx parse_sp | |||||
| jsr tt_clear ; bank-1 BSS is not zeroed at load | |||||
| lda #$01 | |||||
| sta pass | |||||
| jsr parse_pass | |||||
| lda #$02 | |||||
| sta pass | |||||
| jsr parse_pass | |||||
| rts | |||||
| ; Error limit reached: return straight to parse_file's caller. | |||||
| stop_parse: | |||||
| ldx parse_sp | |||||
| txs | |||||
| rts | |||||
| parse_pass: | |||||
| jsr lex_reset | |||||
| lda #$00 | |||||
| sta program_seen | |||||
| sta pending_end | |||||
| sta eof_reported | |||||
| jsr next | |||||
| top: jsr skip_blank | |||||
| lda pending_end ; stray `end X` left by a body: reported | |||||
| beq :+ | |||||
| lda #$00 | |||||
| sta pending_end | |||||
| jsr skip_line | |||||
| jmp top | |||||
| : IFTOK TOK_EOF, @eof | |||||
| IFTOK KW_INTERFACE, @iface | |||||
| IFTOK KW_CLASS, @class | |||||
| IFTOK KW_PROGRAM, @program | |||||
| jsr arg_token ; anything else at top level | |||||
| lda #E_SYN_UNEXPECTED_TOKEN | |||||
| jsr err1 | |||||
| jsr skip_line | |||||
| jmp top | |||||
| @eof: lda program_seen | |||||
| bne @done | |||||
| lda #E_SYN_EXPECTED_PROGRAM | |||||
| jsr err_eof | |||||
| @done: rts | |||||
| @iface: jsr check_order | |||||
| jsr parse_interface | |||||
| jmp top | |||||
| @class: jsr check_order | |||||
| jsr parse_class | |||||
| jmp top | |||||
| @program: | |||||
| jsr check_order | |||||
| jsr parse_program | |||||
| jmp top | |||||
| ; Declarations after the program are out of order (still parsed). | |||||
| check_order: | |||||
| lda program_seen | |||||
| beq :+ | |||||
| jsr arg_token | |||||
| lda #E_SYN_UNEXPECTED_TOKEN | |||||
| jmp err1 | |||||
| : rts | |||||
| ; ----------------------------------------------------------- interface | |||||
| parse_interface: | |||||
| jsr next | |||||
| lda tok_kind | |||||
| cmp #TOK_IDENT | |||||
| beq :+ | |||||
| lda #E_SYN_EXPECTED_IDENTIFIER | |||||
| jsr err1 | |||||
| jsr skip_line | |||||
| jmp @members | |||||
| : lda #TYPE_INTERFACE | |||||
| jsr declare_type | |||||
| jsr next | |||||
| jsr expect_eol | |||||
| @members: | |||||
| lda #$00 | |||||
| sta body_err | |||||
| @loop: jsr skip_blank | |||||
| IFTOK TOK_EOF, @eof | |||||
| jsr at_toplevel_kw | |||||
| beq @missing | |||||
| IFTOK KW_END, @end | |||||
| lda body_err | |||||
| bne @skip | |||||
| IFTOK KW_METHOD, @method | |||||
| lda tok_kind | |||||
| cmp #KW_PUBLIC | |||||
| beq @access | |||||
| cmp #KW_PRIVATE | |||||
| bne @body | |||||
| @access: | |||||
| jsr arg_token ; interfaces have no access modifiers | |||||
| lda #E_SYN_UNEXPECTED_TOKEN | |||||
| jsr err1 | |||||
| jmp @skip | |||||
| @body: lda #E_SYN_INTERFACE_METHOD_BODY | |||||
| jsr err1 | |||||
| lda #$01 | |||||
| sta body_err | |||||
| @skip: jsr skip_line | |||||
| jmp @loop | |||||
| @method: | |||||
| jsr parse_signature | |||||
| jmp @loop | |||||
| @end: jsr next | |||||
| IFTOK KW_INTERFACE, @close | |||||
| lda body_err ; `end method` closes a reported body | |||||
| bne :+ | |||||
| lda #E_SYN_INTERFACE_METHOD_BODY | |||||
| jsr err1 | |||||
| : lda #$00 | |||||
| sta body_err | |||||
| jsr skip_line | |||||
| jmp @loop | |||||
| @close: jsr next | |||||
| jmp expect_eol | |||||
| @eof: lda #E_SYN_EXPECTED_END_INTERFACE | |||||
| jmp err_eof | |||||
| @missing: | |||||
| lda #E_SYN_EXPECTED_END_INTERFACE | |||||
| jmp err1 | |||||
| ; --------------------------------------------------------------- class | |||||
| parse_class: | |||||
| jsr next | |||||
| lda tok_kind | |||||
| cmp #TOK_IDENT | |||||
| beq :+ | |||||
| lda #E_SYN_EXPECTED_IDENTIFIER | |||||
| jsr err1 | |||||
| jsr skip_line | |||||
| jmp @members | |||||
| : lda #TYPE_CLASS | |||||
| jsr declare_type | |||||
| jsr next | |||||
| lda tok_kind | |||||
| cmp #KW_IMPLEMENTS | |||||
| bne @header_end | |||||
| jsr next | |||||
| @impl: lda tok_kind | |||||
| cmp #TOK_IDENT | |||||
| beq :+ | |||||
| lda #E_SYN_EXPECTED_INTERFACE_NAME | |||||
| jsr err1 | |||||
| jsr skip_line | |||||
| jmp @members | |||||
| : jsr type_ref | |||||
| jsr next | |||||
| lda tok_kind | |||||
| cmp #TOK_COMMA | |||||
| bne @header_end | |||||
| jsr next | |||||
| jmp @impl | |||||
| @header_end: | |||||
| jsr expect_eol | |||||
| @members: | |||||
| @loop: lda pending_end ; `end X` seen inside a method body | |||||
| beq @scan | |||||
| lda tok_kind | |||||
| cmp #KW_CLASS | |||||
| bne @lost | |||||
| lda #$00 | |||||
| sta pending_end | |||||
| jsr next | |||||
| jmp expect_eol | |||||
| @lost: lda #E_SYN_EXPECTED_END_CLASS | |||||
| jmp err1 | |||||
| @scan: jsr skip_blank | |||||
| IFTOK TOK_EOF, @eof | |||||
| jsr at_toplevel_kw | |||||
| beq @missing | |||||
| IFTOK KW_END, @end | |||||
| lda tok_kind | |||||
| cmp #KW_PUBLIC | |||||
| beq @access | |||||
| cmp #KW_PRIVATE | |||||
| bne @member | |||||
| @access: | |||||
| jsr next | |||||
| @member: | |||||
| IFTOK KW_METHOD, @method | |||||
| jsr is_type_start | |||||
| bcc @stray | |||||
| jsr parse_type ; field: type name | |||||
| bcs @skip | |||||
| lda tok_kind | |||||
| cmp #TOK_IDENT | |||||
| beq :+ | |||||
| lda #E_SYN_EXPECTED_IDENTIFIER | |||||
| jsr err1 | |||||
| jmp @skip | |||||
| : jsr intern_tok | |||||
| jsr next | |||||
| jsr expect_eol | |||||
| jmp @loop | |||||
| @method: | |||||
| jsr parse_signature ; a bad header still has a body | |||||
| lda #KW_METHOD | |||||
| jsr parse_body | |||||
| jmp @loop | |||||
| @stray: jsr arg_token | |||||
| lda #E_SYN_UNEXPECTED_TOKEN | |||||
| jsr err1 | |||||
| @skip: jsr skip_line | |||||
| jmp @loop | |||||
| @end: jsr next | |||||
| IFTOK KW_CLASS, @close | |||||
| jsr arg_token | |||||
| lda #E_SYN_UNEXPECTED_TOKEN | |||||
| jsr err1 | |||||
| jmp @skip | |||||
| @close: jsr next | |||||
| jmp expect_eol | |||||
| @eof: lda #E_SYN_EXPECTED_END_CLASS | |||||
| jmp err_eof | |||||
| @missing: | |||||
| lda #E_SYN_EXPECTED_END_CLASS | |||||
| jmp err1 | |||||
| ; ------------------------------------------------------------- program | |||||
| parse_program: | |||||
| jsr next | |||||
| lda tok_kind | |||||
| cmp #TOK_IDENT | |||||
| beq :+ | |||||
| lda #E_SYN_EXPECTED_IDENTIFIER | |||||
| jsr err1 | |||||
| jsr skip_line | |||||
| jmp @body | |||||
| : lda #TYPE_PROGRAM | |||||
| jsr declare_type | |||||
| jsr next | |||||
| jsr expect_eol | |||||
| @body: jsr scope_clear | |||||
| lda #KW_PROGRAM | |||||
| jsr parse_body | |||||
| lda #$01 | |||||
| sta program_seen | |||||
| rts | |||||
| ; ------------------------------------------------------ method headers | |||||
| ; method name ( [name as type {, name as type}] ) [as type] EOL | |||||
| ; Starts a new local scope holding the parameters. C=1 if the header had | |||||
| ; an error (the line has been skipped). | |||||
| parse_signature: | |||||
| jsr scope_clear | |||||
| jsr next | |||||
| lda tok_kind ; member names may be keywords (D121), | |||||
| cmp #TOK_IDENT ; e.g. `method print()` in spec 5.1 | |||||
| beq :+ | |||||
| cmp #TOK_KW_FIRST | |||||
| bcs :+ | |||||
| lda #E_SYN_EXPECTED_IDENTIFIER | |||||
| jmp @fail | |||||
| : jsr intern_tok | |||||
| jsr next | |||||
| lda tok_kind | |||||
| cmp #TOK_LPAREN | |||||
| beq :+ | |||||
| lda #E_SYN_EXPECTED_LPAREN | |||||
| jmp @fail | |||||
| : jsr next | |||||
| IFTOK TOK_RPAREN, @close | |||||
| @param: lda tok_kind | |||||
| cmp #TOK_IDENT | |||||
| beq :+ | |||||
| lda #E_SYN_EXPECTED_IDENTIFIER | |||||
| jmp @fail | |||||
| : jsr declare_local | |||||
| jsr next | |||||
| lda tok_kind | |||||
| cmp #KW_AS | |||||
| beq :+ | |||||
| lda #E_SYN_EXPECTED_AS | |||||
| jmp @fail | |||||
| : jsr next | |||||
| jsr parse_type | |||||
| bcs @skip | |||||
| IFTOK TOK_COMMA, @more | |||||
| IFTOK TOK_RPAREN, @close | |||||
| lda #E_SYN_EXPECTED_RPAREN | |||||
| jmp @fail | |||||
| @more: jsr next | |||||
| jmp @param | |||||
| @close: jsr next | |||||
| lda tok_kind | |||||
| cmp #KW_AS | |||||
| bne @eol | |||||
| jsr next | |||||
| jsr parse_type | |||||
| bcs @skip | |||||
| @eol: jsr expect_eol | |||||
| clc | |||||
| rts | |||||
| @fail: jsr err1 | |||||
| @skip: jsr skip_line | |||||
| sec | |||||
| rts | |||||
| ; ---------------------------------------------------------------- types | |||||
| ; primitive | Name, optionally [number]. C=1 after reporting an error. | |||||
| parse_type: | |||||
| jsr is_type_start | |||||
| bcs :+ | |||||
| lda #E_SYN_EXPECTED_TYPE | |||||
| jsr err1 | |||||
| sec | |||||
| rts | |||||
| : lda tok_kind | |||||
| cmp #TOK_IDENT | |||||
| bne :+ | |||||
| jsr type_ref | |||||
| : jsr next | |||||
| lda tok_kind | |||||
| cmp #TOK_LBRACKET | |||||
| bne @ok | |||||
| jsr next | |||||
| lda tok_kind | |||||
| cmp #TOK_NUMBER | |||||
| bne @bad | |||||
| jsr next | |||||
| lda tok_kind | |||||
| cmp #TOK_RBRACKET | |||||
| bne @bad | |||||
| jsr next | |||||
| @ok: clc | |||||
| rts | |||||
| @bad: jsr arg_token | |||||
| lda #E_SYN_UNEXPECTED_TOKEN | |||||
| jsr err1 | |||||
| sec | |||||
| rts | |||||
| ; C=1 when the token can start a type. | |||||
| is_type_start: | |||||
| lda tok_kind | |||||
| ldx #prim_count ; primitives, then TOK_IDENT | |||||
| @find: cmp prim_kinds,x | |||||
| beq @yes | |||||
| dex | |||||
| bpl @find | |||||
| clc | |||||
| rts | |||||
| @yes: sec | |||||
| rts | |||||
| ; Pass 2: the identifier must name a declared interface or class. | |||||
| type_ref: | |||||
| jsr intern_tok | |||||
| beq @done | |||||
| ldx pass | |||||
| cpx #$02 | |||||
| bne @done | |||||
| tay | |||||
| lda #0 ; type_kind table | |||||
| jsr tt_get | |||||
| cmp #TYPE_INTERFACE | |||||
| beq @done | |||||
| cmp #TYPE_CLASS | |||||
| beq @done | |||||
| jsr arg_token | |||||
| lda #E_SEM_UNKNOWN_NAME | |||||
| jsr err2 | |||||
| @done: rts | |||||
| ; ---------------------------------------------------------------- bodies | |||||
| ; Parse a method or program body. A = KW_METHOD or KW_PROGRAM. | |||||
| parse_body: | |||||
| sta block_stack | |||||
| lda #$01 | |||||
| sta depth | |||||
| @loop: jsr skip_blank | |||||
| lda tok_kind | |||||
| cmp #TOK_EOF | |||||
| bne :+ | |||||
| jmp @eof | |||||
| : jsr at_toplevel_kw | |||||
| bne :+ | |||||
| jmp @interrupted | |||||
| : IFTOK KW_VAR, @var | |||||
| IFTOK KW_IF, @open | |||||
| IFTOK KW_WHILE, @open | |||||
| IFTOK KW_FOR, @open | |||||
| IFTOK KW_END, @end | |||||
| jsr skip_line ; statement: E03-S03 parses these | |||||
| jmp @loop | |||||
| @var: jsr parse_var | |||||
| jmp @loop | |||||
| @open: ldx depth | |||||
| cpx #PARSE_MAX_DEPTH | |||||
| bcc :+ | |||||
| lda #E_RES_NESTING_TOO_DEEP | |||||
| jsr err1 | |||||
| jsr skip_line | |||||
| jmp @loop | |||||
| : lda tok_kind | |||||
| sta block_stack,x | |||||
| inc depth | |||||
| jsr skip_line ; condition / loop header: E03-S03 | |||||
| jmp @loop | |||||
| @end: jsr next ; the closer keyword | |||||
| lda tok_kind | |||||
| ldx depth | |||||
| cmp block_stack-1,x | |||||
| beq @matched | |||||
| jsr closer_in_stack ; X = depth of the matching block or 0 | |||||
| cpx #$00 | |||||
| bne @unwind | |||||
| cmp #KW_CLASS ; closes an outer declaration? | |||||
| beq @outer | |||||
| cmp #KW_INTERFACE | |||||
| beq @outer | |||||
| jsr arg_token ; `end method` in a program, `end foo`... | |||||
| lda #E_SYN_UNEXPECTED_TOKEN | |||||
| jsr err1 | |||||
| jsr skip_line | |||||
| jmp @loop | |||||
| @outer: jsr report_missing_end | |||||
| lda tok_kind | |||||
| sta pending_end ; the class/interface parser closes it | |||||
| lda #$00 | |||||
| sta depth | |||||
| rts | |||||
| @unwind: | |||||
| stx unwind_to ; inner blocks were never closed | |||||
| jsr report_missing_end | |||||
| lda unwind_to | |||||
| sta depth | |||||
| @matched: | |||||
| dec depth | |||||
| jsr next | |||||
| jsr expect_eol | |||||
| lda depth | |||||
| beq :+ | |||||
| jmp @loop | |||||
| : rts | |||||
| @eof: jsr missing_end_code | |||||
| jsr err_eof | |||||
| lda #$00 | |||||
| sta depth | |||||
| rts | |||||
| @interrupted: | |||||
| jsr report_missing_end | |||||
| lda #$00 | |||||
| sta depth | |||||
| rts | |||||
| ; X = 1-based depth of the innermost block opened by tok_kind, or 0. A | |||||
| ; foreign `end class` / `end interface` reports the innermost block only. | |||||
| closer_in_stack: | |||||
| ldx depth | |||||
| @scan: cmp block_stack-1,x | |||||
| beq @found | |||||
| dex | |||||
| bne @scan | |||||
| @found: rts | |||||
| report_missing_end: | |||||
| jsr missing_end_code | |||||
| jmp err1 | |||||
| ; A = E_ code for the innermost open block. | |||||
| missing_end_code: | |||||
| ldx depth | |||||
| lda block_stack-1,x | |||||
| ldx #end_count - 1 | |||||
| @find: cmp end_blocks,x | |||||
| beq @found | |||||
| dex | |||||
| bpl @find | |||||
| ldx #$00 | |||||
| @found: lda end_codes,x | |||||
| rts | |||||
| ; var name as type EOL | |||||
| parse_var: | |||||
| jsr next | |||||
| lda tok_kind | |||||
| cmp #TOK_IDENT | |||||
| beq :+ | |||||
| lda #E_SYN_EXPECTED_IDENTIFIER | |||||
| jsr err1 | |||||
| jmp skip_line | |||||
| : jsr declare_local | |||||
| jsr next | |||||
| lda tok_kind | |||||
| cmp #KW_AS | |||||
| beq :+ | |||||
| lda #E_SYN_EXPECTED_AS | |||||
| jsr err1 | |||||
| jmp skip_line | |||||
| : jsr next | |||||
| jsr parse_type | |||||
| bcc :+ | |||||
| jmp skip_line | |||||
| : jmp expect_eol | |||||
| ; --------------------------------------------------------- declarations | |||||
| ; Record the IDENT token as a top-level type of kind A (pass 1). | |||||
| declare_type: | |||||
| sta dk_kind | |||||
| jsr intern_tok | |||||
| beq @done | |||||
| sta dk_id | |||||
| ldx pass | |||||
| cpx #$01 | |||||
| bne @done | |||||
| lda dk_id | |||||
| ldy tok_col | |||||
| ldx tok_len | |||||
| jsr name_declare | |||||
| ldy dk_id | |||||
| lda #0 | |||||
| jsr tt_get | |||||
| beq @new | |||||
| jsr arg_token | |||||
| lda #E_SEM_DUPLICATE_NAME | |||||
| jmp err1 | |||||
| @new: ldy dk_id | |||||
| lda #0 | |||||
| ldx dk_kind | |||||
| jsr tt_set | |||||
| ldy dk_id | |||||
| lda #1 | |||||
| ldx tok_line | |||||
| jsr tt_set | |||||
| ldy dk_id | |||||
| lda #2 | |||||
| ldx tok_line+1 | |||||
| jsr tt_set | |||||
| @done: rts | |||||
| ; Add the IDENT token to the current local scope; report a duplicate. | |||||
| declare_local: | |||||
| jsr intern_tok | |||||
| beq @done | |||||
| tax | |||||
| lsr a ; byte = id / 8, bit = id & 7 | |||||
| lsr a | |||||
| lsr a | |||||
| tay | |||||
| txa | |||||
| and #$07 | |||||
| tax | |||||
| lda bit_masks,x | |||||
| and scope_bits,y | |||||
| bne @dup | |||||
| lda bit_masks,x | |||||
| ora scope_bits,y | |||||
| sta scope_bits,y | |||||
| @done: rts | |||||
| @dup: jsr arg_token | |||||
| lda #E_SEM_DUPLICATE_NAME | |||||
| jmp err1 | |||||
| scope_clear: | |||||
| lda #$00 | |||||
| ldx #31 | |||||
| : sta scope_bits,x | |||||
| dex | |||||
| bpl :- | |||||
| rts | |||||
| ; Intern the IDENT token. A = ID (Z=0), or A = 0 after a full-table error. | |||||
| intern_tok: | |||||
| lda tok_col | |||||
| ldx tok_len | |||||
| jsr name_intern | |||||
| bcc :+ | |||||
| lda #E_RES_TOO_MANY_NAMES | |||||
| jsr err1 | |||||
| lda #$00 | |||||
| : rts | |||||
| ; Zero all three type-table columns. | |||||
| tt_clear: | |||||
| lda #2 | |||||
| sta dk_kind ; column counter (reused scratch) | |||||
| @col: ldy #$00 | |||||
| @byte: lda dk_kind | |||||
| ldx #$00 | |||||
| jsr tt_set | |||||
| iny | |||||
| bne @byte | |||||
| dec dk_kind | |||||
| bpl @col | |||||
| rts | |||||
| ; Type table in bank 1: A = column (0 kind, 1 line lo, 2 line hi), Y = ID. | |||||
| tt_get: jsr tt_ptr | |||||
| jmp mmu_peek1 | |||||
| tt_set: jsr tt_ptr | |||||
| txa | |||||
| jmp mmu_poke1 | |||||
| tt_ptr: clc | |||||
| adc #>type_kind | |||||
| sta mmu_ptr+1 | |||||
| lda #<type_kind | |||||
| sta mmu_ptr | |||||
| rts | |||||
| ; ---------------------------------------------------------------- tokens | |||||
| ; Advance, reporting lexer errors (pass 1); the lexer then yields EOL. | |||||
| next: jsr lex_next | |||||
| lda tok_kind | |||||
| cmp #TOK_ERROR | |||||
| bne :+ | |||||
| lda tok_err ; LEXERR_n -> E_LEX_INVALID_CHAR + n - 1 | |||||
| clc | |||||
| adc #E_LEX_INVALID_CHAR - LEXERR_INVALID_CHAR | |||||
| jsr err1 | |||||
| jmp next | |||||
| : rts | |||||
| skip_blank: | |||||
| lda tok_kind | |||||
| cmp #TOK_EOL | |||||
| bne :+ | |||||
| jsr next | |||||
| jmp skip_blank | |||||
| : rts | |||||
| ; Skip to the end of the line and past its EOL (not past EOF). | |||||
| skip_line: | |||||
| lda tok_kind | |||||
| cmp #TOK_EOF | |||||
| beq @done | |||||
| cmp #TOK_EOL | |||||
| beq @eol | |||||
| jsr next | |||||
| jmp skip_line | |||||
| @eol: jsr next | |||||
| @done: rts | |||||
| expect_eol: | |||||
| lda tok_kind | |||||
| cmp #TOK_EOL | |||||
| beq @eol | |||||
| cmp #TOK_EOF | |||||
| beq @done | |||||
| lda #E_SYN_EXPECTED_EOL | |||||
| jsr err1 | |||||
| jmp skip_line | |||||
| @eol: jmp next | |||||
| @done: rts | |||||
| ; Z=1 when the token is interface/class/program (a new declaration). | |||||
| at_toplevel_kw: | |||||
| lda tok_kind | |||||
| cmp #KW_INTERFACE | |||||
| beq @yes | |||||
| cmp #KW_CLASS | |||||
| beq @yes | |||||
| cmp #KW_PROGRAM | |||||
| @yes: rts | |||||
| ; ----------------------------------------------------------- reporting | |||||
| ; diag_arg1 = the current token's source spelling (at most 16 bytes). | |||||
| arg_token: | |||||
| clc | |||||
| lda #<(src_line - 1) | |||||
| adc tok_col | |||||
| sta diag_arg1 | |||||
| lda #>(src_line - 1) | |||||
| adc #$00 | |||||
| sta diag_arg1+1 | |||||
| lda tok_len | |||||
| cmp #17 | |||||
| bcc :+ | |||||
| lda #16 | |||||
| : sta diag_arg1_len | |||||
| rts | |||||
| ; Report A at the current token in pass 1 / pass 2 only. | |||||
| err1: ldx pass | |||||
| cpx #$01 | |||||
| beq report | |||||
| bne drop | |||||
| err2: ldx pass | |||||
| cpx #$02 | |||||
| beq report | |||||
| drop: ldx #$00 | |||||
| stx diag_arg1_len | |||||
| rts | |||||
| ; Report A at end of file (pass 1): line after the last, column 1. Only | |||||
| ; the first end-of-file error is reported; the rest would be cascades. | |||||
| err_eof: | |||||
| ldx pass | |||||
| cpx #$01 | |||||
| bne drop | |||||
| ldx eof_reported | |||||
| bne drop | |||||
| ldx #$01 | |||||
| stx eof_reported | |||||
| tax | |||||
| clc | |||||
| lda tok_line | |||||
| adc #$01 | |||||
| sta diag_line | |||||
| lda tok_line+1 | |||||
| adc #$00 | |||||
| sta diag_line+1 | |||||
| lda #$01 | |||||
| sta diag_col | |||||
| txa | |||||
| jmp send | |||||
| report: tax | |||||
| lda tok_line | |||||
| sta diag_line | |||||
| lda tok_line+1 | |||||
| sta diag_line+1 | |||||
| lda tok_col | |||||
| sta diag_col | |||||
| txa | |||||
| send: jsr diag_report | |||||
| bcc :+ | |||||
| jmp stop_parse | |||||
| : rts | |||||
| .segment "RODATA" | |||||
| prim_kinds: | |||||
| .byte TOK_IDENT, KW_BOOLEAN, KW_BYTE, KW_CHAR, KW_INTEGER, KW_WORD | |||||
| prim_count = * - prim_kinds - 1 | |||||
| end_blocks: | |||||
| .byte KW_PROGRAM, KW_METHOD, KW_IF, KW_WHILE, KW_FOR | |||||
| end_count = * - end_blocks | |||||
| end_codes: | |||||
| .byte E_SYN_EXPECTED_END_PROGRAM, E_SYN_EXPECTED_END_METHOD | |||||
| .byte E_SYN_EXPECTED_END_IF, E_SYN_EXPECTED_END_WHILE, E_SYN_EXPECTED_END_FOR | |||||
| bit_masks: | |||||
| .byte $01, $02, $04, $08, $10, $20, $40, $80 | |||||
| ; ---------------------------------------------------------------- TABLES | |||||
| ; Top-level types by name ID: kind, then declaration line low/high. | |||||
| .segment "TABLES" | |||||
| type_kind: .res 256 | |||||
| type_line_lo: .res 256 | |||||
| type_line_hi: .res 256 | |||||
| ; ------------------------------------------------------------------- BSS | |||||
| .segment "BSS" | |||||
| parse_sp: .res 1 | |||||
| pass: .res 1 | |||||
| program_seen: .res 1 | |||||
| pending_end: .res 1 | |||||
| eof_reported: .res 1 | |||||
| body_err: .res 1 | |||||
| depth: .res 1 | |||||
| block_stack: .res PARSE_MAX_DEPTH | |||||
| scope_bits: .res 32 | |||||
| dk_kind: .res 1 | |||||
| dk_id: .res 1 | |||||
| unwind_to: .res 1 | |||||
| @@ -0,0 +1,2 @@ | |||||
| // one end-of-file error only: no extra PROGRAM EXPECTED cascade | |||||
| SYN_EXPECTED_END_CLASS 3 1 | |||||
| @@ -0,0 +1,2 @@ | |||||
| class Open | |||||
| private word x | |||||
| @@ -0,0 +1,2 @@ | |||||
| // reported at end of file: line after the last, column 1 | |||||
| SYN_EXPECTED_PROGRAM 3 1 | |||||
| @@ -0,0 +1,2 @@ | |||||
| class Lonely | |||||
| end class | |||||
| @@ -0,0 +1,4 @@ | |||||
| // declarations after the program are out of order but still parsed | |||||
| SYN_UNEXPECTED_TOKEN 4 1 | |||||
| SYN_UNEXPECTED_TOKEN 7 1 | |||||
| SEM_DUPLICATE_NAME 7 11 | |||||
| @@ -0,0 +1,8 @@ | |||||
| program Main | |||||
| end program | |||||
| class Late | |||||
| end class | |||||
| interface Late | |||||
| end interface | |||||
| @@ -0,0 +1,282 @@ | |||||
| ; E03-S01: parser test in x128. | |||||
| ; | |||||
| ; For each fixture: src_load, names_init, diag_init (screen off), parse_file, | |||||
| ; diag_finish. Rejected fixtures leave NAME.ERR on the disk for the host to | |||||
| ; compare; accepted fixtures must leave none. The dump records: | |||||
| ; $FF, fixture index, src_load result, diagnostic count | |||||
| ; per declared top-level type: kind, line lo, line hi, spelling length, | |||||
| ; spelling bytes | |||||
| ; $FD | |||||
| ; then $FE at the end. Mailbox: MB_DATA+0/1 dump address, +2/3 length. | |||||
| .setcpu "6502" | |||||
| .include "../../src/common/c128.inc" | |||||
| .include "../../src/common/mailbox.inc" | |||||
| .include "../../src/common/parser.inc" | |||||
| .import mmu_init, mmu_bank0, mmu_shutdown, mmu_peek1, kcall_inline | |||||
| .importzp mmu_ptr | |||||
| .import src_load | |||||
| .import names_init, name_get, names_count, name_out, name_out_len | |||||
| .import diag_init, diag_finish, diag_screen | |||||
| .import parse_file, type_kind, type_line_lo, type_line_hi | |||||
| DUMP_SIZE = $2000 | |||||
| .segment "LOADADDR" | |||||
| .word $1C01 | |||||
| .segment "EXEHDR" | |||||
| .word basic_end | |||||
| .word 10 | |||||
| .byte $9E | |||||
| .byte "7181" | |||||
| .byte $00 | |||||
| basic_end: | |||||
| .word $0000 | |||||
| .segment "MAILBOX" | |||||
| .res MB_SIZE | |||||
| .segment "ZEROPAGE" | |||||
| dump_ptr: .res 2 | |||||
| fx_ptr: .res 2 | |||||
| .segment "STARTUP" | |||||
| .export test_entry, dump | |||||
| 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 #<dump | |||||
| sta dump_ptr | |||||
| lda #>dump | |||||
| sta dump_ptr+1 | |||||
| lda #<fixtures | |||||
| sta fx_ptr | |||||
| lda #>fixtures | |||||
| sta fx_ptr+1 | |||||
| lda #$00 | |||||
| sta failures | |||||
| sta overflow | |||||
| sta fx_index | |||||
| @fixture: | |||||
| lda #$FF | |||||
| jsr emit | |||||
| lda fx_index | |||||
| jsr emit | |||||
| ldy #0 | |||||
| lda (fx_ptr),y | |||||
| sta name_lo | |||||
| iny | |||||
| lda (fx_ptr),y | |||||
| sta name_hi | |||||
| iny | |||||
| lda (fx_ptr),y | |||||
| sta name_len | |||||
| lda name_lo | |||||
| ldx name_hi | |||||
| ldy name_len | |||||
| jsr src_load | |||||
| pha | |||||
| jsr emit | |||||
| pla | |||||
| beq @parse | |||||
| inc failures | |||||
| lda #$00 | |||||
| jsr emit | |||||
| jmp @advance | |||||
| @parse: jsr names_init | |||||
| lda name_lo | |||||
| ldx name_hi | |||||
| ldy name_len | |||||
| jsr diag_init | |||||
| lda #$00 ; ERR files are the evidence; keep it fast | |||||
| sta diag_screen | |||||
| jsr parse_file | |||||
| jsr diag_finish | |||||
| jsr emit | |||||
| jsr dump_types | |||||
| @advance: | |||||
| lda #$FD | |||||
| jsr emit | |||||
| clc | |||||
| lda fx_ptr | |||||
| adc #3 | |||||
| sta fx_ptr | |||||
| bcc :+ | |||||
| inc fx_ptr+1 | |||||
| : inc fx_index | |||||
| lda fx_index | |||||
| cmp #FIXTURE_COUNT | |||||
| bne @fixture | |||||
| lda #$FE | |||||
| jsr emit | |||||
| lda #<dump ; report while the private ZP is mapped | |||||
| sta MB_DATA+0 | |||||
| lda #>dump | |||||
| sta MB_DATA+1 | |||||
| sec | |||||
| lda dump_ptr | |||||
| sbc #<dump | |||||
| sta MB_DATA+2 | |||||
| lda dump_ptr+1 | |||||
| sbc #>dump | |||||
| sta MB_DATA+3 | |||||
| lda failures ; everything above $3FFF disappears behind | |||||
| ora overflow ; BASIC ROM after mmu_shutdown: report first | |||||
| sta MB_CODE | |||||
| bne @fail | |||||
| lda #MB_ST_PASS | |||||
| ldy #ok_msg - messages ; may be 0: do not branch on it | |||||
| jmp @report | |||||
| @fail: lda #MB_ST_FAIL | |||||
| ldy #fail_msg - messages | |||||
| @report: | |||||
| sta MB_STATUS | |||||
| : lda messages,y | |||||
| beq @end | |||||
| sty print_y | |||||
| jsr kcall_inline | |||||
| .word CHROUT | |||||
| ldy print_y | |||||
| iny | |||||
| bne :- | |||||
| @end: jsr mmu_shutdown | |||||
| plp | |||||
| rts | |||||
| .segment "CODE" | |||||
| ; Emit every name ID that the parser recorded as a top-level type. | |||||
| dump_types: | |||||
| lda #$01 | |||||
| sta id | |||||
| @next: lda id | |||||
| cmp names_count | |||||
| beq :+ | |||||
| bcs @done | |||||
| : lda #<type_kind | |||||
| sta mmu_ptr | |||||
| lda #>type_kind | |||||
| sta mmu_ptr+1 | |||||
| ldy id | |||||
| jsr mmu_peek1 | |||||
| beq @skip | |||||
| jsr emit | |||||
| lda #<type_line_lo | |||||
| sta mmu_ptr | |||||
| lda #>type_line_lo | |||||
| sta mmu_ptr+1 | |||||
| ldy id | |||||
| jsr mmu_peek1 | |||||
| jsr emit | |||||
| lda #<type_line_hi | |||||
| sta mmu_ptr | |||||
| lda #>type_line_hi | |||||
| sta mmu_ptr+1 | |||||
| ldy id | |||||
| jsr mmu_peek1 | |||||
| jsr emit | |||||
| lda id | |||||
| jsr name_get | |||||
| lda name_out_len | |||||
| jsr emit | |||||
| ldx #$00 | |||||
| : cpx name_out_len | |||||
| beq @skip | |||||
| lda name_out,x | |||||
| stx emit_x | |||||
| jsr emit | |||||
| ldx emit_x | |||||
| inx | |||||
| bne :- | |||||
| @skip: inc id | |||||
| bne @next | |||||
| @done: rts | |||||
| emit: ldy dump_ptr+1 | |||||
| cpy #>(dump + DUMP_SIZE) | |||||
| bcc @room | |||||
| lda #$01 | |||||
| sta overflow | |||||
| rts | |||||
| @room: sty emit_y | |||||
| ldy #$00 | |||||
| sta (dump_ptr),y | |||||
| ldy emit_y | |||||
| inc dump_ptr | |||||
| bne :+ | |||||
| inc dump_ptr+1 | |||||
| : rts | |||||
| .segment "RODATA" | |||||
| .macro FIXTURE name | |||||
| .local text, done | |||||
| .word text | |||||
| .byte done - text | |||||
| .pushseg | |||||
| .segment "DATA" | |||||
| text: .byte name | |||||
| done: | |||||
| .popseg | |||||
| .endmacro | |||||
| fixtures: | |||||
| FIXTURE "MINIMAL.N8" | |||||
| FIXTURE "DECLS.N8" | |||||
| FIXTURE "ARRAYS.N8" | |||||
| FIXTURE "EXPRS.N8" | |||||
| FIXTURE "FORWARD.N8" | |||||
| FIXTURE "NULLS.N8" | |||||
| FIXTURE "STMTS.N8" | |||||
| FIXTURE "BADCHAR.N8" | |||||
| FIXTURE "BADCLASS.N8" | |||||
| FIXTURE "BADIFACE.N8" | |||||
| FIXTURE "BADMETHOD.N8" | |||||
| FIXTURE "DUPDECL.N8" | |||||
| FIXTURE "IFACEBODY.N8" | |||||
| FIXTURE "LINECONT.N8" | |||||
| FIXTURE "MISSCLASS.N8" | |||||
| FIXTURE "MISSEND.N8" | |||||
| FIXTURE "MISSIF.N8" | |||||
| FIXTURE "MISSMETH.N8" | |||||
| FIXTURE "MULTISTMT.N8" | |||||
| FIXTURE "ORDER.N8" | |||||
| FIXTURE "UNKNOWN.N8" | |||||
| FIXTURE "RECOVER.N8" | |||||
| FIXTURE "NOPROG.N8" | |||||
| FIXTURE "EOFCLASS.N8" | |||||
| FIXTURE_COUNT = 24 | |||||
| messages: | |||||
| ok_msg: .byte "NOVA128 PARSER TEST OK", $0D, $00 | |||||
| fail_msg: .byte "NOVA128 PARSER TEST FAIL", $0D, $00 | |||||
| .segment "BSS" | |||||
| failures: .res 1 | |||||
| overflow: .res 1 | |||||
| fx_index: .res 1 | |||||
| name_lo: .res 1 | |||||
| name_hi: .res 1 | |||||
| name_len: .res 1 | |||||
| id: .res 1 | |||||
| emit_x: .res 1 | |||||
| emit_y: .res 1 | |||||
| print_y: .res 1 | |||||
| dump: .res DUMP_SIZE | |||||
| @@ -0,0 +1,6 @@ | |||||
| // five independent errors; each line after an error still parses | |||||
| SYN_EXPECTED_IDENTIFIER 1 10 | |||||
| SYN_EXPECTED_INTERFACE_NAME 5 21 | |||||
| SYN_EXPECTED_IDENTIFIER 7 22 | |||||
| SEM_DUPLICATE_NAME 13 7 | |||||
| SYN_EXPECTED_END_IF 16 7 | |||||
| @@ -0,0 +1,17 @@ | |||||
| interface | |||||
| method a() | |||||
| end interface | |||||
| class Box implements | |||||
| private word size | |||||
| public method get( as word | |||||
| end method | |||||
| end class | |||||
| program Main | |||||
| var x as word | |||||
| var x as byte | |||||
| while x | |||||
| if x | |||||
| end while | |||||
| end program | |||||
| @@ -0,0 +1,5 @@ | |||||
| // pass 2: Printable and Worker are forward references; the rest are unknown | |||||
| SEM_UNKNOWN_NAME 1 25 | |||||
| SEM_UNKNOWN_NAME 2 11 | |||||
| SEM_UNKNOWN_NAME 3 28 | |||||
| SEM_UNKNOWN_NAME 13 12 | |||||
| @@ -0,0 +1,14 @@ | |||||
| class Worker implements Missing, Printable | |||||
| private Ghost helper | |||||
| public method run(job as Phantom) as Printable | |||||
| end method | |||||
| end class | |||||
| interface Printable | |||||
| method print() | |||||
| end interface | |||||
| program Main | |||||
| var w as Worker | |||||
| var g as Nothing | |||||
| end program | |||||
Powered by TurnKey Linux.