#8976·hhvm

[ Runtime ] Feedback on expression trees, `\Spliceable` is hardcoded in the typechecker, but not declared, this will cause name collisions

Author: lexidorCreated Jan 28, 2022Updated Sep 18, 2025

Describe the bug The \Spliceable<_, _, _> interface should be a built-in or Hack should allow Spliceable-like interfaces to be declared under other names.

The current implementation is likely to cause collisions between libraries, since every library must declare \Spliceable<_, _, _> in the root namespace.

Standalone code, or other way to reproduce the problem

In order for this code to typecheck, you must add the following line to the .hhconfig file whilst expression trees are still experimental.

allowed_expression_tree_visitors = AmazingSql\Dsl, AmazingJs\Dsl

Block1.hack

hack
interface X extends \Spliceable {}

Block2.hack

hack
// AmazingSql wants to declare a Dsl...
namespace AmazingSql {
  class Dsl {
    const type TAst = mixed;
    public static function makeTree<<<__Explicit>> TInfer>(
      mixed $_pos,
      mixed $_metadata,
      (function(Dsl): Dsl::TAst) $ast,
    ): ExprTree<Dsl, Dsl::TAst, TInfer> {
      return new ExprTree($ast);
    }
    public static function intType(): int {
      invariant_violation('stub');
    }
    public function visitInt(mixed $_pos, int $value): Dsl::TAst {
      return $value;
    }
    public function splice<T>(
      mixed $_pos,
      string $_key,
      Spliceable<Dsl, Dsl::TAst, T> $s,
    ): Dsl::TAst {
      return $s->visit(new Dsl());
    }
  }

  final class ExprTree<TVisitor, TResult, +TInfer>
    implements Spliceable<TVisitor, TResult, TInfer> {
    public function __construct(private (function(TVisitor): TResult) $ast) {}
    public function visit(TVisitor $v): TResult {
      return ($this->ast)($v);
    }
  }
}

// This is not possible, declaring Spliceable in a namespace (or under a different name).
namespace AmazingSql {
  interface Spliceable<TVisitor, TResult, +TInfer> {
    public function visit(TVisitor $v): TResult;
  }
}

namespace UserNamespace {
  <<__EntryPoint>>
  function main(): void {
    $sql_int = \AmazingSql\Dsl`1`;
    $sql_thing = \AmazingSql\Dsl`${$sql_int}`;
    \var_dump($sql_thing->visit(new \AmazingSql\Dsl()));
  }
}

Steps to reproduce the behavior:

  1. Ensure you have added the line to the hhconfig file
  2. Try to typecheck the first code block
  3. Observe that the typechecker says \Spliceable does not exist *1
  4. Run the first block
  5. Observe an unbound name error *2
  6. Try to typecheck the second block
  7. Observe the typechecker insisting on the \Spliceable interface being expected *3
  8. Run the second block
  9. Observe int(1) being printed

*1

Naming[2049] Unbound name: Spliceable (an object type) [1]

Block1.hack:1:21
[1] 1 | interface X extends \Spliceable {}

1 error found.

*2


Fatal error: Undefined interface: Spliceable in /path/to/Block1.hack on line 1

*3

Typing[4110] Typing error [1]
-> Expected Spliceable<[unresolved], [unresolved], [unresolved]> because this is being spliced into another Expression Tree [1]
-> But got AmazingSql\ExprTree<AmazingSql\Dsl, mixed, TInfer#1> where TInfer#1 = int [2]

Block2.hack:45:34
     6 |       mixed $_metadata,
     7 |       (function(Dsl): Dsl::TAst) $ast,
[2]  8 |     ): ExprTree<Dsl, Dsl::TAst, TInfer> {
     9 |       return new ExprTree($ast);
    10 |     }
       :
    43 |   function main(): void {
    44 |     $sql_int = \AmazingSql\Dsl`1`;
[1] 45 |     $sql_thing = \AmazingSql\Dsl`${$sql_int}`;
    46 |     \var_dump($sql_thing->visit(new \AmazingSql\Dsl()));
    47 |   }

1 error found.

Expected behavior

Either \Spliceable<_, _, _> should be a built-in, which libraries can use directly or extend. Or the typechecker should be fine with AmazingSql\Spliceable<_, _, _> being used without extending \Spliceable<_, _, _>.

Actual behavior

