Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[compiler] Promote temporaries when necessary to prevent codegen reordering over side-effectful operations #30554

Merged
merged 5 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -431,17 +431,17 @@ function* runWithEnvironment(
value: reactiveFunction,
});

promoteUsedTemporaries(reactiveFunction);
pruneUnusedLValues(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PromoteUsedTemporaries',
name: 'PruneUnusedLValues',
value: reactiveFunction,
});

pruneUnusedLValues(reactiveFunction);
promoteUsedTemporaries(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PruneUnusedLValues',
name: 'PromoteUsedTemporaries',
value: reactiveFunction,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1198,7 +1198,7 @@ function codegenInstructionNullable(
value = null;
} else {
lvalue = instr.value.lvalue.pattern;
let hasReasign = false;
let hasReassign = false;
let hasDeclaration = false;
for (const place of eachPatternOperand(lvalue)) {
if (
Expand All @@ -1208,18 +1208,18 @@ function codegenInstructionNullable(
cx.temp.set(place.identifier.id, null);
}
const isDeclared = cx.hasDeclared(place.identifier);
hasReasign ||= isDeclared;
hasReassign ||= isDeclared;
hasDeclaration ||= !isDeclared;
}
if (hasReasign && hasDeclaration) {
if (hasReassign && hasDeclaration) {
CompilerError.invariant(false, {
reason:
'Encountered a destructuring operation where some identifiers are already declared (reassignments) but others are not (declarations)',
description: null,
loc: instr.loc,
suggestions: null,
});
} else if (hasReasign) {
} else if (hasReassign) {
kind = InstructionKind.Reassign;
}
value = codegenPlaceToExpression(cx, instr.value.value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import {
Place,
PrunedReactiveScopeBlock,
ReactiveFunction,
ReactiveInstruction,
ReactiveScopeBlock,
ReactiveValue,
ScopeId,
SpreadPattern,
promoteTemporary,
promoteTemporaryJsxTag,
} from '../HIR/HIR';
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';

class Visitor extends ReactiveFunctionVisitor<State> {
override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void {
Expand Down Expand Up @@ -148,6 +151,186 @@ class CollectPromotableTemporaries extends ReactiveFunctionVisitor<State> {
}
}

type InterState = Map<IdentifierId, [Identifier, boolean]>;
class PromoteInterposedTemporaries extends ReactiveFunctionVisitor<InterState> {
#promotable: State;
#consts: Set<IdentifierId> = new Set();

/*
* Unpromoted temporaries will be emitted at their use sites rather than as separate
* declarations. However, this causes errors if an interposing temporary has been
* promoted, or if an interposing instruction has had its lvalues deleted, because such
* temporaries will be emitted as separate statements, which can effectively cause
* code to be reordered, and when that code has side effects that changes program behavior.
* This visitor promotes temporarties that have such interposing instructions to preserve
* source ordering.
*/
constructor(promotable: State, params: Array<Place | SpreadPattern>) {
super();
params.forEach(param => {
switch (param.kind) {
case 'Identifier':
this.#consts.add(param.identifier.id);
break;
case 'Spread':
this.#consts.add(param.place.identifier.id);
break;
}
});
this.#promotable = promotable;
}

override visitPlace(
_id: InstructionId,
place: Place,
state: InterState,
): void {
const promo = state.get(place.identifier.id);
if (promo) {
const [identifier, needsPromotion] = promo;
if (
needsPromotion &&
identifier.name === null &&
!this.#consts.has(identifier.id)
) {
/*
* If the identifier hasn't been promoted but is marked as needing
* promotion by the logic in `visitInstruction`, and we've seen a
* use of it after said marking, promote it
*/
promoteIdentifier(identifier, this.#promotable);
}
}
}

override visitInstruction(
instruction: ReactiveInstruction,
state: InterState,
): void {
for (const lval of eachInstructionValueLValue(instruction.value)) {
CompilerError.invariant(lval.identifier.name != null, {
reason:
'PromoteInterposedTemporaries: Assignment targets not expected to be temporaries',
loc: instruction.loc,
});
}

switch (instruction.value.kind) {
case 'CallExpression':
case 'MethodCall':
case 'Await':
case 'PropertyStore':
case 'PropertyDelete':
case 'ComputedStore':
case 'ComputedDelete':
case 'PostfixUpdate':
case 'PrefixUpdate':
case 'StoreLocal':
case 'StoreContext':
case 'StoreGlobal':
case 'Destructure': {
let constStore = false;

if (
(instruction.value.kind === 'StoreContext' ||
instruction.value.kind === 'StoreLocal') &&
(instruction.value.lvalue.kind === 'Const' ||
instruction.value.lvalue.kind === 'HoistedConst')
) {
/*
* If an identifier is const, we don't need to worry about it
* being mutated between being loaded and being used
*/
this.#consts.add(instruction.value.lvalue.place.identifier.id);
constStore = true;
}
if (
instruction.value.kind === 'Destructure' &&
(instruction.value.lvalue.kind === 'Const' ||
instruction.value.lvalue.kind === 'HoistedConst')
) {
[...eachPatternOperand(instruction.value.lvalue.pattern)].forEach(
ident => this.#consts.add(ident.identifier.id),
);
constStore = true;
}
if (instruction.value.kind === 'MethodCall') {
// Treat property of method call as constlike so we don't promote it.
this.#consts.add(instruction.value.property.identifier.id);
}

super.visitInstruction(instruction, state);
if (
!constStore &&
(instruction.lvalue == null ||
instruction.lvalue.identifier.name != null)
) {
/*
* If we've stripped the lvalue or promoted the lvalue, then we will emit this instruction
* as a statement in codegen.
*
* If this instruction will be emitted directly as a statement rather than as a temporary
* during codegen, then it can interpose between the defs and the uses of other temporaries.
* Since this instruction could potentially mutate those defs, it's not safe to relocate
* the definition of those temporaries to after this instruction. Mark all those temporaries
* as needing promotion, but don't promote them until we actually see them being used.
*/
for (const [key, [ident, _]] of state.entries()) {
state.set(key, [ident, true]);
}
}
if (instruction.lvalue && instruction.lvalue.identifier.name === null) {
// Add this instruction's lvalue to the state, initially not marked as needing promotion
state.set(instruction.lvalue.identifier.id, [
instruction.lvalue.identifier,
false,
]);
}
break;
}
case 'DeclareContext':
case 'DeclareLocal': {
if (
instruction.value.lvalue.kind === 'Const' ||
instruction.value.lvalue.kind === 'HoistedConst'
) {
this.#consts.add(instruction.value.lvalue.place.identifier.id);
}
super.visitInstruction(instruction, state);
break;
}
case 'LoadContext':
case 'LoadLocal': {
if (instruction.lvalue && instruction.lvalue.identifier.name === null) {
if (this.#consts.has(instruction.value.place.identifier.id)) {
this.#consts.add(instruction.lvalue.identifier.id);
}
state.set(instruction.lvalue.identifier.id, [
instruction.lvalue.identifier,
false,
]);
}
super.visitInstruction(instruction, state);
break;
}
case 'PropertyLoad':
case 'ComputedLoad': {
if (instruction.lvalue && instruction.lvalue.identifier.name === null) {
state.set(instruction.lvalue.identifier.id, [
instruction.lvalue.identifier,
false,
]);
}
super.visitInstruction(instruction, state);
break;
}
default: {
super.visitInstruction(instruction, state);
}
}
}
}

export function promoteUsedTemporaries(fn: ReactiveFunction): void {
const state: State = {
tags: new Set(),
Expand All @@ -161,6 +344,12 @@ export function promoteUsedTemporaries(fn: ReactiveFunction): void {
}
}
visitReactiveFunction(fn, new Visitor(), state);

visitReactiveFunction(
fn,
new PromoteInterposedTemporaries(state, fn.params),
new Map(),
);
}

function promoteIdentifier(identifier: Identifier, state: State): void {
Expand Down

This file was deleted.

Loading