Zum Inhalt

Commands in Action: with Storage.arena[...] (v0.1)

Ziel: Konkreter Ablauf, wie ein with Storage.arena[...] { ... }-Scope zur Runtime über Commands ausgeführt wird, ohne dass Subsysteme interne Zustände gegenseitig anfassen.

Dieses Dokument ergänzt den Überblick Frames & Protos um den Command-Pfad (Gegenstück zu Queries).


0. Normativer Rahmen (aus Specs abgeleitet)

0.1 Was die Specs bereits festlegen

  • Subsystem-Kapselung: Subsysteme interagieren über definierte Schnittstellen, nicht über direkte Datenzugriffe. (Queries vs. Commands)
  • Arenas / Scopes: Arena-Scopes sind stackartig; innere Arenas werden vor äußeren beendet; Reset ist “bulk” (Pointer-Reset), kein Traversal.
  • Register-VM: Call erzeugt Frame, Return beendet Frame; Registerfile ist ein Byte-Buffer mit compile-time Layout.
  • JadeValue nur an Grenzen: Call/Return/FFI/Intrinsics; innerhalb eines Frames arbeitet die VM auf dem Registerfile.

0.2 Was die Specs nicht als konkrete Opcode-Form festnageln

Die Spezifikationen definieren die Semantik von with Storage... (Scope, Arena-Reset, Crossing-Regeln), aber typischerweise nicht exakt, ob das als: - Intrinsic-Opcode, - normaler Call in die Stdlib, - oder ein spezieller EnterScope/LeaveScope-Opcode materialisiert wird.

In diesem Dokument wird ein minimaler, einfach implementierbarer Lowering-Pfad gezeigt: - with Storage.arena[A] { body } - → StorageArenaPush(A) vor body - → StorageArenaPopReset() nach body - plus Copy/Move-Regeln bei Arena-Crossing (wenn Werte nach außen gehen)

Das ist keine Interpretation der Semantik, sondern eine konkrete Abbildung, die die Semantik korrekt implementiert.


1. Command-Modell: Domains, Requests, Returns

1.1 Grundregel: Queries vs. Commands

  • Query: read-only, deterministisch, cachebar (CompilerDb-Style).
  • Command: Side-Effect, nicht cachebar, reihenfolgenabhängig (Runtime-Port).

Das Kommunikationsformat kann identisch sein (typed structs), der Vertrag ist anders.

1.2 Storage-Commands (Arena Scope)

Domain: Storage (oder „Runtime/Effects“).

Minimaler Command-Satz: - StorageArenaPush{ arenaId } -> Result(ArenaToken, StorageError) - StorageArenaPopReset{ token } -> void (oder Result(void, StorageError), wenn du recoverable Fehler willst) - Optional: StorageArenaCurrent{} -> ArenaToken (für Debug/Tracing) - Optional: StorageArenaMark/ResetToMark (für verschachtelte Subscopes)

Warum ein Token? - Damit PopReset exakt den richtigen Scope beendet, auch bei verschachtelten Arenas. - Damit du im Unwind/Trap-Fall deterministisch cleanupen kannst.


2. Beispielprogramm (Intention)

def main() -> i64 {
  with Storage.arena[RequestArena] {
    let a = makeArray(4)      // JME allocation: Arena-bound
    a[0] = 1
    a[1] = 2
    a[2] = 3
    a[3] = 4
    sumArray(a)               // returns i64 (Value)
  }
}

Semantik: - a lebt nur im Arena-Scope. - Am Scope-Ende wird die Arena resetet (bulk). - Das i64-Ergebnis verlässt die Arena ohne Copy-Probleme (Value).


3. Lowering: VM sieht Core-Stream + Commands

3.1 Annahme: StorageArenaPush/PopReset als Intrinsics

Damit die VM nicht bei jedem Scope einen Call-Frame bauen muss, ist die saubere Minimalform: - INTRINSIC StorageArenaPush / INTRINSIC StorageArenaPopReset