Expected Spliceable<[unresolved], [unresolved], [unresolved]>, this type does not exist (yet). If AmazingSql were to declare it and use \Spliceable instead of \AmazingSql\Spliceable, it would trample on every other library that wants to declare a Dsl.

Environment

  • Operating system

Ubuntu 18.04

  • Installation method

apt-get with dl.hhvm.com repository

  • HHVM Version
HipHop VM 4.146.0 (rel) (non-lowptr)
Compiler: 1643144849_229965698
Repo schema: 35e2b287a674e6ca77e7702f7b27e194f8310e10
hackc-9ddd1899e3c5cf4954e903514c8f4d89beadd202-4.146.0

Additional context

This results in name collisions like this:

Block3.hack

hack
// Library one, AmazingSql declares a Dsl.
namespace AmazingSql {
  class Dsl {
    const type TAst = mixed;
    public static function makeTree<<<__Explicit>> TInfer>(
      mixed $_pos,
      mixed $_metadata,
      (function(Dsl): Dsl::TAst) $ast,
    ): ExprTree<Dsl, Dsl::TAst, TInfer> { return new ExprTree($ast); }
    public static function intType(): int { invariant_violation('stub'); }
    public function visitInt(mixed $_pos, int $value): Dsl::TAst { return $value; }
    public function splice<T>(
      mixed $_pos,
      string $_key,
      \Spliceable<Dsl, Dsl::TAst, T> $s,
    ): Dsl::TAst { return $s->visit(new Dsl()); }
  }
  final class ExprTree<TVisitor, TResult, +TInfer>
    implements \Spliceable<TVisitor, TResult, TInfer> {
    public function __construct(private (function(TVisitor): TResult) $ast) {}
    public function visit(TVisitor $v): TResult { return ($this->ast)($v); }
  }
}
namespace /* Explicitly in the root namespace */ {
  interface Spliceable<TVisitor, TResult, +TInfer> {
    public function visit(TVisitor $v): TResult;
  }
}

// Library two, AmazingJs declares a Dsl too...
namespace AmazingJs {
  class Dsl {
    const type TAst = mixed;
    public static function makeTree<<<__Explicit>> TInfer>(
      mixed $_pos,
      mixed $_metadata,
      (function(Dsl): Dsl::TAst) $ast,
    ): ExprTree<Dsl, Dsl::TAst, TInfer> { return new ExprTree($ast); }
    public static function intType(): int { invariant_violation('stub'); }
    public function visitInt(mixed $_pos, int $value): Dsl::TAst { return $value; }
    public function splice<T>(
      mixed $_pos,
      string $_key,
      \Spliceable<Dsl, Dsl::TAst, T> $s,
    ): Dsl::TAst { return $s->visit(new Dsl()); }
  }

  final class ExprTree<TVisitor, TResult, +TInfer>
    implements \Spliceable<TVisitor, TResult, TInfer> {
    public function __construct(private (function(TVisitor): TResult) $ast) {}
    public function visit(TVisitor $v): TResult { return ($this->ast)($v); }
  }
}

namespace /* Explicitly in the root namespace */ {
  interface Spliceable<TVisitor, TResult, +TInfer> {
    public function visit(TVisitor $v): TResult;
  }
}

// User of AmazingSql and AmazingJs
namespace UserNamespace {
  <<__EntryPoint>>
  function main(): void {
    $sql_int = \AmazingSql\Dsl`1`;
    $sql_thing = \AmazingSql\Dsl`${$sql_int}`;
    \var_dump($sql_int->visit(new \AmazingSql\Dsl()));

    $js_int = \AmazingJs\Dsl`1`;
    $js_thing = \AmazingJs\Dsl`${$js_int}`;
    \var_dump($js_thing->visit(new \AmazingJs\Dsl()));
  }
}
Naming[2012] Name already bound: Spliceable [1]
-> Previous definition is here [2]

Block3.hack:56:13
    23 | }
    24 | namespace /* Explicitly in the root namespace */ {
[2] 25 |   interface Spliceable<TVisitor, TResult, +TInfer> {
    26 |     public function visit(TVisitor $v): TResult;
    27 |   }
       :
    54 | 
    55 | namespace /* Explicitly in the root namespace */ {
[1] 56 |   interface Spliceable<TVisitor, TResult, +TInfer> {
    57 |     public function visit(TVisitor $v): TResult;
    58 |   }

1 error found.

Both AmazingSql and AmazingJs need \Spliceable<_, _, _>, but if they both declare it... collision.