diff --git a/newLang/cfg/compiler.cfg b/newLang/cfg/compiler.cfg index b573252..1494a13 100644 --- a/newLang/cfg/compiler.cfg +++ b/newLang/cfg/compiler.cfg @@ -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; diff --git a/newLang/docs/architecture.md b/newLang/docs/architecture.md index 79b251b..7043dd5 100644 --- a/newLang/docs/architecture.md +++ b/newLang/docs/architecture.md @@ -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; diff --git a/newLang/docs/decisions.md b/newLang/docs/decisions.md index 87d41ef..f4d593e 100644 --- a/newLang/docs/decisions.md +++ b/newLang/docs/decisions.md @@ -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 diff --git a/newLang/docs/diagnostics.md b/newLang/docs/diagnostics.md index 1c8f1e6..e908b7c 100644 --- a/newLang/docs/diagnostics.md +++ b/newLang/docs/diagnostics.md @@ -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 | diff --git a/newLang/docs/memory-map.md b/newLang/docs/memory-map.md index adcb614..d079071 100644 --- a/newLang/docs/memory-map.md +++ b/newLang/docs/memory-map.md @@ -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 diff --git a/newLang/planning/claims/E03-S02.md b/newLang/planning/claims/E03-S02.md new file mode 100644 index 0000000..db1431b --- /dev/null +++ b/newLang/planning/claims/E03-S02.md @@ -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). diff --git a/newLang/scripts/build-parser-test.ps1 b/newLang/scripts/build-parser-test.ps1 index f908ae3..60a86d3 100644 --- a/newLang/scripts/build-parser-test.ps1 +++ b/newLang/scripts/build-parser-test.ps1 @@ -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 ` diff --git a/newLang/scripts/verify-parser-test.ps1 b/newLang/scripts/verify-parser-test.ps1 index be96bbc..145af58 100644 --- a/newLang/scripts/verify-parser-test.ps1 +++ b/newLang/scripts/verify-parser-test.ps1 @@ -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 "; ") } diff --git a/newLang/src/common/diagcat.inc b/newLang/src/common/diagcat.inc index b11620d..bf2f876 100644 --- a/newLang/src/common/diagcat.inc +++ b/newLang/src/common/diagcat.inc @@ -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" diff --git a/newLang/src/common/parser.inc b/newLang/src/common/parser.inc index e252d69..7994ba6 100644 --- a/newLang/src/common/parser.inc +++ b/newLang/src/common/parser.inc @@ -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 diff --git a/newLang/src/compiler/mmu.s b/newLang/src/compiler/mmu.s index ede2cca..7312a0e 100644 --- a/newLang/src/compiler/mmu.s +++ b/newLang/src/compiler/mmu.s @@ -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" diff --git a/newLang/src/compiler/names.s b/newLang/src/compiler/names.s index 0f309b0..61f3757 100644 --- a/newLang/src/compiler/names.s +++ b/newLang/src/compiler/names.s @@ -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). diff --git a/newLang/src/compiler/parser.s b/newLang/src/compiler/parser.s index fffb383..436765b 100644 --- a/newLang/src/compiler/parser.s +++ b/newLang/src/compiler/parser.s @@ -1,17 +1,25 @@ -; parser.s - top-level declaration parser (E03-S01). +; parser.s - declaration, member, and scope parser (E03-S01, E03-S02). ; ; 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). +; 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 and duplicate-checked; if/while/for/end blocks are tracked; other -; statement lines are skimmed until E03-S03 parses them. +; 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. +; +; 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. ; ; 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 @@ -25,14 +33,21 @@ .include "../common/parser.inc" .import lex_reset, lex_next -.import tok_kind, tok_line, tok_col, tok_len, tok_err +.import tok_kind, tok_line, tok_col, tok_len, tok_value, tok_text, tok_text_len, tok_err .import src_line -.import name_intern, name_declare +.import name_intern, name_find, 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 +.export parse_file, tb_get, mem_count, par_count +.export type_kind, type_line_lo, type_line_hi + +ACC_DEFAULT = 0 +ACC_PUBLIC = 1 +ACC_PRIVATE = 2 +LIFE_INIT = 1 +LIFE_DESTROY = 2 .macro IFTOK kind, target lda tok_kind @@ -47,6 +62,11 @@ parse_file: tsx stx parse_sp jsr tt_clear ; bank-1 BSS is not zeroed at load + lda #$00 + sta mem_count + sta par_count + sta mem_full + sta par_full lda #$01 sta pass jsr parse_pass @@ -67,6 +87,8 @@ parse_pass: sta program_seen sta pending_end sta eof_reported + sta cur_owner + sta cur_class jsr next top: jsr skip_blank lda pending_end ; stray `end X` left by a body: reported @@ -111,6 +133,9 @@ check_order: ; ----------------------------------------------------------- interface parse_interface: + lda #$00 + sta cur_owner + sta cur_class jsr next lda tok_kind cmp #TOK_IDENT @@ -121,6 +146,7 @@ parse_interface: jmp @members : lda #TYPE_INTERFACE jsr declare_type + jsr open_owner jsr next jsr expect_eol @members: @@ -151,6 +177,10 @@ parse_interface: @skip: jsr skip_line jmp @loop @method: + lda #$01 + sta sig_iface + lda #ACC_PUBLIC + sta m_acc jsr parse_signature jmp @loop @end: jsr next @@ -173,6 +203,9 @@ parse_interface: ; --------------------------------------------------------------- class parse_class: + lda #$00 + sta cur_owner + sta cur_class jsr next lda tok_kind cmp #TOK_IDENT @@ -183,6 +216,9 @@ parse_class: jmp @members : lda #TYPE_CLASS jsr declare_type + jsr open_owner + lda cur_owner + sta cur_class jsr next lda tok_kind cmp #KW_IMPLEMENTS @@ -217,16 +253,25 @@ parse_class: @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 #TOK_EOF + bne :+ + jmp @eof +: jsr at_toplevel_kw + bne :+ + jmp @missing +: IFTOK KW_END, @end + ldx #ACC_DEFAULT lda tok_kind cmp #KW_PUBLIC - beq @access - cmp #KW_PRIVATE - bne @member -@access: + bne :+ + ldx #ACC_PUBLIC +: cmp #KW_PRIVATE + bne :+ + ldx #ACC_PRIVATE +: stx m_acc + cpx #ACC_DEFAULT + beq @member jsr next @member: IFTOK KW_METHOD, @method @@ -241,10 +286,20 @@ parse_class: jsr err1 jmp @skip : jsr intern_tok + sta mk_name + lda #$00 ; field, private unless declared public + ldx m_acc + cpx #ACC_PUBLIC + beq :+ + lda #MF_PRIVATE +: sta mk_flags + jsr declare_member jsr next jsr expect_eol jmp @loop @method: + lda #$00 + sta sig_iface jsr parse_signature ; a bad header still has a body lda #KW_METHOD jsr parse_body @@ -270,6 +325,9 @@ parse_class: ; ------------------------------------------------------------- program parse_program: + lda #$00 + sta cur_owner + sta cur_class ; no self, no fields, privates hidden jsr next lda tok_kind cmp #TOK_IDENT @@ -291,10 +349,15 @@ parse_program: ; ------------------------------------------------------ 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). +; In: m_acc, sig_iface. Starts a new local scope holding the parameters and +; (pass 1) records the method and its parameter types under cur_owner. +; C=1 if the header had an error (the line has been skipped). parse_signature: jsr scope_clear + lda #$00 + sta sig_params + lda #$FF + sta cur_mem jsr next lda tok_kind ; member names may be keywords (D121), cmp #TOK_IDENT ; e.g. `method print()` in spec 5.1 @@ -303,7 +366,30 @@ parse_signature: bcs :+ lda #E_SYN_EXPECTED_IDENTIFIER jmp @fail -: jsr intern_tok +: lda tok_col + sta sig_col + lda tok_len + sta sig_len + jsr lifecycle_kind ; life = init / destroy / 0 + lda life + beq @named + lda sig_iface + beq @named + jsr arg_token ; interfaces declare no init/destroy + lda #E_SYN_UNEXPECTED_TOKEN + jsr err1 + lda #$00 + sta life +@named: jsr intern_tok + sta mk_name + lda #MF_METHOD ; public unless declared private + ldx m_acc + cpx #ACC_PRIVATE + bne :+ + ora #MF_PRIVATE +: sta mk_flags + jsr ty_clear ; no result until `as type` + jsr declare_member jsr next lda tok_kind cmp #TOK_LPAREN @@ -317,7 +403,16 @@ parse_signature: beq :+ lda #E_SYN_EXPECTED_IDENTIFIER jmp @fail -: jsr declare_local +: lda life ; destroy takes no parameters + cmp #LIFE_DESTROY + bne :+ + lda sig_params ; report the first one only + bne :+ + jsr arg_name + lda #E_SEM_LIFECYCLE_PARAMS + jsr err1 +: inc sig_params + jsr declare_local jsr next lda tok_kind cmp #KW_AS @@ -327,6 +422,8 @@ parse_signature: : jsr next jsr parse_type bcs @skip + jsr set_local_type + jsr add_param IFTOK TOK_COMMA, @more IFTOK TOK_RPAREN, @close lda #E_SYN_EXPECTED_RPAREN @@ -337,9 +434,15 @@ parse_signature: lda tok_kind cmp #KW_AS bne @eol - jsr next + lda life ; init and destroy return nothing + beq :+ + jsr arg_name + lda #E_SEM_LIFECYCLE_RETURN + jsr err1 +: jsr next jsr parse_type bcs @skip + jsr set_mem_type @eol: jsr expect_eol clc rts @@ -348,19 +451,59 @@ parse_signature: sec rts +; life = LIFE_INIT / LIFE_DESTROY when the IDENT token is `init` / +; `destroy` (tok_text is already folded to $41-$5A), else 0. +lifecycle_kind: + lda #$00 + sta life + lda tok_kind + cmp #TOK_IDENT + bne @done + lda #LIFE_INIT + sta lw_kind + ldx #$00 ; X = offset of the current entry +@entry: lda life_words,x ; length; 0 ends the table + beq @done + sta lw_len + cmp tok_text_len + bne @skip + stx lw_off + ldy #$00 +@cmp: inx + lda life_words,x + cmp tok_text,y + bne @miss + iny + cpy lw_len + bne @cmp + lda lw_kind + sta life +@done: rts +@miss: ldx lw_off +@skip: txa ; next entry: X + length + 1 + sec + adc lw_len + tax + inc lw_kind + bne @entry + ; ---------------------------------------------------------------- types -; primitive | Name, optionally [number]. C=1 after reporting an error. +; primitive | Name, optionally [number]. Fills ty_*. C=1 after reporting +; an error. parse_type: - jsr is_type_start + jsr ty_clear + jsr is_type_start ; X = index into prim_kinds bcs :+ lda #E_SYN_EXPECTED_TYPE jsr err1 sec rts -: lda tok_kind - cmp #TOK_IDENT +: lda prim_types,x + sta ty_base + cmp #TY_REF bne :+ jsr type_ref + sta ty_name : jsr next lda tok_kind cmp #TOK_LBRACKET @@ -369,6 +512,10 @@ parse_type: lda tok_kind cmp #TOK_NUMBER bne @bad + lda tok_value + sta ty_len + lda tok_value+1 + sta ty_len+1 jsr next lda tok_kind cmp #TOK_RBRACKET @@ -382,7 +529,15 @@ parse_type: sec rts -; C=1 when the token can start a type. +ty_clear: + lda #$00 + sta ty_base + sta ty_name + sta ty_len + sta ty_len+1 + rts + +; C=1 when the token can start a type; X = its index in prim_kinds. is_type_start: lda tok_kind ldx #prim_count ; primitives, then TOK_IDENT @@ -395,16 +550,18 @@ is_type_start: @yes: sec rts -; Pass 2: the identifier must name a declared interface or class. +; Intern the IDENT; in pass 2 it must name a declared interface or class. +; A = name ID (0 after a full-table error). type_ref: jsr intern_tok + sta tr_id beq @done ldx pass cpx #$02 bne @done tay - lda #0 ; type_kind table - jsr tt_get + lda #C_TYPE_KIND + jsr tb_get cmp #TYPE_INTERFACE beq @done cmp #TYPE_CLASS @@ -412,7 +569,8 @@ type_ref: jsr arg_token lda #E_SEM_UNKNOWN_NAME jsr err2 -@done: rts +@done: lda tr_id + rts ; ---------------------------------------------------------------- bodies ; Parse a method or program body. A = KW_METHOD or KW_PROGRAM. @@ -420,6 +578,8 @@ parse_body: sta block_stack lda #$01 sta depth + lda #$00 + sta stmt_seen @loop: jsr skip_blank lda tok_kind cmp #TOK_EOF @@ -433,11 +593,16 @@ parse_body: IFTOK KW_WHILE, @open IFTOK KW_FOR, @open IFTOK KW_END, @end - jsr skip_line ; statement: E03-S03 parses these + lda #$01 ; any other line is a statement + sta stmt_seen + jsr stmt_line jmp @loop -@var: jsr parse_var +@var: jsr check_var_place + jsr parse_var ; declared anyway, to avoid cascades jmp @loop -@open: ldx depth +@open: lda #$01 + sta stmt_seen + ldx depth cpx #PARSE_MAX_DEPTH bcc :+ lda #E_RES_NESTING_TOO_DEEP @@ -447,7 +612,7 @@ parse_body: : lda tok_kind sta block_stack,x inc depth - jsr skip_line ; condition / loop header: E03-S03 + jsr stmt_line ; condition / loop header jmp @loop @end: jsr next ; the closer keyword @@ -498,6 +663,36 @@ parse_body: sta depth rts +; `var` belongs at the top level of a body, and in a method only before the +; first statement (spec 4, 6.2). The program may mix them (spec 4 grammar). +check_var_place: + lda depth + cmp #$02 + bcs @bad + lda block_stack + cmp #KW_METHOD + bne @ok + lda stmt_seen + beq @ok +@bad: jsr arg_token + lda #E_SYN_UNEXPECTED_TOKEN + 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: @@ -546,15 +741,327 @@ parse_var: jsr parse_type bcc :+ jmp skip_line -: jmp expect_eol +: 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: + 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 + cmp #TOK_EOL + beq @end + 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 + cmp #KW_SELF + beq @self + cmp #TOK_DOT + beq @adv ; the chain is the member's left side + cmp #TOK_LPAREN + beq @open + cmp #TOK_LBRACKET + beq @index + cmp #TOK_RPAREN + beq @close + cmp #TOK_RBRACKET + beq @close + jsr chain_none ; operators, literals, other keywords + jmp @adv +@ident: lda sc_prev + cmp #KW_NEW + 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 + +; 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 + sta pend_len + lda sc_base + sta pend_base + lda sc_name + sta pend_name + lda sc_len + ora sc_len+1 + sta pend_array + jsr find_tok + sta pend_id + jmp chain_none + +; 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 pend_base + cmp #TY_REF + bne @none + lda pend_array ; an array has no members + bne @none + ldy pend_name + beq @none + lda #C_TYPE_KIND + jsr tb_get + cmp #TYPE_INTERFACE + beq :+ + cmp #TYPE_CLASS + bne @none ; unknown type: reported where declared +: sty fm_owner + lda pend_id + beq @unknown ; never declared anywhere + sta fm_name + ldx #$00 + lda tok_kind + cmp #TOK_LPAREN + bne :+ + ldx #MF_METHOD +: stx fm_kind + jsr find_member + bcc @unknown + sty fm_row + lda #C_MEM_FLAGS + jsr tb_get + and #MF_PRIVATE + beq @visible + lda fm_owner + cmp cur_class ; 0 in the program + beq @visible + lda #E_ACC_PRIVATE_MEMBER + jsr err_pend +@visible: + ldy fm_row + lda fm_kind + bne @method + jmp chain_member ; a field: its declared type +@method: + lda #C_MEM_TLEN_LO ; an array result is not indexed further + jsr tb_get + bne @none + lda #C_MEM_TLEN_HI + jsr tb_get + bne @none + lda #C_MEM_TBASE ; the result, applied at the `)` + jsr tb_get + sta call_base + lda #C_MEM_TNAME + jsr tb_get + sta call_name +@none: rts +@unknown: + lda #E_SEM_UNKNOWN_NAME + jmp err_pend + +; A bare name: a local or parameter, else a field of the enclosing class. +resolve_name: + jsr find_tok + beq @unknown + sta rn_id + jsr scope_test + bne @local + lda cur_class + beq @unknown + sta fm_owner + lda rn_id + sta fm_name + lda #$00 + sta fm_kind + jsr find_member + bcc @unknown + jmp chain_member +@local: ldy rn_id + lda loc_tbase,y + sta sc_base + lda loc_tname,y + sta sc_name + lda loc_tlen_lo,y + sta sc_len + lda loc_tlen_hi,y + sta sc_len+1 + rts +@unknown: + jsr chain_none + jsr arg_token + lda #E_SEM_UNKNOWN_NAME + jmp err2 + +self_ref: + lda cur_class + beq @bad + sta sc_name + lda #TY_REF + sta sc_base + lda #$00 + sta sc_len + sta sc_len+1 + rts +@bad: jsr chain_none + jsr arg_token ; the program has no self + lda #E_SYN_UNEXPECTED_TOKEN + jmp err2 + +; `new Name`: Name must be a declared class or interface. +new_type: + jsr chain_none + jsr find_tok + beq @unknown + sta sc_name + tay + lda #C_TYPE_KIND + jsr tb_get + beq @unknown + cmp #TYPE_PROGRAM + beq @program + lda #TY_REF + sta sc_base + rts +@program: + jmp chain_none ; nonsense, but not a name error +@unknown: + jsr chain_none + jsr arg_token + lda #E_SEM_UNKNOWN_NAME + jmp err2 + +chain_none: + lda #$00 + sta sc_base + sta sc_name + sta sc_len + sta sc_len+1 + rts + +; Chain = the declared type of member row Y. +chain_member: + lda #C_MEM_TBASE + jsr tb_get + sta sc_base + lda #C_MEM_TNAME + jsr tb_get + sta sc_name + lda #C_MEM_TLEN_LO + jsr tb_get + sta sc_len + lda #C_MEM_TLEN_HI + jsr tb_get + sta sc_len+1 + rts + +; A/X = base/name of the chain's element type, or 0/0 if not an array. +element_type: + lda sc_len + ora sc_len+1 + beq @none + lda sc_base + ldx sc_name + rts +@none: lda #$00 + 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 + ldx pend_col + ldy pend_len + jsr arg_at + pla + ldx pend_col + jmp err2_col ; --------------------------------------------------------- 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 + beq @done ldx pass cpx #$01 bne @done @@ -563,30 +1070,232 @@ declare_type: ldx tok_len jsr name_declare ldy dk_id - lda #0 - jsr tt_get + lda #C_TYPE_KIND + jsr tb_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 + lda #C_TYPE_KIND + jsr tb_set ldx tok_line - jsr tt_set - ldy dk_id - lda #2 + lda #C_TYPE_LINE_LO + jsr tb_set ldx tok_line+1 - jsr tt_set + lda #C_TYPE_LINE_HI + jsr tb_set +@done: rts + +; After declare_type, with the name still the current token: cur_owner = its +; ID when this is the declaration the type table recorded (not a duplicate), +; else 0. In pass 1 its members start at the next member row. +open_owner: + lda #$00 + sta cur_owner + ldy dk_id + beq @done + lda #C_TYPE_LINE_LO + jsr tb_get + cmp tok_line + bne @done + lda #C_TYPE_LINE_HI + jsr tb_get + cmp tok_line+1 + bne @done + sty cur_owner + lda pass + cmp #$01 + bne @done + ldx mem_count + lda #C_TYPE_MFIRST + jsr tb_set +@done: rts + +; Pass 1: add member mk_name (flags mk_flags, type ty_*) to cur_owner, +; reporting a duplicate in the same namespace (fields and methods are +; separate). cur_mem = its row, or $FF when nothing was recorded. +declare_member: + lda #$FF + sta cur_mem + lda pass + cmp #$01 + bne @done + lda cur_owner + beq @done + sta fm_owner + lda mk_name + beq @done + sta fm_name + lda mk_flags + and #MF_METHOD + sta fm_kind + jsr find_member + bcc @add + jsr arg_token + lda #E_SEM_DUPLICATE_NAME + jmp err1 +@add: ldy mem_count + cpy #MEMBERS_MAX + bcc :+ + lda mem_full ; reported once + bne @done + inc mem_full + jsr arg_token + lda #E_RES_TOO_MANY_MEMBERS + jmp err1 +: sty cur_mem + ldx cur_owner + lda #C_MEM_OWNER + jsr tb_set + ldx mk_name + lda #C_MEM_NAME + jsr tb_set + ldx mk_flags + lda #C_MEM_FLAGS + jsr tb_set + ldx par_count + lda #C_MEM_PFIRST + jsr tb_set + ldx #$00 + lda #C_MEM_PCOUNT + jsr tb_set + jsr put_mem_type + inc mem_count + ldy cur_owner ; type_mcount[owner] += 1 + lda #C_TYPE_MCOUNT + jsr tb_get + tax + inx + lda #C_TYPE_MCOUNT + jsr tb_set @done: rts +; Pass 1: the method's result type (ty_*) after `as type`. +set_mem_type: + lda pass + cmp #$01 + bne put_done + ldy cur_mem + cpy #$FF + beq put_done +; Store ty_* as the type of member row Y. +put_mem_type: + ldx ty_base + lda #C_MEM_TBASE + jsr tb_set + ldx ty_name + lda #C_MEM_TNAME + jsr tb_set + ldx ty_len + lda #C_MEM_TLEN_LO + jsr tb_set + ldx ty_len+1 + lda #C_MEM_TLEN_HI + jsr tb_set +put_done: + rts + +; Pass 1: append parameter type ty_* to method row cur_mem. +add_param: + lda pass + cmp #$01 + bne @done + lda cur_mem + cmp #$FF + beq @done + ldy par_count + cpy #PARAMS_MAX + bcc :+ + lda par_full ; reported once + bne @done + inc par_full + jsr arg_token + lda #E_RES_TOO_MANY_PARAMS + jmp err1 +: ldx ty_base + lda #C_PAR_TBASE + jsr tb_set + ldx ty_name + lda #C_PAR_TNAME + jsr tb_set + ldx ty_len + lda #C_PAR_TLEN_LO + jsr tb_set + ldx ty_len+1 + lda #C_PAR_TLEN_HI + jsr tb_set + inc par_count + ldy cur_mem + lda #C_MEM_PCOUNT + jsr tb_get + tax + inx + lda #C_MEM_PCOUNT + jsr tb_set +@done: rts + +; Find member fm_name of kind fm_kind (MF_METHOD or 0) in fm_owner's rows. +; C=1 and Y = row when found. +find_member: + ldy fm_owner + lda #C_TYPE_MCOUNT + jsr tb_get + beq @no + sta fm_left + lda #C_TYPE_MFIRST + jsr tb_get + tay +@loop: lda #C_MEM_NAME + jsr tb_get + cmp fm_name + bne @next + lda #C_MEM_FLAGS + jsr tb_get + and #MF_METHOD + cmp fm_kind + beq @yes +@next: iny + dec fm_left + bne @loop +@no: clc + rts +@yes: sec + rts + ; Add the IDENT token to the current local scope; report a duplicate. +; last_local = its ID (0 if it could not be interned). declare_local: jsr intern_tok + sta last_local + beq @done + jsr scope_test + 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 + +; Record ty_* as the type of local last_local. +set_local_type: + ldy last_local beq @done + lda ty_base + sta loc_tbase,y + lda ty_name + sta loc_tname,y + lda ty_len + sta loc_tlen_lo,y + lda ty_len+1 + sta loc_tlen_hi,y +@done: rts + +; A = name ID. Y = scope byte, X = bit; A = that bit if set (Z=0) else 0. +scope_test: tax lsr a ; byte = id / 8, bit = id & 7 lsr a @@ -597,14 +1306,7 @@ declare_local: 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 + rts scope_clear: lda #$00 @@ -625,27 +1327,34 @@ intern_tok: lda #$00 : rts -; Zero all three type-table columns. +; Look up the token's name without interning it. A = ID (Z=0), or 0. +find_tok: + lda tok_col + ldx tok_len + jsr name_find + rts + +; Zero the type-table columns (kind, lines, member range). tt_clear: - lda #2 + lda #C_TYPE_MCOUNT sta dk_kind ; column counter (reused scratch) @col: ldy #$00 @byte: lda dk_kind ldx #$00 - jsr tt_set + jsr tb_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 +; Parser tables in bank 1: A = column (C_*), Y = row. Preserve X and Y. +tb_get: jsr tb_ptr jmp mmu_peek1 -tt_set: jsr tt_ptr +tb_set: jsr tb_ptr txa jmp mmu_poke1 -tt_ptr: clc +tb_ptr: clc adc #>type_kind sta mmu_ptr+1 lda #(src_line - 1) adc #$00 sta diag_arg1+1 - lda tok_len + tya cmp #17 bcc :+ lda #16 : sta diag_arg1_len rts +; diag_arg1 = the method name of the signature being parsed. +arg_name: + ldx sig_col + ldy sig_len + jmp arg_at + ; Report A at the current token in pass 1 / pass 2 only. err1: ldx pass cpx #$01 @@ -736,6 +1453,13 @@ drop: ldx #$00 stx diag_arg1_len rts +; Report A at column X of the current line, pass 2 only. +err2_col: + ldy pass + cpy #$02 + beq report_col + bne drop + ; 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: @@ -759,13 +1483,14 @@ err_eof: txa jmp send -report: tax +report: ldx tok_col +report_col: + stx diag_col + 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 :+ @@ -776,6 +1501,8 @@ send: jsr diag_report prim_kinds: .byte TOK_IDENT, KW_BOOLEAN, KW_BYTE, KW_CHAR, KW_INTEGER, KW_WORD prim_count = * - prim_kinds - 1 +prim_types: + .byte TY_REF, TY_BOOLEAN, TY_BYTE, TY_CHAR, TY_INTEGER, TY_WORD end_blocks: .byte KW_PROGRAM, KW_METHOD, KW_IF, KW_WHILE, KW_FOR end_count = * - end_blocks @@ -784,13 +1511,19 @@ 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 +life_words: ; LIFE_INIT, LIFE_DESTROY order + .byte 4, "INIT" + .byte 7, "DESTROY" + .byte 0 ; ---------------------------------------------------------------- TABLES -; Top-level types by name ID: kind, then declaration line low/high. +; 256-byte columns in C_* order (parser.inc). Type columns are indexed by +; name ID; member and parameter columns by row. .segment "TABLES" type_kind: .res 256 type_line_lo: .res 256 type_line_hi: .res 256 + .res 256 * (TABLE_COLUMNS - 3) ; ------------------------------------------------------------------- BSS .segment "BSS" @@ -802,7 +1535,61 @@ eof_reported: .res 1 body_err: .res 1 depth: .res 1 block_stack: .res PARSE_MAX_DEPTH +stmt_seen: .res 1 scope_bits: .res 32 dk_kind: .res 1 dk_id: .res 1 +tr_id: .res 1 unwind_to: .res 1 +; members +cur_owner: .res 1 ; interface/class receiving members, or 0 +cur_class: .res 1 ; class whose methods are being parsed, or 0 +cur_mem: .res 1 ; member row of the signature, or $FF +mem_count: .res 1 +par_count: .res 1 +mem_full: .res 1 +par_full: .res 1 +m_acc: .res 1 +mk_name: .res 1 +mk_flags: .res 1 +sig_iface: .res 1 +sig_params: .res 1 +sig_col: .res 1 +sig_len: .res 1 +life: .res 1 +lw_kind: .res 1 +lw_len: .res 1 +lw_off: .res 1 +fm_owner: .res 1 +fm_name: .res 1 +fm_kind: .res 1 +fm_left: .res 1 +fm_row: .res 1 +; types and locals +ty_base: .res 1 +ty_name: .res 1 +ty_len: .res 2 +last_local: .res 1 +loc_tbase: .res 256 ; by name ID; valid while its scope bit is set +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 +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 +pend_base: .res 1 +pend_name: .res 1 +pend_array: .res 1 +rn_id: .res 1 diff --git a/newLang/tests/parser/lifecycle.expect b/newLang/tests/parser/lifecycle.expect new file mode 100644 index 0000000..b27bd2c --- /dev/null +++ b/newLang/tests/parser/lifecycle.expect @@ -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 diff --git a/newLang/tests/parser/lifecycle.n8 b/newLang/tests/parser/lifecycle.n8 new file mode 100644 index 0000000..5e34710 --- /dev/null +++ b/newLang/tests/parser/lifecycle.n8 @@ -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 diff --git a/newLang/tests/parser/members.expect b/newLang/tests/parser/members.expect new file mode 100644 index 0000000..3f22612 --- /dev/null +++ b/newLang/tests/parser/members.expect @@ -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 diff --git a/newLang/tests/parser/members.n8 b/newLang/tests/parser/members.n8 new file mode 100644 index 0000000..507d201 --- /dev/null +++ b/newLang/tests/parser/members.n8 @@ -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 diff --git a/newLang/tests/parser/parser-test.s b/newLang/tests/parser/parser-test.s index f4b930b..6f36e8f 100644 --- a/newLang/tests/parser/parser-test.s +++ b/newLang/tests/parser/parser-test.s @@ -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 diff --git a/newLang/tests/parser/private.expect b/newLang/tests/parser/private.expect new file mode 100644 index 0000000..b5ea1ce --- /dev/null +++ b/newLang/tests/parser/private.expect @@ -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 diff --git a/newLang/tests/parser/private.n8 b/newLang/tests/parser/private.n8 new file mode 100644 index 0000000..520af15 --- /dev/null +++ b/newLang/tests/parser/private.n8 @@ -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 diff --git a/newLang/tests/parser/scopes.expect b/newLang/tests/parser/scopes.expect new file mode 100644 index 0000000..d251a23 --- /dev/null +++ b/newLang/tests/parser/scopes.expect @@ -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 diff --git a/newLang/tests/parser/scopes.n8 b/newLang/tests/parser/scopes.n8 new file mode 100644 index 0000000..ec8a2be --- /dev/null +++ b/newLang/tests/parser/scopes.n8 @@ -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