Update on "[compiler][wip] Inline single return JSX for known function components"

See comments, still WIP bc i'm getting a weird error with Babel and line numbers, one of the source locations must be off.

[ghstack-poisoned]
This commit is contained in:
Joe Savona
2024-09-09 17:11:14 -07:00
parent 2461c490ad
commit 80b1ef5827
10 changed files with 588 additions and 145 deletions
@@ -224,11 +224,6 @@ function* runWithEnvironment(
yield log({kind: 'hir', name: 'InstructionReordering', value: hir});
}
if (env.config.enableInlineSingleReturnJSX) {
inlineSingleReturnJSX(hir);
yield log({kind: 'hir', name: 'InlineSingleReturnJSX', value: hir});
}
pruneMaybeThrows(hir);
yield log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
@@ -332,6 +327,11 @@ function* runWithEnvironment(
assertValidBlockNesting(hir);
if (env.config.enableInlineSingleReturnJSX) {
inlineSingleReturnJSX(hir);
yield log({kind: 'hir', name: 'InlineSingleReturnJSX', value: hir});
}
flattenReactiveLoopsHIR(hir);
yield log({
kind: 'hir',
@@ -176,6 +176,12 @@ export function buildReactiveScopeTerminalsHIR(fn: HIRFunction): void {
* Step 5:
* Fix scope and identifier ranges to account for renumbered instructions
*/
updateScopeRangesAfterRenumberingInstructions(fn);
}
export function updateScopeRangesAfterRenumberingInstructions(
fn: HIRFunction,
): void {
for (const [, block] of fn.body.blocks) {
const terminal = block.terminal;
if (terminal.kind === 'scope' || terminal.kind === 'pruned-scope') {
@@ -5,73 +5,114 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '..';
import {
BlockId,
Effect,
HIRFunction,
Instruction,
isFunctionComponentType,
makeInstructionId,
ObjectProperty,
Place,
ReactiveScope,
SpreadPattern,
} from '../HIR';
import {updateScopeRangesAfterRenumberingInstructions} from '../HIR/BuildReactiveScopeTerminalsHIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {BuiltInPropsId} from '../HIR/ObjectShape';
import {retainWhere} from '../Utils/utils';
export function inlineSingleReturnJSX(fn: HIRFunction): void {
if (fn.fnType !== 'Component') {
// This optimization only applies to function components
return;
}
const returnValues: Array<{block: BlockId; place: Place}> = [];
const returnValues: Array<Place> = [];
for (const [, block] of fn.body.blocks) {
if (block.terminal.kind === 'return') {
returnValues.push({
block: block.id,
place: block.terminal.value,
});
returnValues.push(block.terminal.value);
}
}
if (returnValues.length !== 1) {
// This optimization only applies if the component always returns
// the same element, multiple returns are rarely the same element.
// Note that in theory you could return the same tag+props+children
// in two different places, but that's pretty strange and not worth
// optimizing for.
/*
* This optimization only applies if the component always returns
* the same element, multiple returns are rarely the same element.
* Note that in theory you could return the same tag+props+children
* in two different places, but that's pretty strange and not worth
* optimizing for.
*/
return;
}
const returnValue = returnValues[0]!;
const returnBlock = fn.body.blocks.get(returnValue.block)!;
let nextInstructions: Array<Instruction> | null = null;
for (let i = 0; i < returnBlock.instructions.length; i++) {
const instr = returnBlock.instructions[i]!;
if (instr.lvalue.identifier.id !== returnValue.place.identifier.id) {
if (nextInstructions !== null) {
nextInstructions.push(instr);
const scopes: Array<{start: BlockId; end: BlockId; scope: ReactiveScope}> =
[];
for (const [, block] of fn.body.blocks) {
retainWhere(scopes, scope => scope.end !== block.id);
let nextInstructions: Array<Instruction> | null = null;
for (let i = 0; i < block.instructions.length; i++) {
const instr = block.instructions[i]!;
if (instr.lvalue.identifier.id !== returnValue.identifier.id) {
if (nextInstructions !== null) {
nextInstructions.push(instr);
}
continue;
}
continue;
}
const value = instr.value;
const value = instr.value;
if (
value.kind !== 'JsxExpression' ||
value.tag.kind !== 'Identifier' ||
value.tag.reactive
// || isFunctionComponentType(value.tag.identifier)
) {
return;
}
if (
value.kind !== 'JsxExpression' ||
value.tag.kind !== 'Identifier' ||
value.tag.reactive ||
value.props.some(
prop =>
prop.kind === 'JsxAttribute' &&
prop.name === 'key' &&
prop.place.reactive,
) ||
scopes.length !== 1
/*
* ||
* scopes[0].scope.reassignments.size !== 0 ||
* scopes[0].scope.declarations.size !== 1 ||
* !Iterable_some(
* scopes[0].scope.declarations.values(),
* decl => decl.identifier.id === returnValue.identifier.id,
* ) ||
* !isFunctionComponentType(value.tag.identifier)
*/
) {
return;
}
nextInstructions ??= returnBlock.instructions.slice(0, i);
const propsTemp = createTemporaryPlace(fn.env, value.loc);
propsTemp.effect = Effect.Freeze;
propsTemp.identifier.type = {kind: 'Object', shapeId: BuiltInPropsId};
const scopeStart = fn.body.blocks.get(scopes[0].start)!;
CompilerError.invariant(
scopeStart.terminal.kind === 'scope' ||
scopeStart.terminal.kind === 'pruned-scope',
{
reason: 'wip',
loc: scopeStart.terminal.loc,
},
);
scopeStart.terminal = {
kind: 'label',
block: scopeStart.terminal.block,
fallthrough: scopeStart.terminal.fallthrough,
id: scopeStart.terminal.id,
loc: scopeStart.terminal.loc,
};
const properties: Array<ObjectProperty | SpreadPattern> = value.props.map(
attr => {
nextInstructions ??= block.instructions.slice(0, i);
const propsTemp = createTemporaryPlace(fn.env, value.loc);
propsTemp.effect = Effect.Freeze;
propsTemp.identifier.type = {kind: 'Object', shapeId: BuiltInPropsId};
const properties: Array<ObjectProperty | SpreadPattern> = [];
value.props.forEach(attr => {
if (attr.kind === 'JsxAttribute') {
return {
if (attr.name === 'key') {
return;
}
properties.push({
kind: 'ObjectProperty',
key: {
kind: 'identifier',
@@ -79,76 +120,87 @@ export function inlineSingleReturnJSX(fn: HIRFunction): void {
},
place: attr.place,
type: 'property',
} satisfies ObjectProperty;
} satisfies ObjectProperty);
} else {
return {
properties.push({
kind: 'Spread',
place: attr.argument,
} satisfies SpreadPattern;
} satisfies SpreadPattern);
}
},
);
if (value.children !== null) {
if (value.children.length === 1) {
properties.push({
kind: 'ObjectProperty',
key: {
kind: 'identifier',
name: 'children',
},
place: {...value.children[0]!},
type: 'property',
});
} else {
const childrenTemp = createTemporaryPlace(fn.env, value.loc);
childrenTemp.effect = Effect.Freeze;
nextInstructions.push({
id: makeInstructionId(0),
lvalue: {...childrenTemp},
value: {
kind: 'ArrayExpression',
elements: value.children,
});
if (value.children !== null) {
if (value.children.length === 1) {
properties.push({
kind: 'ObjectProperty',
key: {
kind: 'identifier',
name: 'children',
},
place: {...value.children[0]!},
type: 'property',
});
} else {
const childrenTemp = createTemporaryPlace(fn.env, value.loc);
childrenTemp.effect = Effect.Freeze;
nextInstructions.push({
id: makeInstructionId(0),
lvalue: {...childrenTemp},
value: {
kind: 'ArrayExpression',
elements: value.children,
loc: value.loc,
},
loc: value.loc,
},
loc: value.loc,
});
properties.push({
kind: 'ObjectProperty',
key: {
kind: 'identifier',
name: 'children',
},
place: {...childrenTemp},
type: 'property',
});
});
properties.push({
kind: 'ObjectProperty',
key: {
kind: 'identifier',
name: 'children',
},
place: {...childrenTemp},
type: 'property',
});
}
}
}
nextInstructions.push({
id: makeInstructionId(0),
lvalue: {...propsTemp},
value: {
kind: 'ObjectExpression',
properties,
nextInstructions.push({
id: makeInstructionId(0),
lvalue: {...propsTemp},
value: {
kind: 'ObjectExpression',
properties,
loc: value.loc,
},
loc: value.loc,
},
loc: value.loc,
});
nextInstructions.push({
id: makeInstructionId(0),
lvalue: instr.lvalue,
value: {
kind: 'CallExpression',
callee: {...value.tag},
args: [{...propsTemp}],
});
nextInstructions.push({
id: makeInstructionId(0),
lvalue: instr.lvalue,
value: {
kind: 'CallExpression',
callee: {...value.tag},
args: [{...propsTemp}],
loc: value.loc,
},
loc: value.loc,
},
loc: value.loc,
});
}
if (nextInstructions !== null) {
returnBlock.instructions = nextInstructions;
});
}
if (nextInstructions !== null) {
block.instructions = nextInstructions;
}
if (
block.terminal.kind === 'scope' ||
block.terminal.kind === 'pruned-scope'
) {
scopes.push({
start: block.id,
end: block.terminal.fallthrough,
scope: block.terminal.scope,
});
}
}
markInstructionIds(fn.body);
updateScopeRangesAfterRenumberingInstructions(fn);
}
@@ -231,8 +231,10 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
case 'ObjectExpression': {
const type = instruction.lvalue.identifier.type;
if (type.kind === 'Object' && type.shapeId === BuiltInPropsId) {
// If this is an object literal for inlined JSX, we don't need to memoize
// it. The props object is always assumed to have changed.
/*
* If this is an object literal for inlined JSX, we don't need to memoize
* it. The props object is always assumed to have changed.
*/
return false;
}
return true;
@@ -3,15 +3,9 @@
```javascript
// @enableInlineSingleReturnJSX
function Component({a, b}) {
return (
<Child value={a}>
<div>{b}</div>
</Child>
);
}
function Child({value, children}) {
'use no forget';
return (
<div>
<span>{value}</span>
@@ -20,6 +14,14 @@ function Child({value, children}) {
);
}
function Component({a, b}) {
return (
<Child value={a}>
<div>{b}</div>
</Child>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
@@ -39,6 +41,17 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableInlineSingleReturnJSX
function Child({ value, children }) {
"use no forget";
return (
<div>
<span>{value}</span>
{children}
</div>
);
}
function Component(t0) {
const $ = _c(2);
const { a, b } = t0;
@@ -53,34 +66,6 @@ function Component(t0) {
return Child({ value: a, children: t1 });
}
function Child(t0) {
const $ = _c(5);
const { value, children } = t0;
let t1;
if ($[0] !== value) {
t1 = <span>{value}</span>;
$[0] = value;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== t1 || $[3] !== children) {
t2 = (
<div>
{t1}
{children}
</div>
);
$[2] = t1;
$[3] = children;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
@@ -1,13 +1,7 @@
// @enableInlineSingleReturnJSX
function Component({a, b}) {
return (
<Child value={a}>
<div>{b}</div>
</Child>
);
}
function Child({value, children}) {
'use no forget';
return (
<div>
<span>{value}</span>
@@ -16,6 +10,14 @@ function Child({value, children}) {
);
}
function Component({a, b}) {
return (
<Child value={a}>
<div>{b}</div>
</Child>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
@@ -0,0 +1,138 @@
## Input
```javascript
// @enableInlineSingleReturnJSX
import {useEffect, useState} from 'react';
function Component({a, b}) {
return <Child key={b} value={a} />;
}
function Child({value, children}) {
const [state, setState] = useState(value);
useEffect(() => {
if (state === 0 && value === 0) {
setState(1);
}
}, [state]);
return (
<div>
{state}
<span>{value}</span>
{children}
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 0, b: 1},
{a: 0, b: 0},
{a: 1, b: 1},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableInlineSingleReturnJSX
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(3);
const { a, b } = t0;
let t1;
if ($[0] !== b || $[1] !== a) {
t1 = <Child key={b} value={a} />;
$[0] = b;
$[1] = a;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
function Child(t0) {
const $ = _c(11);
const { value, children } = t0;
const [state, setState] = useState(value);
let t1;
if ($[0] !== state || $[1] !== value) {
t1 = () => {
if (state === 0 && value === 0) {
setState(1);
}
};
$[0] = state;
$[1] = value;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== state) {
t2 = [state];
$[3] = state;
$[4] = t2;
} else {
t2 = $[4];
}
useEffect(t1, t2);
let t3;
if ($[5] !== value) {
t3 = <span>{value}</span>;
$[5] = value;
$[6] = t3;
} else {
t3 = $[6];
}
let t4;
if ($[7] !== state || $[8] !== t3 || $[9] !== children) {
t4 = (
<div>
{state}
{t3}
{children}
</div>
);
$[7] = state;
$[8] = t3;
$[9] = children;
$[10] = t4;
} else {
t4 = $[10];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 1, b: 0 },
{ a: 1, b: 1 },
{ a: 0, b: 1 },
{ a: 0, b: 0 },
{ a: 1, b: 1 },
],
};
```
### Eval output
(kind: ok) <div>1<span>0</span></div>
<div>1<span>1</span></div>
<div>1<span>1</span></div>
<div>1<span>0</span></div>
<div>1<span>0</span></div>
<div>1<span>1</span></div>
@@ -0,0 +1,35 @@
// @enableInlineSingleReturnJSX
import {useEffect, useState} from 'react';
function Component({a, b}) {
return <Child key={b} value={a} />;
}
function Child({value, children}) {
const [state, setState] = useState(value);
useEffect(() => {
if (state === 0 && value === 0) {
setState(1);
}
}, [state]);
return (
<div>
{state}
<span>{value}</span>
{children}
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 0, b: 1},
{a: 0, b: 0},
{a: 1, b: 1},
],
};
@@ -0,0 +1,173 @@
## Input
```javascript
// @enableInlineSingleReturnJSX
import {useEffect, useState} from 'react';
function Component({a, b}) {
let Tag = a === 0 ? Child1 : Child2;
return (
<Tag value={a}>
<div>{b}</div>
</Tag>
);
}
function Child1(props) {
'use no forget';
return <Child {...props} />;
}
function Child2(props) {
'use no forget';
return <Child {...props} />;
}
function Child({value, children}) {
const [state, setState] = useState(value);
useEffect(() => {
if (state === 0 && value === 0) {
setState(1);
}
}, [state]);
return (
<div>
{state}
<span>{value}</span>
{children}
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 0, b: 1},
{a: 0, b: 0},
{a: 1, b: 1},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableInlineSingleReturnJSX
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(6);
const { a, b } = t0;
const Tag = a === 0 ? Child1 : Child2;
let t1;
if ($[0] !== b) {
t1 = <div>{b}</div>;
$[0] = b;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== Tag || $[3] !== a || $[4] !== t1) {
t2 = <Tag value={a}>{t1}</Tag>;
$[2] = Tag;
$[3] = a;
$[4] = t1;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
}
function Child1(props) {
"use no forget";
return <Child {...props} />;
}
function Child2(props) {
"use no forget";
return <Child {...props} />;
}
function Child(t0) {
const $ = _c(11);
const { value, children } = t0;
const [state, setState] = useState(value);
let t1;
if ($[0] !== state || $[1] !== value) {
t1 = () => {
if (state === 0 && value === 0) {
setState(1);
}
};
$[0] = state;
$[1] = value;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== state) {
t2 = [state];
$[3] = state;
$[4] = t2;
} else {
t2 = $[4];
}
useEffect(t1, t2);
let t3;
if ($[5] !== value) {
t3 = <span>{value}</span>;
$[5] = value;
$[6] = t3;
} else {
t3 = $[6];
}
let t4;
if ($[7] !== state || $[8] !== t3 || $[9] !== children) {
t4 = (
<div>
{state}
{t3}
{children}
</div>
);
$[7] = state;
$[8] = t3;
$[9] = children;
$[10] = t4;
} else {
t4 = $[10];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 0 }],
sequentialRenders: [
{ a: 0, b: 0 },
{ a: 1, b: 0 },
{ a: 1, b: 1 },
{ a: 0, b: 1 },
{ a: 0, b: 0 },
{ a: 1, b: 1 },
],
};
```
### Eval output
(kind: ok) <div>1<span>0</span><div>0</div></div>
<div>1<span>1</span><div>0</div></div>
<div>1<span>1</span><div>1</div></div>
<div>1<span>0</span><div>1</div></div>
<div>1<span>0</span><div>0</div></div>
<div>1<span>1</span><div>1</div></div>
@@ -0,0 +1,50 @@
// @enableInlineSingleReturnJSX
import {useEffect, useState} from 'react';
function Component({a, b}) {
let Tag = a === 0 ? Child1 : Child2;
return (
<Tag value={a}>
<div>{b}</div>
</Tag>
);
}
function Child1(props) {
'use no forget';
return <Child {...props} />;
}
function Child2(props) {
'use no forget';
return <Child {...props} />;
}
function Child({value, children}) {
const [state, setState] = useState(value);
useEffect(() => {
if (state === 0 && value === 0) {
setState(1);
}
}, [state]);
return (
<div>
{state}
<span>{value}</span>
{children}
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 0}],
sequentialRenders: [
{a: 0, b: 0},
{a: 1, b: 0},
{a: 1, b: 1},
{a: 0, b: 1},
{a: 0, b: 0},
{a: 1, b: 1},
],
};