# Architecture notes

## Responsibilities

### `src/helpers/PhpAstProcessor`

Thin adapter around `nikic/php-parser` only.

It owns:

- PHP source -> nikic AST
- AST -> serialized string inspection form
- serialized string -> AST
- AST/string -> PHP source pretty printing

It must not own:

- file I/O
- bundle packing/unpacking
- PHPIR/ValaIR conversion

### `src/helpers/PhpSerializedBundle`

Generic wrapper for packing exactly one embedded value.

Public API:

```php
PhpSerializedBundle::pack(mixed $value): string
PhpSerializedBundle::unpack(string $bundle): mixed
PhpSerializedBundle::prettySerializedString(string $serialized, int $indentSize = 2): string
```

Wrapper keys are ordered so human-readable views appear first:

- `.phpast` with nikic nodes:

  ```text
  pretty_nikic_dumper
  pretty
  embed_serialized
  ```

- `.phpir` with `PhpIr*` objects:

  ```text
  pretty_phpir_dumper
  pretty
  embed_serialized
  ```

- `.valair` with `ValaIr*` objects:

  ```text
  pretty_valair_dumper
  pretty
  embed_serialized
  ```

`pretty_*_dumper` values intentionally start and end with a newline.

`unpack()` returns the embedded original value from `embed_serialized`; readable keys are metadata only.

### `src/phpir`

Project-owned PHP AST IR. This is the first compiler-facing abstraction after nikic AST.

It is not a dump of nikic internals. It should contain stable, compiler-useful nodes.

Current nodes:

```text
PhpIrProgram
PhpIrComment
PhpIrEcho
PhpIrAssignment
PhpIrNop
PhpIrStringLiteral
PhpIrIntLiteral
PhpIrVariable
PhpIrConcat
PhpIrBuilder
```

Current lowering examples:

```text
Stmt\Echo_                  -> PhpIrEcho
Stmt\Expression(Print_)      -> PhpIrEcho
Stmt\Expression(Assign)      -> PhpIrAssignment
Scalar\String_               -> PhpIrStringLiteral
Scalar\LNumber               -> PhpIrIntLiteral
Expr\Variable                -> PhpIrVariable
Expr\BinaryOp\Concat         -> PhpIrConcat
Stmt\Nop                     -> PhpIrNop
```

### `src/valair`

Project-owned Vala IR. This is Vala-shaped, but still an object IR, not text.

Current nodes:

```text
ValaIrProgram
ValaIrComment
ValaIrCallStatement
ValaIrVariableDeclaration
ValaIrNop
ValaIrStringLiteral
ValaIrIntLiteral
ValaIrVariableReference
ValaIrBinaryExpression
ValaIrBuilder
```

Current lowering examples:

```text
PhpIrEcho          -> ValaIrCallStatement(callee = "print")
PhpIrAssignment    -> ValaIrVariableDeclaration
PhpIrVariable      -> ValaIrVariableReference
PhpIrConcat        -> ValaIrBinaryExpression(operator = "+")
```

### `src/valacodegen`

Text generation from ValaIR to `.vala`.

`ValaCodeGenerator` owns Vala syntax emission.

`ValaCodeLineEmitter` owns source-line preservation rules.

## Line mapping rule

Line numbers should remain correct through `.phpast`, `.phpir`, `.valair`, and `.vala` where practical.

For generated Vala:

- the `int main(string[] args) {` wrapper is line 1
- the next standalone generated line is line 2
- only a source item whose source line is 1 may be appended behind the wrapper opening brace on line 1
- source items for lines greater than 1 are emitted on their own generated lines
- blank lines are inserted until the requested source line is reached

Example with source line 1:

```php
<?php echo "Hallo Linie 1";
```

becomes:

```vala
int main(string[] args) { print("Hallo Linie 1");
    return 0;
}
```

Example with later source lines:

```text
1 <?php
2
3 $a="Welt";
4 echo "Hallo ".$a."!";
7 // comment after
```

becomes:

```text
1 int main(string[] args) {
2
3     var a = "Welt";
4     print((("Hallo " + a) + "!"));
5
6
7     // comment after
8     return 0;
9 }
```

