[ Runtime ] Feedback on expression trees, `\Spliceable` is hardcoded in the typechecker, but not declared, this will cause name collisions
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\DslBlock1.hack
interface X extends \Spliceable {}Block2.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:
- Ensure you have added the line to the hhconfig file
- Try to typecheck the first code block
- Observe that the typechecker says
\Spliceabledoes not exist *1 - Run the first block
- Observe an unbound name error *2
- Try to typecheck the second block
- Observe the typechecker insisting on the
\Spliceableinterface being expected *3 - Run the second block
- 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.0Additional context
This results in name collisions like this:
Block3.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.
Source: facebook/hhvm