Преглед изворни кода

Give Nova128 interface values a delete path and add sequential editor/compiler launchers (E06-S06, E07-S02).

E06-S06 routes deletion through a fat reference to obj_delete behind the heap's
always-on validation, so a stale value is refused with IF_ERR_FREED instead of
reading a freed destructor, and adds the objects.n8 parser fixture.

E07-S02 shares one launcher implementation that locks the writable image and
waits for the emulator exit and image flush, with x64sc for the editor and x128
for the compiler.

Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
main
Daniel Covington пре 3 дана
родитељ
комит
f07c90547c
20 измењених фајлова са 1495 додато и 14 уклоњено
  1. +47
    -9
      newLang/docs/architecture.md
  2. +4
    -0
      newLang/docs/decisions.md
  3. +71
    -0
      newLang/docs/editor-workflow.md
  4. +21
    -3
      newLang/docs/memory-map.md
  5. +110
    -0
      newLang/planning/claims/E06-S06.md
  6. +97
    -0
      newLang/planning/claims/E07-S02.md
  7. +29
    -0
      newLang/scripts/build-lifetime-test.ps1
  8. +1
    -0
      newLang/scripts/build-parser-test.ps1
  9. +47
    -0
      newLang/scripts/launch-compiler.ps1
  10. +64
    -0
      newLang/scripts/launch-editor.ps1
  11. +143
    -0
      newLang/scripts/lib/Nova128Launcher.psm1
  12. +49
    -0
      newLang/scripts/verify-lifetime-test.ps1
  13. +1
    -0
      newLang/src/common/interfaces.inc
  14. +118
    -1
      newLang/src/runtime/heap.s
  15. +85
    -0
      newLang/src/runtime/ifelife.s
  16. +100
    -0
      newLang/tests/disk/launchers.tests.ps1
  17. +2
    -1
      newLang/tests/parser/parser-test.s
  18. +442
    -0
      newLang/tests/runtime/lifetime-test.s
  19. +5
    -0
      newLang/tests/semantic/objects.expect
  20. +59
    -0
      newLang/tests/semantic/objects.n8

+ 47
- 9
newLang/docs/architecture.md Прегледај датотеку

@@ -840,6 +840,37 @@ nothing, that deletion calls the destructor once with the fields intact and
restores the full free capacity, that a `$FFFF` instance fails without calling restores the full free capacity, that a `$FFFF` instance fails without calling
init, and that a class without a destructor still frees completely. init, and that a class without a destructor still frees completely.


### 8.4 Deleting through an interface (E06-S06)

Lifetime is the one operation an interface value performs on its object, so it
lives in `src/runtime/ifelife.s` rather than in the dispatch module: value
fixtures that never allocate stay free of the heap and construction code.

`if_delete` takes A/X = value pointer. A zero pointer, or a value whose object
half is zero, is a successful no-op (C=0, `IF_OK`) and the table half is
ignored — `delete null` through an interface must not call anything. Otherwise
the object half is validated with `heap_check` before any indirect transfer;
only a live block reaches `obj_delete`. A refusal returns C=1 with the new
`IF_ERR_FREED` and leaves `destroy_count` and the free capacity untouched, so
the promise is that a stale value is *refused*, not that memory is inspected
for a destructor that may already be gone.

On success the four value bytes are cleared. That keeps the ordinary single
owner path idempotent — a second `delete` through the same value is a no-op —
without adding a reference count. Aliases are ordinary four-byte copies and may
outlive the deletion; per §8 the detection mechanism is the heap's always-on
validation (range, used bit, even size, `$A55A` magic), which `heap_check`
exposes to this caller. Poisoning remains optional and is not what makes the
stale case correct.

The fixture deletes through a value, checks the destructor ran once and the
full capacity returned, treats a null value and a value with a table but no
object as no-ops, deletes one object through a second interface table and then
shows the first value refused with `IF_ERR_FREED`, and finally deletes through
an alias copy that outlived the deletion. `objects.n8` carries the same shape
through the parser: two interfaces, a class implementing both, and calls and a
`delete` through an interface variable.

## 9. KERNAL and Disk I/O ## 9. KERNAL and Disk I/O


The compiler uses documented KERNAL entry points through `diskio.s` wrappers. The compiler uses documented KERNAL entry points through `diskio.s` wrappers.
@@ -861,15 +892,22 @@ files. Version 1 does not link the editor and compiler in memory.


The development image workflow is strictly sequential: 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.
1. `scripts/launch-editor.ps1` copies `../vim/vim-improved.d81` to
`build/newlang-edit.d81`, launches `x64sc` and autostarts
`../vim/vim128.prg`;
2. the script returns only after the emulator exits and the image is flushed,
releasing its session lock;
3. `scripts/launch-compiler.ps1` prepares `build/newlang-dev.d81` (unless
`-SkipPrepare`), launches `x128` and autostarts `build/n128c.prg`;
4. close the compiler session before editing, inspecting or copying the image.

Both launchers share `scripts/lib/Nova128Launcher.psm1`, which holds an
exclusive `.newlang-session.lock` beside the writable image and reopens the
image exclusively before returning, so a second launcher cannot attach to a
disk a live session owns and no caller sees a partially written image. An
unreadable lock counts as held. `docs/editor-workflow.md` states the order,
recovery for a stale lock and the prohibition on two writable sessions; tests
create a fresh image per run and never write `../vim` disks.


## 11. Diagnostics and Faults ## 11. Diagnostics and Faults




+ 4
- 0
newLang/docs/decisions.md Прегледај датотеку

@@ -79,6 +79,10 @@ documented default. `Proposed` means a later change still needs approval.


| D146 | E06-S05 makes an interface call resolvable at its call site. `src/common/ifcalls.inc` fixes the call-site result codes (`IS_OK`, `IS_ERR_METHOD`), and `src/compiler/interfaces.s` gains `iface_slot` (A = interface name ID, X = method name ID -> A = the declaration slot from `C_MEM_SLOT`, or C=1 with `IS_ERR_METHOD` when the interface declares no such method, which covers a class or program ID too). At run time the slot indexes the table half of the fat reference, so the compile-time slot and the runtime table pointer together select the method; nothing at run time re-derives interface order. | The call sequence itself was already `if_call` (E06-S02) plus the frame ABI, so what was missing was a way for emission to learn the slot without re-deriving declaration order, and an end-to-end proof of the story's four behaviours. The x128 call fixture reaches two classes from one call site, two interfaces from one object by swapping only the table pointer, a nested call with a one-byte result, and a byte/word argument pair returning a word in A/X with the frame stack restored; it also checks `self` is the object, not the value record. | | D146 | E06-S05 makes an interface call resolvable at its call site. `src/common/ifcalls.inc` fixes the call-site result codes (`IS_OK`, `IS_ERR_METHOD`), and `src/compiler/interfaces.s` gains `iface_slot` (A = interface name ID, X = method name ID -> A = the declaration slot from `C_MEM_SLOT`, or C=1 with `IS_ERR_METHOD` when the interface declares no such method, which covers a class or program ID too). At run time the slot indexes the table half of the fat reference, so the compile-time slot and the runtime table pointer together select the method; nothing at run time re-derives interface order. | The call sequence itself was already `if_call` (E06-S02) plus the frame ABI, so what was missing was a way for emission to learn the slot without re-deriving declaration order, and an end-to-end proof of the story's four behaviours. The x128 call fixture reaches two classes from one call site, two interfaces from one object by swapping only the table pointer, a nested call with a one-byte result, and a byte/word argument pair returning a word in A/X with the frame stack restored; it also checks `self` is the object, not the value record. |


| D147 | E06-S06 gives interface values one lifetime operation in a separate module. `src/runtime/ifelife.s` adds `if_delete` (A/X = value pointer) so deletion does not force value and dispatch fixtures to link the heap, objects and construction. A zero pointer or a value whose object half is zero is a no-op with C=0/`IF_OK` (the table half is ignored, because deletion reaches the object). Otherwise the object half is passed to `heap_check` before anything indirect happens, and only a live block reaches `obj_delete`; the four value bytes are cleared so deleting through the same value twice is a no-op. A refused delete returns C=1 with the new `IF_ERR_FREED` (6) and calls and frees nothing. `heap_check` in `heap.s` exposes the always-on validation subset `heap_free` already performed: range, used bit, even size, and the `$A55A` used magic. | Architecture §8 says aliases may outlive a deletion and that the always-on heap validation, not poisoning, is what makes a stale pointer detectable. Checking the object half before the destructor address is read turns use-after-free into a defined refusal instead of reading a freed header, and clearing the value keeps the common single-owner path idempotent without adding a reference count. Keeping the check inside `heap.s` means the rule is written once; a switch to optional poisoning later cannot leave deletion unguarded. |

