Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50f8538911 | ||
|
|
1cb99cadd6 |
@@ -129,6 +129,7 @@ function run(
|
||||
mode,
|
||||
config,
|
||||
contextIdentifiers,
|
||||
func,
|
||||
logger,
|
||||
filename,
|
||||
code,
|
||||
|
||||
@@ -70,12 +70,14 @@ 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, parent ?? func, bindings, capturedRefs);
|
||||
const builder = new HIRBuilder(env, {
|
||||
bindings,
|
||||
context: capturedRefs,
|
||||
});
|
||||
const context: HIRFunction['context'] = [];
|
||||
|
||||
for (const ref of capturedRefs ?? []) {
|
||||
@@ -215,7 +217,7 @@ export function lower(
|
||||
return Ok({
|
||||
id,
|
||||
params,
|
||||
fnType: parent == null ? env.fnType : 'Other',
|
||||
fnType: bindings == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
returnType: makeType(),
|
||||
body: builder.build(),
|
||||
@@ -3417,7 +3419,7 @@ function lowerFunction(
|
||||
| t.ObjectMethod
|
||||
>,
|
||||
): LoweredFunction | null {
|
||||
const componentScope: Scope = builder.parentFunction.scope;
|
||||
const componentScope: Scope = builder.environment.parentFunction.scope;
|
||||
const capturedContext = gatherCapturedContext(expr, componentScope);
|
||||
|
||||
/*
|
||||
@@ -3428,13 +3430,10 @@ 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],
|
||||
builder.parentFunction,
|
||||
);
|
||||
const lowering = lower(expr, builder.environment, builder.bindings, [
|
||||
...builder.context,
|
||||
...capturedContext,
|
||||
]);
|
||||
let loweredFunc: HIRFunction;
|
||||
if (lowering.isErr()) {
|
||||
lowering
|
||||
@@ -3456,7 +3455,7 @@ function lowerExpressionToTemporary(
|
||||
return lowerValueToTemporary(builder, value);
|
||||
}
|
||||
|
||||
function lowerValueToTemporary(
|
||||
export function lowerValueToTemporary(
|
||||
builder: HIRBuilder,
|
||||
value: InstructionValue,
|
||||
): Place {
|
||||
|
||||
@@ -191,6 +191,7 @@ type TerminalRewriteInfo =
|
||||
| {
|
||||
kind: 'StartScope';
|
||||
blockId: BlockId;
|
||||
dependencyId: BlockId;
|
||||
fallthroughId: BlockId;
|
||||
instrId: InstructionId;
|
||||
scope: ReactiveScope;
|
||||
@@ -215,12 +216,14 @@ 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,
|
||||
});
|
||||
@@ -262,10 +265,13 @@ type RewriteContext = {
|
||||
* instr1, instr2, instr3, instr4, [[ original terminal ]]
|
||||
* Rewritten:
|
||||
* bb0:
|
||||
* instr1, [[ scope start block=bb1]]
|
||||
* instr1, [[ scope start dependencies=bb1 block=bb2]]
|
||||
* bb1:
|
||||
* instr2, instr3, [[ scope end goto=bb2 ]]
|
||||
* [[ empty, filled in in PropagateScopeDependenciesHIR ]]
|
||||
* goto bb2
|
||||
* bb2:
|
||||
* instr2, instr3, [[ scope end goto=bb3 ]]
|
||||
* bb3:
|
||||
* instr4, [[ original terminal ]]
|
||||
*/
|
||||
function handleRewrite(
|
||||
@@ -279,6 +285,7 @@ function handleRewrite(
|
||||
? {
|
||||
kind: 'scope',
|
||||
fallthrough: terminalInfo.fallthroughId,
|
||||
dependencies: terminalInfo.dependencyId,
|
||||
block: terminalInfo.blockId,
|
||||
scope: terminalInfo.scope,
|
||||
id: terminalInfo.instrId,
|
||||
@@ -305,7 +312,28 @@ function handleRewrite(
|
||||
context.nextPreds = new Set([currBlockId]);
|
||||
context.nextBlockId =
|
||||
terminalInfo.kind === 'StartScope'
|
||||
? terminalInfo.blockId
|
||||
? terminalInfo.dependencyId
|
||||
: 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,7 +241,10 @@ type PropertyPathNode =
|
||||
class PropertyPathRegistry {
|
||||
roots: Map<IdentifierId, RootNode> = new Map();
|
||||
|
||||
getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
|
||||
getOrCreateIdentifier(
|
||||
identifier: Identifier,
|
||||
reactive: boolean,
|
||||
): PropertyPathNode {
|
||||
/**
|
||||
* Reads from a statically scoped variable are always safe in JS,
|
||||
* with the exception of TDZ (not addressed by this pass).
|
||||
@@ -255,12 +258,19 @@ 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;
|
||||
}
|
||||
@@ -278,6 +288,7 @@ class PropertyPathRegistry {
|
||||
parent: parent,
|
||||
fullPath: {
|
||||
identifier: parent.fullPath.identifier,
|
||||
reactive: parent.fullPath.reactive,
|
||||
path: parent.fullPath.path.concat(entry),
|
||||
},
|
||||
hasOptional: parent.hasOptional || entry.optional,
|
||||
@@ -293,7 +304,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);
|
||||
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive);
|
||||
if (n.path.length === 0) {
|
||||
return currNode;
|
||||
}
|
||||
@@ -315,10 +326,11 @@ function getMaybeNonNullInInstruction(
|
||||
instr: InstructionValue,
|
||||
context: CollectHoistablePropertyLoadsContext,
|
||||
): PropertyPathNode | null {
|
||||
let path = null;
|
||||
let path: ReactiveScopeDependency | null = 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') {
|
||||
@@ -381,7 +393,7 @@ function collectNonNullsInBlocks(
|
||||
) {
|
||||
const identifier = fn.params[0].identifier;
|
||||
knownNonNullIdentifiers.add(
|
||||
context.registry.getOrCreateIdentifier(identifier),
|
||||
context.registry.getOrCreateIdentifier(identifier, true),
|
||||
);
|
||||
}
|
||||
const nodes = new Map<
|
||||
@@ -616,9 +628,11 @@ function reduceMaybeOptionalChains(
|
||||
changed = false;
|
||||
|
||||
for (const original of optionalChainNodes) {
|
||||
let {identifier, path: origPath} = original.fullPath;
|
||||
let currNode: PropertyPathNode =
|
||||
registry.getOrCreateIdentifier(identifier);
|
||||
let {identifier, path: origPath, reactive} = original.fullPath;
|
||||
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
|
||||
identifier,
|
||||
reactive,
|
||||
);
|
||||
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>;
|
||||
};
|
||||
|
||||
type OptionalTraversalContext = {
|
||||
export 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.
|
||||
*/
|
||||
function traverseOptionalBlock(
|
||||
export function traverseOptionalBlock(
|
||||
optional: TBasicBlock<OptionalTerminal>,
|
||||
context: OptionalTraversalContext,
|
||||
outerAlternate: BlockId | null,
|
||||
@@ -290,6 +290,7 @@ function traverseOptionalBlock(
|
||||
);
|
||||
baseObject = {
|
||||
identifier: maybeTest.instructions[0].value.place.identifier,
|
||||
reactive: maybeTest.instructions[0].value.place.reactive,
|
||||
path,
|
||||
};
|
||||
test = maybeTest.terminal;
|
||||
@@ -391,6 +392,7 @@ function traverseOptionalBlock(
|
||||
);
|
||||
const load = {
|
||||
identifier: baseObject.identifier,
|
||||
reactive: baseObject.reactive,
|
||||
path: [
|
||||
...baseObject.path,
|
||||
{
|
||||
|
||||
@@ -25,8 +25,9 @@ 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> = new Map();
|
||||
#deps: Map<Identifier, DependencyNode> = new Map();
|
||||
#hoistableObjects: Map<Identifier, HoistableNode & {reactive: boolean}> =
|
||||
new Map();
|
||||
#deps: Map<Identifier, DependencyNode & {reactive: boolean}> = new Map();
|
||||
|
||||
/**
|
||||
* @param hoistableObjects a set of paths from which we can safely evaluate
|
||||
@@ -35,9 +36,10 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* duplicates when traversing the CFG.
|
||||
*/
|
||||
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
|
||||
for (const {path, identifier} of hoistableObjects) {
|
||||
for (const {path, identifier, reactive} of hoistableObjects) {
|
||||
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#hoistableObjects,
|
||||
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
|
||||
);
|
||||
@@ -70,7 +72,8 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
|
||||
static #getOrCreateRoot<T extends string>(
|
||||
identifier: Identifier,
|
||||
roots: Map<Identifier, TreeNode<T>>,
|
||||
reactive: boolean,
|
||||
roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
|
||||
defaultAccessType: T,
|
||||
): TreeNode<T> {
|
||||
// roots can always be accessed unconditionally in JS
|
||||
@@ -79,9 +82,16 @@ 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;
|
||||
}
|
||||
@@ -92,9 +102,10 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* safe-to-evaluate subpath
|
||||
*/
|
||||
addDependency(dep: ReactiveScopeDependency): void {
|
||||
const {identifier, path} = dep;
|
||||
const {identifier, reactive, path} = dep;
|
||||
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#deps,
|
||||
PropertyAccessType.UnconditionalAccess,
|
||||
);
|
||||
@@ -172,7 +183,13 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
|
||||
const results = new Set<ReactiveScopeDependency>();
|
||||
for (const [rootId, rootNode] of this.#deps.entries()) {
|
||||
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
|
||||
collectMinimalDependenciesInSubtree(
|
||||
rootNode,
|
||||
rootNode.reactive,
|
||||
rootId,
|
||||
[],
|
||||
results,
|
||||
);
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -294,25 +311,24 @@ 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, path});
|
||||
results.add({identifier: rootIdentifier, reactive, 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} from '@babel/traverse';
|
||||
import {Scope as BabelScope, NodePath} from '@babel/traverse';
|
||||
import {TypeSchema} from './TypeSchema';
|
||||
|
||||
export const ReactElementSymbolSchema = z.object({
|
||||
@@ -863,6 +863,7 @@ export class Environment {
|
||||
|
||||
#contextIdentifiers: Set<t.Identifier>;
|
||||
#hoistedIdentifiers: Set<t.Identifier>;
|
||||
parentFunction: NodePath<t.Function>;
|
||||
|
||||
constructor(
|
||||
scope: BabelScope,
|
||||
@@ -870,6 +871,7 @@ 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,
|
||||
@@ -928,6 +930,7 @@ export class Environment {
|
||||
this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType);
|
||||
}
|
||||
|
||||
this.parentFunction = parentFunction;
|
||||
this.#contextIdentifiers = contextIdentifiers;
|
||||
this.#hoistedIdentifiers = new Set();
|
||||
}
|
||||
|
||||
@@ -62,12 +62,14 @@ 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;
|
||||
};
|
||||
@@ -614,6 +616,7 @@ export type MaybeThrowTerminal = {
|
||||
export type ReactiveScopeTerminal = {
|
||||
kind: 'scope';
|
||||
fallthrough: BlockId;
|
||||
dependencies: BlockId;
|
||||
block: BlockId;
|
||||
scope: ReactiveScope;
|
||||
id: InstructionId;
|
||||
@@ -623,6 +626,7 @@ export type ReactiveScopeTerminal = {
|
||||
export type PrunedScopeTerminal = {
|
||||
kind: 'pruned-scope';
|
||||
fallthrough: BlockId;
|
||||
dependencies: BlockId;
|
||||
block: BlockId;
|
||||
scope: ReactiveScope;
|
||||
id: InstructionId;
|
||||
@@ -1472,9 +1476,10 @@ export type ReactiveScope = {
|
||||
range: MutableRange;
|
||||
|
||||
/**
|
||||
* The inputs to this reactive scope
|
||||
* Note the dependencies of a reactive scope are tracked in HIR and
|
||||
* ReactiveFunction
|
||||
*/
|
||||
dependencies: ReactiveScopeDependencies;
|
||||
dependencies: Array<Place>;
|
||||
|
||||
/**
|
||||
* The set of values produced by this scope. This may be empty
|
||||
@@ -1535,6 +1540,18 @@ 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -110,7 +110,6 @@ 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
|
||||
@@ -136,16 +135,17 @@ export default class HIRBuilder {
|
||||
|
||||
constructor(
|
||||
env: Environment,
|
||||
parentFunction: NodePath<t.Function>, // the outermost function being compiled
|
||||
bindings: Bindings | null = null,
|
||||
context: Array<t.Identifier> | null = null,
|
||||
options?: {
|
||||
bindings?: Bindings | null;
|
||||
context?: Array<t.Identifier>;
|
||||
entryBlockKind?: BlockKind;
|
||||
},
|
||||
) {
|
||||
this.#env = env;
|
||||
this.#bindings = bindings ?? new Map();
|
||||
this.parentFunction = parentFunction;
|
||||
this.#context = context ?? [];
|
||||
this.#bindings = options?.bindings ?? new Map();
|
||||
this.#context = options?.context ?? [];
|
||||
this.#entry = makeBlockId(env.nextBlockId);
|
||||
this.#current = newBlock(this.#entry, 'block');
|
||||
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
|
||||
}
|
||||
|
||||
currentBlockKind(): BlockKind {
|
||||
@@ -239,7 +239,7 @@ export default class HIRBuilder {
|
||||
|
||||
// Check if the binding is from module scope
|
||||
const outerBinding =
|
||||
this.parentFunction.scope.parent.getBinding(originalName);
|
||||
this.#env.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.parentFunction.scope.parent.getBinding(
|
||||
const outerBinding = this.#env.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): void {
|
||||
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): BlockId {
|
||||
const {id: blockId, kind, instructions} = this.#current;
|
||||
this.#completed.set(blockId, {
|
||||
kind,
|
||||
@@ -390,6 +390,7 @@ export default class HIRBuilder {
|
||||
const nextId = this.#env.nextBlockId;
|
||||
this.#current = newBlock(nextId, nextBlockKind);
|
||||
}
|
||||
return blockId;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -746,6 +747,11 @@ 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,
|
||||
)} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
|
||||
)} dependencies=bb${terminal.dependencies} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
|
||||
break;
|
||||
}
|
||||
case 'pruned-scope': {
|
||||
value = `[${terminal.id}] <pruned> Scope ${printReactiveScopeSummary(
|
||||
terminal.scope,
|
||||
)} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
|
||||
)} dependencies=bb${terminal.dependencies} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
|
||||
break;
|
||||
}
|
||||
case 'try': {
|
||||
|
||||
@@ -47,6 +47,17 @@ 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 =
|
||||
@@ -73,8 +84,10 @@ 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;
|
||||
}
|
||||
|
||||
@@ -101,17 +114,79 @@ 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(
|
||||
scope.dependencies,
|
||||
dependencies,
|
||||
existingDep =>
|
||||
existingDep.identifier.declarationId ===
|
||||
candidateDep.identifier.declarationId &&
|
||||
areEqualPaths(existingDep.path, candidateDep.path),
|
||||
)
|
||||
)
|
||||
scope.dependencies.add(candidateDep);
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,6 +384,7 @@ function collectTemporariesSidemapImpl(
|
||||
) {
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
identifier: value.place.identifier,
|
||||
reactive: value.place.reactive,
|
||||
path: [],
|
||||
});
|
||||
}
|
||||
@@ -362,11 +438,13 @@ 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}],
|
||||
};
|
||||
}
|
||||
@@ -522,6 +600,7 @@ export class DependencyCollectionContext {
|
||||
this.visitDependency(
|
||||
this.#temporaries.get(place.identifier.id) ?? {
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
},
|
||||
);
|
||||
@@ -586,6 +665,7 @@ export class DependencyCollectionContext {
|
||||
) {
|
||||
maybeDependency = {
|
||||
identifier: maybeDependency.identifier,
|
||||
reactive: maybeDependency.reactive,
|
||||
path: [],
|
||||
};
|
||||
}
|
||||
@@ -607,7 +687,11 @@ export class DependencyCollectionContext {
|
||||
identifier =>
|
||||
identifier.declarationId === place.identifier.declarationId,
|
||||
) &&
|
||||
this.#checkValidDependency({identifier: place.identifier, path: []})
|
||||
this.#checkValidDependency({
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
})
|
||||
) {
|
||||
currentScope.reassignments.add(place.identifier);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,569 @@
|
||||
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,11 +860,13 @@ 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),
|
||||
@@ -1017,7 +1019,7 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
|
||||
}
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
yield terminal.block;
|
||||
yield terminal.dependencies;
|
||||
break;
|
||||
}
|
||||
case 'unreachable':
|
||||
@@ -1068,6 +1070,13 @@ 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':
|
||||
@@ -1081,9 +1090,7 @@ export function mapTerminalOperands(
|
||||
case 'for-in':
|
||||
case 'goto':
|
||||
case 'unreachable':
|
||||
case 'unsupported':
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
case 'unsupported': {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
@@ -1127,6 +1134,13 @@ 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':
|
||||
@@ -1140,9 +1154,7 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
|
||||
case 'for-in':
|
||||
case 'goto':
|
||||
case 'unreachable':
|
||||
case 'unsupported':
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
case 'unsupported': {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ 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';
|
||||
@@ -97,7 +98,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
) {
|
||||
scopeInfos.set(
|
||||
block.terminal.scope.id,
|
||||
block.terminal.scope.dependencies,
|
||||
readScopeDependencies(fn, block.terminal.scope.id),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -485,7 +486,7 @@ function inferDependencies(
|
||||
start: fnInstr.id,
|
||||
end: makeInstructionId(fnInstr.id + 1),
|
||||
},
|
||||
dependencies: new Set(),
|
||||
dependencies: [],
|
||||
reassignments: new Set(),
|
||||
declarations: new Map(),
|
||||
earlyReturnValue: null,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import {PostDominator} from '../HIR/Dominator';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
@@ -292,7 +293,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
let hasReactiveInput = false;
|
||||
/*
|
||||
* NOTE: we want to mark all operands as reactive or not, so we
|
||||
* avoid short-circuting here
|
||||
* avoid short-circuiting here
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const reactive = reactiveIdentifiers.isReactive(operand);
|
||||
@@ -375,6 +376,41 @@ 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,12 +387,10 @@ export function inlineJsxTransform(
|
||||
|
||||
if (block.terminal.kind === 'scope') {
|
||||
const scope = block.terminal.scope;
|
||||
for (const dep of scope.dependencies) {
|
||||
dep.identifier = handleIdentifier(
|
||||
dep.identifier,
|
||||
inlinedJsxDeclarations,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Note that scope dependencies don't need to be renamed explicitly
|
||||
* as they will be visited when traversing scope terminal successors.
|
||||
*/
|
||||
|
||||
for (const [origId, decl] of [...scope.declarations]) {
|
||||
const newDecl = handleIdentifier(
|
||||
|
||||
@@ -17,11 +17,16 @@ import {
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {
|
||||
areEqualPaths,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
PrunedReactiveScopeBlock,
|
||||
ReactiveBreakTerminal,
|
||||
ReactiveContinueTerminal,
|
||||
ReactiveFunction,
|
||||
ReactiveInstructionStatement,
|
||||
ReactiveLogicalValue,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveSequenceValue,
|
||||
ReactiveTerminalStatement,
|
||||
ReactiveTerminalTargetKind,
|
||||
@@ -29,7 +34,12 @@ 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
|
||||
@@ -38,7 +48,7 @@ import {assertExhaustive} from '../Utils/utils';
|
||||
* labels for *all* terminals: see PruneUnusedLabels which removes unnecessary labels.
|
||||
*/
|
||||
export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
|
||||
const cx = new Context(fn.body);
|
||||
const cx = new Context(fn);
|
||||
const driver = new Driver(cx);
|
||||
const body = driver.traverseBlock(cx.block(fn.body.entry));
|
||||
return {
|
||||
@@ -816,17 +826,64 @@ 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);
|
||||
blockValue.push({
|
||||
const scopeBlock: ReactiveScopeBlock | PrunedReactiveScopeBlock = {
|
||||
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': {
|
||||
@@ -1243,6 +1300,7 @@ class Driver {
|
||||
}
|
||||
|
||||
class Context {
|
||||
fn: HIRFunction;
|
||||
ir: HIR;
|
||||
#nextScheduleId: number = 0;
|
||||
|
||||
@@ -1273,8 +1331,9 @@ class Context {
|
||||
*/
|
||||
#controlFlowStack: Array<ControlFlowTarget> = [];
|
||||
|
||||
constructor(ir: HIR) {
|
||||
this.ir = ir;
|
||||
constructor(fn: HIRFunction) {
|
||||
this.fn = fn;
|
||||
this.ir = fn.body;
|
||||
}
|
||||
|
||||
block(id: BlockId): BasicBlock {
|
||||
|
||||
@@ -32,10 +32,10 @@ import {
|
||||
ReactiveBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveInstructionStatement,
|
||||
ReactiveScope,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveScopeDeclaration,
|
||||
ReactiveScopeDependency,
|
||||
ReactiveTerminal,
|
||||
ReactiveValue,
|
||||
SourceLocation,
|
||||
@@ -54,6 +54,7 @@ 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';
|
||||
@@ -539,7 +540,13 @@ function codegenBlockNoReset(
|
||||
}
|
||||
case 'scope': {
|
||||
const temp = new Map(cx.temp);
|
||||
codegenReactiveScope(cx, statements, item.scope, item.instructions);
|
||||
codegenReactiveScope(
|
||||
cx,
|
||||
statements,
|
||||
item.scope,
|
||||
item.instructions,
|
||||
item.dependencyInstructions,
|
||||
);
|
||||
cx.temp = temp;
|
||||
break;
|
||||
}
|
||||
@@ -603,6 +610,7 @@ function codegenReactiveScope(
|
||||
statements: Array<t.Statement>,
|
||||
scope: ReactiveScope,
|
||||
block: ReactiveBlock,
|
||||
dependencyInstructions: Array<ReactiveInstructionStatement>,
|
||||
): void {
|
||||
const cacheStoreStatements: Array<t.Statement> = [];
|
||||
const cacheLoadStatements: Array<t.Statement> = [];
|
||||
@@ -615,9 +623,39 @@ function codegenReactiveScope(
|
||||
const changeExpressionComments: Array<string> = [];
|
||||
const outputComments: Array<string> = [];
|
||||
|
||||
for (const dep of [...scope.dependencies].sort(compareScopeDependency)) {
|
||||
/**
|
||||
* 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) {
|
||||
const index = cx.nextCacheIndex;
|
||||
changeExpressionComments.push(printDependencyComment(dep));
|
||||
changeExpressionComments.push(code);
|
||||
const comparison = t.binaryExpression(
|
||||
'!==',
|
||||
t.memberExpression(
|
||||
@@ -625,7 +663,7 @@ function codegenReactiveScope(
|
||||
t.numericLiteral(index),
|
||||
true,
|
||||
),
|
||||
codegenDependency(cx, dep),
|
||||
dep,
|
||||
);
|
||||
|
||||
if (cx.env.config.enableChangeVariableCodegen) {
|
||||
@@ -652,7 +690,7 @@ function codegenReactiveScope(
|
||||
t.numericLiteral(index),
|
||||
true,
|
||||
),
|
||||
codegenDependency(cx, dep),
|
||||
t.cloneNode(dep, true),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1443,17 +1481,6 @@ 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,
|
||||
@@ -1478,34 +1505,6 @@ 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,
|
||||
): (
|
||||
@@ -2624,30 +2623,6 @@ 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,14 +31,16 @@ export function flattenReactiveLoopsHIR(fn: HIRFunction): void {
|
||||
}
|
||||
case 'scope': {
|
||||
if (activeLoops.length !== 0) {
|
||||
block.terminal = {
|
||||
const newTerminal: PrunedScopeTerminal = {
|
||||
kind: 'pruned-scope',
|
||||
block: terminal.block,
|
||||
dependencies: terminal.dependencies,
|
||||
fallthrough: terminal.fallthrough,
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
scope: terminal.scope,
|
||||
} as PrunedScopeTerminal;
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -83,28 +83,50 @@ 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
|
||||
*/
|
||||
block.terminal = {
|
||||
const newTerminal: LabelTerminal = {
|
||||
kind: 'label',
|
||||
block: terminal.block,
|
||||
fallthrough: terminal.fallthrough,
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
} as LabelTerminal;
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
continue;
|
||||
}
|
||||
|
||||
block.terminal = {
|
||||
const newTerminal: PrunedScopeTerminal = {
|
||||
kind: 'pruned-scope',
|
||||
block: terminal.block,
|
||||
dependencies: terminal.dependencies,
|
||||
fallthrough: terminal.fallthrough,
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
scope: terminal.scope,
|
||||
} as PrunedScopeTerminal;
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
|
||||
scope = {
|
||||
id: fn.env.nextScopeId,
|
||||
range: identifier.mutableRange,
|
||||
dependencies: new Set(),
|
||||
dependencies: [],
|
||||
declarations: new Map(),
|
||||
reassignments: new Set(),
|
||||
earlyReturnValue: null,
|
||||
|
||||
@@ -28,6 +28,7 @@ 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';
|
||||
@@ -119,21 +120,29 @@ 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> {
|
||||
this.visitScope(scopeBlock, scopeBlock.scope.dependencies);
|
||||
if (
|
||||
state !== null &&
|
||||
areEqualDependencies(state, scopeBlock.scope.dependencies)
|
||||
) {
|
||||
const dependencies = this.dependency(scopeBlock);
|
||||
this.visitScope(scopeBlock, dependencies);
|
||||
if (state !== null && areEqualDependencies(state, dependencies)) {
|
||||
return {kind: 'replace-many', value: scopeBlock.instructions};
|
||||
} else {
|
||||
return {kind: 'keep'};
|
||||
@@ -260,7 +269,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
case 'scope': {
|
||||
if (
|
||||
current !== null &&
|
||||
canMergeScopes(current.block, instr) &&
|
||||
canMergeScopes(this, current.block, instr) &&
|
||||
areLValuesLastUsedByScope(
|
||||
instr.scope,
|
||||
current.lvalues,
|
||||
@@ -424,6 +433,7 @@ function areLValuesLastUsedByScope(
|
||||
}
|
||||
|
||||
function canMergeScopes(
|
||||
transform: Transform,
|
||||
current: ReactiveScopeBlock,
|
||||
next: ReactiveScopeBlock,
|
||||
): boolean {
|
||||
@@ -435,10 +445,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(current.scope.dependencies, next.scope.dependencies)
|
||||
) {
|
||||
if (areEqualDependencies(currDeps, nextDeps)) {
|
||||
log(` canMergeScopes: dependencies are equal`);
|
||||
return true;
|
||||
}
|
||||
@@ -456,13 +466,14 @@ function canMergeScopes(
|
||||
new Set(
|
||||
[...current.scope.declarations.values()].map(declaration => ({
|
||||
identifier: declaration.identifier,
|
||||
reactive: false,
|
||||
path: [],
|
||||
})),
|
||||
),
|
||||
next.scope.dependencies,
|
||||
nextDeps,
|
||||
) ||
|
||||
(next.scope.dependencies.size !== 0 &&
|
||||
[...next.scope.dependencies].every(
|
||||
(nextDeps.size !== 0 &&
|
||||
[...nextDeps].every(
|
||||
dep =>
|
||||
isAlwaysInvalidatingType(dep.identifier.type) &&
|
||||
Iterable_some(
|
||||
@@ -537,7 +548,7 @@ function areEqualDependencies(
|
||||
* *never* change and it's also eligible for merging.
|
||||
*/
|
||||
function scopeIsEligibleForMerging(scopeBlock: ReactiveScopeBlock): boolean {
|
||||
if (scopeBlock.scope.dependencies.size === 0) {
|
||||
if (scopeBlock.scope.dependencies.length === 0) {
|
||||
/*
|
||||
* Regardless of the type of value produced, if the scope has no dependencies
|
||||
* then its value will never change.
|
||||
|
||||
@@ -64,11 +64,7 @@ export function printReactiveScopeSummary(scope: ReactiveScope): string {
|
||||
items.push('scope');
|
||||
items.push(`@${scope.id}`);
|
||||
items.push(`[${scope.range.start}:${scope.range.end}]`);
|
||||
items.push(
|
||||
`dependencies=[${Array.from(scope.dependencies)
|
||||
.map(dep => printDependency(dep))
|
||||
.join(', ')}]`,
|
||||
);
|
||||
items.push(`dependencies=[${scope.dependencies.map(printPlace).join(',')}]`);
|
||||
items.push(
|
||||
`declarations=[${Array.from(scope.declarations)
|
||||
.map(([, decl]) =>
|
||||
@@ -96,6 +92,8 @@ 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,18 +26,13 @@ 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
|
||||
@@ -50,7 +45,29 @@ class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
promoteIdentifier(declaration.identifier, state);
|
||||
}
|
||||
}
|
||||
this.traverseScope(scopeBlock, 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);
|
||||
}
|
||||
|
||||
override visitPrunedScope(
|
||||
@@ -75,11 +92,33 @@ 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);
|
||||
@@ -177,6 +216,10 @@ 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;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -427,6 +470,7 @@ 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,6 +13,7 @@ import {
|
||||
ReactiveScopeBlock,
|
||||
ReactiveStatement,
|
||||
} from '../HIR';
|
||||
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
|
||||
|
||||
/**
|
||||
* Some instructions will *always* produce a new value, and unless memoized will *always*
|
||||
@@ -87,7 +88,8 @@ class Transform extends ReactiveFunctionTransform<boolean> {
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.visitScope(scopeBlock, true);
|
||||
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
const scopeDeps = readScopeDependenciesRHIR(scopeBlock);
|
||||
for (const [, dep] of scopeDeps) {
|
||||
if (this.unmemoizedValues.has(dep.identifier)) {
|
||||
/*
|
||||
* This scope depends on an always-invalidating value so the scope will always invalidate:
|
||||
@@ -107,6 +109,7 @@ class Transform extends ReactiveFunctionTransform<boolean> {
|
||||
kind: 'replace',
|
||||
value: {
|
||||
kind: 'pruned-scope',
|
||||
dependencyInstructions: scopeBlock.dependencyInstructions,
|
||||
scope: scopeBlock.scope,
|
||||
instructions: scopeBlock.instructions,
|
||||
},
|
||||
|
||||
@@ -22,6 +22,7 @@ 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';
|
||||
@@ -178,14 +179,22 @@ class Visitor extends ReactiveFunctionVisitor<CreateUpdate> {
|
||||
].map(id => this.map.get(id) ?? 'Unknown'),
|
||||
);
|
||||
super.visitScope(scope, state);
|
||||
[...scope.scope.dependencies].forEach(ident => {
|
||||
// TODO
|
||||
const scopeDeps = readScopeDependenciesRHIR(scope);
|
||||
[...scopeDeps].forEach(([place, dep]) => {
|
||||
let target: undefined | IdentifierId =
|
||||
this.aliases.find(ident.identifier.id) ?? ident.identifier.id;
|
||||
ident.path.forEach(token => {
|
||||
this.aliases.find(dep.identifier.id) ?? dep.identifier.id;
|
||||
dep.path.forEach(token => {
|
||||
target &&= this.paths.get(target)?.get(token.property);
|
||||
});
|
||||
if (target && this.map.get(target) === 'Create') {
|
||||
scope.scope.dependencies.delete(ident);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,13 +95,19 @@ class Visitor extends ReactiveFunctionVisitor<ReactiveIdentifiers> {
|
||||
state: ReactiveIdentifiers,
|
||||
): void {
|
||||
this.traverseScope(scopeBlock, state);
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
const isReactive = state.has(dep.identifier.id);
|
||||
if (!isReactive) {
|
||||
scopeBlock.scope.dependencies.delete(dep);
|
||||
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++;
|
||||
}
|
||||
}
|
||||
if (scopeBlock.scope.dependencies.size !== 0) {
|
||||
scopeBlock.scope.dependencies.length = j;
|
||||
|
||||
if (scopeBlock.scope.dependencies.length !== 0) {
|
||||
/**
|
||||
* If any of a scope's dependencies are reactive, then all of its
|
||||
* outputs will re-evaluate whenever those dependencies change.
|
||||
|
||||
@@ -56,6 +56,7 @@ class Transform extends ReactiveFunctionTransform<State> {
|
||||
value: {
|
||||
kind: 'pruned-scope',
|
||||
scope: scopeBlock.scope,
|
||||
dependencyInstructions: scopeBlock.dependencyInstructions,
|
||||
instructions: scopeBlock.instructions,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
HIRFunction,
|
||||
InstructionId,
|
||||
@@ -185,6 +186,20 @@ 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,6 +17,7 @@ import {
|
||||
isUseInsertionEffectHookType,
|
||||
isUseLayoutEffectHookType,
|
||||
} from '../HIR';
|
||||
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {
|
||||
ReactiveFunctionVisitor,
|
||||
@@ -73,7 +74,7 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
|
||||
* memoized, allowing a transitive memoization check.
|
||||
*/
|
||||
let areDependenciesMemoized = true;
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
for (const [, dep] of readScopeDependenciesRHIR(scopeBlock)) {
|
||||
if (isUnmemoized(dep.identifier, this.scopes)) {
|
||||
areDependenciesMemoized = false;
|
||||
break;
|
||||
|
||||
@@ -25,6 +25,7 @@ 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 {
|
||||
@@ -406,7 +407,8 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
state.manualMemoState != null &&
|
||||
state.manualMemoState.depsFromSource != null
|
||||
) {
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
const deps = readScopeDependenciesRHIR(scopeBlock).values();
|
||||
for (const dep of deps) {
|
||||
validateInferredDep(
|
||||
dep,
|
||||
this.temporaries,
|
||||
|
||||
Reference in New Issue
Block a user