TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

23 KiB

PeachPie Compiler Review: Roslyn Integration, Cross-Language Interop, and Type System

2026-06-11 · Based on source review of /home/swoole/workspace/cpp/peachpie


1. Architecture Overview

PeachPie is a PHP-to-.NET compiler based on Roslyn (Microsoft's .NET compiler platform), with roughly 710 C# source files and 272k lines of code.

Compilation Pipeline

PHP source
  → PhpSyntaxTree (Roslyn SyntaxTree for PHP — Syntax/)
  → SemanticModel + Symbols (Roslyn symbol system — Semantics/, Symbols/)
  → BoundControlFlowGraph (CFG with typed IR — Semantics/Graph/, FlowAnalysis/)
  → CIL Bytecode (EMIT — CodeGen/, Emitter/)
  → .NET Assembly (.dll / .exe)
Stage Component Responsibility
Syntax parsing Syntax/ (PhpSyntaxTree, NodesFactory) PHP → Roslyn SyntaxTree
Semantic binding Semantics/ (SemanticsBinder, BoundExpression) Name resolution, method binding, type inference
Symbol system Symbols/ (SourceTypeSymbol, PEMethodSymbol...) Roslyn symbol table for types/methods/properties
Dataflow analysis FlowAnalysis/ (FlowState, TypeRefMask, ExpressionAnalysis) Type inference, unreachable code detection, conditional narrowing
CFG optimization FlowAnalysis/Passes/ (TransformationRewriter) CFG rewriting, constantization, dead code elimination
Code generation CodeGen/ (CodeGenerator, GhostMethodBuilder) IR → CIL instructions
Assembly output Emitter/ (PEModuleBuilder) CIL → PE file (.dll/.exe)

Core Files

File Lines Purpose
CodeGen/Graph/BoundExpression.cs 5,742 Core IR node definitions and CIL emission
CodeGen/CodeGenerator.Emit.cs 4,396 CIL instruction generation
FlowAnalysis/ExpressionAnalysis.cs 2,911 Expression-level type analysis
Semantics/BoundExpression.cs 2,721 Semantic binding expressions
Runtime/Operators.cs 2,573 Runtime implementation of PHP operators
Runtime/PhpString.cs 2,047 PHP string value type
CodeGen/VariableReference.cs 1,863 Variable references and address analysis
Symbols/Source/SourceTypeSymbol.cs 1,795 Source type symbols
Runtime/Conversions.cs 1,603 Type conversion runtime

Comparison with AOT Compiler

Dimension PeachPie AOT Compiler
Compilation target PHP → CIL → .NET Assembly PHP → C++ → binary
Compiler framework Roslyn (C# compiler-as-a-library) Self-built PHP AST → C++ string
IR form BoundControlFlowGraph (Roslyn pattern) PHP-Parser AST nodes
Type inference FlowState + TypeRefMask bitset SSA + manual type annotations
Symbol system Roslyn Symbol hierarchy (complete) Simplified ClassDef/FunctionDef
Output .NET PE file (cross-platform) Native binary (Linux/Mac/Windows)
Runtime Peachpie.Runtime (PhpValue, PhpArray, PhpString) phpx (C++ RAII wrapper around Zend API)
Cross-language interop First-class citizen — PHP ⇄ C# bidirectional calls FFI only (swoole_cc/cpp extension loading)
Parallel compilation Parallel.ForEach function-level parallelism Serial execution
Code size ~272k lines of C# (including runtime) ~15k lines of PHP
MSBuild integration Complete SDK (dotnet build) None

2. Reusable Innovative Designs

2.1 Roslyn-Based Compiler Architecture

Files: all of Peachpie.CodeAnalysis/

PeachPie's most central design decision is to be fully built on the Microsoft Roslyn compiler platform. This means:

  • Reuse Roslyn's symbol system: TypeSymbol, MethodSymbol, NamedTypeSymbol, and other standard Roslyn types
  • Reuse Roslyn's metadata emission: PEModuleBuilder, PEAssemblyBuilder directly generate PE files
  • Reuse Roslyn's diagnostics system: DiagnosticBag, standardized Error/Warning mechanism
  • Native MSBuild integration: PHP projects are standard .NET projects (.csproj style), compilable with dotnet build

AOT adoption priority: P2

Currently AOT built all compiler infrastructure from scratch. PeachPie's approach is a useful reference, but not suitable for direct porting (AOT's goal is to generate C++ rather than CIL). What can be adopted:

  • Decompose the compiler into standard stages — Syntax → Semantic → IR → CodeGen — with clear interfaces per stage
  • Separate Preprocessor (symbol collection) into an independent Analyzer stage, similar to Roslyn's Compilation concept
  • Define a unified Diagnostic type instead of scattered fatalError() / SyntaxError calls

2.2 TypeRefMask: 64-bit Type Bitset

File: FlowAnalysis/TypeRef/TypeRefMask.cs

PeachPie uses ulong (64-bit) as the type mask:

public struct TypeRefMask {
    ulong _mask;
    // bits 0-61:  type index (up to 62 different types)
    // bit 62:     IncludesSubclasses (type may include subclasses)
    // bit 63:     IsRef (value is a reference/alias)
}

Characteristics:

  • O(1) type comparison: (mask & type_bit) != 0 detects a type
  • Union types: mask1 | mask2 = contains both types
  • Type narrowing: mask & ~excluded_type_bit = excludes a type
  • IsRef marker: tracks whether a value is assigned by reference, affecting alias analysis
  • IncludesSubclasses: distinguishes exactly Class vs Class or subclass

Each TypeRefContext maintains a type registry mapping concrete .NET types (such as System.Int64, Pchp.Core.PhpString) to bit indices.

AOT adoption priority: P0

This is consistent with HHVM's trep idea — using a bitset to represent types. AOT's discrete constants like TYPE_INT, TYPE_STRING can be replaced with a bitset:

class TypeMask {
    const BINT     = 1 << 0;
    const BFLOAT   = 1 << 1;
    const BSTRING  = 1 << 2;
    const BBOOL    = 1 << 3;
    const BARRAY   = 1 << 4;
    const BOBJECT  = 1 << 5;
    // Extension markers
    const BEMPTY      = 1 << 60;  // empty value marker
    const BSUBCLASS   = 1 << 61;  // allow subclasses
    const BREFERENCE  = 1 << 62;  // reference marker
}

Advantage: int|string = BINT|BSTRING, type narrowing = & ~excluded_bits.


2.3 FlowState + Worklist Dataflow Analysis

Files: FlowAnalysis/FlowState.cs, FlowAnalysis/Worklist.cs

PeachPie uses the classic dataflow worklist algorithm for type inference:

class FlowState {
    TypeRefMask[] _varsType;     // type mask for each variable
    ulong _initializedMask;      // whether variables are initialized
    HashSet<NoteData> _notes;    // additional info (such as function return points)
}

Merge operation: when two CFG paths converge, the FlowState(state1, state2) constructor computes:

  • The union of type masks (all possible types)
  • The union of initialization masks (initialized in either branch counts as initialized)
  • The intersection of notes (only info present on both paths is retained)

The Worklist processes blocks in topological order, re-enqueueing when state changes.

AOT adoption priority: P1

Currently AOT's SSA analysis does some type inference, but lacks:

  • Structured FlowState: a unified variable type-state representation
  • Standard merge operation: type merging at join points
  • Worklist iteration: an iterative analysis framework reaching a fixed point

2.4 ConditionBranch-Aware Type Narrowing

Files: FlowAnalysis/ConditionBranch.cs, FlowAnalysis/AnalysisFacts.cs

PeachPie's type analysis is aware of the conditional branch of the current context:

enum ConditionBranch {
    AnyResult = 0,   // ordinary evaluation
    ToTrue = +1,     // branch where the expression result is true
    ToFalse = -1,    // branch where the expression result is false
}

In conditional expressions, the analyzer carries the branch direction to propagate type info:

// if ($x instanceof MyClass) { ... }
// In the ToTrue branch:
//   $x's type is narrowed, excluding types that cannot be MyClass
// In the ToFalse branch:
//   $x's type is narrowed, excluding MyClass

// if (is_int($x)) { ... }
// In the ToTrue branch:
//   $x's type is narrowed to int

AnalysisFacts.HandleSpecialFunctionCall() registers type-check functions like is_int, is_string, is_array, is_callable, function_exists, class_exists, automatically narrowing variable types in branches.

AOT adoption priority: P1

Currently SsaTypeOptimizer does some instanceof narrowing, but:

  • Does not support narrowing via built-in type-check functions like is_int() / is_string()
  • Does not support constant folding of existence checks like class_exists() / function_exists()
  • Can directly adopt AnalysisFacts's "known type-check function registry" pattern

2.5 PhpValue Tagged Union Design

File: Peachpie.Runtime/PhpValue.cs

PeachPie's runtime value type uses an ingenious C# tagged union:

[StructLayout(LayoutKind.Sequential)]
public readonly partial struct PhpValue {
    readonly PhpTypeCode _type;   // 1-byte type tag

    // Explicit-layout union: two fields occupy the same memory
    [StructLayout(LayoutKind.Explicit)]
    struct ValueField {
        [FieldOffset(0)] public bool @bool;
        [FieldOffset(0)] public long @long;
        [FieldOffset(0)] public double @double;
    }

    [StructLayout(LayoutKind.Explicit)]
    struct ObjectField {
        [FieldOffset(0)] public object @object;
        [FieldOffset(0)] public string @string;
        [FieldOffset(0)] public PhpString.Blob blob;
        [FieldOffset(0)] public PhpArray array;
        [FieldOffset(0)] public PhpAlias alias;
    }

    readonly ValueField _value;    // value-type storage
    readonly ObjectField _obj;     // reference-type storage
}

Memory layout: PhpValue = PhpTypeCode (1 byte) + padding + ValueField (8 bytes) + ObjectField (8 bytes, pointer) ≈ 24 bytes.

Advantages of this design:

  • readonly struct — no GC overhead, allocatable on the stack
  • Explicit union — value types and reference types share space, compact
  • PhpAlias mechanism — abstracts PHP references (copy-on-write) via PhpAlias rather than directly copying values
  • MutableString — distinguishes immutable string from writable MutableString (for string concatenation optimization)

AOT adoption priority: P3

Currently AOT uses Variant (based on Zend zval) as the dynamic type. What can be adopted:

  • PhpString's MutableString separation (string builder pattern)
  • PhpAlias's reference semantics abstraction
  • But replacing Variant entirely is a huge effort, so low priority

2.6 GhostMethodBuilder: PHP Method ⇄ C# Method Adaptation

File: CodeGen/GhostMethodBuilder.cs

PeachPie's most distinctive feature is automatically generating ghost stub methods so PHP methods can be called directly from C#:

// Generate C#-callable wrappers for PHP methods:
// - Handle parameter type conversion (PhpValue → CLR type)
// - Handle return type conversion (CLR type → PhpValue)
// - Build and pass PhpContext
// - Support explicit interface override
static MethodSymbol CreateGhostOverload(
    MethodSymbol original, NamedTypeSymbol containingtype,
    PEModuleBuilder module, DiagnosticBag diagnostic,
    TypeSymbol ghostreturn, ImmutableArray<ParameterSymbol> ghostparams,
    bool phphidden = false, MethodSymbol explicitOverride = null)

Ghost methods enable:

  • Type-safe interfaces when C# calls PHP methods
  • PHP implementing C# interfaces (IMethod, INotifyPropertyChanged, etc.)
  • PHP classes usable as .NET generic parameters

AOT adoption priority: P4

Currently AOT does not support PHP calling C++ (or vice versa). If a bidirectional interop layer is needed in the future, the ghost stub pattern is worth referencing.


2.7 DelayedTransformations: Parallel-Safe Deferred Transformations

File: FlowAnalysis/Passes/DelayedTransformations.cs

During the parallel analysis phase, certain transformations (such as marking unreachable functions, promoting conditional functions to unconditional ones) cannot directly modify shared state. PeachPie uses the deferred transformation pattern:

class DelayedTransformations {
    ConcurrentBag<SourceRoutineSymbol> UnreachableRoutines;
    ConcurrentBag<SourceTypeSymbol> UnreachableTypes;
    ConcurrentBag<SourceFunctionSymbol> FunctionsMarkedAsUnconditional;
    // Collected thread-safely during parallel analysis
    // Applied serially via Apply() after analysis completes
}

Analysis threads only place the objects to be transformed into the ConcurrentBag, and after analysis completes a single thread calls Apply().

AOT adoption priority: P2

AOT is currently serial and doesn't need this. But if parallel compilation is introduced in the future (see KPHP review 2.3), deferred transformation is the foundational pattern for thread safety.


2.8 Native MSBuild Integration (Peachpie.NET.Sdk)

File: Peachpie.NET.Sdk/

PeachPie is not just a compiler — it is a complete .NET SDK:

dotnet new classlibrary -o MyPhpLib  # create a PHP class library project
dotnet build                           # compile PHP → .NET DLL
dotnet run                             # run the compiled program
dotnet publish                         # publish as a self-contained app

Implemented via MSBuild targets/props:

  • build/peachpie.targets — defines the compilation task
  • Peachpie.NET.Sdk.nuspec — NuGet package definition
  • BuildTask.cs — MSBuild compilation task

This means PHP projects can seamlessly use the .NET ecosystem: NuGet package references, project references, conditional compilation, multi-targeting, etc.

AOT adoption priority: P2

AOT currently uses php bin/tpc.php <project> on the command line. What can be adopted:

  • Create a Composer plugin or CLI phar package for the AOT compiler
  • Define a JSON Schema for project.yml (similar to .csproj)
  • Support a unified entry point like composer build or php-aot build

2.9 Lazily Evaluable AnalysisFacts

File: FlowAnalysis/AnalysisFacts.cs

PeachPie performs constant evaluation of a large number of PHP runtime functions at compile time:

Function Evaluation strategy
function_exists(X) Check if symbol X exists in the PE assembly → fold to true
class_exists(X) Check if type X exists in the PE assembly → fold to true/false
method_exists(X, M) Check if method M exists in type X → fold to true/false
defined(CONST) Check if the constant exists → fold to true/false
is_callable(F) Check if F is unconditionally declared → fold to true
dirname(__FILE__) Compile-time path computation → __DIR__
basename(__FILE__) Compile-time filename extraction → string constant

These evaluations leverage PeachPie's "PE assembly" concept — already-compiled .NET assemblies contain complete type/method/constant metadata that can be queried at compile time.

AOT adoption priority: P1

Currently FuncCallOptimizer only does the most basic constant folding (strlen("abc")3). It can be extended to compile-time evaluation of reflective functions like function_exists, class_exists, defined — provided a complete symbol table is built in Preprocessor.


2.10 Conditional Declaration Detection and Unreachable Code Elimination

Files: FlowAnalysis/Passes/DelayedTransformations.cs, FlowAnalysis/Passes/TransformationRewriter.cs

PeachPie can detect conditional declarations (if (condition) { function foo() {} }) and optimize them:

  • If analysis proves the condition is always true → the function is marked as unconditionally declared
  • If analysis proves the condition is always false → the function/class is marked as Unreachable and not compiled

This enables writing C-like conditional compilation patterns in PHP:

if (PHP_VERSION_ID >= 80000) {
    function newFeature() { ... }  // not compiled on lower versions
}

AOT adoption priority: P2

Currently AOT compiles every scanned function regardless of reachability. For projects with multi-version PHP compatibility code, conditional declaration elimination can reduce compilation artifact size.


3. Toolchain Analysis

3.1 Test Infrastructure

PeachPie has 529 test files (PHP files) distributed across functional directories:

Directory Content
tests/arrays/ Array operation tests (including a lazy_copy subdirectory)
tests/classes/ Class/object tests
tests/functions/ Function call tests
tests/generators/ Generator/yield tests
tests/strings/ String operation tests
tests/operators/ Operator tests
tests/transformations/ Compiler transformation/optimization tests
tests/constants/ Constant tests
tests/constructs/ Language construct tests
tests/traits/ Trait tests
tests/reflection/ Reflection tests
tests/spl/ SPL tests
tests/bcmath/ tests/hash/ tests/pcre/ Extension tests
tests/pdo/ tests/ftp/ tests/openssl/ Database/network extensions
tests/gd/ tests/xml/ tests/zip/ Graphics/XML/ZIP extensions
tests/web/ tests/scripting/ Web/scripting integration tests

Test execution: compiled assemblies are executed through the .NET test framework (xUnit/NUnit).

AOT adoption:

  • The tests/transformations/ directory specifically tests compiler optimizations — AOT can build similar optimization-correctness tests
  • tests/arrays/lazy_copy/ specifically tests copy-on-write behavior — AOT can also build dedicated COW tests

3.2 Deep Visual Studio Integration

PeachPie provides a complete IDE experience:

  • Visual Studio Extension — project management, IntelliSense, debugging, performance analysis
  • VS Code / Rider support — via OmniSharp / LSP
  • NuGet package management — PHP libraries can be published and referenced as NuGet packages

AOT adoption:

  • Can provide a VSCode extension (Task integration + project templates)
  • AOT's current project.yml can use a JSON Schema to provide IDE auto-completion

3.3 Command-Line Toolchain

# PeachPie's CLI experience
dotnet peach build              # compile a PHP project
dotnet peach run                # run the compiled program
dotnet peach publish            # self-contained publishing
dotnet peach add <package>      # add a dependency

AOT adoption:

# A similar CLI could be designed
php-aot build                    # compile a project
php-aot run                      # compile and run
php-aot new <name>               # create a new project

4. Type System and Interop Analysis

4.1 PeachPie Type Mapping

PHP type PeachPie runtime type .NET CLR type
null PhpTypeCode.Null null (any reference type)
bool PhpTypeCode.Boolean bool
int PhpTypeCode.Long long
float PhpTypeCode.Double double
string PhpTypeCode.String / MutableString string / PhpString
array PhpTypeCode.PhpArray PhpArray
object PhpTypeCode.Object object (concrete class)
reference PhpTypeCode.Alias PhpAlias

4.2 PHP ⇄ C# Interop

PeachPie's bidirectional interop is its most prominent differentiator:

C# calling PHP:

// Compiled PHP classes become .NET classes; C# can directly new and call them
var phpObj = new MyPhpClass(ctx);
phpObj.someMethod(arg1, arg2);

PHP calling C#:

// .NET types can be used directly in PHP
$list = new \System\Collections\Generic\List<int>;
$list->Add(42);

Interop implementation relies on:

  • GhostMethodBuilder generating adapter methods
  • ConversionsExtensions handling automatic PhpValue ↔ CLR type conversion
  • DynamicOperationFactory handling dynamic method call forwarding

4.3 Syntax Differences from the AOT Compiler

Feature PeachPie AOT Compiler
Base PHP version 8.0+ (target) 8.2+
Type annotations Optional (gradually enriched) Optional (phpstan annotations)
Namespaces Standard PHP Standard PHP
Generics None None
C# interop Complete (first-class citizen) FFI extension only
.NET ecosystem Fully compatible Not relevant
MSBuild integration Complete None
Reflection Partial support Not supported
yield/generator Supported Supported

5. Summary and Priority Recommendations

Priority Technique Difficulty Benefit Notes
P0 TypeRefMask bitset type system Medium Very high Union types, type narrowing, non-null markers — foundation for all optimization passes
P1 ConditionBranch type narrowing Low High Automatic narrowing via is_int()/is_string() check functions, not covered by current SSA
P1 AnalysisFacts compile-time evaluation Medium High Compile-time folding of function_exists/class_exists/defined
P1 FlowState worklist analysis framework Medium High Structured variable type state, standard merge operation
P2 Roslyn-style compiler layering High Medium Clear Syntax→Semantic→IR→CodeGen layering; current Preprocessor/CompilerBase responsibilities are mixed
P2 Conditional declaration detection Low Medium Eliminate unreachable functions/classes, reduce compilation artifact size
P2 MSBuild integration / CLI unification Low Medium Standardized project configuration, CI-friendly
P3 PhpValue tagged union High Medium Compact memory layout, but replacing Variant/zval is a large effort
P3 DelayedTransformations Low Low Only meaningful under parallel compilation
P4 GhostMethodBuilder interop Very high Low Requires .NET or a similar FFI runtime
P4 Deep IDE integration High Low Limited ROI for a VSCode extension

Key Takeaways

  1. PeachPie's biggest advantage is .NET ecosystem integration — this is a natural advantage from its architectural choice rather than any single technical innovation. AOT can adopt its "compiler SDK + standardized CLI" thinking without porting specific techniques.

  2. TypeRefMask bitset is the most directly portable design — representing types with a 64-bit bitset while supporting union types, subclass markers, and reference markers. This is a common feature of HHBBC's trep and KPHP's type analysis, indicating that the bitset type lattice is the best practice for PHP compilers.

  3. The ConditionBranch pattern is a low-cost, high-benefit enhancement — carrying "expected result" info in conditional branch analysis enables type-check functions to automatically narrow variable types. AOT's SSA analysis can adopt it immediately.

  4. GhostMethodBuilder reveals a general interop pattern — generating adapter/thunk methods to bridge the calling conventions of two languages. Although AOT targets C++ rather than .NET, if cross-language call needs arise (such as PHP calling C extensions), this pattern can be reused.

  5. PeachPie's roughly 530 tests are notably fewer than KPHP (75+ directories) and HHVM (14,675), indicating relatively lower maturity. But its way of categorizing tests by functional module and optimization type is worth referencing.