Hi Seifeddine,
Restricting dispatch to receivers "the compiler already knows are scalar"
sounds safe, but in practice it covers almost no real code. [...] Move
Example into its own autoloaded file [...] So $x->length() would not
dispatch [...] usable only on literals and casts and almost nothing else.
The receiver in your example is $x, an untyped local, so $x->length()
doesn't dispatch at all. Untyped variables are never receivers, and the
feature never infers a variable's type from its assignment; it's
syntactic, never optimizer-inferred. So file layout is actually
irrelevant here: $x = Example::getStr(); $x->length(); wouldn't dispatch
even with Example in the same file. The "call with a declared scalar
return" rule is for direct chaining, f()->m(), not for a value you've
assigned to a variable first. To make your example dispatch you type the
variable:
string $x = Example::getStr();
$x->length();
and that is provable locally, wherever Example is defined.
(Method-call results are a separate case: Example::getStr()->length()
and $obj->getName()->trim() aren't receivers either, static or instance,
because proving their type would lean on inheritance and LSB the
compiler can't see.)
Where you're right is the one form that genuinely has this problem: a
direct f()->m() where f is a user function. That resolves only if f is
already declared at the point of compilation, which is file-order
dependent, and a whole-program analyser would accept what the
single-file compiler rejects. Fair hit. I'll restrict that form to
internal functions, where the compiler and the analyser agree
unconditionally, or drop it. Marginal either way.
But "almost no real code" isn't right. The receivers that matter aren't
cross-file: literals, casts, concatenation and interpolation, a $this
typed property declared on the class being compiled, and typed locals. A
typed local's type comes from its own declaration in the same function;
the property's from the class being compiled. Single compilation unit,
no file-layout dependence, and an analyser sees them identically. And
when the compiler can't prove the type it doesn't dispatch, it falls
through to today's behaviour. It never dispatches the wrong method; the
failure mode is "current error, or you add a type", not a silent bug.
Real-world values emerge from call chains, conditionals, and cross-file
boundaries
Right, and that's exactly what the typed-local half is for:
string $s = $user->getName();
$s->trim();
is provable locally wherever getName() lives. The free-receiver half is
the deliberately narrow, always-safe core; typed locals are how you get
a receiver for a cross-file value.
If $s->length() is sugar for Str::length($s), then Str [...] must be
visible to userland in some form. [...] Solve the naming problem with
naming, not by blinding the tooling.
The definitions aren't hidden. They're ordinary .stub.php files in core
(str.stub.php, int.stub.php, float.stub.php), which is what PhpStorm,
PHPStan and Psalm already consume for internal APIs. The NUL prefix only
hides the runtime symbol, not the published signatures. And an analyser
has to special-case scalar dispatch regardless, since a string isn't an
object and $s->trim() is never an ordinary method lookup.
That said, "solve naming with naming" is fair. The NUL prefix was only
to avoid adding a new global symbol to bikeshed. A reserved namespace or
a non-colliding visible name is a real alternative, and if it's
friendlier for tooling that's a good reason to prefer it. What shape
would you actually want there?
what happens with methods that take arguments, e.g. $s->indexOf($y)?
Does an arity mismatch fail at compile time, or at runtime with
ArgumentCountError
Same as any call to that method. It compiles to a static call on the
backing method, so an arity or type mismatch is the normal
ArgumentCountError / TypeError. No special path.
trim($s), mb_trim($s), and $s->trim() would all coexist [...] it
introduces a method-call syntax on values that carry no object identity.
I don't think the language is better for it.
That last one is taste, so I won't try to talk you out of a no. Couple
of points anyway. Additive isn't unusual: the pipe operator, named args
and first-class callables all coexist with the older forms. And it makes
no claim about object identity, it's compile-time sugar for a function
call, $s->trim() is just Str::trim($s).
Either way that's a real fix, so thanks. The method sets here are
deliberately minimal, just enough to show the mechanism working. They
can grow later; I'm not trying to settle the set in this thread.
Michal