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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to elephc, a PHP-to-native compiler written in Rust.
Releases are listed newest first.

## [Unreleased]
- Added PHP-compatible `mb_strlen()` to both native compilation and the Magician `eval()` runtime, including the nullable optional `$encoding` argument, UTF-8 malformed-sequence handling, byte-count aliases, iconv-backed multibyte encodings, callable dispatch, and catchable `ValueError` for unknown encodings on every supported target.
- Added experimental PHP `eval()` support across macOS ARM64, Linux ARM64, and Linux x86_64. Eligible literal fragments are parsed at compile time and lowered to native EIR, including direct or scope-backed caller-local synchronization; dynamic strings and unsupported literal shapes fall back to the optional statically linked `elephc-magician` EvalIR interpreter. Within the supported eval subset, the fallback preserves caller/global scope updates, dynamic functions/classes/constants, callables, reflection, builtins, exceptions, ownership/COW behavior, and PHP-visible diagnostics without requiring PHP or the Zend Engine. Bridge linking is automatic when required and can be forced with `--with-eval`; generated builtin documentation now reports AOT and eval availability separately.
- 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ pub(in crate::interpreter) enum EvalDirectHook {
StrReplace,
/// Dispatches `str_split(...)`.
StrSplit,
/// Dispatches `strlen(...)`.
/// Dispatches `strlen(...)` and `mb_strlen(...)`.
Strlen,
/// Dispatches `str_repeat(...)`.
StrRepeat,
Expand Down Expand Up @@ -534,7 +534,11 @@ impl EvalDirectHook {
_ => Err(EvalStatus::RuntimeFatal),
},
Self::StrSplit => eval_builtin_str_split(args, context, scope, values),
Self::Strlen => eval_builtin_strlen(args, context, scope, values),
Self::Strlen => match name {
"mb_strlen" => eval_builtin_mb_strlen(args, context, scope, values),
"strlen" => eval_builtin_strlen(args, context, scope, values),
_ => Err(EvalStatus::RuntimeFatal),
},
Self::StrRepeat => eval_builtin_str_repeat(args, context, scope, values),
Self::Strval => eval_builtin_strval(args, context, scope, values),
Self::Strrev => eval_builtin_strrev(args, context, scope, values),
Expand Down
21 changes: 12 additions & 9 deletions crates/elephc-magician/src/interpreter/builtins/hooks/values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ pub(in crate::interpreter) enum EvalValuesHook {
StrReplace,
/// Dispatches `str_split(...)`.
StrSplit,
/// Dispatches `strlen(...)`.
/// Dispatches `strlen(...)` and `mb_strlen(...)`.
Strlen,
/// Dispatches `str_repeat(...)`.
StrRepeat,
Expand Down Expand Up @@ -615,14 +615,17 @@ impl EvalValuesHook {
[value, length] => eval_str_split_result(*value, Some(*length), values),
_ => Err(EvalStatus::RuntimeFatal),
},
Self::Strlen => {
let [value] = evaluated_args else {
return Err(EvalStatus::RuntimeFatal);
};
let bytes = values.string_bytes(*value)?;
let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?;
values.int(len)
}
Self::Strlen => match name {
"mb_strlen" => match evaluated_args {
[value] => eval_mb_strlen_result(*value, None, context, values),
[value, encoding] => {
eval_mb_strlen_result(*value, Some(*encoding), context, values)
}
_ => Err(EvalStatus::RuntimeFatal),
},
"strlen" => one_arg(evaluated_args, values, eval_strlen_result),
_ => Err(EvalStatus::RuntimeFatal),
},
Self::StrRepeat => two_args(evaluated_args, values, eval_str_repeat_result),
Self::Strval => one_arg(evaluated_args, values, |value, values| {
eval_strval_result(value, context, values)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ fn declared_builtin_registry_derives_core_metadata() {
eval_declared_builtin_param_names("strlen"),
Some(["string"].as_slice())
);
assert_eq!(
eval_declared_builtin_param_names("mb_strlen"),
Some(["string", "encoding"].as_slice())
);
assert_eq!(
eval_declared_builtin_default_value("mb_strlen", 1),
Some(EvalBuiltinDefaultValue::Null)
);
assert_eq!(
eval_declared_builtin_param_names("is_finite"),
Some(["num"].as_slice())
Expand Down
202 changes: 202 additions & 0 deletions crates/elephc-magician/src/interpreter/builtins/string/mb_strlen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
//! Purpose:
//! Declarative eval registry entry and implementation for PHP's `mb_strlen()`.
//!
//! Called from:
//! - `crate::interpreter::builtins::string` and the declarative direct/values hooks.
//!
//! Key details:
//! - The eval signature matches PHP's nullable optional `$encoding` parameter.
//! - UTF-8 malformed/truncated sequences use Rust's decoder boundaries to mirror mbstring;
//! other encodings are counted through libc iconv into fixed-width UTF-32LE chunks.
//! - Unknown encoding names raise a catchable `ValueError` through eval's pending-throw state.

use std::ffi::CString;
use std::os::raw::{c_char, c_void};

use super::super::spec::EvalBuiltinDefaultValue;
use super::super::super::*;

eval_builtin! {
name: "mb_strlen",
area: String,
params: [string, encoding = EvalBuiltinDefaultValue::Null],
direct: Strlen,
values: Strlen,
}

#[cfg_attr(target_os = "macos", link(name = "iconv"))]
unsafe extern "C" {
fn iconv_open(tocode: *const c_char, fromcode: *const c_char) -> *mut c_void;
fn iconv(
cd: *mut c_void,
inbuf: *mut *mut c_char,
inbytesleft: *mut usize,
outbuf: *mut *mut c_char,
outbytesleft: *mut usize,
) -> usize;
fn iconv_close(cd: *mut c_void) -> i32;
}

/// Evaluates direct `mb_strlen()` calls while preserving PHP source-order argument evaluation.
pub(in crate::interpreter) fn eval_builtin_mb_strlen(
args: &[EvalExpr],
context: &mut ElephcEvalContext,
scope: &mut ElephcEvalScope,
values: &mut impl RuntimeValueOps,
) -> Result<RuntimeCellHandle, EvalStatus> {
match args {
[value] => {
let value = eval_expr(value, context, scope, values)?;
eval_mb_strlen_result(value, None, context, values)
}
[value, encoding] => {
let value = eval_expr(value, context, scope, values)?;
let encoding = eval_expr(encoding, context, scope, values)?;
eval_mb_strlen_result(value, Some(encoding), context, values)
}
_ => Err(EvalStatus::RuntimeFatal),
}
}

/// Counts characters in one materialized eval string with PHP-compatible encoding selection.
pub(in crate::interpreter) fn eval_mb_strlen_result(
value: RuntimeCellHandle,
encoding: Option<RuntimeCellHandle>,
context: &mut ElephcEvalContext,
values: &mut impl RuntimeValueOps,
) -> Result<RuntimeCellHandle, EvalStatus> {
let bytes = values.string_bytes(value)?;
let encoding = match encoding {
Some(encoding) if !values.is_null(encoding)? => Some(values.string_bytes(encoding)?),
_ => None,
};
let len = match encoding.as_deref() {
None => count_utf8_chars(&bytes),
Some(encoding) => match count_chars_in_encoding(&bytes, encoding) {
Some(len) => len,
None => return eval_mb_strlen_encoding_error(encoding, context, values),
},
};
let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?;
values.int(len)
}

/// Counts UTF-8 characters, treating each malformed sequence or truncated valid prefix as one.
fn count_utf8_chars(bytes: &[u8]) -> usize {
let mut offset = 0usize;
let mut count = 0usize;
while offset < bytes.len() {
match std::str::from_utf8(&bytes[offset..]) {
Ok(valid) => {
count += valid.chars().count();
break;
}
Err(error) => {
let valid_len = error.valid_up_to();
let valid = std::str::from_utf8(&bytes[offset..offset + valid_len])
.expect("from_utf8 valid prefix");
count += valid.chars().count();
count += 1;
match error.error_len() {
Some(invalid_len) => offset += valid_len + invalid_len,
None => break,
}
}
}
}
count
}

/// Counts characters for a PHP encoding name, returning `None` when iconv rejects the name.
fn count_chars_in_encoding(bytes: &[u8], encoding: &[u8]) -> Option<usize> {
if encoding.eq_ignore_ascii_case(b"UTF-8") || encoding.eq_ignore_ascii_case(b"UTF8") {
return Some(count_utf8_chars(bytes));
}
if encoding.eq_ignore_ascii_case(b"8bit")
|| encoding.eq_ignore_ascii_case(b"binary")
|| encoding.eq_ignore_ascii_case(b"7bit")
{
return Some(bytes.len());
}
count_chars_with_iconv(bytes, encoding)
}

/// Decodes through libc iconv into UTF-32LE chunks and counts produced scalar slots.
fn count_chars_with_iconv(bytes: &[u8], encoding: &[u8]) -> Option<usize> {
let encoding = CString::new(encoding).ok()?;
let utf32le = c"UTF-32LE";
let descriptor = unsafe { iconv_open(utf32le.as_ptr(), encoding.as_ptr()) };
if descriptor as isize == -1 {
return None;
}

let mut input = bytes.as_ptr().cast_mut().cast::<c_char>();
let mut input_left = bytes.len();
let mut count = 0usize;
while input_left > 0 {
let mut output = [0u8; 64];
let mut output_ptr = output.as_mut_ptr().cast::<c_char>();
let mut output_left = output.len();
let status = unsafe {
iconv(
descriptor,
&mut input,
&mut input_left,
&mut output_ptr,
&mut output_left,
)
};
count = count.checked_add((output.len() - output_left) / 4)?;
if status != usize::MAX {
continue;
}

let errno = std::io::Error::last_os_error().raw_os_error();
if errno == Some(libc::E2BIG) {
continue;
}
if errno == Some(libc::EINVAL) {
count = count.checked_add(1)?;
break;
}
if errno != Some(libc::EILSEQ) || input_left == 0 {
unsafe { iconv_close(descriptor) };
return None;
}

input = unsafe { input.add(1) };
input_left -= 1;
count = count.checked_add(1)?;
unsafe {
iconv(
descriptor,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
);
}
}

unsafe { iconv_close(descriptor) };
Some(count)
}

/// Raises PHP's catchable `ValueError` for an encoding name rejected by the runtime.
fn eval_mb_strlen_encoding_error<T>(
encoding: &[u8],
context: &mut ElephcEvalContext,
values: &mut impl RuntimeValueOps,
) -> Result<T, EvalStatus> {
let encoding = String::from_utf8_lossy(encoding);
let message = format!(
"mb_strlen(): Argument #2 ($encoding) must be a valid encoding, \"{}\" given",
encoding
);
let exception = values.new_object("ValueError")?;
let message = values.string(&message)?;
let code = values.int(0)?;
values.construct_object(exception, vec![message, code])?;
context.set_pending_throw(exception);
Err(EvalStatus::UncaughtThrowable)
}
2 changes: 2 additions & 0 deletions crates/elephc-magician/src/interpreter/builtins/string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ mod htmlspecialchars;
mod implode;
mod lcfirst;
mod ltrim;
mod mb_strlen;
mod md5;
mod nl2br;
mod ord;
Expand Down Expand Up @@ -114,6 +115,7 @@ pub(in crate::interpreter) use htmlspecialchars::*;
pub(in crate::interpreter) use implode::*;
pub(in crate::interpreter) use lcfirst::*;
pub(in crate::interpreter) use ltrim::*;
pub(in crate::interpreter) use mb_strlen::*;
pub(in crate::interpreter) use md5::*;
pub(in crate::interpreter) use nl2br::*;
pub(in crate::interpreter) use ord::*;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ pub(in crate::interpreter) fn eval_builtin_strlen(
return Err(EvalStatus::RuntimeFatal);
};
let value = eval_expr(value, context, scope, values)?;
eval_strlen_result(value, values)
}

/// Returns the byte length of one materialized eval string.
pub(in crate::interpreter) fn eval_strlen_result(
value: RuntimeCellHandle,
values: &mut impl RuntimeValueOps,
) -> Result<RuntimeCellHandle, EvalStatus> {
let bytes = values.string_bytes(value)?;
let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?;
values.int(len)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,36 @@
use super::super::*;
use super::support::*;

/// Verifies eval `mb_strlen()` matches encoding, malformed-byte, callable, and error behavior.
#[test]
fn execute_program_dispatches_mb_strlen_builtin() {
let program = parse_fragment(
r#"echo mb_strlen("abc"); echo ":";
echo mb_strlen(string: "héllo", encoding: "8bit"); echo ":";
echo mb_strlen("", null); echo ":";
echo call_user_func("mb_strlen", "日本語"); echo ":";
echo call_user_func_array("mb_strlen", ["string" => "héllo", "encoding" => "UTF-8"]); echo ":";
echo mb_strlen(chr(128), "UTF-8"); echo ":";
echo mb_strlen(chr(104) . chr(0) . chr(233) . chr(0), "UTF-16LE"); echo ":";
try {
mb_strlen("abc", "definitely-not-an-encoding");
} catch (ValueError $error) {
echo "caught";
}
echo ":";
return function_exists("mb_strlen") && is_callable("mb_strlen");"#
.as_bytes(),
)
.expect("parse eval fragment");
let mut scope = ElephcEvalScope::new();
let mut values = FakeOps::default();

let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir");

assert_eq!(values.output, "3:6:0:3:5:1:2:caught:");
assert_eq!(values.get(result), FakeValue::Bool(true));
}

/// Verifies eval `explode()` and `implode()` bridge byte strings and arrays.
#[test]
fn execute_program_dispatches_explode_implode_builtins() {
Expand Down
2 changes: 1 addition & 1 deletion docs/internals/builtins/_internal/__elephc_gmmktime_raw.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "__elephc_gmmktime_raw() — internals"
description: "Compiler internals for __elephc_gmmktime_raw(): lowering path, type checks, and runtime helpers."
sidebar:
order: 437
order: 438
---

## `__elephc_gmmktime_raw()` — internals
Expand Down
2 changes: 1 addition & 1 deletion docs/internals/builtins/_internal/__elephc_mktime_raw.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "__elephc_mktime_raw() — internals"
description: "Compiler internals for __elephc_mktime_raw(): lowering path, type checks, and runtime helpers."
sidebar:
order: 438
order: 439
---

## `__elephc_mktime_raw()` — internals
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "__elephc_phar_bzip2_archive() — internals"
description: "Compiler internals for __elephc_phar_bzip2_archive(): lowering path, type checks, and runtime helpers."
sidebar:
order: 439
order: 440
---

## `__elephc_phar_bzip2_archive()` — internals
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "__elephc_phar_decompress_archive() — internals"
description: "Compiler internals for __elephc_phar_decompress_archive(): lowering path, type checks, and runtime helpers."
sidebar:
order: 440
order: 441
---

## `__elephc_phar_decompress_archive()` — internals
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "__elephc_phar_get_file_metadata() — internals"
description: "Compiler internals for __elephc_phar_get_file_metadata(): lowering path, type checks, and runtime helpers."
sidebar:
order: 441
order: 442
---

## `__elephc_phar_get_file_metadata()` — internals
Expand Down
Loading
Loading