Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to elephc, a PHP-to-native compiler written in Rust.
Releases are listed newest first.

## [Unreleased]
- Fixed several PHP conformance gaps: `Exception`/`Error` constructors now accept and store `$previous` (`?Throwable`) so `getPrevious()` round-trips instead of always returning null; interface and parent method implementations may narrow self returns covariantly, including static methods; soft-keyword `enum` is accepted throughout class-like declarations, type hints, imports, and scoped expressions; `use const` handles lexer-tokenized globals in single, multiple, and grouped imports; and `foreach` over unknown `array` values preserves both integer and string keys instead of treating them as integers only.
- Fixed symbolic defaults for object-typed parameters: enum-case defaults are now resolved and type-checked semantically after class schemas are complete across functions, methods and constructors, constructor-promoted properties, and closures, including named, `self::`, `static::`, and `parent::` receivers. Missing cases and scalar class constants are rejected instead of slipping through syntactic typing, while `ReflectionParameter::getDefaultValueConstantName()` preserves the source-level constant spelling.
- Added PHP-compatible `::class` support on object expressions: `$object::class` now returns the receiver's concrete runtime class name, evaluates the receiver exactly once, and rejects statically known non-object receivers, while existing named and static forms remain unchanged.
- Added support for `static $x;` function-static declarations without an initializer, in both the native parser and the Magician `eval()` parser: the missing initializer desugars to `= null`, matching PHP, where `static $x;` and `static $x = null;` are identical (including `isset()` behavior).
Expand Down
2 changes: 1 addition & 1 deletion docs/php/control-structures.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ try {
Supported subset:

- built-in `Error` and `Exception` classes and the `Throwable` interface are available without declaring them
- `Error` and `Exception` provide `$message`, `$code`, `__construct($message = "", $code = 0)`, and the standard `Throwable` methods: `getMessage()`, `getCode()`, `getFile()`, `getLine()`, `getTrace()`, `getTraceAsString()`, `getPrevious()`, and `__toString()`
- `Error` and `Exception` provide `$message`, `$code`, `$previous`, `__construct($message = "", $code = 0, $previous = null)`, and the standard `Throwable` methods: `getMessage()`, `getCode()`, `getFile()`, `getLine()`, `getTrace()`, `getTraceAsString()`, `getPrevious()`, and `__toString()`
- the SPL exception hierarchy is built-in: `LogicException`, `BadFunctionCallException`, `BadMethodCallException`, `DomainException`, `InvalidArgumentException`, `LengthException`, `OutOfRangeException`, `RuntimeException`, `OutOfBoundsException`, `OverflowException`, `RangeException`, `UnderflowException`, `UnexpectedValueException`. Each is a marker subclass that inherits the constructor, `$message`, and the standard `Throwable` methods from `Exception`. Catch a specific type (`InvalidArgumentException`), an intermediate parent (`LogicException`), or the root (`Exception`/`Throwable`)
- `throw <expr>;` where `<expr>` has an object type implementing `Throwable`
- `throw <expr>` can also be used inside expressions such as `??` and ternaries
Expand Down
4 changes: 2 additions & 2 deletions docs/php/system-and-io.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ The full PHP `JSON_*` family is exposed and can be combined with the bitwise OR
| Symbol | Kind | Description |
|---|---|---|
| `JsonSerializable` | Interface | Implementing classes can override `jsonSerialize(): mixed`; `json_encode()` dispatches to it instead of walking public properties. |
| `Error` | Class | Base PHP error throwable with `message: string`, `code: int`, `__construct(string $message = "", int $code = 0)`, and the standard `Throwable` methods. `FiberError` extends this class. |
| `Exception` | Class | Base PHP exception with `message: string`, `code: int`, `__construct(string $message = "", int $code = 0)`, and the standard `Throwable` methods. |
| `Error` | Class | Base PHP error throwable with `message: string`, `code: int`, `previous: ?Throwable`, `__construct(string $message = "", int $code = 0, ?Throwable $previous = null)`, and the standard `Throwable` methods. `FiberError` extends this class. |
| `Exception` | Class | Base PHP exception with `message: string`, `code: int`, `previous: ?Throwable`, `__construct(string $message = "", int $code = 0, ?Throwable $previous = null)`, and the standard `Throwable` methods. |
| `RuntimeException` | Class | `extends Exception`. Standard PHP "runtime errors" base class. |
| `JsonException` | Class | `extends RuntimeException`. Carries the originating `JSON_ERROR_*` code; `getCode()` returns it (e.g. 4 = SYNTAX, 1 = DEPTH, 10 = UTF16, 7 = INF_OR_NAN). |
| `stdClass` | Class | Dynamic-property container. `$obj = new stdClass(); $obj->name = "x";` works for any property name; storage is a backing hash on the instance. `json_decode($json)` returns stdClass by default (PHP semantics); pass `assoc: true` to get an associative array. |
Expand Down
10 changes: 10 additions & 0 deletions examples/exceptions/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,13 @@ class AppException extends Exception implements AppThrowable {}
} catch (AppThrowable $e) {
echo "caught AppThrowable: " . $e->getMessage() . " #" . $e->getCode() . PHP_EOL;
}

