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

- Thread: https://externals.io/message/131584
- Original archive: https://news-web.php.net/php.internals/131584
- Started by Michal Kral on 2026-06-29T13:00:57+00:00
- Messages: 4
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #131584 — Michal Kral — 2026-06-29T13:00:57+00:00

URL: https://externals.io/message/131584#131584

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

---

## #131586 — Michal Kral — 2026-06-29T13:38:10+00:00

URL: https://externals.io/message/131584#131586 · In reply to: #131584

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

---

## #131588 — Seifeddine Gmati — 2026-06-29T14:21:03+00:00

URL: https://externals.io/message/131584#131588 · In reply to: #131584

>
> 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.

---

## #131630 — Michal Kral — 2026-06-29T22:34:13+00:00

URL: https://externals.io/message/131584#131630 · In reply to: #131588

Dne 29.06.2026 v 16:21 Seifeddine Gmati napsal(a):
> 
> > […]
> 
> 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.
