đš Ich habe einen C-Compiler in PHP geschrieben, der SQLite kompilieren kann.
vom 30. August 2026
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:
- Der Prolog speichert den Frame-Pointer und das Link-Register und setzt
x29. - Lokale Variablen/Parameter erhalten negative Offsets von
x29; Argumente werden vonx0/x1gespeichert. - LadevorgÀnge verwenden
ldrswfĂŒr vorzeichenbehaftete 32-Bit-Werte in 64-Bit-Register. adderzeugt die Summe in einem Hilfsregister und verschiebt sie dann nachx0.- Der Epilog stellt
spausx29wieder 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.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