From f2af2f2aa5cd2fac007cc97848a43254c1231d8d Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Sat, 7 Nov 2026 20:44:46 -0500 Subject: [PATCH 1/2] newLang: add the method, field, and local parser (E03-S02) Pass 1 now records every field and method signature (access, type, parameter types) in bank-1 member and parameter tables, rejects duplicate fields or methods per type, invalid init/destroy shapes (E151/E152, D122), and `var` after a method statement or inside a block. Each signature and the program open a typed local scope. Pass 2 scans statement lines: unqualified names must be locals, parameters, or fields of the enclosing class (D123), and member chains through locals, fields, self, new, calls, and indexing report unknown members (E061) and private access (E120) at the member. The parser test PRG grew past $4000 and crashed: mmu_init copies the COMMON load image in the system mapping, where BASIC ROM hides $4000+. COMMON now loads right after STARTUP and mmu.s asserts both stay below $4000. Verified in x128: 28 fixtures, member tables against a source-derived oracle, privacy and scope mutations caught; map, source, lexer, names, and diagnostics suites pass on the new layout. Co-Authored-By: Claude Opus 5.5 --- newLang/cfg/compiler.cfg | 4 +- newLang/docs/architecture.md | 12 + newLang/docs/decisions.md | 2 + newLang/docs/diagnostics.md | 4 + newLang/docs/memory-map.md | 14 +- newLang/planning/claims/E03-S02.md | 117 ++++ newLang/scripts/build-parser-test.ps1 | 6 +- newLang/scripts/verify-parser-test.ps1 | 92 ++- newLang/src/common/diagcat.inc | 4 + newLang/src/common/parser.inc | 45 +- newLang/src/compiler/mmu.s | 4 + newLang/src/compiler/names.s | 18 +- newLang/src/compiler/parser.s | 933 +++++++++++++++++++++++-- newLang/tests/parser/lifecycle.expect | 9 + newLang/tests/parser/lifecycle.n8 | 25 + newLang/tests/parser/members.expect | 5 + newLang/tests/parser/members.n8 | 24 + newLang/tests/parser/parser-test.s | 105 ++- newLang/tests/parser/private.expect | 14 + newLang/tests/parser/private.n8 | 51 ++ newLang/tests/parser/scopes.expect | 14 + newLang/tests/parser/scopes.n8 | 43 ++ 22 files changed, 1459 insertions(+), 86 deletions(-) create mode 100644 newLang/planning/claims/E03-S02.md create mode 100644 newLang/tests/parser/lifecycle.expect create mode 100644 newLang/tests/parser/lifecycle.n8 create mode 100644 newLang/tests/parser/members.expect create mode 100644 newLang/tests/parser/members.n8 create mode 100644 newLang/tests/parser/private.expect create mode 100644 newLang/tests/parser/private.n8 create mode 100644 newLang/tests/parser/scopes.expect create mode 100644 newLang/tests/parser/scopes.n8 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..13fe36c --- /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` (41 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 From 34b18b37e8bd026d574ca5990cffbb1e83392f11 Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Sat, 7 Nov 2026 20:44:53 -0500 Subject: [PATCH 2/2] newLang: correct the E03-S02 member-row count Co-Authored-By: Claude Opus 5.5 --- newLang/planning/claims/E03-S02.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newLang/planning/claims/E03-S02.md b/newLang/planning/claims/E03-S02.md index 13fe36c..db1431b 100644 --- a/newLang/planning/claims/E03-S02.md +++ b/newLang/planning/claims/E03-S02.md @@ -38,7 +38,7 @@ lookups. - [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` (41 member + 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).