# phpefc

Prototype compiler/toolchain for **phpef**: PHP Executable Format / PHP ELF.

`phpefc` currently lowers PHP source through project-owned IRs into a Vala bootstrap backend. The long-term target is PHP semantics lowered through explicit runtimes/IR into native C-ABI-compatible ELF libraries and binaries, not “PHP to pretty Vala”.

## Name and direction

- `phpef` = **PHP Executable Format** / **PHP ELF**.
- `phpefc` = **PHP Executable Format Compiler**.
- Vala is allowed as a bootstrap/backend implementation detail, but must not define the project identity or final ABI.

## Current pipeline

```text
.php source
  -> .phpast   nikic/php-parser AST bundle
  -> .phpir    project-owned PHP IR bundle
  -> .valair   project-owned Vala-shaped IR bundle
  -> .vala     generated Vala bootstrap source
  -> binary    via valac
```

Current CLI binaries after build:

```text
build/src/cli/phpefc-php2ast
build/src/cli/phpefc-ast2php
build/src/cli/phpefc-phpast2phpir
build/src/cli/phpefc-phpir2valair
build/src/cli/phpefc-valair2vala
```

## Runtime boundary

`phpefc` must use the phpef runtimes, not `valaphp-runtime-*`:

- `phpef-runtime-core`: zval-like PHP value model and core operations.
- `phpef-runtime-embed`: direct pwphpbytecode/libphp-backed embed bridge for PHP/Zend reference behavior.

The tests require installed pkg-config metadata:

```meson
dependency('phpef-runtime-core')
dependency('phpef-runtime-embed')
```

The runtime packages install headers, VAPI, `.deps`, and `.pc` files. There is no `find_library()` fallback in `phpefc` anymore.

## Build

```bash
meson setup build -Dphpslot=8.3 -Dabiver=3
CCACHE_DISABLE=1 meson compile -C build
meson test -C build --print-errorlogs
```

## Current Meson test

- `compiler e2e hallowelt PHP input`: feeds `tests/hallowelt.php` through AST roundtrip and the current bootstrap compile pipeline.

Runtime-library tests intentionally do not live in `phpefc/tests`: `phpef-runtime-core` owns core zval helper tests, and `phpef-runtime-embed` owns embed/reference tests. The current generated Vala uses `PhpefRuntimeCore.Zval` variables instead of native `var` declarations for PHP values.

The current hallowelt expected output is:

```text
Hallo Welt1a!
```

## Current supported PHP subset

- `echo ...;`
- `print ...;` as echo-like output
- simple assignment statements like `$a = "Welt";`
- string literals
- integer literals
- simple variables
- string concatenation with `.` mapped through the current Vala bootstrap backend
- standalone comments via nikic `Stmt\Nop`

## Next direction

Move generated output away from direct Vala `print`/string `+` and toward explicit `phpef-runtime-core` zval calls plus `phpef-runtime-embed` reference/fallback behavior.

See `docs/architecture.md`, `docs/php-aot-compiler-pitfalls.md`, and `TODO.md` for details.

### Unified `phpef-runtime-*` API

PHPEFC resolves `phpef-runtime-embed`, `phpef-runtime-native-*`, and future domain runtimes through the same VAPI-based `phpfunc_*` convention. Zval wrappers use `phpfunc_<phpname>(Zval...)`; typed fast paths use `phpfunc_<phpname>_typed_<return>_argN_<type>...`; literal-dependent specializations encode the literal in the suffix, e.g. `phpfunc_microtime_typed_double_arg1_bool_true`. Optimizers may select a typed wrapper only when the adjacent VAPI comment marks it with `PHPEF typed_fast_path ...`; no sidecar JSON or native-specific naming scheme is allowed. See `docs/architecture.md` for the full contract.

## Current generated Vala runtime style

Generated Vala starts with `using PhpefRuntimeCore;` on line 1 and opens `main` on line 2. Source-mapped PHP statements continue at their original line numbers where possible; for example the current `hallowelt.php` statements on lines 3-5 still emit on generated lines 3-5.

PHP values are emitted as `Zval` variables and concat lowers to `left.concat(right)`, not native Vala `var` or inline `to_php_string() + to_php_string()` expressions.

