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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,7 @@ and 0.x validation rather than by speculative pass work.
- [x] PHP 8.3 typed class constants — declared types enforced for initializers and inherited overrides (including covariant narrowing) on classes, interfaces, traits, and enums, exposed via `ReflectionClassConstant::hasType()`/`getType()`
- [x] `$object::class` on object expressions — returns the receiver's concrete runtime class name, evaluates the receiver exactly once, and rejects statically known non-object receivers
- [x] `mb_strlen()` — nullable optional `$encoding` argument, UTF-8 malformed-sequence handling, byte-count aliases, iconv-backed multibyte encodings, callable dispatch, catchable `ValueError` for unknown encodings
- [x] `mb_strwidth()` — PHP 8.5 East Asian Width for UTF-8 and encoding-aware iconv paths, matching `mb_strlen()` encoding aliases, callable dispatch, and catchable `ValueError` for unknown encodings
- [x] `static $x;` function-static declarations without an initializer — desugar to `= null` in both the native and Magician `eval()` parsers, matching PHP
- [x] Purity / may-throw v2 — closed-world instance dispatch now unions concrete override summaries (with exact fixed-construction/final/private targets), named and dynamic property reads distinguish declared untyped slots, typed-slot throws, missing-property warnings, hooks, and `__get`, known array offsets separate silent reads from undefined-key warnings, and registry/runtime builtin effects use shared argument-sensitive contracts instead of the previous blanket barrier. A final whole-module fixed point writes these summaries onto refinable EIR call/property instructions before validation, while unresolved/eval/external targets retain conservative defaults
- [x] Guard reasoning v2 for dead-code elimination — integer interval facts from `$x <op> int` relational branches when `$x` has a proven integer domain from an exact `int` parameter, typed local, or literal guard (intersected across nested paths and discharged for transitive relational / strict-int contradictions and impossible `switch` int cases); cross-variable relational / strict-equality atoms with safe complements, full exact coupling after strict substitution, and pure non-throwing `while` / `for` body-entry strengthening, still under the path-local AST `GuardState` protocol with write invalidation, float/string-domain refusal, NaN-safe false-branch policy, and no general CFG join
Expand Down
34 changes: 34 additions & 0 deletions crates/elephc-builtin-contract/src/catalog_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11706,6 +11706,40 @@ pub(crate) static CONTRACTS: &[BuiltinContract] = &[
internal: false,
requirements: &[],
},
BuiltinContract {
id: BuiltinId::from_canonical_name("mb_strwidth"),
name: "mb_strwidth",
area: Area::String,
kind: BuiltinKind::Function,
params: &[
ParamSpec {
name: "string",
ty: TypeSpec::Str,
default: None,
by_ref: false,
},
ParamSpec {
name: "encoding",
ty: TypeSpec::Str,
default: Some(DefaultSpec::Null),
by_ref: false,
},
],
variadic: None,
min_args: None,
max_args: None,
arity_error: None,
returns: TypeSpec::Int,
by_ref_return: false,
summary: "Returns the terminal display width of a string in the requested encoding.",
examples: &[
],
php_manual: Some("https://www.php.net/manual/en/function.mb-strwidth.php"),
deprecation: None,
extension: false,
internal: false,
requirements: &[],
},
BuiltinContract {
id: BuiltinId::from_canonical_name("md5"),
name: "md5",
Expand Down
2 changes: 2 additions & 0 deletions crates/elephc-builtin-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod catalog_curl;
mod catalog_surfaces;
mod eval_profile;
mod id;
mod mb_eaw;
mod registry;
mod requirements;
mod runtime_id;
Expand All @@ -33,6 +34,7 @@ pub use eval_profile::{
eval_signature, eval_signature_profile, EvalSignatureOverrideReason, EvalSignatureProfile,
};
pub use id::BuiltinId;
pub use mb_eaw::{character_width, EAW_RANGES, FIRST_DOUBLEWIDTH_CODEPOINT};
pub use registry::{contracts, lookup, lookup_id};
pub use runtime_id::{runtime_builtin_id, RuntimeBuiltinId, RuntimeBuiltinStatus};
pub use spec::{
Expand Down
189 changes: 189 additions & 0 deletions crates/elephc-builtin-contract/src/mb_eaw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
//! Purpose:
//! PHP 8.5 `mb_strwidth()` East Asian Width ranges and the shared width lookup.
//!
//! Called from:
//! - Magician's `mb_strwidth` eval builtin when summing decoded code points.
//! - The AOT runtime emitter when materializing `_mb_eaw_table` and the helper
//! that binary-searches those same ranges.
//!
//! Key details:
//! - Ranges are the PHP 8.5 `mbfl_eaw_table` (Unicode 16.0 W/F properties).
//! - Code points below `FIRST_DOUBLEWIDTH_CODEPOINT` are always width 1.
//! - Invalid/truncated input is not represented here; callers add width 1.

/// First code point that can be double-width in PHP 8.5's East Asian Width table.
pub const FIRST_DOUBLEWIDTH_CODEPOINT: u32 = 0x1100;

/// Inclusive Unicode ranges that PHP 8.5 `mb_strwidth()` treats as width 2.
///
/// Sourced from `ext/mbstring/libmbfl/mbfl/eaw_table.h` on the PHP-8.5 branch.
pub const EAW_RANGES: &[(u32, u32)] = &[
(0x1100, 0x115f),
(0x231a, 0x231b),
(0x2329, 0x232a),
(0x23e9, 0x23ec),
(0x23f0, 0x23f0),
(0x23f3, 0x23f3),
(0x25fd, 0x25fe),
(0x2614, 0x2615),
(0x2630, 0x2637),
(0x2648, 0x2653),
(0x267f, 0x267f),
(0x268a, 0x268f),
(0x2693, 0x2693),
(0x26a1, 0x26a1),
(0x26aa, 0x26ab),
(0x26bd, 0x26be),
(0x26c4, 0x26c5),
(0x26ce, 0x26ce),
(0x26d4, 0x26d4),
(0x26ea, 0x26ea),
(0x26f2, 0x26f3),
(0x26f5, 0x26f5),
(0x26fa, 0x26fa),
(0x26fd, 0x26fd),
(0x2705, 0x2705),
(0x270a, 0x270b),
(0x2728, 0x2728),
(0x274c, 0x274c),
(0x274e, 0x274e),
(0x2753, 0x2755),
(0x2757, 0x2757),
(0x2795, 0x2797),
(0x27b0, 0x27b0),
(0x27bf, 0x27bf),
(0x2b1b, 0x2b1c),
(0x2b50, 0x2b50),
(0x2b55, 0x2b55),
(0x2e80, 0x2e99),
(0x2e9b, 0x2ef3),
(0x2f00, 0x2fd5),
(0x2ff0, 0x303e),
(0x3041, 0x3096),
(0x3099, 0x30ff),
(0x3105, 0x312f),
(0x3131, 0x318e),
(0x3190, 0x31e5),
(0x31ef, 0x321e),
(0x3220, 0x3247),
(0x3250, 0xa48c),
(0xa490, 0xa4c6),
(0xa960, 0xa97c),
(0xac00, 0xd7a3),
(0xf900, 0xfaff),
(0xfe10, 0xfe19),
(0xfe30, 0xfe52),
(0xfe54, 0xfe66),
(0xfe68, 0xfe6b),
(0xff01, 0xff60),
(0xffe0, 0xffe6),
(0x16fe0, 0x16fe4),
(0x16ff0, 0x16ff6),
(0x17000, 0x18cd5),
(0x18cff, 0x18d1e),
(0x18d80, 0x18df2),
(0x1aff0, 0x1aff3),
(0x1aff5, 0x1affb),
(0x1affd, 0x1affe),
(0x1b000, 0x1b122),
(0x1b132, 0x1b132),
(0x1b150, 0x1b152),
(0x1b155, 0x1b155),
(0x1b164, 0x1b167),
(0x1b170, 0x1b2fb),
(0x1d300, 0x1d356),
(0x1d360, 0x1d376),
(0x1f004, 0x1f004),
(0x1f0cf, 0x1f0cf),
(0x1f18e, 0x1f18e),
(0x1f191, 0x1f19a),
(0x1f200, 0x1f202),
(0x1f210, 0x1f23b),
(0x1f240, 0x1f248),
(0x1f250, 0x1f251),
(0x1f260, 0x1f265),
(0x1f300, 0x1f320),
(0x1f32d, 0x1f335),
(0x1f337, 0x1f37c),
(0x1f37e, 0x1f393),
(0x1f3a0, 0x1f3ca),
(0x1f3cf, 0x1f3d3),
(0x1f3e0, 0x1f3f0),
(0x1f3f4, 0x1f3f4),
(0x1f3f8, 0x1f43e),
(0x1f440, 0x1f440),
(0x1f442, 0x1f4fc),
(0x1f4ff, 0x1f53d),
(0x1f54b, 0x1f54e),
(0x1f550, 0x1f567),
(0x1f57a, 0x1f57a),
(0x1f595, 0x1f596),
(0x1f5a4, 0x1f5a4),
(0x1f5fb, 0x1f64f),
(0x1f680, 0x1f6c5),
(0x1f6cc, 0x1f6cc),
(0x1f6d0, 0x1f6d2),
(0x1f6d5, 0x1f6d8),
(0x1f6dc, 0x1f6df),
(0x1f6eb, 0x1f6ec),
(0x1f6f4, 0x1f6fc),
(0x1f7e0, 0x1f7eb),
(0x1f7f0, 0x1f7f0),
(0x1f90c, 0x1f93a),
(0x1f93c, 0x1f945),
(0x1f947, 0x1f9ff),
(0x1fa70, 0x1fa7c),
(0x1fa80, 0x1fa8a),
(0x1fa8e, 0x1fac6),
(0x1fac8, 0x1fac8),
(0x1facd, 0x1fadc),
(0x1fadf, 0x1faea),
(0x1faef, 0x1faf8),
(0x20000, 0x2fffd),
(0x30000, 0x3fffd),
];

/// Returns PHP 8.5 `mb_strwidth()` display width for one Unicode scalar.
pub const fn character_width(codepoint: u32) -> u32 {
if codepoint < FIRST_DOUBLEWIDTH_CODEPOINT {
return 1;
}

let mut lo = 0usize;
let mut hi = EAW_RANGES.len();
while lo < hi {
let probe = (lo + hi) / 2;
let (begin, end) = EAW_RANGES[probe];
if codepoint < begin {
hi = probe;
} else if codepoint > end {
lo = probe + 1;
} else {
return 2;
}
}
1
}

#[cfg(test)]
mod tests {
use super::{character_width, FIRST_DOUBLEWIDTH_CODEPOINT};

/// Verifies ASCII, Latin-1, CJK, fullwidth, and Unicode 15/16 emoji widths.
#[test]
fn php85_strwidth_samples_match_east_asian_width() {
assert_eq!(character_width(b'a' as u32), 1);
assert_eq!(character_width(0x00E9), 1);
assert_eq!(character_width(FIRST_DOUBLEWIDTH_CODEPOINT - 1), 1);
assert_eq!(character_width(0x1100), 2);
assert_eq!(character_width(0x115F), 2);
assert_eq!(character_width(0x1160), 1);
assert_eq!(character_width(0x65E5), 2);
assert_eq!(character_width(0xFF41), 2);
assert_eq!(character_width(0x2630), 2);
assert_eq!(character_width(0x1F418), 2);
assert_eq!(character_width(0x1F6DC), 2);
assert_eq!(character_width(0x1F9FF), 2);
assert_eq!(character_width(0x1FA00), 1);
}
}
2 changes: 1 addition & 1 deletion crates/elephc-builtin-contract/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ mod tests {
// The PHP-visible `curl_*` surface is published only with the `curl`
// feature; see `crate::catalog_curl`'s module doc.
let curl_surface = if cfg!(feature = "curl") { 34 } else { 0 };
assert_eq!(contracts().len(), 597 + curl_surface);
assert_eq!(contracts().len(), 598 + curl_surface);
assert_eq!(lookup("STRLEN").map(|contract| contract.name), Some("strlen"));
assert_eq!(lookup("\\parse_url").map(|contract| contract.name), Some("parse_url"));
}
Expand Down
2 changes: 1 addition & 1 deletion crates/elephc-builtin-contract/src/requirements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ pub(crate) fn fixed_requirements(id: BuiltinId) -> &'static [BuiltinRequirement]
) {
return ZLIB;
}
if matches_name(id, &["mb_strlen"]) {
if matches_name(id, &["mb_strlen", "mb_strwidth"]) {
return ICONV_MACOS;
}
if matches_name(
Expand Down
6 changes: 3 additions & 3 deletions crates/elephc-builtin-contract/src/support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,14 +280,14 @@ mod tests {
// The thirty-four prelude-provided `curl_*` contracts are published only
// with the `curl` feature; see `crate::catalog_curl`'s module doc.
let curl_surface = if cfg!(feature = "curl") { 34 } else { 0 };
assert_eq!(eval_registry, 484 + curl_surface);
assert_eq!(eval_registry, 485 + curl_surface);
assert_eq!(eval_internal, 82);
assert_eq!(eval_pending, 31);
// Main's BCMath registry adds fourteen AOT contracts; this branch also
// promotes get_object_vars from an external surface into the registry and
// adds the ten iconv contracts and forty-three internal `__elephc_curl_*`
// entry points.
assert_eq!(aot_registry, 584);
assert_eq!(aot_registry, 585);
assert_eq!(aot_external, 10 + curl_surface);
assert_eq!(aot_unsupported, 3);
}
Expand Down Expand Up @@ -336,7 +336,7 @@ mod tests {
let curl_surface = if cfg!(feature = "curl") { 34 } else { 0 };
assert_eq!(shared_runtime, 19);
assert_eq!(hybrid_adapter, 2);
assert_eq!(interpreter_adapter, 463 + curl_surface);
assert_eq!(interpreter_adapter, 464 + curl_surface);
assert_eq!(unsupported, 113);
assert_eq!(
eval_execution(lookup("strval").expect("strval contract")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ pub(in crate::interpreter) enum EvalDirectHook {
StrWordCount,
/// Dispatches the whole `iconv*` extension family.
Iconv,
/// Dispatches `strlen(...)` and `mb_strlen(...)`.
/// Dispatches `strlen(...)`, `mb_strlen(...)`, and `mb_strwidth(...)`.
Strlen,
/// Dispatches `str_repeat(...)`.
StrRepeat,
Expand Down Expand Up @@ -543,6 +543,7 @@ impl EvalDirectHook {
Self::Iconv => eval_builtin_iconv_call(name, args, context, scope, values),
Self::Strlen => match name {
"mb_strlen" => eval_builtin_mb_strlen(args, context, scope, values),
"mb_strwidth" => eval_builtin_mb_strwidth(args, context, scope, values),
"strlen" => eval_builtin_strlen(args, context, scope, values),
_ => Err(EvalStatus::RuntimeFatal),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ pub(in crate::interpreter) enum EvalValuesHook {
StrWordCount,
/// Dispatches the whole `iconv*` extension family.
Iconv,
/// Dispatches `strlen(...)` and `mb_strlen(...)`.
/// Dispatches `strlen(...)`, `mb_strlen(...)`, and `mb_strwidth(...)`.
Strlen,
/// Dispatches `str_repeat(...)`.
StrRepeat,
Expand Down Expand Up @@ -679,6 +679,13 @@ impl EvalValuesHook {
}
_ => Err(EvalStatus::RuntimeFatal),
},
"mb_strwidth" => match evaluated_args {
[value] => eval_mb_strwidth_result(*value, None, context, values),
[value, encoding] => {
eval_mb_strwidth_result(*value, Some(*encoding), context, values)
}
_ => Err(EvalStatus::RuntimeFatal),
},
"strlen" => one_arg(evaluated_args, values, eval_strlen_result),
_ => Err(EvalStatus::RuntimeFatal),
},
Expand Down
Loading
Loading