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 | |||
| 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 | |||
| - 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. | | |||
| | 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 | |||
| @@ -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 | | |||
| | E029 | `SYN_INTERFACE_METHOD_BODY` | INTERFACE METHODS HAVE NO BODY | | |||
| | 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) | |||
| @@ -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 | | |||
| | 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 | | |||
| |---|---|---| | |||
| | `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 | | |||
| 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 | |||
| `$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 | |||
| - **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_INTERFACE_METHOD_BODY, 29, "INTERFACE METHODS HAVE NO BODY" | |||
| 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 | |||
| DIAGNOSTIC SEM_DUPLICATE_NAME, 60, "% IS ALREADY DECLARED" | |||
| @@ -66,3 +73,4 @@ | |||
| ; ------------------------------------------------------- resource limits | |||
| DIAGNOSTIC RES_TOO_MANY_NAMES, 220, "MORE THAN 255 NAMES" | |||
| 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.