Commit Graph
226 Commits
Author SHA1 Message Date
Sathya Gunasekaran c130bcc9c2 [typer] Use opaque TypeId to number types 2022-12-20 14:55:52 +00:00
Sathya Gunasekaran 549f5a4af6 [typer] Return a narrow-er type from makeType 2022-12-20 14:55:48 +00:00
Joe Savona 828fd81be3 Add new pass to playground 2022-12-20 12:16:34 -08:00
Joe Savona af91a7ab86 Ensure member path assignments memoize independently
Assignment expressions to a member path are a special case because they're the 
only place where a value isn't assigned to a (possibly temporary) variable, 
which is our unit of memoization. #901 demonstrated how this can lead to values 
that can't be independently memoized: 

```javascript 

const x = {a: a} 

x.y = [b, c]; // array recomputed w `x`, even if only `a` changed 

``` 

This PR ensures that assignment expressions where the LHS is a member path lower 
the RHS to a Place. That means the above example is handled as if you wrote: 

```javascript 

const x = {a: a}; 

const tmp1 = [b, c]; 

x.y = tmp1; 

``` 

And we independently memoize the temporary.
2022-12-20 11:06:08 -08:00
Joe Savona cedaee5b03 Codegen for scopes without dependencies
Completes a todo for the `if` condition of scopes without any inputs. In this 
case since there are no inputs we can check for changes, we check if the first 
_output_ cache slot is set to the sentinel. 

For this to work we need to ensure that all scopes have at least one output, 
which isn't currently the case. Dead code can produce output-less sentinels. So 
this PR also adds a pass to find scopes w/o any outputs and convert them to 
regular blocks. We could in theory also just delete them, but for now let's be 
more conservative. This is something we'd want to highlight as a diagnostic in 
an IDE, though existing dead code linters would almost certainly find this case 
too.
2022-12-20 11:06:07 -08:00
Lauren Tan ae8f9066dd Fix invalid import path in test262-preprocessor 2022-12-20 11:42:51 -05:00
Lauren Tan 2721d2a0d9 Remove compiler flags
Remove our existing compiler flags since they were only being used for 
enabling/disabling passes to aid debugging and to simplify in preparation for 
the upcoming work on diagnostics and bailouts. Additionally with the new 
playground tabs disabling passes has become less necessary. In the future when 
we have actual compiler flags (eg tweaking optimization levels) we can add this 
back. 

I opted to keep the existing `CompilerResult` return value instead of just 
returning the optimized AST as we're still using `scopes` in our test fixtures.
2022-12-20 11:42:51 -05:00
Lauren Tan 7cf8e9ac42 [ez] Fixup missing passes in test262-preprocessor 2022-12-20 11:13:05 -05:00
Lauren Tan 9390e969e2 mv src/HIR/Pipeline src/CompilerPipeline
Small reorganization to move Pipeline out of HIR since it's a compiler module. 
It used to make sense before to be in HIR since the old architecture was the 
still the primary, but no longer!
2022-12-20 11:13:05 -05:00
Sathya Gunasekaran f1af022be1 [typer] Fix objectTypeEquals 2022-12-20 13:38:09 +00:00
Sathya Gunasekaran 6f06b0e08a [typer] Reuse PolyType objects 2022-12-20 13:38:08 +00:00
Sathya Gunasekaran 2c3e572566 [typer] Be more conservative with function type inference
It's not safe to infer types of arguments and return values because Javascript 
is so polymorphic. Instead just infer the type of the callee for non methods. 

