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