Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
| @@ -39,10 +39,12 @@ SEGMENTS { | |||||
| LOADADDR: load = LOADADDR, type = ro; | LOADADDR: load = LOADADDR, type = ro; | ||||
| EXEHDR: load = HEADER, type = ro; | EXEHDR: load = HEADER, type = ro; | ||||
| STARTUP: load = MAIN, type = ro; # first: SYS 7181 enters at $1C0D | 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; | CODE: load = MAIN, type = ro; | ||||
| RODATA: load = MAIN, type = ro, optional = yes; | RODATA: load = MAIN, type = ro, optional = yes; | ||||
| DATA: load = MAIN, type = rw, 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; | BSS: load = MAIN, type = bss, optional = yes, define = yes; | ||||
| DISKBUF: load = DISKBUF, type = bss, optional = yes; | DISKBUF: load = DISKBUF, type = bss, optional = yes; | ||||
| SCRATCH: load = SCRATCH, type = bss, optional = yes; | SCRATCH: load = SCRATCH, type = bss, optional = yes; | ||||
| @@ -174,6 +174,18 @@ duplicate errors; pass 2 replays the source and reports type references that | |||||
| name no declared interface or class. Statement bodies are skimmed until | name no declared interface or class. Statement bodies are skimmed until | ||||
| E03-S03. | 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 | ### Pass 1 — declarations and layout | ||||
| - tokenize the complete source; | - tokenize the complete source; | ||||
| @@ -48,6 +48,8 @@ documented default. `Proposed` means a later change still needs approval. | |||||
| | D119 | The string and character escapes `\n` and `\r` both produce PETSCII `$0D`. | Commodore output has one line-end character, so both escapes start a new line. Implemented as `ESC_N_VALUE`/`ESC_R_VALUE` in `src/common/tokens.inc`; changing them later affects only those constants and the lexer golden files. | | | 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. | | | 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 `.`. | | | 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 | ## Deferred | ||||
| @@ -85,6 +85,8 @@ After 20 errors the next one is replaced by E221 and compilation stops. | |||||
| | Code | Symbol | Message | | | Code | Symbol | Message | | ||||
| |---|---|---| | |---|---|---| | ||||
| | E150 | `SEM_MISSING_RETURN` | MISSING RETURN VALUE | | | 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) | ## 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 | | | E220 | `RES_TOO_MANY_NAMES` | MORE THAN 255 NAMES | | ||||
| | E221 | `RES_ERROR_LIMIT` | TOO MANY ERRORS; COMPILATION STOPPED | | | E221 | `RES_ERROR_LIMIT` | TOO MANY ERRORS; COMPILATION STOPPED | | ||||
| | E222 | `RES_NESTING_TOO_DEEP` | BLOCKS NESTED TOO DEEPLY | | | E222 | `RES_NESTING_TOO_DEEP` | BLOCKS NESTED TOO DEEPLY | | ||||
| | E223 | `RES_TOO_MANY_MEMBERS` | MORE THAN 255 MEMBERS | | |||||
| | E224 | `RES_TOO_MANY_PARAMS` | MORE THAN 255 PARAMETERS | | |||||
| @@ -51,10 +51,11 @@ COMMON, DISKBUF, private ZP, SCRATCH, and MAILBOX are visible from bank 1. | |||||
| | Owner | Bytes | Contents | | | Owner | Bytes | Contents | | ||||
| |---|---|---| | |---|---|---| | ||||
| | `names.s` | 5,164 | 64 hash-chain heads + 255 name records of 20 bytes (D120) | | | `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 | 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 | | | `$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` | 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 | 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`. | `$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 | 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 | `$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 | variables and buffers (and anything else above `$3FFF`) before it calls | ||||
| @@ -0,0 +1,117 @@ | |||||
| --- | |||||
| story: E03-S02 | |||||
| owner: e03_member_parser | |||||
| status: done | |||||
| started: 2026-11-07 | |||||
| depends-on: [E03-S01] | |||||
| --- | |||||
| # E03-S02 Claim | |||||
| ## Intended change | |||||
| Extend `src/compiler/parser.s` so pass 1 records every interface and class | |||||
| member (fields and method signatures, with access, type, and parameter | |||||
| types) in bank-1 tables, enforces unique field and method names per type and | |||||
| the `init`/`destroy` shapes, and keeps locals leading in method bodies. Each | |||||
| method and the program get their own local scope with recorded local types. | |||||
| Pass 2 scans statement lines for name references: bare names must resolve to | |||||
| a local/parameter or (inside a method) a field of the enclosing class, and | |||||
| `expr.member` chains through locals, fields, `self`, `new`, calls, and | |||||
| indexing to report unknown and private members at the member's position. | |||||
| E03-S03 replaces the scan with the real expression parser and reuses the | |||||
| lookups. | |||||
| ## File area | |||||
| - `src/compiler/parser.s`, `src/common/parser.inc` | |||||
| - `src/compiler/names.s` (adds lookup-only `name_find`) | |||||
| - `src/common/diagcat.inc` (appended entries), `docs/diagnostics.md` | |||||
| - `tests/parser/` | |||||
| - `scripts/build-parser-test.ps1`, `scripts/verify-parser-test.ps1` | |||||
| - `docs/architecture.md`, `docs/memory-map.md`, `docs/decisions.md` | |||||
| - `planning/claims/E03-S02.md` | |||||
| - generated files under `build/` | |||||
| ## Acceptance evidence | |||||
| - [x] Parses access, signatures, parameters, fields, and leading local | |||||
| declarations: the member dump (owner, name, field/method, access, type, | |||||
| parameter types) matches an independent source-derived oracle for all 7 | |||||
| accepted fixtures and `members`/`lifecycle`/`scopes`/`private` (51 member | |||||
| rows, default field-private/method-public included). `scopes.n8` rejects | |||||
| `var` after a method statement (22:5) and inside a block (40:5); the | |||||
| program may still interleave `var` at its top level (35). | |||||
| - [x] Enforces unique method names and valid `init`/`destroy` shapes: | |||||
| `members.n8` rejects a second method `area` in an interface (3:10) and a | |||||
| class (17:18) and a second field `size` (9:15), while one field and one | |||||
| method may share a name. `lifecycle.n8` rejects init/destroy in an | |||||
| interface (E030), `init ... as word` (E151, D122), a second `init` | |||||
| (E060), and destroy with parameters (E152, first one) and a result | |||||
| (E151); `class Fine` shows parameterized init and private destroy pass. | |||||
| - [x] Assigns lexical scopes without leaking names across methods/classes: | |||||
| `scopes.n8` reports `one()`'s local and parameter in `two()`, class A's | |||||
| field in class B, `three()`'s parameter and local in `four()`, method | |||||
| locals and fields in the program, `self` in the program, and an unknown | |||||
| `new Ghost()` (8 pass-2 diagnostics, exact positions). Mutation: removing | |||||
| the per-signature scope reset fails 5 fixtures. | |||||
| - [x] Reports private-member violations with source positions: | |||||
| `private.n8` reports 6 E120s at the member's column, through a parameter, | |||||
| local, call result (`v.make().hidden`, `p.look().secret`), and index | |||||
| (`list[0].secret`), with Thief resolving Vault's members before Vault is | |||||
| declared; access from Vault's own methods (through `self`, a bare field, | |||||
| or another Vault) passes; unknown members are E061. Mutation: disabling | |||||
| the privacy test drops the 6 E120s and fails the fixture. | |||||
| ## Coordination notes | |||||
| - No other claim is active. E03-S03 will own statement/expression grammar; | |||||
| the pass-2 scan here only resolves names and members token by token and | |||||
| reports nothing about expression shape or types. | |||||
| ## Work log | |||||
| - Baseline: `scripts/verify-parser-test.ps1` passes 24 fixtures before any | |||||
| change. | |||||
| - Added `name_find` (lookup without interning) so pass 2 never grows the | |||||
| name table: a name first seen in a statement was never declared. | |||||
| - Appended E151 `SEM_LIFECYCLE_RETURN`, E152 `SEM_LIFECYCLE_PARAMS`, E223 | |||||
| `RES_TOO_MANY_MEMBERS`, E224 `RES_TOO_MANY_PARAMS`; docs regenerated. | |||||
| - First x128 run hung with the C128 in its ML monitor. Cause: the test PRG | |||||
| now extends past `$4000`, and `mmu_init` copies the COMMON load image | |||||
| (then placed after DATA) in the system mapping, where BASIC ROM covers | |||||
| `$4000-$7FFF`. Fix: `cfg/compiler.cfg` loads COMMON directly after | |||||
| STARTUP, and `mmu.s` asserts at link time that `mmu_init` and the COMMON | |||||
| image stay at or below `$4000`. Region addresses (D117) are unchanged. | |||||
| Map skeleton, source, lexer, names, and diagnostics suites were rerun on | |||||
| the new layout. | |||||
| - Verifier: members are compared against an independent regex oracle over | |||||
| the fixture source (declaration order, access defaults, first | |||||
| declaration wins) for the 7 accepted fixtures and the 4 new ones. | |||||
| ## Completion | |||||
| - Tests run: `scripts/verify-parser-test.ps1` (final run, plus the two | |||||
| mutations above); `scripts/build-diag-test.ps1` catalog check; because | |||||
| `cfg/compiler.cfg` changed, `verify-map-skeleton.ps1` (including its | |||||
| overflow variants), `verify-source-test.ps1`, `verify-lexer-test.ps1`, | |||||
| `verify-names-test.ps1`, and `verify-diag-test.ps1`. | |||||
| - Results: 28 parser fixtures as expected (2 still pending E03-S03, with no | |||||
| false diagnostics); both mutations caught; all five other suites pass. | |||||
| - Files changed: `src/compiler/parser.s`, `src/common/parser.inc`, | |||||
| `src/compiler/names.s`, `src/compiler/mmu.s`, `cfg/compiler.cfg`, | |||||
| `src/common/diagcat.inc`, `docs/diagnostics.md`, `docs/decisions.md` | |||||
| (D122, D123 proposed), `docs/architecture.md`, `docs/memory-map.md`, | |||||
| `tests/parser/` (4 fixtures + expectations, harness dump), | |||||
| `scripts/build-parser-test.ps1`, `scripts/verify-parser-test.ps1`, this | |||||
| claim. | |||||
| - Remaining risks: D121-D123 await approval. The pass-2 scan is token based: | |||||
| where it cannot know a left side's type (expression results, grouping, | |||||
| primitives, array-valued calls) it checks nothing, and a field used as | |||||
| `x.f(...)` reports "not declared" rather than a type error; E03-S03/S04 | |||||
| should replace it. Access to `init` from `new` outside the class is not | |||||
| checked (spec silent). The type descriptor (base, name, 16-bit length) is | |||||
| provisional until E03-S04. Member and parameter capacities (255 each) are | |||||
| untested at the limit. CODE/DATA may now exceed `$4000`; only `mmu_init` | |||||
| and code run before `mmu_bank0` must stay below it (asserted for | |||||
| `mmu_init` and COMMON; test STARTUP code is first in MAIN). | |||||
| @@ -1,4 +1,4 @@ | |||||
| # E03-S01: build the parser test PRG and a fresh D81 holding every grammar and | |||||
| # E03-S01/E03-S02: build the parser test PRG and a fresh D81 holding every grammar and | |||||
| # parser fixture. The fixture order must match tests/parser/parser-test.s. | # parser fixture. The fixture order must match tests/parser/parser-test.s. | ||||
| [CmdletBinding()] | [CmdletBinding()] | ||||
| param() | param() | ||||
| @@ -37,6 +37,10 @@ $script:ParserFixtures = [ordered]@{ | |||||
| "recover.n8" = @("$p\recover.n8", "exact", "$p\recover.expect") | "recover.n8" = @("$p\recover.n8", "exact", "$p\recover.expect") | ||||
| "noprog.n8" = @("$p\noprog.n8", "exact", "$p\noprog.expect") | "noprog.n8" = @("$p\noprog.n8", "exact", "$p\noprog.expect") | ||||
| "eofclass.n8" = @("$p\eofclass.n8", "exact", "$p\eofclass.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 ` | $link = Invoke-Nova128Link -Name "parser-test" -BuildDir $out ` | ||||
| @@ -28,22 +28,97 @@ try { | |||||
| if ($mb[3] -ne 0xA5) { throw ("C128 reported FAIL (code `${0:X2})" -f $mb[4]) } | if ($mb[3] -ne 0xA5) { throw ("C128 reported FAIL (code `${0:X2})" -f $mb[4]) } | ||||
| # ------------------------------------------------------------ decode dump | # ------------------------------------------------------------ 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) { | while ($dump[$p] -ne 0xFE) { | ||||
| if ($dump[$p] -ne 0xFF) { throw "dump out of sync at $p" } | 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 | $p += 4 | ||||
| while ($dump[$p] -ne 0xFD) { | |||||
| while ($dump[$p] -ne 0xFD -and $dump[$p] -ne 0xFC) { | |||||
| $len = $dump[$p + 3] | $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])) | $r.Types += ("{0} {1} {2}" -f @("", "interface", "class", "program")[$dump[$p]], $spell, ($dump[$p + 1] + 256 * $dump[$p + 2])) | ||||
| $p += 4 + $len | $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++ | $p++ | ||||
| $records += $r | $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" | $c1541 = Get-ViceTool "c1541" | ||||
| $listing = (& $c1541 -attach $disk -list 2>&1) | Out-String | $listing = (& $c1541 -attach $disk -list 2>&1) | Out-String | ||||
| function Get-ErrLines([string]$base) { | function Get-ErrLines([string]$base) { | ||||
| @@ -114,6 +189,13 @@ foreach ($name in $ParserFixtures.Keys) { | |||||
| $detail = "{0} diagnostics" -f $got.Count | $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" } | $status = if ($problems) { "FAIL" } else { "ok" } | ||||
| Write-Host (" {0,-13} {1,-9} {2} {3}" -f $name, $group, $status, $detail) | Write-Host (" {0,-13} {1,-9} {2} {3}" -f $name, $group, $status, $detail) | ||||
| if ($problems) { $failures += "${name}: " + ($problems -join "; ") } | if ($problems) { $failures += "${name}: " + ($problems -join "; ") } | ||||
| @@ -65,6 +65,8 @@ | |||||
| ; ---------------------------------------------------- methods and returns | ; ---------------------------------------------------- methods and returns | ||||
| DIAGNOSTIC SEM_MISSING_RETURN, 150, "MISSING RETURN VALUE" | 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 | ; ------------------------------------------------------------------- disk | ||||
| DIAGNOSTIC DSK_SOURCE_UNREADABLE, 200, "CANNOT READ SOURCE; DOS %" | 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_TOO_MANY_NAMES, 220, "MORE THAN 255 NAMES" | ||||
| DIAGNOSTIC RES_ERROR_LIMIT, 221, "TOO MANY ERRORS; COMPILATION STOPPED" | DIAGNOSTIC RES_ERROR_LIMIT, 221, "TOO MANY ERRORS; COMPILATION STOPPED" | ||||
| DIAGNOSTIC RES_NESTING_TOO_DEEP, 222, "BLOCKS NESTED TOO DEEPLY" | DIAGNOSTIC RES_NESTING_TOO_DEEP, 222, "BLOCKS NESTED TOO DEEPLY" | ||||
| DIAGNOSTIC RES_TOO_MANY_MEMBERS, 223, "MORE THAN 255 MEMBERS" | |||||
| DIAGNOSTIC RES_TOO_MANY_PARAMS, 224, "MORE THAN 255 PARAMETERS" | |||||
| @@ -1,13 +1,56 @@ | |||||
| ; parser.inc - top-level type table and parser limits (E03-S01). | |||||
| ; parser.inc - parser tables and limits (E03-S01, E03-S02). | |||||
| .ifndef PARSER_INC | .ifndef PARSER_INC | ||||
| PARSER_INC = 1 | PARSER_INC = 1 | ||||
| ; Top-level type kinds (type table column C_TYPE_KIND) | |||||
| TYPE_NONE = 0 | TYPE_NONE = 0 | ||||
| TYPE_INTERFACE = 1 | TYPE_INTERFACE = 1 | ||||
| TYPE_CLASS = 2 | TYPE_CLASS = 2 | ||||
| TYPE_PROGRAM = 3 | TYPE_PROGRAM = 3 | ||||
| PARSE_MAX_DEPTH = 16 ; nested method/program/if/while/for blocks | 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 | .endif | ||||
| @@ -109,6 +109,10 @@ mmu_init: | |||||
| lda #>mmu_nmi_guard | lda #>mmu_nmi_guard | ||||
| sta NMINV+1 | sta NMINV+1 | ||||
| rts | 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 | ; ---------------------------------------------------------------- COMMON | ||||
| .segment "COMMON" | .segment "COMMON" | ||||
| @@ -20,7 +20,7 @@ | |||||
| .importzp mmu_ptr, mmu_src | .importzp mmu_ptr, mmu_src | ||||
| .import src_line | .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_out, name_out_len, name_out_flags | ||||
| .export name_heads, name_recs | .export name_heads, name_recs | ||||
| @@ -49,6 +49,22 @@ name_intern: | |||||
| @found: clc | @found: clc | ||||
| @out: rts | @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, | ; 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 | ; unless an earlier declaration already did. The spelling must normalize to | ||||
| ; the same name (the caller interned it from the same token). | ; the same name (the caller interned it from the same token). | ||||
| @@ -0,0 +1,9 @@ | |||||
| // interfaces declare no init/destroy; init and destroy return nothing; | |||||
| // destroy takes no parameters (first one reported); one init per class. | |||||
| // class Fine: init may take parameters and destroy may be private | |||||
| SYN_UNEXPECTED_TOKEN 2 10 | |||||
| SYN_UNEXPECTED_TOKEN 3 10 | |||||
| SEM_LIFECYCLE_RETURN 7 33 | |||||
| SEM_DUPLICATE_NAME 10 17 | |||||
| SEM_LIFECYCLE_PARAMS 12 25 | |||||
| SEM_LIFECYCLE_RETURN 12 56 | |||||
| @@ -0,0 +1,25 @@ | |||||
| interface Life | |||||
| method init() | |||||
| method destroy() | |||||
| end interface | |||||
| class Res | |||||
| public method init(n as word) as word | |||||
| return n | |||||
| end method | |||||
| public method init() | |||||
| end method | |||||
| public method destroy(flag as boolean, more as byte) as boolean | |||||
| return flag | |||||
| end method | |||||
| end class | |||||
| class Fine | |||||
| method init(a as word, b as Res) | |||||
| end method | |||||
| private method destroy() | |||||
| end method | |||||
| end class | |||||
| program Main | |||||
| end program | |||||
| @@ -0,0 +1,5 @@ | |||||
| // fields and methods are separate namespaces per type: `size` and `area` | |||||
| // may each be a field and a method once, never twice in the same namespace | |||||
| SEM_DUPLICATE_NAME 3 10 | |||||
| SEM_DUPLICATE_NAME 9 15 | |||||
| SEM_DUPLICATE_NAME 17 18 | |||||
| @@ -0,0 +1,24 @@ | |||||
| interface Shape | |||||
| method area() as word | |||||
| method area() as word | |||||
| method name() | |||||
| end interface | |||||
| class Box implements Shape | |||||
| word size | |||||
| public byte size | |||||
| public word area | |||||
| public method size() as word | |||||
| return 0 | |||||
| end method | |||||
| public method area() as word | |||||
| return 0 | |||||
| end method | |||||
| private method area() | |||||
| end method | |||||
| method name() | |||||
| end method | |||||
| end class | |||||
| program Main | |||||
| end program | |||||
| @@ -1,4 +1,4 @@ | |||||
| ; E03-S01: parser test in x128. | |||||
| ; E03-S01/E03-S02: parser test in x128. | |||||
| ; | ; | ||||
| ; For each fixture: src_load, names_init, diag_init (screen off), parse_file, | ; 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 | ; 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 | ; $FF, fixture index, src_load result, diagnostic count | ||||
| ; per declared top-level type: kind, line lo, line hi, spelling length, | ; per declared top-level type: kind, line lo, line hi, spelling length, | ||||
| ; spelling bytes | ; 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 | ; $FD | ||||
| ; then $FE at the end. Mailbox: MB_DATA+0/1 dump address, +2/3 length. | ; 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 names_init, name_get, names_count, name_out, name_out_len | ||||
| .import diag_init, diag_finish, diag_screen | .import diag_init, diag_finish, diag_screen | ||||
| .import parse_file, type_kind, type_line_lo, type_line_hi | .import parse_file, type_kind, type_line_lo, type_line_hi | ||||
| .import tb_get, mem_count | |||||
| DUMP_SIZE = $2000 | DUMP_SIZE = $2000 | ||||
| @@ -112,6 +117,9 @@ test_entry: | |||||
| jsr diag_finish | jsr diag_finish | ||||
| jsr emit | jsr emit | ||||
| jsr dump_types | jsr dump_types | ||||
| lda #$FC | |||||
| jsr emit | |||||
| jsr dump_members | |||||
| @advance: | @advance: | ||||
| lda #$FD | lda #$FD | ||||
| jsr emit | jsr emit | ||||
| @@ -211,6 +219,90 @@ dump_types: | |||||
| bne @next | bne @next | ||||
| @done: rts | @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 | emit: ldy dump_ptr+1 | ||||
| cpy #>(dump + DUMP_SIZE) | cpy #>(dump + DUMP_SIZE) | ||||
| bcc @room | bcc @room | ||||
| @@ -262,7 +354,11 @@ fixtures: | |||||
| FIXTURE "RECOVER.N8" | FIXTURE "RECOVER.N8" | ||||
| FIXTURE "NOPROG.N8" | FIXTURE "NOPROG.N8" | ||||
| FIXTURE "EOFCLASS.N8" | FIXTURE "EOFCLASS.N8" | ||||
| FIXTURE_COUNT = 24 | |||||
| FIXTURE "MEMBERS.N8" | |||||
| FIXTURE "LIFECYCLE.N8" | |||||
| FIXTURE "SCOPES.N8" | |||||
| FIXTURE "PRIVATE.N8" | |||||
| FIXTURE_COUNT = 28 | |||||
| messages: | messages: | ||||
| ok_msg: .byte "NOVA128 PARSER TEST OK", $0D, $00 | ok_msg: .byte "NOVA128 PARSER TEST OK", $0D, $00 | ||||
| @@ -279,4 +375,9 @@ id: .res 1 | |||||
| emit_x: .res 1 | emit_x: .res 1 | ||||
| emit_y: .res 1 | emit_y: .res 1 | ||||
| print_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 | dump: .res DUMP_SIZE | ||||
| @@ -0,0 +1,14 @@ | |||||
| // Thief uses Vault before Vault is declared (members resolve in pass 2). | |||||
| // Private members (explicit, or a field by default) are visible only in | |||||
| // Vault's own methods, through self or another Vault; chains through calls | |||||
| // (v.make().hidden, p.look().secret) and indexing (list[0].secret) are | |||||
| // checked; unknown members on a class or interface are reported. | |||||
| ACC_PRIVATE_MEMBER 3 7 | |||||
| ACC_PRIVATE_MEMBER 5 7 | |||||
| ACC_PRIVATE_MEMBER 7 14 | |||||
| SEM_UNKNOWN_NAME 8 7 | |||||
| SEM_UNKNOWN_NAME 9 7 | |||||
| ACC_PRIVATE_MEMBER 44 5 | |||||
| ACC_PRIVATE_MEMBER 45 11 | |||||
| ACC_PRIVATE_MEMBER 48 12 | |||||
| SEM_UNKNOWN_NAME 49 5 | |||||
| @@ -0,0 +1,51 @@ | |||||
| class Thief | |||||
| public method steal(v as Vault) | |||||
| v.secret = 1 | |||||
| v.shown = 2 | |||||
| v.lock() | |||||
| v.open() | |||||
| v.make().hidden = 3 | |||||
| v.missing = 4 | |||||
| v.shown(1) | |||||
| end method | |||||
| end class | |||||
| interface Peek | |||||
| method look() as Vault | |||||
| end interface | |||||
| class Vault implements Peek | |||||
| private word secret | |||||
| public word shown | |||||
| word hidden | |||||
| private method lock() | |||||
| end method | |||||
| method open() | |||||
| self.lock() | |||||
| self.secret = 1 | |||||
| hidden = 2 | |||||
| end method | |||||
| public method peer(other as Vault) | |||||
| other.secret = 2 | |||||
| end method | |||||
| public method make() as Vault | |||||
| return self | |||||
| end method | |||||
| public method look() as Vault | |||||
| return self | |||||
| end method | |||||
| end class | |||||
| program Main | |||||
| var v as Vault | |||||
| var list as Vault[2] | |||||
| var p as Peek | |||||
| v = new Vault() | |||||
| v.hidden = 1 | |||||
| list[0].secret = 5 | |||||
| print v.shown | |||||
| p = v | |||||
| p.look().secret = 6 | |||||
| p.touch() | |||||
| v.open() | |||||
| end program | |||||
| @@ -0,0 +1,14 @@ | |||||
| // pass 1: `var` after a method statement or inside a block | |||||
| SYN_UNEXPECTED_TOKEN 22 5 | |||||
| SYN_UNEXPECTED_TOKEN 40 5 | |||||
| // pass 2: locals/params of one() are not visible in two(); A's field is | |||||
| // not visible in B; three()'s param and local are gone in four(); the | |||||
| // program sees only its own locals and has no self; Ghost is no type | |||||
| SEM_UNKNOWN_NAME 11 5 | |||||
| SEM_UNKNOWN_NAME 12 5 | |||||
| SEM_UNKNOWN_NAME 27 5 | |||||
| SEM_UNKNOWN_NAME 28 5 | |||||
| SEM_UNKNOWN_NAME 36 11 | |||||
| SEM_UNKNOWN_NAME 37 3 | |||||
| SEM_UNKNOWN_NAME 38 3 | |||||
| SYN_UNEXPECTED_TOKEN 42 3 | |||||
| @@ -0,0 +1,43 @@ | |||||
| class A | |||||
| private word count | |||||
| public method one(p as word) | |||||
| var t as word | |||||
| t = p | |||||
| count = t | |||||
| end method | |||||
| public method two() | |||||
| t = 1 | |||||
| p = 2 | |||||
| count = 3 | |||||
| end method | |||||
| end class | |||||
| class B | |||||
| public method three(count as byte) | |||||
| var mine as word | |||||
| count = 1 | |||||
| mine = 2 | |||||
| var late as word | |||||
| late = mine | |||||
| end method | |||||
| public method four() | |||||
| count = 4 | |||||
| mine = 5 | |||||
| end method | |||||
| end class | |||||
| program Main | |||||
| var a as A | |||||
| a = new A() | |||||
| var b as B | |||||
| b = new Ghost() | |||||
| t = 0 | |||||
| count = 0 | |||||
| if a = null | |||||
| var inner as word | |||||
| end if | |||||
| self.count = 1 | |||||
| end program | |||||
Powered by TurnKey Linux.