| D148 | E07-S02 puts both emulator launchers on one shared implementation, `scripts/lib/Nova128Launcher.psm1`. `Enter-Nova128ImageSession` takes an exclusive `.newlang-session.lock` beside the writable image, replaces a stale lock only when its PID is gone, and refuses when the holder is alive or the lock cannot even be read; `Wait-Nova128ImageFlush` reopens the image exclusively with retries so a launcher never returns while the drive is still writing; `Invoke-Nova128Emulator` starts the emulator, waits for exit, then waits for flush and releases the lock. `scripts/launch-editor.ps1` uses `x64sc` with `../vim/vim128.prg` and copies `../vim/vim-improved.d81` to `build/newlang-edit.d81`; `scripts/launch-compiler.ps1` uses `x128` with `build/n128c.prg` and prepares the development image unless `-SkipPrepare`. Both autostart a host-side program path, so no viewer binary has to be on the disk, and both accept `-LimitCycles`, `-Warp` and `-Screenshot`. | The story's rule is that the editor and the compiler never hold the same writable disk at once, which is only reliable if both launchers implement the lock and the flush wait identically. A lock file beside the image is detectable from either script and from a test without a shared daemon; an unreadable lock counts as held because an exclusive holder denies reads too. Copying the editor disk per session keeps `../vim/vim-improved.d81` a read-only original, matching E07-S01's rule for the compiler disk. `x64sc` names the VIC-II exit screenshot `-exitscreenshot` while `x128` uses `-exitscreenshotvicii`, so the option is chosen by emulator name. `docs/editor-workflow.md` carries the user-facing order and recovery rules, and `tests/disk/launchers.tests.ps1` proves the lock, the flush wait, both autostarts and that the source disk hash is unchanged. |

## Deferred ## Deferred


- Native C128/80-column Vim port - Native C128/80-column Vim port


+ 71
- 0
newLang/docs/editor-workflow.md Прегледај датотеку

@@ -0,0 +1,71 @@
---
type: workflow-guide
source: E07-S01 disk builder and E07-S02 launchers
---

# Editor and Compiler Workflow

Nova128 does not link the editor and compiler in memory. A development disk is
edited in one emulator session and compiled in another, strictly in sequence.
`scripts/prepare-disk.ps1` (E07-S01) copies `../vim/work.d81` to a disposable
image without ever writing the source; the launchers below add the two
sessions.

## Sequential rule

Never attach the same disk image writable to two emulators at once, and never
edit an image while a compiler session still has it open. VICE buffers DOS
writes, so a second session can read a half-written directory or overwrite the
first session's changes. The launchers enforce this with a session lock in the
image's folder (`.newlang-session.lock`); the lock holds the owning process and
its kind, and a lock left behind by a dead process is replaced automatically.

## Editor session

```text
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\launch-editor.ps1
```

- Autostarts `../vim/vim128.prg` in `x64sc` with the disk image attached as a
true drive.
- Copies `../vim/vim-improved.d81` to `build/newlang-edit.d81` first, so the
editor never opens the source image.
- Waits for `x64sc` to exit, then waits until the image can be opened
exclusively (the image has flushed) before returning.
- Verifies the source image's SHA-256 is unchanged.

Use `-EditorPrg` and `-SourceImage` to select another editor build or disk.
`-LimitCycles` and `-Screenshot` are for automated checks only; they end the
session early.

## Compiler session

```text
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\launch-compiler.ps1
```

- Rebuilds `build/newlang-dev.d81` from the clean source copy with
`prepare-disk.ps1` (unless `-SkipPrepare` is given), injecting
`build/n128c.prg`.
- Autostarts that compiler PRG in `x128` with the development image attached.
- Waits for `x128` to exit and for the image to flush before returning.

`-CompilerPrg` selects another compiler PRG. The launcher reports a missing
compiler program, source image, or VICE tool before starting anything.

## Recovery

- **Lock reported as still owned**: close the named session's emulator window
first; if the process is gone, the launcher replaces the stale lock.
- **Image still locked after the emulator exits**: wait for the DOS write to
finish; the launcher retries for 30 seconds before failing.
- **Source image changed**: stop and inspect `../vim` with `git status`; the
launchers only ever copy from it.
- **Wrong editor or compiler build**: pass `-EditorPrg`/`-CompilerPrg`
explicitly rather than editing the defaults.

## Limits

Autostart loads the program from the host file system, not from the disk; the
disk stays attached for the session's own file access. Physical C128 testing
and the full edit/compile/run walkthrough belong to E07-S04 and E08-S06.

+ 21
- 3
newLang/docs/memory-map.md Прегледај датотеку

@@ -154,7 +154,7 @@ its normal frame and reads them where the caller pushed them.
| Argument order | caller pushes four bytes right-to-left, object low at `FP+2` | | Argument order | caller pushes four bytes right-to-left, object low at `FP+2` |
| Four-byte result | `IF_RESULT` = `$C6-$C9`; the caller copies it before the next call | | Four-byte result | `IF_RESULT` = `$C6-$C9`; the caller copies it before the next call |
| Module zero page | `if_obj`, `if_tab`, `if_dst`, `if_src`, `if_vec`, `if_self` (link-allocated) | | Module zero page | `if_obj`, `if_tab`, `if_dst`, `if_src`, `if_vec`, `if_self` (link-allocated) |
| Module status | `if_error`: `IF_OK`, `IF_ERR_NULL`, `IF_ERR_TABLE`, `IF_ERR_ENTRY`, `IF_ERR_STACK` |
| Module status | `if_error`: `IF_OK`, `IF_ERR_NULL`, `IF_ERR_TABLE`, `IF_ERR_ENTRY`, `IF_ERR_STACK`, `IF_ERR_FREED` (lifetime, E06-S06) |
| Dispatch | `if_call` takes A/X = value, Y = slot; a refusal leaves A/X=0, C=1 and sets `if_error` | | Dispatch | `if_call` takes A/X = value, Y = slot; a refusal leaves A/X=0, C=1 and sets `if_error` |
| Dispatch floor | TSX at or below `FRAME_HW_MIN` (`$40`) is refused with `IF_ERR_STACK` | | Dispatch floor | TSX at or below `FRAME_HW_MIN` (`$40`) is refused with `IF_ERR_STACK` |
| Table emitter | `it_begin` A/X = first table address; `it_set_addr` A/X = address, Y = member row; `it_class` A = class name ID | | Table emitter | `it_begin` A/X = first table address; `it_set_addr` A/X = address, Y = member row; `it_class` A = class name ID |
@@ -174,8 +174,26 @@ its normal frame and reads them where the caller pushed them.
implements `if_conv`, `if_copy`, `if_is_null`, `if_slot`, `if_call`, implements `if_conv`, `if_copy`, `if_is_null`, `if_slot`, `if_call`,
`if_ret_store`, and `if_ret_load`. The module asserts at link time that `if_ret_store`, and `if_ret_load`. The module asserts at link time that
`IF_RESULT` follows the print vectors (`$C0-$C5`) and ends below the for-end `IF_RESULT` follows the print vectors (`$C0-$C5`) and ends below the for-end
values (`$D0`), and that `if_vec` does not cross a page. Interface lifetime
remains E06-S06.
values (`$D0`), and that `if_vec` does not cross a page.

## Interface Lifetime (E06-S06)

| Element | Value |
|---|---|
| `if_delete` | A/X = value pointer; deletes the object half once and clears the value |
| Null delete | zero pointer, or object half zero with any table, is C=0/`IF_OK` and calls nothing |
| Liveness check | `heap_check` on the object half before `obj_delete`; refusal is C=1/`IF_ERR_FREED` |
| `heap_check` | A/X = payload; validates range, used bit, even size, `$A55A` magic; C=1 with `HEAP_ERR_RANGE`/`DOUBLE`/`CORRUPT` |
| After a successful delete | the four value bytes are zero, so a repeated delete of the same value is a no-op |
| Aliases | an outliving copy is refused by the same check; there is no ownership count |
| Module zero page | `il_val`, `il_obj` (link-allocated) |
| Fixture | bank 0 heap `$4000-$4FFF`, code below `$4000`, five phase flags |

