From e478321f42cd3f66c3b7b37265a01ee7515ccef3 Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Sat, 7 Nov 2026 20:17:50 -0500 Subject: [PATCH] newLang: add the top-level declaration parser (E03-S01) 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 --- newLang/docs/architecture.md | 7 + newLang/docs/decisions.md | 1 + newLang/docs/diagnostics.md | 8 + newLang/docs/memory-map.md | 10 + newLang/planning/claims/E03-S01.md | 88 +++ newLang/scripts/build-parser-test.ps1 | 61 ++ newLang/scripts/verify-parser-test.ps1 | 122 ++++ newLang/src/common/diagcat.inc | 8 + newLang/src/common/parser.inc | 13 + newLang/src/compiler/parser.s | 808 +++++++++++++++++++++++++ newLang/tests/parser/eofclass.expect | 2 + newLang/tests/parser/eofclass.n8 | 2 + newLang/tests/parser/noprog.expect | 2 + newLang/tests/parser/noprog.n8 | 2 + newLang/tests/parser/order.expect | 4 + newLang/tests/parser/order.n8 | 8 + newLang/tests/parser/parser-test.s | 282 +++++++++ newLang/tests/parser/recover.expect | 6 + newLang/tests/parser/recover.n8 | 17 + newLang/tests/parser/unknown.expect | 5 + newLang/tests/parser/unknown.n8 | 14 + 21 files changed, 1470 insertions(+) create mode 100644 newLang/planning/claims/E03-S01.md create mode 100644 newLang/scripts/build-parser-test.ps1 create mode 100644 newLang/scripts/verify-parser-test.ps1 create mode 100644 newLang/src/common/parser.inc create mode 100644 newLang/src/compiler/parser.s create mode 100644 newLang/tests/parser/eofclass.expect create mode 100644 newLang/tests/parser/eofclass.n8 create mode 100644 newLang/tests/parser/noprog.expect create mode 100644 newLang/tests/parser/noprog.n8 create mode 100644 newLang/tests/parser/order.expect create mode 100644 newLang/tests/parser/order.n8 create mode 100644 newLang/tests/parser/parser-test.s create mode 100644 newLang/tests/parser/recover.expect create mode 100644 newLang/tests/parser/recover.n8 create mode 100644 newLang/tests/parser/unknown.expect create mode 100644 newLang/tests/parser/unknown.n8 diff --git a/newLang/docs/architecture.md b/newLang/docs/architecture.md index fd9eb83..79b251b 100644 --- a/newLang/docs/architecture.md +++ b/newLang/docs/architecture.md @@ -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; diff --git a/newLang/docs/decisions.md b/newLang/docs/decisions.md index 604aacd..87d41ef 100644 --- a/newLang/docs/decisions.md +++ b/newLang/docs/decisions.md @@ -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 diff --git a/newLang/docs/diagnostics.md b/newLang/docs/diagnostics.md index 452df76..1c8f1e6 100644 --- a/newLang/docs/diagnostics.md +++ b/newLang/docs/diagnostics.md @@ -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 | diff --git a/newLang/docs/memory-map.md b/newLang/docs/memory-map.md index 49416e5..adcb614 100644 --- a/newLang/docs/memory-map.md +++ b/newLang/docs/memory-map.md @@ -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 diff --git a/newLang/planning/claims/E03-S01.md b/newLang/planning/claims/E03-S01.md new file mode 100644 index 0000000..37718d0 --- /dev/null +++ b/newLang/planning/claims/E03-S01.md @@ -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. diff --git a/newLang/scripts/build-parser-test.ps1 b/newLang/scripts/build-parser-test.ps1 new file mode 100644 index 0000000..f908ae3 --- /dev/null +++ b/newLang/scripts/build-parser-test.ps1 @@ -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) diff --git a/newLang/scripts/verify-parser-test.ps1 b/newLang/scripts/verify-parser-test.ps1 new file mode 100644 index 0000000..be96bbc --- /dev/null +++ b/newLang/scripts/verify-parser-test.ps1 @@ -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) diff --git a/newLang/src/common/diagcat.inc b/newLang/src/common/diagcat.inc index 2d242f4..b11620d 100644 --- a/newLang/src/common/diagcat.inc +++ b/newLang/src/common/diagcat.inc @@ -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" diff --git a/newLang/src/common/parser.inc b/newLang/src/common/parser.inc new file mode 100644 index 0000000..e252d69 --- /dev/null +++ b/newLang/src/common/parser.inc @@ -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 diff --git a/newLang/src/compiler/parser.s b/newLang/src/compiler/parser.s new file mode 100644 index 0000000..fffb383 --- /dev/null +++ b/newLang/src/compiler/parser.s @@ -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 # 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 diff --git a/newLang/tests/parser/eofclass.expect b/newLang/tests/parser/eofclass.expect new file mode 100644 index 0000000..8c53a82 --- /dev/null +++ b/newLang/tests/parser/eofclass.expect @@ -0,0 +1,2 @@ +// one end-of-file error only: no extra PROGRAM EXPECTED cascade +SYN_EXPECTED_END_CLASS 3 1 diff --git a/newLang/tests/parser/eofclass.n8 b/newLang/tests/parser/eofclass.n8 new file mode 100644 index 0000000..3bb3eb9 --- /dev/null +++ b/newLang/tests/parser/eofclass.n8 @@ -0,0 +1,2 @@ +class Open + private word x diff --git a/newLang/tests/parser/noprog.expect b/newLang/tests/parser/noprog.expect new file mode 100644 index 0000000..e79d67a --- /dev/null +++ b/newLang/tests/parser/noprog.expect @@ -0,0 +1,2 @@ +// reported at end of file: line after the last, column 1 +SYN_EXPECTED_PROGRAM 3 1 diff --git a/newLang/tests/parser/noprog.n8 b/newLang/tests/parser/noprog.n8 new file mode 100644 index 0000000..442c3cd --- /dev/null +++ b/newLang/tests/parser/noprog.n8 @@ -0,0 +1,2 @@ +class Lonely +end class diff --git a/newLang/tests/parser/order.expect b/newLang/tests/parser/order.expect new file mode 100644 index 0000000..0c22285 --- /dev/null +++ b/newLang/tests/parser/order.expect @@ -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 diff --git a/newLang/tests/parser/order.n8 b/newLang/tests/parser/order.n8 new file mode 100644 index 0000000..159c0e1 --- /dev/null +++ b/newLang/tests/parser/order.n8 @@ -0,0 +1,8 @@ +program Main +end program + +class Late +end class + +interface Late +end interface diff --git a/newLang/tests/parser/parser-test.s b/newLang/tests/parser/parser-test.s new file mode 100644 index 0000000..f4b930b --- /dev/null +++ b/newLang/tests/parser/parser-test.s @@ -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+1 + 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 + sta MB_DATA+1 + sec + lda dump_ptr + 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+1 + ldy id + jsr mmu_peek1 + beq @skip + jsr emit + lda #type_line_lo + sta mmu_ptr+1 + ldy id + jsr mmu_peek1 + jsr emit + 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 diff --git a/newLang/tests/parser/recover.expect b/newLang/tests/parser/recover.expect new file mode 100644 index 0000000..426c486 --- /dev/null +++ b/newLang/tests/parser/recover.expect @@ -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 diff --git a/newLang/tests/parser/recover.n8 b/newLang/tests/parser/recover.n8 new file mode 100644 index 0000000..085d453 --- /dev/null +++ b/newLang/tests/parser/recover.n8 @@ -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 diff --git a/newLang/tests/parser/unknown.expect b/newLang/tests/parser/unknown.expect new file mode 100644 index 0000000..b58684b --- /dev/null +++ b/newLang/tests/parser/unknown.expect @@ -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 diff --git a/newLang/tests/parser/unknown.n8 b/newLang/tests/parser/unknown.n8 new file mode 100644 index 0000000..19dc43d --- /dev/null +++ b/newLang/tests/parser/unknown.n8 @@ -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