refactor(compiler): Create an AlwaysInline variable mode (#52001)

The template pipeline now supports lexical variables that are always inlined into their call sites, even if multiple call sites exist.

An `AlwaysInline` variable may not rely on the current context, because it will potentially be inlined at several different locations.

Co-authored-by: Alex Rickabaugh <alxhub@users.noreply.github.com>

PR Close #52001
This commit is contained in:
Dylan Hunn
2023-10-18 14:33:11 -07:00
parent af6a75a837
commit db81193ce6
4 changed files with 71 additions and 7 deletions
@@ -336,6 +336,16 @@ export enum ExpressionKind {
ConditionalCase,
}
export enum VariableFlags {
None = 0b0000,
/**
* Always inline this variable, regardless of the number of times it's used.
* An `AlwaysInline` variable may not depend on context, because doing so may cause side effects
* that are illegal when multi-inlined. (The optimizer will enforce this constraint.)
*/
AlwaysInline = 0b0001,
}
/**
* Distinguishes between different kinds of `SemanticVariable`s.
*/
@@ -7,7 +7,7 @@
*/
import * as o from '../../../../../output/output_ast';
import {OpKind} from '../enums';
import {OpKind, VariableFlags} from '../enums';
import {Op, XrefId} from '../operations';
import {SemanticVariable} from '../variable';
@@ -68,18 +68,22 @@ export interface VariableOp<OpT extends Op<OpT>> extends Op<OpT> {
* Expression representing the value of the variable.
*/
initializer: o.Expression;
flags: VariableFlags;
}
/**
* Create a `VariableOp`.
*/
export function createVariableOp<OpT extends Op<OpT>>(
xref: XrefId, variable: SemanticVariable, initializer: o.Expression): VariableOp<OpT> {
xref: XrefId, variable: SemanticVariable, initializer: o.Expression,
flags: VariableFlags): VariableOp<OpT> {
return {
kind: OpKind.Variable,
xref,
variable,
initializer,
flags,
...NEW_OP,
};
}
@@ -177,7 +177,8 @@ function generateVariablesInScopeForView(
// view with a `nextContext` expression. This context switching operation itself declares a
// variable, because the context of the view may be referenced directly.
newOps.push(ir.createVariableOp(
view.job.allocateXrefId(), scope.viewContextVariable, new ir.NextContextExpr()));
view.job.allocateXrefId(), scope.viewContextVariable, new ir.NextContextExpr(),
ir.VariableFlags.None));
}
// Add variables for all context variables available in this scope's view.
@@ -187,13 +188,15 @@ function generateVariablesInScopeForView(
const variable = value === ir.CTX_REF ? context : new o.ReadPropExpr(context, value);
// Add the variable declaration.
newOps.push(ir.createVariableOp(
view.job.allocateXrefId(), scope.contextVariables.get(name)!, variable));
view.job.allocateXrefId(), scope.contextVariables.get(name)!, variable,
ir.VariableFlags.None));
}
// Add variables for all local references declared for elements in this scope.
for (const ref of scope.references) {
newOps.push(ir.createVariableOp(
view.job.allocateXrefId(), ref.variable, new ir.ReferenceExpr(ref.targetId, ref.offset)));
view.job.allocateXrefId(), ref.variable, new ir.ReferenceExpr(ref.targetId, ref.offset),
ir.VariableFlags.None));
}
if (scope.parent !== null) {
@@ -30,6 +30,15 @@ import {CompilationJob} from '../compilation';
*/
export function phaseVariableOptimization(job: CompilationJob): void {
for (const unit of job.units) {
inlineAlwaysInlineVariables(unit.create);
inlineAlwaysInlineVariables(unit.update);
for (const op of unit.create) {
if (op.kind === ir.OpKind.Listener) {
inlineAlwaysInlineVariables(op.handlerOps);
}
}
optimizeVariablesInOpList(unit.create, job.compatibility);
optimizeVariablesInOpList(unit.update, job.compatibility);
@@ -95,6 +104,33 @@ interface OpInfo {
fences: Fence;
}
function inlineAlwaysInlineVariables(ops: ir.OpList<ir.CreateOp|ir.UpdateOp>): void {
const vars = new Map<ir.XrefId, ir.VariableOp<ir.CreateOp|ir.UpdateOp>>();
for (const op of ops) {
if (op.kind === ir.OpKind.Variable) {
ir.visitExpressionsInOp(op, expr => {
if (ir.isIrExpression(expr) && fencesForIrExpression(expr) !== Fence.None) {
throw new Error(`AssertionError: A context-sensitive variable was marked AlwaysInline`);
}
});
vars.set(op.xref, op);
}
ir.transformExpressionsInOp(op, expr => {
if (expr instanceof ir.ReadVariableExpr && vars.has(expr.xref)) {
const varOp = vars.get(expr.xref)!;
// Inline by cloning, because we might inline into multiple places.
return varOp.initializer.clone();
}
return expr;
}, ir.VisitorContextFlag.None);
}
for (const op of vars.values()) {
ir.OpList.remove(op as ir.CreateOp | ir.UpdateOp);
}
}
/**
* Process a list of operations and optimize variables within that list.
*/
@@ -174,10 +210,15 @@ function optimizeVariablesInOpList(
// Next, inline any remaining variables with exactly one usage.
const toInline: ir.XrefId[] = [];
for (const [id, count] of varUsages) {
const decl = varDecls.get(id)!;
const varInfo = opMap.get(decl as ir.CreateOp | ir.UpdateOp)!;
// We can inline variables that:
// - are used once
// - are used exactly once, and
// - are not used remotely
if (count !== 1) {
// OR
// - are marked for always inlining
const isAlwaysInline = !!(decl.flags & ir.VariableFlags.AlwaysInline);
if (count !== 1 || isAlwaysInline) {
// We can't inline this variable as it's used more than once.
continue;
}
@@ -196,6 +237,12 @@ function optimizeVariablesInOpList(
// no future operation will make inlining legal.
const decl = varDecls.get(candidate)!;
const varInfo = opMap.get(decl as ir.CreateOp | ir.UpdateOp)!;
const isAlwaysInline = !!(decl.flags & ir.VariableFlags.AlwaysInline);
if (isAlwaysInline) {
throw new Error(
`AssertionError: Found an 'AlwaysInline' variable after the always inlining pass.`);
}
// Scan operations following the variable declaration and look for the point where that variable
// is used. There should only be one usage given the precondition above.