diff --git a/neo b/neo index d3c91750c..74562d57f 160000 --- a/neo +++ b/neo @@ -1 +1 @@ -Subproject commit d3c91750cc64f16bacecec0eaebd52cf1a35f882 +Subproject commit 74562d57ff9b4e20968dd2bbfcfc9cfa66e5f08a diff --git a/src/Neo.Compiler.CSharp/Optimizer/Analysers/BasicBlock.cs b/src/Neo.Compiler.CSharp/Optimizer/Analysers/BasicBlock.cs new file mode 100644 index 000000000..4e274dc16 --- /dev/null +++ b/src/Neo.Compiler.CSharp/Optimizer/Analysers/BasicBlock.cs @@ -0,0 +1,14 @@ +using Neo.Json; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; +using Neo.VM; +using System.Collections.Generic; + +namespace Neo.Optimizer +{ + static class BasicBlock + { + public static Dictionary> FindBasicBlocks(NefFile nef, ContractManifest manifest, JToken debugInfo) + => new InstructionCoverage(nef, manifest, debugInfo).basicBlocks; + } +} diff --git a/src/Neo.Compiler.CSharp/Optimizer/Analysers/InstructionCoverage.cs b/src/Neo.Compiler.CSharp/Optimizer/Analysers/InstructionCoverage.cs new file mode 100644 index 000000000..40676c903 --- /dev/null +++ b/src/Neo.Compiler.CSharp/Optimizer/Analysers/InstructionCoverage.cs @@ -0,0 +1,269 @@ +using Neo.Json; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; +using Neo.VM; +using System; +using System.Collections.Generic; +using System.Linq; +using static Neo.Optimizer.JumpTarget; +using static Neo.Optimizer.OpCodeTypes; + +namespace Neo.Optimizer +{ + public enum TryStackType + { + ENTRY, + TRY, + CATCH, + FINALLY, + } + + public enum BranchType + { + OK, // One of the branches may return without exception + THROW, // All branches surely have exceptions, but can be catched + ABORT, // All branches abort, and cannot be catched + UNCOVERED, + } + + public class InstructionCoverage + { + Script script; + // Starting from the address, whether the call will surely throw or surely abort, or may be OK + public Dictionary coveredMap { get; protected set; } + public Dictionary> basicBlocks { get; protected set; } + public List<(int a, Instruction i)> addressAndInstructions { get; init; } + public Dictionary> jumpTargetToSources { get; init; } + public InstructionCoverage(NefFile nef, ContractManifest manifest, JToken debugInfo) + { + this.script = nef.Script; + coveredMap = new(); + basicBlocks = new(); + addressAndInstructions = script.EnumerateInstructions().ToList(); + (_, _, jumpTargetToSources) = FindAllJumpAndTrySourceToTargets(addressAndInstructions); + foreach ((int addr, Instruction _) in addressAndInstructions) + coveredMap.Add(addr, BranchType.UNCOVERED); + + // It is unsafe to go parallel, because the coveredMap value is not true/false + //Parallel.ForEach(manifest.Abi.Methods, method => + // CoverInstruction(method.Offset, script, coveredMap) + //); + foreach ((int addr, _) in EntryPoint.EntryPointsByMethod(manifest, debugInfo)) + CoverInstruction(addr); + } + + public static Stack<((int returnAddr, int finallyAddr), TryStackType stackType)> CopyStack + (Stack<((int returnAddr, int finallyAddr), TryStackType stackType)> stack) => new(stack.Reverse()); + + public BranchType HandleThrow(int entranceAddr, int addr, Stack<((int catchAddr, int finallyAddr), TryStackType stackType)> stack) + { + stack = CopyStack(stack); + TryStackType stackType; + int catchAddr; int finallyAddr; + do + ((catchAddr, finallyAddr), stackType) = stack.Pop(); + while (stackType != TryStackType.TRY && stackType != TryStackType.CATCH && stack.Count > 0); + if (stackType == TryStackType.TRY) // goto CATCH or FINALLY + { + // try with catch: cancel throw and execute catch + if (catchAddr != -1) + { + addr = catchAddr; + stack.Push(((-1, finallyAddr), TryStackType.CATCH)); + coveredMap[entranceAddr] = CoverInstruction(addr, stack: stack, throwed: false); + return coveredMap[entranceAddr]; + } + // try without catch: execute finally but keep throwing + else if (finallyAddr != -1) + { + coveredMap[addr] = BranchType.THROW; + addr = finallyAddr; + stack.Push(((-1, -1), TryStackType.FINALLY)); + coveredMap[entranceAddr] = CoverInstruction(addr, stack: stack, throwed: true); + return coveredMap[entranceAddr]; + } + } + // throwed in catch with finally: execute finally but keep throwing + if (stackType == TryStackType.CATCH) + { + if (finallyAddr != -1) + { + addr = finallyAddr; + stack.Push(((-1, -1), TryStackType.FINALLY)); + } + return CoverInstruction(addr, stack: stack, throwed: true); + } + // not in try and not in catch + coveredMap[entranceAddr] = BranchType.THROW; + return BranchType.THROW; + } + + public BranchType HandleAbort(int entranceAddr, int addr, Stack<((int returnAddr, int finallyAddr), TryStackType stackType)> stack) + { + // See if we are in a try or catch. There may still be runtime exceptions + ((int catchAddr, int finallyAddr), TryStackType stackType) = stack.Peek(); + if (stackType == TryStackType.TRY && catchAddr != -1 || + stackType == TryStackType.CATCH && finallyAddr != -1) + { + // Visit catchAddr because there may still be exceptions at runtime + if (HandleThrow(entranceAddr, addr, stack) == BranchType.OK) + { + coveredMap[entranceAddr] = BranchType.OK; + return BranchType.OK; + } + } + coveredMap[entranceAddr] = BranchType.ABORT; + return coveredMap[entranceAddr]; + } + + /// + /// Cover a basic block, and recursively cover all branches + /// + /// + /// + /// + /// Whether it is possible to return without exception + /// + /// + public BranchType CoverInstruction(int addr, + Stack<((int returnAddr, int finallyAddr), TryStackType stackType)>? stack = null, + bool throwed = false) + { + int entranceAddr = addr; + if (stack == null) + { + stack = new(); + stack.Push(((-1, -1), TryStackType.ENTRY)); + } + else + stack = CopyStack(stack); + if (throwed) + { + ((int catchAddr, int finallyAddr), TryStackType stackType) = stack.Peek(); + if (stackType != TryStackType.FINALLY) + { + coveredMap[entranceAddr] = BranchType.THROW; + return BranchType.THROW; + } + } + while (true) + { + // For the analysis of basic blocks, + // we launched new recursion when exception is catched. + // Here we have the exception not catched + if (!coveredMap.ContainsKey(addr)) + throw new BadScriptException($"wrong address {addr}"); + if (coveredMap[addr] != BranchType.UNCOVERED) + // We have visited the code. Skip it. + return coveredMap[addr]; + if (jumpTargetToSources.ContainsKey(addr) && addr != entranceAddr) + // on target of jump, start a new recursion to split basic blocks + return CoverInstruction(addr, stack, throwed); + Instruction instruction = script.GetInstruction(addr); + if (instruction.OpCode != OpCode.NOP) + { + coveredMap[addr] = BranchType.OK; + // Add a basic block starting from entranceAddr + if (!basicBlocks.TryGetValue(entranceAddr, out Dictionary? instructions)) + { + instructions = new Dictionary(); + basicBlocks.Add(entranceAddr, instructions); + } + // Add this instruction to the basic block starting from entranceAddr + instructions.Add(addr, instruction); + } + + // TODO: ABORTMSG may THROW instead of ABORT. Just throw new NotImplementedException for ABORTMSG? + if (instruction.OpCode == OpCode.ABORT || instruction.OpCode == OpCode.ABORTMSG) + return HandleAbort(entranceAddr, addr, stack); + if (callWithJump.Contains(instruction.OpCode)) + { + int callTarget = ComputeJumpTarget(addr, instruction); + BranchType returnedType = CoverInstruction(callTarget); + if (returnedType == BranchType.OK) + return CoverInstruction(addr + instruction.Size, stack); + if (returnedType == BranchType.ABORT) + return HandleAbort(entranceAddr, addr, stack); + if (returnedType == BranchType.THROW) + return HandleThrow(entranceAddr, addr, stack); + } + if (instruction.OpCode == OpCode.RET) + { + // See if we are in a try. There may still be runtime exceptions + HandleThrow(entranceAddr, addr, stack); + coveredMap[entranceAddr] = BranchType.OK; + return coveredMap[entranceAddr]; + } + if (tryThrowFinally.Contains(instruction.OpCode)) + { + if (instruction.OpCode == OpCode.TRY || instruction.OpCode == OpCode.TRY_L) + { + stack.Push((ComputeTryTarget(addr, instruction), TryStackType.TRY)); + return CoverInstruction(addr + instruction.Size, stack); + } + if (instruction.OpCode == OpCode.THROW) + return HandleThrow(entranceAddr, addr, stack); + if (instruction.OpCode == OpCode.ENDTRY || instruction.OpCode == OpCode.ENDTRY_L) + { + ((int catchAddr, int finallyAddr), TryStackType stackType) = stack.Peek(); + if (stackType != TryStackType.TRY && stackType != TryStackType.CATCH) + throw new BadScriptException("No try stack on ENDTRY"); + + // Visit catchAddr and finallyAddr because there may still be exceptions at runtime + HandleThrow(entranceAddr, addr, stack); + coveredMap[entranceAddr] = BranchType.OK; + + stack.Pop(); + int endPointer = ComputeJumpTarget(addr, instruction); + if (finallyAddr != -1) + { + stack.Push(((-1, endPointer), TryStackType.FINALLY)); + addr = finallyAddr; + } + else + addr = endPointer; + return CoverInstruction(addr, stack, throwed); + } + if (instruction.OpCode == OpCode.ENDFINALLY) + { + ((int catchAddr, int finallyAddr), TryStackType stackType) = stack.Pop(); + if (stackType != TryStackType.FINALLY) + throw new BadScriptException("No finally stack on ENDFINALLY"); + if (throwed) + { + // For this basic block in finally, the branch type is OK + coveredMap[entranceAddr] = BranchType.OK; + // The throw is caused by previous codes + return BranchType.THROW; + } + return CoverInstruction(addr + instruction.Size, stack, false); + } + } + if (unconditionalJump.Contains(instruction.OpCode)) + //addr = ComputeJumpTarget(addr, instruction); + //continue; + // For the analysis of basic blocks, we launch a new recursion + return CoverInstruction(ComputeJumpTarget(addr, instruction), stack, throwed); + if (conditionalJump.Contains(instruction.OpCode) || conditionalJump_L.Contains(instruction.OpCode)) + { + BranchType noJump = CoverInstruction(addr + instruction.Size, stack); + BranchType jump = CoverInstruction(ComputeJumpTarget(addr, instruction), stack); + if (noJump == BranchType.OK || jump == BranchType.OK) + { + // See if we are in a try. There may still be runtime exceptions + HandleThrow(entranceAddr, addr, stack); + coveredMap[entranceAddr] = BranchType.OK; + return coveredMap[entranceAddr]; + } + if (noJump == BranchType.ABORT && jump == BranchType.ABORT) + return HandleAbort(entranceAddr, addr, stack); + if (noJump == BranchType.THROW || jump == BranchType.THROW) // THROW, ABORT => THROW + return HandleThrow(entranceAddr, addr, stack); + throw new Exception($"Unknown {nameof(BranchType)} {noJump} {jump}"); + } + + addr += instruction.Size; + } + } + } +} diff --git a/src/Neo.Compiler.CSharp/Optimizer/Analysers/JumpTarget.cs b/src/Neo.Compiler.CSharp/Optimizer/Analysers/JumpTarget.cs index b8f5b92a9..20c66a197 100644 --- a/src/Neo.Compiler.CSharp/Optimizer/Analysers/JumpTarget.cs +++ b/src/Neo.Compiler.CSharp/Optimizer/Analysers/JumpTarget.cs @@ -56,7 +56,7 @@ public static (int catchTarget, int finallyTarget) ComputeTryTarget(int addr, In public static (Dictionary, Dictionary, - Dictionary>) + Dictionary>) FindAllJumpAndTrySourceToTargets(NefFile nef) { Script script = nef.Script; @@ -64,12 +64,12 @@ public static (Dictionary, } public static (Dictionary, Dictionary, - Dictionary>) + Dictionary>) FindAllJumpAndTrySourceToTargets(Script script) => FindAllJumpAndTrySourceToTargets(script.EnumerateInstructions().ToList()); public static ( Dictionary, // jump source to target Dictionary, // try source to targets - Dictionary> // target to source + Dictionary> // target to source ) FindAllJumpAndTrySourceToTargets(List<(int, Instruction)> addressAndInstructionsList) { @@ -78,33 +78,40 @@ public static ( addressToInstruction.Add(a, i); Dictionary jumpSourceToTargets = new(); Dictionary trySourceToTargets = new(); - Dictionary> targetToSources = new(); + Dictionary> targetToSources = new(); foreach ((int a, Instruction i) in addressAndInstructionsList) { if (SingleJumpInOperand(i)) { - Instruction target = addressToInstruction[ComputeJumpTarget(a, i)]; + int targetAddr = ComputeJumpTarget(a, i); + Instruction target = addressToInstruction[targetAddr]; jumpSourceToTargets.TryAdd(i, target); - if (!targetToSources.TryGetValue(target, out HashSet? sources)) sources = new(); - sources.Add(i); + if (!targetToSources.TryGetValue(targetAddr, out HashSet? sources)) + { + sources = new(); + targetToSources.Add(targetAddr, sources); + } + sources.Add(a); } - if (i.OpCode == TRY) + if (i.OpCode == TRY || i.OpCode == TRY_L) { - (Instruction t1, Instruction t2) = (addressToInstruction[a + i.TokenI8], addressToInstruction[a + i.TokenI8_1]); + (int a1, int a2) = i.OpCode == TRY ? + (a + i.TokenI8, a + i.TokenI8_1) : + (a + i.TokenI32, a + i.TokenI32_1); + (Instruction t1, Instruction t2) = (addressToInstruction[a1], addressToInstruction[a2]); trySourceToTargets.TryAdd(i, (t1, t2)); - if (!targetToSources.TryGetValue(t1, out HashSet? sources1)) sources1 = new(); - sources1.Add(i); - if (!targetToSources.TryGetValue(t2, out HashSet? sources2)) sources2 = new(); - sources2.Add(i); - } - if (i.OpCode == TRY_L) - { - (Instruction t1, Instruction t2) = (addressToInstruction[a + i.TokenI32], addressToInstruction[a + i.TokenI32_1]); - trySourceToTargets.TryAdd(i, (t1, t2)); - if (!targetToSources.TryGetValue(t1, out HashSet? sources1)) sources1 = new(); - sources1.Add(i); - if (!targetToSources.TryGetValue(t2, out HashSet? sources2)) sources2 = new(); - sources2.Add(i); + if (!targetToSources.TryGetValue(a1, out HashSet? sources1)) + { + sources1 = new(); + targetToSources.Add(a1, sources1); + } + sources1.Add(a); + if (!targetToSources.TryGetValue(a1, out HashSet? sources2)) + { + sources2 = new(); + targetToSources.Add(a2, sources2); + } + sources2.Add(a); } } return (jumpSourceToTargets, trySourceToTargets, targetToSources); diff --git a/src/Neo.Compiler.CSharp/Optimizer/Strategies/Reachability.cs b/src/Neo.Compiler.CSharp/Optimizer/Strategies/Reachability.cs index 811ead845..8783bf263 100644 --- a/src/Neo.Compiler.CSharp/Optimizer/Strategies/Reachability.cs +++ b/src/Neo.Compiler.CSharp/Optimizer/Strategies/Reachability.cs @@ -18,25 +18,8 @@ static class Reachability #pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. private static readonly Regex RangeRegex = new(@"(\d+)\-(\d+)", RegexOptions.Compiled); private static readonly Regex SequencePointRegex = new(@"(\d+)(\[\d+\]\d+\:\d+\-\d+\:\d+)", RegexOptions.Compiled); - private static readonly Regex DocumentRegex = new(@"\[(\d+)\](\d+)\:(\d+)\-(\d+)\:(\d+)", RegexOptions.Compiled); #pragma warning restore SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. - public enum TryStack - { - ENTRY, - TRY, - CATCH, - FINALLY, - } - - public enum BranchType - { - OK, // One of the branches may return without exception - THROW, // All branches surely has exceptions, but can be catched - ABORT, // All branches abort, and cannot be catched - UNCOVERED, - } - [Strategy(Priority = int.MaxValue)] public static (NefFile, ContractManifest, JObject) RemoveUncoveredInstructions(NefFile nef, ContractManifest manifest, JObject debugInfo) { @@ -76,19 +59,24 @@ public static (NefFile, ContractManifest, JObject) RemoveUncoveredInstructions(N int delta; if (simplifiedInstructionsToAddress.Contains(dst)) // target instruction not deleted delta = (int)simplifiedInstructionsToAddress[dst]! - a; - else if (i.OpCode == OpCode.PUSHA) + else if (i.OpCode == OpCode.PUSHA || i.OpCode == OpCode.ENDTRY || i.OpCode == OpCode.ENDTRY_L) delta = 0; // TODO: decide a good target else - throw new BadScriptException($"Target instruction of {i.OpCode} at address {a} is deleted"); + { + foreach ((int oldAddress, Instruction oldInstruction) in oldAddressAndInstructionsList) + if (oldInstruction == i) + throw new BadScriptException($"Target instruction of {i.OpCode} at old address {oldAddress} is deleted"); + throw new BadScriptException($"Target instruction of {i.OpCode} at new address {a} is deleted"); + } if (i.OpCode == OpCode.JMP || conditionalJump.Contains(i.OpCode) || i.OpCode == OpCode.CALL || i.OpCode == OpCode.ENDTRY) simplifiedScript.Add(BitConverter.GetBytes(delta)[0]); if (i.OpCode == OpCode.PUSHA || i.OpCode == OpCode.JMP_L || conditionalJump_L.Contains(i.OpCode) || i.OpCode == OpCode.CALL_L || i.OpCode == OpCode.ENDTRY_L) simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(delta)).ToList(); continue; } - if (tryInstructionSourceToTargets.ContainsKey(i)) + if (tryInstructionSourceToTargets.TryGetValue(i, out (Instruction dst1, Instruction dst2) dsts)) { - (Instruction dst1, Instruction dst2) = tryInstructionSourceToTargets[i]; + (Instruction dst1, Instruction dst2) = (dsts.dst1, dsts.dst2); (int delta1, int delta2) = ((int)simplifiedInstructionsToAddress[dst1]! - a, (int)simplifiedInstructionsToAddress[dst2]! - a); if (i.OpCode == OpCode.TRY) { @@ -161,249 +149,6 @@ public static (NefFile, ContractManifest, JObject) RemoveUncoveredInstructions(N public static Dictionary FindCoveredInstructions(NefFile nef, ContractManifest manifest, JToken debugInfo) - { - Script script = nef.Script; - Dictionary coveredMap = new(); - foreach ((int addr, Instruction _) in script.EnumerateInstructions()) - coveredMap.Add(addr, BranchType.UNCOVERED); - - // It is unsafe to go parallel, because the coveredMap value is not true/false - //Parallel.ForEach(manifest.Abi.Methods, method => - // CoverInstruction(method.Offset, script, coveredMap) - //); - foreach ((int addr, _) in EntryPoint.EntryPointsByMethod(manifest, debugInfo)) - CoverInstruction(addr, script, coveredMap); - return coveredMap; - } - - /// - /// - /// - /// - /// - /// - /// Whether it is possible to return without exception - /// - /// - public static BranchType CoverInstruction(int addr, Script script, Dictionary coveredMap, Stack<((int returnAddr, int finallyAddr), TryStack stackType)>? stack = null, bool throwed = false) - { - int entranceAddr = addr; - stack ??= new(); - if (stack.Count == 0) - stack.Push(((-1, -1), TryStack.ENTRY)); - while (stack.Count > 0) - { - if (!throwed) - goto HANDLE_NORMAL_CASE; - HANDLE_THROW: - throwed = true; - TryStack stackType; - int catchAddr; int finallyAddr; - do - ((catchAddr, finallyAddr), stackType) = stack.Pop(); - while (stackType != TryStack.TRY && stackType != TryStack.CATCH && stack.Count > 0); - if (stackType == TryStack.TRY) // goto CATCH or FINALLY - { - throwed = false; - if (catchAddr != -1) - { - addr = catchAddr; - stack.Push(((-1, finallyAddr), TryStack.CATCH)); - } - else if (finallyAddr != -1) - { - addr = finallyAddr; - stack.Push(((-1, -1), TryStack.FINALLY)); - } - } - if (stackType == TryStack.CATCH) // goto FINALLY - { - throwed = false; - if (finallyAddr != -1) - { - addr = finallyAddr; - stack.Push(((-1, -1), TryStack.FINALLY)); - } - } - continue; - HANDLE_NORMAL_CASE: - if (!coveredMap.ContainsKey(addr)) - throw new BadScriptException($"wrong address {addr}"); - if (coveredMap[addr] != BranchType.UNCOVERED) - // We have visited the code. Skip it. - return coveredMap[addr]; - Instruction instruction = script.GetInstruction(addr); - if (instruction.OpCode != OpCode.NOP) - coveredMap[addr] = BranchType.OK; - - // TODO: ABORTMSG may THROW instead of ABORT. Just throw new NotImplementedException for ABORTMSG? - if (instruction.OpCode == OpCode.ABORT || instruction.OpCode == OpCode.ABORTMSG) - { - // See if we are in a try. There may still be runtime exceptions - ((catchAddr, finallyAddr), stackType) = stack.Peek(); - if (stackType == TryStack.TRY && catchAddr != -1) - { - // Visit catchAddr because there may still be exceptions at runtime - coveredMap[entranceAddr] = CoverInstruction(catchAddr, script, coveredMap, stack: new(stack.Reverse()), throwed: true); - return coveredMap[entranceAddr]; - } - if (stackType == TryStack.CATCH && finallyAddr != -1) - { - // Visit finallyAddr because there may still be exceptions at runtime - coveredMap[entranceAddr] = CoverInstruction(finallyAddr, script, coveredMap, stack: new(stack.Reverse()), throwed: true); - return coveredMap[entranceAddr]; - } - coveredMap[entranceAddr] = BranchType.ABORT; - return coveredMap[entranceAddr]; - } - if (callWithJump.Contains(instruction.OpCode)) - { - int callTarget = ComputeJumpTarget(addr, instruction); - BranchType returnedType = CoverInstruction(callTarget, script, coveredMap); - if (returnedType == BranchType.OK) - { - addr += instruction.Size; - continue; - } - if (returnedType == BranchType.ABORT) - { - // See if we are in a try. There may still be runtime exceptions - ((catchAddr, finallyAddr), stackType) = stack.Peek(); - if (stackType == TryStack.TRY && catchAddr != -1) - { - // Visit catchAddr because there may still be exceptions at runtime - coveredMap[entranceAddr] = CoverInstruction(catchAddr, script, coveredMap, stack: new(stack.Reverse()), throwed: true); - return coveredMap[entranceAddr]; - } - if (stackType == TryStack.CATCH && finallyAddr != -1) - { - // Visit finallyAddr because there may still be exceptions at runtime - coveredMap[entranceAddr] = CoverInstruction(finallyAddr, script, coveredMap, stack: new(stack.Reverse()), throwed: true); - return coveredMap[entranceAddr]; - } - coveredMap[entranceAddr] = BranchType.ABORT; - return coveredMap[entranceAddr]; - } - if (returnedType == BranchType.THROW) - goto HANDLE_THROW; - } - if (instruction.OpCode == OpCode.RET) - { - // See if we are in a try. There may still be runtime exceptions - ((catchAddr, finallyAddr), stackType) = stack.Peek(); - if (stackType == TryStack.TRY && catchAddr != -1) - // Visit catchAddr because there may still be exceptions at runtime - CoverInstruction(catchAddr, script, coveredMap, stack: new(stack.Reverse()), throwed: true); - if (stackType == TryStack.CATCH && finallyAddr != -1) - // Visit finallyAddr because there may still be exceptions at runtime - CoverInstruction(finallyAddr, script, coveredMap, stack: new(stack.Reverse()), throwed: true); - coveredMap[entranceAddr] = BranchType.OK; - return coveredMap[entranceAddr]; - } - if (tryThrowFinally.Contains(instruction.OpCode)) - { - if (instruction.OpCode == OpCode.TRY || instruction.OpCode == OpCode.TRY_L) - stack.Push((ComputeTryTarget(addr, instruction), TryStack.TRY)); - if (instruction.OpCode == OpCode.THROW) - goto HANDLE_THROW; - if (instruction.OpCode == OpCode.ENDTRY) - { - ((catchAddr, finallyAddr), stackType) = stack.Peek(); - if (stackType != TryStack.TRY && stackType != TryStack.CATCH) - throw new BadScriptException("No try stack on ENDTRY"); - - if (stackType == TryStack.TRY && catchAddr != -1) - // Visit catchAddr because there may still be exceptions at runtime - CoverInstruction(catchAddr, script, coveredMap, stack: new(stack.Reverse()), throwed: true); - if (stackType == TryStack.CATCH && finallyAddr != -1) - // Visit finallyAddr because there may still be exceptions at runtime - CoverInstruction(finallyAddr, script, coveredMap, stack: new(stack.Reverse()), throwed: true); - - stack.Pop(); - int endPointer = addr + instruction.TokenI8; - if (finallyAddr != -1) - { - stack.Push(((-1, endPointer), TryStack.FINALLY)); - addr = finallyAddr; - } - else - addr = endPointer; - continue; - } - if (instruction.OpCode == OpCode.ENDTRY_L) - { - ((_, finallyAddr), stackType) = stack.Pop(); - if (stackType != TryStack.TRY) throw new BadScriptException("No try stack on ENDTRY"); - int endPointer = addr + instruction.TokenI32; - if (finallyAddr != -1) - { - stack.Push(((-1, endPointer), TryStack.FINALLY)); - addr = finallyAddr; - } - else - addr = endPointer; - continue; - } - if (instruction.OpCode == OpCode.ENDFINALLY) - { - ((_, _), stackType) = stack.Pop(); - if (stackType != TryStack.FINALLY) - throw new BadScriptException("No finally stack on ENDFINALLY"); - addr += instruction.Size; - continue; - } - } - if (unconditionalJump.Contains(instruction.OpCode)) - { - addr = ComputeJumpTarget(addr, instruction); - continue; - } - if (conditionalJump.Contains(instruction.OpCode) || conditionalJump_L.Contains(instruction.OpCode)) - { - int targetAddress = ComputeJumpTarget(addr, instruction); - BranchType noJump = CoverInstruction(addr + instruction.Size, script, coveredMap, stack: new(stack.Reverse())); - BranchType jump = CoverInstruction(targetAddress, script, coveredMap, stack: new(stack.Reverse())); - if (noJump == BranchType.OK || jump == BranchType.OK) - { - // See if we are in a try. There may still be runtime exceptions - ((catchAddr, finallyAddr), stackType) = stack.Peek(); - if (stackType == TryStack.TRY && catchAddr != -1) - // Visit catchAddr because there may still be exceptions at runtime - CoverInstruction(catchAddr, script, coveredMap, stack: new(stack.Reverse()), throwed: true); - if (stackType == TryStack.CATCH && finallyAddr != -1) - // Visit finallyAddr because there may still be exceptions at runtime - CoverInstruction(finallyAddr, script, coveredMap, stack: new(stack.Reverse()), throwed: true); - coveredMap[entranceAddr] = BranchType.OK; - return coveredMap[entranceAddr]; - } - if (noJump == BranchType.ABORT && jump == BranchType.ABORT) - { - // See if we are in a try. There may still be runtime exceptions - ((catchAddr, finallyAddr), stackType) = stack.Peek(); - if (stackType == TryStack.TRY && catchAddr != -1) - { - // Visit catchAddr because there may still be exceptions at runtime - coveredMap[entranceAddr] = CoverInstruction(catchAddr, script, coveredMap, stack: new(stack.Reverse()), throwed: true); - return coveredMap[entranceAddr]; - } - if (stackType == TryStack.CATCH && finallyAddr != -1) - { - // Visit finallyAddr because there may still be exceptions at runtime - coveredMap[entranceAddr] = CoverInstruction(finallyAddr, script, coveredMap, stack: new(stack.Reverse()), throwed: true); - return coveredMap[entranceAddr]; - } - coveredMap[entranceAddr] = BranchType.ABORT; - return coveredMap[entranceAddr]; - } - if (noJump == BranchType.THROW || jump == BranchType.THROW) // THROW, ABORT => THROW - goto HANDLE_THROW; - throw new Exception($"Unknown {nameof(BranchType)} {noJump} {jump}"); - } - - addr += instruction.Size; - } - coveredMap[entranceAddr] = throwed ? BranchType.THROW : BranchType.OK; - return coveredMap[entranceAddr]; - } + => new InstructionCoverage(nef, manifest, debugInfo).coveredMap; } } diff --git a/tests/Neo.Compiler.CSharp.TestContracts/Contract_TryCatch.cs b/tests/Neo.Compiler.CSharp.TestContracts/Contract_TryCatch.cs index e3663bb4c..4dfd06b46 100644 --- a/tests/Neo.Compiler.CSharp.TestContracts/Contract_TryCatch.cs +++ b/tests/Neo.Compiler.CSharp.TestContracts/Contract_TryCatch.cs @@ -114,6 +114,8 @@ public static object throwInCatch() { v = 3; } + v = 4; + return v; } public static object tryFinally() diff --git a/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Optimizer/UnitTest_BasicBlock.cs b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Optimizer/UnitTest_BasicBlock.cs new file mode 100644 index 000000000..5ec7a6795 --- /dev/null +++ b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Optimizer/UnitTest_BasicBlock.cs @@ -0,0 +1,87 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.SmartContract.TestEngine; +using Neo.Optimizer; +using System.Collections.Generic; +using Neo.SmartContract; +using Neo.Json; +using Neo.SmartContract.Manifest; +using System.Linq; +using Neo.VM; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Neo.Compiler.CSharp.UnitTests.Optimizer +{ + [TestClass] + public class UnitTest_BasicBlock + { + private TestEngine testengine; + + HashSet allowedEnds = ((OpCode[])Enum.GetValues(typeof(OpCode))) + .Where(i => Neo.Optimizer.JumpTarget.SingleJumpInOperand(i) && i != OpCode.PUSHA || Neo.Optimizer.JumpTarget.DoubleJumpInOperand(i)).ToHashSet() + .Union(new HashSet() { OpCode.RET, OpCode.ABORT, OpCode.ABORTMSG, OpCode.THROW, OpCode.ENDFINALLY }).ToHashSet(); + + public void Test_SingleContractBasicBlockStartEnd(string fileName) + { + testengine = new TestEngine(); + try + { + testengine.AddEntryScript(fileName); + } + catch (Exception e) { return; } + (NefFile nef, ContractManifest manifest, JToken debugInfo) = (testengine.Nef, testengine.Manifest, testengine.DebugInfo); + if (nef == null) { return; } + Dictionary> basicBlocks; + try + { + basicBlocks = BasicBlock.FindBasicBlocks(nef, manifest, debugInfo); + } + catch (Exception e) { return; } + // TODO: support CALLA and do not return + + Script script = nef.Script; + List<(int a, VM.Instruction i)> instructions = script.EnumerateInstructions().ToList(); + (_, _, Dictionary> jumpTargets) = Neo.Optimizer.JumpTarget.FindAllJumpAndTrySourceToTargets(instructions); + + Dictionary nextAddrTable = new(); + int prev = -1; + foreach ((int a, VM.Instruction i) in instructions) + { + if (prev >= 0) + nextAddrTable[prev] = a; + prev = a; + } + + foreach (Dictionary basicBlock in basicBlocks.Values) + { + (int a, VM.Instruction i)[] sortedInstructions = (from kv in basicBlock orderby kv.Key ascending select (kv.Key, kv.Value)).ToArray(); + // Basic block ends with allowed OpCodes only, or the next instruction is a jump target + Assert.IsTrue(allowedEnds.Contains(sortedInstructions.Last().i.OpCode) || jumpTargets.ContainsKey(nextAddrTable[sortedInstructions.Last().a])); + // Other instructions in the basic block are not those in allowedEnds + foreach ((int a, VM.Instruction i) in sortedInstructions.Take(sortedInstructions.Length - 1)) + Assert.IsFalse(allowedEnds.Contains(i.OpCode)); + } + // Each jump target starts a new basic block + foreach (int target in jumpTargets.Keys) + Assert.IsTrue(basicBlocks.ContainsKey(target)); + // Each instruction is included in only 1 basic block + HashSet includedInstructions = new(); + foreach (Dictionary basicBlock in basicBlocks.Values) + foreach (VM.Instruction instruction in basicBlock.Values) + { + Assert.IsFalse(includedInstructions.Contains(instruction)); + includedInstructions.Add(instruction); + } + } + + [TestMethod] + public void Test_BasicBlockStartEnd() + { + string[] files = Directory.GetFiles(Utils.Extensions.TestContractRoot, "Contract*.cs"); + Parallel.ForEach(files, Test_SingleContractBasicBlockStartEnd); + //foreach (string file in files) + // Test_SingleContractBasicBlockStartEnd(file); + } + } +} diff --git a/tests/Neo.SmartContract.Framework.UnitTests/Services/SequencePointInserterTest.cs b/tests/Neo.SmartContract.Framework.UnitTests/Services/SequencePointInserterTest.cs index 17e9819d3..76b3cfb0a 100644 --- a/tests/Neo.SmartContract.Framework.UnitTests/Services/SequencePointInserterTest.cs +++ b/tests/Neo.SmartContract.Framework.UnitTests/Services/SequencePointInserterTest.cs @@ -1,5 +1,4 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.SmartContract.Template.UnitTests.templates; using Neo.SmartContract.Testing; using Neo.SmartContract.Testing.TestingStandards; using Neo.VM; diff --git a/tests/Neo.SmartContract.Framework.UnitTests/TestCleanup.cs b/tests/Neo.SmartContract.Framework.UnitTests/TestCleanup.cs index d0296e3e6..7aea0f1f7 100644 --- a/tests/Neo.SmartContract.Framework.UnitTests/TestCleanup.cs +++ b/tests/Neo.SmartContract.Framework.UnitTests/TestCleanup.cs @@ -8,12 +8,14 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; -namespace Neo.SmartContract.Template.UnitTests.templates +namespace Neo.SmartContract.Framework.UnitTests { [TestClass] public class TestCleanup { + private static readonly Regex WhiteSpaceRegex = new("\\s"); internal static readonly Dictionary DebugInfos = new(); /// @@ -93,7 +95,8 @@ private static NeoDebugInfo CreateArtifact(string typeName, CompilationContext c var debug = NeoDebugInfo.FromDebugInfoJson(debugInfo); var artifact = manifest.GetArtifactsSource(typeName, nef, generateProperties: true); - if (!File.Exists(artifactsPath) || artifact.Trim() != File.ReadAllText(artifactsPath).Trim()) + string writtenArtifact = File.Exists(artifactsPath) ? File.ReadAllText(artifactsPath) : ""; + if (string.IsNullOrEmpty(writtenArtifact) || WhiteSpaceRegex.Replace(artifact, "") != WhiteSpaceRegex.Replace(writtenArtifact, "")) { // Uncomment to overwrite the artifact file File.WriteAllText(artifactsPath, artifact); diff --git a/tests/Neo.SmartContract.Template.UnitTests/templates/TestCleanup.cs b/tests/Neo.SmartContract.Template.UnitTests/templates/TestCleanup.cs index b4b946c50..32d4a0812 100644 --- a/tests/Neo.SmartContract.Template.UnitTests/templates/TestCleanup.cs +++ b/tests/Neo.SmartContract.Template.UnitTests/templates/TestCleanup.cs @@ -7,12 +7,15 @@ using Neo.SmartContract.Testing.Coverage; using Neo.SmartContract.Testing.Coverage.Formats; using Neo.SmartContract.Testing.Extensions; +using System.Text.RegularExpressions; namespace Neo.SmartContract.Template.UnitTests.templates { [TestClass] public class TestCleanup { + private static readonly Regex WhiteSpaceRegex = new("\\s"); + private static NeoDebugInfo? DebugInfo_NEP17; private static NeoDebugInfo? DebugInfo_Oracle; private static NeoDebugInfo? DebugInfo_Ownable; @@ -72,7 +75,8 @@ private static (string artifact, NeoDebugInfo debugInfo) CreateArtifact(Compi var debug = NeoDebugInfo.FromDebugInfoJson(debugInfo); var artifact = manifest.GetArtifactsSource(typeof(T).Name, nef, generateProperties: true); - if (artifact.Trim() != File.ReadAllText(artifactsPath).Trim()) + string writtenArtifact = File.Exists(artifactsPath) ? File.ReadAllText(artifactsPath) : ""; + if (string.IsNullOrEmpty(writtenArtifact) || WhiteSpaceRegex.Replace(artifact, "") != WhiteSpaceRegex.Replace(writtenArtifact, "")) { // Uncomment to overwrite the artifact file File.WriteAllText(artifactsPath, artifact); diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs index 162d77f7f..69ade6d46 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs @@ -2,12 +2,15 @@ using Moq; using Neo.SmartContract.Testing.Coverage; using System.Numerics; +using System.Text.RegularExpressions; namespace Neo.SmartContract.Testing.UnitTests.Coverage { [TestClass] public class CoverageDataTests { + private static readonly Regex WhiteSpaceRegex = new("\\s"); + [TestMethod] public void TestDump() { @@ -17,7 +20,7 @@ public void TestDump() Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); - Assert.AreEqual(@" + Assert.AreEqual(WhiteSpaceRegex.Replace(@" NeoToken [0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5] [5.00 % - 100.00 %] ┌-───────────────────────────────-┬-────────-┬-────────-┐ │ Method │ Line │ Branch │ @@ -43,16 +46,16 @@ NeoToken [0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5] [5.00 % - 100.00 %] │ unregisterCandidate(pubkey) │ 0.00 % │ 100.00 % │ │ vote(account,voteTo) │ 0.00 % │ 100.00 % │ └-───────────────────────────────-┴-────────-┴-────────-┘ -".Trim(), engine.GetCoverage(engine.Native.NEO)?.Dump().Trim()); +", ""), WhiteSpaceRegex.Replace(engine.GetCoverage(engine.Native.NEO)?.Dump(), "")); - Assert.AreEqual(@" + Assert.AreEqual(WhiteSpaceRegex.Replace(@" NeoToken [0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5] [5.00 % - 100.00 %] ┌-─────────────-┬-────────-┬-────────-┐ │ Method │ Line │ Branch │ ├-─────────────-┼-────────-┼-────────-┤ │ totalSupply() │ 100.00 % │ 100.00 % │ └-─────────────-┴-────────-┴-────────-┘ -".Trim(), (engine.Native.NEO.GetCoverage(o => o.TotalSupply) as CoveredMethod)?.Dump().Trim()); +", ""), WhiteSpaceRegex.Replace((engine.Native.NEO.GetCoverage(o => o.TotalSupply) as CoveredMethod)?.Dump(), "")); } [TestMethod]