Darkwood · PHP · C · ARM64 · SQLite
The question
The result
46/46 regression fixtures
What “compiled in PHP” means
Lexer
Preprocessor
Parser
AST
Sema
ARM64 Codegen
as
linker
native executable
Clang does not compile sqlite3.c.
Deliberately small
src/Compiler/Driver/Compiler.php
$tokens = $preprocessor->preprocess($fileId);
$parser = new Parser($tokens, $this->diagnostics);
$decls = $parser->parse();
$sema = new Sema($this->diagnostics);
$tast = $sema->analyze($decls);
$codegen = new Codegen($enumConstants);
$assembly = $codegen->generate($tast);
SOURCE → TOKENS → AST → TYPES → ASM
SourceLoc.php
final readonly class SourceLoc
{
public function __construct(
public int $fileId,
public int $offset,
) {}
}
Token.php
public function __construct(
public TokenKind $kind,
public string $spelling,
public SourceLoc $loc,
public array $hideSet = [],
) {}
Why does a token need a hideSet?
Macro expansion needs memory
#define A B
#define B A
MacroTable.php · MacroExpander
if ($token->hideSet !== []
&& in_array($token->spelling, $token->hideSet, true)) {
$output[] = $token;
continue;
}
$newHideSet = $this->unionHideSet($token->hideSet, [$macro->name]);
$expanded = $this->expandObjectLike(...);
for ($i = count($expanded) - 1; $i >= 0; --$i) {
$pendingReversed[] = $expanded[$i];
}
The preprocessor is a compiler subsystem
SQLite never reaches the parser unless this works.
Parsing C by hand
FuncDeclBinaryExprCallExprCompoundLiteralExprC declarators fight back
int *p[4];
array of pointers
int (*p)[4];
pointer to array
Parser::applyArraySuffix()
if ($type instanceof PointerCType) {
$to = $type->to;
$array = new ArrayCType($to, $count);
return new PointerCType($array);
}
int *p[4]Array
└── Pointer
└── int
int (*p)[4]Pointer
└── Array[4]
└── int
Ast.php
final class BinaryExpr extends Expr
{
public function __construct(
public BinaryOp $op,
public Expr $left,
public Expr $right,
SourceLoc $loc,
public ?CType $resolvedType = null,
) {}
}
Parser builds structure. Sema gives it meaning.
CType.php
CType
├── IntCType / CharCType / …
├── PointerCType
├── ArrayCType
├── FunctionCType
├── StructCType / UnionCType
└── EnumCType
$this instanceof EnumCType => true, // isInteger()
Array ≠ pointer — except when it is
int a[3];
int *p = a + 1;
Semantic analysis
$this->enumConstants[$case->name] = $case->value;
enum { VALUE = 42 };.long 42Global initialization is codegen too
static unsigned char table[4] = {
84, 92, 134, 0
};
_table:
.byte 84
.byte 92
.byte 134
.byte 0
This once became .quad.
A PHP namespace bug corrupted C memory
instanceof ArrayCType
App\Compiler\Common\ArrayCType
App\Compiler\CodeGen\ArrayCType
It assembled. It linked. It was wrong.
From AST to ARM64
int add(int a, int b)
{
return a + b;
}
_add:
stp x29, x30, [sp, #-16]!
mov x29, sp
sub sp, sp, #16
str x0, [x29, #-8]
str x1, [x29, #-16]
… ldrsw … add …
mov x0, x9
ldp x29, x30, [sp], #16
ret
Stack frame
high
──────────────
saved x29 / x30
──────────────
params · locals
temps · compounds
──────────────
SP
low
sub sp, sp, #0 ; FRAME_SIZE_PLACEHOLDER
Patched after emission · 16-byte aligned
Pointer arithmetic means scaling
int a[3];
a + 1
add x?, x?, #1
address + 1 × sizeof(int)
lsl …, #2
035-array-ptr-arith
Direct vs indirect calls
bl _function
blr xN
int call(int (*fn)(int), int value)
{
return fn(value);
}
The ABI is where “valid assembly” stops
Struct returns use registers too
struct S b(void) { return a(); }
preserve only part of x0/x1
str x0, [...]
str x1, [...]
054-struct-return-chain · Loop 48
SQLite is an integration oracle
Failure → fixture → fix
flowchart TB A[SQLite / clang mismatch] B[reduce] C[tiny C fixture] D[find subsystem] E[fix one behavior] F[all fixtures] G[SQLite] A --> B --> C --> D --> E --> F --> G G -.-> A
Bugs became permanent knowledge
Every fixture is a bug the compiler should only have once.
Two different pipelines
tokens
↓ AST
↓ Sema
↓ ARM64
compile
↓ assemble
↓ link
↓ run
↓ validate
Where should Flow live?
Where Darkwood Flow fits
plain PHP
darkwood/flow
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);
One command, many artifacts
php bin/console app:compiler-sqlite
flowchart TB C[sqlite3.c] --> S[sqlite3.s] S --> O[sqlite3.o] H[harness.c] --> HS[harness.s] HS --> HO[harness.o] O --> L[link] HO --> L L --> E[executable] E --> D[darkwood]
The engineering loop
AI becomes useful when it cannot escape the executable feedback loop.
The five-minute wake loop
Loop 48 = last retained engineering change
Eventually
The correct decision was to stop changing the compiler.
What this proves
What it does NOT prove
The Darkwood takeaway
It assembled. It linked. It queried SQLite.