`src/runtime/ifelife.s` holds `if_delete`; it links `obj_delete`, `heap_check`
and the `if_error` word from `interfaces.s`. `src/common/interfaces.inc` fixes
`IF_ERR_FREED = 6`. The module has no fixed addresses, so generated programs
keep choosing the heap region, and the damage from a forged value is bounded by
the heap's own validation rather than by a private copy of it.


## Mappings ## Mappings




+ 110
- 0
newLang/planning/claims/E06-S06.md Прегледај датотеку

@@ -0,0 +1,110 @@
---
story: E06-S06
owner: codebuff
status: done
started: 2026-11-10
completed: 2026-11-10
depends-on: [E06-S05, E05-S03]
---

# E06-S06 Claim

## Intended change

Give an interface value one defined lifetime operation. `if_delete` (A/X = value
pointer) treats `delete null` as a no-op, validates the object half with the
heap's always-on check before any destructor address is read, deletes a live
object exactly once through `obj_delete`, and clears the value so a repeated
delete is a no-op. A refusal is C=1 with the new `IF_ERR_FREED`, and nothing is
called or freed. Aliases stay ordinary copies; the heap validation, not a
reference count, is what makes an outliving alias detectable.

## File area

- `src/runtime/ifelife.s` (new): `if_delete`
- `src/runtime/heap.s`: `heap_check` (exported validation subset of `heap_free`)
- `src/common/interfaces.inc`: `IF_ERR_FREED = 6`
- `tests/runtime/lifetime-test.s` (new), `scripts/build-lifetime-test.ps1`,
`scripts/verify-lifetime-test.ps1` (new)
- `tests/semantic/objects.n8`, `tests/semantic/objects.expect` (new)
- `tests/parser/parser-test.s`: `OBJECTS.N8` fixture, count 43
- `scripts/build-parser-test.ps1`: `objects.n8` expectation entry
- `docs/decisions.md` (D147), `docs/architecture.md` (8.4),
`docs/memory-map.md` (lifetime rows)
- `planning/claims/E06-S06.md`
- generated files under `build/`

## Acceptance evidence

- [x] `delete` through an interface invokes the destructor and frees once:
phase 1 allocates a live object through `obj_new`, deletes it by calling
`if_delete` on a value whose table is `tab_foo`, and requires
`destroy_count = 1`, `if_error = IF_OK`, `heap_free_total = $1000`, and the
four value bytes zero. Phase 3 does the same for an object reached through a
second interface table and checks the destructor count rises to exactly 2.
- [x] `delete null-interface` is a no-op: phase 2 deletes a zero value and a
value with a nonzero table but zero object, both with C=0, no destructor call
(`destroy_count` still 1) and no error.
- [x] Null calls fault predictably: phase 5 calls slot 0 through a null value
and requires C=1, `if_error = IF_ERR_NULL` and no method run (`hit_count`
still 0), then shows the next call on a live value succeeds and bumps
`hit_count` to 1. `if_call` itself is unchanged by this story; `if_delete`
reports C=1 only for a value that is not live.
- [x] A use-after-free scenario produces a detectable failure: phase 3 keeps the
first value after deleting through the second and requires C=1 with
`IF_ERR_FREED`, `destroy_count` still 2 and the free capacity unchanged, so a
freed object's destructor is never read. Phase 4 copies the value to an alias
first, deletes through the original value, and requires the alias to be
refused the same way with the destructor count still 3 and the free capacity
back to `$1000`. Detection is the always-on
`heap_check` (range, used bit, even size, `$A55A` magic) that also guards
`heap_free`, which is the mechanism architecture §8 promises; poisoning
remains optional.
- [x] `objects.n8` covers multiple interfaces end to end: the semantic fixture
declares `Printable` and `Resettable`, a `Counter` implementing both and a
`Ticker`, and uses interface variables for calls and for `delete`; the parser
accepts it with zero diagnostics and dumps 5 types, 10 members and 3 interface
slots.

## Coordination notes

- Depends on E06-S05 (slots and calls) and E05-S03 (`obj_new`/`obj_delete`);
both are complete. The dispatch path is untouched: `if_call` was not modified,
so lifetime cannot regress call behaviour.
- `if_delete` lives in its own module so the value/dispatch fixtures do not link
the heap, objects or construction code; `heap_check` lives in `heap.s` so the
validation rule exists once rather than as a second copy for deletion.
- Generated-code emission of `delete` through an interface is still compiler
work; this story fixes and proves the runtime contract.

## Work log

- `heap_check` first failed to assemble with three out-of-range branches; the
short branches now fall through to `jmp` trampolines, matching the project's
±127-byte convention.
- First fixture build stopped at a misuse of a marker routine as a macro
(`POINT_OK val_foo`); the zero check became a callable `chk_zero`.
- The first runtime run reported phase 1 as failed with the mailbox holding a
breadcrumb instead of an object pointer: the debug `MARK` macro reloads A, so
it must run before the descriptor is loaded, not between the load and
`obj_new`.
- Phase 2 then failed because BSS is not part of the PRG image and `val_null`
held whatever RAM had; the fixture now writes its null values explicitly,
which is also a reminder that generated programs must zero their own state.

## Completion

- Tests run: `scripts/verify-lifetime-test.ps1` (x128, fresh D81),
`scripts/verify-parser-test.ps1` (43 semantic/parser fixtures),
`git diff --check`.
- Results: the lifetime fixture reports PASS with all five flags and prints
`LIFETIME OK`; its PRG ends `$2C85`, below the `$4000` code ceiling. The
parser suite reports `verified parser: 43 fixtures`, with `objects.n8`
accepted as 5 types, 10 members, 3 interface slots and the lowest expression
stack pointer `$A7`.
- Remaining risks: `if_delete` trusts a value whose object half is live, so a
forged value whose object is a live block of another class reaches that
class's destructor through `obj_delete`'s normal descriptor read; the value's
table half is deliberately ignored, so deleting through a stale table is not
detected; and a value that is not four bytes in RAM cannot be cleared, which
the emitter must respect when `delete` is wired into code generation.

+ 97
- 0
newLang/planning/claims/E07-S02.md Прегледај датотеку

@@ -0,0 +1,97 @@
---
story: E07-S02
owner: codebuff
status: done
started: 2026-11-10
completed: 2026-11-10
depends-on: [E07-S01, E04-S01]
---

# E07-S02 Claim

## Intended change

Two launchers, one shared implementation: `scripts/launch-editor.ps1` starts
`x64sc` on a per-session copy of the Vim disk and autostarts the Vim program,
and `scripts/launch-compiler.ps1` starts `x128` and autostarts the compiler.
Both take an exclusive lock beside the writable image, wait for the emulator to
exit and for the image to be flushed before returning, and refuse a second
writable session. `docs/editor-workflow.md` states the order, recovery for a
stale lock, and the prohibition on simultaneous writable attachment.

## File area

- `scripts/lib/Nova128Launcher.psm1` (new): `Get-Nova128ViceTool`,
`Enter-Nova128ImageSession`, `Exit-Nova128ImageSession`,
`Wait-Nova128ImageFlush`, `Invoke-Nova128Emulator`
- `scripts/launch-editor.ps1`, `scripts/launch-compiler.ps1` (new)
- `tests/disk/launchers.tests.ps1` (new)
- `docs/editor-workflow.md` (new), `docs/decisions.md` (D148),
`docs/architecture.md` (§10)
- `planning/claims/E07-S02.md`
- generated files under `build/`

## Acceptance evidence

- [x] Editor launcher uses `x64sc` and autostarts Vim:
`scripts/launch-editor.ps1` defaults to `../vim/vim128.prg`, copies
`../vim/vim-improved.d81` to `build/newlang-edit.d81`, and passes the program
path to `x64sc` for autostart. `tests/disk/launchers.tests.ps1` runs it with
`-X64Sc` against a copy of the fixture disk and asserts the emulator exits,
the session lock is released and the image stays writable afterwards.
- [x] Compiler launcher uses `x128` and autostarts the compiler: it defaults to
`build/n128c.prg`, prepares `build/newlang-dev.d81` (copy plus injection)
unless `-SkipPrepare` is given, and autostarts the compiler in `x128`; the
test exercises the same sequence with `-SkipPrepare` on a disposable image.
- [x] Launchers wait for process/image flush before returning: the shared
`Exit-Nova128ImageSession` waits for emulator exit and `Wait-Nova128ImageFlush`
then reopens the image exclusively with retries; the test takes the lock in
the foreground and asserts the launcher refuses, and asserts the image is
reopenable for writing immediately after each launcher returns.
- [x] Documentation and warnings prohibit simultaneous writable attachment:
`docs/editor-workflow.md` and the launcher banners state that only one
writable session may exist and that the previous session must be closed
before editing or inspecting the disk; the lock makes the rule enforced, not
merely documented.

