diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e8b4e4b0..7607050b15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index d718fea12e..259b270ca1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -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, @@ -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), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index ad08e58ffe..bb693880dd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -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, @@ -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) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_core.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_core.rs index 3b2da14758..fe205772a4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_core.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_core.rs @@ -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()) diff --git a/crates/elephc-magician/src/interpreter/builtins/string/mb_strlen.rs b/crates/elephc-magician/src/interpreter/builtins/string/mb_strlen.rs new file mode 100644 index 0000000000..c748c3823c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/mb_strlen.rs @@ -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 { + 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, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + 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 { + 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 { + 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::(); + 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::(); + 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( + encoding: &[u8], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + 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) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs index 944eb30326..82e8bf27e1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs @@ -42,6 +42,7 @@ mod htmlspecialchars; mod implode; mod lcfirst; mod ltrim; +mod mb_strlen; mod md5; mod nl2br; mod ord; @@ -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::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs b/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs index 2cd65327a0..fcf443c66e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs @@ -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 { let bytes = values.string_bytes(value)?; let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len) diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs index d54d99fe32..5f5dd51115 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs @@ -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() { diff --git a/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md b/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md index fc63d10226..9a19701dfb 100644 --- a/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md @@ -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 diff --git a/docs/internals/builtins/_internal/__elephc_mktime_raw.md b/docs/internals/builtins/_internal/__elephc_mktime_raw.md index d4594ceb36..f838340638 100644 --- a/docs/internals/builtins/_internal/__elephc_mktime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_mktime_raw.md @@ -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 diff --git a/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md b/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md index 0df6b9c8c4..961fe626e0 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md @@ -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 diff --git a/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md b/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md index cbbf97f852..8b34d3b8c3 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md @@ -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 diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md index f1cf109141..a4c6a5ccdd 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md @@ -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 diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md index 69095125a6..9ca079ed07 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_metadata() — internals" description: "Compiler internals for __elephc_phar_get_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 442 + order: 443 --- ## `__elephc_phar_get_metadata()` — internals diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md b/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md index ab785b1d3d..b0aed75af6 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_signature_hash() — internals" description: "Compiler internals for __elephc_phar_get_signature_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 443 + order: 444 --- ## `__elephc_phar_get_signature_hash()` — internals diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md b/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md index 22a982fca5..e284d35450 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_signature_type() — internals" description: "Compiler internals for __elephc_phar_get_signature_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 444 + order: 445 --- ## `__elephc_phar_get_signature_type()` — internals diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_stub.md b/docs/internals/builtins/_internal/__elephc_phar_get_stub.md index 1a811a61f6..3470b3afde 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_stub.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_stub.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_stub() — internals" description: "Compiler internals for __elephc_phar_get_stub(): lowering path, type checks, and runtime helpers." sidebar: - order: 445 + order: 446 --- ## `__elephc_phar_get_stub()` — internals diff --git a/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md b/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md index 47f661eb6e..b951b961ff 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md @@ -2,7 +2,7 @@ title: "__elephc_phar_gzip_archive() — internals" description: "Compiler internals for __elephc_phar_gzip_archive(): lowering path, type checks, and runtime helpers." sidebar: - order: 446 + order: 447 --- ## `__elephc_phar_gzip_archive()` — internals diff --git a/docs/internals/builtins/_internal/__elephc_phar_list_entries.md b/docs/internals/builtins/_internal/__elephc_phar_list_entries.md index 9c8543dbd4..4f5686f53d 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_list_entries.md +++ b/docs/internals/builtins/_internal/__elephc_phar_list_entries.md @@ -2,7 +2,7 @@ title: "__elephc_phar_list_entries() — internals" description: "Compiler internals for __elephc_phar_list_entries(): lowering path, type checks, and runtime helpers." sidebar: - order: 447 + order: 448 --- ## `__elephc_phar_list_entries()` — internals diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_compression.md b/docs/internals/builtins/_internal/__elephc_phar_set_compression.md index 246ed5d392..634f97defc 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_compression.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_compression.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_compression() — internals" description: "Compiler internals for __elephc_phar_set_compression(): lowering path, type checks, and runtime helpers." sidebar: - order: 448 + order: 449 --- ## `__elephc_phar_set_compression()` — internals diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md index d0a4d1354f..795bd3e1bd 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_file_metadata() — internals" description: "Compiler internals for __elephc_phar_set_file_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 449 + order: 450 --- ## `__elephc_phar_set_file_metadata()` — internals diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md index b9af908d49..68042d1822 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_metadata() — internals" description: "Compiler internals for __elephc_phar_set_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 450 + order: 451 --- ## `__elephc_phar_set_metadata()` — internals diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_stub.md b/docs/internals/builtins/_internal/__elephc_phar_set_stub.md index 8e64bae7a0..f98927b0c8 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_stub.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_stub.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_stub() — internals" description: "Compiler internals for __elephc_phar_set_stub(): lowering path, type checks, and runtime helpers." sidebar: - order: 451 + order: 452 --- ## `__elephc_phar_set_stub()` — internals diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md b/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md index f2dc51ce58..325d503a3c 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_zip_password() — internals" description: "Compiler internals for __elephc_phar_set_zip_password(): lowering path, type checks, and runtime helpers." sidebar: - order: 452 + order: 453 --- ## `__elephc_phar_set_zip_password()` — internals diff --git a/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md b/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md index 817eb9f247..9e934424df 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md +++ b/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md @@ -2,7 +2,7 @@ title: "__elephc_phar_sign_hash() — internals" description: "Compiler internals for __elephc_phar_sign_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 453 + order: 454 --- ## `__elephc_phar_sign_hash()` — internals diff --git a/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md b/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md index ea1f5610c9..e9ff5d9c57 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md +++ b/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md @@ -2,7 +2,7 @@ title: "__elephc_phar_sign_openssl() — internals" description: "Compiler internals for __elephc_phar_sign_openssl(): lowering path, type checks, and runtime helpers." sidebar: - order: 454 + order: 455 --- ## `__elephc_phar_sign_openssl()` — internals diff --git a/docs/internals/builtins/_internal/__elephc_strtotime_raw.md b/docs/internals/builtins/_internal/__elephc_strtotime_raw.md index 852c031755..e5b9600df2 100644 --- a/docs/internals/builtins/_internal/__elephc_strtotime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_strtotime_raw.md @@ -2,7 +2,7 @@ title: "__elephc_strtotime_raw() — internals" description: "Compiler internals for __elephc_strtotime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 455 + order: 456 --- ## `__elephc_strtotime_raw()` — internals diff --git a/docs/internals/builtins/string/chr.md b/docs/internals/builtins/string/chr.md index 818ac4cf7d..bbf0ea52ed 100644 --- a/docs/internals/builtins/string/chr.md +++ b/docs/internals/builtins/string/chr.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/chr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/chr.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:876](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L876) (`lower_chr`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:921](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L921) (`lower_chr`) - **Function symbol**: `lower_chr()` diff --git a/docs/internals/builtins/string/crc32.md b/docs/internals/builtins/string/crc32.md index 750fd11c00..3154f6bb69 100644 --- a/docs/internals/builtins/string/crc32.md +++ b/docs/internals/builtins/string/crc32.md @@ -22,9 +22,7 @@ sidebar: The following runtime helpers are referenced: - `__rt_crc32` -- `__rt_hash` -- `__rt_md5` -- `__rt_sha1` +- `__rt_mb_strlen` ## Signature summary diff --git a/docs/internals/builtins/string/gzcompress.md b/docs/internals/builtins/string/gzcompress.md index 22cb6394ac..b65bb99912 100644 --- a/docs/internals/builtins/string/gzcompress.md +++ b/docs/internals/builtins/string/gzcompress.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/gzcompress.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzcompress.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:420](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L420) (`lower_gzcompress`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:465](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L465) (`lower_gzcompress`) - **Function symbol**: `lower_gzcompress()` diff --git a/docs/internals/builtins/string/gzdeflate.md b/docs/internals/builtins/string/gzdeflate.md index adaad84833..47f9f99e53 100644 --- a/docs/internals/builtins/string/gzdeflate.md +++ b/docs/internals/builtins/string/gzdeflate.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/gzdeflate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzdeflate.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:436](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L436) (`lower_gzdeflate`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:481](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L481) (`lower_gzdeflate`) - **Function symbol**: `lower_gzdeflate()` diff --git a/docs/internals/builtins/string/gzinflate.md b/docs/internals/builtins/string/gzinflate.md index 8ab991a106..f293886ddc 100644 --- a/docs/internals/builtins/string/gzinflate.md +++ b/docs/internals/builtins/string/gzinflate.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/gzinflate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzinflate.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:454](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L454) (`lower_gzinflate`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:499](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L499) (`lower_gzinflate`) - **Function symbol**: `lower_gzinflate()` diff --git a/docs/internals/builtins/string/gzuncompress.md b/docs/internals/builtins/string/gzuncompress.md index 4c6c9a18be..4359341fa6 100644 --- a/docs/internals/builtins/string/gzuncompress.md +++ b/docs/internals/builtins/string/gzuncompress.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/gzuncompress.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzuncompress.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:475](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L475) (`lower_gzuncompress`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:520](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L520) (`lower_gzuncompress`) - **Function symbol**: `lower_gzuncompress()` diff --git a/docs/internals/builtins/string/hash_copy.md b/docs/internals/builtins/string/hash_copy.md index 18dc0532f4..68199422c2 100644 --- a/docs/internals/builtins/string/hash_copy.md +++ b/docs/internals/builtins/string/hash_copy.md @@ -23,8 +23,6 @@ sidebar: The following runtime helpers are referenced: - `__rt_crc32` - `__rt_hash_copy` -- `__rt_md5` -- `__rt_sha1` ## Signature summary diff --git a/docs/internals/builtins/string/inet_ntop.md b/docs/internals/builtins/string/inet_ntop.md index 171110348c..83f795ab58 100644 --- a/docs/internals/builtins/string/inet_ntop.md +++ b/docs/internals/builtins/string/inet_ntop.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/inet_ntop.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/inet_ntop.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:515](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L515) (`lower_inet`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:560](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L560) (`lower_inet`) - **Function symbol**: `lower_inet()` diff --git a/docs/internals/builtins/string/inet_pton.md b/docs/internals/builtins/string/inet_pton.md index d103808624..d7432930c8 100644 --- a/docs/internals/builtins/string/inet_pton.md +++ b/docs/internals/builtins/string/inet_pton.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/inet_pton.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/inet_pton.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:515](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L515) (`lower_inet`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:560](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L560) (`lower_inet`) - **Function symbol**: `lower_inet()` diff --git a/docs/internals/builtins/string/ip2long.md b/docs/internals/builtins/string/ip2long.md index f3e2527b7a..cf87419f4c 100644 --- a/docs/internals/builtins/string/ip2long.md +++ b/docs/internals/builtins/string/ip2long.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ip2long.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ip2long.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:506](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L506) (`lower_ip2long`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:551](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L551) (`lower_ip2long`) - **Function symbol**: `lower_ip2long()` diff --git a/docs/internals/builtins/string/long2ip.md b/docs/internals/builtins/string/long2ip.md index ef662e94ba..0945937806 100644 --- a/docs/internals/builtins/string/long2ip.md +++ b/docs/internals/builtins/string/long2ip.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/long2ip.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/long2ip.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:494](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L494) (`lower_long2ip`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:539](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L539) (`lower_long2ip`) - **Function symbol**: `lower_long2ip()` diff --git a/docs/internals/builtins/string/mb_strlen.md b/docs/internals/builtins/string/mb_strlen.md new file mode 100644 index 0000000000..d3e2207175 --- /dev/null +++ b/docs/internals/builtins/string/mb_strlen.md @@ -0,0 +1,44 @@ +--- +title: "mb_strlen() — internals" +description: "Compiler internals for mb_strlen(): lowering path, type checks, and runtime helpers." +sidebar: + order: 375 +--- + +## `mb_strlen()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/mb_strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/mb_strlen.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:375](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L375) (`lower_mb_strlen`) +- **Function symbol**: `lower_mb_strlen()` + + +### Lowering notes + +- Lowers `mb_strlen(string, encoding = null)` through the multibyte runtime helper. +- Omitted/null encodings use a null pointer plus zero length; explicit names stay byte strings for PHP-compatible case-insensitive lookup and `ValueError` handling. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_mb_strlen` + +## Signature summary + +```php +function mb_strlen(string $string, string $encoding = null): int +``` + +## What the type checker enforces + +- **Arity**: takes 1–2 arguments (1 optional). + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/mb_strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/mb_strlen.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `mb_strlen()`](../../../php/builtins/string/mb_strlen.md) diff --git a/docs/internals/builtins/string/md5.md b/docs/internals/builtins/string/md5.md index 9518bf8c88..79d805eace 100644 --- a/docs/internals/builtins/string/md5.md +++ b/docs/internals/builtins/string/md5.md @@ -2,7 +2,7 @@ title: "md5() — internals" description: "Compiler internals for md5(): lowering path, type checks, and runtime helpers." sidebar: - order: 375 + order: 376 --- ## `md5()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/md5.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/md5.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:373](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L373) (`lower_md5`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:418](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L418) (`lower_md5`) - **Function symbol**: `lower_md5()` diff --git a/docs/internals/builtins/string/nl2br.md b/docs/internals/builtins/string/nl2br.md index f3c46ae63a..54ea14d2f4 100644 --- a/docs/internals/builtins/string/nl2br.md +++ b/docs/internals/builtins/string/nl2br.md @@ -2,7 +2,7 @@ title: "nl2br() — internals" description: "Compiler internals for nl2br(): lowering path, type checks, and runtime helpers." sidebar: - order: 376 + order: 377 --- ## `nl2br()` — internals diff --git a/docs/internals/builtins/string/number_format.md b/docs/internals/builtins/string/number_format.md index 8b94b884d7..406e527f57 100644 --- a/docs/internals/builtins/string/number_format.md +++ b/docs/internals/builtins/string/number_format.md @@ -2,7 +2,7 @@ title: "number_format() — internals" description: "Compiler internals for number_format(): lowering path, type checks, and runtime helpers." sidebar: - order: 377 + order: 378 --- ## `number_format()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/number_format.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/number_format.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:893](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L893) (`lower_number_format`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:938](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L938) (`lower_number_format`) - **Function symbol**: `lower_number_format()` diff --git a/docs/internals/builtins/string/ord.md b/docs/internals/builtins/string/ord.md index 36de3e8129..70678a99ad 100644 --- a/docs/internals/builtins/string/ord.md +++ b/docs/internals/builtins/string/ord.md @@ -2,7 +2,7 @@ title: "ord() — internals" description: "Compiler internals for ord(): lowering path, type checks, and runtime helpers." sidebar: - order: 378 + order: 379 --- ## `ord()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ord.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ord.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:852](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L852) (`lower_ord`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:897](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L897) (`lower_ord`) - **Function symbol**: `lower_ord()` diff --git a/docs/internals/builtins/string/printf.md b/docs/internals/builtins/string/printf.md index 269c1d532b..d6be009888 100644 --- a/docs/internals/builtins/string/printf.md +++ b/docs/internals/builtins/string/printf.md @@ -2,7 +2,7 @@ title: "printf() — internals" description: "Compiler internals for printf(): lowering path, type checks, and runtime helpers." sidebar: - order: 379 + order: 380 --- ## `printf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/printf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/printf.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:535](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L535) (`lower_printf`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:580](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L580) (`lower_printf`) - **Function symbol**: `lower_printf()` diff --git a/docs/internals/builtins/string/rawurldecode.md b/docs/internals/builtins/string/rawurldecode.md index f3f2283772..c944580037 100644 --- a/docs/internals/builtins/string/rawurldecode.md +++ b/docs/internals/builtins/string/rawurldecode.md @@ -2,7 +2,7 @@ title: "rawurldecode() — internals" description: "Compiler internals for rawurldecode(): lowering path, type checks, and runtime helpers." sidebar: - order: 380 + order: 381 --- ## `rawurldecode()` — internals diff --git a/docs/internals/builtins/string/rawurlencode.md b/docs/internals/builtins/string/rawurlencode.md index 2b5970a386..694319b9ce 100644 --- a/docs/internals/builtins/string/rawurlencode.md +++ b/docs/internals/builtins/string/rawurlencode.md @@ -2,7 +2,7 @@ title: "rawurlencode() — internals" description: "Compiler internals for rawurlencode(): lowering path, type checks, and runtime helpers." sidebar: - order: 381 + order: 382 --- ## `rawurlencode()` — internals diff --git a/docs/internals/builtins/string/rtrim.md b/docs/internals/builtins/string/rtrim.md index 102f293b2b..880b004fcd 100644 --- a/docs/internals/builtins/string/rtrim.md +++ b/docs/internals/builtins/string/rtrim.md @@ -2,7 +2,7 @@ title: "rtrim() — internals" description: "Compiler internals for rtrim(): lowering path, type checks, and runtime helpers." sidebar: - order: 382 + order: 383 --- ## `rtrim()` — internals diff --git a/docs/internals/builtins/string/sha1.md b/docs/internals/builtins/string/sha1.md index 92f892c5d9..44bc888001 100644 --- a/docs/internals/builtins/string/sha1.md +++ b/docs/internals/builtins/string/sha1.md @@ -2,7 +2,7 @@ title: "sha1() — internals" description: "Compiler internals for sha1(): lowering path, type checks, and runtime helpers." sidebar: - order: 383 + order: 384 --- ## `sha1()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/sha1.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/sha1.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:378](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L378) (`lower_sha1`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:423](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L423) (`lower_sha1`) - **Function symbol**: `lower_sha1()` diff --git a/docs/internals/builtins/string/sprintf.md b/docs/internals/builtins/string/sprintf.md index 1ad8926087..9ab7d34c60 100644 --- a/docs/internals/builtins/string/sprintf.md +++ b/docs/internals/builtins/string/sprintf.md @@ -2,7 +2,7 @@ title: "sprintf() — internals" description: "Compiler internals for sprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 384 + order: 385 --- ## `sprintf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/sprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/sprintf.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:529](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L529) (`lower_sprintf`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:574](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L574) (`lower_sprintf`) - **Function symbol**: `lower_sprintf()` diff --git a/docs/internals/builtins/string/sscanf.md b/docs/internals/builtins/string/sscanf.md index 05bf35a145..6984fe944a 100644 --- a/docs/internals/builtins/string/sscanf.md +++ b/docs/internals/builtins/string/sscanf.md @@ -2,7 +2,7 @@ title: "sscanf() — internals" description: "Compiler internals for sscanf(): lowering path, type checks, and runtime helpers." sidebar: - order: 385 + order: 386 --- ## `sscanf()` — internals diff --git a/docs/internals/builtins/string/str_contains.md b/docs/internals/builtins/string/str_contains.md index 985af2503c..be100d95fd 100644 --- a/docs/internals/builtins/string/str_contains.md +++ b/docs/internals/builtins/string/str_contains.md @@ -2,7 +2,7 @@ title: "str_contains() — internals" description: "Compiler internals for str_contains(): lowering path, type checks, and runtime helpers." sidebar: - order: 386 + order: 387 --- ## `str_contains()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_contains.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_contains.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:700](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L700) (`lower_str_contains`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:745](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L745) (`lower_str_contains`) - **Function symbol**: `lower_str_contains()` diff --git a/docs/internals/builtins/string/str_ends_with.md b/docs/internals/builtins/string/str_ends_with.md index 7751e3a6a3..9bd96e4a3f 100644 --- a/docs/internals/builtins/string/str_ends_with.md +++ b/docs/internals/builtins/string/str_ends_with.md @@ -2,7 +2,7 @@ title: "str_ends_with() — internals" description: "Compiler internals for str_ends_with(): lowering path, type checks, and runtime helpers." sidebar: - order: 387 + order: 388 --- ## `str_ends_with()` — internals diff --git a/docs/internals/builtins/string/str_ireplace.md b/docs/internals/builtins/string/str_ireplace.md index 3f1f7ba9be..898c3d2a59 100644 --- a/docs/internals/builtins/string/str_ireplace.md +++ b/docs/internals/builtins/string/str_ireplace.md @@ -2,7 +2,7 @@ title: "str_ireplace() — internals" description: "Compiler internals for str_ireplace(): lowering path, type checks, and runtime helpers." sidebar: - order: 388 + order: 389 --- ## `str_ireplace()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_ireplace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_ireplace.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:798](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L798) (`lower_string_replace`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:843](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L843) (`lower_string_replace`) - **Function symbol**: `lower_string_replace()` diff --git a/docs/internals/builtins/string/str_pad.md b/docs/internals/builtins/string/str_pad.md index d26a219b72..0efe3b2213 100644 --- a/docs/internals/builtins/string/str_pad.md +++ b/docs/internals/builtins/string/str_pad.md @@ -2,7 +2,7 @@ title: "str_pad() — internals" description: "Compiler internals for str_pad(): lowering path, type checks, and runtime helpers." sidebar: - order: 389 + order: 390 --- ## `str_pad()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_pad.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:836](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L836) (`lower_str_pad`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:881](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L881) (`lower_str_pad`) - **Function symbol**: `lower_str_pad()` diff --git a/docs/internals/builtins/string/str_repeat.md b/docs/internals/builtins/string/str_repeat.md index 1066f3f0e1..5a03ccb651 100644 --- a/docs/internals/builtins/string/str_repeat.md +++ b/docs/internals/builtins/string/str_repeat.md @@ -2,7 +2,7 @@ title: "str_repeat() — internals" description: "Compiler internals for str_repeat(): lowering path, type checks, and runtime helpers." sidebar: - order: 390 + order: 391 --- ## `str_repeat()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_repeat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_repeat.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:764](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L764) (`lower_str_repeat`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:809](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L809) (`lower_str_repeat`) - **Function symbol**: `lower_str_repeat()` diff --git a/docs/internals/builtins/string/str_replace.md b/docs/internals/builtins/string/str_replace.md index 1c8c2c06ef..5462f37b19 100644 --- a/docs/internals/builtins/string/str_replace.md +++ b/docs/internals/builtins/string/str_replace.md @@ -2,7 +2,7 @@ title: "str_replace() — internals" description: "Compiler internals for str_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 391 + order: 392 --- ## `str_replace()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_replace.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:798](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L798) (`lower_string_replace`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:843](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L843) (`lower_string_replace`) - **Function symbol**: `lower_string_replace()` diff --git a/docs/internals/builtins/string/str_split.md b/docs/internals/builtins/string/str_split.md index 67b26a11c7..e0f19870e8 100644 --- a/docs/internals/builtins/string/str_split.md +++ b/docs/internals/builtins/string/str_split.md @@ -2,7 +2,7 @@ title: "str_split() — internals" description: "Compiler internals for str_split(): lowering path, type checks, and runtime helpers." sidebar: - order: 392 + order: 393 --- ## `str_split()` — internals diff --git a/docs/internals/builtins/string/str_starts_with.md b/docs/internals/builtins/string/str_starts_with.md index ab14e8de86..32c7f944c9 100644 --- a/docs/internals/builtins/string/str_starts_with.md +++ b/docs/internals/builtins/string/str_starts_with.md @@ -2,7 +2,7 @@ title: "str_starts_with() — internals" description: "Compiler internals for str_starts_with(): lowering path, type checks, and runtime helpers." sidebar: - order: 393 + order: 394 --- ## `str_starts_with()` — internals diff --git a/docs/internals/builtins/string/strcasecmp.md b/docs/internals/builtins/string/strcasecmp.md index 7677ab0473..d4368a8f35 100644 --- a/docs/internals/builtins/string/strcasecmp.md +++ b/docs/internals/builtins/string/strcasecmp.md @@ -2,7 +2,7 @@ title: "strcasecmp() — internals" description: "Compiler internals for strcasecmp(): lowering path, type checks, and runtime helpers." sidebar: - order: 394 + order: 395 --- ## `strcasecmp()` — internals diff --git a/docs/internals/builtins/string/strcmp.md b/docs/internals/builtins/string/strcmp.md index fef1c20878..cc29e4a6b4 100644 --- a/docs/internals/builtins/string/strcmp.md +++ b/docs/internals/builtins/string/strcmp.md @@ -2,7 +2,7 @@ title: "strcmp() — internals" description: "Compiler internals for strcmp(): lowering path, type checks, and runtime helpers." sidebar: - order: 395 + order: 396 --- ## `strcmp()` — internals diff --git a/docs/internals/builtins/string/stripslashes.md b/docs/internals/builtins/string/stripslashes.md index 0fd90ea4a7..6cff1acd86 100644 --- a/docs/internals/builtins/string/stripslashes.md +++ b/docs/internals/builtins/string/stripslashes.md @@ -2,7 +2,7 @@ title: "stripslashes() — internals" description: "Compiler internals for stripslashes(): lowering path, type checks, and runtime helpers." sidebar: - order: 396 + order: 397 --- ## `stripslashes()` — internals diff --git a/docs/internals/builtins/string/strlen.md b/docs/internals/builtins/string/strlen.md index 3a9144829b..34c628ad41 100644 --- a/docs/internals/builtins/string/strlen.md +++ b/docs/internals/builtins/string/strlen.md @@ -2,7 +2,7 @@ title: "strlen() — internals" description: "Compiler internals for strlen(): lowering path, type checks, and runtime helpers." sidebar: - order: 397 + order: 398 --- ## `strlen()` — internals diff --git a/docs/internals/builtins/string/strpos.md b/docs/internals/builtins/string/strpos.md index e8b8c04234..58503d2273 100644 --- a/docs/internals/builtins/string/strpos.md +++ b/docs/internals/builtins/string/strpos.md @@ -2,7 +2,7 @@ title: "strpos() — internals" description: "Compiler internals for strpos(): lowering path, type checks, and runtime helpers." sidebar: - order: 398 + order: 399 --- ## `strpos()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strpos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strpos.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:718](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L718) (`lower_string_position`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:763](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L763) (`lower_string_position`) - **Function symbol**: `lower_string_position()` diff --git a/docs/internals/builtins/string/strrev.md b/docs/internals/builtins/string/strrev.md index 3b6af20fae..c78d904220 100644 --- a/docs/internals/builtins/string/strrev.md +++ b/docs/internals/builtins/string/strrev.md @@ -2,7 +2,7 @@ title: "strrev() — internals" description: "Compiler internals for strrev(): lowering path, type checks, and runtime helpers." sidebar: - order: 399 + order: 400 --- ## `strrev()` — internals diff --git a/docs/internals/builtins/string/strrpos.md b/docs/internals/builtins/string/strrpos.md index b9ef683672..cddfd67f48 100644 --- a/docs/internals/builtins/string/strrpos.md +++ b/docs/internals/builtins/string/strrpos.md @@ -2,7 +2,7 @@ title: "strrpos() — internals" description: "Compiler internals for strrpos(): lowering path, type checks, and runtime helpers." sidebar: - order: 400 + order: 401 --- ## `strrpos()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strrpos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strrpos.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:718](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L718) (`lower_string_position`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:763](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L763) (`lower_string_position`) - **Function symbol**: `lower_string_position()` diff --git a/docs/internals/builtins/string/strstr.md b/docs/internals/builtins/string/strstr.md index 2162c7e3c6..51b8b8030f 100644 --- a/docs/internals/builtins/string/strstr.md +++ b/docs/internals/builtins/string/strstr.md @@ -2,7 +2,7 @@ title: "strstr() — internals" description: "Compiler internals for strstr(): lowering path, type checks, and runtime helpers." sidebar: - order: 401 + order: 402 --- ## `strstr()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strstr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strstr.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:780](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L780) (`lower_strstr`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:825](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L825) (`lower_strstr`) - **Function symbol**: `lower_strstr()` diff --git a/docs/internals/builtins/string/strtolower.md b/docs/internals/builtins/string/strtolower.md index f7598055a4..616ebd86f0 100644 --- a/docs/internals/builtins/string/strtolower.md +++ b/docs/internals/builtins/string/strtolower.md @@ -2,7 +2,7 @@ title: "strtolower() — internals" description: "Compiler internals for strtolower(): lowering path, type checks, and runtime helpers." sidebar: - order: 402 + order: 403 --- ## `strtolower()` — internals diff --git a/docs/internals/builtins/string/strtoupper.md b/docs/internals/builtins/string/strtoupper.md index 3e1b44f203..756cdc53bf 100644 --- a/docs/internals/builtins/string/strtoupper.md +++ b/docs/internals/builtins/string/strtoupper.md @@ -2,7 +2,7 @@ title: "strtoupper() — internals" description: "Compiler internals for strtoupper(): lowering path, type checks, and runtime helpers." sidebar: - order: 403 + order: 404 --- ## `strtoupper()` — internals diff --git a/docs/internals/builtins/string/substr.md b/docs/internals/builtins/string/substr.md index 1dd8c158ab..0e6f136eaf 100644 --- a/docs/internals/builtins/string/substr.md +++ b/docs/internals/builtins/string/substr.md @@ -2,7 +2,7 @@ title: "substr() — internals" description: "Compiler internals for substr(): lowering path, type checks, and runtime helpers." sidebar: - order: 404 + order: 405 --- ## `substr()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/substr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/substr.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:731](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L731) (`lower_substr`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:776](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L776) (`lower_substr`) - **Function symbol**: `lower_substr()` diff --git a/docs/internals/builtins/string/substr_replace.md b/docs/internals/builtins/string/substr_replace.md index 7e8af23a91..ed2003103a 100644 --- a/docs/internals/builtins/string/substr_replace.md +++ b/docs/internals/builtins/string/substr_replace.md @@ -2,7 +2,7 @@ title: "substr_replace() — internals" description: "Compiler internals for substr_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 405 + order: 406 --- ## `substr_replace()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/substr_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/substr_replace.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:748](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L748) (`lower_substr_replace`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:793](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L793) (`lower_substr_replace`) - **Function symbol**: `lower_substr_replace()` diff --git a/docs/internals/builtins/string/trim.md b/docs/internals/builtins/string/trim.md index c4642fdbec..cffe04ef88 100644 --- a/docs/internals/builtins/string/trim.md +++ b/docs/internals/builtins/string/trim.md @@ -2,7 +2,7 @@ title: "trim() — internals" description: "Compiler internals for trim(): lowering path, type checks, and runtime helpers." sidebar: - order: 406 + order: 407 --- ## `trim()` — internals diff --git a/docs/internals/builtins/string/ucfirst.md b/docs/internals/builtins/string/ucfirst.md index 8ada62280e..34888f9031 100644 --- a/docs/internals/builtins/string/ucfirst.md +++ b/docs/internals/builtins/string/ucfirst.md @@ -2,7 +2,7 @@ title: "ucfirst() — internals" description: "Compiler internals for ucfirst(): lowering path, type checks, and runtime helpers." sidebar: - order: 407 + order: 408 --- ## `ucfirst()` — internals diff --git a/docs/internals/builtins/string/ucwords.md b/docs/internals/builtins/string/ucwords.md index a8c2c0e952..38cbea8d32 100644 --- a/docs/internals/builtins/string/ucwords.md +++ b/docs/internals/builtins/string/ucwords.md @@ -2,7 +2,7 @@ title: "ucwords() — internals" description: "Compiler internals for ucwords(): lowering path, type checks, and runtime helpers." sidebar: - order: 408 + order: 409 --- ## `ucwords()` — internals diff --git a/docs/internals/builtins/string/urldecode.md b/docs/internals/builtins/string/urldecode.md index 3f34e71607..0839f209cc 100644 --- a/docs/internals/builtins/string/urldecode.md +++ b/docs/internals/builtins/string/urldecode.md @@ -2,7 +2,7 @@ title: "urldecode() — internals" description: "Compiler internals for urldecode(): lowering path, type checks, and runtime helpers." sidebar: - order: 409 + order: 410 --- ## `urldecode()` — internals diff --git a/docs/internals/builtins/string/urlencode.md b/docs/internals/builtins/string/urlencode.md index a796b83f6a..bc75300b59 100644 --- a/docs/internals/builtins/string/urlencode.md +++ b/docs/internals/builtins/string/urlencode.md @@ -2,7 +2,7 @@ title: "urlencode() — internals" description: "Compiler internals for urlencode(): lowering path, type checks, and runtime helpers." sidebar: - order: 410 + order: 411 --- ## `urlencode()` — internals diff --git a/docs/internals/builtins/string/vprintf.md b/docs/internals/builtins/string/vprintf.md index cde64be266..76c0dabed0 100644 --- a/docs/internals/builtins/string/vprintf.md +++ b/docs/internals/builtins/string/vprintf.md @@ -2,7 +2,7 @@ title: "vprintf() — internals" description: "Compiler internals for vprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 411 + order: 412 --- ## `vprintf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/vprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/vprintf.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:548](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L548) (`lower_vprintf`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:593](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L593) (`lower_vprintf`) - **Function symbol**: `lower_vprintf()` diff --git a/docs/internals/builtins/string/vsprintf.md b/docs/internals/builtins/string/vsprintf.md index a2a594bb37..b7e3caabda 100644 --- a/docs/internals/builtins/string/vsprintf.md +++ b/docs/internals/builtins/string/vsprintf.md @@ -2,7 +2,7 @@ title: "vsprintf() — internals" description: "Compiler internals for vsprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 412 + order: 413 --- ## `vsprintf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/vsprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/vsprintf.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:542](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L542) (`lower_vsprintf`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:587](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L587) (`lower_vsprintf`) - **Function symbol**: `lower_vsprintf()` diff --git a/docs/internals/builtins/string/wordwrap.md b/docs/internals/builtins/string/wordwrap.md index 3f1dc4258e..89da6e694b 100644 --- a/docs/internals/builtins/string/wordwrap.md +++ b/docs/internals/builtins/string/wordwrap.md @@ -2,7 +2,7 @@ title: "wordwrap() — internals" description: "Compiler internals for wordwrap(): lowering path, type checks, and runtime helpers." sidebar: - order: 413 + order: 414 --- ## `wordwrap()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/wordwrap.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/wordwrap.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:820](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L820) (`lower_wordwrap`) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:865](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L865) (`lower_wordwrap`) - **Function symbol**: `lower_wordwrap()` diff --git a/docs/internals/builtins/type/boolval.md b/docs/internals/builtins/type/boolval.md index f6c9053f32..549790d675 100644 --- a/docs/internals/builtins/type/boolval.md +++ b/docs/internals/builtins/type/boolval.md @@ -2,7 +2,7 @@ title: "boolval() — internals" description: "Compiler internals for boolval(): lowering path, type checks, and runtime helpers." sidebar: - order: 414 + order: 415 --- ## `boolval()` — internals diff --git a/docs/internals/builtins/type/ctype_alnum.md b/docs/internals/builtins/type/ctype_alnum.md index ee8a26ba95..f6eb252d12 100644 --- a/docs/internals/builtins/type/ctype_alnum.md +++ b/docs/internals/builtins/type/ctype_alnum.md @@ -2,7 +2,7 @@ title: "ctype_alnum() — internals" description: "Compiler internals for ctype_alnum(): lowering path, type checks, and runtime helpers." sidebar: - order: 415 + order: 416 --- ## `ctype_alnum()` — internals diff --git a/docs/internals/builtins/type/ctype_alpha.md b/docs/internals/builtins/type/ctype_alpha.md index c029edcf71..8765e595f8 100644 --- a/docs/internals/builtins/type/ctype_alpha.md +++ b/docs/internals/builtins/type/ctype_alpha.md @@ -2,7 +2,7 @@ title: "ctype_alpha() — internals" description: "Compiler internals for ctype_alpha(): lowering path, type checks, and runtime helpers." sidebar: - order: 416 + order: 417 --- ## `ctype_alpha()` — internals diff --git a/docs/internals/builtins/type/ctype_digit.md b/docs/internals/builtins/type/ctype_digit.md index dc9cd2cf56..42b9ce9b34 100644 --- a/docs/internals/builtins/type/ctype_digit.md +++ b/docs/internals/builtins/type/ctype_digit.md @@ -2,7 +2,7 @@ title: "ctype_digit() — internals" description: "Compiler internals for ctype_digit(): lowering path, type checks, and runtime helpers." sidebar: - order: 417 + order: 418 --- ## `ctype_digit()` — internals diff --git a/docs/internals/builtins/type/ctype_space.md b/docs/internals/builtins/type/ctype_space.md index 47ddb640ac..3aed48f4a6 100644 --- a/docs/internals/builtins/type/ctype_space.md +++ b/docs/internals/builtins/type/ctype_space.md @@ -2,7 +2,7 @@ title: "ctype_space() — internals" description: "Compiler internals for ctype_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 418 + order: 419 --- ## `ctype_space()` — internals diff --git a/docs/internals/builtins/type/floatval.md b/docs/internals/builtins/type/floatval.md index 9d7b91bb1d..072c15257d 100644 --- a/docs/internals/builtins/type/floatval.md +++ b/docs/internals/builtins/type/floatval.md @@ -2,7 +2,7 @@ title: "floatval() — internals" description: "Compiler internals for floatval(): lowering path, type checks, and runtime helpers." sidebar: - order: 419 + order: 420 --- ## `floatval()` — internals diff --git a/docs/internals/builtins/type/get_resource_id.md b/docs/internals/builtins/type/get_resource_id.md index 68aeaa0a4c..b9a8aa55d9 100644 --- a/docs/internals/builtins/type/get_resource_id.md +++ b/docs/internals/builtins/type/get_resource_id.md @@ -2,7 +2,7 @@ title: "get_resource_id() — internals" description: "Compiler internals for get_resource_id(): lowering path, type checks, and runtime helpers." sidebar: - order: 420 + order: 421 --- ## `get_resource_id()` — internals diff --git a/docs/internals/builtins/type/get_resource_type.md b/docs/internals/builtins/type/get_resource_type.md index a40b2cb24f..c28486759f 100644 --- a/docs/internals/builtins/type/get_resource_type.md +++ b/docs/internals/builtins/type/get_resource_type.md @@ -2,7 +2,7 @@ title: "get_resource_type() — internals" description: "Compiler internals for get_resource_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 421 + order: 422 --- ## `get_resource_type()` — internals diff --git a/docs/internals/builtins/type/gettype.md b/docs/internals/builtins/type/gettype.md index 6940b55ff1..7272647b30 100644 --- a/docs/internals/builtins/type/gettype.md +++ b/docs/internals/builtins/type/gettype.md @@ -2,7 +2,7 @@ title: "gettype() — internals" description: "Compiler internals for gettype(): lowering path, type checks, and runtime helpers." sidebar: - order: 422 + order: 423 --- ## `gettype()` — internals diff --git a/docs/internals/builtins/type/intval.md b/docs/internals/builtins/type/intval.md index a7f42ca644..c77300b93c 100644 --- a/docs/internals/builtins/type/intval.md +++ b/docs/internals/builtins/type/intval.md @@ -2,7 +2,7 @@ title: "intval() — internals" description: "Compiler internals for intval(): lowering path, type checks, and runtime helpers." sidebar: - order: 423 + order: 424 --- ## `intval()` — internals diff --git a/docs/internals/builtins/type/is_array.md b/docs/internals/builtins/type/is_array.md index ebd1fa9a72..68eca23430 100644 --- a/docs/internals/builtins/type/is_array.md +++ b/docs/internals/builtins/type/is_array.md @@ -2,7 +2,7 @@ title: "is_array() — internals" description: "Compiler internals for is_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 424 + order: 425 --- ## `is_array()` — internals diff --git a/docs/internals/builtins/type/is_bool.md b/docs/internals/builtins/type/is_bool.md index 4a9b9ba6cf..160f7d3d27 100644 --- a/docs/internals/builtins/type/is_bool.md +++ b/docs/internals/builtins/type/is_bool.md @@ -2,7 +2,7 @@ title: "is_bool() — internals" description: "Compiler internals for is_bool(): lowering path, type checks, and runtime helpers." sidebar: - order: 425 + order: 426 --- ## `is_bool()` — internals diff --git a/docs/internals/builtins/type/is_callable.md b/docs/internals/builtins/type/is_callable.md index fe038fea6d..6deaa7685d 100644 --- a/docs/internals/builtins/type/is_callable.md +++ b/docs/internals/builtins/type/is_callable.md @@ -2,7 +2,7 @@ title: "is_callable() — internals" description: "Compiler internals for is_callable(): lowering path, type checks, and runtime helpers." sidebar: - order: 426 + order: 427 --- ## `is_callable()` — internals diff --git a/docs/internals/builtins/type/is_float.md b/docs/internals/builtins/type/is_float.md index 4649c86f79..8097123eac 100644 --- a/docs/internals/builtins/type/is_float.md +++ b/docs/internals/builtins/type/is_float.md @@ -2,7 +2,7 @@ title: "is_float() — internals" description: "Compiler internals for is_float(): lowering path, type checks, and runtime helpers." sidebar: - order: 427 + order: 428 --- ## `is_float()` — internals diff --git a/docs/internals/builtins/type/is_int.md b/docs/internals/builtins/type/is_int.md index ac685e9c43..26cb7481ef 100644 --- a/docs/internals/builtins/type/is_int.md +++ b/docs/internals/builtins/type/is_int.md @@ -2,7 +2,7 @@ title: "is_int() — internals" description: "Compiler internals for is_int(): lowering path, type checks, and runtime helpers." sidebar: - order: 428 + order: 429 --- ## `is_int()` — internals diff --git a/docs/internals/builtins/type/is_iterable.md b/docs/internals/builtins/type/is_iterable.md index 2ab3517822..b28fc91b4d 100644 --- a/docs/internals/builtins/type/is_iterable.md +++ b/docs/internals/builtins/type/is_iterable.md @@ -2,7 +2,7 @@ title: "is_iterable() — internals" description: "Compiler internals for is_iterable(): lowering path, type checks, and runtime helpers." sidebar: - order: 429 + order: 430 --- ## `is_iterable()` — internals diff --git a/docs/internals/builtins/type/is_null.md b/docs/internals/builtins/type/is_null.md index b9dba16781..038f6c1fb9 100644 --- a/docs/internals/builtins/type/is_null.md +++ b/docs/internals/builtins/type/is_null.md @@ -2,7 +2,7 @@ title: "is_null() — internals" description: "Compiler internals for is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 430 + order: 431 --- ## `is_null()` — internals diff --git a/docs/internals/builtins/type/is_numeric.md b/docs/internals/builtins/type/is_numeric.md index fb1de2f6b3..cf72025ee6 100644 --- a/docs/internals/builtins/type/is_numeric.md +++ b/docs/internals/builtins/type/is_numeric.md @@ -2,7 +2,7 @@ title: "is_numeric() — internals" description: "Compiler internals for is_numeric(): lowering path, type checks, and runtime helpers." sidebar: - order: 431 + order: 432 --- ## `is_numeric()` — internals diff --git a/docs/internals/builtins/type/is_object.md b/docs/internals/builtins/type/is_object.md index b6cb3a488c..e5151f84b8 100644 --- a/docs/internals/builtins/type/is_object.md +++ b/docs/internals/builtins/type/is_object.md @@ -2,7 +2,7 @@ title: "is_object() — internals" description: "Compiler internals for is_object(): lowering path, type checks, and runtime helpers." sidebar: - order: 432 + order: 433 --- ## `is_object()` — internals diff --git a/docs/internals/builtins/type/is_resource.md b/docs/internals/builtins/type/is_resource.md index 25cb576ab1..d4224a4f38 100644 --- a/docs/internals/builtins/type/is_resource.md +++ b/docs/internals/builtins/type/is_resource.md @@ -2,7 +2,7 @@ title: "is_resource() — internals" description: "Compiler internals for is_resource(): lowering path, type checks, and runtime helpers." sidebar: - order: 433 + order: 434 --- ## `is_resource()` — internals diff --git a/docs/internals/builtins/type/is_scalar.md b/docs/internals/builtins/type/is_scalar.md index 3bf4b2e635..9b93411339 100644 --- a/docs/internals/builtins/type/is_scalar.md +++ b/docs/internals/builtins/type/is_scalar.md @@ -2,7 +2,7 @@ title: "is_scalar() — internals" description: "Compiler internals for is_scalar(): lowering path, type checks, and runtime helpers." sidebar: - order: 434 + order: 435 --- ## `is_scalar()` — internals diff --git a/docs/internals/builtins/type/is_string.md b/docs/internals/builtins/type/is_string.md index 3eeaf38330..909aea8ce6 100644 --- a/docs/internals/builtins/type/is_string.md +++ b/docs/internals/builtins/type/is_string.md @@ -2,7 +2,7 @@ title: "is_string() — internals" description: "Compiler internals for is_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 435 + order: 436 --- ## `is_string()` — internals diff --git a/docs/internals/builtins/type/settype.md b/docs/internals/builtins/type/settype.md index 91a22393a9..62cfd60a83 100644 --- a/docs/internals/builtins/type/settype.md +++ b/docs/internals/builtins/type/settype.md @@ -2,7 +2,7 @@ title: "settype() — internals" description: "Compiler internals for settype(): lowering path, type checks, and runtime helpers." sidebar: - order: 436 + order: 437 --- ## `settype()` — internals diff --git a/docs/php/builtins.md b/docs/php/builtins.md index e953762a18..3fe1859341 100644 --- a/docs/php/builtins.md +++ b/docs/php/builtins.md @@ -383,6 +383,7 @@ sidebar: | [`lcfirst()`](./builtins/string/lcfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`long2ip()`](./builtins/string/long2ip.md) | `(int $ip): string` | `string` | ✓ | ✓ | | [`ltrim()`](./builtins/string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`mb_strlen()`](./builtins/string/mb_strlen.md) | `(string $string, string $encoding = null): int` | `int` | ✓ | ✓ | | [`md5()`](./builtins/string/md5.md) | `(string $string, bool $binary = false): string` | `string` | ✓ | ✓ | | [`nl2br()`](./builtins/string/nl2br.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`number_format()`](./builtins/string/number_format.md) | `(float $num, int $decimals = 0, string $decimal_separator = '.', string $thousands_separator = ','): string` | `string` | ✓ | ✓ | diff --git a/docs/php/builtins/string.md b/docs/php/builtins/string.md index 9f54a0e732..2458ec39de 100644 --- a/docs/php/builtins/string.md +++ b/docs/php/builtins/string.md @@ -41,6 +41,7 @@ sidebar: | [`lcfirst()`](./string/lcfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`long2ip()`](./string/long2ip.md) | `(int $ip): string` | `string` | ✓ | ✓ | | [`ltrim()`](./string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`mb_strlen()`](./string/mb_strlen.md) | `(string $string, string $encoding = null): int` | `int` | ✓ | ✓ | | [`md5()`](./string/md5.md) | `(string $string, bool $binary = false): string` | `string` | ✓ | ✓ | | [`nl2br()`](./string/nl2br.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`number_format()`](./string/number_format.md) | `(float $num, int $decimals = 0, string $decimal_separator = '.', string $thousands_separator = ','): string` | `string` | ✓ | ✓ | diff --git a/docs/php/builtins/string/mb_strlen.md b/docs/php/builtins/string/mb_strlen.md new file mode 100644 index 0000000000..c6101757fc --- /dev/null +++ b/docs/php/builtins/string/mb_strlen.md @@ -0,0 +1,38 @@ +--- +title: "mb_strlen()" +description: "Returns the character count of a string in the requested encoding." +sidebar: + order: 375 +--- + +## mb_strlen() + +```php +function mb_strlen(string $string, string $encoding = null): int +``` + +Returns the character count of a string in the requested encoding. + +**Parameters**: +- `$string` (`string`) +- `$encoding` (`string`), default `null`, optional + +**Returns**: `int` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/mb_strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/mb_strlen.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `mb_strlen` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/mb_strlen.md). + diff --git a/docs/php/builtins/string/md5.md b/docs/php/builtins/string/md5.md index 78afd7989c..0780f88a76 100644 --- a/docs/php/builtins/string/md5.md +++ b/docs/php/builtins/string/md5.md @@ -2,7 +2,7 @@ title: "md5()" description: "Calculates the MD5 hash of a string." sidebar: - order: 375 + order: 376 --- ## md5() diff --git a/docs/php/builtins/string/nl2br.md b/docs/php/builtins/string/nl2br.md index d42522a437..09153ff5c9 100644 --- a/docs/php/builtins/string/nl2br.md +++ b/docs/php/builtins/string/nl2br.md @@ -2,7 +2,7 @@ title: "nl2br()" description: "Inserts HTML line breaks before newlines in a string." sidebar: - order: 376 + order: 377 --- ## nl2br() diff --git a/docs/php/builtins/string/number_format.md b/docs/php/builtins/string/number_format.md index e662c0d367..21187e07cd 100644 --- a/docs/php/builtins/string/number_format.md +++ b/docs/php/builtins/string/number_format.md @@ -2,7 +2,7 @@ title: "number_format()" description: "Formats a number with grouped thousands." sidebar: - order: 377 + order: 378 --- ## number_format() diff --git a/docs/php/builtins/string/ord.md b/docs/php/builtins/string/ord.md index 32e73ff2ac..332fb968d7 100644 --- a/docs/php/builtins/string/ord.md +++ b/docs/php/builtins/string/ord.md @@ -2,7 +2,7 @@ title: "ord()" description: "Returns the ASCII value of the first character of a string." sidebar: - order: 378 + order: 379 --- ## ord() diff --git a/docs/php/builtins/string/printf.md b/docs/php/builtins/string/printf.md index c0cf42b38d..c24997a931 100644 --- a/docs/php/builtins/string/printf.md +++ b/docs/php/builtins/string/printf.md @@ -2,7 +2,7 @@ title: "printf()" description: "Outputs a formatted string." sidebar: - order: 379 + order: 380 --- ## printf() diff --git a/docs/php/builtins/string/rawurldecode.md b/docs/php/builtins/string/rawurldecode.md index 6b283f1d50..2827fbe258 100644 --- a/docs/php/builtins/string/rawurldecode.md +++ b/docs/php/builtins/string/rawurldecode.md @@ -2,7 +2,7 @@ title: "rawurldecode()" description: "Decodes an RFC 3986 percent-encoded string without treating '+' as a space." sidebar: - order: 380 + order: 381 --- ## rawurldecode() diff --git a/docs/php/builtins/string/rawurlencode.md b/docs/php/builtins/string/rawurlencode.md index ba0bbe5bb9..7853df964c 100644 --- a/docs/php/builtins/string/rawurlencode.md +++ b/docs/php/builtins/string/rawurlencode.md @@ -2,7 +2,7 @@ title: "rawurlencode()" description: "URL-encodes a string using RFC 3986 percent-encoding (no '+' for spaces)." sidebar: - order: 381 + order: 382 --- ## rawurlencode() diff --git a/docs/php/builtins/string/rtrim.md b/docs/php/builtins/string/rtrim.md index 028e86cda6..3c3aa4f52b 100644 --- a/docs/php/builtins/string/rtrim.md +++ b/docs/php/builtins/string/rtrim.md @@ -2,7 +2,7 @@ title: "rtrim()" description: "Strips whitespace (or other characters) from the end of a string." sidebar: - order: 382 + order: 383 --- ## rtrim() diff --git a/docs/php/builtins/string/sha1.md b/docs/php/builtins/string/sha1.md index 5a374b0e21..30a71381f1 100644 --- a/docs/php/builtins/string/sha1.md +++ b/docs/php/builtins/string/sha1.md @@ -2,7 +2,7 @@ title: "sha1()" description: "Calculates the SHA-1 hash of a string." sidebar: - order: 383 + order: 384 --- ## sha1() diff --git a/docs/php/builtins/string/sprintf.md b/docs/php/builtins/string/sprintf.md index 25eb470c99..4bdbb2e290 100644 --- a/docs/php/builtins/string/sprintf.md +++ b/docs/php/builtins/string/sprintf.md @@ -2,7 +2,7 @@ title: "sprintf()" description: "Returns a formatted string." sidebar: - order: 384 + order: 385 --- ## sprintf() diff --git a/docs/php/builtins/string/sscanf.md b/docs/php/builtins/string/sscanf.md index 785fb4b3c7..f337d6ef7d 100644 --- a/docs/php/builtins/string/sscanf.md +++ b/docs/php/builtins/string/sscanf.md @@ -2,7 +2,7 @@ title: "sscanf()" description: "Parses a string according to a format." sidebar: - order: 385 + order: 386 --- ## sscanf() diff --git a/docs/php/builtins/string/str_contains.md b/docs/php/builtins/string/str_contains.md index de8fd5a766..e751d66f6b 100644 --- a/docs/php/builtins/string/str_contains.md +++ b/docs/php/builtins/string/str_contains.md @@ -2,7 +2,7 @@ title: "str_contains()" description: "Determines if a string contains a given substring." sidebar: - order: 386 + order: 387 --- ## str_contains() diff --git a/docs/php/builtins/string/str_ends_with.md b/docs/php/builtins/string/str_ends_with.md index 164376bb04..099668e4b1 100644 --- a/docs/php/builtins/string/str_ends_with.md +++ b/docs/php/builtins/string/str_ends_with.md @@ -2,7 +2,7 @@ title: "str_ends_with()" description: "Checks if a string ends with a given substring." sidebar: - order: 387 + order: 388 --- ## str_ends_with() diff --git a/docs/php/builtins/string/str_ireplace.md b/docs/php/builtins/string/str_ireplace.md index e9bba3b29c..6fcbb6cc88 100644 --- a/docs/php/builtins/string/str_ireplace.md +++ b/docs/php/builtins/string/str_ireplace.md @@ -2,7 +2,7 @@ title: "str_ireplace()" description: "Case-insensitive version of str_replace()." sidebar: - order: 388 + order: 389 --- ## str_ireplace() diff --git a/docs/php/builtins/string/str_pad.md b/docs/php/builtins/string/str_pad.md index 49d4be8770..e048b5f525 100644 --- a/docs/php/builtins/string/str_pad.md +++ b/docs/php/builtins/string/str_pad.md @@ -2,7 +2,7 @@ title: "str_pad()" description: "Pads a string to a certain length with another string." sidebar: - order: 389 + order: 390 --- ## str_pad() diff --git a/docs/php/builtins/string/str_repeat.md b/docs/php/builtins/string/str_repeat.md index a8ce358b4c..7306949932 100644 --- a/docs/php/builtins/string/str_repeat.md +++ b/docs/php/builtins/string/str_repeat.md @@ -2,7 +2,7 @@ title: "str_repeat()" description: "Repeats a string a given number of times." sidebar: - order: 390 + order: 391 --- ## str_repeat() diff --git a/docs/php/builtins/string/str_replace.md b/docs/php/builtins/string/str_replace.md index d018238fd8..a923bf216b 100644 --- a/docs/php/builtins/string/str_replace.md +++ b/docs/php/builtins/string/str_replace.md @@ -2,7 +2,7 @@ title: "str_replace()" description: "Replaces all occurrences of a search string with a replacement string." sidebar: - order: 391 + order: 392 --- ## str_replace() diff --git a/docs/php/builtins/string/str_split.md b/docs/php/builtins/string/str_split.md index 3d6d9432f5..9548d61f9c 100644 --- a/docs/php/builtins/string/str_split.md +++ b/docs/php/builtins/string/str_split.md @@ -2,7 +2,7 @@ title: "str_split()" description: "Converts a string into an array of chunks of the given length." sidebar: - order: 392 + order: 393 --- ## str_split() diff --git a/docs/php/builtins/string/str_starts_with.md b/docs/php/builtins/string/str_starts_with.md index 7e4c58141a..19e3e87f5a 100644 --- a/docs/php/builtins/string/str_starts_with.md +++ b/docs/php/builtins/string/str_starts_with.md @@ -2,7 +2,7 @@ title: "str_starts_with()" description: "Checks if a string starts with a given substring." sidebar: - order: 393 + order: 394 --- ## str_starts_with() diff --git a/docs/php/builtins/string/strcasecmp.md b/docs/php/builtins/string/strcasecmp.md index 8cf921556e..b3a8a03386 100644 --- a/docs/php/builtins/string/strcasecmp.md +++ b/docs/php/builtins/string/strcasecmp.md @@ -2,7 +2,7 @@ title: "strcasecmp()" description: "Binary safe case-insensitive string comparison. Returns negative, zero, or positive." sidebar: - order: 394 + order: 395 --- ## strcasecmp() diff --git a/docs/php/builtins/string/strcmp.md b/docs/php/builtins/string/strcmp.md index 04ba651fe6..a4cd6298a1 100644 --- a/docs/php/builtins/string/strcmp.md +++ b/docs/php/builtins/string/strcmp.md @@ -2,7 +2,7 @@ title: "strcmp()" description: "Binary safe string comparison. Returns negative, zero, or positive." sidebar: - order: 395 + order: 396 --- ## strcmp() diff --git a/docs/php/builtins/string/stripslashes.md b/docs/php/builtins/string/stripslashes.md index e75dee9b8a..6a578507f9 100644 --- a/docs/php/builtins/string/stripslashes.md +++ b/docs/php/builtins/string/stripslashes.md @@ -2,7 +2,7 @@ title: "stripslashes()" description: "Removes backslashes from a string previously escaped by addslashes." sidebar: - order: 396 + order: 397 --- ## stripslashes() diff --git a/docs/php/builtins/string/strlen.md b/docs/php/builtins/string/strlen.md index 2447dc287c..c420cc5de9 100644 --- a/docs/php/builtins/string/strlen.md +++ b/docs/php/builtins/string/strlen.md @@ -2,7 +2,7 @@ title: "strlen()" description: "Returns the length of a string." sidebar: - order: 397 + order: 398 --- ## strlen() diff --git a/docs/php/builtins/string/strpos.md b/docs/php/builtins/string/strpos.md index 8d25bdb870..61d131cbea 100644 --- a/docs/php/builtins/string/strpos.md +++ b/docs/php/builtins/string/strpos.md @@ -2,7 +2,7 @@ title: "strpos()" description: "Finds the numeric position of the first occurrence of a substring." sidebar: - order: 398 + order: 399 --- ## strpos() diff --git a/docs/php/builtins/string/strrev.md b/docs/php/builtins/string/strrev.md index cc53004b0a..ae12db20c6 100644 --- a/docs/php/builtins/string/strrev.md +++ b/docs/php/builtins/string/strrev.md @@ -2,7 +2,7 @@ title: "strrev()" description: "Reverses a string." sidebar: - order: 399 + order: 400 --- ## strrev() diff --git a/docs/php/builtins/string/strrpos.md b/docs/php/builtins/string/strrpos.md index a3e1d06704..0d773a0bac 100644 --- a/docs/php/builtins/string/strrpos.md +++ b/docs/php/builtins/string/strrpos.md @@ -2,7 +2,7 @@ title: "strrpos()" description: "Finds the numeric position of the last occurrence of a substring." sidebar: - order: 400 + order: 401 --- ## strrpos() diff --git a/docs/php/builtins/string/strstr.md b/docs/php/builtins/string/strstr.md index 02a3374550..03b38b673e 100644 --- a/docs/php/builtins/string/strstr.md +++ b/docs/php/builtins/string/strstr.md @@ -2,7 +2,7 @@ title: "strstr()" description: "Returns the portion of a string starting at the first occurrence of a substring." sidebar: - order: 401 + order: 402 --- ## strstr() diff --git a/docs/php/builtins/string/strtolower.md b/docs/php/builtins/string/strtolower.md index 2a10ffbd44..a25a74684a 100644 --- a/docs/php/builtins/string/strtolower.md +++ b/docs/php/builtins/string/strtolower.md @@ -2,7 +2,7 @@ title: "strtolower()" description: "Converts a string to lowercase." sidebar: - order: 402 + order: 403 --- ## strtolower() diff --git a/docs/php/builtins/string/strtoupper.md b/docs/php/builtins/string/strtoupper.md index 931bdc74d2..46459544b0 100644 --- a/docs/php/builtins/string/strtoupper.md +++ b/docs/php/builtins/string/strtoupper.md @@ -2,7 +2,7 @@ title: "strtoupper()" description: "Converts a string to uppercase." sidebar: - order: 403 + order: 404 --- ## strtoupper() diff --git a/docs/php/builtins/string/substr.md b/docs/php/builtins/string/substr.md index f59589b1fb..3ca26745e6 100644 --- a/docs/php/builtins/string/substr.md +++ b/docs/php/builtins/string/substr.md @@ -2,7 +2,7 @@ title: "substr()" description: "Returns a portion of a string specified by the offset and length." sidebar: - order: 404 + order: 405 --- ## substr() diff --git a/docs/php/builtins/string/substr_replace.md b/docs/php/builtins/string/substr_replace.md index 93fbe68cc9..053af8420b 100644 --- a/docs/php/builtins/string/substr_replace.md +++ b/docs/php/builtins/string/substr_replace.md @@ -2,7 +2,7 @@ title: "substr_replace()" description: "Replaces text within a portion of a string." sidebar: - order: 405 + order: 406 --- ## substr_replace() diff --git a/docs/php/builtins/string/trim.md b/docs/php/builtins/string/trim.md index 8a7b53e39a..0da266657e 100644 --- a/docs/php/builtins/string/trim.md +++ b/docs/php/builtins/string/trim.md @@ -2,7 +2,7 @@ title: "trim()" description: "Strips whitespace (or other characters) from the beginning and end of a string." sidebar: - order: 406 + order: 407 --- ## trim() diff --git a/docs/php/builtins/string/ucfirst.md b/docs/php/builtins/string/ucfirst.md index 761cf864f3..95455dd8c7 100644 --- a/docs/php/builtins/string/ucfirst.md +++ b/docs/php/builtins/string/ucfirst.md @@ -2,7 +2,7 @@ title: "ucfirst()" description: "Uppercases the first character of a string." sidebar: - order: 407 + order: 408 --- ## ucfirst() diff --git a/docs/php/builtins/string/ucwords.md b/docs/php/builtins/string/ucwords.md index e19ff6ae0f..a9edd8b145 100644 --- a/docs/php/builtins/string/ucwords.md +++ b/docs/php/builtins/string/ucwords.md @@ -2,7 +2,7 @@ title: "ucwords()" description: "Uppercases the first character of each word in a string." sidebar: - order: 408 + order: 409 --- ## ucwords() diff --git a/docs/php/builtins/string/urldecode.md b/docs/php/builtins/string/urldecode.md index c62400f0dd..ea38ad98ab 100644 --- a/docs/php/builtins/string/urldecode.md +++ b/docs/php/builtins/string/urldecode.md @@ -2,7 +2,7 @@ title: "urldecode()" description: "Decodes a URL-encoded string, including '+' as a space." sidebar: - order: 409 + order: 410 --- ## urldecode() diff --git a/docs/php/builtins/string/urlencode.md b/docs/php/builtins/string/urlencode.md index f851695738..bd16ac2467 100644 --- a/docs/php/builtins/string/urlencode.md +++ b/docs/php/builtins/string/urlencode.md @@ -2,7 +2,7 @@ title: "urlencode()" description: "URL-encodes a string using application/x-www-form-urlencoded rules." sidebar: - order: 410 + order: 411 --- ## urlencode() diff --git a/docs/php/builtins/string/vprintf.md b/docs/php/builtins/string/vprintf.md index ac88bfa994..44e8bfd4ad 100644 --- a/docs/php/builtins/string/vprintf.md +++ b/docs/php/builtins/string/vprintf.md @@ -2,7 +2,7 @@ title: "vprintf()" description: "Outputs a formatted string using an array of values." sidebar: - order: 411 + order: 412 --- ## vprintf() diff --git a/docs/php/builtins/string/vsprintf.md b/docs/php/builtins/string/vsprintf.md index 7aec19ae51..ba335905b4 100644 --- a/docs/php/builtins/string/vsprintf.md +++ b/docs/php/builtins/string/vsprintf.md @@ -2,7 +2,7 @@ title: "vsprintf()" description: "Returns a formatted string using an array of values." sidebar: - order: 412 + order: 413 --- ## vsprintf() diff --git a/docs/php/builtins/string/wordwrap.md b/docs/php/builtins/string/wordwrap.md index afae05ae8d..53991f9be2 100644 --- a/docs/php/builtins/string/wordwrap.md +++ b/docs/php/builtins/string/wordwrap.md @@ -2,7 +2,7 @@ title: "wordwrap()" description: "Wraps a string to a given number of characters." sidebar: - order: 413 + order: 414 --- ## wordwrap() diff --git a/docs/php/builtins/type/boolval.md b/docs/php/builtins/type/boolval.md index c708d4f738..422cfb1591 100644 --- a/docs/php/builtins/type/boolval.md +++ b/docs/php/builtins/type/boolval.md @@ -2,7 +2,7 @@ title: "boolval()" description: "Returns the boolean value of a variable." sidebar: - order: 414 + order: 415 --- ## boolval() diff --git a/docs/php/builtins/type/ctype_alnum.md b/docs/php/builtins/type/ctype_alnum.md index e0009bd063..3e2e5c4de3 100644 --- a/docs/php/builtins/type/ctype_alnum.md +++ b/docs/php/builtins/type/ctype_alnum.md @@ -2,7 +2,7 @@ title: "ctype_alnum()" description: "Checks if all characters in the string are alphanumeric." sidebar: - order: 415 + order: 416 --- ## ctype_alnum() diff --git a/docs/php/builtins/type/ctype_alpha.md b/docs/php/builtins/type/ctype_alpha.md index 1c78690cc1..9a649dd709 100644 --- a/docs/php/builtins/type/ctype_alpha.md +++ b/docs/php/builtins/type/ctype_alpha.md @@ -2,7 +2,7 @@ title: "ctype_alpha()" description: "Checks if all characters in the string are alphabetic." sidebar: - order: 416 + order: 417 --- ## ctype_alpha() diff --git a/docs/php/builtins/type/ctype_digit.md b/docs/php/builtins/type/ctype_digit.md index 0639b02ebc..af11dc09ab 100644 --- a/docs/php/builtins/type/ctype_digit.md +++ b/docs/php/builtins/type/ctype_digit.md @@ -2,7 +2,7 @@ title: "ctype_digit()" description: "Checks if all characters in the string are digits." sidebar: - order: 417 + order: 418 --- ## ctype_digit() diff --git a/docs/php/builtins/type/ctype_space.md b/docs/php/builtins/type/ctype_space.md index 3825e43c1c..9593951cfd 100644 --- a/docs/php/builtins/type/ctype_space.md +++ b/docs/php/builtins/type/ctype_space.md @@ -2,7 +2,7 @@ title: "ctype_space()" description: "Checks if all characters in the string are whitespace characters." sidebar: - order: 418 + order: 419 --- ## ctype_space() diff --git a/docs/php/builtins/type/floatval.md b/docs/php/builtins/type/floatval.md index 089f094671..3b7567b7ee 100644 --- a/docs/php/builtins/type/floatval.md +++ b/docs/php/builtins/type/floatval.md @@ -2,7 +2,7 @@ title: "floatval()" description: "Returns the float value of a variable." sidebar: - order: 419 + order: 420 --- ## floatval() diff --git a/docs/php/builtins/type/get_resource_id.md b/docs/php/builtins/type/get_resource_id.md index 1a2d929fd5..4f0059aefe 100644 --- a/docs/php/builtins/type/get_resource_id.md +++ b/docs/php/builtins/type/get_resource_id.md @@ -2,7 +2,7 @@ title: "get_resource_id()" description: "Returns an integer identifier for the given resource." sidebar: - order: 420 + order: 421 --- ## get_resource_id() diff --git a/docs/php/builtins/type/get_resource_type.md b/docs/php/builtins/type/get_resource_type.md index 62c4d71e3a..7ed41310c8 100644 --- a/docs/php/builtins/type/get_resource_type.md +++ b/docs/php/builtins/type/get_resource_type.md @@ -2,7 +2,7 @@ title: "get_resource_type()" description: "Returns the type of a resource." sidebar: - order: 421 + order: 422 --- ## get_resource_type() diff --git a/docs/php/builtins/type/gettype.md b/docs/php/builtins/type/gettype.md index fd840fb77d..21b915d63f 100644 --- a/docs/php/builtins/type/gettype.md +++ b/docs/php/builtins/type/gettype.md @@ -2,7 +2,7 @@ title: "gettype()" description: "Returns the type of a variable as a string." sidebar: - order: 422 + order: 423 --- ## gettype() diff --git a/docs/php/builtins/type/intval.md b/docs/php/builtins/type/intval.md index 8c584777c9..13ba82844f 100644 --- a/docs/php/builtins/type/intval.md +++ b/docs/php/builtins/type/intval.md @@ -2,7 +2,7 @@ title: "intval()" description: "Returns the integer value of a variable." sidebar: - order: 423 + order: 424 --- ## intval() diff --git a/docs/php/builtins/type/is_array.md b/docs/php/builtins/type/is_array.md index 1fdb21c808..8b52c62267 100644 --- a/docs/php/builtins/type/is_array.md +++ b/docs/php/builtins/type/is_array.md @@ -2,7 +2,7 @@ title: "is_array()" description: "Checks whether a variable is an array." sidebar: - order: 424 + order: 425 --- ## is_array() diff --git a/docs/php/builtins/type/is_bool.md b/docs/php/builtins/type/is_bool.md index fd8c2600af..7de543a492 100644 --- a/docs/php/builtins/type/is_bool.md +++ b/docs/php/builtins/type/is_bool.md @@ -2,7 +2,7 @@ title: "is_bool()" description: "Checks whether a variable is a boolean." sidebar: - order: 425 + order: 426 --- ## is_bool() diff --git a/docs/php/builtins/type/is_callable.md b/docs/php/builtins/type/is_callable.md index b5dfc3c88d..c0af63c53b 100644 --- a/docs/php/builtins/type/is_callable.md +++ b/docs/php/builtins/type/is_callable.md @@ -2,7 +2,7 @@ title: "is_callable()" description: "Checks whether a variable can be called as a function." sidebar: - order: 426 + order: 427 --- ## is_callable() diff --git a/docs/php/builtins/type/is_float.md b/docs/php/builtins/type/is_float.md index e18e41b2f9..a8a544eeea 100644 --- a/docs/php/builtins/type/is_float.md +++ b/docs/php/builtins/type/is_float.md @@ -2,7 +2,7 @@ title: "is_float()" description: "Checks whether a variable is a floating-point number." sidebar: - order: 427 + order: 428 --- ## is_float() diff --git a/docs/php/builtins/type/is_int.md b/docs/php/builtins/type/is_int.md index 56569d6bd5..76e66c0864 100644 --- a/docs/php/builtins/type/is_int.md +++ b/docs/php/builtins/type/is_int.md @@ -2,7 +2,7 @@ title: "is_int()" description: "Checks whether a variable is an integer." sidebar: - order: 428 + order: 429 --- ## is_int() diff --git a/docs/php/builtins/type/is_iterable.md b/docs/php/builtins/type/is_iterable.md index 931aae2140..72bab4a69e 100644 --- a/docs/php/builtins/type/is_iterable.md +++ b/docs/php/builtins/type/is_iterable.md @@ -2,7 +2,7 @@ title: "is_iterable()" description: "Checks whether a variable is iterable." sidebar: - order: 429 + order: 430 --- ## is_iterable() diff --git a/docs/php/builtins/type/is_null.md b/docs/php/builtins/type/is_null.md index 75062324ff..caea7788c9 100644 --- a/docs/php/builtins/type/is_null.md +++ b/docs/php/builtins/type/is_null.md @@ -2,7 +2,7 @@ title: "is_null()" description: "Checks whether a variable is null." sidebar: - order: 430 + order: 431 --- ## is_null() diff --git a/docs/php/builtins/type/is_numeric.md b/docs/php/builtins/type/is_numeric.md index ebe68cb067..d8dfe85e38 100644 --- a/docs/php/builtins/type/is_numeric.md +++ b/docs/php/builtins/type/is_numeric.md @@ -2,7 +2,7 @@ title: "is_numeric()" description: "Checks whether a variable is a number or a numeric string." sidebar: - order: 431 + order: 432 --- ## is_numeric() diff --git a/docs/php/builtins/type/is_object.md b/docs/php/builtins/type/is_object.md index b858336723..0caaa85f74 100644 --- a/docs/php/builtins/type/is_object.md +++ b/docs/php/builtins/type/is_object.md @@ -2,7 +2,7 @@ title: "is_object()" description: "Checks whether a variable is an object." sidebar: - order: 432 + order: 433 --- ## is_object() diff --git a/docs/php/builtins/type/is_resource.md b/docs/php/builtins/type/is_resource.md index 3680361239..af066bb563 100644 --- a/docs/php/builtins/type/is_resource.md +++ b/docs/php/builtins/type/is_resource.md @@ -2,7 +2,7 @@ title: "is_resource()" description: "Checks whether a variable is a resource." sidebar: - order: 433 + order: 434 --- ## is_resource() diff --git a/docs/php/builtins/type/is_scalar.md b/docs/php/builtins/type/is_scalar.md index 65aa1759aa..01e15a67bf 100644 --- a/docs/php/builtins/type/is_scalar.md +++ b/docs/php/builtins/type/is_scalar.md @@ -2,7 +2,7 @@ title: "is_scalar()" description: "Checks whether a variable is a scalar." sidebar: - order: 434 + order: 435 --- ## is_scalar() diff --git a/docs/php/builtins/type/is_string.md b/docs/php/builtins/type/is_string.md index c86c981aa9..2ba9e4bc76 100644 --- a/docs/php/builtins/type/is_string.md +++ b/docs/php/builtins/type/is_string.md @@ -2,7 +2,7 @@ title: "is_string()" description: "Checks whether a variable is a string." sidebar: - order: 435 + order: 436 --- ## is_string() diff --git a/docs/php/builtins/type/settype.md b/docs/php/builtins/type/settype.md index 609df86682..42c622523d 100644 --- a/docs/php/builtins/type/settype.md +++ b/docs/php/builtins/type/settype.md @@ -2,7 +2,7 @@ title: "settype()" description: "Sets the type of a variable." sidebar: - order: 436 + order: 437 --- ## settype() diff --git a/examples/string-ops/main.php b/examples/string-ops/main.php index 3b723f754d..ce5abc58f3 100644 --- a/examples/string-ops/main.php +++ b/examples/string-ops/main.php @@ -104,6 +104,8 @@ // Encoding echo "\n--- Encoding ---\n"; +echo "mb_strlen UTF-8: " . mb_strlen("héllo", "UTF-8") . "\n"; +echo "mb_strlen bytes: " . mb_strlen("héllo", "8bit") . "\n"; echo "htmlspecialchars: " . htmlspecialchars("bold") . "\n"; echo "urlencode: " . urlencode("hello world") . "\n"; echo "base64: " . base64_encode("Hello") . "\n"; diff --git a/scripts/docs/builtin_registry.json b/scripts/docs/builtin_registry.json index f4faf982b8..23926fbe0c 100644 --- a/scripts/docs/builtin_registry.json +++ b/scripts/docs/builtin_registry.json @@ -5426,7 +5426,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_chr", - "codegen_line": 876, + "codegen_line": 921, "notes": [ "Lowers `chr()` by converting an integer code point into a one-byte string." ], @@ -6536,9 +6536,7 @@ ], "runtime_helpers": [ "__rt_crc32", - "__rt_hash", - "__rt_md5", - "__rt_sha1" + "__rt_mb_strlen" ], "sig_arm": null, "sig_file": "src/builtins/string/crc32.rs", @@ -12165,7 +12163,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_gzcompress", - "codegen_line": 420, + "codegen_line": 465, "notes": [ "Lowers `gzcompress(data, level?)` through inline zlib `compress2` calls." ], @@ -12236,7 +12234,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_gzdeflate", - "codegen_line": 436, + "codegen_line": 481, "notes": [ "Lowers `gzdeflate(data, level?)` through inline raw-DEFLATE zlib calls." ], @@ -12307,7 +12305,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_gzinflate", - "codegen_line": 454, + "codegen_line": 499, "notes": [ "Lowers `gzinflate(data, max_length?)` and boxes zlib failures as PHP false." ], @@ -12378,7 +12376,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_gzuncompress", - "codegen_line": 475, + "codegen_line": 520, "notes": [ "Lowers `gzuncompress(data, max_length?)` and boxes zlib failures as PHP false." ], @@ -12583,9 +12581,7 @@ ], "runtime_helpers": [ "__rt_crc32", - "__rt_hash_copy", - "__rt_md5", - "__rt_sha1" + "__rt_hash_copy" ], "sig_arm": null, "sig_file": "src/builtins/string/hash_copy.rs", @@ -13883,7 +13879,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_inet", - "codegen_line": 515, + "codegen_line": 560, "notes": [ "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false." ], @@ -13943,7 +13939,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_inet", - "codegen_line": 515, + "codegen_line": 560, "notes": [ "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false." ], @@ -14206,7 +14202,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_ip2long", - "codegen_line": 506, + "codegen_line": 551, "notes": [ "Lowers `ip2long(string)` and boxes invalid-address results as PHP false." ], @@ -17126,7 +17122,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_long2ip", - "codegen_line": 494, + "codegen_line": 539, "notes": [ "Lowers `long2ip(value)` through the IPv4 formatting runtime helper." ], @@ -17433,6 +17429,80 @@ "slug": "mb_ereg_match", "sub_area": "Regex" }, + { + "area": "String", + "canonical_name": "mb_strlen", + "description": "Returns the character count of a string in the requested encoding.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/mb_strlen.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "encoding", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_mb_strlen", + "codegen_line": 375, + "notes": [ + "Lowers `mb_strlen(string, encoding = null)` through the multibyte runtime helper.", + "Omitted/null encodings use a null pointer plus zero length; explicit names stay byte strings for PHP-compatible case-insensitive lookup and `ValueError` handling." + ], + "runtime_helpers": [ + "__rt_mb_strlen" + ], + "sig_arm": null, + "sig_file": "src/builtins/string/mb_strlen.rs", + "sig_line": null + }, + "name": "mb_strlen", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "null", + "name": "encoding", + "optional": true, + "type": "string" + } + ], + "return_type": "int", + "variadic": null + }, + "slug": "mb_strlen", + "sub_area": "String" + }, { "area": "String", "canonical_name": "md5", @@ -17471,7 +17541,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_md5", - "codegen_line": 373, + "codegen_line": 418, "notes": [ "Lowers `md5(data, binary?)` through the shared crypto-backed runtime helper." ], @@ -18134,7 +18204,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_number_format", - "codegen_line": 893, + "codegen_line": 938, "notes": [ "Lowers `number_format()` by arranging its runtime helper arguments." ], @@ -18277,7 +18347,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_ord", - "codegen_line": 852, + "codegen_line": 897, "notes": [ "Lowers `ord()` by returning the first byte of a string or zero for empty input." ], @@ -19460,7 +19530,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_printf", - "codegen_line": 535, + "codegen_line": 580, "notes": [ "Lowers `printf(format, values...)` as `sprintf()` followed by stdout emission." ], @@ -21975,7 +22045,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_sha1", - "codegen_line": 378, + "codegen_line": 423, "notes": [ "Lowers `sha1(data, binary?)` through the shared crypto-backed runtime helper." ], @@ -22939,7 +23009,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_sprintf", - "codegen_line": 529, + "codegen_line": 574, "notes": [ "Lowers `sprintf(format, values...)` by packing variadic records for `__rt_sprintf`." ], @@ -23199,7 +23269,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_str_contains", - "codegen_line": 700, + "codegen_line": 745, "notes": [ "Lowers `str_contains()` through `strpos()` and converts found positions to bool." ], @@ -23357,7 +23427,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_replace", - "codegen_line": 798, + "codegen_line": 843, "notes": [ "Lowers `str_replace()`/`str_ireplace()` with three string operands." ], @@ -23454,7 +23524,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_str_pad", - "codegen_line": 836, + "codegen_line": 881, "notes": [ "Lowers `str_pad(string, length, pad_string?, pad_type?)` through the shared runtime helper." ], @@ -23541,7 +23611,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_str_repeat", - "codegen_line": 764, + "codegen_line": 809, "notes": [ "Lowers `str_repeat(string, times)` through the shared runtime helper." ], @@ -23626,7 +23696,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_replace", - "codegen_line": 798, + "codegen_line": 843, "notes": [ "Lowers `str_replace()`/`str_ireplace()` with three string operands." ], @@ -27308,7 +27378,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_position", - "codegen_line": 718, + "codegen_line": 763, "notes": [ "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed." ], @@ -27452,7 +27522,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_position", - "codegen_line": 718, + "codegen_line": 763, "notes": [ "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed." ], @@ -27536,7 +27606,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_strstr", - "codegen_line": 780, + "codegen_line": 825, "notes": [ "Lowers `strstr(haystack, needle)` by searching and returning the matching suffix." ], @@ -27819,7 +27889,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_substr", - "codegen_line": 731, + "codegen_line": 776, "notes": [ "Lowers `substr(string, offset, length?)` with target-local pointer arithmetic." ], @@ -27911,7 +27981,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_substr_replace", - "codegen_line": 748, + "codegen_line": 793, "notes": [ "Lowers `substr_replace(string, replacement, start, length?)`." ], @@ -29600,7 +29670,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_vprintf", - "codegen_line": 548, + "codegen_line": 593, "notes": [ "Lowers `vprintf(format, values)` as `vsprintf()` followed by stdout emission." ], @@ -29673,7 +29743,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_vsprintf", - "codegen_line": 542, + "codegen_line": 587, "notes": [ "Lowers `vsprintf(format, values)` through the array-to-sprintf runtime bridge." ], @@ -29758,7 +29828,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_wordwrap", - "codegen_line": 820, + "codegen_line": 865, "notes": [ "Lowers `wordwrap(string, width?, break?, cut?)` through the shared runtime helper." ], diff --git a/src/builtins/string/mb_strlen.rs b/src/builtins/string/mb_strlen.rs new file mode 100644 index 0000000000..d75b433890 --- /dev/null +++ b/src/builtins/string/mb_strlen.rs @@ -0,0 +1,60 @@ +//! Purpose: +//! Home of the PHP `mb_strlen` builtin: declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), both via +//! `crate::builtins::registry`. +//! +//! Key details: +//! - The public signature matches PHP: `mb_strlen(string $string, ?string $encoding = null)`. +//! - Omitted/null encoding uses UTF-8; explicit encodings are handled by the target runtime, +//! which keeps malformed-sequence counting aligned with mbstring and rejects unknown names. + +use crate::{ + builtins::spec::{BuiltinCheckCtx, DefaultSpec}, + codegen::{context::FunctionContext, CodegenIrError}, + errors::CompileError, + ir::Instruction, + types::PhpType, +}; + +builtin! { + name: "mb_strlen", + area: String, + params: [string: Str, encoding: Str = DefaultSpec::Null], + returns: Int, + check: check, + lazy_check: true, + lower: lower, + summary: "Returns the character count of a string in the requested encoding.", + php_manual: "https://www.php.net/manual/en/function.mb-strlen.php", +} + +/// Validates PHP's string plus nullable optional encoding parameter surface. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_macos_builtin_library("iconv"); + let string_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if string_ty != PhpType::Str { + return Err(CompileError::new( + cx.args[0].span, + "mb_strlen() string argument must be string", + )); + } + + if let Some(encoding) = cx.args.get(1) { + let encoding_ty = cx.checker.infer_type(encoding, cx.env)?; + if !matches!(encoding_ty, PhpType::Str | PhpType::Void) { + return Err(CompileError::new( + encoding.span, + "mb_strlen() encoding argument must be string or null", + )); + } + } + + Ok(PhpType::Int) +} + +/// Lowers an `mb_strlen` call by dispatching to the shared `lower_mb_strlen` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_mb_strlen(ctx, inst) +} diff --git a/src/builtins/string/mod.rs b/src/builtins/string/mod.rs index c7ce971ed3..bee4f994b5 100644 --- a/src/builtins/string/mod.rs +++ b/src/builtins/string/mod.rs @@ -48,6 +48,7 @@ pub mod lcfirst; pub mod long2ip; pub mod ltrim; pub mod mb_ereg_match; +pub mod mb_strlen; pub mod md5; pub mod nl2br; pub mod number_format; diff --git a/src/codegen/lower_inst/builtins/strings.rs b/src/codegen/lower_inst/builtins/strings.rs index fdd817ebba..3e63ee06cf 100644 --- a/src/codegen/lower_inst/builtins/strings.rs +++ b/src/codegen/lower_inst/builtins/strings.rs @@ -369,6 +369,51 @@ pub(crate) fn lower_crc32(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> store_if_result(ctx, inst) } +/// Lowers `mb_strlen(string, encoding = null)` through the multibyte runtime helper. +/// +/// Omitted/null encodings use a null pointer plus zero length; explicit names stay byte strings for PHP-compatible case-insensitive lookup and `ValueError` handling. +pub(crate) fn lower_mb_strlen(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + super::ensure_arg_count_between(inst, "mb_strlen", 1, 2)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + load_string_arg_to_regs(ctx, inst, 0, "mb_strlen", "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the source string while loading the optional encoding + load_optional_mb_strlen_encoding(ctx, inst, "x3", "x4")?; + ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the source string for the runtime helper + } + Arch::X86_64 => { + load_string_arg_to_regs(ctx, inst, 0, "mb_strlen", "rax", "rdx")?; + ctx.emitter.instruction("push rax"); // preserve the source string pointer while loading the optional encoding + ctx.emitter.instruction("push rdx"); // preserve the source string length while loading the optional encoding + load_optional_mb_strlen_encoding(ctx, inst, "r8", "r9")?; + ctx.emitter.instruction("pop rdx"); // restore the source string length for the runtime helper + ctx.emitter.instruction("pop rax"); // restore the source string pointer for the runtime helper + } + } + abi::emit_call_label(ctx.emitter, "__rt_mb_strlen"); + store_if_result(ctx, inst) +} + +/// Loads the nullable optional `mb_strlen()` encoding into a pointer/length pair. +fn load_optional_mb_strlen_encoding( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + ptr_reg: &str, + len_reg: &str, +) -> Result<()> { + let Some(encoding) = inst.operands.get(1).copied() else { + abi::emit_load_int_immediate(ctx.emitter, ptr_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, len_reg, 0); + return Ok(()); + }; + if matches!(ctx.value_php_type(encoding)?, PhpType::Void | PhpType::Never) { + abi::emit_load_int_immediate(ctx.emitter, ptr_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, len_reg, 0); + return Ok(()); + } + load_value_as_string_to_regs(ctx, encoding, "mb_strlen encoding", ptr_reg, len_reg) +} + /// Lowers `md5(data, binary?)` through the shared crypto-backed runtime helper. pub(crate) fn lower_md5(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { lower_fixed_hash(ctx, inst, "md5", "__rt_md5") diff --git a/src/codegen_support/runtime/arrays/mod.rs b/src/codegen_support/runtime/arrays/mod.rs index b5fd6ca35d..8833f69846 100644 --- a/src/codegen_support/runtime/arrays/mod.rs +++ b/src/codegen_support/runtime/arrays/mod.rs @@ -154,7 +154,7 @@ mod sort_int; mod sort_str; mod undefined_array_key_warning; mod usort; -mod value_error; +pub(super) mod value_error; pub use array_chunk::emit_array_chunk; /// Emit array chunk helper (split array into chunks). diff --git a/src/codegen_support/runtime/arrays/value_error.rs b/src/codegen_support/runtime/arrays/value_error.rs index 30b40ea716..a337734532 100644 --- a/src/codegen_support/runtime/arrays/value_error.rs +++ b/src/codegen_support/runtime/arrays/value_error.rs @@ -5,6 +5,7 @@ //! Called from: //! - `crate::codegen_support::runtime::arrays::array_filter`. //! - `crate::codegen_support::runtime::arrays::array_filter_refcounted`. +//! - `crate::codegen_support::runtime::strings::mb_strlen`. //! //! Key details: //! - The emitted sequence does not return; it publishes `_exc_value` and enters the unwinder. @@ -17,7 +18,7 @@ use crate::codegen_support::emit::Emitter; /// Allocates a 32-byte Throwable payload, stamps the per-program `ValueError` /// class id, stores the message pointer/length and zero code, then jumps to /// `__rt_throw_current`. -pub(super) fn emit_throw_value_error_aarch64( +pub(in crate::codegen_support::runtime) fn emit_throw_value_error_aarch64( emitter: &mut Emitter, message_symbol: &str, message_len: usize, @@ -43,7 +44,7 @@ pub(super) fn emit_throw_value_error_aarch64( /// /// Preserves `rbp`, aligns the nested allocation call, writes the standard /// Throwable payload layout, and jumps to `__rt_throw_current` without returning. -pub(super) fn emit_throw_value_error_x86_64( +pub(in crate::codegen_support::runtime) fn emit_throw_value_error_x86_64( emitter: &mut Emitter, message_symbol: &str, message_len: usize, diff --git a/src/codegen_support/runtime/data/fixed.rs b/src/codegen_support/runtime/data/fixed.rs index 281a4efc0f..7f40ffaf4d 100644 --- a/src/codegen_support/runtime/data/fixed.rs +++ b/src/codegen_support/runtime/data/fixed.rs @@ -10,7 +10,7 @@ use super::{ DIRNAME_LEVELS_MSG, HASH_HMAC_UNKNOWN_ALGO_MSG, HASH_INIT_UNKNOWN_ALGO_MSG, - HASH_UNKNOWN_ALGO_MSG, + HASH_UNKNOWN_ALGO_MSG, MB_STRLEN_UNKNOWN_ENCODING_MSG, PHP_UNAME_MODE_LEN_MSG, PHP_UNAME_MODE_VALUE_MSG, STR_REPEAT_TIMES_MSG, }; use super::super::system; @@ -163,6 +163,16 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin ".globl _hash_init_unknown_algo_msg\n_hash_init_unknown_algo_msg:\n .ascii {:?}\n", HASH_INIT_UNKNOWN_ALGO_MSG )); + out.push_str(&format!( + ".globl _mb_strlen_unknown_encoding_msg\n_mb_strlen_unknown_encoding_msg:\n .ascii {:?}\n", + MB_STRLEN_UNKNOWN_ENCODING_MSG + )); + out.push_str(".globl _mb_strlen_utf8_name\n_mb_strlen_utf8_name:\n .asciz \"UTF-8\"\n"); + out.push_str(".globl _mb_strlen_utf8_alias\n_mb_strlen_utf8_alias:\n .asciz \"UTF8\"\n"); + out.push_str(".globl _mb_strlen_utf32le_name\n_mb_strlen_utf32le_name:\n .asciz \"UTF-32LE\"\n"); + out.push_str(".globl _mb_strlen_8bit_name\n_mb_strlen_8bit_name:\n .asciz \"8bit\"\n"); + out.push_str(".globl _mb_strlen_binary_name\n_mb_strlen_binary_name:\n .asciz \"binary\"\n"); + out.push_str(".globl _mb_strlen_7bit_name\n_mb_strlen_7bit_name:\n .asciz \"7bit\"\n"); // Fixed algorithm-name constants for md5()/sha1(): both route through the // same elephc_crypto_hash entry point as hash(), so __rt_md5 / __rt_sha1 // load these literal names into the algorithm-name register pair before diff --git a/src/codegen_support/runtime/data/mod.rs b/src/codegen_support/runtime/data/mod.rs index bd133beeb6..b92f6c264c 100644 --- a/src/codegen_support/runtime/data/mod.rs +++ b/src/codegen_support/runtime/data/mod.rs @@ -39,3 +39,6 @@ pub(crate) const HASH_INIT_UNKNOWN_ALGO_MSG: &str = /// name or a non-cryptographic checksum (PHP rejects HMAC over crc32/adler/fnv/joaat). pub(crate) const HASH_HMAC_UNKNOWN_ALGO_MSG: &str = "hash_hmac(): Argument #1 ($algo) must be a valid cryptographic hashing algorithm"; +/// Catchable `\ValueError` message when `mb_strlen()` receives an unknown encoding name. +pub(crate) const MB_STRLEN_UNKNOWN_ENCODING_MSG: &str = + "mb_strlen(): Argument #2 ($encoding) must be a valid encoding"; diff --git a/src/codegen_support/runtime/emitters.rs b/src/codegen_support/runtime/emitters.rs index 083b72b94c..b8e0a513d2 100644 --- a/src/codegen_support/runtime/emitters.rs +++ b/src/codegen_support/runtime/emitters.rs @@ -94,6 +94,9 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { strings::emit_md5(emitter); strings::emit_sha1(emitter); strings::emit_crc32(emitter); + if features.mb_strlen { + strings::emit_mb_strlen(emitter); + } strings::emit_hash(emitter); strings::emit_hash_hmac(emitter); strings::emit_hash_equals(emitter); @@ -603,6 +606,25 @@ mod tests { assert!(!asm.contains("__rt_preg_split:")); } + /// Verifies the iconv-backed `mb_strlen()` helper is emitted only for programs that use it. + #[test] + fn test_runtime_can_gate_mb_strlen_helper() { + let target = Target::new(Platform::MacOS, Arch::AArch64); + let mut omitted = Emitter::new(target); + emit_runtime(&mut omitted, RuntimeFeatures::none()); + assert!(!omitted.output().contains("__rt_mb_strlen:")); + + let mut included = Emitter::new(target); + emit_runtime( + &mut included, + RuntimeFeatures { + mb_strlen: true, + ..RuntimeFeatures::none() + }, + ); + assert!(included.output().contains("__rt_mb_strlen:")); + } + /// Verifies that Linux x86_64 uses the shared runtime surface. #[test] fn test_linux_x86_64_runtime_uses_shared_surface() { diff --git a/src/codegen_support/runtime/strings/mb_strlen.rs b/src/codegen_support/runtime/strings/mb_strlen.rs new file mode 100644 index 0000000000..b6687390a7 --- /dev/null +++ b/src/codegen_support/runtime/strings/mb_strlen.rs @@ -0,0 +1,625 @@ +//! Purpose: +//! Emits `__rt_mb_strlen`, the runtime helper for PHP's `mb_strlen()`. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()`. +//! - `crate::codegen::lower_inst::builtins::strings::lower_mb_strlen()`. +//! +//! Key details: +//! - Omitted/null encoding uses an allocation-free UTF-8 scanner that groups truncated valid +//! prefixes and malformed bytes like PHP mbstring. +//! - `8bit`/`binary`/`7bit` return the byte length; other explicit encodings are decoded through +//! libc `iconv` into a fixed UTF-32LE scratch buffer, so every supported target shares the same +//! character-count contract without allocating proportionally to the input. +//! - Unknown encodings throw a catchable `ValueError` through the normal runtime unwinder. + +use crate::codegen_support::{ + abi, + emit::Emitter, + platform::{Arch, Platform}, + runtime::{arrays::value_error, data::MB_STRLEN_UNKNOWN_ENCODING_MSG}, +}; + +/// Maximum explicit encoding-name length copied into the runtime's stack buffer. +const MAX_ENCODING_NAME_LEN: usize = 63; + +/// Emits `__rt_mb_strlen(str_ptr, str_len, encoding_ptr, encoding_len) -> count`. +pub fn emit_mb_strlen(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_mb_strlen_x86_64(emitter); + } else { + emit_mb_strlen_aarch64(emitter); + } +} + +/// Emits the AArch64 implementation for macOS and Linux. +fn emit_mb_strlen_aarch64(emitter: &mut Emitter) { + let errno_function = match emitter.platform { + Platform::MacOS => "__error", + Platform::Linux => "__errno_location", + Platform::Windows => panic!("Windows target is not yet supported (see issue #379)"), + }; + + emitter.blank(); + emitter.comment("--- runtime: mb_strlen (encoding-aware character count) ---"); + emitter.label_global("__rt_mb_strlen"); + emitter.instruction("cbz x3, __rt_mb_strlen_utf8"); // omitted/null encoding uses the default UTF-8 scanner + emitter.instruction(&format!("cmp x4, #{}", MAX_ENCODING_NAME_LEN)); // does the explicit encoding name fit the stack C-string buffer? + emitter.instruction("b.hi __rt_mb_strlen_unknown_encoding"); // reject names longer than every PHP-supported encoding alias + emitter.instruction("sub sp, sp, #176"); // reserve iconv state, output scratch, and encoding-name storage + emitter.instruction("stp x29, x30, [sp, #160]"); // preserve the caller frame and return address across libc calls + emitter.instruction("add x29, sp, #160"); // establish the helper frame pointer + emitter.instruction("str x1, [sp, #0]"); // iconv input pointer variable starts at the PHP string bytes + emitter.instruction("str x2, [sp, #8]"); // iconv input byte count variable starts at the PHP string length + emitter.instruction("str xzr, [sp, #16]"); // decoded character count starts at zero + + // -- copy the length-delimited PHP encoding name into a stack C string -- + emitter.instruction("add x9, sp, #80"); // destination is the 64-byte encoding-name buffer + emitter.instruction("mov x10, #0"); // copied-byte index starts at zero + emitter.label("__rt_mb_strlen_encoding_copy"); + emitter.instruction("cmp x10, x4"); // copied the whole explicit encoding name? + emitter.instruction("b.hs __rt_mb_strlen_encoding_copied"); // terminate the C string once every byte is copied + emitter.instruction("ldrb w11, [x3, x10]"); // load one encoding-name byte from the PHP string + emitter.instruction("strb w11, [x9, x10]"); // append the byte to the stack C string + emitter.instruction("add x10, x10, #1"); // advance the encoding-name byte index + emitter.instruction("b __rt_mb_strlen_encoding_copy"); // continue copying the remaining encoding-name bytes + emitter.label("__rt_mb_strlen_encoding_copied"); + emitter.instruction("strb wzr, [x9, x4]"); // NUL-terminate the explicit encoding name + + // -- fast-path PHP's default UTF-8 names and byte-count encodings -- + emitter.instruction("add x0, sp, #80"); // first strcasecmp argument is the copied encoding name + abi::emit_symbol_address(emitter, "x1", "_mb_strlen_utf8_name"); + emitter.bl_c("strcasecmp"); // compare the explicit encoding with UTF-8 case-insensitively + emitter.instruction("cbz x0, __rt_mb_strlen_use_utf8_framed"); // UTF-8 uses the allocation-free validated scanner + emitter.instruction("add x0, sp, #80"); // reload the copied encoding name after strcasecmp + abi::emit_symbol_address(emitter, "x1", "_mb_strlen_utf8_alias"); + emitter.bl_c("strcasecmp"); // compare the explicit encoding with PHP's UTF8 alias + emitter.instruction("cbz x0, __rt_mb_strlen_use_utf8_framed"); // the UTF8 alias uses the same validated scanner + emitter.instruction("add x0, sp, #80"); // reload the copied encoding name for the byte-count aliases + abi::emit_symbol_address(emitter, "x1", "_mb_strlen_8bit_name"); + emitter.bl_c("strcasecmp"); // compare the explicit encoding with 8bit + emitter.instruction("cbz x0, __rt_mb_strlen_use_byte_length"); // 8bit counts every byte as one character + emitter.instruction("add x0, sp, #80"); // reload the copied encoding name for the binary alias + abi::emit_symbol_address(emitter, "x1", "_mb_strlen_binary_name"); + emitter.bl_c("strcasecmp"); // compare the explicit encoding with binary + emitter.instruction("cbz x0, __rt_mb_strlen_use_byte_length"); // binary is PHP's alias for 8bit + emitter.instruction("add x0, sp, #80"); // reload the copied encoding name for the 7bit encoding + abi::emit_symbol_address(emitter, "x1", "_mb_strlen_7bit_name"); + emitter.bl_c("strcasecmp"); // compare the explicit encoding with 7bit + emitter.instruction("cbz x0, __rt_mb_strlen_use_byte_length"); // 7bit preserves PHP's one-character-per-byte count + + // -- open a decoder from the requested encoding to fixed-width UTF-32LE -- + abi::emit_symbol_address(emitter, "x0", "_mb_strlen_utf32le_name"); + emitter.instruction("add x1, sp, #80"); // iconv source encoding is the copied explicit name + emitter.bl_c("iconv_open"); // create the encoding-to-UTF-32LE conversion descriptor + emitter.instruction("cmn x0, #1"); // did iconv_open return the `(iconv_t)-1` failure sentinel? + emitter.instruction("b.eq __rt_mb_strlen_unknown_encoding_framed"); // unknown encoding names raise PHP's ValueError + emitter.instruction("str x0, [sp, #24]"); // preserve the iconv descriptor across conversion iterations + + // -- decode chunks into 16 bytes of UTF-32LE and count four-byte code points -- + emitter.label("__rt_mb_strlen_iconv_loop"); + emitter.instruction("ldr x9, [sp, #8]"); // load the number of input bytes still undecoded + emitter.instruction("cbz x9, __rt_mb_strlen_iconv_done"); // close the descriptor after all bytes are consumed + emitter.instruction("add x9, sp, #48"); // point at the fixed 16-byte UTF-32LE output scratch + emitter.instruction("str x9, [sp, #32]"); // initialize iconv's mutable output pointer + emitter.instruction("mov x9, #16"); // each conversion iteration has 16 output bytes available + emitter.instruction("str x9, [sp, #40]"); // initialize iconv's mutable output-byte count + emitter.instruction("ldr x0, [sp, #24]"); // iconv argument 0 is the conversion descriptor + emitter.instruction("add x1, sp, #0"); // iconv argument 1 is `&input_ptr` + emitter.instruction("add x2, sp, #8"); // iconv argument 2 is `&input_bytes_left` + emitter.instruction("add x3, sp, #32"); // iconv argument 3 is `&output_ptr` + emitter.instruction("add x4, sp, #40"); // iconv argument 4 is `&output_bytes_left` + emitter.bl_c("iconv"); // decode as many complete characters as fit in the fixed output scratch + emitter.instruction("str x0, [sp, #72]"); // preserve iconv's status while accounting for produced code points + emitter.instruction("ldr x9, [sp, #40]"); // load unused output bytes after the conversion attempt + emitter.instruction("mov x10, #16"); // reload the fixed output scratch capacity + emitter.instruction("sub x10, x10, x9"); // compute the number of UTF-32LE bytes produced + emitter.instruction("lsr x10, x10, #2"); // four output bytes represent one decoded character + emitter.instruction("ldr x11, [sp, #16]"); // load the accumulated character count + emitter.instruction("add x11, x11, x10"); // add every character decoded in this iteration + emitter.instruction("str x11, [sp, #16]"); // persist the updated character count + emitter.instruction("ldr x0, [sp, #72]"); // restore iconv's return status + emitter.instruction("cmn x0, #1"); // did iconv report an incomplete, malformed, or full-output condition? + emitter.instruction("b.ne __rt_mb_strlen_iconv_loop"); // successful partial progress continues until input is exhausted + emitter.instruction("ldr x9, [sp, #40]"); // inspect remaining output capacity before consulting errno + emitter.instruction("cbz x9, __rt_mb_strlen_iconv_loop"); // a full output buffer is E2BIG and only requires another iteration + emitter.bl_c(errno_function); // fetch the platform thread-local errno written by iconv + emitter.instruction("ldr w9, [x0]"); // load iconv's errno value + emitter.instruction("cmp w9, #22"); // EINVAL means the input ends in a valid but truncated sequence + emitter.instruction("b.eq __rt_mb_strlen_iconv_incomplete"); // mbstring groups that truncated prefix as one character + emitter.instruction("ldr x9, [sp, #8]"); // load bytes remaining at a malformed sequence + emitter.instruction("cbz x9, __rt_mb_strlen_iconv_done"); // defensive completion if iconv consumed the final byte + emitter.instruction("ldr x10, [sp, #0]"); // load iconv's current input pointer + emitter.instruction("add x10, x10, #1"); // skip one malformed input byte like mbstring substitution + emitter.instruction("str x10, [sp, #0]"); // persist the advanced input pointer + emitter.instruction("sub x9, x9, #1"); // remove the malformed byte from the remaining input count + emitter.instruction("str x9, [sp, #8]"); // persist the reduced input byte count + emitter.instruction("ldr x10, [sp, #16]"); // load the accumulated character count + emitter.instruction("add x10, x10, #1"); // one malformed byte becomes one substitution character + emitter.instruction("str x10, [sp, #16]"); // persist the malformed-byte character count + emitter.instruction("ldr x0, [sp, #24]"); // reload the iconv descriptor for a state reset + emitter.instruction("mov x1, #0"); // null input pointer requests iconv shift-state reset + emitter.instruction("mov x2, #0"); // no input byte count participates in the reset + emitter.instruction("mov x3, #0"); // no output pointer participates in the reset + emitter.instruction("mov x4, #0"); // no output byte count participates in the reset + emitter.bl_c("iconv"); // reset stateful decoders after substituting one malformed byte + emitter.instruction("b __rt_mb_strlen_iconv_loop"); // continue decoding after the malformed byte + + emitter.label("__rt_mb_strlen_iconv_incomplete"); + emitter.instruction("ldr x9, [sp, #16]"); // load the character count before the truncated suffix + emitter.instruction("add x9, x9, #1"); // count the whole truncated valid prefix as one character + emitter.instruction("str x9, [sp, #16]"); // persist the final character count + emitter.instruction("str xzr, [sp, #8]"); // mark the truncated suffix as fully handled + emitter.label("__rt_mb_strlen_iconv_done"); + emitter.instruction("ldr x0, [sp, #24]"); // iconv_close argument is the active conversion descriptor + emitter.bl_c("iconv_close"); // release the conversion descriptor before returning + emitter.instruction("ldr x0, [sp, #16]"); // return the accumulated character count + emitter.instruction("ldp x29, x30, [sp, #160]"); // restore the caller frame and return address + emitter.instruction("add sp, sp, #176"); // release the iconv helper frame + emitter.instruction("ret"); // return the encoding-aware character count + + emitter.label("__rt_mb_strlen_use_utf8_framed"); + emitter.instruction("ldr x1, [sp, #0]"); // restore the PHP string pointer for the UTF-8 scanner + emitter.instruction("ldr x2, [sp, #8]"); // restore the PHP string length for the UTF-8 scanner + emitter.instruction("ldp x29, x30, [sp, #160]"); // restore the caller frame and return address + emitter.instruction("add sp, sp, #176"); // release the explicit-encoding helper frame + emitter.instruction("b __rt_mb_strlen_utf8"); // tail-dispatch to the validated UTF-8 scanner + + emitter.label("__rt_mb_strlen_use_byte_length"); + emitter.instruction("ldr x0, [sp, #8]"); // byte encodings count every source byte as one character + emitter.instruction("ldp x29, x30, [sp, #160]"); // restore the caller frame and return address + emitter.instruction("add sp, sp, #176"); // release the explicit-encoding helper frame + emitter.instruction("ret"); // return the original byte length + + emitter.label("__rt_mb_strlen_unknown_encoding_framed"); + emitter.instruction("ldp x29, x30, [sp, #160]"); // restore the caller frame before throwing ValueError + emitter.instruction("add sp, sp, #176"); // release the explicit-encoding helper frame before unwinding + emitter.label("__rt_mb_strlen_unknown_encoding"); + value_error::emit_throw_value_error_aarch64( + emitter, + "_mb_strlen_unknown_encoding_msg", + MB_STRLEN_UNKNOWN_ENCODING_MSG.len(), + ); + + emit_utf8_scanner_aarch64(emitter); +} + +/// Emits PHP-compatible validated UTF-8 counting for the AArch64 runtime. +fn emit_utf8_scanner_aarch64(emitter: &mut Emitter) { + emitter.label("__rt_mb_strlen_utf8"); + emitter.instruction("mov x0, #0"); // UTF-8 character count starts at zero + emitter.instruction("mov x4, #0"); // byte index starts at zero + emitter.label("__rt_mb_strlen_utf8_loop"); + emitter.instruction("cmp x4, x2"); // processed every source byte? + emitter.instruction("b.hs __rt_mb_strlen_utf8_done"); // return once the byte index reaches the string length + emitter.instruction("ldrb w5, [x1, x4]"); // load the next possible UTF-8 leading byte + emitter.instruction("cmp w5, #0x80"); // ASCII bytes are complete one-byte characters + emitter.instruction("b.lo __rt_mb_strlen_utf8_ascii"); // consume one ASCII byte + emitter.instruction("cmp w5, #0xC2"); // C0/C1 and continuation bytes are malformed leaders + emitter.instruction("b.lo __rt_mb_strlen_utf8_invalid"); // substitute one malformed byte + emitter.instruction("cmp w5, #0xE0"); // C2-DF introduce two-byte sequences + emitter.instruction("b.lo __rt_mb_strlen_utf8_two"); // validate a two-byte character + emitter.instruction("cmp w5, #0xF0"); // E0-EF introduce three-byte sequences + emitter.instruction("b.lo __rt_mb_strlen_utf8_three"); // validate a three-byte character + emitter.instruction("cmp w5, #0xF5"); // F0-F4 introduce Unicode-range four-byte sequences + emitter.instruction("b.lo __rt_mb_strlen_utf8_four"); // validate a four-byte character + emitter.instruction("b __rt_mb_strlen_utf8_invalid"); // F5-FF cannot begin valid UTF-8 + + emitter.label("__rt_mb_strlen_utf8_two"); + emitter.instruction("sub x6, x2, x4"); // compute bytes remaining from the two-byte leader + emitter.instruction("cmp x6, #2"); // is the sequence truncated before its continuation byte? + emitter.instruction("b.lo __rt_mb_strlen_utf8_truncated"); // a valid truncated prefix counts as one character + emitter.instruction("add x8, x4, #1"); // address index of the required continuation byte + emitter.instruction("ldrb w7, [x1, x8]"); // load the two-byte sequence continuation + emitter.instruction("and w7, w7, #0xC0"); // isolate the continuation-byte prefix + emitter.instruction("cmp w7, #0x80"); // does the second byte have the required 10xxxxxx shape? + emitter.instruction("b.ne __rt_mb_strlen_utf8_invalid"); // malformed continuation leaves the leader substituted alone + emitter.instruction("add x4, x4, #2"); // consume the complete two-byte character + emitter.instruction("b __rt_mb_strlen_utf8_counted"); // increment the character count once + + emitter.label("__rt_mb_strlen_utf8_three"); + emitter.instruction("sub x6, x2, x4"); // compute bytes remaining from the three-byte leader + emitter.instruction("cmp x6, #3"); // are all two continuation bytes available? + emitter.instruction("b.lo __rt_mb_strlen_utf8_three_partial"); // validate the available prefix before grouping truncation + emitter.instruction("add x8, x4, #1"); // address index of the first continuation byte + emitter.instruction("ldrb w7, [x1, x8]"); // load the first three-byte continuation + emitter.instruction("and w8, w7, #0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp w8, #0x80"); // is the first continuation structurally valid? + emitter.instruction("b.ne __rt_mb_strlen_utf8_invalid"); // malformed continuation substitutes only the leader + emitter.instruction("cmp w5, #0xE0"); // E0 requires a second byte at least A0 to avoid overlong UTF-8 + emitter.instruction("b.ne __rt_mb_strlen_utf8_three_not_e0"); // skip the E0 lower-bound check for other leaders + emitter.instruction("cmp w7, #0xA0"); // is the E0 continuation inside the non-overlong range? + emitter.instruction("b.lo __rt_mb_strlen_utf8_invalid"); // reject an overlong three-byte sequence + emitter.label("__rt_mb_strlen_utf8_three_not_e0"); + emitter.instruction("cmp w5, #0xED"); // ED requires a second byte below A0 to exclude UTF-16 surrogates + emitter.instruction("b.ne __rt_mb_strlen_utf8_three_second"); // skip the surrogate bound for other leaders + emitter.instruction("cmp w7, #0xA0"); // does the ED continuation enter the surrogate range? + emitter.instruction("b.hs __rt_mb_strlen_utf8_invalid"); // reject UTF-8 encodings of surrogate code points + emitter.label("__rt_mb_strlen_utf8_three_second"); + emitter.instruction("add x8, x4, #2"); // address index of the second continuation byte + emitter.instruction("ldrb w7, [x1, x8]"); // load the final three-byte continuation + emitter.instruction("and w7, w7, #0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp w7, #0x80"); // is the final continuation structurally valid? + emitter.instruction("b.ne __rt_mb_strlen_utf8_invalid"); // malformed final byte substitutes only the leader + emitter.instruction("add x4, x4, #3"); // consume the complete three-byte character + emitter.instruction("b __rt_mb_strlen_utf8_counted"); // increment the character count once + + emitter.label("__rt_mb_strlen_utf8_three_partial"); + emitter.instruction("cmp x6, #1"); // is only the valid three-byte leader available? + emitter.instruction("b.eq __rt_mb_strlen_utf8_truncated"); // group a lone valid leader as one truncated character + emitter.instruction("add x8, x4, #1"); // address index of the available continuation byte + emitter.instruction("ldrb w7, [x1, x8]"); // load the partial sequence continuation + emitter.instruction("and w8, w7, #0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp w8, #0x80"); // is the available continuation structurally valid? + emitter.instruction("b.ne __rt_mb_strlen_utf8_invalid"); // malformed partial prefix substitutes only the leader + emitter.instruction("cmp w5, #0xE0"); // apply E0's non-overlong lower bound to partial prefixes + emitter.instruction("b.ne __rt_mb_strlen_utf8_three_partial_not_e0"); // other leaders do not need the E0 bound + emitter.instruction("cmp w7, #0xA0"); // is the E0 continuation non-overlong? + emitter.instruction("b.lo __rt_mb_strlen_utf8_invalid"); // reject an overlong partial prefix + emitter.label("__rt_mb_strlen_utf8_three_partial_not_e0"); + emitter.instruction("cmp w5, #0xED"); // apply ED's surrogate exclusion to partial prefixes + emitter.instruction("b.ne __rt_mb_strlen_utf8_truncated"); // every other valid prefix is one truncated character + emitter.instruction("cmp w7, #0xA0"); // does the ED continuation enter the surrogate range? + emitter.instruction("b.hs __rt_mb_strlen_utf8_invalid"); // reject a surrogate partial prefix + emitter.instruction("b __rt_mb_strlen_utf8_truncated"); // group the valid truncated prefix as one character + + emitter.label("__rt_mb_strlen_utf8_four"); + emitter.instruction("sub x6, x2, x4"); // compute bytes remaining from the four-byte leader + emitter.instruction("cmp x6, #4"); // are all three continuation bytes available? + emitter.instruction("b.lo __rt_mb_strlen_utf8_four_partial"); // validate the available prefix before grouping truncation + emitter.instruction("add x8, x4, #1"); // address index of the first continuation byte + emitter.instruction("ldrb w7, [x1, x8]"); // load the first four-byte continuation + emitter.instruction("and w8, w7, #0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp w8, #0x80"); // is the first continuation structurally valid? + emitter.instruction("b.ne __rt_mb_strlen_utf8_invalid"); // malformed continuation substitutes only the leader + emitter.instruction("cmp w5, #0xF0"); // F0 requires a second byte at least 90 to avoid overlong UTF-8 + emitter.instruction("b.ne __rt_mb_strlen_utf8_four_not_f0"); // skip the F0 lower-bound check for other leaders + emitter.instruction("cmp w7, #0x90"); // is the F0 continuation inside the non-overlong range? + emitter.instruction("b.lo __rt_mb_strlen_utf8_invalid"); // reject an overlong four-byte sequence + emitter.label("__rt_mb_strlen_utf8_four_not_f0"); + emitter.instruction("cmp w5, #0xF4"); // F4 requires a second byte below 90 for Unicode's maximum scalar + emitter.instruction("b.ne __rt_mb_strlen_utf8_four_rest"); // skip the upper bound for F0-F3 + emitter.instruction("cmp w7, #0x90"); // does the F4 continuation exceed U+10FFFF? + emitter.instruction("b.hs __rt_mb_strlen_utf8_invalid"); // reject out-of-range four-byte sequences + emitter.label("__rt_mb_strlen_utf8_four_rest"); + emitter.instruction("add x8, x4, #2"); // address index of the second continuation byte + emitter.instruction("ldrb w7, [x1, x8]"); // load the second four-byte continuation + emitter.instruction("and w7, w7, #0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp w7, #0x80"); // is the second continuation structurally valid? + emitter.instruction("b.ne __rt_mb_strlen_utf8_invalid"); // malformed continuation substitutes only the leader + emitter.instruction("add x8, x4, #3"); // address index of the third continuation byte + emitter.instruction("ldrb w7, [x1, x8]"); // load the final four-byte continuation + emitter.instruction("and w7, w7, #0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp w7, #0x80"); // is the final continuation structurally valid? + emitter.instruction("b.ne __rt_mb_strlen_utf8_invalid"); // malformed continuation substitutes only the leader + emitter.instruction("add x4, x4, #4"); // consume the complete four-byte character + emitter.instruction("b __rt_mb_strlen_utf8_counted"); // increment the character count once + + emitter.label("__rt_mb_strlen_utf8_four_partial"); + emitter.instruction("cmp x6, #1"); // is only the valid four-byte leader available? + emitter.instruction("b.eq __rt_mb_strlen_utf8_truncated"); // group a lone valid leader as one truncated character + emitter.instruction("add x8, x4, #1"); // address index of the available first continuation + emitter.instruction("ldrb w7, [x1, x8]"); // load the first partial continuation + emitter.instruction("and w8, w7, #0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp w8, #0x80"); // is the first partial continuation structurally valid? + emitter.instruction("b.ne __rt_mb_strlen_utf8_invalid"); // malformed partial prefix substitutes only the leader + emitter.instruction("cmp w5, #0xF0"); // apply F0's non-overlong lower bound to partial prefixes + emitter.instruction("b.ne __rt_mb_strlen_utf8_four_partial_not_f0"); // other leaders do not need the F0 bound + emitter.instruction("cmp w7, #0x90"); // is the F0 continuation non-overlong? + emitter.instruction("b.lo __rt_mb_strlen_utf8_invalid"); // reject an overlong partial prefix + emitter.label("__rt_mb_strlen_utf8_four_partial_not_f0"); + emitter.instruction("cmp w5, #0xF4"); // apply F4's Unicode maximum bound to partial prefixes + emitter.instruction("b.ne __rt_mb_strlen_utf8_four_partial_tail"); // F0-F3 continue validating any available tail + emitter.instruction("cmp w7, #0x90"); // does the F4 continuation exceed U+10FFFF? + emitter.instruction("b.hs __rt_mb_strlen_utf8_invalid"); // reject an out-of-range partial prefix + emitter.label("__rt_mb_strlen_utf8_four_partial_tail"); + emitter.instruction("cmp x6, #2"); // are only the leader and first continuation available? + emitter.instruction("b.eq __rt_mb_strlen_utf8_truncated"); // group that valid truncated prefix as one character + emitter.instruction("add x8, x4, #2"); // address index of the available second continuation + emitter.instruction("ldrb w7, [x1, x8]"); // load the second partial continuation + emitter.instruction("and w7, w7, #0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp w7, #0x80"); // is the second partial continuation structurally valid? + emitter.instruction("b.ne __rt_mb_strlen_utf8_invalid"); // malformed partial tail substitutes only the leader + emitter.instruction("b __rt_mb_strlen_utf8_truncated"); // group the valid three-byte prefix as one character + + emitter.label("__rt_mb_strlen_utf8_ascii"); + emitter.instruction("add x4, x4, #1"); // consume one ASCII byte + emitter.instruction("b __rt_mb_strlen_utf8_counted"); // increment the character count once + emitter.label("__rt_mb_strlen_utf8_invalid"); + emitter.instruction("add x4, x4, #1"); // consume one malformed byte for mbstring substitution + emitter.label("__rt_mb_strlen_utf8_counted"); + emitter.instruction("add x0, x0, #1"); // count one valid or substituted character + emitter.instruction("b __rt_mb_strlen_utf8_loop"); // continue scanning the remaining bytes + emitter.label("__rt_mb_strlen_utf8_truncated"); + emitter.instruction("add x0, x0, #1"); // count the final valid truncated prefix as one character + emitter.instruction("ret"); // no bytes remain beyond the truncated prefix + emitter.label("__rt_mb_strlen_utf8_done"); + emitter.instruction("ret"); // return the validated UTF-8 character count +} + +/// Emits the Linux x86_64 implementation. +fn emit_mb_strlen_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: mb_strlen (encoding-aware character count) ---"); + emitter.label_global("__rt_mb_strlen"); + emitter.instruction("test r8, r8"); // omitted/null encoding is represented by a null pointer + emitter.instruction("jz __rt_mb_strlen_utf8_x86"); // use the default UTF-8 scanner when encoding is omitted/null + emitter.instruction(&format!("cmp r9, {}", MAX_ENCODING_NAME_LEN)); // does the encoding name fit the stack C-string buffer? + emitter.instruction("ja __rt_mb_strlen_unknown_encoding_x86"); // reject names longer than every PHP-supported alias + emitter.instruction("push rbp"); // preserve the caller frame pointer across libc calls + emitter.instruction("mov rbp, rsp"); // establish an aligned helper frame + emitter.instruction("sub rsp, 160"); // reserve iconv state, output scratch, and encoding-name storage + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // iconv input pointer variable starts at the PHP string bytes + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // iconv input byte count variable starts at the PHP string length + emitter.instruction("mov QWORD PTR [rbp - 24], 0"); // decoded character count starts at zero + + // -- copy the length-delimited PHP encoding name into a stack C string -- + emitter.instruction("lea rdi, [rbp - 160]"); // destination is the 64-byte encoding-name buffer + emitter.instruction("xor rcx, rcx"); // copied-byte index starts at zero + emitter.label("__rt_mb_strlen_encoding_copy_x86"); + emitter.instruction("cmp rcx, r9"); // copied the whole explicit encoding name? + emitter.instruction("jae __rt_mb_strlen_encoding_copied_x86"); // terminate the C string once every byte is copied + emitter.instruction("mov r10b, BYTE PTR [r8 + rcx]"); // load one encoding-name byte from the PHP string + emitter.instruction("mov BYTE PTR [rdi + rcx], r10b"); // append the byte to the stack C string + emitter.instruction("inc rcx"); // advance the encoding-name byte index + emitter.instruction("jmp __rt_mb_strlen_encoding_copy_x86"); // continue copying the remaining encoding-name bytes + emitter.label("__rt_mb_strlen_encoding_copied_x86"); + emitter.instruction("mov BYTE PTR [rdi + r9], 0"); // NUL-terminate the explicit encoding name + + // -- fast-path PHP's default UTF-8 names and byte-count encodings -- + emitter.instruction("lea rdi, [rbp - 160]"); // first strcasecmp argument is the copied encoding name + abi::emit_symbol_address(emitter, "rsi", "_mb_strlen_utf8_name"); + emitter.instruction("call strcasecmp"); // compare the explicit encoding with UTF-8 case-insensitively + emitter.instruction("test eax, eax"); // did the encoding match UTF-8? + emitter.instruction("jz __rt_mb_strlen_use_utf8_framed_x86"); // UTF-8 uses the allocation-free validated scanner + emitter.instruction("lea rdi, [rbp - 160]"); // reload the copied encoding name after strcasecmp + abi::emit_symbol_address(emitter, "rsi", "_mb_strlen_utf8_alias"); + emitter.instruction("call strcasecmp"); // compare the explicit encoding with PHP's UTF8 alias + emitter.instruction("test eax, eax"); // did the encoding match UTF8? + emitter.instruction("jz __rt_mb_strlen_use_utf8_framed_x86"); // the UTF8 alias uses the same validated scanner + emitter.instruction("lea rdi, [rbp - 160]"); // reload the copied encoding name for the byte-count aliases + abi::emit_symbol_address(emitter, "rsi", "_mb_strlen_8bit_name"); + emitter.instruction("call strcasecmp"); // compare the explicit encoding with 8bit + emitter.instruction("test eax, eax"); // did the encoding match 8bit? + emitter.instruction("jz __rt_mb_strlen_use_byte_length_x86"); // 8bit counts every byte as one character + emitter.instruction("lea rdi, [rbp - 160]"); // reload the copied encoding name for the binary alias + abi::emit_symbol_address(emitter, "rsi", "_mb_strlen_binary_name"); + emitter.instruction("call strcasecmp"); // compare the explicit encoding with binary + emitter.instruction("test eax, eax"); // did the encoding match binary? + emitter.instruction("jz __rt_mb_strlen_use_byte_length_x86"); // binary is PHP's alias for 8bit + emitter.instruction("lea rdi, [rbp - 160]"); // reload the copied encoding name for the 7bit encoding + abi::emit_symbol_address(emitter, "rsi", "_mb_strlen_7bit_name"); + emitter.instruction("call strcasecmp"); // compare the explicit encoding with 7bit + emitter.instruction("test eax, eax"); // did the encoding match 7bit? + emitter.instruction("jz __rt_mb_strlen_use_byte_length_x86"); // 7bit preserves PHP's one-character-per-byte count + + // -- open a decoder from the requested encoding to fixed-width UTF-32LE -- + abi::emit_symbol_address(emitter, "rdi", "_mb_strlen_utf32le_name"); + emitter.instruction("lea rsi, [rbp - 160]"); // iconv source encoding is the copied explicit name + emitter.instruction("call iconv_open"); // create the encoding-to-UTF-32LE conversion descriptor + emitter.instruction("cmp rax, -1"); // did iconv_open return the failure sentinel? + emitter.instruction("je __rt_mb_strlen_unknown_encoding_framed_x86"); // unknown encoding names raise PHP's ValueError + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // preserve the iconv descriptor across conversion iterations + + // -- decode chunks into 16 bytes of UTF-32LE and count four-byte code points -- + emitter.label("__rt_mb_strlen_iconv_loop_x86"); + emitter.instruction("cmp QWORD PTR [rbp - 16], 0"); // are any input bytes still undecoded? + emitter.instruction("je __rt_mb_strlen_iconv_done_x86"); // close the descriptor after all bytes are consumed + emitter.instruction("lea r10, [rbp - 80]"); // point at the fixed 16-byte UTF-32LE output scratch + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // initialize iconv's mutable output pointer + emitter.instruction("mov QWORD PTR [rbp - 48], 16"); // initialize iconv's mutable output-byte count + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // iconv argument 0 is the conversion descriptor + emitter.instruction("lea rsi, [rbp - 8]"); // iconv argument 1 is `&input_ptr` + emitter.instruction("lea rdx, [rbp - 16]"); // iconv argument 2 is `&input_bytes_left` + emitter.instruction("lea rcx, [rbp - 40]"); // iconv argument 3 is `&output_ptr` + emitter.instruction("lea r8, [rbp - 48]"); // iconv argument 4 is `&output_bytes_left` + emitter.instruction("call iconv"); // decode as many complete characters as fit in the output scratch + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // preserve iconv's status while accounting for produced code points + emitter.instruction("mov r10, 16"); // reload the fixed output scratch capacity + emitter.instruction("sub r10, QWORD PTR [rbp - 48]"); // compute the number of UTF-32LE bytes produced + emitter.instruction("shr r10, 2"); // four output bytes represent one decoded character + emitter.instruction("add QWORD PTR [rbp - 24], r10"); // add every character decoded in this iteration + emitter.instruction("cmp QWORD PTR [rbp - 56], -1"); // did iconv report an error condition? + emitter.instruction("jne __rt_mb_strlen_iconv_loop_x86"); // successful partial progress continues until input is exhausted + emitter.instruction("cmp QWORD PTR [rbp - 48], 0"); // did iconv merely fill the output scratch? + emitter.instruction("je __rt_mb_strlen_iconv_loop_x86"); // E2BIG only requires another conversion iteration + emitter.instruction("call __errno_location"); // fetch the Linux thread-local errno written by iconv + emitter.instruction("cmp DWORD PTR [rax], 22"); // EINVAL means a valid but truncated final sequence + emitter.instruction("je __rt_mb_strlen_iconv_incomplete_x86"); // mbstring groups that truncated prefix as one character + emitter.instruction("cmp QWORD PTR [rbp - 16], 0"); // are bytes still present at the malformed sequence? + emitter.instruction("je __rt_mb_strlen_iconv_done_x86"); // defensive completion if iconv consumed the final byte + emitter.instruction("add QWORD PTR [rbp - 8], 1"); // skip one malformed input byte like mbstring substitution + emitter.instruction("sub QWORD PTR [rbp - 16], 1"); // remove the malformed byte from the remaining input count + emitter.instruction("add QWORD PTR [rbp - 24], 1"); // one malformed byte becomes one substitution character + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the iconv descriptor for a state reset + emitter.instruction("xor rsi, rsi"); // null input pointer requests iconv shift-state reset + emitter.instruction("xor rdx, rdx"); // no input byte count participates in the reset + emitter.instruction("xor rcx, rcx"); // no output pointer participates in the reset + emitter.instruction("xor r8, r8"); // no output byte count participates in the reset + emitter.instruction("call iconv"); // reset stateful decoders after substituting one malformed byte + emitter.instruction("jmp __rt_mb_strlen_iconv_loop_x86"); // continue decoding after the malformed byte + + emitter.label("__rt_mb_strlen_iconv_incomplete_x86"); + emitter.instruction("add QWORD PTR [rbp - 24], 1"); // count the whole truncated valid prefix as one character + emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // mark the truncated suffix as fully handled + emitter.label("__rt_mb_strlen_iconv_done_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // iconv_close argument is the active conversion descriptor + emitter.instruction("call iconv_close"); // release the conversion descriptor before returning + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the accumulated character count + emitter.instruction("leave"); // release the iconv helper frame and restore rbp + emitter.instruction("ret"); // return the encoding-aware character count + + emitter.label("__rt_mb_strlen_use_utf8_framed_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // restore the PHP string pointer for the UTF-8 scanner + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // restore the PHP string length for the UTF-8 scanner + emitter.instruction("leave"); // release the explicit-encoding helper frame + emitter.instruction("jmp __rt_mb_strlen_utf8_x86"); // tail-dispatch to the validated UTF-8 scanner + + emitter.label("__rt_mb_strlen_use_byte_length_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // byte encodings count every source byte as one character + emitter.instruction("leave"); // release the explicit-encoding helper frame + emitter.instruction("ret"); // return the original byte length + + emitter.label("__rt_mb_strlen_unknown_encoding_framed_x86"); + emitter.instruction("leave"); // release the explicit-encoding helper frame before unwinding + emitter.label("__rt_mb_strlen_unknown_encoding_x86"); + value_error::emit_throw_value_error_x86_64( + emitter, + "_mb_strlen_unknown_encoding_msg", + MB_STRLEN_UNKNOWN_ENCODING_MSG.len(), + ); + + emit_utf8_scanner_x86_64(emitter); +} + +/// Emits PHP-compatible validated UTF-8 counting for the Linux x86_64 runtime. +fn emit_utf8_scanner_x86_64(emitter: &mut Emitter) { + emitter.label("__rt_mb_strlen_utf8_x86"); + emitter.instruction("mov rsi, rax"); // preserve the source pointer while rax becomes the count + emitter.instruction("xor eax, eax"); // UTF-8 character count starts at zero + emitter.instruction("xor r8, r8"); // byte index starts at zero + emitter.label("__rt_mb_strlen_utf8_loop_x86"); + emitter.instruction("cmp r8, rdx"); // processed every source byte? + emitter.instruction("jae __rt_mb_strlen_utf8_done_x86"); // return once the byte index reaches the string length + emitter.instruction("movzx r9d, BYTE PTR [rsi + r8]"); // load the next possible UTF-8 leading byte + emitter.instruction("cmp r9d, 0x80"); // ASCII bytes are complete one-byte characters + emitter.instruction("jb __rt_mb_strlen_utf8_ascii_x86"); // consume one ASCII byte + emitter.instruction("cmp r9d, 0xC2"); // C0/C1 and continuation bytes are malformed leaders + emitter.instruction("jb __rt_mb_strlen_utf8_invalid_x86"); // substitute one malformed byte + emitter.instruction("cmp r9d, 0xE0"); // C2-DF introduce two-byte sequences + emitter.instruction("jb __rt_mb_strlen_utf8_two_x86"); // validate a two-byte character + emitter.instruction("cmp r9d, 0xF0"); // E0-EF introduce three-byte sequences + emitter.instruction("jb __rt_mb_strlen_utf8_three_x86"); // validate a three-byte character + emitter.instruction("cmp r9d, 0xF5"); // F0-F4 introduce Unicode-range four-byte sequences + emitter.instruction("jb __rt_mb_strlen_utf8_four_x86"); // validate a four-byte character + emitter.instruction("jmp __rt_mb_strlen_utf8_invalid_x86"); // F5-FF cannot begin valid UTF-8 + + emitter.label("__rt_mb_strlen_utf8_two_x86"); + emitter.instruction("mov r10, rdx"); // copy the total byte length to compute remaining bytes + emitter.instruction("sub r10, r8"); // compute bytes remaining from the two-byte leader + emitter.instruction("cmp r10, 2"); // is the sequence truncated before its continuation byte? + emitter.instruction("jb __rt_mb_strlen_utf8_truncated_x86"); // a valid truncated prefix counts as one character + emitter.instruction("movzx r11d, BYTE PTR [rsi + r8 + 1]"); // load the two-byte sequence continuation + emitter.instruction("and r11d, 0xC0"); // isolate the continuation-byte prefix + emitter.instruction("cmp r11d, 0x80"); // does the second byte have the required 10xxxxxx shape? + emitter.instruction("jne __rt_mb_strlen_utf8_invalid_x86"); // malformed continuation leaves the leader substituted alone + emitter.instruction("add r8, 2"); // consume the complete two-byte character + emitter.instruction("jmp __rt_mb_strlen_utf8_counted_x86"); // increment the character count once + + emitter.label("__rt_mb_strlen_utf8_three_x86"); + emitter.instruction("mov r10, rdx"); // copy the total byte length to compute remaining bytes + emitter.instruction("sub r10, r8"); // compute bytes remaining from the three-byte leader + emitter.instruction("cmp r10, 3"); // are all two continuation bytes available? + emitter.instruction("jb __rt_mb_strlen_utf8_three_partial_x86"); // validate the available prefix before grouping truncation + emitter.instruction("movzx r11d, BYTE PTR [rsi + r8 + 1]"); // load the first three-byte continuation + emitter.instruction("mov ecx, r11d"); // preserve the continuation value while checking its prefix + emitter.instruction("and ecx, 0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp ecx, 0x80"); // is the first continuation structurally valid? + emitter.instruction("jne __rt_mb_strlen_utf8_invalid_x86"); // malformed continuation substitutes only the leader + emitter.instruction("cmp r9d, 0xE0"); // E0 requires a second byte at least A0 to avoid overlong UTF-8 + emitter.instruction("jne __rt_mb_strlen_utf8_three_not_e0_x86"); // skip the E0 lower-bound check for other leaders + emitter.instruction("cmp r11d, 0xA0"); // is the E0 continuation inside the non-overlong range? + emitter.instruction("jb __rt_mb_strlen_utf8_invalid_x86"); // reject an overlong three-byte sequence + emitter.label("__rt_mb_strlen_utf8_three_not_e0_x86"); + emitter.instruction("cmp r9d, 0xED"); // ED requires a second byte below A0 to exclude UTF-16 surrogates + emitter.instruction("jne __rt_mb_strlen_utf8_three_second_x86"); // skip the surrogate bound for other leaders + emitter.instruction("cmp r11d, 0xA0"); // does the ED continuation enter the surrogate range? + emitter.instruction("jae __rt_mb_strlen_utf8_invalid_x86"); // reject UTF-8 encodings of surrogate code points + emitter.label("__rt_mb_strlen_utf8_three_second_x86"); + emitter.instruction("movzx r11d, BYTE PTR [rsi + r8 + 2]"); // load the final three-byte continuation + emitter.instruction("and r11d, 0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp r11d, 0x80"); // is the final continuation structurally valid? + emitter.instruction("jne __rt_mb_strlen_utf8_invalid_x86"); // malformed final byte substitutes only the leader + emitter.instruction("add r8, 3"); // consume the complete three-byte character + emitter.instruction("jmp __rt_mb_strlen_utf8_counted_x86"); // increment the character count once + + emitter.label("__rt_mb_strlen_utf8_three_partial_x86"); + emitter.instruction("cmp r10, 1"); // is only the valid three-byte leader available? + emitter.instruction("je __rt_mb_strlen_utf8_truncated_x86"); // group a lone valid leader as one truncated character + emitter.instruction("movzx r11d, BYTE PTR [rsi + r8 + 1]"); // load the partial sequence continuation + emitter.instruction("mov ecx, r11d"); // preserve the continuation value while checking its prefix + emitter.instruction("and ecx, 0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp ecx, 0x80"); // is the available continuation structurally valid? + emitter.instruction("jne __rt_mb_strlen_utf8_invalid_x86"); // malformed partial prefix substitutes only the leader + emitter.instruction("cmp r9d, 0xE0"); // apply E0's non-overlong lower bound to partial prefixes + emitter.instruction("jne __rt_mb_strlen_utf8_three_partial_not_e0_x86"); // other leaders do not need the E0 bound + emitter.instruction("cmp r11d, 0xA0"); // is the E0 continuation non-overlong? + emitter.instruction("jb __rt_mb_strlen_utf8_invalid_x86"); // reject an overlong partial prefix + emitter.label("__rt_mb_strlen_utf8_three_partial_not_e0_x86"); + emitter.instruction("cmp r9d, 0xED"); // apply ED's surrogate exclusion to partial prefixes + emitter.instruction("jne __rt_mb_strlen_utf8_truncated_x86"); // every other valid prefix is one truncated character + emitter.instruction("cmp r11d, 0xA0"); // does the ED continuation enter the surrogate range? + emitter.instruction("jae __rt_mb_strlen_utf8_invalid_x86"); // reject a surrogate partial prefix + emitter.instruction("jmp __rt_mb_strlen_utf8_truncated_x86"); // group the valid truncated prefix as one character + + emitter.label("__rt_mb_strlen_utf8_four_x86"); + emitter.instruction("mov r10, rdx"); // copy the total byte length to compute remaining bytes + emitter.instruction("sub r10, r8"); // compute bytes remaining from the four-byte leader + emitter.instruction("cmp r10, 4"); // are all three continuation bytes available? + emitter.instruction("jb __rt_mb_strlen_utf8_four_partial_x86"); // validate the available prefix before grouping truncation + emitter.instruction("movzx r11d, BYTE PTR [rsi + r8 + 1]"); // load the first four-byte continuation + emitter.instruction("mov ecx, r11d"); // preserve the continuation value while checking its prefix + emitter.instruction("and ecx, 0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp ecx, 0x80"); // is the first continuation structurally valid? + emitter.instruction("jne __rt_mb_strlen_utf8_invalid_x86"); // malformed continuation substitutes only the leader + emitter.instruction("cmp r9d, 0xF0"); // F0 requires a second byte at least 90 to avoid overlong UTF-8 + emitter.instruction("jne __rt_mb_strlen_utf8_four_not_f0_x86"); // skip the F0 lower-bound check for other leaders + emitter.instruction("cmp r11d, 0x90"); // is the F0 continuation inside the non-overlong range? + emitter.instruction("jb __rt_mb_strlen_utf8_invalid_x86"); // reject an overlong four-byte sequence + emitter.label("__rt_mb_strlen_utf8_four_not_f0_x86"); + emitter.instruction("cmp r9d, 0xF4"); // F4 requires a second byte below 90 for Unicode's maximum scalar + emitter.instruction("jne __rt_mb_strlen_utf8_four_rest_x86"); // skip the upper bound for F0-F3 + emitter.instruction("cmp r11d, 0x90"); // does the F4 continuation exceed U+10FFFF? + emitter.instruction("jae __rt_mb_strlen_utf8_invalid_x86"); // reject out-of-range four-byte sequences + emitter.label("__rt_mb_strlen_utf8_four_rest_x86"); + emitter.instruction("movzx r11d, BYTE PTR [rsi + r8 + 2]"); // load the second four-byte continuation + emitter.instruction("and r11d, 0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp r11d, 0x80"); // is the second continuation structurally valid? + emitter.instruction("jne __rt_mb_strlen_utf8_invalid_x86"); // malformed continuation substitutes only the leader + emitter.instruction("movzx r11d, BYTE PTR [rsi + r8 + 3]"); // load the final four-byte continuation + emitter.instruction("and r11d, 0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp r11d, 0x80"); // is the final continuation structurally valid? + emitter.instruction("jne __rt_mb_strlen_utf8_invalid_x86"); // malformed continuation substitutes only the leader + emitter.instruction("add r8, 4"); // consume the complete four-byte character + emitter.instruction("jmp __rt_mb_strlen_utf8_counted_x86"); // increment the character count once + + emitter.label("__rt_mb_strlen_utf8_four_partial_x86"); + emitter.instruction("cmp r10, 1"); // is only the valid four-byte leader available? + emitter.instruction("je __rt_mb_strlen_utf8_truncated_x86"); // group a lone valid leader as one truncated character + emitter.instruction("movzx r11d, BYTE PTR [rsi + r8 + 1]"); // load the first partial continuation + emitter.instruction("mov ecx, r11d"); // preserve the continuation value while checking its prefix + emitter.instruction("and ecx, 0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp ecx, 0x80"); // is the first partial continuation structurally valid? + emitter.instruction("jne __rt_mb_strlen_utf8_invalid_x86"); // malformed partial prefix substitutes only the leader + emitter.instruction("cmp r9d, 0xF0"); // apply F0's non-overlong lower bound to partial prefixes + emitter.instruction("jne __rt_mb_strlen_utf8_four_partial_not_f0_x86"); // other leaders do not need the F0 bound + emitter.instruction("cmp r11d, 0x90"); // is the F0 continuation non-overlong? + emitter.instruction("jb __rt_mb_strlen_utf8_invalid_x86"); // reject an overlong partial prefix + emitter.label("__rt_mb_strlen_utf8_four_partial_not_f0_x86"); + emitter.instruction("cmp r9d, 0xF4"); // apply F4's Unicode maximum bound to partial prefixes + emitter.instruction("jne __rt_mb_strlen_utf8_four_partial_tail_x86"); // F0-F3 continue validating any available tail + emitter.instruction("cmp r11d, 0x90"); // does the F4 continuation exceed U+10FFFF? + emitter.instruction("jae __rt_mb_strlen_utf8_invalid_x86"); // reject an out-of-range partial prefix + emitter.label("__rt_mb_strlen_utf8_four_partial_tail_x86"); + emitter.instruction("cmp r10, 2"); // are only the leader and first continuation available? + emitter.instruction("je __rt_mb_strlen_utf8_truncated_x86"); // group that valid truncated prefix as one character + emitter.instruction("movzx r11d, BYTE PTR [rsi + r8 + 2]"); // load the second partial continuation + emitter.instruction("and r11d, 0xC0"); // isolate its continuation-byte prefix + emitter.instruction("cmp r11d, 0x80"); // is the second partial continuation structurally valid? + emitter.instruction("jne __rt_mb_strlen_utf8_invalid_x86"); // malformed partial tail substitutes only the leader + emitter.instruction("jmp __rt_mb_strlen_utf8_truncated_x86"); // group the valid three-byte prefix as one character + + emitter.label("__rt_mb_strlen_utf8_ascii_x86"); + emitter.instruction("inc r8"); // consume one ASCII byte + emitter.instruction("jmp __rt_mb_strlen_utf8_counted_x86"); // increment the character count once + emitter.label("__rt_mb_strlen_utf8_invalid_x86"); + emitter.instruction("inc r8"); // consume one malformed byte for mbstring substitution + emitter.label("__rt_mb_strlen_utf8_counted_x86"); + emitter.instruction("inc rax"); // count one valid or substituted character + emitter.instruction("jmp __rt_mb_strlen_utf8_loop_x86"); // continue scanning the remaining bytes + emitter.label("__rt_mb_strlen_utf8_truncated_x86"); + emitter.instruction("inc rax"); // count the final valid truncated prefix as one character + emitter.instruction("ret"); // no bytes remain beyond the truncated prefix + emitter.label("__rt_mb_strlen_utf8_done_x86"); + emitter.instruction("ret"); // return the validated UTF-8 character count +} diff --git a/src/codegen_support/runtime/strings/mod.rs b/src/codegen_support/runtime/strings/mod.rs index 7a5f8c9fc9..303f63207d 100644 --- a/src/codegen_support/runtime/strings/mod.rs +++ b/src/codegen_support/runtime/strings/mod.rs @@ -66,6 +66,7 @@ mod vsprintf; mod md5; mod sha1; mod crc32; +mod mb_strlen; mod hash; pub(crate) mod hash_algos; mod hash_context; @@ -193,6 +194,8 @@ pub use md5::emit_md5; pub use sha1::emit_sha1; /// Emit CRC-32 checksum helper. pub use crc32::emit_crc32; +/// Emit mb_strlen UTF-8 code-point-count helper. +pub use mb_strlen::emit_mb_strlen; /// Emit SHA1 hash helper. pub use hash::emit_hash; /// Emit generic hash helper. diff --git a/src/codegen_support/runtime_features.rs b/src/codegen_support/runtime_features.rs index eb54a3cd0d..9a4c784bb4 100644 --- a/src/codegen_support/runtime_features.rs +++ b/src/codegen_support/runtime_features.rs @@ -9,6 +9,8 @@ //! Key details: //! - Direct `preg_*` calls and emitted regex iterator classes both enable regex //! helpers because generated SPL methods can call them. +//! - Lowered `mb_strlen()` calls enable its iconv-backed runtime helper without +//! imposing that native dependency on programs that never use the builtin. //! - Emitted stream/archive classes enable PHAR bridge libraries because their //! generated methods route dynamic paths through `__rt_*_maybe_phar` helpers. //! - The dynamic builtin dispatcher (descriptor invoker) emits per-builtin @@ -31,6 +33,8 @@ use super::program_usage::{collect_required_class_names, program_has_dynamic_ins #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct RuntimeFeatures { pub regex: bool, + /// True when lowered code can call the optional iconv-backed `mb_strlen()` helper. + pub mb_strlen: bool, pub phar_archive: bool, /// True when codegen can emit the runtime callable dispatcher (descriptor /// invoker) that builds per-builtin wrappers referencing `elephc_crypto`. @@ -53,6 +57,7 @@ impl RuntimeFeatures { pub const fn none() -> Self { Self { regex: false, + mb_strlen: false, phar_archive: false, descriptor_invoker: false, eval_bridge: false, @@ -66,6 +71,7 @@ impl RuntimeFeatures { pub const fn all() -> Self { Self { regex: true, + mb_strlen: true, phar_archive: true, descriptor_invoker: true, eval_bridge: true, @@ -1123,6 +1129,7 @@ mod tests { fn test_descriptor_invoker_runtime_features_require_elephc_crypto_library() { assert!(required_libraries_for_runtime_features(RuntimeFeatures { regex: false, + mb_strlen: false, phar_archive: false, descriptor_invoker: true, eval_bridge: false, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 198464c160..835fbdd4a2 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -359,6 +359,7 @@ fn expr_exposes_dynamic_param(expr: &Expr, dynamic_params: &HashSet) -> pub(super) fn include_lowered_runtime_features(module: &mut Module) { let features = lowered_runtime_features(module); module.required_runtime_features.regex |= features.regex; + module.required_runtime_features.mb_strlen |= features.mb_strlen; module.required_runtime_features.phar_archive |= features.phar_archive; module.required_runtime_features.descriptor_invoker |= features.descriptor_invoker; module.required_runtime_features.eval_bridge |= features.eval_bridge; @@ -381,6 +382,9 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { if builtin_call_requires_regex(module, inst) { features.regex = true; } + if builtin_call_requires_mb_strlen(module, inst) { + features.mb_strlen = true; + } if builtin_call_requires_phar_archive(module, function, inst) { features.phar_archive = true; } @@ -974,6 +978,13 @@ fn builtin_call_requires_regex(module: &Module, inst: &crate::ir::Instruction) - is_regex_builtin_name(name) } +/// Returns true when a lowered builtin call references the optional `mb_strlen()` runtime helper. +fn builtin_call_requires_mb_strlen(module: &Module, inst: &crate::ir::Instruction) -> bool { + builtin_call_name(module, inst).is_some_and(|name| { + php_symbol_key(name.trim_start_matches('\\')) == "mb_strlen" + }) +} + /// Returns true when a lowered builtin call emits PHAR bridge pointer publishing. fn builtin_call_requires_phar_archive( module: &Module, diff --git a/src/types/checker/builtins/mod.rs b/src/types/checker/builtins/mod.rs index 6249bc84ca..c4976ce7e6 100644 --- a/src/types/checker/builtins/mod.rs +++ b/src/types/checker/builtins/mod.rs @@ -90,6 +90,9 @@ impl Checker { if args.len() != 1 { return Err(CompileError::new(span, "eval() takes exactly 1 argument")); } + // The magician archive contains the encoding-aware `mb_strlen()` implementation; + // macOS exposes iconv through a separate system library while Linux keeps it in libc. + self.require_macos_builtin_library("iconv"); self.infer_type(&args[0], env)?; return Ok(Some(PhpType::Mixed)); } diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index e859e42c26..87445224c5 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -312,6 +312,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "ltrim", "max", "method_exists", + "mb_strlen", "microtime", "md5", "min", diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs index b645b74fc4..5ec9c24ed5 100644 --- a/tests/codegen/eval_builtin_parity.rs +++ b/tests/codegen/eval_builtin_parity.rs @@ -150,6 +150,22 @@ echo STRLEN("fghi");'); assert_eq!(out, "324"); } +/// Verifies dynamic eval links and dispatches Magician's encoding-aware `mb_strlen()` path. +#[test] +fn test_eval_mb_strlen_encoding_parity() { + let out = compile_and_run( + r#" 0 ? "binary" : "UTF-8"; +echo mb_strlen("héllo", $encoding), ":"; +echo mb_strlen("\x68\x00\xE9\x00", "UTF-16LE"), ":"; +$length = mb_strlen(...); +echo $length("héllo", "8bit");"#, + ); + assert_eq!(out, "5:6:3:6:2:6"); +} + +/// Verifies malformed and truncated UTF-8 follows PHP mbstring substitution boundaries. +#[test] +fn test_mb_strlen_malformed_utf8() { + let out = compile_and_run( + r#" 0 ? "definitely-not-an-encoding" : "UTF-8"; +try { + mb_strlen("abc", $encoding); +} catch (\ValueError $error) { + echo "caught"; +}"#, + ); + assert_eq!(out, "3:caught"); +} diff --git a/tests/error_tests/string_builtins.rs b/tests/error_tests/string_builtins.rs index 4c8537607d..2899866da0 100644 --- a/tests/error_tests/string_builtins.rs +++ b/tests/error_tests/string_builtins.rs @@ -33,6 +33,30 @@ expect_builtin_arity_error!( "mb_ereg_match() takes 2 or 3 arguments" ); +expect_builtin_arity_error!( + test_error_mb_strlen_wrong_args, + "