Skip to content

Commit

Permalink
Closes #151: Optimizing chain assignments
Browse files Browse the repository at this point in the history
  • Loading branch information
cardillan committed Feb 3, 2025
1 parent 509af85 commit 3e24d1e
Show file tree
Hide file tree
Showing 27 changed files with 6,772 additions and 6,606 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project now adheres to [Semantic Versioning](https://semver.org/).

## 3.1.0 - Future release

### Experimental features

* Added the "backpropagation" optimization to Data Flow Optimization (closes [#151](https://github.com/cardillan/mindcode/issues/151)).

## 3.0.0 - 2025-01-26

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import info.teksol.mc.mindcode.compiler.postprocess.LogicInstructionPrinter;
import info.teksol.mc.mindcode.logic.arguments.*;
import info.teksol.mc.mindcode.logic.instructions.*;
import info.teksol.mc.mindcode.logic.opcodes.Opcode;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

Expand Down Expand Up @@ -54,6 +55,11 @@ class DataFlowOptimizer extends BaseOptimizer {
/// Instructions referencing each variable. References are accumulated (not cleared during single pass).
final Map<LogicVariable, List<LogicInstruction>> references = new HashMap<>();

/// Holds the variables defined by an instruction, and other instructions using that variable.
/// If there's just one such reference, and this reference is a Set instruction, the target of the Set can be
/// injected into the definition.
final Map<LogicInstruction, Map<LogicVariable, List<@Nullable LogicInstruction>>> definitionReferences = new IdentityHashMap<>();

/// When a jump outside its context is encountered, current variable state is copied and assigned to the target
/// label. Upon encountering the target label all stored states are merged and flushed.
/// Unresolved label indicates a problem in the data flow analysis -- this would happen if the jump targeted an
Expand All @@ -66,11 +72,18 @@ class DataFlowOptimizer extends BaseOptimizer {
/// An instance used for variable states processing
private final DataFlowVariableStates dataFlowVariableStates;

/// Instruction counter; in encountered order
private int counter = 0;

public DataFlowOptimizer(OptimizationContext optimizationContext) {
super(Optimization.DATA_FLOW_OPTIMIZATION, optimizationContext);
dataFlowVariableStates = new DataFlowVariableStates(this);
}

int getCounter() {
return counter;
}

@Override
protected boolean optimizeProgram(OptimizationPhase phase) {
defines.clear();
Expand All @@ -80,6 +93,7 @@ protected boolean optimizeProgram(OptimizationPhase phase) {
uninitialized.clear();
replacements.clear();
references.clear();
definitionReferences.clear();
labelStates.clear();
functionEndStates.clear();

Expand All @@ -89,6 +103,34 @@ protected boolean optimizeProgram(OptimizationPhase phase) {

getRootContext().children().forEach(this::processTopContext);

if (experimental()) {
boolean updated = false;
for (LogicInstruction instruction : definitionReferences.keySet()) {
int index = instructionIndex(instruction);
if (index < 0) continue;

Map<LogicVariable, List<@Nullable LogicInstruction>> logicVariableListMap = definitionReferences.get(instruction);
for (LogicVariable variable : logicVariableListMap.keySet()) {
List<@Nullable LogicInstruction> list = logicVariableListMap.get(variable);
if (list.size() == 1 && list.getFirst() instanceof SetInstruction set && set.getValue().equals(variable)) {
if (!canEliminate(instruction, variable)) continue;
if (instruction.inputArgumentsStream().anyMatch(v -> v.equals(variable))) continue;

int setIndex = instructionIndex(set);
if (setIndex >= 0) {
invalidateInstruction(setIndex);
replaceInstruction(index, replaceAllArgs(instruction, variable, set.getResult()));
updated = true;
}
}
}
}

if (updated) {
return true;
}
}

// Keep defining instructions for orphaned, uninitialized variables.
orphans.entrySet().stream()
.filter(e -> uninitialized.contains(e.getKey()))
Expand Down Expand Up @@ -707,8 +749,6 @@ private VariableStates resolveLabel(VariableStates variableStates, LogicLabel la
return variableStates;
}

private int counter = 0;

/// Processes a single instruction.
///
/// @param variableStates variable states before executing the instruction
Expand All @@ -721,8 +761,9 @@ private VariableStates processInstruction(VariableStates variableStates, LogicIn
Objects.requireNonNull(variableStates);
Objects.requireNonNull(instruction);

counter++;

if (TRACE) {
counter++;
trace("*" + counter + " Processing instruction ix#" + instructionIndex(instruction) +
": " + LogicInstructionPrinter.toString(instructionProcessor, instruction));

Expand Down Expand Up @@ -844,6 +885,8 @@ public void addUninitialized(LogicVariable variable) {
/// @return true if this assignment can be safely eliminated
boolean canEliminate(LogicInstruction instruction, LogicVariable variable) {
if (variable.isVolatile()) return false;
if (variable.getType() == ArgumentType.FUNCTION_RETVAL
&& (instruction.getOpcode() == Opcode.CALL || instruction.getOpcode() == Opcode.CALLREC)) return false;

return switch (variable.getType()) {
case COMPILER, FUNCTION_RETADDR, PARAMETER -> false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static info.teksol.mc.mindcode.compiler.astcontext.AstSubcontextType.PARAMETERS;
import static info.teksol.mc.mindcode.compiler.astcontext.AstSubcontextType.RECURSIVE_CALL;
import static info.teksol.mc.mindcode.compiler.astcontext.AstSubcontextType.*;
import static info.teksol.mc.mindcode.compiler.optimization.OptimizationCoordinator.TRACE;

@NullMarked
Expand Down Expand Up @@ -59,7 +58,10 @@ class VariableStates {

/// Maps variables to a list of instructions defining its current value (potentially more than one
/// due to branching).
private final Map<LogicVariable, List<LogicInstruction>> definitions;
private final Map<LogicVariable, Definition> definitions;

/// Maps variables to their last encountered read instruction position.
private final Map<LogicVariable, Integer> reads;

/// Identifies instructions that do not have to be kept, because they set value the variable already had.
/// Organized by variable for easier housekeeping.
Expand Down Expand Up @@ -103,6 +105,7 @@ public VariableStates() {
values = new LinkedHashMap<>();
definitions = new HashMap<>();
equivalences = new HashMap<>();
reads = new HashMap<>();
useless = new HashMap<>();
initialized = new HashSet<>();
stored = new HashSet<>();
Expand All @@ -115,6 +118,7 @@ private VariableStates(VariableStates other, int id, boolean isolated, boolean r
values = new LinkedHashMap<>(other.values);
definitions = deepCopy(other.definitions);
equivalences = new HashMap<>(other.equivalences);
reads = new HashMap<>(other.reads);
useless = new HashMap<>(other.useless);
initialized = new HashSet<>(other.initialized);
stored = new HashSet<>(other.stored);
Expand All @@ -126,9 +130,8 @@ private VariableStates(VariableStates other, int id, boolean isolated) {
this(other, id, isolated, other.reachable);
}

private static Map<LogicVariable, List<LogicInstruction>> deepCopy(Map<LogicVariable, List<LogicInstruction>> original) {
return original.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,
e -> new ArrayList<>(e.getValue())));
private static Map<LogicVariable, Definition> deepCopy(Map<LogicVariable, Definition> original) {
return original.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,e -> e.getValue().copy()));
}

/// @return useless instructions, organized by variable they produce.
Expand Down Expand Up @@ -183,7 +186,7 @@ public VariableStates setDead(boolean markRead) {
definitions.entrySet().stream()
.filter(e -> e.getKey().isMainVariable() || e.getKey().isGlobalVariable())
.forEachOrdered(e -> optimizer.orphans.computeIfAbsent(e.getKey(),
l -> new ArrayList<>()).addAll(e.getValue()));
l -> new ArrayList<>()).addAll(e.getValue().instructions));
}
dead = true;
modifications++;
Expand Down Expand Up @@ -295,7 +298,8 @@ public void valueSet(LogicVariable variable, LogicInstruction instruction, @Null
optimizer.defines.add(instruction);
}
initialized.add(variable);
definitions.put(variable, List.of(instruction));
definitions.put(variable, new Definition(optimizer.getCounter(), List.of(instruction)));
reads.remove(variable);

// Purge expressions based on the previous value of this variable
invalidateVariable(variable);
Expand Down Expand Up @@ -350,6 +354,7 @@ public void valueReset(LogicVariable variable) {
} else {
trace(() -> "Value reset: " + variable.toMlog());
values.remove(variable);
reads.remove(variable);
invalidateVariable(variable);
}
}
Expand Down Expand Up @@ -409,6 +414,8 @@ public void updateAfterFunctionCall(MindcodeFunction function, LogicInstruction
modifications++;
trace(() -> "Value read: " + variable.toMlog() + " (instance " + getId() + ")" + (ixReachable ? "" : " instruction unreachable)"));

reads.put(variable, optimizer.getCounter());

// Do not report uninitialized reads in unreachable and dead states
boolean report = reportUninitialized && !isolated && !dead && reachable && ixReachable;
if (report && !initialized.contains(variable) && variable.getType() != ArgumentType.BLOCK) {
Expand All @@ -417,16 +424,54 @@ public void updateAfterFunctionCall(MindcodeFunction function, LogicInstruction
}

if (definitions.containsKey(variable)) {
List<LogicInstruction> definingInstructions = definitions.get(variable).instructions;
if (TRACE) {
trace("Definitions of " + variable.toMlog());
trace(definitions.get(variable).stream().map(this::printInstruction));
trace(definingInstructions.stream().map(this::printInstruction));
}
// Variable value was read, keep all instructions that define its value
if (!isolated && ixReachable) {
if (TRACE) {
trace(definitions.get(variable).stream().map(ix -> "--> Keeping instruction: " + ix.toMlog() + " (variable read)"));
trace(definingInstructions.stream().map(ix -> "--> Keeping instruction: " + ix.toMlog() + " (variable read)"));
}
optimizer.keep.addAll(definingInstructions);
}

// When a variable is read, we're storing information about that read with all defining instructions
// of that variable. When it is discovered that there's precisely one set instruction reading that variable,
// the target of the set instruction can be injected into the definition and the set instruction can be
// removed.
//
// However, this is no possible if the target of the set instruction has been read between the defining
// instruction and current instruction, since this would change the value of the target variable in that
// read.
//
// When the value is read by an instruction other than set, the information is recorded too, because
// it means the substitution cannot be made.
//
// Process:
// - all defining instructions are inspected
// - a valid reference is added when these conditions are met:
// - the referring instruction is a set instruction reading current variable
// - definition of the variable is known (it is, we're here)
// - definition of the variable is just one instruction (we're being safe)
// - the result of the set wasn't read after this variable was defined
// - the set is not part of for-each iterator setup
// - in the other case, an invalid reference is added
int defineIndex = definitions.get(variable).counter;
for (LogicInstruction definition : definingInstructions) {
LogicInstruction reference;
if (definingInstructions.size() == 1 && instruction instanceof SetInstruction set && set.getValue() == variable
&& !set.getAstContext().matches(ITR_LEADING, ITR_TRAILING)) {
Integer readIndex = reads.get(set.getResult());
reference = readIndex == null || readIndex < defineIndex ? instruction : null;
} else {
reference = null;
}
optimizer.keep.addAll(definitions.get(variable));

optimizer.definitionReferences.computeIfAbsent(definition, key -> new HashMap<>())
.computeIfAbsent(variable, v -> new ArrayList<>()).add(reference);
trace("Registering usage of " + variable.toMlog());
}
}

Expand All @@ -441,9 +486,9 @@ public void updateAfterFunctionCall(MindcodeFunction function, LogicInstruction
public void protectVariable(LogicVariable variable) {
if (definitions.containsKey(variable)) {
if (TRACE) {
trace(definitions.get(variable).stream().map(ix -> "--> Keeping instruction: " + ix.toMlog() + " (variable protected)"));
trace(definitions.get(variable).instructions.stream().map(ix -> "--> Keeping instruction: " + ix.toMlog() + " (variable protected)"));
}
optimizer.keep.addAll(definitions.get(variable));
optimizer.keep.addAll(definitions.get(variable).instructions);
}
}

Expand Down Expand Up @@ -487,6 +532,12 @@ public VariableStates merge(VariableStates other, boolean propagateUninitialized

merge(definitions, other.definitions);

for (LogicVariable variable : other.reads.keySet()) {
if (!reads.containsKey(variable) || reads.get(variable) < other.reads.get(variable)) {
reads.put(variable, other.reads.get(variable));
}
}

// Only keep values that are the same in both instances
values.keySet().retainAll(other.values.keySet());
for (LogicVariable variable : other.values.keySet()) {
Expand Down Expand Up @@ -525,23 +576,24 @@ public VariableStates merge(VariableStates other, boolean propagateUninitialized
///
/// @param map1 instance to merge into
/// @param map2 instance to be merged
private void merge(Map<LogicVariable, List<LogicInstruction>> map1, Map<LogicVariable, List<LogicInstruction>> map2) {
private void merge(Map<LogicVariable, Definition> map1, Map<LogicVariable, Definition> map2) {
for (LogicVariable variable : map2.keySet()) {
if (map1.containsKey(variable)) {
List<LogicInstruction> current = map1.get(variable);
List<LogicInstruction> theOther = map2.get(variable);
if (!sameInstances(current, theOther)) {
Definition current = map1.get(variable);
Definition theOther = map2.get(variable);
if (current.counter != theOther.counter || !sameInstances(current.instructions, theOther.instructions)) {
// Merge the two lists
if (current.size() == 1 && theOther.size() == 1) {
ArrayList<LogicInstruction> union = new ArrayList<>();
union.add(current.getFirst());
union.add(theOther.getFirst());
map1.put(variable, union);
if (current.instructions.size() == 1 && theOther.instructions.size() == 1) {
map1.put(variable, new Definition(
Math.min(current.counter, theOther.counter),
List.of(current.instructions.getFirst(), theOther.instructions.getFirst())));
} else {
Set<LogicInstruction> union = createIdentitySet(current.size() + theOther.size());
union.addAll(current);
union.addAll(theOther);
map1.put(variable, new ArrayList<>(union));
Set<LogicInstruction> union = createIdentitySet(current.instructions.size() + theOther.instructions.size());
union.addAll(current.instructions);
union.addAll(theOther.instructions);
map1.put(variable, new Definition(
Math.min(current.counter, theOther.counter),
List.copyOf(union)));
}
invalidateVariable(variable);
}
Expand Down Expand Up @@ -572,7 +624,7 @@ void print(String title) {
values.values().forEach(v -> trace(" " + v));
definitions.forEach((k, v) -> {
trace(" Definitions of " + k.toMlog());
v.forEach(ix -> trace(" " + optimizer.instructionIndex(ix)
v.instructions.forEach(ix -> trace(" " + optimizer.instructionIndex(ix)
+ ": " + LogicInstructionPrinter.toString(instructionProcessor, ix)));
});
equivalences.forEach((k, v) -> trace(" Equivalence: " + k.toMlog() + " = " + v.toMlog()));
Expand All @@ -581,6 +633,10 @@ void print(String title) {
} else {
trace(" Initialized: " + initialized.stream().map(LogicVariable::toMlog).collect(Collectors.joining(", ")));
}
if (!reads.isEmpty()) {
trace(" Reads:");
reads.forEach((k, v) -> trace(" " + k.toMlog() + ": " + v));
}
}
}
}
Expand Down Expand Up @@ -771,6 +827,27 @@ public String toString() {
}
}

static class Definition {
/// The instruction counter of this variable's definition
final int counter;

/// List of defining instructions
final List<LogicInstruction> instructions;

public Definition(int counter, LogicInstruction instruction) {
this(counter, List.of(instruction));
}

public Definition(int counter, List<LogicInstruction> instructions) {
this.counter = counter;
this.instructions = instructions;
}

public Definition copy() {
return new Definition(counter, List.copyOf(instructions));
}
}

private void trace(Stream<String> text) {
optimizationContext.trace(text);
}
Expand Down
Loading

0 comments on commit 3e24d1e

Please sign in to comment.