11 KiB
Raw Blame History

Prometeu VM/Compiler/Bytecode — Atomic PR Plan (Junie-ready)

Entry point contract (confirmed)

  • The PBS entry point is /src/main/modules/main.pbs::frame(): void.
  • The compiler must inject FRAME_SYNC immediately before RET at the end of this function.
  • FRAME_SYNC is a signal only (no GC opcodes). The VM uses it as a safe point.
  • Missing entry point is a fatal compile error.

Goal: Deliver a sequence of small, incremental PRs that bring the implementation closer to the published PBS/VM specs.

Rules for Junie (strict)

  • Do not make product decisions. Only implement what is specified in the PR.
  • If anything is unclear, stop and ask. Do not improvise.
  • All new/modified code comments must be in English.
  • Each PR must be atomic and mergeable.
  • Each PR must include: Briefing, Target, Non-goals, Implementation notes, Tests, Acceptance criteria.

PR-02 — VM: Introduce GateId vs heap index and a minimal GatePool (no RC yet)

Briefing

Current runtime treats Value::Gate(x) as a direct heap base index. The spec requires a Gate Pool where GateId resolves to {alive, base, slots, type_id, rc...} and heap access happens only through gate validation + resolution.

This PR introduces the data model without changing ownership/RC yet, enabling later RC work.

Target

  • Add GateId type (e.g., u32) and a GateEntry struct with fields:

    • alive: bool
    • base: u32
    • slots: u32
    • type_id: u32 (store it, even if VM doesnt use it yet)
  • Add GatePool container to the VM state.

  • Update ALLOC(type_id, slots) to:

    • bump-alloc slots in heap
    • insert new GateEntry { alive: true, base, slots, type_id }
    • push Value::Gate(GateId) (GateId is index into gate_pool)
  • Update GATE_LOAD/GATE_STORE to:

    • validate GateId (in-range + alive)
    • bounds-check offset against slots
    • translate to heap index: base + offset

Non-goals

  • No reference counting yet.
  • No reclaim/free yet.
  • No enforcement of borrow/mutate rules yet.

Implementation notes

  • Keep heap as Vec<Value> (or existing representation). In this PR, do not change heap layout.

  • Add a helper: resolve_gate(gate_id) -> Result<&GateEntry, Trap> and resolve_gate_mut(...).

  • Define two new trap codes (or map onto existing ones if already defined):

    • TRAP_INVALID_GATE (gate_id out of range)
    • TRAP_DEAD_GATE (entry exists but alive == false)
  • Make sure the VM never reads/writes heap directly for gate operations without resolution.

Tests

  1. VM unit tests:

    • Allocate 2 gates; ensure they get distinct GateIds.
    • Store to gate offset 0, load back, assert equal.
    • Store to offset == slots (OOB) triggers OOB trap.
    • Use an invalid GateId triggers INVALID_GATE trap.
  2. If trap codes are new, test for the exact trap code.

Acceptance criteria

  • ALLOC returns GateId (not heap base).
  • GATE_LOAD/STORE uses gate_pool resolution.
  • Invalid/dead gate attempts trap deterministically.

PR-03 — VM: Add strong reference counting (RC) with deterministic retain/release semantics

Briefing

The spec requires strong RC tracking for gates and deterministic behavior for invalid/dead gates. Today GATE_RETAIN/GATE_RELEASE are no-ops (or effectively pop-only).

This PR implements strong_rc and updates runtime to adjust RC in well-defined places.

Target

  • Extend GateEntry with:

    • strong_rc: u32
  • Define semantics:

    • New allocation starts with strong_rc = 1 (gate value returned on stack owns 1 reference)
    • GATE_RETAIN: increment strong_rc
    • GATE_RELEASE: decrement strong_rc; if reaches 0 then mark gate alive=false and schedule reclaim

Non-goals

  • No compacting heap.
  • No weak refs.
  • Reclaim can be minimal (safe-point only) and may not actually reuse memory yet.

Implementation notes

  • Implement a reclaim_queue: Vec<GateId> in VM state.

  • On strong_rc reaching 0:

    • set alive = false
    • push gate_id into reclaim_queue
  • Safe point: drain reclaim queue on FRAME_SYNC.

    • Compiler guarantees FRAME_SYNC at the end of the PBS entry point main.pbs::frame(): void (PR-00).
  • In this PR, reclaim may simply:

    • overwrite the heap range [base, base+slots) with Value::Nil (or a safe default)
    • keep gate_id non-reusable

Tests

  1. RC lifecycle test:

    • alloc gate (rc=1)
    • retain (rc=2)
    • release (rc=1)
    • release (rc=0) => gate becomes dead
    • subsequent load/store traps DEAD_GATE
  2. Reclaim effect test (if you overwrite heap slots):

    • store a value, release to 0, run safe-point
    • confirm heap region is cleared (only if heap is inspectable in tests)

Acceptance criteria

  • GATE_RETAIN/RELEASE changes RC.
  • Gate transitions to dead at rc==0.
  • Dead gate access traps.
  • Reclaim happens at the chosen safe point.

PR-04 — VM: Automatic RC adjustments on stack/local/global/heap moves (no more “manual RC correctness”)

Briefing

Relying on explicit GATE_RETAIN/RELEASE everywhere is error-prone. The spec indicates RC must be adjusted on assignments/pops/stores. This PR makes RC correctness a VM invariant: when a gate value is copied into a slot, RC increments; when replaced/dropped, RC decrements.