## Coordination notes

- Depends on E07-S01 (image preparation) and E04-S01 (container/autostart
shape); both are complete. The compiler launcher reuses E07-S01's prepare
script rather than duplicating injection.
- Autostart uses a host-side program path, so no viewer binary has to live on
the disk and `../vim` stays read-only; only the session's own image is
writable and only for drive 8.
- VICE names exit screenshots differently per emulator: `x64sc` accepts
`-exitscreenshot` while `x128` accepts `-exitscreenshotvicii`. The module
chooses by emulator name after the tests caught a missing screenshot.

## Work log

- The first test run failed the lock case with "cannot access the lock file"
instead of a lock message: a live holder opens the file without sharing, so
reading it can fail. An unreadable lock is now treated as held and reported as
"another Nova128 session already holds <image>".
- The screenshot assertion then failed for `x64sc` because `-exitscreenshotvicii`
is the `x128` spelling; the option is now selected by emulator name.
- The compiler test passed no disk, so the launcher correctly refused with
"disk image not found"; the test now copies its source image to the target
first, which is also what the real workflow does through E07-S01.

## Completion

- Tests run: `tests/disk/launchers.tests.ps1` (lock refusal, flush wait,
`x64sc` autostart, `x128` autostart, source-disk hash),
`scripts/verify-lifetime-test.ps1`, `scripts/verify-parser-test.ps1`,
`git diff --check`.
- Results: the launcher suite prints `launcher tests passed`, covering the disk
copy, lock refusal, flush wait, both autostarts and an unchanged source disk
(`0FF83A621B59B312F1BAE9F1D0785421500D22FD00F833A9CA918EBCA7105983`). Both
wrappers also allow `-LimitCycles`, `-Warp` and `-Screenshot` for unattended
runs.
- Remaining risks: the lock is advisory and identifies a holder by PID, so an
unrelated process that reuses a dead holder's PID in the window before the
lock is read is treated as alive; a launcher killed from the task manager
leaves the lock for the next run to replace; and Vim's own disk writes are
only guaranteed visible after VICE exits cleanly, which the flush wait assumes
but cannot force if the emulator is killed.

+ 29
- 0
newLang/scripts/build-lifetime-test.ps1 Прегледај датотеку

@@ -0,0 +1,29 @@
# E06-S06: build the interface lifetime fixture and its D81.
[CmdletBinding()]
param()

$ErrorActionPreference = "Stop"
$project = Split-Path $PSScriptRoot -Parent
Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force
$out = Join-Path $project "build\lifetime-test"
New-Item -ItemType Directory -Force -Path $out | Out-Null

$link = Invoke-Nova128Link -Name "lifetime-test" -BuildDir $out `
-Config (Join-Path $project "cfg\compiler.cfg") -Sources @(
(Join-Path $project "src\runtime\print.s"),
(Join-Path $project "src\runtime\frames.s"),
(Join-Path $project "src\runtime\heap.s"),
(Join-Path $project "src\runtime\objects.s"),
(Join-Path $project "src\runtime\construct.s"),
(Join-Path $project "src\runtime\interfaces.s"),
(Join-Path $project "src\runtime\ifelife.s"),
(Join-Path $project "tests\runtime\lifetime-test.s"))
if ((Get-Nova128Label $link.Labels "test_entry") -ne 0x1C0D) { throw "test_entry is not at `$1C0D" }
$end = Get-Nova128Label $link.Labels "test_end"
if ($end -ge 0x4000) {
throw ("lifetime-test ends at `$" + $end.ToString("X4") + "; it must stay below `$4000")
}

New-Nova128Disk -Path (Join-Path $out "lifetime-test.d81") -Files @(
@{ Name = "lifetime-test"; Path = $link.Program; Type = "prg" })
Write-Host ("built {0} (end {1:X4})" -f $link.Program, $end)

+ 1
- 0
newLang/scripts/build-parser-test.ps1 Прегледај датотеку

@@ -56,6 +56,7 @@ $script:ParserFixtures = [ordered]@{
"interfaces.n8" = @("$t\interfaces.n8", "iface", "$t\interfaces.expect") "interfaces.n8" = @("$t\interfaces.n8", "iface", "$t\interfaces.expect")
"ifacebad.n8" = @("$t\iface_bad.n8", "exact", "$t\iface_bad.expect") "ifacebad.n8" = @("$t\iface_bad.n8", "exact", "$t\iface_bad.expect")
"unresolved.n8" = @("$p\unresolved.n8", "exact", "$p\unresolved.expect") "unresolved.n8" = @("$p\unresolved.n8", "exact", "$p\unresolved.expect")
"objects.n8" = @("$t\objects.n8", "iface", "$t\objects.expect")
} }


$link = Invoke-Nova128Link -Name "parser-test" -BuildDir $out ` $link = Invoke-Nova128Link -Name "parser-test" -BuildDir $out `


+ 47
- 0
newLang/scripts/launch-compiler.ps1 Прегледај датотеку

@@ -0,0 +1,47 @@
# E07-S02: prepare a disposable development disk and autostart the Nova128
# compiler in x128.
#
# The session is strictly sequential: this launcher refuses to start while an
# editor session owns the same image, waits for x128 to exit and for the image
# to flush, and rebuilds the disposable image from its clean source.
[CmdletBinding()]
param(
[string]$CompilerPrg = "",
[string]$DiskImage = "",
[int]$LimitCycles = 0,
[string]$Screenshot = "",
[switch]$Warp,
[switch]$SkipPrepare
)

$ErrorActionPreference = "Stop"
$project = Split-Path $PSScriptRoot -Parent
$build = Join-Path $project "build"
Import-Module (Join-Path $PSScriptRoot "lib\Nova128Launcher.psm1") -Force

if ($CompilerPrg -eq "") { $CompilerPrg = Join-Path $build "n128c.prg" }
if ($DiskImage -eq "") { $DiskImage = Join-Path $build "newlang-dev.d81" }
$CompilerPrg = [IO.Path]::GetFullPath($CompilerPrg)
$DiskImage = [IO.Path]::GetFullPath($DiskImage)

if (-not (Test-Path -LiteralPath $CompilerPrg -PathType Leaf)) {
throw "compiler program not found: $CompilerPrg (build N128C.PRG first)"
}
$null = Get-Nova128ViceTool "x128"

if (-not $SkipPrepare) {
& (Join-Path $PSScriptRoot "prepare-disk.ps1") -OutputImage $DiskImage `
-CompilerPrg $CompilerPrg -RequireCompiler | Out-Host
}

$session = $null
try {
$session = Enter-Nova128ImageSession -Image $DiskImage -Kind "compiler"
$null = Invoke-Nova128Emulator -Emulator "x128" -Program $CompilerPrg `
-DiskImage $DiskImage -Screenshot $Screenshot -LimitCycles $LimitCycles -Warp:$Warp
} finally {
if ($session) { Exit-Nova128ImageSession -Session $session }
}

Write-Host "compiler session closed and the image is flushed"
Write-Host "close this session before editing or inspecting the disk again"

+ 64
- 0
newLang/scripts/launch-editor.ps1 Прегледај датотеку

@@ -0,0 +1,64 @@
# E07-S02: autostart the Vim editor in x64sc on a disposable disk copy.
#
# The session is strictly sequential: this launcher refuses to start while a
# compiler session owns the same image, waits for x64sc to exit and for the
# image to flush, and never writes the source disk it copies from.
[CmdletBinding()]
param(
[string]$EditorPrg = "",
[string]$SourceImage = "",
[string]$DiskImage = "",
[int]$LimitCycles = 0,
[string]$Screenshot = "",
[switch]$Warp
)

$ErrorActionPreference = "Stop"
$project = Split-Path $PSScriptRoot -Parent
$repository = Split-Path $project -Parent
$build = Join-Path $project "build"
Import-Module (Join-Path $PSScriptRoot "lib\Nova128Launcher.psm1") -Force

