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.
 
 

4.1 KiB

#[Immutable] compile-time effect checking

Purpose

#[Immutable] is a TypePHP compile-time annotation modelled after C++ const. It prevents accidental mutation in statically compiled code without adding a wrapper object, Zend metadata, runtime branch, or ABI change.

It is intentionally a best-effort static tool rather than a security boundary. Calls whose target is deliberately made dynamic are an escape hatch and do not receive a runtime guard.

Supported targets

#[Immutable]
public function name(): string
{
    return $this->name;
}

function inspect(#[Immutable] User $user): string
{
    return $user->name();
}

The attribute is valid on methods and on function, method, and closure parameters. On an instance method it makes $this immutable. On a parameter it makes the binding immutable and, when it can contain an object, treats the referenced object as immutable as well.

Rejected operations

For an immutable root such as $this or $user, the compiler rejects:

  • assignment, destructuring, and array-element or object-property writes;
  • compound assignment, ++, --, unset(), taking a reference, and foreach (... as &$value);
  • a statically named method call unless the resolved method is also marked #[Immutable];
  • a mutating value extension such as $array->sort(); read-only array/string methods remain available;
  • passing an object to a statically resolved parameter that is not itself #[Immutable];
  • passing any immutable value to a mutable by-reference parameter, including extension functions such as sort();
  • storing an immutable object identity in an object property, array, global/static variable, or returning/yielding it as a mutable value.

An immutable by-reference parameter is supported. It acts like a C++ const &: the reference is accepted because the callee is checked against mutation.

#[MethodsFor] follows the same contract. An object extension is callable on an immutable receiver only when its receiver parameter is marked #[Immutable].

Aliases, closures, generators, and inheritance

Local aliases of immutable objects remain immutable:

$alias = $user;
$alias->rename('new'); // compile-time error

clone creates a distinct mutable object and therefore intentionally drops the annotation. Captured variables, arrow functions, closure $this, and Fiber generator bodies carry immutable metadata into their generated function contexts.

An overriding class or interface method may strengthen an ordinary contract by adding #[Immutable], but it cannot remove #[Immutable] from an inherited method or parameter.

Value versus object semantics

Scalar values and PHP copy-on-write values can be read and copied normally. For example, count($values) and $copy = $values do not modify an immutable array. The compiler propagates immutability through an expression only when object identity is possible.

Explicit escape hatches

The following intentionally bypass static method-effect checking:

$method = 'rename';
$user->$method('new');

$callable = getRuntimeCallable();
$callable($user);

The same applies to other runtime-only mechanisms that hide the target from the compiler, including reflection and dynamic ZendVM code. TypePHP neither inserts a runtime read-only proxy nor attempts to recover the escaped value later.

This boundary is deliberate: #[Immutable] should cost nothing in generated code and should not complicate PHPX/ZendVM object semantics.

Property hooks and magic access

Property-hook reads are lowered to generated method calls. Consequently, a hook used through an immutable receiver must itself carry an #[Immutable] method contract; otherwise the generated call is rejected. Fully dynamic magic access is covered by the same escape-hatch rule as other runtime-only behavior.

Implementation boundaries

The implementation is isolated in src/Immutable/ImmutableSupportTrait.php. FunctionDef and ArgInfo retain only compile-time effect bits, while each FunctionContext stores the immutable roots and object aliases relevant to that body. Checks run during AST lowering and emit no C++ code when successful.