Target

  • Implement centralized helpers:

    • inc_rc_if_gate(Value)
    • dec_rc_if_gate(Value)
  • Apply them in all places where values are moved or overwritten:

    • Stack PUSH/POP (when dropping values)
    • Local set/get if they clone values
    • Global set/get
    • GATE_STORE (heap cell overwrite)
    • Any instruction that overwrites an existing slot (e.g., STORE_LOCAL, STORE_GLOBAL, etc.)

Non-goals

  • No changes to compiler output.
  • No borrow/mutate enforcement.

Implementation notes

  • When writing into a slot:

    1. dec_rc_if_gate(old_value)
    2. write new value
    3. inc_rc_if_gate(new_value) only if the semantics is “copy into slot”
  • When moving (not copying) is possible, avoid double inc/dec.

  • If the VM uses clone() widely, be explicit about when RC should increase.

⚠️ If its unclear whether an opcode is “move” or “copy”, stop and ask (do not guess).

Tests

  1. Stack drop test:

    • alloc gate, push into local, pop stack, ensure rc doesnt underflow.
  2. Overwrite test:

    • local = gateA, then local = gateB
    • rc of gateA decremented
    • gateA becomes dead if no other refs
  3. Heap store overwrite test:

    • gateX stores gateA into offset 0
    • then stores gateB into same offset
    • rc adjusts accordingly

Acceptance criteria

  • No RC leaks on overwrites.
  • No premature dead gates when references still exist.
  • Tests cover overwrite in at least 2 storage kinds (local + heap).

PR-05 — VM: Implement (debug-mode) Borrow/Mutate/Peek scopes as observable state

Briefing

Currently GATE_BEGIN_PEEK/BORROW/MUTATE and GATE_END_* are no-ops. Even if v0 is permissive, the VM should at least track scope state to enable future enforcement and better diagnostics.

Target

  • Add per-gate “scope counters” (or a small state machine) in GateEntry:

    • peek_count: u32
    • borrow_count: u32
    • mutate_count: u32 (should be 0/1 if exclusive)
  • Implement opcodes to increment/decrement and validate balanced usage:

    • End without begin => trap (or panic if considered VM bug)
    • Negative underflow => trap
  • In debug builds, optionally enforce:

    • cannot begin_mutate when borrow_count>0
    • cannot begin_borrow when mutate_count>0

Non-goals

  • No compiler changes.
  • No runtime copy-back scratch buffers yet.

Implementation notes

  • Keep enforcement behind a feature flag or debug-only cfg.
  • Always keep counters balanced; mismatches should be deterministic.

Tests

  • Begin/End balance test per scope.
  • Debug-only conflict test (if enabled).

Acceptance criteria

  • Scopes are no longer no-ops.
  • Misbalanced begin/end produces deterministic error.

PR-06 — Bytecode/VM: Represent strings as ConstId (dedup + stable value size)

Briefing

Value::String(String) stores dynamic payload in runtime values. The spec direction prefers string refs into constant pools for stability and dedup. This PR migrates runtime string values to ConstId references.

Target

  • Add Value::StringRef(ConstId) (or Value::String(ConstId))

  • Ensure program image contains a string pool.

  • Update PUSH_CONST behavior for string constants:

    • push StringRef(id) instead of allocating a runtime String

Non-goals

  • No interning beyond the existing constant pool.
  • No changes to the source language.

Implementation notes

  • Decide where the string pool lives (ProgramImage / ConstPool).
  • Update debug printing and trap formatting if needed.

Tests

  • Constant string pushed twice references same ConstId.
  • Equality/comparison behavior remains unchanged (if supported).

Acceptance criteria

  • Strings in runtime values are pool references.
  • Existing programs using constants still run.

PR-07 — Compiler: Enforce import placement rules (top-of-file)

Briefing

The PBS module model specifies that imports must be at the top-level (and typically before other declarations). This PR makes the compiler reject invalid import placement to align with the module/linking spec.

Target

  • In parser/collector phase, detect imports appearing after non-import declarations.
  • Emit a diagnostic with a clear message and span.

Non-goals

  • No changes to how linking works.
  • No auto-fix.

Implementation notes

  • Track a boolean seen_non_import_decl during file scan.
  • When an import is encountered and the flag is set, produce an error diagnostic.

Tests

  • One file with valid imports at top => ok.
  • One file with import after a function/type => error.

Acceptance criteria

  • Compiler rejects invalid import placement with stable diagnostic.

Suggested merge order

  1. PR-00 (entry point validation + FRAME_SYNC injection)
  2. PR-01 (linker JMP relocation)
  3. PR-02 (GatePool + GateId)
  4. PR-03 (RC strong + retain/release + reclaim at FRAME_SYNC)
  5. PR-04 (automatic RC on moves/overwrites)
  6. PR-05 (scope tracking)
  7. PR-06 (StringRef ConstId)
  8. PR-07 (import placement enforcement)

Open questions (must be answered before PR-04)

Junie must stop and ask if any of these cannot be determined from the codebase.

  1. Which exact opcodes correspond to storage overwrites (locals/globals) in the current VM implementation (names may differ).
  2. Which trap codes are already defined for gate errors vs whether we introduce new ones.

Note: the safe point for reclaim is FRAME_SYNC (compiler inserts it at the end of fn frame(): void).