Darkwood Blog Blog
  • Artikel
  • Beobachtung
  • Releases
  • Creators
de
  • en
  • fr
Anmeldung
  • Blog
  • Artikel
  • Beobachtung
  • Releases
  • Creators

🔹 Ich habe einen C-Compiler in PHP geschrieben, der SQLite kompilieren kann.

vom 30. August 2026

Anmelden um auf diesen Beitrag zu reagieren

🚀 1

Ich wollte herausfinden, wie viel von einem C-Compiler ich in PHP implementieren könnte, bevor es in der Lage wÀre, SQLite zu kompilieren.

Die Antwort lautet zum jetzigen Zeitpunkt: ausreichend. Auf macOS ARM64 wandeln ein PHP-basiertes C-Frontend und ein ARM64-Codegenerator die SQLite-3.46.0-Kombination in Assembler-Code um. Apples Assembler und Linker erstellen daraus eine native ausfĂŒhrbare Datei. Ein kleines Hilfsprogramm öffnet eine In-Memory-Datenbank, fĂŒgt eine Zeile ein, wĂ€hlt sie aus und gibt sie aus.

darkwood

Parallel zum Akzeptanztest existiert eine Regressionssuite mit 46/46 bestandenen TestfĂ€llen. Die Ă€ußere Build-/Validierungspipeline wird mit darkwood/flow v8.1.5 orchestriert.

Dieser Artikel bietet einen Überblick ĂŒber die Implementierung: Wie aus Quellcode Token werden, wie Makros expandieren, wie Deklaratoren zu Typen werden, wie semantische Analysen die Codegenerierung beeinflussen, wie ARM64-Code generiert wird und welche Fehler die Abstraktionen zwangen, sich anzupassen. Das Interessante ist nicht die Übersicht ĂŒber Laufzeiten, sondern das PHP, das C implementiert.

Was „SQLite in PHP kompilieren“ tatsĂ€chlich bedeutet

PrĂ€zision ist wichtig, denn die Formulierung kann leicht ĂŒbertrieben werden.

Der PHP-Compiler implementiert:

C source
  → lexer / preprocessor
  → parser
  → AST
  → semantic analysis
  → ARM64 code generation
  → .s assembly text

Die nativen Apple-Tools fĂŒhren dann Folgendes aus:

ARM64 .s
  → Apple assembler (as)
  → Mach-O object

objects
  → system linker via the clang driver
  → native executable

Clang wird nicht zum Kompilieren von sqlite3.c im Akzeptanzpfad verwendet. Es dient als Linker und gelegentlich als Differential-Oracle fĂŒr kleinere Tests. PHP schreibt kein Mach-O. Die interessante Grenze ist:

PHP implementiert das C-Frontend und den ARM64-Codegenerator; der Plattformassembler und der Linker vervollstĂ€ndigen die native ausfĂŒhrbare Datei.

Die bewusst kleine Architektur

Es gibt keine CIR-, keine SSA-Form und keine Optimiererpipeline. Der Treiber in src/Compiler/Driver/Compiler.php ist nahezu linear:

$preprocessor = new Preprocessor($this->sourceManager, $options->includePaths, $options->defines);
$tokens = $preprocessor->preprocess($fileId);

$parser = new Parser($tokens, $this->diagnostics);
$decls = $parser->parse();
unset($parser, $tokens);

$sema = new Sema($this->diagnostics);
$tast = $sema->analyze($decls);
$enumConstants = $sema->enumConstants;
unset($sema);

$codegen = new Codegen($enumConstants);
$assembly = $codegen->generate($tast);

Symfony hostet Konsolenbefehle (app:compiler-check, app:compiler-fixtures, app:compiler-sqlite usw.). Diese sind nicht Teil des Compiler-Algorithmus. Der relevante Code befindet sich unter src/Compiler/ als gewöhnliche PHP-Klassen. Header-Dateien unter include/ bieten eine minimale Darwin/libc-Schnittstelle fĂŒr das Frontend – ausreichend fĂŒr stdio, stdlib, Threads und zugehörige Deklarationen – keine vollstĂ€ndige Neuentwicklung des SDKs.

Die textuelle Assemblierung war eine Entscheidung hinsichtlich des GĂŒltigkeitsbereichs. Die Ausgabe von .s bedeutet, Apples Assembler-Diagnose und einen lesbaren Debugging-Pfad zu ĂŒbernehmen, ohne einen Mach-O-Writer in PHP zu erstellen.

Als Bild:

                     PHP

sqlite3.c
   │
   ▌
┌──────────────┐
│ Preprocessor │
└──────┬───────┘
       ▌
┌──────────────┐
│    Parser    │
└──────┬───────┘
       ▌
┌──────────────┐
│     AST      │
└──────┬───────┘
       ▌
┌──────────────┐
│     Sema     │
└──────┬───────┘
       ▌
┌──────────────┐
│ ARM64 Codegen│
└──────┬───────┘
       │
       ▌
    sqlite3.s

                  macOS toolchain

sqlite3.s → as → sqlite3.o
smoke.s  → as → smoke.o
                 │
                 ▌
              linker → executable → darkwood

Quelldateien und Tokens

Die Kompilierung beginnt mit der QuellcodeidentitÀt, nicht mit der Grammatik.

SourceLoc ist bewusst klein gehalten: eine Datei-ID und ein Byte-Offset.

// src/Compiler/Common/SourceLoc.php
final readonly class SourceLoc
{
  public function __construct(
    public int $fileId,
    public int $offset,
  ) {
  }
}

Zeile und Spalte werden spĂ€ter von SourceManager::lineCol() berechnet. Der Manager speichert den Dateiinhalt und eine zwischengespeicherte Tabelle mit Zeilenstart-Offsets und fĂŒhrt dann eine binĂ€re Suche nach dem grĂ¶ĂŸten Startwert <= Offset durch.

// src/Compiler/Common/SourceManager.php (excerpt)
$starts = $this->lineStarts($loc->fileId);
// Binary search: largest line-start index with start <= offset.
$lo = 0;
$hi = count($starts) - 1;
while ($lo <= $hi) {
  $mid = intdiv($lo + $hi, 2);
  if ($starts[$mid] <= $offset) {
    $lo = $mid + 1;
  } else {
    $hi = $mid - 1;
  }
}
$lineIdx = max(0, $hi);
$lineStart = $starts[$lineIdx];

return ['line' => $lineIdx + 1, 'col' => $offset - $lineStart + 1];

Tokens enthalten Rechtschreibung, Art, Ort und – entscheidend fĂŒr den PrĂ€prozessor – eine Ausblende-Menge:

// src/Compiler/Common/Token.php
final readonly class Token
{
  /**
   * @param list<string> $hideSet Names that should NOT be expanded for this token (blue-painting)
   */
  public function __construct(
    public TokenKind $kind,
    public string $spelling,
    public SourceLoc $loc,
    public array $hideSet = [],
  ) {
  }

  public function withHideSet(array $newHideSet): self
  {
    // ...
    return new self(
      kind: $this->kind,
      spelling: $this->spelling,
      loc: $this->loc,
      hideSet: $newHideSet,
    );
  }
}

TokenKind ist eine kleine AufzĂ€hlung: Bezeichner, SchlĂŒsselwörter, Integer-/Gleitkomma-/Zeichen-/Zeichenkettenliterale, Satzzeichen und EOF. Die Speicherung der Positionen jedes Tokens ermöglicht Diagnosen der Form file.c:Zeile:Spalte: Fehler: ..., wenn spĂ€tere Schritte fehlschlagen.

Dies ist auch eine typisch PHP-Struktur: unverÀnderliche Wertobjekte, Arrays von Token und instanceof/match-Aufrufe spÀter in der Verarbeitungskette.

