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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 101 additions & 17 deletions src/codegen/lower_inst/builtins/arrays.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2024,7 +2024,8 @@ fn lower_user_sort_static_callback(
super::ensure_arg_count(inst, name, 2)?;
let array = expect_operand(inst, 0)?;
let callback = expect_operand(inst, 1)?;
user_sort_element_type(ctx.value_php_type(array)?, name)?;
let elem_ty = user_sort_element_type(ctx.value_php_type(array)?, name)?;
let callback_arg_types = [elem_ty.clone(), elem_ty];
let source_local = source_load_local_slot(ctx, array)?;
ensure_unique_sort_source(ctx, array)?;
if let Some(slot) = source_local {
Expand All @@ -2037,7 +2038,7 @@ fn lower_user_sort_static_callback(
ctx,
callback,
&callback_owner,
Some(&[PhpType::Int, PhpType::Int]),
Some(&callback_arg_types),
)?;
return lower_user_sort_with_static_callback_binding(ctx, inst, array, callback_binding);
}
Expand All @@ -2046,7 +2047,7 @@ fn lower_user_sort_static_callback(
lower_descriptor_callback_runtime(
ctx,
callback,
vec![PhpType::Int, PhpType::Int],
callback_arg_types.to_vec(),
PhpType::Int,
|ctx, wrapper_label, env_bytes| {
let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0);
Expand All @@ -2071,8 +2072,8 @@ fn lower_user_sort_static_callback(
lower_runtime_string_descriptor_callback(
ctx,
callback,
Some(&PhpType::Array(Box::new(PhpType::Int))),
vec![PhpType::Int, PhpType::Int],
Some(&PhpType::Array(Box::new(callback_arg_types[0].clone()))),
callback_arg_types.to_vec(),
PhpType::Int,
name,
|ctx, wrapper_label, env_bytes| {
Expand Down Expand Up @@ -2100,7 +2101,7 @@ fn lower_user_sort_static_callback(
ctx,
callback,
&callback_owner,
Some(&[PhpType::Int, PhpType::Int]),
Some(&callback_arg_types),
)?;
lower_user_sort_with_static_callback_binding(ctx, inst, array, callback_binding)
}
Expand Down Expand Up @@ -2265,18 +2266,23 @@ fn indexed_sort_element_type(ty: PhpType, name: &str, allow_strings: bool) -> Re
///
/// User-comparator sorts (`usort`/`uasort`/`uksort`) permute existing
/// pointer-sized slots through `__rt_usort`, so integer and object/refcounted
/// handles (each a single 8-byte payload) are sortable; the comparator decides
/// the ordering and receives each element by its handle. String elements are
/// rejected here exactly as before — their multi-word descriptors are not
/// permuted by the 8-byte slot sorter — so they keep producing a clear
/// unsupported-feature error rather than a corrupt sort.
/// handles and boxed `Mixed` cells (each a single 8-byte payload) are sortable;
/// the comparator decides the ordering and receives each element through an ABI
/// adapter when the runtime slot type differs from its declared parameters.
/// String elements are rejected here exactly as before — their multi-word
/// descriptors are not permuted by the 8-byte slot sorter — so they keep
/// producing a clear unsupported-feature error rather than a corrupt sort.
fn user_sort_element_type(ty: PhpType, name: &str) -> Result<PhpType> {
match ty.codegen_repr() {
PhpType::Array(elem) => {
let elem = elem.codegen_repr();
if matches!(
elem,
PhpType::Int | PhpType::Void | PhpType::Never | PhpType::Object(_)
PhpType::Int
| PhpType::Void
| PhpType::Never
| PhpType::Mixed
| PhpType::Object(_)
) {
return Ok(elem);
}
Expand Down Expand Up @@ -2674,11 +2680,31 @@ fn static_sort_callback_binding(
Some(callback) => callback,
None => static_callback_name_operand(ctx, value, owner)?,
};
if let Some(callee) = ctx.callable_function_by_name(&callback.name) {
if let Some((label, param_types, return_ty)) = ctx
.callable_function_by_name(&callback.name)
.map(|callee| {
(
function_symbol(&callee.name),
callee
.params
.iter()
.map(|param| param.php_type.codegen_repr())
.collect::<Vec<_>>(),
callee.return_php_type.codegen_repr(),
)
})
{
let (label, env_source) = adapt_direct_callback_visible_args(
ctx,
label,
&param_types,
visible_arg_types,
owner,
)?;
return Ok(StaticSortCallbackBinding {
label: function_symbol(&callee.name),
env_source: None,
return_ty: callee.return_php_type.codegen_repr(),
label,
env_source,
return_ty,
});
}
if callback.kind == StaticCallbackOperandKind::FirstClassCallable {
Expand Down Expand Up @@ -2713,6 +2739,56 @@ fn static_sort_callback_binding(
)))
}

/// Adapts boxed runtime callback arguments to a direct callback's declared ABI types.
fn adapt_direct_callback_visible_args(
ctx: &mut FunctionContext<'_>,
target_label: String,
target_param_types: &[PhpType],
visible_arg_types: Option<&[PhpType]>,
owner: &str,
) -> Result<(String, Option<StaticCallbackEnvSource>)> {
let Some(visible_arg_types) = visible_arg_types else {
return Ok((target_label, None));
};
if visible_arg_types
.iter()
.map(PhpType::codegen_repr)
.eq(target_param_types.iter().map(PhpType::codegen_repr))
{
return Ok((target_label, None));
}
if !visible_arg_types
.iter()
.any(|ty| matches!(ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)))
{
return Ok((target_label, None));
}
if visible_arg_types.len() != target_param_types.len() {
return Err(CodegenIrError::unsupported(format!(
"{} with runtime callback args {:?} for direct target params {:?}",
owner, visible_arg_types, target_param_types
)));
}

let wrapper_label = ctx.next_label("direct_callback_arg_adapter");
let done_label = ctx.next_label("direct_callback_after_arg_adapter");
let wrapper = DeferredCallbackWrapper {
label: wrapper_label.clone(),
visible_arg_types: visible_arg_types.to_vec(),
target_visible_arg_types: Some(target_param_types.to_vec()),
capture_types: Vec::new(),
descriptor_prefix_types: Vec::new(),
descriptor_return_type: None,
};
abi::emit_jump(ctx.emitter, &done_label);
crate::codegen::emit_callback_wrapper(ctx.emitter, &wrapper);
ctx.emitter.label(&done_label);
Ok((
wrapper_label,
Some(StaticCallbackEnvSource::FunctionLabel(target_label)),
))
}

/// Recovers a static `[class, method]` callable array as a static-method callback name.
fn static_callable_array_callback_name(
ctx: &FunctionContext<'_>,
Expand Down Expand Up @@ -3205,11 +3281,12 @@ enum StaticCallbackCalledClass {
}

/// Source used by the sort call site to build the callback environment.
#[derive(Clone, Copy)]
#[derive(Clone)]
enum StaticCallbackEnvSource {
Local(LocalSlotId),
ThisObject(LocalSlotId),
Value(ValueId),
FunctionLabel(String),
}

/// Resolves a sort static-method callback, allowing `static::` with an environment.
Expand Down Expand Up @@ -4045,6 +4122,13 @@ fn reserve_static_callback_env(
)));
}
}
StaticCallbackEnvSource::FunctionLabel(label) => {
abi::emit_symbol_address(
ctx.emitter,
abi::int_result_reg(ctx.emitter),
&label,
);
}
}
match ctx.emitter.target.arch {
Arch::AArch64 => {
Expand Down
64 changes: 58 additions & 6 deletions src/codegen/lower_inst/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2666,7 +2666,6 @@ fn lower_mixed_load_prop_ref_cell(
}
let done_label = ctx.next_label("mixed_propref_done");
let null_label = ctx.next_label("mixed_propref_null");
let stdclass_label = ctx.next_label("mixed_propref_stdclass");
let match_labels = candidates
.iter()
.map(|candidate| {
Expand All @@ -2677,9 +2676,14 @@ fn lower_mixed_load_prop_ref_cell(
ctx.load_value_to_reg(object, abi::int_result_reg(ctx.emitter))?;
abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox");
emit_mixed_object_payload_or_null(ctx, &null_label);
// stdClass has no declared reference property; route it to the null fallback.
emit_mixed_property_class_dispatch(ctx, &candidates, &match_labels, &null_label);
let _ = stdclass_label;
// stdClass and classes without this reference property have no matching cell.
emit_mixed_property_class_dispatch(
ctx,
&candidates,
&match_labels,
&null_label,
&null_label,
);

let int_reg = abi::int_result_reg(ctx.emitter);
for (candidate, label) in candidates.iter().zip(match_labels.iter()) {
Expand Down Expand Up @@ -2920,6 +2924,7 @@ fn lower_declared_mixed_prop_get(
candidates: Vec<MixedPropertyCandidate>,
) -> Result<()> {
let null_label = ctx.next_label("mixed_prop_null");
let miss_label = ctx.next_label("mixed_prop_miss");
let done_label = ctx.next_label("mixed_prop_done");
let stdclass_label = ctx.next_label("mixed_prop_stdclass");
let match_labels = candidates
Expand All @@ -2935,7 +2940,13 @@ fn lower_declared_mixed_prop_get(
ctx.load_value_to_reg(object, abi::int_result_reg(ctx.emitter))?;
abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox");
emit_mixed_object_payload_or_null(ctx, &null_label);
emit_mixed_property_class_dispatch(ctx, &candidates, &match_labels, &stdclass_label);
emit_mixed_property_class_dispatch(
ctx,
&candidates,
&match_labels,
&stdclass_label,
&miss_label,
);

for (candidate, label) in candidates.iter().zip(match_labels.iter()) {
ctx.emitter.label(label);
Expand All @@ -2952,6 +2963,11 @@ fn lower_declared_mixed_prop_get(
emit_stdclass_get_from_loaded_object(ctx, property);
abi::emit_jump(ctx.emitter, &done_label);

ctx.emitter.label(&miss_label);
emit_undefined_property_warning_for_loaded_object(ctx, property);
emit_boxed_null(ctx);
abi::emit_jump(ctx.emitter, &done_label);

ctx.emitter.label(&null_label);
emit_boxed_null(ctx);

Expand Down Expand Up @@ -3029,12 +3045,13 @@ fn emit_mixed_object_payload_or_null(ctx: &mut FunctionContext<'_>, null_label:
}
}

/// Emits class-id dispatch for declared property candidates and stdClass fallback.
/// Emits class-id dispatch for declared property candidates, stdClass, and a real miss branch.
fn emit_mixed_property_class_dispatch(
ctx: &mut FunctionContext<'_>,
candidates: &[MixedPropertyCandidate],
match_labels: &[String],
stdclass_label: &str,
miss_label: &str,
) {
match ctx.emitter.target.arch {
Arch::AArch64 => {
Expand All @@ -3045,6 +3062,7 @@ fn emit_mixed_property_class_dispatch(
ctx.emitter.instruction(&format!("b.eq {}", label)); // read the declared property when the class id matches
}
emit_branch_to_stdclass_candidate(ctx, "x9", "x10", stdclass_label);
abi::emit_jump(ctx.emitter, miss_label);
}
Arch::X86_64 => {
ctx.emitter.instruction("mov r11, QWORD PTR [rax]"); // load the receiver class id for Mixed property dispatch
Expand All @@ -3054,6 +3072,7 @@ fn emit_mixed_property_class_dispatch(
ctx.emitter.instruction(&format!("je {}", label)); // read the declared property when the class id matches
}
emit_branch_to_stdclass_candidate(ctx, "r11", "r10", stdclass_label);
abi::emit_jump(ctx.emitter, miss_label);
}
}
}
Expand Down Expand Up @@ -3197,6 +3216,39 @@ fn emit_property_on_null_warning(ctx: &mut FunctionContext<'_>, property: &str)
abi::emit_call_label(ctx.emitter, "__rt_diag_warning");
}

/// Emits `Warning: Undefined property: Class::$name` for an object already in the result register.
fn emit_undefined_property_warning_for_loaded_object(
ctx: &mut FunctionContext<'_>,
property: &str,
) {
let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter);
match ctx.emitter.target.arch {
Arch::AArch64 => {
ctx.emitter.instruction("ldr x9, [x0]"); // load the missing-property receiver class id
abi::emit_symbol_address(ctx.emitter, "x10", "_class_name_entries");
ctx.emitter.instruction("lsl x11, x9, #4"); // scale the class id to the 16-byte class-name row
ctx.emitter.instruction("add x10, x10, x11"); // address the receiver's class-name metadata
ctx.emitter.instruction("ldr x1, [x10]"); // load the receiver class-name pointer
ctx.emitter.instruction("ldr x2, [x10, #8]"); // load the receiver class-name byte length
}
Arch::X86_64 => {
ctx.emitter.instruction("mov r9, QWORD PTR [rax]"); // load the missing-property receiver class id
abi::emit_symbol_address(ctx.emitter, "r10", "_class_name_entries");
ctx.emitter.instruction("shl r9, 4"); // scale the class id to the 16-byte class-name row
ctx.emitter.instruction("mov rax, QWORD PTR [r10 + r9]"); // load the receiver class-name pointer
ctx.emitter.instruction("mov rdx, QWORD PTR [r10 + r9 + 8]"); // load the receiver class-name byte length
}
}
abi::emit_push_reg_pair(ctx.emitter, ptr_reg, len_reg);
emit_property_warning_fragment(ctx, b"Warning: Undefined property: ");
match ctx.emitter.target.arch {
Arch::AArch64 => abi::emit_pop_reg_pair(ctx.emitter, "x1", "x2"),
Arch::X86_64 => abi::emit_pop_reg_pair(ctx.emitter, "rdi", "rsi"),
}
abi::emit_call_label(ctx.emitter, "__rt_diag_warning");
emit_property_warning_fragment(ctx, format!("::${}\n", property).as_bytes());
}

/// Lowers a nullsafe declared-property read for nullable object receivers.
pub(super) fn lower_nullsafe_prop_get(
ctx: &mut FunctionContext<'_>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ fn contextual_closure_sig(
&format!("Closure parameter ${}", name),
)?;
let specialized_ty =
Checker::specialize_generic_array_hint(&declared_ty, actual_ty);
Checker::specialize_generic_array_param_hint(&declared_ty, actual_ty);
(specialized_ty.clone(), specialized_ty, true)
} else {
(declared_ty.clone(), declared_ty, true)
Expand Down
3 changes: 2 additions & 1 deletion src/types/checker/functions/resolution/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,8 @@ impl Checker {
arg.span,
&format!("Function '{}' parameter ${}", name, param_name),
)?;
let specialized_ty = Self::specialize_generic_array_hint(&declared_ty, &ty);
let specialized_ty =
Self::specialize_generic_array_param_hint(&declared_ty, &ty);
param_types.push((decl.params[arg_idx].clone(), specialized_ty));
arg_idx += 1;
continue;
Expand Down
9 changes: 6 additions & 3 deletions src/types/checker/inference/objects/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,8 @@ impl Checker {
// Sharpen a declared generic `array` parameter to the call-site array
// shape so method `array` params keep their associative shape, matching
// how free-function `array` parameters are specialized (issue #406).
sig.params[i].1 = Self::specialize_generic_array_hint(&sig.params[i].1, arg_ty);
sig.params[i].1 =
Self::specialize_generic_array_param_hint(&sig.params[i].1, arg_ty);
}
if i < regular_param_count
&& !declared_flags.get(i).copied().unwrap_or(false)
Expand Down Expand Up @@ -1076,7 +1077,8 @@ impl Checker {
// Sharpen a declared generic `array` parameter to the call-site array
// shape so static-method `array` params keep their associative shape,
// matching free-function specialization (issue #406).
sig.params[i].1 = Self::specialize_generic_array_hint(&sig.params[i].1, arg_ty);
sig.params[i].1 =
Self::specialize_generic_array_param_hint(&sig.params[i].1, arg_ty);
}
if i < regular_param_count
&& !static_declared_flags.get(i).copied().unwrap_or(false)
Expand Down Expand Up @@ -1145,7 +1147,8 @@ impl Checker {
// Sharpen a declared generic `array` parameter to the call-site array
// shape on `parent::`/`self::` instance dispatch, matching free-function
// specialization (issue #406).
sig.params[i].1 = Self::specialize_generic_array_hint(&sig.params[i].1, arg_ty);
sig.params[i].1 =
Self::specialize_generic_array_param_hint(&sig.params[i].1, arg_ty);
}
if i < regular_param_count
&& !instance_declared_flags.get(i).copied().unwrap_or(false)
Expand Down
4 changes: 3 additions & 1 deletion src/types/checker/method_pass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ impl Checker {
PhpType::Array(_) | PhpType::AssocArray { .. }
)
})
.map(|t| Self::specialize_generic_array_hint(&declared, &t))
.map(|t| {
Self::specialize_generic_array_param_hint(&declared, &t)
})
.unwrap_or(declared)
} else {
declared
Expand Down
Loading
Loading