if ($EditorPrg -eq "") { $EditorPrg = Join-Path $repository "vim\vim128.prg" }
if ($SourceImage -eq "") { $SourceImage = Join-Path $repository "vim\vim-improved.d81" }
if ($DiskImage -eq "") { $DiskImage = Join-Path $build "newlang-edit.d81" }
$EditorPrg = [IO.Path]::GetFullPath($EditorPrg)
$DiskImage = [IO.Path]::GetFullPath($DiskImage)
if ($SourceImage) { $SourceImage = [IO.Path]::GetFullPath($SourceImage) }

if (-not (Test-Path -LiteralPath $EditorPrg -PathType Leaf)) {
throw "editor program not found: $EditorPrg (build Vim or pass -EditorPrg)"
}
$null = Get-Nova128ViceTool "x64sc"

$sourceHash = $null
New-Item -ItemType Directory -Force -Path (Split-Path $DiskImage -Parent) | Out-Null
if ($SourceImage) {
if (-not (Test-Path -LiteralPath $SourceImage -PathType Leaf)) {
throw "source disk image not found: $SourceImage"
}
if ([string]::Equals($SourceImage, $DiskImage, [StringComparison]::OrdinalIgnoreCase)) {
throw "the editor must never start on the source disk image"
}
$sourceHash = (Get-FileHash -LiteralPath $SourceImage -Algorithm SHA256).Hash
if (Test-Path -LiteralPath $DiskImage) { Remove-Item -LiteralPath $DiskImage -Force }
Copy-Item -LiteralPath $SourceImage -Destination $DiskImage
Write-Host "fresh editor disk: $DiskImage"
}