Erstellen eines C-PrÀprozessors in PHP

Der PrÀprozessor ist keine String-Substitution. Es handelt sich um eine Token-Maschine mit bedingter Kompilierung und rekursiver Makro-Expansion.

Preprocessor::processTokens() durchlÀuft einen indizierten Tokenstrom. Ein Bedingungsstapel verfolgt #if / #elif / #else / #endif mit Frames der Form {parentActive, anyBranchTaken, currentActive, seenElse}. Nur wenn jeder Frame aktiv ist, gelangen Token in den Expansionspfad. Inaktive Bereiche parsen weiterhin Direktiven, sodass die Verschachtelung ausgeglichen bleibt.

Wenn die Region aktiv ist, werden nicht-direktive Token bis zum nĂ€chsten Zeilenanfang # gesammelt und dann an MacroExpander::expand() ĂŒbergeben:

// src/Compiler/Preproc/Preprocessor.php (excerpt)
if ($isActive()) {
  $batch = [];
  while ($i < count($tokens)) {
    $t = $tokens[$i];
    if ($t->kind === TokenKind::Punct && $t->spelling === '#'
        && $this->isAtLineStart($output, $tokens, $i)) {
      break;
    }
    $batch[] = $t;
    ++$i;
  }
  if ($batch !== []) {
    array_push($output, ...$this->expander->expand($batch));
  }
  continue;
}

#if-AusdrĂŒcke werden nach der Umformung mit defined(...) durch denselben Expander erweitert und anschließend als konstante AusdrĂŒcke ausgewertet.

Makro-Erweiterungs- und AusblendesÀtze

MacroExpander (in MacroTable.php) verwendet einen indexierten Cursor ĂŒber der ursprĂŒnglichen Eingabe sowie einen umgekehrten Stapel fĂŒr das erneute Scannen. Erweiterungen werden in umgekehrter Reihenfolge hinzugefĂŒgt, sodass das nĂ€chste zu verarbeitende Token das erste Token der Erweiterung ist – klassische Rescan-Semantik, ohne wiederholt array_shift() auf eine große Liste anzuwenden.

Wenn ein Bezeichner ein Makro benennt und nicht durch seine Ausblendmenge deaktiviert ist, fĂŒgt der Expander den Makronamen der Ausblendmenge der Ersetzungstoken hinzu („blaue Zeichnung“). Dadurch wird eine Endlosrekursion verhindert, wenn ein Makro zu seinem eigenen Namen expandiert.

// src/Compiler/Preproc/MacroTable.php (excerpt)
if ($token->hideSet !== [] && in_array($token->spelling, $token->hideSet, true)) {
  $output[] = $token;
  continue;
}

$macro = $this->table->lookup($token->spelling);
// ...
$newHideSet = $this->unionHideSet($token->hideSet, [$macro->name]);
$expanded = $this->expandObjectLike($macro, $token->loc, $newHideSet);
for ($i = count($expanded) - 1; $i >= 0; --$i) {
  $pendingReversed[] = $expanded[$i];
}

FunktionsĂ€hnliche Makros analysieren Argumentlisten, unterstĂŒtzen die String-Ersetzung mit # und das EinfĂŒgen mit ## und zeichnen versteckte Mengen auf Rumpf-Tokens. ObjektĂ€hnliche Makros sind die einfachere Variante desselben Mechanismus.

Die Substitution durchlĂ€uft den Makrokörper Token fĂŒr Token. Parameternamen werden durch die entsprechenden Argument-Token-Listen ersetzt (die selbst Expansionsregeln unterliegen), # wandelt ein Argument in ein String-Literal-Token um und ## fĂŒgt benachbarte Token zu einer neuen Schreibweise zusammen. __VA_ARGS__ wird fĂŒr variadische, funktionsĂ€hnliche Makros verwendet. Jedes erzeugte Token enthĂ€lt eine Ausblendungsmenge, die das aktuell expandierte Makro umfasst, sodass bei einem spĂ€teren erneuten Scan nicht derselbe Name durch dasselbe markierte Token erneut eingegeben wird.

FĂŒr technisch versierte Leser ist Folgendes wichtig: Es handelt sich hier um ein echtes PrĂ€prozessor-Subsystem. Die Amalgamierung von SQLite ist makrointensiv; ohne Hide-Sets, Argumentensammlung, Stringifizierung/EinfĂŒgen und bedingte Kompilierung wĂŒrde der Parser niemals einen zusammenhĂ€ngenden Tokenstrom erhalten.

Eine frĂŒhe Regressionstest-Fixture, 011-macro-eof, kodiert einen Sonderfall, bei dem EOF in Kombination mit Makroersetzung und bedingter Auswertung die Vorverarbeitung beeintrĂ€chtigt. Diese Fixture-Form ist typisch fĂŒr das Projekt: Der Amalgamierungsfehler wird in ein winziges Programm reduziert, das dennoch abstĂŒrzt, wenn der PrĂ€prozessor regressiv arbeitet. Eine weitere frĂŒhe Fixture, 012-octal-escape, befindet sich eine Stufe spĂ€ter im Lexer/String-Decoder – "\040" muss zum korrekten Byte werden –, aber sie ist Teil derselben Erkenntnis: Die „langweiligen“ lexikalischen Regeln von C sind tragend, sobald echte Header verwendet werden.

C ohne Parsergenerator parsen

Der Parser ist eine handgeschriebene Hybridlösung aus rekursivem Abstieg und Pratt-Algorithmus ĂŒber dem Token-Array. Es gibt keine Yacc-Grammatik. Deklarationen, Anweisungen und AusdrĂŒcke sind Methoden der Klasse Parser.

AST-Knoten sind gewöhnliche PHP-Klassen in src/Compiler/Common/Ast.php. ReprÀsentative Formen:

final class FuncDecl extends Decl
{
    public function __construct(
        public string $name,
        public CType $returnType,
        public array $params,
        public bool $variadic,
        SourceLoc $loc,
        public ?CompoundStmt $body = null,
        public StorageClass $storageClass = StorageClass::None,
        public bool $isInline = false,
    ) {
        parent::__construct($loc);
    }
}

final class BinaryExpr extends Expr
{
    public function __construct(
        public BinaryOp $op,
        public Expr $left,
        public Expr $right,
        SourceLoc $loc,
        public ?CType $resolvedType = null,
    ) {
        parent::__construct($loc);
    }
}

final class CallExpr extends Expr
{
    public function __construct(
        public Expr $function,
        public array $arguments,
        SourceLoc $loc,
        public ?CType $resolvedType = null,
    ) {
        parent::__construct($loc);
    }
}

final class CompoundLiteralExpr extends Expr
{
    public function __construct(
        public CType $type,
        public Expr $initList,
        SourceLoc $loc,
        ?CType $resolvedType = null,
    ) { /* ... */ }
}

Ausdrucksknoten können nach der semantischen Analyse einen resolvedType enthalten; der Parser erstellt zuerst die Struktur. Die semantische Analyse annotiert und typisiert direkt, anstatt eine vollstÀndig separate IR zu erzeugen.

Warum C-Deklaratoren schwierig sind

Die Deklarationsgrammatik von C ist eine klassische Falle. Es handelt sich nicht um denselben Typ:

int *p[4];      /* array of 4 pointers to int */
int (*p)[4];    /* pointer to array of 4 int */

parseDeclarator() implementiert diese Unterscheidung. Der ungruppierte Pfad wendet zuerst * an, dann umschließt [N] den aktuellen Typ – sodass *p[4] zu einem Array von Zeigern wird. Der gruppierte Pfad (*p) erzeugt einen Zeiger, kann Dimensionen innerhalb der Klammern sammeln und wendet dann mit applyArraySuffix() nachfolgende Suffixe an, wodurch [N] an den Zieltyp neu gebunden wird, wenn der aktuelle Typ bereits ein Zeiger ist.