Alternative (auch korrekt): - Normaler Call in ein stdlib-builtin, das intern Commands ausführt. - Das kostet mehr, ist aber funktional identisch.

3.2 Schematischer Core-Disassembly

; enter arena scope
0000: Intrinsic  StorageArenaPush, dst=R0, arenaId=RequestArena

; a = makeArray(4)  (Arena-bound allocation über JME)
0001: LoadK      R1, 4
0002: NewArray   R2, R1                 ; R2 = a (HandleId)

0003: LoadK      R3, 0
0004: LoadK      R4, 1
0005: SetIndex   R2, R3, R4

0006: LoadK      R3, 1
0007: LoadK      R4, 2
0008: SetIndex   R2, R3, R4

0009: LoadK      R3, 2
0010: LoadK      R4, 3
0011: SetIndex   R2, R3, R4

0012: LoadK      R3, 3
0013: LoadK      R4, 4
0014: SetIndex   R2, R3, R4

; sumArray(a) -> i64
0015: LoadK      R5, fn sumArray
0016: Call       R6, R5, R2, 1

; leave arena scope (bulk reset)
0017: Intrinsic  StorageArenaPopReset, token=R0

; R2 (a) ist arena-bound; nach Reset ist er invalid (Handle generation mismatch oder trap-on-use)
0018: Return     R6

Wichtig: - Es gibt hier kein Drop R2 nötig, wenn a wirklich arena-bound ist und durch PopReset bulk verschwindet. - Trotzdem muss die VM/Verifier garantieren, dass nach PopReset kein Zugriff auf arena-bound Handles erfolgt (Use-after-scope). Das ist analog zu Use-after-drop/move.


4. Subsystem-Aktionen pro Schritt (Trace)

pc Instruktion VM-Aktion Storage-Command JME-Aktion Semantik
0000 StorageArenaPush -> R0 cmd(StorageArenaPush{RequestArena}) push arena, return token - Arena-Scope beginnt
0002 NewArray R2 cmd(JmeAllocArray{len=4, policy=Arena(current)}) - alloc in current arena a arena-bound
0005/8/11/14 SetIndex store in array - bounds + write -
0016 Call sumArray push callee frame / pass args - ggf. index loads returns i64
0017 StorageArenaPopReset token cmd(StorageArenaPopReset{token}) pop + bulk reset free arena allocations by reset Arena-Scope endet
0018 Return R6 return immediate - - ok

5. D-Sketch: Commands “in Action” (VM → Runtime → Storage/JME)

Hinweis: Dieses Code-Sketch zeigt die Mechanik, nicht deine endgültige API.

5.1 Command-Structs (Storage)

enum CmdDomain : ubyte { JME, Storage, VM }

enum StorageError : ubyte { NoArenaProvider, InvalidToken, Internal }

struct ArenaToken { uint id; uint depth; }

struct StorageArenaPush
{
    enum domain = CmdDomain.Storage;
    alias Return = Result!(ArenaToken, StorageError);

    uint arenaId; // RequestArena etc.
}

struct StorageArenaPopReset
{
    enum domain = CmdDomain.Storage;
    alias Return = void;

    ArenaToken token;
}

5.2 Storage-Port (Subsystem owns the arena stack)

struct StoragePort
{
    // interner Zustand: arena stack, mapping arenaId -> arena object
    // VM darf das NICHT direkt anfassen.

    Result!(ArenaToken, StorageError) exec(ref StorageArenaPush c) @safe nothrow
    {
        // resolve provider, push arena, return token
        // minimal: increment depth + unique id
        auto tok = ArenaToken(1, 42);
        return Result!(ArenaToken, StorageError).success(tok);
    }

    void exec(ref StorageArenaPopReset c) @safe nothrow
    {
        // validate token, pop, reset arena (bulk)
        // reset means: bump pointer back; no traversal
    }
}

5.3 Runtime Command Bus (Routing)

struct Runtime
{
    StoragePort storage;
    JmePort     jme;

