Elvelt

Frontends

PHP Frontend

PHP Frontend

The PHP frontend is a BraceFrontendBase subclass delivered as a single 1,376-line header. It implements modern PHP 8.3 features through a pipeline of nine static source-rewrite transforms that run before the brace parser sees the source. Each transform is comment-aware and string-aware.

File Lines Purpose
frontends/php/PHPFrontend.h 1,376 Frontend class with all method bodies inline
frontends/php/PhpStdLib.h 86 findStdLibMapping for echo, print, abs, strlen, json_encode, …

Language registration

// PHPFrontend.h:12-13
std::string languageName() const override { return "php"; }
std::vector<std::string> fileExtensions() const override { return {".php"}; }

Only .php is recognised.


Preprocessing pipeline

PHPFrontend::parse (line 15) applies the rewrites in this order:

Order Transform Lines Purpose
1 normalizePHPSource 29-201 Strip <?php, ?>, heredocs, comments
2 convertMatchExpressions 432-639 Lower match to if/else-if chain
3 stripNamedArguments 641-759 f(name: expr)f(expr) (positional)
4 transformTryCatchBindings 766-827 catch (E $e)catch (E) (drop binding)
5 stripPhpFunctionModifiers 829-870 Drop final, abstract from function headers
6 stripInterfaceMethodDeclarations 872-911 Interface method bodies become stubs
7 transformEnumAccess 913-1126 case Foo; becomes case Foo_value;
8 transformArrowFunctions 1128-1180 No-op preservation (passthrough)
9 wrapPrintArguments 272-355 Wrap echo x, y arguments in parens

After all nine transforms, the source is fed to BraceFrontendBase::parse.


// PHPFrontend.h:1197-1203
return {
    {"echo", false, false},
    {"print", true, true},
    {"print", false, false}
};

echo is the keyword-form pattern (no parens, comma-separated args); print accepts both forms.


Features (file:line)

Variables: $x = expr

tryParseVarDecl (line 1213) recognises $x = expr with optional modifiers public, private, protected, static, readonly. The $ prefix is stripped. Modifier metadata is added:

public readonly int $id = 0;

VariableDeclNode(name="id", initializer="0", metadata={"php_modifier"="public", "php_readonly"="true"})

Functions: function name($args) { … }

tryParseFunctionDeclHeader (line 1249). Constructors (function __construct(...)) are renamed to constructor to match the JavaScript / Python convention.

Methods: public function name($args): ReturnType { … }

Detected via stripPhpFunctionModifiers (line 829-870) which strips final, abstract, and visibility modifiers from the function header.

Match expressions (PHP 8.0)

convertMatchExpressions (line 432) translates match ($x) { 1 => 'one', default => 'other' } to a sequence of if ($x === 1) { __match_tmp_0 = 'one'; } else if ($x === 'other') { __match_tmp_0 = 'other'; } followed by substituting the temporary for the original match. Strict equality (===) matches PHP match semantics — only the first matching arm fires.

Named arguments (PHP 8.0)

stripNamedArguments (line 641) rewrites f(name: expr) to f(expr), preserving the position. The named form is dropped because the C++ backend has no named-argument support:

array_fill(start_index: 0, num: 10, value: 0);

array_fill(0, 10, 0);

Try/catch with bindings (PHP 8.0)

transformTryCatchBindings (line 766) strips the variable binding from catch (E $e) so it becomes catch (E). The exception variable is exposed via $_exception (a runtime-resolved variable):

try { ... } catch (RuntimeException $e) { echo $e->getMessage(); }

try { ... } catch (RuntimeException) { echo $_exception->getMessage(); }

Enums (PHP 8.1)

transformEnumAccess (line 913) builds a map of case Name;case Name_value; and rewrites every Name::Foo access to Name_value. This allows enums to compile without a true enum runtime.

enum Status { case Pending; case Approved; case Rejected; }
$status = Status::Approved;

class Status { const Pending_value = 0; const Approved_value = 1; const Rejected_value = 2; }
$status = Status_Approved;

Readonly classes (PHP 8.2)

A readonly class modifier is detected but currently stripped (no backend enforcement).

Interfaces

stripInterfaceMethodDeclarations (line 872) replaces interface method bodies with stub ; so the parser doesn't try to lower abstract methods.

Traits

tryParseLanguageSpecificStatement (line 1305) detects trait Name { … } and emits ClassDeclNode(metadata["php_kind"]="trait").

Abstract / final classes

tryParseLanguageSpecificStatement (line 1330) handles:

abstract class Base { … }
final class Concrete extends Base { … }

abstract sets metadata["abstract"]="true", final sets metadata["final"]="true".

Foreach