PHP `echo`/`print` lower to `phpfunc_echo(expr)` / `phpfunc_print(expr)` from `PhpefRuntimeEmbed`, not directly to Vala `stdout.printf`. They are PHP language constructs, so the embed runtime implements them through Zend output helpers rather than `call_user_function()`.

## PHP call wrapper convention

Generated dynamic zval calls should go through `phpfunc_*` runtime wrappers. Core language/runtime constructs that do not require Zend live in `phpef-runtime-core`; embed/Zend-backed PHP functions live in embed runtime packages.

Example:

```vala
phpfunc_echo(value)
```

### Typed runtime fast-path naming

Vala does not expose the classic overload model needed for PHPEF's public runtime API. Runtime functions therefore use two layers of names:

- the canonical dynamic/Zval-compatible base wrapper keeps the original PHP-derived name, e.g. `phpfunc_echo`;
- typed fast-path variants use stable, signature-based names:

  ```text
  phpfunc_<phpname>_typed_<returntype>_arg1_<type>_arg2_<type>...
  ```

Do not use `_s1`, `_s2`, `_s3` sequence suffixes for public typed fast paths. Those names are order-dependent and not stable enough for compiled artifacts or ABI-visible VAPI users.

Example:

```text
phpfunc_echo                         dynamic/Zval-compatible path
phpfunc_echo_typed_void_arg1_string  typed echo(string) fast path
```

Resolver policy:

1. Resolve the original PHP function/construct name to the base wrapper (`echo` -> `phpfunc_echo`).
2. Group additional candidates with the same base and a `phpfunc_<name>_typed_...` signature suffix.
3. Keep parsed argument and return types with the candidates so a later resolver/optimizer pass can pick the best match.

`ValaCodeGenerator` stays deliberately dumb: typed fast-path metadata and selection belong in `Php2ValaFuncResolver`, ValaIR, and optimizer passes, not in final text emission.

## Pre-optimizer readability goal

Before optimizer passes reshape the program, generated Vala should stay semantically close to the original PHP source.

Goals for the unoptimized / correctness-first output:

- preserve source line numbers where possible;
- keep recognizable PHP variable names and statement shape;
- use PHP-semantic runtime wrappers such as `phpfunc_echo(...)` rather than arbitrary Vala/C helper names;
- keep generated zval operations readable, e.g. `Zval.from_string(...).concat(...)`;
- avoid clever lowering that hides the original PHP structure before an explicit optimizer stage runs.

Optimizers may later rewrite this into faster/native forms, but the first emitted form should remain useful for debugging and human recognition.

## Runtime function resolver

`ValaCodeGenerator` does not hard-code mappings such as `print -> phpfunc_print`. `PHPIR -> ValaIR` asks `Php2ValaFuncResolver`, implemented in `src/valair/php2vala_func_resolver.php`.

The resolver reads runtime VAPI metadata from `phpef-runtime-core.vapi` and, when available, `phpef-runtime-embed.vapi`; it validates that the selected runtime wrapper exists. If no wrapper exists, code generation fails instead of emitting an unresolved or guessed call.

VAPI discovery order:

1. `PHPEF_RUNTIME_CORE_VAPI`
2. `PHPEF_RUNTIME_VAPIDIR/phpef-runtime-core.vapi`
3. installed VAPI locations such as `/usr/share/vala/vapi/phpef-runtime-core.vapi`

Current language-element mapping lives in the resolver, not in the generator. For example current ValaIR `print` resolves to runtime wrapper `phpfunc_print` only if that wrapper is present in the runtime VAPI.

## PHPIR to ValaIR function resolution

Runtime function resolution happens during `PHPIR -> ValaIR`, not in final Vala source emission.

`ValaIrBuilder` asks `Php2ValaFuncResolver` (`src/valair/php2vala_func_resolver.php`) to resolve PHP language/function calls into concrete runtime wrappers. `ValaCodeGenerator` then only emits the already-resolved ValaIR callee.

Current example:

```text
PHPIR Echo -> ValaIR CallStatement(callee = "phpfunc_echo") -> Vala source phpfunc_echo(...)
```

