D @nogc nothrow pure — Cheat Sheet für Jade¶
Alle Handler und der kritische Pfad in
query()tragen@nogc nothrow pure. Der D-Compiler erzwingt das statisch — Verletzungen sind Buildfehler.
1. Die drei Attribute — was sie bedeuten¶
T handler(Q q, CompilerDb* db) @nogc nothrow pure { ... }
// │ │ │
// Keine GC-Allokation ─────────┘ │ │
// Keine Exceptions ─────────────────────┘ │
// Kein globaler Zustand (weakly pure) ─────────┘
pure in Jade-Kontext = weakly pure:
Mutation über Parameter (db) ist erlaubt. Globaler Zustand ist verboten.
Das passt exakt zu den Query-Invarianten.
2. Was VERBOTEN ist¶
2.1 GC-Allokation (@nogc)¶
// ✗ new — allokiert auf dem GC-Heap
auto node = new ASTNode();
// ✗ Array-Literal mit new
auto arr = new int[100];
// ✗ Built-in Associative Arrays — intern GC-verwaltet
int[string] map;
map["key"] = 42;
// ✗ Array append (~=) auf GC-verwalteten Arrays
int[] arr;
arr ~= 42; // kann GC-Realloc triggern
// ✗ String-Konkatenation mit ~
string s = "hello" ~ " " ~ "world";
// ✗ std.conv.to!string
import std.conv;
string s = to!string(42); // allokiert GC-String
// ✗ std.format / std.string.format
import std.format;
string s = format!"Value: %d"(42);
// ✗ std.array.array() — materialisiert Range in GC-Array
import std.array;
auto a = iota(10).array();
// ✗ std.array.appender
auto app = appender!(char[]);
// ✗ Closures die Heap-Allokation brauchen
auto fn = (int x) => x + captured; // Closure über captured → kann GC brauchen
2.2 Exceptions (nothrow)¶
// ✗ throw
throw new Exception("error");
// ✗ try/catch mit Exception
try { riskyOp(); }
catch (Exception e) { ... }
// ✗ enforce
import std.exception;
enforce(x > 0, "must be positive");
// ✗ Alles was Exception werfen kann
// z.B. std.utf.decode, assert mit Message-String (Debug-Mode Sonderfall)
2.3 Globaler Zustand (pure)¶
// ✗ Zugriff auf globale/statische mutable Variablen
__gshared int counter;
void inc() pure { counter++; } // Compilerfehler
// ✗ I/O jeder Art
import std.stdio;
writeln("debug"); // nicht pure, nicht @nogc, nicht nothrow
// ✗ Zugriff auf fremde Subsysteme (Jade-Invariante 1)
auto scope_ = db.resolver.currentScope; // VERBOTEN — nicht über db.query()
2.4 Phobos-Module die NICHT funktionieren¶
| Modul | Grund |
|---|---|
std.stdio |
I/O, GC-Strings |
std.conv |
GC-Allokation |
std.format |
GC-Allokation |
std.string |
GC-Allokation (die meisten Funktionen) |
std.array |
array(), appender → GC |
std.exception |
enforce → Exception |
std.container |
Meistens GC-backed |
std.algorithm |
Teilweise OK — Lazy Ranges ja, .array() am Ende nein |
std.range |
Teilweise OK — Lazy-Konstrukte selbst sind @nogc |
3. Was ERLAUBT ist¶
3.1 Arena-Allokation (das Jade-Pattern)¶
struct Arena {
ubyte[] buffer;
size_t offset;
ubyte[] allocate(size_t size) @nogc nothrow pure {
if (offset + size > buffer.length)
return null; // oder: assert(false) im Debug
auto result = buffer[offset .. offset + size];
offset += size;
return result;
}
T* alloc(T)() @nogc nothrow pure {
auto mem = allocate(T.sizeof);
if (mem is null) return null;
auto ptr = cast(T*) mem.ptr;
*ptr = T.init;
return ptr;
}
void reset() @nogc nothrow {
offset = 0;
}
}
3.2 Eigene Hash-Tabelle (statt built-in AA)¶
// Jade-Pattern aus der CompilerDb-Spec:
struct QueryCache {
Arena keyArena;
Arena entryArena;
struct Bucket {
QueryKey key;
QueryResult* entry;
}
Bucket[] buckets; // in Arena allokiert, feste Größe
QueryResult* get(QueryKey key) @nogc nothrow pure {
auto idx = hashKey(key) % buckets.length;
// linear probing...
foreach (i; 0 .. buckets.length) {
auto probe = (idx + i) % buckets.length;
if (buckets[probe].key == key)
return buckets[probe].entry;
if (buckets[probe].entry is null)
return null;
}
return null;
}
}
3.3 Slices (wenn nicht GC-backed)¶
// ✓ Slices über Arena-Buffer
ubyte[] buf = arena.allocate(128);
// ✓ Slices über Stack-Arrays
int[64] stackBuf;
int[] slice = stackBuf[];
// ✓ Slicing bestehender Slices
auto sub = slice[2 .. 10];
3.4 scope(exit) für Cleanup¶
// Jade-Pattern: Dep-Tracking Wiederherstellung
auto prevActive = db.activeQuery;
db.activeQuery = &key;
scope(exit) db.activeQuery = prevActive; // ✓ garantiert, auch bei Early-Return
3.5 Templates und Compile-Time¶
// ✓ Templates sind @nogc — sie erzeugen Code, allokieren nichts
auto dispatch(T, Q)(CompilerDb* db, Q q) @nogc nothrow pure
if (isQuery!Q) // ✓ Template Constraint
{ ... }
// ✓ static if / static foreach
static if (isSimpleStruct!T) { ... }
// ✓ __traits
alias handler = getHandler!Q;
// ✓ static assert
static assert(countHandlers!Q == 1, "...");
// ✓ enum (Compile-Time-Konstante)
enum size_t CACHE_SIZE = 1024;
// ✓ CTFE-Funktionen (werden zur Compile-Zeit ausgeführt, brauchen kein @nogc)
string generateMixin() { return "int x = 42;"; } // OK: läuft nur bei CTFE
mixin(generateMixin());
3.6 core.stdc.* — die C-Stdlib¶
import core.stdc.string : memcpy, memset, memcmp, strlen;
import core.stdc.stdlib : malloc, free, realloc;
// ✓ malloc/free für Allokation außerhalb von Arenen
auto ptr = cast(int*) malloc(int.sizeof * 100);
scope(exit) free(ptr);
// ✓ memcpy für Serialisierung
memcpy(buf.ptr, &value, T.sizeof);
// ✓ memset für Initialisierung
memset(buf.ptr, 0, buf.length);
3.7 Fehlerbehandlung ohne Exceptions¶
// ✓ Result-Type (Jade-Pattern: Invariante 5)
struct Result(T, E) {
// Tagged Union — manuell oder via Template
bool isError;
union { T value; E error; }
}
// ✓ Option-Type
struct Option(T) {
bool hasValue;
T value;
}
// ✓ Handler geben Fehler als Wert zurück
@handles!ResolveType
Result!(TypeNode, Diagnostic) resolveType(ResolveType q, CompilerDb* db)
@nogc nothrow pure
{
if (!found) return err(Diagnostic{ ... });
return ok(node);
}
// ✓ assert — bleibt in @nogc nothrow (löst abort() aus, keine Exception)
assert(ptr !is null); // OK in Debug
assert(idx < buf.length); // bounds check
3.8 final switch für Exhaustiveness¶
// Jade-Pattern: Intrinsic-Dispatch
enum IntrinsicOp : ubyte {
vm_spawn,
vm_await,
vm_park,
vm_resume,
// ...
}
void dispatchIntrinsic(IntrinsicOp op) @nogc nothrow {
final switch (op) { // ✓ Compiler-Fehler wenn ein Case fehlt
case IntrinsicOp.vm_spawn: execSpawn(); break;
case IntrinsicOp.vm_await: execAwait(); break;
case IntrinsicOp.vm_park: execPark(); break;
case IntrinsicOp.vm_resume: execResume(); break;
}
}
4. Häufige Fallen¶
4.1 String-Handling¶
// ✗ String-Konkatenation
string msg = "Error in " ~ name;
// ✓ Fester Buffer + Slice
char[256] buf;
size_t len = 0;
// manuell kopieren oder snprintf nutzen
// ✓ Für IDs: Integers statt Strings
struct TypeId { uint value; } // statt string name
struct SymbolId { uint value; }
// ✓ Interning — einmal allokieren, danach nur Indizes
struct InternTable {
Arena arena;
// String → Index Mapping in Arena
}
4.2 Dynamische Arrays¶
// ✗ Wachsende Arrays
int[] items;
items ~= newItem; // GC-Realloc
// ✓ Arena-backed fixe Arrays
struct DynArray(T) {
T* ptr;
size_t len;
size_t cap;
Arena* arena;
void push(T val) @nogc nothrow {
if (len >= cap) {
// Neuen Block in Arena allokieren, kopieren
auto newCap = cap * 2;
auto newMem = arena.allocate(T.sizeof * newCap);
memcpy(newMem.ptr, ptr, T.sizeof * len);
ptr = cast(T*) newMem.ptr;
cap = newCap;
}
ptr[len++] = val;
}
}
// ✓ Stack-Array wenn Größe bekannt
Token[1024] tokenBuf;
size_t tokenCount = 0;
4.3 Delegates vs. Function Pointers¶
// ✗ Delegate der Context captured (kann GC brauchen)
int x = 42;
auto dg = (int y) => x + y; // Closure über x
// ✓ Function Pointer (kein Context)
int function(int, int) @nogc nothrow pure fp = &add;
// ✓ Delegate über Struct-Pointer (kein GC)
auto dg = &someStruct.method; // OK wenn Struct auf Stack/Arena lebt
4.4 std.algorithm — was geht, was nicht¶
import std.algorithm : filter, map, find, sort, count;
// ✓ Lazy Ranges — allokieren nichts
auto evens = slice.filter!(x => x % 2 == 0); // gibt Range zurück, kein Array
foreach (e; evens) { ... } // ✓ OK
// ✓ sort — in-place, kein GC
int[] data = stackBuf[0 .. n];
data.sort();
// ✗ .array() am Ende einer Range-Pipeline
auto result = slice.filter!(x => x > 0).array(); // GC!
// ✗ Alles was string produziert
auto s = slice.map!(to!string); // to!string → GC
5. Jade-spezifische Patterns — Kurzreferenz¶
Handler-Signatur¶
@handles!SomeQuery
ReturnType handlerName(SomeQuery q, CompilerDb* db) @nogc nothrow pure {
// Zustand: nur über db, nie global
// Andere Subsysteme: nur über db.query(), nie direkt
// Fehler: als Result/Option zurückgeben, nie throw
// Allokation: in eigene Arena oder db.resultArena
}
Query aufrufen¶
auto result = db.query!TypeNode(ResolveType{ id }); // ✓
auto result = db.typeEngine.resolve(id); // ✗ VERBOTEN
Serialisierung in Arena¶
ubyte[] serialize(T)(T value, ref Arena arena) @nogc {
static if (isSimpleStruct!T) {
auto buf = arena.allocate(T.sizeof);
*cast(T*) buf.ptr = value;
return buf;
}
}
UDA-Pattern¶
struct handles(Q) {}
// Compile-Zeit: __traits sammelt alle @handles-Funktionen
// Dispatch wird automatisch generiert — nie manuell
6. Debugging unter @nogc¶
// ✗ writeln — nicht @nogc
writeln("debug: ", value);
// ✓ printf aus core.stdc — ist @nogc nothrow
import core.stdc.stdio : printf;
debug printf("debug: offset=%zu\n", offset);
// ✓ assert — bleibt in @nogc, crasht mit Abort
assert(ptr !is null, "null pointer in handler");
// ✓ debug-Block (wird in Release entfernt)
debug {
printf("cache hit for key %u\n", key.hash);
}
7. Checkliste — vor jedem Commit¶
- Jeder Handler hat
@nogc nothrow pure - Keine
new-Allokation, kein~, kein~=auf GC-Arrays - Keine built-in Associative Arrays (
V[K]) - Keine Phobos-Importe die GC brauchen
- Fehler als
Result/Option, niethrow - Strings: Interning oder feste Buffer, keine Konkatenation
-
scope(exit)für State-Restoration - Andere Subsysteme nur über
db.query() -
final switchüber Enums für Exhaustiveness -
debug printfstattwritelnzum Debuggen