## PWPHP CLI note

Do not use PHP CLI constants such as `STDERR` directly in generated/embedded PWPHP binaries.

Each CLI currently uses a local `cli_error()` helper that writes to `php://stderr` and falls back to `echo`.

Use `@file_get_contents(...)` for expected read failures and then emit explicit errors, otherwise PWPHP/PHP may print native warnings before the intended diagnostic.

## pw-build-meson structure

`pw-build-meson` is not modified for this project.

`src/meson.build` is hand-written and calls `pw-build-meson` for the `src` tree, then loads subdirs in dependency order:

```meson
subdir('helpers')
subdir('phpir')
subdir('valair')
subdir('valacodegen')
subdir('lib')
subdir('cli')
```

Each subfolder owns its own `meson.build.pw`.

`src/lib/` produces only the shared library aggregation.

`src/cli/` produces only CLI binaries.

## Runtime packages

`phpefc` is now detached from `valaphp-runtime-*`.

Current runtime projects:

- `../phpef-runtime-core`: Vala runtime library for zval-like values and core PHP value operations. It installs `libphpef-runtime-core.so`, `phpef-runtime-core.h`, `phpef-runtime-core.vapi`, `phpef-runtime-core.deps`, and `phpef-runtime-core.pc`.
- `../phpef-runtime-embed`: Vala runtime library that directly wraps `pwphpbytecode`/libphp embed behavior without depending on `valaphp-runtime-*`. It installs `libphpef-runtime-embed.so`, `phpef-runtime-embed.h`, `phpef-runtime-embed.vapi`, `phpef-runtime-embed.deps`, and `phpef-runtime-embed.pc`.

`phpefc/tests` should contain PHP compiler-path tests only. Runtime-library checks live in the runtime projects. There is intentionally no `find_library()` fallback in `phpefc`.

## Semantic reference testing

The runtime-embed zval comparison test exercises runtime-core semantics and compares the output with PHP/Zend reference behavior by evaluating equivalent PHP snippets through `phpef-runtime-embed`/libphp. Current cases include:

```php
$a = "5"; $a = $a + 4; echo $a;
$a = 5; $a = $a + "4"; echo $a;
$a = true; $a = $a + "4"; echo $a;
```

## Pre-optimizer generated-code shape

The initial generated Vala/C form should stay semantically close to PHP for recognition and debugging. This is separate from later optimizer stages.

Before optimization, codegen should prefer:

- line-preserving emission;
- PHP variable names and statement order;
- `phpfunc_*` wrappers for PHP-level language/function calls;
- explicit zval operations whose PHP meaning is recognizable.

Later optimizer stages may lower this into faster native/static/embed-specific forms, but that rewrite should be visible as an optimization step rather than hidden in the first correctness-oriented backend.

## Runtime function resolution

Generated-code calls to PHP-level runtime wrappers are resolved by `Php2ValaFuncResolver`, not hard-coded in `ValaCodeGenerator`.

The resolver reads runtime VAPI metadata, currently `phpef-runtime-core.vapi` plus optional `phpef-runtime-embed.vapi`, and confirms the wrapper exists before code generation emits it. Missing wrappers are hard errors. This keeps call lowering explicit and avoids silently generating calls to nonexistent runtime functions.

Current example: ValaIR `print` is treated as a PHP output language element and resolved to `phpfunc_print` through the resolver if and only if `phpfunc_print` exists in a runtime VAPI. `echo`/`print` are provided by `phpef-runtime-embed` because they use Zend output helpers, not `call_user_function()`.

Typed RuntimeEmbed fast paths intentionally do not use sequential `_s1`, `_s2`, `_s3` names. Public Vala/VAPI symbols must be stable for ABI-visible artifacts, so typed candidates use:

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

Example: `phpfunc_echo` is the dynamic/Zval-compatible base wrapper; `phpfunc_echo_typed_void_arg1_string` is the typed string fast path. The resolver groups `phpfunc_echo` with `phpfunc_echo_typed_...` candidates and keeps parsed return/argument signature metadata from the name. VAPI type spelling is normalized for this comparison (`int64`/`integer` -> `int`, `double` -> `float`, `boolean` -> `bool`, etc.), so RuntimeEmbed declarations such as `public static int64 phpfunc_strlen_typed_int_arg1_string(string value)` still expose the stable typed signature `strlen(string): int`.