// src/Compiler/Parser/Parser.php
private function applyArraySuffix(CType $type, ?int $count): CType
{
    if ($type instanceof PointerCType) {
        $to = $type->to;
        $array = $count !== null
            ? new ArrayCType($to, $count)
            : new IncompleteArrayCType($to);

        return new PointerCType($array);
    }

    return $count !== null
        ? new ArrayCType($type, $count)
        : new IncompleteArrayCType($type);
}

Diese Hilfsfunktion existiert, weil eine naive Korrektur von int (*p)[N], bei der das Array-Suffix auf den gesamten Typ angewendet wird, versehentlich int *p[N] ĂŒberschreiben und dadurch echten Code – historisch gesehen auch den SQLite-Pfad – beschĂ€digen kann. Die Fixture 051-pointer-to-array stellt die korrekte Lesart sicher.

int a[2][2] = { {1, 2}, {3, 4} };
int (*p)[2] = a;
return (*p)[0] + (*p)[1];

Zusammengesetzte Literale und designierte Initialisierer

Zusammengesetzte Literale werden in der Cast-Position erkannt: (type){ ... } wird zu CompoundLiteralExpr anstatt zu CastExpr, wenn nach der schließenden Klammer ein Initialisierer in geschweiften Klammern folgt.

Designierte Initialisierer parsen eine C99-Designatorliste (.field/[index]wiederholt, dann=) entweder in einen einzelnen Designator oder einen verschachtelten Pfad, der in InitListExprgespeichert wird. Dieser Pfad wird spĂ€ter fĂŒr verschachtelte Struktur- und Array-Elemente normalisiert – die Funktionsweise hinter der Fixture044-nested-designated-init`.

Darstellung von C-Datentypen in PHP

Alle Kerntypklassen befinden sich in src/Compiler/Common/CType.php: primitive Datentypen, PointerCType, ArrayCType, IncompleteArrayCType, FunctionCType, StructCType, UnionCType, EnumCType, Qualifizierer und typedef-Wrapper.

Fragen, die der Compiler wiederholt stellt – GrĂ¶ĂŸe, Ausrichtung, „Ist das eine ganze Zahl?“ – sind Methoden von CType, die match (true) und instanceof verwenden:

public function isInteger(): bool
{
    return match (true) {
        $this instanceof BoolCType,
        $this instanceof CharCType,
        // ...
        $this instanceof EnumCType => true,
        $this instanceof QualifiedCType => $this->base->isInteger(),
        $this instanceof TypedefCType => $this->base->isInteger(),
        default => false,
    };
}

Da EnumCType ein Integer-Typ ist, ist dies fĂŒr die ABI relevant: Enum-Parameter mĂŒssen wie andere Integer aus Allzweckregistern ausgelagert werden. GrĂ¶ĂŸe und Ausrichtung folgen einem LP64-Ă€hnlichen Layout (int 4, Zeiger 8, Enum 4; Strukturen/Unions verwenden die in ihrem Datensatz definierte GrĂ¶ĂŸe und Ausrichtung). Arrays multiplizieren die ElementgrĂ¶ĂŸe mit der Anzahl; unvollstĂ€ndige Arrays haben keine GrĂ¶ĂŸe.

Die Spannung zwischen Arrays und Zeigern in C zeigt sich ĂŒberall. Arrays verlieren in vielen Ausdruckskontexten an Bedeutung, bleiben aber in Deklaratoren und bei der Zeigerarithmetik erstklassige Datentypen. Wenn nur einer dieser Aspekte korrekt umgesetzt wird, liefert man einen Compiler aus, der zwar linkt, aber trotzdem *(a+1) nicht ausfĂŒhrt.

Semantische Analyse und Symbolauflösung

Sema kann mehr als nur „Typen prĂŒfen“.

Es verwaltet verschachtelte Scope-Objekte (global, Funktion, Block), eine typedef-Map, Datensatztypen und eine öffentliche enumConstants-Tabelle:

// src/Compiler/Sema/Sema.php (excerpt)
final class Sema
{
  private Scope $globalScope;
  private Scope $currentScope;
  /** @var array<string, int> */
  public array $enumConstants = [];

  public function analyze(array $decls): array
  {
    foreach ($decls as $decl) {
      $this->collectTopLevel($decl);
    }
    foreach ($decls as $decl) {
      $this->analyzeDecl($decl);
    }
    return $decls;
  }
}

Im ersten Durchlauf werden Funktionen, Variablen und Typdefinitionen der obersten Ebene eingefĂŒgt und EnumerationsfĂ€lle registriert. Im zweiten Durchlauf werden die TypĂŒberprĂŒfungen durchgefĂŒhrt. Die Enumerationsregistrierung schreibt sowohl die flache Map als auch eine globale Symbol::enumConstant(...):

private function registerEnumConstants(EnumType $enumType): void
{
  foreach ($enumType->cases as $case) {
    $this->enumConstants[$case->name] = $case->value;
    $this->globalScope->insert(Symbol::enumConstant(
      name: $case->name,
      value: $case->value,
      type: IntCType::$instance,
    ));
  }
}

Auch in Strukturen verschachtelte Typedef-Enumerationen und anonyme Enumerationen speisen diesen Registrierungspfad. Die Suche nach Bezeichnern durchlĂ€uft GĂŒltigkeitsbereiche und greift dann auf enumConstants zurĂŒck, sodass VALUE in int result = VALUE; selbst dann aufgelöst wird, wenn die einzige Deklaration ein Enumerationsfall war.

Scope ist eine ĂŒbergeordnete VerknĂŒpfung von Symbol-Werten (Variable, Function, Typedef, EnumConstant). FunktionsrĂŒmpfe öffnen einen Funktionsbereich unter global; zusammengesetzte Anweisungen verschachteln sich weiter. Parameter werden vor der Analyse des Funktionsrumpfs eingefĂŒgt.

Der Treiber ĂŒbergibt dann enumConstants explizit an codegen:

$codegen = new Codegen($enumConstants);

Durch dieses Design wird das stÀndige Zusammenfassen von Enum-Namen in statischen Initialisierern zu einem gemeinsamen Vertrag zwischen Sema und dem Backend, und nicht zu einer zweiten unabhÀngigen Nachschlagetabelle, die wÀhrend der Emission erstellt wird.

Ein konkreter Ablauf fĂŒr einen globalen Enum-Initialisierer sieht folgendermaßen aus:

enum { VALUE = 42 };
int result = VALUE;

Der Parser erstellt einen EnumDecl- bzw. Enum-Typ mit dem Fall VALUE = 42 und anschließend einen VarDecl, dessen Initialisierer ein Identifier("VALUE") ist. Semas erster Durchlauf registriert VALUE → 42 in enumConstants und im globalen GĂŒltigkeitsbereich. Im zweiten Durchlauf wird der Initialisierer als Integer typgeprĂŒft. Beim Ausgeben des globalen result löst die Codegenerierung den Bezeichner ĂŒber die Enum-Konstanten-Map auf und gibt eine numerische Direktive aus – typischerweise .long 42 – anstatt einer symbolischen Relokation. Die Fixture 015-enum-global verhindert, dass dieser Pfad stillschweigend auf „nicht deklarierter Bezeichner“ oder „Null ausgeben“ zurĂŒckfĂ€llt.

Die Ausdruckstypisierung deckt auch die alltĂ€glichen Fallstricke ab, denen SQLite immer noch unterliegt: Array-Zerfall in den meisten Ausdruckskontexten, Anforderungen an L-Werte fĂŒr Zuweisungen und das unĂ€re & sowie Besonderheiten an Anweisungspositionen wie (void)sizeof(x); (Fixture 013-sizeof-stmt). sizeof wird als unĂ€rer Operator interpretiert, der entweder einen Ausdruck oder einen in Klammern gesetzten Typnamen akzeptieren kann; eine fehlerhafte Klassifizierung an Anweisungspositionen ist ein Parserfehler, der in return 42 nie auftritt.

Konstante AusdrĂŒcke und statische Initialisierung

SQLite lebt von statischen Daten: Tabellen, Zeichenketten, Zeigern auf Arrays, zusammengesetzten Initialisierern. Globale Datenausgabe ist daher keine Randnotiz.

Codegen::emitInitializer() wĂ€hlt Assembler-Direktiven aus dem Zieltyp aus. Schmale Ganzzahlen und Zeichen werden zu .byte; breitere Ganzzahlen zu .long / .quad; Arrays werden pro Element rekursiv durchlaufen und der Rest mit .zero aufgefĂŒllt; Stringliterale in Zeichenarrays kopieren Bytes und fĂŒllen auf. Globale Variablen fĂŒr Zeichenarrays werden durch die Fixture 010-char-array-global sichergestellt, die .byte erfordert und .quad 84 im Assembler verbietet.

Eine Live-Kompilierung dieser Vorrichtung gibt derzeit Folgendes aus:

.section __DATA,__data
.globl _table
.p2align 3
_table:
.byte 84
.byte 92
.byte 134
.byte 0

Diese Ausgabe hĂ€ngt davon ab, ob instanceof ArrayCType mit der tatsĂ€chlichen Klasse in App\Compiler\Common ĂŒbereinstimmt. In PHP wird ein ArrayCType-Name innerhalb von App\Compiler\CodeGen zu App\Compiler\CodeGen\ArrayCType aufgelöst, sofern er nicht importiert wird. Ein fehlendes:

use App\Compiler\Common\ArrayCType;

Der Array-Zweig war nicht erreichbar. Globale Variablen wurden ĂŒber einen lĂ€ngeren Pfad verarbeitet und gaben .quad anstelle von .byte aus. Das Programm ließ sich zwar assemblieren und linken, das Laufzeit-Layout war jedoch weiterhin fehlerhaft. Es handelt sich hierbei um einen sprachĂŒbergreifenden Fehler im wahrsten Sinne des Wortes: Die PHP-Namensauflösung hat das statische Datenlayout von C beschĂ€digt.

Globale Zeigerinitialisierer (&arr[i]), vorlĂ€ufige Definitionen und globale zusammengesetzte Literale sind verwandte Probleme in derselben Nachbarschaft: Das Backend muss nicht nur „eine ganze Zahl ausgeben“, sondern auch „eine verschiebbare Adresse ausgeben“, „vorlĂ€ufige Definitionen zusammenfĂŒhren“ und „ein konstantes Aggregat im Datenteil materialisieren“ verstehen.

Von typisiertem AST zu ARM64

Das Backend ist src/Compiler/CodeGen/Codegen.php, die Registrierungskonventionen befinden sich in Arm64.php:

const SCRATCH_REGS = [
    Arm64Reg::X9, Arm64Reg::X10, Arm64Reg::X11, Arm64Reg::X12,
    Arm64Reg::X13, Arm64Reg::X14, Arm64Reg::X15,
];

const ARG_REGS = [
    Arm64Reg::X0, Arm64Reg::X1, Arm64Reg::X2, Arm64Reg::X3,
    Arm64Reg::X4, Arm64Reg::X5, Arm64Reg::X6, Arm64Reg::X7,
];

RegAlloc stellt temporĂ€re Register bereit. Argumente werden gemĂ€ĂŸ der AAPCS64-Konvention in diesem Backend in den Formaten x0 bis x7 ĂŒbergeben. RĂŒckgabewerte verwenden x0 (und x1 fĂŒr grĂ¶ĂŸere kleine Aggregate). Der Assembler-Code wird als Strings akkumuliert und als Text ausgegeben – eine weitere PHP-eigene Vorgehensweise.

Eine konkrete Funktion

Gegeben:

int add(int a, int b)
{
    return a + b;
}

Dieser Compiler gibt Folgendes aus (reprÀsentativ):

.text
.globl _add
.p2align 2
_add:
stp x29, x30, [sp, #-16]!
mov x29, sp
sub sp, sp, #16
str x0, [x29, #-8]
str x1, [x29, #-16]
add x9, x29, #-8
ldrsw x9, [x9]
add x10, x29, #-16
ldrsw x10, [x10]
add x9, x9, x10
mov x0, x9
mov sp, x29
ldp x29, x30, [sp], #16
ret

Das im Lichte der Umsetzung zu lesen:

  1. Der Prolog speichert den Frame-Pointer und das Link-Register und setzt x29.
  2. Lokale Variablen/Parameter erhalten negative Offsets von x29; Argumente werden von x0/x1 gespeichert.
  3. LadevorgĂ€nge verwenden ldrsw fĂŒr vorzeichenbehaftete 32-Bit-Werte in 64-Bit-Register.
  4. add erzeugt die Summe in einem Hilfsregister und verschiebt sie dann nach x0.
  5. Der Epilog stellt sp aus x29 wieder her und gibt zurĂŒck

Die FramegrĂ¶ĂŸe wird anhand der lokalen Anforderungen berechnet und in einen Platzhalter sub sp, sp, #... eingefĂŒgt, der frĂŒh in emitFunction() ausgegeben wird. Die Ausrichtung auf 16 Bytes ist auf AArch64 wichtig; das Backend rundet temporĂ€re Speicherbereiche entsprechend.

Stack-Frames und lokale Variablen

Die lokale Speicherverwaltung ist bewusst einfach und explizit. emitFunction() setzt den Zustand der jeweiligen Funktion zurĂŒck, erzeugt eine Platzhalter-Frame-Subtraktion und ermittelt dann, wie viel Speicherplatz Parameter, lokale Variablen und temporĂ€re Variablen benötigen:

$this->emitLine('stp x29, x30, [sp, #-16]!');
$this->emitLine('mov x29, sp');
$this->emitLine('sub sp, sp, #0  ; FRAME_SIZE_PLACEHOLDER');
$this->framePlaceholderIndex = count($this->lines) - 1;

ensureLocalSpace() vergrĂ¶ĂŸert ein laufendes localOffset, wobei jede Reservierung auf 8 Bytes ausgerichtet wird. allocLocal() speichert den gewĂ€hlten negativen Offset von x29 in localVarOffsets:

private function ensureLocalSpace(int $size): void
{
    $aligned = ($size + 7) & ~7;
    $this->localOffset += $aligned;
    if ($this->localOffset > $this->frameSize) {
        $this->frameSize = $this->localOffset;
    }
}

private function allocLocal(string $name, CType $type): void
{
    $size = $type->sizeInBytes() ?? 8;
    $this->ensureLocalSpace($size);
    $offset = -$this->localOffset;
    $this->localVarOffsets[$name] = $offset;
    $this->localVarTypes[$name] = $type;
}

Parameter, die in den Feldern x0–x7 eintreffen, werden sofort in diese Felder ausgelagert (str x0, [x29, #-8] usw.). Dadurch erhĂ€lt jede benannte lokale Variable eine stabile Adresse fĂŒr &var, zum Abrufen von Feldadressen und zum Neuladen nach Aufrufen. Große StrukturrĂŒckgaben (>16 Byte) reservieren außerdem Speicherplatz fĂŒr den unsichtbaren x8-sret-Zeiger und speichern ihn frĂŒhzeitig.

Wenn der Funktionskörper vollstĂ€ndig ausgegeben wird, wird die Platzhalterzeile mit dem eigentlichen sub sp, sp, # zusammengefĂŒgt. Die RahmengrĂ¶ĂŸe wird aufgerundet, damit sp 16-Byte-ausgerichtet bleibt – eine zwingende Voraussetzung unter AArch64. Der Epilog stellt sp aus x29 wieder her, entfernt das gespeicherte Paar und fĂŒhrt rets aus.

Zusammengesetzte temporĂ€re Variablen, kleine RĂŒckgabewerte und die Zwischenspeicherung von Aufrufargumenten konkurrieren um dieselben Frame-Ressourcen. Daher sehen ABI-Fehler oft wie zufĂ€llige Stack-BeschĂ€digungen aus, bis man bemerkt, dass eine temporĂ€re Variable mit der falschen GrĂ¶ĂŸe allokiert wurde oder ein verschachtelter Aufruf einen Speicherbereich ĂŒberschrieben hat, der noch ein nicht ausgewertetes Argument enthielt.

Zeigerarithmetik

BinĂ€re + / --Operatoren mit zeigerĂ€hnlichen Operanden skalieren die ganzzahlige Seite um die GrĂ¶ĂŸe des Zielobjekts. Arrays gelten in diesem Zusammenhang als zeigerĂ€hnlich.

// src/Compiler/CodeGen/Codegen.php (excerpt)
$leftIsPtrLike = $leftType->isPointer() || $leftType->isArray();
$rightIsPtrLike = $rightType->isPointer() || $rightType->isArray();
// ...
} elseif ($ptrType instanceof ArrayCType || $ptrType instanceof IncompleteArrayCType) {
    $t = $ptrType->of->unqualified();
    $pointeeSize = $t->isPointer() ? 8 : ($t->sizeInBytes() ?? 4);
}
if ($pointeeSize > 1) {
    // lsl #1/#2/#3 or mul by immediate size
}

Vor der Korrektur in Loop 32 konnte *(a+1) add ..., #1 in Bytes ausgeben. Die Fixture 035-array-ptr-arith erfordert nun eine Element-Skalierung.

Aufruf von Funktionen auf ARM64

emitCallExpr() wertet Argumente aus, gibt sie in einer festgelegten Reihenfolge ĂŒber den Stack aus, lĂ€dt x0–x7 (und Stack-Argumente darĂŒber hinaus) und ruft dann entweder bl _name fĂŒr direkte Aufrufe oder blr xn fĂŒr indirekte Aufrufe auf.

Funktionszeiger sind im Typsystem (PointerCType zu ​​FunctionCType) und in der Codegenerierung erstklassige Typen. Ein Minimalbeispiel:

int call(int (*fn)(int), int value)
{
    return fn(value);
}

Gibt eine indirekte Aufrufsequenz aus, die die Adresse des Aufgerufenen lĂ€dt und blr – nicht bl – fĂŒr ein festes Symbol verwendet. Direkte Aufrufe von main nach call verwenden weiterhin bl_call, wĂ€hrend die Adressierung von id adrp/add mit @PAGE-/@PAGEOFF-Relokationen verwendet.

Variadische Aufrufe

Variadische Aufrufe sind prĂ€ziser. Die Argumentauswertung kann Nebeneffekte haben; die Lebensdauer von Registern und Stacks interagiert ungĂŒnstig mit der naiven Ausgabe von links nach rechts, wodurch frĂŒhere Ergebnisse ĂŒberschrieben werden. Die Testumgebung 016-variadic-spill ist die reduzierte Form:

printf("%d%d%d", next(), next(), next());
return counter == 3 ? 42 : 1;

Die erwartete Standardausgabe ist 123, Exit-Code 42. Der printf-Ă€hnliche Pfad des Backends wertet Argumente in temporĂ€re Stack-Variablen aus, bevor der endgĂŒltige variadische Speicherbereich aufgebaut wird, sodass durch die Wiederverwendung von Registern keine Inkremente verloren gehen. Die Generierung syntaktisch gĂŒltigen ARM64-Codes ist wesentlich einfacher als die Implementierung einer Aufrufkonvention, die Argumente mit Seiteneffekten ĂŒbersteht.

Kleine aggregierte Renditen

Apple ARM64 gibt Aggregate von maximal 16 Bytes in x0 (und x1, falls grĂ¶ĂŸer als 8) zurĂŒck. emitSmallAggregateReturn() materialisiert den Wert in einem Stack-Slot und lĂ€dt anschließend x0/x1:

} elseif ($value instanceof CallExpr) {
    $this->emitExpr($value);
    $this->emitLine('str x0, ['.$addrReg->x().']');
    if ($size > 8) {
        $this->emitLine('str x1, ['.$addrReg->x().', #8]');
    }
}
// ...
$this->emitLine('ldr x0, [sp]');
if ($size > 8) {
    $this->emitLine('ldr x1, [sp, #8]');
}

Der CallExpr-Zweig existiert, weil die Weiterleitung von return a(); innerhalb einer anderen Funktion, die eine Struktur zurĂŒckgibt, das vollstĂ€ndige Registerergebnis des verschachtelten Aufrufs erhalten muss. Das Speichern nur einer 32-Bit-Ansicht von x0 fĂŒhrt zum Verlust eines Feldes. Dies ist Schleife 48 / Fixture 054-struct-return-chain.

Im generierten Assembler-Code fĂŒr b ist folgendes Muster zu erkennen: bl _a, dann str x0, [...], dann ldr x0, [sp] vor der RĂŒckgabe – das kleine Aggregat wird als Registerbreite-Wert behandelt, nicht als einzelnes w0, das aus einer skalaren Denkweise stammt.

Strukturen, Vereinigungen und zusammengesetzte Literale

Das Struktur- und Union-Layout wird beim Parsen der Datensatzdefinition berechnet und anschließend in einem RecordType gespeichert, der spĂ€ter von StructCType / UnionCType abgerufen wird. Bei Strukturen wird jedes Feld an seiner eigenen Ausrichtung ausgerichtet, Offsets werden akkumuliert und die GesamtgrĂ¶ĂŸe auf die maximale Ausrichtung des Datensatzes gerundet.

// src/Compiler/Parser/Parser.php (struct field layout excerpt)
$fieldAlign = $fieldType->alignOf() ?? 1;
$fieldSize = $fieldType->sizeInBytes() ?? 0;
$maxAlign = max($maxAlign, $fieldAlign);
$byteOff = ($byteOff + $fieldAlign - 1) & ~($fieldAlign - 1);
$fields[] = new RecordField(
    name: $fieldName,
    type: $fieldType,
    bitWidth: null,
    offset: $byteOff,
    bitOffset: 0,
);
$bitOffset = ($byteOff + $fieldSize) * 8;
// ...
$totalBytes = intdiv($bitOffset + 7, 8);
$totalBytes = ($totalBytes + $maxAlign - 1) & ~($maxAlign - 1);
$rec = new RecordType(name: $tag ?? '', fields: $fields, size: $totalBytes, alignment: $maxAlign);

Unions verwenden die maximale ElementgrĂ¶ĂŸe (wiederum auf die maximale Ausrichtung gerundet) und platzieren jedes Feld an Offset 0. Bitfelder existieren auch im Strukturpfad des Parsers; sie verfolgen Bit-Offsets innerhalb von Speichereinheiten. Codegenerierung verwendet Feld-Offsets beim Ausgeben von Elementzugriffen (add einer Konstanten zu einer Basisadresse) und beim Layouten globaler Strukturinitialisierer (EinfĂŒgen von .zero-AuffĂŒllung zwischen festgelegten Feldpositionen).

Diese Layoutinformationen sind es, die aggregierte Argumente, RĂŒckgabewerte und zusammengesetzte Literale ĂŒberhaupt erst ermöglichen: Ohne verlĂ€ssliche Offsets ist „Feld b einer 8-Byte-Struktur“ reine Spekulation.

Zusammengesetzte Literale werden durch Materialisierung eines temporÀren Objekts reduziert:

// emitAddr() path for CompoundLiteralExpr
if ($expr instanceof CompoundLiteralExpr) {
    // allocate frame slot, compute address in a register
    $this->emitLocalInit($reg, $expr->initList, $expr->type);
    return $reg;
}

Die Übergabe von (struct S){20, 22} als Argument bedeutet daher: Erstelle das temporĂ€re Objekt und ĂŒbergebe dann das Aggregat entsprechend seiner GrĂ¶ĂŸe aus dem Speicher in die Argumentregister oder den Stack – nicht „werte die Initialisierungsliste so aus, als wĂ€re sie nur das erste Feld“. Die Fixture 034-compound-lit-arg existiert, weil dieser Fehler 20 anstelle von 42 zurĂŒckgab.

Verschachtelte designierte Initialisierer ({.iy = 2, .ix = 1}, {.a[1] = 40}) erfordern, dass der Initialisierer-Normalisierer die Designatorpfade in verschachtelte Strukturen und Arrays durchlÀuft, gegebenenfalls auch durch anonyme Elemente. Die Testumgebung 044-nested-designated-init ist die reduzierte Probe.

Die Grenze der nativen Toolchain

Die Toolchain ist bewusst schlank gehalten:

// src/Compiler/Platform/Toolchain.php
public function assemble(string $asmPath, string $objectPath): ProcessResult
{
    return $this->run(['/usr/bin/as', $asmPath, '-o', $objectPath]);
}

public function link(array $objectPaths, string $outputExecutable, array $extraArgs = []): ProcessResult
{
    $args = array_merge(
        ['/usr/bin/clang', ...$objectPaths, '-o', $outputExecutable, '-lm', '-lpthread', '-ldl'],
        $extraArgs
    );
    return $this->run($args);
}

run() verwendet proc_open, erfasst stdout/stderr und gibt einen Exit-Code zurĂŒck. Das genĂŒgt. PHP schreibt .s; as schreibt .o; der Clang-Treiber verlinkt. Clang kompiliert den C-Quellcode im Akzeptanzpfad weiterhin nicht.

SQLite als Integrationsorakel

Die Kompilierung von return 42 beweist, dass die Pipe verbunden ist. Die Kompilierung von SQLite beweist, dass die Subsysteme interagieren.

Das Testsystem (fixtures/compiler/sqlite_smoke_test.c) öffnet :memory:, erstellt eine Tabelle, fĂŒgt den Wert 'darkwood' ein, wĂ€hlt ihn aus und gibt den Text aus. Dabei werden eine große Übersetzungseinheit, makrolastige Header, globale Daten, Funktionszeiger, Strukturen, AufzĂ€hlungen und so viele Aufrufkonventionen beansprucht, dass fehlerhafte SpeicherĂŒberlĂ€ufe zur Laufzeit fehlschlagen, selbst wenn der Assembler-Code scheinbar korrekt ist.

SQLite ist keine Standardsuite. Es ist ein Integrationsorakel: Viele Funktionen mĂŒssen gleichzeitig nahezu perfekt sein, sonst schlĂ€gt der Funktionstest fehl.

Fehler in der Implementierung aufgedeckt

Das wichtigste technische Material in diesem Projekt sind die Fehler, die erst durch das Aufeinandertreffen von Abstraktionen deutlich wurden. Jeder der folgenden FĂ€lle folgt demselben Muster: kleines C-Programm, erwartetes Verhalten, fehlerhaftes Verhalten, Ursache in PHP, Korrektur, Fix.

Fallstudie: ArrayCType und beschÀdigte statische Daten

Programm (010-char-array-global):

static const unsigned char table[4] = {84, 92, 134, 0};
int main(void) { return table[0]; }

Erwartet: Exit-Code 84; Assembly enthÀlt .byte, nicht .quad 84.

Fehlermodus: Fehlende use App\Compiler\Common\ArrayCType; in Codegen.php fĂŒhrte dazu, dass instanceof ArrayCType die falsche Klasse testete. Der Zweig zur Array-Initialisierung wurde nie ausgefĂŒhrt. Weitergehende Direktiven fĂŒhrten zu einer fehlerhaften Tabellenstruktur.

Behebung: Importiere die eigentliche Klasse; gib fĂŒr Zeichenarrays jeweils ein .byte-Element aus. Explizit in den Projektstatusnotizen vermerkt und durch Assertions der Fixture-Assembly gesperrt.

Lektion: Ein PHP-Namensraumfehler ist ein C-ABI-/Layoutfehler. „Links“ bedeutet nicht „funktioniert“.

Fallstudie: Arithmetische Skalierung von Array-Pointern

Programm (035-array-ptr-arith):

int a[3] = {10, 20, 12};
int *p = a + 1;
return *(a + 2) == 12 && *p == 20 ? 42 : 0;

Erwartet: Exit-Code 42 (auch im Vergleich zu clang).

Fehlermodus: a + 1 wurde um Bytes erhöht, da Arrays bei der Skalierung nicht als zeigerÀhnlich behandelt wurden.

Fix: In emitBinaryExpr wird ArrayCType wie ein Zeiger fĂŒr +/- behandelt und mit sizeof(element) ĂŒber lsl/mul skaliert.

Lektion: Array-Decay und Pointee-Skalierung sind nicht derselbe Fehler, aber sie treten nebeneinander auf.

Fallstudie: Zeiger-auf-Array-Deklarationen

Programm (051-Zeiger-auf-Array):

int (*p)[2] = a;
return (*p)[0] + (*p)[1];

Erwartet: KlangÀhnlichkeit.

Fehlermodus: Die Anwendung des Deklaratorsuffixes, die (*p)[N] repariert hat, könnte *p[N] beschÀdigen, wenn sie zu weit gefasst angewendet wird.

Korrektur: Schließen Sie den gruppierten (*name)-Deklarator vor nachfolgenden Array-Suffixen. Verwenden Sie applyArraySuffix(), damit [N] an den Zeiger eines Zeigertyps gebunden wird. Der ungruppierte Pfad zur Erstellung von Zeiger-Arrays bleibt unverĂ€ndert.

Lektion: C-Deklaratoren stellen ein Parserproblem mit semantischen Konsequenzen dar; Fixtures mĂŒssen beide Lesarten abdecken.

Fallstudie: Zusammengesetzte Literalargumente

Programm (034-compound-lit-arg):

return f((struct S){20, 22});

Erwartet: 42.

Fehlermodus: Bei der Auswertung eines zusammengesetzten Literals / einer Initialisierungsliste als Wert wurde nur das erste Feld zurĂŒckgegeben (20).

Korrektur: Materialisiere das zusammengesetzte Literal in einem temporĂ€ren Frame in emitAddr und ĂŒbergebe dann die ≀16-Byte-Struktur aus dem Speicher in die Argumentregister.

Lektion: Ephemere Aggregate benötigen Adressen, nicht „erste Skalare“.

Fallstudie: Verschachtelte designierte Initialisierer

Programm (044-nested-designated-init):

struct Outer o = {.z = 3, .i.y = 2, .i.x = 1};
struct Arr s = {.a[1] = 40, .a[0] = 2};

Erwartet: Die Werte der Felder und Array-Slots stimmen mit den Messwerten von clang ĂŒberein.

Fehlermodus: Einzelebenenbezeichner funktionierten; verschachtelte Pfade und Array-Bezeichner innerhalb von Strukturen funktionierten nicht.

Behebung: Bezeichnungslisten in Pfade parsen; diese Pfade beim Anwenden von Initialisierern normalisieren, auch ĂŒber verschachtelte und anonyme Elemente hinweg.

Lektion: C99-Initialisierer-Zucker ist nicht optional, wenn man „echtes C“ gegen SQLite-Ă€hnlichen Code behaupten will.

Fallstudie: Variadische Leckagen

Programm (016-variadic-spill):

printf("%d%d%d", next(), next(), next());

Erwartet: stdout 123, exit 42.

Fehlermodus: Die Reihenfolge der Argumentauswertung / die Lebensdauer des Registers wurde durch vorherige next()-Ergebnisse vor dem Aufruf zerstört.

Korrektur: Auswertung in temporĂ€re Stack-Objekte, anschließend Zusammenstellung des variadischen Aufrufs (der printf-förmige Pfad in emitCallExpr).

Lektion: Argumente mit Seiteneffekten sind ein ABI-Test, kein Parsing-Test.

Fallstudie: Struktur-RĂŒckgabeketten

Programm (054-struct-return-chain):

struct S b(void) { return a(); }
// main: struct S s = b(); return s.a + s.b;

Erwartet: Exit-Code 3 (clang oracle).

Fehlermodus: Verschachtelte AggregatrĂŒckgabe, nur ein Teil von x0 wurde beibehalten (symptomatisch Exit 2).

Korrektur: In emitSmallAggregateReturn() wird, wenn der zurĂŒckgegebene Ausdruck ein CallExpr ist, das vollstĂ€ndige x0 und gegebenenfalls x1 gespeichert, bevor der RĂŒckgabewert der aktuellen Funktion neu geladen wird.

Lektion: Das Generieren von ret nach bl ist nicht dasselbe wie die Implementierung der aggregierten RĂŒckgabeweiterleitung.

Von Fehlern zu Regressionstests

Die Methodik ist wichtiger als jedes einzelne KEEP:

SQLite failure (or clang mismatch)
  → identify subsystem
  → reduce to a tiny C program
  → compare observable behavior
  → create fixtures/compiler/NNN-*.c + .json
  → fix one narrow behavior
  → run app:compiler-fixtures
  → run app:compiler-sqlite
  → KEEP only if still valid

Fixture-Metadaten können Exit-Codes, stdout, Assembler-Teilstrings und compareWithClang ĂŒberprĂŒfen. Assembler-Assertions decken Layoutfehler auf, die ein zufĂ€lliger Exit-Code möglicherweise ĂŒbersehen wĂŒrde. Die Suite, die mit 46/46 endet, ist keine zufĂ€llige ZĂ€hlung; sie speichert Interaktionen, die bereits einmal auf die Amalgamation zugegriffen haben.

Differenzialsonden

Bei kleinen Programmen liefert das Kompilieren und AusfĂŒhren desselben Quellcodes mit Clang ein ausfĂŒhrbares Orakel: Exit-Code und Standardausgabe. Befehle wie app:compiler-compare-clang und Probe-Flows unterstĂŒtzen diesen Workflow. Clang ist hier ein Laufzeit-Differenzierungstool und nicht der Compiler von sqlite3.c im Akzeptanzpfad.

Diese Unterscheidung ist wichtig. Differenzielle Tests beantworten die Frage: „Stimmt unsere ABI mit dem beobachtbaren Verhalten dieser Probe ĂŒberein?“ Akzeptanztests beantworten die Frage: „Kann unser Compiler SQLite kompilieren?“

Wo Darkwood Flow passt

Die Kompilierung enthĂ€lt zwei verschiedene Pipelines. Eine Vermischung dieser Pipelines fĂŒhrt entweder zu einer nutzlosen Abstraktion oder zu einer privaten Neuimplementierung von etwas, das eigentlich gemeinsam genutzt werden sollte.

Die algorithmische Pipeline ist eng gekoppelt:

tokens → AST → semantic model → assembly

Diese Phasen tauschen compilerspezifische Strukturen aus – Token-Listen, Decl-BĂ€ume, CType-Graphen, Register-Allokatoren. Die Einbettung jedes Analyseschritts in ein Orchestrierungs-Framework wĂŒrde nichts klĂ€ren. Der Hide-Set-Algorithmus wird nicht besser, nur weil er eine Job-Warteschlange durchlĂ€uft.

Die operative AblÀufe sind anders:

compile → assemble → link → execute → validate

Jeder Schritt hat eine klare Artefaktgrenze, kann unabhÀngig fehlschlagen, die Zeitmessung im gemeinsamen Zustand aufzeichnen und kann ersetzt werden, ohne den Lexer neu zu schreiben. Genau dort gehört Darkwood Flow hin.

Dieses Projekt benötigt das Composer-Paket darkwood/flow v8.1.5. Der Compiler-Kern unter src/Compiler/ importiert Flow nicht. Die Ă€ußeren Workflows unter src/Flow/ hingegen schon.

SqliteValidationFlow erzeugt zeitgesteuerte Jobs. Die API-OberflĂ€che ist bewusst klein gehalten: Eine FlowFactory erstellt einen Flow aus einem Generator von Jobs; jeder Job empfĂ€ngt ein Ip (Informationspaket) mit dem SqliteValidationState; await() fĂŒhrt die Sequenz bis zum Abschluss aus.

// src/Flow/SqliteValidationFlow.php
$flow = (new FlowFactory())->create(function () {
    yield $this->timed('compile_sqlite', $this->compileSqlite);
    yield $this->timed('assemble_sqlite', $this->assembleSqlite);
    yield $this->timed('compile_harness', $this->compileHarness);
    yield $this->timed('assemble_harness', $this->assembleHarness);
    yield $this->timed('link', $this->linkSmokeExecutable);
    yield $this->timed('run', $this->runSmokeTest);
});

$flow(new Ip($state));
$flow->await();

Der timed()-Wrapper ist normales PHP um ein JobInterface: Die restliche Arbeit wird ĂŒbersprungen, wenn failure bereits gesetzt ist; der Job wird ausgefĂŒhrt und die verstrichenen Millisekunden in $state->timings gespeichert. Konkrete Jobs (CompileSqlite, AssembleSqlite, CompileHarness, AssembleHarness, LinkSmokeExecutable, RunSmokeTest) rufen Compiler und Toolchain auf. Flow kennt den Begriff Token nicht.

Aus Sicht des Benutzers gibt es einen Befehl:

php -d memory_limit=4G bin/console app:compiler-sqlite

Intern wird dies in unabhĂ€ngig voneinander sinnvolle Phasen unterteilt, die SqliteValidationState gemeinsam nutzen: PHP erzeugt sqlite3.s, as erzeugt sqlite3.o, PHP erzeugt die Harness-Assemblierung, as erzeugt das Harness-Objekt, der Linker erzeugt eine ausfĂŒhrbare Datei, der Prozess gibt darkwood aus, und die Validierung bestĂ€tigt den Erfolg. Verwandte AblĂ€ufe (CompileFlow fĂŒr das Kompilieren/Assemblieren/Linken einer einzelnen Datei, ProbeRunFlow fĂŒr Clang-Baseline-Probes) wenden dasselbe Prinzip in kleinerem Maßstab an.

Es gab eine architektonische Korrektur: Eine lokale, miniaturisierte Flow-Laufzeitumgebung unter src/Flow/Runtime/ wurde entfernt. Das Compiler-Experiment sollte nun das eigentliche Composer-Paket verwenden, anstatt eine private Kopie zu verwalten. Die endgĂŒltige Trennung sieht wie folgt aus:

sqlite-compiler-php
  ├── src/Compiler/   plain PHP algorithms
  └── src/Flow/       darkwood/flow orchestration

Ebenso wichtig ist, was Flow nicht leisten sollte. Es ersetzt weder AST-Knoten, implementiert keine Makroerweiterung, allokiert keine Register, erzeugt keinen ARM64-Code, ersetzt weder Symfony noch die Toolchain des Betriebssystems. Es ist keine Performance-Schicht und wurde nie eingefĂŒhrt, um die SQLite-Kompilierung zu beschleunigen. Sein Wert liegt in der Strukturierung und Transparenz eines mehrstufigen nativen Builds – eine praktische Anwendung von Darkwood Flow fĂŒr etwas ganz anderes als eine Webanfrage.

Kurz gesagt: Der Compiler zeigt, was natives PHP leisten kann; Darkwood Flow macht diese FĂ€higkeiten zu einem wiederholbaren Experiment.

Die Iterationsschleife

Die Entwicklung verlief als Entdeckungsprozess wÀhrend der Implementierung, nicht als eine einzige inspirierte Neuentwicklung.

Das Muster ist die differentielle Compilerentwicklung:

construct probe
  → compile with this compiler
  → compile/run with clang when useful
  → compare observable behavior
  → isolate mismatch
  → reduce fixture
  → inspect parser / sema / codegen
  → modify one narrow behavior
  → run fixture suite
  → run SQLite
  → KEEP only if still valid

Die nĂŒtzliche Aussage besteht nicht darin, dass ein Modell Tausende von PHP-Zeilen generieren kann. Sie besteht darin, dass ein Agent wiederholt mit echtem Compiler-Code, echtem as/clang, echten Fixtures und einem großen Akzeptanzziel interagieren kann – und anschließend nur die Änderungen beibehĂ€lt, die die Validierung bestehen.

Disziplin Ă€ußerte sich weiterhin in der Weigerung, verspĂ€tete Arbeiten zu erfinden, und im Stoppen der Arbeit, wenn weitere Änderungen durch fehlgeschlagene Untersuchungen nicht mehr gerechtfertigt waren.

Die Aufwachschleife

Nach den auf Korrektheit ausgerichteten Engineering-Schleifen durch Schleife (struct return chain / 054-struct-return-chain) wurde eine Wake-Schleife fortgesetzt, um alle fĂŒnf Minuten einen Agenten mit Anweisungen zum Suchen nach der nĂ€chsthöheren Verbesserung wiederaufzunehmen.

Drei Begriffe mĂŒssen klar voneinander abgegrenzt bleiben:

Begriff Bedeutung
Wake-Loop-Tick Eine periodische Wiederaufnahme: PrĂŒfen, Validieren, Entscheiden, ob etwas geĂ€ndert werden sollte.
Engineering-Schleife Ein nummerierter KEEP-Eintrag.
Beibehaltene Änderung Code, der die Validierung ĂŒberstanden hat und mit KEEP protokolliert wurde.

Ticks stellen keine CodeĂ€nderungen dar. Die Wake-Loop wurde beendet. Dazwischen liegt eine lange Sequenz von Ticks, die die Suite weiterhin als erfolgreich einstufte – 46/46, SQLite gibt weiterhin darkwood aus – ohne dass ein signifikanter Fehler vorlag, der ein weiteres KEEP rechtfertigen wĂŒrde.

HÀtte die Wake-Loop-Funktion ohne einen fehlgeschlagenen Test immer wieder Refaktorierungen erfunden, wÀre das nur Rauschen im Gewand des Fortschritts gewesen. Das interessante Ende ist methodischer Natur:

Letztendlich fĂŒhrte die RĂŒckkopplungsschleife zu Entscheidungen, bei denen keine Änderungen vorgenommen wurden.

Was der Compiler unterstĂŒtzt

GenĂŒgend C-Code, um den SQLite 3.46.0 Amalgamation Smoke Path auf macOS ARM64 zu kompilieren und auszufĂŒhren, plus ein Regressionsnetz mit 46 Fixtures, das PrĂ€prozessor-Edges, Globals, Compound Literals, Enums, Variadic Spill Patterns, Declarators, Designated Initializers und Aggregate ABI Cases abdeckt.

Was es bewusst nicht zu sein versucht

  • Keine Behauptung der vollstĂ€ndigen Einhaltung von C99/C11/C17
  • Kein CIR/IR-Optimierer konstruktionsbedingt
  • Kein Ersatz fĂŒr Clang/GCC im Produktiveinsatz
  • Keine vollstĂ€ndige Geschichte zur Darwin SDK-KompatibilitĂ€t
  • Die Floating-Point-ABI und andere grĂ¶ĂŸere LĂŒcken bleiben außerhalb der Leiste „SQLite Smoke + aktuelle Fixtures“.
  • KI beseitigt nicht die Notwendigkeit ausfĂŒhrbarer Akzeptanztests

Bevorzugen Sie die prÀzise Formulierung: genug von der C-Teilmenge, die von SQLite und den aktuellen Regressionstests verwendet wird.

Was ich gelernt habe

PHP kann eine nicht-triviale Compiler-Implementierung mit gewöhnlichen Objekten, Arrays, instanceof-Dispatch, String-basiertem Assembler-Code und proc_open fĂŒr native Tools hosten. Ein direkter Pfad Lexer → PrĂ€prozessor → Parser → SEMA → ARM64 kann genĂŒgend C verarbeiten, um die SQLite-Amalgamation zu kompilieren und einen echten Smoke-Harness auszufĂŒhren.

Die schwierigsten Fehler waren nicht etwa ein fehlendes Semikolon im Emitter. Es waren vielmehr Wechselwirkungen: PHP-NamensrĂ€ume vs. C-Layout, Äquivalenz von Arrays und Zeigern, DeklaratorbĂ€ume, Weiterleitung der Aggregat-ABI, Auswertungsreihenfolge von Variablen. SQLite erzwingt diese Wechselwirkungen. Fixtures speichern sie.

Darkwood Flow verdiente sich seinen Platz als Ă€ußere Pipeline. Es gehörte nicht in den Tokenstrom.

Die spĂ€te Nachlaufschleife lehrte eine weniger offensichtliche Lektion. Das Ausbleiben einer VerĂ€nderung ist ein Beweis dafĂŒr, dass die RĂŒckkopplungsschleife funktioniert – und dass das Experiment einen zu respektierenden Abbruchpunkt erreicht hat.

Wohin ich es als NĂ€chstes bringen wĂŒrde

Die nĂ€chsten Meilensteine ​​sind ambitionierter: eine breitere Gleitkomma-ABI, eine grĂ¶ĂŸere C-UnterstĂŒtzung ĂŒber die SQLite-Ă€hnliche Teilmenge hinaus, vielleicht Streaming oder schlankere Ausgabestrategien. Keiner dieser Punkte ist ein Kandidat fĂŒr eine weitere fĂŒnfminĂŒtige KEEP-Phase, und keiner sollte ohne die gleiche Fixture-plus-Smoke-Gate-Methode neu gestartet werden.

FĂŒr Darkwood liegt die wiederverwendbare Erkenntnis im architektonischen Bereich: Compiler-Algorithmen sollten als einfaches PHP beibehalten werden; operative Pipelines sollten in Flow implementiert werden; und es sollte darauf bestanden werden, dass „funktioniert“ bedeutet, dass Testdaten und ein brutaler Akzeptanztest vorliegen – und nicht nur, dass etwas zusammengebaut wurde.

Sources

  • Code source : https://github.com/matyo91/sqlite-compiler-php
  • Slides : https://github.com/matyo91/slidewire

Anmelden um auf diesen Beitrag zu reagieren

🚀 1

Site

  • Sitemap
  • Kontakt
  • Impressum

Network

  • Hello
  • Blog
  • Apps
  • Photos

Social

Darkwood 2026, alle Rechte vorbehalten