Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cfde32738 |
@@ -129,7 +129,6 @@ function run(
|
||||
mode,
|
||||
config,
|
||||
contextIdentifiers,
|
||||
func,
|
||||
logger,
|
||||
filename,
|
||||
code,
|
||||
|
||||
@@ -70,14 +70,12 @@ import {BuiltInArrayId} from './ObjectShape';
|
||||
export function lower(
|
||||
func: NodePath<t.Function>,
|
||||
env: Environment,
|
||||
// Bindings captured from the outer function, in case lower() is called recursively (for lambdas)
|
||||
bindings: Bindings | null = null,
|
||||
capturedRefs: Array<t.Identifier> = [],
|
||||
// the outermost function being compiled, in case lower() is called recursively (for lambdas)
|
||||
parent: NodePath<t.Function> | null = null,
|
||||
): Result<HIRFunction, CompilerError> {
|
||||
const builder = new HIRBuilder(env, {
|
||||
bindings,
|
||||
context: capturedRefs,
|
||||
});
|
||||
const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs);
|
||||
const context: HIRFunction['context'] = [];
|
||||
|
||||
for (const ref of capturedRefs ?? []) {
|
||||
@@ -217,7 +215,7 @@ export function lower(
|
||||
return Ok({
|
||||
id,
|
||||
params,
|
||||
fnType: bindings == null ? env.fnType : 'Other',
|
||||
fnType: parent == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
returnType: makeType(),
|
||||
body: builder.build(),
|
||||
@@ -3419,7 +3417,7 @@ function lowerFunction(
|
||||
| t.ObjectMethod
|
||||
>,
|
||||
): LoweredFunction | null {
|
||||
const componentScope: Scope = builder.environment.parentFunction.scope;
|
||||
const componentScope: Scope = builder.parentFunction.scope;
|
||||
const capturedContext = gatherCapturedContext(expr, componentScope);
|
||||
|
||||
/*
|
||||
@@ -3430,10 +3428,13 @@ function lowerFunction(
|
||||
* This isn't a problem in practice because use Babel's scope analysis to
|
||||
* identify the correct references.
|
||||
*/
|
||||
const lowering = lower(expr, builder.environment, builder.bindings, [
|
||||
...builder.context,
|
||||
...capturedContext,
|
||||
]);
|
||||
const lowering = lower(
|
||||
expr,
|
||||
builder.environment,
|
||||
builder.bindings,
|
||||
[...builder.context, ...capturedContext],
|
||||
builder.parentFunction,
|
||||
);
|
||||
let loweredFunc: HIRFunction;
|
||||
if (lowering.isErr()) {
|
||||
lowering
|
||||
@@ -3455,7 +3456,7 @@ function lowerExpressionToTemporary(
|
||||
return lowerValueToTemporary(builder, value);
|
||||
}
|
||||
|
||||
export function lowerValueToTemporary(
|
||||
function lowerValueToTemporary(
|
||||
builder: HIRBuilder,
|
||||
value: InstructionValue,
|
||||
): Place {
|
||||
|
||||
@@ -191,7 +191,6 @@ type TerminalRewriteInfo =
|
||||
| {
|
||||
kind: 'StartScope';
|
||||
blockId: BlockId;
|
||||
dependencyId: BlockId;
|
||||
fallthroughId: BlockId;
|
||||
instrId: InstructionId;
|
||||
scope: ReactiveScope;
|
||||
@@ -216,14 +215,12 @@ function pushStartScopeTerminal(
|
||||
scope: ReactiveScope,
|
||||
context: ScopeTraversalContext,
|
||||
): void {
|
||||
const dependencyId = context.env.nextBlockId;
|
||||
const blockId = context.env.nextBlockId;
|
||||
const fallthroughId = context.env.nextBlockId;
|
||||
context.rewrites.push({
|
||||
kind: 'StartScope',
|
||||
blockId,
|
||||
fallthroughId,
|
||||
dependencyId,
|
||||
instrId: scope.range.start,
|
||||
scope,
|
||||
});
|
||||
@@ -265,13 +262,10 @@ type RewriteContext = {
|
||||
* instr1, instr2, instr3, instr4, [[ original terminal ]]
|
||||
* Rewritten:
|
||||
* bb0:
|
||||
* instr1, [[ scope start dependencies=bb1 block=bb2]]
|
||||
* instr1, [[ scope start block=bb1]]
|
||||
* bb1:
|
||||
* [[ empty, filled in in PropagateScopeDependenciesHIR ]]
|
||||
* goto bb2
|
||||
* instr2, instr3, [[ scope end goto=bb2 ]]
|
||||
* bb2:
|
||||
* instr2, instr3, [[ scope end goto=bb3 ]]
|
||||
* bb3:
|
||||
* instr4, [[ original terminal ]]
|
||||
*/
|
||||
function handleRewrite(
|
||||
@@ -285,7 +279,6 @@ function handleRewrite(
|
||||
? {
|
||||
kind: 'scope',
|
||||
fallthrough: terminalInfo.fallthroughId,
|
||||
dependencies: terminalInfo.dependencyId,
|
||||
block: terminalInfo.blockId,
|
||||
scope: terminalInfo.scope,
|
||||
id: terminalInfo.instrId,
|
||||
@@ -312,28 +305,7 @@ function handleRewrite(
|
||||
context.nextPreds = new Set([currBlockId]);
|
||||
context.nextBlockId =
|
||||
terminalInfo.kind === 'StartScope'
|
||||
? terminalInfo.dependencyId
|
||||
? terminalInfo.blockId
|
||||
: terminalInfo.fallthroughId;
|
||||
context.instrSliceIdx = idx;
|
||||
|
||||
if (terminalInfo.kind === 'StartScope') {
|
||||
const currBlockId = context.nextBlockId;
|
||||
context.rewrites.push({
|
||||
kind: context.source.kind,
|
||||
id: currBlockId,
|
||||
instructions: [],
|
||||
preds: context.nextPreds,
|
||||
// Only the first rewrite should reuse source block phis
|
||||
phis: new Set(),
|
||||
terminal: {
|
||||
kind: 'goto',
|
||||
variant: GotoVariant.Break,
|
||||
block: terminal.block,
|
||||
id: terminalInfo.instrId,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
});
|
||||
context.nextPreds = new Set([currBlockId]);
|
||||
context.nextBlockId = terminalInfo.blockId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,10 +241,7 @@ type PropertyPathNode =
|
||||
class PropertyPathRegistry {
|
||||
roots: Map<IdentifierId, RootNode> = new Map();
|
||||
|
||||
getOrCreateIdentifier(
|
||||
identifier: Identifier,
|
||||
reactive: boolean,
|
||||
): PropertyPathNode {
|
||||
getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
|
||||
/**
|
||||
* Reads from a statically scoped variable are always safe in JS,
|
||||
* with the exception of TDZ (not addressed by this pass).
|
||||
@@ -258,19 +255,12 @@ class PropertyPathRegistry {
|
||||
optionalProperties: new Map(),
|
||||
fullPath: {
|
||||
identifier,
|
||||
reactive,
|
||||
path: [],
|
||||
},
|
||||
hasOptional: false,
|
||||
parent: null,
|
||||
};
|
||||
this.roots.set(identifier.id, rootNode);
|
||||
} else {
|
||||
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
|
||||
reason:
|
||||
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope',
|
||||
loc: identifier.loc,
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
}
|
||||
@@ -288,7 +278,6 @@ class PropertyPathRegistry {
|
||||
parent: parent,
|
||||
fullPath: {
|
||||
identifier: parent.fullPath.identifier,
|
||||
reactive: parent.fullPath.reactive,
|
||||
path: parent.fullPath.path.concat(entry),
|
||||
},
|
||||
hasOptional: parent.hasOptional || entry.optional,
|
||||
@@ -304,7 +293,7 @@ class PropertyPathRegistry {
|
||||
* so all subpaths of a PropertyLoad should already exist
|
||||
* (e.g. a.b is added before a.b.c),
|
||||
*/
|
||||
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive);
|
||||
let currNode = this.getOrCreateIdentifier(n.identifier);
|
||||
if (n.path.length === 0) {
|
||||
return currNode;
|
||||
}
|
||||
@@ -326,11 +315,10 @@ function getMaybeNonNullInInstruction(
|
||||
instr: InstructionValue,
|
||||
context: CollectHoistablePropertyLoadsContext,
|
||||
): PropertyPathNode | null {
|
||||
let path: ReactiveScopeDependency | null = null;
|
||||
let path = null;
|
||||
if (instr.kind === 'PropertyLoad') {
|
||||
path = context.temporaries.get(instr.object.identifier.id) ?? {
|
||||
identifier: instr.object.identifier,
|
||||
reactive: instr.object.reactive,
|
||||
path: [],
|
||||
};
|
||||
} else if (instr.kind === 'Destructure') {
|
||||
@@ -393,7 +381,7 @@ function collectNonNullsInBlocks(
|
||||
) {
|
||||
const identifier = fn.params[0].identifier;
|
||||
knownNonNullIdentifiers.add(
|
||||
context.registry.getOrCreateIdentifier(identifier, true),
|
||||
context.registry.getOrCreateIdentifier(identifier),
|
||||
);
|
||||
}
|
||||
const nodes = new Map<
|
||||
@@ -628,11 +616,9 @@ function reduceMaybeOptionalChains(
|
||||
changed = false;
|
||||
|
||||
for (const original of optionalChainNodes) {
|
||||
let {identifier, path: origPath, reactive} = original.fullPath;
|
||||
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
|
||||
identifier,
|
||||
reactive,
|
||||
);
|
||||
let {identifier, path: origPath} = original.fullPath;
|
||||
let currNode: PropertyPathNode =
|
||||
registry.getOrCreateIdentifier(identifier);
|
||||
for (let i = 0; i < origPath.length; i++) {
|
||||
const entry = origPath[i];
|
||||
// If the base is known to be non-null, replace with a non-optional load
|
||||
|
||||
@@ -114,7 +114,7 @@ export type OptionalChainSidemap = {
|
||||
hoistableObjects: ReadonlyMap<BlockId, ReactiveScopeDependency>;
|
||||
};
|
||||
|
||||
export type OptionalTraversalContext = {
|
||||
type OptionalTraversalContext = {
|
||||
currFn: HIRFunction;
|
||||
blocks: ReadonlyMap<BlockId, BasicBlock>;
|
||||
|
||||
@@ -235,7 +235,7 @@ function matchOptionalTestBlock(
|
||||
* property loads. If any part of the optional chain is not hoistable, returns
|
||||
* null.
|
||||
*/
|
||||
export function traverseOptionalBlock(
|
||||
function traverseOptionalBlock(
|
||||
optional: TBasicBlock<OptionalTerminal>,
|
||||
context: OptionalTraversalContext,
|
||||
outerAlternate: BlockId | null,
|
||||
@@ -290,7 +290,6 @@ export function traverseOptionalBlock(
|
||||
);
|
||||
baseObject = {
|
||||
identifier: maybeTest.instructions[0].value.place.identifier,
|
||||
reactive: maybeTest.instructions[0].value.place.reactive,
|
||||
path,
|
||||
};
|
||||
test = maybeTest.terminal;
|
||||
@@ -392,7 +391,6 @@ export function traverseOptionalBlock(
|
||||
);
|
||||
const load = {
|
||||
identifier: baseObject.identifier,
|
||||
reactive: baseObject.reactive,
|
||||
path: [
|
||||
...baseObject.path,
|
||||
{
|
||||
|
||||
@@ -25,9 +25,8 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* `identifier.path`, or `identifier?.path` is in this map, it is safe to
|
||||
* evaluate (non-optional) PropertyLoads from.
|
||||
*/
|
||||
#hoistableObjects: Map<Identifier, HoistableNode & {reactive: boolean}> =
|
||||
new Map();
|
||||
#deps: Map<Identifier, DependencyNode & {reactive: boolean}> = new Map();
|
||||
#hoistableObjects: Map<Identifier, HoistableNode> = new Map();
|
||||
#deps: Map<Identifier, DependencyNode> = new Map();
|
||||
|
||||
/**
|
||||
* @param hoistableObjects a set of paths from which we can safely evaluate
|
||||
@@ -36,10 +35,9 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* duplicates when traversing the CFG.
|
||||
*/
|
||||
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
|
||||
for (const {path, identifier, reactive} of hoistableObjects) {
|
||||
for (const {path, identifier} of hoistableObjects) {
|
||||
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#hoistableObjects,
|
||||
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
|
||||
);
|
||||
@@ -72,8 +70,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
|
||||
static #getOrCreateRoot<T extends string>(
|
||||
identifier: Identifier,
|
||||
reactive: boolean,
|
||||
roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
|
||||
roots: Map<Identifier, TreeNode<T>>,
|
||||
defaultAccessType: T,
|
||||
): TreeNode<T> {
|
||||
// roots can always be accessed unconditionally in JS
|
||||
@@ -82,16 +79,9 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
if (rootNode === undefined) {
|
||||
rootNode = {
|
||||
properties: new Map(),
|
||||
reactive,
|
||||
accessType: defaultAccessType,
|
||||
};
|
||||
roots.set(identifier, rootNode);
|
||||
} else {
|
||||
CompilerError.invariant(reactive === rootNode.reactive, {
|
||||
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
|
||||
description: `Identifier ${printIdentifier(identifier)}`,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
}
|
||||
@@ -102,10 +92,9 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* safe-to-evaluate subpath
|
||||
*/
|
||||
addDependency(dep: ReactiveScopeDependency): void {
|
||||
const {identifier, reactive, path} = dep;
|
||||
const {identifier, path} = dep;
|
||||
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#deps,
|
||||
PropertyAccessType.UnconditionalAccess,
|
||||
);
|
||||
@@ -183,13 +172,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
|
||||
const results = new Set<ReactiveScopeDependency>();
|
||||
for (const [rootId, rootNode] of this.#deps.entries()) {
|
||||
collectMinimalDependenciesInSubtree(
|
||||
rootNode,
|
||||
rootNode.reactive,
|
||||
rootId,
|
||||
[],
|
||||
results,
|
||||
);
|
||||
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -311,24 +294,25 @@ type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
|
||||
type DependencyNode = TreeNode<PropertyAccessType>;
|
||||
|
||||
/**
|
||||
* TODO: this is directly pasted from DeriveMinimalDependencies. Since we no
|
||||
* longer have conditionally accessed nodes, we can simplify
|
||||
*
|
||||
* Recursively calculates minimal dependencies in a subtree.
|
||||
* @param node DependencyNode representing a dependency subtree.
|
||||
* @returns a minimal list of dependencies in this subtree.
|
||||
*/
|
||||
function collectMinimalDependenciesInSubtree(
|
||||
node: DependencyNode,
|
||||
reactive: boolean,
|
||||
rootIdentifier: Identifier,
|
||||
path: Array<DependencyPathEntry>,
|
||||
results: Set<ReactiveScopeDependency>,
|
||||
): void {
|
||||
if (isDependency(node.accessType)) {
|
||||
results.add({identifier: rootIdentifier, reactive, path});
|
||||
results.add({identifier: rootIdentifier, path});
|
||||
} else {
|
||||
for (const [childName, childNode] of node.properties) {
|
||||
collectMinimalDependenciesInSubtree(
|
||||
childNode,
|
||||
reactive,
|
||||
rootIdentifier,
|
||||
[
|
||||
...path,
|
||||
|
||||
@@ -55,7 +55,7 @@ import {
|
||||
ShapeRegistry,
|
||||
addHook,
|
||||
} from './ObjectShape';
|
||||
import {Scope as BabelScope, NodePath} from '@babel/traverse';
|
||||
import {Scope as BabelScope} from '@babel/traverse';
|
||||
import {TypeSchema} from './TypeSchema';
|
||||
|
||||
export const ReactElementSymbolSchema = z.object({
|
||||
@@ -863,7 +863,6 @@ export class Environment {
|
||||
|
||||
#contextIdentifiers: Set<t.Identifier>;
|
||||
#hoistedIdentifiers: Set<t.Identifier>;
|
||||
parentFunction: NodePath<t.Function>;
|
||||
|
||||
constructor(
|
||||
scope: BabelScope,
|
||||
@@ -871,7 +870,6 @@ export class Environment {
|
||||
compilerMode: CompilerMode,
|
||||
config: EnvironmentConfig,
|
||||
contextIdentifiers: Set<t.Identifier>,
|
||||
parentFunction: NodePath<t.Function>, // the outermost function being compiled
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
@@ -930,7 +928,6 @@ export class Environment {
|
||||
this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType);
|
||||
}
|
||||
|
||||
this.parentFunction = parentFunction;
|
||||
this.#contextIdentifiers = contextIdentifiers;
|
||||
this.#hoistedIdentifiers = new Set();
|
||||
}
|
||||
|
||||
@@ -62,14 +62,12 @@ export type ReactiveFunction = {
|
||||
|
||||
export type ReactiveScopeBlock = {
|
||||
kind: 'scope';
|
||||
dependencyInstructions: Array<ReactiveInstructionStatement>;
|
||||
scope: ReactiveScope;
|
||||
instructions: ReactiveBlock;
|
||||
};
|
||||
|
||||
export type PrunedReactiveScopeBlock = {
|
||||
kind: 'pruned-scope';
|
||||
dependencyInstructions: Array<ReactiveInstructionStatement>;
|
||||
scope: ReactiveScope;
|
||||
instructions: ReactiveBlock;
|
||||
};
|
||||
@@ -616,7 +614,6 @@ export type MaybeThrowTerminal = {
|
||||
export type ReactiveScopeTerminal = {
|
||||
kind: 'scope';
|
||||
fallthrough: BlockId;
|
||||
dependencies: BlockId;
|
||||
block: BlockId;
|
||||
scope: ReactiveScope;
|
||||
id: InstructionId;
|
||||
@@ -626,7 +623,6 @@ export type ReactiveScopeTerminal = {
|
||||
export type PrunedScopeTerminal = {
|
||||
kind: 'pruned-scope';
|
||||
fallthrough: BlockId;
|
||||
dependencies: BlockId;
|
||||
block: BlockId;
|
||||
scope: ReactiveScope;
|
||||
id: InstructionId;
|
||||
@@ -1476,10 +1472,9 @@ export type ReactiveScope = {
|
||||
range: MutableRange;
|
||||
|
||||
/**
|
||||
* Note the dependencies of a reactive scope are tracked in HIR and
|
||||
* ReactiveFunction
|
||||
* The inputs to this reactive scope
|
||||
*/
|
||||
dependencies: Array<Place>;
|
||||
dependencies: ReactiveScopeDependencies;
|
||||
|
||||
/**
|
||||
* The set of values produced by this scope. This may be empty
|
||||
@@ -1540,18 +1535,6 @@ export type DependencyPathEntry = {
|
||||
export type DependencyPath = Array<DependencyPathEntry>;
|
||||
export type ReactiveScopeDependency = {
|
||||
identifier: Identifier;
|
||||
/**
|
||||
* Reflects whether the base identifier is reactive. Note that some reactive
|
||||
* objects may have non-reactive properties, but we do not currently track
|
||||
* this.
|
||||
*
|
||||
* ```js
|
||||
* // Technically, result[0] is reactive and result[1] is not.
|
||||
* // Currently, both dependencies would be marked as reactive.
|
||||
* const result = useState();
|
||||
* ```
|
||||
*/
|
||||
reactive: boolean;
|
||||
path: DependencyPath;
|
||||
};
|
||||
|
||||
@@ -1755,40 +1738,6 @@ export function isStableType(id: Identifier): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isStableTypeContainer(id: Identifier): boolean {
|
||||
const type_ = id.type;
|
||||
if (type_.kind !== 'Object') {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
isUseStateType(id) || // setState
|
||||
type_.shapeId === 'BuiltInUseActionState' || // setActionState
|
||||
isUseReducerType(id) || // dispatcher
|
||||
type_.shapeId === 'BuiltInUseTransition' // startTransition
|
||||
);
|
||||
}
|
||||
|
||||
export function evaluatesToStableTypeOrContainer(
|
||||
env: Environment,
|
||||
{value}: Instruction,
|
||||
): boolean {
|
||||
if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
const calleeHookKind = getHookKind(env, callee.identifier);
|
||||
switch (calleeHookKind) {
|
||||
case 'useState':
|
||||
case 'useReducer':
|
||||
case 'useActionState':
|
||||
case 'useRef':
|
||||
case 'useTransition':
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isUseEffectHookType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseEffectHook'
|
||||
|
||||
@@ -110,6 +110,7 @@ export default class HIRBuilder {
|
||||
#bindings: Bindings;
|
||||
#env: Environment;
|
||||
#exceptionHandlerStack: Array<BlockId> = [];
|
||||
parentFunction: NodePath<t.Function>;
|
||||
errors: CompilerError = new CompilerError();
|
||||
/**
|
||||
* Traversal context: counts the number of `fbt` tag parents
|
||||
@@ -135,17 +136,16 @@ export default class HIRBuilder {
|
||||
|
||||
constructor(
|
||||
env: Environment,
|
||||
options?: {
|
||||
bindings?: Bindings | null;
|
||||
context?: Array<t.Identifier>;
|
||||
entryBlockKind?: BlockKind;
|
||||
},
|
||||
parentFunction: NodePath<t.Function>, // the outermost function being compiled
|
||||
bindings: Bindings | null = null,
|
||||
context: Array<t.Identifier> | null = null,
|
||||
) {
|
||||
this.#env = env;
|
||||
this.#bindings = options?.bindings ?? new Map();
|
||||
this.#context = options?.context ?? [];
|
||||
this.#bindings = bindings ?? new Map();
|
||||
this.parentFunction = parentFunction;
|
||||
this.#context = context ?? [];
|
||||
this.#entry = makeBlockId(env.nextBlockId);
|
||||
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
|
||||
this.#current = newBlock(this.#entry, 'block');
|
||||
}
|
||||
|
||||
currentBlockKind(): BlockKind {
|
||||
@@ -239,7 +239,7 @@ export default class HIRBuilder {
|
||||
|
||||
// Check if the binding is from module scope
|
||||
const outerBinding =
|
||||
this.#env.parentFunction.scope.parent.getBinding(originalName);
|
||||
this.parentFunction.scope.parent.getBinding(originalName);
|
||||
if (babelBinding === outerBinding) {
|
||||
const path = babelBinding.path;
|
||||
if (path.isImportDefaultSpecifier()) {
|
||||
@@ -293,7 +293,7 @@ export default class HIRBuilder {
|
||||
const binding = this.#resolveBabelBinding(path);
|
||||
if (binding) {
|
||||
// Check if the binding is from module scope, if so return null
|
||||
const outerBinding = this.#env.parentFunction.scope.parent.getBinding(
|
||||
const outerBinding = this.parentFunction.scope.parent.getBinding(
|
||||
path.node.name,
|
||||
);
|
||||
if (binding === outerBinding) {
|
||||
@@ -376,7 +376,7 @@ export default class HIRBuilder {
|
||||
}
|
||||
|
||||
// Terminate the current block w the given terminal, and start a new block
|
||||
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): BlockId {
|
||||
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): void {
|
||||
const {id: blockId, kind, instructions} = this.#current;
|
||||
this.#completed.set(blockId, {
|
||||
kind,
|
||||
@@ -390,7 +390,6 @@ export default class HIRBuilder {
|
||||
const nextId = this.#env.nextBlockId;
|
||||
this.#current = newBlock(nextId, nextBlockKind);
|
||||
}
|
||||
return blockId;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -747,11 +746,6 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
|
||||
* (eg bb2 then bb1), we ensure that they get reversed back to the correct order.
|
||||
*/
|
||||
const block = func.blocks.get(blockId)!;
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: '[HIRBuilder] Unexpected null block',
|
||||
description: `expected block ${blockId} to exist`,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
|
||||
const fallthrough = terminalFallthrough(block.terminal);
|
||||
|
||||
|
||||
@@ -286,13 +286,13 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
|
||||
case 'scope': {
|
||||
value = `[${terminal.id}] Scope ${printReactiveScopeSummary(
|
||||
terminal.scope,
|
||||
)} dependencies=bb${terminal.dependencies} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
|
||||
)} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
|
||||
break;
|
||||
}
|
||||
case 'pruned-scope': {
|
||||
value = `[${terminal.id}] <pruned> Scope ${printReactiveScopeSummary(
|
||||
terminal.scope,
|
||||
)} dependencies=bb${terminal.dependencies} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
|
||||
)} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
|
||||
break;
|
||||
}
|
||||
case 'try': {
|
||||
|
||||
@@ -47,17 +47,6 @@ import {CompilerError} from '../CompilerError';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
import {ReactiveScopeDependencyTreeHIR} from './DeriveMinimalDependenciesHIR';
|
||||
import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
|
||||
import {
|
||||
fixScopeAndIdentifierRanges,
|
||||
markInstructionIds,
|
||||
markPredecessors,
|
||||
reversePostorderBlocks,
|
||||
} from './HIRBuilder';
|
||||
import {printDependency} from '../ReactiveScopes/PrintReactiveFunction';
|
||||
import {
|
||||
readScopeDependencies,
|
||||
writeScopeDependencies,
|
||||
} from './ScopeDependencyUtils';
|
||||
|
||||
export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
|
||||
const usedOutsideDeclaringScope =
|
||||
@@ -84,10 +73,8 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
|
||||
/**
|
||||
* Derive the minimal set of hoistable dependencies for each scope.
|
||||
*/
|
||||
const minimalDeps = new Map<ReactiveScope, Set<ReactiveScopeDependency>>();
|
||||
for (const [scope, deps] of scopeDeps) {
|
||||
if (deps.length === 0) {
|
||||
minimalDeps.set(scope, new Set());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -114,79 +101,17 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
|
||||
* Step 3: Reduce dependencies to a minimal set.
|
||||
*/
|
||||
const candidates = tree.deriveMinimalDependencies();
|
||||
const dependencies = new Set<ReactiveScopeDependency>();
|
||||
for (const candidateDep of candidates) {
|
||||
if (
|
||||
!Iterable_some(
|
||||
dependencies,
|
||||
scope.dependencies,
|
||||
existingDep =>
|
||||
existingDep.identifier.declarationId ===
|
||||
candidateDep.identifier.declarationId &&
|
||||
areEqualPaths(existingDep.path, candidateDep.path),
|
||||
)
|
||||
)
|
||||
dependencies.add(candidateDep);
|
||||
}
|
||||
minimalDeps.set(scope, dependencies);
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
/**
|
||||
* Step 4: inject dependencies
|
||||
*/
|
||||
for (const [_, {terminal}] of fn.body.blocks) {
|
||||
if (terminal.kind !== 'scope' && terminal.kind !== 'pruned-scope') {
|
||||
continue;
|
||||
}
|
||||
const scope = terminal.scope;
|
||||
const deps = minimalDeps.get(scope);
|
||||
if (deps == null || deps.size === 0) {
|
||||
continue;
|
||||
}
|
||||
writeScopeDependencies(terminal, deps, fn);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 5: fix scope and identifier ranges to account for renumbered
|
||||
* instructions
|
||||
*/
|
||||
if (changed) {
|
||||
reversePostorderBlocks(fn.body);
|
||||
markPredecessors(fn.body);
|
||||
markInstructionIds(fn.body);
|
||||
|
||||
fixScopeAndIdentifierRanges(fn.body);
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
{
|
||||
for (const [scope, deps] of minimalDeps) {
|
||||
const checkedDeps = [...readScopeDependencies(fn, scope.id)];
|
||||
CompilerError.invariant(checkedDeps != null, {
|
||||
reason: '[Rewrite] Cannot find scope dep when reading',
|
||||
loc: scope.loc,
|
||||
});
|
||||
CompilerError.invariant(checkedDeps.length === deps.size, {
|
||||
reason: '[Rewrite] non matching sizes when reading',
|
||||
description: `scopeId=${scope.id} deps=${[...deps].map(printDependency)} checkedDeps=${[...checkedDeps].map(printDependency)}`,
|
||||
loc: scope.loc,
|
||||
});
|
||||
for (const dep of deps) {
|
||||
CompilerError.invariant(
|
||||
checkedDeps.some(
|
||||
checkedDep =>
|
||||
dep.identifier === checkedDep.identifier &&
|
||||
areEqualPaths(dep.path, checkedDep.path),
|
||||
),
|
||||
{
|
||||
reason:
|
||||
'[Rewrite] could not find match for dependency when re-reading',
|
||||
description: `${printDependency(dep)}`,
|
||||
loc: scope.loc,
|
||||
},
|
||||
);
|
||||
}
|
||||
scope.dependencies.add(candidateDep);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,7 +309,6 @@ function collectTemporariesSidemapImpl(
|
||||
) {
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
identifier: value.place.identifier,
|
||||
reactive: value.place.reactive,
|
||||
path: [],
|
||||
});
|
||||
}
|
||||
@@ -438,13 +362,11 @@ function getProperty(
|
||||
if (resolvedDependency == null) {
|
||||
property = {
|
||||
identifier: object.identifier,
|
||||
reactive: object.reactive,
|
||||
path: [{property: propertyName, optional}],
|
||||
};
|
||||
} else {
|
||||
property = {
|
||||
identifier: resolvedDependency.identifier,
|
||||
reactive: resolvedDependency.reactive,
|
||||
path: [...resolvedDependency.path, {property: propertyName, optional}],
|
||||
};
|
||||
}
|
||||
@@ -600,7 +522,6 @@ export class DependencyCollectionContext {
|
||||
this.visitDependency(
|
||||
this.#temporaries.get(place.identifier.id) ?? {
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
},
|
||||
);
|
||||
@@ -665,7 +586,6 @@ export class DependencyCollectionContext {
|
||||
) {
|
||||
maybeDependency = {
|
||||
identifier: maybeDependency.identifier,
|
||||
reactive: maybeDependency.reactive,
|
||||
path: [],
|
||||
};
|
||||
}
|
||||
@@ -687,11 +607,7 @@ export class DependencyCollectionContext {
|
||||
identifier =>
|
||||
identifier.declarationId === place.identifier.declarationId,
|
||||
) &&
|
||||
this.#checkValidDependency({
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
})
|
||||
this.#checkValidDependency({identifier: place.identifier, path: []})
|
||||
) {
|
||||
currentScope.reassignments.add(place.identifier);
|
||||
}
|
||||
|
||||
@@ -1,569 +0,0 @@
|
||||
import {
|
||||
ScopeId,
|
||||
HIRFunction,
|
||||
Place,
|
||||
ReactiveScopeDependency,
|
||||
Identifier,
|
||||
makeInstructionId,
|
||||
InstructionKind,
|
||||
GeneratedSource,
|
||||
IdentifierId,
|
||||
BlockId,
|
||||
makeTemporaryIdentifier,
|
||||
Effect,
|
||||
OptionalTerminal,
|
||||
TBasicBlock,
|
||||
ReactiveScopeTerminal,
|
||||
GotoVariant,
|
||||
PrunedScopeTerminal,
|
||||
ReactiveInstruction,
|
||||
ReactiveValue,
|
||||
ReactiveScopeBlock,
|
||||
PrunedReactiveScopeBlock,
|
||||
} from './HIR';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
OptionalTraversalContext,
|
||||
traverseOptionalBlock,
|
||||
} from './CollectOptionalChainDependencies';
|
||||
import {Environment} from './Environment';
|
||||
import HIRBuilder from './HIRBuilder';
|
||||
import {lowerValueToTemporary} from './BuildHIR';
|
||||
import {printDependency} from '../ReactiveScopes/PrintReactiveFunction';
|
||||
import {printPlace} from './PrintHIR';
|
||||
|
||||
function writeNonOptionalDependency(
|
||||
dep: ReactiveScopeDependency,
|
||||
env: Environment,
|
||||
builder: HIRBuilder,
|
||||
): Identifier {
|
||||
const loc = dep.identifier.loc;
|
||||
let last: Identifier = makeTemporaryIdentifier(env.nextIdentifierId, loc);
|
||||
builder.push({
|
||||
lvalue: {
|
||||
identifier: last,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Mutate,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {
|
||||
identifier: dep.identifier,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
id: makeInstructionId(1),
|
||||
loc: loc,
|
||||
});
|
||||
|
||||
for (const path of dep.path) {
|
||||
const next = makeTemporaryIdentifier(env.nextIdentifierId, loc);
|
||||
builder.push({
|
||||
lvalue: {
|
||||
identifier: next,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Mutate,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
value: {
|
||||
kind: 'PropertyLoad',
|
||||
object: {
|
||||
identifier: last,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
property: path.property,
|
||||
loc,
|
||||
},
|
||||
id: makeInstructionId(1),
|
||||
loc: loc,
|
||||
});
|
||||
last = next;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
export function writeScopeDependencies(
|
||||
terminal: ReactiveScopeTerminal | PrunedScopeTerminal,
|
||||
deps: Set<ReactiveScopeDependency>,
|
||||
fn: HIRFunction,
|
||||
): void {
|
||||
const scopeDepBlock = fn.body.blocks.get(terminal.dependencies);
|
||||
|
||||
CompilerError.invariant(scopeDepBlock != null, {
|
||||
reason: 'Expected to find scope dependency block',
|
||||
loc: terminal.loc,
|
||||
});
|
||||
CompilerError.invariant(
|
||||
scopeDepBlock.instructions.length === 0 &&
|
||||
scopeDepBlock.terminal.kind === 'goto' &&
|
||||
scopeDepBlock.terminal.block === terminal.block,
|
||||
{
|
||||
reason: 'Expected scope.dependencies to be a goto block (invalid cfg)',
|
||||
loc: terminal.loc,
|
||||
},
|
||||
);
|
||||
const builder = new HIRBuilder(fn.env, {
|
||||
entryBlockKind: 'value',
|
||||
});
|
||||
|
||||
for (const dep of deps) {
|
||||
if (dep.path.every(path => !path.optional)) {
|
||||
const last = writeNonOptionalDependency(dep, fn.env, builder);
|
||||
terminal.scope.dependencies.push({
|
||||
kind: 'Identifier',
|
||||
identifier: last,
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Write all optional chaining deps
|
||||
for (const dep of deps) {
|
||||
if (!dep.path.every(path => !path.optional)) {
|
||||
const last = writeOptional(
|
||||
dep.path.length - 1,
|
||||
dep,
|
||||
builder,
|
||||
terminal,
|
||||
null,
|
||||
);
|
||||
terminal.scope.dependencies.push({
|
||||
kind: 'Identifier',
|
||||
identifier: last,
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder terminal for HIRBuilder, to be later replaced by a `goto` to an outer block
|
||||
const lastBlockId = builder.terminate(
|
||||
{
|
||||
kind: 'return',
|
||||
value: {
|
||||
kind: 'Identifier',
|
||||
identifier: makeTemporaryIdentifier(
|
||||
fn.env.nextIdentifierId,
|
||||
GeneratedSource,
|
||||
),
|
||||
effect: Effect.Freeze,
|
||||
loc: GeneratedSource,
|
||||
reactive: true,
|
||||
},
|
||||
loc: GeneratedSource,
|
||||
id: makeInstructionId(0),
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
const dependenciesHIR = builder.build();
|
||||
for (const [id, block] of dependenciesHIR.blocks) {
|
||||
fn.body.blocks.set(id, block);
|
||||
}
|
||||
fn.body.blocks.delete(terminal.dependencies);
|
||||
|
||||
/**
|
||||
* Connect the newly constructed inner HIR to the outer HIR
|
||||
*/
|
||||
terminal.dependencies = dependenciesHIR.entry;
|
||||
// Rewire the replaceholder terminal to the correct goto
|
||||
fn.body.blocks.get(lastBlockId)!.terminal = scopeDepBlock.terminal;
|
||||
}
|
||||
|
||||
function writeOptional(
|
||||
idx: number,
|
||||
dep: ReactiveScopeDependency,
|
||||
builder: HIRBuilder,
|
||||
terminal: ReactiveScopeTerminal | PrunedScopeTerminal,
|
||||
parentAlternate: BlockId | null,
|
||||
): Identifier {
|
||||
const env = builder.environment;
|
||||
CompilerError.invariant(
|
||||
idx >= 0 && !dep.path.slice(0, idx + 1).every(path => !path.optional),
|
||||
{
|
||||
reason: '[WriteOptional] Expected optional path',
|
||||
description: `${idx} ${printDependency(dep)}`,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
const continuationBlock = builder.reserve(builder.currentBlockKind());
|
||||
const consequent = builder.reserve('value');
|
||||
|
||||
const returnPlace: Place = {
|
||||
kind: 'Identifier',
|
||||
identifier: makeTemporaryIdentifier(env.nextIdentifierId, GeneratedSource),
|
||||
effect: Effect.Mutate,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
let alternate;
|
||||
if (parentAlternate != null) {
|
||||
alternate = parentAlternate;
|
||||
} else {
|
||||
/**
|
||||
* Make outermost alternate block
|
||||
* $N = Primitive undefined
|
||||
* $M = StoreLocal $OptionalResult = $N
|
||||
* goto fallthrough
|
||||
*/
|
||||
alternate = builder.enter('value', () => {
|
||||
const temp = lowerValueToTemporary(builder, {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'StoreLocal',
|
||||
lvalue: {kind: InstructionKind.Const, place: {...returnPlace}},
|
||||
value: {...temp},
|
||||
type: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return {
|
||||
kind: 'goto',
|
||||
variant: GotoVariant.Break,
|
||||
block: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
let testIdentifier: Identifier | null = null;
|
||||
const testBlock = builder.enter('value', () => {
|
||||
const firstOptional = dep.path.findIndex(path => path.optional);
|
||||
if (idx === firstOptional) {
|
||||
// Lower test block
|
||||
testIdentifier = writeNonOptionalDependency(
|
||||
{
|
||||
identifier: dep.identifier,
|
||||
reactive: dep.reactive,
|
||||
path: dep.path.slice(0, idx),
|
||||
},
|
||||
env,
|
||||
builder,
|
||||
);
|
||||
} else {
|
||||
testIdentifier = writeOptional(
|
||||
idx - 1,
|
||||
dep,
|
||||
builder,
|
||||
terminal,
|
||||
alternate,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'branch',
|
||||
test: {
|
||||
identifier: testIdentifier,
|
||||
effect: Effect.Freeze,
|
||||
kind: 'Identifier',
|
||||
loc: GeneratedSource,
|
||||
reactive: dep.reactive,
|
||||
},
|
||||
consequent: consequent.id,
|
||||
alternate,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
fallthrough: continuationBlock.id,
|
||||
};
|
||||
});
|
||||
|
||||
builder.enterReserved(consequent, () => {
|
||||
CompilerError.invariant(testIdentifier !== null, {
|
||||
reason: 'Satisfy type checker',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
const tmpConsequent = lowerValueToTemporary(builder, {
|
||||
kind: 'PropertyLoad',
|
||||
object: {
|
||||
identifier: testIdentifier,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
property: dep.path[idx].property,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'StoreLocal',
|
||||
lvalue: {kind: InstructionKind.Const, place: {...returnPlace}},
|
||||
value: {...tmpConsequent},
|
||||
type: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return {
|
||||
kind: 'goto',
|
||||
variant: GotoVariant.Break,
|
||||
block: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
});
|
||||
builder.terminateWithContinuation(
|
||||
{
|
||||
kind: 'optional',
|
||||
optional: dep.path[idx].optional,
|
||||
test: testBlock,
|
||||
fallthrough: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
continuationBlock,
|
||||
);
|
||||
|
||||
return returnPlace.identifier;
|
||||
}
|
||||
|
||||
export function readScopeDependencies(
|
||||
fn: HIRFunction,
|
||||
scope: ScopeId,
|
||||
): Set<ReactiveScopeDependency> {
|
||||
for (const [_, {terminal}] of fn.body.blocks) {
|
||||
if (terminal.kind !== 'scope' && terminal.kind !== 'pruned-scope') {
|
||||
continue;
|
||||
}
|
||||
if (terminal.scope.id !== scope) {
|
||||
continue;
|
||||
}
|
||||
const temporaries = new Map<IdentifierId, ReactiveScopeDependency>();
|
||||
const context: OptionalTraversalContext = {
|
||||
currFn: fn,
|
||||
blocks: fn.body.blocks,
|
||||
seenOptionals: new Set(),
|
||||
processedInstrsInOptional: new Set(),
|
||||
temporariesReadInOptional: temporaries,
|
||||
hoistableObjects: new Map(),
|
||||
};
|
||||
/**
|
||||
* Step 1: read all instructions between within scope dependencies block(s)
|
||||
*/
|
||||
let work = terminal.dependencies;
|
||||
while (true) {
|
||||
const block = fn.body.blocks.get(work)!;
|
||||
for (const {lvalue, value} of block.instructions) {
|
||||
if (value.kind === 'LoadLocal') {
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
identifier: value.place.identifier,
|
||||
reactive: value.place.reactive,
|
||||
path: [],
|
||||
});
|
||||
} else if (value.kind === 'PropertyLoad') {
|
||||
const source = temporaries.get(value.object.identifier.id)!;
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
identifier: source.identifier,
|
||||
reactive: source.reactive,
|
||||
path: [...source.path, {property: value.property, optional: false}],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (block.terminal.kind === 'optional') {
|
||||
traverseOptionalBlock(
|
||||
block as TBasicBlock<OptionalTerminal>,
|
||||
context,
|
||||
null,
|
||||
);
|
||||
work = block.terminal.fallthrough;
|
||||
} else {
|
||||
CompilerError.invariant(
|
||||
block.terminal.kind === 'goto' &&
|
||||
block.terminal.block === terminal.block,
|
||||
{
|
||||
reason: 'unexpected terminal',
|
||||
description: `kind: ${block.terminal.kind}`,
|
||||
loc: block.terminal.loc,
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 2: look up scope dependencies from the temporaries sidemap
|
||||
*/
|
||||
const scopeOwnDependencies = new Set<ReactiveScopeDependency>();
|
||||
for (const dep of terminal.scope.dependencies) {
|
||||
const reactiveScopeDependency = temporaries.get(dep.identifier.id)!;
|
||||
CompilerError.invariant(reactiveScopeDependency != null, {
|
||||
reason: 'Expected dependency to be found',
|
||||
description: `${printPlace(dep)}`,
|
||||
loc: terminal.scope.loc,
|
||||
});
|
||||
scopeOwnDependencies.add(reactiveScopeDependency);
|
||||
}
|
||||
return scopeOwnDependencies;
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected scope to be found',
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
|
||||
function assertNonNull<T>(value: T | null | undefined): T {
|
||||
if (value == null) {
|
||||
throw new Error('Expected nonnull value');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function readScopeDependenciesRHIRInstr(
|
||||
instr: ReactiveInstruction,
|
||||
sidemap: Map<IdentifierId, ReactiveScopeDependency>,
|
||||
): void {
|
||||
const value = reacScopeDependenciesRHIRValue(instr.value, sidemap);
|
||||
if (instr.lvalue != null) {
|
||||
sidemap.set(instr.lvalue.identifier.id, value);
|
||||
}
|
||||
}
|
||||
function reacScopeDependenciesRHIRValue(
|
||||
instr: ReactiveValue,
|
||||
sidemap: Map<IdentifierId, ReactiveScopeDependency>,
|
||||
): ReactiveScopeDependency {
|
||||
if (instr.kind === 'LoadLocal') {
|
||||
const base = sidemap.get(instr.place.identifier.id);
|
||||
if (base != null) {
|
||||
return base;
|
||||
} else {
|
||||
return {
|
||||
identifier: instr.place.identifier,
|
||||
reactive: instr.place.reactive,
|
||||
path: [],
|
||||
};
|
||||
}
|
||||
} else if (instr.kind === 'PropertyLoad') {
|
||||
const base = assertNonNull(sidemap.get(instr.object.identifier.id));
|
||||
return {
|
||||
identifier: base.identifier,
|
||||
reactive: base.reactive,
|
||||
path: [...base.path, {property: instr.property, optional: false}],
|
||||
};
|
||||
} else if (instr.kind === 'SequenceExpression') {
|
||||
for (const inner of instr.instructions) {
|
||||
readScopeDependenciesRHIRInstr(inner, sidemap);
|
||||
}
|
||||
return reacScopeDependenciesRHIRValue(instr.value, sidemap);
|
||||
} else if (instr.kind === 'OptionalExpression') {
|
||||
const value = reacScopeDependenciesRHIRValue(instr.value, sidemap);
|
||||
CompilerError.invariant(
|
||||
value.path.length > 0 && !value.path.at(-1)!.optional,
|
||||
{
|
||||
reason: 'Expected optional chain to be nonempty',
|
||||
loc: instr.loc,
|
||||
},
|
||||
);
|
||||
return {
|
||||
...value,
|
||||
path: [
|
||||
...value.path.slice(0, -1),
|
||||
{property: value.path.at(-1)!.property, optional: instr.optional},
|
||||
],
|
||||
};
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected value kind',
|
||||
description: instr.kind,
|
||||
loc: instr.loc,
|
||||
});
|
||||
}
|
||||
|
||||
export function readScopeDependenciesRHIR(
|
||||
scopeBlock: ReactiveScopeBlock | PrunedReactiveScopeBlock,
|
||||
): Map<Place, ReactiveScopeDependency> {
|
||||
const sidemap = new Map<IdentifierId, ReactiveScopeDependency>();
|
||||
for (const instr of scopeBlock.dependencyInstructions) {
|
||||
readScopeDependenciesRHIRInstr(instr.instruction, sidemap);
|
||||
}
|
||||
return new Map<Place, ReactiveScopeDependency>(
|
||||
scopeBlock.scope.dependencies.map(place => {
|
||||
return [place, assertNonNull(sidemap.get(place.identifier.id))];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run DCE to delete instructions which are not used by any scope dependencies
|
||||
*
|
||||
* Note: this only handles simple pruning i.e. deleted dependency entries,
|
||||
* not more complex rewrites such as pruned or edited entries.
|
||||
*/
|
||||
export function scopeDependenciesDCE(
|
||||
scopeBlock: ReactiveScopeBlock | PrunedReactiveScopeBlock,
|
||||
): void {
|
||||
const usage = new Map<IdentifierId, IdentifierId | null>();
|
||||
for (const {
|
||||
instruction: {lvalue, value},
|
||||
} of scopeBlock.dependencyInstructions) {
|
||||
if (lvalue == null) {
|
||||
continue;
|
||||
}
|
||||
switch (value.kind) {
|
||||
case 'LoadLocal': {
|
||||
usage.set(lvalue.identifier.id, null);
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
usage.set(lvalue.identifier.id, value.object.identifier.id);
|
||||
break;
|
||||
}
|
||||
case 'OptionalExpression': {
|
||||
usage.set(lvalue.identifier.id, null);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected value kind',
|
||||
description: value.kind,
|
||||
loc: value.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notUsed = new Set(usage.keys());
|
||||
const seen = new Set();
|
||||
for (const {identifier} of scopeBlock.scope.dependencies) {
|
||||
let curr: IdentifierId | undefined | null = identifier.id;
|
||||
while (curr != null) {
|
||||
CompilerError.invariant(!seen.has(curr), {
|
||||
reason: 'infinite loop',
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
notUsed.delete(curr);
|
||||
seen.add(curr);
|
||||
curr = usage.get(curr);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Remove unused instructions in place
|
||||
*/
|
||||
let j = 0;
|
||||
for (let i = 0; i < scopeBlock.dependencyInstructions.length; i++) {
|
||||
const instr = scopeBlock.dependencyInstructions[i].instruction;
|
||||
if (instr.lvalue != null && !notUsed.has(instr.lvalue.identifier.id)) {
|
||||
scopeBlock.dependencyInstructions[j] =
|
||||
scopeBlock.dependencyInstructions[i];
|
||||
j++;
|
||||
}
|
||||
}
|
||||
scopeBlock.dependencyInstructions.length = j;
|
||||
}
|
||||
@@ -860,13 +860,11 @@ export function mapTerminalSuccessors(
|
||||
}
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
const dependencies = fn(terminal.dependencies);
|
||||
const block = fn(terminal.block);
|
||||
const fallthrough = fn(terminal.fallthrough);
|
||||
return {
|
||||
kind: terminal.kind,
|
||||
scope: terminal.scope,
|
||||
dependencies,
|
||||
block,
|
||||
fallthrough,
|
||||
id: makeInstructionId(0),
|
||||
@@ -1019,7 +1017,7 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
|
||||
}
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
yield terminal.dependencies;
|
||||
yield terminal.block;
|
||||
break;
|
||||
}
|
||||
case 'unreachable':
|
||||
@@ -1070,13 +1068,6 @@ export function mapTerminalOperands(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
for (let i = 0; i < terminal.scope.dependencies.length; i++) {
|
||||
terminal.scope.dependencies[i] = fn(terminal.scope.dependencies[i]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'maybe-throw':
|
||||
case 'sequence':
|
||||
case 'label':
|
||||
@@ -1090,7 +1081,9 @@ export function mapTerminalOperands(
|
||||
case 'for-in':
|
||||
case 'goto':
|
||||
case 'unreachable':
|
||||
case 'unsupported': {
|
||||
case 'unsupported':
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
@@ -1134,13 +1127,6 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
for (const dep of terminal.scope.dependencies) {
|
||||
yield dep;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'maybe-throw':
|
||||
case 'sequence':
|
||||
case 'label':
|
||||
@@ -1154,7 +1140,9 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
|
||||
case 'for-in':
|
||||
case 'goto':
|
||||
case 'unreachable':
|
||||
case 'unsupported': {
|
||||
case 'unsupported':
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ import {
|
||||
DependencyCollectionContext,
|
||||
handleInstruction,
|
||||
} from '../HIR/PropagateScopeDependenciesHIR';
|
||||
import {readScopeDependencies} from '../HIR/ScopeDependencyUtils';
|
||||
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
|
||||
import {empty} from '../Utils/Stack';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
@@ -98,7 +97,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
) {
|
||||
scopeInfos.set(
|
||||
block.terminal.scope.id,
|
||||
readScopeDependencies(fn, block.terminal.scope.id),
|
||||
block.terminal.scope.dependencies,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -486,7 +485,7 @@ function inferDependencies(
|
||||
start: fnInstr.id,
|
||||
end: makeInstructionId(fnInstr.id + 1),
|
||||
},
|
||||
dependencies: [],
|
||||
dependencies: new Set(),
|
||||
reassignments: new Set(),
|
||||
declarations: new Map(),
|
||||
earlyReturnValue: null,
|
||||
|
||||
@@ -9,24 +9,18 @@ import {CompilerError} from '..';
|
||||
import {
|
||||
BlockId,
|
||||
Effect,
|
||||
Environment,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
Place,
|
||||
computePostDominatorTree,
|
||||
evaluatesToStableTypeOrContainer,
|
||||
getHookKind,
|
||||
isStableType,
|
||||
isStableTypeContainer,
|
||||
isUseOperator,
|
||||
isUseRefType,
|
||||
} from '../HIR';
|
||||
import {PostDominator} from '../HIR/Dominator';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
@@ -37,103 +31,6 @@ import {
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Side map to track and propagate sources of stability (i.e. hook calls such as
|
||||
* `useRef()` and property reads such as `useState()[1]). Note that this
|
||||
* requires forward data flow analysis since stability is not part of React
|
||||
* Compiler's type system.
|
||||
*/
|
||||
class StableSidemap {
|
||||
map: Map<IdentifierId, {isStable: boolean}> = new Map();
|
||||
env: Environment;
|
||||
|
||||
constructor(env: Environment) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
handleInstruction(instr: Instruction): void {
|
||||
const {value, lvalue} = instr;
|
||||
|
||||
switch (value.kind) {
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
/**
|
||||
* Sources of stability are known hook calls
|
||||
*/
|
||||
if (evaluatesToStableTypeOrContainer(this.env, instr)) {
|
||||
if (isStableType(lvalue.identifier)) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: true,
|
||||
});
|
||||
} else {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: false,
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
this.env.config.enableTreatRefLikeIdentifiersAsRefs &&
|
||||
isUseRefType(lvalue.identifier)
|
||||
) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Destructure':
|
||||
case 'PropertyLoad': {
|
||||
/**
|
||||
* PropertyLoads may from stable containers may also produce stable
|
||||
* values. ComputedLoads are technically safe for now (as all stable
|
||||
* containers have differently-typed elements), but are not handled as
|
||||
* they should be rare anyways.
|
||||
*/
|
||||
const source =
|
||||
value.kind === 'Destructure'
|
||||
? value.value.identifier.id
|
||||
: value.object.identifier.id;
|
||||
const entry = this.map.get(source);
|
||||
if (entry) {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
if (isStableTypeContainer(lvalue.identifier)) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: false,
|
||||
});
|
||||
} else if (isStableType(lvalue.identifier)) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'StoreLocal': {
|
||||
const entry = this.map.get(value.value.identifier.id);
|
||||
if (entry) {
|
||||
this.map.set(lvalue.identifier.id, entry);
|
||||
this.map.set(value.lvalue.place.identifier.id, entry);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'LoadLocal': {
|
||||
const entry = this.map.get(value.place.identifier.id);
|
||||
if (entry) {
|
||||
this.map.set(lvalue.identifier.id, entry);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isStable(id: IdentifierId): boolean {
|
||||
const entry = this.map.get(id);
|
||||
return entry != null ? entry.isStable : false;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Infers which `Place`s are reactive, ie may *semantically* change
|
||||
* over the course of the component/hook's lifetime. Places are reactive
|
||||
@@ -214,7 +111,6 @@ class StableSidemap {
|
||||
*/
|
||||
export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
const reactiveIdentifiers = new ReactivityMap(findDisjointMutableValues(fn));
|
||||
const stableIdentifierSources = new StableSidemap(fn.env);
|
||||
for (const param of fn.params) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
reactiveIdentifiers.markReactive(place);
|
||||
@@ -288,12 +184,11 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
for (const instruction of block.instructions) {
|
||||
stableIdentifierSources.handleInstruction(instruction);
|
||||
const {value} = instruction;
|
||||
let hasReactiveInput = false;
|
||||
/*
|
||||
* NOTE: we want to mark all operands as reactive or not, so we
|
||||
* avoid short-circuiting here
|
||||
* avoid short-circuting here
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const reactive = reactiveIdentifiers.isReactive(operand);
|
||||
@@ -323,13 +218,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
|
||||
if (hasReactiveInput) {
|
||||
for (const lvalue of eachInstructionLValue(instruction)) {
|
||||
/**
|
||||
* Note that it's not correct to mark all stable-typed identifiers
|
||||
* as non-reactive, since ternaries and other value blocks can
|
||||
* produce reactive identifiers typed as these.
|
||||
* (e.g. `props.cond ? setState1 : setState2`)
|
||||
*/
|
||||
if (stableIdentifierSources.isStable(lvalue.identifier.id)) {
|
||||
if (isStableType(lvalue.identifier)) {
|
||||
continue;
|
||||
}
|
||||
reactiveIdentifiers.markReactive(lvalue);
|
||||
@@ -376,41 +265,6 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
} while (reactiveIdentifiers.snapshot());
|
||||
|
||||
function propagateReactivityToInnerFunctions(
|
||||
fn: HIRFunction,
|
||||
isOutermost: boolean,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
if (!isOutermost) {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
reactiveIdentifiers.isReactive(operand);
|
||||
}
|
||||
}
|
||||
if (
|
||||
instr.value.kind === 'ObjectMethod' ||
|
||||
instr.value.kind === 'FunctionExpression'
|
||||
) {
|
||||
propagateReactivityToInnerFunctions(
|
||||
instr.value.loweredFunc.func,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!isOutermost) {
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
reactiveIdentifiers.isReactive(operand);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate reactivity for inner functions, as we eventually hoist and dedupe
|
||||
* dependency instructions for scopes.
|
||||
*/
|
||||
propagateReactivityToInnerFunctions(fn, true);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -387,10 +387,12 @@ export function inlineJsxTransform(
|
||||
|
||||
if (block.terminal.kind === 'scope') {
|
||||
const scope = block.terminal.scope;
|
||||
/**
|
||||
* Note that scope dependencies don't need to be renamed explicitly
|
||||
* as they will be visited when traversing scope terminal successors.
|
||||
*/
|
||||
for (const dep of scope.dependencies) {
|
||||
dep.identifier = handleIdentifier(
|
||||
dep.identifier,
|
||||
inlinedJsxDeclarations,
|
||||
);
|
||||
}
|
||||
|
||||
for (const [origId, decl] of [...scope.declarations]) {
|
||||
const newDecl = handleIdentifier(
|
||||
|
||||
@@ -17,16 +17,11 @@ import {
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {
|
||||
areEqualPaths,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
PrunedReactiveScopeBlock,
|
||||
ReactiveBreakTerminal,
|
||||
ReactiveContinueTerminal,
|
||||
ReactiveFunction,
|
||||
ReactiveInstructionStatement,
|
||||
ReactiveLogicalValue,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveSequenceValue,
|
||||
ReactiveTerminalStatement,
|
||||
ReactiveTerminalTargetKind,
|
||||
@@ -34,12 +29,7 @@ import {
|
||||
ReactiveValue,
|
||||
Terminal,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
readScopeDependencies,
|
||||
readScopeDependenciesRHIR,
|
||||
} from '../HIR/ScopeDependencyUtils';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {printDependency} from './PrintReactiveFunction';
|
||||
|
||||
/*
|
||||
* Converts from HIR (lower-level CFG) to ReactiveFunction, a tree representation
|
||||
@@ -48,7 +38,7 @@ import {printDependency} from './PrintReactiveFunction';
|
||||
* labels for *all* terminals: see PruneUnusedLabels which removes unnecessary labels.
|
||||
*/
|
||||
export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
|
||||
const cx = new Context(fn);
|
||||
const cx = new Context(fn.body);
|
||||
const driver = new Driver(cx);
|
||||
const body = driver.traverseBlock(cx.block(fn.body.entry));
|
||||
return {
|
||||
@@ -826,64 +816,17 @@ class Driver {
|
||||
} else {
|
||||
block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!);
|
||||
}
|
||||
{
|
||||
const scheduleId = this.cx.schedule(terminal.block, 'if');
|
||||
scheduleIds.push(scheduleId);
|
||||
this.cx.scopeFallthroughs.add(terminal.block);
|
||||
}
|
||||
CompilerError.invariant(!this.cx.isScheduled(terminal.dependencies), {
|
||||
reason: `Unexpected 'scope' where the dependencies block is already scheduled`,
|
||||
loc: terminal.loc,
|
||||
});
|
||||
const dependencies: Array<ReactiveInstructionStatement> =
|
||||
this.traverseBlock(this.cx.ir.blocks.get(terminal.dependencies)!).map(
|
||||
dep => {
|
||||
CompilerError.invariant(dep.kind === 'instruction', {
|
||||
reason: '[BuildReactiveFunction] Expected reactive instruction',
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return dep;
|
||||
},
|
||||
);
|
||||
|
||||
this.cx.unscheduleAll(scheduleIds);
|
||||
const scopeBlock: ReactiveScopeBlock | PrunedReactiveScopeBlock = {
|
||||
blockValue.push({
|
||||
kind: terminal.kind,
|
||||
dependencyInstructions: dependencies,
|
||||
instructions: block,
|
||||
scope: terminal.scope,
|
||||
};
|
||||
blockValue.push(scopeBlock);
|
||||
});
|
||||
if (fallthroughId !== null) {
|
||||
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanity check: check that dependencies stay the same after converting
|
||||
* to ReactiveFunction.
|
||||
*/
|
||||
const hirDeps = readScopeDependencies(this.cx.fn, terminal.scope.id);
|
||||
const rhirDeps = [...readScopeDependenciesRHIR(scopeBlock)];
|
||||
CompilerError.invariant(hirDeps.size === rhirDeps.length, {
|
||||
reason: `[BuildReactiveFunction] Sanity check failed: mismatch in dependencies count`,
|
||||
loc: terminal.loc,
|
||||
});
|
||||
for (const hirDep of hirDeps) {
|
||||
CompilerError.invariant(
|
||||
rhirDeps.some(
|
||||
([, rhirDep]) =>
|
||||
hirDep.identifier === rhirDep.identifier &&
|
||||
areEqualPaths(hirDep.path, rhirDep.path),
|
||||
),
|
||||
{
|
||||
reason:
|
||||
'[BuildReactiveFunction] Sanity check failed: mismatching scope dependencies',
|
||||
description: `No match found for ${printDependency(hirDep)}. Candidates are ${rhirDeps.map(dep => printDependency(dep[1]))}`,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'unreachable': {
|
||||
@@ -1300,7 +1243,6 @@ class Driver {
|
||||
}
|
||||
|
||||
class Context {
|
||||
fn: HIRFunction;
|
||||
ir: HIR;
|
||||
#nextScheduleId: number = 0;
|
||||
|
||||
@@ -1331,9 +1273,8 @@ class Context {
|
||||
*/
|
||||
#controlFlowStack: Array<ControlFlowTarget> = [];
|
||||
|
||||
constructor(fn: HIRFunction) {
|
||||
this.fn = fn;
|
||||
this.ir = fn.body;
|
||||
constructor(ir: HIR) {
|
||||
this.ir = ir;
|
||||
}
|
||||
|
||||
block(id: BlockId): BasicBlock {
|
||||
|
||||
@@ -32,10 +32,10 @@ import {
|
||||
ReactiveBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveInstructionStatement,
|
||||
ReactiveScope,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveScopeDeclaration,
|
||||
ReactiveScopeDependency,
|
||||
ReactiveTerminal,
|
||||
ReactiveValue,
|
||||
SourceLocation,
|
||||
@@ -54,7 +54,6 @@ import {SINGLE_CHILD_FBT_TAGS} from './MemoizeFbtAndMacroOperandsInSameScope';
|
||||
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
import {EMIT_FREEZE_GLOBAL_GATING, ReactFunctionType} from '../HIR/Environment';
|
||||
import {ProgramContext} from '../Entrypoint';
|
||||
import generate from '@babel/generator';
|
||||
|
||||
export const MEMO_CACHE_SENTINEL = 'react.memo_cache_sentinel';
|
||||
export const EARLY_RETURN_SENTINEL = 'react.early_return_sentinel';
|
||||
@@ -540,13 +539,7 @@ function codegenBlockNoReset(
|
||||
}
|
||||
case 'scope': {
|
||||
const temp = new Map(cx.temp);
|
||||
codegenReactiveScope(
|
||||
cx,
|
||||
statements,
|
||||
item.scope,
|
||||
item.instructions,
|
||||
item.dependencyInstructions,
|
||||
);
|
||||
codegenReactiveScope(cx, statements, item.scope, item.instructions);
|
||||
cx.temp = temp;
|
||||
break;
|
||||
}
|
||||
@@ -610,7 +603,6 @@ function codegenReactiveScope(
|
||||
statements: Array<t.Statement>,
|
||||
scope: ReactiveScope,
|
||||
block: ReactiveBlock,
|
||||
dependencyInstructions: Array<ReactiveInstructionStatement>,
|
||||
): void {
|
||||
const cacheStoreStatements: Array<t.Statement> = [];
|
||||
const cacheLoadStatements: Array<t.Statement> = [];
|
||||
@@ -623,39 +615,9 @@ function codegenReactiveScope(
|
||||
const changeExpressionComments: Array<string> = [];
|
||||
const outputComments: Array<string> = [];
|
||||
|
||||
/**
|
||||
* Step 1: Dependency instructions should codegen their expressions into
|
||||
* the `Context.Temporaries` map.
|
||||
*/
|
||||
for (const instr of dependencyInstructions) {
|
||||
/**
|
||||
* codegenInstructionNullable should never return a statement, as all
|
||||
* dependency instructions should be inlined into `Context.Temporaries`.
|
||||
*/
|
||||
const result = codegenInstructionNullable(cx, instr.instruction);
|
||||
CompilerError.invariant(result == null, {
|
||||
reason: 'Expected dependency instructions to be inlined',
|
||||
loc: instr.instruction.loc,
|
||||
});
|
||||
}
|
||||
const sortedDependencies: Array<[string, t.Expression]> = scope.dependencies
|
||||
.map<[string, t.Expression]>(dep => {
|
||||
const dependency = codegenPlace(cx, dep);
|
||||
CompilerError.invariant(dependency.type !== 'JSXText', {
|
||||
reason: 'Expected dependency to not be JSXText',
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return [generate(dependency).code, dependency];
|
||||
})
|
||||
.sort(([aName], [bName]) => {
|
||||
if (aName < bName) return -1;
|
||||
else if (aName > bName) return 1;
|
||||
else return 0;
|
||||
});
|
||||
|
||||
for (const [code, dep] of sortedDependencies) {
|
||||
for (const dep of [...scope.dependencies].sort(compareScopeDependency)) {
|
||||
const index = cx.nextCacheIndex;
|
||||
changeExpressionComments.push(code);
|
||||
changeExpressionComments.push(printDependencyComment(dep));
|
||||
const comparison = t.binaryExpression(
|
||||
'!==',
|
||||
t.memberExpression(
|
||||
@@ -663,7 +625,7 @@ function codegenReactiveScope(
|
||||
t.numericLiteral(index),
|
||||
true,
|
||||
),
|
||||
dep,
|
||||
codegenDependency(cx, dep),
|
||||
);
|
||||
|
||||
if (cx.env.config.enableChangeVariableCodegen) {
|
||||
@@ -690,7 +652,7 @@ function codegenReactiveScope(
|
||||
t.numericLiteral(index),
|
||||
true,
|
||||
),
|
||||
t.cloneNode(dep, true),
|
||||
codegenDependency(cx, dep),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1481,6 +1443,17 @@ function codegenForInit(
|
||||
}
|
||||
}
|
||||
|
||||
function printDependencyComment(dependency: ReactiveScopeDependency): string {
|
||||
const identifier = convertIdentifier(dependency.identifier);
|
||||
let name = identifier.name;
|
||||
if (dependency.path !== null) {
|
||||
for (const path of dependency.path) {
|
||||
name += `.${path.property}`;
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function printDelimitedCommentList(
|
||||
items: Array<string>,
|
||||
finalCompletion: string,
|
||||
@@ -1505,6 +1478,34 @@ function printDelimitedCommentList(
|
||||
return output.join('');
|
||||
}
|
||||
|
||||
function codegenDependency(
|
||||
cx: Context,
|
||||
dependency: ReactiveScopeDependency,
|
||||
): t.Expression {
|
||||
let object: t.Expression = convertIdentifier(dependency.identifier);
|
||||
if (dependency.path.length !== 0) {
|
||||
const hasOptional = dependency.path.some(path => path.optional);
|
||||
for (const path of dependency.path) {
|
||||
const property =
|
||||
typeof path.property === 'string'
|
||||
? t.identifier(path.property)
|
||||
: t.numericLiteral(path.property);
|
||||
const isComputed = typeof path.property !== 'string';
|
||||
if (hasOptional) {
|
||||
object = t.optionalMemberExpression(
|
||||
object,
|
||||
property,
|
||||
isComputed,
|
||||
path.optional,
|
||||
);
|
||||
} else {
|
||||
object = t.memberExpression(object, property, isComputed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
function withLoc<T extends (...args: Array<any>) => t.Node>(
|
||||
fn: T,
|
||||
): (
|
||||
@@ -2623,6 +2624,30 @@ function convertIdentifier(identifier: Identifier): t.Identifier {
|
||||
return t.identifier(identifier.name.value);
|
||||
}
|
||||
|
||||
function compareScopeDependency(
|
||||
a: ReactiveScopeDependency,
|
||||
b: ReactiveScopeDependency,
|
||||
): number {
|
||||
CompilerError.invariant(
|
||||
a.identifier.name?.kind === 'named' && b.identifier.name?.kind === 'named',
|
||||
{
|
||||
reason: '[Codegen] Expected named identifier for dependency',
|
||||
loc: a.identifier.loc,
|
||||
},
|
||||
);
|
||||
const aName = [
|
||||
a.identifier.name.value,
|
||||
...a.path.map(entry => `${entry.optional ? '?' : ''}${entry.property}`),
|
||||
].join('.');
|
||||
const bName = [
|
||||
b.identifier.name.value,
|
||||
...b.path.map(entry => `${entry.optional ? '?' : ''}${entry.property}`),
|
||||
].join('.');
|
||||
if (aName < bName) return -1;
|
||||
else if (aName > bName) return 1;
|
||||
else return 0;
|
||||
}
|
||||
|
||||
function compareScopeDeclaration(
|
||||
a: ReactiveScopeDeclaration,
|
||||
b: ReactiveScopeDeclaration,
|
||||
|
||||
@@ -31,16 +31,14 @@ export function flattenReactiveLoopsHIR(fn: HIRFunction): void {
|
||||
}
|
||||
case 'scope': {
|
||||
if (activeLoops.length !== 0) {
|
||||
const newTerminal: PrunedScopeTerminal = {
|
||||
block.terminal = {
|
||||
kind: 'pruned-scope',
|
||||
block: terminal.block,
|
||||
dependencies: terminal.dependencies,
|
||||
fallthrough: terminal.fallthrough,
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
scope: terminal.scope,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
} as PrunedScopeTerminal;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -83,50 +83,28 @@ export function flattenScopesWithHooksOrUseHIR(fn: HIRFunction): void {
|
||||
body.terminal.kind === 'goto' &&
|
||||
body.terminal.block === terminal.fallthrough
|
||||
) {
|
||||
/**
|
||||
* Note that scope blocks are unique in that they represent two nested
|
||||
* goto-labels. We entirely remove dependency blocks here to simplify
|
||||
* rewrite logic
|
||||
*/
|
||||
const dependencyBlock = fn.body.blocks.get(terminal.dependencies);
|
||||
CompilerError.invariant(
|
||||
dependencyBlock != null &&
|
||||
dependencyBlock.instructions.length === 0 &&
|
||||
dependencyBlock.terminal.kind === 'goto' &&
|
||||
dependencyBlock.terminal.block === terminal.block,
|
||||
{
|
||||
reason: `Expected scope dependency block to have no instructions and goto scope block`,
|
||||
loc: terminal.loc,
|
||||
},
|
||||
);
|
||||
fn.body.blocks.delete(terminal.dependencies);
|
||||
const scopeBlock = fn.body.blocks.get(terminal.block)!;
|
||||
scopeBlock.preds = new Set([block.id]);
|
||||
/*
|
||||
* This was a scope just for a hook call, which doesn't need memoization.
|
||||
* flatten it away. We rely on the PrunedUnusedLabel step to do the actual
|
||||
* flattening
|
||||
*/
|
||||
const newTerminal: LabelTerminal = {
|
||||
block.terminal = {
|
||||
kind: 'label',
|
||||
block: terminal.block,
|
||||
fallthrough: terminal.fallthrough,
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
} as LabelTerminal;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newTerminal: PrunedScopeTerminal = {
|
||||
block.terminal = {
|
||||
kind: 'pruned-scope',
|
||||
block: terminal.block,
|
||||
dependencies: terminal.dependencies,
|
||||
fallthrough: terminal.fallthrough,
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
scope: terminal.scope,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
} as PrunedScopeTerminal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
|
||||
scope = {
|
||||
id: fn.env.nextScopeId,
|
||||
range: identifier.mutableRange,
|
||||
dependencies: [],
|
||||
dependencies: new Set(),
|
||||
declarations: new Map(),
|
||||
reassignments: new Set(),
|
||||
earlyReturnValue: null,
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
BuiltInJsxId,
|
||||
BuiltInObjectId,
|
||||
} from '../HIR/ObjectShape';
|
||||
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
|
||||
import {eachInstructionLValue} from '../HIR/visitors';
|
||||
import {assertExhaustive, Iterable_some} from '../Utils/utils';
|
||||
import {printReactiveScopeSummary} from './PrintReactiveFunction';
|
||||
@@ -120,29 +119,21 @@ class FindLastUsageVisitor extends ReactiveFunctionVisitor<void> {
|
||||
|
||||
class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | null> {
|
||||
lastUsage: Map<DeclarationId, InstructionId>;
|
||||
cache: Map<ReactiveScopeBlock, ReactiveScopeDependencies> = new Map();
|
||||
|
||||
constructor(lastUsage: Map<DeclarationId, InstructionId>) {
|
||||
super();
|
||||
this.lastUsage = lastUsage;
|
||||
}
|
||||
|
||||
dependency(scopeBlock: ReactiveScopeBlock): ReactiveScopeDependencies {
|
||||
let dependencies = this.cache.get(scopeBlock);
|
||||
if (dependencies == null) {
|
||||
dependencies = new Set(readScopeDependenciesRHIR(scopeBlock).values());
|
||||
this.cache.set(scopeBlock, dependencies);
|
||||
}
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
override transformScope(
|
||||
scopeBlock: ReactiveScopeBlock,
|
||||
state: ReactiveScopeDependencies | null,
|
||||
): Transformed<ReactiveStatement> {
|
||||
const dependencies = this.dependency(scopeBlock);
|
||||
this.visitScope(scopeBlock, dependencies);
|
||||
if (state !== null && areEqualDependencies(state, dependencies)) {
|
||||
this.visitScope(scopeBlock, scopeBlock.scope.dependencies);
|
||||
if (
|
||||
state !== null &&
|
||||
areEqualDependencies(state, scopeBlock.scope.dependencies)
|
||||
) {
|
||||
return {kind: 'replace-many', value: scopeBlock.instructions};
|
||||
} else {
|
||||
return {kind: 'keep'};
|
||||
@@ -269,7 +260,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
case 'scope': {
|
||||
if (
|
||||
current !== null &&
|
||||
canMergeScopes(this, current.block, instr) &&
|
||||
canMergeScopes(current.block, instr) &&
|
||||
areLValuesLastUsedByScope(
|
||||
instr.scope,
|
||||
current.lvalues,
|
||||
@@ -433,7 +424,6 @@ function areLValuesLastUsedByScope(
|
||||
}
|
||||
|
||||
function canMergeScopes(
|
||||
transform: Transform,
|
||||
current: ReactiveScopeBlock,
|
||||
next: ReactiveScopeBlock,
|
||||
): boolean {
|
||||
@@ -445,10 +435,10 @@ function canMergeScopes(
|
||||
log(` cannot merge, has reassignments`);
|
||||
return false;
|
||||
}
|
||||
const currDeps = transform.dependency(current);
|
||||
const nextDeps = transform.dependency(next);
|
||||
// Merge scopes whose dependencies are identical
|
||||
if (areEqualDependencies(currDeps, nextDeps)) {
|
||||
if (
|
||||
areEqualDependencies(current.scope.dependencies, next.scope.dependencies)
|
||||
) {
|
||||
log(` canMergeScopes: dependencies are equal`);
|
||||
return true;
|
||||
}
|
||||
@@ -466,14 +456,13 @@ function canMergeScopes(
|
||||
new Set(
|
||||
[...current.scope.declarations.values()].map(declaration => ({
|
||||
identifier: declaration.identifier,
|
||||
reactive: false,
|
||||
path: [],
|
||||
})),
|
||||
),
|
||||
nextDeps,
|
||||
next.scope.dependencies,
|
||||
) ||
|
||||
(nextDeps.size !== 0 &&
|
||||
[...nextDeps].every(
|
||||
(next.scope.dependencies.size !== 0 &&
|
||||
[...next.scope.dependencies].every(
|
||||
dep =>
|
||||
isAlwaysInvalidatingType(dep.identifier.type) &&
|
||||
Iterable_some(
|
||||
@@ -548,7 +537,7 @@ function areEqualDependencies(
|
||||
* *never* change and it's also eligible for merging.
|
||||
*/
|
||||
function scopeIsEligibleForMerging(scopeBlock: ReactiveScopeBlock): boolean {
|
||||
if (scopeBlock.scope.dependencies.length === 0) {
|
||||
if (scopeBlock.scope.dependencies.size === 0) {
|
||||
/*
|
||||
* Regardless of the type of value produced, if the scope has no dependencies
|
||||
* then its value will never change.
|
||||
|
||||
@@ -64,7 +64,11 @@ export function printReactiveScopeSummary(scope: ReactiveScope): string {
|
||||
items.push('scope');
|
||||
items.push(`@${scope.id}`);
|
||||
items.push(`[${scope.range.start}:${scope.range.end}]`);
|
||||
items.push(`dependencies=[${scope.dependencies.map(printPlace).join(',')}]`);
|
||||
items.push(
|
||||
`dependencies=[${Array.from(scope.dependencies)
|
||||
.map(dep => printDependency(dep))
|
||||
.join(', ')}]`,
|
||||
);
|
||||
items.push(
|
||||
`declarations=[${Array.from(scope.declarations)
|
||||
.map(([, decl]) =>
|
||||
@@ -92,8 +96,6 @@ export function writeReactiveBlock(
|
||||
block: ReactiveScopeBlock,
|
||||
): void {
|
||||
writer.writeLine(`${printReactiveScopeSummary(block.scope)} {`);
|
||||
writeReactiveInstructions(writer, block.dependencyInstructions);
|
||||
writer.writeLine('} /* - end dependencies - */ {');
|
||||
writeReactiveInstructions(writer, block.instructions);
|
||||
writer.writeLine('}');
|
||||
}
|
||||
|
||||
@@ -26,13 +26,18 @@ import {
|
||||
} from '../HIR/HIR';
|
||||
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
|
||||
import {scopeDependenciesDCE} from '../HIR/ScopeDependencyUtils';
|
||||
|
||||
/**
|
||||
* Phase 2: Promote identifiers which are used in a place that requires a named variable.
|
||||
*/
|
||||
class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void {
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
const {identifier} = dep;
|
||||
if (identifier.name == null) {
|
||||
promoteIdentifier(identifier, state);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* This is technically optional. We could prune ReactiveScopes
|
||||
* whose outputs are not used in another computation or return
|
||||
@@ -45,29 +50,7 @@ class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
promoteIdentifier(declaration.identifier, state);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Run DCE to remove dependency instructions that are no longer used due to
|
||||
* pruning passes.
|
||||
*/
|
||||
scopeDependenciesDCE(scopeBlock);
|
||||
/**
|
||||
* Traverse into scope dependency instructions to promote references to
|
||||
* unnamed outer identifiers.
|
||||
*/
|
||||
CompilerError.invariant(state.scopeDepContext == null, {
|
||||
reason: 'PromoteTemporaries: unexpected nested scopeDepContext',
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
this.visitBlock(scopeBlock.dependencyInstructions, {
|
||||
...state,
|
||||
scopeDepContext: {
|
||||
declarations: new Set(),
|
||||
dependencies: new Set(
|
||||
[...scopeBlock.scope.dependencies].map(dep => dep.identifier),
|
||||
),
|
||||
},
|
||||
});
|
||||
this.visitBlock(scopeBlock.instructions, state);
|
||||
this.traverseScope(scopeBlock, state);
|
||||
}
|
||||
|
||||
override visitPrunedScope(
|
||||
@@ -92,33 +75,11 @@ class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
}
|
||||
}
|
||||
|
||||
override visitLValue(_id: InstructionId, lvalue: Place, state: State): void {
|
||||
// Track lvalues from within scope dependency blocks to avoid promoting them.
|
||||
state.scopeDepContext?.declarations.add(lvalue.identifier.declarationId);
|
||||
}
|
||||
|
||||
override visitValue(
|
||||
id: InstructionId,
|
||||
value: ReactiveValue,
|
||||
state: State,
|
||||
): void {
|
||||
if (
|
||||
state.scopeDepContext &&
|
||||
(value.kind === 'LoadLocal' || value.kind === 'LoadContext')
|
||||
) {
|
||||
/**
|
||||
* Scope dependency LoadLocal sources (defined by instructions external to
|
||||
* that scope) should be promoted, as scope boundaries represent
|
||||
* re-ordering barriers
|
||||
*/
|
||||
const identifier = value.place.identifier;
|
||||
if (
|
||||
!state.scopeDepContext.declarations.has(identifier.declarationId) &&
|
||||
identifier.name === null
|
||||
) {
|
||||
promoteIdentifier(identifier, state);
|
||||
}
|
||||
}
|
||||
this.traverseValue(id, value, state);
|
||||
if (value.kind === 'FunctionExpression' || value.kind === 'ObjectMethod') {
|
||||
this.visitHirFunction(value.loweredFunc.func, state);
|
||||
@@ -216,10 +177,6 @@ type State = {
|
||||
DeclarationId,
|
||||
{activeScopes: Array<ScopeId>; usedOutsideScope: boolean}
|
||||
>; // true if referenced within another scope, false if only accessed outside of scopes
|
||||
scopeDepContext: {
|
||||
declarations: Set<DeclarationId>;
|
||||
dependencies: ReadonlySet<Identifier>;
|
||||
} | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -470,7 +427,6 @@ export function promoteUsedTemporaries(fn: ReactiveFunction): void {
|
||||
tags: new Set(),
|
||||
promoted: new Set(),
|
||||
pruned: new Map(),
|
||||
scopeDepContext: null,
|
||||
};
|
||||
visitReactiveFunction(fn, new CollectPromotableTemporaries(), state);
|
||||
for (const operand of fn.params) {
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
ReactiveScopeBlock,
|
||||
ReactiveStatement,
|
||||
} from '../HIR';
|
||||
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
|
||||
|
||||
/**
|
||||
* Some instructions will *always* produce a new value, and unless memoized will *always*
|
||||
@@ -88,8 +87,7 @@ class Transform extends ReactiveFunctionTransform<boolean> {
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.visitScope(scopeBlock, true);
|
||||
|
||||
const scopeDeps = readScopeDependenciesRHIR(scopeBlock);
|
||||
for (const [, dep] of scopeDeps) {
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
if (this.unmemoizedValues.has(dep.identifier)) {
|
||||
/*
|
||||
* This scope depends on an always-invalidating value so the scope will always invalidate:
|
||||
@@ -109,7 +107,6 @@ class Transform extends ReactiveFunctionTransform<boolean> {
|
||||
kind: 'replace',
|
||||
value: {
|
||||
kind: 'pruned-scope',
|
||||
dependencyInstructions: scopeBlock.dependencyInstructions,
|
||||
scope: scopeBlock.scope,
|
||||
instructions: scopeBlock.instructions,
|
||||
},
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
isUseRefType,
|
||||
isUseStateType,
|
||||
} from '../HIR';
|
||||
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
|
||||
import {eachCallArgument, eachInstructionLValue} from '../HIR/visitors';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
@@ -179,22 +178,14 @@ class Visitor extends ReactiveFunctionVisitor<CreateUpdate> {
|
||||
].map(id => this.map.get(id) ?? 'Unknown'),
|
||||
);
|
||||
super.visitScope(scope, state);
|
||||
// TODO
|
||||
const scopeDeps = readScopeDependenciesRHIR(scope);
|
||||
[...scopeDeps].forEach(([place, dep]) => {
|
||||
[...scope.scope.dependencies].forEach(ident => {
|
||||
let target: undefined | IdentifierId =
|
||||
this.aliases.find(dep.identifier.id) ?? dep.identifier.id;
|
||||
dep.path.forEach(token => {
|
||||
this.aliases.find(ident.identifier.id) ?? ident.identifier.id;
|
||||
ident.path.forEach(token => {
|
||||
target &&= this.paths.get(target)?.get(token.property);
|
||||
});
|
||||
if (target && this.map.get(target) === 'Create') {
|
||||
const idx = scope.scope.dependencies.indexOf(place);
|
||||
CompilerError.invariant(idx !== -1, {
|
||||
reason: 'Expected dependency to be found',
|
||||
loc: place.loc,
|
||||
});
|
||||
|
||||
scope.scope.dependencies.splice(idx, 1);
|
||||
scope.scope.dependencies.delete(ident);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,19 +95,13 @@ class Visitor extends ReactiveFunctionVisitor<ReactiveIdentifiers> {
|
||||
state: ReactiveIdentifiers,
|
||||
): void {
|
||||
this.traverseScope(scopeBlock, state);
|
||||
let j = 0;
|
||||
for (let i = 0; i < scopeBlock.scope.dependencies.length; i++) {
|
||||
const isReactive = state.has(
|
||||
scopeBlock.scope.dependencies[i].identifier.id,
|
||||
);
|
||||
if (isReactive) {
|
||||
scopeBlock.scope.dependencies[j] = scopeBlock.scope.dependencies[i];
|
||||
j++;
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
const isReactive = state.has(dep.identifier.id);
|
||||
if (!isReactive) {
|
||||
scopeBlock.scope.dependencies.delete(dep);
|
||||
}
|
||||
}
|
||||
scopeBlock.scope.dependencies.length = j;
|
||||
|
||||
if (scopeBlock.scope.dependencies.length !== 0) {
|
||||
if (scopeBlock.scope.dependencies.size !== 0) {
|
||||
/**
|
||||
* If any of a scope's dependencies are reactive, then all of its
|
||||
* outputs will re-evaluate whenever those dependencies change.
|
||||
|
||||
@@ -56,7 +56,6 @@ class Transform extends ReactiveFunctionTransform<State> {
|
||||
value: {
|
||||
kind: 'pruned-scope',
|
||||
scope: scopeBlock.scope,
|
||||
dependencyInstructions: scopeBlock.dependencyInstructions,
|
||||
instructions: scopeBlock.instructions,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
HIRFunction,
|
||||
InstructionId,
|
||||
@@ -186,20 +185,6 @@ export class ReactiveFunctionVisitor<TState = void> {
|
||||
this.traverseScope(scope, state);
|
||||
}
|
||||
traverseScope(scope: ReactiveScopeBlock, state: TState): void {
|
||||
this.visitBlock(scope.dependencyInstructions, state);
|
||||
const lastDependencyInstruction = scope.dependencyInstructions.at(-1);
|
||||
let lastInstructionId: InstructionId | null = null;
|
||||
if (lastDependencyInstruction !== undefined) {
|
||||
lastInstructionId = lastDependencyInstruction.instruction.id;
|
||||
}
|
||||
for (const dep of scope.scope.dependencies) {
|
||||
CompilerError.invariant(lastInstructionId !== null, {
|
||||
reason:
|
||||
'[ReactiveFunction] Expected at least one dependency instruction.',
|
||||
loc: scope.scope.loc,
|
||||
});
|
||||
this.visitPlace(lastInstructionId, dep, state);
|
||||
}
|
||||
this.visitBlock(scope.instructions, state);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
isUseInsertionEffectHookType,
|
||||
isUseLayoutEffectHookType,
|
||||
} from '../HIR';
|
||||
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {
|
||||
ReactiveFunctionVisitor,
|
||||
@@ -74,7 +73,7 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
|
||||
* memoized, allowing a transitive memoization check.
|
||||
*/
|
||||
let areDependenciesMemoized = true;
|
||||
for (const [, dep] of readScopeDependenciesRHIR(scopeBlock)) {
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
if (isUnmemoized(dep.identifier, this.scopes)) {
|
||||
areDependenciesMemoized = false;
|
||||
break;
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
|
||||
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {collectMaybeMemoDependencies} from '../Inference/DropManualMemoization';
|
||||
import {
|
||||
@@ -407,8 +406,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
state.manualMemoState != null &&
|
||||
state.manualMemoState.depsFromSource != null
|
||||
) {
|
||||
const deps = readScopeDependenciesRHIR(scopeBlock).values();
|
||||
for (const dep of deps) {
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
validateInferredDep(
|
||||
dep,
|
||||
this.temporaries,
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useRef, useEffect} from 'react';
|
||||
import {print, mutate} from 'shared-runtime';
|
||||
|
||||
function Component({cond}) {
|
||||
const arr = useRef([]);
|
||||
const other = useRef([]);
|
||||
// Although arr and other are both stable, derived is not
|
||||
const derived = cond ? arr : other;
|
||||
useEffect(() => {
|
||||
mutate(derived.current);
|
||||
print(derived.current);
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useRef, useEffect } from "react";
|
||||
import { print, mutate } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const { cond } = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = [];
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const arr = useRef(t1);
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const other = useRef(t2);
|
||||
|
||||
const derived = cond ? arr : other;
|
||||
let t3;
|
||||
if ($[2] !== derived) {
|
||||
t3 = () => {
|
||||
mutate(derived.current);
|
||||
print(derived.current);
|
||||
};
|
||||
$[2] = derived;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
useEffect(t3, [derived]);
|
||||
return arr;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,15 +0,0 @@
|
||||
// @inferEffectDependencies
|
||||
import {useRef, useEffect} from 'react';
|
||||
import {print, mutate} from 'shared-runtime';
|
||||
|
||||
function Component({cond}) {
|
||||
const arr = useRef([]);
|
||||
const other = useRef([]);
|
||||
// Although arr and other are both stable, derived is not
|
||||
const derived = cond ? arr : other;
|
||||
useEffect(() => {
|
||||
mutate(derived.current);
|
||||
print(derived.current);
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
@@ -83,10 +83,10 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
import { c as _c2 } from "react/compiler-runtime"; // @inlineJsxTransform
|
||||
|
||||
function Parent(t0) {
|
||||
const $ = _c2(3);
|
||||
const $ = _c2(2);
|
||||
const { children, ref } = t0;
|
||||
let t1;
|
||||
if ($[0] !== children || $[1] !== ref) {
|
||||
if ($[0] !== children) {
|
||||
if (DEV) {
|
||||
t1 = <div ref={ref}>{children}</div>;
|
||||
} else {
|
||||
@@ -99,10 +99,9 @@ function Parent(t0) {
|
||||
};
|
||||
}
|
||||
$[0] = children;
|
||||
$[1] = ref;
|
||||
$[2] = t1;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import Theme, {ThemeToggleButton} from './Theme';
|
||||
|
||||
import './Chrome.css';
|
||||
|
||||
import LargeContent from './LargeContent';
|
||||
|
||||
export default class Chrome extends Component {
|
||||
state = {theme: 'light'};
|
||||
render() {
|
||||
@@ -27,6 +25,7 @@ export default class Chrome extends Component {
|
||||
/>
|
||||
<Suspense fallback="Loading...">
|
||||
<Theme.Provider value={this.state.theme}>
|
||||
{this.props.children}
|
||||
<div>
|
||||
<ThemeToggleButton
|
||||
onChange={theme => {
|
||||
@@ -36,14 +35,9 @@ export default class Chrome extends Component {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{this.props.children}
|
||||
</Theme.Provider>
|
||||
</Suspense>
|
||||
<p>This should appear in the first paint.</p>
|
||||
<Suspense fallback="Loading...">
|
||||
<p>This content should not block paint.</p>
|
||||
<LargeContent />
|
||||
</Suspense>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `assetManifest = ${JSON.stringify(assets)};`,
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
import React, {Fragment} from 'react';
|
||||
|
||||
export default function LargeContent() {
|
||||
return (
|
||||
<Fragment>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris
|
||||
porttitor tortor ac lectus faucibus, eget eleifend elit hendrerit.
|
||||
Integer porttitor nisi in leo congue rutrum. Morbi sed ante posuere,
|
||||
aliquam lorem ac, imperdiet orci. Duis malesuada gravida pharetra. Cras
|
||||
facilisis arcu diam, id dictum lorem imperdiet a. Suspendisse aliquet
|
||||
tempus tortor et ultricies. Aliquam libero velit, posuere tempus ante
|
||||
sed, pellentesque tincidunt lorem. Nullam iaculis, eros a varius
|
||||
aliquet, tortor felis tempor metus, nec cursus felis eros aliquam nulla.
|
||||
Vivamus ut orci sed mauris congue lacinia. Cras eget blandit neque.
|
||||
Pellentesque a massa in turpis ullamcorper volutpat vel at massa. Sed
|
||||
ante est, auctor non diam non, vulputate ultrices metus. Maecenas dictum
|
||||
fermentum quam id aliquam. Donec porta risus vitae pretium posuere.
|
||||
Fusce facilisis eros in lacus tincidunt congue.
|
||||
</p>
|
||||
<p>
|
||||
Pellentesque habitant morbi tristique senectus et netus et malesuada
|
||||
fames ac turpis egestas. Phasellus dolor ante, iaculis vel nisl vitae,
|
||||
ornare ornare orci. Praesent sit amet lobortis sapien. Suspendisse
|
||||
pharetra posuere libero ut dapibus. Donec condimentum ante urna. Aliquam
|
||||
laoreet tincidunt lacus, sed interdum tortor dapibus elementum. Nam sed
|
||||
faucibus lorem. Suspendisse finibus, velit sed molestie finibus, risus
|
||||
purus mollis ante, sit amet aliquet sapien nulla ut nibh. In eget ligula
|
||||
metus. Duis in purus mattis, blandit magna nec, dictum nunc.
|
||||
</p>
|
||||
<p>
|
||||
Sed convallis magna id tortor blandit dictum. Suspendisse in porttitor
|
||||
neque. Integer quis metus consequat, rutrum est sit amet, finibus justo.
|
||||
In hac habitasse platea dictumst. Nullam sagittis, risus sed vehicula
|
||||
porta, sapien elit ultrices nibh, vel luctus odio tortor et ante. Sed
|
||||
porta enim in hendrerit tristique. Pellentesque id feugiat libero, sit
|
||||
amet tempor enim. Proin gravida nisl justo, vel ornare dolor bibendum
|
||||
ac. Mauris scelerisque mattis facilisis. Praesent sodales augue mollis
|
||||
orci vulputate aliquet. Mauris molestie luctus neque, sed congue elit
|
||||
congue ut. Cras quis tortor augue. In auctor nulla vel turpis dapibus
|
||||
egestas. Phasellus consequat rhoncus nisi sed dignissim. Quisque varius
|
||||
justo non ex lobortis finibus cursus nec justo. Nulla erat neque,
|
||||
commodo et sem convallis, tristique faucibus odio.
|
||||
</p>
|
||||
<p>
|
||||
Ut condimentum volutpat sem, id accumsan augue placerat vel. Donec ac
|
||||
efficitur turpis. Suspendisse pretium odio euismod sapien bibendum, sed
|
||||
tempus est condimentum. Etiam nisl magna, consequat at ullamcorper at,
|
||||
sollicitudin eu eros. In mattis ligula arcu. Sed eu consectetur turpis,
|
||||
id molestie ligula. Vestibulum et venenatis enim. Donec condimentum
|
||||
vitae nisi et placerat. Sed fringilla vehicula egestas. Proin
|
||||
consectetur, nibh non ornare scelerisque, diam lorem cursus lectus, ut
|
||||
mattis mauris purus id mi. Curabitur non ligula sit amet augue molestie
|
||||
vulputate. Donec maximus magna at volutpat aliquet. Pellentesque
|
||||
dignissim nulla eget odio eleifend tincidunt. Etiam diam lorem, ornare
|
||||
vel scelerisque vel, iaculis id risus. Donec aliquet aliquam felis, ac
|
||||
vehicula lacus suscipit vitae. Morbi eu ligula elit.
|
||||
</p>
|
||||
<p>
|
||||
Praesent pellentesque, libero ut faucibus tempor, purus elit consequat
|
||||
metus, in ornare nulla lectus at erat. Duis quis blandit turpis. Fusce
|
||||
at ligula rutrum metus molestie tempor sit amet eu justo. Maecenas
|
||||
tincidunt nisl nunc. Morbi ac metus tempor, pretium arcu vel, dapibus
|
||||
velit. Nulla convallis ligula at porta mollis. Duis magna ante, mollis
|
||||
eget nibh in, congue tempor dolor. Sed tincidunt sagittis arcu, in
|
||||
ultricies neque tempor non. Suspendisse eget nunc neque. Nulla sit amet
|
||||
odio volutpat, maximus purus id, dictum metus. Integer consequat, orci
|
||||
nec ullamcorper porta, mauris libero vestibulum ipsum, nec tempor tellus
|
||||
enim non nunc. Quisque nisl risus, dapibus sit amet purus nec, aliquam
|
||||
finibus metus. Nullam condimentum urna viverra finibus cursus. Proin et
|
||||
sollicitudin tellus, porta fermentum felis. Maecenas ac turpis sed dui
|
||||
condimentum interdum sed sed erat. Mauris ut dignissim erat.
|
||||
</p>
|
||||
<p>
|
||||
Proin varius porta dui, id fringilla elit lobortis eget. Integer at
|
||||
metus elementum, efficitur eros id, euismod est. Morbi vestibulum nibh
|
||||
ac leo luctus sagittis. Praesent rhoncus, risus sit amet mattis dictum,
|
||||
diam sapien tempor neque, vel dignissim nulla neque eget ex. Nam
|
||||
sollicitudin metus quis ullamcorper dapibus. Nam tristique euismod
|
||||
efficitur. Pellentesque rhoncus vel sem eget lacinia. Pellentesque
|
||||
volutpat velit ac dignissim luctus. Vivamus euismod tortor at ligula
|
||||
mattis porta. Vestibulum ante ipsum primis in faucibus orci luctus et
|
||||
ultrices posuere cubilia curae;
|
||||
</p>
|
||||
<p>
|
||||
Proin blandit vulputate efficitur. Pellentesque sit amet porta odio.
|
||||
Nunc pulvinar varius rhoncus. Mauris fermentum leo a imperdiet pretium.
|
||||
Mauris scelerisque justo vel ante egestas, eget tempus neque malesuada.
|
||||
Sed dictum ex vel justo dignissim, aliquam commodo diam rutrum. Integer
|
||||
dignissim est ullamcorper augue laoreet consectetur id at diam. Vivamus
|
||||
molestie blandit urna, eget pulvinar augue dictum vestibulum. Duis
|
||||
maximus bibendum mauris, ut ultricies elit rhoncus eu. Praesent gravida
|
||||
placerat mauris. Praesent tempor ipsum at nibh rhoncus sagittis. Duis
|
||||
non sem turpis. Quisque et metus leo. Sed eu purus lorem. Pellentesque
|
||||
dictum metus sed leo viverra interdum. Maecenas vel tincidunt mi.
|
||||
</p>
|
||||
<p>
|
||||
Praesent consequat dapibus pellentesque. Fusce at enim id mauris laoreet
|
||||
commodo. Nullam ut mauris euismod, rhoncus tellus vel, facilisis diam.
|
||||
Aenean porta faucibus augue, a iaculis massa iaculis in. Praesent vel
|
||||
metus purus. Etiam quis augue eget orci lobortis eleifend ac ut lorem.
|
||||
Aenean non orci quis nisi molestie maximus. Mauris interdum, eros et
|
||||
aliquam aliquam, lectus diam pharetra velit, in condimentum odio eros
|
||||
non quam. Praesent bibendum pretium turpis vitae tristique. Mauris
|
||||
convallis, massa ut fermentum fermentum, libero orci tempus ipsum,
|
||||
malesuada ultrices metus sapien placerat lectus. Ut fringilla arcu nec
|
||||
lorem ultrices mattis. Etiam id tortor feugiat magna gravida gravida.
|
||||
Morbi aliquam, mi ac pellentesque mattis, erat ex venenatis erat, a
|
||||
vestibulum eros turpis quis metus. Pellentesque tempus justo in ligula
|
||||
ultricies porta. Phasellus congue felis sit amet dolor tristique
|
||||
finibus. Nunc eget eros non est ultricies vestibulum.
|
||||
</p>
|
||||
<p>
|
||||
Donec efficitur ligula quis odio tincidunt tristique. Duis urna dolor,
|
||||
hendrerit quis enim at, accumsan auctor turpis. Vivamus ante lorem,
|
||||
maximus vitae suscipit ut, congue eget velit. Maecenas sed ligula erat.
|
||||
Aliquam mollis purus at nisi porta suscipit in ut magna. Vivamus a
|
||||
turpis nec tellus egestas suscipit nec ornare nisi. Donec vestibulum
|
||||
libero quis ex suscipit, sit amet luctus leo gravida.
|
||||
</p>
|
||||
<p>
|
||||
Praesent pharetra dolor elit, sed volutpat lorem rhoncus non. Etiam a
|
||||
neque ut velit dignissim sodales. Vestibulum neque risus, condimentum
|
||||
nec consectetur vitae, ultricies ut sapien. Integer iaculis at urna sit
|
||||
amet malesuada. Integer tincidunt, felis ac vulputate semper, velit leo
|
||||
facilisis lorem, quis aliquet leo dui id lorem. Morbi non quam quis nisl
|
||||
sagittis consequat nec vitae libero. Nunc molestie pretium libero, eu
|
||||
eleifend nibh feugiat sed. Ut in bibendum diam, sit amet vehicula risus.
|
||||
Nam ornare ac nisi ac euismod. Nullam id egestas nulla. Etiam porta
|
||||
commodo ante sit amet pellentesque. Suspendisse eleifend purus in urna
|
||||
euismod auctor non vel nisi. Suspendisse rutrum est nunc, sit amet
|
||||
lacinia lacus dictum eget. Pellentesque habitant morbi tristique
|
||||
senectus et netus et malesuada fames ac turpis egestas. Morbi a blandit
|
||||
diam.
|
||||
</p>
|
||||
<p>
|
||||
Donec eget efficitur sapien. Suspendisse diam lacus, varius eu interdum
|
||||
et, congue ac justo. Proin ipsum odio, suscipit elementum mauris sed,
|
||||
porttitor congue est. Cras dapibus dictum ante, vitae gravida elit
|
||||
venenatis sed. Sed massa sem, posuere ut enim sit amet, vestibulum
|
||||
condimentum nibh. Pellentesque pulvinar sodales lacinia. Proin id
|
||||
pretium sapien, non convallis nulla. In mollis tincidunt sem et
|
||||
porttitor.
|
||||
</p>
|
||||
<p>
|
||||
Integer at sollicitudin sem. Suspendisse sed semper orci. Nulla at nibh
|
||||
nec risus suscipit posuere egestas vitae enim. Nullam mauris justo,
|
||||
mattis vel laoreet non, finibus nec nisl. Cras iaculis ultrices nibh,
|
||||
non commodo eros aliquam non. Sed vitae mollis dui, at maximus metus. Ut
|
||||
vestibulum, enim ut lobortis vulputate, lorem urna congue elit, non
|
||||
dictum odio lorem eget velit. Morbi eleifend id ligula vitae vulputate.
|
||||
Suspendisse ac laoreet justo. Proin eu mattis diam.
|
||||
</p>
|
||||
<p>
|
||||
Nunc in ex quis enim ullamcorper scelerisque eget ac eros. Class aptent
|
||||
taciti sociosqu ad litora torquent per conubia nostra, per inceptos
|
||||
himenaeos. Aliquam turpis dui, egestas a rhoncus non, fermentum in
|
||||
tellus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices
|
||||
posuere cubilia curae; Aenean non risus arcu. Nam ultricies lacinia
|
||||
volutpat. Class aptent taciti sociosqu ad litora torquent per conubia
|
||||
nostra, per inceptos himenaeos. Lorem ipsum dolor sit amet, consectetur
|
||||
adipiscing elit.
|
||||
</p>
|
||||
<p>
|
||||
Aliquam a felis leo. Proin lorem ipsum, congue eu cursus in, rhoncus ut
|
||||
libero. Vestibulum sit amet consequat nunc. Ut eleifend lobortis lacus,
|
||||
vel molestie metus viverra eget. Nullam suscipit eu magna scelerisque
|
||||
suscipit. Donec dictum in diam nec lacinia. Mauris pellentesque ex ut
|
||||
purus facilisis, eget placerat turpis semper. Sed dapibus lorem ante, et
|
||||
malesuada dui eleifend ac. Sed diam felis, semper ac nulla vel, posuere
|
||||
ultricies ante.
|
||||
</p>
|
||||
<p>
|
||||
Nunc elementum odio sapien, sit amet vulputate lorem varius at. Fusce
|
||||
non sapien vitae lorem aliquam pretium sit amet congue dolor. Nunc quis
|
||||
tortor luctus, pretium ex a, tincidunt urna. Aliquam fermentum massa a
|
||||
erat pharetra varius. Curabitur at auctor dui. Sed posuere pellentesque
|
||||
massa, vel bibendum urna dictum non. Fusce eget rhoncus urna. Maecenas
|
||||
sed lectus tellus. Pellentesque convallis dapibus nisl vitae venenatis.
|
||||
Quisque ornare a dolor ac pharetra. Nam cursus, mi a lacinia accumsan,
|
||||
felis erat fringilla magna, ac mattis nunc ante a orci.
|
||||
</p>
|
||||
<p>
|
||||
Nunc vel tortor euismod, commodo tortor non, aliquam nisi. Maecenas
|
||||
tempus mollis velit non suscipit. Mauris sit amet dolor sed ex fringilla
|
||||
varius. Suspendisse vel cursus risus. Vivamus pharetra massa nec dolor
|
||||
aliquam feugiat. Fusce finibus enim commodo, scelerisque ante eu,
|
||||
laoreet ex. Curabitur placerat magna quis imperdiet lacinia. Etiam
|
||||
lectus mauris, porttitor ac lacinia sed, posuere eget lacus. Mauris
|
||||
vulputate mattis imperdiet. Nunc id aliquet libero, vitae hendrerit
|
||||
purus. Praesent vestibulum urna ac egestas tempor. In molestie, nunc sit
|
||||
amet sagittis dapibus, ligula enim fermentum mi, lacinia molestie eros
|
||||
dui in tortor. Mauris fermentum pulvinar faucibus. Curabitur laoreet
|
||||
eleifend purus, non tincidunt tortor gravida nec. Nam eu lectus congue,
|
||||
commodo libero et, porttitor est. Nullam tincidunt, nisi eu congue
|
||||
congue, magna justo commodo massa, nec efficitur dui lectus non sem.
|
||||
</p>
|
||||
<p>
|
||||
Nullam vehicula, ipsum quis lacinia tristique, elit nulla dignissim
|
||||
augue, at pulvinar metus justo ac magna. Nullam nec nunc ac sapien
|
||||
mollis cursus eu ac enim. Pellentesque a pharetra erat. Ut tempor magna
|
||||
nisi, accumsan blandit lectus volutpat nec. Vivamus vel lorem nec eros
|
||||
blandit dictum eget ac diam. Nulla nec turpis dolor. Morbi eu euismod
|
||||
libero. Nam ut tortor at arcu porta tincidunt. In gravida ligula
|
||||
fringilla ornare imperdiet. Nulla scelerisque ante erat, efficitur
|
||||
dictum metus ullamcorper vel. Nam ac purus metus. Maecenas eget tempus
|
||||
nulla. Ut magna lorem, efficitur ut ex a, semper aliquam magna. Praesent
|
||||
lobortis, velit ac posuere mattis, justo est accumsan turpis, id
|
||||
sagittis felis mi in lacus.
|
||||
</p>
|
||||
<p>
|
||||
Aenean est mi, semper nec sem at, malesuada consectetur nunc. Aenean
|
||||
consequat sem quis sem consequat, non aliquam est placerat. Cras
|
||||
malesuada magna neque, et pellentesque nibh consequat at. Sed interdum
|
||||
velit et ex interdum, vel lobortis ante vestibulum. Nam placerat lectus
|
||||
eu commodo efficitur. Pellentesque in nunc ac massa porttitor eleifend
|
||||
ut efficitur sem. Aenean at magna auctor, posuere augue in, ultrices
|
||||
arcu. Praesent dignissim augue ex, malesuada maximus metus interdum a.
|
||||
Proin nec odio in nulla vestibulum.
|
||||
</p>
|
||||
<p>
|
||||
Aenean est mi, semper nec sem at, malesuada consectetur nunc. Aenean
|
||||
consequat sem quis sem consequat, non aliquam est placerat. Cras
|
||||
malesuada magna neque, et pellentesque nibh consequat at. Sed interdum
|
||||
velit et ex interdum, vel lobortis ante vestibulum. Nam placerat lectus
|
||||
eu commodo efficitur. Pellentesque in nunc ac massa porttitor eleifend
|
||||
ut efficitur sem. Aenean at magna auctor, posuere augue in, ultrices
|
||||
arcu. Praesent dignissim augue ex, malesuada maximus metus interdum a.
|
||||
Proin nec odio in nulla vestibulum.
|
||||
</p>
|
||||
<p>
|
||||
Aenean est mi, semper nec sem at, malesuada consectetur nunc. Aenean
|
||||
consequat sem quis sem consequat, non aliquam est placerat. Cras
|
||||
malesuada magna neque, et pellentesque nibh consequat at. Sed interdum
|
||||
velit et ex interdum, vel lobortis ante vestibulum. Nam placerat lectus
|
||||
eu commodo efficitur. Pellentesque in nunc ac massa porttitor eleifend
|
||||
ut efficitur sem. Aenean at magna auctor, posuere augue in, ultrices
|
||||
arcu. Praesent dignissim augue ex, malesuada maximus metus interdum a.
|
||||
Proin nec odio in nulla vestibulum.
|
||||
</p>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -906,8 +906,8 @@ describe('InspectedElement', () => {
|
||||
},
|
||||
"usedRejectedPromise": {
|
||||
"reason": Dehydrated {
|
||||
"preview_short": Error: test-error-do-not-surface,
|
||||
"preview_long": Error: test-error-do-not-surface,
|
||||
"preview_short": Error,
|
||||
"preview_long": Error,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
64
packages/react-devtools-shared/src/hydration.js
vendored
64
packages/react-devtools-shared/src/hydration.js
vendored
@@ -397,7 +397,7 @@ export function dehydrate(
|
||||
return object;
|
||||
}
|
||||
|
||||
case 'class_instance': {
|
||||
case 'class_instance':
|
||||
isPathAllowedCheck = isPathAllowed(path);
|
||||
|
||||
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
|
||||
@@ -433,69 +433,7 @@ export function dehydrate(
|
||||
unserializable.push(path);
|
||||
|
||||
return value;
|
||||
}
|
||||
case 'error': {
|
||||
isPathAllowedCheck = isPathAllowed(path);
|
||||
|
||||
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
|
||||
return createDehydrated(type, true, data, cleaned, path);
|
||||
}
|
||||
|
||||
const value: Unserializable = {
|
||||
unserializable: true,
|
||||
type,
|
||||
readonly: true,
|
||||
preview_short: formatDataForPreview(data, false),
|
||||
preview_long: formatDataForPreview(data, true),
|
||||
name: data.name,
|
||||
};
|
||||
|
||||
// name, message, stack and cause are not enumerable yet still interesting.
|
||||
value.message = dehydrate(
|
||||
data.message,
|
||||
cleaned,
|
||||
unserializable,
|
||||
path.concat(['message']),
|
||||
isPathAllowed,
|
||||
isPathAllowedCheck ? 1 : level + 1,
|
||||
);
|
||||
value.stack = dehydrate(
|
||||
data.stack,
|
||||
cleaned,
|
||||
unserializable,
|
||||
path.concat(['stack']),
|
||||
isPathAllowed,
|
||||
isPathAllowedCheck ? 1 : level + 1,
|
||||
);
|
||||
|
||||
if ('cause' in data) {
|
||||
value.cause = dehydrate(
|
||||
data.cause,
|
||||
cleaned,
|
||||
unserializable,
|
||||
path.concat(['cause']),
|
||||
isPathAllowed,
|
||||
isPathAllowedCheck ? 1 : level + 1,
|
||||
);
|
||||
}
|
||||
|
||||
getAllEnumerableKeys(data).forEach(key => {
|
||||
const keyAsString = key.toString();
|
||||
|
||||
value[keyAsString] = dehydrate(
|
||||
data[key],
|
||||
cleaned,
|
||||
unserializable,
|
||||
path.concat([keyAsString]),
|
||||
isPathAllowed,
|
||||
isPathAllowedCheck ? 1 : level + 1,
|
||||
);
|
||||
});
|
||||
|
||||
unserializable.push(path);
|
||||
|
||||
return value;
|
||||
}
|
||||
case 'infinity':
|
||||
case 'nan':
|
||||
case 'undefined':
|
||||
|
||||
20
packages/react-devtools-shared/src/utils.js
vendored
20
packages/react-devtools-shared/src/utils.js
vendored
@@ -554,7 +554,6 @@ export type DataType =
|
||||
| 'class_instance'
|
||||
| 'data_view'
|
||||
| 'date'
|
||||
| 'error'
|
||||
| 'function'
|
||||
| 'html_all_collection'
|
||||
| 'html_element'
|
||||
@@ -574,21 +573,6 @@ export type DataType =
|
||||
| 'undefined'
|
||||
| 'unknown';
|
||||
|
||||
function isError(data: Object): boolean {
|
||||
// If it doesn't event look like an error, it won't be an actual error.
|
||||
if ('name' in data && 'message' in data) {
|
||||
while (data) {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
if (Object.prototype.toString.call(data) === '[object Error]') {
|
||||
return true;
|
||||
}
|
||||
data = Object.getPrototypeOf(data);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a enhanced/artificial type string based on the object instance
|
||||
*/
|
||||
@@ -650,8 +634,6 @@ export function getDataType(data: Object): DataType {
|
||||
return 'regexp';
|
||||
} else if (typeof data.then === 'function') {
|
||||
return 'thenable';
|
||||
} else if (isError(data)) {
|
||||
return 'error';
|
||||
} else {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const toStringValue = Object.prototype.toString.call(data);
|
||||
@@ -1014,8 +996,6 @@ export function formatDataForPreview(
|
||||
} else {
|
||||
return '{…}';
|
||||
}
|
||||
case 'error':
|
||||
return truncateForDisplay(String(data));
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'infinity':
|
||||
|
||||
@@ -130,14 +130,6 @@ const usedRejectedPromise = Promise.reject(
|
||||
new Error('test-error-do-not-surface'),
|
||||
);
|
||||
|
||||
class DigestError extends Error {
|
||||
digest: string;
|
||||
constructor(message: string, options: any, digest: string) {
|
||||
super(message, options);
|
||||
this.digest = digest;
|
||||
}
|
||||
}
|
||||
|
||||
export default function Hydration(): React.Node {
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -157,13 +149,6 @@ export default function Hydration(): React.Node {
|
||||
usedFulfilledRichPromise={usedFulfilledRichPromise}
|
||||
usedPendingPromise={usedPendingPromise}
|
||||
usedRejectedPromise={usedRejectedPromise}
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
error={new Error('test')}
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
errorWithCause={new Error('one', {cause: new TypeError('two')})}
|
||||
errorWithDigest={new DigestError('test', {}, 'some-digest')}
|
||||
// $FlowFixMe[cannot-resolve-name] Flow doesn't know about DOMException
|
||||
domexception={new DOMException('test')}
|
||||
/>
|
||||
<DeepHooks />
|
||||
</Fragment>
|
||||
|
||||
@@ -695,7 +695,6 @@ export function resetResumableState(
|
||||
resumableState.scriptResources = {};
|
||||
resumableState.moduleUnknownResources = {};
|
||||
resumableState.moduleScriptResources = {};
|
||||
resumableState.instructions = NothingSent; // Nothing was flushed so no instructions could've flushed.
|
||||
}
|
||||
|
||||
export function completeResumableState(resumableState: ResumableState): void {
|
||||
|
||||
@@ -219,13 +219,13 @@ function addNestedProperty(
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
if (enableFastAddPropertiesInDiffing) {
|
||||
return fastAddProperties(updatePayload, nextProp, validAttributes);
|
||||
}
|
||||
|
||||
if (!isArray(nextProp)) {
|
||||
// Add each property of the leaf.
|
||||
return slowAddProperties(updatePayload, nextProp, validAttributes);
|
||||
if (enableFastAddPropertiesInDiffing) {
|
||||
return fastAddProperties(updatePayload, nextProp, validAttributes);
|
||||
} else {
|
||||
return addProperties(updatePayload, nextProp, validAttributes);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < nextProp.length; i++) {
|
||||
@@ -516,7 +516,7 @@ function fastAddProperties(
|
||||
/**
|
||||
* addProperties adds all the valid props to the payload after being processed.
|
||||
*/
|
||||
function slowAddProperties(
|
||||
function addProperties(
|
||||
updatePayload: null | Object,
|
||||
props: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
|
||||
@@ -98,8 +98,6 @@ const ReactNoopServer = ReactFizzServer({
|
||||
closeWithError(destination: Destination, error: mixed): void {},
|
||||
flushBuffered(destination: Destination): void {},
|
||||
|
||||
byteLengthOfChunk: null,
|
||||
|
||||
getChildFormatContext(): null {
|
||||
return null;
|
||||
},
|
||||
|
||||
10
packages/react-reconciler/src/ReactFiberHooks.js
vendored
10
packages/react-reconciler/src/ReactFiberHooks.js
vendored
@@ -3355,16 +3355,8 @@ export function requestFormReset(formFiber: Fiber) {
|
||||
);
|
||||
}
|
||||
|
||||
let stateHook: Hook = ensureFormComponentIsStateful(formFiber);
|
||||
const stateHook = ensureFormComponentIsStateful(formFiber);
|
||||
const newResetState = {};
|
||||
if (stateHook.next === null) {
|
||||
// Hack alert. If formFiber is the workInProgress Fiber then
|
||||
// we might get a broken intermediate state. Try the alternate
|
||||
// instead.
|
||||
// TODO: We should really stash the Queue somewhere stateful
|
||||
// just like how setState binds the Queue.
|
||||
stateHook = (formFiber.alternate: any).memoizedState;
|
||||
}
|
||||
const resetStateHook: Hook = (stateHook.next: any);
|
||||
const resetStateQueue = resetStateHook.queue;
|
||||
dispatchSetStateInternal(
|
||||
|
||||
66
packages/react-server/src/ReactFizzServer.js
vendored
66
packages/react-server/src/ReactFizzServer.js
vendored
@@ -50,7 +50,6 @@ import {
|
||||
flushBuffered,
|
||||
close,
|
||||
closeWithError,
|
||||
byteLengthOfChunk,
|
||||
} from './ReactServerStreamConfig';
|
||||
import {
|
||||
writeCompletedRoot,
|
||||
@@ -349,7 +348,6 @@ export opaque type Request = {
|
||||
pendingRootTasks: number, // when this reaches zero, we've finished at least the root boundary.
|
||||
completedRootSegment: null | Segment, // Completed but not yet flushed root segments.
|
||||
completedPreambleSegments: null | Array<Array<Segment>>, // contains the ready-to-flush segments that make up the preamble
|
||||
byteSize: number, // counts the number of bytes accumulated in the shell
|
||||
abortableTasks: Set<Task>,
|
||||
pingedTasks: Array<Task>, // High priority tasks that should be worked on first.
|
||||
// Queues to flush in order of priority
|
||||
@@ -400,13 +398,6 @@ type Preamble = PreambleState;
|
||||
// 500 * 1024 / 8 * .8 * 0.5 / 2
|
||||
const DEFAULT_PROGRESSIVE_CHUNK_SIZE = 12800;
|
||||
|
||||
function isEligibleForOutlining(
|
||||
request: Request,
|
||||
boundary: SuspenseBoundary,
|
||||
): boolean {
|
||||
return boundary.byteSize > request.progressiveChunkSize;
|
||||
}
|
||||
|
||||
function defaultErrorHandler(error: mixed) {
|
||||
if (
|
||||
typeof error === 'object' &&
|
||||
@@ -456,7 +447,6 @@ function RequestInstance(
|
||||
this.pendingRootTasks = 0;
|
||||
this.completedRootSegment = null;
|
||||
this.completedPreambleSegments = null;
|
||||
this.byteSize = 0;
|
||||
this.abortableTasks = abortSet;
|
||||
this.pingedTasks = pingedTasks;
|
||||
this.clientRenderedBoundaries = ([]: Array<SuspenseBoundary>);
|
||||
@@ -1245,7 +1235,6 @@ function renderSuspenseBoundary(
|
||||
boundarySegment.textEmbedded,
|
||||
);
|
||||
boundarySegment.status = COMPLETED;
|
||||
finishedSegment(request, parentBoundary, boundarySegment);
|
||||
} catch (thrownValue: mixed) {
|
||||
if (request.status === ABORTING) {
|
||||
boundarySegment.status = ABORTED;
|
||||
@@ -1312,24 +1301,20 @@ function renderSuspenseBoundary(
|
||||
contentRootSegment.textEmbedded,
|
||||
);
|
||||
contentRootSegment.status = COMPLETED;
|
||||
finishedSegment(request, newBoundary, contentRootSegment);
|
||||
queueCompletedSegment(newBoundary, contentRootSegment);
|
||||
if (newBoundary.pendingTasks === 0 && newBoundary.status === PENDING) {
|
||||
// This must have been the last segment we were waiting on. This boundary is now complete.
|
||||
newBoundary.status = COMPLETED;
|
||||
// Therefore we won't need the fallback. We early return so that we don't have to create
|
||||
// the fallback. However, if this boundary ended up big enough to be eligible for outlining
|
||||
// we can't do that because we might still need the fallback if we outline it.
|
||||
if (!isEligibleForOutlining(request, newBoundary)) {
|
||||
if (request.pendingRootTasks === 0 && task.blockedPreamble) {
|
||||
// The root is complete and this boundary may contribute part of the preamble.
|
||||
// We eagerly attempt to prepare the preamble here because we expect most requests
|
||||
// to have few boundaries which contribute preambles and it allow us to do this
|
||||
// preparation work during the work phase rather than the when flushing.
|
||||
preparePreamble(request);
|
||||
}
|
||||
return;
|
||||
// the fallback.
|
||||
newBoundary.status = COMPLETED;
|
||||
if (request.pendingRootTasks === 0 && task.blockedPreamble) {
|
||||
// The root is complete and this boundary may contribute part of the preamble.
|
||||
// We eagerly attempt to prepare the preamble here because we expect most requests
|
||||
// to have few boundaries which contribute preambles and it allow us to do this
|
||||
// preparation work during the work phase rather than the when flushing.
|
||||
preparePreamble(request);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (thrownValue: mixed) {
|
||||
newBoundary.status = CLIENT_RENDERED;
|
||||
@@ -2466,7 +2451,6 @@ function resumeNode(
|
||||
renderTask.blockedSegment = resumedSegment;
|
||||
renderNode(request, task, node, childIndex);
|
||||
resumedSegment.status = COMPLETED;
|
||||
finishedSegment(request, blockedBoundary, resumedSegment);
|
||||
if (blockedBoundary === null) {
|
||||
request.completedRootSegment = resumedSegment;
|
||||
} else {
|
||||
@@ -4288,27 +4272,6 @@ function queueCompletedSegment(
|
||||
}
|
||||
}
|
||||
|
||||
function finishedSegment(
|
||||
request: Request,
|
||||
boundary: Root | SuspenseBoundary,
|
||||
segment: Segment,
|
||||
) {
|
||||
if (byteLengthOfChunk !== null) {
|
||||
// Count the bytes of all the chunks of this segment.
|
||||
const chunks = segment.chunks;
|
||||
let segmentByteSize = 0;
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
segmentByteSize += byteLengthOfChunk(chunks[i]);
|
||||
}
|
||||
// Accumulate on the parent boundary to power heuristics.
|
||||
if (boundary === null) {
|
||||
request.byteSize += segmentByteSize;
|
||||
} else {
|
||||
boundary.byteSize += segmentByteSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finishedTask(
|
||||
request: Request,
|
||||
boundary: Root | SuspenseBoundary,
|
||||
@@ -4355,13 +4318,9 @@ function finishedTask(
|
||||
// This needs to happen after we read the parentFlushed flags because aborting can finish
|
||||
// work which can trigger user code, which can start flushing, which can change those flags.
|
||||
// If the boundary was POSTPONED, we still need to finish the fallback first.
|
||||
// If the boundary is eligible to be outlined during flushing we can't cancel the fallback
|
||||
// since we might need it when it's being outlined.
|
||||
if (boundary.status === COMPLETED) {
|
||||
if (!isEligibleForOutlining(request, boundary)) {
|
||||
boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request);
|
||||
boundary.fallbackAbortableTasks.clear();
|
||||
}
|
||||
boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request);
|
||||
boundary.fallbackAbortableTasks.clear();
|
||||
|
||||
if (
|
||||
request.pendingRootTasks === 0 &&
|
||||
@@ -4459,7 +4418,6 @@ function retryRenderTask(
|
||||
|
||||
task.abortSet.delete(task);
|
||||
segment.status = COMPLETED;
|
||||
finishedSegment(request, task.blockedBoundary, segment);
|
||||
finishedTask(request, task.blockedBoundary, segment);
|
||||
} catch (thrownValue: mixed) {
|
||||
resetHooksState();
|
||||
@@ -4971,7 +4929,7 @@ function flushSegment(
|
||||
flushSubtree(request, destination, segment, hoistableState);
|
||||
|
||||
return writeEndPendingSuspenseBoundary(destination, request.renderState);
|
||||
} else if (isEligibleForOutlining(request, boundary)) {
|
||||
} else if (boundary.byteSize > request.progressiveChunkSize) {
|
||||
// This boundary is large and will be emitted separately so that we can progressively show
|
||||
// other content. We add it to the queue during the flush because we have to ensure that
|
||||
// the parent flushes first so that there's something to inject it into.
|
||||
|
||||
@@ -60,9 +60,9 @@ export function typedArrayToBinaryChunk(
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
export const byteLengthOfChunk:
|
||||
| null
|
||||
| ((chunk: Chunk | PrecomputedChunk) => number) = null;
|
||||
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
|
||||
throw new Error('Not implemented.');
|
||||
|
||||
Reference in New Issue
Block a user