A typed name alone is not treated as optimizer policy. Optimizer selection is allowed only when the RuntimeEmbed VAPI carries the adjacent metadata comment, for example `PHPEF typed_fast_path static_php_call` or `PHPEF typed_fast_path native`. That keeps static typed C-call boundaries sourced from RuntimeEmbed inventory metadata; `valacodegen` remains deliberately dumb and emits only the already selected call name.

## Function resolution stage

PHP-level call resolution is part of `PHPIR -> ValaIR` lowering.

`ValaIrBuilder` uses `Php2ValaFuncResolver` to convert PHP language/function intent into a concrete runtime wrapper name. Final codegen treats `ValaIrCallStatement.callee` as already resolved and only emits it.

This keeps `ValaCodeGenerator` simple and moves semantic resolution to the IR-lowering layer where PHP intent is still visible.

## Vala imports / uses

Required Vala namespaces are represented in ValaIR program metadata (`root.uses`). Final codegen emits those imports but does not decide which runtime namespaces are needed.

Current example:

```text
ValaIrProgram(root.uses = ["PhpefRuntimeCore"])
  -> using PhpefRuntimeCore;
```

This keeps target/runtime dependency knowledge in the IR-lowering layer instead of in raw source emission.

## Zval lowering stage

Runtime-backed zval lowering happens in `PHPIR -> ValaIR`.

Examples:

```text
PHPIR string literal -> ValaIrStaticCallExpression(callee = "Zval.from_string")
PHPIR int literal    -> ValaIrStaticCallExpression(callee = "Zval.from_int")
PHPIR concat         -> ValaIrMethodCallExpression(methodName = "concat")
PHP variable decl    -> ValaIrVariableDeclaration(typeName = "Zval")
```

Final Vala codegen is intentionally generic: it prints type names, static calls, method calls, variables and literals from ValaIR. It should not decide that a PHP value is a `Zval` or that concat means `.concat(...)`; those decisions belong to ValaIR lowering.

## Runtime wrapper naming convention in resolver

`Php2ValaFuncResolver` currently derives wrapper names by convention instead of storing special mappings:

```text
echo -> phpfunc_echo
strlen -> phpfunc_strlen
```

The derived name is valid only if it exists in the runtime VAPI. Missing wrappers are hard errors in `PHPIR -> ValaIR` lowering.

## Echo vs print

`echo` and `print` must remain distinct in PHPIR.

- `echo` is a language construct without a return value and may take multiple expressions.
- `print` is expression-like, takes one expression, and returns `1`.

Current statement-form lowering keeps this distinction:

```text
Stmt\Echo_    -> PhpIrEcho  -> phpfunc_echo(...)
Expr\Print_   -> PhpIrPrint -> phpfunc_print(...)
```

## Generic PHP function-call IR

Ordinary PHP function calls should be represented by a generic function-call node, not one class per PHP function.

Planned shape:

```text
PhpIrFuncCall(functionName = "strlen", args = [...])
```

The function name remains an attribute until `PHPIR -> ValaIR` lowering resolves it to a runtime wrapper such as `phpfunc_strlen`, validated against the relevant runtime VAPI.

## Missing wrapper diagnostics

A missing `phpfunc_*` wrapper is not always the same as a nonexistent PHP function. For future generic function calls, developer diagnostics should optionally query the selected PHP/Zend runtime through the embed path with `function_exists($functionName)` or an equivalent helper.

This classifies missing wrappers into:

- existing PHP function with missing phpef wrapper;
- unavailable/nonexistent PHP function in the selected runtime/extension set.

<!-- phpefc-architecture-status-2026-06-10 -->
## Architecture snapshot 2026-06-10

The current frontend/backend boundary is:

```text
PHP source
  -> nikic/php-parser AST
  -> PHPIR
  -> ValaIR
  -> line-preserving Vala source
```

