Frames & Protos in Jade VM (v0.1)¶
Dokumenttyp: Technischer Überblick / Implementationsleitfaden (aus Spezifikationen abgeleitet)
Ziel: Das mentale Mapping von JDL-Semantik → Proto → Frame → VM-Ausführung → JME-Allokationen und Cleanup.
Normative Quellen (v0.1):
- 05-vm-instruction-set.md (Core Instruction Set inkl. Call, Return, Move, Drop, Trap)
- 11-jadevalue-und-register-layout.md (JadeValue nur an Grenzen, Register-Layout als Byte-Buffer)
- 03-runtime.md (Memory Policies: Value/Ref/Arena/Pool, with-Scoping, Arena-Regeln, Copy-Mechanismus)
- jade-compiler-db-spec.md (CompilerDb Query-System, Determinismus, Caching)
Architekturprinzip (Subsystem-Kapselung):
- 09-subsystem-kapselung-und-zustandsintegritaet.md beschreibt verbindlich: Subsysteme interagieren über Queries und Commands, kein direkter Zugriff auf interne Datenstrukturen.
Hinweis zur Oberflächensyntax: In Projektquellen kann JDL/Jade-Syntax historisch variieren. In diesem Dokument werden JDL-Snippets ausschließlich zur Intention gezeigt; die normative Ebene für die VM ist der Core-Opcode-Stream (Instruktionsset-Spec).
1. Begriffe und Grundmodell¶
1.1 Proto (Function Prototype)¶
Ein Proto ist das kompilerte Funktionsobjekt: Bytecode + Const-Pool + Register-Layout + Debug-Info.
Er ist statisch (immutable) und wird von mehreren Ausführungen derselben Funktion geteilt.
Aus 05-vm-instruction-set.md:
- Register-VM: jede Funktion hat R0..R(n-1).
- Call erzeugt einen neuen Frame mit eigener Registerdatei.
- Rückgabe ist ein einzelnes JadeValue.
1.2 Frame (Activation Record)¶
Ein Frame ist die Laufzeitinstanz einer Funktionsausführung. Er existiert zwischen:
- Eintritt über Call, und
- Verlassen über Return oder Trap/Unwind.
Ein Frame ist nicht ein {}-Block-Scope. {}-Scopes sind lexikalische Semantik und werden (falls nötig) durch den Compiler zu konkreten Aktionen (z.B. Drop) im Instruktionsstream abgebildet.
1.3 JadeValue (Boundary Container)¶
JadeValue ist nur ein Transportcontainer an Systemgrenzen:
- Call/Return
- Intrinsics
- FFI / Value Bridge
Innerhalb einer Funktion gibt es keine JadeValue-Slots. Stattdessen:
- Registerfile = flacher Byte-Buffer mit compile-time bekanntem Layout (Offsets/Sizes pro Register).
2. Was gehört zu einem Frame?¶
Ein Frame gehört zu genau einer Funktionsausführung.
Alles, was die Funktion während ihrer Ausführung benötigt, liegt im Frame:
proto: Referenz auf den Function Prototypepc: Program Counter im Proto-Codecaller: Link (oder Index) zum Caller-Frame- Registerfile: Byte-Buffer, der alle Registerwerte enthält (Args, Locals, Temporaries)
Was nicht den Frame bestimmt:
- {}-Scopes, with-Blöcke, if-Blöcke. Das sind Semantikgrenzen im Code, aber keine eigenen Frames.
Wie wirken Scopes dennoch auf Frames?
- Der Compiler senkt End-of-Lifetime-Semantik in Drop src ab.
- Der Verifier stellt Use-after-drop als Fehler sicher (05-vm-instruction-set.md).
3. Register-Layout: Byte-Buffer statt uniformer Slots¶
Aus 11-jadevalue-und-register-layout.md:
- Der Generator berechnet zur Compile-Zeit das Layout der Registerdatei:
- Offset & Größe jedes Registers (basierend auf Type-Engine Layout/Policies).
- Die VM allokiert beim Frame-Aufbau den Register-Buffer „in einem Stück“.
Konsequenzen:
- Keine Boxing-Kosten für Primitives.
- memory: Value-Structs liegen inline als Bytes.
- Handle-basierte Werte liegen als HandleId im Register-Buffer.
4. Memory Policies und ihre Runtime-Auswirkung¶
Aus 03-runtime.md:
- Value: inline, kein Handle.
- Ref: Heap, Handle, reference-counted.
- Arena[A]: Bump-allokiert in Arena, bulk reset bei Scope-Ende.
- Pool[P]: Wiederverwendung teurer Objekte.
Normative Regeln:
- Arenas bilden einen Stack; innere Arenas werden vor äußeren zurückgesetzt.
- Keine Cross-Arena-Referenzen. Crossing erfolgt als Copy (oder Fehler, wenn !Copyable).
- with Storage.arena[RequestArena] { ... } resetet am Ende: Pointer-Reset, kein Traversal.
5. VM Core Instruktionsset: die relevante Untermenge¶
Aus 05-vm-instruction-set.md (Core v0.1), die für dieses Thema wichtigsten Instruktionen:
LoadK dst, kCall dst, func, argsBase, argcReturn srcNewStruct dst, kTypeIdGetField dst, obj, kFieldSetField obj, kField, srcNewArray dst, lenRegGetIndex dst, arr, idxRegSetIndex arr, idxReg, srcMove dst, src(src danach unbrauchbar; Verifier garantiert)Drop src(End-of-Lifetime; Handles: RC/Destruction gemäß Policy)Trap kReason
6. Subsystem-Rollen entlang des Ablaufs¶
6.1 Compile-Time: vom JDL-Programm zum Proto¶
Type Engine (Queries):
- Typauflösung und Internierung (TypeId)
- Policies/Refinements (memory:, derive, Copyable, etc.)
- Layout-Daten (size, align, Struct-Feld-Offsets)
- Klassifikation: „in Registerfile als bytes“ vs „HandleId“
Generator (read-only Konsument, Queries):
- ruft Layout/Policy-Queries ab (z.B. MemLayoutQueryApi).
- senkt High-Level-Semantik vollständig in Core-Instruktionen ab.
- erzeugt den Proto:
- code[], constPool[], regLayout, debugSideTable.
VM / JME: - keine Rolle (noch keine Ausführung).
6.2 Runtime: vom Proto zum Frame zur Ausführung¶
VM:
- baut Frame (Header + Registerfile) aus VM-eigener Allocation (VM-Subsystem-Storage).
- interpretiert code[] (Dispatch/Decode) und arbeitet auf Registerfile-Bytes.
JME (Memory Engine, Commands):
- wird nur bei handle-relevanten Instruktionen aktiv (NewStruct, NewArray, GetField, SetField, GetIndex, SetIndex, Drop usw.).
- verwaltet HandleTable inkl. Generation-Counter.
- führt RC-/Destruction-/Arena-Operationen gemäß Policies aus.
Type Engine: - zur Laufzeit nicht aktiv beteiligt; ihre Entscheidungen sind im Proto materialisiert (TypeIds, Layout, Policies).
7. Beispiel: „mittelprächtiger“ JDL Code → VM Sicht¶
7.1 Beispielcode (Intention)¶
type User: struct { name: str, age: i64 } // enthält Handle-Typ → typischerweise memory: Ref
fn main() -> i64 {
let u = User { name: "Elias", age: 42 }
let nameLen = {
let s = u.name
s.len()
}
let nums = [1, 2, 3, u.age]
let sum = 0
let i = 0
while i < 4 {
sum = sum + nums[i]
i = i + 1
}
if nameLen < 5 { sum + 100 } else { sum + 1 }
}
7.2 Const-Pool (schematisch)¶
k0 = TypeId(User)k1 = FieldId(User.name)k2 = FieldId(User.age)k3 = "Elias"k4 = 42k5 = 0k6 = 1k7 = 2k8 = 3k9 = 4k10 = 5k11 = 100k12 = builtin fn str.len(oder Intrinsic-Subcode, implementierungsabhängig)
7.3 Registerrollen (schematisch)¶
R0u (HandleId User)R1temp s/name (HandleId str)R2fn (callable)R3nameLen (i64)R4nums (HandleId Array)R5sum (i64)R6i (i64)R7cond (bool)R8tmpVal (i64)R9limit4 (i64)R10one (i64)R11cond2 (bool)R12five (i64)R13ret (i64)R14hundred (i64)
7.4 Core-Disassembly (VM sieht so etwas)¶
0000: NewStruct R0, k0
0001: LoadK R1, k3
0002: SetField R0, k1, R1
0003: LoadK R1, k4
0004: SetField R0, k2, R1
0005: GetField R1, R0, k1
0006: LoadK R2, k12
0007: Call R3, R2, R1, 1
0008: Drop R1 ; Blockscope endet: s stirbt
0009: LoadK R9, k9
0010: NewArray R4, R9
0011: LoadK R6, k5
0012: LoadK R8, k6
0013: SetIndex R4, R6, R8
0014: LoadK R6, k6
0015: LoadK R8, k7
0016: SetIndex R4, R6, R8
0017: LoadK R6, k7
0018: LoadK R8, k8
0019: SetIndex R4, R6, R8
0020: LoadK R6, k8
0021: GetField R8, R0, k2
0022: SetIndex R4, R6, R8
0023: LoadK R5, k5
0024: LoadK R6, k5
0025: LoadK R10, k6
L_test:
0026: Lt R7, R6, R9
0027: JmpIf R7, L_body
0028: Jmp L_after
L_body:
0029: GetIndex R8, R4, R6
0030: Add R5, R5, R8
0031: Add R6, R6, R10
0032: Jmp L_test
L_after:
0033: LoadK R12, k10
0034: Lt R11, R3, R12
0035: JmpIf R11, L_true
0036: Jmp L_false
L_true:
0037: LoadK R14, k11
0038: Add R13, R5, R14
0039: Jmp L_join
L_false:
0040: Add R13, R5, R10
L_join:
0041: Drop R4
0042: Drop R0
0043: Return R13
8. Trace-Tabelle: PC → Instruktion → Subsystem-Aktionen¶
Die Tabelle zeigt: - was die VM (Frame/Regs/Control) tut, - was die JME (Handles/Allokation/RC) tut, - welche Lifetime-/Scope-Ereignisse materialisiert sind.
| pc | Instruktion | VM-Aktion | JME-Aktion | Lifetime / Scope |
|---|---|---|---|---|
| 0000 | NewStruct R0, User |
Command: alloc(User) → Handle in R0 | Allokation + HandleTable slot + init | u lebendig (HandleId in R0) |
| 0001 | LoadK R1, "Elias" |
Const → R1 | ggf. Literal-Handle liefern/materialisieren | temp handle in R1 |
| 0002 | SetField R0.name = R1 |
Command: storeField | write field; RC/ownership nach Policy | User referenziert name |
| 0005 | GetField R1 = R0.name |
Command: loadField → R1 | read field; ggf. RC/retain (Policy) | Blockscope beginnt: s in R1 |
| 0007 | Call R3 = len(R1) |
Call oder Intrinsic (impl.-Wahl) | ggf. str-length lesen | nameLen in R3 |
| 0008 | Drop R1 |
End-of-lifetime | release/decStrong falls owning | Blockscope endet: s stirbt |
| 0010 | NewArray R4, 4 |
Command: allocArray | Array-Backing + handle | nums lebendig |
| 0029 | GetIndex R8 = nums[i] |
Command: loadIndex | bounds + read element | - |
| 0041 | Drop R4 |
drop handle slot | decStrong; ggf. free backing | nums stirbt |
| 0042 | Drop R0 |
drop handle slot | decStrong; ggf. free User + release fields | u stirbt |
| 0043 | Return R13 |
pop frame, return value boundary | (nur wenn noch cleanup nötig) | frame endet |
Hinweis: Die Tabelle abstrahiert viele “reine VM”-Instruktionen (Add/Lt/Jmp), weil sie keine JME-Aktionen auslösen.
9. Wichtige Invarianten und Safety-Punkte¶
9.1 Use-after-move / Use-after-drop¶
05-vm-instruction-set.md:
- Move dst, src: src ist danach unbrauchbar; Verifier garantiert keine Reads.
- Drop src: End-of-Lifetime; Verifier garantiert keine Reads danach.
- In Debug/Profiling Builds darf die VM Trapen; in Release ist es Verifier-Fehler.
9.2 Handle-Sicherheit (Generation-Check)¶
11-jadevalue-und-register-layout.md:
- HandleId enthält Generation zum Schutz gegen ABA.
- Jeder Zugriff validiert: slot.generation == id.generation, sonst Trap(InvalidHandle).
10. Offene Punkte in v0.1 und einfache Implementationspfade¶
10.1 ABI für memory: Value-Structs > 64 Bit an Grenzen¶
Status: offen (explizit als TBD markiert in 11-jadevalue-und-register-layout.md).
Minimaler Implementationspfad (konsistent und einfach):
- Calling Convention: große Value-Structs werden indirekt über Pointer auf Registerfile-Bereich übergeben.
- Generator emittiert:
- argsBase zeigt auf ein Register, dessen Slot eine Adresse (oder Handle) auf den Struct-Bereich repräsentiert.
- Alternativ: mehrere aufeinanderfolgende Register als “struct payload window” und der Callee kennt den Bereich via RegLayout.
Nutzen: schnelle Weiterentwicklung ohne Boxing; ABI kann später formalisiert werden.
10.2 Literal-Handling (str Konstanten)¶
Status: nicht vollständig festgelegt (wie k3="Elias" materialisiert wird).
Einfacher Implementationspfad:
- Option A: Modul-Const-Pool enthält bereits ein JME-Handle zu internierter/pinned String-Allocation.
- Option B: LoadK triggert einmalige Materialisierung (lazy) und cached Handle im Const-Pool.
Nutzen: deterministische Semantik; keine Sonderfälle im Bytecode.
10.3 Scope-basierte Drops in komplexem Control-Flow¶
Core enthält Drop src (normativ). Was fehlt, ist nur die “Codegen-Strategie”, wie Drops an allen Exit-Pfaden korrekt gesetzt werden.
Einfacher Implementationspfad:
- Generator arbeitet auf CFG-Basis:
- Für jeden Wert wird eine Live-Range bestimmt (Basic Block / PC-Spanne).
- An jedem Übergang, an dem ein Wert tot wird, emittiert der Generator Drop (oder räumt ihn in einen Cleanup-Block).
- Für early exits (Return, Trap) erzeugt der Generator per Block einen Cleanup-Abschnitt vor dem Terminator.
Nutzen: Keine Runtime-Magie. Verifier kann Use-after-drop/Moves prüfen.
10.4 Subsystem-Kommunikation: Queries vs Commands¶
Aus 09-subsystem-kapselung-und-zustandsintegritaet.md:
- Queries: read-only, cachebar (CompilerDb ist ein Beispiel).
- Commands: ändern Zustand (JME alloc/release, VM frame push/pop, arena reset).
Einfacher Implementationspfad (runtime-kompatibel, ohne CompilerDb zu missbrauchen):
- Proto materialisiert alle Daten, die die VM zur Ausführung braucht (TypeId, FieldId, Layout, Drop-Punkte).
- Runtime ruft nur Commands auf (JME), keine compile-time Queries.
11. Implementationsnotiz: Frame Storage in D (nicht normativ)¶
Die Spezifikation schreibt nur vor: - pro Call ein neuer Frame mit eigener Registerdatei, - Registerfile als „ein Stück“ allokiert.
Eine praktische D-Implementierung kann eine VM-eigene Frame-Arena/Pool nutzen, z.B. auf Basis von std.experimental.allocator (Region/AllocatorList/AffixAllocator), um:
- 1 Allokation pro Frame (Header + Registerfile),
- LIFO-freies Pop bei Return,
- deterministische Freigabe.
Diese Notiz ist bewusst nicht normativ. Der konkrete Allocator ist Implementation Choice der VM.
12. Kurz-Zusammenfassung (mentales Modell)¶
1) Type Engine entscheidet Compile-Time:
- Typen, Policies, Layout (was ist Handle, was ist Value-bytes?)
2) Generator baut Proto:
- Core-Opcodes + Const-Pool + RegLayout + Debug + Drops (falls nötig)
3) VM baut Frame:
- FrameHeader + Registerfile (Byte-Buffer) und führt pc-basiert aus
4) JME reagiert auf Commands:
- alloc/store/load/release gemäß Policy; Generation-Checks; RC/Destruction
5) {}-Scopes sind keine Frames:
- Sie werden nur zu Drop-Punkten und/oder with-acquire/release-Patterns materialisiert.
Anhang A: Minimaler Checkliste für die Implementierung¶
- Proto enthält:
code,constPool,regLayout(totalBytes + offsets/sizes),debug(pc→SourceLoc) - Verifier prüft: register bounds, const bounds, CFG validity, use-after-move/drop
- Frame-Push allokiert Registerfile als zusammenhängenden Block
-
Dropruft JME release/decStrong gemäß Policy - Arena-Scopes (
with Storage.arena[A]) machen bulk reset + Copy-on-return - HandleId-Zugriffe validieren Generation, sonst
Trap(InvalidHandle)