tryParseLanguageSpecificStatement (line 1345) handles:

foreach ($items as $value) { … }
foreach ($items as $key => $value) { … }

Lowers to ForEachNode(target=value, iterable=items) (key is dropped).

Namespaces

namespace App\Models;

Line 1287. Stored in metadata["php_namespace"].

Use statements

use App\Models\User;
use App\Models\User as U;

Line 1296. Both forms become ImportNode(module=…).

Fibers (PHP 8.1)

Fiber::suspend(), Fiber::resume(), $fiber->start(), $fiber->isStarted() — recognised as ordinary method calls but not specially lowered. See limitations.md.

Arrow functions: fn($x) => $x + 1

PHP 7.4 arrow functions are passed through (no body). They're parsed as ordinary expressions by BraceFrontendBase.


Standard library mapping (PhpStdLib.h)

The complete table at frontends/php/PhpStdLib.h:38-75:

PHP MPL module.function
echo x Console.Write(x)
print x Console.Write(x)
var_dump(x) Console.WriteLine(x)
print_r(x) Console.WriteLine(x)
abs(x) Math.Abs(x)
floor(x) Math.Floor(x)
ceil(x) Math.Ceil(x)
round(x, n) Math.Round(x, n)
sqrt(x) Math.Sqrt(x)
pow(x, y) Math.Pow(x, y)
max(...) Math.Max(...)
min(...) Math.Min(...)
log(x) Math.Log(x)
sin(x) Math.Sin(x)
cos(x) Math.Cos(x)
rand() / rand(min, max) Math.Random()
strlen(s) String.Length(s)
strtolower(s) String.ToLower(s)
strtoupper(s) String.ToUpper(s)
trim(s) String.Trim(s)
explode(sep, s) String.Split(s, sep)
str_starts_with(s, prefix) String.StartsWith(s, prefix)
str_ends_with(s, suffix) String.EndsWith(s, suffix)
exit(code) Process.Exit(code)
die(code) Process.Exit(code)
getenv(name) Environment.GetVariable(name)
json_encode(v) Json.Stringify(v)
json_decode(s, assoc) Json.Parse(s)
file_get_contents(path) IO.ReadFileText(path)
file_put_contents(path, data) IO.WriteFileText(path, data)
file_exists(path) IO.FileExists(path)

IR examples

Example 1 — match expression

$name = match ($code) {
    200 => 'OK',
    404 => 'Not Found',
    default => 'Error',
};

After convertMatchExpressions:

{
    $__match_tmp_0 = null;
    if ($code === 200) {
        $__match_tmp_0 = 'OK';
    } else if ($code === 404) {
        $__match_tmp_0 = 'Not Found';
    } else {
        $__match_tmp_0 = 'Error';
    }
    $__match_tmp_0
}

Lowers to:

BlockNode
  VariableDeclNode(name="__match_tmp_0", initializer=null)
  IfNode(condition=BinaryOpNode("===", code, 200),
         thenBranch=AssignmentNode(target=__match_tmp_0, value="OK"))
    IfNode(condition=BinaryOpNode("===", code, 404),
           thenBranch=AssignmentNode(…))
      BlockNode
        AssignmentNode(target=__match_tmp_0, value="Error")

Example 2 — enum

enum Color {
    case Red;
    case Green;
    case Blue;
}

$fav = Color::Blue;

After transformEnumAccess:

class Color {
    const Red_value = 0;
    const Green_value = 1;
    const Blue_value = 2;
}

$fav = Color_Blue;

Example 3 — readonly class

readonly class Point {
    public function __construct(
        public float $x,
        public float $y,
    ) {}
}

Lowered to ClassDeclNode(name="Point", metadata={"php_readonly"="true", "php_promoted_properties"="true,float,x;float,y"}) with the constructor's promoted properties tracked in metadata.


What is not supported (PHP-specific gaps)

  • Real enum runtime: lowered to class with const values.
  • Fibers: Fiber::suspend() / Fiber::resume() are ordinary method calls — no coroutine semantics.
  • Generators with yield from: lowered as ordinary statements.
  • Attributes (#[Attr]): single-line forms are skipped via isIgnoredLine (line 369) — they're type-erased.
  • First-class callable syntax (strlen(...)): emitted as a runtime call, not a closure.
  • match expression's UnhandledMatchError: if no arm matches and there's no default, the frontend does NOT throw (PHP would throw UnhandledMatchError).
  • readonly enforcement: modifier is metadata only.
  • Property hooks (PHP 8.4 RFC): not yet supported.
  • Asymmetric visibility (public private set): not supported.
  • Type juggling (string ↔ int coercion in ==): the lowered C++ backend uses strict === always.