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

[email protected] (Michal Kral)
Newsgroups php.internals
Message-ID <[email protected]>
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
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.