đš J'ai créé un compilateur C en PHP capable de compiler du SQLite
le 30 août 2026
Je voulais savoir quelle part du compilateur C je pouvais implémenter en PHP avant qu'il ne soit capable de compiler SQLite.
à l'heure actuelle, la réponse est : suffisant. Sur macOS ARM64, une interface C hébergée par PHP et un générateur de code ARM64 transforment le code SQLite 3.46.0 en assembleur textuel. L'assembleur et l'éditeur de liens d'Apple génÚrent ensuite un exécutable natif. Un petit programme ouvre une base de données en mémoire, y insÚre une ligne, la sélectionne et l'affiche.
darkwood
Ce test d'acceptation est complété par une suite de tests de régression qui réussit 46/46 tests. Le pipeline de compilation/validation externe est orchestré avec darkwood/flow v8.1.5.
Cet article propose une visite guidĂ©e de cette implĂ©mentation : comment le code source devient des jetons, comment les macros sâĂ©tendent, comment les dĂ©clarateurs deviennent des types, comment lâanalyse sĂ©mantique alimente la gĂ©nĂ©ration de code, comment lâarchitecture ARM64 est gĂ©nĂ©rĂ©e et quels bogues ont contraint les abstractions Ă adopter une approche pragmatique. Le plus intĂ©ressant nâest pas un tableau de bord de performances, mais le PHP qui implĂ©mente le C.
Que signifie concrÚtement « compiler SQLite en PHP » ?
La précision est importante, car il est facile de survendre cette expression.
Le compilateur PHP implémente :
C source
â lexer / preprocessor
â parser
â AST
â semantic analysis
â ARM64 code generation
â .s assembly text
Les outils natifs d'Apple effectuent ensuite les opérations suivantes :
ARM64 .s
â Apple assembler (as)
â Mach-O object
objects
â system linker via the clang driver
â native executable
Clang n'est pas utilisĂ© pour compiler sqlite3.c dans le chemin d'acceptation. Il est utilisĂ© comme pilote de liaison et parfois comme oracle diffĂ©rentiel pour les petites requĂȘtes. PHP n'Ă©crit pas de code Mach-O. La limite intĂ©ressante est :
PHP implémente l'interface C et le générateur de code ARM64 ; l'assembleur et l'éditeur de liens de la plateforme finalisent l'exécutable natif.
L'architecture délibérément réduite
Il n'y a ni CIR, ni formulaire SSA, ni pipeline d'optimisation. Le pilote dans src/Compiler/Driver/Compiler.php est presque linéaire :
$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 prend en charge les commandes de la console (app:compiler-check, app:compiler-fixtures, app:compiler-sqlite, etc.). Elles ne font pas partie de l'algorithme de compilation. Le code pertinent se trouve dans src/Compiler/, sous forme de classes PHP classiques. Les fichiers d'en-tĂȘte sous include/ fournissent une interface minimale Darwin/libc pour le frontend â suffisante pour stdio, stdlib, les threads et les dĂ©clarations associĂ©es â sans nĂ©cessiter une réécriture complĂšte du SDK.
L'utilisation de l'assembleur textuel était un choix de portée. Générer un fichier .s permet d'hériter des diagnostics de l'assembleur d'Apple et d'un chemin de débogage lisible, sans avoir à créer un générateur Mach-O en PHP.
En image :
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
Fichiers sources et jetons
La compilation commence par l'identification de la source, et non par la grammaire.
SourceLoc est volontairement trÚs petit : un identifiant de fichier et un décalage en octets.
// src/Compiler/Common/SourceLoc.php
final readonly class SourceLoc
{
public function __construct(
public int $fileId,
public int $offset,
) {
}
}
La ligne et la colonne sont calculées ultérieurement par SourceManager::lineCol(). Le gestionnaire conserve le contenu du fichier et une table en cache des décalages de début de ligne, puis effectue une recherche dichotomique pour trouver le décalage de début le plus grand <= offset :
// 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];
Les jetons contiennent l'orthographe, le type, l'emplacement et â Ă©lĂ©ment crucial pour le prĂ©processeur â un ensemble de masquage :
// 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 est une petite Ă©numĂ©ration : identifiant, mot-clĂ©, littĂ©raux entiers/flottants/caractĂšres/chaĂźnes de caractĂšres, ponctuation et EOF. La conservation de lâemplacement de chaque jeton permet dâĂ©tablir des diagnostics de la forme file.c:line:column: error: ... aprĂšs lâĂ©chec des Ă©tapes ultĂ©rieures.
Il s'agit également d'une structure typiquement PHP : objets de valeur immuables, tableaux de jetons et répartition instanceof/match plus tard dans le pipeline.
Création d'un préprocesseur C en PHP
Le préprocesseur n'effectue pas de substitution de chaßnes de caractÚres. Il s'agit d'une machine à jetons avec compilation conditionnelle et expansion récursive de macros.
La fonction Preprocessor::processTokens() parcourt un flux de jetons indexé. Une pile de conditions suit les directives #if / #elif / #else / #endif avec des cadres de la forme {parentActive, anyBranchTaken, currentActive, seenElse}. Les jetons ne sont intégrés au chemin d'expansion que lorsque tous les cadres sont actifs. Les régions inactives continuent d'analyser les directives, ce qui permet de maintenir l'équilibre de l'imbrication.
Lorsque la région est active, les jetons non directifs sont regroupés jusqu'au prochain début de ligne #, puis transmis à MacroExpander::expand() :
// 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;
}
Les expressions #if sont dĂ©veloppĂ©es par le mĂȘme expandeur aprĂšs la réécriture defined(...), puis Ă©valuĂ©es comme des expressions constantes.
Ensembles d'expansion et de masquage de macros
MacroExpander (dans MacroTable.php) utilise un curseur indexĂ© sur l'entrĂ©e d'origine, ainsi qu'une pile d'attente inversĂ©e pour la rĂ©analyse. Les expansions sont effectuĂ©es en sens inverse, de sorte que le prochain jeton Ă traiter est le premier jeton de l'expansion â une sĂ©mantique de rĂ©analyse classique sans avoir Ă effectuer des appels rĂ©pĂ©tĂ©s Ă array_shift() sur une grande liste.
Lorsqu'un identifiant dĂ©signe une macro et n'est pas dĂ©sactivĂ© par son ensemble de masquage, l'extenseur fusionne le nom de la macro avec l'ensemble de masquage des jetons de remplacement (« peinture bleue »). Cela empĂȘche une rĂ©cursion infinie lorsqu'une macro se dĂ©veloppe en son propre nom :
// 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];
}
Les macros de type fonction analysent les listes d'arguments, prennent en charge la conversion en chaĂźne de caractĂšres avec # et le collage avec ##, et appliquent des ensembles de masquage aux jetons du corps. Les macros de type objet sont une version simplifiĂ©e de ce mĂȘme mĂ©canisme.
La substitution parcourt le corps de la macro jeton par jeton. Les noms des paramĂštres sont remplacĂ©s par les listes de jetons d'arguments correspondantes (elles-mĂȘmes soumises aux rĂšgles d'expansion), # convertit un argument en une chaĂźne littĂ©rale, et ## concatĂšne les jetons adjacents pour former une nouvelle orthographe. __VA_ARGS__ intervient pour les macros de type fonction variadique. Chaque jeton produit possĂšde un ensemble de masquage incluant la macro en cours d'expansion, de sorte qu'une analyse ultĂ©rieure n'entrera pas Ă nouveau le mĂȘme nom via le mĂȘme jeton colorĂ©.
Pour un lecteur technique : il sâagit dâun vĂ©ritable sous-systĂšme de prĂ©processeur. LâagrĂ©gation de SQLite repose fortement sur les macros ; sans les ensembles masquĂ©s, la collecte des arguments, la conversion en chaĂźne de caractĂšres et la compilation conditionnelle, lâanalyseur syntaxique ne pourrait jamais obtenir un flux de jetons cohĂ©rent.
Un des premiers tests de rĂ©gression, 011-macro-eof, encode un cas limite oĂč l'interaction de la fin de fichier (EOF) avec le remplacement de macros et l'Ă©valuation conditionnelle corrompt le prĂ©traitement. Ce type de test est une pratique courante du projet : rĂ©duire l'erreur de fusion Ă un minuscule programme qui plante toujours en cas de rĂ©gression du prĂ©processeur. Un autre test, 012-octal-escape, intervient une Ă©tape plus tard dans l'analyseur lexical/dĂ©codeur de chaĂźnes â "\040" doit ĂȘtre converti en octet correct â mais il illustre le mĂȘme principe : les rĂšgles lexicales « ennuyeuses » du C deviennent essentielles dĂšs l'apparition des vĂ©ritables en-tĂȘtes.
Analyse syntaxique du C sans générateur d'analyseur syntaxique
L'analyseur syntaxique est un hybride récursif descendant/Pratt écrit à la main, appliqué au tableau de jetons. Il n'utilise pas la grammaire yacc. Les déclarations, instructions et expressions sont des méthodes de Parser.
Les nĆuds AST sont des classes PHP ordinaires situĂ©es dans src/Compiler/Common/Ast.php. Exemples de formes :
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,
) { /* ... */ }
}
Les nĆuds d'expression peuvent contenir resolvedType aprĂšs l'analyse sĂ©mantique ; l'analyseur syntaxique construit d'abord la structure. L'analyse sĂ©mantique annote et vĂ©rifie les types directement, sans produire de rĂ©ponse intermĂ©diaire (IR) distincte.
Pourquoi les déclarateurs C sont difficiles
La grammaire dĂ©clarative du C est un piĂšge classique. Il ne s'agit pas du mĂȘme type :
int *p[4]; /* array of 4 pointers to int */
int (*p)[4]; /* pointer to array of 4 int */
La fonction parseDeclarator() implémente cette distinction. Le chemin non groupé applique d'abord *, puis [N] encapsule le type courant ; ainsi, *p[4] devient un tableau de pointeurs. Le chemin groupé (*p) construit un pointeur, peut collecter les dimensions entre parenthÚses, puis applique les suffixes de fin avec applyArraySuffix(), qui réaffecte [N] à la pointée lorsque le type courant est déjà un pointeur.
// 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);
}
Cette fonction auxiliaire existe car une correction simpliste consistant à « appliquer le suffixe de tableau à l'ensemble du type » pour int (*p)[N] peut accidentellement réécrire int *p[N] et casser du code réel, y compris, historiquement, le chemin d'accÚs à SQLite. La fixture 051-pointer-to-array verrouille la lecture correcte :
int a[2][2] = { {1, 2}, {3, 4} };
int (*p)[2] = a;
return (*p)[0] + (*p)[1];
Littéraux composés et initialiseurs désignés
Les littéraux composés sont reconnus en position de conversion : (type){ ... } devient CompoundLiteralExpr au lieu de CastExpr lorsqu'une accolade initiale suit la parenthÚse fermante.
Les initialiseurs dĂ©signĂ©s analysent une liste de dĂ©signateurs C99 (.field/[index]rĂ©pĂ©tĂ©, puis=) pour obtenir soit un dĂ©signateur unique, soit un chemin imbriquĂ© stockĂ© dans InitListExpr. Ce chemin est ensuite normalisĂ© pour les membres de structures et de tableaux imbriquĂ©s â le mĂ©canisme sous-jacent au fixture 044-nested-designated-init`.
Représentation des types C en PHP
Toutes les classes de types principales se trouvent dans src/Compiler/Common/CType.php : primitives, PointerCType, ArrayCType, IncompleteArrayCType, FunctionCType, StructCType, UnionCType, EnumCType, qualificateurs et wrappers typedef.
Les questions que le compilateur pose Ă plusieurs reprises â taille, alignement, « est-ce un entier ? » â sont des mĂ©thodes sur CType utilisant match (true) et instanceof :
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,
};
}
Le fait que EnumCType soit un type entier a une incidence sur l'ABI : les paramĂštres d'une Ă©numĂ©ration doivent ĂȘtre stockĂ©s dans des registres gĂ©nĂ©raux, comme les autres entiers. La taille et l'alignement suivent une disposition de type LP64 (4 entiers, 8 pointeurs, 4 Ă©numĂ©rations ; les structures et les unions utilisent la taille et l'alignement de leur enregistrement). La taille des Ă©lĂ©ments d'un tableau est multipliĂ©e par son nombre ; les tableaux incomplets n'ont pas de taille.
La tension en C entre les tableaux et les pointeurs est omniprésente. Les tableaux perdent en performance dans de nombreux contextes d'expression, mais les types tableaux restent prioritaires dans les déclarateurs et pour l'optimisation arithmétique des pointeurs. Ne maßtriser qu'un seul de ces aspects, c'est risquer de livrer un compilateur qui, une fois lié, échoue toujours à l'opération *(a+1).
Analyse sémantique et résolution des symboles
Sema fait plus que « vérifier les types ».
Il gÚre des objets Scope imbriqués (global, function, block), une carte typedef, des types d'enregistrements et une table publique enumConstants :
// 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;
}
}
Le premier passage insÚre les fonctions, variables et typedefs de niveau supérieur, et enregistre les cas d'énumération. Le deuxiÚme passage effectue une vérification de type du corps. L'enregistrement d'une énumération écrit à la fois la table de correspondance plate et une constante 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,
));
}
}
Les Ă©numĂ©rations de type typedef et les Ă©numĂ©rations anonymes imbriquĂ©es dans des structures alimentent Ă©galement ce chemin d'enregistrement. La recherche d'identificateurs parcourt les portĂ©es, puis utilise enumConstants par dĂ©faut afin que VALUE dans int result = VALUE; soit rĂ©solu mĂȘme si la seule dĂ©claration Ă©tait un cas d'Ă©numĂ©ration.
Scope est une carte liée à son parent, regroupant les valeurs de Symbol (Variable, Function, Typedef, EnumConstant). Le corps d'une fonction ouvre une portée de fonction globale ; les instructions composées s'imbriquent davantage. Les paramÚtres sont insérés avant l'analyse du corps.
Le pilote transmet ensuite explicitement enumConstants à codegen :
$codegen = new Codegen($enumConstants);
Cette conception fait du repliement constant des noms d'énumération dans les initialiseurs statiques un contrat partagé entre sema et le backend, et non une deuxiÚme table de consultation indépendante inventée lors de l'émission.
Voici à quoi ressemble un chemin de parcours concret pour un initialiseur d'énumération global :
enum { VALUE = 42 };
int result = VALUE;
L'analyseur syntaxique construit un EnumDecl / type Ă©numĂ©rĂ© avec le cas VALUE = 42, puis un VarDecl dont l'initialiseur est un Identifier("VALUE"). Lors de la premiĂšre passe de Sema, VALUE â 42 est enregistrĂ© dans enumConstants et dans la portĂ©e globale. Lors de la seconde passe, l'initialiseur est vĂ©rifiĂ© comme Ă©tant un entier. Au moment de gĂ©nĂ©rer le result global, Codegen rĂ©sout l'identificateur via la table des constantes de l'Ă©numĂ©ration et gĂ©nĂšre une directive numĂ©rique (gĂ©nĂ©ralement .long 42) plutĂŽt qu'une relocalisation symbolique. Le fixture 015-enum-global permet d'Ă©viter que le chemin d'exĂ©cution ne rĂ©gresse silencieusement vers une erreur d'« identificateur non dĂ©claré » ou vers une erreur d'« émission de zĂ©ro ».
Le typage des expressions couvre Ă©galement les alĂ©as courants dont SQLite dĂ©pend encore : la dĂ©gradation des tableaux dans la plupart des contextes dâexpression, les exigences relatives aux lvalues ââpour lâaffectation et lâopĂ©rateur unaire &, ainsi que les particularitĂ©s liĂ©es Ă la position dans lâinstruction, telles que (void)sizeof(x); (correctif 013-sizeof-stmt). sizeof est analysĂ© comme un opĂ©rateur unaire pouvant accepter soit une expression, soit un nom de type entre parenthĂšses ; une erreur de classification Ă la position de lâinstruction constitue un bogue de lâanalyseur syntaxique qui nâapparaĂźt jamais dans return 42.
Expressions constantes et initialisation statique
SQLite repose sur des donnĂ©es statiques : tables, chaĂźnes de caractĂšres, pointeurs vers des tableaux, initialiseurs composĂ©s. LâĂ©mission globale nâest donc pas une simple note de bas de page.
La fonction Codegen::emitInitializer() sélectionne les directives d'assemblage à partir du type cible. Les entiers étroits et les caractÚres deviennent .byte ; les entiers larges deviennent .long / .quad ; les tableaux parcourent chaque élément et .zero supprime les caractÚres restants ; les chaßnes littérales converties en tableaux de caractÚres copient les octets et complÚtent le code. Les variables globales de tableaux de caractÚres sont définies par la fixture 010-char-array-global, qui exige .byte et interdit .quad 84 dans l'assembleur.
Une compilation en direct de ce composant produit actuellement :
.section __DATA,__data
.globl _table
.p2align 3
_table:
.byte 84
.byte 92
.byte 134
.byte 0
Ce résultat dépend de la correspondance entre instanceof ArrayCType et la classe réelle dans App\Compiler\Common. En PHP, un nom ArrayCType simple dans App\Compiler\CodeGen est résolu en App\Compiler\CodeGen\ArrayCType sauf s'il est importé. Un élément manquant :
use App\Compiler\Common\ArrayCType;
La branche du tableau est devenue inaccessible. Les variables globales ont Ă©tĂ© dĂ©placĂ©es et ont gĂ©nĂ©rĂ© des fichiers .quad au lieu de .byte. Le programme a pu ĂȘtre assemblĂ© et liĂ©. La disposition Ă l'exĂ©cution restait incorrecte. Il s'agit d'un bogue interlangage au sens strict : la rĂ©solution de noms PHP a corrompu la disposition des donnĂ©es statiques C.
Les initialiseurs globaux de pointeurs (&arr[i]), les dĂ©finitions provisoires et les littĂ©raux composĂ©s globaux sont des problĂšmes frĂšres dans le mĂȘme voisinage : le backend doit comprendre non seulement « Ă©mettre un entier », mais aussi « Ă©mettre une adresse relocalisable », « fusionner les dĂ©finitions provisoires » et « matĂ©rialiser un agrĂ©gat constant dans la section de donnĂ©es ».
De l'AST typé à l'ARM64
Le backend est src/Compiler/CodeGen/Codegen.php, avec les conventions d'enregistrement dans 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,
];
La fonction RegAlloc alloue des registres temporaires. Les arguments sont reçus sous forme de x0 Ă x7, conformĂ©ment Ă l'utilisation de type AAPCS64 dans ce backend. Les valeurs de retour sont de x0 (et de x1 pour les petits agrĂ©gats plus importants). L'assembleur est accumulĂ© sous forme de chaĂźnes de caractĂšres et affichĂ© comme du texte â une autre pratique native de PHP.
Une fonction concrĂšte
Donné:
int add(int a, int b)
{
return a + b;
}
ce compilateur génÚre (représentatif) :
.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
Lire cela au regard de la mise en Ćuvre :
- Le prologue sauvegarde le pointeur de cadre et le registre de lien, définit
x29 - Les variables locales/paramĂštres reçoivent des dĂ©calages nĂ©gatifs par rapport Ă
x29; les arguments sont stockés à partir dex0/x1 - Les chargements utilisent
ldrswpour les valeurs signées 32 bits dans des registres 64 bits addproduit la somme dans un registre temporaire, puis la déplace versx0.- L'épilogue restaure
spĂ partir dex29et renvoie
La taille de la trame est calculée en fonction des besoins locaux et insérée dans un espace réservé sub sp, sp, #... émis au début de emitFunction(). L'alignement sur 16 octets est important sur AArch64 ; le backend arrondit/agrÚge les emplacements temporaires en conséquence.
Cadres de pile et variables locales
L'allocation locale est volontairement simple et explicite. emitFunction() réinitialise l'état de chaque fonction, émet une soustraction de trame de substitution, puis détermine l'espace mémoire nécessaire aux paramÚtres, aux variables locales et aux variables temporaires :
$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;
La fonction ensureLocalSpace() incrémente un localOffset continu, alignant chaque réservation sur 8 octets. La fonction allocLocal() stocke le décalage négatif choisi par rapport à x29 dans 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;
}
Les paramĂštres arrivant dans x0âx7 sont immĂ©diatement rĂ©partis dans les emplacements correspondants (str x0, [x29, #-8], etc.). Cela confĂšre Ă chaque variable locale nommĂ©e une adresse stable pour &var, pour la rĂ©cupĂ©ration des adresses de champs et pour le rechargement aprĂšs les appels. Les structures de grande taille (> 16 octets) rĂ©servent Ă©galement de l'espace pour le pointeur invisible x8 et le stockent en amont.
Lorsque le corps de la fonction est entiĂšrement Ă©mis, la ligne d'espace rĂ©servĂ© est raccordĂ©e Ă la vĂ©ritable sub sp, sp, #<rounded frame> La taille de la trame est arrondie Ă l'entier supĂ©rieur afin que spreste alignĂ© sur 16 octets â une exigence stricte sur AArch64. L'Ă©pilogue restaurespĂ partir dex29, retire la paire sauvegardĂ©e et ret`s.
Les variables temporaires composĂ©es, les emplacements de retour d'agrĂ©gats de petite taille et la mise en attente des arguments d'appel se disputent les mĂȘmes ressources mĂ©moire. C'est pourquoi les bogues d'ABI ressemblent souvent Ă une corruption de pile « alĂ©atoire » jusqu'Ă ce qu'on remarque qu'une variable temporaire a Ă©tĂ© allouĂ©e avec une taille incorrecte ou qu'un appel imbriquĂ© a Ă©crasĂ© un emplacement contenant encore un argument non Ă©valuĂ©.
Arithmétique des pointeurs
Les opérations binaires + et - utilisant des opérandes de type pointeur adaptent la partie entiÚre à la taille de la valeur pointée. Les tableaux sont considérés comme des opérandes de type pointeur à cet effet.
// 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
}
Avant la correction de Loop 32, *(a+1) pouvait renvoyer add ..., #1 en octets. Le fixture 035-array-ptr-arith nécessite désormais une mise à l'échelle des éléments.
Appel de fonctions sur ARM64
emitCallExpr() Ă©value les arguments, les rĂ©partit dans la pile dans un ordre disciplinĂ©, charge x0âx7 (et les arguments de la pile au-delĂ ), puis soit bl _name pour les appels directs, soit blr xn pour les appels indirects.
Les pointeurs de fonction sont des types de premiÚre classe dans le systÚme de types (PointerCType vers FunctionCType) et dans la génération de code. Un exemple minimal :
int call(int (*fn)(int), int value)
{
return fn(value);
}
Ămet une sĂ©quence d'appel indirect qui charge l'adresse de l'appelĂ© et utilise blr (et non bl) vers un symbole fixe. Les appels directs de main vers call utilisent toujours bl _call, tandis que la rĂ©cupĂ©ration de l'adresse de id utilise adrp/add avec les relocalisations @PAGE / @PAGEOFF.
Appels variadiques
Les appels variadiques sont plus précis. L'évaluation des arguments peut avoir des effets de bord ; la durée de vie des registres et de la pile interagit mal avec l'émission naïve de gauche à droite, ce qui fausse les résultats précédents. Le fixture 016-variadic-spill est la forme réduite :
printf("%d%d%d", next(), next(), next());
return counter == 3 ? 42 : 1;
La sortie standard attendue est 123 avec le code de sortie 42. Le chemin d'exécution du backend, de type printf, évalue les arguments en variables temporaires sur la pile avant d'assembler la variable d'origine finale, évitant ainsi la perte d'incréments due à la réutilisation des registres. Générer du code ARM64 syntaxiquement valide est bien plus simple que d'implémenter une convention d'appel qui résiste aux arguments à effets de bord.
Faibles rendements agrégés
Apple ARM64 renvoie des agrégats d'au maximum 16 octets dans x0 (et x1 si supérieur à 8). emitSmallAggregateReturn() matérialise la valeur dans un emplacement de pile, puis charge 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]');
}
La branche CallExpr existe car le transfert de return a(); à l'intérieur d'une autre fonction renvoyant une structure doit préserver l'intégralité du résultat du registre de l'appel imbriqué. Ne stocker qu'une représentation 32 bits de x0 entraßne la perte d'un champ. Ceci correspond à la boucle 48 / fixture 054-struct-return-chain.
Dans l'assembleur gĂ©nĂ©rĂ© pour b, vous pouvez voir le modĂšle : bl _a, puis str x0, [...], puis ldr x0, [sp] avant de retourner â le petit agrĂ©gat est traitĂ© comme une valeur de largeur de registre, et non comme un seul w0 restant d'une mentalitĂ© scalaire.
Structures, unions et littéraux composés
La disposition des structures et des unions est calculée lors de l'analyse de la définition d'enregistrement, puis stockée dans un RecordType consulté ultérieurement par StructCType / UnionCType. Pour les structures, chaque champ est aligné sur son propre alignement, les décalages s'accumulent et la taille totale est arrondie à l'alignement maximal de l'enregistrement.
// 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);
Les unions prennent la taille maximale des membres (arrondie à l'alignement maximal) et placent chaque champ à l'offset 0. Les champs de bits existent également dans le chemin de structure du parseur ; ils suivent les décalages de bits au sein des unités de stockage. Codegen utilise les décalages de champ lors de l'émission de l'accÚs aux membres (ajout d'une constante à une adresse de base) et lors de la mise en place des initialiseurs de structure globaux (insertion d'un remplissage .zero entre les positions de champ désignées).
Ces informations de mise en page rendent possibles les arguments agrégés, les retours et les littéraux composés : sans décalages fiables, « champ b d'une structure de 8 octets » relÚve de la conjecture.
Les littéraux composés sont abaissés en matérialisant un élément temporaire :
// 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;
}
Passer (struct S){20, 22} comme argument signifie donc : construire la variable temporaire, puis passer lâagrĂ©gat de la mĂ©moire dans les registres dâarguments ou la pile en fonction de sa taille â et non « évaluer la liste dâinitialisation comme sâil ne sâagissait que du premier champ ». Le fixture 034-compound-lit-arg existe parce que cette erreur a renvoyĂ© 20 au lieu de 42.
Les initialiseurs désignés imbriqués ({.iy = 2, .ix = 1}, {.a[1] = 40}) exigent que le normaliseur d'initialiseurs parcoure les chemins de désignation dans les structures et tableaux imbriqués, y compris à travers les membres anonymes lorsqu'ils sont présents. Le fixture 044-nested-designated-init est la sonde réduite.
La limite de la chaĂźne d'outils native
Toolchain est volontairement minimaliste :
// 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);
}
La fonction run() utilise proc_open, capture la sortie standard et la sortie d'erreur, et renvoie un code de sortie. Cela suffit. PHP écrit les fichiers .s ; as écrit les fichiers .o ; le pilote Clang effectue les liens. Clang ne compile toujours pas le code source C sur le chemin d'acceptation.
SQLite en tant qu'oracle d'intégration
La compilation de return 42 prouve que le canal est connecté. La compilation de SQLite prouve que les sous-systÚmes interagissent.
Le script (fixtures/compiler/sqlite_smoke_test.c) ouvre :memory:, crĂ©e une table, y insĂšre 'darkwood', la sĂ©lectionne et affiche le texte. Cela sollicite une unitĂ© de traduction importante, des en-tĂȘtes riches en macros, des donnĂ©es globales, des pointeurs de fonction, des structures, des Ă©numĂ©rations et une quantitĂ© suffisante de conventions d'appel pour que des erreurs de dĂ©bordement de tampon provoquent des Ă©checs Ă l'exĂ©cution, mĂȘme si l'assembleur semble correct.
SQLite n'est pas une suite de standards. C'est un oracle d'intĂ©gration : de nombreuses fonctionnalitĂ©s doivent ĂȘtre quasiment parfaites simultanĂ©ment, sinon le test de validation Ă©choue.
Les bugs de l'implémentation exposés
Le principal atout technique de ce projet rĂ©side dans l'ensemble des bogues qui n'ont Ă©tĂ© mis en Ă©vidence que par la confrontation d'abstractions. Chaque cas prĂ©sentĂ© ci-dessous suit le mĂȘme schĂ©ma : petit programme C, comportement attendu, comportement anormal, cause racine en PHP, correctif, fixture.
Ătude de cas : ArrayCType et donnĂ©es statiques corrompues
Programme (010-char-array-global)Â :
static const unsigned char table[4] = {84, 92, 134, 0};
int main(void) { return table[0]; }
RĂ©sultat attendu : sortie 84; lâassembly contient .byte, et non .quad 84.
Mode d'échec : l'absence de use App\Compiler\Common\ArrayCType; dans Codegen.php a entraßné le test de la mauvaise classe par instanceof ArrayCType. La branche d'initialisation du tableau ne s'est jamais exécutée. Des directives plus larges ont mal agencé le tableau.
Correction : importer la classe rĂ©elle ; Ă©mettre un .byte par Ă©lĂ©ment pour les tableaux de caractĂšres. ConservĂ© explicitement dans les notes dâĂ©tat du projet et verrouillĂ© par les assertions dâassemblage des fixtures.
Leçon : une erreur dâespace de noms PHP est un bug dâABI/de structure C. « Liens » ne signifie pas « fonctionne ».
Ătude de cas : mise Ă lâĂ©chelle arithmĂ©tique des pointeurs de tableaux
Programme (035-array-ptr-arith)Â :
int a[3] = {10, 20, 12};
int *p = a + 1;
return *(a + 2) == 12 && *p == 20 ? 42 : 0;
Attendu : sortie 42 (également comparé avec clang).
Mode d'échec : a + 1 avancé d'octets car les tableaux n'ont pas été traités comme des pointeurs pour la mise à l'échelle.
Correction : dans emitBinaryExpr, traiter ArrayCType comme un pointeur pour +/-, mettre à l'échelle par sizeof(element) via lsl/mul.
Leçon : la dĂ©gradation des tableaux et la mise Ă lâĂ©chelle des pointes ne sont pas le mĂȘme bug, mais ils sont Ă©troitement liĂ©s.
Ătude de cas : dĂ©clarations de pointeurs vers des tableaux
Programme (051-pointeur-vers-tableau)Â :
int (*p)[2] = a;
return (*p)[0] + (*p)[1];
Résultat attendu : correspond à clang.
Mode d'échec : l'application du suffixe déclaratif qui a réparé (*p)[N] pourrait casser *p[N] si elle est appliquée de maniÚre trop large.
Correction : Fermez le déclarateur (*name) groupé avant les suffixes de tableau ; utilisez applyArraySuffix() pour que [N] pointe vers un pointeur de type pointeur. Conservez le chemin non groupé pour la construction de tableaux de pointeurs de maniÚre classique.
Leçon : Les dĂ©clarateurs C posent un problĂšme dâanalyse syntaxique avec des consĂ©quences sĂ©mantiques ; les fixtures doivent couvrir les deux interprĂ©tations.
Ătude de cas : arguments littĂ©raux composĂ©s
Programme (034-compound-lit-arg)Â :
return f((struct S){20, 22});
Attendu : 42.
Mode d'échec : l'évaluation d'une liste littérale composée / init comme valeur n'a renvoyé que le premier champ (20).
Correction : matĂ©rialiser le littĂ©ral composĂ© dans un cadre temporaire dans emitAddr, puis passer la structure â€16 octets de la mĂ©moire dans les registres d'arguments.
Leçon : les agrĂ©gats Ă©phĂ©mĂšres ont besoin dâadresses, et non dâun « premier scalaire ».
Ătude de cas : initialiseurs dĂ©signĂ©s imbriquĂ©s
Programme (044-nested-designated-init)Â :
struct Outer o = {.z = 3, .i.y = 2, .i.x = 1};
struct Arr s = {.a[1] = 40, .a[0] = 2};
Résultat attendu : les valeurs des champs et des emplacements de tableau correspondent à la lecture de clang.
Mode d'échec : les désignateurs à un seul niveau fonctionnaient ; les chemins imbriqués et les désignateurs de tableau à l'intérieur des structures ne fonctionnaient pas.
Correction : analyser les listes de dĂ©signateurs en chemins ; normaliser ces chemins lors de lâapplication des initialiseurs, y compris Ă travers les membres imbriquĂ©s et anonymes.
Leçon : Lâinitialisation simplifiĂ©e du C99 nâest pas optionnelle si vous prĂ©tendez utiliser du « vrai C » plutĂŽt quâun code de type SQLite.
Ătude de cas : dĂ©versements variadiques
Programme (016-variadic-spill)Â :
printf("%d%d%d", next(), next(), next());
Résultat attendu : stdout 123, sortie 42.
Mode d'échec : l'ordre d'évaluation des arguments / la durée de vie des registres ont été détruits par les résultats précédents de next() avant l'appel.
Correction : évaluer dans des temporaires de pile, puis assembler l'appel variadique à la maison (le chemin en forme de printf dans emitCallExpr).
Leçon : les arguments Ă effets secondaires sont un test ABI, et non un test dâanalyse syntaxique.
Ătude de cas : chaĂźnes de retour de structures
Programme (054-struct-return-chain)Â :
struct S b(void) { return a(); }
// main: struct S s = b(); return s.a + s.b;
Résultat attendu : sortie 3 (oracle clang).
Mode d'échec : le retour agrégé imbriqué n'a préservé qu'une partie de x0 (sortie symptomatique 2).
Correction : dans emitSmallAggregateReturn(), lorsque l'expression renvoyée est une CallExpr, stockez x0 complet et, si nécessaire, x1 avant de recharger pour le retour de la fonction actuelle.
Leçon : gĂ©nĂ©rer ret aprĂšs bl nâest pas la mĂȘme chose que de mettre en Ćuvre le transfert de retour agrĂ©gĂ©.
Des échecs aux tests de régression
La méthodologie est plus importante que n'importe quel KEEP individuel :
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
Les métadonnées des fixtures peuvent vérifier les codes de sortie, la sortie standard, les sous-chaßnes d'assemblage et compareWithClang. Les assertions d'assemblage permettent de détecter les erreurs de mise en page qu'un code de sortie chanceux pourrait manquer. Le fait que la suite se termine à 46/46 n'est pas un simple effet de mode ; il s'agit d'un souvenir des interactions qui ont déjà affecté l'amalgame.
Sondes différentielles
Pour les petits programmes, la compilation et l'exĂ©cution du mĂȘme code source avec clang fournissent un oracle exĂ©cutable : le code de sortie et la sortie standard. Des commandes telles que app:compiler-compare-clang et les flux de test existent pour faciliter ce processus. Clang est ici un outil de comparaison diffĂ©rentielle Ă l'exĂ©cution, et non le compilateur de sqlite3.c dans le cadre de la validation.
Cette distinction est importante. Les tests diffĂ©rentiels rĂ©pondent Ă la question : « Notre ABI correspond-elle au comportement observable sur cette sonde ? » Les tests dâacceptation rĂ©pondent Ă la question : « Notre compilateur compile-t-il SQLite ? »
OĂč Darkwood Flow trouve sa place
La compilation comprend deux pipelines diffĂ©rents. Les confondre produit soit une abstraction inutile, soit une rĂ©implĂ©mentation privĂ©e d'un Ă©lĂ©ment qui devrait ĂȘtre partagĂ©.
Le pipeline algorithmique est étroitement couplé :
tokens â AST â semantic model â assembly
Ces étapes échangent des structures spécifiques au compilateur : listes de jetons, arbres de déclarations, graphes de types C et allocateurs de registres. Encapsuler chaque étape d'analyse syntaxique dans un framework d'orchestration n'apporterait aucune clarté. L'algorithme de masquage d'ensembles ne s'améliore pas du simple fait de son passage par une file d'attente de tùches.
Le processus opérationnel est différent :
compile â assemble â link â execute â validate
Chaque Ă©tape possĂšde des limites d'artefacts clairement dĂ©finies, peut Ă©chouer indĂ©pendamment, peut enregistrer le temps d'exĂ©cution sur un Ă©tat partagĂ© et peut ĂȘtre remplacĂ©e sans réécrire l'analyseur lexical. C'est lĂ que Darkwood Flow trouve toute sa place.
Ce projet dépend du paquet Composer darkwood/flow v8.1.5. Le noyau du compilateur situé dans src/Compiler/ n'importe pas Flow. Les workflows externes situés dans src/Flow/, en revanche, les importent.
SqliteValidationFlow génÚre des tùches planifiées. Son API est volontairement réduite : une FlowFactory construit un flux à partir d'un générateur de tùches ; chaque tùche reçoit un Ip (paquet d'informations) contenant un SqliteValidationState ; await() exécute la séquence jusqu'à son terme.
// 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();
La fonction timed() est un wrapper PHP classique autour d'une JobInterface : elle ignore le travail restant si failure est déjà défini, lance la tùche et stocke le temps écoulé en millisecondes dans $state->timings. Les tùches concrÚtes (CompileSqlite, AssembleSqlite, CompileHarness, AssembleHarness, LinkSmokeExecutable, RunSmokeTest) font appel à Compiler et Toolchain. Flow ne reconnaßt pas la notion de Token.
Du point de vue de l'utilisateur, il n'y a qu'une seule commande :
php -d memory_limit=4G bin/console app:compiler-sqlite
En interne, cela se dĂ©compose en Ă©tapes indĂ©pendantes et significatives partageant SqliteValidationState : PHP produit sqlite3.s, as produit sqlite3.o, PHP gĂ©nĂšre l'assembly Harness, as produit l'objet Harness, l'Ă©diteur de liens produit un exĂ©cutable, le processus affiche darkwood, la validation confirme la rĂ©ussite. Les flux apparentĂ©s (CompileFlow pour la compilation/l'assemblage/l'Ă©dition de liens d'un seul fichier, ProbeRunFlow pour les tests de base de Clang) appliquent le mĂȘme principe Ă plus petite Ă©chelle.
Une correction architecturale a Ă©tĂ© apportĂ©e : une version miniature locale de lâenvironnement dâexĂ©cution Flow, situĂ©e dans src/Flow/Runtime/, a Ă©tĂ© supprimĂ©e. LâexpĂ©rimentation du compilateur doit dĂ©sormais utiliser le package Composer officiel au lieu de maintenir une copie privĂ©e. La sĂ©paration finale est la suivante :
sqlite-compiler-php
âââ src/Compiler/ plain PHP algorithms
âââ src/Flow/ darkwood/flow orchestration
Il est tout aussi important de comprendre ce que Flow n'a pas Ă©tĂ© conçu pour faire. Il ne remplace pas les nĆuds AST, n'implĂ©mente pas l'expansion des macros, n'alloue pas de registres, n'Ă©met pas de code ARM64, ne remplace pas Symfony ni la chaĂźne d'outils du systĂšme d'exploitation. Il ne s'agit pas d'une couche de performance et il n'a jamais Ă©tĂ© conçu pour accĂ©lĂ©rer la compilation de SQLite. Sa valeur rĂ©side dans la structure et la visibilitĂ© qu'il offre autour d'une compilation native multi-Ă©tapes â une application concrĂšte de Darkwood Flow Ă un contexte trĂšs diffĂ©rent d'une requĂȘte web.
En bref : le compilateur montre ce que PHP natif peut faire ; Darkwood Flow transforme ces capacités en une expérience reproductible.
La boucle d'itération
Le développement s'est déroulé comme un processus de découverte au-delà de la simple implémentation, et non comme une réécriture unique et inspirée.
Le modÚle est le développement de compilateurs différentiels :
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
L'argument pertinent n'est pas qu'un modĂšle puisse gĂ©nĂ©rer des milliers de lignes de PHP, mais qu'un agent puisse interagir de maniĂšre rĂ©pĂ©tĂ©e avec du code de compilateur rĂ©el, du code as/clang rĂ©el, des fixtures rĂ©elles et une cible d'acceptation importante â pour ensuite ne conserver que les modifications validĂ©es.
La discipline apparaissait encore comme un refus d'inventer du travail en retard, et comme un arrĂȘt lorsque de nouveaux changements n'Ă©taient plus justifiĂ©s par des enquĂȘtes infructueuses.
La boucle
AprÚs des boucles d'ingénierie axées sur la correction à travers la boucle (chaßne de retour de structure / 054-struct-return-chain), une boucle de réveil a continué à reprendre un agent toutes les cinq minutes avec des instructions pour rechercher la prochaine amélioration la plus importante.
Trois termes doivent rester distincts :
| Terme | Signification |
|---|---|
| Boucle de rĂ©veil | Reprise pĂ©riodique : analyse, validation, dĂ©cision quant Ă lâopportunitĂ© de modifications. |
| Boucle d'ingénierie | Une entrée KEEP numérotée. |
| Modification conservée | Code validé et enregistré : CONSERVER. |
Les ticks ne correspondent pas à des modifications de code. La boucle de réveil a été interrompue. Entre ces deux événements, une longue période de ticks a révélé que la suite était toujours au vert (46/46, SQLite affichant toujours darkwood), sans qu'aucune anomalie significative ne justifie un autre KEEP.
Si la boucle de veille avait continué à inventer des refactorisations sans qu'une sonde ne soit défaillante, cela n'aurait été que du bruit déguisé en progrÚs. La conclusion intéressante est d'ordre méthodologique :
Finalement, la boucle de rétroaction a convergé vers des décisions de maintien du statu quo.
Ce que le compilateur prend en charge
Suffisamment de C pour compiler et exécuter le chemin de fumée d'amalgame SQLite 3.46.0 sur macOS ARM64, plus un réseau de régression de 46 fixtures couvrant les bords du préprocesseur, les variables globales, les littéraux composés, les énumérations, les modÚles de déversement variadiques, les déclarateurs, les initialiseurs désignés et les cas d'ABI agrégés.
Ce qu'il ne cherche pas dĂ©libĂ©rĂ©ment Ă ĂȘtre
- Ne constitue pas une allégation de conformité totale aux normes C99/C11/C17
- Aucun optimiseur CIR/IR n'est intégré par conception.
- Ne remplace pas Clang/GCC en production
- Un récit de compatibilité avec le SDK Darwin incomplet
- L'ABI à virgule flottante et d'autres écarts plus importants restent en dehors de la barre « SQLite smoke + current fixtures ».
- L'IA ne supprime pas la nécessité de tests d'acceptation exécutables
Il est préférable d'utiliser la formulation exacte : assez du sous-ensemble C utilisé par SQLite et les tests de régression actuels.
Ce que j'ai appris
PHP peut hĂ©berger une implĂ©mentation de compilateur non triviale utilisant des objets ordinaires, des tableaux, la distribution instanceof, de l'assembleur construit Ă partir de chaĂźnes de caractĂšres et proc_open pour les outils natifs. Un chemin direct lexer â prĂ©processeur â analyseur syntaxique â sema â ARM64 permet de gĂ©rer suffisamment de code C pour compiler l'amalgame de SQLite et exĂ©cuter un vĂ©ritable systĂšme de simulation de fumĂ©e.
Les bogues les plus difficiles n'étaient pas dus à un simple point-virgule oublié dans l'émetteur. Il s'agissait d'interactions complexes : espaces de noms PHP vs structure C, équivalence entre tableaux et pointeurs, arbres de déclarateurs, transfert d'ABI agrégé, ordre d'évaluation variadique. SQLite impose ces interactions. Les fixtures les mémorisent.
Darkwood Flow a mérité sa place en tant que pipeline externe. Il n'avait pas sa place au sein du flux de jetons.
La boucle de rĂ©troaction tardive a permis de tirer une leçon plus discrĂšte. L'absence de changement persistant aprĂšs l'analyse prouve que la boucle fonctionne et que l'expĂ©rience a atteint un point d'arrĂȘt qu'il convient de respecter.
OĂč je l'emmĂšnerais ensuite
Les prochaines Ă©tapes importantes sont plus ambitieuses : une interface utilisateur logicielle (ABI) plus Ă©tendue pour les nombres Ă virgule flottante, une couverture plus large du langage C au-delĂ du sous-ensemble de type SQLite, et peut-ĂȘtre des stratĂ©gies de streaming ou dâĂ©mission plus lĂ©gĂšres. Aucune de ces solutions ne peut ĂȘtre considĂ©rĂ©e comme un simple « maintien de cinq minutes », et aucune ne devrait ĂȘtre relancĂ©e sans une procĂ©dure de validation rigoureuse.
Pour Darkwood, la leçon Ă retenir est d'ordre architectural : conserver les algorithmes du compilateur en PHP pur ; conserver les pipelines opĂ©rationnels dans Flow ; insister sur le fait que « fonctionne » signifie des fixtures plus un test d'acceptation rigoureux â et non pas simplement quelque chose d'assemblĂ©.
Sources
- Code source : https://github.com/matyo91/sqlite-compiler-php
- Slides : https://github.com/matyo91/slidewire