Zum Inhalt

Sketch: Escape-Analyse und Refcount-Elision

Status: Entwurf / Diskussionsgrundlage Abhängigkeiten: 12-design-constitution.md (Pass-System), 04a-label-inferenz-regeln.md, jade-compiler-db-spec.md, 11-jadevalue-und-register-layout.md


1. Zwei Pässe, getrennte Verantwortung

Pass 1: EscapeAnalysis (reine Analyse)

Pass EscapeAnalysis:
  erfordert:         [SSAForm, TypesResolved, PoliciesConsistent]
  garantiert:        [EscapeInfoAvailable]
  kann invalidieren: []

Reine Leseoperation auf dem SSA-Graph. Verändert kein IR. Produziert eine Side-Table FuncId → [EscapeInfo].

Pass 2: RefcountElision (Transformation)

Pass RefcountElision:
  erfordert:         [SSAForm, EscapeInfoAvailable, PoliciesConsistent]
  garantiert:        [PoliciesConsistent]
  kann invalidieren: []

Entfernt redundante Retain/Release-Paare im IR. Verändert IR, aber nicht Allokationsstrategie, nicht Drop-Zeitpunkt, nicht beobachtbares Verhalten.


2. Datenmodell

2.1 EscapeState

// spekulativ
enum EscapeState : ubyte {
    NoEscape,       // Wert verlässt den erzeugenden Scope nicht
    ArgEscape,      // Wert wird an Callee übergeben, kehrt nicht zurück
    GlobalEscape,   // Wert verlässt Funktion (Return, Feld, Capture, FFI, Spawn)
}

Hierarchie: NoEscape < ArgEscape < GlobalEscape. Monoton — ein Wert kann nur aufsteigen, nie absteigen.

2.2 EscapeInfo

// spekulativ
struct EscapeInfo {
    AllocSiteId  site;       // SSA-Definition die den Wert erzeugt
    EscapeState  state;      // konservatives Ergebnis
    ScopeId      maxScope;   // tiefster Scope in dem der Wert lebt
    EscapeReason[] reasons;  // Proof-Trace: warum diese Klassifikation
}

struct EscapeReason {
    UseId       use;         // welche Verwendung hat den Escape verursacht
    EscapeKind  kind;        // Return | FieldStore | ClosureCapture | Spawn | FFI | CallArg
    ScopeId     targetScope; // in welchen Scope der Wert escaped
}

2.3 CompilerDb-Query

// spekulativ
struct AnalyzeEscapes {
    FuncId id;
    // → EscapeInfo[]
}

Cachebar, invalidiert wenn sich der Body der Funktion ändert. DepGraph-Kante: AnalyzeEscapes(f) hängt von CheckFunction(f) ab.


3. Algorithmus: EscapeAnalysis

Für jede Allokationsstelle s in Funktion f:

1. Initialisiere: state(s) = NoEscape

2. Für jede Use-Chain von s im SSA-Graph:
   a. Use ist `Return src`:
      → state(s) = max(state(s), GlobalEscape)
      → reason: Return

   b. Use ist `SetField obj, field, src` wobei obj.escapeState == GlobalEscape:
      → state(s) = max(state(s), GlobalEscape)
      → reason: FieldStore

   c. Use ist Capture in Closure c:
      - wenn c selbst NoEscape → state(s) = max(state(s), NoEscape)
      - wenn c ArgEscape      → state(s) = max(state(s), ArgEscape)
      - wenn c GlobalEscape   → state(s) = max(state(s), GlobalEscape)
      → reason: ClosureCapture

   d. Use ist Argument an vm_spawn:
      → state(s) = max(state(s), GlobalEscape)
      → reason: Spawn

   e. Use ist Argument an FFI-Grenze:
      → state(s) = max(state(s), GlobalEscape)
      → reason: FFI

   f. Use ist Call-Argument an Funktion g:
      - wenn g inlined: analysiere transitiv
      - wenn g nicht inlined: state(s) = max(state(s), ArgEscape)
      → reason: CallArg

3. Ergebnis: EscapeInfo { site: s, state: state(s), reasons: [...] }

Komplexität: O(Σ uses pro alloc site) — quasi-linear für typische Funktionen.


