Co-authored-by: Nilton Constantino <nilton.constantino@visma.com> Reviewed-on: #8
360 lines
7.1 KiB
Markdown
360 lines
7.1 KiB
Markdown
# Prometeu VM Memory Model v0
|
|
|
|
> **Status:** v0 (normative, implementer-facing)
|
|
>
|
|
> **Purpose:** define the runtime memory model required to execute PBS programs with stable bytecode.
|
|
>
|
|
> This specification describes the four memory regions and their interactions:
|
|
>
|
|
> 1. **Constant Pool** (read-only)
|
|
> 2. **Stack** (SAFE)
|
|
> 3. **Heap** (HIP storage bytes/slots)
|
|
> 4. **Gate Pool** (HIP handles, RC, metadata)
|
|
|
|
---
|
|
|
|
## 1. Design Goals
|
|
|
|
1. **Bytecode stability**: instruction meanings and data formats must remain stable across versions.
|
|
2. **Deterministic behavior**: no tracing GC; reclamation is defined by reference counts and safe points.
|
|
3. **Explicit costs**: HIP allocation and aliasing are explicit via gates.
|
|
4. **PBS alignment**: SAFE vs HIP semantics match PBS model.
|
|
|
|
---
|
|
|
|
## 2. Memory Regions Overview
|
|
|
|
### 2.1 Constant Pool (RO)
|
|
|
|
A program-wide immutable pool containing:
|
|
|
|
* integers, floats, bounded ints
|
|
* strings
|
|
* (optional in future) constant composite literals
|
|
|
|
Properties:
|
|
|
|
* read-only during execution
|
|
* indexed by `ConstId`
|
|
* VM bytecode uses `PUSH_CONST(ConstId)`
|
|
|
|
---
|
|
|
|
### 2.2 Stack (SAFE)
|
|
|
|
The **stack** contains:
|
|
|
|
* local variables (by slot)
|
|
* operand stack values for instruction evaluation
|
|
|
|
SAFE properties:
|
|
|
|
* values are copied by value
|
|
* no aliasing across variables unless the value is a gate handle
|
|
* stack values are reclaimed automatically when frames unwind
|
|
|
|
---
|
|
|
|
### 2.3 Heap (HIP storage)
|
|
|
|
The **heap** is a contiguous array of machine slots (e.g., `Value` slots), used only as **storage backing** for HIP objects.
|
|
|
|
Heap properties:
|
|
|
|
* heap cells are not directly addressable by bytecode
|
|
* heap is accessed only via **Gate Pool resolution**
|
|
|
|
The heap may implement:
|
|
|
|
* bump allocation (v0)
|
|
* free list (optional)
|
|
* compaction is **not** required in v0
|
|
|
|
---
|
|
|
|
### 2.4 Gate Pool (HIP handles)
|
|
|
|
The **gate pool** is the authoritative table mapping a small integer handle (`GateId`) to a storage object.
|
|
|
|
Gate Pool entry (conceptual):
|
|
|
|
```text
|
|
GateEntry {
|
|
alive: bool,
|
|
base: HeapIndex,
|
|
slots: u32,
|
|
|
|
strong_rc: u32,
|
|
weak_rc: u32, // optional in v0; may be reserved
|
|
|
|
type_id: TypeId, // required for layout + debug
|
|
flags: GateFlags,
|
|
}
|
|
```
|
|
|
|
Properties:
|
|
|
|
* `GateId` is stable during the lifetime of an entry
|
|
* `GateId` values may be reused only after an entry is fully reclaimed (v0 may choose to never reuse)
|
|
* any invalid `GateId` access is a **runtime trap** (deterministic)
|
|
|
|
---
|
|
|
|
## 3. SAFE vs HIP
|
|
|
|
### 3.1 SAFE
|
|
|
|
SAFE is stack-only execution:
|
|
|
|
* primitives
|
|
* structs / arrays as **value copies**
|
|
* temporaries
|
|
|
|
SAFE values are always reclaimed by frame unwinding.
|
|
|
|
### 3.2 HIP
|
|
|
|
HIP is heap-backed storage:
|
|
|
|
* storage objects allocated with `alloc`
|
|
* accessed through **gates**
|
|
* aliasing occurs by copying a gate handle
|
|
|
|
HIP values are reclaimed by **reference counting**.
|
|
|
|
---
|
|
|
|
## 4. Value Representation
|
|
|
|
### 4.1 Stack Values
|
|
|
|
A VM `Value` type must minimally support:
|
|
|
|
* `Int(i64)`
|
|
* `Float(f64)`
|
|
* `Bounded(u32)`
|
|
* `Bool(bool)`
|
|
* `String(ConstId)` or `StringRef(ConstId)` (strings live in const pool)
|
|
* `Gate(GateId)` ← **this is the only HIP pointer form in v0**
|
|
* `Unit`
|
|
|
|
**Rule:** any former `Ref` pointer type must be reinterpreted as `GateId`.
|
|
|
|
---
|
|
|
|
## 5. Allocation (`alloc`) and Gate Creation
|
|
|
|
### 5.1 Concept
|
|
|
|
PBS `alloc` creates:
|
|
|
|
1. heap backing storage (N slots)
|
|
2. a gate pool entry
|
|
3. returns a gate handle onto the stack
|
|
|
|
### 5.2 Required inputs
|
|
|
|
Allocation must be shape-explicit:
|
|
|
|
* `TypeId` describing the allocated storage type
|
|
* `slots` describing the storage size
|
|
|
|
### 5.3 Runtime steps (normative)
|
|
|
|
On `ALLOC(type_id, slots)`:
|
|
|
|
1. allocate `slots` contiguous heap cells
|
|
2. create gate entry:
|
|
|
|
* `base = heap_index`
|
|
* `slots = slots`
|
|
* `strong_rc = 1`
|
|
* `type_id = type_id`
|
|
3. push `Gate(gate_id)` to stack
|
|
|
|
### 5.4 Example
|
|
|
|
PBS:
|
|
|
|
```pbs
|
|
let v: Box<Vector> = box(Vector.ZERO);
|
|
```
|
|
|
|
Lowering conceptually:
|
|
|
|
* compute value `Vector.ZERO` (SAFE)
|
|
* `ALLOC(TypeId(Vector), slots=2)` → returns `Gate(g0)`
|
|
* store the two fields into heap via `STORE_GATE_FIELD`
|
|
|
|
---
|
|
|
|
## 6. Gate Access (Read/Write)
|
|
|
|
### 6.1 Access Principle
|
|
|
|
Heap is never accessed directly.
|
|
All reads/writes go through:
|
|
|
|
1. gate validation
|
|
2. gate → (base, slots)
|
|
3. bounds check
|
|
4. heap read/write
|
|
|
|
### 6.2 Read / Peek
|
|
|
|
`peek` copies from HIP storage to SAFE value.
|
|
|
|
* no RC changes
|
|
* no aliasing is created
|
|
|
|
### 6.3 Borrow (read-only view)
|
|
|
|
Borrow provides temporary read-only access.
|
|
|
|
* runtime may enforce with a borrow stack (debug)
|
|
* v0 may treat borrow as a checked read scope
|
|
|
|
### 6.4 Mutate (mutable view)
|
|
|
|
Mutate provides temporary mutable access.
|
|
|
|
* v0 may treat mutate as:
|
|
|
|
* read into scratch (SAFE)
|
|
* write back on `EndMutate`
|
|
|
|
Or (preferred later):
|
|
|
|
* direct heap writes within a guarded scope
|
|
|
|
---
|
|
|
|
## 7. Reference Counting (RC)
|
|
|
|
### 7.1 Strong RC
|
|
|
|
Strong RC counts how many **live gate handles** exist.
|
|
|
|
A `GateId` is considered live if it exists in:
|
|
|
|
* a stack slot
|
|
* a global slot
|
|
* a heap storage cell (HIP) (future refinement)
|
|
|
|
### 7.2 RC operations
|
|
|
|
When copying a gate handle into a new location:
|
|
|
|
* increment `strong_rc`
|
|
|
|
When a gate handle is removed/overwritten:
|
|
|
|
* decrement `strong_rc`
|
|
|
|
**Rule:** RC updates are required for any VM instruction that:
|
|
|
|
* assigns locals/globals
|
|
* stores into heap cells
|
|
* pops stack values
|
|
|
|
### 7.3 Release and Reclamation
|
|
|
|
When `strong_rc` reaches 0:
|
|
|
|
* gate entry becomes **eligible for reclamation**
|
|
* actual reclamation occurs at a **safe point**
|
|
|
|
Safe points (v0):
|
|
|
|
* end of frame
|
|
* explicit `FRAME_SYNC` (if present)
|
|
|
|
Reclamation:
|
|
|
|
1. mark gate entry `alive = false`
|
|
2. optionally add heap region to a free list
|
|
3. gate id may be recycled (optional)
|
|
|
|
**No tracing GC** is performed.
|
|
|
|
---
|
|
|
|
## 8. Weak Gates (Reserved / Optional)
|
|
|
|
v0 may reserve the field `weak_rc` but does not require full weak semantics.
|
|
|
|
If implemented:
|
|
|
|
* weak handles do not keep storage alive
|
|
* upgrading weak → strong requires a runtime check
|
|
|
|
---
|
|
|
|
## 9. Runtime Traps (Deterministic)
|
|
|
|
The VM must trap deterministically on:
|
|
|
|
* invalid `GateId`
|
|
* accessing a dead gate
|
|
* out-of-bounds offset
|
|
* type mismatch in a typed store/load (if enforced)
|
|
|
|
Traps must include:
|
|
|
|
* opcode
|
|
* span (if debug info present)
|
|
* message
|
|
|
|
---
|
|
|
|
## 10. Examples
|
|
|
|
### 10.1 Aliasing via gates
|
|
|
|
```pbs
|
|
let a: Box<Vector> = box(Vector.ZERO);
|
|
let b: Box<Vector> = a; // copy handle, RC++
|
|
|
|
mutate b {
|
|
it.x += 10;
|
|
}
|
|
|
|
let v: Vector = unbox(a); // observes mutation
|
|
```
|
|
|
|
Explanation:
|
|
|
|
* `a` and `b` are `GateId` copies
|
|
* mutation writes to the same heap storage
|
|
* `unbox(a)` peeks/copies storage into SAFE value
|
|
|
|
### 10.2 No HIP for strings
|
|
|
|
```pbs
|
|
let s: string = "hello";
|
|
```
|
|
|
|
* string literal lives in constant pool
|
|
* `s` is a SAFE value referencing `ConstId`
|
|
|
|
---
|
|
|
|
## 11. Conformance Checklist
|
|
|
|
A VM is conformant with this spec if:
|
|
|
|
* it implements the four memory regions
|
|
* `GateId` is the only HIP pointer form
|
|
* `ALLOC(type_id, slots)` returns `GateId`
|
|
* heap access is only via gate resolution
|
|
* RC increments/decrements occur on gate copies/drops
|
|
* reclamation happens only at safe points
|
|
|
|
---
|
|
|
|
## Appendix A — Implementation Notes (Non-normative)
|
|
|
|
* Start with bump-alloc heap + never-reuse GateIds (simplest v0)
|
|
* Add free list later
|
|
* Add borrow/mutate enforcement later as debug-only checks
|