Interestingly, even this is not conservative enough for JavaScript because Proxy 
can also be callable. But I think for our use cases we will treat Proxy and 
Functions similarly (they're all just objects) so it's ok.
2022-12-20 13:38:07 +00:00
Joe Savona 9207607d37 Delete old architecture
Thanks old architecture! We learned a lot about what does and doesn't work via 
this POC, but it's time to move forward w the ~~new~~ current architecture.
2022-12-19 10:04:54 -08:00
Joe Savona d9b9554948 Remove old architecture from playground
Removes the old architecture from the playground. Next step deletion.
2022-12-19 10:00:16 -08:00
Joe Savona 5db523a98a Fix bug with scope output analysis for nested scopes
When collecting the output of each scope I was checking to see whether each 
operand was being used after the end of the _current_ scope. What we really want 
to be checking is whether the operand is used after _the scope in which it's 
defined_. Those are the same thing when there is no nesting, involved, so the 
previous logic worked for most examples. 

It isn't super easy to tell when if the operand's scope has ended, because we 
don't always know what the "current" InstructionId is inside a ReactiveFunction. 
And that in turn isn't quite so easy to change, because of some edge cases like 
break statements that we synthesize. The solution here is to track the set of 
active scopes, and if an operand is used and its scope is not active then voila, 
it's scope must have completed and its an output.
2022-12-18 12:14:41 -08:00
Joe Savona d649940c27 Test case showing lack of independent memoization of object properties 2022-12-18 12:14:38 -08:00
Joe Savona 05d1fe94f9 [be] Split files into directories
Moves _some_ files from HIR into new top-level directories. To not make 
@gsathya's life a pain I left the files he's touching alone, but I moved some 
others. My intent is to have something like this: 

* Babel/ - code for the Babel plugin, though ideally this actually gets split 
into a separate package and the compiler itself is AST in, AST out w no Babel 
dep. 

* HIR/ - the core HIRFunction, HIR and related data types, plus the HIR 
construction and printing, HIR visitors. 

* Inference/ - the core inference passes that operate on the HIR, including type 
inference, reference effects, alias analysis, mutable range analysis, etc. I'll 
let @gsathya  move these files when at a good stopping point. 

* ReactiveScopes/ - inference relating to reactive scopes, 
constructing/printing/codegenning ReactiveFunction 

* SSA/ - enter/leave SSA and eliminate redundant phis 

* Utils/ - every project needs a place to put stuff that doesn't fit into the 
other categories, this is ours. 

This leaves just index.ts at the top level, and overall feels pretty tidy. Not 
too tedious to figure out where anything goes, hopefully.
2022-12-16 17:25:11 -08:00
Joe Savona 6053703dbb add missing pass to playground
It's pretty tedious to keep the playground in sync w `Pipeline` — we need some 
abstraction so we can write the sequence of passes once and reuse it (while 
inspecting intermediate states).
2022-12-16 09:41:47 -08:00
Joe Savona 075cdcec9e [be] Move old compiler to separate package 2022-12-16 09:33:07 -08:00
Joe Savona 7a29405d68 [be] Rename some types for ReactiveFunction 2022-12-16 08:45:46 -08:00
Sathya Gunasekaran 7ab0303822 [playground] Increase min width of output tabs 2022-12-15 18:38:06 +00:00
Sathya Gunasekaran 25fb36c966 [playground] Make gutter smaller in the output tabs 2022-12-15 18:38:05 +00:00
Sathya Gunasekaran 9e9dba90eb [playground] Make input tab smaller 2022-12-15 18:38:05 +00:00
Sathya Gunasekaran 78d976302f [playground] Remove mobile support
This is mostly unused and there's just not enough benefit for now. We can re-add 
this if folks start using this site on mobile. Removing now to simplify the 
codebase.
2022-12-15 18:38:04 +00:00
Sathya Gunasekaran c4f16fce5c [playground] Remove handlebar to resize input
The only use of this is to resize the input tab immediately to be smaller. 
Instead, a follow on PR will just make the input tab smaller by default.
2022-12-15 18:38:03 +00:00
Joe Savona 6efac28677 Prune unnecessary labels in output 2022-12-14 16:42:54 -08:00
Joe Savona de3952352b Use new codegen in playground
Switches to the new codegen with memoization applied, and puts the JS tab first 
to make it easier to look at the input/output 

side by side.
2022-12-14 16:23:41 -08:00
Joe Savona 575db2b4ea [wip] Codegen with memoization applied
Updates the ReactiveFunction-based codegen from the previous PR to emit 
memoization code for each scope. This is currently naive and has some bugs, but 
it gets the idea across. The core logic is straightforward at this point, all 
the hard work is in earlier passes: 

* Compute one change variable per scope dependency, eg `const c_0 = $[0] === 
maxItems` 

* Generate one `let` binding for each scope output 

* Generate an if block where the test is if any of the change variables are true 
(`||` them together) 

* Generate the consequent block with the original code block, plus statements to 
save dependencies and outputs to their cache slots 

* Generate the alternate block to populate the scope outputs from their cached 
values 

## Todos 

A few things don't quite work yet: 

* Codegen is designed to avoid emitting variables for temporary values, but 
that's causing a few values to sort of disappear n the examples, or get emitted 
twice. There are a variety of ways to achieve this but we'll need to ensure that 
this category of values gets assigned to a variable and then reference the 
variable. This is more involved. 

* Scopes can end up with zero dependencies, in which case we should check that 
the first output cache is initialized. This one is more straightforward. 

* If there are early returns, we don't record that they occurred and replay them 
in the `else` branch for each scope. We know the algorithm though so i'm okay 
delaying that for now.
2022-12-14 16:12:36 -08:00
Joe Savona 70a7d5f41e Codegen from ReactiveFunction (no memoization yet)
Currently codegen operates from HIR using a tree visitor, but for scope 
construction we're converting the HIR (CFG) into a ReactiveFunction (AST-like). 
Our original idea for codegen was that we would convert the ReactiveFunction 
back to HIR, and then codegen from there. However, the ReactiveFunction is 
already in tree form...which makes it very straightforward to generate code 
from. 

So this PR implements codegen from ReactiveFunction. The output is _identical_ 
thanks in large part to reusing as much logic from Codegen.ts as possible. The 
next PR will add memoization logic.
2022-12-14 14:54:37 -08:00
Joe Savona 7199229cb4 Record scope outputs
While we're collecting scope dependencies, we have the exact right information 
to record scope outputs. These are variables that need to be defined outside of 
the scope and populated by recomputing (on change) or via the cached value (if 
no change).
2022-12-14 13:27:20 -08:00
Joe Savona 459263b763 Refactor reactive scope representation 2022-12-14 12:07:50 -08:00
Joe Savona 552fe1878b Cleanup previous dep collection 2022-12-14 08:42:39 -08:00
Joe Savona dd1ff4dc30 Combine dependency collection and propagation, more precise collection
I realized that properly propagating scope dependencies requires reusing the 
same logic as dependency collection itself: a dependency of an inner scope 
should only be propagated upward if the dependency was declared before the outer 
scope, for example. So this PR reimplements dependency collection in the 
propagation pass. 

At the same time I made a few other improvements: 

* Don't report dependencies that are "constant". This is a bit simplistic for 
now, we can use a more advanced analysis later. 

* Try to avoid creating duplicate dependencies. 

This addresses the todo from the previous PR (flattening scopes in loops) since 
now we don't need to compute deps until after that runs. As a follow-up i'll 
remove the existing dependency collection.
2022-12-14 08:42:39 -08:00
Joe Savona d909ae0434 Fix scope dependency collection ordering bug
Dependency collection has to visit the instruction id first before evaluating 
the instruction, in order to completely any scopes that would end at that 
instruction. Note the removed dependencies that don't appear within the scopes.
2022-12-14 08:42:38 -08:00
Joe Savona 972caf5bf1 Propagate scope dependencies upwards
Initial, partial implementation of dependency propagation. This lays the 
groundwork, #876 completes it after some other fixes this uncovered.
2022-12-14 08:42:37 -08:00
Joe Savona 5f3dc0b6c7 Flatten scopes within loops
We can't independently memoize values created within a loop, so this pass 
flattens scopes within loops. Right now this just flattens the scope away 
without propagating any dependency (or output) information, follow-ups will 
extend it to do that.
2022-12-14 08:42:37 -08:00
Joe Savona be9b6356df cleanup unnecessary code from BuildReactiveFunction 2022-12-14 08:42:36 -08:00
Joe Savona 189f48ee0a Dont create scopes for consts w primitive values
We don't need to create scopes for primitive values that are never reassigned. 
The actual rules are more complex — we could choose to skip creating scopes for 
values that don't allocate — but this simple heuristic is good for now.
2022-12-14 08:42:36 -08:00
Joe Savona 73ad571c6b Add invariant for problematic LeaveSSA case
The new LeaveSSA looks ahead to the phis of fallback blocks. However, HIR can 
sometimes have multiple blocks with the same fallthrough (totally fine), so this 
diff clears the phis of fallbacks as they are reached to avoid reprocessing 
them. This caused a previously incorrect case to now fail, yay.
2022-12-14 08:42:35 -08:00
Sathya Gunasekaran 4b66531237 [test] Remove mermaid diagrams from test output 2022-12-14 15:47:13 +00:00
Sathya Gunasekaran 9bfad00410 [type-infer] Implement Hindley Milner type inference
Based on the lambda calculus of ForgetScript, this infers the types of 
primitives, objects and functions.
2022-12-14 15:36:32 +00:00
Joseph Savona 6e52cc3f38 Construct nested reactive scopes
There are a bunch of ways we can go about converting from the input HIR into a 
final form that has the preamble inserted and memoized blocks of code wrapped 
with change detection and caching. This is just one way, it might not be the 
ideal way. In any case, this pass converts HIRFunction -> ReactiveFunction. The 
latter is a recursive (tree-shaped) data structure that attempts to represent 
blocks each of composed of scopes or instructions, where scopes are themselves 
composed of blocks etc. The idea is a) this makes it easy to visualize the 
structure and check that the scopes and their dependencies are correct and b) 
this is a really nice form for adding the memoization code. We can convert from 
a ReactiveFunction back to an HIRFunction, wrapping each scope in the 
appropriate if checks and caching. 

