From 8e20927c092ec276aeccc8d13769937957e49a25 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 1 Mar 2026 22:53:04 -0500 Subject: [PATCH 001/120] Adds tests for ensuring the output from the optimizing compiler. Add OptimizationLevel enum to select level of optimizaiton; BaseIntegrationTest uses no optimization. Default the interpreter to no optimization since it won't be worth it there. --- .../Execution/BaseIntegrationTest.cs | 4 +- .../Execution/OptimizationTest.cs | 122 ++++++++++++++++++ src/kOS.Safe/Compilation/CompilerOptions.cs | 2 + src/kOS.Safe/Compilation/OptimizationLevel.cs | 43 ++++++ src/kOS/Screen/Interpreter.cs | 3 +- 5 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 src/kOS.Safe.Test/Execution/OptimizationTest.cs create mode 100644 src/kOS.Safe/Compilation/OptimizationLevel.cs diff --git a/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs b/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs index 615e99466..67b9db229 100644 --- a/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs +++ b/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs @@ -39,6 +39,7 @@ public abstract class BaseIntegrationTest private SafeSharedObjects shared; private Screen screen; private string baseDir; + protected virtual OptimizationLevel OptimizationLevel => OptimizationLevel.None; private string FindKerboscriptTests() { @@ -86,7 +87,8 @@ protected void RunScript(string fileName) IsCalledFromRun = false, FuncManager = shared.FunctionManager, BindManager = shared.BindingMgr, - AllowClobberBuiltins = SafeHouse.Config.AllowClobberBuiltIns + AllowClobberBuiltins = SafeHouse.Config.AllowClobberBuiltIns, + OptimizationLevel = OptimizationLevel }); cpu.Boot(); diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs new file mode 100644 index 000000000..bae72f254 --- /dev/null +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -0,0 +1,122 @@ +using System; +using NUnit.Framework; +using kOS.Safe.Compilation; + +namespace kOS.Safe.Test.Execution +{ + [TestFixture] + public class OptimizationTest : BaseIntegrationTest + { + protected override OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + + [Test] + public void TestBasic() + { + // Tests that the bare minimum works + + RunScript("integration/basic.ks"); + RunSingleStep(); + AssertOutput( + "text" + ); + } + + [Test] + public void TestVars() + { + // Tests that basic variable assignment and reference works + + RunScript("integration/vars.ks"); + RunSingleStep(); + AssertOutput( + "1", + "2", + "3" + ); + } + + [Test] + public void TestFunc() + { + // Tests that basic no-args function calls work + + RunScript("integration/func.ks"); + RunSingleStep(); + AssertOutput( + "a" + ); + } + + [Test] + public void TestFuncArgs() + { + // Tests that explicit and default function parameters work + + RunScript("integration/func_args.ks"); + RunSingleStep(); + AssertOutput( + "0", + "1", + "2", + "3" + ); + } + + [Test] + public void TestOperators() + { + // Test that all the basic operators work + + RunScript("integration/operators.ks"); + RunSingleStep(); + AssertOutput( + "1", + "1", + "True", + "True", + "3", + "6", + "2", + "9", + "True", + "True", + "True", + "True", + "True", + "True", + "True", + "True" + ); + } + + [Test] + public void TestLock() + { + // Test that locks in the same file works + RunScript("integration/lock.ks"); + RunSingleStep(); + AssertOutput( + "3", + "4", + "5" + ); + } + + [Test] + public void TestSuffixes() + { + // Test that various suffix and index combinations work for getting and setting + RunScript("integration/suffixes.ks"); + RunSingleStep(); + RunSingleStep(); + AssertOutput( + "0", + "1", + "2", + "3", + "0", + "False" + ); + } + } +} diff --git a/src/kOS.Safe/Compilation/CompilerOptions.cs b/src/kOS.Safe/Compilation/CompilerOptions.cs index 9c16e16b6..40714786f 100644 --- a/src/kOS.Safe/Compilation/CompilerOptions.cs +++ b/src/kOS.Safe/Compilation/CompilerOptions.cs @@ -18,6 +18,7 @@ public class CompilerOptions public bool AllowClobberBuiltins { get; set; } public IFunctionManager FuncManager { get; set; } public IBindingManager BindManager { get; set; } + public OptimizationLevel OptimizationLevel { get; set; } public CompilerOptions() { LoadDefaults(); @@ -30,6 +31,7 @@ private void LoadDefaults() BindManager = null; IsCalledFromRun = true; AllowClobberBuiltins = false; + OptimizationLevel = OptimizationLevel.Balanced; } public bool BuiltInFunctionExists(string identifier) diff --git a/src/kOS.Safe/Compilation/OptimizationLevel.cs b/src/kOS.Safe/Compilation/OptimizationLevel.cs new file mode 100644 index 000000000..c0cdc63b4 --- /dev/null +++ b/src/kOS.Safe/Compilation/OptimizationLevel.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace kOS.Safe.Compilation +{ + /* + * O1: + * Replace ship fields with their alias + * Constant propagation, including items from the constant structure + * Replace lex indexing with string constant with suffixing where possible + * Dead code elimination (for the never used case) + * Replace constant() with constant + * O2: + * Redundant expression elimination + * Particularly: Any expression of 3 opcodes used more than twice, + * or any expression of >3 opcodes used more than once + * Replace parameterless suffix method calls with get member (this feels like cheating...) + * Boolean logical simplification + * Code motion - moving expressions outside loops if they do not depend on loop variables + * Replace X * X * ... * X with X^N + * Carry's - moving the N-1 D lookup of >1D lists outside the innermost loop + * Loop jamming? (Combining adjacent loops into one) + * Unswitching (moving conditional evaluation outside the loop) - low priority + * Linear function test replacement - low priority + * O3: + * Local function inlining + * Constant propagation to local functions + * Loop stack manipulation (delayed setting of either index or aggregator) + * O4: + * Constant loop unrolling + */ + public enum OptimizationLevel : int + { + None = 0, // No changes whatsoever + Minimal = 1, // Only changes that trim opcodes without changing any flow + Balanced = 2, // Only changes that trim opcodes without increasing file size + Aggressive = 3, // Favor trimming opcodes over file size. + Extreme = 4 // Include constant loop unrolling + } +} diff --git a/src/kOS/Screen/Interpreter.cs b/src/kOS/Screen/Interpreter.cs index 997fc3d20..41acd0f2f 100644 --- a/src/kOS/Screen/Interpreter.cs +++ b/src/kOS/Screen/Interpreter.cs @@ -140,7 +140,8 @@ protected void CompileCommand(string commandText) FuncManager = Shared.FunctionManager, BindManager = Shared.BindingMgr, AllowClobberBuiltins = SafeHouse.Config.AllowClobberBuiltIns, - IsCalledFromRun = false + IsCalledFromRun = false, + OptimizationLevel = OptimizationLevel.None }; List commandParts = Shared.ScriptHandler.Compile(new InterpreterPath(this), From 844d8df81de032a178e7f0badd47a623f6e1a53f Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 1 Mar 2026 22:59:08 -0500 Subject: [PATCH 002/120] Initial implementation of Three-Address Code interim representation. IRBuilder generates basic blocks, which IREmitter can then convert back to kRISC opcodes. This initial implementation does not accomplish any optimization yet (the opposite sometimes) but all current tests pass on the outputted opcodes. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 85 +++ src/kOS.Safe/Compilation/IR/IRBuilder.cs | 272 +++++++++ src/kOS.Safe/Compilation/IR/IREmitter.cs | 47 ++ src/kOS.Safe/Compilation/IR/IRInstruction.cs | 588 +++++++++++++++++++ src/kOS.Safe/Compilation/IR/IROptimizer.cs | 23 + src/kOS.Safe/Compilation/IR/IRValue.cs | 89 +++ src/kOS.Safe/Compilation/KS/Compiler.cs | 16 + src/kOS.Safe/Compilation/Opcode.cs | 2 +- src/kOS.Safe/Compilation/ProgramBuilder.cs | 12 +- 9 files changed, 1130 insertions(+), 4 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/BasicBlock.cs create mode 100644 src/kOS.Safe/Compilation/IR/IRBuilder.cs create mode 100644 src/kOS.Safe/Compilation/IR/IREmitter.cs create mode 100644 src/kOS.Safe/Compilation/IR/IRInstruction.cs create mode 100644 src/kOS.Safe/Compilation/IR/IROptimizer.cs create mode 100644 src/kOS.Safe/Compilation/IR/IRValue.cs diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs new file mode 100644 index 000000000..ff3b992d1 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR +{ + public class BasicBlock + { + public int StartIndex { get; } + public int EndIndex { get; } + public List Instructions { get; } = new List(); + public List Predecessors { get; } = new List(); + public string Label => $"@BB#{ID}"; + public int ID { get; } + private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. +#if DEBUG + internal Opcode[] OriginalOpcodes { get; set; } + internal Opcode[] GeneratedOpcodes => EmitOpCodes().ToArray(); +#endif + + public BasicBlock(int startIndex, int endIndex, int id) + { + StartIndex = startIndex; + EndIndex = endIndex; + ID = id; + } + + public void Add(IRInstruction instruction) + => Instructions.Add(instruction); + + public void SetStackState(Stack stack) + { + while (stack.Count > 0) + exitStackState.Push(stack.Pop()); + } + + public override string ToString() + { + return $"BasicBlock#{ID}: {StartIndex}-{EndIndex}; {Instructions.Count} Instructions"; + } + + public IEnumerable EmitOpCodes() + { + bool first = true; + if (Instructions.Any()) + { + foreach (IRInstruction instruction in Instructions.Take(Instructions.Count - 1)) + { + foreach (Opcode opcode in instruction.EmitOpcode()) + { + if (first) + { + opcode.Label = Label; + first = false; + } + yield return opcode; + } + } + } + foreach (IRValue stackValue in exitStackState) + { + foreach (Opcode opcode in stackValue.EmitPush()) + { + if (first) + { + opcode.Label = Label; + first = false; + } + yield return opcode; + } + } + if (Instructions.Any()) + { + foreach (Opcode opcode in Instructions.Last().EmitOpcode()) + { + if (first) + { + opcode.Label = Label; + first = false; + } + yield return opcode; + } + } + } + } +} diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs new file mode 100644 index 000000000..8d0e5e895 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR +{ + public class IRBuilder + { + private int nextTempId = 0; + + public List Blocks { get; } = new List(); + + public void Lower(List code) + { + if (code.Count == 0) + return; + Dictionary labels = ProgramBuilder.MapLabels(code); + CreateBlocks(code, labels); + FillBlocks(code, labels); + } + + private void CreateBlocks(List code, Dictionary labels) + { + SortedSet leaders = new SortedSet() { 0 }; + for (int i = 1; i < code.Count; i++) // The first instruction is always a leader so we can skip 0. + { + if (code[i] is BranchOpcode branch) + { + leaders.Add(i + 1); + if (branch.DestinationLabel != string.Empty) + leaders.Add(labels[branch.DestinationLabel]); + else + leaders.Add(i + branch.Distance); + } + else if (code[i] is OpcodeJumpStack jumpstack) + { + throw new NotImplementedException("OpcodeJumpStack is not implemented for optimization because it is non-deterministic. Use OptimizationLevel.None."); + } + else if (code[i] is OpcodeReturn) + { + leaders.Add(i + 1); + } + //else if (code[i] is OpcodePushRelocateLater relocateLater) + //{ + //leaders.Add(labels[relocateLater.DestinationLabel]); + //} + } + leaders.Add(code.Count); + foreach (int startIndex in leaders.Take(leaders.Count - 1)) + { + int endIndex = leaders.First(i => i > startIndex) - 1; + BasicBlock block = new BasicBlock(startIndex, endIndex, Blocks.Count); + Blocks.Add(block); + } + foreach (BasicBlock block in Blocks) + { + Opcode lastOpcode = code[block.EndIndex]; + if (lastOpcode is BranchOpcode branch) + { + int destinationIndex = branch.DestinationLabel != string.Empty ? labels[branch.DestinationLabel] : block.EndIndex + branch.Distance; + Blocks.First(b => b.StartIndex == destinationIndex).Predecessors.Add(block); + if (!(branch is OpcodeBranchJump)) + Blocks.First(b => b.StartIndex == block.EndIndex + 1).Predecessors.Add(block); + } +#if DEBUG + block.OriginalOpcodes = code.ToArray(); +#endif + } + } + + private void FillBlocks(List code, Dictionary labels) + { + Stack stack = new Stack(); + BasicBlock currentBlock = Blocks.First(b => b.StartIndex == 0); + for (int i = 0; i < code.Count; i++) + { + if (i > currentBlock.EndIndex) + { + currentBlock.SetStackState(stack); + currentBlock = Blocks.First(b => b.StartIndex == i); + } + ParseInstruction(code[i], currentBlock, stack, labels, i); + } + } + + private IRTemp CreateTemp() + { + IRTemp result = new IRTemp(nextTempId); + nextTempId++; + return result; + } + + private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack stack, Dictionary labels, int index) + { + HashSet variables = new HashSet(); + switch (opcode) + { + case OpcodeStore store: + IRAssign assignment = new IRAssign(store, stack.Pop()) { Scope = IRAssign.StoreScope.Ambivalent }; + variables.Add(assignment.Target); + currentBlock.Add(assignment); + break; + case OpcodeStoreExist storeExist: + assignment = new IRAssign(storeExist, stack.Pop()) { AssertExists = true }; + variables.Add(assignment.Target); + currentBlock.Add(assignment); + break; + case OpcodeStoreLocal storeLocal: + assignment = new IRAssign(storeLocal, stack.Pop()) { Scope = IRAssign.StoreScope.Local }; + currentBlock.Add(assignment); + variables.Add(assignment.Target); + break; + case OpcodeStoreGlobal storeGlobal: + assignment = new IRAssign(storeGlobal, stack.Pop()) { Scope = IRAssign.StoreScope.Global }; + currentBlock.Add(assignment); + variables.Add(assignment.Target); + break; + case OpcodeExists exists: + IRTemp temp = CreateTemp(); + IRInstruction instruction = new IRUnaryOp(temp, exists, stack.Pop()); + temp.Parent = instruction; + //currentBlock.Add(instruction); + stack.Push(temp); + break; + case OpcodeUnset unset: + currentBlock.Add(new IRUnaryConsumer(unset, stack.Pop(), true)); + break; + case OpcodeGetMethod getMethod: + temp = CreateTemp(); + instruction = new IRSuffixGetMethod(temp, stack.Pop(), getMethod); + //currentBlock.Add(instruction); + temp.Parent = instruction; + stack.Push(temp); + break; + case OpcodeGetMember getMember: + temp = CreateTemp(); + instruction = new IRSuffixGet(temp, stack.Pop(), getMember); + temp.Parent = instruction; + //currentBlock.Add(instruction); + stack.Push(temp); + break; + case OpcodeSetMember setMember: + IRValue value = stack.Pop(); + IRValue memberObj = stack.Pop(); + currentBlock.Add(new IRSuffixSet(memberObj, value, setMember)); + break; + case OpcodeGetIndex _: + IRValue targetIndex = stack.Pop(); + IRValue indexObj = stack.Pop(); + temp = CreateTemp(); + instruction = new IRIndexGet(temp, indexObj, targetIndex); + //currentBlock.Add(instruction); + temp.Parent = instruction; + stack.Push(temp); + break; + case OpcodeSetIndex _: + value = stack.Pop(); + targetIndex = stack.Pop(); + indexObj = stack.Pop(); + currentBlock.Add(new IRIndexSet(indexObj, targetIndex, value)); + break; + case OpcodeEOF _: + case OpcodeEOP _: + case OpcodeNOP _: + case OpcodeBogus _: + case OpcodePushScope _: + case OpcodePopScope _: + currentBlock.Add(new IRNoStackInstruction(opcode)); + break; + case OpcodeBranchIfTrue branchIfTrue: + currentBlock.Add(new IRBranch(stack.Pop(), + Blocks.First(b => b.StartIndex == labels[branchIfTrue.DestinationLabel]), + Blocks.First(b => b.StartIndex == currentBlock.EndIndex + 1))); + break; + case OpcodeBranchIfFalse branchIfFalse: + currentBlock.Add(new IRBranch(stack.Pop(), + Blocks.First(b => b.StartIndex == currentBlock.EndIndex + 1), + Blocks.First(b => b.StartIndex == labels[branchIfFalse.DestinationLabel])) + { PreferFalse = true }); + break; + case OpcodeBranchJump branchJump: + int destinationIndex = branchJump.DestinationLabel != string.Empty ? labels[branchJump.DestinationLabel] : index + branchJump.Distance; + if (branchJump.DestinationLabel == string.Empty) + { + // TODO + bool test = index == currentBlock.EndIndex; + } + currentBlock.Add(new IRJump(Blocks.First(b => b.StartIndex == destinationIndex))); + break; + case OpcodeJumpStack _: + throw new NotImplementedException("OpcodeJumpStack is not implemented for optimization because it is non-deterministic. Use OptimizationLevel.None."); + case OpcodeCompareGT _: + case OpcodeCompareLT _: + case OpcodeCompareGTE _: + case OpcodeCompareLTE _: + case OpcodeCompareNE _: + case OpcodeCompareEqual _: + case OpcodeMathAdd _: + case OpcodeMathSubtract _: + case OpcodeMathMultiply _: + case OpcodeMathDivide _: + case OpcodeMathPower _: + temp = CreateTemp(); + IRValue right = stack.Pop(); + IRValue left = stack.Pop(); + instruction = new IRBinaryOp(temp, (BinaryOpcode)opcode, left, right); + //currentBlock.Add(instruction); + temp.Parent = instruction; + stack.Push(temp); + break; + case OpcodeMathNegate _: + case OpcodeLogicToBool _: + case OpcodeLogicNot _: + temp = CreateTemp(); + instruction = new IRUnaryOp(temp, opcode, stack.Pop()); + temp.Parent = instruction; + //currentBlock.Add(instruction); + stack.Push(temp); + break; + case OpcodeCall call: + temp = CreateTemp(); + Stack arguments = new Stack(); + bool hasArgmarker = stack.Count > 0; // Not even an argument marker on the stack - the dominator block must have it + while (stack.Count > 0) + { + IRValue stackResult = stack.Pop(); + if (stackResult is IRConstant constant && constant.Value is Execution.KOSArgMarkerType) + break; + arguments.Push(stackResult); + } + instruction = new IRCall(temp, call, hasArgmarker, arguments); + if (stack.Count > 0 && !((IRCall)instruction).Direct) + { + ((IRCall)instruction).IndirectMethod = stack.Pop(); + } + temp.Parent = instruction; + currentBlock.Add(instruction); + stack.Push(temp); + break; + case OpcodeReturn opcodeReturn: + currentBlock.Add(new IRReturn(opcodeReturn.Depth) { Value = stack.Pop() }); + break; + case OpcodePush opcodePush: + object argument = opcodePush.Argument; + if (argument is string identifier && identifier.StartsWith("$")) + stack.Push(new IRVariable(identifier)); + else + stack.Push(new IRConstant(argument)); + break; + case OpcodePushDelegateRelocateLater delegateRelocateLater: + stack.Push(new IRDelegateRelocateLater(delegateRelocateLater.DestinationLabel, delegateRelocateLater.WithClosure)); + break; + case OpcodePushRelocateLater relocateLater: + stack.Push(new IRRelocateLater(relocateLater.DestinationLabel)); + break; + case OpcodeAddTrigger _: + case OpcodeRemoveTrigger _: + currentBlock.Add(new IRUnaryConsumer(opcode, stack.Pop(), false)); + break; + case OpcodeWait _: + currentBlock.Add(new IRUnaryConsumer(opcode, stack.Pop(), true)); + break; + case OpcodePop pop: + stack.Pop(); + currentBlock.Add(new IRPop()); + break; + default: + throw new NotImplementedException($"The Opcode of type {opcode.GetType()} is not implemented."); + } + } + } +} \ No newline at end of file diff --git a/src/kOS.Safe/Compilation/IR/IREmitter.cs b/src/kOS.Safe/Compilation/IR/IREmitter.cs new file mode 100644 index 000000000..31d90d999 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IREmitter.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR +{ + public static class IREmitter + { + public static List Emit(List blocks) + { + List result = new List(); + Dictionary jumpLabels = new Dictionary(); + // Emit Opcodes for each block + foreach (BasicBlock block in blocks) + { + jumpLabels.Add(block.Label, result.Count); + result.AddRange(block.EmitOpCodes()); + } + // Remove single-line jumps from fallthrough blocks + for (int i = 0; i < result.Count; i++) + { + Opcode opcode = result[i]; + if (i < result.Count - 1 && opcode is OpcodeBranchJump jump && + jump.DestinationLabel != null && jump.DestinationLabel == result[i + 1].Label) + { + foreach (BasicBlock block in blocks) + if (jumpLabels[block.Label] >= i) + jumpLabels[block.Label] = jumpLabels[block.Label] - 1; + result.RemoveAt(i); + i--; + } + } + // Restore original labels + foreach (Opcode opcode in result) + { + if (opcode.Label != null && jumpLabels.ContainsKey(opcode.Label)) + opcode.Label = CreateLabel(jumpLabels[opcode.Label]); + if (opcode.DestinationLabel != null && jumpLabels.ContainsKey(opcode.DestinationLabel)) + opcode.DestinationLabel = CreateLabel(jumpLabels[opcode.DestinationLabel]); + } + return result; + } + + private static string CreateLabel(int index) + => string.Format("@{0:0000}", index); + } +} diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs new file mode 100644 index 000000000..b8a4f2fd4 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -0,0 +1,588 @@ +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR +{ + public abstract class IRInstruction + { + // Should-be-static + public abstract bool SideEffects { get; } + internal abstract IEnumerable EmitOpcode(); + } + public abstract class IRInteractsInstruction : IRInstruction + { + public override bool SideEffects { get; } + public IRInteractsInstruction(IRValue interactor) + { + switch (interactor.Type) + { + case IRValue.ValueType.Value: + SideEffects = false; + break; + default: + SideEffects = true; + break; + } + } + } + public class IRAssign : IRInstruction + { + public enum StoreScope + { + Ambivalent, + Local, + Global + } + public override bool SideEffects => false; + public string Target { get; } + public IRValue Value { get; } + public StoreScope Scope { get; set; } = StoreScope.Ambivalent; + public bool AssertExists { get; set; } = false; + public IRAssign(string target, IRValue value) + { + Target = target; + Value = value; + } + public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : this(opcode.Identifier, value) { } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Value.EmitPush()) + yield return opcode; + if (AssertExists) + { + yield return new OpcodeStoreExist(Target); + yield break; + } + switch (Scope) + { + case StoreScope.Local: + yield return new OpcodeStoreLocal(Target); + yield break; + case StoreScope.Global: + yield return new OpcodeStoreGlobal(Target); + yield break; + default: + case StoreScope.Ambivalent: + yield return new OpcodeStore(Target); + yield break; + } + } + public override string ToString() + => string.Format("{{store {0}}}", Value.ToString()); + } + public class IRBinaryOp : IRInstruction + { + public override bool SideEffects => false; + public IRTemp Result { get; } + public BinaryOpcode Operation { get; protected set; } + public IRValue Left { get; protected set; } + public IRValue Right { get; protected set; } + public bool Commutative { get; } + public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) + { + Result = result; + Operation = operation; + Left = left; + Right = right; + Commutative = (operation is OpcodeCompareEqual) || + (operation is OpcodeCompareNE) || + (operation is OpcodeCompareGT) || + (operation is OpcodeCompareLT) || + (operation is OpcodeCompareGTE) || + (operation is OpcodeCompareLTE) || + (operation is OpcodeMathAdd) || + (operation is OpcodeMathMultiply); + } + public void ReverseOperands() + { + if (!Commutative) + throw new System.InvalidOperationException($"{this} is not commutative."); + (Right, Left) = (Left, Right); + switch (Operation) + { + case OpcodeCompareGT _: + Operation = new OpcodeCompareLTE(); + break; + case OpcodeCompareLT _: + Operation = new OpcodeCompareGTE(); + break; + case OpcodeCompareGTE _: + Operation = new OpcodeCompareLT(); + break; + case OpcodeCompareLTE _: + Operation = new OpcodeCompareGT(); + break; + } + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Left.EmitPush()) + yield return opcode; + foreach (Opcode opcode in Right.EmitPush()) + yield return opcode; + Operation.Label = string.Empty; + yield return Operation; + } + public override string ToString() + => Operation.ToString(); + } + public class IRUnaryOp : IRInstruction + { + public override bool SideEffects => false; + public IRTemp Result { get; } + public Opcode Operation { get; } + public IRValue Operand { get; } + public IRUnaryOp(IRTemp result, Opcode operation, IRValue operand) + { + Result = result; + Operation = operation; + Operand = operand; + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Operand.EmitPush()) + yield return opcode; + Operation.Label = string.Empty; + yield return Operation; + } + public override string ToString() + => Operation.ToString(); + } + public class IRNoStackInstruction : IRInstruction + { + public override bool SideEffects => false; + public Opcode Operation { get; } + public IRNoStackInstruction(Opcode opcode) + => Operation = opcode; + internal override IEnumerable EmitOpcode() + { + Operation.Label = string.Empty; + yield return Operation; + } + public override string ToString() + => Operation.ToString(); + } + public class IRUnaryConsumer : IRInstruction + { + public override bool SideEffects { get; } + public Opcode Operation { get; } + public IRValue Operand { get; } + public IRUnaryConsumer(Opcode opcode, IRValue operand, bool sideEffects = false) + { + Operation = opcode; + Operand = operand; + SideEffects = sideEffects; + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Operand.EmitPush()) + yield return opcode; + Operation.Label = string.Empty; + yield return Operation; + } + public override string ToString() + => Operation.ToString(); + } + public class IRPop : IRInstruction + { + public override bool SideEffects => false; + internal override IEnumerable EmitOpcode() + { + yield return new OpcodePop(); + } + public override string ToString() + => "{pop}"; + } + public class IRNonVarPush : IRInstruction + { + public override bool SideEffects => false; + public Opcode Operation { get; } + public IRNonVarPush(Opcode opcode) + => Operation = opcode; + internal override IEnumerable EmitOpcode() + { + Operation.Label = string.Empty; + yield return Operation; + } + public override string ToString() + => Operation.ToString(); + } + public class IRSuffixGet : IRInteractsInstruction + { + public IRTemp Result { get; } + public IRValue Object { get; } + public string Suffix { get; } + public IRSuffixGet(IRTemp result, IRValue obj, string suffix) : base(obj) + { + Result = result; + Object = obj; + Suffix = suffix; + } + public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcode) : this(result, obj, opcode.Identifier) { } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Object.EmitPush()) + yield return opcode; + yield return new OpcodeGetMember(Suffix); + } + public override string ToString() + => string.Format("{{gmb \"{0}\"}}", Suffix); + } + public class IRSuffixGetMethod : IRSuffixGet + { + public override bool SideEffects => false; + public IRSuffixGetMethod(IRTemp result, IRValue obj, string suffix) : base(result, obj, suffix) { } + public IRSuffixGetMethod(IRTemp result, IRValue obj, OpcodeGetMethod opcode) : this(result, obj, opcode.Identifier) { } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Object.EmitPush()) + yield return opcode; + yield return new OpcodeGetMethod(Suffix); + } + public override string ToString() + => string.Format("{{gmet \"{0}\"}}", Suffix); + } + public class IRSuffixSet : IRInteractsInstruction + { + public override bool SideEffects { get; } + public IRValue Object { get; } + public IRValue Value { get; } + public string Suffix { get; } + public IRSuffixSet(IRValue obj, IRValue value, string suffix) : base(obj) + { + Object = obj; + Value = value; + Suffix = suffix; + } + public IRSuffixSet(IRValue obj, IRValue value, OpcodeSetMember opcode) : this(obj, value, opcode.Identifier) { } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Object.EmitPush()) + yield return opcode; + foreach (Opcode opcode in Value.EmitPush()) + yield return opcode; + yield return new OpcodeSetMember(Suffix); + } + public override string ToString() + => string.Format("{{smb \"{0}\"}}", Suffix); + } + public class IRIndexGet : IRInstruction + { + public override bool SideEffects => false; + public IRValue Result { get; } + public IRValue Object { get; } + public IRValue Index { get; } + public IRIndexGet(IRValue result, IRValue obj, IRValue index) + { + Result = result; + Object = obj; + Index = index; + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Object.EmitPush()) + yield return opcode; + foreach (Opcode opcode in Index.EmitPush()) + yield return opcode; + yield return new OpcodeGetIndex(); + } + public override string ToString() + => "{gidx}"; + } + public class IRIndexSet : IRInstruction + { + public override bool SideEffects => false; + public IRValue Object { get; } + public IRValue Index { get; } + public IRValue Value { get; } + public IRIndexSet(IRValue obj, IRValue index, IRValue value) + { + Object = obj; + Index = index; + Value = value; + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Object.EmitPush()) + yield return opcode; + foreach (Opcode opcode in Index.EmitPush()) + yield return opcode; + foreach (Opcode opcode in Value.EmitPush()) + yield return opcode; + yield return new OpcodeSetIndex(); + } + public override string ToString() + => "{sidx}"; + } + public class IRJump : IRInstruction + { + public override bool SideEffects => false; + public BasicBlock Target { get; } + public IRJump(BasicBlock target) + { + Target = target; + } + internal override IEnumerable EmitOpcode() + { + yield return new OpcodeBranchJump() { DestinationLabel = Target.Label }; + } + public override string ToString() + => string.Format("{{jump {0}}}", Target.Label); + } + public class IRJumpStack : IRInstruction + { + public override bool SideEffects => false; + public IRValue Distance { get; } + public List Targets { get; } = new List(); + public IRJumpStack(IRValue distance, IEnumerable targets) + { + Distance = distance; + Targets.AddRange(targets); + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Distance.EmitPush()) + yield return opcode; + yield return new OpcodeJumpStack(); + } + } + public class IRBranch : IRInstruction + { + public override bool SideEffects => false; + public IRValue Condition { get; } + public BasicBlock True { get; } + public BasicBlock False { get; } + public bool PreferFalse { get; set; } = false; + public IRBranch(IRValue condition, BasicBlock onTrue, BasicBlock onFalse) + { + Condition = condition; + True = onTrue; + False = onFalse; + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Condition.EmitPush()) + yield return opcode; + if (PreferFalse) + { + yield return new OpcodeBranchIfFalse() { DestinationLabel = False.Label }; + yield return new OpcodeBranchJump() { DestinationLabel = True.Label }; + } + else + { + yield return new OpcodeBranchIfTrue() { DestinationLabel = True.Label }; + yield return new OpcodeBranchJump() { DestinationLabel = False.Label }; + } + } + public override string ToString() + => string.Format("{{br.? {0}/{1}}}", True.Label, False.Label); + } + public class IRCall : IRInstruction + { + protected static readonly Function.FunctionManager functionManager = new Function.FunctionManager(null); + public override bool SideEffects { get; } + public IRTemp Target { get; } + public string Function { get; } + public List Arguments { get; } = new List(); + public IRValue IndirectMethod { get; internal set; } + public bool Direct { get; } + public bool EmitArgMarker; + private IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker) + { + Target = target; + Function = (string)opcode.Destination; + Direct = opcode.Direct; + SideEffects = CheckIfFunctionHasSideEffects(opcode); + EmitArgMarker = emitArgMarker; + } + private bool CheckIfFunctionHasSideEffects(OpcodeCall opcode) + { + if (!Direct) + return true; + if (opcode.Destination is string functionName && functionManager.Exists(functionName)) + { + functionName = functionName.ToLower().Replace("()", ""); + switch (functionName) + { + case "addAlarm": + case "listAlarms": + case "deleteAlarm": + case "buildlist": + case "vcrs": + case "vectorcrossproduct": + case "vdot": + case "vectordotproduct": + case "vxcl": + case "vectorexclude": + case "vang": + case "vectorangle": + case "clearscreen": + case "hudtext": + case "add": + case "remove": + case "processor": + case "edit": + case "printlist": + case "node": + case "v": + case "r": + case "q": + case "createorbit": + case "rotatefromto": + case "lookdirup": + case "angleaxis": + case "latlng": + case "vessel": + case "body": + case "bodyexists": + case "bodyatmosphere": + case "bounds": + case "heading": + case "slidenote": + case "note": + case "getvoice": + case "stopallvoices": + case "time": + case "timestamp": + case "timespan": + case "hsv": + case "hsva": + case "vecdraw": + case "vecdrawargs": + case "clearvecdraws": + case "clearguis": + case "gui": + case "positionat": + case "velocityat": + case "orbitat": + case "career": + case "allwaypoints": + case "waypoint": + case "lex": + case "lexicon": + case "list": + case "pidloop": + case "queue": + case "stack": + case "uniqueset": + case "abs": + case "mod": + case "floor": + case "ceiling": + case "round": + case "sqrt": + case "ln": + case "log10": + case "min": + case "max": + case "random": + case "randomseed": + case "char": + case "unchar": + case "print": + case "printat": + case "logfile": + case "debugdump": + case "debugfreezegame": + case "profileresult": + case "makebuiltindelegate": + case "droppriority": + case "range": + case "constant": + case "sin": + case "cos": + case "tan": + case "arcsin": + case "arccos": + case "arctan": + case "arctan2": + case "anglediff": + return false; + case "stage": + case "warpto": + case "transfer": + case "transferall": + case "run": + case "load": + case "toggleflybywire": + case "selectautopilotmode": + case "reboot": + case "shutdown": + case "scriptpath": + case "switch": + case "cd": + case "chdir": + case "copy_deprecated": + case "rename_file_deprecated": + case "rename_volume_deprecated": + case "delete_deprecated": + case "copypath": + case "movepath": + case "deletepath": + case "writejson": + case "readjson": + case "exists": + case "open": + case "create": + case "createdir": + case "path": + case "volume": + default: + return true; + } + } + else + return true; + } + internal override IEnumerable EmitOpcode() + { + if (EmitArgMarker) + { + if (IndirectMethod != null) + foreach (Opcode opcode in IndirectMethod.EmitPush()) + yield return opcode; + yield return new OpcodePush(new Execution.KOSArgMarkerType()); + } + foreach (IRValue argument in Arguments) + { + foreach (Opcode opcode in argument.EmitPush()) + yield return opcode; + } + yield return new OpcodeCall(Function); + } + + public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, IRValue argument) : this(target, opcode, emitArgMarker) + { + Arguments.Add(argument); + } + public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, IEnumerable arguments) : this(target, opcode, emitArgMarker) + { + Arguments.AddRange(arguments); + } + public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, params IRValue[] arguments) : this(target, opcode, emitArgMarker) + { + Arguments.AddRange(arguments); + } + public override string ToString() + => string.Format("{{call {0}({1})}}", Function.Trim('(', ')'), string.Join(",", Arguments.Select(a => a.ToString()))); + } + public class IRReturn : IRInstruction + { + public override bool SideEffects => false; + public IRValue Value { get; set; } + public short Depth { get; internal set; } + public IRReturn(short depth) + => Depth = depth; + internal override IEnumerable EmitOpcode() + { + if (Value != null) + foreach (Opcode opcode in Value.EmitPush()) + yield return opcode; + else + yield return new OpcodePush(null); + yield return new OpcodeReturn(Depth); + } + public override string ToString() + => string.Format("{{ret {0}}}", Depth); + } +} diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs new file mode 100644 index 000000000..92024ae50 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace kOS.Safe.Compilation.IR +{ + public class IROptimizer + { + public OptimizationLevel OptimizationLevel { get; } + + public IROptimizer(OptimizationLevel optimizationLevel) + { + OptimizationLevel = optimizationLevel; + } + + public List Optimize(List blocks) + { + return blocks; + } + } +} diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs new file mode 100644 index 000000000..999c9c65e --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; + +namespace kOS.Safe.Compilation.IR +{ + public abstract class IRValue + { + public enum ValueType + { + Unknown = 0, + Value = 1, + GameObject = 2 + } + public ValueType Type { get; } + internal abstract IEnumerable EmitPush(); + } + + public class IRConstant : IRValue + { + public object Value { get; } + public IRConstant(object value) + => Value = value; + internal override IEnumerable EmitPush() + { + yield return new OpcodePush(Value); + } + public override string ToString() + => Value.ToString(); + } + public class IRVariable : IRValue + { + public string Name { get; } + public bool IsLock { get; } + public IRVariable(string name, bool isLock = false) + { + Name = name; + IsLock = isLock; + } + internal override IEnumerable EmitPush() + { + yield return new OpcodePush(Name); + } + public override string ToString() + => Name; + } + public class IRRelocateLater : IRConstant + { + public IRRelocateLater(string value) : base(value) { } + + internal override IEnumerable EmitPush() + { + yield return new OpcodePushRelocateLater((string)Value); + } + } + public class IRDelegateRelocateLater : IRRelocateLater + { + public bool WithClosure { get; } + public IRDelegateRelocateLater(string value, bool withClosure) : base(value) + { + WithClosure = withClosure; + } + internal override IEnumerable EmitPush() + { + yield return new OpcodePushDelegateRelocateLater((string)Value, WithClosure); + } + } + public class IRTemp : IRVariable + { + public int ID { get; } + public IRInstruction Parent { get; internal set; } + private bool isPromoted = false; + public IRTemp(int id) : base($"$.temp.{id}", false) + { + ID = id; + } + public IRAssign PromoteToVariable() + { + isPromoted = true; + return new IRAssign(Name, this) { Scope = IRAssign.StoreScope.Local }; + } + internal override IEnumerable EmitPush() + { + if (isPromoted) + yield return new OpcodePush(Name); + else + foreach (Opcode opcode in Parent.EmitOpcode()) + yield return opcode; + } + } +} diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index 2de644b33..6daeae868 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -187,6 +187,17 @@ private void AssertNoFuncBuiltinViolation(string name) return; } + public static void Optimize(List code, CompilerOptions options) + { + IR.IRBuilder irBuilder = new IR.IRBuilder(); + irBuilder.Lower(code); + List blocks = irBuilder.Blocks; + IR.IROptimizer optimizer = new IR.IROptimizer(options.OptimizationLevel); + blocks = optimizer.Optimize(blocks); + code.Clear(); + code.AddRange(IR.IREmitter.Emit(blocks)); + } + public CodePart Compile(int startLineNum, ParseTree tree, Context context, CompilerOptions options) { this.options = options; @@ -202,6 +213,11 @@ public CodePart Compile(int startLineNum, ParseTree tree, Context context, Compi { PreProcess(tree); CompileProgram(tree); + if (options.OptimizationLevel != OptimizationLevel.None) + { + foreach (List code in new[] { part.InitializationCode, part.FunctionsCode, part.MainCode }) + Optimize(code, options); + } } return part; } diff --git a/src/kOS.Safe/Compilation/Opcode.cs b/src/kOS.Safe/Compilation/Opcode.cs index d7a65a8d1..58394eda7 100644 --- a/src/kOS.Safe/Compilation/Opcode.cs +++ b/src/kOS.Safe/Compilation/Opcode.cs @@ -2004,7 +2004,7 @@ public override string ToString() public class OpcodePush : Opcode { [MLField(1,false)] - private object Argument { get; set; } + public object Argument { get; private set; } protected override string Name { get { return "push"; } } public override ByteCode Code { get { return ByteCode.PUSH; } } diff --git a/src/kOS.Safe/Compilation/ProgramBuilder.cs b/src/kOS.Safe/Compilation/ProgramBuilder.cs index 14908bb4c..1f4633773 100644 --- a/src/kOS.Safe/Compilation/ProgramBuilder.cs +++ b/src/kOS.Safe/Compilation/ProgramBuilder.cs @@ -233,8 +233,8 @@ protected virtual void AddEndOfProgram(CodePart linkedObject, bool isMainProgram } } - private void ReplaceLabels(List program) - { + public static Dictionary MapLabels(List program) + { var labels = new Dictionary(); // get the index of every label @@ -259,7 +259,13 @@ private void ReplaceLabels(List program) labels.Add(program[index].Label, index); } } - + return labels; + } + + private void ReplaceLabels(List program) + { + Dictionary labels = MapLabels(program); + // replace destination labels with the corresponding index for (int index = 0; index < program.Count; index++) { From cd2448ff4b29a741c330b2ac483f67cac9b37612 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 15:19:23 -0500 Subject: [PATCH 003/120] Clean up IR building code. Ensure IRInstructions remember the source code position that their Opcode comes from. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 28 +++-- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 46 +++++--- src/kOS.Safe/Compilation/IR/IREmitter.cs | 24 ++-- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 116 ++++++++++++------- src/kOS.Safe/Compilation/IR/IRValue.cs | 2 +- 5 files changed, 131 insertions(+), 85 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index ff3b992d1..79a245023 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -8,7 +8,8 @@ public class BasicBlock public int StartIndex { get; } public int EndIndex { get; } public List Instructions { get; } = new List(); - public List Predecessors { get; } = new List(); + protected internal readonly HashSet predecessors = new HashSet(); + protected internal readonly HashSet sucessors = new HashSet(); public string Label => $"@BB#{ID}"; public int ID { get; } private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. @@ -27,6 +28,16 @@ public BasicBlock(int startIndex, int endIndex, int id) public void Add(IRInstruction instruction) => Instructions.Add(instruction); + public void AddSuccessor(BasicBlock successor) + { + sucessors.Add(successor); + successor.AddPredecessor(this); + } + protected void AddPredecessor(BasicBlock predecessor) + { + predecessors.Add(predecessor); + } + public void SetStackState(Stack stack) { while (stack.Count > 0) @@ -41,19 +52,16 @@ public override string ToString() public IEnumerable EmitOpCodes() { bool first = true; - if (Instructions.Any()) + foreach (IRInstruction instruction in Instructions.Take(Instructions.Count - 1)) { - foreach (IRInstruction instruction in Instructions.Take(Instructions.Count - 1)) + foreach (Opcode opcode in instruction.EmitOpcode()) { - foreach (Opcode opcode in instruction.EmitOpcode()) + if (first) { - if (first) - { - opcode.Label = Label; - first = false; - } - yield return opcode; + opcode.Label = Label; + first = false; } + yield return opcode; } } foreach (IRValue stackValue in exitStackState) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 8d0e5e895..aa97fdf9a 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -58,9 +58,15 @@ private void CreateBlocks(List code, Dictionary labels) if (lastOpcode is BranchOpcode branch) { int destinationIndex = branch.DestinationLabel != string.Empty ? labels[branch.DestinationLabel] : block.EndIndex + branch.Distance; - Blocks.First(b => b.StartIndex == destinationIndex).Predecessors.Add(block); + block.AddSuccessor(GetBlockFromStartIndex(destinationIndex)); if (!(branch is OpcodeBranchJump)) - Blocks.First(b => b.StartIndex == block.EndIndex + 1).Predecessors.Add(block); + block.AddSuccessor(GetBlockFromStartIndex(block.EndIndex + 1)); + } + else if (Blocks.Any(b => b.StartIndex == block.EndIndex + 1)) + { + BasicBlock successor = GetBlockFromStartIndex(block.EndIndex + 1); + block.Add(new IRJump(successor, lastOpcode.SourceLine, lastOpcode.SourceColumn)); + block.AddSuccessor(successor); } #if DEBUG block.OriginalOpcodes = code.ToArray(); @@ -68,16 +74,19 @@ private void CreateBlocks(List code, Dictionary labels) } } + private BasicBlock GetBlockFromStartIndex(int startIndex) + => Blocks.First(b => b.StartIndex == startIndex); + private void FillBlocks(List code, Dictionary labels) { Stack stack = new Stack(); - BasicBlock currentBlock = Blocks.First(b => b.StartIndex == 0); + BasicBlock currentBlock = GetBlockFromStartIndex(0); for (int i = 0; i < code.Count; i++) { if (i > currentBlock.EndIndex) { currentBlock.SetStackState(stack); - currentBlock = Blocks.First(b => b.StartIndex == i); + currentBlock = GetBlockFromStartIndex(i); } ParseInstruction(code[i], currentBlock, stack, labels, i); } @@ -144,20 +153,20 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack b.StartIndex == labels[branchIfTrue.DestinationLabel]), - Blocks.First(b => b.StartIndex == currentBlock.EndIndex + 1))); + GetBlockFromStartIndex(labels[branchIfTrue.DestinationLabel]), + GetBlockFromStartIndex(currentBlock.EndIndex + 1), + branchIfTrue)); break; case OpcodeBranchIfFalse branchIfFalse: currentBlock.Add(new IRBranch(stack.Pop(), - Blocks.First(b => b.StartIndex == currentBlock.EndIndex + 1), - Blocks.First(b => b.StartIndex == labels[branchIfFalse.DestinationLabel])) - { PreferFalse = true }); + GetBlockFromStartIndex(currentBlock.EndIndex + 1), + GetBlockFromStartIndex(labels[branchIfFalse.DestinationLabel]), + branchIfFalse)); break; case OpcodeBranchJump branchJump: int destinationIndex = branchJump.DestinationLabel != string.Empty ? labels[branchJump.DestinationLabel] : index + branchJump.Distance; @@ -185,7 +195,7 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack b.StartIndex == destinationIndex))); + currentBlock.Add(new IRJump(GetBlockFromStartIndex(destinationIndex), branchJump)); break; case OpcodeJumpStack _: throw new NotImplementedException("OpcodeJumpStack is not implemented for optimization because it is non-deterministic. Use OptimizationLevel.None."); @@ -234,16 +244,15 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack Emit(List blocks) Dictionary jumpLabels = new Dictionary(); // Emit Opcodes for each block foreach (BasicBlock block in blocks) - { - jumpLabels.Add(block.Label, result.Count); - result.AddRange(block.EmitOpCodes()); - } + LabelAndEmit(block, jumpLabels, result); // Remove single-line jumps from fallthrough blocks - for (int i = 0; i < result.Count; i++) + for (int i = 0; i < result.Count - 1; i++) { Opcode opcode = result[i]; - if (i < result.Count - 1 && opcode is OpcodeBranchJump jump && + if (opcode is OpcodeBranchJump jump && jump.DestinationLabel != null && jump.DestinationLabel == result[i + 1].Label) { - foreach (BasicBlock block in blocks) - if (jumpLabels[block.Label] >= i) - jumpLabels[block.Label] = jumpLabels[block.Label] - 1; + foreach (var key in jumpLabels.Keys.ToArray()) + if (jumpLabels[key] >= i) + jumpLabels[key] = jumpLabels[key] - 1; result.RemoveAt(i); i--; } @@ -41,7 +37,13 @@ public static List Emit(List blocks) return result; } + private static void LabelAndEmit(BasicBlock block, Dictionary jumpLabels, List result) + { + jumpLabels.Add(block.Label, result.Count); + result.AddRange(block.EmitOpCodes()); + } + private static string CreateLabel(int index) - => string.Format("@{0:0000}", index); + => string.Format("@{0:0000}", index); // TODO: + 1 } } diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index b8a4f2fd4..1242c38a7 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -5,15 +5,36 @@ namespace kOS.Safe.Compilation.IR { public abstract class IRInstruction { + public short SourceLine { get; } // line number in the source code that this was compiled from. + public short SourceColumn { get; } // column number of the token nearest the cause of this Opcode. + // Should-be-static public abstract bool SideEffects { get; } internal abstract IEnumerable EmitOpcode(); + protected IRInstruction(Opcode originalOpcode) + { + SourceLine = originalOpcode.SourceLine; + SourceColumn = originalOpcode.SourceColumn; + } + protected Opcode SetSourceLocation(Opcode opcode) + { + if (opcode == null) + return opcode; + opcode.SourceLine = SourceLine; + opcode.SourceColumn = SourceColumn; + return opcode; + } } public abstract class IRInteractsInstruction : IRInstruction { public override bool SideEffects { get; } - public IRInteractsInstruction(IRValue interactor) + protected IRInteractsInstruction(IRValue interactor, Opcode originalOpcode) : base(originalOpcode) { + if (interactor is IRConstant) + { + SideEffects = false; + return; + } switch (interactor.Type) { case IRValue.ValueType.Value: @@ -38,32 +59,31 @@ public enum StoreScope public IRValue Value { get; } public StoreScope Scope { get; set; } = StoreScope.Ambivalent; public bool AssertExists { get; set; } = false; - public IRAssign(string target, IRValue value) + public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : base(opcode) { - Target = target; + Target = opcode.Identifier; Value = value; } - public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : this(opcode.Identifier, value) { } internal override IEnumerable EmitOpcode() { foreach (Opcode opcode in Value.EmitPush()) yield return opcode; if (AssertExists) { - yield return new OpcodeStoreExist(Target); + yield return SetSourceLocation(new OpcodeStoreExist(Target)); yield break; } switch (Scope) { case StoreScope.Local: - yield return new OpcodeStoreLocal(Target); + yield return SetSourceLocation(new OpcodeStoreLocal(Target)); yield break; case StoreScope.Global: - yield return new OpcodeStoreGlobal(Target); + yield return SetSourceLocation(new OpcodeStoreGlobal(Target)); yield break; default: case StoreScope.Ambivalent: - yield return new OpcodeStore(Target); + yield return SetSourceLocation(new OpcodeStore(Target)); yield break; } } @@ -78,7 +98,7 @@ public class IRBinaryOp : IRInstruction public IRValue Left { get; protected set; } public IRValue Right { get; protected set; } public bool Commutative { get; } - public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) + public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) : base(operation) { Result = result; Operation = operation; @@ -121,7 +141,7 @@ internal override IEnumerable EmitOpcode() foreach (Opcode opcode in Right.EmitPush()) yield return opcode; Operation.Label = string.Empty; - yield return Operation; + yield return SetSourceLocation(Operation); } public override string ToString() => Operation.ToString(); @@ -132,7 +152,7 @@ public class IRUnaryOp : IRInstruction public IRTemp Result { get; } public Opcode Operation { get; } public IRValue Operand { get; } - public IRUnaryOp(IRTemp result, Opcode operation, IRValue operand) + public IRUnaryOp(IRTemp result, Opcode operation, IRValue operand) : base(operation) { Result = result; Operation = operation; @@ -152,7 +172,7 @@ public class IRNoStackInstruction : IRInstruction { public override bool SideEffects => false; public Opcode Operation { get; } - public IRNoStackInstruction(Opcode opcode) + public IRNoStackInstruction(Opcode opcode) : base(opcode) => Operation = opcode; internal override IEnumerable EmitOpcode() { @@ -167,7 +187,7 @@ public class IRUnaryConsumer : IRInstruction public override bool SideEffects { get; } public Opcode Operation { get; } public IRValue Operand { get; } - public IRUnaryConsumer(Opcode opcode, IRValue operand, bool sideEffects = false) + public IRUnaryConsumer(Opcode opcode, IRValue operand, bool sideEffects = false) : base(opcode) { Operation = opcode; Operand = operand; @@ -186,9 +206,17 @@ public override string ToString() public class IRPop : IRInstruction { public override bool SideEffects => false; + public IRValue Value { get; } + public IRPop(IRValue value, OpcodePop opcode) : base(opcode) + => Value = value; + internal override IEnumerable EmitOpcode() { - yield return new OpcodePop(); + if (Value is IRConstant || (Value is IRVariable && !(Value is IRTemp))) + yield break; + foreach (Opcode opcode in Value.EmitPush()) + yield return opcode; + yield return SetSourceLocation(new OpcodePop()); } public override string ToString() => "{pop}"; @@ -197,7 +225,7 @@ public class IRNonVarPush : IRInstruction { public override bool SideEffects => false; public Opcode Operation { get; } - public IRNonVarPush(Opcode opcode) + public IRNonVarPush(Opcode opcode) : base(opcode) => Operation = opcode; internal override IEnumerable EmitOpcode() { @@ -212,18 +240,17 @@ public class IRSuffixGet : IRInteractsInstruction public IRTemp Result { get; } public IRValue Object { get; } public string Suffix { get; } - public IRSuffixGet(IRTemp result, IRValue obj, string suffix) : base(obj) + public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcodeGetMember) : base(obj, opcodeGetMember) { Result = result; Object = obj; - Suffix = suffix; + Suffix = opcodeGetMember.Identifier; } - public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcode) : this(result, obj, opcode.Identifier) { } internal override IEnumerable EmitOpcode() { foreach (Opcode opcode in Object.EmitPush()) yield return opcode; - yield return new OpcodeGetMember(Suffix); + yield return SetSourceLocation(new OpcodeGetMember(Suffix)); } public override string ToString() => string.Format("{{gmb \"{0}\"}}", Suffix); @@ -231,13 +258,12 @@ public override string ToString() public class IRSuffixGetMethod : IRSuffixGet { public override bool SideEffects => false; - public IRSuffixGetMethod(IRTemp result, IRValue obj, string suffix) : base(result, obj, suffix) { } - public IRSuffixGetMethod(IRTemp result, IRValue obj, OpcodeGetMethod opcode) : this(result, obj, opcode.Identifier) { } + public IRSuffixGetMethod(IRTemp result, IRValue obj, OpcodeGetMethod opcode) : base(result, obj, opcode) { } internal override IEnumerable EmitOpcode() { foreach (Opcode opcode in Object.EmitPush()) yield return opcode; - yield return new OpcodeGetMethod(Suffix); + yield return SetSourceLocation(new OpcodeGetMethod(Suffix)); } public override string ToString() => string.Format("{{gmet \"{0}\"}}", Suffix); @@ -248,20 +274,19 @@ public class IRSuffixSet : IRInteractsInstruction public IRValue Object { get; } public IRValue Value { get; } public string Suffix { get; } - public IRSuffixSet(IRValue obj, IRValue value, string suffix) : base(obj) + public IRSuffixSet(IRValue obj, IRValue value, OpcodeSetMember opcodeSetMember) : base(obj, opcodeSetMember) { Object = obj; Value = value; - Suffix = suffix; + Suffix = opcodeSetMember.Identifier; } - public IRSuffixSet(IRValue obj, IRValue value, OpcodeSetMember opcode) : this(obj, value, opcode.Identifier) { } internal override IEnumerable EmitOpcode() { foreach (Opcode opcode in Object.EmitPush()) yield return opcode; foreach (Opcode opcode in Value.EmitPush()) yield return opcode; - yield return new OpcodeSetMember(Suffix); + yield return SetSourceLocation(new OpcodeSetMember(Suffix)); } public override string ToString() => string.Format("{{smb \"{0}\"}}", Suffix); @@ -272,7 +297,7 @@ public class IRIndexGet : IRInstruction public IRValue Result { get; } public IRValue Object { get; } public IRValue Index { get; } - public IRIndexGet(IRValue result, IRValue obj, IRValue index) + public IRIndexGet(IRTemp result, IRValue obj, IRValue index, OpcodeGetIndex opcode) : base(opcode) { Result = result; Object = obj; @@ -284,7 +309,7 @@ internal override IEnumerable EmitOpcode() yield return opcode; foreach (Opcode opcode in Index.EmitPush()) yield return opcode; - yield return new OpcodeGetIndex(); + yield return SetSourceLocation(new OpcodeGetIndex()); } public override string ToString() => "{gidx}"; @@ -295,7 +320,7 @@ public class IRIndexSet : IRInstruction public IRValue Object { get; } public IRValue Index { get; } public IRValue Value { get; } - public IRIndexSet(IRValue obj, IRValue index, IRValue value) + public IRIndexSet(IRValue obj, IRValue index, IRValue value, OpcodeSetIndex opcode) : base(opcode) { Object = obj; Index = index; @@ -309,7 +334,7 @@ internal override IEnumerable EmitOpcode() yield return opcode; foreach (Opcode opcode in Value.EmitPush()) yield return opcode; - yield return new OpcodeSetIndex(); + yield return SetSourceLocation(new OpcodeSetIndex()); } public override string ToString() => "{sidx}"; @@ -318,13 +343,15 @@ public class IRJump : IRInstruction { public override bool SideEffects => false; public BasicBlock Target { get; } - public IRJump(BasicBlock target) + public IRJump(BasicBlock target, OpcodeBranchJump opcode) : base(opcode) { Target = target; } + public IRJump(BasicBlock target, short sourceLine, short sourceColumn) + : this(target, new OpcodeBranchJump() { SourceLine = sourceLine, SourceColumn = sourceColumn }) { } internal override IEnumerable EmitOpcode() { - yield return new OpcodeBranchJump() { DestinationLabel = Target.Label }; + yield return SetSourceLocation(new OpcodeBranchJump() { DestinationLabel = Target.Label }); } public override string ToString() => string.Format("{{jump {0}}}", Target.Label); @@ -334,7 +361,7 @@ public class IRJumpStack : IRInstruction public override bool SideEffects => false; public IRValue Distance { get; } public List Targets { get; } = new List(); - public IRJumpStack(IRValue distance, IEnumerable targets) + public IRJumpStack(IRValue distance, IEnumerable targets, OpcodeJumpStack jumpStack) : base(jumpStack) { Distance = distance; Targets.AddRange(targets); @@ -343,7 +370,7 @@ internal override IEnumerable EmitOpcode() { foreach (Opcode opcode in Distance.EmitPush()) yield return opcode; - yield return new OpcodeJumpStack(); + yield return SetSourceLocation(new OpcodeJumpStack()); } } public class IRBranch : IRInstruction @@ -353,11 +380,12 @@ public class IRBranch : IRInstruction public BasicBlock True { get; } public BasicBlock False { get; } public bool PreferFalse { get; set; } = false; - public IRBranch(IRValue condition, BasicBlock onTrue, BasicBlock onFalse) + public IRBranch(IRValue condition, BasicBlock onTrue, BasicBlock onFalse, BranchOpcode opcodeBranch) : base(opcodeBranch) { Condition = condition; True = onTrue; False = onFalse; + PreferFalse = opcodeBranch is OpcodeBranchIfFalse; } internal override IEnumerable EmitOpcode() { @@ -365,13 +393,13 @@ internal override IEnumerable EmitOpcode() yield return opcode; if (PreferFalse) { - yield return new OpcodeBranchIfFalse() { DestinationLabel = False.Label }; - yield return new OpcodeBranchJump() { DestinationLabel = True.Label }; + yield return SetSourceLocation(new OpcodeBranchIfFalse() { DestinationLabel = False.Label }); + yield return SetSourceLocation(new OpcodeBranchJump() { DestinationLabel = True.Label }); } else { - yield return new OpcodeBranchIfTrue() { DestinationLabel = True.Label }; - yield return new OpcodeBranchJump() { DestinationLabel = False.Label }; + yield return SetSourceLocation(new OpcodeBranchIfTrue() { DestinationLabel = True.Label }); + yield return SetSourceLocation(new OpcodeBranchJump() { DestinationLabel = False.Label }); } } public override string ToString() @@ -387,7 +415,7 @@ public class IRCall : IRInstruction public IRValue IndirectMethod { get; internal set; } public bool Direct { get; } public bool EmitArgMarker; - private IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker) + private IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker) : base(opcode) { Target = target; Function = (string)opcode.Destination; @@ -548,7 +576,7 @@ internal override IEnumerable EmitOpcode() foreach (Opcode opcode in argument.EmitPush()) yield return opcode; } - yield return new OpcodeCall(Function); + yield return SetSourceLocation(new OpcodeCall(Function)); } public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, IRValue argument) : this(target, opcode, emitArgMarker) @@ -571,7 +599,7 @@ public class IRReturn : IRInstruction public override bool SideEffects => false; public IRValue Value { get; set; } public short Depth { get; internal set; } - public IRReturn(short depth) + public IRReturn(short depth, OpcodeReturn opcode) : base(opcode) => Depth = depth; internal override IEnumerable EmitOpcode() { @@ -579,8 +607,8 @@ internal override IEnumerable EmitOpcode() foreach (Opcode opcode in Value.EmitPush()) yield return opcode; else - yield return new OpcodePush(null); - yield return new OpcodeReturn(Depth); + yield return SetSourceLocation(new OpcodePush(null)); + yield return SetSourceLocation(new OpcodeReturn(Depth)); } public override string ToString() => string.Format("{{ret {0}}}", Depth); diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index 999c9c65e..d69251d19 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -75,7 +75,7 @@ public IRTemp(int id) : base($"$.temp.{id}", false) public IRAssign PromoteToVariable() { isPromoted = true; - return new IRAssign(Name, this) { Scope = IRAssign.StoreScope.Local }; + return new IRAssign(new OpcodeStoreLocal(Name), this) { Scope = IRAssign.StoreScope.Local }; } internal override IEnumerable EmitPush() { From 3f609ff46d5766ca0ffa51a774a6cd24b535bb11 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 16:05:48 -0500 Subject: [PATCH 004/120] Align IRInstruction property names and accessibility. Add interfaces that serve as metadata and access points. --- .../Compilation/IR/IOperandInstructionBase.cs | 15 ++ src/kOS.Safe/Compilation/IR/IRInstruction.cs | 136 ++++++++++-------- .../Compilation/IR/IResultingInstruction.cs | 7 + 3 files changed, 97 insertions(+), 61 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs create mode 100644 src/kOS.Safe/Compilation/IR/IResultingInstruction.cs diff --git a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs new file mode 100644 index 000000000..bb87b07c2 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace kOS.Safe.Compilation.IR +{ + public interface IOperandInstructionBase + { } + public interface ISingleOperandInstruction : IOperandInstructionBase + { + IRValue Operand { get; } + } + public interface IMultipleOperandInstruction : IOperandInstructionBase + { + IEnumerable Operands { get; } + } +} diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 1242c38a7..8c9b35973 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -46,7 +46,7 @@ protected IRInteractsInstruction(IRValue interactor, Opcode originalOpcode) : ba } } } - public class IRAssign : IRInstruction + public class IRAssign : IRInstruction, ISingleOperandInstruction { public enum StoreScope { @@ -55,10 +55,12 @@ public enum StoreScope Global } public override bool SideEffects => false; - public string Target { get; } - public IRValue Value { get; } + public string Target { get; set; } + public IRValue Value { get; set; } public StoreScope Scope { get; set; } = StoreScope.Ambivalent; public bool AssertExists { get; set; } = false; + IRValue ISingleOperandInstruction.Operand => Value; + public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : base(opcode) { Target = opcode.Identifier; @@ -90,21 +92,23 @@ internal override IEnumerable EmitOpcode() public override string ToString() => string.Format("{{store {0}}}", Value.ToString()); } - public class IRBinaryOp : IRInstruction + public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { public override bool SideEffects => false; - public IRTemp Result { get; } + public IRValue Result { get; set; } public BinaryOpcode Operation { get; protected set; } - public IRValue Left { get; protected set; } - public IRValue Right { get; protected set; } - public bool Commutative { get; } + public IRValue Left { get; set; } + public IRValue Right { get; set; } + public IEnumerable Operands { get { yield return Left; yield return Right; } } + public bool IsCommutative { get; } + public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) : base(operation) { Result = result; Operation = operation; Left = left; Right = right; - Commutative = (operation is OpcodeCompareEqual) || + IsCommutative = (operation is OpcodeCompareEqual) || (operation is OpcodeCompareNE) || (operation is OpcodeCompareGT) || (operation is OpcodeCompareLT) || @@ -113,10 +117,11 @@ public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue r (operation is OpcodeMathAdd) || (operation is OpcodeMathMultiply); } - public void ReverseOperands() + public void SwapOperands() { - if (!Commutative) - throw new System.InvalidOperationException($"{this} is not commutative."); + if (!IsCommutative) + return; + //throw new System.InvalidOperationException($"{this} is not commutative."); (Right, Left) = (Left, Right); switch (Operation) { @@ -146,12 +151,12 @@ internal override IEnumerable EmitOpcode() public override string ToString() => Operation.ToString(); } - public class IRUnaryOp : IRInstruction + public class IRUnaryOp : IRInstruction, IResultingInstruction, ISingleOperandInstruction { public override bool SideEffects => false; - public IRTemp Result { get; } + public IRValue Result { get; set; } public Opcode Operation { get; } - public IRValue Operand { get; } + public IRValue Operand { get; set; } public IRUnaryOp(IRTemp result, Opcode operation, IRValue operand) : base(operation) { Result = result; @@ -182,7 +187,7 @@ internal override IEnumerable EmitOpcode() public override string ToString() => Operation.ToString(); } - public class IRUnaryConsumer : IRInstruction + public class IRUnaryConsumer : IRInstruction, ISingleOperandInstruction { public override bool SideEffects { get; } public Opcode Operation { get; } @@ -203,10 +208,11 @@ internal override IEnumerable EmitOpcode() public override string ToString() => Operation.ToString(); } - public class IRPop : IRInstruction + public class IRPop : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; - public IRValue Value { get; } + public IRValue Value { get; set; } + IRValue ISingleOperandInstruction.Operand => Value; public IRPop(IRValue value, OpcodePop opcode) : base(opcode) => Value = value; @@ -235,11 +241,12 @@ internal override IEnumerable EmitOpcode() public override string ToString() => Operation.ToString(); } - public class IRSuffixGet : IRInteractsInstruction + public class IRSuffixGet : IRInteractsInstruction, IResultingInstruction, ISingleOperandInstruction { - public IRTemp Result { get; } - public IRValue Object { get; } - public string Suffix { get; } + public IRValue Result { get; } + public IRValue Object { get; set; } + public string Suffix { get; set; } + IRValue ISingleOperandInstruction.Operand => Object; public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcodeGetMember) : base(obj, opcodeGetMember) { Result = result; @@ -268,11 +275,12 @@ internal override IEnumerable EmitOpcode() public override string ToString() => string.Format("{{gmet \"{0}\"}}", Suffix); } - public class IRSuffixSet : IRInteractsInstruction + public class IRSuffixSet : IRInteractsInstruction, IMultipleOperandInstruction { public override bool SideEffects { get; } - public IRValue Object { get; } - public IRValue Value { get; } + public IRValue Object { get; set; } + public IRValue Value { get; set; } + public IEnumerable Operands { get { yield return Object; yield return Value; } } public string Suffix { get; } public IRSuffixSet(IRValue obj, IRValue value, OpcodeSetMember opcodeSetMember) : base(obj, opcodeSetMember) { @@ -291,12 +299,13 @@ internal override IEnumerable EmitOpcode() public override string ToString() => string.Format("{{smb \"{0}\"}}", Suffix); } - public class IRIndexGet : IRInstruction + public class IRIndexGet : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { public override bool SideEffects => false; public IRValue Result { get; } - public IRValue Object { get; } - public IRValue Index { get; } + public IRValue Object { get; set; } + public IRValue Index { get; set; } + public IEnumerable Operands { get { yield return Object; yield return Index; } } public IRIndexGet(IRTemp result, IRValue obj, IRValue index, OpcodeGetIndex opcode) : base(opcode) { Result = result; @@ -314,12 +323,13 @@ internal override IEnumerable EmitOpcode() public override string ToString() => "{gidx}"; } - public class IRIndexSet : IRInstruction + public class IRIndexSet : IRInstruction, IMultipleOperandInstruction { public override bool SideEffects => false; - public IRValue Object { get; } - public IRValue Index { get; } - public IRValue Value { get; } + public IRValue Object { get; set; } + public IRValue Index { get; set; } + public IRValue Value { get; set; } + public IEnumerable Operands { get { yield return Object; yield return Index; yield return Value; } } public IRIndexSet(IRValue obj, IRValue index, IRValue value, OpcodeSetIndex opcode) : base(opcode) { Object = obj; @@ -342,7 +352,7 @@ public override string ToString() public class IRJump : IRInstruction { public override bool SideEffects => false; - public BasicBlock Target { get; } + public BasicBlock Target { get; set; } public IRJump(BasicBlock target, OpcodeBranchJump opcode) : base(opcode) { Target = target; @@ -356,10 +366,11 @@ internal override IEnumerable EmitOpcode() public override string ToString() => string.Format("{{jump {0}}}", Target.Label); } - public class IRJumpStack : IRInstruction + public class IRJumpStack : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; - public IRValue Distance { get; } + public IRValue Distance { get; set; } + IRValue ISingleOperandInstruction.Operand => Distance; public List Targets { get; } = new List(); public IRJumpStack(IRValue distance, IEnumerable targets, OpcodeJumpStack jumpStack) : base(jumpStack) { @@ -373,12 +384,13 @@ internal override IEnumerable EmitOpcode() yield return SetSourceLocation(new OpcodeJumpStack()); } } - public class IRBranch : IRInstruction + public class IRBranch : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; - public IRValue Condition { get; } - public BasicBlock True { get; } - public BasicBlock False { get; } + public IRValue Condition { get; set; } + IRValue ISingleOperandInstruction.Operand => Condition; + public BasicBlock True { get; set; } + public BasicBlock False { get; set; } public bool PreferFalse { get; set; } = false; public IRBranch(IRValue condition, BasicBlock onTrue, BasicBlock onFalse, BranchOpcode opcodeBranch) : base(opcodeBranch) { @@ -405,19 +417,20 @@ internal override IEnumerable EmitOpcode() public override string ToString() => string.Format("{{br.? {0}/{1}}}", True.Label, False.Label); } - public class IRCall : IRInstruction + public class IRCall : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { protected static readonly Function.FunctionManager functionManager = new Function.FunctionManager(null); public override bool SideEffects { get; } - public IRTemp Target { get; } + public IRValue Result { get; } public string Function { get; } public List Arguments { get; } = new List(); + public IEnumerable Operands => Enumerable.Reverse(Arguments); public IRValue IndirectMethod { get; internal set; } public bool Direct { get; } - public bool EmitArgMarker; + public bool EmitArgMarker { get; set; } private IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker) : base(opcode) { - Target = target; + Result = target; Function = (string)opcode.Destination; Direct = opcode.Direct; SideEffects = CheckIfFunctionHasSideEffects(opcode); @@ -432,10 +445,6 @@ private bool CheckIfFunctionHasSideEffects(OpcodeCall opcode) functionName = functionName.ToLower().Replace("()", ""); switch (functionName) { - case "addAlarm": - case "listAlarms": - case "deleteAlarm": - case "buildlist": case "vcrs": case "vectorcrossproduct": case "vdot": @@ -480,12 +489,7 @@ private bool CheckIfFunctionHasSideEffects(OpcodeCall opcode) case "clearvecdraws": case "clearguis": case "gui": - case "positionat": - case "velocityat": - case "orbitat": case "career": - case "allwaypoints": - case "waypoint": case "lex": case "lexicon": case "list": @@ -507,14 +511,6 @@ private bool CheckIfFunctionHasSideEffects(OpcodeCall opcode) case "randomseed": case "char": case "unchar": - case "print": - case "printat": - case "logfile": - case "debugdump": - case "debugfreezegame": - case "profileresult": - case "makebuiltindelegate": - case "droppriority": case "range": case "constant": case "sin": @@ -526,6 +522,23 @@ private bool CheckIfFunctionHasSideEffects(OpcodeCall opcode) case "arctan2": case "anglediff": return false; + case "addAlarm": + case "listAlarms": + case "deleteAlarm": + case "buildlist": + case "positionat": + case "velocityat": + case "orbitat": + case "allwaypoints": + case "waypoint": + case "print": + case "printat": + case "logfile": + case "debugdump": + case "debugfreezegame": + case "profileresult": + case "makebuiltindelegate": + case "droppriority": case "stage": case "warpto": case "transfer": @@ -594,10 +607,11 @@ public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, params IRVal public override string ToString() => string.Format("{{call {0}({1})}}", Function.Trim('(', ')'), string.Join(",", Arguments.Select(a => a.ToString()))); } - public class IRReturn : IRInstruction + public class IRReturn : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; public IRValue Value { get; set; } + IRValue ISingleOperandInstruction.Operand => Value; public short Depth { get; internal set; } public IRReturn(short depth, OpcodeReturn opcode) : base(opcode) => Depth = depth; diff --git a/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs new file mode 100644 index 000000000..a9c7bfcdb --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs @@ -0,0 +1,7 @@ +namespace kOS.Safe.Compilation.IR +{ + public interface IResultingInstruction + { + IRValue Result { get; } + } +} \ No newline at end of file From 1344914117f4f2dcd87dfa2c0ff63717b510d4e7 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 21:03:45 -0500 Subject: [PATCH 005/120] Add concept of Extended Basic Blocks to help with future analysis and optimization methods. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 13 ++++- src/kOS.Safe/Compilation/IR/IROptimizer.cs | 8 ++- .../IR/Optimization/ExtendedBasicBlock.cs | 57 +++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 79a245023..b5ee45722 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -8,10 +8,13 @@ public class BasicBlock public int StartIndex { get; } public int EndIndex { get; } public List Instructions { get; } = new List(); - protected internal readonly HashSet predecessors = new HashSet(); - protected internal readonly HashSet sucessors = new HashSet(); + public IEnumerable Sucessors => sucessors; + public IEnumerable Predecessors => predecessors; + private readonly HashSet predecessors = new HashSet(); + private readonly HashSet sucessors = new HashSet(); public string Label => $"@BB#{ID}"; public int ID { get; } + public Optimization.ExtendedBasicBlock ExtendedBlock { get; set; } private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. #if DEBUG internal Opcode[] OriginalOpcodes { get; set; } @@ -37,6 +40,12 @@ protected void AddPredecessor(BasicBlock predecessor) { predecessors.Add(predecessor); } + public void RemoveSuccessor(BasicBlock sucessor) + { + if (!sucessors.Remove(sucessor)) + throw new System.ArgumentException(nameof(sucessor)); + sucessor.predecessors.Remove(this); + } public void SetStackState(Stack stack) { diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index 92024ae50..1d74b97c0 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using kOS.Safe.Compilation.IR.Optimization; namespace kOS.Safe.Compilation.IR { @@ -17,6 +16,11 @@ public IROptimizer(OptimizationLevel optimizationLevel) public List Optimize(List blocks) { + if (blocks.Count == 0) + return blocks; + + ExtendedBasicBlock extendedRootBlock = ExtendedBasicBlock.CreateExtendedBlockTree(blocks[0]); + HashSet extendedBlocks = new HashSet(ExtendedBasicBlock.DumpTree(extendedRootBlock)); return blocks; } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs b/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs new file mode 100644 index 000000000..59a9b18e6 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR.Optimization +{ + public class ExtendedBasicBlock + { + public List Blocks { get; } = new List(); + public IEnumerable Sucessors => sucessors; + public IEnumerable Predecessors => predecessors; + private readonly HashSet predecessors = new HashSet(); + private readonly HashSet sucessors = new HashSet(); + private ExtendedBasicBlock(BasicBlock startingBlock) + { + AddBlock(startingBlock); + Blocks.Sort(Comparer.Create((x, y) => x.ID.CompareTo(y.ID))); + } + private void AddBlock(BasicBlock block) + { + Blocks.Add(block); + block.ExtendedBlock = this; + foreach (BasicBlock successor in block.Sucessors) + { + if (!successor.Predecessors.Skip(1).Any()) + AddBlock(successor); + else + { + ExtendedBasicBlock extendedSuccessor = successor.ExtendedBlock ?? new ExtendedBasicBlock(successor); + AddSuccessor(extendedSuccessor); + } + } + } + + public static ExtendedBasicBlock CreateExtendedBlockTree(BasicBlock root) + { + ExtendedBasicBlock resultBlock = new ExtendedBasicBlock(root); + return resultBlock; + } + public static IEnumerable DumpTree(ExtendedBasicBlock root) + => Enumerable.Repeat(root, 1).Concat(root.sucessors.SelectMany(DumpTree)); + + public void AddSuccessor(ExtendedBasicBlock successor) + { + sucessors.Add(successor); + successor.AddPredecessor(this); + } + protected void AddPredecessor(ExtendedBasicBlock predecessor) + { + predecessors.Add(predecessor); + } + + public override string ToString() + { + return $"ExtendedBasicBlock: {string.Join(", ", Blocks.Select(bb => $"#{bb.ID}"))}"; + } + } +} From 28410f0f86576f325b7b9bca9e4e7c59cb583bc5 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 21:05:42 -0500 Subject: [PATCH 006/120] Add abstract IRVariableBase class so that IRTemp can share some commonalities with IRVariable but not be a subclass. --- src/kOS.Safe/Compilation/IR/IRValue.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index d69251d19..cfbd015e1 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -26,13 +26,17 @@ internal override IEnumerable EmitPush() public override string ToString() => Value.ToString(); } - public class IRVariable : IRValue + public abstract class IRVariableBase : IRValue { public string Name { get; } + public IRVariableBase(string name) + => Name = name; + } + public class IRVariable : IRVariableBase + { public bool IsLock { get; } - public IRVariable(string name, bool isLock = false) + public IRVariable(string name, bool isLock = false) : base(name) { - Name = name; IsLock = isLock; } internal override IEnumerable EmitPush() @@ -63,12 +67,13 @@ internal override IEnumerable EmitPush() yield return new OpcodePushDelegateRelocateLater((string)Value, WithClosure); } } - public class IRTemp : IRVariable + public class IRTemp : IRVariableBase { public int ID { get; } public IRInstruction Parent { get; internal set; } + private bool isPromoted = false; - public IRTemp(int id) : base($"$.temp.{id}", false) + public IRTemp(int id) : base($"$.temp.{id}") { ID = id; } From 171b2bc496b674012024d7c68b01cd28e991caf1 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 21:44:42 -0500 Subject: [PATCH 007/120] Add methods to, outside of executing an opcode, invoke the same math or logical operation. This will be used for constant folding during compile. Hopefully the C# compiler will inline these back into the original Opcode methods, but the impact to execution should be minimal either way. --- src/kOS.Safe/Compilation/Opcode.cs | 40 +++++++++++++++++++----------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/kOS.Safe/Compilation/Opcode.cs b/src/kOS.Safe/Compilation/Opcode.cs index 58394eda7..6637211e6 100644 --- a/src/kOS.Safe/Compilation/Opcode.cs +++ b/src/kOS.Safe/Compilation/Opcode.cs @@ -479,12 +479,17 @@ public override void Execute(ICpu cpu) object right = cpu.PopValueArgument(); object left = cpu.PopValueArgument(); + object result = ExecuteCalculation(left, right); + cpu.PushArgumentStack(result); + } + + public object ExecuteCalculation(object left, object right) + { var operands = new OperandPair(left, right); Calculator calc = Calculator.GetCalculator(operands); Operands = operands; - object result = ExecuteCalculation(calc); - cpu.PushArgumentStack(result); + return ExecuteCalculation(calc); } protected virtual object ExecuteCalculation(Calculator calc) @@ -1313,27 +1318,29 @@ public override void Execute(ICpu cpu) { Structure value = cpu.PopStructureEncapsulatedArgument(); + cpu.PushArgumentStack(StaticOperation(value)); + } + + public static object StaticOperation(object value) + { var scalarValue = value as ScalarValue; if (scalarValue != null && scalarValue.IsValid) { - cpu.PushArgumentStack(-scalarValue); - return; + return -scalarValue; } - + // Generic last-ditch to catch any sort of object that has // overloaded the unary negate operator '-'. // (For example, kOS.Suffixed.Vector and kOS.Suffixed.Direction) Type t = value.GetType(); - MethodInfo negateMe = t.GetMethod("op_UnaryNegation", BindingFlags.FlattenHierarchy |BindingFlags.Static | BindingFlags.Public); + MethodInfo negateMe = t.GetMethod("op_UnaryNegation", BindingFlags.FlattenHierarchy | BindingFlags.Static | BindingFlags.Public); if (negateMe != null) { - object result = negateMe.Invoke(null, new[]{value}); - cpu.PushArgumentStack(result); + return negateMe.Invoke(null, new[] { value }); } else throw new KOSUnaryOperandTypeException("negate", value); - } } @@ -1462,8 +1469,12 @@ public override void Execute(ICpu cpu) // is to also change integers and floats into booleans. Thus the call to // Convert.ToBoolean(): object value = cpu.PopValueArgument(); + cpu.PushArgumentStack(StaticOperation(value)); + } + public static object StaticOperation(object value) + { bool result = Convert.ToBoolean(value); - cpu.PushArgumentStack(Structure.FromPrimitive(result)); + return Structure.FromPrimitive(result); } } @@ -1485,21 +1496,22 @@ public class OpcodeLogicNot : Opcode public override void Execute(ICpu cpu) { object value = cpu.PopValueArgument(); - object result; - + cpu.PushArgumentStack(StaticOperation(value)); + } + public static object StaticOperation(object value) + { // Convert to bool instead of cast in case the identifier is stored // as an encapsulated BooleanValue, preventing an unboxing collision. // Wrapped in a try/catch since the Convert framework doesn't have a // way to determine if a type can be converted. try { - result = !Convert.ToBoolean(value); + return Structure.FromPrimitive(!Convert.ToBoolean(value)); } catch { throw new KOSCastException(value.GetType(), typeof(BooleanValue)); } - cpu.PushArgumentStack(Structure.FromPrimitive(result)); } } From a53f7b3e561d25a893bc34387c317a42046d46b5 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 22:16:10 -0500 Subject: [PATCH 008/120] Add convenience constructor for compilation exceptions arising from IR instructions. --- src/kOS.Safe/Exceptions/KOSCompileException.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/kOS.Safe/Exceptions/KOSCompileException.cs b/src/kOS.Safe/Exceptions/KOSCompileException.cs index 37320cae0..e59e47eda 100644 --- a/src/kOS.Safe/Exceptions/KOSCompileException.cs +++ b/src/kOS.Safe/Exceptions/KOSCompileException.cs @@ -37,6 +37,11 @@ public KOSCompileException(Token token, string message) { } + public KOSCompileException(Compilation.IR.IRInstruction irInstruction, KOSException innerException) + : this(new LineCol(irInstruction.SourceLine, irInstruction.SourceColumn), innerException.Message) + { + } + public KOSCompileException(LineCol location, string message) { Location = location; From 131a4a9e42f183e464b2b41f2ec369c09875e221 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 22:53:27 -0500 Subject: [PATCH 009/120] Add constant folding pass. This reduces any constant expressions to their most reduced form. Invalid operations found during folding throw a compilation exception, which should report the problematic line and column in the source. Also add tests to confirm correct functionality (and improve current test suite for equivalent non-optimizing tests). --- kerboscript_tests/integration/operators.ks | 3 + .../integration/operators_invalid.ks | 21 ++ .../Execution/OptimizationTest.cs | 15 +- src/kOS.Safe.Test/Execution/SimpleTest.cs | 5 +- src/kOS.Safe/Compilation/IR/IROptimizer.cs | 2 + .../IR/Optimization/ConstantFolding.cs | 275 ++++++++++++++++++ src/kOS.Safe/Compilation/OptimizationLevel.cs | 10 +- 7 files changed, 322 insertions(+), 9 deletions(-) create mode 100644 kerboscript_tests/integration/operators_invalid.ks create mode 100644 src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs diff --git a/kerboscript_tests/integration/operators.ks b/kerboscript_tests/integration/operators.ks index c4aa36b73..f82ebac9e 100644 --- a/kerboscript_tests/integration/operators.ks +++ b/kerboscript_tests/integration/operators.ks @@ -18,3 +18,6 @@ print(1 = 1). print(1 <> 2). print(true and true). print(true or false). +print("A" + "b"). +print(a * 0). +print(a ^ 0). diff --git a/kerboscript_tests/integration/operators_invalid.ks b/kerboscript_tests/integration/operators_invalid.ks new file mode 100644 index 000000000..75473a709 --- /dev/null +++ b/kerboscript_tests/integration/operators_invalid.ks @@ -0,0 +1,21 @@ + +set a to 0. + +print(+ 1). +print(- (-1)). +print(not false). +print(defined a). + +print(1 + 2). +print(2 * 3). +print(4 / 2). +print(3 ^ 2). +print(2 > 1). +print(2 >= 1). +print(1 < 2). +print(1 <= 2). +print(1 = 1). +print(1 <> 2). +print(true and true). +print(true or false). +print(1 + false). diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index bae72f254..0038c69a9 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -1,6 +1,7 @@ using System; using NUnit.Framework; using kOS.Safe.Compilation; +using kOS.Safe.Exceptions; namespace kOS.Safe.Test.Execution { @@ -85,10 +86,22 @@ public void TestOperators() "True", "True", "True", - "True" + "True", + "Ab", + "0", + "1" ); } + [Test] + [ExpectedException(typeof(KOSCompileException))] + public void TestOperatorsException() + { + // Test that an invalid operation throws a compile error during constant folding + RunScript("integration/operators_invalid.ks"); + RunSingleStep(); + } + [Test] public void TestLock() { diff --git a/src/kOS.Safe.Test/Execution/SimpleTest.cs b/src/kOS.Safe.Test/Execution/SimpleTest.cs index 5e8a7309a..526a7b3bf 100644 --- a/src/kOS.Safe.Test/Execution/SimpleTest.cs +++ b/src/kOS.Safe.Test/Execution/SimpleTest.cs @@ -83,7 +83,10 @@ public void TestOperators() "True", "True", "True", - "True" + "True", + "Ab", + "0", + "1" ); } diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index 1d74b97c0..4ed9a7f3a 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -21,6 +21,8 @@ public List Optimize(List blocks) ExtendedBasicBlock extendedRootBlock = ExtendedBasicBlock.CreateExtendedBlockTree(blocks[0]); HashSet extendedBlocks = new HashSet(ExtendedBasicBlock.DumpTree(extendedRootBlock)); + + ConstantFolding.ApplyPass(blocks); return blocks; } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs new file mode 100644 index 000000000..a024bd106 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using kOS.Safe.Exceptions; + +namespace kOS.Safe.Compilation.IR.Optimization +{ + public static class ConstantFolding + { + public static void ApplyPass(IEnumerable blocks) + { + Queue worklist = new Queue(blocks); + IEnumerator enumerator = blocks.GetEnumerator(); + while (enumerator.MoveNext()) + { + ApplyPass(enumerator.Current); + } + } + private static void ApplyPass(BasicBlock block) + { + for (int i = 0; i < block.Instructions.Count; i++) + { + IRInstruction instruction = block.Instructions[i]; + switch (instruction) + { + case IRPop pop: + if (AttemptReductionToConstant(pop.Value) is IRConstant) + { + block.Instructions.RemoveAt(i); + i--; + } + continue; + case IRAssign assign: + assign.Value = AttemptReductionToConstant(assign.Value); + break; + case IRSuffixSet suffixSet: + suffixSet.Value = AttemptReductionToConstant(suffixSet.Value); + suffixSet.Object = AttemptReductionToConstant(suffixSet.Object); + break; + case IRIndexSet indexSet: + indexSet.Value = AttemptReductionToConstant(indexSet.Value); + indexSet.Object = AttemptReductionToConstant(indexSet.Object); + indexSet.Index = AttemptReductionToConstant(indexSet.Index); + break; + case IRBranch branch: + if (AttemptReductionToConstant(branch.Condition) is IRConstant branchConstant) + { + BasicBlock permanentBlock, deprecatedBlock; + (permanentBlock, deprecatedBlock) = Convert.ToBoolean(branchConstant.Value) ? (branch.True, branch.False) : (branch.False, branch.True); + block.Instructions[i] = new IRJump(permanentBlock, new OpcodeBranchJump() { SourceLine = branch.SourceLine, SourceColumn = branch.SourceColumn }); + block.RemoveSuccessor(deprecatedBlock); + // TODO: Resolve any phi values in the deprecated block and consider re-running folding on that block. + } + break; + } + } + } + private static IRValue AttemptReductionToConstant(IRValue input) + { + if (input is IRConstant constant) + return constant; + if (!(input is IRTemp temp)) + return input; + return AttemptReduction(temp.Parent); + } + private static IRValue AttemptReduction(IRInstruction instruction) + { + switch (instruction) + { + case IRUnaryOp unaryOp: + return ReduceUnary(unaryOp); + case IRBinaryOp binaryOp: + return ReduceBinary(binaryOp); + case IRSuffixGet suffixGet: + return ReduceSuffixGet(suffixGet); + case IRIndexGet indexGet: + return ReduceIndexGet(indexGet); + case IRCall call: + return ReduceCall(call); + } + throw new ArgumentException($"{instruction.GetType()} is not supported."); + } + private static IRValue ReduceUnary(IRUnaryOp instruction) + { + if (instruction.Operand is IRTemp temp) + instruction.Operand = AttemptReduction(temp.Parent); + + if (instruction.Operand is IRConstant constant) + { + object input = constant.Value; + IRValue result; + try + { + switch (instruction.Operation) + { + case OpcodeMathNegate _: + result = new IRConstant(OpcodeMathNegate.StaticOperation(input)); + break; + case OpcodeLogicNot _: + result = new IRConstant(OpcodeLogicNot.StaticOperation(input)); + break; + case OpcodeLogicToBool _: + result = new IRConstant(OpcodeLogicToBool.StaticOperation(input)); + break; + default: + result = instruction.Result; + break; + } + instruction.Result = result; + } + catch (KOSUnaryOperandTypeException unaryTypeException) + { + throw new KOSCompileException(instruction, unaryTypeException); + } + } + return instruction.Result; + } + private static IRValue ReduceBinary(IRBinaryOp instruction) + { + if (instruction.Left is IRTemp tempL) + { + instruction.Left = AttemptReduction(tempL.Parent); + } + if (instruction.Right is IRTemp tempR) + { + instruction.Right = AttemptReduction(tempR.Parent); + } + // Put constants to the right, if there are any + if (instruction.IsCommutative && instruction.Left is IRConstant && !(instruction.Right is IRConstant)) + { + instruction.SwapOperands(); + } + // If this is false, neither are constants after the last step, unless this isn't commutative, in which case this cleverness doesn't matter. + if (instruction.Right is IRConstant constantR) + { + // If this is true, both are constants + if (instruction.Left is IRConstant constantL) + { + object left = constantL.Value; + object right = constantR.Value; + try + { + IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right)); + instruction.Result = result; + } + catch (KOSBinaryOperandTypeException binaryTypeException) + { + throw new KOSCompileException(instruction, binaryTypeException); + } + return instruction.Result; + } + else if (instruction.IsCommutative) + { + // The right is constant and the left is not... + // But what if left.Parent is commutative with this operation and has a constant? + if (instruction.Left is IRTemp temp && + temp.Parent is IRBinaryOp leftOp && + leftOp.Operation.GetType() == instruction.Operation.GetType() && + leftOp.Right is IRConstant constantL1) + { + object right = constantR.Value; + object left = constantL1.Value; + try + { + IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right)); + instruction.Result = result; + leftOp.Right = result; + } + catch (KOSBinaryOperandTypeException binaryTypeException) + { + throw new KOSCompileException(instruction, binaryTypeException); + } + return leftOp.Result; + } + } + // Shortcuts for math operations where both sides don't need to be constant + switch (instruction.Operation) + { + case OpcodeMathMultiply _: + // X * 0 = 0 + if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + return constantR; + if (ReduceDivMult(instruction, constantR, out IRValue newResult)) + return newResult; + break; + case OpcodeMathDivide _: + if (ReduceDivMult(instruction, constantR, out newResult)) + return newResult; + break; + case OpcodeMathAdd _: + case OpcodeMathSubtract _: + // X +- 0 = X + if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + return instruction.Left; + break; + case OpcodeMathPower _: + // X^0 = 1 + if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + return new IRConstant(Encapsulation.ScalarIntValue.One); + // X^1 = X + if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) + return instruction.Left; + break; + } + } + else if (!instruction.IsCommutative && instruction.Left is IRConstant constantL) + { + switch (instruction.Operation) + { + case OpcodeMathDivide _: + if (Encapsulation.ScalarIntValue.Zero.Equals(constantL)) + return constantL; + break; + } + } + return instruction.Result; + } + private static bool ReduceDivMult(IRBinaryOp instruction, IRConstant secondOperand, out IRValue newResult) + { + // X */ 1 = X + if (Encapsulation.ScalarIntValue.One.Equals(secondOperand.Value)) + { + newResult = instruction.Left; + return true; + } + // X */ -1 = -X + if (secondOperand.Value.Equals(-Encapsulation.ScalarIntValue.One)) + { + IRTemp tempResult = instruction.Result as IRTemp; + tempResult.Parent = new IRUnaryOp( + tempResult, + new OpcodeMathNegate() + { + SourceColumn = instruction.SourceColumn, + SourceLine = instruction.SourceLine + }, + instruction.Left); + newResult = tempResult; + return true; + } + newResult = null; + return false; + } + + private static IRValue ReduceSuffixGet(IRSuffixGet instruction) + { + if (instruction.Object is IRTemp temp) + { + instruction.Object = AttemptReduction(temp.Parent); + } + return instruction.Result; + } + private static IRValue ReduceIndexGet(IRIndexGet instruction) + { + if (instruction.Object is IRTemp tempObj) + { + instruction.Object = AttemptReduction(tempObj.Parent); + } + if (instruction.Index is IRTemp tempIndex) + { + instruction.Index = AttemptReduction(tempIndex.Parent); + } + return instruction.Result; + } + private static IRValue ReduceCall(IRCall instruction) + { + // TODO: Add reduction for specific functions. E.g. mod(X, 1) = 0, round/ceiling/floor, Ln/Log10, min/max + for (int i = instruction.Arguments.Count - 1; i >= 0; i--) + { + if (instruction.Arguments[i] is IRTemp temp) + instruction.Arguments[i] = AttemptReduction(temp.Parent); + } + return instruction.Result; + } + } +} diff --git a/src/kOS.Safe/Compilation/OptimizationLevel.cs b/src/kOS.Safe/Compilation/OptimizationLevel.cs index c0cdc63b4..7ebb682de 100644 --- a/src/kOS.Safe/Compilation/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/OptimizationLevel.cs @@ -1,15 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace kOS.Safe.Compilation { /* * O1: + * Constant folding + * * Replace ship fields with their alias - * Constant propagation, including items from the constant structure + * Constant propagation * Replace lex indexing with string constant with suffixing where possible * Dead code elimination (for the never used case) * Replace constant() with constant From cd3104f95117de188fa3a619521d4e3467101db8 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 7 Mar 2026 14:32:50 -0500 Subject: [PATCH 010/120] Add constant folding for built-in scalar function calls. This required adding a virtual 'interim CPU' to accomplish the function call through the FunctionManager in order to avoid rewriting the function call logic. --- kerboscript_tests/integration/operators.ks | 2 + .../Execution/OptimizationTest.cs | 2 + src/kOS.Safe.Test/Execution/SimpleTest.cs | 2 + src/kOS.Safe/Compilation/IR/IRInstruction.cs | 2 +- .../IR/Optimization/ConstantFolding.cs | 47 ++++++ .../Compilation/IR/Optimization/InterimCPU.cs | 153 ++++++++++++++++++ 6 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs diff --git a/kerboscript_tests/integration/operators.ks b/kerboscript_tests/integration/operators.ks index f82ebac9e..a68640d51 100644 --- a/kerboscript_tests/integration/operators.ks +++ b/kerboscript_tests/integration/operators.ks @@ -21,3 +21,5 @@ print(true or false). print("A" + "b"). print(a * 0). print(a ^ 0). +print(arcTan2(0,1)). +print(abs(-1)). diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index 0038c69a9..ddc020ef1 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -89,6 +89,8 @@ public void TestOperators() "True", "Ab", "0", + "1", + "0", "1" ); } diff --git a/src/kOS.Safe.Test/Execution/SimpleTest.cs b/src/kOS.Safe.Test/Execution/SimpleTest.cs index 526a7b3bf..0615749a5 100644 --- a/src/kOS.Safe.Test/Execution/SimpleTest.cs +++ b/src/kOS.Safe.Test/Execution/SimpleTest.cs @@ -86,6 +86,8 @@ public void TestOperators() "True", "Ab", "0", + "1", + "0", "1" ); } diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 8c9b35973..a20c168ab 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -421,7 +421,7 @@ public class IRCall : IRInstruction, IResultingInstruction, IMultipleOperandInst { protected static readonly Function.FunctionManager functionManager = new Function.FunctionManager(null); public override bool SideEffects { get; } - public IRValue Result { get; } + public IRValue Result { get; set; } public string Function { get; } public List Arguments { get; } = new List(); public IEnumerable Operands => Enumerable.Reverse(Arguments); diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs index a024bd106..34c339ed8 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs @@ -1,11 +1,19 @@ using System; using System.Collections.Generic; +using System.Linq; using kOS.Safe.Exceptions; +using kOS.Safe.Function; namespace kOS.Safe.Compilation.IR.Optimization { public static class ConstantFolding { + private static readonly InterimCPU interimCPU = new InterimCPU(); + private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = interimCPU }; + static ConstantFolding() + { + shared.FunctionManager = new FunctionManager(shared); + } public static void ApplyPass(IEnumerable blocks) { Queue worklist = new Queue(blocks); @@ -269,6 +277,45 @@ private static IRValue ReduceCall(IRCall instruction) if (instruction.Arguments[i] is IRTemp temp) instruction.Arguments[i] = AttemptReduction(temp.Parent); } + string functionName = instruction.Function.Replace("()", ""); + if (shared.FunctionManager.Exists(functionName) && instruction.Arguments.All(arg => arg is IRConstant)) + { + try + { + switch (functionName) + { + case "abs": + case "mod": + case "floor": + case "ceiling": + case "round": + case "sqrt": + case "ln": + case "log10": + case "min": + case "max": + case "sin": + case "cos": + case "tan": + case "arcsin": + case "arccos": + case "arctan": + case "arctan2": + case "anglediff": + interimCPU.Boot(); // Clear the stack out of caution. + interimCPU.PushArgumentStack(new Execution.KOSArgMarkerType()); + foreach (IRValue arg in instruction.Arguments) + interimCPU.PushArgumentStack(((IRConstant)arg).Value); + shared.FunctionManager.CallFunction(functionName); + instruction.Result = new IRConstant(interimCPU.PopValueArgument()); + return instruction.Result; + } + } + catch (KOSException e) + { + throw new KOSCompileException(instruction, e); + } + } return instruction.Result; } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs b/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs new file mode 100644 index 000000000..413b02ee3 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using kOS.Safe.Encapsulation; +using kOS.Safe.Execution; + +namespace kOS.Safe.Compilation.IR.Optimization +{ + internal class InterimCPU : ICpu + { + public int InstructionPointer { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public double SessionTime => throw new NotImplementedException(); + public List ProfileResult => throw new NotImplementedException(); + public int NextTriggerInstanceId => throw new NotImplementedException(); + public int InstructionsThisUpdate => throw new NotImplementedException(); + public bool IsPoppingContext => throw new NotImplementedException(); + + private readonly Stack stack = new Stack(); + public void Boot() + { + stack.Clear(); + } + public object PopArgumentStack() + => stack.PopArgument(); + public object PopValueArgument(bool barewordOkay = false) + => GetValue(PopArgumentStack(), barewordOkay); + public void PushArgumentStack(object item) + => stack.PushArgument(item); + public object GetValue(object testValue, bool barewordOkay = false) + { + // $cos cos named variable + // cos() cos trigonometric function + // cos string literal "cos" + + // If it's a variable, meaning it starts with "$" but + // does NOT have a value like $<.....>, which are special + // flags used internally: + var identifier = testValue as string; + if (identifier == null || + identifier.Length <= 1 || + identifier[0] != '$' || + identifier[1] == '<') + { + return testValue; + } + + throw new InvalidOperationException("Only compile-time constants can be accessed. Contact a kOS developer if you see this message."); + } + + public void AddPopContextNotifyee(IPopContextNotifyee notifyee) + => throw new NotImplementedException(); + public TriggerInfo AddTrigger(int triggerFunctionPointer, InterruptPriority priority, int instanceId, bool immediate, List closure) + => throw new NotImplementedException(); + public TriggerInfo AddTrigger(TriggerInfo trigger, bool immediate) + => throw new NotImplementedException(); + public TriggerInfo AddTrigger(UserDelegate del, InterruptPriority priority, int instanceId, bool immediate, List args) + => throw new NotImplementedException(); + public TriggerInfo AddTrigger(UserDelegate del, InterruptPriority priority, int instanceId, bool immediate, params Structure[] args) + => throw new NotImplementedException(); + public void AddVariable(Variable variable, string identifier, bool local, bool overwrite = false) + => throw new NotImplementedException(); + public void AssertValidDelegateCall(IUserDelegate userDelegate) + => throw new NotImplementedException(); + public void BreakExecution(bool manual) + => throw new NotImplementedException(); + public bool BuiltInExists(string functionName) + => throw new NotImplementedException(); + public void CallBuiltinFunction(string functionName) + => throw new NotImplementedException(); + public void CancelCalledTriggers(int triggerFunctionPointer, int instanceId) + => throw new NotImplementedException(); + public void CancelCalledTriggers(TriggerInfo trigger) + => throw new NotImplementedException(); + public void Dispose() + => throw new NotImplementedException(); + public void DropBackPriority() + => throw new NotImplementedException(); + public string DumpStack() + => throw new NotImplementedException(); + public string DumpVariables() + => throw new NotImplementedException(); + public int GetArgumentStackSize() + => throw new NotImplementedException(); + public List GetCallTrace() + => throw new NotImplementedException(); + public List GetCodeFragment(int contextLines) + => throw new NotImplementedException(); + public List GetCurrentClosure() + => throw new NotImplementedException(); + public IProgramContext GetCurrentContext() + => throw new NotImplementedException(); + public Opcode GetCurrentOpcode() + => throw new NotImplementedException(); + public SubroutineContext GetCurrentSubroutineContext() + => throw new NotImplementedException(); + public Opcode GetOpcodeAt(int instructionPtr) + => throw new NotImplementedException(); + public Structure GetStructureEncapsulatedArgument(Structure testValue, bool barewordOkay = false) + => throw new NotImplementedException(); + public bool IdentifierExistsInScope(string identifier) + => throw new NotImplementedException(); + public void KOSFixedUpdate(double deltaTime) + => throw new NotImplementedException(); + public IUserDelegate MakeUserDelegate(int entryPoint, bool withClosure) + => throw new NotImplementedException(); + public object PeekRawArgument(int digDepth, out bool checkOkay) + => throw new NotImplementedException(); + public object PeekRawScope(int digDepth, out bool checkOkay) + => throw new NotImplementedException(); + public Structure PeekStructureEncapsulatedArgument(int digDepth, bool barewordOkay = false) + => throw new NotImplementedException(); + public object PeekValueArgument(int digDepth, bool barewordOkay = false) + => throw new NotImplementedException(); + public object PeekValueEncapsulatedArgument(int digDepth, bool barewordOkay = false) + => throw new NotImplementedException(); + public object PopScopeStack(int howMany) + => throw new NotImplementedException(); + public Structure PopStructureEncapsulatedArgument(bool barewordOkay = false) + => throw new NotImplementedException(); + public object PopValueEncapsulatedArgument(bool barewordOkay = false) + => throw new NotImplementedException(); + + public void PushNewScope(short scopeId, short parentScopeId) + => throw new NotImplementedException(); + public void PushScopeStack(object thing) + => throw new NotImplementedException(); + public void RemovePopContextNotifyee(IPopContextNotifyee notifyee) + => throw new NotImplementedException(); + public void RemoveTrigger(int triggerFunctionPointer, int instanceId) + => throw new NotImplementedException(); + public void RemoveTrigger(TriggerInfo trigger) + => throw new NotImplementedException(); + public void RemoveVariable(string identifier) + => throw new NotImplementedException(); + public void RunProgram(List program) + => throw new NotImplementedException(); + public void SetGlobal(string identifier, object value) + => throw new NotImplementedException(); + public void SetNewLocal(string identifier, object value) + => throw new NotImplementedException(); + public void SetValue(string identifier, object value) + => throw new NotImplementedException(); + public void SetValueExists(string identifier, object value) + => throw new NotImplementedException(); + public void StartCompileStopwatch() + => throw new NotImplementedException(); + public void StopCompileStopwatch() + => throw new NotImplementedException(); + public IProgramContext SwitchToProgramContext() + => throw new NotImplementedException(); + public void YieldProgram(YieldFinishedDetector yieldTracker) + => throw new NotImplementedException(); + } +} From 2f11d218f0fe3277b2a2c764d919b49c42813e6f Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 7 Mar 2026 15:40:37 -0500 Subject: [PATCH 011/120] Add infrastructure for automatic inclusion of optimization passes using the AssemblyWalk attribute to discover all classes implementing IOptimizationPass. --- src/kOS.Safe/Compilation/IR/IROptimizer.cs | 42 +++++++++++++++++-- .../IR/Optimization/ConstantFolding.cs | 8 +++- .../IR/Optimization/IOptimizationPass.cs | 15 +++++++ 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index 4ed9a7f3a..6bb0e095e 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -1,18 +1,25 @@ using System; using System.Collections.Generic; -using System.Linq; using kOS.Safe.Compilation.IR.Optimization; +using kOS.Safe.Utilities; namespace kOS.Safe.Compilation.IR { + [AssemblyWalk(InterfaceType = typeof(IOptimizationPass), StaticRegisterMethod = "RegisterMethod")] public class IROptimizer { + private static readonly SortedSet optimizationPasses = new SortedSet( + Comparer.Create((a, b) => a.SortIndex.CompareTo(b.SortIndex))); public OptimizationLevel OptimizationLevel { get; } public IROptimizer(OptimizationLevel optimizationLevel) { OptimizationLevel = optimizationLevel; } + public static void RegisterMethod(Type type) + { + optimizationPasses.Add((IOptimizationPass)Activator.CreateInstance(type)); + } public List Optimize(List blocks) { @@ -20,9 +27,38 @@ public List Optimize(List blocks) return blocks; ExtendedBasicBlock extendedRootBlock = ExtendedBasicBlock.CreateExtendedBlockTree(blocks[0]); - HashSet extendedBlocks = new HashSet(ExtendedBasicBlock.DumpTree(extendedRootBlock)); + List extendedBlocks = new List(ExtendedBasicBlock.DumpTree(extendedRootBlock)); + + foreach (IOptimizationPass pass in optimizationPasses) + { + if (pass.OptimizationLevel > OptimizationLevel) + continue; - ConstantFolding.ApplyPass(blocks); + SafeHouse.Logger.Log($"Applying optimization pass: {pass.GetType()}."); + try + { + switch (pass) + { + case IOptimizationPass blockPass: + blockPass.ApplyPass(blocks); + break; + case IOptimizationPass extendedBlockPass: + extendedBlockPass.ApplyPass(extendedBlocks); + break; + case IOptimizationPass instructionPass: + foreach (BasicBlock block in blocks) + instructionPass.ApplyPass(block.Instructions); + break; + default: + SafeHouse.Logger.LogWarning($"{pass.GetType()}, implementing IOptimizingPass, uses an unsupported generic parameter."); + break; + } + } + catch (Exception e) + { + throw new Exceptions.KOSCompileException(new KS.LineCol(0, 0), e.Message); + } + } return blocks; } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs index 34c339ed8..f13e28acf 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs @@ -6,15 +6,19 @@ namespace kOS.Safe.Compilation.IR.Optimization { - public static class ConstantFolding + public class ConstantFolding : IOptimizationPass { private static readonly InterimCPU interimCPU = new InterimCPU(); private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = interimCPU }; + + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + public short SortIndex => 1; + static ConstantFolding() { shared.FunctionManager = new FunctionManager(shared); } - public static void ApplyPass(IEnumerable blocks) + public void ApplyPass(List blocks) { Queue worklist = new Queue(blocks); IEnumerator enumerator = blocks.GetEnumerator(); diff --git a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs b/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs new file mode 100644 index 000000000..fec2bd9a5 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace kOS.Safe.Compilation.IR.Optimization +{ + public interface IOptimizationPass + { + OptimizationLevel OptimizationLevel { get; } + short SortIndex { get; } + } + public interface IOptimizationPass : IOptimizationPass + { + void ApplyPass(List code); + } +} From 159e0680eb42b3cddc4805b1e606d1f9e41eb88c Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 7 Mar 2026 17:06:30 -0500 Subject: [PATCH 012/120] Move the InterimCPU ownership to IROptimizer to share resources across passes. --- src/kOS.Safe/Compilation/IR/IROptimizer.cs | 11 +++++++++++ .../Compilation/IR/Optimization/ConstantFolding.cs | 12 +++--------- .../Compilation/IR/Optimization/InterimCPU.cs | 4 ++-- src/kOS.Safe/Exceptions/KOSCompileException.cs | 2 +- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index 6bb0e095e..8e42256d7 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using kOS.Safe.Compilation.IR.Optimization; +using kOS.Safe.Function; using kOS.Safe.Utilities; namespace kOS.Safe.Compilation.IR @@ -8,10 +9,18 @@ namespace kOS.Safe.Compilation.IR [AssemblyWalk(InterfaceType = typeof(IOptimizationPass), StaticRegisterMethod = "RegisterMethod")] public class IROptimizer { + internal static InterimCPU InterimCPU { get; } = new InterimCPU(); + private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = InterimCPU }; + public static IFunctionManager FunctionManager => shared.FunctionManager; + private static readonly SortedSet optimizationPasses = new SortedSet( Comparer.Create((a, b) => a.SortIndex.CompareTo(b.SortIndex))); public OptimizationLevel OptimizationLevel { get; } + static IROptimizer() + { + shared.FunctionManager = new FunctionManager(shared); + } public IROptimizer(OptimizationLevel optimizationLevel) { OptimizationLevel = optimizationLevel; @@ -56,6 +65,8 @@ public List Optimize(List blocks) } catch (Exception e) { + if (e is Exceptions.KOSCompileException) + throw; throw new Exceptions.KOSCompileException(new KS.LineCol(0, 0), e.Message); } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs index f13e28acf..82232264a 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs @@ -8,16 +8,9 @@ namespace kOS.Safe.Compilation.IR.Optimization { public class ConstantFolding : IOptimizationPass { - private static readonly InterimCPU interimCPU = new InterimCPU(); - private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = interimCPU }; - public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; public short SortIndex => 1; - static ConstantFolding() - { - shared.FunctionManager = new FunctionManager(shared); - } public void ApplyPass(List blocks) { Queue worklist = new Queue(blocks); @@ -282,7 +275,7 @@ private static IRValue ReduceCall(IRCall instruction) instruction.Arguments[i] = AttemptReduction(temp.Parent); } string functionName = instruction.Function.Replace("()", ""); - if (shared.FunctionManager.Exists(functionName) && instruction.Arguments.All(arg => arg is IRConstant)) + if (IROptimizer.FunctionManager.Exists(functionName) && instruction.Arguments.All(arg => arg is IRConstant)) { try { @@ -306,11 +299,12 @@ private static IRValue ReduceCall(IRCall instruction) case "arctan": case "arctan2": case "anglediff": + InterimCPU interimCPU = IROptimizer.InterimCPU; interimCPU.Boot(); // Clear the stack out of caution. interimCPU.PushArgumentStack(new Execution.KOSArgMarkerType()); foreach (IRValue arg in instruction.Arguments) interimCPU.PushArgumentStack(((IRConstant)arg).Value); - shared.FunctionManager.CallFunction(functionName); + IROptimizer.FunctionManager.CallFunction(functionName); instruction.Result = new IRConstant(interimCPU.PopValueArgument()); return instruction.Result; } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs b/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs index 413b02ee3..68452d427 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs @@ -25,6 +25,8 @@ public object PopValueArgument(bool barewordOkay = false) => GetValue(PopArgumentStack(), barewordOkay); public void PushArgumentStack(object item) => stack.PushArgument(item); + public object PopValueEncapsulatedArgument(bool barewordOkay = false) + => Structure.FromPrimitive(PopValueArgument(barewordOkay)); public object GetValue(object testValue, bool barewordOkay = false) { // $cos cos named variable @@ -116,8 +118,6 @@ public object PopScopeStack(int howMany) => throw new NotImplementedException(); public Structure PopStructureEncapsulatedArgument(bool barewordOkay = false) => throw new NotImplementedException(); - public object PopValueEncapsulatedArgument(bool barewordOkay = false) - => throw new NotImplementedException(); public void PushNewScope(short scopeId, short parentScopeId) => throw new NotImplementedException(); diff --git a/src/kOS.Safe/Exceptions/KOSCompileException.cs b/src/kOS.Safe/Exceptions/KOSCompileException.cs index e59e47eda..4ae21be2f 100644 --- a/src/kOS.Safe/Exceptions/KOSCompileException.cs +++ b/src/kOS.Safe/Exceptions/KOSCompileException.cs @@ -37,7 +37,7 @@ public KOSCompileException(Token token, string message) { } - public KOSCompileException(Compilation.IR.IRInstruction irInstruction, KOSException innerException) + public KOSCompileException(Compilation.IR.IRInstruction irInstruction, System.Exception innerException) : this(new LineCol(irInstruction.SourceLine, irInstruction.SourceColumn), innerException.Message) { } From 2a71eccdd469fe6a1519e2f9215b2d734e679ed0 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 7 Mar 2026 17:08:35 -0500 Subject: [PATCH 013/120] Add suffix replacement pass. This replaces all ship: suffixes that can be aliased with the alias (saves one opcode per access). This also replaces all constant() or constant: suffix accesses with the constant value, and does so in time for the constant folding pass. --- .../integration/suffixReplacement.ks | 4 + .../Execution/OptimizationTest.cs | 13 ++ src/kOS.Safe/Compilation/IR/IRInstruction.cs | 2 +- .../IR/Optimization/SuffixReplacement.cs | 194 ++++++++++++++++++ src/kOS.Safe/Compilation/OptimizationLevel.cs | 4 +- 5 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 kerboscript_tests/integration/suffixReplacement.ks create mode 100644 src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs diff --git a/kerboscript_tests/integration/suffixReplacement.ks b/kerboscript_tests/integration/suffixReplacement.ks new file mode 100644 index 000000000..b1cde7f96 --- /dev/null +++ b/kerboscript_tests/integration/suffixReplacement.ks @@ -0,0 +1,4 @@ +set airspeed to 100. // Clobber the built-in so that this value is pulled after replacement. +print(CONSTANT:g0). +print(CONSTANT():pi). +print(SHIP:airspeed). \ No newline at end of file diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index ddc020ef1..141399853 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -133,5 +133,18 @@ public void TestSuffixes() "False" ); } + + [Test] + public void TestSuffixReplacement() + { + // Test that certain suffixes are replaced + RunScript("integration/suffixReplacement.ks"); + RunSingleStep(); + AssertOutput( + "9.80665", + "3.14159265358979", + "100" + ); + } } } diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index a20c168ab..2abfca91a 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -243,7 +243,7 @@ public override string ToString() } public class IRSuffixGet : IRInteractsInstruction, IResultingInstruction, ISingleOperandInstruction { - public IRValue Result { get; } + public IRValue Result { get; set; } public IRValue Object { get; set; } public string Suffix { get; set; } IRValue ISingleOperandInstruction.Operand => Object; diff --git a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs b/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs new file mode 100644 index 000000000..264e53544 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; + +namespace kOS.Safe.Compilation.IR.Optimization +{ + public class SuffixReplacement : IOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + public short SortIndex => 0; + + public void ApplyPass(List code) + { + for (int i = 0; i < code.Count; i++) + { + IRInstruction instruction = code[i]; + switch (instruction) + { + case IRPop pop: + if (AttemptReplacement(pop.Value) is IRConstant) + { + code.RemoveAt(i); + i--; + } + continue; + case IRAssign assign: + assign.Value = AttemptReplacement(assign.Value); + break; + case IRSuffixSet suffixSet: + suffixSet.Value = AttemptReplacement(suffixSet.Value); + suffixSet.Object = AttemptReplacement(suffixSet.Object); + break; + case IRIndexSet indexSet: + indexSet.Value = AttemptReplacement(indexSet.Value); + indexSet.Object = AttemptReplacement(indexSet.Object); + indexSet.Index = AttemptReplacement(indexSet.Index); + break; + case IRBranch branch: + AttemptReplacement(branch.Condition); + break; + } + } + } + + private static IRValue AttemptReplacement(IRValue input) + { + if (input is IRTemp temp) + { + if (temp.Parent is IRSuffixGet suffixGet) + { + AttempReplaceSuffix(suffixGet); + } + else + { + AttemptReplacement(temp.Parent); + } + } + return input; + } + private static IRValue AttempReplaceSuffix(IRSuffixGet suffixGet) + { + if (suffixGet.Object is IRVariable objVariable) + { + if (objVariable.Name == "$ship") + { + switch (suffixGet.Suffix) + { + case "name": + // Rename the alias shortcut appropriately + suffixGet.Suffix = "shipname"; + break; + case "heading": + case "prograde": + case "retrograde": + case "facing": + case "maxthrust": + case "velocity": + case "geoposition": + case "latitude": + case "longitude": + case "up": + case "north": + case "body": + case "angularmomentum": + case "angularvel": + case "mass": + case "verticalSpeed": + case "groundspeed": + case "airspeed": + case "altitude": + case "apoapsis": + case "periapsis": + case "sensors": + case "srfprograde": + case "srfretrograde": + case "obt": + case "status": + break; + // All other suffixes don't have an alias, so just return the original IRTemp + default: + return suffixGet.Result; + } + // Instead of the IRTemp, which leads to resolving the suffix, return just the alias shortcut. + suffixGet.Result = new IRVariable($"${suffixGet.Suffix}"); + return suffixGet.Result; + } + if (objVariable.Name == "$constant") + { + return ReplaceConstantSuffix(suffixGet); + } + } + else if (suffixGet.Object is IRTemp tempObj && tempObj.Parent is IRCall call && call.Function == "constant()" && call.Arguments.Count == 0) + { + return ReplaceConstantSuffix(suffixGet); + } + return suffixGet.Result; + } + private static IRConstant ReplaceConstantSuffix(IRSuffixGet suffixGet) + { + IROptimizer.InterimCPU.PushArgumentStack(new Encapsulation.ConstantValue()); + try + { + new OpcodeGetMember(suffixGet.Suffix).Execute(IROptimizer.InterimCPU); + } + catch (Exception e) + { + throw new Exceptions.KOSCompileException(suffixGet, e); + } + IRConstant result = new IRConstant(IROptimizer.InterimCPU.PopValueArgument()); + suffixGet.Result = result; + return result; + } + private static IRValue AttemptReplacement(IRInstruction instruction) + { + switch (instruction) + { + case IRUnaryOp unaryOp: + return ReduceUnary(unaryOp); + case IRBinaryOp binaryOp: + return ReduceBinary(binaryOp); + case IRSuffixGet suffixGet: + return ReduceSuffixGet(suffixGet); + case IRIndexGet indexGet: + return ReduceIndexGet(indexGet); + case IRCall call: + return ReduceCall(call); + } + throw new ArgumentException($"{instruction.GetType()} is not supported."); + } + private static IRValue ReduceUnary(IRUnaryOp instruction) + { + if (instruction.Operand is IRTemp temp) + instruction.Operand = AttemptReplacement(temp); + + return instruction.Result; + } + private static IRValue ReduceBinary(IRBinaryOp instruction) + { + if (instruction.Left is IRTemp tempL) + instruction.Left = AttemptReplacement(tempL); + if (instruction.Right is IRTemp tempR) + instruction.Right = AttemptReplacement(tempR); + + return instruction.Result; + } + + private static IRValue ReduceSuffixGet(IRSuffixGet instruction) + { + if (instruction.Object is IRTemp temp) + instruction.Object = AttemptReplacement(temp); + + return instruction.Result; + } + private static IRValue ReduceIndexGet(IRIndexGet instruction) + { + if (instruction.Object is IRTemp tempObj) + instruction.Object = AttemptReplacement(tempObj); + + if (instruction.Index is IRTemp tempIndex) + instruction.Index = AttemptReplacement(tempIndex); + + return instruction.Result; + } + private static IRValue ReduceCall(IRCall instruction) + { + for (int i = instruction.Arguments.Count - 1; i >= 0; i--) + { + if (instruction.Arguments[i] is IRTemp temp) + instruction.Arguments[i] = AttemptReplacement(temp); + } + + return instruction.Result; + } + } +} diff --git a/src/kOS.Safe/Compilation/OptimizationLevel.cs b/src/kOS.Safe/Compilation/OptimizationLevel.cs index 7ebb682de..920f4e426 100644 --- a/src/kOS.Safe/Compilation/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/OptimizationLevel.cs @@ -2,13 +2,13 @@ namespace kOS.Safe.Compilation { /* * O1: + * Replace CONSTANT: values with the constant + * Replace ship fields with their alias * Constant folding * - * Replace ship fields with their alias * Constant propagation * Replace lex indexing with string constant with suffixing where possible * Dead code elimination (for the never used case) - * Replace constant() with constant * O2: * Redundant expression elimination * Particularly: Any expression of 3 opcodes used more than twice, From eb11e29a41ae171d2c03cfafc4bd2fae18f977ad Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 7 Mar 2026 19:42:44 -0500 Subject: [PATCH 014/120] Store variable and constant source line and column so that (theoretically) every emitted opcode will have a location that makes sense. --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 8 +-- src/kOS.Safe/Compilation/IR/IRValue.cs | 61 +++++++++++++++---- .../IR/Optimization/ConstantFolding.cs | 14 ++--- .../IR/Optimization/SuffixReplacement.cs | 4 +- 4 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index aa97fdf9a..ca7d6c7f7 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -252,15 +252,15 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack Value = value; + protected readonly short sourceLine, sourceColumn; + public IRConstant(object value, Opcode opcode) : this(value, opcode.SourceLine, opcode.SourceColumn) { } + public IRConstant(object value, IRInstruction instruction) : this(value, instruction.SourceLine, instruction.SourceColumn) { } + public IRConstant(object value, short sourceLine, short sourceColumn) + { + Value = value; + this.sourceLine = sourceLine; + this.sourceColumn = sourceColumn; + } internal override IEnumerable EmitPush() { - yield return new OpcodePush(Value); + yield return new OpcodePush(Value) + { + SourceLine = sourceLine, + SourceColumn = sourceColumn + }; } public override string ToString() => Value.ToString(); @@ -34,37 +45,54 @@ public IRVariableBase(string name) } public class IRVariable : IRVariableBase { + protected readonly short sourceLine, sourceColumn; public bool IsLock { get; } - public IRVariable(string name, bool isLock = false) : base(name) + public IRVariable(string name, IRInstruction instruction, bool isLock = false) : this(name, instruction.SourceLine, instruction.SourceColumn, isLock) { } + public IRVariable(string name, Opcode opcode, bool isLock = false) : this(name, opcode.SourceLine, opcode.SourceColumn, isLock) { } + public IRVariable(string name, short sourceLine, short sourceColumn, bool isLock = false) : base(name) { IsLock = isLock; + this.sourceLine = sourceLine; + this.sourceColumn = sourceColumn; } internal override IEnumerable EmitPush() { - yield return new OpcodePush(Name); + yield return new OpcodePush(Name) + { + SourceLine = sourceLine, + SourceColumn = sourceColumn + }; } public override string ToString() => Name; } public class IRRelocateLater : IRConstant { - public IRRelocateLater(string value) : base(value) { } + public IRRelocateLater(string value, OpcodePushRelocateLater opcode) : base(value, opcode) { } internal override IEnumerable EmitPush() { - yield return new OpcodePushRelocateLater((string)Value); + yield return new OpcodePushRelocateLater((string)Value) + { + SourceLine = sourceLine, + SourceColumn = sourceColumn + }; } } public class IRDelegateRelocateLater : IRRelocateLater { public bool WithClosure { get; } - public IRDelegateRelocateLater(string value, bool withClosure) : base(value) + public IRDelegateRelocateLater(string value, bool withClosure, OpcodePushDelegateRelocateLater opcode) : base(value, opcode) { WithClosure = withClosure; } internal override IEnumerable EmitPush() { - yield return new OpcodePushDelegateRelocateLater((string)Value, WithClosure); + yield return new OpcodePushDelegateRelocateLater((string)Value, WithClosure) + { + SourceLine = sourceLine, + SourceColumn = sourceColumn + }; } } public class IRTemp : IRVariableBase @@ -80,12 +108,23 @@ public IRTemp(int id) : base($"$.temp.{id}") public IRAssign PromoteToVariable() { isPromoted = true; - return new IRAssign(new OpcodeStoreLocal(Name), this) { Scope = IRAssign.StoreScope.Local }; + return new IRAssign(new OpcodeStoreLocal(Name) + { + SourceLine = -1, + SourceColumn = 0 + }, this) + { + Scope = IRAssign.StoreScope.Local + }; } internal override IEnumerable EmitPush() { if (isPromoted) - yield return new OpcodePush(Name); + yield return new OpcodePush(Name) + { + SourceLine = -1, + SourceColumn = 0 + }; else foreach (Opcode opcode in Parent.EmitOpcode()) yield return opcode; diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs index 82232264a..05a861308 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs @@ -98,13 +98,13 @@ private static IRValue ReduceUnary(IRUnaryOp instruction) switch (instruction.Operation) { case OpcodeMathNegate _: - result = new IRConstant(OpcodeMathNegate.StaticOperation(input)); + result = new IRConstant(OpcodeMathNegate.StaticOperation(input), instruction); break; case OpcodeLogicNot _: - result = new IRConstant(OpcodeLogicNot.StaticOperation(input)); + result = new IRConstant(OpcodeLogicNot.StaticOperation(input), instruction); break; case OpcodeLogicToBool _: - result = new IRConstant(OpcodeLogicToBool.StaticOperation(input)); + result = new IRConstant(OpcodeLogicToBool.StaticOperation(input), instruction); break; default: result = instruction.Result; @@ -144,7 +144,7 @@ private static IRValue ReduceBinary(IRBinaryOp instruction) object right = constantR.Value; try { - IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right)); + IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right), instruction); instruction.Result = result; } catch (KOSBinaryOperandTypeException binaryTypeException) @@ -166,7 +166,7 @@ temp.Parent is IRBinaryOp leftOp && object left = constantL1.Value; try { - IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right)); + IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right), instruction); instruction.Result = result; leftOp.Right = result; } @@ -200,7 +200,7 @@ temp.Parent is IRBinaryOp leftOp && case OpcodeMathPower _: // X^0 = 1 if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) - return new IRConstant(Encapsulation.ScalarIntValue.One); + return new IRConstant(Encapsulation.ScalarIntValue.One, instruction); // X^1 = X if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) return instruction.Left; @@ -305,7 +305,7 @@ private static IRValue ReduceCall(IRCall instruction) foreach (IRValue arg in instruction.Arguments) interimCPU.PushArgumentStack(((IRConstant)arg).Value); IROptimizer.FunctionManager.CallFunction(functionName); - instruction.Result = new IRConstant(interimCPU.PopValueArgument()); + instruction.Result = new IRConstant(interimCPU.PopValueArgument(), instruction); return instruction.Result; } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs b/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs index 264e53544..758408a2e 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs @@ -100,7 +100,7 @@ private static IRValue AttempReplaceSuffix(IRSuffixGet suffixGet) return suffixGet.Result; } // Instead of the IRTemp, which leads to resolving the suffix, return just the alias shortcut. - suffixGet.Result = new IRVariable($"${suffixGet.Suffix}"); + suffixGet.Result = new IRVariable($"${suffixGet.Suffix}", suffixGet); return suffixGet.Result; } if (objVariable.Name == "$constant") @@ -125,7 +125,7 @@ private static IRConstant ReplaceConstantSuffix(IRSuffixGet suffixGet) { throw new Exceptions.KOSCompileException(suffixGet, e); } - IRConstant result = new IRConstant(IROptimizer.InterimCPU.PopValueArgument()); + IRConstant result = new IRConstant(IROptimizer.InterimCPU.PopValueArgument(), suffixGet); suffixGet.Result = result; return result; } From 25b2897cd43a696ab3c25583c598fbd49cf54640 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 7 Mar 2026 20:11:13 -0500 Subject: [PATCH 015/120] Offload optimization steps from Compiler. Add a new IRCodePart representation for whole-program optimizations (e.g. function inlining). --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 64 +++++++++---------- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 25 ++++++++ src/kOS.Safe/Compilation/IR/IROptimizer.cs | 24 +++++-- .../IR/Optimization/IOptimizationPass.cs | 4 ++ src/kOS.Safe/Compilation/KS/Compiler.cs | 22 +++---- 5 files changed, 88 insertions(+), 51 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/IRCodePart.cs diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index ca7d6c7f7..44a3bf4fc 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -8,18 +8,18 @@ public class IRBuilder { private int nextTempId = 0; - public List Blocks { get; } = new List(); - - public void Lower(List code) + public List Lower(List code) { + List blocks = new List(); if (code.Count == 0) - return; + return blocks; Dictionary labels = ProgramBuilder.MapLabels(code); - CreateBlocks(code, labels); - FillBlocks(code, labels); + CreateBlocks(code, labels, blocks); + FillBlocks(code, labels, blocks); + return blocks; } - private void CreateBlocks(List code, Dictionary labels) + private void CreateBlocks(List code, Dictionary labels, List blocks) { SortedSet leaders = new SortedSet() { 0 }; for (int i = 1; i < code.Count; i++) // The first instruction is always a leader so we can skip 0. @@ -32,39 +32,37 @@ private void CreateBlocks(List code, Dictionary labels) else leaders.Add(i + branch.Distance); } - else if (code[i] is OpcodeJumpStack jumpstack) + else if (code[i] is OpcodeJumpStack) { throw new NotImplementedException("OpcodeJumpStack is not implemented for optimization because it is non-deterministic. Use OptimizationLevel.None."); } else if (code[i] is OpcodeReturn) - { leaders.Add(i + 1); - } - //else if (code[i] is OpcodePushRelocateLater relocateLater) - //{ - //leaders.Add(labels[relocateLater.DestinationLabel]); - //} + else if (code[i] is OpcodePushScope) + leaders.Add(i); + else if (code[i] is OpcodePopScope) + leaders.Add(i + 1); } leaders.Add(code.Count); foreach (int startIndex in leaders.Take(leaders.Count - 1)) { int endIndex = leaders.First(i => i > startIndex) - 1; - BasicBlock block = new BasicBlock(startIndex, endIndex, Blocks.Count); - Blocks.Add(block); + BasicBlock block = new BasicBlock(startIndex, endIndex, blocks.Count); + blocks.Add(block); } - foreach (BasicBlock block in Blocks) + foreach (BasicBlock block in blocks) { Opcode lastOpcode = code[block.EndIndex]; if (lastOpcode is BranchOpcode branch) { int destinationIndex = branch.DestinationLabel != string.Empty ? labels[branch.DestinationLabel] : block.EndIndex + branch.Distance; - block.AddSuccessor(GetBlockFromStartIndex(destinationIndex)); + block.AddSuccessor(GetBlockFromStartIndex(blocks, destinationIndex)); if (!(branch is OpcodeBranchJump)) - block.AddSuccessor(GetBlockFromStartIndex(block.EndIndex + 1)); + block.AddSuccessor(GetBlockFromStartIndex(blocks, block.EndIndex + 1)); } - else if (Blocks.Any(b => b.StartIndex == block.EndIndex + 1)) + else if (blocks.Any(b => b.StartIndex == block.EndIndex + 1)) { - BasicBlock successor = GetBlockFromStartIndex(block.EndIndex + 1); + BasicBlock successor = GetBlockFromStartIndex(blocks, block.EndIndex + 1); block.Add(new IRJump(successor, lastOpcode.SourceLine, lastOpcode.SourceColumn)); block.AddSuccessor(successor); } @@ -74,21 +72,21 @@ private void CreateBlocks(List code, Dictionary labels) } } - private BasicBlock GetBlockFromStartIndex(int startIndex) - => Blocks.First(b => b.StartIndex == startIndex); + private BasicBlock GetBlockFromStartIndex(List blocks, int startIndex) + => blocks.First(b => b.StartIndex == startIndex); - private void FillBlocks(List code, Dictionary labels) + private void FillBlocks(List code, Dictionary labels, List blocks) { Stack stack = new Stack(); - BasicBlock currentBlock = GetBlockFromStartIndex(0); + BasicBlock currentBlock = GetBlockFromStartIndex(blocks, 0); for (int i = 0; i < code.Count; i++) { if (i > currentBlock.EndIndex) { currentBlock.SetStackState(stack); - currentBlock = GetBlockFromStartIndex(i); + currentBlock = GetBlockFromStartIndex(blocks, i); } - ParseInstruction(code[i], currentBlock, stack, labels, i); + ParseInstruction(code[i], currentBlock, stack, labels, i, blocks); } } @@ -99,7 +97,7 @@ private IRTemp CreateTemp() return result; } - private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack stack, Dictionary labels, int index) + private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack stack, Dictionary labels, int index, List blocks) { HashSet variables = new HashSet(); switch (opcode) @@ -178,14 +176,14 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack FunctionsCode { get; set; } + public List InitializationCode { get; set; } + public List MainCode { get; set; } + public void EmitToCodePart(CodePart codePart) + { + codePart.FunctionsCode = IREmitter.Emit(FunctionsCode); + codePart.InitializationCode = IREmitter.Emit(InitializationCode); + codePart.MainCode = IREmitter.Emit(MainCode); + } + } +} diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index 8e42256d7..fc6d71bc1 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -30,13 +30,24 @@ public static void RegisterMethod(Type type) optimizationPasses.Add((IOptimizationPass)Activator.CreateInstance(type)); } - public List Optimize(List blocks) + public List Optimize(IRCodePart codePart) { - if (blocks.Count == 0) - return blocks; + List blocks = new List(); + blocks.AddRange(codePart.InitializationCode); + blocks.AddRange(codePart.FunctionsCode); + blocks.AddRange(codePart.MainCode); - ExtendedBasicBlock extendedRootBlock = ExtendedBasicBlock.CreateExtendedBlockTree(blocks[0]); - List extendedBlocks = new List(ExtendedBasicBlock.DumpTree(extendedRootBlock)); + ExtendedBasicBlock initializationRoot = codePart.InitializationCode.Count > 0 ? ExtendedBasicBlock.CreateExtendedBlockTree(codePart.InitializationCode[0]) : null; + ExtendedBasicBlock functionsRoot = codePart.FunctionsCode.Count > 0 ? ExtendedBasicBlock.CreateExtendedBlockTree(codePart.FunctionsCode[0]) : null; + ExtendedBasicBlock mainRoot = codePart.MainCode.Count > 0 ? ExtendedBasicBlock.CreateExtendedBlockTree(codePart.MainCode[0]) : null; + + List extendedBlocks = new List(); + if (initializationRoot != null) + extendedBlocks.AddRange(ExtendedBasicBlock.DumpTree(initializationRoot)); + if (functionsRoot != null) + extendedBlocks.AddRange(ExtendedBasicBlock.DumpTree(functionsRoot)); + if (mainRoot != null) + extendedBlocks.AddRange(ExtendedBasicBlock.DumpTree(mainRoot)); foreach (IOptimizationPass pass in optimizationPasses) { @@ -48,6 +59,9 @@ public List Optimize(List blocks) { switch (pass) { + case IHolisticOptimizationPass codePartpass: + codePartpass.ApplyPass(codePart); + break; case IOptimizationPass blockPass: blockPass.ApplyPass(blocks); break; diff --git a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs b/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs index fec2bd9a5..8c746c845 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs @@ -12,4 +12,8 @@ public interface IOptimizationPass : IOptimizationPass { void ApplyPass(List code); } + public interface IHolisticOptimizationPass : IOptimizationPass + { + void ApplyPass(IRCodePart codePart); + } } diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index 6daeae868..3c9b6a814 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -187,17 +187,6 @@ private void AssertNoFuncBuiltinViolation(string name) return; } - public static void Optimize(List code, CompilerOptions options) - { - IR.IRBuilder irBuilder = new IR.IRBuilder(); - irBuilder.Lower(code); - List blocks = irBuilder.Blocks; - IR.IROptimizer optimizer = new IR.IROptimizer(options.OptimizationLevel); - blocks = optimizer.Optimize(blocks); - code.Clear(); - code.AddRange(IR.IREmitter.Emit(blocks)); - } - public CodePart Compile(int startLineNum, ParseTree tree, Context context, CompilerOptions options) { this.options = options; @@ -215,13 +204,20 @@ public CodePart Compile(int startLineNum, ParseTree tree, Context context, Compi CompileProgram(tree); if (options.OptimizationLevel != OptimizationLevel.None) { - foreach (List code in new[] { part.InitializationCode, part.FunctionsCode, part.MainCode }) - Optimize(code, options); + Optimize(part, options); } } return part; } + public static void Optimize(CodePart code, CompilerOptions options) + { + IR.IRCodePart irCode = new IR.IRCodePart(code); + IR.IROptimizer optimizer = new IR.IROptimizer(options.OptimizationLevel); + optimizer.Optimize(irCode); + irCode.EmitToCodePart(code); + } + private void CompileProgram(ParseTree tree) { currentCodeSection = part.MainCode; From c4da4dfac712601aa65d53d2f19198976f445ad0 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 00:09:55 -0500 Subject: [PATCH 016/120] Add nonsequential label capability to BasicBlocks to account for weird function and lock labels. Fix fallthrough jumps happening at the wrong time. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 12 ++++++++++-- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 7 +++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index b5ee45722..5bd0abc81 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -12,20 +12,23 @@ public class BasicBlock public IEnumerable Predecessors => predecessors; private readonly HashSet predecessors = new HashSet(); private readonly HashSet sucessors = new HashSet(); - public string Label => $"@BB#{ID}"; + public string Label => nonSequentialLabel ?? $"@BB#{ID}"; + private string nonSequentialLabel = null; public int ID { get; } public Optimization.ExtendedBasicBlock ExtendedBlock { get; set; } private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. + public IRJump FallthroughJump { get; set; } = null; #if DEBUG internal Opcode[] OriginalOpcodes { get; set; } internal Opcode[] GeneratedOpcodes => EmitOpCodes().ToArray(); #endif - public BasicBlock(int startIndex, int endIndex, int id) + public BasicBlock(int startIndex, int endIndex, int id, string nonSequentialLabel = null) { StartIndex = startIndex; EndIndex = endIndex; ID = id; + this.nonSequentialLabel = nonSequentialLabel; } public void Add(IRInstruction instruction) @@ -60,6 +63,9 @@ public override string ToString() public IEnumerable EmitOpCodes() { + bool addedFallthrough = FallthroughJump != null && Instructions.LastOrDefault() == FallthroughJump; + if (addedFallthrough) + Instructions.Add(FallthroughJump); bool first = true; foreach (IRInstruction instruction in Instructions.Take(Instructions.Count - 1)) { @@ -97,6 +103,8 @@ public IEnumerable EmitOpCodes() yield return opcode; } } + if (addedFallthrough) + Instructions.Remove(FallthroughJump); } } } diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 44a3bf4fc..fbe5a7460 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -47,7 +47,10 @@ private void CreateBlocks(List code, Dictionary labels, Lis foreach (int startIndex in leaders.Take(leaders.Count - 1)) { int endIndex = leaders.First(i => i > startIndex) - 1; - BasicBlock block = new BasicBlock(startIndex, endIndex, blocks.Count); + string label = code[startIndex].Label; + if (label.StartsWith("@")) + label = null; + BasicBlock block = new BasicBlock(startIndex, endIndex, blocks.Count, label); blocks.Add(block); } foreach (BasicBlock block in blocks) @@ -63,7 +66,7 @@ private void CreateBlocks(List code, Dictionary labels, Lis else if (blocks.Any(b => b.StartIndex == block.EndIndex + 1)) { BasicBlock successor = GetBlockFromStartIndex(blocks, block.EndIndex + 1); - block.Add(new IRJump(successor, lastOpcode.SourceLine, lastOpcode.SourceColumn)); + block.FallthroughJump = new IRJump(successor, lastOpcode.SourceLine, lastOpcode.SourceColumn); block.AddSuccessor(successor); } #if DEBUG From 8f0ee703abd4454ecf8b8a144c0395ff8d7c30e0 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 00:11:23 -0500 Subject: [PATCH 017/120] Implement arg tests. --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 8 ++++++++ src/kOS.Safe/Compilation/IR/IRInstruction.cs | 14 ++++++++++---- .../Compilation/IR/Optimization/ConstantFolding.cs | 2 ++ .../IR/Optimization/SuffixReplacement.cs | 2 ++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index fbe5a7460..5bd071c7a 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -175,8 +175,16 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack EmitOpcode() public override string ToString() => "{pop}"; } - public class IRNonVarPush : IRInstruction + public class IRNonVarPush : IRInstruction, IResultingInstruction { public override bool SideEffects => false; public Opcode Operation { get; } - public IRNonVarPush(Opcode opcode) : base(opcode) - => Operation = opcode; + + public IRValue Result { get; } + + public IRNonVarPush(IRValue result, Opcode opcode) : base(opcode) + { + Operation = opcode; + Result = result; + } internal override IEnumerable EmitOpcode() { Operation.Label = string.Empty; @@ -625,6 +631,6 @@ internal override IEnumerable EmitOpcode() yield return SetSourceLocation(new OpcodeReturn(Depth)); } public override string ToString() - => string.Format("{{ret {0}}}", Depth); + => string.Format("{return {0} deep}", Depth); } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs index 05a861308..33d358033 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs @@ -81,6 +81,8 @@ private static IRValue AttemptReduction(IRInstruction instruction) return ReduceIndexGet(indexGet); case IRCall call: return ReduceCall(call); + case IResultingInstruction resulting: + return resulting.Result; } throw new ArgumentException($"{instruction.GetType()} is not supported."); } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs b/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs index 758408a2e..f8fe76b62 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs @@ -143,6 +143,8 @@ private static IRValue AttemptReplacement(IRInstruction instruction) return ReduceIndexGet(indexGet); case IRCall call: return ReduceCall(call); + case IResultingInstruction resulting: + return resulting.Result; } throw new ArgumentException($"{instruction.GetType()} is not supported."); } From 3a5cf78695ffd8a003e272e3d9bf8a7762f14b52 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 00:11:36 -0500 Subject: [PATCH 018/120] Fix stack underflow when storing parameters. --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 12 ++++++++---- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 5 +++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 5bd071c7a..9163d8610 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -106,22 +106,26 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack 0 ? stack.Pop() : null; + IRAssign assignment = new IRAssign(store, storedValue) { Scope = IRAssign.StoreScope.Ambivalent }; variables.Add(assignment.Target); currentBlock.Add(assignment); break; case OpcodeStoreExist storeExist: - assignment = new IRAssign(storeExist, stack.Pop()) { AssertExists = true }; + storedValue = stack.Count > 0 ? stack.Pop() : null; + assignment = new IRAssign(storeExist, storedValue) { AssertExists = true }; variables.Add(assignment.Target); currentBlock.Add(assignment); break; case OpcodeStoreLocal storeLocal: - assignment = new IRAssign(storeLocal, stack.Pop()) { Scope = IRAssign.StoreScope.Local }; + storedValue = stack.Count > 0 ? stack.Pop() : null; + assignment = new IRAssign(storeLocal, storedValue) { Scope = IRAssign.StoreScope.Local }; currentBlock.Add(assignment); variables.Add(assignment.Target); break; case OpcodeStoreGlobal storeGlobal: - assignment = new IRAssign(storeGlobal, stack.Pop()) { Scope = IRAssign.StoreScope.Global }; + storedValue = stack.Count > 0 ? stack.Pop() : null; + assignment = new IRAssign(storeGlobal, storedValue) { Scope = IRAssign.StoreScope.Global }; currentBlock.Add(assignment); variables.Add(assignment.Target); break; diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index f44a841bc..be7281c74 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -68,8 +68,9 @@ public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : base(opcode) } internal override IEnumerable EmitOpcode() { - foreach (Opcode opcode in Value.EmitPush()) - yield return opcode; + if (Value != null) + foreach (Opcode opcode in Value.EmitPush()) + yield return opcode; if (AssertExists) { yield return SetSourceLocation(new OpcodeStoreExist(Target)); From 6651a5ff1798fbababb3b62ea18565161303f3ff Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 00:16:21 -0500 Subject: [PATCH 019/120] Rewrite IRCodePart and the flow from Compiler to Optimizer to enable optimizing of function code fragments. Add inspection methods to UserFunction and UserFunctionCollection to intercept the code fragments during the optimization pipeline. --- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 97 ++++++++++++++++--- src/kOS.Safe/Compilation/IR/IREmitter.cs | 26 +++-- src/kOS.Safe/Compilation/IR/IROptimizer.cs | 64 +++++------- src/kOS.Safe/Compilation/KS/Compiler.cs | 10 +- src/kOS.Safe/Compilation/KS/UserFunction.cs | 3 + .../Compilation/KS/UserFunctionCollection.cs | 2 + 6 files changed, 135 insertions(+), 67 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index 3330be5b9..a1877f8af 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -1,25 +1,98 @@ +using System; using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.KS; namespace kOS.Safe.Compilation.IR { public class IRCodePart { - public IRCodePart(CodePart codePart) + public List MainCode { get; set; } + public List Functions { get; set; } + public List RootBlocks { get; } = new List(); + public List Blocks { get; } = new List(); + + public IRCodePart(CodePart codePart, List userFunctions) { - IRBuilder irBuilder = new IRBuilder(); - InitializationCode = irBuilder.Lower(codePart.InitializationCode); - FunctionsCode = irBuilder.Lower(codePart.FunctionsCode); - MainCode = irBuilder.Lower(codePart.MainCode); + if (codePart.InitializationCode.Count > 0) + throw new ArgumentException($"{nameof(codePart)} has initialization code and is structured unexpectedly."); + if (codePart.FunctionsCode.Count > 0) + throw new ArgumentException($"{nameof(codePart)} has function code and is structured unexpectedly."); + + IRBuilder builder = new IRBuilder(); + MainCode = builder.Lower(codePart.MainCode); + Functions = userFunctions.Select(f => new IRFunction(builder, f)).ToList(); + + Blocks.AddRange(MainCode); + if (MainCode.Count > 0) + RootBlocks.Add(MainCode[0]); + foreach (IRFunction function in Functions) + { + Blocks.AddRange(function.InitializationCode); + if (function.InitializationCode.Count > 0) + RootBlocks.Add(function.InitializationCode[0]); + foreach (IRFunction.IRFunctionFragment fragment in function.Fragments.Values) + { + Blocks.AddRange(fragment.FunctionCode); + if (fragment.FunctionCode.Count > 0) + RootBlocks.Add(fragment.FunctionCode[0]); + } + } } - public List FunctionsCode { get; set; } - public List InitializationCode { get; set; } - public List MainCode { get; set; } - public void EmitToCodePart(CodePart codePart) + public void EmitCode(CodePart codePart) { - codePart.FunctionsCode = IREmitter.Emit(FunctionsCode); - codePart.InitializationCode = IREmitter.Emit(InitializationCode); - codePart.MainCode = IREmitter.Emit(MainCode); + IREmitter emitter = new IREmitter(); + foreach (IRFunction function in Functions) + { + function.EmitCode(emitter); + } + codePart.MainCode = emitter.Emit(MainCode); + } + + public class IRFunction + { + private readonly UserFunction function; + public IRFunction(IRBuilder builder, UserFunction function) + { + this.function = function; + InitializationCode = builder.Lower(function.InitializationCode); + fragments = function.PeekNewCodeFragments().ToList(); + foreach (UserFunctionCodeFragment fragment in fragments) + { + Fragments.Add(fragment, new IRFunctionFragment(builder, fragment)); + } + fragments.Reverse(); + } + + public void EmitCode(IREmitter emitter) + { + function.InitializationCode.Clear(); + function.InitializationCode.AddRange(emitter.Emit(InitializationCode)); + foreach (UserFunctionCodeFragment fragment in fragments) + { + Fragments[fragment].EmitCode(emitter); + } + } + + public List InitializationCode { get; set; } + public Dictionary Fragments { get; } = new Dictionary(); + private readonly List fragments; + public class IRFunctionFragment + { + public List FunctionCode { get; set; } + private readonly UserFunctionCodeFragment fragment; + public IRFunctionFragment(IRBuilder builder, UserFunctionCodeFragment codeFragment) + { + fragment = codeFragment; + FunctionCode = builder.Lower(codeFragment.Code); + } + public void EmitCode(IREmitter emitter) + { + fragment.Code.Clear(); + fragment.Code.AddRange(emitter.Emit(FunctionCode)); + } + } } } } diff --git a/src/kOS.Safe/Compilation/IR/IREmitter.cs b/src/kOS.Safe/Compilation/IR/IREmitter.cs index 985c244a2..3056f99e3 100644 --- a/src/kOS.Safe/Compilation/IR/IREmitter.cs +++ b/src/kOS.Safe/Compilation/IR/IREmitter.cs @@ -3,9 +3,10 @@ namespace kOS.Safe.Compilation.IR { - public static class IREmitter + public class IREmitter { - public static List Emit(List blocks) + int labelIndex = 0; + public List Emit(List blocks) { List result = new List(); Dictionary jumpLabels = new Dictionary(); @@ -13,9 +14,13 @@ public static List Emit(List blocks) foreach (BasicBlock block in blocks) LabelAndEmit(block, jumpLabels, result); // Remove single-line jumps from fallthrough blocks - for (int i = 0; i < result.Count - 1; i++) + for (int i = 0; i < result.Count; i++) { Opcode opcode = result[i]; + if (string.IsNullOrEmpty(opcode.Label) || opcode.Label.StartsWith("@")) + opcode.Label = CreateLabel(i + labelIndex); + if (i >= result.Count - 2) + continue; if (opcode is OpcodeBranchJump jump && jump.DestinationLabel != null && jump.DestinationLabel == result[i + 1].Label) { @@ -29,21 +34,24 @@ public static List Emit(List blocks) // Restore original labels foreach (Opcode opcode in result) { - if (opcode.Label != null && jumpLabels.ContainsKey(opcode.Label)) - opcode.Label = CreateLabel(jumpLabels[opcode.Label]); - if (opcode.DestinationLabel != null && jumpLabels.ContainsKey(opcode.DestinationLabel)) + if (opcode.DestinationLabel != null && + opcode.DestinationLabel.StartsWith("@") && + jumpLabels.ContainsKey(opcode.DestinationLabel)) + { opcode.DestinationLabel = CreateLabel(jumpLabels[opcode.DestinationLabel]); + } } + labelIndex += result.Count; return result; } - private static void LabelAndEmit(BasicBlock block, Dictionary jumpLabels, List result) + private void LabelAndEmit(BasicBlock block, Dictionary jumpLabels, List result) { - jumpLabels.Add(block.Label, result.Count); + jumpLabels.Add(block.Label, result.Count + labelIndex); result.AddRange(block.EmitOpCodes()); } private static string CreateLabel(int index) - => string.Format("@{0:0000}", index); // TODO: + 1 + => string.Format("@{0:0000}", index + 1); } } diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index fc6d71bc1..0b6c0ed2c 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using kOS.Safe.Compilation.IR.Optimization; using kOS.Safe.Function; using kOS.Safe.Utilities; @@ -32,22 +33,12 @@ public static void RegisterMethod(Type type) public List Optimize(IRCodePart codePart) { - List blocks = new List(); - blocks.AddRange(codePart.InitializationCode); - blocks.AddRange(codePart.FunctionsCode); - blocks.AddRange(codePart.MainCode); + List blocks = codePart.Blocks; - ExtendedBasicBlock initializationRoot = codePart.InitializationCode.Count > 0 ? ExtendedBasicBlock.CreateExtendedBlockTree(codePart.InitializationCode[0]) : null; - ExtendedBasicBlock functionsRoot = codePart.FunctionsCode.Count > 0 ? ExtendedBasicBlock.CreateExtendedBlockTree(codePart.FunctionsCode[0]) : null; - ExtendedBasicBlock mainRoot = codePart.MainCode.Count > 0 ? ExtendedBasicBlock.CreateExtendedBlockTree(codePart.MainCode[0]) : null; - - List extendedBlocks = new List(); - if (initializationRoot != null) - extendedBlocks.AddRange(ExtendedBasicBlock.DumpTree(initializationRoot)); - if (functionsRoot != null) - extendedBlocks.AddRange(ExtendedBasicBlock.DumpTree(functionsRoot)); - if (mainRoot != null) - extendedBlocks.AddRange(ExtendedBasicBlock.DumpTree(mainRoot)); + List rootExtendedBlocks = new List( + codePart.RootBlocks.Select(b => ExtendedBasicBlock.CreateExtendedBlockTree(b))); + List extendedBlocks = new List( + rootExtendedBlocks.SelectMany(ExtendedBasicBlock.DumpTree)); foreach (IOptimizationPass pass in optimizationPasses) { @@ -55,33 +46,24 @@ public List Optimize(IRCodePart codePart) continue; SafeHouse.Logger.Log($"Applying optimization pass: {pass.GetType()}."); - try - { - switch (pass) - { - case IHolisticOptimizationPass codePartpass: - codePartpass.ApplyPass(codePart); - break; - case IOptimizationPass blockPass: - blockPass.ApplyPass(blocks); - break; - case IOptimizationPass extendedBlockPass: - extendedBlockPass.ApplyPass(extendedBlocks); - break; - case IOptimizationPass instructionPass: - foreach (BasicBlock block in blocks) - instructionPass.ApplyPass(block.Instructions); - break; - default: - SafeHouse.Logger.LogWarning($"{pass.GetType()}, implementing IOptimizingPass, uses an unsupported generic parameter."); - break; - } - } - catch (Exception e) + switch (pass) { - if (e is Exceptions.KOSCompileException) - throw; - throw new Exceptions.KOSCompileException(new KS.LineCol(0, 0), e.Message); + case IHolisticOptimizationPass codePartpass: + codePartpass.ApplyPass(codePart); + break; + case IOptimizationPass blockPass: + blockPass.ApplyPass(blocks); + break; + case IOptimizationPass extendedBlockPass: + extendedBlockPass.ApplyPass(extendedBlocks); + break; + case IOptimizationPass instructionPass: + foreach (BasicBlock block in blocks) + instructionPass.ApplyPass(block.Instructions); + break; + default: + SafeHouse.Logger.LogWarning($"{pass.GetType()}, implementing IOptimizingPass, uses an unsupported generic parameter."); + break; } } return blocks; diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index 3c9b6a814..d8a78e4d6 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -204,18 +204,18 @@ public CodePart Compile(int startLineNum, ParseTree tree, Context context, Compi CompileProgram(tree); if (options.OptimizationLevel != OptimizationLevel.None) { - Optimize(part, options); + Optimize(part, context, options); } } return part; } - public static void Optimize(CodePart code, CompilerOptions options) + public static void Optimize(CodePart code, Context context, CompilerOptions options) { - IR.IRCodePart irCode = new IR.IRCodePart(code); + IR.IRCodePart irCodePart = new IR.IRCodePart(code, context.UserFunctions.PeekNewFunctions()); IR.IROptimizer optimizer = new IR.IROptimizer(options.OptimizationLevel); - optimizer.Optimize(irCode); - irCode.EmitToCodePart(code); + optimizer.Optimize(irCodePart); + irCodePart.EmitCode(code); } private void CompileProgram(ParseTree tree) diff --git a/src/kOS.Safe/Compilation/KS/UserFunction.cs b/src/kOS.Safe/Compilation/KS/UserFunction.cs index dd397cc5f..8e2a96e80 100644 --- a/src/kOS.Safe/Compilation/KS/UserFunction.cs +++ b/src/kOS.Safe/Compilation/KS/UserFunction.cs @@ -135,6 +135,9 @@ public string GetUserFunctionLabel(int expressionHash) return String.Format("{0}-{1}", Identifier, expressionHash.ToString("x")); } + internal IEnumerable PeekNewCodeFragments() + => newFunctions; + public CodePart GetCodePart() { var mergedPart = new CodePart diff --git a/src/kOS.Safe/Compilation/KS/UserFunctionCollection.cs b/src/kOS.Safe/Compilation/KS/UserFunctionCollection.cs index 23d2446d2..87faf7cc2 100644 --- a/src/kOS.Safe/Compilation/KS/UserFunctionCollection.cs +++ b/src/kOS.Safe/Compilation/KS/UserFunctionCollection.cs @@ -113,6 +113,8 @@ public List GetParts() return GetParts(userFuncs.Values.ToList()); } + internal List PeekNewFunctions() + => newUserFuncs; public IEnumerable GetNewParts() { // new locks or functions From 048d5bb8c1f50a21421cfb74e604c293b7c0cb33 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 16:45:59 -0400 Subject: [PATCH 020/120] Give BasicBlocks unique IDs across the entire compilation, rather than just that part of the code. --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 9163d8610..2e4a681b5 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -7,6 +7,7 @@ namespace kOS.Safe.Compilation.IR public class IRBuilder { private int nextTempId = 0; + private int blockID = 0; public List Lower(List code) { @@ -50,7 +51,7 @@ private void CreateBlocks(List code, Dictionary labels, Lis string label = code[startIndex].Label; if (label.StartsWith("@")) label = null; - BasicBlock block = new BasicBlock(startIndex, endIndex, blocks.Count, label); + BasicBlock block = new BasicBlock(startIndex, endIndex, blockID++, label); blocks.Add(block); } foreach (BasicBlock block in blocks) From 60c4458ac94782625c67a858337be3e246f6f603 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 16:53:07 -0400 Subject: [PATCH 021/120] Add concept of dominance to basic blocks. This will be key for determining variable scoping. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 97 +++++++++++++++++-- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 5 + .../IR/Optimization/ExtendedBasicBlock.cs | 2 +- 3 files changed, 93 insertions(+), 11 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 5bd0abc81..fa79135d4 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -5,18 +5,20 @@ namespace kOS.Safe.Compilation.IR { public class BasicBlock { + private readonly HashSet predecessors = new HashSet(); + private readonly HashSet successors = new HashSet(); + private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. + private readonly string nonSequentialLabel = null; + public int StartIndex { get; } public int EndIndex { get; } public List Instructions { get; } = new List(); - public IEnumerable Sucessors => sucessors; + public IEnumerable Successors => successors; public IEnumerable Predecessors => predecessors; - private readonly HashSet predecessors = new HashSet(); - private readonly HashSet sucessors = new HashSet(); public string Label => nonSequentialLabel ?? $"@BB#{ID}"; - private string nonSequentialLabel = null; public int ID { get; } + public BasicBlock Dominator { get; protected set; } public Optimization.ExtendedBasicBlock ExtendedBlock { get; set; } - private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. public IRJump FallthroughJump { get; set; } = null; #if DEBUG internal Opcode[] OriginalOpcodes { get; set; } @@ -36,18 +38,93 @@ public void Add(IRInstruction instruction) public void AddSuccessor(BasicBlock successor) { - sucessors.Add(successor); + successors.Add(successor); successor.AddPredecessor(this); } protected void AddPredecessor(BasicBlock predecessor) { predecessors.Add(predecessor); } - public void RemoveSuccessor(BasicBlock sucessor) + public void RemoveSuccessor(BasicBlock successor) { - if (!sucessors.Remove(sucessor)) - throw new System.ArgumentException(nameof(sucessor)); - sucessor.predecessors.Remove(this); + if (!successors.Remove(successor)) + throw new System.ArgumentException(nameof(successor)); + successor.predecessors.Remove(this); + if (successor.predecessors.Count == 0) + successor.Dominator = null; + else + successor.Dominator.EstablishDominance(); + } + public void EstablishDominance() + { + // Compute reverse postorder + var postorder = new List(); + var visited = new HashSet(); + DepthFirstSearch(this, visited, postorder); + + postorder.Reverse(); + + // Map block to index + Dictionary index = new Dictionary(); + for (int i = 0; i < postorder.Count; i++) + index[postorder[i]] = i; + + // Initialize + postorder.Remove(this); + bool changed = true; + while (changed) + { + changed = false; + + foreach (BasicBlock block in postorder) + { + // Pick first predecessor with defined dominator + BasicBlock newIdom = block.predecessors.Where(p => p != block).FirstOrDefault + (p => p == this || p.Dominator != null); + + if (newIdom == null) + continue; + + foreach (BasicBlock predecessor in block.predecessors) + { + if (predecessor == newIdom) + continue; + + if (predecessor.Dominator != null) + newIdom = Intersect(predecessor, newIdom, index); + } + + if (block.Dominator != newIdom) + { + block.Dominator = newIdom; + changed = true; + } + } + } + } + private static BasicBlock Intersect(BasicBlock b1, BasicBlock b2, Dictionary index) + { + while (b1 != b2) + { + while (index[b1] > index[b2]) + b1 = b1.Dominator; + + while (index[b2] > index[b1]) + b2 = b2.Dominator; + } + + return b1; + } + private static void DepthFirstSearch(BasicBlock block, HashSet visited, List postorder) + { + if (!visited.Add(block)) + return; + + foreach (BasicBlock successor in block.successors) + DepthFirstSearch(successor, visited, postorder); + + postorder.Add(block); + } } public void SetStackState(Stack stack) diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index a1877f8af..3324945b8 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -38,6 +38,11 @@ public IRCodePart(CodePart codePart, List userFunctions) RootBlocks.Add(fragment.FunctionCode[0]); } } + + foreach (BasicBlock block in RootBlocks) + { + block.EstablishDominance(); + } } public void EmitCode(CodePart codePart) diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs b/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs index 59a9b18e6..d15e1dc29 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs @@ -19,7 +19,7 @@ private void AddBlock(BasicBlock block) { Blocks.Add(block); block.ExtendedBlock = this; - foreach (BasicBlock successor in block.Sucessors) + foreach (BasicBlock successor in block.Successors) { if (!successor.Predecessors.Skip(1).Any()) AddBlock(successor); From cb8e5c698cd7ed0e493210cf3cb18cbac2c58987 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 16:55:05 -0400 Subject: [PATCH 022/120] Introduce IRParameter as a special type of IRValue, for instances where something would be popped from the stack, but a BasicBlock's stack is empty at that point. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 23 ++++++++ src/kOS.Safe/Compilation/IR/IRBuilder.cs | 67 ++++++++++++----------- src/kOS.Safe/Compilation/IR/IRValue.cs | 4 ++ 3 files changed, 63 insertions(+), 31 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index fa79135d4..e45e32b7d 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -7,6 +7,9 @@ public class BasicBlock { private readonly HashSet predecessors = new HashSet(); private readonly HashSet successors = new HashSet(); + private readonly List parameters = new List(); + private readonly Dictionary variables = new Dictionary(); + private readonly Dictionary externalGlobalVariables = new Dictionary(); private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. private readonly string nonSequentialLabel = null; @@ -125,6 +128,26 @@ private static void DepthFirstSearch(BasicBlock block, HashSet visit postorder.Add(block); } + + public void AddParameter(IRParameter parameter) + { + parameters.Add(parameter); + } + public void StoreVariable(IRVariable variable) + { + variables[variable.Name] = variable; + } + public IRVariable PushVariable(string name, Opcode opcode) + { + if (variables.ContainsKey(name)) + return variables[name]; + if (Dominator != null) + return Dominator.PushVariable(name, opcode); + if (externalGlobalVariables.ContainsKey(name)) + return externalGlobalVariables[name]; + IRVariable newExternalGlobal = new IRVariable(name, opcode); + externalGlobalVariables.Add(name, newExternalGlobal); + return newExternalGlobal; } public void SetStackState(Stack stack) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 2e4a681b5..e1d1b010e 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -100,68 +100,73 @@ private IRTemp CreateTemp() nextTempId++; return result; } + private static IRValue PopFromStack(Stack stack, BasicBlock block) + { + if (stack.Count > 0) + return stack.Pop(); + IRParameter parameter = new IRParameter(); + block.AddParameter(parameter); + return parameter; + } private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack stack, Dictionary labels, int index, List blocks) { + IRValue PopStack() => PopFromStack(stack, currentBlock); HashSet variables = new HashSet(); switch (opcode) { case OpcodeStore store: - IRValue storedValue = stack.Count > 0 ? stack.Pop() : null; - IRAssign assignment = new IRAssign(store, storedValue) { Scope = IRAssign.StoreScope.Ambivalent }; + IRAssign assignment = new IRAssign(store, PopStack()) { Scope = IRAssign.StoreScope.Ambivalent }; variables.Add(assignment.Target); currentBlock.Add(assignment); break; case OpcodeStoreExist storeExist: - storedValue = stack.Count > 0 ? stack.Pop() : null; - assignment = new IRAssign(storeExist, storedValue) { AssertExists = true }; + assignment = new IRAssign(storeExist, PopStack()) { AssertExists = true }; variables.Add(assignment.Target); currentBlock.Add(assignment); break; case OpcodeStoreLocal storeLocal: - storedValue = stack.Count > 0 ? stack.Pop() : null; - assignment = new IRAssign(storeLocal, storedValue) { Scope = IRAssign.StoreScope.Local }; + assignment = new IRAssign(storeLocal, PopStack()) { Scope = IRAssign.StoreScope.Local }; currentBlock.Add(assignment); variables.Add(assignment.Target); break; case OpcodeStoreGlobal storeGlobal: - storedValue = stack.Count > 0 ? stack.Pop() : null; - assignment = new IRAssign(storeGlobal, storedValue) { Scope = IRAssign.StoreScope.Global }; + assignment = new IRAssign(storeGlobal, PopStack()) { Scope = IRAssign.StoreScope.Global }; currentBlock.Add(assignment); variables.Add(assignment.Target); break; case OpcodeExists exists: IRTemp temp = CreateTemp(); - IRInstruction instruction = new IRUnaryOp(temp, exists, stack.Pop()); + IRInstruction instruction = new IRUnaryOp(temp, exists, PopStack()); temp.Parent = instruction; //currentBlock.Add(instruction); stack.Push(temp); break; case OpcodeUnset unset: - currentBlock.Add(new IRUnaryConsumer(unset, stack.Pop(), true)); + currentBlock.Add(new IRUnaryConsumer(unset, PopStack(), true)); break; case OpcodeGetMethod getMethod: temp = CreateTemp(); - instruction = new IRSuffixGetMethod(temp, stack.Pop(), getMethod); + instruction = new IRSuffixGetMethod(temp, PopStack(), getMethod); //currentBlock.Add(instruction); temp.Parent = instruction; stack.Push(temp); break; case OpcodeGetMember getMember: temp = CreateTemp(); - instruction = new IRSuffixGet(temp, stack.Pop(), getMember); + instruction = new IRSuffixGet(temp, PopStack(), getMember); temp.Parent = instruction; //currentBlock.Add(instruction); stack.Push(temp); break; case OpcodeSetMember setMember: - IRValue value = stack.Pop(); - IRValue memberObj = stack.Pop(); + IRValue value = PopStack(); + IRValue memberObj = PopStack(); currentBlock.Add(new IRSuffixSet(memberObj, value, setMember)); break; case OpcodeGetIndex getIndex: - IRValue targetIndex = stack.Pop(); - IRValue indexObj = stack.Pop(); + IRValue targetIndex = PopStack(); + IRValue indexObj = PopStack(); temp = CreateTemp(); instruction = new IRIndexGet(temp, indexObj, targetIndex, getIndex); //currentBlock.Add(instruction); @@ -169,9 +174,9 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack 0; // Not even an argument marker on the stack - the dominator block must have it while (stack.Count > 0) { - IRValue stackResult = stack.Pop(); + IRValue stackResult = PopStack(); if (stackResult is IRConstant constant && constant.Value is Execution.KOSArgMarkerType) break; arguments.Push(stackResult); @@ -255,18 +260,18 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack 0 && !((IRCall)instruction).Direct) { - ((IRCall)instruction).IndirectMethod = stack.Pop(); + ((IRCall)instruction).IndirectMethod = PopStack(); } temp.Parent = instruction; stack.Push(temp); break; case OpcodeReturn opcodeReturn: - currentBlock.Add(new IRReturn(opcodeReturn.Depth, opcodeReturn) { Value = stack.Pop() }); + currentBlock.Add(new IRReturn(opcodeReturn.Depth, opcodeReturn) { Value = PopStack() }); break; case OpcodePush opcodePush: object argument = opcodePush.Argument; if (argument is string identifier && identifier.StartsWith("$")) - stack.Push(new IRVariable(identifier, opcodePush, false)); + stack.Push(currentBlock.PushVariable(identifier, opcodePush)); else stack.Push(new IRConstant(argument, opcodePush)); break; @@ -278,13 +283,13 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack EmitPush() yield return opcode; } } + public class IRParameter : IRValue + { + internal override IEnumerable EmitPush() => System.Linq.Enumerable.Empty(); + } } From 294900fc158c4f3028e8ecd9ffb8f67e098e618d Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 22:02:11 -0400 Subject: [PATCH 023/120] Add Dead Code Elimination pass. This pass eliminates any basic blocks that are no longer reachable. --- src/kOS.Safe/Compilation/IR/IROptimizer.cs | 40 ++++++++++++++----- .../IR/Optimization/DeadCodeElimination.cs | 32 +++++++++++++++ .../IR/Optimization/IOptimizationPass.cs | 5 +++ src/kOS.Safe/Compilation/OptimizationLevel.cs | 4 +- 4 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index 0b6c0ed2c..50998ed95 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -10,34 +10,49 @@ namespace kOS.Safe.Compilation.IR [AssemblyWalk(InterfaceType = typeof(IOptimizationPass), StaticRegisterMethod = "RegisterMethod")] public class IROptimizer { + public OptimizationLevel OptimizationLevel { get; } + public List Blocks { get; private set; } + public List ExtendedBlocks { get; private set; } + public HashSet RootBlocks { get; private set; } + public IRCodePart Code { get; private set; } internal static InterimCPU InterimCPU { get; } = new InterimCPU(); - private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = InterimCPU }; public static IFunctionManager FunctionManager => shared.FunctionManager; - private static readonly SortedSet optimizationPasses = new SortedSet( + private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = InterimCPU }; + private readonly SortedSet optimizationPasses = new SortedSet( Comparer.Create((a, b) => a.SortIndex.CompareTo(b.SortIndex))); - public OptimizationLevel OptimizationLevel { get; } + private readonly static HashSet availablePassTypes = new HashSet(); static IROptimizer() { shared.FunctionManager = new FunctionManager(shared); } + public IROptimizer(OptimizationLevel optimizationLevel) { OptimizationLevel = optimizationLevel; + foreach (Type type in availablePassTypes) + { + IOptimizationPass pass = (IOptimizationPass)Activator.CreateInstance(type); + optimizationPasses.Add(pass); + if (pass is ILinkedOptimizationPass linkedPass) + linkedPass.Optimizer = this; + } } public static void RegisterMethod(Type type) { - optimizationPasses.Add((IOptimizationPass)Activator.CreateInstance(type)); + availablePassTypes.Add(type); } public List Optimize(IRCodePart codePart) { - List blocks = codePart.Blocks; + Code = codePart; + Blocks = codePart.Blocks; + RootBlocks = new HashSet(codePart.RootBlocks); List rootExtendedBlocks = new List( codePart.RootBlocks.Select(b => ExtendedBasicBlock.CreateExtendedBlockTree(b))); - List extendedBlocks = new List( + ExtendedBlocks = new List( rootExtendedBlocks.SelectMany(ExtendedBasicBlock.DumpTree)); foreach (IOptimizationPass pass in optimizationPasses) @@ -48,17 +63,20 @@ public List Optimize(IRCodePart codePart) SafeHouse.Logger.Log($"Applying optimization pass: {pass.GetType()}."); switch (pass) { + case ILinkedOptimizationPass linkedPass: + linkedPass.ApplyPass(); + break; case IHolisticOptimizationPass codePartpass: - codePartpass.ApplyPass(codePart); + codePartpass.ApplyPass(Code); break; case IOptimizationPass blockPass: - blockPass.ApplyPass(blocks); + blockPass.ApplyPass(Blocks); break; case IOptimizationPass extendedBlockPass: - extendedBlockPass.ApplyPass(extendedBlocks); + extendedBlockPass.ApplyPass(ExtendedBlocks); break; case IOptimizationPass instructionPass: - foreach (BasicBlock block in blocks) + foreach (BasicBlock block in Blocks) instructionPass.ApplyPass(block.Instructions); break; default: @@ -66,7 +84,7 @@ public List Optimize(IRCodePart codePart) break; } } - return blocks; + return Blocks; } } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs b/src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs new file mode 100644 index 000000000..7d4e028d0 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR.Optimization +{ + internal class DeadCodeElimination : ILinkedOptimizationPass + { + public IROptimizer Optimizer { set => optimizer = value; } + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + public short SortIndex => 3; + + private IROptimizer optimizer; + + public void ApplyPass() + { + List blocksToRemove = new List(); + foreach (BasicBlock block in optimizer.Blocks) + { + if (block.Predecessors.Any() || optimizer.RootBlocks.Contains(block)) + continue; + blocksToRemove.Add(block); + } + foreach (BasicBlock block in blocksToRemove) + { + optimizer.Blocks.Remove(block); + foreach (BasicBlock successor in block.Successors.ToArray()) + block.RemoveSuccessor(successor); + } + } + } +} diff --git a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs b/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs index 8c746c845..90edaf715 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs @@ -16,4 +16,9 @@ public interface IHolisticOptimizationPass : IOptimizationPass { void ApplyPass(IRCodePart codePart); } + public interface ILinkedOptimizationPass : IOptimizationPass + { + IROptimizer Optimizer { set; } + void ApplyPass(); + } } diff --git a/src/kOS.Safe/Compilation/OptimizationLevel.cs b/src/kOS.Safe/Compilation/OptimizationLevel.cs index 920f4e426..accae1ee7 100644 --- a/src/kOS.Safe/Compilation/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/OptimizationLevel.cs @@ -5,12 +5,12 @@ namespace kOS.Safe.Compilation * Replace CONSTANT: values with the constant * Replace ship fields with their alias * Constant folding + * Dead code elimination * * Constant propagation * Replace lex indexing with string constant with suffixing where possible - * Dead code elimination (for the never used case) * O2: - * Redundant expression elimination + * Common expression elimination * Particularly: Any expression of 3 opcodes used more than twice, * or any expression of >3 opcodes used more than once * Replace parameterless suffix method calls with get member (this feels like cheating...) From f727550ccbc358404da47525059fe302cd030dbe Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 9 Mar 2026 17:35:12 -0400 Subject: [PATCH 024/120] Adjust file structure and namespaces to be cleaner. Adjust numbering of pass sort indices to give more space for expansion. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 3 ++- src/kOS.Safe/Compilation/KS/Compiler.cs | 2 +- .../{IR => }/Optimization/ExtendedBasicBlock.cs | 3 ++- .../{IR => }/Optimization/IOptimizationPass.cs | 6 +++--- .../{IR => }/Optimization/InterimCPU.cs | 2 +- .../{ => Optimization}/OptimizationLevel.cs | 0 .../IROptimizer.cs => Optimization/Optimizer.cs} | 10 +++++----- .../Passes}/ConstantFolding.cs | 16 +++++++++------- .../Passes}/DeadCodeElimination.cs | 10 +++++----- .../Passes}/SuffixReplacement.cs | 11 ++++++----- 10 files changed, 34 insertions(+), 29 deletions(-) rename src/kOS.Safe/Compilation/{IR => }/Optimization/ExtendedBasicBlock.cs (96%) rename src/kOS.Safe/Compilation/{IR => }/Optimization/IOptimizationPass.cs (82%) rename src/kOS.Safe/Compilation/{IR => }/Optimization/InterimCPU.cs (99%) rename src/kOS.Safe/Compilation/{ => Optimization}/OptimizationLevel.cs (100%) rename src/kOS.Safe/Compilation/{IR/IROptimizer.cs => Optimization/Optimizer.cs} (94%) rename src/kOS.Safe/Compilation/{IR/Optimization => Optimization/Passes}/ConstantFolding.cs (95%) rename src/kOS.Safe/Compilation/{IR/Optimization => Optimization/Passes}/DeadCodeElimination.cs (79%) rename src/kOS.Safe/Compilation/{IR/Optimization => Optimization/Passes}/SuffixReplacement.cs (94%) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index e45e32b7d..503305570 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using kOS.Safe.Compilation.Optimization; namespace kOS.Safe.Compilation.IR { @@ -21,7 +22,7 @@ public class BasicBlock public string Label => nonSequentialLabel ?? $"@BB#{ID}"; public int ID { get; } public BasicBlock Dominator { get; protected set; } - public Optimization.ExtendedBasicBlock ExtendedBlock { get; set; } + public ExtendedBasicBlock ExtendedBlock { get; set; } public IRJump FallthroughJump { get; set; } = null; #if DEBUG internal Opcode[] OriginalOpcodes { get; set; } diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index d8a78e4d6..0df55690b 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -213,7 +213,7 @@ public CodePart Compile(int startLineNum, ParseTree tree, Context context, Compi public static void Optimize(CodePart code, Context context, CompilerOptions options) { IR.IRCodePart irCodePart = new IR.IRCodePart(code, context.UserFunctions.PeekNewFunctions()); - IR.IROptimizer optimizer = new IR.IROptimizer(options.OptimizationLevel); + Optimization.Optimizer optimizer = new Optimization.Optimizer(options.OptimizationLevel); optimizer.Optimize(irCodePart); irCodePart.EmitCode(code); } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs b/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs similarity index 96% rename from src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs rename to src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs index d15e1dc29..53dbad6a9 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs +++ b/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; using System.Linq; +using kOS.Safe.Compilation.IR; -namespace kOS.Safe.Compilation.IR.Optimization +namespace kOS.Safe.Compilation.Optimization { public class ExtendedBasicBlock { diff --git a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs b/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs similarity index 82% rename from src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs rename to src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs index 90edaf715..c503f8f51 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs +++ b/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs @@ -1,7 +1,7 @@ -using System; using System.Collections.Generic; +using kOS.Safe.Compilation.IR; -namespace kOS.Safe.Compilation.IR.Optimization +namespace kOS.Safe.Compilation.Optimization { public interface IOptimizationPass { @@ -18,7 +18,7 @@ public interface IHolisticOptimizationPass : IOptimizationPass } public interface ILinkedOptimizationPass : IOptimizationPass { - IROptimizer Optimizer { set; } + Optimizer Optimizer { set; } void ApplyPass(); } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs b/src/kOS.Safe/Compilation/Optimization/InterimCPU.cs similarity index 99% rename from src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs rename to src/kOS.Safe/Compilation/Optimization/InterimCPU.cs index 68452d427..b54c6b4a4 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs +++ b/src/kOS.Safe/Compilation/Optimization/InterimCPU.cs @@ -3,7 +3,7 @@ using kOS.Safe.Encapsulation; using kOS.Safe.Execution; -namespace kOS.Safe.Compilation.IR.Optimization +namespace kOS.Safe.Compilation.Optimization { internal class InterimCPU : ICpu { diff --git a/src/kOS.Safe/Compilation/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs similarity index 100% rename from src/kOS.Safe/Compilation/OptimizationLevel.cs rename to src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs similarity index 94% rename from src/kOS.Safe/Compilation/IR/IROptimizer.cs rename to src/kOS.Safe/Compilation/Optimization/Optimizer.cs index 50998ed95..6345b5e5b 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs @@ -1,14 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; -using kOS.Safe.Compilation.IR.Optimization; +using kOS.Safe.Compilation.IR; using kOS.Safe.Function; using kOS.Safe.Utilities; -namespace kOS.Safe.Compilation.IR +namespace kOS.Safe.Compilation.Optimization { [AssemblyWalk(InterfaceType = typeof(IOptimizationPass), StaticRegisterMethod = "RegisterMethod")] - public class IROptimizer + public class Optimizer { public OptimizationLevel OptimizationLevel { get; } public List Blocks { get; private set; } @@ -23,12 +23,12 @@ public class IROptimizer Comparer.Create((a, b) => a.SortIndex.CompareTo(b.SortIndex))); private readonly static HashSet availablePassTypes = new HashSet(); - static IROptimizer() + static Optimizer() { shared.FunctionManager = new FunctionManager(shared); } - public IROptimizer(OptimizationLevel optimizationLevel) + public Optimizer(OptimizationLevel optimizationLevel) { OptimizationLevel = optimizationLevel; foreach (Type type in availablePassTypes) diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs similarity index 95% rename from src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs rename to src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index 33d358033..1a00cec00 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -1,15 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; +using kOS.Safe.Compilation.IR; using kOS.Safe.Exceptions; -using kOS.Safe.Function; -namespace kOS.Safe.Compilation.IR.Optimization +namespace kOS.Safe.Compilation.Optimization.Passes { public class ConstantFolding : IOptimizationPass { public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; - public short SortIndex => 1; + public short SortIndex => 30; public void ApplyPass(List blocks) { @@ -47,7 +47,9 @@ private static void ApplyPass(BasicBlock block) indexSet.Index = AttemptReductionToConstant(indexSet.Index); break; case IRBranch branch: - if (AttemptReductionToConstant(branch.Condition) is IRConstant branchConstant) + if (branch.True == branch.False) + block.Instructions[i] = new IRJump(branch.True, branch.SourceLine, branch.SourceColumn); + else if (AttemptReductionToConstant(branch.Condition) is IRConstant branchConstant) { BasicBlock permanentBlock, deprecatedBlock; (permanentBlock, deprecatedBlock) = Convert.ToBoolean(branchConstant.Value) ? (branch.True, branch.False) : (branch.False, branch.True); @@ -277,7 +279,7 @@ private static IRValue ReduceCall(IRCall instruction) instruction.Arguments[i] = AttemptReduction(temp.Parent); } string functionName = instruction.Function.Replace("()", ""); - if (IROptimizer.FunctionManager.Exists(functionName) && instruction.Arguments.All(arg => arg is IRConstant)) + if (Optimizer.FunctionManager.Exists(functionName) && instruction.Arguments.All(arg => arg is IRConstant)) { try { @@ -301,12 +303,12 @@ private static IRValue ReduceCall(IRCall instruction) case "arctan": case "arctan2": case "anglediff": - InterimCPU interimCPU = IROptimizer.InterimCPU; + InterimCPU interimCPU = Optimizer.InterimCPU; interimCPU.Boot(); // Clear the stack out of caution. interimCPU.PushArgumentStack(new Execution.KOSArgMarkerType()); foreach (IRValue arg in instruction.Arguments) interimCPU.PushArgumentStack(((IRConstant)arg).Value); - IROptimizer.FunctionManager.CallFunction(functionName); + Optimizer.FunctionManager.CallFunction(functionName); instruction.Result = new IRConstant(interimCPU.PopValueArgument(), instruction); return instruction.Result; } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs b/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs similarity index 79% rename from src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs rename to src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs index 7d4e028d0..f75cd8bb1 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs @@ -1,16 +1,16 @@ -using System; using System.Collections.Generic; using System.Linq; +using kOS.Safe.Compilation.IR; -namespace kOS.Safe.Compilation.IR.Optimization +namespace kOS.Safe.Compilation.Optimization.Passes { internal class DeadCodeElimination : ILinkedOptimizationPass { - public IROptimizer Optimizer { set => optimizer = value; } + public Optimizer Optimizer { set => optimizer = value; } public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; - public short SortIndex => 3; + public short SortIndex => 50; - private IROptimizer optimizer; + private Optimizer optimizer; public void ApplyPass() { diff --git a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs similarity index 94% rename from src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs rename to src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs index f8fe76b62..5922a972b 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using kOS.Safe.Compilation.IR; -namespace kOS.Safe.Compilation.IR.Optimization +namespace kOS.Safe.Compilation.Optimization.Passes { public class SuffixReplacement : IOptimizationPass { public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; - public short SortIndex => 0; + public short SortIndex => 10; public void ApplyPass(List code) { @@ -116,16 +117,16 @@ private static IRValue AttempReplaceSuffix(IRSuffixGet suffixGet) } private static IRConstant ReplaceConstantSuffix(IRSuffixGet suffixGet) { - IROptimizer.InterimCPU.PushArgumentStack(new Encapsulation.ConstantValue()); + Optimizer.InterimCPU.PushArgumentStack(new Encapsulation.ConstantValue()); try { - new OpcodeGetMember(suffixGet.Suffix).Execute(IROptimizer.InterimCPU); + new OpcodeGetMember(suffixGet.Suffix).Execute(Optimizer.InterimCPU); } catch (Exception e) { throw new Exceptions.KOSCompileException(suffixGet, e); } - IRConstant result = new IRConstant(IROptimizer.InterimCPU.PopValueArgument(), suffixGet); + IRConstant result = new IRConstant(Optimizer.InterimCPU.PopValueArgument(), suffixGet); suffixGet.Result = result; return result; } From 4d59836fa0d48305dbb65f286ee2bec0f515b43b Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 9 Mar 2026 19:40:19 -0400 Subject: [PATCH 025/120] Outline planned sorting of optimization passes. --- .../Optimization/OptimizationLevel.cs | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index accae1ee7..8f7210e7b 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -1,36 +1,43 @@ +#pragma warning disable IDE0130 // Namespace does not match folder structure namespace kOS.Safe.Compilation +#pragma warning restore IDE0130 // Namespace does not match folder structure { /* * O1: - * Replace CONSTANT: values with the constant - * Replace ship fields with their alias - * Constant folding - * Dead code elimination + * 10. Suffix replacement: + * Replace CONSTANT: values with the constant + * Replace ship fields with their alias + * 30. Constant folding + * 50. Dead code elimination * - * Constant propagation - * Replace lex indexing with string constant with suffixing where possible + * 20. Constant propagation + * 1050. Peephole optimizations: + * Replace lex indexing with string constant with suffixing where possible + * Replace parameterless suffix method calls with get member (this feels like cheating...) + * Replace calls to VectorDotProduct with multiplication + * Replace --X with X + * Replace !!X with X + * Branch logical simplification (e.g. !A branch = A branch!) + * Algebraic simplification (e.g. A*B+A*C = A*(B+C), A+-B=A-B, A--B=A+B, -A+B=B-A, X*X*...*X=N^X) * O2: - * Common expression elimination - * Particularly: Any expression of 3 opcodes used more than twice, - * or any expression of >3 opcodes used more than once - * Replace parameterless suffix method calls with get member (this feels like cheating...) - * Boolean logical simplification - * Code motion - moving expressions outside loops if they do not depend on loop variables - * Replace X * X * ... * X with X^N - * Carry's - moving the N-1 D lookup of >1D lists outside the innermost loop + * 1000. Common expression elimination + * Particularly: Any expression of 3 opcodes used more than twice, + * or any expression of >3 opcodes used more than once + * 2100. Code motion - moving expressions outside loops if they do not depend on loop variables + * 2150. Carry's - moving the N-1 D lookup of >1D lists outside the innermost loop * Loop jamming? (Combining adjacent loops into one) * Unswitching (moving conditional evaluation outside the loop) - low priority * Linear function test replacement - low priority * O3: - * Local function inlining - * Constant propagation to local functions - * Loop stack manipulation (delayed setting of either index or aggregator) + * 3100. Local function inlining + * 3200. Constant propagation to local functions + * 3500. Loop stack manipulation (delayed setting of either index or aggregator) * O4: - * Constant loop unrolling + * 4000. Constant loop unrolling */ public enum OptimizationLevel : int { - None = 0, // No changes whatsoever + None = 0, // No changes whatsoever Minimal = 1, // Only changes that trim opcodes without changing any flow Balanced = 2, // Only changes that trim opcodes without increasing file size Aggressive = 3, // Favor trimming opcodes over file size. From 91f179a11919546e1fbe7ed0bbdb12c957cfd09b Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 9 Mar 2026 19:43:37 -0400 Subject: [PATCH 026/120] Clean up code for DCE and downgrade it to IHolisticOptimizationPass. --- .../Passes/DeadCodeElimination.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs b/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs index f75cd8bb1..02fbe38f8 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs @@ -4,26 +4,39 @@ namespace kOS.Safe.Compilation.Optimization.Passes { - internal class DeadCodeElimination : ILinkedOptimizationPass + internal class DeadCodeElimination : IHolisticOptimizationPass { - public Optimizer Optimizer { set => optimizer = value; } public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; public short SortIndex => 50; - private Optimizer optimizer; + public void ApplyPass(IRCodePart code) + { + HashSet rootBlocks = new HashSet(code.RootBlocks); + + RemoveDeadBlocks(code.Blocks, rootBlocks); + + foreach (IRCodePart.IRFunction function in code.Functions) + { + RemoveDeadBlocks(function.InitializationCode, rootBlocks); + foreach (IRCodePart.IRFunction.IRFunctionFragment fragment in function.Fragments.Values) + { + RemoveDeadBlocks(fragment.FunctionCode, rootBlocks); + } + } + } - public void ApplyPass() + private static void RemoveDeadBlocks(List blocks, HashSet rootBlocks) { List blocksToRemove = new List(); - foreach (BasicBlock block in optimizer.Blocks) + foreach (BasicBlock block in blocks) { - if (block.Predecessors.Any() || optimizer.RootBlocks.Contains(block)) + if (block.Predecessors.Any() || rootBlocks.Contains(block)) continue; blocksToRemove.Add(block); } foreach (BasicBlock block in blocksToRemove) { - optimizer.Blocks.Remove(block); + blocks.Remove(block); foreach (BasicBlock successor in block.Successors.ToArray()) block.RemoveSuccessor(successor); } From b0b85b9c57394c2111065783c82409b344adee28 Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 9 Mar 2026 23:16:04 -0400 Subject: [PATCH 027/120] Add methods for storing variable at local, global, or ambiguous scope. Note that scopes are not fully implemented yet. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 27 ++++++++++++++++++++++- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 10 ++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 503305570..c40ea2d3f 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -134,10 +134,35 @@ public void AddParameter(IRParameter parameter) { parameters.Add(parameter); } - public void StoreVariable(IRVariable variable) + public void StoreLocalVariable(IRVariable variable) { variables[variable.Name] = variable; } + public void StoreGlobalVariable(IRVariable variable) + { + if (Dominator != null) + { + Dominator.StoreGlobalVariable(variable); + return; + } + externalGlobalVariables[variable.Name] = variable; + } + public void StoreVariable(IRVariable variable) + { + if (!TryStoreVariable(variable)) + StoreGlobalVariable(variable); + } + public bool TryStoreVariable(IRVariable variable) + { + if (variables.ContainsKey(variable.Name)) + { + variables[variable.Name] = variable; + return true; + } + if (Dominator != null) + return Dominator.TryStoreVariable(variable); + return false; + } public IRVariable PushVariable(string name, Opcode opcode) { if (variables.ContainsKey(name)) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index e1d1b010e..3adf5e491 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -112,28 +112,28 @@ private static IRValue PopFromStack(Stack stack, BasicBlock block) private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack stack, Dictionary labels, int index, List blocks) { IRValue PopStack() => PopFromStack(stack, currentBlock); - HashSet variables = new HashSet(); switch (opcode) { case OpcodeStore store: IRAssign assignment = new IRAssign(store, PopStack()) { Scope = IRAssign.StoreScope.Ambivalent }; - variables.Add(assignment.Target); + currentBlock.StoreVariable(new IRVariable(assignment.Target, assignment)); currentBlock.Add(assignment); break; case OpcodeStoreExist storeExist: assignment = new IRAssign(storeExist, PopStack()) { AssertExists = true }; - variables.Add(assignment.Target); + if (!currentBlock.TryStoreVariable(new IRVariable(assignment.Target, assignment))) + throw new Exceptions.KOSCompileException(new KS.LineCol(assignment.SourceLine, assignment.SourceColumn), "Assert that variable exists failed."); currentBlock.Add(assignment); break; case OpcodeStoreLocal storeLocal: assignment = new IRAssign(storeLocal, PopStack()) { Scope = IRAssign.StoreScope.Local }; + currentBlock.StoreLocalVariable(new IRVariable(assignment.Target, assignment)); currentBlock.Add(assignment); - variables.Add(assignment.Target); break; case OpcodeStoreGlobal storeGlobal: assignment = new IRAssign(storeGlobal, PopStack()) { Scope = IRAssign.StoreScope.Global }; + currentBlock.StoreGlobalVariable(new IRVariable(assignment.Target, assignment)); currentBlock.Add(assignment); - variables.Add(assignment.Target); break; case OpcodeExists exists: IRTemp temp = CreateTemp(); From 80e6cc35dc7d2885e1b5770ebd9de04de289319c Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 9 Mar 2026 23:35:54 -0400 Subject: [PATCH 028/120] Add X/X=1 simplification to the constant folding pass. This will say that 0/0=1 or 0/X=0 for all X (including 0) instead of throwing an exception. But that's already undefined behaviour so this should be acceptable. --- .../Optimization/Passes/ConstantFolding.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index 1a00cec00..45144d835 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -192,6 +192,8 @@ temp.Parent is IRBinaryOp leftOp && return newResult; break; case OpcodeMathDivide _: + if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + throw new KOSCompileException(instruction, new DivideByZeroException()); if (ReduceDivMult(instruction, constantR, out newResult)) return newResult; break; @@ -211,13 +213,26 @@ temp.Parent is IRBinaryOp leftOp && break; } } - else if (!instruction.IsCommutative && instruction.Left is IRConstant constantL) + else { switch (instruction.Operation) { case OpcodeMathDivide _: - if (Encapsulation.ScalarIntValue.Zero.Equals(constantL)) + // 0 / X = 0 + // Technically not true when X = 0 + // But that would otherwise throw a "Tried to push infinite on to the stack" error + // So this is an acceptable assumption that improves performance and eliminates an error. + // TODO: Add an "EXIT" (EOP) command to the language because this will break the + // PRINT(1/0) shortcut to cause a program to terminate. + if (instruction.Left is IRConstant constantL && + Encapsulation.ScalarIntValue.Zero.Equals(constantL.Value)) return constantL; + // X / X = 1 + // Technically not true when X = 0 + // But that would otherwise throw a "Tried to push infinite on to the stack" error + // So this is an acceptable assumption that improves performance and eliminates an error. + if (instruction.Left == instruction.Right) + return new IRConstant(Encapsulation.ScalarIntValue.One, instruction); break; } } From 0afa7aa6e0459b136c17396733488a0073e644e1 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 16:38:36 -0400 Subject: [PATCH 029/120] Add set access to IRInstruction interfaces, as well as indexing for IMultipleOperandInstructions. --- .../Compilation/IR/IOperandInstructionBase.cs | 4 +- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 80 +++++++++++++++++-- 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs index bb87b07c2..ed1aa7251 100644 --- a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs +++ b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs @@ -6,10 +6,12 @@ public interface IOperandInstructionBase { } public interface ISingleOperandInstruction : IOperandInstructionBase { - IRValue Operand { get; } + IRValue Operand { get; set; } } public interface IMultipleOperandInstruction : IOperandInstructionBase { IEnumerable Operands { get; } + IRValue this[int index] { get; set; } + int OperandCount { get; } } } diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index be7281c74..d913be6c9 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -59,7 +59,7 @@ public enum StoreScope public IRValue Value { get; set; } public StoreScope Scope { get; set; } = StoreScope.Ambivalent; public bool AssertExists { get; set; } = false; - IRValue ISingleOperandInstruction.Operand => Value; + IRValue ISingleOperandInstruction.Operand { get => Value; set => Value = value; } public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : base(opcode) { @@ -97,10 +97,24 @@ public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperand { public override bool SideEffects => false; public IRValue Result { get; set; } - public BinaryOpcode Operation { get; protected set; } + public BinaryOpcode Operation { get; set; } public IRValue Left { get; set; } public IRValue Right { get; set; } public IEnumerable Operands { get { yield return Left; yield return Right; } } + public int OperandCount => 2; + IRValue IMultipleOperandInstruction.this[int index] + { + get => index == 0 ? Left : index == 1 ? Right : throw new System.ArgumentOutOfRangeException(); + set + { + if (index == 0) + Left = value; + else if (index == 1) + Right = value; + else + throw new System.ArgumentOutOfRangeException(); + } + } public bool IsCommutative { get; } public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) : base(operation) @@ -192,7 +206,7 @@ public class IRUnaryConsumer : IRInstruction, ISingleOperandInstruction { public override bool SideEffects { get; } public Opcode Operation { get; } - public IRValue Operand { get; } + public IRValue Operand { get; set; } public IRUnaryConsumer(Opcode opcode, IRValue operand, bool sideEffects = false) : base(opcode) { Operation = opcode; @@ -213,7 +227,7 @@ public class IRPop : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; public IRValue Value { get; set; } - IRValue ISingleOperandInstruction.Operand => Value; + IRValue ISingleOperandInstruction.Operand { get => Value; set => Value = value; } public IRPop(IRValue value, OpcodePop opcode) : base(opcode) => Value = value; @@ -253,7 +267,7 @@ public class IRSuffixGet : IRInteractsInstruction, IResultingInstruction, ISingl public IRValue Result { get; set; } public IRValue Object { get; set; } public string Suffix { get; set; } - IRValue ISingleOperandInstruction.Operand => Object; + IRValue ISingleOperandInstruction.Operand { get => Object; set => Object = value; } public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcodeGetMember) : base(obj, opcodeGetMember) { Result = result; @@ -288,6 +302,20 @@ public class IRSuffixSet : IRInteractsInstruction, IMultipleOperandInstruction public IRValue Object { get; set; } public IRValue Value { get; set; } public IEnumerable Operands { get { yield return Object; yield return Value; } } + public int OperandCount => 2; + IRValue IMultipleOperandInstruction.this[int index] + { + get => index == 0 ? Object : index == 1 ? Value : throw new System.ArgumentOutOfRangeException(); + set + { + if (index == 0) + Object = value; + else if (index == 1) + Value = value; + else + throw new System.ArgumentOutOfRangeException(); + } + } public string Suffix { get; } public IRSuffixSet(IRValue obj, IRValue value, OpcodeSetMember opcodeSetMember) : base(obj, opcodeSetMember) { @@ -313,6 +341,20 @@ public class IRIndexGet : IRInstruction, IResultingInstruction, IMultipleOperand public IRValue Object { get; set; } public IRValue Index { get; set; } public IEnumerable Operands { get { yield return Object; yield return Index; } } + public int OperandCount => 2; + IRValue IMultipleOperandInstruction.this[int index] + { + get => index == 0 ? Object : index == 1 ? Index : throw new System.ArgumentOutOfRangeException(); + set + { + if (index == 0) + Object = value; + else if (index == 1) + Index = value; + else + throw new System.ArgumentOutOfRangeException(); + } + } public IRIndexGet(IRTemp result, IRValue obj, IRValue index, OpcodeGetIndex opcode) : base(opcode) { Result = result; @@ -337,6 +379,22 @@ public class IRIndexSet : IRInstruction, IMultipleOperandInstruction public IRValue Index { get; set; } public IRValue Value { get; set; } public IEnumerable Operands { get { yield return Object; yield return Index; yield return Value; } } + public int OperandCount => 3; + IRValue IMultipleOperandInstruction.this[int index] + { + get => index == 0 ? Object : index == 1 ? Index : index == 2 ? Value : throw new System.ArgumentOutOfRangeException(); + set + { + if (index == 0) + Object = value; + else if (index == 1) + Index = value; + else if (index == 2) + Value = value; + else + throw new System.ArgumentOutOfRangeException(); + } + } public IRIndexSet(IRValue obj, IRValue index, IRValue value, OpcodeSetIndex opcode) : base(opcode) { Object = obj; @@ -377,7 +435,7 @@ public class IRJumpStack : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; public IRValue Distance { get; set; } - IRValue ISingleOperandInstruction.Operand => Distance; + IRValue ISingleOperandInstruction.Operand { get => Distance; set => Distance = value; } public List Targets { get; } = new List(); public IRJumpStack(IRValue distance, IEnumerable targets, OpcodeJumpStack jumpStack) : base(jumpStack) { @@ -395,7 +453,7 @@ public class IRBranch : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; public IRValue Condition { get; set; } - IRValue ISingleOperandInstruction.Operand => Condition; + IRValue ISingleOperandInstruction.Operand { get => Condition; set => Condition = value; } public BasicBlock True { get; set; } public BasicBlock False { get; set; } public bool PreferFalse { get; set; } = false; @@ -432,6 +490,12 @@ public class IRCall : IRInstruction, IResultingInstruction, IMultipleOperandInst public string Function { get; } public List Arguments { get; } = new List(); public IEnumerable Operands => Enumerable.Reverse(Arguments); + public int OperandCount => Arguments.Count; + IRValue IMultipleOperandInstruction.this[int index] + { + get => Arguments[index]; + set => Arguments[index] = value; + } public IRValue IndirectMethod { get; internal set; } public bool Direct { get; } public bool EmitArgMarker { get; set; } @@ -618,7 +682,7 @@ public class IRReturn : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; public IRValue Value { get; set; } - IRValue ISingleOperandInstruction.Operand => Value; + IRValue ISingleOperandInstruction.Operand { get => Value; set => Value = value; } public short Depth { get; internal set; } public IRReturn(short depth, OpcodeReturn opcode) : base(opcode) => Depth = depth; From cf885399a47712f69e80636c8706884982500c58 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 16:40:47 -0400 Subject: [PATCH 030/120] Make ConstantFolding.AttemptReduction publicly accessible to allow later passes to do targeted constant folding after making a change. --- .../Compilation/Optimization/Passes/ConstantFolding.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index 45144d835..ab3bb56e0 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -69,7 +69,7 @@ private static IRValue AttemptReductionToConstant(IRValue input) return input; return AttemptReduction(temp.Parent); } - private static IRValue AttemptReduction(IRInstruction instruction) + public static IRValue AttemptReduction(IRInstruction instruction) { switch (instruction) { @@ -86,7 +86,7 @@ private static IRValue AttemptReduction(IRInstruction instruction) case IResultingInstruction resulting: return resulting.Result; } - throw new ArgumentException($"{instruction.GetType()} is not supported."); + throw new ArgumentException($"{instruction.GetType()} is not supported for constant folding."); } private static IRValue ReduceUnary(IRUnaryOp instruction) { From a435ddb4bf0e88c78856c8b775ca0427126459d3 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 16:41:52 -0400 Subject: [PATCH 031/120] Provide a method for overwriting the source location of an instruction. This should be used carefully for in-place restructuring of IR instructions. --- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index d913be6c9..09d6a4013 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -5,8 +5,8 @@ namespace kOS.Safe.Compilation.IR { public abstract class IRInstruction { - public short SourceLine { get; } // line number in the source code that this was compiled from. - public short SourceColumn { get; } // column number of the token nearest the cause of this Opcode. + public short SourceLine { get; private set; } // line number in the source code that this was compiled from. + public short SourceColumn { get; private set; } // column number of the token nearest the cause of this Opcode. // Should-be-static public abstract bool SideEffects { get; } @@ -24,6 +24,11 @@ protected Opcode SetSourceLocation(Opcode opcode) opcode.SourceColumn = SourceColumn; return opcode; } + public void OverwriteSourceLocation(short sourceLine, short sourceColumn) + { + SourceLine = sourceLine; + SourceColumn = sourceColumn; + } } public abstract class IRInteractsInstruction : IRInstruction { From 336ac48fe4af9c39b5212588491eb7a3b7ca75dc Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 16:43:31 -0400 Subject: [PATCH 032/120] Add a static utility class to make further passes that search the instruction tree more concise and easier to write. --- .../Optimization/OptimizationTools.cs | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs new file mode 100644 index 000000000..3a17a091e --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Compilation.Optimization +{ + public static class OptimizationTools + { + /*public static IEnumerable FindExpressionsMatching(this IEnumerable instructions, Predicate predicate) + => instructions.SelectMany(i => ExpressionMatchDepthFirst(i, predicate)); + + public static IEnumerable FindFirstExpressionsMatching(this IEnumerable instructions, Predicate predicate) + => instructions.SelectMany(i => ExpressionMatchBreadthFirst(i, predicate)); + */ + + public static IEnumerable DepthFirst(this IEnumerable instructions) + => instructions.SelectMany(DepthFirst); + + public static IEnumerable FindInstructionsMatching(this IEnumerable instructions, Predicate predicate) + => instructions.SelectMany(i => InstructionMatchDepthFirst(i, predicate)); + + public static IEnumerable FindFirstInstructionsMatching(this IEnumerable instructions, Predicate predicate) + => instructions.SelectMany(i => InstructionMatchBreadthFirst(i, predicate)); + + private static IEnumerable ExpressionMatchDepthFirst(IRInstruction instruction, Predicate predicate) + { + if (instruction is ISingleOperandInstruction singleOperandInstruction) + { + if (singleOperandInstruction.Operand is IRTemp temp) + { + foreach (IRTemp predecessorMatch in ExpressionMatchDepthFirst(temp.Parent, predicate)) + yield return predecessorMatch; + if (predicate(temp.Parent)) + yield return temp; + } + } + else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) + { + foreach (IRValue operand in multipleOperandInstruction.Operands) + { + if (operand is IRTemp temp) + { + foreach (IRTemp predecessorMatch in ExpressionMatchDepthFirst(temp.Parent, predicate)) + yield return predecessorMatch; + if (predicate(temp.Parent)) + yield return temp; + } + } + } + } + + private static IEnumerable ExpressionMatchBreadthFirst(IRInstruction instruction, Predicate predicate) + { + if (instruction is ISingleOperandInstruction singleOperandInstruction) + { + if (singleOperandInstruction.Operand is IRTemp temp) + { + if (predicate(temp.Parent)) + yield return temp; + foreach (IRTemp predecessorMatch in ExpressionMatchBreadthFirst(temp.Parent, predicate)) + yield return predecessorMatch; + } + } + else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) + { + foreach (IRValue operand in multipleOperandInstruction.Operands) + { + if (operand is IRTemp temp && predicate(temp.Parent)) + yield return temp; + } + foreach (IRValue operand in multipleOperandInstruction.Operands) + { + if (operand is IRTemp temp) + { + foreach (IRTemp predecessorMatch in ExpressionMatchBreadthFirst(temp.Parent, predicate)) + yield return predecessorMatch; + } + } + } + } + + private static IEnumerable InstructionMatchDepthFirst(IRInstruction instruction, Predicate predicate) + { + foreach (IRInstruction match in CrawlTreeForMatch(instruction, predicate, InstructionMatchDepthFirst)) + yield return match; + if (predicate(instruction)) + yield return instruction; + } + + private static IEnumerable InstructionMatchBreadthFirst(IRInstruction instruction, Predicate predicate) + { + if (predicate(instruction)) + yield return instruction; + foreach (IRInstruction match in CrawlTreeForMatch(instruction, predicate, InstructionMatchBreadthFirst)) + yield return match; + } + + private static IEnumerable DepthFirst(IRInstruction instruction) + { + if (instruction is ISingleOperandInstruction singleOperandInstruction) + { + if (singleOperandInstruction.Operand is IRTemp temp) + foreach (IRInstruction predecessor in DepthFirst(temp.Parent)) + yield return predecessor; + } + else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) + { + foreach (IRValue operand in multipleOperandInstruction.Operands) + { + if (operand is IRTemp temp) + foreach (IRInstruction predecessor in DepthFirst(temp.Parent)) + yield return predecessor; + } + } + yield return instruction; + } + + private static IEnumerable CrawlTreeForMatch(IRInstruction instruction, Predicate predicate, Func, IEnumerable> searchFunc) + { + if (instruction is ISingleOperandInstruction singleOperandInstruction) + { + if (singleOperandInstruction.Operand is IRTemp temp) + foreach (IRInstruction match in searchFunc(temp.Parent, predicate)) + yield return match; + } + else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) + { + foreach (IRValue operand in multipleOperandInstruction.Operands) + { + if (operand is IRTemp temp) + foreach (IRInstruction match in searchFunc(temp.Parent, predicate)) + yield return match; + } + } + } + } +} From a879ba8abadcb986316843f135635612a1d67162 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 22:38:15 -0400 Subject: [PATCH 033/120] Implement equality checking for IR values and instructions. Instructions are equal if their operation and operands are equal. Temporary values are equal if the sequence of operations to return them are equal. --- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 109 +++++++++++++++++++ src/kOS.Safe/Compilation/IR/IRValue.cs | 33 ++++++ 2 files changed, 142 insertions(+) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 09d6a4013..63bb51394 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -97,6 +97,15 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{{store {0}}}", Value.ToString()); + public override bool Equals(object obj) + { + if (obj is IRAssign assignment) + return string.Equals(Target, assignment.Target, System.StringComparison.OrdinalIgnoreCase) && + Value.Equals(assignment.Value); + return base.Equals(obj); + } + public override int GetHashCode() + => Target.ToLower().GetHashCode(); } public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { @@ -170,6 +179,18 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => Operation.ToString(); + public override bool Equals(object obj) + { + if (obj is IRBinaryOp binaryOp && + Operation.GetType() == binaryOp.Operation.GetType()) + { + return (Left.Equals(binaryOp.Left) && Right.Equals(binaryOp.Right)) || + (IsCommutative && Left.Equals(binaryOp.Right) && Right.Equals(binaryOp.Left)); + } + return false; + } + public override int GetHashCode() + => Operation.GetHashCode(); } public class IRUnaryOp : IRInstruction, IResultingInstruction, ISingleOperandInstruction { @@ -192,6 +213,12 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => Operation.ToString(); + public override bool Equals(object obj) + => obj is IRUnaryOp unaryOp && + Operation.GetType() == unaryOp.Operation.GetType() && + Operand.Equals(unaryOp.Operand); + public override int GetHashCode() + => Operation.GetHashCode(); } public class IRNoStackInstruction : IRInstruction { @@ -206,6 +233,10 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => Operation.ToString(); + public override bool Equals(object obj) + => obj is IRNoStackInstruction instruction && Operation.GetType() == instruction.Operation.GetType(); + public override int GetHashCode() + => Operation.GetHashCode(); } public class IRUnaryConsumer : IRInstruction, ISingleOperandInstruction { @@ -227,6 +258,12 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => Operation.ToString(); + public override bool Equals(object obj) + => obj is IRUnaryConsumer unaryConsumer && + Operation.GetType() == unaryConsumer.Operation.GetType() && + Operand.Equals(unaryConsumer.Operand); + public override int GetHashCode() + => Operation.GetHashCode(); } public class IRPop : IRInstruction, ISingleOperandInstruction { @@ -246,6 +283,10 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => "{pop}"; + public override bool Equals(object obj) + => obj is IRPop pop && Value.Equals(pop.Value); + public override int GetHashCode() + => Value.GetHashCode(); } public class IRNonVarPush : IRInstruction, IResultingInstruction { @@ -266,6 +307,11 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => Operation.ToString(); + public override bool Equals(object obj) + => obj is IRNonVarPush instruction && + Operation.GetType() == instruction.Operation.GetType(); + public override int GetHashCode() + => Operation.GetHashCode(); } public class IRSuffixGet : IRInteractsInstruction, IResultingInstruction, ISingleOperandInstruction { @@ -287,6 +333,13 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{{gmb \"{0}\"}}", Suffix); + public override bool Equals(object obj) + => obj is IRSuffixGet suffixGet && + !(suffixGet is IRSuffixGetMethod) && + string.Equals(Suffix, suffixGet.Suffix, System.StringComparison.OrdinalIgnoreCase) && + Object == suffixGet.Object; + public override int GetHashCode() + => (Object, Suffix).GetHashCode(); } public class IRSuffixGetMethod : IRSuffixGet { @@ -300,6 +353,12 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{{gmet \"{0}\"}}", Suffix); + public override bool Equals(object obj) + => obj is IRSuffixGetMethod suffixGet && + string.Equals(Suffix, suffixGet.Suffix, System.StringComparison.OrdinalIgnoreCase) && + Object == suffixGet.Object; + public override int GetHashCode() + => (Object, Suffix).GetHashCode(); } public class IRSuffixSet : IRInteractsInstruction, IMultipleOperandInstruction { @@ -338,6 +397,13 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{{smb \"{0}\"}}", Suffix); + public override bool Equals(object obj) + => obj is IRSuffixSet suffixSet && + string.Equals(Suffix, suffixSet.Suffix, System.StringComparison.OrdinalIgnoreCase) && + Object.Equals(suffixSet.Object) && + Value.Equals(suffixSet.Value); + public override int GetHashCode() + => (Object, Suffix).GetHashCode(); } public class IRIndexGet : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { @@ -376,6 +442,12 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => "{gidx}"; + public override bool Equals(object obj) + => obj is IRIndexGet indexGet && + Object.Equals(indexGet.Object) && + Index.Equals(indexGet.Index); + public override int GetHashCode() + => Object.GetHashCode(); } public class IRIndexSet : IRInstruction, IMultipleOperandInstruction { @@ -418,6 +490,13 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => "{sidx}"; + public override bool Equals(object obj) + => obj is IRIndexSet indexGet && + Object.Equals(indexGet.Object) && + Index.Equals(indexGet.Index) && + Value.Equals(indexGet.Value); + public override int GetHashCode() + => Object.GetHashCode(); } public class IRJump : IRInstruction { @@ -435,6 +514,11 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{{jump {0}}}", Target.Label); + public override bool Equals(object obj) + => obj is IRJump jump && + Target == jump.Target; + public override int GetHashCode() + => Target.GetHashCode(); } public class IRJumpStack : IRInstruction, ISingleOperandInstruction { @@ -453,6 +537,12 @@ internal override IEnumerable EmitOpcode() yield return opcode; yield return SetSourceLocation(new OpcodeJumpStack()); } + public override bool Equals(object obj) + => obj is IRJumpStack jumpStack && + Distance == jumpStack.Distance && + Targets.SequenceEqual(jumpStack.Targets); + public override int GetHashCode() + => Targets.GetHashCode(); } public class IRBranch : IRInstruction, ISingleOperandInstruction { @@ -486,6 +576,13 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{{br.? {0}/{1}}}", True.Label, False.Label); + public override bool Equals(object obj) + => obj is IRBranch branch && + Condition.Equals(branch.Condition) && + True == branch.True && + False == branch.False; + public override int GetHashCode() + => True.GetHashCode() ^ False.GetHashCode(); } public class IRCall : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { @@ -682,6 +779,12 @@ public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, params IRVal } public override string ToString() => string.Format("{{call {0}({1})}}", Function.Trim('(', ')'), string.Join(",", Arguments.Select(a => a.ToString()))); + public override bool Equals(object obj) + => obj is IRCall call && + string.Equals(Function.Replace("()", ""), call.Function.Replace("()", ""), System.StringComparison.OrdinalIgnoreCase) && + Arguments.SequenceEqual(call.Arguments); + public override int GetHashCode() + => Function.ToLower().GetHashCode(); } public class IRReturn : IRInstruction, ISingleOperandInstruction { @@ -702,5 +805,11 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{return {0} deep}", Depth); + public override bool Equals(object obj) + => obj is IRReturn ret && + Depth == ret.Depth && + Value.Equals(ret.Value); + public override int GetHashCode() + => base.GetHashCode(); } } diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index 2645fd4ba..dd5b85661 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -34,6 +34,10 @@ internal override IEnumerable EmitPush() SourceColumn = sourceColumn }; } + public override bool Equals(object obj) + => Value.Equals(obj); + public override int GetHashCode() + => Value.GetHashCode(); public override string ToString() => Value.ToString(); } @@ -42,6 +46,7 @@ public abstract class IRVariableBase : IRValue public string Name { get; } public IRVariableBase(string name) => Name = name; + public BasicBlock Scope { get; } } public class IRVariable : IRVariableBase { @@ -65,6 +70,12 @@ internal override IEnumerable EmitPush() } public override string ToString() => Name; + public override bool Equals(object obj) + => obj is IRVariable variable && + Scope == variable.Scope && + string.Equals(Name, variable.Name, System.StringComparison.OrdinalIgnoreCase); + public override int GetHashCode() + => Name.ToLower().GetHashCode(); } public class IRRelocateLater : IRConstant { @@ -129,6 +140,28 @@ internal override IEnumerable EmitPush() foreach (Opcode opcode in Parent.EmitOpcode()) yield return opcode; } + public override bool Equals(object obj) + { + if (obj is IRTemp temp) + { + /*if (isPromoted) + { + return temp.isPromoted && + Scope == temp.Scope && + string.Equals(Name, temp.Name, System.StringComparison.OrdinalIgnoreCase); + }*/ + return Parent.Equals(temp.Parent); + } + if (obj is IRVariable variable) + { + return isPromoted && + Scope == variable.Scope && + string.Equals(Name, variable.Name, System.StringComparison.OrdinalIgnoreCase); + } + return false; + } + public override int GetHashCode() + => Name.ToLower().GetHashCode(); } public class IRParameter : IRValue { From c86d30833e70e7a5f6f6ec05f5abdfbb43fd3d0e Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 22:39:00 -0400 Subject: [PATCH 034/120] Fix binary instructions losing appropriate commutative status when their operation is changed. --- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 22 ++++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 63bb51394..d6b74f5de 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -116,6 +117,17 @@ public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperand public IRValue Right { get; set; } public IEnumerable Operands { get { yield return Left; yield return Right; } } public int OperandCount => 2; + private static Type[] commutativeTypes = + { + typeof(OpcodeCompareEqual), + typeof(OpcodeCompareNE), + typeof(OpcodeCompareGT), + typeof(OpcodeCompareLT), + typeof(OpcodeCompareGTE), + typeof(OpcodeCompareLTE), + typeof(OpcodeMathAdd), + typeof(OpcodeMathMultiply) + }; IRValue IMultipleOperandInstruction.this[int index] { get => index == 0 ? Left : index == 1 ? Right : throw new System.ArgumentOutOfRangeException(); @@ -129,7 +141,7 @@ IRValue IMultipleOperandInstruction.this[int index] throw new System.ArgumentOutOfRangeException(); } } - public bool IsCommutative { get; } + public bool IsCommutative => commutativeTypes.Contains(Operation.GetType()); public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) : base(operation) { @@ -137,14 +149,6 @@ public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue r Operation = operation; Left = left; Right = right; - IsCommutative = (operation is OpcodeCompareEqual) || - (operation is OpcodeCompareNE) || - (operation is OpcodeCompareGT) || - (operation is OpcodeCompareLT) || - (operation is OpcodeCompareGTE) || - (operation is OpcodeCompareLTE) || - (operation is OpcodeMathAdd) || - (operation is OpcodeMathMultiply); } public void SwapOperands() { From 852ece87841faa9ddb8eb021f3b8bc766c3ff0ad Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 22:40:10 -0400 Subject: [PATCH 035/120] Add an argument marker to the stack before running a unit test program. This fixes programs with parameters breaking when they check for an argument marker. --- src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs b/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs index 67b9db229..1731e16ed 100644 --- a/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs +++ b/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs @@ -94,6 +94,7 @@ protected void RunScript(string fileName) screen.ClearOutput(); + cpu.PushArgumentStack(new KOSArgMarkerType()); cpu.GetCurrentContext().AddParts(compiled); } From 73dee514a4f9297e0dbd554701facb87678b599b Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 22:40:41 -0400 Subject: [PATCH 036/120] Force clobber builtins as necessary. --- kerboscript_tests/integration/suffixReplacement.ks | 1 + 1 file changed, 1 insertion(+) diff --git a/kerboscript_tests/integration/suffixReplacement.ks b/kerboscript_tests/integration/suffixReplacement.ks index b1cde7f96..2e93f6cd1 100644 --- a/kerboscript_tests/integration/suffixReplacement.ks +++ b/kerboscript_tests/integration/suffixReplacement.ks @@ -1,3 +1,4 @@ +@CLOBBERBUILTINS OFF. set airspeed to 100. // Clobber the built-in so that this value is pulled after replacement. print(CONSTANT:g0). print(CONSTANT():pi). From 60624a617f148b8337a5530344b834c0faf98266 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 22:42:23 -0400 Subject: [PATCH 037/120] Add a peephole optimization pass. This pass makes various small optimizations, as well as larger algebraic simplifications. See the list in OptimizationLevel.cs and the complete set of algebraic simplifications in PeepholeOptimizations.cs. Includes a unit test for these operations. --- .../integration/peepholeOptimizations.ks | 51 ++ .../Execution/OptimizationTest.cs | 32 ++ .../Optimization/OptimizationLevel.cs | 6 +- .../Passes/PeepholeOptimizations.cs | 445 ++++++++++++++++++ 4 files changed, 531 insertions(+), 3 deletions(-) create mode 100644 kerboscript_tests/integration/peepholeOptimizations.ks create mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs diff --git a/kerboscript_tests/integration/peepholeOptimizations.ks b/kerboscript_tests/integration/peepholeOptimizations.ks new file mode 100644 index 000000000..b6ef105a9 --- /dev/null +++ b/kerboscript_tests/integration/peepholeOptimizations.ks @@ -0,0 +1,51 @@ +parameter a is TRUE. +parameter c is 3. +parameter x is 2. +parameter z is 5. + +print("test":typename()). +// v() is unavailable in unit testing. +//local v1 is v(1,2,3). +//local v2 is v(3,4,5). +// Multiplying scalars will work equally well. +local v1 is 2. +local v2 is 3. + +print(vectorDotProduct(v1,v2)). // =6 +print(vdot(v1,v2)). // =6 + +if not a { + print("Don't print this."). +} + +if not (not a) { + print("Print this."). +} + +print(c*x+c*z). // = 21 +print(c*x+z*c). // = 21 +print(x*c+c*z). // = 21 +print(x*c+z*c). // = 21 + +print(-x+z). // = 3 +print(x+-z). // = -3 + +print(x--z). // = 7 + +print(x^z/x). // = 16 +print(z^4/z). // = 125 + +print(x^z*x). // = 64 +print(z^4*z). // = 3125 + +print(x^z*x^c). // = 256 + +print(x^z/x^c). // = 4 + +print(z^3/z). // = 25 + +print(z*z*z). // = 125 + +print(z^3/z*z). // = 125 + +print(x^2*x^z/x*x^3). // = 512 \ No newline at end of file diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index 141399853..d35a9783e 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -146,5 +146,37 @@ public void TestSuffixReplacement() "100" ); } + + + [Test] + public void TestPeepholeOptimizations() + { + // Test that certain suffixes are replaced + RunScript("integration/peepholeOptimizations.ks"); + RunSingleStep(); + AssertOutput( + "String", + "6", + "6", + "Print this.", + "21", + "21", + "21", + "21", + "3", + "-3", + "7", + "16", + "125", + "64", + "3125", + "256", + "4", + "25", + "125", + "125", + "512" + ); + } } } diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index 8f7210e7b..35e7fe161 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -9,16 +9,16 @@ namespace kOS.Safe.Compilation * Replace ship fields with their alias * 30. Constant folding * 50. Dead code elimination - * - * 20. Constant propagation * 1050. Peephole optimizations: * Replace lex indexing with string constant with suffixing where possible * Replace parameterless suffix method calls with get member (this feels like cheating...) * Replace calls to VectorDotProduct with multiplication * Replace --X with X * Replace !!X with X - * Branch logical simplification (e.g. !A branch = A branch!) + * Branch logical simplification (e.g. !X branch = X branch!) * Algebraic simplification (e.g. A*B+A*C = A*(B+C), A+-B=A-B, A--B=A+B, -A+B=B-A, X*X*...*X=N^X) + * + * 20. Constant propagation * O2: * 1000. Common expression elimination * Particularly: Any expression of 3 opcodes used more than twice, diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs new file mode 100644 index 000000000..ebda09f71 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs @@ -0,0 +1,445 @@ +using System; +using System.Collections.Generic; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class PeepholeOptimizations : IOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + + public short SortIndex => 1050; + + // TODO: Replace lex indexing with string constant with suffixing where possible. + public void ApplyPass(List code) + { + foreach (IRInstruction instruction in code.DepthFirst()) + { + PeepholeFilter(instruction); + } + } + + private static void PeepholeFilter(IRInstruction instruction) + { + // Replace lex indexing using string constant with suffixing where possible + // TODO: this is harder since it requires knowing the object type being indexed against. + + // Replace parameterless suffix method calls with get member + if (instruction is IRCall suffixCall && + !suffixCall.Direct && + suffixCall.Arguments.Count == 0 && + suffixCall.IndirectMethod is IRTemp tempSuffixCall && + tempSuffixCall.Parent is IRSuffixGetMethod) + { + ReplaceParameterlessSuffix(suffixCall); + return; + } + // Replace calls to VectorDotProduct with multiplication + if (instruction is IRCall vDotCall && + (vDotCall.Function == "vdot" || vDotCall.Function == "vectordotproduct") && + vDotCall.Arguments.Count == 2) + { + ReplaceVectorDotProduct(vDotCall); + return; + } + // Algebraic simplifications + if (instruction is IRBinaryOp binaryOp) + { + IRTemp tempL = binaryOp.Left as IRTemp; + IRTemp tempR = binaryOp.Right as IRTemp; + switch (binaryOp.Operation) + { + case OpcodeMathAdd _: + { + // A*B + A*C = A*(B+C) + { + if (tempL != null && + tempL.Parent is IRBinaryOp opL && + tempR != null && + tempR.Parent is IRBinaryOp opR && + opL.Operation is OpcodeMathMultiply && + opR.Operation is OpcodeMathMultiply) + { + if (opR.Left == opL.Left) + { + DistributeMultiplication(binaryOp); + return; + } + if (opR.Left == opL.Right) + { + opL.SwapOperands(); + DistributeMultiplication(binaryOp); + return; + } + if (opR.Right == opL.Left) + { + opR.SwapOperands(); + DistributeMultiplication(binaryOp); + return; + } + if (opR.Right == opL.Right) + { + opL.SwapOperands(); + opR.SwapOperands(); + DistributeMultiplication(binaryOp); + return; + } + } + } + // -B+A = A+-B = A-B + { + if (tempR != null && + tempR.Parent is IRUnaryOp opR && + opR.Operation is OpcodeMathNegate) + { + DistributeNegationB(binaryOp); + return; + } + if (tempL != null && + tempL.Parent is IRUnaryOp opL && + opL.Operation is OpcodeMathNegate) + { + binaryOp.SwapOperands(); + DistributeNegationB(binaryOp); + return; + } + } + // -A+B = B-A + { + if (tempL != null && + tempL.Parent is IRUnaryOp opL && + opL.Operation is OpcodeMathNegate) + { + DistributeNegationA(binaryOp); + return; + } + } + } + break; + case OpcodeMathSubtract _: + // A--B=A+B + { + if (tempR != null && + tempR.Parent is IRUnaryOp unaryOp && + unaryOp.Operation is OpcodeMathNegate) + { + ReplaceNegateSubtract(binaryOp); + return; + } + } + break; + case OpcodeMathDivide _: + if (binaryOp.Right is IRConstant constantR && + Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + throw new Exceptions.KOSCompileException(instruction, new DivideByZeroException()); + // X^N/X=X^(N-1) + { + if (tempL != null && + tempL.Parent is IRBinaryOp opL && + opL.Operation is OpcodeMathPower && + opL.Left == binaryOp.Right) + { + IncreasePower(binaryOp, -1); + return; + } + } + // X^N/X^M=X^(N-M) + { + if (tempL != null && + tempL.Parent is IRBinaryOp opL && + opL.Operation is OpcodeMathPower && + tempR != null && + tempR.Parent is IRBinaryOp opR && + opR.Operation is OpcodeMathPower && + opL.Left == opR.Left) + { + DividePowers(binaryOp); + return; + } + } + break; + case OpcodeMathMultiply _: + // X^N*X=X^(N+1) + { + if (tempL != null && + tempL.Parent is IRBinaryOp opL && + opL.Operation is OpcodeMathPower && + opL.Left == binaryOp.Right) + { + IncreasePower(binaryOp, 1); + return; + } + if (tempR != null && + tempR.Parent is IRBinaryOp opR && + opR.Operation is OpcodeMathPower && + opR.Left == binaryOp.Left) + { + binaryOp.SwapOperands(); + IncreasePower(binaryOp, 1); + return; + } + } + // X*X*...*X=N^X + // Start with (X*X)*X = X*(X*X) = X^3 + // Then the previous rule will kick in and exponentiate from there + { + if (tempR != null && + tempR.Parent is IRBinaryOp opR && + opR.Operation is OpcodeMathMultiply && + opR.Left == opR.Right && + opR.Left == binaryOp.Left) + { + CreatePower(binaryOp); + return; + } + if (tempL != null && + tempL.Parent is IRBinaryOp opL && + opL.Operation is OpcodeMathMultiply && + opL.Left == opL.Right && + opL.Left == binaryOp.Right) + { + binaryOp.SwapOperands(); + CreatePower(binaryOp); + return; + } + } + // X^N*X^M=X^(N+M) + { + if (tempL != null && + tempL.Parent is IRBinaryOp opL && + opL.Operation is OpcodeMathPower && + tempR != null && + tempR.Parent is IRBinaryOp opR && + opR.Operation is OpcodeMathPower && + opL.Left == opR.Left) + { + CombinePowers(binaryOp); + return; + } + } + break; + } + } + // Redundant unary operation replacements + // E.g. !!X = X and --X = X + if (instruction is ISingleOperandInstruction singleOperandInstruction) + { + if (singleOperandInstruction.Operand is IRTemp tempOperand && + tempOperand.Parent is IRUnaryOp unaryParent) + { + IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); + if (potentialResult != null) + singleOperandInstruction.Operand = potentialResult; + } + } + else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) + { + for (int i = multipleOperandInstruction.OperandCount - 1; i >= 0; i--) + { + IRValue operand = multipleOperandInstruction[i]; + if (operand is IRTemp tempOperand && + tempOperand.Parent is IRUnaryOp unaryParent) + { + IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); + if (potentialResult != null) + multipleOperandInstruction[i] = potentialResult; + } + } + } + // Branch logical simplification (e.g. !X branch = X branch!) + if (instruction is IRBranch branch && + branch.Condition is IRTemp temp && + temp.Parent is IRUnaryOp negateBranch && + negateBranch.Operation is OpcodeLogicNot) + { + ReplaceRedundantNotBranch(branch); + return; + } + } + + private static void ReplaceParameterlessSuffix(IRCall call) + { + IRSuffixGet suffixMethod = (IRSuffixGet)((IRTemp)call.IndirectMethod).Parent; + ((IRTemp)call.Result).Parent = new IRSuffixGet( + (IRTemp)call.Result, + suffixMethod.Object, + new OpcodeGetMember(suffixMethod.Suffix) + { + SourceLine = suffixMethod.SourceLine, + SourceColumn = suffixMethod.SourceColumn + }); + } + + private static void ReplaceVectorDotProduct(IRCall call) + { + ((IRTemp)call.Result).Parent = new IRBinaryOp( + (IRTemp)call.Result, + new OpcodeMathMultiply() + { + SourceLine = call.SourceLine, + SourceColumn = call.SourceColumn + }, + call.Arguments[0], + call.Arguments[1]); + } + + private static IRValue AttempReplaceRedundantUnaryOp(IRUnaryOp unaryParent) + { + // Replace --X with X + if (CanOperationBeReplaced(unaryParent, typeof(OpcodeMathNegate))) + return GetDoubleNestedValue(unaryParent); + // Replace !!X with X + if (CanOperationBeReplaced(unaryParent, typeof(OpcodeLogicNot))) + return GetDoubleNestedValue(unaryParent); + return null; + } + private static bool CanOperationBeReplaced(IRUnaryOp operation, Type opcodeType) + { + return operation.Operation.GetType() == opcodeType && + operation.Operand is IRTemp operand && + operand.Parent is IRUnaryOp neg2 && + neg2.Operation.GetType() == opcodeType; + } + private static IRValue GetDoubleNestedValue(ISingleOperandInstruction operation) + { + return ((IRUnaryOp)((IRTemp)operation.Operand).Parent).Operand; + } + + private static void ReplaceRedundantNotBranch(IRBranch branch) + { + branch.Condition = GetDoubleNestedValue(branch); + branch.PreferFalse = !branch.PreferFalse; + (branch.True, branch.False) = (branch.False, branch.True); + } + + private static void DistributeMultiplication(IRBinaryOp instruction) + { + // A*B + A*C = A*(B+C) + IRTemp tempL = (IRTemp)instruction.Left; + IRTemp tempR = (IRTemp)instruction.Right; + IRBinaryOp opL = (IRBinaryOp)tempL.Parent; + IRBinaryOp opR = (IRBinaryOp)tempR.Parent; + // Restructure to create the parentheses + opR.Operation = new OpcodeMathAdd(); + opR.Left = opL.Right; + // Restructure the multiplication term. + instruction.Left = opL.Left; + instruction.Operation = new OpcodeMathMultiply(); + } + + private static void DistributeNegationA(IRBinaryOp instruction) + { + // -A+B = B-A + IRTemp tempL = (IRTemp)instruction.Left; + IRUnaryOp opL = (IRUnaryOp)tempL.Parent; + instruction.Left = opL.Operand; + instruction.SwapOperands(); + instruction.Operation = new OpcodeMathSubtract(); + } + + private static void DistributeNegationB(IRBinaryOp instruction) + { + // A+-B = A-B + IRTemp tempR = (IRTemp)instruction.Right; + IRUnaryOp opR = (IRUnaryOp)tempR.Parent; + instruction.Right = opR.Operand; + instruction.Operation = new OpcodeMathSubtract(); + } + + private static void ReplaceNegateSubtract(IRBinaryOp instruction) + { + // A--B=A+B + IRTemp tempR = (IRTemp)instruction.Right; + IRUnaryOp opR = (IRUnaryOp)tempR.Parent; + instruction.Right = opR.Operand; + instruction.Operation = new OpcodeMathAdd(); + } + + private static void IncreasePower(IRBinaryOp instruction, int powerIncrease) + { + // X^N*X=X^(N+1) + // X^N/X=X^(N-1) + // Assumes |N +/- 1| < 2^31 - 1 (for integer scalars) or 2^53 (for double scalars) + // Going beyond that overflows an integer or the mantissa for double. + + IRTemp tempL = (IRTemp)instruction.Left; + IRBinaryOp opL = (IRBinaryOp)tempL.Parent; + + short divLine = instruction.SourceLine; + short divColumn = instruction.SourceColumn; + + IRConstant powConst = new IRConstant(new Encapsulation.ScalarIntValue(powerIncrease), instruction); + + // Set up the new power operation + instruction.Left = opL.Left; + instruction.Operation = new OpcodeMathPower(); + instruction.OverwriteSourceLocation(opL.SourceLine, opL.SourceColumn); + + // Reuse the old power instruction for the addition/subtraction + instruction.Right = tempL; + opL.Left = powConst; + opL.Operation = new OpcodeMathAdd(); + opL.OverwriteSourceLocation(divLine, divColumn); + + // Attempt constant folding afterwards + instruction.Right = ConstantFolding.AttemptReduction(opL); + + // Special case for when N == 2 afterwards + // then revert back to X * X + if (instruction.Right is IRConstant constantR && + Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) + { + instruction.Right = instruction.Left; + instruction.Operation = new OpcodeMathMultiply(); + } + } + + private static void CreatePower(IRBinaryOp instruction) + { + // X*(X*X) = X^3 + instruction.Operation = new OpcodeMathPower(); + IRTemp tempR = (IRTemp)instruction.Right; + instruction.Right = new IRConstant(new Encapsulation.ScalarIntValue(3), tempR.Parent); + } + + private static void CombinePowers(IRBinaryOp instruction) + => CombinePowers(instruction, new OpcodeMathAdd()); + private static void DividePowers(IRBinaryOp instruction) + => CombinePowers(instruction, new OpcodeMathSubtract()); + private static void CombinePowers(IRBinaryOp instruction, BinaryOpcode newOperation) + { + // X^N*X^M=X^(N+M) + // Assumes |N + M| < 2^31 - 1 (for integer scalars) or 2^53 (for double scalars) + + IRTemp tempL = (IRTemp)instruction.Left; + IRTemp tempR = (IRTemp)instruction.Right; + IRBinaryOp opL = (IRBinaryOp)tempL.Parent; + IRBinaryOp opR = (IRBinaryOp)tempR.Parent; + + short divLine = instruction.SourceLine; + short divColumn = instruction.SourceColumn; + + // Set up the new power operation + instruction.Left = opL.Left; + instruction.Operation = new OpcodeMathPower(); + instruction.OverwriteSourceLocation(opL.SourceLine, opL.SourceColumn); + + // Reuse an old power instruction for the addition/subtraction + opR.Left = opL.Right; + opR.Operation = newOperation; + opR.OverwriteSourceLocation(divLine, divColumn); + + // Attempt constant folding afterwards + instruction.Right = ConstantFolding.AttemptReduction(opR); + + // Special case for when N == 2 afterwards + // then revert back to X * X + if (instruction.Right is IRConstant constantR && + Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) + { + instruction.Right = instruction.Left; + instruction.Operation = new OpcodeMathMultiply(); + } + } + } +} From b6bb80ca63bb20306ae1304c1181c0789c67f936 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 12:17:12 -0400 Subject: [PATCH 038/120] Add property to enumerate which blocks are dominated by a BasicBlock. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index c40ea2d3f..8b6f10aac 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -8,6 +8,8 @@ public class BasicBlock { private readonly HashSet predecessors = new HashSet(); private readonly HashSet successors = new HashSet(); + private BasicBlock dominator; + private readonly HashSet dominates = new HashSet(); private readonly List parameters = new List(); private readonly Dictionary variables = new Dictionary(); private readonly Dictionary externalGlobalVariables = new Dictionary(); @@ -17,11 +19,21 @@ public class BasicBlock public int StartIndex { get; } public int EndIndex { get; } public List Instructions { get; } = new List(); - public IEnumerable Successors => successors; - public IEnumerable Predecessors => predecessors; + public IReadOnlyCollection Successors => successors; + public IReadOnlyCollection Predecessors => predecessors; public string Label => nonSequentialLabel ?? $"@BB#{ID}"; public int ID { get; } - public BasicBlock Dominator { get; protected set; } + public BasicBlock Dominator + { + get => dominator; + protected set + { + dominator?.dominates.Remove(this); + dominator = value; + dominator?.dominates.Add(this); + } + } + public IReadOnlyCollection Dominates => dominates; public ExtendedBasicBlock ExtendedBlock { get; set; } public IRJump FallthroughJump { get; set; } = null; #if DEBUG From ef43c4cffb61c50c3ed096cbabc40321bc752466 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 12:20:31 -0400 Subject: [PATCH 039/120] Implement scope awareness for BasicBlocks. Class IRScope should not be confused with class Scope or class VariableScope... --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 66 ++++++------- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 55 +++++++++-- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 5 - src/kOS.Safe/Compilation/IR/IRScope.cs | 107 ++++++++++++++++++++++ 4 files changed, 181 insertions(+), 52 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/IRScope.cs diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 8b6f10aac..9d863b96f 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -11,11 +11,20 @@ public class BasicBlock private BasicBlock dominator; private readonly HashSet dominates = new HashSet(); private readonly List parameters = new List(); - private readonly Dictionary variables = new Dictionary(); - private readonly Dictionary externalGlobalVariables = new Dictionary(); private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. private readonly string nonSequentialLabel = null; + private IRScope scope; + public IRScope Scope + { + get => scope; + set + { + scope?.RemoveBlock(this); + scope = value; + scope.EnrollBlock(this); + } + } public int StartIndex { get; } public int EndIndex { get; } public List Instructions { get; } = new List(); @@ -146,46 +155,23 @@ public void AddParameter(IRParameter parameter) { parameters.Add(parameter); } - public void StoreLocalVariable(IRVariable variable) - { - variables[variable.Name] = variable; - } - public void StoreGlobalVariable(IRVariable variable) - { - if (Dominator != null) - { - Dominator.StoreGlobalVariable(variable); - return; - } - externalGlobalVariables[variable.Name] = variable; - } - public void StoreVariable(IRVariable variable) - { - if (!TryStoreVariable(variable)) - StoreGlobalVariable(variable); - } - public bool TryStoreVariable(IRVariable variable) - { - if (variables.ContainsKey(variable.Name)) + public void StoreLocalVariable(IRVariableBase variable) + => Scope.StoreLocalVariable(variable); + public void StoreGlobalVariable(IRVariableBase variable) + => Scope.StoreGlobalVariable(variable); + public void StoreVariable(IRVariableBase variable) + => Scope.StoreVariable(variable); + public bool TryStoreVariable(IRVariableBase variable) + => Scope.TryStoreVariable(variable); + public IRVariableBase PushVariable(string name, Opcode opcode) + { + IRVariableBase result = Scope.GetVariable(name); + if (result == null) { - variables[variable.Name] = variable; - return true; + result = new IRVariable(name, opcode); + Scope.StoreGlobalVariable(result); } - if (Dominator != null) - return Dominator.TryStoreVariable(variable); - return false; - } - public IRVariable PushVariable(string name, Opcode opcode) - { - if (variables.ContainsKey(name)) - return variables[name]; - if (Dominator != null) - return Dominator.PushVariable(name, opcode); - if (externalGlobalVariables.ContainsKey(name)) - return externalGlobalVariables[name]; - IRVariable newExternalGlobal = new IRVariable(name, opcode); - externalGlobalVariables.Add(name, newExternalGlobal); - return newExternalGlobal; + return result; } public void SetStackState(Stack stack) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 3adf5e491..67566095d 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -22,8 +22,11 @@ public List Lower(List code) private void CreateBlocks(List code, Dictionary labels, List blocks) { + IRScope globalScope = new IRScope(null) { IsGlobalScope = true }; SortedSet leaders = new SortedSet() { 0 }; - for (int i = 1; i < code.Count; i++) // The first instruction is always a leader so we can skip 0. + HashSet scopePushes = new HashSet(); + HashSet scopePops = new HashSet(); + for (int i = 0; i < code.Count; i++) { if (code[i] is BranchOpcode branch) { @@ -37,12 +40,22 @@ private void CreateBlocks(List code, Dictionary labels, Lis { throw new NotImplementedException("OpcodeJumpStack is not implemented for optimization because it is non-deterministic. Use OptimizationLevel.None."); } - else if (code[i] is OpcodeReturn) + else if (code[i] is OpcodeReturn ret) + { leaders.Add(i + 1); + if (ret.Depth > 0) + scopePops.Add(i); + } else if (code[i] is OpcodePushScope) + { leaders.Add(i); + scopePushes.Add(i); + } else if (code[i] is OpcodePopScope) + { leaders.Add(i + 1); + scopePops.Add(i); + } } leaders.Add(code.Count); foreach (int startIndex in leaders.Take(leaders.Count - 1)) @@ -52,6 +65,7 @@ private void CreateBlocks(List code, Dictionary labels, Lis if (label.StartsWith("@")) label = null; BasicBlock block = new BasicBlock(startIndex, endIndex, blockID++, label); + blocks.Add(block); } foreach (BasicBlock block in blocks) @@ -74,11 +88,43 @@ private void CreateBlocks(List code, Dictionary labels, Lis block.OriginalOpcodes = code.ToArray(); #endif } + + BasicBlock rootBlock = GetBlockFromStartIndex(blocks, 0); + rootBlock.EstablishDominance(); + + AssignScopes(rootBlock, globalScope, scopePushes, scopePops); } private BasicBlock GetBlockFromStartIndex(List blocks, int startIndex) => blocks.First(b => b.StartIndex == startIndex); + private static void AssignScopes(BasicBlock root, IRScope globalScope, HashSet scopePushIndices, HashSet scopePopIndices) + { + Stack scopeStack = new Stack(); + scopeStack.Push(globalScope); + + void Visit(BasicBlock block) + { + if (scopePushIndices.Contains(block.StartIndex)) + scopeStack.Push( + new IRScope(scopeStack.Peek()) + { + HeaderBlock = block + }); + + block.Scope = scopeStack.Peek(); + + // Return statements can pop multiple scopes + if (scopePopIndices.Contains(block.EndIndex)) + scopeStack.Pop().FooterBlock = block; + + foreach (BasicBlock child in block.Dominates) + Visit(child); + } + + Visit(root); + } + private void FillBlocks(List code, Dictionary labels, List blocks) { Stack stack = new Stack(); @@ -209,11 +255,6 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack userFunctions) RootBlocks.Add(fragment.FunctionCode[0]); } } - - foreach (BasicBlock block in RootBlocks) - { - block.EstablishDominance(); - } } public void EmitCode(CodePart codePart) diff --git a/src/kOS.Safe/Compilation/IR/IRScope.cs b/src/kOS.Safe/Compilation/IR/IRScope.cs new file mode 100644 index 000000000..b95167d1e --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IRScope.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR +{ + public class IRScope + { + private readonly Dictionary variables = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private IRScope parent; + private readonly HashSet childScopes = new HashSet(); + private readonly HashSet blocks = new HashSet(); + + private int nextChildIndex = 0; + private int index; + + public IRScope ParentScope + { + get => parent; + set + { + parent?.childScopes.Remove(this); + parent = value; + if (parent != null) + { + parent.childScopes.Add(this); + index = parent.nextChildIndex++; + } + else + index = 0; + } + } + public IReadOnlyCollection Children => childScopes; + public IReadOnlyCollection Blocks => blocks; + public BasicBlock HeaderBlock { get; set; } + public BasicBlock FooterBlock { get; set; } + public IReadOnlyCollection Variables => variables.Keys; + public bool IsGlobalScope { get; internal set; } = false; + + public IRScope(IRScope parent) + { + ParentScope = parent; + } + + public void StoreLocalVariable(IRVariableBase variable) + { + variables[variable.Name] = variable; + } + public void StoreGlobalVariable(IRVariableBase variable) + { + if (!IsGlobalScope) + ParentScope.StoreGlobalVariable(variable); + else + StoreLocalVariable(variable); + } + public void StoreVariable(IRVariableBase variable) + { + if (!TryStoreVariable(variable)) + StoreGlobalVariable(variable); + } + public bool TryStoreVariable(IRVariableBase variable) + { + if (variables.ContainsKey(variable.Name)) + { + variables[variable.Name] = variable; + return true; + } + return ParentScope?.TryStoreVariable(variable) ?? false; + } + + public IRVariableBase GetVariable(string name) + { + if (variables.ContainsKey(name)) + return variables[name]; + return ParentScope?.GetVariable(name); + } + + public bool IsVariableInScope(string name) + { + if (variables.ContainsKey(name)) + return true; + return ParentScope?.IsVariableInScope(name) ?? false; + } + + public void EnrollBlock(BasicBlock block) + { + block.Scope?.RemoveBlock(block); + blocks.Add(block); + } + public void RemoveBlock(BasicBlock block) + => blocks.Remove(block); + + public override string ToString() + { + if (IsGlobalScope) + return "IRScope: Global"; + return $"IRScope: {IndexString()}"; + } + private string IndexString() + { + if (ParentScope == null) + return $"{index}"; + return $"{ParentScope.IndexString()}.{index}"; + } + } +} From 9dbea3874ce548e07b8382b38064828bc461f1d9 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 12:21:41 -0400 Subject: [PATCH 040/120] Add pass to remove unnecessary scope pushes and pops. This also removes scope pops that can be replaced by incrementing the return depth. --- .../Optimization/OptimizationLevel.cs | 1 + .../Passes/UnnecessaryScopeRemoval.cs | 117 ++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index 35e7fe161..19e108481 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -17,6 +17,7 @@ namespace kOS.Safe.Compilation * Replace !!X with X * Branch logical simplification (e.g. !X branch = X branch!) * Algebraic simplification (e.g. A*B+A*C = A*(B+C), A+-B=A-B, A--B=A+B, -A+B=B-A, X*X*...*X=N^X) + * 32000. Remove unnecessary scope pushes/pops * * 20. Constant propagation * O2: diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs new file mode 100644 index 000000000..aa95d2579 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.IR; +using kOS.Safe.Exceptions; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class UnnecessaryScopeRemoval : IOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + + public short SortIndex => 32000; + + public void ApplyPass(List code) + { + HashSet scopes = new HashSet(); + HashSet removedScopes = new HashSet(); + foreach (BasicBlock block in code) + scopes.Add(block.Scope); + + foreach (IRScope scope in scopes) + { + if (scope.Variables.Any()) + continue; + + if (scope.IsGlobalScope) + continue; + + CollapseScope(scope); + + RemovePushScope(scope.HeaderBlock); + + RemovePopScope(scope.FooterBlock); + + removedScopes.Add(scope); + } + + scopes.ExceptWith(removedScopes); + foreach (BasicBlock block in code) + { + if (block.Instructions.Count > 0 && + !(block.Instructions.Last() is IRReturn)) + continue; + + AttemptPopReturnCollapse(block); + } + } + + private static void CollapseScope(IRScope scope) + { + IRScope newScope = scope.ParentScope; + foreach (IRScope childScope in scope.Children) + childScope.ParentScope = newScope; + foreach (BasicBlock block in scope.Blocks.ToArray()) + block.Scope = newScope; + scope.ParentScope = null; + } + + private static void RemovePushScope(BasicBlock header) + { + if (header.Instructions[0] is IRNoStackInstruction instruction && + instruction.Operation is OpcodePushScope) + header.Instructions.RemoveAt(0); + else + throw new KOSYouShouldNeverSeeThisException($"The first instruction ({header.Instructions[0]}) in scope header block {header} was not OpcodePushScope."); + } + + private static void RemovePopScope(BasicBlock footer) + { + int lastIndex = footer.Instructions.Count - 1; + IRInstruction lastInstruction = footer.Instructions[lastIndex]; + + if (lastInstruction is IRReturn ret) + { + ret.Depth -= 1; + if (ret.Depth < 0) + throw new KOSYouShouldNeverSeeThisException($"After removing unecessary scopes, the return statement in {footer} had negative depth."); + } + else if (lastInstruction is IRNoStackInstruction popInstruction && + popInstruction.Operation is OpcodePopScope) + { + footer.Instructions.RemoveAt(lastIndex); + } + else + throw new KOSYouShouldNeverSeeThisException($"The last instruction ({lastInstruction}) in scope footer block {footer} was not OpcodePopScope or OpcodeReturn."); + } + + private static void AttemptPopReturnCollapse(BasicBlock returnBlock) + { + // Don't do anything if there is more than one predecessor + int numPredecessors = returnBlock.Predecessors.Count; + if (numPredecessors == 0 || numPredecessors > 1) + return; + + BasicBlock predecessorBlock = returnBlock.Predecessors.First(); + + if (predecessorBlock.Instructions.Last() is IRNoStackInstruction popInstruction && + popInstruction.Operation is OpcodePopScope) + { + predecessorBlock.Instructions.RemoveAt(predecessorBlock.Instructions.Count - 1); + ((IRReturn)returnBlock.Instructions.Last()).Depth += 1; + returnBlock.Scope = predecessorBlock.Scope; + returnBlock.Scope.FooterBlock = returnBlock; + //BasicBlock.Merge(predecessorBlock, returnBlock); + + // Since a pop closes a BasicBlock, then if there are no instructions remaining, + // there's a chance that the pop's predecessor also ends with a pop that we can remove, + // if the pop only has a single predecessor. + if (predecessorBlock.Instructions.Count == 0) + { + AttemptPopReturnCollapse(returnBlock); + } + } + } + } +} From dfaa8ad6afa5b7598d011775a2049e524e83d878 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 12:39:12 -0400 Subject: [PATCH 041/120] Implement scope awareness into IRVariableBase so that variables are compared not just by their string name, but also by their scope. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 5 +++- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 14 +++++++--- src/kOS.Safe/Compilation/IR/IRScope.cs | 16 +++++++++++ src/kOS.Safe/Compilation/IR/IRValue.cs | 28 +++++++++++++------ .../Optimization/Passes/SuffixReplacement.cs | 2 +- 5 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 9d863b96f..651d507ca 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -165,14 +165,17 @@ public bool TryStoreVariable(IRVariableBase variable) => Scope.TryStoreVariable(variable); public IRVariableBase PushVariable(string name, Opcode opcode) { + IRScope globalScope = Scope.GetGlobalScope(); IRVariableBase result = Scope.GetVariable(name); if (result == null) { - result = new IRVariable(name, opcode); + result = new IRVariable(name, globalScope, opcode); Scope.StoreGlobalVariable(result); } return result; } + public IRScope GetScopeForVariableNamed(string name) + => Scope.GetScopeForVariableNamed(name); public void SetStackState(Stack stack) { diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 67566095d..f1b119f90 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -162,23 +162,29 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack Name = name; - public BasicBlock Scope { get; } + public virtual IRScope Scope { get; protected set; } + public IRVariableBase(string name, IRScope declaringScope) + { + Name = name; + Scope = declaringScope; + } } public class IRVariable : IRVariableBase { protected readonly short sourceLine, sourceColumn; public bool IsLock { get; } - public IRVariable(string name, IRInstruction instruction, bool isLock = false) : this(name, instruction.SourceLine, instruction.SourceColumn, isLock) { } - public IRVariable(string name, Opcode opcode, bool isLock = false) : this(name, opcode.SourceLine, opcode.SourceColumn, isLock) { } - public IRVariable(string name, short sourceLine, short sourceColumn, bool isLock = false) : base(name) + public override IRScope Scope { get => base.Scope; } + public IRVariable(string name, IRScope scope, IRInstruction instruction, bool isLock = false) : + this(name, scope, instruction.SourceLine, instruction.SourceColumn, isLock) { } + public IRVariable(string name, IRScope scope, Opcode opcode, bool isLock = false) : + this(name, scope, opcode.SourceLine, opcode.SourceColumn, isLock) { } + public IRVariable(string name, IRScope scope, short sourceLine, short sourceColumn, bool isLock = false) : + base(name, scope) { IsLock = isLock; this.sourceLine = sourceLine; @@ -108,17 +115,20 @@ internal override IEnumerable EmitPush() } public class IRTemp : IRVariableBase { + private bool isPromoted = false; + public int ID { get; } public IRInstruction Parent { get; internal set; } - private bool isPromoted = false; - public IRTemp(int id) : base($"$.temp.{id}") + public IRTemp(int id) : base($"$.temp.{id}", null) { ID = id; + Scope = null; } - public IRAssign PromoteToVariable() + public IRAssign PromoteToVariable(IRScope scope) { isPromoted = true; + Scope = scope; return new IRAssign(new OpcodeStoreLocal(Name) { SourceLine = -1, diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs index 5922a972b..14feaa9f5 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs @@ -101,7 +101,7 @@ private static IRValue AttempReplaceSuffix(IRSuffixGet suffixGet) return suffixGet.Result; } // Instead of the IRTemp, which leads to resolving the suffix, return just the alias shortcut. - suffixGet.Result = new IRVariable($"${suffixGet.Suffix}", suffixGet); + suffixGet.Result = new IRVariable($"${suffixGet.Suffix}", null, suffixGet); return suffixGet.Result; } if (objVariable.Name == "$constant") From bd241434d8cbbe83351710d404ec2fc7e8d8d1da Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 13:53:11 -0400 Subject: [PATCH 042/120] Finish peephole optimization pass by implementing lex indexing shortcuts using suffixing wherever possible. --- .../Optimization/OptimizationTools.cs | 2 +- .../Passes/PeepholeOptimizations.cs | 102 ++++++++++++++---- 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs index 3a17a091e..0c241d449 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs @@ -96,7 +96,7 @@ private static IEnumerable InstructionMatchBreadthFirst(IRInstruc yield return match; } - private static IEnumerable DepthFirst(IRInstruction instruction) + public static IEnumerable DepthFirst(this IRInstruction instruction) { if (instruction is ISingleOperandInstruction singleOperandInstruction) { diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs index ebda09f71..fbe4ae204 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs @@ -10,21 +10,28 @@ public class PeepholeOptimizations : IOptimizationPass public short SortIndex => 1050; - // TODO: Replace lex indexing with string constant with suffixing where possible. public void ApplyPass(List code) { - foreach (IRInstruction instruction in code.DepthFirst()) + for (int i = 0; i < code.Count; i++) { - PeepholeFilter(instruction); + IRInstruction instruction = code[i]; + foreach (IRInstruction nestedInstruction in instruction.DepthFirst()) + PeepholeFilter(nestedInstruction); + + // Replace lex indexing with string constant with suffixing where possible. + if (instruction is IRIndexSet indexSet) + { + IRInstruction potentialResult = AttemptReplaceIndexSetWithSuffixSet(indexSet); + if (potentialResult != null) + code[i] = potentialResult; + } } } private static void PeepholeFilter(IRInstruction instruction) { - // Replace lex indexing using string constant with suffixing where possible - // TODO: this is harder since it requires knowing the object type being indexed against. - // Replace parameterless suffix method calls with get member + // TODO: Skip this if clobber built-ins is active. if (instruction is IRCall suffixCall && !suffixCall.Direct && suffixCall.Arguments.Count == 0 && @@ -35,6 +42,7 @@ suffixCall.IndirectMethod is IRTemp tempSuffixCall && return; } // Replace calls to VectorDotProduct with multiplication + // TODO: Skip this if clobber built-ins is active. if (instruction is IRCall vDotCall && (vDotCall.Function == "vdot" || vDotCall.Function == "vectordotproduct") && vDotCall.Arguments.Count == 2) @@ -220,16 +228,29 @@ opR.Operation is OpcodeMathPower && break; } } - // Redundant unary operation replacements - // E.g. !!X = X and --X = X + if (instruction is ISingleOperandInstruction singleOperandInstruction) { - if (singleOperandInstruction.Operand is IRTemp tempOperand && - tempOperand.Parent is IRUnaryOp unaryParent) + if (singleOperandInstruction.Operand is IRTemp tempOperand) { - IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); - if (potentialResult != null) - singleOperandInstruction.Operand = potentialResult; + // Redundant unary operation replacements + // E.g. !!X = X and --X = X + if (tempOperand.Parent is IRUnaryOp unaryParent) + { + IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); + if (potentialResult != null) + singleOperandInstruction.Operand = potentialResult; + } + // Replace lex indexing using string constant with suffixing where possible + if (tempOperand.Parent is IRIndexGet indexGet && + indexGet.Index is IRConstant indexConstant && + (indexConstant.Value is string || + indexConstant.Value is Encapsulation.StringValue)) + { + IRInstruction potentialResult = AttemptReplaceIndexGetWithSuffixGet(indexGet); + if (potentialResult != null) + tempOperand.Parent = potentialResult; + } } } else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) @@ -237,12 +258,23 @@ opR.Operation is OpcodeMathPower && for (int i = multipleOperandInstruction.OperandCount - 1; i >= 0; i--) { IRValue operand = multipleOperandInstruction[i]; - if (operand is IRTemp tempOperand && - tempOperand.Parent is IRUnaryOp unaryParent) + if (operand is IRTemp tempOperand) { - IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); - if (potentialResult != null) - multipleOperandInstruction[i] = potentialResult; + // Redundant unary operation replacements + // E.g. !!X = X and --X = X + if (tempOperand.Parent is IRUnaryOp unaryParent) + { + IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); + if (potentialResult != null) + multipleOperandInstruction[i] = potentialResult; + } + // Replace lex indexing using string constant with suffixing where possible + if (tempOperand.Parent is IRIndexGet indexGet) + { + IRInstruction potentialResult = AttemptReplaceIndexGetWithSuffixGet(indexGet); + if (potentialResult != null) + tempOperand.Parent = potentialResult; + } } } } @@ -305,6 +337,40 @@ private static IRValue GetDoubleNestedValue(ISingleOperandInstruction operation) return ((IRUnaryOp)((IRTemp)operation.Operand).Parent).Operand; } + private static IRInstruction AttemptReplaceIndexGetWithSuffixGet(IRIndexGet indexGet) + { + if (indexGet.Index is IRConstant indexConstant && + indexConstant.Value is Encapsulation.StringValue stringIndex && + StringUtil.IsValidIdentifier(stringIndex)) + { + return new IRSuffixGet((IRTemp)indexGet.Result, + indexGet.Object, + new OpcodeGetMember(stringIndex) + { + SourceLine = indexGet.SourceLine, + SourceColumn = indexGet.SourceColumn + }); + } + return null; + } + + private static IRInstruction AttemptReplaceIndexSetWithSuffixSet(IRIndexSet indexSet) + { + if (indexSet.Index is IRConstant indexConstant && + indexConstant.Value is Encapsulation.StringValue stringIndex && + StringUtil.IsValidIdentifier(stringIndex)) + { + return new IRSuffixSet(indexSet.Object, + indexSet.Value, + new OpcodeSetMember(stringIndex) + { + SourceLine = indexSet.SourceLine, + SourceColumn = indexSet.SourceColumn + }); + } + return null; + } + private static void ReplaceRedundantNotBranch(IRBranch branch) { branch.Condition = GetDoubleNestedValue(branch); From 281d3ae15269cde6979b4828fd52c9fb875accd6 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 13:53:49 -0400 Subject: [PATCH 043/120] Clean up and restructure IRCodePart and its child classes. --- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 26 +++++++++++-------- .../Passes/DeadCodeElimination.cs | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index a1877f8af..1623ed31d 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -31,7 +31,7 @@ public IRCodePart(CodePart codePart, List userFunctions) Blocks.AddRange(function.InitializationCode); if (function.InitializationCode.Count > 0) RootBlocks.Add(function.InitializationCode[0]); - foreach (IRFunction.IRFunctionFragment fragment in function.Fragments.Values) + foreach (IRFunction.IRFunctionFragment fragment in function.Fragments) { Blocks.AddRange(fragment.FunctionCode); if (fragment.FunctionCode.Count > 0) @@ -53,35 +53,39 @@ public void EmitCode(CodePart codePart) public class IRFunction { private readonly UserFunction function; + private readonly List userFunctionFragments; + + public string Identifier => function.Identifier; + public List InitializationCode { get; set; } + private readonly Dictionary fragments = new Dictionary(); + public IReadOnlyCollection Fragments => fragments.Values; + public IRFunction(IRBuilder builder, UserFunction function) { this.function = function; InitializationCode = builder.Lower(function.InitializationCode); - fragments = function.PeekNewCodeFragments().ToList(); - foreach (UserFunctionCodeFragment fragment in fragments) + userFunctionFragments = function.PeekNewCodeFragments().ToList(); + foreach (UserFunctionCodeFragment fragment in userFunctionFragments) { - Fragments.Add(fragment, new IRFunctionFragment(builder, fragment)); + fragments.Add(fragment, new IRFunctionFragment(builder, fragment)); } - fragments.Reverse(); + userFunctionFragments.Reverse(); } public void EmitCode(IREmitter emitter) { function.InitializationCode.Clear(); function.InitializationCode.AddRange(emitter.Emit(InitializationCode)); - foreach (UserFunctionCodeFragment fragment in fragments) + foreach (UserFunctionCodeFragment fragment in userFunctionFragments) { - Fragments[fragment].EmitCode(emitter); + fragments[fragment].EmitCode(emitter); } } - public List InitializationCode { get; set; } - public Dictionary Fragments { get; } = new Dictionary(); - private readonly List fragments; public class IRFunctionFragment { - public List FunctionCode { get; set; } private readonly UserFunctionCodeFragment fragment; + public List FunctionCode { get; set; } public IRFunctionFragment(IRBuilder builder, UserFunctionCodeFragment codeFragment) { fragment = codeFragment; diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs b/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs index 02fbe38f8..5e6255cf3 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs @@ -18,7 +18,7 @@ public void ApplyPass(IRCodePart code) foreach (IRCodePart.IRFunction function in code.Functions) { RemoveDeadBlocks(function.InitializationCode, rootBlocks); - foreach (IRCodePart.IRFunction.IRFunctionFragment fragment in function.Fragments.Values) + foreach (IRCodePart.IRFunction.IRFunctionFragment fragment in function.Fragments) { RemoveDeadBlocks(fragment.FunctionCode, rootBlocks); } From 5040e4643eedb3c1a34e5ec6d58b6acb6d98c5fe Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 20:12:55 -0400 Subject: [PATCH 044/120] Fix opcodebranch where the jump is relative instead of a label. Fix exceptions when storing a variable defined globally in another scope. --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index f1b119f90..a1e87aa6f 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -169,7 +169,9 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack Date: Thu, 12 Mar 2026 20:13:55 -0400 Subject: [PATCH 045/120] Fix IRCodePart behaviour for triggers. It turns out they aren't just special functions! --- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 32 ++++++++++++++++++- src/kOS.Safe/Compilation/KS/Compiler.cs | 2 +- .../Compilation/KS/TriggerCollection.cs | 3 ++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index 1623ed31d..515a2a423 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -9,10 +9,11 @@ public class IRCodePart { public List MainCode { get; set; } public List Functions { get; set; } + public List Triggers { get; set; } public List RootBlocks { get; } = new List(); public List Blocks { get; } = new List(); - public IRCodePart(CodePart codePart, List userFunctions) + public IRCodePart(CodePart codePart, List userFunctions, List triggers) { if (codePart.InitializationCode.Count > 0) throw new ArgumentException($"{nameof(codePart)} has initialization code and is structured unexpectedly."); @@ -22,10 +23,17 @@ public IRCodePart(CodePart codePart, List userFunctions) IRBuilder builder = new IRBuilder(); MainCode = builder.Lower(codePart.MainCode); Functions = userFunctions.Select(f => new IRFunction(builder, f)).ToList(); + Triggers = triggers.Select(t => new IRTrigger(builder, t)).ToList(); Blocks.AddRange(MainCode); if (MainCode.Count > 0) RootBlocks.Add(MainCode[0]); + foreach (IRTrigger trigger in Triggers) + { + Blocks.AddRange(trigger.Code); + if (trigger.Code.Count > 0) + RootBlocks.Add(trigger.Code[0]); + } foreach (IRFunction function in Functions) { Blocks.AddRange(function.InitializationCode); @@ -43,6 +51,10 @@ public IRCodePart(CodePart codePart, List userFunctions) public void EmitCode(CodePart codePart) { IREmitter emitter = new IREmitter(); + foreach (IRTrigger trigger in Triggers) + { + trigger.EmitCode(emitter); + } foreach (IRFunction function in Functions) { function.EmitCode(emitter); @@ -50,6 +62,24 @@ public void EmitCode(CodePart codePart) codePart.MainCode = emitter.Emit(MainCode); } + public class IRTrigger + { + private readonly Trigger trigger; + public string Identifier { get; } + public List Code { get; set; } + public IRTrigger(IRBuilder builder, Trigger trigger) + { + this.trigger = trigger; + Identifier = trigger.Code.FirstOrDefault()?.Label ?? ""; + Code = builder.Lower(trigger.Code); + } + public void EmitCode(IREmitter emitter) + { + trigger.Code.Clear(); + trigger.Code.AddRange(emitter.Emit(Code)); + } + } + public class IRFunction { private readonly UserFunction function; diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index 0df55690b..564fd51c1 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -212,7 +212,7 @@ public CodePart Compile(int startLineNum, ParseTree tree, Context context, Compi public static void Optimize(CodePart code, Context context, CompilerOptions options) { - IR.IRCodePart irCodePart = new IR.IRCodePart(code, context.UserFunctions.PeekNewFunctions()); + IR.IRCodePart irCodePart = new IR.IRCodePart(code, context.UserFunctions.PeekNewFunctions(), context.Triggers.PeekNewParts()); Optimization.Optimizer optimizer = new Optimization.Optimizer(options.OptimizationLevel); optimizer.Optimize(irCodePart); irCodePart.EmitCode(code); diff --git a/src/kOS.Safe/Compilation/KS/TriggerCollection.cs b/src/kOS.Safe/Compilation/KS/TriggerCollection.cs index fb9de153b..82181e396 100644 --- a/src/kOS.Safe/Compilation/KS/TriggerCollection.cs +++ b/src/kOS.Safe/Compilation/KS/TriggerCollection.cs @@ -35,6 +35,9 @@ public List GetParts() return GetParts(triggers.Values.ToList()); } + + internal List PeekNewParts() + => newTriggers; public IEnumerable GetNewParts() { List parts = GetParts(newTriggers); From 04ca8ce033841b8a25c326ce00533a02fd32ab06 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 13 Mar 2026 16:47:25 -0400 Subject: [PATCH 046/120] Harmonize IRVariableBase and subclass equality, hashing, and string output. --- src/kOS.Safe/Compilation/IR/IRScope.cs | 12 +++++------ src/kOS.Safe/Compilation/IR/IRValue.cs | 29 ++++++++++++++++---------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRScope.cs b/src/kOS.Safe/Compilation/IR/IRScope.cs index 3530a4f23..39f68e738 100644 --- a/src/kOS.Safe/Compilation/IR/IRScope.cs +++ b/src/kOS.Safe/Compilation/IR/IRScope.cs @@ -108,14 +108,12 @@ public void RemoveBlock(BasicBlock block) => blocks.Remove(block); public override string ToString() + => $"IRScope: {IndexString()}"; + public string IndexString() { - if (IsGlobalScope) - return "IRScope: Global"; - return $"IRScope: {IndexString()}"; - } - private string IndexString() - { - if (ParentScope == null) + if (IsGlobalScope || ParentScope == null) + return "Global"; + if (ParentScope.IsGlobalScope) return $"{index}"; return $"{ParentScope.IndexString()}.{index}"; } diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index d6fdd80ff..d138d2519 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -50,6 +50,16 @@ public IRVariableBase(string name, IRScope declaringScope) Name = name; Scope = declaringScope; } + public override string ToString() + => $"{Name} {Scope.IndexString()}"; + public override bool Equals(object obj) + => obj is IRVariableBase variable && + (Scope == variable.Scope || + Scope.IsEncompassedBy(variable.Scope) || + variable.Scope.IsEncompassedBy(Scope)) && + string.Equals(Name, variable.Name, System.StringComparison.OrdinalIgnoreCase); + public override int GetHashCode() + => Name.ToLower().GetHashCode(); } public class IRVariable : IRVariableBase { @@ -75,14 +85,6 @@ internal override IEnumerable EmitPush() SourceColumn = sourceColumn }; } - public override string ToString() - => Name; - public override bool Equals(object obj) - => obj is IRVariable variable && - Scope == variable.Scope && - string.Equals(Name, variable.Name, System.StringComparison.OrdinalIgnoreCase); - public override int GetHashCode() - => Name.ToLower().GetHashCode(); } public class IRRelocateLater : IRConstant { @@ -165,13 +167,18 @@ public override bool Equals(object obj) if (obj is IRVariable variable) { return isPromoted && - Scope == variable.Scope && - string.Equals(Name, variable.Name, System.StringComparison.OrdinalIgnoreCase); + base.Equals(variable); } return false; } + public override string ToString() + { + if (isPromoted) + return base.ToString(); + return $"| {Parent}"; + } public override int GetHashCode() - => Name.ToLower().GetHashCode(); + => base.GetHashCode(); } public class IRParameter : IRValue { From 58f6e52a1a0d6998b69c22024c821bfe736f1115 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 13 Mar 2026 16:53:48 -0400 Subject: [PATCH 047/120] Make IRAssignment more strict by knowing the exact variable (with scope) that it produces. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 2 +- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 30 +++++++++++--------- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 25 +++++++--------- src/kOS.Safe/Compilation/IR/IRScope.cs | 27 ++++++++++++++++-- src/kOS.Safe/Compilation/IR/IRValue.cs | 6 +++- 5 files changed, 58 insertions(+), 32 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 651d507ca..b3d6cec95 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -166,7 +166,7 @@ public bool TryStoreVariable(IRVariableBase variable) public IRVariableBase PushVariable(string name, Opcode opcode) { IRScope globalScope = Scope.GetGlobalScope(); - IRVariableBase result = Scope.GetVariable(name); + IRVariableBase result = Scope.GetVariableNamed(name); if (result == null) { result = new IRVariable(name, globalScope, opcode); diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index a1e87aa6f..5951b460f 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -161,32 +161,36 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack false; - public string Target { get; set; } + public IRVariableBase Target { get; set; } public IRValue Value { get; set; } public StoreScope Scope { get; set; } = StoreScope.Ambivalent; public bool AssertExists { get; set; } = false; IRValue ISingleOperandInstruction.Operand { get => Value; set => Value = value; } - public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : base(opcode) + public IRAssign(OpcodeIdentifierBase opcode, IRVariableBase target, IRValue value) : base(opcode) { - Target = opcode.Identifier; + Target = target; Value = value; } internal override IEnumerable EmitOpcode() @@ -79,34 +79,31 @@ internal override IEnumerable EmitOpcode() yield return opcode; if (AssertExists) { - yield return SetSourceLocation(new OpcodeStoreExist(Target)); + yield return SetSourceLocation(new OpcodeStoreExist(Target.Name)); yield break; } switch (Scope) { case StoreScope.Local: - yield return SetSourceLocation(new OpcodeStoreLocal(Target)); + yield return SetSourceLocation(new OpcodeStoreLocal(Target.Name)); yield break; case StoreScope.Global: - yield return SetSourceLocation(new OpcodeStoreGlobal(Target)); + yield return SetSourceLocation(new OpcodeStoreGlobal(Target.Name)); yield break; default: case StoreScope.Ambivalent: - yield return SetSourceLocation(new OpcodeStore(Target)); + yield return SetSourceLocation(new OpcodeStore(Target.Name)); yield break; } } public override string ToString() => string.Format("{{store {0}}}", Value.ToString()); public override bool Equals(object obj) - { - if (obj is IRAssign assignment) - return string.Equals(Target, assignment.Target, System.StringComparison.OrdinalIgnoreCase) && - Value.Equals(assignment.Value); - return base.Equals(obj); - } + => obj is IRAssign assignment && + Target.Equals(assignment.Target) && + Value.Equals(assignment.Value); public override int GetHashCode() - => Target.ToLower().GetHashCode(); + => Target.GetHashCode(); } public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { diff --git a/src/kOS.Safe/Compilation/IR/IRScope.cs b/src/kOS.Safe/Compilation/IR/IRScope.cs index 39f68e738..4437d0c48 100644 --- a/src/kOS.Safe/Compilation/IR/IRScope.cs +++ b/src/kOS.Safe/Compilation/IR/IRScope.cs @@ -35,7 +35,8 @@ public IRScope ParentScope public IReadOnlyCollection Blocks => blocks; public BasicBlock HeaderBlock { get; set; } public BasicBlock FooterBlock { get; set; } - public IReadOnlyCollection Variables => variables.Keys; + public IReadOnlyCollection Variables => variables.Values; + public IReadOnlyCollection VariableNames => variables.Keys; public bool IsGlobalScope { get; internal set; } = false; public IRScope(IRScope parent) @@ -69,11 +70,11 @@ public bool TryStoreVariable(IRVariableBase variable) return ParentScope?.TryStoreVariable(variable) ?? false; } - public IRVariableBase GetVariable(string name) + public IRVariableBase GetVariableNamed(string name) { if (variables.ContainsKey(name)) return variables[name]; - return ParentScope?.GetVariable(name); + return ParentScope?.GetVariableNamed(name); } public bool IsVariableInScope(string name) @@ -82,6 +83,10 @@ public bool IsVariableInScope(string name) return true; return ParentScope?.IsVariableInScope(name) ?? false; } + public bool IsVariableInScope(IRVariableBase variable) + { + return variable.Scope.IsEncompassedBy(this); + } public IRScope GetScopeForVariableNamed(string name) { @@ -92,6 +97,22 @@ public IRScope GetScopeForVariableNamed(string name) return ParentScope.GetScopeForVariableNamed(name); } + public void ClearVariable(string name) + { + variables.Remove(name); + } + public void ClearVariable(IRVariableBase variable) + => ClearVariable(variable.Name); + + public bool IsEncompassedBy(IRScope scope) + { + if (scope.IsGlobalScope) + return true; + if (scope.childScopes.Contains(this)) + return true; + return ParentScope?.IsEncompassedBy(scope) ?? false; + } + public IRScope GetGlobalScope() { if (IsGlobalScope) diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index d138d2519..cf66475e0 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -68,6 +68,8 @@ public class IRVariable : IRVariableBase public override IRScope Scope { get => base.Scope; } public IRVariable(string name, IRScope scope, IRInstruction instruction, bool isLock = false) : this(name, scope, instruction.SourceLine, instruction.SourceColumn, isLock) { } + public IRVariable(OpcodeIdentifierBase opcode, IRScope scope, bool isLock = false) : + this(opcode.Identifier, scope, opcode, isLock) { } public IRVariable(string name, IRScope scope, Opcode opcode, bool isLock = false) : this(name, scope, opcode.SourceLine, opcode.SourceColumn, isLock) { } public IRVariable(string name, IRScope scope, short sourceLine, short sourceColumn, bool isLock = false) : @@ -135,7 +137,9 @@ public IRAssign PromoteToVariable(IRScope scope) { SourceLine = -1, SourceColumn = 0 - }, this) + }, + this, + this) { Scope = IRAssign.StoreScope.Local }; From 898bd3afea3f8d1b4325f868b452636249a5787e Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 13 Mar 2026 16:54:18 -0400 Subject: [PATCH 048/120] Improve debug string readability. --- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index c926d5efd..b0477d47e 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -97,7 +97,7 @@ internal override IEnumerable EmitOpcode() } } public override string ToString() - => string.Format("{{store {0}}}", Value.ToString()); + => string.Format("{{store {0} -> {1}}}", Value.ToString(), Target.ToString()); public override bool Equals(object obj) => obj is IRAssign assignment && Target.Equals(assignment.Target) && @@ -283,7 +283,7 @@ internal override IEnumerable EmitOpcode() yield return SetSourceLocation(new OpcodePop()); } public override string ToString() - => "{pop}"; + => $"{{pop {Value}}}"; public override bool Equals(object obj) => obj is IRPop pop && Value.Equals(pop.Value); public override int GetHashCode() From 5b420330fa388f3144208deef32e42d8c987071e Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 13 Mar 2026 16:54:42 -0400 Subject: [PATCH 049/120] Fix collection changed while enumerating exception. --- .../Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs index aa95d2579..e33a78bb5 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs @@ -50,7 +50,7 @@ public void ApplyPass(List code) private static void CollapseScope(IRScope scope) { IRScope newScope = scope.ParentScope; - foreach (IRScope childScope in scope.Children) + foreach (IRScope childScope in scope.Children.ToArray()) childScope.ParentScope = newScope; foreach (BasicBlock block in scope.Blocks.ToArray()) block.Scope = newScope; From 023de41c7440f260a5ea400039d3a7fe0bf5a96d Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 13 Mar 2026 17:17:36 -0400 Subject: [PATCH 050/120] Fix infinite loop when dumping the tree where there is a loop contained. --- .../Optimization/ExtendedBasicBlock.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs b/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs index 53dbad6a9..1456d26a8 100644 --- a/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs +++ b/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs @@ -38,7 +38,22 @@ public static ExtendedBasicBlock CreateExtendedBlockTree(BasicBlock root) return resultBlock; } public static IEnumerable DumpTree(ExtendedBasicBlock root) - => Enumerable.Repeat(root, 1).Concat(root.sucessors.SelectMany(DumpTree)); + { + HashSet visited = new HashSet(); + return DumpTree(root, visited); + } + private static IEnumerable DumpTree(ExtendedBasicBlock root, HashSet visited) + { + yield return root; + foreach (ExtendedBasicBlock successor in root.Sucessors) + { + if (visited.Add(successor)) + { + foreach (ExtendedBasicBlock further in DumpTree(successor, visited)) + yield return further; + } + } + } public void AddSuccessor(ExtendedBasicBlock successor) { From c59ce691dbc69558528a83bb407d8b002394422b Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 14 Mar 2026 00:24:33 -0400 Subject: [PATCH 051/120] Fix equality not working between constants. --- src/kOS.Safe/Compilation/IR/IRValue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index cf66475e0..5fe141e9b 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -35,7 +35,7 @@ internal override IEnumerable EmitPush() }; } public override bool Equals(object obj) - => Value.Equals(obj); + => obj is IRConstant constant && Value.Equals(constant.Value) || Value.Equals(obj); public override int GetHashCode() => Value.GetHashCode(); public override string ToString() From b59a7b0e8747c766f916ea38bea3e5db77c71af7 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 14 Mar 2026 00:26:33 -0400 Subject: [PATCH 052/120] Add constant propagation pass. This pass replaces local variables with constants wherever it can. Variables that are global, or that are written to within a LOCK statement are left alone, and variables that are written to in a function are not replaced after the first call to that function, unless subsequently reset. This works across the reaching definition, although the current implementation is a bit messy. --- .../integration/constantPropagation.ks | 63 +++ .../Execution/OptimizationTest.cs | 28 + .../Optimization/OptimizationLevel.cs | 4 +- .../Passes/ConstantPropagation.cs | 508 ++++++++++++++++++ 4 files changed, 602 insertions(+), 1 deletion(-) create mode 100644 kerboscript_tests/integration/constantPropagation.ks create mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs diff --git a/kerboscript_tests/integration/constantPropagation.ks b/kerboscript_tests/integration/constantPropagation.ks new file mode 100644 index 000000000..a44ed8106 --- /dev/null +++ b/kerboscript_tests/integration/constantPropagation.ks @@ -0,0 +1,63 @@ +@lazyGlobal OFF. + +global a is 5. +global _false is false. + +local b is 2. +local c is 4. +local d is 7. +local h is false. + +print("test"). // The assignments should happen after this line. + +print(b+c). // 6 +print(h). // False + +global function func { + print(b+c). // 6 + print(b+d). // 9 + print(b+a). // 7 + set h to true. +} + +func(). + +{ + local i is 10. + print(i). // 10 + print(i+c). // 14 +} + +print(h). // True + +set h to false. + +print(h). // False + +lock e to a + b. + +print(e). // 7 +set b to 1. +print(e). // 6 + +lock g to c + d. +print(g). // 11 +set d to 5. +print(g). // 9 + +when _false then { + set c to 10. +} + +print(b+c). // 5 + +set b to 1. +set b to b + d. + +print(b). // 6 + +until b = 1 { + set b to b - 1. +} + +print(b). // 1 diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index d35a9783e..4216b0a5f 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -178,5 +178,33 @@ public void TestPeepholeOptimizations() "512" ); } + + + [Test] + public void TestConstantPropagation() + { + // Test that local constant variables propagate + RunScript("integration/constantPropagation.ks"); + RunSingleStep(); + AssertOutput( + "test", + "6", + "False", + "6", + "9", + "7", + "10", + "14", + "True", + "False", + "7", + "6", + "11", + "9", + "5", + "6", + "1" + ); + } } } diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index 19e108481..474db720b 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -7,6 +7,7 @@ namespace kOS.Safe.Compilation * 10. Suffix replacement: * Replace CONSTANT: values with the constant * Replace ship fields with their alias + * 20. Constant propagation * 30. Constant folding * 50. Dead code elimination * 1050. Peephole optimizations: @@ -19,7 +20,6 @@ namespace kOS.Safe.Compilation * Algebraic simplification (e.g. A*B+A*C = A*(B+C), A+-B=A-B, A--B=A+B, -A+B=B-A, X*X*...*X=N^X) * 32000. Remove unnecessary scope pushes/pops * - * 20. Constant propagation * O2: * 1000. Common expression elimination * Particularly: Any expression of 3 opcodes used more than twice, @@ -29,10 +29,12 @@ namespace kOS.Safe.Compilation * Loop jamming? (Combining adjacent loops into one) * Unswitching (moving conditional evaluation outside the loop) - low priority * Linear function test replacement - low priority + * * O3: * 3100. Local function inlining * 3200. Constant propagation to local functions * 3500. Loop stack manipulation (delayed setting of either index or aggregator) + * * O4: * 4000. Constant loop unrolling */ diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs new file mode 100644 index 000000000..7d04469f5 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs @@ -0,0 +1,508 @@ +using kOS.Safe.Compilation.IR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class ConstantPropagation : IHolisticOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + + public short SortIndex => 20; + + public void ApplyPass(IRCodePart codePart) + { + Dictionary> funcExternalWrites = new Dictionary>(); + Dictionary> funcExternalReads = new Dictionary>(); + + // For each function, find the sets of non-local variables read and those written to. + foreach (IRCodePart.IRFunction function in codePart.Functions) + { + HashSet externalWrites = new HashSet(); + HashSet externalReads = new HashSet(); + foreach (IRCodePart.IRFunction.IRFunctionFragment fragment in function.Fragments) + { + AnalyzeFunction(fragment.FunctionCode, externalWrites, externalReads); + } + funcExternalWrites[function.Identifier] = externalWrites; + funcExternalReads[function.Identifier] = externalReads; + } + foreach (IRCodePart.IRTrigger trigger in codePart.Triggers) + { + HashSet externalWrites = new HashSet(); + HashSet externalReads = new HashSet(); + AnalyzeFunction(trigger.Code, externalWrites, externalReads); + funcExternalWrites[trigger.Identifier] = externalWrites; + funcExternalReads[trigger.Identifier] = externalReads; + } + + Dictionary> variablesPropagated = new Dictionary>(); + Dictionary> variablesUnpropagated = new Dictionary>(); + + Dictionary inOutData = new Dictionary(); + Dictionary storedFuncs = new Dictionary(); + + foreach (BasicBlock rootBlock in codePart.RootBlocks) + { + Dictionary map = MapIncoming(rootBlock, + funcExternalWrites, + out Dictionary storedFunctions); + foreach (KeyValuePair pair in map) + inOutData.Add(pair.Key, pair.Value); + foreach (KeyValuePair pair in storedFunctions) + storedFuncs.Add(pair.Key, pair.Value); + } + + foreach (BasicBlock block in codePart.Blocks) + { + if (block.Scope.IsGlobalScope) + PropagateWithinBlock(block, inOutData, + storedFuncs, + new HashSet(), + variablesUnpropagated, + funcExternalReads, + funcExternalWrites); + else + { + if (!variablesPropagated.ContainsKey(block.Scope)) + { + variablesPropagated.Add(block.Scope, new HashSet()); + variablesUnpropagated.Add(block.Scope, new HashSet()); + } + PropagateWithinBlock(block, inOutData, + storedFuncs, + variablesPropagated[block.Scope], + variablesUnpropagated, + funcExternalReads, + funcExternalWrites); + } + } + + foreach (IRScope scope in variablesPropagated.Keys) + { + if (variablesUnpropagated.ContainsKey(scope)) + variablesPropagated[scope].ExceptWith(variablesUnpropagated[scope]); + foreach (IRVariableBase variable in variablesPropagated[scope]) + scope.ClearVariable(variable); + } + } + + private static void AnalyzeFunction(List functionCode, HashSet writes, HashSet reads) + { + if (functionCode.Any()) + reads.UnionWith(functionCode.First().Scope.GetGlobalScope().Variables); + foreach (BasicBlock block in functionCode) + { + foreach (IRInstruction instruction in block.Instructions) + { + if (instruction is IRAssign assignment && + assignment.Target.Scope.IsGlobalScope) + writes.Add(assignment.Target); + } + } + } + + private static void RescopeFunctionVars(HashSet variables, IRScope scope) + { + HashSet temp = new HashSet(); + foreach(IRVariableBase variable in variables) + { + if (scope.IsVariableInScope(variable.Name)) + temp.Add(scope.GetVariableNamed(variable.Name)); + else + temp.Add(variable); + } + variables.Clear(); + variables.UnionWith(temp); + } + + private static Dictionary MapIncoming(BasicBlock rootBlock, + Dictionary> funcExternalWrites, + out Dictionary storedFunctions) + { + storedFunctions = new Dictionary(); + + HashSet GetValueOrDefault(Dictionary> dict, BasicBlock key, IEqualityComparer comparer) + { + if (!dict.ContainsKey(key)) + { + if (comparer == null) + comparer = EqualityComparer.Default; + HashSet result = new HashSet(comparer); + dict.Add(key, result); + return result; + } + return dict[key]; + } + + Dictionary> incoming = new Dictionary>(); + Dictionary> blacklistIn = new Dictionary>(); + + Dictionary> outgoing = new Dictionary>(); + Dictionary> blacklistOut = new Dictionary>(); + Dictionary> defs = new Dictionary>(); + + HashSet basicBlocks = new HashSet(); + Queue worklist = new Queue(); + worklist.Enqueue(rootBlock); + + while (worklist.Count > 0) + { + BasicBlock block = worklist.Dequeue(); + basicBlocks.Add(block); + + if (!defs.ContainsKey(block)) + { + ParseDefs(block, + out Dictionary defsMade, + GetValueOrDefault(blacklistOut, block, null), + storedFunctions, + funcExternalWrites); + defs[block] = new HashSet<(IRVariableBase variable, IRAssign value)>(defsMade.Select(kvp => (kvp.Key, kvp.Value)), VariableTupleEqualityComparer.Instance); + } + + incoming.Remove(block); + HashSet<(IRVariableBase variable, IRAssign value)> defsIn = GetValueOrDefault(incoming, block, VariableTupleEqualityComparer.Instance); + blacklistIn.Remove(block); + HashSet localBlacklist = GetValueOrDefault(blacklistIn, block, null); + BasicBlock firstPredecessor = block.Predecessors.FirstOrDefault(); + if (firstPredecessor != null) + { + defsIn.UnionWith(GetValueOrDefault(outgoing, firstPredecessor, VariableTupleEqualityComparer.Instance)); + localBlacklist.UnionWith(GetValueOrDefault(blacklistOut, firstPredecessor, null)); + } + + foreach (BasicBlock predecessor in block.Predecessors.Skip(1)) + { + if (!outgoing.ContainsKey(predecessor)) + continue; + defsIn.IntersectWith(GetValueOrDefault(outgoing, predecessor, VariableTupleEqualityComparer.Instance)); + localBlacklist.UnionWith(GetValueOrDefault(blacklistOut, predecessor, null)); + } + + defsIn.RemoveWhere(vv => !(vv.variable.Scope == block.Scope || block.Scope.IsEncompassedBy(vv.variable.Scope))); + defsIn.RemoveWhere(vv => localBlacklist.Contains(vv.variable)); + + HashSet<(IRVariableBase variable, IRAssign value)> defsOut = new HashSet<(IRVariableBase variable, IRAssign value)>(defsIn, VariableTupleEqualityComparer.Instance); + HashSet<(IRVariableBase variable, IRAssign value)> definitions = defs[block]; + + defsOut.UnionWith(defsIn); + foreach (var def in definitions) + { + defsOut.RemoveWhere(vv => vv.variable.Equals(def.variable)); + defsOut.Add(def); + } + defsOut.RemoveWhere(vv => localBlacklist.Contains(vv.variable)); + + if (outgoing.ContainsKey(block) && defsOut.SetEquals(outgoing[block])) + continue; + + outgoing[block] = defsOut; + blacklistOut[block].UnionWith(localBlacklist); + foreach (BasicBlock successor in block.Successors) + worklist.Enqueue(successor); + } + + Dictionary map = new Dictionary(); + foreach (BasicBlock block in basicBlocks) + map.Add(block, new BlockDictionaries(incoming[block], blacklistIn[block])); + + return map; + } + + private static void ParseDefs(BasicBlock block, + out Dictionary defs, + HashSet blacklist, + Dictionary storedFunctions, + Dictionary> funcExternalWrites) + { + defs = new Dictionary(); + List instructions = block.Instructions; + for (int i = 0; i < block.Instructions.Count; i++) + { + IRInstruction instruction = instructions[i]; + // Step through instructions (depth first) and replace variables + // with their cached constant whenever they're available. + foreach (IRInstruction nestedInstruction in instruction.DepthFirst()) + { + RemoveVarsWrittenInFunction(nestedInstruction, + block.Scope, + defs, + storedFunctions, + funcExternalWrites); + } + + switch (instruction) + { + // On a local assignment by a constant, cache that constant against the variable. + case IRAssign assignment: + if (!blacklist.Contains(assignment.Target) && + !assignment.Target.Scope.IsGlobalScope) + { + defs[assignment.Target] = assignment; + } + + // On encountering a PushRelocateLater, note the scope for its closure variables. + if (assignment.Value is IRRelocateLater lockOrFunctionPointer) + { + string pointer = ((string)lockOrFunctionPointer.Value).Split('-').First(); + storedFunctions[assignment.Target] = pointer; + // Re-scope the stored variables from Global to the current scope. + RescopeFunctionVars(funcExternalWrites[pointer], block.Scope); + } + break; + + // On encountering a trigger: + // Clear the cache of any variables that are written in that trigger or body. + // Blacklist any variables that are written in the trigger or body. + case IRUnaryConsumer trigger: + if (trigger.Operation is OpcodeAddTrigger) + { + string destination = (string)((IRConstant)trigger.Operand).Value; + if (funcExternalWrites.ContainsKey(destination)) + { + foreach (IRVariableBase variable in funcExternalWrites[destination]) + defs.Remove(variable); + + // Re-scope the stored variables from Global to the current scope. + RescopeFunctionVars(funcExternalWrites[destination], block.Scope); + blacklist.UnionWith(funcExternalWrites[destination]); + } + } + break; + } + } + } + + private static void RemoveVarsWrittenInFunction(IRInstruction instr, + IRScope scope, + Dictionary defsCreated, + Dictionary storedFunctions, + Dictionary> funcExternalWrites) + { + if (instr is IRCall call) + { + // On encountering a Call, where that identifier is in our dictionary of function closure variables. + // Inject the assignment call to any constant variables that are read inside the function. + // Clear the cache of any variables that are written in that function. + IRVariableBase functionCall = scope.GetVariableNamed(call.Function); + if (functionCall != null && storedFunctions.ContainsKey(functionCall)) + { + string funcRef = storedFunctions[functionCall]; + + foreach (IRVariableBase variable in funcExternalWrites[funcRef]) + defsCreated.Remove(variable); + } + } + } + + private static void PropagateWithinBlock(BasicBlock block, + Dictionary inOutData, + Dictionary storedFunctions, + HashSet scopeVarsPropagated, + Dictionary> variablesUnpropagated, + Dictionary> externalVariablesRead, + Dictionary> externalVariablesWritten) + { + BlockDictionaries localInOut = inOutData[block]; + + List instructions = block.Instructions; + + inOutData[block].InitializeForPropagation( + out Dictionary constantCache, + out Dictionary assignmentInstructions, + out HashSet blacklist); + + for (int i = 0; i < instructions.Count; i++) + { + IRInstruction instruction = instructions[i]; + // Step through instructions (depth first) and replace variables + // with their cached constant whenever they're available. + foreach (IRInstruction nestedInstruction in instruction.DepthFirst()) + { + AttemptPropagation(nestedInstruction, + instructions, + ref i, + block.Scope, + constantCache, + storedFunctions, + externalVariablesRead, + externalVariablesWritten,assignmentInstructions, + variablesUnpropagated); + } + + switch (instruction) + { + // On a local assignment by a constant, cache that constant against the variable. + case IRAssign assignment: + if (!blacklist.Contains(assignment.Target) && + !assignment.Target.Scope.IsGlobalScope) + { + if (assignment.Value is IRTemp temp) + assignment.Value = ConstantFolding.AttemptReduction(temp.Parent); + if (assignment.Value is IRConstant constantValue) + { + scopeVarsPropagated.Add(assignment.Target); + instructions.RemoveAt(i); + assignmentInstructions[assignment.Target] = assignment; + constantCache[assignment.Target] = constantValue; + i--; + } + else + { + scopeVarsPropagated.Remove(assignment.Target); + assignmentInstructions.Remove(assignment.Target); + constantCache.Remove(assignment.Target); + } + } + + // On encountering a PushDelegateRelocateLater, note which variables are cached. + // Store the intersections of that set and the function variable sets. + else if (assignment.Value is IRDelegateRelocateLater functionPointer) + { + string pointer = ((string)functionPointer.Value).Split('-').First(); + storedFunctions[assignment.Target] = pointer; + // Re-scope the stored variables from Global to the current scope. + // Writes were accomplished on the first pass. + RescopeFunctionVars(externalVariablesRead[pointer], block.Scope); + } + + // On encountering a Lock call, we will:: + // Inject the assignment call to any constant variables that are read inside the function. + // Clear the cache of any variables that are written in that lock. + else if (assignment.Value is IRRelocateLater lockPointer) + { + string pointer = ((string)lockPointer.Value).Split('-').First(); + storedFunctions[assignment.Target] = pointer; + // Re-scope the stored variables from Global to the current scope. + // Writes were accomplished on the first pass. + RescopeFunctionVars(externalVariablesRead[pointer], block.Scope); + } + break; + + // On encountering a trigger: + // Inject the assignment call to any constant variables that are read inside the trigger or body. + // Clear the cache of any variables that are written in that trigger or body. + // Blacklist any variables that are written in the trigger or body. + case IRUnaryConsumer trigger: + if (trigger.Operation is OpcodeAddTrigger) + { + string destination = (string)((IRConstant)trigger.Operand).Value; + if (externalVariablesRead.ContainsKey(destination)) + { + foreach (IRVariableBase variable in externalVariablesRead[destination].Union( + externalVariablesWritten[destination])) + if (assignmentInstructions.ContainsKey(variable)) + { + instructions.Insert(i++, assignmentInstructions[variable]); + assignmentInstructions.Remove(variable); + variablesUnpropagated[variable.Scope].Add(variable); + } + + foreach (IRVariableBase variable in externalVariablesWritten[destination]) + constantCache.Remove(variable); + + blacklist.UnionWith(externalVariablesWritten[destination]); + } + } + break; + } + } + } + + private static void AttemptPropagation(IRInstruction instruction, + List instructions, + ref int i, + IRScope scope, + Dictionary constantCache, + Dictionary storedFunctions, + Dictionary> funcExternalReads, + Dictionary> funcExternalWrites, + Dictionary assignmentInstructions, + Dictionary> variablesUnpropagated) + { + if (instruction is IRCall call) + { + // On encountering a Call, where that identifier is in our dictionary of function closure variables. + // Inject the assignment call to any constant variables that are read inside the function. + // Clear the cache of any variables that are written in that function. + IRVariableBase functionCall = scope.GetVariableNamed(call.Function); + if (functionCall != null && storedFunctions.ContainsKey(functionCall)) + { + string funcRef = storedFunctions[functionCall]; + foreach (IRVariableBase variable in funcExternalReads[funcRef].Union( + funcExternalWrites[funcRef])) + if (assignmentInstructions.ContainsKey(variable)) + { + instructions.Insert(i++, assignmentInstructions[variable]); + assignmentInstructions.Remove(variable); + variablesUnpropagated[variable.Scope].Add(variable); + } + + foreach (IRVariableBase variable in funcExternalWrites[funcRef]) + constantCache.Remove(variable); + } + } + if (instruction is ISingleOperandInstruction singleOperandInstruction) + { + if (singleOperandInstruction.Operand is IRVariable variable && + constantCache.ContainsKey(variable) && + constantCache[variable] is IRConstant) + { + singleOperandInstruction.Operand = constantCache[variable]; + } + } + else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) + { + for (int j = multipleOperandInstruction.OperandCount - 1; j >= 0; j--) + { + if (multipleOperandInstruction[j] is IRVariable variable && + constantCache.ContainsKey(variable) && + constantCache[variable] is IRConstant) + { + multipleOperandInstruction[j] = constantCache[variable]; + } + } + } + } + + private class BlockDictionaries + { + public HashSet<(IRVariableBase IRVariableBase, IRAssign value)> incomingDefinitions; + public HashSet blacklist; + public BlockDictionaries( + HashSet<(IRVariableBase, IRAssign)> incomingDefinitions, + HashSet blacklist) + { + this.incomingDefinitions = incomingDefinitions; + this.blacklist = blacklist; + } + public void InitializeForPropagation( + out Dictionary constantCache, + out Dictionary assignments, + out HashSet blacklist) + { + constantCache = new Dictionary(); + assignments = new Dictionary(); + foreach ((IRVariableBase variable, IRAssign value) in incomingDefinitions) + { + constantCache.Add(variable, value.Value); + assignments.Add(variable, value); + } + blacklist = new HashSet(this.blacklist); + } + } + + private class VariableTupleEqualityComparer : IEqualityComparer<(IRVariableBase variable, IRAssign value)> + { + public static VariableTupleEqualityComparer Instance { get; } = new VariableTupleEqualityComparer(); + public bool Equals((IRVariableBase variable, IRAssign value) x, (IRVariableBase variable, IRAssign value) y) + => x.variable.Equals(y.variable) && (x.value.Value.Equals(y.value.Value)); + + public int GetHashCode((IRVariableBase variable, IRAssign value) obj) + => obj.variable.GetHashCode(); + } + } +} From 7cfbd7913ab58380526007842c9f3896d201be4e Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 14 Mar 2026 00:52:17 -0400 Subject: [PATCH 053/120] Patch scopes losing ancestry when an unused scope is collapsed. --- .../Optimization/Passes/UnnecessaryScopeRemoval.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs index e33a78bb5..868846623 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs @@ -50,12 +50,24 @@ public void ApplyPass(List code) private static void CollapseScope(IRScope scope) { IRScope newScope = scope.ParentScope; + short parentID = GetPushOpcode(newScope).ScopeId; foreach (IRScope childScope in scope.Children.ToArray()) + { childScope.ParentScope = newScope; + OpcodePushScope opcode = GetPushOpcode(childScope); + opcode.ParentScopeId = parentID; + } foreach (BasicBlock block in scope.Blocks.ToArray()) block.Scope = newScope; scope.ParentScope = null; } + private static OpcodePushScope GetPushOpcode(IRScope scope) + { + if (scope.IsGlobalScope) + return new OpcodePushScope(0, 0); + IRNoStackInstruction pushScopeInstruction = scope.HeaderBlock.Instructions[0] as IRNoStackInstruction; + return (OpcodePushScope)pushScopeInstruction.Operation; + } private static void RemovePushScope(BasicBlock header) { From b094ce305a0683ec5a0d8f286de56fbca6775131 Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 16 Mar 2026 20:35:10 -0400 Subject: [PATCH 054/120] Add return type indicator to FunctionAttribute and tag all kOS functions accordingly. --- src/kOS.Safe/Encapsulation/Lexicon.cs | 4 +- src/kOS.Safe/Encapsulation/ListValue.cs | 2 +- src/kOS.Safe/Encapsulation/PIDLoop.cs | 2 +- src/kOS.Safe/Encapsulation/QueueValue.cs | 2 +- src/kOS.Safe/Encapsulation/StackValue.cs | 2 +- src/kOS.Safe/Encapsulation/UniqueSetValue.cs | 2 +- src/kOS.Safe/Function/FunctionAttribute.cs | 22 +++++- src/kOS.Safe/Function/Math.cs | 28 +++---- src/kOS.Safe/Function/Misc.cs | 28 +++---- src/kOS.Safe/Function/Persistence.cs | 32 ++++---- src/kOS.Safe/Function/Suffixed.cs | 4 +- src/kOS.Safe/Function/Trigonometry.cs | 18 ++--- src/kOS.Safe/Persistence/PathValue.cs | 2 +- src/kOS.Safe/Persistence/Volume.cs | 2 +- .../AddOns/KerbalAlarmClock/KACFunctions.cs | 10 +-- src/kOS/Function/BuildList.cs | 2 +- src/kOS/Function/Math.cs | 10 +-- src/kOS/Function/Misc.cs | 14 ++-- src/kOS/Function/Persistence.cs | 4 +- src/kOS/Function/PrintList.cs | 2 +- src/kOS/Function/Suffixed.cs | 78 +++++++++---------- 21 files changed, 144 insertions(+), 126 deletions(-) diff --git a/src/kOS.Safe/Encapsulation/Lexicon.cs b/src/kOS.Safe/Encapsulation/Lexicon.cs index e61603567..6e4edfdff 100644 --- a/src/kOS.Safe/Encapsulation/Lexicon.cs +++ b/src/kOS.Safe/Encapsulation/Lexicon.cs @@ -11,10 +11,10 @@ namespace kOS.Safe.Encapsulation { [kOS.Safe.Utilities.KOSNomenclature("Lexicon")] - [kOS.Safe.Utilities.KOSNomenclature("Lex", CSharpToKOS = false) ] + [kOS.Safe.Utilities.KOSNomenclature("Lex", CSharpToKOS = false)] public class Lexicon : SerializableStructure, IDictionary, IIndexable { - [Function("lex", "lexicon")] + [Function("lex", "lexicon", ReturnType = typeof(Lexicon), IsInvariant = true)] public class FunctionLexicon : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/ListValue.cs b/src/kOS.Safe/Encapsulation/ListValue.cs index 565038028..bda8e2e00 100644 --- a/src/kOS.Safe/Encapsulation/ListValue.cs +++ b/src/kOS.Safe/Encapsulation/ListValue.cs @@ -150,7 +150,7 @@ public void Insert(int index, T item) [kOS.Safe.Utilities.KOSNomenclature("List", KOSToCSharp = false)] // one-way because the generic templated ListValue is the canonical one. public class ListValue : ListValue { - [Function("list")] + [Function("list", ReturnType = typeof(ListValue), IsInvariant = true)] public class FunctionList : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/PIDLoop.cs b/src/kOS.Safe/Encapsulation/PIDLoop.cs index c2c36af9c..99231a9b9 100644 --- a/src/kOS.Safe/Encapsulation/PIDLoop.cs +++ b/src/kOS.Safe/Encapsulation/PIDLoop.cs @@ -9,7 +9,7 @@ namespace kOS.Safe.Encapsulation [kOS.Safe.Utilities.KOSNomenclature("PIDLoop")] public class PIDLoop : SerializableStructure { - [Function("pidloop")] + [Function("pidloop", ReturnType = typeof(PIDLoop), IsInvariant = true)] public class PIDLoopConstructor : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/QueueValue.cs b/src/kOS.Safe/Encapsulation/QueueValue.cs index 9c91ac7e3..bf30461f8 100644 --- a/src/kOS.Safe/Encapsulation/QueueValue.cs +++ b/src/kOS.Safe/Encapsulation/QueueValue.cs @@ -68,7 +68,7 @@ public static QueueValue CreateQueue(IEnumerable list) [kOS.Safe.Utilities.KOSNomenclature("Queue", KOSToCSharp = false)] // one-way because the generic templated QueueValue is the canonical one. public class QueueValue : QueueValue { - [Function("queue")] + [Function("queue", ReturnType = typeof(QueueValue), IsInvariant = true)] public class FunctionQueue : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/StackValue.cs b/src/kOS.Safe/Encapsulation/StackValue.cs index 1eed16dae..63b8e83d7 100644 --- a/src/kOS.Safe/Encapsulation/StackValue.cs +++ b/src/kOS.Safe/Encapsulation/StackValue.cs @@ -74,7 +74,7 @@ public static StackValue CreateStack(IEnumerable list) [kOS.Safe.Utilities.KOSNomenclature("Stack", KOSToCSharp = false)] // one-way because the generic templated StackValue is the canonical one. public class StackValue : StackValue { - [Function("stack")] + [Function("stack", ReturnType = typeof(StackValue), IsInvariant = true)] public class FunctionStack : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/UniqueSetValue.cs b/src/kOS.Safe/Encapsulation/UniqueSetValue.cs index 4e0e1c0da..e02e00eff 100644 --- a/src/kOS.Safe/Encapsulation/UniqueSetValue.cs +++ b/src/kOS.Safe/Encapsulation/UniqueSetValue.cs @@ -73,7 +73,7 @@ private void SetInitializeSuffixes() [kOS.Safe.Utilities.KOSNomenclature("UniqueSet", KOSToCSharp = false)] // one-way because the generic templated UniqueSetValue is the canonical one. public class UniqueSetValue : UniqueSetValue { - [Function("uniqueset")] + [Function("uniqueset", ReturnType = typeof(UniqueSetValue), IsInvariant = true)] public class FunctionSet : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/FunctionAttribute.cs b/src/kOS.Safe/Function/FunctionAttribute.cs index 6fb891d87..1c878c858 100644 --- a/src/kOS.Safe/Function/FunctionAttribute.cs +++ b/src/kOS.Safe/Function/FunctionAttribute.cs @@ -1,11 +1,29 @@ -using System; +using System; namespace kOS.Safe.Function { public class FunctionAttribute : Attribute { public string[] Names { get; set; } - + public bool IsInvariant { get; set; } = false; + public Type ReturnType + { + get => returnType; + set + { + if (value != null && !value.IsSubclassOf(typeof(Encapsulation.Structure))) + { +#if DEBUG + throw new ArgumentException($"{value} is not an accepted return type for a kOS function since it does not subclass {typeof(Encapsulation.Structure)}."); +#else + Utilities.SafeHouse.Logger.LogError($"The supplied type, {value}, is not an accepted return type for a kOS function since it is not a subclass of ${typeof(Encapsulation.Structure)}."); + returnType = null; +#endif + } + returnType = value; + } + } + private Type returnType = null; public FunctionAttribute(params string[] names) { Names = names; diff --git a/src/kOS.Safe/Function/Math.cs b/src/kOS.Safe/Function/Math.cs index 554a8c062..6ed78a4ac 100644 --- a/src/kOS.Safe/Function/Math.cs +++ b/src/kOS.Safe/Function/Math.cs @@ -4,7 +4,7 @@ namespace kOS.Safe.Function { - [Function("abs")] + [Function("abs", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionAbs : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -16,7 +16,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("mod")] + [Function("mod", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionMod : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -29,7 +29,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("floor")] + [Function("floor", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionFloor : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -44,7 +44,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("ceiling")] + [Function("ceiling", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionCeiling : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -59,7 +59,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("round")] + [Function("round", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionRound : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -86,7 +86,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("sqrt")] + [Function("sqrt", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionSqrt : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -99,7 +99,7 @@ public override void Execute(SafeSharedObjects shared) } - [Function("ln")] + [Function("ln", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionLn : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -111,7 +111,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("log10")] + [Function("log10", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionLog10 : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -123,7 +123,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("min")] + [Function("min", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionMin : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -153,7 +153,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("max")] + [Function("max", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionMax : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -183,7 +183,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("random")] + [Function("random", ReturnType = typeof(ScalarDoubleValue), IsInvariant = false)] public class FunctionRandom : SafeFunctionBase { private readonly Random random = new Random(); @@ -201,7 +201,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("randomseed")] + [Function("randomseed", ReturnType = null, IsInvariant = false)] public class FunctionRandomSeed : SafeFunctionBase { private readonly Random random = new Random(); @@ -215,7 +215,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("char")] + [Function("char", ReturnType = typeof(StringValue), IsInvariant = true)] public class FunctionChar : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -227,7 +227,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("unchar")] + [Function("unchar", ReturnType = typeof(ScalarIntValue), IsInvariant = true)] public class FunctionUnchar : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/Misc.cs b/src/kOS.Safe/Function/Misc.cs index b0a7d64e2..889038507 100644 --- a/src/kOS.Safe/Function/Misc.cs +++ b/src/kOS.Safe/Function/Misc.cs @@ -10,7 +10,7 @@ namespace kOS.Safe.Function { - [Function("print")] + [Function("print", ReturnType = null, IsInvariant = false)] public class FunctionPrint : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -21,7 +21,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("printat")] + [Function("printat", ReturnType = null, IsInvariant = false)] public class FunctionPrintAt : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -34,7 +34,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("toggleflybywire")] + [Function("toggleflybywire", ReturnType = null, IsInvariant = false)] public class FunctionToggleFlyByWire : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -46,7 +46,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("selectautopilotmode")] + [Function("selectautopilotmode", ReturnType = null, IsInvariant = false)] public class FunctionSelectAutopilotMode : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -57,7 +57,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("run")] + [Function("run", ReturnType = null, IsInvariant = false)] public class FunctionRun : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -133,7 +133,7 @@ public override void Execute(SafeSharedObjects shared) } } - [FunctionAttribute("load")] + [Function("load", ReturnType = null, IsInvariant = false)] public class FunctionLoad : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -262,7 +262,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("logfile")] + [Function("logfile", ReturnType = null, IsInvariant = false)] public class FunctionLogFile : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -300,7 +300,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("reboot")] + [Function("reboot", ReturnType = null, IsInvariant = false)] public class FunctionReboot : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -315,7 +315,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("shutdown")] + [Function("shutdown", ReturnType = null, IsInvariant = false)] public class FunctionShutdown : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -326,7 +326,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("debugdump")] + [Function("debugdump", ReturnType = null, IsInvariant = false)] public class DebugDump : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -336,7 +336,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("debugfreezegame")] + [Function("debugfreezegame", ReturnType = null, IsInvariant = false)] /// /// Deliberately cause physics lag by making the main game thread sleep. /// Clearly not something there's a good reason to do *except* when @@ -361,7 +361,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("profileresult")] + [Function("profileresult", ReturnType = typeof(StringValue), IsInvariant = false)] public class ProfileResult : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -383,7 +383,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("makebuiltindelegate")] + [Function("makebuiltindelegate", ReturnType = typeof(BuiltinDelegate), IsInvariant = false)] public class MakeBuiltinDelegate : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -395,7 +395,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("droppriority")] + [Function("droppriority", ReturnType = null, IsInvariant = false)] public class AllowInterrupt : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/Persistence.cs b/src/kOS.Safe/Function/Persistence.cs index 59a248e96..90b1b4aa6 100644 --- a/src/kOS.Safe/Function/Persistence.cs +++ b/src/kOS.Safe/Function/Persistence.cs @@ -12,7 +12,7 @@ namespace kOS.Safe.Function * remove these function below as well any metions of delete/rename file/rename volume/copy from kRISC.tpg in the future. */ - [Function("copy_deprecated")] + [Function("copy_deprecated", ReturnType = null, IsInvariant = false)] public class FunctionCopyDeprecated : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -49,7 +49,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("rename_file_deprecated")] + [Function("rename_file_deprecated", ReturnType = null, IsInvariant = false)] public class FunctionRenameFileDeprecated : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -76,7 +76,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("rename_volume_deprecated")] + [Function("rename_volume_deprecated", ReturnType = null, IsInvariant = false)] public class FunctionRenameVolumeDeprecated : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -99,7 +99,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("delete_deprecated")] + [Function("delete_deprecated", ReturnType = null, IsInvariant = false)] public class FunctionDeleteDeprecated : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -127,7 +127,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("scriptpath")] + [Function("scriptpath", ReturnType = typeof(PathValue), IsInvariant = false)] public class FunctionScriptPath : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -141,7 +141,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("switch")] + [Function("switch", ReturnType = null, IsInvariant = false)] public class FunctionSwitch : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -164,7 +164,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("cd", "chdir")] + [Function("cd", "chdir", ReturnType = null, IsInvariant = false)] public class FunctionCd : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -198,7 +198,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("copypath")] + [Function("copypath", ReturnType = typeof(BooleanValue), IsInvariant = false)] public class FunctionCopyPath : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -214,7 +214,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("movepath")] + [Function("movepath", ReturnType = typeof(BooleanValue), IsInvariant = false)] public class FunctionMove : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -230,7 +230,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("deletepath")] + [Function("deletepath", ReturnType = typeof(BooleanValue), IsInvariant = false)] public class FunctionDeletePath : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -245,7 +245,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("writejson")] + [Function("writejson", ReturnType = typeof(VolumeFile), IsInvariant = false)] public class FunctionWriteJson : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -270,7 +270,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("readjson")] + [Function("readjson", ReturnType = typeof(Structure), IsInvariant = false)] public class FunctionReadJson : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -293,7 +293,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("exists")] + [Function("exists", ReturnType = typeof(BooleanValue), IsInvariant = false)] public class FunctionExists : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -308,7 +308,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("open")] + [Function("open", ReturnType = typeof(Structure), IsInvariant = false)] public class FunctionOpen : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -328,7 +328,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("create")] + [Function("create", ReturnType = typeof(VolumeFile), IsInvariant = false)] public class FunctionCreate : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -345,7 +345,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("createdir")] + [Function("createdir", ReturnType = typeof(VolumeDirectory), IsInvariant = false)] public class FunctionCreateDirectory : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/Suffixed.cs b/src/kOS.Safe/Function/Suffixed.cs index 3367b373d..ffe94b966 100644 --- a/src/kOS.Safe/Function/Suffixed.cs +++ b/src/kOS.Safe/Function/Suffixed.cs @@ -3,7 +3,7 @@ namespace kOS.Safe.Function { - [Function("range")] + [Function("range", ReturnType = typeof(RangeValue), IsInvariant = true)] public class FunctionRange : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -38,7 +38,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("constant")] + [Function("constant", ReturnType = typeof(ConstantValue), IsInvariant = true)] public class FunctionConstant : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/Trigonometry.cs b/src/kOS.Safe/Function/Trigonometry.cs index f8e53cd51..c362c9a13 100644 --- a/src/kOS.Safe/Function/Trigonometry.cs +++ b/src/kOS.Safe/Function/Trigonometry.cs @@ -1,10 +1,10 @@ -using System; +using System; using kOS.Safe.Encapsulation; using kOS.Safe.Utilities; namespace kOS.Safe.Function { - [Function("sin")] + [Function("sin", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionSin : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -17,7 +17,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("cos")] + [Function("cos", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionCos : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -30,7 +30,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("tan")] + [Function("tan", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionTan : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -43,7 +43,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("arcsin")] + [Function("arcsin", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionArcSin : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -55,7 +55,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("arccos")] + [Function("arccos", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionArcCos : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -67,7 +67,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("arctan")] + [Function("arctan", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionArcTan : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -79,7 +79,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("arctan2")] + [Function("arctan2", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionArcTan2 : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -92,7 +92,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("anglediff")] + [Function("anglediff", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionAngleDiff : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Persistence/PathValue.cs b/src/kOS.Safe/Persistence/PathValue.cs index 6c10d9c0c..2a53bac29 100644 --- a/src/kOS.Safe/Persistence/PathValue.cs +++ b/src/kOS.Safe/Persistence/PathValue.cs @@ -18,7 +18,7 @@ namespace kOS.Safe [kOS.Safe.Utilities.KOSNomenclature("Path")] public class PathValue : SerializableStructure { - [Function("path")] + [Function("path", ReturnType = typeof(PathValue), IsInvariant = false)] public class FunctionPath : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Persistence/Volume.cs b/src/kOS.Safe/Persistence/Volume.cs index d4127657f..4d22ac86f 100644 --- a/src/kOS.Safe/Persistence/Volume.cs +++ b/src/kOS.Safe/Persistence/Volume.cs @@ -10,7 +10,7 @@ namespace kOS.Safe.Persistence [kOS.Safe.Utilities.KOSNomenclature("Volume")] public abstract class Volume : Structure { - [Function("volume")] + [Function("volume", ReturnType = typeof(Volume), IsInvariant = false)] public class FunctionVolume : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS/AddOns/KerbalAlarmClock/KACFunctions.cs b/src/kOS/AddOns/KerbalAlarmClock/KACFunctions.cs index 30e1e6485..b9b4fb324 100644 --- a/src/kOS/AddOns/KerbalAlarmClock/KACFunctions.cs +++ b/src/kOS/AddOns/KerbalAlarmClock/KACFunctions.cs @@ -1,4 +1,4 @@ -using kOS.Function; +using kOS.Function; using kOS.Safe.Encapsulation; using kOS.Safe.Exceptions; using kOS.Safe.Function; @@ -8,7 +8,7 @@ namespace kOS.AddOns.KerbalAlarmClock { - [Function("addAlarm")] + [Function("addAlarm", ReturnType = typeof(Structure), IsInvariant = false)] public class FunctionAddAlarm : FunctionBase { public override void Execute(SharedObjects shared) @@ -66,12 +66,12 @@ public override void Execute(SharedObjects shared) } } - [Function("listAlarms")] + [Function("listAlarms", ReturnType = typeof(ListValue), IsInvariant = false)] public class FunctionListAlarms : FunctionBase { public override void Execute(SharedObjects shared) { - var list = new ListValue(); + var list = new ListValue(); string alarmTypes = PopValueAssert(shared).ToString(); AssertArgBottomAndConsume(shared); @@ -100,7 +100,7 @@ public override void Execute(SharedObjects shared) } } - [Function("deleteAlarm")] + [Function("deleteAlarm", ReturnType = typeof(BooleanValue), IsInvariant = false)] public class FunctionDeleteAlarm : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/BuildList.cs b/src/kOS/Function/BuildList.cs index 84376baff..3305a3ee5 100644 --- a/src/kOS/Function/BuildList.cs +++ b/src/kOS/Function/BuildList.cs @@ -9,7 +9,7 @@ namespace kOS.Function { - [Function("buildlist")] + [Function("buildlist", ReturnType = typeof(ListValue), IsInvariant = false)] public class FunctionBuildList : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/Math.cs b/src/kOS/Function/Math.cs index 8850e087e..ddf6b5998 100644 --- a/src/kOS/Function/Math.cs +++ b/src/kOS/Function/Math.cs @@ -1,4 +1,4 @@ -using System; +using System; using kOS.Safe.Compilation; using kOS.Safe.Function; using kOS.Suffixed; @@ -7,7 +7,7 @@ namespace kOS.Function { - [Function("vcrs", "vectorcrossproduct")] + [Function("vcrs", "vectorcrossproduct", ReturnType = typeof(Vector), IsInvariant = true)] public class FunctionVectorCross : FunctionBase { public override void Execute(SharedObjects shared) @@ -26,7 +26,7 @@ public override void Execute(SharedObjects shared) } } - [Function("vdot", "vectordotproduct")] + [Function("vdot", "vectordotproduct", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionVectorDot : FunctionBase { public override void Execute(SharedObjects shared) @@ -45,7 +45,7 @@ public override void Execute(SharedObjects shared) } } - [Function("vxcl", "vectorexclude")] + [Function("vxcl", "vectorexclude", ReturnType = typeof(Vector), IsInvariant = true)] public class FunctionVectorExclude : FunctionBase { public override void Execute(SharedObjects shared) @@ -64,7 +64,7 @@ public override void Execute(SharedObjects shared) } } - [Function("vang", "vectorangle")] + [Function("vang", "vectorangle", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionVectorAngle : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/Misc.cs b/src/kOS/Function/Misc.cs index eb32c6619..e090c09b4 100644 --- a/src/kOS/Function/Misc.cs +++ b/src/kOS/Function/Misc.cs @@ -19,7 +19,7 @@ namespace kOS.Function { - [Function("clearscreen")] + [Function("clearscreen", ReturnType = null, IsInvariant = false)] public class FunctionClearScreen : FunctionBase { public override void Execute(SharedObjects shared) @@ -29,7 +29,7 @@ public override void Execute(SharedObjects shared) } } - [Function("hudtext")] + [Function("hudtext", ReturnType = null, IsInvariant = false)] public class FunctionHudText : FunctionBase { public override void Execute(SharedObjects shared) @@ -71,7 +71,7 @@ public override void Execute(SharedObjects shared) } } - [Function("stage")] + [Function("stage", ReturnType = null, IsInvariant = false)] public class FunctionStage : FunctionBase { public override void Execute(SharedObjects shared) @@ -93,7 +93,7 @@ public override void Execute(SharedObjects shared) } } - [Function("add")] + [Function("add", ReturnType = null, IsInvariant = false)] public class FunctionAddNode : FunctionBase { public override void Execute(SharedObjects shared) @@ -104,7 +104,7 @@ public override void Execute(SharedObjects shared) } } - [Function("remove")] + [Function("remove", ReturnType = null, IsInvariant = false)] public class FunctionRemoveNode : FunctionBase { public override void Execute(SharedObjects shared) @@ -115,7 +115,7 @@ public override void Execute(SharedObjects shared) } } - [Function("warpto")] + [Function("warpto", ReturnType = null, IsInvariant = false)] public class WarpTo : FunctionBase { public override void Execute(SharedObjects shared) @@ -137,7 +137,7 @@ public override void Execute(SharedObjects shared) } } - [Function("processor")] + [Function("processor", ReturnType = typeof(PartModuleFields), IsInvariant = false)] public class FunctionProcessor : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/Persistence.cs b/src/kOS/Function/Persistence.cs index 4b2129974..8b1ef6e50 100644 --- a/src/kOS/Function/Persistence.cs +++ b/src/kOS/Function/Persistence.cs @@ -1,4 +1,4 @@ -using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation; using kOS.Safe.Exceptions; using kOS.Safe.Function; using kOS.Safe.Persistence; @@ -13,7 +13,7 @@ namespace kOS.Function { - [Function("edit")] + [Function("edit", ReturnType = null, IsInvariant = false)] public class FunctionEdit : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/PrintList.cs b/src/kOS/Function/PrintList.cs index 0667db142..e36d88ba6 100644 --- a/src/kOS/Function/PrintList.cs +++ b/src/kOS/Function/PrintList.cs @@ -16,7 +16,7 @@ namespace kOS.Function { - [Function("printlist")] + [Function("printlist", ReturnType = null, IsInvariant = false)] public class FunctionPrintList : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/Suffixed.cs b/src/kOS/Function/Suffixed.cs index abf577fe2..e918f251e 100644 --- a/src/kOS/Function/Suffixed.cs +++ b/src/kOS/Function/Suffixed.cs @@ -15,7 +15,7 @@ namespace kOS.Function { - [Function("node")] + [Function("node", ReturnType = typeof(Node), IsInvariant = false)] public class FunctionNode : FunctionBase { public override void Execute(SharedObjects shared) @@ -40,7 +40,7 @@ public override void Execute(SharedObjects shared) } } - [Function("v")] + [Function("v", ReturnType = typeof(Vector), IsInvariant = true)] public class FunctionVector : FunctionBase { public override void Execute(SharedObjects shared) @@ -55,7 +55,7 @@ public override void Execute(SharedObjects shared) } } - [Function("r")] + [Function("r", ReturnType = typeof(Direction), IsInvariant = true)] public class FunctionRotation : FunctionBase { public override void Execute(SharedObjects shared) @@ -70,7 +70,7 @@ public override void Execute(SharedObjects shared) } } - [Function("q")] + [Function("q", ReturnType = typeof(Direction), IsInvariant = true)] public class FunctionQuaternion : FunctionBase { public override void Execute(SharedObjects shared) @@ -86,7 +86,7 @@ public override void Execute(SharedObjects shared) } } - [Function("createOrbit")] + [Function("createOrbit", ReturnType = typeof(OrbitInfo), IsInvariant = false)] public class FunctionCreateOrbit : FunctionBase { public override void Execute(SharedObjects shared) @@ -131,7 +131,7 @@ public override void Execute(SharedObjects shared) } } - [Function("rotatefromto")] + [Function("rotatefromto", ReturnType = typeof(Direction), IsInvariant = true)] public class FunctionRotateFromTo : FunctionBase { public override void Execute(SharedObjects shared) @@ -145,7 +145,7 @@ public override void Execute(SharedObjects shared) } } - [Function("lookdirup")] + [Function("lookdirup", ReturnType = typeof(Direction), IsInvariant = true)] public class FunctionLookDirUp : FunctionBase { public override void Execute(SharedObjects shared) @@ -159,7 +159,7 @@ public override void Execute(SharedObjects shared) } } - [Function("angleaxis")] + [Function("angleaxis", ReturnType = typeof(Direction), IsInvariant = true)] public class FunctionAngleAxis : FunctionBase { public override void Execute(SharedObjects shared) @@ -173,7 +173,7 @@ public override void Execute(SharedObjects shared) } } - [Function("latlng")] + [Function("latlng", ReturnType = typeof(GeoCoordinates), IsInvariant = false)] public class FunctionLatLng : FunctionBase { public override void Execute(SharedObjects shared) @@ -187,7 +187,7 @@ public override void Execute(SharedObjects shared) } } - [Function("vessel")] + [Function("vessel", ReturnType = typeof(VesselTarget), IsInvariant = false)] public class FunctionVessel : FunctionBase { public override void Execute(SharedObjects shared) @@ -199,7 +199,7 @@ public override void Execute(SharedObjects shared) } } - [Function("body")] + [Function("body", ReturnType = typeof(BodyTarget), IsInvariant = false)] public class FunctionBody : FunctionBase { public override void Execute(SharedObjects shared) @@ -211,7 +211,7 @@ public override void Execute(SharedObjects shared) } } - [Function("bodyexists")] + [Function("bodyexists", ReturnType = typeof(BooleanValue), IsInvariant = false)] public class FunctionBodyExists : FunctionBase { public override void Execute(SharedObjects shared) @@ -222,7 +222,7 @@ public override void Execute(SharedObjects shared) } } - [Function("bodyatmosphere")] + [Function("bodyatmosphere", ReturnType = typeof(BodyAtmosphere), IsInvariant = false)] public class FunctionBodyAtmosphere : FunctionBase { public override void Execute(SharedObjects shared) @@ -237,7 +237,7 @@ public override void Execute(SharedObjects shared) } } - [Function("bounds")] + [Function("bounds", ReturnType = typeof(BoundsValue), IsInvariant = false)] public class FunctionBounds : FunctionBase { public override void Execute(SharedObjects shared) @@ -252,7 +252,7 @@ public override void Execute(SharedObjects shared) } } - [Function("heading")] + [Function("heading", ReturnType = typeof(Direction), IsInvariant = false)] public class FunctionHeading : FunctionBase { public override void Execute(SharedObjects shared) @@ -274,7 +274,7 @@ public override void Execute(SharedObjects shared) } } - [Function("slidenote")] + [Function("slidenote", ReturnType = typeof(NoteValue), IsInvariant = true)] public class FunctionSlideNote : FunctionBase { public override void Execute(SharedObjects shared) @@ -304,7 +304,7 @@ public override void Execute(SharedObjects shared) } } - [Function("note")] + [Function("note", ReturnType = typeof(NoteValue), IsInvariant = true)] public class FunctionNote : FunctionBase { public override void Execute(SharedObjects shared) @@ -333,7 +333,7 @@ public override void Execute(SharedObjects shared) } } - [Function("GetVoice")] + [Function("GetVoice", ReturnType = typeof(VoiceValue), IsInvariant = false)] public class FunctionGetVoice : FunctionBase { public override void Execute(SharedObjects shared) @@ -354,7 +354,7 @@ public override void Execute(SharedObjects shared) } - [Function("StopAllVoices")] + [Function("StopAllVoices", ReturnType = null, IsInvariant = false)] public class FunctionStopAllVoices : FunctionBase { public override void Execute(SharedObjects shared) @@ -362,7 +362,7 @@ public override void Execute(SharedObjects shared) shared.SoundMaker.StopAllVoices(); } } - [Function("timestamp", "time")] + [Function("timestamp", "time", ReturnType = typeof(TimeStamp), IsInvariant = false)] public class FunctionTimeStamp : FunctionBase { // Note: "TIME" is both a bound variable AND a built-in function now. @@ -426,7 +426,7 @@ public override void Execute(SharedObjects shared) } } - [Function("timespan")] + [Function("timespan", ReturnType = typeof(Suffixed.TimeSpan), IsInvariant = true)] public class FunctionTimeSpan : FunctionBase { public override void Execute(SharedObjects shared) @@ -477,7 +477,7 @@ public override void Execute(SharedObjects shared) } } - [Function("hsv")] + [Function("hsv", ReturnType = typeof(HsvaColor), IsInvariant = true)] public class FunctionHsv : FunctionBase { public override void Execute(SharedObjects shared) @@ -490,7 +490,7 @@ public override void Execute(SharedObjects shared) } } - [Function("hsva")] + [Function("hsva", ReturnType = typeof(HsvaColor), IsInvariant = true)] public class FunctionHsva : FunctionBase { public override void Execute(SharedObjects shared) @@ -504,7 +504,7 @@ public override void Execute(SharedObjects shared) } } - [Function("rgb")] + [Function("rgb", ReturnType = typeof(RgbaColor), IsInvariant = true)] public class FunctionRgb : FunctionBase { public override void Execute(SharedObjects shared) @@ -517,7 +517,7 @@ public override void Execute(SharedObjects shared) } } - [Function("rgba")] + [Function("rgba", ReturnType = typeof(RgbaColor), IsInvariant = true)] public class FunctionRgba : FunctionBase { public override void Execute(SharedObjects shared) @@ -539,7 +539,7 @@ public override void Execute(SharedObjects shared) // Note: vecdraw now counts the args and changes its behavior accordingly. // For backward compatibility, vecdrawargs has been aliased to vecdraw. // - [Function("vecdraw", "vecdrawargs")] + [Function("vecdraw", "vecdrawargs", ReturnType = typeof(VectorRenderer), IsInvariant = false)] public class FunctionVecDrawNull : FunctionBase { protected RgbaColor GetDefaultColor() @@ -629,7 +629,7 @@ public void DoExecuteWork( } } - [Function("clearvecdraws")] + [Function("clearvecdraws", ReturnType = null, IsInvariant = false)] public class FunctionHideAllVecdraws : FunctionBase { public override void Execute(SharedObjects shared) @@ -639,7 +639,7 @@ public override void Execute(SharedObjects shared) } } - [Function("clearguis")] + [Function("clearguis", ReturnType = null, IsInvariant = false)] public class FunctionClearAllGuis : FunctionBase { public override void Execute(SharedObjects shared) @@ -649,7 +649,7 @@ public override void Execute(SharedObjects shared) } } - [Function("gui")] + [Function("gui", ReturnType = typeof(GUIWidgets), IsInvariant = false)] public class FunctionWidgets : FunctionBase { public override void Execute(SharedObjects shared) @@ -662,7 +662,7 @@ public override void Execute(SharedObjects shared) } } - [Function("positionat")] + [Function("positionat", ReturnType = typeof(Vector), IsInvariant = false)] public class FunctionPositionAt : FunctionBase { public override void Execute(SharedObjects shared) @@ -675,7 +675,7 @@ public override void Execute(SharedObjects shared) } } - [Function("velocityat")] + [Function("velocityat", ReturnType = typeof(Vector), IsInvariant = false)] public class FunctionVelocityAt : FunctionBase { public override void Execute(SharedObjects shared) @@ -688,7 +688,7 @@ public override void Execute(SharedObjects shared) } } - [Function("highlight")] + [Function("highlight", ReturnType = typeof(HighlightStructure), IsInvariant = false)] public class FunctionHightlight : FunctionBase { public override void Execute(SharedObjects shared) @@ -701,7 +701,7 @@ public override void Execute(SharedObjects shared) } } - [Function("orbitat")] + [Function("orbitat", ReturnType = typeof(OrbitInfo), IsInvariant = false)] public class FunctionOrbitAt : FunctionBase { public override void Execute(SharedObjects shared) @@ -714,7 +714,7 @@ public override void Execute(SharedObjects shared) } } - [Function("career")] + [Function("career", ReturnType = typeof(Career), IsInvariant = false)] public class FunctionCareer : FunctionBase { public override void Execute(SharedObjects shared) @@ -724,7 +724,7 @@ public override void Execute(SharedObjects shared) } } - [Function("allwaypoints")] + [Function("allwaypoints", ReturnType = typeof(ListValue), IsInvariant = false)] public class FunctionAllWaypoints : FunctionBase { public override void Execute(SharedObjects shared) @@ -732,7 +732,7 @@ public override void Execute(SharedObjects shared) AssertArgBottomAndConsume(shared); // no args // ReSharper disable SuggestUseVarKeywordEvident - ListValue returnList = new ListValue(); + ListValue returnList = new ListValue(); // ReSharper enable SuggestUseVarKeywordEvident WaypointManager wpm = WaypointManager.Instance(); @@ -758,7 +758,7 @@ public override void Execute(SharedObjects shared) } } - [Function("waypoint")] + [Function("waypoint", ReturnType = typeof(WaypointValue), IsInvariant = false)] public class FunctionWaypoint : FunctionBase { public override void Execute(SharedObjects shared) @@ -805,7 +805,7 @@ public override void Execute(SharedObjects shared) } } - [Function("transferall")] + [Function("transferall", ReturnType = typeof(ResourceTransferValue), IsInvariant = false)] public class FunctionTransferAll : FunctionBase { public override void Execute(SharedObjects shared) @@ -828,7 +828,7 @@ public override void Execute(SharedObjects shared) } - [Function("transfer")] + [Function("transfer", ReturnType = typeof(ResourceTransferValue), IsInvariant = false)] public class FunctionTransfer : FunctionBase { public override void Execute(SharedObjects shared) From 8faf6143894cb26a18973d291bf329a97228f1db Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 16 Mar 2026 21:02:20 -0400 Subject: [PATCH 055/120] Fix Structure not counting as a valid return type. --- src/kOS.Safe/Function/FunctionAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kOS.Safe/Function/FunctionAttribute.cs b/src/kOS.Safe/Function/FunctionAttribute.cs index 1c878c858..ebe06e739 100644 --- a/src/kOS.Safe/Function/FunctionAttribute.cs +++ b/src/kOS.Safe/Function/FunctionAttribute.cs @@ -11,7 +11,7 @@ public Type ReturnType get => returnType; set { - if (value != null && !value.IsSubclassOf(typeof(Encapsulation.Structure))) + if (value != null && !typeof(Encapsulation.Structure).IsAssignableFrom(value)) { #if DEBUG throw new ArgumentException($"{value} is not an accepted return type for a kOS function since it does not subclass {typeof(Encapsulation.Structure)}."); From 9402dce674e86256bc2d8bc394696faf0f78b2ce Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 16 Mar 2026 21:04:17 -0400 Subject: [PATCH 056/120] Add methods to check if functions are invariant and to get their return type. --- src/kOS.Safe/Function/FunctionManager.cs | 26 +++++++++++++++++++++-- src/kOS.Safe/Function/IFunctionManager.cs | 2 ++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Function/FunctionManager.cs b/src/kOS.Safe/Function/FunctionManager.cs index 2c0043fb6..443f2a55c 100644 --- a/src/kOS.Safe/Function/FunctionManager.cs +++ b/src/kOS.Safe/Function/FunctionManager.cs @@ -10,7 +10,9 @@ namespace kOS.Safe.Function public class FunctionManager : IFunctionManager { private readonly SafeSharedObjects shared; - private Dictionary functions; + private readonly Dictionary functions = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary functionTypes = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly HashSet invariantFunctions = new HashSet(); private static readonly Dictionary rawAttributes = new Dictionary(); public FunctionManager(SafeSharedObjects shared) @@ -21,7 +23,10 @@ public FunctionManager(SafeSharedObjects shared) public void Load() { - functions = new Dictionary(StringComparer.OrdinalIgnoreCase); + functions.Clear(); + functionTypes.Clear(); + invariantFunctions.Clear(); + foreach (FunctionAttribute attr in rawAttributes.Keys) { var type = rawAttributes[attr]; @@ -32,6 +37,9 @@ public void Load() if (functionName != string.Empty) { functions.Add(functionName, (SafeFunctionBase)functionObject); + functionTypes.Add(functionName, attr.ReturnType); + if (attr.IsInvariant) + invariantFunctions.Add(functionName); } } } @@ -70,5 +78,19 @@ public bool Exists(string functionName) { return functions.ContainsKey(functionName); } + + public bool IsFunctionInvariant(string functionName) + { + return invariantFunctions.Contains(functionName); + } + + public Type FunctionReturnType(string functionName) + { + if (!functions.ContainsKey(functionName)) + { + throw new Exception("Queried return type of a non-existent function " + functionName); + } + return functionTypes[functionName]; + } } } \ No newline at end of file diff --git a/src/kOS.Safe/Function/IFunctionManager.cs b/src/kOS.Safe/Function/IFunctionManager.cs index 1a243698f..54bd58d46 100644 --- a/src/kOS.Safe/Function/IFunctionManager.cs +++ b/src/kOS.Safe/Function/IFunctionManager.cs @@ -5,5 +5,7 @@ public interface IFunctionManager void Load(); void CallFunction(string functionName); bool Exists(string functionName); + bool IsFunctionInvariant(string functionName); + System.Type FunctionReturnType(string functionName); } } \ No newline at end of file From b11f5d273423879235122265cdb7716ff8304308 Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 16 Mar 2026 21:54:16 -0400 Subject: [PATCH 057/120] Add methods to determine the resulting type of calculator operations. --- src/kOS.Safe/Compilation/Calculator.cs | 36 ++++++ src/kOS.Safe/Compilation/CalculatorBool.cs | 20 +++ src/kOS.Safe/Compilation/CalculatorScalar.cs | 11 ++ src/kOS.Safe/Compilation/CalculatorString.cs | 18 +++ .../Compilation/CalculatorStructure.cs | 115 ++++++++++++++++++ .../KOSBinaryOperandTypeException.cs | 13 ++ 6 files changed, 213 insertions(+) diff --git a/src/kOS.Safe/Compilation/Calculator.cs b/src/kOS.Safe/Compilation/Calculator.cs index c5f16c098..cb4b5b5ed 100644 --- a/src/kOS.Safe/Compilation/Calculator.cs +++ b/src/kOS.Safe/Compilation/Calculator.cs @@ -17,6 +17,18 @@ public abstract class Calculator public abstract object NotEqual(OperandPair pair); public abstract object Equal(OperandPair pair); + public abstract Type GetAddResultType(Type leftType, Type rightType); + public abstract Type GetSubtractResultType(Type leftType, Type rightType); + public abstract Type GetMultiplyResultType(Type leftType, Type rightType); + public abstract Type GetDivideResultType(Type leftType, Type rightType); + public abstract Type GetPowerResultType(Type leftType, Type rightType); + public virtual Type GetGreaterThanResultType(Type leftType, Type rightType) => typeof(BooleanValue); + public virtual Type GetLessThanResultType(Type leftType, Type rightType) => typeof(BooleanValue); + public virtual Type GetGreaterThanEqualResultType(Type leftType, Type rightType) => typeof(BooleanValue); + public virtual Type GetLessThanEqualResultType(Type leftType, Type rightType) => typeof(BooleanValue); + public virtual Type GetNotEqualResultType(Type leftType, Type rightType) => typeof(BooleanValue); + public virtual Type GetEqualResultType(Type leftType, Type rightType) => typeof(BooleanValue); + private static CalculatorScalar calculatorScalar; private static CalculatorString calculatorString; private static CalculatorBool calculatorBool; @@ -45,5 +57,29 @@ public static Calculator GetCalculator(OperandPair operandPair) throw new NotImplementedException(string.Format("Can't operate types {0} and {1}", operandPair.Left.GetType(), operandPair.Right.GetType())); } + + public static Calculator GetCalculator(Type leftType, Type rightType) + { + var scalarCount = 0; + var stringCount = 0; + var specialCount = 0; + var boolCount = 0; + + if (typeof(ScalarValue).IsAssignableFrom(leftType)) scalarCount++; + if (typeof(StringValue).IsAssignableFrom(leftType)) stringCount++; + if (typeof(ISuffixed).IsAssignableFrom(leftType)) specialCount++; + if (typeof(BooleanValue).IsAssignableFrom(leftType)) boolCount++; + if (typeof(ScalarValue).IsAssignableFrom(rightType)) scalarCount++; + if (typeof(StringValue).IsAssignableFrom(rightType)) stringCount++; + if (typeof(ISuffixed).IsAssignableFrom(rightType)) specialCount++; + if (typeof(BooleanValue).IsAssignableFrom(rightType)) boolCount++; + + if (scalarCount == 2) return calculatorScalar ?? (calculatorScalar = new CalculatorScalar()); + if (stringCount > 0) return calculatorString ?? (calculatorString = new CalculatorString()); + if (boolCount > 0) return calculatorBool ?? (calculatorBool = new CalculatorBool()); + if (specialCount > 0) return calculatorStructure ?? (calculatorStructure = new CalculatorStructure()); + + throw new NotImplementedException(string.Format("Can't operate types {0} and {1}", leftType, rightType)); + } } } \ No newline at end of file diff --git a/src/kOS.Safe/Compilation/CalculatorBool.cs b/src/kOS.Safe/Compilation/CalculatorBool.cs index 9c0b00dc3..46b69ba0e 100644 --- a/src/kOS.Safe/Compilation/CalculatorBool.cs +++ b/src/kOS.Safe/Compilation/CalculatorBool.cs @@ -10,26 +10,46 @@ public override object Add(OperandPair pair) { throw new KOSBinaryOperandTypeException(pair, "add", "to"); } + public override Type GetAddResultType(Type leftType, Type rightType) + { + throw new KOSBinaryOperandTypeException(leftType, rightType, "add", "to"); + } public override object Subtract(OperandPair pair) { throw new KOSBinaryOperandTypeException(pair, "subtract", "from"); } + public override Type GetSubtractResultType(Type leftType, Type rightType) + { + throw new KOSBinaryOperandTypeException(leftType, rightType, "subtract", "from"); + } public override object Multiply(OperandPair pair) { throw new KOSBinaryOperandTypeException(pair, "multiply", "by"); } + public override Type GetMultiplyResultType(Type leftType, Type rightType) + { + throw new KOSBinaryOperandTypeException(leftType, rightType, "multiply", "by"); + } public override object Divide(OperandPair pair) { throw new KOSBinaryOperandTypeException(pair, "divide", "by"); } + public override Type GetDivideResultType(Type leftType, Type rightType) + { + throw new KOSBinaryOperandTypeException(leftType, rightType, "divide", "by"); + } public override object Power(OperandPair pair) { throw new KOSBinaryOperandTypeException(pair, "exponentiate", "by"); } + public override Type GetPowerResultType(Type leftType, Type rightType) + { + throw new KOSBinaryOperandTypeException(leftType, rightType, "exponentiate", "by"); + } public override object GreaterThan(OperandPair pair) { diff --git a/src/kOS.Safe/Compilation/CalculatorScalar.cs b/src/kOS.Safe/Compilation/CalculatorScalar.cs index a5d1426d0..5c0124090 100644 --- a/src/kOS.Safe/Compilation/CalculatorScalar.cs +++ b/src/kOS.Safe/Compilation/CalculatorScalar.cs @@ -1,3 +1,4 @@ +using System; using kOS.Safe.Encapsulation; namespace kOS.Safe.Compilation @@ -8,26 +9,36 @@ public override object Add(OperandPair pair) { return ScalarValue.Create(pair.Left) + ScalarValue.Create(pair.Right); } + public override Type GetAddResultType(Type leftType, Type rightType) + => typeof(ScalarValue); public override object Subtract(OperandPair pair) { return ScalarValue.Create(pair.Left) - ScalarValue.Create(pair.Right); } + public override Type GetSubtractResultType(Type leftType, Type rightType) + => typeof(ScalarValue); public override object Multiply(OperandPair pair) { return ScalarValue.Create(pair.Left) * ScalarValue.Create(pair.Right); } + public override Type GetMultiplyResultType(Type leftType, Type rightType) + => typeof(ScalarValue); public override object Divide(OperandPair pair) { return ScalarValue.Create(pair.Left) / ScalarValue.Create(pair.Right); } + public override Type GetDivideResultType(Type leftType, Type rightType) + => typeof(ScalarValue); public override object Power(OperandPair pair) { return ScalarValue.Create(pair.Left) ^ ScalarValue.Create(pair.Right); } + public override Type GetPowerResultType(Type leftType, Type rightType) + => typeof(ScalarValue); public override object GreaterThan(OperandPair pair) { diff --git a/src/kOS.Safe/Compilation/CalculatorString.cs b/src/kOS.Safe/Compilation/CalculatorString.cs index c7675c1df..10dc459c5 100644 --- a/src/kOS.Safe/Compilation/CalculatorString.cs +++ b/src/kOS.Safe/Compilation/CalculatorString.cs @@ -10,26 +10,44 @@ public override object Add(OperandPair pair) { return new StringValue(string.Concat(pair.Left, pair.Right)); } + public override Type GetAddResultType(Type leftType, Type rightType) + => typeof(StringValue); public override object Subtract(OperandPair pair) { throw new KOSBinaryOperandTypeException(pair, "subtract", "from"); } + public override Type GetSubtractResultType(Type leftType, Type rightType) + { + throw new KOSBinaryOperandTypeException(leftType, rightType, "subtract", "from"); + } public override object Multiply(OperandPair pair) { throw new KOSBinaryOperandTypeException(pair, "multiply", "by"); } + public override Type GetMultiplyResultType(Type leftType, Type rightType) + { + throw new KOSBinaryOperandTypeException(leftType, rightType, "multiply", "by"); + } public override object Divide(OperandPair pair) { throw new KOSBinaryOperandTypeException(pair, "divide", "by"); } + public override Type GetDivideResultType(Type leftType, Type rightType) + { + throw new KOSBinaryOperandTypeException(leftType, rightType, "divide", "by"); + } public override object Power(OperandPair pair) { throw new KOSBinaryOperandTypeException(pair, "exponentiate", "by"); } + public override Type GetPowerResultType(Type leftType, Type rightType) + { + throw new KOSBinaryOperandTypeException(leftType, rightType, "exponentiate", "by"); + } public override object GreaterThan(OperandPair pair) { diff --git a/src/kOS.Safe/Compilation/CalculatorStructure.cs b/src/kOS.Safe/Compilation/CalculatorStructure.cs index fa05d6e1c..073deb13d 100644 --- a/src/kOS.Safe/Compilation/CalculatorStructure.cs +++ b/src/kOS.Safe/Compilation/CalculatorStructure.cs @@ -28,6 +28,8 @@ public override object Add(OperandPair pair) throw new KOSException(GetMessage("+", pair)); } + public override Type GetAddResultType(Type leftType, Type rightType) + => GetTypeForOperation(leftType, rightType, "Add", "op_Addition", "+"); public override object Subtract(OperandPair pair) { @@ -47,6 +49,8 @@ public override object Subtract(OperandPair pair) throw new KOSException(GetMessage("-", pair)); } + public override Type GetSubtractResultType(Type leftType, Type rightType) + => GetTypeForOperation(leftType, rightType, "Subtract", "op_Subtraction", "-"); public override object Multiply(OperandPair pair) { @@ -66,6 +70,8 @@ public override object Multiply(OperandPair pair) throw new KOSException(GetMessage("*", pair)); } + public override Type GetMultiplyResultType(Type leftType, Type rightType) + => GetTypeForOperation(leftType, rightType, "Multiply", "op_Multiply", "*"); public override object Divide(OperandPair pair) { @@ -85,6 +91,8 @@ public override object Divide(OperandPair pair) throw new KOSException(GetMessage("/", pair)); } + public override Type GetDivideResultType(Type leftType, Type rightType) + => GetTypeForOperation(leftType, rightType, "Divide", "op_Division", "/"); public override object Power(OperandPair pair) { @@ -104,6 +112,8 @@ public override object Power(OperandPair pair) throw new KOSException(GetMessage("^", pair)); } + public override Type GetPowerResultType(Type leftType, Type rightType) + => GetTypeForOperation(leftType, rightType, "Power", "op_ExclusiveOr", "^"); public override object GreaterThan(OperandPair pair) { @@ -123,6 +133,8 @@ public override object GreaterThan(OperandPair pair) throw new KOSException(GetMessage(">", pair)); } + public override Type GetGreaterThanResultType(Type leftType, Type rightType) + => GetTypeForOperation(leftType, rightType, "GreaterThan", "op_GreaterThan", ">"); public override object LessThan(OperandPair pair) { @@ -142,6 +154,8 @@ public override object LessThan(OperandPair pair) throw new KOSException(GetMessage("<", pair)); } + public override Type GetLessThanResultType(Type leftType, Type rightType) + => GetTypeForOperation(leftType, rightType, "LessThan", "op_LessThan", "<"); public override object GreaterThanEqual(OperandPair pair) { @@ -161,6 +175,8 @@ public override object GreaterThanEqual(OperandPair pair) throw new KOSException(GetMessage(">=", pair)); } + public override Type GetGreaterThanEqualResultType(Type leftType, Type rightType) + => GetTypeForOperation(leftType, rightType, "GreaterThanEqual", "op_GreaterThanEqual", ">="); public override object LessThanEqual(OperandPair pair) { @@ -180,6 +196,8 @@ public override object LessThanEqual(OperandPair pair) throw new KOSException(GetMessage("<=", pair)); } + public override Type GetLessThanEqualResultType(Type leftType, Type rightType) + => GetTypeForOperation(leftType, rightType, "LessThanEqual", "op_LessThanEqual", "<="); public override object NotEqual(OperandPair pair) { @@ -199,6 +217,17 @@ public override object NotEqual(OperandPair pair) return !pair.Left.Equals(pair.Right); } + public override Type GetNotEqualResultType(Type leftType, Type rightType) + { + CheckTypesForNull(leftType, rightType, "NotEqual"); + if (TryTypingExplicit(leftType, rightType, "op_Inequality", out Type result)) + return result; + + if (TryTypingImplicit(leftType, rightType, out Type newLeftType, out Type newRightType)) + return GetNotEqualResultType(newLeftType, newRightType); + + return typeof(BooleanValue); + } public override object Equal(OperandPair pair) { @@ -218,6 +247,29 @@ public override object Equal(OperandPair pair) return pair.Left.Equals(pair.Right); } + public override Type GetEqualResultType(Type leftType, Type rightType) + { + CheckTypesForNull(leftType, rightType, "Equal"); + if (TryTypingExplicit(leftType, rightType, "op_Equality", out Type result)) + return result; + + if (TryTypingImplicit(leftType, rightType, out Type newLeftType, out Type newRightType)) + return GetNotEqualResultType(newLeftType, newRightType); + + return typeof(BooleanValue); + } + + private Type GetTypeForOperation(Type leftType, Type rightType, string opName, string methodName, string opAbbreviation) + { + CheckTypesForNull(leftType, rightType, opName); + if (TryTypingExplicit(leftType, rightType, methodName, out Type result)) + return result; + + if (TryTypingImplicit(leftType, rightType, out Type newLeftType, out Type newRightType)) + return GetTypeForOperation(newLeftType, newRightType, opName, methodName, opAbbreviation); + + throw new KOSException(GetMessage(opAbbreviation, leftType, rightType)); + } private static string GetMessage(string op, OperandPair pair) { @@ -225,6 +277,12 @@ private static string GetMessage(string op, OperandPair pair) string t2 = pair.Right == null ? "" : KOSNomenclature.GetKOSName(pair.Right.GetType()); return string.Format("Cannot perform the operation: {0} On Structures {1} and {2}", op, t1, t2); } + private static string GetMessage(string op, Type left, Type right) + { + string t1 = left == null ? "" : KOSNomenclature.GetKOSName(left.GetType()); + string t2 = right == null ? "" : KOSNomenclature.GetKOSName(right.GetType()); + return string.Format("Cannot perform the operation: {0} On Structures {1} and {2}", op, t1, t2); + } /// /// By default when you call MethodInfo.Invoke() it masks the exceptions @@ -268,6 +326,24 @@ private bool TryInvokeExplicit(OperandPair pair, string methodName, out object r return false; } + private bool TryTypingExplicit(Type left, Type right, string methodName, out Type result) + { + MethodInfo method1 = left.GetMethod(methodName, FLAGS, null, new[] { left, right }, null); + if (method1 != null) + { + result = method1.ReturnType; + return true; + } + MethodInfo method2 = right.GetMethod(methodName, FLAGS, null, new[] { left, right }, null); + if (method2 != null) + { + result = method2.ReturnType; + return true; + } + result = null; + return false; + } + private void CheckPairForNull(OperandPair pair, string opName) { if (pair.Left == null || pair.Right == null) @@ -275,6 +351,11 @@ private void CheckPairForNull(OperandPair pair, string opName) throw new InvalidOperationException(GetMessage(opName, pair)); } } + private void CheckTypesForNull(Type left, Type right, string opName) + { + if (left == null || right == null) + throw new InvalidOperationException(GetMessage(opName, left, right)); + } private bool TryCoerceImplicit(OperandPair pair, out OperandPair resultPair) { @@ -316,5 +397,39 @@ private bool TryCoerceImplicit(OperandPair pair, out OperandPair resultPair) return couldCoerce; } + private bool TryTypingImplicit(Type left, Type right, out Type newLeft, out Type newRight) + { + bool couldCoerce = false; + if (left == right) + { + newLeft = null; + newRight = null; + return false; + } + MethodInfo convert2 = left.GetMethod("op_Implicit", FLAGS | BindingFlags.ExactBinding, null, new[] { right }, null); + if (convert2 != null) + { + couldCoerce = true; + newRight = convert2.ReturnType; + } + else + { + newRight = right; + } + + MethodInfo convert1 = right.GetMethod("op_Implicit", FLAGS | BindingFlags.ExactBinding, null, new[] { left }, null); + if (convert1 != null) + { + couldCoerce = true; + newLeft = convert1.ReturnType; + } + else + { + newLeft = left; + } + + return couldCoerce; + } + } } \ No newline at end of file diff --git a/src/kOS.Safe/Exceptions/KOSBinaryOperandTypeException.cs b/src/kOS.Safe/Exceptions/KOSBinaryOperandTypeException.cs index c8cd52b22..e30959ac5 100644 --- a/src/kOS.Safe/Exceptions/KOSBinaryOperandTypeException.cs +++ b/src/kOS.Safe/Exceptions/KOSBinaryOperandTypeException.cs @@ -27,5 +27,18 @@ public KOSBinaryOperandTypeException(OperandPair pair, string verb, string prepo base(String.Format(TERSE_MSG_FMT, verb, pair.Left.GetType().Name, preposition, pair.Right.GetType().Name)) { } + + /// + /// Describe the error in terms of the two operands and the verb/preposition + /// being done with them. For example: + /// + /// The of the left operand. + /// The of the right operand. + /// present-tense singular conjugation of the operation's verb, i.e "add" + /// preposition usually used with the verb, i.e you add "to", but divide "by". + public KOSBinaryOperandTypeException(Type leftType, Type rightType, string verb, string preposition) : + base(String.Format(TERSE_MSG_FMT, verb, leftType.Name, preposition, rightType.Name)) + { + } } } From e14da0bf61fe6977208ee4c77bf23c018802fb9b Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 16 Mar 2026 21:55:17 -0400 Subject: [PATCH 058/120] Remove null checks in favour of readonly, statically-constructed fields. --- src/kOS.Safe/Compilation/Calculator.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/kOS.Safe/Compilation/Calculator.cs b/src/kOS.Safe/Compilation/Calculator.cs index cb4b5b5ed..4362fc2c9 100644 --- a/src/kOS.Safe/Compilation/Calculator.cs +++ b/src/kOS.Safe/Compilation/Calculator.cs @@ -29,10 +29,10 @@ public abstract class Calculator public virtual Type GetNotEqualResultType(Type leftType, Type rightType) => typeof(BooleanValue); public virtual Type GetEqualResultType(Type leftType, Type rightType) => typeof(BooleanValue); - private static CalculatorScalar calculatorScalar; - private static CalculatorString calculatorString; - private static CalculatorBool calculatorBool; - private static CalculatorStructure calculatorStructure; + private static readonly CalculatorScalar calculatorScalar = new CalculatorScalar(); + private static readonly CalculatorString calculatorString = new CalculatorString(); + private static readonly CalculatorBool calculatorBool = new CalculatorBool(); + private static readonly CalculatorStructure calculatorStructure = new CalculatorStructure(); public static Calculator GetCalculator(OperandPair operandPair) { @@ -50,10 +50,10 @@ public static Calculator GetCalculator(OperandPair operandPair) if (operandPair.Right is ISuffixed) specialCount++; if (operandPair.Right is BooleanValue) boolCount++; - if (scalarCount == 2) return calculatorScalar ?? (calculatorScalar = new CalculatorScalar()); - if (stringCount > 0) return calculatorString ?? (calculatorString = new CalculatorString()); - if (boolCount > 0) return calculatorBool ?? (calculatorBool = new CalculatorBool()); - if (specialCount > 0) return calculatorStructure ?? (calculatorStructure = new CalculatorStructure()); + if (scalarCount == 2) return calculatorScalar; + if (stringCount > 0) return calculatorString; + if (boolCount > 0) return calculatorBool; + if (specialCount > 0) return calculatorStructure; throw new NotImplementedException(string.Format("Can't operate types {0} and {1}", operandPair.Left.GetType(), operandPair.Right.GetType())); } @@ -74,10 +74,10 @@ public static Calculator GetCalculator(Type leftType, Type rightType) if (typeof(ISuffixed).IsAssignableFrom(rightType)) specialCount++; if (typeof(BooleanValue).IsAssignableFrom(rightType)) boolCount++; - if (scalarCount == 2) return calculatorScalar ?? (calculatorScalar = new CalculatorScalar()); - if (stringCount > 0) return calculatorString ?? (calculatorString = new CalculatorString()); - if (boolCount > 0) return calculatorBool ?? (calculatorBool = new CalculatorBool()); - if (specialCount > 0) return calculatorStructure ?? (calculatorStructure = new CalculatorStructure()); + if (scalarCount == 2) return calculatorScalar; + if (stringCount > 0) return calculatorString; + if (boolCount > 0) return calculatorBool; + if (specialCount > 0) return calculatorStructure; throw new NotImplementedException(string.Format("Can't operate types {0} and {1}", leftType, rightType)); } From 50b2dda030f9efd026c35e83cdee0b15e143ed51 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 18 Mar 2026 22:12:28 -0400 Subject: [PATCH 059/120] Add suffix and index type inferencing through a utility class. --- .../Compilation/TypeInferencing.cs | 47 ++++ src/kOS.Safe.Test/Opcode/FakeCpu.cs | 6 +- src/kOS.Safe/Compilation/IR/TypeInferencer.cs | 220 ++++++++++++++++++ 3 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 src/kOS.Safe.Test/Compilation/TypeInferencing.cs create mode 100644 src/kOS.Safe/Compilation/IR/TypeInferencer.cs diff --git a/src/kOS.Safe.Test/Compilation/TypeInferencing.cs b/src/kOS.Safe.Test/Compilation/TypeInferencing.cs new file mode 100644 index 000000000..87a950281 --- /dev/null +++ b/src/kOS.Safe.Test/Compilation/TypeInferencing.cs @@ -0,0 +1,47 @@ +using System; +using NUnit.Framework; +using kOS.Safe.Encapsulation; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Test.Compilation +{ + [TestFixture] + public class TypeInferencing + { + [Test] + public void StructureSuffixTest() + { + // Tests that the bare minimum works + Type structureType = typeof(Encapsulation.Structure); + Type result = TypeInferencer.GetTypeForSuffix(structureType, "TOSTRING"); + Assert.AreEqual(result, typeof(StringValue)); + + Type integerType = typeof(ScalarIntValue); + result = TypeInferencer.GetTypeForSuffix(integerType, "istype"); + Assert.AreEqual(result, typeof(BooleanValue)); + } + + [Test] + public void ListSuffixTest() + { + // Test that lists and enumerables work + Type listType = typeof(ListValue); + Type result = TypeInferencer.GetTypeForSuffix(listType, "ITERATOR"); + Assert.AreEqual(result, typeof(Enumerator)); + + result = TypeInferencer.GetTypeForSuffix(listType, "COPY"); + Assert.AreEqual(result, typeof(ListValue)); + + result = TypeInferencer.GetTypeForIndex(listType); + Assert.AreEqual(result, typeof(Encapsulation.Structure)); + + result = TypeInferencer.GetTypeForSuffix(listType, "CLEAR"); + Assert.AreEqual(result, null); + + // Test an abstract generic type + listType = typeof(EnumerableValue>); + result = TypeInferencer.GetTypeForSuffix(listType, "ITERATOR"); + Assert.AreEqual(result, typeof(Enumerator)); + } + } +} diff --git a/src/kOS.Safe.Test/Opcode/FakeCpu.cs b/src/kOS.Safe.Test/Opcode/FakeCpu.cs index 3be5ffd25..fd664633a 100644 --- a/src/kOS.Safe.Test/Opcode/FakeCpu.cs +++ b/src/kOS.Safe.Test/Opcode/FakeCpu.cs @@ -277,7 +277,7 @@ public void RemovePopContextNotifyee(IPopContextNotifyee notifyee) throw new NotImplementedException(); } - public Compilation.Opcode GetOpcodeAt(int instructionPtr) + public Safe.Compilation.Opcode GetOpcodeAt(int instructionPtr) { throw new NotImplementedException(); } @@ -297,7 +297,7 @@ public List GetCodeFragment(int contextLines) throw new NotImplementedException(); } - public void RunProgram(List program) + public void RunProgram(List program) { throw new NotImplementedException(); } @@ -318,7 +318,7 @@ public void StopCompileStopwatch() throw new NotImplementedException(); } - public Compilation.Opcode GetCurrentOpcode() + public Safe.Compilation.Opcode GetCurrentOpcode() { throw new NotImplementedException(); } diff --git a/src/kOS.Safe/Compilation/IR/TypeInferencer.cs b/src/kOS.Safe/Compilation/IR/TypeInferencer.cs new file mode 100644 index 000000000..5a3e8f044 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/TypeInferencer.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation.Suffixes; + +namespace kOS.Safe.Compilation.IR +{ + public static class TypeInferencer + { + private static readonly Dictionary> suffixDictionaries = new Dictionary>(); + private static IDictionary> structureGlobalSuffixRef; + private static readonly Dictionary indexTypes = new Dictionary(); + private static readonly FieldInfo instanceSuffixesRef; + + static TypeInferencer() + { + instanceSuffixesRef = typeof(Structure).GetField("instanceSuffixes", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + // Handle abstract classes that cannot be done through reflection. + // Structure: + Dictionary structureDict = new Dictionary(StringComparer.OrdinalIgnoreCase); + AddStructureSuffixes(structureDict); + suffixDictionaries.Add(typeof(Structure), structureDict); + + // EnumerableValue: + structureDict = new Dictionary(StringComparer.OrdinalIgnoreCase); + AddStructureSuffixes(structureDict); + AddEnumerableSuffixes(structureDict); + suffixDictionaries.Add(typeof(EnumerableValue<,>), structureDict); + + // CollectionValue: + structureDict = new Dictionary(StringComparer.OrdinalIgnoreCase); + AddStructureSuffixes(structureDict); + AddEnumerableSuffixes(structureDict); + structureDict.Add("CLEAR", null); + suffixDictionaries.Add(typeof(CollectionValue<,>), structureDict); + + // KOSDelegate: + structureDict = new Dictionary(StringComparer.OrdinalIgnoreCase); + AddStructureSuffixes(structureDict); + structureDict.Add("CALL", typeof(Structure)); + structureDict.Add("BIND", typeof(KOSDelegate)); + structureDict.Add("ISDEAD", typeof(BooleanValue)); + suffixDictionaries.Add(typeof(KOSDelegate), structureDict); + + // ScalarValue: + structureDict = new Dictionary(StringComparer.OrdinalIgnoreCase); + AddStructureSuffixes(structureDict); + structureDict.Add("ISINTEGER", typeof(BooleanValue)); + structureDict.Add("ISVALID", typeof(BooleanValue)); + suffixDictionaries.Add(typeof(ScalarValue), structureDict); + + void AddStructureSuffixes(Dictionary suffixDict) + { + suffixDict.Add("TOSTRING", typeof(StringValue)); + suffixDict.Add("HASSUFFIX", typeof(BooleanValue)); + suffixDict.Add("SUFFIXNAMES", typeof(ListValue)); + suffixDict.Add("ISSERIALIZABLE", typeof(BooleanValue)); + suffixDict.Add("TYPENAME", typeof(StringValue)); + suffixDict.Add("ISTYPE", typeof(BooleanValue)); + suffixDict.Add("INHERITANCE", typeof(StringValue)); + } + void AddEnumerableSuffixes(Dictionary suffixDict) + { + suffixDict.Add("ITERATOR", typeof(Enumerator)); + suffixDict.Add("REVERSEITERATOR", typeof(Enumerator)); + suffixDict.Add("LENGTH", typeof(ScalarValue)); + suffixDict.Add("CONTAINS", typeof(BooleanValue)); + suffixDict.Add("EMPTY", typeof(BooleanValue)); + suffixDict.Add("DUMP", typeof(StringValue)); + } + } + + public static Type GetTypeForSuffix(Type structureType, string suffix) + { + if (!typeof(ISuffixed).IsAssignableFrom(structureType)) + throw new InvalidOperationException($"Tried to get suffixes on a non-suffixed type {structureType}."); + + if (suffixDictionaries.ContainsKey(structureType)) + { + if (suffixDictionaries[structureType].TryGetValue(suffix, out Type suffixType)) + return suffixType; + if (typeof(Lexicon).IsAssignableFrom(structureType)) + return typeof(Structure); + throw new Exceptions.KOSSuffixUseException("get", suffix, structureType.ToString()); + } + + // Processing won't work for an abstract type. They should already be added in the constructor. + // But the constructor only considers the generic type definition. + if (structureType.IsAbstract && structureType.IsGenericType) + return GetTypeForSuffix(structureType.GetGenericTypeDefinition(), suffix); + + ProcessTypeForSuffixes(structureType); + return GetTypeForSuffix(structureType, suffix); + } + + private static void ProcessTypeForSuffixes(Type type) + { + if (!typeof(Structure).IsAssignableFrom(type)) + throw new Exceptions.KOSYouShouldNeverSeeThisException($"Tried to process the suffix types from a non-Structure type {type}."); + if (type.IsAbstract) + throw new Exceptions.KOSYouShouldNeverSeeThisException($"Tried to process the suffixes of an abstract type."); + + Dictionary suffixes = new Dictionary(StringComparer.OrdinalIgnoreCase); + suffixDictionaries.Add(type, suffixes); + + // Create an instance that we can examine. + Structure instance = (Structure)Activator.CreateInstance(type, true); + // Call HasSuffix on the instance object to force the lazy initialization of suffixes to act. + instance.HasSuffix(string.Empty); + + // Grab the instanceSuffixes field and peek at its values + //var test2 = typeof(Structure).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + //var test = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + //FieldInfo instanceSuffixField = type.GetField("instanceSuffixes", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + IDictionary instanceSuffixes = (IDictionary)instanceSuffixesRef.GetValue(instance); + + // We'll also grab the globalSuffixes field + IEnumerable> globalSuffixes; + if (structureGlobalSuffixRef == null) + { + FieldInfo globalSuffixField = typeof(Structure).GetField("globalSuffixes", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + structureGlobalSuffixRef = (IDictionary>)globalSuffixField.GetValue(null); + } + if (structureGlobalSuffixRef.ContainsKey(type)) + globalSuffixes = structureGlobalSuffixRef[type]; + else + globalSuffixes = Enumerable.Empty>(); + + foreach (KeyValuePair suffixEntry in instanceSuffixes.Union(globalSuffixes)) + { + string suffixName = suffixEntry.Key; + ISuffix suffixObj = suffixEntry.Value; + Type iSuffixType = suffixObj.GetType(); + + if (!iSuffixType.IsGenericType) + { + // Being non-generic means that no return type is defined + // This covers: + // NoArgsVoidSuffix + suffixes.Add(suffixName, null); + continue; + } + + // Strip the generic type arguments to allow comparison + Type suffixGenericType = iSuffixType.GetGenericTypeDefinition(); + + // Check against the generic types where the return type is the first argument: + if (typeof(NoArgsSuffix<>).IsAssignableFrom(suffixGenericType) || + typeof(OneArgsSuffix<,>).IsAssignableFrom(suffixGenericType) || + typeof(TwoArgsSuffix<,,>).IsAssignableFrom(suffixGenericType) || + typeof(ThreeArgsSuffix<,,,>).IsAssignableFrom(suffixGenericType) || + typeof(VarArgsSuffix<,>).IsAssignableFrom(suffixGenericType) || + typeof(OptionalArgsSuffix<>).IsAssignableFrom(suffixGenericType) || + typeof(Suffix<>).IsAssignableFrom(suffixGenericType)) + { + suffixes.Add(suffixName, iSuffixType.GetGenericArguments()[0]); + continue; + } + + // Check against the void return types: + if (typeof(OneArgsSuffix<>).IsAssignableFrom(suffixGenericType) || + typeof(TwoArgsSuffix<,>).IsAssignableFrom(suffixGenericType) || + typeof(ThreeArgsSuffix<,,>).IsAssignableFrom(suffixGenericType)) + { + suffixes.Add(suffixName, null); + continue; + } + +#if DEBUG + throw new NotImplementedException($"Suffix {suffixName} in type {type} is not implemented in TypeInferencer. Probably because {iSuffixType} is not matched in the decision tree."); +#else + // Fail gently, a null return will disable potential optimizations but won't break anything. + Utilities.SafeHouse.Logger.LogError($"Suffix {suffixName} in type {type} is not implemented in TypeInferencer. Probably because {iSuffixType} is not matched in the decision tree."); + suffixes.Add(suffixName, null); +#endif + } + } + + public static Type GetTypeForIndex(Type type) + { + if (!typeof(IIndexable).IsAssignableFrom(type)) + throw new InvalidOperationException($"Tried to get the index from a non-indexable type {type}"); + + if (indexTypes.TryGetValue(type, out Type indexType)) + { + return indexType; + } + indexType = ProcessIndexTypesForType(type); + indexTypes.Add(type, indexType); + return indexType; + } + + private static Type ProcessIndexTypesForType(Type type) + { + if (!typeof(Structure).IsAssignableFrom(type)) + throw new Exceptions.KOSYouShouldNeverSeeThisException($"Tried to process the index from a non-Structure type {type}."); + + if (!type.IsGenericType) + { + // Covers Lexicon + return typeof(Structure); + } + + Type genericType = type.GetGenericTypeDefinition(); + // Covers List, Range, Queue, Stack + if (typeof(EnumerableValue<,>).IsAssignableFrom(genericType)) + return genericType.GetGenericArguments()[0]; + +#if DEBUG + throw new Exceptions.KOSYouShouldNeverSeeThisException($"A kOS developer hasn't implemented indexing correctly in TypeInferencer for {type}"); +#else + // Fail gently, a null return will disable potential optimizations but won't break anything. + Utilities.SafeHouse.Logger.LogError($"Type {type} is not implemented in TypeInferencer for indexing."); + return typeof(Structure); +#endif + } + } +} From 9eeb2ca799eba283d645c0030d1cb3e362a8d843 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 21 Mar 2026 15:55:25 -0400 Subject: [PATCH 060/120] Finish implementing type inferencing through IRInstructions and IRValues. Also converts the previously-unused IRInstruction.SideEffects property to IsInvariant, which indicates that the effect of the instruction, or the value of the IRValue, can be fully known at compile time. --- .../Compilation/TypeInferencing.cs | 48 ++- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 327 ++++++++---------- src/kOS.Safe/Compilation/IR/IRValue.cs | 101 ++++-- .../Compilation/IR/IResultingInstruction.cs | 3 + .../Optimization/Passes/SuffixReplacement.cs | 2 +- 5 files changed, 260 insertions(+), 221 deletions(-) diff --git a/src/kOS.Safe.Test/Compilation/TypeInferencing.cs b/src/kOS.Safe.Test/Compilation/TypeInferencing.cs index 87a950281..e90967a1b 100644 --- a/src/kOS.Safe.Test/Compilation/TypeInferencing.cs +++ b/src/kOS.Safe.Test/Compilation/TypeInferencing.cs @@ -1,10 +1,36 @@ using System; using NUnit.Framework; -using kOS.Safe.Encapsulation; +using kOS.Safe.Compilation; using kOS.Safe.Compilation.IR; +using kOS.Safe.Encapsulation; +using kOS.Safe.Test.Execution; +using kOS.Safe.Utilities; namespace kOS.Safe.Test.Compilation { + + [SetUpFixture] + public class StaticSetup + { + [SetUp] + public void Setup() + { + SafeHouse.Init(new Config(), new VersionInfo(0, 0, 0, 0), "", false, "./"); + SafeHouse.Logger = new NoopLogger(); + + try + { + AssemblyWalkAttribute.Walk(); + } + catch (Exception e) + { + Console.WriteLine(e); + Console.WriteLine(e.StackTrace); + throw; + } + } + } + [TestFixture] public class TypeInferencing { @@ -43,5 +69,25 @@ public void ListSuffixTest() result = TypeInferencer.GetTypeForSuffix(listType, "ITERATOR"); Assert.AreEqual(result, typeof(Enumerator)); } + + [Test] + public void TestInstructionInferencing() + { + IRConstant a = new IRConstant(ScalarIntValue.One, 0, 0); + IRConstant b = new IRConstant(ScalarIntValue.Two, 0, 0); + IRTemp result = new IRTemp(0); + IRBinaryOp add = new IRBinaryOp(result, new OpcodeMathAdd(), a, b); + + Assert.AreEqual(add.ResultType, typeof(ScalarValue)); + + IRCall call = new IRCall(result, new OpcodeCall("sin"), true, b); + Assert.IsTrue(typeof(ScalarValue).IsAssignableFrom(call.ResultType)); + + IRCall print = new IRCall(result, new OpcodeCall("print"), true, a); + Assert.AreEqual(print.ResultType, null); + + IRCall userCall = new IRCall(result, new OpcodeCall("$test*"), true, a, b); + Assert.AreEqual(userCall.ResultType, typeof(Encapsulation.Structure)); + } } } diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index b0477d47e..66b0139fa 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -9,8 +9,7 @@ public abstract class IRInstruction public short SourceLine { get; private set; } // line number in the source code that this was compiled from. public short SourceColumn { get; private set; } // column number of the token nearest the cause of this Opcode. - // Should-be-static - public abstract bool SideEffects { get; } + public abstract bool IsInvariant { get; } internal abstract IEnumerable EmitOpcode(); protected IRInstruction(Opcode originalOpcode) { @@ -31,27 +30,7 @@ public void OverwriteSourceLocation(short sourceLine, short sourceColumn) SourceColumn = sourceColumn; } } - public abstract class IRInteractsInstruction : IRInstruction - { - public override bool SideEffects { get; } - protected IRInteractsInstruction(IRValue interactor, Opcode originalOpcode) : base(originalOpcode) - { - if (interactor is IRConstant) - { - SideEffects = false; - return; - } - switch (interactor.Type) - { - case IRValue.ValueType.Value: - SideEffects = false; - break; - default: - SideEffects = true; - break; - } - } - } + public class IRAssign : IRInstruction, ISingleOperandInstruction { public enum StoreScope @@ -60,7 +39,7 @@ public enum StoreScope Local, Global } - public override bool SideEffects => false; + public override bool IsInvariant => Value.IsInvariant; public IRVariableBase Target { get; set; } public IRValue Value { get; set; } public StoreScope Scope { get; set; } = StoreScope.Ambivalent; @@ -107,14 +86,7 @@ public override int GetHashCode() } public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { - public override bool SideEffects => false; - public IRValue Result { get; set; } - public BinaryOpcode Operation { get; set; } - public IRValue Left { get; set; } - public IRValue Right { get; set; } - public IEnumerable Operands { get { yield return Left; yield return Right; } } - public int OperandCount => 2; - private static Type[] commutativeTypes = + private static readonly Type[] commutativeTypes = { typeof(OpcodeCompareEqual), typeof(OpcodeCompareNE), @@ -125,9 +97,52 @@ public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperand typeof(OpcodeMathAdd), typeof(OpcodeMathMultiply) }; + + public override bool IsInvariant => Left.IsInvariant && Right.IsInvariant; + public IRValue Result { get; set; } + public BinaryOpcode Operation { get; set; } + public IRValue Left { get; set; } + public IRValue Right { get; set; } + public IEnumerable Operands { get { yield return Left; yield return Right; } } + public int OperandCount => 2; + public Type ResultType + { + get + { + Calculator calculator = Calculator.GetCalculator(Left.ValueType, Right.ValueType); + switch (Operation) + { + case OpcodeMathAdd _: + return calculator.GetAddResultType(Left.ValueType, Right.ValueType); + case OpcodeMathSubtract _: + return calculator.GetSubtractResultType(Left.ValueType, Right.ValueType); + case OpcodeMathMultiply _: + return calculator.GetMultiplyResultType(Left.ValueType, Right.ValueType); + case OpcodeMathDivide _: + return calculator.GetDivideResultType(Left.ValueType, Right.ValueType); + case OpcodeMathPower _: + return calculator.GetPowerResultType(Left.ValueType, Right.ValueType); + case OpcodeCompareEqual _: + return calculator.GetEqualResultType(Left.ValueType, Right.ValueType); + case OpcodeCompareNE _: + return calculator.GetNotEqualResultType(Left.ValueType, Right.ValueType); + case OpcodeCompareGT _: + return calculator.GetGreaterThanResultType(Left.ValueType, Right.ValueType); + case OpcodeCompareLT _: + return calculator.GetLessThanResultType(Left.ValueType, Right.ValueType); + case OpcodeCompareGTE _: + return calculator.GetGreaterThanEqualResultType(Left.ValueType, Right.ValueType); + case OpcodeCompareLTE _: + return calculator.GetLessThanEqualResultType(Left.ValueType, Right.ValueType); + default: + throw new NotImplementedException(); + } + } + } + IRValue IMultipleOperandInstruction.this[int index] { - get => index == 0 ? Left : index == 1 ? Right : throw new System.ArgumentOutOfRangeException(); + get => index == 0 ? Left : index == 1 ? Right : throw new ArgumentOutOfRangeException(); set { if (index == 0) @@ -135,7 +150,7 @@ IRValue IMultipleOperandInstruction.this[int index] else if (index == 1) Right = value; else - throw new System.ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(); } } public bool IsCommutative => commutativeTypes.Contains(Operation.GetType()); @@ -195,10 +210,27 @@ public override int GetHashCode() } public class IRUnaryOp : IRInstruction, IResultingInstruction, ISingleOperandInstruction { - public override bool SideEffects => false; + public override bool IsInvariant => Operand.IsInvariant; public IRValue Result { get; set; } public Opcode Operation { get; } public IRValue Operand { get; set; } + public Type ResultType + { + get + { + switch (Operation) + { + case OpcodeExists _: + case OpcodeLogicNot _: + case OpcodeLogicToBool _: + return typeof(Encapsulation.BooleanValue); + case OpcodeMathNegate _: + return Operand.ValueType; + default: + throw new NotImplementedException(); + } + } + } public IRUnaryOp(IRTemp result, Opcode operation, IRValue operand) : base(operation) { Result = result; @@ -223,7 +255,7 @@ public override int GetHashCode() } public class IRNoStackInstruction : IRInstruction { - public override bool SideEffects => false; + public override bool IsInvariant => false; public Opcode Operation { get; } public IRNoStackInstruction(Opcode opcode) : base(opcode) => Operation = opcode; @@ -241,14 +273,15 @@ public override int GetHashCode() } public class IRUnaryConsumer : IRInstruction, ISingleOperandInstruction { - public override bool SideEffects { get; } + private readonly bool operationHasSideEffects; + public override bool IsInvariant => !operationHasSideEffects && Operand.IsInvariant; public Opcode Operation { get; } public IRValue Operand { get; set; } public IRUnaryConsumer(Opcode opcode, IRValue operand, bool sideEffects = false) : base(opcode) { Operation = opcode; Operand = operand; - SideEffects = sideEffects; + operationHasSideEffects = sideEffects; } internal override IEnumerable EmitOpcode() { @@ -268,7 +301,7 @@ public override int GetHashCode() } public class IRPop : IRInstruction, ISingleOperandInstruction { - public override bool SideEffects => false; + public override bool IsInvariant => Value.IsInvariant; public IRValue Value { get; set; } IRValue ISingleOperandInstruction.Operand { get => Value; set => Value = value; } public IRPop(IRValue value, OpcodePop opcode) : base(opcode) @@ -291,10 +324,22 @@ public override int GetHashCode() } public class IRNonVarPush : IRInstruction, IResultingInstruction { - public override bool SideEffects => false; + public override bool IsInvariant => false; public Opcode Operation { get; } - public IRValue Result { get; } + public Type ResultType + { + get + { + switch (Operation) + { + case OpcodeTestArgBottom _: + return typeof(Encapsulation.BooleanValue); + default: + throw new NotImplementedException(); + } + } + } public IRNonVarPush(IRValue result, Opcode opcode) : base(opcode) { @@ -314,13 +359,15 @@ public override bool Equals(object obj) public override int GetHashCode() => Operation.GetHashCode(); } - public class IRSuffixGet : IRInteractsInstruction, IResultingInstruction, ISingleOperandInstruction + public class IRSuffixGet : IRInstruction, IResultingInstruction, ISingleOperandInstruction { + public override bool IsInvariant => Object.IsInvariant; public IRValue Result { get; set; } public IRValue Object { get; set; } public string Suffix { get; set; } IRValue ISingleOperandInstruction.Operand { get => Object; set => Object = value; } - public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcodeGetMember) : base(obj, opcodeGetMember) + public Type ResultType => TypeInferencer.GetTypeForSuffix(Object.ValueType, Suffix); + public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcodeGetMember) : base(opcodeGetMember) { Result = result; Object = obj; @@ -344,7 +391,6 @@ public override int GetHashCode() } public class IRSuffixGetMethod : IRSuffixGet { - public override bool SideEffects => false; public IRSuffixGetMethod(IRTemp result, IRValue obj, OpcodeGetMethod opcode) : base(result, obj, opcode) { } internal override IEnumerable EmitOpcode() { @@ -361,16 +407,16 @@ public override bool Equals(object obj) public override int GetHashCode() => (Object, Suffix).GetHashCode(); } - public class IRSuffixSet : IRInteractsInstruction, IMultipleOperandInstruction + public class IRSuffixSet : IRInstruction, IMultipleOperandInstruction { - public override bool SideEffects { get; } + public override bool IsInvariant => false; public IRValue Object { get; set; } public IRValue Value { get; set; } public IEnumerable Operands { get { yield return Object; yield return Value; } } public int OperandCount => 2; IRValue IMultipleOperandInstruction.this[int index] { - get => index == 0 ? Object : index == 1 ? Value : throw new System.ArgumentOutOfRangeException(); + get => index == 0 ? Object : index == 1 ? Value : throw new ArgumentOutOfRangeException(); set { if (index == 0) @@ -378,11 +424,11 @@ IRValue IMultipleOperandInstruction.this[int index] else if (index == 1) Value = value; else - throw new System.ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(); } } public string Suffix { get; } - public IRSuffixSet(IRValue obj, IRValue value, OpcodeSetMember opcodeSetMember) : base(obj, opcodeSetMember) + public IRSuffixSet(IRValue obj, IRValue value, OpcodeSetMember opcodeSetMember) : base(opcodeSetMember) { Object = obj; Value = value; @@ -408,12 +454,13 @@ public override int GetHashCode() } public class IRIndexGet : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { - public override bool SideEffects => false; + public override bool IsInvariant => Object.IsInvariant && Index.IsInvariant; public IRValue Result { get; } public IRValue Object { get; set; } public IRValue Index { get; set; } public IEnumerable Operands { get { yield return Object; yield return Index; } } public int OperandCount => 2; + public Type ResultType => TypeInferencer.GetTypeForIndex(Object.ValueType); IRValue IMultipleOperandInstruction.this[int index] { get => index == 0 ? Object : index == 1 ? Index : throw new System.ArgumentOutOfRangeException(); @@ -452,7 +499,7 @@ public override int GetHashCode() } public class IRIndexSet : IRInstruction, IMultipleOperandInstruction { - public override bool SideEffects => false; + public override bool IsInvariant => false; public IRValue Object { get; set; } public IRValue Index { get; set; } public IRValue Value { get; set; } @@ -501,7 +548,7 @@ public override int GetHashCode() } public class IRJump : IRInstruction { - public override bool SideEffects => false; + public override bool IsInvariant => true; public BasicBlock Target { get; set; } public IRJump(BasicBlock target, OpcodeBranchJump opcode) : base(opcode) { @@ -523,7 +570,7 @@ public override int GetHashCode() } public class IRJumpStack : IRInstruction, ISingleOperandInstruction { - public override bool SideEffects => false; + public override bool IsInvariant => Distance.IsInvariant; public IRValue Distance { get; set; } IRValue ISingleOperandInstruction.Operand { get => Distance; set => Distance = value; } public List Targets { get; } = new List(); @@ -547,7 +594,7 @@ public override int GetHashCode() } public class IRBranch : IRInstruction, ISingleOperandInstruction { - public override bool SideEffects => false; + public override bool IsInvariant => Condition.IsInvariant; public IRValue Condition { get; set; } IRValue ISingleOperandInstruction.Operand { get => Condition; set => Condition = value; } public BasicBlock True { get; set; } @@ -587,13 +634,31 @@ public override int GetHashCode() } public class IRCall : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { + private bool isResultTypeInformed = false; + private Type resultType = null; + private bool? isFunctionInvariant = null; + protected static readonly Function.FunctionManager functionManager = new Function.FunctionManager(null); - public override bool SideEffects { get; } + public bool IsFunctionInvariant + { + get => isFunctionInvariant ?? IsCallInvariant(Function); + set => isFunctionInvariant = value; + } + public override bool IsInvariant => IsFunctionInvariant && Arguments.All(a => a.IsInvariant); public IRValue Result { get; set; } public string Function { get; } public List Arguments { get; } = new List(); public IEnumerable Operands => Enumerable.Reverse(Arguments); public int OperandCount => Arguments.Count; + public Type ResultType + { + get => isResultTypeInformed ? resultType : GetDefaultReturnType(); + set + { + resultType = value; + isResultTypeInformed = true; + } + } IRValue IMultipleOperandInstruction.this[int index] { get => Arguments[index]; @@ -607,147 +672,27 @@ private IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker) : base(opco Result = target; Function = (string)opcode.Destination; Direct = opcode.Direct; - SideEffects = CheckIfFunctionHasSideEffects(opcode); EmitArgMarker = emitArgMarker; } - private bool CheckIfFunctionHasSideEffects(OpcodeCall opcode) + private bool IsCallInvariant(string functionName) { if (!Direct) - return true; - if (opcode.Destination is string functionName && functionManager.Exists(functionName)) + return false; + if (functionManager.Exists(functionName)) { - functionName = functionName.ToLower().Replace("()", ""); - switch (functionName) - { - case "vcrs": - case "vectorcrossproduct": - case "vdot": - case "vectordotproduct": - case "vxcl": - case "vectorexclude": - case "vang": - case "vectorangle": - case "clearscreen": - case "hudtext": - case "add": - case "remove": - case "processor": - case "edit": - case "printlist": - case "node": - case "v": - case "r": - case "q": - case "createorbit": - case "rotatefromto": - case "lookdirup": - case "angleaxis": - case "latlng": - case "vessel": - case "body": - case "bodyexists": - case "bodyatmosphere": - case "bounds": - case "heading": - case "slidenote": - case "note": - case "getvoice": - case "stopallvoices": - case "time": - case "timestamp": - case "timespan": - case "hsv": - case "hsva": - case "vecdraw": - case "vecdrawargs": - case "clearvecdraws": - case "clearguis": - case "gui": - case "career": - case "lex": - case "lexicon": - case "list": - case "pidloop": - case "queue": - case "stack": - case "uniqueset": - case "abs": - case "mod": - case "floor": - case "ceiling": - case "round": - case "sqrt": - case "ln": - case "log10": - case "min": - case "max": - case "random": - case "randomseed": - case "char": - case "unchar": - case "range": - case "constant": - case "sin": - case "cos": - case "tan": - case "arcsin": - case "arccos": - case "arctan": - case "arctan2": - case "anglediff": - return false; - case "addAlarm": - case "listAlarms": - case "deleteAlarm": - case "buildlist": - case "positionat": - case "velocityat": - case "orbitat": - case "allwaypoints": - case "waypoint": - case "print": - case "printat": - case "logfile": - case "debugdump": - case "debugfreezegame": - case "profileresult": - case "makebuiltindelegate": - case "droppriority": - case "stage": - case "warpto": - case "transfer": - case "transferall": - case "run": - case "load": - case "toggleflybywire": - case "selectautopilotmode": - case "reboot": - case "shutdown": - case "scriptpath": - case "switch": - case "cd": - case "chdir": - case "copy_deprecated": - case "rename_file_deprecated": - case "rename_volume_deprecated": - case "delete_deprecated": - case "copypath": - case "movepath": - case "deletepath": - case "writejson": - case "readjson": - case "exists": - case "open": - case "create": - case "createdir": - case "path": - case "volume": - default: - return true; - } + return functionManager.IsFunctionInvariant(functionName); } - else - return true; + return false; + + } + private Type GetDefaultReturnType() + { + if (functionManager.Exists(Function)) + return functionManager.FunctionReturnType(Function); + if (IndirectMethod is IRTemp tempSuffixCall && + tempSuffixCall.Parent is IRSuffixGetMethod suffixGetMethod) + return suffixGetMethod.ResultType; + return typeof(Encapsulation.Structure); } internal override IEnumerable EmitOpcode() { @@ -789,7 +734,7 @@ public override int GetHashCode() } public class IRReturn : IRInstruction, ISingleOperandInstruction { - public override bool SideEffects => false; + public override bool IsInvariant => Value.IsInvariant; public IRValue Value { get; set; } IRValue ISingleOperandInstruction.Operand { get => Value; set => Value = value; } public short Depth { get; internal set; } diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index 5fe141e9b..9b010f250 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -1,23 +1,30 @@ +using System; using System.Collections.Generic; namespace kOS.Safe.Compilation.IR { public abstract class IRValue { - public enum ValueType - { - Unknown = 0, - Value = 1, - GameObject = 2 - } - public ValueType Type { get; } + public virtual Type ValueType { get; set; } = typeof(Encapsulation.Structure); + public abstract bool IsInvariant { get; set; } internal abstract IEnumerable EmitPush(); } public class IRConstant : IRValue { - public object Value { get; } protected readonly short sourceLine, sourceColumn; + + public override bool IsInvariant + { + get => true; + set + { + if (value) + return; + throw new InvalidOperationException("Cannot set the invariant state of a constant to false."); + } + } + public object Value { get; } public IRConstant(object value, Opcode opcode) : this(value, opcode.SourceLine, opcode.SourceColumn) { } public IRConstant(object value, IRInstruction instruction) : this(value, instruction.SourceLine, instruction.SourceColumn) { } public IRConstant(object value, short sourceLine, short sourceColumn) @@ -25,6 +32,7 @@ public IRConstant(object value, short sourceLine, short sourceColumn) Value = value; this.sourceLine = sourceLine; this.sourceColumn = sourceColumn; + ValueType = value.GetType(); } internal override IEnumerable EmitPush() { @@ -52,22 +60,23 @@ public IRVariableBase(string name, IRScope declaringScope) } public override string ToString() => $"{Name} {Scope.IndexString()}"; - public override bool Equals(object obj) - => obj is IRVariableBase variable && - (Scope == variable.Scope || - Scope.IsEncompassedBy(variable.Scope) || - variable.Scope.IsEncompassedBy(Scope)) && - string.Equals(Name, variable.Name, System.StringComparison.OrdinalIgnoreCase); - public override int GetHashCode() + protected bool NameAndScopeEquals(IRVariableBase variable) + => string.Equals(Name, variable.Name, StringComparison.OrdinalIgnoreCase) && + (Scope.IsEqualOrEncompassedBy(variable.Scope) || + variable.Scope.IsEncompassedBy(Scope)); + protected int GetBaseHashCode() => Name.ToLower().GetHashCode(); } public class IRVariable : IRVariableBase { - protected readonly short sourceLine, sourceColumn; + private ushort nextSSAIndex = 0; + private readonly Dictionary iterations = new Dictionary(); + internal readonly short sourceLine, sourceColumn; + + public override bool IsInvariant { get; set; } = false; public bool IsLock { get; } - public override IRScope Scope { get => base.Scope; } - public IRVariable(string name, IRScope scope, IRInstruction instruction, bool isLock = false) : - this(name, scope, instruction.SourceLine, instruction.SourceColumn, isLock) { } + public IReadOnlyDictionary Iterations => iterations; + public IRVariable(OpcodeIdentifierBase opcode, IRScope scope, bool isLock = false) : this(opcode.Identifier, scope, opcode, isLock) { } public IRVariable(string name, IRScope scope, Opcode opcode, bool isLock = false) : @@ -87,10 +96,20 @@ internal override IEnumerable EmitPush() SourceColumn = sourceColumn }; } + public override bool Equals(object obj) + => !(obj is SSAVariable) && + obj is IRVariable variable && + NameAndScopeEquals(variable); + public override int GetHashCode() + => GetBaseHashCode(); } public class IRRelocateLater : IRConstant { - public IRRelocateLater(string value, OpcodePushRelocateLater opcode) : base(value, opcode) { } + public IRRelocateLater(string value, OpcodePushRelocateLater opcode) : base(value, opcode) + { + // Not technically correct, but this removes it from any optimization. + ValueType = null; + } internal override IEnumerable EmitPush() { @@ -121,8 +140,18 @@ public class IRTemp : IRVariableBase { private bool isPromoted = false; + public override Type ValueType + { + get => ((IResultingInstruction)Parent).ResultType; + set => throw new InvalidOperationException($"Cannot set the result type state of an {nameof(IRTemp)}."); + } public int ID { get; } public IRInstruction Parent { get; internal set; } + public override bool IsInvariant + { + get => Parent.IsInvariant; + set => throw new InvalidOperationException($"Cannot set the invariant state of an {nameof(IRTemp)}."); + } public IRTemp(int id) : base($"$.temp.{id}", null) { @@ -156,6 +185,13 @@ internal override IEnumerable EmitPush() foreach (Opcode opcode in Parent.EmitOpcode()) yield return opcode; } + + public override string ToString() + { + if (isPromoted) + return base.ToString(); + return $"| {Parent}"; + } public override bool Equals(object obj) { if (obj is IRTemp temp) @@ -171,21 +207,30 @@ public override bool Equals(object obj) if (obj is IRVariable variable) { return isPromoted && - base.Equals(variable); + NameAndScopeEquals(variable); } return false; } - public override string ToString() - { - if (isPromoted) - return base.ToString(); - return $"| {Parent}"; - } public override int GetHashCode() - => base.GetHashCode(); + => GetBaseHashCode(); } public class IRParameter : IRValue { + public override Type ValueType + { + get => null; + set => throw new InvalidOperationException("Cannot set the value type of a parameter."); + } + public override bool IsInvariant + { + get => false; + set + { + if (!value) + return; + throw new InvalidOperationException("Cannot set the invariant state of a parameter to true."); + } + } internal override IEnumerable EmitPush() => System.Linq.Enumerable.Empty(); } } diff --git a/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs index a9c7bfcdb..da221dcd4 100644 --- a/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs @@ -1,7 +1,10 @@ +using System; + namespace kOS.Safe.Compilation.IR { public interface IResultingInstruction { IRValue Result { get; } + Type ResultType { get; } } } \ No newline at end of file diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs index 14feaa9f5..49a4bc097 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs @@ -101,7 +101,7 @@ private static IRValue AttempReplaceSuffix(IRSuffixGet suffixGet) return suffixGet.Result; } // Instead of the IRTemp, which leads to resolving the suffix, return just the alias shortcut. - suffixGet.Result = new IRVariable($"${suffixGet.Suffix}", null, suffixGet); + suffixGet.Result = new IRVariable($"${suffixGet.Suffix}", null, suffixGet.SourceLine, suffixGet.SourceColumn); return suffixGet.Result; } if (objVariable.Name == "$constant") From ee2920059fea08fbf4adfccefa92edb5410c02f3 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 21 Mar 2026 17:45:27 -0400 Subject: [PATCH 061/120] Properly implement static single assignment. This breaks the previous ConstantPropagation pass, which needs rewriting. Variables that are written to inside of triggers and global variables are excluded from SSA conversion, and are represented instead with the base IRVariable instead of the derived SSAVariable. Two areas remain problematic for type inferencing: determining the type of a phi value that when a block precedes itself (requires solving a circular reference), and determining the type of a local variable that is written to in a function (requires resolving blocks in call order, which could be a problem in the case of circular calls). --- .../Compilation/CalculatorStructure.cs | 6 +- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 43 ++- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 52 +++- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 73 ++++- src/kOS.Safe/Compilation/IR/IRScope.cs | 18 +- src/kOS.Safe/Compilation/IR/IRValue.cs | 44 ++- .../Compilation/IR/SingleStaticAssignment.cs | 283 ++++++++++++++++++ .../Passes/ConstantPropagation.cs | 2 +- 8 files changed, 486 insertions(+), 35 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs diff --git a/src/kOS.Safe/Compilation/CalculatorStructure.cs b/src/kOS.Safe/Compilation/CalculatorStructure.cs index 073deb13d..aa932faec 100644 --- a/src/kOS.Safe/Compilation/CalculatorStructure.cs +++ b/src/kOS.Safe/Compilation/CalculatorStructure.cs @@ -268,7 +268,7 @@ private Type GetTypeForOperation(Type leftType, Type rightType, string opName, s if (TryTypingImplicit(leftType, rightType, out Type newLeftType, out Type newRightType)) return GetTypeForOperation(newLeftType, newRightType, opName, methodName, opAbbreviation); - throw new KOSException(GetMessage(opAbbreviation, leftType, rightType)); + return typeof(Structure); } private static string GetMessage(string op, OperandPair pair) @@ -279,8 +279,8 @@ private static string GetMessage(string op, OperandPair pair) } private static string GetMessage(string op, Type left, Type right) { - string t1 = left == null ? "" : KOSNomenclature.GetKOSName(left.GetType()); - string t2 = right == null ? "" : KOSNomenclature.GetKOSName(right.GetType()); + string t1 = left == null ? "" : KOSNomenclature.GetKOSName(left); + string t2 = right == null ? "" : KOSNomenclature.GetKOSName(right); return string.Format("Cannot perform the operation: {0} On Structures {1} and {2}", op, t1, t2); } diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index b3d6cec95..bfa7b5241 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -14,6 +14,8 @@ public class BasicBlock private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. private readonly string nonSequentialLabel = null; private IRScope scope; + private readonly HashSet variablesWritten = new HashSet(); + private readonly HashSet variablesRead = new HashSet(); public IRScope Scope { @@ -30,6 +32,12 @@ public IRScope Scope public List Instructions { get; } = new List(); public IReadOnlyCollection Successors => successors; public IReadOnlyCollection Predecessors => predecessors; + public HashSet VariablesWritten => variablesWritten; + public IReadOnlyCollection VariablesRead => variablesRead; + public Dictionary values)> Phis { get; } = + new Dictionary values)>(); + public HashSet IncomingVariableDefinitions { get; set; } + public HashSet TriggerPropagationBlacklist { get; } = new HashSet(); public string Label => nonSequentialLabel ?? $"@BB#{ID}"; public int ID { get; } public BasicBlock Dominator @@ -151,27 +159,44 @@ private static void DepthFirstSearch(BasicBlock block, HashSet visit postorder.Add(block); } + public void AddParameter(IRParameter parameter) { parameters.Add(parameter); } - public void StoreLocalVariable(IRVariableBase variable) - => Scope.StoreLocalVariable(variable); - public void StoreGlobalVariable(IRVariableBase variable) - => Scope.StoreGlobalVariable(variable); - public void StoreVariable(IRVariableBase variable) - => Scope.StoreVariable(variable); - public bool TryStoreVariable(IRVariableBase variable) - => Scope.TryStoreVariable(variable); + public void StoreLocalVariable(SSAVariable variable) + { + SingleStaticAssignment.OverwriteVariable(variablesWritten, variable); + Scope.StoreLocalVariable(variable.Parent); + } + public void StoreGlobalVariable(SSAVariable variable) + { + SingleStaticAssignment.OverwriteVariable(variablesWritten, variable); + Scope.StoreGlobalVariable(variable.Parent); + } + public void StoreVariable(SSAVariable variable) + { + SingleStaticAssignment.OverwriteVariable(variablesWritten, variable); + Scope.StoreVariable(variable.Parent); + } + public bool TryStoreVariable(SSAVariable variable) + { + bool result = Scope.TryStoreVariable(variable.Parent); + if (result) + SingleStaticAssignment.OverwriteVariable(variablesWritten, variable); + return result; + } + public IRVariableBase PushVariable(string name, Opcode opcode) { - IRScope globalScope = Scope.GetGlobalScope(); IRVariableBase result = Scope.GetVariableNamed(name); if (result == null) { + IRScope globalScope = Scope.GetGlobalScope(); result = new IRVariable(name, globalScope, opcode); Scope.StoreGlobalVariable(result); } + variablesRead.Add(result); return result; } public IRScope GetScopeForVariableNamed(string name) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 5951b460f..f380d0b8a 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -162,34 +162,45 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack opcodePush.Argument is string identifier && identifier.StartsWith("$"); + + private SSAVariable GetSSAVariable(IRScope scope, OpcodeIdentifierBase store) + { + IRVariable variable = (IRVariable)scope.GetVariableNamed(store.Identifier) ?? new IRVariable(store, scope); + return variable.GetNewSSAVariable(); + } } } \ No newline at end of file diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index 515a2a423..86ec77c4e 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -46,6 +46,8 @@ public IRCodePart(CodePart codePart, List userFunctions, List Code { get; set; } + + public HashSet ExternalReads { get; } = new HashSet(); + public HashSet ExternalWrites { get; } = new HashSet(); + public IRTrigger(IRBuilder builder, Trigger trigger) { this.trigger = trigger; Identifier = trigger.Code.FirstOrDefault()?.Label ?? ""; Code = builder.Lower(trigger.Code); + if (Code.Count > 0) + { + ExternalReads.UnionWith(Code[0].Scope.GetGlobalScope().Variables.Cast()); + foreach (BasicBlock block in Code) + { + ExternalWrites.UnionWith(block.VariablesWritten.Where(v => v.Scope.IsGlobalScope)); + /*foreach (IRInstruction instruction in block.Instructions) + { + if (instruction is IRAssign assignment && + assignment.Target.Scope.IsGlobalScope) + writes.Add(assignment.Target); + }*/ + } + } } public void EmitCode(IREmitter emitter) { @@ -80,15 +100,17 @@ public void EmitCode(IREmitter emitter) } } - public class IRFunction + public class IRFunction : IClosureVariableUser { private readonly UserFunction function; private readonly List userFunctionFragments; + private readonly Dictionary fragments = new Dictionary(); public string Identifier => function.Identifier; public List InitializationCode { get; set; } - private readonly Dictionary fragments = new Dictionary(); public IReadOnlyCollection Fragments => fragments.Values; + public HashSet ExternalReads { get; } = new HashSet(); + public HashSet ExternalWrites { get; } = new HashSet(); public IRFunction(IRBuilder builder, UserFunction function) { @@ -100,6 +122,23 @@ public IRFunction(IRBuilder builder, UserFunction function) fragments.Add(fragment, new IRFunctionFragment(builder, fragment)); } userFunctionFragments.Reverse(); + + foreach (IRFunctionFragment fragment in Fragments) + { + if (fragment.FunctionCode.Count == 0) + continue; + ExternalReads.UnionWith(fragment.FunctionCode[0].Scope.GetGlobalScope().Variables.Cast()); + foreach (BasicBlock block in fragment.FunctionCode) + { + ExternalWrites.UnionWith(block.VariablesWritten.Where(v => v.Scope.IsGlobalScope)); + /*foreach (IRInstruction instruction in block.Instructions) + { + if (instruction is IRAssign assignment && + assignment.Target.Scope.IsGlobalScope) + writes.Add(assignment.Target); + }*/ + } + } } public void EmitCode(IREmitter emitter) @@ -128,5 +167,33 @@ public void EmitCode(IREmitter emitter) } } } + + public static void SetDefiningScope(IClosureVariableUser function, IRScope scope) + { + HashSet tempWrites = new HashSet(function.ExternalWrites); + foreach (SSAVariable variable in tempWrites) + { + if (scope.IsVariableInScope(variable.Name)) + { + variable.RedefineScope(scope.GetVariableNamed(variable.Name).Scope); + } + } + + HashSet tempReads = new HashSet(function.ExternalReads); + foreach (IRVariable variable in tempReads) + { + if (scope.IsVariableInScope(variable.Name)) + { + function.ExternalReads.Remove(variable); + function.ExternalReads.Add((IRVariable)scope.GetVariableNamed(variable.Name)); + } + } + } + + public interface IClosureVariableUser + { + HashSet ExternalReads { get; } + HashSet ExternalWrites { get; } + } } } diff --git a/src/kOS.Safe/Compilation/IR/IRScope.cs b/src/kOS.Safe/Compilation/IR/IRScope.cs index 4437d0c48..02cabdb85 100644 --- a/src/kOS.Safe/Compilation/IR/IRScope.cs +++ b/src/kOS.Safe/Compilation/IR/IRScope.cs @@ -11,6 +11,7 @@ public class IRScope private IRScope parent; private readonly HashSet childScopes = new HashSet(); private readonly HashSet blocks = new HashSet(); + private Dictionary functionRefs = new Dictionary(); private int nextChildIndex = 0; private int index; @@ -64,12 +65,23 @@ public bool TryStoreVariable(IRVariableBase variable) { if (variables.ContainsKey(variable.Name)) { - variables[variable.Name] = variable; + StoreLocalVariable(variable); return true; } return ParentScope?.TryStoreVariable(variable) ?? false; } + public void EnrollFunction(string variable, string functionRef) + { + functionRefs[variable] = functionRef.Split('-').First(); + } + public string GetFunctionNameFromVariable(string variable) + { + if (GetScopeForVariableNamed(variable).functionRefs.TryGetValue(variable, out var functionRef)) + return functionRef; + return null; + } + public IRVariableBase GetVariableNamed(string name) { if (variables.ContainsKey(name)) @@ -85,7 +97,7 @@ public bool IsVariableInScope(string name) } public bool IsVariableInScope(IRVariableBase variable) { - return variable.Scope.IsEncompassedBy(this); + return variable.Scope.IsEqualOrEncompassedBy(this); } public IRScope GetScopeForVariableNamed(string name) @@ -104,6 +116,8 @@ public void ClearVariable(string name) public void ClearVariable(IRVariableBase variable) => ClearVariable(variable.Name); + public bool IsEqualOrEncompassedBy(IRScope scope) + => this == scope || IsEncompassedBy(scope); public bool IsEncompassedBy(IRScope scope) { if (scope.IsGlobalScope) diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index 9b010f250..bc4e84b55 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -58,6 +58,10 @@ public IRVariableBase(string name, IRScope declaringScope) Name = name; Scope = declaringScope; } + + public void RedefineScope(IRScope newScope) + => Scope = newScope; + public override string ToString() => $"{Name} {Scope.IndexString()}"; protected bool NameAndScopeEquals(IRVariableBase variable) @@ -88,6 +92,15 @@ public IRVariable(string name, IRScope scope, short sourceLine, short sourceColu this.sourceLine = sourceLine; this.sourceColumn = sourceColumn; } + + public virtual SSAVariable GetNewSSAVariable() + { + SSAVariable nextIteration = new SSAVariable(this, nextSSAIndex); + iterations[nextSSAIndex] = nextIteration; + nextSSAIndex += 1; + return nextIteration; + } + internal override IEnumerable EmitPush() { yield return new OpcodePush(Name) @@ -103,6 +116,35 @@ obj is IRVariable variable && public override int GetHashCode() => GetBaseHashCode(); } + public class SSAVariable : IRVariable + { + private readonly ushort ssaIndex; + + public override bool IsInvariant { get; set; } = true; + public IRVariable Parent { get; } + internal SSAVariable(IRVariable baseVariable, ushort ssaIndex) : + base(baseVariable.Name, baseVariable.Scope, baseVariable.sourceLine, baseVariable.sourceColumn) + { + this.ssaIndex = ssaIndex; + Parent = baseVariable; + ValueType = baseVariable.ValueType; + } + + public override SSAVariable GetNewSSAVariable() + => Parent.GetNewSSAVariable(); + + public override string ToString() + { + string baseString = base.ToString(); + return baseString.Insert(baseString.IndexOf(" "), $".{ssaIndex}"); + } + public override bool Equals(object obj) + => obj is SSAVariable ssaVariable && + ssaIndex == ssaVariable.ssaIndex && + NameAndScopeEquals(ssaVariable); + public override int GetHashCode() + => (ssaIndex, GetBaseHashCode()).GetHashCode(); + } public class IRRelocateLater : IRConstant { public IRRelocateLater(string value, OpcodePushRelocateLater opcode) : base(value, opcode) @@ -218,7 +260,7 @@ public class IRParameter : IRValue { public override Type ValueType { - get => null; + get => typeof(Encapsulation.Structure); set => throw new InvalidOperationException("Cannot set the value type of a parameter."); } public override bool IsInvariant diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs new file mode 100644 index 000000000..c7afed641 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -0,0 +1,283 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.Optimization; + +namespace kOS.Safe.Compilation.IR +{ + public static class SingleStaticAssignment + { + private static readonly Dictionary> postCallSSAVariables = + new Dictionary>(); + + public static void FinalizeSSA(IRCodePart codePart) + { + foreach (BasicBlock block in codePart.Blocks) + { + AnalyzeBlock(block, codePart); + } + + foreach (BasicBlock root in codePart.RootBlocks) + { + BuildPhis(root); + } + + foreach (BasicBlock block in codePart.Blocks) + { + ApplyUses(block, codePart.Triggers); + } + } + + private static void AnalyzeBlock(BasicBlock block, IRCodePart codePart) + { + HashSet variables = new HashSet(); + List instructions = block.Instructions; + for (int i = 0; i < instructions.Count; i++) + { + IRInstruction instruction = instructions[i]; + + foreach (IRCall call in instruction.DepthFirst().Where(inst => inst is IRCall).Cast()) + { + string functionIdentifier = block.Scope.GetFunctionNameFromVariable(call.Function); + if (functionIdentifier == null) + continue; + IRCodePart.IRFunction function = codePart.Functions.FirstOrDefault(f => string.Equals(f.Identifier, functionIdentifier, StringComparison.OrdinalIgnoreCase)); + if (function != null) + { + HashSet ssaVariables = new HashSet(); + foreach (SSAVariable variable in function.ExternalWrites) + { + SSAVariable ssaVariable = variable.GetNewSSAVariable(); + OverwriteVariable(variables, ssaVariable); + ssaVariables.Add(ssaVariable); + } + if (postCallSSAVariables.TryGetValue(call, out IEnumerable storedVariables)) + ssaVariables.UnionWith(storedVariables); + postCallSSAVariables[call] = ssaVariables; + } + } + + switch (instruction) + { + case IRAssign assignment: + OverwriteVariable(variables, (SSAVariable)assignment.Target); + // On encountering a PushRelocateLater, note the scope for its closure variables. + if (assignment.Value is IRRelocateLater lockOrFunctionPointer) + { + string pointer = ((string)lockOrFunctionPointer.Value).Split('-').First(); + // Re-scope the stored variables from Global to the current scope. + IRCodePart.IRFunction function = codePart.Functions.FirstOrDefault(f => string.Equals(f.Identifier, pointer, StringComparison.OrdinalIgnoreCase)); + if (function != null) + IRCodePart.SetDefiningScope(function, block.Scope); + } + break; + + // On encountering a trigger: + // Clear the cache of any variables that are written in that trigger or body. + // Blacklist any variables that are written in the trigger or body. + case IRUnaryConsumer triggerInstruction: + if (triggerInstruction.Operation is OpcodeAddTrigger) + { + string pointer = (string)((IRConstant)triggerInstruction.Operand).Value; + // Re-scope the stored variables from Global to the current scope. + IRCodePart.IRTrigger trigger = codePart.Triggers.FirstOrDefault(t => string.Equals(t.Identifier, pointer, StringComparison.OrdinalIgnoreCase)); + if (trigger != null) + IRCodePart.SetDefiningScope(trigger, block.Scope); + block.TriggerPropagationBlacklist.UnionWith(trigger.ExternalWrites); + } + break; + } + } + + block.VariablesWritten.Clear(); + block.VariablesWritten.UnionWith(variables); + } + + private static void BuildPhis(BasicBlock root) + { + Dictionary> variablesOut = new Dictionary>(); + + Queue worklist = new Queue(); + worklist.Enqueue(root); + while (worklist.Count > 0) + { + BasicBlock block = worklist.Dequeue(); + + HashSet blacklist = block.TriggerPropagationBlacklist; + List<(BasicBlock block, SSAVariable variable)> varsIn = new List<(BasicBlock block, SSAVariable variable)>(); + + foreach (BasicBlock predecessor in block.Predecessors) + { + blacklist.UnionWith(predecessor.TriggerPropagationBlacklist); + if (variablesOut.TryGetValue(predecessor, out HashSet predVarsOut)) + { + foreach (SSAVariable variable in predVarsOut + .Where(v => block.Scope.IsEqualOrEncompassedBy(v.Scope))) + varsIn.Add((predecessor, variable)); + } + } + + HashSet variablesIn = new HashSet(); + foreach (IGrouping definition in + varsIn.GroupBy(bv => bv.variable.Parent)) + { + if (definition.Select(bv => bv.variable).Distinct().Skip(1).Any()) + { + // Phi required + Dictionary phiDict; + SSAVariable phiVar; + if (block.Phis.TryGetValue(definition.Key, out (SSAVariable phiVar, Dictionary values) result)) + { + phiDict = result.values; + phiVar = result.phiVar; + } + else + { + phiVar = definition.Key.GetNewSSAVariable(); + phiDict = new Dictionary(); + block.Phis[definition.Key] = (phiVar, phiDict); + } + + foreach ((BasicBlock block, SSAVariable variable) def in definition) + phiDict[def.block] = def.variable; + + phiVar.IsInvariant = false; + variablesIn.Add(phiVar); + } + else + { + block.Phis.Remove(definition.Key); + variablesIn.Add(definition.First().variable); + } + } + + block.IncomingVariableDefinitions = variablesIn; + + HashSet varsOut = new HashSet(); + varsOut.UnionWith(variablesIn); + + foreach (SSAVariable variable in block.VariablesWritten.Where(v => !v.Scope.IsGlobalScope)) + OverwriteVariable(varsOut, variable); + + if (variablesOut.ContainsKey(block) && variablesOut[block].SetEquals(varsOut)) + continue; + + variablesOut[block] = varsOut; + + foreach (BasicBlock successor in block.Successors) + worklist.Enqueue(successor); + } + } + + private static Type GetFirstCommonBaseType(Type typeA, Type typeB) + { + if (typeA == null || typeB == null) return null; + + Type current = typeA; + while (current != null) + { + if (current.IsAssignableFrom(typeB)) + { + return current; + } + current = current.BaseType; + } + + throw new Exceptions.KOSYouShouldNeverSeeThisException($"Couldn't find a base class between {typeA} and {typeB}, when all kOS types should derive from {nameof(Encapsulation.Structure)}."); + } + + private static void ApplyUses(BasicBlock block, List triggers) + { + List instructions = block.Instructions; + Dictionary liveDefinitions = new Dictionary(); + + HashSet triggerBlacklist = new HashSet(); + foreach (BasicBlock predecessor in block.Predecessors) + triggerBlacklist.UnionWith(predecessor.TriggerPropagationBlacklist); + + foreach (SSAVariable variable in block.IncomingVariableDefinitions) + liveDefinitions[variable.Parent] = variable; + foreach (var (phiVar, values) in block.Phis.Values) + { + Type proposedType = values.Values.First().ValueType; + // MUSTFIX: This doesn't work for circular references and only emits typeof(Structure) in those cases. + foreach (SSAVariable variable in values.Values) + proposedType = GetFirstCommonBaseType(proposedType, variable.ValueType); + + phiVar.ValueType = proposedType; + } + + for (int i = 0; i < instructions.Count; i++) + { + IRInstruction instruction = instructions[i]; + foreach (IRInstruction inst in instruction.DepthFirst()) + { + if (inst is IRUnaryConsumer triggerInstruction && + triggerInstruction.Operation is OpcodeAddTrigger) + { + string pointer = (string)((IRConstant)triggerInstruction.Operand).Value; + IRCodePart.IRTrigger trigger = triggers.FirstOrDefault(t => string.Equals(t.Identifier, pointer, StringComparison.OrdinalIgnoreCase)); + + triggerBlacklist.UnionWith(trigger.ExternalWrites); + + foreach (IRVariable variable in trigger.ExternalWrites) + liveDefinitions.Remove(variable); + } + else if (inst is IRAssign assignment && + assignment.Target is SSAVariable ssaVariable) + { + ssaVariable.ValueType = assignment.Value.ValueType; + + if (!triggerBlacklist.Contains(ssaVariable.Parent)) + liveDefinitions[ssaVariable.Parent] = ssaVariable; + else + liveDefinitions.Remove(ssaVariable.Parent); + } + else if (inst is IRCall call) + { + if (postCallSSAVariables.ContainsKey(call)) + { + foreach (SSAVariable variable in postCallSSAVariables[call]) + { + if (!triggerBlacklist.Contains(variable.Parent)) + liveDefinitions[variable.Parent] = variable; + else + liveDefinitions.Remove(variable.Parent); + } + } + } + switch (inst) + { + case ISingleOperandInstruction singleOperandInstruction: + { + if (singleOperandInstruction.Operand is IRVariable variable && + !triggerBlacklist.Contains(variable) && + liveDefinitions.TryGetValue(variable, out SSAVariable currentValue)) + { + singleOperandInstruction.Operand = currentValue; + } + } + break; + case IMultipleOperandInstruction multipleOperandInstruction: + for (int j = 0; j < multipleOperandInstruction.OperandCount; j++) + { + if (multipleOperandInstruction[j] is IRVariable variable && + !triggerBlacklist.Contains(variable) && + liveDefinitions.TryGetValue(variable, out SSAVariable currentValue)) + { + multipleOperandInstruction[j] = currentValue; + } + } + break; + } + } + } + } + + public static void OverwriteVariable(HashSet variables, SSAVariable newVariable) + { + variables.RemoveWhere(v => v.Parent == newVariable.Parent); + variables.Add(newVariable); + } + } +} diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs index 7d04469f5..026425054 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs @@ -6,7 +6,7 @@ namespace kOS.Safe.Compilation.Optimization.Passes { - public class ConstantPropagation : IHolisticOptimizationPass + public class ConstantPropagation// : IHolisticOptimizationPass { public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; From 3cc27a86a6b3c8f8fa30c66d56e60cdca357b284 Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 6 Apr 2026 19:55:17 -0400 Subject: [PATCH 062/120] Documentation pass. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 167 +++++++++++++++++- .../Compilation/IR/IOperandInstructionBase.cs | 25 ++- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 15 +- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 123 +++++++++++++ src/kOS.Safe/Compilation/IR/IREmitter.cs | 9 + src/kOS.Safe/Compilation/IR/IRScope.cs | 49 ++++- .../Compilation/IR/IResultingInstruction.cs | 11 ++ .../Compilation/IR/SingleStaticAssignment.cs | 12 ++ .../Optimization/IOptimizationPass.cs | 30 ++++ .../Compilation/Optimization/InterimCPU.cs | 6 + .../Compilation/Optimization/Optimizer.cs | 41 ++++- 11 files changed, 468 insertions(+), 20 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index bfa7b5241..b52d3bbc4 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -4,8 +4,14 @@ namespace kOS.Safe.Compilation.IR { + /// + /// This class describes a basic block for the optimizing compiler. + /// A basic block runs in its entirety without branching (function + /// calls are allowed), and is limited to a single scope. + /// public class BasicBlock { + private static uint nextID = 0; private readonly HashSet predecessors = new HashSet(); private readonly HashSet successors = new HashSet(); private BasicBlock dominator; @@ -17,6 +23,10 @@ public class BasicBlock private readonly HashSet variablesWritten = new HashSet(); private readonly HashSet variablesRead = new HashSet(); + /// + /// Gets or sets the scope of this block. This describes the + /// narrowest scope at the time of this block's execution. + /// public IRScope Scope { get => scope; @@ -27,67 +37,186 @@ public IRScope Scope scope.EnrollBlock(this); } } + /// + /// Gets the start index of the opcodes that form this block. + /// public int StartIndex { get; } + /// + /// Gets the end index of the opcodes that form this block. + /// public int EndIndex { get; } + /// + /// Gets the list of instructions that this block executes. Note + /// that the instructions listed here are only the terminal + /// instructions - those that set a variable or suffix, those that + /// branch to another block, or those that pop an item from the + /// stack. + /// public List Instructions { get; } = new List(); + /// + /// Gets the successor blocks of this block. These are the blocks + /// to which flow can branch after completing this block. + /// + /// + /// Note that a block can be one of its own successors, so be + /// cautious of creating infinite loops when iterating using + /// successors. + /// public IReadOnlyCollection Successors => successors; + /// + /// Gets the predecessor blocks of this block. These are the + /// blocks from which flow can be entering this block. + /// + /// + /// Note that a block can be one of its own predecessors, so be + /// cautious of creating infinite loops when iterating using + /// predecessors. + /// public IReadOnlyCollection Predecessors => predecessors; + /// + /// Gets the collection of variables that are written in this + /// block in SSA form. This list only returns the last SSA + /// instance for a given variable name. + /// public HashSet VariablesWritten => variablesWritten; + /// + /// Gets the collection of variables that are read in this block. + /// This is not in SSA form and includes global and bound + /// variables. + /// public IReadOnlyCollection VariablesRead => variablesRead; + /// + /// Gets the collection of phi functions that define variables + /// that may have one of several different values upon entering + /// this block. + /// + /// + /// This data is populated during . + /// public Dictionary values)> Phis { get; } = new Dictionary values)>(); - public HashSet IncomingVariableDefinitions { get; set; } + /// + /// Gets the set of incoming SSA variables that this block + /// receives, including the results of any phi functions. + /// + /// + /// This data is populated during . + /// + public HashSet IncomingVariableDefinitions { get; internal set; } + /// + /// Gets the set of variables that are blacklisted against + /// caching or propagation due to their presence in active + /// triggers. + /// + /// + /// This data is populated during . + /// public HashSet TriggerPropagationBlacklist { get; } = new HashSet(); + /// + /// Gets the instruction label with which to start the block. + /// The special prefix "@BB#" will be overwritten during linking. + /// public string Label => nonSequentialLabel ?? $"@BB#{ID}"; - public int ID { get; } + /// + /// Gets a unique ID to help identify this basic block during debugging. + /// + public uint ID { get; } + /// + /// Gets the BasicBlock that dominates this block. That is, + /// the most recent predecessor that is guaranteed to have + /// executed before this block. + /// public BasicBlock Dominator { get => dominator; - protected set + private set { dominator?.dominates.Remove(this); dominator = value; dominator?.dominates.Add(this); } } + /// + /// Gets the collection of BasicBlocks for which this block is + /// the Dominator. + /// public IReadOnlyCollection Dominates => dominates; + /// + /// Gets or sets the Extended Basic Block of which this block + /// is a member. + /// public ExtendedBasicBlock ExtendedBlock { get; set; } + /// + /// Gets or sets the instruction that this + /// block will terminate with if it does not branch to another. + /// public IRJump FallthroughJump { get; set; } = null; #if DEBUG internal Opcode[] OriginalOpcodes { get; set; } internal Opcode[] GeneratedOpcodes => EmitOpCodes().ToArray(); #endif - public BasicBlock(int startIndex, int endIndex, int id, string nonSequentialLabel = null) + /// + /// Initializes a new instance of a . + /// + /// The starting index in the original sequence of s. + /// The ending index in the original sequence of s. + /// A non sequential label, if present. + public BasicBlock(int startIndex, int endIndex, string nonSequentialLabel = null) { StartIndex = startIndex; EndIndex = endIndex; - ID = id; + ID = nextID++; this.nonSequentialLabel = nonSequentialLabel; } + /// + /// Adds the specified instruction to this block's list of instructions. + /// + /// The instruction to add. public void Add(IRInstruction instruction) => Instructions.Add(instruction); + /// + /// Adds a successor block. + /// + /// The successor block to add. public void AddSuccessor(BasicBlock successor) { successors.Add(successor); successor.AddPredecessor(this); } + + /// + /// Adds a predecessor block. + /// + /// The predecessor block to add. protected void AddPredecessor(BasicBlock predecessor) { predecessors.Add(predecessor); } + + /// + /// Removes a successor block. + /// + /// The successor block to remove. + /// Cannot remove as it is not a successor. public void RemoveSuccessor(BasicBlock successor) { if (!successors.Remove(successor)) - throw new System.ArgumentException(nameof(successor)); + throw new System.ArgumentException($"Cannot remove {successor} as it is not a successor."); successor.predecessors.Remove(this); if (successor.predecessors.Count == 0) successor.Dominator = null; else successor.Dominator.EstablishDominance(); } + + /// + /// Establishes the dominance tree. This must be called on the + /// root block, or the root of the branch that needs + /// re-establishment. + /// public void EstablishDominance() { // Compute reverse postorder @@ -159,11 +288,15 @@ private static void DepthFirstSearch(BasicBlock block, HashSet visit postorder.Add(block); } - + /// + /// Adds a parameter to this block. + /// + /// The parameter object to add. public void AddParameter(IRParameter parameter) { parameters.Add(parameter); } + public void StoreLocalVariable(SSAVariable variable) { SingleStaticAssignment.OverwriteVariable(variablesWritten, variable); @@ -199,9 +332,25 @@ public IRVariableBase PushVariable(string name, Opcode opcode) variablesRead.Add(result); return result; } + /// + /// Alias for + /// using this block's . + /// + /// The name of the variable to find. + /// + /// The IRScope object containing the supplied variable, or the + /// global scope if the variable is not yet tracked. + /// public IRScope GetScopeForVariableNamed(string name) => Scope.GetScopeForVariableNamed(name); + /// + /// Sets the state of the stack upon exiting this block. + /// + /// The stack state to set. + /// + /// Use extreme caution when manipulating the stack state. + /// public void SetStackState(Stack stack) { while (stack.Count > 0) @@ -213,6 +362,10 @@ public override string ToString() return $"BasicBlock#{ID}: {StartIndex}-{EndIndex}; {Instructions.Count} Instructions"; } + /// + /// Emits the the sequence of Opcodes for the instructions + /// contained in this block. + /// public IEnumerable EmitOpCodes() { bool addedFallthrough = FallthroughJump != null && Instructions.LastOrDefault() == FallthroughJump; diff --git a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs index ed1aa7251..606979b9d 100644 --- a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs +++ b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs @@ -2,16 +2,33 @@ namespace kOS.Safe.Compilation.IR { - public interface IOperandInstructionBase - { } - public interface ISingleOperandInstruction : IOperandInstructionBase + /// + /// Represents instructions which operate on a single operand. + /// + public interface ISingleOperandInstruction { + /// + /// Gets or sets the operand for this instruction. + /// IRValue Operand { get; set; } } - public interface IMultipleOperandInstruction : IOperandInstructionBase + /// + /// Represents instructions which operate on multiple operands. + /// + public interface IMultipleOperandInstruction { + /// + /// Gets the collection of operands for the instruction. + /// IEnumerable Operands { get; } + /// + /// Gets or sets the operand at the specified index. + /// + /// The index to get or set. The meaning of this will be implementation-specific. IRValue this[int index] { get; set; } + /// + /// Gets the number of operands for this instruction. + /// int OperandCount { get; } } } diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index f380d0b8a..20b788e5f 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -4,11 +4,22 @@ namespace kOS.Safe.Compilation.IR { + /// + /// This class is used to convert the Opcode representation to a form + /// that is more appropriate for optimization passes. A single + /// instance must be used for a complete program element + /// (i.e. a ). + /// public class IRBuilder { private int nextTempId = 0; - private int blockID = 0; + /// + /// Lowers the specified code from a sequence of s + /// to a three-address code interim representation. + /// + /// The code to lower. + /// A sequence of objects, representing the instructions. public List Lower(List code) { List blocks = new List(); @@ -64,7 +75,7 @@ private void CreateBlocks(List code, Dictionary labels, Lis string label = code[startIndex].Label; if (label.StartsWith("@")) label = null; - BasicBlock block = new BasicBlock(startIndex, endIndex, blockID++, label); + BasicBlock block = new BasicBlock(startIndex, endIndex, label); blocks.Add(block); } diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index 86ec77c4e..d133585aa 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -5,14 +5,41 @@ namespace kOS.Safe.Compilation.IR { + /// + /// This class is the interim representation of a program, including + /// the functions, triggers, and mainline code defined therein. + /// public class IRCodePart { + /// + /// Gets or sets the mainline code, in BasicBlock format. + /// public List MainCode { get; set; } + /// + /// Gets or sets the function definitions. + /// public List Functions { get; set; } + /// + /// Gets or sets the trigger definitions. + /// public List Triggers { get; set; } + /// + /// Gets the collection of root blocks, across all mainline + /// code, functions, and triggers. + /// public List RootBlocks { get; } = new List(); + /// + /// Gets the collection of blocks, across all program elements. + /// public List Blocks { get; } = new List(); + /// + /// Initializes a new instance of the class. + /// + /// The code part containing main code. + /// The user functions defined in this program. + /// The triggers defined in this program. + /// public IRCodePart(CodePart codePart, List userFunctions, List triggers) { if (codePart.InitializationCode.Count > 0) @@ -50,6 +77,11 @@ public IRCodePart(CodePart codePart, List userFunctions, List + /// Emits the code into Opcode representation, and back into + /// its source objects. + /// + /// The code part into which to emit mainline code. public void EmitCode(CodePart codePart) { IREmitter emitter = new IREmitter(); @@ -64,15 +96,38 @@ public void EmitCode(CodePart codePart) codePart.MainCode = emitter.Emit(MainCode); } + /// + /// This class represents a trigger definition. + /// + /// public class IRTrigger : IClosureVariableUser { private readonly Trigger trigger; + /// + /// Gets the identifier string for this trigger. + /// public string Identifier { get; } + /// + /// Gets or sets the code for this trigger, in BasicBlock representation. + /// public List Code { get; set; } + /// + /// Gets the collection of external variables that are read + /// within the trigger. + /// public HashSet ExternalReads { get; } = new HashSet(); + /// + /// Gets the collection of external variables that are written + /// to within the trigger. + /// public HashSet ExternalWrites { get; } = new HashSet(); + /// + /// Initializes a new instance of the class. + /// + /// The IRBuilder object in use. + /// The trigger object to convert. public IRTrigger(IRBuilder builder, Trigger trigger) { this.trigger = trigger; @@ -93,6 +148,11 @@ public IRTrigger(IRBuilder builder, Trigger trigger) } } } + /// + /// Emits the code into Opcode representation back into the + /// trigger's source object. + /// + /// The IREmitter object in use. public void EmitCode(IREmitter emitter) { trigger.Code.Clear(); @@ -100,18 +160,44 @@ public void EmitCode(IREmitter emitter) } } + /// + /// This class represents a user-defined function. + /// + /// public class IRFunction : IClosureVariableUser { private readonly UserFunction function; private readonly List userFunctionFragments; private readonly Dictionary fragments = new Dictionary(); + /// + /// Gets the identifier string for this function. + /// public string Identifier => function.Identifier; + /// + /// Gets or sets the initialization code, in BasicBlock format. + /// public List InitializationCode { get; set; } + /// + /// Gets the collection of function fragments. + /// public IReadOnlyCollection Fragments => fragments.Values; + /// + /// Gets the collection of external variables that are read + /// within the function. + /// public HashSet ExternalReads { get; } = new HashSet(); + /// + /// Gets the collection of external variables that are written + /// to within the function. + /// public HashSet ExternalWrites { get; } = new HashSet(); + /// + /// Initializes a new instance of the class. + /// + /// The IRBuilder object in use. + /// The user function object to convert. public IRFunction(IRBuilder builder, UserFunction function) { this.function = function; @@ -141,6 +227,11 @@ public IRFunction(IRBuilder builder, UserFunction function) } } + /// + /// Emits the code into Opcode representation back into the + /// trigger's source object. + /// + /// The IREmitter object in use. public void EmitCode(IREmitter emitter) { function.InitializationCode.Clear(); @@ -151,15 +242,31 @@ public void EmitCode(IREmitter emitter) } } + /// + /// This class represents a function fragment. See . + /// public class IRFunctionFragment { private readonly UserFunctionCodeFragment fragment; + /// + /// Gets or sets the function code, in BasicBlock representation. + /// public List FunctionCode { get; set; } + /// + /// Initializes a new instance of the class. + /// + /// The IRBuilder object in use. + /// The function code fragment to convert. public IRFunctionFragment(IRBuilder builder, UserFunctionCodeFragment codeFragment) { fragment = codeFragment; FunctionCode = builder.Lower(codeFragment.Code); } + /// + /// Emits the code into Opcode representation back into the + /// trigger's source object. + /// + /// The IREmitter object in use. public void EmitCode(IREmitter emitter) { fragment.Code.Clear(); @@ -168,6 +275,11 @@ public void EmitCode(IREmitter emitter) } } + /// + /// Sets the scope in which a function or trigger is defined. + /// + /// The function or trigger to target. + /// The scope to set as parent. public static void SetDefiningScope(IClosureVariableUser function, IRScope scope) { HashSet tempWrites = new HashSet(function.ExternalWrites); @@ -190,9 +302,20 @@ public static void SetDefiningScope(IClosureVariableUser function, IRScope scope } } + /// + /// Represents a user of a closure and its contained variables. + /// public interface IClosureVariableUser { + /// + /// Gets the collection of external variables that are read + /// within the closure. + /// HashSet ExternalReads { get; } + /// + /// Gets the collection of external variables that are written + /// to within the closure. + /// HashSet ExternalWrites { get; } } } diff --git a/src/kOS.Safe/Compilation/IR/IREmitter.cs b/src/kOS.Safe/Compilation/IR/IREmitter.cs index 3056f99e3..ac20446c8 100644 --- a/src/kOS.Safe/Compilation/IR/IREmitter.cs +++ b/src/kOS.Safe/Compilation/IR/IREmitter.cs @@ -3,9 +3,18 @@ namespace kOS.Safe.Compilation.IR { + /// + /// This class is used to emit code back into Opcode representation + /// from the Three-Address Code interim representation. + /// public class IREmitter { int labelIndex = 0; + /// + /// Emits the specified blocks to Opcode representation. + /// + /// The interim representation basic blocks for which to emit. + /// The sequence of Opcodes representing the code. public List Emit(List blocks) { List result = new List(); diff --git a/src/kOS.Safe/Compilation/IR/IRScope.cs b/src/kOS.Safe/Compilation/IR/IRScope.cs index 02cabdb85..87257fc98 100644 --- a/src/kOS.Safe/Compilation/IR/IRScope.cs +++ b/src/kOS.Safe/Compilation/IR/IRScope.cs @@ -4,6 +4,9 @@ namespace kOS.Safe.Compilation.IR { + /// + /// This class represents a variable scope, analogous to . + /// public class IRScope { private readonly Dictionary variables = @@ -11,11 +14,14 @@ public class IRScope private IRScope parent; private readonly HashSet childScopes = new HashSet(); private readonly HashSet blocks = new HashSet(); - private Dictionary functionRefs = new Dictionary(); + private readonly Dictionary functionRefs = new Dictionary(); private int nextChildIndex = 0; private int index; + /// + /// Gets or sets the parent scope. + /// public IRScope ParentScope { get => parent; @@ -32,14 +38,55 @@ public IRScope ParentScope index = 0; } } + /// + /// Gets the collection of child scopes. + /// public IReadOnlyCollection Children => childScopes; + /// + /// Gets the collection of blocks that associate with this scope. + /// public IReadOnlyCollection Blocks => blocks; + /// + /// Gets or sets the block where this scope is pushed upon entered. + /// public BasicBlock HeaderBlock { get; set; } + /// + /// Gets or sets the block where this scope is popped upon exiting. + /// public BasicBlock FooterBlock { get; set; } + /// + /// Gets the collection of variables associated with this scope. + /// public IReadOnlyCollection Variables => variables.Values; + /// + /// Gets the collection of variable names associated with this + /// scope. + /// public IReadOnlyCollection VariableNames => variables.Keys; + /// + /// Gets the collection of variables written to within this scope. + /// This differs from in that it can + /// contain multiple SSA variables of the same base name. + /// + public HashSet VariablesWritten { get; } = new HashSet(); + /// + /// Gets a value indicating whether this instance represents the + /// global scope. + /// + /// + /// true if this instance represents the global scope; otherwise, false. + /// + /// + /// Multiple instances can represent the global scope + /// simultaneously. The global scope implicitly contains every + /// variable name. + /// public bool IsGlobalScope { get; internal set; } = false; + /// + /// Initializes a new instance of the class. + /// + /// The parent scope. public IRScope(IRScope parent) { ParentScope = parent; diff --git a/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs index da221dcd4..c9f6e72a6 100644 --- a/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs @@ -2,9 +2,20 @@ namespace kOS.Safe.Compilation.IR { + /// + /// Represents instructions that push a value back onto the stack. + /// public interface IResultingInstruction { + /// + /// Gets the representation of the value that would be pushed + /// to the stack. + /// IRValue Result { get; } + /// + /// Gets the of the result, for type + /// inferencing purposes. + /// Type ResultType { get; } } } \ No newline at end of file diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index c7afed641..91b3bdd5c 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -5,11 +5,18 @@ namespace kOS.Safe.Compilation.IR { + /// + /// This utility class converts an IRCodePart into single static + /// assignment form. + /// public static class SingleStaticAssignment { private static readonly Dictionary> postCallSSAVariables = new Dictionary>(); + /// + /// Finalizes a program into single static assignment form. + /// public static void FinalizeSSA(IRCodePart codePart) { foreach (BasicBlock block in codePart.Blocks) @@ -274,6 +281,11 @@ private static void ApplyUses(BasicBlock block, List trigg } } + /// + /// Overwrites an SSA variable with the latest assignment. + /// + /// The active variables. + /// The new variable as assigned. public static void OverwriteVariable(HashSet variables, SSAVariable newVariable) { variables.RemoveWhere(v => v.Parent == newVariable.Parent); diff --git a/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs b/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs index c503f8f51..bcf368f40 100644 --- a/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs +++ b/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs @@ -3,22 +3,52 @@ namespace kOS.Safe.Compilation.Optimization { + /// + /// Represents an optimization pass to be performed. + /// public interface IOptimizationPass { + /// + /// Gets the optimization level associated with this pass. + /// + /// + /// The result should be constant across all instances. + /// OptimizationLevel OptimizationLevel { get; } + /// + /// An index on which passes are sorted before being called. + /// + /// + /// The result should be constant across all instances. + /// short SortIndex { get; } } public interface IOptimizationPass : IOptimizationPass { + /// + /// Executes the optimization pass. + /// + /// The code representation to be optimized. void ApplyPass(List code); } public interface IHolisticOptimizationPass : IOptimizationPass { + /// + /// Executes the optimization pass. + /// + /// The code to be optimized. void ApplyPass(IRCodePart codePart); } public interface ILinkedOptimizationPass : IOptimizationPass { + /// + /// Allows the Optimizer to reference itself before executing the + /// optimization pass. + /// Optimizer Optimizer { set; } + /// + /// Executes the optimization pass. + /// void ApplyPass(); } } diff --git a/src/kOS.Safe/Compilation/Optimization/InterimCPU.cs b/src/kOS.Safe/Compilation/Optimization/InterimCPU.cs index b54c6b4a4..478faaa4f 100644 --- a/src/kOS.Safe/Compilation/Optimization/InterimCPU.cs +++ b/src/kOS.Safe/Compilation/Optimization/InterimCPU.cs @@ -5,6 +5,12 @@ namespace kOS.Safe.Compilation.Optimization { + /// + /// A partial implementation of + /// that can implement a stack for executing certain functions + /// at compile time. + /// + /// internal class InterimCPU : ICpu { public int InstructionPointer { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } diff --git a/src/kOS.Safe/Compilation/Optimization/Optimizer.cs b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs index 6345b5e5b..d92fea233 100644 --- a/src/kOS.Safe/Compilation/Optimization/Optimizer.cs +++ b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs @@ -7,27 +7,53 @@ namespace kOS.Safe.Compilation.Optimization { + /// + /// This class performs the actual optimization by applying all + /// optimization passes, as identified by implementation of . + /// [AssemblyWalk(InterfaceType = typeof(IOptimizationPass), StaticRegisterMethod = "RegisterMethod")] public class Optimizer { + internal static InterimCPU InterimCPU { get; } = new InterimCPU(); + private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = InterimCPU }; + private readonly SortedSet optimizationPasses = new SortedSet( + Comparer.Create((a, b) => a.SortIndex.CompareTo(b.SortIndex))); + private readonly static HashSet availablePassTypes = new HashSet(); + + /// + /// Gets the optimization level to be applied. + /// public OptimizationLevel OptimizationLevel { get; } + /// + /// Gets the collection of Basic Blocks being operated upon. + /// public List Blocks { get; private set; } + /// + /// Gets the collection of extended basic blocks being operated upon. + /// public List ExtendedBlocks { get; private set; } + /// + /// Gets the collection of root blocks. + /// public HashSet RootBlocks { get; private set; } + /// + /// Gets the IRCodePart object being operated upon. + /// public IRCodePart Code { get; private set; } - internal static InterimCPU InterimCPU { get; } = new InterimCPU(); + /// + /// Gets the function manager. + /// public static IFunctionManager FunctionManager => shared.FunctionManager; - private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = InterimCPU }; - private readonly SortedSet optimizationPasses = new SortedSet( - Comparer.Create((a, b) => a.SortIndex.CompareTo(b.SortIndex))); - private readonly static HashSet availablePassTypes = new HashSet(); - static Optimizer() { shared.FunctionManager = new FunctionManager(shared); } + /// + /// Initializes a new instance of the class. + /// + /// The optimization level to be applied. public Optimizer(OptimizationLevel optimizationLevel) { OptimizationLevel = optimizationLevel; @@ -44,6 +70,9 @@ public static void RegisterMethod(Type type) availablePassTypes.Add(type); } + /// + /// Applies the optimization passes to the specified code part. + /// public List Optimize(IRCodePart codePart) { Code = codePart; From 28271319bca5d5dab42d0e70819ad81b0553b264 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 17 Apr 2026 16:33:28 -0400 Subject: [PATCH 063/120] Reimplement IOperandInstructionBase and make it actually useful. Adds two abstract subclasses of IRInstruction that make mutating or forEach'ing across operands easy. --- .../Compilation/IR/IOperandInstructionBase.cs | 27 +++- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 129 +++++++++++------- .../Compilation/IR/SingleStaticAssignment.cs | 31 ++--- .../Passes/ConstantPropagation.cs | 24 +--- .../Passes/PeepholeOptimizations.cs | 57 +++----- 5 files changed, 135 insertions(+), 133 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs index 606979b9d..c9f446380 100644 --- a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs +++ b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs @@ -1,11 +1,29 @@ +using System; using System.Collections.Generic; namespace kOS.Safe.Compilation.IR { + /// + /// Represents an instruction that operates on one or more operands. + /// + public interface IOperandInstructionBase + { + /// + /// Applies an action for each operand. + /// + void ForEachOperand(Action action); + + /// + /// Mutates each operand by replacing it with the result of applied to that operand. + /// + /// The mutation function. + void MutateEachOperand(Func mutateFunc); + } + /// /// Represents instructions which operate on a single operand. /// - public interface ISingleOperandInstruction + public interface ISingleOperandInstruction : IOperandInstructionBase { /// /// Gets or sets the operand for this instruction. @@ -15,18 +33,13 @@ public interface ISingleOperandInstruction /// /// Represents instructions which operate on multiple operands. /// - public interface IMultipleOperandInstruction + public interface IMultipleOperandInstruction : IOperandInstructionBase { /// /// Gets the collection of operands for the instruction. /// IEnumerable Operands { get; } /// - /// Gets or sets the operand at the specified index. - /// - /// The index to get or set. The meaning of this will be implementation-specific. - IRValue this[int index] { get; set; } - /// /// Gets the number of operands for this instruction. /// int OperandCount { get; } diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 66b0139fa..1d7593bbd 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -31,7 +31,42 @@ public void OverwriteSourceLocation(short sourceLine, short sourceColumn) } } - public class IRAssign : IRInstruction, ISingleOperandInstruction + public abstract class SingleOperandInstruction : IRInstruction, ISingleOperandInstruction + { + protected IRValue operand; + + protected SingleOperandInstruction(Opcode originalOpcode) : base(originalOpcode) { } + + IRValue ISingleOperandInstruction.Operand { get => operand; set => operand = value; } + + public void ForEachOperand(Action action) + => action(operand); + + public void MutateEachOperand(Func mutateFunc) + => operand = mutateFunc(operand); + } + public abstract class MultipleOperandInstruction : IRInstruction, IMultipleOperandInstruction + { + protected MultipleOperandInstruction(Opcode originalOpcode) : base(originalOpcode) { } + + public abstract IEnumerable Operands { get; } + public abstract int OperandCount { get; } + protected abstract IRValue this[int index] { get; set; } + + public void ForEachOperand(Action action) + { + foreach (IRValue operand in Operands) + action(operand); + } + + public void MutateEachOperand(Func mutateFunc) + { + for (int i = 0; i < OperandCount; i++) + this[i] = mutateFunc(this[i]); + } + } + + public class IRAssign : SingleOperandInstruction { public enum StoreScope { @@ -41,10 +76,9 @@ public enum StoreScope } public override bool IsInvariant => Value.IsInvariant; public IRVariableBase Target { get; set; } - public IRValue Value { get; set; } + public IRValue Value { get => operand; set => operand = value; } public StoreScope Scope { get; set; } = StoreScope.Ambivalent; public bool AssertExists { get; set; } = false; - IRValue ISingleOperandInstruction.Operand { get => Value; set => Value = value; } public IRAssign(OpcodeIdentifierBase opcode, IRVariableBase target, IRValue value) : base(opcode) { @@ -84,7 +118,7 @@ public override bool Equals(object obj) public override int GetHashCode() => Target.GetHashCode(); } - public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperandInstruction + public class IRBinaryOp : MultipleOperandInstruction, IResultingInstruction { private static readonly Type[] commutativeTypes = { @@ -103,8 +137,8 @@ public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperand public BinaryOpcode Operation { get; set; } public IRValue Left { get; set; } public IRValue Right { get; set; } - public IEnumerable Operands { get { yield return Left; yield return Right; } } - public int OperandCount => 2; + public override IEnumerable Operands { get { yield return Left; yield return Right; } } + public override int OperandCount => 2; public Type ResultType { get @@ -140,7 +174,7 @@ public Type ResultType } } - IRValue IMultipleOperandInstruction.this[int index] + protected override IRValue this[int index] { get => index == 0 ? Left : index == 1 ? Right : throw new ArgumentOutOfRangeException(); set @@ -208,12 +242,12 @@ public override bool Equals(object obj) public override int GetHashCode() => Operation.GetHashCode(); } - public class IRUnaryOp : IRInstruction, IResultingInstruction, ISingleOperandInstruction + public class IRUnaryOp : SingleOperandInstruction, IResultingInstruction { public override bool IsInvariant => Operand.IsInvariant; public IRValue Result { get; set; } public Opcode Operation { get; } - public IRValue Operand { get; set; } + public IRValue Operand { get => operand; set => operand = value; } public Type ResultType { get @@ -271,12 +305,12 @@ public override bool Equals(object obj) public override int GetHashCode() => Operation.GetHashCode(); } - public class IRUnaryConsumer : IRInstruction, ISingleOperandInstruction + public class IRUnaryConsumer : SingleOperandInstruction { private readonly bool operationHasSideEffects; public override bool IsInvariant => !operationHasSideEffects && Operand.IsInvariant; public Opcode Operation { get; } - public IRValue Operand { get; set; } + public IRValue Operand { get => operand; set => operand = value; } public IRUnaryConsumer(Opcode opcode, IRValue operand, bool sideEffects = false) : base(opcode) { Operation = opcode; @@ -299,11 +333,10 @@ public override bool Equals(object obj) public override int GetHashCode() => Operation.GetHashCode(); } - public class IRPop : IRInstruction, ISingleOperandInstruction + public class IRPop : SingleOperandInstruction { public override bool IsInvariant => Value.IsInvariant; - public IRValue Value { get; set; } - IRValue ISingleOperandInstruction.Operand { get => Value; set => Value = value; } + public IRValue Value { get => operand; set => operand = value; } public IRPop(IRValue value, OpcodePop opcode) : base(opcode) => Value = value; @@ -359,13 +392,12 @@ public override bool Equals(object obj) public override int GetHashCode() => Operation.GetHashCode(); } - public class IRSuffixGet : IRInstruction, IResultingInstruction, ISingleOperandInstruction + public class IRSuffixGet : SingleOperandInstruction, IResultingInstruction { public override bool IsInvariant => Object.IsInvariant; public IRValue Result { get; set; } - public IRValue Object { get; set; } + public IRValue Object { get => operand; set => operand = value; } public string Suffix { get; set; } - IRValue ISingleOperandInstruction.Operand { get => Object; set => Object = value; } public Type ResultType => TypeInferencer.GetTypeForSuffix(Object.ValueType, Suffix); public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcodeGetMember) : base(opcodeGetMember) { @@ -384,7 +416,7 @@ public override string ToString() public override bool Equals(object obj) => obj is IRSuffixGet suffixGet && !(suffixGet is IRSuffixGetMethod) && - string.Equals(Suffix, suffixGet.Suffix, System.StringComparison.OrdinalIgnoreCase) && + string.Equals(Suffix, suffixGet.Suffix, StringComparison.OrdinalIgnoreCase) && Object == suffixGet.Object; public override int GetHashCode() => (Object, Suffix).GetHashCode(); @@ -402,19 +434,19 @@ public override string ToString() => string.Format("{{gmet \"{0}\"}}", Suffix); public override bool Equals(object obj) => obj is IRSuffixGetMethod suffixGet && - string.Equals(Suffix, suffixGet.Suffix, System.StringComparison.OrdinalIgnoreCase) && + string.Equals(Suffix, suffixGet.Suffix, StringComparison.OrdinalIgnoreCase) && Object == suffixGet.Object; public override int GetHashCode() => (Object, Suffix).GetHashCode(); } - public class IRSuffixSet : IRInstruction, IMultipleOperandInstruction + public class IRSuffixSet : MultipleOperandInstruction { public override bool IsInvariant => false; public IRValue Object { get; set; } public IRValue Value { get; set; } - public IEnumerable Operands { get { yield return Object; yield return Value; } } - public int OperandCount => 2; - IRValue IMultipleOperandInstruction.this[int index] + public override IEnumerable Operands { get { yield return Object; yield return Value; } } + public override int OperandCount => 2; + protected override IRValue this[int index] { get => index == 0 ? Object : index == 1 ? Value : throw new ArgumentOutOfRangeException(); set @@ -446,24 +478,24 @@ public override string ToString() => string.Format("{{smb \"{0}\"}}", Suffix); public override bool Equals(object obj) => obj is IRSuffixSet suffixSet && - string.Equals(Suffix, suffixSet.Suffix, System.StringComparison.OrdinalIgnoreCase) && + string.Equals(Suffix, suffixSet.Suffix, StringComparison.OrdinalIgnoreCase) && Object.Equals(suffixSet.Object) && Value.Equals(suffixSet.Value); public override int GetHashCode() => (Object, Suffix).GetHashCode(); } - public class IRIndexGet : IRInstruction, IResultingInstruction, IMultipleOperandInstruction + public class IRIndexGet : MultipleOperandInstruction, IResultingInstruction { public override bool IsInvariant => Object.IsInvariant && Index.IsInvariant; public IRValue Result { get; } public IRValue Object { get; set; } public IRValue Index { get; set; } - public IEnumerable Operands { get { yield return Object; yield return Index; } } - public int OperandCount => 2; + public override IEnumerable Operands { get { yield return Object; yield return Index; } } + public override int OperandCount => 2; public Type ResultType => TypeInferencer.GetTypeForIndex(Object.ValueType); - IRValue IMultipleOperandInstruction.this[int index] + protected override IRValue this[int index] { - get => index == 0 ? Object : index == 1 ? Index : throw new System.ArgumentOutOfRangeException(); + get => index == 0 ? Object : index == 1 ? Index : throw new ArgumentOutOfRangeException(); set { if (index == 0) @@ -471,7 +503,7 @@ IRValue IMultipleOperandInstruction.this[int index] else if (index == 1) Index = value; else - throw new System.ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(); } } public IRIndexGet(IRTemp result, IRValue obj, IRValue index, OpcodeGetIndex opcode) : base(opcode) @@ -497,17 +529,17 @@ public override bool Equals(object obj) public override int GetHashCode() => Object.GetHashCode(); } - public class IRIndexSet : IRInstruction, IMultipleOperandInstruction + public class IRIndexSet : MultipleOperandInstruction { public override bool IsInvariant => false; public IRValue Object { get; set; } public IRValue Index { get; set; } public IRValue Value { get; set; } - public IEnumerable Operands { get { yield return Object; yield return Index; yield return Value; } } - public int OperandCount => 3; - IRValue IMultipleOperandInstruction.this[int index] + public override IEnumerable Operands { get { yield return Object; yield return Index; yield return Value; } } + public override int OperandCount => 3; + protected override IRValue this[int index] { - get => index == 0 ? Object : index == 1 ? Index : index == 2 ? Value : throw new System.ArgumentOutOfRangeException(); + get => index == 0 ? Object : index == 1 ? Index : index == 2 ? Value : throw new ArgumentOutOfRangeException(); set { if (index == 0) @@ -517,7 +549,7 @@ IRValue IMultipleOperandInstruction.this[int index] else if (index == 2) Value = value; else - throw new System.ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(); } } public IRIndexSet(IRValue obj, IRValue index, IRValue value, OpcodeSetIndex opcode) : base(opcode) @@ -568,11 +600,10 @@ public override bool Equals(object obj) public override int GetHashCode() => Target.GetHashCode(); } - public class IRJumpStack : IRInstruction, ISingleOperandInstruction + public class IRJumpStack : SingleOperandInstruction { public override bool IsInvariant => Distance.IsInvariant; - public IRValue Distance { get; set; } - IRValue ISingleOperandInstruction.Operand { get => Distance; set => Distance = value; } + public IRValue Distance { get => operand; set => operand = value; } public List Targets { get; } = new List(); public IRJumpStack(IRValue distance, IEnumerable targets, OpcodeJumpStack jumpStack) : base(jumpStack) { @@ -592,11 +623,10 @@ public override bool Equals(object obj) public override int GetHashCode() => Targets.GetHashCode(); } - public class IRBranch : IRInstruction, ISingleOperandInstruction + public class IRBranch : SingleOperandInstruction { public override bool IsInvariant => Condition.IsInvariant; - public IRValue Condition { get; set; } - IRValue ISingleOperandInstruction.Operand { get => Condition; set => Condition = value; } + public IRValue Condition { get => operand; set => operand = value; } public BasicBlock True { get; set; } public BasicBlock False { get; set; } public bool PreferFalse { get; set; } = false; @@ -632,7 +662,7 @@ public override bool Equals(object obj) public override int GetHashCode() => True.GetHashCode() ^ False.GetHashCode(); } - public class IRCall : IRInstruction, IResultingInstruction, IMultipleOperandInstruction + public class IRCall : MultipleOperandInstruction, IResultingInstruction { private bool isResultTypeInformed = false; private Type resultType = null; @@ -648,8 +678,8 @@ public bool IsFunctionInvariant public IRValue Result { get; set; } public string Function { get; } public List Arguments { get; } = new List(); - public IEnumerable Operands => Enumerable.Reverse(Arguments); - public int OperandCount => Arguments.Count; + public override IEnumerable Operands => Enumerable.Reverse(Arguments); + public override int OperandCount => Arguments.Count; public Type ResultType { get => isResultTypeInformed ? resultType : GetDefaultReturnType(); @@ -659,7 +689,7 @@ public Type ResultType isResultTypeInformed = true; } } - IRValue IMultipleOperandInstruction.this[int index] + protected override IRValue this[int index] { get => Arguments[index]; set => Arguments[index] = value; @@ -727,16 +757,15 @@ public override string ToString() => string.Format("{{call {0}({1})}}", Function.Trim('(', ')'), string.Join(",", Arguments.Select(a => a.ToString()))); public override bool Equals(object obj) => obj is IRCall call && - string.Equals(Function.Replace("()", ""), call.Function.Replace("()", ""), System.StringComparison.OrdinalIgnoreCase) && + string.Equals(Function.Replace("()", ""), call.Function.Replace("()", ""), StringComparison.OrdinalIgnoreCase) && Arguments.SequenceEqual(call.Arguments); public override int GetHashCode() => Function.ToLower().GetHashCode(); } - public class IRReturn : IRInstruction, ISingleOperandInstruction + public class IRReturn : SingleOperandInstruction { public override bool IsInvariant => Value.IsInvariant; - public IRValue Value { get; set; } - IRValue ISingleOperandInstruction.Operand { get => Value; set => Value = value; } + public IRValue Value { get => operand; set => operand = value; } public short Depth { get; internal set; } public IRReturn(short depth, OpcodeReturn opcode) : base(opcode) => Depth = depth; diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index 91b3bdd5c..75ac465a3 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -253,29 +253,16 @@ private static void ApplyUses(BasicBlock block, List trigg } } } - switch (inst) + if (inst is IOperandInstructionBase operandInstruction) { - case ISingleOperandInstruction singleOperandInstruction: - { - if (singleOperandInstruction.Operand is IRVariable variable && - !triggerBlacklist.Contains(variable) && - liveDefinitions.TryGetValue(variable, out SSAVariable currentValue)) - { - singleOperandInstruction.Operand = currentValue; - } - } - break; - case IMultipleOperandInstruction multipleOperandInstruction: - for (int j = 0; j < multipleOperandInstruction.OperandCount; j++) - { - if (multipleOperandInstruction[j] is IRVariable variable && - !triggerBlacklist.Contains(variable) && - liveDefinitions.TryGetValue(variable, out SSAVariable currentValue)) - { - multipleOperandInstruction[j] = currentValue; - } - } - break; + operandInstruction.MutateEachOperand(op => + { + if (op is IRVariable variable && + !triggerBlacklist.Contains(variable) && + liveDefinitions.TryGetValue(variable, out SSAVariable currentValue)) + return currentValue; + return op; + }); } } } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs index 026425054..f00127009 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs @@ -445,26 +445,16 @@ private static void AttemptPropagation(IRInstruction instruction, constantCache.Remove(variable); } } - if (instruction is ISingleOperandInstruction singleOperandInstruction) + if (instruction is IOperandInstructionBase operandInstruction) { - if (singleOperandInstruction.Operand is IRVariable variable && + operandInstruction.MutateEachOperand(op => + { + if (op is IRVariable variable && constantCache.ContainsKey(variable) && constantCache[variable] is IRConstant) - { - singleOperandInstruction.Operand = constantCache[variable]; - } - } - else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) - { - for (int j = multipleOperandInstruction.OperandCount - 1; j >= 0; j--) - { - if (multipleOperandInstruction[j] is IRVariable variable && - constantCache.ContainsKey(variable) && - constantCache[variable] is IRConstant) - { - multipleOperandInstruction[j] = constantCache[variable]; - } - } + return constantCache[variable]; + return op; + }); } } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs index fbe4ae204..ca6b8f2d7 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs @@ -228,55 +228,38 @@ opR.Operation is OpcodeMathPower && break; } } - - if (instruction is ISingleOperandInstruction singleOperandInstruction) + + if (instruction is IOperandInstructionBase operandInstruction) { - if (singleOperandInstruction.Operand is IRTemp tempOperand) + // Redundant unary operation replacements + // E.g. !!X = X and --X = X + operandInstruction.MutateEachOperand(op => { - // Redundant unary operation replacements - // E.g. !!X = X and --X = X - if (tempOperand.Parent is IRUnaryOp unaryParent) + if (op is IRTemp tempOperand && + tempOperand.Parent is IRUnaryOp unaryParent) { IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); if (potentialResult != null) - singleOperandInstruction.Operand = potentialResult; + return potentialResult; } - // Replace lex indexing using string constant with suffixing where possible - if (tempOperand.Parent is IRIndexGet indexGet && + return op; + }); + + // Replace lex indexing using string constant with suffixing where possible + operandInstruction.ForEachOperand(op => + { + if (op is IRTemp tempOperand && + tempOperand.Parent is IRIndexGet indexGet && indexGet.Index is IRConstant indexConstant && - (indexConstant.Value is string || - indexConstant.Value is Encapsulation.StringValue)) + (indexConstant.Value is Encapsulation.StringValue || + indexConstant.Value is string)) { IRInstruction potentialResult = AttemptReplaceIndexGetWithSuffixGet(indexGet); if (potentialResult != null) tempOperand.Parent = potentialResult; } - } - } - else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) - { - for (int i = multipleOperandInstruction.OperandCount - 1; i >= 0; i--) - { - IRValue operand = multipleOperandInstruction[i]; - if (operand is IRTemp tempOperand) - { - // Redundant unary operation replacements - // E.g. !!X = X and --X = X - if (tempOperand.Parent is IRUnaryOp unaryParent) - { - IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); - if (potentialResult != null) - multipleOperandInstruction[i] = potentialResult; - } - // Replace lex indexing using string constant with suffixing where possible - if (tempOperand.Parent is IRIndexGet indexGet) - { - IRInstruction potentialResult = AttemptReplaceIndexGetWithSuffixGet(indexGet); - if (potentialResult != null) - tempOperand.Parent = potentialResult; - } - } - } + + }); } // Branch logical simplification (e.g. !X branch = X branch!) if (instruction is IRBranch branch && From fa0345b0308be19508b1687c3d28df22e12b5d3b Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 17 Apr 2026 16:37:11 -0400 Subject: [PATCH 064/120] Make OpcodeStore code shared. Fix StoreLocal not creating a local variable if one of the same name existed at a higher scope. --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 66 ++++++++++-------------- src/kOS.Safe/Compilation/IR/IRScope.cs | 15 ++++-- 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 20b788e5f..8b90653b3 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -172,48 +172,16 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack opcodePush.Argument is string identifier && identifier.StartsWith("$"); - private SSAVariable GetSSAVariable(IRScope scope, OpcodeIdentifierBase store) + private static SSAVariable GetSSAVariable(IRScope scope, OpcodeIdentifierBase store, bool includeParents = true) { - IRVariable variable = (IRVariable)scope.GetVariableNamed(store.Identifier) ?? new IRVariable(store, scope); + IRVariable variable = (IRVariable)scope.GetVariableNamed(store.Identifier, includeParents) ?? new IRVariable(store, scope); return variable.GetNewSSAVariable(); } } diff --git a/src/kOS.Safe/Compilation/IR/IRScope.cs b/src/kOS.Safe/Compilation/IR/IRScope.cs index 87257fc98..f6e9b7cdb 100644 --- a/src/kOS.Safe/Compilation/IR/IRScope.cs +++ b/src/kOS.Safe/Compilation/IR/IRScope.cs @@ -117,6 +117,13 @@ public bool TryStoreVariable(IRVariableBase variable) } return ParentScope?.TryStoreVariable(variable) ?? false; } + public void UnsetVariable(string variable) + { + if (variables.ContainsKey(variable)) + variables.Remove(variable); + else + ParentScope?.UnsetVariable(variable); + } public void EnrollFunction(string variable, string functionRef) { @@ -129,18 +136,18 @@ public string GetFunctionNameFromVariable(string variable) return null; } - public IRVariableBase GetVariableNamed(string name) + public IRVariableBase GetVariableNamed(string name, bool includeParent = true) { if (variables.ContainsKey(name)) return variables[name]; - return ParentScope?.GetVariableNamed(name); + return includeParent ? ParentScope?.GetVariableNamed(name, includeParent) : null; } - public bool IsVariableInScope(string name) + public bool IsVariableInScope(string name, bool includeParent = true) { if (variables.ContainsKey(name)) return true; - return ParentScope?.IsVariableInScope(name) ?? false; + return includeParent && (ParentScope?.IsVariableInScope(name, includeParent) ?? false); } public bool IsVariableInScope(IRVariableBase variable) { From b8e350d990679a05cc13900eff04ba8ff72ef217 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 18 Apr 2026 22:21:31 -0400 Subject: [PATCH 065/120] Implement SCCP for constant propagation, replacing the former constant propagation pass. This pass also propagates type inferences across Phi variables. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 3 +- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 7 + src/kOS.Safe/Compilation/IR/IRValue.cs | 74 ++- .../Compilation/IR/SingleStaticAssignment.cs | 96 ++-- .../Optimization/Passes/ConstantFolding.cs | 1 - .../Passes/ConstantPropagation.cs | 498 ------------------ .../Passes/SCCPWithTypePropagation.cs | 263 +++++++++ 7 files changed, 391 insertions(+), 551 deletions(-) delete mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs create mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index b52d3bbc4..9e8b16d4f 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -93,8 +93,7 @@ public IRScope Scope /// /// This data is populated during . /// - public Dictionary values)> Phis { get; } = - new Dictionary values)>(); + public HashSet Phis { get; } = new HashSet(); /// /// Gets the set of incoming SSA variables that this block /// receives, including the results of any phi functions. diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 1d7593bbd..f9787e1cb 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -51,6 +51,11 @@ protected MultipleOperandInstruction(Opcode originalOpcode) : base(originalOpcod public abstract IEnumerable Operands { get; } public abstract int OperandCount { get; } + /// + /// Allows replacing operands from a common function. + /// The meaning of the index and ordering are irrelevant, + /// as long as it covers the range [0, ). + /// protected abstract IRValue this[int index] { get; set; } public void ForEachOperand(Action action) @@ -84,6 +89,8 @@ public IRAssign(OpcodeIdentifierBase opcode, IRVariableBase target, IRValue valu { Target = target; Value = value; + if (target is SSAVariable ssaTarget) + ssaTarget.AssignedAt = this; } internal override IEnumerable EmitOpcode() { diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index bc4e84b55..d91e58ba1 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace kOS.Safe.Compilation.IR { @@ -101,6 +102,14 @@ public virtual SSAVariable GetNewSSAVariable() return nextIteration; } + public virtual PhiVariable GetNewPhiVariable() + { + PhiVariable phiVariable = new PhiVariable(this, nextSSAIndex); + iterations[nextSSAIndex] = phiVariable; + nextSSAIndex += 1; + return phiVariable; + } + internal override IEnumerable EmitPush() { yield return new OpcodePush(Name) @@ -120,8 +129,13 @@ public class SSAVariable : IRVariable { private readonly ushort ssaIndex; - public override bool IsInvariant { get; set; } = true; + public override bool IsInvariant + { + get => AssignedAt.IsInvariant; + set => throw new InvalidOperationException($"Cannot set the invariant state of an {nameof(SSAVariable)}."); + } public IRVariable Parent { get; } + public IRAssign AssignedAt { get; set; } internal SSAVariable(IRVariable baseVariable, ushort ssaIndex) : base(baseVariable.Name, baseVariable.Scope, baseVariable.sourceLine, baseVariable.sourceColumn) { @@ -132,6 +146,8 @@ internal SSAVariable(IRVariable baseVariable, ushort ssaIndex) : public override SSAVariable GetNewSSAVariable() => Parent.GetNewSSAVariable(); + public override PhiVariable GetNewPhiVariable() + => Parent.GetNewPhiVariable(); public override string ToString() { @@ -145,6 +161,58 @@ public override bool Equals(object obj) public override int GetHashCode() => (ssaIndex, GetBaseHashCode()).GetHashCode(); } + + public class PhiVariable : SSAVariable, IMultipleOperandInstruction + { + public Dictionary PossibleValues { get; } = new Dictionary(); + public override bool IsInvariant { get => !PossibleValues.Skip(1).Any() && PossibleValues.Values.First().IsInvariant; set => throw new InvalidOperationException(); } + + IEnumerable IMultipleOperandInstruction.Operands => PossibleValues.Values; + + int IMultipleOperandInstruction.OperandCount => PossibleValues.Count; + + internal PhiVariable(IRVariable baseVariable, ushort ssaIndex) : base(baseVariable, ssaIndex) { } + + public void RefreshType() + { + Type proposedType = PossibleValues.Values.First().ValueType; + + foreach (SSAVariable variable in PossibleValues.Values.Skip(1)) + proposedType = GetFirstCommonBaseType(proposedType, variable.ValueType); + + ValueType = proposedType; + } + + public static Type GetFirstCommonBaseType(Type typeA, Type typeB) + { + if (typeA == null || typeB == null) return null; + + Type current = typeA; + while (current != null) + { + if (current.IsAssignableFrom(typeB)) + { + return current; + } + current = current.BaseType; + } + + throw new Exceptions.KOSYouShouldNeverSeeThisException($"Couldn't find a base class between {typeA} and {typeB}, when all kOS types should derive from {nameof(Encapsulation.Structure)}."); + } + + void IOperandInstructionBase.ForEachOperand(Action action) + { + foreach (SSAVariable variable in PossibleValues.Values) + action(variable); + } + + void IOperandInstructionBase.MutateEachOperand(Func mutateFunc) + { + foreach (BasicBlock block in PossibleValues.Keys.ToArray()) + PossibleValues[block] = (SSAVariable)mutateFunc(PossibleValues[block]); + } + } + public class IRRelocateLater : IRConstant { public IRRelocateLater(string value, OpcodePushRelocateLater opcode) : base(value, opcode) @@ -162,6 +230,7 @@ internal override IEnumerable EmitPush() }; } } + public class IRDelegateRelocateLater : IRRelocateLater { public bool WithClosure { get; } @@ -178,6 +247,7 @@ internal override IEnumerable EmitPush() }; } } + public class IRTemp : IRVariableBase { private bool isPromoted = false; @@ -188,7 +258,7 @@ public override Type ValueType set => throw new InvalidOperationException($"Cannot set the result type state of an {nameof(IRTemp)}."); } public int ID { get; } - public IRInstruction Parent { get; internal set; } + public IRInstruction Parent { get; set; } public override bool IsInvariant { get => Parent.IsInvariant; diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index 75ac465a3..36bff5153 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -13,6 +13,8 @@ public static class SingleStaticAssignment { private static readonly Dictionary> postCallSSAVariables = new Dictionary>(); + private static readonly Dictionary postCallSSAVariableLinks = + new Dictionary(); /// /// Finalizes a program into single static assignment form. @@ -55,6 +57,7 @@ private static void AnalyzeBlock(BasicBlock block, IRCodePart codePart) foreach (SSAVariable variable in function.ExternalWrites) { SSAVariable ssaVariable = variable.GetNewSSAVariable(); + postCallSSAVariableLinks.Add(variable, ssaVariable); OverwriteVariable(variables, ssaVariable); ssaVariables.Add(ssaVariable); } @@ -79,19 +82,52 @@ private static void AnalyzeBlock(BasicBlock block, IRCodePart codePart) } break; - // On encountering a trigger: - // Clear the cache of any variables that are written in that trigger or body. - // Blacklist any variables that are written in the trigger or body. - case IRUnaryConsumer triggerInstruction: - if (triggerInstruction.Operation is OpcodeAddTrigger) + case IRUnaryConsumer unaryConsumer: + // On encountering a trigger: + // Clear the cache of any variables that are written in that trigger or body. + // Blacklist any variables that are written in the trigger or body. + if (unaryConsumer.Operation is OpcodeAddTrigger) { - string pointer = (string)((IRConstant)triggerInstruction.Operand).Value; + string pointer = (string)((IRConstant)unaryConsumer.Operand).Value; // Re-scope the stored variables from Global to the current scope. IRCodePart.IRTrigger trigger = codePart.Triggers.FirstOrDefault(t => string.Equals(t.Identifier, pointer, StringComparison.OrdinalIgnoreCase)); if (trigger != null) IRCodePart.SetDefiningScope(trigger, block.Scope); block.TriggerPropagationBlacklist.UnionWith(trigger.ExternalWrites); } + // On encountering an unset operation: + // Remove the affected definition from the stored variables. + // If the affected definition cannot be determined, clear all current variables + if (unaryConsumer.Operation is OpcodeUnset) + { + if (!unaryConsumer.IsInvariant) + { + variables.Clear(); + } + else + { + IRConstant unsetConst = unaryConsumer.Operand as IRConstant; + if (unsetConst == null && unaryConsumer.Operand is IRTemp temp) + unsetConst = Optimization.Passes.ConstantFolding.AttemptReduction(temp.Parent) as IRConstant; + if (unsetConst != null && + (unsetConst.Value is Encapsulation.StringValue || + unsetConst.Value is string)) + { + /*IRVariableBase unsetVar = block.Scope.GetVariableNamed( + (Encapsulation.StringValue)unsetConst.Value); + if (unsetVar != null) + variables.RemoveWhere(v => v.Parent.Equals(unsetVar));*/ + variables.RemoveWhere(v => + v.Name.Equals((Encapsulation.StringValue)unsetConst.Value, + StringComparison.OrdinalIgnoreCase)); + } + else + // TODO: See if this can be optimized through the SCCP pass. + // Probably not worth it since unsets are rather rare, + // and unsets without a string const even more so. + variables.Clear(); + } + } break; } } @@ -131,29 +167,21 @@ private static void BuildPhis(BasicBlock root) if (definition.Select(bv => bv.variable).Distinct().Skip(1).Any()) { // Phi required - Dictionary phiDict; - SSAVariable phiVar; - if (block.Phis.TryGetValue(definition.Key, out (SSAVariable phiVar, Dictionary values) result)) - { - phiDict = result.values; - phiVar = result.phiVar; - } - else + PhiVariable phiVar = block.Phis.FirstOrDefault(v => v.Parent.Equals(definition.Key)); + if (phiVar == null) { - phiVar = definition.Key.GetNewSSAVariable(); - phiDict = new Dictionary(); - block.Phis[definition.Key] = (phiVar, phiDict); + phiVar = definition.Key.GetNewPhiVariable(); + block.Phis.Add(phiVar); } foreach ((BasicBlock block, SSAVariable variable) def in definition) - phiDict[def.block] = def.variable; + phiVar.PossibleValues[def.block] = def.variable; - phiVar.IsInvariant = false; variablesIn.Add(phiVar); } else { - block.Phis.Remove(definition.Key); + block.Phis.RemoveWhere(v => v.Parent.Equals(definition.Key)); variablesIn.Add(definition.First().variable); } } @@ -176,23 +204,6 @@ private static void BuildPhis(BasicBlock root) } } - private static Type GetFirstCommonBaseType(Type typeA, Type typeB) - { - if (typeA == null || typeB == null) return null; - - Type current = typeA; - while (current != null) - { - if (current.IsAssignableFrom(typeB)) - { - return current; - } - current = current.BaseType; - } - - throw new Exceptions.KOSYouShouldNeverSeeThisException($"Couldn't find a base class between {typeA} and {typeB}, when all kOS types should derive from {nameof(Encapsulation.Structure)}."); - } - private static void ApplyUses(BasicBlock block, List triggers) { List instructions = block.Instructions; @@ -204,15 +215,6 @@ private static void ApplyUses(BasicBlock block, List trigg foreach (SSAVariable variable in block.IncomingVariableDefinitions) liveDefinitions[variable.Parent] = variable; - foreach (var (phiVar, values) in block.Phis.Values) - { - Type proposedType = values.Values.First().ValueType; - // MUSTFIX: This doesn't work for circular references and only emits typeof(Structure) in those cases. - foreach (SSAVariable variable in values.Values) - proposedType = GetFirstCommonBaseType(proposedType, variable.ValueType); - - phiVar.ValueType = proposedType; - } for (int i = 0; i < instructions.Count; i++) { @@ -233,8 +235,6 @@ private static void ApplyUses(BasicBlock block, List trigg else if (inst is IRAssign assignment && assignment.Target is SSAVariable ssaVariable) { - ssaVariable.ValueType = assignment.Value.ValueType; - if (!triggerBlacklist.Contains(ssaVariable.Parent)) liveDefinitions[ssaVariable.Parent] = ssaVariable; else diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index ab3bb56e0..b07f0f3d0 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -55,7 +55,6 @@ private static void ApplyPass(BasicBlock block) (permanentBlock, deprecatedBlock) = Convert.ToBoolean(branchConstant.Value) ? (branch.True, branch.False) : (branch.False, branch.True); block.Instructions[i] = new IRJump(permanentBlock, new OpcodeBranchJump() { SourceLine = branch.SourceLine, SourceColumn = branch.SourceColumn }); block.RemoveSuccessor(deprecatedBlock); - // TODO: Resolve any phi values in the deprecated block and consider re-running folding on that block. } break; } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs deleted file mode 100644 index f00127009..000000000 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs +++ /dev/null @@ -1,498 +0,0 @@ -using kOS.Safe.Compilation.IR; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace kOS.Safe.Compilation.Optimization.Passes -{ - public class ConstantPropagation// : IHolisticOptimizationPass - { - public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; - - public short SortIndex => 20; - - public void ApplyPass(IRCodePart codePart) - { - Dictionary> funcExternalWrites = new Dictionary>(); - Dictionary> funcExternalReads = new Dictionary>(); - - // For each function, find the sets of non-local variables read and those written to. - foreach (IRCodePart.IRFunction function in codePart.Functions) - { - HashSet externalWrites = new HashSet(); - HashSet externalReads = new HashSet(); - foreach (IRCodePart.IRFunction.IRFunctionFragment fragment in function.Fragments) - { - AnalyzeFunction(fragment.FunctionCode, externalWrites, externalReads); - } - funcExternalWrites[function.Identifier] = externalWrites; - funcExternalReads[function.Identifier] = externalReads; - } - foreach (IRCodePart.IRTrigger trigger in codePart.Triggers) - { - HashSet externalWrites = new HashSet(); - HashSet externalReads = new HashSet(); - AnalyzeFunction(trigger.Code, externalWrites, externalReads); - funcExternalWrites[trigger.Identifier] = externalWrites; - funcExternalReads[trigger.Identifier] = externalReads; - } - - Dictionary> variablesPropagated = new Dictionary>(); - Dictionary> variablesUnpropagated = new Dictionary>(); - - Dictionary inOutData = new Dictionary(); - Dictionary storedFuncs = new Dictionary(); - - foreach (BasicBlock rootBlock in codePart.RootBlocks) - { - Dictionary map = MapIncoming(rootBlock, - funcExternalWrites, - out Dictionary storedFunctions); - foreach (KeyValuePair pair in map) - inOutData.Add(pair.Key, pair.Value); - foreach (KeyValuePair pair in storedFunctions) - storedFuncs.Add(pair.Key, pair.Value); - } - - foreach (BasicBlock block in codePart.Blocks) - { - if (block.Scope.IsGlobalScope) - PropagateWithinBlock(block, inOutData, - storedFuncs, - new HashSet(), - variablesUnpropagated, - funcExternalReads, - funcExternalWrites); - else - { - if (!variablesPropagated.ContainsKey(block.Scope)) - { - variablesPropagated.Add(block.Scope, new HashSet()); - variablesUnpropagated.Add(block.Scope, new HashSet()); - } - PropagateWithinBlock(block, inOutData, - storedFuncs, - variablesPropagated[block.Scope], - variablesUnpropagated, - funcExternalReads, - funcExternalWrites); - } - } - - foreach (IRScope scope in variablesPropagated.Keys) - { - if (variablesUnpropagated.ContainsKey(scope)) - variablesPropagated[scope].ExceptWith(variablesUnpropagated[scope]); - foreach (IRVariableBase variable in variablesPropagated[scope]) - scope.ClearVariable(variable); - } - } - - private static void AnalyzeFunction(List functionCode, HashSet writes, HashSet reads) - { - if (functionCode.Any()) - reads.UnionWith(functionCode.First().Scope.GetGlobalScope().Variables); - foreach (BasicBlock block in functionCode) - { - foreach (IRInstruction instruction in block.Instructions) - { - if (instruction is IRAssign assignment && - assignment.Target.Scope.IsGlobalScope) - writes.Add(assignment.Target); - } - } - } - - private static void RescopeFunctionVars(HashSet variables, IRScope scope) - { - HashSet temp = new HashSet(); - foreach(IRVariableBase variable in variables) - { - if (scope.IsVariableInScope(variable.Name)) - temp.Add(scope.GetVariableNamed(variable.Name)); - else - temp.Add(variable); - } - variables.Clear(); - variables.UnionWith(temp); - } - - private static Dictionary MapIncoming(BasicBlock rootBlock, - Dictionary> funcExternalWrites, - out Dictionary storedFunctions) - { - storedFunctions = new Dictionary(); - - HashSet GetValueOrDefault(Dictionary> dict, BasicBlock key, IEqualityComparer comparer) - { - if (!dict.ContainsKey(key)) - { - if (comparer == null) - comparer = EqualityComparer.Default; - HashSet result = new HashSet(comparer); - dict.Add(key, result); - return result; - } - return dict[key]; - } - - Dictionary> incoming = new Dictionary>(); - Dictionary> blacklistIn = new Dictionary>(); - - Dictionary> outgoing = new Dictionary>(); - Dictionary> blacklistOut = new Dictionary>(); - Dictionary> defs = new Dictionary>(); - - HashSet basicBlocks = new HashSet(); - Queue worklist = new Queue(); - worklist.Enqueue(rootBlock); - - while (worklist.Count > 0) - { - BasicBlock block = worklist.Dequeue(); - basicBlocks.Add(block); - - if (!defs.ContainsKey(block)) - { - ParseDefs(block, - out Dictionary defsMade, - GetValueOrDefault(blacklistOut, block, null), - storedFunctions, - funcExternalWrites); - defs[block] = new HashSet<(IRVariableBase variable, IRAssign value)>(defsMade.Select(kvp => (kvp.Key, kvp.Value)), VariableTupleEqualityComparer.Instance); - } - - incoming.Remove(block); - HashSet<(IRVariableBase variable, IRAssign value)> defsIn = GetValueOrDefault(incoming, block, VariableTupleEqualityComparer.Instance); - blacklistIn.Remove(block); - HashSet localBlacklist = GetValueOrDefault(blacklistIn, block, null); - BasicBlock firstPredecessor = block.Predecessors.FirstOrDefault(); - if (firstPredecessor != null) - { - defsIn.UnionWith(GetValueOrDefault(outgoing, firstPredecessor, VariableTupleEqualityComparer.Instance)); - localBlacklist.UnionWith(GetValueOrDefault(blacklistOut, firstPredecessor, null)); - } - - foreach (BasicBlock predecessor in block.Predecessors.Skip(1)) - { - if (!outgoing.ContainsKey(predecessor)) - continue; - defsIn.IntersectWith(GetValueOrDefault(outgoing, predecessor, VariableTupleEqualityComparer.Instance)); - localBlacklist.UnionWith(GetValueOrDefault(blacklistOut, predecessor, null)); - } - - defsIn.RemoveWhere(vv => !(vv.variable.Scope == block.Scope || block.Scope.IsEncompassedBy(vv.variable.Scope))); - defsIn.RemoveWhere(vv => localBlacklist.Contains(vv.variable)); - - HashSet<(IRVariableBase variable, IRAssign value)> defsOut = new HashSet<(IRVariableBase variable, IRAssign value)>(defsIn, VariableTupleEqualityComparer.Instance); - HashSet<(IRVariableBase variable, IRAssign value)> definitions = defs[block]; - - defsOut.UnionWith(defsIn); - foreach (var def in definitions) - { - defsOut.RemoveWhere(vv => vv.variable.Equals(def.variable)); - defsOut.Add(def); - } - defsOut.RemoveWhere(vv => localBlacklist.Contains(vv.variable)); - - if (outgoing.ContainsKey(block) && defsOut.SetEquals(outgoing[block])) - continue; - - outgoing[block] = defsOut; - blacklistOut[block].UnionWith(localBlacklist); - foreach (BasicBlock successor in block.Successors) - worklist.Enqueue(successor); - } - - Dictionary map = new Dictionary(); - foreach (BasicBlock block in basicBlocks) - map.Add(block, new BlockDictionaries(incoming[block], blacklistIn[block])); - - return map; - } - - private static void ParseDefs(BasicBlock block, - out Dictionary defs, - HashSet blacklist, - Dictionary storedFunctions, - Dictionary> funcExternalWrites) - { - defs = new Dictionary(); - List instructions = block.Instructions; - for (int i = 0; i < block.Instructions.Count; i++) - { - IRInstruction instruction = instructions[i]; - // Step through instructions (depth first) and replace variables - // with their cached constant whenever they're available. - foreach (IRInstruction nestedInstruction in instruction.DepthFirst()) - { - RemoveVarsWrittenInFunction(nestedInstruction, - block.Scope, - defs, - storedFunctions, - funcExternalWrites); - } - - switch (instruction) - { - // On a local assignment by a constant, cache that constant against the variable. - case IRAssign assignment: - if (!blacklist.Contains(assignment.Target) && - !assignment.Target.Scope.IsGlobalScope) - { - defs[assignment.Target] = assignment; - } - - // On encountering a PushRelocateLater, note the scope for its closure variables. - if (assignment.Value is IRRelocateLater lockOrFunctionPointer) - { - string pointer = ((string)lockOrFunctionPointer.Value).Split('-').First(); - storedFunctions[assignment.Target] = pointer; - // Re-scope the stored variables from Global to the current scope. - RescopeFunctionVars(funcExternalWrites[pointer], block.Scope); - } - break; - - // On encountering a trigger: - // Clear the cache of any variables that are written in that trigger or body. - // Blacklist any variables that are written in the trigger or body. - case IRUnaryConsumer trigger: - if (trigger.Operation is OpcodeAddTrigger) - { - string destination = (string)((IRConstant)trigger.Operand).Value; - if (funcExternalWrites.ContainsKey(destination)) - { - foreach (IRVariableBase variable in funcExternalWrites[destination]) - defs.Remove(variable); - - // Re-scope the stored variables from Global to the current scope. - RescopeFunctionVars(funcExternalWrites[destination], block.Scope); - blacklist.UnionWith(funcExternalWrites[destination]); - } - } - break; - } - } - } - - private static void RemoveVarsWrittenInFunction(IRInstruction instr, - IRScope scope, - Dictionary defsCreated, - Dictionary storedFunctions, - Dictionary> funcExternalWrites) - { - if (instr is IRCall call) - { - // On encountering a Call, where that identifier is in our dictionary of function closure variables. - // Inject the assignment call to any constant variables that are read inside the function. - // Clear the cache of any variables that are written in that function. - IRVariableBase functionCall = scope.GetVariableNamed(call.Function); - if (functionCall != null && storedFunctions.ContainsKey(functionCall)) - { - string funcRef = storedFunctions[functionCall]; - - foreach (IRVariableBase variable in funcExternalWrites[funcRef]) - defsCreated.Remove(variable); - } - } - } - - private static void PropagateWithinBlock(BasicBlock block, - Dictionary inOutData, - Dictionary storedFunctions, - HashSet scopeVarsPropagated, - Dictionary> variablesUnpropagated, - Dictionary> externalVariablesRead, - Dictionary> externalVariablesWritten) - { - BlockDictionaries localInOut = inOutData[block]; - - List instructions = block.Instructions; - - inOutData[block].InitializeForPropagation( - out Dictionary constantCache, - out Dictionary assignmentInstructions, - out HashSet blacklist); - - for (int i = 0; i < instructions.Count; i++) - { - IRInstruction instruction = instructions[i]; - // Step through instructions (depth first) and replace variables - // with their cached constant whenever they're available. - foreach (IRInstruction nestedInstruction in instruction.DepthFirst()) - { - AttemptPropagation(nestedInstruction, - instructions, - ref i, - block.Scope, - constantCache, - storedFunctions, - externalVariablesRead, - externalVariablesWritten,assignmentInstructions, - variablesUnpropagated); - } - - switch (instruction) - { - // On a local assignment by a constant, cache that constant against the variable. - case IRAssign assignment: - if (!blacklist.Contains(assignment.Target) && - !assignment.Target.Scope.IsGlobalScope) - { - if (assignment.Value is IRTemp temp) - assignment.Value = ConstantFolding.AttemptReduction(temp.Parent); - if (assignment.Value is IRConstant constantValue) - { - scopeVarsPropagated.Add(assignment.Target); - instructions.RemoveAt(i); - assignmentInstructions[assignment.Target] = assignment; - constantCache[assignment.Target] = constantValue; - i--; - } - else - { - scopeVarsPropagated.Remove(assignment.Target); - assignmentInstructions.Remove(assignment.Target); - constantCache.Remove(assignment.Target); - } - } - - // On encountering a PushDelegateRelocateLater, note which variables are cached. - // Store the intersections of that set and the function variable sets. - else if (assignment.Value is IRDelegateRelocateLater functionPointer) - { - string pointer = ((string)functionPointer.Value).Split('-').First(); - storedFunctions[assignment.Target] = pointer; - // Re-scope the stored variables from Global to the current scope. - // Writes were accomplished on the first pass. - RescopeFunctionVars(externalVariablesRead[pointer], block.Scope); - } - - // On encountering a Lock call, we will:: - // Inject the assignment call to any constant variables that are read inside the function. - // Clear the cache of any variables that are written in that lock. - else if (assignment.Value is IRRelocateLater lockPointer) - { - string pointer = ((string)lockPointer.Value).Split('-').First(); - storedFunctions[assignment.Target] = pointer; - // Re-scope the stored variables from Global to the current scope. - // Writes were accomplished on the first pass. - RescopeFunctionVars(externalVariablesRead[pointer], block.Scope); - } - break; - - // On encountering a trigger: - // Inject the assignment call to any constant variables that are read inside the trigger or body. - // Clear the cache of any variables that are written in that trigger or body. - // Blacklist any variables that are written in the trigger or body. - case IRUnaryConsumer trigger: - if (trigger.Operation is OpcodeAddTrigger) - { - string destination = (string)((IRConstant)trigger.Operand).Value; - if (externalVariablesRead.ContainsKey(destination)) - { - foreach (IRVariableBase variable in externalVariablesRead[destination].Union( - externalVariablesWritten[destination])) - if (assignmentInstructions.ContainsKey(variable)) - { - instructions.Insert(i++, assignmentInstructions[variable]); - assignmentInstructions.Remove(variable); - variablesUnpropagated[variable.Scope].Add(variable); - } - - foreach (IRVariableBase variable in externalVariablesWritten[destination]) - constantCache.Remove(variable); - - blacklist.UnionWith(externalVariablesWritten[destination]); - } - } - break; - } - } - } - - private static void AttemptPropagation(IRInstruction instruction, - List instructions, - ref int i, - IRScope scope, - Dictionary constantCache, - Dictionary storedFunctions, - Dictionary> funcExternalReads, - Dictionary> funcExternalWrites, - Dictionary assignmentInstructions, - Dictionary> variablesUnpropagated) - { - if (instruction is IRCall call) - { - // On encountering a Call, where that identifier is in our dictionary of function closure variables. - // Inject the assignment call to any constant variables that are read inside the function. - // Clear the cache of any variables that are written in that function. - IRVariableBase functionCall = scope.GetVariableNamed(call.Function); - if (functionCall != null && storedFunctions.ContainsKey(functionCall)) - { - string funcRef = storedFunctions[functionCall]; - foreach (IRVariableBase variable in funcExternalReads[funcRef].Union( - funcExternalWrites[funcRef])) - if (assignmentInstructions.ContainsKey(variable)) - { - instructions.Insert(i++, assignmentInstructions[variable]); - assignmentInstructions.Remove(variable); - variablesUnpropagated[variable.Scope].Add(variable); - } - - foreach (IRVariableBase variable in funcExternalWrites[funcRef]) - constantCache.Remove(variable); - } - } - if (instruction is IOperandInstructionBase operandInstruction) - { - operandInstruction.MutateEachOperand(op => - { - if (op is IRVariable variable && - constantCache.ContainsKey(variable) && - constantCache[variable] is IRConstant) - return constantCache[variable]; - return op; - }); - } - } - - private class BlockDictionaries - { - public HashSet<(IRVariableBase IRVariableBase, IRAssign value)> incomingDefinitions; - public HashSet blacklist; - public BlockDictionaries( - HashSet<(IRVariableBase, IRAssign)> incomingDefinitions, - HashSet blacklist) - { - this.incomingDefinitions = incomingDefinitions; - this.blacklist = blacklist; - } - public void InitializeForPropagation( - out Dictionary constantCache, - out Dictionary assignments, - out HashSet blacklist) - { - constantCache = new Dictionary(); - assignments = new Dictionary(); - foreach ((IRVariableBase variable, IRAssign value) in incomingDefinitions) - { - constantCache.Add(variable, value.Value); - assignments.Add(variable, value); - } - blacklist = new HashSet(this.blacklist); - } - } - - private class VariableTupleEqualityComparer : IEqualityComparer<(IRVariableBase variable, IRAssign value)> - { - public static VariableTupleEqualityComparer Instance { get; } = new VariableTupleEqualityComparer(); - public bool Equals((IRVariableBase variable, IRAssign value) x, (IRVariableBase variable, IRAssign value) y) - => x.variable.Equals(y.variable) && (x.value.Value.Equals(y.value.Value)); - - public int GetHashCode((IRVariableBase variable, IRAssign value) obj) - => obj.variable.GetHashCode(); - } - } -} diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs new file mode 100644 index 000000000..aa68f8683 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.IR; +using kOS.Safe.Encapsulation; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class SCCPWithTypePropagation : ILinkedOptimizationPass + { + /// + /// Gets the optimization level associated with this pass. + /// + /// + /// Actual propagation only occurs for optimization levels + /// or above. + /// + public OptimizationLevel OptimizationLevel => OptimizationLevel.None; + + public short SortIndex => short.MinValue; + + public Optimizer Optimizer { private get; set; } + + public void ApplyPass() + { + IRCodePart codePart = Optimizer.Code; + + Dictionary> ssaUses = + MapUsesAndPropagateTypes(codePart, out HashSet executableBlocks); + + if (Optimizer.OptimizationLevel > OptimizationLevel.None) + PropagateConstants(ssaUses, executableBlocks); + } + + private static Dictionary> MapUsesAndPropagateTypes(IRCodePart codePart, out HashSet executableBlocks) + { + Dictionary> variableUses = + new Dictionary>(); + executableBlocks = new HashSet(); + HashSet visitedBlocks = new HashSet(); + + foreach (BasicBlock root in codePart.RootBlocks) + { + Queue blockQueue = new Queue(); + blockQueue.Enqueue(root); + Queue instructionQueue = new Queue(); + + while (blockQueue.Count > 0 || instructionQueue.Count > 0) + { + // Mark a block executable + // Visit every expression in that block + // Queue that block's successors + while (instructionQueue.Count > 0) + { + IOperandInstructionBase instruction = instructionQueue.Dequeue(); + if (VisitInstruction(instruction, blockQueue, executableBlocks)) + { + if (instruction is IRAssign assignment && + assignment.Target is SSAVariable ssaVariable) + foreach (IOperandInstructionBase use in GetOrCreate(variableUses, ssaVariable)) + instructionQueue.Enqueue(use); + else if (instruction is PhiVariable phi) + foreach (IOperandInstructionBase use in GetOrCreate(variableUses, phi)) + instructionQueue.Enqueue(use); + } + } + + if (blockQueue.Count > 0) + { + BasicBlock block = blockQueue.Dequeue(); + if (!visitedBlocks.Add(block)) + continue; + + foreach (PhiVariable phi in block.Phis) + { + foreach (SSAVariable variable in phi.PossibleValues.Values) + GetOrCreate(variableUses, variable).Add(phi); + + VisitInstruction(phi, blockQueue, executableBlocks); + } + + foreach (IRInstruction instruction in block.Instructions) + { + foreach (IOperandInstructionBase inst in instruction.DepthFirst().Where(i => i is IOperandInstructionBase).Cast()) + { + inst.ForEachOperand(op => + { + if (op is SSAVariable ssaVariable) + { + GetOrCreate(variableUses, ssaVariable).Add(inst); + if (instruction is IOperandInstructionBase opInst) + variableUses[ssaVariable].Add(opInst); + } + }); + if (inst is IRCall call) + { + string functionIdentifier = block.Scope.GetFunctionNameFromVariable(call.Function); + if (functionIdentifier != null) + { + IRCodePart.IRFunction function = codePart.Functions.FirstOrDefault(f => string.Equals(f.Identifier, functionIdentifier, StringComparison.OrdinalIgnoreCase)); + if (function != null) + { + foreach (IRVariable externalVar in function.ExternalReads.Union(function.ExternalWrites)) + { + if (externalVar is SSAVariable ssaVariable) + GetOrCreate(variableUses, ssaVariable).Add(call); + } + } + } + } + VisitInstruction(inst, blockQueue, executableBlocks); + } + + if (instruction is IOperandInstructionBase operandInstruction) + { + operandInstruction.ForEachOperand(op => + { + if (op is SSAVariable ssaVariable) + GetOrCreate(variableUses, ssaVariable).Add(operandInstruction); + }); + VisitInstruction(operandInstruction, blockQueue, executableBlocks); + } + } + + if (block.Successors.Count == 1) + blockQueue.Enqueue(block.Successors.First()); + + executableBlocks.Add(block); + } + } + } + + return variableUses; + } + + private static bool VisitInstruction(IOperandInstructionBase instruction, Queue blockQueue, HashSet executableBlocks) + { + if (instruction is IRAssign assignment) + { + bool result = assignment.Target.ValueType != assignment.Value.ValueType; + assignment.Target.ValueType = assignment.Value.ValueType; + return result; + } + else if (instruction is PhiVariable phi) + { + List possibleValues = new List( + phi.PossibleValues.Where(kvp => executableBlocks.Contains(kvp.Key)).Select(kvp => kvp.Value)); + + Type proposedType = possibleValues[0].ValueType; + foreach (SSAVariable variable in possibleValues.Skip(1)) + proposedType = PhiVariable.GetFirstCommonBaseType(proposedType, variable.ValueType); + + bool result = proposedType != phi.ValueType; + phi.ValueType = proposedType; + return result; + } + else if (instruction is IRBranch branch) + { + if (branch.IsInvariant) + { + IRConstant constantCondition = branch.Condition as IRConstant; + if (constantCondition == null && branch.Condition is IRTemp temp) + constantCondition = ConstantFolding.AttemptReduction(temp.Parent) as IRConstant; + + if (constantCondition != null && constantCondition.Value is BooleanValue boolean) + { + blockQueue.Enqueue(boolean ? branch.True : branch.False); + } + else + { + blockQueue.Enqueue(branch.True); + blockQueue.Enqueue(branch.False); + } + } + else + { + blockQueue.Enqueue(branch.True); + blockQueue.Enqueue(branch.False); + } + } + + return false; + } + + private static TValue GetOrCreate(Dictionary dictionary, TKey key) where TValue : new() + { + if (!dictionary.TryGetValue(key, out TValue value)) + value = dictionary[key] = new TValue(); + return value; + } + + private static void PropagateConstants(Dictionary> ssaUses, HashSet executableBlocks) + { + // TODO: Keep track of which uses are replaced and if there are no more uses, remove the assignment. + // But watch for functions that external read that variable and make sure to retain the assignment before then. + List constantVariables = ssaUses.Keys.Where(v => v.IsInvariant).ToList(); + constantVariables.AddRange(ssaUses.Keys.Where(v => v is PhiVariable p && p.PossibleValues.Keys.Where(b => !executableBlocks.Contains(b)).Skip(1).Any())); +#if DEBUG + constantVariables.Sort(Comparer.Create((a, b) => string.Compare(a.ToString(), b.ToString()))); +#endif + Queue queue = new Queue(constantVariables.Distinct()); + Dictionary replacements = new Dictionary(); + + while (queue.Count > 0) + { + SSAVariable variable = queue.Dequeue(); + if (queue.Any(v => + { + if (variable is PhiVariable phi && !replacements.ContainsKey(phi)) + return true; + if (ssaUses[v].Contains(variable.AssignedAt)) + return true; + return false; + })) + { + queue.Enqueue(variable); + continue; + } + + IRConstant replacement; + if (!(variable is PhiVariable)) + { + if (variable.AssignedAt.Value is IRConstant constant) + replacement = constant; + else + replacement = ConstantFolding.AttemptReduction(((IRTemp)variable.AssignedAt.Value).Parent) as IRConstant; + } + else + { + replacement = replacements[variable]; + } + + if (replacement != null) + { + foreach (IOperandInstructionBase instruction in ssaUses[variable]) + { + if (instruction is PhiVariable phi) + { + if (constantVariables.Contains(phi)) + replacements[phi] = replacement; + continue; + } + // TODO: Make this propagate True + if (instruction is IRUnaryOp unaryOp && + unaryOp.Operation is OpcodeExists) + continue; + instruction.MutateEachOperand(op => op.Equals(variable) ? replacement : op); + } + } + } + } + + private static void Propagation(BasicBlock block) + { + //ssaVariable.ValueType = assignment.Value.ValueType; + + //if (postCallSSAVariableLinks.TryGetValue(ssaVariable, out SSAVariable postCallLink)) + // MUSTFIX: This does not guarantee that this happens before ssaVariable is accessed! + // Particularly in the case of recursive or bouncing functions. + //postCallLink.ValueType = ssaVariable.ValueType; + } + } +} From 98e6fd771368395bff9cb16342dac30441cbe4d5 Mon Sep 17 00:00:00 2001 From: DBooots Date: Tue, 21 Apr 2026 00:11:19 -0400 Subject: [PATCH 066/120] Fix multiple issues with how phi values and their invariance state are propagated. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 7 + src/kOS.Safe/Compilation/IR/IRValue.cs | 12 +- .../Passes/SCCPWithTypePropagation.cs | 146 +++++++++++++----- 3 files changed, 126 insertions(+), 39 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 9e8b16d4f..b07e10c45 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -23,6 +23,13 @@ public class BasicBlock private readonly HashSet variablesWritten = new HashSet(); private readonly HashSet variablesRead = new HashSet(); + /// + /// Gets or sets a value indicating whether this block is executable (reachable). + /// + /// + /// true if this block is executable; otherwise, false. + /// + public bool IsExecutable { get; set; } /// /// Gets or sets the scope of this block. This describes the /// narrowest scope at the time of this block's execution. diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index d91e58ba1..eb9819fad 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -165,7 +165,17 @@ public override int GetHashCode() public class PhiVariable : SSAVariable, IMultipleOperandInstruction { public Dictionary PossibleValues { get; } = new Dictionary(); - public override bool IsInvariant { get => !PossibleValues.Skip(1).Any() && PossibleValues.Values.First().IsInvariant; set => throw new InvalidOperationException(); } + public override bool IsInvariant + { + get + { + IEnumerable> reachableValues = + PossibleValues.Where(kvp => kvp.Key.IsExecutable); + // Return true if there is exactly one reachable value and it is invariant. + return reachableValues.Any() && !reachableValues.Skip(1).Any() && reachableValues.First().Value.IsInvariant; + } + set => throw new InvalidOperationException(); + } IEnumerable IMultipleOperandInstruction.Operands => PossibleValues.Values; diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index aa68f8683..30d5ab3e3 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -26,59 +26,64 @@ public void ApplyPass() IRCodePart codePart = Optimizer.Code; Dictionary> ssaUses = - MapUsesAndPropagateTypes(codePart, out HashSet executableBlocks); + MapUsesAndPropagateTypes(codePart); if (Optimizer.OptimizationLevel > OptimizationLevel.None) - PropagateConstants(ssaUses, executableBlocks); + PropagateConstants(ssaUses); } - private static Dictionary> MapUsesAndPropagateTypes(IRCodePart codePart, out HashSet executableBlocks) + /// + /// Applies the sparse conditional side of SCCP, and propagates + /// type information to Phi variables as blocks become executable. + /// + /// + /// + /// The collection of blocks that are marked as executable. + /// + /// + /// A dictionary of instructions (or phi variables) that make use + /// of SSA variables (indirectly or directly). + /// + private static Dictionary> MapUsesAndPropagateTypes(IRCodePart codePart) { Dictionary> variableUses = new Dictionary>(); - executableBlocks = new HashSet(); HashSet visitedBlocks = new HashSet(); + Dictionary invariance = new Dictionary(); + // Apply the algorithm starting from each entry block. foreach (BasicBlock root in codePart.RootBlocks) { + // The queue of blocks that are executable Queue blockQueue = new Queue(); + // Queues the entry block blockQueue.Enqueue(root); + + // The queue of instructions that need updating Queue instructionQueue = new Queue(); while (blockQueue.Count > 0 || instructionQueue.Count > 0) { - // Mark a block executable - // Visit every expression in that block + // Visit every expression and phi in a block // Queue that block's successors - while (instructionQueue.Count > 0) - { - IOperandInstructionBase instruction = instructionQueue.Dequeue(); - if (VisitInstruction(instruction, blockQueue, executableBlocks)) - { - if (instruction is IRAssign assignment && - assignment.Target is SSAVariable ssaVariable) - foreach (IOperandInstructionBase use in GetOrCreate(variableUses, ssaVariable)) - instructionQueue.Enqueue(use); - else if (instruction is PhiVariable phi) - foreach (IOperandInstructionBase use in GetOrCreate(variableUses, phi)) - instructionQueue.Enqueue(use); - } - } - - if (blockQueue.Count > 0) + while (blockQueue.Count > 0) { BasicBlock block = blockQueue.Dequeue(); + // Disregard if this block has already been visited + // any changes will be captured in the sparse pass below. if (!visitedBlocks.Add(block)) continue; + // Process Phis first, as if they are instructions. foreach (PhiVariable phi in block.Phis) { foreach (SSAVariable variable in phi.PossibleValues.Values) GetOrCreate(variableUses, variable).Add(phi); - VisitInstruction(phi, blockQueue, executableBlocks); + VisitInstruction(phi, blockQueue, invariance); } + // Process instructions. foreach (IRInstruction instruction in block.Instructions) { foreach (IOperandInstructionBase inst in instruction.DepthFirst().Where(i => i is IOperandInstructionBase).Cast()) @@ -87,11 +92,17 @@ private static Dictionary> MapUses { if (op is SSAVariable ssaVariable) { + // Add this instruction to the list of uses for each operand. GetOrCreate(variableUses, ssaVariable).Add(inst); + + // Also add the base instruction, + // which is the more important reference + // since anything else is a temp result. if (instruction is IOperandInstructionBase opInst) variableUses[ssaVariable].Add(opInst); } }); + // Calls get to be special to address their external read needs. if (inst is IRCall call) { string functionIdentifier = block.Scope.GetFunctionNameFromVariable(call.Function); @@ -108,9 +119,14 @@ private static Dictionary> MapUses } } } - VisitInstruction(inst, blockQueue, executableBlocks); + // Reduce duplication (and the risk of + // inadvertently changing the return value). + // The else if the next if statement. + if (inst != instruction || !(instruction is IOperandInstructionBase)) + VisitInstruction(inst, blockQueue, invariance); } + if (instruction is IOperandInstructionBase operandInstruction) { operandInstruction.ForEachOperand(op => @@ -118,14 +134,37 @@ private static Dictionary> MapUses if (op is SSAVariable ssaVariable) GetOrCreate(variableUses, ssaVariable).Add(operandInstruction); }); - VisitInstruction(operandInstruction, blockQueue, executableBlocks); + if (VisitInstruction(operandInstruction, blockQueue, invariance) && + operandInstruction is IRAssign assignment && + assignment.Target is SSAVariable ssaTarget && + variableUses.TryGetValue(ssaTarget, out HashSet uses)) + foreach (IOperandInstructionBase use in uses) + instructionQueue.Enqueue(use); } } + // Multiple successors are covered in VisitInstruction() + // where it covers IRBranch. if (block.Successors.Count == 1) blockQueue.Enqueue(block.Successors.First()); - executableBlocks.Add(block); + block.IsExecutable = true; + } + + // Loop over any instructions (or phis) that need updating. + while (instructionQueue.Count > 0) + { + IOperandInstructionBase instruction = instructionQueue.Dequeue(); + if (VisitInstruction(instruction, blockQueue, invariance)) + { + if (instruction is IRAssign assignment && + assignment.Target is SSAVariable ssaVariable) + foreach (IOperandInstructionBase use in GetOrCreate(variableUses, ssaVariable)) + instructionQueue.Enqueue(use); + else if (instruction is PhiVariable phi) + foreach (IOperandInstructionBase use in GetOrCreate(variableUses, phi)) + instructionQueue.Enqueue(use); + } } } } @@ -133,25 +172,52 @@ private static Dictionary> MapUses return variableUses; } - private static bool VisitInstruction(IOperandInstructionBase instruction, Queue blockQueue, HashSet executableBlocks) + /// + /// Visits an instruction and determines if it has changes + /// that need to be propagated. + /// + /// true if the instruction needs to propagate changes. + private static bool VisitInstruction(IOperandInstructionBase instruction, + Queue blockQueue, + Dictionary invariance) { if (instruction is IRAssign assignment) { bool result = assignment.Target.ValueType != assignment.Value.ValueType; assignment.Target.ValueType = assignment.Value.ValueType; + if (assignment.Target is SSAVariable ssaVariable) + { + if (invariance.TryGetValue(ssaVariable, out bool storedInvariance)) + { + invariance[ssaVariable] &= ssaVariable.IsInvariant; + result |= storedInvariance != invariance[ssaVariable]; + } + else + { + invariance[ssaVariable] = ssaVariable.IsInvariant; + result = true; + } + } + return result; } else if (instruction is PhiVariable phi) { - List possibleValues = new List( - phi.PossibleValues.Where(kvp => executableBlocks.Contains(kvp.Key)).Select(kvp => kvp.Value)); + Type pastType = phi.ValueType; + phi.RefreshType(); - Type proposedType = possibleValues[0].ValueType; - foreach (SSAVariable variable in possibleValues.Skip(1)) - proposedType = PhiVariable.GetFirstCommonBaseType(proposedType, variable.ValueType); + bool result = pastType != phi.ValueType; - bool result = proposedType != phi.ValueType; - phi.ValueType = proposedType; + if (invariance.TryGetValue(phi, out bool storedInvariance)) + { + invariance[phi] &= storedInvariance; + result |= storedInvariance != invariance[phi]; + } + else + { + invariance[phi] = phi.IsInvariant; + result = true; + } return result; } else if (instruction is IRBranch branch) @@ -189,16 +255,20 @@ private static bool VisitInstruction(IOperandInstructionBase instruction, Queue< return value; } - private static void PropagateConstants(Dictionary> ssaUses, HashSet executableBlocks) + /// + /// Propagates any constant SSA variables to their uses. + /// + private static void PropagateConstants(Dictionary> ssaUses) { // TODO: Keep track of which uses are replaced and if there are no more uses, remove the assignment. // But watch for functions that external read that variable and make sure to retain the assignment before then. List constantVariables = ssaUses.Keys.Where(v => v.IsInvariant).ToList(); - constantVariables.AddRange(ssaUses.Keys.Where(v => v is PhiVariable p && p.PossibleValues.Keys.Where(b => !executableBlocks.Contains(b)).Skip(1).Any())); -#if DEBUG + + +#if DEBUG // Sort to make debugging variable iterations easier. constantVariables.Sort(Comparer.Create((a, b) => string.Compare(a.ToString(), b.ToString()))); #endif - Queue queue = new Queue(constantVariables.Distinct()); + Queue queue = new Queue(constantVariables); Dictionary replacements = new Dictionary(); while (queue.Count > 0) From fef06dacde18ef640a696a49e49b14045e744605 Mon Sep 17 00:00:00 2001 From: DBooots Date: Tue, 21 Apr 2026 00:19:21 -0400 Subject: [PATCH 067/120] Add convenience methods to get triggers or functions by their string identifier. --- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 22 +++++++++++++++++++ .../Compilation/IR/SingleStaticAssignment.cs | 8 +++---- .../Passes/SCCPWithTypePropagation.cs | 13 +++++------ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index d133585aa..3523d691c 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -77,6 +77,28 @@ public IRCodePart(CodePart codePart, List userFunctions, List + /// Gets a function by string reference. + /// + /// The function identifier string. + public IRFunction GetFunction(string identifier) + { + if (string.IsNullOrEmpty(identifier)) + return null; + return Functions.FirstOrDefault(f => string.Equals(f.Identifier, identifier, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Gets a trigger by string reference. + /// + /// The trigger identifier string. + public IRTrigger GetTrigger(string identifier) + { + if (string.IsNullOrEmpty(identifier)) + return null; + return Triggers.FirstOrDefault(t => string.Equals(t.Identifier, identifier, StringComparison.OrdinalIgnoreCase)); + } + /// /// Emits the code into Opcode representation, and back into /// its source objects. diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index 36bff5153..74a9962f6 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -48,9 +48,7 @@ private static void AnalyzeBlock(BasicBlock block, IRCodePart codePart) foreach (IRCall call in instruction.DepthFirst().Where(inst => inst is IRCall).Cast()) { string functionIdentifier = block.Scope.GetFunctionNameFromVariable(call.Function); - if (functionIdentifier == null) - continue; - IRCodePart.IRFunction function = codePart.Functions.FirstOrDefault(f => string.Equals(f.Identifier, functionIdentifier, StringComparison.OrdinalIgnoreCase)); + IRCodePart.IRFunction function = codePart.GetFunction(functionIdentifier); if (function != null) { HashSet ssaVariables = new HashSet(); @@ -76,7 +74,7 @@ private static void AnalyzeBlock(BasicBlock block, IRCodePart codePart) { string pointer = ((string)lockOrFunctionPointer.Value).Split('-').First(); // Re-scope the stored variables from Global to the current scope. - IRCodePart.IRFunction function = codePart.Functions.FirstOrDefault(f => string.Equals(f.Identifier, pointer, StringComparison.OrdinalIgnoreCase)); + IRCodePart.IRFunction function = codePart.GetFunction(pointer); if (function != null) IRCodePart.SetDefiningScope(function, block.Scope); } @@ -90,7 +88,7 @@ private static void AnalyzeBlock(BasicBlock block, IRCodePart codePart) { string pointer = (string)((IRConstant)unaryConsumer.Operand).Value; // Re-scope the stored variables from Global to the current scope. - IRCodePart.IRTrigger trigger = codePart.Triggers.FirstOrDefault(t => string.Equals(t.Identifier, pointer, StringComparison.OrdinalIgnoreCase)); + IRCodePart.IRTrigger trigger = codePart.GetTrigger(pointer); if (trigger != null) IRCodePart.SetDefiningScope(trigger, block.Scope); block.TriggerPropagationBlacklist.UnionWith(trigger.ExternalWrites); diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index 30d5ab3e3..20c02daf5 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -106,16 +106,13 @@ private static Dictionary> MapUses if (inst is IRCall call) { string functionIdentifier = block.Scope.GetFunctionNameFromVariable(call.Function); - if (functionIdentifier != null) + IRCodePart.IRFunction function = codePart.GetFunction(functionIdentifier); + if (function != null) { - IRCodePart.IRFunction function = codePart.Functions.FirstOrDefault(f => string.Equals(f.Identifier, functionIdentifier, StringComparison.OrdinalIgnoreCase)); - if (function != null) + foreach (IRVariable externalVar in function.ExternalReads.Union(function.ExternalWrites)) { - foreach (IRVariable externalVar in function.ExternalReads.Union(function.ExternalWrites)) - { - if (externalVar is SSAVariable ssaVariable) - GetOrCreate(variableUses, ssaVariable).Add(call); - } + if (externalVar is SSAVariable ssaVariable) + GetOrCreate(variableUses, ssaVariable).Add(call); } } } From 189b71404a8b4c568cf88d10e951661e0d7f3207 Mon Sep 17 00:00:00 2001 From: DBooots Date: Tue, 21 Apr 2026 19:25:56 -0400 Subject: [PATCH 068/120] Add an Attribute to flag if an arithmetic method does not follow standard commutativity rules. Use this to ensure that arithmetic operations are optimized appropriately. --- src/kOS.Safe/Compilation/Calculator.cs | 5 ++ src/kOS.Safe/Compilation/CalculatorString.cs | 6 ++ .../Compilation/CalculatorStructure.cs | 85 +++++++++++++++++-- .../Compilation/CommutativeAttribute.cs | 27 ++++++ src/kOS.Safe/Compilation/IR/IRInstruction.cs | 55 ++++++++---- .../Passes/PeepholeOptimizations.cs | 67 +++++++++++---- src/kOS.Safe/Encapsulation/StringValue.cs | 3 + src/kOS/Suffixed/Direction.cs | 3 + src/kOS/Suffixed/TimeSpan.cs | 3 + src/kOS/Suffixed/TimeStamp.cs | 6 ++ 10 files changed, 222 insertions(+), 38 deletions(-) create mode 100644 src/kOS.Safe/Compilation/CommutativeAttribute.cs diff --git a/src/kOS.Safe/Compilation/Calculator.cs b/src/kOS.Safe/Compilation/Calculator.cs index 4362fc2c9..64d76ab70 100644 --- a/src/kOS.Safe/Compilation/Calculator.cs +++ b/src/kOS.Safe/Compilation/Calculator.cs @@ -29,6 +29,11 @@ public abstract class Calculator public virtual Type GetNotEqualResultType(Type leftType, Type rightType) => typeof(BooleanValue); public virtual Type GetEqualResultType(Type leftType, Type rightType) => typeof(BooleanValue); + public virtual bool IsAdditionCommutative(Type leftType, Type rightType) => true; + public virtual bool IsSubtractionCommutative(Type leftType, Type rightType) => true; + public virtual bool IsMultiplicationCommmutative(Type leftType, Type rightType) => true; + public virtual bool IsDivisionCommutative(Type leftType, Type rightType) => false; + private static readonly CalculatorScalar calculatorScalar = new CalculatorScalar(); private static readonly CalculatorString calculatorString = new CalculatorString(); private static readonly CalculatorBool calculatorBool = new CalculatorBool(); diff --git a/src/kOS.Safe/Compilation/CalculatorString.cs b/src/kOS.Safe/Compilation/CalculatorString.cs index 10dc459c5..8387569af 100644 --- a/src/kOS.Safe/Compilation/CalculatorString.cs +++ b/src/kOS.Safe/Compilation/CalculatorString.cs @@ -12,6 +12,8 @@ public override object Add(OperandPair pair) } public override Type GetAddResultType(Type leftType, Type rightType) => typeof(StringValue); + public override bool IsAdditionCommutative(Type leftType, Type rightType) + => false; public override object Subtract(OperandPair pair) { @@ -21,6 +23,8 @@ public override Type GetSubtractResultType(Type leftType, Type rightType) { throw new KOSBinaryOperandTypeException(leftType, rightType, "subtract", "from"); } + public override bool IsSubtractionCommutative(Type leftType, Type rightType) + => false; public override object Multiply(OperandPair pair) { @@ -30,6 +34,8 @@ public override Type GetMultiplyResultType(Type leftType, Type rightType) { throw new KOSBinaryOperandTypeException(leftType, rightType, "multiply", "by"); } + public override bool IsMultiplicationCommmutative(Type leftType, Type rightType) + => false; public override object Divide(OperandPair pair) { diff --git a/src/kOS.Safe/Compilation/CalculatorStructure.cs b/src/kOS.Safe/Compilation/CalculatorStructure.cs index aa932faec..60dfdb13d 100644 --- a/src/kOS.Safe/Compilation/CalculatorStructure.cs +++ b/src/kOS.Safe/Compilation/CalculatorStructure.cs @@ -30,6 +30,8 @@ public override object Add(OperandPair pair) } public override Type GetAddResultType(Type leftType, Type rightType) => GetTypeForOperation(leftType, rightType, "Add", "op_Addition", "+"); + public override bool IsAdditionCommutative(Type leftType, Type rightType) + => GetCommutativityForOperation(leftType, rightType, "Add", "op_Addition", "+"); public override object Subtract(OperandPair pair) { @@ -51,6 +53,8 @@ public override object Subtract(OperandPair pair) } public override Type GetSubtractResultType(Type leftType, Type rightType) => GetTypeForOperation(leftType, rightType, "Subtract", "op_Subtraction", "-"); + public override bool IsSubtractionCommutative(Type leftType, Type rightType) + => GetCommutativityForOperation(leftType, rightType, "Subtract", "op_Subtraction", "-"); public override object Multiply(OperandPair pair) { @@ -72,6 +76,8 @@ public override object Multiply(OperandPair pair) } public override Type GetMultiplyResultType(Type leftType, Type rightType) => GetTypeForOperation(leftType, rightType, "Multiply", "op_Multiply", "*"); + public override bool IsMultiplicationCommmutative(Type leftType, Type rightType) + => GetCommutativityForOperation(leftType, rightType, "Multiply", "op_Multiply", "*"); public override object Divide(OperandPair pair) { @@ -93,6 +99,8 @@ public override object Divide(OperandPair pair) } public override Type GetDivideResultType(Type leftType, Type rightType) => GetTypeForOperation(leftType, rightType, "Divide", "op_Division", "/"); + public override bool IsDivisionCommutative(Type leftType, Type rightType) + => GetCommutativityForOperation(leftType, rightType, "Divide", "op_Division", "/"); public override object Power(OperandPair pair) { @@ -219,7 +227,8 @@ public override object NotEqual(OperandPair pair) } public override Type GetNotEqualResultType(Type leftType, Type rightType) { - CheckTypesForNull(leftType, rightType, "NotEqual"); + if (CheckTypesForNull(leftType, rightType)) + return null; if (TryTypingExplicit(leftType, rightType, "op_Inequality", out Type result)) return result; @@ -249,7 +258,8 @@ public override object Equal(OperandPair pair) } public override Type GetEqualResultType(Type leftType, Type rightType) { - CheckTypesForNull(leftType, rightType, "Equal"); + if (CheckTypesForNull(leftType, rightType)) + return null; if (TryTypingExplicit(leftType, rightType, "op_Equality", out Type result)) return result; @@ -261,7 +271,8 @@ public override Type GetEqualResultType(Type leftType, Type rightType) private Type GetTypeForOperation(Type leftType, Type rightType, string opName, string methodName, string opAbbreviation) { - CheckTypesForNull(leftType, rightType, opName); + if (CheckTypesForNull(leftType, rightType)) + return null; if (TryTypingExplicit(leftType, rightType, methodName, out Type result)) return result; @@ -271,6 +282,19 @@ private Type GetTypeForOperation(Type leftType, Type rightType, string opName, s return typeof(Structure); } + private bool GetCommutativityForOperation(Type leftType, Type rightType, string opName, string methodName, string opAbbreviation) + { + if (CheckTypesForNull(leftType, rightType)) + return false; + if (CheckCommutativityExplicit(leftType, rightType, methodName, out bool result)) + return result; + + if (TryTypingImplicit(leftType, rightType, out Type newLeftType, out Type newRightType)) + return GetCommutativityForOperation(newLeftType, newRightType, opName, methodName, opAbbreviation); + + return false; + } + private static string GetMessage(string op, OperandPair pair) { string t1 = pair.Left == null ? "" : KOSNomenclature.GetKOSName(pair.Left.GetType()); @@ -344,6 +368,56 @@ private bool TryTypingExplicit(Type left, Type right, string methodName, out Typ return false; } + private bool CheckCommutativityExplicit(Type left, Type right, string methodName, out bool result) + { + MethodInfo method1 = left.GetMethod(methodName, FLAGS, null, new[] { left, right }, null); + CommutativeAttribute commutativeAttribute = method1?.GetCustomAttribute(); + if (method1 != null) + { + if (commutativeAttribute != null) + result = commutativeAttribute.IsCommutative; + else + result = DefaultCommutativity(methodName, right); + return true; + } + MethodInfo method2 = right.GetMethod(methodName, FLAGS, null, new[] { left, right }, null); + commutativeAttribute = method2?.GetCustomAttribute(); + if (method2 != null) + { + if (commutativeAttribute != null) + result = commutativeAttribute.IsCommutative; + else + result = DefaultCommutativity(methodName, right); + return true; + } + result = false; + return false; + } + private bool DefaultCommutativity(string methodName, Type _) + { + switch (methodName) + { + case "op_Addition": + case "op_Multiply": + return true; + case "op_GreaterThan": + case "op_LessThan": + case "op_GreaterThanEqual": + case "op_LessThanEqual": + return true; + case "op_Equality": + case "op_Inequality": + return true; + case "op_Subtraction": + return true; + //return right.GetMethod("op_UnaryNegation", FLAGS, null, new[] { right }, null) != null; + case "op_Division": + case "op_ExclusiveOr": + default: + return false; + } + } + private void CheckPairForNull(OperandPair pair, string opName) { if (pair.Left == null || pair.Right == null) @@ -351,10 +425,9 @@ private void CheckPairForNull(OperandPair pair, string opName) throw new InvalidOperationException(GetMessage(opName, pair)); } } - private void CheckTypesForNull(Type left, Type right, string opName) + private bool CheckTypesForNull(Type left, Type right) { - if (left == null || right == null) - throw new InvalidOperationException(GetMessage(opName, left, right)); + return left == null || right == null; } private bool TryCoerceImplicit(OperandPair pair, out OperandPair resultPair) diff --git a/src/kOS.Safe/Compilation/CommutativeAttribute.cs b/src/kOS.Safe/Compilation/CommutativeAttribute.cs new file mode 100644 index 000000000..dbc706784 --- /dev/null +++ b/src/kOS.Safe/Compilation/CommutativeAttribute.cs @@ -0,0 +1,27 @@ +using System; + +namespace kOS.Safe.Compilation +{ + /// + /// This attribute is used to mark math operators that do not + /// follow standard commutativity rules. + /// + /// + /// Operators where the operands are different Types are + /// assumed to be commutative in the absence of this + /// attribute, if the operation would normally be commutative. + /// Subtraction (a-b) is assumed to normally be commutative with + /// negation and addition (-b+a), as well as interchangeably with + /// addition. + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class CommutativeAttribute : Attribute + { + public bool IsCommutative { get; } + public CommutativeAttribute(bool isCommutative) + { + IsCommutative = isCommutative; + } + } +} diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index f9787e1cb..302998794 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -127,18 +127,6 @@ public override int GetHashCode() } public class IRBinaryOp : MultipleOperandInstruction, IResultingInstruction { - private static readonly Type[] commutativeTypes = - { - typeof(OpcodeCompareEqual), - typeof(OpcodeCompareNE), - typeof(OpcodeCompareGT), - typeof(OpcodeCompareLT), - typeof(OpcodeCompareGTE), - typeof(OpcodeCompareLTE), - typeof(OpcodeMathAdd), - typeof(OpcodeMathMultiply) - }; - public override bool IsInvariant => Left.IsInvariant && Right.IsInvariant; public IRValue Result { get; set; } public BinaryOpcode Operation { get; set; } @@ -194,7 +182,41 @@ protected override IRValue this[int index] throw new ArgumentOutOfRangeException(); } } - public bool IsCommutative => commutativeTypes.Contains(Operation.GetType()); + public bool IsCommutative + { + get + { + Calculator calculator = Calculator.GetCalculator(Left.ValueType, Right.ValueType); + switch (Operation) + { + case OpcodeMathAdd _: + return calculator.IsAdditionCommutative(Left.ValueType, Right.ValueType); + case OpcodeMathSubtract _: + return calculator.IsSubtractionCommutative(Left.ValueType, Right.ValueType); + case OpcodeMathMultiply _: + return calculator.IsMultiplicationCommmutative(Left.ValueType, Right.ValueType); + case OpcodeMathDivide _: + return calculator.IsDivisionCommutative(Left.ValueType, Right.ValueType); + case OpcodeMathPower _: + return false; + case OpcodeCompareEqual _: + case OpcodeCompareNE _: + return true; + case OpcodeCompareGT _: + case OpcodeCompareLT _: + case OpcodeCompareGTE _: + case OpcodeCompareLTE _: + return true; + default: +#pragma warning disable CS0162 // Unreachable code detected +#if DEBUG + throw new NotImplementedException(); +#endif + return false; +#pragma warning restore CS0162 // Unreachable code detected + } + } + } public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) : base(operation) { @@ -203,11 +225,13 @@ public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue r Left = left; Right = right; } - public void SwapOperands() + public bool SwapOperands() { if (!IsCommutative) - return; + return false; //throw new System.InvalidOperationException($"{this} is not commutative."); + if (Operation is OpcodeMathSubtract) + return false; (Right, Left) = (Left, Right); switch (Operation) { @@ -224,6 +248,7 @@ public void SwapOperands() Operation = new OpcodeCompareGT(); break; } + return true; } internal override IEnumerable EmitOpcode() { diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs index ca6b8f2d7..c1bc42cab 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs @@ -66,7 +66,10 @@ tempL.Parent is IRBinaryOp opL && tempR != null && tempR.Parent is IRBinaryOp opR && opL.Operation is OpcodeMathMultiply && - opR.Operation is OpcodeMathMultiply) + opR.Operation is OpcodeMathMultiply && + binaryOp.IsCommutative && + opL.IsCommutative && + opR.IsCommutative) { if (opR.Left == opL.Left) { @@ -75,20 +78,23 @@ opL.Operation is OpcodeMathMultiply && } if (opR.Left == opL.Right) { - opL.SwapOperands(); + if (!opL.SwapOperands()) + return; DistributeMultiplication(binaryOp); return; } if (opR.Right == opL.Left) { - opR.SwapOperands(); + if (!opR.SwapOperands()) + return; DistributeMultiplication(binaryOp); return; } if (opR.Right == opL.Right) { - opL.SwapOperands(); - opR.SwapOperands(); + if (!opL.SwapOperands() || + !opR.SwapOperands()) + return; DistributeMultiplication(binaryOp); return; } @@ -105,9 +111,9 @@ tempR.Parent is IRUnaryOp opR && } if (tempL != null && tempL.Parent is IRUnaryOp opL && - opL.Operation is OpcodeMathNegate) + opL.Operation is OpcodeMathNegate && + binaryOp.SwapOperands()) { - binaryOp.SwapOperands(); DistributeNegationB(binaryOp); return; } @@ -180,9 +186,9 @@ opL.Operation is OpcodeMathPower && if (tempR != null && tempR.Parent is IRBinaryOp opR && opR.Operation is OpcodeMathPower && - opR.Left == binaryOp.Left) + opR.Left == binaryOp.Left && + binaryOp.SwapOperands()) { - binaryOp.SwapOperands(); IncreasePower(binaryOp, 1); return; } @@ -204,9 +210,9 @@ opR.Operation is OpcodeMathMultiply && tempL.Parent is IRBinaryOp opL && opL.Operation is OpcodeMathMultiply && opL.Left == opL.Right && - opL.Left == binaryOp.Right) + opL.Left == binaryOp.Right && + binaryOp.SwapOperands()) { - binaryOp.SwapOperands(); CreatePower(binaryOp); return; } @@ -379,29 +385,55 @@ private static void DistributeMultiplication(IRBinaryOp instruction) private static void DistributeNegationA(IRBinaryOp instruction) { // -A+B = B-A - IRTemp tempL = (IRTemp)instruction.Left; + if (!(instruction.Left is IRTemp tempL)) + return; + if (!instruction.IsCommutative) + return; + BinaryOpcode originalOperation = instruction.Operation; + instruction.Operation = new OpcodeMathSubtract(); + if (!instruction.IsCommutative) + { + instruction.Operation = originalOperation; + return; + } IRUnaryOp opL = (IRUnaryOp)tempL.Parent; instruction.Left = opL.Operand; instruction.SwapOperands(); - instruction.Operation = new OpcodeMathSubtract(); } private static void DistributeNegationB(IRBinaryOp instruction) { // A+-B = A-B - IRTemp tempR = (IRTemp)instruction.Right; + if (!(instruction.Right is IRTemp tempR)) + return; + if (!instruction.IsCommutative) + return; + BinaryOpcode originalOperation = instruction.Operation; + instruction.Operation = new OpcodeMathSubtract(); + if (!instruction.IsCommutative) + { + instruction.Operation = originalOperation; + return; + } IRUnaryOp opR = (IRUnaryOp)tempR.Parent; instruction.Right = opR.Operand; - instruction.Operation = new OpcodeMathSubtract(); } private static void ReplaceNegateSubtract(IRBinaryOp instruction) { // A--B=A+B + if (!instruction.IsCommutative) + return; + BinaryOpcode originalOperation = instruction.Operation; + instruction.Operation = new OpcodeMathAdd(); + if (!instruction.IsCommutative) + { + instruction.Operation = originalOperation; + return; + } IRTemp tempR = (IRTemp)instruction.Right; IRUnaryOp opR = (IRUnaryOp)tempR.Parent; instruction.Right = opR.Operand; - instruction.Operation = new OpcodeMathAdd(); } private static void IncreasePower(IRBinaryOp instruction, int powerIncrease) @@ -446,8 +478,9 @@ private static void IncreasePower(IRBinaryOp instruction, int powerIncrease) private static void CreatePower(IRBinaryOp instruction) { // X*(X*X) = X^3 + if (!(instruction.Right is IRTemp tempR)) + return; instruction.Operation = new OpcodeMathPower(); - IRTemp tempR = (IRTemp)instruction.Right; instruction.Right = new IRConstant(new Encapsulation.ScalarIntValue(3), tempR.Parent); } diff --git a/src/kOS.Safe/Encapsulation/StringValue.cs b/src/kOS.Safe/Encapsulation/StringValue.cs index a7a509499..b02097aab 100644 --- a/src/kOS.Safe/Encapsulation/StringValue.cs +++ b/src/kOS.Safe/Encapsulation/StringValue.cs @@ -355,16 +355,19 @@ public static implicit operator StringValue(string value) return new StringValue(value); } + [kOS.Safe.Compilation.Commutative(false)] // String concatenation is not commutative. public static StringValue operator +(StringValue val1, StringValue val2) { return new StringValue(val1.ToString() + val2.ToString()); } + [kOS.Safe.Compilation.Commutative(false)] // String concatenation is not commutative. public static StringValue operator +(StringValue val1, Structure val2) { return new StringValue(val1.ToString() + val2.ToString()); } + [kOS.Safe.Compilation.Commutative(false)] // String concatenation is not commutative. public static StringValue operator +(Structure val1, StringValue val2) { return new StringValue(val1.ToString() + val2.ToString()); diff --git a/src/kOS/Suffixed/Direction.cs b/src/kOS/Suffixed/Direction.cs index 013edcb82..b00b519f7 100644 --- a/src/kOS/Suffixed/Direction.cs +++ b/src/kOS/Suffixed/Direction.cs @@ -158,6 +158,7 @@ private void DirectionInitializeSuffixes() AddSuffix("INVERSE", new Suffix(() => new Direction(Quaternion.Inverse(rotation)), "Returns the inverse of this direction - meaning the rotation that would go FROM this direction TO the universe's raw orientation.")); } + [kOS.Safe.Compilation.Commutative(false)] public static Direction operator *(Direction a, Direction b) { return new Direction(a.Rotation * b.Rotation); @@ -183,11 +184,13 @@ private void DirectionInitializeSuffixes() return new Vector(a.Rotation * (Vector3d)b); } + [kOS.Safe.Compilation.Commutative(false)] public static Direction operator +(Direction a, Direction b) { return new Direction(a.Euler + b.Euler, true); } + [kOS.Safe.Compilation.Commutative(false)] public static Direction operator -(Direction a, Direction b) { return new Direction(a.Euler - b.Euler, true); diff --git a/src/kOS/Suffixed/TimeSpan.cs b/src/kOS/Suffixed/TimeSpan.cs index 8b15e3dd5..1aca98c81 100644 --- a/src/kOS/Suffixed/TimeSpan.cs +++ b/src/kOS/Suffixed/TimeSpan.cs @@ -82,6 +82,7 @@ protected override void InitializeSuffixes() // Binary arithmetic operators where both operands are TimeSpans: // public static TimeSpan operator +(TimeSpan a, TimeSpan b) { return new TimeSpan(a.ToUnixStyleTime() + b.ToUnixStyleTime()); } + [Commutative(false)] // TODO: Remove this if -TimeSpan is implemented. public static TimeSpan operator -(TimeSpan a, TimeSpan b) { return new TimeSpan(a.ToUnixStyleTime() - b.ToUnixStyleTime()); } public static TimeSpan operator *(TimeSpan a, TimeSpan b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "multiply", "by"); } public static TimeSpan operator /(TimeSpan a, TimeSpan b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "divide", "by"); } @@ -95,6 +96,7 @@ protected override void InitializeSuffixes() public static TimeSpan operator *(TimeSpan a, double b) { return new TimeSpan(a.ToUnixStyleTime() * b); } public static TimeSpan operator /(TimeSpan a, double b) { return new TimeSpan(a.ToUnixStyleTime() / b); } public static TimeSpan operator +(double a, TimeSpan b) { return new TimeSpan(a + b.ToUnixStyleTime()); } + [Commutative(false)] // TODO: Remove this if -TimeSpan is implemented. public static TimeSpan operator -(double a, TimeSpan b) { return new TimeSpan(a - b.ToUnixStyleTime()); } public static TimeSpan operator *(double a, TimeSpan b) { return new TimeSpan(a * b.ToUnixStyleTime()); } public static TimeSpan operator /(double a, TimeSpan b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "divide", "by"); } @@ -103,6 +105,7 @@ protected override void InitializeSuffixes() public static TimeSpan operator *(TimeSpan a, ScalarValue b) { return new TimeSpan(a.ToUnixStyleTime() * b); } public static TimeSpan operator /(TimeSpan a, ScalarValue b) { return new TimeSpan(a.ToUnixStyleTime() / b); } public static TimeSpan operator +(ScalarValue b, TimeSpan a) { return new TimeSpan(b + a.ToUnixStyleTime()); } + [Commutative(false)] // TODO: Remove this if -TimeSpan is implemented. public static TimeSpan operator -(ScalarValue b, TimeSpan a) { return new TimeSpan(b - a.ToUnixStyleTime()); } public static TimeSpan operator *(ScalarValue b, TimeSpan a) { return new TimeSpan(b * a.ToUnixStyleTime()); } public static TimeSpan operator /(ScalarValue b, TimeSpan a) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "divide", "by"); } diff --git a/src/kOS/Suffixed/TimeStamp.cs b/src/kOS/Suffixed/TimeStamp.cs index a6f9f5629..adafb08df 100644 --- a/src/kOS/Suffixed/TimeStamp.cs +++ b/src/kOS/Suffixed/TimeStamp.cs @@ -79,6 +79,8 @@ protected override void InitializeSuffixes() // Binary arithmetic operators in which both operands are TimeStamp: // public static TimeStamp operator +(TimeStamp a, TimeStamp b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "add", "to"); } + + [Commutative(false)] // TimeStamp does not implement negation. public static TimeSpan operator -(TimeStamp a, TimeStamp b) { return new TimeSpan(a.ToUnixStyleTime() - b.ToUnixStyleTime()); } public static TimeStamp operator *(TimeStamp a, TimeStamp b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "multiply", "by"); } public static TimeStamp operator /(TimeStamp a, TimeStamp b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "divide", "by"); } @@ -92,6 +94,7 @@ protected override void InitializeSuffixes() public static TimeStamp operator *(TimeStamp a, double b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "multiply", "by"); } public static TimeStamp operator /(TimeStamp a, double b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "divide", "by"); } public static TimeStamp operator +(double a, TimeStamp b) { return new TimeStamp(a + b.ToUnixStyleTime()); } + [Commutative(false)] // TimeStamp does not implement negation. public static TimeStamp operator -(double a, TimeStamp b) { return new TimeStamp(a - b.ToUnixStyleTime()); } public static TimeStamp operator *(double a, TimeStamp b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "multiply", "by"); } public static TimeStamp operator /(double a, TimeStamp b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "divide", "by"); } @@ -100,6 +103,7 @@ protected override void InitializeSuffixes() public static TimeStamp operator *(TimeStamp a, ScalarValue b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "multiply", "by"); } public static TimeStamp operator /(TimeStamp a, ScalarValue b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "divide", "by"); ; } public static TimeStamp operator +(ScalarValue a, TimeStamp b) { return new TimeStamp(a + b.ToUnixStyleTime()); } + [Commutative(false)] // TimeStamp does not implement negation. public static TimeStamp operator -(ScalarValue a, TimeStamp b) { return new TimeStamp(a - b.ToUnixStyleTime()); } public static TimeStamp operator *(ScalarValue a, TimeStamp b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "divide", "by"); } public static TimeStamp operator /(ScalarValue a, TimeStamp b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "divide", "by"); } @@ -136,10 +140,12 @@ protected override void InitializeSuffixes() // place they've all been defined here: // public static TimeStamp operator +(TimeStamp a, TimeSpan b) { return new TimeStamp(a.ToUnixStyleTime() + b.ToUnixStyleTime()); } + [Commutative(false)] // TODO: Remove this if -TimeSpan is implemented. public static TimeStamp operator -(TimeStamp a, TimeSpan b) { return new TimeStamp(a.ToUnixStyleTime() - b.ToUnixStyleTime()); } public static TimeStamp operator *(TimeStamp a, TimeSpan b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "multiply", "by"); } public static TimeStamp operator /(TimeStamp a, TimeSpan b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "divide", "by"); } public static TimeStamp operator +(TimeSpan a, TimeStamp b) { return new TimeStamp(a.ToUnixStyleTime() + b.ToUnixStyleTime()); } + [Commutative(false)] // TimeStamp does not implement negation. public static TimeStamp operator -(TimeSpan a, TimeStamp b) { return new TimeStamp(a.ToUnixStyleTime() - b.ToUnixStyleTime()); } public static TimeStamp operator *(TimeSpan a, TimeStamp b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "multiply", "by"); } public static TimeStamp operator /(TimeSpan a, TimeStamp b) { throw new KOSBinaryOperandTypeException(new OperandPair(a, b), "divide", "by"); } From e90e35c28914696924a5d901f957fd62bd88ff31 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 6 May 2026 22:32:17 -0400 Subject: [PATCH 069/120] Rewrite how operands and variables are implemented so that SSA can work with dynamically unset variables. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 73 +- .../Compilation/IR/IInterimOperand.cs | 29 + .../Compilation/IR/IOperandInstructionBase.cs | 8 +- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 189 +++-- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 251 ++++--- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 474 +++++++------ src/kOS.Safe/Compilation/IR/IRScope.cs | 86 +-- src/kOS.Safe/Compilation/IR/IRValue.cs | 358 ---------- .../Compilation/IR/IResultingInstruction.cs | 25 +- .../Compilation/IR/InterimConstantValue.cs | 83 +++ .../Compilation/IR/InterimVariables.cs | 493 +++++++++++++ .../Compilation/IR/SingleStaticAssignment.cs | 658 +++++++++++++----- src/kOS.Safe/Compilation/IR/TypeInferencer.cs | 6 +- .../Optimization/OptimizationTools.cs | 115 +-- .../Optimization/Passes/ConstantFolding.cs | 250 ++----- .../Passes/PeepholeOptimizations.cs | 206 +++--- .../Passes/SCCPWithTypePropagation.cs | 162 ++--- .../Optimization/Passes/SuffixReplacement.cs | 131 +--- 18 files changed, 1912 insertions(+), 1685 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/IInterimOperand.cs delete mode 100644 src/kOS.Safe/Compilation/IR/IRValue.cs create mode 100644 src/kOS.Safe/Compilation/IR/InterimConstantValue.cs create mode 100644 src/kOS.Safe/Compilation/IR/InterimVariables.cs diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index b07e10c45..78a6c8218 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -17,12 +17,11 @@ public class BasicBlock private BasicBlock dominator; private readonly HashSet dominates = new HashSet(); private readonly List parameters = new List(); - private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. + private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. private readonly string nonSequentialLabel = null; private IRScope scope; - private readonly HashSet variablesWritten = new HashSet(); - private readonly HashSet variablesRead = new HashSet(); + public IRCodePart CodePart { get; } /// /// Gets or sets a value indicating whether this block is executable (reachable). /// @@ -81,18 +80,6 @@ public IRScope Scope /// public IReadOnlyCollection Predecessors => predecessors; /// - /// Gets the collection of variables that are written in this - /// block in SSA form. This list only returns the last SSA - /// instance for a given variable name. - /// - public HashSet VariablesWritten => variablesWritten; - /// - /// Gets the collection of variables that are read in this block. - /// This is not in SSA form and includes global and bound - /// variables. - /// - public IReadOnlyCollection VariablesRead => variablesRead; - /// /// Gets the collection of phi functions that define variables /// that may have one of several different values upon entering /// this block. @@ -100,7 +87,8 @@ public IRScope Scope /// /// This data is populated during . /// - public HashSet Phis { get; } = new HashSet(); + public Dictionary<(string Name, IRScope Scope), PhiNode> Phis { get; } = + new Dictionary<(string Name, IRScope Scope), PhiNode>(); /// /// Gets the set of incoming SSA variables that this block /// receives, including the results of any phi functions. @@ -108,7 +96,7 @@ public IRScope Scope /// /// This data is populated during . /// - public HashSet IncomingVariableDefinitions { get; internal set; } + public Dictionary<(string, IRScope), SSADefinition> IncomingVariableDefinitions { get; internal set; } /// /// Gets the set of variables that are blacklisted against /// caching or propagation due to their presence in active @@ -117,7 +105,7 @@ public IRScope Scope /// /// This data is populated during . /// - public HashSet TriggerPropagationBlacklist { get; } = new HashSet(); + public HashSet<(string, IRScope)> TriggerPropagationBlacklist { get; } = new HashSet<(string, IRScope)>(); /// /// Gets the instruction label with which to start the block. /// The special prefix "@BB#" will be overwritten during linking. @@ -168,8 +156,9 @@ private set /// The starting index in the original sequence of s. /// The ending index in the original sequence of s. /// A non sequential label, if present. - public BasicBlock(int startIndex, int endIndex, string nonSequentialLabel = null) + public BasicBlock(IRCodePart codePart, int startIndex, int endIndex, string nonSequentialLabel = null) { + CodePart = codePart; StartIndex = startIndex; EndIndex = endIndex; ID = nextID++; @@ -302,42 +291,6 @@ public void AddParameter(IRParameter parameter) { parameters.Add(parameter); } - - public void StoreLocalVariable(SSAVariable variable) - { - SingleStaticAssignment.OverwriteVariable(variablesWritten, variable); - Scope.StoreLocalVariable(variable.Parent); - } - public void StoreGlobalVariable(SSAVariable variable) - { - SingleStaticAssignment.OverwriteVariable(variablesWritten, variable); - Scope.StoreGlobalVariable(variable.Parent); - } - public void StoreVariable(SSAVariable variable) - { - SingleStaticAssignment.OverwriteVariable(variablesWritten, variable); - Scope.StoreVariable(variable.Parent); - } - public bool TryStoreVariable(SSAVariable variable) - { - bool result = Scope.TryStoreVariable(variable.Parent); - if (result) - SingleStaticAssignment.OverwriteVariable(variablesWritten, variable); - return result; - } - - public IRVariableBase PushVariable(string name, Opcode opcode) - { - IRVariableBase result = Scope.GetVariableNamed(name); - if (result == null) - { - IRScope globalScope = Scope.GetGlobalScope(); - result = new IRVariable(name, globalScope, opcode); - Scope.StoreGlobalVariable(result); - } - variablesRead.Add(result); - return result; - } /// /// Alias for /// using this block's . @@ -357,7 +310,7 @@ public IRScope GetScopeForVariableNamed(string name) /// /// Use extreme caution when manipulating the stack state. /// - public void SetStackState(Stack stack) + public void SetStackState(Stack stack) { while (stack.Count > 0) exitStackState.Push(stack.Pop()); @@ -380,7 +333,7 @@ public IEnumerable EmitOpCodes() bool first = true; foreach (IRInstruction instruction in Instructions.Take(Instructions.Count - 1)) { - foreach (Opcode opcode in instruction.EmitOpcode()) + foreach (Opcode opcode in instruction.EmitOpcodes()) { if (first) { @@ -390,9 +343,9 @@ public IEnumerable EmitOpCodes() yield return opcode; } } - foreach (IRValue stackValue in exitStackState) + foreach (IInterimOperand stackValue in exitStackState) { - foreach (Opcode opcode in stackValue.EmitPush()) + foreach (Opcode opcode in stackValue.EmitOpcodes()) { if (first) { @@ -404,7 +357,7 @@ public IEnumerable EmitOpCodes() } if (Instructions.Any()) { - foreach (Opcode opcode in Instructions.Last().EmitOpcode()) + foreach (Opcode opcode in Instructions.Last().EmitOpcodes()) { if (first) { diff --git a/src/kOS.Safe/Compilation/IR/IInterimOperand.cs b/src/kOS.Safe/Compilation/IR/IInterimOperand.cs new file mode 100644 index 000000000..458338ea0 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IInterimOperand.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace kOS.Safe.Compilation.IR +{ + public interface IInterimOperand + { + /// + /// Gets a value indicating whether this instance is invariant. + /// That is, if the value of the operand can be known at + /// compile time. + /// + /// + /// true if this instance is invariant; otherwise, false. + /// + bool IsInvariant { get; } + + /// + /// Gets the type of the operand. + /// + Type Type { get; } + + /// + /// Emits the opcodes that will generate the operand. + /// + /// One or more instances. + IEnumerable EmitOpcodes(); + } +} diff --git a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs index c9f446380..df2a14634 100644 --- a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs +++ b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs @@ -11,13 +11,13 @@ public interface IOperandInstructionBase /// /// Applies an action for each operand. /// - void ForEachOperand(Action action); + void ForEachOperand(Action action); /// /// Mutates each operand by replacing it with the result of applied to that operand. /// /// The mutation function. - void MutateEachOperand(Func mutateFunc); + void MutateEachOperand(Func mutateFunc); } /// @@ -28,7 +28,7 @@ public interface ISingleOperandInstruction : IOperandInstructionBase /// /// Gets or sets the operand for this instruction. /// - IRValue Operand { get; set; } + IInterimOperand Operand { get; set; } } /// /// Represents instructions which operate on multiple operands. @@ -38,7 +38,7 @@ public interface IMultipleOperandInstruction : IOperandInstructionBase /// /// Gets the collection of operands for the instruction. /// - IEnumerable Operands { get; } + IEnumerable Operands { get; } /// /// Gets the number of operands for this instruction. /// diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 8b90653b3..f9429b081 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -12,28 +12,26 @@ namespace kOS.Safe.Compilation.IR /// public class IRBuilder { - private int nextTempId = 0; - /// /// Lowers the specified code from a sequence of s /// to a three-address code interim representation. /// /// The code to lower. /// A sequence of objects, representing the instructions. - public List Lower(List code) + public List Lower(List code, IRCodePart codePart, IRScope parentScope = null) { List blocks = new List(); if (code.Count == 0) return blocks; Dictionary labels = ProgramBuilder.MapLabels(code); - CreateBlocks(code, labels, blocks); - FillBlocks(code, labels, blocks); + CreateBlocks(code, codePart, labels, blocks, parentScope); + FillBlocks(code, labels, blocks, codePart); return blocks; } - private void CreateBlocks(List code, Dictionary labels, List blocks) + private void CreateBlocks(List code, IRCodePart codePart, Dictionary labels, List blocks, IRScope parentScope) { - IRScope globalScope = new IRScope(null) { IsGlobalScope = true }; + IRScope globalScope = parentScope ?? new IRScope(parentScope, null); SortedSet leaders = new SortedSet() { 0 }; HashSet scopePushes = new HashSet(); HashSet scopePops = new HashSet(); @@ -75,7 +73,7 @@ private void CreateBlocks(List code, Dictionary labels, Lis string label = code[startIndex].Label; if (label.StartsWith("@")) label = null; - BasicBlock block = new BasicBlock(startIndex, endIndex, label); + BasicBlock block = new BasicBlock(codePart, startIndex, endIndex, label); blocks.Add(block); } @@ -92,7 +90,7 @@ private void CreateBlocks(List code, Dictionary labels, Lis else if (blocks.Any(b => b.StartIndex == block.EndIndex + 1)) { BasicBlock successor = GetBlockFromStartIndex(blocks, block.EndIndex + 1); - block.FallthroughJump = new IRJump(successor, lastOpcode.SourceLine, lastOpcode.SourceColumn); + block.FallthroughJump = new IRJump(block, successor, lastOpcode.SourceLine, lastOpcode.SourceColumn); block.AddSuccessor(successor); } #if DEBUG @@ -117,11 +115,7 @@ private static void AssignScopes(BasicBlock root, IRScope globalScope, HashSet code, Dictionary labels, List blocks) + private void FillBlocks(List code, Dictionary labels, List blocks, IRCodePart codePart) { - Stack stack = new Stack(); + Stack stack = new Stack(); BasicBlock currentBlock = GetBlockFromStartIndex(blocks, 0); for (int i = 0; i < code.Count; i++) { @@ -147,17 +141,11 @@ private void FillBlocks(List code, Dictionary labels, List< currentBlock.SetStackState(stack); currentBlock = GetBlockFromStartIndex(blocks, i); } - ParseInstruction(code[i], currentBlock, stack, labels, i, blocks); + ParseInstruction(code[i], currentBlock, stack, labels, i, blocks, codePart); } } - private IRTemp CreateTemp() - { - IRTemp result = new IRTemp(nextTempId); - nextTempId++; - return result; - } - private static IRValue PopFromStack(Stack stack, BasicBlock block) + private static IInterimOperand PopFromStack(Stack stack, BasicBlock block) { if (stack.Count > 0) return stack.Pop(); @@ -166,66 +154,57 @@ private static IRValue PopFromStack(Stack stack, BasicBlock block) return parameter; } - private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack stack, Dictionary labels, int index, List blocks) + private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack stack, Dictionary labels, int index, List blocks, IRCodePart codePart) { - IRValue PopStack() => PopFromStack(stack, currentBlock); + IInterimOperand PopStack() + =>PopFromStack(stack, currentBlock); + switch (opcode) { case OpcodeStore store: - Store(PopStack(), currentBlock, currentBlock.Scope, store); + Store(PopStack(), currentBlock, store, codePart); break; case OpcodeStoreExist storeExist: - Store(PopStack(), currentBlock, currentBlock.Scope, storeExist, assertExist: true); + Store(PopStack(), currentBlock, storeExist, codePart, assertExist: true); break; case OpcodeStoreLocal storeLocal: - Store(PopStack(), currentBlock, currentBlock.Scope, storeLocal, IRAssign.StoreScope.Local); + Store(PopStack(), currentBlock, storeLocal, codePart, IRAssign.StoreScope.Local); break; case OpcodeStoreGlobal storeGlobal: - Store(PopStack(), currentBlock, currentBlock.Scope.GetGlobalScope(), storeGlobal, IRAssign.StoreScope.Global); + Store(PopStack(), currentBlock, storeGlobal, codePart, IRAssign.StoreScope.Global); break; case OpcodeExists exists: - IRTemp temp = CreateTemp(); - IRInstruction instruction = new IRUnaryOp(temp, exists, PopStack()); - temp.Parent = instruction; - //currentBlock.Add(instruction); - stack.Push(temp); + IResultingInstruction instruction = new IRUnaryOp(currentBlock, exists, PopStack()); + stack.Push(instruction); break; case OpcodeUnset unset: - currentBlock.Add(new IRUnaryConsumer(unset, PopStack(), true)); + IInterimOperand variableIdentifier = PopStack(); + currentBlock.Add(new IRUnset(currentBlock, unset, variableIdentifier)); break; case OpcodeGetMethod getMethod: - temp = CreateTemp(); - instruction = new IRSuffixGetMethod(temp, PopStack(), getMethod); - //currentBlock.Add(instruction); - temp.Parent = instruction; - stack.Push(temp); + instruction = new IRSuffixGetMethod(currentBlock, PopStack(), getMethod); + stack.Push(instruction); break; case OpcodeGetMember getMember: - temp = CreateTemp(); - instruction = new IRSuffixGet(temp, PopStack(), getMember); - temp.Parent = instruction; - //currentBlock.Add(instruction); - stack.Push(temp); + instruction = new IRSuffixGet(currentBlock, PopStack(), getMember); + stack.Push(instruction); break; case OpcodeSetMember setMember: - IRValue value = PopStack(); - IRValue memberObj = PopStack(); - currentBlock.Add(new IRSuffixSet(memberObj, value, setMember)); + IInterimOperand value = PopStack(); + IInterimOperand memberObj = PopStack(); + currentBlock.Add(new IRSuffixSet(currentBlock, memberObj, value, setMember)); break; case OpcodeGetIndex getIndex: - IRValue targetIndex = PopStack(); - IRValue indexObj = PopStack(); - temp = CreateTemp(); - instruction = new IRIndexGet(temp, indexObj, targetIndex, getIndex); - //currentBlock.Add(instruction); - temp.Parent = instruction; - stack.Push(temp); + IInterimOperand targetIndex = PopStack(); + IInterimOperand indexObj = PopStack(); + instruction = new IRIndexGet(currentBlock, indexObj, targetIndex, getIndex); + stack.Push(instruction); break; case OpcodeSetIndex setIndex: value = PopStack(); targetIndex = PopStack(); indexObj = PopStack(); - currentBlock.Add(new IRIndexSet(indexObj, targetIndex, value, setIndex)); + currentBlock.Add(new IRIndexSet(currentBlock, indexObj, targetIndex, value, setIndex)); break; case OpcodeEOF _: case OpcodeEOP _: @@ -234,14 +213,11 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack arguments = new Stack(); + Stack arguments = new Stack(); bool hasArgmarker = stack.Count > 0; // Not even an argument marker on the stack - the dominator block must have it while (stack.Count > 0) { - IRValue stackResult = PopStack(); - if (stackResult is IRConstant constant && constant.Value is Execution.KOSArgMarkerType) + IInterimOperand stackResult = PopStack(); + if (stackResult is InterimConstantValue constant && constant.Value is Execution.KOSArgMarkerType) break; arguments.Push(stackResult); } - instruction = new IRCall(temp, call, hasArgmarker, arguments); + instruction = new IRCall(currentBlock, call, hasArgmarker, arguments); if (stack.Count > 0 && !((IRCall)instruction).Direct) { ((IRCall)instruction).IndirectMethod = PopStack(); } - temp.Parent = instruction; - stack.Push(temp); + stack.Push(instruction); break; case OpcodeReturn opcodeReturn: - currentBlock.Add(new IRReturn(opcodeReturn.Depth, opcodeReturn) { Value = PopStack() }); + currentBlock.Add(new IRReturn(currentBlock, opcodeReturn.Depth, opcodeReturn) { Value = PopStack() }); break; case OpcodePush opcodePush: object argument = opcodePush.Argument; if (IsPushingVariable(opcodePush)) - stack.Push(currentBlock.PushVariable((string)argument, opcodePush)); + stack.Push(new InterimVariableReference((string)argument, opcodePush)); else - stack.Push(new IRConstant(argument, opcodePush)); + stack.Push(new InterimConstantValue(argument, opcodePush)); break; case OpcodePushDelegateRelocateLater delegateRelocateLater: stack.Push(new IRDelegateRelocateLater(delegateRelocateLater.DestinationLabel, delegateRelocateLater.WithClosure, delegateRelocateLater)); @@ -334,49 +302,52 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack opcodePush.Argument is string identifier && identifier.StartsWith("$"); - - private static SSAVariable GetSSAVariable(IRScope scope, OpcodeIdentifierBase store, bool includeParents = true) - { - IRVariable variable = (IRVariable)scope.GetVariableNamed(store.Identifier, includeParents) ?? new IRVariable(store, scope); - return variable.GetNewSSAVariable(); - } } } \ No newline at end of file diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index 3523d691c..bb33ac32e 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -11,6 +11,10 @@ namespace kOS.Safe.Compilation.IR /// public class IRCodePart { + private readonly Dictionary functionRefs = new Dictionary(); + private readonly Dictionary closureScopes = + new Dictionary(); + /// /// Gets or sets the mainline code, in BasicBlock format. /// @@ -48,31 +52,31 @@ public IRCodePart(CodePart codePart, List userFunctions, List new IRFunction(builder, f)).ToList(); - Triggers = triggers.Select(t => new IRTrigger(builder, t)).ToList(); + MainCode = builder.Lower(codePart.MainCode, this); + + Functions = new List(); + Queue functionsToLower = new Queue(userFunctions.Where(f => closureScopes.ContainsKey(f.Identifier))); + HashSet completedFunctions = new HashSet(); + while (functionsToLower.Count > 0) + { + UserFunction function = functionsToLower.Dequeue(); + Functions.Add(new IRFunction(builder, function, this)); + completedFunctions.Add(function); + foreach (UserFunction func in userFunctions.Where( + f => closureScopes.ContainsKey(f.Identifier) && + !completedFunctions.Contains(f) && + !functionsToLower.Contains(f))) + functionsToLower.Enqueue(func); + } + Triggers = triggers.Select(t => new IRTrigger(builder, t, this)).ToList(); + foreach (UserFunction func in userFunctions.Except(completedFunctions)) + Functions.Add(new IRFunction(builder, func, this)); Blocks.AddRange(MainCode); if (MainCode.Count > 0) RootBlocks.Add(MainCode[0]); - foreach (IRTrigger trigger in Triggers) - { - Blocks.AddRange(trigger.Code); - if (trigger.Code.Count > 0) - RootBlocks.Add(trigger.Code[0]); - } - foreach (IRFunction function in Functions) - { - Blocks.AddRange(function.InitializationCode); - if (function.InitializationCode.Count > 0) - RootBlocks.Add(function.InitializationCode[0]); - foreach (IRFunction.IRFunctionFragment fragment in function.Fragments) - { - Blocks.AddRange(fragment.FunctionCode); - if (fragment.FunctionCode.Count > 0) - RootBlocks.Add(fragment.FunctionCode[0]); - } - } + RootBlocks.AddRange(Triggers.Select(t => t.RootBlock)); + RootBlocks.AddRange(Functions.SelectMany(f => f.RootBlocks)); SingleStaticAssignment.FinalizeSSA(this); } @@ -99,6 +103,37 @@ public IRTrigger GetTrigger(string identifier) return Triggers.FirstOrDefault(t => string.Equals(t.Identifier, identifier, StringComparison.OrdinalIgnoreCase)); } + public static void FlattenCallTree(IClosureVariableUser funcOrTrigger) + { + funcOrTrigger.FunctionCalls.UnionWith(GetFunctionsCalled(funcOrTrigger)); + funcOrTrigger.TriggersCreated.UnionWith(GetTriggersCreated(funcOrTrigger)); + funcOrTrigger.ExternalWrites.UnionWith(funcOrTrigger.FunctionCalls.SelectMany(f => f.ExternalWrites)); + funcOrTrigger.ExternalUnsets.UnionWith(funcOrTrigger.FunctionCalls.SelectMany(f => f.ExternalUnsets)); + } + public void FlattenCallTrees() + { + foreach (IRFunction function in Functions) + FlattenCallTree(function); + foreach (IRTrigger trigger in Triggers) + FlattenCallTree(trigger); + } + private static HashSet GetFunctionsCalled(IClosureVariableUser funcOrTrigger) + { + HashSet result = new HashSet(); + GetFunctionsCalled_Recursive(funcOrTrigger, result); + return result; + } + private static void GetFunctionsCalled_Recursive(IClosureVariableUser funcOrTrigger, HashSet result) + { + foreach (IRFunction func in funcOrTrigger.FunctionCalls) + { + if (result.Add(func)) + GetFunctionsCalled_Recursive(func, result); + } + } + public static HashSet GetTriggersCreated(IClosureVariableUser funcOrTrigger) + => new HashSet(GetFunctionsCalled(funcOrTrigger).SelectMany(f => f.TriggersCreated)); + /// /// Emits the code into Opcode representation, and back into /// its source objects. @@ -118,6 +153,36 @@ public void EmitCode(CodePart codePart) codePart.MainCode = emitter.Emit(MainCode); } + public void EnrollClosure(string pointer, IRScope closureScope, bool isGlobal = false) + { + closureScopes[pointer] = (closureScope, isGlobal); + } + public void EnrollFunction(string variable, string functionRef, IRScope closureScope, bool isGlobal) + { + string functionID = functionRef.Split('-').First(); + functionRefs[variable] = functionID; + if (Functions == null) + { + EnrollClosure(functionID, closureScope, isGlobal); + } + else + { + IRFunction function = GetFunction(functionID); + if (function == null) + EnrollClosure(functionID, closureScope, isGlobal); + else + function.IsGlobal = isGlobal; + } + } + public IRFunction GetFunction(IRCall call) + { + if (!functionRefs.TryGetValue(call.Function, out string functionName)) + return null; + if (functionName == null) + return null; + return GetFunction(functionName); + } + /// /// This class represents a trigger definition. /// @@ -133,41 +198,29 @@ public class IRTrigger : IClosureVariableUser /// Gets or sets the code for this trigger, in BasicBlock representation. /// public List Code { get; set; } + public HashSet ExternalReads { get; set; } = new HashSet(); + public HashSet ExternalWrites { get; } = new HashSet(); + public HashSet<(string, IRUnset)> ExternalUnsets { get; } = new HashSet<(string, IRUnset)>(); + public HashSet TriggersCreated { get; } = new HashSet(); + public HashSet FunctionCalls { get; } = new HashSet(); + public IRScope ClosureScope { get; } - /// - /// Gets the collection of external variables that are read - /// within the trigger. - /// - public HashSet ExternalReads { get; } = new HashSet(); - /// - /// Gets the collection of external variables that are written - /// to within the trigger. - /// - public HashSet ExternalWrites { get; } = new HashSet(); + public BasicBlock RootBlock { get; } /// /// Initializes a new instance of the class. /// /// The IRBuilder object in use. /// The trigger object to convert. - public IRTrigger(IRBuilder builder, Trigger trigger) + public IRTrigger(IRBuilder builder, Trigger trigger, IRCodePart codePart) { this.trigger = trigger; Identifier = trigger.Code.FirstOrDefault()?.Label ?? ""; - Code = builder.Lower(trigger.Code); + ClosureScope = codePart.closureScopes[Identifier].Scope; + Code = builder.Lower(trigger.Code, codePart, ClosureScope); if (Code.Count > 0) { - ExternalReads.UnionWith(Code[0].Scope.GetGlobalScope().Variables.Cast()); - foreach (BasicBlock block in Code) - { - ExternalWrites.UnionWith(block.VariablesWritten.Where(v => v.Scope.IsGlobalScope)); - /*foreach (IRInstruction instruction in block.Instructions) - { - if (instruction is IRAssign assignment && - assignment.Target.Scope.IsGlobalScope) - writes.Add(assignment.Target); - }*/ - } + RootBlock = Code[0]; } } /// @@ -185,7 +238,7 @@ public void EmitCode(IREmitter emitter) /// /// This class represents a user-defined function. /// - /// + /// public class IRFunction : IClosureVariableUser { private readonly UserFunction function; @@ -197,6 +250,14 @@ public class IRFunction : IClosureVariableUser /// public string Identifier => function.Identifier; /// + /// Gets or sets a value indicating whether this instance + /// is stored at the global scope. + /// + /// + /// true if this instance is global; otherwise, false. + /// + public bool IsGlobal { get; internal set; } = false; + /// /// Gets or sets the initialization code, in BasicBlock format. /// public List InitializationCode { get; set; } @@ -204,48 +265,45 @@ public class IRFunction : IClosureVariableUser /// Gets the collection of function fragments. /// public IReadOnlyCollection Fragments => fragments.Values; + public HashSet ExternalReads { get; set; } = new HashSet(); + public HashSet ExternalWrites { get; } = new HashSet(); + public HashSet<(string, IRUnset)> ExternalUnsets { get; } = new HashSet<(string, IRUnset)>(); + public HashSet TriggersCreated { get; } = new HashSet(); + public HashSet FunctionCalls { get; } = new HashSet(); + public IRScope ClosureScope { get; } /// - /// Gets the collection of external variables that are read - /// within the function. - /// - public HashSet ExternalReads { get; } = new HashSet(); - /// - /// Gets the collection of external variables that are written - /// to within the function. + /// Gets a value indicating whether this instance may be recursive. /// - public HashSet ExternalWrites { get; } = new HashSet(); + /// + /// true if this instance may be recursive; otherwise, false. + /// + public bool IsRecursive => FunctionCalls.Contains(this); + + public List RootBlocks { get; } = new List(); /// /// Initializes a new instance of the class. /// /// The IRBuilder object in use. /// The user function object to convert. - public IRFunction(IRBuilder builder, UserFunction function) + public IRFunction(IRBuilder builder, UserFunction function, IRCodePart codePart) { this.function = function; - InitializationCode = builder.Lower(function.InitializationCode); + (ClosureScope, IsGlobal) = codePart.closureScopes[Identifier]; + InitializationCode = builder.Lower(function.InitializationCode, codePart, ClosureScope); userFunctionFragments = function.PeekNewCodeFragments().ToList(); foreach (UserFunctionCodeFragment fragment in userFunctionFragments) { - fragments.Add(fragment, new IRFunctionFragment(builder, fragment)); + fragments.Add(fragment, new IRFunctionFragment(builder, fragment, codePart, ClosureScope)); } userFunctionFragments.Reverse(); + if (function.InitializationCode.Count > 0) + RootBlocks.Add(InitializationCode[0]); foreach (IRFunctionFragment fragment in Fragments) { - if (fragment.FunctionCode.Count == 0) - continue; - ExternalReads.UnionWith(fragment.FunctionCode[0].Scope.GetGlobalScope().Variables.Cast()); - foreach (BasicBlock block in fragment.FunctionCode) - { - ExternalWrites.UnionWith(block.VariablesWritten.Where(v => v.Scope.IsGlobalScope)); - /*foreach (IRInstruction instruction in block.Instructions) - { - if (instruction is IRAssign assignment && - assignment.Target.Scope.IsGlobalScope) - writes.Add(assignment.Target); - }*/ - } + if (fragment.FunctionCode.Count > 0) + RootBlocks.Add(fragment.FunctionCode[0]); } } @@ -279,10 +337,10 @@ public class IRFunctionFragment /// /// The IRBuilder object in use. /// The function code fragment to convert. - public IRFunctionFragment(IRBuilder builder, UserFunctionCodeFragment codeFragment) + public IRFunctionFragment(IRBuilder builder, UserFunctionCodeFragment codeFragment, IRCodePart codePart, IRScope ClosureScope) { fragment = codeFragment; - FunctionCode = builder.Lower(codeFragment.Code); + FunctionCode = builder.Lower(codeFragment.Code, codePart, ClosureScope); } /// /// Emits the code into Opcode representation back into the @@ -297,33 +355,6 @@ public void EmitCode(IREmitter emitter) } } - /// - /// Sets the scope in which a function or trigger is defined. - /// - /// The function or trigger to target. - /// The scope to set as parent. - public static void SetDefiningScope(IClosureVariableUser function, IRScope scope) - { - HashSet tempWrites = new HashSet(function.ExternalWrites); - foreach (SSAVariable variable in tempWrites) - { - if (scope.IsVariableInScope(variable.Name)) - { - variable.RedefineScope(scope.GetVariableNamed(variable.Name).Scope); - } - } - - HashSet tempReads = new HashSet(function.ExternalReads); - foreach (IRVariable variable in tempReads) - { - if (scope.IsVariableInScope(variable.Name)) - { - function.ExternalReads.Remove(variable); - function.ExternalReads.Add((IRVariable)scope.GetVariableNamed(variable.Name)); - } - } - } - /// /// Represents a user of a closure and its contained variables. /// @@ -333,12 +364,28 @@ public interface IClosureVariableUser /// Gets the collection of external variables that are read /// within the closure. /// - HashSet ExternalReads { get; } + HashSet ExternalReads { get; set; } + /// + /// Gets the collection of external variables that may be written + /// to by this instance. + /// + HashSet ExternalWrites { get; } + /// + /// Gets the collection of external variables that may be unset by this instance. + /// + HashSet<(string, IRUnset)> ExternalUnsets { get; } + /// + /// Gets the collection of triggers that could be created from this instance. + /// + HashSet TriggersCreated { get; } + /// + /// Gets the functions called from within this instance's body. + /// + HashSet FunctionCalls { get; } /// - /// Gets the collection of external variables that are written - /// to within the closure. + /// Gets the scope of the closure this instance uses. /// - HashSet ExternalWrites { get; } + IRScope ClosureScope { get; } } } } diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 302998794..23c684b7f 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -1,20 +1,23 @@ using System; using System.Collections.Generic; using System.Linq; +using kOS.Safe.Exceptions; namespace kOS.Safe.Compilation.IR { public abstract class IRInstruction { + public BasicBlock Block { get; } public short SourceLine { get; private set; } // line number in the source code that this was compiled from. public short SourceColumn { get; private set; } // column number of the token nearest the cause of this Opcode. public abstract bool IsInvariant { get; } - internal abstract IEnumerable EmitOpcode(); - protected IRInstruction(Opcode originalOpcode) + public abstract IEnumerable EmitOpcodes(); + protected IRInstruction(Opcode originalOpcode, BasicBlock block) { SourceLine = originalOpcode.SourceLine; SourceColumn = originalOpcode.SourceColumn; + Block = block; } protected Opcode SetSourceLocation(Opcode opcode) { @@ -33,38 +36,38 @@ public void OverwriteSourceLocation(short sourceLine, short sourceColumn) public abstract class SingleOperandInstruction : IRInstruction, ISingleOperandInstruction { - protected IRValue operand; + protected IInterimOperand operand; - protected SingleOperandInstruction(Opcode originalOpcode) : base(originalOpcode) { } + protected SingleOperandInstruction(Opcode originalOpcode, BasicBlock block) : base(originalOpcode, block) { } - IRValue ISingleOperandInstruction.Operand { get => operand; set => operand = value; } + IInterimOperand ISingleOperandInstruction.Operand { get => operand; set => operand = value; } - public void ForEachOperand(Action action) + public void ForEachOperand(Action action) => action(operand); - public void MutateEachOperand(Func mutateFunc) + public void MutateEachOperand(Func mutateFunc) => operand = mutateFunc(operand); } public abstract class MultipleOperandInstruction : IRInstruction, IMultipleOperandInstruction { - protected MultipleOperandInstruction(Opcode originalOpcode) : base(originalOpcode) { } + protected MultipleOperandInstruction(Opcode originalOpcode, BasicBlock block) : base(originalOpcode, block) { } - public abstract IEnumerable Operands { get; } + public abstract IEnumerable Operands { get; } public abstract int OperandCount { get; } /// /// Allows replacing operands from a common function. /// The meaning of the index and ordering are irrelevant, /// as long as it covers the range [0, ). /// - protected abstract IRValue this[int index] { get; set; } + protected abstract IInterimOperand this[int index] { get; set; } - public void ForEachOperand(Action action) + public void ForEachOperand(Action action) { - foreach (IRValue operand in Operands) + foreach (IInterimOperand operand in Operands) action(operand); } - public void MutateEachOperand(Func mutateFunc) + public void MutateEachOperand(Func mutateFunc) { for (int i = 0; i < OperandCount; i++) this[i] = mutateFunc(this[i]); @@ -80,22 +83,20 @@ public enum StoreScope Global } public override bool IsInvariant => Value.IsInvariant; - public IRVariableBase Target { get; set; } - public IRValue Value { get => operand; set => operand = value; } + public SSASetDefinition Target { get; set; } + public IInterimOperand Value { get => operand; set => operand = value; } public StoreScope Scope { get; set; } = StoreScope.Ambivalent; public bool AssertExists { get; set; } = false; - public IRAssign(OpcodeIdentifierBase opcode, IRVariableBase target, IRValue value) : base(opcode) + public IRAssign(BasicBlock block, OpcodeIdentifierBase opcode, IInterimOperand value) : base(opcode, block) { - Target = target; Value = value; - if (target is SSAVariable ssaTarget) - ssaTarget.AssignedAt = this; + Target = new SSASetDefinition(opcode.Identifier, this); } - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { if (Value != null) - foreach (Opcode opcode in Value.EmitPush()) + foreach (Opcode opcode in Value.EmitOpcodes()) yield return opcode; if (AssertExists) { @@ -118,58 +119,53 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{{store {0} -> {1}}}", Value.ToString(), Target.ToString()); - public override bool Equals(object obj) - => obj is IRAssign assignment && - Target.Equals(assignment.Target) && - Value.Equals(assignment.Value); - public override int GetHashCode() - => Target.GetHashCode(); } public class IRBinaryOp : MultipleOperandInstruction, IResultingInstruction { public override bool IsInvariant => Left.IsInvariant && Right.IsInvariant; - public IRValue Result { get; set; } public BinaryOpcode Operation { get; set; } - public IRValue Left { get; set; } - public IRValue Right { get; set; } - public override IEnumerable Operands { get { yield return Left; yield return Right; } } + public IInterimOperand Left { get; set; } + public IInterimOperand Right { get; set; } + public override IEnumerable Operands { get { yield return Left; yield return Right; } } public override int OperandCount => 2; - public Type ResultType + public Type Type { get { - Calculator calculator = Calculator.GetCalculator(Left.ValueType, Right.ValueType); + if (Left.Type == null || Right.Type == null) + return null; + Calculator calculator = Calculator.GetCalculator(Left.Type, Right.Type); switch (Operation) { case OpcodeMathAdd _: - return calculator.GetAddResultType(Left.ValueType, Right.ValueType); + return calculator.GetAddResultType(Left.Type, Right.Type); case OpcodeMathSubtract _: - return calculator.GetSubtractResultType(Left.ValueType, Right.ValueType); + return calculator.GetSubtractResultType(Left.Type, Right.Type); case OpcodeMathMultiply _: - return calculator.GetMultiplyResultType(Left.ValueType, Right.ValueType); + return calculator.GetMultiplyResultType(Left.Type, Right.Type); case OpcodeMathDivide _: - return calculator.GetDivideResultType(Left.ValueType, Right.ValueType); + return calculator.GetDivideResultType(Left.Type, Right.Type); case OpcodeMathPower _: - return calculator.GetPowerResultType(Left.ValueType, Right.ValueType); + return calculator.GetPowerResultType(Left.Type, Right.Type); case OpcodeCompareEqual _: - return calculator.GetEqualResultType(Left.ValueType, Right.ValueType); + return calculator.GetEqualResultType(Left.Type, Right.Type); case OpcodeCompareNE _: - return calculator.GetNotEqualResultType(Left.ValueType, Right.ValueType); + return calculator.GetNotEqualResultType(Left.Type, Right.Type); case OpcodeCompareGT _: - return calculator.GetGreaterThanResultType(Left.ValueType, Right.ValueType); + return calculator.GetGreaterThanResultType(Left.Type, Right.Type); case OpcodeCompareLT _: - return calculator.GetLessThanResultType(Left.ValueType, Right.ValueType); + return calculator.GetLessThanResultType(Left.Type, Right.Type); case OpcodeCompareGTE _: - return calculator.GetGreaterThanEqualResultType(Left.ValueType, Right.ValueType); + return calculator.GetGreaterThanEqualResultType(Left.Type, Right.Type); case OpcodeCompareLTE _: - return calculator.GetLessThanEqualResultType(Left.ValueType, Right.ValueType); + return calculator.GetLessThanEqualResultType(Left.Type, Right.Type); default: throw new NotImplementedException(); } } } - protected override IRValue this[int index] + protected override IInterimOperand this[int index] { get => index == 0 ? Left : index == 1 ? Right : throw new ArgumentOutOfRangeException(); set @@ -186,17 +182,19 @@ public bool IsCommutative { get { - Calculator calculator = Calculator.GetCalculator(Left.ValueType, Right.ValueType); + if (Left.Type == null || Right.Type == null) + return false; + Calculator calculator = Calculator.GetCalculator(Left.Type, Right.Type); switch (Operation) { case OpcodeMathAdd _: - return calculator.IsAdditionCommutative(Left.ValueType, Right.ValueType); + return calculator.IsAdditionCommutative(Left.Type, Right.Type); case OpcodeMathSubtract _: - return calculator.IsSubtractionCommutative(Left.ValueType, Right.ValueType); + return calculator.IsSubtractionCommutative(Left.Type, Right.Type); case OpcodeMathMultiply _: - return calculator.IsMultiplicationCommmutative(Left.ValueType, Right.ValueType); + return calculator.IsMultiplicationCommmutative(Left.Type, Right.Type); case OpcodeMathDivide _: - return calculator.IsDivisionCommutative(Left.ValueType, Right.ValueType); + return calculator.IsDivisionCommutative(Left.Type, Right.Type); case OpcodeMathPower _: return false; case OpcodeCompareEqual _: @@ -218,9 +216,8 @@ public bool IsCommutative } } - public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) : base(operation) + public IRBinaryOp(BasicBlock block, BinaryOpcode operation, IInterimOperand left, IInterimOperand right) : base(operation, block) { - Result = result; Operation = operation; Left = left; Right = right; @@ -250,11 +247,11 @@ public bool SwapOperands() } return true; } - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { - foreach (Opcode opcode in Left.EmitPush()) + foreach (Opcode opcode in Left.EmitOpcodes()) yield return opcode; - foreach (Opcode opcode in Right.EmitPush()) + foreach (Opcode opcode in Right.EmitOpcodes()) yield return opcode; Operation.Label = string.Empty; yield return SetSourceLocation(Operation); @@ -273,14 +270,29 @@ public override bool Equals(object obj) } public override int GetHashCode() => Operation.GetHashCode(); + + public InterimConstantValue Evaluate() + { + if (!IsInvariant) + throw new InvalidOperationException(); + object left = (Left as IEvaluatableToConstant)?.Evaluate().Value; + object right = (Right as IEvaluatableToConstant)?.Evaluate().Value; + try + { + return new InterimConstantValue(Operation.ExecuteCalculation(left, right), this); + } + catch (KOSBinaryOperandTypeException binaryTypeException) + { + throw new KOSCompileException(this, binaryTypeException); + } + } } public class IRUnaryOp : SingleOperandInstruction, IResultingInstruction { - public override bool IsInvariant => Operand.IsInvariant; - public IRValue Result { get; set; } + public override bool IsInvariant => Operand.IsInvariant && !(Operation is OpcodeExists); public Opcode Operation { get; } - public IRValue Operand { get => operand; set => operand = value; } - public Type ResultType + public IInterimOperand Operand { get => operand; set => operand = value; } + public Type Type { get { @@ -291,21 +303,20 @@ public Type ResultType case OpcodeLogicToBool _: return typeof(Encapsulation.BooleanValue); case OpcodeMathNegate _: - return Operand.ValueType; + return Operand.Type; default: throw new NotImplementedException(); } } } - public IRUnaryOp(IRTemp result, Opcode operation, IRValue operand) : base(operation) + public IRUnaryOp(BasicBlock block, Opcode operation, IInterimOperand operand) : base(operation, block) { - Result = result; Operation = operation; Operand = operand; } - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { - foreach (Opcode opcode in Operand.EmitPush()) + foreach (Opcode opcode in Operand.EmitOpcodes()) yield return opcode; Operation.Label = string.Empty; yield return Operation; @@ -317,82 +328,104 @@ public override bool Equals(object obj) Operation.GetType() == unaryOp.Operation.GetType() && Operand.Equals(unaryOp.Operand); public override int GetHashCode() - => Operation.GetHashCode(); + => (Operation, Operand).GetHashCode(); + + public InterimConstantValue Evaluate() + { + if (!IsInvariant) + throw new InvalidOperationException(); + object input = (Operand as IEvaluatableToConstant)?.Evaluate().Value; + try + { + switch (Operation) + { + case OpcodeMathNegate _: + return new InterimConstantValue(OpcodeMathNegate.StaticOperation(input), this); + case OpcodeLogicNot _: + return new InterimConstantValue(OpcodeLogicNot.StaticOperation(input), this); + case OpcodeLogicToBool _: + return new InterimConstantValue(OpcodeLogicToBool.StaticOperation(input), this); + default: + throw new NotImplementedException(); + } + } + catch (KOSUnaryOperandTypeException unaryTypeException) + { + throw new KOSCompileException(this, unaryTypeException); + } + } } public class IRNoStackInstruction : IRInstruction { public override bool IsInvariant => false; public Opcode Operation { get; } - public IRNoStackInstruction(Opcode opcode) : base(opcode) + public IRNoStackInstruction(BasicBlock block, Opcode opcode) : base(opcode, block) => Operation = opcode; - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { Operation.Label = string.Empty; yield return Operation; } public override string ToString() => Operation.ToString(); - public override bool Equals(object obj) - => obj is IRNoStackInstruction instruction && Operation.GetType() == instruction.Operation.GetType(); - public override int GetHashCode() - => Operation.GetHashCode(); } public class IRUnaryConsumer : SingleOperandInstruction { private readonly bool operationHasSideEffects; public override bool IsInvariant => !operationHasSideEffects && Operand.IsInvariant; public Opcode Operation { get; } - public IRValue Operand { get => operand; set => operand = value; } - public IRUnaryConsumer(Opcode opcode, IRValue operand, bool sideEffects = false) : base(opcode) + public IInterimOperand Operand { get => operand; set => operand = value; } + public IRUnaryConsumer(BasicBlock block, Opcode opcode, IInterimOperand operand, bool sideEffects = false) : base(opcode, block) { Operation = opcode; Operand = operand; operationHasSideEffects = sideEffects; } - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { - foreach (Opcode opcode in Operand.EmitPush()) + foreach (Opcode opcode in Operand.EmitOpcodes()) yield return opcode; Operation.Label = string.Empty; yield return Operation; } public override string ToString() => Operation.ToString(); - public override bool Equals(object obj) - => obj is IRUnaryConsumer unaryConsumer && - Operation.GetType() == unaryConsumer.Operation.GetType() && - Operand.Equals(unaryConsumer.Operand); - public override int GetHashCode() - => Operation.GetHashCode(); + } + public class IRUnset : IRUnaryConsumer + { + public override bool IsInvariant => Target != null; + public SSASetDefinition Target { get; set; } + public bool IsExecutable => Block.IsExecutable; + public IRUnset(BasicBlock block, OpcodeUnset opcode, IInterimOperand operand) : base(block, opcode, operand, false) + { + if (operand.IsInvariant) + { + string name = (string)(operand as IEvaluatableToConstant).Evaluate().Value; + Target = new SSASetDefinition(name, this); + } + } } public class IRPop : SingleOperandInstruction { public override bool IsInvariant => Value.IsInvariant; - public IRValue Value { get => operand; set => operand = value; } - public IRPop(IRValue value, OpcodePop opcode) : base(opcode) + public IInterimOperand Value { get => operand; set => operand = value; } + public IRPop(BasicBlock block, IInterimOperand value, OpcodePop opcode) : base(opcode, block) => Value = value; - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { - if (Value is IRConstant || (Value is IRVariable && !(Value is IRTemp))) - yield break; - foreach (Opcode opcode in Value.EmitPush()) + foreach (Opcode opcode in Value.EmitOpcodes()) yield return opcode; yield return SetSourceLocation(new OpcodePop()); } public override string ToString() => $"{{pop {Value}}}"; - public override bool Equals(object obj) - => obj is IRPop pop && Value.Equals(pop.Value); - public override int GetHashCode() - => Value.GetHashCode(); } public class IRNonVarPush : IRInstruction, IResultingInstruction { public override bool IsInvariant => false; public Opcode Operation { get; } - public IRValue Result { get; } - public Type ResultType + public Type Type { get { @@ -406,79 +439,90 @@ public Type ResultType } } - public IRNonVarPush(IRValue result, Opcode opcode) : base(opcode) + public IRNonVarPush(BasicBlock block, Opcode opcode) : base(opcode, block) { Operation = opcode; - Result = result; } - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { Operation.Label = string.Empty; yield return Operation; } public override string ToString() => Operation.ToString(); - public override bool Equals(object obj) - => obj is IRNonVarPush instruction && - Operation.GetType() == instruction.Operation.GetType(); - public override int GetHashCode() - => Operation.GetHashCode(); + + public InterimConstantValue Evaluate() + => throw new InvalidOperationException(); } public class IRSuffixGet : SingleOperandInstruction, IResultingInstruction { - public override bool IsInvariant => Object.IsInvariant; - public IRValue Result { get; set; } - public IRValue Object { get => operand; set => operand = value; } + // TODO: Consider implementing this. + public override bool IsInvariant => false && Object.IsInvariant; + public IInterimOperand Object { get => operand; set => operand = value; } public string Suffix { get; set; } - public Type ResultType => TypeInferencer.GetTypeForSuffix(Object.ValueType, Suffix); - public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcodeGetMember) : base(opcodeGetMember) + public Type Type => TypeInferencer.GetTypeForSuffix(Object.Type, Suffix); + public IRSuffixGet(BasicBlock block, IInterimOperand obj, OpcodeGetMember opcodeGetMember) : base(opcodeGetMember, block) { - Result = result; Object = obj; Suffix = opcodeGetMember.Identifier; } - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { - foreach (Opcode opcode in Object.EmitPush()) + foreach (Opcode opcode in Object.EmitOpcodes()) yield return opcode; yield return SetSourceLocation(new OpcodeGetMember(Suffix)); } public override string ToString() => string.Format("{{gmb \"{0}\"}}", Suffix); public override bool Equals(object obj) - => obj is IRSuffixGet suffixGet && + => obj == this || + (IsInvariant && + obj is IRSuffixGet suffixGet && + suffixGet.IsInvariant && !(suffixGet is IRSuffixGetMethod) && string.Equals(Suffix, suffixGet.Suffix, StringComparison.OrdinalIgnoreCase) && - Object == suffixGet.Object; + Object == suffixGet.Object); public override int GetHashCode() => (Object, Suffix).GetHashCode(); + + public InterimConstantValue Evaluate() + { + if (!IsInvariant) + throw new InvalidOperationException(); + throw new NotImplementedException(); + // MUSTFIX: +#pragma warning disable CS0162 // Unreachable code detected + Encapsulation.Structure obj = (Encapsulation.Structure)(Object as IEvaluatableToConstant).Evaluate()?.Value; +#pragma warning restore CS0162 // Unreachable code detected + object result = obj.GetSuffix(Suffix); + return new InterimConstantValue(result, this); + } } public class IRSuffixGetMethod : IRSuffixGet { - public IRSuffixGetMethod(IRTemp result, IRValue obj, OpcodeGetMethod opcode) : base(result, obj, opcode) { } - internal override IEnumerable EmitOpcode() + public IRSuffixGetMethod(BasicBlock block, IInterimOperand obj, OpcodeGetMethod opcode) : base(block, obj, opcode) { } + public override IEnumerable EmitOpcodes() { - foreach (Opcode opcode in Object.EmitPush()) + foreach (Opcode opcode in Object.EmitOpcodes()) yield return opcode; yield return SetSourceLocation(new OpcodeGetMethod(Suffix)); } public override string ToString() => string.Format("{{gmet \"{0}\"}}", Suffix); public override bool Equals(object obj) - => obj is IRSuffixGetMethod suffixGet && - string.Equals(Suffix, suffixGet.Suffix, StringComparison.OrdinalIgnoreCase) && - Object == suffixGet.Object; + => obj is IRSuffixGetMethod && + base.Equals(obj); public override int GetHashCode() - => (Object, Suffix).GetHashCode(); + => base.GetHashCode(); } public class IRSuffixSet : MultipleOperandInstruction { public override bool IsInvariant => false; - public IRValue Object { get; set; } - public IRValue Value { get; set; } - public override IEnumerable Operands { get { yield return Object; yield return Value; } } + public IInterimOperand Object { get; set; } + public IInterimOperand Value { get; set; } + public override IEnumerable Operands { get { yield return Object; yield return Value; } } public override int OperandCount => 2; - protected override IRValue this[int index] + protected override IInterimOperand this[int index] { get => index == 0 ? Object : index == 1 ? Value : throw new ArgumentOutOfRangeException(); set @@ -492,40 +536,43 @@ protected override IRValue this[int index] } } public string Suffix { get; } - public IRSuffixSet(IRValue obj, IRValue value, OpcodeSetMember opcodeSetMember) : base(opcodeSetMember) + public IRSuffixSet(BasicBlock block, IInterimOperand obj, IInterimOperand value, OpcodeSetMember opcodeSetMember) : base(opcodeSetMember, block) { Object = obj; Value = value; Suffix = opcodeSetMember.Identifier; } - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { - foreach (Opcode opcode in Object.EmitPush()) + foreach (Opcode opcode in Object.EmitOpcodes()) yield return opcode; - foreach (Opcode opcode in Value.EmitPush()) + foreach (Opcode opcode in Value.EmitOpcodes()) yield return opcode; yield return SetSourceLocation(new OpcodeSetMember(Suffix)); } public override string ToString() => string.Format("{{smb \"{0}\"}}", Suffix); public override bool Equals(object obj) - => obj is IRSuffixSet suffixSet && + => obj == this || + (IsInvariant && + obj is IRSuffixSet suffixSet && + suffixSet.IsInvariant && string.Equals(Suffix, suffixSet.Suffix, StringComparison.OrdinalIgnoreCase) && Object.Equals(suffixSet.Object) && - Value.Equals(suffixSet.Value); + Value.Equals(suffixSet.Value)); public override int GetHashCode() - => (Object, Suffix).GetHashCode(); + => (Object, Suffix, Value).GetHashCode(); } public class IRIndexGet : MultipleOperandInstruction, IResultingInstruction { - public override bool IsInvariant => Object.IsInvariant && Index.IsInvariant; - public IRValue Result { get; } - public IRValue Object { get; set; } - public IRValue Index { get; set; } - public override IEnumerable Operands { get { yield return Object; yield return Index; } } + // TODO: Consider implementing this. + public override bool IsInvariant => false && Object.IsInvariant && Index.IsInvariant; + public IInterimOperand Object { get; set; } + public IInterimOperand Index { get; set; } + public override IEnumerable Operands { get { yield return Object; yield return Index; } } public override int OperandCount => 2; - public Type ResultType => TypeInferencer.GetTypeForIndex(Object.ValueType); - protected override IRValue this[int index] + public Type Type => TypeInferencer.GetTypeForIndex(Object.Type); + protected override IInterimOperand this[int index] { get => index == 0 ? Object : index == 1 ? Index : throw new ArgumentOutOfRangeException(); set @@ -538,38 +585,49 @@ protected override IRValue this[int index] throw new ArgumentOutOfRangeException(); } } - public IRIndexGet(IRTemp result, IRValue obj, IRValue index, OpcodeGetIndex opcode) : base(opcode) + public IRIndexGet(BasicBlock block, IInterimOperand obj, IInterimOperand index, OpcodeGetIndex opcode) : base(opcode, block) { - Result = result; Object = obj; Index = index; } - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { - foreach (Opcode opcode in Object.EmitPush()) + foreach (Opcode opcode in Object.EmitOpcodes()) yield return opcode; - foreach (Opcode opcode in Index.EmitPush()) + foreach (Opcode opcode in Index.EmitOpcodes()) yield return opcode; yield return SetSourceLocation(new OpcodeGetIndex()); } public override string ToString() => "{gidx}"; public override bool Equals(object obj) - => obj is IRIndexGet indexGet && + => obj == this || + (IsInvariant && + obj is IRIndexGet indexGet && + indexGet.IsInvariant && Object.Equals(indexGet.Object) && - Index.Equals(indexGet.Index); + Index.Equals(indexGet.Index)); public override int GetHashCode() - => Object.GetHashCode(); + => (Object, Index).GetHashCode(); + + public InterimConstantValue Evaluate() + { + if (!IsInvariant) + throw new InvalidOperationException(); + // MUSTFIX: + throw new NotImplementedException(); + //((Encapsulation.IIndexable)Object).GetIndex(); + } } public class IRIndexSet : MultipleOperandInstruction { public override bool IsInvariant => false; - public IRValue Object { get; set; } - public IRValue Index { get; set; } - public IRValue Value { get; set; } - public override IEnumerable Operands { get { yield return Object; yield return Index; yield return Value; } } + public IInterimOperand Object { get; set; } + public IInterimOperand Index { get; set; } + public IInterimOperand Value { get; set; } + public override IEnumerable Operands { get { yield return Object; yield return Index; yield return Value; } } public override int OperandCount => 3; - protected override IRValue this[int index] + protected override IInterimOperand this[int index] { get => index == 0 ? Object : index == 1 ? Index : index == 2 ? Value : throw new ArgumentOutOfRangeException(); set @@ -584,29 +642,32 @@ protected override IRValue this[int index] throw new ArgumentOutOfRangeException(); } } - public IRIndexSet(IRValue obj, IRValue index, IRValue value, OpcodeSetIndex opcode) : base(opcode) + public IRIndexSet(BasicBlock block, IInterimOperand obj, IInterimOperand index, IInterimOperand value, OpcodeSetIndex opcode) : base(opcode, block) { Object = obj; Index = index; Value = value; } - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { - foreach (Opcode opcode in Object.EmitPush()) + foreach (Opcode opcode in Object.EmitOpcodes()) yield return opcode; - foreach (Opcode opcode in Index.EmitPush()) + foreach (Opcode opcode in Index.EmitOpcodes()) yield return opcode; - foreach (Opcode opcode in Value.EmitPush()) + foreach (Opcode opcode in Value.EmitOpcodes()) yield return opcode; yield return SetSourceLocation(new OpcodeSetIndex()); } public override string ToString() => "{sidx}"; public override bool Equals(object obj) - => obj is IRIndexSet indexGet && + => obj == this || + (IsInvariant && + obj is IRIndexSet indexGet && + indexGet.IsInvariant && Object.Equals(indexGet.Object) && Index.Equals(indexGet.Index) && - Value.Equals(indexGet.Value); + Value.Equals(indexGet.Value)); public override int GetHashCode() => Object.GetHashCode(); } @@ -614,13 +675,13 @@ public class IRJump : IRInstruction { public override bool IsInvariant => true; public BasicBlock Target { get; set; } - public IRJump(BasicBlock target, OpcodeBranchJump opcode) : base(opcode) + public IRJump(BasicBlock block, BasicBlock target, OpcodeBranchJump opcode) : base(opcode, block) { Target = target; } - public IRJump(BasicBlock target, short sourceLine, short sourceColumn) - : this(target, new OpcodeBranchJump() { SourceLine = sourceLine, SourceColumn = sourceColumn }) { } - internal override IEnumerable EmitOpcode() + public IRJump(BasicBlock block, BasicBlock target, short sourceLine, short sourceColumn) + : this(block, target, new OpcodeBranchJump() { SourceLine = sourceLine, SourceColumn = sourceColumn }) { } + public override IEnumerable EmitOpcodes() { yield return SetSourceLocation(new OpcodeBranchJump() { DestinationLabel = Target.Label }); } @@ -635,16 +696,16 @@ public override int GetHashCode() public class IRJumpStack : SingleOperandInstruction { public override bool IsInvariant => Distance.IsInvariant; - public IRValue Distance { get => operand; set => operand = value; } + public IInterimOperand Distance { get => operand; set => operand = value; } public List Targets { get; } = new List(); - public IRJumpStack(IRValue distance, IEnumerable targets, OpcodeJumpStack jumpStack) : base(jumpStack) + public IRJumpStack(BasicBlock block, IInterimOperand distance, IEnumerable targets, OpcodeJumpStack jumpStack) : base(jumpStack, block) { Distance = distance; Targets.AddRange(targets); } - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { - foreach (Opcode opcode in Distance.EmitPush()) + foreach (Opcode opcode in Distance.EmitOpcodes()) yield return opcode; yield return SetSourceLocation(new OpcodeJumpStack()); } @@ -658,20 +719,20 @@ public override int GetHashCode() public class IRBranch : SingleOperandInstruction { public override bool IsInvariant => Condition.IsInvariant; - public IRValue Condition { get => operand; set => operand = value; } + public IInterimOperand Condition { get => operand; set => operand = value; } public BasicBlock True { get; set; } public BasicBlock False { get; set; } public bool PreferFalse { get; set; } = false; - public IRBranch(IRValue condition, BasicBlock onTrue, BasicBlock onFalse, BranchOpcode opcodeBranch) : base(opcodeBranch) + public IRBranch(BasicBlock block, IInterimOperand condition, BasicBlock onTrue, BasicBlock onFalse, BranchOpcode opcodeBranch) : base(opcodeBranch, block) { Condition = condition; True = onTrue; False = onFalse; PreferFalse = opcodeBranch is OpcodeBranchIfFalse; } - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { - foreach (Opcode opcode in Condition.EmitPush()) + foreach (Opcode opcode in Condition.EmitOpcodes()) yield return opcode; if (PreferFalse) { @@ -700,19 +761,17 @@ public class IRCall : MultipleOperandInstruction, IResultingInstruction private Type resultType = null; private bool? isFunctionInvariant = null; - protected static readonly Function.FunctionManager functionManager = new Function.FunctionManager(null); public bool IsFunctionInvariant { get => isFunctionInvariant ?? IsCallInvariant(Function); set => isFunctionInvariant = value; } public override bool IsInvariant => IsFunctionInvariant && Arguments.All(a => a.IsInvariant); - public IRValue Result { get; set; } public string Function { get; } - public List Arguments { get; } = new List(); - public override IEnumerable Operands => Enumerable.Reverse(Arguments); + public List Arguments { get; } = new List(); + public override IEnumerable Operands => Enumerable.Reverse(Arguments); public override int OperandCount => Arguments.Count; - public Type ResultType + public Type Type { get => isResultTypeInformed ? resultType : GetDefaultReturnType(); set @@ -721,67 +780,64 @@ public Type ResultType isResultTypeInformed = true; } } - protected override IRValue this[int index] + protected override IInterimOperand this[int index] { get => Arguments[index]; set => Arguments[index] = value; } - public IRValue IndirectMethod { get; internal set; } + public IInterimOperand IndirectMethod { get; internal set; } public bool Direct { get; } public bool EmitArgMarker { get; set; } - private IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker) : base(opcode) + private IRCall(BasicBlock block, OpcodeCall opcode, bool emitArgMarker) : base(opcode, block) { - Result = target; Function = (string)opcode.Destination; Direct = opcode.Direct; EmitArgMarker = emitArgMarker; } private bool IsCallInvariant(string functionName) { + // TODO: Consider that some suffix methods may actually be known at compile time. if (!Direct) return false; - if (functionManager.Exists(functionName)) - { - return functionManager.IsFunctionInvariant(functionName); - } + if (Optimization.Optimizer.FunctionManager.Exists(functionName)) + return Optimization.Optimizer.FunctionManager.IsFunctionInvariant(functionName); return false; } private Type GetDefaultReturnType() { - if (functionManager.Exists(Function)) - return functionManager.FunctionReturnType(Function); - if (IndirectMethod is IRTemp tempSuffixCall && - tempSuffixCall.Parent is IRSuffixGetMethod suffixGetMethod) - return suffixGetMethod.ResultType; + if (Optimization.Optimizer.FunctionManager.Exists(Function)) + return Optimization.Optimizer.FunctionManager.FunctionReturnType(Function); + if (!Direct && IndirectMethod is IRSuffixGetMethod suffixGetMethod) + return suffixGetMethod.Type; return typeof(Encapsulation.Structure); } - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { if (EmitArgMarker) { if (IndirectMethod != null) - foreach (Opcode opcode in IndirectMethod.EmitPush()) + foreach (Opcode opcode in IndirectMethod.EmitOpcodes()) yield return opcode; yield return new OpcodePush(new Execution.KOSArgMarkerType()); } - foreach (IRValue argument in Arguments) + foreach (IInterimOperand argument in Arguments) { - foreach (Opcode opcode in argument.EmitPush()) + foreach (Opcode opcode in argument.EmitOpcodes()) yield return opcode; } yield return SetSourceLocation(new OpcodeCall(Function)); } - public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, IRValue argument) : this(target, opcode, emitArgMarker) + public IRCall(BasicBlock block, OpcodeCall opcode, bool emitArgMarker, IInterimOperand argument) : this(block, opcode, emitArgMarker) { Arguments.Add(argument); } - public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, IEnumerable arguments) : this(target, opcode, emitArgMarker) + public IRCall(BasicBlock block, OpcodeCall opcode, bool emitArgMarker, IEnumerable arguments) : this(block, opcode, emitArgMarker) { Arguments.AddRange(arguments); } - public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, params IRValue[] arguments) : this(target, opcode, emitArgMarker) + public IRCall(BasicBlock block, OpcodeCall opcode, bool emitArgMarker, params IInterimOperand[] arguments) : this(block, opcode, emitArgMarker) { Arguments.AddRange(arguments); } @@ -793,30 +849,48 @@ public override bool Equals(object obj) Arguments.SequenceEqual(call.Arguments); public override int GetHashCode() => Function.ToLower().GetHashCode(); + + public InterimConstantValue Evaluate() + { + if (!IsInvariant) + throw new InvalidOperationException(); + + string functionName = Function.Replace("()", ""); + Optimization.InterimCPU interimCPU = Optimization.Optimizer.InterimCPU; + interimCPU.Boot(); // Clear the stack out of caution. + interimCPU.PushArgumentStack(new Execution.KOSArgMarkerType()); + foreach (IInterimOperand arg in Arguments) + { + object argValue = (arg as IEvaluatableToConstant)?.Evaluate().Value + ?? throw new ArgumentNullException(arg.ToString()); + interimCPU.PushArgumentStack(argValue); + } + Optimization.Optimizer.FunctionManager.CallFunction(functionName); + return new InterimConstantValue(interimCPU.PopValueArgument(), this); + } } public class IRReturn : SingleOperandInstruction { public override bool IsInvariant => Value.IsInvariant; - public IRValue Value { get => operand; set => operand = value; } + public IInterimOperand Value { get => operand; set => operand = value; } public short Depth { get; internal set; } - public IRReturn(short depth, OpcodeReturn opcode) : base(opcode) + public IRReturn(BasicBlock block, short depth, OpcodeReturn opcode) : base(opcode, block) => Depth = depth; - internal override IEnumerable EmitOpcode() + public override IEnumerable EmitOpcodes() { if (Value != null) - foreach (Opcode opcode in Value.EmitPush()) + foreach (Opcode opcode in Value.EmitOpcodes()) yield return opcode; else yield return SetSourceLocation(new OpcodePush(null)); yield return SetSourceLocation(new OpcodeReturn(Depth)); } public override string ToString() - => string.Format("{return {0} deep}", Depth); + => string.Format("{{return {0} deep}}", Depth); public override bool Equals(object obj) => obj is IRReturn ret && - Depth == ret.Depth && Value.Equals(ret.Value); public override int GetHashCode() - => base.GetHashCode(); + => Value.GetHashCode(); } } diff --git a/src/kOS.Safe/Compilation/IR/IRScope.cs b/src/kOS.Safe/Compilation/IR/IRScope.cs index f6e9b7cdb..0430c361a 100644 --- a/src/kOS.Safe/Compilation/IR/IRScope.cs +++ b/src/kOS.Safe/Compilation/IR/IRScope.cs @@ -9,12 +9,10 @@ namespace kOS.Safe.Compilation.IR /// public class IRScope { - private readonly Dictionary variables = - new Dictionary(StringComparer.OrdinalIgnoreCase); private IRScope parent; private readonly HashSet childScopes = new HashSet(); private readonly HashSet blocks = new HashSet(); - private readonly Dictionary functionRefs = new Dictionary(); + private readonly HashSet variables = new HashSet(); private int nextChildIndex = 0; private int index; @@ -57,18 +55,7 @@ public IRScope ParentScope /// /// Gets the collection of variables associated with this scope. /// - public IReadOnlyCollection Variables => variables.Values; - /// - /// Gets the collection of variable names associated with this - /// scope. - /// - public IReadOnlyCollection VariableNames => variables.Keys; - /// - /// Gets the collection of variables written to within this scope. - /// This differs from in that it can - /// contain multiple SSA variables of the same base name. - /// - public HashSet VariablesWritten { get; } = new HashSet(); + public IReadOnlyCollection Variables => variables; /// /// Gets a value indicating whether this instance represents the /// global scope. @@ -81,84 +68,43 @@ public IRScope ParentScope /// simultaneously. The global scope implicitly contains every /// variable name. /// - public bool IsGlobalScope { get; internal set; } = false; + public bool IsGlobalScope => ParentScope == null; /// /// Initializes a new instance of the class. /// /// The parent scope. - public IRScope(IRScope parent) + /// The first block in this scope. + public IRScope(IRScope parent, BasicBlock headerBlock) { ParentScope = parent; + HeaderBlock = headerBlock; } - public void StoreLocalVariable(IRVariableBase variable) + public void StoreLocalVariable(string variableName) { - variables[variable.Name] = variable; + variables.Add(variableName); } - public void StoreGlobalVariable(IRVariableBase variable) + public void StoreGlobalVariable(string variableName) { if (!IsGlobalScope) - ParentScope.StoreGlobalVariable(variable); + ParentScope.StoreGlobalVariable(variableName); else - StoreLocalVariable(variable); - } - public void StoreVariable(IRVariableBase variable) - { - if (!TryStoreVariable(variable)) - StoreGlobalVariable(variable); - } - public bool TryStoreVariable(IRVariableBase variable) - { - if (variables.ContainsKey(variable.Name)) - { - StoreLocalVariable(variable); - return true; - } - return ParentScope?.TryStoreVariable(variable) ?? false; - } - public void UnsetVariable(string variable) - { - if (variables.ContainsKey(variable)) - variables.Remove(variable); - else - ParentScope?.UnsetVariable(variable); - } - - public void EnrollFunction(string variable, string functionRef) - { - functionRefs[variable] = functionRef.Split('-').First(); - } - public string GetFunctionNameFromVariable(string variable) - { - if (GetScopeForVariableNamed(variable).functionRefs.TryGetValue(variable, out var functionRef)) - return functionRef; - return null; - } - - public IRVariableBase GetVariableNamed(string name, bool includeParent = true) - { - if (variables.ContainsKey(name)) - return variables[name]; - return includeParent ? ParentScope?.GetVariableNamed(name, includeParent) : null; + StoreLocalVariable(variableName); } public bool IsVariableInScope(string name, bool includeParent = true) { - if (variables.ContainsKey(name)) + if (variables.Contains(name)) return true; return includeParent && (ParentScope?.IsVariableInScope(name, includeParent) ?? false); } - public bool IsVariableInScope(IRVariableBase variable) - { - return variable.Scope.IsEqualOrEncompassedBy(this); - } public IRScope GetScopeForVariableNamed(string name) { if (IsGlobalScope) return this; - if (variables.ContainsKey(name)) + if (variables.Contains(name)) return this; return ParentScope.GetScopeForVariableNamed(name); } @@ -167,8 +113,6 @@ public void ClearVariable(string name) { variables.Remove(name); } - public void ClearVariable(IRVariableBase variable) - => ClearVariable(variable.Name); public bool IsEqualOrEncompassedBy(IRScope scope) => this == scope || IsEncompassedBy(scope); @@ -178,7 +122,7 @@ public bool IsEncompassedBy(IRScope scope) return true; if (scope.childScopes.Contains(this)) return true; - return ParentScope?.IsEncompassedBy(scope) ?? false; + return IsGlobalScope || ParentScope.IsEncompassedBy(scope); } public IRScope GetGlobalScope() @@ -200,7 +144,7 @@ public override string ToString() => $"IRScope: {IndexString()}"; public string IndexString() { - if (IsGlobalScope || ParentScope == null) + if (IsGlobalScope) return "Global"; if (ParentScope.IsGlobalScope) return $"{index}"; diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs deleted file mode 100644 index eb9819fad..000000000 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ /dev/null @@ -1,358 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace kOS.Safe.Compilation.IR -{ - public abstract class IRValue - { - public virtual Type ValueType { get; set; } = typeof(Encapsulation.Structure); - public abstract bool IsInvariant { get; set; } - internal abstract IEnumerable EmitPush(); - } - - public class IRConstant : IRValue - { - protected readonly short sourceLine, sourceColumn; - - public override bool IsInvariant - { - get => true; - set - { - if (value) - return; - throw new InvalidOperationException("Cannot set the invariant state of a constant to false."); - } - } - public object Value { get; } - public IRConstant(object value, Opcode opcode) : this(value, opcode.SourceLine, opcode.SourceColumn) { } - public IRConstant(object value, IRInstruction instruction) : this(value, instruction.SourceLine, instruction.SourceColumn) { } - public IRConstant(object value, short sourceLine, short sourceColumn) - { - Value = value; - this.sourceLine = sourceLine; - this.sourceColumn = sourceColumn; - ValueType = value.GetType(); - } - internal override IEnumerable EmitPush() - { - yield return new OpcodePush(Value) - { - SourceLine = sourceLine, - SourceColumn = sourceColumn - }; - } - public override bool Equals(object obj) - => obj is IRConstant constant && Value.Equals(constant.Value) || Value.Equals(obj); - public override int GetHashCode() - => Value.GetHashCode(); - public override string ToString() - => Value.ToString(); - } - public abstract class IRVariableBase : IRValue - { - public string Name { get; } - public virtual IRScope Scope { get; protected set; } - public IRVariableBase(string name, IRScope declaringScope) - { - Name = name; - Scope = declaringScope; - } - - public void RedefineScope(IRScope newScope) - => Scope = newScope; - - public override string ToString() - => $"{Name} {Scope.IndexString()}"; - protected bool NameAndScopeEquals(IRVariableBase variable) - => string.Equals(Name, variable.Name, StringComparison.OrdinalIgnoreCase) && - (Scope.IsEqualOrEncompassedBy(variable.Scope) || - variable.Scope.IsEncompassedBy(Scope)); - protected int GetBaseHashCode() - => Name.ToLower().GetHashCode(); - } - public class IRVariable : IRVariableBase - { - private ushort nextSSAIndex = 0; - private readonly Dictionary iterations = new Dictionary(); - internal readonly short sourceLine, sourceColumn; - - public override bool IsInvariant { get; set; } = false; - public bool IsLock { get; } - public IReadOnlyDictionary Iterations => iterations; - - public IRVariable(OpcodeIdentifierBase opcode, IRScope scope, bool isLock = false) : - this(opcode.Identifier, scope, opcode, isLock) { } - public IRVariable(string name, IRScope scope, Opcode opcode, bool isLock = false) : - this(name, scope, opcode.SourceLine, opcode.SourceColumn, isLock) { } - public IRVariable(string name, IRScope scope, short sourceLine, short sourceColumn, bool isLock = false) : - base(name, scope) - { - IsLock = isLock; - this.sourceLine = sourceLine; - this.sourceColumn = sourceColumn; - } - - public virtual SSAVariable GetNewSSAVariable() - { - SSAVariable nextIteration = new SSAVariable(this, nextSSAIndex); - iterations[nextSSAIndex] = nextIteration; - nextSSAIndex += 1; - return nextIteration; - } - - public virtual PhiVariable GetNewPhiVariable() - { - PhiVariable phiVariable = new PhiVariable(this, nextSSAIndex); - iterations[nextSSAIndex] = phiVariable; - nextSSAIndex += 1; - return phiVariable; - } - - internal override IEnumerable EmitPush() - { - yield return new OpcodePush(Name) - { - SourceLine = sourceLine, - SourceColumn = sourceColumn - }; - } - public override bool Equals(object obj) - => !(obj is SSAVariable) && - obj is IRVariable variable && - NameAndScopeEquals(variable); - public override int GetHashCode() - => GetBaseHashCode(); - } - public class SSAVariable : IRVariable - { - private readonly ushort ssaIndex; - - public override bool IsInvariant - { - get => AssignedAt.IsInvariant; - set => throw new InvalidOperationException($"Cannot set the invariant state of an {nameof(SSAVariable)}."); - } - public IRVariable Parent { get; } - public IRAssign AssignedAt { get; set; } - internal SSAVariable(IRVariable baseVariable, ushort ssaIndex) : - base(baseVariable.Name, baseVariable.Scope, baseVariable.sourceLine, baseVariable.sourceColumn) - { - this.ssaIndex = ssaIndex; - Parent = baseVariable; - ValueType = baseVariable.ValueType; - } - - public override SSAVariable GetNewSSAVariable() - => Parent.GetNewSSAVariable(); - public override PhiVariable GetNewPhiVariable() - => Parent.GetNewPhiVariable(); - - public override string ToString() - { - string baseString = base.ToString(); - return baseString.Insert(baseString.IndexOf(" "), $".{ssaIndex}"); - } - public override bool Equals(object obj) - => obj is SSAVariable ssaVariable && - ssaIndex == ssaVariable.ssaIndex && - NameAndScopeEquals(ssaVariable); - public override int GetHashCode() - => (ssaIndex, GetBaseHashCode()).GetHashCode(); - } - - public class PhiVariable : SSAVariable, IMultipleOperandInstruction - { - public Dictionary PossibleValues { get; } = new Dictionary(); - public override bool IsInvariant - { - get - { - IEnumerable> reachableValues = - PossibleValues.Where(kvp => kvp.Key.IsExecutable); - // Return true if there is exactly one reachable value and it is invariant. - return reachableValues.Any() && !reachableValues.Skip(1).Any() && reachableValues.First().Value.IsInvariant; - } - set => throw new InvalidOperationException(); - } - - IEnumerable IMultipleOperandInstruction.Operands => PossibleValues.Values; - - int IMultipleOperandInstruction.OperandCount => PossibleValues.Count; - - internal PhiVariable(IRVariable baseVariable, ushort ssaIndex) : base(baseVariable, ssaIndex) { } - - public void RefreshType() - { - Type proposedType = PossibleValues.Values.First().ValueType; - - foreach (SSAVariable variable in PossibleValues.Values.Skip(1)) - proposedType = GetFirstCommonBaseType(proposedType, variable.ValueType); - - ValueType = proposedType; - } - - public static Type GetFirstCommonBaseType(Type typeA, Type typeB) - { - if (typeA == null || typeB == null) return null; - - Type current = typeA; - while (current != null) - { - if (current.IsAssignableFrom(typeB)) - { - return current; - } - current = current.BaseType; - } - - throw new Exceptions.KOSYouShouldNeverSeeThisException($"Couldn't find a base class between {typeA} and {typeB}, when all kOS types should derive from {nameof(Encapsulation.Structure)}."); - } - - void IOperandInstructionBase.ForEachOperand(Action action) - { - foreach (SSAVariable variable in PossibleValues.Values) - action(variable); - } - - void IOperandInstructionBase.MutateEachOperand(Func mutateFunc) - { - foreach (BasicBlock block in PossibleValues.Keys.ToArray()) - PossibleValues[block] = (SSAVariable)mutateFunc(PossibleValues[block]); - } - } - - public class IRRelocateLater : IRConstant - { - public IRRelocateLater(string value, OpcodePushRelocateLater opcode) : base(value, opcode) - { - // Not technically correct, but this removes it from any optimization. - ValueType = null; - } - - internal override IEnumerable EmitPush() - { - yield return new OpcodePushRelocateLater((string)Value) - { - SourceLine = sourceLine, - SourceColumn = sourceColumn - }; - } - } - - public class IRDelegateRelocateLater : IRRelocateLater - { - public bool WithClosure { get; } - public IRDelegateRelocateLater(string value, bool withClosure, OpcodePushDelegateRelocateLater opcode) : base(value, opcode) - { - WithClosure = withClosure; - } - internal override IEnumerable EmitPush() - { - yield return new OpcodePushDelegateRelocateLater((string)Value, WithClosure) - { - SourceLine = sourceLine, - SourceColumn = sourceColumn - }; - } - } - - public class IRTemp : IRVariableBase - { - private bool isPromoted = false; - - public override Type ValueType - { - get => ((IResultingInstruction)Parent).ResultType; - set => throw new InvalidOperationException($"Cannot set the result type state of an {nameof(IRTemp)}."); - } - public int ID { get; } - public IRInstruction Parent { get; set; } - public override bool IsInvariant - { - get => Parent.IsInvariant; - set => throw new InvalidOperationException($"Cannot set the invariant state of an {nameof(IRTemp)}."); - } - - public IRTemp(int id) : base($"$.temp.{id}", null) - { - ID = id; - Scope = null; - } - public IRAssign PromoteToVariable(IRScope scope) - { - isPromoted = true; - Scope = scope; - return new IRAssign(new OpcodeStoreLocal(Name) - { - SourceLine = -1, - SourceColumn = 0 - }, - this, - this) - { - Scope = IRAssign.StoreScope.Local - }; - } - internal override IEnumerable EmitPush() - { - if (isPromoted) - yield return new OpcodePush(Name) - { - SourceLine = -1, - SourceColumn = 0 - }; - else - foreach (Opcode opcode in Parent.EmitOpcode()) - yield return opcode; - } - - public override string ToString() - { - if (isPromoted) - return base.ToString(); - return $"| {Parent}"; - } - public override bool Equals(object obj) - { - if (obj is IRTemp temp) - { - /*if (isPromoted) - { - return temp.isPromoted && - Scope == temp.Scope && - string.Equals(Name, temp.Name, System.StringComparison.OrdinalIgnoreCase); - }*/ - return Parent.Equals(temp.Parent); - } - if (obj is IRVariable variable) - { - return isPromoted && - NameAndScopeEquals(variable); - } - return false; - } - public override int GetHashCode() - => GetBaseHashCode(); - } - public class IRParameter : IRValue - { - public override Type ValueType - { - get => typeof(Encapsulation.Structure); - set => throw new InvalidOperationException("Cannot set the value type of a parameter."); - } - public override bool IsInvariant - { - get => false; - set - { - if (!value) - return; - throw new InvalidOperationException("Cannot set the invariant state of a parameter to true."); - } - } - internal override IEnumerable EmitPush() => System.Linq.Enumerable.Empty(); - } -} diff --git a/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs index c9f6e72a6..a7065b5b4 100644 --- a/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs @@ -5,17 +5,28 @@ namespace kOS.Safe.Compilation.IR /// /// Represents instructions that push a value back onto the stack. /// - public interface IResultingInstruction + public interface IResultingInstruction : IEvaluatableToConstant, IInterimOperand + { + } + + public interface IEvaluatableToConstant { /// - /// Gets the representation of the value that would be pushed - /// to the stack. + /// Gets a value indicating whether this instance is invariant. + /// That is, if the value of the operand can be known at + /// compile time. /// - IRValue Result { get; } + /// + /// true if this instance is invariant; otherwise, false. + /// + bool IsInvariant { get; } + /// - /// Gets the of the result, for type - /// inferencing purposes. + /// Evaluates the instruction to a constant value. /// - Type ResultType { get; } + /// This method should only be valid if is true. + /// A constant value that equates to the value of the runtime result of this instruction. + /// If this instruction is not invariant. + InterimConstantValue Evaluate(); } } \ No newline at end of file diff --git a/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs b/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs new file mode 100644 index 000000000..aa99c6e4f --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; + +namespace kOS.Safe.Compilation.IR +{ + public class InterimConstantValue : IInterimOperand, IEvaluatableToConstant + { + protected readonly short sourceLine, sourceColumn; + + public virtual bool IsInvariant => true; + public object Value { get; } + public Type Type { get; protected set; } + public bool IsPrimitive { get => Type.IsPrimitive || typeof(Encapsulation.PrimitiveStructure).IsAssignableFrom(Type); } + public InterimConstantValue(object value, Opcode opcode) : this(value, opcode.SourceLine, opcode.SourceColumn) { } + public InterimConstantValue(object value, IRInstruction instruction) : this(value, instruction.SourceLine, instruction.SourceColumn) { } + public InterimConstantValue(object value, short sourceLine, short sourceColumn) + { + Value = value; + this.sourceLine = sourceLine; + this.sourceColumn = sourceColumn; + Type = value.GetType(); + } + public virtual IEnumerable EmitOpcodes() + { + yield return new OpcodePush(Value) + { + SourceLine = sourceLine, + SourceColumn = sourceColumn + }; + } + public override bool Equals(object obj) + => obj is InterimConstantValue constant && Value.Equals(constant.Value) || Value.Equals(obj); + public override int GetHashCode() + => Value.GetHashCode(); + public override string ToString() + => Value.ToString(); + + public InterimConstantValue Evaluate() + => this; + } + + public class IRRelocateLater : InterimConstantValue + { + public IRRelocateLater(string value, OpcodePushRelocateLater opcode) : base(value, opcode) + { + // Not technically correct, but this removes it from any optimization. + Type = null; + } + + public override IEnumerable EmitOpcodes() + { + yield return new OpcodePushRelocateLater((string)Value) + { + SourceLine = sourceLine, + SourceColumn = sourceColumn + }; + } + } + + public class IRDelegateRelocateLater : IRRelocateLater + { + public bool WithClosure { get; } + public IRDelegateRelocateLater(string value, bool withClosure, OpcodePushDelegateRelocateLater opcode) : base(value, opcode) + { + WithClosure = withClosure; + } + public override IEnumerable EmitOpcodes() + { + yield return new OpcodePushDelegateRelocateLater((string)Value, WithClosure) + { + SourceLine = sourceLine, + SourceColumn = sourceColumn + }; + } + } + + public class IRParameter : IInterimOperand + { + public Type Type => null;//typeof(Encapsulation.Structure); + public bool IsInvariant => false; + public IEnumerable EmitOpcodes() => System.Linq.Enumerable.Empty(); + } +} diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs new file mode 100644 index 000000000..41c289688 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -0,0 +1,493 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR +{ + public interface IInterimVariableReference : IInterimOperand + { + string Name { get; } + } + public readonly struct InterimVariableReference : IInterimVariableReference + { + public short SourceLine { get; } + public short SourceColumn { get; } + public string Name { get; } + public bool IsInvariant => false; + + public Type Type => typeof(Encapsulation.Structure); + + public InterimVariableReference(string name, Opcode opcode) : + this(name, opcode.SourceLine, opcode.SourceColumn) { } + public InterimVariableReference(string name, short sourceLine, short sourceColumn) + { + Name = name; + SourceLine = sourceLine; + SourceColumn = sourceColumn; + } + + public IEnumerable EmitOpcodes() + { + yield return new OpcodePush(Name) + { + SourceLine = SourceLine, + SourceColumn = SourceColumn + }; + } + + public override string ToString() + => $"{Name}"; + public override bool Equals(object obj) + => obj is InterimVariableReference variable && + string.Equals(Name, variable.Name, StringComparison.OrdinalIgnoreCase); + public override int GetHashCode() + => Name.ToLower().GetHashCode(); + } + + public readonly struct InterimVariableReference : IInterimVariableReference, IEvaluatableToConstant where T : SSADefinition + { + public short SourceLine { get; } + public short SourceColumn { get; } + public T Reference { get; } + public string Name => Reference.Name; + public bool IsInvariant => Reference.IsInvariant; + public Type Type => Reference.Type; + + public InterimVariableReference(T reference, short sourceLine, short sourceColumn) + { + Reference = reference; + SourceLine = sourceLine; + SourceColumn = sourceColumn; + } + public InterimVariableReference(T reference, InterimVariableReference oldRef) : + this(reference, oldRef.SourceLine, oldRef.SourceColumn) { } + + public IEnumerable EmitOpcodes() + { + yield return new OpcodePush(Name) + { + SourceLine = SourceLine, + SourceColumn = SourceColumn + }; + } + + public InterimConstantValue Evaluate() + { + if (!IsInvariant) + throw new InvalidOperationException(); + return Reference.Evaluate(); + } + + public override string ToString() + => $"{Name}"; + public override bool Equals(object obj) + => obj is InterimVariableReference variable && + Reference.Equals(variable.Reference); + public override int GetHashCode() + => Reference.GetHashCode(); + } + + internal static class SSAIndexIssuer + { + private static readonly Dictionary indices = new Dictionary(); + public static uint GetIndex(string name) + { + if (indices.ContainsKey(name)) + return ++indices[name]; + indices[name] = 0; + return 0; + } + } + + public abstract class SSADefinition + { + protected readonly uint ssaIndex; + protected Dictionary potentialUnsetSites; + protected readonly Dictionary potentialClobberDefinitions = + new Dictionary(); + + public enum SetState + { + Set = 1, + PotentiallyUnset = 0, + Unset = -1 + } + + public string Name { get; } + public abstract bool IsInvariant { get; } + public abstract Type Type { get; } + public virtual SetState State { get; } + public IRInstruction AssignedAt { get; } + protected SSADefinition(string name) + { + Name = name; + ssaIndex = SSAIndexIssuer.GetIndex(name); + } + protected SSADefinition(string name, IRInstruction assignedAt) : this(name) + { + AssignedAt = assignedAt; + } + protected SSADefinition(string name, SetState state, IRInstruction assignedAt) : this(name, assignedAt) + { + State = state; + } + + public abstract SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt); + public abstract InterimConstantValue Evaluate(); + + public SSADefinition PotentiallyOverwrite(SSASetDefinition newDefinition) + { + if (State == SetState.Unset) + return this; + if (newDefinition.State == SetState.Unset) + throw new ArgumentException($"{nameof(newDefinition)} must not be definitively unset."); + if (newDefinition.Equals(this)) + return this; + if (potentialClobberDefinitions.TryGetValue(newDefinition, out SSAPotentialDefinition result)) + return result; + result = new SSAPotentialDefinition(this, newDefinition); + potentialClobberDefinitions[newDefinition] = result; + return result; + } + + protected static string SetState_ToString(SetState state) + { + switch (state) + { + default: + case SetState.Set: + return "#"; + case SetState.PotentiallyUnset: + return "?"; + case SetState.Unset: + return "⊥"; + } + } + public override string ToString() + => $"{Name} {SetState_ToString(State)}{ssaIndex}"; + /*public override bool Equals(object obj) + => obj == this; + public override int GetHashCode() + => (ssaIndex, Name).GetHashCode();*/ + } + + public class SSASetDefinition : SSADefinition + { + private static readonly Dictionary<(string, IRCall), SSASetDefinition> postCallDefinitions = + new Dictionary<(string, IRCall), SSASetDefinition>(); + + public IRAssign DefinedAt { get; } + public override bool IsInvariant => State != SetState.PotentiallyUnset && DefinedAt.IsInvariant && AssignedAt.IsInvariant; + public override Type Type { get; } + + public SSASetDefinition(string name, IRAssign assignedAt) : base(name, SetState.Set, assignedAt) + { + DefinedAt = assignedAt; + potentialUnsetSites = new Dictionary(); + Type = DefinedAt.Value.Type; + } + public SSASetDefinition(string name, IRUnset unsetAt) : base(name, SetState.Unset, unsetAt) + { + Type = null; + } + private SSASetDefinition(string name, IRCall assignedIn) : base(name, SetState.Set, assignedIn) + { + Type = typeof(Encapsulation.Structure); + } + public static SSASetDefinition FromCallSite(string name, IRCall assignedIn) + { + if (postCallDefinitions.TryGetValue((name, assignedIn), out SSASetDefinition result)) + return result; + result = new SSASetDefinition(name, assignedIn); + postCallDefinitions[(name, assignedIn)] = result; + return result; + } + private SSASetDefinition(SSASetDefinition definition, IRUnset potentiallyUnsetAt) : + base(definition.Name, SetState.PotentiallyUnset, potentiallyUnsetAt) + { + DefinedAt = definition.DefinedAt; + potentialUnsetSites = definition.potentialUnsetSites; + } + public override SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt) + { + if (State == SetState.Unset) + return this; + + if (potentialUnsetSites.TryGetValue(potentiallyUnsetAt, out SSADefinition result)) + return result; + + result = new SSASetDefinition(this, potentiallyUnsetAt); + potentialUnsetSites[potentiallyUnsetAt] = result; + return result; + } + public override InterimConstantValue Evaluate() + => (DefinedAt.Value as IEvaluatableToConstant).Evaluate(); + } + public class SSAPotentialDefinition : SSADefinition, IMultipleOperandInstruction + { + private static readonly Dictionary<(IRUnset, SSASetDefinition), SSAPotentialDefinition> potentialSets = + new Dictionary<(IRUnset, SSASetDefinition), SSAPotentialDefinition>(); + + public SSADefinition Preceding { get; private set; } + public SSADefinition Succeeding { get; private set; } + public IRUnset Conditional { get; } + public override Type Type => State == SetState.Set ? + PhiNode.GetFirstCommonBaseType(Preceding.Type, Succeeding.Type) : null; + public override bool IsInvariant => Conditional?.IsInvariant ?? false; + + public IEnumerable Operands + { + get + { + short sourceLine = Conditional?.SourceLine ?? -1; + short sourceColumn = Conditional?.SourceColumn ?? -1; + yield return new InterimVariableReference(Preceding, sourceLine, sourceColumn); + yield return new InterimVariableReference(Succeeding, sourceLine, sourceColumn); + } + } + public int OperandCount => 2; + + internal SSAPotentialDefinition(SSADefinition preceding, SSASetDefinition succeeding) : + base(preceding.Name, (SetState)Math.Min((int)preceding.State, (int)succeeding.State), succeeding.AssignedAt) + { + if (!preceding.Name.Equals(succeeding.Name, StringComparison.OrdinalIgnoreCase)) + throw new ArgumentException("Preceding and suceeding definitions must share a name."); + if (succeeding.State != SetState.Set) + throw new ArgumentException("The succeeding definition must be definitively set."); + Preceding = preceding; + Succeeding = succeeding; + potentialUnsetSites = new Dictionary(); + Conditional = preceding.AssignedAt as IRUnset; + } + private SSAPotentialDefinition(SSAPotentialDefinition definition, IRUnset potentiallyUnsetAt) : + base(definition.Name, SetState.PotentiallyUnset, potentiallyUnsetAt) + { + Preceding = definition.Preceding; + Succeeding = definition.Succeeding; + potentialUnsetSites = definition.potentialUnsetSites; + Conditional = definition.Conditional; + } + private SSAPotentialDefinition(SSASetDefinition potentialDefinition, IRUnset condition) : + base(potentialDefinition.Name, SetState.PotentiallyUnset, potentialDefinition.AssignedAt) + { + Preceding = null; + Succeeding = potentialDefinition; + potentialUnsetSites = new Dictionary(); + Conditional = condition; + } + public static SSAPotentialDefinition PotentiallySet(SSASetDefinition potentialDefinition, IRUnset condition) + { + if (potentialSets.TryGetValue((condition, potentialDefinition), out var potentialSet)) + return potentialSet; + SSAPotentialDefinition result = new SSAPotentialDefinition(potentialDefinition, condition); + potentialSets[(condition, potentialDefinition)] = result; + return result; + } + + public override SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt) + { + if (State == SetState.Unset) + return this; + + if (potentialUnsetSites.TryGetValue(potentiallyUnsetAt, out SSADefinition result)) + return result; + + result = new SSAPotentialDefinition(this, potentiallyUnsetAt); + potentialUnsetSites[potentiallyUnsetAt] = result; + return result; + } + + public void ForEachOperand(Action action) + { + action(new InterimVariableReference(Preceding, 0, 0)); + action(new InterimVariableReference(Succeeding, 0, 0)); + } + public void MutateEachOperand(Func mutateFunc) + { + Preceding = Mutate(mutateFunc, Preceding); + Succeeding = Mutate(mutateFunc, Succeeding); + } + private static SSADefinition Mutate(Func func, SSADefinition definition) + { + IInterimOperand result = func(new InterimVariableReference(definition, 0, 0)); + return ((InterimVariableReference)result).Reference; + } + public override InterimConstantValue Evaluate() + { + if (!IsInvariant) + throw new InvalidOperationException(); + if (Conditional.IsExecutable) + return Succeeding.Evaluate(); + else + return Preceding.Evaluate(); + } + } + public class PhiVariable : SSADefinition + { + private readonly SetState internalSetState = SetState.Set; + + public PhiNode Node { get; } + public override SetState State + { + get + { + IEnumerable possibleValues = Node.PossibleValues.Values; + if (possibleValues.Any(v => v.State < SetState.Set)) + { + if (possibleValues.All(v => v.State == SetState.Unset)) + return SetState.Unset; + else + return SetState.PotentiallyUnset; + } + return internalSetState; + } + } + public override bool IsInvariant => Node.IsInvariant; + public override Type Type => Node.Type; + + public PhiVariable(string name, PhiNode node) : base(name) + { + Node = node; + } + + public PhiVariable(PhiVariable phiVariable, IRUnset potentiallyUnsetAt) : base(phiVariable.Name, potentiallyUnsetAt) + { + Node = phiVariable.Node; + internalSetState = SetState.PotentiallyUnset; + } + + public override SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt) + { + if (potentialUnsetSites.TryGetValue(potentiallyUnsetAt, out SSADefinition result)) + return result; + + result = new PhiVariable(this, potentiallyUnsetAt); + potentialUnsetSites[potentiallyUnsetAt] = result; + return result; + } + + public override InterimConstantValue Evaluate() + => Node.Evaluate(); + + public override string ToString() + => $"{Name} #{ssaIndex}"; + public override bool Equals(object obj) + => obj == this; + public override int GetHashCode() + => (ssaIndex, Name).GetHashCode(); + } + public class PhiNode : PhiNode + { + protected override bool ObjIsInvariant(SSADefinition obj) + => obj.IsInvariant; + protected override Type ObjType(SSADefinition obj) + => obj.Type; + public PhiVariable Result { get; } + + public PhiNode(string name) + { + Result = new PhiVariable(name, this); + } + protected override IEnumerable Operands => + PossibleValues.Select(kvp => + { + SSASetDefinition ssaDef = kvp.Value as SSASetDefinition; + short sourceLine = ssaDef?.DefinedAt.SourceLine ?? -1; + short sourceColumn = ssaDef?.DefinedAt.SourceColumn ?? -1; + return new InterimVariableReference(kvp.Value, sourceLine, sourceColumn) as IInterimOperand; + }); + + protected override InterimConstantValue EvaluateObj(SSADefinition obj) + => obj.Evaluate(); + + protected override void ForEachOperand(Action action) + { + foreach (SSADefinition variable in PossibleValues.Values) + action(new InterimVariableReference(variable, -1, -1)); + } + + protected override void MutateEachOperand(Func mutateFunc) + { + foreach (BasicBlock block in PossibleValues.Keys) + PossibleValues[block] = ((InterimVariableReference)mutateFunc(new InterimVariableReference(PossibleValues[block], -1, -1))).Reference; + } + + } + public abstract class PhiNode : IMultipleOperandInstruction + { + public bool IsInvariant + { + get + { + IEnumerable> reachableValues = + PossibleValues.Where(kvp => kvp.Key.IsExecutable); + if (!reachableValues.Any()) + return true; + // Return true if there is exactly one reachable value and it is invariant. + return reachableValues.Any() && !reachableValues.Skip(1).Any() && ObjIsInvariant(reachableValues.First().Value); + } + } + protected abstract bool ObjIsInvariant(T obj); + public Type Type + { + get + { + IEnumerable reachableValues = + PossibleValues.Where(kvp => kvp.Key.IsExecutable).Select(kvp => kvp.Value); + if (!reachableValues.Any()) + return null; + Type proposedType = ObjType(reachableValues.FirstOrDefault()); + foreach (T variable in reachableValues.Skip(1)) + proposedType = GetFirstCommonBaseType(proposedType, ObjType(variable)); + + return proposedType; + } + } + protected abstract Type ObjType(T obj); + public Dictionary PossibleValues { get; } = new Dictionary(); + + IEnumerable IMultipleOperandInstruction.Operands => Operands; + + protected abstract IEnumerable Operands { get; } + + int IMultipleOperandInstruction.OperandCount => PossibleValues.Count; + + public virtual InterimConstantValue Evaluate() + { + if (!IsInvariant) + throw new InvalidOperationException(); + T variable = PossibleValues.First(kvp => kvp.Key.IsExecutable).Value; + return EvaluateObj(variable); + } + protected abstract InterimConstantValue EvaluateObj(T obj); + + public static Type GetFirstCommonBaseType(Type typeA, Type typeB) + { + if (typeA == null || typeB == null) return null; + + Type current = typeA; + while (current != null) + { + if (current.IsAssignableFrom(typeB)) + { + return current; + } + current = current.BaseType; + } + +#if DEBUG + throw new Exceptions.KOSYouShouldNeverSeeThisException($"Couldn't find a base class between {typeA} and {typeB}, when all kOS types should derive from {nameof(Encapsulation.Structure)}."); +#else + return typeof(Encapsulation.Structure); +#endif + } + + void IOperandInstructionBase.ForEachOperand(Action action) + => ForEachOperand(action); + protected abstract void ForEachOperand(Action action); + + void IOperandInstructionBase.MutateEachOperand(Func mutateFunc) + => MutateEachOperand(mutateFunc); + protected abstract void MutateEachOperand(Func mutateFunc); + } +} diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index 74a9962f6..4141f26ae 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using kOS.Safe.Compilation.Optimization; +using static kOS.Safe.Compilation.IR.IRCodePart; namespace kOS.Safe.Compilation.IR { @@ -11,35 +12,96 @@ namespace kOS.Safe.Compilation.IR /// public static class SingleStaticAssignment { - private static readonly Dictionary> postCallSSAVariables = - new Dictionary>(); - private static readonly Dictionary postCallSSAVariableLinks = - new Dictionary(); + private static readonly Dictionary> reachableVariables = + new Dictionary>(); + + public static IReadOnlyDictionary> + ReachableVariables => reachableVariables; /// /// Finalizes a program into single static assignment form. /// public static void FinalizeSSA(IRCodePart codePart) { - foreach (BasicBlock block in codePart.Blocks) + Dictionary> funcCallTrees = new Dictionary>(); + Dictionary> callers = new Dictionary>(); + Queue functionQueue = new Queue(codePart.Functions.OrderBy(f => f.FunctionCalls.Count)); + + // Establish the call trees and ensure that all propagated effects are current. + while (functionQueue.Count > 0) { - AnalyzeBlock(block, codePart); + IRFunction function = functionQueue.Dequeue(); + HashSet functionCalls = new HashSet(function.FunctionCalls); + + foreach (BasicBlock root in function.RootBlocks) + BuildPhis(root, codePart, function); + + FlattenCallTree(function); + + if (funcCallTrees.TryGetValue(function, out HashSet cachedCalls)) + { + if (cachedCalls.SetEquals(functionCalls)) + continue; + funcCallTrees[function].UnionWith(function.FunctionCalls); + } + else + funcCallTrees.Add(function, new HashSet(function.FunctionCalls)); + + foreach (IRFunction callee in function.FunctionCalls) + { + if (callers.TryGetValue(callee, out HashSet callerSet)) + callerSet.Add(function); + else + callers.Add(callee, new HashSet() { function }); + } + if (callers.ContainsKey(function)) + { + foreach (IRFunction caller in callers[function]) + { + functionQueue.Enqueue(caller); + } + } } + // At this point, all functions have reached a stable point. - foreach (BasicBlock root in codePart.RootBlocks) + // Triggers may create triggers, but don't call them in the same + // way that functions can call functions. A single pass is sufficient. + foreach (IRTrigger trigger in codePart.Triggers) { - BuildPhis(root); + BuildPhis(trigger.RootBlock, codePart, trigger); + FlattenCallTree(trigger); } - foreach (BasicBlock block in codePart.Blocks) + // Now the main code can be SSA'd. + if (codePart.MainCode.Count > 0) + BuildPhis(codePart.MainCode[0], codePart, null); + + // Then apply the SSA definitions to all the variable push operations. + foreach (IRFunction function in codePart.Functions) + { + foreach (BasicBlock block in function.InitializationCode) + ApplyUses(block, codePart, function); + foreach (IRFunction.IRFunctionFragment fragment in function.Fragments) + foreach (BasicBlock block in fragment.FunctionCode) + ApplyUses(block, codePart, function); + } + foreach (IRTrigger trigger in codePart.Triggers) { - ApplyUses(block, codePart.Triggers); + foreach (BasicBlock block in trigger.Code) + ApplyUses(block, codePart, trigger); } + foreach (BasicBlock block in codePart.MainCode) + ApplyUses(block, codePart, null); } - private static void AnalyzeBlock(BasicBlock block, IRCodePart codePart) + private static Dictionary<(string VariableName, IRScope Scope), SSADefinition> AnalyzeBlock(BasicBlock block, IRCodePart codePart, IClosureVariableUser funcOrTrigger) { - HashSet variables = new HashSet(); + Dictionary<(string Name, IRScope Scope), SSADefinition> variables = + new Dictionary<(string, IRScope), SSADefinition>(); + + foreach (KeyValuePair<(string, IRScope), SSADefinition> varIn in block.IncomingVariableDefinitions) + variables.Add(varIn.Key, varIn.Value); + List instructions = block.Instructions; for (int i = 0; i < instructions.Count; i++) { @@ -47,96 +109,322 @@ private static void AnalyzeBlock(BasicBlock block, IRCodePart codePart) foreach (IRCall call in instruction.DepthFirst().Where(inst => inst is IRCall).Cast()) { - string functionIdentifier = block.Scope.GetFunctionNameFromVariable(call.Function); - IRCodePart.IRFunction function = codePart.GetFunction(functionIdentifier); + ProcessCall(call, codePart, variables, block.TriggerPropagationBlacklist, funcOrTrigger); + IRFunction function = codePart.GetFunction(call); if (function != null) - { - HashSet ssaVariables = new HashSet(); - foreach (SSAVariable variable in function.ExternalWrites) - { - SSAVariable ssaVariable = variable.GetNewSSAVariable(); - postCallSSAVariableLinks.Add(variable, ssaVariable); - OverwriteVariable(variables, ssaVariable); - ssaVariables.Add(ssaVariable); - } - if (postCallSSAVariables.TryGetValue(call, out IEnumerable storedVariables)) - ssaVariables.UnionWith(storedVariables); - postCallSSAVariables[call] = ssaVariables; - } + funcOrTrigger?.FunctionCalls.Add(codePart.GetFunction(call)); + } switch (instruction) { case IRAssign assignment: - OverwriteVariable(variables, (SSAVariable)assignment.Target); - // On encountering a PushRelocateLater, note the scope for its closure variables. - if (assignment.Value is IRRelocateLater lockOrFunctionPointer) - { - string pointer = ((string)lockOrFunctionPointer.Value).Split('-').First(); - // Re-scope the stored variables from Global to the current scope. - IRCodePart.IRFunction function = codePart.GetFunction(pointer); - if (function != null) - IRCodePart.SetDefiningScope(function, block.Scope); - } + if (ProcessAssignment(assignment, codePart, variables, block.TriggerPropagationBlacklist)) + funcOrTrigger?.ExternalWrites.Add(assignment.Target.Name); + break; + case IRUnset unset: + ProcessUnset(unset, variables, funcOrTrigger?.ExternalUnsets); break; - case IRUnaryConsumer unaryConsumer: - // On encountering a trigger: - // Clear the cache of any variables that are written in that trigger or body. - // Blacklist any variables that are written in the trigger or body. + // On encountering a trigger, blacklist any variables that are written in the trigger or body. + // Potentially apply any unsets in the trigger body. if (unaryConsumer.Operation is OpcodeAddTrigger) { - string pointer = (string)((IRConstant)unaryConsumer.Operand).Value; - // Re-scope the stored variables from Global to the current scope. - IRCodePart.IRTrigger trigger = codePart.GetTrigger(pointer); - if (trigger != null) - IRCodePart.SetDefiningScope(trigger, block.Scope); - block.TriggerPropagationBlacklist.UnionWith(trigger.ExternalWrites); - } - // On encountering an unset operation: - // Remove the affected definition from the stored variables. - // If the affected definition cannot be determined, clear all current variables - if (unaryConsumer.Operation is OpcodeUnset) - { - if (!unaryConsumer.IsInvariant) - { - variables.Clear(); - } - else - { - IRConstant unsetConst = unaryConsumer.Operand as IRConstant; - if (unsetConst == null && unaryConsumer.Operand is IRTemp temp) - unsetConst = Optimization.Passes.ConstantFolding.AttemptReduction(temp.Parent) as IRConstant; - if (unsetConst != null && - (unsetConst.Value is Encapsulation.StringValue || - unsetConst.Value is string)) - { - /*IRVariableBase unsetVar = block.Scope.GetVariableNamed( - (Encapsulation.StringValue)unsetConst.Value); - if (unsetVar != null) - variables.RemoveWhere(v => v.Parent.Equals(unsetVar));*/ - variables.RemoveWhere(v => - v.Name.Equals((Encapsulation.StringValue)unsetConst.Value, - StringComparison.OrdinalIgnoreCase)); - } - else - // TODO: See if this can be optimized through the SCCP pass. - // Probably not worth it since unsets are rather rare, - // and unsets without a string const even more so. - variables.Clear(); - } + string pointer = (string)((InterimConstantValue)unaryConsumer.Operand).Value; + IRTrigger trigger = codePart.GetTrigger(pointer); + ProcessTrigger(trigger, variables, block.TriggerPropagationBlacklist); + funcOrTrigger?.TriggersCreated.Add(trigger); } break; } } - block.VariablesWritten.Clear(); - block.VariablesWritten.UnionWith(variables); + return variables; + } + + private static bool ProcessAssignment(IRAssign assignment, IRCodePart codePart, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> blacklist) + { + IRScope startingScope = assignment.Block.Scope; + SSASetDefinition definition = assignment.Target; + switch (assignment.Scope) + { + case IRAssign.StoreScope.Local: + variables[(definition.Name, startingScope)] = definition; + // IRFunction.IsGlobal initiates as false, so there's no need to |= false. + break; + case IRAssign.StoreScope.Global: + variables[(definition.Name, startingScope.GetGlobalScope())] = definition; + SetFunctionToGlobal(assignment, codePart, variables, blacklist); + break; + default: + if (ApplyDefinitionToName(definition, startingScope, variables)) + { + // This isn't necessarily true in the case of nested function definitions. + // But true function definitions are definitively set as local or global. + // This only applies to a nested lock, which deserves to lose out on optimizations. + SetFunctionToGlobal(assignment, codePart, variables, blacklist); + return true; + } + break; + } + return false; + } + private static bool ApplyDefinitionToName(SSASetDefinition definition, IRScope scope, Dictionary<(string VariableName, IRScope Scope), SSADefinition> variables) + { + string name = definition.Name; + bool potential = false; + SSADefinition lastPotential = null; + while (scope != null) + { + // If there is no variable slot at this scope for this name, escalate one scope level. + // Similarly if there is a slot but it's unset. + if (!variables.TryGetValue((definition.Name, scope), out SSADefinition slotValue) || + slotValue.State == SSADefinition.SetState.Unset) + { + // If this is already the global scope, store the definition and break. + if (scope.IsGlobalScope) + { + if (potential) + variables[(name, scope)] = SSAPotentialDefinition.PotentiallySet(definition, (IRUnset)lastPotential.AssignedAt); + else + variables[(name, scope)] = definition; + // Since the SSA algorithm for a function won't know + // of any slots filled between the function's top and the + // global scope, this is where propagation to the higher + // scope is appropriate. + // Save the loop check, we know there's no higher scope. + return true; + } + scope = scope.ParentScope; + continue; + } + // If the slot is in a potentially unset state, put it at the new definition's + // potentially unset state. + // Also recall that we've done this because further values become only potentially overwritten. + if (slotValue.State == SSADefinition.SetState.PotentiallyUnset) + { + if (potential) + variables[(name, scope)] = slotValue.PotentiallyOverwrite(definition); + else + variables[(name, scope)] = definition.PotentiallyUnset((IRUnset)slotValue.AssignedAt); + potential = true; + lastPotential = slotValue; + } + else // The slot must be in a set state + { + // If the higher value was only potentially set, anything further becomes potentially overwritten. + if (potential) + variables[(name, scope)] = slotValue.PotentiallyOverwrite(definition); + else + variables[(name, scope)] = definition; + break; + } + scope = scope.ParentScope; + } + return false; + } + private static void SetFunctionToGlobal(IRAssign assignment, IRCodePart codePart, Dictionary<(string VariableName, IRScope Scope), + SSADefinition> variables, HashSet<(string, IRScope)> blacklist) + { + if (codePart == null || + !(assignment.Value is IRRelocateLater funcOrTrigger)) + return; + IRFunction function = codePart.GetFunction(((string)funcOrTrigger.Value).Split('-').First()); + if (function == null) + return; + function.IsGlobal = true; + + // A global function could be added to a trigger in external code. + // Treat it as a trigger body itself. + ProcessTrigger(function, variables, blacklist); + } + + private static void ProcessUnset(IRUnset unset, Dictionary<(string Name, IRScope), SSADefinition> variables, HashSet<(string, IRUnset)> recordTo) + { + IRScope startingScope = unset.Block.Scope; + // On encountering an unset operation remove the affected definition from the stored variables. + if (unset.IsInvariant) + { + if (Unset(unset, startingScope, variables)) + recordTo?.Add((unset.Target.Name, unset)); + } + else // If the affected definition cannot be determined, potentially unset all reachable variables. + { + foreach (string varName in variables.Keys.Select(d => d.Name).Distinct()) + { + if (PotentiallyUnsetByName(unset, varName, startingScope, variables)) + recordTo?.Add((varName, unset)); + } + } + } + private static bool Unset(IRUnset unset, IRScope scope, Dictionary<(string VariableName, IRScope Scope), SSADefinition> variables) + { + bool closureVariableAffected = false; + // Target is an instance of SSASetDefinition that is definitively Unset. + SSASetDefinition target = unset.Target; + string varName = target.Name; + bool potential = false; + while (scope != null) + { + if (scope.IsGlobalScope) + closureVariableAffected = true; + if (variables.TryGetValue((varName, scope), out SSADefinition slotValue) && + slotValue.State != SSADefinition.SetState.Unset) + { + // If the higher scope slot was potentially unset, this one may remain valid + // Mark it as potentially unset as well. + if (potential) + variables[(varName, scope)] = slotValue.PotentiallyUnset(unset); + else + variables[(varName, scope)] = target; + + // If this slot was potentially unset, the next higher scope slot + // could be the target of this unset. + // If this slot was definitively set, higher scopes are unaffected. + if (slotValue.State == SSADefinition.SetState.PotentiallyUnset) + potential = true; + else + break; + } + scope = scope.ParentScope; + } + return closureVariableAffected; + } + private static bool PotentiallyUnsetByName(IRUnset unset, string varName, IRScope scope, Dictionary<(string VariableName, IRScope Scope), SSADefinition> variables) + { + bool closureVariableAffected = false; + if (string.IsNullOrEmpty(varName)) + return closureVariableAffected; + while (scope != null) + { + if (scope.IsGlobalScope) + closureVariableAffected = true; + if (variables.TryGetValue((varName, scope), out SSADefinition slotValue) && + slotValue.State != SSADefinition.SetState.Unset) + { + variables[(varName, scope)] = slotValue.PotentiallyUnset(unset); + // Break when there is a slot that is definitively Set. + if (slotValue.State == SSADefinition.SetState.Set) + break; + } + scope = scope.ParentScope; + } + return closureVariableAffected; + } + + private static void ProcessTrigger(IClosureVariableUser trigger, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> blacklist) + { + foreach ((string varName, IRUnset unset) in trigger.ExternalUnsets) + { + IRScope scope = trigger.ClosureScope; + while (scope != null) + { + if (variables.TryGetValue((varName, scope), out SSADefinition ssaDef) && + ssaDef.State != SSADefinition.SetState.Unset) + { + variables[(varName, scope)] = ssaDef.PotentiallyUnset(unset); + } + } + } + foreach (string varName in trigger.ExternalWrites) + { + IRScope scope = trigger.ClosureScope; + while (scope != null) + { + if (variables.TryGetValue((varName, scope), out SSADefinition ssaDef) && + ssaDef.State != SSADefinition.SetState.Unset) + { + blacklist.Add((varName, scope)); + } + scope = scope.ParentScope; + } + } + } + + private static void ProcessCall(IRCall call, IRCodePart codePart, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> blacklist, IClosureVariableUser callingClosure) + { + IRFunction function = codePart.GetFunction(call); + if (function != null) + { + HandleFunction(call, function, variables, blacklist, callingClosure); + } + else if (!call.Direct && !(call.IndirectMethod is IRSuffixGetMethod)) + { + // Function calls to a UserDelegate could be to any function + // Global variables don't get SSA'd, so we don't care about + // functions from other code parts. + // Clobber/potentially overwrite anything affected by any + // function in this code part. + foreach (IRFunction func in codePart.Functions) + { + HandleFunction(call, func, variables, blacklist, callingClosure); + } + } + } + static void HandleFunction(IRCall call, IRFunction function, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> blacklist, IClosureVariableUser callingClosure) + { + HashSet externalWrites = callingClosure?.ExternalWrites; + HashSet<(string, IRUnset)> externalUnsets = callingClosure?.ExternalUnsets; + + // Potential unsets must happen first, so that any sets propagate + // through any uncertainties. + // All unsets from functions are treated as potential only since + // no analysis of control flow is done at this point. + // This could be improved in the future, but is probably fine for + // the rarity of unsets. + foreach ((string varName, IRUnset unset) in function.ExternalUnsets) + { + IRScope scope = function.ClosureScope; + while (scope != null) + { + if (scope.IsGlobalScope) + externalUnsets?.Add((varName, unset)); + + if (variables.TryGetValue((varName, scope), out SSADefinition ssaDef) && + ssaDef.State != SSADefinition.SetState.Unset) + { + variables[(varName, scope)] = ssaDef.PotentiallyUnset(unset); + + // A recursive function could unset the same variable multiple times. + // So this goes all the way to the top. + if (ssaDef.State == SSADefinition.SetState.Set && !function.IsRecursive) + break; + } + } + } + // Sets are also treated as potential overwrites since control flow + // is not guaranteed. + foreach (string varName in function.ExternalWrites) + { + IRScope scope = function.ClosureScope; + while (scope != null) + { + if (scope.IsGlobalScope) + externalWrites?.Add(varName); + + if (variables.TryGetValue((varName, scope), out SSADefinition ssaDef) && + ssaDef.State != SSADefinition.SetState.Unset) + { + variables[(varName, scope)] = ssaDef.PotentiallyOverwrite(SSASetDefinition.FromCallSite(varName, call)); + + if (ssaDef.State == SSADefinition.SetState.Set) + break; + } + scope = scope.ParentScope; + } + } + // Finally, the trigger-affected variables are propagated. + // This is handled as normal for a trigger. + foreach (IRTrigger trigger in function.TriggersCreated) + { + ProcessTrigger(trigger, variables, blacklist); + } } - private static void BuildPhis(BasicBlock root) + private static void BuildPhis(BasicBlock root, IRCodePart codePart, IClosureVariableUser funcOrTrigger) { - Dictionary> variablesOut = new Dictionary>(); + Dictionary> variablesOut = + new Dictionary>(); Queue worklist = new Queue(); worklist.Enqueue(root); @@ -144,137 +432,187 @@ private static void BuildPhis(BasicBlock root) { BasicBlock block = worklist.Dequeue(); - HashSet blacklist = block.TriggerPropagationBlacklist; - List<(BasicBlock block, SSAVariable variable)> varsIn = new List<(BasicBlock block, SSAVariable variable)>(); + HashSet<(string, IRScope)> blacklist = block.TriggerPropagationBlacklist; + List<(BasicBlock Block, IRScope Scope, SSADefinition Variable)> varsIn = new List<(BasicBlock, IRScope, SSADefinition)>(); + // Make the blacklist the union of all incoming blacklists + // Collect all incoming variable definitions foreach (BasicBlock predecessor in block.Predecessors) { blacklist.UnionWith(predecessor.TriggerPropagationBlacklist); - if (variablesOut.TryGetValue(predecessor, out HashSet predVarsOut)) + if (variablesOut.TryGetValue(predecessor, out Dictionary<(string Name, IRScope Scope), SSADefinition> predVarsOut)) { - foreach (SSAVariable variable in predVarsOut - .Where(v => block.Scope.IsEqualOrEncompassedBy(v.Scope))) - varsIn.Add((predecessor, variable)); + foreach (KeyValuePair<(string Name, IRScope Scope), SSADefinition> variable in predVarsOut + .Where(v => block.Scope.IsEqualOrEncompassedBy(v.Key.Scope))) + varsIn.Add((predecessor, variable.Key.Scope, variable.Value)); } } - HashSet variablesIn = new HashSet(); - foreach (IGrouping definition in - varsIn.GroupBy(bv => bv.variable.Parent)) + Dictionary<(string, IRScope), SSADefinition> variablesIn = new Dictionary<(string, IRScope), SSADefinition>(); + + // Group variable definitions by their scope slot. + // If a scope slot has multiple distinct definitions, generate a phi. + foreach (IGrouping<(string Name, IRScope Scope), (BasicBlock Block, IRScope Scope, SSADefinition Variable)> definitionSet in + varsIn.GroupBy(v => (v.Variable.Name, v.Scope))) { - if (definition.Select(bv => bv.variable).Distinct().Skip(1).Any()) + if (definitionSet.Select(def => (def.Scope, def.Variable)).Distinct().Skip(1).Any()) { // Phi required - PhiVariable phiVar = block.Phis.FirstOrDefault(v => v.Parent.Equals(definition.Key)); - if (phiVar == null) + if (!block.Phis.TryGetValue(definitionSet.Key, out PhiNode phiVar)) { - phiVar = definition.Key.GetNewPhiVariable(); - block.Phis.Add(phiVar); + phiVar = new PhiNode(definitionSet.Key.Name); + block.Phis.Add(definitionSet.Key, phiVar); } - foreach ((BasicBlock block, SSAVariable variable) def in definition) - phiVar.PossibleValues[def.block] = def.variable; + foreach ((BasicBlock Block, IRScope _, SSADefinition Variable) in definitionSet) + phiVar.PossibleValues[Block] = Variable; - variablesIn.Add(phiVar); + variablesIn.Add(definitionSet.Key, phiVar.Result); } else { - block.Phis.RemoveWhere(v => v.Parent.Equals(definition.Key)); - variablesIn.Add(definition.First().variable); + block.Phis.Remove(definitionSet.Key); + variablesIn.Add(definitionSet.Key, definitionSet.First().Variable); } } + // Store the resulting incoming variable definitions to the block. block.IncomingVariableDefinitions = variablesIn; - HashSet varsOut = new HashSet(); - varsOut.UnionWith(variablesIn); - - foreach (SSAVariable variable in block.VariablesWritten.Where(v => !v.Scope.IsGlobalScope)) - OverwriteVariable(varsOut, variable); + // Analyze the block with that set of incoming variable definitions + Dictionary<(string Name, IRScope Scope), SSADefinition> varsOut = + AnalyzeBlock(block, codePart, funcOrTrigger); - if (variablesOut.ContainsKey(block) && variablesOut[block].SetEquals(varsOut)) - continue; + // If this block was previously analyzed, and + // if the definitions all match, there's no need to queue + // this block's successors. + if (variablesOut.ContainsKey(block)) + { + Dictionary<(string, IRScope), SSADefinition> oldDefinition = variablesOut[block]; + if (oldDefinition.Count == varsOut.Count && + varsOut.All(kvp => oldDefinition[kvp.Key].Equals(kvp.Value))) + continue; + } + // Cache this result for comparison in future passes. variablesOut[block] = varsOut; + // Enqueue all successor blocks. foreach (BasicBlock successor in block.Successors) worklist.Enqueue(successor); } } - private static void ApplyUses(BasicBlock block, List triggers) + private static void ApplyUses(BasicBlock block, IRCodePart codePart, IClosureVariableUser funcOrTrigger) { List instructions = block.Instructions; - Dictionary liveDefinitions = new Dictionary(); - HashSet triggerBlacklist = new HashSet(); + // Start with a clone of the block's incoming variable definitions. + Dictionary<(string Name, IRScope Scope), SSADefinition> liveDefinitions = + new Dictionary<(string, IRScope), SSADefinition>(); + foreach (KeyValuePair<(string Name, IRScope Scope), SSADefinition> definition in block.IncomingVariableDefinitions) + liveDefinitions[definition.Key] = definition.Value; + + // Start with the union of all incoming trigger blacklists. + HashSet<(string, IRScope)> triggerBlacklist = new HashSet<(string, IRScope)>(); foreach (BasicBlock predecessor in block.Predecessors) triggerBlacklist.UnionWith(predecessor.TriggerPropagationBlacklist); - foreach (SSAVariable variable in block.IncomingVariableDefinitions) - liveDefinitions[variable.Parent] = variable; + // Create a local function for SSA replacement for this specific block. + IInterimOperand ScopedSSAReplacement(IInterimOperand op) + => SSAReplacement(op, block.Scope, liveDefinitions, triggerBlacklist, funcOrTrigger); - for (int i = 0; i < instructions.Count; i++) + foreach (IRInstruction instruction in block.Instructions) { - IRInstruction instruction = instructions[i]; + // Process call sites and replace variable definitions. foreach (IRInstruction inst in instruction.DepthFirst()) { - if (inst is IRUnaryConsumer triggerInstruction && - triggerInstruction.Operation is OpcodeAddTrigger) - { - string pointer = (string)((IRConstant)triggerInstruction.Operand).Value; - IRCodePart.IRTrigger trigger = triggers.FirstOrDefault(t => string.Equals(t.Identifier, pointer, StringComparison.OrdinalIgnoreCase)); - - triggerBlacklist.UnionWith(trigger.ExternalWrites); - - foreach (IRVariable variable in trigger.ExternalWrites) - liveDefinitions.Remove(variable); - } - else if (inst is IRAssign assignment && - assignment.Target is SSAVariable ssaVariable) + if (inst is IRCall call) { - if (!triggerBlacklist.Contains(ssaVariable.Parent)) - liveDefinitions[ssaVariable.Parent] = ssaVariable; - else - liveDefinitions.Remove(ssaVariable.Parent); + ProcessCall(call, codePart, liveDefinitions, triggerBlacklist, null); + IRFunction function = codePart.GetFunction(call); + if (function != null) + { + Dictionary<(string, IRScope), SSADefinition> reachableVariables = + new Dictionary<(string, IRScope), SSADefinition>(); + foreach ((string, IRScope) slot in liveDefinitions.Keys + .Where(k => function.ExternalReads.Contains(k.Name))) + reachableVariables.Add(slot, liveDefinitions[slot]); + SingleStaticAssignment.reachableVariables[call] = reachableVariables; + } } - else if (inst is IRCall call) + if (inst is IOperandInstructionBase operandInstruction) + operandInstruction.MutateEachOperand(ScopedSSAReplacement); + } + + // Process assignments, unsets, and new triggers. + switch (instruction) + { + case IRAssign assignment: + ProcessAssignment(assignment, null, liveDefinitions, triggerBlacklist); + break; + case IRUnset unset: + ProcessUnset(unset, liveDefinitions, null); + break; + case IRUnaryConsumer irTrigger: + if (irTrigger.Operation is OpcodeAddTrigger) + { + string pointer = (string)((InterimConstantValue)irTrigger.Operand).Value; + IRTrigger trigger = codePart.GetTrigger(pointer); + ProcessTrigger(trigger, liveDefinitions, triggerBlacklist); + } + break; + } + } + } + + private static IInterimOperand SSAReplacement(IInterimOperand operand, IRScope scope, Dictionary<(string, IRScope), SSADefinition> liveDefinitions, HashSet<(string, IRScope)> triggerBlacklist, IClosureVariableUser funcOrTrigger) + { + { + if (operand is InterimVariableReference variableRef) + { + string name = variableRef.Name; + // Don't apply to the global scope since SSA cannot be + // guaranteed for global variables. + while (!scope.IsGlobalScope) { - if (postCallSSAVariables.ContainsKey(call)) + if (liveDefinitions.TryGetValue((name, scope), out SSADefinition value) && + value.State != SSADefinition.SetState.Unset) { - foreach (SSAVariable variable in postCallSSAVariables[call]) + if (triggerBlacklist.Contains((name, scope))) { - if (!triggerBlacklist.Contains(variable.Parent)) - liveDefinitions[variable.Parent] = variable; - else - liveDefinitions.Remove(variable.Parent); + return variableRef; } + return new InterimVariableReference(value, variableRef); } + scope = scope.ParentScope; } - if (inst is IOperandInstructionBase operandInstruction) + funcOrTrigger?.ExternalReads.Add(name); + } + else if (operand is IRUnaryOp existOp && existOp.Operation is OpcodeExists) + { + if (existOp.Operand.IsInvariant) { - operandInstruction.MutateEachOperand(op => + string name; + if (existOp.Operand is IInterimVariableReference reference) + name = reference.Name; + else if (existOp is IEvaluatableToConstant constant) + name = (string)constant.Evaluate().Value; + else + return operand; + + while (scope != null) { - if (op is IRVariable variable && - !triggerBlacklist.Contains(variable) && - liveDefinitions.TryGetValue(variable, out SSAVariable currentValue)) - return currentValue; - return op; - }); + if (liveDefinitions.TryGetValue((name, scope), out SSADefinition value) && + value.State == SSADefinition.SetState.Set) + return new InterimConstantValue(Encapsulation.BooleanValue.True, existOp); + scope = scope.ParentScope; + } + funcOrTrigger?.ExternalReads.Add(name); } } + return operand; } } - - /// - /// Overwrites an SSA variable with the latest assignment. - /// - /// The active variables. - /// The new variable as assigned. - public static void OverwriteVariable(HashSet variables, SSAVariable newVariable) - { - variables.RemoveWhere(v => v.Parent == newVariable.Parent); - variables.Add(newVariable); - } } } diff --git a/src/kOS.Safe/Compilation/IR/TypeInferencer.cs b/src/kOS.Safe/Compilation/IR/TypeInferencer.cs index 5a3e8f044..32f50a609 100644 --- a/src/kOS.Safe/Compilation/IR/TypeInferencer.cs +++ b/src/kOS.Safe/Compilation/IR/TypeInferencer.cs @@ -83,7 +83,8 @@ public static Type GetTypeForSuffix(Type structureType, string suffix) return suffixType; if (typeof(Lexicon).IsAssignableFrom(structureType)) return typeof(Structure); - throw new Exceptions.KOSSuffixUseException("get", suffix, structureType.ToString()); + //throw new Exceptions.KOSSuffixUseException("get", suffix, structureType.ToString()); + return typeof(Structure); } // Processing won't work for an abstract type. They should already be added in the constructor. @@ -181,7 +182,8 @@ private static void ProcessTypeForSuffixes(Type type) public static Type GetTypeForIndex(Type type) { if (!typeof(IIndexable).IsAssignableFrom(type)) - throw new InvalidOperationException($"Tried to get the index from a non-indexable type {type}"); + //throw new InvalidOperationException($"Tried to get the index from a non-indexable type {type}"); + return typeof(Structure); if (indexTypes.TryGetValue(type, out Type indexType)) { diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs index 0c241d449..37d5ad3b9 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs @@ -7,132 +7,27 @@ namespace kOS.Safe.Compilation.Optimization { public static class OptimizationTools { - /*public static IEnumerable FindExpressionsMatching(this IEnumerable instructions, Predicate predicate) - => instructions.SelectMany(i => ExpressionMatchDepthFirst(i, predicate)); - - public static IEnumerable FindFirstExpressionsMatching(this IEnumerable instructions, Predicate predicate) - => instructions.SelectMany(i => ExpressionMatchBreadthFirst(i, predicate)); - */ - public static IEnumerable DepthFirst(this IEnumerable instructions) => instructions.SelectMany(DepthFirst); - public static IEnumerable FindInstructionsMatching(this IEnumerable instructions, Predicate predicate) - => instructions.SelectMany(i => InstructionMatchDepthFirst(i, predicate)); - - public static IEnumerable FindFirstInstructionsMatching(this IEnumerable instructions, Predicate predicate) - => instructions.SelectMany(i => InstructionMatchBreadthFirst(i, predicate)); - - private static IEnumerable ExpressionMatchDepthFirst(IRInstruction instruction, Predicate predicate) - { - if (instruction is ISingleOperandInstruction singleOperandInstruction) - { - if (singleOperandInstruction.Operand is IRTemp temp) - { - foreach (IRTemp predecessorMatch in ExpressionMatchDepthFirst(temp.Parent, predicate)) - yield return predecessorMatch; - if (predicate(temp.Parent)) - yield return temp; - } - } - else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) - { - foreach (IRValue operand in multipleOperandInstruction.Operands) - { - if (operand is IRTemp temp) - { - foreach (IRTemp predecessorMatch in ExpressionMatchDepthFirst(temp.Parent, predicate)) - yield return predecessorMatch; - if (predicate(temp.Parent)) - yield return temp; - } - } - } - } - - private static IEnumerable ExpressionMatchBreadthFirst(IRInstruction instruction, Predicate predicate) - { - if (instruction is ISingleOperandInstruction singleOperandInstruction) - { - if (singleOperandInstruction.Operand is IRTemp temp) - { - if (predicate(temp.Parent)) - yield return temp; - foreach (IRTemp predecessorMatch in ExpressionMatchBreadthFirst(temp.Parent, predicate)) - yield return predecessorMatch; - } - } - else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) - { - foreach (IRValue operand in multipleOperandInstruction.Operands) - { - if (operand is IRTemp temp && predicate(temp.Parent)) - yield return temp; - } - foreach (IRValue operand in multipleOperandInstruction.Operands) - { - if (operand is IRTemp temp) - { - foreach (IRTemp predecessorMatch in ExpressionMatchBreadthFirst(temp.Parent, predicate)) - yield return predecessorMatch; - } - } - } - } - - private static IEnumerable InstructionMatchDepthFirst(IRInstruction instruction, Predicate predicate) - { - foreach (IRInstruction match in CrawlTreeForMatch(instruction, predicate, InstructionMatchDepthFirst)) - yield return match; - if (predicate(instruction)) - yield return instruction; - } - - private static IEnumerable InstructionMatchBreadthFirst(IRInstruction instruction, Predicate predicate) - { - if (predicate(instruction)) - yield return instruction; - foreach (IRInstruction match in CrawlTreeForMatch(instruction, predicate, InstructionMatchBreadthFirst)) - yield return match; - } - public static IEnumerable DepthFirst(this IRInstruction instruction) { if (instruction is ISingleOperandInstruction singleOperandInstruction) { - if (singleOperandInstruction.Operand is IRTemp temp) - foreach (IRInstruction predecessor in DepthFirst(temp.Parent)) + if (singleOperandInstruction.Operand is IRInstruction inst) + foreach (IRInstruction predecessor in DepthFirst(inst)) yield return predecessor; } else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) { - foreach (IRValue operand in multipleOperandInstruction.Operands) + foreach (IInterimOperand operand in multipleOperandInstruction.Operands) { - if (operand is IRTemp temp) - foreach (IRInstruction predecessor in DepthFirst(temp.Parent)) + if (operand is IRInstruction inst) + foreach (IRInstruction predecessor in DepthFirst(inst)) yield return predecessor; } } yield return instruction; } - - private static IEnumerable CrawlTreeForMatch(IRInstruction instruction, Predicate predicate, Func, IEnumerable> searchFunc) - { - if (instruction is ISingleOperandInstruction singleOperandInstruction) - { - if (singleOperandInstruction.Operand is IRTemp temp) - foreach (IRInstruction match in searchFunc(temp.Parent, predicate)) - yield return match; - } - else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) - { - foreach (IRValue operand in multipleOperandInstruction.Operands) - { - if (operand is IRTemp temp) - foreach (IRInstruction match in searchFunc(temp.Parent, predicate)) - yield return match; - } - } - } } } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index b07f0f3d0..ca64d0db4 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -13,7 +13,6 @@ public class ConstantFolding : IOptimizationPass public void ApplyPass(List blocks) { - Queue worklist = new Queue(blocks); IEnumerator enumerator = blocks.GetEnumerator(); while (enumerator.MoveNext()) { @@ -25,159 +24,112 @@ private static void ApplyPass(BasicBlock block) for (int i = 0; i < block.Instructions.Count; i++) { IRInstruction instruction = block.Instructions[i]; - switch (instruction) + if (instruction is IRPop pop) { - case IRPop pop: - if (AttemptReductionToConstant(pop.Value) is IRConstant) - { - block.Instructions.RemoveAt(i); - i--; - } + if (pop.IsInvariant) + { + block.Instructions.RemoveAt(i); + i--; continue; - case IRAssign assign: - assign.Value = AttemptReductionToConstant(assign.Value); - break; - case IRSuffixSet suffixSet: - suffixSet.Value = AttemptReductionToConstant(suffixSet.Value); - suffixSet.Object = AttemptReductionToConstant(suffixSet.Object); - break; - case IRIndexSet indexSet: - indexSet.Value = AttemptReductionToConstant(indexSet.Value); - indexSet.Object = AttemptReductionToConstant(indexSet.Object); - indexSet.Index = AttemptReductionToConstant(indexSet.Index); - break; - case IRBranch branch: - if (branch.True == branch.False) - block.Instructions[i] = new IRJump(branch.True, branch.SourceLine, branch.SourceColumn); - else if (AttemptReductionToConstant(branch.Condition) is IRConstant branchConstant) + } + } + else if (instruction is IRBranch branch) + { + if (branch.True == branch.False) + { + block.Instructions[i] = new IRJump(block, branch.True, branch.SourceLine, branch.SourceColumn); + continue; + } + if (branch.IsInvariant) + { + BasicBlock permanentBlock, deprecatedBlock; + InterimConstantValue branchConstant = (branch.Condition as IEvaluatableToConstant).Evaluate(); + if (branchConstant != null) { - BasicBlock permanentBlock, deprecatedBlock; (permanentBlock, deprecatedBlock) = Convert.ToBoolean(branchConstant.Value) ? (branch.True, branch.False) : (branch.False, branch.True); - block.Instructions[i] = new IRJump(permanentBlock, new OpcodeBranchJump() { SourceLine = branch.SourceLine, SourceColumn = branch.SourceColumn }); + block.Instructions[i] = new IRJump(block, permanentBlock, new OpcodeBranchJump() { SourceLine = branch.SourceLine, SourceColumn = branch.SourceColumn }); block.RemoveSuccessor(deprecatedBlock); } - break; + } + } + foreach (IRInstruction inst in instruction.DepthFirst()) + { + if (inst is IOperandInstructionBase operandInstruction) + { + operandInstruction.MutateEachOperand(AttemptReductionToConstant); + } } } } - private static IRValue AttemptReductionToConstant(IRValue input) + private static IInterimOperand AttemptReductionToConstant(IInterimOperand input) { - if (input is IRConstant constant) - return constant; - if (!(input is IRTemp temp)) - return input; - return AttemptReduction(temp.Parent); + if (input is IResultingInstruction instruction) + return AttemptReduction(instruction); + return input; } - public static IRValue AttemptReduction(IRInstruction instruction) + + public static IInterimOperand AttemptReduction(IResultingInstruction instruction) { + // Only fold into an IRConstant when it is a primitive that can be stored in ksm. + if (!typeof(Encapsulation.PrimitiveStructure).IsAssignableFrom(instruction.Type)) + return instruction; + switch (instruction) { case IRUnaryOp unaryOp: return ReduceUnary(unaryOp); case IRBinaryOp binaryOp: return ReduceBinary(binaryOp); - case IRSuffixGet suffixGet: - return ReduceSuffixGet(suffixGet); - case IRIndexGet indexGet: - return ReduceIndexGet(indexGet); case IRCall call: return ReduceCall(call); - case IResultingInstruction resulting: - return resulting.Result; + default: + return instruction; } - throw new ArgumentException($"{instruction.GetType()} is not supported for constant folding."); } - private static IRValue ReduceUnary(IRUnaryOp instruction) + private static IInterimOperand ReduceUnary(IRUnaryOp instruction) { - if (instruction.Operand is IRTemp temp) - instruction.Operand = AttemptReduction(temp.Parent); - - if (instruction.Operand is IRConstant constant) - { - object input = constant.Value; - IRValue result; - try - { - switch (instruction.Operation) - { - case OpcodeMathNegate _: - result = new IRConstant(OpcodeMathNegate.StaticOperation(input), instruction); - break; - case OpcodeLogicNot _: - result = new IRConstant(OpcodeLogicNot.StaticOperation(input), instruction); - break; - case OpcodeLogicToBool _: - result = new IRConstant(OpcodeLogicToBool.StaticOperation(input), instruction); - break; - default: - result = instruction.Result; - break; - } - instruction.Result = result; - } - catch (KOSUnaryOperandTypeException unaryTypeException) - { - throw new KOSCompileException(instruction, unaryTypeException); - } - } - return instruction.Result; + if (instruction.IsInvariant) + return instruction.Evaluate(); + return instruction; } - private static IRValue ReduceBinary(IRBinaryOp instruction) + private static IInterimOperand ReduceBinary(IRBinaryOp instruction) { - if (instruction.Left is IRTemp tempL) - { - instruction.Left = AttemptReduction(tempL.Parent); - } - if (instruction.Right is IRTemp tempR) - { - instruction.Right = AttemptReduction(tempR.Parent); - } + if (instruction.IsInvariant) + return instruction.Evaluate(); + // Put constants to the right, if there are any - if (instruction.IsCommutative && instruction.Left is IRConstant && !(instruction.Right is IRConstant)) + if (instruction.IsCommutative && instruction.Left is InterimConstantValue && !(instruction.Right is InterimConstantValue)) { instruction.SwapOperands(); } // If this is false, neither are constants after the last step, unless this isn't commutative, in which case this cleverness doesn't matter. - if (instruction.Right is IRConstant constantR) + if (instruction.Right is InterimConstantValue constantR) { // If this is true, both are constants - if (instruction.Left is IRConstant constantL) + if (instruction.Left is InterimConstantValue) { - object left = constantL.Value; - object right = constantR.Value; - try - { - IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right), instruction); - instruction.Result = result; - } - catch (KOSBinaryOperandTypeException binaryTypeException) - { - throw new KOSCompileException(instruction, binaryTypeException); - } - return instruction.Result; + return instruction.Evaluate(); } else if (instruction.IsCommutative) { // The right is constant and the left is not... // But what if left.Parent is commutative with this operation and has a constant? - if (instruction.Left is IRTemp temp && - temp.Parent is IRBinaryOp leftOp && + if (instruction.Left is IRBinaryOp leftOp && leftOp.Operation.GetType() == instruction.Operation.GetType() && - leftOp.Right is IRConstant constantL1) + leftOp.Right is InterimConstantValue constantL1) { object right = constantR.Value; object left = constantL1.Value; try { - IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right), instruction); - instruction.Result = result; + InterimConstantValue result = new InterimConstantValue(instruction.Operation.ExecuteCalculation(left, right), instruction); leftOp.Right = result; } catch (KOSBinaryOperandTypeException binaryTypeException) { throw new KOSCompileException(instruction, binaryTypeException); } - return leftOp.Result; + return leftOp; } } // Shortcuts for math operations where both sides don't need to be constant @@ -187,7 +139,7 @@ temp.Parent is IRBinaryOp leftOp && // X * 0 = 0 if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) return constantR; - if (ReduceDivMult(instruction, constantR, out IRValue newResult)) + if (ReduceDivMult(instruction, constantR, out IInterimOperand newResult)) return newResult; break; case OpcodeMathDivide _: @@ -205,7 +157,7 @@ temp.Parent is IRBinaryOp leftOp && case OpcodeMathPower _: // X^0 = 1 if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) - return new IRConstant(Encapsulation.ScalarIntValue.One, instruction); + return new InterimConstantValue(Encapsulation.ScalarIntValue.One, instruction); // X^1 = X if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) return instruction.Left; @@ -223,7 +175,7 @@ temp.Parent is IRBinaryOp leftOp && // So this is an acceptable assumption that improves performance and eliminates an error. // TODO: Add an "EXIT" (EOP) command to the language because this will break the // PRINT(1/0) shortcut to cause a program to terminate. - if (instruction.Left is IRConstant constantL && + if (instruction.Left is InterimConstantValue constantL && Encapsulation.ScalarIntValue.Zero.Equals(constantL.Value)) return constantL; // X / X = 1 @@ -231,13 +183,13 @@ temp.Parent is IRBinaryOp leftOp && // But that would otherwise throw a "Tried to push infinite on to the stack" error // So this is an acceptable assumption that improves performance and eliminates an error. if (instruction.Left == instruction.Right) - return new IRConstant(Encapsulation.ScalarIntValue.One, instruction); + return new InterimConstantValue(Encapsulation.ScalarIntValue.One, instruction); break; } } - return instruction.Result; + return instruction; } - private static bool ReduceDivMult(IRBinaryOp instruction, IRConstant secondOperand, out IRValue newResult) + private static bool ReduceDivMult(IRBinaryOp instruction, InterimConstantValue secondOperand, out IInterimOperand newResult) { // X */ 1 = X if (Encapsulation.ScalarIntValue.One.Equals(secondOperand.Value)) @@ -248,91 +200,27 @@ private static bool ReduceDivMult(IRBinaryOp instruction, IRConstant secondOpera // X */ -1 = -X if (secondOperand.Value.Equals(-Encapsulation.ScalarIntValue.One)) { - IRTemp tempResult = instruction.Result as IRTemp; - tempResult.Parent = new IRUnaryOp( - tempResult, + newResult = new IRUnaryOp( + instruction.Block, new OpcodeMathNegate() { SourceColumn = instruction.SourceColumn, SourceLine = instruction.SourceLine }, instruction.Left); - newResult = tempResult; return true; } newResult = null; return false; } - private static IRValue ReduceSuffixGet(IRSuffixGet instruction) - { - if (instruction.Object is IRTemp temp) - { - instruction.Object = AttemptReduction(temp.Parent); - } - return instruction.Result; - } - private static IRValue ReduceIndexGet(IRIndexGet instruction) - { - if (instruction.Object is IRTemp tempObj) - { - instruction.Object = AttemptReduction(tempObj.Parent); - } - if (instruction.Index is IRTemp tempIndex) - { - instruction.Index = AttemptReduction(tempIndex.Parent); - } - return instruction.Result; - } - private static IRValue ReduceCall(IRCall instruction) + private static IInterimOperand ReduceCall(IRCall instruction) { // TODO: Add reduction for specific functions. E.g. mod(X, 1) = 0, round/ceiling/floor, Ln/Log10, min/max - for (int i = instruction.Arguments.Count - 1; i >= 0; i--) - { - if (instruction.Arguments[i] is IRTemp temp) - instruction.Arguments[i] = AttemptReduction(temp.Parent); - } - string functionName = instruction.Function.Replace("()", ""); - if (Optimizer.FunctionManager.Exists(functionName) && instruction.Arguments.All(arg => arg is IRConstant)) - { - try - { - switch (functionName) - { - case "abs": - case "mod": - case "floor": - case "ceiling": - case "round": - case "sqrt": - case "ln": - case "log10": - case "min": - case "max": - case "sin": - case "cos": - case "tan": - case "arcsin": - case "arccos": - case "arctan": - case "arctan2": - case "anglediff": - InterimCPU interimCPU = Optimizer.InterimCPU; - interimCPU.Boot(); // Clear the stack out of caution. - interimCPU.PushArgumentStack(new Execution.KOSArgMarkerType()); - foreach (IRValue arg in instruction.Arguments) - interimCPU.PushArgumentStack(((IRConstant)arg).Value); - Optimizer.FunctionManager.CallFunction(functionName); - instruction.Result = new IRConstant(interimCPU.PopValueArgument(), instruction); - return instruction.Result; - } - } - catch (KOSException e) - { - throw new KOSCompileException(instruction, e); - } - } - return instruction.Result; + if (instruction.IsInvariant) + return instruction.Evaluate(); + + return instruction; } } } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs index c1bc42cab..0f4c52b1c 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs @@ -16,7 +16,12 @@ public void ApplyPass(List code) { IRInstruction instruction = code[i]; foreach (IRInstruction nestedInstruction in instruction.DepthFirst()) - PeepholeFilter(nestedInstruction); + { + if (nestedInstruction is IOperandInstructionBase operandInstruction) + operandInstruction.MutateEachOperand(OperandPeepholeFilter); + + InstructionPeepholeFilter(nestedInstruction); + } // Replace lex indexing with string constant with suffixing where possible. if (instruction is IRIndexSet indexSet) @@ -28,43 +33,63 @@ public void ApplyPass(List code) } } - private static void PeepholeFilter(IRInstruction instruction) + private static IInterimOperand OperandPeepholeFilter(IInterimOperand operand) { // Replace parameterless suffix method calls with get member // TODO: Skip this if clobber built-ins is active. - if (instruction is IRCall suffixCall && + if (operand is IRCall suffixCall && !suffixCall.Direct && suffixCall.Arguments.Count == 0 && - suffixCall.IndirectMethod is IRTemp tempSuffixCall && - tempSuffixCall.Parent is IRSuffixGetMethod) + suffixCall.IndirectMethod is IRSuffixGetMethod) { - ReplaceParameterlessSuffix(suffixCall); - return; + return ReplaceParameterlessSuffix(suffixCall); } + // Replace calls to VectorDotProduct with multiplication // TODO: Skip this if clobber built-ins is active. - if (instruction is IRCall vDotCall && + if (operand is IRCall vDotCall && (vDotCall.Function == "vdot" || vDotCall.Function == "vectordotproduct") && vDotCall.Arguments.Count == 2) { - ReplaceVectorDotProduct(vDotCall); - return; + return ReplaceVectorDotProduct(vDotCall); + } + + // Redundant unary operation replacements + // E.g. !!X = X and --X = X + if (operand is IRUnaryOp unaryParent) + { + IInterimOperand potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); + if (potentialResult != null) + return potentialResult; } + + // Replace lex indexing using string constant with suffixing where possible + if (operand is IRIndexGet indexGet && + indexGet.Index is InterimConstantValue indexConstant && + (indexConstant.Value is Encapsulation.StringValue || + indexConstant.Value is string)) + { + IRSuffixGet potentialResult = AttemptReplaceIndexGetWithSuffixGet(indexGet); + if (potentialResult != null) + return potentialResult; + } + + return operand; + } + + private static void InstructionPeepholeFilter(IRInstruction instruction) + { // Algebraic simplifications if (instruction is IRBinaryOp binaryOp) { - IRTemp tempL = binaryOp.Left as IRTemp; - IRTemp tempR = binaryOp.Right as IRTemp; switch (binaryOp.Operation) { case OpcodeMathAdd _: { // A*B + A*C = A*(B+C) { - if (tempL != null && - tempL.Parent is IRBinaryOp opL && - tempR != null && - tempR.Parent is IRBinaryOp opR && + if (binaryOp.Left is IRBinaryOp opL && + binaryOp.Right is IRBinaryOp opR && opL.Operation is OpcodeMathMultiply && opR.Operation is OpcodeMathMultiply && binaryOp.IsCommutative && @@ -102,15 +127,13 @@ opR.Operation is OpcodeMathMultiply && } // -B+A = A+-B = A-B { - if (tempR != null && - tempR.Parent is IRUnaryOp opR && + if (binaryOp.Right is IRUnaryOp opR && opR.Operation is OpcodeMathNegate) { DistributeNegationB(binaryOp); return; } - if (tempL != null && - tempL.Parent is IRUnaryOp opL && + if (binaryOp.Left is IRUnaryOp opL && opL.Operation is OpcodeMathNegate && binaryOp.SwapOperands()) { @@ -120,8 +143,7 @@ opL.Operation is OpcodeMathNegate && } // -A+B = B-A { - if (tempL != null && - tempL.Parent is IRUnaryOp opL && + if (binaryOp.Left is IRUnaryOp opL && opL.Operation is OpcodeMathNegate) { DistributeNegationA(binaryOp); @@ -133,8 +155,7 @@ tempL.Parent is IRUnaryOp opL && case OpcodeMathSubtract _: // A--B=A+B { - if (tempR != null && - tempR.Parent is IRUnaryOp unaryOp && + if (binaryOp.Right is IRUnaryOp unaryOp && unaryOp.Operation is OpcodeMathNegate) { ReplaceNegateSubtract(binaryOp); @@ -143,13 +164,13 @@ tempR.Parent is IRUnaryOp unaryOp && } break; case OpcodeMathDivide _: - if (binaryOp.Right is IRConstant constantR && + // TODO: Handle distributivity + if (binaryOp.Right is InterimConstantValue constantR && Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) throw new Exceptions.KOSCompileException(instruction, new DivideByZeroException()); // X^N/X=X^(N-1) { - if (tempL != null && - tempL.Parent is IRBinaryOp opL && + if (binaryOp.Left is IRBinaryOp opL && opL.Operation is OpcodeMathPower && opL.Left == binaryOp.Right) { @@ -159,11 +180,9 @@ opL.Operation is OpcodeMathPower && } // X^N/X^M=X^(N-M) { - if (tempL != null && - tempL.Parent is IRBinaryOp opL && + if (binaryOp.Left is IRBinaryOp opL && opL.Operation is OpcodeMathPower && - tempR != null && - tempR.Parent is IRBinaryOp opR && + binaryOp.Right is IRBinaryOp opR && opR.Operation is OpcodeMathPower && opL.Left == opR.Left) { @@ -175,16 +194,14 @@ opR.Operation is OpcodeMathPower && case OpcodeMathMultiply _: // X^N*X=X^(N+1) { - if (tempL != null && - tempL.Parent is IRBinaryOp opL && + if (binaryOp.Left is IRBinaryOp opL && opL.Operation is OpcodeMathPower && opL.Left == binaryOp.Right) { IncreasePower(binaryOp, 1); return; } - if (tempR != null && - tempR.Parent is IRBinaryOp opR && + if (binaryOp.Right is IRBinaryOp opR && opR.Operation is OpcodeMathPower && opR.Left == binaryOp.Left && binaryOp.SwapOperands()) @@ -197,8 +214,7 @@ opR.Operation is OpcodeMathPower && // Start with (X*X)*X = X*(X*X) = X^3 // Then the previous rule will kick in and exponentiate from there { - if (tempR != null && - tempR.Parent is IRBinaryOp opR && + if (binaryOp.Right is IRBinaryOp opR && opR.Operation is OpcodeMathMultiply && opR.Left == opR.Right && opR.Left == binaryOp.Left) @@ -206,8 +222,7 @@ opR.Operation is OpcodeMathMultiply && CreatePower(binaryOp); return; } - if (tempL != null && - tempL.Parent is IRBinaryOp opL && + if (binaryOp.Left is IRBinaryOp opL && opL.Operation is OpcodeMathMultiply && opL.Left == opL.Right && opL.Left == binaryOp.Right && @@ -219,11 +234,9 @@ opL.Operation is OpcodeMathMultiply && } // X^N*X^M=X^(N+M) { - if (tempL != null && - tempL.Parent is IRBinaryOp opL && + if (binaryOp.Left is IRBinaryOp opL && opL.Operation is OpcodeMathPower && - tempR != null && - tempR.Parent is IRBinaryOp opR && + binaryOp.Right is IRBinaryOp opR && opR.Operation is OpcodeMathPower && opL.Left == opR.Left) { @@ -235,42 +248,9 @@ opR.Operation is OpcodeMathPower && } } - if (instruction is IOperandInstructionBase operandInstruction) - { - // Redundant unary operation replacements - // E.g. !!X = X and --X = X - operandInstruction.MutateEachOperand(op => - { - if (op is IRTemp tempOperand && - tempOperand.Parent is IRUnaryOp unaryParent) - { - IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); - if (potentialResult != null) - return potentialResult; - } - return op; - }); - - // Replace lex indexing using string constant with suffixing where possible - operandInstruction.ForEachOperand(op => - { - if (op is IRTemp tempOperand && - tempOperand.Parent is IRIndexGet indexGet && - indexGet.Index is IRConstant indexConstant && - (indexConstant.Value is Encapsulation.StringValue || - indexConstant.Value is string)) - { - IRInstruction potentialResult = AttemptReplaceIndexGetWithSuffixGet(indexGet); - if (potentialResult != null) - tempOperand.Parent = potentialResult; - } - - }); - } // Branch logical simplification (e.g. !X branch = X branch!) if (instruction is IRBranch branch && - branch.Condition is IRTemp temp && - temp.Parent is IRUnaryOp negateBranch && + branch.Condition is IRUnaryOp negateBranch && negateBranch.Operation is OpcodeLogicNot) { ReplaceRedundantNotBranch(branch); @@ -278,11 +258,11 @@ temp.Parent is IRUnaryOp negateBranch && } } - private static void ReplaceParameterlessSuffix(IRCall call) + private static IRSuffixGet ReplaceParameterlessSuffix(IRCall call) { - IRSuffixGet suffixMethod = (IRSuffixGet)((IRTemp)call.IndirectMethod).Parent; - ((IRTemp)call.Result).Parent = new IRSuffixGet( - (IRTemp)call.Result, + IRSuffixGet suffixMethod = (IRSuffixGet)call.IndirectMethod; + return new IRSuffixGet( + call.Block, suffixMethod.Object, new OpcodeGetMember(suffixMethod.Suffix) { @@ -291,10 +271,10 @@ private static void ReplaceParameterlessSuffix(IRCall call) }); } - private static void ReplaceVectorDotProduct(IRCall call) + private static IRBinaryOp ReplaceVectorDotProduct(IRCall call) { - ((IRTemp)call.Result).Parent = new IRBinaryOp( - (IRTemp)call.Result, + return new IRBinaryOp( + call.Block, new OpcodeMathMultiply() { SourceLine = call.SourceLine, @@ -304,7 +284,7 @@ private static void ReplaceVectorDotProduct(IRCall call) call.Arguments[1]); } - private static IRValue AttempReplaceRedundantUnaryOp(IRUnaryOp unaryParent) + private static IInterimOperand AttempReplaceRedundantUnaryOp(IRUnaryOp unaryParent) { // Replace --X with X if (CanOperationBeReplaced(unaryParent, typeof(OpcodeMathNegate))) @@ -317,22 +297,22 @@ private static IRValue AttempReplaceRedundantUnaryOp(IRUnaryOp unaryParent) private static bool CanOperationBeReplaced(IRUnaryOp operation, Type opcodeType) { return operation.Operation.GetType() == opcodeType && - operation.Operand is IRTemp operand && - operand.Parent is IRUnaryOp neg2 && + operation.Operand is IRUnaryOp neg2 && neg2.Operation.GetType() == opcodeType; } - private static IRValue GetDoubleNestedValue(ISingleOperandInstruction operation) + private static IInterimOperand GetDoubleNestedValue(ISingleOperandInstruction operation) { - return ((IRUnaryOp)((IRTemp)operation.Operand).Parent).Operand; + return ((IRUnaryOp)operation.Operand).Operand; } - private static IRInstruction AttemptReplaceIndexGetWithSuffixGet(IRIndexGet indexGet) + private static IRSuffixGet AttemptReplaceIndexGetWithSuffixGet(IRIndexGet indexGet) { - if (indexGet.Index is IRConstant indexConstant && + if (indexGet.Index is InterimConstantValue indexConstant && indexConstant.Value is Encapsulation.StringValue stringIndex && StringUtil.IsValidIdentifier(stringIndex)) { - return new IRSuffixGet((IRTemp)indexGet.Result, + return new IRSuffixGet( + indexGet.Block, indexGet.Object, new OpcodeGetMember(stringIndex) { @@ -345,11 +325,13 @@ indexConstant.Value is Encapsulation.StringValue stringIndex && private static IRInstruction AttemptReplaceIndexSetWithSuffixSet(IRIndexSet indexSet) { - if (indexSet.Index is IRConstant indexConstant && + if (indexSet.Index is InterimConstantValue indexConstant && indexConstant.Value is Encapsulation.StringValue stringIndex && StringUtil.IsValidIdentifier(stringIndex)) { - return new IRSuffixSet(indexSet.Object, + return new IRSuffixSet( + indexSet.Block, + indexSet.Object, indexSet.Value, new OpcodeSetMember(stringIndex) { @@ -370,10 +352,8 @@ private static void ReplaceRedundantNotBranch(IRBranch branch) private static void DistributeMultiplication(IRBinaryOp instruction) { // A*B + A*C = A*(B+C) - IRTemp tempL = (IRTemp)instruction.Left; - IRTemp tempR = (IRTemp)instruction.Right; - IRBinaryOp opL = (IRBinaryOp)tempL.Parent; - IRBinaryOp opR = (IRBinaryOp)tempR.Parent; + IRBinaryOp opL = (IRBinaryOp)instruction.Left; + IRBinaryOp opR = (IRBinaryOp)instruction.Right; // Restructure to create the parentheses opR.Operation = new OpcodeMathAdd(); opR.Left = opL.Right; @@ -385,7 +365,7 @@ private static void DistributeMultiplication(IRBinaryOp instruction) private static void DistributeNegationA(IRBinaryOp instruction) { // -A+B = B-A - if (!(instruction.Left is IRTemp tempL)) + if (!(instruction.Left is IRUnaryOp opL)) return; if (!instruction.IsCommutative) return; @@ -396,7 +376,6 @@ private static void DistributeNegationA(IRBinaryOp instruction) instruction.Operation = originalOperation; return; } - IRUnaryOp opL = (IRUnaryOp)tempL.Parent; instruction.Left = opL.Operand; instruction.SwapOperands(); } @@ -404,7 +383,7 @@ private static void DistributeNegationA(IRBinaryOp instruction) private static void DistributeNegationB(IRBinaryOp instruction) { // A+-B = A-B - if (!(instruction.Right is IRTemp tempR)) + if (!(instruction.Right is IRUnaryOp opR)) return; if (!instruction.IsCommutative) return; @@ -415,7 +394,6 @@ private static void DistributeNegationB(IRBinaryOp instruction) instruction.Operation = originalOperation; return; } - IRUnaryOp opR = (IRUnaryOp)tempR.Parent; instruction.Right = opR.Operand; } @@ -431,8 +409,7 @@ private static void ReplaceNegateSubtract(IRBinaryOp instruction) instruction.Operation = originalOperation; return; } - IRTemp tempR = (IRTemp)instruction.Right; - IRUnaryOp opR = (IRUnaryOp)tempR.Parent; + IRUnaryOp opR = (IRUnaryOp)instruction.Right; instruction.Right = opR.Operand; } @@ -443,13 +420,12 @@ private static void IncreasePower(IRBinaryOp instruction, int powerIncrease) // Assumes |N +/- 1| < 2^31 - 1 (for integer scalars) or 2^53 (for double scalars) // Going beyond that overflows an integer or the mantissa for double. - IRTemp tempL = (IRTemp)instruction.Left; - IRBinaryOp opL = (IRBinaryOp)tempL.Parent; + IRBinaryOp opL = (IRBinaryOp)instruction.Left; short divLine = instruction.SourceLine; short divColumn = instruction.SourceColumn; - IRConstant powConst = new IRConstant(new Encapsulation.ScalarIntValue(powerIncrease), instruction); + InterimConstantValue powConst = new InterimConstantValue(new Encapsulation.ScalarIntValue(powerIncrease), instruction); // Set up the new power operation instruction.Left = opL.Left; @@ -457,7 +433,7 @@ private static void IncreasePower(IRBinaryOp instruction, int powerIncrease) instruction.OverwriteSourceLocation(opL.SourceLine, opL.SourceColumn); // Reuse the old power instruction for the addition/subtraction - instruction.Right = tempL; + instruction.Right = opL; opL.Left = powConst; opL.Operation = new OpcodeMathAdd(); opL.OverwriteSourceLocation(divLine, divColumn); @@ -467,7 +443,7 @@ private static void IncreasePower(IRBinaryOp instruction, int powerIncrease) // Special case for when N == 2 afterwards // then revert back to X * X - if (instruction.Right is IRConstant constantR && + if (instruction.Right is InterimConstantValue constantR && Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) { instruction.Right = instruction.Left; @@ -478,10 +454,8 @@ private static void IncreasePower(IRBinaryOp instruction, int powerIncrease) private static void CreatePower(IRBinaryOp instruction) { // X*(X*X) = X^3 - if (!(instruction.Right is IRTemp tempR)) - return; instruction.Operation = new OpcodeMathPower(); - instruction.Right = new IRConstant(new Encapsulation.ScalarIntValue(3), tempR.Parent); + instruction.Right = new InterimConstantValue(new Encapsulation.ScalarIntValue(3), (IRInstruction)instruction.Right); } private static void CombinePowers(IRBinaryOp instruction) @@ -493,10 +467,8 @@ private static void CombinePowers(IRBinaryOp instruction, BinaryOpcode newOperat // X^N*X^M=X^(N+M) // Assumes |N + M| < 2^31 - 1 (for integer scalars) or 2^53 (for double scalars) - IRTemp tempL = (IRTemp)instruction.Left; - IRTemp tempR = (IRTemp)instruction.Right; - IRBinaryOp opL = (IRBinaryOp)tempL.Parent; - IRBinaryOp opR = (IRBinaryOp)tempR.Parent; + IRBinaryOp opL = (IRBinaryOp)instruction.Left; + IRBinaryOp opR = (IRBinaryOp)instruction.Right; short divLine = instruction.SourceLine; short divColumn = instruction.SourceColumn; @@ -516,7 +488,7 @@ private static void CombinePowers(IRBinaryOp instruction, BinaryOpcode newOperat // Special case for when N == 2 afterwards // then revert back to X * X - if (instruction.Right is IRConstant constantR && + if (instruction.Right is InterimConstantValue constantR && Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) { instruction.Right = instruction.Left; diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index 20c02daf5..12ba56cb0 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -25,7 +25,7 @@ public void ApplyPass() { IRCodePart codePart = Optimizer.Code; - Dictionary> ssaUses = + Dictionary> ssaUses = MapUsesAndPropagateTypes(codePart); if (Optimizer.OptimizationLevel > OptimizationLevel.None) @@ -44,12 +44,12 @@ public void ApplyPass() /// A dictionary of instructions (or phi variables) that make use /// of SSA variables (indirectly or directly). /// - private static Dictionary> MapUsesAndPropagateTypes(IRCodePart codePart) + private static Dictionary> MapUsesAndPropagateTypes(IRCodePart codePart) { - Dictionary> variableUses = - new Dictionary>(); + Dictionary> variableUses = + new Dictionary>(); HashSet visitedBlocks = new HashSet(); - Dictionary invariance = new Dictionary(); + Dictionary typeAndInvarianceCache = new Dictionary(); // Apply the algorithm starting from each entry block. foreach (BasicBlock root in codePart.RootBlocks) @@ -75,12 +75,12 @@ private static Dictionary> MapUses continue; // Process Phis first, as if they are instructions. - foreach (PhiVariable phi in block.Phis) + foreach (PhiNode phi in block.Phis.Values) { - foreach (SSAVariable variable in phi.PossibleValues.Values) + foreach (SSADefinition variable in phi.PossibleValues.Values) GetOrCreate(variableUses, variable).Add(phi); - VisitInstruction(phi, blockQueue, invariance); + VisitInstruction(phi, blockQueue, typeAndInvarianceCache); } // Process instructions. @@ -90,7 +90,7 @@ private static Dictionary> MapUses { inst.ForEachOperand(op => { - if (op is SSAVariable ssaVariable) + if (op is SSADefinition ssaVariable) { // Add this instruction to the list of uses for each operand. GetOrCreate(variableUses, ssaVariable).Add(inst); @@ -105,14 +105,26 @@ private static Dictionary> MapUses // Calls get to be special to address their external read needs. if (inst is IRCall call) { - string functionIdentifier = block.Scope.GetFunctionNameFromVariable(call.Function); - IRCodePart.IRFunction function = codePart.GetFunction(functionIdentifier); + IRCodePart.IRFunction function = codePart.GetFunction(call); if (function != null) { - foreach (IRVariable externalVar in function.ExternalReads.Union(function.ExternalWrites)) + Dictionary<(string Name, IRScope Scope), SSADefinition> variables = SingleStaticAssignment.ReachableVariables[call]; + + foreach (string variableName in function.ExternalReads.Union( + function.ExternalWrites)) { - if (externalVar is SSAVariable ssaVariable) - GetOrCreate(variableUses, ssaVariable).Add(call); + IRScope scope = block.Scope; + while (scope != null) + { + if (variables.TryGetValue((variableName, scope), out SSADefinition definition) && + definition.State != SSADefinition.SetState.Unset) + { + GetOrCreate(variableUses, definition).Add(call); + if (definition.State == SSADefinition.SetState.Set) + break; + } + scope = scope.ParentScope; + } } } } @@ -120,7 +132,7 @@ private static Dictionary> MapUses // inadvertently changing the return value). // The else if the next if statement. if (inst != instruction || !(instruction is IOperandInstructionBase)) - VisitInstruction(inst, blockQueue, invariance); + VisitInstruction(inst, blockQueue, typeAndInvarianceCache); } @@ -128,13 +140,14 @@ private static Dictionary> MapUses { operandInstruction.ForEachOperand(op => { - if (op is SSAVariable ssaVariable) + // TODO: Come back to this... + if (op is InterimVariableReference ssaRef && + ssaRef.Reference is SSASetDefinition ssaVariable) GetOrCreate(variableUses, ssaVariable).Add(operandInstruction); }); - if (VisitInstruction(operandInstruction, blockQueue, invariance) && + if (VisitInstruction(operandInstruction, blockQueue, typeAndInvarianceCache) && operandInstruction is IRAssign assignment && - assignment.Target is SSAVariable ssaTarget && - variableUses.TryGetValue(ssaTarget, out HashSet uses)) + variableUses.TryGetValue(assignment.Target, out HashSet uses)) foreach (IOperandInstructionBase use in uses) instructionQueue.Enqueue(use); } @@ -152,11 +165,10 @@ assignment.Target is SSAVariable ssaTarget && while (instructionQueue.Count > 0) { IOperandInstructionBase instruction = instructionQueue.Dequeue(); - if (VisitInstruction(instruction, blockQueue, invariance)) + if (VisitInstruction(instruction, blockQueue, typeAndInvarianceCache)) { - if (instruction is IRAssign assignment && - assignment.Target is SSAVariable ssaVariable) - foreach (IOperandInstructionBase use in GetOrCreate(variableUses, ssaVariable)) + if (instruction is IRAssign assignment) + foreach (IOperandInstructionBase use in GetOrCreate(variableUses, assignment.Target)) instructionQueue.Enqueue(use); else if (instruction is PhiVariable phi) foreach (IOperandInstructionBase use in GetOrCreate(variableUses, phi)) @@ -176,64 +188,43 @@ assignment.Target is SSAVariable ssaTarget && /// true if the instruction needs to propagate changes. private static bool VisitInstruction(IOperandInstructionBase instruction, Queue blockQueue, - Dictionary invariance) + Dictionary typeAndInvarianceCache) { if (instruction is IRAssign assignment) { - bool result = assignment.Target.ValueType != assignment.Value.ValueType; - assignment.Target.ValueType = assignment.Value.ValueType; - if (assignment.Target is SSAVariable ssaVariable) + SSASetDefinition ssaVariable = assignment.Target; + if (typeAndInvarianceCache.TryGetValue(ssaVariable, out (Type storedType, bool storedInvariance) cached)) { - if (invariance.TryGetValue(ssaVariable, out bool storedInvariance)) - { - invariance[ssaVariable] &= ssaVariable.IsInvariant; - result |= storedInvariance != invariance[ssaVariable]; - } - else - { - invariance[ssaVariable] = ssaVariable.IsInvariant; - result = true; - } + typeAndInvarianceCache[ssaVariable] = (ssaVariable.Type, cached.storedInvariance &= ssaVariable.IsInvariant); + return cached != typeAndInvarianceCache[ssaVariable]; + } + else + { + typeAndInvarianceCache[ssaVariable] = (ssaVariable.Type, ssaVariable.IsInvariant); + return true; } - - return result; } else if (instruction is PhiVariable phi) { - Type pastType = phi.ValueType; - phi.RefreshType(); - - bool result = pastType != phi.ValueType; - - if (invariance.TryGetValue(phi, out bool storedInvariance)) + if (typeAndInvarianceCache.TryGetValue(phi, out (Type storedType, bool storedInvariance) cached)) { - invariance[phi] &= storedInvariance; - result |= storedInvariance != invariance[phi]; + typeAndInvarianceCache[phi] = (phi.Type, cached.storedInvariance &= phi.IsInvariant); + return cached != typeAndInvarianceCache[phi]; } else { - invariance[phi] = phi.IsInvariant; - result = true; + typeAndInvarianceCache[phi] = (phi.Type, phi.IsInvariant); + return true; } - return result; } else if (instruction is IRBranch branch) { if (branch.IsInvariant) { - IRConstant constantCondition = branch.Condition as IRConstant; - if (constantCondition == null && branch.Condition is IRTemp temp) - constantCondition = ConstantFolding.AttemptReduction(temp.Parent) as IRConstant; + InterimConstantValue constantCondition = (branch.Condition as IEvaluatableToConstant).Evaluate(); + bool result = Convert.ToBoolean(constantCondition.Value); - if (constantCondition != null && constantCondition.Value is BooleanValue boolean) - { - blockQueue.Enqueue(boolean ? branch.True : branch.False); - } - else - { - blockQueue.Enqueue(branch.True); - blockQueue.Enqueue(branch.False); - } + blockQueue.Enqueue(result ? branch.True : branch.False); } else { @@ -255,27 +246,31 @@ private static bool VisitInstruction(IOperandInstructionBase instruction, /// /// Propagates any constant SSA variables to their uses. /// - private static void PropagateConstants(Dictionary> ssaUses) + private static void PropagateConstants(Dictionary> ssaUses) { // TODO: Keep track of which uses are replaced and if there are no more uses, remove the assignment. // But watch for functions that external read that variable and make sure to retain the assignment before then. - List constantVariables = ssaUses.Keys.Where(v => v.IsInvariant).ToList(); + // TODO: Look at branch instructions that employ eq and propagate that constant. + List constantVariables = ssaUses.Keys.Where( + v => v.State == SSADefinition.SetState.Set && + v.IsInvariant && + typeof(PrimitiveStructure).IsAssignableFrom(v.Type)) + .ToList(); - #if DEBUG // Sort to make debugging variable iterations easier. - constantVariables.Sort(Comparer.Create((a, b) => string.Compare(a.ToString(), b.ToString()))); + constantVariables.Sort(Comparer.Create((a, b) => string.Compare(a.ToString(), b.ToString()))); #endif - Queue queue = new Queue(constantVariables); - Dictionary replacements = new Dictionary(); + Queue queue = new Queue(constantVariables); + Dictionary replacements = new Dictionary(); while (queue.Count > 0) { - SSAVariable variable = queue.Dequeue(); + SSADefinition variable = queue.Dequeue(); if (queue.Any(v => { if (variable is PhiVariable phi && !replacements.ContainsKey(phi)) return true; - if (ssaUses[v].Contains(variable.AssignedAt)) + if (variable is SSASetDefinition setDef && ssaUses[v].Contains(setDef.DefinedAt)) return true; return false; })) @@ -284,18 +279,12 @@ private static void PropagateConstants(Dictionary code) for (int i = 0; i < code.Count; i++) { IRInstruction instruction = code[i]; - switch (instruction) + foreach (IRInstruction inst in instruction.DepthFirst()) { - case IRPop pop: - if (AttemptReplacement(pop.Value) is IRConstant) - { - code.RemoveAt(i); - i--; - } - continue; - case IRAssign assign: - assign.Value = AttemptReplacement(assign.Value); - break; - case IRSuffixSet suffixSet: - suffixSet.Value = AttemptReplacement(suffixSet.Value); - suffixSet.Object = AttemptReplacement(suffixSet.Object); - break; - case IRIndexSet indexSet: - indexSet.Value = AttemptReplacement(indexSet.Value); - indexSet.Object = AttemptReplacement(indexSet.Object); - indexSet.Index = AttemptReplacement(indexSet.Index); - break; - case IRBranch branch: - AttemptReplacement(branch.Condition); - break; + if (inst is IOperandInstructionBase operandInstruction) + { + operandInstruction.MutateEachOperand(AttemptReplacement); + } } } } - private static IRValue AttemptReplacement(IRValue input) + private static IInterimOperand AttemptReplacement(IInterimOperand operand) { - if (input is IRTemp temp) - { - if (temp.Parent is IRSuffixGet suffixGet) - { - AttempReplaceSuffix(suffixGet); - } - else - { - AttemptReplacement(temp.Parent); - } - } - return input; + if (operand is IRSuffixGet suffixGet) + return AttempReplaceSuffix(suffixGet); + return operand; } - private static IRValue AttempReplaceSuffix(IRSuffixGet suffixGet) + private static IInterimOperand AttempReplaceSuffix(IRSuffixGet suffixGet) { - if (suffixGet.Object is IRVariable objVariable) + if (suffixGet.Object is InterimVariableReference variableReference) { - if (objVariable.Name == "$ship") + if (variableReference.Name.Equals("$ship", StringComparison.OrdinalIgnoreCase)) { switch (suffixGet.Suffix) { @@ -98,24 +71,23 @@ private static IRValue AttempReplaceSuffix(IRSuffixGet suffixGet) break; // All other suffixes don't have an alias, so just return the original IRTemp default: - return suffixGet.Result; + return suffixGet; } // Instead of the IRTemp, which leads to resolving the suffix, return just the alias shortcut. - suffixGet.Result = new IRVariable($"${suffixGet.Suffix}", null, suffixGet.SourceLine, suffixGet.SourceColumn); - return suffixGet.Result; + return new InterimVariableReference($"${suffixGet.Suffix}", suffixGet.SourceLine, suffixGet.SourceColumn); } - if (objVariable.Name == "$constant") + if (variableReference.Name.Equals("$constant", StringComparison.OrdinalIgnoreCase)) { return ReplaceConstantSuffix(suffixGet); } } - else if (suffixGet.Object is IRTemp tempObj && tempObj.Parent is IRCall call && call.Function == "constant()" && call.Arguments.Count == 0) + else if (suffixGet.Object is IRCall call && call.Function.Equals("constant()", StringComparison.OrdinalIgnoreCase) && call.Arguments.Count == 0) { return ReplaceConstantSuffix(suffixGet); } - return suffixGet.Result; + return suffixGet; } - private static IRConstant ReplaceConstantSuffix(IRSuffixGet suffixGet) + private static InterimConstantValue ReplaceConstantSuffix(IRSuffixGet suffixGet) { Optimizer.InterimCPU.PushArgumentStack(new Encapsulation.ConstantValue()); try @@ -126,72 +98,7 @@ private static IRConstant ReplaceConstantSuffix(IRSuffixGet suffixGet) { throw new Exceptions.KOSCompileException(suffixGet, e); } - IRConstant result = new IRConstant(Optimizer.InterimCPU.PopValueArgument(), suffixGet); - suffixGet.Result = result; - return result; - } - private static IRValue AttemptReplacement(IRInstruction instruction) - { - switch (instruction) - { - case IRUnaryOp unaryOp: - return ReduceUnary(unaryOp); - case IRBinaryOp binaryOp: - return ReduceBinary(binaryOp); - case IRSuffixGet suffixGet: - return ReduceSuffixGet(suffixGet); - case IRIndexGet indexGet: - return ReduceIndexGet(indexGet); - case IRCall call: - return ReduceCall(call); - case IResultingInstruction resulting: - return resulting.Result; - } - throw new ArgumentException($"{instruction.GetType()} is not supported."); - } - private static IRValue ReduceUnary(IRUnaryOp instruction) - { - if (instruction.Operand is IRTemp temp) - instruction.Operand = AttemptReplacement(temp); - - return instruction.Result; - } - private static IRValue ReduceBinary(IRBinaryOp instruction) - { - if (instruction.Left is IRTemp tempL) - instruction.Left = AttemptReplacement(tempL); - if (instruction.Right is IRTemp tempR) - instruction.Right = AttemptReplacement(tempR); - - return instruction.Result; - } - - private static IRValue ReduceSuffixGet(IRSuffixGet instruction) - { - if (instruction.Object is IRTemp temp) - instruction.Object = AttemptReplacement(temp); - - return instruction.Result; - } - private static IRValue ReduceIndexGet(IRIndexGet instruction) - { - if (instruction.Object is IRTemp tempObj) - instruction.Object = AttemptReplacement(tempObj); - - if (instruction.Index is IRTemp tempIndex) - instruction.Index = AttemptReplacement(tempIndex); - - return instruction.Result; - } - private static IRValue ReduceCall(IRCall instruction) - { - for (int i = instruction.Arguments.Count - 1; i >= 0; i--) - { - if (instruction.Arguments[i] is IRTemp temp) - instruction.Arguments[i] = AttemptReplacement(temp); - } - - return instruction.Result; + return new InterimConstantValue(Optimizer.InterimCPU.PopValueArgument(), suffixGet); } } } From 8250589db8a575ade0ac53ad59182e664f4db971 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 8 May 2026 19:32:00 -0400 Subject: [PATCH 070/120] Make the Evaluate function on a constant only available when boxed as an IEvaluatableToConstant since that's the only time it makes sense. --- src/kOS.Safe/Compilation/IR/InterimConstantValue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs b/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs index aa99c6e4f..67bbe9bed 100644 --- a/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs +++ b/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs @@ -35,7 +35,7 @@ public override int GetHashCode() public override string ToString() => Value.ToString(); - public InterimConstantValue Evaluate() + InterimConstantValue IEvaluatableToConstant.Evaluate() => this; } From eb059f8a8ddceef58cc59e8d1b0f56004700dd03 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 8 May 2026 19:50:55 -0400 Subject: [PATCH 071/120] Fix function external variables read not reflecting the full range of possible values in the case of potentially unset variables. Fix variables unset in triggers being forgotten at the next assignment. --- .../integration/constantPropagation.ks | 5 +- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 9 + src/kOS.Safe/Compilation/IR/IRCodePart.cs | 6 +- .../Compilation/IR/InterimVariables.cs | 102 ++++++++-- .../Compilation/IR/SingleStaticAssignment.cs | 177 ++++++++++++------ .../Passes/SCCPWithTypePropagation.cs | 43 +++-- 6 files changed, 248 insertions(+), 94 deletions(-) diff --git a/kerboscript_tests/integration/constantPropagation.ks b/kerboscript_tests/integration/constantPropagation.ks index a44ed8106..5374fd6cb 100644 --- a/kerboscript_tests/integration/constantPropagation.ks +++ b/kerboscript_tests/integration/constantPropagation.ks @@ -3,7 +3,8 @@ global a is 5. global _false is false. -local b is 2. +local b is 1. +set b to 2. local c is 4. local d is 7. local h is false. @@ -13,7 +14,7 @@ print("test"). // The assignments should happen after this line. print(b+c). // 6 print(h). // False -global function func { +local function func { print(b+c). // 6 print(b+d). // 9 print(b+a). // 7 diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 78a6c8218..d8c5709e8 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -107,6 +107,15 @@ public IRScope Scope /// public HashSet<(string, IRScope)> TriggerPropagationBlacklist { get; } = new HashSet<(string, IRScope)>(); /// + /// Gets the set of variables that are blacklisted against + /// setting definitive values due to their being unset in active + /// triggers. + /// + /// + /// This data is populated during . + /// + public Dictionary<(string, IRScope), IRUnset> TriggerUnsetBlacklist { get; } = new Dictionary<(string, IRScope), IRUnset>(); + /// /// Gets the instruction label with which to start the block. /// The special prefix "@BB#" will be overwritten during linking. /// diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index bb33ac32e..41ba1be8f 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -198,7 +198,7 @@ public class IRTrigger : IClosureVariableUser /// Gets or sets the code for this trigger, in BasicBlock representation. /// public List Code { get; set; } - public HashSet ExternalReads { get; set; } = new HashSet(); + public HashSet ExternalReads { get; set; } = new HashSet(); public HashSet ExternalWrites { get; } = new HashSet(); public HashSet<(string, IRUnset)> ExternalUnsets { get; } = new HashSet<(string, IRUnset)>(); public HashSet TriggersCreated { get; } = new HashSet(); @@ -265,7 +265,7 @@ public class IRFunction : IClosureVariableUser /// Gets the collection of function fragments. /// public IReadOnlyCollection Fragments => fragments.Values; - public HashSet ExternalReads { get; set; } = new HashSet(); + public HashSet ExternalReads { get; set; } = new HashSet(); public HashSet ExternalWrites { get; } = new HashSet(); public HashSet<(string, IRUnset)> ExternalUnsets { get; } = new HashSet<(string, IRUnset)>(); public HashSet TriggersCreated { get; } = new HashSet(); @@ -364,7 +364,7 @@ public interface IClosureVariableUser /// Gets the collection of external variables that are read /// within the closure. /// - HashSet ExternalReads { get; set; } + HashSet ExternalReads { get; set; } /// /// Gets the collection of external variables that may be written /// to by this instance. diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs index 41c289688..c011036f8 100644 --- a/src/kOS.Safe/Compilation/IR/InterimVariables.cs +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -7,6 +7,8 @@ namespace kOS.Safe.Compilation.IR public interface IInterimVariableReference : IInterimOperand { string Name { get; } + short SourceLine { get; } + short SourceColumn { get; } } public readonly struct InterimVariableReference : IInterimVariableReference { @@ -19,6 +21,8 @@ public interface IInterimVariableReference : IInterimOperand public InterimVariableReference(string name, Opcode opcode) : this(name, opcode.SourceLine, opcode.SourceColumn) { } + public InterimVariableReference(string name, IRInstruction instruction) : + this(name, instruction.SourceLine, instruction.SourceColumn) { } public InterimVariableReference(string name, short sourceLine, short sourceColumn) { Name = name; @@ -44,23 +48,23 @@ public override int GetHashCode() => Name.ToLower().GetHashCode(); } - public readonly struct InterimVariableReference : IInterimVariableReference, IEvaluatableToConstant where T : SSADefinition + public readonly struct InterimResolvedReference : IInterimVariableReference, IEvaluatableToConstant { public short SourceLine { get; } public short SourceColumn { get; } - public T Reference { get; } + public SSADefinition Reference { get; } public string Name => Reference.Name; public bool IsInvariant => Reference.IsInvariant; public Type Type => Reference.Type; - public InterimVariableReference(T reference, short sourceLine, short sourceColumn) + public InterimResolvedReference(SSADefinition reference, IRInstruction instruction) : + this(reference, instruction.SourceLine, instruction.SourceColumn) { } + public InterimResolvedReference(SSADefinition reference, short sourceLine, short sourceColumn) { Reference = reference; SourceLine = sourceLine; SourceColumn = sourceColumn; } - public InterimVariableReference(T reference, InterimVariableReference oldRef) : - this(reference, oldRef.SourceLine, oldRef.SourceColumn) { } public IEnumerable EmitOpcodes() { @@ -81,12 +85,80 @@ public InterimConstantValue Evaluate() public override string ToString() => $"{Name}"; public override bool Equals(object obj) - => obj is InterimVariableReference variable && + => obj is InterimResolvedReference variable && Reference.Equals(variable.Reference); public override int GetHashCode() => Reference.GetHashCode(); } + public class InterimUnresolvedReference : IInterimVariableReference + { + private readonly List references; + + public short SourceLine { get; } + public short SourceColumn { get; } + public IReadOnlyCollection References => references; + public string Name { get; } + public bool IsInvariant => false; + public Type Type + { + get + { + Type proposedType = References.First().Type; + foreach (SSADefinition variable in References.Skip(1)) + proposedType = PhiNode.GetFirstCommonBaseType(proposedType, variable.Type); + + return proposedType; + } + } + + private InterimUnresolvedReference(string name, short sourceLine, short sourceColumn) + { + Name = name; + SourceLine = sourceLine; + SourceColumn = sourceColumn; + } + public InterimUnresolvedReference(SSADefinition reference, short sourceLine, short sourceColumn) : + this(reference.Name, sourceLine, sourceColumn) + { + references = new List + { + reference + }; + } + public InterimUnresolvedReference(IEnumerable references, short sourceLine, short sourceColumn) : + this(references.First().Name, sourceLine, sourceColumn) + { + this.references = new List(); + foreach (SSADefinition reference in references) + AddReference(reference); + } + + public void AddReference(SSADefinition reference) + { + if (!string.Equals(reference.Name, Name, StringComparison.OrdinalIgnoreCase)) + throw new ArgumentException($"Names must match: {reference.Name} vs. {Name}"); + references.Add(reference); + } + + public IEnumerable EmitOpcodes() + { + yield return new OpcodePush(Name) + { + SourceLine = SourceLine, + SourceColumn = SourceColumn + }; + } + + public override string ToString() + => $"{Name}"; + public override bool Equals(object obj) + => obj is InterimUnresolvedReference unresolvedRef && + unresolvedRef.references.SequenceEqual(references); + public override int GetHashCode() + => References.GetHashCode(); + } + internal static class SSAIndexIssuer { private static readonly Dictionary indices = new Dictionary(); @@ -241,8 +313,8 @@ public IEnumerable Operands { short sourceLine = Conditional?.SourceLine ?? -1; short sourceColumn = Conditional?.SourceColumn ?? -1; - yield return new InterimVariableReference(Preceding, sourceLine, sourceColumn); - yield return new InterimVariableReference(Succeeding, sourceLine, sourceColumn); + yield return new InterimResolvedReference(Preceding, sourceLine, sourceColumn); + yield return new InterimResolvedReference(Succeeding, sourceLine, sourceColumn); } } public int OperandCount => 2; @@ -299,8 +371,8 @@ public override SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt) public void ForEachOperand(Action action) { - action(new InterimVariableReference(Preceding, 0, 0)); - action(new InterimVariableReference(Succeeding, 0, 0)); + action(new InterimResolvedReference(Preceding, 0, 0)); + action(new InterimResolvedReference(Succeeding, 0, 0)); } public void MutateEachOperand(Func mutateFunc) { @@ -309,8 +381,8 @@ public void MutateEachOperand(Func mutateFunc) } private static SSADefinition Mutate(Func func, SSADefinition definition) { - IInterimOperand result = func(new InterimVariableReference(definition, 0, 0)); - return ((InterimVariableReference)result).Reference; + IInterimOperand result = func(new InterimResolvedReference(definition, 0, 0)); + return ((InterimResolvedReference)result).Reference; } public override InterimConstantValue Evaluate() { @@ -394,7 +466,7 @@ public PhiNode(string name) SSASetDefinition ssaDef = kvp.Value as SSASetDefinition; short sourceLine = ssaDef?.DefinedAt.SourceLine ?? -1; short sourceColumn = ssaDef?.DefinedAt.SourceColumn ?? -1; - return new InterimVariableReference(kvp.Value, sourceLine, sourceColumn) as IInterimOperand; + return new InterimResolvedReference(kvp.Value, sourceLine, sourceColumn) as IInterimOperand; }); protected override InterimConstantValue EvaluateObj(SSADefinition obj) @@ -403,13 +475,13 @@ protected override InterimConstantValue EvaluateObj(SSADefinition obj) protected override void ForEachOperand(Action action) { foreach (SSADefinition variable in PossibleValues.Values) - action(new InterimVariableReference(variable, -1, -1)); + action(new InterimResolvedReference(variable, -1, -1)); } protected override void MutateEachOperand(Func mutateFunc) { foreach (BasicBlock block in PossibleValues.Keys) - PossibleValues[block] = ((InterimVariableReference)mutateFunc(new InterimVariableReference(PossibleValues[block], -1, -1))).Reference; + PossibleValues[block] = ((InterimResolvedReference)mutateFunc(new InterimResolvedReference(PossibleValues[block], -1, -1))).Reference; } } diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index 4141f26ae..51f0f5eac 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -12,11 +12,8 @@ namespace kOS.Safe.Compilation.IR /// public static class SingleStaticAssignment { - private static readonly Dictionary> reachableVariables = - new Dictionary>(); - - public static IReadOnlyDictionary> - ReachableVariables => reachableVariables; + public static Dictionary> ReachableVariables { get; } = + new Dictionary>(); /// /// Finalizes a program into single static assignment form. @@ -77,13 +74,26 @@ public static void FinalizeSSA(IRCodePart codePart) BuildPhis(codePart.MainCode[0], codePart, null); // Then apply the SSA definitions to all the variable push operations. - foreach (IRFunction function in codePart.Functions) + // Functions are done iteratively to propagate the ExternalReads property + // up the call chain correctly. + foreach (IRFunction function in codePart.Functions.OrderBy(f => f.FunctionCalls.Count)) + functionQueue.Enqueue(function); + while (functionQueue.Count > 0) { + IRFunction function = functionQueue.Dequeue(); + HashSet externalReads = new HashSet(function.ExternalReads); foreach (BasicBlock block in function.InitializationCode) ApplyUses(block, codePart, function); foreach (IRFunction.IRFunctionFragment fragment in function.Fragments) foreach (BasicBlock block in fragment.FunctionCode) ApplyUses(block, codePart, function); + + if (callers.ContainsKey(function) && + !externalReads.SetEquals(function.ExternalReads)) + { + foreach (IRFunction caller in callers[function]) + functionQueue.Enqueue(caller); + } } foreach (IRTrigger trigger in codePart.Triggers) { @@ -109,7 +119,7 @@ public static void FinalizeSSA(IRCodePart codePart) foreach (IRCall call in instruction.DepthFirst().Where(inst => inst is IRCall).Cast()) { - ProcessCall(call, codePart, variables, block.TriggerPropagationBlacklist, funcOrTrigger); + ProcessCall(call, codePart, variables, block.TriggerPropagationBlacklist, block.TriggerUnsetBlacklist, funcOrTrigger); IRFunction function = codePart.GetFunction(call); if (function != null) funcOrTrigger?.FunctionCalls.Add(codePart.GetFunction(call)); @@ -119,7 +129,7 @@ public static void FinalizeSSA(IRCodePart codePart) switch (instruction) { case IRAssign assignment: - if (ProcessAssignment(assignment, codePart, variables, block.TriggerPropagationBlacklist)) + if (ProcessAssignment(assignment, codePart, variables, block.TriggerPropagationBlacklist, block.TriggerUnsetBlacklist)) funcOrTrigger?.ExternalWrites.Add(assignment.Target.Name); break; case IRUnset unset: @@ -132,29 +142,39 @@ public static void FinalizeSSA(IRCodePart codePart) { string pointer = (string)((InterimConstantValue)unaryConsumer.Operand).Value; IRTrigger trigger = codePart.GetTrigger(pointer); - ProcessTrigger(trigger, variables, block.TriggerPropagationBlacklist); + ProcessTrigger(trigger, variables, block.TriggerPropagationBlacklist, block.TriggerUnsetBlacklist); funcOrTrigger?.TriggersCreated.Add(trigger); } break; + case IRReturn ret: + if (funcOrTrigger is IRFunction function) + function.Returns.PossibleValues[block] = ret.Value; + break; } } return variables; } - private static bool ProcessAssignment(IRAssign assignment, IRCodePart codePart, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> blacklist) + private static bool ProcessAssignment(IRAssign assignment, IRCodePart codePart, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist) { IRScope startingScope = assignment.Block.Scope; SSASetDefinition definition = assignment.Target; switch (assignment.Scope) { case IRAssign.StoreScope.Local: - variables[(definition.Name, startingScope)] = definition; + if (writeBlacklist.TryGetValue((definition.Name, startingScope), out IRUnset unset)) + variables[(definition.Name, startingScope)] = definition.PotentiallyUnset(unset); + else + variables[(definition.Name, startingScope)] = definition; // IRFunction.IsGlobal initiates as false, so there's no need to |= false. break; case IRAssign.StoreScope.Global: - variables[(definition.Name, startingScope.GetGlobalScope())] = definition; - SetFunctionToGlobal(assignment, codePart, variables, blacklist); + if (writeBlacklist.TryGetValue((definition.Name, startingScope), out IRUnset globalUnset)) + variables[(definition.Name, startingScope)] = SSAPotentialDefinition.PotentiallySet(definition, globalUnset); + else + variables[(definition.Name, startingScope.GetGlobalScope())] = definition; + SetFunctionToGlobal(assignment, codePart, variables, readBlacklist, writeBlacklist); break; default: if (ApplyDefinitionToName(definition, startingScope, variables)) @@ -162,7 +182,7 @@ private static bool ProcessAssignment(IRAssign assignment, IRCodePart codePart, // This isn't necessarily true in the case of nested function definitions. // But true function definitions are definitively set as local or global. // This only applies to a nested lock, which deserves to lose out on optimizations. - SetFunctionToGlobal(assignment, codePart, variables, blacklist); + SetFunctionToGlobal(assignment, codePart, variables, readBlacklist, writeBlacklist); return true; } break; @@ -224,7 +244,7 @@ private static bool ApplyDefinitionToName(SSASetDefinition definition, IRScope s return false; } private static void SetFunctionToGlobal(IRAssign assignment, IRCodePart codePart, Dictionary<(string VariableName, IRScope Scope), - SSADefinition> variables, HashSet<(string, IRScope)> blacklist) + SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist) { if (codePart == null || !(assignment.Value is IRRelocateLater funcOrTrigger)) @@ -236,7 +256,7 @@ private static void SetFunctionToGlobal(IRAssign assignment, IRCodePart codePart // A global function could be added to a trigger in external code. // Treat it as a trigger body itself. - ProcessTrigger(function, variables, blacklist); + ProcessTrigger(function, variables, readBlacklist, writeBlacklist); } private static void ProcessUnset(IRUnset unset, Dictionary<(string Name, IRScope), SSADefinition> variables, HashSet<(string, IRUnset)> recordTo) @@ -312,18 +332,20 @@ private static bool PotentiallyUnsetByName(IRUnset unset, string varName, IRScop return closureVariableAffected; } - private static void ProcessTrigger(IClosureVariableUser trigger, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> blacklist) + private static void ProcessTrigger(IClosureVariableUser trigger, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist) { foreach ((string varName, IRUnset unset) in trigger.ExternalUnsets) { IRScope scope = trigger.ClosureScope; while (scope != null) { + writeBlacklist[(varName, scope)] = unset; if (variables.TryGetValue((varName, scope), out SSADefinition ssaDef) && ssaDef.State != SSADefinition.SetState.Unset) { variables[(varName, scope)] = ssaDef.PotentiallyUnset(unset); } + scope = scope.ParentScope; } } foreach (string varName in trigger.ExternalWrites) @@ -331,22 +353,18 @@ private static void ProcessTrigger(IClosureVariableUser trigger, Dictionary<(str IRScope scope = trigger.ClosureScope; while (scope != null) { - if (variables.TryGetValue((varName, scope), out SSADefinition ssaDef) && - ssaDef.State != SSADefinition.SetState.Unset) - { - blacklist.Add((varName, scope)); - } + readBlacklist.Add((varName, scope)); scope = scope.ParentScope; } } } - private static void ProcessCall(IRCall call, IRCodePart codePart, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> blacklist, IClosureVariableUser callingClosure) + private static void ProcessCall(IRCall call, IRCodePart codePart, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist, IClosureVariableUser callingClosure) { IRFunction function = codePart.GetFunction(call); if (function != null) { - HandleFunction(call, function, variables, blacklist, callingClosure); + HandleFunction(call, function, variables, readBlacklist, writeBlacklist, callingClosure); } else if (!call.Direct && !(call.IndirectMethod is IRSuffixGetMethod)) { @@ -357,11 +375,11 @@ private static void ProcessCall(IRCall call, IRCodePart codePart, Dictionary<(st // function in this code part. foreach (IRFunction func in codePart.Functions) { - HandleFunction(call, func, variables, blacklist, callingClosure); + HandleFunction(call, func, variables, readBlacklist, writeBlacklist, callingClosure); } } } - static void HandleFunction(IRCall call, IRFunction function, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> blacklist, IClosureVariableUser callingClosure) + static void HandleFunction(IRCall call, IRFunction function, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist, IClosureVariableUser callingClosure) { HashSet externalWrites = callingClosure?.ExternalWrites; HashSet<(string, IRUnset)> externalUnsets = callingClosure?.ExternalUnsets; @@ -417,7 +435,7 @@ static void HandleFunction(IRCall call, IRFunction function, Dictionary<(string, // This is handled as normal for a trigger. foreach (IRTrigger trigger in function.TriggersCreated) { - ProcessTrigger(trigger, variables, blacklist); + ProcessTrigger(trigger, variables, readBlacklist, writeBlacklist); } } @@ -433,6 +451,7 @@ private static void BuildPhis(BasicBlock root, IRCodePart codePart, IClosureVari BasicBlock block = worklist.Dequeue(); HashSet<(string, IRScope)> blacklist = block.TriggerPropagationBlacklist; + Dictionary<(string, IRScope), IRUnset> writeBlacklist = block.TriggerUnsetBlacklist; List<(BasicBlock Block, IRScope Scope, SSADefinition Variable)> varsIn = new List<(BasicBlock, IRScope, SSADefinition)>(); // Make the blacklist the union of all incoming blacklists @@ -440,6 +459,8 @@ private static void BuildPhis(BasicBlock root, IRCodePart codePart, IClosureVari foreach (BasicBlock predecessor in block.Predecessors) { blacklist.UnionWith(predecessor.TriggerPropagationBlacklist); + foreach (KeyValuePair<(string, IRScope), IRUnset> item in writeBlacklist) + writeBlacklist[item.Key] = item.Value; if (variablesOut.TryGetValue(predecessor, out Dictionary<(string Name, IRScope Scope), SSADefinition> predVarsOut)) { foreach (KeyValuePair<(string Name, IRScope Scope), SSADefinition> variable in predVarsOut @@ -515,8 +536,13 @@ private static void ApplyUses(BasicBlock block, IRCodePart codePart, IClosureVar // Start with the union of all incoming trigger blacklists. HashSet<(string, IRScope)> triggerBlacklist = new HashSet<(string, IRScope)>(); + Dictionary<(string, IRScope), IRUnset> triggerWriteBlacklist = new Dictionary<(string, IRScope), IRUnset>(); foreach (BasicBlock predecessor in block.Predecessors) + { triggerBlacklist.UnionWith(predecessor.TriggerPropagationBlacklist); + foreach (KeyValuePair<(string, IRScope), IRUnset> item in predecessor.TriggerUnsetBlacklist) + triggerWriteBlacklist[item.Key] = item.Value; + } // Create a local function for SSA replacement for this specific block. IInterimOperand ScopedSSAReplacement(IInterimOperand op) @@ -529,16 +555,11 @@ IInterimOperand ScopedSSAReplacement(IInterimOperand op) { if (inst is IRCall call) { - ProcessCall(call, codePart, liveDefinitions, triggerBlacklist, null); + ProcessCall(call, codePart, liveDefinitions, triggerBlacklist, triggerWriteBlacklist, null); IRFunction function = codePart.GetFunction(call); if (function != null) { - Dictionary<(string, IRScope), SSADefinition> reachableVariables = - new Dictionary<(string, IRScope), SSADefinition>(); - foreach ((string, IRScope) slot in liveDefinitions.Keys - .Where(k => function.ExternalReads.Contains(k.Name))) - reachableVariables.Add(slot, liveDefinitions[slot]); - SingleStaticAssignment.reachableVariables[call] = reachableVariables; + ReachableVariables[call] = DetermineCallReaches(call, function, funcOrTrigger, liveDefinitions, triggerBlacklist); } } if (inst is IOperandInstructionBase operandInstruction) @@ -549,7 +570,7 @@ IInterimOperand ScopedSSAReplacement(IInterimOperand op) switch (instruction) { case IRAssign assignment: - ProcessAssignment(assignment, null, liveDefinitions, triggerBlacklist); + ProcessAssignment(assignment, null, liveDefinitions, triggerBlacklist, triggerWriteBlacklist); break; case IRUnset unset: ProcessUnset(unset, liveDefinitions, null); @@ -559,7 +580,7 @@ IInterimOperand ScopedSSAReplacement(IInterimOperand op) { string pointer = (string)((InterimConstantValue)irTrigger.Operand).Value; IRTrigger trigger = codePart.GetTrigger(pointer); - ProcessTrigger(trigger, liveDefinitions, triggerBlacklist); + ProcessTrigger(trigger, liveDefinitions, triggerBlacklist, triggerWriteBlacklist); } break; } @@ -571,23 +592,13 @@ private static IInterimOperand SSAReplacement(IInterimOperand operand, IRScope s { if (operand is InterimVariableReference variableRef) { - string name = variableRef.Name; - // Don't apply to the global scope since SSA cannot be - // guaranteed for global variables. - while (!scope.IsGlobalScope) - { - if (liveDefinitions.TryGetValue((name, scope), out SSADefinition value) && - value.State != SSADefinition.SetState.Unset) - { - if (triggerBlacklist.Contains((name, scope))) - { - return variableRef; - } - return new InterimVariableReference(value, variableRef); - } - scope = scope.ParentScope; - } - funcOrTrigger?.ExternalReads.Add(name); + IInterimVariableReference result = AttemptResolveReference( + variableRef, scope, + liveDefinitions, triggerBlacklist, out bool exceededClosure); + + if (exceededClosure) + funcOrTrigger?.ExternalReads.Add(result); + return result; } else if (operand is IRUnaryOp existOp && existOp.Operation is OpcodeExists) { @@ -608,11 +619,69 @@ private static IInterimOperand SSAReplacement(IInterimOperand operand, IRScope s return new InterimConstantValue(Encapsulation.BooleanValue.True, existOp); scope = scope.ParentScope; } - funcOrTrigger?.ExternalReads.Add(name); + funcOrTrigger?.ExternalReads.Add(new InterimVariableReference(name, existOp)); } } return operand; } } + + private static IInterimVariableReference AttemptResolveReference(IInterimVariableReference variableRef, IRScope startingScope, Dictionary<(string, IRScope), SSADefinition> liveDefinitions, HashSet<(string, IRScope)> triggerBlacklist, out bool exceededClosure) + { + exceededClosure = false; + string name = variableRef.Name; + bool blacklisted = false; + IRScope scope = startingScope; + InterimUnresolvedReference unresolvedReference = null; + // Don't apply to the global scope since SSA cannot be + // guaranteed for global variables. + while (!scope.IsGlobalScope) + { + if (liveDefinitions.TryGetValue((name, scope), out SSADefinition value) && + value.State != SSADefinition.SetState.Unset) + { + if (triggerBlacklist.Contains((name, scope))) + { + blacklisted = true; + unresolvedReference = null; + } + + if (unresolvedReference != null) + { + unresolvedReference.AddReference(value); + if (value.State == SSADefinition.SetState.Set) + return unresolvedReference; + } + else if (value.State == SSADefinition.SetState.Set) + { + if (blacklisted) + return variableRef; + return new InterimResolvedReference(value, variableRef.SourceLine, variableRef.SourceColumn); + } + else if (!blacklisted) + unresolvedReference = new InterimUnresolvedReference(value, variableRef.SourceLine, variableRef.SourceColumn); + } + scope = scope.ParentScope; + } + exceededClosure = true; + return variableRef; + } + + private static HashSet DetermineCallReaches(IRCall call, IRFunction function, IClosureVariableUser funcOrTrigger, Dictionary<(string, IRScope), SSADefinition> liveDefinitions, HashSet<(string, IRScope)> triggerBlacklist) + { + HashSet reachableVariables = new HashSet(); + + foreach (string name in function.ExternalReads.Select(v => v.Name).Union(function.ExternalWrites)) + { + IInterimVariableReference result = AttemptResolveReference( + new InterimVariableReference(name, call), function.ClosureScope, + liveDefinitions, triggerBlacklist, out bool exceededClosure); + if (exceededClosure) + funcOrTrigger?.ExternalReads.Add(result); + if (!(result is InterimVariableReference)) + reachableVariables.Add(result); + } + return reachableVariables; + } } } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index 12ba56cb0..3320b53de 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -108,23 +108,10 @@ private static Dictionary> MapUs IRCodePart.IRFunction function = codePart.GetFunction(call); if (function != null) { - Dictionary<(string Name, IRScope Scope), SSADefinition> variables = SingleStaticAssignment.ReachableVariables[call]; - - foreach (string variableName in function.ExternalReads.Union( - function.ExternalWrites)) + HashSet variables = SingleStaticAssignment.ReachableVariables[call]; + foreach (SSADefinition variable in variables.SelectMany(GetSSADefinitionsFromReferences)) { - IRScope scope = block.Scope; - while (scope != null) - { - if (variables.TryGetValue((variableName, scope), out SSADefinition definition) && - definition.State != SSADefinition.SetState.Unset) - { - GetOrCreate(variableUses, definition).Add(call); - if (definition.State == SSADefinition.SetState.Set) - break; - } - scope = scope.ParentScope; - } + GetOrCreate(variableUses, variable).Add(call); } } } @@ -140,10 +127,13 @@ private static Dictionary> MapUs { operandInstruction.ForEachOperand(op => { - // TODO: Come back to this... - if (op is InterimVariableReference ssaRef && - ssaRef.Reference is SSASetDefinition ssaVariable) - GetOrCreate(variableUses, ssaVariable).Add(operandInstruction); + if (op is InterimResolvedReference ssaRef) + GetOrCreate(variableUses, ssaRef.Reference).Add(operandInstruction); + if (op is InterimUnresolvedReference unresolvedRef) + { + foreach (SSADefinition ssaDef in GetSSADefinitionsFromReferences(unresolvedRef)) + GetOrCreate(variableUses, ssaDef).Add(operandInstruction); + } }); if (VisitInstruction(operandInstruction, blockQueue, typeAndInvarianceCache) && operandInstruction is IRAssign assignment && @@ -181,6 +171,19 @@ operandInstruction is IRAssign assignment && return variableUses; } + private static IEnumerable GetSSADefinitionsFromReferences(IInterimVariableReference reference) + { + switch (reference) + { + case InterimResolvedReference resolvedReference: + return Enumerable.Repeat(resolvedReference.Reference, 1); + case InterimUnresolvedReference unresolvedReference: + return unresolvedReference.References; + default: + throw new NotImplementedException(); + } + } + /// /// Visits an instruction and determines if it has changes /// that need to be propagated. From 49e6e7ee414fd4de34509d3bd04f532b525ff531 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 8 May 2026 20:03:01 -0400 Subject: [PATCH 072/120] Add traceability for which assignments a scope receives (or may receive). --- src/kOS.Safe/Compilation/IR/IRScope.cs | 9 ++++++++- src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs | 5 +++++ .../Optimization/Passes/UnnecessaryScopeRemoval.cs | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRScope.cs b/src/kOS.Safe/Compilation/IR/IRScope.cs index 0430c361a..7c10b9c7e 100644 --- a/src/kOS.Safe/Compilation/IR/IRScope.cs +++ b/src/kOS.Safe/Compilation/IR/IRScope.cs @@ -53,10 +53,17 @@ public IRScope ParentScope /// public BasicBlock FooterBlock { get; set; } /// - /// Gets the collection of variables associated with this scope. + /// Gets the collection of variable names associated with this scope. /// public IReadOnlyCollection Variables => variables; /// + /// Gets the set of assignments that are made (or possibly made) to this scope. + /// + /// + /// These are populated in . + /// + public HashSet Assignments { get; } = new HashSet(); + /// /// Gets a value indicating whether this instance represents the /// global scope. /// diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index 51f0f5eac..0ca74441d 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -167,6 +167,7 @@ private static bool ProcessAssignment(IRAssign assignment, IRCodePart codePart, variables[(definition.Name, startingScope)] = definition.PotentiallyUnset(unset); else variables[(definition.Name, startingScope)] = definition; + startingScope.Assignments.Add(assignment); // IRFunction.IsGlobal initiates as false, so there's no need to |= false. break; case IRAssign.StoreScope.Global: @@ -174,6 +175,7 @@ private static bool ProcessAssignment(IRAssign assignment, IRCodePart codePart, variables[(definition.Name, startingScope)] = SSAPotentialDefinition.PotentiallySet(definition, globalUnset); else variables[(definition.Name, startingScope.GetGlobalScope())] = definition; + startingScope.GetGlobalScope().Assignments.Add(assignment); SetFunctionToGlobal(assignment, codePart, variables, readBlacklist, writeBlacklist); break; default: @@ -208,6 +210,7 @@ private static bool ApplyDefinitionToName(SSASetDefinition definition, IRScope s variables[(name, scope)] = SSAPotentialDefinition.PotentiallySet(definition, (IRUnset)lastPotential.AssignedAt); else variables[(name, scope)] = definition; + scope.Assignments.Add(definition.DefinedAt); // Since the SSA algorithm for a function won't know // of any slots filled between the function's top and the // global scope, this is where propagation to the higher @@ -227,6 +230,7 @@ private static bool ApplyDefinitionToName(SSASetDefinition definition, IRScope s variables[(name, scope)] = slotValue.PotentiallyOverwrite(definition); else variables[(name, scope)] = definition.PotentiallyUnset((IRUnset)slotValue.AssignedAt); + scope.Assignments.Add(definition.DefinedAt); potential = true; lastPotential = slotValue; } @@ -237,6 +241,7 @@ private static bool ApplyDefinitionToName(SSASetDefinition definition, IRScope s variables[(name, scope)] = slotValue.PotentiallyOverwrite(definition); else variables[(name, scope)] = definition; + scope.Assignments.Add(definition.DefinedAt); break; } scope = scope.ParentScope; diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs index 868846623..32fe845ed 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs @@ -21,7 +21,7 @@ public void ApplyPass(List code) foreach (IRScope scope in scopes) { - if (scope.Variables.Any()) + if (scope.Assignments.Any(a => a.Block.IsExecutable)) continue; if (scope.IsGlobalScope) From 495d963f8d0993b47ba98e7b612a6d1eda13a541 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 8 May 2026 22:35:56 -0400 Subject: [PATCH 073/120] Fix function return type and invariant status not being queried correctly. --- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 35 ++++++------------- .../Optimization/Passes/ConstantFolding.cs | 1 - 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 23c684b7f..ed2ba20ce 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -757,29 +757,12 @@ public override int GetHashCode() } public class IRCall : MultipleOperandInstruction, IResultingInstruction { - private bool isResultTypeInformed = false; - private Type resultType = null; - private bool? isFunctionInvariant = null; - - public bool IsFunctionInvariant - { - get => isFunctionInvariant ?? IsCallInvariant(Function); - set => isFunctionInvariant = value; - } - public override bool IsInvariant => IsFunctionInvariant && Arguments.All(a => a.IsInvariant); + public override bool IsInvariant => IsCallInvariant() && Arguments.All(a => a.IsInvariant); public string Function { get; } public List Arguments { get; } = new List(); public override IEnumerable Operands => Enumerable.Reverse(Arguments); public override int OperandCount => Arguments.Count; - public Type Type - { - get => isResultTypeInformed ? resultType : GetDefaultReturnType(); - set - { - resultType = value; - isResultTypeInformed = true; - } - } + public Type Type => GetDefaultReturnType(); protected override IInterimOperand this[int index] { get => Arguments[index]; @@ -794,20 +777,22 @@ private IRCall(BasicBlock block, OpcodeCall opcode, bool emitArgMarker) : base(o Direct = opcode.Direct; EmitArgMarker = emitArgMarker; } - private bool IsCallInvariant(string functionName) + private bool IsCallInvariant() { // TODO: Consider that some suffix methods may actually be known at compile time. if (!Direct) return false; - if (Optimization.Optimizer.FunctionManager.Exists(functionName)) - return Optimization.Optimizer.FunctionManager.IsFunctionInvariant(functionName); + if (Optimization.Optimizer.FunctionManager.Exists(Function.Replace("()", ""))) + return Optimization.Optimizer.FunctionManager.IsFunctionInvariant(Function.Replace("()", "")); return false; - } private Type GetDefaultReturnType() { - if (Optimization.Optimizer.FunctionManager.Exists(Function)) - return Optimization.Optimizer.FunctionManager.FunctionReturnType(Function); + IRCodePart.IRFunction function = Block.CodePart.GetFunction(this); + if (function != null) + return function.Returns.Type; + if (Optimization.Optimizer.FunctionManager.Exists(Function.Replace("()", ""))) + return Optimization.Optimizer.FunctionManager.FunctionReturnType(Function.Replace("()", "")); if (!Direct && IndirectMethod is IRSuffixGetMethod suffixGetMethod) return suffixGetMethod.Type; return typeof(Encapsulation.Structure); diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index ca64d0db4..27fbdfc46 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -216,7 +216,6 @@ private static bool ReduceDivMult(IRBinaryOp instruction, InterimConstantValue s private static IInterimOperand ReduceCall(IRCall instruction) { - // TODO: Add reduction for specific functions. E.g. mod(X, 1) = 0, round/ceiling/floor, Ln/Log10, min/max if (instruction.IsInvariant) return instruction.Evaluate(); From f8b5ed0a3b1da12c34a9483cc521dce59004e163 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 17 May 2026 12:14:09 -0400 Subject: [PATCH 074/120] Improve test coverage for optimization passes. The new tests inspect the actual opcode-level transformations that are applied. Adds DEBUG-level ability to restrict which passes are performed, for when some passes might obfuscate the actions of the pass under test. --- kerboscript_tests/integration/blank.ks | 1 + .../integration/peepholeOptimizations.ks | 8 +- .../Compilation/TypeInferencing.cs | 21 +- .../Execution/BaseIntegrationTest.cs | 6 +- .../Execution/OptimizationTest.cs | 499 +++++++++++++++++- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 34 ++ .../Compilation/Optimization/Optimizer.cs | 16 + 7 files changed, 563 insertions(+), 22 deletions(-) create mode 100644 kerboscript_tests/integration/blank.ks diff --git a/kerboscript_tests/integration/blank.ks b/kerboscript_tests/integration/blank.ks new file mode 100644 index 000000000..8fcda713c --- /dev/null +++ b/kerboscript_tests/integration/blank.ks @@ -0,0 +1 @@ +@lazyGlobal OFF. \ No newline at end of file diff --git a/kerboscript_tests/integration/peepholeOptimizations.ks b/kerboscript_tests/integration/peepholeOptimizations.ks index b6ef105a9..12943de96 100644 --- a/kerboscript_tests/integration/peepholeOptimizations.ks +++ b/kerboscript_tests/integration/peepholeOptimizations.ks @@ -1,7 +1,7 @@ -parameter a is TRUE. -parameter c is 3. -parameter x is 2. -parameter z is 5. +local a is TRUE. +local c is 3. +local x is 2. +local z is 5. print("test":typename()). // v() is unavailable in unit testing. diff --git a/src/kOS.Safe.Test/Compilation/TypeInferencing.cs b/src/kOS.Safe.Test/Compilation/TypeInferencing.cs index e90967a1b..55f942439 100644 --- a/src/kOS.Safe.Test/Compilation/TypeInferencing.cs +++ b/src/kOS.Safe.Test/Compilation/TypeInferencing.cs @@ -73,21 +73,20 @@ public void ListSuffixTest() [Test] public void TestInstructionInferencing() { - IRConstant a = new IRConstant(ScalarIntValue.One, 0, 0); - IRConstant b = new IRConstant(ScalarIntValue.Two, 0, 0); - IRTemp result = new IRTemp(0); - IRBinaryOp add = new IRBinaryOp(result, new OpcodeMathAdd(), a, b); + InterimConstantValue a = new InterimConstantValue(ScalarIntValue.One, 0, 0); + InterimConstantValue b = new InterimConstantValue(ScalarIntValue.Two, 0, 0); + IRBinaryOp add = new IRBinaryOp(null, new OpcodeMathAdd(), a, b); - Assert.AreEqual(add.ResultType, typeof(ScalarValue)); + Assert.AreEqual(add.Type, typeof(ScalarValue)); - IRCall call = new IRCall(result, new OpcodeCall("sin"), true, b); - Assert.IsTrue(typeof(ScalarValue).IsAssignableFrom(call.ResultType)); + IRCall call = new IRCall(null, new OpcodeCall("sin"), true, b); + Assert.IsTrue(typeof(ScalarValue).IsAssignableFrom(call.Type)); - IRCall print = new IRCall(result, new OpcodeCall("print"), true, a); - Assert.AreEqual(print.ResultType, null); + IRCall print = new IRCall(null, new OpcodeCall("print"), true, a); + Assert.AreEqual(print.Type, null); - IRCall userCall = new IRCall(result, new OpcodeCall("$test*"), true, a, b); - Assert.AreEqual(userCall.ResultType, typeof(Encapsulation.Structure)); + IRCall userCall = new IRCall(null, new OpcodeCall("$test*"), true, a, b); + Assert.AreEqual(userCall.Type, typeof(Encapsulation.Structure)); } } } diff --git a/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs b/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs index 1731e16ed..880a14de6 100644 --- a/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs +++ b/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs @@ -35,10 +35,10 @@ public void Setup() public abstract class BaseIntegrationTest { - private ICpu cpu; - private SafeSharedObjects shared; private Screen screen; - private string baseDir; + protected ICpu cpu; + protected SafeSharedObjects shared; + protected string baseDir; protected virtual OptimizationLevel OptimizationLevel => OptimizationLevel.None; private string FindKerboscriptTests() diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index 4216b0a5f..5dfae9138 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using NUnit.Framework; using kOS.Safe.Compilation; +using kOS.Safe.Compilation.IR; using kOS.Safe.Exceptions; namespace kOS.Safe.Test.Execution @@ -8,7 +10,93 @@ namespace kOS.Safe.Test.Execution [TestFixture] public class OptimizationTest : BaseIntegrationTest { - protected override OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + protected override OptimizationLevel OptimizationLevel => optimizationLevel; + protected OptimizationLevel optimizationLevel = OptimizationLevel.Minimal; + + protected List CompileCodePart(string fileName) + { + string contents = System.IO.File.ReadAllText(System.IO.Path.Combine(baseDir, fileName)); + Safe.Persistence.GlobalPath path = shared.VolumeMgr.GlobalPathFromObject("0:/" + fileName); + var compiled = shared.ScriptHandler.Compile(path, 1, contents, "test", new CompilerOptions() + { + LoadProgramsInSameAddressSpace = false, + IsCalledFromRun = false, + FuncManager = shared.FunctionManager, + BindManager = shared.BindingMgr, + AllowClobberBuiltins = Utilities.SafeHouse.Config.AllowClobberBuiltIns, + OptimizationLevel = OptimizationLevel + }); + + return compiled; + } + public List GetOpcodesAfterOptimization(List code, OptimizationLevel optimizationLevel = OptimizationLevel.Minimal) + { + IRCodePart codePart = new IRCodePart(code, new List(), new List()); + var optimizer = new Safe.Compilation.Optimization.Optimizer(optimizationLevel); + optimizer.Optimize(codePart); + CodePart result = new CodePart(); + codePart.EmitCode(result); + return result.MainCode; + } + + #region Holistic Tests + [Test] + public void TestSSA() + { + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/blank.ks"); + optimizationLevel = OptimizationLevel.Minimal; + IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); + BasicBlock block = codePart.MainCode[0]; + List instructions = new List + { + // Block 1: Local store will be SSA + new IRAssign(block, new OpcodeStoreLocal("a"), new InterimConstantValue(Encapsulation.ScalarIntValue.One, -1, -1)) {Scope = IRAssign.StoreScope.Local}, + new IRPop(block, new IRCall(block, new OpcodeCall("print"), true, new InterimVariableReference("a", -1, -1)), new OpcodePop()), + // Block 2 & 3: Store exist should be SSA and overwrite Block 1 + new IRAssign(block, new OpcodeStoreExist("a"), new InterimConstantValue(Encapsulation.ScalarIntValue.Two, -1, -1)), + new IRPop(block, new IRCall(block, new OpcodeCall("print"), true, new InterimVariableReference("a", -1, -1)), new OpcodePop()), + // Block 4: The variable "a" should now refer to a global, unresolved reference. + new IRUnset(block, new OpcodeUnset(), new InterimConstantValue("a", -1, -1)), + new IRPop(block, new IRCall(block, new OpcodeCall("print"), true, new InterimVariableReference("a", -1, -1)), new OpcodePop()), + // Block 5: The variable "a" should now refer to a global reference, which will not be resolved. + new IRAssign(block, new OpcodeStore("a"), new InterimConstantValue(Encapsulation.ScalarIntValue.One, -1, -1)), + new IRPop(block, new IRCall(block, new OpcodeCall("print"), true, new InterimVariableReference("a", -1, -1)), new OpcodePop()), + // Block 6: The variable "a" exists again at the local scope and should be resolved. + new IRAssign(block, new OpcodeStoreLocal("a"), new InterimConstantValue(Encapsulation.ScalarIntValue.One, -1, -1)) {Scope = IRAssign.StoreScope.Local}, + new IRPop(block, new IRCall(block, new OpcodeCall("print"), true, new InterimVariableReference("a", -1, -1)), new OpcodePop()) + + }; + block.Instructions.InsertRange(1, instructions); + SingleStaticAssignment.FinalizeSSA(codePart); + instructions = codePart.MainCode[0].Instructions; + + // Block 1 + Assert.IsInstanceOf(instructions[2]); + Assert.IsInstanceOf((instructions[2] as IRPop)?.Value); + Assert.IsInstanceOf(((instructions[2] as IRPop)?.Value as IRCall)?.Arguments[0]); + // Block 2 + Assert.IsInstanceOf(instructions[4]); + Assert.IsInstanceOf((instructions[4] as IRPop)?.Value); + Assert.IsInstanceOf(((instructions[4] as IRPop)?.Value as IRCall)?.Arguments[0]); + // Block 3 + Assert.IsNotNull(((InterimResolvedReference)((instructions[2] as IRPop)?.Value as IRCall)?.Arguments[0]).Reference); + Assert.IsNotNull(((InterimResolvedReference)((instructions[4] as IRPop)?.Value as IRCall)?.Arguments[0]).Reference); + Assert.AreNotEqual(((InterimResolvedReference)((instructions[2] as IRPop)?.Value as IRCall)?.Arguments[0]).Reference, + ((InterimResolvedReference)((instructions[4] as IRPop)?.Value as IRCall)?.Arguments[0]).Reference); + // Block 4 + Assert.IsInstanceOf(instructions[6]); + Assert.IsInstanceOf((instructions[6] as IRPop)?.Value); + Assert.IsInstanceOf(((instructions[6] as IRPop)?.Value as IRCall)?.Arguments[0]); + // Block 5 + Assert.IsInstanceOf(instructions[8]); + Assert.IsInstanceOf((instructions[8] as IRPop)?.Value); + Assert.IsInstanceOf(((instructions[8] as IRPop)?.Value as IRCall)?.Arguments[0]); + // Block 6 + Assert.IsInstanceOf(instructions[10]); + Assert.IsInstanceOf((instructions[10] as IRPop)?.Value); + Assert.IsInstanceOf(((instructions[10] as IRPop)?.Value as IRCall)?.Arguments[0]); + } [Test] public void TestBasic() @@ -96,12 +184,13 @@ public void TestOperators() } [Test] - [ExpectedException(typeof(KOSCompileException))] public void TestOperatorsException() { // Test that an invalid operation throws a compile error during constant folding - RunScript("integration/operators_invalid.ks"); - RunSingleStep(); + Assert.Throws(() => + { + RunScript("integration/operators_invalid.ks"); + }); } [Test] @@ -133,7 +222,9 @@ public void TestSuffixes() "False" ); } + #endregion + #region Suffix Replacement [Test] public void TestSuffixReplacement() { @@ -147,10 +238,47 @@ public void TestSuffixReplacement() ); } + [Test] + public void TestSuffixReplacement_Inspect() + { + // Tests that ship suffixes are replaced with direct accesses. + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePush("$ship"), + new OpcodeGetMember("name"), + new OpcodePop(), + new OpcodePush("$ship"), + new OpcodeGetMember("body"), + new OpcodePop(), + new OpcodePush("$ship"), + new OpcodeGetMember("nonexistentSuffix"), + new OpcodePop() + } + ); + + Assert.IsInstanceOf(result[0]); + Assert.IsInstanceOf((result[0] as OpcodePush)?.Argument); + Assert.That("$shipname".Equals((result[0] as OpcodePush)?.Argument as string, StringComparison.OrdinalIgnoreCase)); + + Assert.IsInstanceOf(result[2]); + Assert.IsInstanceOf((result[2] as OpcodePush)?.Argument); + Assert.That("$body".Equals((result[2] as OpcodePush)?.Argument as string, StringComparison.OrdinalIgnoreCase)); + Assert.IsInstanceOf(result[4]); + Assert.IsInstanceOf((result[4] as OpcodePush)?.Argument); + Assert.That("$ship".Equals((result[4] as OpcodePush)?.Argument as string, StringComparison.OrdinalIgnoreCase)); + + Assert.IsInstanceOf(result[5]); + } + #endregion + + #region Peephole Optimizations [Test] public void TestPeepholeOptimizations() { + HashSet passesToSkip = Safe.Compilation.Optimization.Optimizer.PassesToSkip; + passesToSkip.Add(typeof(Safe.Compilation.Optimization.Passes.ConstantFolding)); // Test that certain suffixes are replaced RunScript("integration/peepholeOptimizations.ks"); RunSingleStep(); @@ -177,9 +305,337 @@ public void TestPeepholeOptimizations() "125", "512" ); + passesToSkip.Clear(); + } + + [Test] + public void TestParameterlessSuffixMethodReplacement() + { + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush("$ship"), + new OpcodeGetMethod("method"), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodeCall(""), + new OpcodePop(), + new OpcodePush("$ship"), + new OpcodeGetMethod("method"), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodePush(new Safe.Compilation.PseudoNull()), + new OpcodeCall(""), + new OpcodePop(), + new OpcodePopScope() + } + ); + // The first one should be replaced + Assert.IsInstanceOf(result[1]); + Assert.IsNotInstanceOf(result[1]); + // The second one should not. + Assert.IsInstanceOf(result[4]); + } + + [Test] + public void TestVectorDotProductReplacement() + { + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeCall("vectordotproduct"), + new OpcodePop(), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeCall("vdot"), + new OpcodePop(), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodePush("$c"), + new OpcodeCall("vectordotproduct"), + new OpcodePop(), + new OpcodePopScope() + } + ); + + Assert.IsInstanceOf(result[2]); + Assert.IsInstanceOf(result[6]); + Assert.IsInstanceOf(result[12]); + } + + [Test] + public void TestRedundantUnaryOps() + { + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush("$a"), + new OpcodeMathNegate(), + new OpcodeMathNegate(), + new OpcodePop(), + new OpcodePush("$b"), + new OpcodeLogicNot(), + new OpcodeLogicNot(), + new OpcodePop(), + new OpcodePopScope() + } + ); + Assert.IsInstanceOf(result[1]); + Assert.IsInstanceOf(result[3]); + } + + [Test] + public void TestLexIndexReplacement() + { + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush("$a"), + new OpcodePush(new Encapsulation.StringValue("test")), + new OpcodeGetIndex(), + new OpcodePop(), + new OpcodePopScope() + } + ); + Assert.IsInstanceOf(result[1]); + Assert.That((result[1] as OpcodeGetMember)?.Identifier is string index && + index.Equals("test", StringComparison.OrdinalIgnoreCase)); + } + + [Test] + public void TestMultiplicationDistribution() + { + // A*B + A*C = A*(B+C) + Safe.Compilation.Optimization.Optimizer.PassesToSkip.Add(typeof(Safe.Compilation.Optimization.Passes.ConstantFolding)); + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeStoreLocal("$a"), + new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodeStoreLocal("$b"), + new OpcodePush(new Encapsulation.ScalarIntValue(3)), + new OpcodeStoreLocal("$c"), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeMathMultiply(), + new OpcodePush("$a"), + new OpcodePush("$c"), + new OpcodeMathMultiply(), + new OpcodeMathAdd(), + new OpcodePop(), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeMathMultiply(), + new OpcodePush("$a"), + new OpcodePush("$c"), + new OpcodeMathMultiply(), + new OpcodeMathSubtract(), + new OpcodePop(), + new OpcodePopScope() + } + ); + Safe.Compilation.Optimization.Optimizer.PassesToSkip.Clear(); + Assert.IsInstanceOf(result[10]); + Assert.IsInstanceOf(result[11]); + Assert.IsInstanceOf(result[16]); + Assert.IsInstanceOf(result[17]); } + [Test] + public void TestAdditionSubtraction() + { + // -B+A = A+-B = A-B + // -A+B = B-A + // A--B=A+B + Safe.Compilation.Optimization.Optimizer.PassesToSkip.Add(typeof(Safe.Compilation.Optimization.Passes.ConstantFolding)); + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeStoreLocal("$a"), + new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodeStoreLocal("$b"), + new OpcodePush("$b"), + new OpcodeMathNegate(), + new OpcodePush("$a"), + new OpcodeMathAdd(), + new OpcodePop(), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeMathNegate(), + new OpcodeMathAdd(), + new OpcodePop(), + new OpcodePush("$a"), + new OpcodeMathNegate(), + new OpcodePush("$b"), + new OpcodeMathAdd(), + new OpcodePop(), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeMathNegate(), + new OpcodeMathSubtract(), + new OpcodePopScope() + } + ); + Safe.Compilation.Optimization.Optimizer.PassesToSkip.Clear(); + Assert.IsInstanceOf(result[7]); + Assert.IsInstanceOf(result[11]); + Assert.IsInstanceOf(result[15]); + } + [Test] + public void TestDivisionSubtraction() + { + // X^N/X=X^(N-1) + // X^N/X^M=X^(N-M) + Safe.Compilation.Optimization.Optimizer.PassesToSkip.Add(typeof(Safe.Compilation.Optimization.Passes.ConstantFolding)); + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeStoreLocal("$a"), + new OpcodePush(new Encapsulation.ScalarDoubleValue(2.5)), + new OpcodeStoreLocal("$b"), + new OpcodePush(new Encapsulation.ScalarDoubleValue(3)), + new OpcodeStoreLocal("$c"), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeMathPower(), + new OpcodePush("$a"), + new OpcodeMathDivide(), + new OpcodePop(), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeMathPower(), + new OpcodePush("$a"), + new OpcodePush("$c"), + new OpcodeMathPower(), + new OpcodeMathDivide(), + new OpcodePop(), + new OpcodePopScope() + } + ); + Safe.Compilation.Optimization.Optimizer.PassesToSkip.Clear(); + Assert.IsInstanceOf(result[8]); + Assert.AreEqual((result[8] as OpcodePush)?.Argument, new Encapsulation.ScalarDoubleValue(1.5)); + Assert.IsInstanceOf(result[12]); + Assert.AreEqual((result[12] as OpcodePush)?.Argument, new Encapsulation.ScalarDoubleValue(-0.5)); + } + + [Test] + public void TestPowerAddition() + { + // X^N*X=X^(N+1) + // X^N*X^M=X^(N+M) + Safe.Compilation.Optimization.Optimizer.PassesToSkip.Add(typeof(Safe.Compilation.Optimization.Passes.ConstantFolding)); + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeStoreLocal("$a"), + new OpcodePush(new Encapsulation.ScalarDoubleValue(2.5)), + new OpcodeStoreLocal("$b"), + new OpcodePush(new Encapsulation.ScalarDoubleValue(3)), + new OpcodeStoreLocal("$c"), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeMathPower(), + new OpcodePush("$a"), + new OpcodeMathMultiply(), + new OpcodePop(), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeMathPower(), + new OpcodePush("$a"), + new OpcodePush("$c"), + new OpcodeMathPower(), + new OpcodeMathMultiply(), + new OpcodePop(), + new OpcodePopScope() + } + ); + Safe.Compilation.Optimization.Optimizer.PassesToSkip.Clear(); + Assert.IsInstanceOf(result[8]); + Assert.AreEqual((result[8] as OpcodePush)?.Argument, new Encapsulation.ScalarDoubleValue(3.5)); + Assert.IsInstanceOf(result[12]); + Assert.AreEqual((result[12] as OpcodePush)?.Argument, new Encapsulation.ScalarDoubleValue(5.5)); + } + + [Test] + public void TestPowerCreation() + { + // X*X*X = X^3 + Safe.Compilation.Optimization.Optimizer.PassesToSkip.Add(typeof(Safe.Compilation.Optimization.Passes.ConstantFolding)); + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeStoreLocal("$a"), + new OpcodePush("$a"), + new OpcodePush("$a"), + new OpcodePush("$a"), + new OpcodeMathMultiply(), + new OpcodeMathMultiply(), + new OpcodePop(), + new OpcodePush("$a"), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeMathPower(), + new OpcodePush("$a"), + new OpcodeMathDivide(), + new OpcodePop(), + new OpcodePush("$a"), + new OpcodePush(new Encapsulation.ScalarIntValue(3)), + new OpcodeMathPower(), + new OpcodePush("$a"), + new OpcodeMathDivide(), + new OpcodePop(), + new OpcodePush("$a"), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeMathPower(), + new OpcodePush("$a"), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeMathPower(), + new OpcodeMathDivide(), + new OpcodePop(), + new OpcodePush("$a"), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeMathPower(), + new OpcodePush("$a"), + new OpcodeMathDivide(), + new OpcodePush("$a"), + new OpcodeMathDivide(), + new OpcodePop(), + new OpcodePopScope() + } + ); + Safe.Compilation.Optimization.Optimizer.PassesToSkip.Clear(); + Assert.IsInstanceOf(result[4]); + Assert.AreEqual((result[4] as OpcodePush)?.Argument, new Encapsulation.ScalarIntValue(3)); + Assert.IsInstanceOf(result[5]); + Assert.IsInstanceOf(result[7]); + Assert.IsInstanceOf(result[8]); + Assert.IsInstanceOf(result[11]); + Assert.IsInstanceOf(result[13]); + Assert.AreEqual((result[13] as OpcodePush)?.Argument, Encapsulation.ScalarIntValue.One); + Assert.IsInstanceOf(result[15]); + Assert.AreEqual((result[15] as OpcodePush)?.Argument, Encapsulation.ScalarIntValue.One); + } + #endregion + + #region Constant Propagation [Test] public void TestConstantPropagation() { @@ -206,5 +662,40 @@ public void TestConstantPropagation() "1" ); } + + [Test] + public void TestConstantPropagationAndFolding() + { + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodeStoreLocal("$b"), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeStoreGlobal("$g"), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeStoreLocal("$c"), + new OpcodePush("$b"), + new OpcodePush("$c"), + new OpcodeMathAdd(), + new OpcodeStoreLocal("$d"), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeMathAdd(), + new OpcodeStoreLocal("$e"), + new OpcodePush("$b"), + new OpcodePush("$g"), + new OpcodeMathAdd(), + new OpcodeStoreLocal("$f"), + new OpcodePopScope() + } + ); + Assert.IsInstanceOf(result[7]); + Assert.AreEqual((result[7] as OpcodePush)?.Argument, new Encapsulation.ScalarIntValue(3)); + Assert.IsInstanceOf(result[11]); + Assert.IsInstanceOf(result[15]); + } + #endregion } } diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index 41ba1be8f..dc59fae40 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -81,6 +81,40 @@ public IRCodePart(CodePart codePart, List userFunctions, List mainCode, List userFunctions, List triggers) + { + IRBuilder builder = new IRBuilder(); + MainCode = builder.Lower(mainCode, this); + + Functions = new List(); + Queue functionsToLower = new Queue(userFunctions.Where(f => closureScopes.ContainsKey(f.Identifier))); + HashSet completedFunctions = new HashSet(); + while (functionsToLower.Count > 0) + { + UserFunction function = functionsToLower.Dequeue(); + Functions.Add(new IRFunction(builder, function, this)); + completedFunctions.Add(function); + foreach (UserFunction func in userFunctions.Where( + f => closureScopes.ContainsKey(f.Identifier) && + !completedFunctions.Contains(f) && + !functionsToLower.Contains(f))) + functionsToLower.Enqueue(func); + } + Triggers = triggers.Select(t => new IRTrigger(builder, t, this)).ToList(); + foreach (UserFunction func in userFunctions.Except(completedFunctions)) + Functions.Add(new IRFunction(builder, func, this)); + + Blocks.AddRange(MainCode); + if (MainCode.Count > 0) + RootBlocks.Add(MainCode[0]); + RootBlocks.AddRange(Triggers.Select(t => t.RootBlock)); + RootBlocks.AddRange(Functions.SelectMany(f => f.RootBlocks)); + + SingleStaticAssignment.FinalizeSSA(this); + } +#endif + /// /// Gets a function by string reference. /// diff --git a/src/kOS.Safe/Compilation/Optimization/Optimizer.cs b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs index d92fea233..955611630 100644 --- a/src/kOS.Safe/Compilation/Optimization/Optimizer.cs +++ b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs @@ -14,6 +14,10 @@ namespace kOS.Safe.Compilation.Optimization [AssemblyWalk(InterfaceType = typeof(IOptimizationPass), StaticRegisterMethod = "RegisterMethod")] public class Optimizer { +#if DEBUG + public static HashSet PassesToSkip { get; } = new HashSet(); + public static HashSet OnlyThesePasses { get; set; } = null; +#endif internal static InterimCPU InterimCPU { get; } = new InterimCPU(); private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = InterimCPU }; private readonly SortedSet optimizationPasses = new SortedSet( @@ -59,6 +63,18 @@ public Optimizer(OptimizationLevel optimizationLevel) OptimizationLevel = optimizationLevel; foreach (Type type in availablePassTypes) { +#if DEBUG + if (type != typeof(Passes.SCCPWithTypePropagation)) + { + if (OnlyThesePasses != null) + { + if (!OnlyThesePasses.Contains(type)) + continue; + } + else if (PassesToSkip.Contains(type)) + continue; + } +#endif IOptimizationPass pass = (IOptimizationPass)Activator.CreateInstance(type); optimizationPasses.Add(pass); if (pass is ILinkedOptimizationPass linkedPass) From c310518555d7052f6e3febe9f38e876e9a53dd0e Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 17 May 2026 12:18:18 -0400 Subject: [PATCH 075/120] Fix unresolved variable reference equality indicating equality across scopes. Unresolved variable references of the same name are now only equal if they originate from the same line of source (assumes the user does not expect triggers to interrupt and redefine a variable within the opcodes emitted for a single line of code). Improve ToString() representation of resolved variable references. Make the SSAIndexIssuer publicly accessible. --- .../Compilation/IR/InterimVariables.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs index c011036f8..a1789bc96 100644 --- a/src/kOS.Safe/Compilation/IR/InterimVariables.cs +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -41,11 +41,19 @@ public IEnumerable EmitOpcodes() public override string ToString() => $"{Name}"; + + // Assumes that variable reference on the same line are intended + // to be the same variable. This is only not true if a trigger + // modifies a variable between opcodes of the same line. + // Users are likely to not expect that to occur, and certainly + // should not count on it occurring, so it should be a valid + // assumption to make. public override bool Equals(object obj) => obj is InterimVariableReference variable && - string.Equals(Name, variable.Name, StringComparison.OrdinalIgnoreCase); + string.Equals(Name, variable.Name, StringComparison.OrdinalIgnoreCase) && + SourceLine == variable.SourceLine; public override int GetHashCode() - => Name.ToLower().GetHashCode(); + => (Name.ToLower(), SourceLine).GetHashCode(); } public readonly struct InterimResolvedReference : IInterimVariableReference, IEvaluatableToConstant @@ -83,7 +91,7 @@ public InterimConstantValue Evaluate() } public override string ToString() - => $"{Name}"; + => Reference.ToString(); public override bool Equals(object obj) => obj is InterimResolvedReference variable && Reference.Equals(variable.Reference); @@ -151,7 +159,7 @@ public IEnumerable EmitOpcodes() } public override string ToString() - => $"{Name}"; + => $"{Name} #?"; public override bool Equals(object obj) => obj is InterimUnresolvedReference unresolvedRef && unresolvedRef.references.SequenceEqual(references); @@ -159,7 +167,7 @@ public override int GetHashCode() => References.GetHashCode(); } - internal static class SSAIndexIssuer + public static class SSAIndexIssuer { private static readonly Dictionary indices = new Dictionary(); public static uint GetIndex(string name) From 8dd74d0b47983c2ee9f430135f329420e7942b73 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 17 May 2026 12:18:46 -0400 Subject: [PATCH 076/120] Add support for OpcodeDup and OpcodeSwap, which were missing. --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index f9429b081..2c74b689d 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -315,6 +315,15 @@ IInterimOperand PopStack() case OpcodePop pop: currentBlock.Add(new IRPop(currentBlock, PopStack(), pop)); break; + case OpcodeDup _: + stack.Push(stack.Peek()); + break; + case OpcodeSwap _: + IInterimOperand first = stack.Pop(); + IInterimOperand second = stack.Pop(); + stack.Push(first); + stack.Push(second); + break; default: throw new NotImplementedException($"The Opcode of type {opcode.GetType()} is not implemented."); } From efc4748c54c0e6e8e8d65034ca3bb81788299e7d Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 17 May 2026 12:21:55 -0400 Subject: [PATCH 077/120] Fix 'exist' not being resolved appropriately when it uses a variable reference. Limit search for 'exist' to not include the global scope because that could always be unset. --- src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index 0ca74441d..bb77431f5 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -607,7 +607,7 @@ private static IInterimOperand SSAReplacement(IInterimOperand operand, IRScope s } else if (operand is IRUnaryOp existOp && existOp.Operation is OpcodeExists) { - if (existOp.Operand.IsInvariant) + if (existOp.Operand.IsInvariant || existOp.Operand is IInterimVariableReference) { string name; if (existOp.Operand is IInterimVariableReference reference) @@ -617,7 +617,7 @@ private static IInterimOperand SSAReplacement(IInterimOperand operand, IRScope s else return operand; - while (scope != null) + while (!scope.IsGlobalScope) { if (liveDefinitions.TryGetValue((name, scope), out SSADefinition value) && value.State == SSADefinition.SetState.Set) From 99103d960e8131225d0f8bcd638121b66889264b Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 17 May 2026 12:56:52 -0400 Subject: [PATCH 078/120] Implement Equals(IInterimOperand) in the IInterimOperand interface. Switch to using 'Equals' instead of '==' for comparing operands. --- .../Compilation/IR/IInterimOperand.cs | 6 + src/kOS.Safe/Compilation/IR/IRInstruction.cs | 52 ++++- .../Compilation/IR/InterimConstantValue.cs | 13 +- .../Compilation/IR/InterimVariables.cs | 18 +- .../Optimization/Passes/ConstantFolding.cs | 3 +- .../Passes/PeepholeOptimizations.cs | 204 ++++++++++-------- 6 files changed, 200 insertions(+), 96 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IInterimOperand.cs b/src/kOS.Safe/Compilation/IR/IInterimOperand.cs index 458338ea0..d8647d134 100644 --- a/src/kOS.Safe/Compilation/IR/IInterimOperand.cs +++ b/src/kOS.Safe/Compilation/IR/IInterimOperand.cs @@ -25,5 +25,11 @@ public interface IInterimOperand /// /// One or more instances. IEnumerable EmitOpcodes(); + + /// + /// Tests equality between operands. + /// + /// true if the operands are equal, otherwise false. + bool Equals(IInterimOperand other); } } diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index ed2ba20ce..518b0ab97 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -258,6 +258,20 @@ public override IEnumerable EmitOpcodes() } public override string ToString() => Operation.ToString(); + public bool Equals(IInterimOperand other) + { + if (other is IRBinaryOp binaryOp && + Operation.GetType() == binaryOp.Operation.GetType()) + { + return (Left.Equals(binaryOp.Left) && Right.Equals(binaryOp.Right)) || + (IsCommutative && Left.Equals(binaryOp.Right) && Right.Equals(binaryOp.Left)); + } + if (IsInvariant && + other is IEvaluatableToConstant evaluatableToConstant && + evaluatableToConstant.IsInvariant) + return Evaluate().Equals(evaluatableToConstant.Evaluate()); + return false; + } public override bool Equals(object obj) { if (obj is IRBinaryOp binaryOp && @@ -266,6 +280,10 @@ public override bool Equals(object obj) return (Left.Equals(binaryOp.Left) && Right.Equals(binaryOp.Right)) || (IsCommutative && Left.Equals(binaryOp.Right) && Right.Equals(binaryOp.Left)); } + if (IsInvariant && + obj is IEvaluatableToConstant evaluatableToConstant && + evaluatableToConstant.IsInvariant) + return Evaluate().Equals(evaluatableToConstant.Evaluate()); return false; } public override int GetHashCode() @@ -323,6 +341,14 @@ public override IEnumerable EmitOpcodes() } public override string ToString() => Operation.ToString(); + public bool Equals(IInterimOperand other) + => (other is IRUnaryOp unaryOp && + Operation.GetType() == unaryOp.Operation.GetType() && + Operand.Equals(unaryOp.Operand)) || + (IsInvariant && + other is IEvaluatableToConstant evaluatableToConstant && + evaluatableToConstant.IsInvariant && + Evaluate().Equals(evaluatableToConstant.Evaluate())); public override bool Equals(object obj) => obj is IRUnaryOp unaryOp && Operation.GetType() == unaryOp.Operation.GetType() && @@ -450,7 +476,8 @@ public override IEnumerable EmitOpcodes() } public override string ToString() => Operation.ToString(); - + public bool Equals(IInterimOperand other) + => other == this; public InterimConstantValue Evaluate() => throw new InvalidOperationException(); } @@ -474,6 +501,14 @@ public override IEnumerable EmitOpcodes() } public override string ToString() => string.Format("{{gmb \"{0}\"}}", Suffix); + public bool Equals(IInterimOperand other) + => other == this || + (IsInvariant && + other is IRSuffixGet suffixGet && + suffixGet.IsInvariant && + !(suffixGet is IRSuffixGetMethod) && + string.Equals(Suffix, suffixGet.Suffix, StringComparison.OrdinalIgnoreCase) && + Object == suffixGet.Object); public override bool Equals(object obj) => obj == this || (IsInvariant && @@ -600,6 +635,13 @@ public override IEnumerable EmitOpcodes() } public override string ToString() => "{gidx}"; + public bool Equals(IInterimOperand other) + => other == this || + (IsInvariant && + other is IRIndexGet indexGet && + indexGet.IsInvariant && + Object.Equals(indexGet.Object) && + Index.Equals(indexGet.Index)); public override bool Equals(object obj) => obj == this || (IsInvariant && @@ -828,6 +870,14 @@ public IRCall(BasicBlock block, OpcodeCall opcode, bool emitArgMarker, params II } public override string ToString() => string.Format("{{call {0}({1})}}", Function.Trim('(', ')'), string.Join(",", Arguments.Select(a => a.ToString()))); + public bool Equals(IInterimOperand other) + => (other is IRCall call && + string.Equals(Function.Replace("()", ""), call.Function.Replace("()", ""), StringComparison.OrdinalIgnoreCase) && + Arguments.SequenceEqual(call.Arguments)) || + (IsInvariant && + other is IEvaluatableToConstant evaluatableToConstant && + evaluatableToConstant.IsInvariant && + Evaluate().Equals(evaluatableToConstant.Evaluate())); public override bool Equals(object obj) => obj is IRCall call && string.Equals(Function.Replace("()", ""), call.Function.Replace("()", ""), StringComparison.OrdinalIgnoreCase) && diff --git a/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs b/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs index 67bbe9bed..20843c905 100644 --- a/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs +++ b/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs @@ -28,8 +28,17 @@ public virtual IEnumerable EmitOpcodes() SourceColumn = sourceColumn }; } + public bool Equals(InterimConstantValue other) + => Value.Equals(other.Value); + public bool Equals(IInterimOperand other) + => (other is InterimConstantValue constant && Equals(constant)) || + (other is IEvaluatableToConstant evaluatableToConstant && + evaluatableToConstant.IsInvariant && + Equals(evaluatableToConstant.Evaluate())); public override bool Equals(object obj) - => obj is InterimConstantValue constant && Value.Equals(constant.Value) || Value.Equals(obj); + => (obj is InterimConstantValue constant && + Equals(constant)) || + Value.Equals(obj); public override int GetHashCode() => Value.GetHashCode(); public override string ToString() @@ -79,5 +88,7 @@ public class IRParameter : IInterimOperand public Type Type => null;//typeof(Encapsulation.Structure); public bool IsInvariant => false; public IEnumerable EmitOpcodes() => System.Linq.Enumerable.Empty(); + public bool Equals(IInterimOperand other) + => other == this; } } diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs index a1789bc96..9527d5242 100644 --- a/src/kOS.Safe/Compilation/IR/InterimVariables.cs +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -48,8 +48,12 @@ public override string ToString() // Users are likely to not expect that to occur, and certainly // should not count on it occurring, so it should be a valid // assumption to make. + public bool Equals(IInterimOperand other) + => other is IInterimVariableReference variableRef && + string.Equals(Name, variableRef.Name, StringComparison.OrdinalIgnoreCase) && + SourceLine == variableRef.SourceLine; public override bool Equals(object obj) - => obj is InterimVariableReference variable && + => obj is IInterimVariableReference variable && string.Equals(Name, variable.Name, StringComparison.OrdinalIgnoreCase) && SourceLine == variable.SourceLine; public override int GetHashCode() @@ -92,6 +96,13 @@ public InterimConstantValue Evaluate() public override string ToString() => Reference.ToString(); + public bool Equals(IInterimOperand other) + => (other is InterimResolvedReference variable && + Reference.Equals(variable.Reference)) || + (IsInvariant && + other is IEvaluatableToConstant evaluatableToConstant && + evaluatableToConstant.IsInvariant && + Evaluate().Equals(evaluatableToConstant.Evaluate())); public override bool Equals(object obj) => obj is InterimResolvedReference variable && Reference.Equals(variable.Reference); @@ -160,9 +171,12 @@ public IEnumerable EmitOpcodes() public override string ToString() => $"{Name} #?"; + public bool Equals(IInterimOperand other) + => other is InterimUnresolvedReference unresolvedRef && + references.SequenceEqual(unresolvedRef.references); public override bool Equals(object obj) => obj is InterimUnresolvedReference unresolvedRef && - unresolvedRef.references.SequenceEqual(references); + references.SequenceEqual(unresolvedRef.references); public override int GetHashCode() => References.GetHashCode(); } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index 27fbdfc46..c98b38518 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using kOS.Safe.Compilation.IR; using kOS.Safe.Exceptions; @@ -182,7 +181,7 @@ private static IInterimOperand ReduceBinary(IRBinaryOp instruction) // Technically not true when X = 0 // But that would otherwise throw a "Tried to push infinite on to the stack" error // So this is an acceptable assumption that improves performance and eliminates an error. - if (instruction.Left == instruction.Right) + if (instruction.Left.Equals(instruction.Right)) return new InterimConstantValue(Encapsulation.ScalarIntValue.One, instruction); break; } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs index 0f4c52b1c..f36638b18 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs @@ -74,13 +74,9 @@ indexGet.Index is InterimConstantValue indexConstant && return potentialResult; } - return operand; - } - private static void InstructionPeepholeFilter(IRInstruction instruction) - { // Algebraic simplifications - if (instruction is IRBinaryOp binaryOp) + if (operand is IRBinaryOp binaryOp) { switch (binaryOp.Operation) { @@ -96,33 +92,7 @@ opR.Operation is OpcodeMathMultiply && opL.IsCommutative && opR.IsCommutative) { - if (opR.Left == opL.Left) - { - DistributeMultiplication(binaryOp); - return; - } - if (opR.Left == opL.Right) - { - if (!opL.SwapOperands()) - return; - DistributeMultiplication(binaryOp); - return; - } - if (opR.Right == opL.Left) - { - if (!opR.SwapOperands()) - return; - DistributeMultiplication(binaryOp); - return; - } - if (opR.Right == opL.Right) - { - if (!opL.SwapOperands() || - !opR.SwapOperands()) - return; - DistributeMultiplication(binaryOp); - return; - } + return DistributeMultiplication(binaryOp, opL, opR); } } // -B+A = A+-B = A-B @@ -130,15 +100,13 @@ opR.Operation is OpcodeMathMultiply && if (binaryOp.Right is IRUnaryOp opR && opR.Operation is OpcodeMathNegate) { - DistributeNegationB(binaryOp); - return; + return DistributeNegationB(binaryOp); } if (binaryOp.Left is IRUnaryOp opL && opL.Operation is OpcodeMathNegate && binaryOp.SwapOperands()) { - DistributeNegationB(binaryOp); - return; + return DistributeNegationB(binaryOp); } } // -A+B = B-A @@ -146,20 +114,31 @@ opL.Operation is OpcodeMathNegate && if (binaryOp.Left is IRUnaryOp opL && opL.Operation is OpcodeMathNegate) { - DistributeNegationA(binaryOp); - return; + return DistributeNegationA(binaryOp); } } } break; case OpcodeMathSubtract _: + // A*B - A*C = A*(B-C) + { + if (binaryOp.Left is IRBinaryOp opL && + binaryOp.Right is IRBinaryOp opR && + opL.Operation is OpcodeMathMultiply && + opR.Operation is OpcodeMathMultiply && + binaryOp.IsCommutative && + opL.IsCommutative && + opR.IsCommutative) + { + return DistributeMultiplication(binaryOp, opL, opR); + } + } // A--B=A+B { if (binaryOp.Right is IRUnaryOp unaryOp && unaryOp.Operation is OpcodeMathNegate) { - ReplaceNegateSubtract(binaryOp); - return; + return ReplaceNegateSubtract(binaryOp); } } break; @@ -167,15 +146,14 @@ opL.Operation is OpcodeMathNegate && // TODO: Handle distributivity if (binaryOp.Right is InterimConstantValue constantR && Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) - throw new Exceptions.KOSCompileException(instruction, new DivideByZeroException()); + throw new Exceptions.KOSCompileException(binaryOp, new DivideByZeroException()); // X^N/X=X^(N-1) { if (binaryOp.Left is IRBinaryOp opL && opL.Operation is OpcodeMathPower && - opL.Left == binaryOp.Right) + opL.Left.Equals(binaryOp.Right)) { - IncreasePower(binaryOp, -1); - return; + return IncreasePower(binaryOp, -1); } } // X^N/X^M=X^(N-M) @@ -184,10 +162,9 @@ opL.Operation is OpcodeMathPower && opL.Operation is OpcodeMathPower && binaryOp.Right is IRBinaryOp opR && opR.Operation is OpcodeMathPower && - opL.Left == opR.Left) + opL.Left.Equals(opR.Left)) { - DividePowers(binaryOp); - return; + return DividePowers(binaryOp); } } break; @@ -196,18 +173,16 @@ opR.Operation is OpcodeMathPower && { if (binaryOp.Left is IRBinaryOp opL && opL.Operation is OpcodeMathPower && - opL.Left == binaryOp.Right) + opL.Left.Equals(binaryOp.Right)) { - IncreasePower(binaryOp, 1); - return; + return IncreasePower(binaryOp, 1); } if (binaryOp.Right is IRBinaryOp opR && opR.Operation is OpcodeMathPower && - opR.Left == binaryOp.Left && + opR.Left.Equals(binaryOp.Left) && binaryOp.SwapOperands()) { - IncreasePower(binaryOp, 1); - return; + return IncreasePower(binaryOp, 1); } } // X*X*...*X=N^X @@ -216,20 +191,18 @@ opR.Operation is OpcodeMathPower && { if (binaryOp.Right is IRBinaryOp opR && opR.Operation is OpcodeMathMultiply && - opR.Left == opR.Right && - opR.Left == binaryOp.Left) + opR.Left.Equals(opR.Right) && + opR.Left.Equals(binaryOp.Left)) { - CreatePower(binaryOp); - return; + return CreatePower(binaryOp); } if (binaryOp.Left is IRBinaryOp opL && opL.Operation is OpcodeMathMultiply && - opL.Left == opL.Right && - opL.Left == binaryOp.Right && + opL.Left.Equals(opL.Right) && + opL.Left.Equals(binaryOp.Right) && binaryOp.SwapOperands()) { - CreatePower(binaryOp); - return; + return CreatePower(binaryOp); } } // X^N*X^M=X^(N+M) @@ -238,16 +211,20 @@ opL.Operation is OpcodeMathMultiply && opL.Operation is OpcodeMathPower && binaryOp.Right is IRBinaryOp opR && opR.Operation is OpcodeMathPower && - opL.Left == opR.Left) + opL.Left.Equals(opR.Left)) { - CombinePowers(binaryOp); - return; + return CombinePowers(binaryOp); } } break; } } + return operand; + } + + private static void InstructionPeepholeFilter(IRInstruction instruction) + { // Branch logical simplification (e.g. !X branch = X branch!) if (instruction is IRBranch branch && branch.Condition is IRUnaryOp negateBranch && @@ -349,71 +326,103 @@ private static void ReplaceRedundantNotBranch(IRBranch branch) (branch.True, branch.False) = (branch.False, branch.True); } - private static void DistributeMultiplication(IRBinaryOp instruction) + private static IInterimOperand DistributeMultiplication(IRBinaryOp binaryOp, IRBinaryOp opL, IRBinaryOp opR) + { + if (opR.Left.Equals(opL.Left)) + { + return DistributeMultiplication(binaryOp); + } + if (opR.Left.Equals(opL.Right)) + { + if (!opL.SwapOperands()) + return binaryOp; + return DistributeMultiplication(binaryOp); + } + if (opR.Right.Equals(opL.Left)) + { + if (!opR.SwapOperands()) + return binaryOp; + return DistributeMultiplication(binaryOp); + } + if (opR.Right.Equals(opL.Right)) + { + if (!opL.SwapOperands() || + !opR.SwapOperands()) + return binaryOp; + return DistributeMultiplication(binaryOp); + } + return binaryOp; + } + + private static IInterimOperand DistributeMultiplication(IRBinaryOp instruction) { // A*B + A*C = A*(B+C) IRBinaryOp opL = (IRBinaryOp)instruction.Left; IRBinaryOp opR = (IRBinaryOp)instruction.Right; // Restructure to create the parentheses - opR.Operation = new OpcodeMathAdd(); + opR.Operation = instruction.Operation; opR.Left = opL.Right; // Restructure the multiplication term. instruction.Left = opL.Left; instruction.Operation = new OpcodeMathMultiply(); + return instruction; } - private static void DistributeNegationA(IRBinaryOp instruction) + private static IInterimOperand DistributeNegationA(IRBinaryOp instruction) { // -A+B = B-A if (!(instruction.Left is IRUnaryOp opL)) - return; + return instruction; if (!instruction.IsCommutative) - return; + return instruction; BinaryOpcode originalOperation = instruction.Operation; instruction.Operation = new OpcodeMathSubtract(); if (!instruction.IsCommutative) { instruction.Operation = originalOperation; - return; + return instruction; } instruction.Left = opL.Operand; instruction.SwapOperands(); + return instruction; } - private static void DistributeNegationB(IRBinaryOp instruction) + private static IInterimOperand DistributeNegationB(IRBinaryOp instruction) { // A+-B = A-B if (!(instruction.Right is IRUnaryOp opR)) - return; + return instruction; if (!instruction.IsCommutative) - return; + return instruction; BinaryOpcode originalOperation = instruction.Operation; instruction.Operation = new OpcodeMathSubtract(); if (!instruction.IsCommutative) { instruction.Operation = originalOperation; - return; + return instruction; } instruction.Right = opR.Operand; + return instruction; } - private static void ReplaceNegateSubtract(IRBinaryOp instruction) + private static IInterimOperand ReplaceNegateSubtract(IRBinaryOp instruction) { // A--B=A+B if (!instruction.IsCommutative) - return; + return instruction; BinaryOpcode originalOperation = instruction.Operation; instruction.Operation = new OpcodeMathAdd(); if (!instruction.IsCommutative) { instruction.Operation = originalOperation; - return; + return instruction; } IRUnaryOp opR = (IRUnaryOp)instruction.Right; instruction.Right = opR.Operand; + return instruction; } - private static void IncreasePower(IRBinaryOp instruction, int powerIncrease) + private static IInterimOperand IncreasePower(IRBinaryOp instruction, int powerIncrease) { // X^N*X=X^(N+1) // X^N/X=X^(N-1) @@ -443,26 +452,34 @@ private static void IncreasePower(IRBinaryOp instruction, int powerIncrease) // Special case for when N == 2 afterwards // then revert back to X * X - if (instruction.Right is InterimConstantValue constantR && - Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) + if (instruction.Right is InterimConstantValue constantR) { - instruction.Right = instruction.Left; - instruction.Operation = new OpcodeMathMultiply(); + if (Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) + { + instruction.Right = instruction.Left; + instruction.Operation = new OpcodeMathMultiply(); + } + else if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) + return instruction.Left; + else if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + return new InterimConstantValue(Encapsulation.ScalarIntValue.One, instruction); } + return instruction; } - private static void CreatePower(IRBinaryOp instruction) + private static IInterimOperand CreatePower(IRBinaryOp instruction) { // X*(X*X) = X^3 instruction.Operation = new OpcodeMathPower(); instruction.Right = new InterimConstantValue(new Encapsulation.ScalarIntValue(3), (IRInstruction)instruction.Right); + return instruction; } - private static void CombinePowers(IRBinaryOp instruction) + private static IInterimOperand CombinePowers(IRBinaryOp instruction) => CombinePowers(instruction, new OpcodeMathAdd()); - private static void DividePowers(IRBinaryOp instruction) + private static IInterimOperand DividePowers(IRBinaryOp instruction) => CombinePowers(instruction, new OpcodeMathSubtract()); - private static void CombinePowers(IRBinaryOp instruction, BinaryOpcode newOperation) + private static IInterimOperand CombinePowers(IRBinaryOp instruction, BinaryOpcode newOperation) { // X^N*X^M=X^(N+M) // Assumes |N + M| < 2^31 - 1 (for integer scalars) or 2^53 (for double scalars) @@ -488,12 +505,19 @@ private static void CombinePowers(IRBinaryOp instruction, BinaryOpcode newOperat // Special case for when N == 2 afterwards // then revert back to X * X - if (instruction.Right is InterimConstantValue constantR && - Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) + if (instruction.Right is InterimConstantValue constantR) { - instruction.Right = instruction.Left; - instruction.Operation = new OpcodeMathMultiply(); + if (Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) + { + instruction.Right = instruction.Left; + instruction.Operation = new OpcodeMathMultiply(); + } + else if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) + return instruction.Left; + else if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + return new InterimConstantValue(Encapsulation.ScalarIntValue.One, instruction); } + return instruction; } } } From 268d418ab8f24e0ec0c5ad557738411060f75957 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 17 May 2026 23:02:58 -0400 Subject: [PATCH 079/120] Refactor algebraic simplifications into the constant folding pass. Make equal-priority operations interchangeable for folding across algebraic branches. Fix logic for when operands are folded to a constant, and when they are just simplified. --- .../Execution/OptimizationTest.cs | 329 +++++++++----- src/kOS.Safe/Compilation/Calculator.cs | 6 + src/kOS.Safe/Compilation/IR/IRInstruction.cs | 5 +- .../Passes/AlgebraicSimplifications.cs | 421 ++++++++++++++++++ .../Optimization/Passes/ConstantFolding.cs | 171 ++++--- .../Passes/PeepholeOptimizations.cs | 372 +--------------- 6 files changed, 752 insertions(+), 552 deletions(-) create mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index 5dfae9138..2d01cae30 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -408,22 +408,154 @@ public void TestLexIndexReplacement() Assert.That((result[1] as OpcodeGetMember)?.Identifier is string index && index.Equals("test", StringComparison.OrdinalIgnoreCase)); } + #endregion + #region Constant Propagation, Folding, and Algebraic Simplifications [Test] - public void TestMultiplicationDistribution() + public void TestConstantPropagation() + { + // Test that local constant variables propagate + RunScript("integration/constantPropagation.ks"); + RunSingleStep(); + AssertOutput( + "test", + "6", + "False", + "6", + "9", + "7", + "10", + "14", + "True", + "False", + "7", + "6", + "11", + "9", + "5", + "6", + "1" + ); + } + + [Test] + public void TestConstantPropagationAndFolding() { - // A*B + A*C = A*(B+C) - Safe.Compilation.Optimization.Optimizer.PassesToSkip.Add(typeof(Safe.Compilation.Optimization.Passes.ConstantFolding)); List result = GetOpcodesAfterOptimization( new List() { new OpcodePushScope(1, 0), + new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodeStoreLocal("$b"), new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeStoreGlobal("$g"), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeStoreLocal("$c"), + new OpcodePush("$b"), + new OpcodePush("$c"), + new OpcodeMathAdd(), + new OpcodeStoreLocal("$d"), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeMathAdd(), + new OpcodeStoreLocal("$e"), + new OpcodePush("$b"), + new OpcodePush("$g"), + new OpcodeMathAdd(), + new OpcodeStoreLocal("$f"), + new OpcodePopScope() + } + ); + Assert.IsInstanceOf(result[7]); + Assert.AreEqual(new Encapsulation.ScalarIntValue(3), (result[7] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[11]); + Assert.IsInstanceOf(result[15]); + } + + [Test] + public void TestConstantPropagationCommutivity() + { + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodeCall("random"), new OpcodeStoreLocal("$a"), + // Block 1 + new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodePush("$a"), + new OpcodeMathAdd(), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeMathAdd(), + new OpcodeStoreLocal("$result"), + // Block 2 + new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodePush("$a"), + new OpcodeMathAdd(), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeMathSubtract(), + new OpcodeStoreLocal("$result"), + // Block 3 + new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodePush("$a"), + new OpcodeMathSubtract(), + new OpcodeStoreLocal("$result"), + // Block 4 + new OpcodePush("$a"), + new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodeMathSubtract(), + new OpcodeStoreLocal("$result"), + // Block 5 new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodePush("$a"), + new OpcodeMathSubtract(), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeMathAdd(), + new OpcodeStoreLocal("$result"), + new OpcodePopScope() + } + ); + // Block 1 + Assert.AreEqual(new Encapsulation.ScalarIntValue(3), (result[4] as OpcodePush)?.Argument); + Assert.AreEqual("$a", (result[5] as OpcodePush)?.Argument); + // Block 2 + Assert.AreEqual(new Encapsulation.ScalarIntValue(-1), (result[8] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[10]); + //Block 3 + Assert.AreEqual(Encapsulation.ScalarIntValue.One, (result[12] as OpcodePush)?.Argument); + Assert.AreEqual("$a", (result[13] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[14]); + //Block 4 + Assert.AreEqual(new Encapsulation.ScalarIntValue(-1), (result[16] as OpcodePush)?.Argument); + Assert.AreEqual("$a", (result[17] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[18]); + //Block 5 + Assert.AreEqual(new Encapsulation.ScalarIntValue(3), (result[20] as OpcodePush)?.Argument); + Assert.AreEqual("$a", (result[21] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[22]); + } + + [Test] + public void TestMultiplicationDistribution() + { + // A*B + A*C = A*(B+C) + // A/B + C/B = (A+C)/B + // A/B - C/B = (A-C)/B + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodeCall("random"), + new OpcodeStoreLocal("$a"), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodeCall("random"), new OpcodeStoreLocal("$b"), - new OpcodePush(new Encapsulation.ScalarIntValue(3)), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodeCall("random"), new OpcodeStoreLocal("$c"), + // Block 1 new OpcodePush("$a"), new OpcodePush("$b"), new OpcodeMathMultiply(), @@ -432,6 +564,7 @@ public void TestMultiplicationDistribution() new OpcodeMathMultiply(), new OpcodeMathAdd(), new OpcodePop(), + // Block 2 new OpcodePush("$a"), new OpcodePush("$b"), new OpcodeMathMultiply(), @@ -440,14 +573,63 @@ public void TestMultiplicationDistribution() new OpcodeMathMultiply(), new OpcodeMathSubtract(), new OpcodePop(), + // Block 3 + new OpcodePush("$b"), + new OpcodePush("$a"), + new OpcodeMathDivide(), + new OpcodePush("$c"), + new OpcodePush("$a"), + new OpcodeMathDivide(), + new OpcodeMathAdd(), + new OpcodePop(), + // Block 4 + new OpcodePush("$b"), + new OpcodePush("$a"), + new OpcodeMathDivide(), + new OpcodePush("$c"), + new OpcodePush("$a"), + new OpcodeMathDivide(), + new OpcodeMathSubtract(), + new OpcodePop(), new OpcodePopScope() } ); - Safe.Compilation.Optimization.Optimizer.PassesToSkip.Clear(); - Assert.IsInstanceOf(result[10]); - Assert.IsInstanceOf(result[11]); - Assert.IsInstanceOf(result[16]); - Assert.IsInstanceOf(result[17]); + // Block 1 + Assert.IsInstanceOf(result[10]); + Assert.AreEqual("$a", (result[10] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[11]); + Assert.AreEqual("$b", (result[11] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[12]); + Assert.AreEqual("$c", (result[12] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[13]); + Assert.IsInstanceOf(result[14]); + // Block 2 + Assert.IsInstanceOf(result[16]); + Assert.AreEqual("$a", (result[16] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[17]); + Assert.AreEqual("$b", (result[17] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[18]); + Assert.AreEqual("$c", (result[18] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[19]); + Assert.IsInstanceOf(result[20]); + // Block 3 + Assert.IsInstanceOf(result[22]); + Assert.AreEqual("$b", (result[22] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[23]); + Assert.AreEqual("$c", (result[23] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[25]); + Assert.AreEqual("$a", (result[25] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[24]); + Assert.IsInstanceOf(result[26]); + // Block 4 + Assert.IsInstanceOf(result[28]); + Assert.AreEqual("$b", (result[28] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[29]); + Assert.AreEqual("$c", (result[29] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[31]); + Assert.AreEqual("$a", (result[31] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[30]); + Assert.IsInstanceOf(result[32]); } [Test] @@ -456,14 +638,15 @@ public void TestAdditionSubtraction() // -B+A = A+-B = A-B // -A+B = B-A // A--B=A+B - Safe.Compilation.Optimization.Optimizer.PassesToSkip.Add(typeof(Safe.Compilation.Optimization.Passes.ConstantFolding)); List result = GetOpcodesAfterOptimization( new List() { new OpcodePushScope(1, 0), - new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodeCall("random"), new OpcodeStoreLocal("$a"), - new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodeCall("random"), new OpcodeStoreLocal("$b"), new OpcodePush("$b"), new OpcodeMathNegate(), @@ -487,10 +670,9 @@ public void TestAdditionSubtraction() new OpcodePopScope() } ); - Safe.Compilation.Optimization.Optimizer.PassesToSkip.Clear(); - Assert.IsInstanceOf(result[7]); - Assert.IsInstanceOf(result[11]); - Assert.IsInstanceOf(result[15]); + Assert.IsInstanceOf(result[9]); + Assert.IsInstanceOf(result[13]); + Assert.IsInstanceOf(result[17]); } [Test] @@ -498,12 +680,12 @@ public void TestDivisionSubtraction() { // X^N/X=X^(N-1) // X^N/X^M=X^(N-M) - Safe.Compilation.Optimization.Optimizer.PassesToSkip.Add(typeof(Safe.Compilation.Optimization.Passes.ConstantFolding)); List result = GetOpcodesAfterOptimization( new List() { new OpcodePushScope(1, 0), - new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodeCall("random"), new OpcodeStoreLocal("$a"), new OpcodePush(new Encapsulation.ScalarDoubleValue(2.5)), new OpcodeStoreLocal("$b"), @@ -526,11 +708,10 @@ public void TestDivisionSubtraction() new OpcodePopScope() } ); - Safe.Compilation.Optimization.Optimizer.PassesToSkip.Clear(); - Assert.IsInstanceOf(result[8]); - Assert.AreEqual((result[8] as OpcodePush)?.Argument, new Encapsulation.ScalarDoubleValue(1.5)); - Assert.IsInstanceOf(result[12]); - Assert.AreEqual((result[12] as OpcodePush)?.Argument, new Encapsulation.ScalarDoubleValue(-0.5)); + Assert.IsInstanceOf(result[9]); + Assert.AreEqual(new Encapsulation.ScalarDoubleValue(1.5), (result[9] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[13]); + Assert.AreEqual(new Encapsulation.ScalarDoubleValue(-0.5), (result[13] as OpcodePush)?.Argument); } [Test] @@ -538,12 +719,12 @@ public void TestPowerAddition() { // X^N*X=X^(N+1) // X^N*X^M=X^(N+M) - Safe.Compilation.Optimization.Optimizer.PassesToSkip.Add(typeof(Safe.Compilation.Optimization.Passes.ConstantFolding)); List result = GetOpcodesAfterOptimization( new List() { new OpcodePushScope(1, 0), - new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodeCall("random"), new OpcodeStoreLocal("$a"), new OpcodePush(new Encapsulation.ScalarDoubleValue(2.5)), new OpcodeStoreLocal("$b"), @@ -566,23 +747,22 @@ public void TestPowerAddition() new OpcodePopScope() } ); - Safe.Compilation.Optimization.Optimizer.PassesToSkip.Clear(); - Assert.IsInstanceOf(result[8]); - Assert.AreEqual((result[8] as OpcodePush)?.Argument, new Encapsulation.ScalarDoubleValue(3.5)); - Assert.IsInstanceOf(result[12]); - Assert.AreEqual((result[12] as OpcodePush)?.Argument, new Encapsulation.ScalarDoubleValue(5.5)); + Assert.IsInstanceOf(result[9]); + Assert.AreEqual(new Encapsulation.ScalarDoubleValue(3.5), (result[9] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[13]); + Assert.AreEqual(new Encapsulation.ScalarDoubleValue(5.5), (result[13] as OpcodePush)?.Argument); } [Test] public void TestPowerCreation() { // X*X*X = X^3 - Safe.Compilation.Optimization.Optimizer.PassesToSkip.Add(typeof(Safe.Compilation.Optimization.Passes.ConstantFolding)); List result = GetOpcodesAfterOptimization( new List() { new OpcodePushScope(1, 0), - new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodeCall("random"), new OpcodeStoreLocal("$a"), new OpcodePush("$a"), new OpcodePush("$a"), @@ -610,6 +790,7 @@ public void TestPowerCreation() new OpcodeMathPower(), new OpcodeMathDivide(), new OpcodePop(), + // TODO: It should reduce this to Encapsulation.One new OpcodePush("$a"), new OpcodePush(Encapsulation.ScalarIntValue.Two), new OpcodeMathPower(), @@ -621,80 +802,16 @@ public void TestPowerCreation() new OpcodePopScope() } ); - Safe.Compilation.Optimization.Optimizer.PassesToSkip.Clear(); - Assert.IsInstanceOf(result[4]); - Assert.AreEqual((result[4] as OpcodePush)?.Argument, new Encapsulation.ScalarIntValue(3)); - Assert.IsInstanceOf(result[5]); - Assert.IsInstanceOf(result[7]); - Assert.IsInstanceOf(result[8]); - Assert.IsInstanceOf(result[11]); - Assert.IsInstanceOf(result[13]); - Assert.AreEqual((result[13] as OpcodePush)?.Argument, Encapsulation.ScalarIntValue.One); - Assert.IsInstanceOf(result[15]); - Assert.AreEqual((result[15] as OpcodePush)?.Argument, Encapsulation.ScalarIntValue.One); - } - #endregion - - #region Constant Propagation - [Test] - public void TestConstantPropagation() - { - // Test that local constant variables propagate - RunScript("integration/constantPropagation.ks"); - RunSingleStep(); - AssertOutput( - "test", - "6", - "False", - "6", - "9", - "7", - "10", - "14", - "True", - "False", - "7", - "6", - "11", - "9", - "5", - "6", - "1" - ); - } - - [Test] - public void TestConstantPropagationAndFolding() - { - List result = GetOpcodesAfterOptimization( - new List() - { - new OpcodePushScope(1, 0), - new OpcodePush(Encapsulation.ScalarIntValue.One), - new OpcodeStoreLocal("$b"), - new OpcodePush(Encapsulation.ScalarIntValue.Two), - new OpcodeStoreGlobal("$g"), - new OpcodePush(Encapsulation.ScalarIntValue.Two), - new OpcodeStoreLocal("$c"), - new OpcodePush("$b"), - new OpcodePush("$c"), - new OpcodeMathAdd(), - new OpcodeStoreLocal("$d"), - new OpcodePush("$a"), - new OpcodePush("$b"), - new OpcodeMathAdd(), - new OpcodeStoreLocal("$e"), - new OpcodePush("$b"), - new OpcodePush("$g"), - new OpcodeMathAdd(), - new OpcodeStoreLocal("$f"), - new OpcodePopScope() - } - ); - Assert.IsInstanceOf(result[7]); - Assert.AreEqual((result[7] as OpcodePush)?.Argument, new Encapsulation.ScalarIntValue(3)); - Assert.IsInstanceOf(result[11]); - Assert.IsInstanceOf(result[15]); + Assert.IsInstanceOf(result[5]); + Assert.AreEqual(new Encapsulation.ScalarIntValue(3), (result[5] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[6]); + Assert.IsInstanceOf(result[8]); + Assert.IsInstanceOf(result[9]); + Assert.IsInstanceOf(result[12]); + Assert.IsInstanceOf(result[14]); + Assert.AreEqual(Encapsulation.ScalarIntValue.One, (result[14] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[16]); + Assert.AreEqual(Encapsulation.ScalarIntValue.One, (result[16] as OpcodePush)?.Argument); } #endregion } diff --git a/src/kOS.Safe/Compilation/Calculator.cs b/src/kOS.Safe/Compilation/Calculator.cs index 64d76ab70..590bee8a0 100644 --- a/src/kOS.Safe/Compilation/Calculator.cs +++ b/src/kOS.Safe/Compilation/Calculator.cs @@ -30,6 +30,12 @@ public abstract class Calculator public virtual Type GetEqualResultType(Type leftType, Type rightType) => typeof(BooleanValue); public virtual bool IsAdditionCommutative(Type leftType, Type rightType) => true; + /// + /// Determines whether subtraction is commutative with negation/addition. + /// + /// + /// true if subtraction is commutative with negation/addition; otherwise, false. + /// public virtual bool IsSubtractionCommutative(Type leftType, Type rightType) => true; public virtual bool IsMultiplicationCommmutative(Type leftType, Type rightType) => true; public virtual bool IsDivisionCommutative(Type leftType, Type rightType) => false; diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 518b0ab97..82ea99e10 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -228,7 +228,10 @@ public bool SwapOperands() return false; //throw new System.InvalidOperationException($"{this} is not commutative."); if (Operation is OpcodeMathSubtract) - return false; + { + Right = new IRUnaryOp(Block, new OpcodeMathNegate(), Right); + Operation = new OpcodeMathAdd(); + } (Right, Left) = (Left, Right); switch (Operation) { diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs b/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs new file mode 100644 index 000000000..0cc2e48bc --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs @@ -0,0 +1,421 @@ +using System; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public static class AlgebraicSimplifications + { + public static IInterimOperand AttemptUnarySimplification(IRUnaryOp unaryOp) + { + + // Redundant unary operation replacements + // E.g. !!X = X and --X = X + IInterimOperand potentialResult = AttempReplaceRedundantUnaryOp(unaryOp); + if (potentialResult != null) + return potentialResult; + return unaryOp; + } + + private static IInterimOperand AttempReplaceRedundantUnaryOp(IRUnaryOp unaryParent) + { + // Replace --X with X + if (CanOperationBeReplaced(unaryParent, typeof(OpcodeMathNegate))) + return GetDoubleNestedValue(unaryParent); + // Replace !!X with X + if (CanOperationBeReplaced(unaryParent, typeof(OpcodeLogicNot))) + return GetDoubleNestedValue(unaryParent); + return null; + } + private static bool CanOperationBeReplaced(IRUnaryOp operation, Type opcodeType) + { + return operation.Operation.GetType() == opcodeType && + operation.Operand is IRUnaryOp neg2 && + neg2.Operation.GetType() == opcodeType; + } + public static IInterimOperand GetDoubleNestedValue(ISingleOperandInstruction operation) + { + return ((IRUnaryOp)operation.Operand).Operand; + } + + + public static IRBinaryOp AttemptAlgebraicSimplification(IRBinaryOp binaryOp) + { + switch (binaryOp.Operation) + { + case OpcodeMathAdd _: + { + // A*B + A*C = A*(B+C) + { + if (binaryOp.Left is IRBinaryOp opL && + binaryOp.Right is IRBinaryOp opR && + opL.Operation is OpcodeMathMultiply && + opR.Operation is OpcodeMathMultiply && + binaryOp.IsCommutative && + opL.IsCommutative && + opR.IsCommutative) + { + return DistributeMultiplication(binaryOp, opL, opR); + } + } + // A/B + C/B = (A+C)/B + { + if (binaryOp.Left is IRBinaryOp opL && + binaryOp.Right is IRBinaryOp opR && + opL.Operation is OpcodeMathDivide && + opR.Operation is OpcodeMathDivide && + binaryOp.IsCommutative) + { + return DistributeDivision(binaryOp); + } + } + // -B+A = A+-B = A-B + { + if (binaryOp.Right is IRUnaryOp opR && + opR.Operation is OpcodeMathNegate) + { + return DistributeNegationB(binaryOp); + } + if (binaryOp.Left is IRUnaryOp opL && + opL.Operation is OpcodeMathNegate && + binaryOp.SwapOperands()) + { + return DistributeNegationB(binaryOp); + } + } + // -A+B = B-A + { + if (binaryOp.Left is IRUnaryOp opL && + opL.Operation is OpcodeMathNegate) + { + return DistributeNegationA(binaryOp); + } + } + } + break; + case OpcodeMathSubtract _: + // A*B - A*C = A*(B-C) + { + if (binaryOp.Left is IRBinaryOp opL && + binaryOp.Right is IRBinaryOp opR && + opL.Operation is OpcodeMathMultiply && + opR.Operation is OpcodeMathMultiply && + binaryOp.IsCommutative && + opL.IsCommutative && + opR.IsCommutative) + { + return DistributeMultiplication(binaryOp, opL, opR); + } + } + // A--B=A+B + { + if (binaryOp.Right is IRUnaryOp unaryOp && + unaryOp.Operation is OpcodeMathNegate) + { + return ReplaceNegateSubtract(binaryOp); + } + } + // A/B - C/B = (A-C)/B + { + if (binaryOp.Left is IRBinaryOp opL && + binaryOp.Right is IRBinaryOp opR && + opL.Operation is OpcodeMathDivide && + opR.Operation is OpcodeMathDivide && + binaryOp.IsCommutative) + { + return DistributeDivision(binaryOp); + } + } + break; + case OpcodeMathDivide _: + // TODO: Handle distributivity + if (binaryOp.Right is InterimConstantValue constantR && + Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + throw new Exceptions.KOSCompileException(binaryOp, new DivideByZeroException()); + // X^N/X=X^(N-1) + { + if (binaryOp.Left is IRBinaryOp opL && + opL.Operation is OpcodeMathPower && + opL.Left.Equals(binaryOp.Right)) + { + return IncreasePower(binaryOp, -1); + } + } + // X^N/X^M=X^(N-M) + { + if (binaryOp.Left is IRBinaryOp opL && + opL.Operation is OpcodeMathPower && + binaryOp.Right is IRBinaryOp opR && + opR.Operation is OpcodeMathPower && + opL.Left.Equals(opR.Left)) + { + return DividePowers(binaryOp); + } + } + break; + case OpcodeMathMultiply _: + // X^N*X=X^(N+1) + { + if (binaryOp.Left is IRBinaryOp opL && + opL.Operation is OpcodeMathPower && + opL.Left.Equals(binaryOp.Right)) + { + return IncreasePower(binaryOp, 1); + } + if (binaryOp.Right is IRBinaryOp opR && + opR.Operation is OpcodeMathPower && + opR.Left.Equals(binaryOp.Left) && + binaryOp.SwapOperands()) + { + return IncreasePower(binaryOp, 1); + } + } + // X*X*...*X=N^X + // Start with (X*X)*X = X*(X*X) = X^3 + // Then the previous rule will kick in and exponentiate from there + { + // This rule only applies to known scalar values. + // Exponentiation is meaningless to other types. + if (typeof(Encapsulation.ScalarValue).IsAssignableFrom(binaryOp.Left.Type) && + binaryOp.Right is IRBinaryOp opR && + opR.Operation is OpcodeMathMultiply && + opR.Left.Equals(opR.Right) && + opR.Left.Equals(binaryOp.Left)) + { + return CreatePower(binaryOp); + } + if (typeof(Encapsulation.ScalarValue).IsAssignableFrom(binaryOp.Right.Type) && + binaryOp.Left is IRBinaryOp opL && + opL.Operation is OpcodeMathMultiply && + opL.Left.Equals(opL.Right) && + opL.Left.Equals(binaryOp.Right) && + binaryOp.SwapOperands()) + { + return CreatePower(binaryOp); + } + } + // X^N*X^M=X^(N+M) + { + if (binaryOp.Left is IRBinaryOp opL && + opL.Operation is OpcodeMathPower && + binaryOp.Right is IRBinaryOp opR && + opR.Operation is OpcodeMathPower && + opL.Left.Equals(opR.Left)) + { + return CombinePowers(binaryOp); + } + } + break; + } + return binaryOp; + } + + private static IRBinaryOp DistributeMultiplication(IRBinaryOp binaryOp, IRBinaryOp opL, IRBinaryOp opR) + { + if (opR.Left.Equals(opL.Left)) + { + return DistributeMultiplication(binaryOp); + } + if (opR.Left.Equals(opL.Right)) + { + if (!opL.SwapOperands()) + return binaryOp; + return DistributeMultiplication(binaryOp); + } + if (opR.Right.Equals(opL.Left)) + { + if (!opR.SwapOperands()) + return binaryOp; + return DistributeMultiplication(binaryOp); + } + if (opR.Right.Equals(opL.Right)) + { + if (!opL.SwapOperands() || + !opR.SwapOperands()) + return binaryOp; + return DistributeMultiplication(binaryOp); + } + return binaryOp; + } + + private static IRBinaryOp DistributeMultiplication(IRBinaryOp instruction) + { + // A*B + A*C = A*(B+C) + IRBinaryOp opL = (IRBinaryOp)instruction.Left; + IRBinaryOp opR = (IRBinaryOp)instruction.Right; + // Restructure to create the parentheses + opR.Operation = instruction.Operation; + opR.Left = opL.Right; + // Restructure the multiplication term. + instruction.Left = opL.Left; + instruction.Operation = opL.Operation; + return instruction; + } + private static IRBinaryOp DistributeDivision(IRBinaryOp instruction) + { + // B/A + C/A = (B+C)/A + IRBinaryOp opL = (IRBinaryOp)instruction.Left; + IRBinaryOp opR = (IRBinaryOp)instruction.Right; + // Restructure to create the parentheses + opL.Operation = instruction.Operation; + opL.Right = opR.Left; + // Restructure the multiplication term. + instruction.Right = opR.Right; + instruction.Operation = opR.Operation; + return instruction; + } + + private static IRBinaryOp DistributeNegationA(IRBinaryOp instruction) + { + // -A+B = B-A + if (!(instruction.Left is IRUnaryOp opL)) + return instruction; + if (!instruction.IsCommutative) + return instruction; + BinaryOpcode originalOperation = instruction.Operation; + instruction.Operation = new OpcodeMathSubtract(); + if (!instruction.IsCommutative) + { + instruction.Operation = originalOperation; + return instruction; + } + instruction.Left = opL.Operand; + instruction.SwapOperands(); + return instruction; + } + + private static IRBinaryOp DistributeNegationB(IRBinaryOp instruction) + { + // A+-B = A-B + if (!(instruction.Right is IRUnaryOp opR)) + return instruction; + if (!instruction.IsCommutative) + return instruction; + BinaryOpcode originalOperation = instruction.Operation; + instruction.Operation = new OpcodeMathSubtract(); + if (!instruction.IsCommutative) + { + instruction.Operation = originalOperation; + return instruction; + } + instruction.Right = opR.Operand; + return instruction; + } + + private static IRBinaryOp ReplaceNegateSubtract(IRBinaryOp instruction) + { + // A--B=A+B + if (!instruction.IsCommutative) + return instruction; + BinaryOpcode originalOperation = instruction.Operation; + instruction.Operation = new OpcodeMathAdd(); + if (!instruction.IsCommutative) + { + instruction.Operation = originalOperation; + return instruction; + } + IRUnaryOp opR = (IRUnaryOp)instruction.Right; + instruction.Right = opR.Operand; + return instruction; + } + + private static IRBinaryOp IncreasePower(IRBinaryOp instruction, int powerIncrease) + { + // X^N*X=X^(N+1) + // X^N/X=X^(N-1) + // Assumes |N +/- 1| < 2^31 - 1 (for integer scalars) or 2^53 (for double scalars) + // Going beyond that overflows an integer or the mantissa for double. + + IRBinaryOp opL = (IRBinaryOp)instruction.Left; + + short divLine = instruction.SourceLine; + short divColumn = instruction.SourceColumn; + + InterimConstantValue powConst = new InterimConstantValue(new Encapsulation.ScalarIntValue(powerIncrease), instruction); + + // Set up the new power operation + instruction.Left = opL.Left; + instruction.Operation = new OpcodeMathPower(); + instruction.OverwriteSourceLocation(opL.SourceLine, opL.SourceColumn); + + // Reuse the old power instruction for the addition/subtraction + instruction.Right = opL; + opL.Left = powConst; + opL.Operation = new OpcodeMathAdd(); + opL.OverwriteSourceLocation(divLine, divColumn); + + // Attempt constant folding afterwards + instruction.Right = ConstantFolding.AttemptReduction(opL); + + // Special case for when N == 2 afterwards + // then revert back to X * X + if (instruction.Right is InterimConstantValue constantR) + { + if (Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) + { + instruction.Right = instruction.Left; + instruction.Operation = new OpcodeMathMultiply(); + } + /*else if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) + return instruction.Left; + else if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + return new InterimConstantValue(Encapsulation.ScalarIntValue.One, instruction);*/ + } + return instruction; + } + + private static IRBinaryOp DividePowers(IRBinaryOp instruction) + => CombinePowers(instruction, new OpcodeMathSubtract()); + + private static IRBinaryOp CombinePowers(IRBinaryOp instruction) + => CombinePowers(instruction, new OpcodeMathAdd()); + + private static IRBinaryOp CombinePowers(IRBinaryOp instruction, BinaryOpcode newOperation) + { + // X^N*X^M=X^(N+M) + // Assumes |N + M| < 2^31 - 1 (for integer scalars) or 2^53 (for double scalars) + + IRBinaryOp opL = (IRBinaryOp)instruction.Left; + IRBinaryOp opR = (IRBinaryOp)instruction.Right; + + short divLine = instruction.SourceLine; + short divColumn = instruction.SourceColumn; + + // Set up the new power operation + instruction.Left = opL.Left; + instruction.Operation = new OpcodeMathPower(); + instruction.OverwriteSourceLocation(opL.SourceLine, opL.SourceColumn); + + // Reuse an old power instruction for the addition/subtraction + opR.Left = opL.Right; + opR.Operation = newOperation; + opR.OverwriteSourceLocation(divLine, divColumn); + + // Attempt constant folding afterwards + instruction.Right = ConstantFolding.AttemptReduction(opR); + + // Special case for when N == 2 afterwards + // then revert back to X * X + if (instruction.Right is InterimConstantValue constantR) + { + if (Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) + { + instruction.Right = instruction.Left; + instruction.Operation = new OpcodeMathMultiply(); + } + /*else if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) + return instruction.Left; + else if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + return new InterimConstantValue(Encapsulation.ScalarIntValue.One, instruction);*/ + } + return instruction; + } + + private static IRBinaryOp CreatePower(IRBinaryOp instruction) + { + // X*(X*X) = X^3 + instruction.Operation = new OpcodeMathPower(); + instruction.Right = new InterimConstantValue(new Encapsulation.ScalarIntValue(3), (IRInstruction)instruction.Right); + return instruction; + } + } +} diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index c98b38518..bb3df21a2 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -55,103 +55,125 @@ private static void ApplyPass(BasicBlock block) { if (inst is IOperandInstructionBase operandInstruction) { - operandInstruction.MutateEachOperand(AttemptReductionToConstant); + operandInstruction.MutateEachOperand(AttemptReduction); } } } } - private static IInterimOperand AttemptReductionToConstant(IInterimOperand input) - { - if (input is IResultingInstruction instruction) - return AttemptReduction(instruction); - return input; - } - public static IInterimOperand AttemptReduction(IResultingInstruction instruction) + public static IInterimOperand AttemptReduction(IInterimOperand input) { // Only fold into an IRConstant when it is a primitive that can be stored in ksm. - if (!typeof(Encapsulation.PrimitiveStructure).IsAssignableFrom(instruction.Type)) - return instruction; + if (input is IEvaluatableToConstant evaluatableToConstant && + evaluatableToConstant.IsInvariant && + typeof(Encapsulation.PrimitiveStructure).IsAssignableFrom(input.Type)) + return evaluatableToConstant.Evaluate(); + + return Simplify(input); + } - switch (instruction) + private static IInterimOperand Simplify(IInterimOperand input) + { + switch (input) { case IRUnaryOp unaryOp: - return ReduceUnary(unaryOp); + return AlgebraicSimplifications.AttemptUnarySimplification(unaryOp); case IRBinaryOp binaryOp: - return ReduceBinary(binaryOp); - case IRCall call: - return ReduceCall(call); + return AttemptBinarySimplification(binaryOp); default: - return instruction; + return input; } } - private static IInterimOperand ReduceUnary(IRUnaryOp instruction) - { - if (instruction.IsInvariant) - return instruction.Evaluate(); - return instruction; - } - private static IInterimOperand ReduceBinary(IRBinaryOp instruction) + + private static IInterimOperand AttemptBinarySimplification(IRBinaryOp instruction) { - if (instruction.IsInvariant) - return instruction.Evaluate(); + instruction = AlgebraicSimplifications.AttemptAlgebraicSimplification(instruction); - // Put constants to the right, if there are any - if (instruction.IsCommutative && instruction.Left is InterimConstantValue && !(instruction.Right is InterimConstantValue)) + // Put constants to the left, if there are any + if (instruction.IsCommutative && instruction.Right is InterimConstantValue && !(instruction.Left is InterimConstantValue)) { - instruction.SwapOperands(); + BinaryOpcode originalOperation = instruction.Operation; + bool swapped = instruction.SwapOperands(); + // The value on the left is now a negation operation + // wrapping a constant. Simplify that, + if (swapped && originalOperation is OpcodeMathSubtract && + instruction.Left is IRUnaryOp negateOp) + { + instruction.Left = negateOp.Evaluate(); + } } // If this is false, neither are constants after the last step, unless this isn't commutative, in which case this cleverness doesn't matter. - if (instruction.Right is InterimConstantValue constantR) + if (instruction.Left is InterimConstantValue constantL) { // If this is true, both are constants - if (instruction.Left is InterimConstantValue) + if (instruction.Right is InterimConstantValue) { - return instruction.Evaluate(); + // Both may be constants, but if AttemptReduction() + // didn't evaluate this, the return type is not a valid + // opcode argument. Return the unchanged instruction. + return instruction; } else if (instruction.IsCommutative) { - // The right is constant and the left is not... - // But what if left.Parent is commutative with this operation and has a constant? - if (instruction.Left is IRBinaryOp leftOp && - leftOp.Operation.GetType() == instruction.Operation.GetType() && - leftOp.Right is InterimConstantValue constantL1) + // The left is constant and the right is not... + // But what if the right is commutative with this operation and has a constant? + if (instruction.Right is IRBinaryOp rightOp && + rightOp.IsCommutative && + OperationsHaveEqualPriority(rightOp.Operation, instruction.Operation) && + rightOp.Left is InterimConstantValue constantR1) { - object right = constantR.Value; - object left = constantL1.Value; + object left = constantR1.Value; + object right = constantL.Value; try { InterimConstantValue result = new InterimConstantValue(instruction.Operation.ExecuteCalculation(left, right), instruction); - leftOp.Right = result; + rightOp.Left = result; } catch (KOSBinaryOperandTypeException binaryTypeException) { throw new KOSCompileException(instruction, binaryTypeException); } - return leftOp; + return rightOp; } } // Shortcuts for math operations where both sides don't need to be constant switch (instruction.Operation) { case OpcodeMathMultiply _: - // X * 0 = 0 - if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) - return constantR; - if (ReduceDivMult(instruction, constantR, out IInterimOperand newResult)) + // 0 * X = 0 + if (Encapsulation.ScalarIntValue.Zero.Equals(constantL.Value)) + return constantL; + if (ReduceDivMult(instruction, constantL, out IInterimOperand newResult)) return newResult; break; case OpcodeMathDivide _: - if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) - throw new KOSCompileException(instruction, new DivideByZeroException()); - if (ReduceDivMult(instruction, constantR, out newResult)) + // 0 / X = 0 + // Technically not true when X = 0 + // But that would otherwise throw a "Tried to push infinite on to the stack" error + // So this is an acceptable assumption that improves performance and eliminates an error. + // TODO: Add an "EXIT" (EOP) command to the language because this will break the + // PRINT(1/0) shortcut to cause a program to terminate. + if (Encapsulation.ScalarIntValue.Zero.Equals(constantL.Value)) + return constantL; + if (ReduceDivMult(instruction, constantL, out newResult)) return newResult; break; case OpcodeMathAdd _: case OpcodeMathSubtract _: - // X +- 0 = X + // 0 +- X = X + if (Encapsulation.ScalarIntValue.Zero.Equals(constantL.Value)) + return instruction.Right; + break; + } + } + else if (instruction.Right is InterimConstantValue constantR) + { + switch (instruction.Operation) + { + case OpcodeMathDivide _: + // X / 0 = Error if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) - return instruction.Left; + throw new KOSCompileException(instruction, new DivideByZeroException()); break; case OpcodeMathPower _: // X^0 = 1 @@ -163,20 +185,11 @@ private static IInterimOperand ReduceBinary(IRBinaryOp instruction) break; } } - else + else // Neither operand is constant { switch (instruction.Operation) { case OpcodeMathDivide _: - // 0 / X = 0 - // Technically not true when X = 0 - // But that would otherwise throw a "Tried to push infinite on to the stack" error - // So this is an acceptable assumption that improves performance and eliminates an error. - // TODO: Add an "EXIT" (EOP) command to the language because this will break the - // PRINT(1/0) shortcut to cause a program to terminate. - if (instruction.Left is InterimConstantValue constantL && - Encapsulation.ScalarIntValue.Zero.Equals(constantL.Value)) - return constantL; // X / X = 1 // Technically not true when X = 0 // But that would otherwise throw a "Tried to push infinite on to the stack" error @@ -188,16 +201,34 @@ private static IInterimOperand ReduceBinary(IRBinaryOp instruction) } return instruction; } - private static bool ReduceDivMult(IRBinaryOp instruction, InterimConstantValue secondOperand, out IInterimOperand newResult) + + private static bool OperationsHaveEqualPriority(BinaryOpcode operation1, BinaryOpcode operation2) { - // X */ 1 = X - if (Encapsulation.ScalarIntValue.One.Equals(secondOperand.Value)) + Type op1 = operation1.GetType(); + Type op2 = operation2.GetType(); + if (op1 == op2) + return true; + if (op1 == typeof(OpcodeMathAdd) && op2 == typeof(OpcodeMathSubtract)) + return true; + if (op2 == typeof(OpcodeMathAdd) && op1 == typeof(OpcodeMathSubtract)) + return true; + if (op1 == typeof(OpcodeMathMultiply) && op2 == typeof(OpcodeMathDivide)) + return true; + if (op2 == typeof(OpcodeMathMultiply) && op1 == typeof(OpcodeMathDivide)) + return true; + return false; + } + + private static bool ReduceDivMult(IRBinaryOp instruction, InterimConstantValue constantOperand, out IInterimOperand newResult) + { + // 1 */ X = X + if (Encapsulation.ScalarIntValue.One.Equals(constantOperand.Value)) { - newResult = instruction.Left; + newResult = instruction.Right; return true; } - // X */ -1 = -X - if (secondOperand.Value.Equals(-Encapsulation.ScalarIntValue.One)) + // -1 */ X = -X + if (constantOperand.Value.Equals(-Encapsulation.ScalarIntValue.One)) { newResult = new IRUnaryOp( instruction.Block, @@ -206,19 +237,11 @@ private static bool ReduceDivMult(IRBinaryOp instruction, InterimConstantValue s SourceColumn = instruction.SourceColumn, SourceLine = instruction.SourceLine }, - instruction.Left); + instruction.Right); return true; } newResult = null; return false; } - - private static IInterimOperand ReduceCall(IRCall instruction) - { - if (instruction.IsInvariant) - return instruction.Evaluate(); - - return instruction; - } } } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs index f36638b18..40c2b070d 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs @@ -54,15 +54,6 @@ private static IInterimOperand OperandPeepholeFilter(IInterimOperand operand) return ReplaceVectorDotProduct(vDotCall); } - // Redundant unary operation replacements - // E.g. !!X = X and --X = X - if (operand is IRUnaryOp unaryParent) - { - IInterimOperand potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); - if (potentialResult != null) - return potentialResult; - } - // Replace lex indexing using string constant with suffixing where possible if (operand is IRIndexGet indexGet && indexGet.Index is InterimConstantValue indexConstant && @@ -74,152 +65,6 @@ indexGet.Index is InterimConstantValue indexConstant && return potentialResult; } - - // Algebraic simplifications - if (operand is IRBinaryOp binaryOp) - { - switch (binaryOp.Operation) - { - case OpcodeMathAdd _: - { - // A*B + A*C = A*(B+C) - { - if (binaryOp.Left is IRBinaryOp opL && - binaryOp.Right is IRBinaryOp opR && - opL.Operation is OpcodeMathMultiply && - opR.Operation is OpcodeMathMultiply && - binaryOp.IsCommutative && - opL.IsCommutative && - opR.IsCommutative) - { - return DistributeMultiplication(binaryOp, opL, opR); - } - } - // -B+A = A+-B = A-B - { - if (binaryOp.Right is IRUnaryOp opR && - opR.Operation is OpcodeMathNegate) - { - return DistributeNegationB(binaryOp); - } - if (binaryOp.Left is IRUnaryOp opL && - opL.Operation is OpcodeMathNegate && - binaryOp.SwapOperands()) - { - return DistributeNegationB(binaryOp); - } - } - // -A+B = B-A - { - if (binaryOp.Left is IRUnaryOp opL && - opL.Operation is OpcodeMathNegate) - { - return DistributeNegationA(binaryOp); - } - } - } - break; - case OpcodeMathSubtract _: - // A*B - A*C = A*(B-C) - { - if (binaryOp.Left is IRBinaryOp opL && - binaryOp.Right is IRBinaryOp opR && - opL.Operation is OpcodeMathMultiply && - opR.Operation is OpcodeMathMultiply && - binaryOp.IsCommutative && - opL.IsCommutative && - opR.IsCommutative) - { - return DistributeMultiplication(binaryOp, opL, opR); - } - } - // A--B=A+B - { - if (binaryOp.Right is IRUnaryOp unaryOp && - unaryOp.Operation is OpcodeMathNegate) - { - return ReplaceNegateSubtract(binaryOp); - } - } - break; - case OpcodeMathDivide _: - // TODO: Handle distributivity - if (binaryOp.Right is InterimConstantValue constantR && - Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) - throw new Exceptions.KOSCompileException(binaryOp, new DivideByZeroException()); - // X^N/X=X^(N-1) - { - if (binaryOp.Left is IRBinaryOp opL && - opL.Operation is OpcodeMathPower && - opL.Left.Equals(binaryOp.Right)) - { - return IncreasePower(binaryOp, -1); - } - } - // X^N/X^M=X^(N-M) - { - if (binaryOp.Left is IRBinaryOp opL && - opL.Operation is OpcodeMathPower && - binaryOp.Right is IRBinaryOp opR && - opR.Operation is OpcodeMathPower && - opL.Left.Equals(opR.Left)) - { - return DividePowers(binaryOp); - } - } - break; - case OpcodeMathMultiply _: - // X^N*X=X^(N+1) - { - if (binaryOp.Left is IRBinaryOp opL && - opL.Operation is OpcodeMathPower && - opL.Left.Equals(binaryOp.Right)) - { - return IncreasePower(binaryOp, 1); - } - if (binaryOp.Right is IRBinaryOp opR && - opR.Operation is OpcodeMathPower && - opR.Left.Equals(binaryOp.Left) && - binaryOp.SwapOperands()) - { - return IncreasePower(binaryOp, 1); - } - } - // X*X*...*X=N^X - // Start with (X*X)*X = X*(X*X) = X^3 - // Then the previous rule will kick in and exponentiate from there - { - if (binaryOp.Right is IRBinaryOp opR && - opR.Operation is OpcodeMathMultiply && - opR.Left.Equals(opR.Right) && - opR.Left.Equals(binaryOp.Left)) - { - return CreatePower(binaryOp); - } - if (binaryOp.Left is IRBinaryOp opL && - opL.Operation is OpcodeMathMultiply && - opL.Left.Equals(opL.Right) && - opL.Left.Equals(binaryOp.Right) && - binaryOp.SwapOperands()) - { - return CreatePower(binaryOp); - } - } - // X^N*X^M=X^(N+M) - { - if (binaryOp.Left is IRBinaryOp opL && - opL.Operation is OpcodeMathPower && - binaryOp.Right is IRBinaryOp opR && - opR.Operation is OpcodeMathPower && - opL.Left.Equals(opR.Left)) - { - return CombinePowers(binaryOp); - } - } - break; - } - } - return operand; } @@ -261,27 +106,6 @@ private static IRBinaryOp ReplaceVectorDotProduct(IRCall call) call.Arguments[1]); } - private static IInterimOperand AttempReplaceRedundantUnaryOp(IRUnaryOp unaryParent) - { - // Replace --X with X - if (CanOperationBeReplaced(unaryParent, typeof(OpcodeMathNegate))) - return GetDoubleNestedValue(unaryParent); - // Replace !!X with X - if (CanOperationBeReplaced(unaryParent, typeof(OpcodeLogicNot))) - return GetDoubleNestedValue(unaryParent); - return null; - } - private static bool CanOperationBeReplaced(IRUnaryOp operation, Type opcodeType) - { - return operation.Operation.GetType() == opcodeType && - operation.Operand is IRUnaryOp neg2 && - neg2.Operation.GetType() == opcodeType; - } - private static IInterimOperand GetDoubleNestedValue(ISingleOperandInstruction operation) - { - return ((IRUnaryOp)operation.Operand).Operand; - } - private static IRSuffixGet AttemptReplaceIndexGetWithSuffixGet(IRIndexGet indexGet) { if (indexGet.Index is InterimConstantValue indexConstant && @@ -321,203 +145,9 @@ indexConstant.Value is Encapsulation.StringValue stringIndex && private static void ReplaceRedundantNotBranch(IRBranch branch) { - branch.Condition = GetDoubleNestedValue(branch); + branch.Condition = AlgebraicSimplifications.GetDoubleNestedValue(branch); branch.PreferFalse = !branch.PreferFalse; (branch.True, branch.False) = (branch.False, branch.True); } - - private static IInterimOperand DistributeMultiplication(IRBinaryOp binaryOp, IRBinaryOp opL, IRBinaryOp opR) - { - if (opR.Left.Equals(opL.Left)) - { - return DistributeMultiplication(binaryOp); - } - if (opR.Left.Equals(opL.Right)) - { - if (!opL.SwapOperands()) - return binaryOp; - return DistributeMultiplication(binaryOp); - } - if (opR.Right.Equals(opL.Left)) - { - if (!opR.SwapOperands()) - return binaryOp; - return DistributeMultiplication(binaryOp); - } - if (opR.Right.Equals(opL.Right)) - { - if (!opL.SwapOperands() || - !opR.SwapOperands()) - return binaryOp; - return DistributeMultiplication(binaryOp); - } - return binaryOp; - } - - private static IInterimOperand DistributeMultiplication(IRBinaryOp instruction) - { - // A*B + A*C = A*(B+C) - IRBinaryOp opL = (IRBinaryOp)instruction.Left; - IRBinaryOp opR = (IRBinaryOp)instruction.Right; - // Restructure to create the parentheses - opR.Operation = instruction.Operation; - opR.Left = opL.Right; - // Restructure the multiplication term. - instruction.Left = opL.Left; - instruction.Operation = new OpcodeMathMultiply(); - return instruction; - } - - private static IInterimOperand DistributeNegationA(IRBinaryOp instruction) - { - // -A+B = B-A - if (!(instruction.Left is IRUnaryOp opL)) - return instruction; - if (!instruction.IsCommutative) - return instruction; - BinaryOpcode originalOperation = instruction.Operation; - instruction.Operation = new OpcodeMathSubtract(); - if (!instruction.IsCommutative) - { - instruction.Operation = originalOperation; - return instruction; - } - instruction.Left = opL.Operand; - instruction.SwapOperands(); - return instruction; - } - - private static IInterimOperand DistributeNegationB(IRBinaryOp instruction) - { - // A+-B = A-B - if (!(instruction.Right is IRUnaryOp opR)) - return instruction; - if (!instruction.IsCommutative) - return instruction; - BinaryOpcode originalOperation = instruction.Operation; - instruction.Operation = new OpcodeMathSubtract(); - if (!instruction.IsCommutative) - { - instruction.Operation = originalOperation; - return instruction; - } - instruction.Right = opR.Operand; - return instruction; - } - - private static IInterimOperand ReplaceNegateSubtract(IRBinaryOp instruction) - { - // A--B=A+B - if (!instruction.IsCommutative) - return instruction; - BinaryOpcode originalOperation = instruction.Operation; - instruction.Operation = new OpcodeMathAdd(); - if (!instruction.IsCommutative) - { - instruction.Operation = originalOperation; - return instruction; - } - IRUnaryOp opR = (IRUnaryOp)instruction.Right; - instruction.Right = opR.Operand; - return instruction; - } - - private static IInterimOperand IncreasePower(IRBinaryOp instruction, int powerIncrease) - { - // X^N*X=X^(N+1) - // X^N/X=X^(N-1) - // Assumes |N +/- 1| < 2^31 - 1 (for integer scalars) or 2^53 (for double scalars) - // Going beyond that overflows an integer or the mantissa for double. - - IRBinaryOp opL = (IRBinaryOp)instruction.Left; - - short divLine = instruction.SourceLine; - short divColumn = instruction.SourceColumn; - - InterimConstantValue powConst = new InterimConstantValue(new Encapsulation.ScalarIntValue(powerIncrease), instruction); - - // Set up the new power operation - instruction.Left = opL.Left; - instruction.Operation = new OpcodeMathPower(); - instruction.OverwriteSourceLocation(opL.SourceLine, opL.SourceColumn); - - // Reuse the old power instruction for the addition/subtraction - instruction.Right = opL; - opL.Left = powConst; - opL.Operation = new OpcodeMathAdd(); - opL.OverwriteSourceLocation(divLine, divColumn); - - // Attempt constant folding afterwards - instruction.Right = ConstantFolding.AttemptReduction(opL); - - // Special case for when N == 2 afterwards - // then revert back to X * X - if (instruction.Right is InterimConstantValue constantR) - { - if (Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) - { - instruction.Right = instruction.Left; - instruction.Operation = new OpcodeMathMultiply(); - } - else if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) - return instruction.Left; - else if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) - return new InterimConstantValue(Encapsulation.ScalarIntValue.One, instruction); - } - return instruction; - } - - private static IInterimOperand CreatePower(IRBinaryOp instruction) - { - // X*(X*X) = X^3 - instruction.Operation = new OpcodeMathPower(); - instruction.Right = new InterimConstantValue(new Encapsulation.ScalarIntValue(3), (IRInstruction)instruction.Right); - return instruction; - } - - private static IInterimOperand CombinePowers(IRBinaryOp instruction) - => CombinePowers(instruction, new OpcodeMathAdd()); - private static IInterimOperand DividePowers(IRBinaryOp instruction) - => CombinePowers(instruction, new OpcodeMathSubtract()); - private static IInterimOperand CombinePowers(IRBinaryOp instruction, BinaryOpcode newOperation) - { - // X^N*X^M=X^(N+M) - // Assumes |N + M| < 2^31 - 1 (for integer scalars) or 2^53 (for double scalars) - - IRBinaryOp opL = (IRBinaryOp)instruction.Left; - IRBinaryOp opR = (IRBinaryOp)instruction.Right; - - short divLine = instruction.SourceLine; - short divColumn = instruction.SourceColumn; - - // Set up the new power operation - instruction.Left = opL.Left; - instruction.Operation = new OpcodeMathPower(); - instruction.OverwriteSourceLocation(opL.SourceLine, opL.SourceColumn); - - // Reuse an old power instruction for the addition/subtraction - opR.Left = opL.Right; - opR.Operation = newOperation; - opR.OverwriteSourceLocation(divLine, divColumn); - - // Attempt constant folding afterwards - instruction.Right = ConstantFolding.AttemptReduction(opR); - - // Special case for when N == 2 afterwards - // then revert back to X * X - if (instruction.Right is InterimConstantValue constantR) - { - if (Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) - { - instruction.Right = instruction.Left; - instruction.Operation = new OpcodeMathMultiply(); - } - else if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) - return instruction.Left; - else if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) - return new InterimConstantValue(Encapsulation.ScalarIntValue.One, instruction); - } - return instruction; - } } } From b00d1979d41f0e2dd97f6ae93e97486fd633701a Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 18 May 2026 14:37:25 -0400 Subject: [PATCH 080/120] Make it more clear what Commutative means in the sense of subtraction, since the use is slightly nonstandard. --- src/kOS.Safe/Compilation/Calculator.cs | 2 +- src/kOS.Safe/Compilation/CalculatorString.cs | 2 +- src/kOS.Safe/Compilation/CalculatorStructure.cs | 2 +- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 9 +++++++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/kOS.Safe/Compilation/Calculator.cs b/src/kOS.Safe/Compilation/Calculator.cs index 590bee8a0..869a39b38 100644 --- a/src/kOS.Safe/Compilation/Calculator.cs +++ b/src/kOS.Safe/Compilation/Calculator.cs @@ -36,7 +36,7 @@ public abstract class Calculator /// /// true if subtraction is commutative with negation/addition; otherwise, false. /// - public virtual bool IsSubtractionCommutative(Type leftType, Type rightType) => true; + public virtual bool IsSubtractionCommutativeWithNegation(Type leftType, Type rightType) => true; public virtual bool IsMultiplicationCommmutative(Type leftType, Type rightType) => true; public virtual bool IsDivisionCommutative(Type leftType, Type rightType) => false; diff --git a/src/kOS.Safe/Compilation/CalculatorString.cs b/src/kOS.Safe/Compilation/CalculatorString.cs index 8387569af..055f1df63 100644 --- a/src/kOS.Safe/Compilation/CalculatorString.cs +++ b/src/kOS.Safe/Compilation/CalculatorString.cs @@ -23,7 +23,7 @@ public override Type GetSubtractResultType(Type leftType, Type rightType) { throw new KOSBinaryOperandTypeException(leftType, rightType, "subtract", "from"); } - public override bool IsSubtractionCommutative(Type leftType, Type rightType) + public override bool IsSubtractionCommutativeWithNegation(Type leftType, Type rightType) => false; public override object Multiply(OperandPair pair) diff --git a/src/kOS.Safe/Compilation/CalculatorStructure.cs b/src/kOS.Safe/Compilation/CalculatorStructure.cs index 60dfdb13d..a38405d33 100644 --- a/src/kOS.Safe/Compilation/CalculatorStructure.cs +++ b/src/kOS.Safe/Compilation/CalculatorStructure.cs @@ -53,7 +53,7 @@ public override object Subtract(OperandPair pair) } public override Type GetSubtractResultType(Type leftType, Type rightType) => GetTypeForOperation(leftType, rightType, "Subtract", "op_Subtraction", "-"); - public override bool IsSubtractionCommutative(Type leftType, Type rightType) + public override bool IsSubtractionCommutativeWithNegation(Type leftType, Type rightType) => GetCommutativityForOperation(leftType, rightType, "Subtract", "op_Subtraction", "-"); public override object Multiply(OperandPair pair) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 82ea99e10..0ccaa14be 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -190,7 +190,7 @@ public bool IsCommutative case OpcodeMathAdd _: return calculator.IsAdditionCommutative(Left.Type, Right.Type); case OpcodeMathSubtract _: - return calculator.IsSubtractionCommutative(Left.Type, Right.Type); + return calculator.IsSubtractionCommutativeWithNegation(Left.Type, Right.Type); case OpcodeMathMultiply _: return calculator.IsMultiplicationCommmutative(Left.Type, Right.Type); case OpcodeMathDivide _: @@ -226,12 +226,17 @@ public bool SwapOperands() { if (!IsCommutative) return false; - //throw new System.InvalidOperationException($"{this} is not commutative."); if (Operation is OpcodeMathSubtract) { Right = new IRUnaryOp(Block, new OpcodeMathNegate(), Right); Operation = new OpcodeMathAdd(); } + else if (Operation is OpcodeMathAdd && + Left is IRUnaryOp negation) + { + Left = negation.Operand; + Operation = new OpcodeMathSubtract(); + } (Right, Left) = (Left, Right); switch (Operation) { From 992414871e6aea0b41d272c0706e662b91b05b4e Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 18 May 2026 14:38:18 -0400 Subject: [PATCH 081/120] Improve reordering of operations to maximize the amount of constants that can be folded. --- .../Optimization/Passes/ConstantFolding.cs | 69 +++++++++++++++---- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index bb3df21a2..35828020c 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -113,29 +113,35 @@ private static IInterimOperand AttemptBinarySimplification(IRBinaryOp instructio // opcode argument. Return the unchanged instruction. return instruction; } - else if (instruction.IsCommutative) + + // Reorder/reflow operations if it helps to fold constants + if (instruction.Right is IRBinaryOp rightOp && + OperationsHaveEqualPriority(instruction.Operation, rightOp.Operation)) { - // The left is constant and the right is not... - // But what if the right is commutative with this operation and has a constant? - if (instruction.Right is IRBinaryOp rightOp && - rightOp.IsCommutative && - OperationsHaveEqualPriority(rightOp.Operation, instruction.Operation) && - rightOp.Left is InterimConstantValue constantR1) + // L _ (L1 _ R1) + // C _ (A _ B) = (C _ A) _ B (doesn't require commutativity) + if (rightOp.Left is InterimConstantValue constantRL) { - object left = constantR1.Value; - object right = constantL.Value; - try + InterimConstantValue result = ExecuteOperation(instruction, constantL, constantRL); + if (result != null) { - InterimConstantValue result = new InterimConstantValue(instruction.Operation.ExecuteCalculation(left, right), instruction); rightOp.Left = result; + instruction = rightOp; } - catch (KOSBinaryOperandTypeException binaryTypeException) + } + // C _ (A _ B) = A _ (C _ B) = (C _ B) _ A (requires commutativity between A and C) + else if (instruction.IsCommutative && + rightOp.Right is InterimConstantValue constantRR) + { + InterimConstantValue result = ExecuteOperation(rightOp, constantL, constantRR); + if (result != null) { - throw new KOSCompileException(instruction, binaryTypeException); + instruction.Right = instruction.Left; + instruction.Left = result; } - return rightOp; } } + // Shortcuts for math operations where both sides don't need to be constant switch (instruction.Operation) { @@ -168,6 +174,23 @@ private static IInterimOperand AttemptBinarySimplification(IRBinaryOp instructio } else if (instruction.Right is InterimConstantValue constantR) { + // Reorder/reflow operations if it helps to fold constants + if (instruction.Left is IRBinaryOp leftOp && + OperationsHaveEqualPriority(instruction.Operation, leftOp.Operation)) + { + // (L1 _ R1) _ R + // (A _ B) _ C = A _ (B _ C) (doesn't require commutativity) + if (leftOp.Right is InterimConstantValue constantLR) + { + InterimConstantValue result = ExecuteOperation(instruction, constantLR, constantR); + if (result != null) + { + leftOp.Right = result; + instruction = leftOp; + } + } + } + switch (instruction.Operation) { case OpcodeMathDivide _: @@ -219,6 +242,24 @@ private static bool OperationsHaveEqualPriority(BinaryOpcode operation1, BinaryO return false; } + private static InterimConstantValue ExecuteOperation(IRBinaryOp operation, InterimConstantValue left, InterimConstantValue right) + { + object leftValue = left.Value; + object rightValue = right.Value; + try + { + return new InterimConstantValue(operation.Operation.ExecuteCalculation(leftValue, rightValue), operation); + } + catch (KOSBinaryOperandTypeException binaryTypeException) + { +#if DEBUG + throw new KOSCompileException(operation, binaryTypeException); +#else + return null; +#endif + } + } + private static bool ReduceDivMult(IRBinaryOp instruction, InterimConstantValue constantOperand, out IInterimOperand newResult) { // 1 */ X = X From 4689cfe074bffaeecb46fe645f9e13ff3809ff25 Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 18 May 2026 14:52:46 -0400 Subject: [PATCH 082/120] Add a simplification for X/X^N. --- .../Passes/AlgebraicSimplifications.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs b/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs index 0cc2e48bc..beff4dd2e 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs @@ -127,7 +127,6 @@ opR.Operation is OpcodeMathDivide && } break; case OpcodeMathDivide _: - // TODO: Handle distributivity if (binaryOp.Right is InterimConstantValue constantR && Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) throw new Exceptions.KOSCompileException(binaryOp, new DivideByZeroException()); @@ -151,9 +150,24 @@ opR.Operation is OpcodeMathPower && return DividePowers(binaryOp); } } + // X/X^N = X^(1-N) + { + if (binaryOp.Right is IRBinaryOp opR && + opR.Operation is OpcodeMathPower && + opR.Left.Equals(binaryOp.Left)) + { + // Cheat by change X to X^1 and reusing the same code. + binaryOp.Left = new IRBinaryOp( + binaryOp.Block, + new OpcodeMathPower(), + binaryOp.Left, + new InterimConstantValue(Encapsulation.ScalarIntValue.One, binaryOp)); + return DividePowers(binaryOp); + } + } break; case OpcodeMathMultiply _: - // X^N*X=X^(N+1) + // X^N*X=X*X^N=X^(N+1) { if (binaryOp.Left is IRBinaryOp opL && opL.Operation is OpcodeMathPower && From 3e866e1f9ee2b64a27c2631a7cb883e173f1f4ff Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 18 May 2026 15:18:43 -0400 Subject: [PATCH 083/120] Pass the context of whether builtins may be clobbered. Disable some optimizations that could go awry if function names have changed. --- .../Execution/OptimizationTest.cs | 17 ++++++-- src/kOS.Safe/Compilation/KS/Compiler.cs | 2 +- .../Optimization/IOptimizationPass.cs | 4 -- .../Compilation/Optimization/Optimizer.cs | 13 +++--- .../Passes/AlgebraicSimplifications.cs | 30 ++++++------- .../Optimization/Passes/ConstantFolding.cs | 38 ++++++++++------- .../Passes/PeepholeOptimizations.cs | 42 +++++++++++-------- .../Passes/SCCPWithTypePropagation.cs | 6 +-- .../Optimization/Passes/SuffixReplacement.cs | 3 +- 9 files changed, 87 insertions(+), 68 deletions(-) diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index 2d01cae30..a02735631 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -29,10 +29,17 @@ protected List CompileCodePart(string fileName) return compiled; } - public List GetOpcodesAfterOptimization(List code, OptimizationLevel optimizationLevel = OptimizationLevel.Minimal) + public List GetOpcodesAfterOptimization(List code, + OptimizationLevel optimizationLevel = OptimizationLevel.Minimal, + bool allowClobberBuiltins = true) { + CompilerOptions options = new CompilerOptions() + { + AllowClobberBuiltins = allowClobberBuiltins, + OptimizationLevel = optimizationLevel + }; IRCodePart codePart = new IRCodePart(code, new List(), new List()); - var optimizer = new Safe.Compilation.Optimization.Optimizer(optimizationLevel); + var optimizer = new Safe.Compilation.Optimization.Optimizer(options); optimizer.Optimize(codePart); CodePart result = new CodePart(); codePart.EmitCode(result); @@ -327,7 +334,8 @@ public void TestParameterlessSuffixMethodReplacement() new OpcodeCall(""), new OpcodePop(), new OpcodePopScope() - } + }, + allowClobberBuiltins: false ); // The first one should be replaced Assert.IsInstanceOf(result[1]); @@ -360,7 +368,8 @@ public void TestVectorDotProductReplacement() new OpcodeCall("vectordotproduct"), new OpcodePop(), new OpcodePopScope() - } + }, + allowClobberBuiltins: false ); Assert.IsInstanceOf(result[2]); diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index 564fd51c1..255fb9761 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -213,7 +213,7 @@ public CodePart Compile(int startLineNum, ParseTree tree, Context context, Compi public static void Optimize(CodePart code, Context context, CompilerOptions options) { IR.IRCodePart irCodePart = new IR.IRCodePart(code, context.UserFunctions.PeekNewFunctions(), context.Triggers.PeekNewParts()); - Optimization.Optimizer optimizer = new Optimization.Optimizer(options.OptimizationLevel); + Optimization.Optimizer optimizer = new Optimization.Optimizer(options); optimizer.Optimize(irCodePart); irCodePart.EmitCode(code); } diff --git a/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs b/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs index bcf368f40..5409d98e3 100644 --- a/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs +++ b/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs @@ -46,9 +46,5 @@ public interface ILinkedOptimizationPass : IOptimizationPass /// optimization pass. /// Optimizer Optimizer { set; } - /// - /// Executes the optimization pass. - /// - void ApplyPass(); } } diff --git a/src/kOS.Safe/Compilation/Optimization/Optimizer.cs b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs index 955611630..4790a2022 100644 --- a/src/kOS.Safe/Compilation/Optimization/Optimizer.cs +++ b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs @@ -29,6 +29,10 @@ public class Optimizer /// public OptimizationLevel OptimizationLevel { get; } /// + /// Gets a value indicating whether built-in names may be clobbered. + /// + public bool AllowClobberBuiltins { get; } + /// /// Gets the collection of Basic Blocks being operated upon. /// public List Blocks { get; private set; } @@ -58,9 +62,11 @@ static Optimizer() /// Initializes a new instance of the class. /// /// The optimization level to be applied. - public Optimizer(OptimizationLevel optimizationLevel) + public Optimizer(CompilerOptions options) { - OptimizationLevel = optimizationLevel; + OptimizationLevel = options.OptimizationLevel; + AllowClobberBuiltins = options.AllowClobberBuiltins; + foreach (Type type in availablePassTypes) { #if DEBUG @@ -108,9 +114,6 @@ public List Optimize(IRCodePart codePart) SafeHouse.Logger.Log($"Applying optimization pass: {pass.GetType()}."); switch (pass) { - case ILinkedOptimizationPass linkedPass: - linkedPass.ApplyPass(); - break; case IHolisticOptimizationPass codePartpass: codePartpass.ApplyPass(Code); break; diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs b/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs index beff4dd2e..22778b268 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs @@ -38,7 +38,7 @@ public static IInterimOperand GetDoubleNestedValue(ISingleOperandInstruction ope } - public static IRBinaryOp AttemptAlgebraicSimplification(IRBinaryOp binaryOp) + public static IRBinaryOp AttemptAlgebraicSimplification(IRBinaryOp binaryOp, bool allowClobberBuiltins) { switch (binaryOp.Operation) { @@ -136,7 +136,7 @@ opR.Operation is OpcodeMathDivide && opL.Operation is OpcodeMathPower && opL.Left.Equals(binaryOp.Right)) { - return IncreasePower(binaryOp, -1); + return IncreasePower(binaryOp, -1, allowClobberBuiltins); } } // X^N/X^M=X^(N-M) @@ -147,7 +147,7 @@ binaryOp.Right is IRBinaryOp opR && opR.Operation is OpcodeMathPower && opL.Left.Equals(opR.Left)) { - return DividePowers(binaryOp); + return DividePowers(binaryOp, allowClobberBuiltins); } } // X/X^N = X^(1-N) @@ -162,7 +162,7 @@ opR.Operation is OpcodeMathPower && new OpcodeMathPower(), binaryOp.Left, new InterimConstantValue(Encapsulation.ScalarIntValue.One, binaryOp)); - return DividePowers(binaryOp); + return DividePowers(binaryOp, allowClobberBuiltins); } } break; @@ -173,14 +173,14 @@ opR.Operation is OpcodeMathPower && opL.Operation is OpcodeMathPower && opL.Left.Equals(binaryOp.Right)) { - return IncreasePower(binaryOp, 1); + return IncreasePower(binaryOp, 1, allowClobberBuiltins); } if (binaryOp.Right is IRBinaryOp opR && opR.Operation is OpcodeMathPower && opR.Left.Equals(binaryOp.Left) && binaryOp.SwapOperands()) { - return IncreasePower(binaryOp, 1); + return IncreasePower(binaryOp, 1, allowClobberBuiltins); } } // X*X*...*X=N^X @@ -215,7 +215,7 @@ binaryOp.Right is IRBinaryOp opR && opR.Operation is OpcodeMathPower && opL.Left.Equals(opR.Left)) { - return CombinePowers(binaryOp); + return CombinePowers(binaryOp, allowClobberBuiltins); } } break; @@ -332,7 +332,7 @@ private static IRBinaryOp ReplaceNegateSubtract(IRBinaryOp instruction) return instruction; } - private static IRBinaryOp IncreasePower(IRBinaryOp instruction, int powerIncrease) + private static IRBinaryOp IncreasePower(IRBinaryOp instruction, int powerIncrease, bool allowClobberBuiltins) { // X^N*X=X^(N+1) // X^N/X=X^(N-1) @@ -358,7 +358,7 @@ private static IRBinaryOp IncreasePower(IRBinaryOp instruction, int powerIncreas opL.OverwriteSourceLocation(divLine, divColumn); // Attempt constant folding afterwards - instruction.Right = ConstantFolding.AttemptReduction(opL); + instruction.Right = ConstantFolding.AttemptReduction(opL, allowClobberBuiltins); // Special case for when N == 2 afterwards // then revert back to X * X @@ -377,13 +377,13 @@ private static IRBinaryOp IncreasePower(IRBinaryOp instruction, int powerIncreas return instruction; } - private static IRBinaryOp DividePowers(IRBinaryOp instruction) - => CombinePowers(instruction, new OpcodeMathSubtract()); + private static IRBinaryOp DividePowers(IRBinaryOp instruction, bool allowClobberBuiltins) + => CombinePowers(instruction, new OpcodeMathSubtract(), allowClobberBuiltins); - private static IRBinaryOp CombinePowers(IRBinaryOp instruction) - => CombinePowers(instruction, new OpcodeMathAdd()); + private static IRBinaryOp CombinePowers(IRBinaryOp instruction, bool allowClobberBuiltins) + => CombinePowers(instruction, new OpcodeMathAdd(), allowClobberBuiltins); - private static IRBinaryOp CombinePowers(IRBinaryOp instruction, BinaryOpcode newOperation) + private static IRBinaryOp CombinePowers(IRBinaryOp instruction, BinaryOpcode newOperation, bool allowClobberBuiltins) { // X^N*X^M=X^(N+M) // Assumes |N + M| < 2^31 - 1 (for integer scalars) or 2^53 (for double scalars) @@ -405,7 +405,7 @@ private static IRBinaryOp CombinePowers(IRBinaryOp instruction, BinaryOpcode new opR.OverwriteSourceLocation(divLine, divColumn); // Attempt constant folding afterwards - instruction.Right = ConstantFolding.AttemptReduction(opR); + instruction.Right = ConstantFolding.AttemptReduction(opR, allowClobberBuiltins); // Special case for when N == 2 afterwards // then revert back to X * X diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index 35828020c..23f051f1e 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -5,8 +5,9 @@ namespace kOS.Safe.Compilation.Optimization.Passes { - public class ConstantFolding : IOptimizationPass + public class ConstantFolding : IOptimizationPass, ILinkedOptimizationPass { + public Optimizer Optimizer { get; set; } public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; public short SortIndex => 30; @@ -15,11 +16,14 @@ public void ApplyPass(List blocks) IEnumerator enumerator = blocks.GetEnumerator(); while (enumerator.MoveNext()) { - ApplyPass(enumerator.Current); + ApplyPass(enumerator.Current, Optimizer.AllowClobberBuiltins); } } - private static void ApplyPass(BasicBlock block) + private static void ApplyPass(BasicBlock block, bool allowClobberBuiltins) { + IInterimOperand AttemptReduction_Internal(IInterimOperand operand) + => AttemptReduction(operand, allowClobberBuiltins); + for (int i = 0; i < block.Instructions.Count; i++) { IRInstruction instruction = block.Instructions[i]; @@ -55,39 +59,43 @@ private static void ApplyPass(BasicBlock block) { if (inst is IOperandInstructionBase operandInstruction) { - operandInstruction.MutateEachOperand(AttemptReduction); + operandInstruction.MutateEachOperand(AttemptReduction_Internal); } } } } - public static IInterimOperand AttemptReduction(IInterimOperand input) + public static IInterimOperand AttemptReduction(IInterimOperand input, bool allowClobberBuiltins) { - // Only fold into an IRConstant when it is a primitive that can be stored in ksm. - if (input is IEvaluatableToConstant evaluatableToConstant && - evaluatableToConstant.IsInvariant && - typeof(Encapsulation.PrimitiveStructure).IsAssignableFrom(input.Type)) - return evaluatableToConstant.Evaluate(); + // Skip calls if builtins may be clobbered because they may not be what is expected. + if (!(allowClobberBuiltins && input is IRCall)) + { + // Only fold into an IRConstant when it is a primitive that can be stored in ksm. + if (input is IEvaluatableToConstant evaluatableToConstant && + evaluatableToConstant.IsInvariant && + typeof(Encapsulation.PrimitiveStructure).IsAssignableFrom(input.Type)) + return evaluatableToConstant.Evaluate(); + } - return Simplify(input); + return Simplify(input, allowClobberBuiltins); } - private static IInterimOperand Simplify(IInterimOperand input) + private static IInterimOperand Simplify(IInterimOperand input, bool allowClobberBuiltins) { switch (input) { case IRUnaryOp unaryOp: return AlgebraicSimplifications.AttemptUnarySimplification(unaryOp); case IRBinaryOp binaryOp: - return AttemptBinarySimplification(binaryOp); + return AttemptBinarySimplification(binaryOp, allowClobberBuiltins); default: return input; } } - private static IInterimOperand AttemptBinarySimplification(IRBinaryOp instruction) + private static IInterimOperand AttemptBinarySimplification(IRBinaryOp instruction, bool allowClobberBuiltins) { - instruction = AlgebraicSimplifications.AttemptAlgebraicSimplification(instruction); + instruction = AlgebraicSimplifications.AttemptAlgebraicSimplification(instruction, allowClobberBuiltins); // Put constants to the left, if there are any if (instruction.IsCommutative && instruction.Right is InterimConstantValue && !(instruction.Left is InterimConstantValue)) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs index 40c2b070d..b300fb2f3 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs @@ -4,21 +4,25 @@ namespace kOS.Safe.Compilation.Optimization.Passes { - public class PeepholeOptimizations : IOptimizationPass + public class PeepholeOptimizations : IOptimizationPass, ILinkedOptimizationPass { + public Optimizer Optimizer { get; set; } public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; public short SortIndex => 1050; public void ApplyPass(List code) { + IInterimOperand OperandPeepholeFilter_Internal(IInterimOperand operand) + => OperandPeepholeFilter(operand, Optimizer.AllowClobberBuiltins); + for (int i = 0; i < code.Count; i++) { IRInstruction instruction = code[i]; foreach (IRInstruction nestedInstruction in instruction.DepthFirst()) { if (nestedInstruction is IOperandInstructionBase operandInstruction) - operandInstruction.MutateEachOperand(OperandPeepholeFilter); + operandInstruction.MutateEachOperand(OperandPeepholeFilter_Internal); InstructionPeepholeFilter(nestedInstruction); } @@ -33,25 +37,27 @@ public void ApplyPass(List code) } } - private static IInterimOperand OperandPeepholeFilter(IInterimOperand operand) + private static IInterimOperand OperandPeepholeFilter(IInterimOperand operand, bool allowClobberBuiltins) { - // Replace parameterless suffix method calls with get member - // TODO: Skip this if clobber built-ins is active. - if (operand is IRCall suffixCall && - !suffixCall.Direct && - suffixCall.Arguments.Count == 0 && - suffixCall.IndirectMethod is IRSuffixGetMethod) + // These calls may not be what is expected if clobbering is permitted. + if (!allowClobberBuiltins) { - return ReplaceParameterlessSuffix(suffixCall); - } + // Replace parameterless suffix method calls with get member + if (operand is IRCall suffixCall && + !suffixCall.Direct && + suffixCall.Arguments.Count == 0 && + suffixCall.IndirectMethod is IRSuffixGetMethod) + { + return ReplaceParameterlessSuffix(suffixCall); + } - // Replace calls to VectorDotProduct with multiplication - // TODO: Skip this if clobber built-ins is active. - if (operand is IRCall vDotCall && - (vDotCall.Function == "vdot" || vDotCall.Function == "vectordotproduct") && - vDotCall.Arguments.Count == 2) - { - return ReplaceVectorDotProduct(vDotCall); + // Replace calls to VectorDotProduct with multiplication + if (operand is IRCall vDotCall && + (vDotCall.Function == "vdot" || vDotCall.Function == "vectordotproduct") && + vDotCall.Arguments.Count == 2) + { + return ReplaceVectorDotProduct(vDotCall); + } } // Replace lex indexing using string constant with suffixing where possible diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index 3320b53de..0e8d2b93e 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -6,7 +6,7 @@ namespace kOS.Safe.Compilation.Optimization.Passes { - public class SCCPWithTypePropagation : ILinkedOptimizationPass + public class SCCPWithTypePropagation : IHolisticOptimizationPass, ILinkedOptimizationPass { /// /// Gets the optimization level associated with this pass. @@ -21,10 +21,8 @@ public class SCCPWithTypePropagation : ILinkedOptimizationPass public Optimizer Optimizer { private get; set; } - public void ApplyPass() + public void ApplyPass(IRCodePart codePart) { - IRCodePart codePart = Optimizer.Code; - Dictionary> ssaUses = MapUsesAndPropagateTypes(codePart); diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs index b741bb33a..f1137de31 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs @@ -69,11 +69,10 @@ private static IInterimOperand AttempReplaceSuffix(IRSuffixGet suffixGet) case "obt": case "status": break; - // All other suffixes don't have an alias, so just return the original IRTemp + // All other suffixes don't have an alias, so just return the original default: return suffixGet; } - // Instead of the IRTemp, which leads to resolving the suffix, return just the alias shortcut. return new InterimVariableReference($"${suffixGet.Suffix}", suffixGet.SourceLine, suffixGet.SourceColumn); } if (variableReference.Name.Equals("$constant", StringComparison.OrdinalIgnoreCase)) From bf47ba7026c42bb3081b52e242e0b9f045d45061 Mon Sep 17 00:00:00 2001 From: DBooots Date: Tue, 19 May 2026 07:28:38 -0400 Subject: [PATCH 084/120] Revert inadvertent inclusion of a partially-implemented property. --- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 2 -- src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs | 4 ---- 2 files changed, 6 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 0ccaa14be..9e4f44c43 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -839,8 +839,6 @@ private bool IsCallInvariant() private Type GetDefaultReturnType() { IRCodePart.IRFunction function = Block.CodePart.GetFunction(this); - if (function != null) - return function.Returns.Type; if (Optimization.Optimizer.FunctionManager.Exists(Function.Replace("()", ""))) return Optimization.Optimizer.FunctionManager.FunctionReturnType(Function.Replace("()", "")); if (!Direct && IndirectMethod is IRSuffixGetMethod suffixGetMethod) diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index bb77431f5..b0d103b9a 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -146,10 +146,6 @@ public static void FinalizeSSA(IRCodePart codePart) funcOrTrigger?.TriggersCreated.Add(trigger); } break; - case IRReturn ret: - if (funcOrTrigger is IRFunction function) - function.Returns.PossibleValues[block] = ret.Value; - break; } } From 94e2c609484c69aa56b1f963e51e0ffed7950c3b Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 21 May 2026 22:36:16 -0400 Subject: [PATCH 085/120] Fix unit tests failing to compile on release builds.. --- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 8 ++++++-- src/kOS.Safe/Compilation/Optimization/Optimizer.cs | 14 +++----------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index dc59fae40..f87dc5677 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -81,7 +81,12 @@ public IRCodePart(CodePart codePart, List userFunctions, List + /// Initializes a new instance of the class for DEBUG purposes. + /// + /// + /// This constructor is only intended for unit testing. + /// public IRCodePart(List mainCode, List userFunctions, List triggers) { IRBuilder builder = new IRBuilder(); @@ -113,7 +118,6 @@ public IRCodePart(List mainCode, List userFunctions, List< SingleStaticAssignment.FinalizeSSA(this); } -#endif /// /// Gets a function by string reference. diff --git a/src/kOS.Safe/Compilation/Optimization/Optimizer.cs b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs index 4790a2022..9c5e12338 100644 --- a/src/kOS.Safe/Compilation/Optimization/Optimizer.cs +++ b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs @@ -14,10 +14,8 @@ namespace kOS.Safe.Compilation.Optimization [AssemblyWalk(InterfaceType = typeof(IOptimizationPass), StaticRegisterMethod = "RegisterMethod")] public class Optimizer { -#if DEBUG public static HashSet PassesToSkip { get; } = new HashSet(); - public static HashSet OnlyThesePasses { get; set; } = null; -#endif + internal static InterimCPU InterimCPU { get; } = new InterimCPU(); private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = InterimCPU }; private readonly SortedSet optimizationPasses = new SortedSet( @@ -69,18 +67,12 @@ public Optimizer(CompilerOptions options) foreach (Type type in availablePassTypes) { -#if DEBUG if (type != typeof(Passes.SCCPWithTypePropagation)) { - if (OnlyThesePasses != null) - { - if (!OnlyThesePasses.Contains(type)) - continue; - } - else if (PassesToSkip.Contains(type)) + if (PassesToSkip.Contains(type)) continue; } -#endif + IOptimizationPass pass = (IOptimizationPass)Activator.CreateInstance(type); optimizationPasses.Add(pass); if (pass is ILinkedOptimizationPass linkedPass) From d168f9c3dfee5d60c4d52a1c447fa80f10069655 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 21 May 2026 22:37:45 -0400 Subject: [PATCH 086/120] Fix error messages in unit tests. --- .../Compilation/TypeInferencing.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/kOS.Safe.Test/Compilation/TypeInferencing.cs b/src/kOS.Safe.Test/Compilation/TypeInferencing.cs index 55f942439..9e46abf5d 100644 --- a/src/kOS.Safe.Test/Compilation/TypeInferencing.cs +++ b/src/kOS.Safe.Test/Compilation/TypeInferencing.cs @@ -40,11 +40,11 @@ public void StructureSuffixTest() // Tests that the bare minimum works Type structureType = typeof(Encapsulation.Structure); Type result = TypeInferencer.GetTypeForSuffix(structureType, "TOSTRING"); - Assert.AreEqual(result, typeof(StringValue)); + Assert.AreEqual(typeof(StringValue), result); Type integerType = typeof(ScalarIntValue); result = TypeInferencer.GetTypeForSuffix(integerType, "istype"); - Assert.AreEqual(result, typeof(BooleanValue)); + Assert.AreEqual(typeof(BooleanValue), result); } [Test] @@ -53,21 +53,21 @@ public void ListSuffixTest() // Test that lists and enumerables work Type listType = typeof(ListValue); Type result = TypeInferencer.GetTypeForSuffix(listType, "ITERATOR"); - Assert.AreEqual(result, typeof(Enumerator)); + Assert.AreEqual(typeof(Enumerator), result); result = TypeInferencer.GetTypeForSuffix(listType, "COPY"); - Assert.AreEqual(result, typeof(ListValue)); + Assert.AreEqual(typeof(ListValue), result); result = TypeInferencer.GetTypeForIndex(listType); - Assert.AreEqual(result, typeof(Encapsulation.Structure)); + Assert.AreEqual(typeof(Encapsulation.Structure), result); result = TypeInferencer.GetTypeForSuffix(listType, "CLEAR"); - Assert.AreEqual(result, null); + Assert.AreEqual(null, result); // Test an abstract generic type listType = typeof(EnumerableValue>); result = TypeInferencer.GetTypeForSuffix(listType, "ITERATOR"); - Assert.AreEqual(result, typeof(Enumerator)); + Assert.AreEqual(typeof(Enumerator), result); } [Test] @@ -77,16 +77,16 @@ public void TestInstructionInferencing() InterimConstantValue b = new InterimConstantValue(ScalarIntValue.Two, 0, 0); IRBinaryOp add = new IRBinaryOp(null, new OpcodeMathAdd(), a, b); - Assert.AreEqual(add.Type, typeof(ScalarValue)); + Assert.AreEqual(typeof(ScalarValue), add.Type); IRCall call = new IRCall(null, new OpcodeCall("sin"), true, b); Assert.IsTrue(typeof(ScalarValue).IsAssignableFrom(call.Type)); IRCall print = new IRCall(null, new OpcodeCall("print"), true, a); - Assert.AreEqual(print.Type, null); + Assert.AreEqual(null, print.Type); IRCall userCall = new IRCall(null, new OpcodeCall("$test*"), true, a, b); - Assert.AreEqual(userCall.Type, typeof(Encapsulation.Structure)); + Assert.AreEqual(typeof(Encapsulation.Structure), userCall.Type); } } } From f1529840c5e8df00d93a7c38ac57e2eb1c90916b Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 21 May 2026 22:48:59 -0400 Subject: [PATCH 087/120] Fix constant variable propagation. Add a feature to remove assignments that become redundant following constant propagation. --- .../Execution/OptimizationTest.cs | 36 ++-- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 12 +- .../Compilation/IR/InterimVariables.cs | 161 ++++++++++---- .../Compilation/IR/SingleStaticAssignment.cs | 146 ++++++++----- .../Passes/SCCPWithTypePropagation.cs | 200 ++++++++++++++---- 5 files changed, 392 insertions(+), 163 deletions(-) diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index a02735631..fd7579966 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -463,22 +463,22 @@ public void TestConstantPropagationAndFolding() new OpcodePush("$b"), new OpcodePush("$c"), new OpcodeMathAdd(), - new OpcodeStoreLocal("$d"), + new OpcodeStoreGlobal("$d"), new OpcodePush("$a"), new OpcodePush("$b"), new OpcodeMathAdd(), - new OpcodeStoreLocal("$e"), + new OpcodeStoreGlobal("$e"), new OpcodePush("$b"), new OpcodePush("$g"), new OpcodeMathAdd(), - new OpcodeStoreLocal("$f"), + new OpcodeStoreGlobal("$f"), new OpcodePopScope() } ); - Assert.IsInstanceOf(result[7]); - Assert.AreEqual(new Encapsulation.ScalarIntValue(3), (result[7] as OpcodePush)?.Argument); - Assert.IsInstanceOf(result[11]); - Assert.IsInstanceOf(result[15]); + Assert.IsInstanceOf(result[2]); + Assert.AreEqual(new Encapsulation.ScalarIntValue(3), (result[2] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[6]); + Assert.IsInstanceOf(result[10]); } [Test] @@ -497,31 +497,31 @@ public void TestConstantPropagationCommutivity() new OpcodeMathAdd(), new OpcodePush(Encapsulation.ScalarIntValue.Two), new OpcodeMathAdd(), - new OpcodeStoreLocal("$result"), + new OpcodeStoreGlobal("$result"), // Block 2 new OpcodePush(Encapsulation.ScalarIntValue.One), new OpcodePush("$a"), new OpcodeMathAdd(), new OpcodePush(Encapsulation.ScalarIntValue.Two), new OpcodeMathSubtract(), - new OpcodeStoreLocal("$result"), + new OpcodeStoreExist("$result"), // Block 3 new OpcodePush(Encapsulation.ScalarIntValue.One), new OpcodePush("$a"), new OpcodeMathSubtract(), - new OpcodeStoreLocal("$result"), + new OpcodeStoreExist("$result"), // Block 4 new OpcodePush("$a"), new OpcodePush(Encapsulation.ScalarIntValue.One), new OpcodeMathSubtract(), - new OpcodeStoreLocal("$result"), + new OpcodeStoreExist("$result"), // Block 5 new OpcodePush(Encapsulation.ScalarIntValue.One), new OpcodePush("$a"), new OpcodeMathSubtract(), new OpcodePush(Encapsulation.ScalarIntValue.Two), new OpcodeMathAdd(), - new OpcodeStoreLocal("$result"), + new OpcodeStoreExist("$result"), new OpcodePopScope() } ); @@ -717,10 +717,10 @@ public void TestDivisionSubtraction() new OpcodePopScope() } ); + Assert.IsInstanceOf(result[5]); + Assert.AreEqual(new Encapsulation.ScalarDoubleValue(1.5), (result[5] as OpcodePush)?.Argument); Assert.IsInstanceOf(result[9]); - Assert.AreEqual(new Encapsulation.ScalarDoubleValue(1.5), (result[9] as OpcodePush)?.Argument); - Assert.IsInstanceOf(result[13]); - Assert.AreEqual(new Encapsulation.ScalarDoubleValue(-0.5), (result[13] as OpcodePush)?.Argument); + Assert.AreEqual(new Encapsulation.ScalarDoubleValue(-0.5), (result[9] as OpcodePush)?.Argument); } [Test] @@ -756,10 +756,10 @@ public void TestPowerAddition() new OpcodePopScope() } ); + Assert.IsInstanceOf(result[5]); + Assert.AreEqual(new Encapsulation.ScalarDoubleValue(3.5), (result[5] as OpcodePush)?.Argument); Assert.IsInstanceOf(result[9]); - Assert.AreEqual(new Encapsulation.ScalarDoubleValue(3.5), (result[9] as OpcodePush)?.Argument); - Assert.IsInstanceOf(result[13]); - Assert.AreEqual(new Encapsulation.ScalarDoubleValue(5.5), (result[13] as OpcodePush)?.Argument); + Assert.AreEqual(new Encapsulation.ScalarDoubleValue(5.5), (result[9] as OpcodePush)?.Argument); } [Test] diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index f87dc5677..89a056d4d 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -236,9 +236,9 @@ public class IRTrigger : IClosureVariableUser /// Gets or sets the code for this trigger, in BasicBlock representation. /// public List Code { get; set; } - public HashSet ExternalReads { get; set; } = new HashSet(); + public HashSet ExternalReads { get; set; } = new HashSet(); public HashSet ExternalWrites { get; } = new HashSet(); - public HashSet<(string, IRUnset)> ExternalUnsets { get; } = new HashSet<(string, IRUnset)>(); + public HashSet<(string Name, IRUnset Instruction)> ExternalUnsets { get; } = new HashSet<(string, IRUnset)>(); public HashSet TriggersCreated { get; } = new HashSet(); public HashSet FunctionCalls { get; } = new HashSet(); public IRScope ClosureScope { get; } @@ -303,9 +303,9 @@ public class IRFunction : IClosureVariableUser /// Gets the collection of function fragments. /// public IReadOnlyCollection Fragments => fragments.Values; - public HashSet ExternalReads { get; set; } = new HashSet(); + public HashSet ExternalReads { get; set; } = new HashSet(); public HashSet ExternalWrites { get; } = new HashSet(); - public HashSet<(string, IRUnset)> ExternalUnsets { get; } = new HashSet<(string, IRUnset)>(); + public HashSet<(string Name, IRUnset Instruction)> ExternalUnsets { get; } = new HashSet<(string, IRUnset)>(); public HashSet TriggersCreated { get; } = new HashSet(); public HashSet FunctionCalls { get; } = new HashSet(); public IRScope ClosureScope { get; } @@ -402,7 +402,7 @@ public interface IClosureVariableUser /// Gets the collection of external variables that are read /// within the closure. /// - HashSet ExternalReads { get; set; } + HashSet ExternalReads { get; set; } /// /// Gets the collection of external variables that may be written /// to by this instance. @@ -411,7 +411,7 @@ public interface IClosureVariableUser /// /// Gets the collection of external variables that may be unset by this instance. /// - HashSet<(string, IRUnset)> ExternalUnsets { get; } + HashSet<(string Name, IRUnset Instruction)> ExternalUnsets { get; } /// /// Gets the collection of triggers that could be created from this instance. /// diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs index 9527d5242..23ace33a7 100644 --- a/src/kOS.Safe/Compilation/IR/InterimVariables.cs +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -193,7 +193,7 @@ public static uint GetIndex(string name) } } - public abstract class SSADefinition + public abstract class SSADefinition : IEquatable { protected readonly uint ssaIndex; protected Dictionary potentialUnsetSites; @@ -212,6 +212,9 @@ public enum SetState public abstract Type Type { get; } public virtual SetState State { get; } public IRInstruction AssignedAt { get; } + public HashSet ReplacedBy { get; } = new HashSet(); + public HashSet Replaces { get; } = new HashSet(); + protected SSADefinition(string name) { Name = name; @@ -226,10 +229,10 @@ protected SSADefinition(string name, SetState state, IRInstruction assignedAt) : State = state; } - public abstract SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt); + public abstract SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt, bool writeReplaceChain); public abstract InterimConstantValue Evaluate(); - public SSADefinition PotentiallyOverwrite(SSASetDefinition newDefinition) + public SSADefinition PotentiallyOverwrite(SSASetDefinition newDefinition, bool writeReplaceChain) { if (State == SetState.Unset) return this; @@ -237,10 +240,16 @@ public SSADefinition PotentiallyOverwrite(SSASetDefinition newDefinition) throw new ArgumentException($"{nameof(newDefinition)} must not be definitively unset."); if (newDefinition.Equals(this)) return this; - if (potentialClobberDefinitions.TryGetValue(newDefinition, out SSAPotentialDefinition result)) - return result; - result = new SSAPotentialDefinition(this, newDefinition); - potentialClobberDefinitions[newDefinition] = result; + if (!potentialClobberDefinitions.TryGetValue(newDefinition, out SSAPotentialDefinition result)) + { + result = new SSAPotentialDefinition(this, newDefinition); + potentialClobberDefinitions[newDefinition] = result; + } + if (writeReplaceChain) + { + newDefinition.Replaces.Add(this); + ReplacedBy.Add(newDefinition); + } return result; } @@ -259,10 +268,20 @@ protected static string SetState_ToString(SetState state) } public override string ToString() => $"{Name} {SetState_ToString(State)}{ssaIndex}"; - /*public override bool Equals(object obj) - => obj == this; + public abstract bool ValuesEqual(SSADefinition other); + public bool Equals(SSADefinition other) + { + if (State != other.State) + return false; + if (!string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase)) + return false; + return ValuesEqual(other); + } + + public override bool Equals(object obj) + => obj is SSADefinition ssaDef && Equals(ssaDef); public override int GetHashCode() - => (ssaIndex, Name).GetHashCode();*/ + => Name.ToLower().GetHashCode(); } public class SSASetDefinition : SSADefinition @@ -271,7 +290,7 @@ public class SSASetDefinition : SSADefinition new Dictionary<(string, IRCall), SSASetDefinition>(); public IRAssign DefinedAt { get; } - public override bool IsInvariant => State != SetState.PotentiallyUnset && DefinedAt.IsInvariant && AssignedAt.IsInvariant; + public override bool IsInvariant => State != SetState.PotentiallyUnset && AssignedAt.IsInvariant; public override Type Type { get; } public SSASetDefinition(string name, IRAssign assignedAt) : base(name, SetState.Set, assignedAt) @@ -302,20 +321,36 @@ private SSASetDefinition(SSASetDefinition definition, IRUnset potentiallyUnsetAt DefinedAt = definition.DefinedAt; potentialUnsetSites = definition.potentialUnsetSites; } - public override SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt) + public override SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt, bool writeReplaceChain) { if (State == SetState.Unset) return this; - if (potentialUnsetSites.TryGetValue(potentiallyUnsetAt, out SSADefinition result)) - return result; - - result = new SSASetDefinition(this, potentiallyUnsetAt); - potentialUnsetSites[potentiallyUnsetAt] = result; + if (!potentialUnsetSites.TryGetValue(potentiallyUnsetAt, out SSADefinition result)) + { + result = new SSASetDefinition(this, potentiallyUnsetAt); + potentialUnsetSites[potentiallyUnsetAt] = result; + } + if (writeReplaceChain) + { + result.Replaces.Add(this); + ReplacedBy.Add(result); + } return result; } public override InterimConstantValue Evaluate() => (DefinedAt.Value as IEvaluatableToConstant).Evaluate(); + + public override bool ValuesEqual(SSADefinition other) + { + if (other is SSASetDefinition setDefinition) + { + if (setDefinition.DefinedAt == null) + return false; + return DefinedAt?.Value.Equals(setDefinition.DefinedAt.Value) ?? false; + } + return other.Equals(this); + } } public class SSAPotentialDefinition : SSADefinition, IMultipleOperandInstruction { @@ -327,7 +362,8 @@ public class SSAPotentialDefinition : SSADefinition, IMultipleOperandInstruction public IRUnset Conditional { get; } public override Type Type => State == SetState.Set ? PhiNode.GetFirstCommonBaseType(Preceding.Type, Succeeding.Type) : null; - public override bool IsInvariant => Conditional?.IsInvariant ?? false; + public override bool IsInvariant => (Conditional?.IsInvariant ?? false) && + (Conditional.IsExecutable ? Succeeding.IsInvariant : Preceding.IsInvariant); public IEnumerable Operands { @@ -369,25 +405,36 @@ private SSAPotentialDefinition(SSASetDefinition potentialDefinition, IRUnset con potentialUnsetSites = new Dictionary(); Conditional = condition; } - public static SSAPotentialDefinition PotentiallySet(SSASetDefinition potentialDefinition, IRUnset condition) + public static SSAPotentialDefinition PotentiallySet(SSASetDefinition potentialDefinition, IRUnset condition, bool writeReplaceChain) { - if (potentialSets.TryGetValue((condition, potentialDefinition), out var potentialSet)) - return potentialSet; - SSAPotentialDefinition result = new SSAPotentialDefinition(potentialDefinition, condition); - potentialSets[(condition, potentialDefinition)] = result; + if (!potentialSets.TryGetValue((condition, potentialDefinition), out SSAPotentialDefinition result)) + { + result = new SSAPotentialDefinition(potentialDefinition, condition); + potentialSets[(condition, potentialDefinition)] = result; + } + if (writeReplaceChain) + { + result.Replaces.Add(potentialDefinition); + potentialDefinition.ReplacedBy.Add(result); + } return result; } - public override SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt) + public override SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt, bool writeReplaceChain) { if (State == SetState.Unset) return this; - if (potentialUnsetSites.TryGetValue(potentiallyUnsetAt, out SSADefinition result)) - return result; - - result = new SSAPotentialDefinition(this, potentiallyUnsetAt); - potentialUnsetSites[potentiallyUnsetAt] = result; + if (!potentialUnsetSites.TryGetValue(potentiallyUnsetAt, out SSADefinition result)) + { + result = new SSAPotentialDefinition(this, potentiallyUnsetAt); + potentialUnsetSites[potentiallyUnsetAt] = result; + } + if (writeReplaceChain) + { + result.Replaces.Add(this); + ReplacedBy.Add(result); + } return result; } @@ -415,6 +462,21 @@ public override InterimConstantValue Evaluate() else return Preceding.Evaluate(); } + + public override bool ValuesEqual(SSADefinition other) + { + if (IsInvariant) + { + if (Conditional.IsExecutable) + return Succeeding.Equals(other); + else + return Preceding.Equals(other); + } + return other is SSAPotentialDefinition potentialDefinition && + potentialDefinition.Conditional == Conditional && + potentialDefinition.Succeeding.Equals(Succeeding) && + potentialDefinition.Preceding.Equals(Preceding); + } } public class PhiVariable : SSADefinition { @@ -450,25 +512,46 @@ public PhiVariable(PhiVariable phiVariable, IRUnset potentiallyUnsetAt) : base(p internalSetState = SetState.PotentiallyUnset; } - public override SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt) + public override SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt, bool writeReplaceChain) { - if (potentialUnsetSites.TryGetValue(potentiallyUnsetAt, out SSADefinition result)) - return result; - - result = new PhiVariable(this, potentiallyUnsetAt); - potentialUnsetSites[potentiallyUnsetAt] = result; + if (!potentialUnsetSites.TryGetValue(potentiallyUnsetAt, out SSADefinition result)) + { + result = new PhiVariable(this, potentiallyUnsetAt); + potentialUnsetSites[potentiallyUnsetAt] = result; + } + if (writeReplaceChain) + { + result.Replaces.Add(this); + ReplacedBy.Add(result); + } return result; } public override InterimConstantValue Evaluate() => Node.Evaluate(); + + public override bool ValuesEqual(SSADefinition other) + { + if (IsInvariant) + { + return Node.PossibleValues.FirstOrDefault(kvp => kvp.Key.IsExecutable).Value?.Equals(other) ?? false; + } + if (other is PhiVariable otherPhi) + { + Dictionary possibleValues = Node.PossibleValues; + Dictionary otherPossibleValues = otherPhi.Node.PossibleValues; + return possibleValues.Count == otherPossibleValues.Count && + possibleValues.Keys.All( + key => + otherPossibleValues.ContainsKey(key) && + (otherPossibleValues[key] == possibleValues[key] || + otherPossibleValues[key].Equals(possibleValues[key]))); + } + return false; + } public override string ToString() => $"{Name} #{ssaIndex}"; - public override bool Equals(object obj) - => obj == this; - public override int GetHashCode() - => (ssaIndex, Name).GetHashCode(); } public class PhiNode : PhiNode { diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index b0d103b9a..c62f96058 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -81,7 +81,7 @@ public static void FinalizeSSA(IRCodePart codePart) while (functionQueue.Count > 0) { IRFunction function = functionQueue.Dequeue(); - HashSet externalReads = new HashSet(function.ExternalReads); + HashSet externalReads = new HashSet(function.ExternalReads); foreach (BasicBlock block in function.InitializationCode) ApplyUses(block, codePart, function); foreach (IRFunction.IRFunctionFragment fragment in function.Fragments) @@ -119,7 +119,7 @@ public static void FinalizeSSA(IRCodePart codePart) foreach (IRCall call in instruction.DepthFirst().Where(inst => inst is IRCall).Cast()) { - ProcessCall(call, codePart, variables, block.TriggerPropagationBlacklist, block.TriggerUnsetBlacklist, funcOrTrigger); + ProcessCall(call, codePart, variables, block.TriggerPropagationBlacklist, block.TriggerUnsetBlacklist, funcOrTrigger, false); IRFunction function = codePart.GetFunction(call); if (function != null) funcOrTrigger?.FunctionCalls.Add(codePart.GetFunction(call)); @@ -129,11 +129,11 @@ public static void FinalizeSSA(IRCodePart codePart) switch (instruction) { case IRAssign assignment: - if (ProcessAssignment(assignment, codePart, variables, block.TriggerPropagationBlacklist, block.TriggerUnsetBlacklist)) + if (ProcessAssignment(assignment, codePart, variables, block.TriggerPropagationBlacklist, block.TriggerUnsetBlacklist, false)) funcOrTrigger?.ExternalWrites.Add(assignment.Target.Name); break; case IRUnset unset: - ProcessUnset(unset, variables, funcOrTrigger?.ExternalUnsets); + ProcessUnset(unset, variables, funcOrTrigger?.ExternalUnsets, false); break; case IRUnaryConsumer unaryConsumer: // On encountering a trigger, blacklist any variables that are written in the trigger or body. @@ -142,7 +142,7 @@ public static void FinalizeSSA(IRCodePart codePart) { string pointer = (string)((InterimConstantValue)unaryConsumer.Operand).Value; IRTrigger trigger = codePart.GetTrigger(pointer); - ProcessTrigger(trigger, variables, block.TriggerPropagationBlacklist, block.TriggerUnsetBlacklist); + ProcessTrigger(trigger, variables, block.TriggerPropagationBlacklist, block.TriggerUnsetBlacklist, false); funcOrTrigger?.TriggersCreated.Add(trigger); } break; @@ -152,7 +152,7 @@ public static void FinalizeSSA(IRCodePart codePart) return variables; } - private static bool ProcessAssignment(IRAssign assignment, IRCodePart codePart, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist) + private static bool ProcessAssignment(IRAssign assignment, IRCodePart codePart, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist, bool writeReplaceChain) { IRScope startingScope = assignment.Block.Scope; SSASetDefinition definition = assignment.Target; @@ -160,34 +160,34 @@ private static bool ProcessAssignment(IRAssign assignment, IRCodePart codePart, { case IRAssign.StoreScope.Local: if (writeBlacklist.TryGetValue((definition.Name, startingScope), out IRUnset unset)) - variables[(definition.Name, startingScope)] = definition.PotentiallyUnset(unset); + ReplaceDefinition(variables, (definition.Name, startingScope), definition.PotentiallyUnset(unset, writeReplaceChain), writeReplaceChain); else - variables[(definition.Name, startingScope)] = definition; + ReplaceDefinition(variables, (definition.Name, startingScope), definition, writeReplaceChain); startingScope.Assignments.Add(assignment); // IRFunction.IsGlobal initiates as false, so there's no need to |= false. break; case IRAssign.StoreScope.Global: if (writeBlacklist.TryGetValue((definition.Name, startingScope), out IRUnset globalUnset)) - variables[(definition.Name, startingScope)] = SSAPotentialDefinition.PotentiallySet(definition, globalUnset); + ReplaceDefinition(variables, (definition.Name, startingScope.GetGlobalScope()), SSAPotentialDefinition.PotentiallySet(definition, globalUnset, writeReplaceChain), writeReplaceChain); else - variables[(definition.Name, startingScope.GetGlobalScope())] = definition; + ReplaceDefinition(variables, (definition.Name, startingScope.GetGlobalScope()), definition, writeReplaceChain); startingScope.GetGlobalScope().Assignments.Add(assignment); - SetFunctionToGlobal(assignment, codePart, variables, readBlacklist, writeBlacklist); + SetFunctionToGlobal(assignment, codePart, variables, readBlacklist, writeBlacklist, writeReplaceChain); break; default: - if (ApplyDefinitionToName(definition, startingScope, variables)) + if (ApplyDefinitionToName(definition, startingScope, variables, writeReplaceChain)) { // This isn't necessarily true in the case of nested function definitions. // But true function definitions are definitively set as local or global. // This only applies to a nested lock, which deserves to lose out on optimizations. - SetFunctionToGlobal(assignment, codePart, variables, readBlacklist, writeBlacklist); + SetFunctionToGlobal(assignment, codePart, variables, readBlacklist, writeBlacklist, writeReplaceChain); return true; } break; } return false; } - private static bool ApplyDefinitionToName(SSASetDefinition definition, IRScope scope, Dictionary<(string VariableName, IRScope Scope), SSADefinition> variables) + private static bool ApplyDefinitionToName(SSASetDefinition definition, IRScope scope, Dictionary<(string VariableName, IRScope Scope), SSADefinition> variables, bool writeReplaceChain) { string name = definition.Name; bool potential = false; @@ -203,9 +203,9 @@ private static bool ApplyDefinitionToName(SSASetDefinition definition, IRScope s if (scope.IsGlobalScope) { if (potential) - variables[(name, scope)] = SSAPotentialDefinition.PotentiallySet(definition, (IRUnset)lastPotential.AssignedAt); + ReplaceDefinition(variables, (name, scope), SSAPotentialDefinition.PotentiallySet(definition, (IRUnset)lastPotential.AssignedAt, writeReplaceChain), writeReplaceChain); else - variables[(name, scope)] = definition; + ReplaceDefinition(variables, (name, scope), definition, writeReplaceChain); scope.Assignments.Add(definition.DefinedAt); // Since the SSA algorithm for a function won't know // of any slots filled between the function's top and the @@ -223,9 +223,9 @@ private static bool ApplyDefinitionToName(SSASetDefinition definition, IRScope s if (slotValue.State == SSADefinition.SetState.PotentiallyUnset) { if (potential) - variables[(name, scope)] = slotValue.PotentiallyOverwrite(definition); + ReplaceDefinition(variables, (name, scope), slotValue.PotentiallyOverwrite(definition, writeReplaceChain), writeReplaceChain); else - variables[(name, scope)] = definition.PotentiallyUnset((IRUnset)slotValue.AssignedAt); + ReplaceDefinition(variables, (name, scope), definition.PotentiallyUnset((IRUnset)slotValue.AssignedAt, writeReplaceChain), writeReplaceChain); scope.Assignments.Add(definition.DefinedAt); potential = true; lastPotential = slotValue; @@ -234,18 +234,29 @@ private static bool ApplyDefinitionToName(SSASetDefinition definition, IRScope s { // If the higher value was only potentially set, anything further becomes potentially overwritten. if (potential) - variables[(name, scope)] = slotValue.PotentiallyOverwrite(definition); + ReplaceDefinition(variables, (name, scope), slotValue.PotentiallyOverwrite(definition, writeReplaceChain), writeReplaceChain); else - variables[(name, scope)] = definition; + ReplaceDefinition(variables, (name, scope), definition, writeReplaceChain); scope.Assignments.Add(definition.DefinedAt); break; } scope = scope.ParentScope; } - return false; + return scope.IsGlobalScope; + } + private static void ReplaceDefinition(Dictionary<(string, IRScope), SSADefinition> variables, (string, IRScope) key, SSADefinition replacement, bool writeReplaceChain) + { + if (writeReplaceChain && + variables.TryGetValue(key, out SSADefinition original)) + { + original.ReplacedBy.Add(replacement); + replacement.Replaces.Add(original); + } + variables[key] = replacement; } + private static void SetFunctionToGlobal(IRAssign assignment, IRCodePart codePart, Dictionary<(string VariableName, IRScope Scope), - SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist) + SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist, bool writeReplaceChain) { if (codePart == null || !(assignment.Value is IRRelocateLater funcOrTrigger)) @@ -257,28 +268,28 @@ private static void SetFunctionToGlobal(IRAssign assignment, IRCodePart codePart // A global function could be added to a trigger in external code. // Treat it as a trigger body itself. - ProcessTrigger(function, variables, readBlacklist, writeBlacklist); + ProcessTrigger(function, variables, readBlacklist, writeBlacklist, writeReplaceChain); } - private static void ProcessUnset(IRUnset unset, Dictionary<(string Name, IRScope), SSADefinition> variables, HashSet<(string, IRUnset)> recordTo) + private static void ProcessUnset(IRUnset unset, Dictionary<(string Name, IRScope), SSADefinition> variables, HashSet<(string, IRUnset)> recordTo, bool writeReplaceChain) { IRScope startingScope = unset.Block.Scope; // On encountering an unset operation remove the affected definition from the stored variables. if (unset.IsInvariant) { - if (Unset(unset, startingScope, variables)) + if (Unset(unset, startingScope, variables, writeReplaceChain)) recordTo?.Add((unset.Target.Name, unset)); } else // If the affected definition cannot be determined, potentially unset all reachable variables. { foreach (string varName in variables.Keys.Select(d => d.Name).Distinct()) { - if (PotentiallyUnsetByName(unset, varName, startingScope, variables)) + if (PotentiallyUnsetByName(unset, varName, startingScope, variables, writeReplaceChain)) recordTo?.Add((varName, unset)); } } } - private static bool Unset(IRUnset unset, IRScope scope, Dictionary<(string VariableName, IRScope Scope), SSADefinition> variables) + private static bool Unset(IRUnset unset, IRScope scope, Dictionary<(string VariableName, IRScope Scope), SSADefinition> variables, bool writeReplaceChain) { bool closureVariableAffected = false; // Target is an instance of SSASetDefinition that is definitively Unset. @@ -288,16 +299,20 @@ private static bool Unset(IRUnset unset, IRScope scope, Dictionary<(string Varia while (scope != null) { if (scope.IsGlobalScope) + { closureVariableAffected = true; + if (potential) + target.Replaces.Add(null); + } if (variables.TryGetValue((varName, scope), out SSADefinition slotValue) && slotValue.State != SSADefinition.SetState.Unset) { // If the higher scope slot was potentially unset, this one may remain valid // Mark it as potentially unset as well. if (potential) - variables[(varName, scope)] = slotValue.PotentiallyUnset(unset); + ReplaceDefinition(variables, (varName, scope), slotValue.PotentiallyUnset(unset, writeReplaceChain), writeReplaceChain); else - variables[(varName, scope)] = target; + ReplaceDefinition(variables, (varName, scope), target, writeReplaceChain); // If this slot was potentially unset, the next higher scope slot // could be the target of this unset. @@ -311,7 +326,7 @@ private static bool Unset(IRUnset unset, IRScope scope, Dictionary<(string Varia } return closureVariableAffected; } - private static bool PotentiallyUnsetByName(IRUnset unset, string varName, IRScope scope, Dictionary<(string VariableName, IRScope Scope), SSADefinition> variables) + private static bool PotentiallyUnsetByName(IRUnset unset, string varName, IRScope scope, Dictionary<(string VariableName, IRScope Scope), SSADefinition> variables, bool writeReplaceChain) { bool closureVariableAffected = false; if (string.IsNullOrEmpty(varName)) @@ -323,7 +338,7 @@ private static bool PotentiallyUnsetByName(IRUnset unset, string varName, IRScop if (variables.TryGetValue((varName, scope), out SSADefinition slotValue) && slotValue.State != SSADefinition.SetState.Unset) { - variables[(varName, scope)] = slotValue.PotentiallyUnset(unset); + ReplaceDefinition(variables, (varName, scope), slotValue.PotentiallyUnset(unset, writeReplaceChain), writeReplaceChain); // Break when there is a slot that is definitively Set. if (slotValue.State == SSADefinition.SetState.Set) break; @@ -333,7 +348,7 @@ private static bool PotentiallyUnsetByName(IRUnset unset, string varName, IRScop return closureVariableAffected; } - private static void ProcessTrigger(IClosureVariableUser trigger, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist) + private static void ProcessTrigger(IClosureVariableUser trigger, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist, bool writeReplaceChain) { foreach ((string varName, IRUnset unset) in trigger.ExternalUnsets) { @@ -344,7 +359,7 @@ private static void ProcessTrigger(IClosureVariableUser trigger, Dictionary<(str if (variables.TryGetValue((varName, scope), out SSADefinition ssaDef) && ssaDef.State != SSADefinition.SetState.Unset) { - variables[(varName, scope)] = ssaDef.PotentiallyUnset(unset); + ReplaceDefinition(variables, (varName, scope), ssaDef.PotentiallyUnset(unset, writeReplaceChain), writeReplaceChain); } scope = scope.ParentScope; } @@ -360,12 +375,12 @@ private static void ProcessTrigger(IClosureVariableUser trigger, Dictionary<(str } } - private static void ProcessCall(IRCall call, IRCodePart codePart, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist, IClosureVariableUser callingClosure) + private static void ProcessCall(IRCall call, IRCodePart codePart, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist, IClosureVariableUser callingClosure, bool writeReplaceChain) { IRFunction function = codePart.GetFunction(call); if (function != null) { - HandleFunction(call, function, variables, readBlacklist, writeBlacklist, callingClosure); + HandleFunction(call, function, variables, readBlacklist, writeBlacklist, callingClosure, writeReplaceChain); } else if (!call.Direct && !(call.IndirectMethod is IRSuffixGetMethod)) { @@ -376,11 +391,11 @@ private static void ProcessCall(IRCall call, IRCodePart codePart, Dictionary<(st // function in this code part. foreach (IRFunction func in codePart.Functions) { - HandleFunction(call, func, variables, readBlacklist, writeBlacklist, callingClosure); + HandleFunction(call, func, variables, readBlacklist, writeBlacklist, callingClosure, writeReplaceChain); } } } - static void HandleFunction(IRCall call, IRFunction function, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist, IClosureVariableUser callingClosure) + static void HandleFunction(IRCall call, IRFunction function, Dictionary<(string, IRScope), SSADefinition> variables, HashSet<(string, IRScope)> readBlacklist, Dictionary<(string, IRScope), IRUnset> writeBlacklist, IClosureVariableUser callingClosure, bool writeReplaceChain) { HashSet externalWrites = callingClosure?.ExternalWrites; HashSet<(string, IRUnset)> externalUnsets = callingClosure?.ExternalUnsets; @@ -402,7 +417,7 @@ static void HandleFunction(IRCall call, IRFunction function, Dictionary<(string, if (variables.TryGetValue((varName, scope), out SSADefinition ssaDef) && ssaDef.State != SSADefinition.SetState.Unset) { - variables[(varName, scope)] = ssaDef.PotentiallyUnset(unset); + ReplaceDefinition(variables, (varName, scope), ssaDef.PotentiallyUnset(unset, writeReplaceChain), writeReplaceChain); // A recursive function could unset the same variable multiple times. // So this goes all the way to the top. @@ -424,7 +439,7 @@ static void HandleFunction(IRCall call, IRFunction function, Dictionary<(string, if (variables.TryGetValue((varName, scope), out SSADefinition ssaDef) && ssaDef.State != SSADefinition.SetState.Unset) { - variables[(varName, scope)] = ssaDef.PotentiallyOverwrite(SSASetDefinition.FromCallSite(varName, call)); + ReplaceDefinition(variables, (varName, scope), ssaDef.PotentiallyOverwrite(SSASetDefinition.FromCallSite(varName, call), writeReplaceChain), writeReplaceChain); if (ssaDef.State == SSADefinition.SetState.Set) break; @@ -436,7 +451,7 @@ static void HandleFunction(IRCall call, IRFunction function, Dictionary<(string, // This is handled as normal for a trigger. foreach (IRTrigger trigger in function.TriggersCreated) { - ProcessTrigger(trigger, variables, readBlacklist, writeBlacklist); + ProcessTrigger(trigger, variables, readBlacklist, writeBlacklist, writeReplaceChain); } } @@ -486,14 +501,27 @@ private static void BuildPhis(BasicBlock root, IRCodePart codePart, IClosureVari block.Phis.Add(definitionSet.Key, phiVar); } - foreach ((BasicBlock Block, IRScope _, SSADefinition Variable) in definitionSet) - phiVar.PossibleValues[Block] = Variable; + foreach ((BasicBlock incomingBlock, _, SSADefinition definition) in definitionSet) + { + phiVar.PossibleValues[incomingBlock] = definition; + definition.ReplacedBy.Add(phiVar.Result); + phiVar.Result.Replaces.Add(definition); + } variablesIn.Add(definitionSet.Key, phiVar.Result); } else { - block.Phis.Remove(definitionSet.Key); + if (block.Phis.ContainsKey(definitionSet.Key)) + { + PhiNode phiVar = block.Phis[definitionSet.Key]; + foreach ((_, _, SSADefinition definition) in definitionSet) + { + definition.ReplacedBy.Remove(phiVar.Result); + phiVar.Result.Replaces.Remove(definition); + } + block.Phis.Remove(definitionSet.Key); + } variablesIn.Add(definitionSet.Key, definitionSet.First().Variable); } } @@ -512,7 +540,7 @@ private static void BuildPhis(BasicBlock root, IRCodePart codePart, IClosureVari { Dictionary<(string, IRScope), SSADefinition> oldDefinition = variablesOut[block]; if (oldDefinition.Count == varsOut.Count && - varsOut.All(kvp => oldDefinition[kvp.Key].Equals(kvp.Value))) + varsOut.All(kvp => oldDefinition.ContainsKey(kvp.Key) && oldDefinition[kvp.Key].Equals((object)kvp.Value))) continue; } @@ -556,12 +584,12 @@ IInterimOperand ScopedSSAReplacement(IInterimOperand op) { if (inst is IRCall call) { - ProcessCall(call, codePart, liveDefinitions, triggerBlacklist, triggerWriteBlacklist, null); IRFunction function = codePart.GetFunction(call); if (function != null) { ReachableVariables[call] = DetermineCallReaches(call, function, funcOrTrigger, liveDefinitions, triggerBlacklist); } + ProcessCall(call, codePart, liveDefinitions, triggerBlacklist, triggerWriteBlacklist, null, true); } if (inst is IOperandInstructionBase operandInstruction) operandInstruction.MutateEachOperand(ScopedSSAReplacement); @@ -571,17 +599,17 @@ IInterimOperand ScopedSSAReplacement(IInterimOperand op) switch (instruction) { case IRAssign assignment: - ProcessAssignment(assignment, null, liveDefinitions, triggerBlacklist, triggerWriteBlacklist); + ProcessAssignment(assignment, null, liveDefinitions, triggerBlacklist, triggerWriteBlacklist, true); break; case IRUnset unset: - ProcessUnset(unset, liveDefinitions, null); + ProcessUnset(unset, liveDefinitions, null, true); break; case IRUnaryConsumer irTrigger: if (irTrigger.Operation is OpcodeAddTrigger) { string pointer = (string)((InterimConstantValue)irTrigger.Operand).Value; IRTrigger trigger = codePart.GetTrigger(pointer); - ProcessTrigger(trigger, liveDefinitions, triggerBlacklist, triggerWriteBlacklist); + ProcessTrigger(trigger, liveDefinitions, triggerBlacklist, triggerWriteBlacklist, true); } break; } @@ -598,7 +626,7 @@ private static IInterimOperand SSAReplacement(IInterimOperand operand, IRScope s liveDefinitions, triggerBlacklist, out bool exceededClosure); if (exceededClosure) - funcOrTrigger?.ExternalReads.Add(result); + funcOrTrigger?.ExternalReads.Add(result.Name); return result; } else if (operand is IRUnaryOp existOp && existOp.Operation is OpcodeExists) @@ -620,7 +648,7 @@ private static IInterimOperand SSAReplacement(IInterimOperand operand, IRScope s return new InterimConstantValue(Encapsulation.BooleanValue.True, existOp); scope = scope.ParentScope; } - funcOrTrigger?.ExternalReads.Add(new InterimVariableReference(name, existOp)); + funcOrTrigger?.ExternalReads.Add(name); } } return operand; @@ -672,13 +700,25 @@ private static HashSet DetermineCallReaches(IRCall ca { HashSet reachableVariables = new HashSet(); - foreach (string name in function.ExternalReads.Select(v => v.Name).Union(function.ExternalWrites)) + foreach (string name in function.ExternalReads. + Union(function.ExternalWrites). + Union(function.ExternalUnsets.Select(unset => unset.Name))) { IInterimVariableReference result = AttemptResolveReference( new InterimVariableReference(name, call), function.ClosureScope, liveDefinitions, triggerBlacklist, out bool exceededClosure); - if (exceededClosure) - funcOrTrigger?.ExternalReads.Add(result); + if (exceededClosure && funcOrTrigger != null) + { + if (function.ExternalReads.Contains(name)) + funcOrTrigger.ExternalReads.Add(result.Name); + if (function.ExternalWrites.Contains(name)) + funcOrTrigger.ExternalWrites.Add(result.Name); + foreach ((string, IRUnset) externalUnset in function.ExternalUnsets. + Where(scopeSlot => scopeSlot.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + funcOrTrigger.ExternalUnsets.Add(externalUnset); + } + } if (!(result is InterimVariableReference)) reachableVariables.Add(result); } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index 0e8d2b93e..40a9ccd53 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -26,8 +26,13 @@ public void ApplyPass(IRCodePart codePart) Dictionary> ssaUses = MapUsesAndPropagateTypes(codePart); - if (Optimizer.OptimizationLevel > OptimizationLevel.None) - PropagateConstants(ssaUses); + if (Optimizer.OptimizationLevel == OptimizationLevel.None) + return; + + HashSet usedDefinitions = PropagateConstants(ssaUses); + + foreach (BasicBlock block in codePart.Blocks) + RemoveRedundantAssignments(block, usedDefinitions); } /// @@ -88,16 +93,20 @@ private static Dictionary> MapUs { inst.ForEachOperand(op => { - if (op is SSADefinition ssaVariable) + if (op is IInterimVariableReference reference && + !(op is InterimVariableReference)) { - // Add this instruction to the list of uses for each operand. - GetOrCreate(variableUses, ssaVariable).Add(inst); - - // Also add the base instruction, - // which is the more important reference - // since anything else is a temp result. - if (instruction is IOperandInstructionBase opInst) - variableUses[ssaVariable].Add(opInst); + foreach (SSADefinition variable in GetSSADefinitionsFromReferences(reference)) + { + // Add this instruction to the list of uses for each operand. + GetOrCreate(variableUses, variable).Add(inst); + + // Also add the base instruction, + // which is the more important reference + // since anything else is a temp result. + if (instruction is IOperandInstructionBase opInst) + GetOrCreate(variableUses, variable).Add(opInst); + } } }); // Calls get to be special to address their external read needs. @@ -247,11 +256,8 @@ private static bool VisitInstruction(IOperandInstructionBase instruction, /// /// Propagates any constant SSA variables to their uses. /// - private static void PropagateConstants(Dictionary> ssaUses) + private static HashSet PropagateConstants(Dictionary> ssaUses) { - // TODO: Keep track of which uses are replaced and if there are no more uses, remove the assignment. - // But watch for functions that external read that variable and make sure to retain the assignment before then. - // TODO: Look at branch instructions that employ eq and propagate that constant. List constantVariables = ssaUses.Keys.Where( v => v.State == SSADefinition.SetState.Set && v.IsInvariant && @@ -260,50 +266,150 @@ private static void PropagateConstants(Dictionary.Create((a, b) => string.Compare(a.ToString(), b.ToString()))); + List usedVariables = ssaUses.Keys.ToList(); + usedVariables.Sort(Comparer.Create((a, b) => string.Compare(a.ToString(), b.ToString()))); #endif - Queue queue = new Queue(constantVariables); + HashSet requiredDefinitions = new HashSet(constantVariables.Where(def => + ssaUses[def].Any(use => + { + if (use is IRCall call) + { + var function = call.Block.CodePart.GetFunction(call); + if (function == null) + return false; + if (function.ExternalReads.Any(var => var.Equals(def.Name, StringComparison.OrdinalIgnoreCase))) + return true; + if (function.ExternalWrites.Any(var => var.Equals(def.Name, StringComparison.OrdinalIgnoreCase))) + return true; + if (function.ExternalUnsets.Any(unset => unset.Name.Equals(def.Name, StringComparison.OrdinalIgnoreCase))) + return true; + } + return false; + }))); + Dictionary replacements = new Dictionary(); - while (queue.Count > 0) + foreach (SSADefinition ssaDef in constantVariables) { - SSADefinition variable = queue.Dequeue(); - if (queue.Any(v => - { - if (variable is PhiVariable phi && !replacements.ContainsKey(phi)) - return true; - if (variable is SSASetDefinition setDef && ssaUses[v].Contains(setDef.DefinedAt)) - return true; - return false; - })) + replacements[ssaDef] = ssaDef.Evaluate(); + } + + foreach (SSADefinition constantDef in constantVariables) + { + IInterimOperand Propagate(IInterimOperand operand) + => PropagateConstant(operand, constantDef, replacements[constantDef]); + foreach (IOperandInstructionBase instruction in ssaUses[constantDef]) { - queue.Enqueue(variable); - continue; + if (instruction is IRUnaryOp unaryOp && + unaryOp.Operation is OpcodeExists) + continue; + if (instruction is IRUnset) + continue; + instruction.MutateEachOperand(Propagate); } + } + requiredDefinitions.UnionWith(ssaUses.Keys.Except(constantVariables)); - InterimConstantValue replacement; - if (variable is PhiVariable) - replacement = replacements[variable]; - else - replacement = variable.Evaluate(); + return requiredDefinitions; + } - if (replacement != null) + private static void RemoveRedundantAssignments(BasicBlock block, HashSet usedVariables) + { + for (int i = 0; i < block.Instructions.Count; i++) + { + if (block.Instructions[i] is IRAssign assignment && + AssignmentMayBeEliminated(assignment, usedVariables)) { - foreach (IOperandInstructionBase instruction in ssaUses[variable]) - { - if (instruction is PhiVariable phi) - { - if (constantVariables.Contains(phi)) - replacements[phi] = replacement; - continue; - } - if (instruction is IRUnaryOp unaryOp && - unaryOp.Operation is OpcodeExists) - continue; - instruction.MutateEachOperand(op => op.Equals(variable) ? replacement : op); - } + RemoveAssignment(assignment); + i--; } } } + + private static bool AssignmentMayBeEliminated(IRAssign assignment, HashSet usedVariables) + { + // RelocateLater may not be eliminated since that is how functions are stored. + if (assignment.Value is IRRelocateLater) + return false; + // Global assignments cannot be eliminated. + if (assignment.Block.Scope.GetGlobalScope().Assignments.Contains(assignment)) + return false; + if (DefinitionIsProtected(assignment.Target, usedVariables)) + return false; + return true; + } + private static bool DefinitionIsProtected(SSADefinition definition, HashSet usedVariables) + { + // Must not remove definitions that are used. + if (usedVariables.Contains(definition)) + return true; + // Must not remove assignments that are later unset, if those unsets cannot also be removed. + // Unsets can only be removed if they may unset anything besides this one. + if (definition.ReplacedBy.Any(ssaDef => ssaDef.State == SSADefinition.SetState.Unset && ssaDef.Replaces.Count > 1)) + return true; + // Must not remove assignments whose lifespan is not invariant. + if (definition.ReplacedBy.Any(ssaDef => ssaDef.State == SSADefinition.SetState.PotentiallyUnset)) + return true; + // Must not remove assignments that feed into a phi if there are multiple possible incoming values. + foreach (PhiVariable phi in definition.ReplacedBy.Where(ssaDef => ssaDef is PhiVariable).Cast()) + { + // Ignore restrictions on phis if this definition's block is not executable. + if (!phi.Node.PossibleValues.First(kvp => kvp.Value == definition).Key.IsExecutable) + continue; + // If the phi variable is protected, its incoming definitions must be too. + if (DefinitionIsProtected(phi, usedVariables)) + return true; + // If the phi has multiple possible incoming values (from executable blocks), + // they must all be preserved. + if (!phi.Node.PossibleValues.Where(kvp => kvp.Key.IsExecutable).All(kvp => kvp.Value.Equals(definition))) + return true; + } + // If none of the above apply, it is safe to delete this definition. + return false; + } + + private static void RemoveAssignment(IRAssign assignment) + { + // Remove this assignment instruction + assignment.Block.Instructions.Remove(assignment); + + // Remove this assignment from all scopes. + IRScope scope = assignment.Block.Scope; + while (scope != null) + { + scope.Assignments.Remove(assignment); + scope = scope.ParentScope; + } + + SSASetDefinition definition = assignment.Target; + // Remove subsequent unsets + // We've already assured no inadvertent side effects of this in DefinitionIsProtected() + foreach (IRUnset unset in definition.ReplacedBy. + Where(ssaDef => ssaDef.State == SSADefinition.SetState.Unset). + Select(ssaDef => ssaDef.AssignedAt).Cast()) + { + unset.Block.Instructions.Remove(unset); + } + + // Convert subsequent assignments to be declarative + foreach (IRAssign nextAssign in definition.ReplacedBy. + Where(ssaDef => ssaDef.State == SSADefinition.SetState.Set). + Cast().Select(ssaDef => ssaDef.DefinedAt)) + { + nextAssign.Scope = IRAssign.StoreScope.Local; + nextAssign.AssertExists = false; + } + } + + private static IInterimOperand PropagateConstant(IInterimOperand operand, SSADefinition definition, InterimConstantValue constant) + { + if (operand is InterimResolvedReference resolvedReference && + resolvedReference.Reference.Equals((object)definition)) + { + return constant; + } + return operand; + } } } From af1a9efbbed3cc1961896248613da8e7196a8619 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 27 May 2026 20:29:01 -0400 Subject: [PATCH 088/120] Fix phi variables not getting updated as blocks become executable. --- .../Compilation/IR/InterimVariables.cs | 26 +++++++++++++++---- .../Passes/SCCPWithTypePropagation.cs | 7 ++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs index 23ace33a7..abab12d86 100644 --- a/src/kOS.Safe/Compilation/IR/InterimVariables.cs +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -538,14 +538,17 @@ public override bool ValuesEqual(SSADefinition other) } if (other is PhiVariable otherPhi) { - Dictionary possibleValues = Node.PossibleValues; + IEnumerable possibleValues = Node.PossibleValues.Keys; Dictionary otherPossibleValues = otherPhi.Node.PossibleValues; - return possibleValues.Count == otherPossibleValues.Count && - possibleValues.Keys.All( + possibleValues = possibleValues.Where(key => key.IsExecutable); + if (possibleValues.Count() != otherPossibleValues.Where(kvp => kvp.Key.IsExecutable).Count()) + return false; + + return possibleValues.All( key => otherPossibleValues.ContainsKey(key) && - (otherPossibleValues[key] == possibleValues[key] || - otherPossibleValues[key].Equals(possibleValues[key]))); + (otherPossibleValues[key] == Node.PossibleValues[key] || + otherPossibleValues[key].Equals(Node.PossibleValues[key]))); } return false; } @@ -553,6 +556,19 @@ public override bool ValuesEqual(SSADefinition other) public override string ToString() => $"{Name} #{ssaIndex}"; } + + public class SSAReferenceEqualityComparer : IEqualityComparer + { + public static SSAReferenceEqualityComparer Instance = + new SSAReferenceEqualityComparer(); + + public bool Equals(SSADefinition x, SSADefinition y) + => x == y; + + public int GetHashCode(SSADefinition obj) + => obj.GetHashCode(); + } + public class PhiNode : PhiNode { protected override bool ObjIsInvariant(SSADefinition obj) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index 40a9ccd53..da26eeb62 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -50,7 +50,7 @@ public void ApplyPass(IRCodePart codePart) private static Dictionary> MapUsesAndPropagateTypes(IRCodePart codePart) { Dictionary> variableUses = - new Dictionary>(); + new Dictionary>(SSAReferenceEqualityComparer.Instance); HashSet visitedBlocks = new HashSet(); Dictionary typeAndInvarianceCache = new Dictionary(); @@ -156,6 +156,11 @@ operandInstruction is IRAssign assignment && blockQueue.Enqueue(block.Successors.First()); block.IsExecutable = true; + + // With the block marked executable, all the phis based on it may have changed. + // Iterate through them and add those uses to the queue. + foreach (IOperandInstructionBase use in variableUses.Where(kvp => kvp.Key is PhiVariable p && p.Node.PossibleValues.ContainsKey(block)).SelectMany(kvp => kvp.Value)) + instructionQueue.Enqueue(use); } // Loop over any instructions (or phis) that need updating. From e558ca738ce72ed6265ca2b5cc3bc896dafcb1af Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 27 May 2026 20:31:03 -0400 Subject: [PATCH 089/120] Replace the DeadCodeElimination pass with a more complete Basic Block Ordering pass that minimizes unconditional jumps. --- .../integration/branching/for.ks | 7 + .../integration/branching/from.ks | 10 + kerboscript_tests/integration/branching/if.ks | 7 + .../integration/branching/ifElse.ks | 10 + .../integration/branching/ifElseIf.ks | 10 + .../integration/branching/ifElseIfElse.ks | 13 + .../integration/branching/until.ks | 7 + .../Execution/OptimizationTest.cs | 118 +++++++ src/kOS.Safe/Compilation/IR/BasicBlock.cs | 127 +++++-- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 13 + .../Optimization/OptimizationLevel.cs | 2 +- .../Optimization/Passes/BlockOrdering.cs | 334 ++++++++++++++++++ .../Passes/DeadCodeElimination.cs | 45 --- 13 files changed, 626 insertions(+), 77 deletions(-) create mode 100644 kerboscript_tests/integration/branching/for.ks create mode 100644 kerboscript_tests/integration/branching/from.ks create mode 100644 kerboscript_tests/integration/branching/if.ks create mode 100644 kerboscript_tests/integration/branching/ifElse.ks create mode 100644 kerboscript_tests/integration/branching/ifElseIf.ks create mode 100644 kerboscript_tests/integration/branching/ifElseIfElse.ks create mode 100644 kerboscript_tests/integration/branching/until.ks create mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs delete mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs diff --git a/kerboscript_tests/integration/branching/for.ks b/kerboscript_tests/integration/branching/for.ks new file mode 100644 index 000000000..6d27bf6fc --- /dev/null +++ b/kerboscript_tests/integration/branching/for.ks @@ -0,0 +1,7 @@ +@lazyGlobal OFF. + +print("beginning"). +for item in a { + print("body"). +} +print("trunk"). \ No newline at end of file diff --git a/kerboscript_tests/integration/branching/from.ks b/kerboscript_tests/integration/branching/from.ks new file mode 100644 index 000000000..5d7347f14 --- /dev/null +++ b/kerboscript_tests/integration/branching/from.ks @@ -0,0 +1,10 @@ +@lazyGlobal OFF. + +print("beginning"). +from {local i is 0.} +until i = 10 +step {set i to i + 1.} +do { + print("body"). +} +print("trunk"). \ No newline at end of file diff --git a/kerboscript_tests/integration/branching/if.ks b/kerboscript_tests/integration/branching/if.ks new file mode 100644 index 000000000..ede6afae4 --- /dev/null +++ b/kerboscript_tests/integration/branching/if.ks @@ -0,0 +1,7 @@ +@lazyGlobal OFF. + +print("beginning"). +if (a) { + print("branch"). +} +print("trunk"). \ No newline at end of file diff --git a/kerboscript_tests/integration/branching/ifElse.ks b/kerboscript_tests/integration/branching/ifElse.ks new file mode 100644 index 000000000..eeece4e3d --- /dev/null +++ b/kerboscript_tests/integration/branching/ifElse.ks @@ -0,0 +1,10 @@ +@lazyGlobal OFF. + +print("beginning"). +if (a) { + print("branch"). +} +else { + print("else"). +} +print("trunk"). \ No newline at end of file diff --git a/kerboscript_tests/integration/branching/ifElseIf.ks b/kerboscript_tests/integration/branching/ifElseIf.ks new file mode 100644 index 000000000..7c9390ba9 --- /dev/null +++ b/kerboscript_tests/integration/branching/ifElseIf.ks @@ -0,0 +1,10 @@ +@lazyGlobal OFF. + +print("beginning"). +if (a) { + print("branch"). +} +else if (b) { + print("elseIf"). +} +print("trunk"). \ No newline at end of file diff --git a/kerboscript_tests/integration/branching/ifElseIfElse.ks b/kerboscript_tests/integration/branching/ifElseIfElse.ks new file mode 100644 index 000000000..0b84e8c60 --- /dev/null +++ b/kerboscript_tests/integration/branching/ifElseIfElse.ks @@ -0,0 +1,13 @@ +@lazyGlobal OFF. + +print("beginning"). +if (a) { + print("branch"). +} +else if (b) { + print("elseIf"). +} +else{ + print("else"). +} +print("trunk"). \ No newline at end of file diff --git a/kerboscript_tests/integration/branching/until.ks b/kerboscript_tests/integration/branching/until.ks new file mode 100644 index 000000000..59fde1858 --- /dev/null +++ b/kerboscript_tests/integration/branching/until.ks @@ -0,0 +1,7 @@ +@lazyGlobal OFF. + +print("beginning"). +until (a) { + print("body"). +} +print("trunk"). \ No newline at end of file diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index fd7579966..1638505d8 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -4,6 +4,7 @@ using kOS.Safe.Compilation; using kOS.Safe.Compilation.IR; using kOS.Safe.Exceptions; +using System.Linq; namespace kOS.Safe.Test.Execution { @@ -823,5 +824,122 @@ public void TestPowerCreation() Assert.AreEqual(Encapsulation.ScalarIntValue.One, (result[16] as OpcodePush)?.Argument); } #endregion + + #region Block Ordering + [Test] + public void TestUntilLoopDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/until.ks"); + optimizationLevel = OptimizationLevel.Minimal; + IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(BlockSequence(codePart.MainCode[1]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.Loop loopData); + Assert.AreEqual(2, loopData.body?.ID); + Assert.AreEqual(4, loopData.exit?.ID); + Assert.AreEqual(1, codePart.MainCode. + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(BlockSequence(b), null, out _)).Count()); + var optimizer = new Safe.Compilation.Optimization.Optimizer(new CompilerOptions() { OptimizationLevel = optimizationLevel }); + optimizer.Optimize(codePart); + } + [Test] + public void TestForLoopDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/for.ks"); + optimizationLevel = OptimizationLevel.Minimal; + IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(BlockSequence(codePart.MainCode[2]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.Loop loopData); + Assert.AreEqual(3, loopData.body?.ID); + Assert.AreEqual(6, loopData.exit?.ID); + Assert.AreEqual(1, codePart.MainCode. + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(BlockSequence(b), null, out _)).Count()); + } + [Test] + public void TestFromLoopDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/from.ks"); + optimizationLevel = OptimizationLevel.Minimal; + IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(BlockSequence(codePart.MainCode[2]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.Loop loopData); + Assert.AreEqual(3, loopData.body?.ID); + Assert.AreEqual(5, loopData.exit?.ID); + Assert.AreEqual(1, codePart.MainCode. + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(BlockSequence(b), null, out _)).Count()); + } + [Test] + public void TestIfDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/if.ks"); + optimizationLevel = OptimizationLevel.Minimal; + IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(codePart.MainCode[0]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.IfElse branchData); + Assert.AreEqual(1, branchData.ifBlock?.ID); + Assert.IsNull(branchData.elseBlock); + Assert.AreEqual(2, branchData.exit?.ID); + Assert.AreEqual(1, codePart.MainCode. + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(b), null, out _)).Count()); + } + [Test] + public void TestIfElseDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/ifElse.ks"); + optimizationLevel = OptimizationLevel.Minimal; + IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(codePart.MainCode[0]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.IfElse branchData); + Assert.AreEqual(1, branchData.ifBlock?.ID); + Assert.AreEqual(3, branchData.elseBlock?.ID); + Assert.AreEqual(4, branchData.exit?.ID); + Assert.AreEqual(1, codePart.MainCode. + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(b), null, out _)).Count()); + } + [Test] + public void TestIfElseIfDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/ifElseIf.ks"); + optimizationLevel = OptimizationLevel.Minimal; + IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(codePart.MainCode[0]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.IfElse branchData); + Assert.AreEqual(1, branchData.ifBlock?.ID); + Assert.AreEqual(3, branchData.elseBlock?.ID); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(branchData.elseBlock), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.IfElse branchData2); + Assert.AreEqual(4, branchData2.ifBlock?.ID); + Assert.IsNull(branchData2.elseBlock); + Assert.AreSame(branchData.exit, branchData2.exit); + Assert.AreEqual(5, branchData.exit?.ID); + Assert.AreEqual(2, codePart.MainCode. + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(b), null, out _)).Count()); + } + [Test] + public void TestIfElseIfElseDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/ifElseIfElse.ks"); + optimizationLevel = OptimizationLevel.Minimal; + IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(codePart.MainCode[0]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.IfElse branchData); + Assert.AreEqual(1, branchData.ifBlock?.ID); + Assert.AreEqual(3, branchData.elseBlock?.ID); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(branchData.elseBlock), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.IfElse branchData2); + Assert.AreEqual(4, branchData2.ifBlock?.ID); + Assert.AreEqual(6, branchData2.elseBlock?.ID); + Assert.AreSame(branchData.exit, branchData2.exit); + Assert.AreEqual(7, branchData.exit?.ID); + Assert.AreEqual(2, codePart.MainCode. + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(b), null, out _)).Count()); + } + private static Safe.Compilation.Optimization.Passes.BlockOrdering.BlockSequence BlockSequence(BasicBlock block) + => new Safe.Compilation.Optimization.Passes.BlockOrdering.BasicBlockSequence(block); + #endregion } } diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index d8c5709e8..c7dd64b69 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using kOS.Safe.Compilation.Optimization; @@ -16,11 +17,16 @@ public class BasicBlock private readonly HashSet successors = new HashSet(); private BasicBlock dominator; private readonly HashSet dominates = new HashSet(); + private BasicBlock postDominator; + private readonly HashSet postDominates = new HashSet(); private readonly List parameters = new List(); private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. private readonly string nonSequentialLabel = null; private IRScope scope; + public static void ResetNextID() + => nextID = 0; + public IRCodePart CodePart { get; } /// /// Gets or sets a value indicating whether this block is executable (reachable). @@ -145,6 +151,26 @@ private set /// public IReadOnlyCollection Dominates => dominates; /// + /// Gets the BasicBlock that post-dominates this block. That is, + /// the earliest successor that is guaranteed to be + /// executed after this block. + /// + public BasicBlock PostDominator + { + get => postDominator; + private set + { + postDominator?.postDominates.Remove(this); + postDominator = value; + postDominator?.postDominates.Add(this); + } + } + /// + /// Gets the collection of BasicBlocks for which this block is + /// the Post-Dominator. + /// + public IReadOnlyCollection PostDominates => postDominates; + /// /// Gets or sets the Extended Basic Block of which this block /// is a member. /// @@ -204,90 +230,122 @@ protected void AddPredecessor(BasicBlock predecessor) /// Removes a successor block. /// /// The successor block to remove. - /// Cannot remove as it is not a successor. + /// Cannot remove as it is not a successor. public void RemoveSuccessor(BasicBlock successor) { + // Break the appropriate links if (!successors.Remove(successor)) - throw new System.ArgumentException($"Cannot remove {successor} as it is not a successor."); + throw new ArgumentException($"Cannot remove {successor} as it is not a successor."); successor.predecessors.Remove(this); - if (successor.predecessors.Count == 0) - successor.Dominator = null; - else - successor.Dominator.EstablishDominance(); + + // Recompute the Dominance tree(s) + successor.Dominator = null; + EstablishDominance(); + + // Recompute the Post-Dominance tree(s) + PostDominator = null; + successor.EstablishPostDominance(); } + private static IEnumerable GetSuccessors(BasicBlock block) + => block.successors; + private static BasicBlock GetDominator(BasicBlock block) + => block.Dominator; + private static void SetDominator(BasicBlock block, BasicBlock dominator) + => block.Dominator = dominator; + /// - /// Establishes the dominance tree. This must be called on the - /// root block, or the root of the branch that needs - /// re-establishment. + /// Establishes the dominance tree. /// public void EstablishDominance() + => EstablishDominanceCore(this, GetPredecessors, GetSuccessors, GetDominator, SetDominator); + + private static IEnumerable GetPredecessors(BasicBlock block) + => block.predecessors; + private static BasicBlock GetPostDominator(BasicBlock block) + => block.PostDominator; + private static void SetPostDominator(BasicBlock block, BasicBlock postDominator) + => block.PostDominator = postDominator; + + /// + /// Establishes the post-dominance tree. + public void EstablishPostDominance() + => EstablishDominanceCore(this, GetSuccessors, GetPredecessors, GetPostDominator, SetPostDominator); + + private static void EstablishDominanceCore(BasicBlock root, Func> getPrecedents, Func> getSubsequents, Func getDominator, Action setDominator) { + while (getDominator(root) != null) + root = getDominator(root); + // Compute reverse postorder - var postorder = new List(); - var visited = new HashSet(); - DepthFirstSearch(this, visited, postorder); - - postorder.Reverse(); + List reversePostOrder = GetReversePostOrder(root, getSubsequents); // Map block to index Dictionary index = new Dictionary(); - for (int i = 0; i < postorder.Count; i++) - index[postorder[i]] = i; + for (int i = 0; i < reversePostOrder.Count; i++) + index[reversePostOrder[i]] = i; // Initialize - postorder.Remove(this); + reversePostOrder.Remove(root); bool changed = true; while (changed) { changed = false; - foreach (BasicBlock block in postorder) + foreach (BasicBlock block in reversePostOrder) { // Pick first predecessor with defined dominator - BasicBlock newIdom = block.predecessors.Where(p => p != block).FirstOrDefault - (p => p == this || p.Dominator != null); + BasicBlock newIdom = getPrecedents(block).Where(p => p != block).FirstOrDefault + (p => p == root || getDominator(p) != null); if (newIdom == null) continue; - foreach (BasicBlock predecessor in block.predecessors) + foreach (BasicBlock predecessor in getPrecedents(block)) { if (predecessor == newIdom) continue; - if (predecessor.Dominator != null) - newIdom = Intersect(predecessor, newIdom, index); + if (getDominator(predecessor) != null) + newIdom = Intersect(predecessor, newIdom, index, getDominator); } - if (block.Dominator != newIdom) + if (getDominator(block) != newIdom) { - block.Dominator = newIdom; + setDominator(block, newIdom); changed = true; } } } } - private static BasicBlock Intersect(BasicBlock b1, BasicBlock b2, Dictionary index) + private static BasicBlock Intersect(BasicBlock b1, BasicBlock b2, Dictionary index, Func getDominator) { while (b1 != b2) { while (index[b1] > index[b2]) - b1 = b1.Dominator; + b1 = getDominator(b1); while (index[b2] > index[b1]) - b2 = b2.Dominator; + b2 = getDominator(b2); } return b1; } - private static void DepthFirstSearch(BasicBlock block, HashSet visited, List postorder) + public static List GetReversePostOrder(BasicBlock root, Func> getEdges) + { + List result = new List(); + HashSet visited = new HashSet(); + DepthFirstSearch(root, visited, result, getEdges); + result.Reverse(); + return result; + } + private static void DepthFirstSearch(BasicBlock block, HashSet visited, List postorder, Func> getEdges) { if (!visited.Add(block)) return; - foreach (BasicBlock successor in block.successors) - DepthFirstSearch(successor, visited, postorder); + foreach (BasicBlock successor in getEdges(block)) + DepthFirstSearch(successor, visited, postorder, getEdges); postorder.Add(block); } @@ -380,4 +438,11 @@ public IEnumerable EmitOpCodes() Instructions.Remove(FallthroughJump); } } + + public sealed class SyntheticReturnBlock : BasicBlock + { + public SyntheticReturnBlock(IRCodePart codePart) : base(codePart, -1, -1, "syntheticReturn") + { + } + } } diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 2c74b689d..00e7874b2 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -101,6 +101,19 @@ private void CreateBlocks(List code, IRCodePart codePart, Dictionary exitBlocks = blocks.Where(b => !b.Successors.Any()).ToList(); + if (exitBlocks.Count == 1) + { + exitBlocks[0].EstablishPostDominance(); + } + else + { + BasicBlock unifiedReturn = new SyntheticReturnBlock(codePart); + foreach (BasicBlock exitBlock in exitBlocks) + exitBlock.AddSuccessor(unifiedReturn); + unifiedReturn.EstablishPostDominance(); + } + AssignScopes(rootBlock, globalScope, scopePushes, scopePops); } diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index 474db720b..83804ad25 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -9,7 +9,6 @@ namespace kOS.Safe.Compilation * Replace ship fields with their alias * 20. Constant propagation * 30. Constant folding - * 50. Dead code elimination * 1050. Peephole optimizations: * Replace lex indexing with string constant with suffixing where possible * Replace parameterless suffix method calls with get member (this feels like cheating...) @@ -19,6 +18,7 @@ namespace kOS.Safe.Compilation * Branch logical simplification (e.g. !X branch = X branch!) * Algebraic simplification (e.g. A*B+A*C = A*(B+C), A+-B=A-B, A--B=A+B, -A+B=B-A, X*X*...*X=N^X) * 32000. Remove unnecessary scope pushes/pops + * 32767. Block ordering to minimize unconditional jumps. * * O2: * 1000. Common expression elimination diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs new file mode 100644 index 000000000..4cca71cbc --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs @@ -0,0 +1,334 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class BlockOrdering : IHolisticOptimizationPass, ILinkedOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.None; + + public short SortIndex => short.MaxValue; + + public Optimizer Optimizer { get; set; } + + public static bool InitialPredicate(BasicBlock _) + => true; + public static bool IsExecutablePredicate(BasicBlock block) + => block.IsExecutable; + + // Optimally arrange blocks, while eliminating non-executable blocks. + public void ApplyPass(IRCodePart codePart) + { + if (Optimizer.OptimizationLevel >= OptimizationLevel.Minimal) + ApplyOrdering(codePart, IsExecutablePredicate); + else + ApplyOrdering(codePart, InitialPredicate); + } + + + // Optimally arrange blocks assuming that all are executable + public static void ApplyInitialOrdering(IRCodePart codePart) + => ApplyOrdering(codePart, InitialPredicate); + + private static void ApplyOrdering(IRCodePart codePart, Func inclusionPredicate) + { + foreach (IRCodePart.IRFunction function in codePart.Functions) + foreach (IRCodePart.IRFunction.IRFunctionFragment fragment in function.Fragments) + fragment.FunctionCode = ApplyOrdering(fragment.FunctionCode[0], inclusionPredicate); + foreach (IRCodePart.IRTrigger trigger in codePart.Triggers) + trigger.Code = ApplyOrdering(trigger.Code[0], inclusionPredicate); + codePart.MainCode = ApplyOrdering(codePart.MainCode[0], inclusionPredicate); + } + + public static List ApplyOrdering(BasicBlock root, Func inclusionPredicate) + { + IEnumerable GetEdges(BasicBlock block) + => block.Successors.Where(inclusionPredicate); + + Queue worklist = new Queue(); + // If the root itself isn't executable, throw an exception because this will + // probably break labels somewhere. + if (!inclusionPredicate(root)) + throw new Exceptions.KOSYouShouldNeverSeeThisException("Root block isn't executable!"); + + List reversePostOrder = BasicBlock.GetReversePostOrder(root, GetEdges); + Stack regionExits = new Stack(); + regionExits.Push(reversePostOrder[reversePostOrder.Count - 1]); + + List metaSequences = new List(); + + Queue sequenceStarts = new Queue(); + sequenceStarts.Enqueue(root); + + while (sequenceStarts.Count > 0) + { + MetaBlockSequence sequence = ConstructMetaSequence(sequenceStarts.Dequeue(), regionExits, out Queue newStarts); + foreach (BasicBlock start in newStarts) + sequenceStarts.Enqueue(start); + metaSequences.Add(sequence); + } + + List results = metaSequences.SelectMany(s => s.Blocks).ToList(); + + if (Optimizer.PassesToSkip.Contains(typeof(ConstantFolding))) + { + foreach (BasicBlock b in results) + { + List instructions = b.Instructions; + int branchIdx = instructions.Count - 1; + if (branchIdx >= 0 && instructions[branchIdx] is IRBranch branch) + { + if (!branch.True.IsExecutable) + instructions[branchIdx] = new IRJump(branch.Block, branch.False, branch.SourceLine, branch.SourceColumn); + else if (!branch.False.IsExecutable) + instructions[branchIdx] = new IRJump(branch.Block, branch.True, branch.SourceLine, branch.SourceColumn); + } + } + } + + return results; + } + + private static MetaBlockSequence ConstructMetaSequence(BasicBlock root, Stack regionExits, out Queue newOffshoots) + { + MetaBlockSequence sequence = new MetaBlockSequence(root); + BasicBlock block = root; + newOffshoots = new Queue(); + while (block != null && block != regionExits.Peek()) + { + if (IdentifyLoop(sequence, regionExits.Peek(), out Loop loopData)) + { + regionExits.Push(loopData.exit); + sequence.Add(ConstructMetaSequence(loopData.body, regionExits, out Queue childOffshoots)); + regionExits.Pop(); + foreach (BasicBlock child in childOffshoots) + newOffshoots.Enqueue(child); + block = loopData.exit; + } + else if (IdentifyBranch(sequence, regionExits.Peek(), out IfElse branchData)) + { + sequence.Add(ConstructMetaSequence(branchData.ifBlock, regionExits, out Queue childOffshoots)); + foreach (BasicBlock child in childOffshoots) + newOffshoots.Enqueue(child); + if (branchData.exit == null && branchData.elseBlock != null) + { + block = branchData.elseBlock; + } + else + { + if (branchData.elseBlock != null) + newOffshoots.Enqueue(branchData.elseBlock); + block = branchData.exit; + } + } + else if (block.PostDominator?.Dominator == block) + { + block = block.PostDominator; + } + else + return sequence; + if (block != null) + sequence.Add(block); + } + return sequence; + } + + public static bool IdentifyLoop(BlockSequence headerBlock, BasicBlock regionExit, out Loop loopData) + { + loopData = new Loop(); + if (!IdentifyBranch(headerBlock, regionExit, out IfElse branchData)) + return false; + + if (headerBlock.Successors.Count != 2) + return false; + + loopData = new Loop(headerBlock.Last, branchData.ifBlock, branchData.exit ?? branchData.elseBlock); + foreach (BasicBlock successor in headerBlock.Successors) + { + if (BackEdgeDetection(successor, headerBlock.Last)) + return true; + } + loopData = new Loop(); + return false; + } + public static bool IdentifyBranch(BlockSequence branchingBlock, BasicBlock regionExit, out IfElse branchData) + { + branchData = new IfElse(); + BasicBlock _branchingBlock = branchingBlock.Last; + + if (_branchingBlock.Instructions.Count == 0) + return false; + if (!(_branchingBlock.Instructions[_branchingBlock.Instructions.Count - 1] is IRBranch branch)) + return false; + + BasicBlock ifBlock, elseBlock, rejoinsAt; + if (!branch.PreferFalse) + { + ifBlock = branch.False; + elseBlock = branch.True; + } + else + { + ifBlock = branch.True; + elseBlock = branch.False; + } + + rejoinsAt = FindLocalMerge(_branchingBlock, regionExit); + //rejoinsAt = _branchingBlock.PostDominator; + //if (rejoinsAt == null || rejoinsAt is SyntheticReturnBlock) + //return false; + + if (elseBlock == rejoinsAt || GetSequenceEnd(elseBlock) == rejoinsAt) + elseBlock = null; + else if (ifBlock == rejoinsAt || GetSequenceEnd(ifBlock) == rejoinsAt) + { + ifBlock = elseBlock; + elseBlock = null; + branch.PreferFalse = !branch.PreferFalse; + } + branchData = new IfElse(_branchingBlock, ifBlock, elseBlock, rejoinsAt); + return true; + } + private static BasicBlock GetSequenceEnd(BasicBlock block) + { + while (block.PostDominator?.Dominator == block) + block = block.PostDominator; + return block; + } + private static bool BackEdgeDetection(BasicBlock successor, BasicBlock target) + { + if (successor.Dominator != target) + return false; + + while (successor.Successors.Count == 1) + { + successor = successor.Successors.First(); + if (successor == target) + return true; + } + + return successor.Successors.Contains(target); + } + private static BasicBlock FindLocalMerge(BasicBlock ifBlock, BasicBlock regionExit) + { + BasicBlock candidate = ifBlock.PostDominator; + while (candidate == regionExit || !IsInsideRegion(candidate, regionExit)) + candidate = candidate.PostDominator; + // If we've walked all the way out, there is no local merge + return candidate == regionExit ? null : candidate; + } + private static bool IsInsideRegion(BasicBlock candidate, BasicBlock regionExit) + { + // Walk the post-dominator chain of regionExit upward. + // If we encounter candidate, it is outside or on the boundary. + BasicBlock block = regionExit; + while (block != null) + { + if (block == candidate) + return false; + block = block.PostDominator; + } + return true; + } + + public readonly struct IfElse + { + public readonly BasicBlock branch; + public readonly BasicBlock ifBlock; + public readonly BasicBlock elseBlock; + public readonly BasicBlock exit; + public IfElse(BasicBlock branch, BasicBlock ifBlock, BasicBlock elseBlock, BasicBlock exit) + { + this.branch = branch; + this.ifBlock = ifBlock; + this.elseBlock = elseBlock; + this.exit = exit; + } + } + public readonly struct Loop + { + public readonly BasicBlock header; + public readonly BasicBlock body; + public readonly BasicBlock exit; + public Loop(BasicBlock header, BasicBlock body, BasicBlock exit) + { + this.header = header; + this.body = body; + this.exit = exit; + } + } + + public abstract class BlockSequence + { + public abstract IReadOnlyList Blocks { get; } + public BasicBlock First { get; } + public BasicBlock Last { get; protected set; } + public IReadOnlyCollection Predecessors => First.Predecessors; + public IReadOnlyCollection Successors => Last.Successors; + public virtual bool Contains(BasicBlock block) + => Blocks.Contains(block); + public abstract void Add(BasicBlock block); + protected BlockSequence(BasicBlock first) + { + First = first; + } + public static explicit operator BlockSequence(BasicBlock block) + => new BasicBlockSequence(block); + } + public class BasicBlockSequence : BlockSequence + { + private readonly List blocks; + public override IReadOnlyList Blocks => blocks; + + public BasicBlockSequence(IEnumerable blocks) : base(blocks.First()) + { + this.blocks = blocks.ToList(); + Last = Blocks[Blocks.Count - 1]; + } + public BasicBlockSequence(BasicBlock block) : base(block) + { + blocks = new List() { block }; + Last = block; + } + public override void Add(BasicBlock block) + { + blocks.Add(block); + Last = block; + } + } + public class MetaBlockSequence : BlockSequence + { + private readonly List blocks; + public override IReadOnlyList Blocks => blocks.SelectMany(b => b.Blocks).ToList(); + public MetaBlockSequence(IEnumerable blocks) : base(blocks.First().First) + { + this.blocks = blocks.ToList(); + Last = Blocks[Blocks.Count - 1]; + } + public MetaBlockSequence(BasicBlock block) : base(block) + { + blocks = new List() { new BasicBlockSequence(block) }; + Last = block; + } + public MetaBlockSequence(BlockSequence block) : base(block.First) + { + blocks = new List() { block }; + Last = block.Last; + } + public override void Add(BasicBlock block) + { + blocks[blocks.Count - 1].Add(block); + Last = block; + } + public void Add(BlockSequence block) + { + blocks.Add(block); + Last = block.Last; + } + public override bool Contains(BasicBlock block) + => blocks.Any(b => b.Contains(block)); + } + } +} diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs b/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs deleted file mode 100644 index 5e6255cf3..000000000 --- a/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using kOS.Safe.Compilation.IR; - -namespace kOS.Safe.Compilation.Optimization.Passes -{ - internal class DeadCodeElimination : IHolisticOptimizationPass - { - public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; - public short SortIndex => 50; - - public void ApplyPass(IRCodePart code) - { - HashSet rootBlocks = new HashSet(code.RootBlocks); - - RemoveDeadBlocks(code.Blocks, rootBlocks); - - foreach (IRCodePart.IRFunction function in code.Functions) - { - RemoveDeadBlocks(function.InitializationCode, rootBlocks); - foreach (IRCodePart.IRFunction.IRFunctionFragment fragment in function.Fragments) - { - RemoveDeadBlocks(fragment.FunctionCode, rootBlocks); - } - } - } - - private static void RemoveDeadBlocks(List blocks, HashSet rootBlocks) - { - List blocksToRemove = new List(); - foreach (BasicBlock block in blocks) - { - if (block.Predecessors.Any() || rootBlocks.Contains(block)) - continue; - blocksToRemove.Add(block); - } - foreach (BasicBlock block in blocksToRemove) - { - blocks.Remove(block); - foreach (BasicBlock successor in block.Successors.ToArray()) - block.RemoveSuccessor(successor); - } - } - } -} From 780b9d68379f60b76c0d3cd72f567acbcb2ba9b6 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 27 May 2026 23:48:19 -0400 Subject: [PATCH 090/120] Refactor some block ordering methods. --- .../Execution/OptimizationTest.cs | 34 ++++++++--------- .../Optimization/Passes/BlockOrdering.cs | 38 +++++++++---------- 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index 1638505d8..f0f728b90 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -834,11 +834,11 @@ public void TestUntilLoopDetection() List _code = CompileCodePart("integration/branching/until.ks"); optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); - Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(BlockSequence(codePart.MainCode[1]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.Loop loopData); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(codePart.MainCode[1], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.LoopData loopData); Assert.AreEqual(2, loopData.body?.ID); Assert.AreEqual(4, loopData.exit?.ID); Assert.AreEqual(1, codePart.MainCode. - Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(BlockSequence(b), null, out _)).Count()); + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(b, null, out _)).Count()); var optimizer = new Safe.Compilation.Optimization.Optimizer(new CompilerOptions() { OptimizationLevel = optimizationLevel }); optimizer.Optimize(codePart); } @@ -850,11 +850,11 @@ public void TestForLoopDetection() List _code = CompileCodePart("integration/branching/for.ks"); optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); - Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(BlockSequence(codePart.MainCode[2]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.Loop loopData); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(codePart.MainCode[2], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.LoopData loopData); Assert.AreEqual(3, loopData.body?.ID); Assert.AreEqual(6, loopData.exit?.ID); Assert.AreEqual(1, codePart.MainCode. - Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(BlockSequence(b), null, out _)).Count()); + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(b, null, out _)).Count()); } [Test] public void TestFromLoopDetection() @@ -864,11 +864,11 @@ public void TestFromLoopDetection() List _code = CompileCodePart("integration/branching/from.ks"); optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); - Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(BlockSequence(codePart.MainCode[2]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.Loop loopData); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(codePart.MainCode[2], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.LoopData loopData); Assert.AreEqual(3, loopData.body?.ID); Assert.AreEqual(5, loopData.exit?.ID); Assert.AreEqual(1, codePart.MainCode. - Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(BlockSequence(b), null, out _)).Count()); + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(b, null, out _)).Count()); } [Test] public void TestIfDetection() @@ -878,12 +878,12 @@ public void TestIfDetection() List _code = CompileCodePart("integration/branching/if.ks"); optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); - Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(codePart.MainCode[0]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.IfElse branchData); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(codePart.MainCode[0], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.BranchData branchData); Assert.AreEqual(1, branchData.ifBlock?.ID); Assert.IsNull(branchData.elseBlock); Assert.AreEqual(2, branchData.exit?.ID); Assert.AreEqual(1, codePart.MainCode. - Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(b), null, out _)).Count()); + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(b, null, out _)).Count()); } [Test] public void TestIfElseDetection() @@ -893,12 +893,12 @@ public void TestIfElseDetection() List _code = CompileCodePart("integration/branching/ifElse.ks"); optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); - Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(codePart.MainCode[0]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.IfElse branchData); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(codePart.MainCode[0], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.BranchData branchData); Assert.AreEqual(1, branchData.ifBlock?.ID); Assert.AreEqual(3, branchData.elseBlock?.ID); Assert.AreEqual(4, branchData.exit?.ID); Assert.AreEqual(1, codePart.MainCode. - Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(b), null, out _)).Count()); + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(b, null, out _)).Count()); } [Test] public void TestIfElseIfDetection() @@ -908,16 +908,16 @@ public void TestIfElseIfDetection() List _code = CompileCodePart("integration/branching/ifElseIf.ks"); optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); - Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(codePart.MainCode[0]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.IfElse branchData); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(codePart.MainCode[0], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.BranchData branchData); Assert.AreEqual(1, branchData.ifBlock?.ID); Assert.AreEqual(3, branchData.elseBlock?.ID); - Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(branchData.elseBlock), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.IfElse branchData2); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(branchData.elseBlock, null, out Safe.Compilation.Optimization.Passes.BlockOrdering.BranchData branchData2); Assert.AreEqual(4, branchData2.ifBlock?.ID); Assert.IsNull(branchData2.elseBlock); Assert.AreSame(branchData.exit, branchData2.exit); Assert.AreEqual(5, branchData.exit?.ID); Assert.AreEqual(2, codePart.MainCode. - Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(b), null, out _)).Count()); + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(b, null, out _)).Count()); } [Test] public void TestIfElseIfElseDetection() @@ -927,19 +927,17 @@ public void TestIfElseIfElseDetection() List _code = CompileCodePart("integration/branching/ifElseIfElse.ks"); optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); - Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(codePart.MainCode[0]), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.IfElse branchData); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(codePart.MainCode[0], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.BranchData branchData); Assert.AreEqual(1, branchData.ifBlock?.ID); Assert.AreEqual(3, branchData.elseBlock?.ID); - Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(branchData.elseBlock), null, out Safe.Compilation.Optimization.Passes.BlockOrdering.IfElse branchData2); + Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(branchData.elseBlock, null, out Safe.Compilation.Optimization.Passes.BlockOrdering.BranchData branchData2); Assert.AreEqual(4, branchData2.ifBlock?.ID); Assert.AreEqual(6, branchData2.elseBlock?.ID); Assert.AreSame(branchData.exit, branchData2.exit); Assert.AreEqual(7, branchData.exit?.ID); Assert.AreEqual(2, codePart.MainCode. - Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(BlockSequence(b), null, out _)).Count()); + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(b, null, out _)).Count()); } - private static Safe.Compilation.Optimization.Passes.BlockOrdering.BlockSequence BlockSequence(BasicBlock block) - => new Safe.Compilation.Optimization.Passes.BlockOrdering.BasicBlockSequence(block); #endregion } } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs index 4cca71cbc..ef3cc2b33 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs @@ -47,7 +47,6 @@ public static List ApplyOrdering(BasicBlock root, Func GetEdges(BasicBlock block) => block.Successors.Where(inclusionPredicate); - Queue worklist = new Queue(); // If the root itself isn't executable, throw an exception because this will // probably break labels somewhere. if (!inclusionPredicate(root)) @@ -98,7 +97,7 @@ private static MetaBlockSequence ConstructMetaSequence(BasicBlock root, Stack(); while (block != null && block != regionExits.Peek()) { - if (IdentifyLoop(sequence, regionExits.Peek(), out Loop loopData)) + if (IdentifyLoop(block, regionExits.Peek(), out LoopData loopData)) { regionExits.Push(loopData.exit); sequence.Add(ConstructMetaSequence(loopData.body, regionExits, out Queue childOffshoots)); @@ -107,7 +106,7 @@ private static MetaBlockSequence ConstructMetaSequence(BasicBlock root, Stack childOffshoots)); foreach (BasicBlock child in childOffshoots) @@ -135,32 +134,31 @@ private static MetaBlockSequence ConstructMetaSequence(BasicBlock root, Stack Date: Wed, 27 May 2026 23:51:17 -0400 Subject: [PATCH 091/120] Fix phi variable equality comparisons in cases where reference equality is necessary. --- src/kOS.Safe/Compilation/IR/InterimVariables.cs | 4 ++-- .../Compilation/IR/SingleStaticAssignment.cs | 14 ++++++++++++-- .../Optimization/Passes/SCCPWithTypePropagation.cs | 4 +++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs index abab12d86..239403173 100644 --- a/src/kOS.Safe/Compilation/IR/InterimVariables.cs +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -212,8 +212,8 @@ public enum SetState public abstract Type Type { get; } public virtual SetState State { get; } public IRInstruction AssignedAt { get; } - public HashSet ReplacedBy { get; } = new HashSet(); - public HashSet Replaces { get; } = new HashSet(); + public HashSet ReplacedBy { get; } = new HashSet(SSAReferenceEqualityComparer.Instance); + public HashSet Replaces { get; } = new HashSet(SSAReferenceEqualityComparer.Instance); protected SSADefinition(string name) { diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index c62f96058..34e30b923 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -455,6 +455,16 @@ static void HandleFunction(IRCall call, IRFunction function, Dictionary<(string, } } + private class PhiComparer : IEqualityComparer<(IRScope, SSADefinition)> + { + public static PhiComparer Instance = new PhiComparer(); + public bool Equals((IRScope, SSADefinition) x, (IRScope, SSADefinition) y) + => x.Item1.Equals(y.Item1) && SSAReferenceEqualityComparer.Instance.Equals(x.Item2, y.Item2); + + public int GetHashCode((IRScope, SSADefinition) obj) + => (obj.Item1.GetHashCode(), SSAReferenceEqualityComparer.Instance.GetHashCode(obj.Item2)).GetHashCode(); + } + private static void BuildPhis(BasicBlock root, IRCodePart codePart, IClosureVariableUser funcOrTrigger) { Dictionary> variablesOut = @@ -492,7 +502,7 @@ private static void BuildPhis(BasicBlock root, IRCodePart codePart, IClosureVari foreach (IGrouping<(string Name, IRScope Scope), (BasicBlock Block, IRScope Scope, SSADefinition Variable)> definitionSet in varsIn.GroupBy(v => (v.Variable.Name, v.Scope))) { - if (definitionSet.Select(def => (def.Scope, def.Variable)).Distinct().Skip(1).Any()) + if (definitionSet.Select(def => (def.Scope, def.Variable)).Distinct(PhiComparer.Instance).Skip(1).Any()) { // Phi required if (!block.Phis.TryGetValue(definitionSet.Key, out PhiNode phiVar)) @@ -540,7 +550,7 @@ private static void BuildPhis(BasicBlock root, IRCodePart codePart, IClosureVari { Dictionary<(string, IRScope), SSADefinition> oldDefinition = variablesOut[block]; if (oldDefinition.Count == varsOut.Count && - varsOut.All(kvp => oldDefinition.ContainsKey(kvp.Key) && oldDefinition[kvp.Key].Equals((object)kvp.Value))) + varsOut.All(kvp => oldDefinition.ContainsKey(kvp.Key) && SSAReferenceEqualityComparer.Instance.Equals(oldDefinition[kvp.Key], kvp.Value))) continue; } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index da26eeb62..9a096f2bc 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -305,6 +305,8 @@ IInterimOperand Propagate(IInterimOperand operand) => PropagateConstant(operand, constantDef, replacements[constantDef]); foreach (IOperandInstructionBase instruction in ssaUses[constantDef]) { + if (instruction is PhiNode) + continue; if (instruction is IRUnaryOp unaryOp && unaryOp.Operation is OpcodeExists) continue; @@ -410,7 +412,7 @@ private static void RemoveAssignment(IRAssign assignment) private static IInterimOperand PropagateConstant(IInterimOperand operand, SSADefinition definition, InterimConstantValue constant) { if (operand is InterimResolvedReference resolvedReference && - resolvedReference.Reference.Equals((object)definition)) + resolvedReference.Reference.Equals(definition)) { return constant; } From 766b96fb06aeb93b80e72c42e23e933c675b0a1a Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 27 May 2026 23:53:15 -0400 Subject: [PATCH 092/120] Fix some memory holds that last longer than necessary by moving to instance data from static data. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 5 ++++- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 6 ++++++ src/kOS.Safe/Compilation/IR/InterimVariables.cs | 5 ++++- src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs | 4 +--- .../Optimization/Passes/SCCPWithTypePropagation.cs | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index c7dd64b69..59b033e35 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -196,7 +196,10 @@ public BasicBlock(IRCodePart codePart, int startIndex, int endIndex, string nonS CodePart = codePart; StartIndex = startIndex; EndIndex = endIndex; - ID = nextID++; + unchecked + { + ID = nextID++; + } this.nonSequentialLabel = nonSequentialLabel; } diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index 89a056d4d..872fd78fd 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -37,6 +37,12 @@ public class IRCodePart /// public List Blocks { get; } = new List(); + /// + /// Gets the reachable variables for a given call site. + /// + public Dictionary> ReachableVariables { get; } = + new Dictionary>(); + /// /// Initializes a new instance of the class. /// diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs index 239403173..0efc32130 100644 --- a/src/kOS.Safe/Compilation/IR/InterimVariables.cs +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -187,7 +187,10 @@ public static class SSAIndexIssuer public static uint GetIndex(string name) { if (indices.ContainsKey(name)) - return ++indices[name]; + unchecked + { + return ++indices[name]; + } indices[name] = 0; return 0; } diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index 34e30b923..21dc57b5e 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -12,8 +12,6 @@ namespace kOS.Safe.Compilation.IR /// public static class SingleStaticAssignment { - public static Dictionary> ReachableVariables { get; } = - new Dictionary>(); /// /// Finalizes a program into single static assignment form. @@ -597,7 +595,7 @@ IInterimOperand ScopedSSAReplacement(IInterimOperand op) IRFunction function = codePart.GetFunction(call); if (function != null) { - ReachableVariables[call] = DetermineCallReaches(call, function, funcOrTrigger, liveDefinitions, triggerBlacklist); + codePart.ReachableVariables[call] = DetermineCallReaches(call, function, funcOrTrigger, liveDefinitions, triggerBlacklist); } ProcessCall(call, codePart, liveDefinitions, triggerBlacklist, triggerWriteBlacklist, null, true); } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index 9a096f2bc..98bfe40bd 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -115,7 +115,7 @@ private static Dictionary> MapUs IRCodePart.IRFunction function = codePart.GetFunction(call); if (function != null) { - HashSet variables = SingleStaticAssignment.ReachableVariables[call]; + HashSet variables = codePart.ReachableVariables[call]; foreach (SSADefinition variable in variables.SelectMany(GetSSADefinitionsFromReferences)) { GetOrCreate(variableUses, variable).Add(call); From 0c45f1f64c218144d9502a1012be4a544ca3fc70 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 27 May 2026 23:54:22 -0400 Subject: [PATCH 093/120] Fix dominator tree reconstruction keyNotFound exceptions. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 59b033e35..88c4ff5b0 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -242,11 +242,9 @@ public void RemoveSuccessor(BasicBlock successor) successor.predecessors.Remove(this); // Recompute the Dominance tree(s) - successor.Dominator = null; EstablishDominance(); // Recompute the Post-Dominance tree(s) - PostDominator = null; successor.EstablishPostDominance(); } @@ -286,7 +284,11 @@ private static void EstablishDominanceCore(BasicBlock root, Func index = new Dictionary(); for (int i = 0; i < reversePostOrder.Count; i++) + { index[reversePostOrder[i]] = i; + // Initialize dominators to null. + setDominator(reversePostOrder[i], null); + } // Initialize reversePostOrder.Remove(root); @@ -298,13 +300,13 @@ private static void EstablishDominanceCore(BasicBlock root, Func p != block).FirstOrDefault + BasicBlock newIdom = getPrecedents(block).Where(p => p != block).Where(index.ContainsKey).FirstOrDefault (p => p == root || getDominator(p) != null); if (newIdom == null) continue; - foreach (BasicBlock predecessor in getPrecedents(block)) + foreach (BasicBlock predecessor in getPrecedents(block).Where(index.ContainsKey)) { if (predecessor == newIdom) continue; From 86407ef2c8d5d26b2020a0e7f2512b8a89501be4 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 27 May 2026 23:56:29 -0400 Subject: [PATCH 094/120] Implement the SSA conversion as an optimizer pass. This lets other passes occur first (SortIndex < -2000) if they want. Finally a use for negative numbers in the int field! --- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 2 -- src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs | 7 ++++++- src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs | 5 +++++ .../Optimization/Passes/SCCPWithTypePropagation.cs | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index 872fd78fd..6ed66c7b7 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -121,8 +121,6 @@ public IRCodePart(List mainCode, List userFunctions, List< RootBlocks.Add(MainCode[0]); RootBlocks.AddRange(Triggers.Select(t => t.RootBlock)); RootBlocks.AddRange(Functions.SelectMany(f => f.RootBlocks)); - - SingleStaticAssignment.FinalizeSSA(this); } /// diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index 21dc57b5e..e50c650b8 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -10,8 +10,13 @@ namespace kOS.Safe.Compilation.IR /// This utility class converts an IRCodePart into single static /// assignment form. /// - public static class SingleStaticAssignment + public class SingleStaticAssignment : IHolisticOptimizationPass { + public OptimizationLevel OptimizationLevel => OptimizationLevel.None; + public short SortIndex => -2000; + + public void ApplyPass(IRCodePart codePart) + => FinalizeSSA(codePart); /// /// Finalizes a program into single static assignment form. diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index 83804ad25..847d3ce44 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -3,7 +3,12 @@ namespace kOS.Safe.Compilation #pragma warning restore IDE0130 // Namespace does not match folder structure { /* + * O0: + * -2000. Conversion to SSA (analysis pass) + * -1000. Type Propagation + * * O1: + * -1000. SCCP with Type Propagation * 10. Suffix replacement: * Replace CONSTANT: values with the constant * Replace ship fields with their alias diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index 98bfe40bd..db4275a8f 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -17,7 +17,7 @@ public class SCCPWithTypePropagation : IHolisticOptimizationPass, ILinkedOptimiz /// public OptimizationLevel OptimizationLevel => OptimizationLevel.None; - public short SortIndex => short.MinValue; + public short SortIndex => -1000; public Optimizer Optimizer { private get; set; } From 4722d8621f48e1400ec93ca4aaaa78b870226596 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 27 May 2026 23:57:18 -0400 Subject: [PATCH 095/120] Improve the number of jump statements that can be eliminated. --- src/kOS.Safe/Compilation/IR/IREmitter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kOS.Safe/Compilation/IR/IREmitter.cs b/src/kOS.Safe/Compilation/IR/IREmitter.cs index ac20446c8..714305e62 100644 --- a/src/kOS.Safe/Compilation/IR/IREmitter.cs +++ b/src/kOS.Safe/Compilation/IR/IREmitter.cs @@ -31,7 +31,7 @@ public List Emit(List blocks) if (i >= result.Count - 2) continue; if (opcode is OpcodeBranchJump jump && - jump.DestinationLabel != null && jump.DestinationLabel == result[i + 1].Label) + jump.DestinationLabel != null && jumpLabels[jump.DestinationLabel] == i + 1) { foreach (var key in jumpLabels.Keys.ToArray()) if (jumpLabels[key] >= i) From a19e33984db2ca0aa4d747ce891a3a8049d8912d Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 28 May 2026 00:00:07 -0400 Subject: [PATCH 096/120] Implement cloning of IInterimOperands. The 'block' parameter is required so nested IResultingInstructions can know which BasicBlock they're within. --- .../Compilation/IR/IInterimOperand.cs | 6 +++ src/kOS.Safe/Compilation/IR/IRInstruction.cs | 38 +++++++++++++++++-- .../Compilation/IR/InterimConstantValue.cs | 6 +++ .../Compilation/IR/InterimVariables.cs | 9 +++++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IInterimOperand.cs b/src/kOS.Safe/Compilation/IR/IInterimOperand.cs index d8647d134..1256c2f1e 100644 --- a/src/kOS.Safe/Compilation/IR/IInterimOperand.cs +++ b/src/kOS.Safe/Compilation/IR/IInterimOperand.cs @@ -31,5 +31,11 @@ public interface IInterimOperand /// /// true if the operands are equal, otherwise false. bool Equals(IInterimOperand other); + + /// + /// Clones this instance. + /// + /// The block within which this instance is being cloned. + IInterimOperand Clone(BasicBlock block); } } diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 9e4f44c43..c35c6cbec 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -255,6 +255,10 @@ public bool SwapOperands() } return true; } + + public IInterimOperand Clone(BasicBlock block) + => new IRBinaryOp(block, (BinaryOpcode)SetSourceLocation(Operation), Left.Clone(block), Right.Clone(block)); + public override IEnumerable EmitOpcodes() { foreach (Opcode opcode in Left.EmitOpcodes()) @@ -264,6 +268,7 @@ public override IEnumerable EmitOpcodes() Operation.Label = string.Empty; yield return SetSourceLocation(Operation); } + public override string ToString() => Operation.ToString(); public bool Equals(IInterimOperand other) @@ -340,6 +345,9 @@ public IRUnaryOp(BasicBlock block, Opcode operation, IInterimOperand operand) : Operation = operation; Operand = operand; } + + public IInterimOperand Clone(BasicBlock block) + => new IRUnaryOp(block, Operation, Operand.Clone(block)); public override IEnumerable EmitOpcodes() { foreach (Opcode opcode in Operand.EmitOpcodes()) @@ -477,6 +485,8 @@ public IRNonVarPush(BasicBlock block, Opcode opcode) : base(opcode, block) { Operation = opcode; } + public IInterimOperand Clone(BasicBlock block) + => new IRNonVarPush(block, Operation); public override IEnumerable EmitOpcodes() { Operation.Label = string.Empty; @@ -501,6 +511,8 @@ public IRSuffixGet(BasicBlock block, IInterimOperand obj, OpcodeGetMember opcode Object = obj; Suffix = opcodeGetMember.Identifier; } + public IInterimOperand Clone(BasicBlock block) + => new IRSuffixGet(block, Object.Clone(block), new OpcodeGetMember(Suffix) { SourceLine = SourceLine, SourceColumn = SourceColumn }); public override IEnumerable EmitOpcodes() { foreach (Opcode opcode in Object.EmitOpcodes()) @@ -533,7 +545,6 @@ public InterimConstantValue Evaluate() if (!IsInvariant) throw new InvalidOperationException(); throw new NotImplementedException(); - // MUSTFIX: #pragma warning disable CS0162 // Unreachable code detected Encapsulation.Structure obj = (Encapsulation.Structure)(Object as IEvaluatableToConstant).Evaluate()?.Value; #pragma warning restore CS0162 // Unreachable code detected @@ -633,6 +644,8 @@ public IRIndexGet(BasicBlock block, IInterimOperand obj, IInterimOperand index, Object = obj; Index = index; } + public IInterimOperand Clone(BasicBlock block) + => new IRIndexGet(block, Object.Clone(block), Index.Clone(block), new OpcodeGetIndex() { SourceLine = SourceLine, SourceColumn = SourceColumn }); public override IEnumerable EmitOpcodes() { foreach (Opcode opcode in Object.EmitOpcodes()) @@ -664,7 +677,6 @@ public InterimConstantValue Evaluate() { if (!IsInvariant) throw new InvalidOperationException(); - // MUSTFIX: throw new NotImplementedException(); //((Encapsulation.IIndexable)Object).GetIndex(); } @@ -795,6 +807,17 @@ public override IEnumerable EmitOpcodes() yield return SetSourceLocation(new OpcodeBranchJump() { DestinationLabel = False.Label }); } } + public IRBranch Clone(BasicBlock block) + { + BranchOpcode opcode; + if (PreferFalse) + opcode = new OpcodeBranchIfFalse(); + else + opcode = new OpcodeBranchIfTrue(); + opcode.SourceLine = SourceLine; + opcode.SourceColumn = SourceColumn; + return new IRBranch(block, Condition.Clone(block), True, False, opcode); + } public override string ToString() => string.Format("{{br.? {0}/{1}}}", True.Label, False.Label); public override bool Equals(object obj) @@ -838,13 +861,22 @@ private bool IsCallInvariant() } private Type GetDefaultReturnType() { - IRCodePart.IRFunction function = Block.CodePart.GetFunction(this); + IRCodePart.IRFunction function = Block?.CodePart?.GetFunction(this); + if (function != null) + return function.Returns.Type; if (Optimization.Optimizer.FunctionManager.Exists(Function.Replace("()", ""))) return Optimization.Optimizer.FunctionManager.FunctionReturnType(Function.Replace("()", "")); if (!Direct && IndirectMethod is IRSuffixGetMethod suffixGetMethod) return suffixGetMethod.Type; return typeof(Encapsulation.Structure); } + public IInterimOperand Clone(BasicBlock block) + => new IRCall(block, new OpcodeCall(Function) + { + Direct = Direct, + SourceLine = SourceLine, + SourceColumn = SourceColumn + }, EmitArgMarker, Arguments); public override IEnumerable EmitOpcodes() { if (EmitArgMarker) diff --git a/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs b/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs index 20843c905..a4d3672ce 100644 --- a/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs +++ b/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs @@ -46,6 +46,9 @@ public override string ToString() InterimConstantValue IEvaluatableToConstant.Evaluate() => this; + + IInterimOperand IInterimOperand.Clone(BasicBlock _) + => this; } public class IRRelocateLater : InterimConstantValue @@ -90,5 +93,8 @@ public class IRParameter : IInterimOperand public IEnumerable EmitOpcodes() => System.Linq.Enumerable.Empty(); public bool Equals(IInterimOperand other) => other == this; + // This shouldn't need to be implemented... Shouldn't. + public IInterimOperand Clone(BasicBlock _) + => throw new NotImplementedException(); } } diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs index 0efc32130..d960c45b6 100644 --- a/src/kOS.Safe/Compilation/IR/InterimVariables.cs +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -30,6 +30,9 @@ public InterimVariableReference(string name, short sourceLine, short sourceColum SourceColumn = sourceColumn; } + public IInterimOperand Clone(BasicBlock _) + => this; + public IEnumerable EmitOpcodes() { yield return new OpcodePush(Name) @@ -78,6 +81,9 @@ public InterimResolvedReference(SSADefinition reference, short sourceLine, short SourceColumn = sourceColumn; } + public IInterimOperand Clone(BasicBlock _) + => this; + public IEnumerable EmitOpcodes() { yield return new OpcodePush(Name) @@ -160,6 +166,9 @@ public void AddReference(SSADefinition reference) references.Add(reference); } + public IInterimOperand Clone(BasicBlock _) + => new InterimUnresolvedReference(References, SourceLine, SourceColumn); + public IEnumerable EmitOpcodes() { yield return new OpcodePush(Name) From 453dc11334dc2e144c597a5c745300eea5bac1e2 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 28 May 2026 00:02:33 -0400 Subject: [PATCH 097/120] Add new pass to restructure the conditional checks for loops. This saves an unconditional jump from the loop's end by repeating the conditional check at that point and conditionally jumping to the start of the loop body instead of unconditionally to the loop header. For safety, this pass occurs before the SSA conversion. --- .../Execution/OptimizationTest.cs | 39 ++++++++ .../Optimization/OptimizationLevel.cs | 1 + .../Optimization/Passes/BlockOrdering.cs | 7 +- .../Passes/LoopConditionalRelocation.cs | 96 +++++++++++++++++++ 4 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index f0f728b90..a0a49379f 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -939,5 +939,44 @@ public void TestIfElseIfElseDetection() Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(b, null, out _)).Count()); } #endregion + + #region Loop Condition Reordering + [Test] + public void TestUntilLoopCondition() + { + optimizationLevel = OptimizationLevel.Balanced; + List _code = CompileCodePart("integration/branching/until.ks"); + optimizationLevel = OptimizationLevel.Minimal; + List opcodes = _code[0].MainCode; + Assert.AreEqual(opcodes[4].ToString(), opcodes[10].ToString()); + Assert.IsInstanceOf(typeof(OpcodeBranchIfTrue), opcodes[5]); + Assert.IsInstanceOf(typeof(OpcodeBranchIfFalse), opcodes[11]); + } + [Test] + public void TestForLoopCondition() + { + optimizationLevel = OptimizationLevel.Balanced; + List _code = CompileCodePart("integration/branching/for.ks"); + optimizationLevel = OptimizationLevel.Minimal; + List opcodes = _code[0].MainCode; + Assert.AreEqual(opcodes[8].ToString(), opcodes[15].ToString()); + Assert.AreEqual(opcodes[9].ToString(), opcodes[16].ToString()); + Assert.IsInstanceOf(typeof(OpcodeBranchIfFalse), opcodes[10]); + Assert.IsInstanceOf(typeof(OpcodeBranchIfTrue), opcodes[17]); + } + [Test] + public void TestFromLoopCondition() + { + optimizationLevel = OptimizationLevel.Balanced; + List _code = CompileCodePart("integration/branching/from.ks"); + optimizationLevel = OptimizationLevel.Minimal; + List opcodes = _code[0].MainCode; + Assert.AreEqual(opcodes[7].ToString(), opcodes[19].ToString()); + Assert.AreEqual(opcodes[8].ToString(), opcodes[20].ToString()); + Assert.AreEqual(opcodes[9].ToString(), opcodes[21].ToString()); + Assert.IsInstanceOf(typeof(OpcodeBranchIfTrue), opcodes[10]); + Assert.IsInstanceOf(typeof(OpcodeBranchIfFalse), opcodes[22]); + } + #endregion } } diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index 847d3ce44..f35b062c2 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -26,6 +26,7 @@ namespace kOS.Safe.Compilation * 32767. Block ordering to minimize unconditional jumps. * * O2: + * -10000. Loop condition reorganization * 1000. Common expression elimination * Particularly: Any expression of 3 opcodes used more than twice, * or any expression of >3 opcodes used more than once diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs index ef3cc2b33..8b41abe9a 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs @@ -106,7 +106,9 @@ private static MetaBlockSequence ConstructMetaSequence(BasicBlock root, Stack childOffshoots)); foreach (BasicBlock child in childOffshoots) @@ -197,6 +199,7 @@ private static BasicBlock GetSequenceEnd(BasicBlock block) } private static bool BackEdgeDetection(BasicBlock successor, BasicBlock target) { + BasicBlock firstSuccessor = successor; if (successor.Dominator != target) return false; @@ -207,7 +210,7 @@ private static bool BackEdgeDetection(BasicBlock successor, BasicBlock target) return true; } - return successor.Successors.Contains(target); + return successor.Successors.Contains(target) || successor.Successors.Contains(firstSuccessor); } private static BasicBlock FindLocalMerge(BasicBlock ifBlock, BasicBlock regionExit) { diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs new file mode 100644 index 000000000..b17328e26 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class LoopConditionalRelocation : IHolisticOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.Balanced; + + public short SortIndex => -10000; + + public void ApplyPass(IRCodePart codePart) + { + foreach (IRCodePart.IRFunction function in codePart.Functions) + foreach (IRCodePart.IRFunction.IRFunctionFragment fragment in function.Fragments) + ApplyPass(fragment.FunctionCode[0]); + foreach (IRCodePart.IRTrigger trigger in codePart.Triggers) + ApplyPass(trigger.Code[0]); + ApplyPass(codePart.MainCode[0]); + } + + private static void ApplyPass(BasicBlock root) + { + IEnumerable GetEdges(BasicBlock block) + => block.Successors.Where(BlockOrdering.InitialPredicate); + + List reversePostOrder = BasicBlock.GetReversePostOrder(root, GetEdges); + Stack regionExits = new Stack(); + regionExits.Push(reversePostOrder[reversePostOrder.Count - 1]); + + List loopData = new List(); + + FindLoops(root, regionExits, loopData); + + foreach (BlockOrdering.LoopData loop in loopData) + { + RelocateConditional(loop); + } + } + + private static void RelocateConditional(BlockOrdering.LoopData loopData) + { + BasicBlock header = loopData.header; + BasicBlock block = loopData.body; + while (block.PostDominator?.Dominator == block) + block = block.PostDominator; + + IRBranch newBranch = (IRBranch)header.Instructions[header.Instructions.Count - 1]; + newBranch = newBranch.Clone(block); + newBranch.PreferFalse = !newBranch.PreferFalse; + + block.Instructions[block.Instructions.Count - 1] = newBranch; + block.AddSuccessor(newBranch.True); + block.AddSuccessor(newBranch.False); + block.RemoveSuccessor(header); + } + + private static void FindLoops(BasicBlock root, Stack regionExits, List loops) + { + BasicBlock block = root; + while (block != null && block != regionExits.Peek()) + { + if (BlockOrdering.IdentifyLoop(block, regionExits.Peek(), out BlockOrdering.LoopData loopData)) + { + loops.Add(loopData); + regionExits.Push(loopData.exit); + FindLoops(loopData.body, regionExits, loops); + regionExits.Pop(); + block = loopData.exit; + } + else if (BlockOrdering.IdentifyBranch(block, regionExits.Peek(), out BlockOrdering.BranchData branchData)) + { + FindLoops(branchData.ifBlock, regionExits, loops); + if (branchData.exit == null && branchData.elseBlock != null) + { + block = branchData.elseBlock; + } + else + { + if (branchData.elseBlock != null) + FindLoops(branchData.elseBlock, regionExits, loops); + block = branchData.exit; + } + } + else if (block.PostDominator?.Dominator == block) + { + block = block.PostDominator; + } + else + return; + } + } + } +} From 40356d8ef23f245c79f3e6e1b9e6db2d2962a8a3 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 28 May 2026 18:09:28 -0400 Subject: [PATCH 098/120] Implement invariance testing and resultant value tracking for user functions. Now a user function that returns a constant, without doing anything outside itself, can be treated as a constant. This also enables type inferencing of user function returns. --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 6 +- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 62 +++++++++++++++++++ src/kOS.Safe/Compilation/IR/IRInstruction.cs | 11 +++- .../Compilation/IR/InterimVariables.cs | 29 +++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 00e7874b2..22afa12f6 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -223,11 +223,13 @@ IInterimOperand PopStack() case OpcodeEOP _: case OpcodeNOP _: case OpcodeBogus _: - case OpcodePushScope _: - case OpcodePopScope _: case OpcodeArgBottom _: currentBlock.Add(new IRNoStackInstruction(currentBlock, opcode)); break; + case OpcodePushScope _: + case OpcodePopScope _: + currentBlock.Add(new IRNoStackInstruction(currentBlock, opcode, true)); + break; case OpcodeTestArgBottom _: instruction = new IRNonVarPush(currentBlock, opcode); stack.Push(instruction); diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index 6ed66c7b7..fd6408bea 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -320,6 +320,68 @@ public class IRFunction : IClosureVariableUser /// true if this instance may be recursive; otherwise, false. /// public bool IsRecursive => FunctionCalls.Contains(this); + /// + /// Gets the return value of this function. + /// + public PhiOperand Returns + { + get + { + foreach (IRFunctionFragment fragment in Fragments) + { + foreach (BasicBlock block in fragment.FunctionCode) + { + if (block.Successors.Any()) + { + returns.PossibleValues.Remove(block); + continue; + } + if (!(block.Instructions[block.Instructions.Count - 1] is IRReturn ret)) + returns.PossibleValues[block] = null; + else + returns.PossibleValues[block] = ret.Value; + } + } + return returns; + } + } + private readonly PhiOperand returns = new PhiOperand(); + /// + /// Gets a value indicating whether this instance is invariant. + /// A user function that contains any calls to non-invariant functions is, + /// itself, not invariant. Any assignments or unsets to the enclosing scope or + /// setting any suffixes or indexes also makes a function non-invariant. + /// + /// + /// true if this instance is invariant; otherwise, false. + /// + public bool IsInvariant + => Returns.IsInvariant && + IsSelfInvariant() && + FunctionCalls.Where(func => func != this).All(function => function.IsSelfInvariant()); + + private bool IsSelfInvariant() + { + if (ExternalWrites.Count > 0 || ExternalUnsets.Count > 0) + return false; + + return Fragments.All(fragment => + fragment.FunctionCode.Where(block => block.IsExecutable).All(block => + block.Instructions.All(instruction => + { + switch (instruction) + { + case IRNoStackInstruction noStackInstruction: + case IRSuffixSet _: + case IRIndexSet _: + return instruction.IsInvariant; + default: + return true; + } + }) + ) + ); + } public List RootBlocks { get; } = new List(); diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index c35c6cbec..3eedb9bc6 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -399,10 +399,12 @@ public InterimConstantValue Evaluate() } public class IRNoStackInstruction : IRInstruction { - public override bool IsInvariant => false; + public override bool IsInvariant { get; } = false; public Opcode Operation { get; } public IRNoStackInstruction(BasicBlock block, Opcode opcode) : base(opcode, block) => Operation = opcode; + public IRNoStackInstruction(BasicBlock block, Opcode opcode, bool isInvariant) : this(block, opcode) + => IsInvariant = isInvariant; public override IEnumerable EmitOpcodes() { Operation.Label = string.Empty; @@ -853,6 +855,9 @@ private IRCall(BasicBlock block, OpcodeCall opcode, bool emitArgMarker) : base(o private bool IsCallInvariant() { // TODO: Consider that some suffix methods may actually be known at compile time. + IRCodePart.IRFunction function = Block?.CodePart?.GetFunction(this); + if (function != null) + return function.IsInvariant; if (!Direct) return false; if (Optimization.Optimizer.FunctionManager.Exists(Function.Replace("()", ""))) @@ -928,6 +933,10 @@ public InterimConstantValue Evaluate() if (!IsInvariant) throw new InvalidOperationException(); + IRCodePart.IRFunction function = Block?.CodePart?.GetFunction(this); + if (function != null) + return function.Returns.Evaluate(); + string functionName = Function.Replace("()", ""); Optimization.InterimCPU interimCPU = Optimization.Optimizer.InterimCPU; interimCPU.Boot(); // Clear the stack out of caution. diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs index d960c45b6..7f9646792 100644 --- a/src/kOS.Safe/Compilation/IR/InterimVariables.cs +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -618,6 +618,35 @@ protected override void MutateEachOperand(Func } } + public class PhiOperand : PhiNode, IEvaluatableToConstant + { + protected override bool ObjIsInvariant(IInterimOperand obj) + => obj == null || (obj.IsInvariant && obj is IEvaluatableToConstant); + protected override Type ObjType(IInterimOperand obj) + => obj.Type; + protected override IEnumerable Operands => PossibleValues.Values; + + public override InterimConstantValue Evaluate() + { + if (!PossibleValues.Any()) + return null; + return base.Evaluate(); + } + + protected override InterimConstantValue EvaluateObj(IInterimOperand obj) + => (obj as IEvaluatableToConstant)?.Evaluate(); + + protected override void ForEachOperand(Action action) + { + foreach (IInterimOperand operand in PossibleValues.Values) + action(operand); + } + protected override void MutateEachOperand(Func mutateFunc) + { + foreach (BasicBlock block in PossibleValues.Keys) + PossibleValues[block] = mutateFunc(PossibleValues[block]); + } + } public abstract class PhiNode : IMultipleOperandInstruction { public bool IsInvariant From 03720c43f9a956efd3cb726eb79454a7c4869157 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 28 May 2026 22:59:05 -0400 Subject: [PATCH 099/120] Fix IRBinaryInstruction and IRUnaryInstruction hash codes not being equal for a given operation type. --- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 4 ++-- .../Optimization/Passes/SCCPWithTypePropagation.cs | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 3eedb9bc6..a4b04ed2b 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -300,7 +300,7 @@ obj is IEvaluatableToConstant evaluatableToConstant && return false; } public override int GetHashCode() - => Operation.GetHashCode(); + => Operation.GetType().GetHashCode(); public InterimConstantValue Evaluate() { @@ -370,7 +370,7 @@ public override bool Equals(object obj) Operation.GetType() == unaryOp.Operation.GetType() && Operand.Equals(unaryOp.Operand); public override int GetHashCode() - => (Operation, Operand).GetHashCode(); + => (Operation.GetType(), Operand).GetHashCode(); public InterimConstantValue Evaluate() { diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index db4275a8f..cc1259332 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -251,12 +251,20 @@ private static bool VisitInstruction(IOperandInstructionBase instruction, return false; } - private static TValue GetOrCreate(Dictionary dictionary, TKey key) where TValue : new() + private static HashSet GetOrCreate(Dictionary> dictionary, SSADefinition key) { - if (!dictionary.TryGetValue(key, out TValue value)) - value = dictionary[key] = new TValue(); + if (!dictionary.TryGetValue(key, out HashSet value)) + value = dictionary[key] = new HashSet(ReferenceEqualityComparer.Instance); return value; } + private class ReferenceEqualityComparer : IEqualityComparer + { + public static ReferenceEqualityComparer Instance = new ReferenceEqualityComparer(); + public bool Equals(IOperandInstructionBase x, IOperandInstructionBase y) + => x == y; + public int GetHashCode(IOperandInstructionBase obj) + => obj.GetHashCode(); + } /// /// Propagates any constant SSA variables to their uses. From 27fc9e8cf2d710b91371772a4163bc1e96cc471c Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 28 May 2026 23:00:19 -0400 Subject: [PATCH 100/120] Fix incorrect return values in IRScope.IsEncompassedBy --- src/kOS.Safe/Compilation/IR/IRScope.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kOS.Safe/Compilation/IR/IRScope.cs b/src/kOS.Safe/Compilation/IR/IRScope.cs index 7c10b9c7e..4ae1d7a82 100644 --- a/src/kOS.Safe/Compilation/IR/IRScope.cs +++ b/src/kOS.Safe/Compilation/IR/IRScope.cs @@ -129,7 +129,7 @@ public bool IsEncompassedBy(IRScope scope) return true; if (scope.childScopes.Contains(this)) return true; - return IsGlobalScope || ParentScope.IsEncompassedBy(scope); + return !IsGlobalScope && ParentScope.IsEncompassedBy(scope); } public IRScope GetGlobalScope() From 1404ab369069015b8599669a6227dabb0a5db64d Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 28 May 2026 23:02:13 -0400 Subject: [PATCH 101/120] Add a pass to eliminate common subexpressions by using compiler-generated variables. --- .../commonExpressionElimination.ks | 22 +++ .../Execution/OptimizationTest.cs | 32 ++++ src/kOS.Safe/Compilation/IR/BasicBlock.cs | 4 +- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 76 +++++++- .../Compilation/IR/IResultingInstruction.cs | 1 + .../Optimization/OptimizationLevel.cs | 3 +- .../Passes/CommonExpressionElimination.cs | 173 ++++++++++++++++++ 7 files changed, 307 insertions(+), 4 deletions(-) create mode 100644 kerboscript_tests/integration/commonExpressionElimination.ks create mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs diff --git a/kerboscript_tests/integration/commonExpressionElimination.ks b/kerboscript_tests/integration/commonExpressionElimination.ks new file mode 100644 index 000000000..79561e150 --- /dev/null +++ b/kerboscript_tests/integration/commonExpressionElimination.ks @@ -0,0 +1,22 @@ +@lazyGlobal OFF. + +randomseed("random", 0). +local a is 1. +local b is random("random"). +local d is random("random"). + +global c is a + b. + +print c. +print a + b. +print a + b. +{ + print a + b. + global e is b + d. + print b + d. + print b + d. + local f is e. + print f. +} +print "Outside". +print b + d. \ No newline at end of file diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index a0a49379f..15d55dc3e 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -978,5 +978,37 @@ public void TestFromLoopCondition() Assert.IsInstanceOf(typeof(OpcodeBranchIfFalse), opcodes[22]); } #endregion + + [Test] + public void TestCommonExpressionElimination() + { + // Test that common expressions are eliminated + optimizationLevel = OptimizationLevel.Balanced; + RunScript("integration/commonExpressionElimination.ks"); + RunSingleStep(); + AssertOutput( + "1.72624326996796", + "1.72624326996796", + "1.72624326996796", + "1.72624326996796", + "1.54356862955893", + "1.54356862955893", + "1.54356862955893", + "Outside", + "1.54356862955893" + ); + + optimizationLevel = OptimizationLevel.None; + List codePart = CompileCodePart("integration/commonExpressionElimination.ks"); + List result = GetOpcodesAfterOptimization(codePart[0].MainCode, OptimizationLevel.Balanced, false); + optimizationLevel = OptimizationLevel.Minimal; + + Assert.IsInstanceOf(result[18]); + Assert.IsInstanceOf(result[25]); + Assert.IsInstanceOf(result[26]); + Assert.IsInstanceOf(result[63]); + Assert.IsInstanceOf(result[64]); + Assert.IsInstanceOf(result[65]); + } } } diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 88c4ff5b0..90d43c5b2 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -248,7 +248,7 @@ public void RemoveSuccessor(BasicBlock successor) successor.EstablishPostDominance(); } - private static IEnumerable GetSuccessors(BasicBlock block) + public static IEnumerable GetSuccessors(BasicBlock block) => block.successors; private static BasicBlock GetDominator(BasicBlock block) => block.Dominator; @@ -261,7 +261,7 @@ private static void SetDominator(BasicBlock block, BasicBlock dominator) public void EstablishDominance() => EstablishDominanceCore(this, GetPredecessors, GetSuccessors, GetDominator, SetDominator); - private static IEnumerable GetPredecessors(BasicBlock block) + public static IEnumerable GetPredecessors(BasicBlock block) => block.predecessors; private static BasicBlock GetPostDominator(BasicBlock block) => block.PostDominator; diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index a4b04ed2b..c58ec279a 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -164,6 +164,22 @@ public Type Type } } } + public ushort OpcodeCount + { + get + { + ushort result = 1; + if (Left is IResultingInstruction left) + result += left.OpcodeCount; + else + result += 1; + if (Right is IResultingInstruction right) + result += right.OpcodeCount; + else + result += 1; + return result; + } + } protected override IInterimOperand this[int index] { @@ -340,6 +356,19 @@ public Type Type } } } + public ushort OpcodeCount + { + get + { + ushort result = 1; + if (Operand is IResultingInstruction resulting) + result += resulting.OpcodeCount; + else + result += 1; + return result; + } + } + public IRUnaryOp(BasicBlock block, Opcode operation, IInterimOperand operand) : base(operation, block) { Operation = operation; @@ -482,6 +511,7 @@ public Type Type } } } + public ushort OpcodeCount => 1; public IRNonVarPush(BasicBlock block, Opcode opcode) : base(opcode, block) { @@ -508,6 +538,18 @@ public class IRSuffixGet : SingleOperandInstruction, IResultingInstruction public IInterimOperand Object { get => operand; set => operand = value; } public string Suffix { get; set; } public Type Type => TypeInferencer.GetTypeForSuffix(Object.Type, Suffix); + public ushort OpcodeCount + { + get + { + ushort result = 1; + if (Object is IResultingInstruction obj) + result += obj.OpcodeCount; + else + result += 1; + return result; + } + } public IRSuffixGet(BasicBlock block, IInterimOperand obj, OpcodeGetMember opcodeGetMember) : base(opcodeGetMember, block) { Object = obj; @@ -628,6 +670,22 @@ public class IRIndexGet : MultipleOperandInstruction, IResultingInstruction public override IEnumerable Operands { get { yield return Object; yield return Index; } } public override int OperandCount => 2; public Type Type => TypeInferencer.GetTypeForIndex(Object.Type); + public ushort OpcodeCount + { + get + { + ushort result = 1; + if (Object is IResultingInstruction obj) + result += obj.OpcodeCount; + else + result += 1; + if (Index is IResultingInstruction index) + result += index.OpcodeCount; + else + result += 1; + return result; + } + } protected override IInterimOperand this[int index] { get => index == 0 ? Object : index == 1 ? Index : throw new ArgumentOutOfRangeException(); @@ -838,6 +896,22 @@ public class IRCall : MultipleOperandInstruction, IResultingInstruction public override IEnumerable Operands => Enumerable.Reverse(Arguments); public override int OperandCount => Arguments.Count; public Type Type => GetDefaultReturnType(); + public ushort OpcodeCount + { + get + { + ushort result = 2; + if (IndirectMethod is IResultingInstruction method) + result += method.OpcodeCount; + else + result += 1; + foreach (IInterimOperand argument in Arguments) + if (argument is IResultingInstruction arg) + result += arg.OpcodeCount; + else result += 1; + return result; + } + } protected override IInterimOperand this[int index] { get => Arguments[index]; @@ -852,7 +926,7 @@ private IRCall(BasicBlock block, OpcodeCall opcode, bool emitArgMarker) : base(o Direct = opcode.Direct; EmitArgMarker = emitArgMarker; } - private bool IsCallInvariant() + public bool IsCallInvariant() { // TODO: Consider that some suffix methods may actually be known at compile time. IRCodePart.IRFunction function = Block?.CodePart?.GetFunction(this); diff --git a/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs index a7065b5b4..77f8febc6 100644 --- a/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs @@ -7,6 +7,7 @@ namespace kOS.Safe.Compilation.IR /// public interface IResultingInstruction : IEvaluatableToConstant, IInterimOperand { + ushort OpcodeCount { get; } } public interface IEvaluatableToConstant diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index f35b062c2..b3b5ae949 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -23,13 +23,14 @@ namespace kOS.Safe.Compilation * Branch logical simplification (e.g. !X branch = X branch!) * Algebraic simplification (e.g. A*B+A*C = A*(B+C), A+-B=A-B, A--B=A+B, -A+B=B-A, X*X*...*X=N^X) * 32000. Remove unnecessary scope pushes/pops - * 32767. Block ordering to minimize unconditional jumps. + * 32767. Block ordering to minimize unconditional jumps. Also removes blocks that are not executable. * * O2: * -10000. Loop condition reorganization * 1000. Common expression elimination * Particularly: Any expression of 3 opcodes used more than twice, * or any expression of >3 opcodes used more than once + * * 2100. Code motion - moving expressions outside loops if they do not depend on loop variables * 2150. Carry's - moving the N-1 D lookup of >1D lists outside the innermost loop * Loop jamming? (Combining adjacent loops into one) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs b/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs new file mode 100644 index 000000000..2c0a36566 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class CommonExpressionElimination : IHolisticOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.Balanced; + + public short SortIndex => 1000; + + public void ApplyPass(IRCodePart codePart) + { + foreach (BasicBlock rootBlock in codePart.RootBlocks) + EliminateCommonExpressions(rootBlock); + } + private void EliminateCommonExpressions(BasicBlock rootBlock) + { + Dictionary<(IResultingInstruction Expression, IRScope Scope), ExpressionData> expressions = + new Dictionary<(IResultingInstruction, IRScope), ExpressionData>(ExpressionComparer.Instance); + foreach (BasicBlock block in BasicBlock.GetReversePostOrder(rootBlock, BasicBlock.GetSuccessors)) + { + IdentifyExpressions(block, expressions); + } + foreach (var expressionData in expressions.OrderBy(kvp => kvp.Value.sortIndex)) + { + IResultingInstruction expression = expressionData.Key.Expression; + ExpressionData data = expressionData.Value; + int useCount = data.uses.Count; + if (useCount == 0) + continue; + ushort opcodeCount = expression.OpcodeCount; + if (opcodeCount < 2) + continue; + switch (opcodeCount) + { + case 2: + if (useCount < 3) + continue; + break; + case 3: + if (useCount < 2) + continue; + break; + } + + bool first = true; + CompilerTemporaryVariable storedExpression = new CompilerTemporaryVariable(expression, ((IRInstruction)data.origin).Block); + data.origin.MutateEachOperand(op => + { + if (first && op.Equals(expression)) + { + first = false; + return storedExpression; + } + return op; + }); + foreach (IOperandInstructionBase use in data.uses) + { + use.MutateEachOperand(op => new InterimResolvedReference(storedExpression.Definition, (IRInstruction)use)); + } + } + } + private void IdentifyExpressions(BasicBlock block, Dictionary<(IResultingInstruction Expression, IRScope Scope), ExpressionData> expressions) + { + foreach (IRInstruction instruction in block.Instructions) + { + ushort sortIndex = 0; + foreach (IRInstruction subexpression in instruction.DepthFirst()) + { + bool breaking = false; + void AddToExpressions(IInterimOperand operand) + { + if (operand is IRCall call && !call.IsCallInvariant()) + breaking = true; + else if (operand is IResultingInstruction resultingInstruction) + { + if (expressions.TryGetValue((resultingInstruction, block.Scope), out ExpressionData data)) + data.uses.Add((IOperandInstructionBase)subexpression); + else + expressions[(resultingInstruction, block.Scope)] = new ExpressionData((IOperandInstructionBase)subexpression, sortIndex++); + } + } + + if (breaking) + break; + if (subexpression is IOperandInstructionBase operandInstruction) + operandInstruction.ForEachOperand(AddToExpressions); + } + } + } + private class ExpressionComparer : IEqualityComparer<(IResultingInstruction Expression, IRScope Scope)> + { + public static readonly ExpressionComparer Instance = new ExpressionComparer(); + public bool Equals((IResultingInstruction Expression, IRScope Scope) x, (IResultingInstruction Expression, IRScope Scope) y) + => x.Expression.Equals(y.Expression) && y.Scope.IsEqualOrEncompassedBy(x.Scope); + + public int GetHashCode((IResultingInstruction Expression, IRScope Scope) obj) + => obj.Expression.GetHashCode(); + } + private readonly struct ExpressionData + { + public readonly IOperandInstructionBase origin; + public readonly ushort sortIndex; + public readonly List uses; + public ExpressionData(IOperandInstructionBase origin, ushort sortIndex) + { + this.origin = origin; + this.sortIndex = sortIndex; + uses = new List(); + } + } + + public class CompilerTemporaryVariable : IResultingInstruction, IOperandInstructionBase + { + private static ushort nextID; + + public IResultingInstruction Value { get; } + public SSASetDefinition Definition { get; } + public string Identifier { get; } + public ushort OpcodeCount => (ushort)(Value.OpcodeCount + 2); + public bool IsInvariant => false; + public Type Type => Value.Type; + + public CompilerTemporaryVariable(IResultingInstruction value, BasicBlock block) + { + Value = value; + ushort id; + unchecked + { + id = nextID++; + } + Identifier = $"$compiler.Temp.{id}"; + Definition = new SSASetDefinition(Identifier, new IRAssign(block, new OpcodeStoreLocal(Identifier), value)); + } + + public IInterimOperand Clone(BasicBlock block) + => Value.Clone(block); + + public IEnumerable EmitOpcodes() + { + foreach (Opcode opcode in Value.EmitOpcodes()) + { + yield return opcode; + } + yield return new OpcodeDup(); + yield return new OpcodeStoreLocal(Identifier); + } + + public bool Equals(IInterimOperand other) + => other == this; + + public InterimConstantValue Evaluate() + => throw new InvalidOperationException(); + + public void ForEachOperand(Action action) + { + if (Value is IOperandInstructionBase operandInstruction) + operandInstruction.ForEachOperand(action); + } + public void MutateEachOperand(Func mutateFunc) + { + if (Value is IOperandInstructionBase operandInstruction) + operandInstruction.MutateEachOperand(mutateFunc); + } + + public override string ToString() + => Definition.ToString(); + } + } +} From 082215a98950584fde91f8593a1d512761e5d377 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 28 May 2026 23:02:42 -0400 Subject: [PATCH 102/120] Add a small optimization for returning 0 in lieu of X - X. --- .../Compilation/Optimization/Passes/ConstantFolding.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index 23f051f1e..5da0cbeeb 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -228,6 +228,11 @@ private static IInterimOperand AttemptBinarySimplification(IRBinaryOp instructio if (instruction.Left.Equals(instruction.Right)) return new InterimConstantValue(Encapsulation.ScalarIntValue.One, instruction); break; + case OpcodeMathSubtract _: + // X - X = 0 + if (instruction.Left.Equals(instruction.Right)) + return new InterimConstantValue(Encapsulation.ScalarIntValue.Zero, instruction); + break; } } return instruction; From 4c02ccc9e0181ac4baeb19d1401e59a71ca87571 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 29 May 2026 23:31:47 -0400 Subject: [PATCH 103/120] Implement some IEqualityComparer classes better. --- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 10 +++++++- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 11 +++++++++ .../Compilation/IR/InterimVariables.cs | 24 +++++++++---------- .../Compilation/IR/SingleStaticAssignment.cs | 7 +++--- .../Passes/SCCPWithTypePropagation.cs | 9 ++++--- 5 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index fd6408bea..605a88ed7 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -40,8 +40,16 @@ public class IRCodePart /// /// Gets the reachable variables for a given call site. /// + /// This is populated in public Dictionary> ReachableVariables { get; } = - new Dictionary>(); + new Dictionary>(IRInstruction.ReferenceEqualityComparer); + + /// + /// Gets or sets the variable uses. + /// + /// The set accessor is used to populate this in + public Dictionary> VariableUses { get; set; } = + new Dictionary>(SSADefinition.ReferenceEqualityComparer); /// /// Initializes a new instance of the class. diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index c58ec279a..9c76e47c8 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -32,6 +32,17 @@ public void OverwriteSourceLocation(short sourceLine, short sourceColumn) SourceLine = sourceLine; SourceColumn = sourceColumn; } + public static IEqualityComparer ReferenceEqualityComparer => InstructionReferenceEqualityComparer.Instance; + + private class InstructionReferenceEqualityComparer : IEqualityComparer + { + public static InstructionReferenceEqualityComparer Instance { get; } = + new InstructionReferenceEqualityComparer(); + public bool Equals(IRInstruction x, IRInstruction y) + => x == y; + public int GetHashCode(IRInstruction obj) + => obj.GetHashCode(); + } } public abstract class SingleOperandInstruction : IRInstruction, ISingleOperandInstruction diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs index 7f9646792..0649caea8 100644 --- a/src/kOS.Safe/Compilation/IR/InterimVariables.cs +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -219,6 +219,8 @@ public enum SetState Unset = -1 } + public static IEqualityComparer ReferenceEqualityComparer => SSAReferenceEqualityComparer.Instance; + public string Name { get; } public abstract bool IsInvariant { get; } public abstract Type Type { get; } @@ -294,6 +296,16 @@ public override bool Equals(object obj) => obj is SSADefinition ssaDef && Equals(ssaDef); public override int GetHashCode() => Name.ToLower().GetHashCode(); + + private class SSAReferenceEqualityComparer : IEqualityComparer + { + public static SSAReferenceEqualityComparer Instance { get; } = + new SSAReferenceEqualityComparer(); + public bool Equals(SSADefinition x, SSADefinition y) + => x == y; + public int GetHashCode(SSADefinition obj) + => obj.GetHashCode(); + } } public class SSASetDefinition : SSADefinition @@ -569,18 +581,6 @@ public override string ToString() => $"{Name} #{ssaIndex}"; } - public class SSAReferenceEqualityComparer : IEqualityComparer - { - public static SSAReferenceEqualityComparer Instance = - new SSAReferenceEqualityComparer(); - - public bool Equals(SSADefinition x, SSADefinition y) - => x == y; - - public int GetHashCode(SSADefinition obj) - => obj.GetHashCode(); - } - public class PhiNode : PhiNode { protected override bool ObjIsInvariant(SSADefinition obj) diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index e50c650b8..c660e9e20 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -126,7 +126,6 @@ public static void FinalizeSSA(IRCodePart codePart) IRFunction function = codePart.GetFunction(call); if (function != null) funcOrTrigger?.FunctionCalls.Add(codePart.GetFunction(call)); - } switch (instruction) @@ -462,10 +461,10 @@ private class PhiComparer : IEqualityComparer<(IRScope, SSADefinition)> { public static PhiComparer Instance = new PhiComparer(); public bool Equals((IRScope, SSADefinition) x, (IRScope, SSADefinition) y) - => x.Item1.Equals(y.Item1) && SSAReferenceEqualityComparer.Instance.Equals(x.Item2, y.Item2); + => x.Item1.Equals(y.Item1) && SSADefinition.ReferenceEqualityComparer.Equals(x.Item2, y.Item2); public int GetHashCode((IRScope, SSADefinition) obj) - => (obj.Item1.GetHashCode(), SSAReferenceEqualityComparer.Instance.GetHashCode(obj.Item2)).GetHashCode(); + => (obj.Item1.GetHashCode(), SSADefinition.ReferenceEqualityComparer.GetHashCode(obj.Item2)).GetHashCode(); } private static void BuildPhis(BasicBlock root, IRCodePart codePart, IClosureVariableUser funcOrTrigger) @@ -553,7 +552,7 @@ private static void BuildPhis(BasicBlock root, IRCodePart codePart, IClosureVari { Dictionary<(string, IRScope), SSADefinition> oldDefinition = variablesOut[block]; if (oldDefinition.Count == varsOut.Count && - varsOut.All(kvp => oldDefinition.ContainsKey(kvp.Key) && SSAReferenceEqualityComparer.Instance.Equals(oldDefinition[kvp.Key], kvp.Value))) + varsOut.All(kvp => oldDefinition.ContainsKey(kvp.Key) && SSADefinition.ReferenceEqualityComparer.Equals(oldDefinition[kvp.Key], kvp.Value))) continue; } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index cc1259332..e81b82440 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -23,13 +23,12 @@ public class SCCPWithTypePropagation : IHolisticOptimizationPass, ILinkedOptimiz public void ApplyPass(IRCodePart codePart) { - Dictionary> ssaUses = - MapUsesAndPropagateTypes(codePart); + codePart.VariableUses = MapUsesAndPropagateTypes(codePart); if (Optimizer.OptimizationLevel == OptimizationLevel.None) return; - HashSet usedDefinitions = PropagateConstants(ssaUses); + HashSet usedDefinitions = PropagateConstants(codePart.VariableUses); foreach (BasicBlock block in codePart.Blocks) RemoveRedundantAssignments(block, usedDefinitions); @@ -50,7 +49,7 @@ public void ApplyPass(IRCodePart codePart) private static Dictionary> MapUsesAndPropagateTypes(IRCodePart codePart) { Dictionary> variableUses = - new Dictionary>(SSAReferenceEqualityComparer.Instance); + new Dictionary>(SSADefinition.ReferenceEqualityComparer); HashSet visitedBlocks = new HashSet(); Dictionary typeAndInvarianceCache = new Dictionary(); @@ -298,7 +297,7 @@ private static HashSet PropagateConstants(Dictionary replacements = new Dictionary(); From 22c5eac829168cd1db2e635a2f774e65fba2968c Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 29 May 2026 23:33:28 -0400 Subject: [PATCH 104/120] Make variable assignment removal part of the Balanced optimization level to keep with the philosophy that outright removing code like that could have an effect (but should be safe). --- src/kOS.Safe.Test/Execution/OptimizationTest.cs | 6 +++--- .../Optimization/Passes/SCCPWithTypePropagation.cs | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index 15d55dc3e..a484142a8 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -474,7 +474,7 @@ public void TestConstantPropagationAndFolding() new OpcodeMathAdd(), new OpcodeStoreGlobal("$f"), new OpcodePopScope() - } + }, OptimizationLevel.Balanced ); Assert.IsInstanceOf(result[2]); Assert.AreEqual(new Encapsulation.ScalarIntValue(3), (result[2] as OpcodePush)?.Argument); @@ -716,7 +716,7 @@ public void TestDivisionSubtraction() new OpcodeMathDivide(), new OpcodePop(), new OpcodePopScope() - } + }, OptimizationLevel.Balanced ); Assert.IsInstanceOf(result[5]); Assert.AreEqual(new Encapsulation.ScalarDoubleValue(1.5), (result[5] as OpcodePush)?.Argument); @@ -755,7 +755,7 @@ public void TestPowerAddition() new OpcodeMathMultiply(), new OpcodePop(), new OpcodePopScope() - } + }, OptimizationLevel.Balanced ); Assert.IsInstanceOf(result[5]); Assert.AreEqual(new Encapsulation.ScalarDoubleValue(3.5), (result[5] as OpcodePush)?.Argument); diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index e81b82440..988578217 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -30,8 +30,9 @@ public void ApplyPass(IRCodePart codePart) HashSet usedDefinitions = PropagateConstants(codePart.VariableUses); - foreach (BasicBlock block in codePart.Blocks) - RemoveRedundantAssignments(block, usedDefinitions); + if (Optimizer.OptimizationLevel >= OptimizationLevel.Balanced) + foreach (BasicBlock block in codePart.Blocks) + RemoveRedundantAssignments(block, usedDefinitions); } /// From 75255a96bc2c8fcc7846827c9c04725d9283b41c Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 29 May 2026 23:34:54 -0400 Subject: [PATCH 105/120] Prevent calls to non-invariant functions from being optimized away. This is accomplished by treating calls to non-invariant functions as not Equal to each other (which they aren't, because they can affect external things). --- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 1 + src/kOS.Safe/Compilation/IR/InterimVariables.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 9c76e47c8..7bd31ce52 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -1000,6 +1000,7 @@ public override string ToString() => string.Format("{{call {0}({1})}}", Function.Trim('(', ')'), string.Join(",", Arguments.Select(a => a.ToString()))); public bool Equals(IInterimOperand other) => (other is IRCall call && + IsCallInvariant() && // If the call does something beyond arithmetic, this condition prevents optimizing it away. string.Equals(Function.Replace("()", ""), call.Function.Replace("()", ""), StringComparison.OrdinalIgnoreCase) && Arguments.SequenceEqual(call.Arguments)) || (IsInvariant && diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs index 0649caea8..b946bebf8 100644 --- a/src/kOS.Safe/Compilation/IR/InterimVariables.cs +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -371,6 +371,8 @@ public override bool ValuesEqual(SSADefinition other) { if (setDefinition.DefinedAt == null) return false; + if (other == this) + return true; return DefinedAt?.Value.Equals(setDefinition.DefinedAt.Value) ?? false; } return other.Equals(this); From 9c1e729dae48368a1c075a122b41cabd7cb6417f Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 29 May 2026 23:38:30 -0400 Subject: [PATCH 106/120] Minor refactoring. Move common expression elimination to after unnecessary scope removal. Even though it assigns new variables, doing that at a wider scope is always better for that pass. Remove commented-out code. Fix CEE mutating operands that it shouldn't target. --- .../Optimization/OptimizationLevel.cs | 2 +- .../Passes/AlgebraicSimplifications.cs | 8 -------- .../Optimization/Passes/BlockOrdering.cs | 19 +++++++----------- .../Passes/CommonExpressionElimination.cs | 20 +++++++------------ .../Passes/LoopConditionalRelocation.cs | 2 +- 5 files changed, 16 insertions(+), 35 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index b3b5ae949..374c822da 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -27,7 +27,7 @@ namespace kOS.Safe.Compilation * * O2: * -10000. Loop condition reorganization - * 1000. Common expression elimination + * 32100. Common expression elimination * Particularly: Any expression of 3 opcodes used more than twice, * or any expression of >3 opcodes used more than once * diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs b/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs index 22778b268..6a380d718 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs @@ -369,10 +369,6 @@ private static IRBinaryOp IncreasePower(IRBinaryOp instruction, int powerIncreas instruction.Right = instruction.Left; instruction.Operation = new OpcodeMathMultiply(); } - /*else if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) - return instruction.Left; - else if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) - return new InterimConstantValue(Encapsulation.ScalarIntValue.One, instruction);*/ } return instruction; } @@ -416,10 +412,6 @@ private static IRBinaryOp CombinePowers(IRBinaryOp instruction, BinaryOpcode new instruction.Right = instruction.Left; instruction.Operation = new OpcodeMathMultiply(); } - /*else if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) - return instruction.Left; - else if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) - return new InterimConstantValue(Encapsulation.ScalarIntValue.One, instruction);*/ } return instruction; } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs index 8b41abe9a..3403eb198 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs @@ -8,29 +8,27 @@ namespace kOS.Safe.Compilation.Optimization.Passes public class BlockOrdering : IHolisticOptimizationPass, ILinkedOptimizationPass { public OptimizationLevel OptimizationLevel => OptimizationLevel.None; - public short SortIndex => short.MaxValue; - public Optimizer Optimizer { get; set; } - public static bool InitialPredicate(BasicBlock _) + public static bool AllBlocksPredicate(BasicBlock _) => true; - public static bool IsExecutablePredicate(BasicBlock block) + public static bool ExecutableBlocksPredicate(BasicBlock block) => block.IsExecutable; // Optimally arrange blocks, while eliminating non-executable blocks. public void ApplyPass(IRCodePart codePart) { if (Optimizer.OptimizationLevel >= OptimizationLevel.Minimal) - ApplyOrdering(codePart, IsExecutablePredicate); + ApplyOrdering(codePart, ExecutableBlocksPredicate); else - ApplyOrdering(codePart, InitialPredicate); + ApplyOrdering(codePart, AllBlocksPredicate); } // Optimally arrange blocks assuming that all are executable public static void ApplyInitialOrdering(IRCodePart codePart) - => ApplyOrdering(codePart, InitialPredicate); + => ApplyOrdering(codePart, AllBlocksPredicate); private static void ApplyOrdering(IRCodePart codePart, Func inclusionPredicate) { @@ -79,9 +77,9 @@ IEnumerable GetEdges(BasicBlock block) int branchIdx = instructions.Count - 1; if (branchIdx >= 0 && instructions[branchIdx] is IRBranch branch) { - if (!branch.True.IsExecutable) + if (!inclusionPredicate(branch.True)) instructions[branchIdx] = new IRJump(branch.Block, branch.False, branch.SourceLine, branch.SourceColumn); - else if (!branch.False.IsExecutable) + else if (!inclusionPredicate(branch.False)) instructions[branchIdx] = new IRJump(branch.Block, branch.True, branch.SourceLine, branch.SourceColumn); } } @@ -176,9 +174,6 @@ public static bool IdentifyBranch(BasicBlock branchingBlock, BasicBlock regionEx } rejoinsAt = FindLocalMerge(branchingBlock, regionExit); - //rejoinsAt = _branchingBlock.PostDominator; - //if (rejoinsAt == null || rejoinsAt is SyntheticReturnBlock) - //return false; if (elseBlock == rejoinsAt || GetSequenceEnd(elseBlock) == rejoinsAt) elseBlock = null; diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs b/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs index 2c0a36566..d3275d1e5 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using kOS.Safe.Compilation.IR; namespace kOS.Safe.Compilation.Optimization.Passes @@ -8,8 +7,7 @@ namespace kOS.Safe.Compilation.Optimization.Passes public class CommonExpressionElimination : IHolisticOptimizationPass { public OptimizationLevel OptimizationLevel => OptimizationLevel.Balanced; - - public short SortIndex => 1000; + public short SortIndex => 32100; public void ApplyPass(IRCodePart codePart) { @@ -24,7 +22,7 @@ private void EliminateCommonExpressions(BasicBlock rootBlock) { IdentifyExpressions(block, expressions); } - foreach (var expressionData in expressions.OrderBy(kvp => kvp.Value.sortIndex)) + foreach (KeyValuePair<(IResultingInstruction Expression, IRScope), ExpressionData> expressionData in expressions) { IResultingInstruction expression = expressionData.Key.Expression; ExpressionData data = expressionData.Value; @@ -59,7 +57,9 @@ private void EliminateCommonExpressions(BasicBlock rootBlock) }); foreach (IOperandInstructionBase use in data.uses) { - use.MutateEachOperand(op => new InterimResolvedReference(storedExpression.Definition, (IRInstruction)use)); + use.MutateEachOperand(op => + op.Equals(expression) ? + new InterimResolvedReference(storedExpression.Definition, (IRInstruction)use) : op); } } } @@ -67,7 +67,6 @@ private void IdentifyExpressions(BasicBlock block, Dictionary<(IResultingInstruc { foreach (IRInstruction instruction in block.Instructions) { - ushort sortIndex = 0; foreach (IRInstruction subexpression in instruction.DepthFirst()) { bool breaking = false; @@ -80,7 +79,7 @@ void AddToExpressions(IInterimOperand operand) if (expressions.TryGetValue((resultingInstruction, block.Scope), out ExpressionData data)) data.uses.Add((IOperandInstructionBase)subexpression); else - expressions[(resultingInstruction, block.Scope)] = new ExpressionData((IOperandInstructionBase)subexpression, sortIndex++); + expressions[(resultingInstruction, block.Scope)] = new ExpressionData((IOperandInstructionBase)subexpression); } } @@ -96,19 +95,16 @@ private class ExpressionComparer : IEqualityComparer<(IResultingInstruction Expr public static readonly ExpressionComparer Instance = new ExpressionComparer(); public bool Equals((IResultingInstruction Expression, IRScope Scope) x, (IResultingInstruction Expression, IRScope Scope) y) => x.Expression.Equals(y.Expression) && y.Scope.IsEqualOrEncompassedBy(x.Scope); - public int GetHashCode((IResultingInstruction Expression, IRScope Scope) obj) => obj.Expression.GetHashCode(); } private readonly struct ExpressionData { public readonly IOperandInstructionBase origin; - public readonly ushort sortIndex; public readonly List uses; - public ExpressionData(IOperandInstructionBase origin, ushort sortIndex) + public ExpressionData(IOperandInstructionBase origin) { this.origin = origin; - this.sortIndex = sortIndex; uses = new List(); } } @@ -142,9 +138,7 @@ public IInterimOperand Clone(BasicBlock block) public IEnumerable EmitOpcodes() { foreach (Opcode opcode in Value.EmitOpcodes()) - { yield return opcode; - } yield return new OpcodeDup(); yield return new OpcodeStoreLocal(Identifier); } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs index b17328e26..72e64500c 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs @@ -24,7 +24,7 @@ public void ApplyPass(IRCodePart codePart) private static void ApplyPass(BasicBlock root) { IEnumerable GetEdges(BasicBlock block) - => block.Successors.Where(BlockOrdering.InitialPredicate); + => block.Successors.Where(BlockOrdering.AllBlocksPredicate); List reversePostOrder = BasicBlock.GetReversePostOrder(root, GetEdges); Stack regionExits = new Stack(); From 6546259cd6bbcad3f326f105beaf63dc76d44e78 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 29 May 2026 23:40:10 -0400 Subject: [PATCH 107/120] Add documentation to the most recent optimization passes. --- .../Passes/AlgebraicSimplifications.cs | 3 + .../Optimization/Passes/BlockOrdering.cs | 69 ++++++++++++++++++- .../Passes/CommonExpressionElimination.cs | 37 ++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs b/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs index 6a380d718..659222d49 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs @@ -225,6 +225,8 @@ opR.Operation is OpcodeMathPower && private static IRBinaryOp DistributeMultiplication(IRBinaryOp binaryOp, IRBinaryOp opL, IRBinaryOp opR) { + // Rearrange equation to a standard form, then call + // the method to do the actual distribution. if (opR.Left.Equals(opL.Left)) { return DistributeMultiplication(binaryOp); @@ -248,6 +250,7 @@ private static IRBinaryOp DistributeMultiplication(IRBinaryOp binaryOp, IRBinary return binaryOp; return DistributeMultiplication(binaryOp); } + // Fallthrough to return the original operand without changes. return binaryOp; } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs index 3403eb198..4e930d1e7 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs @@ -11,12 +11,15 @@ public class BlockOrdering : IHolisticOptimizationPass, ILinkedOptimizationPass public short SortIndex => short.MaxValue; public Optimizer Optimizer { get; set; } + // These predicates can be used to filter basic blocks to + // all blocks or just those that are flagged as IsExecutable. public static bool AllBlocksPredicate(BasicBlock _) => true; public static bool ExecutableBlocksPredicate(BasicBlock block) => block.IsExecutable; - // Optimally arrange blocks, while eliminating non-executable blocks. + // Optimally arrange blocks, while eliminating non-executable blocks + // (if the optimization level is Balanced or above). public void ApplyPass(IRCodePart codePart) { if (Optimizer.OptimizationLevel >= OptimizationLevel.Minimal) @@ -26,10 +29,16 @@ public void ApplyPass(IRCodePart codePart) } - // Optimally arrange blocks assuming that all are executable + // Optimally arrange blocks assuming that all are executable. + // This method is publicly available outside the optimization pipeline. public static void ApplyInitialOrdering(IRCodePart codePart) => ApplyOrdering(codePart, AllBlocksPredicate); + /// + /// Applies the block ordering by overwriting the block list of each code part unit. + /// + /// The code part. + /// The inclusion predicate (all or executable blocks). private static void ApplyOrdering(IRCodePart codePart, Func inclusionPredicate) { foreach (IRCodePart.IRFunction function in codePart.Functions) @@ -40,8 +49,16 @@ private static void ApplyOrdering(IRCodePart codePart, Func in codePart.MainCode = ApplyOrdering(codePart.MainCode[0], inclusionPredicate); } + /// + /// Applies the ordering within a code part unit. + /// + /// The root block. + /// The inclusion predicate (all or executable). + /// The ordered list of blocks. + /// Root block isn't executable! public static List ApplyOrdering(BasicBlock root, Func inclusionPredicate) { + // Local function to get the edges to subsequent blocks. IEnumerable GetEdges(BasicBlock block) => block.Successors.Where(inclusionPredicate); @@ -50,12 +67,19 @@ IEnumerable GetEdges(BasicBlock block) if (!inclusionPredicate(root)) throw new Exceptions.KOSYouShouldNeverSeeThisException("Root block isn't executable!"); + // Find the last executable block and push it onto the stack + // of region exit blocks. List reversePostOrder = BasicBlock.GetReversePostOrder(root, GetEdges); Stack regionExits = new Stack(); regionExits.Push(reversePostOrder[reversePostOrder.Count - 1]); + // MetaBlockSequences are sequences of sequences of blocks. + // The first item will be the primary execution path. + // Subsequent items will be orphaned paths, like the 'else' body of branching statements. List metaSequences = new List(); + // This queue is used to process the root blocks of execution paths. + // It starts with the entry block (root) and will add orphaned paths root blocks. Queue sequenceStarts = new Queue(); sequenceStarts.Enqueue(root); @@ -67,8 +91,14 @@ IEnumerable GetEdges(BasicBlock block) metaSequences.Add(sequence); } + // Create a unified sequence of BasicBlocks. List results = metaSequences.SelectMany(s => s.Blocks).ToList(); + // This block is to avoid leaving branch instructions to blocks + // that aren't actually being emitted, since that will throw + // an exeption during block labelling. + // The Constant Folding pass does this more thoroughly, but + // if it is skipped, this becomes necessary. if (Optimizer.PassesToSkip.Contains(typeof(ConstantFolding))) { foreach (BasicBlock b in results) @@ -90,44 +120,67 @@ IEnumerable GetEdges(BasicBlock block) private static MetaBlockSequence ConstructMetaSequence(BasicBlock root, Stack regionExits, out Queue newOffshoots) { + // Create a new sequence with just the root block. MetaBlockSequence sequence = new MetaBlockSequence(root); BasicBlock block = root; newOffshoots = new Queue(); + // If the block is the next region exit (or null), + // that is the end of the current sequence. + // Note that region exits are part of the enclosing region's sequence. while (block != null && block != regionExits.Peek()) { + // Identify loops first because loop branches are subsets of branches. if (IdentifyLoop(block, regionExits.Peek(), out LoopData loopData)) { + // Push the next region exit. regionExits.Push(loopData.exit); + // Add the sequence from the loop's body. sequence.Add(ConstructMetaSequence(loopData.body, regionExits, out Queue childOffshoots)); + // That region is now popped. regionExits.Pop(); + // Enqueue any new offshoots. foreach (BasicBlock child in childOffshoots) newOffshoots.Enqueue(child); + // Set the exit block as the next block for this sequence. block = loopData.exit; } else if (IdentifyBranch(block, regionExits.Peek(), out BranchData branchData) && + // These checks are for edge cases of loop structures that are neither loops or branches. !((branchData.ifBlock?.Dominator != null && branchData.ifBlock.Dominator != block) || (branchData.elseBlock?.Dominator != null && branchData.elseBlock.Dominator != block))) { + // Add the 'if/then' block to the sequence. + // The branch instruction will skip ahead to the exit, so this ordering + // allows the 'if/then' block to fall through to the exit. sequence.Add(ConstructMetaSequence(branchData.ifBlock, regionExits, out Queue childOffshoots)); + // Enqueue any new offshoots. foreach (BasicBlock child in childOffshoots) newOffshoots.Enqueue(child); + // If the exit block is null, that's probably because the + // 'else' block was categorized as the exit. if (branchData.exit == null && branchData.elseBlock != null) { + // Set the 'exit' block as the next block for this sequence. block = branchData.elseBlock; } else { + // The 'else' block, if present, becomes a new offshoot. if (branchData.elseBlock != null) newOffshoots.Enqueue(branchData.elseBlock); + // Set the 'exit' block as the next block for this sequence. block = branchData.exit; } } + // This condition occurs only in branches/loops, or with a single successor. else if (block.PostDominator?.Dominator == block) { + // Set that successor as the next block for this sequence. block = block.PostDominator; } else return sequence; + // Add the next block to the sequence. if (block != null) sequence.Add(block); } @@ -137,6 +190,8 @@ private static MetaBlockSequence ConstructMetaSequence(BasicBlock root, Stack expressions = new Dictionary<(IResultingInstruction, IRScope), ExpressionData>(ExpressionComparer.Instance); + + // Populate the known expression in reverse post-order + // to ensure that the definition sites are before their + // first use. foreach (BasicBlock block in BasicBlock.GetReversePostOrder(rootBlock, BasicBlock.GetSuccessors)) { IdentifyExpressions(block, expressions); } + // Replace expressions when appropriate. foreach (KeyValuePair<(IResultingInstruction Expression, IRScope), ExpressionData> expressionData in expressions) { IResultingInstruction expression = expressionData.Key.Expression; ExpressionData data = expressionData.Value; + // Skip expressions that are not reused. int useCount = data.uses.Count; if (useCount == 0) continue; ushort opcodeCount = expression.OpcodeCount; + // Skip expressions that as fast to recompute. if (opcodeCount < 2) continue; + // Skip expressions that are not reused enough to + // be worth the cost of storing them. switch (opcodeCount) { case 2: @@ -44,8 +55,11 @@ private void EliminateCommonExpressions(BasicBlock rootBlock) break; } + // Flag to only replace the first usage (in case an + // instruction both defines and reuses an expression). bool first = true; CompilerTemporaryVariable storedExpression = new CompilerTemporaryVariable(expression, ((IRInstruction)data.origin).Block); + // Replace the first use with the temporary variable definition. data.origin.MutateEachOperand(op => { if (first && op.Equals(expression)) @@ -55,6 +69,7 @@ private void EliminateCommonExpressions(BasicBlock rootBlock) } return op; }); + // Replace subsequent uses with the temporary variable. foreach (IOperandInstructionBase use in data.uses) { use.MutateEachOperand(op => @@ -72,6 +87,9 @@ private void IdentifyExpressions(BasicBlock block, Dictionary<(IResultingInstruc bool breaking = false; void AddToExpressions(IInterimOperand operand) { + // Break from the subexpression loop if a + // non-invariant call is encountered so as + // to not 'optimize' away a call that does something. if (operand is IRCall call && !call.IsCallInvariant()) breaking = true; else if (operand is IResultingInstruction resultingInstruction) @@ -90,9 +108,14 @@ void AddToExpressions(IInterimOperand operand) } } } + // Custom equality comparer to ensure the scoping of the + // temporary variable is appropriate. private class ExpressionComparer : IEqualityComparer<(IResultingInstruction Expression, IRScope Scope)> { public static readonly ExpressionComparer Instance = new ExpressionComparer(); + // IRScope.IsEqualOrEncompassedBy(IRScope) is directional + // so the y.Scope.IsEqualOrEncompassedBy(x.Scope) is a specific + // choice to work with how the dictionary keys are checked. public bool Equals((IResultingInstruction Expression, IRScope Scope) x, (IResultingInstruction Expression, IRScope Scope) y) => x.Expression.Equals(y.Expression) && y.Scope.IsEqualOrEncompassedBy(x.Scope); public int GetHashCode((IResultingInstruction Expression, IRScope Scope) obj) @@ -117,6 +140,9 @@ public class CompilerTemporaryVariable : IResultingInstruction, IOperandInstruct public SSASetDefinition Definition { get; } public string Identifier { get; } public ushort OpcodeCount => (ushort)(Value.OpcodeCount + 2); + // If this were invariant, it would already be replaced in + // constant folding. Always return false to protect it from + // being folded later. public bool IsInvariant => false; public Type Type => Value.Type; @@ -124,23 +150,34 @@ public CompilerTemporaryVariable(IResultingInstruction value, BasicBlock block) { Value = value; ushort id; + // Wrapping is fine - no one should have more than + // 65,000 re-used expressions in a single scope. unchecked { id = nextID++; } + // This identified is impossible for a user to + // create in a .ks file. Identifier = $"$compiler.Temp.{id}"; Definition = new SSASetDefinition(Identifier, new IRAssign(block, new OpcodeStoreLocal(Identifier), value)); } + // This implementation of Clone gives the original + // expression back because the scope can't be assured. public IInterimOperand Clone(BasicBlock block) => Value.Clone(block); public IEnumerable EmitOpcodes() { + // Emit the original expression. foreach (Opcode opcode in Value.EmitOpcodes()) yield return opcode; + // Duplicate it's result on the stack. yield return new OpcodeDup(); + // Store one of those copies. yield return new OpcodeStoreLocal(Identifier); + // The remaining copy is consumed by the user of the + // original point of this expression. } public bool Equals(IInterimOperand other) From 2208b4153dd5275c744277f68572904ea4747926 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 30 May 2026 19:03:00 -0400 Subject: [PATCH 108/120] Fix skipping passes not working. Improve unit test readability and robustness. --- .../Execution/OptimizationTest.cs | 42 +++++++++++-------- .../Compilation/Optimization/Optimizer.cs | 7 ++++ .../Passes/SCCPWithTypePropagation.cs | 3 +- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index a484142a8..7bdaf0110 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -12,7 +12,8 @@ namespace kOS.Safe.Test.Execution public class OptimizationTest : BaseIntegrationTest { protected override OptimizationLevel OptimizationLevel => optimizationLevel; - protected OptimizationLevel optimizationLevel = OptimizationLevel.Minimal; + protected OptimizationLevel optimizationLevel = OptimizationLevel.Balanced; + protected OptimizationLevel resetOptimizationLevel = OptimizationLevel.Balanced; protected List CompileCodePart(string fileName) { @@ -47,13 +48,26 @@ protected List CompileCodePart(string fileName) return result.MainCode; } + protected void EnsureAtLeastLevel(OptimizationLevel level) + { + if (optimizationLevel < level) + optimizationLevel = level; + } + + [SetUp] + public void ResetOptimizationBases() + { + BasicBlock.ResetNextID(); + optimizationLevel = resetOptimizationLevel; + Safe.Compilation.Optimization.Optimizer.PassesToSkip.Clear(); + } + #region Holistic Tests [Test] public void TestSSA() { optimizationLevel = OptimizationLevel.None; List _code = CompileCodePart("integration/blank.ks"); - optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); BasicBlock block = codePart.MainCode[0]; List instructions = new List @@ -832,14 +846,14 @@ public void TestUntilLoopDetection() BasicBlock.ResetNextID(); optimizationLevel = OptimizationLevel.None; List _code = CompileCodePart("integration/branching/until.ks"); - optimizationLevel = OptimizationLevel.Minimal; + ResetOptimizationBases(); IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(codePart.MainCode[1], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.LoopData loopData); Assert.AreEqual(2, loopData.body?.ID); Assert.AreEqual(4, loopData.exit?.ID); Assert.AreEqual(1, codePart.MainCode. Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(b, null, out _)).Count()); - var optimizer = new Safe.Compilation.Optimization.Optimizer(new CompilerOptions() { OptimizationLevel = optimizationLevel }); + var optimizer = new Safe.Compilation.Optimization.Optimizer(new CompilerOptions() { OptimizationLevel = OptimizationLevel }); optimizer.Optimize(codePart); } [Test] @@ -848,7 +862,6 @@ public void TestForLoopDetection() BasicBlock.ResetNextID(); optimizationLevel = OptimizationLevel.None; List _code = CompileCodePart("integration/branching/for.ks"); - optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(codePart.MainCode[2], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.LoopData loopData); Assert.AreEqual(3, loopData.body?.ID); @@ -862,7 +875,6 @@ public void TestFromLoopDetection() BasicBlock.ResetNextID(); optimizationLevel = OptimizationLevel.None; List _code = CompileCodePart("integration/branching/from.ks"); - optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(codePart.MainCode[2], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.LoopData loopData); Assert.AreEqual(3, loopData.body?.ID); @@ -876,7 +888,6 @@ public void TestIfDetection() BasicBlock.ResetNextID(); optimizationLevel = OptimizationLevel.None; List _code = CompileCodePart("integration/branching/if.ks"); - optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(codePart.MainCode[0], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.BranchData branchData); Assert.AreEqual(1, branchData.ifBlock?.ID); @@ -891,7 +902,6 @@ public void TestIfElseDetection() BasicBlock.ResetNextID(); optimizationLevel = OptimizationLevel.None; List _code = CompileCodePart("integration/branching/ifElse.ks"); - optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(codePart.MainCode[0], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.BranchData branchData); Assert.AreEqual(1, branchData.ifBlock?.ID); @@ -906,7 +916,6 @@ public void TestIfElseIfDetection() BasicBlock.ResetNextID(); optimizationLevel = OptimizationLevel.None; List _code = CompileCodePart("integration/branching/ifElseIf.ks"); - optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(codePart.MainCode[0], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.BranchData branchData); Assert.AreEqual(1, branchData.ifBlock?.ID); @@ -925,7 +934,6 @@ public void TestIfElseIfElseDetection() BasicBlock.ResetNextID(); optimizationLevel = OptimizationLevel.None; List _code = CompileCodePart("integration/branching/ifElseIfElse.ks"); - optimizationLevel = OptimizationLevel.Minimal; IRCodePart codePart = new IRCodePart(_code[0], new List(), new List()); Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(codePart.MainCode[0], null, out Safe.Compilation.Optimization.Passes.BlockOrdering.BranchData branchData); Assert.AreEqual(1, branchData.ifBlock?.ID); @@ -944,9 +952,8 @@ public void TestIfElseIfElseDetection() [Test] public void TestUntilLoopCondition() { - optimizationLevel = OptimizationLevel.Balanced; + EnsureAtLeastLevel(OptimizationLevel.Balanced); List _code = CompileCodePart("integration/branching/until.ks"); - optimizationLevel = OptimizationLevel.Minimal; List opcodes = _code[0].MainCode; Assert.AreEqual(opcodes[4].ToString(), opcodes[10].ToString()); Assert.IsInstanceOf(typeof(OpcodeBranchIfTrue), opcodes[5]); @@ -955,9 +962,8 @@ public void TestUntilLoopCondition() [Test] public void TestForLoopCondition() { - optimizationLevel = OptimizationLevel.Balanced; + EnsureAtLeastLevel(OptimizationLevel.Balanced); List _code = CompileCodePart("integration/branching/for.ks"); - optimizationLevel = OptimizationLevel.Minimal; List opcodes = _code[0].MainCode; Assert.AreEqual(opcodes[8].ToString(), opcodes[15].ToString()); Assert.AreEqual(opcodes[9].ToString(), opcodes[16].ToString()); @@ -967,9 +973,10 @@ public void TestForLoopCondition() [Test] public void TestFromLoopCondition() { - optimizationLevel = OptimizationLevel.Balanced; + EnsureAtLeastLevel(OptimizationLevel.Balanced); + Safe.Compilation.Optimization.Optimizer.PassesToSkip.Add(typeof(Safe.Compilation.Optimization.Passes.ConstantFolding)); + Safe.Compilation.Optimization.Optimizer.PassesToSkip.Add(typeof(Safe.Compilation.Optimization.Passes.SCCPWithTypePropagation)); List _code = CompileCodePart("integration/branching/from.ks"); - optimizationLevel = OptimizationLevel.Minimal; List opcodes = _code[0].MainCode; Assert.AreEqual(opcodes[7].ToString(), opcodes[19].ToString()); Assert.AreEqual(opcodes[8].ToString(), opcodes[20].ToString()); @@ -983,7 +990,7 @@ public void TestFromLoopCondition() public void TestCommonExpressionElimination() { // Test that common expressions are eliminated - optimizationLevel = OptimizationLevel.Balanced; + EnsureAtLeastLevel(OptimizationLevel.Balanced); RunScript("integration/commonExpressionElimination.ks"); RunSingleStep(); AssertOutput( @@ -1001,7 +1008,6 @@ public void TestCommonExpressionElimination() optimizationLevel = OptimizationLevel.None; List codePart = CompileCodePart("integration/commonExpressionElimination.ks"); List result = GetOpcodesAfterOptimization(codePart[0].MainCode, OptimizationLevel.Balanced, false); - optimizationLevel = OptimizationLevel.Minimal; Assert.IsInstanceOf(result[18]); Assert.IsInstanceOf(result[25]); diff --git a/src/kOS.Safe/Compilation/Optimization/Optimizer.cs b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs index 9c5e12338..6dd6f9b99 100644 --- a/src/kOS.Safe/Compilation/Optimization/Optimizer.cs +++ b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs @@ -103,6 +103,13 @@ public List Optimize(IRCodePart codePart) if (pass.OptimizationLevel > OptimizationLevel) continue; + if (PassesToSkip.Contains(pass.GetType())) + { + if (!(pass is Passes.SCCPWithTypePropagation || + pass is SingleStaticAssignment)) + continue; + } + SafeHouse.Logger.Log($"Applying optimization pass: {pass.GetType()}."); switch (pass) { diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index 988578217..bbf8ea3c8 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -25,7 +25,8 @@ public void ApplyPass(IRCodePart codePart) { codePart.VariableUses = MapUsesAndPropagateTypes(codePart); - if (Optimizer.OptimizationLevel == OptimizationLevel.None) + if (Optimizer.OptimizationLevel == OptimizationLevel.None || + Optimizer.PassesToSkip.Contains(typeof(SCCPWithTypePropagation))) return; HashSet usedDefinitions = PropagateConstants(codePart.VariableUses); From 4853178beb8f34b05fb679d01ceb420f8691eedd Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 30 May 2026 19:04:04 -0400 Subject: [PATCH 109/120] Fix propagation of variables through branches and loops being lost and broken. --- .../Passes/SCCPWithTypePropagation.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs index bbf8ea3c8..e4ac9a3ed 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -53,7 +53,7 @@ private static Dictionary> MapUs Dictionary> variableUses = new Dictionary>(SSADefinition.ReferenceEqualityComparer); HashSet visitedBlocks = new HashSet(); - Dictionary typeAndInvarianceCache = new Dictionary(); + Dictionary typeAndInvarianceCache = new Dictionary(SSADefinition.ReferenceEqualityComparer); // Apply the algorithm starting from each entry block. foreach (BasicBlock root in codePart.RootBlocks) @@ -173,8 +173,8 @@ operandInstruction is IRAssign assignment && if (instruction is IRAssign assignment) foreach (IOperandInstructionBase use in GetOrCreate(variableUses, assignment.Target)) instructionQueue.Enqueue(use); - else if (instruction is PhiVariable phi) - foreach (IOperandInstructionBase use in GetOrCreate(variableUses, phi)) + else if (instruction is PhiNode phi) + foreach (IOperandInstructionBase use in GetOrCreate(variableUses, phi.Result)) instructionQueue.Enqueue(use); } } @@ -211,7 +211,7 @@ private static bool VisitInstruction(IOperandInstructionBase instruction, SSASetDefinition ssaVariable = assignment.Target; if (typeAndInvarianceCache.TryGetValue(ssaVariable, out (Type storedType, bool storedInvariance) cached)) { - typeAndInvarianceCache[ssaVariable] = (ssaVariable.Type, cached.storedInvariance &= ssaVariable.IsInvariant); + typeAndInvarianceCache[ssaVariable] = (ssaVariable.Type, cached.storedInvariance & ssaVariable.IsInvariant); return cached != typeAndInvarianceCache[ssaVariable]; } else @@ -220,16 +220,16 @@ private static bool VisitInstruction(IOperandInstructionBase instruction, return true; } } - else if (instruction is PhiVariable phi) + else if (instruction is PhiNode phi) { - if (typeAndInvarianceCache.TryGetValue(phi, out (Type storedType, bool storedInvariance) cached)) + if (typeAndInvarianceCache.TryGetValue(phi.Result, out (Type storedType, bool storedInvariance) cached)) { - typeAndInvarianceCache[phi] = (phi.Type, cached.storedInvariance &= phi.IsInvariant); - return cached != typeAndInvarianceCache[phi]; + typeAndInvarianceCache[phi.Result] = (phi.Type, cached.storedInvariance & phi.IsInvariant); + return cached != typeAndInvarianceCache[phi.Result]; } else { - typeAndInvarianceCache[phi] = (phi.Type, phi.IsInvariant); + typeAndInvarianceCache[phi.Result] = (phi.Type, phi.IsInvariant); return true; } } From 4030f7369f5b423066a2a2d17db99c309e178be7 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 30 May 2026 19:04:50 -0400 Subject: [PATCH 110/120] Remove unnecessary pre-conversion to SSA. --- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index 605a88ed7..d28e00fd6 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -91,8 +91,6 @@ public IRCodePart(CodePart codePart, List userFunctions, List t.RootBlock)); RootBlocks.AddRange(Functions.SelectMany(f => f.RootBlocks)); - - SingleStaticAssignment.FinalizeSSA(this); } /// From e68c0387934090d026502db8451fdc79f034367c Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 30 May 2026 19:06:12 -0400 Subject: [PATCH 111/120] Fix opcode labelling being broken after cloning an instruction of certain types. --- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 66 +++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 7bd31ce52..fb792fcf6 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -284,7 +284,37 @@ public bool SwapOperands() } public IInterimOperand Clone(BasicBlock block) - => new IRBinaryOp(block, (BinaryOpcode)SetSourceLocation(Operation), Left.Clone(block), Right.Clone(block)); + => new IRBinaryOp(block, (BinaryOpcode)SetSourceLocation(CloneOperation()), Left.Clone(block), Right.Clone(block)); + private BinaryOpcode CloneOperation() + { + switch (Operation) + { + case OpcodeMathAdd _: + return new OpcodeMathAdd(); + case OpcodeMathSubtract _: + return new OpcodeMathSubtract(); + case OpcodeMathMultiply _: + return new OpcodeMathMultiply(); + case OpcodeMathDivide _: + return new OpcodeMathDivide(); + case OpcodeMathPower _: + return new OpcodeMathPower(); + case OpcodeCompareEqual _: + return new OpcodeCompareEqual(); + case OpcodeCompareNE _: + return new OpcodeCompareNE(); + case OpcodeCompareGT _: + return new OpcodeCompareGT(); + case OpcodeCompareLT _: + return new OpcodeCompareGT(); + case OpcodeCompareGTE _: + return new OpcodeCompareGTE(); + case OpcodeCompareLTE _: + return new OpcodeCompareLTE(); + default: + throw new NotImplementedException(); + } + } public override IEnumerable EmitOpcodes() { @@ -387,7 +417,23 @@ public IRUnaryOp(BasicBlock block, Opcode operation, IInterimOperand operand) : } public IInterimOperand Clone(BasicBlock block) - => new IRUnaryOp(block, Operation, Operand.Clone(block)); + => new IRUnaryOp(block, SetSourceLocation(CloneOperation()), Operand.Clone(block)); + private Opcode CloneOperation() + { + switch (Operation) + { + case OpcodeExists _: + return new OpcodeExists(); + case OpcodeLogicNot _: + return new OpcodeLogicNot(); + case OpcodeLogicToBool _: + return new OpcodeLogicToBool(); + case OpcodeMathNegate _: + return new OpcodeMathNegate(); + default: + throw new NotImplementedException(); + } + } public override IEnumerable EmitOpcodes() { foreach (Opcode opcode in Operand.EmitOpcodes()) @@ -529,7 +575,21 @@ public IRNonVarPush(BasicBlock block, Opcode opcode) : base(opcode, block) Operation = opcode; } public IInterimOperand Clone(BasicBlock block) - => new IRNonVarPush(block, Operation); + => new IRNonVarPush(block, CloneOperation()); + private Opcode CloneOperation() + { + switch (Operation) + { + case OpcodeTestArgBottom _: + return new OpcodeTestArgBottom() + { + SourceLine = SourceLine, + SourceColumn = SourceColumn + }; + default: + throw new NotImplementedException(); + } + } public override IEnumerable EmitOpcodes() { Operation.Label = string.Empty; From 4306ef23e3922b8d3527669eb2ef6d983a2058bb Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 30 May 2026 19:07:03 -0400 Subject: [PATCH 112/120] Replace the iterator 'magic string' with one that can be externally referenced. --- src/kOS.Safe/Compilation/KS/Compiler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index 5e1a4d8f1..79fa536b9 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -3435,6 +3435,7 @@ private void VisitShutdownStatement(ParseNode node) AddOpcode(new OpcodePop()); // all functions now return a value even if we ignore it. Not sure it matters in the case of shutdown() though. } + public const string iteratorSuffix = "-iterator"; private void VisitForStatement(ParseNode node) { NodeStartHousekeeping(node); @@ -3442,7 +3443,7 @@ private void VisitForStatement(ParseNode node) bool remember = nowInALoop; nowInALoop = true; - string iteratorIdentifier = "$" + GetIdentifierText(node.Nodes[3]) + "-iterator"; + string iteratorIdentifier = "$" + GetIdentifierText(node.Nodes[3]) + iteratorSuffix; // Add a scope level to hold the iterator variable. This will live just "outside" the // brace scope of the function body. From 382bd56f5d38474c8edb0c09eb334aaf559d0922 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 30 May 2026 19:08:38 -0400 Subject: [PATCH 113/120] Fix infinite loop in loop detection (ironic). Make helper methods publicly accessible. --- .../Passes/LoopConditionalRelocation.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs index 72e64500c..3835ca33a 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs @@ -21,18 +21,16 @@ public void ApplyPass(IRCodePart codePart) ApplyPass(codePart.MainCode[0]); } + private static IEnumerable GetEdges(BasicBlock block) + => block.Successors.Where(BlockOrdering.AllBlocksPredicate); + private static void ApplyPass(BasicBlock root) { - IEnumerable GetEdges(BasicBlock block) - => block.Successors.Where(BlockOrdering.AllBlocksPredicate); - List reversePostOrder = BasicBlock.GetReversePostOrder(root, GetEdges); Stack regionExits = new Stack(); regionExits.Push(reversePostOrder[reversePostOrder.Count - 1]); - List loopData = new List(); - - FindLoops(root, regionExits, loopData); + List loopData = FindLoops(root, regionExits); foreach (BlockOrdering.LoopData loop in loopData) { @@ -57,6 +55,12 @@ private static void RelocateConditional(BlockOrdering.LoopData loopData) block.RemoveSuccessor(header); } + public static List FindLoops(BasicBlock root, Stack regionExits) + { + List loopData = new List(); + FindLoops(root, regionExits, loopData); + return loopData; + } private static void FindLoops(BasicBlock root, Stack regionExits, List loops) { BasicBlock block = root; @@ -70,7 +74,10 @@ private static void FindLoops(BasicBlock root, Stack regionExits, Li regionExits.Pop(); block = loopData.exit; } - else if (BlockOrdering.IdentifyBranch(block, regionExits.Peek(), out BlockOrdering.BranchData branchData)) + else if (BlockOrdering.IdentifyBranch(block, regionExits.Peek(), out BlockOrdering.BranchData branchData) && + //These checks are for edge cases of loop-like structures that are neither loops themselves or branches. + !((branchData.ifBlock?.Dominator != null && branchData.ifBlock.Dominator != block) || + (branchData.elseBlock?.Dominator != null && branchData.elseBlock.Dominator != block))) { FindLoops(branchData.ifBlock, regionExits, loops); if (branchData.exit == null && branchData.elseBlock != null) From 3af6d228e3b9a043e1719efd3209cd4f1fc8505d Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 30 May 2026 19:09:04 -0400 Subject: [PATCH 114/120] Make helper method publicly accessible. --- src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs index 4e930d1e7..7dca3ff52 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs @@ -280,7 +280,7 @@ private static BasicBlock FindLocalMerge(BasicBlock ifBlock, BasicBlock regionEx // If we've walked all the way out, there is no local merge return candidate == regionExit ? null : candidate; } - private static bool IsInsideRegion(BasicBlock candidate, BasicBlock regionExit) + public static bool IsInsideRegion(BasicBlock candidate, BasicBlock regionExit) { // Walk the post-dominator chain of regionExit upward. // If we encounter candidate, it is outside or on the boundary. From 77cf9d699e6b4b3e27d338dea133e7f71b4e94d9 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 30 May 2026 19:11:38 -0400 Subject: [PATCH 115/120] Add boolean 'Any' and 'All' methods to IOperandInstructionBase. --- .../Compilation/IR/IOperandInstructionBase.cs | 14 +++++++ src/kOS.Safe/Compilation/IR/IRInstruction.cs | 22 +++++++++++ .../Compilation/IR/InterimVariables.cs | 39 +++++++++++-------- .../Passes/CommonExpressionElimination.cs | 12 ++++++ 4 files changed, 70 insertions(+), 17 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs index df2a14634..e54ee23fd 100644 --- a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs +++ b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs @@ -18,6 +18,20 @@ public interface IOperandInstructionBase /// /// The mutation function. void MutateEachOperand(Func mutateFunc); + + /// + /// Checks if any operand meets the supplied predicate. + /// + /// The predicate. + /// true if any operand matches the predicate, otherwise false. + bool AnyOperand(Func predicate); + + /// + /// Checks if all operands meet the supplied predicate. + /// + /// The predicate. + /// true if all operands match the predicate, otherwise false. + bool AllOperands(Func predicate); } /// diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index fb792fcf6..aecef83e8 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -58,6 +58,12 @@ public void ForEachOperand(Action action) public void MutateEachOperand(Func mutateFunc) => operand = mutateFunc(operand); + + public bool AnyOperand(Func predicate) + => predicate(operand); + + public bool AllOperands(Func predicate) + => predicate(operand); } public abstract class MultipleOperandInstruction : IRInstruction, IMultipleOperandInstruction { @@ -83,6 +89,22 @@ public void MutateEachOperand(Func mutateFunc) for (int i = 0; i < OperandCount; i++) this[i] = mutateFunc(this[i]); } + + public bool AnyOperand(Func predicate) + { + for (int i = 0; i < OperandCount; i++) + if (predicate(this[i])) + return true; + return false; + } + + public bool AllOperands(Func predicate) + { + for (int i = 0; i < OperandCount; i++) + if (!predicate(this[i])) + return false; + return true; + } } public class IRAssign : SingleOperandInstruction diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs index b946bebf8..1554134f5 100644 --- a/src/kOS.Safe/Compilation/IR/InterimVariables.cs +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -466,19 +466,25 @@ public override SSADefinition PotentiallyUnset(IRUnset potentiallyUnsetAt, bool public void ForEachOperand(Action action) { - action(new InterimResolvedReference(Preceding, 0, 0)); - action(new InterimResolvedReference(Succeeding, 0, 0)); + action(new InterimResolvedReference(Preceding, -1, -1)); + action(new InterimResolvedReference(Succeeding, -1, -1)); } public void MutateEachOperand(Func mutateFunc) { Preceding = Mutate(mutateFunc, Preceding); Succeeding = Mutate(mutateFunc, Succeeding); } + public bool AnyOperand(Func predicate) + => EvaluatePredicate(predicate, Preceding) || EvaluatePredicate(predicate, Succeeding); + public bool AllOperands(Func predicate) + => EvaluatePredicate(predicate, Preceding) && EvaluatePredicate(predicate, Succeeding); private static SSADefinition Mutate(Func func, SSADefinition definition) { - IInterimOperand result = func(new InterimResolvedReference(definition, 0, 0)); + IInterimOperand result = func(new InterimResolvedReference(definition, -1, -1)); return ((InterimResolvedReference)result).Reference; } + private static bool EvaluatePredicate(Func predicate, SSADefinition definition) + => predicate(new InterimResolvedReference(definition, -1, -1)); public override InterimConstantValue Evaluate() { if (!IsInvariant) @@ -607,17 +613,13 @@ public PhiNode(string name) protected override InterimConstantValue EvaluateObj(SSADefinition obj) => obj.Evaluate(); - protected override void ForEachOperand(Action action) - { - foreach (SSADefinition variable in PossibleValues.Values) - action(new InterimResolvedReference(variable, -1, -1)); - } - protected override void MutateEachOperand(Func mutateFunc) { foreach (BasicBlock block in PossibleValues.Keys) PossibleValues[block] = ((InterimResolvedReference)mutateFunc(new InterimResolvedReference(PossibleValues[block], -1, -1))).Reference; } + protected override IInterimOperand ValueAsOperand(SSADefinition item) + => new InterimResolvedReference(item, -1, -1); } public class PhiOperand : PhiNode, IEvaluatableToConstant @@ -638,16 +640,13 @@ public override InterimConstantValue Evaluate() protected override InterimConstantValue EvaluateObj(IInterimOperand obj) => (obj as IEvaluatableToConstant)?.Evaluate(); - protected override void ForEachOperand(Action action) - { - foreach (IInterimOperand operand in PossibleValues.Values) - action(operand); - } protected override void MutateEachOperand(Func mutateFunc) { foreach (BasicBlock block in PossibleValues.Keys) PossibleValues[block] = mutateFunc(PossibleValues[block]); } + protected override IInterimOperand ValueAsOperand(IInterimOperand item) + => item; } public abstract class PhiNode : IMultipleOperandInstruction { @@ -719,11 +718,17 @@ public static Type GetFirstCommonBaseType(Type typeA, Type typeB) } void IOperandInstructionBase.ForEachOperand(Action action) - => ForEachOperand(action); - protected abstract void ForEachOperand(Action action); - + { + foreach (T item in PossibleValues.Values) + action(ValueAsOperand(item)); + } void IOperandInstructionBase.MutateEachOperand(Func mutateFunc) => MutateEachOperand(mutateFunc); protected abstract void MutateEachOperand(Func mutateFunc); + bool IOperandInstructionBase.AnyOperand(Func predicate) + => PossibleValues.Values.Select(ValueAsOperand).Any(predicate); + bool IOperandInstructionBase.AllOperands(Func predicate) + => PossibleValues.Values.Select(ValueAsOperand).All(predicate); + protected abstract IInterimOperand ValueAsOperand(T item); } } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs b/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs index ff436aa3c..065bd10a4 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs @@ -196,6 +196,18 @@ public void MutateEachOperand(Func mutateFunc) if (Value is IOperandInstructionBase operandInstruction) operandInstruction.MutateEachOperand(mutateFunc); } + public bool AnyOperand(Func predicate) + { + if (Value is IOperandInstructionBase operandInstruction) + return operandInstruction.AnyOperand(predicate); + return false; + } + public bool AllOperands(Func predicate) + { + if (Value is IOperandInstructionBase operandInstruction) + return operandInstruction.AllOperands(predicate); + return false; + } public override string ToString() => Definition.ToString(); From defdd0a6d26e3b70212c6c21342a2dd944e6a572 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 31 May 2026 12:52:26 -0400 Subject: [PATCH 116/120] Implement the concept of a function or instruction being 'inert' - not affecting other objects or the simulation state - as separate from being 'invariant' - where the result of the function or operation can be known at compile time. Invariant operands or functions can be replaced by their result - invariant functions or operations can be moved within the code without effect. --- .../Compilation/IR/IActionInstruction.cs | 15 ++++ src/kOS.Safe/Compilation/IR/IRCodePart.cs | 43 ++++----- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 90 +++++++++++++------ .../Compilation/IR/IResultingInstruction.cs | 12 +-- .../Compilation/IR/InterimVariables.cs | 2 +- .../Passes/CommonExpressionElimination.cs | 2 +- src/kOS.Safe/Encapsulation/Lexicon.cs | 2 +- src/kOS.Safe/Encapsulation/ListValue.cs | 2 +- src/kOS.Safe/Encapsulation/PIDLoop.cs | 2 +- src/kOS.Safe/Encapsulation/QueueValue.cs | 2 +- src/kOS.Safe/Encapsulation/StackValue.cs | 2 +- src/kOS.Safe/Encapsulation/UniqueSetValue.cs | 2 +- src/kOS.Safe/Function/FunctionAttribute.cs | 23 +++++ src/kOS.Safe/Function/FunctionManager.cs | 8 ++ src/kOS.Safe/Function/IFunctionManager.cs | 1 + src/kOS.Safe/Function/Math.cs | 28 +++--- src/kOS.Safe/Function/Misc.cs | 28 +++--- src/kOS.Safe/Function/Persistence.cs | 32 +++---- src/kOS.Safe/Function/Suffixed.cs | 4 +- src/kOS.Safe/Function/Trigonometry.cs | 16 ++-- src/kOS.Safe/Persistence/PathValue.cs | 2 +- src/kOS.Safe/Persistence/Volume.cs | 2 +- .../AddOns/KerbalAlarmClock/KACFunctions.cs | 6 +- src/kOS/Function/BuildList.cs | 2 +- src/kOS/Function/Math.cs | 8 +- src/kOS/Function/Misc.cs | 14 +-- src/kOS/Function/Persistence.cs | 2 +- src/kOS/Function/PrintList.cs | 2 +- src/kOS/Function/Suffixed.cs | 76 ++++++++-------- 29 files changed, 251 insertions(+), 179 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/IActionInstruction.cs diff --git a/src/kOS.Safe/Compilation/IR/IActionInstruction.cs b/src/kOS.Safe/Compilation/IR/IActionInstruction.cs new file mode 100644 index 000000000..08e0297e9 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IActionInstruction.cs @@ -0,0 +1,15 @@ +namespace kOS.Safe.Compilation.IR +{ + public interface IActionInstruction + { + /// + /// Gets a value indicating whether this instruction affects + /// the wider simulation state or changes the value of an + /// object. + /// + /// + /// true if this instruction is inert; otherwise, false. + /// + bool IsInert { get; } + } +} diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index d28e00fd6..132b0d17a 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using kOS.Safe.Compilation.KS; +using kOS.Safe.Compilation.Optimization; namespace kOS.Safe.Compilation.IR { @@ -363,30 +364,30 @@ public PhiOperand Returns /// public bool IsInvariant => Returns.IsInvariant && - IsSelfInvariant() && - FunctionCalls.Where(func => func != this).All(function => function.IsSelfInvariant()); - - private bool IsSelfInvariant() + FunctionCalls.Where(f => f != this).All(function => function.Returns.IsInvariant); + public bool IsInert + => IsSelfInert && + FunctionCalls.Where(f => f != this).All(function => function.IsSelfInert); + private bool IsSelfInert { - if (ExternalWrites.Count > 0 || ExternalUnsets.Count > 0) - return false; + get + { + if (ExternalWrites.Count > 0 || ExternalUnsets.Count > 0) + return false; - return Fragments.All(fragment => - fragment.FunctionCode.Where(block => block.IsExecutable).All(block => - block.Instructions.All(instruction => - { - switch (instruction) + return Fragments.All(fragment => + fragment.FunctionCode.Where(block => block.IsExecutable).All(block => + block.Instructions.All(instruction => { - case IRNoStackInstruction noStackInstruction: - case IRSuffixSet _: - case IRIndexSet _: - return instruction.IsInvariant; - default: - return true; - } - }) - ) - ); + foreach (IRInstruction operation in instruction.DepthFirst()) + if (operation is IActionInstruction actionInstruction && + !actionInstruction.IsInert) + return false; + return true; + }) + ) + ); + } } public List RootBlocks { get; } = new List(); diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index aecef83e8..dab456caa 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -11,6 +11,14 @@ public abstract class IRInstruction public short SourceLine { get; private set; } // line number in the source code that this was compiled from. public short SourceColumn { get; private set; } // column number of the token nearest the cause of this Opcode. + /// + /// Gets a value indicating whether this instance is invariant. + /// That is, if the effects and result of the operation can + /// be known at compile time. + /// + /// + /// true if this instance is invariant; otherwise, false. + /// public abstract bool IsInvariant { get; } public abstract IEnumerable EmitOpcodes(); protected IRInstruction(Opcode originalOpcode, BasicBlock block) @@ -107,7 +115,7 @@ public bool AllOperands(Func predicate) } } - public class IRAssign : SingleOperandInstruction + public class IRAssign : SingleOperandInstruction, IActionInstruction { public enum StoreScope { @@ -116,6 +124,7 @@ public enum StoreScope Global } public override bool IsInvariant => Value.IsInvariant; + public bool IsInert { get; set; } public SSASetDefinition Target { get; set; } public IInterimOperand Value { get => operand; set => operand = value; } public StoreScope Scope { get; set; } = StoreScope.Ambivalent; @@ -505,14 +514,15 @@ public InterimConstantValue Evaluate() } } } - public class IRNoStackInstruction : IRInstruction + public class IRNoStackInstruction : IRInstruction, IActionInstruction { - public override bool IsInvariant { get; } = false; + public override bool IsInvariant => true; + public bool IsInert { get; } = false; public Opcode Operation { get; } public IRNoStackInstruction(BasicBlock block, Opcode opcode) : base(opcode, block) => Operation = opcode; - public IRNoStackInstruction(BasicBlock block, Opcode opcode, bool isInvariant) : this(block, opcode) - => IsInvariant = isInvariant; + public IRNoStackInstruction(BasicBlock block, Opcode opcode, bool isInert) : this(block, opcode) + => IsInert = isInert; public override IEnumerable EmitOpcodes() { Operation.Label = string.Empty; @@ -521,10 +531,11 @@ public override IEnumerable EmitOpcodes() public override string ToString() => Operation.ToString(); } - public class IRUnaryConsumer : SingleOperandInstruction + public class IRUnaryConsumer : SingleOperandInstruction, IActionInstruction { private readonly bool operationHasSideEffects; - public override bool IsInvariant => !operationHasSideEffects && Operand.IsInvariant; + public override bool IsInvariant => Operand.IsInvariant; + public bool IsInert => !operationHasSideEffects; public Opcode Operation { get; } public IInterimOperand Operand { get => operand; set => operand = value; } public IRUnaryConsumer(BasicBlock block, Opcode opcode, IInterimOperand operand, bool sideEffects = false) : base(opcode, block) @@ -543,12 +554,12 @@ public override IEnumerable EmitOpcodes() public override string ToString() => Operation.ToString(); } - public class IRUnset : IRUnaryConsumer + public class IRUnset : IRUnaryConsumer, IActionInstruction { public override bool IsInvariant => Target != null; public SSASetDefinition Target { get; set; } public bool IsExecutable => Block.IsExecutable; - public IRUnset(BasicBlock block, OpcodeUnset opcode, IInterimOperand operand) : base(block, opcode, operand, false) + public IRUnset(BasicBlock block, OpcodeUnset opcode, IInterimOperand operand) : base(block, opcode, operand, true) { if (operand.IsInvariant) { @@ -557,9 +568,10 @@ public IRUnset(BasicBlock block, OpcodeUnset opcode, IInterimOperand operand) : } } } - public class IRPop : SingleOperandInstruction + public class IRPop : SingleOperandInstruction, IActionInstruction { public override bool IsInvariant => Value.IsInvariant; + public bool IsInert => true; public IInterimOperand Value { get => operand; set => operand = value; } public IRPop(BasicBlock block, IInterimOperand value, OpcodePop opcode) : base(opcode, block) => Value = value; @@ -689,8 +701,10 @@ public InterimConstantValue Evaluate() return new InterimConstantValue(result, this); } } - public class IRSuffixGetMethod : IRSuffixGet + public class IRSuffixGetMethod : IRSuffixGet, IActionInstruction { + // TODO: Consider implementing this. + public bool IsInert => false; public IRSuffixGetMethod(BasicBlock block, IInterimOperand obj, OpcodeGetMethod opcode) : base(block, obj, opcode) { } public override IEnumerable EmitOpcodes() { @@ -706,9 +720,10 @@ public override bool Equals(object obj) public override int GetHashCode() => base.GetHashCode(); } - public class IRSuffixSet : MultipleOperandInstruction + public class IRSuffixSet : MultipleOperandInstruction, IActionInstruction { - public override bool IsInvariant => false; + public override bool IsInvariant => IsInert && Object.IsInvariant && Value.IsInvariant; + public bool IsInert => false; public IInterimOperand Object { get; set; } public IInterimOperand Value { get; set; } public override IEnumerable Operands { get { yield return Object; yield return Value; } } @@ -834,9 +849,10 @@ public InterimConstantValue Evaluate() //((Encapsulation.IIndexable)Object).GetIndex(); } } - public class IRIndexSet : MultipleOperandInstruction + public class IRIndexSet : MultipleOperandInstruction, IActionInstruction { - public override bool IsInvariant => false; + public override bool IsInvariant => IsInert && Object.IsInvariant && Index.IsInvariant && Value.IsInvariant; + public bool IsInert => false; public IInterimOperand Object { get; set; } public IInterimOperand Index { get; set; } public IInterimOperand Value { get; set; } @@ -972,7 +988,7 @@ public IRBranch Clone(BasicBlock block) return new IRBranch(block, Condition.Clone(block), True, False, opcode); } public override string ToString() - => string.Format("{{br.? {0}/{1}}}", True.Label, False.Label); + => string.Format("{{br.?{2} {0}/{1}}}", True.Label, False.Label, PreferFalse ? "f" : "t"); public override bool Equals(object obj) => obj is IRBranch branch && Condition.Equals(branch.Condition) && @@ -981,9 +997,9 @@ public override bool Equals(object obj) public override int GetHashCode() => True.GetHashCode() ^ False.GetHashCode(); } - public class IRCall : MultipleOperandInstruction, IResultingInstruction + public class IRCall : MultipleOperandInstruction, IResultingInstruction, IActionInstruction { - public override bool IsInvariant => IsCallInvariant() && Arguments.All(a => a.IsInvariant); + public override bool IsInvariant => IsSelfInvariant && IsInert && Arguments.All(a => a.IsInvariant); public string Function { get; } public List Arguments { get; } = new List(); public override IEnumerable Operands => Enumerable.Reverse(Arguments); @@ -1019,17 +1035,35 @@ private IRCall(BasicBlock block, OpcodeCall opcode, bool emitArgMarker) : base(o Direct = opcode.Direct; EmitArgMarker = emitArgMarker; } - public bool IsCallInvariant() + private bool IsSelfInvariant { - // TODO: Consider that some suffix methods may actually be known at compile time. - IRCodePart.IRFunction function = Block?.CodePart?.GetFunction(this); - if (function != null) - return function.IsInvariant; - if (!Direct) + get + { + // TODO: Consider that some suffix methods may actually be known at compile time. + IRCodePart.IRFunction function = Block?.CodePart?.GetFunction(this); + if (function != null) + return function.IsInvariant; + if (!Direct) + return false; + if (Optimization.Optimizer.FunctionManager.Exists(Function.Replace("()", ""))) + return Optimization.Optimizer.FunctionManager.IsFunctionInvariant(Function.Replace("()", "")); return false; - if (Optimization.Optimizer.FunctionManager.Exists(Function.Replace("()", ""))) - return Optimization.Optimizer.FunctionManager.IsFunctionInvariant(Function.Replace("()", "")); - return false; + } + } + public bool IsInert + { + get + { + // TODO: Consider that some suffix methods may actually be inert. + IRCodePart.IRFunction function = Block?.CodePart?.GetFunction(this); + if (function != null) + return function.IsInert; + if (!Direct) + return false; + if (Optimization.Optimizer.FunctionManager.Exists(Function.Replace("()", ""))) + return Optimization.Optimizer.FunctionManager.IsFunctionInert(Function.Replace("()", "")); + return false; + } } private Type GetDefaultReturnType() { @@ -1082,7 +1116,7 @@ public override string ToString() => string.Format("{{call {0}({1})}}", Function.Trim('(', ')'), string.Join(",", Arguments.Select(a => a.ToString()))); public bool Equals(IInterimOperand other) => (other is IRCall call && - IsCallInvariant() && // If the call does something beyond arithmetic, this condition prevents optimizing it away. + IsInert && // If the call is not inert, the underlying state is affected, which makes any similar call non-equal. string.Equals(Function.Replace("()", ""), call.Function.Replace("()", ""), StringComparison.OrdinalIgnoreCase) && Arguments.SequenceEqual(call.Arguments)) || (IsInvariant && diff --git a/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs index 77f8febc6..d31696e9b 100644 --- a/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs @@ -10,18 +10,8 @@ public interface IResultingInstruction : IEvaluatableToConstant, IInterimOperand ushort OpcodeCount { get; } } - public interface IEvaluatableToConstant + public interface IEvaluatableToConstant : IInterimOperand { - /// - /// Gets a value indicating whether this instance is invariant. - /// That is, if the value of the operand can be known at - /// compile time. - /// - /// - /// true if this instance is invariant; otherwise, false. - /// - bool IsInvariant { get; } - /// /// Evaluates the instruction to a constant value. /// diff --git a/src/kOS.Safe/Compilation/IR/InterimVariables.cs b/src/kOS.Safe/Compilation/IR/InterimVariables.cs index 1554134f5..44daf179d 100644 --- a/src/kOS.Safe/Compilation/IR/InterimVariables.cs +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -622,7 +622,7 @@ protected override IInterimOperand ValueAsOperand(SSADefinition item) => new InterimResolvedReference(item, -1, -1); } - public class PhiOperand : PhiNode, IEvaluatableToConstant + public class PhiOperand : PhiNode { protected override bool ObjIsInvariant(IInterimOperand obj) => obj == null || (obj.IsInvariant && obj is IEvaluatableToConstant); diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs b/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs index 065bd10a4..1891e8b3e 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs @@ -90,7 +90,7 @@ void AddToExpressions(IInterimOperand operand) // Break from the subexpression loop if a // non-invariant call is encountered so as // to not 'optimize' away a call that does something. - if (operand is IRCall call && !call.IsCallInvariant()) + if (operand is IRCall call && !call.IsInvariant) breaking = true; else if (operand is IResultingInstruction resultingInstruction) { diff --git a/src/kOS.Safe/Encapsulation/Lexicon.cs b/src/kOS.Safe/Encapsulation/Lexicon.cs index 6e4edfdff..a8d77ca0f 100644 --- a/src/kOS.Safe/Encapsulation/Lexicon.cs +++ b/src/kOS.Safe/Encapsulation/Lexicon.cs @@ -14,7 +14,7 @@ namespace kOS.Safe.Encapsulation [kOS.Safe.Utilities.KOSNomenclature("Lex", CSharpToKOS = false)] public class Lexicon : SerializableStructure, IDictionary, IIndexable { - [Function("lex", "lexicon", ReturnType = typeof(Lexicon), IsInvariant = true)] + [Function("lex", "lexicon", ReturnType = typeof(Lexicon), IsInvariant = true, IsInert = true)] public class FunctionLexicon : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/ListValue.cs b/src/kOS.Safe/Encapsulation/ListValue.cs index bda8e2e00..a87ab2af5 100644 --- a/src/kOS.Safe/Encapsulation/ListValue.cs +++ b/src/kOS.Safe/Encapsulation/ListValue.cs @@ -150,7 +150,7 @@ public void Insert(int index, T item) [kOS.Safe.Utilities.KOSNomenclature("List", KOSToCSharp = false)] // one-way because the generic templated ListValue is the canonical one. public class ListValue : ListValue { - [Function("list", ReturnType = typeof(ListValue), IsInvariant = true)] + [Function("list", ReturnType = typeof(ListValue), IsInvariant = true, IsInert = true)] public class FunctionList : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/PIDLoop.cs b/src/kOS.Safe/Encapsulation/PIDLoop.cs index 99231a9b9..da4a87582 100644 --- a/src/kOS.Safe/Encapsulation/PIDLoop.cs +++ b/src/kOS.Safe/Encapsulation/PIDLoop.cs @@ -9,7 +9,7 @@ namespace kOS.Safe.Encapsulation [kOS.Safe.Utilities.KOSNomenclature("PIDLoop")] public class PIDLoop : SerializableStructure { - [Function("pidloop", ReturnType = typeof(PIDLoop), IsInvariant = true)] + [Function("pidloop", ReturnType = typeof(PIDLoop), IsInvariant = true, IsInert = true)] public class PIDLoopConstructor : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/QueueValue.cs b/src/kOS.Safe/Encapsulation/QueueValue.cs index bf30461f8..4433cce0b 100644 --- a/src/kOS.Safe/Encapsulation/QueueValue.cs +++ b/src/kOS.Safe/Encapsulation/QueueValue.cs @@ -68,7 +68,7 @@ public static QueueValue CreateQueue(IEnumerable list) [kOS.Safe.Utilities.KOSNomenclature("Queue", KOSToCSharp = false)] // one-way because the generic templated QueueValue is the canonical one. public class QueueValue : QueueValue { - [Function("queue", ReturnType = typeof(QueueValue), IsInvariant = true)] + [Function("queue", ReturnType = typeof(QueueValue), IsInvariant = true, IsInert = true)] public class FunctionQueue : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/StackValue.cs b/src/kOS.Safe/Encapsulation/StackValue.cs index 63b8e83d7..11ba15de6 100644 --- a/src/kOS.Safe/Encapsulation/StackValue.cs +++ b/src/kOS.Safe/Encapsulation/StackValue.cs @@ -74,7 +74,7 @@ public static StackValue CreateStack(IEnumerable list) [kOS.Safe.Utilities.KOSNomenclature("Stack", KOSToCSharp = false)] // one-way because the generic templated StackValue is the canonical one. public class StackValue : StackValue { - [Function("stack", ReturnType = typeof(StackValue), IsInvariant = true)] + [Function("stack", ReturnType = typeof(StackValue), IsInvariant = true, IsInert = true)] public class FunctionStack : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/UniqueSetValue.cs b/src/kOS.Safe/Encapsulation/UniqueSetValue.cs index e02e00eff..341f8c1af 100644 --- a/src/kOS.Safe/Encapsulation/UniqueSetValue.cs +++ b/src/kOS.Safe/Encapsulation/UniqueSetValue.cs @@ -73,7 +73,7 @@ private void SetInitializeSuffixes() [kOS.Safe.Utilities.KOSNomenclature("UniqueSet", KOSToCSharp = false)] // one-way because the generic templated UniqueSetValue is the canonical one. public class UniqueSetValue : UniqueSetValue { - [Function("uniqueset", ReturnType = typeof(UniqueSetValue), IsInvariant = true)] + [Function("uniqueset", ReturnType = typeof(UniqueSetValue), IsInvariant = true, IsInert = true)] public class FunctionSet : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/FunctionAttribute.cs b/src/kOS.Safe/Function/FunctionAttribute.cs index ebe06e739..73c8104d3 100644 --- a/src/kOS.Safe/Function/FunctionAttribute.cs +++ b/src/kOS.Safe/Function/FunctionAttribute.cs @@ -5,7 +5,30 @@ namespace kOS.Safe.Function public class FunctionAttribute : Attribute { public string[] Names { get; set; } + /// + /// Gets a value indicating whether this function's result is + /// invariant. That is, if the value of the result can be + /// known at compile time. + /// + /// A function that is both invariant and inert can + /// be replaced by its result at compile time. + /// + /// true if this function is invariant; otherwise, false. + /// public bool IsInvariant { get; set; } = false; + /// + /// Gets a value indicating whether this function affects + /// the wider simulation state or changes the value of an + /// object. + /// + /// A function that is both inert and invariant can + /// be replaced by its result at compile time. A function + /// that is inert can be relocated within a script without + /// issue (for the same input arguments). + /// + /// true if this function is inert; otherwise, false. + /// + public bool IsInert { get; set; } = false; public Type ReturnType { get => returnType; diff --git a/src/kOS.Safe/Function/FunctionManager.cs b/src/kOS.Safe/Function/FunctionManager.cs index 443f2a55c..2d61f0484 100644 --- a/src/kOS.Safe/Function/FunctionManager.cs +++ b/src/kOS.Safe/Function/FunctionManager.cs @@ -13,6 +13,7 @@ public class FunctionManager : IFunctionManager private readonly Dictionary functions = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly Dictionary functionTypes = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly HashSet invariantFunctions = new HashSet(); + private readonly HashSet inertFunctions = new HashSet(); private static readonly Dictionary rawAttributes = new Dictionary(); public FunctionManager(SafeSharedObjects shared) @@ -26,6 +27,7 @@ public void Load() functions.Clear(); functionTypes.Clear(); invariantFunctions.Clear(); + inertFunctions.Clear(); foreach (FunctionAttribute attr in rawAttributes.Keys) { @@ -40,6 +42,8 @@ public void Load() functionTypes.Add(functionName, attr.ReturnType); if (attr.IsInvariant) invariantFunctions.Add(functionName); + if (attr.IsInert) + inertFunctions.Add(functionName); } } } @@ -83,6 +87,10 @@ public bool IsFunctionInvariant(string functionName) { return invariantFunctions.Contains(functionName); } + public bool IsFunctionInert(string functionName) + { + return inertFunctions.Contains(functionName); + } public Type FunctionReturnType(string functionName) { diff --git a/src/kOS.Safe/Function/IFunctionManager.cs b/src/kOS.Safe/Function/IFunctionManager.cs index 54bd58d46..5133aba74 100644 --- a/src/kOS.Safe/Function/IFunctionManager.cs +++ b/src/kOS.Safe/Function/IFunctionManager.cs @@ -6,6 +6,7 @@ public interface IFunctionManager void CallFunction(string functionName); bool Exists(string functionName); bool IsFunctionInvariant(string functionName); + bool IsFunctionInert(string functionName); System.Type FunctionReturnType(string functionName); } } \ No newline at end of file diff --git a/src/kOS.Safe/Function/Math.cs b/src/kOS.Safe/Function/Math.cs index 6ed78a4ac..f04476723 100644 --- a/src/kOS.Safe/Function/Math.cs +++ b/src/kOS.Safe/Function/Math.cs @@ -4,7 +4,7 @@ namespace kOS.Safe.Function { - [Function("abs", ReturnType = typeof(ScalarValue), IsInvariant = true)] + [Function("abs", ReturnType = typeof(ScalarValue), IsInvariant = true, IsInert = true)] public class FunctionAbs : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -16,7 +16,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("mod", ReturnType = typeof(ScalarValue), IsInvariant = true)] + [Function("mod", ReturnType = typeof(ScalarValue), IsInvariant = true, IsInert = true)] public class FunctionMod : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -29,7 +29,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("floor", ReturnType = typeof(ScalarValue), IsInvariant = true)] + [Function("floor", ReturnType = typeof(ScalarValue), IsInvariant = true, IsInert = true)] public class FunctionFloor : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -44,7 +44,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("ceiling", ReturnType = typeof(ScalarValue), IsInvariant = true)] + [Function("ceiling", ReturnType = typeof(ScalarValue), IsInvariant = true, IsInert = true)] public class FunctionCeiling : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -59,7 +59,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("round", ReturnType = typeof(ScalarValue), IsInvariant = true)] + [Function("round", ReturnType = typeof(ScalarValue), IsInvariant = true, IsInert = true)] public class FunctionRound : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -86,7 +86,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("sqrt", ReturnType = typeof(ScalarValue), IsInvariant = true)] + [Function("sqrt", ReturnType = typeof(ScalarValue), IsInvariant = true, IsInert = true)] public class FunctionSqrt : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -99,7 +99,7 @@ public override void Execute(SafeSharedObjects shared) } - [Function("ln", ReturnType = typeof(ScalarValue), IsInvariant = true)] + [Function("ln", ReturnType = typeof(ScalarValue), IsInvariant = true, IsInert = true)] public class FunctionLn : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -111,7 +111,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("log10", ReturnType = typeof(ScalarValue), IsInvariant = true)] + [Function("log10", ReturnType = typeof(ScalarValue), IsInvariant = true, IsInert = true)] public class FunctionLog10 : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -123,7 +123,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("min", ReturnType = typeof(ScalarValue), IsInvariant = true)] + [Function("min", ReturnType = typeof(ScalarValue), IsInvariant = true, IsInert = true)] public class FunctionMin : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -153,7 +153,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("max", ReturnType = typeof(ScalarValue), IsInvariant = true)] + [Function("max", ReturnType = typeof(ScalarValue), IsInvariant = true, IsInert = true)] public class FunctionMax : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -183,7 +183,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("random", ReturnType = typeof(ScalarDoubleValue), IsInvariant = false)] + [Function("random", ReturnType = typeof(ScalarDoubleValue), IsInvariant = false, IsInert = true)] public class FunctionRandom : SafeFunctionBase { private readonly Random random = new Random(); @@ -201,7 +201,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("randomseed", ReturnType = null, IsInvariant = false)] + [Function("randomseed", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionRandomSeed : SafeFunctionBase { private readonly Random random = new Random(); @@ -215,7 +215,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("char", ReturnType = typeof(StringValue), IsInvariant = true)] + [Function("char", ReturnType = typeof(StringValue), IsInvariant = true, IsInert = true)] public class FunctionChar : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -227,7 +227,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("unchar", ReturnType = typeof(ScalarIntValue), IsInvariant = true)] + [Function("unchar", ReturnType = typeof(ScalarIntValue), IsInvariant = true, IsInert = true)] public class FunctionUnchar : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/Misc.cs b/src/kOS.Safe/Function/Misc.cs index 889038507..0e70faeae 100644 --- a/src/kOS.Safe/Function/Misc.cs +++ b/src/kOS.Safe/Function/Misc.cs @@ -10,7 +10,7 @@ namespace kOS.Safe.Function { - [Function("print", ReturnType = null, IsInvariant = false)] + [Function("print", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionPrint : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -21,7 +21,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("printat", ReturnType = null, IsInvariant = false)] + [Function("printat", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionPrintAt : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -34,7 +34,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("toggleflybywire", ReturnType = null, IsInvariant = false)] + [Function("toggleflybywire", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionToggleFlyByWire : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -46,7 +46,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("selectautopilotmode", ReturnType = null, IsInvariant = false)] + [Function("selectautopilotmode", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionSelectAutopilotMode : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -57,7 +57,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("run", ReturnType = null, IsInvariant = false)] + [Function("run", ReturnType = null, IsInvariant = false, IsInert = false)] public class FunctionRun : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -133,7 +133,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("load", ReturnType = null, IsInvariant = false)] + [Function("load", ReturnType = null, IsInvariant = false, IsInert = false)] public class FunctionLoad : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -262,7 +262,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("logfile", ReturnType = null, IsInvariant = false)] + [Function("logfile", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionLogFile : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -300,7 +300,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("reboot", ReturnType = null, IsInvariant = false)] + [Function("reboot", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionReboot : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -315,7 +315,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("shutdown", ReturnType = null, IsInvariant = false)] + [Function("shutdown", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionShutdown : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -326,7 +326,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("debugdump", ReturnType = null, IsInvariant = false)] + [Function("debugdump", ReturnType = null, IsInvariant = true, IsInert = false)] public class DebugDump : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -336,7 +336,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("debugfreezegame", ReturnType = null, IsInvariant = false)] + [Function("debugfreezegame", ReturnType = null, IsInvariant = true, IsInert = false)] /// /// Deliberately cause physics lag by making the main game thread sleep. /// Clearly not something there's a good reason to do *except* when @@ -361,7 +361,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("profileresult", ReturnType = typeof(StringValue), IsInvariant = false)] + [Function("profileresult", ReturnType = typeof(StringValue), IsInvariant = false, IsInert = true)] public class ProfileResult : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -383,7 +383,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("makebuiltindelegate", ReturnType = typeof(BuiltinDelegate), IsInvariant = false)] + [Function("makebuiltindelegate", ReturnType = typeof(BuiltinDelegate), IsInvariant = false, IsInert = true)] public class MakeBuiltinDelegate : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -395,7 +395,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("droppriority", ReturnType = null, IsInvariant = false)] + [Function("droppriority", ReturnType = null, IsInvariant = true, IsInert = false)] public class AllowInterrupt : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/Persistence.cs b/src/kOS.Safe/Function/Persistence.cs index 90b1b4aa6..227508e04 100644 --- a/src/kOS.Safe/Function/Persistence.cs +++ b/src/kOS.Safe/Function/Persistence.cs @@ -12,7 +12,7 @@ namespace kOS.Safe.Function * remove these function below as well any metions of delete/rename file/rename volume/copy from kRISC.tpg in the future. */ - [Function("copy_deprecated", ReturnType = null, IsInvariant = false)] + [Function("copy_deprecated", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionCopyDeprecated : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -49,7 +49,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("rename_file_deprecated", ReturnType = null, IsInvariant = false)] + [Function("rename_file_deprecated", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionRenameFileDeprecated : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -76,7 +76,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("rename_volume_deprecated", ReturnType = null, IsInvariant = false)] + [Function("rename_volume_deprecated", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionRenameVolumeDeprecated : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -99,7 +99,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("delete_deprecated", ReturnType = null, IsInvariant = false)] + [Function("delete_deprecated", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionDeleteDeprecated : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -127,7 +127,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("scriptpath", ReturnType = typeof(PathValue), IsInvariant = false)] + [Function("scriptpath", ReturnType = typeof(PathValue), IsInvariant = false, IsInert = true)] public class FunctionScriptPath : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -141,7 +141,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("switch", ReturnType = null, IsInvariant = false)] + [Function("switch", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionSwitch : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -164,7 +164,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("cd", "chdir", ReturnType = null, IsInvariant = false)] + [Function("cd", "chdir", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionCd : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -198,7 +198,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("copypath", ReturnType = typeof(BooleanValue), IsInvariant = false)] + [Function("copypath", ReturnType = typeof(BooleanValue), IsInvariant = false, IsInert = false)] public class FunctionCopyPath : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -214,7 +214,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("movepath", ReturnType = typeof(BooleanValue), IsInvariant = false)] + [Function("movepath", ReturnType = typeof(BooleanValue), IsInvariant = false, IsInert = false)] public class FunctionMove : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -230,7 +230,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("deletepath", ReturnType = typeof(BooleanValue), IsInvariant = false)] + [Function("deletepath", ReturnType = typeof(BooleanValue), IsInvariant = false, IsInert = false)] public class FunctionDeletePath : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -245,7 +245,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("writejson", ReturnType = typeof(VolumeFile), IsInvariant = false)] + [Function("writejson", ReturnType = typeof(VolumeFile), IsInvariant = false, IsInert = false)] public class FunctionWriteJson : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -270,7 +270,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("readjson", ReturnType = typeof(Structure), IsInvariant = false)] + [Function("readjson", ReturnType = typeof(Structure), IsInvariant = false, IsInert = true)] public class FunctionReadJson : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -293,7 +293,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("exists", ReturnType = typeof(BooleanValue), IsInvariant = false)] + [Function("exists", ReturnType = typeof(BooleanValue), IsInvariant = false, IsInert = true)] public class FunctionExists : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -308,7 +308,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("open", ReturnType = typeof(Structure), IsInvariant = false)] + [Function("open", ReturnType = typeof(Structure), IsInvariant = false, IsInert = false)] public class FunctionOpen : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -328,7 +328,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("create", ReturnType = typeof(VolumeFile), IsInvariant = false)] + [Function("create", ReturnType = typeof(VolumeFile), IsInvariant = false, IsInert = false)] public class FunctionCreate : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -345,7 +345,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("createdir", ReturnType = typeof(VolumeDirectory), IsInvariant = false)] + [Function("createdir", ReturnType = typeof(VolumeDirectory), IsInvariant = false, IsInert = false)] public class FunctionCreateDirectory : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/Suffixed.cs b/src/kOS.Safe/Function/Suffixed.cs index ffe94b966..9f6fe919a 100644 --- a/src/kOS.Safe/Function/Suffixed.cs +++ b/src/kOS.Safe/Function/Suffixed.cs @@ -3,7 +3,7 @@ namespace kOS.Safe.Function { - [Function("range", ReturnType = typeof(RangeValue), IsInvariant = true)] + [Function("range", ReturnType = typeof(RangeValue), IsInvariant = true, IsInert = true)] public class FunctionRange : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -38,7 +38,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("constant", ReturnType = typeof(ConstantValue), IsInvariant = true)] + [Function("constant", ReturnType = typeof(ConstantValue), IsInvariant = true, IsInert = true)] public class FunctionConstant : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/Trigonometry.cs b/src/kOS.Safe/Function/Trigonometry.cs index c362c9a13..27f4559a3 100644 --- a/src/kOS.Safe/Function/Trigonometry.cs +++ b/src/kOS.Safe/Function/Trigonometry.cs @@ -4,7 +4,7 @@ namespace kOS.Safe.Function { - [Function("sin", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] + [Function("sin", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true, IsInert = true)] public class FunctionSin : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -17,7 +17,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("cos", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] + [Function("cos", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true, IsInert = true)] public class FunctionCos : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -30,7 +30,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("tan", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] + [Function("tan", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true, IsInert = true)] public class FunctionTan : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -43,7 +43,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("arcsin", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] + [Function("arcsin", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true, IsInert = true)] public class FunctionArcSin : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -55,7 +55,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("arccos", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] + [Function("arccos", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true, IsInert = true)] public class FunctionArcCos : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -67,7 +67,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("arctan", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] + [Function("arctan", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true, IsInert = true)] public class FunctionArcTan : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -79,7 +79,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("arctan2", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] + [Function("arctan2", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true, IsInert = true)] public class FunctionArcTan2 : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -92,7 +92,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("anglediff", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] + [Function("anglediff", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true, IsInert = true)] public class FunctionAngleDiff : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Persistence/PathValue.cs b/src/kOS.Safe/Persistence/PathValue.cs index 2a53bac29..59cff6a8a 100644 --- a/src/kOS.Safe/Persistence/PathValue.cs +++ b/src/kOS.Safe/Persistence/PathValue.cs @@ -18,7 +18,7 @@ namespace kOS.Safe [kOS.Safe.Utilities.KOSNomenclature("Path")] public class PathValue : SerializableStructure { - [Function("path", ReturnType = typeof(PathValue), IsInvariant = false)] + [Function("path", ReturnType = typeof(PathValue), IsInvariant = false, IsInert = false)] public class FunctionPath : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Persistence/Volume.cs b/src/kOS.Safe/Persistence/Volume.cs index 4d22ac86f..5e7cd77e2 100644 --- a/src/kOS.Safe/Persistence/Volume.cs +++ b/src/kOS.Safe/Persistence/Volume.cs @@ -10,7 +10,7 @@ namespace kOS.Safe.Persistence [kOS.Safe.Utilities.KOSNomenclature("Volume")] public abstract class Volume : Structure { - [Function("volume", ReturnType = typeof(Volume), IsInvariant = false)] + [Function("volume", ReturnType = typeof(Volume), IsInvariant = false, IsInert = true)] public class FunctionVolume : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS/AddOns/KerbalAlarmClock/KACFunctions.cs b/src/kOS/AddOns/KerbalAlarmClock/KACFunctions.cs index b9b4fb324..333cb5722 100644 --- a/src/kOS/AddOns/KerbalAlarmClock/KACFunctions.cs +++ b/src/kOS/AddOns/KerbalAlarmClock/KACFunctions.cs @@ -8,7 +8,7 @@ namespace kOS.AddOns.KerbalAlarmClock { - [Function("addAlarm", ReturnType = typeof(Structure), IsInvariant = false)] + [Function("addAlarm", ReturnType = typeof(KACAlarmWrapper), IsInvariant = false, IsInert = false)] public class FunctionAddAlarm : FunctionBase { public override void Execute(SharedObjects shared) @@ -66,7 +66,7 @@ public override void Execute(SharedObjects shared) } } - [Function("listAlarms", ReturnType = typeof(ListValue), IsInvariant = false)] + [Function("listAlarms", ReturnType = typeof(ListValue), IsInvariant = false, IsInert = true)] public class FunctionListAlarms : FunctionBase { public override void Execute(SharedObjects shared) @@ -100,7 +100,7 @@ public override void Execute(SharedObjects shared) } } - [Function("deleteAlarm", ReturnType = typeof(BooleanValue), IsInvariant = false)] + [Function("deleteAlarm", ReturnType = typeof(BooleanValue), IsInvariant = false, IsInert = false)] public class FunctionDeleteAlarm : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/BuildList.cs b/src/kOS/Function/BuildList.cs index 3305a3ee5..82c2586c4 100644 --- a/src/kOS/Function/BuildList.cs +++ b/src/kOS/Function/BuildList.cs @@ -9,7 +9,7 @@ namespace kOS.Function { - [Function("buildlist", ReturnType = typeof(ListValue), IsInvariant = false)] + [Function("buildlist", ReturnType = typeof(ListValue), IsInvariant = false, IsInert = true)] public class FunctionBuildList : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/Math.cs b/src/kOS/Function/Math.cs index ddf6b5998..430727bd5 100644 --- a/src/kOS/Function/Math.cs +++ b/src/kOS/Function/Math.cs @@ -7,7 +7,7 @@ namespace kOS.Function { - [Function("vcrs", "vectorcrossproduct", ReturnType = typeof(Vector), IsInvariant = true)] + [Function("vcrs", "vectorcrossproduct", ReturnType = typeof(Vector), IsInvariant = true, IsInert = true)] public class FunctionVectorCross : FunctionBase { public override void Execute(SharedObjects shared) @@ -26,7 +26,7 @@ public override void Execute(SharedObjects shared) } } - [Function("vdot", "vectordotproduct", ReturnType = typeof(ScalarValue), IsInvariant = true)] + [Function("vdot", "vectordotproduct", ReturnType = typeof(ScalarValue), IsInvariant = true, IsInert = true)] public class FunctionVectorDot : FunctionBase { public override void Execute(SharedObjects shared) @@ -45,7 +45,7 @@ public override void Execute(SharedObjects shared) } } - [Function("vxcl", "vectorexclude", ReturnType = typeof(Vector), IsInvariant = true)] + [Function("vxcl", "vectorexclude", ReturnType = typeof(Vector), IsInvariant = true, IsInert = true)] public class FunctionVectorExclude : FunctionBase { public override void Execute(SharedObjects shared) @@ -64,7 +64,7 @@ public override void Execute(SharedObjects shared) } } - [Function("vang", "vectorangle", ReturnType = typeof(ScalarValue), IsInvariant = true)] + [Function("vang", "vectorangle", ReturnType = typeof(ScalarValue), IsInvariant = true, IsInert = true)] public class FunctionVectorAngle : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/Misc.cs b/src/kOS/Function/Misc.cs index e090c09b4..070634eaf 100644 --- a/src/kOS/Function/Misc.cs +++ b/src/kOS/Function/Misc.cs @@ -19,7 +19,7 @@ namespace kOS.Function { - [Function("clearscreen", ReturnType = null, IsInvariant = false)] + [Function("clearscreen", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionClearScreen : FunctionBase { public override void Execute(SharedObjects shared) @@ -29,7 +29,7 @@ public override void Execute(SharedObjects shared) } } - [Function("hudtext", ReturnType = null, IsInvariant = false)] + [Function("hudtext", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionHudText : FunctionBase { public override void Execute(SharedObjects shared) @@ -71,7 +71,7 @@ public override void Execute(SharedObjects shared) } } - [Function("stage", ReturnType = null, IsInvariant = false)] + [Function("stage", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionStage : FunctionBase { public override void Execute(SharedObjects shared) @@ -93,7 +93,7 @@ public override void Execute(SharedObjects shared) } } - [Function("add", ReturnType = null, IsInvariant = false)] + [Function("add", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionAddNode : FunctionBase { public override void Execute(SharedObjects shared) @@ -104,7 +104,7 @@ public override void Execute(SharedObjects shared) } } - [Function("remove", ReturnType = null, IsInvariant = false)] + [Function("remove", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionRemoveNode : FunctionBase { public override void Execute(SharedObjects shared) @@ -115,7 +115,7 @@ public override void Execute(SharedObjects shared) } } - [Function("warpto", ReturnType = null, IsInvariant = false)] + [Function("warpto", ReturnType = null, IsInvariant = true, IsInert = false)] public class WarpTo : FunctionBase { public override void Execute(SharedObjects shared) @@ -137,7 +137,7 @@ public override void Execute(SharedObjects shared) } } - [Function("processor", ReturnType = typeof(PartModuleFields), IsInvariant = false)] + [Function("processor", ReturnType = typeof(PartModuleFields), IsInvariant = false, IsInert = true)] public class FunctionProcessor : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/Persistence.cs b/src/kOS/Function/Persistence.cs index 8b1ef6e50..bed001275 100644 --- a/src/kOS/Function/Persistence.cs +++ b/src/kOS/Function/Persistence.cs @@ -13,7 +13,7 @@ namespace kOS.Function { - [Function("edit", ReturnType = null, IsInvariant = false)] + [Function("edit", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionEdit : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/PrintList.cs b/src/kOS/Function/PrintList.cs index e36d88ba6..badde9a13 100644 --- a/src/kOS/Function/PrintList.cs +++ b/src/kOS/Function/PrintList.cs @@ -16,7 +16,7 @@ namespace kOS.Function { - [Function("printlist", ReturnType = null, IsInvariant = false)] + [Function("printlist", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionPrintList : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/Suffixed.cs b/src/kOS/Function/Suffixed.cs index e918f251e..f0f521529 100644 --- a/src/kOS/Function/Suffixed.cs +++ b/src/kOS/Function/Suffixed.cs @@ -15,7 +15,7 @@ namespace kOS.Function { - [Function("node", ReturnType = typeof(Node), IsInvariant = false)] + [Function("node", ReturnType = typeof(Node), IsInvariant = false, IsInert = false)] public class FunctionNode : FunctionBase { public override void Execute(SharedObjects shared) @@ -40,7 +40,7 @@ public override void Execute(SharedObjects shared) } } - [Function("v", ReturnType = typeof(Vector), IsInvariant = true)] + [Function("v", ReturnType = typeof(Vector), IsInvariant = true, IsInert = true)] public class FunctionVector : FunctionBase { public override void Execute(SharedObjects shared) @@ -55,7 +55,7 @@ public override void Execute(SharedObjects shared) } } - [Function("r", ReturnType = typeof(Direction), IsInvariant = true)] + [Function("r", ReturnType = typeof(Direction), IsInvariant = true, IsInert = true)] public class FunctionRotation : FunctionBase { public override void Execute(SharedObjects shared) @@ -70,7 +70,7 @@ public override void Execute(SharedObjects shared) } } - [Function("q", ReturnType = typeof(Direction), IsInvariant = true)] + [Function("q", ReturnType = typeof(Direction), IsInvariant = true, IsInert = true)] public class FunctionQuaternion : FunctionBase { public override void Execute(SharedObjects shared) @@ -86,7 +86,7 @@ public override void Execute(SharedObjects shared) } } - [Function("createOrbit", ReturnType = typeof(OrbitInfo), IsInvariant = false)] + [Function("createOrbit", ReturnType = typeof(OrbitInfo), IsInvariant = false, IsInert = true)] public class FunctionCreateOrbit : FunctionBase { public override void Execute(SharedObjects shared) @@ -131,7 +131,7 @@ public override void Execute(SharedObjects shared) } } - [Function("rotatefromto", ReturnType = typeof(Direction), IsInvariant = true)] + [Function("rotatefromto", ReturnType = typeof(Direction), IsInvariant = true, IsInert = true)] public class FunctionRotateFromTo : FunctionBase { public override void Execute(SharedObjects shared) @@ -145,7 +145,7 @@ public override void Execute(SharedObjects shared) } } - [Function("lookdirup", ReturnType = typeof(Direction), IsInvariant = true)] + [Function("lookdirup", ReturnType = typeof(Direction), IsInvariant = true, IsInert = true)] public class FunctionLookDirUp : FunctionBase { public override void Execute(SharedObjects shared) @@ -159,7 +159,7 @@ public override void Execute(SharedObjects shared) } } - [Function("angleaxis", ReturnType = typeof(Direction), IsInvariant = true)] + [Function("angleaxis", ReturnType = typeof(Direction), IsInvariant = true, IsInert = true)] public class FunctionAngleAxis : FunctionBase { public override void Execute(SharedObjects shared) @@ -173,7 +173,7 @@ public override void Execute(SharedObjects shared) } } - [Function("latlng", ReturnType = typeof(GeoCoordinates), IsInvariant = false)] + [Function("latlng", ReturnType = typeof(GeoCoordinates), IsInvariant = true, IsInert = false)] public class FunctionLatLng : FunctionBase { public override void Execute(SharedObjects shared) @@ -187,7 +187,7 @@ public override void Execute(SharedObjects shared) } } - [Function("vessel", ReturnType = typeof(VesselTarget), IsInvariant = false)] + [Function("vessel", ReturnType = typeof(VesselTarget), IsInvariant = false, IsInert = true)] public class FunctionVessel : FunctionBase { public override void Execute(SharedObjects shared) @@ -199,7 +199,7 @@ public override void Execute(SharedObjects shared) } } - [Function("body", ReturnType = typeof(BodyTarget), IsInvariant = false)] + [Function("body", ReturnType = typeof(BodyTarget), IsInvariant = false, IsInert = true)] public class FunctionBody : FunctionBase { public override void Execute(SharedObjects shared) @@ -211,7 +211,7 @@ public override void Execute(SharedObjects shared) } } - [Function("bodyexists", ReturnType = typeof(BooleanValue), IsInvariant = false)] + [Function("bodyexists", ReturnType = typeof(BooleanValue), IsInvariant = false, IsInert = true)] public class FunctionBodyExists : FunctionBase { public override void Execute(SharedObjects shared) @@ -222,7 +222,7 @@ public override void Execute(SharedObjects shared) } } - [Function("bodyatmosphere", ReturnType = typeof(BodyAtmosphere), IsInvariant = false)] + [Function("bodyatmosphere", ReturnType = typeof(BodyAtmosphere), IsInvariant = false, IsInert = true)] public class FunctionBodyAtmosphere : FunctionBase { public override void Execute(SharedObjects shared) @@ -237,7 +237,7 @@ public override void Execute(SharedObjects shared) } } - [Function("bounds", ReturnType = typeof(BoundsValue), IsInvariant = false)] + [Function("bounds", ReturnType = typeof(BoundsValue), IsInvariant = true, IsInert = true)] public class FunctionBounds : FunctionBase { public override void Execute(SharedObjects shared) @@ -252,7 +252,7 @@ public override void Execute(SharedObjects shared) } } - [Function("heading", ReturnType = typeof(Direction), IsInvariant = false)] + [Function("heading", ReturnType = typeof(Direction), IsInvariant = false, IsInert = true)] public class FunctionHeading : FunctionBase { public override void Execute(SharedObjects shared) @@ -274,7 +274,7 @@ public override void Execute(SharedObjects shared) } } - [Function("slidenote", ReturnType = typeof(NoteValue), IsInvariant = true)] + [Function("slidenote", ReturnType = typeof(NoteValue), IsInvariant = true, IsInert = true)] public class FunctionSlideNote : FunctionBase { public override void Execute(SharedObjects shared) @@ -304,7 +304,7 @@ public override void Execute(SharedObjects shared) } } - [Function("note", ReturnType = typeof(NoteValue), IsInvariant = true)] + [Function("note", ReturnType = typeof(NoteValue), IsInvariant = true, IsInert = true)] public class FunctionNote : FunctionBase { public override void Execute(SharedObjects shared) @@ -333,7 +333,7 @@ public override void Execute(SharedObjects shared) } } - [Function("GetVoice", ReturnType = typeof(VoiceValue), IsInvariant = false)] + [Function("GetVoice", ReturnType = typeof(VoiceValue), IsInvariant = false, IsInert = true)] public class FunctionGetVoice : FunctionBase { public override void Execute(SharedObjects shared) @@ -354,7 +354,7 @@ public override void Execute(SharedObjects shared) } - [Function("StopAllVoices", ReturnType = null, IsInvariant = false)] + [Function("StopAllVoices", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionStopAllVoices : FunctionBase { public override void Execute(SharedObjects shared) @@ -362,7 +362,7 @@ public override void Execute(SharedObjects shared) shared.SoundMaker.StopAllVoices(); } } - [Function("timestamp", "time", ReturnType = typeof(TimeStamp), IsInvariant = false)] + [Function("timestamp", "time", ReturnType = typeof(TimeStamp), IsInvariant = false, IsInert = true)] public class FunctionTimeStamp : FunctionBase { // Note: "TIME" is both a bound variable AND a built-in function now. @@ -426,7 +426,7 @@ public override void Execute(SharedObjects shared) } } - [Function("timespan", ReturnType = typeof(Suffixed.TimeSpan), IsInvariant = true)] + [Function("timespan", ReturnType = typeof(Suffixed.TimeSpan), IsInvariant = true, IsInert = true)] public class FunctionTimeSpan : FunctionBase { public override void Execute(SharedObjects shared) @@ -477,7 +477,7 @@ public override void Execute(SharedObjects shared) } } - [Function("hsv", ReturnType = typeof(HsvaColor), IsInvariant = true)] + [Function("hsv", ReturnType = typeof(HsvaColor), IsInvariant = true, IsInert = true)] public class FunctionHsv : FunctionBase { public override void Execute(SharedObjects shared) @@ -490,7 +490,7 @@ public override void Execute(SharedObjects shared) } } - [Function("hsva", ReturnType = typeof(HsvaColor), IsInvariant = true)] + [Function("hsva", ReturnType = typeof(HsvaColor), IsInvariant = true, IsInert = true)] public class FunctionHsva : FunctionBase { public override void Execute(SharedObjects shared) @@ -504,7 +504,7 @@ public override void Execute(SharedObjects shared) } } - [Function("rgb", ReturnType = typeof(RgbaColor), IsInvariant = true)] + [Function("rgb", ReturnType = typeof(RgbaColor), IsInvariant = true, IsInert = true)] public class FunctionRgb : FunctionBase { public override void Execute(SharedObjects shared) @@ -517,7 +517,7 @@ public override void Execute(SharedObjects shared) } } - [Function("rgba", ReturnType = typeof(RgbaColor), IsInvariant = true)] + [Function("rgba", ReturnType = typeof(RgbaColor), IsInvariant = true, IsInert = true)] public class FunctionRgba : FunctionBase { public override void Execute(SharedObjects shared) @@ -539,7 +539,7 @@ public override void Execute(SharedObjects shared) // Note: vecdraw now counts the args and changes its behavior accordingly. // For backward compatibility, vecdrawargs has been aliased to vecdraw. // - [Function("vecdraw", "vecdrawargs", ReturnType = typeof(VectorRenderer), IsInvariant = false)] + [Function("vecdraw", "vecdrawargs", ReturnType = typeof(VectorRenderer), IsInvariant = true, IsInert = false)] public class FunctionVecDrawNull : FunctionBase { protected RgbaColor GetDefaultColor() @@ -629,7 +629,7 @@ public void DoExecuteWork( } } - [Function("clearvecdraws", ReturnType = null, IsInvariant = false)] + [Function("clearvecdraws", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionHideAllVecdraws : FunctionBase { public override void Execute(SharedObjects shared) @@ -639,7 +639,7 @@ public override void Execute(SharedObjects shared) } } - [Function("clearguis", ReturnType = null, IsInvariant = false)] + [Function("clearguis", ReturnType = null, IsInvariant = true, IsInert = false)] public class FunctionClearAllGuis : FunctionBase { public override void Execute(SharedObjects shared) @@ -649,7 +649,7 @@ public override void Execute(SharedObjects shared) } } - [Function("gui", ReturnType = typeof(GUIWidgets), IsInvariant = false)] + [Function("gui", ReturnType = typeof(GUIWidgets), IsInvariant = true, IsInert = false)] public class FunctionWidgets : FunctionBase { public override void Execute(SharedObjects shared) @@ -662,7 +662,7 @@ public override void Execute(SharedObjects shared) } } - [Function("positionat", ReturnType = typeof(Vector), IsInvariant = false)] + [Function("positionat", ReturnType = typeof(Vector), IsInvariant = false, IsInert = true)] public class FunctionPositionAt : FunctionBase { public override void Execute(SharedObjects shared) @@ -675,7 +675,7 @@ public override void Execute(SharedObjects shared) } } - [Function("velocityat", ReturnType = typeof(Vector), IsInvariant = false)] + [Function("velocityat", ReturnType = typeof(Vector), IsInvariant = false, IsInert = true)] public class FunctionVelocityAt : FunctionBase { public override void Execute(SharedObjects shared) @@ -688,7 +688,7 @@ public override void Execute(SharedObjects shared) } } - [Function("highlight", ReturnType = typeof(HighlightStructure), IsInvariant = false)] + [Function("highlight", ReturnType = typeof(HighlightStructure), IsInvariant = true, IsInert = false)] public class FunctionHightlight : FunctionBase { public override void Execute(SharedObjects shared) @@ -701,7 +701,7 @@ public override void Execute(SharedObjects shared) } } - [Function("orbitat", ReturnType = typeof(OrbitInfo), IsInvariant = false)] + [Function("orbitat", ReturnType = typeof(OrbitInfo), IsInvariant = false, IsInert = true)] public class FunctionOrbitAt : FunctionBase { public override void Execute(SharedObjects shared) @@ -714,7 +714,7 @@ public override void Execute(SharedObjects shared) } } - [Function("career", ReturnType = typeof(Career), IsInvariant = false)] + [Function("career", ReturnType = typeof(Career), IsInvariant = false, IsInert = true)] public class FunctionCareer : FunctionBase { public override void Execute(SharedObjects shared) @@ -724,7 +724,7 @@ public override void Execute(SharedObjects shared) } } - [Function("allwaypoints", ReturnType = typeof(ListValue), IsInvariant = false)] + [Function("allwaypoints", ReturnType = typeof(ListValue), IsInvariant = false, IsInert = true)] public class FunctionAllWaypoints : FunctionBase { public override void Execute(SharedObjects shared) @@ -758,7 +758,7 @@ public override void Execute(SharedObjects shared) } } - [Function("waypoint", ReturnType = typeof(WaypointValue), IsInvariant = false)] + [Function("waypoint", ReturnType = typeof(WaypointValue), IsInvariant = false, IsInert = true)] public class FunctionWaypoint : FunctionBase { public override void Execute(SharedObjects shared) @@ -805,7 +805,7 @@ public override void Execute(SharedObjects shared) } } - [Function("transferall", ReturnType = typeof(ResourceTransferValue), IsInvariant = false)] + [Function("transferall", ReturnType = typeof(ResourceTransferValue), IsInvariant = false, IsInert = false)] public class FunctionTransferAll : FunctionBase { public override void Execute(SharedObjects shared) @@ -828,7 +828,7 @@ public override void Execute(SharedObjects shared) } - [Function("transfer", ReturnType = typeof(ResourceTransferValue), IsInvariant = false)] + [Function("transfer", ReturnType = typeof(ResourceTransferValue), IsInvariant = false, IsInert = false)] public class FunctionTransfer : FunctionBase { public override void Execute(SharedObjects shared) From 438b7697c4219738747441a103b948eac5018321 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 31 May 2026 16:47:18 -0400 Subject: [PATCH 117/120] Fix block ordering for do-while structures. Improves loop and branch detection. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 14 +- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 15 +- .../Optimization/Passes/BlockOrdering.cs | 250 +++++++++++++++--- .../Passes/LoopConditionalRelocation.cs | 12 +- 4 files changed, 231 insertions(+), 60 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 90d43c5b2..45abdb5f2 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -196,9 +196,14 @@ public BasicBlock(IRCodePart codePart, int startIndex, int endIndex, string nonS CodePart = codePart; StartIndex = startIndex; EndIndex = endIndex; - unchecked + if (nonSequentialLabel == SyntheticReturnBlock.syntheticReturnLabel) + ID = uint.MaxValue; + else { - ID = nextID++; + unchecked + { + ID = nextID++; + } } this.nonSequentialLabel = nonSequentialLabel; } @@ -446,8 +451,11 @@ public IEnumerable EmitOpCodes() public sealed class SyntheticReturnBlock : BasicBlock { - public SyntheticReturnBlock(IRCodePart codePart) : base(codePart, -1, -1, "syntheticReturn") + public const string syntheticReturnLabel = "syntheticReturn"; + public SyntheticReturnBlock(IRCodePart codePart) : base(codePart, -1, -1, syntheticReturnLabel) { } + public override string ToString() + => $"BasicBlock:SyntheticReturn"; } } diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 22afa12f6..04e4f441c 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -102,17 +102,10 @@ private void CreateBlocks(List code, IRCodePart codePart, Dictionary exitBlocks = blocks.Where(b => !b.Successors.Any()).ToList(); - if (exitBlocks.Count == 1) - { - exitBlocks[0].EstablishPostDominance(); - } - else - { - BasicBlock unifiedReturn = new SyntheticReturnBlock(codePart); - foreach (BasicBlock exitBlock in exitBlocks) - exitBlock.AddSuccessor(unifiedReturn); - unifiedReturn.EstablishPostDominance(); - } + BasicBlock unifiedReturn = new SyntheticReturnBlock(codePart) { Scope = globalScope }; + foreach (BasicBlock exitBlock in exitBlocks) + exitBlock.AddSuccessor(unifiedReturn); + unifiedReturn.EstablishPostDominance(); AssignScopes(rootBlock, globalScope, scopePushes, scopePops); } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs index 7dca3ff52..44218b718 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs @@ -94,6 +94,15 @@ IEnumerable GetEdges(BasicBlock block) // Create a unified sequence of BasicBlocks. List results = metaSequences.SelectMany(s => s.Blocks).ToList(); + for (int i = 0; i < results.Count - 1; i++) + { + BasicBlock block = results[i]; + if (block.Instructions.Count > 0 && + block.Instructions[block.Instructions.Count - 1] is IRBranch branch && + branch.True == results[i + 1]) + branch.PreferFalse = true; + } + // This block is to avoid leaving branch instructions to blocks // that aren't actually being emitted, since that will throw // an exeption during block labelling. @@ -130,8 +139,14 @@ private static MetaBlockSequence ConstructMetaSequence(BasicBlock root, Stack childOffshoots)); + regionExits.Pop(); // Enqueue any new offshoots. foreach (BasicBlock child in childOffshoots) newOffshoots.Enqueue(child); @@ -172,43 +193,162 @@ private static MetaBlockSequence ConstructMetaSequence(BasicBlock root, Stack s != successor); - if (headerBlock.Successors.Count != 2) - return false; + // Sanity check: the exit should not be dominated by 'block' + // (it must be reachable without going through the loop body). + // Also verify exit is not inside the loop. + if (IsBackEdge(exit, bodyEntry)) + continue; // both successors loop back — degenerate, skip + + loopData = new LoopData( + header: block, + branchBlock: block, + body: bodyEntry, + exit: exit); + return true; + } + } + } + + // --- Case 2: do-while loop --- + // The block is entered unconditionally (no branch at the header). + // The branch is at the end of the body (the latch), and one of its + // successors is a back edge to 'block' (making 'block' the body entry). + // + // We detect this by checking whether 'block' is the target of any back edge + // from a block that 'block' dominates, and that back-edge block has a branch + // to an exit outside the loop. + if (block.Successors.Count == 1) + { + BasicBlock latch = FindDoWhileLatch(block, regionExit); + if (latch != null && latch.Successors.Count == 2 && HasBranchInstruction(latch)) + { + // One successor of the latch loops back; the other is the exit. + BasicBlock exit = latch.Successors.FirstOrDefault(s => s != block); + if (exit != null && (exit == regionExit || IsInsideRegion(exit, regionExit))) + { + // Do-while: no header (body is entered unconditionally), + // branch is at the latch, body starts at 'block'. + loopData = new LoopData( + header: null, + branchBlock: latch, + body: block, + exit: exit); + return true; + } + } + } + return false; + } + + /// + /// Given a block that is a direct successor of the header, walks forward + /// through linear (single-successor) chains to find a block that has a + /// back edge to 'header'. Returns the latch block if found, null otherwise. + /// + private static BasicBlock FindLatch(BasicBlock block, BasicBlock header) + { + while (block != null) + { + // A back edge exists if 'header' dominates 'cursor' and + // 'cursor' has 'header' as a successor. + if (IsBackEdge(block, header) && block.Successors.Contains(header)) + return block; - loopData = new LoopData(headerBlock, branchData.ifBlock, branchData.exit ?? branchData.elseBlock); - // At least one of the branches must return to the header - // block or the top of that branch to be a loop. - foreach (BasicBlock successor in headerBlock.Successors) + // Follow the acyclical post-dominator chain to walk the loop + // body without going into inner loops or branch bodies. + if (block.PostDominator?.Dominator == block) + block = block.PostDominator; + else + break; + } + return null; + } + + /// + /// For do-while detection: starting from the body entry block, walks the + /// dominator subtree to find a block that has a back edge pointing back to + /// 'bodyEntry' (i.e. a block dominated by bodyEntry that jumps back to it). + /// + private static BasicBlock FindDoWhileLatch(BasicBlock bodyEntry, BasicBlock regionExit) + { + // Walk all blocks dominated by bodyEntry (depth-first through dominates tree) + // and look for one that has a successor equal to bodyEntry. + Stack stack = new Stack(bodyEntry.Dominates); + while (stack.Count > 0) + { + BasicBlock candidate = stack.Pop(); + + // Don't cross out of the region. + if (!IsInsideRegion(candidate, regionExit)) + continue; + + if (candidate.Successors.Contains(bodyEntry)) + return candidate; + + foreach (BasicBlock dominated in candidate.Dominates) + stack.Push(dominated); + } + return null; + } + + /// + /// Returns true if 'block' dominates 'target', meaning the edge + /// from the predecessor of 'target' back to something dominated by + /// 'block' constitutes a back edge. + /// More precisely: is 'target' in the dominator subtree of 'ancestor'? + /// + private static bool IsBackEdge(BasicBlock block, BasicBlock ancestor) + { + while (block != null) { - if (BackEdgeDetection(successor, headerBlock)) + if (block == ancestor) return true; + block = block.Dominator; } - loopData = new LoopData(); return false; } + + private static bool HasBranchInstruction(BasicBlock block) + => block.Instructions.Count > 0 && + block.Instructions[block.Instructions.Count - 1] is IRBranch; + public static bool IdentifyBranch(BasicBlock branchingBlock, BasicBlock regionExit, out BranchData branchData) { branchData = new BranchData(); @@ -257,21 +397,6 @@ private static BasicBlock GetSequenceEnd(BasicBlock block) block = block.PostDominator; return block; } - private static bool BackEdgeDetection(BasicBlock successor, BasicBlock target) - { - BasicBlock firstSuccessor = successor; - if (successor.Dominator != target) - return false; - - while (successor.Successors.Count == 1) - { - successor = successor.Successors.First(); - if (successor == target) - return true; - } - - return successor.Successors.Contains(target) || successor.Successors.Contains(firstSuccessor); - } private static BasicBlock FindLocalMerge(BasicBlock ifBlock, BasicBlock regionExit) { BasicBlock candidate = ifBlock.PostDominator; @@ -296,9 +421,25 @@ public static bool IsInsideRegion(BasicBlock candidate, BasicBlock regionExit) public readonly struct BranchData { + /// + /// The branch's header block — the block where execution + /// branches upon exiting. + /// public readonly BasicBlock branch; + /// + /// The first block of the 'if' body (the block entered + /// when the branch condition occurs. + /// public readonly BasicBlock ifBlock; + /// + /// The first block of the 'else' body (the block entered + /// when the branch condition does not occur. Null when + /// this would be the exit block. + /// public readonly BasicBlock elseBlock; + /// + /// The block following the branch — where execution goes when the branch resolves. + /// public readonly BasicBlock exit; public BranchData(BasicBlock branch, BasicBlock ifBlock, BasicBlock elseBlock, BasicBlock exit) { @@ -310,12 +451,37 @@ public BranchData(BasicBlock branch, BasicBlock ifBlock, BasicBlock elseBlock, B } public readonly struct LoopData { + /// + /// The loop's header block — the block entered on each + /// iteration from outside and jumped back to by the + /// latch. Null for do-while style loops where the body is + /// entered unconditionally and the branch is at the end. + /// public readonly BasicBlock header; + + /// + /// The block containing the conditional branch instruction + /// that either continues the loop or exits it. For while + /// loops this is the header; for do-while loops this is + /// the latch (last block of the body). + /// + public readonly BasicBlock branchBlock; + + /// + /// The first block of the loop body (the block entered when + /// the loop entry condition is true / the loop continues). + /// public readonly BasicBlock body; + + /// + /// The block following the loop — where execution goes when the loop exits. + /// public readonly BasicBlock exit; - public LoopData(BasicBlock header, BasicBlock body, BasicBlock exit) + + public LoopData(BasicBlock header, BasicBlock branchBlock, BasicBlock body, BasicBlock exit) { this.header = header; + this.branchBlock = branchBlock; this.body = body; this.exit = exit; } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs index 3835ca33a..98a0712dd 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs @@ -66,7 +66,11 @@ private static void FindLoops(BasicBlock root, Stack regionExits, Li BasicBlock block = root; while (block != null && block != regionExits.Peek()) { - if (BlockOrdering.IdentifyLoop(block, regionExits.Peek(), out BlockOrdering.LoopData loopData)) + // Identify loops first because loop branches are subsets of branches. + // If root == loopData.body it's because this was just called recursively + // below. This can be treated as not a loop since it is already identified + // as a loop body. + if (BlockOrdering.IdentifyLoop(block, regionExits.Peek(), out BlockOrdering.LoopData loopData) && root != loopData.body) { loops.Add(loopData); regionExits.Push(loopData.exit); @@ -74,10 +78,10 @@ private static void FindLoops(BasicBlock root, Stack regionExits, Li regionExits.Pop(); block = loopData.exit; } + // If branchData.elseBlock == root, that's because this is the branch + // instruction at the end of a loop body. else if (BlockOrdering.IdentifyBranch(block, regionExits.Peek(), out BlockOrdering.BranchData branchData) && - //These checks are for edge cases of loop-like structures that are neither loops themselves or branches. - !((branchData.ifBlock?.Dominator != null && branchData.ifBlock.Dominator != block) || - (branchData.elseBlock?.Dominator != null && branchData.elseBlock.Dominator != block))) + branchData.elseBlock != root && branchData.ifBlock != root) { FindLoops(branchData.ifBlock, regionExits, loops); if (branchData.exit == null && branchData.elseBlock != null) From 0daa1761ab23bd67e0390cf291736826319e0261 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 31 May 2026 17:17:40 -0400 Subject: [PATCH 118/120] Fix not identifying do-while type loops. --- .../Passes/LoopConditionalRelocation.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs index 98a0712dd..f11423c00 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs @@ -70,12 +70,15 @@ private static void FindLoops(BasicBlock root, Stack regionExits, Li // If root == loopData.body it's because this was just called recursively // below. This can be treated as not a loop since it is already identified // as a loop body. - if (BlockOrdering.IdentifyLoop(block, regionExits.Peek(), out BlockOrdering.LoopData loopData) && root != loopData.body) + if (BlockOrdering.IdentifyLoop(block, regionExits.Peek(), out BlockOrdering.LoopData loopData)) { loops.Add(loopData); - regionExits.Push(loopData.exit); - FindLoops(loopData.body, regionExits, loops); - regionExits.Pop(); + if (root != loopData.body) + { + regionExits.Push(loopData.exit); + FindLoops(loopData.body, regionExits, loops); + regionExits.Pop(); + } block = loopData.exit; } // If branchData.elseBlock == root, that's because this is the branch @@ -83,7 +86,9 @@ private static void FindLoops(BasicBlock root, Stack regionExits, Li else if (BlockOrdering.IdentifyBranch(block, regionExits.Peek(), out BlockOrdering.BranchData branchData) && branchData.elseBlock != root && branchData.ifBlock != root) { + regionExits.Push(branchData.exit ?? regionExits.Peek()); FindLoops(branchData.ifBlock, regionExits, loops); + regionExits.Pop(); if (branchData.exit == null && branchData.elseBlock != null) { block = branchData.elseBlock; From 0f286f9f802f902034697bf1e50818ce82d22b73 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 31 May 2026 18:26:33 -0400 Subject: [PATCH 119/120] Add a loop-invariant code motion pass. This pass moves operations that do not depend on the context of the loop to occur before the loop. --- kerboscript_tests/integration/codeMotion.ks | 19 +++ .../Execution/OptimizationTest.cs | 40 +++++ src/kOS.Safe/Compilation/IR/BasicBlock.cs | 36 +++++ .../Compilation/IR/SingleStaticAssignment.cs | 5 + .../Optimization/OptimizationLevel.cs | 5 +- .../Passes/LoopInvariantCodeMotion.cs | 144 ++++++++++++++++++ 6 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 kerboscript_tests/integration/codeMotion.ks create mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/LoopInvariantCodeMotion.cs diff --git a/kerboscript_tests/integration/codeMotion.ks b/kerboscript_tests/integration/codeMotion.ks new file mode 100644 index 000000000..c0b2e9d6a --- /dev/null +++ b/kerboscript_tests/integration/codeMotion.ks @@ -0,0 +1,19 @@ +@lazyGlobal OFF. + +randomseed("random", 0). +local a is random("random"). +local b is a + 1. +print(b). +from {local i is 0.} +until i = 4 +step {set i to i + 1.} +do { + set b to a + 2. + print(i). +} +print(b). +for j in range(0, 4) { + set b to a + 1. + print (j). +} +print(b). \ No newline at end of file diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index 7bdaf0110..422e2e935 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -1016,5 +1016,45 @@ public void TestCommonExpressionElimination() Assert.IsInstanceOf(result[64]); Assert.IsInstanceOf(result[65]); } + + [Test] + public void TestLoopInvariantCodeMotion() + { + // Test that common expressions are eliminated + EnsureAtLeastLevel(OptimizationLevel.Balanced); + RunScript("integration/codeMotion.ks"); + RunSingleStep(); + AssertOutput( + "1.72624326996796", + "0", + "1", + "2", + "3", + "2.72624326996796", + "0", + "1", + "2", + "3", + "1.72624326996796" + ); + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List codePart = CompileCodePart("integration/codeMotion.ks"); + List result = GetOpcodesAfterOptimization(codePart[0].MainCode, OptimizationLevel.Balanced, false); + Assert.IsInstanceOf(result[21]); + Assert.AreEqual("$a", ((OpcodePush)result[21]).Argument); + Assert.IsInstanceOf(result[22]); + Assert.AreEqual(Encapsulation.ScalarIntValue.Two, ((OpcodePush)result[22]).Argument); + Assert.IsInstanceOf(result[23]); + Assert.IsInstanceOf(result[24]); + Assert.AreEqual("$b", ((OpcodeStoreExist)result[24]).Identifier); + Assert.IsInstanceOf(result[52]); + Assert.AreEqual("$a", ((OpcodePush)result[52]).Argument); + Assert.IsInstanceOf(result[53]); + Assert.AreEqual(Encapsulation.ScalarIntValue.One, ((OpcodePush)result[53]).Argument); + Assert.IsInstanceOf(result[54]); + Assert.IsInstanceOf(result[55]); + Assert.AreEqual("$b", ((OpcodeStoreExist)result[55]).Identifier); + } } } diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 45abdb5f2..5658e3847 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -447,6 +447,42 @@ public IEnumerable EmitOpCodes() if (addedFallthrough) Instructions.Remove(FallthroughJump); } + + public static BasicBlock InsertBlockBetween(BasicBlock precursor, BasicBlock successor, bool useHeaderScope = false) + { + if (!precursor.Successors.Contains(successor)) + throw new InvalidOperationException("The successor block must be a successor of the precursor block."); + BasicBlock betweenBlock = new BasicBlock(precursor.CodePart, precursor.EndIndex, successor.StartIndex) + { + Scope = useHeaderScope ? precursor.Scope : successor.Scope, + ExtendedBlock = successor.ExtendedBlock, + IsExecutable = successor.IsExecutable + }; + betweenBlock.TriggerPropagationBlacklist.UnionWith(successor.TriggerPropagationBlacklist); + foreach (var key in successor.TriggerUnsetBlacklist.Keys) + betweenBlock.TriggerUnsetBlacklist[key] = successor.TriggerUnsetBlacklist[key]; + betweenBlock.IncomingVariableDefinitions = new Dictionary<(string, IRScope), SSADefinition>(); + foreach (var key in successor.IncomingVariableDefinitions.Keys) + betweenBlock.IncomingVariableDefinitions[key] = successor.IncomingVariableDefinitions[key]; + betweenBlock.FallthroughJump = new IRJump(betweenBlock, successor, -1, -1); + betweenBlock.AddSuccessor(successor); + precursor.AddSuccessor(betweenBlock); + precursor.RemoveSuccessor(successor); + if (precursor.FallthroughJump != null) + precursor.FallthroughJump = new IRJump(precursor, + betweenBlock, + precursor.FallthroughJump.SourceLine, + precursor.FallthroughJump.SourceColumn); + if (precursor.Instructions.Count > 0 && + precursor.Instructions[precursor.Instructions.Count - 1] is IRBranch branch) + { + if (branch.True == successor) + branch.True = betweenBlock; + if (branch.False == successor) + branch.False = betweenBlock; + } + return betweenBlock; + } } public sealed class SyntheticReturnBlock : BasicBlock diff --git a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs index c660e9e20..40613829e 100644 --- a/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -166,6 +166,7 @@ private static bool ProcessAssignment(IRAssign assignment, IRCodePart codePart, else ReplaceDefinition(variables, (definition.Name, startingScope), definition, writeReplaceChain); startingScope.Assignments.Add(assignment); + assignment.IsInert = true; // IRFunction.IsGlobal initiates as false, so there's no need to |= false. break; case IRAssign.StoreScope.Global: @@ -175,6 +176,7 @@ private static bool ProcessAssignment(IRAssign assignment, IRCodePart codePart, ReplaceDefinition(variables, (definition.Name, startingScope.GetGlobalScope()), definition, writeReplaceChain); startingScope.GetGlobalScope().Assignments.Add(assignment); SetFunctionToGlobal(assignment, codePart, variables, readBlacklist, writeBlacklist, writeReplaceChain); + assignment.IsInert = false; break; default: if (ApplyDefinitionToName(definition, startingScope, variables, writeReplaceChain)) @@ -183,8 +185,11 @@ private static bool ProcessAssignment(IRAssign assignment, IRCodePart codePart, // But true function definitions are definitively set as local or global. // This only applies to a nested lock, which deserves to lose out on optimizations. SetFunctionToGlobal(assignment, codePart, variables, readBlacklist, writeBlacklist, writeReplaceChain); + assignment.IsInert = false; return true; } + else + assignment.IsInert = true; break; } return false; diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index 374c822da..f9222d817 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -30,9 +30,8 @@ namespace kOS.Safe.Compilation * 32100. Common expression elimination * Particularly: Any expression of 3 opcodes used more than twice, * or any expression of >3 opcodes used more than once + * 21. Code motion - moving expressions outside loops if they do not depend on loop variables * - * 2100. Code motion - moving expressions outside loops if they do not depend on loop variables - * 2150. Carry's - moving the N-1 D lookup of >1D lists outside the innermost loop * Loop jamming? (Combining adjacent loops into one) * Unswitching (moving conditional evaluation outside the loop) - low priority * Linear function test replacement - low priority @@ -43,7 +42,7 @@ namespace kOS.Safe.Compilation * 3500. Loop stack manipulation (delayed setting of either index or aggregator) * * O4: - * 4000. Constant loop unrolling + * 4000. Constant length loop unrolling */ public enum OptimizationLevel : int { diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/LoopInvariantCodeMotion.cs b/src/kOS.Safe/Compilation/Optimization/Passes/LoopInvariantCodeMotion.cs new file mode 100644 index 000000000..33416b2cb --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/LoopInvariantCodeMotion.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class LoopInvariantCodeMotion : IHolisticOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.Balanced; + public short SortIndex => 21; + + public void ApplyPass(IRCodePart codePart) + { + foreach (IRCodePart.IRFunction function in codePart.Functions) + foreach (IRCodePart.IRFunction.IRFunctionFragment fragment in function.Fragments) + ApplyPass(fragment.FunctionCode[0]); + foreach (IRCodePart.IRTrigger trigger in codePart.Triggers) + ApplyPass(trigger.Code[0]); + ApplyPass(codePart.MainCode[0]); + } + + private static void ApplyPass(BasicBlock root) + { + IEnumerable GetEdges(BasicBlock block) + => block.Successors.Where(BlockOrdering.AllBlocksPredicate); + + List reversePostOrder = BasicBlock.GetReversePostOrder(root, GetEdges); + Stack regionExits = new Stack(); + regionExits.Push(reversePostOrder[reversePostOrder.Count - 1]); + + List loopData = LoopConditionalRelocation.FindLoops(root, regionExits); + + foreach (BlockOrdering.LoopData loop in loopData) + { + RelocateInstructions(loop); + } + } + + private static void RelocateInstructions(BlockOrdering.LoopData loop) + { + // Things that are not able to be relocated: + // - Calls to non-inert functions. + // - Suffix or index sets or gets. + // - InterimVariableReference operands + // - InterimUnresolvedReference operands (maybe?) + // - InterimResolvedReference operands if that variable definition + // does not come into the loop body, unless it comes from an + // assignment that is relocated. + // - Anythign that happens inside a conditional. + // (Another pass can deal with restructuring conditionals) + HashSet allowableReferences = new HashSet(loop.body.IncomingVariableDefinitions.Values); + BasicBlock bodyEnd = loop.body; + while (bodyEnd.PostDominator?.Dominator == bodyEnd) + bodyEnd = bodyEnd.PostDominator; + allowableReferences.RemoveWhere(ssaDef => ssaDef is PhiVariable phi && phi.Node.PossibleValues.ContainsKey(bodyEnd)); + bool IsOperandForbidden_(IInterimOperand operand) + => IsOperandForbidden(operand, allowableReferences); + + List relocatedInstructions = + loop.body.Dominator?.PostDominator == loop.body ? loop.body.Dominator.Instructions : + new List(); + int insertionIndex = GetInsertionIndex(relocatedInstructions); + + BasicBlock block = loop.body; + while (BlockOrdering.IsInsideRegion(block, loop.exit)) + { + // Walk each instruction + for (int i = 0; i < block.Instructions.Count; i++) + { + IRInstruction instruction = block.Instructions[i]; + // Check that nothing invalidating occurs: + bool invalid = false; + if (instruction is IRBranch || + instruction is IRNoStackInstruction) + continue; + foreach (IRInstruction inst in instruction.DepthFirst()) + { + if (inst is IActionInstruction action && + !action.IsInert) + { + invalid = true; + break; + } + if (inst is IRSuffixGet || inst is IRIndexGet) + { + invalid = true; + break; + } + if (inst is IOperandInstructionBase operandInstruction && + operandInstruction.AnyOperand(IsOperandForbidden_)) + { + invalid = true; + break; + } + } + if (invalid) + continue; + + // If it reaches this point, the instruction can + // be moved outside the loop. + relocatedInstructions.Insert(insertionIndex, instruction); + block.Instructions.RemoveAt(i); + i--; + if (instruction is IRAssign assignment) + allowableReferences.Add(assignment.Target); + } + // Walking by post-dominator means that this will skip + // any conditional branches inside the loop. + block = block.PostDominator; + } + + if (loop.body.Dominator?.PostDominator != loop.body) + { + BasicBlock prefaceBlock = BasicBlock.InsertBlockBetween(loop.body.Dominator, loop.body); + prefaceBlock.Instructions.AddRange(relocatedInstructions); + } + } + + private static bool IsOperandForbidden(IInterimOperand operand, HashSet allowableReferences) + { + switch (operand) + { + case InterimVariableReference _: + return true; + case InterimUnresolvedReference _: + return true; + case InterimResolvedReference resolvedReference: + return !allowableReferences.Contains(resolvedReference.Reference); + default: + return false; + } + } + + private static int GetInsertionIndex(List instructionList) + { + int insertionIndex = instructionList.Count; + while (insertionIndex > 0 && + instructionList[insertionIndex - 1] is IRBranch) + insertionIndex--; + return insertionIndex; + } + } +} From 994d6e5e3cc9df3567da5831cf2272cabca2da88 Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 1 Jun 2026 22:36:57 -0400 Subject: [PATCH 120/120] Fix loops being not identified or identified twice depending on when in the optimization order they were checked. Also fixed forgetting to add newly created blocks to the IRCodePart's Blocks property. --- src/kOS.Safe.Test/Execution/OptimizationTest.cs | 10 +++++----- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 1 + .../Compilation/Optimization/OptimizationLevel.cs | 2 +- .../Passes/LoopConditionalRelocation.cs | 13 +++++++------ .../Optimization/Passes/LoopInvariantCodeMotion.cs | 12 ++++++++---- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index 422e2e935..930224f99 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -1020,7 +1020,7 @@ public void TestCommonExpressionElimination() [Test] public void TestLoopInvariantCodeMotion() { - // Test that common expressions are eliminated + // Test that loop-invariant expressions are relocated EnsureAtLeastLevel(OptimizationLevel.Balanced); RunScript("integration/codeMotion.ks"); RunSingleStep(); @@ -1042,16 +1042,16 @@ public void TestLoopInvariantCodeMotion() List codePart = CompileCodePart("integration/codeMotion.ks"); List result = GetOpcodesAfterOptimization(codePart[0].MainCode, OptimizationLevel.Balanced, false); Assert.IsInstanceOf(result[21]); - Assert.AreEqual("$a", ((OpcodePush)result[21]).Argument); + Assert.AreEqual(Encapsulation.ScalarIntValue.Two, ((OpcodePush)result[21]).Argument); Assert.IsInstanceOf(result[22]); - Assert.AreEqual(Encapsulation.ScalarIntValue.Two, ((OpcodePush)result[22]).Argument); + Assert.AreEqual("$a", ((OpcodePush)result[22]).Argument); Assert.IsInstanceOf(result[23]); Assert.IsInstanceOf(result[24]); Assert.AreEqual("$b", ((OpcodeStoreExist)result[24]).Identifier); Assert.IsInstanceOf(result[52]); - Assert.AreEqual("$a", ((OpcodePush)result[52]).Argument); + Assert.AreEqual(Encapsulation.ScalarIntValue.One, ((OpcodePush)result[52]).Argument); Assert.IsInstanceOf(result[53]); - Assert.AreEqual(Encapsulation.ScalarIntValue.One, ((OpcodePush)result[53]).Argument); + Assert.AreEqual("$a", ((OpcodePush)result[53]).Argument); Assert.IsInstanceOf(result[54]); Assert.IsInstanceOf(result[55]); Assert.AreEqual("$b", ((OpcodeStoreExist)result[55]).Identifier); diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 5658e3847..6f1f0263b 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -481,6 +481,7 @@ public static BasicBlock InsertBlockBetween(BasicBlock precursor, BasicBlock suc if (branch.False == successor) branch.False = betweenBlock; } + betweenBlock.CodePart.Blocks.Add(betweenBlock); return betweenBlock; } } diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index f9222d817..d48612bd7 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -30,7 +30,7 @@ namespace kOS.Safe.Compilation * 32100. Common expression elimination * Particularly: Any expression of 3 opcodes used more than twice, * or any expression of >3 opcodes used more than once - * 21. Code motion - moving expressions outside loops if they do not depend on loop variables + * 2100. Code motion - moving expressions outside loops if they do not depend on loop variables * * Loop jamming? (Combining adjacent loops into one) * Unswitching (moving conditional evaluation outside the loop) - low priority diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs index f11423c00..e3ebe808b 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs @@ -58,10 +58,10 @@ private static void RelocateConditional(BlockOrdering.LoopData loopData) public static List FindLoops(BasicBlock root, Stack regionExits) { List loopData = new List(); - FindLoops(root, regionExits, loopData); + FindLoops(root, regionExits, loopData, false); return loopData; } - private static void FindLoops(BasicBlock root, Stack regionExits, List loops) + private static void FindLoops(BasicBlock root, Stack regionExits, List loops, bool fromLoop) { BasicBlock block = root; while (block != null && block != regionExits.Peek()) @@ -72,11 +72,12 @@ private static void FindLoops(BasicBlock root, Stack regionExits, Li // as a loop body. if (BlockOrdering.IdentifyLoop(block, regionExits.Peek(), out BlockOrdering.LoopData loopData)) { - loops.Add(loopData); + if (!fromLoop || root != loopData.body) + loops.Add(loopData); if (root != loopData.body) { regionExits.Push(loopData.exit); - FindLoops(loopData.body, regionExits, loops); + FindLoops(loopData.body, regionExits, loops, true); regionExits.Pop(); } block = loopData.exit; @@ -87,7 +88,7 @@ private static void FindLoops(BasicBlock root, Stack regionExits, Li branchData.elseBlock != root && branchData.ifBlock != root) { regionExits.Push(branchData.exit ?? regionExits.Peek()); - FindLoops(branchData.ifBlock, regionExits, loops); + FindLoops(branchData.ifBlock, regionExits, loops, false); regionExits.Pop(); if (branchData.exit == null && branchData.elseBlock != null) { @@ -96,7 +97,7 @@ private static void FindLoops(BasicBlock root, Stack regionExits, Li else { if (branchData.elseBlock != null) - FindLoops(branchData.elseBlock, regionExits, loops); + FindLoops(branchData.elseBlock, regionExits, loops, false); block = branchData.exit; } } diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/LoopInvariantCodeMotion.cs b/src/kOS.Safe/Compilation/Optimization/Passes/LoopInvariantCodeMotion.cs index 33416b2cb..6cdd2e4a0 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/LoopInvariantCodeMotion.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/LoopInvariantCodeMotion.cs @@ -8,7 +8,7 @@ namespace kOS.Safe.Compilation.Optimization.Passes public class LoopInvariantCodeMotion : IHolisticOptimizationPass { public OptimizationLevel OptimizationLevel => OptimizationLevel.Balanced; - public short SortIndex => 21; + public short SortIndex => 2100; public void ApplyPass(IRCodePart codePart) { @@ -51,7 +51,7 @@ private static void RelocateInstructions(BlockOrdering.LoopData loop) // (Another pass can deal with restructuring conditionals) HashSet allowableReferences = new HashSet(loop.body.IncomingVariableDefinitions.Values); BasicBlock bodyEnd = loop.body; - while (bodyEnd.PostDominator?.Dominator == bodyEnd) + while (bodyEnd.PostDominator?.Dominator == bodyEnd && bodyEnd.PostDominator != loop.exit) bodyEnd = bodyEnd.PostDominator; allowableReferences.RemoveWhere(ssaDef => ssaDef is PhiVariable phi && phi.Node.PossibleValues.ContainsKey(bodyEnd)); bool IsOperandForbidden_(IInterimOperand operand) @@ -61,6 +61,7 @@ bool IsOperandForbidden_(IInterimOperand operand) loop.body.Dominator?.PostDominator == loop.body ? loop.body.Dominator.Instructions : new List(); int insertionIndex = GetInsertionIndex(relocatedInstructions); + int originalCount = relocatedInstructions.Count; BasicBlock block = loop.body; while (BlockOrdering.IsInsideRegion(block, loop.exit)) @@ -110,7 +111,8 @@ bool IsOperandForbidden_(IInterimOperand operand) block = block.PostDominator; } - if (loop.body.Dominator?.PostDominator != loop.body) + if (loop.body.Dominator?.PostDominator != loop.body && + originalCount != relocatedInstructions.Count) { BasicBlock prefaceBlock = BasicBlock.InsertBlockBetween(loop.body.Dominator, loop.body); prefaceBlock.Instructions.AddRange(relocatedInstructions); @@ -136,7 +138,9 @@ private static int GetInsertionIndex(List instructionList) { int insertionIndex = instructionList.Count; while (insertionIndex > 0 && - instructionList[insertionIndex - 1] is IRBranch) + (instructionList[insertionIndex - 1] is IRBranch || + instructionList[insertionIndex - 1] is IRJump || + instructionList[insertionIndex - 1] is IRJumpStack)) insertionIndex--; return insertionIndex; }