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:
Callerzeugt Frame,Returnbeendet 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
PopReseteineCopyToParent-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
PopResetdie 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.