diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac862e5513..f11a511de7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ Releases are listed newest first.
- Added PHP 8.3 typed class constants for classes, interfaces, traits, and enums. Declared types are enforced for initializers and inherited overrides, including covariant narrowing, and are exposed through `ReflectionClassConstant::hasType()` and `getType()`; untyped constants and enum cases retain PHP-compatible reflection defaults.
- Fixed `--web --max-requests` crash-loop accounting (issue #516): workers that exit after a planned request-quota recycle now use a dedicated status that the master excludes from startup-death streaks, so sustained traffic with a low quota keeps respawning workers instead of shutting down the server; genuine setup failures, other exit codes, and signal deaths still feed the guard.
- Fixed nullable chained array reads (issue #525): consuming a value from a `?array` receiver now releases the one-shot hidden owned Mixed temporary created by nullable access, on both null and non-null paths, without invalidating the extracted result or double-releasing repeated reads.
+- Fixed chained container reads after a missing outer array offset (issues #526 and #554): indexed, associative, mixed-key, truthiness, `empty()`, `(bool)`, `count()`, spread-length, object-property, and method-call consumers now recognize null-container receivers before dereferencing them, preventing segfaults across every supported target. Direct nested reads emit PHP's missing-key and null-offset diagnostics, associative misses warn consistently, and invalid `count()`, spread, and method operations raise catchable PHP errors without evaluating skipped method arguments; `isset()`, `empty()`, and null coalescing remain silent across the full subscript chain, string-valued misses retain their null marker so `??` selects its default without conflating real empty strings, and `foreach` skips a missing container instead of crashing.
- Fixed owned boxed Mixed temporaries leaking after string conversion (issue #527): explicit `(string)` casts plus implicit concatenation and interpolation now release detached Mixed sources, including by-value `foreach` element reads and non-null unions represented as Mixed; borrowed, persistent, moved, and non-heap values remain release no-ops.
- Fixed boxed Mixed values leaking when nested loops reinitialize a local whose storage widens after lowering (issue #534): EIR now records a deferred `ReleaseLocalSlot` before the overwrite, prunes it for final scalar or ref-bound slots, and lowers it using the final slot representation. Cleanup is flow-sensitive across conditional ref-cell promotion, aliases, by-reference `foreach` and pointer paths, and widened parameters, preventing both missed releases and double frees with the EIR optimizer enabled or disabled.
- Fixed function- and method-local indexed arrays leaking previous COW hash generations after promotion to string-keyed associative storage (issue #538): lowering now releases the boxed Mixed slot owner before consuming mutations and finalizes provisional concrete-load releases against the slot's final storage type. This preserves real COW aliases while avoiding an artificial clone on every insertion, leaving heap-debug clean with the EIR optimizer enabled or disabled across every supported target.
diff --git a/docs/internals/builtins/misc/empty.md b/docs/internals/builtins/misc/empty.md
index fe367e862f..6427743662 100644
--- a/docs/internals/builtins/misc/empty.md
+++ b/docs/internals/builtins/misc/empty.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1216](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1216) (`lower_empty`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1232](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1232) (`lower_empty`)
- **Function symbol**: `lower_empty()`
diff --git a/docs/internals/builtins/string/strlen.md b/docs/internals/builtins/string/strlen.md
index 872fc4ec8d..3f03332476 100644
--- a/docs/internals/builtins/string/strlen.md
+++ b/docs/internals/builtins/string/strlen.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/builtins/string/strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strlen.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1077](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1077) (`lower_strlen`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1093](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1093) (`lower_strlen`)
- **Function symbol**: `lower_strlen()`
diff --git a/docs/internals/builtins/type/boolval.md b/docs/internals/builtins/type/boolval.md
index 1230c0e181..46fec33146 100644
--- a/docs/internals/builtins/type/boolval.md
+++ b/docs/internals/builtins/type/boolval.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/builtins/types/boolval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/boolval.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1174](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1174) (`lower_boolval`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1190](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1190) (`lower_boolval`)
- **Function symbol**: `lower_boolval()`
diff --git a/docs/internals/builtins/type/floatval.md b/docs/internals/builtins/type/floatval.md
index 813d03f851..aa5b0c5fba 100644
--- a/docs/internals/builtins/type/floatval.md
+++ b/docs/internals/builtins/type/floatval.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/builtins/types/floatval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/floatval.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1140](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1140) (`lower_floatval`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1156](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1156) (`lower_floatval`)
- **Function symbol**: `lower_floatval()`
diff --git a/docs/internals/builtins/type/intval.md b/docs/internals/builtins/type/intval.md
index 8cde0f5d6b..32bdf511fa 100644
--- a/docs/internals/builtins/type/intval.md
+++ b/docs/internals/builtins/type/intval.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/builtins/types/intval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/intval.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1107](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1107) (`lower_intval`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1123](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1123) (`lower_intval`)
- **Function symbol**: `lower_intval()`
diff --git a/docs/internals/builtins/type/is_array.md b/docs/internals/builtins/type/is_array.md
index 266b6a2189..723e633168 100644
--- a/docs/internals/builtins/type/is_array.md
+++ b/docs/internals/builtins/type/is_array.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/builtins/types/is_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_array.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1601](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1601) (`lower_is_array`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1617](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1617) (`lower_is_array`)
- **Function symbol**: `lower_is_array()`
diff --git a/docs/internals/builtins/type/is_bool.md b/docs/internals/builtins/type/is_bool.md
index c409247bc4..130c3d56c4 100644
--- a/docs/internals/builtins/type/is_bool.md
+++ b/docs/internals/builtins/type/is_bool.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/builtins/types/is_bool.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_bool.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1334](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1334) (`lower_static_type_predicate`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1350](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1350) (`lower_static_type_predicate`)
- **Function symbol**: `lower_static_type_predicate()`
diff --git a/docs/internals/builtins/type/is_float.md b/docs/internals/builtins/type/is_float.md
index 2a70b99832..191c5a11ca 100644
--- a/docs/internals/builtins/type/is_float.md
+++ b/docs/internals/builtins/type/is_float.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/builtins/types/is_float.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_float.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1334](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1334) (`lower_static_type_predicate`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1350](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1350) (`lower_static_type_predicate`)
- **Function symbol**: `lower_static_type_predicate()`
diff --git a/docs/internals/builtins/type/is_int.md b/docs/internals/builtins/type/is_int.md
index 60d6ab7819..d326a35c40 100644
--- a/docs/internals/builtins/type/is_int.md
+++ b/docs/internals/builtins/type/is_int.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/builtins/types/is_int.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_int.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1334](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1334) (`lower_static_type_predicate`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1350](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1350) (`lower_static_type_predicate`)
- **Function symbol**: `lower_static_type_predicate()`
diff --git a/docs/internals/builtins/type/is_iterable.md b/docs/internals/builtins/type/is_iterable.md
index 0ceb5c15e8..f5a387b916 100644
--- a/docs/internals/builtins/type/is_iterable.md
+++ b/docs/internals/builtins/type/is_iterable.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/builtins/types/is_iterable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_iterable.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1392](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1392) (`lower_is_iterable`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1408](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1408) (`lower_is_iterable`)
- **Function symbol**: `lower_is_iterable()`
diff --git a/docs/internals/builtins/type/is_null.md b/docs/internals/builtins/type/is_null.md
index 43d925866b..3e1744fc7a 100644
--- a/docs/internals/builtins/type/is_null.md
+++ b/docs/internals/builtins/type/is_null.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/builtins/types/is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_null.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1591](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1591) (`lower_is_null_builtin`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1607](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1607) (`lower_is_null_builtin`)
- **Function symbol**: `lower_is_null_builtin()`
diff --git a/docs/internals/builtins/type/is_object.md b/docs/internals/builtins/type/is_object.md
index ac062b6fc5..f38cc47b3a 100644
--- a/docs/internals/builtins/type/is_object.md
+++ b/docs/internals/builtins/type/is_object.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/builtins/types/is_object.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_object.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1616](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1616) (`lower_is_object`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1632](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1632) (`lower_is_object`)
- **Function symbol**: `lower_is_object()`
diff --git a/docs/internals/builtins/type/is_scalar.md b/docs/internals/builtins/type/is_scalar.md
index c2f88a9dea..d2e39f2879 100644
--- a/docs/internals/builtins/type/is_scalar.md
+++ b/docs/internals/builtins/type/is_scalar.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/builtins/types/is_scalar.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_scalar.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1632](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1632) (`lower_is_scalar`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1648](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1648) (`lower_is_scalar`)
- **Function symbol**: `lower_is_scalar()`
diff --git a/docs/internals/builtins/type/is_string.md b/docs/internals/builtins/type/is_string.md
index 1954c30c4a..365820d012 100644
--- a/docs/internals/builtins/type/is_string.md
+++ b/docs/internals/builtins/type/is_string.md
@@ -10,7 +10,7 @@ sidebar:
## Where it lives
- **Signature**: [`src/builtins/types/is_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_string.rs)
-- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1334](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1334) (`lower_static_type_predicate`)
+- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1350](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1350) (`lower_static_type_predicate`)
- **Function symbol**: `lower_static_type_predicate()`
diff --git a/scripts/docs/builtin_registry.json b/scripts/docs/builtin_registry.json
index ffb7021d8d..8a8100caa7 100644
--- a/scripts/docs/builtin_registry.json
+++ b/scripts/docs/builtin_registry.json
@@ -4784,7 +4784,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_boolval",
- "codegen_line": 1174,
+ "codegen_line": 1190,
"notes": [
"Lowers `boolval()` using the same concrete scalar PHP truthiness rules as `IsTruthy`."
],
@@ -7717,7 +7717,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_empty",
- "codegen_line": 1216,
+ "codegen_line": 1232,
"notes": [
"Lowers `empty()` for concrete scalar and array-like operands."
],
@@ -9476,7 +9476,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_floatval",
- "codegen_line": 1140,
+ "codegen_line": 1156,
"notes": [
"Lowers `floatval()` for concrete scalar operands."
],
@@ -14509,7 +14509,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_intval",
- "codegen_line": 1107,
+ "codegen_line": 1123,
"notes": [
"Lowers `intval()` for concrete scalar operands."
],
@@ -14718,7 +14718,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_is_array",
- "codegen_line": 1601,
+ "codegen_line": 1617,
"notes": [
"Lowers `is_array()`: true for statically-known arrays/hashes, or a boxed Mixed/Union value",
"whose runtime tag is an indexed (4) or associative (5) array. An `iterable`-typed value is",
@@ -14779,7 +14779,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_static_type_predicate",
- "codegen_line": 1334,
+ "codegen_line": 1350,
"notes": [
"Lowers a static `is_*` predicate for concrete non-Mixed values."
],
@@ -15161,7 +15161,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_static_type_predicate",
- "codegen_line": 1334,
+ "codegen_line": 1350,
"notes": [
"Lowers a static `is_*` predicate for concrete non-Mixed values."
],
@@ -15279,7 +15279,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_static_type_predicate",
- "codegen_line": 1334,
+ "codegen_line": 1350,
"notes": [
"Lowers a static `is_*` predicate for concrete non-Mixed values."
],
@@ -15338,7 +15338,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_is_iterable",
- "codegen_line": 1392,
+ "codegen_line": 1408,
"notes": [
"Lowers `is_iterable()` for concrete values and boxed Mixed payloads."
],
@@ -15520,7 +15520,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_is_null_builtin",
- "codegen_line": 1591,
+ "codegen_line": 1607,
"notes": [
"Lowers `is_null()` for concrete scalar values and boxed Mixed payloads."
],
@@ -15638,7 +15638,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_is_object",
- "codegen_line": 1616,
+ "codegen_line": 1632,
"notes": [
"Lowers `is_object()`: true for statically-known objects, or a boxed Mixed/Union value whose",
"runtime tag is an object (6)."
@@ -15820,7 +15820,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_is_scalar",
- "codegen_line": 1632,
+ "codegen_line": 1648,
"notes": [
"Lowers `is_scalar()`: true for int/float/string/bool, a non-null tagged scalar, or a boxed",
"Mixed/Union value whose runtime tag is int (0), string (1), float (2), or bool (3). Null,",
@@ -15881,7 +15881,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_static_type_predicate",
- "codegen_line": 1334,
+ "codegen_line": 1350,
"notes": [
"Lowers a static `is_*` predicate for concrete non-Mixed values."
],
@@ -27868,7 +27868,7 @@
"checker_line": null,
"codegen_file": "src/codegen/lower_inst/builtins.rs",
"codegen_function": "lower_strlen",
- "codegen_line": 1077,
+ "codegen_line": 1093,
"notes": [
"Lowers `strlen()` by coercing string-like values and returning the byte length."
],
diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs
index afe4ed6538..4dff94df3c 100644
--- a/src/codegen/lower_inst.rs
+++ b/src/codegen/lower_inst.rs
@@ -40,6 +40,7 @@ mod callables;
mod comparisons;
mod conversions;
mod enums;
+mod exceptions;
mod externs;
mod floats;
mod hashes;
@@ -151,7 +152,8 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId)
Op::ArrayToHash => arrays::lower_array_to_hash(ctx, &inst),
Op::HashNew => hashes::lower_hash_new(ctx, &inst),
Op::HashLen => hashes::lower_hash_len(ctx, &inst),
- Op::HashGet => hashes::lower_hash_get(ctx, &inst),
+ Op::HashGet => hashes::lower_hash_get(ctx, &inst, true),
+ Op::HashGetSilent => hashes::lower_hash_get(ctx, &inst, false),
Op::HashIsset => builtins::lower_hash_isset(ctx, &inst),
Op::HashSet => hashes::lower_hash_set(ctx, &inst),
Op::HashUnset => hashes::lower_hash_unset(ctx, &inst),
@@ -234,6 +236,8 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId)
Op::EchoValue => lower_echo_value(ctx, &inst),
Op::PrintValue => lower_print_value(ctx, &inst),
Op::ThrowException => lower_throw_exception(ctx, &inst),
+ Op::ThrowError => lower_throw_error(ctx, &inst),
+ Op::ThrowErrorValue => lower_throw_error_value(ctx, &inst),
Op::TryPushHandler => lower_try_push_handler(ctx, &inst),
Op::TryPopHandler => lower_try_pop_handler(ctx, &inst),
Op::CatchCurrent => lower_catch_current(ctx, &inst),
@@ -2787,6 +2791,32 @@ fn lower_throw_exception(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R
super::lower_term::lower_throw_value(ctx, value)
}
+/// Lowers a static-message catchable PHP `Error` without evaluating later operands.
+fn lower_throw_error(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> {
+ if !inst.operands.is_empty() {
+ return Err(CodegenIrError::invalid_module(format!(
+ "{} expects no operands",
+ inst.op.name()
+ )));
+ }
+ let data = expect_data(inst)?;
+ let message = ctx
+ .module
+ .data
+ .strings
+ .get(data.as_raw() as usize)
+ .ok_or_else(|| CodegenIrError::missing_entry("data string", data.as_raw()))?
+ .clone();
+ exceptions::emit_error(ctx, &message);
+ Ok(())
+}
+
+/// Lowers a runtime-string catchable PHP `Error` without evaluating later operands.
+fn lower_throw_error_value(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> {
+ let message = expect_operand(inst, 0)?;
+ exceptions::emit_error_value(ctx, message)
+}
+
/// Pushes an EIR exception handler and branches to the handler block after `longjmp`.
fn lower_try_push_handler(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> {
let token = expect_i64(inst)?;
@@ -2899,6 +2929,7 @@ fn lower_method_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Resul
object_ty
)));
};
+ guard_static_method_receiver(ctx, object, &method_name)?;
if let Some(state) = fiber_state_predicate(&class_name, &method_name) {
return lower_fiber_state_predicate(ctx, inst, object, state);
}
@@ -2964,6 +2995,30 @@ fn lower_method_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Resul
emit_ref_arg_writebacks(ctx, &call_args.ref_writebacks)
}
+/// Rejects the raw null-container representation before a static object method dispatch.
+fn guard_static_method_receiver(
+ ctx: &mut FunctionContext<'_>,
+ object: ValueId,
+ method_name: &str,
+) -> Result<()> {
+ let receiver_reg = abi::symbol_scratch_reg(ctx.emitter);
+ let scratch_reg = abi::secondary_scratch_reg(ctx.emitter);
+ let null_label = ctx.next_label("static_method_receiver_null");
+ let done_label = ctx.next_label("static_method_receiver_checked");
+ ctx.load_value_to_reg(object, receiver_reg)?;
+ crate::codegen::sentinels::emit_branch_if_null_container(
+ ctx.emitter,
+ receiver_reg,
+ scratch_reg,
+ &null_label,
+ );
+ 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);
+ Ok(())
+}
+
/// Lowers an instance-method call whose receiver is boxed as `Mixed`.
fn lower_mixed_method_call(
ctx: &mut FunctionContext<'_>,
@@ -3353,31 +3408,10 @@ fn lower_nullable_receiver_interface_method_call(
/// Emits PHP's fatal diagnostic for calling an instance method on null.
fn emit_method_call_on_null_fatal(ctx: &mut FunctionContext<'_>, method_name: &str) {
- let message = format!(
- "Fatal error: Call to a member function {}() on null\n",
- method_name
+ exceptions::emit_error(
+ ctx,
+ &format!("Call to a member function {}() on null", method_name),
);
- let (message_label, message_len) = ctx.data.add_string(message.as_bytes());
- match ctx.emitter.target.arch {
- Arch::AArch64 => {
- ctx.emitter.instruction("mov x0, #2"); // write the member-call-on-null fatal to stderr
- ctx.emitter.adrp("x1", &message_label);
- ctx.emitter.add_lo12("x1", "x1", &message_label);
- ctx.emitter
- .instruction(&format!("mov x2, #{}", message_len)); // pass the member-call-on-null fatal byte length
- ctx.emitter.syscall(4);
- abi::emit_exit(ctx.emitter, 1);
- }
- Arch::X86_64 => {
- ctx.emitter.instruction("mov edi, 2"); // write the member-call-on-null fatal to Linux stderr
- abi::emit_symbol_address(ctx.emitter, "rsi", &message_label);
- ctx.emitter
- .instruction(&format!("mov edx, {}", message_len)); // pass the member-call-on-null fatal byte length
- ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write
- ctx.emitter.instruction("syscall"); // emit the member-call-on-null fatal before exiting
- abi::emit_exit(ctx.emitter, 1);
- }
- }
}
/// Returns the direct runtime intrinsic for built-in `Generator` instance methods.
diff --git a/src/codegen/lower_inst/arrays.rs b/src/codegen/lower_inst/arrays.rs
index d649f0c1e1..7185e22ada 100644
--- a/src/codegen/lower_inst/arrays.rs
+++ b/src/codegen/lower_inst/arrays.rs
@@ -58,7 +58,23 @@ pub(super) fn lower_array_len(ctx: &mut FunctionContext<'_>, inst: &Instruction)
let array = expect_operand(inst, 0)?;
require_indexed_array(ctx.load_value_to_result(array)?, inst)?;
let result_reg = abi::int_result_reg(ctx.emitter);
+ let null_label = ctx.next_label("array_len_null");
+ let done_label = ctx.next_label("array_len_done");
+ let scratch_reg = abi::secondary_scratch_reg(ctx.emitter);
+ crate::codegen::sentinels::emit_branch_if_null_container(
+ ctx.emitter,
+ result_reg,
+ scratch_reg,
+ &null_label,
+ );
abi::emit_load_from_address(ctx.emitter, result_reg, result_reg, 0);
+ abi::emit_jump(ctx.emitter, &done_label);
+ ctx.emitter.label(&null_label);
+ super::exceptions::emit_error(
+ ctx,
+ "Only arrays and Traversables can be unpacked, null given",
+ );
+ ctx.emitter.label(&done_label);
store_if_result(ctx, inst)
}
@@ -616,8 +632,17 @@ fn lower_array_get_aarch64(
ctx.load_value_to_reg(index, result_reg)?;
ctx.load_value_to_reg(array, array_reg)?;
let null_label = ctx.next_label("array_get_null");
+ let null_receiver_label = ctx.next_label("array_get_null_recv");
+ let fallback_label = ctx.next_label("array_get_fallback");
let done_label = ctx.next_label("array_get_done");
+ // -- guard the receiver: a missed outer read carries a null/sentinel container --
+ crate::codegen::sentinels::emit_branch_if_null_container(
+ ctx.emitter,
+ array_reg,
+ len_reg,
+ &null_receiver_label,
+ );
ctx.emitter.instruction(&format!("cmp {}, #0", result_reg)); // check whether the indexed-array offset is negative
ctx.emitter.instruction(&format!("b.lt {}", null_label)); // negative indexed-array offsets read as null
abi::emit_load_from_address(ctx.emitter, len_reg, array_reg, 0);
@@ -629,6 +654,12 @@ fn lower_array_get_aarch64(
if warn_on_missing {
emit_undefined_array_key_warning(ctx);
}
+ abi::emit_jump(ctx.emitter, &fallback_label);
+ ctx.emitter.label(&null_receiver_label);
+ if warn_on_missing {
+ emit_array_offset_on_null_warning(ctx);
+ }
+ ctx.emitter.label(&fallback_label);
emit_array_get_null_fallback(ctx, result_ty);
ctx.emitter.label(&done_label);
store_if_result(ctx, inst)
@@ -695,8 +726,17 @@ fn lower_array_get_x86_64(
ctx.load_value_to_reg(array, array_reg)?;
ctx.load_value_to_reg(index, result_reg)?;
let null_label = ctx.next_label("array_get_null");
+ let null_receiver_label = ctx.next_label("array_get_null_recv");
+ let fallback_label = ctx.next_label("array_get_fallback");
let done_label = ctx.next_label("array_get_done");
+ // -- guard the receiver: a missed outer read carries a null/sentinel container --
+ crate::codegen::sentinels::emit_branch_if_null_container(
+ ctx.emitter,
+ array_reg,
+ len_reg,
+ &null_receiver_label,
+ );
ctx.emitter.instruction(&format!("cmp {}, 0", result_reg)); // check whether the indexed-array offset is negative
ctx.emitter.instruction(&format!("jl {}", null_label)); // negative indexed-array offsets read as null
abi::emit_load_from_address(ctx.emitter, len_reg, array_reg, 0);
@@ -708,6 +748,12 @@ fn lower_array_get_x86_64(
if warn_on_missing {
emit_undefined_array_key_warning(ctx);
}
+ abi::emit_jump(ctx.emitter, &fallback_label);
+ ctx.emitter.label(&null_receiver_label);
+ if warn_on_missing {
+ emit_array_offset_on_null_warning(ctx);
+ }
+ ctx.emitter.label(&fallback_label);
emit_array_get_null_fallback(ctx, result_ty);
ctx.emitter.label(&done_label);
store_if_result(ctx, inst)
@@ -986,8 +1032,13 @@ fn emit_undefined_array_key_warning(ctx: &mut FunctionContext<'_>) {
abi::emit_call_label(ctx.emitter, "__rt_warn_undefined_array_key_int");
}
+/// Emits PHP's warning for a direct array-offset read whose receiver is null.
+pub(super) fn emit_array_offset_on_null_warning(ctx: &mut FunctionContext<'_>) {
+ abi::emit_call_label(ctx.emitter, "__rt_warn_array_offset_on_null");
+}
+
/// Emits the null/miss fallback in the result shape expected by the array element type.
-fn emit_array_get_null_fallback(ctx: &mut FunctionContext<'_>, elem_ty: &PhpType) {
+pub(super) fn emit_array_get_null_fallback(ctx: &mut FunctionContext<'_>, elem_ty: &PhpType) {
match elem_ty {
PhpType::TaggedScalar => {
crate::codegen::sentinels::emit_tagged_scalar_null(ctx.emitter);
@@ -1002,7 +1053,11 @@ fn emit_array_get_null_fallback(ctx: &mut FunctionContext<'_>, elem_ty: &PhpType
},
PhpType::Str => {
let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter);
- abi::emit_load_int_immediate(ctx.emitter, ptr_reg, 0);
+ abi::emit_load_int_immediate(
+ ctx.emitter,
+ ptr_reg,
+ crate::codegen::NULL_SENTINEL,
+ );
abi::emit_load_int_immediate(ctx.emitter, len_reg, 0);
}
PhpType::Mixed => match ctx.emitter.target.arch {
diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs
index f189924150..7b99d7c392 100644
--- a/src/codegen/lower_inst/builtins.rs
+++ b/src/codegen/lower_inst/builtins.rs
@@ -1028,7 +1028,23 @@ pub(crate) fn lower_count(ctx: &mut FunctionContext<'_>, inst: &Instruction) ->
PhpType::Array(_) | PhpType::AssocArray { .. } => {
ctx.load_value_to_result(value)?;
let result_reg = abi::int_result_reg(ctx.emitter);
+ let null_label = ctx.next_label("count_null_container");
+ let done_label = ctx.next_label("count_done");
+ let scratch_reg = abi::secondary_scratch_reg(ctx.emitter);
+ crate::codegen::sentinels::emit_branch_if_null_container(
+ ctx.emitter,
+ result_reg,
+ scratch_reg,
+ &null_label,
+ );
abi::emit_load_from_address(ctx.emitter, result_reg, result_reg, 0);
+ abi::emit_jump(ctx.emitter, &done_label);
+ ctx.emitter.label(&null_label);
+ super::exceptions::emit_type_error(
+ ctx,
+ "count(): Argument #1 ($value) must be of type Countable|array, null given",
+ );
+ ctx.emitter.label(&done_label);
store_if_result(ctx, inst)
}
PhpType::Mixed | PhpType::Union(_) => {
diff --git a/src/codegen/lower_inst/builtins/isset.rs b/src/codegen/lower_inst/builtins/isset.rs
index c5a3b4acc7..09161a2665 100644
--- a/src/codegen/lower_inst/builtins/isset.rs
+++ b/src/codegen/lower_inst/builtins/isset.rs
@@ -74,7 +74,9 @@ fn emit_isset_missing_result(ctx: &mut FunctionContext<'_>, value: ValueId) -> R
if let Some(inst) = source_instruction(ctx, value)? {
match inst.op {
Op::StrCharAt => return emit_isset_string_offset_missing_result(ctx, &inst),
- Op::ArrayGet => return emit_isset_array_offset_missing_result(ctx, &inst),
+ Op::ArrayGet | Op::ArrayGetSilent => {
+ return emit_isset_array_offset_missing_result(ctx, &inst)
+ }
Op::HashGet => return emit_isset_hash_offset_missing_result(ctx, &inst),
_ => {}
}
@@ -317,6 +319,8 @@ fn emit_isset_array_offset_missing_aarch64(
let result_reg = abi::int_result_reg(ctx.emitter);
ctx.load_value_to_reg(index, result_reg)?;
ctx.load_value_to_reg(array, array_reg)?;
+ // -- guard the receiver: a missed outer read carries a null/sentinel container --
+ crate::codegen::sentinels::emit_branch_if_null_container(ctx.emitter, array_reg, len_reg, missing);
ctx.emitter.instruction(&format!("cmp {}, #0", result_reg)); // reject negative indexes as missing array elements
ctx.emitter.instruction(&format!("b.lt {}", missing)); // missing indexes make isset return false
abi::emit_load_from_address(ctx.emitter, len_reg, array_reg, 0);
@@ -344,6 +348,8 @@ fn emit_isset_array_offset_missing_x86_64(
let result_reg = abi::int_result_reg(ctx.emitter);
ctx.load_value_to_reg(array, array_reg)?;
ctx.load_value_to_reg(index, result_reg)?;
+ // -- guard the receiver: a missed outer read carries a null/sentinel container --
+ crate::codegen::sentinels::emit_branch_if_null_container(ctx.emitter, array_reg, len_reg, missing);
ctx.emitter.instruction(&format!("cmp {}, 0", result_reg)); // reject negative indexes as missing array elements
ctx.emitter.instruction(&format!("jl {}", missing)); // missing indexes make isset return false
abi::emit_load_from_address(ctx.emitter, len_reg, array_reg, 0);
@@ -467,17 +473,22 @@ fn require_isset_integer_like_index(
}
}
-/// Returns the instruction that produced an SSA value, when it has one.
-fn source_instruction(ctx: &FunctionContext<'_>, value: ValueId) -> Result