Current PHPIR additions beyond the older minimal subset include:

```text
PhpIrPrint
PhpIrFuncCall
```

`PhpIrFuncCall` represents ordinary PHP function calls as expressions. A function call used directly as a statement still needs an enclosing PHPIR statement node or call-statement lowering. This is why `exec("hostname");` currently fails at `Stmt\Expression` even though `Expr\FuncCall` exists.

Runtime wrapper resolution belongs to PHPIR -> ValaIR:

```text
PhpIrFuncCall(functionName="substr", args=[...])
  -> ValaIR static call to phpfunc_substr(...)
```

The resolver should validate wrapper presence and arity against the runtime VAPI before final Vala text emission. Final Vala codegen should only emit the resolved ValaIR shape.

`print` and `echo` are language constructs, not ordinary functions:

```text
PhpIrEcho  -> phpfunc_echo(...)
PhpIrPrint -> phpfunc_print(...)
```

Keep this split even though both currently live in the embed runtime API surface.

## Debug source-line metadata and DWARF mapping

Source locations are explicit compiler metadata. They must not be inferred from coincidental equality between PHP and generated Vala line numbers.

Current contract:

- `.phpast` carries `phpSourceFile` at the root.
- `PhpIrProgram` and `ValaIrProgram` preserve `phpSourceFile`.
- `ValaCodeGenerator` records mappings from the actual emitted Vala line to the original PHP file/line.
- `phpefc-valair2vala` writes this mapping at the end of the generated `.vala` file as a `PHPEFC-C-SOURCE-LINES-BEGIN/END` JSON comment.
- `phpefc-c-add-source-lines <input.c> <output.c> <input.vala> [debug-bundle.json]` reads that comment, rewrites Valac C `#line` markers to PHP file/line locations, preserves the original Vala location as an inline comment on that same `#line` line, and optionally writes a `phpef-debug-bundle-v0` JSON bundle.

Optimizer rule: every optimization pass that changes generated source locations must preserve, rewrite, or explicitly invalidate/update the mapping metadata. The debug map is `actual generated Vala file:line -> original PHP file:line`; it is not `same line number in both files`.

The optional debug bundle contains `lineRewrites` with both directions:

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

When embedded as `.debug_phpef`, the bundle is debug material: `strip --strip-debug` removes it. DWARF line info makes GDB show PHP frames natively; `.debug_phpef` lets PHPEF-aware stacktrace tooling resolve those PHP frames back to the generated Vala origin for additional context.

## Build-pipeline descriptor for pw-build-meson

`pw-build-meson` integration should consume a compiler-provided pipeline descriptor rather than hard-coding each PHPEF stage.

Proposed command:

```text
phpefc-get-build-pipeline <input.php> [options]
```

The output should be a machine-readable `phpefc-build-pipeline-v0` document that lists stages, tools, inputs, outputs, runtime dependencies, debug artifacts, and final targets. `pw-build-meson` then renders that into Meson `custom_target()`, `executable()`, `objcopy`, tests, and install rules.

This keeps the build graph explicit and incremental while avoiding repeated `pw-build-meson` patches whenever PHPEF adds a new stage. Future additions such as optimizer passes, C source-line rewriting, `.debug_phpef` generation, alternate backends, or different target artifact kinds should be expressible by the descriptor.

Do not replace the visible graph with one monolithic end-to-end compiler command. Meson should still see intermediate artifacts and compile/link mapped C with its own compiler flags.

### Runtime function wrapper naming

PHPEFC resolves all `phpef-runtime-*` packages (`embed`, `native-*`, and future domain runtimes) through one function-resolver model. Native runtimes must follow the same public API convention as `phpef-runtime-embed`; they must not invent package-specific wrapper names or sidecar metadata files.

### Runtime selection boundary

`phpir2valair` / `Php2ValaFuncResolver` must resolve only `PhpefRuntimeCore` plus `PhpefRuntimeEmbed`. Native runtime packages such as `PhpefRuntimeNativeStandard` are not normal frontend resolution targets and must not appear in the first ValaIR output (`.valair.1`).

