parse_statement parses every spec 7 form (assignment, call, if/else, while, for/to/step, return, print, delete) and requires the line to end after it. Expressions use precedence climbing over spec 8's levels: binary operators left associative, unary not and - nesting rightwards, postfix member access, calls, and indexing tightest. A syntax error is reported in pass 1, unwinds to the statement, and skips the line; E03-S02's name, member, and access checks now run from the parser in pass 2, replacing the token scan. Expression nesting is bounded at 16 levels (E225). Verified in x128: 31 fixtures, including the two statement fixtures that were pending since E03-S01, a postfix trace golden for precedence and associativity (both mutations caught), every statement form, and 15 independent syntax errors with recovery. Lowest stack pointer $A7. New diagnostics E038-E042 and E225. The demo passes; its Vim typing is slowed to 200 ms per key after one doubled keystroke. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
| @@ -171,8 +171,7 @@ 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. | |||
| name no declared interface or class. | |||
| E03-S02 adds members and scopes. Pass 1 records every field and method | |||
| signature (access, type, parameter types) in bank-1 member and parameter | |||
| @@ -183,8 +182,21 @@ locals carry their declared types. Pass 2 scans statement lines token by | |||
| token: an unqualified name must be a local, parameter, or (in a method) a field | |||
| of the enclosing class (D123); `expr.member` follows the left side's type | |||
| through locals, fields, `self`, `new`, call results, and indexing, and reports | |||
| unknown members and private members used outside their class. E03-S03 is | |||
| expected to replace that scan with the expression parser and keep the lookups. | |||
| unknown members and private members used outside their class. | |||
| E03-S03 replaced that scan with a real statement and expression parser that | |||
| runs in both passes. `parse_statement` handles every spec 7 form (assignment, | |||
| call, if/else, while, for/to/step, return, print, delete) and requires the | |||
| line to end after it; a syntax error is reported in pass 1, unwinds to the | |||
| statement through a saved stack pointer, and skips the rest of the line, so | |||
| later lines still parse. Expressions use precedence climbing over spec 8's | |||
| levels with left-associative binary operators and right-nesting unary `not` | |||
| and `-`; postfix member access, calls, and indexing bind tightest. Name, | |||
| member, and access lookups from E03-S02 run from the parser in pass 2. Each | |||
| expression level is counted, and more than 16 is E225, which bounds hardware | |||
| stack use (the deepest fixture leaves SP at `$A7`). For tests, pass 1 can | |||
| record every expression in postfix order (`ex_trace_on`). Operator and | |||
| conversion types remain E03-S04. | |||
| ### Pass 1 — declarations and layout | |||
| @@ -52,6 +52,11 @@ After 20 errors the next one is replaced by E221 and compilation stops. | |||
| | E035 | `SYN_EXPECTED_TYPE` | TYPE EXPECTED | | |||
| | E036 | `SYN_EXPECTED_END_WHILE` | END WHILE EXPECTED | | |||
| | E037 | `SYN_EXPECTED_END_FOR` | END FOR EXPECTED | | |||
| | E038 | `SYN_NOT_A_STATEMENT` | NOT A STATEMENT | | |||
| | E039 | `SYN_NOT_ASSIGNABLE` | CANNOT ASSIGN TO THIS | | |||
| | E040 | `SYN_EXPECTED_RBRACKET` | ']' EXPECTED | | |||
| | E041 | `SYN_EXPECTED_TO` | TO EXPECTED | | |||
| | E042 | `SYN_EXPECTED_EQUALS` | '=' EXPECTED | | |||
| ## Names and declarations (E060-E079) | |||
| @@ -104,3 +109,4 @@ After 20 errors the next one is replaced by E221 and compilation stops. | |||
| | E222 | `RES_NESTING_TOO_DEEP` | BLOCKS NESTED TOO DEEPLY | | |||
| | E223 | `RES_TOO_MANY_MEMBERS` | MORE THAN 255 MEMBERS | | |||
| | E224 | `RES_TOO_MANY_PARAMS` | MORE THAN 255 PARAMETERS | | |||
| | E225 | `RES_EXPR_TOO_DEEP` | EXPRESSION NESTED TOO DEEPLY | | |||
| @@ -0,0 +1,97 @@ | |||
| --- | |||
| story: E03-S03 | |||
| owner: e03_expr_stmt_parser | |||
| status: done | |||
| started: 2026-11-07 | |||
| depends-on: [E03-S02] | |||
| --- | |||
| # E03-S03 Claim | |||
| ## Intended change | |||
| Replace the pass-2 token scan in `src/compiler/parser.s` with a recursive | |||
| descent statement parser and a precedence-climbing expression parser | |||
| (spec 7 and 8). Every statement line is parsed in both passes: syntax | |||
| errors are reported in pass 1, name/member/access errors (the E03-S02 | |||
| lookups, now driven by the parser) in pass 2. A syntax error unwinds to | |||
| the statement and skips its line. Expression nesting is bounded so the | |||
| hardware stack cannot overflow. For tests, pass 1 can record each | |||
| expression in postfix order (operands by column, then operators), which | |||
| shows precedence and associativity directly. | |||
| ## 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/language-spec.md` (only if a decision is | |||
| needed), `docs/decisions.md` | |||
| - `planning/claims/E03-S03.md` | |||
| - generated files under `build/` | |||
| ## Acceptance evidence | |||
| - [x] Implements documented precedence and left/right associativity: | |||
| `prec.n8` records each statement's expression in postfix order in x128 | |||
| and all 15 match `prec.expect` (`a - b - c` -> `a b - c -`, `a / b / c`, | |||
| `- - a`, `not d and d` -> `d not d and`, `d or d and d`, mixed levels, | |||
| comparisons below arithmetic, groups). Mutations: making binary operators | |||
| right associative, or swapping the `+`/`*` levels, each fail the golden. | |||
| - [x] Parses every version 1 statement form: `forms.n8` (assignment to a | |||
| local, field, bare field, and element; call statements including chained | |||
| calls and calls as arguments; if/else nested; while; for with and without | |||
| `step`, negative step; return with and without a value; print of string, | |||
| char, boolean, and word; delete; `new` with an expression argument) and | |||
| all 7 accepted grammar fixtures parse with no diagnostics. | |||
| - [x] Rejects multiple statements per line and cross-line expressions: | |||
| `multi_stmt.n8` gives E021 at the second `print` (2:11) and | |||
| `line_cont.n8` gives E022 at the end of `total = 1 +` (3:14); both were | |||
| pending since E03-S01 and now pass as ordinary rejected fixtures. | |||
| `syntax.n8` adds `a = 1 a = 2` (E021) and `print` with no operand (E022). | |||
| - [x] Recovers sufficiently to report a later independent error: | |||
| `syntax.n8` reports 15 independent syntax errors on consecutive lines | |||
| (missing `)`/`]`, not-a-statement, unassignable target, `=`/`to` | |||
| missing in `for`, stray `)`/`*`, `else` outside an if and twice in one, | |||
| an expression nested too deeply) and then a pass-2 unknown name; a | |||
| lexer error is not followed by a cascaded E022. | |||
| ## Coordination notes | |||
| - No other claim is active. Types of operators and conversions remain | |||
| E03-S04; the parser only carries the member-chain type that E03-S02's | |||
| access checks need. | |||
| ## Work log | |||
| - Appended E038 NOT A STATEMENT, E039 CANNOT ASSIGN TO THIS, E040 ']' | |||
| EXPECTED, E041 TO EXPECTED, E042 '=' EXPECTED, and E225 EXPRESSION | |||
| NESTED TOO DEEPLY; docs regenerated. | |||
| - The pass-2 token scan (`scan_refs`) is gone; its name and member lookups | |||
| are called by the parser, so all E03-S02 fixtures pass unchanged. | |||
| - Expression depth is bounded at 16 levels; the harness records the | |||
| lowest hardware SP seen in `ex_enter` and fails below `$60` (seen: `$A7`). | |||
| - The write-and-check demo (which links the parser) passes; its Vim typing | |||
| once doubled a key at 120 ms per key, which the demo's read-back caught, | |||
| so its default is now 200 ms. | |||
| ## Completion | |||
| - Tests run: `scripts/verify-parser-test.ps1` (final run and two | |||
| mutations), `scripts/build-diag-test.ps1` catalog check, | |||
| `scripts/demo-write-check.ps1`. | |||
| - Results: 31 parser fixtures pass (no pending ones remain); both mutations | |||
| caught; demo passes. | |||
| - Files changed: `src/compiler/parser.s`, `src/common/parser.inc`, | |||
| `src/common/diagcat.inc`, `docs/diagnostics.md`, `docs/architecture.md`, | |||
| `tests/parser/` (`prec`, `forms`, `syntax` fixtures, harness trace and SP | |||
| dump), `scripts/build-parser-test.ps1`, `scripts/verify-parser-test.ps1`, | |||
| `scripts/demo-write-check.ps1`, this claim. | |||
| - Remaining risks: `return` is accepted in the program body (spec 7 does | |||
| not restrict it; E03-S04/E04 must define what it does there). | |||
| Parenthesized groups do not carry a member-chain type, so `(x).f` is not | |||
| access-checked. Literals accept postfix syntax (`5.x`) that E03-S04 must | |||
| reject as a type error. A bare call `f()` reports the name or E021, not a | |||
| dedicated message (D123). The trace hook writes without a bound; only the | |||
| test enables it. | |||
| @@ -1,4 +1,4 @@ | |||
| # E03-S01/E03-S02: build the parser test PRG and a fresh D81 holding every grammar and | |||
| # E03-S01..E03-S03: 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() | |||
| @@ -26,12 +26,12 @@ $script:ParserFixtures = [ordered]@{ | |||
| "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") | |||
| "linecont.n8" = @("$g\rejected\line_cont.n8", "rejected", "$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") | |||
| "multistmt.n8" = @("$g\rejected\multi_stmt.n8", "rejected", "$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") | |||
| @@ -41,6 +41,9 @@ $script:ParserFixtures = [ordered]@{ | |||
| "lifecycle.n8" = @("$p\lifecycle.n8", "exact", "$p\lifecycle.expect") | |||
| "scopes.n8" = @("$p\scopes.n8", "exact", "$p\scopes.expect") | |||
| "private.n8" = @("$p\private.n8", "exact", "$p\private.expect") | |||
| "prec.n8" = @("$p\prec.n8", "accepted", "$p\prec.expect") | |||
| "forms.n8" = @("$p\forms.n8", "accepted", $null) | |||
| "syntax.n8" = @("$p\syntax.n8", "exact", "$p\syntax.expect") | |||
| } | |||
| $link = Invoke-Nova128Link -Name "parser-test" -BuildDir $out ` | |||
| @@ -15,7 +15,9 @@ | |||
| # 4. The screen and NAME.ERR, if any, are shown. | |||
| # Screenshots go to build/demo/. No code is generated yet (epic E04). | |||
| [CmdletBinding()] | |||
| param([string[]]$Programs = @("good.n8", "bad.n8"), [int]$KeyDelay = 120) | |||
| # At 120 ms per key Vim once saw a doubled key and a lost RETURN; the | |||
| # read-back comparison catches that, and 200 ms leaves more margin. | |||
| param([string[]]$Programs = @("good.n8", "bad.n8"), [int]$KeyDelay = 200) | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| @@ -52,8 +52,9 @@ function Read-DumpType { | |||
| $records = @(); $script: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 = @(); Members = @() } | |||
| $p += 4 | |||
| $r = [pscustomobject]@{ Index = $dump[$p + 1]; Load = $dump[$p + 2]; Count = $dump[$p + 3] | |||
| MinSp = $dump[$p + 4]; Types = @(); Members = @(); Trace = $null } | |||
| $p += 5 | |||
| while ($dump[$p] -ne 0xFD -and $dump[$p] -ne 0xFC) { | |||
| $len = $dump[$p + 3] | |||
| $spell = ConvertFrom-Spelling $dump[($p + 4)..($p + 3 + $len)] | |||
| @@ -62,7 +63,7 @@ while ($dump[$p] -ne 0xFE) { | |||
| } | |||
| if ($dump[$p] -eq 0xFC) { | |||
| $p++ | |||
| while ($dump[$p] -ne 0xFD) { | |||
| while ($dump[$p] -ne 0xFD -and $dump[$p] -ne 0xFB) { | |||
| $owner = Read-DumpName | |||
| $name = Read-DumpName | |||
| $flags = $dump[$p]; $p++ | |||
| @@ -73,11 +74,42 @@ while ($dump[$p] -ne 0xFE) { | |||
| $(if ($flags -band 1) { "method" } else { "field" }), | |||
| $(if ($flags -band 2) { "private" } else { "public" }), $type, ($params -join ",")) | |||
| } | |||
| if ($dump[$p] -eq 0xFB) { | |||
| $n = $dump[$p + 1] + 256 * $dump[$p + 2] | |||
| $r.Trace = if ($n) { [byte[]]$dump[($p + 3)..($p + 2 + $n)] } else { [byte[]]@() } | |||
| $p += 3 + $n | |||
| } | |||
| } | |||
| $p++ | |||
| $records += $r | |||
| } | |||
| # Expression trace -> "line: postfix" strings. Operands are named by the | |||
| # source text at their column; operators by their symbol (parser.inc TR_*). | |||
| $traceOps = @{ 0x17 = "="; 0x1E = "<>"; 0x1C = "<"; 0x1D = ">"; 0x1F = "<="; 0x20 = ">=" | |||
| 0x18 = "+"; 0x19 = "-"; 0x1A = "*"; 0x1B = "/"; 0x40 = "and"; 0x53 = "or" } | |||
| function ConvertFrom-Trace([byte[]]$trace, [string[]]$lines) { | |||
| $out = @(); $k = 0; $cur = $null | |||
| while ($k -lt $trace.Length) { | |||
| $b = $trace[$k] | |||
| if ($b -eq 0xFF) { | |||
| if ($cur) { $out += $cur } | |||
| $line = $trace[$k + 1] + 256 * $trace[$k + 2]; $k += 3 | |||
| $cur = "${line}:"; $src = $lines[$line - 1]; continue | |||
| } | |||
| if ($b -eq 0xF0) { $cur += " neg" } | |||
| elseif ($b -eq 0xF1) { $cur += " not" } | |||
| elseif ($b -ge 0x80) { $cur += " " + $traceOps[$b - 0x80] } | |||
| else { | |||
| $m = [regex]::Match($src.Substring($b - 1), '^(\w+|"[^"]*"|''.'')') | |||
| $cur += " " + $m.Value.ToLower() | |||
| } | |||
| $k++ | |||
| } | |||
| if ($cur) { $out += $cur } | |||
| return ,$out | |||
| } | |||
| # Members the source declares, derived independently by regex: per class or | |||
| # interface in order; access defaults (fields private, methods public); | |||
| # a second declaration of a name in the same namespace is not recorded. | |||
| @@ -135,7 +167,7 @@ function Parse-Err([string]$line) { | |||
| } | |||
| # ---------------------------------------------------------------- check | |||
| $failures = @(); $i = 0; $pending = @() | |||
| $failures = @(); $i = 0; $minSp = 0xFF | |||
| foreach ($name in $ParserFixtures.Keys) { | |||
| $src, $group, $expect = $ParserFixtures[$name] | |||
| $base = $name -replace '\.n8$', '' | |||
| @@ -174,11 +206,6 @@ foreach ($name in $ParserFixtures.Keys) { | |||
| } | |||
| $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] }) | |||
| @@ -196,9 +223,24 @@ foreach ($name in $ParserFixtures.Keys) { | |||
| } | |||
| $detail += ", {0} members" -f $r.Members.Count | |||
| } | |||
| if ($group -eq "accepted" -and $expect) { # expression trace golden | |||
| $want = @(Get-Content -LiteralPath $expect | Where-Object { $_ -and -not $_.StartsWith("//") }) | |||
| if ($null -eq $r.Trace) { $problems += "no expression trace" } | |||
| else { | |||
| $got = ConvertFrom-Trace $r.Trace @(Get-Content -LiteralPath $src) | |||
| for ($k = 0; $k -lt [Math]::Max($got.Count, $want.Count); $k++) { | |||
| if ($got[$k] -ne $want[$k]) { $problems += "trace '$($got[$k])' expected '$($want[$k])'" } | |||
| } | |||
| $detail += ", {0} traced statements" -f $got.Count | |||
| } | |||
| } | |||
| if ($r.MinSp -ne 0xFF -and $r.MinSp -lt $minSp) { $minSp = $r.MinSp } | |||
| $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 "; ") } | |||
| } | |||
| # The expression bound must leave hardware stack to spare (KERNAL calls and | |||
| # interrupts run below the parser). | |||
| if ($minSp -lt 0x60) { $failures += ("expression parsing took the stack down to `${0:X2}" -f $minSp) } | |||
| 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) | |||
| Write-Host ("verified parser: {0} fixtures; lowest stack pointer in expressions `${1:X2}" -f $ParserFixtures.Count, $minSp) | |||
| @@ -47,6 +47,11 @@ | |||
| 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" | |||
| DIAGNOSTIC SYN_NOT_A_STATEMENT, 38, "NOT A STATEMENT" | |||
| DIAGNOSTIC SYN_NOT_ASSIGNABLE, 39, "CANNOT ASSIGN TO THIS" | |||
| DIAGNOSTIC SYN_EXPECTED_RBRACKET, 40, "']' EXPECTED" | |||
| DIAGNOSTIC SYN_EXPECTED_TO, 41, "TO EXPECTED" | |||
| DIAGNOSTIC SYN_EXPECTED_EQUALS, 42, "'=' EXPECTED" | |||
| ; -------------------------------------------- names and declarations | |||
| DIAGNOSTIC SEM_DUPLICATE_NAME, 60, "% IS ALREADY DECLARED" | |||
| @@ -78,3 +83,4 @@ | |||
| DIAGNOSTIC RES_NESTING_TOO_DEEP, 222, "BLOCKS NESTED TOO DEEPLY" | |||
| DIAGNOSTIC RES_TOO_MANY_MEMBERS, 223, "MORE THAN 255 MEMBERS" | |||
| DIAGNOSTIC RES_TOO_MANY_PARAMS, 224, "MORE THAN 255 PARAMETERS" | |||
| DIAGNOSTIC RES_EXPR_TOO_DEEP, 225, "EXPRESSION NESTED TOO DEEPLY" | |||
| @@ -10,7 +10,18 @@ TYPE_CLASS = 2 | |||
| TYPE_PROGRAM = 3 | |||
| PARSE_MAX_DEPTH = 16 ; nested method/program/if/while/for blocks | |||
| SCAN_MAX_DEPTH = 16 ; nested ( and [ tracked by the pass-2 scan | |||
| EX_MAX_DEPTH = 16 ; expression levels: parse_expr calls + unary ops | |||
| ; What a postfix expression ends in (statement checks) | |||
| EX_OTHER = 0 ; literal, self, new, (group), operator result | |||
| EX_PLACE = 1 ; name, field, or element: assignable | |||
| EX_CALL = 2 ; method call: a statement by itself | |||
| ; Test trace bytes (parser ex_trace_on): operand = its column (1-80) | |||
| TR_BINARY = $80 ; + operator token kind ($80-$DF) | |||
| TR_NEG = $F0 ; unary - | |||
| TR_NOT = $F1 ; unary not | |||
| TR_LINE = $FF ; statement start, then line lo/hi | |||
| ; Declared-type descriptor (provisional until E03-S04): base, name, length. | |||
| ; base TY_*; TY_NONE for a method without a result | |||
| @@ -1,25 +1,26 @@ | |||
| ; parser.s - declaration, member, and scope parser (E03-S01, E03-S02). | |||
| ; parser.s - declarations, members, scopes, statements, and expressions | |||
| ; (E03-S01, E03-S02, E03-S03). | |||
| ; | |||
| ; 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 and each field and method signature in the | |||
| ; member and parameter tables; reports syntax, ordering, duplicate, | |||
| ; init/destroy shape, and local-placement errors. | |||
| ; pass 2 replays the source and reports type references that do not name | |||
| ; a declared interface or class (forward references are therefore | |||
| ; legal), and scans statement lines for names: see scan_refs. | |||
| ; Method and program bodies are parsed line by line: `var` declarations are | |||
| ; parsed, typed, and duplicate-checked in the body's own scope; if/while/for/ | |||
| ; end blocks are tracked; other statement lines are skimmed in pass 1 and | |||
| ; scanned for name references in pass 2 until E03-S03 parses them. | |||
| ; member and parameter tables; parses every statement; reports | |||
| ; syntax, ordering, duplicate, init/destroy shape, and | |||
| ; local-placement errors. | |||
| ; pass 2 replays the source, parsing everything again, and reports type | |||
| ; references that do not name a declared interface or class | |||
| ; (forward references are therefore legal) and name, member, and | |||
| ; access errors in statements. | |||
| ; Statements are parsed by parse_statement and expressions by precedence | |||
| ; climbing (parse_expr); see the "statements" and "expressions" sections. | |||
| ; | |||
| ; Scopes: every method signature and the program start an empty local scope | |||
| ; (parameters and locals share it). Inside a method an unqualified name is a | |||
| ; local or parameter, else a field of the enclosing class; in the program it | |||
| ; is a local. Members are reached through `expr.name`; a private member is | |||
| ; visible only inside methods of its own class. | |||
| ; local or parameter, else a field of the enclosing class (D123); in the | |||
| ; program it is a local. Members are reached through `expr.name`; a private | |||
| ; member is visible only inside methods of its own class. | |||
| ; | |||
| ; 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 | |||
| @@ -41,6 +42,8 @@ | |||
| .importzp mmu_ptr | |||
| .export parse_file, tb_get, mem_count, par_count | |||
| .export ex_trace_on, ex_min_sp | |||
| .exportzp ex_tp | |||
| .export type_kind, type_line_lo, type_line_hi | |||
| ACC_DEFAULT = 0 | |||
| @@ -49,6 +52,9 @@ ACC_PRIVATE = 2 | |||
| LIFE_INIT = 1 | |||
| LIFE_DESTROY = 2 | |||
| .segment "ZEROPAGE" | |||
| ex_tp: .res 2 ; test trace pointer (ex_trace_on) | |||
| .macro IFTOK kind, target | |||
| lda tok_kind | |||
| cmp #kind | |||
| @@ -67,6 +73,8 @@ parse_file: | |||
| sta par_count | |||
| sta mem_full | |||
| sta par_full | |||
| lda #$FF | |||
| sta ex_min_sp | |||
| lda #$01 | |||
| sta pass | |||
| jsr parse_pass | |||
| @@ -89,6 +97,8 @@ parse_pass: | |||
| sta eof_reported | |||
| sta cur_owner | |||
| sta cur_class | |||
| sta lex_err_line ; lines are 1-based: 0 matches none | |||
| sta lex_err_line+1 | |||
| jsr next | |||
| top: jsr skip_blank | |||
| lda pending_end ; stray `end X` left by a body: reported | |||
| @@ -576,10 +586,19 @@ type_ref: | |||
| ; Parse a method or program body. A = KW_METHOD or KW_PROGRAM. | |||
| parse_body: | |||
| sta block_stack | |||
| lda #$00 | |||
| sta block_else | |||
| sta stmt_seen | |||
| lda #$01 | |||
| sta depth | |||
| sta refs_on ; names resolve, except in the methods of | |||
| lda block_stack ; a class whose name failed to parse | |||
| cmp #KW_METHOD | |||
| bne @loop | |||
| lda cur_class | |||
| bne @loop | |||
| lda #$00 | |||
| sta stmt_seen | |||
| sta refs_on | |||
| @loop: jsr skip_blank | |||
| lda tok_kind | |||
| cmp #TOK_EOF | |||
| @@ -589,31 +608,14 @@ parse_body: | |||
| bne :+ | |||
| jmp @interrupted | |||
| : IFTOK KW_VAR, @var | |||
| IFTOK KW_IF, @open | |||
| IFTOK KW_WHILE, @open | |||
| IFTOK KW_FOR, @open | |||
| IFTOK KW_END, @end | |||
| lda #$01 ; any other line is a statement | |||
| sta stmt_seen | |||
| jsr stmt_line | |||
| jsr parse_statement | |||
| jmp @loop | |||
| @var: jsr check_var_place | |||
| jsr parse_var ; declared anyway, to avoid cascades | |||
| jmp @loop | |||
| @open: lda #$01 | |||
| sta stmt_seen | |||
| 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 stmt_line ; condition / loop header | |||
| jmp @loop | |||
| @end: jsr next ; the closer keyword | |||
| lda tok_kind | |||
| @@ -679,20 +681,6 @@ check_var_place: | |||
| jmp err1 | |||
| @ok: rts | |||
| ; A statement line: skimmed in pass 1, scanned for names in pass 2. The | |||
| ; methods of a class whose name failed to parse are only skimmed. | |||
| stmt_line: | |||
| lda pass | |||
| cmp #$02 | |||
| bne @skim | |||
| lda block_stack | |||
| cmp #KW_METHOD | |||
| bne @scan | |||
| lda cur_class | |||
| beq @skim | |||
| @scan: jmp scan_refs | |||
| @skim: jmp skip_line | |||
| ; 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: | |||
| @@ -744,89 +732,439 @@ parse_var: | |||
| : jsr set_local_type | |||
| jmp expect_eol | |||
| ; ------------------------------------------------------ pass-2 name scan | |||
| ; Scan one statement line (pass 2) up to and past its EOL, resolving names: | |||
| ; name a local/parameter, else (in a method) a field of the class | |||
| ; self the enclosing class; not allowed in the program | |||
| ; new Name a declared class or interface | |||
| ; expr.name a field, or a method when `(` follows, of expr's class or | |||
| ; interface; private members only inside their own class | |||
| ; expr's type is followed through locals, fields, self, new, call results | |||
| ; (`x.get().y`), and indexing (`a[i].y`). Where it is unknown or not a class | |||
| ; or interface (an expression result, a primitive, an unknown type), the | |||
| ; member is not checked: type errors belong to E03-S04. | |||
| scan_refs: | |||
| ; ----------------------------------------------------------- statements | |||
| ; One statement line, through its EOL (spec 7). Syntax errors are reported | |||
| ; in pass 1 by syn_error, which also unwinds to here and skips the line; | |||
| ; name, member, and access errors are reported in pass 2 as the parser | |||
| ; meets them (resolve_name, resolve_member, self_ref, new_type). | |||
| parse_statement: | |||
| tsx | |||
| stx stmt_sp | |||
| lda #$00 | |||
| sta sc_sp | |||
| sta sc_over | |||
| sta pend | |||
| sta sc_prev ; TOK_EOF: no previous token | |||
| jsr chain_none | |||
| @loop: lda #$00 | |||
| sta call_base ; no method result waiting for `(` | |||
| lda pend | |||
| beq @tok | |||
| jsr resolve_member ; the token after a member name decides | |||
| @tok: lda tok_kind | |||
| sta ex_depth | |||
| lda tok_col | |||
| sta stmt_col | |||
| jsr trace_line | |||
| lda tok_kind | |||
| cmp #KW_IF | |||
| beq @block | |||
| cmp #KW_WHILE | |||
| beq @block | |||
| cmp #KW_FOR | |||
| beq @for | |||
| cmp #KW_ELSE | |||
| beq @else | |||
| cmp #KW_RETURN | |||
| beq @return | |||
| cmp #KW_PRINT | |||
| beq @value | |||
| cmp #KW_DELETE | |||
| beq @value | |||
| jmp stmt_expr | |||
| @block: jsr open_block ; if / while condition | |||
| @value: jsr next ; print / delete operand | |||
| jsr parse_full | |||
| jmp end_stmt | |||
| @return: | |||
| jsr next | |||
| lda tok_kind | |||
| cmp #TOK_EOL | |||
| beq @end | |||
| beq :+ | |||
| cmp #TOK_EOF | |||
| beq @end | |||
| ldx sc_prev | |||
| cpx #TOK_DOT | |||
| bne @kind | |||
| cmp #TOK_IDENT ; member name; keywords allowed (D121) | |||
| beq @member | |||
| cmp #TOK_KW_FIRST | |||
| bcc @kind | |||
| @member: | |||
| jsr pend_member | |||
| jmp @adv | |||
| @kind: cmp #TOK_IDENT | |||
| beq @ident | |||
| beq :+ | |||
| jsr parse_full | |||
| : jmp end_stmt | |||
| @else: ldx depth ; only once, directly inside an if | |||
| lda block_stack-1,x | |||
| cmp #KW_IF | |||
| bne @bad_else | |||
| lda block_else-1,x | |||
| bne @bad_else | |||
| lda #$01 | |||
| sta block_else-1,x | |||
| jsr next | |||
| jmp end_stmt | |||
| @bad_else: | |||
| jsr arg_token | |||
| lda #E_SYN_UNEXPECTED_TOKEN | |||
| jmp syn_error | |||
| ; for name = expr to expr [step expr] | |||
| @for: jsr open_block | |||
| jsr next | |||
| lda tok_kind | |||
| cmp #TOK_IDENT | |||
| beq :+ | |||
| lda #E_SYN_EXPECTED_IDENTIFIER | |||
| jmp syn_error | |||
| : jsr resolve_name | |||
| jsr next | |||
| lda tok_kind | |||
| cmp #TOK_EQ | |||
| beq :+ | |||
| lda #E_SYN_EXPECTED_EQUALS | |||
| jmp syn_error | |||
| : jsr next | |||
| jsr parse_full | |||
| lda tok_kind | |||
| cmp #KW_TO | |||
| beq :+ | |||
| lda #E_SYN_EXPECTED_TO | |||
| jmp syn_error | |||
| : jsr next | |||
| jsr parse_full | |||
| lda tok_kind | |||
| cmp #KW_STEP | |||
| bne end_stmt | |||
| jsr next | |||
| jsr parse_full | |||
| ; fall through | |||
| ; The statement must end here: one statement per line (spec 2.2). | |||
| end_stmt: | |||
| lda tok_kind | |||
| cmp #TOK_EOL | |||
| beq @eol | |||
| cmp #TOK_EOF | |||
| beq @done | |||
| lda #E_SYN_EXPECTED_EOL | |||
| jmp syn_error | |||
| @eol: jmp next | |||
| @done: rts | |||
| ; Assignment `target = expr` or a call `expr.method(args)`. | |||
| stmt_expr: | |||
| jsr parse_postfix | |||
| lda tok_kind | |||
| cmp #TOK_EQ | |||
| beq @assign | |||
| lda ex_last ; anything else must be a call | |||
| cmp #EX_CALL | |||
| beq end_stmt | |||
| lda #E_SYN_NOT_A_STATEMENT | |||
| ldx stmt_col | |||
| jmp syn_error_col | |||
| @assign: | |||
| lda ex_last | |||
| cmp #EX_PLACE | |||
| beq :+ | |||
| lda #E_SYN_NOT_ASSIGNABLE | |||
| ldx stmt_col | |||
| jmp syn_error_col | |||
| : jsr next | |||
| jsr parse_full | |||
| jmp end_stmt | |||
| ; Push the block opened by the current keyword (if/while/for). | |||
| open_block: | |||
| ldx depth | |||
| cpx #PARSE_MAX_DEPTH | |||
| bcc :+ | |||
| lda #E_RES_NESTING_TOO_DEEP | |||
| jmp syn_error | |||
| : lda tok_kind | |||
| sta block_stack,x | |||
| lda #$00 | |||
| sta block_else,x | |||
| inc depth | |||
| rts | |||
| ; Report A at the current token (or column X) in pass 1, unless the lexer | |||
| ; already reported an error on this line; then skip the rest of the line | |||
| ; and return from parse_statement. | |||
| syn_error: | |||
| ldx tok_col | |||
| syn_error_col: | |||
| ldy lex_err_line | |||
| cpy tok_line | |||
| bne @report | |||
| ldy lex_err_line+1 | |||
| cpy tok_line+1 | |||
| beq @skip | |||
| @report: | |||
| ldy pass | |||
| cpy #$01 | |||
| bne @skip | |||
| jsr report_col | |||
| @skip: ldx stmt_sp | |||
| txs | |||
| jmp skip_line | |||
| ; ---------------------------------------------------------- expressions | |||
| ; Precedence climbing over spec 8's binary levels (or 1, and 2, = <> 3, | |||
| ; < <= > >= 4, + - 5, * / 6); every binary operator is left associative, | |||
| ; so its right operand is parsed one level tighter. Unary not and - (level | |||
| ; 7) nest to the right. Postfix member access, calls, and indexing bind | |||
| ; tightest. Each nesting level (parse_expr or a unary operator) counts | |||
| ; toward EX_MAX_DEPTH, which bounds hardware-stack use. | |||
| ; | |||
| ; sc_* is the member-chain type when the expression is a single postfix | |||
| ; chain (none after an operator); ex_last says what that chain ends in. | |||
| parse_full: | |||
| lda #$01 | |||
| ; Parse an expression whose binary operators have precedence >= A. | |||
| parse_expr: | |||
| jsr ex_enter | |||
| sta ex_min,x | |||
| jsr parse_unary | |||
| @loop: ldx #bin_count - 1 | |||
| lda tok_kind | |||
| @find: cmp bin_kinds,x | |||
| beq @binary | |||
| dex | |||
| bpl @find | |||
| @done: dec ex_depth | |||
| rts | |||
| @binary: | |||
| lda bin_precs,x | |||
| ldx ex_depth | |||
| cmp ex_min,x | |||
| bcc @done ; binds looser: the caller takes it | |||
| sta ex_prec,x | |||
| lda tok_kind | |||
| sta ex_op,x | |||
| jsr next | |||
| ldx ex_depth | |||
| lda ex_prec,x | |||
| clc | |||
| adc #$01 ; left associative | |||
| jsr parse_expr | |||
| ldx ex_depth | |||
| lda ex_op,x | |||
| ora #TR_BINARY | |||
| jsr trace_byte | |||
| jsr chain_none | |||
| lda #EX_OTHER | |||
| sta ex_last | |||
| jmp @loop | |||
| parse_unary: | |||
| lda tok_kind | |||
| cmp #KW_NOT | |||
| beq @op | |||
| cmp #TOK_MINUS | |||
| beq @op | |||
| jmp parse_postfix | |||
| @op: jsr ex_enter | |||
| lda tok_kind | |||
| sta ex_op,x | |||
| jsr next | |||
| jsr parse_unary ; right associative: - not -x | |||
| ldx ex_depth | |||
| lda #TR_NOT | |||
| ldy ex_op,x | |||
| cpy #KW_NOT | |||
| beq :+ | |||
| lda #TR_NEG | |||
| : jsr trace_byte | |||
| jsr chain_none | |||
| lda #EX_OTHER | |||
| sta ex_last | |||
| dec ex_depth | |||
| rts | |||
| ; One more nesting level; A preserved, X = the new depth. | |||
| ex_enter: | |||
| inc ex_depth | |||
| sta ex_tmp | |||
| tsx ; deepest hardware stack seen (evidence) | |||
| cpx ex_min_sp | |||
| bcs :+ | |||
| stx ex_min_sp | |||
| : ldx ex_depth | |||
| cpx #EX_MAX_DEPTH + 1 | |||
| bcs @deep | |||
| lda ex_tmp | |||
| rts | |||
| @deep: lda #E_RES_EXPR_TOO_DEEP | |||
| jmp syn_error | |||
| ; primary { .name | .name(args) | [expr] } | |||
| parse_postfix: | |||
| lda #EX_OTHER | |||
| sta ex_last | |||
| jsr chain_none | |||
| lda tok_kind | |||
| cmp #TOK_LPAREN ; a group is traced by its contents | |||
| beq @group | |||
| lda tok_col ; trace: the operand, by its column | |||
| jsr trace_byte | |||
| lda tok_kind | |||
| cmp #TOK_IDENT | |||
| beq @name | |||
| cmp #KW_NEW | |||
| beq @new | |||
| cmp #KW_SELF | |||
| beq @self | |||
| cmp #TOK_DOT | |||
| beq @adv ; the chain is the member's left side | |||
| ldx #lit_count - 1 | |||
| @lit: cmp lit_kinds,x | |||
| beq @literal | |||
| dex | |||
| bpl @lit | |||
| cmp #TOK_EOL ; an operand is missing at the line end: | |||
| beq @early ; expressions do not continue (spec 2.2) | |||
| cmp #TOK_EOF | |||
| beq @early | |||
| jsr arg_token | |||
| lda #E_SYN_UNEXPECTED_TOKEN | |||
| jmp syn_error | |||
| @early: lda #E_SYN_UNEXPECTED_EOL | |||
| jmp syn_error | |||
| @literal: | |||
| jsr next | |||
| jmp @post | |||
| @name: jsr resolve_name | |||
| lda #EX_PLACE | |||
| sta ex_last | |||
| jsr next | |||
| jmp @post | |||
| @self: jsr self_ref | |||
| jsr next | |||
| jmp @post | |||
| @group: jsr next | |||
| jsr parse_full | |||
| jsr chain_none | |||
| lda #EX_OTHER | |||
| sta ex_last | |||
| lda tok_kind | |||
| cmp #TOK_RPAREN | |||
| beq :+ | |||
| lda #E_SYN_EXPECTED_RPAREN | |||
| jmp syn_error | |||
| : jsr next | |||
| jmp @post | |||
| @new: jsr next ; new Name(args) | |||
| lda tok_kind | |||
| cmp #TOK_IDENT | |||
| beq :+ | |||
| lda #E_SYN_EXPECTED_IDENTIFIER | |||
| jmp syn_error | |||
| : jsr new_type | |||
| jsr next | |||
| lda tok_kind | |||
| cmp #TOK_LPAREN | |||
| beq @open | |||
| beq :+ | |||
| lda #E_SYN_EXPECTED_LPAREN | |||
| jmp syn_error | |||
| : jsr ex_save_chain | |||
| jsr parse_args | |||
| jsr ex_restore_chain | |||
| lda #EX_OTHER | |||
| sta ex_last | |||
| @post: lda tok_kind | |||
| cmp #TOK_DOT | |||
| beq @member | |||
| cmp #TOK_LBRACKET | |||
| beq @index | |||
| cmp #TOK_RPAREN | |||
| beq @close | |||
| rts | |||
| @member: | |||
| jsr next | |||
| lda tok_kind ; member names may be keywords (D121) | |||
| cmp #TOK_IDENT | |||
| beq :+ | |||
| cmp #TOK_KW_FIRST | |||
| bcs :+ | |||
| lda #E_SYN_EXPECTED_IDENTIFIER | |||
| jmp syn_error | |||
| : jsr pend_member ; the name and the left side's type | |||
| jsr next | |||
| lda #$00 | |||
| sta call_base | |||
| sta call_name | |||
| jsr resolve_member ; `(` next: a method, else a field | |||
| lda tok_kind | |||
| cmp #TOK_LPAREN | |||
| beq @call | |||
| lda #EX_PLACE | |||
| sta ex_last | |||
| jmp @post | |||
| @call: lda call_base ; the result survives the arguments | |||
| sta sc_base | |||
| lda call_name | |||
| sta sc_name | |||
| jsr ex_save_chain | |||
| jsr parse_args | |||
| jsr ex_restore_chain | |||
| lda #EX_CALL | |||
| sta ex_last | |||
| jmp @post | |||
| @index: jsr element_type | |||
| sta sc_base | |||
| stx sc_name | |||
| jsr ex_save_chain | |||
| jsr next | |||
| jsr parse_full | |||
| lda tok_kind | |||
| cmp #TOK_RBRACKET | |||
| beq :+ | |||
| lda #E_SYN_EXPECTED_RBRACKET | |||
| jmp syn_error | |||
| : jsr next | |||
| jsr ex_restore_chain | |||
| lda #EX_PLACE | |||
| sta ex_last | |||
| jmp @post | |||
| ; Keep sc_base/sc_name (never an array here) across nested expressions. | |||
| ex_save_chain: | |||
| ldx ex_depth | |||
| lda sc_base | |||
| sta ex_cbase,x | |||
| lda sc_name | |||
| sta ex_cname,x | |||
| rts | |||
| ex_restore_chain: | |||
| ldx ex_depth | |||
| lda ex_cbase,x | |||
| sta sc_base | |||
| lda ex_cname,x | |||
| sta sc_name | |||
| lda #$00 | |||
| sta sc_len | |||
| sta sc_len+1 | |||
| rts | |||
| ; `(` [expr {, expr}] `)` with the current token at `(`. | |||
| parse_args: | |||
| jsr next | |||
| lda tok_kind | |||
| cmp #TOK_RPAREN | |||
| beq @close | |||
| jsr chain_none ; operators, literals, other keywords | |||
| jmp @adv | |||
| @ident: lda sc_prev | |||
| cmp #KW_NEW | |||
| @arg: jsr parse_full | |||
| lda tok_kind | |||
| cmp #TOK_COMMA | |||
| bne :+ | |||
| jsr new_type | |||
| jmp @adv | |||
| : jsr resolve_name | |||
| jmp @adv | |||
| @self: jsr self_ref | |||
| jmp @adv | |||
| @open: lda call_base ; a call's result, or none (grouping) | |||
| ldx call_name | |||
| jsr push_type | |||
| jsr chain_none | |||
| jmp @adv | |||
| @index: jsr element_type | |||
| jsr push_type | |||
| jsr chain_none | |||
| jmp @adv | |||
| @close: jsr pop_type | |||
| @adv: lda tok_kind | |||
| sta sc_prev | |||
| jsr next | |||
| jmp @loop | |||
| @end: jmp skip_line ; past the EOL; EOF stays | |||
| jmp @arg | |||
| : cmp #TOK_RPAREN | |||
| beq @close | |||
| lda #E_SYN_EXPECTED_RPAREN | |||
| jmp syn_error | |||
| @close: jmp next | |||
| ; Test trace (pass 1, when ex_trace_on): each statement starts with $FF and | |||
| ; its line; operands are their column; operators follow their operands. | |||
| trace_line: | |||
| lda #TR_LINE | |||
| jsr trace_byte | |||
| lda tok_line | |||
| jsr trace_byte | |||
| lda tok_line+1 | |||
| trace_byte: | |||
| ldy ex_trace_on | |||
| beq @off | |||
| ldy pass | |||
| cpy #$01 | |||
| bne @off | |||
| ldy #$00 | |||
| sta (ex_tp),y | |||
| inc ex_tp | |||
| bne @off | |||
| inc ex_tp+1 | |||
| @off: rts | |||
| ; Remember the member name token; the next token picks field or method. | |||
| pend_member: | |||
| lda #$01 | |||
| sta pend | |||
| lda tok_col | |||
| sta pend_col | |||
| lda tok_len | |||
| @@ -845,8 +1183,8 @@ pend_member: | |||
| ; Resolve the pending member against its left side's type. The member is a | |||
| ; method when the current token is `(`, else a field. | |||
| resolve_member: | |||
| lda #$00 | |||
| sta pend | |||
| lda refs_on | |||
| beq @none | |||
| lda pend_base | |||
| cmp #TY_REF | |||
| bne @none | |||
| @@ -907,7 +1245,10 @@ resolve_member: | |||
| ; A bare name: a local or parameter, else a field of the enclosing class. | |||
| resolve_name: | |||
| jsr find_tok | |||
| lda refs_on | |||
| bne :+ | |||
| jmp chain_none | |||
| : jsr find_tok | |||
| beq @unknown | |||
| sta rn_id | |||
| jsr scope_test | |||
| @@ -939,7 +1280,10 @@ resolve_name: | |||
| jmp err2 | |||
| self_ref: | |||
| lda cur_class | |||
| lda refs_on | |||
| bne :+ | |||
| jmp chain_none | |||
| : lda cur_class | |||
| beq @bad | |||
| sta sc_name | |||
| lda #TY_REF | |||
| @@ -956,7 +1300,10 @@ self_ref: | |||
| ; `new Name`: Name must be a declared class or interface. | |||
| new_type: | |||
| jsr chain_none | |||
| jsr find_tok | |||
| lda refs_on | |||
| bne :+ | |||
| rts | |||
| : jsr find_tok | |||
| beq @unknown | |||
| sta sc_name | |||
| tay | |||
| @@ -1012,39 +1359,6 @@ element_type: | |||
| tax | |||
| rts | |||
| ; Push base A / name X for the matching `)` or `]`. Beyond SCAN_MAX_DEPTH | |||
| ; levels only the count is kept and the result is unknown. | |||
| push_type: | |||
| ldy sc_sp | |||
| cpy #SCAN_MAX_DEPTH | |||
| bcc :+ | |||
| inc sc_over | |||
| rts | |||
| : sta sc_sbase,y | |||
| txa | |||
| sta sc_sname,y | |||
| inc sc_sp | |||
| rts | |||
| pop_type: | |||
| lda sc_over | |||
| beq :+ | |||
| dec sc_over | |||
| jmp chain_none | |||
| : ldy sc_sp | |||
| bne :+ | |||
| jmp chain_none ; unbalanced: E03-S03 reports it | |||
| : dey | |||
| sty sc_sp | |||
| lda sc_sbase,y | |||
| sta sc_base | |||
| lda sc_sname,y | |||
| sta sc_name | |||
| lda #$00 | |||
| sta sc_len | |||
| sta sc_len+1 | |||
| rts | |||
| ; Report A at the pending member's column (pass 2). | |||
| err_pend: | |||
| pha | |||
| @@ -1367,6 +1681,10 @@ next: jsr lex_next | |||
| lda tok_kind | |||
| cmp #TOK_ERROR | |||
| bne :+ | |||
| lda tok_line ; statement errors on this line would | |||
| sta lex_err_line ; only be cascades | |||
| lda tok_line+1 | |||
| sta lex_err_line+1 | |||
| lda tok_err ; LEXERR_n -> E_LEX_INVALID_CHAR + n - 1 | |||
| clc | |||
| adc #E_LEX_INVALID_CHAR - LEXERR_INVALID_CHAR | |||
| @@ -1511,6 +1829,15 @@ end_codes: | |||
| .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 | |||
| bin_kinds: ; binary operators and their precedence | |||
| .byte KW_OR, KW_AND, TOK_EQ, TOK_NE, TOK_LT, TOK_LE, TOK_GT, TOK_GE | |||
| .byte TOK_PLUS, TOK_MINUS, TOK_STAR, TOK_SLASH | |||
| bin_count = * - bin_kinds | |||
| bin_precs: | |||
| .byte 1, 2, 3, 3, 4, 4, 4, 4, 5, 5, 6, 6 | |||
| lit_kinds: | |||
| .byte TOK_NUMBER, TOK_STRING, TOK_CHAR, KW_TRUE, KW_FALSE, KW_NULL | |||
| lit_count = * - lit_kinds | |||
| life_words: ; LIFE_INIT, LIFE_DESTROY order | |||
| .byte 4, "INIT" | |||
| .byte 7, "DESTROY" | |||
| @@ -1574,18 +1901,27 @@ loc_tbase: .res 256 ; by name ID; valid while its scope bit is se | |||
| loc_tname: .res 256 | |||
| loc_tlen_lo: .res 256 | |||
| loc_tlen_hi: .res 256 | |||
| ; pass-2 scan | |||
| sc_prev: .res 1 | |||
| sc_base: .res 1 ; type of the expression so far | |||
| ; statements and expressions | |||
| block_else: .res PARSE_MAX_DEPTH ; the if at this depth has had its else | |||
| refs_on: .res 1 ; resolve names in this body | |||
| lex_err_line: .res 2 ; last line with a lexer error | |||
| stmt_sp: .res 1 ; hardware SP inside parse_statement | |||
| stmt_col: .res 1 | |||
| ex_depth: .res 1 | |||
| ex_min: .res EX_MAX_DEPTH + 1 | |||
| ex_prec: .res EX_MAX_DEPTH + 1 | |||
| ex_op: .res EX_MAX_DEPTH + 1 | |||
| ex_cbase: .res EX_MAX_DEPTH + 1 | |||
| ex_cname: .res EX_MAX_DEPTH + 1 | |||
| ex_last: .res 1 ; EX_* | |||
| ex_tmp: .res 1 | |||
| ex_min_sp: .res 1 ; lowest SP seen in ex_enter | |||
| ex_trace_on: .res 1 ; test hook: record pass-1 expressions | |||
| sc_base: .res 1 ; type of the member chain so far | |||
| sc_name: .res 1 | |||
| sc_len: .res 2 | |||
| sc_sp: .res 1 | |||
| sc_over: .res 1 | |||
| sc_sbase: .res SCAN_MAX_DEPTH | |||
| sc_sname: .res SCAN_MAX_DEPTH | |||
| call_base: .res 1 | |||
| call_name: .res 1 | |||
| pend: .res 1 | |||
| pend_id: .res 1 | |||
| pend_col: .res 1 | |||
| pend_len: .res 1 | |||
| @@ -0,0 +1,58 @@ | |||
| interface Source | |||
| method next() as word | |||
| end interface | |||
| class Counter implements Source | |||
| private word value | |||
| private word[4] seen | |||
| public method init(start as word) | |||
| self.value = start | |||
| end method | |||
| public method next() as word | |||
| self.value = self.value + 1 | |||
| return self.value | |||
| end method | |||
| public method twice() as Counter | |||
| return self | |||
| end method | |||
| public method fill(n as word) | |||
| var i as word | |||
| for i = 0 to 3 step 1 | |||
| seen[i] = self.next() | |||
| end for | |||
| for i = 3 to 0 step -1 | |||
| if seen[i] = 0 | |||
| return | |||
| else | |||
| if i > n and not (i = 2) | |||
| seen[i] = seen[i - 1] | |||
| end if | |||
| end if | |||
| end for | |||
| while value < n | |||
| value = value + 1 | |||
| end while | |||
| end method | |||
| end class | |||
| program Main | |||
| var c as Counter | |||
| var s as Source | |||
| var w as word | |||
| c = new Counter((1 + 2) * 3) | |||
| c.fill(c.next()) | |||
| c.twice().twice().fill(w) | |||
| s = c | |||
| w = s.next() | |||
| print "done" | |||
| print 'x' | |||
| print true | |||
| print w | |||
| if s <> null | |||
| delete s | |||
| end if | |||
| end program | |||
| @@ -1,15 +1,18 @@ | |||
| ; E03-S01/E03-S02: parser test in x128. | |||
| ; E03-S01..E03-S03: 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 | |||
| ; $FF, fixture index, src_load result, diagnostic count, lowest SP seen | |||
| ; by the expression parser ($FF if it never ran) | |||
| ; per declared top-level type: kind, line lo, line hi, spelling length, | |||
| ; spelling bytes | |||
| ; $FC | |||
| ; per member row: owner name, member name, flags, type, parameter count, | |||
| ; then each parameter's type. A name is a length and its spelling (length | |||
| ; 0 = none); a type is base, name, length lo, length hi. | |||
| ; for the TRACE_INDEX fixture only: $FB, length lo/hi, the expression | |||
| ; trace (parser.inc TR_*) | |||
| ; $FD | |||
| ; then $FE at the end. Mailbox: MB_DATA+0/1 dump address, +2/3 length. | |||
| @@ -24,7 +27,8 @@ | |||
| .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 | |||
| .import tb_get, mem_count | |||
| .import tb_get, mem_count, ex_trace_on, ex_min_sp | |||
| .importzp ex_tp | |||
| DUMP_SIZE = $2000 | |||
| @@ -46,6 +50,7 @@ basic_end: | |||
| .segment "ZEROPAGE" | |||
| dump_ptr: .res 2 | |||
| fx_ptr: .res 2 | |||
| fx_tmp: .res 2 | |||
| .segment "STARTUP" | |||
| .export test_entry, dump | |||
| @@ -105,6 +110,8 @@ test_entry: | |||
| inc failures | |||
| lda #$00 | |||
| jsr emit | |||
| lda #$FF | |||
| jsr emit | |||
| jmp @advance | |||
| @parse: jsr names_init | |||
| lda name_lo | |||
| @@ -113,13 +120,31 @@ test_entry: | |||
| jsr diag_init | |||
| lda #$00 ; ERR files are the evidence; keep it fast | |||
| sta diag_screen | |||
| lda #<trace_buf | |||
| sta ex_tp | |||
| lda #>trace_buf | |||
| sta ex_tp+1 | |||
| lda #$00 | |||
| ldx fx_index | |||
| cpx #TRACE_INDEX | |||
| bne :+ | |||
| lda #$01 | |||
| : sta ex_trace_on | |||
| jsr parse_file | |||
| lda #$00 | |||
| sta ex_trace_on | |||
| jsr diag_finish | |||
| jsr emit | |||
| lda ex_min_sp | |||
| jsr emit | |||
| jsr dump_types | |||
| lda #$FC | |||
| jsr emit | |||
| jsr dump_members | |||
| lda fx_index | |||
| cmp #TRACE_INDEX | |||
| bne @advance | |||
| jsr dump_trace | |||
| @advance: | |||
| lda #$FD | |||
| jsr emit | |||
| @@ -132,9 +157,10 @@ test_entry: | |||
| : inc fx_index | |||
| lda fx_index | |||
| cmp #FIXTURE_COUNT | |||
| bne @fixture | |||
| beq :+ | |||
| jmp @fixture | |||
| lda #$FE | |||
| : lda #$FE | |||
| jsr emit | |||
| lda #<dump ; report while the private ZP is mapped | |||
| sta MB_DATA+0 | |||
| @@ -262,6 +288,38 @@ dump_members: | |||
| jmp @next | |||
| @done: rts | |||
| ; Emit $FB, the trace length, and the trace bytes. | |||
| dump_trace: | |||
| lda #$FB | |||
| jsr emit | |||
| sec | |||
| lda ex_tp | |||
| sbc #<trace_buf | |||
| sta trace_len | |||
| jsr emit | |||
| lda ex_tp+1 | |||
| sbc #>trace_buf | |||
| sta trace_len+1 | |||
| jsr emit | |||
| lda #<trace_buf | |||
| sta fx_tmp | |||
| lda #>trace_buf | |||
| sta fx_tmp+1 | |||
| @byte: lda fx_tmp | |||
| cmp ex_tp | |||
| bne :+ | |||
| lda fx_tmp+1 | |||
| cmp ex_tp+1 | |||
| beq @done | |||
| : ldy #$00 | |||
| lda (fx_tmp),y | |||
| jsr emit | |||
| inc fx_tmp | |||
| bne @byte | |||
| inc fx_tmp+1 | |||
| bne @byte | |||
| @done: rts | |||
| ; Emit the four type columns starting at column A for row Y. | |||
| emit_type: | |||
| sta col | |||
| @@ -358,7 +416,11 @@ fixtures: | |||
| FIXTURE "LIFECYCLE.N8" | |||
| FIXTURE "SCOPES.N8" | |||
| FIXTURE "PRIVATE.N8" | |||
| FIXTURE_COUNT = 28 | |||
| FIXTURE "PREC.N8" | |||
| FIXTURE "FORMS.N8" | |||
| FIXTURE "SYNTAX.N8" | |||
| FIXTURE_COUNT = 31 | |||
| TRACE_INDEX = 28 ; PREC.N8 | |||
| messages: | |||
| ok_msg: .byte "NOVA128 PARSER TEST OK", $0D, $00 | |||
| @@ -380,4 +442,6 @@ prow: .res 1 | |||
| pleft: .res 1 | |||
| col: .res 1 | |||
| trow: .res 1 | |||
| trace_len: .res 2 | |||
| trace_buf: .res 1024 | |||
| dump: .res DUMP_SIZE | |||
| @@ -0,0 +1,17 @@ | |||
| // postfix order per statement: operands by name, then operators. | |||
| // binary operators are left associative; unary operators nest rightwards | |||
| 6: a a b c * + | |||
| 7: a a b - c - | |||
| 8: a a b / c / | |||
| 9: a a b + c * | |||
| 10: a a neg b * | |||
| 11: a a neg neg | |||
| 12: d d not d and | |||
| 13: d d d d and or | |||
| 14: d a b < d = | |||
| 15: d a b = a c <> d not and or | |||
| 16: a a b * c a * + b c / - | |||
| 17: d a b + c a * < | |||
| 18: d a b <= a c >= or | |||
| 19: a b + c neg * | |||
| 20: a a b c - - | |||
| @@ -0,0 +1,21 @@ | |||
| program main | |||
| var a as integer | |||
| var b as integer | |||
| var c as integer | |||
| var d as boolean | |||
| a = a + b * c | |||
| a = a - b - c | |||
| a = a / b / c | |||
| a = (a + b) * c | |||
| a = -a * b | |||
| a = - - a | |||
| d = not d and d | |||
| d = d or d and d | |||
| d = a < b = d | |||
| d = a = b or a <> c and not d | |||
| a = a * b + c * a - b / c | |||
| d = a + b < c * a | |||
| d = a <= b or a >= c | |||
| print (a + b) * -c | |||
| a = a - (b - c) | |||
| end program | |||
| @@ -0,0 +1,20 @@ | |||
| // one independent syntax error per line; each line after an error parses. | |||
| // line 23: the lexer error is not followed by a cascaded LINE ENDS TOO EARLY | |||
| // line 24: 18 nested groups exceed the 16-level expression bound, 15 do not | |||
| SYN_UNEXPECTED_EOL 4 10 | |||
| SYN_EXPECTED_RPAREN 5 13 | |||
| SYN_EXPECTED_RBRACKET 6 10 | |||
| SYN_NOT_A_STATEMENT 7 3 | |||
| SYN_NOT_ASSIGNABLE 8 3 | |||
| SYN_EXPECTED_EOL 9 9 | |||
| SYN_UNEXPECTED_EOL 10 8 | |||
| SYN_EXPECTED_EQUALS 11 9 | |||
| SYN_EXPECTED_TO 13 13 | |||
| SYN_UNEXPECTED_TOKEN 15 13 | |||
| SYN_UNEXPECTED_TOKEN 17 3 | |||
| SYN_UNEXPECTED_TOKEN 20 3 | |||
| SYN_UNEXPECTED_TOKEN 22 7 | |||
| LEX_BAD_CHAR_LITERAL 23 7 | |||
| RES_EXPR_TOO_DEEP 24 23 | |||
| // pass 2: name errors are still found after all of the above | |||
| SEM_UNKNOWN_NAME 26 3 | |||
| @@ -0,0 +1,28 @@ | |||
| program main | |||
| var a as word | |||
| var b as word[4] | |||
| a = 1 + | |||
| a = (1 + 2 | |||
| a = b[1 | |||
| a + 1 | |||
| 5 = a | |||
| a = 1 a = 2 | |||
| for a 1 to 2 | |||
| end for | |||
| for a = 1 2 | |||
| end for | |||
| while a = ) | |||
| end while | |||
| else | |||
| if a = 1 | |||
| else | |||
| else | |||
| end if | |||
| a = * 2 | |||
| a = 'xy' | |||
| a = ((((((((((((((((((1)))))))))))))))))) | |||
| a = (((((((((((((((1))))))))))))))) | |||
| zz = 1 | |||
| return a | |||
| end program | |||
Powered by TurnKey Linux.