Re: [PHP-DEV] [Pre-RFC] Scalar object methods (with a working implementation)

[email protected] ("Rob Landers")
Newsgroups php.internals
Message-ID <[email protected]>

On Tue, Jun 30, 2026, at 00:32, Michal Kral wrote:
> Hi all,
> 
> Per Seifeddine's suggestion to keep this out of the karma-request 
> thread, I'm opening a pre-RFC discussion for scalar object methods -- 
> calling a small, curated set of methods directly on scalar values, e.g. 
> $str->trim(), (3)->pow(2). There's a complete, tested implementation and 
> a full write-up (links below); I'd like to surface the strongest 
> objections before I write the formal RFC.
> 
> Disclosure: I built this with an AI assistant (Claude) as a tool. The 
> design and the decisions are mine, and I've independently verified the 
> engine behaviour, performance, JIT correctness, leak-freedom and the BC 
> scan. Flagging it up front for transparency.
> 
> 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 intended as one proposal with two independent votes:
> 
> 1. 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.
> 
> 2. 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 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 add dedicated *_TYPED assignment opcodes, but the untyped 
> hot path stays byte-identical.)
> - 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. A 
> typed-local write benchmarks at ~0.79x the cost of a typed-property 
> write -- a check the language already runs on every typed-property write 
> since 7.4.
> - 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) found zero method-call sites with a 
> guaranteed-scalar receiver -- i.e. zero call sites that change behaviour 
> (every such site is a fatal error today). Userland Str classes (incl. 
> Laravel's Illuminate\Support\Str) coexist with the backing class, verified.
> 
> Full write-up (RFC draft, plus the method-set, performance, and 
> BC-impact analyses): 
> https://github.com/kralmichal/php-src/tree/rfc/docs/scalar-object-methods-rfc
> 
> Implementation 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'd value discussing before I write the formal RFC:
> 
> 1. Does the "compile-time-guaranteed receivers only" framing actually 
> resolve the loose-typing objection, or is there a hole I'm not seeing?
> 2. The method-set and naming is the most open part -- is a small 
> curated, clean-slate set (distinct from the procedural names) the right 
> direction, or a non-starter? How should it relate to the existing 
> userland efforts in this space (e.g. Psl)?
> 3. Anything that would sink this before I invest in the full RFC.
> 
> Thanks,
> Michal Kral
> 

Hi Michal,

This is the part that gets me:

> requiring an explicit cast where the type isn't already clear.

Explicit casting in PHP is *dangerous*:

(int) "123password" === 123

https://3v4l.org/IAF8W

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