The resolver validates the wrapper against runtime VAPI metadata. Missing wrappers fail during `phpefc-phpir2valair`, before Vala code is generated.

## Current ValaIR runtime-lowered shape

The current unoptimized compiler path lowers PHP values to zval-backed generic ValaIR before source emission:

- PHP variables become `ValaIrVariableDeclaration(typeName = "Zval")`.
- PHP literals become generic static calls such as `Zval.from_string(...)` / `Zval.from_int(...)`.
- PHP concat becomes a generic method call `.concat(...)`.
- PHP echo is resolved to `phpfunc_echo(...)` during `PHPIR -> ValaIR`.

`ValaCodeGenerator` emits this ValaIR generically and should not hard-code zval/runtime semantics.

`Php2ValaFuncResolver` uses the `phpfunc_*` naming convention directly. It derives wrappers such as `phpfunc_echo` / `phpfunc_print` from the PHP/ValaIR call name and validates the derived wrapper plus arity against the runtime VAPI.

For typed fast paths, the resolver keeps the base wrapper as the fallback and exposes a candidate list for stable signature-based variants such as `phpfunc_echo_typed_void_arg1_string`. Candidate metadata includes parsed VAPI argument and return types so later ValaIR optimizer stages can select the best implementation without putting pass-specific logic into `ValaCodeGenerator`.

<!-- phpefc-status-2026-06-10 -->
## Status snapshot 2026-06-10

The compiler is currently on the runtime-first / zval-first path:

- PHPIR -> ValaIR owns zval-backed lowering and runtime wrapper resolution.
- `ValaCodeGenerator` should remain mostly generic Vala syntax emission and should not hard-code Zend/runtime policy.
- Generated Vala should stay readable and close to PHP before optimizer passes rewrite it.
- PHP function calls lower through `PhpIrFuncCall` and then through `phpfunc_*` wrappers validated from runtime VAPI metadata.
- `echo` and `print` remain separate PHP language constructs, not ordinary PHP functions.

Current ordinary function-call coverage works when the call is used as an expression, for example:

```php
$a = trim("1");
$c = substr($a, 0, 7);
```

A standalone call statement such as:

```php
exec("hostname");
```

is still intentionally not lowered by PHPIR yet. nikic/php-parser represents it as `Stmt\Expression` containing `Expr\FuncCall`; `exprToIr()` knows `FuncCall`, but `stmtToIr()` still lacks a generic expression-statement / ignored-result call-statement node. The current diagnostic is therefore:

```text
Unsupported PHP statement for phpast-ir-v0: PhpParser\Node\Stmt\Expression
```

## Debug source-line pipeline

`phpefc` now treats PHP source locations as explicit compiler metadata, not as an accidental equality between PHP and generated Vala line numbers.

The debug-oriented pipeline is:

```text
.php
  -> .phpast   includes phpSourceFile metadata
  -> .phpir    preserves phpSourceFile
  -> .valair   preserves phpSourceFile
  -> .vala     generated source plus PHPEFC-C-SOURCE-LINES JSON comment
  -> .c        via valac --debug -C, producing Vala #line markers
  -> mapped .c via phpefc-c-add-source-lines
  -> ELF       via Meson executable(), with optional .debug_phpef section
```

Important rule: never rely on PHP and Vala line numbers being identical. Future optimizer passes may move, merge, delete, or synthesize Vala lines. The authoritative mapping is explicit metadata of the form:

```text
actual generated Vala file:line -> original PHP file:line
```

`phpefc-c-add-source-lines` rewrites Valac C `#line` markers from generated Vala locations to original PHP locations so GCC/DWARF and GDB can report PHP source lines natively. It also preserves the old Vala location as an inline comment on the rewritten `#line` directive, so no extra C source line is inserted, and can emit a `phpef-debug-bundle-v0` JSON bundle with `lineRewrites` entries:

```json
{
  "old": {"file": "hallowelt.vala", "line": 16, "language": "vala"},
  "new": {"file": "/path/hallowelt.php", "line": 16, "language": "php"}
}
```

Binaries may embed that bundle in a strip-debug-removable `.debug_phpef` ELF section. Runtime stacktrace tooling can later use it to enrich PHP frames with the generated Vala origin.
