From 8e20927c092ec276aeccc8d13769937957e49a25 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 1 Mar 2026 22:53:04 -0500 Subject: [PATCH 01/56] Adds tests for ensuring the output from the optimizing compiler. Add OptimizationLevel enum to select level of optimizaiton; BaseIntegrationTest uses no optimization. Default the interpreter to no optimization since it won't be worth it there. --- .../Execution/BaseIntegrationTest.cs | 4 +- .../Execution/OptimizationTest.cs | 122 ++++++++++++++++++ src/kOS.Safe/Compilation/CompilerOptions.cs | 2 + src/kOS.Safe/Compilation/OptimizationLevel.cs | 43 ++++++ src/kOS/Screen/Interpreter.cs | 3 +- 5 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 src/kOS.Safe.Test/Execution/OptimizationTest.cs create mode 100644 src/kOS.Safe/Compilation/OptimizationLevel.cs diff --git a/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs b/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs index 615e99466..67b9db229 100644 --- a/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs +++ b/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs @@ -39,6 +39,7 @@ public abstract class BaseIntegrationTest private SafeSharedObjects shared; private Screen screen; private string baseDir; + protected virtual OptimizationLevel OptimizationLevel => OptimizationLevel.None; private string FindKerboscriptTests() { @@ -86,7 +87,8 @@ protected void RunScript(string fileName) IsCalledFromRun = false, FuncManager = shared.FunctionManager, BindManager = shared.BindingMgr, - AllowClobberBuiltins = SafeHouse.Config.AllowClobberBuiltIns + AllowClobberBuiltins = SafeHouse.Config.AllowClobberBuiltIns, + OptimizationLevel = OptimizationLevel }); cpu.Boot(); diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs new file mode 100644 index 000000000..bae72f254 --- /dev/null +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -0,0 +1,122 @@ +using System; +using NUnit.Framework; +using kOS.Safe.Compilation; + +namespace kOS.Safe.Test.Execution +{ + [TestFixture] + public class OptimizationTest : BaseIntegrationTest + { + protected override OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + + [Test] + public void TestBasic() + { + // Tests that the bare minimum works + + RunScript("integration/basic.ks"); + RunSingleStep(); + AssertOutput( + "text" + ); + } + + [Test] + public void TestVars() + { + // Tests that basic variable assignment and reference works + + RunScript("integration/vars.ks"); + RunSingleStep(); + AssertOutput( + "1", + "2", + "3" + ); + } + + [Test] + public void TestFunc() + { + // Tests that basic no-args function calls work + + RunScript("integration/func.ks"); + RunSingleStep(); + AssertOutput( + "a" + ); + } + + [Test] + public void TestFuncArgs() + { + // Tests that explicit and default function parameters work + + RunScript("integration/func_args.ks"); + RunSingleStep(); + AssertOutput( + "0", + "1", + "2", + "3" + ); + } + + [Test] + public void TestOperators() + { + // Test that all the basic operators work + + RunScript("integration/operators.ks"); + RunSingleStep(); + AssertOutput( + "1", + "1", + "True", + "True", + "3", + "6", + "2", + "9", + "True", + "True", + "True", + "True", + "True", + "True", + "True", + "True" + ); + } + + [Test] + public void TestLock() + { + // Test that locks in the same file works + RunScript("integration/lock.ks"); + RunSingleStep(); + AssertOutput( + "3", + "4", + "5" + ); + } + + [Test] + public void TestSuffixes() + { + // Test that various suffix and index combinations work for getting and setting + RunScript("integration/suffixes.ks"); + RunSingleStep(); + RunSingleStep(); + AssertOutput( + "0", + "1", + "2", + "3", + "0", + "False" + ); + } + } +} diff --git a/src/kOS.Safe/Compilation/CompilerOptions.cs b/src/kOS.Safe/Compilation/CompilerOptions.cs index 9c16e16b6..40714786f 100644 --- a/src/kOS.Safe/Compilation/CompilerOptions.cs +++ b/src/kOS.Safe/Compilation/CompilerOptions.cs @@ -18,6 +18,7 @@ public class CompilerOptions public bool AllowClobberBuiltins { get; set; } public IFunctionManager FuncManager { get; set; } public IBindingManager BindManager { get; set; } + public OptimizationLevel OptimizationLevel { get; set; } public CompilerOptions() { LoadDefaults(); @@ -30,6 +31,7 @@ private void LoadDefaults() BindManager = null; IsCalledFromRun = true; AllowClobberBuiltins = false; + OptimizationLevel = OptimizationLevel.Balanced; } public bool BuiltInFunctionExists(string identifier) diff --git a/src/kOS.Safe/Compilation/OptimizationLevel.cs b/src/kOS.Safe/Compilation/OptimizationLevel.cs new file mode 100644 index 000000000..c0cdc63b4 --- /dev/null +++ b/src/kOS.Safe/Compilation/OptimizationLevel.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace kOS.Safe.Compilation +{ + /* + * O1: + * Replace ship fields with their alias + * Constant propagation, including items from the constant structure + * Replace lex indexing with string constant with suffixing where possible + * Dead code elimination (for the never used case) + * Replace constant() with constant + * O2: + * Redundant expression elimination + * Particularly: Any expression of 3 opcodes used more than twice, + * or any expression of >3 opcodes used more than once + * Replace parameterless suffix method calls with get member (this feels like cheating...) + * Boolean logical simplification + * Code motion - moving expressions outside loops if they do not depend on loop variables + * Replace X * X * ... * X with X^N + * Carry's - moving the N-1 D lookup of >1D lists outside the innermost loop + * Loop jamming? (Combining adjacent loops into one) + * Unswitching (moving conditional evaluation outside the loop) - low priority + * Linear function test replacement - low priority + * O3: + * Local function inlining + * Constant propagation to local functions + * Loop stack manipulation (delayed setting of either index or aggregator) + * O4: + * Constant loop unrolling + */ + public enum OptimizationLevel : int + { + None = 0, // No changes whatsoever + Minimal = 1, // Only changes that trim opcodes without changing any flow + Balanced = 2, // Only changes that trim opcodes without increasing file size + Aggressive = 3, // Favor trimming opcodes over file size. + Extreme = 4 // Include constant loop unrolling + } +} diff --git a/src/kOS/Screen/Interpreter.cs b/src/kOS/Screen/Interpreter.cs index 997fc3d20..41acd0f2f 100644 --- a/src/kOS/Screen/Interpreter.cs +++ b/src/kOS/Screen/Interpreter.cs @@ -140,7 +140,8 @@ protected void CompileCommand(string commandText) FuncManager = Shared.FunctionManager, BindManager = Shared.BindingMgr, AllowClobberBuiltins = SafeHouse.Config.AllowClobberBuiltIns, - IsCalledFromRun = false + IsCalledFromRun = false, + OptimizationLevel = OptimizationLevel.None }; List commandParts = Shared.ScriptHandler.Compile(new InterpreterPath(this), From 844d8df81de032a178e7f0badd47a623f6e1a53f Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 1 Mar 2026 22:59:08 -0500 Subject: [PATCH 02/56] Initial implementation of Three-Address Code interim representation. IRBuilder generates basic blocks, which IREmitter can then convert back to kRISC opcodes. This initial implementation does not accomplish any optimization yet (the opposite sometimes) but all current tests pass on the outputted opcodes. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 85 +++ src/kOS.Safe/Compilation/IR/IRBuilder.cs | 272 +++++++++ src/kOS.Safe/Compilation/IR/IREmitter.cs | 47 ++ src/kOS.Safe/Compilation/IR/IRInstruction.cs | 588 +++++++++++++++++++ src/kOS.Safe/Compilation/IR/IROptimizer.cs | 23 + src/kOS.Safe/Compilation/IR/IRValue.cs | 89 +++ src/kOS.Safe/Compilation/KS/Compiler.cs | 16 + src/kOS.Safe/Compilation/Opcode.cs | 2 +- src/kOS.Safe/Compilation/ProgramBuilder.cs | 12 +- 9 files changed, 1130 insertions(+), 4 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/BasicBlock.cs create mode 100644 src/kOS.Safe/Compilation/IR/IRBuilder.cs create mode 100644 src/kOS.Safe/Compilation/IR/IREmitter.cs create mode 100644 src/kOS.Safe/Compilation/IR/IRInstruction.cs create mode 100644 src/kOS.Safe/Compilation/IR/IROptimizer.cs create mode 100644 src/kOS.Safe/Compilation/IR/IRValue.cs diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs new file mode 100644 index 000000000..ff3b992d1 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR +{ + public class BasicBlock + { + public int StartIndex { get; } + public int EndIndex { get; } + public List Instructions { get; } = new List(); + public List Predecessors { get; } = new List(); + public string Label => $"@BB#{ID}"; + public int ID { get; } + private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. +#if DEBUG + internal Opcode[] OriginalOpcodes { get; set; } + internal Opcode[] GeneratedOpcodes => EmitOpCodes().ToArray(); +#endif + + public BasicBlock(int startIndex, int endIndex, int id) + { + StartIndex = startIndex; + EndIndex = endIndex; + ID = id; + } + + public void Add(IRInstruction instruction) + => Instructions.Add(instruction); + + public void SetStackState(Stack stack) + { + while (stack.Count > 0) + exitStackState.Push(stack.Pop()); + } + + public override string ToString() + { + return $"BasicBlock#{ID}: {StartIndex}-{EndIndex}; {Instructions.Count} Instructions"; + } + + public IEnumerable EmitOpCodes() + { + bool first = true; + if (Instructions.Any()) + { + foreach (IRInstruction instruction in Instructions.Take(Instructions.Count - 1)) + { + foreach (Opcode opcode in instruction.EmitOpcode()) + { + if (first) + { + opcode.Label = Label; + first = false; + } + yield return opcode; + } + } + } + foreach (IRValue stackValue in exitStackState) + { + foreach (Opcode opcode in stackValue.EmitPush()) + { + if (first) + { + opcode.Label = Label; + first = false; + } + yield return opcode; + } + } + if (Instructions.Any()) + { + foreach (Opcode opcode in Instructions.Last().EmitOpcode()) + { + if (first) + { + opcode.Label = Label; + first = false; + } + yield return opcode; + } + } + } + } +} diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs new file mode 100644 index 000000000..8d0e5e895 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR +{ + public class IRBuilder + { + private int nextTempId = 0; + + public List Blocks { get; } = new List(); + + public void Lower(List code) + { + if (code.Count == 0) + return; + Dictionary labels = ProgramBuilder.MapLabels(code); + CreateBlocks(code, labels); + FillBlocks(code, labels); + } + + private void CreateBlocks(List code, Dictionary labels) + { + SortedSet leaders = new SortedSet() { 0 }; + for (int i = 1; i < code.Count; i++) // The first instruction is always a leader so we can skip 0. + { + if (code[i] is BranchOpcode branch) + { + leaders.Add(i + 1); + if (branch.DestinationLabel != string.Empty) + leaders.Add(labels[branch.DestinationLabel]); + else + leaders.Add(i + branch.Distance); + } + else if (code[i] is OpcodeJumpStack jumpstack) + { + throw new NotImplementedException("OpcodeJumpStack is not implemented for optimization because it is non-deterministic. Use OptimizationLevel.None."); + } + else if (code[i] is OpcodeReturn) + { + leaders.Add(i + 1); + } + //else if (code[i] is OpcodePushRelocateLater relocateLater) + //{ + //leaders.Add(labels[relocateLater.DestinationLabel]); + //} + } + leaders.Add(code.Count); + foreach (int startIndex in leaders.Take(leaders.Count - 1)) + { + int endIndex = leaders.First(i => i > startIndex) - 1; + BasicBlock block = new BasicBlock(startIndex, endIndex, Blocks.Count); + Blocks.Add(block); + } + foreach (BasicBlock block in Blocks) + { + Opcode lastOpcode = code[block.EndIndex]; + if (lastOpcode is BranchOpcode branch) + { + int destinationIndex = branch.DestinationLabel != string.Empty ? labels[branch.DestinationLabel] : block.EndIndex + branch.Distance; + Blocks.First(b => b.StartIndex == destinationIndex).Predecessors.Add(block); + if (!(branch is OpcodeBranchJump)) + Blocks.First(b => b.StartIndex == block.EndIndex + 1).Predecessors.Add(block); + } +#if DEBUG + block.OriginalOpcodes = code.ToArray(); +#endif + } + } + + private void FillBlocks(List code, Dictionary labels) + { + Stack stack = new Stack(); + BasicBlock currentBlock = Blocks.First(b => b.StartIndex == 0); + for (int i = 0; i < code.Count; i++) + { + if (i > currentBlock.EndIndex) + { + currentBlock.SetStackState(stack); + currentBlock = Blocks.First(b => b.StartIndex == i); + } + ParseInstruction(code[i], currentBlock, stack, labels, i); + } + } + + private IRTemp CreateTemp() + { + IRTemp result = new IRTemp(nextTempId); + nextTempId++; + return result; + } + + private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack stack, Dictionary labels, int index) + { + HashSet variables = new HashSet(); + switch (opcode) + { + case OpcodeStore store: + IRAssign assignment = new IRAssign(store, stack.Pop()) { Scope = IRAssign.StoreScope.Ambivalent }; + variables.Add(assignment.Target); + currentBlock.Add(assignment); + break; + case OpcodeStoreExist storeExist: + assignment = new IRAssign(storeExist, stack.Pop()) { AssertExists = true }; + variables.Add(assignment.Target); + currentBlock.Add(assignment); + break; + case OpcodeStoreLocal storeLocal: + assignment = new IRAssign(storeLocal, stack.Pop()) { Scope = IRAssign.StoreScope.Local }; + currentBlock.Add(assignment); + variables.Add(assignment.Target); + break; + case OpcodeStoreGlobal storeGlobal: + assignment = new IRAssign(storeGlobal, stack.Pop()) { Scope = IRAssign.StoreScope.Global }; + currentBlock.Add(assignment); + variables.Add(assignment.Target); + break; + case OpcodeExists exists: + IRTemp temp = CreateTemp(); + IRInstruction instruction = new IRUnaryOp(temp, exists, stack.Pop()); + temp.Parent = instruction; + //currentBlock.Add(instruction); + stack.Push(temp); + break; + case OpcodeUnset unset: + currentBlock.Add(new IRUnaryConsumer(unset, stack.Pop(), true)); + break; + case OpcodeGetMethod getMethod: + temp = CreateTemp(); + instruction = new IRSuffixGetMethod(temp, stack.Pop(), getMethod); + //currentBlock.Add(instruction); + temp.Parent = instruction; + stack.Push(temp); + break; + case OpcodeGetMember getMember: + temp = CreateTemp(); + instruction = new IRSuffixGet(temp, stack.Pop(), getMember); + temp.Parent = instruction; + //currentBlock.Add(instruction); + stack.Push(temp); + break; + case OpcodeSetMember setMember: + IRValue value = stack.Pop(); + IRValue memberObj = stack.Pop(); + currentBlock.Add(new IRSuffixSet(memberObj, value, setMember)); + break; + case OpcodeGetIndex _: + IRValue targetIndex = stack.Pop(); + IRValue indexObj = stack.Pop(); + temp = CreateTemp(); + instruction = new IRIndexGet(temp, indexObj, targetIndex); + //currentBlock.Add(instruction); + temp.Parent = instruction; + stack.Push(temp); + break; + case OpcodeSetIndex _: + value = stack.Pop(); + targetIndex = stack.Pop(); + indexObj = stack.Pop(); + currentBlock.Add(new IRIndexSet(indexObj, targetIndex, value)); + break; + case OpcodeEOF _: + case OpcodeEOP _: + case OpcodeNOP _: + case OpcodeBogus _: + case OpcodePushScope _: + case OpcodePopScope _: + currentBlock.Add(new IRNoStackInstruction(opcode)); + break; + case OpcodeBranchIfTrue branchIfTrue: + currentBlock.Add(new IRBranch(stack.Pop(), + Blocks.First(b => b.StartIndex == labels[branchIfTrue.DestinationLabel]), + Blocks.First(b => b.StartIndex == currentBlock.EndIndex + 1))); + break; + case OpcodeBranchIfFalse branchIfFalse: + currentBlock.Add(new IRBranch(stack.Pop(), + Blocks.First(b => b.StartIndex == currentBlock.EndIndex + 1), + Blocks.First(b => b.StartIndex == labels[branchIfFalse.DestinationLabel])) + { PreferFalse = true }); + break; + case OpcodeBranchJump branchJump: + int destinationIndex = branchJump.DestinationLabel != string.Empty ? labels[branchJump.DestinationLabel] : index + branchJump.Distance; + if (branchJump.DestinationLabel == string.Empty) + { + // TODO + bool test = index == currentBlock.EndIndex; + } + currentBlock.Add(new IRJump(Blocks.First(b => b.StartIndex == destinationIndex))); + break; + case OpcodeJumpStack _: + throw new NotImplementedException("OpcodeJumpStack is not implemented for optimization because it is non-deterministic. Use OptimizationLevel.None."); + case OpcodeCompareGT _: + case OpcodeCompareLT _: + case OpcodeCompareGTE _: + case OpcodeCompareLTE _: + case OpcodeCompareNE _: + case OpcodeCompareEqual _: + case OpcodeMathAdd _: + case OpcodeMathSubtract _: + case OpcodeMathMultiply _: + case OpcodeMathDivide _: + case OpcodeMathPower _: + temp = CreateTemp(); + IRValue right = stack.Pop(); + IRValue left = stack.Pop(); + instruction = new IRBinaryOp(temp, (BinaryOpcode)opcode, left, right); + //currentBlock.Add(instruction); + temp.Parent = instruction; + stack.Push(temp); + break; + case OpcodeMathNegate _: + case OpcodeLogicToBool _: + case OpcodeLogicNot _: + temp = CreateTemp(); + instruction = new IRUnaryOp(temp, opcode, stack.Pop()); + temp.Parent = instruction; + //currentBlock.Add(instruction); + stack.Push(temp); + break; + case OpcodeCall call: + temp = CreateTemp(); + Stack arguments = new Stack(); + bool hasArgmarker = stack.Count > 0; // Not even an argument marker on the stack - the dominator block must have it + while (stack.Count > 0) + { + IRValue stackResult = stack.Pop(); + if (stackResult is IRConstant constant && constant.Value is Execution.KOSArgMarkerType) + break; + arguments.Push(stackResult); + } + instruction = new IRCall(temp, call, hasArgmarker, arguments); + if (stack.Count > 0 && !((IRCall)instruction).Direct) + { + ((IRCall)instruction).IndirectMethod = stack.Pop(); + } + temp.Parent = instruction; + currentBlock.Add(instruction); + stack.Push(temp); + break; + case OpcodeReturn opcodeReturn: + currentBlock.Add(new IRReturn(opcodeReturn.Depth) { Value = stack.Pop() }); + break; + case OpcodePush opcodePush: + object argument = opcodePush.Argument; + if (argument is string identifier && identifier.StartsWith("$")) + stack.Push(new IRVariable(identifier)); + else + stack.Push(new IRConstant(argument)); + break; + case OpcodePushDelegateRelocateLater delegateRelocateLater: + stack.Push(new IRDelegateRelocateLater(delegateRelocateLater.DestinationLabel, delegateRelocateLater.WithClosure)); + break; + case OpcodePushRelocateLater relocateLater: + stack.Push(new IRRelocateLater(relocateLater.DestinationLabel)); + break; + case OpcodeAddTrigger _: + case OpcodeRemoveTrigger _: + currentBlock.Add(new IRUnaryConsumer(opcode, stack.Pop(), false)); + break; + case OpcodeWait _: + currentBlock.Add(new IRUnaryConsumer(opcode, stack.Pop(), true)); + break; + case OpcodePop pop: + stack.Pop(); + currentBlock.Add(new IRPop()); + break; + default: + throw new NotImplementedException($"The Opcode of type {opcode.GetType()} is not implemented."); + } + } + } +} \ No newline at end of file diff --git a/src/kOS.Safe/Compilation/IR/IREmitter.cs b/src/kOS.Safe/Compilation/IR/IREmitter.cs new file mode 100644 index 000000000..31d90d999 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IREmitter.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR +{ + public static class IREmitter + { + public static List Emit(List blocks) + { + List result = new List(); + Dictionary jumpLabels = new Dictionary(); + // Emit Opcodes for each block + foreach (BasicBlock block in blocks) + { + jumpLabels.Add(block.Label, result.Count); + result.AddRange(block.EmitOpCodes()); + } + // Remove single-line jumps from fallthrough blocks + for (int i = 0; i < result.Count; i++) + { + Opcode opcode = result[i]; + if (i < result.Count - 1 && opcode is OpcodeBranchJump jump && + jump.DestinationLabel != null && jump.DestinationLabel == result[i + 1].Label) + { + foreach (BasicBlock block in blocks) + if (jumpLabels[block.Label] >= i) + jumpLabels[block.Label] = jumpLabels[block.Label] - 1; + result.RemoveAt(i); + i--; + } + } + // Restore original labels + foreach (Opcode opcode in result) + { + if (opcode.Label != null && jumpLabels.ContainsKey(opcode.Label)) + opcode.Label = CreateLabel(jumpLabels[opcode.Label]); + if (opcode.DestinationLabel != null && jumpLabels.ContainsKey(opcode.DestinationLabel)) + opcode.DestinationLabel = CreateLabel(jumpLabels[opcode.DestinationLabel]); + } + return result; + } + + private static string CreateLabel(int index) + => string.Format("@{0:0000}", index); + } +} diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs new file mode 100644 index 000000000..b8a4f2fd4 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -0,0 +1,588 @@ +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR +{ + public abstract class IRInstruction + { + // Should-be-static + public abstract bool SideEffects { get; } + internal abstract IEnumerable EmitOpcode(); + } + public abstract class IRInteractsInstruction : IRInstruction + { + public override bool SideEffects { get; } + public IRInteractsInstruction(IRValue interactor) + { + switch (interactor.Type) + { + case IRValue.ValueType.Value: + SideEffects = false; + break; + default: + SideEffects = true; + break; + } + } + } + public class IRAssign : IRInstruction + { + public enum StoreScope + { + Ambivalent, + Local, + Global + } + public override bool SideEffects => false; + public string Target { get; } + public IRValue Value { get; } + public StoreScope Scope { get; set; } = StoreScope.Ambivalent; + public bool AssertExists { get; set; } = false; + public IRAssign(string target, IRValue value) + { + Target = target; + Value = value; + } + public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : this(opcode.Identifier, value) { } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Value.EmitPush()) + yield return opcode; + if (AssertExists) + { + yield return new OpcodeStoreExist(Target); + yield break; + } + switch (Scope) + { + case StoreScope.Local: + yield return new OpcodeStoreLocal(Target); + yield break; + case StoreScope.Global: + yield return new OpcodeStoreGlobal(Target); + yield break; + default: + case StoreScope.Ambivalent: + yield return new OpcodeStore(Target); + yield break; + } + } + public override string ToString() + => string.Format("{{store {0}}}", Value.ToString()); + } + public class IRBinaryOp : IRInstruction + { + public override bool SideEffects => false; + public IRTemp Result { get; } + public BinaryOpcode Operation { get; protected set; } + public IRValue Left { get; protected set; } + public IRValue Right { get; protected set; } + public bool Commutative { get; } + public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) + { + Result = result; + Operation = operation; + Left = left; + Right = right; + Commutative = (operation is OpcodeCompareEqual) || + (operation is OpcodeCompareNE) || + (operation is OpcodeCompareGT) || + (operation is OpcodeCompareLT) || + (operation is OpcodeCompareGTE) || + (operation is OpcodeCompareLTE) || + (operation is OpcodeMathAdd) || + (operation is OpcodeMathMultiply); + } + public void ReverseOperands() + { + if (!Commutative) + throw new System.InvalidOperationException($"{this} is not commutative."); + (Right, Left) = (Left, Right); + switch (Operation) + { + case OpcodeCompareGT _: + Operation = new OpcodeCompareLTE(); + break; + case OpcodeCompareLT _: + Operation = new OpcodeCompareGTE(); + break; + case OpcodeCompareGTE _: + Operation = new OpcodeCompareLT(); + break; + case OpcodeCompareLTE _: + Operation = new OpcodeCompareGT(); + break; + } + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Left.EmitPush()) + yield return opcode; + foreach (Opcode opcode in Right.EmitPush()) + yield return opcode; + Operation.Label = string.Empty; + yield return Operation; + } + public override string ToString() + => Operation.ToString(); + } + public class IRUnaryOp : IRInstruction + { + public override bool SideEffects => false; + public IRTemp Result { get; } + public Opcode Operation { get; } + public IRValue Operand { get; } + public IRUnaryOp(IRTemp result, Opcode operation, IRValue operand) + { + Result = result; + Operation = operation; + Operand = operand; + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Operand.EmitPush()) + yield return opcode; + Operation.Label = string.Empty; + yield return Operation; + } + public override string ToString() + => Operation.ToString(); + } + public class IRNoStackInstruction : IRInstruction + { + public override bool SideEffects => false; + public Opcode Operation { get; } + public IRNoStackInstruction(Opcode opcode) + => Operation = opcode; + internal override IEnumerable EmitOpcode() + { + Operation.Label = string.Empty; + yield return Operation; + } + public override string ToString() + => Operation.ToString(); + } + public class IRUnaryConsumer : IRInstruction + { + public override bool SideEffects { get; } + public Opcode Operation { get; } + public IRValue Operand { get; } + public IRUnaryConsumer(Opcode opcode, IRValue operand, bool sideEffects = false) + { + Operation = opcode; + Operand = operand; + SideEffects = sideEffects; + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Operand.EmitPush()) + yield return opcode; + Operation.Label = string.Empty; + yield return Operation; + } + public override string ToString() + => Operation.ToString(); + } + public class IRPop : IRInstruction + { + public override bool SideEffects => false; + internal override IEnumerable EmitOpcode() + { + yield return new OpcodePop(); + } + public override string ToString() + => "{pop}"; + } + public class IRNonVarPush : IRInstruction + { + public override bool SideEffects => false; + public Opcode Operation { get; } + public IRNonVarPush(Opcode opcode) + => Operation = opcode; + internal override IEnumerable EmitOpcode() + { + Operation.Label = string.Empty; + yield return Operation; + } + public override string ToString() + => Operation.ToString(); + } + public class IRSuffixGet : IRInteractsInstruction + { + public IRTemp Result { get; } + public IRValue Object { get; } + public string Suffix { get; } + public IRSuffixGet(IRTemp result, IRValue obj, string suffix) : base(obj) + { + Result = result; + Object = obj; + Suffix = suffix; + } + public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcode) : this(result, obj, opcode.Identifier) { } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Object.EmitPush()) + yield return opcode; + yield return new OpcodeGetMember(Suffix); + } + public override string ToString() + => string.Format("{{gmb \"{0}\"}}", Suffix); + } + public class IRSuffixGetMethod : IRSuffixGet + { + public override bool SideEffects => false; + public IRSuffixGetMethod(IRTemp result, IRValue obj, string suffix) : base(result, obj, suffix) { } + public IRSuffixGetMethod(IRTemp result, IRValue obj, OpcodeGetMethod opcode) : this(result, obj, opcode.Identifier) { } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Object.EmitPush()) + yield return opcode; + yield return new OpcodeGetMethod(Suffix); + } + public override string ToString() + => string.Format("{{gmet \"{0}\"}}", Suffix); + } + public class IRSuffixSet : IRInteractsInstruction + { + public override bool SideEffects { get; } + public IRValue Object { get; } + public IRValue Value { get; } + public string Suffix { get; } + public IRSuffixSet(IRValue obj, IRValue value, string suffix) : base(obj) + { + Object = obj; + Value = value; + Suffix = suffix; + } + public IRSuffixSet(IRValue obj, IRValue value, OpcodeSetMember opcode) : this(obj, value, opcode.Identifier) { } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Object.EmitPush()) + yield return opcode; + foreach (Opcode opcode in Value.EmitPush()) + yield return opcode; + yield return new OpcodeSetMember(Suffix); + } + public override string ToString() + => string.Format("{{smb \"{0}\"}}", Suffix); + } + public class IRIndexGet : IRInstruction + { + public override bool SideEffects => false; + public IRValue Result { get; } + public IRValue Object { get; } + public IRValue Index { get; } + public IRIndexGet(IRValue result, IRValue obj, IRValue index) + { + Result = result; + Object = obj; + Index = index; + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Object.EmitPush()) + yield return opcode; + foreach (Opcode opcode in Index.EmitPush()) + yield return opcode; + yield return new OpcodeGetIndex(); + } + public override string ToString() + => "{gidx}"; + } + public class IRIndexSet : IRInstruction + { + public override bool SideEffects => false; + public IRValue Object { get; } + public IRValue Index { get; } + public IRValue Value { get; } + public IRIndexSet(IRValue obj, IRValue index, IRValue value) + { + Object = obj; + Index = index; + Value = value; + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Object.EmitPush()) + yield return opcode; + foreach (Opcode opcode in Index.EmitPush()) + yield return opcode; + foreach (Opcode opcode in Value.EmitPush()) + yield return opcode; + yield return new OpcodeSetIndex(); + } + public override string ToString() + => "{sidx}"; + } + public class IRJump : IRInstruction + { + public override bool SideEffects => false; + public BasicBlock Target { get; } + public IRJump(BasicBlock target) + { + Target = target; + } + internal override IEnumerable EmitOpcode() + { + yield return new OpcodeBranchJump() { DestinationLabel = Target.Label }; + } + public override string ToString() + => string.Format("{{jump {0}}}", Target.Label); + } + public class IRJumpStack : IRInstruction + { + public override bool SideEffects => false; + public IRValue Distance { get; } + public List Targets { get; } = new List(); + public IRJumpStack(IRValue distance, IEnumerable targets) + { + Distance = distance; + Targets.AddRange(targets); + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Distance.EmitPush()) + yield return opcode; + yield return new OpcodeJumpStack(); + } + } + public class IRBranch : IRInstruction + { + public override bool SideEffects => false; + public IRValue Condition { get; } + public BasicBlock True { get; } + public BasicBlock False { get; } + public bool PreferFalse { get; set; } = false; + public IRBranch(IRValue condition, BasicBlock onTrue, BasicBlock onFalse) + { + Condition = condition; + True = onTrue; + False = onFalse; + } + internal override IEnumerable EmitOpcode() + { + foreach (Opcode opcode in Condition.EmitPush()) + yield return opcode; + if (PreferFalse) + { + yield return new OpcodeBranchIfFalse() { DestinationLabel = False.Label }; + yield return new OpcodeBranchJump() { DestinationLabel = True.Label }; + } + else + { + yield return new OpcodeBranchIfTrue() { DestinationLabel = True.Label }; + yield return new OpcodeBranchJump() { DestinationLabel = False.Label }; + } + } + public override string ToString() + => string.Format("{{br.? {0}/{1}}}", True.Label, False.Label); + } + public class IRCall : IRInstruction + { + protected static readonly Function.FunctionManager functionManager = new Function.FunctionManager(null); + public override bool SideEffects { get; } + public IRTemp Target { get; } + public string Function { get; } + public List Arguments { get; } = new List(); + public IRValue IndirectMethod { get; internal set; } + public bool Direct { get; } + public bool EmitArgMarker; + private IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker) + { + Target = target; + Function = (string)opcode.Destination; + Direct = opcode.Direct; + SideEffects = CheckIfFunctionHasSideEffects(opcode); + EmitArgMarker = emitArgMarker; + } + private bool CheckIfFunctionHasSideEffects(OpcodeCall opcode) + { + if (!Direct) + return true; + if (opcode.Destination is string functionName && functionManager.Exists(functionName)) + { + functionName = functionName.ToLower().Replace("()", ""); + switch (functionName) + { + case "addAlarm": + case "listAlarms": + case "deleteAlarm": + case "buildlist": + case "vcrs": + case "vectorcrossproduct": + case "vdot": + case "vectordotproduct": + case "vxcl": + case "vectorexclude": + case "vang": + case "vectorangle": + case "clearscreen": + case "hudtext": + case "add": + case "remove": + case "processor": + case "edit": + case "printlist": + case "node": + case "v": + case "r": + case "q": + case "createorbit": + case "rotatefromto": + case "lookdirup": + case "angleaxis": + case "latlng": + case "vessel": + case "body": + case "bodyexists": + case "bodyatmosphere": + case "bounds": + case "heading": + case "slidenote": + case "note": + case "getvoice": + case "stopallvoices": + case "time": + case "timestamp": + case "timespan": + case "hsv": + case "hsva": + case "vecdraw": + case "vecdrawargs": + case "clearvecdraws": + case "clearguis": + case "gui": + case "positionat": + case "velocityat": + case "orbitat": + case "career": + case "allwaypoints": + case "waypoint": + case "lex": + case "lexicon": + case "list": + case "pidloop": + case "queue": + case "stack": + case "uniqueset": + case "abs": + case "mod": + case "floor": + case "ceiling": + case "round": + case "sqrt": + case "ln": + case "log10": + case "min": + case "max": + case "random": + case "randomseed": + case "char": + case "unchar": + case "print": + case "printat": + case "logfile": + case "debugdump": + case "debugfreezegame": + case "profileresult": + case "makebuiltindelegate": + case "droppriority": + case "range": + case "constant": + case "sin": + case "cos": + case "tan": + case "arcsin": + case "arccos": + case "arctan": + case "arctan2": + case "anglediff": + return false; + case "stage": + case "warpto": + case "transfer": + case "transferall": + case "run": + case "load": + case "toggleflybywire": + case "selectautopilotmode": + case "reboot": + case "shutdown": + case "scriptpath": + case "switch": + case "cd": + case "chdir": + case "copy_deprecated": + case "rename_file_deprecated": + case "rename_volume_deprecated": + case "delete_deprecated": + case "copypath": + case "movepath": + case "deletepath": + case "writejson": + case "readjson": + case "exists": + case "open": + case "create": + case "createdir": + case "path": + case "volume": + default: + return true; + } + } + else + return true; + } + internal override IEnumerable EmitOpcode() + { + if (EmitArgMarker) + { + if (IndirectMethod != null) + foreach (Opcode opcode in IndirectMethod.EmitPush()) + yield return opcode; + yield return new OpcodePush(new Execution.KOSArgMarkerType()); + } + foreach (IRValue argument in Arguments) + { + foreach (Opcode opcode in argument.EmitPush()) + yield return opcode; + } + yield return new OpcodeCall(Function); + } + + public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, IRValue argument) : this(target, opcode, emitArgMarker) + { + Arguments.Add(argument); + } + public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, IEnumerable arguments) : this(target, opcode, emitArgMarker) + { + Arguments.AddRange(arguments); + } + public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, params IRValue[] arguments) : this(target, opcode, emitArgMarker) + { + Arguments.AddRange(arguments); + } + public override string ToString() + => string.Format("{{call {0}({1})}}", Function.Trim('(', ')'), string.Join(",", Arguments.Select(a => a.ToString()))); + } + public class IRReturn : IRInstruction + { + public override bool SideEffects => false; + public IRValue Value { get; set; } + public short Depth { get; internal set; } + public IRReturn(short depth) + => Depth = depth; + internal override IEnumerable EmitOpcode() + { + if (Value != null) + foreach (Opcode opcode in Value.EmitPush()) + yield return opcode; + else + yield return new OpcodePush(null); + yield return new OpcodeReturn(Depth); + } + public override string ToString() + => string.Format("{{ret {0}}}", Depth); + } +} diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs new file mode 100644 index 000000000..92024ae50 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace kOS.Safe.Compilation.IR +{ + public class IROptimizer + { + public OptimizationLevel OptimizationLevel { get; } + + public IROptimizer(OptimizationLevel optimizationLevel) + { + OptimizationLevel = optimizationLevel; + } + + public List Optimize(List blocks) + { + return blocks; + } + } +} diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs new file mode 100644 index 000000000..999c9c65e --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; + +namespace kOS.Safe.Compilation.IR +{ + public abstract class IRValue + { + public enum ValueType + { + Unknown = 0, + Value = 1, + GameObject = 2 + } + public ValueType Type { get; } + internal abstract IEnumerable EmitPush(); + } + + public class IRConstant : IRValue + { + public object Value { get; } + public IRConstant(object value) + => Value = value; + internal override IEnumerable EmitPush() + { + yield return new OpcodePush(Value); + } + public override string ToString() + => Value.ToString(); + } + public class IRVariable : IRValue + { + public string Name { get; } + public bool IsLock { get; } + public IRVariable(string name, bool isLock = false) + { + Name = name; + IsLock = isLock; + } + internal override IEnumerable EmitPush() + { + yield return new OpcodePush(Name); + } + public override string ToString() + => Name; + } + public class IRRelocateLater : IRConstant + { + public IRRelocateLater(string value) : base(value) { } + + internal override IEnumerable EmitPush() + { + yield return new OpcodePushRelocateLater((string)Value); + } + } + public class IRDelegateRelocateLater : IRRelocateLater + { + public bool WithClosure { get; } + public IRDelegateRelocateLater(string value, bool withClosure) : base(value) + { + WithClosure = withClosure; + } + internal override IEnumerable EmitPush() + { + yield return new OpcodePushDelegateRelocateLater((string)Value, WithClosure); + } + } + public class IRTemp : IRVariable + { + public int ID { get; } + public IRInstruction Parent { get; internal set; } + private bool isPromoted = false; + public IRTemp(int id) : base($"$.temp.{id}", false) + { + ID = id; + } + public IRAssign PromoteToVariable() + { + isPromoted = true; + return new IRAssign(Name, this) { Scope = IRAssign.StoreScope.Local }; + } + internal override IEnumerable EmitPush() + { + if (isPromoted) + yield return new OpcodePush(Name); + else + foreach (Opcode opcode in Parent.EmitOpcode()) + yield return opcode; + } + } +} diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index 2de644b33..6daeae868 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -187,6 +187,17 @@ private void AssertNoFuncBuiltinViolation(string name) return; } + public static void Optimize(List code, CompilerOptions options) + { + IR.IRBuilder irBuilder = new IR.IRBuilder(); + irBuilder.Lower(code); + List blocks = irBuilder.Blocks; + IR.IROptimizer optimizer = new IR.IROptimizer(options.OptimizationLevel); + blocks = optimizer.Optimize(blocks); + code.Clear(); + code.AddRange(IR.IREmitter.Emit(blocks)); + } + public CodePart Compile(int startLineNum, ParseTree tree, Context context, CompilerOptions options) { this.options = options; @@ -202,6 +213,11 @@ public CodePart Compile(int startLineNum, ParseTree tree, Context context, Compi { PreProcess(tree); CompileProgram(tree); + if (options.OptimizationLevel != OptimizationLevel.None) + { + foreach (List code in new[] { part.InitializationCode, part.FunctionsCode, part.MainCode }) + Optimize(code, options); + } } return part; } diff --git a/src/kOS.Safe/Compilation/Opcode.cs b/src/kOS.Safe/Compilation/Opcode.cs index d7a65a8d1..58394eda7 100644 --- a/src/kOS.Safe/Compilation/Opcode.cs +++ b/src/kOS.Safe/Compilation/Opcode.cs @@ -2004,7 +2004,7 @@ public override string ToString() public class OpcodePush : Opcode { [MLField(1,false)] - private object Argument { get; set; } + public object Argument { get; private set; } protected override string Name { get { return "push"; } } public override ByteCode Code { get { return ByteCode.PUSH; } } diff --git a/src/kOS.Safe/Compilation/ProgramBuilder.cs b/src/kOS.Safe/Compilation/ProgramBuilder.cs index 14908bb4c..1f4633773 100644 --- a/src/kOS.Safe/Compilation/ProgramBuilder.cs +++ b/src/kOS.Safe/Compilation/ProgramBuilder.cs @@ -233,8 +233,8 @@ protected virtual void AddEndOfProgram(CodePart linkedObject, bool isMainProgram } } - private void ReplaceLabels(List program) - { + public static Dictionary MapLabels(List program) + { var labels = new Dictionary(); // get the index of every label @@ -259,7 +259,13 @@ private void ReplaceLabels(List program) labels.Add(program[index].Label, index); } } - + return labels; + } + + private void ReplaceLabels(List program) + { + Dictionary labels = MapLabels(program); + // replace destination labels with the corresponding index for (int index = 0; index < program.Count; index++) { From cd2448ff4b29a741c330b2ac483f67cac9b37612 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 15:19:23 -0500 Subject: [PATCH 03/56] Clean up IR building code. Ensure IRInstructions remember the source code position that their Opcode comes from. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 28 +++-- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 46 +++++--- src/kOS.Safe/Compilation/IR/IREmitter.cs | 24 ++-- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 116 ++++++++++++------- src/kOS.Safe/Compilation/IR/IRValue.cs | 2 +- 5 files changed, 131 insertions(+), 85 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index ff3b992d1..79a245023 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -8,7 +8,8 @@ public class BasicBlock public int StartIndex { get; } public int EndIndex { get; } public List Instructions { get; } = new List(); - public List Predecessors { get; } = new List(); + protected internal readonly HashSet predecessors = new HashSet(); + protected internal readonly HashSet sucessors = new HashSet(); public string Label => $"@BB#{ID}"; public int ID { get; } private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. @@ -27,6 +28,16 @@ public BasicBlock(int startIndex, int endIndex, int id) public void Add(IRInstruction instruction) => Instructions.Add(instruction); + public void AddSuccessor(BasicBlock successor) + { + sucessors.Add(successor); + successor.AddPredecessor(this); + } + protected void AddPredecessor(BasicBlock predecessor) + { + predecessors.Add(predecessor); + } + public void SetStackState(Stack stack) { while (stack.Count > 0) @@ -41,19 +52,16 @@ public override string ToString() public IEnumerable EmitOpCodes() { bool first = true; - if (Instructions.Any()) + foreach (IRInstruction instruction in Instructions.Take(Instructions.Count - 1)) { - foreach (IRInstruction instruction in Instructions.Take(Instructions.Count - 1)) + foreach (Opcode opcode in instruction.EmitOpcode()) { - foreach (Opcode opcode in instruction.EmitOpcode()) + if (first) { - if (first) - { - opcode.Label = Label; - first = false; - } - yield return opcode; + opcode.Label = Label; + first = false; } + yield return opcode; } } foreach (IRValue stackValue in exitStackState) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 8d0e5e895..aa97fdf9a 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -58,9 +58,15 @@ private void CreateBlocks(List code, Dictionary labels) if (lastOpcode is BranchOpcode branch) { int destinationIndex = branch.DestinationLabel != string.Empty ? labels[branch.DestinationLabel] : block.EndIndex + branch.Distance; - Blocks.First(b => b.StartIndex == destinationIndex).Predecessors.Add(block); + block.AddSuccessor(GetBlockFromStartIndex(destinationIndex)); if (!(branch is OpcodeBranchJump)) - Blocks.First(b => b.StartIndex == block.EndIndex + 1).Predecessors.Add(block); + block.AddSuccessor(GetBlockFromStartIndex(block.EndIndex + 1)); + } + else if (Blocks.Any(b => b.StartIndex == block.EndIndex + 1)) + { + BasicBlock successor = GetBlockFromStartIndex(block.EndIndex + 1); + block.Add(new IRJump(successor, lastOpcode.SourceLine, lastOpcode.SourceColumn)); + block.AddSuccessor(successor); } #if DEBUG block.OriginalOpcodes = code.ToArray(); @@ -68,16 +74,19 @@ private void CreateBlocks(List code, Dictionary labels) } } + private BasicBlock GetBlockFromStartIndex(int startIndex) + => Blocks.First(b => b.StartIndex == startIndex); + private void FillBlocks(List code, Dictionary labels) { Stack stack = new Stack(); - BasicBlock currentBlock = Blocks.First(b => b.StartIndex == 0); + BasicBlock currentBlock = GetBlockFromStartIndex(0); for (int i = 0; i < code.Count; i++) { if (i > currentBlock.EndIndex) { currentBlock.SetStackState(stack); - currentBlock = Blocks.First(b => b.StartIndex == i); + currentBlock = GetBlockFromStartIndex(i); } ParseInstruction(code[i], currentBlock, stack, labels, i); } @@ -144,20 +153,20 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack b.StartIndex == labels[branchIfTrue.DestinationLabel]), - Blocks.First(b => b.StartIndex == currentBlock.EndIndex + 1))); + GetBlockFromStartIndex(labels[branchIfTrue.DestinationLabel]), + GetBlockFromStartIndex(currentBlock.EndIndex + 1), + branchIfTrue)); break; case OpcodeBranchIfFalse branchIfFalse: currentBlock.Add(new IRBranch(stack.Pop(), - Blocks.First(b => b.StartIndex == currentBlock.EndIndex + 1), - Blocks.First(b => b.StartIndex == labels[branchIfFalse.DestinationLabel])) - { PreferFalse = true }); + GetBlockFromStartIndex(currentBlock.EndIndex + 1), + GetBlockFromStartIndex(labels[branchIfFalse.DestinationLabel]), + branchIfFalse)); break; case OpcodeBranchJump branchJump: int destinationIndex = branchJump.DestinationLabel != string.Empty ? labels[branchJump.DestinationLabel] : index + branchJump.Distance; @@ -185,7 +195,7 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack b.StartIndex == destinationIndex))); + currentBlock.Add(new IRJump(GetBlockFromStartIndex(destinationIndex), branchJump)); break; case OpcodeJumpStack _: throw new NotImplementedException("OpcodeJumpStack is not implemented for optimization because it is non-deterministic. Use OptimizationLevel.None."); @@ -234,16 +244,15 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack Emit(List blocks) Dictionary jumpLabels = new Dictionary(); // Emit Opcodes for each block foreach (BasicBlock block in blocks) - { - jumpLabels.Add(block.Label, result.Count); - result.AddRange(block.EmitOpCodes()); - } + LabelAndEmit(block, jumpLabels, result); // Remove single-line jumps from fallthrough blocks - for (int i = 0; i < result.Count; i++) + for (int i = 0; i < result.Count - 1; i++) { Opcode opcode = result[i]; - if (i < result.Count - 1 && opcode is OpcodeBranchJump jump && + if (opcode is OpcodeBranchJump jump && jump.DestinationLabel != null && jump.DestinationLabel == result[i + 1].Label) { - foreach (BasicBlock block in blocks) - if (jumpLabels[block.Label] >= i) - jumpLabels[block.Label] = jumpLabels[block.Label] - 1; + foreach (var key in jumpLabels.Keys.ToArray()) + if (jumpLabels[key] >= i) + jumpLabels[key] = jumpLabels[key] - 1; result.RemoveAt(i); i--; } @@ -41,7 +37,13 @@ public static List Emit(List blocks) return result; } + private static void LabelAndEmit(BasicBlock block, Dictionary jumpLabels, List result) + { + jumpLabels.Add(block.Label, result.Count); + result.AddRange(block.EmitOpCodes()); + } + private static string CreateLabel(int index) - => string.Format("@{0:0000}", index); + => string.Format("@{0:0000}", index); // TODO: + 1 } } diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index b8a4f2fd4..1242c38a7 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -5,15 +5,36 @@ namespace kOS.Safe.Compilation.IR { public abstract class IRInstruction { + public short SourceLine { get; } // line number in the source code that this was compiled from. + public short SourceColumn { get; } // column number of the token nearest the cause of this Opcode. + // Should-be-static public abstract bool SideEffects { get; } internal abstract IEnumerable EmitOpcode(); + protected IRInstruction(Opcode originalOpcode) + { + SourceLine = originalOpcode.SourceLine; + SourceColumn = originalOpcode.SourceColumn; + } + protected Opcode SetSourceLocation(Opcode opcode) + { + if (opcode == null) + return opcode; + opcode.SourceLine = SourceLine; + opcode.SourceColumn = SourceColumn; + return opcode; + } } public abstract class IRInteractsInstruction : IRInstruction { public override bool SideEffects { get; } - public IRInteractsInstruction(IRValue interactor) + protected IRInteractsInstruction(IRValue interactor, Opcode originalOpcode) : base(originalOpcode) { + if (interactor is IRConstant) + { + SideEffects = false; + return; + } switch (interactor.Type) { case IRValue.ValueType.Value: @@ -38,32 +59,31 @@ public enum StoreScope public IRValue Value { get; } public StoreScope Scope { get; set; } = StoreScope.Ambivalent; public bool AssertExists { get; set; } = false; - public IRAssign(string target, IRValue value) + public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : base(opcode) { - Target = target; + Target = opcode.Identifier; Value = value; } - public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : this(opcode.Identifier, value) { } internal override IEnumerable EmitOpcode() { foreach (Opcode opcode in Value.EmitPush()) yield return opcode; if (AssertExists) { - yield return new OpcodeStoreExist(Target); + yield return SetSourceLocation(new OpcodeStoreExist(Target)); yield break; } switch (Scope) { case StoreScope.Local: - yield return new OpcodeStoreLocal(Target); + yield return SetSourceLocation(new OpcodeStoreLocal(Target)); yield break; case StoreScope.Global: - yield return new OpcodeStoreGlobal(Target); + yield return SetSourceLocation(new OpcodeStoreGlobal(Target)); yield break; default: case StoreScope.Ambivalent: - yield return new OpcodeStore(Target); + yield return SetSourceLocation(new OpcodeStore(Target)); yield break; } } @@ -78,7 +98,7 @@ public class IRBinaryOp : IRInstruction public IRValue Left { get; protected set; } public IRValue Right { get; protected set; } public bool Commutative { get; } - public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) + public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) : base(operation) { Result = result; Operation = operation; @@ -121,7 +141,7 @@ internal override IEnumerable EmitOpcode() foreach (Opcode opcode in Right.EmitPush()) yield return opcode; Operation.Label = string.Empty; - yield return Operation; + yield return SetSourceLocation(Operation); } public override string ToString() => Operation.ToString(); @@ -132,7 +152,7 @@ public class IRUnaryOp : IRInstruction public IRTemp Result { get; } public Opcode Operation { get; } public IRValue Operand { get; } - public IRUnaryOp(IRTemp result, Opcode operation, IRValue operand) + public IRUnaryOp(IRTemp result, Opcode operation, IRValue operand) : base(operation) { Result = result; Operation = operation; @@ -152,7 +172,7 @@ public class IRNoStackInstruction : IRInstruction { public override bool SideEffects => false; public Opcode Operation { get; } - public IRNoStackInstruction(Opcode opcode) + public IRNoStackInstruction(Opcode opcode) : base(opcode) => Operation = opcode; internal override IEnumerable EmitOpcode() { @@ -167,7 +187,7 @@ public class IRUnaryConsumer : IRInstruction public override bool SideEffects { get; } public Opcode Operation { get; } public IRValue Operand { get; } - public IRUnaryConsumer(Opcode opcode, IRValue operand, bool sideEffects = false) + public IRUnaryConsumer(Opcode opcode, IRValue operand, bool sideEffects = false) : base(opcode) { Operation = opcode; Operand = operand; @@ -186,9 +206,17 @@ public override string ToString() public class IRPop : IRInstruction { public override bool SideEffects => false; + public IRValue Value { get; } + public IRPop(IRValue value, OpcodePop opcode) : base(opcode) + => Value = value; + internal override IEnumerable EmitOpcode() { - yield return new OpcodePop(); + if (Value is IRConstant || (Value is IRVariable && !(Value is IRTemp))) + yield break; + foreach (Opcode opcode in Value.EmitPush()) + yield return opcode; + yield return SetSourceLocation(new OpcodePop()); } public override string ToString() => "{pop}"; @@ -197,7 +225,7 @@ public class IRNonVarPush : IRInstruction { public override bool SideEffects => false; public Opcode Operation { get; } - public IRNonVarPush(Opcode opcode) + public IRNonVarPush(Opcode opcode) : base(opcode) => Operation = opcode; internal override IEnumerable EmitOpcode() { @@ -212,18 +240,17 @@ public class IRSuffixGet : IRInteractsInstruction public IRTemp Result { get; } public IRValue Object { get; } public string Suffix { get; } - public IRSuffixGet(IRTemp result, IRValue obj, string suffix) : base(obj) + public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcodeGetMember) : base(obj, opcodeGetMember) { Result = result; Object = obj; - Suffix = suffix; + Suffix = opcodeGetMember.Identifier; } - public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcode) : this(result, obj, opcode.Identifier) { } internal override IEnumerable EmitOpcode() { foreach (Opcode opcode in Object.EmitPush()) yield return opcode; - yield return new OpcodeGetMember(Suffix); + yield return SetSourceLocation(new OpcodeGetMember(Suffix)); } public override string ToString() => string.Format("{{gmb \"{0}\"}}", Suffix); @@ -231,13 +258,12 @@ public override string ToString() public class IRSuffixGetMethod : IRSuffixGet { public override bool SideEffects => false; - public IRSuffixGetMethod(IRTemp result, IRValue obj, string suffix) : base(result, obj, suffix) { } - public IRSuffixGetMethod(IRTemp result, IRValue obj, OpcodeGetMethod opcode) : this(result, obj, opcode.Identifier) { } + public IRSuffixGetMethod(IRTemp result, IRValue obj, OpcodeGetMethod opcode) : base(result, obj, opcode) { } internal override IEnumerable EmitOpcode() { foreach (Opcode opcode in Object.EmitPush()) yield return opcode; - yield return new OpcodeGetMethod(Suffix); + yield return SetSourceLocation(new OpcodeGetMethod(Suffix)); } public override string ToString() => string.Format("{{gmet \"{0}\"}}", Suffix); @@ -248,20 +274,19 @@ public class IRSuffixSet : IRInteractsInstruction public IRValue Object { get; } public IRValue Value { get; } public string Suffix { get; } - public IRSuffixSet(IRValue obj, IRValue value, string suffix) : base(obj) + public IRSuffixSet(IRValue obj, IRValue value, OpcodeSetMember opcodeSetMember) : base(obj, opcodeSetMember) { Object = obj; Value = value; - Suffix = suffix; + Suffix = opcodeSetMember.Identifier; } - public IRSuffixSet(IRValue obj, IRValue value, OpcodeSetMember opcode) : this(obj, value, opcode.Identifier) { } internal override IEnumerable EmitOpcode() { foreach (Opcode opcode in Object.EmitPush()) yield return opcode; foreach (Opcode opcode in Value.EmitPush()) yield return opcode; - yield return new OpcodeSetMember(Suffix); + yield return SetSourceLocation(new OpcodeSetMember(Suffix)); } public override string ToString() => string.Format("{{smb \"{0}\"}}", Suffix); @@ -272,7 +297,7 @@ public class IRIndexGet : IRInstruction public IRValue Result { get; } public IRValue Object { get; } public IRValue Index { get; } - public IRIndexGet(IRValue result, IRValue obj, IRValue index) + public IRIndexGet(IRTemp result, IRValue obj, IRValue index, OpcodeGetIndex opcode) : base(opcode) { Result = result; Object = obj; @@ -284,7 +309,7 @@ internal override IEnumerable EmitOpcode() yield return opcode; foreach (Opcode opcode in Index.EmitPush()) yield return opcode; - yield return new OpcodeGetIndex(); + yield return SetSourceLocation(new OpcodeGetIndex()); } public override string ToString() => "{gidx}"; @@ -295,7 +320,7 @@ public class IRIndexSet : IRInstruction public IRValue Object { get; } public IRValue Index { get; } public IRValue Value { get; } - public IRIndexSet(IRValue obj, IRValue index, IRValue value) + public IRIndexSet(IRValue obj, IRValue index, IRValue value, OpcodeSetIndex opcode) : base(opcode) { Object = obj; Index = index; @@ -309,7 +334,7 @@ internal override IEnumerable EmitOpcode() yield return opcode; foreach (Opcode opcode in Value.EmitPush()) yield return opcode; - yield return new OpcodeSetIndex(); + yield return SetSourceLocation(new OpcodeSetIndex()); } public override string ToString() => "{sidx}"; @@ -318,13 +343,15 @@ public class IRJump : IRInstruction { public override bool SideEffects => false; public BasicBlock Target { get; } - public IRJump(BasicBlock target) + public IRJump(BasicBlock target, OpcodeBranchJump opcode) : base(opcode) { Target = target; } + public IRJump(BasicBlock target, short sourceLine, short sourceColumn) + : this(target, new OpcodeBranchJump() { SourceLine = sourceLine, SourceColumn = sourceColumn }) { } internal override IEnumerable EmitOpcode() { - yield return new OpcodeBranchJump() { DestinationLabel = Target.Label }; + yield return SetSourceLocation(new OpcodeBranchJump() { DestinationLabel = Target.Label }); } public override string ToString() => string.Format("{{jump {0}}}", Target.Label); @@ -334,7 +361,7 @@ public class IRJumpStack : IRInstruction public override bool SideEffects => false; public IRValue Distance { get; } public List Targets { get; } = new List(); - public IRJumpStack(IRValue distance, IEnumerable targets) + public IRJumpStack(IRValue distance, IEnumerable targets, OpcodeJumpStack jumpStack) : base(jumpStack) { Distance = distance; Targets.AddRange(targets); @@ -343,7 +370,7 @@ internal override IEnumerable EmitOpcode() { foreach (Opcode opcode in Distance.EmitPush()) yield return opcode; - yield return new OpcodeJumpStack(); + yield return SetSourceLocation(new OpcodeJumpStack()); } } public class IRBranch : IRInstruction @@ -353,11 +380,12 @@ public class IRBranch : IRInstruction public BasicBlock True { get; } public BasicBlock False { get; } public bool PreferFalse { get; set; } = false; - public IRBranch(IRValue condition, BasicBlock onTrue, BasicBlock onFalse) + public IRBranch(IRValue condition, BasicBlock onTrue, BasicBlock onFalse, BranchOpcode opcodeBranch) : base(opcodeBranch) { Condition = condition; True = onTrue; False = onFalse; + PreferFalse = opcodeBranch is OpcodeBranchIfFalse; } internal override IEnumerable EmitOpcode() { @@ -365,13 +393,13 @@ internal override IEnumerable EmitOpcode() yield return opcode; if (PreferFalse) { - yield return new OpcodeBranchIfFalse() { DestinationLabel = False.Label }; - yield return new OpcodeBranchJump() { DestinationLabel = True.Label }; + yield return SetSourceLocation(new OpcodeBranchIfFalse() { DestinationLabel = False.Label }); + yield return SetSourceLocation(new OpcodeBranchJump() { DestinationLabel = True.Label }); } else { - yield return new OpcodeBranchIfTrue() { DestinationLabel = True.Label }; - yield return new OpcodeBranchJump() { DestinationLabel = False.Label }; + yield return SetSourceLocation(new OpcodeBranchIfTrue() { DestinationLabel = True.Label }); + yield return SetSourceLocation(new OpcodeBranchJump() { DestinationLabel = False.Label }); } } public override string ToString() @@ -387,7 +415,7 @@ public class IRCall : IRInstruction public IRValue IndirectMethod { get; internal set; } public bool Direct { get; } public bool EmitArgMarker; - private IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker) + private IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker) : base(opcode) { Target = target; Function = (string)opcode.Destination; @@ -548,7 +576,7 @@ internal override IEnumerable EmitOpcode() foreach (Opcode opcode in argument.EmitPush()) yield return opcode; } - yield return new OpcodeCall(Function); + yield return SetSourceLocation(new OpcodeCall(Function)); } public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, IRValue argument) : this(target, opcode, emitArgMarker) @@ -571,7 +599,7 @@ public class IRReturn : IRInstruction public override bool SideEffects => false; public IRValue Value { get; set; } public short Depth { get; internal set; } - public IRReturn(short depth) + public IRReturn(short depth, OpcodeReturn opcode) : base(opcode) => Depth = depth; internal override IEnumerable EmitOpcode() { @@ -579,8 +607,8 @@ internal override IEnumerable EmitOpcode() foreach (Opcode opcode in Value.EmitPush()) yield return opcode; else - yield return new OpcodePush(null); - yield return new OpcodeReturn(Depth); + yield return SetSourceLocation(new OpcodePush(null)); + yield return SetSourceLocation(new OpcodeReturn(Depth)); } public override string ToString() => string.Format("{{ret {0}}}", Depth); diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index 999c9c65e..d69251d19 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -75,7 +75,7 @@ public IRTemp(int id) : base($"$.temp.{id}", false) public IRAssign PromoteToVariable() { isPromoted = true; - return new IRAssign(Name, this) { Scope = IRAssign.StoreScope.Local }; + return new IRAssign(new OpcodeStoreLocal(Name), this) { Scope = IRAssign.StoreScope.Local }; } internal override IEnumerable EmitPush() { From 3f609ff46d5766ca0ffa51a774a6cd24b535bb11 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 16:05:48 -0500 Subject: [PATCH 04/56] Align IRInstruction property names and accessibility. Add interfaces that serve as metadata and access points. --- .../Compilation/IR/IOperandInstructionBase.cs | 15 ++ src/kOS.Safe/Compilation/IR/IRInstruction.cs | 136 ++++++++++-------- .../Compilation/IR/IResultingInstruction.cs | 7 + 3 files changed, 97 insertions(+), 61 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs create mode 100644 src/kOS.Safe/Compilation/IR/IResultingInstruction.cs diff --git a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs new file mode 100644 index 000000000..bb87b07c2 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace kOS.Safe.Compilation.IR +{ + public interface IOperandInstructionBase + { } + public interface ISingleOperandInstruction : IOperandInstructionBase + { + IRValue Operand { get; } + } + public interface IMultipleOperandInstruction : IOperandInstructionBase + { + IEnumerable Operands { get; } + } +} diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 1242c38a7..8c9b35973 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -46,7 +46,7 @@ protected IRInteractsInstruction(IRValue interactor, Opcode originalOpcode) : ba } } } - public class IRAssign : IRInstruction + public class IRAssign : IRInstruction, ISingleOperandInstruction { public enum StoreScope { @@ -55,10 +55,12 @@ public enum StoreScope Global } public override bool SideEffects => false; - public string Target { get; } - public IRValue Value { get; } + public string Target { get; set; } + public IRValue Value { get; set; } public StoreScope Scope { get; set; } = StoreScope.Ambivalent; public bool AssertExists { get; set; } = false; + IRValue ISingleOperandInstruction.Operand => Value; + public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : base(opcode) { Target = opcode.Identifier; @@ -90,21 +92,23 @@ internal override IEnumerable EmitOpcode() public override string ToString() => string.Format("{{store {0}}}", Value.ToString()); } - public class IRBinaryOp : IRInstruction + public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { public override bool SideEffects => false; - public IRTemp Result { get; } + public IRValue Result { get; set; } public BinaryOpcode Operation { get; protected set; } - public IRValue Left { get; protected set; } - public IRValue Right { get; protected set; } - public bool Commutative { get; } + public IRValue Left { get; set; } + public IRValue Right { get; set; } + public IEnumerable Operands { get { yield return Left; yield return Right; } } + public bool IsCommutative { get; } + public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) : base(operation) { Result = result; Operation = operation; Left = left; Right = right; - Commutative = (operation is OpcodeCompareEqual) || + IsCommutative = (operation is OpcodeCompareEqual) || (operation is OpcodeCompareNE) || (operation is OpcodeCompareGT) || (operation is OpcodeCompareLT) || @@ -113,10 +117,11 @@ public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue r (operation is OpcodeMathAdd) || (operation is OpcodeMathMultiply); } - public void ReverseOperands() + public void SwapOperands() { - if (!Commutative) - throw new System.InvalidOperationException($"{this} is not commutative."); + if (!IsCommutative) + return; + //throw new System.InvalidOperationException($"{this} is not commutative."); (Right, Left) = (Left, Right); switch (Operation) { @@ -146,12 +151,12 @@ internal override IEnumerable EmitOpcode() public override string ToString() => Operation.ToString(); } - public class IRUnaryOp : IRInstruction + public class IRUnaryOp : IRInstruction, IResultingInstruction, ISingleOperandInstruction { public override bool SideEffects => false; - public IRTemp Result { get; } + public IRValue Result { get; set; } public Opcode Operation { get; } - public IRValue Operand { get; } + public IRValue Operand { get; set; } public IRUnaryOp(IRTemp result, Opcode operation, IRValue operand) : base(operation) { Result = result; @@ -182,7 +187,7 @@ internal override IEnumerable EmitOpcode() public override string ToString() => Operation.ToString(); } - public class IRUnaryConsumer : IRInstruction + public class IRUnaryConsumer : IRInstruction, ISingleOperandInstruction { public override bool SideEffects { get; } public Opcode Operation { get; } @@ -203,10 +208,11 @@ internal override IEnumerable EmitOpcode() public override string ToString() => Operation.ToString(); } - public class IRPop : IRInstruction + public class IRPop : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; - public IRValue Value { get; } + public IRValue Value { get; set; } + IRValue ISingleOperandInstruction.Operand => Value; public IRPop(IRValue value, OpcodePop opcode) : base(opcode) => Value = value; @@ -235,11 +241,12 @@ internal override IEnumerable EmitOpcode() public override string ToString() => Operation.ToString(); } - public class IRSuffixGet : IRInteractsInstruction + public class IRSuffixGet : IRInteractsInstruction, IResultingInstruction, ISingleOperandInstruction { - public IRTemp Result { get; } - public IRValue Object { get; } - public string Suffix { get; } + public IRValue Result { get; } + public IRValue Object { get; set; } + public string Suffix { get; set; } + IRValue ISingleOperandInstruction.Operand => Object; public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcodeGetMember) : base(obj, opcodeGetMember) { Result = result; @@ -268,11 +275,12 @@ internal override IEnumerable EmitOpcode() public override string ToString() => string.Format("{{gmet \"{0}\"}}", Suffix); } - public class IRSuffixSet : IRInteractsInstruction + public class IRSuffixSet : IRInteractsInstruction, IMultipleOperandInstruction { public override bool SideEffects { get; } - public IRValue Object { get; } - public IRValue Value { get; } + public IRValue Object { get; set; } + public IRValue Value { get; set; } + public IEnumerable Operands { get { yield return Object; yield return Value; } } public string Suffix { get; } public IRSuffixSet(IRValue obj, IRValue value, OpcodeSetMember opcodeSetMember) : base(obj, opcodeSetMember) { @@ -291,12 +299,13 @@ internal override IEnumerable EmitOpcode() public override string ToString() => string.Format("{{smb \"{0}\"}}", Suffix); } - public class IRIndexGet : IRInstruction + public class IRIndexGet : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { public override bool SideEffects => false; public IRValue Result { get; } - public IRValue Object { get; } - public IRValue Index { get; } + public IRValue Object { get; set; } + public IRValue Index { get; set; } + public IEnumerable Operands { get { yield return Object; yield return Index; } } public IRIndexGet(IRTemp result, IRValue obj, IRValue index, OpcodeGetIndex opcode) : base(opcode) { Result = result; @@ -314,12 +323,13 @@ internal override IEnumerable EmitOpcode() public override string ToString() => "{gidx}"; } - public class IRIndexSet : IRInstruction + public class IRIndexSet : IRInstruction, IMultipleOperandInstruction { public override bool SideEffects => false; - public IRValue Object { get; } - public IRValue Index { get; } - public IRValue Value { get; } + public IRValue Object { get; set; } + public IRValue Index { get; set; } + public IRValue Value { get; set; } + public IEnumerable Operands { get { yield return Object; yield return Index; yield return Value; } } public IRIndexSet(IRValue obj, IRValue index, IRValue value, OpcodeSetIndex opcode) : base(opcode) { Object = obj; @@ -342,7 +352,7 @@ public override string ToString() public class IRJump : IRInstruction { public override bool SideEffects => false; - public BasicBlock Target { get; } + public BasicBlock Target { get; set; } public IRJump(BasicBlock target, OpcodeBranchJump opcode) : base(opcode) { Target = target; @@ -356,10 +366,11 @@ internal override IEnumerable EmitOpcode() public override string ToString() => string.Format("{{jump {0}}}", Target.Label); } - public class IRJumpStack : IRInstruction + public class IRJumpStack : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; - public IRValue Distance { get; } + public IRValue Distance { get; set; } + IRValue ISingleOperandInstruction.Operand => Distance; public List Targets { get; } = new List(); public IRJumpStack(IRValue distance, IEnumerable targets, OpcodeJumpStack jumpStack) : base(jumpStack) { @@ -373,12 +384,13 @@ internal override IEnumerable EmitOpcode() yield return SetSourceLocation(new OpcodeJumpStack()); } } - public class IRBranch : IRInstruction + public class IRBranch : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; - public IRValue Condition { get; } - public BasicBlock True { get; } - public BasicBlock False { get; } + public IRValue Condition { get; set; } + IRValue ISingleOperandInstruction.Operand => Condition; + public BasicBlock True { get; set; } + public BasicBlock False { get; set; } public bool PreferFalse { get; set; } = false; public IRBranch(IRValue condition, BasicBlock onTrue, BasicBlock onFalse, BranchOpcode opcodeBranch) : base(opcodeBranch) { @@ -405,19 +417,20 @@ internal override IEnumerable EmitOpcode() public override string ToString() => string.Format("{{br.? {0}/{1}}}", True.Label, False.Label); } - public class IRCall : IRInstruction + public class IRCall : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { protected static readonly Function.FunctionManager functionManager = new Function.FunctionManager(null); public override bool SideEffects { get; } - public IRTemp Target { get; } + public IRValue Result { get; } public string Function { get; } public List Arguments { get; } = new List(); + public IEnumerable Operands => Enumerable.Reverse(Arguments); public IRValue IndirectMethod { get; internal set; } public bool Direct { get; } - public bool EmitArgMarker; + public bool EmitArgMarker { get; set; } private IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker) : base(opcode) { - Target = target; + Result = target; Function = (string)opcode.Destination; Direct = opcode.Direct; SideEffects = CheckIfFunctionHasSideEffects(opcode); @@ -432,10 +445,6 @@ private bool CheckIfFunctionHasSideEffects(OpcodeCall opcode) functionName = functionName.ToLower().Replace("()", ""); switch (functionName) { - case "addAlarm": - case "listAlarms": - case "deleteAlarm": - case "buildlist": case "vcrs": case "vectorcrossproduct": case "vdot": @@ -480,12 +489,7 @@ private bool CheckIfFunctionHasSideEffects(OpcodeCall opcode) case "clearvecdraws": case "clearguis": case "gui": - case "positionat": - case "velocityat": - case "orbitat": case "career": - case "allwaypoints": - case "waypoint": case "lex": case "lexicon": case "list": @@ -507,14 +511,6 @@ private bool CheckIfFunctionHasSideEffects(OpcodeCall opcode) case "randomseed": case "char": case "unchar": - case "print": - case "printat": - case "logfile": - case "debugdump": - case "debugfreezegame": - case "profileresult": - case "makebuiltindelegate": - case "droppriority": case "range": case "constant": case "sin": @@ -526,6 +522,23 @@ private bool CheckIfFunctionHasSideEffects(OpcodeCall opcode) case "arctan2": case "anglediff": return false; + case "addAlarm": + case "listAlarms": + case "deleteAlarm": + case "buildlist": + case "positionat": + case "velocityat": + case "orbitat": + case "allwaypoints": + case "waypoint": + case "print": + case "printat": + case "logfile": + case "debugdump": + case "debugfreezegame": + case "profileresult": + case "makebuiltindelegate": + case "droppriority": case "stage": case "warpto": case "transfer": @@ -594,10 +607,11 @@ public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, params IRVal public override string ToString() => string.Format("{{call {0}({1})}}", Function.Trim('(', ')'), string.Join(",", Arguments.Select(a => a.ToString()))); } - public class IRReturn : IRInstruction + public class IRReturn : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; public IRValue Value { get; set; } + IRValue ISingleOperandInstruction.Operand => Value; public short Depth { get; internal set; } public IRReturn(short depth, OpcodeReturn opcode) : base(opcode) => Depth = depth; diff --git a/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs new file mode 100644 index 000000000..a9c7bfcdb --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IResultingInstruction.cs @@ -0,0 +1,7 @@ +namespace kOS.Safe.Compilation.IR +{ + public interface IResultingInstruction + { + IRValue Result { get; } + } +} \ No newline at end of file From 1344914117f4f2dcd87dfa2c0ff63717b510d4e7 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 21:03:45 -0500 Subject: [PATCH 05/56] Add concept of Extended Basic Blocks to help with future analysis and optimization methods. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 13 ++++- src/kOS.Safe/Compilation/IR/IROptimizer.cs | 8 ++- .../IR/Optimization/ExtendedBasicBlock.cs | 57 +++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 79a245023..b5ee45722 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -8,10 +8,13 @@ public class BasicBlock public int StartIndex { get; } public int EndIndex { get; } public List Instructions { get; } = new List(); - protected internal readonly HashSet predecessors = new HashSet(); - protected internal readonly HashSet sucessors = new HashSet(); + public IEnumerable Sucessors => sucessors; + public IEnumerable Predecessors => predecessors; + private readonly HashSet predecessors = new HashSet(); + private readonly HashSet sucessors = new HashSet(); public string Label => $"@BB#{ID}"; public int ID { get; } + public Optimization.ExtendedBasicBlock ExtendedBlock { get; set; } private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. #if DEBUG internal Opcode[] OriginalOpcodes { get; set; } @@ -37,6 +40,12 @@ protected void AddPredecessor(BasicBlock predecessor) { predecessors.Add(predecessor); } + public void RemoveSuccessor(BasicBlock sucessor) + { + if (!sucessors.Remove(sucessor)) + throw new System.ArgumentException(nameof(sucessor)); + sucessor.predecessors.Remove(this); + } public void SetStackState(Stack stack) { diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index 92024ae50..1d74b97c0 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using kOS.Safe.Compilation.IR.Optimization; namespace kOS.Safe.Compilation.IR { @@ -17,6 +16,11 @@ public IROptimizer(OptimizationLevel optimizationLevel) public List Optimize(List blocks) { + if (blocks.Count == 0) + return blocks; + + ExtendedBasicBlock extendedRootBlock = ExtendedBasicBlock.CreateExtendedBlockTree(blocks[0]); + HashSet extendedBlocks = new HashSet(ExtendedBasicBlock.DumpTree(extendedRootBlock)); return blocks; } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs b/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs new file mode 100644 index 000000000..59a9b18e6 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR.Optimization +{ + public class ExtendedBasicBlock + { + public List Blocks { get; } = new List(); + public IEnumerable Sucessors => sucessors; + public IEnumerable Predecessors => predecessors; + private readonly HashSet predecessors = new HashSet(); + private readonly HashSet sucessors = new HashSet(); + private ExtendedBasicBlock(BasicBlock startingBlock) + { + AddBlock(startingBlock); + Blocks.Sort(Comparer.Create((x, y) => x.ID.CompareTo(y.ID))); + } + private void AddBlock(BasicBlock block) + { + Blocks.Add(block); + block.ExtendedBlock = this; + foreach (BasicBlock successor in block.Sucessors) + { + if (!successor.Predecessors.Skip(1).Any()) + AddBlock(successor); + else + { + ExtendedBasicBlock extendedSuccessor = successor.ExtendedBlock ?? new ExtendedBasicBlock(successor); + AddSuccessor(extendedSuccessor); + } + } + } + + public static ExtendedBasicBlock CreateExtendedBlockTree(BasicBlock root) + { + ExtendedBasicBlock resultBlock = new ExtendedBasicBlock(root); + return resultBlock; + } + public static IEnumerable DumpTree(ExtendedBasicBlock root) + => Enumerable.Repeat(root, 1).Concat(root.sucessors.SelectMany(DumpTree)); + + public void AddSuccessor(ExtendedBasicBlock successor) + { + sucessors.Add(successor); + successor.AddPredecessor(this); + } + protected void AddPredecessor(ExtendedBasicBlock predecessor) + { + predecessors.Add(predecessor); + } + + public override string ToString() + { + return $"ExtendedBasicBlock: {string.Join(", ", Blocks.Select(bb => $"#{bb.ID}"))}"; + } + } +} From 28410f0f86576f325b7b9bca9e4e7c59cb583bc5 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 21:05:42 -0500 Subject: [PATCH 06/56] Add abstract IRVariableBase class so that IRTemp can share some commonalities with IRVariable but not be a subclass. --- src/kOS.Safe/Compilation/IR/IRValue.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index d69251d19..cfbd015e1 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -26,13 +26,17 @@ internal override IEnumerable EmitPush() public override string ToString() => Value.ToString(); } - public class IRVariable : IRValue + public abstract class IRVariableBase : IRValue { public string Name { get; } + public IRVariableBase(string name) + => Name = name; + } + public class IRVariable : IRVariableBase + { public bool IsLock { get; } - public IRVariable(string name, bool isLock = false) + public IRVariable(string name, bool isLock = false) : base(name) { - Name = name; IsLock = isLock; } internal override IEnumerable EmitPush() @@ -63,12 +67,13 @@ internal override IEnumerable EmitPush() yield return new OpcodePushDelegateRelocateLater((string)Value, WithClosure); } } - public class IRTemp : IRVariable + public class IRTemp : IRVariableBase { public int ID { get; } public IRInstruction Parent { get; internal set; } + private bool isPromoted = false; - public IRTemp(int id) : base($"$.temp.{id}", false) + public IRTemp(int id) : base($"$.temp.{id}") { ID = id; } From 171b2bc496b674012024d7c68b01cd28e991caf1 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 21:44:42 -0500 Subject: [PATCH 07/56] Add methods to, outside of executing an opcode, invoke the same math or logical operation. This will be used for constant folding during compile. Hopefully the C# compiler will inline these back into the original Opcode methods, but the impact to execution should be minimal either way. --- src/kOS.Safe/Compilation/Opcode.cs | 40 +++++++++++++++++++----------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/kOS.Safe/Compilation/Opcode.cs b/src/kOS.Safe/Compilation/Opcode.cs index 58394eda7..6637211e6 100644 --- a/src/kOS.Safe/Compilation/Opcode.cs +++ b/src/kOS.Safe/Compilation/Opcode.cs @@ -479,12 +479,17 @@ public override void Execute(ICpu cpu) object right = cpu.PopValueArgument(); object left = cpu.PopValueArgument(); + object result = ExecuteCalculation(left, right); + cpu.PushArgumentStack(result); + } + + public object ExecuteCalculation(object left, object right) + { var operands = new OperandPair(left, right); Calculator calc = Calculator.GetCalculator(operands); Operands = operands; - object result = ExecuteCalculation(calc); - cpu.PushArgumentStack(result); + return ExecuteCalculation(calc); } protected virtual object ExecuteCalculation(Calculator calc) @@ -1313,27 +1318,29 @@ public override void Execute(ICpu cpu) { Structure value = cpu.PopStructureEncapsulatedArgument(); + cpu.PushArgumentStack(StaticOperation(value)); + } + + public static object StaticOperation(object value) + { var scalarValue = value as ScalarValue; if (scalarValue != null && scalarValue.IsValid) { - cpu.PushArgumentStack(-scalarValue); - return; + return -scalarValue; } - + // Generic last-ditch to catch any sort of object that has // overloaded the unary negate operator '-'. // (For example, kOS.Suffixed.Vector and kOS.Suffixed.Direction) Type t = value.GetType(); - MethodInfo negateMe = t.GetMethod("op_UnaryNegation", BindingFlags.FlattenHierarchy |BindingFlags.Static | BindingFlags.Public); + MethodInfo negateMe = t.GetMethod("op_UnaryNegation", BindingFlags.FlattenHierarchy | BindingFlags.Static | BindingFlags.Public); if (negateMe != null) { - object result = negateMe.Invoke(null, new[]{value}); - cpu.PushArgumentStack(result); + return negateMe.Invoke(null, new[] { value }); } else throw new KOSUnaryOperandTypeException("negate", value); - } } @@ -1462,8 +1469,12 @@ public override void Execute(ICpu cpu) // is to also change integers and floats into booleans. Thus the call to // Convert.ToBoolean(): object value = cpu.PopValueArgument(); + cpu.PushArgumentStack(StaticOperation(value)); + } + public static object StaticOperation(object value) + { bool result = Convert.ToBoolean(value); - cpu.PushArgumentStack(Structure.FromPrimitive(result)); + return Structure.FromPrimitive(result); } } @@ -1485,21 +1496,22 @@ public class OpcodeLogicNot : Opcode public override void Execute(ICpu cpu) { object value = cpu.PopValueArgument(); - object result; - + cpu.PushArgumentStack(StaticOperation(value)); + } + public static object StaticOperation(object value) + { // Convert to bool instead of cast in case the identifier is stored // as an encapsulated BooleanValue, preventing an unboxing collision. // Wrapped in a try/catch since the Convert framework doesn't have a // way to determine if a type can be converted. try { - result = !Convert.ToBoolean(value); + return Structure.FromPrimitive(!Convert.ToBoolean(value)); } catch { throw new KOSCastException(value.GetType(), typeof(BooleanValue)); } - cpu.PushArgumentStack(Structure.FromPrimitive(result)); } } From a53f7b3e561d25a893bc34387c317a42046d46b5 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 22:16:10 -0500 Subject: [PATCH 08/56] Add convenience constructor for compilation exceptions arising from IR instructions. --- src/kOS.Safe/Exceptions/KOSCompileException.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/kOS.Safe/Exceptions/KOSCompileException.cs b/src/kOS.Safe/Exceptions/KOSCompileException.cs index 37320cae0..e59e47eda 100644 --- a/src/kOS.Safe/Exceptions/KOSCompileException.cs +++ b/src/kOS.Safe/Exceptions/KOSCompileException.cs @@ -37,6 +37,11 @@ public KOSCompileException(Token token, string message) { } + public KOSCompileException(Compilation.IR.IRInstruction irInstruction, KOSException innerException) + : this(new LineCol(irInstruction.SourceLine, irInstruction.SourceColumn), innerException.Message) + { + } + public KOSCompileException(LineCol location, string message) { Location = location; From 131a4a9e42f183e464b2b41f2ec369c09875e221 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 6 Mar 2026 22:53:27 -0500 Subject: [PATCH 09/56] Add constant folding pass. This reduces any constant expressions to their most reduced form. Invalid operations found during folding throw a compilation exception, which should report the problematic line and column in the source. Also add tests to confirm correct functionality (and improve current test suite for equivalent non-optimizing tests). --- kerboscript_tests/integration/operators.ks | 3 + .../integration/operators_invalid.ks | 21 ++ .../Execution/OptimizationTest.cs | 15 +- src/kOS.Safe.Test/Execution/SimpleTest.cs | 5 +- src/kOS.Safe/Compilation/IR/IROptimizer.cs | 2 + .../IR/Optimization/ConstantFolding.cs | 275 ++++++++++++++++++ src/kOS.Safe/Compilation/OptimizationLevel.cs | 10 +- 7 files changed, 322 insertions(+), 9 deletions(-) create mode 100644 kerboscript_tests/integration/operators_invalid.ks create mode 100644 src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs diff --git a/kerboscript_tests/integration/operators.ks b/kerboscript_tests/integration/operators.ks index c4aa36b73..f82ebac9e 100644 --- a/kerboscript_tests/integration/operators.ks +++ b/kerboscript_tests/integration/operators.ks @@ -18,3 +18,6 @@ print(1 = 1). print(1 <> 2). print(true and true). print(true or false). +print("A" + "b"). +print(a * 0). +print(a ^ 0). diff --git a/kerboscript_tests/integration/operators_invalid.ks b/kerboscript_tests/integration/operators_invalid.ks new file mode 100644 index 000000000..75473a709 --- /dev/null +++ b/kerboscript_tests/integration/operators_invalid.ks @@ -0,0 +1,21 @@ + +set a to 0. + +print(+ 1). +print(- (-1)). +print(not false). +print(defined a). + +print(1 + 2). +print(2 * 3). +print(4 / 2). +print(3 ^ 2). +print(2 > 1). +print(2 >= 1). +print(1 < 2). +print(1 <= 2). +print(1 = 1). +print(1 <> 2). +print(true and true). +print(true or false). +print(1 + false). diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index bae72f254..0038c69a9 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -1,6 +1,7 @@ using System; using NUnit.Framework; using kOS.Safe.Compilation; +using kOS.Safe.Exceptions; namespace kOS.Safe.Test.Execution { @@ -85,10 +86,22 @@ public void TestOperators() "True", "True", "True", - "True" + "True", + "Ab", + "0", + "1" ); } + [Test] + [ExpectedException(typeof(KOSCompileException))] + public void TestOperatorsException() + { + // Test that an invalid operation throws a compile error during constant folding + RunScript("integration/operators_invalid.ks"); + RunSingleStep(); + } + [Test] public void TestLock() { diff --git a/src/kOS.Safe.Test/Execution/SimpleTest.cs b/src/kOS.Safe.Test/Execution/SimpleTest.cs index 5e8a7309a..526a7b3bf 100644 --- a/src/kOS.Safe.Test/Execution/SimpleTest.cs +++ b/src/kOS.Safe.Test/Execution/SimpleTest.cs @@ -83,7 +83,10 @@ public void TestOperators() "True", "True", "True", - "True" + "True", + "Ab", + "0", + "1" ); } diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index 1d74b97c0..4ed9a7f3a 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -21,6 +21,8 @@ public List Optimize(List blocks) ExtendedBasicBlock extendedRootBlock = ExtendedBasicBlock.CreateExtendedBlockTree(blocks[0]); HashSet extendedBlocks = new HashSet(ExtendedBasicBlock.DumpTree(extendedRootBlock)); + + ConstantFolding.ApplyPass(blocks); return blocks; } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs new file mode 100644 index 000000000..a024bd106 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using kOS.Safe.Exceptions; + +namespace kOS.Safe.Compilation.IR.Optimization +{ + public static class ConstantFolding + { + public static void ApplyPass(IEnumerable blocks) + { + Queue worklist = new Queue(blocks); + IEnumerator enumerator = blocks.GetEnumerator(); + while (enumerator.MoveNext()) + { + ApplyPass(enumerator.Current); + } + } + private static void ApplyPass(BasicBlock block) + { + for (int i = 0; i < block.Instructions.Count; i++) + { + IRInstruction instruction = block.Instructions[i]; + switch (instruction) + { + case IRPop pop: + if (AttemptReductionToConstant(pop.Value) is IRConstant) + { + block.Instructions.RemoveAt(i); + i--; + } + continue; + case IRAssign assign: + assign.Value = AttemptReductionToConstant(assign.Value); + break; + case IRSuffixSet suffixSet: + suffixSet.Value = AttemptReductionToConstant(suffixSet.Value); + suffixSet.Object = AttemptReductionToConstant(suffixSet.Object); + break; + case IRIndexSet indexSet: + indexSet.Value = AttemptReductionToConstant(indexSet.Value); + indexSet.Object = AttemptReductionToConstant(indexSet.Object); + indexSet.Index = AttemptReductionToConstant(indexSet.Index); + break; + case IRBranch branch: + if (AttemptReductionToConstant(branch.Condition) is IRConstant branchConstant) + { + BasicBlock permanentBlock, deprecatedBlock; + (permanentBlock, deprecatedBlock) = Convert.ToBoolean(branchConstant.Value) ? (branch.True, branch.False) : (branch.False, branch.True); + block.Instructions[i] = new IRJump(permanentBlock, new OpcodeBranchJump() { SourceLine = branch.SourceLine, SourceColumn = branch.SourceColumn }); + block.RemoveSuccessor(deprecatedBlock); + // TODO: Resolve any phi values in the deprecated block and consider re-running folding on that block. + } + break; + } + } + } + private static IRValue AttemptReductionToConstant(IRValue input) + { + if (input is IRConstant constant) + return constant; + if (!(input is IRTemp temp)) + return input; + return AttemptReduction(temp.Parent); + } + private static IRValue AttemptReduction(IRInstruction instruction) + { + switch (instruction) + { + case IRUnaryOp unaryOp: + return ReduceUnary(unaryOp); + case IRBinaryOp binaryOp: + return ReduceBinary(binaryOp); + case IRSuffixGet suffixGet: + return ReduceSuffixGet(suffixGet); + case IRIndexGet indexGet: + return ReduceIndexGet(indexGet); + case IRCall call: + return ReduceCall(call); + } + throw new ArgumentException($"{instruction.GetType()} is not supported."); + } + private static IRValue ReduceUnary(IRUnaryOp instruction) + { + if (instruction.Operand is IRTemp temp) + instruction.Operand = AttemptReduction(temp.Parent); + + if (instruction.Operand is IRConstant constant) + { + object input = constant.Value; + IRValue result; + try + { + switch (instruction.Operation) + { + case OpcodeMathNegate _: + result = new IRConstant(OpcodeMathNegate.StaticOperation(input)); + break; + case OpcodeLogicNot _: + result = new IRConstant(OpcodeLogicNot.StaticOperation(input)); + break; + case OpcodeLogicToBool _: + result = new IRConstant(OpcodeLogicToBool.StaticOperation(input)); + break; + default: + result = instruction.Result; + break; + } + instruction.Result = result; + } + catch (KOSUnaryOperandTypeException unaryTypeException) + { + throw new KOSCompileException(instruction, unaryTypeException); + } + } + return instruction.Result; + } + private static IRValue ReduceBinary(IRBinaryOp instruction) + { + if (instruction.Left is IRTemp tempL) + { + instruction.Left = AttemptReduction(tempL.Parent); + } + if (instruction.Right is IRTemp tempR) + { + instruction.Right = AttemptReduction(tempR.Parent); + } + // Put constants to the right, if there are any + if (instruction.IsCommutative && instruction.Left is IRConstant && !(instruction.Right is IRConstant)) + { + instruction.SwapOperands(); + } + // If this is false, neither are constants after the last step, unless this isn't commutative, in which case this cleverness doesn't matter. + if (instruction.Right is IRConstant constantR) + { + // If this is true, both are constants + if (instruction.Left is IRConstant constantL) + { + object left = constantL.Value; + object right = constantR.Value; + try + { + IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right)); + instruction.Result = result; + } + catch (KOSBinaryOperandTypeException binaryTypeException) + { + throw new KOSCompileException(instruction, binaryTypeException); + } + return instruction.Result; + } + else if (instruction.IsCommutative) + { + // The right is constant and the left is not... + // But what if left.Parent is commutative with this operation and has a constant? + if (instruction.Left is IRTemp temp && + temp.Parent is IRBinaryOp leftOp && + leftOp.Operation.GetType() == instruction.Operation.GetType() && + leftOp.Right is IRConstant constantL1) + { + object right = constantR.Value; + object left = constantL1.Value; + try + { + IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right)); + instruction.Result = result; + leftOp.Right = result; + } + catch (KOSBinaryOperandTypeException binaryTypeException) + { + throw new KOSCompileException(instruction, binaryTypeException); + } + return leftOp.Result; + } + } + // Shortcuts for math operations where both sides don't need to be constant + switch (instruction.Operation) + { + case OpcodeMathMultiply _: + // X * 0 = 0 + if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + return constantR; + if (ReduceDivMult(instruction, constantR, out IRValue newResult)) + return newResult; + break; + case OpcodeMathDivide _: + if (ReduceDivMult(instruction, constantR, out newResult)) + return newResult; + break; + case OpcodeMathAdd _: + case OpcodeMathSubtract _: + // X +- 0 = X + if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + return instruction.Left; + break; + case OpcodeMathPower _: + // X^0 = 1 + if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + return new IRConstant(Encapsulation.ScalarIntValue.One); + // X^1 = X + if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) + return instruction.Left; + break; + } + } + else if (!instruction.IsCommutative && instruction.Left is IRConstant constantL) + { + switch (instruction.Operation) + { + case OpcodeMathDivide _: + if (Encapsulation.ScalarIntValue.Zero.Equals(constantL)) + return constantL; + break; + } + } + return instruction.Result; + } + private static bool ReduceDivMult(IRBinaryOp instruction, IRConstant secondOperand, out IRValue newResult) + { + // X */ 1 = X + if (Encapsulation.ScalarIntValue.One.Equals(secondOperand.Value)) + { + newResult = instruction.Left; + return true; + } + // X */ -1 = -X + if (secondOperand.Value.Equals(-Encapsulation.ScalarIntValue.One)) + { + IRTemp tempResult = instruction.Result as IRTemp; + tempResult.Parent = new IRUnaryOp( + tempResult, + new OpcodeMathNegate() + { + SourceColumn = instruction.SourceColumn, + SourceLine = instruction.SourceLine + }, + instruction.Left); + newResult = tempResult; + return true; + } + newResult = null; + return false; + } + + private static IRValue ReduceSuffixGet(IRSuffixGet instruction) + { + if (instruction.Object is IRTemp temp) + { + instruction.Object = AttemptReduction(temp.Parent); + } + return instruction.Result; + } + private static IRValue ReduceIndexGet(IRIndexGet instruction) + { + if (instruction.Object is IRTemp tempObj) + { + instruction.Object = AttemptReduction(tempObj.Parent); + } + if (instruction.Index is IRTemp tempIndex) + { + instruction.Index = AttemptReduction(tempIndex.Parent); + } + return instruction.Result; + } + private static IRValue ReduceCall(IRCall instruction) + { + // TODO: Add reduction for specific functions. E.g. mod(X, 1) = 0, round/ceiling/floor, Ln/Log10, min/max + for (int i = instruction.Arguments.Count - 1; i >= 0; i--) + { + if (instruction.Arguments[i] is IRTemp temp) + instruction.Arguments[i] = AttemptReduction(temp.Parent); + } + return instruction.Result; + } + } +} diff --git a/src/kOS.Safe/Compilation/OptimizationLevel.cs b/src/kOS.Safe/Compilation/OptimizationLevel.cs index c0cdc63b4..7ebb682de 100644 --- a/src/kOS.Safe/Compilation/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/OptimizationLevel.cs @@ -1,15 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace kOS.Safe.Compilation { /* * O1: + * Constant folding + * * Replace ship fields with their alias - * Constant propagation, including items from the constant structure + * Constant propagation * Replace lex indexing with string constant with suffixing where possible * Dead code elimination (for the never used case) * Replace constant() with constant From cd3104f95117de188fa3a619521d4e3467101db8 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 7 Mar 2026 14:32:50 -0500 Subject: [PATCH 10/56] Add constant folding for built-in scalar function calls. This required adding a virtual 'interim CPU' to accomplish the function call through the FunctionManager in order to avoid rewriting the function call logic. --- kerboscript_tests/integration/operators.ks | 2 + .../Execution/OptimizationTest.cs | 2 + src/kOS.Safe.Test/Execution/SimpleTest.cs | 2 + src/kOS.Safe/Compilation/IR/IRInstruction.cs | 2 +- .../IR/Optimization/ConstantFolding.cs | 47 ++++++ .../Compilation/IR/Optimization/InterimCPU.cs | 153 ++++++++++++++++++ 6 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs diff --git a/kerboscript_tests/integration/operators.ks b/kerboscript_tests/integration/operators.ks index f82ebac9e..a68640d51 100644 --- a/kerboscript_tests/integration/operators.ks +++ b/kerboscript_tests/integration/operators.ks @@ -21,3 +21,5 @@ print(true or false). print("A" + "b"). print(a * 0). print(a ^ 0). +print(arcTan2(0,1)). +print(abs(-1)). diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index 0038c69a9..ddc020ef1 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -89,6 +89,8 @@ public void TestOperators() "True", "Ab", "0", + "1", + "0", "1" ); } diff --git a/src/kOS.Safe.Test/Execution/SimpleTest.cs b/src/kOS.Safe.Test/Execution/SimpleTest.cs index 526a7b3bf..0615749a5 100644 --- a/src/kOS.Safe.Test/Execution/SimpleTest.cs +++ b/src/kOS.Safe.Test/Execution/SimpleTest.cs @@ -86,6 +86,8 @@ public void TestOperators() "True", "Ab", "0", + "1", + "0", "1" ); } diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 8c9b35973..a20c168ab 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -421,7 +421,7 @@ public class IRCall : IRInstruction, IResultingInstruction, IMultipleOperandInst { protected static readonly Function.FunctionManager functionManager = new Function.FunctionManager(null); public override bool SideEffects { get; } - public IRValue Result { get; } + public IRValue Result { get; set; } public string Function { get; } public List Arguments { get; } = new List(); public IEnumerable Operands => Enumerable.Reverse(Arguments); diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs index a024bd106..34c339ed8 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs @@ -1,11 +1,19 @@ using System; using System.Collections.Generic; +using System.Linq; using kOS.Safe.Exceptions; +using kOS.Safe.Function; namespace kOS.Safe.Compilation.IR.Optimization { public static class ConstantFolding { + private static readonly InterimCPU interimCPU = new InterimCPU(); + private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = interimCPU }; + static ConstantFolding() + { + shared.FunctionManager = new FunctionManager(shared); + } public static void ApplyPass(IEnumerable blocks) { Queue worklist = new Queue(blocks); @@ -269,6 +277,45 @@ private static IRValue ReduceCall(IRCall instruction) if (instruction.Arguments[i] is IRTemp temp) instruction.Arguments[i] = AttemptReduction(temp.Parent); } + string functionName = instruction.Function.Replace("()", ""); + if (shared.FunctionManager.Exists(functionName) && instruction.Arguments.All(arg => arg is IRConstant)) + { + try + { + switch (functionName) + { + case "abs": + case "mod": + case "floor": + case "ceiling": + case "round": + case "sqrt": + case "ln": + case "log10": + case "min": + case "max": + case "sin": + case "cos": + case "tan": + case "arcsin": + case "arccos": + case "arctan": + case "arctan2": + case "anglediff": + interimCPU.Boot(); // Clear the stack out of caution. + interimCPU.PushArgumentStack(new Execution.KOSArgMarkerType()); + foreach (IRValue arg in instruction.Arguments) + interimCPU.PushArgumentStack(((IRConstant)arg).Value); + shared.FunctionManager.CallFunction(functionName); + instruction.Result = new IRConstant(interimCPU.PopValueArgument()); + return instruction.Result; + } + } + catch (KOSException e) + { + throw new KOSCompileException(instruction, e); + } + } return instruction.Result; } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs b/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs new file mode 100644 index 000000000..413b02ee3 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using kOS.Safe.Encapsulation; +using kOS.Safe.Execution; + +namespace kOS.Safe.Compilation.IR.Optimization +{ + internal class InterimCPU : ICpu + { + public int InstructionPointer { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public double SessionTime => throw new NotImplementedException(); + public List ProfileResult => throw new NotImplementedException(); + public int NextTriggerInstanceId => throw new NotImplementedException(); + public int InstructionsThisUpdate => throw new NotImplementedException(); + public bool IsPoppingContext => throw new NotImplementedException(); + + private readonly Stack stack = new Stack(); + public void Boot() + { + stack.Clear(); + } + public object PopArgumentStack() + => stack.PopArgument(); + public object PopValueArgument(bool barewordOkay = false) + => GetValue(PopArgumentStack(), barewordOkay); + public void PushArgumentStack(object item) + => stack.PushArgument(item); + public object GetValue(object testValue, bool barewordOkay = false) + { + // $cos cos named variable + // cos() cos trigonometric function + // cos string literal "cos" + + // If it's a variable, meaning it starts with "$" but + // does NOT have a value like $<.....>, which are special + // flags used internally: + var identifier = testValue as string; + if (identifier == null || + identifier.Length <= 1 || + identifier[0] != '$' || + identifier[1] == '<') + { + return testValue; + } + + throw new InvalidOperationException("Only compile-time constants can be accessed. Contact a kOS developer if you see this message."); + } + + public void AddPopContextNotifyee(IPopContextNotifyee notifyee) + => throw new NotImplementedException(); + public TriggerInfo AddTrigger(int triggerFunctionPointer, InterruptPriority priority, int instanceId, bool immediate, List closure) + => throw new NotImplementedException(); + public TriggerInfo AddTrigger(TriggerInfo trigger, bool immediate) + => throw new NotImplementedException(); + public TriggerInfo AddTrigger(UserDelegate del, InterruptPriority priority, int instanceId, bool immediate, List args) + => throw new NotImplementedException(); + public TriggerInfo AddTrigger(UserDelegate del, InterruptPriority priority, int instanceId, bool immediate, params Structure[] args) + => throw new NotImplementedException(); + public void AddVariable(Variable variable, string identifier, bool local, bool overwrite = false) + => throw new NotImplementedException(); + public void AssertValidDelegateCall(IUserDelegate userDelegate) + => throw new NotImplementedException(); + public void BreakExecution(bool manual) + => throw new NotImplementedException(); + public bool BuiltInExists(string functionName) + => throw new NotImplementedException(); + public void CallBuiltinFunction(string functionName) + => throw new NotImplementedException(); + public void CancelCalledTriggers(int triggerFunctionPointer, int instanceId) + => throw new NotImplementedException(); + public void CancelCalledTriggers(TriggerInfo trigger) + => throw new NotImplementedException(); + public void Dispose() + => throw new NotImplementedException(); + public void DropBackPriority() + => throw new NotImplementedException(); + public string DumpStack() + => throw new NotImplementedException(); + public string DumpVariables() + => throw new NotImplementedException(); + public int GetArgumentStackSize() + => throw new NotImplementedException(); + public List GetCallTrace() + => throw new NotImplementedException(); + public List GetCodeFragment(int contextLines) + => throw new NotImplementedException(); + public List GetCurrentClosure() + => throw new NotImplementedException(); + public IProgramContext GetCurrentContext() + => throw new NotImplementedException(); + public Opcode GetCurrentOpcode() + => throw new NotImplementedException(); + public SubroutineContext GetCurrentSubroutineContext() + => throw new NotImplementedException(); + public Opcode GetOpcodeAt(int instructionPtr) + => throw new NotImplementedException(); + public Structure GetStructureEncapsulatedArgument(Structure testValue, bool barewordOkay = false) + => throw new NotImplementedException(); + public bool IdentifierExistsInScope(string identifier) + => throw new NotImplementedException(); + public void KOSFixedUpdate(double deltaTime) + => throw new NotImplementedException(); + public IUserDelegate MakeUserDelegate(int entryPoint, bool withClosure) + => throw new NotImplementedException(); + public object PeekRawArgument(int digDepth, out bool checkOkay) + => throw new NotImplementedException(); + public object PeekRawScope(int digDepth, out bool checkOkay) + => throw new NotImplementedException(); + public Structure PeekStructureEncapsulatedArgument(int digDepth, bool barewordOkay = false) + => throw new NotImplementedException(); + public object PeekValueArgument(int digDepth, bool barewordOkay = false) + => throw new NotImplementedException(); + public object PeekValueEncapsulatedArgument(int digDepth, bool barewordOkay = false) + => throw new NotImplementedException(); + public object PopScopeStack(int howMany) + => throw new NotImplementedException(); + public Structure PopStructureEncapsulatedArgument(bool barewordOkay = false) + => throw new NotImplementedException(); + public object PopValueEncapsulatedArgument(bool barewordOkay = false) + => throw new NotImplementedException(); + + public void PushNewScope(short scopeId, short parentScopeId) + => throw new NotImplementedException(); + public void PushScopeStack(object thing) + => throw new NotImplementedException(); + public void RemovePopContextNotifyee(IPopContextNotifyee notifyee) + => throw new NotImplementedException(); + public void RemoveTrigger(int triggerFunctionPointer, int instanceId) + => throw new NotImplementedException(); + public void RemoveTrigger(TriggerInfo trigger) + => throw new NotImplementedException(); + public void RemoveVariable(string identifier) + => throw new NotImplementedException(); + public void RunProgram(List program) + => throw new NotImplementedException(); + public void SetGlobal(string identifier, object value) + => throw new NotImplementedException(); + public void SetNewLocal(string identifier, object value) + => throw new NotImplementedException(); + public void SetValue(string identifier, object value) + => throw new NotImplementedException(); + public void SetValueExists(string identifier, object value) + => throw new NotImplementedException(); + public void StartCompileStopwatch() + => throw new NotImplementedException(); + public void StopCompileStopwatch() + => throw new NotImplementedException(); + public IProgramContext SwitchToProgramContext() + => throw new NotImplementedException(); + public void YieldProgram(YieldFinishedDetector yieldTracker) + => throw new NotImplementedException(); + } +} From 2f11d218f0fe3277b2a2c764d919b49c42813e6f Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 7 Mar 2026 15:40:37 -0500 Subject: [PATCH 11/56] Add infrastructure for automatic inclusion of optimization passes using the AssemblyWalk attribute to discover all classes implementing IOptimizationPass. --- src/kOS.Safe/Compilation/IR/IROptimizer.cs | 42 +++++++++++++++++-- .../IR/Optimization/ConstantFolding.cs | 8 +++- .../IR/Optimization/IOptimizationPass.cs | 15 +++++++ 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index 4ed9a7f3a..6bb0e095e 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -1,18 +1,25 @@ using System; using System.Collections.Generic; -using System.Linq; using kOS.Safe.Compilation.IR.Optimization; +using kOS.Safe.Utilities; namespace kOS.Safe.Compilation.IR { + [AssemblyWalk(InterfaceType = typeof(IOptimizationPass), StaticRegisterMethod = "RegisterMethod")] public class IROptimizer { + private static readonly SortedSet optimizationPasses = new SortedSet( + Comparer.Create((a, b) => a.SortIndex.CompareTo(b.SortIndex))); public OptimizationLevel OptimizationLevel { get; } public IROptimizer(OptimizationLevel optimizationLevel) { OptimizationLevel = optimizationLevel; } + public static void RegisterMethod(Type type) + { + optimizationPasses.Add((IOptimizationPass)Activator.CreateInstance(type)); + } public List Optimize(List blocks) { @@ -20,9 +27,38 @@ public List Optimize(List blocks) return blocks; ExtendedBasicBlock extendedRootBlock = ExtendedBasicBlock.CreateExtendedBlockTree(blocks[0]); - HashSet extendedBlocks = new HashSet(ExtendedBasicBlock.DumpTree(extendedRootBlock)); + List extendedBlocks = new List(ExtendedBasicBlock.DumpTree(extendedRootBlock)); + + foreach (IOptimizationPass pass in optimizationPasses) + { + if (pass.OptimizationLevel > OptimizationLevel) + continue; - ConstantFolding.ApplyPass(blocks); + SafeHouse.Logger.Log($"Applying optimization pass: {pass.GetType()}."); + try + { + switch (pass) + { + case IOptimizationPass blockPass: + blockPass.ApplyPass(blocks); + break; + case IOptimizationPass extendedBlockPass: + extendedBlockPass.ApplyPass(extendedBlocks); + break; + case IOptimizationPass instructionPass: + foreach (BasicBlock block in blocks) + instructionPass.ApplyPass(block.Instructions); + break; + default: + SafeHouse.Logger.LogWarning($"{pass.GetType()}, implementing IOptimizingPass, uses an unsupported generic parameter."); + break; + } + } + catch (Exception e) + { + throw new Exceptions.KOSCompileException(new KS.LineCol(0, 0), e.Message); + } + } return blocks; } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs index 34c339ed8..f13e28acf 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs @@ -6,15 +6,19 @@ namespace kOS.Safe.Compilation.IR.Optimization { - public static class ConstantFolding + public class ConstantFolding : IOptimizationPass { private static readonly InterimCPU interimCPU = new InterimCPU(); private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = interimCPU }; + + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + public short SortIndex => 1; + static ConstantFolding() { shared.FunctionManager = new FunctionManager(shared); } - public static void ApplyPass(IEnumerable blocks) + public void ApplyPass(List blocks) { Queue worklist = new Queue(blocks); IEnumerator enumerator = blocks.GetEnumerator(); diff --git a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs b/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs new file mode 100644 index 000000000..fec2bd9a5 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace kOS.Safe.Compilation.IR.Optimization +{ + public interface IOptimizationPass + { + OptimizationLevel OptimizationLevel { get; } + short SortIndex { get; } + } + public interface IOptimizationPass : IOptimizationPass + { + void ApplyPass(List code); + } +} From 159e0680eb42b3cddc4805b1e606d1f9e41eb88c Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 7 Mar 2026 17:06:30 -0500 Subject: [PATCH 12/56] Move the InterimCPU ownership to IROptimizer to share resources across passes. --- src/kOS.Safe/Compilation/IR/IROptimizer.cs | 11 +++++++++++ .../Compilation/IR/Optimization/ConstantFolding.cs | 12 +++--------- .../Compilation/IR/Optimization/InterimCPU.cs | 4 ++-- src/kOS.Safe/Exceptions/KOSCompileException.cs | 2 +- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index 6bb0e095e..8e42256d7 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using kOS.Safe.Compilation.IR.Optimization; +using kOS.Safe.Function; using kOS.Safe.Utilities; namespace kOS.Safe.Compilation.IR @@ -8,10 +9,18 @@ namespace kOS.Safe.Compilation.IR [AssemblyWalk(InterfaceType = typeof(IOptimizationPass), StaticRegisterMethod = "RegisterMethod")] public class IROptimizer { + internal static InterimCPU InterimCPU { get; } = new InterimCPU(); + private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = InterimCPU }; + public static IFunctionManager FunctionManager => shared.FunctionManager; + private static readonly SortedSet optimizationPasses = new SortedSet( Comparer.Create((a, b) => a.SortIndex.CompareTo(b.SortIndex))); public OptimizationLevel OptimizationLevel { get; } + static IROptimizer() + { + shared.FunctionManager = new FunctionManager(shared); + } public IROptimizer(OptimizationLevel optimizationLevel) { OptimizationLevel = optimizationLevel; @@ -56,6 +65,8 @@ public List Optimize(List blocks) } catch (Exception e) { + if (e is Exceptions.KOSCompileException) + throw; throw new Exceptions.KOSCompileException(new KS.LineCol(0, 0), e.Message); } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs index f13e28acf..82232264a 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs @@ -8,16 +8,9 @@ namespace kOS.Safe.Compilation.IR.Optimization { public class ConstantFolding : IOptimizationPass { - private static readonly InterimCPU interimCPU = new InterimCPU(); - private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = interimCPU }; - public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; public short SortIndex => 1; - static ConstantFolding() - { - shared.FunctionManager = new FunctionManager(shared); - } public void ApplyPass(List blocks) { Queue worklist = new Queue(blocks); @@ -282,7 +275,7 @@ private static IRValue ReduceCall(IRCall instruction) instruction.Arguments[i] = AttemptReduction(temp.Parent); } string functionName = instruction.Function.Replace("()", ""); - if (shared.FunctionManager.Exists(functionName) && instruction.Arguments.All(arg => arg is IRConstant)) + if (IROptimizer.FunctionManager.Exists(functionName) && instruction.Arguments.All(arg => arg is IRConstant)) { try { @@ -306,11 +299,12 @@ private static IRValue ReduceCall(IRCall instruction) case "arctan": case "arctan2": case "anglediff": + InterimCPU interimCPU = IROptimizer.InterimCPU; interimCPU.Boot(); // Clear the stack out of caution. interimCPU.PushArgumentStack(new Execution.KOSArgMarkerType()); foreach (IRValue arg in instruction.Arguments) interimCPU.PushArgumentStack(((IRConstant)arg).Value); - shared.FunctionManager.CallFunction(functionName); + IROptimizer.FunctionManager.CallFunction(functionName); instruction.Result = new IRConstant(interimCPU.PopValueArgument()); return instruction.Result; } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs b/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs index 413b02ee3..68452d427 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs @@ -25,6 +25,8 @@ public object PopValueArgument(bool barewordOkay = false) => GetValue(PopArgumentStack(), barewordOkay); public void PushArgumentStack(object item) => stack.PushArgument(item); + public object PopValueEncapsulatedArgument(bool barewordOkay = false) + => Structure.FromPrimitive(PopValueArgument(barewordOkay)); public object GetValue(object testValue, bool barewordOkay = false) { // $cos cos named variable @@ -116,8 +118,6 @@ public object PopScopeStack(int howMany) => throw new NotImplementedException(); public Structure PopStructureEncapsulatedArgument(bool barewordOkay = false) => throw new NotImplementedException(); - public object PopValueEncapsulatedArgument(bool barewordOkay = false) - => throw new NotImplementedException(); public void PushNewScope(short scopeId, short parentScopeId) => throw new NotImplementedException(); diff --git a/src/kOS.Safe/Exceptions/KOSCompileException.cs b/src/kOS.Safe/Exceptions/KOSCompileException.cs index e59e47eda..4ae21be2f 100644 --- a/src/kOS.Safe/Exceptions/KOSCompileException.cs +++ b/src/kOS.Safe/Exceptions/KOSCompileException.cs @@ -37,7 +37,7 @@ public KOSCompileException(Token token, string message) { } - public KOSCompileException(Compilation.IR.IRInstruction irInstruction, KOSException innerException) + public KOSCompileException(Compilation.IR.IRInstruction irInstruction, System.Exception innerException) : this(new LineCol(irInstruction.SourceLine, irInstruction.SourceColumn), innerException.Message) { } From 2a71eccdd469fe6a1519e2f9215b2d734e679ed0 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 7 Mar 2026 17:08:35 -0500 Subject: [PATCH 13/56] Add suffix replacement pass. This replaces all ship: suffixes that can be aliased with the alias (saves one opcode per access). This also replaces all constant() or constant: suffix accesses with the constant value, and does so in time for the constant folding pass. --- .../integration/suffixReplacement.ks | 4 + .../Execution/OptimizationTest.cs | 13 ++ src/kOS.Safe/Compilation/IR/IRInstruction.cs | 2 +- .../IR/Optimization/SuffixReplacement.cs | 194 ++++++++++++++++++ src/kOS.Safe/Compilation/OptimizationLevel.cs | 4 +- 5 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 kerboscript_tests/integration/suffixReplacement.ks create mode 100644 src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs diff --git a/kerboscript_tests/integration/suffixReplacement.ks b/kerboscript_tests/integration/suffixReplacement.ks new file mode 100644 index 000000000..b1cde7f96 --- /dev/null +++ b/kerboscript_tests/integration/suffixReplacement.ks @@ -0,0 +1,4 @@ +set airspeed to 100. // Clobber the built-in so that this value is pulled after replacement. +print(CONSTANT:g0). +print(CONSTANT():pi). +print(SHIP:airspeed). \ No newline at end of file diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index ddc020ef1..141399853 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -133,5 +133,18 @@ public void TestSuffixes() "False" ); } + + [Test] + public void TestSuffixReplacement() + { + // Test that certain suffixes are replaced + RunScript("integration/suffixReplacement.ks"); + RunSingleStep(); + AssertOutput( + "9.80665", + "3.14159265358979", + "100" + ); + } } } diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index a20c168ab..2abfca91a 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -243,7 +243,7 @@ public override string ToString() } public class IRSuffixGet : IRInteractsInstruction, IResultingInstruction, ISingleOperandInstruction { - public IRValue Result { get; } + public IRValue Result { get; set; } public IRValue Object { get; set; } public string Suffix { get; set; } IRValue ISingleOperandInstruction.Operand => Object; diff --git a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs b/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs new file mode 100644 index 000000000..264e53544 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; + +namespace kOS.Safe.Compilation.IR.Optimization +{ + public class SuffixReplacement : IOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + public short SortIndex => 0; + + public void ApplyPass(List code) + { + for (int i = 0; i < code.Count; i++) + { + IRInstruction instruction = code[i]; + switch (instruction) + { + case IRPop pop: + if (AttemptReplacement(pop.Value) is IRConstant) + { + code.RemoveAt(i); + i--; + } + continue; + case IRAssign assign: + assign.Value = AttemptReplacement(assign.Value); + break; + case IRSuffixSet suffixSet: + suffixSet.Value = AttemptReplacement(suffixSet.Value); + suffixSet.Object = AttemptReplacement(suffixSet.Object); + break; + case IRIndexSet indexSet: + indexSet.Value = AttemptReplacement(indexSet.Value); + indexSet.Object = AttemptReplacement(indexSet.Object); + indexSet.Index = AttemptReplacement(indexSet.Index); + break; + case IRBranch branch: + AttemptReplacement(branch.Condition); + break; + } + } + } + + private static IRValue AttemptReplacement(IRValue input) + { + if (input is IRTemp temp) + { + if (temp.Parent is IRSuffixGet suffixGet) + { + AttempReplaceSuffix(suffixGet); + } + else + { + AttemptReplacement(temp.Parent); + } + } + return input; + } + private static IRValue AttempReplaceSuffix(IRSuffixGet suffixGet) + { + if (suffixGet.Object is IRVariable objVariable) + { + if (objVariable.Name == "$ship") + { + switch (suffixGet.Suffix) + { + case "name": + // Rename the alias shortcut appropriately + suffixGet.Suffix = "shipname"; + break; + case "heading": + case "prograde": + case "retrograde": + case "facing": + case "maxthrust": + case "velocity": + case "geoposition": + case "latitude": + case "longitude": + case "up": + case "north": + case "body": + case "angularmomentum": + case "angularvel": + case "mass": + case "verticalSpeed": + case "groundspeed": + case "airspeed": + case "altitude": + case "apoapsis": + case "periapsis": + case "sensors": + case "srfprograde": + case "srfretrograde": + case "obt": + case "status": + break; + // All other suffixes don't have an alias, so just return the original IRTemp + default: + return suffixGet.Result; + } + // Instead of the IRTemp, which leads to resolving the suffix, return just the alias shortcut. + suffixGet.Result = new IRVariable($"${suffixGet.Suffix}"); + return suffixGet.Result; + } + if (objVariable.Name == "$constant") + { + return ReplaceConstantSuffix(suffixGet); + } + } + else if (suffixGet.Object is IRTemp tempObj && tempObj.Parent is IRCall call && call.Function == "constant()" && call.Arguments.Count == 0) + { + return ReplaceConstantSuffix(suffixGet); + } + return suffixGet.Result; + } + private static IRConstant ReplaceConstantSuffix(IRSuffixGet suffixGet) + { + IROptimizer.InterimCPU.PushArgumentStack(new Encapsulation.ConstantValue()); + try + { + new OpcodeGetMember(suffixGet.Suffix).Execute(IROptimizer.InterimCPU); + } + catch (Exception e) + { + throw new Exceptions.KOSCompileException(suffixGet, e); + } + IRConstant result = new IRConstant(IROptimizer.InterimCPU.PopValueArgument()); + suffixGet.Result = result; + return result; + } + private static IRValue AttemptReplacement(IRInstruction instruction) + { + switch (instruction) + { + case IRUnaryOp unaryOp: + return ReduceUnary(unaryOp); + case IRBinaryOp binaryOp: + return ReduceBinary(binaryOp); + case IRSuffixGet suffixGet: + return ReduceSuffixGet(suffixGet); + case IRIndexGet indexGet: + return ReduceIndexGet(indexGet); + case IRCall call: + return ReduceCall(call); + } + throw new ArgumentException($"{instruction.GetType()} is not supported."); + } + private static IRValue ReduceUnary(IRUnaryOp instruction) + { + if (instruction.Operand is IRTemp temp) + instruction.Operand = AttemptReplacement(temp); + + return instruction.Result; + } + private static IRValue ReduceBinary(IRBinaryOp instruction) + { + if (instruction.Left is IRTemp tempL) + instruction.Left = AttemptReplacement(tempL); + if (instruction.Right is IRTemp tempR) + instruction.Right = AttemptReplacement(tempR); + + return instruction.Result; + } + + private static IRValue ReduceSuffixGet(IRSuffixGet instruction) + { + if (instruction.Object is IRTemp temp) + instruction.Object = AttemptReplacement(temp); + + return instruction.Result; + } + private static IRValue ReduceIndexGet(IRIndexGet instruction) + { + if (instruction.Object is IRTemp tempObj) + instruction.Object = AttemptReplacement(tempObj); + + if (instruction.Index is IRTemp tempIndex) + instruction.Index = AttemptReplacement(tempIndex); + + return instruction.Result; + } + private static IRValue ReduceCall(IRCall instruction) + { + for (int i = instruction.Arguments.Count - 1; i >= 0; i--) + { + if (instruction.Arguments[i] is IRTemp temp) + instruction.Arguments[i] = AttemptReplacement(temp); + } + + return instruction.Result; + } + } +} diff --git a/src/kOS.Safe/Compilation/OptimizationLevel.cs b/src/kOS.Safe/Compilation/OptimizationLevel.cs index 7ebb682de..920f4e426 100644 --- a/src/kOS.Safe/Compilation/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/OptimizationLevel.cs @@ -2,13 +2,13 @@ namespace kOS.Safe.Compilation { /* * O1: + * Replace CONSTANT: values with the constant + * Replace ship fields with their alias * Constant folding * - * Replace ship fields with their alias * Constant propagation * Replace lex indexing with string constant with suffixing where possible * Dead code elimination (for the never used case) - * Replace constant() with constant * O2: * Redundant expression elimination * Particularly: Any expression of 3 opcodes used more than twice, From eb11e29a41ae171d2c03cfafc4bd2fae18f977ad Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 7 Mar 2026 19:42:44 -0500 Subject: [PATCH 14/56] Store variable and constant source line and column so that (theoretically) every emitted opcode will have a location that makes sense. --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 8 +-- src/kOS.Safe/Compilation/IR/IRValue.cs | 61 +++++++++++++++---- .../IR/Optimization/ConstantFolding.cs | 14 ++--- .../IR/Optimization/SuffixReplacement.cs | 4 +- 4 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index aa97fdf9a..ca7d6c7f7 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -252,15 +252,15 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack Value = value; + protected readonly short sourceLine, sourceColumn; + public IRConstant(object value, Opcode opcode) : this(value, opcode.SourceLine, opcode.SourceColumn) { } + public IRConstant(object value, IRInstruction instruction) : this(value, instruction.SourceLine, instruction.SourceColumn) { } + public IRConstant(object value, short sourceLine, short sourceColumn) + { + Value = value; + this.sourceLine = sourceLine; + this.sourceColumn = sourceColumn; + } internal override IEnumerable EmitPush() { - yield return new OpcodePush(Value); + yield return new OpcodePush(Value) + { + SourceLine = sourceLine, + SourceColumn = sourceColumn + }; } public override string ToString() => Value.ToString(); @@ -34,37 +45,54 @@ public IRVariableBase(string name) } public class IRVariable : IRVariableBase { + protected readonly short sourceLine, sourceColumn; public bool IsLock { get; } - public IRVariable(string name, bool isLock = false) : base(name) + public IRVariable(string name, IRInstruction instruction, bool isLock = false) : this(name, instruction.SourceLine, instruction.SourceColumn, isLock) { } + public IRVariable(string name, Opcode opcode, bool isLock = false) : this(name, opcode.SourceLine, opcode.SourceColumn, isLock) { } + public IRVariable(string name, short sourceLine, short sourceColumn, bool isLock = false) : base(name) { IsLock = isLock; + this.sourceLine = sourceLine; + this.sourceColumn = sourceColumn; } internal override IEnumerable EmitPush() { - yield return new OpcodePush(Name); + yield return new OpcodePush(Name) + { + SourceLine = sourceLine, + SourceColumn = sourceColumn + }; } public override string ToString() => Name; } public class IRRelocateLater : IRConstant { - public IRRelocateLater(string value) : base(value) { } + public IRRelocateLater(string value, OpcodePushRelocateLater opcode) : base(value, opcode) { } internal override IEnumerable EmitPush() { - yield return new OpcodePushRelocateLater((string)Value); + yield return new OpcodePushRelocateLater((string)Value) + { + SourceLine = sourceLine, + SourceColumn = sourceColumn + }; } } public class IRDelegateRelocateLater : IRRelocateLater { public bool WithClosure { get; } - public IRDelegateRelocateLater(string value, bool withClosure) : base(value) + public IRDelegateRelocateLater(string value, bool withClosure, OpcodePushDelegateRelocateLater opcode) : base(value, opcode) { WithClosure = withClosure; } internal override IEnumerable EmitPush() { - yield return new OpcodePushDelegateRelocateLater((string)Value, WithClosure); + yield return new OpcodePushDelegateRelocateLater((string)Value, WithClosure) + { + SourceLine = sourceLine, + SourceColumn = sourceColumn + }; } } public class IRTemp : IRVariableBase @@ -80,12 +108,23 @@ public IRTemp(int id) : base($"$.temp.{id}") public IRAssign PromoteToVariable() { isPromoted = true; - return new IRAssign(new OpcodeStoreLocal(Name), this) { Scope = IRAssign.StoreScope.Local }; + return new IRAssign(new OpcodeStoreLocal(Name) + { + SourceLine = -1, + SourceColumn = 0 + }, this) + { + Scope = IRAssign.StoreScope.Local + }; } internal override IEnumerable EmitPush() { if (isPromoted) - yield return new OpcodePush(Name); + yield return new OpcodePush(Name) + { + SourceLine = -1, + SourceColumn = 0 + }; else foreach (Opcode opcode in Parent.EmitOpcode()) yield return opcode; diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs index 82232264a..05a861308 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs @@ -98,13 +98,13 @@ private static IRValue ReduceUnary(IRUnaryOp instruction) switch (instruction.Operation) { case OpcodeMathNegate _: - result = new IRConstant(OpcodeMathNegate.StaticOperation(input)); + result = new IRConstant(OpcodeMathNegate.StaticOperation(input), instruction); break; case OpcodeLogicNot _: - result = new IRConstant(OpcodeLogicNot.StaticOperation(input)); + result = new IRConstant(OpcodeLogicNot.StaticOperation(input), instruction); break; case OpcodeLogicToBool _: - result = new IRConstant(OpcodeLogicToBool.StaticOperation(input)); + result = new IRConstant(OpcodeLogicToBool.StaticOperation(input), instruction); break; default: result = instruction.Result; @@ -144,7 +144,7 @@ private static IRValue ReduceBinary(IRBinaryOp instruction) object right = constantR.Value; try { - IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right)); + IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right), instruction); instruction.Result = result; } catch (KOSBinaryOperandTypeException binaryTypeException) @@ -166,7 +166,7 @@ temp.Parent is IRBinaryOp leftOp && object left = constantL1.Value; try { - IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right)); + IRConstant result = new IRConstant(instruction.Operation.ExecuteCalculation(left, right), instruction); instruction.Result = result; leftOp.Right = result; } @@ -200,7 +200,7 @@ temp.Parent is IRBinaryOp leftOp && case OpcodeMathPower _: // X^0 = 1 if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) - return new IRConstant(Encapsulation.ScalarIntValue.One); + return new IRConstant(Encapsulation.ScalarIntValue.One, instruction); // X^1 = X if (Encapsulation.ScalarIntValue.One.Equals(constantR.Value)) return instruction.Left; @@ -305,7 +305,7 @@ private static IRValue ReduceCall(IRCall instruction) foreach (IRValue arg in instruction.Arguments) interimCPU.PushArgumentStack(((IRConstant)arg).Value); IROptimizer.FunctionManager.CallFunction(functionName); - instruction.Result = new IRConstant(interimCPU.PopValueArgument()); + instruction.Result = new IRConstant(interimCPU.PopValueArgument(), instruction); return instruction.Result; } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs b/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs index 264e53544..758408a2e 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs @@ -100,7 +100,7 @@ private static IRValue AttempReplaceSuffix(IRSuffixGet suffixGet) return suffixGet.Result; } // Instead of the IRTemp, which leads to resolving the suffix, return just the alias shortcut. - suffixGet.Result = new IRVariable($"${suffixGet.Suffix}"); + suffixGet.Result = new IRVariable($"${suffixGet.Suffix}", suffixGet); return suffixGet.Result; } if (objVariable.Name == "$constant") @@ -125,7 +125,7 @@ private static IRConstant ReplaceConstantSuffix(IRSuffixGet suffixGet) { throw new Exceptions.KOSCompileException(suffixGet, e); } - IRConstant result = new IRConstant(IROptimizer.InterimCPU.PopValueArgument()); + IRConstant result = new IRConstant(IROptimizer.InterimCPU.PopValueArgument(), suffixGet); suffixGet.Result = result; return result; } From 25b2897cd43a696ab3c25583c598fbd49cf54640 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 7 Mar 2026 20:11:13 -0500 Subject: [PATCH 15/56] Offload optimization steps from Compiler. Add a new IRCodePart representation for whole-program optimizations (e.g. function inlining). --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 64 +++++++++---------- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 25 ++++++++ src/kOS.Safe/Compilation/IR/IROptimizer.cs | 24 +++++-- .../IR/Optimization/IOptimizationPass.cs | 4 ++ src/kOS.Safe/Compilation/KS/Compiler.cs | 22 +++---- 5 files changed, 88 insertions(+), 51 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/IRCodePart.cs diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index ca7d6c7f7..44a3bf4fc 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -8,18 +8,18 @@ public class IRBuilder { private int nextTempId = 0; - public List Blocks { get; } = new List(); - - public void Lower(List code) + public List Lower(List code) { + List blocks = new List(); if (code.Count == 0) - return; + return blocks; Dictionary labels = ProgramBuilder.MapLabels(code); - CreateBlocks(code, labels); - FillBlocks(code, labels); + CreateBlocks(code, labels, blocks); + FillBlocks(code, labels, blocks); + return blocks; } - private void CreateBlocks(List code, Dictionary labels) + private void CreateBlocks(List code, Dictionary labels, List blocks) { SortedSet leaders = new SortedSet() { 0 }; for (int i = 1; i < code.Count; i++) // The first instruction is always a leader so we can skip 0. @@ -32,39 +32,37 @@ private void CreateBlocks(List code, Dictionary labels) else leaders.Add(i + branch.Distance); } - else if (code[i] is OpcodeJumpStack jumpstack) + else if (code[i] is OpcodeJumpStack) { throw new NotImplementedException("OpcodeJumpStack is not implemented for optimization because it is non-deterministic. Use OptimizationLevel.None."); } else if (code[i] is OpcodeReturn) - { leaders.Add(i + 1); - } - //else if (code[i] is OpcodePushRelocateLater relocateLater) - //{ - //leaders.Add(labels[relocateLater.DestinationLabel]); - //} + else if (code[i] is OpcodePushScope) + leaders.Add(i); + else if (code[i] is OpcodePopScope) + leaders.Add(i + 1); } leaders.Add(code.Count); foreach (int startIndex in leaders.Take(leaders.Count - 1)) { int endIndex = leaders.First(i => i > startIndex) - 1; - BasicBlock block = new BasicBlock(startIndex, endIndex, Blocks.Count); - Blocks.Add(block); + BasicBlock block = new BasicBlock(startIndex, endIndex, blocks.Count); + blocks.Add(block); } - foreach (BasicBlock block in Blocks) + foreach (BasicBlock block in blocks) { Opcode lastOpcode = code[block.EndIndex]; if (lastOpcode is BranchOpcode branch) { int destinationIndex = branch.DestinationLabel != string.Empty ? labels[branch.DestinationLabel] : block.EndIndex + branch.Distance; - block.AddSuccessor(GetBlockFromStartIndex(destinationIndex)); + block.AddSuccessor(GetBlockFromStartIndex(blocks, destinationIndex)); if (!(branch is OpcodeBranchJump)) - block.AddSuccessor(GetBlockFromStartIndex(block.EndIndex + 1)); + block.AddSuccessor(GetBlockFromStartIndex(blocks, block.EndIndex + 1)); } - else if (Blocks.Any(b => b.StartIndex == block.EndIndex + 1)) + else if (blocks.Any(b => b.StartIndex == block.EndIndex + 1)) { - BasicBlock successor = GetBlockFromStartIndex(block.EndIndex + 1); + BasicBlock successor = GetBlockFromStartIndex(blocks, block.EndIndex + 1); block.Add(new IRJump(successor, lastOpcode.SourceLine, lastOpcode.SourceColumn)); block.AddSuccessor(successor); } @@ -74,21 +72,21 @@ private void CreateBlocks(List code, Dictionary labels) } } - private BasicBlock GetBlockFromStartIndex(int startIndex) - => Blocks.First(b => b.StartIndex == startIndex); + private BasicBlock GetBlockFromStartIndex(List blocks, int startIndex) + => blocks.First(b => b.StartIndex == startIndex); - private void FillBlocks(List code, Dictionary labels) + private void FillBlocks(List code, Dictionary labels, List blocks) { Stack stack = new Stack(); - BasicBlock currentBlock = GetBlockFromStartIndex(0); + BasicBlock currentBlock = GetBlockFromStartIndex(blocks, 0); for (int i = 0; i < code.Count; i++) { if (i > currentBlock.EndIndex) { currentBlock.SetStackState(stack); - currentBlock = GetBlockFromStartIndex(i); + currentBlock = GetBlockFromStartIndex(blocks, i); } - ParseInstruction(code[i], currentBlock, stack, labels, i); + ParseInstruction(code[i], currentBlock, stack, labels, i, blocks); } } @@ -99,7 +97,7 @@ private IRTemp CreateTemp() return result; } - private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack stack, Dictionary labels, int index) + private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack stack, Dictionary labels, int index, List blocks) { HashSet variables = new HashSet(); switch (opcode) @@ -178,14 +176,14 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack FunctionsCode { get; set; } + public List InitializationCode { get; set; } + public List MainCode { get; set; } + public void EmitToCodePart(CodePart codePart) + { + codePart.FunctionsCode = IREmitter.Emit(FunctionsCode); + codePart.InitializationCode = IREmitter.Emit(InitializationCode); + codePart.MainCode = IREmitter.Emit(MainCode); + } + } +} diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index 8e42256d7..fc6d71bc1 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -30,13 +30,24 @@ public static void RegisterMethod(Type type) optimizationPasses.Add((IOptimizationPass)Activator.CreateInstance(type)); } - public List Optimize(List blocks) + public List Optimize(IRCodePart codePart) { - if (blocks.Count == 0) - return blocks; + List blocks = new List(); + blocks.AddRange(codePart.InitializationCode); + blocks.AddRange(codePart.FunctionsCode); + blocks.AddRange(codePart.MainCode); - ExtendedBasicBlock extendedRootBlock = ExtendedBasicBlock.CreateExtendedBlockTree(blocks[0]); - List extendedBlocks = new List(ExtendedBasicBlock.DumpTree(extendedRootBlock)); + ExtendedBasicBlock initializationRoot = codePart.InitializationCode.Count > 0 ? ExtendedBasicBlock.CreateExtendedBlockTree(codePart.InitializationCode[0]) : null; + ExtendedBasicBlock functionsRoot = codePart.FunctionsCode.Count > 0 ? ExtendedBasicBlock.CreateExtendedBlockTree(codePart.FunctionsCode[0]) : null; + ExtendedBasicBlock mainRoot = codePart.MainCode.Count > 0 ? ExtendedBasicBlock.CreateExtendedBlockTree(codePart.MainCode[0]) : null; + + List extendedBlocks = new List(); + if (initializationRoot != null) + extendedBlocks.AddRange(ExtendedBasicBlock.DumpTree(initializationRoot)); + if (functionsRoot != null) + extendedBlocks.AddRange(ExtendedBasicBlock.DumpTree(functionsRoot)); + if (mainRoot != null) + extendedBlocks.AddRange(ExtendedBasicBlock.DumpTree(mainRoot)); foreach (IOptimizationPass pass in optimizationPasses) { @@ -48,6 +59,9 @@ public List Optimize(List blocks) { switch (pass) { + case IHolisticOptimizationPass codePartpass: + codePartpass.ApplyPass(codePart); + break; case IOptimizationPass blockPass: blockPass.ApplyPass(blocks); break; diff --git a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs b/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs index fec2bd9a5..8c746c845 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs @@ -12,4 +12,8 @@ public interface IOptimizationPass : IOptimizationPass { void ApplyPass(List code); } + public interface IHolisticOptimizationPass : IOptimizationPass + { + void ApplyPass(IRCodePart codePart); + } } diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index 6daeae868..3c9b6a814 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -187,17 +187,6 @@ private void AssertNoFuncBuiltinViolation(string name) return; } - public static void Optimize(List code, CompilerOptions options) - { - IR.IRBuilder irBuilder = new IR.IRBuilder(); - irBuilder.Lower(code); - List blocks = irBuilder.Blocks; - IR.IROptimizer optimizer = new IR.IROptimizer(options.OptimizationLevel); - blocks = optimizer.Optimize(blocks); - code.Clear(); - code.AddRange(IR.IREmitter.Emit(blocks)); - } - public CodePart Compile(int startLineNum, ParseTree tree, Context context, CompilerOptions options) { this.options = options; @@ -215,13 +204,20 @@ public CodePart Compile(int startLineNum, ParseTree tree, Context context, Compi CompileProgram(tree); if (options.OptimizationLevel != OptimizationLevel.None) { - foreach (List code in new[] { part.InitializationCode, part.FunctionsCode, part.MainCode }) - Optimize(code, options); + Optimize(part, options); } } return part; } + public static void Optimize(CodePart code, CompilerOptions options) + { + IR.IRCodePart irCode = new IR.IRCodePart(code); + IR.IROptimizer optimizer = new IR.IROptimizer(options.OptimizationLevel); + optimizer.Optimize(irCode); + irCode.EmitToCodePart(code); + } + private void CompileProgram(ParseTree tree) { currentCodeSection = part.MainCode; From c4da4dfac712601aa65d53d2f19198976f445ad0 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 00:09:55 -0500 Subject: [PATCH 16/56] Add nonsequential label capability to BasicBlocks to account for weird function and lock labels. Fix fallthrough jumps happening at the wrong time. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 12 ++++++++++-- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 7 +++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index b5ee45722..5bd0abc81 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -12,20 +12,23 @@ public class BasicBlock public IEnumerable Predecessors => predecessors; private readonly HashSet predecessors = new HashSet(); private readonly HashSet sucessors = new HashSet(); - public string Label => $"@BB#{ID}"; + public string Label => nonSequentialLabel ?? $"@BB#{ID}"; + private string nonSequentialLabel = null; public int ID { get; } public Optimization.ExtendedBasicBlock ExtendedBlock { get; set; } private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. + public IRJump FallthroughJump { get; set; } = null; #if DEBUG internal Opcode[] OriginalOpcodes { get; set; } internal Opcode[] GeneratedOpcodes => EmitOpCodes().ToArray(); #endif - public BasicBlock(int startIndex, int endIndex, int id) + public BasicBlock(int startIndex, int endIndex, int id, string nonSequentialLabel = null) { StartIndex = startIndex; EndIndex = endIndex; ID = id; + this.nonSequentialLabel = nonSequentialLabel; } public void Add(IRInstruction instruction) @@ -60,6 +63,9 @@ public override string ToString() public IEnumerable EmitOpCodes() { + bool addedFallthrough = FallthroughJump != null && Instructions.LastOrDefault() == FallthroughJump; + if (addedFallthrough) + Instructions.Add(FallthroughJump); bool first = true; foreach (IRInstruction instruction in Instructions.Take(Instructions.Count - 1)) { @@ -97,6 +103,8 @@ public IEnumerable EmitOpCodes() yield return opcode; } } + if (addedFallthrough) + Instructions.Remove(FallthroughJump); } } } diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 44a3bf4fc..fbe5a7460 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -47,7 +47,10 @@ private void CreateBlocks(List code, Dictionary labels, Lis foreach (int startIndex in leaders.Take(leaders.Count - 1)) { int endIndex = leaders.First(i => i > startIndex) - 1; - BasicBlock block = new BasicBlock(startIndex, endIndex, blocks.Count); + string label = code[startIndex].Label; + if (label.StartsWith("@")) + label = null; + BasicBlock block = new BasicBlock(startIndex, endIndex, blocks.Count, label); blocks.Add(block); } foreach (BasicBlock block in blocks) @@ -63,7 +66,7 @@ private void CreateBlocks(List code, Dictionary labels, Lis else if (blocks.Any(b => b.StartIndex == block.EndIndex + 1)) { BasicBlock successor = GetBlockFromStartIndex(blocks, block.EndIndex + 1); - block.Add(new IRJump(successor, lastOpcode.SourceLine, lastOpcode.SourceColumn)); + block.FallthroughJump = new IRJump(successor, lastOpcode.SourceLine, lastOpcode.SourceColumn); block.AddSuccessor(successor); } #if DEBUG From 8f0ee703abd4454ecf8b8a144c0395ff8d7c30e0 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 00:11:23 -0500 Subject: [PATCH 17/56] Implement arg tests. --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 8 ++++++++ src/kOS.Safe/Compilation/IR/IRInstruction.cs | 14 ++++++++++---- .../Compilation/IR/Optimization/ConstantFolding.cs | 2 ++ .../IR/Optimization/SuffixReplacement.cs | 2 ++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index fbe5a7460..5bd071c7a 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -175,8 +175,16 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack EmitOpcode() public override string ToString() => "{pop}"; } - public class IRNonVarPush : IRInstruction + public class IRNonVarPush : IRInstruction, IResultingInstruction { public override bool SideEffects => false; public Opcode Operation { get; } - public IRNonVarPush(Opcode opcode) : base(opcode) - => Operation = opcode; + + public IRValue Result { get; } + + public IRNonVarPush(IRValue result, Opcode opcode) : base(opcode) + { + Operation = opcode; + Result = result; + } internal override IEnumerable EmitOpcode() { Operation.Label = string.Empty; @@ -625,6 +631,6 @@ internal override IEnumerable EmitOpcode() yield return SetSourceLocation(new OpcodeReturn(Depth)); } public override string ToString() - => string.Format("{{ret {0}}}", Depth); + => string.Format("{return {0} deep}", Depth); } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs index 05a861308..33d358033 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs @@ -81,6 +81,8 @@ private static IRValue AttemptReduction(IRInstruction instruction) return ReduceIndexGet(indexGet); case IRCall call: return ReduceCall(call); + case IResultingInstruction resulting: + return resulting.Result; } throw new ArgumentException($"{instruction.GetType()} is not supported."); } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs b/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs index 758408a2e..f8fe76b62 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs @@ -143,6 +143,8 @@ private static IRValue AttemptReplacement(IRInstruction instruction) return ReduceIndexGet(indexGet); case IRCall call: return ReduceCall(call); + case IResultingInstruction resulting: + return resulting.Result; } throw new ArgumentException($"{instruction.GetType()} is not supported."); } From 3a5cf78695ffd8a003e272e3d9bf8a7762f14b52 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 00:11:36 -0500 Subject: [PATCH 18/56] Fix stack underflow when storing parameters. --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 12 ++++++++---- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 5 +++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 5bd071c7a..9163d8610 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -106,22 +106,26 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack 0 ? stack.Pop() : null; + IRAssign assignment = new IRAssign(store, storedValue) { Scope = IRAssign.StoreScope.Ambivalent }; variables.Add(assignment.Target); currentBlock.Add(assignment); break; case OpcodeStoreExist storeExist: - assignment = new IRAssign(storeExist, stack.Pop()) { AssertExists = true }; + storedValue = stack.Count > 0 ? stack.Pop() : null; + assignment = new IRAssign(storeExist, storedValue) { AssertExists = true }; variables.Add(assignment.Target); currentBlock.Add(assignment); break; case OpcodeStoreLocal storeLocal: - assignment = new IRAssign(storeLocal, stack.Pop()) { Scope = IRAssign.StoreScope.Local }; + storedValue = stack.Count > 0 ? stack.Pop() : null; + assignment = new IRAssign(storeLocal, storedValue) { Scope = IRAssign.StoreScope.Local }; currentBlock.Add(assignment); variables.Add(assignment.Target); break; case OpcodeStoreGlobal storeGlobal: - assignment = new IRAssign(storeGlobal, stack.Pop()) { Scope = IRAssign.StoreScope.Global }; + storedValue = stack.Count > 0 ? stack.Pop() : null; + assignment = new IRAssign(storeGlobal, storedValue) { Scope = IRAssign.StoreScope.Global }; currentBlock.Add(assignment); variables.Add(assignment.Target); break; diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index f44a841bc..be7281c74 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -68,8 +68,9 @@ public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : base(opcode) } internal override IEnumerable EmitOpcode() { - foreach (Opcode opcode in Value.EmitPush()) - yield return opcode; + if (Value != null) + foreach (Opcode opcode in Value.EmitPush()) + yield return opcode; if (AssertExists) { yield return SetSourceLocation(new OpcodeStoreExist(Target)); From 6651a5ff1798fbababb3b62ea18565161303f3ff Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 00:16:21 -0500 Subject: [PATCH 19/56] Rewrite IRCodePart and the flow from Compiler to Optimizer to enable optimizing of function code fragments. Add inspection methods to UserFunction and UserFunctionCollection to intercept the code fragments during the optimization pipeline. --- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 97 ++++++++++++++++--- src/kOS.Safe/Compilation/IR/IREmitter.cs | 26 +++-- src/kOS.Safe/Compilation/IR/IROptimizer.cs | 64 +++++------- src/kOS.Safe/Compilation/KS/Compiler.cs | 10 +- src/kOS.Safe/Compilation/KS/UserFunction.cs | 3 + .../Compilation/KS/UserFunctionCollection.cs | 2 + 6 files changed, 135 insertions(+), 67 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index 3330be5b9..a1877f8af 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -1,25 +1,98 @@ +using System; using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.KS; namespace kOS.Safe.Compilation.IR { public class IRCodePart { - public IRCodePart(CodePart codePart) + public List MainCode { get; set; } + public List Functions { get; set; } + public List RootBlocks { get; } = new List(); + public List Blocks { get; } = new List(); + + public IRCodePart(CodePart codePart, List userFunctions) { - IRBuilder irBuilder = new IRBuilder(); - InitializationCode = irBuilder.Lower(codePart.InitializationCode); - FunctionsCode = irBuilder.Lower(codePart.FunctionsCode); - MainCode = irBuilder.Lower(codePart.MainCode); + if (codePart.InitializationCode.Count > 0) + throw new ArgumentException($"{nameof(codePart)} has initialization code and is structured unexpectedly."); + if (codePart.FunctionsCode.Count > 0) + throw new ArgumentException($"{nameof(codePart)} has function code and is structured unexpectedly."); + + IRBuilder builder = new IRBuilder(); + MainCode = builder.Lower(codePart.MainCode); + Functions = userFunctions.Select(f => new IRFunction(builder, f)).ToList(); + + Blocks.AddRange(MainCode); + if (MainCode.Count > 0) + RootBlocks.Add(MainCode[0]); + foreach (IRFunction function in Functions) + { + Blocks.AddRange(function.InitializationCode); + if (function.InitializationCode.Count > 0) + RootBlocks.Add(function.InitializationCode[0]); + foreach (IRFunction.IRFunctionFragment fragment in function.Fragments.Values) + { + Blocks.AddRange(fragment.FunctionCode); + if (fragment.FunctionCode.Count > 0) + RootBlocks.Add(fragment.FunctionCode[0]); + } + } } - public List FunctionsCode { get; set; } - public List InitializationCode { get; set; } - public List MainCode { get; set; } - public void EmitToCodePart(CodePart codePart) + public void EmitCode(CodePart codePart) { - codePart.FunctionsCode = IREmitter.Emit(FunctionsCode); - codePart.InitializationCode = IREmitter.Emit(InitializationCode); - codePart.MainCode = IREmitter.Emit(MainCode); + IREmitter emitter = new IREmitter(); + foreach (IRFunction function in Functions) + { + function.EmitCode(emitter); + } + codePart.MainCode = emitter.Emit(MainCode); + } + + public class IRFunction + { + private readonly UserFunction function; + public IRFunction(IRBuilder builder, UserFunction function) + { + this.function = function; + InitializationCode = builder.Lower(function.InitializationCode); + fragments = function.PeekNewCodeFragments().ToList(); + foreach (UserFunctionCodeFragment fragment in fragments) + { + Fragments.Add(fragment, new IRFunctionFragment(builder, fragment)); + } + fragments.Reverse(); + } + + public void EmitCode(IREmitter emitter) + { + function.InitializationCode.Clear(); + function.InitializationCode.AddRange(emitter.Emit(InitializationCode)); + foreach (UserFunctionCodeFragment fragment in fragments) + { + Fragments[fragment].EmitCode(emitter); + } + } + + public List InitializationCode { get; set; } + public Dictionary Fragments { get; } = new Dictionary(); + private readonly List fragments; + public class IRFunctionFragment + { + public List FunctionCode { get; set; } + private readonly UserFunctionCodeFragment fragment; + public IRFunctionFragment(IRBuilder builder, UserFunctionCodeFragment codeFragment) + { + fragment = codeFragment; + FunctionCode = builder.Lower(codeFragment.Code); + } + public void EmitCode(IREmitter emitter) + { + fragment.Code.Clear(); + fragment.Code.AddRange(emitter.Emit(FunctionCode)); + } + } } } } diff --git a/src/kOS.Safe/Compilation/IR/IREmitter.cs b/src/kOS.Safe/Compilation/IR/IREmitter.cs index 985c244a2..3056f99e3 100644 --- a/src/kOS.Safe/Compilation/IR/IREmitter.cs +++ b/src/kOS.Safe/Compilation/IR/IREmitter.cs @@ -3,9 +3,10 @@ namespace kOS.Safe.Compilation.IR { - public static class IREmitter + public class IREmitter { - public static List Emit(List blocks) + int labelIndex = 0; + public List Emit(List blocks) { List result = new List(); Dictionary jumpLabels = new Dictionary(); @@ -13,9 +14,13 @@ public static List Emit(List blocks) foreach (BasicBlock block in blocks) LabelAndEmit(block, jumpLabels, result); // Remove single-line jumps from fallthrough blocks - for (int i = 0; i < result.Count - 1; i++) + for (int i = 0; i < result.Count; i++) { Opcode opcode = result[i]; + if (string.IsNullOrEmpty(opcode.Label) || opcode.Label.StartsWith("@")) + opcode.Label = CreateLabel(i + labelIndex); + if (i >= result.Count - 2) + continue; if (opcode is OpcodeBranchJump jump && jump.DestinationLabel != null && jump.DestinationLabel == result[i + 1].Label) { @@ -29,21 +34,24 @@ public static List Emit(List blocks) // Restore original labels foreach (Opcode opcode in result) { - if (opcode.Label != null && jumpLabels.ContainsKey(opcode.Label)) - opcode.Label = CreateLabel(jumpLabels[opcode.Label]); - if (opcode.DestinationLabel != null && jumpLabels.ContainsKey(opcode.DestinationLabel)) + if (opcode.DestinationLabel != null && + opcode.DestinationLabel.StartsWith("@") && + jumpLabels.ContainsKey(opcode.DestinationLabel)) + { opcode.DestinationLabel = CreateLabel(jumpLabels[opcode.DestinationLabel]); + } } + labelIndex += result.Count; return result; } - private static void LabelAndEmit(BasicBlock block, Dictionary jumpLabels, List result) + private void LabelAndEmit(BasicBlock block, Dictionary jumpLabels, List result) { - jumpLabels.Add(block.Label, result.Count); + jumpLabels.Add(block.Label, result.Count + labelIndex); result.AddRange(block.EmitOpCodes()); } private static string CreateLabel(int index) - => string.Format("@{0:0000}", index); // TODO: + 1 + => string.Format("@{0:0000}", index + 1); } } diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index fc6d71bc1..0b6c0ed2c 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using kOS.Safe.Compilation.IR.Optimization; using kOS.Safe.Function; using kOS.Safe.Utilities; @@ -32,22 +33,12 @@ public static void RegisterMethod(Type type) public List Optimize(IRCodePart codePart) { - List blocks = new List(); - blocks.AddRange(codePart.InitializationCode); - blocks.AddRange(codePart.FunctionsCode); - blocks.AddRange(codePart.MainCode); + List blocks = codePart.Blocks; - ExtendedBasicBlock initializationRoot = codePart.InitializationCode.Count > 0 ? ExtendedBasicBlock.CreateExtendedBlockTree(codePart.InitializationCode[0]) : null; - ExtendedBasicBlock functionsRoot = codePart.FunctionsCode.Count > 0 ? ExtendedBasicBlock.CreateExtendedBlockTree(codePart.FunctionsCode[0]) : null; - ExtendedBasicBlock mainRoot = codePart.MainCode.Count > 0 ? ExtendedBasicBlock.CreateExtendedBlockTree(codePart.MainCode[0]) : null; - - List extendedBlocks = new List(); - if (initializationRoot != null) - extendedBlocks.AddRange(ExtendedBasicBlock.DumpTree(initializationRoot)); - if (functionsRoot != null) - extendedBlocks.AddRange(ExtendedBasicBlock.DumpTree(functionsRoot)); - if (mainRoot != null) - extendedBlocks.AddRange(ExtendedBasicBlock.DumpTree(mainRoot)); + List rootExtendedBlocks = new List( + codePart.RootBlocks.Select(b => ExtendedBasicBlock.CreateExtendedBlockTree(b))); + List extendedBlocks = new List( + rootExtendedBlocks.SelectMany(ExtendedBasicBlock.DumpTree)); foreach (IOptimizationPass pass in optimizationPasses) { @@ -55,33 +46,24 @@ public List Optimize(IRCodePart codePart) continue; SafeHouse.Logger.Log($"Applying optimization pass: {pass.GetType()}."); - try - { - switch (pass) - { - case IHolisticOptimizationPass codePartpass: - codePartpass.ApplyPass(codePart); - break; - case IOptimizationPass blockPass: - blockPass.ApplyPass(blocks); - break; - case IOptimizationPass extendedBlockPass: - extendedBlockPass.ApplyPass(extendedBlocks); - break; - case IOptimizationPass instructionPass: - foreach (BasicBlock block in blocks) - instructionPass.ApplyPass(block.Instructions); - break; - default: - SafeHouse.Logger.LogWarning($"{pass.GetType()}, implementing IOptimizingPass, uses an unsupported generic parameter."); - break; - } - } - catch (Exception e) + switch (pass) { - if (e is Exceptions.KOSCompileException) - throw; - throw new Exceptions.KOSCompileException(new KS.LineCol(0, 0), e.Message); + case IHolisticOptimizationPass codePartpass: + codePartpass.ApplyPass(codePart); + break; + case IOptimizationPass blockPass: + blockPass.ApplyPass(blocks); + break; + case IOptimizationPass extendedBlockPass: + extendedBlockPass.ApplyPass(extendedBlocks); + break; + case IOptimizationPass instructionPass: + foreach (BasicBlock block in blocks) + instructionPass.ApplyPass(block.Instructions); + break; + default: + SafeHouse.Logger.LogWarning($"{pass.GetType()}, implementing IOptimizingPass, uses an unsupported generic parameter."); + break; } } return blocks; diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index 3c9b6a814..d8a78e4d6 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -204,18 +204,18 @@ public CodePart Compile(int startLineNum, ParseTree tree, Context context, Compi CompileProgram(tree); if (options.OptimizationLevel != OptimizationLevel.None) { - Optimize(part, options); + Optimize(part, context, options); } } return part; } - public static void Optimize(CodePart code, CompilerOptions options) + public static void Optimize(CodePart code, Context context, CompilerOptions options) { - IR.IRCodePart irCode = new IR.IRCodePart(code); + IR.IRCodePart irCodePart = new IR.IRCodePart(code, context.UserFunctions.PeekNewFunctions()); IR.IROptimizer optimizer = new IR.IROptimizer(options.OptimizationLevel); - optimizer.Optimize(irCode); - irCode.EmitToCodePart(code); + optimizer.Optimize(irCodePart); + irCodePart.EmitCode(code); } private void CompileProgram(ParseTree tree) diff --git a/src/kOS.Safe/Compilation/KS/UserFunction.cs b/src/kOS.Safe/Compilation/KS/UserFunction.cs index dd397cc5f..8e2a96e80 100644 --- a/src/kOS.Safe/Compilation/KS/UserFunction.cs +++ b/src/kOS.Safe/Compilation/KS/UserFunction.cs @@ -135,6 +135,9 @@ public string GetUserFunctionLabel(int expressionHash) return String.Format("{0}-{1}", Identifier, expressionHash.ToString("x")); } + internal IEnumerable PeekNewCodeFragments() + => newFunctions; + public CodePart GetCodePart() { var mergedPart = new CodePart diff --git a/src/kOS.Safe/Compilation/KS/UserFunctionCollection.cs b/src/kOS.Safe/Compilation/KS/UserFunctionCollection.cs index 23d2446d2..87faf7cc2 100644 --- a/src/kOS.Safe/Compilation/KS/UserFunctionCollection.cs +++ b/src/kOS.Safe/Compilation/KS/UserFunctionCollection.cs @@ -113,6 +113,8 @@ public List GetParts() return GetParts(userFuncs.Values.ToList()); } + internal List PeekNewFunctions() + => newUserFuncs; public IEnumerable GetNewParts() { // new locks or functions From 048d5bb8c1f50a21421cfb74e604c293b7c0cb33 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 16:45:59 -0400 Subject: [PATCH 20/56] Give BasicBlocks unique IDs across the entire compilation, rather than just that part of the code. --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 9163d8610..2e4a681b5 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -7,6 +7,7 @@ namespace kOS.Safe.Compilation.IR public class IRBuilder { private int nextTempId = 0; + private int blockID = 0; public List Lower(List code) { @@ -50,7 +51,7 @@ private void CreateBlocks(List code, Dictionary labels, Lis string label = code[startIndex].Label; if (label.StartsWith("@")) label = null; - BasicBlock block = new BasicBlock(startIndex, endIndex, blocks.Count, label); + BasicBlock block = new BasicBlock(startIndex, endIndex, blockID++, label); blocks.Add(block); } foreach (BasicBlock block in blocks) From 60c4458ac94782625c67a858337be3e246f6f603 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 16:53:07 -0400 Subject: [PATCH 21/56] Add concept of dominance to basic blocks. This will be key for determining variable scoping. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 97 +++++++++++++++++-- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 5 + .../IR/Optimization/ExtendedBasicBlock.cs | 2 +- 3 files changed, 93 insertions(+), 11 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 5bd0abc81..fa79135d4 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -5,18 +5,20 @@ namespace kOS.Safe.Compilation.IR { public class BasicBlock { + private readonly HashSet predecessors = new HashSet(); + private readonly HashSet successors = new HashSet(); + private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. + private readonly string nonSequentialLabel = null; + public int StartIndex { get; } public int EndIndex { get; } public List Instructions { get; } = new List(); - public IEnumerable Sucessors => sucessors; + public IEnumerable Successors => successors; public IEnumerable Predecessors => predecessors; - private readonly HashSet predecessors = new HashSet(); - private readonly HashSet sucessors = new HashSet(); public string Label => nonSequentialLabel ?? $"@BB#{ID}"; - private string nonSequentialLabel = null; public int ID { get; } + public BasicBlock Dominator { get; protected set; } public Optimization.ExtendedBasicBlock ExtendedBlock { get; set; } - private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. public IRJump FallthroughJump { get; set; } = null; #if DEBUG internal Opcode[] OriginalOpcodes { get; set; } @@ -36,18 +38,93 @@ public void Add(IRInstruction instruction) public void AddSuccessor(BasicBlock successor) { - sucessors.Add(successor); + successors.Add(successor); successor.AddPredecessor(this); } protected void AddPredecessor(BasicBlock predecessor) { predecessors.Add(predecessor); } - public void RemoveSuccessor(BasicBlock sucessor) + public void RemoveSuccessor(BasicBlock successor) { - if (!sucessors.Remove(sucessor)) - throw new System.ArgumentException(nameof(sucessor)); - sucessor.predecessors.Remove(this); + if (!successors.Remove(successor)) + throw new System.ArgumentException(nameof(successor)); + successor.predecessors.Remove(this); + if (successor.predecessors.Count == 0) + successor.Dominator = null; + else + successor.Dominator.EstablishDominance(); + } + public void EstablishDominance() + { + // Compute reverse postorder + var postorder = new List(); + var visited = new HashSet(); + DepthFirstSearch(this, visited, postorder); + + postorder.Reverse(); + + // Map block to index + Dictionary index = new Dictionary(); + for (int i = 0; i < postorder.Count; i++) + index[postorder[i]] = i; + + // Initialize + postorder.Remove(this); + bool changed = true; + while (changed) + { + changed = false; + + foreach (BasicBlock block in postorder) + { + // Pick first predecessor with defined dominator + BasicBlock newIdom = block.predecessors.Where(p => p != block).FirstOrDefault + (p => p == this || p.Dominator != null); + + if (newIdom == null) + continue; + + foreach (BasicBlock predecessor in block.predecessors) + { + if (predecessor == newIdom) + continue; + + if (predecessor.Dominator != null) + newIdom = Intersect(predecessor, newIdom, index); + } + + if (block.Dominator != newIdom) + { + block.Dominator = newIdom; + changed = true; + } + } + } + } + private static BasicBlock Intersect(BasicBlock b1, BasicBlock b2, Dictionary index) + { + while (b1 != b2) + { + while (index[b1] > index[b2]) + b1 = b1.Dominator; + + while (index[b2] > index[b1]) + b2 = b2.Dominator; + } + + return b1; + } + private static void DepthFirstSearch(BasicBlock block, HashSet visited, List postorder) + { + if (!visited.Add(block)) + return; + + foreach (BasicBlock successor in block.successors) + DepthFirstSearch(successor, visited, postorder); + + postorder.Add(block); + } } public void SetStackState(Stack stack) diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index a1877f8af..3324945b8 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -38,6 +38,11 @@ public IRCodePart(CodePart codePart, List userFunctions) RootBlocks.Add(fragment.FunctionCode[0]); } } + + foreach (BasicBlock block in RootBlocks) + { + block.EstablishDominance(); + } } public void EmitCode(CodePart codePart) diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs b/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs index 59a9b18e6..d15e1dc29 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs @@ -19,7 +19,7 @@ private void AddBlock(BasicBlock block) { Blocks.Add(block); block.ExtendedBlock = this; - foreach (BasicBlock successor in block.Sucessors) + foreach (BasicBlock successor in block.Successors) { if (!successor.Predecessors.Skip(1).Any()) AddBlock(successor); From cb8e5c698cd7ed0e493210cf3cb18cbac2c58987 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 16:55:05 -0400 Subject: [PATCH 22/56] Introduce IRParameter as a special type of IRValue, for instances where something would be popped from the stack, but a BasicBlock's stack is empty at that point. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 23 ++++++++ src/kOS.Safe/Compilation/IR/IRBuilder.cs | 67 ++++++++++++----------- src/kOS.Safe/Compilation/IR/IRValue.cs | 4 ++ 3 files changed, 63 insertions(+), 31 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index fa79135d4..e45e32b7d 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -7,6 +7,9 @@ public class BasicBlock { private readonly HashSet predecessors = new HashSet(); private readonly HashSet successors = new HashSet(); + private readonly List parameters = new List(); + private readonly Dictionary variables = new Dictionary(); + private readonly Dictionary externalGlobalVariables = new Dictionary(); private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. private readonly string nonSequentialLabel = null; @@ -125,6 +128,26 @@ private static void DepthFirstSearch(BasicBlock block, HashSet visit postorder.Add(block); } + + public void AddParameter(IRParameter parameter) + { + parameters.Add(parameter); + } + public void StoreVariable(IRVariable variable) + { + variables[variable.Name] = variable; + } + public IRVariable PushVariable(string name, Opcode opcode) + { + if (variables.ContainsKey(name)) + return variables[name]; + if (Dominator != null) + return Dominator.PushVariable(name, opcode); + if (externalGlobalVariables.ContainsKey(name)) + return externalGlobalVariables[name]; + IRVariable newExternalGlobal = new IRVariable(name, opcode); + externalGlobalVariables.Add(name, newExternalGlobal); + return newExternalGlobal; } public void SetStackState(Stack stack) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 2e4a681b5..e1d1b010e 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -100,68 +100,73 @@ private IRTemp CreateTemp() nextTempId++; return result; } + private static IRValue PopFromStack(Stack stack, BasicBlock block) + { + if (stack.Count > 0) + return stack.Pop(); + IRParameter parameter = new IRParameter(); + block.AddParameter(parameter); + return parameter; + } private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack stack, Dictionary labels, int index, List blocks) { + IRValue PopStack() => PopFromStack(stack, currentBlock); HashSet variables = new HashSet(); switch (opcode) { case OpcodeStore store: - IRValue storedValue = stack.Count > 0 ? stack.Pop() : null; - IRAssign assignment = new IRAssign(store, storedValue) { Scope = IRAssign.StoreScope.Ambivalent }; + IRAssign assignment = new IRAssign(store, PopStack()) { Scope = IRAssign.StoreScope.Ambivalent }; variables.Add(assignment.Target); currentBlock.Add(assignment); break; case OpcodeStoreExist storeExist: - storedValue = stack.Count > 0 ? stack.Pop() : null; - assignment = new IRAssign(storeExist, storedValue) { AssertExists = true }; + assignment = new IRAssign(storeExist, PopStack()) { AssertExists = true }; variables.Add(assignment.Target); currentBlock.Add(assignment); break; case OpcodeStoreLocal storeLocal: - storedValue = stack.Count > 0 ? stack.Pop() : null; - assignment = new IRAssign(storeLocal, storedValue) { Scope = IRAssign.StoreScope.Local }; + assignment = new IRAssign(storeLocal, PopStack()) { Scope = IRAssign.StoreScope.Local }; currentBlock.Add(assignment); variables.Add(assignment.Target); break; case OpcodeStoreGlobal storeGlobal: - storedValue = stack.Count > 0 ? stack.Pop() : null; - assignment = new IRAssign(storeGlobal, storedValue) { Scope = IRAssign.StoreScope.Global }; + assignment = new IRAssign(storeGlobal, PopStack()) { Scope = IRAssign.StoreScope.Global }; currentBlock.Add(assignment); variables.Add(assignment.Target); break; case OpcodeExists exists: IRTemp temp = CreateTemp(); - IRInstruction instruction = new IRUnaryOp(temp, exists, stack.Pop()); + IRInstruction instruction = new IRUnaryOp(temp, exists, PopStack()); temp.Parent = instruction; //currentBlock.Add(instruction); stack.Push(temp); break; case OpcodeUnset unset: - currentBlock.Add(new IRUnaryConsumer(unset, stack.Pop(), true)); + currentBlock.Add(new IRUnaryConsumer(unset, PopStack(), true)); break; case OpcodeGetMethod getMethod: temp = CreateTemp(); - instruction = new IRSuffixGetMethod(temp, stack.Pop(), getMethod); + instruction = new IRSuffixGetMethod(temp, PopStack(), getMethod); //currentBlock.Add(instruction); temp.Parent = instruction; stack.Push(temp); break; case OpcodeGetMember getMember: temp = CreateTemp(); - instruction = new IRSuffixGet(temp, stack.Pop(), getMember); + instruction = new IRSuffixGet(temp, PopStack(), getMember); temp.Parent = instruction; //currentBlock.Add(instruction); stack.Push(temp); break; case OpcodeSetMember setMember: - IRValue value = stack.Pop(); - IRValue memberObj = stack.Pop(); + IRValue value = PopStack(); + IRValue memberObj = PopStack(); currentBlock.Add(new IRSuffixSet(memberObj, value, setMember)); break; case OpcodeGetIndex getIndex: - IRValue targetIndex = stack.Pop(); - IRValue indexObj = stack.Pop(); + IRValue targetIndex = PopStack(); + IRValue indexObj = PopStack(); temp = CreateTemp(); instruction = new IRIndexGet(temp, indexObj, targetIndex, getIndex); //currentBlock.Add(instruction); @@ -169,9 +174,9 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack 0; // Not even an argument marker on the stack - the dominator block must have it while (stack.Count > 0) { - IRValue stackResult = stack.Pop(); + IRValue stackResult = PopStack(); if (stackResult is IRConstant constant && constant.Value is Execution.KOSArgMarkerType) break; arguments.Push(stackResult); @@ -255,18 +260,18 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack 0 && !((IRCall)instruction).Direct) { - ((IRCall)instruction).IndirectMethod = stack.Pop(); + ((IRCall)instruction).IndirectMethod = PopStack(); } temp.Parent = instruction; stack.Push(temp); break; case OpcodeReturn opcodeReturn: - currentBlock.Add(new IRReturn(opcodeReturn.Depth, opcodeReturn) { Value = stack.Pop() }); + currentBlock.Add(new IRReturn(opcodeReturn.Depth, opcodeReturn) { Value = PopStack() }); break; case OpcodePush opcodePush: object argument = opcodePush.Argument; if (argument is string identifier && identifier.StartsWith("$")) - stack.Push(new IRVariable(identifier, opcodePush, false)); + stack.Push(currentBlock.PushVariable(identifier, opcodePush)); else stack.Push(new IRConstant(argument, opcodePush)); break; @@ -278,13 +283,13 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack EmitPush() yield return opcode; } } + public class IRParameter : IRValue + { + internal override IEnumerable EmitPush() => System.Linq.Enumerable.Empty(); + } } From 294900fc158c4f3028e8ecd9ffb8f67e098e618d Mon Sep 17 00:00:00 2001 From: DBooots Date: Sun, 8 Mar 2026 22:02:11 -0400 Subject: [PATCH 23/56] Add Dead Code Elimination pass. This pass eliminates any basic blocks that are no longer reachable. --- src/kOS.Safe/Compilation/IR/IROptimizer.cs | 40 ++++++++++++++----- .../IR/Optimization/DeadCodeElimination.cs | 32 +++++++++++++++ .../IR/Optimization/IOptimizationPass.cs | 5 +++ src/kOS.Safe/Compilation/OptimizationLevel.cs | 4 +- 4 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/IR/IROptimizer.cs index 0b6c0ed2c..50998ed95 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/IR/IROptimizer.cs @@ -10,34 +10,49 @@ namespace kOS.Safe.Compilation.IR [AssemblyWalk(InterfaceType = typeof(IOptimizationPass), StaticRegisterMethod = "RegisterMethod")] public class IROptimizer { + public OptimizationLevel OptimizationLevel { get; } + public List Blocks { get; private set; } + public List ExtendedBlocks { get; private set; } + public HashSet RootBlocks { get; private set; } + public IRCodePart Code { get; private set; } internal static InterimCPU InterimCPU { get; } = new InterimCPU(); - private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = InterimCPU }; public static IFunctionManager FunctionManager => shared.FunctionManager; - private static readonly SortedSet optimizationPasses = new SortedSet( + private static readonly SafeSharedObjects shared = new SafeSharedObjects() { Cpu = InterimCPU }; + private readonly SortedSet optimizationPasses = new SortedSet( Comparer.Create((a, b) => a.SortIndex.CompareTo(b.SortIndex))); - public OptimizationLevel OptimizationLevel { get; } + private readonly static HashSet availablePassTypes = new HashSet(); static IROptimizer() { shared.FunctionManager = new FunctionManager(shared); } + public IROptimizer(OptimizationLevel optimizationLevel) { OptimizationLevel = optimizationLevel; + foreach (Type type in availablePassTypes) + { + IOptimizationPass pass = (IOptimizationPass)Activator.CreateInstance(type); + optimizationPasses.Add(pass); + if (pass is ILinkedOptimizationPass linkedPass) + linkedPass.Optimizer = this; + } } public static void RegisterMethod(Type type) { - optimizationPasses.Add((IOptimizationPass)Activator.CreateInstance(type)); + availablePassTypes.Add(type); } public List Optimize(IRCodePart codePart) { - List blocks = codePart.Blocks; + Code = codePart; + Blocks = codePart.Blocks; + RootBlocks = new HashSet(codePart.RootBlocks); List rootExtendedBlocks = new List( codePart.RootBlocks.Select(b => ExtendedBasicBlock.CreateExtendedBlockTree(b))); - List extendedBlocks = new List( + ExtendedBlocks = new List( rootExtendedBlocks.SelectMany(ExtendedBasicBlock.DumpTree)); foreach (IOptimizationPass pass in optimizationPasses) @@ -48,17 +63,20 @@ public List Optimize(IRCodePart codePart) SafeHouse.Logger.Log($"Applying optimization pass: {pass.GetType()}."); switch (pass) { + case ILinkedOptimizationPass linkedPass: + linkedPass.ApplyPass(); + break; case IHolisticOptimizationPass codePartpass: - codePartpass.ApplyPass(codePart); + codePartpass.ApplyPass(Code); break; case IOptimizationPass blockPass: - blockPass.ApplyPass(blocks); + blockPass.ApplyPass(Blocks); break; case IOptimizationPass extendedBlockPass: - extendedBlockPass.ApplyPass(extendedBlocks); + extendedBlockPass.ApplyPass(ExtendedBlocks); break; case IOptimizationPass instructionPass: - foreach (BasicBlock block in blocks) + foreach (BasicBlock block in Blocks) instructionPass.ApplyPass(block.Instructions); break; default: @@ -66,7 +84,7 @@ public List Optimize(IRCodePart codePart) break; } } - return blocks; + return Blocks; } } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs b/src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs new file mode 100644 index 000000000..7d4e028d0 --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR.Optimization +{ + internal class DeadCodeElimination : ILinkedOptimizationPass + { + public IROptimizer Optimizer { set => optimizer = value; } + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + public short SortIndex => 3; + + private IROptimizer optimizer; + + public void ApplyPass() + { + List blocksToRemove = new List(); + foreach (BasicBlock block in optimizer.Blocks) + { + if (block.Predecessors.Any() || optimizer.RootBlocks.Contains(block)) + continue; + blocksToRemove.Add(block); + } + foreach (BasicBlock block in blocksToRemove) + { + optimizer.Blocks.Remove(block); + foreach (BasicBlock successor in block.Successors.ToArray()) + block.RemoveSuccessor(successor); + } + } + } +} diff --git a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs b/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs index 8c746c845..90edaf715 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs +++ b/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs @@ -16,4 +16,9 @@ public interface IHolisticOptimizationPass : IOptimizationPass { void ApplyPass(IRCodePart codePart); } + public interface ILinkedOptimizationPass : IOptimizationPass + { + IROptimizer Optimizer { set; } + void ApplyPass(); + } } diff --git a/src/kOS.Safe/Compilation/OptimizationLevel.cs b/src/kOS.Safe/Compilation/OptimizationLevel.cs index 920f4e426..accae1ee7 100644 --- a/src/kOS.Safe/Compilation/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/OptimizationLevel.cs @@ -5,12 +5,12 @@ namespace kOS.Safe.Compilation * Replace CONSTANT: values with the constant * Replace ship fields with their alias * Constant folding + * Dead code elimination * * Constant propagation * Replace lex indexing with string constant with suffixing where possible - * Dead code elimination (for the never used case) * O2: - * Redundant expression elimination + * Common expression elimination * Particularly: Any expression of 3 opcodes used more than twice, * or any expression of >3 opcodes used more than once * Replace parameterless suffix method calls with get member (this feels like cheating...) From f727550ccbc358404da47525059fe302cd030dbe Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 9 Mar 2026 17:35:12 -0400 Subject: [PATCH 24/56] Adjust file structure and namespaces to be cleaner. Adjust numbering of pass sort indices to give more space for expansion. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 3 ++- src/kOS.Safe/Compilation/KS/Compiler.cs | 2 +- .../{IR => }/Optimization/ExtendedBasicBlock.cs | 3 ++- .../{IR => }/Optimization/IOptimizationPass.cs | 6 +++--- .../{IR => }/Optimization/InterimCPU.cs | 2 +- .../{ => Optimization}/OptimizationLevel.cs | 0 .../IROptimizer.cs => Optimization/Optimizer.cs} | 10 +++++----- .../Passes}/ConstantFolding.cs | 16 +++++++++------- .../Passes}/DeadCodeElimination.cs | 10 +++++----- .../Passes}/SuffixReplacement.cs | 11 ++++++----- 10 files changed, 34 insertions(+), 29 deletions(-) rename src/kOS.Safe/Compilation/{IR => }/Optimization/ExtendedBasicBlock.cs (96%) rename src/kOS.Safe/Compilation/{IR => }/Optimization/IOptimizationPass.cs (82%) rename src/kOS.Safe/Compilation/{IR => }/Optimization/InterimCPU.cs (99%) rename src/kOS.Safe/Compilation/{ => Optimization}/OptimizationLevel.cs (100%) rename src/kOS.Safe/Compilation/{IR/IROptimizer.cs => Optimization/Optimizer.cs} (94%) rename src/kOS.Safe/Compilation/{IR/Optimization => Optimization/Passes}/ConstantFolding.cs (95%) rename src/kOS.Safe/Compilation/{IR/Optimization => Optimization/Passes}/DeadCodeElimination.cs (79%) rename src/kOS.Safe/Compilation/{IR/Optimization => Optimization/Passes}/SuffixReplacement.cs (94%) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index e45e32b7d..503305570 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using kOS.Safe.Compilation.Optimization; namespace kOS.Safe.Compilation.IR { @@ -21,7 +22,7 @@ public class BasicBlock public string Label => nonSequentialLabel ?? $"@BB#{ID}"; public int ID { get; } public BasicBlock Dominator { get; protected set; } - public Optimization.ExtendedBasicBlock ExtendedBlock { get; set; } + public ExtendedBasicBlock ExtendedBlock { get; set; } public IRJump FallthroughJump { get; set; } = null; #if DEBUG internal Opcode[] OriginalOpcodes { get; set; } diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index d8a78e4d6..0df55690b 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -213,7 +213,7 @@ public CodePart Compile(int startLineNum, ParseTree tree, Context context, Compi public static void Optimize(CodePart code, Context context, CompilerOptions options) { IR.IRCodePart irCodePart = new IR.IRCodePart(code, context.UserFunctions.PeekNewFunctions()); - IR.IROptimizer optimizer = new IR.IROptimizer(options.OptimizationLevel); + Optimization.Optimizer optimizer = new Optimization.Optimizer(options.OptimizationLevel); optimizer.Optimize(irCodePart); irCodePart.EmitCode(code); } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs b/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs similarity index 96% rename from src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs rename to src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs index d15e1dc29..53dbad6a9 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ExtendedBasicBlock.cs +++ b/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; using System.Linq; +using kOS.Safe.Compilation.IR; -namespace kOS.Safe.Compilation.IR.Optimization +namespace kOS.Safe.Compilation.Optimization { public class ExtendedBasicBlock { diff --git a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs b/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs similarity index 82% rename from src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs rename to src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs index 90edaf715..c503f8f51 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/IOptimizationPass.cs +++ b/src/kOS.Safe/Compilation/Optimization/IOptimizationPass.cs @@ -1,7 +1,7 @@ -using System; using System.Collections.Generic; +using kOS.Safe.Compilation.IR; -namespace kOS.Safe.Compilation.IR.Optimization +namespace kOS.Safe.Compilation.Optimization { public interface IOptimizationPass { @@ -18,7 +18,7 @@ public interface IHolisticOptimizationPass : IOptimizationPass } public interface ILinkedOptimizationPass : IOptimizationPass { - IROptimizer Optimizer { set; } + Optimizer Optimizer { set; } void ApplyPass(); } } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs b/src/kOS.Safe/Compilation/Optimization/InterimCPU.cs similarity index 99% rename from src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs rename to src/kOS.Safe/Compilation/Optimization/InterimCPU.cs index 68452d427..b54c6b4a4 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/InterimCPU.cs +++ b/src/kOS.Safe/Compilation/Optimization/InterimCPU.cs @@ -3,7 +3,7 @@ using kOS.Safe.Encapsulation; using kOS.Safe.Execution; -namespace kOS.Safe.Compilation.IR.Optimization +namespace kOS.Safe.Compilation.Optimization { internal class InterimCPU : ICpu { diff --git a/src/kOS.Safe/Compilation/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs similarity index 100% rename from src/kOS.Safe/Compilation/OptimizationLevel.cs rename to src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs diff --git a/src/kOS.Safe/Compilation/IR/IROptimizer.cs b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs similarity index 94% rename from src/kOS.Safe/Compilation/IR/IROptimizer.cs rename to src/kOS.Safe/Compilation/Optimization/Optimizer.cs index 50998ed95..6345b5e5b 100644 --- a/src/kOS.Safe/Compilation/IR/IROptimizer.cs +++ b/src/kOS.Safe/Compilation/Optimization/Optimizer.cs @@ -1,14 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; -using kOS.Safe.Compilation.IR.Optimization; +using kOS.Safe.Compilation.IR; using kOS.Safe.Function; using kOS.Safe.Utilities; -namespace kOS.Safe.Compilation.IR +namespace kOS.Safe.Compilation.Optimization { [AssemblyWalk(InterfaceType = typeof(IOptimizationPass), StaticRegisterMethod = "RegisterMethod")] - public class IROptimizer + public class Optimizer { public OptimizationLevel OptimizationLevel { get; } public List Blocks { get; private set; } @@ -23,12 +23,12 @@ public class IROptimizer Comparer.Create((a, b) => a.SortIndex.CompareTo(b.SortIndex))); private readonly static HashSet availablePassTypes = new HashSet(); - static IROptimizer() + static Optimizer() { shared.FunctionManager = new FunctionManager(shared); } - public IROptimizer(OptimizationLevel optimizationLevel) + public Optimizer(OptimizationLevel optimizationLevel) { OptimizationLevel = optimizationLevel; foreach (Type type in availablePassTypes) diff --git a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs similarity index 95% rename from src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs rename to src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index 33d358033..1a00cec00 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -1,15 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; +using kOS.Safe.Compilation.IR; using kOS.Safe.Exceptions; -using kOS.Safe.Function; -namespace kOS.Safe.Compilation.IR.Optimization +namespace kOS.Safe.Compilation.Optimization.Passes { public class ConstantFolding : IOptimizationPass { public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; - public short SortIndex => 1; + public short SortIndex => 30; public void ApplyPass(List blocks) { @@ -47,7 +47,9 @@ private static void ApplyPass(BasicBlock block) indexSet.Index = AttemptReductionToConstant(indexSet.Index); break; case IRBranch branch: - if (AttemptReductionToConstant(branch.Condition) is IRConstant branchConstant) + if (branch.True == branch.False) + block.Instructions[i] = new IRJump(branch.True, branch.SourceLine, branch.SourceColumn); + else if (AttemptReductionToConstant(branch.Condition) is IRConstant branchConstant) { BasicBlock permanentBlock, deprecatedBlock; (permanentBlock, deprecatedBlock) = Convert.ToBoolean(branchConstant.Value) ? (branch.True, branch.False) : (branch.False, branch.True); @@ -277,7 +279,7 @@ private static IRValue ReduceCall(IRCall instruction) instruction.Arguments[i] = AttemptReduction(temp.Parent); } string functionName = instruction.Function.Replace("()", ""); - if (IROptimizer.FunctionManager.Exists(functionName) && instruction.Arguments.All(arg => arg is IRConstant)) + if (Optimizer.FunctionManager.Exists(functionName) && instruction.Arguments.All(arg => arg is IRConstant)) { try { @@ -301,12 +303,12 @@ private static IRValue ReduceCall(IRCall instruction) case "arctan": case "arctan2": case "anglediff": - InterimCPU interimCPU = IROptimizer.InterimCPU; + InterimCPU interimCPU = Optimizer.InterimCPU; interimCPU.Boot(); // Clear the stack out of caution. interimCPU.PushArgumentStack(new Execution.KOSArgMarkerType()); foreach (IRValue arg in instruction.Arguments) interimCPU.PushArgumentStack(((IRConstant)arg).Value); - IROptimizer.FunctionManager.CallFunction(functionName); + Optimizer.FunctionManager.CallFunction(functionName); instruction.Result = new IRConstant(interimCPU.PopValueArgument(), instruction); return instruction.Result; } diff --git a/src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs b/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs similarity index 79% rename from src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs rename to src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs index 7d4e028d0..f75cd8bb1 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/DeadCodeElimination.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs @@ -1,16 +1,16 @@ -using System; using System.Collections.Generic; using System.Linq; +using kOS.Safe.Compilation.IR; -namespace kOS.Safe.Compilation.IR.Optimization +namespace kOS.Safe.Compilation.Optimization.Passes { internal class DeadCodeElimination : ILinkedOptimizationPass { - public IROptimizer Optimizer { set => optimizer = value; } + public Optimizer Optimizer { set => optimizer = value; } public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; - public short SortIndex => 3; + public short SortIndex => 50; - private IROptimizer optimizer; + private Optimizer optimizer; public void ApplyPass() { diff --git a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs similarity index 94% rename from src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs rename to src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs index f8fe76b62..5922a972b 100644 --- a/src/kOS.Safe/Compilation/IR/Optimization/SuffixReplacement.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using kOS.Safe.Compilation.IR; -namespace kOS.Safe.Compilation.IR.Optimization +namespace kOS.Safe.Compilation.Optimization.Passes { public class SuffixReplacement : IOptimizationPass { public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; - public short SortIndex => 0; + public short SortIndex => 10; public void ApplyPass(List code) { @@ -116,16 +117,16 @@ private static IRValue AttempReplaceSuffix(IRSuffixGet suffixGet) } private static IRConstant ReplaceConstantSuffix(IRSuffixGet suffixGet) { - IROptimizer.InterimCPU.PushArgumentStack(new Encapsulation.ConstantValue()); + Optimizer.InterimCPU.PushArgumentStack(new Encapsulation.ConstantValue()); try { - new OpcodeGetMember(suffixGet.Suffix).Execute(IROptimizer.InterimCPU); + new OpcodeGetMember(suffixGet.Suffix).Execute(Optimizer.InterimCPU); } catch (Exception e) { throw new Exceptions.KOSCompileException(suffixGet, e); } - IRConstant result = new IRConstant(IROptimizer.InterimCPU.PopValueArgument(), suffixGet); + IRConstant result = new IRConstant(Optimizer.InterimCPU.PopValueArgument(), suffixGet); suffixGet.Result = result; return result; } From 4d59836fa0d48305dbb65f286ee2bec0f515b43b Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 9 Mar 2026 19:40:19 -0400 Subject: [PATCH 25/56] Outline planned sorting of optimization passes. --- .../Optimization/OptimizationLevel.cs | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index accae1ee7..8f7210e7b 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -1,36 +1,43 @@ +#pragma warning disable IDE0130 // Namespace does not match folder structure namespace kOS.Safe.Compilation +#pragma warning restore IDE0130 // Namespace does not match folder structure { /* * O1: - * Replace CONSTANT: values with the constant - * Replace ship fields with their alias - * Constant folding - * Dead code elimination + * 10. Suffix replacement: + * Replace CONSTANT: values with the constant + * Replace ship fields with their alias + * 30. Constant folding + * 50. Dead code elimination * - * Constant propagation - * Replace lex indexing with string constant with suffixing where possible + * 20. Constant propagation + * 1050. Peephole optimizations: + * Replace lex indexing with string constant with suffixing where possible + * Replace parameterless suffix method calls with get member (this feels like cheating...) + * Replace calls to VectorDotProduct with multiplication + * Replace --X with X + * Replace !!X with X + * Branch logical simplification (e.g. !A branch = A branch!) + * Algebraic simplification (e.g. A*B+A*C = A*(B+C), A+-B=A-B, A--B=A+B, -A+B=B-A, X*X*...*X=N^X) * O2: - * Common expression elimination - * Particularly: Any expression of 3 opcodes used more than twice, - * or any expression of >3 opcodes used more than once - * Replace parameterless suffix method calls with get member (this feels like cheating...) - * Boolean logical simplification - * Code motion - moving expressions outside loops if they do not depend on loop variables - * Replace X * X * ... * X with X^N - * Carry's - moving the N-1 D lookup of >1D lists outside the innermost loop + * 1000. Common expression elimination + * Particularly: Any expression of 3 opcodes used more than twice, + * or any expression of >3 opcodes used more than once + * 2100. Code motion - moving expressions outside loops if they do not depend on loop variables + * 2150. Carry's - moving the N-1 D lookup of >1D lists outside the innermost loop * Loop jamming? (Combining adjacent loops into one) * Unswitching (moving conditional evaluation outside the loop) - low priority * Linear function test replacement - low priority * O3: - * Local function inlining - * Constant propagation to local functions - * Loop stack manipulation (delayed setting of either index or aggregator) + * 3100. Local function inlining + * 3200. Constant propagation to local functions + * 3500. Loop stack manipulation (delayed setting of either index or aggregator) * O4: - * Constant loop unrolling + * 4000. Constant loop unrolling */ public enum OptimizationLevel : int { - None = 0, // No changes whatsoever + None = 0, // No changes whatsoever Minimal = 1, // Only changes that trim opcodes without changing any flow Balanced = 2, // Only changes that trim opcodes without increasing file size Aggressive = 3, // Favor trimming opcodes over file size. From 91f179a11919546e1fbe7ed0bbdb12c957cfd09b Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 9 Mar 2026 19:43:37 -0400 Subject: [PATCH 26/56] Clean up code for DCE and downgrade it to IHolisticOptimizationPass. --- .../Passes/DeadCodeElimination.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs b/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs index f75cd8bb1..02fbe38f8 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs @@ -4,26 +4,39 @@ namespace kOS.Safe.Compilation.Optimization.Passes { - internal class DeadCodeElimination : ILinkedOptimizationPass + internal class DeadCodeElimination : IHolisticOptimizationPass { - public Optimizer Optimizer { set => optimizer = value; } public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; public short SortIndex => 50; - private Optimizer optimizer; + public void ApplyPass(IRCodePart code) + { + HashSet rootBlocks = new HashSet(code.RootBlocks); + + RemoveDeadBlocks(code.Blocks, rootBlocks); + + foreach (IRCodePart.IRFunction function in code.Functions) + { + RemoveDeadBlocks(function.InitializationCode, rootBlocks); + foreach (IRCodePart.IRFunction.IRFunctionFragment fragment in function.Fragments.Values) + { + RemoveDeadBlocks(fragment.FunctionCode, rootBlocks); + } + } + } - public void ApplyPass() + private static void RemoveDeadBlocks(List blocks, HashSet rootBlocks) { List blocksToRemove = new List(); - foreach (BasicBlock block in optimizer.Blocks) + foreach (BasicBlock block in blocks) { - if (block.Predecessors.Any() || optimizer.RootBlocks.Contains(block)) + if (block.Predecessors.Any() || rootBlocks.Contains(block)) continue; blocksToRemove.Add(block); } foreach (BasicBlock block in blocksToRemove) { - optimizer.Blocks.Remove(block); + blocks.Remove(block); foreach (BasicBlock successor in block.Successors.ToArray()) block.RemoveSuccessor(successor); } From b0b85b9c57394c2111065783c82409b344adee28 Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 9 Mar 2026 23:16:04 -0400 Subject: [PATCH 27/56] Add methods for storing variable at local, global, or ambiguous scope. Note that scopes are not fully implemented yet. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 27 ++++++++++++++++++++++- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 10 ++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 503305570..c40ea2d3f 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -134,10 +134,35 @@ public void AddParameter(IRParameter parameter) { parameters.Add(parameter); } - public void StoreVariable(IRVariable variable) + public void StoreLocalVariable(IRVariable variable) { variables[variable.Name] = variable; } + public void StoreGlobalVariable(IRVariable variable) + { + if (Dominator != null) + { + Dominator.StoreGlobalVariable(variable); + return; + } + externalGlobalVariables[variable.Name] = variable; + } + public void StoreVariable(IRVariable variable) + { + if (!TryStoreVariable(variable)) + StoreGlobalVariable(variable); + } + public bool TryStoreVariable(IRVariable variable) + { + if (variables.ContainsKey(variable.Name)) + { + variables[variable.Name] = variable; + return true; + } + if (Dominator != null) + return Dominator.TryStoreVariable(variable); + return false; + } public IRVariable PushVariable(string name, Opcode opcode) { if (variables.ContainsKey(name)) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index e1d1b010e..3adf5e491 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -112,28 +112,28 @@ private static IRValue PopFromStack(Stack stack, BasicBlock block) private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack stack, Dictionary labels, int index, List blocks) { IRValue PopStack() => PopFromStack(stack, currentBlock); - HashSet variables = new HashSet(); switch (opcode) { case OpcodeStore store: IRAssign assignment = new IRAssign(store, PopStack()) { Scope = IRAssign.StoreScope.Ambivalent }; - variables.Add(assignment.Target); + currentBlock.StoreVariable(new IRVariable(assignment.Target, assignment)); currentBlock.Add(assignment); break; case OpcodeStoreExist storeExist: assignment = new IRAssign(storeExist, PopStack()) { AssertExists = true }; - variables.Add(assignment.Target); + if (!currentBlock.TryStoreVariable(new IRVariable(assignment.Target, assignment))) + throw new Exceptions.KOSCompileException(new KS.LineCol(assignment.SourceLine, assignment.SourceColumn), "Assert that variable exists failed."); currentBlock.Add(assignment); break; case OpcodeStoreLocal storeLocal: assignment = new IRAssign(storeLocal, PopStack()) { Scope = IRAssign.StoreScope.Local }; + currentBlock.StoreLocalVariable(new IRVariable(assignment.Target, assignment)); currentBlock.Add(assignment); - variables.Add(assignment.Target); break; case OpcodeStoreGlobal storeGlobal: assignment = new IRAssign(storeGlobal, PopStack()) { Scope = IRAssign.StoreScope.Global }; + currentBlock.StoreGlobalVariable(new IRVariable(assignment.Target, assignment)); currentBlock.Add(assignment); - variables.Add(assignment.Target); break; case OpcodeExists exists: IRTemp temp = CreateTemp(); From 80e6cc35dc7d2885e1b5770ebd9de04de289319c Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 9 Mar 2026 23:35:54 -0400 Subject: [PATCH 28/56] Add X/X=1 simplification to the constant folding pass. This will say that 0/0=1 or 0/X=0 for all X (including 0) instead of throwing an exception. But that's already undefined behaviour so this should be acceptable. --- .../Optimization/Passes/ConstantFolding.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index 1a00cec00..45144d835 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -192,6 +192,8 @@ temp.Parent is IRBinaryOp leftOp && return newResult; break; case OpcodeMathDivide _: + if (Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + throw new KOSCompileException(instruction, new DivideByZeroException()); if (ReduceDivMult(instruction, constantR, out newResult)) return newResult; break; @@ -211,13 +213,26 @@ temp.Parent is IRBinaryOp leftOp && break; } } - else if (!instruction.IsCommutative && instruction.Left is IRConstant constantL) + else { switch (instruction.Operation) { case OpcodeMathDivide _: - if (Encapsulation.ScalarIntValue.Zero.Equals(constantL)) + // 0 / X = 0 + // Technically not true when X = 0 + // But that would otherwise throw a "Tried to push infinite on to the stack" error + // So this is an acceptable assumption that improves performance and eliminates an error. + // TODO: Add an "EXIT" (EOP) command to the language because this will break the + // PRINT(1/0) shortcut to cause a program to terminate. + if (instruction.Left is IRConstant constantL && + Encapsulation.ScalarIntValue.Zero.Equals(constantL.Value)) return constantL; + // X / X = 1 + // Technically not true when X = 0 + // But that would otherwise throw a "Tried to push infinite on to the stack" error + // So this is an acceptable assumption that improves performance and eliminates an error. + if (instruction.Left == instruction.Right) + return new IRConstant(Encapsulation.ScalarIntValue.One, instruction); break; } } From 0afa7aa6e0459b136c17396733488a0073e644e1 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 16:38:36 -0400 Subject: [PATCH 29/56] Add set access to IRInstruction interfaces, as well as indexing for IMultipleOperandInstructions. --- .../Compilation/IR/IOperandInstructionBase.cs | 4 +- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 80 +++++++++++++++++-- 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs index bb87b07c2..ed1aa7251 100644 --- a/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs +++ b/src/kOS.Safe/Compilation/IR/IOperandInstructionBase.cs @@ -6,10 +6,12 @@ public interface IOperandInstructionBase { } public interface ISingleOperandInstruction : IOperandInstructionBase { - IRValue Operand { get; } + IRValue Operand { get; set; } } public interface IMultipleOperandInstruction : IOperandInstructionBase { IEnumerable Operands { get; } + IRValue this[int index] { get; set; } + int OperandCount { get; } } } diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index be7281c74..d913be6c9 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -59,7 +59,7 @@ public enum StoreScope public IRValue Value { get; set; } public StoreScope Scope { get; set; } = StoreScope.Ambivalent; public bool AssertExists { get; set; } = false; - IRValue ISingleOperandInstruction.Operand => Value; + IRValue ISingleOperandInstruction.Operand { get => Value; set => Value = value; } public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : base(opcode) { @@ -97,10 +97,24 @@ public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperand { public override bool SideEffects => false; public IRValue Result { get; set; } - public BinaryOpcode Operation { get; protected set; } + public BinaryOpcode Operation { get; set; } public IRValue Left { get; set; } public IRValue Right { get; set; } public IEnumerable Operands { get { yield return Left; yield return Right; } } + public int OperandCount => 2; + IRValue IMultipleOperandInstruction.this[int index] + { + get => index == 0 ? Left : index == 1 ? Right : throw new System.ArgumentOutOfRangeException(); + set + { + if (index == 0) + Left = value; + else if (index == 1) + Right = value; + else + throw new System.ArgumentOutOfRangeException(); + } + } public bool IsCommutative { get; } public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) : base(operation) @@ -192,7 +206,7 @@ public class IRUnaryConsumer : IRInstruction, ISingleOperandInstruction { public override bool SideEffects { get; } public Opcode Operation { get; } - public IRValue Operand { get; } + public IRValue Operand { get; set; } public IRUnaryConsumer(Opcode opcode, IRValue operand, bool sideEffects = false) : base(opcode) { Operation = opcode; @@ -213,7 +227,7 @@ public class IRPop : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; public IRValue Value { get; set; } - IRValue ISingleOperandInstruction.Operand => Value; + IRValue ISingleOperandInstruction.Operand { get => Value; set => Value = value; } public IRPop(IRValue value, OpcodePop opcode) : base(opcode) => Value = value; @@ -253,7 +267,7 @@ public class IRSuffixGet : IRInteractsInstruction, IResultingInstruction, ISingl public IRValue Result { get; set; } public IRValue Object { get; set; } public string Suffix { get; set; } - IRValue ISingleOperandInstruction.Operand => Object; + IRValue ISingleOperandInstruction.Operand { get => Object; set => Object = value; } public IRSuffixGet(IRTemp result, IRValue obj, OpcodeGetMember opcodeGetMember) : base(obj, opcodeGetMember) { Result = result; @@ -288,6 +302,20 @@ public class IRSuffixSet : IRInteractsInstruction, IMultipleOperandInstruction public IRValue Object { get; set; } public IRValue Value { get; set; } public IEnumerable Operands { get { yield return Object; yield return Value; } } + public int OperandCount => 2; + IRValue IMultipleOperandInstruction.this[int index] + { + get => index == 0 ? Object : index == 1 ? Value : throw new System.ArgumentOutOfRangeException(); + set + { + if (index == 0) + Object = value; + else if (index == 1) + Value = value; + else + throw new System.ArgumentOutOfRangeException(); + } + } public string Suffix { get; } public IRSuffixSet(IRValue obj, IRValue value, OpcodeSetMember opcodeSetMember) : base(obj, opcodeSetMember) { @@ -313,6 +341,20 @@ public class IRIndexGet : IRInstruction, IResultingInstruction, IMultipleOperand public IRValue Object { get; set; } public IRValue Index { get; set; } public IEnumerable Operands { get { yield return Object; yield return Index; } } + public int OperandCount => 2; + IRValue IMultipleOperandInstruction.this[int index] + { + get => index == 0 ? Object : index == 1 ? Index : throw new System.ArgumentOutOfRangeException(); + set + { + if (index == 0) + Object = value; + else if (index == 1) + Index = value; + else + throw new System.ArgumentOutOfRangeException(); + } + } public IRIndexGet(IRTemp result, IRValue obj, IRValue index, OpcodeGetIndex opcode) : base(opcode) { Result = result; @@ -337,6 +379,22 @@ public class IRIndexSet : IRInstruction, IMultipleOperandInstruction public IRValue Index { get; set; } public IRValue Value { get; set; } public IEnumerable Operands { get { yield return Object; yield return Index; yield return Value; } } + public int OperandCount => 3; + IRValue IMultipleOperandInstruction.this[int index] + { + get => index == 0 ? Object : index == 1 ? Index : index == 2 ? Value : throw new System.ArgumentOutOfRangeException(); + set + { + if (index == 0) + Object = value; + else if (index == 1) + Index = value; + else if (index == 2) + Value = value; + else + throw new System.ArgumentOutOfRangeException(); + } + } public IRIndexSet(IRValue obj, IRValue index, IRValue value, OpcodeSetIndex opcode) : base(opcode) { Object = obj; @@ -377,7 +435,7 @@ public class IRJumpStack : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; public IRValue Distance { get; set; } - IRValue ISingleOperandInstruction.Operand => Distance; + IRValue ISingleOperandInstruction.Operand { get => Distance; set => Distance = value; } public List Targets { get; } = new List(); public IRJumpStack(IRValue distance, IEnumerable targets, OpcodeJumpStack jumpStack) : base(jumpStack) { @@ -395,7 +453,7 @@ public class IRBranch : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; public IRValue Condition { get; set; } - IRValue ISingleOperandInstruction.Operand => Condition; + IRValue ISingleOperandInstruction.Operand { get => Condition; set => Condition = value; } public BasicBlock True { get; set; } public BasicBlock False { get; set; } public bool PreferFalse { get; set; } = false; @@ -432,6 +490,12 @@ public class IRCall : IRInstruction, IResultingInstruction, IMultipleOperandInst public string Function { get; } public List Arguments { get; } = new List(); public IEnumerable Operands => Enumerable.Reverse(Arguments); + public int OperandCount => Arguments.Count; + IRValue IMultipleOperandInstruction.this[int index] + { + get => Arguments[index]; + set => Arguments[index] = value; + } public IRValue IndirectMethod { get; internal set; } public bool Direct { get; } public bool EmitArgMarker { get; set; } @@ -618,7 +682,7 @@ public class IRReturn : IRInstruction, ISingleOperandInstruction { public override bool SideEffects => false; public IRValue Value { get; set; } - IRValue ISingleOperandInstruction.Operand => Value; + IRValue ISingleOperandInstruction.Operand { get => Value; set => Value = value; } public short Depth { get; internal set; } public IRReturn(short depth, OpcodeReturn opcode) : base(opcode) => Depth = depth; From cf885399a47712f69e80636c8706884982500c58 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 16:40:47 -0400 Subject: [PATCH 30/56] Make ConstantFolding.AttemptReduction publicly accessible to allow later passes to do targeted constant folding after making a change. --- .../Compilation/Optimization/Passes/ConstantFolding.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs index 45144d835..ab3bb56e0 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantFolding.cs @@ -69,7 +69,7 @@ private static IRValue AttemptReductionToConstant(IRValue input) return input; return AttemptReduction(temp.Parent); } - private static IRValue AttemptReduction(IRInstruction instruction) + public static IRValue AttemptReduction(IRInstruction instruction) { switch (instruction) { @@ -86,7 +86,7 @@ private static IRValue AttemptReduction(IRInstruction instruction) case IResultingInstruction resulting: return resulting.Result; } - throw new ArgumentException($"{instruction.GetType()} is not supported."); + throw new ArgumentException($"{instruction.GetType()} is not supported for constant folding."); } private static IRValue ReduceUnary(IRUnaryOp instruction) { From a435ddb4bf0e88c78856c8b775ca0427126459d3 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 16:41:52 -0400 Subject: [PATCH 31/56] Provide a method for overwriting the source location of an instruction. This should be used carefully for in-place restructuring of IR instructions. --- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index d913be6c9..09d6a4013 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -5,8 +5,8 @@ namespace kOS.Safe.Compilation.IR { public abstract class IRInstruction { - public short SourceLine { get; } // line number in the source code that this was compiled from. - public short SourceColumn { get; } // column number of the token nearest the cause of this Opcode. + public short SourceLine { get; private set; } // line number in the source code that this was compiled from. + public short SourceColumn { get; private set; } // column number of the token nearest the cause of this Opcode. // Should-be-static public abstract bool SideEffects { get; } @@ -24,6 +24,11 @@ protected Opcode SetSourceLocation(Opcode opcode) opcode.SourceColumn = SourceColumn; return opcode; } + public void OverwriteSourceLocation(short sourceLine, short sourceColumn) + { + SourceLine = sourceLine; + SourceColumn = sourceColumn; + } } public abstract class IRInteractsInstruction : IRInstruction { From 336ac48fe4af9c39b5212588491eb7a3b7ca75dc Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 16:43:31 -0400 Subject: [PATCH 32/56] Add a static utility class to make further passes that search the instruction tree more concise and easier to write. --- .../Optimization/OptimizationTools.cs | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs new file mode 100644 index 000000000..3a17a091e --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Compilation.Optimization +{ + public static class OptimizationTools + { + /*public static IEnumerable FindExpressionsMatching(this IEnumerable instructions, Predicate predicate) + => instructions.SelectMany(i => ExpressionMatchDepthFirst(i, predicate)); + + public static IEnumerable FindFirstExpressionsMatching(this IEnumerable instructions, Predicate predicate) + => instructions.SelectMany(i => ExpressionMatchBreadthFirst(i, predicate)); + */ + + public static IEnumerable DepthFirst(this IEnumerable instructions) + => instructions.SelectMany(DepthFirst); + + public static IEnumerable FindInstructionsMatching(this IEnumerable instructions, Predicate predicate) + => instructions.SelectMany(i => InstructionMatchDepthFirst(i, predicate)); + + public static IEnumerable FindFirstInstructionsMatching(this IEnumerable instructions, Predicate predicate) + => instructions.SelectMany(i => InstructionMatchBreadthFirst(i, predicate)); + + private static IEnumerable ExpressionMatchDepthFirst(IRInstruction instruction, Predicate predicate) + { + if (instruction is ISingleOperandInstruction singleOperandInstruction) + { + if (singleOperandInstruction.Operand is IRTemp temp) + { + foreach (IRTemp predecessorMatch in ExpressionMatchDepthFirst(temp.Parent, predicate)) + yield return predecessorMatch; + if (predicate(temp.Parent)) + yield return temp; + } + } + else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) + { + foreach (IRValue operand in multipleOperandInstruction.Operands) + { + if (operand is IRTemp temp) + { + foreach (IRTemp predecessorMatch in ExpressionMatchDepthFirst(temp.Parent, predicate)) + yield return predecessorMatch; + if (predicate(temp.Parent)) + yield return temp; + } + } + } + } + + private static IEnumerable ExpressionMatchBreadthFirst(IRInstruction instruction, Predicate predicate) + { + if (instruction is ISingleOperandInstruction singleOperandInstruction) + { + if (singleOperandInstruction.Operand is IRTemp temp) + { + if (predicate(temp.Parent)) + yield return temp; + foreach (IRTemp predecessorMatch in ExpressionMatchBreadthFirst(temp.Parent, predicate)) + yield return predecessorMatch; + } + } + else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) + { + foreach (IRValue operand in multipleOperandInstruction.Operands) + { + if (operand is IRTemp temp && predicate(temp.Parent)) + yield return temp; + } + foreach (IRValue operand in multipleOperandInstruction.Operands) + { + if (operand is IRTemp temp) + { + foreach (IRTemp predecessorMatch in ExpressionMatchBreadthFirst(temp.Parent, predicate)) + yield return predecessorMatch; + } + } + } + } + + private static IEnumerable InstructionMatchDepthFirst(IRInstruction instruction, Predicate predicate) + { + foreach (IRInstruction match in CrawlTreeForMatch(instruction, predicate, InstructionMatchDepthFirst)) + yield return match; + if (predicate(instruction)) + yield return instruction; + } + + private static IEnumerable InstructionMatchBreadthFirst(IRInstruction instruction, Predicate predicate) + { + if (predicate(instruction)) + yield return instruction; + foreach (IRInstruction match in CrawlTreeForMatch(instruction, predicate, InstructionMatchBreadthFirst)) + yield return match; + } + + private static IEnumerable DepthFirst(IRInstruction instruction) + { + if (instruction is ISingleOperandInstruction singleOperandInstruction) + { + if (singleOperandInstruction.Operand is IRTemp temp) + foreach (IRInstruction predecessor in DepthFirst(temp.Parent)) + yield return predecessor; + } + else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) + { + foreach (IRValue operand in multipleOperandInstruction.Operands) + { + if (operand is IRTemp temp) + foreach (IRInstruction predecessor in DepthFirst(temp.Parent)) + yield return predecessor; + } + } + yield return instruction; + } + + private static IEnumerable CrawlTreeForMatch(IRInstruction instruction, Predicate predicate, Func, IEnumerable> searchFunc) + { + if (instruction is ISingleOperandInstruction singleOperandInstruction) + { + if (singleOperandInstruction.Operand is IRTemp temp) + foreach (IRInstruction match in searchFunc(temp.Parent, predicate)) + yield return match; + } + else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) + { + foreach (IRValue operand in multipleOperandInstruction.Operands) + { + if (operand is IRTemp temp) + foreach (IRInstruction match in searchFunc(temp.Parent, predicate)) + yield return match; + } + } + } + } +} From a879ba8abadcb986316843f135635612a1d67162 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 22:38:15 -0400 Subject: [PATCH 33/56] Implement equality checking for IR values and instructions. Instructions are equal if their operation and operands are equal. Temporary values are equal if the sequence of operations to return them are equal. --- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 109 +++++++++++++++++++ src/kOS.Safe/Compilation/IR/IRValue.cs | 33 ++++++ 2 files changed, 142 insertions(+) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 09d6a4013..63bb51394 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -97,6 +97,15 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{{store {0}}}", Value.ToString()); + public override bool Equals(object obj) + { + if (obj is IRAssign assignment) + return string.Equals(Target, assignment.Target, System.StringComparison.OrdinalIgnoreCase) && + Value.Equals(assignment.Value); + return base.Equals(obj); + } + public override int GetHashCode() + => Target.ToLower().GetHashCode(); } public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { @@ -170,6 +179,18 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => Operation.ToString(); + public override bool Equals(object obj) + { + if (obj is IRBinaryOp binaryOp && + Operation.GetType() == binaryOp.Operation.GetType()) + { + return (Left.Equals(binaryOp.Left) && Right.Equals(binaryOp.Right)) || + (IsCommutative && Left.Equals(binaryOp.Right) && Right.Equals(binaryOp.Left)); + } + return false; + } + public override int GetHashCode() + => Operation.GetHashCode(); } public class IRUnaryOp : IRInstruction, IResultingInstruction, ISingleOperandInstruction { @@ -192,6 +213,12 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => Operation.ToString(); + public override bool Equals(object obj) + => obj is IRUnaryOp unaryOp && + Operation.GetType() == unaryOp.Operation.GetType() && + Operand.Equals(unaryOp.Operand); + public override int GetHashCode() + => Operation.GetHashCode(); } public class IRNoStackInstruction : IRInstruction { @@ -206,6 +233,10 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => Operation.ToString(); + public override bool Equals(object obj) + => obj is IRNoStackInstruction instruction && Operation.GetType() == instruction.Operation.GetType(); + public override int GetHashCode() + => Operation.GetHashCode(); } public class IRUnaryConsumer : IRInstruction, ISingleOperandInstruction { @@ -227,6 +258,12 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => Operation.ToString(); + public override bool Equals(object obj) + => obj is IRUnaryConsumer unaryConsumer && + Operation.GetType() == unaryConsumer.Operation.GetType() && + Operand.Equals(unaryConsumer.Operand); + public override int GetHashCode() + => Operation.GetHashCode(); } public class IRPop : IRInstruction, ISingleOperandInstruction { @@ -246,6 +283,10 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => "{pop}"; + public override bool Equals(object obj) + => obj is IRPop pop && Value.Equals(pop.Value); + public override int GetHashCode() + => Value.GetHashCode(); } public class IRNonVarPush : IRInstruction, IResultingInstruction { @@ -266,6 +307,11 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => Operation.ToString(); + public override bool Equals(object obj) + => obj is IRNonVarPush instruction && + Operation.GetType() == instruction.Operation.GetType(); + public override int GetHashCode() + => Operation.GetHashCode(); } public class IRSuffixGet : IRInteractsInstruction, IResultingInstruction, ISingleOperandInstruction { @@ -287,6 +333,13 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{{gmb \"{0}\"}}", Suffix); + public override bool Equals(object obj) + => obj is IRSuffixGet suffixGet && + !(suffixGet is IRSuffixGetMethod) && + string.Equals(Suffix, suffixGet.Suffix, System.StringComparison.OrdinalIgnoreCase) && + Object == suffixGet.Object; + public override int GetHashCode() + => (Object, Suffix).GetHashCode(); } public class IRSuffixGetMethod : IRSuffixGet { @@ -300,6 +353,12 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{{gmet \"{0}\"}}", Suffix); + public override bool Equals(object obj) + => obj is IRSuffixGetMethod suffixGet && + string.Equals(Suffix, suffixGet.Suffix, System.StringComparison.OrdinalIgnoreCase) && + Object == suffixGet.Object; + public override int GetHashCode() + => (Object, Suffix).GetHashCode(); } public class IRSuffixSet : IRInteractsInstruction, IMultipleOperandInstruction { @@ -338,6 +397,13 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{{smb \"{0}\"}}", Suffix); + public override bool Equals(object obj) + => obj is IRSuffixSet suffixSet && + string.Equals(Suffix, suffixSet.Suffix, System.StringComparison.OrdinalIgnoreCase) && + Object.Equals(suffixSet.Object) && + Value.Equals(suffixSet.Value); + public override int GetHashCode() + => (Object, Suffix).GetHashCode(); } public class IRIndexGet : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { @@ -376,6 +442,12 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => "{gidx}"; + public override bool Equals(object obj) + => obj is IRIndexGet indexGet && + Object.Equals(indexGet.Object) && + Index.Equals(indexGet.Index); + public override int GetHashCode() + => Object.GetHashCode(); } public class IRIndexSet : IRInstruction, IMultipleOperandInstruction { @@ -418,6 +490,13 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => "{sidx}"; + public override bool Equals(object obj) + => obj is IRIndexSet indexGet && + Object.Equals(indexGet.Object) && + Index.Equals(indexGet.Index) && + Value.Equals(indexGet.Value); + public override int GetHashCode() + => Object.GetHashCode(); } public class IRJump : IRInstruction { @@ -435,6 +514,11 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{{jump {0}}}", Target.Label); + public override bool Equals(object obj) + => obj is IRJump jump && + Target == jump.Target; + public override int GetHashCode() + => Target.GetHashCode(); } public class IRJumpStack : IRInstruction, ISingleOperandInstruction { @@ -453,6 +537,12 @@ internal override IEnumerable EmitOpcode() yield return opcode; yield return SetSourceLocation(new OpcodeJumpStack()); } + public override bool Equals(object obj) + => obj is IRJumpStack jumpStack && + Distance == jumpStack.Distance && + Targets.SequenceEqual(jumpStack.Targets); + public override int GetHashCode() + => Targets.GetHashCode(); } public class IRBranch : IRInstruction, ISingleOperandInstruction { @@ -486,6 +576,13 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{{br.? {0}/{1}}}", True.Label, False.Label); + public override bool Equals(object obj) + => obj is IRBranch branch && + Condition.Equals(branch.Condition) && + True == branch.True && + False == branch.False; + public override int GetHashCode() + => True.GetHashCode() ^ False.GetHashCode(); } public class IRCall : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { @@ -682,6 +779,12 @@ public IRCall(IRTemp target, OpcodeCall opcode, bool emitArgMarker, params IRVal } public override string ToString() => string.Format("{{call {0}({1})}}", Function.Trim('(', ')'), string.Join(",", Arguments.Select(a => a.ToString()))); + public override bool Equals(object obj) + => obj is IRCall call && + string.Equals(Function.Replace("()", ""), call.Function.Replace("()", ""), System.StringComparison.OrdinalIgnoreCase) && + Arguments.SequenceEqual(call.Arguments); + public override int GetHashCode() + => Function.ToLower().GetHashCode(); } public class IRReturn : IRInstruction, ISingleOperandInstruction { @@ -702,5 +805,11 @@ internal override IEnumerable EmitOpcode() } public override string ToString() => string.Format("{return {0} deep}", Depth); + public override bool Equals(object obj) + => obj is IRReturn ret && + Depth == ret.Depth && + Value.Equals(ret.Value); + public override int GetHashCode() + => base.GetHashCode(); } } diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index 2645fd4ba..dd5b85661 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -34,6 +34,10 @@ internal override IEnumerable EmitPush() SourceColumn = sourceColumn }; } + public override bool Equals(object obj) + => Value.Equals(obj); + public override int GetHashCode() + => Value.GetHashCode(); public override string ToString() => Value.ToString(); } @@ -42,6 +46,7 @@ public abstract class IRVariableBase : IRValue public string Name { get; } public IRVariableBase(string name) => Name = name; + public BasicBlock Scope { get; } } public class IRVariable : IRVariableBase { @@ -65,6 +70,12 @@ internal override IEnumerable EmitPush() } public override string ToString() => Name; + public override bool Equals(object obj) + => obj is IRVariable variable && + Scope == variable.Scope && + string.Equals(Name, variable.Name, System.StringComparison.OrdinalIgnoreCase); + public override int GetHashCode() + => Name.ToLower().GetHashCode(); } public class IRRelocateLater : IRConstant { @@ -129,6 +140,28 @@ internal override IEnumerable EmitPush() foreach (Opcode opcode in Parent.EmitOpcode()) yield return opcode; } + public override bool Equals(object obj) + { + if (obj is IRTemp temp) + { + /*if (isPromoted) + { + return temp.isPromoted && + Scope == temp.Scope && + string.Equals(Name, temp.Name, System.StringComparison.OrdinalIgnoreCase); + }*/ + return Parent.Equals(temp.Parent); + } + if (obj is IRVariable variable) + { + return isPromoted && + Scope == variable.Scope && + string.Equals(Name, variable.Name, System.StringComparison.OrdinalIgnoreCase); + } + return false; + } + public override int GetHashCode() + => Name.ToLower().GetHashCode(); } public class IRParameter : IRValue { From c86d30833e70e7a5f6f6ec05f5abdfbb43fd3d0e Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 22:39:00 -0400 Subject: [PATCH 34/56] Fix binary instructions losing appropriate commutative status when their operation is changed. --- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 22 ++++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index 63bb51394..d6b74f5de 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -116,6 +117,17 @@ public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperand public IRValue Right { get; set; } public IEnumerable Operands { get { yield return Left; yield return Right; } } public int OperandCount => 2; + private static Type[] commutativeTypes = + { + typeof(OpcodeCompareEqual), + typeof(OpcodeCompareNE), + typeof(OpcodeCompareGT), + typeof(OpcodeCompareLT), + typeof(OpcodeCompareGTE), + typeof(OpcodeCompareLTE), + typeof(OpcodeMathAdd), + typeof(OpcodeMathMultiply) + }; IRValue IMultipleOperandInstruction.this[int index] { get => index == 0 ? Left : index == 1 ? Right : throw new System.ArgumentOutOfRangeException(); @@ -129,7 +141,7 @@ IRValue IMultipleOperandInstruction.this[int index] throw new System.ArgumentOutOfRangeException(); } } - public bool IsCommutative { get; } + public bool IsCommutative => commutativeTypes.Contains(Operation.GetType()); public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue right) : base(operation) { @@ -137,14 +149,6 @@ public IRBinaryOp(IRTemp result, BinaryOpcode operation, IRValue left, IRValue r Operation = operation; Left = left; Right = right; - IsCommutative = (operation is OpcodeCompareEqual) || - (operation is OpcodeCompareNE) || - (operation is OpcodeCompareGT) || - (operation is OpcodeCompareLT) || - (operation is OpcodeCompareGTE) || - (operation is OpcodeCompareLTE) || - (operation is OpcodeMathAdd) || - (operation is OpcodeMathMultiply); } public void SwapOperands() { From 852ece87841faa9ddb8eb021f3b8bc766c3ff0ad Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 22:40:10 -0400 Subject: [PATCH 35/56] Add an argument marker to the stack before running a unit test program. This fixes programs with parameters breaking when they check for an argument marker. --- src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs b/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs index 67b9db229..1731e16ed 100644 --- a/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs +++ b/src/kOS.Safe.Test/Execution/BaseIntegrationTest.cs @@ -94,6 +94,7 @@ protected void RunScript(string fileName) screen.ClearOutput(); + cpu.PushArgumentStack(new KOSArgMarkerType()); cpu.GetCurrentContext().AddParts(compiled); } From 73dee514a4f9297e0dbd554701facb87678b599b Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 22:40:41 -0400 Subject: [PATCH 36/56] Force clobber builtins as necessary. --- kerboscript_tests/integration/suffixReplacement.ks | 1 + 1 file changed, 1 insertion(+) diff --git a/kerboscript_tests/integration/suffixReplacement.ks b/kerboscript_tests/integration/suffixReplacement.ks index b1cde7f96..2e93f6cd1 100644 --- a/kerboscript_tests/integration/suffixReplacement.ks +++ b/kerboscript_tests/integration/suffixReplacement.ks @@ -1,3 +1,4 @@ +@CLOBBERBUILTINS OFF. set airspeed to 100. // Clobber the built-in so that this value is pulled after replacement. print(CONSTANT:g0). print(CONSTANT():pi). From 60624a617f148b8337a5530344b834c0faf98266 Mon Sep 17 00:00:00 2001 From: DBooots Date: Wed, 11 Mar 2026 22:42:23 -0400 Subject: [PATCH 37/56] Add a peephole optimization pass. This pass makes various small optimizations, as well as larger algebraic simplifications. See the list in OptimizationLevel.cs and the complete set of algebraic simplifications in PeepholeOptimizations.cs. Includes a unit test for these operations. --- .../integration/peepholeOptimizations.ks | 51 ++ .../Execution/OptimizationTest.cs | 32 ++ .../Optimization/OptimizationLevel.cs | 6 +- .../Passes/PeepholeOptimizations.cs | 445 ++++++++++++++++++ 4 files changed, 531 insertions(+), 3 deletions(-) create mode 100644 kerboscript_tests/integration/peepholeOptimizations.ks create mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs diff --git a/kerboscript_tests/integration/peepholeOptimizations.ks b/kerboscript_tests/integration/peepholeOptimizations.ks new file mode 100644 index 000000000..b6ef105a9 --- /dev/null +++ b/kerboscript_tests/integration/peepholeOptimizations.ks @@ -0,0 +1,51 @@ +parameter a is TRUE. +parameter c is 3. +parameter x is 2. +parameter z is 5. + +print("test":typename()). +// v() is unavailable in unit testing. +//local v1 is v(1,2,3). +//local v2 is v(3,4,5). +// Multiplying scalars will work equally well. +local v1 is 2. +local v2 is 3. + +print(vectorDotProduct(v1,v2)). // =6 +print(vdot(v1,v2)). // =6 + +if not a { + print("Don't print this."). +} + +if not (not a) { + print("Print this."). +} + +print(c*x+c*z). // = 21 +print(c*x+z*c). // = 21 +print(x*c+c*z). // = 21 +print(x*c+z*c). // = 21 + +print(-x+z). // = 3 +print(x+-z). // = -3 + +print(x--z). // = 7 + +print(x^z/x). // = 16 +print(z^4/z). // = 125 + +print(x^z*x). // = 64 +print(z^4*z). // = 3125 + +print(x^z*x^c). // = 256 + +print(x^z/x^c). // = 4 + +print(z^3/z). // = 25 + +print(z*z*z). // = 125 + +print(z^3/z*z). // = 125 + +print(x^2*x^z/x*x^3). // = 512 \ No newline at end of file diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index 141399853..d35a9783e 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -146,5 +146,37 @@ public void TestSuffixReplacement() "100" ); } + + + [Test] + public void TestPeepholeOptimizations() + { + // Test that certain suffixes are replaced + RunScript("integration/peepholeOptimizations.ks"); + RunSingleStep(); + AssertOutput( + "String", + "6", + "6", + "Print this.", + "21", + "21", + "21", + "21", + "3", + "-3", + "7", + "16", + "125", + "64", + "3125", + "256", + "4", + "25", + "125", + "125", + "512" + ); + } } } diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index 8f7210e7b..35e7fe161 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -9,16 +9,16 @@ namespace kOS.Safe.Compilation * Replace ship fields with their alias * 30. Constant folding * 50. Dead code elimination - * - * 20. Constant propagation * 1050. Peephole optimizations: * Replace lex indexing with string constant with suffixing where possible * Replace parameterless suffix method calls with get member (this feels like cheating...) * Replace calls to VectorDotProduct with multiplication * Replace --X with X * Replace !!X with X - * Branch logical simplification (e.g. !A branch = A branch!) + * Branch logical simplification (e.g. !X branch = X branch!) * Algebraic simplification (e.g. A*B+A*C = A*(B+C), A+-B=A-B, A--B=A+B, -A+B=B-A, X*X*...*X=N^X) + * + * 20. Constant propagation * O2: * 1000. Common expression elimination * Particularly: Any expression of 3 opcodes used more than twice, diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs new file mode 100644 index 000000000..ebda09f71 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs @@ -0,0 +1,445 @@ +using System; +using System.Collections.Generic; +using kOS.Safe.Compilation.IR; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class PeepholeOptimizations : IOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + + public short SortIndex => 1050; + + // TODO: Replace lex indexing with string constant with suffixing where possible. + public void ApplyPass(List code) + { + foreach (IRInstruction instruction in code.DepthFirst()) + { + PeepholeFilter(instruction); + } + } + + private static void PeepholeFilter(IRInstruction instruction) + { + // Replace lex indexing using string constant with suffixing where possible + // TODO: this is harder since it requires knowing the object type being indexed against. + + // Replace parameterless suffix method calls with get member + if (instruction is IRCall suffixCall && + !suffixCall.Direct && + suffixCall.Arguments.Count == 0 && + suffixCall.IndirectMethod is IRTemp tempSuffixCall && + tempSuffixCall.Parent is IRSuffixGetMethod) + { + ReplaceParameterlessSuffix(suffixCall); + return; + } + // Replace calls to VectorDotProduct with multiplication + if (instruction is IRCall vDotCall && + (vDotCall.Function == "vdot" || vDotCall.Function == "vectordotproduct") && + vDotCall.Arguments.Count == 2) + { + ReplaceVectorDotProduct(vDotCall); + return; + } + // Algebraic simplifications + if (instruction is IRBinaryOp binaryOp) + { + IRTemp tempL = binaryOp.Left as IRTemp; + IRTemp tempR = binaryOp.Right as IRTemp; + switch (binaryOp.Operation) + { + case OpcodeMathAdd _: + { + // A*B + A*C = A*(B+C) + { + if (tempL != null && + tempL.Parent is IRBinaryOp opL && + tempR != null && + tempR.Parent is IRBinaryOp opR && + opL.Operation is OpcodeMathMultiply && + opR.Operation is OpcodeMathMultiply) + { + if (opR.Left == opL.Left) + { + DistributeMultiplication(binaryOp); + return; + } + if (opR.Left == opL.Right) + { + opL.SwapOperands(); + DistributeMultiplication(binaryOp); + return; + } + if (opR.Right == opL.Left) + { + opR.SwapOperands(); + DistributeMultiplication(binaryOp); + return; + } + if (opR.Right == opL.Right) + { + opL.SwapOperands(); + opR.SwapOperands(); + DistributeMultiplication(binaryOp); + return; + } + } + } + // -B+A = A+-B = A-B + { + if (tempR != null && + tempR.Parent is IRUnaryOp opR && + opR.Operation is OpcodeMathNegate) + { + DistributeNegationB(binaryOp); + return; + } + if (tempL != null && + tempL.Parent is IRUnaryOp opL && + opL.Operation is OpcodeMathNegate) + { + binaryOp.SwapOperands(); + DistributeNegationB(binaryOp); + return; + } + } + // -A+B = B-A + { + if (tempL != null && + tempL.Parent is IRUnaryOp opL && + opL.Operation is OpcodeMathNegate) + { + DistributeNegationA(binaryOp); + return; + } + } + } + break; + case OpcodeMathSubtract _: + // A--B=A+B + { + if (tempR != null && + tempR.Parent is IRUnaryOp unaryOp && + unaryOp.Operation is OpcodeMathNegate) + { + ReplaceNegateSubtract(binaryOp); + return; + } + } + break; + case OpcodeMathDivide _: + if (binaryOp.Right is IRConstant constantR && + Encapsulation.ScalarIntValue.Zero.Equals(constantR.Value)) + throw new Exceptions.KOSCompileException(instruction, new DivideByZeroException()); + // X^N/X=X^(N-1) + { + if (tempL != null && + tempL.Parent is IRBinaryOp opL && + opL.Operation is OpcodeMathPower && + opL.Left == binaryOp.Right) + { + IncreasePower(binaryOp, -1); + return; + } + } + // X^N/X^M=X^(N-M) + { + if (tempL != null && + tempL.Parent is IRBinaryOp opL && + opL.Operation is OpcodeMathPower && + tempR != null && + tempR.Parent is IRBinaryOp opR && + opR.Operation is OpcodeMathPower && + opL.Left == opR.Left) + { + DividePowers(binaryOp); + return; + } + } + break; + case OpcodeMathMultiply _: + // X^N*X=X^(N+1) + { + if (tempL != null && + tempL.Parent is IRBinaryOp opL && + opL.Operation is OpcodeMathPower && + opL.Left == binaryOp.Right) + { + IncreasePower(binaryOp, 1); + return; + } + if (tempR != null && + tempR.Parent is IRBinaryOp opR && + opR.Operation is OpcodeMathPower && + opR.Left == binaryOp.Left) + { + binaryOp.SwapOperands(); + IncreasePower(binaryOp, 1); + return; + } + } + // X*X*...*X=N^X + // Start with (X*X)*X = X*(X*X) = X^3 + // Then the previous rule will kick in and exponentiate from there + { + if (tempR != null && + tempR.Parent is IRBinaryOp opR && + opR.Operation is OpcodeMathMultiply && + opR.Left == opR.Right && + opR.Left == binaryOp.Left) + { + CreatePower(binaryOp); + return; + } + if (tempL != null && + tempL.Parent is IRBinaryOp opL && + opL.Operation is OpcodeMathMultiply && + opL.Left == opL.Right && + opL.Left == binaryOp.Right) + { + binaryOp.SwapOperands(); + CreatePower(binaryOp); + return; + } + } + // X^N*X^M=X^(N+M) + { + if (tempL != null && + tempL.Parent is IRBinaryOp opL && + opL.Operation is OpcodeMathPower && + tempR != null && + tempR.Parent is IRBinaryOp opR && + opR.Operation is OpcodeMathPower && + opL.Left == opR.Left) + { + CombinePowers(binaryOp); + return; + } + } + break; + } + } + // Redundant unary operation replacements + // E.g. !!X = X and --X = X + if (instruction is ISingleOperandInstruction singleOperandInstruction) + { + if (singleOperandInstruction.Operand is IRTemp tempOperand && + tempOperand.Parent is IRUnaryOp unaryParent) + { + IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); + if (potentialResult != null) + singleOperandInstruction.Operand = potentialResult; + } + } + else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) + { + for (int i = multipleOperandInstruction.OperandCount - 1; i >= 0; i--) + { + IRValue operand = multipleOperandInstruction[i]; + if (operand is IRTemp tempOperand && + tempOperand.Parent is IRUnaryOp unaryParent) + { + IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); + if (potentialResult != null) + multipleOperandInstruction[i] = potentialResult; + } + } + } + // Branch logical simplification (e.g. !X branch = X branch!) + if (instruction is IRBranch branch && + branch.Condition is IRTemp temp && + temp.Parent is IRUnaryOp negateBranch && + negateBranch.Operation is OpcodeLogicNot) + { + ReplaceRedundantNotBranch(branch); + return; + } + } + + private static void ReplaceParameterlessSuffix(IRCall call) + { + IRSuffixGet suffixMethod = (IRSuffixGet)((IRTemp)call.IndirectMethod).Parent; + ((IRTemp)call.Result).Parent = new IRSuffixGet( + (IRTemp)call.Result, + suffixMethod.Object, + new OpcodeGetMember(suffixMethod.Suffix) + { + SourceLine = suffixMethod.SourceLine, + SourceColumn = suffixMethod.SourceColumn + }); + } + + private static void ReplaceVectorDotProduct(IRCall call) + { + ((IRTemp)call.Result).Parent = new IRBinaryOp( + (IRTemp)call.Result, + new OpcodeMathMultiply() + { + SourceLine = call.SourceLine, + SourceColumn = call.SourceColumn + }, + call.Arguments[0], + call.Arguments[1]); + } + + private static IRValue AttempReplaceRedundantUnaryOp(IRUnaryOp unaryParent) + { + // Replace --X with X + if (CanOperationBeReplaced(unaryParent, typeof(OpcodeMathNegate))) + return GetDoubleNestedValue(unaryParent); + // Replace !!X with X + if (CanOperationBeReplaced(unaryParent, typeof(OpcodeLogicNot))) + return GetDoubleNestedValue(unaryParent); + return null; + } + private static bool CanOperationBeReplaced(IRUnaryOp operation, Type opcodeType) + { + return operation.Operation.GetType() == opcodeType && + operation.Operand is IRTemp operand && + operand.Parent is IRUnaryOp neg2 && + neg2.Operation.GetType() == opcodeType; + } + private static IRValue GetDoubleNestedValue(ISingleOperandInstruction operation) + { + return ((IRUnaryOp)((IRTemp)operation.Operand).Parent).Operand; + } + + private static void ReplaceRedundantNotBranch(IRBranch branch) + { + branch.Condition = GetDoubleNestedValue(branch); + branch.PreferFalse = !branch.PreferFalse; + (branch.True, branch.False) = (branch.False, branch.True); + } + + private static void DistributeMultiplication(IRBinaryOp instruction) + { + // A*B + A*C = A*(B+C) + IRTemp tempL = (IRTemp)instruction.Left; + IRTemp tempR = (IRTemp)instruction.Right; + IRBinaryOp opL = (IRBinaryOp)tempL.Parent; + IRBinaryOp opR = (IRBinaryOp)tempR.Parent; + // Restructure to create the parentheses + opR.Operation = new OpcodeMathAdd(); + opR.Left = opL.Right; + // Restructure the multiplication term. + instruction.Left = opL.Left; + instruction.Operation = new OpcodeMathMultiply(); + } + + private static void DistributeNegationA(IRBinaryOp instruction) + { + // -A+B = B-A + IRTemp tempL = (IRTemp)instruction.Left; + IRUnaryOp opL = (IRUnaryOp)tempL.Parent; + instruction.Left = opL.Operand; + instruction.SwapOperands(); + instruction.Operation = new OpcodeMathSubtract(); + } + + private static void DistributeNegationB(IRBinaryOp instruction) + { + // A+-B = A-B + IRTemp tempR = (IRTemp)instruction.Right; + IRUnaryOp opR = (IRUnaryOp)tempR.Parent; + instruction.Right = opR.Operand; + instruction.Operation = new OpcodeMathSubtract(); + } + + private static void ReplaceNegateSubtract(IRBinaryOp instruction) + { + // A--B=A+B + IRTemp tempR = (IRTemp)instruction.Right; + IRUnaryOp opR = (IRUnaryOp)tempR.Parent; + instruction.Right = opR.Operand; + instruction.Operation = new OpcodeMathAdd(); + } + + private static void IncreasePower(IRBinaryOp instruction, int powerIncrease) + { + // X^N*X=X^(N+1) + // X^N/X=X^(N-1) + // Assumes |N +/- 1| < 2^31 - 1 (for integer scalars) or 2^53 (for double scalars) + // Going beyond that overflows an integer or the mantissa for double. + + IRTemp tempL = (IRTemp)instruction.Left; + IRBinaryOp opL = (IRBinaryOp)tempL.Parent; + + short divLine = instruction.SourceLine; + short divColumn = instruction.SourceColumn; + + IRConstant powConst = new IRConstant(new Encapsulation.ScalarIntValue(powerIncrease), instruction); + + // Set up the new power operation + instruction.Left = opL.Left; + instruction.Operation = new OpcodeMathPower(); + instruction.OverwriteSourceLocation(opL.SourceLine, opL.SourceColumn); + + // Reuse the old power instruction for the addition/subtraction + instruction.Right = tempL; + opL.Left = powConst; + opL.Operation = new OpcodeMathAdd(); + opL.OverwriteSourceLocation(divLine, divColumn); + + // Attempt constant folding afterwards + instruction.Right = ConstantFolding.AttemptReduction(opL); + + // Special case for when N == 2 afterwards + // then revert back to X * X + if (instruction.Right is IRConstant constantR && + Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) + { + instruction.Right = instruction.Left; + instruction.Operation = new OpcodeMathMultiply(); + } + } + + private static void CreatePower(IRBinaryOp instruction) + { + // X*(X*X) = X^3 + instruction.Operation = new OpcodeMathPower(); + IRTemp tempR = (IRTemp)instruction.Right; + instruction.Right = new IRConstant(new Encapsulation.ScalarIntValue(3), tempR.Parent); + } + + private static void CombinePowers(IRBinaryOp instruction) + => CombinePowers(instruction, new OpcodeMathAdd()); + private static void DividePowers(IRBinaryOp instruction) + => CombinePowers(instruction, new OpcodeMathSubtract()); + private static void CombinePowers(IRBinaryOp instruction, BinaryOpcode newOperation) + { + // X^N*X^M=X^(N+M) + // Assumes |N + M| < 2^31 - 1 (for integer scalars) or 2^53 (for double scalars) + + IRTemp tempL = (IRTemp)instruction.Left; + IRTemp tempR = (IRTemp)instruction.Right; + IRBinaryOp opL = (IRBinaryOp)tempL.Parent; + IRBinaryOp opR = (IRBinaryOp)tempR.Parent; + + short divLine = instruction.SourceLine; + short divColumn = instruction.SourceColumn; + + // Set up the new power operation + instruction.Left = opL.Left; + instruction.Operation = new OpcodeMathPower(); + instruction.OverwriteSourceLocation(opL.SourceLine, opL.SourceColumn); + + // Reuse an old power instruction for the addition/subtraction + opR.Left = opL.Right; + opR.Operation = newOperation; + opR.OverwriteSourceLocation(divLine, divColumn); + + // Attempt constant folding afterwards + instruction.Right = ConstantFolding.AttemptReduction(opR); + + // Special case for when N == 2 afterwards + // then revert back to X * X + if (instruction.Right is IRConstant constantR && + Encapsulation.ScalarIntValue.Two.Equals(constantR.Value)) + { + instruction.Right = instruction.Left; + instruction.Operation = new OpcodeMathMultiply(); + } + } + } +} From b6bb80ca63bb20306ae1304c1181c0789c67f936 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 12:17:12 -0400 Subject: [PATCH 38/56] Add property to enumerate which blocks are dominated by a BasicBlock. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index c40ea2d3f..8b6f10aac 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -8,6 +8,8 @@ public class BasicBlock { private readonly HashSet predecessors = new HashSet(); private readonly HashSet successors = new HashSet(); + private BasicBlock dominator; + private readonly HashSet dominates = new HashSet(); private readonly List parameters = new List(); private readonly Dictionary variables = new Dictionary(); private readonly Dictionary externalGlobalVariables = new Dictionary(); @@ -17,11 +19,21 @@ public class BasicBlock public int StartIndex { get; } public int EndIndex { get; } public List Instructions { get; } = new List(); - public IEnumerable Successors => successors; - public IEnumerable Predecessors => predecessors; + public IReadOnlyCollection Successors => successors; + public IReadOnlyCollection Predecessors => predecessors; public string Label => nonSequentialLabel ?? $"@BB#{ID}"; public int ID { get; } - public BasicBlock Dominator { get; protected set; } + public BasicBlock Dominator + { + get => dominator; + protected set + { + dominator?.dominates.Remove(this); + dominator = value; + dominator?.dominates.Add(this); + } + } + public IReadOnlyCollection Dominates => dominates; public ExtendedBasicBlock ExtendedBlock { get; set; } public IRJump FallthroughJump { get; set; } = null; #if DEBUG From ef43c4cffb61c50c3ed096cbabc40321bc752466 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 12:20:31 -0400 Subject: [PATCH 39/56] Implement scope awareness for BasicBlocks. Class IRScope should not be confused with class Scope or class VariableScope... --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 66 ++++++------- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 55 +++++++++-- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 5 - src/kOS.Safe/Compilation/IR/IRScope.cs | 107 ++++++++++++++++++++++ 4 files changed, 181 insertions(+), 52 deletions(-) create mode 100644 src/kOS.Safe/Compilation/IR/IRScope.cs diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 8b6f10aac..9d863b96f 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -11,11 +11,20 @@ public class BasicBlock private BasicBlock dominator; private readonly HashSet dominates = new HashSet(); private readonly List parameters = new List(); - private readonly Dictionary variables = new Dictionary(); - private readonly Dictionary externalGlobalVariables = new Dictionary(); private readonly Stack exitStackState = new Stack(); // Note that this is reversed from the real stack. Just now we don't reverse it four times. private readonly string nonSequentialLabel = null; + private IRScope scope; + public IRScope Scope + { + get => scope; + set + { + scope?.RemoveBlock(this); + scope = value; + scope.EnrollBlock(this); + } + } public int StartIndex { get; } public int EndIndex { get; } public List Instructions { get; } = new List(); @@ -146,46 +155,23 @@ public void AddParameter(IRParameter parameter) { parameters.Add(parameter); } - public void StoreLocalVariable(IRVariable variable) - { - variables[variable.Name] = variable; - } - public void StoreGlobalVariable(IRVariable variable) - { - if (Dominator != null) - { - Dominator.StoreGlobalVariable(variable); - return; - } - externalGlobalVariables[variable.Name] = variable; - } - public void StoreVariable(IRVariable variable) - { - if (!TryStoreVariable(variable)) - StoreGlobalVariable(variable); - } - public bool TryStoreVariable(IRVariable variable) - { - if (variables.ContainsKey(variable.Name)) + public void StoreLocalVariable(IRVariableBase variable) + => Scope.StoreLocalVariable(variable); + public void StoreGlobalVariable(IRVariableBase variable) + => Scope.StoreGlobalVariable(variable); + public void StoreVariable(IRVariableBase variable) + => Scope.StoreVariable(variable); + public bool TryStoreVariable(IRVariableBase variable) + => Scope.TryStoreVariable(variable); + public IRVariableBase PushVariable(string name, Opcode opcode) + { + IRVariableBase result = Scope.GetVariable(name); + if (result == null) { - variables[variable.Name] = variable; - return true; + result = new IRVariable(name, opcode); + Scope.StoreGlobalVariable(result); } - if (Dominator != null) - return Dominator.TryStoreVariable(variable); - return false; - } - public IRVariable PushVariable(string name, Opcode opcode) - { - if (variables.ContainsKey(name)) - return variables[name]; - if (Dominator != null) - return Dominator.PushVariable(name, opcode); - if (externalGlobalVariables.ContainsKey(name)) - return externalGlobalVariables[name]; - IRVariable newExternalGlobal = new IRVariable(name, opcode); - externalGlobalVariables.Add(name, newExternalGlobal); - return newExternalGlobal; + return result; } public void SetStackState(Stack stack) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 3adf5e491..67566095d 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -22,8 +22,11 @@ public List Lower(List code) private void CreateBlocks(List code, Dictionary labels, List blocks) { + IRScope globalScope = new IRScope(null) { IsGlobalScope = true }; SortedSet leaders = new SortedSet() { 0 }; - for (int i = 1; i < code.Count; i++) // The first instruction is always a leader so we can skip 0. + HashSet scopePushes = new HashSet(); + HashSet scopePops = new HashSet(); + for (int i = 0; i < code.Count; i++) { if (code[i] is BranchOpcode branch) { @@ -37,12 +40,22 @@ private void CreateBlocks(List code, Dictionary labels, Lis { throw new NotImplementedException("OpcodeJumpStack is not implemented for optimization because it is non-deterministic. Use OptimizationLevel.None."); } - else if (code[i] is OpcodeReturn) + else if (code[i] is OpcodeReturn ret) + { leaders.Add(i + 1); + if (ret.Depth > 0) + scopePops.Add(i); + } else if (code[i] is OpcodePushScope) + { leaders.Add(i); + scopePushes.Add(i); + } else if (code[i] is OpcodePopScope) + { leaders.Add(i + 1); + scopePops.Add(i); + } } leaders.Add(code.Count); foreach (int startIndex in leaders.Take(leaders.Count - 1)) @@ -52,6 +65,7 @@ private void CreateBlocks(List code, Dictionary labels, Lis if (label.StartsWith("@")) label = null; BasicBlock block = new BasicBlock(startIndex, endIndex, blockID++, label); + blocks.Add(block); } foreach (BasicBlock block in blocks) @@ -74,11 +88,43 @@ private void CreateBlocks(List code, Dictionary labels, Lis block.OriginalOpcodes = code.ToArray(); #endif } + + BasicBlock rootBlock = GetBlockFromStartIndex(blocks, 0); + rootBlock.EstablishDominance(); + + AssignScopes(rootBlock, globalScope, scopePushes, scopePops); } private BasicBlock GetBlockFromStartIndex(List blocks, int startIndex) => blocks.First(b => b.StartIndex == startIndex); + private static void AssignScopes(BasicBlock root, IRScope globalScope, HashSet scopePushIndices, HashSet scopePopIndices) + { + Stack scopeStack = new Stack(); + scopeStack.Push(globalScope); + + void Visit(BasicBlock block) + { + if (scopePushIndices.Contains(block.StartIndex)) + scopeStack.Push( + new IRScope(scopeStack.Peek()) + { + HeaderBlock = block + }); + + block.Scope = scopeStack.Peek(); + + // Return statements can pop multiple scopes + if (scopePopIndices.Contains(block.EndIndex)) + scopeStack.Pop().FooterBlock = block; + + foreach (BasicBlock child in block.Dominates) + Visit(child); + } + + Visit(root); + } + private void FillBlocks(List code, Dictionary labels, List blocks) { Stack stack = new Stack(); @@ -209,11 +255,6 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack userFunctions) RootBlocks.Add(fragment.FunctionCode[0]); } } - - foreach (BasicBlock block in RootBlocks) - { - block.EstablishDominance(); - } } public void EmitCode(CodePart codePart) diff --git a/src/kOS.Safe/Compilation/IR/IRScope.cs b/src/kOS.Safe/Compilation/IR/IRScope.cs new file mode 100644 index 000000000..b95167d1e --- /dev/null +++ b/src/kOS.Safe/Compilation/IR/IRScope.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace kOS.Safe.Compilation.IR +{ + public class IRScope + { + private readonly Dictionary variables = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private IRScope parent; + private readonly HashSet childScopes = new HashSet(); + private readonly HashSet blocks = new HashSet(); + + private int nextChildIndex = 0; + private int index; + + public IRScope ParentScope + { + get => parent; + set + { + parent?.childScopes.Remove(this); + parent = value; + if (parent != null) + { + parent.childScopes.Add(this); + index = parent.nextChildIndex++; + } + else + index = 0; + } + } + public IReadOnlyCollection Children => childScopes; + public IReadOnlyCollection Blocks => blocks; + public BasicBlock HeaderBlock { get; set; } + public BasicBlock FooterBlock { get; set; } + public IReadOnlyCollection Variables => variables.Keys; + public bool IsGlobalScope { get; internal set; } = false; + + public IRScope(IRScope parent) + { + ParentScope = parent; + } + + public void StoreLocalVariable(IRVariableBase variable) + { + variables[variable.Name] = variable; + } + public void StoreGlobalVariable(IRVariableBase variable) + { + if (!IsGlobalScope) + ParentScope.StoreGlobalVariable(variable); + else + StoreLocalVariable(variable); + } + public void StoreVariable(IRVariableBase variable) + { + if (!TryStoreVariable(variable)) + StoreGlobalVariable(variable); + } + public bool TryStoreVariable(IRVariableBase variable) + { + if (variables.ContainsKey(variable.Name)) + { + variables[variable.Name] = variable; + return true; + } + return ParentScope?.TryStoreVariable(variable) ?? false; + } + + public IRVariableBase GetVariable(string name) + { + if (variables.ContainsKey(name)) + return variables[name]; + return ParentScope?.GetVariable(name); + } + + public bool IsVariableInScope(string name) + { + if (variables.ContainsKey(name)) + return true; + return ParentScope?.IsVariableInScope(name) ?? false; + } + + public void EnrollBlock(BasicBlock block) + { + block.Scope?.RemoveBlock(block); + blocks.Add(block); + } + public void RemoveBlock(BasicBlock block) + => blocks.Remove(block); + + public override string ToString() + { + if (IsGlobalScope) + return "IRScope: Global"; + return $"IRScope: {IndexString()}"; + } + private string IndexString() + { + if (ParentScope == null) + return $"{index}"; + return $"{ParentScope.IndexString()}.{index}"; + } + } +} From 9dbea3874ce548e07b8382b38064828bc461f1d9 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 12:21:41 -0400 Subject: [PATCH 40/56] Add pass to remove unnecessary scope pushes and pops. This also removes scope pops that can be replaced by incrementing the return depth. --- .../Optimization/OptimizationLevel.cs | 1 + .../Passes/UnnecessaryScopeRemoval.cs | 117 ++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index 35e7fe161..19e108481 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -17,6 +17,7 @@ namespace kOS.Safe.Compilation * Replace !!X with X * Branch logical simplification (e.g. !X branch = X branch!) * Algebraic simplification (e.g. A*B+A*C = A*(B+C), A+-B=A-B, A--B=A+B, -A+B=B-A, X*X*...*X=N^X) + * 32000. Remove unnecessary scope pushes/pops * * 20. Constant propagation * O2: diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs new file mode 100644 index 000000000..aa95d2579 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Compilation.IR; +using kOS.Safe.Exceptions; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class UnnecessaryScopeRemoval : IOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + + public short SortIndex => 32000; + + public void ApplyPass(List code) + { + HashSet scopes = new HashSet(); + HashSet removedScopes = new HashSet(); + foreach (BasicBlock block in code) + scopes.Add(block.Scope); + + foreach (IRScope scope in scopes) + { + if (scope.Variables.Any()) + continue; + + if (scope.IsGlobalScope) + continue; + + CollapseScope(scope); + + RemovePushScope(scope.HeaderBlock); + + RemovePopScope(scope.FooterBlock); + + removedScopes.Add(scope); + } + + scopes.ExceptWith(removedScopes); + foreach (BasicBlock block in code) + { + if (block.Instructions.Count > 0 && + !(block.Instructions.Last() is IRReturn)) + continue; + + AttemptPopReturnCollapse(block); + } + } + + private static void CollapseScope(IRScope scope) + { + IRScope newScope = scope.ParentScope; + foreach (IRScope childScope in scope.Children) + childScope.ParentScope = newScope; + foreach (BasicBlock block in scope.Blocks.ToArray()) + block.Scope = newScope; + scope.ParentScope = null; + } + + private static void RemovePushScope(BasicBlock header) + { + if (header.Instructions[0] is IRNoStackInstruction instruction && + instruction.Operation is OpcodePushScope) + header.Instructions.RemoveAt(0); + else + throw new KOSYouShouldNeverSeeThisException($"The first instruction ({header.Instructions[0]}) in scope header block {header} was not OpcodePushScope."); + } + + private static void RemovePopScope(BasicBlock footer) + { + int lastIndex = footer.Instructions.Count - 1; + IRInstruction lastInstruction = footer.Instructions[lastIndex]; + + if (lastInstruction is IRReturn ret) + { + ret.Depth -= 1; + if (ret.Depth < 0) + throw new KOSYouShouldNeverSeeThisException($"After removing unecessary scopes, the return statement in {footer} had negative depth."); + } + else if (lastInstruction is IRNoStackInstruction popInstruction && + popInstruction.Operation is OpcodePopScope) + { + footer.Instructions.RemoveAt(lastIndex); + } + else + throw new KOSYouShouldNeverSeeThisException($"The last instruction ({lastInstruction}) in scope footer block {footer} was not OpcodePopScope or OpcodeReturn."); + } + + private static void AttemptPopReturnCollapse(BasicBlock returnBlock) + { + // Don't do anything if there is more than one predecessor + int numPredecessors = returnBlock.Predecessors.Count; + if (numPredecessors == 0 || numPredecessors > 1) + return; + + BasicBlock predecessorBlock = returnBlock.Predecessors.First(); + + if (predecessorBlock.Instructions.Last() is IRNoStackInstruction popInstruction && + popInstruction.Operation is OpcodePopScope) + { + predecessorBlock.Instructions.RemoveAt(predecessorBlock.Instructions.Count - 1); + ((IRReturn)returnBlock.Instructions.Last()).Depth += 1; + returnBlock.Scope = predecessorBlock.Scope; + returnBlock.Scope.FooterBlock = returnBlock; + //BasicBlock.Merge(predecessorBlock, returnBlock); + + // Since a pop closes a BasicBlock, then if there are no instructions remaining, + // there's a chance that the pop's predecessor also ends with a pop that we can remove, + // if the pop only has a single predecessor. + if (predecessorBlock.Instructions.Count == 0) + { + AttemptPopReturnCollapse(returnBlock); + } + } + } + } +} From dfaa8ad6afa5b7598d011775a2049e524e83d878 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 12:39:12 -0400 Subject: [PATCH 41/56] Implement scope awareness into IRVariableBase so that variables are compared not just by their string name, but also by their scope. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 5 +++- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 14 +++++++--- src/kOS.Safe/Compilation/IR/IRScope.cs | 16 +++++++++++ src/kOS.Safe/Compilation/IR/IRValue.cs | 28 +++++++++++++------ .../Optimization/Passes/SuffixReplacement.cs | 2 +- 5 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 9d863b96f..651d507ca 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -165,14 +165,17 @@ public bool TryStoreVariable(IRVariableBase variable) => Scope.TryStoreVariable(variable); public IRVariableBase PushVariable(string name, Opcode opcode) { + IRScope globalScope = Scope.GetGlobalScope(); IRVariableBase result = Scope.GetVariable(name); if (result == null) { - result = new IRVariable(name, opcode); + result = new IRVariable(name, globalScope, opcode); Scope.StoreGlobalVariable(result); } return result; } + public IRScope GetScopeForVariableNamed(string name) + => Scope.GetScopeForVariableNamed(name); public void SetStackState(Stack stack) { diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index 67566095d..f1b119f90 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -162,23 +162,29 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack Name = name; - public BasicBlock Scope { get; } + public virtual IRScope Scope { get; protected set; } + public IRVariableBase(string name, IRScope declaringScope) + { + Name = name; + Scope = declaringScope; + } } public class IRVariable : IRVariableBase { protected readonly short sourceLine, sourceColumn; public bool IsLock { get; } - public IRVariable(string name, IRInstruction instruction, bool isLock = false) : this(name, instruction.SourceLine, instruction.SourceColumn, isLock) { } - public IRVariable(string name, Opcode opcode, bool isLock = false) : this(name, opcode.SourceLine, opcode.SourceColumn, isLock) { } - public IRVariable(string name, short sourceLine, short sourceColumn, bool isLock = false) : base(name) + public override IRScope Scope { get => base.Scope; } + public IRVariable(string name, IRScope scope, IRInstruction instruction, bool isLock = false) : + this(name, scope, instruction.SourceLine, instruction.SourceColumn, isLock) { } + public IRVariable(string name, IRScope scope, Opcode opcode, bool isLock = false) : + this(name, scope, opcode.SourceLine, opcode.SourceColumn, isLock) { } + public IRVariable(string name, IRScope scope, short sourceLine, short sourceColumn, bool isLock = false) : + base(name, scope) { IsLock = isLock; this.sourceLine = sourceLine; @@ -108,17 +115,20 @@ internal override IEnumerable EmitPush() } public class IRTemp : IRVariableBase { + private bool isPromoted = false; + public int ID { get; } public IRInstruction Parent { get; internal set; } - private bool isPromoted = false; - public IRTemp(int id) : base($"$.temp.{id}") + public IRTemp(int id) : base($"$.temp.{id}", null) { ID = id; + Scope = null; } - public IRAssign PromoteToVariable() + public IRAssign PromoteToVariable(IRScope scope) { isPromoted = true; + Scope = scope; return new IRAssign(new OpcodeStoreLocal(Name) { SourceLine = -1, diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs index 5922a972b..14feaa9f5 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/SuffixReplacement.cs @@ -101,7 +101,7 @@ private static IRValue AttempReplaceSuffix(IRSuffixGet suffixGet) return suffixGet.Result; } // Instead of the IRTemp, which leads to resolving the suffix, return just the alias shortcut. - suffixGet.Result = new IRVariable($"${suffixGet.Suffix}", suffixGet); + suffixGet.Result = new IRVariable($"${suffixGet.Suffix}", null, suffixGet); return suffixGet.Result; } if (objVariable.Name == "$constant") From bd241434d8cbbe83351710d404ec2fc7e8d8d1da Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 13:53:11 -0400 Subject: [PATCH 42/56] Finish peephole optimization pass by implementing lex indexing shortcuts using suffixing wherever possible. --- .../Optimization/OptimizationTools.cs | 2 +- .../Passes/PeepholeOptimizations.cs | 102 ++++++++++++++---- 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs index 3a17a091e..0c241d449 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationTools.cs @@ -96,7 +96,7 @@ private static IEnumerable InstructionMatchBreadthFirst(IRInstruc yield return match; } - private static IEnumerable DepthFirst(IRInstruction instruction) + public static IEnumerable DepthFirst(this IRInstruction instruction) { if (instruction is ISingleOperandInstruction singleOperandInstruction) { diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs index ebda09f71..fbe4ae204 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/PeepholeOptimizations.cs @@ -10,21 +10,28 @@ public class PeepholeOptimizations : IOptimizationPass public short SortIndex => 1050; - // TODO: Replace lex indexing with string constant with suffixing where possible. public void ApplyPass(List code) { - foreach (IRInstruction instruction in code.DepthFirst()) + for (int i = 0; i < code.Count; i++) { - PeepholeFilter(instruction); + IRInstruction instruction = code[i]; + foreach (IRInstruction nestedInstruction in instruction.DepthFirst()) + PeepholeFilter(nestedInstruction); + + // Replace lex indexing with string constant with suffixing where possible. + if (instruction is IRIndexSet indexSet) + { + IRInstruction potentialResult = AttemptReplaceIndexSetWithSuffixSet(indexSet); + if (potentialResult != null) + code[i] = potentialResult; + } } } private static void PeepholeFilter(IRInstruction instruction) { - // Replace lex indexing using string constant with suffixing where possible - // TODO: this is harder since it requires knowing the object type being indexed against. - // Replace parameterless suffix method calls with get member + // TODO: Skip this if clobber built-ins is active. if (instruction is IRCall suffixCall && !suffixCall.Direct && suffixCall.Arguments.Count == 0 && @@ -35,6 +42,7 @@ suffixCall.IndirectMethod is IRTemp tempSuffixCall && return; } // Replace calls to VectorDotProduct with multiplication + // TODO: Skip this if clobber built-ins is active. if (instruction is IRCall vDotCall && (vDotCall.Function == "vdot" || vDotCall.Function == "vectordotproduct") && vDotCall.Arguments.Count == 2) @@ -220,16 +228,29 @@ opR.Operation is OpcodeMathPower && break; } } - // Redundant unary operation replacements - // E.g. !!X = X and --X = X + if (instruction is ISingleOperandInstruction singleOperandInstruction) { - if (singleOperandInstruction.Operand is IRTemp tempOperand && - tempOperand.Parent is IRUnaryOp unaryParent) + if (singleOperandInstruction.Operand is IRTemp tempOperand) { - IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); - if (potentialResult != null) - singleOperandInstruction.Operand = potentialResult; + // Redundant unary operation replacements + // E.g. !!X = X and --X = X + if (tempOperand.Parent is IRUnaryOp unaryParent) + { + IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); + if (potentialResult != null) + singleOperandInstruction.Operand = potentialResult; + } + // Replace lex indexing using string constant with suffixing where possible + if (tempOperand.Parent is IRIndexGet indexGet && + indexGet.Index is IRConstant indexConstant && + (indexConstant.Value is string || + indexConstant.Value is Encapsulation.StringValue)) + { + IRInstruction potentialResult = AttemptReplaceIndexGetWithSuffixGet(indexGet); + if (potentialResult != null) + tempOperand.Parent = potentialResult; + } } } else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) @@ -237,12 +258,23 @@ opR.Operation is OpcodeMathPower && for (int i = multipleOperandInstruction.OperandCount - 1; i >= 0; i--) { IRValue operand = multipleOperandInstruction[i]; - if (operand is IRTemp tempOperand && - tempOperand.Parent is IRUnaryOp unaryParent) + if (operand is IRTemp tempOperand) { - IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); - if (potentialResult != null) - multipleOperandInstruction[i] = potentialResult; + // Redundant unary operation replacements + // E.g. !!X = X and --X = X + if (tempOperand.Parent is IRUnaryOp unaryParent) + { + IRValue potentialResult = AttempReplaceRedundantUnaryOp(unaryParent); + if (potentialResult != null) + multipleOperandInstruction[i] = potentialResult; + } + // Replace lex indexing using string constant with suffixing where possible + if (tempOperand.Parent is IRIndexGet indexGet) + { + IRInstruction potentialResult = AttemptReplaceIndexGetWithSuffixGet(indexGet); + if (potentialResult != null) + tempOperand.Parent = potentialResult; + } } } } @@ -305,6 +337,40 @@ private static IRValue GetDoubleNestedValue(ISingleOperandInstruction operation) return ((IRUnaryOp)((IRTemp)operation.Operand).Parent).Operand; } + private static IRInstruction AttemptReplaceIndexGetWithSuffixGet(IRIndexGet indexGet) + { + if (indexGet.Index is IRConstant indexConstant && + indexConstant.Value is Encapsulation.StringValue stringIndex && + StringUtil.IsValidIdentifier(stringIndex)) + { + return new IRSuffixGet((IRTemp)indexGet.Result, + indexGet.Object, + new OpcodeGetMember(stringIndex) + { + SourceLine = indexGet.SourceLine, + SourceColumn = indexGet.SourceColumn + }); + } + return null; + } + + private static IRInstruction AttemptReplaceIndexSetWithSuffixSet(IRIndexSet indexSet) + { + if (indexSet.Index is IRConstant indexConstant && + indexConstant.Value is Encapsulation.StringValue stringIndex && + StringUtil.IsValidIdentifier(stringIndex)) + { + return new IRSuffixSet(indexSet.Object, + indexSet.Value, + new OpcodeSetMember(stringIndex) + { + SourceLine = indexSet.SourceLine, + SourceColumn = indexSet.SourceColumn + }); + } + return null; + } + private static void ReplaceRedundantNotBranch(IRBranch branch) { branch.Condition = GetDoubleNestedValue(branch); From 281d3ae15269cde6979b4828fd52c9fb875accd6 Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 13:53:49 -0400 Subject: [PATCH 43/56] Clean up and restructure IRCodePart and its child classes. --- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 26 +++++++++++-------- .../Passes/DeadCodeElimination.cs | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index a1877f8af..1623ed31d 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -31,7 +31,7 @@ public IRCodePart(CodePart codePart, List userFunctions) Blocks.AddRange(function.InitializationCode); if (function.InitializationCode.Count > 0) RootBlocks.Add(function.InitializationCode[0]); - foreach (IRFunction.IRFunctionFragment fragment in function.Fragments.Values) + foreach (IRFunction.IRFunctionFragment fragment in function.Fragments) { Blocks.AddRange(fragment.FunctionCode); if (fragment.FunctionCode.Count > 0) @@ -53,35 +53,39 @@ public void EmitCode(CodePart codePart) public class IRFunction { private readonly UserFunction function; + private readonly List userFunctionFragments; + + public string Identifier => function.Identifier; + public List InitializationCode { get; set; } + private readonly Dictionary fragments = new Dictionary(); + public IReadOnlyCollection Fragments => fragments.Values; + public IRFunction(IRBuilder builder, UserFunction function) { this.function = function; InitializationCode = builder.Lower(function.InitializationCode); - fragments = function.PeekNewCodeFragments().ToList(); - foreach (UserFunctionCodeFragment fragment in fragments) + userFunctionFragments = function.PeekNewCodeFragments().ToList(); + foreach (UserFunctionCodeFragment fragment in userFunctionFragments) { - Fragments.Add(fragment, new IRFunctionFragment(builder, fragment)); + fragments.Add(fragment, new IRFunctionFragment(builder, fragment)); } - fragments.Reverse(); + userFunctionFragments.Reverse(); } public void EmitCode(IREmitter emitter) { function.InitializationCode.Clear(); function.InitializationCode.AddRange(emitter.Emit(InitializationCode)); - foreach (UserFunctionCodeFragment fragment in fragments) + foreach (UserFunctionCodeFragment fragment in userFunctionFragments) { - Fragments[fragment].EmitCode(emitter); + fragments[fragment].EmitCode(emitter); } } - public List InitializationCode { get; set; } - public Dictionary Fragments { get; } = new Dictionary(); - private readonly List fragments; public class IRFunctionFragment { - public List FunctionCode { get; set; } private readonly UserFunctionCodeFragment fragment; + public List FunctionCode { get; set; } public IRFunctionFragment(IRBuilder builder, UserFunctionCodeFragment codeFragment) { fragment = codeFragment; diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs b/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs index 02fbe38f8..5e6255cf3 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/DeadCodeElimination.cs @@ -18,7 +18,7 @@ public void ApplyPass(IRCodePart code) foreach (IRCodePart.IRFunction function in code.Functions) { RemoveDeadBlocks(function.InitializationCode, rootBlocks); - foreach (IRCodePart.IRFunction.IRFunctionFragment fragment in function.Fragments.Values) + foreach (IRCodePart.IRFunction.IRFunctionFragment fragment in function.Fragments) { RemoveDeadBlocks(fragment.FunctionCode, rootBlocks); } From 5040e4643eedb3c1a34e5ec6d58b6acb6d98c5fe Mon Sep 17 00:00:00 2001 From: DBooots Date: Thu, 12 Mar 2026 20:12:55 -0400 Subject: [PATCH 44/56] Fix opcodebranch where the jump is relative instead of a label. Fix exceptions when storing a variable defined globally in another scope. --- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index f1b119f90..a1e87aa6f 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -169,7 +169,9 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack Date: Thu, 12 Mar 2026 20:13:55 -0400 Subject: [PATCH 45/56] Fix IRCodePart behaviour for triggers. It turns out they aren't just special functions! --- src/kOS.Safe/Compilation/IR/IRCodePart.cs | 32 ++++++++++++++++++- src/kOS.Safe/Compilation/KS/Compiler.cs | 2 +- .../Compilation/KS/TriggerCollection.cs | 3 ++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRCodePart.cs b/src/kOS.Safe/Compilation/IR/IRCodePart.cs index 1623ed31d..515a2a423 100644 --- a/src/kOS.Safe/Compilation/IR/IRCodePart.cs +++ b/src/kOS.Safe/Compilation/IR/IRCodePart.cs @@ -9,10 +9,11 @@ public class IRCodePart { public List MainCode { get; set; } public List Functions { get; set; } + public List Triggers { get; set; } public List RootBlocks { get; } = new List(); public List Blocks { get; } = new List(); - public IRCodePart(CodePart codePart, List userFunctions) + public IRCodePart(CodePart codePart, List userFunctions, List triggers) { if (codePart.InitializationCode.Count > 0) throw new ArgumentException($"{nameof(codePart)} has initialization code and is structured unexpectedly."); @@ -22,10 +23,17 @@ public IRCodePart(CodePart codePart, List userFunctions) IRBuilder builder = new IRBuilder(); MainCode = builder.Lower(codePart.MainCode); Functions = userFunctions.Select(f => new IRFunction(builder, f)).ToList(); + Triggers = triggers.Select(t => new IRTrigger(builder, t)).ToList(); Blocks.AddRange(MainCode); if (MainCode.Count > 0) RootBlocks.Add(MainCode[0]); + foreach (IRTrigger trigger in Triggers) + { + Blocks.AddRange(trigger.Code); + if (trigger.Code.Count > 0) + RootBlocks.Add(trigger.Code[0]); + } foreach (IRFunction function in Functions) { Blocks.AddRange(function.InitializationCode); @@ -43,6 +51,10 @@ public IRCodePart(CodePart codePart, List userFunctions) public void EmitCode(CodePart codePart) { IREmitter emitter = new IREmitter(); + foreach (IRTrigger trigger in Triggers) + { + trigger.EmitCode(emitter); + } foreach (IRFunction function in Functions) { function.EmitCode(emitter); @@ -50,6 +62,24 @@ public void EmitCode(CodePart codePart) codePart.MainCode = emitter.Emit(MainCode); } + public class IRTrigger + { + private readonly Trigger trigger; + public string Identifier { get; } + public List Code { get; set; } + public IRTrigger(IRBuilder builder, Trigger trigger) + { + this.trigger = trigger; + Identifier = trigger.Code.FirstOrDefault()?.Label ?? ""; + Code = builder.Lower(trigger.Code); + } + public void EmitCode(IREmitter emitter) + { + trigger.Code.Clear(); + trigger.Code.AddRange(emitter.Emit(Code)); + } + } + public class IRFunction { private readonly UserFunction function; diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index 0df55690b..564fd51c1 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -212,7 +212,7 @@ public CodePart Compile(int startLineNum, ParseTree tree, Context context, Compi public static void Optimize(CodePart code, Context context, CompilerOptions options) { - IR.IRCodePart irCodePart = new IR.IRCodePart(code, context.UserFunctions.PeekNewFunctions()); + IR.IRCodePart irCodePart = new IR.IRCodePart(code, context.UserFunctions.PeekNewFunctions(), context.Triggers.PeekNewParts()); Optimization.Optimizer optimizer = new Optimization.Optimizer(options.OptimizationLevel); optimizer.Optimize(irCodePart); irCodePart.EmitCode(code); diff --git a/src/kOS.Safe/Compilation/KS/TriggerCollection.cs b/src/kOS.Safe/Compilation/KS/TriggerCollection.cs index fb9de153b..82181e396 100644 --- a/src/kOS.Safe/Compilation/KS/TriggerCollection.cs +++ b/src/kOS.Safe/Compilation/KS/TriggerCollection.cs @@ -35,6 +35,9 @@ public List GetParts() return GetParts(triggers.Values.ToList()); } + + internal List PeekNewParts() + => newTriggers; public IEnumerable GetNewParts() { List parts = GetParts(newTriggers); From 04ca8ce033841b8a25c326ce00533a02fd32ab06 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 13 Mar 2026 16:47:25 -0400 Subject: [PATCH 46/56] Harmonize IRVariableBase and subclass equality, hashing, and string output. --- src/kOS.Safe/Compilation/IR/IRScope.cs | 12 +++++------ src/kOS.Safe/Compilation/IR/IRValue.cs | 29 ++++++++++++++++---------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRScope.cs b/src/kOS.Safe/Compilation/IR/IRScope.cs index 3530a4f23..39f68e738 100644 --- a/src/kOS.Safe/Compilation/IR/IRScope.cs +++ b/src/kOS.Safe/Compilation/IR/IRScope.cs @@ -108,14 +108,12 @@ public void RemoveBlock(BasicBlock block) => blocks.Remove(block); public override string ToString() + => $"IRScope: {IndexString()}"; + public string IndexString() { - if (IsGlobalScope) - return "IRScope: Global"; - return $"IRScope: {IndexString()}"; - } - private string IndexString() - { - if (ParentScope == null) + if (IsGlobalScope || ParentScope == null) + return "Global"; + if (ParentScope.IsGlobalScope) return $"{index}"; return $"{ParentScope.IndexString()}.{index}"; } diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index d6fdd80ff..d138d2519 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -50,6 +50,16 @@ public IRVariableBase(string name, IRScope declaringScope) Name = name; Scope = declaringScope; } + public override string ToString() + => $"{Name} {Scope.IndexString()}"; + public override bool Equals(object obj) + => obj is IRVariableBase variable && + (Scope == variable.Scope || + Scope.IsEncompassedBy(variable.Scope) || + variable.Scope.IsEncompassedBy(Scope)) && + string.Equals(Name, variable.Name, System.StringComparison.OrdinalIgnoreCase); + public override int GetHashCode() + => Name.ToLower().GetHashCode(); } public class IRVariable : IRVariableBase { @@ -75,14 +85,6 @@ internal override IEnumerable EmitPush() SourceColumn = sourceColumn }; } - public override string ToString() - => Name; - public override bool Equals(object obj) - => obj is IRVariable variable && - Scope == variable.Scope && - string.Equals(Name, variable.Name, System.StringComparison.OrdinalIgnoreCase); - public override int GetHashCode() - => Name.ToLower().GetHashCode(); } public class IRRelocateLater : IRConstant { @@ -165,13 +167,18 @@ public override bool Equals(object obj) if (obj is IRVariable variable) { return isPromoted && - Scope == variable.Scope && - string.Equals(Name, variable.Name, System.StringComparison.OrdinalIgnoreCase); + base.Equals(variable); } return false; } + public override string ToString() + { + if (isPromoted) + return base.ToString(); + return $"| {Parent}"; + } public override int GetHashCode() - => Name.ToLower().GetHashCode(); + => base.GetHashCode(); } public class IRParameter : IRValue { From 58f6e52a1a0d6998b69c22024c821bfe736f1115 Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 13 Mar 2026 16:53:48 -0400 Subject: [PATCH 47/56] Make IRAssignment more strict by knowing the exact variable (with scope) that it produces. --- src/kOS.Safe/Compilation/IR/BasicBlock.cs | 2 +- src/kOS.Safe/Compilation/IR/IRBuilder.cs | 30 +++++++++++--------- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 25 +++++++--------- src/kOS.Safe/Compilation/IR/IRScope.cs | 27 ++++++++++++++++-- src/kOS.Safe/Compilation/IR/IRValue.cs | 6 +++- 5 files changed, 58 insertions(+), 32 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/BasicBlock.cs b/src/kOS.Safe/Compilation/IR/BasicBlock.cs index 651d507ca..b3d6cec95 100644 --- a/src/kOS.Safe/Compilation/IR/BasicBlock.cs +++ b/src/kOS.Safe/Compilation/IR/BasicBlock.cs @@ -166,7 +166,7 @@ public bool TryStoreVariable(IRVariableBase variable) public IRVariableBase PushVariable(string name, Opcode opcode) { IRScope globalScope = Scope.GetGlobalScope(); - IRVariableBase result = Scope.GetVariable(name); + IRVariableBase result = Scope.GetVariableNamed(name); if (result == null) { result = new IRVariable(name, globalScope, opcode); diff --git a/src/kOS.Safe/Compilation/IR/IRBuilder.cs b/src/kOS.Safe/Compilation/IR/IRBuilder.cs index a1e87aa6f..5951b460f 100644 --- a/src/kOS.Safe/Compilation/IR/IRBuilder.cs +++ b/src/kOS.Safe/Compilation/IR/IRBuilder.cs @@ -161,32 +161,36 @@ private void ParseInstruction(Opcode opcode, BasicBlock currentBlock, Stack false; - public string Target { get; set; } + public IRVariableBase Target { get; set; } public IRValue Value { get; set; } public StoreScope Scope { get; set; } = StoreScope.Ambivalent; public bool AssertExists { get; set; } = false; IRValue ISingleOperandInstruction.Operand { get => Value; set => Value = value; } - public IRAssign(OpcodeIdentifierBase opcode, IRValue value) : base(opcode) + public IRAssign(OpcodeIdentifierBase opcode, IRVariableBase target, IRValue value) : base(opcode) { - Target = opcode.Identifier; + Target = target; Value = value; } internal override IEnumerable EmitOpcode() @@ -79,34 +79,31 @@ internal override IEnumerable EmitOpcode() yield return opcode; if (AssertExists) { - yield return SetSourceLocation(new OpcodeStoreExist(Target)); + yield return SetSourceLocation(new OpcodeStoreExist(Target.Name)); yield break; } switch (Scope) { case StoreScope.Local: - yield return SetSourceLocation(new OpcodeStoreLocal(Target)); + yield return SetSourceLocation(new OpcodeStoreLocal(Target.Name)); yield break; case StoreScope.Global: - yield return SetSourceLocation(new OpcodeStoreGlobal(Target)); + yield return SetSourceLocation(new OpcodeStoreGlobal(Target.Name)); yield break; default: case StoreScope.Ambivalent: - yield return SetSourceLocation(new OpcodeStore(Target)); + yield return SetSourceLocation(new OpcodeStore(Target.Name)); yield break; } } public override string ToString() => string.Format("{{store {0}}}", Value.ToString()); public override bool Equals(object obj) - { - if (obj is IRAssign assignment) - return string.Equals(Target, assignment.Target, System.StringComparison.OrdinalIgnoreCase) && - Value.Equals(assignment.Value); - return base.Equals(obj); - } + => obj is IRAssign assignment && + Target.Equals(assignment.Target) && + Value.Equals(assignment.Value); public override int GetHashCode() - => Target.ToLower().GetHashCode(); + => Target.GetHashCode(); } public class IRBinaryOp : IRInstruction, IResultingInstruction, IMultipleOperandInstruction { diff --git a/src/kOS.Safe/Compilation/IR/IRScope.cs b/src/kOS.Safe/Compilation/IR/IRScope.cs index 39f68e738..4437d0c48 100644 --- a/src/kOS.Safe/Compilation/IR/IRScope.cs +++ b/src/kOS.Safe/Compilation/IR/IRScope.cs @@ -35,7 +35,8 @@ public IRScope ParentScope public IReadOnlyCollection Blocks => blocks; public BasicBlock HeaderBlock { get; set; } public BasicBlock FooterBlock { get; set; } - public IReadOnlyCollection Variables => variables.Keys; + public IReadOnlyCollection Variables => variables.Values; + public IReadOnlyCollection VariableNames => variables.Keys; public bool IsGlobalScope { get; internal set; } = false; public IRScope(IRScope parent) @@ -69,11 +70,11 @@ public bool TryStoreVariable(IRVariableBase variable) return ParentScope?.TryStoreVariable(variable) ?? false; } - public IRVariableBase GetVariable(string name) + public IRVariableBase GetVariableNamed(string name) { if (variables.ContainsKey(name)) return variables[name]; - return ParentScope?.GetVariable(name); + return ParentScope?.GetVariableNamed(name); } public bool IsVariableInScope(string name) @@ -82,6 +83,10 @@ public bool IsVariableInScope(string name) return true; return ParentScope?.IsVariableInScope(name) ?? false; } + public bool IsVariableInScope(IRVariableBase variable) + { + return variable.Scope.IsEncompassedBy(this); + } public IRScope GetScopeForVariableNamed(string name) { @@ -92,6 +97,22 @@ public IRScope GetScopeForVariableNamed(string name) return ParentScope.GetScopeForVariableNamed(name); } + public void ClearVariable(string name) + { + variables.Remove(name); + } + public void ClearVariable(IRVariableBase variable) + => ClearVariable(variable.Name); + + public bool IsEncompassedBy(IRScope scope) + { + if (scope.IsGlobalScope) + return true; + if (scope.childScopes.Contains(this)) + return true; + return ParentScope?.IsEncompassedBy(scope) ?? false; + } + public IRScope GetGlobalScope() { if (IsGlobalScope) diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index d138d2519..cf66475e0 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -68,6 +68,8 @@ public class IRVariable : IRVariableBase public override IRScope Scope { get => base.Scope; } public IRVariable(string name, IRScope scope, IRInstruction instruction, bool isLock = false) : this(name, scope, instruction.SourceLine, instruction.SourceColumn, isLock) { } + public IRVariable(OpcodeIdentifierBase opcode, IRScope scope, bool isLock = false) : + this(opcode.Identifier, scope, opcode, isLock) { } public IRVariable(string name, IRScope scope, Opcode opcode, bool isLock = false) : this(name, scope, opcode.SourceLine, opcode.SourceColumn, isLock) { } public IRVariable(string name, IRScope scope, short sourceLine, short sourceColumn, bool isLock = false) : @@ -135,7 +137,9 @@ public IRAssign PromoteToVariable(IRScope scope) { SourceLine = -1, SourceColumn = 0 - }, this) + }, + this, + this) { Scope = IRAssign.StoreScope.Local }; From 898bd3afea3f8d1b4325f868b452636249a5787e Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 13 Mar 2026 16:54:18 -0400 Subject: [PATCH 48/56] Improve debug string readability. --- src/kOS.Safe/Compilation/IR/IRInstruction.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Compilation/IR/IRInstruction.cs b/src/kOS.Safe/Compilation/IR/IRInstruction.cs index c926d5efd..b0477d47e 100644 --- a/src/kOS.Safe/Compilation/IR/IRInstruction.cs +++ b/src/kOS.Safe/Compilation/IR/IRInstruction.cs @@ -97,7 +97,7 @@ internal override IEnumerable EmitOpcode() } } public override string ToString() - => string.Format("{{store {0}}}", Value.ToString()); + => string.Format("{{store {0} -> {1}}}", Value.ToString(), Target.ToString()); public override bool Equals(object obj) => obj is IRAssign assignment && Target.Equals(assignment.Target) && @@ -283,7 +283,7 @@ internal override IEnumerable EmitOpcode() yield return SetSourceLocation(new OpcodePop()); } public override string ToString() - => "{pop}"; + => $"{{pop {Value}}}"; public override bool Equals(object obj) => obj is IRPop pop && Value.Equals(pop.Value); public override int GetHashCode() From 5b420330fa388f3144208deef32e42d8c987071e Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 13 Mar 2026 16:54:42 -0400 Subject: [PATCH 49/56] Fix collection changed while enumerating exception. --- .../Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs index aa95d2579..e33a78bb5 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs @@ -50,7 +50,7 @@ public void ApplyPass(List code) private static void CollapseScope(IRScope scope) { IRScope newScope = scope.ParentScope; - foreach (IRScope childScope in scope.Children) + foreach (IRScope childScope in scope.Children.ToArray()) childScope.ParentScope = newScope; foreach (BasicBlock block in scope.Blocks.ToArray()) block.Scope = newScope; From 023de41c7440f260a5ea400039d3a7fe0bf5a96d Mon Sep 17 00:00:00 2001 From: DBooots Date: Fri, 13 Mar 2026 17:17:36 -0400 Subject: [PATCH 50/56] Fix infinite loop when dumping the tree where there is a loop contained. --- .../Optimization/ExtendedBasicBlock.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs b/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs index 53dbad6a9..1456d26a8 100644 --- a/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs +++ b/src/kOS.Safe/Compilation/Optimization/ExtendedBasicBlock.cs @@ -38,7 +38,22 @@ public static ExtendedBasicBlock CreateExtendedBlockTree(BasicBlock root) return resultBlock; } public static IEnumerable DumpTree(ExtendedBasicBlock root) - => Enumerable.Repeat(root, 1).Concat(root.sucessors.SelectMany(DumpTree)); + { + HashSet visited = new HashSet(); + return DumpTree(root, visited); + } + private static IEnumerable DumpTree(ExtendedBasicBlock root, HashSet visited) + { + yield return root; + foreach (ExtendedBasicBlock successor in root.Sucessors) + { + if (visited.Add(successor)) + { + foreach (ExtendedBasicBlock further in DumpTree(successor, visited)) + yield return further; + } + } + } public void AddSuccessor(ExtendedBasicBlock successor) { From c59ce691dbc69558528a83bb407d8b002394422b Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 14 Mar 2026 00:24:33 -0400 Subject: [PATCH 51/56] Fix equality not working between constants. --- src/kOS.Safe/Compilation/IR/IRValue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kOS.Safe/Compilation/IR/IRValue.cs b/src/kOS.Safe/Compilation/IR/IRValue.cs index cf66475e0..5fe141e9b 100644 --- a/src/kOS.Safe/Compilation/IR/IRValue.cs +++ b/src/kOS.Safe/Compilation/IR/IRValue.cs @@ -35,7 +35,7 @@ internal override IEnumerable EmitPush() }; } public override bool Equals(object obj) - => Value.Equals(obj); + => obj is IRConstant constant && Value.Equals(constant.Value) || Value.Equals(obj); public override int GetHashCode() => Value.GetHashCode(); public override string ToString() From b59a7b0e8747c766f916ea38bea3e5db77c71af7 Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 14 Mar 2026 00:26:33 -0400 Subject: [PATCH 52/56] Add constant propagation pass. This pass replaces local variables with constants wherever it can. Variables that are global, or that are written to within a LOCK statement are left alone, and variables that are written to in a function are not replaced after the first call to that function, unless subsequently reset. This works across the reaching definition, although the current implementation is a bit messy. --- .../integration/constantPropagation.ks | 63 +++ .../Execution/OptimizationTest.cs | 28 + .../Optimization/OptimizationLevel.cs | 4 +- .../Passes/ConstantPropagation.cs | 508 ++++++++++++++++++ 4 files changed, 602 insertions(+), 1 deletion(-) create mode 100644 kerboscript_tests/integration/constantPropagation.ks create mode 100644 src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs diff --git a/kerboscript_tests/integration/constantPropagation.ks b/kerboscript_tests/integration/constantPropagation.ks new file mode 100644 index 000000000..a44ed8106 --- /dev/null +++ b/kerboscript_tests/integration/constantPropagation.ks @@ -0,0 +1,63 @@ +@lazyGlobal OFF. + +global a is 5. +global _false is false. + +local b is 2. +local c is 4. +local d is 7. +local h is false. + +print("test"). // The assignments should happen after this line. + +print(b+c). // 6 +print(h). // False + +global function func { + print(b+c). // 6 + print(b+d). // 9 + print(b+a). // 7 + set h to true. +} + +func(). + +{ + local i is 10. + print(i). // 10 + print(i+c). // 14 +} + +print(h). // True + +set h to false. + +print(h). // False + +lock e to a + b. + +print(e). // 7 +set b to 1. +print(e). // 6 + +lock g to c + d. +print(g). // 11 +set d to 5. +print(g). // 9 + +when _false then { + set c to 10. +} + +print(b+c). // 5 + +set b to 1. +set b to b + d. + +print(b). // 6 + +until b = 1 { + set b to b - 1. +} + +print(b). // 1 diff --git a/src/kOS.Safe.Test/Execution/OptimizationTest.cs b/src/kOS.Safe.Test/Execution/OptimizationTest.cs index d35a9783e..4216b0a5f 100644 --- a/src/kOS.Safe.Test/Execution/OptimizationTest.cs +++ b/src/kOS.Safe.Test/Execution/OptimizationTest.cs @@ -178,5 +178,33 @@ public void TestPeepholeOptimizations() "512" ); } + + + [Test] + public void TestConstantPropagation() + { + // Test that local constant variables propagate + RunScript("integration/constantPropagation.ks"); + RunSingleStep(); + AssertOutput( + "test", + "6", + "False", + "6", + "9", + "7", + "10", + "14", + "True", + "False", + "7", + "6", + "11", + "9", + "5", + "6", + "1" + ); + } } } diff --git a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs index 19e108481..474db720b 100644 --- a/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs +++ b/src/kOS.Safe/Compilation/Optimization/OptimizationLevel.cs @@ -7,6 +7,7 @@ namespace kOS.Safe.Compilation * 10. Suffix replacement: * Replace CONSTANT: values with the constant * Replace ship fields with their alias + * 20. Constant propagation * 30. Constant folding * 50. Dead code elimination * 1050. Peephole optimizations: @@ -19,7 +20,6 @@ namespace kOS.Safe.Compilation * Algebraic simplification (e.g. A*B+A*C = A*(B+C), A+-B=A-B, A--B=A+B, -A+B=B-A, X*X*...*X=N^X) * 32000. Remove unnecessary scope pushes/pops * - * 20. Constant propagation * O2: * 1000. Common expression elimination * Particularly: Any expression of 3 opcodes used more than twice, @@ -29,10 +29,12 @@ namespace kOS.Safe.Compilation * Loop jamming? (Combining adjacent loops into one) * Unswitching (moving conditional evaluation outside the loop) - low priority * Linear function test replacement - low priority + * * O3: * 3100. Local function inlining * 3200. Constant propagation to local functions * 3500. Loop stack manipulation (delayed setting of either index or aggregator) + * * O4: * 4000. Constant loop unrolling */ diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs new file mode 100644 index 000000000..7d04469f5 --- /dev/null +++ b/src/kOS.Safe/Compilation/Optimization/Passes/ConstantPropagation.cs @@ -0,0 +1,508 @@ +using kOS.Safe.Compilation.IR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace kOS.Safe.Compilation.Optimization.Passes +{ + public class ConstantPropagation : IHolisticOptimizationPass + { + public OptimizationLevel OptimizationLevel => OptimizationLevel.Minimal; + + public short SortIndex => 20; + + public void ApplyPass(IRCodePart codePart) + { + Dictionary> funcExternalWrites = new Dictionary>(); + Dictionary> funcExternalReads = new Dictionary>(); + + // For each function, find the sets of non-local variables read and those written to. + foreach (IRCodePart.IRFunction function in codePart.Functions) + { + HashSet externalWrites = new HashSet(); + HashSet externalReads = new HashSet(); + foreach (IRCodePart.IRFunction.IRFunctionFragment fragment in function.Fragments) + { + AnalyzeFunction(fragment.FunctionCode, externalWrites, externalReads); + } + funcExternalWrites[function.Identifier] = externalWrites; + funcExternalReads[function.Identifier] = externalReads; + } + foreach (IRCodePart.IRTrigger trigger in codePart.Triggers) + { + HashSet externalWrites = new HashSet(); + HashSet externalReads = new HashSet(); + AnalyzeFunction(trigger.Code, externalWrites, externalReads); + funcExternalWrites[trigger.Identifier] = externalWrites; + funcExternalReads[trigger.Identifier] = externalReads; + } + + Dictionary> variablesPropagated = new Dictionary>(); + Dictionary> variablesUnpropagated = new Dictionary>(); + + Dictionary inOutData = new Dictionary(); + Dictionary storedFuncs = new Dictionary(); + + foreach (BasicBlock rootBlock in codePart.RootBlocks) + { + Dictionary map = MapIncoming(rootBlock, + funcExternalWrites, + out Dictionary storedFunctions); + foreach (KeyValuePair pair in map) + inOutData.Add(pair.Key, pair.Value); + foreach (KeyValuePair pair in storedFunctions) + storedFuncs.Add(pair.Key, pair.Value); + } + + foreach (BasicBlock block in codePart.Blocks) + { + if (block.Scope.IsGlobalScope) + PropagateWithinBlock(block, inOutData, + storedFuncs, + new HashSet(), + variablesUnpropagated, + funcExternalReads, + funcExternalWrites); + else + { + if (!variablesPropagated.ContainsKey(block.Scope)) + { + variablesPropagated.Add(block.Scope, new HashSet()); + variablesUnpropagated.Add(block.Scope, new HashSet()); + } + PropagateWithinBlock(block, inOutData, + storedFuncs, + variablesPropagated[block.Scope], + variablesUnpropagated, + funcExternalReads, + funcExternalWrites); + } + } + + foreach (IRScope scope in variablesPropagated.Keys) + { + if (variablesUnpropagated.ContainsKey(scope)) + variablesPropagated[scope].ExceptWith(variablesUnpropagated[scope]); + foreach (IRVariableBase variable in variablesPropagated[scope]) + scope.ClearVariable(variable); + } + } + + private static void AnalyzeFunction(List functionCode, HashSet writes, HashSet reads) + { + if (functionCode.Any()) + reads.UnionWith(functionCode.First().Scope.GetGlobalScope().Variables); + foreach (BasicBlock block in functionCode) + { + foreach (IRInstruction instruction in block.Instructions) + { + if (instruction is IRAssign assignment && + assignment.Target.Scope.IsGlobalScope) + writes.Add(assignment.Target); + } + } + } + + private static void RescopeFunctionVars(HashSet variables, IRScope scope) + { + HashSet temp = new HashSet(); + foreach(IRVariableBase variable in variables) + { + if (scope.IsVariableInScope(variable.Name)) + temp.Add(scope.GetVariableNamed(variable.Name)); + else + temp.Add(variable); + } + variables.Clear(); + variables.UnionWith(temp); + } + + private static Dictionary MapIncoming(BasicBlock rootBlock, + Dictionary> funcExternalWrites, + out Dictionary storedFunctions) + { + storedFunctions = new Dictionary(); + + HashSet GetValueOrDefault(Dictionary> dict, BasicBlock key, IEqualityComparer comparer) + { + if (!dict.ContainsKey(key)) + { + if (comparer == null) + comparer = EqualityComparer.Default; + HashSet result = new HashSet(comparer); + dict.Add(key, result); + return result; + } + return dict[key]; + } + + Dictionary> incoming = new Dictionary>(); + Dictionary> blacklistIn = new Dictionary>(); + + Dictionary> outgoing = new Dictionary>(); + Dictionary> blacklistOut = new Dictionary>(); + Dictionary> defs = new Dictionary>(); + + HashSet basicBlocks = new HashSet(); + Queue worklist = new Queue(); + worklist.Enqueue(rootBlock); + + while (worklist.Count > 0) + { + BasicBlock block = worklist.Dequeue(); + basicBlocks.Add(block); + + if (!defs.ContainsKey(block)) + { + ParseDefs(block, + out Dictionary defsMade, + GetValueOrDefault(blacklistOut, block, null), + storedFunctions, + funcExternalWrites); + defs[block] = new HashSet<(IRVariableBase variable, IRAssign value)>(defsMade.Select(kvp => (kvp.Key, kvp.Value)), VariableTupleEqualityComparer.Instance); + } + + incoming.Remove(block); + HashSet<(IRVariableBase variable, IRAssign value)> defsIn = GetValueOrDefault(incoming, block, VariableTupleEqualityComparer.Instance); + blacklistIn.Remove(block); + HashSet localBlacklist = GetValueOrDefault(blacklistIn, block, null); + BasicBlock firstPredecessor = block.Predecessors.FirstOrDefault(); + if (firstPredecessor != null) + { + defsIn.UnionWith(GetValueOrDefault(outgoing, firstPredecessor, VariableTupleEqualityComparer.Instance)); + localBlacklist.UnionWith(GetValueOrDefault(blacklistOut, firstPredecessor, null)); + } + + foreach (BasicBlock predecessor in block.Predecessors.Skip(1)) + { + if (!outgoing.ContainsKey(predecessor)) + continue; + defsIn.IntersectWith(GetValueOrDefault(outgoing, predecessor, VariableTupleEqualityComparer.Instance)); + localBlacklist.UnionWith(GetValueOrDefault(blacklistOut, predecessor, null)); + } + + defsIn.RemoveWhere(vv => !(vv.variable.Scope == block.Scope || block.Scope.IsEncompassedBy(vv.variable.Scope))); + defsIn.RemoveWhere(vv => localBlacklist.Contains(vv.variable)); + + HashSet<(IRVariableBase variable, IRAssign value)> defsOut = new HashSet<(IRVariableBase variable, IRAssign value)>(defsIn, VariableTupleEqualityComparer.Instance); + HashSet<(IRVariableBase variable, IRAssign value)> definitions = defs[block]; + + defsOut.UnionWith(defsIn); + foreach (var def in definitions) + { + defsOut.RemoveWhere(vv => vv.variable.Equals(def.variable)); + defsOut.Add(def); + } + defsOut.RemoveWhere(vv => localBlacklist.Contains(vv.variable)); + + if (outgoing.ContainsKey(block) && defsOut.SetEquals(outgoing[block])) + continue; + + outgoing[block] = defsOut; + blacklistOut[block].UnionWith(localBlacklist); + foreach (BasicBlock successor in block.Successors) + worklist.Enqueue(successor); + } + + Dictionary map = new Dictionary(); + foreach (BasicBlock block in basicBlocks) + map.Add(block, new BlockDictionaries(incoming[block], blacklistIn[block])); + + return map; + } + + private static void ParseDefs(BasicBlock block, + out Dictionary defs, + HashSet blacklist, + Dictionary storedFunctions, + Dictionary> funcExternalWrites) + { + defs = new Dictionary(); + List instructions = block.Instructions; + for (int i = 0; i < block.Instructions.Count; i++) + { + IRInstruction instruction = instructions[i]; + // Step through instructions (depth first) and replace variables + // with their cached constant whenever they're available. + foreach (IRInstruction nestedInstruction in instruction.DepthFirst()) + { + RemoveVarsWrittenInFunction(nestedInstruction, + block.Scope, + defs, + storedFunctions, + funcExternalWrites); + } + + switch (instruction) + { + // On a local assignment by a constant, cache that constant against the variable. + case IRAssign assignment: + if (!blacklist.Contains(assignment.Target) && + !assignment.Target.Scope.IsGlobalScope) + { + defs[assignment.Target] = assignment; + } + + // On encountering a PushRelocateLater, note the scope for its closure variables. + if (assignment.Value is IRRelocateLater lockOrFunctionPointer) + { + string pointer = ((string)lockOrFunctionPointer.Value).Split('-').First(); + storedFunctions[assignment.Target] = pointer; + // Re-scope the stored variables from Global to the current scope. + RescopeFunctionVars(funcExternalWrites[pointer], block.Scope); + } + break; + + // On encountering a trigger: + // Clear the cache of any variables that are written in that trigger or body. + // Blacklist any variables that are written in the trigger or body. + case IRUnaryConsumer trigger: + if (trigger.Operation is OpcodeAddTrigger) + { + string destination = (string)((IRConstant)trigger.Operand).Value; + if (funcExternalWrites.ContainsKey(destination)) + { + foreach (IRVariableBase variable in funcExternalWrites[destination]) + defs.Remove(variable); + + // Re-scope the stored variables from Global to the current scope. + RescopeFunctionVars(funcExternalWrites[destination], block.Scope); + blacklist.UnionWith(funcExternalWrites[destination]); + } + } + break; + } + } + } + + private static void RemoveVarsWrittenInFunction(IRInstruction instr, + IRScope scope, + Dictionary defsCreated, + Dictionary storedFunctions, + Dictionary> funcExternalWrites) + { + if (instr is IRCall call) + { + // On encountering a Call, where that identifier is in our dictionary of function closure variables. + // Inject the assignment call to any constant variables that are read inside the function. + // Clear the cache of any variables that are written in that function. + IRVariableBase functionCall = scope.GetVariableNamed(call.Function); + if (functionCall != null && storedFunctions.ContainsKey(functionCall)) + { + string funcRef = storedFunctions[functionCall]; + + foreach (IRVariableBase variable in funcExternalWrites[funcRef]) + defsCreated.Remove(variable); + } + } + } + + private static void PropagateWithinBlock(BasicBlock block, + Dictionary inOutData, + Dictionary storedFunctions, + HashSet scopeVarsPropagated, + Dictionary> variablesUnpropagated, + Dictionary> externalVariablesRead, + Dictionary> externalVariablesWritten) + { + BlockDictionaries localInOut = inOutData[block]; + + List instructions = block.Instructions; + + inOutData[block].InitializeForPropagation( + out Dictionary constantCache, + out Dictionary assignmentInstructions, + out HashSet blacklist); + + for (int i = 0; i < instructions.Count; i++) + { + IRInstruction instruction = instructions[i]; + // Step through instructions (depth first) and replace variables + // with their cached constant whenever they're available. + foreach (IRInstruction nestedInstruction in instruction.DepthFirst()) + { + AttemptPropagation(nestedInstruction, + instructions, + ref i, + block.Scope, + constantCache, + storedFunctions, + externalVariablesRead, + externalVariablesWritten,assignmentInstructions, + variablesUnpropagated); + } + + switch (instruction) + { + // On a local assignment by a constant, cache that constant against the variable. + case IRAssign assignment: + if (!blacklist.Contains(assignment.Target) && + !assignment.Target.Scope.IsGlobalScope) + { + if (assignment.Value is IRTemp temp) + assignment.Value = ConstantFolding.AttemptReduction(temp.Parent); + if (assignment.Value is IRConstant constantValue) + { + scopeVarsPropagated.Add(assignment.Target); + instructions.RemoveAt(i); + assignmentInstructions[assignment.Target] = assignment; + constantCache[assignment.Target] = constantValue; + i--; + } + else + { + scopeVarsPropagated.Remove(assignment.Target); + assignmentInstructions.Remove(assignment.Target); + constantCache.Remove(assignment.Target); + } + } + + // On encountering a PushDelegateRelocateLater, note which variables are cached. + // Store the intersections of that set and the function variable sets. + else if (assignment.Value is IRDelegateRelocateLater functionPointer) + { + string pointer = ((string)functionPointer.Value).Split('-').First(); + storedFunctions[assignment.Target] = pointer; + // Re-scope the stored variables from Global to the current scope. + // Writes were accomplished on the first pass. + RescopeFunctionVars(externalVariablesRead[pointer], block.Scope); + } + + // On encountering a Lock call, we will:: + // Inject the assignment call to any constant variables that are read inside the function. + // Clear the cache of any variables that are written in that lock. + else if (assignment.Value is IRRelocateLater lockPointer) + { + string pointer = ((string)lockPointer.Value).Split('-').First(); + storedFunctions[assignment.Target] = pointer; + // Re-scope the stored variables from Global to the current scope. + // Writes were accomplished on the first pass. + RescopeFunctionVars(externalVariablesRead[pointer], block.Scope); + } + break; + + // On encountering a trigger: + // Inject the assignment call to any constant variables that are read inside the trigger or body. + // Clear the cache of any variables that are written in that trigger or body. + // Blacklist any variables that are written in the trigger or body. + case IRUnaryConsumer trigger: + if (trigger.Operation is OpcodeAddTrigger) + { + string destination = (string)((IRConstant)trigger.Operand).Value; + if (externalVariablesRead.ContainsKey(destination)) + { + foreach (IRVariableBase variable in externalVariablesRead[destination].Union( + externalVariablesWritten[destination])) + if (assignmentInstructions.ContainsKey(variable)) + { + instructions.Insert(i++, assignmentInstructions[variable]); + assignmentInstructions.Remove(variable); + variablesUnpropagated[variable.Scope].Add(variable); + } + + foreach (IRVariableBase variable in externalVariablesWritten[destination]) + constantCache.Remove(variable); + + blacklist.UnionWith(externalVariablesWritten[destination]); + } + } + break; + } + } + } + + private static void AttemptPropagation(IRInstruction instruction, + List instructions, + ref int i, + IRScope scope, + Dictionary constantCache, + Dictionary storedFunctions, + Dictionary> funcExternalReads, + Dictionary> funcExternalWrites, + Dictionary assignmentInstructions, + Dictionary> variablesUnpropagated) + { + if (instruction is IRCall call) + { + // On encountering a Call, where that identifier is in our dictionary of function closure variables. + // Inject the assignment call to any constant variables that are read inside the function. + // Clear the cache of any variables that are written in that function. + IRVariableBase functionCall = scope.GetVariableNamed(call.Function); + if (functionCall != null && storedFunctions.ContainsKey(functionCall)) + { + string funcRef = storedFunctions[functionCall]; + foreach (IRVariableBase variable in funcExternalReads[funcRef].Union( + funcExternalWrites[funcRef])) + if (assignmentInstructions.ContainsKey(variable)) + { + instructions.Insert(i++, assignmentInstructions[variable]); + assignmentInstructions.Remove(variable); + variablesUnpropagated[variable.Scope].Add(variable); + } + + foreach (IRVariableBase variable in funcExternalWrites[funcRef]) + constantCache.Remove(variable); + } + } + if (instruction is ISingleOperandInstruction singleOperandInstruction) + { + if (singleOperandInstruction.Operand is IRVariable variable && + constantCache.ContainsKey(variable) && + constantCache[variable] is IRConstant) + { + singleOperandInstruction.Operand = constantCache[variable]; + } + } + else if (instruction is IMultipleOperandInstruction multipleOperandInstruction) + { + for (int j = multipleOperandInstruction.OperandCount - 1; j >= 0; j--) + { + if (multipleOperandInstruction[j] is IRVariable variable && + constantCache.ContainsKey(variable) && + constantCache[variable] is IRConstant) + { + multipleOperandInstruction[j] = constantCache[variable]; + } + } + } + } + + private class BlockDictionaries + { + public HashSet<(IRVariableBase IRVariableBase, IRAssign value)> incomingDefinitions; + public HashSet blacklist; + public BlockDictionaries( + HashSet<(IRVariableBase, IRAssign)> incomingDefinitions, + HashSet blacklist) + { + this.incomingDefinitions = incomingDefinitions; + this.blacklist = blacklist; + } + public void InitializeForPropagation( + out Dictionary constantCache, + out Dictionary assignments, + out HashSet blacklist) + { + constantCache = new Dictionary(); + assignments = new Dictionary(); + foreach ((IRVariableBase variable, IRAssign value) in incomingDefinitions) + { + constantCache.Add(variable, value.Value); + assignments.Add(variable, value); + } + blacklist = new HashSet(this.blacklist); + } + } + + private class VariableTupleEqualityComparer : IEqualityComparer<(IRVariableBase variable, IRAssign value)> + { + public static VariableTupleEqualityComparer Instance { get; } = new VariableTupleEqualityComparer(); + public bool Equals((IRVariableBase variable, IRAssign value) x, (IRVariableBase variable, IRAssign value) y) + => x.variable.Equals(y.variable) && (x.value.Value.Equals(y.value.Value)); + + public int GetHashCode((IRVariableBase variable, IRAssign value) obj) + => obj.variable.GetHashCode(); + } + } +} From 7cfbd7913ab58380526007842c9f3896d201be4e Mon Sep 17 00:00:00 2001 From: DBooots Date: Sat, 14 Mar 2026 00:52:17 -0400 Subject: [PATCH 53/56] Patch scopes losing ancestry when an unused scope is collapsed. --- .../Optimization/Passes/UnnecessaryScopeRemoval.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs index e33a78bb5..868846623 100644 --- a/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs +++ b/src/kOS.Safe/Compilation/Optimization/Passes/UnnecessaryScopeRemoval.cs @@ -50,12 +50,24 @@ public void ApplyPass(List code) private static void CollapseScope(IRScope scope) { IRScope newScope = scope.ParentScope; + short parentID = GetPushOpcode(newScope).ScopeId; foreach (IRScope childScope in scope.Children.ToArray()) + { childScope.ParentScope = newScope; + OpcodePushScope opcode = GetPushOpcode(childScope); + opcode.ParentScopeId = parentID; + } foreach (BasicBlock block in scope.Blocks.ToArray()) block.Scope = newScope; scope.ParentScope = null; } + private static OpcodePushScope GetPushOpcode(IRScope scope) + { + if (scope.IsGlobalScope) + return new OpcodePushScope(0, 0); + IRNoStackInstruction pushScopeInstruction = scope.HeaderBlock.Instructions[0] as IRNoStackInstruction; + return (OpcodePushScope)pushScopeInstruction.Operation; + } private static void RemovePushScope(BasicBlock header) { From b094ce305a0683ec5a0d8f286de56fbca6775131 Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 16 Mar 2026 20:35:10 -0400 Subject: [PATCH 54/56] Add return type indicator to FunctionAttribute and tag all kOS functions accordingly. --- src/kOS.Safe/Encapsulation/Lexicon.cs | 4 +- src/kOS.Safe/Encapsulation/ListValue.cs | 2 +- src/kOS.Safe/Encapsulation/PIDLoop.cs | 2 +- src/kOS.Safe/Encapsulation/QueueValue.cs | 2 +- src/kOS.Safe/Encapsulation/StackValue.cs | 2 +- src/kOS.Safe/Encapsulation/UniqueSetValue.cs | 2 +- src/kOS.Safe/Function/FunctionAttribute.cs | 22 +++++- src/kOS.Safe/Function/Math.cs | 28 +++---- src/kOS.Safe/Function/Misc.cs | 28 +++---- src/kOS.Safe/Function/Persistence.cs | 32 ++++---- src/kOS.Safe/Function/Suffixed.cs | 4 +- src/kOS.Safe/Function/Trigonometry.cs | 18 ++--- src/kOS.Safe/Persistence/PathValue.cs | 2 +- src/kOS.Safe/Persistence/Volume.cs | 2 +- .../AddOns/KerbalAlarmClock/KACFunctions.cs | 10 +-- src/kOS/Function/BuildList.cs | 2 +- src/kOS/Function/Math.cs | 10 +-- src/kOS/Function/Misc.cs | 14 ++-- src/kOS/Function/Persistence.cs | 4 +- src/kOS/Function/PrintList.cs | 2 +- src/kOS/Function/Suffixed.cs | 78 +++++++++---------- 21 files changed, 144 insertions(+), 126 deletions(-) diff --git a/src/kOS.Safe/Encapsulation/Lexicon.cs b/src/kOS.Safe/Encapsulation/Lexicon.cs index e61603567..6e4edfdff 100644 --- a/src/kOS.Safe/Encapsulation/Lexicon.cs +++ b/src/kOS.Safe/Encapsulation/Lexicon.cs @@ -11,10 +11,10 @@ namespace kOS.Safe.Encapsulation { [kOS.Safe.Utilities.KOSNomenclature("Lexicon")] - [kOS.Safe.Utilities.KOSNomenclature("Lex", CSharpToKOS = false) ] + [kOS.Safe.Utilities.KOSNomenclature("Lex", CSharpToKOS = false)] public class Lexicon : SerializableStructure, IDictionary, IIndexable { - [Function("lex", "lexicon")] + [Function("lex", "lexicon", ReturnType = typeof(Lexicon), IsInvariant = true)] public class FunctionLexicon : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/ListValue.cs b/src/kOS.Safe/Encapsulation/ListValue.cs index 565038028..bda8e2e00 100644 --- a/src/kOS.Safe/Encapsulation/ListValue.cs +++ b/src/kOS.Safe/Encapsulation/ListValue.cs @@ -150,7 +150,7 @@ public void Insert(int index, T item) [kOS.Safe.Utilities.KOSNomenclature("List", KOSToCSharp = false)] // one-way because the generic templated ListValue is the canonical one. public class ListValue : ListValue { - [Function("list")] + [Function("list", ReturnType = typeof(ListValue), IsInvariant = true)] public class FunctionList : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/PIDLoop.cs b/src/kOS.Safe/Encapsulation/PIDLoop.cs index c2c36af9c..99231a9b9 100644 --- a/src/kOS.Safe/Encapsulation/PIDLoop.cs +++ b/src/kOS.Safe/Encapsulation/PIDLoop.cs @@ -9,7 +9,7 @@ namespace kOS.Safe.Encapsulation [kOS.Safe.Utilities.KOSNomenclature("PIDLoop")] public class PIDLoop : SerializableStructure { - [Function("pidloop")] + [Function("pidloop", ReturnType = typeof(PIDLoop), IsInvariant = true)] public class PIDLoopConstructor : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/QueueValue.cs b/src/kOS.Safe/Encapsulation/QueueValue.cs index 9c91ac7e3..bf30461f8 100644 --- a/src/kOS.Safe/Encapsulation/QueueValue.cs +++ b/src/kOS.Safe/Encapsulation/QueueValue.cs @@ -68,7 +68,7 @@ public static QueueValue CreateQueue(IEnumerable list) [kOS.Safe.Utilities.KOSNomenclature("Queue", KOSToCSharp = false)] // one-way because the generic templated QueueValue is the canonical one. public class QueueValue : QueueValue { - [Function("queue")] + [Function("queue", ReturnType = typeof(QueueValue), IsInvariant = true)] public class FunctionQueue : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/StackValue.cs b/src/kOS.Safe/Encapsulation/StackValue.cs index 1eed16dae..63b8e83d7 100644 --- a/src/kOS.Safe/Encapsulation/StackValue.cs +++ b/src/kOS.Safe/Encapsulation/StackValue.cs @@ -74,7 +74,7 @@ public static StackValue CreateStack(IEnumerable list) [kOS.Safe.Utilities.KOSNomenclature("Stack", KOSToCSharp = false)] // one-way because the generic templated StackValue is the canonical one. public class StackValue : StackValue { - [Function("stack")] + [Function("stack", ReturnType = typeof(StackValue), IsInvariant = true)] public class FunctionStack : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Encapsulation/UniqueSetValue.cs b/src/kOS.Safe/Encapsulation/UniqueSetValue.cs index 4e0e1c0da..e02e00eff 100644 --- a/src/kOS.Safe/Encapsulation/UniqueSetValue.cs +++ b/src/kOS.Safe/Encapsulation/UniqueSetValue.cs @@ -73,7 +73,7 @@ private void SetInitializeSuffixes() [kOS.Safe.Utilities.KOSNomenclature("UniqueSet", KOSToCSharp = false)] // one-way because the generic templated UniqueSetValue is the canonical one. public class UniqueSetValue : UniqueSetValue { - [Function("uniqueset")] + [Function("uniqueset", ReturnType = typeof(UniqueSetValue), IsInvariant = true)] public class FunctionSet : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/FunctionAttribute.cs b/src/kOS.Safe/Function/FunctionAttribute.cs index 6fb891d87..1c878c858 100644 --- a/src/kOS.Safe/Function/FunctionAttribute.cs +++ b/src/kOS.Safe/Function/FunctionAttribute.cs @@ -1,11 +1,29 @@ -using System; +using System; namespace kOS.Safe.Function { public class FunctionAttribute : Attribute { public string[] Names { get; set; } - + public bool IsInvariant { get; set; } = false; + public Type ReturnType + { + get => returnType; + set + { + if (value != null && !value.IsSubclassOf(typeof(Encapsulation.Structure))) + { +#if DEBUG + throw new ArgumentException($"{value} is not an accepted return type for a kOS function since it does not subclass {typeof(Encapsulation.Structure)}."); +#else + Utilities.SafeHouse.Logger.LogError($"The supplied type, {value}, is not an accepted return type for a kOS function since it is not a subclass of ${typeof(Encapsulation.Structure)}."); + returnType = null; +#endif + } + returnType = value; + } + } + private Type returnType = null; public FunctionAttribute(params string[] names) { Names = names; diff --git a/src/kOS.Safe/Function/Math.cs b/src/kOS.Safe/Function/Math.cs index 554a8c062..6ed78a4ac 100644 --- a/src/kOS.Safe/Function/Math.cs +++ b/src/kOS.Safe/Function/Math.cs @@ -4,7 +4,7 @@ namespace kOS.Safe.Function { - [Function("abs")] + [Function("abs", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionAbs : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -16,7 +16,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("mod")] + [Function("mod", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionMod : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -29,7 +29,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("floor")] + [Function("floor", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionFloor : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -44,7 +44,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("ceiling")] + [Function("ceiling", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionCeiling : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -59,7 +59,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("round")] + [Function("round", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionRound : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -86,7 +86,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("sqrt")] + [Function("sqrt", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionSqrt : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -99,7 +99,7 @@ public override void Execute(SafeSharedObjects shared) } - [Function("ln")] + [Function("ln", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionLn : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -111,7 +111,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("log10")] + [Function("log10", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionLog10 : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -123,7 +123,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("min")] + [Function("min", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionMin : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -153,7 +153,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("max")] + [Function("max", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionMax : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -183,7 +183,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("random")] + [Function("random", ReturnType = typeof(ScalarDoubleValue), IsInvariant = false)] public class FunctionRandom : SafeFunctionBase { private readonly Random random = new Random(); @@ -201,7 +201,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("randomseed")] + [Function("randomseed", ReturnType = null, IsInvariant = false)] public class FunctionRandomSeed : SafeFunctionBase { private readonly Random random = new Random(); @@ -215,7 +215,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("char")] + [Function("char", ReturnType = typeof(StringValue), IsInvariant = true)] public class FunctionChar : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -227,7 +227,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("unchar")] + [Function("unchar", ReturnType = typeof(ScalarIntValue), IsInvariant = true)] public class FunctionUnchar : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/Misc.cs b/src/kOS.Safe/Function/Misc.cs index b0a7d64e2..889038507 100644 --- a/src/kOS.Safe/Function/Misc.cs +++ b/src/kOS.Safe/Function/Misc.cs @@ -10,7 +10,7 @@ namespace kOS.Safe.Function { - [Function("print")] + [Function("print", ReturnType = null, IsInvariant = false)] public class FunctionPrint : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -21,7 +21,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("printat")] + [Function("printat", ReturnType = null, IsInvariant = false)] public class FunctionPrintAt : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -34,7 +34,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("toggleflybywire")] + [Function("toggleflybywire", ReturnType = null, IsInvariant = false)] public class FunctionToggleFlyByWire : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -46,7 +46,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("selectautopilotmode")] + [Function("selectautopilotmode", ReturnType = null, IsInvariant = false)] public class FunctionSelectAutopilotMode : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -57,7 +57,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("run")] + [Function("run", ReturnType = null, IsInvariant = false)] public class FunctionRun : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -133,7 +133,7 @@ public override void Execute(SafeSharedObjects shared) } } - [FunctionAttribute("load")] + [Function("load", ReturnType = null, IsInvariant = false)] public class FunctionLoad : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -262,7 +262,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("logfile")] + [Function("logfile", ReturnType = null, IsInvariant = false)] public class FunctionLogFile : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -300,7 +300,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("reboot")] + [Function("reboot", ReturnType = null, IsInvariant = false)] public class FunctionReboot : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -315,7 +315,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("shutdown")] + [Function("shutdown", ReturnType = null, IsInvariant = false)] public class FunctionShutdown : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -326,7 +326,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("debugdump")] + [Function("debugdump", ReturnType = null, IsInvariant = false)] public class DebugDump : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -336,7 +336,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("debugfreezegame")] + [Function("debugfreezegame", ReturnType = null, IsInvariant = false)] /// /// Deliberately cause physics lag by making the main game thread sleep. /// Clearly not something there's a good reason to do *except* when @@ -361,7 +361,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("profileresult")] + [Function("profileresult", ReturnType = typeof(StringValue), IsInvariant = false)] public class ProfileResult : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -383,7 +383,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("makebuiltindelegate")] + [Function("makebuiltindelegate", ReturnType = typeof(BuiltinDelegate), IsInvariant = false)] public class MakeBuiltinDelegate : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -395,7 +395,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("droppriority")] + [Function("droppriority", ReturnType = null, IsInvariant = false)] public class AllowInterrupt : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/Persistence.cs b/src/kOS.Safe/Function/Persistence.cs index 59a248e96..90b1b4aa6 100644 --- a/src/kOS.Safe/Function/Persistence.cs +++ b/src/kOS.Safe/Function/Persistence.cs @@ -12,7 +12,7 @@ namespace kOS.Safe.Function * remove these function below as well any metions of delete/rename file/rename volume/copy from kRISC.tpg in the future. */ - [Function("copy_deprecated")] + [Function("copy_deprecated", ReturnType = null, IsInvariant = false)] public class FunctionCopyDeprecated : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -49,7 +49,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("rename_file_deprecated")] + [Function("rename_file_deprecated", ReturnType = null, IsInvariant = false)] public class FunctionRenameFileDeprecated : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -76,7 +76,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("rename_volume_deprecated")] + [Function("rename_volume_deprecated", ReturnType = null, IsInvariant = false)] public class FunctionRenameVolumeDeprecated : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -99,7 +99,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("delete_deprecated")] + [Function("delete_deprecated", ReturnType = null, IsInvariant = false)] public class FunctionDeleteDeprecated : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -127,7 +127,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("scriptpath")] + [Function("scriptpath", ReturnType = typeof(PathValue), IsInvariant = false)] public class FunctionScriptPath : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -141,7 +141,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("switch")] + [Function("switch", ReturnType = null, IsInvariant = false)] public class FunctionSwitch : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -164,7 +164,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("cd", "chdir")] + [Function("cd", "chdir", ReturnType = null, IsInvariant = false)] public class FunctionCd : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -198,7 +198,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("copypath")] + [Function("copypath", ReturnType = typeof(BooleanValue), IsInvariant = false)] public class FunctionCopyPath : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -214,7 +214,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("movepath")] + [Function("movepath", ReturnType = typeof(BooleanValue), IsInvariant = false)] public class FunctionMove : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -230,7 +230,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("deletepath")] + [Function("deletepath", ReturnType = typeof(BooleanValue), IsInvariant = false)] public class FunctionDeletePath : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -245,7 +245,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("writejson")] + [Function("writejson", ReturnType = typeof(VolumeFile), IsInvariant = false)] public class FunctionWriteJson : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -270,7 +270,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("readjson")] + [Function("readjson", ReturnType = typeof(Structure), IsInvariant = false)] public class FunctionReadJson : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -293,7 +293,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("exists")] + [Function("exists", ReturnType = typeof(BooleanValue), IsInvariant = false)] public class FunctionExists : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -308,7 +308,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("open")] + [Function("open", ReturnType = typeof(Structure), IsInvariant = false)] public class FunctionOpen : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -328,7 +328,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("create")] + [Function("create", ReturnType = typeof(VolumeFile), IsInvariant = false)] public class FunctionCreate : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -345,7 +345,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("createdir")] + [Function("createdir", ReturnType = typeof(VolumeDirectory), IsInvariant = false)] public class FunctionCreateDirectory : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/Suffixed.cs b/src/kOS.Safe/Function/Suffixed.cs index 3367b373d..ffe94b966 100644 --- a/src/kOS.Safe/Function/Suffixed.cs +++ b/src/kOS.Safe/Function/Suffixed.cs @@ -3,7 +3,7 @@ namespace kOS.Safe.Function { - [Function("range")] + [Function("range", ReturnType = typeof(RangeValue), IsInvariant = true)] public class FunctionRange : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -38,7 +38,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("constant")] + [Function("constant", ReturnType = typeof(ConstantValue), IsInvariant = true)] public class FunctionConstant : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Function/Trigonometry.cs b/src/kOS.Safe/Function/Trigonometry.cs index f8e53cd51..c362c9a13 100644 --- a/src/kOS.Safe/Function/Trigonometry.cs +++ b/src/kOS.Safe/Function/Trigonometry.cs @@ -1,10 +1,10 @@ -using System; +using System; using kOS.Safe.Encapsulation; using kOS.Safe.Utilities; namespace kOS.Safe.Function { - [Function("sin")] + [Function("sin", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionSin : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -17,7 +17,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("cos")] + [Function("cos", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionCos : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -30,7 +30,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("tan")] + [Function("tan", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionTan : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -43,7 +43,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("arcsin")] + [Function("arcsin", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionArcSin : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -55,7 +55,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("arccos")] + [Function("arccos", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionArcCos : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -67,7 +67,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("arctan")] + [Function("arctan", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionArcTan : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -79,7 +79,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("arctan2")] + [Function("arctan2", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionArcTan2 : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) @@ -92,7 +92,7 @@ public override void Execute(SafeSharedObjects shared) } } - [Function("anglediff")] + [Function("anglediff", ReturnType = typeof(ScalarDoubleValue), IsInvariant = true)] public class FunctionAngleDiff : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Persistence/PathValue.cs b/src/kOS.Safe/Persistence/PathValue.cs index 6c10d9c0c..2a53bac29 100644 --- a/src/kOS.Safe/Persistence/PathValue.cs +++ b/src/kOS.Safe/Persistence/PathValue.cs @@ -18,7 +18,7 @@ namespace kOS.Safe [kOS.Safe.Utilities.KOSNomenclature("Path")] public class PathValue : SerializableStructure { - [Function("path")] + [Function("path", ReturnType = typeof(PathValue), IsInvariant = false)] public class FunctionPath : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS.Safe/Persistence/Volume.cs b/src/kOS.Safe/Persistence/Volume.cs index d4127657f..4d22ac86f 100644 --- a/src/kOS.Safe/Persistence/Volume.cs +++ b/src/kOS.Safe/Persistence/Volume.cs @@ -10,7 +10,7 @@ namespace kOS.Safe.Persistence [kOS.Safe.Utilities.KOSNomenclature("Volume")] public abstract class Volume : Structure { - [Function("volume")] + [Function("volume", ReturnType = typeof(Volume), IsInvariant = false)] public class FunctionVolume : SafeFunctionBase { public override void Execute(SafeSharedObjects shared) diff --git a/src/kOS/AddOns/KerbalAlarmClock/KACFunctions.cs b/src/kOS/AddOns/KerbalAlarmClock/KACFunctions.cs index 30e1e6485..b9b4fb324 100644 --- a/src/kOS/AddOns/KerbalAlarmClock/KACFunctions.cs +++ b/src/kOS/AddOns/KerbalAlarmClock/KACFunctions.cs @@ -1,4 +1,4 @@ -using kOS.Function; +using kOS.Function; using kOS.Safe.Encapsulation; using kOS.Safe.Exceptions; using kOS.Safe.Function; @@ -8,7 +8,7 @@ namespace kOS.AddOns.KerbalAlarmClock { - [Function("addAlarm")] + [Function("addAlarm", ReturnType = typeof(Structure), IsInvariant = false)] public class FunctionAddAlarm : FunctionBase { public override void Execute(SharedObjects shared) @@ -66,12 +66,12 @@ public override void Execute(SharedObjects shared) } } - [Function("listAlarms")] + [Function("listAlarms", ReturnType = typeof(ListValue), IsInvariant = false)] public class FunctionListAlarms : FunctionBase { public override void Execute(SharedObjects shared) { - var list = new ListValue(); + var list = new ListValue(); string alarmTypes = PopValueAssert(shared).ToString(); AssertArgBottomAndConsume(shared); @@ -100,7 +100,7 @@ public override void Execute(SharedObjects shared) } } - [Function("deleteAlarm")] + [Function("deleteAlarm", ReturnType = typeof(BooleanValue), IsInvariant = false)] public class FunctionDeleteAlarm : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/BuildList.cs b/src/kOS/Function/BuildList.cs index 84376baff..3305a3ee5 100644 --- a/src/kOS/Function/BuildList.cs +++ b/src/kOS/Function/BuildList.cs @@ -9,7 +9,7 @@ namespace kOS.Function { - [Function("buildlist")] + [Function("buildlist", ReturnType = typeof(ListValue), IsInvariant = false)] public class FunctionBuildList : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/Math.cs b/src/kOS/Function/Math.cs index 8850e087e..ddf6b5998 100644 --- a/src/kOS/Function/Math.cs +++ b/src/kOS/Function/Math.cs @@ -1,4 +1,4 @@ -using System; +using System; using kOS.Safe.Compilation; using kOS.Safe.Function; using kOS.Suffixed; @@ -7,7 +7,7 @@ namespace kOS.Function { - [Function("vcrs", "vectorcrossproduct")] + [Function("vcrs", "vectorcrossproduct", ReturnType = typeof(Vector), IsInvariant = true)] public class FunctionVectorCross : FunctionBase { public override void Execute(SharedObjects shared) @@ -26,7 +26,7 @@ public override void Execute(SharedObjects shared) } } - [Function("vdot", "vectordotproduct")] + [Function("vdot", "vectordotproduct", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionVectorDot : FunctionBase { public override void Execute(SharedObjects shared) @@ -45,7 +45,7 @@ public override void Execute(SharedObjects shared) } } - [Function("vxcl", "vectorexclude")] + [Function("vxcl", "vectorexclude", ReturnType = typeof(Vector), IsInvariant = true)] public class FunctionVectorExclude : FunctionBase { public override void Execute(SharedObjects shared) @@ -64,7 +64,7 @@ public override void Execute(SharedObjects shared) } } - [Function("vang", "vectorangle")] + [Function("vang", "vectorangle", ReturnType = typeof(ScalarValue), IsInvariant = true)] public class FunctionVectorAngle : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/Misc.cs b/src/kOS/Function/Misc.cs index eb32c6619..e090c09b4 100644 --- a/src/kOS/Function/Misc.cs +++ b/src/kOS/Function/Misc.cs @@ -19,7 +19,7 @@ namespace kOS.Function { - [Function("clearscreen")] + [Function("clearscreen", ReturnType = null, IsInvariant = false)] public class FunctionClearScreen : FunctionBase { public override void Execute(SharedObjects shared) @@ -29,7 +29,7 @@ public override void Execute(SharedObjects shared) } } - [Function("hudtext")] + [Function("hudtext", ReturnType = null, IsInvariant = false)] public class FunctionHudText : FunctionBase { public override void Execute(SharedObjects shared) @@ -71,7 +71,7 @@ public override void Execute(SharedObjects shared) } } - [Function("stage")] + [Function("stage", ReturnType = null, IsInvariant = false)] public class FunctionStage : FunctionBase { public override void Execute(SharedObjects shared) @@ -93,7 +93,7 @@ public override void Execute(SharedObjects shared) } } - [Function("add")] + [Function("add", ReturnType = null, IsInvariant = false)] public class FunctionAddNode : FunctionBase { public override void Execute(SharedObjects shared) @@ -104,7 +104,7 @@ public override void Execute(SharedObjects shared) } } - [Function("remove")] + [Function("remove", ReturnType = null, IsInvariant = false)] public class FunctionRemoveNode : FunctionBase { public override void Execute(SharedObjects shared) @@ -115,7 +115,7 @@ public override void Execute(SharedObjects shared) } } - [Function("warpto")] + [Function("warpto", ReturnType = null, IsInvariant = false)] public class WarpTo : FunctionBase { public override void Execute(SharedObjects shared) @@ -137,7 +137,7 @@ public override void Execute(SharedObjects shared) } } - [Function("processor")] + [Function("processor", ReturnType = typeof(PartModuleFields), IsInvariant = false)] public class FunctionProcessor : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/Persistence.cs b/src/kOS/Function/Persistence.cs index 4b2129974..8b1ef6e50 100644 --- a/src/kOS/Function/Persistence.cs +++ b/src/kOS/Function/Persistence.cs @@ -1,4 +1,4 @@ -using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation; using kOS.Safe.Exceptions; using kOS.Safe.Function; using kOS.Safe.Persistence; @@ -13,7 +13,7 @@ namespace kOS.Function { - [Function("edit")] + [Function("edit", ReturnType = null, IsInvariant = false)] public class FunctionEdit : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/PrintList.cs b/src/kOS/Function/PrintList.cs index 0667db142..e36d88ba6 100644 --- a/src/kOS/Function/PrintList.cs +++ b/src/kOS/Function/PrintList.cs @@ -16,7 +16,7 @@ namespace kOS.Function { - [Function("printlist")] + [Function("printlist", ReturnType = null, IsInvariant = false)] public class FunctionPrintList : FunctionBase { public override void Execute(SharedObjects shared) diff --git a/src/kOS/Function/Suffixed.cs b/src/kOS/Function/Suffixed.cs index abf577fe2..e918f251e 100644 --- a/src/kOS/Function/Suffixed.cs +++ b/src/kOS/Function/Suffixed.cs @@ -15,7 +15,7 @@ namespace kOS.Function { - [Function("node")] + [Function("node", ReturnType = typeof(Node), IsInvariant = false)] public class FunctionNode : FunctionBase { public override void Execute(SharedObjects shared) @@ -40,7 +40,7 @@ public override void Execute(SharedObjects shared) } } - [Function("v")] + [Function("v", ReturnType = typeof(Vector), IsInvariant = true)] public class FunctionVector : FunctionBase { public override void Execute(SharedObjects shared) @@ -55,7 +55,7 @@ public override void Execute(SharedObjects shared) } } - [Function("r")] + [Function("r", ReturnType = typeof(Direction), IsInvariant = true)] public class FunctionRotation : FunctionBase { public override void Execute(SharedObjects shared) @@ -70,7 +70,7 @@ public override void Execute(SharedObjects shared) } } - [Function("q")] + [Function("q", ReturnType = typeof(Direction), IsInvariant = true)] public class FunctionQuaternion : FunctionBase { public override void Execute(SharedObjects shared) @@ -86,7 +86,7 @@ public override void Execute(SharedObjects shared) } } - [Function("createOrbit")] + [Function("createOrbit", ReturnType = typeof(OrbitInfo), IsInvariant = false)] public class FunctionCreateOrbit : FunctionBase { public override void Execute(SharedObjects shared) @@ -131,7 +131,7 @@ public override void Execute(SharedObjects shared) } } - [Function("rotatefromto")] + [Function("rotatefromto", ReturnType = typeof(Direction), IsInvariant = true)] public class FunctionRotateFromTo : FunctionBase { public override void Execute(SharedObjects shared) @@ -145,7 +145,7 @@ public override void Execute(SharedObjects shared) } } - [Function("lookdirup")] + [Function("lookdirup", ReturnType = typeof(Direction), IsInvariant = true)] public class FunctionLookDirUp : FunctionBase { public override void Execute(SharedObjects shared) @@ -159,7 +159,7 @@ public override void Execute(SharedObjects shared) } } - [Function("angleaxis")] + [Function("angleaxis", ReturnType = typeof(Direction), IsInvariant = true)] public class FunctionAngleAxis : FunctionBase { public override void Execute(SharedObjects shared) @@ -173,7 +173,7 @@ public override void Execute(SharedObjects shared) } } - [Function("latlng")] + [Function("latlng", ReturnType = typeof(GeoCoordinates), IsInvariant = false)] public class FunctionLatLng : FunctionBase { public override void Execute(SharedObjects shared) @@ -187,7 +187,7 @@ public override void Execute(SharedObjects shared) } } - [Function("vessel")] + [Function("vessel", ReturnType = typeof(VesselTarget), IsInvariant = false)] public class FunctionVessel : FunctionBase { public override void Execute(SharedObjects shared) @@ -199,7 +199,7 @@ public override void Execute(SharedObjects shared) } } - [Function("body")] + [Function("body", ReturnType = typeof(BodyTarget), IsInvariant = false)] public class FunctionBody : FunctionBase { public override void Execute(SharedObjects shared) @@ -211,7 +211,7 @@ public override void Execute(SharedObjects shared) } } - [Function("bodyexists")] + [Function("bodyexists", ReturnType = typeof(BooleanValue), IsInvariant = false)] public class FunctionBodyExists : FunctionBase { public override void Execute(SharedObjects shared) @@ -222,7 +222,7 @@ public override void Execute(SharedObjects shared) } } - [Function("bodyatmosphere")] + [Function("bodyatmosphere", ReturnType = typeof(BodyAtmosphere), IsInvariant = false)] public class FunctionBodyAtmosphere : FunctionBase { public override void Execute(SharedObjects shared) @@ -237,7 +237,7 @@ public override void Execute(SharedObjects shared) } } - [Function("bounds")] + [Function("bounds", ReturnType = typeof(BoundsValue), IsInvariant = false)] public class FunctionBounds : FunctionBase { public override void Execute(SharedObjects shared) @@ -252,7 +252,7 @@ public override void Execute(SharedObjects shared) } } - [Function("heading")] + [Function("heading", ReturnType = typeof(Direction), IsInvariant = false)] public class FunctionHeading : FunctionBase { public override void Execute(SharedObjects shared) @@ -274,7 +274,7 @@ public override void Execute(SharedObjects shared) } } - [Function("slidenote")] + [Function("slidenote", ReturnType = typeof(NoteValue), IsInvariant = true)] public class FunctionSlideNote : FunctionBase { public override void Execute(SharedObjects shared) @@ -304,7 +304,7 @@ public override void Execute(SharedObjects shared) } } - [Function("note")] + [Function("note", ReturnType = typeof(NoteValue), IsInvariant = true)] public class FunctionNote : FunctionBase { public override void Execute(SharedObjects shared) @@ -333,7 +333,7 @@ public override void Execute(SharedObjects shared) } } - [Function("GetVoice")] + [Function("GetVoice", ReturnType = typeof(VoiceValue), IsInvariant = false)] public class FunctionGetVoice : FunctionBase { public override void Execute(SharedObjects shared) @@ -354,7 +354,7 @@ public override void Execute(SharedObjects shared) } - [Function("StopAllVoices")] + [Function("StopAllVoices", ReturnType = null, IsInvariant = false)] public class FunctionStopAllVoices : FunctionBase { public override void Execute(SharedObjects shared) @@ -362,7 +362,7 @@ public override void Execute(SharedObjects shared) shared.SoundMaker.StopAllVoices(); } } - [Function("timestamp", "time")] + [Function("timestamp", "time", ReturnType = typeof(TimeStamp), IsInvariant = false)] public class FunctionTimeStamp : FunctionBase { // Note: "TIME" is both a bound variable AND a built-in function now. @@ -426,7 +426,7 @@ public override void Execute(SharedObjects shared) } } - [Function("timespan")] + [Function("timespan", ReturnType = typeof(Suffixed.TimeSpan), IsInvariant = true)] public class FunctionTimeSpan : FunctionBase { public override void Execute(SharedObjects shared) @@ -477,7 +477,7 @@ public override void Execute(SharedObjects shared) } } - [Function("hsv")] + [Function("hsv", ReturnType = typeof(HsvaColor), IsInvariant = true)] public class FunctionHsv : FunctionBase { public override void Execute(SharedObjects shared) @@ -490,7 +490,7 @@ public override void Execute(SharedObjects shared) } } - [Function("hsva")] + [Function("hsva", ReturnType = typeof(HsvaColor), IsInvariant = true)] public class FunctionHsva : FunctionBase { public override void Execute(SharedObjects shared) @@ -504,7 +504,7 @@ public override void Execute(SharedObjects shared) } } - [Function("rgb")] + [Function("rgb", ReturnType = typeof(RgbaColor), IsInvariant = true)] public class FunctionRgb : FunctionBase { public override void Execute(SharedObjects shared) @@ -517,7 +517,7 @@ public override void Execute(SharedObjects shared) } } - [Function("rgba")] + [Function("rgba", ReturnType = typeof(RgbaColor), IsInvariant = true)] public class FunctionRgba : FunctionBase { public override void Execute(SharedObjects shared) @@ -539,7 +539,7 @@ public override void Execute(SharedObjects shared) // Note: vecdraw now counts the args and changes its behavior accordingly. // For backward compatibility, vecdrawargs has been aliased to vecdraw. // - [Function("vecdraw", "vecdrawargs")] + [Function("vecdraw", "vecdrawargs", ReturnType = typeof(VectorRenderer), IsInvariant = false)] public class FunctionVecDrawNull : FunctionBase { protected RgbaColor GetDefaultColor() @@ -629,7 +629,7 @@ public void DoExecuteWork( } } - [Function("clearvecdraws")] + [Function("clearvecdraws", ReturnType = null, IsInvariant = false)] public class FunctionHideAllVecdraws : FunctionBase { public override void Execute(SharedObjects shared) @@ -639,7 +639,7 @@ public override void Execute(SharedObjects shared) } } - [Function("clearguis")] + [Function("clearguis", ReturnType = null, IsInvariant = false)] public class FunctionClearAllGuis : FunctionBase { public override void Execute(SharedObjects shared) @@ -649,7 +649,7 @@ public override void Execute(SharedObjects shared) } } - [Function("gui")] + [Function("gui", ReturnType = typeof(GUIWidgets), IsInvariant = false)] public class FunctionWidgets : FunctionBase { public override void Execute(SharedObjects shared) @@ -662,7 +662,7 @@ public override void Execute(SharedObjects shared) } } - [Function("positionat")] + [Function("positionat", ReturnType = typeof(Vector), IsInvariant = false)] public class FunctionPositionAt : FunctionBase { public override void Execute(SharedObjects shared) @@ -675,7 +675,7 @@ public override void Execute(SharedObjects shared) } } - [Function("velocityat")] + [Function("velocityat", ReturnType = typeof(Vector), IsInvariant = false)] public class FunctionVelocityAt : FunctionBase { public override void Execute(SharedObjects shared) @@ -688,7 +688,7 @@ public override void Execute(SharedObjects shared) } } - [Function("highlight")] + [Function("highlight", ReturnType = typeof(HighlightStructure), IsInvariant = false)] public class FunctionHightlight : FunctionBase { public override void Execute(SharedObjects shared) @@ -701,7 +701,7 @@ public override void Execute(SharedObjects shared) } } - [Function("orbitat")] + [Function("orbitat", ReturnType = typeof(OrbitInfo), IsInvariant = false)] public class FunctionOrbitAt : FunctionBase { public override void Execute(SharedObjects shared) @@ -714,7 +714,7 @@ public override void Execute(SharedObjects shared) } } - [Function("career")] + [Function("career", ReturnType = typeof(Career), IsInvariant = false)] public class FunctionCareer : FunctionBase { public override void Execute(SharedObjects shared) @@ -724,7 +724,7 @@ public override void Execute(SharedObjects shared) } } - [Function("allwaypoints")] + [Function("allwaypoints", ReturnType = typeof(ListValue), IsInvariant = false)] public class FunctionAllWaypoints : FunctionBase { public override void Execute(SharedObjects shared) @@ -732,7 +732,7 @@ public override void Execute(SharedObjects shared) AssertArgBottomAndConsume(shared); // no args // ReSharper disable SuggestUseVarKeywordEvident - ListValue returnList = new ListValue(); + ListValue returnList = new ListValue(); // ReSharper enable SuggestUseVarKeywordEvident WaypointManager wpm = WaypointManager.Instance(); @@ -758,7 +758,7 @@ public override void Execute(SharedObjects shared) } } - [Function("waypoint")] + [Function("waypoint", ReturnType = typeof(WaypointValue), IsInvariant = false)] public class FunctionWaypoint : FunctionBase { public override void Execute(SharedObjects shared) @@ -805,7 +805,7 @@ public override void Execute(SharedObjects shared) } } - [Function("transferall")] + [Function("transferall", ReturnType = typeof(ResourceTransferValue), IsInvariant = false)] public class FunctionTransferAll : FunctionBase { public override void Execute(SharedObjects shared) @@ -828,7 +828,7 @@ public override void Execute(SharedObjects shared) } - [Function("transfer")] + [Function("transfer", ReturnType = typeof(ResourceTransferValue), IsInvariant = false)] public class FunctionTransfer : FunctionBase { public override void Execute(SharedObjects shared) From 8faf6143894cb26a18973d291bf329a97228f1db Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 16 Mar 2026 21:02:20 -0400 Subject: [PATCH 55/56] Fix Structure not counting as a valid return type. --- src/kOS.Safe/Function/FunctionAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kOS.Safe/Function/FunctionAttribute.cs b/src/kOS.Safe/Function/FunctionAttribute.cs index 1c878c858..ebe06e739 100644 --- a/src/kOS.Safe/Function/FunctionAttribute.cs +++ b/src/kOS.Safe/Function/FunctionAttribute.cs @@ -11,7 +11,7 @@ public Type ReturnType get => returnType; set { - if (value != null && !value.IsSubclassOf(typeof(Encapsulation.Structure))) + if (value != null && !typeof(Encapsulation.Structure).IsAssignableFrom(value)) { #if DEBUG throw new ArgumentException($"{value} is not an accepted return type for a kOS function since it does not subclass {typeof(Encapsulation.Structure)}."); From 9402dce674e86256bc2d8bc394696faf0f78b2ce Mon Sep 17 00:00:00 2001 From: DBooots Date: Mon, 16 Mar 2026 21:04:17 -0400 Subject: [PATCH 56/56] Add methods to check if functions are invariant and to get their return type. --- src/kOS.Safe/Function/FunctionManager.cs | 26 +++++++++++++++++++++-- src/kOS.Safe/Function/IFunctionManager.cs | 2 ++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/kOS.Safe/Function/FunctionManager.cs b/src/kOS.Safe/Function/FunctionManager.cs index 2c0043fb6..443f2a55c 100644 --- a/src/kOS.Safe/Function/FunctionManager.cs +++ b/src/kOS.Safe/Function/FunctionManager.cs @@ -10,7 +10,9 @@ namespace kOS.Safe.Function public class FunctionManager : IFunctionManager { private readonly SafeSharedObjects shared; - private Dictionary functions; + private readonly Dictionary functions = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary functionTypes = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly HashSet invariantFunctions = new HashSet(); private static readonly Dictionary rawAttributes = new Dictionary(); public FunctionManager(SafeSharedObjects shared) @@ -21,7 +23,10 @@ public FunctionManager(SafeSharedObjects shared) public void Load() { - functions = new Dictionary(StringComparer.OrdinalIgnoreCase); + functions.Clear(); + functionTypes.Clear(); + invariantFunctions.Clear(); + foreach (FunctionAttribute attr in rawAttributes.Keys) { var type = rawAttributes[attr]; @@ -32,6 +37,9 @@ public void Load() if (functionName != string.Empty) { functions.Add(functionName, (SafeFunctionBase)functionObject); + functionTypes.Add(functionName, attr.ReturnType); + if (attr.IsInvariant) + invariantFunctions.Add(functionName); } } } @@ -70,5 +78,19 @@ public bool Exists(string functionName) { return functions.ContainsKey(functionName); } + + public bool IsFunctionInvariant(string functionName) + { + return invariantFunctions.Contains(functionName); + } + + public Type FunctionReturnType(string functionName) + { + if (!functions.ContainsKey(functionName)) + { + throw new Exception("Queried return type of a non-existent function " + functionName); + } + return functionTypes[functionName]; + } } } \ No newline at end of file diff --git a/src/kOS.Safe/Function/IFunctionManager.cs b/src/kOS.Safe/Function/IFunctionManager.cs index 1a243698f..54bd58d46 100644 --- a/src/kOS.Safe/Function/IFunctionManager.cs +++ b/src/kOS.Safe/Function/IFunctionManager.cs @@ -5,5 +5,7 @@ public interface IFunctionManager void Load(); void CallFunction(string functionName); bool Exists(string functionName); + bool IsFunctionInvariant(string functionName); + System.Type FunctionReturnType(string functionName); } } \ No newline at end of file