## Example 

Consider the following example, which has 2 main scopes: an outer one for `x` 
and an inner one in the consequent for `y`: 

```javascript 

function foo(a, b, c) { 

const x = []; 

if (a) { 

const y = []; 

y.push(b); 

x.push(<div>{y}</div>); 

} else { 

x.push(c); 

} 

return x; 

} 

``` 

## Output 

The new builder constructs a ReactiveFunction for this example along the lines 
of the following (note that inputs are always empty bc we don't collect those 
yet): 

``` 

{ 

scope @0 [1:11] inputs=[]  { 

[1] Const mutate x$11_@0[1:11] = Array [] 

[2] if (read a$8) { 

scope @1 [3:5] inputs=[] { 

[3] Const mutate y$12_@1[3:5] = Array [] 

[4] Call mutate y$12_@1.push(read b$9) 

} 

scope @2 [5:6] inputs=[] { 

[5] Const mutate $13_@2 = "div" 

} 

scope @3 [6:7] inputs=[] { 

[6] Const mutate $14_@3 = JSX <read $13_@2>{freeze y$12_@1}</read $13_@2> 

} 

[7] Call mutate x$11_@0.push(read $14_@3) 

} else { 

[9] Call mutate x$11_@0.push(read c$10) 

} 

} 

[10] return x$11; 

} 

``` 

