Commit Graph
202 Commits
Author SHA1 Message Date
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
Joseph Savona b8d78a94a8 Improve "value block" handling
This is a pre-req to construct reactive scopes in #857. "Value blocks" such as 
`for` init/test/update and `while` test need to be consistently wrapped in 
enter/leave calls so that we can extend the range of values properly. We also 
need to handle `for` init blocks a bit differently, since they allow variable 
declarations but not other types of statements. 

This PR ensures that we use consistent methods for handling value blocks (`for` 
test/update and `while` test) and treats `for` init as a new type with its own 
enter/append/leave visitor functions.
2022-12-12 11:20:10 -08:00
Lauren Tan c8bb9ea0af Move pretty printing Scopes into PrintHIR 2022-12-09 17:12:45 -05:00
Jan Kassens 255700e2cd Increase precision in InferMutableRangesForAlias
Previously, this step just set the mutable range of any alias set including any 

mutation to the end of the last mutable range of any of the containing 
identifiers. 

This change makes it so that the ends are only updated of the ranges that end 

before the last mutation. 

Fixes #852
2022-12-09 13:10:01 -05:00
Joseph Savona 748992d508 LeaveSSA: handle phi as operand to subsequent phi
Fixes https://github.com/facebook/react-forget/pull/858#discussion_r1044626137. 
The case is 

```javascript 

function foo() { 

let x$1 = 1; 

let y = 2; 

if (y === 2) { 

x$2 = 3; 

} 

x$3 = phi(x$1, x$2) 

if (y === 3) { 

x4 = 5; 

} 

x$5 = phi(x$3, x$4); 

y = x$5; 

} 

``` 

What happens here is that there are two _sequential_ phis for `x`. Previously 
when we encountered the second phi we would find that there is no `let` 
declaration for the phi or any of its operands, and create a new one before the 
second `if`. That's incorrect, these should all merge into a single `x` 
declaration. We now look up the phi operands to see if they are part of a 
previous phi, and merge them correctly.
2022-12-09 09:47:47 -08:00
Joseph Savona d03c5bc0a0 Run LeaveSSA prior to analyzing reactive scopes
Reorders LeaveSSA so that it runs before we begin evaluating reactive scopes. 
Note that reactive scopes must span the full construction of each variable — for 
variables with a phi, this must span the declaration and all assignments of the 
phi operands. And that's exactly what the new LeaveSSA does! LeaveSSA removes 
phi nodes and ensure that all versions of a variable which flow into a phi have 
been assigned a single canonical identifier (with an appropriate mutable range). 

This PR includes this and some related changes: 

* Reorders the pass 

* Changes hir-test to print the final HIR, eg just prior to codegen 

* Teaches LeaveSSA to update the mutable range of the canonical identifiers it 
assigns, based on the min/max of the variables assigned.
2022-12-09 07:39:28 -08:00
Sathya Gunasekaran daaf95854c [hir] Don't alias fields unless a mutation occurs after aliasing
A wise Joe once said, "We only care about observed aliasing". 

Instead of performing aliasing for fields and non fields together, split the 
analysis to happen over separate passes. Similarly split inferring mutable 
lifetimes pass for fields and non fields. 

Now, we can identify the aliases of fields that are *not* mutated and only alias 
the ones that do mutate. 

The algorithm is roughly as follows: 

1. Build the set of aliases for non fields 

2. Infer mutable ranges for all instructions except aliasing fields 

3. Infer mutable ranges for all aliased instructions based on the alias set 
calculated in step 1 (this doesn't include aliasing fields) 

4. Extend the set of aliases (calculated in step 1)  to include fields only if 
the field or the receiver is mutated after the aliasing, ie, if _mutability is 
observed_. 

5. Run infer mutable ranges again for all instructions including the fields that 
were aliased in the previous step. 

6. Run infer mutable ranges for all aliased instructions including the fields 
that were aliased.
2022-12-08 23:29:41 +00:00
Joseph Savona 21652a135b Improved LeaveSSA pass
## Problem 

The previous version of LeaveSSA used a very simple approach in which 
identifiers stored their pre-ssa id, and LeaveSSA restored this id back. The 
upside of this approach is that it's very simple and trivially correct (assuming 
no reordering of code). The downside is that after running LeaveSSA we lose all 
information about which versions of variable declarations are distinct, and 
which might merge together in a phi. That information is really useful for scope 
analysis! Consider this input (variables are numbered as they would be in SSA 
form): 