The only boundary that may rewrite Embed calls to native runtime calls is the ValaIR library/native optimizer stage (`library-embed2native`). This keeps stage metrics honest: typing/lowering can show the Embed-compatible PHP semantics first, then the library optimizer explicitly records what was replaced by a native runtime.

Generated Vala should import/use the core runtime namespace only where needed for core value types (`using PhpefRuntimeCore;`). Calls into Embed or Native runtimes should stay fully qualified (`PhpefRuntimeEmbed.*`, `PhpefRuntimeNativeStandard.*`) so the chosen runtime remains visible in generated and optimized artifacts.

Constant fetches follow the same rule: frontend lowering should use the generic Embed-compatible `phpfunc_constant(Zval.from_string(name))` shape; `library-embed2native` may later rewrite to a native `phpfunc_constant()` provider when that runtime exposes the compatible wrapper. Constant names/categories belong to the runtime or runtime-provided metadata, not to hard-coded compiler policy.

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

All `phpef-runtime-*` packages (`embed`, `native-*`, and future domain runtimes) use the same public Vala/VAPI naming and resolving scheme. PHPEFC must be able to resolve embed and native candidates with the same `Php2ValaFuncResolver` code path; runtime-specific packages only differ by VAPI metadata such as `typed_fast_path native` vs `typed_fast_path static_php_call`.

Native runtime packages must mirror the complete public Embed wrapper surface for any function/construct they take over. That means implementing the base Zval-compatible wrapper and every typed/derived variant with the exact same wrapper names/signatures as `phpef-runtime-embed`, e.g. `phpfunc_echo(PhpefRuntimeCore.Zval value)` plus `phpfunc_echo_typed_void_arg1_string(string value)`. Optimizers may choose `PhpefRuntimeNativeStandard.*`, but the ABI shape stays identical.

### Zval-compatible PHP wrappers

```text
phpfunc_<phpname>(Zval...)
```

- Return type is `PhpefRuntimeCore.Zval`.
- Arguments are `PhpefRuntimeCore.Zval` / `Zval?` as needed for PHP default/nullability behavior.
- Semantics should match PHP/Zend for the selected runtime. Native wrappers must be covered against the embed wrapper before they are treated as interchangeable.
- Public `phpfunc_*` wrappers are resolver/ABI endpoints and should not call each other. Shared implementation code belongs in internal, non-VAPI helper functions/files.
- Example: `phpfunc_microtime(Zval? as_float = null)`.

### Typed fast paths

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

- Return and argument types are scalar Vala types (`string`, `int`, `double`, `bool`, `void`, ...), normalized by PHPEFC for matching.
- Multiple typed variants for the same PHP function are allowed when each variant has a distinct signature suffix.
- The VAPI must carry an adjacent metadata comment before PHPEFC may optimize to the wrapper, for example:

```vala
// PHPEF typed_fast_path native; source php_function; PHP function: foo; typed_signature _typed_int_arg1_string
public static int phpfunc_foo_typed_int_arg1_string(string value);
```

For embed-backed static typed calls, use `typed_fast_path static_php_call` and include the boundary name when relevant. For native direct implementations, use `typed_fast_path native`.

### Special typed fast paths with argument-dependent return type

When the PHP return type depends on a literal argument value, encode that literal in the typed suffix:

```text
phpfunc_<phpname>_typed_<returntype>_argN_<type>_<literal>
```

Example:

```vala
// PHPEF typed_fast_path native; source php_function; PHP function: microtime; typed_signature _typed_double_arg1_bool_true
public static double phpfunc_microtime_typed_double_arg1_bool_true();
```

Here `arg1_bool_true` describes the PHP call shape (`microtime(true)`). The native Vala wrapper does not have to receive that argument if the literal is fully represented by the selected specialization. PHPEFC derives the PHP name and typed signature from the `phpfunc_*_typed_*` name and uses the VAPI comment only as optimizer policy/metadata.

The resolver validates the base wrapper and typed candidates against runtime VAPI declarations. The `phpfunc_*_typed_*` name provides PHP name, return type, and argument/literal signature; the adjacent VAPI `PHPEF typed_fast_path ...` comment is optimizer policy and optional boundary metadata.

