Commit Graph

187 Commits

Author SHA1 Message Date
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 Tan 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
Jan Kassens 9f7878c7c0 [easy] remove isFallthrough arg from mapTerminalSuccessors
The argument was unused and a confusing boolean argument that's easy to mix up. 
Suggesting to remove it until we see a need for it at which point we might want 
to introduce an enum to make the argument more obvious.
2022-11-28 17:48:59 -05:00
Jan Kassens 0bbb36e1a4 [codegen] more locations: binary, return, call (#820) 2022-11-28 16:54:46 -05:00
jacdebug b932ce19a7 [playground] TabbedWindowItem lifting tabs state up
- List local state in TabbedWindowItem to Editor/index
2022-11-25 09:00:30 +00:00
Sathya Gunasekaran 9ae698c734 [playground] Add tab to display output from old architecture 2022-11-24 18:04:16 +00:00
Sathya Gunasekaran 76a4bd67f4 [playground] Remove unused kind parameter 2022-11-24 17:56:49 +00:00
jacdebug c203d07d8c [Playground] vertical tabs to see multiple stages same time
# Add vertical tabs to see multiple stages same time 

<img width="781" alt="image" 
src="https://user-images.githubusercontent.com/430289/203837762-ccf37491-2006-4512-8014-0b7893f3396f.png">
2022-11-24 17:25:13 +00:00
Sathya Gunasekaran d958d4b73e [playground] Remove line number from output
We already have instruction numbers as part of the output so line numbers just add extra visual noise.
2022-11-24 16:04:27 +00:00
Sathya Gunasekaran 06ca8b1026 [playground] Refactor compilation of HIR
Stack created with [Sapling](https://sapling-scm.com). Best reviewed with 
[ReviewStack](https://reviewstack.dev/facebook/react-forget/pull/825). * __->__ 
#825 

[playground] Refactor compilation of HIR
2022-11-24 15:28:38 +00:00
Sathya Gunasekaran e2cfbeb3d3 [playground] Display only new HIR
Stack created with [Sapling](https://sapling-scm.com). Best reviewed with 
[ReviewStack](https://reviewstack.dev/facebook/react-forget/pull/824). * #825 * 
__->__ #824 

[playground] Display only new HIR
2022-11-24 15:24:40 +00:00
Jan Kassens da6d84049e easy: name InstructionKind enum values (#818) 2022-11-23 10:48:53 -05:00
Joe Savona e51553f5da InferReactiveScopes considers all operands, incl terminals 2022-11-22 14:10:06 -08:00
Joe Savona a0054a1837 Visit terminal ids before terminal branches
This is a follow-up to merging ranges. I realized that we need to mark terminal 
ids as visited _before_ processing the branches of that terminal (whereas before 
we were marking terminal ids only _after_ processing the branches). That exposed 
another bug where interleaving could fail to be detected (with an error, 
thankfully) if one of the branches had completed already. 

Our small test suite is already really good!
2022-11-22 12:10:26 -08:00
Joe Savona d2e5c31fc6 Test case showing reassignment block scoping problem
This demonstrates a situation we don't handle well today. The basic structure is 
that you have some variable defined at the top level, then some control flow 
like if/switch where _all_ branches reassign the variable, then some code after 
that references the resulting phi node: 

```javascript 

let x1; 

// ... mutate/read x1 

if (cond) { 

x2 = {}; 

} else { 

x3 = {}; 

} 

x4 = phi(x2, x3); 

``` 

We currently group x3, x3, and x4 into a scope together, but note that...there's 
no `let` declaration for any of those! This means that it looks like the scope 
for x2 and x3 start in the consequent/alternate, but the true scope spans from 
before-after the if. I'm inclined to say that LeaveSSA should run _before_ scope 
analysis, and produce something like the following in this case: 

```javascript 

let x1; 

// ...mutate/read x1 

let x2; // new variable declaration for the new version of x 

if (cond) { 

x2 = {}; 

} else { 

x2 = {}; 

} 

x2; 

``` 

This then allows us to construct a correct range for x2, which starts in the 
other block.
2022-11-22 10:42:36 -08:00
Joe Savona 4a3640f25c Merge scopes that interleave or cross control-flow boundaries together
- [x] Merge scopes that are interleaved 

- [x] Merge scopes if they both cross control-flow boundaries together 

- [x] Don't merge scopes that strictly shadow 

Still WIP because I want to double-check and see if i can find a simpler 
algorithm for this. But it works.
2022-11-22 10:42:33 -08:00
Sathya Gunasekaran d9c6d61192 [test] Print HIR before leaving SSA
Stack created with [Sapling](https://sapling-scm.com). Best reviewed with 
[ReviewStack](https://reviewstack.dev/facebook/react-forget/pull/812). * __->__ 
#812 

[test] Print HIR before leaving SSA Looking at the HIR before it leaves SSA 
helps debugging better.
2022-11-22 17:59:42 +00:00