```javascript 

function foo(a, b, c) { 

let x$1 = null; 

if (a) { 

x$2 = b; 

} else { 

x$3 = c; 

} 

x$4 = phi(x$2, x$3); 

return x$4; 

``` 

The current LeaveSSA assigns all 4 variables back to `x$1`, with a single let 
declaration at `let x$1 = null`. However, from a reactive scopes perspective, 
there are really just 2 versions of x: the initial x$1 (defined and never used) 
and then x$2, x$3, and x$4, which have to be merged into a single scope because 
they are part of a phi. In other words, we can't independently compute x$2, x$3, 
or x$4 - if any of their inputs changes, we have to redo all the computation. 
However, the existing structure makes it difficult to figure out the correct 
starting point for this scope — there is no initial `let` declaration that we 
can refer to. 

Instead, we can represent the program as follows after LeaveSSA, and then use 
this form for scope analysis: 

```javascript 

function foo(a, b, c) { 

const x$1 = null; // NOTE: rewritten to const 

let x$2; // synthesized declaration to allow later reassignment 

if (a) { 

x$2 = b; 

} else { 

x$2 = c; 

} 

return x$2; 

``` 

Note that there are only 2 versions of x, and we have synthesized a variable 
declaration for x$2 at the appropriate scope. Our scope analysis can then 
determine that the range of x$2 is from the declaration to the end of the if. 

## Approach 

This pass does two main rewrites: 

* For variables that do *not* appear as a phi id or operand, it rewrites the 
declaration to be `const`. You can see this above for x$1. 

* For variables that *do* appear as a phi or operand, it synthesizes a new `let` 
binding at the appropriate scope (ie, in the appropriate block), and updates all 
other operands from the phi to use the same id for the variable.  You can see 
this above for x$2, x$3, and x$4. 

Note that the let binding is generated at the narrowest scope possible. In this 
example, we generate distinct let bindings for the other if and else branches: 

```javascript 

function foo(a, b, c) { 

let x = null; 

if (a) { 

// we generate a `let x$2` here 

if (b) { 

x = 0; // becomes x$2 

} else { 

x = 1;  // becomes x$2 

} 

x // becomes x$2 

} else { 

// we generate a `let x$3` here 

if (c) { 

x = 2; // becomes x$3 

} else { 

x = 3; // becomes x$3 

} 

x; // becomes x$3 

} 

} 

``` 

Because the different x values from the outer if/else can never join in a phi, 
we can treat them as independent variables and (re)compute them independently. 

The algorithm works by iterating in reverse-postorder, and looking ahead at 
fallthrough blocks to find phi nodes that may need a let declaration (see above 
example of where these are generated). It also tracks variables which _don't_ 
participate in a phi so that it can rewrite their declarations to `const`. 

## TODO 

This PR does *not* yet work for cases where there is unconditional assignment 
within a `while` test condition. That would technically create a distinct 
version of the variable that shadows the value for the loop, and you can't have 
variable declarations in a while test condition. 

That case already doesn't work, though, so i'm punting on it for now until we 
figure out a bit more around 

"value" blocks. We have some good options, like desugaring to a `for(;;)` and 
manually implementing the while semantics in that case.
2022-12-08 07:35:14 -08:00
Jan Kassens 4837e21de3 [HIR] add for terminal
The changes here are pretty significant and there's a bunch more left: 

