diff --git a/kerboscript_tests/integration/blank.ks b/kerboscript_tests/integration/blank.ks new file mode 100644 index 0000000000..8fcda713c6 --- /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/branching/for.ks b/kerboscript_tests/integration/branching/for.ks new file mode 100644 index 0000000000..6d27bf6fc8 --- /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 0000000000..5d7347f14d --- /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 0000000000..ede6afae4d --- /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 0000000000..eeece4e3dc --- /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 0000000000..7c9390ba93 --- /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 0000000000..0b84e8c602 --- /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 0000000000..59fde18585 --- /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/kerboscript_tests/integration/codeMotion.ks b/kerboscript_tests/integration/codeMotion.ks new file mode 100644 index 0000000000..c0b2e9d6ab --- /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/kerboscript_tests/integration/commonExpressionElimination.ks b/kerboscript_tests/integration/commonExpressionElimination.ks new file mode 100644 index 0000000000..79561e150b --- /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/kerboscript_tests/integration/constantPropagation.ks b/kerboscript_tests/integration/constantPropagation.ks new file mode 100644 index 0000000000..5374fd6cba --- /dev/null +++ b/kerboscript_tests/integration/constantPropagation.ks @@ -0,0 +1,64 @@ +@lazyGlobal OFF. + +global a is 5. +global _false is false. + +local b is 1. +set b to 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 + +local 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/kerboscript_tests/integration/operators.ks b/kerboscript_tests/integration/operators.ks index c4aa36b732..a68640d512 100644 --- a/kerboscript_tests/integration/operators.ks +++ b/kerboscript_tests/integration/operators.ks @@ -18,3 +18,8 @@ print(1 = 1). print(1 <> 2). print(true and true). print(true or false). +print("A" + "b"). +print(a * 0). +print(a ^ 0). +print(arcTan2(0,1)). +print(abs(-1)). diff --git a/kerboscript_tests/integration/operators_invalid.ks b/kerboscript_tests/integration/operators_invalid.ks new file mode 100644 index 0000000000..75473a709a --- /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/kerboscript_tests/integration/peepholeOptimizations.ks b/kerboscript_tests/integration/peepholeOptimizations.ks new file mode 100644 index 0000000000..12943de96c --- /dev/null +++ b/kerboscript_tests/integration/peepholeOptimizations.ks @@ -0,0 +1,51 @@ +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. +//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/kerboscript_tests/integration/suffixReplacement.ks b/kerboscript_tests/integration/suffixReplacement.ks new file mode 100644 index 0000000000..2e93f6cd13 --- /dev/null +++ b/kerboscript_tests/integration/suffixReplacement.ks @@ -0,0 +1,5 @@ +@CLOBBERBUILTINS OFF. +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/Compilation/TypeInferencing.cs b/src/kOS.Safe.Test/Compilation/TypeInferencing.cs new file mode 100644 index 0000000000..9e46abf5d2 --- /dev/null +++ b/src/kOS.Safe.Test/Compilation/TypeInferencing.cs @@ -0,0 +1,92 @@ +using System; +using NUnit.Framework; +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 + { + [Test] + public void StructureSuffixTest() + { + // Tests that the bare minimum works + Type structureType = typeof(Encapsulation.Structure); + Type result = TypeInferencer.GetTypeForSuffix(structureType, "TOSTRING"); + Assert.AreEqual(typeof(StringValue), result); + + Type integerType = typeof(ScalarIntValue); + result = TypeInferencer.GetTypeForSuffix(integerType, "istype"); + Assert.AreEqual(typeof(BooleanValue), result); + } + + [Test] + public void ListSuffixTest() + { + // Test that lists and enumerables work + Type listType = typeof(ListValue); + Type result = TypeInferencer.GetTypeForSuffix(listType, "ITERATOR"); + Assert.AreEqual(typeof(Enumerator), result); + + result = TypeInferencer.GetTypeForSuffix(listType, "COPY"); + Assert.AreEqual(typeof(ListValue), result); + + result = TypeInferencer.GetTypeForIndex(listType); + Assert.AreEqual(typeof(Encapsulation.Structure), result); + + result = TypeInferencer.GetTypeForSuffix(listType, "CLEAR"); + Assert.AreEqual(null, result); + + // Test an abstract generic type + listType = typeof(EnumerableValue>); + result = TypeInferencer.GetTypeForSuffix(listType, "ITERATOR"); + Assert.AreEqual(typeof(Enumerator), result); + } + + [Test] + public void TestInstructionInferencing() + { + 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(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(null, print.Type); + + IRCall userCall = new IRCall(null, new OpcodeCall("$test*"), true, a, b); + Assert.AreEqual(typeof(Encapsulation.Structure), userCall.Type); + } + } +} diff --git a/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs b/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs index 615e99466e..880a14de6c 100644 --- a/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs +++ b/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs @@ -35,10 +35,11 @@ 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() { @@ -86,12 +87,14 @@ 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(); screen.ClearOutput(); + cpu.PushArgumentStack(new KOSArgMarkerType()); cpu.GetCurrentContext().AddParts(compiled); } diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs new file mode 100644 index 0000000000..930224f99b --- /dev/null +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -0,0 +1,1060 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using kOS.Safe.Compilation; +using kOS.Safe.Compilation.IR; +using kOS.Safe.Exceptions; +using System.Linq; + +namespace kOS.Safe.Test.Execution +{ + [TestFixture] + public class OptimizationTest : BaseIntegrationTest + { + protected override OptimizationLevel OptimizationLevel => optimizationLevel; + protected OptimizationLevel optimizationLevel = OptimizationLevel.Balanced; + protected OptimizationLevel resetOptimizationLevel = OptimizationLevel.Balanced; + + 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, + 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(options); + optimizer.Optimize(codePart); + CodePart result = new CodePart(); + codePart.EmitCode(result); + 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"); + 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() + { + // 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", + "Ab", + "0", + "1", + "0", + "1" + ); + } + + [Test] + public void TestOperatorsException() + { + // Test that an invalid operation throws a compile error during constant folding + Assert.Throws(() => + { + RunScript("integration/operators_invalid.ks"); + }); + } + + [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" + ); + } + #endregion + + #region Suffix Replacement + [Test] + public void TestSuffixReplacement() + { + // Test that certain suffixes are replaced + RunScript("integration/suffixReplacement.ks"); + RunSingleStep(); + AssertOutput( + "9.80665", + "3.14159265358979", + "100" + ); + } + + [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(); + AssertOutput( + "String", + "6", + "6", + "Print this.", + "21", + "21", + "21", + "21", + "3", + "-3", + "7", + "16", + "125", + "64", + "3125", + "256", + "4", + "25", + "125", + "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() + }, + allowClobberBuiltins: false + ); + // 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() + }, + allowClobberBuiltins: false + ); + + 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)); + } + #endregion + + #region Constant Propagation, Folding, and Algebraic Simplifications + [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 OpcodeStoreGlobal("$d"), + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeMathAdd(), + new OpcodeStoreGlobal("$e"), + new OpcodePush("$b"), + new OpcodePush("$g"), + new OpcodeMathAdd(), + new OpcodeStoreGlobal("$f"), + new OpcodePopScope() + }, OptimizationLevel.Balanced + ); + Assert.IsInstanceOf(result[2]); + Assert.AreEqual(new Encapsulation.ScalarIntValue(3), (result[2] as OpcodePush)?.Argument); + Assert.IsInstanceOf(result[6]); + Assert.IsInstanceOf(result[10]); + } + + [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 OpcodeStoreGlobal("$result"), + // Block 2 + new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodePush("$a"), + new OpcodeMathAdd(), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeMathSubtract(), + new OpcodeStoreExist("$result"), + // Block 3 + new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodePush("$a"), + new OpcodeMathSubtract(), + new OpcodeStoreExist("$result"), + // Block 4 + new OpcodePush("$a"), + new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodeMathSubtract(), + new OpcodeStoreExist("$result"), + // Block 5 + new OpcodePush(Encapsulation.ScalarIntValue.One), + new OpcodePush("$a"), + new OpcodeMathSubtract(), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeMathAdd(), + new OpcodeStoreExist("$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 Safe.Execution.KOSArgMarkerType()), + new OpcodeCall("random"), + new OpcodeStoreLocal("$c"), + // Block 1 + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeMathMultiply(), + new OpcodePush("$a"), + new OpcodePush("$c"), + new OpcodeMathMultiply(), + new OpcodeMathAdd(), + new OpcodePop(), + // Block 2 + new OpcodePush("$a"), + new OpcodePush("$b"), + new OpcodeMathMultiply(), + new OpcodePush("$a"), + new OpcodePush("$c"), + 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() + } + ); + // 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] + public void TestAdditionSubtraction() + { + // -B+A = A+-B = A-B + // -A+B = B-A + // A--B=A+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("$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() + } + ); + Assert.IsInstanceOf(result[9]); + Assert.IsInstanceOf(result[13]); + Assert.IsInstanceOf(result[17]); + } + + [Test] + public void TestDivisionSubtraction() + { + // X^N/X=X^(N-1) + // X^N/X^M=X^(N-M) + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodeCall("random"), + 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() + }, OptimizationLevel.Balanced + ); + 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(-0.5), (result[9] as OpcodePush)?.Argument); + } + + [Test] + public void TestPowerAddition() + { + // X^N*X=X^(N+1) + // X^N*X^M=X^(N+M) + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodeCall("random"), + 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() + }, OptimizationLevel.Balanced + ); + 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(5.5), (result[9] as OpcodePush)?.Argument); + } + + [Test] + public void TestPowerCreation() + { + // X*X*X = X^3 + List result = GetOpcodesAfterOptimization( + new List() + { + new OpcodePushScope(1, 0), + new OpcodePush(new Safe.Execution.KOSArgMarkerType()), + new OpcodeCall("random"), + 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(), + // TODO: It should reduce this to Encapsulation.One + new OpcodePush("$a"), + new OpcodePush(Encapsulation.ScalarIntValue.Two), + new OpcodeMathPower(), + new OpcodePush("$a"), + new OpcodeMathDivide(), + new OpcodePush("$a"), + new OpcodeMathDivide(), + new OpcodePop(), + new OpcodePopScope() + } + ); + 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 + + #region Block Ordering + [Test] + public void TestUntilLoopDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/until.ks"); + 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 }); + optimizer.Optimize(codePart); + } + [Test] + public void TestForLoopDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/for.ks"); + 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); + Assert.AreEqual(6, loopData.exit?.ID); + Assert.AreEqual(1, codePart.MainCode. + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(b, null, out _)).Count()); + } + [Test] + public void TestFromLoopDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/from.ks"); + 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); + Assert.AreEqual(5, loopData.exit?.ID); + Assert.AreEqual(1, codePart.MainCode. + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyLoop(b, null, out _)).Count()); + } + [Test] + public void TestIfDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/if.ks"); + 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); + Assert.IsNull(branchData.elseBlock); + Assert.AreEqual(2, branchData.exit?.ID); + Assert.AreEqual(1, codePart.MainCode. + Where(b => Safe.Compilation.Optimization.Passes.BlockOrdering.IdentifyBranch(b, null, out _)).Count()); + } + [Test] + public void TestIfElseDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/ifElse.ks"); + 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); + 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(b, null, out _)).Count()); + } + [Test] + public void TestIfElseIfDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/ifElseIf.ks"); + 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); + Assert.AreEqual(3, branchData.elseBlock?.ID); + 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(b, null, out _)).Count()); + } + [Test] + public void TestIfElseIfElseDetection() + { + BasicBlock.ResetNextID(); + optimizationLevel = OptimizationLevel.None; + List _code = CompileCodePart("integration/branching/ifElseIfElse.ks"); + 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); + Assert.AreEqual(3, branchData.elseBlock?.ID); + 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(b, null, out _)).Count()); + } + #endregion + + #region Loop Condition Reordering + [Test] + public void TestUntilLoopCondition() + { + EnsureAtLeastLevel(OptimizationLevel.Balanced); + List _code = CompileCodePart("integration/branching/until.ks"); + 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() + { + EnsureAtLeastLevel(OptimizationLevel.Balanced); + List _code = CompileCodePart("integration/branching/for.ks"); + 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() + { + 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"); + 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 + + [Test] + public void TestCommonExpressionElimination() + { + // Test that common expressions are eliminated + EnsureAtLeastLevel(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); + + Assert.IsInstanceOf(result[18]); + Assert.IsInstanceOf(result[25]); + Assert.IsInstanceOf(result[26]); + Assert.IsInstanceOf(result[63]); + Assert.IsInstanceOf(result[64]); + Assert.IsInstanceOf(result[65]); + } + + [Test] + public void TestLoopInvariantCodeMotion() + { + // Test that loop-invariant expressions are relocated + 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(Encapsulation.ScalarIntValue.Two, ((OpcodePush)result[21]).Argument); + Assert.IsInstanceOf(result[22]); + 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(Encapsulation.ScalarIntValue.One, ((OpcodePush)result[52]).Argument); + Assert.IsInstanceOf(result[53]); + 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.Test/Execution/SimpleTest.cs b/src/kOS.Safe.Test/Execution/SimpleTest.cs index 5e8a7309a7..0615749a52 100644 --- a/src/kOS.Safe.Test/Execution/SimpleTest.cs +++ b/src/kOS.Safe.Test/Execution/SimpleTest.cs @@ -83,7 +83,12 @@ public void TestOperators() "True", "True", "True", - "True" + "True", + "Ab", + "0", + "1", + "0", + "1" ); } diff --git a/src/kOS.Safe.Test/Opcode/FakeCpu.cs b/src/kOS.Safe.Test/Opcode/FakeCpu.cs index 3be5ffd251..fd664633ac 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/Calculator.cs b/src/kOS.Safe/Compilation/Calculator.cs index c5f16c098e..869a39b382 100644 --- a/src/kOS.Safe/Compilation/Calculator.cs +++ b/src/kOS.Safe/Compilation/Calculator.cs @@ -17,10 +17,33 @@ public abstract class Calculator public abstract object NotEqual(OperandPair pair); public abstract object Equal(OperandPair pair); - private static CalculatorScalar calculatorScalar; - private static CalculatorString calculatorString; - private static CalculatorBool calculatorBool; - private static CalculatorStructure calculatorStructure; + 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); + + 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 IsSubtractionCommutativeWithNegation(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(); + private static readonly CalculatorStructure calculatorStructure = new CalculatorStructure(); public static Calculator GetCalculator(OperandPair operandPair) { @@ -38,12 +61,36 @@ 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())); } + + 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; + 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)); + } } } \ No newline at end of file diff --git a/src/kOS.Safe/Compilation/CalculatorBool.cs b/src/kOS.Safe/Compilation/CalculatorBool.cs index 9c0b00dc3b..46b69ba0e8 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 a5d1426d0f..5c01240902 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 c7675c1df4..055f1df63e 100644 --- a/src/kOS.Safe/Compilation/CalculatorString.cs +++ b/src/kOS.Safe/Compilation/CalculatorString.cs @@ -10,26 +10,50 @@ 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 bool IsAdditionCommutative(Type leftType, Type rightType) + => false; 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 bool IsSubtractionCommutativeWithNegation(Type leftType, Type rightType) + => false; 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 bool IsMultiplicationCommmutative(Type leftType, Type rightType) + => false; 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 fa05d6e1c7..a38405d33c 100644 --- a/src/kOS.Safe/Compilation/CalculatorStructure.cs +++ b/src/kOS.Safe/Compilation/CalculatorStructure.cs @@ -28,6 +28,10 @@ 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 bool IsAdditionCommutative(Type leftType, Type rightType) + => GetCommutativityForOperation(leftType, rightType, "Add", "op_Addition", "+"); public override object Subtract(OperandPair pair) { @@ -47,6 +51,10 @@ 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 bool IsSubtractionCommutativeWithNegation(Type leftType, Type rightType) + => GetCommutativityForOperation(leftType, rightType, "Subtract", "op_Subtraction", "-"); public override object Multiply(OperandPair pair) { @@ -66,6 +74,10 @@ 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 bool IsMultiplicationCommmutative(Type leftType, Type rightType) + => GetCommutativityForOperation(leftType, rightType, "Multiply", "op_Multiply", "*"); public override object Divide(OperandPair pair) { @@ -85,6 +97,10 @@ 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 bool IsDivisionCommutative(Type leftType, Type rightType) + => GetCommutativityForOperation(leftType, rightType, "Divide", "op_Division", "/"); public override object Power(OperandPair pair) { @@ -104,6 +120,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 +141,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 +162,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 +183,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 +204,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 +225,18 @@ public override object NotEqual(OperandPair pair) return !pair.Left.Equals(pair.Right); } + public override Type GetNotEqualResultType(Type leftType, Type rightType) + { + if (CheckTypesForNull(leftType, rightType)) + return null; + 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 +256,44 @@ public override object Equal(OperandPair pair) return pair.Left.Equals(pair.Right); } + public override Type GetEqualResultType(Type leftType, Type rightType) + { + if (CheckTypesForNull(leftType, rightType)) + return null; + 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) + { + if (CheckTypesForNull(leftType, rightType)) + return null; + 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); + + 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) { @@ -225,6 +301,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); + string t2 = right == null ? "" : KOSNomenclature.GetKOSName(right); + 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 +350,74 @@ 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 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) @@ -275,6 +425,10 @@ private void CheckPairForNull(OperandPair pair, string opName) throw new InvalidOperationException(GetMessage(opName, pair)); } } + private bool CheckTypesForNull(Type left, Type right) + { + return left == null || right == null; + } private bool TryCoerceImplicit(OperandPair pair, out OperandPair resultPair) { @@ -316,5 +470,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/Compilation/CommutativeAttribute.cs b/src/kOS.Safe/Compilation/CommutativeAttribute.cs new file mode 100644 index 0000000000..dbc706784b --- /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/CompilerOptions.cs b/src/kOS.Safe/Compilation/CompilerOptions.cs index 9c16e16b62..40714786f0 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/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs new file mode 100644 index 0000000000..6f1f0263b2 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -0,0 +1,498 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.Optimization; + +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; + 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). + /// + /// + /// 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. + /// + public IRScope Scope + { + get => scope; + set + { + scope?.RemoveBlock(this); + scope = value; + 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 phi functions that define variables + /// that may have one of several different values upon entering + /// this block. + /// + /// + /// This data is populated during . + /// + 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. + /// + /// + /// This data is populated during . + /// + 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 + /// triggers. + /// + /// + /// This data is populated during . + /// + 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. + /// + public string Label => nonSequentialLabel ?? $"@BB#{ID}"; + /// + /// 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; + 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 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. + /// + 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 + + /// + /// 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(IRCodePart codePart, int startIndex, int endIndex, string nonSequentialLabel = null) + { + CodePart = codePart; + StartIndex = startIndex; + EndIndex = endIndex; + if (nonSequentialLabel == SyntheticReturnBlock.syntheticReturnLabel) + ID = uint.MaxValue; + else + { + unchecked + { + 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) + { + // Break the appropriate links + if (!successors.Remove(successor)) + throw new ArgumentException($"Cannot remove {successor} as it is not a successor."); + successor.predecessors.Remove(this); + + // Recompute the Dominance tree(s) + EstablishDominance(); + + // Recompute the Post-Dominance tree(s) + successor.EstablishPostDominance(); + } + + public 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. + /// + public void EstablishDominance() + => EstablishDominanceCore(this, GetPredecessors, GetSuccessors, GetDominator, SetDominator); + + public 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 + List reversePostOrder = GetReversePostOrder(root, getSubsequents); + + // Map block to index + Dictionary 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); + bool changed = true; + while (changed) + { + changed = false; + + foreach (BasicBlock block in reversePostOrder) + { + // Pick first predecessor with defined dominator + 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).Where(index.ContainsKey)) + { + if (predecessor == newIdom) + continue; + + if (getDominator(predecessor) != null) + newIdom = Intersect(predecessor, newIdom, index, getDominator); + } + + if (getDominator(block) != newIdom) + { + setDominator(block, newIdom); + changed = true; + } + } + } + } + private static BasicBlock Intersect(BasicBlock b1, BasicBlock b2, Dictionary index, Func getDominator) + { + while (b1 != b2) + { + while (index[b1] > index[b2]) + b1 = getDominator(b1); + + while (index[b2] > index[b1]) + b2 = getDominator(b2); + } + + return b1; + } + 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 getEdges(block)) + DepthFirstSearch(successor, visited, postorder, getEdges); + + postorder.Add(block); + } + + /// + /// Adds a parameter to this block. + /// + /// The parameter object to add. + public void AddParameter(IRParameter parameter) + { + parameters.Add(parameter); + } + /// + /// 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) + exitStackState.Push(stack.Pop()); + } + + 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; + if (addedFallthrough) + Instructions.Add(FallthroughJump); + bool first = true; + foreach (IRInstruction instruction in Instructions.Take(Instructions.Count - 1)) + { + foreach (Opcode opcode in instruction.EmitOpcodes()) + { + if (first) + { + opcode.Label = Label; + first = false; + } + yield return opcode; + } + } + foreach (IInterimOperand stackValue in exitStackState) + { + foreach (Opcode opcode in stackValue.EmitOpcodes()) + { + if (first) + { + opcode.Label = Label; + first = false; + } + yield return opcode; + } + } + if (Instructions.Any()) + { + foreach (Opcode opcode in Instructions.Last().EmitOpcodes()) + { + if (first) + { + opcode.Label = Label; + first = false; + } + yield return opcode; + } + } + 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; + } + betweenBlock.CodePart.Blocks.Add(betweenBlock); + return betweenBlock; + } + } + + public sealed class SyntheticReturnBlock : BasicBlock + { + 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/IActionInstruction.cs b/src/kOS.Safe/Compilation/IR/IActionInstruction.cs new file mode 100644 index 0000000000..08e0297e9e --- /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/IInterimOperand.cs b/src/kOS.Safe/Compilation/IR/IInterimOperand.cs new file mode 100644 index 0000000000..1256c2f1ea --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IInterimOperand.cs @@ -0,0 +1,41 @@ +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(); + + /// + /// Tests equality between operands. + /// + /// 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/IOperandInstructionBase.cs b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs new file mode 100644 index 0000000000..e54ee23fd0 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs @@ -0,0 +1,61 @@ +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); + + /// + /// 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); + } + + /// + /// Represents instructions which operate on a single operand. + /// + public interface ISingleOperandInstruction : IOperandInstructionBase + { + /// + /// Gets or sets the operand for this instruction. + /// + IInterimOperand Operand { get; set; } + } + /// + /// Represents instructions which operate on multiple operands. + /// + public interface IMultipleOperandInstruction : IOperandInstructionBase + { + /// + /// Gets the collection of operands for the instruction. + /// + IEnumerable Operands { get; } + /// + /// 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 new file mode 100644 index 0000000000..04e4f441c4 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -0,0 +1,370 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +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 + { + /// + /// 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, IRCodePart codePart, IRScope parentScope = null) + { + List blocks = new List(); + if (code.Count == 0) + return blocks; + Dictionary labels = ProgramBuilder.MapLabels(code); + CreateBlocks(code, codePart, labels, blocks, parentScope); + FillBlocks(code, labels, blocks, codePart); + return blocks; + } + + private void CreateBlocks(List code, IRCodePart codePart, Dictionary labels, List blocks, IRScope parentScope) + { + IRScope globalScope = parentScope ?? new IRScope(parentScope, null); + SortedSet leaders = new SortedSet() { 0 }; + HashSet scopePushes = new HashSet(); + HashSet scopePops = new HashSet(); + for (int i = 0; i < code.Count; i++) + { + 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) + { + throw new NotImplementedException("OpcodeJumpStack is not implemented for optimization because it is non-deterministic. Use OptimizationLevel.None."); + } + 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)) + { + int endIndex = leaders.First(i => i > startIndex) - 1; + string label = code[startIndex].Label; + if (label.StartsWith("@")) + label = null; + BasicBlock block = new BasicBlock(codePart, startIndex, endIndex, label); + + 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; + block.AddSuccessor(GetBlockFromStartIndex(blocks, destinationIndex)); + if (!(branch is OpcodeBranchJump)) + block.AddSuccessor(GetBlockFromStartIndex(blocks, block.EndIndex + 1)); + } + else if (blocks.Any(b => b.StartIndex == block.EndIndex + 1)) + { + BasicBlock successor = GetBlockFromStartIndex(blocks, block.EndIndex + 1); + block.FallthroughJump = new IRJump(block, successor, lastOpcode.SourceLine, lastOpcode.SourceColumn); + block.AddSuccessor(successor); + } +#if DEBUG + block.OriginalOpcodes = code.ToArray(); +#endif + } + + BasicBlock rootBlock = GetBlockFromStartIndex(blocks, 0); + rootBlock.EstablishDominance(); + + List exitBlocks = blocks.Where(b => !b.Successors.Any()).ToList(); + BasicBlock unifiedReturn = new SyntheticReturnBlock(codePart) { Scope = globalScope }; + foreach (BasicBlock exitBlock in exitBlocks) + exitBlock.AddSuccessor(unifiedReturn); + unifiedReturn.EstablishPostDominance(); + + 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(), 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, IRCodePart codePart) + { + Stack stack = new Stack(); + BasicBlock currentBlock = GetBlockFromStartIndex(blocks, 0); + for (int i = 0; i < code.Count; i++) + { + if (i > currentBlock.EndIndex) + { + currentBlock.SetStackState(stack); + currentBlock = GetBlockFromStartIndex(blocks, i); + } + ParseInstruction(code[i], currentBlock, stack, labels, i, blocks, codePart); + } + } + + private static IInterimOperand 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, IRCodePart codePart) + { + IInterimOperand PopStack() + =>PopFromStack(stack, currentBlock); + + switch (opcode) + { + case OpcodeStore store: + Store(PopStack(), currentBlock, store, codePart); + break; + case OpcodeStoreExist storeExist: + Store(PopStack(), currentBlock, storeExist, codePart, assertExist: true); + break; + case OpcodeStoreLocal storeLocal: + Store(PopStack(), currentBlock, storeLocal, codePart, IRAssign.StoreScope.Local); + break; + case OpcodeStoreGlobal storeGlobal: + Store(PopStack(), currentBlock, storeGlobal, codePart, IRAssign.StoreScope.Global); + break; + case OpcodeExists exists: + IResultingInstruction instruction = new IRUnaryOp(currentBlock, exists, PopStack()); + stack.Push(instruction); + break; + case OpcodeUnset unset: + IInterimOperand variableIdentifier = PopStack(); + currentBlock.Add(new IRUnset(currentBlock, unset, variableIdentifier)); + break; + case OpcodeGetMethod getMethod: + instruction = new IRSuffixGetMethod(currentBlock, PopStack(), getMethod); + stack.Push(instruction); + break; + case OpcodeGetMember getMember: + instruction = new IRSuffixGet(currentBlock, PopStack(), getMember); + stack.Push(instruction); + break; + case OpcodeSetMember setMember: + IInterimOperand value = PopStack(); + IInterimOperand memberObj = PopStack(); + currentBlock.Add(new IRSuffixSet(currentBlock, memberObj, value, setMember)); + break; + case OpcodeGetIndex getIndex: + 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(currentBlock, indexObj, targetIndex, value, setIndex)); + break; + case OpcodeEOF _: + case OpcodeEOP _: + case OpcodeNOP _: + case OpcodeBogus _: + 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); + break; + case OpcodeBranchIfTrue branchIfTrue: + int target; + if (string.IsNullOrEmpty(branchIfTrue.DestinationLabel)) + target = index + branchIfTrue.Distance; + else + target = labels[branchIfTrue.DestinationLabel]; + currentBlock.Add(new IRBranch(currentBlock, PopStack(), + GetBlockFromStartIndex(blocks, target), + GetBlockFromStartIndex(blocks, currentBlock.EndIndex + 1), + branchIfTrue)); + break; + case OpcodeBranchIfFalse branchIfFalse: + if (string.IsNullOrEmpty(branchIfFalse.DestinationLabel)) + target = index + branchIfFalse.Distance; + else + target = labels[branchIfFalse.DestinationLabel]; + currentBlock.Add(new IRBranch(currentBlock, PopStack(), + GetBlockFromStartIndex(blocks, currentBlock.EndIndex + 1), + GetBlockFromStartIndex(blocks, target), + branchIfFalse)); + break; + case OpcodeBranchJump branchJump: + int destinationIndex = branchJump.DestinationLabel != string.Empty ? labels[branchJump.DestinationLabel] : index + branchJump.Distance; + currentBlock.Add(new IRJump(currentBlock, GetBlockFromStartIndex(blocks, destinationIndex), branchJump)); + 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 _: + IInterimOperand right = PopStack(); + IInterimOperand left = PopStack(); + instruction = new IRBinaryOp(currentBlock, (BinaryOpcode)opcode, left, right); + stack.Push(instruction); + break; + case OpcodeMathNegate _: + case OpcodeLogicToBool _: + case OpcodeLogicNot _: + instruction = new IRUnaryOp(currentBlock, opcode, PopStack()); + stack.Push(instruction); + break; + case OpcodeCall call: + 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) + { + IInterimOperand stackResult = PopStack(); + if (stackResult is InterimConstantValue constant && constant.Value is Execution.KOSArgMarkerType) + break; + arguments.Push(stackResult); + } + instruction = new IRCall(currentBlock, call, hasArgmarker, arguments); + if (stack.Count > 0 && !((IRCall)instruction).Direct) + { + ((IRCall)instruction).IndirectMethod = PopStack(); + } + stack.Push(instruction); + break; + case OpcodeReturn opcodeReturn: + currentBlock.Add(new IRReturn(currentBlock, opcodeReturn.Depth, opcodeReturn) { Value = PopStack() }); + break; + case OpcodePush opcodePush: + object argument = opcodePush.Argument; + if (IsPushingVariable(opcodePush)) + stack.Push(new InterimVariableReference((string)argument, opcodePush)); + else + stack.Push(new InterimConstantValue(argument, opcodePush)); + break; + case OpcodePushDelegateRelocateLater delegateRelocateLater: + stack.Push(new IRDelegateRelocateLater(delegateRelocateLater.DestinationLabel, delegateRelocateLater.WithClosure, delegateRelocateLater)); + break; + case OpcodePushRelocateLater relocateLater: + stack.Push(new IRRelocateLater(relocateLater.DestinationLabel, relocateLater)); + break; + case OpcodeAddTrigger _: + IInterimOperand pointer = PopStack(); + currentBlock.Add(new IRUnaryConsumer(currentBlock, opcode, pointer, false)); + codePart.EnrollClosure((string)((InterimConstantValue)pointer).Value, currentBlock.Scope); + break; + case OpcodeRemoveTrigger _: + currentBlock.Add(new IRUnaryConsumer(currentBlock, opcode, PopStack(), false)); + break; + case OpcodeWait _: + currentBlock.Add(new IRUnaryConsumer(currentBlock, opcode, PopStack(), true)); + break; + 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."); + } + } + + private static void Store(IInterimOperand value, BasicBlock block, OpcodeIdentifierBase opcode, IRCodePart codePart, IRAssign.StoreScope storeScope = IRAssign.StoreScope.Ambivalent, bool assertExist = false) + { + IInterimOperand stackValue = value; + IRAssign assignment = new IRAssign(block, opcode, stackValue) { Scope = storeScope, AssertExists = assertExist }; + IRScope scope; + switch (storeScope) + { + case IRAssign.StoreScope.Local: + scope = block.Scope; + break; + case IRAssign.StoreScope.Global: + scope = block.Scope.GetGlobalScope(); + break; + default: + scope = block.Scope.GetScopeForVariableNamed(opcode.Identifier); + break; + } + block.Add(assignment); + + scope.StoreLocalVariable(opcode.Identifier); + + if (stackValue is IRRelocateLater lockOrFunctionPointer) + { + codePart.EnrollFunction(opcode.Identifier, (string)lockOrFunctionPointer.Value, block.Scope, storeScope == IRAssign.StoreScope.Global); + } + } + + private static bool IsPushingVariable(OpcodePush opcodePush) + => opcodePush.Argument is string identifier && identifier.StartsWith("$"); + } +} \ No newline at end of file diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs new file mode 100644 index 0000000000..132b0d17a4 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -0,0 +1,502 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.KS; +using kOS.Safe.Compilation.Optimization; + +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 + { + private readonly Dictionary functionRefs = new Dictionary(); + private readonly Dictionary closureScopes = + new Dictionary(); + + /// + /// 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(); + + /// + /// Gets the reachable variables for a given call site. + /// + /// This is populated in + public Dictionary> ReachableVariables { get; } = + 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. + /// + /// 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) + 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, 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)); + } + + /// + /// 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(); + 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)); + } + + /// + /// 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)); + } + + 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. + /// + /// The code part into which to emit mainline code. + 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); + } + 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. + /// + /// + 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; } + public HashSet ExternalReads { get; set; } = new HashSet(); + public HashSet ExternalWrites { get; } = new HashSet(); + 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; } + + 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, IRCodePart codePart) + { + this.trigger = trigger; + Identifier = trigger.Code.FirstOrDefault()?.Label ?? ""; + ClosureScope = codePart.closureScopes[Identifier].Scope; + Code = builder.Lower(trigger.Code, codePart, ClosureScope); + if (Code.Count > 0) + { + RootBlock = Code[0]; + } + } + /// + /// 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(); + trigger.Code.AddRange(emitter.Emit(Code)); + } + } + + /// + /// 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 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; } + /// + /// 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 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; } + /// + /// Gets a value indicating whether this instance may be recursive. + /// + /// + /// 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 && + 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 + { + 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 => + { + foreach (IRInstruction operation in instruction.DepthFirst()) + if (operation is IActionInstruction actionInstruction && + !actionInstruction.IsInert) + return false; + return true; + }) + ) + ); + } + } + + 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, IRCodePart codePart) + { + this.function = function; + (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, codePart, ClosureScope)); + } + userFunctionFragments.Reverse(); + + if (function.InitializationCode.Count > 0) + RootBlocks.Add(InitializationCode[0]); + foreach (IRFunctionFragment fragment in Fragments) + { + if (fragment.FunctionCode.Count > 0) + RootBlocks.Add(fragment.FunctionCode[0]); + } + } + + /// + /// 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(); + function.InitializationCode.AddRange(emitter.Emit(InitializationCode)); + foreach (UserFunctionCodeFragment fragment in userFunctionFragments) + { + fragments[fragment].EmitCode(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, IRCodePart codePart, IRScope ClosureScope) + { + fragment = codeFragment; + FunctionCode = builder.Lower(codeFragment.Code, codePart, ClosureScope); + } + /// + /// 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(); + fragment.Code.AddRange(emitter.Emit(FunctionCode)); + } + } + } + + /// + /// 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; 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 Name, IRUnset Instruction)> 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 scope of the closure this instance uses. + /// + IRScope ClosureScope { get; } + } + } +} diff --git a/src/kOS.Safe/Compilation/IR/IREmitter.cs b/src/kOS.Safe/Compilation/IR/IREmitter.cs new file mode 100644 index 0000000000..714305e62c --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IREmitter.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; + +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(); + Dictionary jumpLabels = new Dictionary(); + // Emit Opcodes for each block + foreach (BasicBlock block in blocks) + LabelAndEmit(block, jumpLabels, result); + // Remove single-line jumps from fallthrough blocks + 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 && jumpLabels[jump.DestinationLabel] == i + 1) + { + foreach (var key in jumpLabels.Keys.ToArray()) + if (jumpLabels[key] >= i) + jumpLabels[key] = jumpLabels[key] - 1; + result.RemoveAt(i); + i--; + } + } + // Restore original labels + foreach (Opcode opcode in result) + { + if (opcode.DestinationLabel != null && + opcode.DestinationLabel.StartsWith("@") && + jumpLabels.ContainsKey(opcode.DestinationLabel)) + { + opcode.DestinationLabel = CreateLabel(jumpLabels[opcode.DestinationLabel]); + } + } + labelIndex += result.Count; + return result; + } + + private void LabelAndEmit(BasicBlock block, Dictionary jumpLabels, List result) + { + jumpLabels.Add(block.Label, result.Count + labelIndex); + result.AddRange(block.EmitOpCodes()); + } + + private static string CreateLabel(int index) + => string.Format("@{0:0000}", index + 1); + } +} diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs new file mode 100644 index 0000000000..dab456caa9 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -0,0 +1,1180 @@ +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. + + /// + /// 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) + { + SourceLine = originalOpcode.SourceLine; + SourceColumn = originalOpcode.SourceColumn; + Block = block; + } + protected Opcode SetSourceLocation(Opcode opcode) + { + if (opcode == null) + return opcode; + opcode.SourceLine = SourceLine; + opcode.SourceColumn = SourceColumn; + return opcode; + } + 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 + { + protected IInterimOperand operand; + + protected SingleOperandInstruction(Opcode originalOpcode, BasicBlock block) : base(originalOpcode, block) { } + + IInterimOperand ISingleOperandInstruction.Operand { get => operand; set => operand = value; } + + public void ForEachOperand(Action action) + => action(operand); + + 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 + { + protected MultipleOperandInstruction(Opcode originalOpcode, BasicBlock block) : base(originalOpcode, block) { } + + 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 IInterimOperand this[int index] { get; set; } + + public void ForEachOperand(Action action) + { + foreach (IInterimOperand operand in Operands) + action(operand); + } + + 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, IActionInstruction + { + public enum StoreScope + { + Ambivalent, + Local, + 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; + public bool AssertExists { get; set; } = false; + + public IRAssign(BasicBlock block, OpcodeIdentifierBase opcode, IInterimOperand value) : base(opcode, block) + { + Value = value; + Target = new SSASetDefinition(opcode.Identifier, this); + } + public override IEnumerable EmitOpcodes() + { + if (Value != null) + foreach (Opcode opcode in Value.EmitOpcodes()) + yield return opcode; + if (AssertExists) + { + yield return SetSourceLocation(new OpcodeStoreExist(Target.Name)); + yield break; + } + switch (Scope) + { + case StoreScope.Local: + yield return SetSourceLocation(new OpcodeStoreLocal(Target.Name)); + yield break; + case StoreScope.Global: + yield return SetSourceLocation(new OpcodeStoreGlobal(Target.Name)); + yield break; + default: + case StoreScope.Ambivalent: + yield return SetSourceLocation(new OpcodeStore(Target.Name)); + yield break; + } + } + public override string ToString() + => string.Format("{{store {0} -> {1}}}", Value.ToString(), Target.ToString()); + } + public class IRBinaryOp : MultipleOperandInstruction, IResultingInstruction + { + public override bool IsInvariant => Left.IsInvariant && Right.IsInvariant; + public BinaryOpcode Operation { get; set; } + 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 Type + { + get + { + 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.Type, Right.Type); + case OpcodeMathSubtract _: + return calculator.GetSubtractResultType(Left.Type, Right.Type); + case OpcodeMathMultiply _: + return calculator.GetMultiplyResultType(Left.Type, Right.Type); + case OpcodeMathDivide _: + return calculator.GetDivideResultType(Left.Type, Right.Type); + case OpcodeMathPower _: + return calculator.GetPowerResultType(Left.Type, Right.Type); + case OpcodeCompareEqual _: + return calculator.GetEqualResultType(Left.Type, Right.Type); + case OpcodeCompareNE _: + return calculator.GetNotEqualResultType(Left.Type, Right.Type); + case OpcodeCompareGT _: + return calculator.GetGreaterThanResultType(Left.Type, Right.Type); + case OpcodeCompareLT _: + return calculator.GetLessThanResultType(Left.Type, Right.Type); + case OpcodeCompareGTE _: + return calculator.GetGreaterThanEqualResultType(Left.Type, Right.Type); + case OpcodeCompareLTE _: + return calculator.GetLessThanEqualResultType(Left.Type, Right.Type); + default: + throw new NotImplementedException(); + } + } + } + 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] + { + get => index == 0 ? Left : index == 1 ? Right : throw new ArgumentOutOfRangeException(); + set + { + if (index == 0) + Left = value; + else if (index == 1) + Right = value; + else + throw new ArgumentOutOfRangeException(); + } + } + public bool IsCommutative + { + get + { + 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.Type, Right.Type); + case OpcodeMathSubtract _: + return calculator.IsSubtractionCommutativeWithNegation(Left.Type, Right.Type); + case OpcodeMathMultiply _: + return calculator.IsMultiplicationCommmutative(Left.Type, Right.Type); + case OpcodeMathDivide _: + return calculator.IsDivisionCommutative(Left.Type, Right.Type); + 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(BasicBlock block, BinaryOpcode operation, IInterimOperand left, IInterimOperand right) : base(operation, block) + { + Operation = operation; + Left = left; + Right = right; + } + public bool SwapOperands() + { + if (!IsCommutative) + return false; + 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) + { + case OpcodeCompareGT _: + Operation = new OpcodeCompareLTE(); + break; + case OpcodeCompareLT _: + Operation = new OpcodeCompareGTE(); + break; + case OpcodeCompareGTE _: + Operation = new OpcodeCompareLT(); + break; + case OpcodeCompareLTE _: + Operation = new OpcodeCompareGT(); + break; + } + return true; + } + + public IInterimOperand Clone(BasicBlock 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() + { + foreach (Opcode opcode in Left.EmitOpcodes()) + yield return opcode; + foreach (Opcode opcode in Right.EmitOpcodes()) + yield return opcode; + Operation.Label = string.Empty; + yield return SetSourceLocation(Operation); + } + + 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 && + 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 && + obj is IEvaluatableToConstant evaluatableToConstant && + evaluatableToConstant.IsInvariant) + return Evaluate().Equals(evaluatableToConstant.Evaluate()); + return false; + } + public override int GetHashCode() + => Operation.GetType().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 && !(Operation is OpcodeExists); + public Opcode Operation { get; } + public IInterimOperand Operand { get => operand; set => operand = value; } + public Type Type + { + get + { + switch (Operation) + { + case OpcodeExists _: + case OpcodeLogicNot _: + case OpcodeLogicToBool _: + return typeof(Encapsulation.BooleanValue); + case OpcodeMathNegate _: + return Operand.Type; + default: + throw new NotImplementedException(); + } + } + } + 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; + Operand = operand; + } + + public IInterimOperand Clone(BasicBlock 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()) + yield return opcode; + Operation.Label = string.Empty; + yield return Operation; + } + 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() && + Operand.Equals(unaryOp.Operand); + public override int GetHashCode() + => (Operation.GetType(), 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, IActionInstruction + { + 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 isInert) : this(block, opcode) + => IsInert = isInert; + public override IEnumerable EmitOpcodes() + { + Operation.Label = string.Empty; + yield return Operation; + } + public override string ToString() + => Operation.ToString(); + } + public class IRUnaryConsumer : SingleOperandInstruction, IActionInstruction + { + private readonly bool operationHasSideEffects; + 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) + { + Operation = opcode; + Operand = operand; + operationHasSideEffects = sideEffects; + } + public override IEnumerable EmitOpcodes() + { + foreach (Opcode opcode in Operand.EmitOpcodes()) + yield return opcode; + Operation.Label = string.Empty; + yield return Operation; + } + public override string ToString() + => Operation.ToString(); + } + 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, true) + { + if (operand.IsInvariant) + { + string name = (string)(operand as IEvaluatableToConstant).Evaluate().Value; + Target = new SSASetDefinition(name, this); + } + } + } + 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; + + public override IEnumerable EmitOpcodes() + { + foreach (Opcode opcode in Value.EmitOpcodes()) + yield return opcode; + yield return SetSourceLocation(new OpcodePop()); + } + public override string ToString() + => $"{{pop {Value}}}"; + } + public class IRNonVarPush : IRInstruction, IResultingInstruction + { + public override bool IsInvariant => false; + public Opcode Operation { get; } + public Type Type + { + get + { + switch (Operation) + { + case OpcodeTestArgBottom _: + return typeof(Encapsulation.BooleanValue); + default: + throw new NotImplementedException(); + } + } + } + public ushort OpcodeCount => 1; + + public IRNonVarPush(BasicBlock block, Opcode opcode) : base(opcode, block) + { + Operation = opcode; + } + public IInterimOperand Clone(BasicBlock block) + => 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; + yield return Operation; + } + public override string ToString() + => Operation.ToString(); + public bool Equals(IInterimOperand other) + => other == this; + public InterimConstantValue Evaluate() + => throw new InvalidOperationException(); + } + public class IRSuffixGet : SingleOperandInstruction, IResultingInstruction + { + // 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 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; + 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()) + yield return opcode; + yield return SetSourceLocation(new OpcodeGetMember(Suffix)); + } + 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 && + obj is IRSuffixGet suffixGet && + suffixGet.IsInvariant && + !(suffixGet is IRSuffixGetMethod) && + string.Equals(Suffix, suffixGet.Suffix, StringComparison.OrdinalIgnoreCase) && + Object == suffixGet.Object); + public override int GetHashCode() + => (Object, Suffix).GetHashCode(); + + public InterimConstantValue Evaluate() + { + if (!IsInvariant) + throw new InvalidOperationException(); + throw new NotImplementedException(); +#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, 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() + { + 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 && + base.Equals(obj); + public override int GetHashCode() + => base.GetHashCode(); + } + public class IRSuffixSet : MultipleOperandInstruction, IActionInstruction + { + 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; } } + public override int OperandCount => 2; + protected override IInterimOperand this[int index] + { + get => index == 0 ? Object : index == 1 ? Value : throw new ArgumentOutOfRangeException(); + set + { + if (index == 0) + Object = value; + else if (index == 1) + Value = value; + else + throw new ArgumentOutOfRangeException(); + } + } + public string Suffix { get; } + public IRSuffixSet(BasicBlock block, IInterimOperand obj, IInterimOperand value, OpcodeSetMember opcodeSetMember) : base(opcodeSetMember, block) + { + Object = obj; + Value = value; + Suffix = opcodeSetMember.Identifier; + } + public override IEnumerable EmitOpcodes() + { + foreach (Opcode opcode in Object.EmitOpcodes()) + yield return opcode; + 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 == this || + (IsInvariant && + obj is IRSuffixSet suffixSet && + suffixSet.IsInvariant && + string.Equals(Suffix, suffixSet.Suffix, StringComparison.OrdinalIgnoreCase) && + Object.Equals(suffixSet.Object) && + Value.Equals(suffixSet.Value)); + public override int GetHashCode() + => (Object, Suffix, Value).GetHashCode(); + } + public class IRIndexGet : MultipleOperandInstruction, IResultingInstruction + { + // 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 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(); + set + { + if (index == 0) + Object = value; + else if (index == 1) + Index = value; + else + throw new ArgumentOutOfRangeException(); + } + } + public IRIndexGet(BasicBlock block, IInterimOperand obj, IInterimOperand index, OpcodeGetIndex opcode) : base(opcode, block) + { + 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()) + yield return opcode; + foreach (Opcode opcode in Index.EmitOpcodes()) + yield return opcode; + yield return SetSourceLocation(new OpcodeGetIndex()); + } + 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 && + obj is IRIndexGet indexGet && + indexGet.IsInvariant && + Object.Equals(indexGet.Object) && + Index.Equals(indexGet.Index)); + public override int GetHashCode() + => (Object, Index).GetHashCode(); + + public InterimConstantValue Evaluate() + { + if (!IsInvariant) + throw new InvalidOperationException(); + throw new NotImplementedException(); + //((Encapsulation.IIndexable)Object).GetIndex(); + } + } + public class IRIndexSet : MultipleOperandInstruction, IActionInstruction + { + 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; } + public override IEnumerable Operands { get { yield return Object; yield return Index; yield return Value; } } + public override int OperandCount => 3; + protected override IInterimOperand this[int index] + { + get => index == 0 ? Object : index == 1 ? Index : index == 2 ? Value : throw new ArgumentOutOfRangeException(); + set + { + if (index == 0) + Object = value; + else if (index == 1) + Index = value; + else if (index == 2) + Value = value; + else + throw new ArgumentOutOfRangeException(); + } + } + public IRIndexSet(BasicBlock block, IInterimOperand obj, IInterimOperand index, IInterimOperand value, OpcodeSetIndex opcode) : base(opcode, block) + { + Object = obj; + Index = index; + Value = value; + } + public override IEnumerable EmitOpcodes() + { + foreach (Opcode opcode in Object.EmitOpcodes()) + yield return opcode; + foreach (Opcode opcode in Index.EmitOpcodes()) + yield return opcode; + 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 == this || + (IsInvariant && + obj is IRIndexSet indexGet && + indexGet.IsInvariant && + Object.Equals(indexGet.Object) && + Index.Equals(indexGet.Index) && + Value.Equals(indexGet.Value)); + public override int GetHashCode() + => Object.GetHashCode(); + } + public class IRJump : IRInstruction + { + public override bool IsInvariant => true; + public BasicBlock Target { get; set; } + public IRJump(BasicBlock block, BasicBlock target, OpcodeBranchJump opcode) : base(opcode, block) + { + Target = target; + } + 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 }); + } + 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 : SingleOperandInstruction + { + public override bool IsInvariant => Distance.IsInvariant; + public IInterimOperand Distance { get => operand; set => operand = value; } + public List Targets { get; } = new List(); + public IRJumpStack(BasicBlock block, IInterimOperand distance, IEnumerable targets, OpcodeJumpStack jumpStack) : base(jumpStack, block) + { + Distance = distance; + Targets.AddRange(targets); + } + public override IEnumerable EmitOpcodes() + { + foreach (Opcode opcode in Distance.EmitOpcodes()) + 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 : SingleOperandInstruction + { + public override bool IsInvariant => Condition.IsInvariant; + 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(BasicBlock block, IInterimOperand condition, BasicBlock onTrue, BasicBlock onFalse, BranchOpcode opcodeBranch) : base(opcodeBranch, block) + { + Condition = condition; + True = onTrue; + False = onFalse; + PreferFalse = opcodeBranch is OpcodeBranchIfFalse; + } + public override IEnumerable EmitOpcodes() + { + foreach (Opcode opcode in Condition.EmitOpcodes()) + yield return opcode; + if (PreferFalse) + { + yield return SetSourceLocation(new OpcodeBranchIfFalse() { DestinationLabel = False.Label }); + yield return SetSourceLocation(new OpcodeBranchJump() { DestinationLabel = True.Label }); + } + else + { + yield return SetSourceLocation(new OpcodeBranchIfTrue() { DestinationLabel = True.Label }); + 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.?{2} {0}/{1}}}", True.Label, False.Label, PreferFalse ? "f" : "t"); + 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 : MultipleOperandInstruction, IResultingInstruction, IActionInstruction + { + 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); + 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]; + set => Arguments[index] = value; + } + public IInterimOperand IndirectMethod { get; internal set; } + public bool Direct { get; } + public bool EmitArgMarker { get; set; } + private IRCall(BasicBlock block, OpcodeCall opcode, bool emitArgMarker) : base(opcode, block) + { + Function = (string)opcode.Destination; + Direct = opcode.Direct; + EmitArgMarker = emitArgMarker; + } + private bool IsSelfInvariant + { + 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; + } + } + 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() + { + 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) + { + if (IndirectMethod != null) + foreach (Opcode opcode in IndirectMethod.EmitOpcodes()) + yield return opcode; + yield return new OpcodePush(new Execution.KOSArgMarkerType()); + } + foreach (IInterimOperand argument in Arguments) + { + foreach (Opcode opcode in argument.EmitOpcodes()) + yield return opcode; + } + yield return SetSourceLocation(new OpcodeCall(Function)); + } + + public IRCall(BasicBlock block, OpcodeCall opcode, bool emitArgMarker, IInterimOperand argument) : this(block, opcode, emitArgMarker) + { + Arguments.Add(argument); + } + public IRCall(BasicBlock block, OpcodeCall opcode, bool emitArgMarker, IEnumerable arguments) : this(block, opcode, emitArgMarker) + { + Arguments.AddRange(arguments); + } + public IRCall(BasicBlock block, OpcodeCall opcode, bool emitArgMarker, params IInterimOperand[] arguments) : this(block, opcode, emitArgMarker) + { + Arguments.AddRange(arguments); + } + 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 && + 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 && + 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) && + Arguments.SequenceEqual(call.Arguments); + public override int GetHashCode() + => Function.ToLower().GetHashCode(); + + 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. + 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 IInterimOperand Value { get => operand; set => operand = value; } + public short Depth { get; internal set; } + public IRReturn(BasicBlock block, short depth, OpcodeReturn opcode) : base(opcode, block) + => Depth = depth; + public override IEnumerable EmitOpcodes() + { + if (Value != null) + 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); + public override bool Equals(object obj) + => obj is IRReturn ret && + Value.Equals(ret.Value); + public override int GetHashCode() + => Value.GetHashCode(); + } +} diff --git a/src/kOS.Safe/Compilation/IR/IRScope.cs b/src/kOS.Safe/Compilation/IR/IRScope.cs new file mode 100644 index 0000000000..4ae1d7a82f --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IRScope.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR +{ + /// + /// This class represents a variable scope, analogous to . + /// + public class IRScope + { + private IRScope parent; + private readonly HashSet childScopes = new HashSet(); + private readonly HashSet blocks = new HashSet(); + private readonly HashSet variables = new HashSet(); + + private int nextChildIndex = 0; + private int index; + + /// + /// Gets or sets the parent scope. + /// + 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; + } + } + /// + /// 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 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. + /// + /// + /// 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 => ParentScope == null; + + /// + /// Initializes a new instance of the class. + /// + /// The parent scope. + /// The first block in this scope. + public IRScope(IRScope parent, BasicBlock headerBlock) + { + ParentScope = parent; + HeaderBlock = headerBlock; + } + + public void StoreLocalVariable(string variableName) + { + variables.Add(variableName); + } + public void StoreGlobalVariable(string variableName) + { + if (!IsGlobalScope) + ParentScope.StoreGlobalVariable(variableName); + else + StoreLocalVariable(variableName); + } + + public bool IsVariableInScope(string name, bool includeParent = true) + { + if (variables.Contains(name)) + return true; + return includeParent && (ParentScope?.IsVariableInScope(name, includeParent) ?? false); + } + + public IRScope GetScopeForVariableNamed(string name) + { + if (IsGlobalScope) + return this; + if (variables.Contains(name)) + return this; + return ParentScope.GetScopeForVariableNamed(name); + } + + public void ClearVariable(string name) + { + variables.Remove(name); + } + + public bool IsEqualOrEncompassedBy(IRScope scope) + => this == scope || IsEncompassedBy(scope); + public bool IsEncompassedBy(IRScope scope) + { + if (scope.IsGlobalScope) + return true; + if (scope.childScopes.Contains(this)) + return true; + return !IsGlobalScope && ParentScope.IsEncompassedBy(scope); + } + + public IRScope GetGlobalScope() + { + if (IsGlobalScope) + return this; + return ParentScope.GetGlobalScope(); + } + + public void EnrollBlock(BasicBlock block) + { + block.Scope?.RemoveBlock(block); + blocks.Add(block); + } + public void RemoveBlock(BasicBlock block) + => blocks.Remove(block); + + public override string ToString() + => $"IRScope: {IndexString()}"; + public string IndexString() + { + if (IsGlobalScope) + return "Global"; + if (ParentScope.IsGlobalScope) + return $"{index}"; + return $"{ParentScope.IndexString()}.{index}"; + } + } +} diff --git a/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs new file mode 100644 index 0000000000..d31696e9be --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs @@ -0,0 +1,23 @@ +using System; + +namespace kOS.Safe.Compilation.IR +{ + /// + /// Represents instructions that push a value back onto the stack. + /// + public interface IResultingInstruction : IEvaluatableToConstant, IInterimOperand + { + ushort OpcodeCount { get; } + } + + public interface IEvaluatableToConstant : IInterimOperand + { + /// + /// Evaluates the instruction to a constant value. + /// + /// 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 0000000000..a4d3672cef --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/InterimConstantValue.cs @@ -0,0 +1,100 @@ +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 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 && + Equals(constant)) || + Value.Equals(obj); + public override int GetHashCode() + => Value.GetHashCode(); + public override string ToString() + => Value.ToString(); + + InterimConstantValue IEvaluatableToConstant.Evaluate() + => this; + + IInterimOperand IInterimOperand.Clone(BasicBlock _) + => 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(); + 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 new file mode 100644 index 0000000000..44daf179d6 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/InterimVariables.cs @@ -0,0 +1,734 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR +{ + public interface IInterimVariableReference : IInterimOperand + { + string Name { get; } + short SourceLine { get; } + short SourceColumn { 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, IRInstruction instruction) : + this(name, instruction.SourceLine, instruction.SourceColumn) { } + public InterimVariableReference(string name, short sourceLine, short sourceColumn) + { + Name = name; + SourceLine = sourceLine; + SourceColumn = sourceColumn; + } + + public IInterimOperand Clone(BasicBlock _) + => this; + + public IEnumerable EmitOpcodes() + { + yield return new OpcodePush(Name) + { + SourceLine = SourceLine, + SourceColumn = SourceColumn + }; + } + + 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 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 IInterimVariableReference variable && + string.Equals(Name, variable.Name, StringComparison.OrdinalIgnoreCase) && + SourceLine == variable.SourceLine; + public override int GetHashCode() + => (Name.ToLower(), SourceLine).GetHashCode(); + } + + public readonly struct InterimResolvedReference : IInterimVariableReference, IEvaluatableToConstant + { + public short SourceLine { get; } + public short SourceColumn { get; } + public SSADefinition Reference { get; } + public string Name => Reference.Name; + public bool IsInvariant => Reference.IsInvariant; + public Type Type => Reference.Type; + + 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 IInterimOperand Clone(BasicBlock _) + => this; + + 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() + => 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); + 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 IInterimOperand Clone(BasicBlock _) + => new InterimUnresolvedReference(References, SourceLine, SourceColumn); + + public IEnumerable EmitOpcodes() + { + yield return new OpcodePush(Name) + { + SourceLine = SourceLine, + SourceColumn = SourceColumn + }; + } + + 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 && + references.SequenceEqual(unresolvedRef.references); + public override int GetHashCode() + => References.GetHashCode(); + } + + public static class SSAIndexIssuer + { + private static readonly Dictionary indices = new Dictionary(); + public static uint GetIndex(string name) + { + if (indices.ContainsKey(name)) + unchecked + { + return ++indices[name]; + } + indices[name] = 0; + return 0; + } + } + + public abstract class SSADefinition : IEquatable + { + protected readonly uint ssaIndex; + protected Dictionary potentialUnsetSites; + protected readonly Dictionary potentialClobberDefinitions = + new Dictionary(); + + public enum SetState + { + Set = 1, + PotentiallyUnset = 0, + Unset = -1 + } + + public static IEqualityComparer ReferenceEqualityComparer => SSAReferenceEqualityComparer.Instance; + + public string Name { get; } + public abstract bool IsInvariant { get; } + public abstract Type Type { get; } + public virtual SetState State { get; } + public IRInstruction AssignedAt { get; } + public HashSet ReplacedBy { get; } = new HashSet(SSAReferenceEqualityComparer.Instance); + public HashSet Replaces { get; } = new HashSet(SSAReferenceEqualityComparer.Instance); + + 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, bool writeReplaceChain); + public abstract InterimConstantValue Evaluate(); + + public SSADefinition PotentiallyOverwrite(SSASetDefinition newDefinition, bool writeReplaceChain) + { + 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)) + { + result = new SSAPotentialDefinition(this, newDefinition); + potentialClobberDefinitions[newDefinition] = result; + } + if (writeReplaceChain) + { + newDefinition.Replaces.Add(this); + ReplacedBy.Add(newDefinition); + } + 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 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() + => 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 + { + private static readonly Dictionary<(string, IRCall), SSASetDefinition> postCallDefinitions = + new Dictionary<(string, IRCall), SSASetDefinition>(); + + public IRAssign DefinedAt { get; } + 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) + { + 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, bool writeReplaceChain) + { + if (State == SetState.Unset) + return this; + + 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; + if (other == this) + return true; + return DefinedAt?.Value.Equals(setDefinition.DefinedAt.Value) ?? false; + } + return other.Equals(this); + } + } + 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) && + (Conditional.IsExecutable ? Succeeding.IsInvariant : Preceding.IsInvariant); + + public IEnumerable Operands + { + get + { + short sourceLine = Conditional?.SourceLine ?? -1; + short sourceColumn = Conditional?.SourceColumn ?? -1; + yield return new InterimResolvedReference(Preceding, sourceLine, sourceColumn); + yield return new InterimResolvedReference(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, bool writeReplaceChain) + { + 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, bool writeReplaceChain) + { + if (State == SetState.Unset) + return this; + + 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; + } + + public void ForEachOperand(Action action) + { + 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, -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) + throw new InvalidOperationException(); + if (Conditional.IsExecutable) + return Succeeding.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 + { + 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, bool writeReplaceChain) + { + 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) + { + IEnumerable possibleValues = Node.PossibleValues.Keys; + Dictionary otherPossibleValues = otherPhi.Node.PossibleValues; + 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] == Node.PossibleValues[key] || + otherPossibleValues[key].Equals(Node.PossibleValues[key]))); + } + return false; + } + + public override string ToString() + => $"{Name} #{ssaIndex}"; + } + + 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 InterimResolvedReference(kvp.Value, sourceLine, sourceColumn) as IInterimOperand; + }); + + protected override InterimConstantValue EvaluateObj(SSADefinition obj) + => obj.Evaluate(); + + 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 + { + 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 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 + { + 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) + { + 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/IR/SingleStaticAssignment.cs b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs new file mode 100644 index 0000000000..40613829ea --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/SingleStaticAssignment.cs @@ -0,0 +1,745 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.Optimization; +using static kOS.Safe.Compilation.IR.IRCodePart; + +namespace kOS.Safe.Compilation.IR +{ + /// + /// This utility class converts an IRCodePart into single static + /// assignment form. + /// + 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. + /// + public static void FinalizeSSA(IRCodePart codePart) + { + 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) + { + 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. + + // 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(trigger.RootBlock, codePart, trigger); + FlattenCallTree(trigger); + } + + // 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. + // 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) + { + foreach (BasicBlock block in trigger.Code) + ApplyUses(block, codePart, trigger); + } + foreach (BasicBlock block in codePart.MainCode) + ApplyUses(block, codePart, null); + } + + private static Dictionary<(string VariableName, IRScope Scope), SSADefinition> AnalyzeBlock(BasicBlock block, IRCodePart codePart, IClosureVariableUser funcOrTrigger) + { + 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++) + { + IRInstruction instruction = instructions[i]; + + foreach (IRCall call in instruction.DepthFirst().Where(inst => inst is IRCall).Cast()) + { + ProcessCall(call, codePart, variables, block.TriggerPropagationBlacklist, block.TriggerUnsetBlacklist, funcOrTrigger, false); + IRFunction function = codePart.GetFunction(call); + if (function != null) + funcOrTrigger?.FunctionCalls.Add(codePart.GetFunction(call)); + } + + switch (instruction) + { + case IRAssign assignment: + 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, false); + break; + case IRUnaryConsumer unaryConsumer: + // 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)((InterimConstantValue)unaryConsumer.Operand).Value; + IRTrigger trigger = codePart.GetTrigger(pointer); + ProcessTrigger(trigger, variables, block.TriggerPropagationBlacklist, block.TriggerUnsetBlacklist, false); + funcOrTrigger?.TriggersCreated.Add(trigger); + } + break; + } + } + + return variables; + } + + 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; + switch (assignment.Scope) + { + case IRAssign.StoreScope.Local: + if (writeBlacklist.TryGetValue((definition.Name, startingScope), out IRUnset unset)) + ReplaceDefinition(variables, (definition.Name, startingScope), definition.PotentiallyUnset(unset, writeReplaceChain), writeReplaceChain); + 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: + if (writeBlacklist.TryGetValue((definition.Name, startingScope), out IRUnset globalUnset)) + ReplaceDefinition(variables, (definition.Name, startingScope.GetGlobalScope()), SSAPotentialDefinition.PotentiallySet(definition, globalUnset, writeReplaceChain), writeReplaceChain); + else + 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)) + { + // 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, writeReplaceChain); + assignment.IsInert = false; + return true; + } + else + assignment.IsInert = true; + break; + } + return false; + } + private static bool ApplyDefinitionToName(SSASetDefinition definition, IRScope scope, Dictionary<(string VariableName, IRScope Scope), SSADefinition> variables, bool writeReplaceChain) + { + 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) + ReplaceDefinition(variables, (name, scope), SSAPotentialDefinition.PotentiallySet(definition, (IRUnset)lastPotential.AssignedAt, writeReplaceChain), writeReplaceChain); + else + 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 + // 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) + ReplaceDefinition(variables, (name, scope), slotValue.PotentiallyOverwrite(definition, writeReplaceChain), writeReplaceChain); + else + ReplaceDefinition(variables, (name, scope), definition.PotentiallyUnset((IRUnset)slotValue.AssignedAt, writeReplaceChain), writeReplaceChain); + scope.Assignments.Add(definition.DefinedAt); + 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) + ReplaceDefinition(variables, (name, scope), slotValue.PotentiallyOverwrite(definition, writeReplaceChain), writeReplaceChain); + else + ReplaceDefinition(variables, (name, scope), definition, writeReplaceChain); + scope.Assignments.Add(definition.DefinedAt); + break; + } + scope = scope.ParentScope; + } + 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, bool writeReplaceChain) + { + 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, readBlacklist, writeBlacklist, writeReplaceChain); + } + + 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, 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, writeReplaceChain)) + recordTo?.Add((varName, unset)); + } + } + } + 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. + SSASetDefinition target = unset.Target; + string varName = target.Name; + bool potential = false; + 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) + ReplaceDefinition(variables, (varName, scope), slotValue.PotentiallyUnset(unset, writeReplaceChain), writeReplaceChain); + else + ReplaceDefinition(variables, (varName, scope), target, writeReplaceChain); + + // 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 writeReplaceChain) + { + 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) + { + 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; + } + scope = scope.ParentScope; + } + return closureVariableAffected; + } + + 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) + { + IRScope scope = trigger.ClosureScope; + while (scope != null) + { + writeBlacklist[(varName, scope)] = unset; + if (variables.TryGetValue((varName, scope), out SSADefinition ssaDef) && + ssaDef.State != SSADefinition.SetState.Unset) + { + ReplaceDefinition(variables, (varName, scope), ssaDef.PotentiallyUnset(unset, writeReplaceChain), writeReplaceChain); + } + scope = scope.ParentScope; + } + } + foreach (string varName in trigger.ExternalWrites) + { + IRScope scope = trigger.ClosureScope; + while (scope != null) + { + readBlacklist.Add((varName, scope)); + scope = scope.ParentScope; + } + } + } + + 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, writeReplaceChain); + } + 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, 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, bool writeReplaceChain) + { + 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) + { + 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. + 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) + { + ReplaceDefinition(variables, (varName, scope), ssaDef.PotentiallyOverwrite(SSASetDefinition.FromCallSite(varName, call), writeReplaceChain), writeReplaceChain); + + 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, readBlacklist, writeBlacklist, writeReplaceChain); + } + } + + 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) && SSADefinition.ReferenceEqualityComparer.Equals(x.Item2, y.Item2); + + public int GetHashCode((IRScope, SSADefinition) obj) + => (obj.Item1.GetHashCode(), SSADefinition.ReferenceEqualityComparer.GetHashCode(obj.Item2)).GetHashCode(); + } + + private static void BuildPhis(BasicBlock root, IRCodePart codePart, IClosureVariableUser funcOrTrigger) + { + Dictionary> variablesOut = + new Dictionary>(); + + Queue worklist = new Queue(); + worklist.Enqueue(root); + while (worklist.Count > 0) + { + 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 + // Collect all incoming variable definitions + 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 + .Where(v => block.Scope.IsEqualOrEncompassedBy(v.Key.Scope))) + varsIn.Add((predecessor, variable.Key.Scope, variable.Value)); + } + } + + 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 (definitionSet.Select(def => (def.Scope, def.Variable)).Distinct(PhiComparer.Instance).Skip(1).Any()) + { + // Phi required + if (!block.Phis.TryGetValue(definitionSet.Key, out PhiNode phiVar)) + { + phiVar = new PhiNode(definitionSet.Key.Name); + block.Phis.Add(definitionSet.Key, phiVar); + } + + 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 + { + 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); + } + } + + // Store the resulting incoming variable definitions to the block. + block.IncomingVariableDefinitions = variablesIn; + + // Analyze the block with that set of incoming variable definitions + Dictionary<(string Name, IRScope Scope), SSADefinition> varsOut = + AnalyzeBlock(block, codePart, funcOrTrigger); + + // 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.ContainsKey(kvp.Key) && SSADefinition.ReferenceEqualityComparer.Equals(oldDefinition[kvp.Key], 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, IRCodePart codePart, IClosureVariableUser funcOrTrigger) + { + List instructions = block.Instructions; + + // 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)>(); + 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) + => SSAReplacement(op, block.Scope, liveDefinitions, triggerBlacklist, funcOrTrigger); + + foreach (IRInstruction instruction in block.Instructions) + { + // Process call sites and replace variable definitions. + foreach (IRInstruction inst in instruction.DepthFirst()) + { + if (inst is IRCall call) + { + IRFunction function = codePart.GetFunction(call); + if (function != null) + { + codePart.ReachableVariables[call] = DetermineCallReaches(call, function, funcOrTrigger, liveDefinitions, triggerBlacklist); + } + ProcessCall(call, codePart, liveDefinitions, triggerBlacklist, triggerWriteBlacklist, null, true); + } + if (inst is IOperandInstructionBase operandInstruction) + operandInstruction.MutateEachOperand(ScopedSSAReplacement); + } + + // Process assignments, unsets, and new triggers. + switch (instruction) + { + case IRAssign assignment: + ProcessAssignment(assignment, null, liveDefinitions, triggerBlacklist, triggerWriteBlacklist, true); + break; + case IRUnset unset: + 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, true); + } + 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) + { + IInterimVariableReference result = AttemptResolveReference( + variableRef, scope, + liveDefinitions, triggerBlacklist, out bool exceededClosure); + + if (exceededClosure) + funcOrTrigger?.ExternalReads.Add(result.Name); + return result; + } + else if (operand is IRUnaryOp existOp && existOp.Operation is OpcodeExists) + { + if (existOp.Operand.IsInvariant || existOp.Operand is IInterimVariableReference) + { + 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.IsGlobalScope) + { + 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; + } + } + + 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. + 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 != 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); + } + return reachableVariables; + } + } +} diff --git a/src/kOS.Safe/Compilation/IR/TypeInferencer.cs b/src/kOS.Safe/Compilation/IR/TypeInferencer.cs new file mode 100644 index 0000000000..32f50a609c --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/TypeInferencer.cs @@ -0,0 +1,222 @@ +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()); + return typeof(Structure); + } + + // 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}"); + return typeof(Structure); + + 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 + } + } +} diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index b31533f8cc..79fa536b96 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -202,10 +202,22 @@ public CodePart Compile(int startLineNum, ParseTree tree, Context context, Compi { PreProcess(tree); CompileProgram(tree); + if (options.OptimizationLevel != OptimizationLevel.None) + { + Optimize(part, context, options); + } } return part; } + 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); + optimizer.Optimize(irCodePart); + irCodePart.EmitCode(code); + } + private void CompileProgram(ParseTree tree) { currentCodeSection = part.MainCode; @@ -3423,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); @@ -3430,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. diff --git a/src/kOS.Safe/Compilation/KS/TriggerCollection.cs b/src/kOS.Safe/Compilation/KS/TriggerCollection.cs index fb9de153bb..82181e3965 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); diff --git a/src/kOS.Safe/Compilation/KS/UserFunction.cs b/src/kOS.Safe/Compilation/KS/UserFunction.cs index dd397cc5f8..8e2a96e808 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 23d2446d2d..87faf7cc2e 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 diff --git a/src/kOS.Safe/Compilation/Opcode.cs b/src/kOS.Safe/Compilation/Opcode.cs index ab916db15b..b7f8034646 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) @@ -1328,27 +1333,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); - } } @@ -1477,8 +1484,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); } } @@ -1500,21 +1511,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)); } } @@ -2019,7 +2031,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/Optimization/ExtendedBasicBlock.cs b/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs new file mode 100644 index 0000000000..1456d26a86 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Compilation.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.Successors) + { + 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) + { + 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) + { + 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}"))}"; + } + } +} diff --git a/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs b/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs new file mode 100644 index 0000000000..5409d98e36 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using kOS.Safe.Compilation.IR; + +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; } + } +} diff --git a/src/kOS.Safe/Compilation/Optimization/InterimCPU.cs b/src/kOS.Safe/Compilation/Optimization/InterimCPU.cs new file mode 100644 index 0000000000..478faaa4f1 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/InterimCPU.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using kOS.Safe.Encapsulation; +using kOS.Safe.Execution; + +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(); } + 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 PopValueEncapsulatedArgument(bool barewordOkay = false) + => Structure.FromPrimitive(PopValueArgument(barewordOkay)); + 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 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(); + } +} diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs new file mode 100644 index 0000000000..d48612bd72 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -0,0 +1,55 @@ +#pragma warning disable IDE0130 // Namespace does not match folder structure +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 + * 20. Constant propagation + * 30. Constant folding + * 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. !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. Also removes blocks that are not executable. + * + * O2: + * -10000. Loop condition reorganization + * 32100. 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 + * + * 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 length 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.Safe/Compilation/Optimization/OptimizationTools.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs new file mode 100644 index 0000000000..37d5ad3b9f --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs @@ -0,0 +1,33 @@ +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 DepthFirst(this IEnumerable instructions) + => instructions.SelectMany(DepthFirst); + + public static IEnumerable DepthFirst(this IRInstruction instruction) + { + if (instruction is ISingleOperandInstruction singleOperandInstruction) + { + if (singleOperandInstruction.Operand is IRInstruction inst) + foreach (IRInstruction predecessor in DepthFirst(inst)) + yield return predecessor; + } + else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) + { + foreach (IInterimOperand operand in multipleOperandInstruction.Operands) + { + if (operand is IRInstruction inst) + foreach (IRInstruction predecessor in DepthFirst(inst)) + yield return predecessor; + } + } + yield return instruction; + } + } +} diff --git a/src/kOS.Safe/Compilation/Optimization/Optimizer.cs b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs new file mode 100644 index 0000000000..6dd6f9b994 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.IR; +using kOS.Safe.Function; +using kOS.Safe.Utilities; + +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 + { + public static HashSet PassesToSkip { get; } = new HashSet(); + + 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 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; } + /// + /// 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; } + /// + /// Gets the function manager. + /// + public static IFunctionManager FunctionManager => shared.FunctionManager; + + static Optimizer() + { + shared.FunctionManager = new FunctionManager(shared); + } + + /// + /// Initializes a new instance of the class. + /// + /// The optimization level to be applied. + public Optimizer(CompilerOptions options) + { + OptimizationLevel = options.OptimizationLevel; + AllowClobberBuiltins = options.AllowClobberBuiltins; + + foreach (Type type in availablePassTypes) + { + if (type != typeof(Passes.SCCPWithTypePropagation)) + { + if (PassesToSkip.Contains(type)) + continue; + } + + IOptimizationPass pass = (IOptimizationPass)Activator.CreateInstance(type); + optimizationPasses.Add(pass); + if (pass is ILinkedOptimizationPass linkedPass) + linkedPass.Optimizer = this; + } + } + 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; + Blocks = codePart.Blocks; + RootBlocks = new HashSet(codePart.RootBlocks); + + List rootExtendedBlocks = new List( + codePart.RootBlocks.Select(b => ExtendedBasicBlock.CreateExtendedBlockTree(b))); + ExtendedBlocks = new List( + rootExtendedBlocks.SelectMany(ExtendedBasicBlock.DumpTree)); + + foreach (IOptimizationPass pass in optimizationPasses) + { + 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) + { + case IHolisticOptimizationPass codePartpass: + codePartpass.ApplyPass(Code); + 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/Optimization/Passes/AlgebraicSimplifications.cs b/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs new file mode 100644 index 0000000000..659222d499 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/AlgebraicSimplifications.cs @@ -0,0 +1,430 @@ +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, bool allowClobberBuiltins) + { + 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 _: + 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, allowClobberBuiltins); + } + } + // 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, allowClobberBuiltins); + } + } + // 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, allowClobberBuiltins); + } + } + break; + case OpcodeMathMultiply _: + // X^N*X=X*X^N=X^(N+1) + { + if (binaryOp.Left is IRBinaryOp opL && + opL.Operation is OpcodeMathPower && + opL.Left.Equals(binaryOp.Right)) + { + 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, allowClobberBuiltins); + } + } + // 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, allowClobberBuiltins); + } + } + break; + } + return binaryOp; + } + + 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); + } + 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); + } + // Fallthrough to return the original operand without changes. + 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, bool allowClobberBuiltins) + { + // 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, allowClobberBuiltins); + + // 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(); + } + } + return instruction; + } + + private static IRBinaryOp DividePowers(IRBinaryOp instruction, bool allowClobberBuiltins) + => CombinePowers(instruction, new OpcodeMathSubtract(), allowClobberBuiltins); + + private static IRBinaryOp CombinePowers(IRBinaryOp instruction, bool allowClobberBuiltins) + => CombinePowers(instruction, new OpcodeMathAdd(), allowClobberBuiltins); + + 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) + + 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, allowClobberBuiltins); + + // 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(); + } + } + 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/BlockOrdering.cs b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs new file mode 100644 index 0000000000..44218b7180 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/BlockOrdering.cs @@ -0,0 +1,561 @@ +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; } + + // 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 + // (if the optimization level is Balanced or above). + public void ApplyPass(IRCodePart codePart) + { + if (Optimizer.OptimizationLevel >= OptimizationLevel.Minimal) + ApplyOrdering(codePart, ExecutableBlocksPredicate); + else + ApplyOrdering(codePart, AllBlocksPredicate); + } + + + // 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) + 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); + } + + /// + /// 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); + + // 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!"); + + // 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); + + while (sequenceStarts.Count > 0) + { + MetaBlockSequence sequence = ConstructMetaSequence(sequenceStarts.Dequeue(), regionExits, out Queue newStarts); + foreach (BasicBlock start in newStarts) + sequenceStarts.Enqueue(start); + metaSequences.Add(sequence); + } + + // 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. + // 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) + { + List instructions = b.Instructions; + int branchIdx = instructions.Count - 1; + if (branchIdx >= 0 && instructions[branchIdx] is IRBranch branch) + { + if (!inclusionPredicate(branch.True)) + instructions[branchIdx] = new IRJump(branch.Block, branch.False, branch.SourceLine, branch.SourceColumn); + else if (!inclusionPredicate(branch.False)) + 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) + { + // 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 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 (IdentifyLoop(block, regionExits.Peek(), out LoopData loopData) && root != loopData.body) + { + // Add the header to the current sequence, if necessary. + if (loopData.body != block) + sequence.Add(block); + // 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; + } + // If branchData.elseBlock == root, that's because this is the branch + // instruction at the end of a loop body. + else if (IdentifyBranch(block, regionExits.Peek(), out BranchData branchData) && + branchData.elseBlock != root && branchData.ifBlock != root) + { + // Add the branching block to the current sequence. + // The root is already included. + if (block != root) + sequence.Add(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. + regionExits.Push(branchData.exit ?? regionExits.Peek()); + sequence.Add(ConstructMetaSequence(branchData.ifBlock, regionExits, out Queue childOffshoots)); + regionExits.Pop(); + // 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; + } + } + else + { + if (block != root) + sequence.Add(block); + // This condition occurs only in branches/loops, or with a single successor. + if (block.PostDominator?.Dominator == block) + { + // Set that successor as the next block for this sequence. + block = block.PostDominator; + } + else + return sequence; + } + } + return sequence; + } + + public static bool IdentifyLoop(BasicBlock block, BasicBlock regionExit, out LoopData loopData) + { + loopData = new LoopData(); + // --- Case 1: while/for loop --- + // The header block has a conditional branch. One successor + // is inside the loop (the body) and one is outside (the + // exit). The latch has a back edge to this header. + if (block.Successors.Count == 2 && HasBranchInstruction(block)) + { + foreach (BasicBlock successor in block.Successors) + { + // A successor is a latch if it (or the end of its linear chain) + // has a back edge to 'block', meaning 'block' dominates it. + BasicBlock latch = FindLatch(successor, block); + if (latch != null) + { + // The other successor is the exit. + BasicBlock bodyEntry = successor; + BasicBlock exit = block.Successors.First(s => s != successor); + + // 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; + + // 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 (block == ancestor) + return true; + block = block.Dominator; + } + 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(); + + // Get the branch instruction (or return false). + if (branchingBlock.Instructions.Count == 0) + return false; + if (!(branchingBlock.Instructions[branchingBlock.Instructions.Count - 1] is IRBranch branch)) + return false; + + // Use the context cues for which branch is which. + // This is critical for loops. + BasicBlock ifBlock, elseBlock, rejoinsAt; + if (!branch.PreferFalse) + { + ifBlock = branch.False; + elseBlock = branch.True; + } + else + { + ifBlock = branch.True; + elseBlock = branch.False; + } + + // Find the next post-dominator that is in the region. + // This accounts for branch bodies that break from a loop + // or return from a function. + rejoinsAt = FindLocalMerge(branchingBlock, regionExit); + + // Use actual control flow to correct assumptions about + // which block is which. + 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 BranchData(branchingBlock, ifBlock, elseBlock, rejoinsAt); + return true; + } + private static BasicBlock GetSequenceEnd(BasicBlock block) + { + while (block.PostDominator?.Dominator == block) + block = block.PostDominator; + return block; + } + 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; + } + 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. + BasicBlock block = regionExit; + while (block != null) + { + if (block == candidate) + return false; + block = block.PostDominator; + } + return true; + } + + 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) + { + this.branch = branch; + this.ifBlock = ifBlock; + this.elseBlock = elseBlock; + this.exit = exit; + } + } + 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 branchBlock, BasicBlock body, BasicBlock exit) + { + this.header = header; + this.branchBlock = branchBlock; + 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/CommonExpressionElimination.cs b/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs new file mode 100644 index 0000000000..1891e8b3e8 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/CommonExpressionElimination.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class CommonExpressionElimination : IHolisticOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.Balanced; + public short SortIndex => 32100; + + public void ApplyPass(IRCodePart codePart) + { + foreach (BasicBlock rootBlock in codePart.RootBlocks) + EliminateCommonExpressions(rootBlock); + } + private void EliminateCommonExpressions(BasicBlock rootBlock) + { + // Track expressions at the outermost context for + // this code part unit. + Dictionary<(IResultingInstruction Expression, IRScope Scope), ExpressionData> 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: + if (useCount < 3) + continue; + break; + case 3: + if (useCount < 2) + continue; + 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)) + { + first = false; + return storedExpression; + } + return op; + }); + // Replace subsequent uses with the temporary variable. + foreach (IOperandInstructionBase use in data.uses) + { + use.MutateEachOperand(op => + op.Equals(expression) ? + new InterimResolvedReference(storedExpression.Definition, (IRInstruction)use) : op); + } + } + } + private void IdentifyExpressions(BasicBlock block, Dictionary<(IResultingInstruction Expression, IRScope Scope), ExpressionData> expressions) + { + foreach (IRInstruction instruction in block.Instructions) + { + foreach (IRInstruction subexpression in instruction.DepthFirst()) + { + 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.IsInvariant) + 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); + } + } + + if (breaking) + break; + if (subexpression is IOperandInstructionBase operandInstruction) + operandInstruction.ForEachOperand(AddToExpressions); + } + } + } + // 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) + => obj.Expression.GetHashCode(); + } + private readonly struct ExpressionData + { + public readonly IOperandInstructionBase origin; + public readonly List uses; + public ExpressionData(IOperandInstructionBase origin) + { + this.origin = origin; + 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); + // 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; + + 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) + => 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 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(); + } + } +} diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs new file mode 100644 index 0000000000..5da0cbeeb6 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using kOS.Safe.Compilation.IR; +using kOS.Safe.Exceptions; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class ConstantFolding : IOptimizationPass, ILinkedOptimizationPass + { + public Optimizer Optimizer { get; set; } + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + public short SortIndex => 30; + + public void ApplyPass(List blocks) + { + IEnumerator enumerator = blocks.GetEnumerator(); + while (enumerator.MoveNext()) + { + ApplyPass(enumerator.Current, Optimizer.AllowClobberBuiltins); + } + } + 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]; + if (instruction is IRPop pop) + { + if (pop.IsInvariant) + { + block.Instructions.RemoveAt(i); + i--; + continue; + } + } + 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) + { + (permanentBlock, deprecatedBlock) = Convert.ToBoolean(branchConstant.Value) ? (branch.True, branch.False) : (branch.False, branch.True); + block.Instructions[i] = new IRJump(block, permanentBlock, new OpcodeBranchJump() { SourceLine = branch.SourceLine, SourceColumn = branch.SourceColumn }); + block.RemoveSuccessor(deprecatedBlock); + } + } + } + foreach (IRInstruction inst in instruction.DepthFirst()) + { + if (inst is IOperandInstructionBase operandInstruction) + { + operandInstruction.MutateEachOperand(AttemptReduction_Internal); + } + } + } + } + + public static IInterimOperand AttemptReduction(IInterimOperand input, bool allowClobberBuiltins) + { + // 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, allowClobberBuiltins); + } + + private static IInterimOperand Simplify(IInterimOperand input, bool allowClobberBuiltins) + { + switch (input) + { + case IRUnaryOp unaryOp: + return AlgebraicSimplifications.AttemptUnarySimplification(unaryOp); + case IRBinaryOp binaryOp: + return AttemptBinarySimplification(binaryOp, allowClobberBuiltins); + default: + return input; + } + } + + private static IInterimOperand AttemptBinarySimplification(IRBinaryOp instruction, bool allowClobberBuiltins) + { + 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)) + { + 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.Left is InterimConstantValue constantL) + { + // If this is true, both are constants + if (instruction.Right is InterimConstantValue) + { + // 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; + } + + // Reorder/reflow operations if it helps to fold constants + if (instruction.Right is IRBinaryOp rightOp && + OperationsHaveEqualPriority(instruction.Operation, rightOp.Operation)) + { + // L _ (L1 _ R1) + // C _ (A _ B) = (C _ A) _ B (doesn't require commutativity) + if (rightOp.Left is InterimConstantValue constantRL) + { + InterimConstantValue result = ExecuteOperation(instruction, constantL, constantRL); + if (result != null) + { + rightOp.Left = result; + instruction = rightOp; + } + } + // 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) + { + instruction.Right = instruction.Left; + instruction.Left = result; + } + } + } + + // Shortcuts for math operations where both sides don't need to be constant + switch (instruction.Operation) + { + case OpcodeMathMultiply _: + // 0 * X = 0 + if (Encapsulation.ScalarIntValue.Zero.Equals(constantL.Value)) + return constantL; + if (ReduceDivMult(instruction, constantL, out IInterimOperand newResult)) + return newResult; + break; + 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 (Encapsulation.ScalarIntValue.Zero.Equals(constantL.Value)) + return constantL; + if (ReduceDivMult(instruction, constantL, out newResult)) + return newResult; + break; + case OpcodeMathAdd _: + case OpcodeMathSubtract _: + // 0 +- X = X + if (Encapsulation.ScalarIntValue.Zero.Equals(constantL.Value)) + return instruction.Right; + break; + } + } + 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 _: + // X / 0 = Error + if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + throw new KOSCompileException(instruction, new DivideByZeroException()); + break; + case OpcodeMathPower _: + // X^0 = 1 + if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + return new InterimConstantValue(Encapsulation.ScalarIntValue.One, instruction); + // X^1 = X + if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) + return instruction.Left; + break; + } + } + else // Neither operand is constant + { + switch (instruction.Operation) + { + case OpcodeMathDivide _: + // 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.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; + } + + private static bool OperationsHaveEqualPriority(BinaryOpcode operation1, BinaryOpcode operation2) + { + 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 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 + if (Encapsulation.ScalarIntValue.One.Equals(constantOperand.Value)) + { + newResult = instruction.Right; + return true; + } + // -1 */ X = -X + if (constantOperand.Value.Equals(-Encapsulation.ScalarIntValue.One)) + { + newResult = new IRUnaryOp( + instruction.Block, + new OpcodeMathNegate() + { + SourceColumn = instruction.SourceColumn, + SourceLine = instruction.SourceLine + }, + instruction.Right); + return true; + } + newResult = null; + return false; + } + } +} 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 0000000000..e3ebe808b9 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/LoopConditionalRelocation.cs @@ -0,0 +1,113 @@ +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 IEnumerable GetEdges(BasicBlock block) + => block.Successors.Where(BlockOrdering.AllBlocksPredicate); + + private static void ApplyPass(BasicBlock root) + { + List reversePostOrder = BasicBlock.GetReversePostOrder(root, GetEdges); + Stack regionExits = new Stack(); + regionExits.Push(reversePostOrder[reversePostOrder.Count - 1]); + + List loopData = FindLoops(root, regionExits); + + 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); + } + + public static List FindLoops(BasicBlock root, Stack regionExits) + { + List loopData = new List(); + FindLoops(root, regionExits, loopData, false); + return loopData; + } + private static void FindLoops(BasicBlock root, Stack regionExits, List loops, bool fromLoop) + { + BasicBlock block = root; + while (block != null && block != regionExits.Peek()) + { + // 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)) + { + if (!fromLoop || root != loopData.body) + loops.Add(loopData); + if (root != loopData.body) + { + regionExits.Push(loopData.exit); + FindLoops(loopData.body, regionExits, loops, true); + 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) && + branchData.elseBlock != root && branchData.ifBlock != root) + { + regionExits.Push(branchData.exit ?? regionExits.Peek()); + FindLoops(branchData.ifBlock, regionExits, loops, false); + regionExits.Pop(); + if (branchData.exit == null && branchData.elseBlock != null) + { + block = branchData.elseBlock; + } + else + { + if (branchData.elseBlock != null) + FindLoops(branchData.elseBlock, regionExits, loops, false); + block = branchData.exit; + } + } + else if (block.PostDominator?.Dominator == block) + { + block = block.PostDominator; + } + else + return; + } + } + } +} 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 0000000000..6cdd2e4a0d --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/LoopInvariantCodeMotion.cs @@ -0,0 +1,148 @@ +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 => 2100; + + 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.PostDominator != loop.exit) + 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); + int originalCount = relocatedInstructions.Count; + + 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 && + originalCount != relocatedInstructions.Count) + { + 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 || + instructionList[insertionIndex - 1] is IRJump || + instructionList[insertionIndex - 1] is IRJumpStack)) + insertionIndex--; + return insertionIndex; + } + } +} 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 0000000000..b300fb2f32 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + 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_Internal); + + InstructionPeepholeFilter(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 IInterimOperand OperandPeepholeFilter(IInterimOperand operand, bool allowClobberBuiltins) + { + // These calls may not be what is expected if clobbering is permitted. + if (!allowClobberBuiltins) + { + // 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 + 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 + 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) + { + // Branch logical simplification (e.g. !X branch = X branch!) + if (instruction is IRBranch branch && + branch.Condition is IRUnaryOp negateBranch && + negateBranch.Operation is OpcodeLogicNot) + { + ReplaceRedundantNotBranch(branch); + return; + } + } + + private static IRSuffixGet ReplaceParameterlessSuffix(IRCall call) + { + IRSuffixGet suffixMethod = (IRSuffixGet)call.IndirectMethod; + return new IRSuffixGet( + call.Block, + suffixMethod.Object, + new OpcodeGetMember(suffixMethod.Suffix) + { + SourceLine = suffixMethod.SourceLine, + SourceColumn = suffixMethod.SourceColumn + }); + } + + private static IRBinaryOp ReplaceVectorDotProduct(IRCall call) + { + return new IRBinaryOp( + call.Block, + new OpcodeMathMultiply() + { + SourceLine = call.SourceLine, + SourceColumn = call.SourceColumn + }, + call.Arguments[0], + call.Arguments[1]); + } + + private static IRSuffixGet AttemptReplaceIndexGetWithSuffixGet(IRIndexGet indexGet) + { + if (indexGet.Index is InterimConstantValue indexConstant && + indexConstant.Value is Encapsulation.StringValue stringIndex && + StringUtil.IsValidIdentifier(stringIndex)) + { + return new IRSuffixGet( + indexGet.Block, + indexGet.Object, + new OpcodeGetMember(stringIndex) + { + SourceLine = indexGet.SourceLine, + SourceColumn = indexGet.SourceColumn + }); + } + return null; + } + + private static IRInstruction AttemptReplaceIndexSetWithSuffixSet(IRIndexSet indexSet) + { + if (indexSet.Index is InterimConstantValue indexConstant && + indexConstant.Value is Encapsulation.StringValue stringIndex && + StringUtil.IsValidIdentifier(stringIndex)) + { + return new IRSuffixSet( + indexSet.Block, + indexSet.Object, + indexSet.Value, + new OpcodeSetMember(stringIndex) + { + SourceLine = indexSet.SourceLine, + SourceColumn = indexSet.SourceColumn + }); + } + return null; + } + + private static void ReplaceRedundantNotBranch(IRBranch branch) + { + branch.Condition = AlgebraicSimplifications.GetDoubleNestedValue(branch); + branch.PreferFalse = !branch.PreferFalse; + (branch.True, branch.False) = (branch.False, branch.True); + } + } +} 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 0000000000..e4ac9a3ed4 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SCCPWithTypePropagation.cs @@ -0,0 +1,431 @@ +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 : IHolisticOptimizationPass, 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 => -1000; + + public Optimizer Optimizer { private get; set; } + + public void ApplyPass(IRCodePart codePart) + { + codePart.VariableUses = MapUsesAndPropagateTypes(codePart); + + if (Optimizer.OptimizationLevel == OptimizationLevel.None || + Optimizer.PassesToSkip.Contains(typeof(SCCPWithTypePropagation))) + return; + + HashSet usedDefinitions = PropagateConstants(codePart.VariableUses); + + if (Optimizer.OptimizationLevel >= OptimizationLevel.Balanced) + foreach (BasicBlock block in codePart.Blocks) + RemoveRedundantAssignments(block, usedDefinitions); + } + + /// + /// 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>(SSADefinition.ReferenceEqualityComparer); + HashSet visitedBlocks = new HashSet(); + Dictionary typeAndInvarianceCache = new Dictionary(SSADefinition.ReferenceEqualityComparer); + + // 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) + { + // Visit every expression and phi in a block + // Queue that block's successors + 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 (PhiNode phi in block.Phis.Values) + { + foreach (SSADefinition variable in phi.PossibleValues.Values) + GetOrCreate(variableUses, variable).Add(phi); + + VisitInstruction(phi, blockQueue, typeAndInvarianceCache); + } + + // Process instructions. + foreach (IRInstruction instruction in block.Instructions) + { + foreach (IOperandInstructionBase inst in instruction.DepthFirst().Where(i => i is IOperandInstructionBase).Cast()) + { + inst.ForEachOperand(op => + { + if (op is IInterimVariableReference reference && + !(op is InterimVariableReference)) + { + 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. + if (inst is IRCall call) + { + IRCodePart.IRFunction function = codePart.GetFunction(call); + if (function != null) + { + HashSet variables = codePart.ReachableVariables[call]; + foreach (SSADefinition variable in variables.SelectMany(GetSSADefinitionsFromReferences)) + { + GetOrCreate(variableUses, variable).Add(call); + } + } + } + // 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, typeAndInvarianceCache); + } + + + if (instruction is IOperandInstructionBase operandInstruction) + { + operandInstruction.ForEachOperand(op => + { + 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 && + variableUses.TryGetValue(assignment.Target, 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()); + + 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. + while (instructionQueue.Count > 0) + { + IOperandInstructionBase instruction = instructionQueue.Dequeue(); + if (VisitInstruction(instruction, blockQueue, typeAndInvarianceCache)) + { + if (instruction is IRAssign assignment) + foreach (IOperandInstructionBase use in GetOrCreate(variableUses, assignment.Target)) + instructionQueue.Enqueue(use); + else if (instruction is PhiNode phi) + foreach (IOperandInstructionBase use in GetOrCreate(variableUses, phi.Result)) + instructionQueue.Enqueue(use); + } + } + } + } + + 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. + /// + /// true if the instruction needs to propagate changes. + private static bool VisitInstruction(IOperandInstructionBase instruction, + Queue blockQueue, + Dictionary typeAndInvarianceCache) + { + if (instruction is IRAssign assignment) + { + SSASetDefinition ssaVariable = assignment.Target; + if (typeAndInvarianceCache.TryGetValue(ssaVariable, out (Type storedType, bool storedInvariance) cached)) + { + typeAndInvarianceCache[ssaVariable] = (ssaVariable.Type, cached.storedInvariance & ssaVariable.IsInvariant); + return cached != typeAndInvarianceCache[ssaVariable]; + } + else + { + typeAndInvarianceCache[ssaVariable] = (ssaVariable.Type, ssaVariable.IsInvariant); + return true; + } + } + else if (instruction is PhiNode phi) + { + if (typeAndInvarianceCache.TryGetValue(phi.Result, out (Type storedType, bool storedInvariance) cached)) + { + typeAndInvarianceCache[phi.Result] = (phi.Type, cached.storedInvariance & phi.IsInvariant); + return cached != typeAndInvarianceCache[phi.Result]; + } + else + { + typeAndInvarianceCache[phi.Result] = (phi.Type, phi.IsInvariant); + return true; + } + } + else if (instruction is IRBranch branch) + { + if (branch.IsInvariant) + { + InterimConstantValue constantCondition = (branch.Condition as IEvaluatableToConstant).Evaluate(); + bool result = Convert.ToBoolean(constantCondition.Value); + + blockQueue.Enqueue(result ? branch.True : branch.False); + } + else + { + blockQueue.Enqueue(branch.True); + blockQueue.Enqueue(branch.False); + } + } + + return false; + } + + private static HashSet GetOrCreate(Dictionary> dictionary, SSADefinition key) + { + 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. + /// + private static HashSet PropagateConstants(Dictionary> ssaUses) + { + 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()))); + List usedVariables = ssaUses.Keys.ToList(); + usedVariables.Sort(Comparer.Create((a, b) => string.Compare(a.ToString(), b.ToString()))); +#endif + 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; + })), SSADefinition.ReferenceEqualityComparer); + + Dictionary replacements = new Dictionary(); + + foreach (SSADefinition ssaDef in constantVariables) + { + replacements[ssaDef] = ssaDef.Evaluate(); + } + + foreach (SSADefinition constantDef in constantVariables) + { + 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; + if (instruction is IRUnset) + continue; + instruction.MutateEachOperand(Propagate); + } + } + + requiredDefinitions.UnionWith(ssaUses.Keys.Except(constantVariables)); + + return requiredDefinitions; + } + + 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)) + { + 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(definition)) + { + return constant; + } + return operand; + } + } +} diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs new file mode 100644 index 0000000000..f1137de31d --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class SuffixReplacement : IOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + public short SortIndex => 10; + + public void ApplyPass(List code) + { + for (int i = 0; i < code.Count; i++) + { + IRInstruction instruction = code[i]; + foreach (IRInstruction inst in instruction.DepthFirst()) + { + if (inst is IOperandInstructionBase operandInstruction) + { + operandInstruction.MutateEachOperand(AttemptReplacement); + } + } + } + } + + private static IInterimOperand AttemptReplacement(IInterimOperand operand) + { + if (operand is IRSuffixGet suffixGet) + return AttempReplaceSuffix(suffixGet); + return operand; + } + private static IInterimOperand AttempReplaceSuffix(IRSuffixGet suffixGet) + { + if (suffixGet.Object is InterimVariableReference variableReference) + { + if (variableReference.Name.Equals("$ship", StringComparison.OrdinalIgnoreCase)) + { + 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 + default: + return suffixGet; + } + return new InterimVariableReference($"${suffixGet.Suffix}", suffixGet.SourceLine, suffixGet.SourceColumn); + } + if (variableReference.Name.Equals("$constant", StringComparison.OrdinalIgnoreCase)) + { + return ReplaceConstantSuffix(suffixGet); + } + } + else if (suffixGet.Object is IRCall call && call.Function.Equals("constant()", StringComparison.OrdinalIgnoreCase) && call.Arguments.Count == 0) + { + return ReplaceConstantSuffix(suffixGet); + } + return suffixGet; + } + private static InterimConstantValue ReplaceConstantSuffix(IRSuffixGet suffixGet) + { + Optimizer.InterimCPU.PushArgumentStack(new Encapsulation.ConstantValue()); + try + { + new OpcodeGetMember(suffixGet.Suffix).Execute(Optimizer.InterimCPU); + } + catch (Exception e) + { + throw new Exceptions.KOSCompileException(suffixGet, e); + } + return new InterimConstantValue(Optimizer.InterimCPU.PopValueArgument(), suffixGet); + } + } +} 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 0000000000..32fe845ed3 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs @@ -0,0 +1,129 @@ +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.Assignments.Any(a => a.Block.IsExecutable)) + 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; + 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) + { + 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); + } + } + } + } +} diff --git a/src/kOS.Safe/Compilation/ProgramBuilder.cs b/src/kOS.Safe/Compilation/ProgramBuilder.cs index 14908bb4c1..1f46337730 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++) { diff --git a/src/kOS.Safe/Encapsulation/Lexicon.cs b/src/kOS.Safe/Encapsulation/Lexicon.cs index e616035670..a8d77ca0f7 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, 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 5650380282..a87ab2af52 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, 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 c2c36af9c9..da4a875826 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, 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 9c91ac7e3e..4433cce0bf 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, 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 1eed16dae6..11ba15de6f 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, IsInert = true)] public class FunctionStack : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/StringValue.cs b/src/kOS.Safe/Encapsulation/StringValue.cs index a7a509499b..b02097aab2 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.Safe/Encapsulation/UniqueSetValue.cs b/src/kOS.Safe/Encapsulation/UniqueSetValue.cs index 4e0e1c0da9..341f8c1af4 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, IsInert = true)] public class FunctionSet : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Exceptions/KOSBinaryOperandTypeException.cs b/src/kOS.Safe/Exceptions/KOSBinaryOperandTypeException.cs index c8cd52b22c..e30959ac5a 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)) + { + } } } diff --git a/src/kOS.Safe/Exceptions/KOSCompileException.cs b/src/kOS.Safe/Exceptions/KOSCompileException.cs index 37320cae0e..4ae21be2fa 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, System.Exception innerException) + : this(new LineCol(irInstruction.SourceLine, irInstruction.SourceColumn), innerException.Message) + { + } + public KOSCompileException(LineCol location, string message) { Location = location; diff --git a/src/kOS.Safe/Function/FunctionAttribute.cs b/src/kOS.Safe/Function/FunctionAttribute.cs index 6fb891d874..73c8104d3a 100644 --- a/src/kOS.Safe/Function/FunctionAttribute.cs +++ b/src/kOS.Safe/Function/FunctionAttribute.cs @@ -1,11 +1,52 @@ -using System; +using System; 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; + set + { + 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)}."); +#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/FunctionManager.cs b/src/kOS.Safe/Function/FunctionManager.cs index 2c0043fb6d..2d61f04843 100644 --- a/src/kOS.Safe/Function/FunctionManager.cs +++ b/src/kOS.Safe/Function/FunctionManager.cs @@ -10,7 +10,10 @@ 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 readonly HashSet inertFunctions = new HashSet(); private static readonly Dictionary rawAttributes = new Dictionary(); public FunctionManager(SafeSharedObjects shared) @@ -21,7 +24,11 @@ public FunctionManager(SafeSharedObjects shared) public void Load() { - functions = new Dictionary(StringComparer.OrdinalIgnoreCase); + functions.Clear(); + functionTypes.Clear(); + invariantFunctions.Clear(); + inertFunctions.Clear(); + foreach (FunctionAttribute attr in rawAttributes.Keys) { var type = rawAttributes[attr]; @@ -32,6 +39,11 @@ public void Load() if (functionName != string.Empty) { functions.Add(functionName, (SafeFunctionBase)functionObject); + functionTypes.Add(functionName, attr.ReturnType); + if (attr.IsInvariant) + invariantFunctions.Add(functionName); + if (attr.IsInert) + inertFunctions.Add(functionName); } } } @@ -70,5 +82,23 @@ public bool Exists(string functionName) { return functions.ContainsKey(functionName); } + + public bool IsFunctionInvariant(string functionName) + { + return invariantFunctions.Contains(functionName); + } + public bool IsFunctionInert(string functionName) + { + return inertFunctions.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 1a243698f7..5133aba744 100644 --- a/src/kOS.Safe/Function/IFunctionManager.cs +++ b/src/kOS.Safe/Function/IFunctionManager.cs @@ -5,5 +5,8 @@ public interface IFunctionManager void Load(); 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 554a8c062a..f04476723e 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = true)] 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 = 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")] + [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")] + [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 b0a7d64e2d..0e70faeaee 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 = 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")] + [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")] + [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")] + [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")] + [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) } } - [FunctionAttribute("load")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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 59a248e969..227508e04c 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 = 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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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 3367b373d6..9f6fe919a0 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, IsInert = 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, 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 f8e53cd512..27f4559a35 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = 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, 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 6c10d9c0ce..59cff6a8ac 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, 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 d4127657ff..5e7cd77e23 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, 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 30e1e64853..333cb57225 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(KACAlarmWrapper), IsInvariant = false, IsInert = 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, IsInert = true)] 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, 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 84376baff6..82c2586c48 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, 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 8850e087e7..430727bd5c 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, 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")] + [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")] + [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")] + [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 eb32c6619f..070634eafd 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 = 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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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 4b21299740..bed0012752 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 = 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 0667db142c..badde9a134 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 = 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 abf577fe28..f0f5215298 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = 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, IsInert = true)] 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, IsInert = 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, IsInert = 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, IsInert = 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 = 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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [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")] + [Function("allwaypoints", ReturnType = typeof(ListValue), IsInvariant = false, IsInert = true)] 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, IsInert = true)] 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, IsInert = 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, IsInert = false)] public class FunctionTransfer : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Screen/Interpreter.cs b/src/kOS/Screen/Interpreter.cs index 997fc3d201..41acd0f2fe 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), diff --git a/src/kOS/Suffixed/Direction.cs b/src/kOS/Suffixed/Direction.cs index d644d628fb..63815a6b34 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 8b15e3dd5a..1aca98c814 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 a6f9f5629f..adafb08dfd 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"); }