diff a/src/jdk.incubator.code/share/classes/jdk/incubator/code/internal/ControlFlowPreparation.java b/src/jdk.incubator.code/share/classes/jdk/incubator/code/internal/ControlFlowPreparation.java --- /dev/null +++ b/src/jdk.incubator.code/share/classes/jdk/incubator/code/internal/ControlFlowPreparation.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.code.internal; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.BitSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import jdk.incubator.code.Block; +import jdk.incubator.code.Body; +import jdk.incubator.code.CodeContext; +import jdk.incubator.code.CodeTransformer; +import jdk.incubator.code.Op; +import jdk.incubator.code.Value; +import jdk.incubator.code.dialect.core.CoreOp; +import jdk.incubator.code.dialect.java.JavaOp; +import jdk.incubator.code.dialect.java.JavaType; + +/** + * Prepares a fixed point control-flow and builds the model once + */ +public final class ControlFlowPreparation implements CodeTransformer { + + @SuppressWarnings("unchecked") + public static O transform(O operation) { + Objects.requireNonNull(operation); + List branches = operation.elements().filter(e -> e instanceof CoreOp.BranchOp + || e instanceof CoreOp.ConditionalBranchOp).map(Op.class::cast).toList(); + if (branches.stream().noneMatch(CoreOp.ConditionalBranchOp.class::isInstance)) { + return operation; + } + ControlFlowPreparation cfp = new ControlFlowPreparation(operation, branches); + cfp.prepare(); + return cfp.rewrites.isEmpty() ? operation : (O) operation.transform(CodeContext.create(), cfp); + } + + private record Target(Block block, List arguments) { + Target(Block.Reference reference) { + this(reference.targetBlock(), reference.arguments()); + } + + Block.Reference map(CodeContext context) { + return context.getBlock(block).reference(context.getValues(arguments)); + } + } + + final Op root; + final List branches; + final Map> rewrites = new HashMap<>(); + final Map> originalTargets = new HashMap<>(); + final Map reachable = new HashMap<>(); + final BitSet routeVisited = new BitSet(); + final Map routable = new HashMap<>(); + final Map>> incoming = new HashMap<>(); + final ConstantValueAnalysis constants = new ConstantValueAnalysis(block -> incoming.getOrDefault(block, List.of())); + + private ControlFlowPreparation(Op root, List branches) { + this.root = root; + this.branches = branches; + } + + private void prepare() { + boolean changed; + do { + reachable.values().forEach(BitSet::clear); + incoming.values().forEach(List::clear); + Deque work = new ArrayDeque<>(); + for (var body : root.bodies()) work.add(body.entryBlock()); + while (!work.isEmpty()) { + Block block = work.removeFirst(); + BitSet live = reachable.computeIfAbsent(block.parent(), _ -> new BitSet()); + if (live.get(block.index())) continue; + live.set(block.index()); + for (Op operation : block.ops()) + for (var body : operation.bodies()) work.add(body.entryBlock()); + for (Target target : targets(block.terminatingOp())) { + incoming.computeIfAbsent(target.block, _ -> new ArrayList<>()).add(target.arguments); + work.add(target.block); + } + } + // stop when every reachable conditional is resolved + if (branches.stream().noneMatch(op -> op instanceof CoreOp.ConditionalBranchOp + && isReachable(op.parent()) && targets(op).size() == 2)) return; + // known constants remain valid + constants.clearUnknowns(); + changed = false; + for (Op branch : branches) { + if (isReachable(branch.parent())) changed |= discover(branch); + } + } while (changed); + } + + private List targets(Op operation) { + List changed = rewrites.get(operation); + return changed != null ? changed : originalTargets.computeIfAbsent(operation, + op -> op.successors().stream().map(Target::new).toList()); + } + + private boolean isReachable(Block block) { + BitSet live = reachable.get(block.parent()); + return live != null && live.get(block.index()); + } + + private boolean discover(Op operation) { + List before = targets(operation); + List after; + if (operation instanceof CoreOp.ConditionalBranchOp branch && before.size() == 2 + && constants.booleanValue(branch.predicateOperand()) instanceof Boolean value) { + after = List.of(route(before.get(value ? 0 : 1))); + } else { + // allocate replacement successors when routing changes + after = null; + for (int i = 0; i < before.size(); i++) { + Target next = route(before.get(i)); + if (!next.equals(before.get(i))) { + if (after == null) after = new ArrayList<>(before); + after.set(i, next); + } + } + if (after == null) return false; + } + if (before.equals(after)) return false; + rewrites.put(operation, after); + return true; + } + + private boolean canRoute(Block block) { + return routable.computeIfAbsent(block, b -> + (b.terminatingOp() instanceof CoreOp.BranchOp + || b.terminatingOp() instanceof CoreOp.ConditionalBranchOp) + && (b.ops().size() == 1 || b.parameters().size() == 1 + && b.parameters().getFirst().type().equals(JavaType.BOOLEAN) + && b.ops().stream().limit(b.ops().size() - 1) + .allMatch(JavaOp.NotOp.class::isInstance))); + } + + private Target route(Target start) { + if (!canRoute(start.block)) return start; + Target current = start, selected = start; + Boolean argument = booleanArgument(current); + // routing stays within one immutable body + routeVisited.clear(); + while (!current.block.isEntryBlock() && !routeVisited.get(current.block.index())) { + Block block = current.block; + routeVisited.set(block.index()); + Op terminator = block.terminatingOp(); + List operations = block.ops().subList(0, block.ops().size() - 1); + List outgoing = targets(terminator); + // empty forwarding block with no side-used parameters + if (operations.isEmpty() && outgoing.size() == 1 + && (terminator instanceof CoreOp.BranchOp || terminator instanceof CoreOp.ConditionalBranchOp) + && block.parameters().stream().allMatch(p -> onlyUsedBy(p, terminator))) { + // compose edge arguments through empty forwarding blocks without merging blocks or deleting parameters + if (!canRoute(outgoing.getFirst().block)) break; + List arguments = current.arguments; + Boolean previous = argument; + List forwarded = outgoing.getFirst().arguments.stream() + .map(v -> v instanceof Block.Parameter p && p.declaringBlock() == block + ? arguments.get(p.index()) : v).toList(); + argument = outgoing.getFirst().arguments.size() == 1 + && block.parameters().size() == 1 + && outgoing.getFirst().arguments.getFirst() == block.parameters().getFirst() + ? previous : booleanArgument(new Target(outgoing.getFirst().block, forwarded)); + current = new Target(outgoing.getFirst().block, forwarded); + continue; + } + // remaining blocks with single boolean + if (argument == null || block.parameters().size() != 1 + || !block.parameters().getFirst().type().equals(JavaType.BOOLEAN)) break; + Value value = block.parameters().getFirst(); + boolean valid = true; + // successive negations with no side uses + for (Op operation : operations) { + if (!(operation instanceof JavaOp.NotOp not) + || not.operand() != value || !onlyUsedBy(value, operation)) { + valid = false; + break; + } + value = operation.result(); + argument = !argument; + } + if (!valid || !onlyUsedBy(value, terminator)) break; + // shortcut for boolean dispatch with no-args exits + if (terminator instanceof CoreOp.ConditionalBranchOp branch + && branch.predicateOperand() == value + && outgoing.stream().allMatch(target -> target.arguments.isEmpty())) { + current = outgoing.get(outgoing.size() == 1 ? 0 : argument ? 0 : 1); + selected = current; + argument = booleanArgument(current); + } else if (terminator instanceof CoreOp.BranchOp + && outgoing.getFirst().arguments.size() == 1 + && outgoing.getFirst().arguments.getFirst() == value) { + // carry boolean through a chain, no synthetic literal + current = new Target(outgoing.getFirst().block, current.arguments); + } else { + break; + } + } + // retain the last target + return selected; + } + + private Boolean booleanArgument(Target target) { + return target.arguments.size() == 1 + ? constants.booleanValue(target.arguments.getFirst()) : null; + } + + private boolean onlyUsedBy(Value value, Op selected) { + // routing commits only zero-argument dispatch exits + // conditional selection only removes edges + for (Op.Result use : value.uses()) { + Op operation = use.op(); + if (operation == selected || !isReachable(operation.parent())) continue; + boolean collapsed = operation instanceof CoreOp.ConditionalBranchOp + && targets(operation).size() == 1; + if (!collapsed && operation.operands().contains(value) + || targets(operation).stream().anyMatch(t -> t.arguments.contains(value))) return false; + } + return true; + } + + @Override + public void acceptBlock(Block.Builder builder, Block block) { + if (isReachable(block)) CodeTransformer.super.acceptBlock(builder, block); + } + + @Override + public Block.Builder acceptOp(Block.Builder builder, Op operation) { + List targets = rewrites.get(operation); + if (targets == null) { + builder.add(operation); + } else { + CodeContext context = builder.context(); + Op replacement = targets.size() == 1 + ? CoreOp.branch(targets.getFirst().map(context)) + : CoreOp.conditionalBranch(context.getValue(operation.operands().getFirst()), + targets.get(0).map(context), targets.get(1).map(context)); + Op.Result result = builder.add(replacement); + result.op().setLocation(operation.location()); + context.mapValue(operation.result(), result); + } + return builder; + } +}