Zum Inhalt

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 Prototype
  • pc: Program Counter im Proto-Code
  • caller: 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, k
  • Call dst, func, argsBase, argc
  • Return src
  • NewStruct dst, kTypeId
  • GetField dst, obj, kField
  • SetField obj, kField, src
  • NewArray dst, lenReg
  • GetIndex dst, arr, idxReg
  • SetIndex arr, idxReg, src
  • Move 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 = 42
  • k5 = 0
  • k6 = 1
  • k7 = 2
  • k8 = 3
  • k9 = 4
  • k10 = 5
  • k11 = 100
  • k12 = builtin fn str.len (oder Intrinsic-Subcode, implementierungsabhängig)

7.3 Registerrollen (schematisch)

  • R0 u (HandleId User)
  • R1 temp s/name (HandleId str)
  • R2 fn (callable)
  • R3 nameLen (i64)
  • R4 nums (HandleId Array)
  • R5 sum (i64)
  • R6 i (i64)
  • R7 cond (bool)
  • R8 tmpVal (i64)
  • R9 limit4 (i64)
  • R10 one (i64)
  • R11 cond2 (bool)
  • R12 five (i64)
  • R13 ret (i64)
  • R14 hundred (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
  • Drop ruft JME release/decStrong gemäß Policy
  • Arena-Scopes (with Storage.arena[A]) machen bulk reset + Copy-on-return
  • HandleId-Zugriffe validieren Generation, sonst Trap(InvalidHandle)