This shows the hierarchy: there's an outer scope, `@0` to compute `x` (the first 
scope), then within the if consequent there's another scope, `@1`, to compute 
`y`. We have some technically extraneous scopes to compute the JSX element; that 
can be cleaned up with a bit more refinement. 

With this structure — and the inputs and outputs of each scope filled in — we 
can convert to code in a straightforward manner. Each scope turns into a block 
along the lines of the following: 

(note here we use strings to index the cache, in reality these would be ints) 

```javascript 

// one change variable pet input: 

let c_a = a !== $['a']; 

... 

// one variable for each output: 

let x; 

... 

// if (changed) { recompute } else { use-cache } 

if (c_a || ... ) { 

x = ...; 

// one assignment per output 

$['x'] = x; 

// update cache per input 

$['a'] = a; 

... 

} else { 

// one assignment per output 

x = $['x']; 

... 

} 

```
2022-12-13 11:53:42 -08:00
Sathya Gunasekaran 2df0c832f5 [be] Remove stale TODO
Fixed in fa1ae5de5be84dd25263845fb43157d77c0826aa
2022-12-13 09:28:00 +00:00
Sathya Gunasekaran 74599a92a2 [hir] Update Place.type based on value inference 2022-12-13 09:17:19 +00:00
Sathya Gunasekaran 7f8ad49165 [hir] Split value tracking into a separate pass 2022-12-13 09:17:19 +00:00
Joe Savona 5aa4143e24 TreeVisitor distinguish blocks from statements
TreeVisitor didn't distinguish between the type of a block and the type of an 
item that can occur within a block - this was fine for Codegen which can use 
`t.Statement` for both of those values. However, the upcoming scope construction 
needs to distinguish instructions in a block from a block itself, so this PR 
adds a new type parameter.
2022-12-12 16:46:55 -08:00
Joseph Savona 35ba4149ec Support assignment in for statement's update clause
Per the title, this PR adds support for assignment expressions in update 
clauses. This was mostly fixed by the previous diff to improve value block 
handling, and there's only a bit more to do here to allow a "value block" that 
doesn't produce a value (we need a better name).
2022-12-12 15:40:46 -08:00
Sathya Gunasekaran b8cf85d77d [hir] Add Type to Place 2022-12-12 21:10:50 +00:00
Sathya Gunasekaran e2e5e389af [be] Move buildAliasSets to DisjointSet 2022-12-12 20:35:23 +00:00
Sathya Gunasekaran 3c976a24b3 [hir] Run mutable range analysis for aliases to fix point 2022-12-12 20:35:23 +00:00