Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
| @@ -39,10 +39,12 @@ SEGMENTS { | |||
| LOADADDR: load = LOADADDR, type = ro; | |||
| EXEHDR: load = HEADER, type = ro; | |||
| STARTUP: load = MAIN, type = ro; # first: SYS 7181 enters at $1C0D | |||
| # COMMON's load image comes next so mmu_init, which copies it in the | |||
| # system mapping (BASIC ROM over $4000-$7FFF), reads it below $4000. | |||
| COMMON: load = MAIN, run = COMMONRAM, type = rw, define = yes; | |||
| CODE: load = MAIN, type = ro; | |||
| RODATA: load = MAIN, type = ro, optional = yes; | |||
| DATA: load = MAIN, type = rw, optional = yes; | |||
| COMMON: load = MAIN, run = COMMONRAM, type = rw, define = yes; | |||
| BSS: load = MAIN, type = bss, optional = yes, define = yes; | |||
| DISKBUF: load = DISKBUF, type = bss, optional = yes; | |||
| SCRATCH: load = SCRATCH, type = bss, optional = yes; | |||
| @@ -174,6 +174,18 @@ 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. | |||
| E03-S02 adds members and scopes. Pass 1 records every field and method | |||
| signature (access, type, parameter types) in bank-1 member and parameter | |||
| tables, rejects duplicate fields or methods per type (separate namespaces), | |||
| `init`/`destroy` shapes (D122), and `var` after a method statement or inside | |||
| a block. Each method signature and the program open a fresh local scope whose | |||
| 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. | |||
| ### Pass 1 — declarations and layout | |||
| - tokenize the complete source; | |||
| @@ -48,6 +48,8 @@ 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 `.`. | | |||
| | D122 | `init` returns no value (`as type` is E151), like `destroy`. | Spec 6.3 says `new Class(args)` yields the object, so an `init` result could never be observed. Spec 6.3 already gives `destroy` no parameters and no result (E152, E151). Implemented by E03-S02. | | |||
| | D123 | Inside a method an unqualified name is a parameter or local, else a field of the enclosing class; methods are always called through an expression such as `self.tick()`. | Spec 9 lets a local "shadow a field", which implies fields are in scope unqualified, but no fixture used one. Spec 7 defines calls only as `expression.method(arguments)`, so a bare `tick()` is an unknown name. Implemented by E03-S02. | | |||
| ## Deferred | |||
| @@ -85,6 +85,8 @@ After 20 errors the next one is replaced by E221 and compilation stops. | |||
| | Code | Symbol | Message | | |||
| |---|---|---| | |||
| | E150 | `SEM_MISSING_RETURN` | MISSING RETURN VALUE | | |||
| | E151 | `SEM_LIFECYCLE_RETURN` | % CANNOT RETURN A VALUE | | |||
| | E152 | `SEM_LIFECYCLE_PARAMS` | % TAKES NO PARAMETERS | | |||
| ## Disk (E200-E219) | |||
| @@ -100,3 +102,5 @@ 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 | | |||
| | E223 | `RES_TOO_MANY_MEMBERS` | MORE THAN 255 MEMBERS | | |||
| | E224 | `RES_TOO_MANY_PARAMS` | MORE THAN 255 PARAMETERS | | |||
| @@ -51,10 +51,11 @@ 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) | | |||
| | `parser.s` | 4,608 | 18 columns of 256 bytes (`src/common/parser.inc`): type table by name ID (kind, declaration line, member range; E03-S01/E03-S02), 255 member rows (owner, name, flags, type, parameter range) and 255 parameter-type rows (E03-S02) | | |||
| BSS is not zeroed at load: every table must be cleared by its owner before | |||
| use (`names_init`, `parser.s` `tt_clear`). | |||
| use (`names_init`, `parser.s` `tt_clear`). The member and parameter rows are | |||
| bounded by their counts instead, so only the type columns are cleared. | |||
| | `$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` | |||
| @@ -77,6 +78,15 @@ 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`. | |||
| `mmu_init` runs in the system mapping too, before the first `mmu_bank0`, and | |||
| copies the COMMON load image with ordinary loads. `cfg/compiler.cfg` therefore | |||
| places that image directly after STARTUP, and `mmu.s` asserts at link time that | |||
| both `mmu_init` and the image end at or below `$4000`. E03-S02 found this when | |||
| the parser test's load image first crossed `$4000`: COMMON was copied from | |||
| BASIC ROM and the test crashed into the monitor. KERNAL file names are not | |||
| affected: `diskio.s` selects bank 0 with SETBNK, and the KERNAL reads names | |||
| through an all-RAM mapping. | |||
| 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 | |||
| @@ -0,0 +1,117 @@ | |||
| --- | |||
| story: E03-S02 | |||
| owner: e03_member_parser | |||
| status: done | |||
| started: 2026-11-07 | |||
| depends-on: [E03-S01] | |||
| --- | |||
| # E03-S02 Claim | |||
| ## Intended change | |||
| Extend `src/compiler/parser.s` so pass 1 records every interface and class | |||
| member (fields and method signatures, with access, type, and parameter | |||
| types) in bank-1 tables, enforces unique field and method names per type and | |||
| the `init`/`destroy` shapes, and keeps locals leading in method bodies. Each | |||
| method and the program get their own local scope with recorded local types. | |||
| Pass 2 scans statement lines for name references: bare names must resolve to | |||
| a local/parameter or (inside a method) a field of the enclosing class, and | |||
| `expr.member` chains through locals, fields, `self`, `new`, calls, and | |||
| indexing to report unknown and private members at the member's position. | |||
| E03-S03 replaces the scan with the real expression parser and reuses the | |||
| lookups. | |||
| ## File area | |||
| - `src/compiler/parser.s`, `src/common/parser.inc` | |||
| - `src/compiler/names.s` (adds lookup-only `name_find`) | |||
| - `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`, `docs/decisions.md` | |||
| - `planning/claims/E03-S02.md` | |||
| - generated files under `build/` | |||
| ## Acceptance evidence | |||
| - [x] Parses access, signatures, parameters, fields, and leading local | |||
| declarations: the member dump (owner, name, field/method, access, type, | |||
| parameter types) matches an independent source-derived oracle for all 7 | |||
| accepted fixtures and `members`/`lifecycle`/`scopes`/`private` (51 member | |||
| rows, default field-private/method-public included). `scopes.n8` rejects | |||
| `var` after a method statement (22:5) and inside a block (40:5); the | |||
| program may still interleave `var` at its top level (35). | |||
| - [x] Enforces unique method names and valid `init`/`destroy` shapes: | |||
| `members.n8` rejects a second method `area` in an interface (3:10) and a | |||
| class (17:18) and a second field `size` (9:15), while one field and one | |||
| method may share a name. `lifecycle.n8` rejects init/destroy in an | |||
| interface (E030), `init ... as word` (E151, D122), a second `init` | |||
| (E060), and destroy with parameters (E152, first one) and a result | |||
| (E151); `class Fine` shows parameterized init and private destroy pass. | |||
| - [x] Assigns lexical scopes without leaking names across methods/classes: | |||
| `scopes.n8` reports `one()`'s local and parameter in `two()`, class A's | |||
| field in class B, `three()`'s parameter and local in `four()`, method | |||
| locals and fields in the program, `self` in the program, and an unknown | |||
| `new Ghost()` (8 pass-2 diagnostics, exact positions). Mutation: removing | |||
| the per-signature scope reset fails 5 fixtures. | |||
| - [x] Reports private-member violations with source positions: | |||
| `private.n8` reports 6 E120s at the member's column, through a parameter, | |||
| local, call result (`v.make().hidden`, `p.look().secret`), and index | |||
| (`list[0].secret`), with Thief resolving Vault's members before Vault is | |||
| declared; access from Vault's own methods (through `self`, a bare field, | |||
| or another Vault) passes; unknown members are E061. Mutation: disabling | |||
| the privacy test drops the 6 E120s and fails the fixture. | |||
| ## Coordination notes | |||
| - No other claim is active. E03-S03 will own statement/expression grammar; | |||
| the pass-2 scan here only resolves names and members token by token and | |||
| reports nothing about expression shape or types. | |||
| ## Work log | |||
| - Baseline: `scripts/verify-parser-test.ps1` passes 24 fixtures before any | |||
| change. | |||
| - Added `name_find` (lookup without interning) so pass 2 never grows the | |||
| name table: a name first seen in a statement was never declared. | |||
| - Appended E151 `SEM_LIFECYCLE_RETURN`, E152 `SEM_LIFECYCLE_PARAMS`, E223 | |||
| `RES_TOO_MANY_MEMBERS`, E224 `RES_TOO_MANY_PARAMS`; docs regenerated. | |||
| - First x128 run hung with the C128 in its ML monitor. Cause: the test PRG | |||
| now extends past `$4000`, and `mmu_init` copies the COMMON load image | |||
| (then placed after DATA) in the system mapping, where BASIC ROM covers | |||
| `$4000-$7FFF`. Fix: `cfg/compiler.cfg` loads COMMON directly after | |||
| STARTUP, and `mmu.s` asserts at link time that `mmu_init` and the COMMON | |||
| image stay at or below `$4000`. Region addresses (D117) are unchanged. | |||
| Map skeleton, source, lexer, names, and diagnostics suites were rerun on | |||
| the new layout. | |||
| - Verifier: members are compared against an independent regex oracle over | |||
| the fixture source (declaration order, access defaults, first | |||
| declaration wins) for the 7 accepted fixtures and the 4 new ones. | |||
| ## Completion | |||
| - Tests run: `scripts/verify-parser-test.ps1` (final run, plus the two | |||
| mutations above); `scripts/build-diag-test.ps1` catalog check; because | |||
| `cfg/compiler.cfg` changed, `verify-map-skeleton.ps1` (including its | |||
| overflow variants), `verify-source-test.ps1`, `verify-lexer-test.ps1`, | |||
| `verify-names-test.ps1`, and `verify-diag-test.ps1`. | |||
| - Results: 28 parser fixtures as expected (2 still pending E03-S03, with no | |||
| false diagnostics); both mutations caught; all five other suites pass. | |||
| - Files changed: `src/compiler/parser.s`, `src/common/parser.inc`, | |||
| `src/compiler/names.s`, `src/compiler/mmu.s`, `cfg/compiler.cfg`, | |||
| `src/common/diagcat.inc`, `docs/diagnostics.md`, `docs/decisions.md` | |||
| (D122, D123 proposed), `docs/architecture.md`, `docs/memory-map.md`, | |||
| `tests/parser/` (4 fixtures + expectations, harness dump), | |||
| `scripts/build-parser-test.ps1`, `scripts/verify-parser-test.ps1`, this | |||
| claim. | |||
| - Remaining risks: D121-D123 await approval. The pass-2 scan is token based: | |||
| where it cannot know a left side's type (expression results, grouping, | |||
| primitives, array-valued calls) it checks nothing, and a field used as | |||
| `x.f(...)` reports "not declared" rather than a type error; E03-S03/S04 | |||
| should replace it. Access to `init` from `new` outside the class is not | |||
| checked (spec silent). The type descriptor (base, name, 16-bit length) is | |||
| provisional until E03-S04. Member and parameter capacities (255 each) are | |||
| untested at the limit. CODE/DATA may now exceed `$4000`; only `mmu_init` | |||
| and code run before `mmu_bank0` must stay below it (asserted for | |||
| `mmu_init` and COMMON; test STARTUP code is first in MAIN). | |||
| @@ -1,4 +1,4 @@ | |||
| # E03-S01: build the parser test PRG and a fresh D81 holding every grammar and | |||
| # E03-S01/E03-S02: 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() | |||
| @@ -37,6 +37,10 @@ $script:ParserFixtures = [ordered]@{ | |||
| "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") | |||
| "members.n8" = @("$p\members.n8", "exact", "$p\members.expect") | |||
| "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") | |||
| } | |||
| $link = Invoke-Nova128Link -Name "parser-test" -BuildDir $out ` | |||
| @@ -28,22 +28,97 @@ try { | |||
| if ($mb[3] -ne 0xA5) { throw ("C128 reported FAIL (code `${0:X2})" -f $mb[4]) } | |||
| # ------------------------------------------------------------ decode dump | |||
| $records = @(); $p = 0 | |||
| function ConvertFrom-Spelling([byte[]]$bytes) { | |||
| return -join ($bytes | ForEach-Object { | |||
| if ($_ -ge 0x41 -and $_ -le 0x5A) { [char]($_ + 0x20) } elseif ($_ -ge 0xC1 -and $_ -le 0xDA) { [char]($_ - 0x80) } else { [char]$_ } }) | |||
| } | |||
| # A name is a length and its spelling; a type is base, name, length lo/hi. | |||
| # Both are rendered lowercase, as the oracle below writes them. | |||
| function Read-DumpName { | |||
| $len = $dump[$script:p]; $script:p++ | |||
| if ($len -eq 0) { return "" } | |||
| $s = ConvertFrom-Spelling $dump[$script:p..($script:p + $len - 1)] | |||
| $script:p += $len | |||
| return $s.ToLower() | |||
| } | |||
| function Read-DumpType { | |||
| $base = $dump[$script:p]; $script:p++ | |||
| $name = Read-DumpName | |||
| $n = $dump[$script:p] + 256 * $dump[$script:p + 1]; $script:p += 2 | |||
| $t = @("none", "boolean", "byte", "char", "integer", "word", $name)[$base] | |||
| if ($n) { $t += "[$n]" } | |||
| return $t | |||
| } | |||
| $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 = @() } | |||
| $r = [pscustomobject]@{ Index = $dump[$p + 1]; Load = $dump[$p + 2]; Count = $dump[$p + 3]; Types = @(); Members = @() } | |||
| $p += 4 | |||
| while ($dump[$p] -ne 0xFD) { | |||
| while ($dump[$p] -ne 0xFD -and $dump[$p] -ne 0xFC) { | |||
| $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]$_ } }) | |||
| $spell = ConvertFrom-Spelling $dump[($p + 4)..($p + 3 + $len)] | |||
| $r.Types += ("{0} {1} {2}" -f @("", "interface", "class", "program")[$dump[$p]], $spell, ($dump[$p + 1] + 256 * $dump[$p + 2])) | |||
| $p += 4 + $len | |||
| } | |||
| if ($dump[$p] -eq 0xFC) { | |||
| $p++ | |||
| while ($dump[$p] -ne 0xFD) { | |||
| $owner = Read-DumpName | |||
| $name = Read-DumpName | |||
| $flags = $dump[$p]; $p++ | |||
| $type = Read-DumpType | |||
| $count = $dump[$p]; $p++ | |||
| $params = @(for ($k = 0; $k -lt $count; $k++) { Read-DumpType }) | |||
| $r.Members += ("{0}.{1} {2} {3} {4} ({5})" -f $owner, $name, | |||
| $(if ($flags -band 1) { "method" } else { "field" }), | |||
| $(if ($flags -band 2) { "private" } else { "public" }), $type, ($params -join ",")) | |||
| } | |||
| } | |||
| $p++ | |||
| $records += $r | |||
| } | |||
| # 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. | |||
| $typeRx = '(boolean|byte|char|integer|word|\w+)(\[(\d+)\])?' | |||
| function Format-SourceType([string]$base, [string]$n) { | |||
| $t = $base.ToLower(); if ($n) { $t += "[$n]" }; return $t | |||
| } | |||
| function Get-SourceMembers([string]$path) { | |||
| $members = @(); $seen = @{}; $owner = $null; $inBody = $false | |||
| foreach ($line in Get-Content -LiteralPath $path) { | |||
| if ($line -match '^\s*(interface|class)\s+(\w+)') { $ownerKind = $Matches[1]; $owner = $Matches[2].ToLower(); continue } | |||
| if ($line -match '^\s*end\s+(interface|class)\b') { $owner = $null; continue } | |||
| if ($line -match '^\s*end\s+method\b') { $inBody = $false; continue } | |||
| if (-not $owner -or $inBody) { continue } | |||
| $m = [regex]::Match($line, "^\s*(public|private)?\s*method\s+(\w+)\s*\((.*)\)\s*(as\s+$typeRx)?\s*$") | |||
| if ($m.Success) { | |||
| $kind = "method"; $name = $m.Groups[2].Value.ToLower() | |||
| $access = if ($m.Groups[1].Value -eq "private") { "private" } else { "public" } | |||
| $type = if ($m.Groups[4].Success) { Format-SourceType $m.Groups[5].Value $m.Groups[7].Value } else { "none" } | |||
| $params = @(foreach ($part in ($m.Groups[3].Value -split ',' | Where-Object { $_.Trim() })) { | |||
| $pm = [regex]::Match($part.Trim(), "^\w+\s+as\s+$typeRx$") | |||
| Format-SourceType $pm.Groups[1].Value $pm.Groups[3].Value }) | |||
| $inBody = ($ownerKind -eq "class") # interface methods have no body | |||
| } else { | |||
| $m = [regex]::Match($line, "^\s*(public|private)?\s*$typeRx\s+(\w+)\s*$") | |||
| if (-not $m.Success) { continue } | |||
| $kind = "field"; $name = $m.Groups[5].Value.ToLower() | |||
| $access = if ($m.Groups[1].Value -eq "public") { "public" } else { "private" } | |||
| $type = Format-SourceType $m.Groups[2].Value $m.Groups[4].Value | |||
| $params = @() | |||
| } | |||
| $key = "$owner.$kind.$name" | |||
| if ($seen[$key]) { continue } | |||
| $seen[$key] = $true | |||
| $members += ("{0}.{1} {2} {3} {4} ({5})" -f $owner, $name, $kind, $access, $type, ($params -join ",")) | |||
| } | |||
| return ,$members | |||
| } | |||
| # Fixtures whose member tables are compared (headers all well formed). | |||
| $memberChecked = @("members.n8", "lifecycle.n8", "scopes.n8", "private.n8") | |||
| $c1541 = Get-ViceTool "c1541" | |||
| $listing = (& $c1541 -attach $disk -list 2>&1) | Out-String | |||
| function Get-ErrLines([string]$base) { | |||
| @@ -114,6 +189,13 @@ foreach ($name in $ParserFixtures.Keys) { | |||
| $detail = "{0} diagnostics" -f $got.Count | |||
| } | |||
| } | |||
| if ($group -eq "accepted" -or $memberChecked -contains $name) { | |||
| $wantMembers = Get-SourceMembers $src | |||
| if (($r.Members -join ';') -ne ($wantMembers -join ';')) { | |||
| $problems += "members [$($r.Members -join '; ')] expected [$($wantMembers -join '; ')]" | |||
| } | |||
| $detail += ", {0} members" -f $r.Members.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 "; ") } | |||
| @@ -65,6 +65,8 @@ | |||
| ; ---------------------------------------------------- methods and returns | |||
| DIAGNOSTIC SEM_MISSING_RETURN, 150, "MISSING RETURN VALUE" | |||
| DIAGNOSTIC SEM_LIFECYCLE_RETURN, 151, "% CANNOT RETURN A VALUE" | |||
| DIAGNOSTIC SEM_LIFECYCLE_PARAMS, 152, "% TAKES NO PARAMETERS" | |||
| ; ------------------------------------------------------------------- disk | |||
| DIAGNOSTIC DSK_SOURCE_UNREADABLE, 200, "CANNOT READ SOURCE; DOS %" | |||
| @@ -74,3 +76,5 @@ | |||
| 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" | |||
| DIAGNOSTIC RES_TOO_MANY_MEMBERS, 223, "MORE THAN 255 MEMBERS" | |||
| DIAGNOSTIC RES_TOO_MANY_PARAMS, 224, "MORE THAN 255 PARAMETERS" | |||
| @@ -1,13 +1,56 @@ | |||
| ; parser.inc - top-level type table and parser limits (E03-S01). | |||
| ; parser.inc - parser tables and limits (E03-S01, E03-S02). | |||
| .ifndef PARSER_INC | |||
| PARSER_INC = 1 | |||
| ; Top-level type kinds (type table column C_TYPE_KIND) | |||
| TYPE_NONE = 0 | |||
| TYPE_INTERFACE = 1 | |||
| 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 | |||
| ; Declared-type descriptor (provisional until E03-S04): base, name, length. | |||
| ; base TY_*; TY_NONE for a method without a result | |||
| ; name class/interface name ID when base = TY_REF, else 0 | |||
| ; length array length (2 bytes); 0 = not an array | |||
| TY_NONE = 0 | |||
| TY_BOOLEAN = 1 | |||
| TY_BYTE = 2 | |||
| TY_CHAR = 3 | |||
| TY_INTEGER = 4 | |||
| TY_WORD = 5 | |||
| TY_REF = 6 | |||
| ; Member flags | |||
| MF_METHOD = $01 ; clear = field | |||
| MF_PRIVATE = $02 ; clear = public | |||
| MEMBERS_MAX = 255 ; rows 0..254, all classes and interfaces | |||
| PARAMS_MAX = 255 ; rows 0..254, all method signatures | |||
| ; Bank-1 parser tables: 256-byte columns, consecutive from type_kind. | |||
| ; tb_get/tb_set take A = column and Y = row (a name ID for type columns). | |||
| C_TYPE_KIND = 0 ; TYPE_* | |||
| C_TYPE_LINE_LO = 1 ; declaration line | |||
| C_TYPE_LINE_HI = 2 | |||
| C_TYPE_MFIRST = 3 ; first member row | |||
| C_TYPE_MCOUNT = 4 ; member rows (contiguous, declaration order) | |||
| C_MEM_OWNER = 5 ; owning class/interface name ID | |||
| C_MEM_NAME = 6 ; member name ID | |||
| C_MEM_FLAGS = 7 ; MF_* | |||
| C_MEM_TBASE = 8 ; field type / method result type | |||
| C_MEM_TNAME = 9 | |||
| C_MEM_TLEN_LO = 10 | |||
| C_MEM_TLEN_HI = 11 | |||
| C_MEM_PFIRST = 12 ; first parameter row | |||
| C_MEM_PCOUNT = 13 ; parameter rows (declaration order) | |||
| C_PAR_TBASE = 14 ; parameter type | |||
| C_PAR_TNAME = 15 | |||
| C_PAR_TLEN_LO = 16 | |||
| C_PAR_TLEN_HI = 17 | |||
| TABLE_COLUMNS = 18 | |||
| .endif | |||
| @@ -109,6 +109,10 @@ mmu_init: | |||
| lda #>mmu_nmi_guard | |||
| sta NMINV+1 | |||
| rts | |||
| ; mmu_init runs, and reads the COMMON load image, in the system mapping, where | |||
| ; BASIC ROM hides bank-0 RAM from $4000. Both must stay below it. | |||
| .assert * <= $4000, error, "mmu_init must lie below $4000" | |||
| .assert __COMMON_LOAD__ + __COMMON_SIZE__ <= $4000, error, "COMMON load image must lie below $4000" | |||
| ; ---------------------------------------------------------------- COMMON | |||
| .segment "COMMON" | |||
| @@ -20,7 +20,7 @@ | |||
| .importzp mmu_ptr, mmu_src | |||
| .import src_line | |||
| .export names_init, name_intern, name_declare, name_get, names_count | |||
| .export names_init, name_intern, name_find, name_declare, name_get, names_count | |||
| .export name_out, name_out_len, name_out_flags | |||
| .export name_heads, name_recs | |||
| @@ -49,6 +49,22 @@ name_intern: | |||
| @found: clc | |||
| @out: rts | |||
| ; Look up the identifier at src_line[A-1], length X, without adding it. | |||
| ; out: C=0, A = ID; C=1, A = 0 when the name was never interned | |||
| name_find: | |||
| jsr set_key | |||
| bcs @no | |||
| lda #$00 | |||
| sta nk_add | |||
| jsr nt_find | |||
| lda nk_result | |||
| beq @no | |||
| clc | |||
| rts | |||
| @no: lda #$00 | |||
| sec | |||
| rts | |||
| ; Record the declaration spelling at src_line[Y-1], length X, for ID A, | |||
| ; unless an earlier declaration already did. The spelling must normalize to | |||
| ; the same name (the caller interned it from the same token). | |||
| @@ -0,0 +1,9 @@ | |||
| // interfaces declare no init/destroy; init and destroy return nothing; | |||
| // destroy takes no parameters (first one reported); one init per class. | |||
| // class Fine: init may take parameters and destroy may be private | |||
| SYN_UNEXPECTED_TOKEN 2 10 | |||
| SYN_UNEXPECTED_TOKEN 3 10 | |||
| SEM_LIFECYCLE_RETURN 7 33 | |||
| SEM_DUPLICATE_NAME 10 17 | |||
| SEM_LIFECYCLE_PARAMS 12 25 | |||
| SEM_LIFECYCLE_RETURN 12 56 | |||
| @@ -0,0 +1,25 @@ | |||
| interface Life | |||
| method init() | |||
| method destroy() | |||
| end interface | |||
| class Res | |||
| public method init(n as word) as word | |||
| return n | |||
| end method | |||
| public method init() | |||
| end method | |||
| public method destroy(flag as boolean, more as byte) as boolean | |||
| return flag | |||
| end method | |||
| end class | |||
| class Fine | |||
| method init(a as word, b as Res) | |||
| end method | |||
| private method destroy() | |||
| end method | |||
| end class | |||
| program Main | |||
| end program | |||
| @@ -0,0 +1,5 @@ | |||
| // fields and methods are separate namespaces per type: `size` and `area` | |||
| // may each be a field and a method once, never twice in the same namespace | |||
| SEM_DUPLICATE_NAME 3 10 | |||
| SEM_DUPLICATE_NAME 9 15 | |||
| SEM_DUPLICATE_NAME 17 18 | |||
| @@ -0,0 +1,24 @@ | |||
| interface Shape | |||
| method area() as word | |||
| method area() as word | |||
| method name() | |||
| end interface | |||
| class Box implements Shape | |||
| word size | |||
| public byte size | |||
| public word area | |||
| public method size() as word | |||
| return 0 | |||
| end method | |||
| public method area() as word | |||
| return 0 | |||
| end method | |||
| private method area() | |||
| end method | |||
| method name() | |||
| end method | |||
| end class | |||
| program Main | |||
| end program | |||
| @@ -1,4 +1,4 @@ | |||
| ; E03-S01: parser test in x128. | |||
| ; E03-S01/E03-S02: 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 | |||
| @@ -6,6 +6,10 @@ | |||
| ; $FF, fixture index, src_load result, diagnostic count | |||
| ; 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. | |||
| ; $FD | |||
| ; then $FE at the end. Mailbox: MB_DATA+0/1 dump address, +2/3 length. | |||
| @@ -20,6 +24,7 @@ | |||
| .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 | |||
| DUMP_SIZE = $2000 | |||
| @@ -112,6 +117,9 @@ test_entry: | |||
| jsr diag_finish | |||
| jsr emit | |||
| jsr dump_types | |||
| lda #$FC | |||
| jsr emit | |||
| jsr dump_members | |||
| @advance: | |||
| lda #$FD | |||
| jsr emit | |||
| @@ -211,6 +219,90 @@ dump_types: | |||
| bne @next | |||
| @done: rts | |||
| ; Emit every member row with its parameter types. | |||
| dump_members: | |||
| lda #$00 | |||
| sta row | |||
| @next: lda row | |||
| cmp mem_count | |||
| bcs @done | |||
| ldy row | |||
| lda #C_MEM_OWNER | |||
| jsr tb_get | |||
| jsr emit_name | |||
| ldy row | |||
| lda #C_MEM_NAME | |||
| jsr tb_get | |||
| jsr emit_name | |||
| ldy row | |||
| lda #C_MEM_FLAGS | |||
| jsr tb_get | |||
| jsr emit | |||
| ldy row | |||
| lda #C_MEM_TBASE | |||
| jsr emit_type | |||
| ldy row | |||
| lda #C_MEM_PCOUNT | |||
| jsr tb_get | |||
| sta pleft | |||
| jsr emit | |||
| ldy row | |||
| lda #C_MEM_PFIRST | |||
| jsr tb_get | |||
| sta prow | |||
| @par: lda pleft | |||
| beq @adv | |||
| ldy prow | |||
| lda #C_PAR_TBASE | |||
| jsr emit_type | |||
| inc prow | |||
| dec pleft | |||
| jmp @par | |||
| @adv: inc row | |||
| jmp @next | |||
| @done: rts | |||
| ; Emit the four type columns starting at column A for row Y. | |||
| emit_type: | |||
| sta col | |||
| sty trow | |||
| jsr tb_get ; base | |||
| jsr emit | |||
| inc col | |||
| lda col | |||
| ldy trow | |||
| jsr tb_get ; name ID | |||
| jsr emit_name | |||
| inc col | |||
| lda col | |||
| ldy trow | |||
| jsr tb_get ; length lo | |||
| jsr emit | |||
| inc col | |||
| lda col | |||
| ldy trow | |||
| jsr tb_get ; length hi | |||
| jmp emit | |||
| ; Emit name ID A as length + spelling; 0 emits a zero length. | |||
| emit_name: | |||
| cmp #$00 | |||
| bne :+ | |||
| jmp emit | |||
| : jsr name_get | |||
| lda name_out_len | |||
| jsr emit | |||
| ldx #$00 | |||
| @byte: cpx name_out_len | |||
| beq @done | |||
| lda name_out,x | |||
| stx emit_x | |||
| jsr emit | |||
| ldx emit_x | |||
| inx | |||
| bne @byte | |||
| @done: rts | |||
| emit: ldy dump_ptr+1 | |||
| cpy #>(dump + DUMP_SIZE) | |||
| bcc @room | |||
| @@ -262,7 +354,11 @@ fixtures: | |||
| FIXTURE "RECOVER.N8" | |||
| FIXTURE "NOPROG.N8" | |||
| FIXTURE "EOFCLASS.N8" | |||
| FIXTURE_COUNT = 24 | |||
| FIXTURE "MEMBERS.N8" | |||
| FIXTURE "LIFECYCLE.N8" | |||
| FIXTURE "SCOPES.N8" | |||
| FIXTURE "PRIVATE.N8" | |||
| FIXTURE_COUNT = 28 | |||
| messages: | |||
| ok_msg: .byte "NOVA128 PARSER TEST OK", $0D, $00 | |||
| @@ -279,4 +375,9 @@ id: .res 1 | |||
| emit_x: .res 1 | |||
| emit_y: .res 1 | |||
| print_y: .res 1 | |||
| row: .res 1 | |||
| prow: .res 1 | |||
| pleft: .res 1 | |||
| col: .res 1 | |||
| trow: .res 1 | |||
| dump: .res DUMP_SIZE | |||
| @@ -0,0 +1,14 @@ | |||
| // Thief uses Vault before Vault is declared (members resolve in pass 2). | |||
| // Private members (explicit, or a field by default) are visible only in | |||
| // Vault's own methods, through self or another Vault; chains through calls | |||
| // (v.make().hidden, p.look().secret) and indexing (list[0].secret) are | |||
| // checked; unknown members on a class or interface are reported. | |||
| ACC_PRIVATE_MEMBER 3 7 | |||
| ACC_PRIVATE_MEMBER 5 7 | |||
| ACC_PRIVATE_MEMBER 7 14 | |||
| SEM_UNKNOWN_NAME 8 7 | |||
| SEM_UNKNOWN_NAME 9 7 | |||
| ACC_PRIVATE_MEMBER 44 5 | |||
| ACC_PRIVATE_MEMBER 45 11 | |||
| ACC_PRIVATE_MEMBER 48 12 | |||
| SEM_UNKNOWN_NAME 49 5 | |||
| @@ -0,0 +1,51 @@ | |||
| class Thief | |||
| public method steal(v as Vault) | |||
| v.secret = 1 | |||
| v.shown = 2 | |||
| v.lock() | |||
| v.open() | |||
| v.make().hidden = 3 | |||
| v.missing = 4 | |||
| v.shown(1) | |||
| end method | |||
| end class | |||
| interface Peek | |||
| method look() as Vault | |||
| end interface | |||
| class Vault implements Peek | |||
| private word secret | |||
| public word shown | |||
| word hidden | |||
| private method lock() | |||
| end method | |||
| method open() | |||
| self.lock() | |||
| self.secret = 1 | |||
| hidden = 2 | |||
| end method | |||
| public method peer(other as Vault) | |||
| other.secret = 2 | |||
| end method | |||
| public method make() as Vault | |||
| return self | |||
| end method | |||
| public method look() as Vault | |||
| return self | |||
| end method | |||
| end class | |||
| program Main | |||
| var v as Vault | |||
| var list as Vault[2] | |||
| var p as Peek | |||
| v = new Vault() | |||
| v.hidden = 1 | |||
| list[0].secret = 5 | |||
| print v.shown | |||
| p = v | |||
| p.look().secret = 6 | |||
| p.touch() | |||
| v.open() | |||
| end program | |||
| @@ -0,0 +1,14 @@ | |||
| // pass 1: `var` after a method statement or inside a block | |||
| SYN_UNEXPECTED_TOKEN 22 5 | |||
| SYN_UNEXPECTED_TOKEN 40 5 | |||
| // pass 2: locals/params of one() are not visible in two(); A's field is | |||
| // not visible in B; three()'s param and local are gone in four(); the | |||
| // program sees only its own locals and has no self; Ghost is no type | |||
| SEM_UNKNOWN_NAME 11 5 | |||
| SEM_UNKNOWN_NAME 12 5 | |||
| SEM_UNKNOWN_NAME 27 5 | |||
| SEM_UNKNOWN_NAME 28 5 | |||
| SEM_UNKNOWN_NAME 36 11 | |||
| SEM_UNKNOWN_NAME 37 3 | |||
| SEM_UNKNOWN_NAME 38 3 | |||
| SYN_UNEXPECTED_TOKEN 42 3 | |||
| @@ -0,0 +1,43 @@ | |||
| class A | |||
| private word count | |||
| public method one(p as word) | |||
| var t as word | |||
| t = p | |||
| count = t | |||
| end method | |||
| public method two() | |||
| t = 1 | |||
| p = 2 | |||
| count = 3 | |||
| end method | |||
| end class | |||
| class B | |||
| public method three(count as byte) | |||
| var mine as word | |||
| count = 1 | |||
| mine = 2 | |||
| var late as word | |||
| late = mine | |||
| end method | |||
| public method four() | |||
| count = 4 | |||
| mine = 5 | |||
| end method | |||
| end class | |||
| program Main | |||
| var a as A | |||
| a = new A() | |||
| var b as B | |||
| b = new Ghost() | |||
| t = 0 | |||
| count = 0 | |||
| if a = null | |||
| var inner as word | |||
| end if | |||
| self.count = 1 | |||
| end program | |||
Powered by TurnKey Linux.