[ ZR-41 · EXECUTION RESEARCH ] Architecture

From Source to Sealed Operations.

The proposed engineering path toward a safer PHP-powered web: turn PHP source into explicit operations, check ownership and effects, and execute inside request-owned regions. These are design obligations to implement and test, not an existing safe-browsing service.

The pipeline

The proposed pipeline has five stages before execution. Each one narrows what the next stage must handle, and each one is allowed to stop with a diagnostic instead of guessing.

PLATE A · Semantic loweringfig. 2
PHP sourcefiles + includes Syntax/scopeAST, symbols Normalizeexplicit coercion ZIRsealed operations Own/effectchecks Vessel Enginerequest cell Diagnostics: unsupported or ambiguous constructs are reported, never guessed
Source moves left to right through analysis, normalization, ZIR construction and ownership/effect checks before the Vessel Engine executes it. Any stage can stop with a diagnostic; nothing that fails a check reaches execution.

1. Syntax and scope analysis

Parsing is the easy part. Scope analysis is not: PHP allows variable variables, extract(), compact(), include inside functions, and global declarations. The analyser classifies each function as statically scoped, or as needing a dynamic symbol table, and records that choice so later stages can stay precise where the code allows it.

2. Semantic normalization

Implicit behaviour becomes explicit. String-to-number conversion, comparison rules, array key normalization (the string "8" becomes integer key 8), and parameter coercion under the caller's strict_types mode are all rewritten as named operations. The goal is that no ZIR operation silently converts anything.

3. ZIR construction

ZIR is a typed, register-style representation where every value carries an origin: a request region, an immutable shared constant, or a capability handle. Memory operations name their region; effects name their capability. ZIR is designed to be boring to read.

4. Ownership and effect checks

Checks reject programs where a request value could escape into process-wide state, where an effect has no capability path, or where a handle could outlive its region. Some checks are static; others become runtime guards that fail with a defined error.

5. Vessel Engine execution

The engine executes ZIR inside a request cell that owns the region, the capability set and the resource budget. It is an interpreter in the first design, so its correctness can be compared against the reference engine before any compilation strategy is considered.

Semantics that resist lowering

Dynamic values

A PHP variable can hold any type and change it at any assignment. ZIR represents values as a tagged union with an explicit region origin, and operations that depend on type dispatch on the tag. Type inference can remove checks where it proves a type, but correctness may never depend on inference succeeding.

References

References are the most delicate feature. A reference is modelled as a shared slot owned by the request region, and variables bound to it hold a slot handle rather than a value. Because slots are region-owned, a reference can never point into another request; but the engine must still reproduce reference-in-array behaviour and copy-on-write separation exactly as PHP does.

Coercions

PHP 8 tightened many conversions, yet leading-numeric strings, numeric-string comparison and integer overflow into floats remain intricate. Every coercion is a named ZIR operation with a specified warning or exception, so differential tests can compare not just results but the diagnostics produced on the way.

Closures

Closures capture by value unless use (&$x) captures by reference, and arrow functions capture by value automatically. A closure object is region-owned; storing it in process-wide state is exactly the kind of escape the ownership check rejects.

Exceptions and unwinding

ZIR has explicit landing blocks. Unwinding runs finally bodies in order and releases frame-local slots, and the engine records which capability operations were in progress so that a half-completed effect is either finished or reported, never silently abandoned.

Objects

Objects are handles with identity. Magic methods, property hooks, dynamic properties and destructors all route through dispatch operations in ZIR. Destructor timing is the hardest part: reproducing reference-count timing requires reference counting, or an equivalent analysis, for objects that define destructors.

Resource cleanup

Files, sockets and database handles are capability-backed resources tied to the request cell. Explicit fclose() releases early; otherwise teardown releases them in reverse acquisition order, after destructors have had their bounded chance to run.

Worked example

The function below is ordinary PHP. It mixes a by-reference loop, string array keys, arithmetic coercion and a returned array that shares nothing with the caller. Its lowering shows how much implicit behaviour becomes explicit.

tally.phpPHP
<?php
// Fresh example: tally order sizes, writing a computed total back into each row.
function tally(array $orders, float $threshold): array {
    $buckets = ['small' => 0, 'large' => 0];
    foreach ($orders as &$order) {
        $order['total'] = $order['qty'] * $order['price'];
        $key = $order['total'] >= $threshold ? 'large' : 'small';
        $buckets[$key]++;
    }
    unset($order); // drop the dangling reference to the last row
    return [$buckets, $orders];
}
tally.zirZIR pseudocode
; ZIR pseudocode. Illustrative lowering, not output of a working compiler.
fn tally(%orders, %threshold) -> val
  region R                                ; request-owned region for this call
  %orders  = coerce.array  %orders        ; TypeError if not an array
  %thr     = coerce.float  %threshold     ; int widens; string per caller strict_types
  %buckets = arr.new R { "small": 0, "large": 0 }
  %orders  = arr.separate R %orders       ; by-ref loop forces a private copy
  %it      = iter.by_ref %orders
loop:
  %slot    = iter.next %it else done      ; slot aliases one element of %orders
  %qty     = slot.load %slot "qty"        ; missing key: warning, then null
  %price   = slot.load %slot "price"
  %total   = num.mul %qty %price          ; non-numeric string: TypeError
  slot.store %slot "total" %total
  %big     = cmp.ge %total %thr           ; PHP 8 comparison rules
  %key     = select %big "large" "small"
  arr.incr %buckets %key
  jmp loop
done:
  ref.bind $order %slot                   ; $order still aliases the last row here
  ref.unbind $order                       ; unset($order)
  ret arr.pack R [ %buckets, %orders ]

Three details carry most of the weight. arr.separate is where copy-on-write becomes visible: the by-reference loop must not mutate the caller's array. slot.load carries PHP's undefined-key behaviour as a warning plus null, which then multiplies to zero. And ref.bind exists because, after the loop, $order is still a reference to the last element; without the unset, a later loop over $order would overwrite it.

Engineering challenges

  • Reproducing observable details. Warning text, warning order, float formatting and sort stability are observable. Matching them is tedious, measurable work, and the only credible measurement is differential testing against the reference engine.
  • The standard library is the language. Hundreds of built-in functions carry their own coercion and error behaviour. Lowering user code is a fraction of the work.
  • Dynamic code. eval, runtime include of computed paths, and autoloaders mean the pipeline must also run at request time, under the same checks and the same budget.
  • Destructors. Exact destructor timing and region-based reclamation pull in opposite directions. The design keeps reference counting for objects with destructors and accepts the cost.
  • A memory-safe host language is not a proof. Writing the engine in Rust removes classes of host bugs, but logic errors in coercion, aliasing or teardown remain ordinary bugs that tests must find.

Next: Authority Has a Lifetime describes the request cell and its boundaries.