$session = $null
try {
$session = Enter-Nova128ImageSession -Image $DiskImage -Kind "editor"
$null = Invoke-Nova128Emulator -Emulator "x64sc" -Program $EditorPrg `
-DiskImage $DiskImage -Screenshot $Screenshot -LimitCycles $LimitCycles -Warp:$Warp
} finally {
if ($session) { Exit-Nova128ImageSession -Session $session }
}

if ($sourceHash) {
$after = (Get-FileHash -LiteralPath $SourceImage -Algorithm SHA256).Hash
if ($after -ne $sourceHash) { throw "the source disk image changed during the editor session" }
Write-Host "source disk unchanged: $after"
}
Write-Host "editor session closed and the image is flushed"
Write-Host "next: run scripts\launch-compiler.ps1; never attach two writable sessions"

+ 143
- 0
newLang/scripts/lib/Nova128Launcher.psm1 Прегледај датотеку

@@ -0,0 +1,143 @@
# Shared helpers for the sequential Nova128 editor/compiler launchers (E07-S02).
# Import-Module (Join-Path $PSScriptRoot "lib\Nova128Launcher.psm1") -Force

$script:ViceBin = "C:\Program Files\GTK3VICE-3.10-win64\bin"

function Get-Nova128ViceTool([string]$Name) {
$path = Join-Path $script:ViceBin "$Name.exe"
if (-not (Test-Path -LiteralPath $path)) { throw "$Name.exe not found at $path" }
return $path
}

# One writable session per image directory. The lock holds the owning PID and
# kind; a lock whose process is gone is stale and is replaced.
function Enter-Nova128ImageSession {
param(
[Parameter(Mandatory)][string]$Image,
[Parameter(Mandatory)][ValidateSet("editor", "compiler")][string]$Kind
)
$folder = Split-Path ([IO.Path]::GetFullPath($Image)) -Parent
New-Item -ItemType Directory -Force -Path $folder | Out-Null
$lockPath = Join-Path $folder ".newlang-session.lock"

if (Test-Path -LiteralPath $lockPath) {
# A live holder keeps the file open without sharing, so even reading it
# can fail. That is the same answer as a readable live PID.
$text = ""
try {
$text = Get-Content -Raw -LiteralPath $lockPath
} catch {
throw "another Nova128 session already holds $Image"
}
$owner = 0
$ownerKind = "another"
if ($text -match '(?m)^(\d+)\s+(\S+)') {
$owner = [int]$Matches[1]
$ownerKind = $Matches[2]
}
$alive = $owner -gt 0 -and $null -ne (Get-Process -Id $owner -ErrorAction SilentlyContinue)
if ($alive) {
throw "the $ownerKind session (pid $owner) still owns $Image; close it before starting another"
}
Remove-Item -LiteralPath $lockPath -Force
}

try {
$stream = [IO.File]::Open($lockPath, [IO.FileMode]::CreateNew,
[IO.FileAccess]::Write, [IO.FileShare]::None)
} catch {
throw "another Nova128 session already holds $Image"
}
$writer = [IO.StreamWriter]::new($stream)
$writer.WriteLine("$PID $Kind")
$writer.Flush()
return [pscustomobject]@{ Path = $lockPath; Stream = $stream; Writer = $writer
Image = $Image; Kind = $Kind }
}

function Exit-Nova128ImageSession {
param([Parameter(Mandatory)]$Session)
try {
$Session.Writer.Dispose()
$Session.Stream.Dispose()
} finally {
if (Test-Path -LiteralPath $Session.Path) {
Remove-Item -LiteralPath $Session.Path -Force
}
if ($Session.Image) {
Wait-Nova128ImageFlush -Image $Session.Image
}
}
}

# The emulator has exited, but DOS writes may still be in flight. Return only
# when the image can be opened exclusively.
function Wait-Nova128ImageFlush {
param(
[Parameter(Mandatory)][string]$Image,
[int]$TimeoutSeconds = 30
)
if (-not (Test-Path -LiteralPath $Image)) { return }
$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
while ($true) {
try {
$stream = [IO.File]::Open($Image, [IO.FileMode]::Open,
[IO.FileAccess]::Read, [IO.FileShare]::None)
$stream.Dispose()
return
} catch {
if ([DateTime]::UtcNow -ge $deadline) {
throw "the emulator exited but $Image is still locked (image not flushed)"
}
Start-Sleep -Milliseconds 200
}
}
}

# Start an emulator and wait for it to exit. Autostart uses the host program
# path, so the program is deterministic; the disk image stays attached for the
# session's own reads and writes.
function Invoke-Nova128Emulator {
param(
[Parameter(Mandatory)][ValidateSet("x64sc", "x128")][string]$Emulator,
[Parameter(Mandatory)][string]$Program,
[string]$DiskImage,
[string]$Screenshot,
[int]$LimitCycles = 0,
[switch]$Warp
)
$tool = Get-Nova128ViceTool $Emulator
if (-not (Test-Path -LiteralPath $Program)) { throw "program not found: $Program" }

$arguments = @()
if ($DiskImage) {
if (-not (Test-Path -LiteralPath $DiskImage)) {
throw "disk image not found: $DiskImage"
}
$arguments += @("-drive8type", "1581", "-drive8truedrive", "-8",
('"' + $DiskImage + '"'))
}
$arguments += @("-autostart", ('"' + $Program + '"'), "-autostart-warp",
"-sounddev", "dummy")
if ($Warp) { $arguments += "-warp" }
if ($LimitCycles -gt 0) { $arguments += @("-limitcycles", "$LimitCycles") }
if ($Screenshot) {
if (Test-Path -LiteralPath $Screenshot) {
Remove-Item -LiteralPath $Screenshot -Force
}
# x64sc names the VIC-II exit screenshot differently from x128.
$shotOption = if ($Emulator -eq "x64sc") { "-exitscreenshot" } else { "-exitscreenshotvicii" }
$arguments += @($shotOption, ('"' + $Screenshot + '"'))
}

Write-Host ("launching {0} with {1}" -f $Emulator, $Program)
$process = Start-Process -FilePath $tool -ArgumentList $arguments -PassThru -Wait
if ($Screenshot -and -not (Test-Path -LiteralPath $Screenshot)) {
throw "$Emulator did not create the exit screenshot $Screenshot"
}
if ($DiskImage) { Wait-Nova128ImageFlush -Image $DiskImage }
return $process.ExitCode
}

Export-ModuleMember -Function Get-Nova128ViceTool, Enter-Nova128ImageSession,
Exit-Nova128ImageSession, Wait-Nova128ImageFlush, Invoke-Nova128Emulator

+ 49
- 0
newLang/scripts/verify-lifetime-test.ps1 Прегледај датотеку

@@ -0,0 +1,49 @@
# E06-S06: run the interface lifetime fixture in x128 and check its mailbox.
[CmdletBinding()]
param([int]$Port = 6561, [int]$TimeoutSeconds = 180)

$ErrorActionPreference = "Stop"
$project = Split-Path $PSScriptRoot -Parent
Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force
& (Join-Path $PSScriptRoot "build-lifetime-test.ps1")

$out = Join-Path $project "build\lifetime-test"
$disk = Join-Path $out "lifetime-test.d81"
$failures = @()

$session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port `
-Screenshot (Join-Path $out "lifetime-test-vicii.png")
try {
$mb = Wait-Nova128Mailbox $session -TimeoutSeconds $TimeoutSeconds
Start-Sleep -Milliseconds 500
$screenRam = Read-Nova128Memory $session 0x0400 1000
} finally {
Stop-Nova128X128 $session -SettleSeconds 0
}

if ($mb[2] -ne 0x03) { $failures += "producer $($mb[2]) expected 3" }
if ($mb[3] -ne 0xA5) { $failures += ("status {0:X2} expected A5" -f $mb[3]) }
if ($mb[4] -ne 0 -or $mb[5] -ne 0) { $failures += "code $($mb[4] + 256 * $mb[5]) expected 0" }
for ($i = 0; $i -lt 5; $i++) {
if ($mb[8 + $i] -ne 1) { $failures += "phase $i flag is $($mb[8 + $i]), expected 1" }
}

$rows = for ($r = 0; $r -lt 25; $r++) {
$chars = for ($c = 0; $c -lt 40; $c++) {
$code = $screenRam[$r * 40 + $c]
if ($code -le 0x1F) { [char]($code + 0x40) } elseif ($code -le 0x3F) { [char]$code } else { '?' }
}
(-join $chars).TrimEnd()
}
if (-not ($rows -contains "LIFETIME OK")) {
$failures += "screen does not contain 'LIFETIME OK'"
}

if ($failures) {
throw ("interface lifetime verification failed (code {0}, addr {1:X4}):`n " -f ($mb[4] + 256 * $mb[5]), ($mb[6] + 256 * $mb[7]) +
($failures -join "`n ") + "`n mailbox: " + ($mb -join ' ') + "`n screen:`n " + (($rows | Where-Object { $_ }) -join "`n "))
}
Write-Host "screen:"
$rows | Where-Object { $_ } | ForEach-Object { Write-Host " |$_" }
Write-Host "mailbox: producer 3, PASS, code 0, all 5 lifetime flags set"
Write-Host "verified delete through one and two interface values, null delete, alias detection, and the null call fault"

+ 1
- 0
newLang/src/common/interfaces.inc Прегледај датотеку

@@ -37,5 +37,6 @@ IF_ERR_TABLE = 2 ; non-null object with no method table
IF_ERR_ENTRY = 3 ; method table slot holds address zero IF_ERR_ENTRY = 3 ; method table slot holds address zero
IF_ERR_STACK = 4 ; 6502 stack too low for a dispatch frame IF_ERR_STACK = 4 ; 6502 stack too low for a dispatch frame
IF_ERR_RANGE = 5 ; array element address wrapped past $FFFF IF_ERR_RANGE = 5 ; array element address wrapped past $FFFF
IF_ERR_FREED = 6 ; delete through a value whose object is gone


.endif .endif

+ 118
- 1
newLang/src/runtime/heap.s Прегледај датотеку

@@ -12,7 +12,7 @@
.setcpu "6502" .setcpu "6502"
.include "../common/heap.inc" .include "../common/heap.inc"


.export heap_init, heap_alloc, heap_free
.export heap_init, heap_alloc, heap_free, heap_check
.export heap_base, heap_end, heap_free_head, heap_free_total, heap_error .export heap_base, heap_end, heap_free_head, heap_free_total, heap_error


.segment "ZEROPAGE" .segment "ZEROPAGE"
@@ -400,6 +400,123 @@ heap_zero:
@done: @done:
rts rts


; A/X = payload pointer. C=0 when it names an allocated block whose header is
; inside the heap, marked used, even-sized, and carries the used magic. C=1
; with HEAP_ERR_DOUBLE when the block is already free, HEAP_ERR_RANGE when it
; lies outside the heap, or HEAP_ERR_CORRUPT. This is the same validation
; heap_free applies, exposed so a delete can refuse a stale object pointer
; before a destructor is read out of freed memory.
heap_check:
sta heap_arg
stx heap_arg+1
lda #HEAP_OK
sta heap_error
lda heap_arg
ora heap_arg+1
bne :+
jmp @corrupt
: lda heap_arg
and #1
beq :+
jmp @corrupt
: lda heap_arg
sec
sbc #HEAP_HEADER_SIZE
sta heap_block
lda heap_arg+1
sbc #0
sta heap_block+1
bcs :+
jmp @corrupt
: lda heap_block+1
cmp heap_base+1
bcs @base_high_ok
jmp @range
@base_high_ok:
beq @base_same
jmp @above
@base_same:
lda heap_block
cmp heap_base
bcs @above
jmp @range
@above: lda heap_block+1
cmp heap_end+1
bcc @in_range
bne @end_high_bad
lda heap_block
cmp heap_end
bcc @in_range
@end_high_bad:
jmp @corrupt
@in_range:
lda heap_block
sta heap_ptr
lda heap_block+1
sta heap_ptr+1
ldy #0
lda (heap_ptr),y
sta heap_bsize
iny
lda (heap_ptr),y
sta heap_bsize+1
and #$80
bne :+
jmp @double
: lda heap_bsize+1
and #$7F
sta heap_bsize+1
ora heap_bsize
beq @corrupt
lda heap_bsize
and #1
bne @corrupt
lda heap_block
clc
adc heap_bsize
sta heap_tmp
lda heap_block+1
adc heap_bsize+1
sta heap_tmp+1
bcs @far_corrupt
lda heap_tmp+1
cmp heap_end+1
bcc @end_ok
bne @end_bad
lda heap_tmp
cmp heap_end
bcc @end_ok
@end_bad:
jmp @corrupt
@end_ok:
ldy #2
lda (heap_ptr),y
cmp #$5A
bne @magic_bad
iny
lda (heap_ptr),y
cmp #$A5
bne @magic_bad
clc
rts
@far_corrupt:
jmp @corrupt
@magic_bad:
jmp @corrupt
@double:
lda #HEAP_ERR_DOUBLE
jmp @set
@range:
lda #HEAP_ERR_RANGE
jmp @set
@corrupt:
lda #HEAP_ERR_CORRUPT
@set: sta heap_error
lda #$00
tax
sec
rts

; A/X = payload pointer. Null is a successful no-op. On success the block is ; A/X = payload pointer. Null is a successful no-op. On success the block is
; free and coalesced; on failure C=1 and heap_error is set. ; free and coalesced; on failure C=1 and heap_error is set.
heap_free: heap_free:


+ 85
- 0
newLang/src/runtime/ifelife.s Прегледај датотеку

@@ -0,0 +1,85 @@
; ifelife.s - deleting through an interface value (E06-S06).
;
; Kept apart from interfaces.s so value transfer and dispatch fixtures do not
; have to link the heap, objects, and construction. The architecture's
; lifetime policy is that aliases may outlive a deletion and that the always-on
; heap validation is the detection mechanism; this module applies that policy
; to an interface value instead of introducing ownership counts.
;
; API:
; if_delete A/X = interface value pointer. A canonical null value (or a
; zero pointer) is a successful no-op, which is `delete null`
; through an interface. Otherwise the object half is checked with
; heap_check before anything jumps: a value whose object has
; already been freed is refused with IF_ERR_FREED instead of
; reading a destructor address out of freed memory. A live object
; is deleted once by obj_delete, and the four value bytes are
; cleared so deleting through the same value again is a no-op.
; The table half is ignored: deletion reaches the object, not the
; interface.
;
; C=0 means the object was deleted or there was nothing to delete; C=1 with
; if_error = IF_ERR_FREED means the value did not name a live object and
; nothing was called or freed.

.setcpu "6502"
.include "../common/interfaces.inc"

.import obj_delete
.import heap_check
.import if_error

.export if_delete

.segment "ZEROPAGE"
il_val: .res 2 ; the interface value being deleted
il_obj: .res 2 ; its object half

.segment "CODE"

; A/X = value pointer. See the API above.
if_delete:
sta il_val
stx il_val+1
lda #IF_OK
sta if_error
lda il_val
ora il_val+1
beq @ok
ldy #IF_OBJECT
lda (il_val),y
sta il_obj
iny
lda (il_val),y
sta il_obj+1
ora il_obj
beq @ok
lda il_obj
ldx il_obj+1
jsr heap_check
bcc @live
jmp @freed
@live: lda il_obj
ldx il_obj+1
jsr obj_delete
bcc @clear
jmp @freed
@clear: ldy #$00
lda #$00
@clr: sta (il_val),y
iny
cpy #IF_VALUE_SIZE
bne @clr
@ok: lda #$00
tax
clc
rts
@freed: lda #IF_ERR_FREED
sta if_error
lda #$00
tax
sec
rts

.segment "BSS"
if_del_error: .res 1 ; reserved debug breadcrumb (unused)

+ 100
- 0
newLang/tests/disk/launchers.tests.ps1 Прегледај датотеку

@@ -0,0 +1,100 @@
# E07-S02 integration checks for scripts/launch-editor.ps1 and
# scripts/launch-compiler.ps1.
[CmdletBinding()]
param()

$ErrorActionPreference = "Stop"

$project = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$repository = Split-Path $project -Parent
$launchEditor = Join-Path $project "scripts\launch-editor.ps1"
$launchCompiler = Join-Path $project "scripts\launch-compiler.ps1"
$modulePath = Join-Path $project "scripts\lib\Nova128Launcher.psm1"
$source = Join-Path $repository "vim\vim-improved.d81"
$minimal = Join-Path $project "build\minimal.prg"
$testRoot = Join-Path $project "build\launcher-test"
$diskA = Join-Path $testRoot "editor.d81"
$diskB = Join-Path $testRoot "compiler.d81"
$editorShot = Join-Path $testRoot "editor.png"
$compilerShot = Join-Path $testRoot "compiler.png"

foreach ($path in @($launchEditor, $launchCompiler, $modulePath)) {
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "missing $path" }
}
if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { throw "source image missing: $source" }

