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
7 changes: 7 additions & 0 deletions docs/php/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ sidebar:
| `**` | `$a ** $b` | Exponentiation (right-associative, returns float) |
| `-$x` | `-$x` | Unary negation |

Numeric strings are accepted as arithmetic operands and coerced at runtime,
matching PHP semantics. Pure integer-form strings (e.g. `"123"`) coerce to
`int`; float-form strings (e.g. `"1.5"`) coerce to `float`. Leading-numeric
strings (e.g. `"12abc"`) emit a compile-time `Warning: A non-numeric value
encountered` and use the numeric prefix. The result type is `Mixed` when either
operand is a string, since the runtime type depends on the string's content.

## Comparison

| Operator | Example | Notes |
Expand Down
6 changes: 6 additions & 0 deletions examples/arithmetic/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@
echo "b % a = " . ($b % $a) . "\n";
echo "2 + 3 * 4 = " . (2 + 3 * 4) . "\n";
echo "(2 + 3) * 4 = " . ((2 + 3) * 4) . "\n";

// Numeric strings are coerced at runtime, matching PHP semantics.
echo '"123" + 3 = ' . ("123" + 3) . "\n";
echo '"1.5" + 3 = ' . ("1.5" + 3) . "\n";
echo '"100" - 30 = ' . ("100" - 30) . "\n";
echo '"10" / 3 = ' . ("10" / 3) . "\n";
57 changes: 57 additions & 0 deletions src/codegen/runtime/arrays/mixed_numeric_binops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,36 @@ pub fn emit_mixed_numeric_binops(emitter: &mut Emitter) {
emitter.instruction("cmp x9, #2"); // does the right operand hold a double payload?
emitter.instruction("b.eq __rt_mixed_numeric_float_path"); // any double payload makes the whole operation double-valued

// -- string operands may be float-form (e.g. "1.5"); check and route to the float path --
emitter.instruction("ldr x9, [sp, #24]"); // reload the left runtime value tag
emitter.instruction("cmp x9, #1"); // does the left operand hold a string payload?
emitter.instruction("b.eq __rt_mixed_numeric_check_str_float_left"); // check if the left string is float-form
emitter.instruction("ldr x9, [sp, #32]"); // reload the right runtime value tag
emitter.instruction("cmp x9, #1"); // does the right operand hold a string payload?
emitter.instruction("b.eq __rt_mixed_numeric_check_str_float_right"); // check if the right string is float-form
emitter.instruction("b __rt_mixed_numeric_int_path"); // neither operand is a string; proceed with integer arithmetic

emitter.label("__rt_mixed_numeric_check_str_float_left");
emitter.instruction("ldr x0, [sp, #0]"); // load the boxed left operand pointer
emitter.instruction("bl __rt_mixed_unbox"); // unbox: x0=tag, x1=string ptr, x2=string length
emitter.instruction("bl __rt_str_is_float_form"); // check if the left string is float-form (x1/x2 preserved)
emitter.instruction("cmp x0, #0"); // is the left string float-form?
emitter.instruction("b.ne __rt_mixed_numeric_float_path"); // yes: route to the float path for correct float arithmetic
emitter.instruction("ldr x9, [sp, #32]"); // reload the right runtime value tag
emitter.instruction("cmp x9, #1"); // does the right operand hold a string payload?
emitter.instruction("b.eq __rt_mixed_numeric_check_str_float_right"); // check if the right string is float-form
emitter.instruction("b __rt_mixed_numeric_int_path"); // right is not a string; proceed with integer arithmetic

emitter.label("__rt_mixed_numeric_check_str_float_right");
emitter.instruction("ldr x0, [sp, #8]"); // load the boxed right operand pointer
emitter.instruction("bl __rt_mixed_unbox"); // unbox: x0=tag, x1=string ptr, x2=string length
emitter.instruction("bl __rt_str_is_float_form"); // check if the right string is float-form (x1/x2 preserved)
emitter.instruction("cmp x0, #0"); // is the right string float-form?
emitter.instruction("b.ne __rt_mixed_numeric_float_path"); // yes: route to the float path for correct float arithmetic
emitter.instruction("b __rt_mixed_numeric_int_path"); // right string is int-form; proceed with integer arithmetic

// -- integer path with PHP overflow promotion --
emitter.label("__rt_mixed_numeric_int_path");
emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed left operand before casting to integer
emitter.instruction("bl __rt_mixed_cast_int"); // coerce the left operand using the current integer numeric rules
emitter.instruction("str x0, [sp, #40]"); // save the left integer payload across the right cast
Expand Down Expand Up @@ -190,7 +219,35 @@ fn emit_mixed_numeric_binops_linux_x86_64(emitter: &mut Emitter) {
emitter.instruction("cmp QWORD PTR [rbp - 40], 2"); // does the right operand hold a double payload?
emitter.instruction("je __rt_mixed_numeric_float_path_linux_x86_64"); // any double payload makes the whole operation double-valued

// -- string operands may be float-form (e.g. "1.5"); check and route to the float path --
emitter.instruction("cmp QWORD PTR [rbp - 32], 1"); // does the left operand hold a string payload?
emitter.instruction("je __rt_mixed_numeric_check_str_float_left_x86_64"); // check if the left string is float-form
emitter.instruction("cmp QWORD PTR [rbp - 40], 1"); // does the right operand hold a string payload?
emitter.instruction("je __rt_mixed_numeric_check_str_float_right_x86_64"); // check if the right string is float-form
emitter.instruction("jmp __rt_mixed_numeric_int_path_linux_x86_64"); // neither operand is a string; proceed with integer arithmetic

emitter.label("__rt_mixed_numeric_check_str_float_left_x86_64");
emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // load the boxed left operand pointer
emitter.instruction("call __rt_mixed_unbox"); // unbox: rax=tag, rdi=string ptr, rdx=string length
emitter.instruction("mov rax, rdi"); // move string pointer to rax for __rt_str_is_float_form
emitter.instruction("call __rt_str_is_float_form"); // check if the left string is float-form
emitter.instruction("test rax, rax"); // is the left string float-form?
emitter.instruction("jne __rt_mixed_numeric_float_path_linux_x86_64"); // yes: route to the float path for correct float arithmetic
emitter.instruction("cmp QWORD PTR [rbp - 40], 1"); // does the right operand hold a string payload?
emitter.instruction("je __rt_mixed_numeric_check_str_float_right_x86_64"); // check if the right string is float-form
emitter.instruction("jmp __rt_mixed_numeric_int_path_linux_x86_64"); // right is not a string; proceed with integer arithmetic

emitter.label("__rt_mixed_numeric_check_str_float_right_x86_64");
emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // load the boxed right operand pointer
emitter.instruction("call __rt_mixed_unbox"); // unbox: rax=tag, rdi=string ptr, rdx=string length
emitter.instruction("mov rax, rdi"); // move string pointer to rax for __rt_str_is_float_form
emitter.instruction("call __rt_str_is_float_form"); // check if the right string is float-form
emitter.instruction("test rax, rax"); // is the right string float-form?
emitter.instruction("jne __rt_mixed_numeric_float_path_linux_x86_64"); // yes: route to the float path for correct float arithmetic
emitter.instruction("jmp __rt_mixed_numeric_int_path_linux_x86_64"); // right string is int-form; proceed with integer arithmetic

// -- integer path with PHP overflow promotion --
emitter.label("__rt_mixed_numeric_int_path_linux_x86_64");
emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the boxed left operand before casting to integer
emitter.instruction("call __rt_mixed_cast_int"); // coerce the left operand using the current integer numeric rules
emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the left integer payload across the right cast
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 @@ -44,6 +44,7 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) {
strings::emit_str_eq(emitter);
strings::emit_str_to_number(emitter);
strings::emit_str_to_int(emitter);
strings::emit_str_is_float_form(emitter);
strings::emit_str_loose_eq(emitter);
strings::emit_number_format(emitter);
strings::emit_strcopy(emitter);
Expand Down
3 changes: 3 additions & 0 deletions src/codegen/runtime/strings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod str_eq;
mod str_loose_eq;
mod str_to_number;
mod str_to_int;
mod str_is_float_form;
mod number_format;
mod atoi;
mod grapheme_strrev;
Expand Down Expand Up @@ -93,6 +94,8 @@ pub use str_to_number::emit_str_to_number;
/// Emit string-to-number conversion helper.
pub use str_to_int::emit_str_to_int;
/// Emit PHP string-to-integer cast helper.
pub use str_is_float_form::emit_str_is_float_form;
/// Emit string float-form detection helper.
pub use number_format::emit_number_format;
/// Emit number formatting helper.
pub use atoi::emit_atoi;
Expand Down
112 changes: 112 additions & 0 deletions src/codegen/runtime/strings/str_is_float_form.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//! Purpose:
//! Emits the `__rt_str_is_float_form` runtime helper that reports whether a PHP
//! string is float-form (contains a `.` or exponent that `strtod` consumes beyond
//! what `strtoll` parses). Used by the mixed numeric dispatch to route string
//! operands to the correct arithmetic path.
//!
//! Called from:
//! - `crate::codegen::runtime::arrays::mixed_numeric_binops` during operand classification.
//!
//! Key details:
//! - Returns 1 in the integer result register when the string is float-form, 0 otherwise.
//! - The input string follows the active string-result convention (AArch64 x1/x2, x86_64 rax/rdx).

use crate::codegen::emit::Emitter;
use crate::codegen::platform::Arch;

/// Emits `__rt_str_is_float_form` for both supported targets.
///
/// Input follows the active string-result convention:
/// AArch64 uses `x1`/`x2`; x86_64 uses `rax`/`rdx`.
/// Output: integer result register holds 1 (float-form) or 0 (int-form).
pub fn emit_str_is_float_form(emitter: &mut Emitter) {
if emitter.target.arch == Arch::X86_64 {
emit_str_is_float_form_linux_x86_64(emitter);
return;
}

emitter.blank();
emitter.comment("--- runtime: str_is_float_form ---");
emitter.label_global("__rt_str_is_float_form");

// -- set up the helper frame (slots: end_i=[sp,#0], end_d=[sp,#8], cstr=[sp,#16]) --
emitter.instruction("sub sp, sp, #32"); // allocate slots for both end pointers and the C-string pointer
emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the libc calls
emitter.instruction("add x29, sp, #16"); // establish a stable helper frame pointer

// -- copy the PHP string into the C-string scratch buffer --
emitter.instruction("bl __rt_cstr"); // copy the bounded PHP string into the C-string scratch buffer
emitter.instruction("str x0, [sp, #16]"); // save the C-string pointer for the second parse

// -- integer parse: strtoll(cstr, &end_i, 10) reports where the integer prefix ends --
emitter.instruction("add x1, sp, #0"); // pass &end_i so strtoll reports where the integer prefix ended
emitter.instruction("mov x2, #10"); // parse in base 10 like PHP string-to-int
emitter.bl_c("strtoll");

// -- float parse: strtod(cstr, &end_d) reports where the numeric value ended --
emitter.instruction("ldr x0, [sp, #16]"); // reload the C-string pointer for strtod
emitter.instruction("add x1, sp, #8"); // pass &end_d so strtod reports where the numeric value ended
emitter.bl_c("strtod");

// -- if strtod consumed more bytes than strtoll, the string is float-form --
emitter.instruction("ldr x9, [sp, #8]"); // load the end pointer returned by strtod
emitter.instruction("ldr x10, [sp, #0]"); // load the end pointer returned by strtoll
emitter.instruction("cmp x9, x10"); // did strtod consume more bytes than strtoll?
emitter.instruction("b.hi __rt_str_is_float_form_true"); // yes: the string is float-form
emitter.instruction("mov x0, #0"); // no: the string is int-form
emitter.instruction("b __rt_str_is_float_form_done"); // skip the true path

emitter.label("__rt_str_is_float_form_true");
emitter.instruction("mov x0, #1"); // report that the string is float-form

emitter.label("__rt_str_is_float_form_done");
emitter.instruction("ldp x29, x30, [sp, #16]"); // restore caller frame pointer and return address
emitter.instruction("add sp, sp, #32"); // release the helper frame
emitter.instruction("ret"); // return the float-form flag in x0
}

/// Emits the Linux x86_64 `__rt_str_is_float_form` runtime helper.
///
/// The input string arrives in the elephc string-result registers (`rax`/`rdx`).
/// Parses with `strtoll` and `strtod`, returning 1 in `rax` if `strtod` consumed
/// more bytes (float-form), 0 otherwise.
fn emit_str_is_float_form_linux_x86_64(emitter: &mut Emitter) {
emitter.blank();
emitter.comment("--- runtime: str_is_float_form ---");
emitter.label_global("__rt_str_is_float_form");

// -- set up the helper frame (locals: cstr=[rbp-8], end_i=[rbp-16], end_d=[rbp-24]) --
emitter.instruction("push rbp"); // preserve the caller frame pointer before calling libc parsers
emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer
emitter.instruction("sub rsp, 32"); // allocate aligned slots for the C-string pointer and end pointers

// -- copy the PHP string into the C-string scratch buffer --
emitter.instruction("call __rt_cstr"); // copy the bounded PHP string into the C-string scratch buffer
emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the C-string pointer for the second parse

// -- integer parse: strtoll(cstr, &end_i, 10) reports where the integer prefix ends --
emitter.instruction("mov rdi, rax"); // strtoll arg1: the C-string pointer
emitter.instruction("lea rsi, [rbp - 16]"); // strtoll arg2: &end_i
emitter.instruction("mov edx, 10"); // strtoll arg3: parse in base 10
emitter.instruction("call strtoll"); // parse the integer prefix

// -- float parse: strtod(cstr, &end_d) reports where the numeric value ended --
emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the C-string pointer for strtod
emitter.instruction("lea rsi, [rbp - 24]"); // strtod arg2: &end_d
emitter.instruction("call strtod"); // parse the full numeric value

// -- if strtod consumed more bytes than strtoll, the string is float-form --
emitter.instruction("mov r8, QWORD PTR [rbp - 24]"); // load the end pointer returned by strtod
emitter.instruction("cmp r8, QWORD PTR [rbp - 16]"); // did strtod consume more bytes than strtoll?
emitter.instruction("ja __rt_str_is_float_form_true_linux_x86_64"); // yes: the string is float-form
emitter.instruction("xor rax, rax"); // no: the string is int-form
emitter.instruction("jmp __rt_str_is_float_form_done_linux_x86_64"); // skip the true path

emitter.label("__rt_str_is_float_form_true_linux_x86_64");
emitter.instruction("mov rax, 1"); // report that the string is float-form

emitter.label("__rt_str_is_float_form_done_linux_x86_64");
emitter.instruction("add rsp, 32"); // release the helper frame
emitter.instruction("pop rbp"); // restore the caller frame pointer
emitter.instruction("ret"); // return the float-form flag in rax
}
13 changes: 13 additions & 0 deletions src/ir_lower/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,18 @@ fn lower_numeric_binary(
return lower_mixed_numeric_binary(ctx, lhs, rhs, mixed_op, expr);
}
}
if matches!(op, BinOp::Div) && (lhs.ir_type == IrType::Str || rhs.ir_type == IrType::Str) {
let lhs = coerce_to_float(ctx, lhs, expr);
let rhs = coerce_to_float(ctx, rhs, expr);
return ctx.emit_value(
Op::FDiv,
vec![lhs.value, rhs.value],
None,
PhpType::Float,
Op::FDiv.default_effects(),
Some(expr.span),
);
}
if lhs.ir_type == IrType::F64 || rhs.ir_type == IrType::F64 {
let lhs = coerce_to_float(ctx, lhs, expr);
let rhs = coerce_to_float(ctx, rhs, expr);
Expand Down Expand Up @@ -9314,6 +9326,7 @@ fn coerce_to_float_at_span(
match value.ir_type {
IrType::F64 => value,
IrType::I64 => ctx.emit_value(Op::IToF, vec![value.value], None, PhpType::Float, Op::IToF.default_effects(), span),
IrType::Str => ctx.emit_value(Op::StrToF, vec![value.value], None, PhpType::Float, Op::StrToF.default_effects(), span),
_ => ctx.emit_value(
Op::Cast,
vec![value.value],
Expand Down
Loading
Loading