Skip to content
Draft
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
288 changes: 288 additions & 0 deletions src/codegen/runtime/arrays/array_get_mixed_key.rs

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/codegen/runtime/arrays/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ mod array_push_str;
mod array_set_int;
mod array_set_mixed;
mod array_set_mixed_key;
mod array_get_mixed_key;
mod array_set_refcounted;
mod array_set_str;
mod array_rand;
Expand Down Expand Up @@ -254,6 +255,8 @@ pub use array_set_int::emit_array_set_int;
pub use array_set_mixed::emit_array_set_mixed;
/// Emit boxed-Mixed-key indexed/hash array set helper.
pub use array_set_mixed_key::emit_array_set_mixed_key;
/// Emit boxed-Mixed-key indexed/hash array get helper.
pub use array_get_mixed_key::emit_array_get_mixed_key;
/// Emit refcounted indexed-array set helper.
pub use array_set_refcounted::emit_array_set_refcounted;
/// Emit refcounted indexed-array set helper.
Expand Down
1 change: 1 addition & 0 deletions src/codegen/runtime/emitters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) {
arrays::emit_array_set_int(emitter);
arrays::emit_array_set_mixed(emitter);
arrays::emit_array_set_mixed_key(emitter);
arrays::emit_array_get_mixed_key(emitter);
arrays::emit_array_set_refcounted(emitter);
arrays::emit_array_set_str(emitter);
arrays::emit_array_union(emitter);
Expand Down
1 change: 1 addition & 0 deletions src/codegen_ir/lower_inst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId)
Op::ArrayIsset => builtins::lower_array_isset(ctx, &inst),
Op::ArraySet => arrays::lower_array_set(ctx, &inst),
Op::ArraySetMixedKey => arrays::lower_array_set_mixed_key(ctx, &inst),
Op::ArrayGetMixedKey => arrays::lower_array_get_mixed_key(ctx, &inst),
Op::ArrayPush => arrays::lower_array_push(ctx, &inst),
Op::MixedArrayAppend => arrays::lower_mixed_array_append(ctx, &inst),
Op::ArrayUnion => arrays::lower_array_union(ctx, &inst),
Expand Down
54 changes: 54 additions & 0 deletions src/codegen_ir/lower_inst/arrays.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,60 @@ fn lower_array_set_mixed_key_x86_64(
Ok(())
}

/// Lowers a boxed-Mixed-key read from a statically `Array(Mixed)` indexed local.
///
/// The key tag is only known at runtime (PHP `foreach` keys are always `Mixed`
/// in EIR), so the read goes through `__rt_array_get_mixed_key`, which dispatches
/// on the destination runtime kind: an already-promoted hash reads through
/// `__rt_hash_get`, while an indexed array bounds-checks integer keys and returns
/// `Mixed(null)` for string keys. The result is an owned boxed `Mixed` cell.
pub(super) fn lower_array_get_mixed_key(
ctx: &mut FunctionContext<'_>,
inst: &Instruction,
) -> Result<()> {
let array = expect_operand(inst, 0)?;
let key = expect_operand(inst, 1)?;
require_indexed_array(ctx.value_php_type(array)?.codegen_repr(), inst)?;
let key_ty = ctx.value_php_type(key)?.codegen_repr();
if !matches!(key_ty, PhpType::Mixed | PhpType::Union(_)) {
return Err(CodegenIrError::unsupported(format!(
"array_get_mixed_key key PHP type {:?}",
key_ty
)));
}
match ctx.emitter.target.arch {
Arch::AArch64 => lower_array_get_mixed_key_aarch64(ctx, array, key)?,
Arch::X86_64 => lower_array_get_mixed_key_x86_64(ctx, array, key)?,
}
store_if_result(ctx, inst)
}

/// Loads the array and boxed Mixed key into argument registers on AArch64 and
/// calls `__rt_array_get_mixed_key`, leaving the owned Mixed result in x0.
fn lower_array_get_mixed_key_aarch64(
ctx: &mut FunctionContext<'_>,
array: ValueId,
key: ValueId,
) -> Result<()> {
ctx.load_value_to_reg(array, "x0")?;
ctx.load_value_to_reg(key, "x1")?;
abi::emit_call_label(ctx.emitter, "__rt_array_get_mixed_key");
Ok(())
}

/// Loads the array and boxed Mixed key into argument registers on x86_64 and
/// calls `__rt_array_get_mixed_key`, leaving the owned Mixed result in rax.
fn lower_array_get_mixed_key_x86_64(
ctx: &mut FunctionContext<'_>,
array: ValueId,
key: ValueId,
) -> Result<()> {
ctx.load_value_to_reg(array, "rdi")?;
ctx.load_value_to_reg(key, "rsi")?;
abi::emit_call_label(ctx.emitter, "__rt_array_get_mixed_key");
Ok(())
}

