[PHP-DEV] Re: RFC karma request: scalar object methods (with a working implementation)

[email protected] (Michal Kral)
Newsgroups php.internals
Message-ID <[email protected]>
Dne 29.06.2026 v 15:00 Michal Kral napsal(a):
> Hi all,
> 
> I have a complete, tested implementation of scalar object methods — 
> $str->trim(), (3)->pow(2) — and I'd like to write it up as a formal RFC. 
> Could someone grant me wiki RFC karma? The design and the branches are 
> below; I'd genuinely value early reactions while I write it up — 
> especially from anyone who remembers why the 2014 attempt stalled, since 
> this is built specifically to resolve that.
> 
> I know "methods on primitives" was proposed and declined before 
> (Nikita's 2014 "Methods on primitive types in PHP"). The reason it 
> stalled was loose typing: $x->trim() would need a runtime type check and 
> would behave differently depending on what $x held. This proposal 
> sidesteps that entirely, by generalizing the resolution Nikita himself 
> suggested in that thread — requiring an explicit cast where the type 
> isn't already clear.
> 
> The idea: dispatch only on receivers the compiler already knows are 
> scalar. The method call is rewritten at compile time to an ordinary call 
> into an internal backing class — no runtime type dispatch, no new 
> opcode, the object method-call path is untouched. A receiver qualifies 
> only if its type is guaranteed syntactically: a literal, a (string)/ 
> (int) cast, a concatenation/interpolation, a non-nullable scalar-typed 
> property, or a call with a declared non-nullable scalar return type. An 
> untyped $x->trim() is left exactly as today (Error). Crucially, dispatch 
> never depends on optimizer-inferred types, so behaviour is identical 
> with and without opcache.
> 
> echo "  Hello World  "->trim()->upper();   // "HELLO WORLD"
> echo (3)->pow(2);                          // 9
> echo "hello"->length()->pow(2);            // 25 — length():int chains 
> into the int methods
> So the cast Nikita proposed (((string) $num)->chunk()) is only needed 
> where the type isn't already guaranteed; everywhere else the dispatch is 
> sound by construction, with no runtime check.
> 
> It's structured as one proposal with two independent votes:
> 
> Scalar methods on guaranteed free receivers (the above). A pure 
> capability — it adds a way to call scalar operations and changes nothing 
> about untyped code. Proposed initial sets: a small curated Str (trim/ 
> upper/lower/length + contains/startsWith/endsWith), Int (abs/pow/clamp), 
> and Float (round/ceil/floor/abs); bool deliberately gets none (its 
> operations are operators, not methods). The sets are governed by 
> explicit criteria and are the easiest thing to tune in discussion.
> 
> Scalar-typed local variables (int $x = ...;, scalar types only), which 
> additionally make a typed local a guaranteed receiver (string $s = ...; 
> $s->trim()). This is the more contested half — it also carries the 
> "local type discipline" argument — so it's a separate vote: a "no" here 
> ships the capability without typed locals.
> 
> What I'm deliberately NOT doing, up front so it's not a surprise:
> 
> No method-call-result receivers ($this->getName()->trim()) — that would 
> rest on return-type covariance under inheritance; not worth the surface.
> Int::abs/pow return int|float (they can overflow, as the global 
> functions do), so they're honest terminals — they don't chain. 
> (Int::clamp is the one initial int method provably : int for all inputs, 
> so it does chain — no method declares : int while secretly overflowing.)
> No int|false typed locals — that's a sentinel state, not a committed 
> type; ?T is supported, sentinel-unions are not.
> The backing classes are internal-only (NUL-prefixed name, like anonymous 
> classes): class_exists('Str') is false, no Reflection, userland class 
> Str {} can't collide.
> Implementation status — this is built and tested, not a sketch:
> 
> Scalar methods add zero new opcodes — the desugar emits an ordinary 
> static call, and the object method-call path is byte-for-byte unchanged. 
> (Typed locals do add dedicated *_TYPED assignment opcodes, but the 
> untyped hot path stays byte-identical — see the perf point below.)
> Performance (deterministic callgrind, release build): the untyped hot 
> path is byte-identical; the standard bench.php suite is +0.145% 
> instructions, entirely from predicted-not-taken branches in reference 
> opcodes only, with zero added cache misses or branch mispredictions. 
> Untyped code pays effectively nothing.
> References (the objection that sank prior typed-locals attempts) are 
> enforced through every path — =&, by-ref params, array/object/static- 
> prop refs, yield, closure capture, $$name, extract, $GLOBALS, global — 
> via the existing typed-property reference machinery. Leak-checked under 
> stress.
> Correct under JIT in all three modes (interpreter, function, tracing — 
> differential byte-identical output). opcache SHM + file_cache round-trip 
> verified.
> BC impact, measured: an AST scan of the 1,000 most-downloaded Packagist 
> packages (173k+ files, incl. Laravel, Symfony, the AWS SDK, Guzzle, 
> PHPUnit, Doctrine) found zero affected call sites — every guaranteed- 
> scalar method-call site is a fatal error today, so none exist in real 
> code. Userland Str classes (incl. Laravel's Illuminate\Support\Str) 
> coexist with the backing class, verified.
> Branches (PHP 8.6-dev base):
> 
> Primary (scalar methods): https://github.com/kralmichal/php-src/tree/ 
> rfc/scalar-methods
> Secondary (typed locals, stacked): https://github.com/kralmichal/php- 
> src/tree/rfc/typed-locals
> What I'm asking:
> 
> RFC karma, so I can write this up on the wiki.
> Input I'd value while I write it up: does the "compile-time-guaranteed 
> receivers only" framing actually resolve the loose-typing objection for 
> you, or is there a hole I'm not seeing?
> The method-set/naming is the most open part — is a small curated set (a 
> "clean slate" API, distinct from the procedural names) the right call, 
> or a non-starter?
> (I'm not asking you to pre-approve the idea — I'll put the full RFC on 
> the wiki and we can have the real discussion there. This is to get karma 
> and to catch any fatal objection before I do.)
> 
> Thanks for reading, Michal Kral
Sorry forgot wiki username: michalkral
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.