Nova128 is an object-oriented language with an on-machine C128 compiler. This adds the spec, decisions, architecture, backlog, and story claims, plus the grammar fixtures (E00-S02), disk-image builder (E07-S01), and the x128-verified platform probes: - E01-S01 minimal native PRG - E01-S02 MMU/common-RAM probe. With bottom common on, a P0/P1 pointer aimed at bank 1 lands in bank 0 at the translated page. The verifier traces every MMU write in the probe window. - E01-S03 KERNAL wrapper (kcall). CHROUT, drive status, and a device-not-present failure work from bank 0 and bank 1 mappings. A/X/Y/C/ST are preserved and the MMU state is restored exactly. - E01-S04 40/80-column display and clock probe Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
| @@ -0,0 +1,7 @@ | |||
| # Generated compiler, disk-image, screenshot, and test artifacts | |||
| build/ | |||
| # Agent-local scratch files; durable story state belongs in planning/claims/ | |||
| *.tmp | |||
| *.log | |||
| @@ -0,0 +1,109 @@ | |||
| # AGENTS.md — Nova128 Project Rules | |||
| This file applies to every file below `newLang/`. | |||
| ## Mission | |||
| Build an object-oriented language and an on-machine compiler for the Commodore | |||
| 128. The compiler is written in documented 8502/6502 assembly, runs in C128 | |||
| native mode, reads source written by the repository's existing Vim clone, and | |||
| emits native C128 PRG files. | |||
| `Nova128` and the `.n8` suffix are working names until the user approves them. | |||
| ## Start Every Work Session Here | |||
| Read these files in order before changing implementation files: | |||
| 1. `README.md` | |||
| 2. `docs/decisions.md` | |||
| 3. `docs/language-spec.md` | |||
| 4. `docs/architecture.md` | |||
| 5. `planning/epics-and-stories.md` | |||
| 6. `planning/agent-workflow.md` | |||
| Also read the repository-root `AGENTS.md` and the relevant skills under | |||
| `../skills/`. Compiler work requires `../skills/compiler-design/SKILL.md`. | |||
| Emulator work also requires `../skills/vice-emulator/SKILL.md`. | |||
| ## Authority and Change Control | |||
| The canonical order is: | |||
| 1. The user's latest instruction. | |||
| 2. `docs/decisions.md` for accepted and provisional product decisions. | |||
| 3. `docs/language-spec.md` for source-language behavior. | |||
| 4. `docs/architecture.md` for compiler/runtime implementation contracts. | |||
| 5. `planning/epics-and-stories.md` for scope and acceptance criteria. | |||
| Do not silently change syntax, type rules, object layout, the interface ABI, | |||
| file formats, or memory ownership. Record a proposed change in | |||
| `docs/decisions.md` and obtain user approval when it changes visible behavior | |||
| or project scope. | |||
| ## Story Ownership | |||
| - Work from a story ID in `planning/epics-and-stories.md`. | |||
| - Before implementation, create `planning/claims/<story-id>.md` from the | |||
| template in `planning/agent-workflow.md`. | |||
| - One agent owns one active story unless coordination is explicit. | |||
| - Check existing claims and `git status` before editing. | |||
| - Do not take a story whose dependencies are incomplete. | |||
| - Keep changes inside the story's listed file areas when possible. | |||
| - Never discard, reset, or overwrite another agent's uncommitted work. | |||
| ## Definition of Done | |||
| A story is complete only when: | |||
| - Every acceptance criterion has evidence. | |||
| - Relevant automated tests pass. | |||
| - VICE behavior is verified when the story touches C128 execution. | |||
| - Documentation matches the implementation. | |||
| - Generated files are not confused with authoritative source files. | |||
| - The claim records files changed, commands run, results, and remaining risks. | |||
| ## Platform Constraints | |||
| - Target C128 native mode on the 8502. C64 and Z80 targets are out of scope for | |||
| version 1. | |||
| - Use documented 6502/8502 instructions; do not depend on illegal opcodes. | |||
| - Use `ca65` and `ld65` for host assembly/linking. | |||
| - Test C128 programs with VICE `x128.exe`, not `x64sc.exe`. | |||
| - Use explicit MMU wrapper routines. Compiler modules must not perform ad hoc | |||
| bank switching. | |||
| - Keep the hardware stack for calls, interrupts, and short-lived internals. | |||
| Language frames and substantial compiler stacks belong in software-managed | |||
| memory. | |||
| - KERNAL entry must occur through wrappers that install a compatible memory | |||
| configuration and restore the compiler configuration afterward. | |||
| - Exact memory addresses remain provisional until the E01 platform probes pass. | |||
| ## Editor and Disk Safety | |||
| - The current Vim clone is a C64-mode application. Version 1 integrates by | |||
| exchanging PETSCII SEQ files on a copied D81 image. | |||
| - Never run `x64sc` and `x128` against the same writable D81 simultaneously. | |||
| - Never modify `../vim/work.d81` or a reference disk image in place. | |||
| - Build and test scripts must make a working copy under `newLang/build/`. | |||
| - Human-readable host source is authoritative. PETSCII files and disk images | |||
| are generated artifacts unless a fixture explicitly says otherwise. | |||
| ## Source and Output Contracts | |||
| - Nova128 source is PETSCII SEQ with `$0D` line endings. | |||
| - The supported editor profile is at most 80 bytes per logical line, about | |||
| 18,999 source bytes, and 599 lines per file. | |||
| - Keep source base names at 12 characters or fewer so `.n8`, `.err`, and `.map` | |||
| variants fit Commodore DOS's 16-character filename limit. | |||
| - Compiler outputs are `.prg`, `.err`, and `.map` files as specified in the | |||
| architecture document. | |||
| ## Repository Hygiene | |||
| - Put all new project work under `newLang/`. | |||
| - Do not edit `../vim/` merely to make the compiler easier to build. | |||
| - Do not commit generated `build/` contents unless a release story explicitly | |||
| requires a packaged artifact. | |||
| - Prefer small, reviewable ca65 modules and deterministic test fixtures. | |||
| @@ -0,0 +1,74 @@ | |||
| # Nova128 | |||
| Nova128 is the working name for an object-oriented programming language for the | |||
| Commodore 128. Its compiler will be written in 8502/6502 assembly, run on the | |||
| C128 in native mode, and generate native C128 PRG files. | |||
| This directory contains the project specification, agent-ready backlog, and the | |||
| implementation as it is built. The user approved continuation on 2026-11-07; | |||
| stories may be claimed in dependency order under `AGENTS.md`. | |||
| ## Version 1 Product Boundary | |||
| - On-machine C128-native compiler written in ca65 assembly | |||
| - Statically typed, class-based language | |||
| - Classes, private/public fields and methods, constructors, and destructors | |||
| - Explicit `new` and `delete` | |||
| - Multiple interfaces per class | |||
| - Interface-typed variables and parameters | |||
| - Dynamic dispatch through interface method tables | |||
| - Procedural control flow and native 8502 code generation | |||
| - PETSCII SEQ source compatibility with the existing C64 Vim clone | |||
| - 40- or 80-column compiler output through KERNAL-compatible routines | |||
| Version 1 does not include class inheritance, garbage collection, exceptions, | |||
| generics, closures, overloading, floating point, a Z80 target, or a C64 target. | |||
| ## Document Map | |||
| | File | Purpose | | |||
| |---|---| | |||
| | `AGENTS.md` | Mandatory operating rules for agents | | |||
| | `docs/decisions.md` | Accepted, provisional, deferred, and open decisions | | |||
| | `docs/language-spec.md` | Normative source-language specification | | |||
| | `docs/architecture.md` | Compiler, runtime, ABI, memory, and file architecture | | |||
| | `planning/epics-and-stories.md` | Work breakdown with dependencies and acceptance criteria | | |||
| | `planning/agent-workflow.md` | Claiming, handoff, verification, and completion process | | |||
| ## Intended Workflow | |||
| ```text | |||
| x64sc + Vim clone | |||
| | | |||
| | PETSCII SEQ source: PROGRAM.N8 | |||
| v | |||
| copied development D81 | |||
| | | |||
| | close x64sc before continuing | |||
| v | |||
| x128 + Nova128 compiler | |||
| | | |||
| +--> PROGRAM.PRG | |||
| +--> PROGRAM.ERR | |||
| +--> PROGRAM.MAP | |||
| ``` | |||
| The original `vim/work.d81` is an input template, never a writable project | |||
| disk. A preparation script will copy it into `newLang/build/` and inject the | |||
| compiler, runtime, examples, and fixtures. | |||
| ## Approved Version 1 Defaults | |||
| The user approved continuation with these documented defaults: | |||
| - Working language name: `Nova128` | |||
| - Source suffix: `.n8` | |||
| - Compiler runs entirely on the C128 | |||
| - Host tools only assemble/package/test the compiler itself | |||
| - Vim remains C64-mode for version 1 | |||
| - Interfaces are required in version 1 | |||
| - Class inheritance is deferred | |||
| - Memory management is explicit and deterministic | |||
| See `docs/decisions.md` for decision status and unresolved questions. | |||
| @@ -0,0 +1,19 @@ | |||
| # E01-S04-only layout. This is not the final compiler memory map. | |||
| MEMORY { | |||
| LOADADDR: file = %O, start = $1BFF, size = $0002; | |||
| HEADER: file = %O, start = $1C01, size = $000C; | |||
| MAIN: file = %O, start = $1C0D, size = $0FF3; | |||
| } | |||
| SEGMENTS { | |||
| LOADADDR: load = LOADADDR, type = ro; | |||
| EXEHDR: load = HEADER, type = ro; | |||
| CODE: load = MAIN, type = ro; | |||
| RODATA: load = MAIN, type = ro; | |||
| DATA: load = MAIN, type = rw; | |||
| } | |||
| SYMBOLS { | |||
| __E01_S04_ONLY__: type = weak, value = 1; | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| # E01-S03-only layout. The complete image must remain inside the bottom 8K so | |||
| # it executes from bank-0 common RAM while the CPU selects bank 1. | |||
| MEMORY { | |||
| LOADADDR: file = %O, start = $1BFF, size = $0002; | |||
| HEADER: file = %O, start = $1C01, size = $000C; | |||
| COMMON: file = %O, start = $1C0D, size = $03F3; | |||
| } | |||
| SEGMENTS { | |||
| LOADADDR: load = LOADADDR, type = ro; | |||
| EXEHDR: load = HEADER, type = ro; | |||
| CODE: load = COMMON, type = ro; | |||
| RODATA: load = COMMON, type = ro; | |||
| DATA: load = COMMON, type = rw; | |||
| } | |||
| SYMBOLS { | |||
| __E01_S03_ONLY__: type = weak, value = 1; | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| # E01-S01-only layout. This is not the final compiler memory map. | |||
| MEMORY { | |||
| LOADADDR: file = %O, start = $1BFF, size = $0002; | |||
| HEADER: file = %O, start = $1C01, size = $000C; | |||
| MAIN: file = %O, start = $1C0D, size = $0FF3; | |||
| } | |||
| SEGMENTS { | |||
| LOADADDR: load = LOADADDR, type = ro; | |||
| EXEHDR: load = HEADER, type = ro; | |||
| CODE: load = MAIN, type = ro; | |||
| RODATA: load = MAIN, type = ro; | |||
| DATA: load = MAIN, type = rw; | |||
| } | |||
| SYMBOLS { | |||
| __E01_S01_ONLY__: type = weak, value = 1; | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| # E01-S02-only layout. The complete image must remain inside the bottom 8K so | |||
| # it executes from bank-0 common RAM while the CPU selects bank 1. | |||
| MEMORY { | |||
| LOADADDR: file = %O, start = $1BFF, size = $0002; | |||
| HEADER: file = %O, start = $1C01, size = $000C; | |||
| COMMON: file = %O, start = $1C0D, size = $03F3; | |||
| } | |||
| SEGMENTS { | |||
| LOADADDR: load = LOADADDR, type = ro; | |||
| EXEHDR: load = HEADER, type = ro; | |||
| CODE: load = COMMON, type = ro; | |||
| RODATA: load = COMMON, type = ro; | |||
| DATA: load = COMMON, type = rw; | |||
| } | |||
| SYMBOLS { | |||
| __E01_S02_ONLY__: type = weak, value = 1; | |||
| } | |||
| @@ -0,0 +1,371 @@ | |||
| --- | |||
| type: compiler-runtime-architecture | |||
| source-path: expert design plan, repository Vim notes, C128 reference architecture | |||
| ingested-at: 2026-11-07 | |||
| version: 0.1-draft | |||
| --- | |||
| # Nova128 Compiler and Runtime Architecture | |||
| Status: approved version 1 baseline. Exact addresses are deliberately absent | |||
| until the platform-probe epic validates them in VICE and, when available, on | |||
| hardware. | |||
| ## 1. System Boundary | |||
| The deliverable compiler is an 8502 program that runs on a Commodore 128 in | |||
| native mode. Host-side tools may: | |||
| - assemble and link the compiler with `ca65` and `ld65`; | |||
| - copy files into or out of D81 images with `c1541`; | |||
| - launch and control VICE; | |||
| - compare deterministic test outputs. | |||
| Host tools must not perform Nova128 parsing, type checking, or code generation | |||
| in the release workflow. The C128 compiler performs those functions. | |||
| Generated version 1 programs target C128 native 8502 mode. They do not target | |||
| C64 mode or the Z80. | |||
| ## 2. Toolchain and Artifacts | |||
| ### 2.1 Authoritative source | |||
| - Compiler/runtime assembly: ASCII ca65 source under `src/` | |||
| - Linker layouts: ld65 configuration under `cfg/` | |||
| - Language examples: readable ASCII mirrors under `examples/` | |||
| - PETSCII/D81 forms: generated under `build/` | |||
| ### 2.2 Compiler artifacts | |||
| | Artifact | Purpose | | |||
| |---|---| | |||
| | `N128C.PRG` | On-machine compiler executable | | |||
| | `NAME.N8` | PETSCII SEQ input source | | |||
| | `NAME.PRG` | Generated application | | |||
| | `NAME.ERR` | Complete stable-code diagnostics | | |||
| | `NAME.MAP` | Symbols, addresses, class layouts, interface slots, and section sizes | | |||
| Output replacement must be explicit. The compiler must not silently overwrite | |||
| an existing PRG after a failed compilation. | |||
| ## 3. Repository Module Plan | |||
| ```text | |||
| src/ | |||
| compiler/ | |||
| main.s command loop and phase orchestration | |||
| mmu.s all MMU transitions and bank-safe copies | |||
| diskio.s KERNAL file and command-channel wrappers | |||
| source.s buffered source stream and positions | |||
| lexer.s PETSCII normalization and tokenization | |||
| parser.s recursive-descent declarations/statements | |||
| expr.s precedence expression parser | |||
| names.s interning and identifier comparison | |||
| symbols.s scopes, symbols, and lookups | |||
| types.s type representation and compatibility | |||
| classes.s field/method layout and access checks | |||
| interfaces.s conformance and interface slot assignment | |||
| sizing.s dry-run code sizing and address assignment | |||
| codegen.s documented 8502 instruction emission | |||
| output.s PRG, ERR, and MAP writers | |||
| diagnostics.s stable error catalog and recovery | |||
| runtime/ | |||
| startup.s generated-program entry and initialization | |||
| frames.s software argument/local-frame support | |||
| objects.s object construction and destruction | |||
| interfaces.s interface conversion and indirect call trampoline | |||
| heap.s deterministic allocator and free list | |||
| arithmetic.s non-inline arithmetic helpers | |||
| print.s scalar and literal output | |||
| faults.s null, bounds, divide, stack, and heap failures | |||
| common/ | |||
| c128.inc hardware and KERNAL symbols | |||
| formats.inc compiler record and output constants | |||
| tokens.inc token numbers | |||
| errors.inc stable diagnostic numbers | |||
| ``` | |||
| Files are introduced by stories; this tree is a target, not permission to add | |||
| empty placeholders. | |||
| ## 4. C128 Memory Strategy | |||
| The design uses the two 64K RAM banks and a verified common-RAM window. | |||
| ### 4.1 Compiler bank roles | |||
| - **Bank 0:** resident compiler code, phase controller, lexer/parser state, | |||
| disk/KERNAL wrappers, diagnostics, and small buffers. | |||
| - **Bank 1:** interned names, symbols, class records, interface records, method | |||
| signatures, type records, and pass workspaces. | |||
| - **Common RAM:** MMU transition code, hardware stack, software-stack metadata, | |||
| disk transfer buffer, and state required while either bank is selected. | |||
| No module other than `mmu.s` directly changes the MMU. Banked data is accessed | |||
| through handles or explicit copy routines; ordinary pointers must not escape | |||
| the bank in which they are valid. | |||
| ### 4.2 Memory-map freeze gate | |||
| Story E01-S02 must prove: | |||
| - chosen MMU configurations; | |||
| - common-memory size and placement; | |||
| - page-zero and page-one behavior; | |||
| - safe execution of the bank-switch trampoline; | |||
| - KERNAL entry and return mappings; | |||
| - interrupt behavior while a non-default bank is active. | |||
| Only then may linker addresses and zero-page allocations become normative. | |||
| E01-S02 evidence is in `docs/mmu-probe.md`. In VICE x128 3.10, distinct banks, | |||
| 8K bottom+top common RAM, bank-switched `JSR`/`RTS`, and full MMU restoration | |||
| pass. Page pointers aimed at bank 1 while bottom common is active resolve to | |||
| bank 0 at the translated page, so Nova128 keeps P0/P1 in bank 0. Non-default | |||
| mappings run with IRQs masked; an IRQ path under bank 1 is not yet proven. | |||
| E01-S03 evidence is in `docs/kernal-wrapper.md`. A common-RAM wrapper enters | |||
| the KERNAL in BASIC's mapping and restores the exact compiler MMU state. From | |||
| bank 0 and bank 1 compiler mappings, CHROUT, a drive-8 status read, and a | |||
| device-not-present failure all work, with A/X/Y/C/ST preserved and no channels | |||
| left open. | |||
| ### 4.3 Display and clock | |||
| Compiler text uses KERNAL-compatible character output on the display already | |||
| selected by the user; it does not force 80-column mode. E01-S04 verified the | |||
| editor's `$00D7` mode flag in x128: 40-column uses a deterministic 1 MHz | |||
| fallback, while 80-column may set `$D030` bit 0 for 2 MHz KERNAL output. The | |||
| speed bit is cleared before returning to BASIC, and any uncertain mode falls | |||
| back to 1 MHz. Direct VDC screen memory is display-only and is not compiler | |||
| semantic storage. See `docs/platform-display-clock.md` for evidence. | |||
| ## 5. Compiler Passes | |||
| The compiler reparses a buffered/streamed source rather than retaining a large | |||
| pointer-rich abstract syntax tree. | |||
| ### Pass 1 — declarations and layout | |||
| - tokenize the complete source; | |||
| - intern names; | |||
| - collect interfaces, classes, fields, methods, parameters, and program data; | |||
| - validate duplicate declarations and top-level structure; | |||
| - assign field offsets; | |||
| - assign a stable slot order within every interface; | |||
| - build class-to-interface conformance records. | |||
| ### Pass 2 — semantics and sizing | |||
| - parse method/program bodies; | |||
| - resolve names and access control; | |||
| - check types, returns, and interface conversions; | |||
| - perform definite-local-initialization checks that fit the compiler budget; | |||
| - dry-run code generation to determine method sizes and addresses; | |||
| - calculate literal, descriptor, interface-table, and runtime sections. | |||
| ### Pass 3 — emission | |||
| - repeat semantic parsing deterministically; | |||
| - emit documented 8502 instructions; | |||
| - emit literals, class descriptors, and interface method tables; | |||
| - resolve branches and addresses against Pass 2 assignments; | |||
| - append/link required runtime sections; | |||
| - write PRG and MAP output; | |||
| - delete or invalidate partial output on failure. | |||
| Repeated passes trade disk/source traversal time for bounded memory. The source | |||
| stream should be rewindable without reopening unsafe shared emulator state. | |||
| ## 6. Compiler Data Records | |||
| Final byte layouts are story outputs, but records require these logical fields: | |||
| - **Interned name:** hash, length, original spelling location, normalized bytes | |||
| - **Symbol:** name ID, kind, type ID, scope ID, flags, value/address | |||
| - **Type:** kind, size, referenced class/interface/element, array length | |||
| - **Class:** name ID, instance size, field range, method range, interface range | |||
| - **Field:** owner class, name ID, type ID, access, byte offset | |||
| - **Method:** owner, name ID, signature, access, code address, code size | |||
| - **Interface:** name ID, ordered signature range | |||
| - **Implementation:** class ID, interface ID, emitted table address | |||
| - **Diagnostic:** code, file position, arguments | |||
| Use compact fixed-width records where practical. Every table has an explicit | |||
| capacity and produces a resource diagnostic rather than corrupting adjacent | |||
| memory. | |||
| ## 7. Generated Program ABI | |||
| ### 7.1 Scalar representation | |||
| - Multi-byte values are little-endian. | |||
| - `boolean`, `byte`, and `char` occupy one byte. | |||
| - `integer`, `word`, concrete references, and addresses occupy two bytes. | |||
| - Interface references occupy four bytes. | |||
| ### 7.2 Return convention | |||
| - One-byte result: accumulator A | |||
| - Two-byte result: A is low byte, X is high byte | |||
| - Four-byte interface result: runtime-defined result area referenced through a | |||
| reserved ABI pointer; exact zero-page location is frozen after E01 | |||
| - No result: A and X are caller-clobbered | |||
| The final register-clobber table is part of story E02-S02. | |||
| ### 7.3 Frames and calls | |||
| - The hardware stack carries return addresses and small runtime temporaries. | |||
| - Arguments, locals, and expression spills use a software-managed stack/frame. | |||
| - `self` is held in a reserved two-byte ABI pointer during an instance call. | |||
| - Parameters are laid out in declaration order using their ABI sizes. | |||
| - Concrete method calls are statically resolved to a direct `JSR`. | |||
| - The runtime checks stack bounds before frame growth in debug builds and at | |||
| defined safety points in release builds. | |||
| ### 7.4 Object layout | |||
| Every heap object begins with a two-byte class-descriptor pointer, followed by | |||
| fields in declaration order with no hidden alignment padding: | |||
| ```text | |||
| +0 class descriptor pointer (2) | |||
| +2 first declared field | |||
| ... remaining fields | |||
| ``` | |||
| The class descriptor contains the instance size, optional destructor address, | |||
| and a directory of implemented interfaces. Its final packed format is frozen | |||
| by E05 and E06 stories. | |||
| ### 7.5 Interface representation and dispatch | |||
| An interface value is a fat reference: | |||
| ```text | |||
| +0 object pointer (2) | |||
| +2 interface method-table pointer (2) | |||
| ``` | |||
| Each interface defines a slot order from its source declaration. For every | |||
| implemented interface, a class emits a table of two-byte method addresses in | |||
| that order. | |||
| Converting a concrete reference to an interface reference: | |||
| 1. copies the object pointer; | |||
| 2. uses compile-time-known class/interface metadata to select the class's | |||
| implementation table; | |||
| 3. stores the table pointer in the interface value. | |||
| An interface call: | |||
| 1. checks the object pointer for null; | |||
| 2. installs the object pointer as `self`; | |||
| 3. loads the two-byte target from the known interface slot; | |||
| 4. invokes the runtime's indirect-call trampoline. | |||
| The 8502 has no `JSR (address)` instruction, so the trampoline must establish a | |||
| synthetic return path safely. It is tested independently before interface code | |||
| generation relies on it. | |||
| `delete` through an interface uses the object pointer and its class descriptor, | |||
| not the interface table, to find the optional destructor and object size. | |||
| ## 8. Heap and Ownership | |||
| - The generated-program heap initially resides in the same RAM bank as program | |||
| data; banked heaps are deferred. | |||
| - `new` requests the class instance size, zeroes the allocation, stores the | |||
| descriptor pointer, and invokes `init`. | |||
| - `delete` invokes `destroy` at most once for that call and returns the entire | |||
| block to a coalescing free list. | |||
| - Allocation failure and corrupt/double-free detection use runtime fault codes. | |||
| - Reference copying does not change an ownership count. | |||
| - Aliases can outlive a deletion; that is a program error. Debug poisoning may | |||
| help diagnose it but is not a correctness mechanism. | |||
| ## 9. KERNAL and Disk I/O | |||
| The compiler uses documented KERNAL entry points through `diskio.s` wrappers. | |||
| Each wrapper: | |||
| 1. records compiler mapping state; | |||
| 2. installs the verified KERNAL-compatible mapping; | |||
| 3. performs the operation and reads status; | |||
| 4. restores compiler mapping state; | |||
| 5. reports errors without leaving files or channels open. | |||
| The command channel must be opened before source/output files when status is | |||
| needed and closed last, matching the proven editor workflow. | |||
| ## 10. Vim Integration | |||
| The existing Vim clone is treated as an external producer/consumer of SEQ | |||
| files. Version 1 does not link the editor and compiler in memory. | |||
| The development image workflow is strictly sequential: | |||
| 1. copy `../vim/work.d81` to `build/newlang-dev.d81`; | |||
| 2. inject `N128C.PRG`, examples, and fixtures; | |||
| 3. launch `x64sc` and autostart `VIM` for editing; | |||
| 4. close the process and wait for image flush; | |||
| 5. launch `x128` and autostart `N128C.PRG` for compilation; | |||
| 6. close the compiler session before inspecting or modifying the image. | |||
| Scripts must refuse obvious concurrent use where detectable. Tests create a | |||
| fresh image per run. | |||
| ## 11. Diagnostics and Faults | |||
| Compile-time diagnostics use stable IDs such as `E014`. Runtime faults use a | |||
| separate stable namespace such as `R003`. Numeric assignments live in | |||
| `src/common/errors.inc` once story E02-S03 freezes the catalog. | |||
| Compiler errors include source name, line, column, and compact message. Runtime | |||
| faults always include a stable fault code and the best available program address | |||
| that can be resolved through the MAP file. Rich stack traces are deferred. | |||
| ## 12. Verification Architecture | |||
| Testing is layered: | |||
| 1. Host checks for deterministic converters, image preparation, and byte-level | |||
| expected files. | |||
| 2. Compiler fixtures for lexer, parser, symbols, types, class layout, interface | |||
| conformance, and diagnostics. | |||
| 3. Golden PRG bytes and disassembly for small generated routines. | |||
| 4. End-to-end x128 runs that write a result signature to a fixed test mailbox | |||
| chosen during E01. | |||
| 5. Monitor scripts with MMU, stack, heap, and dispatch break/watch points. | |||
| 6. Editor round-trip: Vim saves source, C128 compiler consumes it without host | |||
| conversion, and Vim reopens unchanged source. | |||
| 7. Physical C128 smoke testing when hardware is available. | |||
| Each emulator test has a cycle/time limit and captures enough output to explain | |||
| failure without relying only on a screenshot. | |||
| ## 13. Principal Risks | |||
| - Incorrect MMU assumptions corrupt compiler state or KERNAL execution. | |||
| - Interface fat references increase local/array memory use. | |||
| - The indirect-call trampoline can corrupt the hardware stack if designed or | |||
| tested casually. | |||
| - Repeated parsing can be slow on real disks. | |||
| - Cross-bank raw pointers can create intermittent corruption. | |||
| - Feature growth can exceed the on-machine compiler's memory budget. | |||
| - Two VICE processes can overwrite the same D81 image. | |||
| The story order makes each high-risk mechanism prove itself before downstream | |||
| features depend on it. | |||
| ## Open Questions | |||
| - Exact bank layouts, zero-page reservations, and common-RAM window | |||
| - Final interface-result convention | |||
| - Compiler table capacities and their user-visible resource limits | |||
| - Whether to cache the complete source in banked RAM when it fits | |||
| - Physical-hardware acceptance requirements | |||
| @@ -0,0 +1,63 @@ | |||
| --- | |||
| type: project-decisions | |||
| source-path: user conversation and repository inspection | |||
| ingested-at: 2026-11-07 | |||
| --- | |||
| # Project Decisions | |||
| This file prevents later agents from silently changing the approved product. | |||
| `Accepted` means the user explicitly requested, confirmed, or approved the | |||
| documented default. `Proposed` means a later change still needs approval. | |||
| ## Accepted | |||
| | ID | Decision | Consequence | | |||
| |---|---|---| | |||
| | D001 | Create an object-oriented language for the Commodore 128. | C128 behavior and limits define the platform contract. | | |||
| | D002 | Write the compiler in assembly. | Compiler implementation uses documented 8502/6502 instructions and ca65 source. | | |||
| | D003 | Source creation must work with the repository's new Vim clone. | The compiler accepts the editor's PETSCII SEQ output directly. | | |||
| | D004 | Put project programs and supporting material under `newLang/`. | Existing `vim/` and reference assets remain external inputs. | | |||
| | D005 | Plan and obtain approval before implementation. | The user approved continuation on 2026-11-07; implementation stories may now be claimed in dependency order. | | |||
| | D006 | Support interfaces. | Interfaces and dynamic interface dispatch are version 1 work, not an optional extension. | | |||
| | D007 | Create a specification and epics/stories suitable for other agents. | Work is divided into claimable, testable stories with durable context. | | |||
| | D100 | Use `Nova128` as the language name and `.n8` as the source suffix. | Short, recognizable, and compatible with 16-character disk filenames. | | |||
| | D101 | Run the compiler in C128 native 8502 mode. | Gives access to both RAM banks, the MMU, 80-column facilities, and 2 MHz operation where safe. | | |||
| | D102 | Use host `ca65`/`ld65` only to build the on-machine compiler. | The resulting compiler still runs entirely on the C128. | | |||
| | D103 | Keep the existing Vim binary in C64 mode for version 1. | A native C128 editor port is a separate large project. | | |||
| | D104 | Exchange source through a copied D81 image. | It works with the current editor without inventing an editor API. | | |||
| | D105 | Use explicit `new`/`delete` and a deterministic free-list heap. | Avoids the size and timing cost of garbage collection. | | |||
| | D106 | Permit multiple interfaces but no class inheritance in version 1. | Delivers polymorphism while limiting layout and dispatch complexity. | | |||
| | D107 | Use statically dispatched concrete method calls and dynamically dispatched interface calls. | Keeps ordinary calls fast while preserving interface polymorphism. | | |||
| | D108 | Represent interface values as a four-byte pair: object pointer plus interface-table pointer. | Makes interface invocation constant-time without searching class metadata on every call. | | |||
| | D109 | Compile one source file into one PRG in version 1. | Separate compilation and object linking can follow a proven compiler. | | |||
| | D110 | Use KERNAL-compatible output so the compiler can operate on 40- or 80-column displays. | Avoids coupling the first compiler UI directly to one video chip. | | |||
| | D111 | Physical C128 testing is P2 validation, not a version 1 release blocker when hardware is unavailable. | VICE x128 remains the mandatory automated acceptance environment. | | |||
| | D112 | Compiler diagnostics use the active C128 display and do not force 80-column mode. | The compiler remains usable with either display; safe clock acceleration is opportunistic. | | |||
| | D113 | Version 1 integer overflow always wraps in the destination representation. | Debug overflow trapping is deferred, keeping semantics identical between builds. | | |||
| | D114 | Version 1 has no interface-to-class down-cast operation. | Programs express polymorphism through interfaces without runtime type reflection. | | |||
| | D115 | Character literals use single quotes and contain exactly one PETSCII character or supported escape. | The `char` type has a direct, unambiguous literal form while `//` remains the comment syntax. | | |||
| | D116 | Runtime faults report a stable code and the best available map-resolvable program address. | Rich stack traces are deferred; faults remain diagnosable within the runtime budget. | | |||
| ## Proposed | |||
| No proposed product changes are currently pending. | |||
| ## Deferred | |||
| - Native C128/80-column Vim port | |||
| - Vim `:make` and error-navigation commands | |||
| - Class inheritance and virtual class methods | |||
| - Interface inheritance and default methods | |||
| - Separate compilation and relocatable Nova128 object files | |||
| - Garbage collection or reference counting | |||
| - Exceptions, generics, closures, reflection, and method overloading | |||
| - Floating point | |||
| - C64 and Z80 code-generation targets | |||
| - Bank 1 heaps for generated programs | |||
| ## Open Questions | |||
| 1. What exact compiler and generated-program memory maps pass the E01 probes? | |||
| 2. What compiler table capacities fit after the verified memory map is frozen? | |||
| @@ -0,0 +1,100 @@ | |||
| # C128 KERNAL Mapping Wrapper Probe | |||
| Story: E01-S03 | |||
| Status: observed in VICE x128 3.10 with a true-emulation 1541; not the final | |||
| compiler map | |||
| ## Wrapper Contract (`kcall`) | |||
| ```text | |||
| in: kc_target = KERNAL entry address; A/X/Y = routine arguments | |||
| out: A/X/Y and N/V/Z/C exactly as the routine returned them | |||
| I flag = the caller's I flag | |||
| CR, RCR, P0L/H, P1L/H = their values at the call | |||
| ``` | |||
| Sequence (`src/platform/kernal-probe.s`): | |||
| 1. `PHP`/`SEI`, save A and the caller's P. | |||
| 2. Save the compiler's CR and RCR. | |||
| 3. Write the KERNAL CR first (bank 0, so the wrapper stays visible), then the | |||
| KERNAL RCR. The KERNAL CR/RCR are the values BASIC had when the program | |||
| started. | |||
| 4. `CLI` (the KERNAL runs in its own default mapping with IRQs enabled), then | |||
| `JSR` through an indirect jump to the routine. | |||
| 5. `PHP`/`SEI`, save A and the result P. | |||
| 6. Write the compiler RCR first (restoring bottom common), then the compiler | |||
| CR (which may select bank 1). | |||
| 7. Return the result N/V/Z/C merged with the caller's I flag. | |||
| The ordering in steps 3 and 6 matters: whenever CR selects bank 1, bottom | |||
| common RAM must already be enabled, or the wrapper's own code disappears. | |||
| The wrapper code and its variables must be in bottom common RAM. Hardware stack | |||
| cost is the caller's return address plus one more (`kc_jump`), on top of the | |||
| routine's own usage. `kc_target` must not sit at `$xxFF` (6502 `JMP ($xxFF)` | |||
| bug); the source has a link-time `.assert`. | |||
| The probe also has `kcall_inline`, a compact call form (`JSR kcall_inline` | |||
| followed by `.word target`, 5 bytes per call site) that preserves A/X/Y. It was | |||
| needed to fit the probe inside the 8K common window. | |||
| ## Compiler Mappings Tested | |||
| | State | CR | RCR (low bits) | Meaning | | |||
| |---|---|---|---| | |||
| | 0 | `$0E` | `%0110` | RAM bank 0 to `$BFFF`, system ROM high, I/O, 8K bottom common | | |||
| | 1 | `$4E` | `%0110` | Same with RAM bank 1 | | |||
| RCR bits 6–7 (VIC bank) are carried over from the entry value. | |||
| ## Results | |||
| All checks pass; both per-state result bytes are `$FF`. | |||
| | Bit | Check | State 0 | State 1 | | |||
| |---|---|---|---| | |||
| | `$01` | Every CHROUT returns C=0 | pass | pass | | |||
| | `$02` | `OPEN 15,8,15` + `CHKIN 15` return C=0 | pass | pass | | |||
| | `$04` | Status text starts with two digits and ends in CR | `00, OK,00,00` | `00, OK,00,00` | | |||
| | `$08` | `LDTND` (`$98`) = 0 after CLRCHN/CLOSE on the success path | pass | pass | | |||
| | `$10` | `CHKIN` on absent device 30 returns C=1, A=5 | pass | pass | | |||
| | `$20` | `LDTND` = 0 after CLRCHN/CLOSE on the failure path | pass | pass | | |||
| | `$40` | CR/RCR/P0/P1 identical after **every** wrapper call | pass | pass | | |||
| | `$80` | `$4000` still shows this bank's marker after all calls | pass | pass | | |||
| `READST` after the failed `CHKIN` returns `$80` (device not present) from both | |||
| states, so ST also survives the round trip. Note that `OPEN` with no filename | |||
| sends nothing on the serial bus, so the device-not-present error surfaces at | |||
| `CHKIN`, not `OPEN`. Compiler disk code must check both. | |||
| The x128 screenshot (`build/kernal-probe-vicii.png`) shows one CHROUT line per | |||
| state, both drive-8 status lines, and `NOVA128 KERNAL PROBE OK`. | |||
| ### Negative control | |||
| Changing the wrapper's flag merge from `AND #$FB` to `AND #$FA` (dropping the | |||
| routine's carry) makes both state results `$EF` and the verifier fails with | |||
| `CHKIN carry was clear`. The checks are therefore not vacuous. | |||
| ## Open Risks | |||
| - **NMI:** RESTORE-key NMIs cannot be masked. An NMI taken in compiler state 1 | |||
| has not been tested. E01-S05 should either prove the KERNAL NMI path under | |||
| bank 1 or install a common-RAM NMI handler. | |||
| - **IRQ in compiler mappings:** still masked by policy (see | |||
| `docs/mmu-probe.md`). | |||
| - **Fast serial / other drives:** only a 1541 on device 8 was tested; the | |||
| 1571/1581 burst protocol is not exercised. | |||
| - VICE-only evidence; physical hardware is E08-S06. | |||
| ## Reproducing | |||
| ```powershell | |||
| scripts/build-kernal-probe.ps1 # PRG + fresh build/kernal-probe.d64 | |||
| scripts/verify-kernal-probe.ps1 # true-drive autostart, remote checks, screenshot | |||
| ``` | |||
| The verifier polls `kprobe_complete` over the remote monitor because loading | |||
| through true drive emulation takes a variable number of seconds. The monitor | |||
| transcript is saved to `build/kernal-probe-remote.txt`. | |||
| @@ -0,0 +1,380 @@ | |||
| --- | |||
| type: language-specification | |||
| source-path: approved feature requests and expert design plan | |||
| ingested-at: 2026-11-07 | |||
| version: 0.1-draft | |||
| --- | |||
| # Nova128 Language Specification | |||
| Status: approved version 1 baseline. Changes to visible syntax or semantics | |||
| require a recorded decision. `Nova128` is the approved project name. | |||
| ## 1. Design Goals | |||
| Nova128 is a small, predictable, statically typed object-oriented language that | |||
| can be compiled on a Commodore 128 with bounded memory. | |||
| Version 1 prioritizes: | |||
| - understandable generated 8502 code; | |||
| - deterministic memory and execution costs; | |||
| - useful encapsulation and interface polymorphism; | |||
| - source that is comfortable to enter on a Commodore keyboard; | |||
| - diagnostics that remain readable on a 40-column display; | |||
| - a compiler architecture that fits within the C128's banked memory. | |||
| Compactness is a product constraint. A feature is not part of version 1 unless | |||
| this specification defines its syntax, semantics, diagnostics, and tests. | |||
| ## 2. Source Profile | |||
| ### 2.1 Physical format | |||
| - Source files are PETSCII sequential files. | |||
| - Logical lines end in carriage return (`$0D`). | |||
| - The compiler accepts a missing final carriage return. | |||
| - A source file may contain both PETSCII letter ranges `$41-$5A` and | |||
| `$C1-$DA`; the lexer normalizes them for comparison. | |||
| - The version 1 editor profile permits at most 80 bytes per logical line, | |||
| 18,999 source bytes, and 599 logical lines. | |||
| - A source base name should contain at most 12 characters so `.n8`, `.err`, | |||
| and `.map` names fit the Commodore DOS filename limit. | |||
| - One source file produces one program in version 1. | |||
| ### 2.2 Lexical rules | |||
| - Keywords and identifiers are case-insensitive. | |||
| - Diagnostics and map files preserve the spelling of the first declaration. | |||
| - Identifiers begin with a letter and continue with letters, digits, or `_`. | |||
| - At most 16 identifier characters are significant. Longer identifiers are an | |||
| error rather than being silently truncated. | |||
| - Spaces and tabs separate tokens. | |||
| - A newline terminates a statement or declaration. Version 1 does not continue | |||
| expressions across lines and does not allow multiple statements on one line. | |||
| - `//` begins a comment that continues through the end of the logical line. | |||
| - Decimal numbers use digits. `$` introduces hexadecimal and `%` introduces | |||
| binary. | |||
| - String literals use double quotes. Version 1 escape sequences are `\"`, | |||
| `\\`, `\n`, and `\r`. | |||
| - Character literals use single quotes and contain exactly one PETSCII | |||
| character or one supported escape: `\'`, `\\`, `\n`, or `\r`. | |||
| - Reserved punctuation is `(`, `)`, `[`, `]`, `,`, `.`, `:`, `=`, `+`, `-`, | |||
| `*`, `/`, `<`, `>`, `<>`, and the single quote that delimits character | |||
| literals. | |||
| - Boolean operators are the words `and`, `or`, and `not`; `&&` and `||` are | |||
| not aliases. | |||
| ## 3. Types and Values | |||
| ### 3.1 Primitive types | |||
| | Type | Size | Values | | |||
| |---|---:|---| | |||
| | `boolean` | 1 byte | `false` (`0`) or `true` (`1`) | | |||
| | `byte` | 1 byte | `0` through `255` | | |||
| | `char` | 1 byte | One PETSCII byte | | |||
| | `integer` | 2 bytes | Signed two's-complement `-32768` through `32767` | | |||
| | `word` | 2 bytes | Unsigned `0` through `65535` | | |||
| String literals are length-address pairs in generated read-only data. General | |||
| mutable string variables are deferred. `print` accepts a string literal or a | |||
| supported scalar value. | |||
| ### 3.2 Reference types | |||
| - Each class name defines a concrete object-reference type. | |||
| - Each interface name defines an interface-reference type. | |||
| - A concrete object reference is logically an address or `null`. | |||
| - An interface reference identifies both an object and its implementation of a | |||
| particular interface, or is `null`. | |||
| - Reference assignment copies identity; it does not copy object fields. | |||
| - Equality and inequality compare object identity. | |||
| ### 3.3 Arrays | |||
| - Version 1 supports fixed-size arrays with a compile-time positive length, | |||
| written as `type[length]`. | |||
| - Arrays may be fields or local variables. | |||
| - Array indexing is zero-based. | |||
| - Bounds checking is controlled by the compiler's debug/release option. | |||
| - Arrays of interface values use the interface representation defined by the | |||
| ABI; language code does not observe its byte layout. | |||
| ### 3.4 Conversions | |||
| - `byte` widens implicitly to `word` or `integer`. | |||
| - A character literal has type `char`. `char` does not silently become text or | |||
| a number in version 1. | |||
| - `boolean` never converts to or from a numeric type implicitly. | |||
| - `word` and `integer` do not convert implicitly in either direction. | |||
| - A class reference converts implicitly to an interface reference only when | |||
| the class implements that interface. | |||
| - An interface reference never converts implicitly to a concrete class. | |||
| - `null` is assignable to any class or interface reference. | |||
| ## 4. Program Structure | |||
| A file contains zero or more interface declarations, zero or more class | |||
| declarations, and exactly one program declaration. Declarations may refer to | |||
| types declared later in the file. | |||
| Simplified grammar uses `{x}` for repetition and `[x]` for an optional part: | |||
| ```text | |||
| file = { interface-decl | class-decl }, program-decl, EOF ; | |||
| interface-decl = "interface", identifier, EOL, | |||
| { method-signature }, | |||
| "end", "interface", EOL ; | |||
| class-decl = "class", identifier, | |||
| [ "implements", type-list ], EOL, | |||
| { field-decl | method-decl }, | |||
| "end", "class", EOL ; | |||
| program-decl = "program", identifier, EOL, | |||
| { variable-decl | statement }, | |||
| "end", "program", EOL ; | |||
| method-signature = "method", identifier, "(", [ parameter-list ], ")", | |||
| [ "as", type ], EOL ; | |||
| method-decl = [ access ], "method", identifier, "(", | |||
| [ parameter-list ], ")", [ "as", type ], EOL, | |||
| { variable-decl }, { statement }, | |||
| "end", "method", EOL ; | |||
| field-decl = [ access ], type, identifier, EOL ; | |||
| variable-decl = "var", identifier, "as", type, EOL ; | |||
| access = "public" | "private" ; | |||
| type-list = identifier, { ",", identifier } ; | |||
| ``` | |||
| The complete parser may use equivalent productions, but accepted and rejected | |||
| programs must follow the behavior in this specification. | |||
| ## 5. Interfaces | |||
| ### 5.1 Declaration | |||
| An interface contains method signatures only: | |||
| ```text | |||
| interface Printable | |||
| method print() | |||
| end interface | |||
| ``` | |||
| Interfaces cannot declare fields, constructors, destructors, method bodies, or | |||
| access modifiers. Interface inheritance and default methods are deferred. | |||
| ### 5.2 Implementation | |||
| A class implements one or more interfaces with a comma-separated list: | |||
| ```text | |||
| class Counter implements Printable, Resettable | |||
| ``` | |||
| For every interface method, the class must provide exactly one public method | |||
| with the same case-insensitive name, parameter count, parameter types, and | |||
| return type. Version 1 has no method overloading, so a class cannot declare two | |||
| methods with the same name. | |||
| If two implemented interfaces require the same signature, one class method may | |||
| satisfy both. If they use the same name with incompatible signatures, the class | |||
| declaration is an error. | |||
| ### 5.3 Interface calls | |||
| Calls made through a concrete class type are statically bound. Calls made | |||
| through an interface type are dynamically dispatched to the object's class | |||
| implementation. | |||
| ```text | |||
| var output as Printable | |||
| output = new Counter(10) | |||
| output.print() | |||
| ``` | |||
| Calling a method on a `null` interface reference is a runtime fault in debug | |||
| mode. Release behavior must still terminate through the defined runtime-error | |||
| path rather than jumping through address zero. | |||
| ## 6. Classes and Objects | |||
| ### 6.1 Fields and access | |||
| - Fields default to `private`. | |||
| - Methods default to `public`. | |||
| - Private members are accessible only inside methods of their declaring class. | |||
| - Version 1 has no class inheritance, protected access, static fields, or | |||
| static methods. | |||
| ### 6.2 Methods | |||
| - An instance method has an implicit `self` reference. | |||
| - Parameters are passed by value. Passing a reference copies the reference. | |||
| - Local variables must be declared before executable statements in a method. | |||
| - A non-returning method omits `as type`. | |||
| - A returning method must return a compatible value on every reachable path. | |||
| - Recursion is permitted by the language but bounded by the generated | |||
| program's hardware and software stacks. | |||
| ### 6.3 Construction and destruction | |||
| - `init` is the optional constructor name. A class may declare at most one. | |||
| - `destroy` is the optional destructor name. It takes no explicit parameters | |||
| and returns no value. | |||
| - `new Class(args)` allocates zero-initialized storage and then invokes `init`. | |||
| - If allocation fails, the runtime follows the defined out-of-memory path and | |||
| does not call `init`. | |||
| - `delete expression` invokes `destroy` when present and then releases storage. | |||
| - Deleting `null` is a no-op. | |||
| - Deleting through an interface reference acts on the underlying object. | |||
| - Using a reference after deletion is erroneous; version 1 does not attempt | |||
| complete alias or lifetime proof. | |||
| ## 7. Statements | |||
| Version 1 statements are: | |||
| ```text | |||
| assignment target = expression | |||
| call expression.method(arguments) | |||
| if if expression | |||
| statements | |||
| [else | |||
| statements] | |||
| end if | |||
| while while expression | |||
| statements | |||
| end while | |||
| for for name = expression to expression [step expression] | |||
| statements | |||
| end for | |||
| return return [expression] | |||
| print print expression | |||
| delete delete expression | |||
| ``` | |||
| Conditions must have type `boolean`; numeric values are not truth values. | |||
| `and` and `or` short-circuit from left to right. A `for` loop evaluates its end | |||
| and step expressions once before the first iteration. | |||
| ## 8. Expressions | |||
| From highest to lowest precedence: | |||
| 1. Parentheses, member access, calls, and indexing | |||
| 2. Unary `not` and unary `-` | |||
| 3. `*` and `/` | |||
| 4. `+` and `-` | |||
| 5. `<`, `<=`, `>`, and `>=` | |||
| 6. `=`, `<>` | |||
| 7. `and` | |||
| 8. `or` | |||
| Arithmetic is performed in the operand type after permitted widening. Version | |||
| 1 arithmetic overflow always wraps in the destination's two's-complement or | |||
| unsigned representation. Debug and release builds have identical overflow | |||
| semantics. Division by zero follows the runtime-error path. | |||
| ## 9. Names and Scopes | |||
| - Top-level interface and class names share one type namespace. | |||
| - Field and method names occupy separate namespaces within a class. | |||
| - Parameters and locals share a method-local namespace. | |||
| - A local or parameter may shadow a field; `self.field` selects the field. | |||
| - Duplicate declarations in the same namespace are errors. | |||
| - Forward references to top-level types and methods are permitted because the | |||
| compiler's declaration pass records signatures before bodies are compiled. | |||
| ## 10. Built-in Operations | |||
| Version 1 reserves these operations: | |||
| - `print value` — output a string literal, `char`, `byte`, `word`, `integer`, | |||
| or `boolean` through the runtime's KERNAL-compatible text layer. | |||
| - `new Class(args)` — allocate and construct an object. | |||
| - `delete reference` — destroy and release an object. | |||
| Additional machine and KERNAL facilities will be introduced through explicit | |||
| runtime APIs rather than unrestricted inline assembly in version 1. | |||
| ## 11. Diagnostics | |||
| Diagnostics have stable codes and source positions. The screen form must fit a | |||
| 40-column display: | |||
| ```text | |||
| E014 TEST.N8 23 7 | |||
| BYTE EXPECTED; WORD FOUND | |||
| ``` | |||
| The compiler writes the complete diagnostic list to `TEST.ERR`. It stops after | |||
| 20 errors by default so a malformed file cannot exhaust compiler memory. | |||
| Required diagnostic categories include: | |||
| - invalid PETSCII or malformed token; | |||
| - line, file, or identifier limit exceeded; | |||
| - duplicate or unknown name; | |||
| - syntax error with expected token; | |||
| - type mismatch or illegal conversion; | |||
| - access violation; | |||
| - incomplete or conflicting interface implementation; | |||
| - missing return; | |||
| - resource limit, disk error, or output collision. | |||
| ## 12. Complete Example | |||
| ```text | |||
| interface Printable | |||
| method print() | |||
| end interface | |||
| interface Resettable | |||
| method reset() | |||
| end interface | |||
| class Counter implements Printable, Resettable | |||
| private word value | |||
| public method init(start as word) | |||
| self.value = start | |||
| end method | |||
| public method increment() | |||
| self.value = self.value + 1 | |||
| end method | |||
| public method print() | |||
| print self.value | |||
| end method | |||
| public method reset() | |||
| self.value = 0 | |||
| end method | |||
| end class | |||
| program Main | |||
| var count as Counter | |||
| var output as Printable | |||
| count = new Counter(10) | |||
| count.increment() | |||
| output = count | |||
| output.print() | |||
| delete output | |||
| end program | |||
| ``` | |||
| ## Open Questions | |||
| - Which additional KERNAL-backed library operations belong in the first | |||
| standard runtime after the compiler core is proven. | |||
| - Which runtime-fault messages fit after the generated-program memory map is | |||
| measured; stable fault codes and a map-resolvable address are required. | |||
| @@ -0,0 +1,87 @@ | |||
| # C128 MMU and Common-RAM Probe | |||
| Story: E01-S02 | |||
| Status: observed in VICE x128 3.10; not a final compiler memory map | |||
| ## Registers Used | |||
| | Address | Name | Use in the probe | | |||
| |---|---|---| | |||
| | `$FF00` | CR | Bank/ROM/I/O configuration; visible in every mapping | | |||
| | `$D506` | RCR | Common-RAM size/placement; bits 6–7 (VIC bank) preserved | | |||
| | `$D507/$D508` | P0L/P0H | Page-zero pointer; write H first, L commits | | |||
| | `$D509/$D50A` | P1L/P1H | Page-one pointer; write H first, L commits | | |||
| Configurations exercised: | |||
| - `CR=$0E`: RAM bank 0, KERNAL ROM high, I/O at `$D000`. | |||
| - `CR=$4E`: the same with RAM bank 1 selected (bit 6). | |||
| - `RCR=$0E` (low bits): 8K common RAM at both bottom (`$0000–$1FFF`) and top | |||
| (`$E000–$FFFF`), always backed by bank 0. | |||
| The probe lives entirely in `$1C01–$1F46`, inside the bottom 8K common window, | |||
| so its code, data, zero page, and hardware stack stay visible with bank 1 | |||
| selected. | |||
| ## Observed Results | |||
| All checks pass (`mmu_probe_result = $FF`). The x128 screenshot shows | |||
| `NOVA128 MMU PROBE OK` followed by `READY.`. | |||
| | Check | Observation | | |||
| |---|---| | |||
| | Distinct banks | `$4000` holds `$11` in bank 0 and `$22` in bank 1 at the same time | | |||
| | Common RAM | A marker written in bank 0 is read and rewritten while bank 1 is selected, and the rewrite is visible from bank 0 | | |||
| | Bank-switched return | `JSR`/`RTS` with bank 1 selected returns with the same SP | | |||
| | P0 relocation, common off | P0 → bank 1 page `$20`: a write to `$00F0` appears at bank1:`$20F0` (`$77`) | | |||
| | P0 with common on | P0 → bank 1 page `$21`: a write to `$00F1` lands at **bank0**:`$21F1` (`$66`); logical `$00F1` and bank1:`$21F1` are unchanged | | |||
| | P1 with common on | P1 → bank 1 page `$21`: `PHA` lands at **bank0**:`$2100+SP` (`$5A`); bank0 page 1 and the bank-1 sentinel (`$33`) are unchanged; `PLA` returns the pushed value | | |||
| | Restoration | CR, RCR, P0L/H, P1L/H read back equal to their entry values before `PLP` | | |||
| | MMU write audit | 42 stores to `$FF00`/`$D500–$D50B` between entry and done, all from probe code; only `$FF00` and `$D506–$D50A` are written | | |||
| ### Common-RAM priority model | |||
| The original probe assumed common RAM would *cancel* a page pointer aimed at | |||
| bank 1. That was wrong. The observed rule is: | |||
| > The page pointer's page translation always applies. When the logical page | |||
| > (0 or 1) lies inside the active bottom common window, the pointer's bank | |||
| > bit is ignored and the access goes to bank 0 at the translated page. | |||
| Consequences for the compiler: | |||
| 1. With bottom common enabled, P0/P1 can relocate zero page or the stack only | |||
| within bank 0. Nova128 should keep both pointers in bank 0. | |||
| 2. Relocating P0/P1 into bank 1 requires bottom common disabled, which also | |||
| removes common visibility of the code doing the switch. Avoid it. | |||
| 3. A relocated page swaps with physical page 0/1 at the target address, so a | |||
| relocation target must not overlap live compiler data. | |||
| ## Interrupt Policy | |||
| The probe saves P with `PHP`, executes `SEI` before the first MMU write, and | |||
| does not restore the caller's I flag (`PLP`) until every MMU register has been | |||
| restored and checked. No KERNAL call occurs while a non-default mapping is | |||
| active. Taking an IRQ with bank 1 selected was deliberately **not** tested; the | |||
| `mmu.s` wrappers must either keep IRQs masked across non-default mappings or | |||
| E01-S03/E01-S05 must prove a common-RAM IRQ path. | |||
| ## Hardware Caveat | |||
| These facts are VICE x128 3.10 observations. Physical-hardware confirmation | |||
| belongs to E08-S06. | |||
| ## Reproducing | |||
| ```powershell | |||
| scripts/build-mmu-probe.ps1 # assemble + link, checks it fits below $2000 | |||
| scripts/run-mmu-probe.ps1 # visible run, writes build/mmu-probe-vicii.png | |||
| scripts/verify-mmu-probe.ps1 # remote-monitor result block + MMU write audit | |||
| ``` | |||
| `verify-mmu-probe.ps1` builds `build/mmu-probe-trace.gen.txt` from | |||
| `monitor/mmu-probe-trace.txt` plus the current `mmu_probe_done` address. The | |||
| `$FF00` store trace is armed only between probe entry and done, because the | |||
| KERNAL's common-RAM fetch helper at `$02A5/$02AB` writes `$FF00` hundreds of | |||
| times during boot. The full trace is saved to `build/mmu-probe-trace-dump.txt`. | |||
| For interactive debugging, use `monitor/mmu-probe-debug.txt`. | |||
| @@ -0,0 +1,96 @@ | |||
| # C128 Display and Clock Probe | |||
| Story: E01-S04 | |||
| Status: observed in VICE x128 3.10; not a final compiler memory map | |||
| ## Primary Platform Facts | |||
| The *Commodore 128 Programmer's Reference Guide* documents: | |||
| - `$00D7` (`MODE`) bit 7: `$00` selects the active 40-column editor and `$80` | |||
| selects the active 80-column editor. | |||
| - `$FF5F` (`SWAPPER`): switches the C128 editor's 40/80-column local state. | |||
| - `$D030` bit 0: set selects 2 MHz and disables VIC video; clear selects 1 MHz | |||
| and permits VIC video. | |||
| - `$D030` bit 1 is a chip-test facility and must remain clear. | |||
| - I/O accesses in 2 MHz mode are stretched to remain synchronized with 1 MHz | |||
| I/O devices. | |||
| Sources: | |||
| - *Commodore 128 Programmer's Reference Guide*, SWAPPER, page 449. | |||
| - Same guide, 8564 VIC “2 MHz Operation” and “System Clock Control,” pages | |||
| 589–590: <https://www.pagetable.com/docs/Commodore%20128%20Programmer%27s%20Reference%20Guide.pdf> | |||
| - Installed VICE 3.10 manual, C128 options `-40col` and `-80col`. | |||
| ## Probe Policy | |||
| `src/platform/display-clock.s` follows the policy approved by D112: | |||
| 1. Respect the editor/display already selected by the user. | |||
| 2. Send all text through KERNAL `CHROUT` (`$FFD2`). | |||
| 3. Force the conservative 1 MHz baseline before inspecting the display. | |||
| 4. In 40-column mode, stay at 1 MHz so VIC output remains visible. | |||
| 5. In 80-column mode, set `$D030` bit 0, perform KERNAL output at 2 MHz, and | |||
| then restore 1 MHz before returning to BASIC. | |||
| 6. If display mode or fast-mode activation is uncertain, use 1 MHz. This is the | |||
| deterministic fallback for the compiler. | |||
| The probe does not call SWAPPER because the compiler must not override the | |||
| user's selected display. SWAPPER remains the documented mechanism if a future, | |||
| separately approved feature explicitly changes displays. | |||
| ## Observed Results | |||
| The verification script boots x128 independently with `-40col` and `-80col`. | |||
| | Mode | MODE captured | Result | Fast observed | Final `$D030` bit 0 | Trace | | |||
| |---|---:|---:|---:|---:|---| | |||
| | 40-column | `$00` | `$44` | `$00` | `$00` | `$1C2E` then `$1C8C`; never `$1C49` | | |||
| | 80-column | `$80` | `$85` | `$01` | `$00` | `$1C3D`, `$1C49`, then `$1C8C` | | |||
| VICE reads `$FC` from `$D030` after fallback because unconnected upper bits | |||
| read high; the meaningful speed bit 0 and test bit 1 are both clear. The probe's | |||
| `final_clock` field stores the masked speed bit and is `$00` in both modes. | |||
| Result bit meanings are probe-local: bit 7 is the 80-column path, bit 6 is the | |||
| 40-column path, bit 2 proves ordinary semantic RAM markers survived, and bit 0 | |||
| proves the 2 MHz control bit read back set. | |||
| The 40-column screenshot contains: | |||
| ```text | |||
| NOVA128 40 COL KERNAL OK | |||
| 1MHZ FALLBACK OK | |||
| ``` | |||
| The 80-column screenshot contains: | |||
| ```text | |||
| NOVA128 80 COL KERNAL OK | |||
| 2MHZ KERNAL IO OK | |||
| RESTORED 1MHZ | |||
| ``` | |||
| ## VDC RAM Boundary | |||
| The probe never reads or writes VDC address/data registers `$D600/$D601`. | |||
| Machine-readable state lives in normal CPU RAM at `$1D0F–$1D14`; the build | |||
| script rejects executable source references to `$D600/$D601`. In 80-column | |||
| mode, only the C128 KERNAL/editor owns display transfer to VDC-local RAM. | |||
| This establishes the architectural rule: VDC RAM is display-only and never a | |||
| home for compiler symbols, semantic tables, stacks, or intermediate data. | |||
| ## Reproduction | |||
| From `newLang/`: | |||
| ```powershell | |||
| ./scripts/build-display-clock.ps1 | |||
| ./scripts/verify-display-clock.ps1 | |||
| ``` | |||
| Generated screenshots, remote-monitor trace transcripts, PRG, map, and labels | |||
| remain under ignored `build/`. | |||
| @@ -0,0 +1,19 @@ | |||
| ; E01-S04 non-stopping trace checkpoints. | |||
| ; 40-column must hit $1C2E then $1C8C. | |||
| ; 80-column must hit $1C3D, $1C49, then $1C8C. | |||
| radix H | |||
| device c: | |||
| sidefx off | |||
| add_label $1C0D .display_clock_entry | |||
| add_label $1C2E .mode_40_checkpoint | |||
| add_label $1C3D .mode_80_checkpoint | |||
| add_label $1C49 .fast_checkpoint | |||
| add_label $1C8C .fallback_done | |||
| trace exec .mode_40_checkpoint | |||
| trace exec .mode_80_checkpoint | |||
| trace exec .fast_checkpoint | |||
| trace exec .fallback_done | |||
| @@ -0,0 +1,9 @@ | |||
| ; E01-S03 non-stopping monitor evidence for scripts/verify-kernal-probe.ps1. | |||
| ; Records probe entry in the monitor log; results are read from the | |||
| ; kprobe_* block over the remote monitor. | |||
| radix H | |||
| device c: | |||
| sidefx off | |||
| trace exec $1C0D | |||
| @@ -0,0 +1,28 @@ | |||
| ; E01-S01 interactive VICE monitor setup. | |||
| ; Launch x128 with the built PRG and enter: | |||
| ; playback "monitor/minimal-debug.txt" | |||
| ; | |||
| ; RUN/autostart transfers from BASIC to $1C0D. The first breakpoint proves the | |||
| ; native entry path. Continue with `x`; the second breakpoint is immediately | |||
| ; before RTS. At that break, `m .minimal_result .minimal_result` must show A5. | |||
| radix H | |||
| device c: | |||
| sidefx off | |||
| add_label $1C0D .minimal_entry | |||
| add_label $1C24 .minimal_done | |||
| add_label $1C3E .minimal_result | |||
| break exec .minimal_entry | |||
| break exec .minimal_done | |||
| d .minimal_entry .minimal_done | |||
| m .minimal_result .minimal_result | |||
| r | |||
| ; Useful commands after the entry breakpoint: | |||
| ; x | |||
| ; m .minimal_result .minimal_result | |||
| ; return | |||
| @@ -0,0 +1,14 @@ | |||
| ; Non-stopping trace setup used by scripts/verify-minimal.ps1. | |||
| ; `trace` records execution in the monitor log without pausing emulation. | |||
| radix H | |||
| device c: | |||
| sidefx off | |||
| add_label $1C0D .minimal_entry | |||
| add_label $1C24 .minimal_done | |||
| add_label $1C3E .minimal_result | |||
| trace exec .minimal_entry | |||
| trace exec .minimal_done | |||
| @@ -0,0 +1,12 @@ | |||
| ; E01-S02 interactive setup. The build label file gives the current result | |||
| ; addresses. Use scripts/verify-mmu-probe.ps1 for automated assertions. | |||
| radix H | |||
| device c: | |||
| sidefx off | |||
| break exec $1C0D | |||
| watch store $FF00 | |||
| watch store $D506 $D50A | |||
| r | |||
| @@ -0,0 +1,18 @@ | |||
| ; E01-S02 non-stopping monitor evidence. Side effects are disabled before | |||
| ; tracing the configuration, common-RAM, and page-pointer registers. | |||
| ; | |||
| ; The KERNAL's common-RAM fetch helper ($02A5/$02AB) stores to $FF00 hundreds | |||
| ; of times while booting, so the $FF00 trace (#2) starts disabled and is armed | |||
| ; only by the probe-entry checkpoint (#1). scripts/verify-mmu-probe.ps1 appends | |||
| ; checkpoint #4 at mmu_probe_done, which disarms #2 again, to a generated copy | |||
| ; under build/ because that address moves with the code. | |||
| radix H | |||
| device c: | |||
| sidefx off | |||
| trace exec $1C0D | |||
| trace store $FF00 | |||
| disable 2 | |||
| command 1 "enable 2" | |||
| trace store $D500 $D50B | |||
| @@ -0,0 +1,145 @@ | |||
| --- | |||
| type: agent-workflow | |||
| source-path: project collaboration requirements | |||
| ingested-at: 2026-11-07 | |||
| --- | |||
| # Agent Workflow | |||
| This process lets multiple agents contribute without treating chat history as | |||
| the project database. | |||
| ## 1. Global Implementation Gate | |||
| The user requested approval before coding. Until the user explicitly approves | |||
| implementation, agents may improve specifications, investigate read-only facts, | |||
| or propose decisions, but must not add compiler/runtime implementation. | |||
| After approval, dependency-free stories may move from `planned` to `ready`. | |||
| ## 2. Before Claiming Work | |||
| 1. Read `../AGENTS.md` and the documents it routes to. | |||
| 2. Run `git status --short` and preserve unrelated changes. | |||
| 3. Read all files in `planning/claims/`. | |||
| 4. Select one story whose dependencies are complete. | |||
| 5. Confirm its expected file area does not overlap an active claim. | |||
| 6. Create a claim file named exactly `<story-id>.md`. | |||
| Do not edit a shared central status table merely to claim a story. Separate | |||
| claim files reduce merge conflicts. | |||
| ## 3. Claim Template | |||
| ```markdown | |||
| --- | |||
| story: E00-S00 | |||
| owner: agent-name-or-session | |||
| status: in-progress | |||
| started: YYYY-MM-DD | |||
| depends-on: [] | |||
| --- | |||
| # E00-S00 Claim | |||
| ## Intended change | |||
| One paragraph describing the bounded result. | |||
| ## File area | |||
| - Exact files or directories expected to change | |||
| ## Acceptance evidence | |||
| - [ ] Criterion copied from the backlog | |||
| ## Coordination notes | |||
| - Known overlaps, messages sent, or assumptions | |||
| ## Work log | |||
| - Commands and important findings | |||
| ## Completion | |||
| - Tests run: | |||
| - Results: | |||
| - Files changed: | |||
| - Remaining risks: | |||
| ``` | |||
| Valid claim statuses are `in-progress`, `blocked`, `review`, and `done`. | |||
| Agents must not mark a claim `done` when required verification is unavailable. | |||
| ## 4. Story Execution | |||
| - Work only on the claimed behavior. | |||
| - Use `apply_patch` for hand-authored file edits. | |||
| - Keep generated files under `build/`. | |||
| - Add a failing test or fixture before, or in the same change as, behavior when | |||
| practical. | |||
| - Prefer explicit error returns and bounded tables over implicit assumptions. | |||
| - Record discovered platform facts in the relevant document with evidence. | |||
| - If implementation conflicts with the spec, stop and open a decision; do not | |||
| quietly make the implementation the new specification. | |||
| ## 5. Blocking and Handoff | |||
| A blocked claim must state: | |||
| - the exact failed acceptance criterion; | |||
| - evidence already collected; | |||
| - the smallest decision or external fact needed; | |||
| - safe work that remains possible without that decision. | |||
| For a handoff, leave the claim in `in-progress` or `blocked`, summarize the | |||
| current state, and list the next command or file to inspect. Do not create | |||
| ambiguous half-completed status. | |||
| ## 6. Review | |||
| A reviewer should verify: | |||
| - the story did not expand scope; | |||
| - all acceptance criteria have direct evidence; | |||
| - source and generated artifacts are distinguishable; | |||
| - MMU/KERNAL behavior was tested in `x128` when relevant; | |||
| - no writable reference/editor image was modified; | |||
| - error paths and capacity boundaries were exercised; | |||
| - docs and stable diagnostic IDs remain synchronized. | |||
| When review passes, update the claim to `done` with the evidence. The backlog | |||
| itself remains a stable specification of work rather than a frequently edited | |||
| task board. | |||
| ## 7. Decisions and ADR-Like Records | |||
| Small implementation choices can be recorded in a claim. A choice requires an | |||
| entry in `docs/decisions.md` when it changes: | |||
| - user-visible syntax or semantics; | |||
| - compiler/runtime ABI; | |||
| - object or interface layout; | |||
| - source, PRG, ERR, or MAP formats; | |||
| - memory ownership; | |||
| - supported hardware or editor workflow; | |||
| - version 1 scope. | |||
| Use a new `Dxxx` identifier. Mark it `Proposed` until the user approves a | |||
| visible or scope-changing decision. | |||
| ## 8. Parallel Work Guidance | |||
| Safe early parallel lanes after implementation approval include: | |||
| - E01 platform probes, provided each uses separate PRG/linker targets; | |||
| - E02 diagnostic catalog and lexer fixtures; | |||
| - E06 host-side disk-copy scripts using disposable images; | |||
| - E07 test harness scaffolding that does not invent runtime addresses. | |||
| Do not parallelize two agents into the same assembly module or linker config. | |||
| The class ABI and interface ABI must be frozen before multiple backend agents | |||
| depend on them. | |||
| @@ -0,0 +1,48 @@ | |||
| --- | |||
| story: E00-S01 | |||
| owner: root | |||
| status: done | |||
| started: 2026-11-07 | |||
| depends-on: [] | |||
| --- | |||
| # E00-S01 Claim | |||
| ## Intended change | |||
| Record the user's instruction to continue as approval of the complete Nova128 | |||
| plan and its documented defaults. | |||
| ## File area | |||
| - `README.md` | |||
| - `docs/decisions.md` | |||
| - `docs/language-spec.md` | |||
| - `docs/architecture.md` | |||
| ## Acceptance evidence | |||
| - [x] User approval is recorded in `docs/decisions.md`. | |||
| - [x] `Nova128` and `.n8` are accepted. | |||
| - [x] C128 native mode, on-machine compilation, and C64-mode Vim integration | |||
| are accepted. | |||
| - [x] Version 1 interfaces and explicit memory management are accepted. | |||
| ## Coordination notes | |||
| The user replied `continue` immediately after being asked to approve the | |||
| Nova128 implementation. No contrary changes were supplied. | |||
| ## Work log | |||
| - Updated product status from planning-only to approved implementation. | |||
| - Moved D100-D110 into the accepted decision table. | |||
| - Preserved open implementation questions whose answers depend on probes. | |||
| ## Completion | |||
| - Tests run: documentation consistency inspection | |||
| - Results: approval gate is open; dependency-free stories may be claimed | |||
| - Files changed: README and canonical decision/spec/architecture headers | |||
| - Remaining risks: physical-hardware release policy remains open | |||
| @@ -0,0 +1,53 @@ | |||
| --- | |||
| story: E00-S02 | |||
| owner: e00-grammar-fixtures | |||
| status: done | |||
| started: 2026-11-07 | |||
| depends-on: [E00-S01] | |||
| --- | |||
| # E00-S02 Claim | |||
| ## Intended change | |||
| Create accepted and rejected Nova128 grammar fixtures plus a coverage manifest | |||
| and structural validator. This story adds no parser or compiler implementation. | |||
| ## File area | |||
| - `tests/grammar/` | |||
| - `planning/claims/E00-S02.md` | |||
| ## Acceptance evidence | |||
| - [x] At least one accepted fixture covers every declaration and statement form. | |||
| - [x] Rejected fixtures cover missing terminators, invalid line continuation, | |||
| duplicate declarations, and malformed interface/class headers. | |||
| - [x] Fixtures remain within the editor source profile. | |||
| - [x] Expected symbolic diagnostic IDs are named. | |||
| ## Coordination notes | |||
| - E01-S01 is active in another file area. | |||
| - Diagnostic IDs are symbolic placeholders for E02-S04; this story does not | |||
| assign numeric values. | |||
| ## Work log | |||
| - Read canonical documents and compiler-design guidance. | |||
| - Confirmed E00-S01 is complete and no grammar claim exists. | |||
| - Added seven accepted fixtures and eleven rejected fixtures with diagnostic | |||
| sidecars. | |||
| - Added a production/statement coverage matrix and structural validator. | |||
| ## Completion | |||
| - Tests run: `powershell -NoProfile -ExecutionPolicy Bypass -File | |||
| tests/grammar/validate.ps1` | |||
| - Results: `grammar fixtures ok: 7 accepted, 11 rejected`; longest source line | |||
| is 59 bytes and every fixture is below all editor-profile limits. | |||
| - Files changed: `tests/grammar/**`, `planning/claims/E00-S02.md` | |||
| - Remaining risks: numeric diagnostic assignments belong to E02-S04; these | |||
| fixtures intentionally specify symbolic IDs only. Parser execution evidence | |||
| cannot exist until the parser stories are implemented. | |||
| @@ -0,0 +1,52 @@ | |||
| --- | |||
| story: E00-S03 | |||
| owner: root | |||
| status: done | |||
| started: 2026-11-07 | |||
| depends-on: [E00-S01] | |||
| --- | |||
| # E00-S03 Claim | |||
| ## Intended change | |||
| Resolve the remaining language-level questions that would otherwise force the | |||
| lexer, code generator, or runtime-fault implementation to guess. | |||
| ## File area | |||
| - `docs/decisions.md` | |||
| - `docs/language-spec.md` | |||
| - `docs/architecture.md` | |||
| - character-literal coverage under `tests/grammar/` | |||
| ## Acceptance evidence | |||
| - [x] Cast, character-literal, overflow, and runtime-fault behavior is accepted, | |||
| deferred, or rejected. | |||
| - [x] No version 1 production relies on undefined behavior. | |||
| - [x] Decisions and fixtures agree. | |||
| ## Coordination notes | |||
| E00-S02 completed before character-literal fixture changes began. No active | |||
| agent owns the grammar fixture area. | |||
| ## Work log | |||
| - Recorded D111-D116 as baseline decisions following implementation approval. | |||
| - Deferred interface-to-class down-casts and debug overflow traps. | |||
| - Chose single-quoted PETSCII character literals and added accepted/rejected | |||
| grammar evidence. | |||
| - Required stable runtime fault codes plus a map-resolvable address. | |||
| ## Completion | |||
| - Tests run: `powershell -NoProfile -ExecutionPolicy Bypass -File | |||
| tests/grammar/validate.ps1` | |||
| - Results: `grammar fixtures ok: 7 accepted, 12 rejected` | |||
| - Files changed: canonical decisions/spec/architecture, character-literal | |||
| fixtures, grammar coverage matrix, and this claim | |||
| - Remaining risks: exact runtime fault payload and table capacities depend on | |||
| the E01 memory-map probes | |||
| @@ -0,0 +1,64 @@ | |||
| --- | |||
| story: E01-S01 | |||
| owner: e01_native_prg | |||
| status: done | |||
| started: 2026-11-07 | |||
| depends-on: [E00-S01] | |||
| --- | |||
| # E01-S01 Claim | |||
| ## Intended change | |||
| Create and verify the smallest ca65/ld65 PRG that starts in C128 native mode, | |||
| prints a deterministic signature, records a machine-readable success byte, and | |||
| returns cleanly to BASIC. | |||
| ## File area | |||
| - `src/platform/minimal.s` | |||
| - `cfg/minimal-c128.cfg` | |||
| - `scripts/build-minimal.ps1` | |||
| - `scripts/run-minimal.ps1` | |||
| - `monitor/minimal-debug.txt` | |||
| - generated files under `build/` | |||
| ## Acceptance evidence | |||
| - [x] `ca65`/`ld65` builds from a clean workspace. | |||
| - [x] `x128` autostarts the PRG in C128 native mode. | |||
| - [x] The PRG writes an expected screen/message and machine-readable result. | |||
| - [x] A monitor script reaches the documented entry point. | |||
| ## Coordination notes | |||
| This story will not establish MMU/common-RAM behavior or final compiler memory | |||
| addresses; those belong to E01-S02 and E01-S05. | |||
| ## Work log | |||
| - Claim created after confirming E00-S01 is complete. | |||
| - Added a 64-byte PRG with a BASIC 7 `SYS 7181` header and documented 6502 | |||
| instructions only. | |||
| - Added a bounded linker layout used only by this platform proof. | |||
| - Added clean build, screenshot run, remote-monitor verification, interactive | |||
| debug, and non-stopping trace paths. | |||
| - Observed the C128 BASIC 7 banner followed by `NOVA128 C128 NATIVE OK` and a | |||
| return to `READY.` in `build/minimal-vicii.png`. | |||
| - Verified trace hits at entry `$1C0D` and completion `$1C24`; remote monitor | |||
| read `$A5` from `minimal_result` at `$1C3E`. | |||
| ## Completion | |||
| - Tests run: `scripts/build-minimal.ps1` twice; `scripts/run-minimal.ps1 | |||
| -LimitCycles 30000000`; `scripts/verify-minimal.ps1`; | |||
| `tests/grammar/validate.ps1` as a coordination regression. | |||
| - Results: repeatable build; x128 native autostart and visible signature pass; | |||
| monitor trace contains `$1C0D` and `$1C24`; result byte is `$A5`; grammar | |||
| fixtures still report `7 accepted, 12 rejected`. | |||
| - Files changed: `src/platform/minimal.s`, `cfg/minimal-c128.cfg`, three | |||
| platform scripts, two monitor command files, and this claim. | |||
| - Remaining risks: the load address and result location are probe-local, not | |||
| the final compiler map; MMU/common-RAM behavior is intentionally untested and | |||
| belongs to E01-S02. | |||
| @@ -0,0 +1,80 @@ | |||
| --- | |||
| story: E01-S02 | |||
| owner: e01_mmu_probe | |||
| status: done | |||
| started: 2026-11-07 | |||
| depends-on: [E01-S01] | |||
| --- | |||
| # E01-S02 Claim | |||
| ## Intended change | |||
| Create an isolated C128 MMU probe that proves distinct RAM banks, verified | |||
| common memory, safe return across a bank switch, explicit IRQ handling, and | |||
| observed page-zero/page-one behavior. | |||
| ## File area | |||
| - `src/platform/mmu-probe.s` | |||
| - `cfg/mmu-probe-c128.cfg` | |||
| - `scripts/build-mmu-probe.ps1` | |||
| - `scripts/run-mmu-probe.ps1` | |||
| - `scripts/verify-mmu-probe.ps1` | |||
| - `monitor/mmu-probe-*.txt` | |||
| - `docs/mmu-probe.md` | |||
| - `planning/claims/E01-S02.md` | |||
| - generated files under `build/` | |||
| ## Acceptance evidence | |||
| - [x] Both 64K banks hold and return distinct test patterns (`$4000` = `$11` | |||
| in bank 0, `$22` in bank 1). | |||
| - [x] The chosen common region remains visible from both bank selections | |||
| (8K bottom+top common; marker round-trip passes). | |||
| - [x] Common code safely switches bank and returns with IRQ handling explicit | |||
| (`JSR`/`RTS` under bank 1 with SP unchanged; `PHP`/`SEI` ... `PLP`). | |||
| - [x] Page-zero/page-one relocation and common-priority behavior are recorded | |||
| in `docs/mmu-probe.md`. | |||
| - [x] VICE monitor evidence shows only intended MMU writes (42 stores in the | |||
| entry..done window, all from probe code, only `$FF00` and `$D506-$D50A`). | |||
| ## Coordination notes | |||
| E01-S01 files are read-only inputs. KERNAL mapping wrappers belong to E01-S03 | |||
| and are not implemented here. Added one paragraph to `docs/architecture.md` | |||
| §4.2 pointing to the evidence, as E01-S04 did for display/clock. | |||
| ## Work log | |||
| - Consulted the Commodore 128 Programmer's Reference Guide MMU sections. | |||
| - First verified run: result `$DF`; only the page-zero common-priority bit | |||
| failed. The probe had assumed bottom common would cancel a P0 pointer aimed | |||
| at bank 1. | |||
| - Added observation bytes. VICE x128 3.10 shows the translation applies but | |||
| the bank bit is forced to 0: P0 write landed at bank0:`$21F1`; P1 `PHA` | |||
| landed at bank0:`$2100+SP`. Rewrote both checks to assert that model. | |||
| - Fixed a probe side effect: the P1 test left `$5A` at bank0:`$21F6` (BASIC | |||
| area). That byte is now saved and restored. | |||
| - Fixed the verifier: byte checks parse monitor dump rows instead of only | |||
| matching row-start addresses; removed an extra `r` command that stopped | |||
| `q` from exiting x128; replaced a text grep of register names with a real | |||
| store trace on `$FF00` and `$D500-$D50B`, armed only during the probe window | |||
| because the KERNAL `$02A5/$02AB` helper floods `$FF00` stores during boot. | |||
| ## Completion | |||
| - Tests run: `scripts/verify-mmu-probe.ps1` (twice); | |||
| `scripts/run-mmu-probe.ps1`; `scripts/verify-minimal.ps1`; | |||
| `scripts/verify-display-clock.ps1`; `tests/grammar/validate.ps1`. | |||
| - Results: result `$FF`, completion `$A5`, banks `$11/$22`, pages `$77/$33`, | |||
| common-priority bank0 `$66/$5A`, 42 audited MMU stores; screenshot shows | |||
| `NOVA128 MMU PROBE OK` then `READY.`; minimal, display/clock, and grammar | |||
| regressions pass. | |||
| - Files changed: `src/platform/mmu-probe.s`, `scripts/verify-mmu-probe.ps1`, | |||
| `monitor/mmu-probe-trace.txt`, `docs/mmu-probe.md` (new), | |||
| `docs/architecture.md` §4.2, this claim. | |||
| - Remaining risks: VICE-only evidence (physical check is E08-S06). An IRQ | |||
| taken with bank 1 selected is untested; `mmu.s` must keep IRQs masked across | |||
| non-default mappings unless E01-S03/E01-S05 proves a common-RAM IRQ path. | |||
| The probe's addresses are not the final compiler map (E01-S05). | |||
| @@ -0,0 +1,74 @@ | |||
| --- | |||
| story: E01-S03 | |||
| owner: e01_kernal_wrapper | |||
| status: done | |||
| started: 2026-11-07 | |||
| depends-on: [E01-S02] | |||
| --- | |||
| # E01-S03 Claim | |||
| ## Intended change | |||
| Create an isolated C128 probe containing a reusable KERNAL call wrapper. From | |||
| two compiler mappings (bank 0 and bank 1 RAM with 8K bottom common), the | |||
| wrapper enters the KERNAL in the BASIC/KERNAL default mapping, passes A/X/Y in, | |||
| returns A/X/Y/C out, and restores the exact compiler MMU state. The probe uses | |||
| it for CHROUT, a drive-8 status read, and a device-not-present failure path. | |||
| ## File area | |||
| - `src/platform/kernal-probe.s` | |||
| - `cfg/kernal-probe-c128.cfg` | |||
| - `scripts/build-kernal-probe.ps1` | |||
| - `scripts/verify-kernal-probe.ps1` | |||
| - `monitor/kernal-probe-*.txt` | |||
| - `docs/kernal-wrapper.md` | |||
| - `planning/claims/E01-S03.md` | |||
| - generated files under `build/` | |||
| ## Acceptance evidence | |||
| - [x] Character output and a disk-status call work from both compiler bank | |||
| states (CHROUT lines visible; drive 8 returns `00, OK,00,00` from both). | |||
| - [x] MMU state before and after each wrapper is identical (CR/RCR/P0/P1 | |||
| compared after every call; `$4000` bank marker intact). | |||
| - [x] Carry/status and error values survive the wrapper contract (CHKIN on | |||
| device 30: C=1, A=5, READST `$80` in both states; carry-drop mutation is | |||
| detected). | |||
| - [x] Open channels are closed on success and failure (`LDTND` = 0 after both | |||
| paths in both states). | |||
| ## Coordination notes | |||
| Builds on E01-S02 facts in `docs/mmu-probe.md`. Does not create `mmu.s` or | |||
| the final compiler map; those belong to E01-S05 and later compiler stories. | |||
| ## Work log | |||
| - First layout overflowed the 8K common window by ~15 bytes; replaced the | |||
| 13-byte KCALL macro with an inline-word call form (`kcall_inline`). | |||
| - Verifier autostarts a fresh D64 through a true-emulation 1541 and polls the | |||
| completion byte, since true-drive load time varies. | |||
| - Monitor `screenshot` captured the black VDC window; switched to | |||
| `-exitscreenshotvicii`. | |||
| - Observed that `OPEN` with no filename sends nothing, so device-not-present | |||
| surfaces at `CHKIN`. | |||
| - Negative control: `AND #$FB` -> `AND #$FA` in the flag merge yields state | |||
| results `$EF` and a failing verifier; source restored afterwards. | |||
| ## Completion | |||
| - Tests run: `scripts/verify-kernal-probe.ps1` (three passing runs plus one | |||
| mutation run); `scripts/verify-mmu-probe.ps1`; `tests/grammar/validate.ps1`. | |||
| - Results: result `$FF`, state results `$FF/$FF`, fail A `$05/$05`, READST | |||
| `$80/$80`, status `00, OK,00,00` from both banks; screenshot shows | |||
| `NOVA128 KERNAL PROBE OK`; regressions pass. | |||
| - Files changed: `src/platform/kernal-probe.s`, `cfg/kernal-probe-c128.cfg`, | |||
| `scripts/build-kernal-probe.ps1`, `scripts/verify-kernal-probe.ps1`, | |||
| `monitor/kernal-probe-trace.txt`, `docs/kernal-wrapper.md`, | |||
| `docs/architecture.md` section 4.2, this claim. | |||
| - Remaining risks: unmaskable RESTORE NMI under bank 1 is untested (E01-S05); | |||
| only a 1541 on device 8 tested, no burst/fast serial; the probe spans | |||
| `$1C01-$1FFA`, 1018 of the 1023 bytes below `$2000`, so the real compiler must | |||
| keep only wrappers and shared state in common RAM; VICE-only evidence. | |||
| @@ -0,0 +1,68 @@ | |||
| --- | |||
| story: E01-S04 | |||
| owner: e01_display_clock | |||
| status: done | |||
| started: 2026-11-07 | |||
| depends-on: [E01-S01] | |||
| --- | |||
| # E01-S04 Claim | |||
| ## Intended change | |||
| Create a separate C128 platform probe that demonstrates KERNAL-compatible text | |||
| on the 40- and 80-column displays, observes safe 1/2 MHz selection behavior, | |||
| and documents a deterministic compiler fallback without using VDC RAM for | |||
| semantic state. | |||
| ## File area | |||
| - `src/platform/display-clock.s` | |||
| - `cfg/display-clock-c128.cfg` | |||
| - `scripts/build-display-clock.ps1` | |||
| - `scripts/verify-display-clock.ps1` | |||
| - `monitor/display-clock-*.txt` | |||
| - `docs/platform-display-clock.md` | |||
| - generated evidence under `build/` | |||
| ## Acceptance evidence | |||
| - [x] Text is legible in tested 40- and 80-column modes. | |||
| - [x] Safe clock switching conditions are documented from observed behavior. | |||
| - [x] No semantic compiler data is placed in VDC RAM. | |||
| - [x] Fallback clock/display behavior is deterministic. | |||
| ## Coordination notes | |||
| E01-S02 owns MMU/common-RAM probes in separate files. This story does not | |||
| freeze the final compiler memory map or alter E01-S01. | |||
| ## Work log | |||
| - Read scoped instructions, canonical documents, and compiler/VICE skills. | |||
| - Used the primary C128 guide's `$D7`, `$D030`, and KERNAL facts to define the | |||
| probe policy. | |||
| - Added an independent PRG, linker layout, clean build, remote-monitor | |||
| verification, non-stopping trace commands, and two display captures. | |||
| - Verified the 40-column path never enters the fast checkpoint. | |||
| - Verified the 80-column path observes `$D030` bit 0 set while KERNAL text is | |||
| emitted, then clears it before return. | |||
| - Statically rejected executable `$D600/$D601` references and kept semantic | |||
| markers in ordinary CPU RAM at `$1D13/$1D14`. | |||
| - Visually inspected both generated screenshots; expected messages are legible | |||
| and both sessions return to `READY.`. | |||
| ## Completion | |||
| - Tests run: `scripts/build-display-clock.ps1`; | |||
| `scripts/verify-display-clock.ps1`; `tests/grammar/validate.ps1`; | |||
| `scripts/build-minimal.ps1` | |||
| - Results: 278-byte probe builds; x128 40-column result `$44` and 80-column | |||
| result `$85`; both preserve `$5A/$C3` semantic markers and leave speed bit 0 | |||
| clear; trace paths and screenshots match the documented modes; grammar and | |||
| minimal-PRG regressions pass | |||
| - Files changed: separate E01-S04 source/config/scripts/trace/documentation, | |||
| the display/clock architecture paragraph, and this claim | |||
| - Remaining risks: VICE is mandatory evidence, but physical C128 validation is | |||
| still the P2 E08-S06 story; final compiler addresses remain E01-S05 work | |||
| @@ -0,0 +1,71 @@ | |||
| --- | |||
| story: E07-S01 | |||
| owner: e07_disk_builder | |||
| status: done | |||
| started: 2026-11-07 | |||
| depends-on: [E01-S01] | |||
| --- | |||
| # E07-S01 Claim | |||
| ## Intended change | |||
| Create and verify a PowerShell builder that always starts from a fresh copy of | |||
| the existing Vim work D81, injects currently available Nova128 artifacts, and | |||
| never changes the source image. | |||
| ## File area | |||
| - `scripts/prepare-disk.ps1` | |||
| - `tests/disk/prepare-disk.tests.ps1` | |||
| - generated files under `build/` | |||
| - this claim | |||
| ## Acceptance evidence | |||
| - [x] Copies `../vim/work.d81`; never writes the original. | |||
| - [x] Injects compiler/examples deterministically with `c1541`. | |||
| - [x] Rebuilding starts from a known clean image. | |||
| - [x] Script clearly reports missing tools and source images. | |||
| ## Coordination notes | |||
| - E01-S01 is complete and provides `build/minimal.prg`, the only current | |||
| Nova128 executable artifact. | |||
| - E01-S02 is active in separate platform-probe files. | |||
| - This story does not add editor/compiler launchers. | |||
| ## Work log | |||
| - Confirmed source D81 SHA-256 before work: | |||
| `CE4FAE640A8558977C9C0AF2FD76769C1EF997E7445000C4C7C04638D05D6081`. | |||
| - Confirmed VICE 3.10 `c1541.exe` is installed. | |||
| - Added `prepare-disk.ps1` with validated defaults, explicit source/output | |||
| separation, fresh-copy rebuilds, optional-artifact warnings, and verified | |||
| `c1541` directory entries. | |||
| - Added deterministic ASCII-to-PETSCII SEQ conversion for future `.n8` | |||
| examples, sorted by filename. | |||
| - Added an integration test that injects a compiler-shaped PRG, the E01-S01 | |||
| probe, and a generated example into a disposable image. | |||
| - Deliberately damaged the first output, rebuilt it, and obtained the same | |||
| SHA-256 from the clean source copy. | |||
| - Exercised missing source, missing tool, source/output alias rejection, and | |||
| absent optional artifact paths. | |||
| ## Completion | |||
| - Tests run: `tests/disk/prepare-disk.tests.ps1`; | |||
| `tests/grammar/validate.ps1`; PowerShell parser validation for both scripts; | |||
| default `scripts/prepare-disk.ps1`; `c1541 -list` and read-back inspection | |||
| - Results: tests passed; two clean preparations produced SHA-256 | |||
| `208EF297611453CE739B78AF4F7DF59455107A549C1CA15E9EF6B949D56FDB68`; | |||
| source SHA-256 remained | |||
| `CE4FAE640A8558977C9C0AF2FD76769C1EF997E7445000C4C7C04638D05D6081`; | |||
| directory entries `n128c` (PRG), `n128probe` (PRG), and `demo.n8` (SEQ) | |||
| were verified | |||
| - Files changed: `scripts/prepare-disk.ps1`, | |||
| `tests/disk/prepare-disk.tests.ps1`, and this claim | |||
| - Remaining risks: the real `N128C.PRG` and production examples do not exist | |||
| yet, so the test uses the verified E01-S01 PRG as a compiler-shaped fixture; | |||
| the default run warns and continues until those optional artifacts are built | |||
| @@ -0,0 +1,9 @@ | |||
| # Story Claims | |||
| Agents create one `<story-id>.md` file here using the template in | |||
| `../agent-workflow.md`. Absence of a claim does not override story dependencies | |||
| or the user's implementation-approval gate. | |||
| Do not delete completed claims; they are the durable implementation and test | |||
| history for later agents. | |||
| @@ -0,0 +1,670 @@ | |||
| --- | |||
| type: product-backlog | |||
| source-path: language specification and compiler architecture | |||
| ingested-at: 2026-11-07 | |||
| version: 0.1-draft | |||
| --- | |||
| # Nova128 Epics and Stories | |||
| All stories are `planned` until the user approves implementation. Story IDs are | |||
| stable. Splitting a story creates child IDs such as `E02-S03a`; do not renumber | |||
| existing work. | |||
| ## Status and Priority | |||
| - **P0:** required to prove feasibility or unblock most other work | |||
| - **P1:** required for version 1 | |||
| - **P2:** valuable release hardening | |||
| A dependency is complete only when its claim is `done` with acceptance evidence. | |||
| ## Epic Summary | |||
| | Epic | Outcome | Exit gate | | |||
| |---|---|---| | |||
| | E00 | Product contracts are frozen enough to implement | User approves plan and provisional defaults | | |||
| | E01 | C128 platform mechanisms are proven | Stable compiler memory-map inputs and test mailbox | | |||
| | E02 | Source can be read, tokenized, diagnosed, and represented | Vim-created token fixture passes on x128 | | |||
| | E03 | Programs parse and type-check | Procedural semantic fixtures pass | | |||
| | E04 | Procedural programs compile and run | Control-flow and call examples pass end to end | | |||
| | E05 | Classes and deterministic objects work | Two independent objects construct, mutate, and delete | | |||
| | E06 | Interfaces and dynamic dispatch work | Multiple interface implementations dispatch correctly | | |||
| | E07 | Editor-to-compiler workflow is safe and repeatable | Vim-created source builds from a copied D81 | | |||
| | E08 | Version 1 is hardened and packaged | Regression, stress, docs, and release gates pass | | |||
| --- | |||
| ## E00 — Product and Contract Freeze | |||
| Purpose: turn provisional assumptions into explicit user-approved contracts. | |||
| Primary files: `docs/`, `README.md`, `AGENTS.md`. | |||
| ### E00-S01 — Approve product defaults (P0) | |||
| **Depends on:** none | |||
| **Story:** As the product owner, I want the working name, source suffix, target, | |||
| editor strategy, interface scope, and ownership model confirmed so agents do | |||
| not implement mutually incompatible products. | |||
| **Acceptance criteria:** | |||
| - User approval or requested changes are recorded in `docs/decisions.md`. | |||
| - The language name and source suffix have an explicit status. | |||
| - C128 native mode, on-machine compilation, and C64-mode Vim integration have | |||
| explicit status. | |||
| - Version 1 interface and memory-management scope is explicit. | |||
| ### E00-S02 — Freeze source grammar fixtures (P1) | |||
| **Depends on:** E00-S01 | |||
| **Story:** As a parser implementer, I want accepted and rejected source fixtures | |||
| for every grammar production so parser behavior is testable without guessing. | |||
| **Acceptance criteria:** | |||
| - At least one accepted fixture covers every declaration and statement form. | |||
| - Rejected fixtures cover missing terminators, invalid line continuation, | |||
| duplicate declarations, and malformed interface/class headers. | |||
| - Fixtures remain within the editor source profile. | |||
| - Expected diagnostic IDs are named even if numeric assignments come later. | |||
| ### E00-S03 — Resolve specification open questions (P1) | |||
| **Depends on:** E00-S01 | |||
| **Story:** As a language implementer, I want cast, char-literal, overflow, and | |||
| runtime-fault decisions resolved before their code paths are written. | |||
| **Acceptance criteria:** | |||
| - Each open language-spec question is accepted, deferred, or rejected. | |||
| - No version 1 grammar production references undefined behavior. | |||
| - Changes are reflected in fixtures and decisions. | |||
| --- | |||
| ## E01 — C128 Platform Foundation | |||
| Purpose: prove the hardware/emulator facts on which every assembly module | |||
| depends. Primary files: `src/common/`, `src/compiler/mmu.s`, `cfg/`, `tests/platform/`, | |||
| `monitor/`, and story-specific probe targets. | |||
| ### E01-S01 — Minimal native C128 PRG (P0) | |||
| **Depends on:** E00-S01 | |||
| **Story:** As a compiler developer, I want a minimal ca65/ld65 C128-native PRG | |||
| that starts, writes a deterministic signature, and exits predictably. | |||
| **Acceptance criteria:** | |||
| - `ca65`/`ld65` builds from a clean workspace. | |||
| - `x128` autostarts the PRG in C128 native mode. | |||
| - The PRG writes an expected screen/message and machine-readable result. | |||
| - A monitor script reaches the documented entry point. | |||
| ### E01-S02 — MMU and common-RAM probe (P0) | |||
| **Depends on:** E01-S01 | |||
| **Story:** As a memory architect, I want verified bank configurations and a | |||
| common-memory trampoline so bank switching cannot be based on C64 assumptions. | |||
| **Acceptance criteria:** | |||
| - Both 64K banks are written/read with distinct patterns. | |||
| - The chosen common region remains visible across configurations. | |||
| - Code safely switches banks and returns with interrupts handled explicitly. | |||
| - Page-zero/page-one behavior is recorded. | |||
| - VICE watchpoints show only the intended MMU writes. | |||
| ### E01-S03 — KERNAL mapping wrapper probe (P0) | |||
| **Depends on:** E01-S02 | |||
| **Story:** As an I/O implementer, I want a wrapper that enters KERNAL routines | |||
| from compiler mappings and restores state afterward. | |||
| **Acceptance criteria:** | |||
| - Character output and a disk-status call work from both compiler bank states. | |||
| - MMU state before and after each wrapper is identical. | |||
| - Carry/status and error values survive the wrapper contract. | |||
| - Open channels are closed on success and failure. | |||
| ### E01-S04 — Clock and display-mode probe (P1) | |||
| **Depends on:** E01-S01 | |||
| **Story:** As a user, I want compiler output to work on either C128 display | |||
| without unsafe 2 MHz assumptions. | |||
| **Acceptance criteria:** | |||
| - Text is legible in tested 40- and 80-column modes. | |||
| - Safe clock switching conditions are documented from observed behavior. | |||
| - No semantic compiler data is placed in VDC RAM. | |||
| - The fallback clock/display behavior is deterministic. | |||
| ### E01-S05 — Freeze memory-map inputs and test mailbox (P0) | |||
| **Depends on:** E01-S02, E01-S03, E01-S04 | |||
| **Story:** As downstream agents, we need agreed linker regions, reserved zero | |||
| page, and a test mailbox so independent modules share one platform contract. | |||
| **Acceptance criteria:** | |||
| - `docs/architecture.md` lists verified regions and reservations. | |||
| - `cfg/compiler.cfg` can link a capacity-checking skeleton. | |||
| - `src/common/c128.inc` defines observed hardware/KERNAL constants. | |||
| - The test mailbox address and protocol are documented and exercised. | |||
| --- | |||
| ## E02 — Source, Lexing, Names, and Diagnostics | |||
| Purpose: consume the exact files produced by Vim and establish bounded compiler | |||
| tables. Primary files: `src/compiler/source.s`, `lexer.s`, `names.s`, | |||
| `diagnostics.s`, `src/common/tokens.inc`, `errors.inc`, `tests/lexer/`. | |||
| ### E02-S01 — Buffered PETSCII source reader (P0) | |||
| **Depends on:** E01-S03, E01-S05 | |||
| **Story:** As the compiler, I want a rewindable SEQ source stream with exact | |||
| line/column tracking. | |||
| **Acceptance criteria:** | |||
| - Reads Vim-created SEQ source without host conversion. | |||
| - Accepts `$0D` lines and a missing final `$0D`. | |||
| - Enforces line/file limits with stable errors. | |||
| - Rewinds identically for all compiler passes. | |||
| - Disk failure closes channels and leaves no partial output. | |||
| ### E02-S02 — PETSCII lexer (P0) | |||
| **Depends on:** E02-S01, E00-S02 | |||
| **Story:** As a parser, I want normalized, positioned tokens for the complete | |||
| version 1 lexical grammar. | |||
| **Acceptance criteria:** | |||
| - Both supported PETSCII letter ranges compare case-insensitively. | |||
| - Keywords, identifiers, numbers, strings, operators, comments, EOL, and EOF | |||
| match the specification. | |||
| - Invalid bytes and overlong identifiers report exact positions. | |||
| - Token dump of a Vim-created fixture matches a golden expected file. | |||
| ### E02-S03 — Name interning and bounded tables (P1) | |||
| **Depends on:** E01-S05, E02-S02 | |||
| **Story:** As semantic passes, I want compact interned names with deterministic | |||
| capacity errors. | |||
| **Acceptance criteria:** | |||
| - Equal normalized identifiers share an ID. | |||
| - First-declaration spelling is preserved for diagnostics/MAP output. | |||
| - Hash collisions are tested. | |||
| - Full tables report a resource error without memory corruption. | |||
| ### E02-S04 — Stable diagnostic catalog and ERR writer (P1) | |||
| **Depends on:** E01-S03, E00-S02 | |||
| **Story:** As a user and test author, I want stable diagnostic IDs, compact | |||
| screen messages, and complete ERR output. | |||
| **Acceptance criteria:** | |||
| - Lexical, syntax, type, access, interface, disk, and resource categories have | |||
| stable IDs. | |||
| - Screen form fits 40 columns for representative diagnostics. | |||
| - ERR output contains source, line, column, code, and message. | |||
| - Error limiting defaults to 20 and terminates cleanly. | |||
| --- | |||
| ## E03 — Parser and Semantic Model | |||
| Purpose: recognize complete version 1 source and reject invalid programs before | |||
| code emission. Primary files: `parser.s`, `expr.s`, `symbols.s`, `types.s`, | |||
| `classes.s`, `interfaces.s`, `tests/parser/`, `tests/semantic/`. | |||
| ### E03-S01 — Top-level declaration parser (P0) | |||
| **Depends on:** E02-S02, E02-S03, E02-S04 | |||
| **Acceptance criteria:** | |||
| - Parses interfaces, classes, implements lists, and exactly one program. | |||
| - Supports forward top-level type references. | |||
| - Rejects duplicate names and invalid ordering with recovery to later lines. | |||
| - Accepted/rejected fixtures from E00-S02 pass. | |||
| ### E03-S02 — Method, field, and local parser (P1) | |||
| **Depends on:** E03-S01 | |||
| **Acceptance criteria:** | |||
| - Parses access, signatures, parameters, fields, and leading local declarations. | |||
| - Enforces unique method names and valid `init`/`destroy` shapes. | |||
| - Assigns lexical scopes without leaking names across methods/classes. | |||
| - Reports private-member violations with source positions. | |||
| ### E03-S03 — Expression and statement parser (P1) | |||
| **Depends on:** E03-S02 | |||
| **Acceptance criteria:** | |||
| - Implements documented precedence and left/right associativity. | |||
| - Parses every version 1 statement form. | |||
| - Rejects multiple statements per line and cross-line expressions. | |||
| - Recovers sufficiently to report a later independent error. | |||
| ### E03-S04 — Type representation and checking (P1) | |||
| **Depends on:** E03-S03 | |||
| **Acceptance criteria:** | |||
| - Represents primitives, arrays, class references, interface references, and | |||
| null. | |||
| - Implements exactly the specified implicit conversions. | |||
| - Checks calls, assignments, returns, conditions, indexing, and operators. | |||
| - Reports incompatible interface assignments and missing returns. | |||
| ### E03-S05 — Class layout and access model (P1) | |||
| **Depends on:** E03-S02, E03-S04 | |||
| **Acceptance criteria:** | |||
| - Field offsets begin after the class-descriptor pointer and are deterministic. | |||
| - Instance size overflow/capacity is diagnosed. | |||
| - Field default privacy and method default publicity match the specification. | |||
| - MAP fixture lists sizes and offsets in declaration order. | |||
| ### E03-S06 — Interface conformance model (P1) | |||
| **Depends on:** E03-S02, E03-S04 | |||
| **Acceptance criteria:** | |||
| - Interface method slots follow interface source order. | |||
| - A public exact-signature method satisfies a requirement. | |||
| - One method may satisfy identical requirements from multiple interfaces. | |||
| - Same-name incompatible requirements are rejected. | |||
| - Missing, private, parameter-mismatched, and return-mismatched methods each | |||
| have fixture coverage. | |||
| --- | |||
| ## E04 — Procedural Code Generation | |||
| Purpose: produce runnable native code before adding heap/object complexity. | |||
| Primary files: `sizing.s`, `codegen.s`, `output.s`, `src/runtime/startup.s`, | |||
| `frames.s`, `arithmetic.s`, `print.s`, `faults.s`, `tests/codegen/`. | |||
| ### E04-S01 — PRG and MAP writer (P0) | |||
| **Depends on:** E01-S05, E02-S04 | |||
| **Acceptance criteria:** | |||
| - Writes a valid load-address header and deterministic sections. | |||
| - Emits a runnable startup path and clean termination. | |||
| - Refuses/handles output-name collision according to the approved policy. | |||
| - Removes or marks partial output after any failure. | |||
| - MAP records section start, size, and entry point. | |||
| ### E04-S02 — Software frames and calling convention (P0) | |||
| **Depends on:** E01-S05, E03-S04 | |||
| **Acceptance criteria:** | |||
| - Arguments and locals use the documented software frame. | |||
| - Nested calls preserve required registers and frame state. | |||
| - One- and two-byte returns follow the ABI. | |||
| - Stack overflow follows a stable runtime-fault path. | |||
| - ABI tests include recursion to a bounded successful depth and overflow. | |||
| ### E04-S03 — Scalar expressions and assignments (P1) | |||
| **Depends on:** E04-S02 | |||
| **Acceptance criteria:** | |||
| - Emits correct byte, word, integer, boolean, comparison, and assignment code. | |||
| - Short-circuit `and`/`or` avoids evaluating the unused operand. | |||
| - Division by zero faults predictably. | |||
| - Golden disassembly covers representative expression shapes. | |||
| ### E04-S04 — Control flow and functions (P1) | |||
| **Depends on:** E04-S03, E03-S03 | |||
| **Acceptance criteria:** | |||
| - Emits `if`, `else`, `while`, `for`, calls, and returns. | |||
| - Pass 2 sizes/addresses exactly match Pass 3 output. | |||
| - Relative branches that cannot reach use a correct long-branch expansion. | |||
| - Nested-control fixtures execute expected mailbox signatures. | |||
| ### E04-S05 — Print and runtime faults (P1) | |||
| **Depends on:** E04-S03, E01-S04 | |||
| **Acceptance criteria:** | |||
| - Prints literals and every supported scalar on tested displays. | |||
| - Null, bounds, divide, stack, and heap faults have stable codes. | |||
| - Runtime messages do not corrupt the program's memory mapping. | |||
| - `hello.n8` compiles and runs end to end in x128. | |||
| --- | |||
| ## E05 — Classes, Heap, and Concrete Methods | |||
| Purpose: implement object identity, encapsulated state, construction, and | |||
| deterministic lifetime before interface dispatch. Primary files: | |||
| `src/runtime/heap.s`, `objects.s`, compiler class/codegen modules, object tests. | |||
| ### E05-S01 — Free-list heap (P0) | |||
| **Depends on:** E01-S05, E04-S05 | |||
| **Acceptance criteria:** | |||
| - Allocates, zeroes, frees, splits, and coalesces blocks deterministically. | |||
| - Heap exhaustion faults without corrupting allocated blocks. | |||
| - Debug mode detects representative double-free/corrupt-header cases. | |||
| - Stress test returns the heap to its initial free capacity. | |||
| ### E05-S02 — Class descriptor format (P1) | |||
| **Depends on:** E03-S05, E05-S01 | |||
| **Acceptance criteria:** | |||
| - Descriptor identifies instance size and optional destructor. | |||
| - Every object begins with a valid descriptor pointer. | |||
| - Descriptor/MAP output is deterministic. | |||
| - Format reserves a documented path to interface-directory data without | |||
| introducing class inheritance. | |||
| ### E05-S03 — Construction and deletion (P1) | |||
| **Depends on:** E05-S02, E04-S02 | |||
| **Acceptance criteria:** | |||
| - `new` allocates, initializes the header, then calls `init` once. | |||
| - Failed allocation does not call `init`. | |||
| - `delete null` is a no-op. | |||
| - `delete` calls `destroy` once and frees the complete object. | |||
| - Constructor arguments and destructor side effects have end-to-end tests. | |||
| ### E05-S04 — Fields, self, and concrete calls (P1) | |||
| **Depends on:** E05-S03, E03-S05 | |||
| **Acceptance criteria:** | |||
| - `self.field` loads/stores correct offsets. | |||
| - Concrete calls use direct `JSR` and preserve the caller ABI. | |||
| - Private access is rejected at compile time. | |||
| - Two objects of one class retain independent state. | |||
| - `counter.n8` compiles and runs with the expected result. | |||
| ### E05-S05 — Fixed arrays and debug bounds (P2) | |||
| **Depends on:** E04-S03, E05-S04 | |||
| **Acceptance criteria:** | |||
| - Local and field arrays use specified element sizes. | |||
| - Constant and computed indices address correct elements. | |||
| - Debug bounds checks fault on low/high invalid indices. | |||
| - Release behavior matches the approved bounds-check policy. | |||
| --- | |||
| ## E06 — Interfaces and Dynamic Dispatch | |||
| Purpose: deliver the user-requested interface polymorphism. Primary files: | |||
| compiler/runtime `interfaces.s`, class descriptors, code generation, ABI tests. | |||
| ### E06-S01 — Freeze interface ABI (P0) | |||
| **Depends on:** E01-S05, E03-S06, E05-S02 | |||
| **Acceptance criteria:** | |||
| - Four-byte interface value layout is documented and tested little-endian. | |||
| - Interface slot and per-class table layouts are frozen. | |||
| - Conversion, parameter, return, local, field, and array storage rules are | |||
| specified. | |||
| - Register clobbers and the four-byte return convention are finalized. | |||
| ### E06-S02 — Indirect-call trampoline (P0) | |||
| **Depends on:** E06-S01, E04-S02 | |||
| **Acceptance criteria:** | |||
| - Calls at least three target methods selected at runtime. | |||
| - Returns correctly through nested and recursive calls. | |||
| - Preserves documented registers, software frame, and interrupt state. | |||
| - Null/invalid targets use a fault path rather than executing address zero. | |||
| - Hardware-stack boundary tests pass. | |||
| ### E06-S03 — Emit interface implementation tables (P1) | |||
| **Depends on:** E06-S01, E05-S04 | |||
| **Acceptance criteria:** | |||
| - Each class emits one method table per implemented interface. | |||
| - Table addresses and slots appear in MAP output. | |||
| - One method can populate slots for multiple compatible interfaces. | |||
| - Different classes implementing one interface emit distinct correct tables. | |||
| ### E06-S04 — Concrete-to-interface conversion (P1) | |||
| **Depends on:** E06-S03 | |||
| **Acceptance criteria:** | |||
| - Assignment creates the correct object/table pair. | |||
| - `null` conversion produces a canonical null interface value. | |||
| - Passing, returning, storing, and array-copying interface values preserve all | |||
| four bytes. | |||
| - Invalid class-to-interface conversion is a compile-time error. | |||
| ### E06-S05 — Interface method calls (P1) | |||
| **Depends on:** E06-S02, E06-S04 | |||
| **Acceptance criteria:** | |||
| - Call uses the compile-time interface slot and runtime table pointer. | |||
| - `self` is the underlying object, not the fat-reference address. | |||
| - Two classes dispatch the same interface call to different methods. | |||
| - One class implements and dispatches at least two interfaces. | |||
| - Arguments and one-/two-byte returns work through interface calls. | |||
| ### E06-S06 — Interface lifetime and failure paths (P1) | |||
| **Depends on:** E06-S05, E05-S03 | |||
| **Acceptance criteria:** | |||
| - `delete` through an interface invokes the class destructor and frees once. | |||
| - `delete null-interface` is a no-op. | |||
| - Null calls fault predictably. | |||
| - A debug use-after-free scenario produces a detectable failure where the | |||
| approved debug policy promises detection. | |||
| - `objects.n8` covers multiple interfaces end to end. | |||
| --- | |||
| ## E07 — Vim and Disk Workflow | |||
| Purpose: make source creation and compilation repeatable without modifying the | |||
| original editor. Primary files: `scripts/`, `docs/editor-workflow.md`, | |||
| integration fixtures, generated `build/` artifacts. | |||
| ### E07-S01 — Disposable development image builder (P0) | |||
| **Depends on:** E01-S01 | |||
| **Acceptance criteria:** | |||
| - Copies `../vim/work.d81`; never writes the original. | |||
| - Injects compiler/examples deterministically with `c1541`. | |||
| - Rebuilding starts from a known clean image. | |||
| - Script clearly reports missing tools and source images. | |||
| ### E07-S02 — Sequential editor/compiler launchers (P1) | |||
| **Depends on:** E07-S01, E04-S01 | |||
| **Acceptance criteria:** | |||
| - Editor launcher uses `x64sc` and autostarts Vim. | |||
| - Compiler launcher uses `x128` and autostarts the compiler. | |||
| - Launchers wait for process/image flush before returning. | |||
| - Documentation and warnings prohibit simultaneous writable attachment. | |||
| ### E07-S03 — Vim source round-trip (P0) | |||
| **Depends on:** E07-S02, E02-S02 | |||
| **Acceptance criteria:** | |||
| - A source file created entirely in Vim tokenizes on C128 without host | |||
| conversion. | |||
| - Reopening it in Vim preserves content. | |||
| - Extracted bytes match the expected PETSCII/CR fixture. | |||
| - Both supported PETSCII letter ranges are exercised. | |||
| ### E07-S04 — Edit/compile/run walkthrough (P1) | |||
| **Depends on:** E07-S03, E06-S06 | |||
| **Acceptance criteria:** | |||
| - A new user can edit, compile, inspect diagnostics, and run an interface-based | |||
| sample from documented commands. | |||
| - Failure recovery for syntax errors and output collisions is documented. | |||
| - No step requires modifying `vim/` or a reference disk. | |||
| --- | |||
| ## E08 — Hardening and Release | |||
| Purpose: prove version 1 against limits and package a reproducible release. | |||
| Primary files: `tests/`, `monitor/`, `release/`, and final documentation. | |||
| ### E08-S01 — Automated x128 regression harness (P0) | |||
| **Depends on:** E01-S05, E02-S04 | |||
| **Acceptance criteria:** | |||
| - Creates a fresh D81 per test run. | |||
| - Enforces time/cycle limits and captures machine-readable results. | |||
| - Extracts PRG/ERR/MAP outputs and compares expected artifacts. | |||
| - A failed test identifies the fixture and observed signature. | |||
| ### E08-S02 — Golden code and disassembly suite (P1) | |||
| **Depends on:** E04-S04, E05-S04, E06-S05 | |||
| **Acceptance criteria:** | |||
| - Covers scalar operations, branches, frames, object fields, construction, | |||
| deletion, conversions, and interface dispatch. | |||
| - Expected bytes/disassembly are reviewed rather than blindly regenerated. | |||
| - All emitted opcodes are documented 8502/6502 instructions. | |||
| ### E08-S03 — Capacity and failure stress suite (P1) | |||
| **Depends on:** E05-S05, E06-S06, E08-S01 | |||
| **Acceptance criteria:** | |||
| - Exercises maximum line/file/identifier limits and every compiler table. | |||
| - Exercises disk errors, heap exhaustion, stack exhaustion, and error limit. | |||
| - Exercises many classes, interfaces, slots, fields, locals, and nested calls. | |||
| - Every limit fails cleanly with its stable code. | |||
| ### E08-S04 — Example and conformance suite (P1) | |||
| **Depends on:** E06-S06, E08-S01 | |||
| **Acceptance criteria:** | |||
| - Includes Hello World, Counter, two class instances, two implementations of | |||
| one interface, one class with two interfaces, constructor/destructor, arrays, | |||
| and deliberate compile/runtime failures. | |||
| - Each example states expected output. | |||
| - Every normative language-spec section maps to at least one test or an | |||
| explicit non-executable review check. | |||
| ### E08-S05 — Release image and documentation (P1) | |||
| **Depends on:** E07-S04, E08-S02, E08-S03, E08-S04 | |||
| **Acceptance criteria:** | |||
| - Clean build produces compiler PRG, copied development D81, checksums, and | |||
| release notes. | |||
| - User guide covers editor workflow, language basics, interfaces, diagnostics, | |||
| limits, and recovery. | |||
| - Developer guide covers build, memory map, ABI, tests, and adding diagnostics. | |||
| - Release artifacts are regenerated from source without manual image edits. | |||
| ### E08-S06 — Physical C128 smoke test (P2) | |||
| **Depends on:** E08-S05; physical hardware availability | |||
| **Acceptance criteria:** | |||
| - Compiler starts in native mode from the release media. | |||
| - Vim-created source compiles and the generated interface example runs. | |||
| - 40/80-column and disk behavior are recorded. | |||
| - Any emulator/hardware difference becomes a tracked story, not an undocumented | |||
| workaround. | |||
| --- | |||
| ## Suggested First Parallel Wave After Approval | |||
| - Agent A: E01-S01 minimal native PRG | |||
| - Agent B: E00-S02 grammar fixtures | |||
| - Agent C: E02-S04 diagnostic catalog design (documentation/fixtures only until | |||
| E01 wrappers exist) | |||
| - Agent D: E07-S01 disposable image builder | |||
| The MMU probe, ABI modules, and linker configurations require deliberate | |||
| coordination; they should not be edited concurrently by unrelated stories. | |||
| ## Open Questions | |||
| - Which provisional decisions does the user want changed before E00-S01 closes? | |||
| - Is physical-hardware testing a release gate or a post-release validation? | |||
| - How many compiler-table records fit after E01 freezes the real memory map? | |||
| - Should separate compilation become the first post-version-1 epic? | |||
| @@ -0,0 +1,74 @@ | |||
| # E01-S04: build the independent C128 display/clock probe. | |||
| [CmdletBinding()] | |||
| param() | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| $build = Join-Path $project "build" | |||
| $source = Join-Path $project "src\platform\display-clock.s" | |||
| $config = Join-Path $project "cfg\display-clock-c128.cfg" | |||
| $object = Join-Path $build "display-clock.o" | |||
| $program = Join-Path $build "display-clock.prg" | |||
| $map = Join-Path $build "display-clock.map" | |||
| $labels = Join-Path $build "display-clock.lbl" | |||
| $debug = Join-Path $build "display-clock.dbg" | |||
| $ca65 = (Get-Command ca65 -ErrorAction Stop).Source | |||
| $ld65 = (Get-Command ld65 -ErrorAction Stop).Source | |||
| New-Item -ItemType Directory -Force -Path $build | Out-Null | |||
| foreach ($path in @($object, $program, $map, $labels, $debug)) { | |||
| if (Test-Path -LiteralPath $path) { | |||
| Remove-Item -LiteralPath $path -Force | |||
| } | |||
| } | |||
| # VDC register access would make semantic storage/display behavior ambiguous. | |||
| $sourceText = Get-Content -Raw -LiteralPath $source | |||
| $codeOnly = ($sourceText -split "`n" | ForEach-Object { | |||
| ($_ -split ';', 2)[0] | |||
| }) -join "`n" | |||
| if ($codeOnly -match '(?i)\$D60[01]') { | |||
| throw "display/clock probe must not access VDC address/data registers" | |||
| } | |||
| & $ca65 --cpu 6502 --debug-info -o $object $source | |||
| if ($LASTEXITCODE -ne 0) { throw "ca65 failed with exit code $LASTEXITCODE" } | |||
| & $ld65 -C $config -o $program -m $map -Ln $labels --dbgfile $debug $object | |||
| if ($LASTEXITCODE -ne 0) { throw "ld65 failed with exit code $LASTEXITCODE" } | |||
| $bytes = [IO.File]::ReadAllBytes($program) | |||
| if ($bytes.Length -lt 16) { throw "display-clock.prg is unexpectedly short" } | |||
| if ($bytes[0] -ne 0x01 -or $bytes[1] -ne 0x1C) { | |||
| throw "display-clock.prg load address is not `$1C01" | |||
| } | |||
| $labelText = Get-Content -Raw -LiteralPath $labels | |||
| $expectedLabels = @{ | |||
| display_clock_entry = '001C0D' | |||
| mode_40_checkpoint = '001C2E' | |||
| mode_80_checkpoint = '001C3D' | |||
| fast_checkpoint = '001C49' | |||
| fallback_done = '001C8C' | |||
| display_result = '001D0F' | |||
| mode_observed = '001D10' | |||
| fast_observed = '001D11' | |||
| final_clock = '001D12' | |||
| semantic_a = '001D13' | |||
| semantic_b = '001D14' | |||
| } | |||
| foreach ($item in $expectedLabels.GetEnumerator()) { | |||
| $pattern = "(?im)^al\s+" + $item.Value + "\s+\." + | |||
| [regex]::Escape($item.Key) + "\s*$" | |||
| if ($labelText -notmatch $pattern) { | |||
| throw ("linker label {0} did not resolve to `${1}" -f | |||
| $item.Key, $item.Value.Substring(2)) | |||
| } | |||
| } | |||
| Write-Host ("built {0} ({1} bytes)" -f $program, $bytes.Length) | |||
| Write-Host ("map {0}" -f $map) | |||
| Write-Host ("labels {0}" -f $labels) | |||
| @@ -0,0 +1,74 @@ | |||
| # E01-S03: build the C128 KERNAL wrapper probe and its test disk. | |||
| [CmdletBinding()] | |||
| param() | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| $build = Join-Path $project "build" | |||
| $source = Join-Path $project "src\platform\kernal-probe.s" | |||
| $config = Join-Path $project "cfg\kernal-probe-c128.cfg" | |||
| $object = Join-Path $build "kernal-probe.o" | |||
| $program = Join-Path $build "kernal-probe.prg" | |||
| $map = Join-Path $build "kernal-probe.map" | |||
| $labels = Join-Path $build "kernal-probe.lbl" | |||
| $debug = Join-Path $build "kernal-probe.dbg" | |||
| $listing = Join-Path $build "kernal-probe.lst" | |||
| $ca65 = (Get-Command ca65 -ErrorAction Stop).Source | |||
| $ld65 = (Get-Command ld65 -ErrorAction Stop).Source | |||
| New-Item -ItemType Directory -Force -Path $build | Out-Null | |||
| foreach ($path in @($object, $program, $map, $labels, $debug, $listing)) { | |||
| if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Force } | |||
| } | |||
| & $ca65 --cpu 6502 --debug-info -l $listing -o $object $source | |||
| if ($LASTEXITCODE -ne 0) { throw "ca65 failed with exit code $LASTEXITCODE" } | |||
| & $ld65 -C $config -o $program -m $map -Ln $labels --dbgfile $debug $object | |||
| if ($LASTEXITCODE -ne 0) { throw "ld65 failed with exit code $LASTEXITCODE" } | |||
| $bytes = [IO.File]::ReadAllBytes($program) | |||
| if ($bytes.Length -lt 16) { throw "kernal-probe.prg is unexpectedly short" } | |||
| if ($bytes[0] -ne 0x01 -or $bytes[1] -ne 0x1C) { | |||
| throw "kernal-probe.prg load address is not `$1C01" | |||
| } | |||
| $expectedHeader = [byte[]](0x0B,0x1C,0x0A,0x00,0x9E,0x37,0x31,0x38,0x31,0x00,0x00,0x00) | |||
| for ($i = 0; $i -lt $expectedHeader.Length; $i++) { | |||
| if ($bytes[$i + 2] -ne $expectedHeader[$i]) { | |||
| throw ("BASIC header differs at file offset {0}" -f ($i + 2)) | |||
| } | |||
| } | |||
| # The last loaded byte must remain below $2000, the end of the selected 8K | |||
| # bottom-common window. File bytes after the two-byte load address begin $1C01. | |||
| $lastAddress = 0x1C01 + ($bytes.Length - 2) - 1 | |||
| if ($lastAddress -ge 0x2000) { | |||
| throw ("probe ends at `${0:X4}, outside bottom common RAM" -f $lastAddress) | |||
| } | |||
| $labelText = Get-Content -Raw -LiteralPath $labels | |||
| foreach ($name in @( | |||
| 'kprobe_entry','kprobe_done','kprobe_result','kprobe_complete', | |||
| 'kprobe_state_result','kprobe_fail_a','kprobe_fail_p','kprobe_fail_st', | |||
| 'kprobe_status_buf','kprobe_code_end','kcall')) { | |||
| if ($labelText -notmatch ("(?im)^al\s+[0-9a-f]+\s+\." + [regex]::Escape($name) + "\s*$")) { | |||
| throw "missing exported linker label $name" | |||
| } | |||
| } | |||
| # The drive-8 success path needs a real disk: a fresh D64 holding only the | |||
| # probe, so autostart loads it through true drive emulation. | |||
| $c1541 = "C:\Program Files\GTK3VICE-3.10-win64\bin\c1541.exe" | |||
| if (-not (Test-Path -LiteralPath $c1541)) { throw "c1541.exe not found at $c1541" } | |||
| $disk = Join-Path $build "kernal-probe.d64" | |||
| if (Test-Path -LiteralPath $disk) { Remove-Item -LiteralPath $disk -Force } | |||
| & $c1541 -format "kprobe,kp" d64 $disk -write $program "kernal-probe" | Out-Null | |||
| if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $disk)) { | |||
| throw "c1541 could not create $disk" | |||
| } | |||
| Write-Host ("built {0} ({1} bytes, ends `${2:X4})" -f $program, $bytes.Length, $lastAddress) | |||
| Write-Host ("disk {0}" -f $disk) | |||
| Write-Host ("map {0}" -f $map) | |||
| Write-Host ("labels {0}" -f $labels) | |||
| @@ -0,0 +1,64 @@ | |||
| # E01-S01: build the minimal native-mode C128 platform proof. | |||
| [CmdletBinding()] | |||
| param() | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| $build = Join-Path $project "build" | |||
| $source = Join-Path $project "src\platform\minimal.s" | |||
| $config = Join-Path $project "cfg\minimal-c128.cfg" | |||
| $object = Join-Path $build "minimal.o" | |||
| $program = Join-Path $build "minimal.prg" | |||
| $map = Join-Path $build "minimal.map" | |||
| $labels = Join-Path $build "minimal.lbl" | |||
| $debug = Join-Path $build "minimal.dbg" | |||
| $ca65 = (Get-Command ca65 -ErrorAction Stop).Source | |||
| $ld65 = (Get-Command ld65 -ErrorAction Stop).Source | |||
| New-Item -ItemType Directory -Force -Path $build | Out-Null | |||
| foreach ($path in @($object, $program, $map, $labels, $debug)) { | |||
| if (Test-Path -LiteralPath $path) { | |||
| Remove-Item -LiteralPath $path -Force | |||
| } | |||
| } | |||
| & $ca65 --cpu 6502 --debug-info -o $object $source | |||
| if ($LASTEXITCODE -ne 0) { throw "ca65 failed with exit code $LASTEXITCODE" } | |||
| & $ld65 -C $config -o $program -m $map -Ln $labels --dbgfile $debug $object | |||
| if ($LASTEXITCODE -ne 0) { throw "ld65 failed with exit code $LASTEXITCODE" } | |||
| $bytes = [IO.File]::ReadAllBytes($program) | |||
| if ($bytes.Length -lt 16) { throw "minimal.prg is unexpectedly short" } | |||
| if ($bytes[0] -ne 0x01 -or $bytes[1] -ne 0x1C) { | |||
| throw "minimal.prg load address is not `$1C01" | |||
| } | |||
| # The first BASIC line must be SYS 7181, whose target is $1C0D. | |||
| $expectedHeader = [byte[]](0x0B,0x1C,0x0A,0x00,0x9E,0x37,0x31,0x38,0x31,0x00,0x00,0x00) | |||
| for ($i = 0; $i -lt $expectedHeader.Length; $i++) { | |||
| if ($bytes[$i + 2] -ne $expectedHeader[$i]) { | |||
| throw ("BASIC header differs at file offset {0}: expected {1:X2}, got {2:X2}" -f | |||
| ($i + 2), $expectedHeader[$i], $bytes[$i + 2]) | |||
| } | |||
| } | |||
| $labelText = Get-Content -Raw -LiteralPath $labels | |||
| $expectedLabels = @{ | |||
| minimal_entry = "001C0D" | |||
| minimal_done = "001C24" | |||
| minimal_result = "001C3E" | |||
| } | |||
| foreach ($item in $expectedLabels.GetEnumerator()) { | |||
| $pattern = "(?im)^al\s+" + $item.Value + "\s+\." + [regex]::Escape($item.Key) + "\s*$" | |||
| if ($labelText -notmatch $pattern) { | |||
| throw ("linker label {0} did not resolve to `${1}" -f $item.Key, $item.Value.Substring(2)) | |||
| } | |||
| } | |||
| Write-Host ("built {0} ({1} bytes)" -f $program, $bytes.Length) | |||
| Write-Host ("map {0}" -f $map) | |||
| Write-Host ("labels {0}" -f $labels) | |||
| @@ -0,0 +1,62 @@ | |||
| # E01-S02: build the isolated C128 MMU/common-RAM proof. | |||
| [CmdletBinding()] | |||
| param() | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| $build = Join-Path $project "build" | |||
| $source = Join-Path $project "src\platform\mmu-probe.s" | |||
| $config = Join-Path $project "cfg\mmu-probe-c128.cfg" | |||
| $object = Join-Path $build "mmu-probe.o" | |||
| $program = Join-Path $build "mmu-probe.prg" | |||
| $map = Join-Path $build "mmu-probe.map" | |||
| $labels = Join-Path $build "mmu-probe.lbl" | |||
| $debug = Join-Path $build "mmu-probe.dbg" | |||
| $listing = Join-Path $build "mmu-probe.lst" | |||
| $ca65 = (Get-Command ca65 -ErrorAction Stop).Source | |||
| $ld65 = (Get-Command ld65 -ErrorAction Stop).Source | |||
| New-Item -ItemType Directory -Force -Path $build | Out-Null | |||
| foreach ($path in @($object, $program, $map, $labels, $debug, $listing)) { | |||
| if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Force } | |||
| } | |||
| & $ca65 --cpu 6502 --debug-info -l $listing -o $object $source | |||
| if ($LASTEXITCODE -ne 0) { throw "ca65 failed with exit code $LASTEXITCODE" } | |||
| & $ld65 -C $config -o $program -m $map -Ln $labels --dbgfile $debug $object | |||
| if ($LASTEXITCODE -ne 0) { throw "ld65 failed with exit code $LASTEXITCODE" } | |||
| $bytes = [IO.File]::ReadAllBytes($program) | |||
| if ($bytes.Length -lt 16) { throw "mmu-probe.prg is unexpectedly short" } | |||
| if ($bytes[0] -ne 0x01 -or $bytes[1] -ne 0x1C) { | |||
| throw "mmu-probe.prg load address is not `$1C01" | |||
| } | |||
| $expectedHeader = [byte[]](0x0B,0x1C,0x0A,0x00,0x9E,0x37,0x31,0x38,0x31,0x00,0x00,0x00) | |||
| for ($i = 0; $i -lt $expectedHeader.Length; $i++) { | |||
| if ($bytes[$i + 2] -ne $expectedHeader[$i]) { | |||
| throw ("BASIC header differs at file offset {0}" -f ($i + 2)) | |||
| } | |||
| } | |||
| # The last loaded byte must remain below $2000, the end of the selected 8K | |||
| # bottom-common window. File bytes after the two-byte load address begin $1C01. | |||
| $lastAddress = 0x1C01 + ($bytes.Length - 2) - 1 | |||
| if ($lastAddress -ge 0x2000) { | |||
| throw ("probe ends at `${0:X4}, outside bottom common RAM" -f $lastAddress) | |||
| } | |||
| $labelText = Get-Content -Raw -LiteralPath $labels | |||
| foreach ($name in @( | |||
| 'mmu_probe_entry','mmu_probe_done','mmu_probe_result','mmu_probe_complete', | |||
| 'mmu_probe_observed_bank0','mmu_probe_observed_bank1', | |||
| 'mmu_probe_observed_p0','mmu_probe_observed_p1')) { | |||
| if ($labelText -notmatch ("(?im)^al\s+[0-9a-f]+\s+\." + [regex]::Escape($name) + "\s*$")) { | |||
| throw "missing exported linker label $name" | |||
| } | |||
| } | |||
| Write-Host ("built {0} ({1} bytes, ends `${2:X4})" -f $program, $bytes.Length, $lastAddress) | |||
| Write-Host ("map {0}" -f $map) | |||
| Write-Host ("labels {0}" -f $labels) | |||
| @@ -0,0 +1,206 @@ | |||
| # E07-S01: create a disposable Nova128 development D81 from the Vim work disk. | |||
| [CmdletBinding()] | |||
| param( | |||
| [string]$SourceImage = "", | |||
| [string]$OutputImage = "", | |||
| [string]$C1541Path = "", | |||
| [string]$CompilerPrg = "", | |||
| [string]$ProbePrg = "", | |||
| [string]$ExamplesDirectory = "", | |||
| [switch]$RequireCompiler | |||
| ) | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| $repository = Split-Path $project -Parent | |||
| $build = Join-Path $project "build" | |||
| if ($SourceImage -eq "") { | |||
| $SourceImage = Join-Path $repository "vim\work.d81" | |||
| } | |||
| if ($OutputImage -eq "") { | |||
| $OutputImage = Join-Path $build "newlang-dev.d81" | |||
| } | |||
| if ($C1541Path -eq "") { | |||
| $C1541Path = "C:\Program Files\GTK3VICE-3.10-win64\bin\c1541.exe" | |||
| } | |||
| if ($CompilerPrg -eq "") { | |||
| $CompilerPrg = Join-Path $build "n128c.prg" | |||
| } | |||
| if ($ProbePrg -eq "") { | |||
| $ProbePrg = Join-Path $build "minimal.prg" | |||
| } | |||
| if ($ExamplesDirectory -eq "") { | |||
| $ExamplesDirectory = Join-Path $project "examples" | |||
| } | |||
| $SourceImage = [IO.Path]::GetFullPath($SourceImage) | |||
| $OutputImage = [IO.Path]::GetFullPath($OutputImage) | |||
| $C1541Path = [IO.Path]::GetFullPath($C1541Path) | |||
| $CompilerPrg = [IO.Path]::GetFullPath($CompilerPrg) | |||
| $ProbePrg = [IO.Path]::GetFullPath($ProbePrg) | |||
| $ExamplesDirectory = [IO.Path]::GetFullPath($ExamplesDirectory) | |||
| if (-not (Test-Path -LiteralPath $SourceImage -PathType Leaf)) { | |||
| throw "source D81 not found: $SourceImage" | |||
| } | |||
| if (-not (Test-Path -LiteralPath $C1541Path -PathType Leaf)) { | |||
| throw "c1541 tool not found: $C1541Path" | |||
| } | |||
| if ([IO.Path]::GetExtension($OutputImage) -ine ".d81") { | |||
| throw "output image must use the .d81 extension: $OutputImage" | |||
| } | |||
| if ([string]::Equals($SourceImage, $OutputImage, | |||
| [StringComparison]::OrdinalIgnoreCase)) { | |||
| throw "output image must not be the source D81" | |||
| } | |||
| $sourceHashBefore = (Get-FileHash -LiteralPath $SourceImage -Algorithm SHA256).Hash | |||
| $outputDirectory = Split-Path $OutputImage -Parent | |||
| New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null | |||
| # Only the exact generated output is removed. The source image and its folder | |||
| # are never used as deletion targets. | |||
| if (Test-Path -LiteralPath $OutputImage) { | |||
| Remove-Item -LiteralPath $OutputImage -Force | |||
| } | |||
| Copy-Item -LiteralPath $SourceImage -Destination $OutputImage | |||
| $staging = Join-Path $build "disk-staging" | |||
| New-Item -ItemType Directory -Force -Path $staging | Out-Null | |||
| $injected = New-Object System.Collections.Generic.List[string] | |||
| function Get-DiskDirectory { | |||
| $listing = (& $C1541Path $OutputImage -list 2>&1) -join "`n" | |||
| if ($LASTEXITCODE -ne 0) { | |||
| throw "c1541 could not list the copied D81:`n$listing" | |||
| } | |||
| return $listing | |||
| } | |||
| function Add-DiskFile { | |||
| param( | |||
| [Parameter(Mandatory = $true)][string]$HostPath, | |||
| [Parameter(Mandatory = $true)][string]$DiskName, | |||
| [Parameter(Mandatory = $true)][ValidateSet("prg", "seq")][string]$Type | |||
| ) | |||
| if (-not (Test-Path -LiteralPath $HostPath -PathType Leaf)) { | |||
| throw "artifact disappeared before injection: $HostPath" | |||
| } | |||
| $destination = if ($Type -eq "seq") { "$DiskName,s" } else { $DiskName } | |||
| $writeOutput = (& $C1541Path $OutputImage -write $HostPath $destination 2>&1) -join "`n" | |||
| if ($LASTEXITCODE -ne 0) { | |||
| throw "c1541 failed to inject '$DiskName':`n$writeOutput" | |||
| } | |||
| $listing = Get-DiskDirectory | |||
| $pattern = '(?im)"' + [regex]::Escape($DiskName) + '"\s+' + $Type + '\s*$' | |||
| if ($listing -notmatch $pattern) { | |||
| throw "c1541 reported success but '$DiskName' ($Type) is absent from the D81" | |||
| } | |||
| $injected.Add("$DiskName ($Type)") | |||
| } | |||
| function ConvertTo-PetsciiSeq { | |||
| param( | |||
| [Parameter(Mandatory = $true)][string]$InputPath, | |||
| [Parameter(Mandatory = $true)][string]$OutputPath | |||
| ) | |||
| $bytes = [IO.File]::ReadAllBytes($InputPath) | |||
| $start = 0 | |||
| if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and | |||
| $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { | |||
| $start = 3 | |||
| } | |||
| $converted = New-Object System.Collections.Generic.List[byte] | |||
| for ($i = $start; $i -lt $bytes.Length; $i++) { | |||
| $value = $bytes[$i] | |||
| if ($value -eq 13) { | |||
| if ($i + 1 -lt $bytes.Length -and $bytes[$i + 1] -eq 10) { | |||
| continue | |||
| } | |||
| $converted.Add([byte]13) | |||
| continue | |||
| } | |||
| if ($value -eq 10) { | |||
| $converted.Add([byte]13) | |||
| continue | |||
| } | |||
| if ($value -gt 0x7F) { | |||
| throw "example source is not ASCII/UTF-8 text: $InputPath at byte $i" | |||
| } | |||
| if ($value -ge 0x61 -and $value -le 0x7A) { | |||
| $converted.Add([byte]($value - 0x20)) | |||
| continue | |||
| } | |||
| if ($value -ge 0x41 -and $value -le 0x5A) { | |||
| $converted.Add([byte]($value + 0x80)) | |||
| continue | |||
| } | |||
| $converted.Add([byte]$value) | |||
| } | |||
| [IO.File]::WriteAllBytes($OutputPath, $converted.ToArray()) | |||
| } | |||
| if (Test-Path -LiteralPath $CompilerPrg -PathType Leaf) { | |||
| Add-DiskFile -HostPath $CompilerPrg -DiskName "n128c" -Type prg | |||
| } elseif ($RequireCompiler) { | |||
| throw "required Nova128 compiler PRG not found: $CompilerPrg" | |||
| } else { | |||
| Write-Warning "Nova128 compiler is not built yet; skipped $CompilerPrg" | |||
| } | |||
| if (Test-Path -LiteralPath $ProbePrg -PathType Leaf) { | |||
| Add-DiskFile -HostPath $ProbePrg -DiskName "n128probe" -Type prg | |||
| } else { | |||
| Write-Warning "optional native C128 probe is absent; skipped $ProbePrg" | |||
| } | |||
| if (Test-Path -LiteralPath $ExamplesDirectory -PathType Container) { | |||
| $examples = @(Get-ChildItem -LiteralPath $ExamplesDirectory -Filter "*.n8" -File | | |||
| Sort-Object Name) | |||
| foreach ($example in $examples) { | |||
| if ($example.BaseName.Length -gt 12) { | |||
| throw "example base name exceeds the 12-character source profile: $($example.Name)" | |||
| } | |||
| $diskName = $example.BaseName.ToLowerInvariant() + ".n8" | |||
| $petscii = Join-Path $staging ($example.BaseName + ".pet") | |||
| ConvertTo-PetsciiSeq -InputPath $example.FullName -OutputPath $petscii | |||
| Add-DiskFile -HostPath $petscii -DiskName $diskName -Type seq | |||
| } | |||
| if ($examples.Count -eq 0) { | |||
| Write-Warning "examples directory contains no .n8 files: $ExamplesDirectory" | |||
| } | |||
| } else { | |||
| Write-Warning "examples directory does not exist yet; skipped $ExamplesDirectory" | |||
| } | |||
| $sourceHashAfter = (Get-FileHash -LiteralPath $SourceImage -Algorithm SHA256).Hash | |||
| if ($sourceHashAfter -ne $sourceHashBefore) { | |||
| throw "source D81 changed while preparing the disposable image" | |||
| } | |||
| # A final listing proves the resulting image is readable after all writes. | |||
| $null = Get-DiskDirectory | |||
| $outputHash = (Get-FileHash -LiteralPath $OutputImage -Algorithm SHA256).Hash | |||
| Write-Host "prepared disposable D81: $OutputImage" | |||
| Write-Host "source SHA-256 unchanged: $sourceHashAfter" | |||
| if ($injected.Count -eq 0) { | |||
| Write-Host "injected: no optional Nova128 artifacts are built yet" | |||
| } else { | |||
| Write-Host ("injected: " + ($injected -join ", ")) | |||
| } | |||
| [pscustomobject]@{ | |||
| SourceImage = $SourceImage | |||
| OutputImage = $OutputImage | |||
| SourceHash = $sourceHashAfter | |||
| OutputHash = $outputHash | |||
| Injected = @($injected) | |||
| } | |||
| @@ -0,0 +1,40 @@ | |||
| # E01-S01: build and launch the platform proof in VICE x128. | |||
| [CmdletBinding()] | |||
| param( | |||
| [int]$LimitCycles = 30000000 | |||
| ) | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| $build = Join-Path $project "build" | |||
| $program = Join-Path $build "minimal.prg" | |||
| $screenshot = Join-Path $build "minimal-vicii.png" | |||
| $x128 = "C:\Program Files\GTK3VICE-3.10-win64\bin\x128.exe" | |||
| & (Join-Path $PSScriptRoot "build-minimal.ps1") | |||
| if ($LASTEXITCODE -ne 0) { throw "minimal build failed" } | |||
| if (-not (Test-Path -LiteralPath $x128)) { throw "x128.exe not found at $x128" } | |||
| if (Test-Path -LiteralPath $screenshot) { | |||
| Remove-Item -LiteralPath $screenshot -Force | |||
| } | |||
| $arguments = @( | |||
| "-autostart", $program, | |||
| "-autostart-warp", "-warp", | |||
| "-sounddev", "dummy", | |||
| "-limitcycles", "$LimitCycles", | |||
| "-exitscreenshotvicii", $screenshot | |||
| ) | |||
| $process = Start-Process -FilePath $x128 -ArgumentList $arguments -PassThru -Wait -WindowStyle Hidden | |||
| # VICE uses a non-zero exit status when -limitcycles ends the session. The | |||
| # screenshot and deterministic program state are the evidence, not that status. | |||
| if (-not (Test-Path -LiteralPath $screenshot)) { | |||
| throw "x128 did not create $screenshot" | |||
| } | |||
| Write-Host ("x128 exit code: {0} (cycle-limit exit may be non-zero)" -f $process.ExitCode) | |||
| Write-Host ("screenshot: {0}" -f $screenshot) | |||
| @@ -0,0 +1,27 @@ | |||
| # E01-S02: build and visibly run the MMU probe in VICE x128. | |||
| [CmdletBinding()] | |||
| param([int]$LimitCycles = 30000000) | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| $build = Join-Path $project "build" | |||
| $program = Join-Path $build "mmu-probe.prg" | |||
| $screenshot = Join-Path $build "mmu-probe-vicii.png" | |||
| $x128 = "C:\Program Files\GTK3VICE-3.10-win64\bin\x128.exe" | |||
| & (Join-Path $PSScriptRoot "build-mmu-probe.ps1") | |||
| if ($LASTEXITCODE -ne 0) { throw "MMU probe build failed" } | |||
| if (-not (Test-Path -LiteralPath $x128)) { throw "x128.exe not found at $x128" } | |||
| if (Test-Path -LiteralPath $screenshot) { Remove-Item -LiteralPath $screenshot -Force } | |||
| $arguments = @( | |||
| "-autostart", $program, | |||
| "-autostart-warp", "-warp", "-sounddev", "dummy", | |||
| "-limitcycles", "$LimitCycles", "-exitscreenshotvicii", $screenshot | |||
| ) | |||
| $process = Start-Process -FilePath $x128 -ArgumentList $arguments -PassThru -Wait -WindowStyle Hidden | |||
| if (-not (Test-Path -LiteralPath $screenshot)) { throw "x128 did not create $screenshot" } | |||
| Write-Host ("x128 exit code: {0} (cycle-limit exit may be non-zero)" -f $process.ExitCode) | |||
| Write-Host ("screenshot: {0}" -f $screenshot) | |||
| @@ -0,0 +1,174 @@ | |||
| # E01-S04: build and verify display/clock behavior in both x128 modes. | |||
| [CmdletBinding()] | |||
| param( | |||
| [int]$RemotePort = 6510, | |||
| [int]$LimitCycles = 30000000 | |||
| ) | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| $build = Join-Path $project "build" | |||
| $program = Join-Path $build "display-clock.prg" | |||
| $traceCommands = Join-Path $project "monitor\display-clock-trace.txt" | |||
| $x128 = "C:\Program Files\GTK3VICE-3.10-win64\bin\x128.exe" | |||
| & (Join-Path $PSScriptRoot "build-display-clock.ps1") | |||
| if ($LASTEXITCODE -ne 0) { throw "display/clock build failed" } | |||
| if (-not (Test-Path -LiteralPath $x128)) { throw "x128.exe not found at $x128" } | |||
| function Quote-Arg([string]$value) { | |||
| if ($value -ne "" -and $value -notmatch '[\s"]') { return $value } | |||
| $escaped = $value -replace '(\\*)"', '$1$1\"' | |||
| $escaped = $escaped -replace '(\\+)$', '$1$1' | |||
| return '"' + $escaped + '"' | |||
| } | |||
| function Start-X128([string[]]$arguments) { | |||
| $psi = [Diagnostics.ProcessStartInfo]::new() | |||
| $psi.FileName = $x128 | |||
| $psi.Arguments = ($arguments | ForEach-Object { Quote-Arg $_ }) -join ' ' | |||
| $psi.UseShellExecute = $true | |||
| $psi.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden | |||
| return [Diagnostics.Process]::Start($psi) | |||
| } | |||
| function Invoke-Screenshot([string]$mode) { | |||
| $screenshot = Join-Path $build ("display-clock-{0}.png" -f $mode) | |||
| if (Test-Path -LiteralPath $screenshot) { | |||
| Remove-Item -LiteralPath $screenshot -Force | |||
| } | |||
| $captureOption = if ($mode -eq '40') { '-exitscreenshotvicii' } else { '-exitscreenshot' } | |||
| $process = Start-X128 @( | |||
| ("-{0}col" -f $mode), | |||
| '-autostart', $program, | |||
| '-autostart-warp', '-warp', | |||
| '-sounddev', 'dummy', | |||
| '-limitcycles', "$LimitCycles", | |||
| $captureOption, $screenshot | |||
| ) | |||
| $process.WaitForExit() | |||
| if (-not (Test-Path -LiteralPath $screenshot)) { | |||
| throw "x128 did not create $screenshot" | |||
| } | |||
| if ((Get-Item -LiteralPath $screenshot).Length -lt 1000) { | |||
| throw "$screenshot is unexpectedly small" | |||
| } | |||
| Write-Host ("{0}-column screenshot: {1}" -f $mode, $screenshot) | |||
| } | |||
| function Invoke-MonitorVerification( | |||
| [string]$mode, | |||
| [byte]$expectedResult, | |||
| [byte]$expectedMode, | |||
| [byte]$expectedFast | |||
| ) { | |||
| $transcript = Join-Path $build ("display-clock-{0}-remote.txt" -f $mode) | |||
| foreach ($path in @($transcript)) { | |||
| if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Force } | |||
| } | |||
| $process = Start-X128 @( | |||
| ("-{0}col" -f $mode), | |||
| '-autostart', $program, | |||
| '-autostart-warp', '-warp', | |||
| '-sounddev', 'dummy', | |||
| '-moncommands', $traceCommands, | |||
| '-remotemonitor', | |||
| '-remotemonitoraddress', ("ip4://127.0.0.1:{0}" -f $RemotePort) | |||
| ) | |||
| $client = $null | |||
| try { | |||
| $deadline = [DateTime]::UtcNow.AddSeconds(10) | |||
| while ([DateTime]::UtcNow -lt $deadline) { | |||
| if ($process.HasExited) { throw "x128 exited before $mode-column verification" } | |||
| try { | |||
| $client = [Net.Sockets.TcpClient]::new() | |||
| $client.Connect('127.0.0.1', $RemotePort) | |||
| break | |||
| } catch { | |||
| if ($client) { $client.Dispose(); $client = $null } | |||
| Start-Sleep -Milliseconds 200 | |||
| } | |||
| } | |||
| if (-not $client -or -not $client.Connected) { | |||
| throw "could not connect to the VICE remote monitor" | |||
| } | |||
| Start-Sleep -Seconds 3 | |||
| $stream = $client.GetStream() | |||
| $stream.ReadTimeout = 2000 | |||
| $writer = [IO.StreamWriter]::new($stream, [Text.Encoding]::ASCII, 1024, $true) | |||
| $writer.NewLine = "`n" | |||
| $writer.AutoFlush = $true | |||
| $writer.WriteLine('m $1d0f $1d14') | |||
| Start-Sleep -Milliseconds 300 | |||
| $writer.WriteLine('m $d030 $d030') | |||
| Start-Sleep -Milliseconds 700 | |||
| $buffer = New-Object byte[] 8192 | |||
| $response = "" | |||
| while ($stream.DataAvailable) { | |||
| $count = $stream.Read($buffer, 0, $buffer.Length) | |||
| if ($count -le 0) { break } | |||
| $response += [Text.Encoding]::ASCII.GetString($buffer, 0, $count) | |||
| Start-Sleep -Milliseconds 50 | |||
| } | |||
| [IO.File]::WriteAllText($transcript, $response, [Text.Encoding]::ASCII) | |||
| $expectedBytes = '{0:x2}\s+{1:x2}\s+{2:x2}\s+00\s+5a\s+c3' -f | |||
| $expectedResult, $expectedMode, $expectedFast | |||
| if ($response -notmatch ("(?i)C:1d0f\s+" + $expectedBytes)) { | |||
| throw "$mode-column result block mismatch; response: $response" | |||
| } | |||
| $clockMatch = [regex]::Match($response, '(?i)C:d030\s+([0-9a-f]{2})\b') | |||
| if (-not $clockMatch.Success) { | |||
| throw "$mode-column response did not include `$D030" | |||
| } | |||
| $clockValue = [Convert]::ToByte($clockMatch.Groups[1].Value, 16) | |||
| if (($clockValue -band 0x01) -ne 0) { | |||
| throw "$mode-column fallback left `$D030 speed bit set" | |||
| } | |||
| $writer.WriteLine('q') | |||
| $writer.Dispose() | |||
| $client.Dispose() | |||
| $client = $null | |||
| if (-not $process.WaitForExit(5000)) { | |||
| throw "x128 did not exit after remote-monitor quit" | |||
| } | |||
| # With a remote monitor connected, non-stopping trace messages are | |||
| # delivered in the remote transcript. | |||
| $trace = $response | |||
| if ($trace -notmatch '(?i)1c8c') { | |||
| throw "$mode-column trace did not reach fallback_done" | |||
| } | |||
| if ($mode -eq '40') { | |||
| if ($trace -notmatch '(?i)1c2e') { throw '40-column trace missed its path' } | |||
| if ($trace -match '(?i)1c49') { throw '40-column path entered fast checkpoint' } | |||
| } else { | |||
| if ($trace -notmatch '(?i)1c3d') { throw '80-column trace missed its path' } | |||
| if ($trace -notmatch '(?i)1c49') { throw '80-column trace missed fast checkpoint' } | |||
| } | |||
| Write-Host ("verified {0}-column result `${1:X2}, mode `${2:X2}, fast `${3:X2}, final clock `$00" -f | |||
| $mode, $expectedResult, $expectedMode, $expectedFast) | |||
| } finally { | |||
| if ($client) { $client.Dispose() } | |||
| if ($process -and -not $process.HasExited) { | |||
| $process.Kill() | |||
| $process.WaitForExit() | |||
| } | |||
| } | |||
| } | |||
| Invoke-Screenshot '40' | |||
| Invoke-Screenshot '80' | |||
| Invoke-MonitorVerification '40' 0x44 0x00 0x00 | |||
| Invoke-MonitorVerification '80' 0x85 0x80 0x01 | |||
| Write-Host 'display/clock probe passed in 40- and 80-column modes' | |||
| @@ -0,0 +1,189 @@ | |||
| # E01-S03: run the KERNAL wrapper probe from a true-drive D64 in VICE x128 and | |||
| # verify its machine-readable result block over the remote monitor. | |||
| [CmdletBinding()] | |||
| param( | |||
| [int]$RemotePort = 6512, | |||
| [int]$TimeoutSeconds = 60 | |||
| ) | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| $build = Join-Path $project "build" | |||
| $disk = Join-Path $build "kernal-probe.d64" | |||
| $labelsPath = Join-Path $build "kernal-probe.lbl" | |||
| $traceCommands = Join-Path $project "monitor\kernal-probe-trace.txt" | |||
| $monitorLog = Join-Path $build "kernal-probe-monitor.log" | |||
| $screenshot = Join-Path $build "kernal-probe-vicii.png" | |||
| $x128 = "C:\Program Files\GTK3VICE-3.10-win64\bin\x128.exe" | |||
| & (Join-Path $PSScriptRoot "build-kernal-probe.ps1") | |||
| if ($LASTEXITCODE -ne 0) { throw "KERNAL probe build failed" } | |||
| if (-not (Test-Path -LiteralPath $x128)) { throw "x128.exe not found at $x128" } | |||
| foreach ($path in @($monitorLog, $screenshot)) { | |||
| if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Force } | |||
| } | |||
| $labelText = Get-Content -Raw -LiteralPath $labelsPath | |||
| function Get-LabelAddress([string]$name) { | |||
| $match = [regex]::Match($labelText, | |||
| "(?im)^al\s+([0-9a-f]+)\s+\." + [regex]::Escape($name) + "\s*$") | |||
| if (-not $match.Success) { throw "missing label $name" } | |||
| return [Convert]::ToInt32($match.Groups[1].Value, 16) | |||
| } | |||
| $resultAddress = Get-LabelAddress "kprobe_result" | |||
| $completeAddress = Get-LabelAddress "kprobe_complete" | |||
| $stateAddress = Get-LabelAddress "kprobe_state_result" | |||
| $failAAddress = Get-LabelAddress "kprobe_fail_a" | |||
| $failPAddress = Get-LabelAddress "kprobe_fail_p" | |||
| $failStAddress = Get-LabelAddress "kprobe_fail_st" | |||
| $statusAddress = Get-LabelAddress "kprobe_status_buf" | |||
| $statusMax = 32 | |||
| $blockEnd = $statusAddress + 2 * $statusMax - 1 | |||
| $arguments = @( | |||
| "-drive8type", "1541", "-drive8truedrive", "-8", ('"' + $disk + '"'), | |||
| "-autostart", ('"' + $disk + '"'), | |||
| "-autostart-warp", "-warp", "-sounddev", "dummy", | |||
| "-exitscreenshotvicii", ('"' + $screenshot + '"'), | |||
| "-moncommands", ('"' + $traceCommands + '"'), | |||
| "-monlog", "-monlogname", ('"' + $monitorLog + '"'), | |||
| "-remotemonitor", "-remotemonitoraddress", ("ip4://127.0.0.1:{0}" -f $RemotePort) | |||
| ) -join ' ' | |||
| $psi = [Diagnostics.ProcessStartInfo]::new() | |||
| $psi.FileName = $x128 | |||
| $psi.Arguments = $arguments | |||
| $psi.UseShellExecute = $true | |||
| $psi.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden | |||
| $process = [Diagnostics.Process]::Start($psi) | |||
| $client = $null | |||
| function Read-Available($stream) { | |||
| $buffer = New-Object byte[] 16384 | |||
| $text = "" | |||
| Start-Sleep -Milliseconds 300 | |||
| while ($stream.DataAvailable) { | |||
| $count = $stream.Read($buffer, 0, $buffer.Length) | |||
| if ($count -le 0) { break } | |||
| $text += [Text.Encoding]::ASCII.GetString($buffer, 0, $count) | |||
| Start-Sleep -Milliseconds 50 | |||
| } | |||
| return $text | |||
| } | |||
| function Convert-Dump([string]$text) { | |||
| $memory = @{} | |||
| $rows = [regex]::Matches($text, "(?i)C:([0-9a-f]{4})((?:[ ]{1,2}[0-9a-f]{2}){1,16})") | |||
| foreach ($row in $rows) { | |||
| $address = [Convert]::ToInt32($row.Groups[1].Value, 16) | |||
| foreach ($hex in ($row.Groups[2].Value.Trim() -split '\s+')) { | |||
| $memory[$address] = [Convert]::ToInt32($hex, 16) | |||
| $address++ | |||
| } | |||
| } | |||
| return $memory | |||
| } | |||
| try { | |||
| $deadline = [DateTime]::UtcNow.AddSeconds(10) | |||
| while ([DateTime]::UtcNow -lt $deadline) { | |||
| if ($process.HasExited) { throw "x128 exited before remote verification" } | |||
| try { | |||
| $client = [Net.Sockets.TcpClient]::new() | |||
| $client.Connect("127.0.0.1", $RemotePort) | |||
| break | |||
| } catch { | |||
| if ($client) { $client.Dispose(); $client = $null } | |||
| Start-Sleep -Milliseconds 200 | |||
| } | |||
| } | |||
| if (-not $client -or -not $client.Connected) { | |||
| throw "could not connect to VICE remote monitor" | |||
| } | |||
| $stream = $client.GetStream() | |||
| $writer = [IO.StreamWriter]::new($stream, [Text.Encoding]::ASCII, 1024, $true) | |||
| $writer.NewLine = "`n" | |||
| $writer.AutoFlush = $true | |||
| # True-drive loading takes several emulated seconds; poll completion. | |||
| $transcript = "" | |||
| $memory = @{} | |||
| $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) | |||
| while ($true) { | |||
| Start-Sleep -Seconds 1 | |||
| $writer.WriteLine(("m `${0:X4} `${1:X4}" -f $resultAddress, $blockEnd)) | |||
| $chunk = Read-Available $stream | |||
| $transcript += $chunk | |||
| $memory = Convert-Dump $chunk | |||
| if ($memory[$completeAddress] -eq 0xA5) { break } | |||
| if ([DateTime]::UtcNow -ge $deadline) { | |||
| throw "probe did not complete within $TimeoutSeconds s; last response: $chunk" | |||
| } | |||
| $writer.WriteLine("x") | |||
| $transcript += Read-Available $stream | |||
| } | |||
| # Let the post-done summary finish printing; x128 writes the VIC-II | |||
| # screenshot when the monitor quits it below. | |||
| $writer.WriteLine("x") | |||
| Start-Sleep -Seconds 2 | |||
| $writer.WriteLine("m 0000 0000") | |||
| $transcript += Read-Available $stream | |||
| [IO.File]::WriteAllText((Join-Path $build "kernal-probe-remote.txt"), $transcript) | |||
| $failures = @() | |||
| function Expect([string]$name, [int]$address, [int]$value) { | |||
| if ($memory[$address] -ne $value) { | |||
| $script:failures += ("{0} at `${1:X4} = `${2:X2}, expected `${3:X2}" -f | |||
| $name, $address, $memory[$address], $value) | |||
| } | |||
| } | |||
| Expect "result" $resultAddress 0xFF | |||
| Expect "complete" $completeAddress 0xA5 | |||
| foreach ($state in 0, 1) { | |||
| Expect "state$state result" ($stateAddress + $state) 0xFF | |||
| Expect "state$state CHKIN error" ($failAAddress + $state) 0x05 | |||
| if (($memory[$failPAddress + $state] -band 0x01) -ne 0x01) { | |||
| $failures += "state$state CHKIN carry was clear" | |||
| } | |||
| if (($memory[$failStAddress + $state] -band 0x80) -ne 0x80) { | |||
| $failures += ("state{0} READST `${1:X2} lacks device-not-present bit 7" -f | |||
| $state, $memory[$failStAddress + $state]) | |||
| } | |||
| } | |||
| $statusText = @() | |||
| foreach ($state in 0, 1) { | |||
| $chars = @() | |||
| for ($i = 0; $i -lt $statusMax; $i++) { | |||
| $byte = $memory[$statusAddress + $state * $statusMax + $i] | |||
| if ($byte -eq 0x0D) { break } | |||
| $chars += [char]$byte | |||
| } | |||
| $statusText += (-join $chars) | |||
| } | |||
| $writer.WriteLine("quit") | |||
| $writer.Dispose() | |||
| $client.Dispose() | |||
| $client = $null | |||
| if (-not $process.WaitForExit(10000)) { throw "x128 did not exit after monitor quit" } | |||
| if ($failures.Count -gt 0) { | |||
| throw ("KERNAL probe verification failed:`n " + ($failures -join "`n ")) | |||
| } | |||
| Write-Host ("verified state results `$FF/`$FF, CHKIN(30) error 5 with C=1, " + | |||
| "READST `${0:X2}/`${1:X2}" -f $memory[$failStAddress], $memory[$failStAddress + 1]) | |||
| Write-Host ("drive 8 status from bank 0: '{0}'" -f $statusText[0]) | |||
| Write-Host ("drive 8 status from bank 1: '{0}'" -f $statusText[1]) | |||
| if (Test-Path -LiteralPath $screenshot) { Write-Host ("screenshot: {0}" -f $screenshot) } | |||
| } finally { | |||
| if ($client) { $client.Dispose() } | |||
| if ($process -and -not $process.HasExited) { | |||
| $process.Kill() | |||
| $process.WaitForExit() | |||
| } | |||
| } | |||
| @@ -0,0 +1,111 @@ | |||
| # E01-S01: execute the platform proof and verify entry, completion, and result. | |||
| [CmdletBinding()] | |||
| param( | |||
| [int]$RemotePort = 6510 | |||
| ) | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| $build = Join-Path $project "build" | |||
| $program = Join-Path $build "minimal.prg" | |||
| $traceCommands = Join-Path $project "monitor\minimal-trace.txt" | |||
| $monitorLog = Join-Path $build "minimal-monitor.log" | |||
| $x128 = "C:\Program Files\GTK3VICE-3.10-win64\bin\x128.exe" | |||
| & (Join-Path $PSScriptRoot "build-minimal.ps1") | |||
| if ($LASTEXITCODE -ne 0) { throw "minimal build failed" } | |||
| if (-not (Test-Path -LiteralPath $x128)) { throw "x128.exe not found at $x128" } | |||
| if (Test-Path -LiteralPath $monitorLog) { | |||
| Remove-Item -LiteralPath $monitorLog -Force | |||
| } | |||
| $arguments = @( | |||
| "-autostart", ('"' + $program + '"'), | |||
| "-autostart-warp", "-warp", | |||
| "-sounddev", "dummy", | |||
| "-moncommands", ('"' + $traceCommands + '"'), | |||
| "-monlog", "-monlogname", ('"' + $monitorLog + '"'), | |||
| "-remotemonitor", | |||
| "-remotemonitoraddress", ("ip4://127.0.0.1:{0}" -f $RemotePort) | |||
| ) -join ' ' | |||
| $psi = [Diagnostics.ProcessStartInfo]::new() | |||
| $psi.FileName = $x128 | |||
| $psi.Arguments = $arguments | |||
| $psi.UseShellExecute = $true | |||
| $psi.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden | |||
| $process = [Diagnostics.Process]::Start($psi) | |||
| $client = $null | |||
| try { | |||
| $deadline = [DateTime]::UtcNow.AddSeconds(10) | |||
| while ([DateTime]::UtcNow -lt $deadline) { | |||
| if ($process.HasExited) { throw "x128 exited before remote verification" } | |||
| try { | |||
| $client = [Net.Sockets.TcpClient]::new() | |||
| $client.Connect("127.0.0.1", $RemotePort) | |||
| break | |||
| } catch { | |||
| if ($client) { $client.Dispose(); $client = $null } | |||
| Start-Sleep -Milliseconds 200 | |||
| } | |||
| } | |||
| if (-not $client -or -not $client.Connected) { | |||
| throw "could not connect to the VICE remote monitor" | |||
| } | |||
| # Autostart runs in warp. Give BASIC enough wall time to load, RUN, execute | |||
| # the message loop, and return before inspecting the result byte. | |||
| Start-Sleep -Seconds 3 | |||
| $stream = $client.GetStream() | |||
| $stream.ReadTimeout = 2000 | |||
| $writer = [IO.StreamWriter]::new($stream, [Text.Encoding]::ASCII, 1024, $true) | |||
| $writer.NewLine = "`n" | |||
| $writer.AutoFlush = $true | |||
| $writer.WriteLine('m $1c3e $1c3e') | |||
| Start-Sleep -Milliseconds 300 | |||
| $buffer = New-Object byte[] 8192 | |||
| $response = "" | |||
| while ($stream.DataAvailable) { | |||
| $count = $stream.Read($buffer, 0, $buffer.Length) | |||
| if ($count -le 0) { break } | |||
| $response += [Text.Encoding]::ASCII.GetString($buffer, 0, $count) | |||
| Start-Sleep -Milliseconds 50 | |||
| } | |||
| if ($response -notmatch '(?i)C:1c3e\s+a5\b') { | |||
| throw "minimal_result was not `$A5; monitor response: $response" | |||
| } | |||
| $writer.WriteLine("q") | |||
| $writer.Dispose() | |||
| $client.Dispose() | |||
| $client = $null | |||
| if (-not $process.WaitForExit(5000)) { | |||
| throw "x128 did not exit after remote-monitor quit" | |||
| } | |||
| if (-not (Test-Path -LiteralPath $monitorLog)) { | |||
| throw "VICE did not create the monitor trace log" | |||
| } | |||
| $trace = Get-Content -Raw -LiteralPath $monitorLog | |||
| if ($trace -notmatch '(?i)1c0d') { | |||
| throw "monitor trace did not record minimal_entry at `$1C0D" | |||
| } | |||
| if ($trace -notmatch '(?i)1c24') { | |||
| throw "monitor trace did not record minimal_done at `$1C24" | |||
| } | |||
| Write-Host "verified x128 entry `$1C0D, done `$1C24, result `$A5" | |||
| Write-Host ("monitor log: {0}" -f $monitorLog) | |||
| } finally { | |||
| if ($client) { $client.Dispose() } | |||
| if ($process -and -not $process.HasExited) { | |||
| $process.Kill() | |||
| $process.WaitForExit() | |||
| } | |||
| } | |||
| @@ -0,0 +1,188 @@ | |||
| # E01-S02: execute the MMU probe and verify its machine-readable result block. | |||
| [CmdletBinding()] | |||
| param([int]$RemotePort = 6511) | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| $build = Join-Path $project "build" | |||
| $program = Join-Path $build "mmu-probe.prg" | |||
| $labelsPath = Join-Path $build "mmu-probe.lbl" | |||
| $traceCommands = Join-Path $project "monitor\mmu-probe-trace.txt" | |||
| $monitorLog = Join-Path $build "mmu-probe-monitor.log" | |||
| $x128 = "C:\Program Files\GTK3VICE-3.10-win64\bin\x128.exe" | |||
| & (Join-Path $PSScriptRoot "build-mmu-probe.ps1") | |||
| if ($LASTEXITCODE -ne 0) { throw "MMU probe build failed" } | |||
| if (Test-Path -LiteralPath $monitorLog) { Remove-Item -LiteralPath $monitorLog -Force } | |||
| $labelText = Get-Content -Raw -LiteralPath $labelsPath | |||
| function Get-LabelAddress([string]$name) { | |||
| $match = [regex]::Match($labelText, | |||
| "(?im)^al\s+([0-9a-f]+)\s+\." + [regex]::Escape($name) + "\s*$") | |||
| if (-not $match.Success) { throw "missing label $name" } | |||
| return [Convert]::ToInt32($match.Groups[1].Value, 16) | |||
| } | |||
| $resultAddress = Get-LabelAddress "mmu_probe_result" | |||
| $completeAddress = Get-LabelAddress "mmu_probe_complete" | |||
| $bank0Address = Get-LabelAddress "mmu_probe_observed_bank0" | |||
| $bank1Address = Get-LabelAddress "mmu_probe_observed_bank1" | |||
| $p0Address = Get-LabelAddress "mmu_probe_observed_p0" | |||
| $p1Address = Get-LabelAddress "mmu_probe_observed_p1" | |||
| $p0cBank0Address = Get-LabelAddress "mmu_probe_obs_p0c_b0" | |||
| $p1cBank0Address = Get-LabelAddress "mmu_probe_obs_p1c_b0" | |||
| $stateEndAddress = Get-LabelAddress "mmu_probe_common_rcr" | |||
| $entryAddress = Get-LabelAddress "mmu_probe_entry" | |||
| $doneAddress = Get-LabelAddress "mmu_probe_done" | |||
| $codeEndAddress = Get-LabelAddress "mmu_probe_code_end" | |||
| # The checked-in trace file has the fixed entry point; the done address moves | |||
| # with the code, so append it from the fresh label file. | |||
| $generatedTrace = Join-Path $build "mmu-probe-trace.gen.txt" | |||
| $traceText = (Get-Content -Raw -LiteralPath $traceCommands) + | |||
| ("trace exec `${0:X4}`ncommand 4 `"disable 2`"`n" -f $doneAddress) | |||
| [IO.File]::WriteAllText($generatedTrace, $traceText, [Text.Encoding]::ASCII) | |||
| $traceCommands = $generatedTrace | |||
| $arguments = @( | |||
| "-autostart", ('"' + $program + '"'), | |||
| "-autostart-warp", "-warp", "-sounddev", "dummy", | |||
| "-moncommands", ('"' + $traceCommands + '"'), | |||
| "-monlog", "-monlogname", ('"' + $monitorLog + '"'), | |||
| "-remotemonitor", "-remotemonitoraddress", ("ip4://127.0.0.1:{0}" -f $RemotePort) | |||
| ) -join ' ' | |||
| $psi = [Diagnostics.ProcessStartInfo]::new() | |||
| $psi.FileName = $x128 | |||
| $psi.Arguments = $arguments | |||
| $psi.UseShellExecute = $true | |||
| $psi.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden | |||
| $process = [Diagnostics.Process]::Start($psi) | |||
| $client = $null | |||
| try { | |||
| $deadline = [DateTime]::UtcNow.AddSeconds(10) | |||
| while ([DateTime]::UtcNow -lt $deadline) { | |||
| if ($process.HasExited) { throw "x128 exited before remote verification" } | |||
| try { | |||
| $client = [Net.Sockets.TcpClient]::new() | |||
| $client.Connect("127.0.0.1", $RemotePort) | |||
| break | |||
| } catch { | |||
| if ($client) { $client.Dispose(); $client = $null } | |||
| Start-Sleep -Milliseconds 200 | |||
| } | |||
| } | |||
| if (-not $client -or -not $client.Connected) { | |||
| throw "could not connect to VICE remote monitor" | |||
| } | |||
| Start-Sleep -Seconds 3 | |||
| $stream = $client.GetStream() | |||
| $stream.ReadTimeout = 2000 | |||
| $writer = [IO.StreamWriter]::new($stream, [Text.Encoding]::ASCII, 1024, $true) | |||
| $writer.NewLine = "`n" | |||
| $writer.AutoFlush = $true | |||
| $writer.WriteLine(("m `${0:X4} `${1:X4}" -f $resultAddress, $stateEndAddress)) | |||
| Start-Sleep -Milliseconds 400 | |||
| $buffer = New-Object byte[] 16384 | |||
| $response = "" | |||
| while ($stream.DataAvailable) { | |||
| $count = $stream.Read($buffer, 0, $buffer.Length) | |||
| if ($count -le 0) { break } | |||
| $response += [Text.Encoding]::ASCII.GetString($buffer, 0, $count) | |||
| Start-Sleep -Milliseconds 50 | |||
| } | |||
| $expected = @( | |||
| @{ Address = $resultAddress; Value = 0xFF; Name = "result" }, | |||
| @{ Address = $completeAddress; Value = 0xA5; Name = "complete" }, | |||
| @{ Address = $bank0Address; Value = 0x11; Name = "bank0" }, | |||
| @{ Address = $bank1Address; Value = 0x22; Name = "bank1" }, | |||
| @{ Address = $p0Address; Value = 0x77; Name = "page0" }, | |||
| @{ Address = $p1Address; Value = 0x33; Name = "page1" }, | |||
| @{ Address = $p0cBank0Address; Value = 0x66; Name = "page0-common-bank0" }, | |||
| @{ Address = $p1cBank0Address; Value = 0x5A; Name = "page1-common-bank0" } | |||
| ) | |||
| # Parse every monitor dump row into address -> byte so checks do not | |||
| # depend on where a 16-byte row happens to start. | |||
| $memory = @{} | |||
| $rows = [regex]::Matches($response, | |||
| "(?i)C:([0-9a-f]{4})((?:[ ]{1,2}[0-9a-f]{2}){1,16})") | |||
| foreach ($row in $rows) { | |||
| $address = [Convert]::ToInt32($row.Groups[1].Value, 16) | |||
| foreach ($hex in ($row.Groups[2].Value.Trim() -split '\s+')) { | |||
| $memory[$address] = [Convert]::ToInt32($hex, 16) | |||
| $address++ | |||
| } | |||
| } | |||
| foreach ($item in $expected) { | |||
| if (-not $memory.ContainsKey($item.Address) -or | |||
| $memory[$item.Address] -ne $item.Value) { | |||
| throw ("{0} at `${1:X4} was not `${2:X2}; monitor response: {3}" -f | |||
| $item.Name, $item.Address, $item.Value, $response) | |||
| } | |||
| } | |||
| $writer.WriteLine("q") | |||
| $writer.Dispose() | |||
| $client.Dispose() | |||
| $client = $null | |||
| if (-not $process.WaitForExit(5000)) { throw "x128 did not exit after monitor quit" } | |||
| if (-not (Test-Path -LiteralPath $monitorLog)) { throw "monitor log not created" } | |||
| # Trace hits go to the log before the remote client connects and to the | |||
| # remote stream afterwards; examine both. | |||
| $trace = (Get-Content -Raw -LiteralPath $monitorLog) + "`n" + $response | |||
| [IO.File]::WriteAllText((Join-Path $build "mmu-probe-trace-dump.txt"), $trace) | |||
| # Every hit is "#n (Trace <kind> <addr>) ..." followed by ".C:<pc> ...". | |||
| $hits = [regex]::Matches($trace, | |||
| "(?im)^#\d+ \(Trace\s+(exec|store) ([0-9a-f]{4})\)[^\r\n]*\r?\n\.C:([0-9a-f]{4})") | |||
| $inWindow = $false | |||
| $windowClosed = $false | |||
| $storeCount = 0 | |||
| $written = @{} | |||
| foreach ($hit in $hits) { | |||
| $kind = $hit.Groups[1].Value.ToLowerInvariant() | |||
| $target = [Convert]::ToInt32($hit.Groups[2].Value, 16) | |||
| $pc = [Convert]::ToInt32($hit.Groups[3].Value, 16) | |||
| if ($kind -eq 'exec') { | |||
| if ($target -eq $entryAddress -and -not $windowClosed) { $inWindow = $true } | |||
| elseif ($target -eq $doneAddress -and $inWindow) { | |||
| $inWindow = $false; $windowClosed = $true; break | |||
| } | |||
| continue | |||
| } | |||
| if (-not $inWindow) { continue } | |||
| if ($pc -lt $entryAddress -or $pc -ge $codeEndAddress) { | |||
| throw ("MMU store to `${0:X4} from `${1:X4}, outside probe code" -f $target, $pc) | |||
| } | |||
| $written[$target] = $true | |||
| $storeCount++ | |||
| } | |||
| if (-not $windowClosed) { throw "trace has no complete entry..done probe window" } | |||
| foreach ($register in @(0xFF00, 0xD506, 0xD507, 0xD508, 0xD509, 0xD50A)) { | |||
| if (-not $written.ContainsKey($register)) { | |||
| throw ("probe window has no store to `${0:X4}" -f $register) | |||
| } | |||
| } | |||
| foreach ($register in @(0xD500, 0xD501, 0xD502, 0xD503, 0xD504, 0xD505, 0xD50B)) { | |||
| if ($written.ContainsKey($register)) { | |||
| throw ("probe wrote unintended MMU register `${0:X4}" -f $register) | |||
| } | |||
| } | |||
| Write-Host ("verified {0} MMU stores in the probe window, all from probe code" -f $storeCount) | |||
| Write-Host ("verified MMU result `$FF, completion `$A5, banks `$11/`$22, " + | |||
| "pages `$77/`$33, common-priority bank0 `$66/`$5A") | |||
| Write-Host ("monitor log: {0}" -f $monitorLog) | |||
| } finally { | |||
| if ($client) { $client.Dispose() } | |||
| if ($process -and -not $process.HasExited) { | |||
| $process.Kill() | |||
| $process.WaitForExit() | |||
| } | |||
| } | |||
| @@ -0,0 +1,166 @@ | |||
| ; E01-S04: C128 display/clock behavior probe. | |||
| ; | |||
| ; The program is loaded as BASIC 7 text at $1C01 and entered with SYS 7181. | |||
| ; It deliberately leaves display selection to the C128 editor. MODE ($D7) | |||
| ; bit 7 reports the active display: clear = VIC 40-column, set = VDC | |||
| ; 80-column. KERNAL CHROUT therefore targets whichever editor is active. | |||
| ; | |||
| ; The C128 Programmer's Reference Guide documents VIC register $D030 bit 0: | |||
| ; set = 2 MHz with VIC video disabled, clear = 1 MHz with VIC video enabled. | |||
| ; This probe selects 2 MHz only for an active 80-column display, exercises | |||
| ; CHROUT while fast (I/O clock stretching is hardware-defined), then restores | |||
| ; 1 MHz before returning to BASIC. It never accesses VDC $D600/$D601 and keeps | |||
| ; all machine-readable state in ordinary program RAM. | |||
| .setcpu "6502" | |||
| MODE = $00D7 | |||
| CLOCK = $D030 | |||
| CHROUT = $FFD2 | |||
| .export display_clock_entry | |||
| .export mode_40_checkpoint | |||
| .export mode_80_checkpoint | |||
| .export fast_checkpoint | |||
| .export fallback_done | |||
| .export display_result | |||
| .export mode_observed | |||
| .export fast_observed | |||
| .export final_clock | |||
| .export semantic_a | |||
| .export semantic_b | |||
| .segment "LOADADDR" | |||
| .word $1C01 | |||
| .segment "EXEHDR" | |||
| .word basic_end | |||
| .word 10 | |||
| .byte $9E ; BASIC SYS token | |||
| .byte "7181" ; entry at $1C0D | |||
| .byte $00 | |||
| basic_end: | |||
| .word $0000 | |||
| .segment "CODE" | |||
| display_clock_entry: | |||
| lda MODE | |||
| sta mode_observed | |||
| ; Deterministic safe baseline: bit 1 is a chip-test facility and must | |||
| ; be clear; bit 0 clear selects 1 MHz and restores VIC visibility. | |||
| lda CLOCK | |||
| and #$FC | |||
| sta CLOCK | |||
| lda #$00 | |||
| sta display_result | |||
| sta fast_observed | |||
| sta final_clock | |||
| lda MODE | |||
| bmi mode_80 | |||
| mode_40: | |||
| lda #$40 ; result bit 6 = 40-column path | |||
| sta display_result | |||
| mode_40_checkpoint: | |||
| ldx #<message_40 | |||
| ldy #>message_40 | |||
| jsr print_message | |||
| jmp verify_semantic_ram | |||
| mode_80: | |||
| lda #$80 ; result bit 7 = 80-column path | |||
| sta display_result | |||
| mode_80_checkpoint: | |||
| ldx #<message_80 | |||
| ldy #>message_80 | |||
| jsr print_message | |||
| ; Fast mode is safe for the VDC display. Preserve normal/test bits as | |||
| ; zero and set only the documented speed bit. | |||
| lda #$01 | |||
| sta CLOCK | |||
| fast_checkpoint: | |||
| lda CLOCK | |||
| and #$01 | |||
| sta fast_observed | |||
| beq fast_message | |||
| lda display_result | |||
| ora #$01 ; result bit 0 = fast bit observed set | |||
| sta display_result | |||
| fast_message: | |||
| ldx #<message_fast | |||
| ldy #>message_fast | |||
| jsr print_message ; KERNAL I/O while CPU fast | |||
| ; Always return the machine to the conservative fallback. | |||
| lda #$00 | |||
| sta CLOCK | |||
| ldx #<message_restore | |||
| ldy #>message_restore | |||
| jsr print_message | |||
| verify_semantic_ram: | |||
| lda semantic_a | |||
| cmp #$5A | |||
| bne semantic_done | |||
| lda semantic_b | |||
| cmp #$C3 | |||
| bne semantic_done | |||
| lda display_result | |||
| ora #$04 ; result bit 2 = ordinary RAM survived | |||
| sta display_result | |||
| semantic_done: | |||
| lda CLOCK | |||
| and #$01 | |||
| sta final_clock | |||
| fallback_done: | |||
| rts | |||
| ; X/Y = little-endian pointer to a zero-terminated PETSCII string. The load | |||
| ; operand is self-modified in program RAM, avoiding undocumented zero-page | |||
| ; assumptions in this pre-memory-map probe. | |||
| print_message: | |||
| stx message_load + 1 | |||
| sty message_load + 2 | |||
| print_next: | |||
| message_load: | |||
| lda $FFFF | |||
| beq print_done | |||
| jsr CHROUT | |||
| inc message_load + 1 | |||
| bne print_next | |||
| inc message_load + 2 | |||
| bne print_next | |||
| print_done: | |||
| rts | |||
| .segment "RODATA" | |||
| message_40: | |||
| .byte $0D | |||
| .byte "NOVA128 40 COL KERNAL OK", $0D | |||
| .byte "1MHZ FALLBACK OK", $0D, $00 | |||
| message_80: | |||
| .byte $0D | |||
| .byte "NOVA128 80 COL KERNAL OK", $0D, $00 | |||
| message_fast: | |||
| .byte "2MHZ KERNAL IO OK", $0D, $00 | |||
| message_restore: | |||
| .byte "RESTORED 1MHZ", $0D, $00 | |||
| .segment "DATA" | |||
| display_result: | |||
| .byte $00 | |||
| mode_observed: | |||
| .byte $00 | |||
| fast_observed: | |||
| .byte $00 | |||
| final_clock: | |||
| .byte $00 | |||
| semantic_a: | |||
| .byte $5A | |||
| semantic_b: | |||
| .byte $C3 | |||
| @@ -0,0 +1,507 @@ | |||
| ; E01-S03: C128 KERNAL mapping-wrapper probe. | |||
| ; | |||
| ; The probe enters two "compiler mappings" that match the E01-S02 findings: | |||
| ; state 0: CR=$0E RAM bank 0 to $BFFF, system ROM high, I/O | |||
| ; state 1: CR=$4E the same with RAM bank 1 | |||
| ; both with 8K bottom common RAM (RCR low bits %0110). Compiler mappings run | |||
| ; with IRQs masked (see docs/mmu-probe.md). | |||
| ; | |||
| ; kcall is the wrapper contract under test: | |||
| ; in: kc_target = KERNAL entry; A/X/Y = routine arguments | |||
| ; out: A/X/Y and N/Z/C exactly as the routine returned them; the I flag is | |||
| ; the caller's; CR/RCR/P0/P1 equal their values at entry | |||
| ; uses: kc_* variables; hardware stack: the caller's JSR plus one more | |||
| ; return address (kc_jump) on top of the routine's own usage | |||
| ; Inside the call the KERNAL sees the mapping BASIC had when the probe began | |||
| ; (CR/RCR saved at entry) with IRQs enabled. The wrapper and every byte it | |||
| ; touches live below $2000, i.e. in bottom common RAM, so it runs unchanged | |||
| ; from either bank. | |||
| ; | |||
| ; From each state the probe prints a line through CHROUT, reads the drive-8 | |||
| ; error channel (success path), and attempts to read from absent device 30 | |||
| ; (failure path), closing channels on both paths. A per-state result byte of | |||
| ; $FF means every check passed; see docs/kernal-wrapper.md for the bits. | |||
| .setcpu "6502" | |||
| SETBNK = $FF68 | |||
| READST = $FFB7 | |||
| SETLFS = $FFBA | |||
| SETNAM = $FFBD | |||
| OPEN = $FFC0 | |||
| CLOSE = $FFC3 | |||
| CHKIN = $FFC6 | |||
| CLRCHN = $FFCC | |||
| CHRIN = $FFCF | |||
| CHROUT = $FFD2 | |||
| LDTND = $98 ; number of open logical files | |||
| MMU_CR = $FF00 | |||
| MMU_RCR = $D506 ; RCR..P1H are $D506..$D50A | |||
| RCR_COMMON_8K_BOTTOM = $06 | |||
| STATUS_MAX = 32 ; bytes per status buffer | |||
| FAIL_DEVICE = 30 ; no device answers here in VICE | |||
| BIT_CHROUT = $01 ; every CHROUT returned carry clear | |||
| BIT_DISK_OPEN = $02 ; OPEN + CHKIN on 8,15 returned carry clear | |||
| BIT_STATUS = $04 ; status text is "dd...<CR>" | |||
| BIT_CLOSED_OK = $08 ; LDTND = 0 after the success path | |||
| BIT_FAIL_ERR = $10 ; CHKIN on device 30: C=1, A=5 | |||
| BIT_CLOSED_ER = $20 ; LDTND = 0 after the failure path | |||
| BIT_MMU_SAME = $40 ; CR/RCR/P0/P1 identical after every kcall | |||
| BIT_BANK_SAME = $80 ; $4000 still shows this bank's marker | |||
| .macro KCALL addr | |||
| jsr kcall_inline ; A/X/Y pass through untouched | |||
| .word addr | |||
| .endmacro | |||
| .export kprobe_entry | |||
| .export kprobe_done | |||
| .export kprobe_result | |||
| .export kprobe_complete | |||
| .export kprobe_state_result | |||
| .export kprobe_fail_a | |||
| .export kprobe_fail_p | |||
| .export kprobe_fail_st | |||
| .export kprobe_status_buf | |||
| .export kprobe_code_end | |||
| .export kcall | |||
| .segment "LOADADDR" | |||
| .word $1C01 | |||
| .segment "EXEHDR" | |||
| .word basic_end | |||
| .word 10 | |||
| .byte $9E | |||
| .byte "7181" ; SYS $1C0D | |||
| .byte $00 | |||
| basic_end: | |||
| .word $0000 | |||
| .segment "CODE" | |||
| kprobe_entry: | |||
| php | |||
| sei | |||
| lda #$00 | |||
| sta kprobe_result | |||
| sta kprobe_complete | |||
| lda MMU_CR | |||
| sta entry_mmu | |||
| ldx #4 | |||
| @save: lda MMU_RCR,x | |||
| sta entry_mmu+1,x | |||
| dex | |||
| bpl @save | |||
| lda entry_mmu ; KERNAL calls use BASIC's mapping | |||
| sta kc_kernal_cr | |||
| lda entry_mmu+1 | |||
| sta kc_kernal_rcr | |||
| and #$C0 ; keep VIC bank bits | |||
| ora #RCR_COMMON_8K_BOTTOM | |||
| sta compiler_rcr | |||
| lda #$00 | |||
| sta state_idx | |||
| jsr run_state | |||
| inc state_idx | |||
| jsr run_state | |||
| ; run_state left BASIC's mapping in place; confirm it exactly. | |||
| lda #$FF | |||
| sta kprobe_result | |||
| lda MMU_CR | |||
| cmp entry_mmu | |||
| bne @bad | |||
| ldx #4 | |||
| @chk: lda MMU_RCR,x | |||
| cmp entry_mmu+1,x | |||
| bne @bad | |||
| dex | |||
| bpl @chk | |||
| lda kprobe_state_result | |||
| and kprobe_state_result+1 | |||
| cmp #$FF | |||
| beq @good | |||
| @bad: lda #$00 | |||
| sta kprobe_result | |||
| @good: | |||
| lda #$A5 | |||
| sta kprobe_complete | |||
| kprobe_done: | |||
| plp | |||
| ; Default mapping again: print what each state read from drive 8. | |||
| ldx #$00 | |||
| jsr print_status | |||
| ldx #STATUS_MAX | |||
| jsr print_status | |||
| ldy #ok_message - messages | |||
| lda kprobe_result | |||
| cmp #$FF | |||
| beq @msg | |||
| ldy #fail_message - messages | |||
| @msg: jsr print_message | |||
| rts | |||
| ; ---------------------------------------------------------------- one state | |||
| run_state: | |||
| lda #$FF | |||
| sta mmu_ok | |||
| lda #$00 | |||
| sta result_bits | |||
| ldx state_idx | |||
| lda compiler_rcr ; bottom common first, then the bank | |||
| sta MMU_RCR | |||
| lda state_cr,x | |||
| sta MMU_CR | |||
| lda MMU_CR | |||
| sta exp_mmu | |||
| ldx #4 | |||
| @snap: lda MMU_RCR,x | |||
| sta exp_mmu+1,x | |||
| dex | |||
| bpl @snap | |||
| ldx state_idx | |||
| lda $4000 | |||
| sta saved_4000 | |||
| lda bank_marker,x | |||
| sta $4000 | |||
| ; ------------------------------------------- CHROUT from this state | |||
| lda #BIT_CHROUT | |||
| sta chrout_ok | |||
| ldy #state_message - messages | |||
| @out: lda messages,y | |||
| beq @digit | |||
| sty msg_y | |||
| KCALL CHROUT | |||
| bcc :+ | |||
| lda #$00 | |||
| sta chrout_ok | |||
| : ldy msg_y | |||
| iny | |||
| bne @out | |||
| @digit: lda state_idx | |||
| ora #'0' | |||
| KCALL CHROUT | |||
| lda #$0D | |||
| KCALL CHROUT | |||
| lda result_bits | |||
| ora chrout_ok | |||
| sta result_bits | |||
| ; ------------------------------------- success path: drive 8 status | |||
| lda #$00 | |||
| tax | |||
| KCALL SETBNK ; filename/data bank 0 (unused: no name) | |||
| lda #15 | |||
| ldx #8 | |||
| ldy #15 | |||
| KCALL SETLFS | |||
| lda #$00 | |||
| KCALL SETNAM | |||
| KCALL OPEN | |||
| bcs @close_ok | |||
| ldx #15 | |||
| KCALL CHKIN | |||
| bcs @close_ok | |||
| lda result_bits | |||
| ora #BIT_DISK_OPEN | |||
| sta result_bits | |||
| ldx state_idx | |||
| lda status_base,x | |||
| sta buf_pos | |||
| clc | |||
| adc #STATUS_MAX | |||
| sta buf_end | |||
| @read: KCALL CHRIN | |||
| ldx buf_pos | |||
| sta kprobe_status_buf,x | |||
| inc buf_pos | |||
| cmp #$0D | |||
| beq @read_done | |||
| inx | |||
| inx | |||
| cpx buf_end ; leave room for the terminating CR | |||
| bcc @read | |||
| lda #$0D | |||
| ldx buf_pos | |||
| sta kprobe_status_buf,x | |||
| jmp @close_ok ; truncated: no BIT_STATUS | |||
| @read_done: | |||
| ldx state_idx ; status must start with two digits | |||
| ldy status_base,x | |||
| lda kprobe_status_buf,y | |||
| jsr is_digit | |||
| bcc @close_ok | |||
| lda kprobe_status_buf+1,y | |||
| jsr is_digit | |||
| bcc @close_ok | |||
| lda result_bits | |||
| ora #BIT_STATUS | |||
| sta result_bits | |||
| @close_ok: | |||
| KCALL CLRCHN | |||
| lda #15 | |||
| KCALL CLOSE | |||
| lda LDTND | |||
| bne :+ | |||
| lda result_bits | |||
| ora #BIT_CLOSED_OK | |||
| sta result_bits | |||
| : | |||
| ; ------------------------------------ failure path: absent device | |||
| lda #14 | |||
| ldx #FAIL_DEVICE | |||
| ldy #15 | |||
| KCALL SETLFS | |||
| lda #$00 | |||
| KCALL SETNAM | |||
| KCALL OPEN ; no name: nothing is sent, so C=0 | |||
| ldx #14 | |||
| KCALL CHKIN ; TALK reaches nobody | |||
| sta tmp_a | |||
| php | |||
| pla | |||
| ldx state_idx | |||
| sta kprobe_fail_p,x | |||
| lda tmp_a | |||
| sta kprobe_fail_a,x | |||
| KCALL READST | |||
| ldx state_idx | |||
| sta kprobe_fail_st,x | |||
| lda kprobe_fail_p,x | |||
| lsr a ; C flag of CHKIN | |||
| bcc @fail_close | |||
| lda kprobe_fail_a,x | |||
| cmp #5 ; DEVICE NOT PRESENT | |||
| bne @fail_close | |||
| lda result_bits | |||
| ora #BIT_FAIL_ERR | |||
| sta result_bits | |||
| @fail_close: | |||
| KCALL CLRCHN | |||
| lda #14 | |||
| KCALL CLOSE | |||
| lda LDTND | |||
| bne :+ | |||
| lda result_bits | |||
| ora #BIT_CLOSED_ER | |||
| sta result_bits | |||
| : | |||
| ; ------------------------------------------------ mapping integrity | |||
| ldx state_idx | |||
| lda $4000 | |||
| cmp bank_marker,x | |||
| bne :+ | |||
| lda result_bits | |||
| ora #BIT_BANK_SAME | |||
| sta result_bits | |||
| : lda saved_4000 | |||
| sta $4000 | |||
| lda mmu_ok | |||
| beq :+ | |||
| lda result_bits | |||
| ora #BIT_MMU_SAME | |||
| sta result_bits | |||
| : lda result_bits | |||
| sta kprobe_state_result,x | |||
| lda kc_kernal_cr ; bank first: this code is in bank 0 | |||
| sta MMU_CR | |||
| lda kc_kernal_rcr | |||
| sta MMU_RCR | |||
| rts | |||
| ; C=1 when A is an ASCII/PETSCII digit. | |||
| is_digit: | |||
| cmp #'0' | |||
| bcc @no | |||
| cmp #'9'+1 | |||
| bcs @no | |||
| sec | |||
| rts | |||
| @no: clc | |||
| rts | |||
| ; Probe-only: read the inline target word after the JSR, step the return | |||
| ; address over it, then behave exactly like kcall_checked. Preserves A/X/Y. | |||
| kcall_inline: | |||
| sta inl_a | |||
| sty inl_y | |||
| pla | |||
| sta @src+1 ; return address = JSR's last byte | |||
| pla | |||
| sta @src+2 | |||
| ldy #1 | |||
| @src: lda $FFFF,y ; patched above | |||
| sta kc_target-1,y | |||
| iny | |||
| cpy #3 | |||
| bne @src | |||
| lda @src+1 ; resume after the inline word | |||
| clc | |||
| adc #2 | |||
| tay | |||
| lda @src+2 | |||
| adc #0 | |||
| pha | |||
| tya | |||
| pha | |||
| ldy inl_y | |||
| lda inl_a | |||
| ; fall through | |||
| ; Probe-only: kcall followed by an exact MMU comparison against exp_mmu. | |||
| kcall_checked: | |||
| jsr kcall | |||
| php | |||
| pha | |||
| stx chk_x | |||
| lda MMU_CR | |||
| cmp exp_mmu | |||
| bne @bad | |||
| ldx #4 | |||
| @cmp: lda MMU_RCR,x | |||
| cmp exp_mmu+1,x | |||
| bne @bad | |||
| dex | |||
| bpl @cmp | |||
| bmi @done | |||
| @bad: lda #$00 | |||
| sta mmu_ok | |||
| @done: ldx chk_x | |||
| pla | |||
| plp | |||
| rts | |||
| ; ------------------------------------------------------- the wrapper itself | |||
| kcall: | |||
| php | |||
| sei | |||
| sta kc_a | |||
| pla | |||
| sta kc_caller_p | |||
| lda MMU_CR | |||
| sta kc_saved_cr | |||
| lda MMU_RCR | |||
| sta kc_saved_rcr | |||
| lda kc_kernal_cr ; bank 0 first: kcall lives in bank 0 | |||
| sta MMU_CR | |||
| lda kc_kernal_rcr | |||
| sta MMU_RCR | |||
| cli ; KERNAL mapping is the default one | |||
| lda kc_a | |||
| jsr kc_jump | |||
| php | |||
| sei | |||
| sta kc_a | |||
| pla | |||
| sta kc_result_p | |||
| lda kc_saved_rcr ; common first, then the compiler bank | |||
| sta MMU_RCR | |||
| lda kc_saved_cr | |||
| sta MMU_CR | |||
| lda kc_caller_p ; result N/V/Z/C with the caller's I flag | |||
| and #$04 | |||
| sta kc_i_bit | |||
| lda kc_result_p | |||
| and #$FB | |||
| ora kc_i_bit | |||
| pha | |||
| lda kc_a | |||
| plp | |||
| rts | |||
| kc_jump: | |||
| jmp (kc_target) | |||
| .assert <kc_target <> $FF, error, "kc_target must not straddle a page (JMP bug)" | |||
| ; ------------------------------------------- default-mapping print helpers | |||
| print_message: | |||
| lda messages,y | |||
| beq @done | |||
| sty print_y | |||
| jsr CHROUT | |||
| ldy print_y | |||
| iny | |||
| bne print_message | |||
| @done: rts | |||
| print_status: | |||
| stx print_x | |||
| ldy #drive_message - messages | |||
| jsr print_message | |||
| ldx print_x | |||
| @loop: lda kprobe_status_buf,x | |||
| sta print_ch | |||
| stx print_x | |||
| jsr CHROUT | |||
| ldx print_x | |||
| lda print_ch | |||
| cmp #$0D | |||
| beq @done | |||
| inx | |||
| bne @loop | |||
| @done: rts | |||
| kprobe_code_end: | |||
| .segment "RODATA" | |||
| state_cr: .byte $0E, $4E | |||
| bank_marker: .byte $B0, $B1 | |||
| status_base: .byte 0, STATUS_MAX | |||
| messages: | |||
| state_message: | |||
| .byte "KCALL CHROUT STATE ", $00 | |||
| drive_message: | |||
| .byte "DRIVE 8: ", $00 | |||
| ok_message: | |||
| .byte "NOVA128 KERNAL PROBE OK", $0D, $00 | |||
| fail_message: | |||
| .byte "NOVA128 KERNAL PROBE FAIL", $0D, $00 | |||
| .segment "DATA" | |||
| kprobe_result: .byte $00 | |||
| kprobe_complete: .byte $00 | |||
| kprobe_state_result: .byte $00, $00 | |||
| kprobe_fail_a: .byte $00, $00 | |||
| kprobe_fail_p: .byte $00, $00 | |||
| kprobe_fail_st: .byte $00, $00 | |||
| kprobe_status_buf: .res STATUS_MAX * 2, $00 | |||
| entry_mmu: .res 6, $00 ; CR, RCR, P0L, P0H, P1L, P1H | |||
| exp_mmu: .res 6, $00 | |||
| compiler_rcr: .byte $00 | |||
| state_idx: .byte $00 | |||
| result_bits: .byte $00 | |||
| chrout_ok: .byte $00 | |||
| mmu_ok: .byte $00 | |||
| saved_4000: .byte $00 | |||
| msg_y: .byte $00 | |||
| buf_pos: .byte $00 | |||
| buf_end: .byte $00 | |||
| tmp_a: .byte $00 | |||
| chk_x: .byte $00 | |||
| inl_a: .byte $00 | |||
| inl_y: .byte $00 | |||
| print_x: .byte $00 | |||
| print_y: .byte $00 | |||
| print_ch: .byte $00 | |||
| kc_target: .word $0000 | |||
| kc_kernal_cr: .byte $00 | |||
| kc_kernal_rcr: .byte $00 | |||
| kc_saved_cr: .byte $00 | |||
| kc_saved_rcr: .byte $00 | |||
| kc_caller_p: .byte $00 | |||
| kc_result_p: .byte $00 | |||
| kc_i_bit: .byte $00 | |||
| kc_a: .byte $00 | |||
| @@ -0,0 +1,60 @@ | |||
| ; E01-S01: smallest C128-native Nova128 platform proof. | |||
| ; | |||
| ; Loaded as a BASIC 7 program at $1C01. RUN executes SYS 7181, which enters | |||
| ; `minimal_entry` at $1C0D. The routine uses the documented KERNAL CHROUT jump | |||
| ; table while the normal C128 BASIC mapping is active, records $A5 in | |||
| ; `minimal_result`, and returns to BASIC with RTS. | |||
| ; | |||
| ; This file deliberately does not change the MMU or CPU clock. Those behaviors | |||
| ; belong to E01-S02 and E01-S04. | |||
| .setcpu "6502" | |||
| CHROUT = $FFD2 | |||
| .export minimal_entry | |||
| .export minimal_done | |||
| .export minimal_result | |||
| .segment "LOADADDR" | |||
| .word $1C01 | |||
| .segment "EXEHDR" | |||
| .word basic_end ; link to BASIC end marker | |||
| .word 10 ; BASIC line 10 | |||
| .byte $9E ; SYS token | |||
| .byte "7181" ; $1C0D, first byte after the header | |||
| .byte $00 ; end of BASIC line | |||
| basic_end: | |||
| .word $0000 ; end of BASIC program | |||
| .segment "CODE" | |||
| minimal_entry: | |||
| lda #$00 | |||
| sta minimal_result | |||
| ldx #$00 | |||
| print_loop: | |||
| lda minimal_message,x | |||
| beq print_complete | |||
| jsr CHROUT | |||
| inx | |||
| bne print_loop ; message is deliberately shorter than 256 | |||
| print_complete: | |||
| lda #$A5 | |||
| sta minimal_result | |||
| minimal_done: | |||
| rts ; return from BASIC's SYS call | |||
| .segment "RODATA" | |||
| minimal_message: | |||
| .byte $0D | |||
| .byte "NOVA128 C128 NATIVE OK" | |||
| .byte $0D, $00 | |||
| .segment "DATA" | |||
| minimal_result: | |||
| .byte $00 | |||
| @@ -0,0 +1,505 @@ | |||
| ; E01-S02: C128 MMU, common-RAM, and page-pointer platform proof. | |||
| ; | |||
| ; The probe starts under BASIC 7 at $1C0D. It temporarily configures 8K common | |||
| ; RAM at both bottom and top, so its code/data, zero page, and hardware stack | |||
| ; remain in bank 0 while CR bit 6 selects RAM bank 1. KERNAL ROM and I/O remain | |||
| ; mapped in both probe configurations ($0E/$4E). | |||
| ; | |||
| ; IRQ state is saved with PHP, IRQs are disabled with SEI for every MMU/page | |||
| ; pointer transition, all MMU state is restored, and PLP restores the caller's | |||
| ; interrupt flag before KERNAL output. This probe does not define the final | |||
| ; compiler map and does not implement the E01-S03 KERNAL wrapper contract. | |||
| .setcpu "6502" | |||
| CHROUT = $FFD2 | |||
| MMU_CR = $FF00 ; always visible configuration register | |||
| MMU_RCR = $D506 ; I/O-visible RAM/common configuration | |||
| MMU_P0L = $D507 | |||
| MMU_P0H = $D508 | |||
| MMU_P1L = $D509 | |||
| MMU_P1H = $D50A | |||
| CR_BANK0_RAM_KERNAL_IO = $0E ; RAM $4000-$BFFF, KERNAL high, I/O | |||
| CR_BANK1_RAM_KERNAL_IO = $4E | |||
| RCR_COMMON_8K_BOTH = $0E ; 8K common, bottom + top | |||
| BIT_BANK0 = $01 | |||
| BIT_BANK1 = $02 | |||
| BIT_COMMON = $04 | |||
| BIT_RETURN = $08 | |||
| BIT_P0_RELOC = $10 | |||
| BIT_P0_COMMON = $20 | |||
| BIT_P1_COMMON = $40 | |||
| BIT_RESTORED = $80 | |||
| .export mmu_probe_entry | |||
| .export mmu_probe_done | |||
| .export mmu_probe_result | |||
| .export mmu_probe_complete | |||
| .export mmu_probe_observed_bank0 | |||
| .export mmu_probe_observed_bank1 | |||
| .export mmu_probe_observed_p0 | |||
| .export mmu_probe_observed_p1 | |||
| .export mmu_probe_saved_cr | |||
| .export mmu_probe_saved_rcr | |||
| .export mmu_probe_saved_p0l | |||
| .export mmu_probe_saved_p0h | |||
| .export mmu_probe_saved_p1l | |||
| .export mmu_probe_saved_p1h | |||
| .export mmu_probe_common_rcr | |||
| .export mmu_probe_code_end | |||
| .export mmu_probe_obs_p0c_zp | |||
| .export mmu_probe_obs_p0c_b0 | |||
| .export mmu_probe_obs_p0c_b1 | |||
| .export mmu_probe_obs_p1c_stack | |||
| .export mmu_probe_obs_p1c_b0 | |||
| .segment "LOADADDR" | |||
| .word $1C01 | |||
| .segment "EXEHDR" | |||
| .word basic_end | |||
| .word 10 | |||
| .byte $9E | |||
| .byte "7181" ; SYS $1C0D | |||
| .byte $00 | |||
| basic_end: | |||
| .word $0000 | |||
| .segment "CODE" | |||
| mmu_probe_entry: | |||
| php ; preserve BASIC's interrupt state | |||
| sei ; no IRQ may observe a temporary mapping | |||
| lda #$00 | |||
| sta mmu_probe_result | |||
| sta mmu_probe_complete | |||
| sta common_stage | |||
| sta return_marker | |||
| lda MMU_CR | |||
| sta saved_cr | |||
| lda MMU_RCR | |||
| sta saved_rcr | |||
| lda MMU_P0L | |||
| sta saved_p0l | |||
| lda MMU_P0H | |||
| sta saved_p0h | |||
| lda MMU_P1L | |||
| sta saved_p1l | |||
| lda MMU_P1H | |||
| sta saved_p1h | |||
| tsx | |||
| stx saved_sp | |||
| ; Preserve the VIC bank while selecting 8K common at top and bottom. | |||
| lda saved_rcr | |||
| and #$C0 | |||
| ora #RCR_COMMON_8K_BOTH | |||
| sta probe_rcr_common | |||
| and #$C0 ; same VIC bank, no common RAM | |||
| sta probe_rcr_none | |||
| lda probe_rcr_common | |||
| sta MMU_RCR | |||
| ; ------------------------------------------------ distinct banks | |||
| lda #CR_BANK0_RAM_KERNAL_IO | |||
| sta MMU_CR | |||
| lda $4000 | |||
| sta saved_bank0_byte | |||
| lda #$11 | |||
| sta $4000 | |||
| lda #$3C | |||
| sta common_marker | |||
| lda #CR_BANK1_RAM_KERNAL_IO | |||
| sta MMU_CR | |||
| lda $4000 | |||
| sta saved_bank1_byte | |||
| lda #$22 | |||
| sta $4000 | |||
| lda $4000 | |||
| sta mmu_probe_observed_bank1 | |||
| cmp #$22 | |||
| bne bank1_bad | |||
| lda mmu_probe_result | |||
| ora #BIT_BANK1 | |||
| sta mmu_probe_result | |||
| bank1_bad: | |||
| lda common_marker | |||
| cmp #$3C | |||
| bne common_bank1_bad | |||
| inc common_stage | |||
| lda #$C3 | |||
| sta common_marker | |||
| common_bank1_bad: | |||
| ; A JSR/RTS while bank 1 is selected proves code and hardware stack are | |||
| ; safely visible through bottom common RAM. | |||
| jsr bank1_return_probe | |||
| tsx | |||
| cpx saved_sp | |||
| bne bank_return_bad | |||
| lda return_marker | |||
| cmp #$5A | |||
| bne bank_return_bad | |||
| lda mmu_probe_result | |||
| ora #BIT_RETURN | |||
| sta mmu_probe_result | |||
| bank_return_bad: | |||
| lda #CR_BANK0_RAM_KERNAL_IO | |||
| sta MMU_CR | |||
| lda $4000 | |||
| sta mmu_probe_observed_bank0 | |||
| cmp #$11 | |||
| bne bank0_bad | |||
| lda mmu_probe_result | |||
| ora #BIT_BANK0 | |||
| sta mmu_probe_result | |||
| bank0_bad: | |||
| lda common_stage | |||
| cmp #$01 | |||
| bne common_bad | |||
| lda common_marker | |||
| cmp #$C3 | |||
| bne common_bad | |||
| lda mmu_probe_result | |||
| ora #BIT_COMMON | |||
| sta mmu_probe_result | |||
| common_bad: | |||
| ; ------------------------------------------ page-zero relocation test | |||
| ; With common disabled, relocate CPU page zero to physical page $20 in | |||
| ; RAM bank 1. Save/write through $00F0, then later select bank 1 and | |||
| ; inspect physical $20F0 directly. | |||
| lda probe_rcr_none | |||
| sta MMU_RCR | |||
| lda #$01 ; P0 target is in RAM bank 1 | |||
| sta MMU_P0H ; high first, low commits translation | |||
| lda #$20 | |||
| sta MMU_P0L | |||
| lda $00F0 | |||
| sta saved_p0_target | |||
| lda #$77 | |||
| sta $00F0 | |||
| ; Prepare a bank-1 page-$21 sentinel at the exact hardware-stack | |||
| ; offset. P1 common-priority testing must leave this byte unchanged. | |||
| lda #$01 | |||
| sta MMU_P0H | |||
| lda #$21 | |||
| sta MMU_P0L | |||
| ldx saved_sp | |||
| lda $0000,x | |||
| sta saved_p1_target | |||
| lda #$33 | |||
| sta $0000,x | |||
| jsr restore_p0 | |||
| lda probe_rcr_common | |||
| sta MMU_RCR | |||
| lda #CR_BANK1_RAM_KERNAL_IO | |||
| sta MMU_CR | |||
| lda $20F0 | |||
| sta mmu_probe_observed_p0 | |||
| cmp #$77 | |||
| bne p0_reloc_bad | |||
| lda mmu_probe_result | |||
| ora #BIT_P0_RELOC | |||
| sta mmu_probe_result | |||
| p0_reloc_bad: | |||
| lda #CR_BANK0_RAM_KERNAL_IO | |||
| sta MMU_CR | |||
| ; -------------------------------------- page-zero common-RAM priority | |||
| ; Observe where a write lands when P0 aims at bank 1 page $21 while | |||
| ; bottom common RAM is active: logical $00F1, bank0:$21F1, or | |||
| ; bank1:$21F1. Originals of all three bytes are saved and restored. | |||
| lda #CR_BANK1_RAM_KERNAL_IO | |||
| sta MMU_CR | |||
| lda $21F1 | |||
| sta saved_b1_21f1 | |||
| lda #CR_BANK0_RAM_KERNAL_IO | |||
| sta MMU_CR | |||
| lda $21F1 | |||
| sta saved_b0_21f1 | |||
| lda $00F1 | |||
| sta saved_zp_f1 | |||
| lda #$01 | |||
| sta MMU_P0H | |||
| lda #$21 | |||
| sta MMU_P0L | |||
| lda #$66 | |||
| sta $00F1 | |||
| jsr restore_p0 | |||
| lda $00F1 | |||
| sta mmu_probe_obs_p0c_zp | |||
| lda $21F1 | |||
| sta mmu_probe_obs_p0c_b0 | |||
| lda saved_b0_21f1 | |||
| sta $21F1 | |||
| lda #CR_BANK1_RAM_KERNAL_IO | |||
| sta MMU_CR | |||
| lda $21F1 | |||
| sta mmu_probe_obs_p0c_b1 | |||
| lda saved_b1_21f1 | |||
| sta $21F1 | |||
| lda #CR_BANK0_RAM_KERNAL_IO | |||
| sta MMU_CR | |||
| ; Observed VICE 3.10 model: the page translation applies, but because | |||
| ; logical page 0 lies in common RAM the pointer's bank bit is forced | |||
| ; to bank 0. Only bank0:$21F1 receives the write. | |||
| lda mmu_probe_obs_p0c_zp | |||
| cmp #$66 | |||
| beq p0_common_bad | |||
| lda mmu_probe_obs_p0c_b0 | |||
| cmp #$66 | |||
| bne p0_common_bad | |||
| lda mmu_probe_obs_p0c_b1 | |||
| cmp #$66 | |||
| beq p0_common_bad | |||
| lda mmu_probe_result | |||
| ora #BIT_P0_COMMON | |||
| sta mmu_probe_result | |||
| p0_common_bad: | |||
| lda saved_zp_f1 | |||
| sta $00F1 | |||
| ; --------------------------------------- page-one common-RAM priority | |||
| ; With bottom common active, P1 aimed at bank 1 page $21 follows the | |||
| ; same model as P0: the stack moves to bank0:$21xx, not bank 1. PHA/PLA | |||
| ; stays coherent, the bank-1 sentinel stays $33, bank0:$01xx is not | |||
| ; written, and bank0:$2100+SP receives the pushed byte. | |||
| ldx saved_sp | |||
| lda $2100,x | |||
| sta saved_b0_21sp | |||
| lda #$01 | |||
| sta MMU_P1H | |||
| lda #$21 | |||
| sta MMU_P1L | |||
| lda #$5A | |||
| pha | |||
| pla | |||
| cmp #$5A | |||
| bne p1_stack_bad | |||
| inc p1_stack_ok | |||
| p1_stack_bad: | |||
| ; Restore P1 inline. A JSR here would push its return address through | |||
| ; the very mapping under test and then change that mapping before RTS. | |||
| lda saved_p1h | |||
| sta MMU_P1H | |||
| lda saved_p1l | |||
| sta MMU_P1L | |||
| ldx saved_sp ; PHA wrote at offset saved_sp | |||
| lda $0100,x | |||
| sta mmu_probe_obs_p1c_stack | |||
| cmp #$5A | |||
| bne p1_page1_clean | |||
| dec p1_stack_ok ; stack was not relocated at all | |||
| p1_page1_clean: | |||
| lda $2100,x | |||
| sta mmu_probe_obs_p1c_b0 | |||
| cmp #$5A | |||
| beq p1_b0_hit | |||
| dec p1_stack_ok ; push did not land in bank0 page $21 | |||
| p1_b0_hit: | |||
| lda saved_b0_21sp | |||
| sta $2100,x | |||
| ; Disable common RAM, inspect/restore the bank-1 sentinel through a | |||
| ; relocated page zero, and restore the earlier page-zero target too. | |||
| lda probe_rcr_none | |||
| sta MMU_RCR | |||
| lda #$01 | |||
| sta MMU_P0H | |||
| lda #$21 | |||
| sta MMU_P0L | |||
| ldx saved_sp | |||
| lda $0000,x | |||
| sta mmu_probe_observed_p1 | |||
| cmp #$33 | |||
| bne p1_sentinel_bad | |||
| lda p1_stack_ok | |||
| cmp #$01 | |||
| bne p1_sentinel_bad | |||
| lda mmu_probe_result | |||
| ora #BIT_P1_COMMON | |||
| sta mmu_probe_result | |||
| p1_sentinel_bad: | |||
| lda saved_p1_target | |||
| sta $0000,x | |||
| lda #$01 | |||
| sta MMU_P0H | |||
| lda #$20 | |||
| sta MMU_P0L | |||
| lda saved_p0_target | |||
| sta $00F0 | |||
| jsr restore_p0 | |||
| ; --------------------------------------------------------- cleanup | |||
| lda probe_rcr_common | |||
| sta MMU_RCR | |||
| lda #CR_BANK1_RAM_KERNAL_IO | |||
| sta MMU_CR | |||
| lda saved_bank1_byte | |||
| sta $4000 | |||
| lda #CR_BANK0_RAM_KERNAL_IO | |||
| sta MMU_CR | |||
| lda saved_bank0_byte | |||
| sta $4000 | |||
| jsr restore_p0 | |||
| jsr restore_p1 | |||
| lda saved_rcr | |||
| sta MMU_RCR | |||
| lda saved_cr | |||
| sta MMU_CR | |||
| ; All visible MMU state must match the entry state before interrupts | |||
| ; are restored or any KERNAL routine is called. | |||
| lda MMU_CR | |||
| cmp saved_cr | |||
| bne restore_bad | |||
| lda MMU_RCR | |||
| cmp saved_rcr | |||
| bne restore_bad | |||
| lda MMU_P0L | |||
| cmp saved_p0l | |||
| bne restore_bad | |||
| lda MMU_P0H | |||
| cmp saved_p0h | |||
| bne restore_bad | |||
| lda MMU_P1L | |||
| cmp saved_p1l | |||
| bne restore_bad | |||
| lda MMU_P1H | |||
| cmp saved_p1h | |||
| bne restore_bad | |||
| lda mmu_probe_result | |||
| ora #BIT_RESTORED | |||
| sta mmu_probe_result | |||
| restore_bad: | |||
| lda #$A5 | |||
| sta mmu_probe_complete | |||
| mmu_probe_done: | |||
| plp ; restore BASIC's original IRQ state | |||
| ldx #$00 | |||
| lda mmu_probe_result | |||
| cmp #$FF | |||
| beq print_ok_loop | |||
| print_fail_loop: | |||
| lda fail_message,x | |||
| beq print_done | |||
| txa | |||
| pha | |||
| lda fail_message,x | |||
| jsr CHROUT | |||
| pla | |||
| tax | |||
| inx | |||
| bne print_fail_loop | |||
| print_ok_loop: | |||
| lda ok_message,x | |||
| beq print_done | |||
| txa | |||
| pha | |||
| lda ok_message,x | |||
| jsr CHROUT | |||
| pla | |||
| tax | |||
| inx | |||
| bne print_ok_loop | |||
| print_done: | |||
| rts | |||
| ; Runs with bank 1 selected. Code, data, zero page, and stack are all visible | |||
| ; because the bottom 8K is common bank-0 RAM. | |||
| bank1_return_probe: | |||
| lda #$5A | |||
| sta return_marker | |||
| rts | |||
| restore_p0: | |||
| lda saved_p0h ; high first, low commits pointer update | |||
| sta MMU_P0H | |||
| lda saved_p0l | |||
| sta MMU_P0L | |||
| rts | |||
| restore_p1: | |||
| lda saved_p1h | |||
| sta MMU_P1H | |||
| lda saved_p1l | |||
| sta MMU_P1L | |||
| rts | |||
| mmu_probe_code_end: ; verifier: MMU stores must come from below | |||
| .segment "RODATA" | |||
| ok_message: | |||
| .byte $0D, "NOVA128 MMU PROBE OK", $0D, $00 | |||
| fail_message: | |||
| .byte $0D, "NOVA128 MMU PROBE FAIL", $0D, $00 | |||
| .segment "DATA" | |||
| mmu_probe_result: | |||
| .byte $00 | |||
| mmu_probe_complete: | |||
| .byte $00 | |||
| mmu_probe_observed_bank0: | |||
| .byte $00 | |||
| mmu_probe_observed_bank1: | |||
| .byte $00 | |||
| mmu_probe_observed_p0: | |||
| .byte $00 | |||
| mmu_probe_observed_p1: | |||
| .byte $00 | |||
| mmu_probe_obs_p0c_zp: ; logical $00F1 after P0 restored | |||
| .byte $00 | |||
| mmu_probe_obs_p0c_b0: ; bank0:$21F1 | |||
| .byte $00 | |||
| mmu_probe_obs_p0c_b1: ; bank1:$21F1 | |||
| .byte $00 | |||
| mmu_probe_obs_p1c_stack: ; bank0:$0100+SP after relocated PHA | |||
| .byte $00 | |||
| mmu_probe_obs_p1c_b0: ; bank0:$2100+SP after relocated PHA | |||
| .byte $00 | |||
| saved_b0_21sp: .byte $00 | |||
| saved_b0_21f1: .byte $00 | |||
| saved_b1_21f1: .byte $00 | |||
| mmu_probe_saved_cr: | |||
| saved_cr: .byte $00 | |||
| mmu_probe_saved_rcr: | |||
| saved_rcr: .byte $00 | |||
| mmu_probe_saved_p0l: | |||
| saved_p0l: .byte $00 | |||
| mmu_probe_saved_p0h: | |||
| saved_p0h: .byte $00 | |||
| mmu_probe_saved_p1l: | |||
| saved_p1l: .byte $00 | |||
| mmu_probe_saved_p1h: | |||
| saved_p1h: .byte $00 | |||
| saved_sp: .byte $00 | |||
| mmu_probe_common_rcr: | |||
| probe_rcr_common: .byte $00 | |||
| probe_rcr_none: .byte $00 | |||
| saved_bank0_byte: .byte $00 | |||
| saved_bank1_byte: .byte $00 | |||
| saved_p0_target: .byte $00 | |||
| saved_p1_target: .byte $00 | |||
| saved_zp_f1: .byte $00 | |||
| common_marker: .byte $00 | |||
| common_stage: .byte $00 | |||
| return_marker: .byte $00 | |||
| p1_stack_ok: .byte $00 | |||
| @@ -0,0 +1,122 @@ | |||
| # E07-S01 integration checks for scripts/prepare-disk.ps1. | |||
| [CmdletBinding()] | |||
| param() | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent | |||
| $repository = Split-Path $project -Parent | |||
| $prepare = Join-Path $project "scripts\prepare-disk.ps1" | |||
| $buildMinimal = Join-Path $project "scripts\build-minimal.ps1" | |||
| $source = Join-Path $repository "vim\work.d81" | |||
| $c1541 = "C:\Program Files\GTK3VICE-3.10-win64\bin\c1541.exe" | |||
| $testRoot = Join-Path $project "build\disk-test" | |||
| $output = Join-Path $testRoot "newlang-test.d81" | |||
| $examples = Join-Path $testRoot "examples" | |||
| $testCompiler = Join-Path $testRoot "n128c-test.prg" | |||
| $missing = Join-Path $testRoot "missing" | |||
| if (-not (Test-Path -LiteralPath $prepare -PathType Leaf)) { | |||
| throw "prepare-disk.ps1 not found" | |||
| } | |||
| if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { | |||
| throw "test source D81 not found: $source" | |||
| } | |||
| if (-not (Test-Path -LiteralPath $c1541 -PathType Leaf)) { | |||
| throw "test c1541 not found: $c1541" | |||
| } | |||
| New-Item -ItemType Directory -Force -Path $examples | Out-Null | |||
| [IO.File]::WriteAllText((Join-Path $examples "demo.n8"), | |||
| "program Main`n print `"Nova128`"`nend program`n", | |||
| [Text.UTF8Encoding]::new($false)) | |||
| # E01-S01 is this story's completed dependency and provides a known-good PRG. | |||
| & $buildMinimal | Out-Host | |||
| $minimal = Join-Path $project "build\minimal.prg" | |||
| Copy-Item -LiteralPath $minimal -Destination $testCompiler -Force | |||
| $sourceHashBefore = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash | |||
| $first = & $prepare -SourceImage $source -OutputImage $output -C1541Path $c1541 ` | |||
| -CompilerPrg $testCompiler -ProbePrg $minimal -ExamplesDirectory $examples ` | |||
| -RequireCompiler | |||
| if (-not (Test-Path -LiteralPath $output -PathType Leaf)) { | |||
| throw "prepare-disk did not create the disposable image" | |||
| } | |||
| $firstHash = (Get-FileHash -LiteralPath $output -Algorithm SHA256).Hash | |||
| $listing = (& $c1541 $output -list 2>&1) -join "`n" | |||
| foreach ($expected in @( | |||
| '(?im)"n128c"\s+prg\s*$', | |||
| '(?im)"n128probe"\s+prg\s*$', | |||
| '(?im)"demo\.n8"\s+seq\s*$')) { | |||
| if ($listing -notmatch $expected) { | |||
| throw "prepared directory lacks expected entry $expected`n$listing" | |||
| } | |||
| } | |||
| # Damage only the generated output, then prove a rebuild returns to the exact | |||
| # same bytes from the clean source image. | |||
| [IO.File]::WriteAllBytes($output, [byte[]](1, 2, 3, 4)) | |||
| $second = & $prepare -SourceImage $source -OutputImage $output -C1541Path $c1541 ` | |||
| -CompilerPrg $testCompiler -ProbePrg $minimal -ExamplesDirectory $examples ` | |||
| -RequireCompiler | |||
| $secondHash = (Get-FileHash -LiteralPath $output -Algorithm SHA256).Hash | |||
| if ($firstHash -ne $secondHash) { | |||
| throw "two clean prepares produced different D81 hashes" | |||
| } | |||
| $sourceHashAfter = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash | |||
| if ($sourceHashAfter -ne $sourceHashBefore) { | |||
| throw "source D81 hash changed during prepare test" | |||
| } | |||
| # Optional, not-yet-built artifacts must be skipped without preventing a clean | |||
| # copy. This deliberately uses absent compiler, probe, and examples paths. | |||
| $optionalOutput = Join-Path $testRoot "optional-only.d81" | |||
| $optional = & $prepare -SourceImage $source -OutputImage $optionalOutput ` | |||
| -C1541Path $c1541 -CompilerPrg (Join-Path $missing "n128c.prg") ` | |||
| -ProbePrg (Join-Path $missing "probe.prg") ` | |||
| -ExamplesDirectory (Join-Path $missing "examples") 3>&1 | |||
| if (-not (Test-Path -LiteralPath $optionalOutput -PathType Leaf)) { | |||
| throw "missing optional artifacts prevented creation of the copied D81" | |||
| } | |||
| function Assert-FailsWith { | |||
| param( | |||
| [Parameter(Mandatory = $true)][scriptblock]$Action, | |||
| [Parameter(Mandatory = $true)][string]$Pattern | |||
| ) | |||
| $caught = $null | |||
| try { | |||
| & $Action | |||
| } catch { | |||
| $caught = $_.Exception.Message | |||
| } | |||
| if ($null -eq $caught) { | |||
| throw "expected failure matching '$Pattern', but the command succeeded" | |||
| } | |||
| if ($caught -notmatch $Pattern) { | |||
| throw "failure '$caught' did not match '$Pattern'" | |||
| } | |||
| } | |||
| Assert-FailsWith -Pattern 'source D81 not found' -Action { | |||
| & $prepare -SourceImage (Join-Path $missing "work.d81") ` | |||
| -OutputImage (Join-Path $testRoot "never-source.d81") ` | |||
| -C1541Path $c1541 | |||
| } | |||
| Assert-FailsWith -Pattern 'c1541 tool not found' -Action { | |||
| & $prepare -SourceImage $source ` | |||
| -OutputImage (Join-Path $testRoot "never-tool.d81") ` | |||
| -C1541Path (Join-Path $missing "c1541.exe") | |||
| } | |||
| Assert-FailsWith -Pattern 'must not be the source' -Action { | |||
| & $prepare -SourceImage $source -OutputImage $source -C1541Path $c1541 | |||
| } | |||
| Write-Host "prepare-disk tests passed" | |||
| Write-Host "source SHA-256 unchanged: $sourceHashAfter" | |||
| Write-Host "deterministic output SHA-256: $secondHash" | |||
| Write-Host "directory entries verified: n128c, n128probe, demo.n8" | |||
| @@ -0,0 +1,79 @@ | |||
| # Grammar Fixtures | |||
| These fixtures define the accepted and rejected surface grammar before parser | |||
| implementation. They are readable ASCII mirrors of Nova128 source. Later | |||
| PETSCII conversion tests must derive from these sources rather than replacing | |||
| them as the authoritative text. | |||
| Run the structural check from `newLang/`: | |||
| ```powershell | |||
| powershell -NoProfile -ExecutionPolicy Bypass -File tests/grammar/validate.ps1 | |||
| ``` | |||
| The validator enforces the Vim editor profile for every `.n8` fixture: | |||
| - base name at most 12 characters; | |||
| - at most 80 ASCII bytes per logical line; | |||
| - at most 18,999 source bytes; | |||
| - at most 599 logical lines; | |||
| - one symbolic diagnostic expectation for every rejected fixture; | |||
| - no diagnostic sidecars for accepted fixtures. | |||
| `.expect` files deliberately use symbolic diagnostic names. Story E02-S04 will | |||
| assign stable numeric codes without requiring grammar fixtures to guess them. | |||
| ## Accepted Coverage | |||
| | Production or form | Fixture evidence | | |||
| |---|---| | |||
| | Empty file body and program declaration | `accepted/minimal.n8` | | |||
| | Comment and EOL | `accepted/minimal.n8` | | |||
| | Interface declaration and method signatures | `decls.n8`, `forward.n8` | | |||
| | Class declaration and `implements` list | `decls.n8` | | |||
| | Forward top-level type reference | `forward.n8` | | |||
| | Field declaration, access, and fixed array type | `decls.n8` | | |||
| | Default field/method access | `nulls.n8` | | |||
| | Method declaration, parameters, locals, result type | `decls.n8`, `stmts.n8` | | |||
| | `init` and `destroy` declarations | `decls.n8` | | |||
| | Program variable declarations | all non-minimal accepted fixtures | | |||
| | Assignment to local, field, and array element | `stmts.n8`, `arrays.n8` | | |||
| | Call statement and call expression | `stmts.n8` | | |||
| | `if`, `else`, and `end if` | `decls.n8`, `stmts.n8`, `exprs.n8` | | |||
| | `if` without `else` | `exprs.n8` | | |||
| | `while` | `stmts.n8` | | |||
| | `for` with and without `step` | `stmts.n8` | | |||
| | `return` with and without a value | `decls.n8`, `nulls.n8` | | |||
| | `print` literal and scalar | `stmts.n8`, `exprs.n8` | | |||
| | `delete` concrete and interface references | `stmts.n8`, `decls.n8` | | |||
| | `new` expression and constructor arguments | `decls.n8`, `stmts.n8` | | |||
| | Member access, method call, and array indexing | `decls.n8`, `arrays.n8` | | |||
| | Parentheses and all documented operator levels | `exprs.n8` | | |||
| | Decimal number, string escape, character literal, booleans, and `null` | `exprs.n8`, `decls.n8`, `nulls.n8` | | |||
| Hexadecimal and binary numeric forms are lexer behavior and belong to E02-S02 | |||
| token fixtures, not parser grammar coverage. | |||
| ## Rejected Coverage | |||
| | Required rejection | Fixture | Expected symbolic diagnostic | | |||
| |---|---|---| | |||
| | Missing program terminator | `miss_end.n8` | `SYN_EXPECTED_END_PROGRAM` | | |||
| | Invalid expression continuation | `line_cont.n8` | `SYN_UNEXPECTED_EOL` | | |||
| | Duplicate declaration | `dup_decl.n8` | `SEM_DUPLICATE_NAME` | | |||
| | Malformed interface header | `bad_iface.n8` | `SYN_EXPECTED_IDENTIFIER` | | |||
| | Malformed class header | `bad_class.n8` | `SYN_EXPECTED_INTERFACE_NAME` | | |||
| | Missing method terminator | `miss_meth.n8` | `SYN_EXPECTED_END_METHOD` | | |||
| | Missing `if` terminator | `miss_if.n8` | `SYN_EXPECTED_END_IF` | | |||
| | Missing class terminator | `miss_class.n8` | `SYN_EXPECTED_END_CLASS` | | |||
| | Multiple statements on one line | `multi_stmt.n8` | `SYN_EXPECTED_EOL` | | |||
| | Interface method body | `iface_body.n8` | `SYN_INTERFACE_METHOD_BODY` | | |||
| | Malformed method parameter list | `bad_method.n8` | `SYN_EXPECTED_RPAREN` | | |||
| | Character literal with multiple characters | `bad_char.n8` | `LEX_BAD_CHAR_LITERAL` | | |||
| ## Future Parser Contract | |||
| Accepted fixtures must parse and pass declaration-level semantic checks. | |||
| Rejected fixtures assert the first primary diagnostic named in their sidecar; | |||
| later recovery diagnostics do not invalidate that expectation. | |||
| @@ -0,0 +1,23 @@ | |||
| class Buffer | |||
| private byte[4] data | |||
| public method set(index as word, value as byte) | |||
| self.data[index] = value | |||
| end method | |||
| public method get(index as word) as byte | |||
| return self.data[index] | |||
| end method | |||
| end class | |||
| program Main | |||
| var buffer as Buffer | |||
| var values as word[4] | |||
| var item as byte | |||
| buffer = new Buffer() | |||
| values[0] = 1 | |||
| buffer.set(values[0], 7) | |||
| item = buffer.get(values[0]) | |||
| print item | |||
| delete buffer | |||
| end program | |||
| @@ -0,0 +1,50 @@ | |||
| interface Printable | |||
| method print() | |||
| end interface | |||
| interface Resettable | |||
| method reset() | |||
| method active() as boolean | |||
| end interface | |||
| class Counter implements Printable, Resettable | |||
| private word value | |||
| private byte[4] history | |||
| public method init(start as word) | |||
| var copy as word | |||
| copy = start | |||
| self.value = copy | |||
| self.history[0] = 0 | |||
| end method | |||
| public method destroy() | |||
| self.value = 0 | |||
| return | |||
| end method | |||
| public method print() | |||
| print self.value | |||
| end method | |||
| public method reset() | |||
| self.value = 0 | |||
| end method | |||
| public method active() as boolean | |||
| if self.value <> 0 | |||
| return true | |||
| else | |||
| return false | |||
| end if | |||
| end method | |||
| end class | |||
| program Main | |||
| var count as Counter | |||
| var output as Printable | |||
| count = new Counter(10) | |||
| output = count | |||
| output.print() | |||
| delete output | |||
| end program | |||
| @@ -0,0 +1,34 @@ | |||
| class Formula | |||
| public method eval(a as integer, b as integer) as integer | |||
| var result as integer | |||
| result = -(a + b) * 2 / 1 | |||
| if not (a = b) and (a <> 0 or b >= a) | |||
| result = result + a - b | |||
| else | |||
| result = 0 | |||
| end if | |||
| if a < b | |||
| result = result + 1 | |||
| end if | |||
| if a <= b | |||
| result = result + 1 | |||
| end if | |||
| if a > b | |||
| result = result - 1 | |||
| end if | |||
| return result | |||
| end method | |||
| end class | |||
| program Main | |||
| var formula as Formula | |||
| var answer as integer | |||
| var glyph as char | |||
| formula = new Formula() | |||
| answer = formula.eval(-2, 3) | |||
| glyph = 'A' | |||
| print "answer\n" | |||
| print answer | |||
| print glyph | |||
| delete formula | |||
| end program | |||
| @@ -0,0 +1,24 @@ | |||
| class Runner implements Job | |||
| public method execute(code as byte) as boolean | |||
| if code = 0 | |||
| return false | |||
| else | |||
| return true | |||
| end if | |||
| end method | |||
| end class | |||
| interface Job | |||
| method execute(code as byte) as boolean | |||
| end interface | |||
| program Main | |||
| var runner as Runner | |||
| var job as Job | |||
| var ok as boolean | |||
| runner = new Runner() | |||
| job = runner | |||
| ok = job.execute(1) | |||
| print ok | |||
| delete job | |||
| end program | |||
| @@ -0,0 +1,3 @@ | |||
| // The smallest valid Nova128 compilation unit. | |||
| program Main | |||
| end program | |||
| @@ -0,0 +1,19 @@ | |||
| interface Marker | |||
| method mark() | |||
| end interface | |||
| class Empty implements Marker | |||
| word value | |||
| method mark() | |||
| return | |||
| end method | |||
| end class | |||
| program Main | |||
| var object as Empty | |||
| var marker as Marker | |||
| object = null | |||
| marker = null | |||
| delete marker | |||
| end program | |||
| @@ -0,0 +1,44 @@ | |||
| class Worker | |||
| private word value | |||
| public method init(start as word) | |||
| self.value = start | |||
| end method | |||
| public method tick(amount as word) | |||
| self.value = self.value + amount | |||
| end method | |||
| public method run(limit as word) as word | |||
| var i as word | |||
| var total as word | |||
| i = 0 | |||
| total = 0 | |||
| while i < limit | |||
| total = total + i | |||
| i = i + 1 | |||
| end while | |||
| if total > 100 | |||
| print "large" | |||
| else | |||
| print total | |||
| end if | |||
| for i = 0 to 3 step 1 | |||
| self.tick(i) | |||
| end for | |||
| for i = 0 to 1 | |||
| self.tick(i) | |||
| end for | |||
| return total | |||
| end method | |||
| end class | |||
| program Main | |||
| var worker as Worker | |||
| var total as word | |||
| worker = new Worker(1) | |||
| worker.tick(1) | |||
| total = worker.run(4) | |||
| print total | |||
| delete worker | |||
| end program | |||
| @@ -0,0 +1,4 @@ | |||
| diagnostic: LEX_BAD_CHAR_LITERAL | |||
| line: 3 | |||
| column: 11 | |||
| note: a character literal contains exactly one character or supported escape | |||
| @@ -0,0 +1,4 @@ | |||
| program Main | |||
| var glyph as char | |||
| glyph = 'AB' | |||
| end program | |||
| @@ -0,0 +1,3 @@ | |||
| diagnostic: SYN_EXPECTED_INTERFACE_NAME | |||
| line: 1 | |||
| reason: implements requires at least one interface name | |||
| @@ -0,0 +1,5 @@ | |||
| class Widget implements | |||
| end class | |||
| program Main | |||
| end program | |||
| @@ -0,0 +1,3 @@ | |||
| diagnostic: SYN_EXPECTED_IDENTIFIER | |||
| line: 1 | |||
| reason: an interface declaration requires a name on its header line | |||
| @@ -0,0 +1,6 @@ | |||
| interface | |||
| method print() | |||
| end interface | |||
| program Main | |||
| end program | |||
| @@ -0,0 +1,3 @@ | |||
| diagnostic: SYN_EXPECTED_RPAREN | |||
| line: 2 | |||
| reason: a method parameter list must end with a right parenthesis | |||
| @@ -0,0 +1,8 @@ | |||
| class Counter | |||
| public method set(value as word | |||
| self.value = value | |||
| end method | |||
| end class | |||
| program Main | |||
| end program | |||
| @@ -0,0 +1,3 @@ | |||
| diagnostic: SEM_DUPLICATE_NAME | |||
| line: 3 | |||
| reason: value is declared twice in the program-local namespace | |||
| @@ -0,0 +1,4 @@ | |||
| program Main | |||
| var value as word | |||
| var value as byte | |||
| end program | |||
| @@ -0,0 +1,3 @@ | |||
| diagnostic: SYN_INTERFACE_METHOD_BODY | |||
| line: 3 | |||
| reason: interface methods are signatures and cannot contain a body | |||
| @@ -0,0 +1,8 @@ | |||
| interface Printable | |||
| method print() | |||
| print "body" | |||
| end method | |||
| end interface | |||
| program Main | |||
| end program | |||
| @@ -0,0 +1,3 @@ | |||
| diagnostic: SYN_UNEXPECTED_EOL | |||
| line: 3 | |||
| reason: version 1 expressions cannot continue across a logical line | |||
| @@ -0,0 +1,5 @@ | |||
| program Main | |||
| var total as word | |||
| total = 1 + | |||
| 2 | |||
| end program | |||
| @@ -0,0 +1,3 @@ | |||
| diagnostic: SYN_EXPECTED_END_CLASS | |||
| line: 4 | |||
| reason: a program declaration cannot begin inside an open class | |||
| @@ -0,0 +1,5 @@ | |||
| class Counter | |||
| private word value | |||
| program Main | |||
| end program | |||
| @@ -0,0 +1,3 @@ | |||
| diagnostic: SYN_EXPECTED_END_PROGRAM | |||
| line: 3 | |||
| reason: end-of-file reached before the required end program terminator | |||
| @@ -0,0 +1,2 @@ | |||
| program Main | |||
| print "unfinished" | |||
| @@ -0,0 +1,3 @@ | |||
| diagnostic: SYN_EXPECTED_END_IF | |||
| line: 4 | |||
| reason: end program cannot terminate an open if statement | |||
| @@ -0,0 +1,4 @@ | |||
| program Main | |||
| if true | |||
| print "yes" | |||
| end program | |||
| @@ -0,0 +1,3 @@ | |||
| diagnostic: SYN_EXPECTED_END_METHOD | |||
| line: 4 | |||
| reason: end class cannot terminate an open method declaration | |||
| @@ -0,0 +1,7 @@ | |||
| class Counter | |||
| public method get() as word | |||
| return 0 | |||
| end class | |||
| program Main | |||
| end program | |||
| @@ -0,0 +1,3 @@ | |||
| diagnostic: SYN_EXPECTED_EOL | |||
| line: 2 | |||
| reason: version 1 permits only one statement on a logical line | |||
| @@ -0,0 +1,3 @@ | |||
| program Main | |||
| print 1 print 2 | |||
| end program | |||
| @@ -0,0 +1,72 @@ | |||
| $ErrorActionPreference = "Stop" | |||
| $root = $PSScriptRoot | |||
| $accepted = Join-Path $root "accepted" | |||
| $rejected = Join-Path $root "rejected" | |||
| $errors = New-Object System.Collections.Generic.List[string] | |||
| function Test-SourceProfile([System.IO.FileInfo]$file) { | |||
| if ($file.BaseName.Length -gt 12) { | |||
| $errors.Add("$($file.FullName): base name exceeds 12 characters") | |||
| } | |||
| $bytes = [IO.File]::ReadAllBytes($file.FullName) | |||
| if ($bytes.Length -gt 18999) { | |||
| $errors.Add("$($file.FullName): $($bytes.Length) bytes exceeds 18999") | |||
| } | |||
| $text = [Text.Encoding]::ASCII.GetString($bytes) | |||
| $lines = $text -split "`r`n|`n|`r" | |||
| if ($lines.Count -gt 599) { | |||
| $errors.Add("$($file.FullName): $($lines.Count) lines exceeds 599") | |||
| } | |||
| for ($i = 0; $i -lt $lines.Count; $i++) { | |||
| $count = [Text.Encoding]::ASCII.GetByteCount($lines[$i]) | |||
| if ($count -gt 80) { | |||
| $errors.Add("$($file.FullName):$($i + 1): $count bytes exceeds 80") | |||
| } | |||
| } | |||
| } | |||
| $acceptedSources = @(Get-ChildItem -LiteralPath $accepted -Filter "*.n8" -File) | |||
| $rejectedSources = @(Get-ChildItem -LiteralPath $rejected -Filter "*.n8" -File) | |||
| if ($acceptedSources.Count -eq 0) { $errors.Add("no accepted fixtures found") } | |||
| if ($rejectedSources.Count -eq 0) { $errors.Add("no rejected fixtures found") } | |||
| foreach ($file in $acceptedSources) { | |||
| Test-SourceProfile $file | |||
| $sidecar = [IO.Path]::ChangeExtension($file.FullName, ".expect") | |||
| if (Test-Path -LiteralPath $sidecar) { | |||
| $errors.Add("${sidecar}: accepted fixtures must not have diagnostic sidecars") | |||
| } | |||
| } | |||
| foreach ($file in $rejectedSources) { | |||
| Test-SourceProfile $file | |||
| $sidecar = [IO.Path]::ChangeExtension($file.FullName, ".expect") | |||
| if (-not (Test-Path -LiteralPath $sidecar)) { | |||
| $errors.Add("$($file.FullName): missing .expect sidecar") | |||
| continue | |||
| } | |||
| $expect = Get-Content -Raw -LiteralPath $sidecar | |||
| $matches = [regex]::Matches($expect, '(?m)^diagnostic: ([A-Z][A-Z0-9_]+)$') | |||
| if ($matches.Count -ne 1) { | |||
| $errors.Add("${sidecar}: expected exactly one symbolic diagnostic") | |||
| } | |||
| } | |||
| $orphanSidecars = @(Get-ChildItem -LiteralPath $rejected -Filter "*.expect" -File | | |||
| Where-Object { -not (Test-Path -LiteralPath ([IO.Path]::ChangeExtension($_.FullName, ".n8"))) }) | |||
| foreach ($orphan in $orphanSidecars) { | |||
| $errors.Add("$($orphan.FullName): orphan diagnostic sidecar") | |||
| } | |||
| if ($errors.Count -gt 0) { | |||
| $errors | ForEach-Object { Write-Error $_ } | |||
| exit 1 | |||
| } | |||
| Write-Host ("grammar fixtures ok: {0} accepted, {1} rejected" -f | |||
| $acceptedSources.Count, $rejectedSources.Count) | |||
Powered by TurnKey Linux.