Repository navigation
feat(ext): introduce Intl support - #2221
Open
Guikingone wants to merge 69 commits into
Open
Guikingone wants to merge 69 commits into
Guikingone wants to merge 69 commits into
Conversation
Intl support
Guikingone
force-pushed
the
feat/intl-managed-icu
branch
from
November 3, 2026 17:58
6a2f7e0 to
8c6028b
Compare
- Add the pinned `icu` native package (ICU 78.3), the C++ shim that owns the ICU objects and error state, and the `elephc-intl` Rust bridge behind the fixed C ABI. - Declare the Intl class family and the object-bearing procedural surface as synthetic AST; scalar entry points are registry builtins sharing one typed EIR lowering. No PHP source is parsed outside the test corpus. - Migrate the php-src ext/intl PHPT corpus to inline Rust codegen cases, with platform skips preserved. - Generate the Intl builtin reference, area index, module section, and comparison data. - Build and ship `libelephc_intl.a` in CI, the Docker scripts, the release tarball, the Homebrew formula, and the nextest archive.
Guikingone
force-pushed
the
feat/intl-managed-icu
branch
from
November 3, 2026 19:57
80871b0 to
b511f21
Compare
- DateTime splits a trailing zone token, applies the fixed offset, and stores PHP's zone name plus fixed-offset state; getTimezone/getOffset/format preserve it. - A shared compile-time tzdb abbreviation table feeds both the synthetic splitter and a literal strtotime() fold. - IntlTimeZone::fromDateTimeZone/IntlCalendar::setTimeZone/fromDateTime reject ICU-unknown abbreviations and too-large offsets with php-src's messages, recording on the calendar object where php does. - The ICU shim gains calendar __elephc_set_error; the managed recipe moves to revision 32.
- strtotime literal zones use a UTC helper, preserving the fixed offset for non-UTC processes. - Collator by-reference arrays stay mixed until typed ref storage lands; parity documents the subset. - IDN rejects non-UTS46 variants and empty domains, and returns false on ICU IDNA errors while filling the details array. - UConverter::getStandards is a string array so array_unique takes the string path. - IntlBreakIterator/IntlTimeZone implement php's compare handlers; IntlTimeZone debug info hides unset fields and inherited __debugInfo is dispatched. - BreakIterator clones copy the code-point state.
IntlTimeZone keeps initialized backing properties so cloning an unconstructed subclass reaches __clone and throws php's Error instead of dereferencing unset slots. Document the trailing tzdb abbreviation/fixed-offset parsing.
…ne errors - IntlPartsIterator follows the break iterator lazily and syncs its position per part. - IntlTimeZone::createEnumeration reports php's TypeError for invalid argument shapes. - Deprecated U_MULTIPLE_DECIMAL_SEPERATORS reads and constant() calls emit php's deprecation. - str_replace with a literal search array lowers to nested scalar replacements. - IDN length failures leave the info array empty.
…nstants new IntlBreakIterator() reaches php's private-constructor Error at runtime, and reads of NumberFormatter::TYPE_CURRENCY (direct or through constant()) emit php's runtime deprecation.
The constructor throws php's generic creation failure, create() returns null with a create-prefixed global error, and msgfmt_create() returns null with the procedural prefix. Invalid locales report the generic creation failure text.
The shim gained calendar object errors, IDN error returns, break-iterator clone state, root-locale handling, and message-formatter creation text; revision 33 invalidates stale native caches.
formatMessage()/parseMessage() report 'Creating message formatter failed' with the caller's zero code, while the constructor/create path reports the generic ICU-code failure.
numfmt_format()/parse() and the NumberFormatter methods reject TYPE_CURRENCY with php's ValueError but no longer duplicate the read-site deprecation.
…rmatter - Constructor/create/msgfmt_create reset the shared Intl error, then raise php's ArgumentCountError at runtime while accepting the call statically.\n- Passing null emits php's parameter deprecation and coerces to an empty string; arrays raise the runtime TypeError.\n- The constructor throws the detailed creation context, empty patterns fail like php, and invalid UTF-8 patterns report the UTF-16 conversion error.\n- U_UNMATCHED_BRACES reports the generic creation failure while U_PATTERN_SYNTAX_ERROR keeps the parse detail.\n- Bump the managed ICU recipe to 34 so the shim changes invalidate native caches.
# Conflicts: # crates/elephc-builtin-contract/src/registry.rs # crates/elephc-builtin-contract/src/support.rs # docs/php/compatibility.json # docs/php/compatibility.md # scripts/docs/audit_builtins.py # scripts/docs/elephc_builtins/test_contract_pipeline.py # tests/builtin_parity_tests.rs
- The direct-callable type guard now accepts a null argument for an object parameter, matching the dynamic-call path the optimizer tests rely on; the callee's own access/entry guard produces the observable error. - Apply the user-function type guard only to the Intl synthetic surface. Ordinary user functions already emit PHP's standard "Argument #N ($x) must be of type" TypeError and must observe argument side effects first. - Restore main's expectation that a bare null local passed to ?int &$slot is rejected; the explicit ?int declaration remains the supported recovery.
Guikingone
marked this pull request as ready for review
November 7, 2026 21:42
This was referenced Nov 7, 2026
Collaborator
Author
|
@nahime0 Ready for review when you have time |
# Conflicts: # crates/elephc-builtin-contract/src/registry.rs # crates/elephc-builtin-contract/src/support.rs
# Conflicts: # docs/php/compatibility.md # src/codegen/lower_inst/core_builtins/handlers.rs # src/optimize/fold/expr.rs # tests/codegen/gmp_math.rs
# Conflicts: # .github/workflows/ci.yml # .github/workflows/nightly.yml # .github/workflows/release.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ready for review —
ext/intlparity work. The multi-model review findings are fixed, the branch is updated with currentmain, and the full CI matrix is green on the updated head.What this PR adds
elephc native add icu) built from the pinned release archive, with a C++ shim owning every ICU object and a fixed C ABI into theelephc-intlbridge crate.synthetic_class, procedural functions through generated declarations, and bridge calls through typed EIR helpers.ext/intlcorpus migrated undertests/php_src/intl/, with a generated Rust harness (tests/codegen/intl_php_src_cases.rs) and per-fixture skip statuses.Review fixes (multi-model audit)
A five-model audit of the Intl packet produced confirmed findings; this PR now fixes them all, with regressions in
tests/codegen/intl_review_fixes.rs(21 tests):__destruct;cloneowns a distinct handle, andelephc_intl_live_count()returns to baseline afterunset.shared_ptrs, so a concurrentclosecannot free an object mid-call; oversized bridge payloads are fetched throughelephc_icu_intl_fetch_resultinstead of being truncated to 1 MiB.UConverter: the substitution-marker scrub only runs for single-byte destinations, so UTF-16 output such asFFFE 1A01is preserved.IntlDateFormatter::localtime(): reportstm_isdstfrominDaylightTime()and no longer dereferences a nullSimpleDateFormat; style-only formatters are supported.locale_set_default(''): resets to the process system default like PHP, andini_set('intl.*')alone links the bridge.IntlIterator:current()/key()are bounds-guarded and the array-backed iterator keeps its deliberate self-cycle (no destructor), sogc_collect_cycles()still reports it;IntlPartsIterator::getRuleStatus()carries the part's ICU rule status instead of a hard-coded zero.NumberFormatter: omitted$typeisTYPE_DOUBLE, andROUNDING_INCREMENTis a double attribute.Normalizer: unpublished forms throwValueError(getRawDecompositionreturns""like PHP) instead of silently normalizing as NFC.html_entity_decode(): default/ENT_QUOTESflags and UTF-8 are supported; numeric references still fold underENT_NOQUOTESwhen the literal contains no quote entity.ENT_COMPAT, HTML5 doctypes, runtime flags, and non-UTF-8 encodings (or quote entities the runtime cannot decode) are refused at compile time.defined(): exact Intl constant membership (includingLC_*) instead of prefix matching.str_replace()search arrays apply in source order (the builtin contract stays string-only, so an unlowerable array search is refused with PHP's message), objects at builtin string parameters are coerced only for native SimpleXML views,strtotime()carries$baseTimestamp, large literalrange()stops folding past 256 elements, dynamic callable guards evaluate arguments before throwing and use PHP's messages, andunset($object)only defers the safe point forIntlIterator.Final parity ledger
cargo test --test codegen_tests test_php_src_intl:434 passed / 0 failed / 48 ignored (previous: 426/8, 425/9, 424/10, 423/11, 412/22, 393/41, 388/46, 378/56, 377/57, 375/59, 373/61, 367/67, 359/75, 351/83, 323/111).
The 48 ignored cases are exactly what php-src itself does on this platform, verified against reference PHP 8.5.10 + ICU 78.3 on macOS ARM64: 47 upstream
SKIP(ICU upper-bound gates, 32-bit-only, Windows-only, x86-only, missing optional extensions) and 1 upstreamXFAIL(dateformat_format_variant3.phpt). No applicable fixture is skipped by elephc.Parity work completed in this branch
formatObjecterror paths,localtimetm arrays, DateTime offset zones, and the procedural aliases.unum_open, pattern styles, compact styles, padding/currency text attributes, parse offsets, TYPE_INT64, php's TYPE_* / type-currency ValueErrors, symbol validation, and serialization refusal.sort()/asort()/sortWithSortKeys()order by a string key while keeping the originalMixedcells, so object values survive (bug75193) and object__toStringside effects keep php-src'scollator_sort_modify_during_comparebehaviour.ures_openDirectfallback semantics, sub-table ownership, key iteration, numeric-string keys, localized error contexts, and the fallback warning state.toDateTimeerror wrapping.grapheme_substrlength handling, UTF-8 MAXBYTES/MAXCHARS extraction withnext, and collation-aware Levenshtein.valid/text/typeid), rule-based construction diagnostics and runtime arity/type checks,__clonehandle ownership, and code-point method reachability.toUCallback()/fromUCallback()run a PHP-side UTF-8 <-> ASCII callback path with php-src's exact callback sequence (reset/illegal/unassigned/close), because ICU's C++ hooks cannot re-enter generated PHP. Converters without overrides and every other encoding keep the managed bridge. The callback marker table is keyed by the bridge handle and cleared on destruction.IntlIteratoris reclaimed and reported bygc_collect_cycles()exactly like php-src's cycle-management test.intl_get_error_*status formatting,DateTimeZoneunknown-id rejection, and a recipe revision bump (lock revision 40) so the shim is rebuilt.Compiler/runtime support added for the corpus
foreachunrolling with per-iterationcontinueskip flags and distinct source columns, soReflectionFunction/ReflectionMethodmigration loops carry statically-known names;get_extension_funcs("intl")and two-literalrange()fold to literal arrays, and literal array facts survive unrelated runtime-state setters (set_error_handler, …).$class::$prop/($expr)::$propparse throughStaticReceiver::Dynamic, the optimizer folds a literal class-string receiver toNamedbefore type checking, andC::$prop =& $localaliases the static slot in both directions.ReflectionClass::newInstance()/newInstanceArgs()constructs a statically-known class directly (including$subclass::$ARGStables); abstract targets and any non-public constructor (declared or inherited) take the reflective path and raise PHP's catchableReflectionException: Access to non-public constructor of class …. Dynamic callable sites evaluate arguments before raising PHP-shapedArgumentCountError/TypeErrorerrors, with variadic collector parameters excluded from the per-element type guard.gc_collect_cycles()counts unreachable object/callable graph nodes (falling back to total nodes for array-only graphs); unsetting a destructor-less object local only drops a root, so the next explicit collection observes the cycle as PHP does, while classes with__destructkeep the eager safe point that destructor-resurrection paths rely on.WeakReference::get()'s receiver asmixed, so an unrelated class'sgetsignature (Intl'sResourceBundle::get) cannot pin the local and reject the following private WeakMap call. This keepsmethod_exists()/get_class_methods()on Intl objects working in programs that also inject the WeakMap prelude.TypeError/ArgumentCountErrorclasses (not a genericError), resolve$thisand unresolved object receivers without false type mismatches, and leave the language-constructclone()to its descriptor path so argument side effects precede the error. Effectful-key/property and root-drop unsets keep unconditional collection; only destructor-less object locals defer to the next explicit collector.mixeduntil the checker resolves their value, so static defaults like['x', SomeClass::VALUE]no longer materialize as strings.Validation
cargo test --test codegen_tests codegen::intl_review_fixes— 21 passed / 0 failed on the updated head; the full CI run for945c0d0b46is green, and thebadargs/all-target unit tests now have explicit 300s nextest budgets.origin/main:cargo test -p elephc-builtin-contract— 67 passed / 0 failed.6792ab60be. The harness adaptations restore the fixture INI locale forini_restore('intl.default_locale')(locale_get_default.phpt) and use a typed-bool helper fornormalizer_is_normalized, matching the OOisNormalizedresults.cargo buildis warning-free;git diff --checkis clean; generated builtin docs/registry and parity/audit gates are green (Errors: 0; 2881 generated pages validated); the PHP coverage page regenerated to 1265 functions / 191 classes / 1420 constants after the merge.libsqlite3-sysis bumped to 0.39.1 for RUSTSEC-2026-0459;cargo test -p elephc-pdois 140 passed / 0 failed / 2 ignored. The Linux codegen shard cap is raised to 60 minutes (the Intl corpus widened the shards; an aarch64 shard hit the old 40-minute cap).origin/mainwas merged at67a956b17d(merge commit3af94a77be), resolving the builtin-count and generated-doc ledgers for the new eval/reflection contracts.ci-build-host.yml;elephc-intlis in the shared bridge list there and in the release-artifacts action, and the ICU cache/install steps warm the shards’ key once per platform.examples/intl/carries ICU recipe revision 40.Notes
array_intersect()sparse-key preservation is a separate general gap (elephc-d63l); the Intl case only needs values and is unaffected.C::$prop =& $localremain tracked byelephc-4ml.12; the static-property form required bybug62915_2is implemented here.6792ab60be; the PR is mergeable with currentmain.