try {
try {
throw new AppException("root cause", 10);
} catch (AppException $previous) {
throw new AppException("wrapped failure", 11, $previous);
}
} catch (AppException $e) {
echo $e->getMessage() . " <- " . $e->getPrevious()?->getMessage() . PHP_EOL;
}
136 changes: 127 additions & 9 deletions src/codegen/lower_inst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3219,6 +3219,13 @@ fn lower_interface_method_call(
interface_name: &str,
method_name: &str,
) -> Result<()> {
// Builtin Throwable methods are compact-payload intrinsics. Their interface vtable
// slots stay zero because no synthetic method bodies are emitted, so dispatch here
// would `blr` to null. Use the same intrinsic path as a concrete Throwable receiver.
if is_throwable_standard_method_call(ctx, interface_name, method_name) {
let object = expect_operand(inst, 0)?;
return lower_throwable_standard_method(ctx, inst, object, method_name);
}
let (normalized, method_key, callee_sig) =
resolve_interface_call_signature(ctx, interface_name, method_name, inst.operands.len())?;
let mut param_types = Vec::with_capacity(callee_sig.params.len() + 1);
Expand Down Expand Up @@ -3347,6 +3354,22 @@ fn lower_nullable_receiver_interface_method_call(
interface_name: &str,
method_name: &str,
) -> Result<()> {
// Same compact-payload intrinsic path as `lower_interface_method_call`: Throwable's
// interface vtable slots are empty, so nullable `?Throwable` must not dispatch through them.
if is_throwable_standard_method_call(ctx, interface_name, method_name) {
let null_label = ctx.next_label("method_receiver_null");
let done_label = ctx.next_label("method_receiver_done");
let receiver_reg = abi::nested_call_reg(ctx.emitter);
objects::emit_nullable_receiver_object_payload(ctx, object, &null_label, receiver_reg)?;
// Re-materialize the unboxed object into the SSA operand's result register so the
// shared Throwable intrinsic lowerer can `load_value_to_reg` the receiver.
lower_throwable_standard_method_from_reg(ctx, inst, receiver_reg, method_name)?;
abi::emit_jump(ctx.emitter, &done_label);
ctx.emitter.label(&null_label);
emit_method_call_on_null_fatal(ctx, method_name);
ctx.emitter.label(&done_label);
return Ok(());
}
let normalized = interface_name.trim_start_matches('\\');
let method_key = php_symbol_key(method_name);
let callee_sig = ctx
Expand Down Expand Up @@ -3839,13 +3862,43 @@ fn lower_throwable_standard_method(
}
let object_reg = abi::symbol_scratch_reg(ctx.emitter);
ctx.load_value_to_reg(object, object_reg)?;
lower_throwable_standard_method_loaded(ctx, inst, object_reg, method_name)
}

/// Lowers a compact Throwable method when the receiver object pointer is already in `object_reg`.
///
/// Used by nullable `?Throwable` / Mixed-unbox paths that have already extracted the object
/// payload and must not reload the original SSA value (which may still be a Mixed cell).
fn lower_throwable_standard_method_from_reg(
ctx: &mut FunctionContext<'_>,
inst: &Instruction,
object_reg: &str,
method_name: &str,
) -> Result<()> {
if inst.operands.len() != 1 {
return Err(CodegenIrError::unsupported(format!(
"Throwable::{} with {} EIR operands",
method_name,
inst.operands.len()
)));
}
lower_throwable_standard_method_loaded(ctx, inst, object_reg, method_name)
}

/// Shared compact-payload Throwable method body after the receiver object is in `object_reg`.
fn lower_throwable_standard_method_loaded(
ctx: &mut FunctionContext<'_>,
inst: &Instruction,
object_reg: &str,
method_name: &str,
) -> Result<()> {
let return_ty = match php_symbol_key(method_name).as_str() {
"getmessage" => lower_throwable_get_message(ctx, object_reg),
"getcode" => lower_throwable_get_code(ctx, object_reg),
"getfile" | "gettraceasstring" => lower_throwable_empty_string(ctx),
"getline" => lower_throwable_zero_int(ctx),
"gettrace" => lower_throwable_empty_trace_array(ctx),
"getprevious" => lower_throwable_null_previous(ctx, inst),
"getprevious" => lower_throwable_get_previous(ctx, object_reg, inst),
"__tostring" => lower_throwable_get_message(ctx, object_reg),
_ => Err(CodegenIrError::unsupported(format!(
"Throwable intrinsic method {}",
Expand Down Expand Up @@ -3913,18 +3966,83 @@ fn lower_throwable_empty_trace_array(ctx: &mut FunctionContext<'_>) -> Result<Ph
Ok(PhpType::Array(Box::new(PhpType::Mixed)))
}

/// Materializes the synthetic null result used by `Throwable::getPrevious()`.
fn lower_throwable_null_previous(
/// Loads `Throwable::getPrevious()` from payload offset 40, retaining a non-null previous.
///
/// When the EIR result is `Mixed` (`?Throwable`), both the object and null arms box here and
/// return `Mixed` so the shared intrinsic post-box path does not retag a live object as null
/// (`PhpType::Void` → runtime tag 8).
fn lower_throwable_get_previous(
ctx: &mut FunctionContext<'_>,
object_reg: &str,
inst: &Instruction,
) -> Result<PhpType> {
let payload = if inst.result_php_type.codegen_repr() == PhpType::Mixed {
0
let result_reg = abi::int_result_reg(ctx.emitter);
let null_label = ctx.next_label("throwable_previous_null");
let done_label = ctx.next_label("throwable_previous_done");
let result_is_mixed = matches!(inst.result_php_type.codegen_repr(), PhpType::Mixed);
let object_ty = PhpType::Object("Throwable".to_string());
abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, 40);
match ctx.emitter.target.arch {
Arch::AArch64 => {
ctx.emitter
.instruction(&format!("cbz {}, {}", result_reg, null_label)); // missing previous → null
// `__rt_incref` expects the object in x0.
if result_reg != "x0" {
ctx.emitter
.instruction(&format!("mov x0, {}", result_reg)); // move previous into incref arg
}
abi::emit_call_label(ctx.emitter, "__rt_incref"); // caller owns the returned previous
if result_reg != "x0" {
ctx.emitter
.instruction(&format!("mov {}, x0", result_reg)); // restore result register
}
if result_is_mixed {
emit_box_current_value_as_mixed(ctx.emitter, &object_ty);
}
ctx.emitter
.instruction(&format!("b {}", done_label)); // skip null materialization
ctx.emitter.label(&null_label);
if result_is_mixed {
abi::emit_load_int_immediate(ctx.emitter, result_reg, 0);
emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Void);
} else {
abi::emit_load_int_immediate(ctx.emitter, result_reg, 0x7fff_ffff_ffff_fffe);
}
ctx.emitter.label(&done_label);
}
Arch::X86_64 => {
ctx.emitter.instruction(&format!("test {}, {}", result_reg, result_reg)); // missing previous → null
ctx.emitter
.instruction(&format!("jz {}", null_label));
if result_reg != "rax" {
ctx.emitter
.instruction(&format!("mov rax, {}", result_reg)); // move previous into incref arg
}
abi::emit_call_label(ctx.emitter, "__rt_incref"); // caller owns the returned previous
if result_reg != "rax" {
ctx.emitter
.instruction(&format!("mov {}, rax", result_reg)); // restore result register
}
if result_is_mixed {
emit_box_current_value_as_mixed(ctx.emitter, &object_ty);
}
ctx.emitter
.instruction(&format!("jmp {}", done_label)); // skip null materialization
ctx.emitter.label(&null_label);
if result_is_mixed {
abi::emit_load_int_immediate(ctx.emitter, result_reg, 0);
emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Void);
} else {
abi::emit_load_int_immediate(ctx.emitter, result_reg, 0x7fff_ffff_ffff_fffe);
}
ctx.emitter.label(&done_label);
}
}
if result_is_mixed {
Ok(PhpType::Mixed)
} else {
0x7fff_ffff_ffff_fffe
};
abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), payload);
Ok(PhpType::Void)
Ok(object_ty)
}
}

/// Lowers `Fiber::start(...)` by copying boxed start arguments into the Fiber object.
Expand Down
6 changes: 4 additions & 2 deletions src/codegen/lower_inst/builtins/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ fn emit_throw_value_error_aarch64(
message_symbol: &str,
message_len: usize,
) {
ctx.emitter.instruction("mov x0, #32"); // request Throwable payload storage for the clamp ValueError
ctx.emitter.instruction("mov x0, #56"); // request Throwable payload storage for the clamp ValueError
ctx.emitter.instruction("bl __rt_heap_alloc"); // allocate the ValueError object payload
ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks an object instance allocation
ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation header as a runtime object
Expand All @@ -636,6 +636,7 @@ fn emit_throw_value_error_aarch64(
ctx.emitter.instruction(&format!("mov x9, #{}", message_len)); // materialize the static ValueError message length
ctx.emitter.instruction("str x9, [x0, #16]"); // store the exception message length
ctx.emitter.instruction("str xzr, [x0, #24]"); // store the default zero exception code
ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null
abi::emit_symbol_address(ctx.emitter, "x9", "_exc_value");
ctx.emitter.instruction("str x0, [x9]"); // publish the active ValueError object
ctx.emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder
Expand All @@ -650,7 +651,7 @@ fn emit_throw_value_error_x86_64(
ctx.emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation
ctx.emitter.instruction("mov rbp, rsp"); // establish an aligned helper frame for heap allocation
ctx.emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned
ctx.emitter.instruction("mov rax, 32"); // request Throwable payload storage for the clamp ValueError
ctx.emitter.instruction("mov rax, 56"); // request Throwable payload storage for the clamp ValueError
ctx.emitter.instruction("call __rt_heap_alloc"); // allocate the ValueError object payload
ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // materialize the x86_64 object heap-kind header
ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation header as a runtime object
Expand All @@ -660,6 +661,7 @@ fn emit_throw_value_error_x86_64(
ctx.emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the static ValueError message pointer
ctx.emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", message_len)); // store the exception message length
ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // store the default zero exception code
ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null
ctx.emitter.instruction("mov QWORD PTR [rip + _exc_value], rax"); // publish the active ValueError object
ctx.emitter.instruction("mov rsp, rbp"); // release the helper frame before throwing
ctx.emitter.instruction("pop rbp"); // restore caller frame pointer before throwing
Expand Down
Loading
Loading