New-Item -ItemType Directory -Force -Path $testRoot | Out-Null
& (Join-Path $project "scripts\build-minimal.ps1") | Out-Host
if (-not (Test-Path -LiteralPath $minimal -PathType Leaf)) { throw "minimal PRG missing" }

Import-Module $modulePath -Force

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 it succeeded" }
if ($caught -notmatch $Pattern) { throw "failure '$caught' did not match '$Pattern'" }
}

$sourceHashBefore = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash

# 1: missing inputs are reported before any emulator starts.
Assert-FailsWith -Pattern 'editor program not found' -Action {
& $launchEditor -EditorPrg (Join-Path $testRoot "missing.prg") `
-SourceImage $source -DiskImage $diskA
}
Assert-FailsWith -Pattern 'compiler program not found' -Action {
& $launchCompiler -CompilerPrg (Join-Path $testRoot "missing.prg") `
-DiskImage $diskB -SkipPrepare
}

# 2: an editor session owns the image for the duration; a live lock blocks the
# compiler launcher on the same folder.
Copy-Item -LiteralPath $source -Destination $diskA -Force
$held = Enter-Nova128ImageSession -Image $diskA -Kind "editor"
try {
Assert-FailsWith -Pattern 'still owns|already holds' -Action {
& $launchCompiler -CompilerPrg $minimal -DiskImage $diskA -SkipPrepare
}
} finally {
Exit-Nova128ImageSession -Session $held
}
if (Test-Path -LiteralPath (Join-Path $testRoot ".newlang-session.lock")) {
throw "the session lock was not released"
}

# 3: flush waiting refuses a locked image and accepts a released one.
$partner = [IO.File]::Open($diskA, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None)
try {
Assert-FailsWith -Pattern 'still locked' -Action {
Wait-Nova128ImageFlush -Image $diskA -TimeoutSeconds 1
}
} finally {
$partner.Dispose()
}
Wait-Nova128ImageFlush -Image $diskA -TimeoutSeconds 2

# 4: the editor launcher autostarts its program in x64sc on a fresh copy and
# returns only after the process exits and the image flushes.
& $launchEditor -EditorPrg $minimal -SourceImage $source -DiskImage $diskA `
-LimitCycles 2000000 -Screenshot $editorShot -Warp
if (-not (Test-Path -LiteralPath $editorShot)) { throw "editor launcher produced no screenshot" }

# 5: the compiler launcher autostarts its program in x128 and leaves the
# disposable image flushed and unlocked.
Copy-Item -LiteralPath $source -Destination $diskB -Force
& $launchCompiler -CompilerPrg $minimal -DiskImage $diskB -LimitCycles 2000000 `
-Screenshot $compilerShot -Warp -SkipPrepare
if (-not (Test-Path -LiteralPath $compilerShot)) { throw "compiler launcher produced no screenshot" }
Wait-Nova128ImageFlush -Image $diskB -TimeoutSeconds 2

$sourceHashAfter = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash
if ($sourceHashAfter -ne $sourceHashBefore) { throw "the Vim source image changed during the session tests" }

Write-Host "launcher tests passed"
Write-Host "editor disk copy, lock refusal, flush wait, x64sc autostart, x128 autostart"
Write-Host "source disk unchanged: $sourceHashAfter"

+ 2
- 1
newLang/tests/parser/parser-test.s Прегледај датотеку

@@ -578,7 +578,8 @@ fixtures:
FIXTURE "INTERFACES.N8" FIXTURE "INTERFACES.N8"
FIXTURE "IFACEBAD.N8" FIXTURE "IFACEBAD.N8"
FIXTURE "UNRESOLVED.N8" FIXTURE "UNRESOLVED.N8"
FIXTURE_COUNT = 42
FIXTURE "OBJECTS.N8"
FIXTURE_COUNT = 43
TRACE_INDEX = 28 ; PREC.N8 TRACE_INDEX = 28 ; PREC.N8


messages: messages:


+ 442
- 0
newLang/tests/runtime/lifetime-test.s Прегледај датотеку

@@ -0,0 +1,442 @@
; E06-S06: interface lifetime and failure paths in x128.
;
; Runs under CR=$0E with a 4K heap at $4000. It checks:
; - delete through an interface value calls the class destructor once, frees
; the complete object, and clears the value so it cannot delete twice;
; - delete on a canonical null interface value is a no-op;
; - one object reachable through two interface tables is freed once, and the
; other value is refused afterwards;
; - an alias (a copy of the value) that outlives the deletion is a detectable
; failure: IF_ERR_FREED, no second destructor call, no heap change;
; - a null interface call faults predictably with IF_ERR_NULL, executes no
; target, and leaves dispatch working.
;
; MB_DATA flags 0..4 report the five phases.

.setcpu "6502"
.include "../../src/common/c128.inc"
.include "../../src/common/mailbox.inc"
.include "../../src/common/print.inc"
.include "../../src/common/heap.inc"
.include "../../src/common/class.inc"
.include "../../src/common/frames.inc"
.include "../../src/common/interfaces.inc"

.import pr_char, pr_cr, pr_text
.import heap_init, heap_free_total, heap_error
.import frame_init
.import obj_new, obj_init_ptr
.import if_call, if_error
.import if_delete

HEAP_BASE = $4000
HEAP_PAGES = 16

.macro TEXT src, len
lda #<src
ldx #>src
ldy #len
jsr pr_text
.endmacro

.macro PHASE n
lda #n
sta MB_CODE
lda #$00
sta MB_CODE+1
.endmacro

.macro MARK n
lda #n
sta MB_ADDR
.endmacro

.macro FAIL_CS
bcc :+
jmp @fail
:
.endmacro

.macro FAIL_CC
bcs :+
jmp @fail
:
.endmacro

.macro FAIL_NE
beq :+
jmp @fail
:
.endmacro

; A word in A/X must equal the two bytes at addr.
.macro CHK_WORD addr
cmp addr
bne @bad
txa
cmp addr+1
beq @ok
@bad: jmp @fail
@ok:
.endmacro

.segment "LOADADDR"
.word $1C01

.segment "EXEHDR"
.word basic_end
.word 10
.byte $9E
.byte "7181"
.byte $00
basic_end:
.word $0000

.segment "MAILBOX"
.res MB_SIZE

.segment "STARTUP"
.export test_entry, test_end
test_entry:
php
sei
ldx #MB_SIZE - 1
lda #$00
@clr: sta MB_BASE,x
dex
bpl @clr
lda #MB_MAGIC0_VALUE
sta MB_MAGIC0
lda #MB_MAGIC1_VALUE
sta MB_MAGIC1
lda #MB_PROD_PROGRAM
sta MB_PRODUCER
lda #MB_ST_RUNNING
sta MB_STATUS
lda #$00
sta MB_CODE
sta MB_CODE+1
sta destroy_count
sta hit_count
ldx #$04
@flags: sta MB_DATA,x
dex
bpl @flags