    auto cmd(C)(ref C c) @safe nothrow
        if (is(typeof(C.domain)) && is(typeof(C.Return)))
    {
        static if (C.domain == CmdDomain.Storage) return storage.exec(c);
        static if (C.domain == CmdDomain.JME)     return jme.exec(c);
        static assert(0, "Unhandled command domain");
    }
}

5.4 VM Execution der Intrinsics

struct VM
{
    Runtime* rt;
    RegFile  regs;

    void execArenaPush(uint dstReg, uint arenaId) @safe nothrow
    {
        StorageArenaPush c = { arenaId: arenaId };
        auto r = rt.cmd(c);
        if (!r.ok) return trap(TrapReason.Internal); // oder spezifischer
        regs.storeArenaToken(dstReg, r.value);       // token als Value im RegFile
    }

    void execArenaPopReset(uint tokenReg) @safe nothrow
    {
        auto tok = regs.loadArenaToken(tokenReg);
        StorageArenaPopReset c = { token: tok };
        rt.cmd(c); // infallible unter invariants
    }
}

Token im Registerfile - Ein ArenaToken ist ein Value-Typ (inline im Registerbuffer). - Keine Handles, kein RC, keine JME-Beteiligung.


6. Arena-Crossing (Scope-Verlassen mit Werten)

6.1 Was die Semantik verlangt

Wenn ein Wert v in einer Arena lebt und aus dem Scope zurückgegeben wird, muss entweder: - Copy in die äußere Arena/Storage stattfinden (wenn Copyable), oder - Compile-Error, oder - explizite Konversion/Move-Regel, die Ownership transferiert (nur wenn Semantik/Policies das erlauben)

6.2 Minimaler Implementationspfad

  • Generator erkennt: Return-Value ist arena-bound Handle.
  • Generator emittiert vor PopReset eine CopyToParent-Operation (als Intrinsic oder Command), die:
  • die Objektstruktur über TypeDescriptor/Copy-Protokoll dupliziert,
  • neue Handles in Parent-Storage erzeugt,
  • Referenzen intern korrekt remapped (Sharing preserving).
  • Danach darf PopReset die Arena zerstören.

Benefit: Der VM-Core bleibt dumm. Die Komplexität liegt im Copy-Protokoll, das sowieso notwendig ist.


7. Was du damit bekommst (ohne neue Magie)

  • with Storage.arena[...] ist zur Runtime nur:
  • ein Push-Command vor dem Body
  • ein PopReset-Command nach dem Body
  • JME-Allokationen im Scope werden automatisch arena-gebunden (Policy/CurrentArena).
  • Der Bytecode bleibt Core; Subsysteme bleiben gekapselt.
  • Fehler/Unwind sind sauber modellierbar, weil du ein Token hast (deterministischer Cleanup).

8. Offene Punkte (falls noch nicht vollständig normiert)

1) Opcode-Form: Intrinsic vs Call vs Spezial-Opcode. (Semantik ist unabhängig.) 2) Use-after-scope Enforcement: Verifier-Regel oder Runtime Trap. 3) Copy-Protokoll für Arena-Crossing: Muss normiert werden (TypeDescriptor-driven Copy + Handle remap). 4) Unwind: Bei Trap innerhalb des Scopes muss garantiert PopReset passieren (Cleanup-Plan / Unwind stack).

Für (4) ist der einfache Pfad: - VM führt einen Unwind-Stack (Value-only) im Frame, der Cleanup-Aktionen enthält (z.B. “pop token X”). - Generator pusht Cleanup beim Push, und poppt beim normalen PopReset.


Anhang: Minimaler Unwind-Plan (einfach, robust)

Beim ArenaPush - VM registriert Cleanup: StorageArenaPopReset(token)

Beim normalen Scope-Ende - Cleanup wird als “erledigt” markiert (oder vom Stack gepoppt)

Bei Trap - VM läuft Cleanup-Stack rückwärts ab und führt noch offene Cleanups aus

Damit ist with robust gegenüber early exits, ohne dass du überall manuell PopReset in jeden CFG-Kantenblock stopfen musst.