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

Reply via email to