/// Lowers an indexed-array append through the runtime helper for the value type.
pub(super) fn lower_array_push(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> {
let array = expect_operand(inst, 0)?;
Expand Down
15 changes: 5 additions & 10 deletions src/image_prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1064,12 +1064,9 @@ function image_type_to_mime_type(int $image_type): string {
}
}

// Known limitation: PHP returns `string|false`, returning `false` for an unknown
// type. elephc currently collapses a string|false function return to `string`
// (coercing the `false` to ""), and an explicit union return type hits an
// unsupported EIR path for the dead-code copy of this function. So an unknown
// type yields "" here rather than `false`. Revisit when scalar-union values are
// representable end-to-end (the union-value-runtime work).
// PHP returns `string|false` for `image_type_to_extension`: `false` for an
// unknown type. The inferred return type widens to Mixed so `false` preserves
// its bool tag through the boxed Mixed cell at runtime.
function image_type_to_extension(int $image_type, bool $include_dot = true) {
$ext = "";
switch ($image_type) {
Expand Down Expand Up @@ -1176,10 +1173,8 @@ function exif_imagetype(string $filename) {
return elephc_img_probe_type();
}

// PHP returns string|false (false for an unknown tag). elephc collapses a
// string|false return to string, so an unknown tag yields "" here rather than
// false — the same limitation documented on image_type_to_extension. Test with
// `=== ""` instead of `=== false`.
// PHP returns string|false (false for an unknown tag). The inferred return
// type widens to Mixed so `false` preserves its bool tag.
function exif_tagname(int $index) {
$_len = elephc_exif_tagname($index);
if ($_len < 0) {
Expand Down
3 changes: 3 additions & 0 deletions src/ir/instr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ pub enum Op {
HashArrayUnion,
ArrayToHash,
ArraySetMixedKey,
ArrayGetMixedKey,
ArrayKeyExists,
OffsetExists,
OffsetUnset,
Expand Down Expand Up @@ -415,6 +416,7 @@ impl Op {
| PtrWriteString => E::WRITES_HEAP | E::MAY_FATAL | E::REFCOUNT_OP,
MixedArrayAppend => E::READS_HEAP | E::WRITES_HEAP | E::ALLOC_HEAP | E::MAY_FATAL | E::REFCOUNT_OP,
ArraySetMixedKey => E::READS_HEAP | E::WRITES_HEAP | E::ALLOC_HEAP | E::MAY_FATAL | E::REFCOUNT_OP,
ArrayGetMixedKey => E::READS_HEAP | E::MAY_FATAL | E::REFCOUNT_OP,
ArrayUnion | HashUnion | ArrayHashUnion | HashArrayUnion | ArrayToHash => {
E::READS_HEAP | E::ALLOC_HEAP | E::REFCOUNT_OP
}
Expand Down Expand Up @@ -579,6 +581,7 @@ impl Op {
HashArrayUnion => "hash_array_union",
ArrayToHash => "array_to_hash",
ArraySetMixedKey => "array_set_mixed_key",
ArrayGetMixedKey => "array_get_mixed_key",
ArrayKeyExists => "array_key_exists",
OffsetExists => "offset_exists",
OffsetUnset => "offset_unset",
Expand Down
2 changes: 1 addition & 1 deletion src/ir/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio
ArrayHashUnion => check_array_hash_union(function, inst_id, inst),
HashArrayUnion => check_hash_array_union(function, inst_id, inst),
ArrayLen | ArrayGet | ArrayIsset | ArraySet | ArrayPush | ArrayEnsureUnique
| ArrayCloneShallow | ArrayToHash | ArraySetMixedKey => {
| ArrayCloneShallow | ArrayToHash | ArraySetMixedKey | ArrayGetMixedKey => {
check_first_heap(function, inst_id, inst, IrHeapKind::Array, "Heap(Array)")
}
MixedArrayAppend => {
Expand Down
45 changes: 43 additions & 2 deletions src/ir_lower/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6693,8 +6693,21 @@ fn lower_array_access_from_value(
let mut index_value = lower_expr(ctx, index);
let op = match array_value.ir_type {
IrType::Heap(IrHeapKind::Array) => {
index_value = coerce_to_int_at_span(ctx, index_value, Some(index.span));
Op::ArrayGet
// Array(Mixed) may have been promoted to a hash at runtime (e.g.
// foreach-rebuilt arrays), so the key tag is only known at runtime.
// Route through ArrayGetMixedKey instead of coercing the index to
// int, which would collapse a string key onto int 0.
let is_mixed_array = matches!(
ctx.builder.value_php_type(array_value.value).codegen_repr(),
PhpType::Array(boxed) if *boxed == PhpType::Mixed
);
if is_mixed_array {
index_value = box_index_for_mixed_key(ctx, index_value, index);
Op::ArrayGetMixedKey
} else {
index_value = coerce_to_int_at_span(ctx, index_value, Some(index.span));
Op::ArrayGet
}
}
IrType::Heap(IrHeapKind::Hash) => Op::HashGet,
IrType::Heap(IrHeapKind::Buffer) => Op::BufferGet,
Expand All @@ -6715,6 +6728,33 @@ fn lower_array_access_from_value(
)
}

/// Boxes a lowered index into a Mixed cell for `Op::ArrayGetMixedKey`.
///
/// A foreach loop key already arrives as a boxed Mixed/Union cell, so it is
/// passed through unchanged. Literal int/string keys (and any other typed
/// index) are boxed via `Op::MixedBox` so the runtime helper can dispatch on
/// the key tag exactly like `Op::ArraySetMixedKey`.
fn box_index_for_mixed_key(
ctx: &mut LoweringContext<'_, '_>,
index_value: LoweredValue,
index: &Expr,
) -> LoweredValue {
if matches!(
index_value.ir_type,
IrType::Heap(IrHeapKind::Mixed) | IrType::Heap(IrHeapKind::Union)
) {
return index_value;
}
ctx.emit_value(
Op::MixedBox,
vec![index_value.value],
None,
PhpType::Mixed,
Op::MixedBox.default_effects(),
Some(index.span),
)
}

/// Lowers nullable receiver indexing without evaluating the index on a null receiver.
fn lower_nullable_array_access(
ctx: &mut LoweringContext<'_, '_>,
Expand Down Expand Up @@ -6766,6 +6806,7 @@ fn array_access_result_type(
) -> PhpType {
match op {
Op::StrCharAt => PhpType::Str,
Op::ArrayGetMixedKey => PhpType::Mixed,
Op::ArrayGet => match ctx.builder.value_php_type(array).codegen_repr() {
PhpType::Array(elem_ty) => {
array_access_element_result_type(normalize_value_php_type(*elem_ty))
Expand Down
13 changes: 10 additions & 3 deletions src/types/checker/functions/returns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,15 +420,22 @@ impl Checker {

/// Computes the wider of two PHP types for return-type merging:
/// - If equal, returns a clone.
/// - Str + anything → Str; Float + anything → Float.
/// - Str/Float absorb compatible numeric scalars (Int/Float/numeric-string) but
/// do NOT absorb Bool, so `string|false` and `float|false` widen to Mixed
/// (boxed tagged cell) instead of collapsing `false` to `""`/`0.0`.
/// - Void or Never resolves to the other type; otherwise → Mixed.
pub(crate) fn wider_type(a: &PhpType, b: &PhpType) -> PhpType {
match (a, b) {
_ if a == b => a.clone(),
(PhpType::Str, _) | (_, PhpType::Str) => PhpType::Str,
(PhpType::Float, _) | (_, PhpType::Float) => PhpType::Float,
(PhpType::Void, other) | (other, PhpType::Void) => other.clone(),
(PhpType::Never, other) | (other, PhpType::Never) => other.clone(),
// Once we reach Mixed (e.g. Bool + scalar), stay Mixed: don't let
// Str/Float absorb it back, which would re-collapse `false`.
(PhpType::Mixed, _) | (_, PhpType::Mixed) => PhpType::Mixed,
// Bool + scalar must not be absorbed: `false` would lose its tag.
(PhpType::Bool, _) | (_, PhpType::Bool) => PhpType::Mixed,
(PhpType::Str, _) | (_, PhpType::Str) => PhpType::Str,
(PhpType::Float, _) | (_, PhpType::Float) => PhpType::Float,
_ => PhpType::Mixed,
}
}
Expand Down
14 changes: 12 additions & 2 deletions src/types/checker/inference/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ impl Checker {
Ok(PhpType::Str)
}
PhpType::Array(elem_ty) => {
// Array(Mixed) may have been promoted to a hash at runtime
// (e.g. foreach-rebuilt arrays). Allow any PHP array key.
if **elem_ty == PhpType::Mixed {
return Ok(PhpType::Mixed);
}
if normalized_idx_ty != PhpType::Int {
return Err(CompileError::new(
expr.span,
Expand Down Expand Up @@ -215,12 +220,17 @@ impl Checker {
}
PhpType::Array(elem_ty) => {
saw_indexable_member = true;
if normalized_idx_ty != PhpType::Int {
// Array(Mixed) may have been promoted to a hash
// at runtime; allow any PHP array key.
if **elem_ty == PhpType::Mixed {
result_members.push(PhpType::Mixed);
} else if normalized_idx_ty != PhpType::Int {
first_index_error =
first_index_error.or(Some("Array index must be integer"));
continue;
} else {
result_members.push(*elem_ty.clone());
}
result_members.push(*elem_ty.clone());
}
PhpType::AssocArray { value, .. } => {
saw_indexable_member = true;
Expand Down
20 changes: 10 additions & 10 deletions tests/codegen/image/exif_iptc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ echo exif_tagname(0x010F), "|", exif_tagname(0x0112), "|", exif_tagname(0x8825),
assert_eq!(out, "Make|Orientation|GPS_IFD_Pointer|ExposureTime");
}

/// An unknown tag yields "" (the documented `string|false` collapse), not false.
/// An unknown tag yields `false` (PHP's `string|false` return semantics), not "".
#[test]
fn test_exif_tagname_unknown_is_empty() {
fn test_exif_tagname_unknown_is_false() {
let out = compile_and_run(
r#"<?php
$n = exif_tagname(0x9999);
echo ($n === "" ? "EMPTY" : "VALUE:" . $n);
var_dump($n === false);
"#,
);
assert_eq!(out, "EMPTY");
assert_eq!(out, "bool(true)\n");
}

/// `exif_imagetype` returns the IMAGETYPE_* code for a real image and false for a
Expand Down Expand Up @@ -170,10 +170,10 @@ echo "|", $w, "x", $h, "|", $ty, "|", ($out === $thumb ? "MATCH" : "DIFF");
assert_eq!(out, "Y|6x9|2|MATCH");
}

/// `exif_thumbnail` yields "" (the `string|false` collapse) when the EXIF data has
/// no thumbnail, leaving the by-ref out-params untouched.
/// `exif_thumbnail` yields `false` (PHP's `string|false` return semantics) when the EXIF
/// data has no thumbnail, leaving the by-ref out-params untouched.
#[test]
fn test_exif_thumbnail_none_is_empty() {
fn test_exif_thumbnail_none_is_false() {
let src = format!(
r#"<?php
{HELPERS}
Expand All @@ -186,11 +186,11 @@ $p = (string) tempnam(sys_get_temp_dir(), "elephc_img_p6nt_");
file_put_contents($p, $jpeg);
$w = -1;
$out = exif_thumbnail($p, $w);
echo ($out === "" ? "EMPTY" : "STR:" . strlen($out)), "|", $w;
echo ($out === false ? "FALSE" : "STR:" . strlen($out)), "|", $w;
"#
);
let out = compile_and_run(&src);
assert_eq!(out, "EMPTY|-1");
assert_eq!(out, "FALSE|-1");
}

/// `iptcparse` decodes an IIM block into `record#dataset` keys, grouping repeated
Expand Down Expand Up @@ -237,7 +237,7 @@ $iptc = chr(0x1C).chr(2).chr(5).be16(9)."headline!";
$embedded = iptcembed($iptc, $p);
$hasPs = strpos($embedded, "Photoshop 3.0") !== false ? "Y" : "N";
$hasData = strpos($embedded, "headline!") !== false ? "Y" : "N";
$re = imagecreatefromstring($embedded);
$re = imagecreatefromstring((string) $embedded);
echo $hasPs, "|", $hasData, "|", imagesx($re), "x", imagesy($re);
"#
);
Expand Down
13 changes: 5 additions & 8 deletions tests/codegen/image/foundation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,19 @@ echo image_type_to_mime_type(IMAGETYPE_WEBP) . "\n";
}

/// `image_type_to_extension` returns the extension with the leading dot by
/// default and without it when `$include_dot` is false.
///
/// Known limitation: PHP returns `false` for an unknown type, but elephc
/// currently collapses a `string|false` function return to `string`, so an
/// unknown type yields "" here (asserted as the empty third line). This is
/// tracked with the scalar-union value-runtime work; revisit when fixed.
/// default and without it when `$include_dot` is false. Unknown types return
/// `false` (not `""`), matching PHP's `string|false` return semantics.
#[test]
fn test_image_type_to_extension() {
let out = compile_and_run(
r#"<?php
echo image_type_to_extension(IMAGETYPE_PNG) . "\n";
echo image_type_to_extension(IMAGETYPE_JPEG, false) . "\n";
echo image_type_to_extension(IMAGETYPE_UNKNOWN) . "\n";
$r = image_type_to_extension(IMAGETYPE_UNKNOWN);
var_dump($r === false);
"#,
);
assert_eq!(out, ".png\njpeg\n\n");
assert_eq!(out, ".png\njpeg\nbool(true)\n");
}

/// `imagesx`/`imagesy` report the dimensions of a freshly created image without
Expand Down
4 changes: 4 additions & 0 deletions tests/codegen/regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ mod mixed_method_dispatch;
mod switch_and_float_params;
#[path = "regressions/return_this_ownership.rs"]
mod return_this_ownership;
#[path = "regressions/scalar_union_returns.rs"]
mod scalar_union_returns;
#[path = "regressions/mixed_array_read.rs"]
mod mixed_array_read;
Loading
Loading