getify / TypL

The Type Linter for JS
https://TypL.dev
MIT License
374 stars 13 forks source link

Interesting quirk, should be aware of #27

Open getify opened 5 years ago

getify commented 5 years ago

There's nothing to do for this issue... I just want to document this quirk somewhere.

var x;
foo(x);
x = 3;
foo(x);
function foo(a = number) {}

Think carefully about this code. What do you expect to happen? Any errors?

This code requires a second pass, because of the foo(..) hoisting. On the first pass, x is initially implied as undef, then it's re-implied as number. So... on the second pass, would we expect the first foo(x) to throw an error or not?

I claim: yes it should throw an error. Even though x becomes number later in the code, at the moment we first see foo(x), it's still undef. On that second pass, we know foo(..) expects a number, so we should get an error about argument type mismatch.

Likewise, for the second foo(x)... since that one comes after a = 3, by that moment (even on the first pass), we know that foo(x) is trying to pass a number. So... since that's valid, no error should be thrown for that line.

The output (currently) is:

(pass 1) ------------------------
Implying x as inferred-type 'undef', at line 1, column 4
Re-implying x to inferred-type 'number', at line 3, column 0
Implying foo as inferred-type 'func', at line 5, column 0
Implying a as tagged-type 'number', at line 5, column 13
Function 'foo' signature: {"type":"func","params":[{"tagged":"number"}],"hasRestParam":false,"return":{"default":true,"inferred":"undef"}}, at line 5, column 0
(pass 2) ------------------------
Argument type mismatch: expected type 'number', but found type 'undef', at line 2, column 4
Function 'foo' signature: {"type":"func","params":[{"tagged":"number"}],"hasRestParam":false,"return":{"default":true,"inferred":"undef"}}, at line 5, column 0
mraak commented 5 years ago

Definitely yes, should throw an error for first foo(x) and not for second. Just because the function is declared later in the code, it is declared with a certain signature that developer added with a purpose to be respected. The fact we're doing multi passes and that function is hoisted should not play a role, it is just a technical detail.

I actually think this is a very good example in how typl differs from other static type checkers and is suited to JS style.

getify commented 5 years ago

:)

getify commented 5 years ago

BTW, the reason why the second pass matters here is, if I wasn't careful to preserve this behavior, on the second pass x would just always be a number, and therefore neither of the function calls would report an error.

The way this works is, if an identifier is determined to be undef (on any pass), that is actually stored for that node. Subsequent passes do not update node types that are already assigned (even if undef), but they can update an unknown.

So, in the first pass, the x node in foo(x) is set to undef because at that moment, it hasn't been assigned any other type (and it's local to that scope, not a param). Then the x = 3 forces an update to the scope binding for x, setting it to number.

On the second pass, even though the scope binding for x is now number, the x in foo(x) is still undef from the first pass.

That's basically how Typl uses the lexical ordering to infer different types (specifically undef vs any other type) throughout the lifetime of that scope.