RFC karma request: scalar object methods (with a working implementation)

php.internals

Michal Kral

64 days ago
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

Michal Kral

64 days ago
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

Seifeddine Gmati

64 days ago
On Mon, 29 Jun 2026 at 14:02, Michal Kral <michal@entrylog.eu> wrote:
> > I'd genuinely value early reactions while I write it up
Hi Michal, Could you please open a PRE-RFC discussion thread? I don't think the karma request thread is the right place to discuss this. Cheers, Seifeddine.

Michal Kral

63 days ago
Dne 29.06.2026 v 16:21 Seifeddine Gmati napsal(a):
> On Mon, 29 Jun 2026 at 14:02, Michal Kral <michal@entrylog.eu> wrote: >> >> I'd genuinely value early reactions while I write it up > > Hi Michal, > > Could you please open a PRE-RFC discussion thread? I don't think the > karma request thread is the right place to discuss this. > > Cheers, > Seifeddine.
Fair point — agreed. I've opened a pre-RFC discussion thread for it; would value your take there, especially given your Psl work.