Compare commits

...

2 Commits

Author SHA1 Message Date
Mofei Zhang
50f8538911 [compiler][rewrite] Represent scope dependencies with value blocks
(needs cleanup)

- Scopes no longer store a flat list of their dependencies. Instead:
  - Scope terminals are effectively a `goto` for scope dependency instructions (represented as value blocks that terminate with a `goto scopeBlock` for HIR and a series of ReactiveInstructions for ReactiveIR)
  - Scopes themselves store `dependencies: Array<Place>`, which refer to temporaries written to by scope dependency instructions

Next steps:
- new pass to dedupe scope dependency instructions after all dependency and scope pruning passes, effectively 'hoisting' dependencies out
- more complex dependencies (unary ops like `Boolean` or `Not`, binary ops like `!==` or logical operators)
2025-04-29 18:43:01 -04:00
Mofei Zhang
1cb99cadd6 [compiler] Prepare HIRBuilder to be used by later passes 2025-04-29 18:38:31 -04:00
31 changed files with 1122 additions and 188 deletions

View File

@@ -129,6 +129,7 @@ function run(
mode,
config,
contextIdentifiers,
func,
logger,
filename,
code,

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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,
{

View File

@@ -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,

View File

@@ -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();
}

View File

@@ -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;
};

View File

@@ -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);

View File

@@ -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': {

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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);
}
/*

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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.

View File

@@ -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('}');
}

View File

@@ -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) {

View File

@@ -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,
},

View File

@@ -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);
}
});
}

View File

@@ -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.

View File

@@ -56,6 +56,7 @@ class Transform extends ReactiveFunctionTransform<State> {
value: {
kind: 'pruned-scope',
scope: scopeBlock.scope,
dependencyInstructions: scopeBlock.dependencyInstructions,
instructions: scopeBlock.instructions,
},
};

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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,