lda MMU_CR
sta saved_cr
lda #CR_RAM0_KERNAL
sta MMU_CR
lda #PR_CLEAR
jsr pr_char
TEXT title, title_len
jsr pr_cr

PHASE 1
lda #$00
ldx #>HEAP_BASE
ldy #HEAP_PAGES
jsr heap_init
FAIL_CS
MARK $01
jsr frame_init
lda #$00
sta obj_init_ptr
sta obj_init_ptr+1

; 1: delete through an interface frees once and clears the value.
MARK $02
lda #<desc_life
ldx #>desc_life
jsr obj_new
FAIL_CS
sta live_obj
stx live_obj+1
jsr make_foo
MARK $03
lda #<val_foo
ldx #>val_foo
jsr if_delete
bcc :+
lda if_error
sta MB_CODE
lda #$08
sta MB_ADDR
jmp @fail
: MARK $04
lda if_error
FAIL_NE
lda destroy_count
cmp #1
FAIL_NE
MARK $05
lda destroy_self
cmp live_obj
FAIL_NE
lda destroy_self+1
cmp live_obj+1
FAIL_NE
lda heap_free_total
cmp #$00
FAIL_NE
lda heap_free_total+1
cmp #$10
FAIL_NE
MARK $06
jsr chk_zero
FAIL_CS
MARK $07
lda #$01
sta MB_DATA+0

; 2: delete on a canonical null value is a no-op.
PHASE 2
MARK $11
; BSS is not part of the PRG image, so state the null values here.
ldx #$03
lda #$00
@init2: sta val_null,x
sta val_objnull,x
dex
bpl @init2
lda #$C0
sta val_objnull+IF_TABLE
lda #<val_null
ldx #>val_null
jsr if_delete
FAIL_CS
MARK $12
lda if_error
FAIL_NE
lda destroy_count
cmp #1
FAIL_NE
; A value with a table but no object is still null for deletion.
lda #<val_objnull
ldx #>val_objnull
jsr if_delete
FAIL_CS
lda destroy_count
cmp #1
FAIL_NE
lda #$01
sta MB_DATA+1

; 3: one object, two interface tables: the first delete frees it, the
; second value is refused rather than reading a freed destructor.
PHASE 3
lda #<desc_life
ldx #>desc_life
jsr obj_new
FAIL_CS
sta live_obj
stx live_obj+1
jsr make_foo
lda live_obj
sta val_bar
lda live_obj+1
sta val_bar+1
lda #<tab_bar
sta val_bar+IF_TABLE
lda #>tab_bar
sta val_bar+IF_TABLE+1
lda #<val_bar
ldx #>val_bar
jsr if_delete
FAIL_CS
lda destroy_count
cmp #2
FAIL_NE
lda heap_free_total+1
cmp #$10
FAIL_NE
lda #<val_foo
ldx #>val_foo
jsr if_delete
FAIL_CC
lda if_error
cmp #IF_ERR_FREED
FAIL_NE
lda destroy_count
cmp #2
FAIL_NE
lda heap_free_total+1
cmp #$10
FAIL_NE
lda #$01
sta MB_DATA+2

; 4: an alias copy that outlives the deletion is detected.
PHASE 4
lda #<desc_life
ldx #>desc_life
jsr obj_new
FAIL_CS
sta live_obj
stx live_obj+1
jsr make_foo
ldx #$00
@alias: lda val_foo,x
sta val_alias,x
inx
cpx #IF_VALUE_SIZE
bne @alias
lda #<val_foo
ldx #>val_foo
jsr if_delete
FAIL_CS
lda destroy_count
cmp #3
FAIL_NE
lda #<val_alias
ldx #>val_alias
jsr if_delete
FAIL_CC
lda if_error
cmp #IF_ERR_FREED
FAIL_NE
lda destroy_count
cmp #3
FAIL_NE
lda heap_free_total
cmp #$00
FAIL_NE
lda heap_free_total+1
cmp #$10
FAIL_NE
lda #$01
sta MB_DATA+3

; 5: a null call faults before any target runs, and dispatch recovers.
PHASE 5
lda #<val_null
ldx #>val_null
ldy #$00
jsr if_call
FAIL_CC
lda if_error
cmp #IF_ERR_NULL
FAIL_NE
lda hit_count
FAIL_NE

lda #<desc_life
ldx #>desc_life
jsr obj_new
FAIL_CS
sta live_obj
stx live_obj+1
jsr make_foo
lda #<val_foo
ldx #>val_foo
ldy #$00
jsr if_call
FAIL_NE
lda if_error
FAIL_NE
lda hit_count
cmp #1
FAIL_NE
lda #<val_foo
ldx #>val_foo
jsr if_delete
FAIL_CS
lda destroy_count
cmp #4
FAIL_NE
lda #$01
sta MB_DATA+4

TEXT ok_msg, ok_len
jsr pr_cr
lda #MB_ST_PASS
sta MB_STATUS
lda #$00
sta MB_CODE
sta MB_CODE+1
jmp @restore

@fail:
lda #MB_ST_FAIL
sta MB_STATUS
TEXT fail_msg, fail_len
jsr pr_cr
@restore:
lda saved_cr
sta MMU_CR
plp
rts

; val_foo = live_obj + tab_foo.
make_foo:
lda live_obj
sta val_foo
lda live_obj+1
sta val_foo+1
lda #<tab_foo
sta val_foo+IF_TABLE
lda #>tab_foo
sta val_foo+IF_TABLE+1
rts

; C=1 unless the four bytes at val_foo are zero.
chk_zero:
ldx #$00
@z: lda val_foo,x
bne @nz
inx
cpx #IF_VALUE_SIZE
bne @z
clc
rts
@nz: sec
rts

.segment "CODE"
; Destructor: records how often and for which object it ran.
destroy_life:
sta destroy_self
stx destroy_self+1
inc destroy_count
rts

; Slot 0 target used by the null-call check.
m_hit:
inc hit_count
lda #$11
ldx #$00
rts

.segment "DATA"
desc_life:
.word 4 ; header + one word field
.word destroy_life
.byte 0
.word 0
.byte 0
.assert * - desc_life = CLASS_DESC_SIZE, error, "life descriptor size"

tab_foo: .word m_hit
tab_bar: .word m_hit
.assert tab_bar - tab_foo = IF_SLOT_SIZE, error, "one slot per table"

.segment "RODATA"
title: .byte "NOVA128 LIFETIME TEST"
title_len = * - title
ok_msg: .byte "LIFETIME OK"
ok_len = * - ok_msg
fail_msg: .byte "LIFETIME FAIL"
fail_len = * - fail_msg

.segment "BSS"
saved_cr: .res 1
live_obj: .res 2
destroy_count: .res 1
destroy_self: .res 2
hit_count: .res 1
val_foo: .res IF_VALUE_SIZE
val_bar: .res IF_VALUE_SIZE
val_alias: .res IF_VALUE_SIZE
val_null: .res IF_VALUE_SIZE
val_objnull: .res IF_VALUE_SIZE
test_end:

+ 5
- 0
newLang/tests/semantic/objects.expect Прегледај датотеку

@@ -0,0 +1,5 @@
// E06-S06: two classes, two implementations of Printable, and Counter
// satisfying both Printable and Resettable end to end.
printable slot 0 print
printable slot 1 tag
resettable slot 0 reset

+ 59
- 0
newLang/tests/semantic/objects.n8 Прегледај датотеку

@@ -0,0 +1,59 @@
interface Printable
method print()
method tag() as byte
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 print()
print self.value
end method

public method tag() as byte
return 1
end method

public method reset()
self.value = 0
end method
end class

class Ticker implements Printable
public method print()
print 7
end method

public method tag() as byte
return 2
end method
end class

program Main
var count as Counter
var tick as Ticker
var shown as Printable
var zeroed as Resettable
var ok as boolean

count = new Counter(10)
tick = new Ticker()
shown = count
shown.print()
ok = shown.tag() = 1
zeroed = count
zeroed.reset()
shown = tick
shown.print()
ok = shown.tag() = 2
delete shown
delete count
end program

Loading…
Откажи
Сачувај

Powered by TurnKey Linux.