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¶
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)¶
-
Drop-Zeitpunkt ist identisch. Das finale Release das den Refcount auf 0 bringt wird nie entfernt. Beobachtbares Verhalten ändert sich nicht.
-
Allokationsort ist identisch. Memory(Rc)-Werte bleiben auf dem Heap. Keine Arena-Promotion, keine Value-Promotion.
-
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.
-
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.