Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

5.6KB


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

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.

Powered by TurnKey Linux.