Zum Inhalt

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, nie throw
  • 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 printf statt writeln zum Debuggen