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.