facebook / facebook/hhvm

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

Open
#8,976 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
18.7k
Forks
3.1k
Avg merge
1h 47m
Merged PRs (30d)
2

Description

**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 {
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(
mixed $_pos,
string $_key,
Spliceable $s,
): Dsl::TAst {
return $s->visit(new Dsl());
}
}

final class ExprTree
implements Spliceable {
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 {
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 where TInfer#1 = int [2]

Block2.hack:45:34
6 | mixed $_metadata,
7 | (function(Dsl): Dsl::TAst) $ast,
[2] 8 | ): ExprTree {
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 { 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(
mixed $_pos,
string $_key,
\Spliceable $s,
): Dsl::TAst { return $s->visit(new Dsl()); }
}
final class ExprTree
implements \Spliceable {
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 {
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 { 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(
mixed $_pos,
string $_key,
\Spliceable $s,
): Dsl::TAst { return $s->visit(new Dsl()); }
}

final class ExprTree
implements \Spliceable {
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 {
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 {
26 | public function visit(TVisitor $v): TResult;
27 | }
:
54 |
55 | namespace /* Explicitly in the root namespace */ {
[1] 56 | interface Spliceable {
57 | public function visit(TVisitor $v): TResult;
58 | }

1 error found.
```

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

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the issue with the shown hhconfig setting and the Block1.hack, Block2.hack, and Block3.hack examples. Trace the expression-tree typechecking and name-resolution behavior, then verify that separate DSL libraries can use Spliceable without undefined-name or root-namespace collision errors.

Written by the indexing model from the issue text.

Assessment

Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.