Skip to content

Latest commit

 

History

History
145 lines (115 loc) · 5.66 KB

turbofish.md

File metadata and controls

145 lines (115 loc) · 5.66 KB

Turbofish: call-site type arguments

xphp uses the turbofish operator ::<T> to attach type arguments to a call site. The operator works in five shapes, all whitespace-sensitive: there must be no space between :: and <.

This syntax tracks the PHP RFC: bound-erased generic types proposal, so source written for xphp today is forward-compatible with a future RFC-aligned native PHP runtime.

Example

<?php
declare(strict_types=1);

namespace App;

// 1. Constructor (with `new`)
$box = new Box::<int>(42);

// 2. Static method call
$x = Util::identity::<int>(7);

// 3. Free function call
$p = pair::<int, string>(1, 'a');

// 4. Instance method call (regular + nullsafe)
$names = $users->map::<string>(fn(User $u): string => $u->name);
$result = $obj?->process::<Order>($payload);

// 5. Variable turbofish (a closure or arrow assigned to a variable)
$id = fn<T>(T $x): T => $x;
$y  = $id::<int>(42);

// All five shapes accept the empty turbofish when every type-param
// has a default:
$cache = new Cache::<>;          // pads to <string, mixed>
$any   = Util::any::<>();        // method with all-defaulted params
$f     = $closure::<>($arg);     // closure with defaulted T

What gets emitted

The turbofish strips from the cleaned source so PHP's parser sees a normal call. The compiler then rewrites the call to point at the right specialization:

// new Box::<int>(42)  ->
new \XPHP\Generated\App\Box\T_<hash-of-int>(42);

// Util::identity::<int>(7)  ->
Util::identity_T_<hash-of-int>(7);

// pair::<int, string>(1, 'a')  ->
\App\pair_T_<hash-of-(int,string)>(1, 'a');

// $users->map::<string>($fn)  ->
$users->map_T_<hash-of-string>($fn);

// $id::<int>(42)  ->  (variable turbofish goes through the dispatcher)
$id('T_<hash-of-int>', 42);

Rules

  • Whitespace-sensitive: Foo:: <T> is not a turbofish; the scanner requires ::< to be adjacent.
  • Anchored to a name: Foo<T>(...) (no ::) is a PHP-side ambiguity (Foo < T could be comparison) and is rejected.
  • The method name must be a literal. A turbofish on a dynamically-named method or static call — $o->$m::<int>(), the nullsafe $o?->$m::<int>(), the static Foo::$m::<int>(), or a variable-variable $$g::<int>() — is rejected with a diagnostic: the specialized method name cannot be resolved from a value known only at runtime. The name may be any literal identifier, including a PHP keyword — $o->list::<int>(), Foo::print::<int>(), and the matching declaration public function list<T>(...) all work, since PHP permits keywords as method names.
  • All call-site shapes can carry empty turbofish ::<> when the template is all-defaulted.
  • Bare new Foo; (no ( or ::<>) also works for all-defaulted class templates — see defaults.
  • The type argument is not inferred from the call arguments. A turbofish-less call ($x->pick('a') instead of $x->pick::<string>('a')) is a compile error (xphp.missing_type_argument), not a silent skip — xphp check catches a forgotten turbofish at build time rather than letting it fatal at runtime. A generic method whose type parameters are all defaulted may still be called bare; a named generic function or closure has no bare or empty-turbofish form, so it always needs an explicit turbofish. (A first-class callable pick(...) creates a closure rather than calling, and is left alone.)

Receiver-type analysis (instance methods)

For instance-method turbofish $x->m::<T>(...), the compiler needs to know what class $x holds to pick the right method template. The receiver's type is determined from:

  1. A declared type: a typed parameter, a typed property ($this->prop), or $this.
  2. A local assigned from new Foo(), a method return, a chained call, or a self/static factory.
  3. A branch whose arms all agree on the same class.

If the analysis can't prove a single class — $x is an untyped foreach variable, or it's reassigned across a branch whose arms disagree — the turbofish call can't be specialized. The generic method is stripped from its class, so a non-specialized call would fatal at runtime ("undefined method"); rather than emit that, the compiler reports a compile-time error (xphp.undetermined_receiver). Give the receiver a statically-known type.

Once the receiver class is known, the method is resolved through its inheritance chain (nearest ancestor first), so a generic method declared on a base class is callable on a subclass receiver. The same holds for the static (Sub::m::<T>()) and nullsafe ($x?->m::<T>()) shapes. See methods and functions → inheritance.

A turbofish call whose generic method can't be resolved on the receiver or any of its ancestors is a compile-time error (xphp.unresolved_generic_call) rather than a silent pass-through that fatals at runtime.

Caveats

  • ⚠️ Branching narrowing precision — receiver-type analysis conservatively refuses to ground $x after it's reassigned across branches whose arms disagree; a turbofish call there is a compile error (xphp.undetermined_receiver), not a silent de-specialization. See caveats.

See also