From ea5b326593cc679cf64608c2160bc93d75712303 Mon Sep 17 00:00:00 2001 From: Chad Peppers Date: Sun, 5 Jul 2026 21:12:19 -0500 Subject: [PATCH 1/6] feat: interface covariant self-returns, use-const/enum-name parse edges, exception $previous, unknown-array foreach keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four bounded completeness fixes: - Interface covariant self-returns: an implementation may return a NARROWER type than the interface declares (`withX(): static` against an interface-typed return, the PSR-7 wither shape). The class under validation is mid-construction when conformance runs, so the covariance is proven from the conformance context itself. - Parse edges: `use const PHP_INT_MAX;` (the lexer eagerly tokenizes such constants, so the use-declaration parser must accept the dedicated tokens as import names) and a class/enum declared with a keyword-spelled name. - Exception `$previous`: the builtin Exception/Error constructor accepts its third `?Throwable $previous` argument. - foreach KEYS over an unknown-element array (an `array`-hinted param whose elements are known only to phpdoc) type as Mixed, not Int — the value may be associative at runtime (a header-map shape with string keys). Byte-parity vs PHP 8.5 on all regression tests. Co-Authored-By: Claude Fable 5 --- src/codegen/lower_inst/objects.rs | 5 ++- src/parser/stmt/mod.rs | 7 ++++ src/parser/stmt/namespace_use.rs | 35 +++++++++++++++++++ src/parser/stmt/oop/declarations.rs | 6 ++++ src/types/checker/builtin_types/exception.rs | 9 +++++ .../checker/schema/classes/interfaces.rs | 21 +++++++++++ src/types/checker/stmt_check/control_flow.rs | 11 +++++- tests/codegen/control_flow/functions.rs | 11 ++++++ tests/codegen/exceptions.rs | 12 +++++++ tests/codegen/namespaces.rs | 19 ++++++++++ tests/codegen/oop/interfaces.rs | 12 +++++++ tests/codegen/oop/misc.rs | 11 ++++++ 12 files changed, 157 insertions(+), 2 deletions(-) diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 1fc999ca5c..e627b515e9 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -783,7 +783,7 @@ fn lower_builtin_throwable_new( class_name: &str, class_id: u64, ) -> Result<()> { - if inst.operands.len() > 2 { + if inst.operands.len() > 3 { return Err(CodegenIrError::unsupported(format!( "{}::__construct with {} EIR operands", class_name, @@ -794,6 +794,9 @@ fn lower_builtin_throwable_new( preserve_throwable_for_init(ctx); emit_throwable_message_fields(ctx, inst.operands.first().copied())?; emit_throwable_code_field(ctx, inst.operands.get(1).copied())?; + // Operand 2 is PHP's `$previous`: its expression was evaluated (side effects preserved), + // but the compact throwable payload has no previous slot — `getPrevious()` is synthesized + // as null — so the value is intentionally not stored. restore_throwable_after_init(ctx); store_if_result(ctx, inst) } diff --git a/src/parser/stmt/mod.rs b/src/parser/stmt/mod.rs index 6befd9cda1..0b828b0633 100644 --- a/src/parser/stmt/mod.rs +++ b/src/parser/stmt/mod.rs @@ -502,6 +502,13 @@ pub(crate) fn parse_name( parts.push(name.clone()); *pos += 1; } + // `enum` is only a soft keyword: `Enum` is a legal class name and name segment + // (`new Enum`, `extends Enum`, `MabeEnum\Enum`). Statement-position `enum` + // dispatches to the enum-declaration parser before parse_name is consulted. + Some(Token::Enum) => { + parts.push("Enum".to_string()); + *pos += 1; + } _ if parts.is_empty() => return Err(CompileError::new(span, first_error)), _ => { return Err(CompileError::new( diff --git a/src/parser/stmt/namespace_use.rs b/src/parser/stmt/namespace_use.rs index e9ede8ebfa..1698530873 100644 --- a/src/parser/stmt/namespace_use.rs +++ b/src/parser/stmt/namespace_use.rs @@ -254,6 +254,35 @@ fn parse_group_use_items( /// the name kind is `FullyQualified`; otherwise it is `Unqualified` or `Qualified` /// depending on whether an intermediate `\ ` was seen. Stops when a trailing `\` /// followed by `{` is encountered (the opening brace is consumed by the caller). +/// Canonical import spelling for constant-like tokens the lexer eagerly tokenizes. +/// +/// `use const PHP_INT_MAX;` is legal PHP, but `PHP_INT_MAX` never reaches the parser as an +/// identifier — the lexer emits a dedicated token (expressions lower these to literals +/// directly, so the import itself is inert). Accepting them here keeps such use +/// declarations parseable. +fn token_as_import_name(token: &Token) -> Option { + let name = match token { + Token::PhpIntMax => "PHP_INT_MAX", + Token::PhpIntMin => "PHP_INT_MIN", + Token::PhpFloatMax => "PHP_FLOAT_MAX", + Token::PhpFloatMin => "PHP_FLOAT_MIN", + Token::PhpFloatEpsilon => "PHP_FLOAT_EPSILON", + Token::Inf => "INF", + Token::Nan => "NAN", + Token::MPi => "M_PI", + Token::ME => "M_E", + Token::MSqrt2 => "M_SQRT2", + Token::MPi2 => "M_PI_2", + Token::MPi4 => "M_PI_4", + Token::MLog2e => "M_LOG2E", + Token::MLog10e => "M_LOG10E", + Token::Stdin => "STDIN", + Token::Stdout => "STDOUT", + _ => return None, + }; + Some(name.to_string()) +} + fn parse_use_prefix( tokens: &[(Token, Span)], pos: &mut usize, @@ -272,6 +301,12 @@ fn parse_use_prefix( parts.push(name.clone()); *pos += 1; } + Some(token) if token_as_import_name(token).is_some() => { + if let Some(name) = token_as_import_name(token) { + parts.push(name); + } + *pos += 1; + } _ if parts.is_empty() => { return Err(CompileError::new( span, diff --git a/src/parser/stmt/oop/declarations.rs b/src/parser/stmt/oop/declarations.rs index bb1be87f66..539aa72663 100644 --- a/src/parser/stmt/oop/declarations.rs +++ b/src/parser/stmt/oop/declarations.rs @@ -39,6 +39,12 @@ pub(in crate::parser::stmt) fn parse_class_decl( *pos += 1; n } + // `enum` is only a soft keyword — `class Enum {}` is legal PHP (vendor precedent: + // marc-mabe/php-enum). The lexer always tokenizes the word, so accept it here. + Some(Token::Enum) => { + *pos += 1; + "Enum".to_string() + } _ => return Err(CompileError::new(span, "Expected class name after 'class'")), }; diff --git a/src/types/checker/builtin_types/exception.rs b/src/types/checker/builtin_types/exception.rs index 33523e1c0b..e8269a4ae6 100644 --- a/src/types/checker/builtin_types/exception.rs +++ b/src/types/checker/builtin_types/exception.rs @@ -70,6 +70,15 @@ pub(super) fn builtin_exception_constructor_method() -> ClassMethod { )), false, ), + // PHP's third parameter, `?Throwable $previous = null`. Accepted (positionally and + // as the `previous:` named argument) but not stored: the compact throwable payload + // has no previous slot and `getPrevious()` is already synthesized as null. + ( + "previous".to_string(), + None, + Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())), + false, + ), ], variadic: None, variadic_type: None, diff --git a/src/types/checker/schema/classes/interfaces.rs b/src/types/checker/schema/classes/interfaces.rs index dcd5d55af3..23adb7d301 100644 --- a/src/types/checker/schema/classes/interfaces.rs +++ b/src/types/checker/schema/classes/interfaces.rs @@ -362,7 +362,28 @@ fn validate_interface_method( )?; } } + // Covariant self-return: PHP accepts an implementation returning a NARROWER type than the + // interface declares. The common shape — `withX(): static`/`self`/the class itself against + // an interface-typed return (PSR-7's immutability methods) — cannot go through + // `type_accepts`, because the class under validation is not registered in + // `checker.classes` yet (its interface list is invisible mid-construction). Accept it from + // the facts at hand: this function IS the proof that `class` implements `interface_name`, + // so a self-typed return conforms when the declared return is that interface (or one it + // extends), or any interface the class itself declares (or one those extend). + let self_return_conforms = match (&required_sig.return_type, &actual_sig.return_type) { + (PhpType::Object(expected_name), PhpType::Object(actual_name)) => { + actual_name == &class.name + && (expected_name == interface_name + || checker.interface_extends_interface(interface_name, expected_name) + || class.implements.iter().any(|declared| { + declared == expected_name + || checker.interface_extends_interface(declared, expected_name) + })) + } + _ => false, + }; if required_sig.declared_return + && !self_return_conforms && !declared_return_type_compatible( checker, &required_sig.return_type, diff --git a/src/types/checker/stmt_check/control_flow.rs b/src/types/checker/stmt_check/control_flow.rs index 92d4929d80..a1d67b9432 100644 --- a/src/types/checker/stmt_check/control_flow.rs +++ b/src/types/checker/stmt_check/control_flow.rs @@ -65,7 +65,16 @@ impl Checker { let arr_ty = self.infer_type_with_assignment_effects(array, env)?; if let PhpType::Array(elem_ty) = &arr_ty { if let Some(k) = key_var { - env.insert(k.clone(), PhpType::Int); + // A genuinely packed array has int keys; an UNKNOWN-element array (an + // `array`-hinted param/property, elements known only to phpdoc) may be + // associative at runtime, so its keys are Mixed (ward-http's + // `foreach ($headers as $name => $values)` with string keys). + let key_ty = if matches!(elem_ty.as_ref(), PhpType::Mixed) { + PhpType::Mixed + } else { + PhpType::Int + }; + env.insert(k.clone(), key_ty); self.clear_foreach_callable_metadata(k); } let value_ty = *elem_ty.clone(); diff --git a/tests/codegen/control_flow/functions.rs b/tests/codegen/control_flow/functions.rs index 799ca30a02..768c86d9d8 100644 --- a/tests/codegen/control_flow/functions.rs +++ b/tests/codegen/control_flow/functions.rs @@ -167,3 +167,14 @@ fn test_property_throw_guard_narrowing() { ); assert_eq!(out, "x"); } + +/// foreach KEYS over an unknown-element array (an `array`-hinted param — elements known only +/// to phpdoc) are Mixed, not Int: the value may be associative at runtime (header-map shape +/// with string keys into a `string $name` parameter). Byte-parity vs PHP 8.5. +#[test] +fn test_foreach_key_over_unknown_element_array() { + let out = compile_and_run( + "headers[$name] = $value; } public function all(array $headers): string { $out = ''; foreach ($headers as $name => $value) { $this->setHeader($name, $value); $out .= $name . '=' . $value . ';'; } return $out; } } function main(): void { echo (new H())->all(['a' => '1', 'b' => '2']); } main();", + ); + assert_eq!(out, "a=1;b=2;"); +} diff --git a/tests/codegen/exceptions.rs b/tests/codegen/exceptions.rs index 58e17e9f99..11bb421121 100644 --- a/tests/codegen/exceptions.rs +++ b/tests/codegen/exceptions.rs @@ -624,3 +624,15 @@ try { echo $c->foo(); echo 'no'; } catch (Error $e) { echo 'err'; } ); assert_eq!(out, "err"); } + +/// The builtin exception constructors accept PHP's third `$previous` parameter (positional or +/// the `previous:` named argument). It is not stored — `getPrevious()` remains synthesized as +/// null — but wrap-and-rethrow code compiles and the message/code round-trip is byte-identical +/// to PHP 8.5. +#[test] +fn test_exception_constructor_accepts_previous() { + let out = compile_and_run( + "getMessage(), $e->getCode(), previous: $e); } } catch (\\InvalidArgumentException $x) { return $x->getMessage() . '/' . $x->getCode(); } } echo f();", + ); + assert_eq!(out, "outer: inner/0"); +} diff --git a/tests/codegen/namespaces.rs b/tests/codegen/namespaces.rs index 5b5e8a7e93..a9a041d48e 100644 --- a/tests/codegen/namespaces.rs +++ b/tests/codegen/namespaces.rs @@ -480,3 +480,22 @@ echo \App\C\K::mk()->t->p; ); assert_eq!(out, "/"); } + +/// `use const PHP_INT_MAX;` — the lexer eagerly tokenizes such constants, so the +/// use-declaration parser must accept the dedicated tokens as import names (the import itself +/// is inert; expression uses lower to literals). +#[test] +fn test_use_const_of_lexer_tokenized_constant() { + let out = compile_and_run( + r#" 0 ? 'max' : '?', ':', PHP_INT_MIN < 0 ? 'min' : '?'; +"#, + ); + assert_eq!(out, "max:min"); +} diff --git a/tests/codegen/oop/interfaces.rs b/tests/codegen/oop/interfaces.rs index dbdf52168c..0a0e2975d9 100644 --- a/tests/codegen/oop/interfaces.rs +++ b/tests/codegen/oop/interfaces.rs @@ -282,3 +282,15 @@ echo implode(',', C::previews()); ); assert_eq!(out, "a,b"); } + +/// An implementation may return a NARROWER type than the interface declares — the PSR-7 shape +/// `withX(): static` (resolving to the class) against an interface-typed return. The class +/// under validation is mid-construction when conformance runs, so the covariance is proven +/// from the conformance context itself. Byte-parity vs PHP 8.5. +#[test] +fn test_interface_covariant_self_return() { + let out = compile_and_run( + "w() instanceof C ? 'ok' : 'no';", + ); + assert_eq!(out, "ok"); +} diff --git a/tests/codegen/oop/misc.rs b/tests/codegen/oop/misc.rs index 32b46055ca..0c9ac5b0d2 100644 --- a/tests/codegen/oop/misc.rs +++ b/tests/codegen/oop/misc.rs @@ -102,3 +102,14 @@ fn test_example_v017_trio_compiles_and_runs() { let out = compile_and_run(include_str!("../../../examples/v017-trio/main.php")); assert_eq!(out, "health:[ok]:missing"); } + +/// EC-10 (#493): `enum` is only a soft keyword — `class Enum {}` is legal PHP (vendor +/// precedent: marc-mabe/php-enum). The always-tokenizing lexer must not block the class +/// declaration. Byte-parity vs PHP 8.5. +#[test] +fn test_class_named_enum_declares() { + let out = compile_and_run( + "tag();", + ); + assert_eq!(out, "e"); +} From 4f666151fbc219d094780505c7f1731f0fe80483 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 19 Jul 2026 18:05:32 +0200 Subject: [PATCH 2/6] fix: complete previous, covariance, enum soft-keyword, and use-const Store Exception/Error $previous so getPrevious() round-trips, route ?Throwable methods through compact intrinsics, finish covariant self returns at wither/override sites, and accept enum as a bareword type name plus use-const aliases for lexer-tokenized globals. --- CHANGELOG.md | 1 + docs/php/control-structures.md | 2 +- docs/php/system-and-io.md | 4 +- src/codegen/lower_inst.rs | 136 ++++++++++++++++-- src/codegen/lower_inst/builtins/math.rs | 6 +- .../lower_inst/builtins/math/binary.rs | 6 +- src/codegen/lower_inst/enums.rs | 18 ++- src/codegen/lower_inst/exceptions.rs | 12 +- src/codegen/lower_inst/objects.rs | 100 +++++++++++-- src/codegen/lower_inst/static_properties.rs | 6 +- src/codegen_support/hash_crypto.rs | 6 +- src/codegen_support/prescan.rs | 83 +++++++++++ .../runtime/arrays/value_error.rs | 6 +- .../runtime/spl/doubly_linked_list.rs | 6 +- .../runtime/spl/fixed_array.rs | 6 +- .../runtime/system/json_throw_error.rs | 6 +- src/ir_lower/expr/mod.rs | 20 +++ src/name_resolver/names.rs | 60 +++++--- src/parser/stmt/mod.rs | 31 ++-- src/parser/stmt/namespace_use.rs | 59 ++++---- src/parser/stmt/oop/body.rs | 20 ++- src/parser/stmt/oop/declarations.rs | 11 +- .../checker/builtin_types/declarations.rs | 5 +- src/types/checker/builtin_types/exception.rs | 45 +++++- src/types/checker/driver/init.rs | 18 +++ .../checker/inference/objects/methods.rs | 12 ++ src/types/checker/schema/classes/methods.rs | 4 +- src/types/checker/schema/validation.rs | 48 ++++++- src/types/checker/type_compat/unions.rs | 2 +- tests/codegen/exceptions.rs | 19 ++- tests/codegen/namespaces.rs | 19 ++- tests/codegen/oop/interfaces.rs | 40 ++++++ tests/codegen/oop/misc.rs | 14 +- tests/error_tests/type_system.rs | 9 ++ 34 files changed, 702 insertions(+), 138 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 450820a019..b20f71881b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 method returns may narrow `self` covariantly (including PSR-7-style wither call sites and parent `static` overrides); soft-keyword `enum` is accepted as a class/interface/trait name (`class Enum`, `implements Enum`); `use const` imports lexer-tokenized globals and aliases; and `foreach` over unknown `array` values widens keys to `int|string` 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). diff --git a/docs/php/control-structures.md b/docs/php/control-structures.md index 19815ec934..3c526820d5 100644 --- a/docs/php/control-structures.md +++ b/docs/php/control-structures.md @@ -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 ;` where `` has an object type implementing `Throwable` - `throw ` can also be used inside expressions such as `??` and ternaries diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 5c8744ce97..4241a049bf 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -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. | diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 4dff94df3c..780a896b2b 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -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); @@ -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 @@ -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 {}", @@ -3913,18 +3966,83 @@ fn lower_throwable_empty_trace_array(ctx: &mut FunctionContext<'_>) -> Result, + object_reg: &str, inst: &Instruction, ) -> Result { - 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. diff --git a/src/codegen/lower_inst/builtins/math.rs b/src/codegen/lower_inst/builtins/math.rs index 2e6c4f8d9a..8bef09e013 100644 --- a/src/codegen/lower_inst/builtins/math.rs +++ b/src/codegen/lower_inst/builtins/math.rs @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/src/codegen/lower_inst/builtins/math/binary.rs b/src/codegen/lower_inst/builtins/math/binary.rs index 88215fb384..f72df8469a 100644 --- a/src/codegen/lower_inst/builtins/math/binary.rs +++ b/src/codegen/lower_inst/builtins/math/binary.rs @@ -216,7 +216,7 @@ fn emit_intdiv_overflow_throw(ctx: &mut FunctionContext<'_>) { .add_string(b"Division of PHP_INT_MIN by -1 is not an integer"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #32"); // request Throwable payload storage for the ArithmeticError + ctx.emitter.instruction("mov x0, #56"); // request Throwable payload storage (message/code/previous) for the ArithmeticError ctx.emitter.instruction("bl __rt_heap_alloc"); // allocate the ArithmeticError 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 @@ -229,6 +229,7 @@ fn emit_intdiv_overflow_throw(ctx: &mut FunctionContext<'_>) { ctx.emitter.instruction(&format!("mov x9, #{}", msg_len)); // materialize the static ArithmeticError 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 ArithmeticError object ctx.emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder @@ -237,7 +238,7 @@ fn emit_intdiv_overflow_throw(ctx: &mut FunctionContext<'_>) { 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 ArithmeticError + ctx.emitter.instruction("mov rax, 56"); // request Throwable payload storage (message/code/previous) for the ArithmeticError ctx.emitter.instruction("call __rt_heap_alloc"); // allocate the ArithmeticError object payload ctx.emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 6)); // 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 @@ -247,6 +248,7 @@ fn emit_intdiv_overflow_throw(ctx: &mut FunctionContext<'_>) { ctx.emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the static ArithmeticError message pointer ctx.emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", msg_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 ArithmeticError object ctx.emitter.instruction("mov rsp, rbp"); // release the helper frame before throwing ctx.emitter.instruction("pop rbp"); // restore caller frame pointer before throwing diff --git a/src/codegen/lower_inst/enums.rs b/src/codegen/lower_inst/enums.rs index 1ad4740788..a496cc6ad4 100644 --- a/src/codegen/lower_inst/enums.rs +++ b/src/codegen/lower_inst/enums.rs @@ -417,7 +417,7 @@ fn emit_throw_value_error_from_string_result(ctx: &mut FunctionContext<'_>) { /// Emits the AArch64 `ValueError` allocation and unwinder handoff. fn emit_throw_value_error_from_string_result_aarch64(ctx: &mut FunctionContext<'_>) { - abi::emit_load_int_immediate(ctx.emitter, "x0", 32); + abi::emit_load_int_immediate(ctx.emitter, "x0", 56); // compact Throwable: message/code/previous abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov x9, #6"); // heap kind 6 = throwable object instance ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object @@ -428,6 +428,7 @@ fn emit_throw_value_error_from_string_result_aarch64(ctx: &mut FunctionContext<' abi::emit_load_temporary_stack_slot(ctx.emitter, "x9", 8); ctx.emitter.instruction("str x9, [x0, #16]"); // store dynamic exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "x0", "_exc_value", 0); abi::emit_release_temporary_stack(ctx.emitter, 16); abi::emit_jump(ctx.emitter, "__rt_throw_current"); @@ -435,7 +436,7 @@ fn emit_throw_value_error_from_string_result_aarch64(ctx: &mut FunctionContext<' /// Emits the x86_64 `ValueError` allocation and unwinder handoff. fn emit_throw_value_error_from_string_result_x86_64(ctx: &mut FunctionContext<'_>) { - abi::emit_load_int_immediate(ctx.emitter, "rax", 32); + abi::emit_load_int_immediate(ctx.emitter, "rax", 56); // compact Throwable: message/code/previous abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object @@ -446,6 +447,7 @@ fn emit_throw_value_error_from_string_result_x86_64(ctx: &mut FunctionContext<'_ abi::emit_load_temporary_stack_slot(ctx.emitter, "r10", 8); ctx.emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store dynamic exception message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); abi::emit_release_temporary_stack(ctx.emitter, 16); abi::emit_jump(ctx.emitter, "__rt_throw_current"); @@ -663,7 +665,7 @@ fn emit_throw_type_error_from_string_result(ctx: &mut FunctionContext<'_>) { abi::emit_push_reg_pair(ctx.emitter, message_ptr_reg, message_len_reg); match ctx.emitter.target.arch { Arch::AArch64 => { - abi::emit_load_int_immediate(ctx.emitter, "x0", 32); + abi::emit_load_int_immediate(ctx.emitter, "x0", 56); // compact Throwable: message/code/previous abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov x9, #6"); // heap kind 6 = throwable object instance ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object @@ -674,12 +676,13 @@ fn emit_throw_type_error_from_string_result(ctx: &mut FunctionContext<'_>) { abi::emit_load_temporary_stack_slot(ctx.emitter, "x9", 8); ctx.emitter.instruction("str x9, [x0, #16]"); // store dynamic exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "x0", "_exc_value", 0); abi::emit_release_temporary_stack(ctx.emitter, 16); abi::emit_jump(ctx.emitter, "__rt_throw_current"); } Arch::X86_64 => { - abi::emit_load_int_immediate(ctx.emitter, "rax", 32); + abi::emit_load_int_immediate(ctx.emitter, "rax", 56); // compact Throwable: message/code/previous abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HELP magic + kind 6 object ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object @@ -690,6 +693,7 @@ fn emit_throw_type_error_from_string_result(ctx: &mut FunctionContext<'_>) { abi::emit_load_temporary_stack_slot(ctx.emitter, "r10", 8); ctx.emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store dynamic exception message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); abi::emit_release_temporary_stack(ctx.emitter, 16); abi::emit_jump(ctx.emitter, "__rt_throw_current"); @@ -703,7 +707,7 @@ fn emit_throw_enum_from_type_error_aarch64( message_label: &str, message_len: usize, ) { - abi::emit_load_int_immediate(ctx.emitter, "x0", 32); + abi::emit_load_int_immediate(ctx.emitter, "x0", 56); // compact Throwable: message/code/previous abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov x9, #6"); // heap kind 6 = throwable object instance ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object @@ -714,6 +718,7 @@ fn emit_throw_enum_from_type_error_aarch64( abi::emit_load_int_immediate(ctx.emitter, "x9", message_len as i64); ctx.emitter.instruction("str x9, [x0, #16]"); // store static exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "x0", "_exc_value", 0); abi::emit_jump(ctx.emitter, "__rt_throw_current"); } @@ -724,7 +729,7 @@ fn emit_throw_enum_from_type_error_x86_64( message_label: &str, message_len: usize, ) { - abi::emit_load_int_immediate(ctx.emitter, "rax", 32); + abi::emit_load_int_immediate(ctx.emitter, "rax", 56); // compact Throwable: message/code/previous abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HELP magic + kind 6 object ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object @@ -735,6 +740,7 @@ fn emit_throw_enum_from_type_error_x86_64( abi::emit_load_int_immediate(ctx.emitter, "r10", message_len as i64); ctx.emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store static exception message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); abi::emit_jump(ctx.emitter, "__rt_throw_current"); } diff --git a/src/codegen/lower_inst/exceptions.rs b/src/codegen/lower_inst/exceptions.rs index 2f6846e968..ba02778bd2 100644 --- a/src/codegen/lower_inst/exceptions.rs +++ b/src/codegen/lower_inst/exceptions.rs @@ -50,7 +50,7 @@ fn emit_static_exception( let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - abi::emit_load_int_immediate(ctx.emitter, "x0", 32); + abi::emit_load_int_immediate(ctx.emitter, "x0", 56); // compact Throwable: message/code/previous abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov x9, #6"); // heap kind 6 = throwable object instance ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a runtime object @@ -61,11 +61,12 @@ fn emit_static_exception( abi::emit_load_int_immediate(ctx.emitter, "x9", message_len as i64); ctx.emitter.instruction("str x9, [x0, #16]"); // store the exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "x0", "_exc_value", 0); abi::emit_jump(ctx.emitter, "__rt_throw_current"); } Arch::X86_64 => { - abi::emit_load_int_immediate(ctx.emitter, "rax", 32); + abi::emit_load_int_immediate(ctx.emitter, "rax", 56); // compact Throwable: message/code/previous abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap kind 6 with the runtime magic marker ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as a runtime object @@ -76,6 +77,7 @@ fn emit_static_exception( abi::emit_load_int_immediate(ctx.emitter, "r10", message_len as i64); ctx.emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store the exception message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); abi::emit_jump(ctx.emitter, "__rt_throw_current"); } @@ -166,7 +168,7 @@ fn emit_uncaught_dynamic_error_fatal_if_no_handler(ctx: &mut FunctionContext<'_> fn emit_dynamic_error_object(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - abi::emit_load_int_immediate(ctx.emitter, "x0", 32); + abi::emit_load_int_immediate(ctx.emitter, "x0", 56); // compact Throwable: message/code/previous abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov x9, #6"); // heap kind 6 = throwable object instance ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a runtime object @@ -177,12 +179,13 @@ fn emit_dynamic_error_object(ctx: &mut FunctionContext<'_>) { abi::emit_load_temporary_stack_slot(ctx.emitter, "x9", 8); ctx.emitter.instruction("str x9, [x0, #16]"); // store the runtime exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_release_temporary_stack(ctx.emitter, 16); abi::emit_store_reg_to_symbol(ctx.emitter, "x0", "_exc_value", 0); abi::emit_jump(ctx.emitter, "__rt_throw_current"); } Arch::X86_64 => { - abi::emit_load_int_immediate(ctx.emitter, "rax", 32); + abi::emit_load_int_immediate(ctx.emitter, "rax", 56); // compact Throwable: message/code/previous abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap kind 6 with the runtime magic marker ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as a runtime object @@ -193,6 +196,7 @@ fn emit_dynamic_error_object(ctx: &mut FunctionContext<'_>) { abi::emit_load_temporary_stack_slot(ctx.emitter, "r10", 8); ctx.emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store the runtime exception message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_release_temporary_stack(ctx.emitter, 16); abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); abi::emit_jump(ctx.emitter, "__rt_throw_current"); diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 291c4a7b96..6ea707eafb 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -799,7 +799,7 @@ fn emit_validate_iterator_iterator_aggregate_downcast(ctx: &mut FunctionContext< fn emit_throw_iterator_iterator_downcast_logic_exception(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #32"); // request Throwable payload storage + ctx.emitter.instruction("mov x0, #56"); // request Throwable payload storage (message/code/previous) abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks object instances ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object @@ -814,6 +814,7 @@ fn emit_throw_iterator_iterator_downcast_logic_exception(ctx: &mut FunctionConte )); // load static exception message length ctx.emitter.instruction("str x9, [x0, #16]"); // store static exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + 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 exception object ctx.emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder @@ -822,7 +823,7 @@ fn emit_throw_iterator_iterator_downcast_logic_exception(ctx: &mut FunctionConte ctx.emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation ctx.emitter.instruction("mov rbp, rsp"); // establish an aligned helper frame ctx.emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call aligned - ctx.emitter.instruction("mov rax, 32"); // request Throwable payload storage + ctx.emitter.instruction("mov rax, 56"); // request Throwable payload storage (message/code/previous) abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // materialize the x86_64 object heap kind word ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object @@ -837,6 +838,7 @@ fn emit_throw_iterator_iterator_downcast_logic_exception(ctx: &mut FunctionConte ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len() )); // store static exception message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + 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 exception object ctx.emitter.instruction("mov rsp, rbp"); // release helper frame before throwing @@ -923,9 +925,7 @@ fn lower_builtin_throwable_new( preserve_throwable_for_init(ctx); emit_throwable_message_fields(ctx, inst.operands.first().copied())?; emit_throwable_code_field(ctx, inst.operands.get(1).copied())?; - // Operand 2 is PHP's `$previous`: its expression was evaluated (side effects preserved), - // but the compact throwable payload has no previous slot — `getPrevious()` is synthesized - // as null — so the value is intentionally not stored. + emit_throwable_previous_field(ctx, inst.operands.get(2).copied())?; restore_throwable_after_init(ctx); store_if_result(ctx, inst) } @@ -998,19 +998,29 @@ fn class_declares_own_constructor(class_name: &str, class_info: &ClassInfo) -> b .is_some_and(|declaring_class| declaring_class == class_name) } -/// Allocates a 32-byte Throwable payload and stamps its heap kind and class id. +/// Compact Throwable payload bytes: class_id + message(16) + code(16) + previous(16). +const THROWABLE_COMPACT_PAYLOAD_SIZE: u64 = 56; + +/// Allocates a compact Throwable payload and stamps its heap kind and class id. fn emit_throwable_allocation(ctx: &mut FunctionContext<'_>, class_id: u64) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #32"); // request compact Throwable payload storage + ctx.emitter.instruction(&format!( + "mov x0, #{}", + THROWABLE_COMPACT_PAYLOAD_SIZE + )); // request compact Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks runtime object payloads ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the Throwable payload ctx.emitter.instruction(&format!("mov x9, #{}", class_id)); // materialize the Throwable runtime class id ctx.emitter.instruction("str x9, [x0]"); // store class id at payload offset zero + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null until constructor init } Arch::X86_64 => { - ctx.emitter.instruction("mov rax, 32"); // request compact Throwable payload storage + ctx.emitter.instruction(&format!( + "mov rax, {}", + THROWABLE_COMPACT_PAYLOAD_SIZE + )); // request compact Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction(&format!( "mov r10, 0x{:x}", @@ -1019,6 +1029,7 @@ fn emit_throwable_allocation(ctx: &mut FunctionContext<'_>, class_id: u64) { ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the Throwable payload ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the Throwable runtime class id ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at payload offset zero + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null until constructor init } } } @@ -1123,6 +1134,73 @@ fn emit_throwable_code_field_x86_64( Ok(()) } +/// Writes PHP's `$previous` object pointer into the compact Throwable payload at offset 40. +fn emit_throwable_previous_field( + ctx: &mut FunctionContext<'_>, + previous: Option, +) -> Result<()> { + match ctx.emitter.target.arch { + Arch::AArch64 => emit_throwable_previous_field_aarch64(ctx, previous), + Arch::X86_64 => emit_throwable_previous_field_x86_64(ctx, previous), + } +} + +/// Writes the AArch64 Throwable previous field, retaining a non-null previous object. +fn emit_throwable_previous_field_aarch64( + ctx: &mut FunctionContext<'_>, + previous: Option, +) -> Result<()> { + if let Some(previous) = previous { + let store_label = ctx.next_label("throwable_previous_store"); + let null_label = ctx.next_label("throwable_previous_null"); + ctx.load_value_to_reg(previous, "x0")?; + ctx.emitter.instruction(&format!("cbz x0, {}", null_label)); // missing previous → store null + abi::emit_load_int_immediate(ctx.emitter, "x9", RUNTIME_NULL_SENTINEL); + ctx.emitter.instruction("cmp x0, x9"); // is previous the in-band null sentinel? + ctx.emitter + .instruction(&format!("b.eq {}", null_label)); // treat sentinel as null payload + abi::emit_call_label(ctx.emitter, "__rt_incref"); // retain previous for the Throwable payload + ctx.emitter.instruction(&format!("b {}", store_label)); // keep the retained previous pointer + ctx.emitter.label(&null_label); + ctx.emitter.instruction("mov x0, xzr"); // compact payload stores raw null, not the in-band sentinel + ctx.emitter.label(&store_label); + ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for previous initialization + ctx.emitter.instruction("str x0, [x9, #40]"); // store Throwable previous pointer + } else { + ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for previous initialization + ctx.emitter.instruction("str xzr, [x9, #40]"); // previous defaults to null + } + Ok(()) +} + +/// Writes the x86_64 Throwable previous field, retaining a non-null previous object. +fn emit_throwable_previous_field_x86_64( + ctx: &mut FunctionContext<'_>, + previous: Option, +) -> Result<()> { + if let Some(previous) = previous { + let store_label = ctx.next_label("throwable_previous_store"); + let null_label = ctx.next_label("throwable_previous_null"); + ctx.load_value_to_reg(previous, "rax")?; + ctx.emitter.instruction("test rax, rax"); // missing previous → store null + ctx.emitter.instruction(&format!("jz {}", null_label)); + abi::emit_load_int_immediate(ctx.emitter, "r10", RUNTIME_NULL_SENTINEL); + ctx.emitter.instruction("cmp rax, r10"); // is previous the in-band null sentinel? + ctx.emitter.instruction(&format!("je {}", null_label)); // treat sentinel as null payload + abi::emit_call_label(ctx.emitter, "__rt_incref"); // retain previous for the Throwable payload + ctx.emitter.instruction(&format!("jmp {}", store_label)); // keep the retained previous pointer + ctx.emitter.label(&null_label); + ctx.emitter.instruction("xor rax, rax"); // compact payload stores raw null, not the in-band sentinel + ctx.emitter.label(&store_label); + ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for previous initialization + ctx.emitter.instruction("mov QWORD PTR [r11 + 40], rax"); // store Throwable previous pointer + } else { + ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for previous initialization + ctx.emitter.instruction("mov QWORD PTR [r11 + 40], 0"); // previous defaults to null + } + Ok(()) +} + /// Lowers `new Fiber($callable)` through the runtime-managed Fiber constructor. fn lower_fiber_new(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let class_id = ctx @@ -6015,7 +6093,7 @@ fn emit_uninitialized_typed_property_fatal( let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #32"); // request Throwable payload storage + ctx.emitter.instruction("mov x0, #56"); // request Throwable payload storage (message/code/previous) ctx.emitter.instruction("bl __rt_heap_alloc"); // allocate the Error object payload ctx.emitter.instruction("mov x9, #6"); // heap kind 6 = object instance ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object @@ -6027,6 +6105,7 @@ fn emit_uninitialized_typed_property_fatal( ctx.emitter.instruction(&format!("mov x9, #{}", message_len)); // load Error message length ctx.emitter.instruction("str x9, [x0, #16]"); // store exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_symbol_address(ctx.emitter, "x9", "_exc_value"); // materialize the active exception cell ctx.emitter.instruction("str x0, [x9]"); // publish the active exception object ctx.emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder @@ -6035,7 +6114,7 @@ fn emit_uninitialized_typed_property_fatal( ctx.emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation ctx.emitter.instruction("mov rbp, rsp"); // establish aligned helper frame 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 + ctx.emitter.instruction("mov rax, 56"); // request Throwable payload storage (message/code/previous) ctx.emitter.instruction("call __rt_heap_alloc"); // allocate the Error object payload ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object @@ -6045,6 +6124,7 @@ fn emit_uninitialized_typed_property_fatal( ctx.emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static Error message pointer ctx.emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", message_len)); // store Error message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); // publish the active exception object ctx.emitter.instruction("mov rsp, rbp"); // release helper frame before throwing ctx.emitter.instruction("pop rbp"); // restore caller frame pointer before throwing diff --git a/src/codegen/lower_inst/static_properties.rs b/src/codegen/lower_inst/static_properties.rs index 7dc0ab36e1..31a6c138b6 100644 --- a/src/codegen/lower_inst/static_properties.rs +++ b/src/codegen/lower_inst/static_properties.rs @@ -938,7 +938,7 @@ fn emit_uninitialized_static_property_fatal( let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #32"); // request Throwable payload storage + ctx.emitter.instruction("mov x0, #56"); // request Throwable payload storage (message/code/previous) ctx.emitter.instruction("bl __rt_heap_alloc"); // allocate the Error object payload ctx.emitter.instruction("mov x9, #6"); // heap kind 6 = object instance ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object @@ -950,6 +950,7 @@ fn emit_uninitialized_static_property_fatal( ctx.emitter.instruction(&format!("mov x9, #{}", message_len)); // load Error message length ctx.emitter.instruction("str x9, [x0, #16]"); // store exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_symbol_address(ctx.emitter, "x9", "_exc_value"); // materialize the active exception cell ctx.emitter.instruction("str x0, [x9]"); // publish the active exception object ctx.emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder @@ -958,7 +959,7 @@ fn emit_uninitialized_static_property_fatal( ctx.emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation ctx.emitter.instruction("mov rbp, rsp"); // establish aligned helper frame 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 + ctx.emitter.instruction("mov rax, 56"); // request Throwable payload storage (message/code/previous) ctx.emitter.instruction("call __rt_heap_alloc"); // allocate the Error object payload ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object @@ -968,6 +969,7 @@ fn emit_uninitialized_static_property_fatal( ctx.emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static Error message pointer ctx.emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", message_len)); // store Error message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); // publish the active exception object ctx.emitter.instruction("mov rsp, rbp"); // release helper frame before throwing ctx.emitter.instruction("pop rbp"); // restore caller frame pointer before throwing diff --git a/src/codegen_support/hash_crypto.rs b/src/codegen_support/hash_crypto.rs index 4f6a604ada..61a671320f 100644 --- a/src/codegen_support/hash_crypto.rs +++ b/src/codegen_support/hash_crypto.rs @@ -88,7 +88,7 @@ pub(crate) fn emit_throw_unknown_algorithm_value_error( /// Emits the AArch64 allocation and unwinder handoff for the `hash()` `\ValueError`. fn emit_throw_value_error_aarch64(emitter: &mut Emitter, message_symbol: &str, message_len: usize) { - emitter.instruction("mov x0, #32"); // request Throwable payload storage + emitter.instruction("mov x0, #56"); // request Throwable payload storage (message/code/previous) emitter.instruction("bl __rt_heap_alloc"); // allocate the ValueError object payload emitter.instruction("mov x9, #6"); // heap kind 6 = object instance emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object @@ -100,6 +100,7 @@ fn emit_throw_value_error_aarch64(emitter: &mut Emitter, message_symbol: &str, m emitter.instruction(&format!("mov x9, #{}", message_len)); // load static ValueError message length emitter.instruction("str x9, [x0, #16]"); // store exception message length emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_symbol_address(emitter, "x9", "_exc_value"); emitter.instruction("str x0, [x9]"); // publish the active exception object emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder @@ -110,7 +111,7 @@ fn emit_throw_value_error_x86_64(emitter: &mut Emitter, message_symbol: &str, me emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation emitter.instruction("mov rbp, rsp"); // establish aligned helper frame emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned - emitter.instruction("mov rax, 32"); // request Throwable payload storage + emitter.instruction("mov rax, 56"); // request Throwable payload storage (message/code/previous) emitter.instruction("call __rt_heap_alloc"); // allocate the ValueError object payload emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object @@ -120,6 +121,7 @@ fn emit_throw_value_error_x86_64(emitter: &mut Emitter, message_symbol: &str, me emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static ValueError message pointer emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", message_len)); // store static ValueError message length emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(emitter, "rax", "_exc_value", 0); // publish the active exception object emitter.instruction("mov rsp, rbp"); // release helper frame before throwing emitter.instruction("pop rbp"); // restore caller frame pointer before throwing diff --git a/src/codegen_support/prescan.rs b/src/codegen_support/prescan.rs index 03c93d2060..327a40ddea 100644 --- a/src/codegen_support/prescan.rs +++ b/src/codegen_support/prescan.rs @@ -162,6 +162,89 @@ pub(crate) fn collect_constants( (ExprKind::IntLiteral(*value), PhpType::Int), ); } + // Lexer-tokenized numeric / math constants (also reachable via `use const` aliases). + constants.insert( + "PHP_INT_MAX".to_string(), + (ExprKind::IntLiteral(i64::MAX), PhpType::Int), + ); + constants.insert( + "PHP_INT_MIN".to_string(), + (ExprKind::IntLiteral(i64::MIN), PhpType::Int), + ); + constants.insert( + "PHP_FLOAT_MAX".to_string(), + (ExprKind::FloatLiteral(f64::MAX), PhpType::Float), + ); + constants.insert( + "PHP_FLOAT_MIN".to_string(), + (ExprKind::FloatLiteral(f64::MIN_POSITIVE), PhpType::Float), + ); + constants.insert( + "PHP_FLOAT_EPSILON".to_string(), + (ExprKind::FloatLiteral(f64::EPSILON), PhpType::Float), + ); + constants.insert( + "INF".to_string(), + (ExprKind::FloatLiteral(f64::INFINITY), PhpType::Float), + ); + constants.insert( + "NAN".to_string(), + (ExprKind::FloatLiteral(f64::NAN), PhpType::Float), + ); + constants.insert( + "M_PI".to_string(), + (ExprKind::FloatLiteral(std::f64::consts::PI), PhpType::Float), + ); + constants.insert( + "M_E".to_string(), + (ExprKind::FloatLiteral(std::f64::consts::E), PhpType::Float), + ); + constants.insert( + "M_SQRT2".to_string(), + ( + ExprKind::FloatLiteral(std::f64::consts::SQRT_2), + PhpType::Float, + ), + ); + constants.insert( + "M_PI_2".to_string(), + ( + ExprKind::FloatLiteral(std::f64::consts::FRAC_PI_2), + PhpType::Float, + ), + ); + constants.insert( + "M_PI_4".to_string(), + ( + ExprKind::FloatLiteral(std::f64::consts::FRAC_PI_4), + PhpType::Float, + ), + ); + constants.insert( + "M_LOG2E".to_string(), + ( + ExprKind::FloatLiteral(std::f64::consts::LOG2_E), + PhpType::Float, + ), + ); + constants.insert( + "M_LOG10E".to_string(), + ( + ExprKind::FloatLiteral(std::f64::consts::LOG10_E), + PhpType::Float, + ), + ); + constants.insert( + "PHP_EOL".to_string(), + (ExprKind::StringLiteral("\n".to_string()), PhpType::Str), + ); + constants.insert( + "DIRECTORY_SEPARATOR".to_string(), + ( + ExprKind::StringLiteral(std::path::MAIN_SEPARATOR.to_string()), + PhpType::Str, + ), + ); collect_constant_decls(program, &mut constants); constants } diff --git a/src/codegen_support/runtime/arrays/value_error.rs b/src/codegen_support/runtime/arrays/value_error.rs index a337734532..d41d4f5665 100644 --- a/src/codegen_support/runtime/arrays/value_error.rs +++ b/src/codegen_support/runtime/arrays/value_error.rs @@ -23,7 +23,7 @@ pub(in crate::codegen_support::runtime) fn emit_throw_value_error_aarch64( message_symbol: &str, message_len: usize, ) { - emitter.instruction("mov x0, #32"); // request Throwable payload storage + emitter.instruction("mov x0, #56"); // request Throwable payload storage (message/code/previous) emitter.instruction("bl __rt_heap_alloc"); // allocate the ValueError object payload emitter.instruction("mov x9, #6"); // heap kind 6 = object instance emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object @@ -35,6 +35,7 @@ pub(in crate::codegen_support::runtime) fn emit_throw_value_error_aarch64( emitter.instruction(&format!("mov x9, #{}", message_len)); // load static ValueError message length emitter.instruction("str x9, [x0, #16]"); // store exception message length emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_symbol_address(emitter, "x9", "_exc_value"); emitter.instruction("str x0, [x9]"); // publish the active exception object emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder @@ -52,7 +53,7 @@ pub(in crate::codegen_support::runtime) fn emit_throw_value_error_x86_64( emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation emitter.instruction("mov rbp, rsp"); // establish aligned helper frame emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned - emitter.instruction("mov rax, 32"); // request Throwable payload storage + emitter.instruction("mov rax, 56"); // request Throwable payload storage (message/code/previous) emitter.instruction("call __rt_heap_alloc"); // allocate the ValueError object payload emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object @@ -62,6 +63,7 @@ pub(in crate::codegen_support::runtime) fn emit_throw_value_error_x86_64( emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static ValueError message pointer emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", message_len)); // store static ValueError message length emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(emitter, "rax", "_exc_value", 0); // publish the active exception object emitter.instruction("mov rsp, rbp"); // release helper frame before throwing emitter.instruction("pop rbp"); // restore caller frame pointer before throwing diff --git a/src/codegen_support/runtime/spl/doubly_linked_list.rs b/src/codegen_support/runtime/spl/doubly_linked_list.rs index 56ef9777d8..cb42622876 100644 --- a/src/codegen_support/runtime/spl/doubly_linked_list.rs +++ b/src/codegen_support/runtime/spl/doubly_linked_list.rs @@ -2492,7 +2492,7 @@ fn emit_throw_exception_aarch64( message_symbol: &str, message_len: usize, ) { - emitter.instruction("mov x0, #32"); // request Throwable payload storage + emitter.instruction("mov x0, #56"); // request Throwable payload storage (message/code/previous) emitter.instruction("bl __rt_heap_alloc"); // allocate the exception object payload emitter.instruction("mov x9, #6"); // heap kind 6 = object instance emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object @@ -2504,6 +2504,7 @@ fn emit_throw_exception_aarch64( emitter.instruction(&format!("mov x9, #{}", message_len)); // load static exception message length emitter.instruction("str x9, [x0, #16]"); // store exception message length emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_symbol_address(emitter, "x9", "_exc_value"); emitter.instruction("str x0, [x9]"); // publish the active exception object emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder @@ -2523,7 +2524,7 @@ fn emit_throw_exception_x86_64( emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation emitter.instruction("mov rbp, rsp"); // establish aligned helper frame emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned - emitter.instruction("mov rax, 32"); // request Throwable payload storage + emitter.instruction("mov rax, 56"); // request Throwable payload storage (message/code/previous) emitter.instruction("call __rt_heap_alloc"); // allocate the exception object payload emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object @@ -2533,6 +2534,7 @@ fn emit_throw_exception_x86_64( emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static exception message pointer emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", message_len)); // store static exception message length emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(emitter, "rax", "_exc_value", 0); // publish the active exception object emitter.instruction("mov rsp, rbp"); // release helper frame before throwing emitter.instruction("pop rbp"); // restore caller frame pointer before throwing diff --git a/src/codegen_support/runtime/spl/fixed_array.rs b/src/codegen_support/runtime/spl/fixed_array.rs index c3db05f1ff..f59ebcb3e5 100644 --- a/src/codegen_support/runtime/spl/fixed_array.rs +++ b/src/codegen_support/runtime/spl/fixed_array.rs @@ -1414,7 +1414,7 @@ fn emit_throw_exception_aarch64( message_symbol: &str, message_len: usize, ) { - emitter.instruction("mov x0, #32"); // request Throwable payload storage + emitter.instruction("mov x0, #56"); // request Throwable payload storage (message/code/previous) emitter.instruction("bl __rt_heap_alloc"); // allocate the exception object payload emitter.instruction("mov x9, #6"); // heap kind 6 = object instance emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object @@ -1426,6 +1426,7 @@ fn emit_throw_exception_aarch64( emitter.instruction(&format!("mov x9, #{}", message_len)); // load static exception message length emitter.instruction("str x9, [x0, #16]"); // store exception message length emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_symbol_address(emitter, "x9", "_exc_value"); emitter.instruction("str x0, [x9]"); // publish the active exception object emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder @@ -1445,7 +1446,7 @@ fn emit_throw_exception_x86_64( emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation emitter.instruction("mov rbp, rsp"); // establish aligned helper frame emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned - emitter.instruction("mov rax, 32"); // request Throwable payload storage + emitter.instruction("mov rax, 56"); // request Throwable payload storage (message/code/previous) emitter.instruction("call __rt_heap_alloc"); // allocate the exception object payload emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object @@ -1455,6 +1456,7 @@ fn emit_throw_exception_x86_64( emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static exception message pointer emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", message_len)); // store static exception message length emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(emitter, "rax", "_exc_value", 0); // publish the active exception object emitter.instruction("mov rsp, rbp"); // release helper frame before throwing emitter.instruction("pop rbp"); // restore caller frame pointer before throwing diff --git a/src/codegen_support/runtime/system/json_throw_error.rs b/src/codegen_support/runtime/system/json_throw_error.rs index 3cc87e97a5..417a871eab 100644 --- a/src/codegen_support/runtime/system/json_throw_error.rs +++ b/src/codegen_support/runtime/system/json_throw_error.rs @@ -60,7 +60,7 @@ pub(crate) fn emit_json_throw_error(emitter: &mut Emitter) { emitter.instruction("b.eq __rt_json_throw_error_return"); // bail out when throwing is not requested // Allocate a 32-byte JsonException payload (class_id + message ptr/len + code). - emitter.instruction("mov x0, #32"); // size = class_id (8) + message ptr/len (16) + code (8) + emitter.instruction("mov x0, #56"); // size = class_id + message + code + previous slots emitter.instruction("bl __rt_heap_alloc"); // allocate the JsonException payload emitter.instruction("mov x9, #6"); // heap kind 6 = object emitter.instruction("str x9, [x0, #-8]"); // tag the allocation as an object in the uniform header @@ -78,6 +78,7 @@ pub(crate) fn emit_json_throw_error(emitter: &mut Emitter) { emitter.instruction("str x2, [x0, #16]"); // obj.message_len emitter.instruction("ldr x10, [sp, #0]"); // reload the saved error code for $code field emitter.instruction("str x10, [x0, #24]"); // obj.code (matches Exception's `code` property layout) + emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null // Publish the new exception object via _exc_value and longjmp to the // active catch handler through the standard throw helper. @@ -159,7 +160,7 @@ fn emit_json_throw_error_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("test rdx, 0x400000"); // JSON_THROW_ON_ERROR = 0x400000 emitter.instruction("je __rt_json_throw_error_return_x"); // bail out when throwing is not requested - emitter.instruction("mov rax, 32"); // size = class_id (8) + message ptr/len (16) + code (8) + emitter.instruction("mov rax, 56"); // size = class_id + message + code + previous slots emitter.instruction("call __rt_heap_alloc"); // allocate the JsonException payload (rax = payload ptr) emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 (object) emitter.instruction("mov QWORD PTR [rax - 8], r10"); // tag the allocation as an object in the uniform header @@ -176,6 +177,7 @@ fn emit_json_throw_error_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rax + 16], r11"); // obj.message_len emitter.instruction("mov rcx, QWORD PTR [rbp - 8]"); // reload the saved error code for $code field emitter.instruction("mov QWORD PTR [rax + 24], rcx"); // obj.code (matches Exception's `code` property layout) + emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(emitter, "rax", "_exc_value", 0); // _exc_value = JsonException pointer emitter.instruction("mov rsp, rbp"); // unwind the helper scratch frame before tail-call diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 38b9c540f5..5485b4eed8 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -14051,6 +14051,26 @@ fn method_call_result_type( } return fallback_expr_type(expr); }; + // Fluent `with*` wither (@return static): a method declared to return a strict + // ancestor interface of the receiver interface actually returns the receiver. + // Must agree with `infer_method_call_on_interface_type`. + let fluent_receiver = if let (Some((recv_name, _)), PhpType::Object(return_name)) = + (singular_object_class(&object_ty), &return_ty) + { + if php_symbol_key(method).starts_with("with") + && ctx.interfaces.contains_key(recv_name.trim_start_matches('\\')) + && php_symbol_key(return_name.trim_start_matches('\\')) + != php_symbol_key(recv_name.trim_start_matches('\\')) + && interface_extends_interface_for_ir(ctx, recv_name, return_name) + { + Some(recv_name.to_string()) + } else { + None + } + } else { + None + }; + let return_ty = fluent_receiver.map(PhpType::Object).unwrap_or(return_ty); if op == Op::NullsafeMethodCall && nullable { nullable_result_type(return_ty) } else { diff --git a/src/name_resolver/names.rs b/src/name_resolver/names.rs index f4786929d4..b0d6edb930 100644 --- a/src/name_resolver/names.rs +++ b/src/name_resolver/names.rs @@ -348,28 +348,44 @@ pub(super) fn resolve_constant_name( /// Returns true if `name` is a builtin global constant that should bypass symbol-table /// resolution (e.g., PHP_OS, SID, STDIN, STDOUT, STDERR, FNM_* pathinfo flags). fn is_builtin_global_constant(name: &str) -> bool { - if matches!( - name, - "PHP_OS" - | "SID" - | "PATHINFO_DIRNAME" - | "PATHINFO_BASENAME" - | "PATHINFO_EXTENSION" - | "PATHINFO_FILENAME" - | "PATHINFO_ALL" - | "FNM_NOESCAPE" - | "FNM_PATHNAME" - | "FNM_PERIOD" - | "FNM_CASEFOLD" - | "ARRAY_FILTER_USE_VALUE" - | "ARRAY_FILTER_USE_BOTH" - | "ARRAY_FILTER_USE_KEY" - | "STDIN" - | "STDOUT" - | "STDERR" - ) { - return true; - } + if matches!( + name, + "PHP_OS" + | "SID" + | "PATHINFO_DIRNAME" + | "PATHINFO_BASENAME" + | "PATHINFO_EXTENSION" + | "PATHINFO_FILENAME" + | "PATHINFO_ALL" + | "FNM_NOESCAPE" + | "FNM_PATHNAME" + | "FNM_PERIOD" + | "FNM_CASEFOLD" + | "ARRAY_FILTER_USE_VALUE" + | "ARRAY_FILTER_USE_BOTH" + | "ARRAY_FILTER_USE_KEY" + | "STDIN" + | "STDOUT" + | "STDERR" + | "PHP_INT_MAX" + | "PHP_INT_MIN" + | "PHP_FLOAT_MAX" + | "PHP_FLOAT_MIN" + | "PHP_FLOAT_EPSILON" + | "INF" + | "NAN" + | "M_PI" + | "M_E" + | "M_SQRT2" + | "M_PI_2" + | "M_PI_4" + | "M_LOG2E" + | "M_LOG10E" + | "PHP_EOL" + | "DIRECTORY_SEPARATOR" + ) { + return true; + } // Shared source-of-truth slices for JSON, stream/socket, and session constants. crate::types::json_constants::JSON_INT_CONSTANTS .iter() diff --git a/src/parser/stmt/mod.rs b/src/parser/stmt/mod.rs index 08f03ea608..5d9f7dad45 100644 --- a/src/parser/stmt/mod.rs +++ b/src/parser/stmt/mod.rs @@ -471,14 +471,18 @@ pub(crate) fn expect_token( } } -/// Returns true if the token at `pos` is the start of a PHP name (identifier or backslash). +/// Returns true if the token at `pos` is the start of a PHP name (identifier, soft keyword, or backslash). /// -/// Used to distinguish name-based declarations from generic expression statements. +/// Soft keywords such as `enum` are valid class/interface/trait names in PHP, so `implements Enum` +/// and similar name lists must treat `Token::Enum` as a bareword name start. pub(crate) fn name_starts_at(tokens: &[SpannedToken], pos: usize) -> bool { - matches!( - tokens.get(pos).map(|(t, _)| t), - Some(Token::Identifier(_)) | Some(Token::Backslash) - ) + match tokens.get(pos) { + Some((Token::Identifier(_), _)) | Some((Token::Backslash, _)) => true, + Some((token, meta)) => { + crate::parser::keyword_name::bareword_name_from_token(token, meta).is_some() + } + None => false, + } } /// Parses a PHP qualified or unqualified name from the token stream. @@ -501,19 +505,24 @@ pub(crate) fn parse_name( let mut parts = Vec::new(); loop { - match tokens.get(*pos).map(|(t, _)| t) { - Some(Token::Identifier(name)) => { + match tokens.get(*pos) { + Some((Token::Identifier(name), _)) => { parts.push(name.clone()); *pos += 1; } // `enum` is only a soft keyword: `Enum` is a legal class name and name segment // (`new Enum`, `extends Enum`, `MabeEnum\Enum`). Statement-position `enum` // dispatches to the enum-declaration parser before parse_name is consulted. - Some(Token::Enum) => { - parts.push("Enum".to_string()); + Some((Token::Enum, meta)) => { + parts.push( + crate::parser::keyword_name::bareword_name_from_token(&Token::Enum, meta) + .unwrap_or_else(|| "enum".to_string()), + ); *pos += 1; } - _ if parts.is_empty() => return Err(CompileError::new(span, first_error)), + _ if parts.is_empty() => { + return Err(CompileError::new(span, first_error)) + } _ => { return Err(CompileError::new( span, diff --git a/src/parser/stmt/namespace_use.rs b/src/parser/stmt/namespace_use.rs index bebb36661e..79c889dbf3 100644 --- a/src/parser/stmt/namespace_use.rs +++ b/src/parser/stmt/namespace_use.rs @@ -257,30 +257,33 @@ fn parse_group_use_items( /// Canonical import spelling for constant-like tokens the lexer eagerly tokenizes. /// /// `use const PHP_INT_MAX;` is legal PHP, but `PHP_INT_MAX` never reaches the parser as an -/// identifier — the lexer emits a dedicated token (expressions lower these to literals -/// directly, so the import itself is inert). Accepting them here keeps such use -/// declarations parseable. -fn token_as_import_name(token: &Token) -> Option { - let name = match token { - Token::PhpIntMax => "PHP_INT_MAX", - Token::PhpIntMin => "PHP_INT_MIN", - Token::PhpFloatMax => "PHP_FLOAT_MAX", - Token::PhpFloatMin => "PHP_FLOAT_MIN", - Token::PhpFloatEpsilon => "PHP_FLOAT_EPSILON", - Token::Inf => "INF", - Token::Nan => "NAN", - Token::MPi => "M_PI", - Token::ME => "M_E", - Token::MSqrt2 => "M_SQRT2", - Token::MPi2 => "M_PI_2", - Token::MPi4 => "M_PI_4", - Token::MLog2e => "M_LOG2E", - Token::MLog10e => "M_LOG10E", - Token::Stdin => "STDIN", - Token::Stdout => "STDOUT", - _ => return None, - }; - Some(name.to_string()) +/// identifier — the lexer emits a dedicated token. Accepting them here keeps such use +/// declarations parseable; aliases resolve through the normal constant import table because +/// the same names are seeded in the checker/prescan constant maps. +fn token_as_import_name(token: &Token, metadata: &crate::lexer::TokenMetadata) -> Option { + match token { + Token::PhpIntMax + | Token::PhpIntMin + | Token::PhpFloatMax + | Token::PhpFloatMin + | Token::PhpFloatEpsilon + | Token::Inf + | Token::Nan + | Token::MPi + | Token::ME + | Token::MSqrt2 + | Token::MPi2 + | Token::MPi4 + | Token::MLog2e + | Token::MLog10e + | Token::Stdin + | Token::Stdout + | Token::Stderr + | Token::PhpEol + | Token::PhpOs + | Token::DirectorySeparator => token.word_spelling(metadata).map(str::to_string), + _ => None, + } } fn parse_use_prefix( @@ -296,13 +299,13 @@ fn parse_use_prefix( let mut parts = Vec::new(); loop { - match tokens.get(*pos).map(|(t, _)| t) { - Some(Token::Identifier(name)) => { + match tokens.get(*pos) { + Some((Token::Identifier(name), _)) => { parts.push(name.clone()); *pos += 1; } - Some(token) if token_as_import_name(token).is_some() => { - if let Some(name) = token_as_import_name(token) { + Some((token, meta)) if token_as_import_name(token, meta).is_some() => { + if let Some(name) = token_as_import_name(token, meta) { parts.push(name); } *pos += 1; diff --git a/src/parser/stmt/oop/body.rs b/src/parser/stmt/oop/body.rs index eaa8d83a71..55e31ffbe7 100644 --- a/src/parser/stmt/oop/body.rs +++ b/src/parser/stmt/oop/body.rs @@ -33,12 +33,18 @@ pub(in crate::parser::stmt) fn parse_interface_decl( ) -> Result { *pos += 1; // consume 'interface' - let name = match tokens.get(*pos).map(|(t, _)| t) { - Some(Token::Identifier(n)) => { + let name = match tokens.get(*pos) { + Some((Token::Identifier(n), _)) => { let n = n.clone(); *pos += 1; n } + // Soft keyword: `interface Enum` is legal PHP class-like naming. + Some((Token::Enum, meta)) => { + *pos += 1; + crate::parser::keyword_name::bareword_name_from_token(&Token::Enum, meta) + .unwrap_or_else(|| "enum".to_string()) + } _ => { return Err(CompileError::new( span, @@ -94,12 +100,18 @@ pub(in crate::parser::stmt) fn parse_trait_decl( ) -> Result { *pos += 1; // consume 'trait' - let name = match tokens.get(*pos).map(|(t, _)| t) { - Some(Token::Identifier(n)) => { + let name = match tokens.get(*pos) { + Some((Token::Identifier(n), _)) => { let n = n.clone(); *pos += 1; n } + // Soft keyword: `trait Enum` is legal PHP class-like naming. + Some((Token::Enum, meta)) => { + *pos += 1; + crate::parser::keyword_name::bareword_name_from_token(&Token::Enum, meta) + .unwrap_or_else(|| "enum".to_string()) + } _ => return Err(CompileError::new(span, "Expected trait name after 'trait'")), }; diff --git a/src/parser/stmt/oop/declarations.rs b/src/parser/stmt/oop/declarations.rs index 94de884301..bd62365e0b 100644 --- a/src/parser/stmt/oop/declarations.rs +++ b/src/parser/stmt/oop/declarations.rs @@ -33,17 +33,18 @@ pub(in crate::parser::stmt) fn parse_class_decl( ) -> Result { *pos += 1; // consume 'class' - let name = match tokens.get(*pos).map(|(t, _)| t) { - Some(Token::Identifier(n)) => { + let name = match tokens.get(*pos) { + Some((Token::Identifier(n), _)) => { let n = n.clone(); *pos += 1; n } // `enum` is only a soft keyword — `class Enum {}` is legal PHP (vendor precedent: - // marc-mabe/php-enum). The lexer always tokenizes the word, so accept it here. - Some(Token::Enum) => { + // marc-mabe/php-enum). Preserve source spelling via token metadata. + Some((Token::Enum, meta)) => { *pos += 1; - "Enum".to_string() + crate::parser::keyword_name::bareword_name_from_token(&Token::Enum, meta) + .unwrap_or_else(|| "enum".to_string()) } _ => return Err(CompileError::new(span, "Expected class name after 'class'")), }; diff --git a/src/types/checker/builtin_types/declarations.rs b/src/types/checker/builtin_types/declarations.rs index dae06ef040..264f04fcfd 100644 --- a/src/types/checker/builtin_types/declarations.rs +++ b/src/types/checker/builtin_types/declarations.rs @@ -21,7 +21,8 @@ use super::exception::{ builtin_exception_get_line_method, builtin_exception_get_message_method, builtin_exception_get_previous_method, builtin_exception_get_trace_as_string_method, builtin_exception_get_trace_method, builtin_exception_message_property, - builtin_exception_to_string_method, builtin_throwable_methods, + builtin_exception_previous_property, builtin_exception_to_string_method, + builtin_throwable_methods, }; use super::fiber::builtin_fiber_methods; @@ -118,6 +119,7 @@ pub(crate) fn inject_builtin_throwables( properties: vec![ builtin_exception_message_property(), builtin_exception_code_property(), + builtin_exception_previous_property(), ], methods: vec![ builtin_exception_constructor_method(), @@ -149,6 +151,7 @@ pub(crate) fn inject_builtin_throwables( properties: vec![ builtin_exception_message_property(), builtin_exception_code_property(), + builtin_exception_previous_property(), ], methods: vec![ builtin_exception_constructor_method(), diff --git a/src/types/checker/builtin_types/exception.rs b/src/types/checker/builtin_types/exception.rs index b6ddabf743..c8a2577fad 100644 --- a/src/types/checker/builtin_types/exception.rs +++ b/src/types/checker/builtin_types/exception.rs @@ -42,7 +42,8 @@ pub(super) fn builtin_exception_message_property() -> ClassProperty { } /// Returns a synthetic `ClassMethod` AST node for the `__construct` method of builtin Exception classes. -/// Takes `message` (string, default `""`) and `code` (int, default `0`) parameters and assigns them to the corresponding properties. +/// Takes `message` (string, default `""`), `code` (int, default `0`), and `previous` +/// (`?Throwable`, default `null`) — matching PHP's `Exception::__construct` arity. pub(super) fn builtin_exception_constructor_method() -> ClassMethod { ClassMethod { name: "__construct".to_string(), @@ -70,12 +71,9 @@ pub(super) fn builtin_exception_constructor_method() -> ClassMethod { )), false, ), - // PHP's third parameter, `?Throwable $previous = null`. Accepted (positionally and - // as the `previous:` named argument) but not stored: the compact throwable payload - // has no previous slot and `getPrevious()` is already synthesized as null. ( "previous".to_string(), - None, + Some(nullable_throwable_type()), Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())), false, ), @@ -109,6 +107,17 @@ pub(super) fn builtin_exception_constructor_method() -> ClassMethod { }, crate::span::Span::dummy(), ), + Stmt::new( + StmtKind::PropertyAssign { + object: Box::new(Expr::new(ExprKind::This, crate::span::Span::dummy())), + property: "previous".to_string(), + value: Expr::new( + ExprKind::Variable("previous".to_string()), + crate::span::Span::dummy(), + ), + }, + crate::span::Span::dummy(), + ), ], span: crate::span::Span::dummy(), attributes: Vec::new(), @@ -139,6 +148,29 @@ pub(super) fn builtin_exception_code_property() -> ClassProperty { } } +/// Returns a synthetic `ClassProperty` for PHP's `$previous` chain on builtin Exception classes. +/// +/// Typed `?Throwable` and stored at compact-payload offset 40 so `getPrevious()` round-trips +/// wrap-and-rethrow chains. Default is `null`. +pub(super) fn builtin_exception_previous_property() -> ClassProperty { + ClassProperty { + name: "previous".to_string(), + visibility: Visibility::Protected, + set_visibility: None, + type_expr: Some(nullable_throwable_type()), + hooks: PropertyHooks::none(), + readonly: false, + is_final: false, + is_static: false, + is_abstract: false, + by_ref: false, + is_promoted: false, + default: Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())), + span: crate::span::Span::dummy(), + attributes: Vec::new(), + } +} + /// Returns a synthetic `ClassMethod` for `Exception::getCode()`. /// Body returns `$this->code` cast to `int`. pub(super) fn builtin_exception_get_code_method() -> ClassMethod { @@ -383,6 +415,9 @@ pub(crate) fn patch_builtin_exception_signatures(checker: &mut Checker) { if let Some(param) = sig.params.get_mut(1) { param.1 = PhpType::Int; } + if let Some(param) = sig.params.get_mut(2) { + param.1 = nullable_throwable.clone(); + } sig.return_type = PhpType::Void; } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getMessage")) { diff --git a/src/types/checker/driver/init.rs b/src/types/checker/driver/init.rs index b02fb945f2..5266f6c8cf 100644 --- a/src/types/checker/driver/init.rs +++ b/src/types/checker/driver/init.rs @@ -84,6 +84,24 @@ impl Checker { for (name, _value) in ERROR_LEVEL_CONSTANTS { constants.insert((*name).to_string(), PhpType::Int); } + // Lexer-tokenized numeric / math constants — needed so `use const PHP_INT_MAX as X` + // aliases resolve through ConstRef rather than only via dedicated lexer tokens. + constants.insert("PHP_INT_MAX".to_string(), PhpType::Int); + constants.insert("PHP_INT_MIN".to_string(), PhpType::Int); + constants.insert("PHP_FLOAT_MAX".to_string(), PhpType::Float); + constants.insert("PHP_FLOAT_MIN".to_string(), PhpType::Float); + constants.insert("PHP_FLOAT_EPSILON".to_string(), PhpType::Float); + constants.insert("INF".to_string(), PhpType::Float); + constants.insert("NAN".to_string(), PhpType::Float); + constants.insert("M_PI".to_string(), PhpType::Float); + constants.insert("M_E".to_string(), PhpType::Float); + constants.insert("M_SQRT2".to_string(), PhpType::Float); + constants.insert("M_PI_2".to_string(), PhpType::Float); + constants.insert("M_PI_4".to_string(), PhpType::Float); + constants.insert("M_LOG2E".to_string(), PhpType::Float); + constants.insert("M_LOG10E".to_string(), PhpType::Float); + constants.insert("PHP_EOL".to_string(), PhpType::Str); + constants.insert("DIRECTORY_SEPARATOR".to_string(), PhpType::Str); Self { target_platform, diff --git a/src/types/checker/inference/objects/methods.rs b/src/types/checker/inference/objects/methods.rs index dc07723306..a0c5e66a86 100644 --- a/src/types/checker/inference/objects/methods.rs +++ b/src/types/checker/inference/objects/methods.rs @@ -286,6 +286,18 @@ impl Checker { env, &format!("Method {}::{}", interface_name, method), )?; + // Immutable "wither" fluent methods (PSR-FIG `with*`): declared return is often an + // ancestor interface while runtime returns `clone $this` / `@return static`. Keep the + // receiver interface so descendant-only chained calls still resolve. + if method_key.starts_with("with") { + if let PhpType::Object(return_name) = &sig.return_type { + if return_name != interface_name + && self.interface_extends_interface(interface_name, return_name) + { + return Ok(PhpType::Object(interface_name.to_string())); + } + } + } Ok(sig.return_type) } diff --git a/src/types/checker/schema/classes/methods.rs b/src/types/checker/schema/classes/methods.rs index a938c1d4df..ebb4069fb7 100644 --- a/src/types/checker/schema/classes/methods.rs +++ b/src/types/checker/schema/classes/methods.rs @@ -133,7 +133,7 @@ fn apply_static_method( } } if let Some(parent_sig) = state.static_sigs.get(&method_key) { - validate_override_signature(checker, &class.name, method, parent_sig, true)?; + validate_override_signature(checker, class, method, parent_sig, true)?; } else if has_override_attribute(method) && !interface_declares_method(checker, state, class, &method_key, true) { @@ -230,7 +230,7 @@ fn apply_instance_method( } } if let Some(parent_sig) = state.method_sigs.get(&method_key) { - validate_override_signature(checker, &class.name, method, parent_sig, false)?; + validate_override_signature(checker, class, method, parent_sig, false)?; } else if has_override_attribute(method) && !interface_declares_method(checker, state, class, &method_key, false) { diff --git a/src/types/checker/schema/validation.rs b/src/types/checker/schema/validation.rs index 8eb097dc0e..4051c67c93 100644 --- a/src/types/checker/schema/validation.rs +++ b/src/types/checker/schema/validation.rs @@ -311,12 +311,13 @@ pub(crate) fn declared_return_type_compatible( /// return type when the parent has one or make it incompatible. pub(crate) fn validate_override_signature( checker: &Checker, - class_name: &str, + class: &crate::types::traits::FlattenedClass, method: &ClassMethod, parent_sig: &FunctionSig, is_static: bool, ) -> Result<(), CompileError> { let kind = if is_static { "static method" } else { "method" }; + let class_name = class.name.as_str(); let child_sig = build_method_sig(checker, method)?; if php_symbol_key(&method.name) == "__construct" { return Ok(()); @@ -345,6 +346,14 @@ pub(crate) fn validate_override_signature( &parent_sig.return_type, &child_sig.return_type, ) + && !covariant_self_return_compatible( + checker, + class_name, + class.extends.as_deref(), + &class.implements, + parent_sig, + &child_sig, + ) { return Err(CompileError::new( method.span, @@ -356,3 +365,40 @@ pub(crate) fn validate_override_signature( } Ok(()) } + +/// Returns true when a child method may return the child class itself against a wider parent return. +/// +/// Covers PHP covariant returns (`parent::w(): Base` overridden as `w(): static` / `w(): Child`) +/// while the child is mid-construction — `type_accepts` cannot see the subclass edge yet because +/// `checker.classes` lacks the child, but the parent class is already registered. +fn covariant_self_return_compatible( + checker: &Checker, + class_name: &str, + extends: Option<&str>, + implements: &[String], + parent_sig: &FunctionSig, + child_sig: &FunctionSig, +) -> bool { + match (&parent_sig.return_type, &child_sig.return_type) { + (PhpType::Object(expected_name), PhpType::Object(actual_name)) + if actual_name == class_name => + { + if expected_name == class_name { + return true; + } + if let Some(parent) = extends { + if parent == expected_name + || checker.is_subclass_of(parent, expected_name) + || checker.class_implements_interface(parent, expected_name) + { + return true; + } + } + implements.iter().any(|iface| { + iface == expected_name + || checker.interface_extends_interface(iface, expected_name) + }) + } + _ => false, + } +} diff --git a/src/types/checker/type_compat/unions.rs b/src/types/checker/type_compat/unions.rs index 5261a6cabc..100bd13b95 100644 --- a/src/types/checker/type_compat/unions.rs +++ b/src/types/checker/type_compat/unions.rs @@ -96,7 +96,7 @@ impl Checker { && self.type_accepts(expected_value.as_ref(), actual_value.as_ref()) } PhpType::Array(actual_elem) - if matches!(expected_key.as_ref(), PhpType::Mixed) + if matches!(expected_key.as_ref(), PhpType::Mixed | PhpType::Int) && self.type_accepts(expected_value.as_ref(), actual_elem.as_ref()) => { true diff --git a/tests/codegen/exceptions.rs b/tests/codegen/exceptions.rs index b760ca98b9..1e383720c3 100644 --- a/tests/codegen/exceptions.rs +++ b/tests/codegen/exceptions.rs @@ -651,13 +651,22 @@ try { echo $c->foo(); echo 'no'; } catch (Error $e) { echo 'err'; } } /// The builtin exception constructors accept PHP's third `$previous` parameter (positional or -/// the `previous:` named argument). It is not stored — `getPrevious()` remains synthesized as -/// null — but wrap-and-rethrow code compiles and the message/code round-trip is byte-identical -/// to PHP 8.5. +/// the `previous:` named argument), store it on the Throwable payload, and expose it through +/// `getPrevious()`. Byte-parity vs PHP 8.5 for message/code/`getPrevious()` round-trips. #[test] fn test_exception_constructor_accepts_previous() { let out = compile_and_run( - "getMessage(), $e->getCode(), previous: $e); } } catch (\\InvalidArgumentException $x) { return $x->getMessage() . '/' . $x->getCode(); } } echo f();", + "getMessage(), $e->getCode(), previous: $e); } } catch (\\InvalidArgumentException $x) { $prev = $x->getPrevious(); return $x->getMessage() . '/' . $x->getCode() . '/' . ($prev === null ? 'none' : $prev->getMessage()); } } echo f();", ); - assert_eq!(out, "outer: inner/0"); + assert_eq!(out, "outer: inner/0/inner"); +} + +/// `getPrevious()` returns `?Throwable`; method calls on that nullable interface type must use +/// compact Throwable intrinsics (the interface vtable slots stay empty for builtins). +#[test] +fn test_nullable_throwable_get_message_via_previous() { + let out = compile_and_run( + "getMessage(); } $inner = new ValueError('inner'); $outer = new Exception('outer', 0, $inner); echo show($outer->getPrevious());", + ); + assert_eq!(out, "inner"); } diff --git a/tests/codegen/namespaces.rs b/tests/codegen/namespaces.rs index a9a041d48e..f3a149a364 100644 --- a/tests/codegen/namespaces.rs +++ b/tests/codegen/namespaces.rs @@ -482,8 +482,8 @@ echo \App\C\K::mk()->t->p; } /// `use const PHP_INT_MAX;` — the lexer eagerly tokenizes such constants, so the -/// use-declaration parser must accept the dedicated tokens as import names (the import itself -/// is inert; expression uses lower to literals). +/// use-declaration parser must accept the dedicated tokens as import names. Aliases +/// resolve through the seeded constant map (expression uses are not lexer-only). #[test] fn test_use_const_of_lexer_tokenized_constant() { let out = compile_and_run( @@ -493,9 +493,24 @@ namespace App; use const PHP_INT_MAX; use const PHP_INT_MIN; +use const STDERR; echo PHP_INT_MAX > 0 ? 'max' : '?', ':', PHP_INT_MIN < 0 ? 'min' : '?'; "#, ); assert_eq!(out, "max:min"); } + +/// `use const PHP_INT_MAX as MAX` must resolve the alias through ConstRef, not only the +/// dedicated lexer token path. +#[test] +fn test_use_const_alias_of_lexer_tokenized_constant() { + let out = compile_and_run( + r#" 0 ? 'ok' : 'no'; +"#, + ); + assert_eq!(out, "ok"); +} diff --git a/tests/codegen/oop/interfaces.rs b/tests/codegen/oop/interfaces.rs index 9108d0922d..ee6200d630 100644 --- a/tests/codegen/oop/interfaces.rs +++ b/tests/codegen/oop/interfaces.rs @@ -363,3 +363,43 @@ fn test_interface_covariant_self_return() { ); assert_eq!(out, "ok"); } + +/// PSR-FIG immutable "wither" fluent methods carry `@return static` but declare the ancestor +/// interface as the return type. Called through a descendant-interface receiver, the result must +/// keep the receiver's type so a subsequent descendant-only call resolves. +#[test] +fn test_interface_wither_ancestor_return_stays_receiver() { + let out = compile_and_run( + r#"verb, $value); } + public function withMethod(string $method): Request { return new Req($method, $this->payload); } + public function body(): string { return $this->payload; } + public function method(): string { return $this->verb; } +} +function chain(Request $r): string { + return $r->withHeader('x-trace')->withMethod('POST')->method(); +} +echo chain(new Req()); +"#, + ); + assert_eq!(out, "POST"); +} + +/// Parent method returns the parent class; child may override with `static` / self (covariant). +#[test] +fn test_class_covariant_self_return_override() { + let out = compile_and_run( + "w() instanceof Child ? 'ok' : 'no';", + ); + assert_eq!(out, "ok"); +} diff --git a/tests/codegen/oop/misc.rs b/tests/codegen/oop/misc.rs index 6729499297..d2b82419b8 100644 --- a/tests/codegen/oop/misc.rs +++ b/tests/codegen/oop/misc.rs @@ -119,9 +119,8 @@ fn test_example_v017_trio_compiles_and_runs() { assert_eq!(out, "health:[ok]:missing"); } -/// EC-10 (#493): `enum` is only a soft keyword — `class Enum {}` is legal PHP (vendor -/// precedent: marc-mabe/php-enum). The always-tokenizing lexer must not block the class -/// declaration. Byte-parity vs PHP 8.5. +/// EC-10: `enum` is only a soft keyword — `class Enum {}` / `interface Enum` / `new Enum` +/// are legal PHP (vendor precedent: marc-mabe/php-enum). Byte-parity vs PHP 8.5. #[test] fn test_class_named_enum_declares() { let out = compile_and_run( @@ -129,3 +128,12 @@ fn test_class_named_enum_declares() { ); assert_eq!(out, "e"); } + +/// Soft-keyword `enum` is also legal as an interface name. +#[test] +fn test_interface_named_enum_declares() { + let out = compile_and_run( + "tag();", + ); + assert_eq!(out, "i"); +} diff --git a/tests/error_tests/type_system.rs b/tests/error_tests/type_system.rs index d59ac7201a..14ccb234c6 100644 --- a/tests/error_tests/type_system.rs +++ b/tests/error_tests/type_system.rs @@ -756,3 +756,12 @@ fn test_error_nullable_intersection_type_rejected() { "?A&B should be rejected, not silently accepted", ); } + +/// `Exception::__construct` third parameter must be `?Throwable`, matching PHP. +#[test] +fn test_error_exception_previous_rejects_non_throwable() { + expect_error( + " Date: Sun, 19 Jul 2026 18:24:00 +0200 Subject: [PATCH 3/6] fix(types): keep interface return covariance sound --- src/ir_lower/expr/mod.rs | 20 ------ .../checker/inference/objects/methods.rs | 12 ---- .../checker/schema/classes/interfaces.rs | 61 ++++++++++++------- tests/codegen/oop/interfaces.rs | 30 +++------ tests/error_tests/misc/classes.rs | 20 ++++++ 5 files changed, 68 insertions(+), 75 deletions(-) diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 5485b4eed8..38b9c540f5 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -14051,26 +14051,6 @@ fn method_call_result_type( } return fallback_expr_type(expr); }; - // Fluent `with*` wither (@return static): a method declared to return a strict - // ancestor interface of the receiver interface actually returns the receiver. - // Must agree with `infer_method_call_on_interface_type`. - let fluent_receiver = if let (Some((recv_name, _)), PhpType::Object(return_name)) = - (singular_object_class(&object_ty), &return_ty) - { - if php_symbol_key(method).starts_with("with") - && ctx.interfaces.contains_key(recv_name.trim_start_matches('\\')) - && php_symbol_key(return_name.trim_start_matches('\\')) - != php_symbol_key(recv_name.trim_start_matches('\\')) - && interface_extends_interface_for_ir(ctx, recv_name, return_name) - { - Some(recv_name.to_string()) - } else { - None - } - } else { - None - }; - let return_ty = fluent_receiver.map(PhpType::Object).unwrap_or(return_ty); if op == Op::NullsafeMethodCall && nullable { nullable_result_type(return_ty) } else { diff --git a/src/types/checker/inference/objects/methods.rs b/src/types/checker/inference/objects/methods.rs index a0c5e66a86..dc07723306 100644 --- a/src/types/checker/inference/objects/methods.rs +++ b/src/types/checker/inference/objects/methods.rs @@ -286,18 +286,6 @@ impl Checker { env, &format!("Method {}::{}", interface_name, method), )?; - // Immutable "wither" fluent methods (PSR-FIG `with*`): declared return is often an - // ancestor interface while runtime returns `clone $this` / `@return static`. Keep the - // receiver interface so descendant-only chained calls still resolve. - if method_key.starts_with("with") { - if let PhpType::Object(return_name) = &sig.return_type { - if return_name != interface_name - && self.interface_extends_interface(interface_name, return_name) - { - return Ok(PhpType::Object(interface_name.to_string())); - } - } - } Ok(sig.return_type) } diff --git a/src/types/checker/schema/classes/interfaces.rs b/src/types/checker/schema/classes/interfaces.rs index 15d6b824d6..339d7706eb 100644 --- a/src/types/checker/schema/classes/interfaces.rs +++ b/src/types/checker/schema/classes/interfaces.rs @@ -103,6 +103,32 @@ fn class_can_implement_throwable_contract( .any(|interface_name| php_symbol_key(interface_name) == php_symbol_key("Throwable")) } +/// Returns whether the class's own type is a valid covariant implementation return. +/// +/// Class metadata is not registered yet while interface contracts are validated, so +/// this derives the subtype relationship from the interface currently being checked +/// and the interfaces declared directly on the class. +fn interface_self_return_conforms( + checker: &Checker, + class: &FlattenedClass, + interface_name: &str, + required_return: &PhpType, + actual_return: &PhpType, +) -> bool { + match (required_return, actual_return) { + (PhpType::Object(expected_name), PhpType::Object(actual_name)) => { + actual_name == &class.name + && (expected_name == interface_name + || checker.interface_extends_interface(interface_name, expected_name) + || class.implements.iter().any(|declared| { + declared == expected_name + || checker.interface_extends_interface(declared, expected_name) + })) + } + _ => false, + } +} + /// Validates that `class` satisfies all method and property contracts for each interface it /// implements (including transitive parents). /// @@ -273,6 +299,13 @@ fn validate_static_interface_method( } } if required_sig.declared_return + && !interface_self_return_conforms( + checker, + class, + interface_name, + &required_sig.return_type, + &actual_sig.return_type, + ) && !declared_return_type_compatible( checker, &required_sig.return_type, @@ -463,28 +496,14 @@ fn validate_interface_method( )?; } } - // Covariant self-return: PHP accepts an implementation returning a NARROWER type than the - // interface declares. The common shape — `withX(): static`/`self`/the class itself against - // an interface-typed return (PSR-7's immutability methods) — cannot go through - // `type_accepts`, because the class under validation is not registered in - // `checker.classes` yet (its interface list is invisible mid-construction). Accept it from - // the facts at hand: this function IS the proof that `class` implements `interface_name`, - // so a self-typed return conforms when the declared return is that interface (or one it - // extends), or any interface the class itself declares (or one those extend). - let self_return_conforms = match (&required_sig.return_type, &actual_sig.return_type) { - (PhpType::Object(expected_name), PhpType::Object(actual_name)) => { - actual_name == &class.name - && (expected_name == interface_name - || checker.interface_extends_interface(interface_name, expected_name) - || class.implements.iter().any(|declared| { - declared == expected_name - || checker.interface_extends_interface(declared, expected_name) - })) - } - _ => false, - }; if required_sig.declared_return - && !self_return_conforms + && !interface_self_return_conforms( + checker, + class, + interface_name, + &required_sig.return_type, + &actual_sig.return_type, + ) && !declared_return_type_compatible( checker, &required_sig.return_type, diff --git a/tests/codegen/oop/interfaces.rs b/tests/codegen/oop/interfaces.rs index ee6200d630..23a1824618 100644 --- a/tests/codegen/oop/interfaces.rs +++ b/tests/codegen/oop/interfaces.rs @@ -364,35 +364,21 @@ fn test_interface_covariant_self_return() { assert_eq!(out, "ok"); } -/// PSR-FIG immutable "wither" fluent methods carry `@return static` but declare the ancestor -/// interface as the return type. Called through a descendant-interface receiver, the result must -/// keep the receiver's type so a subsequent descendant-only call resolves. +/// A static implementation may return its concrete class against an interface return contract. #[test] -fn test_interface_wither_ancestor_return_stays_receiver() { +fn test_static_interface_covariant_self_return() { let out = compile_and_run( r#"verb, $value); } - public function withMethod(string $method): Request { return new Req($method, $this->payload); } - public function body(): string { return $this->payload; } - public function method(): string { return $this->verb; } -} -function chain(Request $r): string { - return $r->withHeader('x-trace')->withMethod('POST')->method(); -} -echo chain(new Req()); +echo Product::make() instanceof Product ? 'ok' : 'no'; "#, ); - assert_eq!(out, "POST"); + assert_eq!(out, "ok"); } /// Parent method returns the parent class; child may override with `static` / self (covariant). diff --git a/tests/error_tests/misc/classes.rs b/tests/error_tests/misc/classes.rs index 0ca6a8be52..2ab7997223 100644 --- a/tests/error_tests/misc/classes.rs +++ b/tests/error_tests/misc/classes.rs @@ -46,6 +46,26 @@ fn test_error_undefined_method() { ); } +/// Verifies an interface call keeps its declared ancestor return type. +#[test] +fn test_error_interface_method_does_not_infer_receiver_from_wither_name() { + expect_error( + r#"withHeader()->requestOnly(); +} +"#, + "Undefined method: Message::requestOnly", + ); +} + /// Verifies `::class` rejects a receiver whose static type is not an object. #[test] fn test_error_object_class_name_requires_object() { From 41d575d4fa8eb522bf4a7635c8553a4abafbf3b0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 19 Jul 2026 18:29:10 +0200 Subject: [PATCH 4/6] fix(parser): complete soft enum and const imports --- src/parser/expr/prefix.rs | 4 +- src/parser/stmt/mod.rs | 67 ++++++++++++++++------- src/parser/stmt/namespace_use.rs | 83 ++++++++++++++++++----------- src/parser/stmt/oop/body.rs | 42 +++------------ src/parser/stmt/oop/declarations.rs | 51 +++++------------- src/parser/stmt/params.rs | 2 +- tests/codegen/namespaces.rs | 29 ++++++++++ tests/codegen/types/enums.rs | 15 ++++++ tests/parser_tests/declarations.rs | 14 +++++ tests/parser_tests/namespaces.rs | 18 +++++++ 10 files changed, 204 insertions(+), 121 deletions(-) diff --git a/src/parser/expr/prefix.rs b/src/parser/expr/prefix.rs index c337706752..cd8499ebca 100644 --- a/src/parser/expr/prefix.rs +++ b/src/parser/expr/prefix.rs @@ -210,7 +210,9 @@ pub(super) fn parse_prefix( Token::Function => parse_closure(tokens, pos, span, false), Token::Fn => parse_arrow_closure(tokens, pos, span, false), Token::AttrOpen => parse_attributed_closure(tokens, pos, span), - Token::Identifier(_) | Token::Backslash => parse_named_expr(tokens, pos, span), + Token::Identifier(_) | Token::Enum | Token::Backslash => { + parse_named_expr(tokens, pos, span) + } Token::Self_ => { *pos += 1; parse_scoped_static_call(tokens, pos, span, StaticReceiver::Self_, "self") diff --git a/src/parser/stmt/mod.rs b/src/parser/stmt/mod.rs index 5d9f7dad45..9572cbeb61 100644 --- a/src/parser/stmt/mod.rs +++ b/src/parser/stmt/mod.rs @@ -108,7 +108,13 @@ fn parse_stmt_dispatch( Token::This => simple::parse_this_stmt(tokens, pos, span), Token::PlusPlus | Token::MinusMinus => assign::parse_incdec_stmt(tokens, pos, span), Token::Class => oop::parse_class_decl(tokens, pos, span, false, false, false), - Token::Enum => oop::parse_enum_decl(tokens, pos, span), + Token::Enum + if tokens.get(*pos + 1).is_some_and(|(token, metadata)| { + name_part_from_token(token, metadata).is_some() + }) => + { + oop::parse_enum_decl(tokens, pos, span) + } Token::ReadOnly => oop::parse_readonly_decl(tokens, pos, span), Token::Packed => oop::parse_packed_decl(tokens, pos, span), Token::Interface => oop::parse_interface_decl(tokens, pos, span), @@ -151,6 +157,7 @@ fn parse_stmt_dispatch( } Token::LBracket => assign::parse_list_unpack(tokens, pos, span), Token::Identifier(_) + | Token::Enum | Token::Self_ | Token::Parent | Token::Backslash @@ -471,20 +478,51 @@ pub(crate) fn expect_token( } } -/// Returns true if the token at `pos` is the start of a PHP name (identifier, soft keyword, or backslash). +/// Converts a token accepted as an ordinary PHP name segment to its source spelling. +/// +/// `enum` is a soft keyword in class-like name contexts; other reserved words remain +/// excluded here even though PHP permits them in the broader member-name grammar. +pub(crate) fn name_part_from_token( + token: &Token, + metadata: &crate::lexer::TokenMetadata, +) -> Option { + match token { + Token::Identifier(name) => Some(name.clone()), + Token::Enum => crate::parser::keyword_name::bareword_name_from_token(token, metadata), + _ => None, + } +} + +/// Returns true if the token at `pos` starts a PHP class-like name. /// -/// Soft keywords such as `enum` are valid class/interface/trait names in PHP, so `implements Enum` -/// and similar name lists must treat `Token::Enum` as a bareword name start. +/// Accepts identifiers, the soft keyword `enum`, and a leading namespace separator. pub(crate) fn name_starts_at(tokens: &[SpannedToken], pos: usize) -> bool { match tokens.get(pos) { - Some((Token::Identifier(_), _)) | Some((Token::Backslash, _)) => true, - Some((token, meta)) => { - crate::parser::keyword_name::bareword_name_from_token(token, meta).is_some() - } + Some((Token::Backslash, _)) => true, + Some((token, metadata)) => name_part_from_token(token, metadata).is_some(), None => false, } } +/// Parses one unqualified class-like declaration name. +/// +/// This accepts the soft keyword `enum` alongside ordinary identifiers but never +/// consumes namespace separators, which are invalid in declaration names. +pub(crate) fn parse_unqualified_name( + tokens: &[SpannedToken], + pos: &mut usize, + span: Span, + error: &str, +) -> Result { + let Some((token, metadata)) = tokens.get(*pos) else { + return Err(CompileError::new(span, error)); + }; + let name = name_part_from_token(token, metadata) + .ok_or_else(|| CompileError::new(span, error))?; + *pos += 1; + Ok(name) +} + /// Parses a PHP qualified or unqualified name from the token stream. /// /// Handles backslash-prefixed fully-qualified names (`\Foo\Bar`), qualified names (`Foo\Bar`), @@ -506,17 +544,10 @@ pub(crate) fn parse_name( let mut parts = Vec::new(); loop { match tokens.get(*pos) { - Some((Token::Identifier(name), _)) => { - parts.push(name.clone()); - *pos += 1; - } - // `enum` is only a soft keyword: `Enum` is a legal class name and name segment - // (`new Enum`, `extends Enum`, `MabeEnum\Enum`). Statement-position `enum` - // dispatches to the enum-declaration parser before parse_name is consulted. - Some((Token::Enum, meta)) => { + Some((token, metadata)) if name_part_from_token(token, metadata).is_some() => { parts.push( - crate::parser::keyword_name::bareword_name_from_token(&Token::Enum, meta) - .unwrap_or_else(|| "enum".to_string()), + name_part_from_token(token, metadata) + .expect("name part was checked immediately above"), ); *pos += 1; } diff --git a/src/parser/stmt/namespace_use.rs b/src/parser/stmt/namespace_use.rs index 79c889dbf3..92942d6a26 100644 --- a/src/parser/stmt/namespace_use.rs +++ b/src/parser/stmt/namespace_use.rs @@ -14,7 +14,10 @@ use crate::names::{Name, NameKind}; use crate::parser::ast::{Stmt, StmtKind, UseItem, UseKind}; use crate::span::Span; -use super::{expect_semicolon, expect_token, parse_name, parse_stmt, recover_to_statement_boundary}; +use super::{ + expect_semicolon, expect_token, name_part_from_token, parse_name, parse_stmt, + recover_to_statement_boundary, +}; /// Parses a `namespace` declaration or block. /// @@ -102,7 +105,13 @@ pub(super) fn parse_use_stmt( UseKind::Class }; - let prefix = parse_use_prefix(tokens, pos, span)?; + let prefix = parse_use_name( + tokens, + pos, + span, + "Expected imported name after 'use'", + true, + )?; let imports = if *pos < tokens.len() && tokens[*pos].0 == Token::LBrace { parse_group_use_items(tokens, pos, span, prefix, default_kind.clone())? @@ -128,11 +137,12 @@ pub(super) fn parse_use_stmt( } else { default_kind.clone() }; - let name = parse_name( + let name = parse_use_name( tokens, pos, span, "Expected imported name after ',' in use declaration", + false, )?; all_imports.push(parse_single_use_item_after_name( tokens, pos, span, name, item_kind, @@ -152,16 +162,22 @@ pub(super) fn parse_use_stmt( /// /// Returns `None` if no `as` keyword is present. Otherwise consumes `as` and the /// following identifier, returning the alias name. -fn parse_optional_alias(tokens: &[SpannedToken], pos: &mut usize) -> Option { +fn parse_optional_alias( + tokens: &[SpannedToken], + pos: &mut usize, + span: Span, +) -> Result, CompileError> { if *pos < tokens.len() && tokens[*pos].0 == Token::As { *pos += 1; - if let Some(Token::Identifier(alias)) = tokens.get(*pos).map(|(t, _)| t) { - let alias = alias.clone(); - *pos += 1; - return Some(alias); - } + let Some((token, metadata)) = tokens.get(*pos) else { + return Err(CompileError::new(span, "Expected alias after 'as'")); + }; + let alias = name_part_from_token(token, metadata) + .ok_or_else(|| CompileError::new(span, "Expected alias after 'as'"))?; + *pos += 1; + return Ok(Some(alias)); } - None + Ok(None) } /// Builds a `UseItem` from an already-parsed name and kind. @@ -175,7 +191,7 @@ fn parse_single_use_item_after_name( name: Name, kind: UseKind, ) -> Result { - let alias = parse_optional_alias(tokens, pos) + let alias = parse_optional_alias(tokens, pos, span)? .or_else(|| name.last_segment().map(str::to_string)) .ok_or_else(|| CompileError::new(span, "Imported name cannot be empty"))?; Ok(UseItem { kind, name, alias }) @@ -220,11 +236,12 @@ fn parse_group_use_items( default_kind.clone() }; - let suffix = parse_name( + let suffix = parse_use_name( tokens, pos, span, "Expected imported name inside group use declaration", + false, )?; if suffix.is_fully_qualified() { return Err(CompileError::new( @@ -248,12 +265,6 @@ fn parse_group_use_items( Ok(imports) } -/// Parses the name prefix before the optional grouped-use `{`. -/// -/// Reads a sequence of identifiers separated by `\`. If the sequence begins with `\`, -/// the name kind is `FullyQualified`; otherwise it is `Unqualified` or `Qualified` -/// depending on whether an intermediate `\ ` was seen. Stops when a trailing `\` -/// followed by `{` is encountered (the opening brace is consumed by the caller). /// Canonical import spelling for constant-like tokens the lexer eagerly tokenizes. /// /// `use const PHP_INT_MAX;` is legal PHP, but `PHP_INT_MAX` never reaches the parser as an @@ -286,10 +297,17 @@ fn token_as_import_name(token: &Token, metadata: &crate::lexer::TokenMetadata) - } } -fn parse_use_prefix( +/// Parses one imported name, including lexer-tokenized predefined constants. +/// +/// When `allow_group_prefix` is true, a trailing namespace separator before `{` +/// terminates the shared prefix without consuming the brace. Other callers require +/// a complete name and reject a trailing separator. +fn parse_use_name( tokens: &[SpannedToken], pos: &mut usize, span: Span, + first_error: &str, + allow_group_prefix: bool, ) -> Result { let mut kind = NameKind::Unqualified; if *pos < tokens.len() && tokens[*pos].0 == Token::Backslash { @@ -300,27 +318,32 @@ fn parse_use_prefix( let mut parts = Vec::new(); loop { match tokens.get(*pos) { - Some((Token::Identifier(name), _)) => { - parts.push(name.clone()); - *pos += 1; - } - Some((token, meta)) if token_as_import_name(token, meta).is_some() => { - if let Some(name) = token_as_import_name(token, meta) { - parts.push(name); - } + Some((token, metadata)) + if name_part_from_token(token, metadata).is_some() + || token_as_import_name(token, metadata).is_some() => + { + let part = name_part_from_token(token, metadata) + .or_else(|| token_as_import_name(token, metadata)) + .expect("import name part was checked immediately above"); + parts.push(part); *pos += 1; } _ if parts.is_empty() => { + return Err(CompileError::new(span, first_error)) + } + _ => { return Err(CompileError::new( span, - "Expected imported name after 'use'", + "Expected identifier after '\\' in imported name", )) } - _ => break, } if *pos < tokens.len() && tokens[*pos].0 == Token::Backslash { - if *pos + 1 < tokens.len() && tokens[*pos + 1].0 == Token::LBrace { + if allow_group_prefix + && *pos + 1 < tokens.len() + && tokens[*pos + 1].0 == Token::LBrace + { *pos += 1; break; } diff --git a/src/parser/stmt/oop/body.rs b/src/parser/stmt/oop/body.rs index 55e31ffbe7..7eaa4a1197 100644 --- a/src/parser/stmt/oop/body.rs +++ b/src/parser/stmt/oop/body.rs @@ -19,7 +19,7 @@ use crate::parser::expr::parse_expr; use crate::span::Span; use super::super::params::{looks_like_typed_param, parse_name_list, parse_type_expr}; -use super::super::{expect_semicolon, expect_token, parse_block}; +use super::super::{expect_semicolon, expect_token, parse_block, parse_unqualified_name}; use super::method_params::parse_method_params; use super::traits::parse_trait_use; @@ -33,25 +33,12 @@ pub(in crate::parser::stmt) fn parse_interface_decl( ) -> Result { *pos += 1; // consume 'interface' - let name = match tokens.get(*pos) { - Some((Token::Identifier(n), _)) => { - let n = n.clone(); - *pos += 1; - n - } - // Soft keyword: `interface Enum` is legal PHP class-like naming. - Some((Token::Enum, meta)) => { - *pos += 1; - crate::parser::keyword_name::bareword_name_from_token(&Token::Enum, meta) - .unwrap_or_else(|| "enum".to_string()) - } - _ => { - return Err(CompileError::new( - span, - "Expected interface name after 'interface'", - )) - } - }; + let name = parse_unqualified_name( + tokens, + pos, + span, + "Expected interface name after 'interface'", + )?; let extends = if *pos < tokens.len() && tokens[*pos].0 == Token::Extends { *pos += 1; @@ -100,20 +87,7 @@ pub(in crate::parser::stmt) fn parse_trait_decl( ) -> Result { *pos += 1; // consume 'trait' - let name = match tokens.get(*pos) { - Some((Token::Identifier(n), _)) => { - let n = n.clone(); - *pos += 1; - n - } - // Soft keyword: `trait Enum` is legal PHP class-like naming. - Some((Token::Enum, meta)) => { - *pos += 1; - crate::parser::keyword_name::bareword_name_from_token(&Token::Enum, meta) - .unwrap_or_else(|| "enum".to_string()) - } - _ => return Err(CompileError::new(span, "Expected trait name after 'trait'")), - }; + let name = parse_unqualified_name(tokens, pos, span, "Expected trait name after 'trait'")?; expect_token(tokens, pos, &Token::LBrace, "Expected '{' after trait name")?; let (trait_uses, properties, methods, constants, _cases) = diff --git a/src/parser/stmt/oop/declarations.rs b/src/parser/stmt/oop/declarations.rs index bd62365e0b..a0a0212dc0 100644 --- a/src/parser/stmt/oop/declarations.rs +++ b/src/parser/stmt/oop/declarations.rs @@ -17,7 +17,7 @@ use crate::parser::{next_anonymous_class_name, register_anonymous_class}; use crate::span::Span; use super::super::params::{parse_name_list, parse_type_expr}; -use super::super::{expect_semicolon, expect_token, parse_name}; +use super::super::{expect_semicolon, expect_token, parse_name, parse_unqualified_name}; use super::body::parse_class_like_body; /// Parses a class declaration: `class Name extends Parent { implements Ifaces { body } }`. @@ -33,21 +33,12 @@ pub(in crate::parser::stmt) fn parse_class_decl( ) -> Result { *pos += 1; // consume 'class' - let name = match tokens.get(*pos) { - Some((Token::Identifier(n), _)) => { - let n = n.clone(); - *pos += 1; - n - } - // `enum` is only a soft keyword — `class Enum {}` is legal PHP (vendor precedent: - // marc-mabe/php-enum). Preserve source spelling via token metadata. - Some((Token::Enum, meta)) => { - *pos += 1; - crate::parser::keyword_name::bareword_name_from_token(&Token::Enum, meta) - .unwrap_or_else(|| "enum".to_string()) - } - _ => return Err(CompileError::new(span, "Expected class name after 'class'")), - }; + let name = parse_unqualified_name( + tokens, + pos, + span, + "Expected class name after 'class'", + )?; let extends = if *pos < tokens.len() && tokens[*pos].0 == Token::Extends { *pos += 1; @@ -195,14 +186,7 @@ pub(in crate::parser::stmt) fn parse_enum_decl( ) -> Result { *pos += 1; // consume 'enum' - let name = match tokens.get(*pos).map(|(t, _)| t) { - Some(Token::Identifier(n)) => { - let n = n.clone(); - *pos += 1; - n - } - _ => return Err(CompileError::new(span, "Expected enum name after 'enum'")), - }; + let name = parse_unqualified_name(tokens, pos, span, "Expected enum name after 'enum'")?; let backing_type = if *pos < tokens.len() && tokens[*pos].0 == Token::Colon { *pos += 1; @@ -263,19 +247,12 @@ pub(in crate::parser::stmt) fn parse_packed_decl( "Expected 'class' after 'packed'", )?; - let name = match tokens.get(*pos).map(|(t, _)| t) { - Some(Token::Identifier(n)) => { - let n = n.clone(); - *pos += 1; - n - } - _ => { - return Err(CompileError::new( - span, - "Expected class name after 'packed class'", - )) - } - }; + let name = parse_unqualified_name( + tokens, + pos, + span, + "Expected class name after 'packed class'", + )?; expect_token( tokens, diff --git a/src/parser/stmt/params.rs b/src/parser/stmt/params.rs index 6fbddf54d2..fe3843f39e 100644 --- a/src/parser/stmt/params.rs +++ b/src/parser/stmt/params.rs @@ -319,7 +319,7 @@ fn parse_atomic_type_expr( *pos += 1; Ok(TypeExpr::Named(Name::unqualified("parent"))) } - Some(Token::Identifier(_)) | Some(Token::Backslash) => Ok(TypeExpr::Named(parse_name( + Some(_) if name_starts_at(tokens, *pos) => Ok(TypeExpr::Named(parse_name( tokens, pos, span, diff --git a/tests/codegen/namespaces.rs b/tests/codegen/namespaces.rs index f3a149a364..b57475cf75 100644 --- a/tests/codegen/namespaces.rs +++ b/tests/codegen/namespaces.rs @@ -514,3 +514,32 @@ echo MAX > 0 ? 'ok' : 'no'; ); assert_eq!(out, "ok"); } + +/// Imports multiple lexer-tokenized predefined constants in one `use const` declaration. +#[test] +fn test_use_const_multiple_lexer_tokenized_constants() { + let out = compile_and_run( + r#" 0 && MIN < 0 ? 'ok' : 'no'; +"#, + ); + assert_eq!(out, "ok"); +} + +/// Verifies `Enum` is accepted as an import alias, type name, constructor, and scoped receiver. +#[test] +fn test_enum_soft_keyword_import_alias() { + let out = compile_and_run( + r#"name; +} +echo describe(Enum::X); +"#, + ); + assert_eq!(out, "Enum:X"); +} + /// Verifies backed enum with `int` underlying type: `->value` returns the integer case value /// and `Color::from(2)` resolves to `Color::Green` by identity comparison. #[test] diff --git a/tests/parser_tests/declarations.rs b/tests/parser_tests/declarations.rs index 296c41ef6b..105c7f1a0d 100644 --- a/tests/parser_tests/declarations.rs +++ b/tests/parser_tests/declarations.rs @@ -184,6 +184,20 @@ fn test_parse_enum_case_expr() { } } +/// Verifies `enum` remains a soft keyword in declarations, type hints, and scoped expressions. +#[test] +fn test_parse_enum_named_enum_across_name_contexts() { + let stmts = parse_source( + " { + assert_eq!(imports.len(), 2); + assert_eq!(imports[0].name.as_str(), "Vendor\\PHP_INT_MAX"); + assert_eq!(imports[0].alias, "MAX"); + assert_eq!(imports[1].name.as_str(), "Vendor\\PHP_INT_MIN"); + assert_eq!(imports[1].alias, "MIN"); + } + other => panic!("expected use decl, got {:?}", other), + } +} + /// Parses a namespace block containing a class with extends, implements, trait use, /// and a fully-qualified static method call inside a method body. #[test] From bbf298ca0f373a25eda5e8e203e3b088e6ec355e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 19 Jul 2026 18:54:56 +0200 Subject: [PATCH 5/6] chore(conformance): finish guardrails and example --- CHANGELOG.md | 2 +- examples/exceptions/main.php | 10 +++ src/codegen/lower_inst/builtins/math.rs | 4 +- .../lower_inst/builtins/math/binary.rs | 4 +- src/codegen/lower_inst/enums.rs | 12 +-- src/codegen/lower_inst/exceptions.rs | 8 +- src/codegen/lower_inst/objects.rs | 90 +++++++++---------- src/codegen/lower_inst/static_properties.rs | 4 +- .../runtime/system/json_throw_error.rs | 2 +- src/parser/stmt/namespace_use.rs | 4 +- tests/codegen/static_class_features.rs | 9 ++ 11 files changed, 82 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b20f71881b..0727eec38a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +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 method returns may narrow `self` covariantly (including PSR-7-style wither call sites and parent `static` overrides); soft-keyword `enum` is accepted as a class/interface/trait name (`class Enum`, `implements Enum`); `use const` imports lexer-tokenized globals and aliases; and `foreach` over unknown `array` values widens keys to `int|string` instead of treating them as integers only. +- 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). diff --git a/examples/exceptions/main.php b/examples/exceptions/main.php index a39d47493a..1971132068 100644 --- a/examples/exceptions/main.php +++ b/examples/exceptions/main.php @@ -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; +} diff --git a/src/codegen/lower_inst/builtins/math.rs b/src/codegen/lower_inst/builtins/math.rs index 8bef09e013..5279145673 100644 --- a/src/codegen/lower_inst/builtins/math.rs +++ b/src/codegen/lower_inst/builtins/math.rs @@ -636,7 +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 + 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 @@ -661,7 +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 [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 diff --git a/src/codegen/lower_inst/builtins/math/binary.rs b/src/codegen/lower_inst/builtins/math/binary.rs index f72df8469a..6a98438b35 100644 --- a/src/codegen/lower_inst/builtins/math/binary.rs +++ b/src/codegen/lower_inst/builtins/math/binary.rs @@ -229,7 +229,7 @@ fn emit_intdiv_overflow_throw(ctx: &mut FunctionContext<'_>) { ctx.emitter.instruction(&format!("mov x9, #{}", msg_len)); // materialize the static ArithmeticError 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 + 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 ArithmeticError object ctx.emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder @@ -248,7 +248,7 @@ fn emit_intdiv_overflow_throw(ctx: &mut FunctionContext<'_>) { ctx.emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the static ArithmeticError message pointer ctx.emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", msg_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 [rax + 40], 0"); // previous defaults to null ctx.emitter.instruction("mov QWORD PTR [rip + _exc_value], rax"); // publish the active ArithmeticError object ctx.emitter.instruction("mov rsp, rbp"); // release the helper frame before throwing ctx.emitter.instruction("pop rbp"); // restore caller frame pointer before throwing diff --git a/src/codegen/lower_inst/enums.rs b/src/codegen/lower_inst/enums.rs index a496cc6ad4..b9ab5e51d6 100644 --- a/src/codegen/lower_inst/enums.rs +++ b/src/codegen/lower_inst/enums.rs @@ -428,7 +428,7 @@ fn emit_throw_value_error_from_string_result_aarch64(ctx: &mut FunctionContext<' abi::emit_load_temporary_stack_slot(ctx.emitter, "x9", 8); ctx.emitter.instruction("str x9, [x0, #16]"); // store dynamic exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero - ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "x0", "_exc_value", 0); abi::emit_release_temporary_stack(ctx.emitter, 16); abi::emit_jump(ctx.emitter, "__rt_throw_current"); @@ -447,7 +447,7 @@ fn emit_throw_value_error_from_string_result_x86_64(ctx: &mut FunctionContext<'_ abi::emit_load_temporary_stack_slot(ctx.emitter, "r10", 8); ctx.emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store dynamic exception message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); abi::emit_release_temporary_stack(ctx.emitter, 16); abi::emit_jump(ctx.emitter, "__rt_throw_current"); @@ -676,7 +676,7 @@ fn emit_throw_type_error_from_string_result(ctx: &mut FunctionContext<'_>) { abi::emit_load_temporary_stack_slot(ctx.emitter, "x9", 8); ctx.emitter.instruction("str x9, [x0, #16]"); // store dynamic exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero - ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "x0", "_exc_value", 0); abi::emit_release_temporary_stack(ctx.emitter, 16); abi::emit_jump(ctx.emitter, "__rt_throw_current"); @@ -693,7 +693,7 @@ fn emit_throw_type_error_from_string_result(ctx: &mut FunctionContext<'_>) { abi::emit_load_temporary_stack_slot(ctx.emitter, "r10", 8); ctx.emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store dynamic exception message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); abi::emit_release_temporary_stack(ctx.emitter, 16); abi::emit_jump(ctx.emitter, "__rt_throw_current"); @@ -718,7 +718,7 @@ fn emit_throw_enum_from_type_error_aarch64( abi::emit_load_int_immediate(ctx.emitter, "x9", message_len as i64); ctx.emitter.instruction("str x9, [x0, #16]"); // store static exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero - ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "x0", "_exc_value", 0); abi::emit_jump(ctx.emitter, "__rt_throw_current"); } @@ -740,7 +740,7 @@ fn emit_throw_enum_from_type_error_x86_64( abi::emit_load_int_immediate(ctx.emitter, "r10", message_len as i64); ctx.emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store static exception message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); abi::emit_jump(ctx.emitter, "__rt_throw_current"); } diff --git a/src/codegen/lower_inst/exceptions.rs b/src/codegen/lower_inst/exceptions.rs index ba02778bd2..91d3031c14 100644 --- a/src/codegen/lower_inst/exceptions.rs +++ b/src/codegen/lower_inst/exceptions.rs @@ -61,7 +61,7 @@ fn emit_static_exception( abi::emit_load_int_immediate(ctx.emitter, "x9", message_len as i64); ctx.emitter.instruction("str x9, [x0, #16]"); // store the exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero - ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "x0", "_exc_value", 0); abi::emit_jump(ctx.emitter, "__rt_throw_current"); } @@ -77,7 +77,7 @@ fn emit_static_exception( abi::emit_load_int_immediate(ctx.emitter, "r10", message_len as i64); ctx.emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store the exception message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); abi::emit_jump(ctx.emitter, "__rt_throw_current"); } @@ -179,7 +179,7 @@ fn emit_dynamic_error_object(ctx: &mut FunctionContext<'_>) { abi::emit_load_temporary_stack_slot(ctx.emitter, "x9", 8); ctx.emitter.instruction("str x9, [x0, #16]"); // store the runtime exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero - ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_release_temporary_stack(ctx.emitter, 16); abi::emit_store_reg_to_symbol(ctx.emitter, "x0", "_exc_value", 0); abi::emit_jump(ctx.emitter, "__rt_throw_current"); @@ -196,7 +196,7 @@ fn emit_dynamic_error_object(ctx: &mut FunctionContext<'_>) { abi::emit_load_temporary_stack_slot(ctx.emitter, "r10", 8); ctx.emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store the runtime exception message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_release_temporary_stack(ctx.emitter, 16); abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); abi::emit_jump(ctx.emitter, "__rt_throw_current"); diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 6ea707eafb..f1d7f29bbd 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -799,7 +799,7 @@ fn emit_validate_iterator_iterator_aggregate_downcast(ctx: &mut FunctionContext< fn emit_throw_iterator_iterator_downcast_logic_exception(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #56"); // request Throwable payload storage (message/code/previous) + ctx.emitter.instruction("mov x0, #56"); // request Throwable payload storage (message/code/previous) abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks object instances ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object @@ -814,7 +814,7 @@ fn emit_throw_iterator_iterator_downcast_logic_exception(ctx: &mut FunctionConte )); // load static exception message length ctx.emitter.instruction("str x9, [x0, #16]"); // store static exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero - ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null + 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 exception object ctx.emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder @@ -823,7 +823,7 @@ fn emit_throw_iterator_iterator_downcast_logic_exception(ctx: &mut FunctionConte ctx.emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation ctx.emitter.instruction("mov rbp, rsp"); // establish an aligned helper frame ctx.emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call aligned - ctx.emitter.instruction("mov rax, 56"); // request Throwable payload storage (message/code/previous) + ctx.emitter.instruction("mov rax, 56"); // request Throwable payload storage (message/code/previous) abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // materialize the x86_64 object heap kind word ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object @@ -838,7 +838,7 @@ fn emit_throw_iterator_iterator_downcast_logic_exception(ctx: &mut FunctionConte ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len() )); // store static exception message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null + 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 exception object ctx.emitter.instruction("mov rsp, rbp"); // release helper frame before throwing @@ -1005,31 +1005,24 @@ const THROWABLE_COMPACT_PAYLOAD_SIZE: u64 = 56; fn emit_throwable_allocation(ctx: &mut FunctionContext<'_>, class_id: u64) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!( - "mov x0, #{}", - THROWABLE_COMPACT_PAYLOAD_SIZE - )); // request compact Throwable payload storage + // -- allocate and stamp the compact Throwable payload -- + ctx.emitter.instruction(&format!("mov x0, #{}", THROWABLE_COMPACT_PAYLOAD_SIZE)); // request compact Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks runtime object payloads - ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the Throwable payload - ctx.emitter.instruction(&format!("mov x9, #{}", class_id)); // materialize the Throwable runtime class id - ctx.emitter.instruction("str x9, [x0]"); // store class id at payload offset zero - ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null until constructor init + ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks runtime object payloads + ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the Throwable payload + ctx.emitter.instruction(&format!("mov x9, #{}", class_id)); // materialize the Throwable runtime class id + ctx.emitter.instruction("str x9, [x0]"); // store class id at payload offset zero + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null until constructor init } Arch::X86_64 => { - ctx.emitter.instruction(&format!( - "mov rax, {}", - THROWABLE_COMPACT_PAYLOAD_SIZE - )); // request compact Throwable payload storage + // -- allocate and stamp the compact Throwable payload -- + ctx.emitter.instruction(&format!("mov rax, {}", THROWABLE_COMPACT_PAYLOAD_SIZE)); // request compact Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction(&format!( - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 6 - )); // materialize the x86_64 Throwable heap kind word - ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the Throwable payload - ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the Throwable runtime class id - ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at payload offset zero - ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null until constructor init + ctx.emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 6)); // materialize the x86_64 Throwable heap kind word + ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the Throwable payload + ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the Throwable runtime class id + ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at payload offset zero + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null until constructor init } } } @@ -1151,24 +1144,25 @@ fn emit_throwable_previous_field_aarch64( previous: Option, ) -> Result<()> { if let Some(previous) = previous { + // -- normalize and retain the previous Throwable -- let store_label = ctx.next_label("throwable_previous_store"); let null_label = ctx.next_label("throwable_previous_null"); ctx.load_value_to_reg(previous, "x0")?; - ctx.emitter.instruction(&format!("cbz x0, {}", null_label)); // missing previous → store null + ctx.emitter.instruction(&format!("cbz x0, {}", null_label)); // missing previous → store null abi::emit_load_int_immediate(ctx.emitter, "x9", RUNTIME_NULL_SENTINEL); - ctx.emitter.instruction("cmp x0, x9"); // is previous the in-band null sentinel? - ctx.emitter - .instruction(&format!("b.eq {}", null_label)); // treat sentinel as null payload + ctx.emitter.instruction("cmp x0, x9"); // is previous the in-band null sentinel? + ctx.emitter.instruction(&format!("b.eq {}", null_label)); // treat sentinel as null payload abi::emit_call_label(ctx.emitter, "__rt_incref"); // retain previous for the Throwable payload - ctx.emitter.instruction(&format!("b {}", store_label)); // keep the retained previous pointer + ctx.emitter.instruction(&format!("b {}", store_label)); // keep the retained previous pointer ctx.emitter.label(&null_label); - ctx.emitter.instruction("mov x0, xzr"); // compact payload stores raw null, not the in-band sentinel + ctx.emitter.instruction("mov x0, xzr"); // compact payload stores raw null, not the in-band sentinel ctx.emitter.label(&store_label); - ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for previous initialization - ctx.emitter.instruction("str x0, [x9, #40]"); // store Throwable previous pointer + ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for previous initialization + ctx.emitter.instruction("str x0, [x9, #40]"); // store Throwable previous pointer } else { - ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for previous initialization - ctx.emitter.instruction("str xzr, [x9, #40]"); // previous defaults to null + // -- initialize an omitted previous Throwable -- + ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for previous initialization + ctx.emitter.instruction("str xzr, [x9, #40]"); // previous defaults to null } Ok(()) } @@ -1179,24 +1173,26 @@ fn emit_throwable_previous_field_x86_64( previous: Option, ) -> Result<()> { if let Some(previous) = previous { + // -- normalize and retain the previous Throwable -- let store_label = ctx.next_label("throwable_previous_store"); let null_label = ctx.next_label("throwable_previous_null"); ctx.load_value_to_reg(previous, "rax")?; - ctx.emitter.instruction("test rax, rax"); // missing previous → store null - ctx.emitter.instruction(&format!("jz {}", null_label)); + ctx.emitter.instruction("test rax, rax"); // missing previous → store null + ctx.emitter.instruction(&format!("jz {}", null_label)); // branch around retain for a missing previous abi::emit_load_int_immediate(ctx.emitter, "r10", RUNTIME_NULL_SENTINEL); - ctx.emitter.instruction("cmp rax, r10"); // is previous the in-band null sentinel? - ctx.emitter.instruction(&format!("je {}", null_label)); // treat sentinel as null payload + ctx.emitter.instruction("cmp rax, r10"); // is previous the in-band null sentinel? + ctx.emitter.instruction(&format!("je {}", null_label)); // treat sentinel as null payload abi::emit_call_label(ctx.emitter, "__rt_incref"); // retain previous for the Throwable payload - ctx.emitter.instruction(&format!("jmp {}", store_label)); // keep the retained previous pointer + ctx.emitter.instruction(&format!("jmp {}", store_label)); // keep the retained previous pointer ctx.emitter.label(&null_label); - ctx.emitter.instruction("xor rax, rax"); // compact payload stores raw null, not the in-band sentinel + ctx.emitter.instruction("xor rax, rax"); // compact payload stores raw null, not the in-band sentinel ctx.emitter.label(&store_label); - ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for previous initialization - ctx.emitter.instruction("mov QWORD PTR [r11 + 40], rax"); // store Throwable previous pointer + ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for previous initialization + ctx.emitter.instruction("mov QWORD PTR [r11 + 40], rax"); // store Throwable previous pointer } else { - ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for previous initialization - ctx.emitter.instruction("mov QWORD PTR [r11 + 40], 0"); // previous defaults to null + // -- initialize an omitted previous Throwable -- + ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for previous initialization + ctx.emitter.instruction("mov QWORD PTR [r11 + 40], 0"); // previous defaults to null } Ok(()) } @@ -6105,7 +6101,7 @@ fn emit_uninitialized_typed_property_fatal( ctx.emitter.instruction(&format!("mov x9, #{}", message_len)); // load Error message length ctx.emitter.instruction("str x9, [x0, #16]"); // store exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero - ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_symbol_address(ctx.emitter, "x9", "_exc_value"); // materialize the active exception cell ctx.emitter.instruction("str x0, [x9]"); // publish the active exception object ctx.emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder @@ -6124,7 +6120,7 @@ fn emit_uninitialized_typed_property_fatal( ctx.emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static Error message pointer ctx.emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", message_len)); // store Error message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); // publish the active exception object ctx.emitter.instruction("mov rsp, rbp"); // release helper frame before throwing ctx.emitter.instruction("pop rbp"); // restore caller frame pointer before throwing diff --git a/src/codegen/lower_inst/static_properties.rs b/src/codegen/lower_inst/static_properties.rs index 31a6c138b6..8a4f5f8ef2 100644 --- a/src/codegen/lower_inst/static_properties.rs +++ b/src/codegen/lower_inst/static_properties.rs @@ -950,7 +950,7 @@ fn emit_uninitialized_static_property_fatal( ctx.emitter.instruction(&format!("mov x9, #{}", message_len)); // load Error message length ctx.emitter.instruction("str x9, [x0, #16]"); // store exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero - ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null + ctx.emitter.instruction("str xzr, [x0, #40]"); // previous defaults to null abi::emit_symbol_address(ctx.emitter, "x9", "_exc_value"); // materialize the active exception cell ctx.emitter.instruction("str x0, [x9]"); // publish the active exception object ctx.emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder @@ -969,7 +969,7 @@ fn emit_uninitialized_static_property_fatal( ctx.emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static Error message pointer ctx.emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", message_len)); // store Error message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null + ctx.emitter.instruction("mov QWORD PTR [rax + 40], 0"); // previous defaults to null abi::emit_store_reg_to_symbol(ctx.emitter, "rax", "_exc_value", 0); // publish the active exception object ctx.emitter.instruction("mov rsp, rbp"); // release helper frame before throwing ctx.emitter.instruction("pop rbp"); // restore caller frame pointer before throwing diff --git a/src/codegen_support/runtime/system/json_throw_error.rs b/src/codegen_support/runtime/system/json_throw_error.rs index 417a871eab..d24085fcad 100644 --- a/src/codegen_support/runtime/system/json_throw_error.rs +++ b/src/codegen_support/runtime/system/json_throw_error.rs @@ -59,7 +59,7 @@ pub(crate) fn emit_json_throw_error(emitter: &mut Emitter) { emitter.instruction("tst x9, x10"); // is the throw bit set? emitter.instruction("b.eq __rt_json_throw_error_return"); // bail out when throwing is not requested - // Allocate a 32-byte JsonException payload (class_id + message ptr/len + code). + // Allocate a 56-byte JsonException payload (class_id + message, code, and previous slots). emitter.instruction("mov x0, #56"); // size = class_id + message + code + previous slots emitter.instruction("bl __rt_heap_alloc"); // allocate the JsonException payload emitter.instruction("mov x9, #6"); // heap kind 6 = object diff --git a/src/parser/stmt/namespace_use.rs b/src/parser/stmt/namespace_use.rs index 92942d6a26..2050d79c23 100644 --- a/src/parser/stmt/namespace_use.rs +++ b/src/parser/stmt/namespace_use.rs @@ -160,8 +160,8 @@ pub(super) fn parse_use_stmt( /// Parses an optional `as Alias` clause after a use item name. /// -/// Returns `None` if no `as` keyword is present. Otherwise consumes `as` and the -/// following identifier, returning the alias name. +/// Returns `Ok(None)` if no `as` keyword is present. Otherwise consumes `as` and +/// the following ordinary or soft-keyword name, returning an error when it is absent. fn parse_optional_alias( tokens: &[SpannedToken], pos: &mut usize, diff --git a/tests/codegen/static_class_features.rs b/tests/codegen/static_class_features.rs index 5bc30e5f8e..26e9f69ebe 100644 --- a/tests/codegen/static_class_features.rs +++ b/tests/codegen/static_class_features.rs @@ -121,6 +121,15 @@ fn test_new_static_returns_instance_of_called_class() { assert_eq!(out, "Child"); } +/// Verifies a static child override may narrow a parent-class return to `static`. +#[test] +fn test_static_override_covariant_self_return() { + let out = compile_and_run( + " Date: Mon, 20 Jul 2026 05:10:31 +0200 Subject: [PATCH 6/6] fix(eval): bridge Throwable previous constructor argument --- src/codegen/eval_constructor_helpers.rs | 100 +++++++++++++++++++----- tests/codegen/eval.rs | 15 ++++ 2 files changed, 95 insertions(+), 20 deletions(-) diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 129832fc6f..81bf02c2ef 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -431,34 +431,34 @@ fn emit_aarch64_constructor_exception_boundary_push(emitter: &mut Emitter, escap let handler_offset = CONSTRUCTOR_HELPER_HANDLER_OFFSET - 48; emitter.comment("push eval constructor exception boundary"); abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_handler_top", 0); - emitter.instruction(&format!("str x10, [x29, #{}]", handler_offset)); // save the previous native exception-handler head + emitter.instruction(&format!("str x10, [x29, #{}]", handler_offset)); // save the previous native exception-handler head abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_call_frame_top", 0); - emitter.instruction(&format!("str x10, [x29, #{}]", handler_offset + 8)); // preserve the caller activation frame across constructor unwinding + emitter.instruction(&format!("str x10, [x29, #{}]", handler_offset + 8)); // preserve the caller activation frame across constructor unwinding abi::emit_load_symbol_to_reg(emitter, "x10", "_rt_diag_suppression", 0); emitter.instruction(&format!( "str x10, [x29, #{}]", handler_offset + TRY_HANDLER_DIAG_DEPTH_OFFSET - )); // save diagnostic suppression depth for restoration - emitter.instruction(&format!("add x10, x29, #{}", handler_offset)); // compute the boundary handler record address + )); // save diagnostic suppression depth for restoration + emitter.instruction(&format!("add x10, x29, #{}", handler_offset)); // compute the boundary handler record address abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); emitter.instruction(&format!( "add x0, x29, #{}", handler_offset + TRY_HANDLER_JMP_BUF_OFFSET - )); // pass the boundary jmp_buf to setjmp - emitter.bl_c("setjmp"); // snapshot the bridge stack before entering native constructors - emitter.instruction(&format!("cbnz x0, {}", escape_label)); // non-zero setjmp result means a constructor Throwable escaped + )); // pass the boundary jmp_buf to setjmp + emitter.bl_c("setjmp"); // snapshot the bridge stack before entering native constructors + emitter.instruction(&format!("cbnz x0, {}", escape_label)); // non-zero setjmp result means a constructor Throwable escaped } /// Emits an ARM64 boundary pop that preserves the constructor status in x0. fn emit_aarch64_constructor_exception_boundary_pop(emitter: &mut Emitter) { let handler_offset = CONSTRUCTOR_HELPER_HANDLER_OFFSET - 48; emitter.comment("pop eval constructor exception boundary"); - emitter.instruction(&format!("ldr x10, [x29, #{}]", handler_offset)); // reload the previous native exception-handler head + emitter.instruction(&format!("ldr x10, [x29, #{}]", handler_offset)); // reload the previous native exception-handler head abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); emitter.instruction(&format!( "ldr x10, [x29, #{}]", handler_offset + TRY_HANDLER_DIAG_DEPTH_OFFSET - )); // reload the saved diagnostic suppression depth + )); // reload the saved diagnostic suppression depth abi::emit_store_reg_to_symbol(emitter, "x10", "_rt_diag_suppression", 0); } @@ -475,7 +475,7 @@ fn emit_x86_64_constructor_exception_boundary_push(emitter: &mut Emitter, escape "mov QWORD PTR [rbp - {}], r10", handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET )); // save diagnostic suppression depth for restoration - emitter.instruction(&format!("lea r10, [rbp - {}]", handler_base)); // compute the boundary handler record address + emitter.instruction(&format!("lea r10, [rbp - {}]", handler_base)); // compute the boundary handler record address abi::emit_store_reg_to_symbol(emitter, "r10", "_exc_handler_top", 0); emitter.instruction(&format!( "lea rdi, [rbp - {}]", @@ -587,7 +587,7 @@ fn emit_aarch64_builtin_throwable_constructor_body( ) { emit_aarch64_validate_builtin_throwable_arg_count(module, emitter, fail_label); emit_aarch64_default_builtin_throwable_fields(emitter); - emitter.instruction("ldr x9, [sp, #32]"); // reload constructor argc before testing the message argument + emitter.instruction("ldr x9, [sp, #40]"); // reload constructor argc before testing the message argument emitter.instruction("cmp x9, #0"); // did the eval call pass a message argument? emitter.instruction(&format!("b.eq {}", success_label)); // keep the empty Throwable defaults when no message was supplied emit_aarch64_load_eval_arg(module, emitter, 0); @@ -603,7 +603,7 @@ fn emit_aarch64_builtin_throwable_constructor_body( emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for message initialization emitter.instruction("str x1, [x9, #8]"); // store the message pointer in the compact Throwable payload emitter.instruction("str x2, [x9, #16]"); // store the message length in the compact Throwable payload - emitter.instruction("ldr x9, [sp, #32]"); // reload constructor argc before testing the code argument + emitter.instruction("ldr x9, [sp, #40]"); // reload constructor argc before testing the code argument emitter.instruction("cmp x9, #1"); // did the eval call pass a code argument? emitter.instruction(&format!("b.le {}", success_label)); // keep code zero when only the message was supplied emit_aarch64_load_eval_arg(module, emitter, 1); @@ -618,7 +618,12 @@ fn emit_aarch64_builtin_throwable_constructor_body( ); emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for code initialization emitter.instruction("str x0, [x9, #24]"); // store the integer exception code - emitter.instruction(&format!("b {}", success_label)); // builtin Throwable construction completed + emit_aarch64_builtin_throwable_previous_arg( + module, + emitter, + fail_label, + success_label, + ); } /// Initializes the compact Throwable payload for eval-created x86_64 builtin exceptions. @@ -663,6 +668,59 @@ fn emit_x86_64_builtin_throwable_constructor_body( ); emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for code initialization emitter.instruction("mov QWORD PTR [r11 + 24], rax"); // store the integer exception code + emit_x86_64_builtin_throwable_previous_arg( + module, + emitter, + fail_label, + success_label, + ); +} + +/// Stores the nullable third Throwable constructor argument on ARM64. +fn emit_aarch64_builtin_throwable_previous_arg( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, + success_label: &str, +) { + emitter.instruction("ldr x9, [sp, #40]"); // reload argc before testing the previous argument + emitter.instruction("cmp x9, #2"); // did eval supply the normalized previous argument? + emitter.instruction(&format!("b.le {}", success_label)); // keep null when the legacy bridge omitted previous + emit_aarch64_load_eval_arg(module, emitter, 2); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed previous argument for inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the nullable previous payload + emitter.instruction("cmp x0, #8"); // runtime tag 8 means the previous argument is null + emitter.instruction(&format!("b.eq {}", success_label)); // keep the default raw null previous pointer + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the previous argument is an object + emitter.instruction(&format!("b.ne {}", fail_label)); // reject malformed non-object previous arguments + emitter.instruction("mov x0, x1"); // move the previous object payload into the retain ABI + abi::emit_call_label(emitter, "__rt_incref"); + emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object after retaining previous + emitter.instruction("str x0, [x9, #40]"); // store the retained previous object pointer + emitter.instruction(&format!("b {}", success_label)); // builtin Throwable construction completed +} + +/// Stores the nullable third Throwable constructor argument on x86_64. +fn emit_x86_64_builtin_throwable_previous_arg( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, + success_label: &str, +) { + emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload argc before testing the previous argument + emitter.instruction("cmp r11, 2"); // did eval supply the normalized previous argument? + emitter.instruction(&format!("jle {}", success_label)); // keep null when the legacy bridge omitted previous + emit_x86_64_load_eval_arg(module, emitter, 2); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed previous argument for inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the nullable previous payload + emitter.instruction("cmp rax, 8"); // runtime tag 8 means the previous argument is null + emitter.instruction(&format!("je {}", success_label)); // keep the default raw null previous pointer + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the previous argument is an object + emitter.instruction(&format!("jne {}", fail_label)); // reject malformed non-object previous arguments + emitter.instruction("mov rax, rdi"); // move the previous object payload into the retain ABI + abi::emit_call_label(emitter, "__rt_incref"); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object after retaining previous + emitter.instruction("mov QWORD PTR [r11 + 40], rax"); // store the retained previous object pointer emitter.instruction(&format!("jmp {}", success_label)); // builtin Throwable construction completed } @@ -675,9 +733,9 @@ fn emit_aarch64_validate_builtin_throwable_arg_count( emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for builtin Throwable arity validation let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); - emitter.instruction("str x0, [sp, #32]"); // save constructor argc for message/code initialization - emitter.instruction("cmp x0, #2"); // compact Throwable initialization supports message and code arguments - emitter.instruction(&format!("b.gt {}", fail_label)); // reject unsupported previous-Throwable arguments from eval + emitter.instruction("str x0, [sp, #40]"); // preserve argc outside the eval argument scratch slot + emitter.instruction("cmp x0, #3"); // compact Throwable initialization supports message/code/previous + emitter.instruction(&format!("b.gt {}", fail_label)); // reject excess builtin Throwable arguments from eval } /// Emits x86_64 arity validation for compact builtin Throwable constructors. @@ -690,24 +748,26 @@ fn emit_x86_64_validate_builtin_throwable_arg_count( let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save constructor argc for message/code initialization - emitter.instruction("cmp rax, 2"); // compact Throwable initialization supports message and code arguments - emitter.instruction(&format!("jg {}", fail_label)); // reject unsupported previous-Throwable arguments from eval + emitter.instruction("cmp rax, 3"); // compact Throwable initialization supports message/code/previous + emitter.instruction(&format!("jg {}", fail_label)); // reject excess builtin Throwable arguments from eval } -/// Writes ARM64 empty-message and zero-code defaults into the compact Throwable payload. +/// Writes ARM64 empty-message, zero-code, and null-previous Throwable defaults. fn emit_aarch64_default_builtin_throwable_fields(emitter: &mut Emitter) { emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for default initialization emitter.instruction("str xzr, [x9, #8]"); // default the message pointer to an empty string payload emitter.instruction("str xzr, [x9, #16]"); // default the message length to zero emitter.instruction("str xzr, [x9, #24]"); // default the exception code to zero + emitter.instruction("str xzr, [x9, #40]"); // default the previous Throwable pointer to null } -/// Writes x86_64 empty-message and zero-code defaults into the compact Throwable payload. +/// Writes x86_64 empty-message, zero-code, and null-previous Throwable defaults. fn emit_x86_64_default_builtin_throwable_fields(emitter: &mut Emitter) { emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for default initialization emitter.instruction("mov QWORD PTR [r11 + 8], 0"); // default the message pointer to an empty string payload emitter.instruction("mov QWORD PTR [r11 + 16], 0"); // default the message length to zero emitter.instruction("mov QWORD PTR [r11 + 24], 0"); // default the exception code to zero + emitter.instruction("mov QWORD PTR [r11 + 40], 0"); // default the previous Throwable pointer to null } /// Emits ARM64 class-id dispatch for supported constructor bodies. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 22b67dbff3..6797098ca3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -28739,6 +28739,21 @@ return 0;'); assert_eq!(out, "8"); } +/// Verifies eval's native Throwable bridge retains and exposes the third `$previous` argument. +#[test] +fn test_eval_exception_previous_round_trips_through_native_bridge() { + let out = compile_and_run( + r#"getMessage(), "|", $caught->getCode(), "|", $caught->getPrevious()->getMessage(); +} +"#, + ); + assert_eq!(out, "outer|7|inner"); +} + /// Verifies eval-internal catch type narrowing uses the thrown object's class. #[test] fn test_eval_try_catch_matches_specific_exception_inside_eval() {