diff --git a/newLang/.gitignore b/newLang/.gitignore new file mode 100644 index 0000000..c41f18b --- /dev/null +++ b/newLang/.gitignore @@ -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 + diff --git a/newLang/AGENTS.md b/newLang/AGENTS.md new file mode 100644 index 0000000..4e1dea4 --- /dev/null +++ b/newLang/AGENTS.md @@ -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/.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. + diff --git a/newLang/README.md b/newLang/README.md new file mode 100644 index 0000000..f7fd836 --- /dev/null +++ b/newLang/README.md @@ -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. + diff --git a/newLang/cfg/display-clock-c128.cfg b/newLang/cfg/display-clock-c128.cfg new file mode 100644 index 0000000..107eb39 --- /dev/null +++ b/newLang/cfg/display-clock-c128.cfg @@ -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; +} + diff --git a/newLang/cfg/kernal-probe-c128.cfg b/newLang/cfg/kernal-probe-c128.cfg new file mode 100644 index 0000000..4140d98 --- /dev/null +++ b/newLang/cfg/kernal-probe-c128.cfg @@ -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; +} + diff --git a/newLang/cfg/minimal-c128.cfg b/newLang/cfg/minimal-c128.cfg new file mode 100644 index 0000000..bdcefa8 --- /dev/null +++ b/newLang/cfg/minimal-c128.cfg @@ -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; +} + diff --git a/newLang/cfg/mmu-probe-c128.cfg b/newLang/cfg/mmu-probe-c128.cfg new file mode 100644 index 0000000..0ba0a93 --- /dev/null +++ b/newLang/cfg/mmu-probe-c128.cfg @@ -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; +} + diff --git a/newLang/docs/architecture.md b/newLang/docs/architecture.md new file mode 100644 index 0000000..af6b8c0 --- /dev/null +++ b/newLang/docs/architecture.md @@ -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 + diff --git a/newLang/docs/decisions.md b/newLang/docs/decisions.md new file mode 100644 index 0000000..f7971e0 --- /dev/null +++ b/newLang/docs/decisions.md @@ -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? + diff --git a/newLang/docs/kernal-wrapper.md b/newLang/docs/kernal-wrapper.md new file mode 100644 index 0000000..ee35b3b --- /dev/null +++ b/newLang/docs/kernal-wrapper.md @@ -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`. diff --git a/newLang/docs/language-spec.md b/newLang/docs/language-spec.md new file mode 100644 index 0000000..6690b25 --- /dev/null +++ b/newLang/docs/language-spec.md @@ -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. + diff --git a/newLang/docs/mmu-probe.md b/newLang/docs/mmu-probe.md new file mode 100644 index 0000000..8010f3a --- /dev/null +++ b/newLang/docs/mmu-probe.md @@ -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`. diff --git a/newLang/docs/platform-display-clock.md b/newLang/docs/platform-display-clock.md new file mode 100644 index 0000000..03734f2 --- /dev/null +++ b/newLang/docs/platform-display-clock.md @@ -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: +- 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/`. + diff --git a/newLang/monitor/display-clock-trace.txt b/newLang/monitor/display-clock-trace.txt new file mode 100644 index 0000000..8b7527d --- /dev/null +++ b/newLang/monitor/display-clock-trace.txt @@ -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 + diff --git a/newLang/monitor/kernal-probe-trace.txt b/newLang/monitor/kernal-probe-trace.txt new file mode 100644 index 0000000..7121fc1 --- /dev/null +++ b/newLang/monitor/kernal-probe-trace.txt @@ -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 diff --git a/newLang/monitor/minimal-debug.txt b/newLang/monitor/minimal-debug.txt new file mode 100644 index 0000000..9f232c4 --- /dev/null +++ b/newLang/monitor/minimal-debug.txt @@ -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 + diff --git a/newLang/monitor/minimal-trace.txt b/newLang/monitor/minimal-trace.txt new file mode 100644 index 0000000..2498888 --- /dev/null +++ b/newLang/monitor/minimal-trace.txt @@ -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 + diff --git a/newLang/monitor/mmu-probe-debug.txt b/newLang/monitor/mmu-probe-debug.txt new file mode 100644 index 0000000..60f90f3 --- /dev/null +++ b/newLang/monitor/mmu-probe-debug.txt @@ -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 + diff --git a/newLang/monitor/mmu-probe-trace.txt b/newLang/monitor/mmu-probe-trace.txt new file mode 100644 index 0000000..e84d8e4 --- /dev/null +++ b/newLang/monitor/mmu-probe-trace.txt @@ -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 diff --git a/newLang/planning/agent-workflow.md b/newLang/planning/agent-workflow.md new file mode 100644 index 0000000..02b6ad0 --- /dev/null +++ b/newLang/planning/agent-workflow.md @@ -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 `.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. + diff --git a/newLang/planning/claims/E00-S01.md b/newLang/planning/claims/E00-S01.md new file mode 100644 index 0000000..2202840 --- /dev/null +++ b/newLang/planning/claims/E00-S01.md @@ -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 + diff --git a/newLang/planning/claims/E00-S02.md b/newLang/planning/claims/E00-S02.md new file mode 100644 index 0000000..471c522 --- /dev/null +++ b/newLang/planning/claims/E00-S02.md @@ -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. + diff --git a/newLang/planning/claims/E00-S03.md b/newLang/planning/claims/E00-S03.md new file mode 100644 index 0000000..5e2e443 --- /dev/null +++ b/newLang/planning/claims/E00-S03.md @@ -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 + diff --git a/newLang/planning/claims/E01-S01.md b/newLang/planning/claims/E01-S01.md new file mode 100644 index 0000000..4e8baf4 --- /dev/null +++ b/newLang/planning/claims/E01-S01.md @@ -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. + diff --git a/newLang/planning/claims/E01-S02.md b/newLang/planning/claims/E01-S02.md new file mode 100644 index 0000000..f112e9a --- /dev/null +++ b/newLang/planning/claims/E01-S02.md @@ -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). diff --git a/newLang/planning/claims/E01-S03.md b/newLang/planning/claims/E01-S03.md new file mode 100644 index 0000000..baefb77 --- /dev/null +++ b/newLang/planning/claims/E01-S03.md @@ -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. diff --git a/newLang/planning/claims/E01-S04.md b/newLang/planning/claims/E01-S04.md new file mode 100644 index 0000000..1ae1926 --- /dev/null +++ b/newLang/planning/claims/E01-S04.md @@ -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 + diff --git a/newLang/planning/claims/E07-S01.md b/newLang/planning/claims/E07-S01.md new file mode 100644 index 0000000..db5d071 --- /dev/null +++ b/newLang/planning/claims/E07-S01.md @@ -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 + diff --git a/newLang/planning/claims/README.md b/newLang/planning/claims/README.md new file mode 100644 index 0000000..0c69f7e --- /dev/null +++ b/newLang/planning/claims/README.md @@ -0,0 +1,9 @@ +# Story Claims + +Agents create one `.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. + diff --git a/newLang/planning/epics-and-stories.md b/newLang/planning/epics-and-stories.md new file mode 100644 index 0000000..253e473 --- /dev/null +++ b/newLang/planning/epics-and-stories.md @@ -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? + diff --git a/newLang/scripts/build-display-clock.ps1 b/newLang/scripts/build-display-clock.ps1 new file mode 100644 index 0000000..4b88a33 --- /dev/null +++ b/newLang/scripts/build-display-clock.ps1 @@ -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) + + diff --git a/newLang/scripts/build-kernal-probe.ps1 b/newLang/scripts/build-kernal-probe.ps1 new file mode 100644 index 0000000..5d22303 --- /dev/null +++ b/newLang/scripts/build-kernal-probe.ps1 @@ -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) + diff --git a/newLang/scripts/build-minimal.ps1 b/newLang/scripts/build-minimal.ps1 new file mode 100644 index 0000000..3e3a5d3 --- /dev/null +++ b/newLang/scripts/build-minimal.ps1 @@ -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) + diff --git a/newLang/scripts/build-mmu-probe.ps1 b/newLang/scripts/build-mmu-probe.ps1 new file mode 100644 index 0000000..0de7a12 --- /dev/null +++ b/newLang/scripts/build-mmu-probe.ps1 @@ -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) + diff --git a/newLang/scripts/prepare-disk.ps1 b/newLang/scripts/prepare-disk.ps1 new file mode 100644 index 0000000..d3368ae --- /dev/null +++ b/newLang/scripts/prepare-disk.ps1 @@ -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) +} + diff --git a/newLang/scripts/run-minimal.ps1 b/newLang/scripts/run-minimal.ps1 new file mode 100644 index 0000000..c9dd573 --- /dev/null +++ b/newLang/scripts/run-minimal.ps1 @@ -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) + diff --git a/newLang/scripts/run-mmu-probe.ps1 b/newLang/scripts/run-mmu-probe.ps1 new file mode 100644 index 0000000..b3a5053 --- /dev/null +++ b/newLang/scripts/run-mmu-probe.ps1 @@ -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) + diff --git a/newLang/scripts/verify-display-clock.ps1 b/newLang/scripts/verify-display-clock.ps1 new file mode 100644 index 0000000..05befa2 --- /dev/null +++ b/newLang/scripts/verify-display-clock.ps1 @@ -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' + diff --git a/newLang/scripts/verify-kernal-probe.ps1 b/newLang/scripts/verify-kernal-probe.ps1 new file mode 100644 index 0000000..cd57a5f --- /dev/null +++ b/newLang/scripts/verify-kernal-probe.ps1 @@ -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() + } +} diff --git a/newLang/scripts/verify-minimal.ps1 b/newLang/scripts/verify-minimal.ps1 new file mode 100644 index 0000000..a2ea3d7 --- /dev/null +++ b/newLang/scripts/verify-minimal.ps1 @@ -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() + } +} + diff --git a/newLang/scripts/verify-mmu-probe.ps1 b/newLang/scripts/verify-mmu-probe.ps1 new file mode 100644 index 0000000..ddb7219 --- /dev/null +++ b/newLang/scripts/verify-mmu-probe.ps1 @@ -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 ) ..." followed by ".C: ...". + $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() + } +} + diff --git a/newLang/src/platform/display-clock.s b/newLang/src/platform/display-clock.s new file mode 100644 index 0000000..108c1c8 --- /dev/null +++ b/newLang/src/platform/display-clock.s @@ -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 + 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 + 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 + jsr print_message ; KERNAL I/O while CPU fast + + ; Always return the machine to the conservative fallback. + lda #$00 + sta CLOCK + ldx #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 + diff --git a/newLang/src/platform/kernal-probe.s b/newLang/src/platform/kernal-probe.s new file mode 100644 index 0000000..6a93c29 --- /dev/null +++ b/newLang/src/platform/kernal-probe.s @@ -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..." +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 $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 diff --git a/newLang/src/platform/minimal.s b/newLang/src/platform/minimal.s new file mode 100644 index 0000000..9ad2290 --- /dev/null +++ b/newLang/src/platform/minimal.s @@ -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 + diff --git a/newLang/src/platform/mmu-probe.s b/newLang/src/platform/mmu-probe.s new file mode 100644 index 0000000..c0e0f47 --- /dev/null +++ b/newLang/src/platform/mmu-probe.s @@ -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 + diff --git a/newLang/tests/disk/prepare-disk.tests.ps1 b/newLang/tests/disk/prepare-disk.tests.ps1 new file mode 100644 index 0000000..7172e55 --- /dev/null +++ b/newLang/tests/disk/prepare-disk.tests.ps1 @@ -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" + diff --git a/newLang/tests/grammar/README.md b/newLang/tests/grammar/README.md new file mode 100644 index 0000000..47a313f --- /dev/null +++ b/newLang/tests/grammar/README.md @@ -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. + diff --git a/newLang/tests/grammar/accepted/arrays.n8 b/newLang/tests/grammar/accepted/arrays.n8 new file mode 100644 index 0000000..97c0fcc --- /dev/null +++ b/newLang/tests/grammar/accepted/arrays.n8 @@ -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 diff --git a/newLang/tests/grammar/accepted/decls.n8 b/newLang/tests/grammar/accepted/decls.n8 new file mode 100644 index 0000000..55a378f --- /dev/null +++ b/newLang/tests/grammar/accepted/decls.n8 @@ -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 diff --git a/newLang/tests/grammar/accepted/exprs.n8 b/newLang/tests/grammar/accepted/exprs.n8 new file mode 100644 index 0000000..472c85c --- /dev/null +++ b/newLang/tests/grammar/accepted/exprs.n8 @@ -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 diff --git a/newLang/tests/grammar/accepted/forward.n8 b/newLang/tests/grammar/accepted/forward.n8 new file mode 100644 index 0000000..840d948 --- /dev/null +++ b/newLang/tests/grammar/accepted/forward.n8 @@ -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 diff --git a/newLang/tests/grammar/accepted/minimal.n8 b/newLang/tests/grammar/accepted/minimal.n8 new file mode 100644 index 0000000..8f2be27 --- /dev/null +++ b/newLang/tests/grammar/accepted/minimal.n8 @@ -0,0 +1,3 @@ +// The smallest valid Nova128 compilation unit. +program Main +end program diff --git a/newLang/tests/grammar/accepted/nulls.n8 b/newLang/tests/grammar/accepted/nulls.n8 new file mode 100644 index 0000000..7672311 --- /dev/null +++ b/newLang/tests/grammar/accepted/nulls.n8 @@ -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 diff --git a/newLang/tests/grammar/accepted/stmts.n8 b/newLang/tests/grammar/accepted/stmts.n8 new file mode 100644 index 0000000..63b7a4d --- /dev/null +++ b/newLang/tests/grammar/accepted/stmts.n8 @@ -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 diff --git a/newLang/tests/grammar/rejected/bad_char.expect b/newLang/tests/grammar/rejected/bad_char.expect new file mode 100644 index 0000000..93ad5b8 --- /dev/null +++ b/newLang/tests/grammar/rejected/bad_char.expect @@ -0,0 +1,4 @@ +diagnostic: LEX_BAD_CHAR_LITERAL +line: 3 +column: 11 +note: a character literal contains exactly one character or supported escape diff --git a/newLang/tests/grammar/rejected/bad_char.n8 b/newLang/tests/grammar/rejected/bad_char.n8 new file mode 100644 index 0000000..a2b1aa7 --- /dev/null +++ b/newLang/tests/grammar/rejected/bad_char.n8 @@ -0,0 +1,4 @@ +program Main + var glyph as char + glyph = 'AB' +end program diff --git a/newLang/tests/grammar/rejected/bad_class.expect b/newLang/tests/grammar/rejected/bad_class.expect new file mode 100644 index 0000000..81755e9 --- /dev/null +++ b/newLang/tests/grammar/rejected/bad_class.expect @@ -0,0 +1,3 @@ +diagnostic: SYN_EXPECTED_INTERFACE_NAME +line: 1 +reason: implements requires at least one interface name diff --git a/newLang/tests/grammar/rejected/bad_class.n8 b/newLang/tests/grammar/rejected/bad_class.n8 new file mode 100644 index 0000000..af5581e --- /dev/null +++ b/newLang/tests/grammar/rejected/bad_class.n8 @@ -0,0 +1,5 @@ +class Widget implements +end class + +program Main +end program diff --git a/newLang/tests/grammar/rejected/bad_iface.expect b/newLang/tests/grammar/rejected/bad_iface.expect new file mode 100644 index 0000000..184eaca --- /dev/null +++ b/newLang/tests/grammar/rejected/bad_iface.expect @@ -0,0 +1,3 @@ +diagnostic: SYN_EXPECTED_IDENTIFIER +line: 1 +reason: an interface declaration requires a name on its header line diff --git a/newLang/tests/grammar/rejected/bad_iface.n8 b/newLang/tests/grammar/rejected/bad_iface.n8 new file mode 100644 index 0000000..807ec5a --- /dev/null +++ b/newLang/tests/grammar/rejected/bad_iface.n8 @@ -0,0 +1,6 @@ +interface + method print() +end interface + +program Main +end program diff --git a/newLang/tests/grammar/rejected/bad_method.expect b/newLang/tests/grammar/rejected/bad_method.expect new file mode 100644 index 0000000..de27c4c --- /dev/null +++ b/newLang/tests/grammar/rejected/bad_method.expect @@ -0,0 +1,3 @@ +diagnostic: SYN_EXPECTED_RPAREN +line: 2 +reason: a method parameter list must end with a right parenthesis diff --git a/newLang/tests/grammar/rejected/bad_method.n8 b/newLang/tests/grammar/rejected/bad_method.n8 new file mode 100644 index 0000000..bfc1648 --- /dev/null +++ b/newLang/tests/grammar/rejected/bad_method.n8 @@ -0,0 +1,8 @@ +class Counter + public method set(value as word + self.value = value + end method +end class + +program Main +end program diff --git a/newLang/tests/grammar/rejected/dup_decl.expect b/newLang/tests/grammar/rejected/dup_decl.expect new file mode 100644 index 0000000..d4f3fc9 --- /dev/null +++ b/newLang/tests/grammar/rejected/dup_decl.expect @@ -0,0 +1,3 @@ +diagnostic: SEM_DUPLICATE_NAME +line: 3 +reason: value is declared twice in the program-local namespace diff --git a/newLang/tests/grammar/rejected/dup_decl.n8 b/newLang/tests/grammar/rejected/dup_decl.n8 new file mode 100644 index 0000000..5ad0dda --- /dev/null +++ b/newLang/tests/grammar/rejected/dup_decl.n8 @@ -0,0 +1,4 @@ +program Main + var value as word + var value as byte +end program diff --git a/newLang/tests/grammar/rejected/iface_body.expect b/newLang/tests/grammar/rejected/iface_body.expect new file mode 100644 index 0000000..1065660 --- /dev/null +++ b/newLang/tests/grammar/rejected/iface_body.expect @@ -0,0 +1,3 @@ +diagnostic: SYN_INTERFACE_METHOD_BODY +line: 3 +reason: interface methods are signatures and cannot contain a body diff --git a/newLang/tests/grammar/rejected/iface_body.n8 b/newLang/tests/grammar/rejected/iface_body.n8 new file mode 100644 index 0000000..feb3487 --- /dev/null +++ b/newLang/tests/grammar/rejected/iface_body.n8 @@ -0,0 +1,8 @@ +interface Printable + method print() + print "body" + end method +end interface + +program Main +end program diff --git a/newLang/tests/grammar/rejected/line_cont.expect b/newLang/tests/grammar/rejected/line_cont.expect new file mode 100644 index 0000000..fd8efe4 --- /dev/null +++ b/newLang/tests/grammar/rejected/line_cont.expect @@ -0,0 +1,3 @@ +diagnostic: SYN_UNEXPECTED_EOL +line: 3 +reason: version 1 expressions cannot continue across a logical line diff --git a/newLang/tests/grammar/rejected/line_cont.n8 b/newLang/tests/grammar/rejected/line_cont.n8 new file mode 100644 index 0000000..291e167 --- /dev/null +++ b/newLang/tests/grammar/rejected/line_cont.n8 @@ -0,0 +1,5 @@ +program Main + var total as word + total = 1 + + 2 +end program diff --git a/newLang/tests/grammar/rejected/miss_class.expect b/newLang/tests/grammar/rejected/miss_class.expect new file mode 100644 index 0000000..dea9cc4 --- /dev/null +++ b/newLang/tests/grammar/rejected/miss_class.expect @@ -0,0 +1,3 @@ +diagnostic: SYN_EXPECTED_END_CLASS +line: 4 +reason: a program declaration cannot begin inside an open class diff --git a/newLang/tests/grammar/rejected/miss_class.n8 b/newLang/tests/grammar/rejected/miss_class.n8 new file mode 100644 index 0000000..2b2a2f9 --- /dev/null +++ b/newLang/tests/grammar/rejected/miss_class.n8 @@ -0,0 +1,5 @@ +class Counter + private word value + +program Main +end program diff --git a/newLang/tests/grammar/rejected/miss_end.expect b/newLang/tests/grammar/rejected/miss_end.expect new file mode 100644 index 0000000..d1cd26c --- /dev/null +++ b/newLang/tests/grammar/rejected/miss_end.expect @@ -0,0 +1,3 @@ +diagnostic: SYN_EXPECTED_END_PROGRAM +line: 3 +reason: end-of-file reached before the required end program terminator diff --git a/newLang/tests/grammar/rejected/miss_end.n8 b/newLang/tests/grammar/rejected/miss_end.n8 new file mode 100644 index 0000000..ff35ace --- /dev/null +++ b/newLang/tests/grammar/rejected/miss_end.n8 @@ -0,0 +1,2 @@ +program Main + print "unfinished" diff --git a/newLang/tests/grammar/rejected/miss_if.expect b/newLang/tests/grammar/rejected/miss_if.expect new file mode 100644 index 0000000..37a399b --- /dev/null +++ b/newLang/tests/grammar/rejected/miss_if.expect @@ -0,0 +1,3 @@ +diagnostic: SYN_EXPECTED_END_IF +line: 4 +reason: end program cannot terminate an open if statement diff --git a/newLang/tests/grammar/rejected/miss_if.n8 b/newLang/tests/grammar/rejected/miss_if.n8 new file mode 100644 index 0000000..ea43b2e --- /dev/null +++ b/newLang/tests/grammar/rejected/miss_if.n8 @@ -0,0 +1,4 @@ +program Main + if true + print "yes" +end program diff --git a/newLang/tests/grammar/rejected/miss_meth.expect b/newLang/tests/grammar/rejected/miss_meth.expect new file mode 100644 index 0000000..6cfb2f9 --- /dev/null +++ b/newLang/tests/grammar/rejected/miss_meth.expect @@ -0,0 +1,3 @@ +diagnostic: SYN_EXPECTED_END_METHOD +line: 4 +reason: end class cannot terminate an open method declaration diff --git a/newLang/tests/grammar/rejected/miss_meth.n8 b/newLang/tests/grammar/rejected/miss_meth.n8 new file mode 100644 index 0000000..0f70b57 --- /dev/null +++ b/newLang/tests/grammar/rejected/miss_meth.n8 @@ -0,0 +1,7 @@ +class Counter + public method get() as word + return 0 +end class + +program Main +end program diff --git a/newLang/tests/grammar/rejected/multi_stmt.expect b/newLang/tests/grammar/rejected/multi_stmt.expect new file mode 100644 index 0000000..30985da --- /dev/null +++ b/newLang/tests/grammar/rejected/multi_stmt.expect @@ -0,0 +1,3 @@ +diagnostic: SYN_EXPECTED_EOL +line: 2 +reason: version 1 permits only one statement on a logical line diff --git a/newLang/tests/grammar/rejected/multi_stmt.n8 b/newLang/tests/grammar/rejected/multi_stmt.n8 new file mode 100644 index 0000000..a34b652 --- /dev/null +++ b/newLang/tests/grammar/rejected/multi_stmt.n8 @@ -0,0 +1,3 @@ +program Main + print 1 print 2 +end program diff --git a/newLang/tests/grammar/validate.ps1 b/newLang/tests/grammar/validate.ps1 new file mode 100644 index 0000000..a9b949c --- /dev/null +++ b/newLang/tests/grammar/validate.ps1 @@ -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) +