4. Algorithmus: RefcountElision

Für jedes Retain/Release-Paar im IR:

1. Identifiziere den zugehörigen Wert v und seine EscapeInfo

2. Prüfe Elision-Bedingungen:
   a. v.escapeState == NoEscape
   b. v.type hat proven(Memory(Rc))
   c. v.type hat NICHT proven(Drop(Custom))
      ODER das Retain/Release ist NICHT das finale (das den Drop auslöst)
   d. Das Retain/Release-Paar ist lokal balanciert im selben Scope

3. Wenn alle Bedingungen erfüllt:
   → Entferne das Retain/Release-Paar aus dem IR
   → Annotiere die Elision im Debug-Trace

4. Das FINALE Release (das den Refcount auf 0 bringt) wird NIE entfernt.
   → Drop-Zeitpunkt bleibt identisch
   → Drop(Custom)-Seiteneffekte bleiben erhalten

Was entfernt wird (Beispiel)

// Vor Elision:
  r3 = Alloc(MyStruct)          // refcount = 1
  Retain(r3)                     // refcount = 2 (wegen lokalem Alias)
  r7 = Copy(r3)                  // r7 ist lokaler Alias
  ... benutze r7 ...
  Release(r7)                    // refcount = 1
  ... benutze r3 ...
  Release(r3)                    // refcount = 0 → Drop

// Nach Elision (r3 ist NoEscape):
  r3 = Alloc(MyStruct)          // refcount = 1
  r7 = Copy(r3)                  // Alias, kein Retain
  ... benutze r7 ...
  // kein Release für r7
  ... benutze r3 ...
  Release(r3)                    // refcount = 0 → Drop (unverändert)

5. Semantik-Invarianten (normativ für Korrektheit)

  1. Drop-Zeitpunkt ist identisch. Das finale Release das den Refcount auf 0 bringt wird nie entfernt. Beobachtbares Verhalten ändert sich nicht.

  2. Allokationsort ist identisch. Memory(Rc)-Werte bleiben auf dem Heap. Keine Arena-Promotion, keine Value-Promotion.

  3. Debug/Release-Äquivalenz. Die Elision darf in beiden Modi laufen oder in keinem — aber wenn sie läuft, ändert sie keine Semantik. Empfehlung: in beiden Modi aktiv, da semantisch unsichtbar.

  4. Proof-Trace. Jede entfernte Retain/Release-Operation wird im Debug-Trace annotiert mit: Grund (NoEscape), Allokationsstelle, Scope-Zugehörigkeit. LSP kann das als Inlay-Hint anzeigen.


6. Zusatz-Nutzen der EscapeInfo für bestehende Features

Die EscapeAnalysis-Query ist auch Consumer für:

Feature Wie EscapeInfo hilft
Task-Grenz-Enforcement GlobalEscape via Spawn → Compile-Fehler wenn Share(Local)
Closure-Capture (S-8) Capture-Analyse ist Teilmenge der Escape-Analyse
Slice-Auto-Copy NoEscape-Slice → View; GlobalEscape-Slice → Copy
LSP Memory Hints "Dieser Wert verlässt den Scope nicht" als Inlay-Hint
LSP Arena-Suggestion "NoEscape + viele Allokationen → with arena vorschlagen"
Use-After-Move Detection MoveOnSend + GlobalEscape via Spawn → Zugriff nach Move prüfen

7. Abgrenzung: Was dieser Entwurf NICHT tut

  • Keine automatische Arena-Promotion. Allokationsstrategie bleibt wie vom Typsystem bestimmt. Gegenargumente 1–6 gelten weiterhin.

  • Keine interprozedurale Analyse. Nicht-inlinede Callees werden konservativ als ArgEscape behandelt. Modulgrenzen bleiben respektiert.

  • Keine neue IR-Invariante für Korrektheit. EscapeInfoAvailable ist eine Optimierungs-Vorbedingung, kein Korrektheitskriterium. Code der ohne Escape-Analyse kompiliert wird, ist korrekt — nur langsamer.

  • Kein neues Label. EscapeInfo ist pro Allokationsstelle, nicht pro Typ. Sie lebt als Side-Table in der CompilerDb, nicht im LabelSet.