- support for with any of `<init>`, `<test>` or `<update>` empty. - support for 
with `<init>` as `Expression` instead of VariableDeclaration` node - support 
assignment expressions in `<update>`, this seems like it might require further 
new abstractions to allow something like a block to codegen into a single 
expression.
2022-12-06 12:01:32 -05:00
Jan Kassens db740366f8 "ValueBlock" for while test node
The instructions of a while test node cannot just be pushed to the previous 
block. This creates a new block for the test node and then during code gen 
converts the statements pushed to the "value block" into expressions.
2022-12-06 11:28:15 -05:00
Sathya Gunasekaran 3a1124fe14 [hir] Add alias analysis for lvalue aggregates
This adds a field sensitive, flow-insensitive, context-insensitive alias 
analysis for lvalue aggregates. 

In the future, InferMutableLifetimes can refine it's analysis using these field 
sensitive alias sets.
2022-12-05 22:27:01 +00:00
Sathya Gunasekaran a855e76494 [hir] Introduce AliasSet type
Starting to get wonky typing out the entire type. This typedef is better for 
typing and readability.
2022-12-05 22:27:01 +00:00
Sathya Gunasekaran 2dab6f6c4d [hir] Store Primitives in AbstractState 2022-12-05 22:27:00 +00:00
Sathya Gunasekaran 9da7ad557d [hir] Introduce AbstractState.store
Currently AbstractState.alias performs three operations: - Reading a value - 
Storing the value - Updating aliasing (in the DisjointSet) 

This commit makes AbstractState.alias only responsible for updating the alias 
information. The rest of the operations are split into separate functions.
2022-12-05 22:27:00 +00:00
Sathya Gunasekaran 1c6e39a1d6 [hir] Introduce AbstractState.read to lookup value
Split AbstractState.alias into two separate operations for better readability.
2022-12-05 22:26:59 +00:00
Sathya Gunasekaran 7bdd0f03cb [hir] Perform alias analysis for aggregate rvalues
Maintain and use abstract memory to peform more refined aliasing of member 
expressions.
2022-12-01 19:03:50 +00:00
Sathya Gunasekaran 00f74d4e70 [hir] Correctly identify mutated identifiers
mutableRange.end is exclusive so account for this extra 1 instruction.
2022-12-01 19:03:47 +00:00
Sathya Gunasekaran c96c9b458f [hir] Add a buildAliasSets pass
Moves the existing alias set building logic from inferMutableLifetimes to a 
separate pass. 

Additionally maintain abstract state to refine aliasing to not include 
primitives.
2022-12-01 19:03:44 +00:00
Joseph Savona 4b9c2f36fe add missing reactive scope passes to playground
I forget to add these passes to the playground, oops.
2022-12-01 11:02:27 -08:00
Jan Kassens df58546f4e Codegen TValue are just Expressions (#839) 2022-12-01 12:56:45 -05:00
Joe Savona 509aa9f0e5 HIRVisitor - change labels to block ids
HIRTreeVisitor previously passed a string label (for certain blocks). This 
changes to pass the raw BlockId, and have codegen convert that to a string. I'm 
not sure if we'll need this but it would be helpful for eg visiting the IR and 
emitting a new IR, while mapping block ids forward. Even if we don't need that 
it makes sense for Codegen to decide how to convert a block id into a label 
(which has to obey the rules of an identifier, not the visitor's concern).
2022-11-30 10:24:51 -08:00
Jan Kassens aa2b152ea8 Fixture tests for 2 cases of expressions that produce incorrect output
They're fairly related, but I figured it's worth keeping more examples. 

- For the `while` example we need to codegen into a single expression. - For the 
expression with contained assignment we need to either keep the SSA ids around 
or re-create a similar expression during codegen.
2022-11-29 18:33:00 -05:00
Lauren TanandJoe Savona <joesavonafb.com> b3a26f1de2 Infer reactive scope dependencies
This builds upon @josephsavona's prior PR #817 to add support for inferring 
reactive scope dependencies for all instructions and terminals. 

Still TODO (probably in follow up PRs): 

- [ ] fix duplicate/different identity scope issue (see this [test 
fixture](https://github.com/facebook/react-forget/blob/5f3b260aaa3e936fb013716df3ef4e7676b7e9ea/forget/src/__tests__/fixtures/hir/overlapping-scopes-while.expect.md)) 
- [ ] also collect outputs of each scope - [ ] add new tree visitor that only 
visits, consider renaming the current `visitTree` to `mapTree` or similar 

Co-authored-by: Joe Savona <joesavonafb.com>
2022-11-29 17:07:55 -05:00
Lauren Tan ccca2d38cf [ez] Delete some old files 2022-11-29 15:23:56 -05:00
Lauren Tan 179a7f9605 Fix output tabs growing infinitely in height
The `automaticLayout` config option for Monaco causes it to remeasure itself, 
and the parent container's height being longer than the screen causes it to 
constantly grow infinitely. You can observe this bug by going to the playground, 
expanding any tab, and watch the scrollbar grow as the editor quickly grows to 
ridiculous heights and your laptop starts glowing red hot and̶͙̕ H̴͉͘e comes 
t̵͙́o ̴̜̿de̷̼̚s̷̻̍ec̶̮͒rate all knowled̵̥̆ge
2022-11-29 11:07:03 -05:00