Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: string.Trim only trim ' ' #1194

Merged
merged 6 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 118 additions & 44 deletions src/Neo.Compiler.CSharp/MethodConvert/System/SystemCall.String.cs
Original file line number Diff line number Diff line change
Expand Up @@ -300,61 +300,135 @@ private static void HandleStringTrim(MethodConvert methodConvert, SemanticModel
if (arguments is not null)
methodConvert.PrepareArgumentsForMethod(model, symbol, arguments);

methodConvert.AddInstruction(OpCode.LDARG0); // Load the string
var strLen = methodConvert.AddAnonymousVariable();
var startIndex = methodConvert.AddAnonymousVariable();
var endIndex = methodConvert.AddAnonymousVariable();

// Trim leading whitespace
methodConvert.AddInstruction(OpCode.DUP); // Duplicate the string
methodConvert.AddInstruction(OpCode.PUSH0); // Push the initial index (0)
InitStrLen(); // strLen = string.Length
InitStartIndex(); // startIndex = 0
InitEndIndex(); // endIndex = string.Length - 1

// loop to trim leading whitespace
var loopStart = new JumpTarget();
var loopEnd = new JumpTarget();
loopStart.Instruction = methodConvert.AddInstruction(OpCode.NOP);

methodConvert.AddInstruction(OpCode.DUP); // Duplicate the index
methodConvert.AddInstruction(OpCode.LDARG0); // Load the string
methodConvert.AddInstruction(OpCode.SIZE); // Get the length of the string
methodConvert.AddInstruction(OpCode.LT); // Check if index < length
methodConvert.Jump(OpCode.JMPIFNOT, loopEnd); // If not, exit the loop

methodConvert.AddInstruction(OpCode.DUP); // Duplicate the index
methodConvert.AddInstruction(OpCode.LDARG0); // Load the string
methodConvert.AddInstruction(OpCode.PICKITEM); // Get the character at the current index
methodConvert.Push((ushort)' '); // Push space character
methodConvert.AddInstruction(OpCode.EQUAL); // Check if character is a space
methodConvert.Jump(OpCode.JMPIFNOT, loopEnd); // If not, exit the loop

methodConvert.AddInstruction(OpCode.INC); // Increment the index
methodConvert.Jump(OpCode.JMP, loopStart); // Jump to the start of the loop

CheckStartIndex(loopEnd);
PickCharStart(); // pick a char to check
CheckWithin(loopEnd);
MoveStartIndexAndLoop(loopStart);
loopEnd.Instruction = methodConvert.AddInstruction(OpCode.NOP);
methodConvert.AddInstruction(OpCode.SUBSTR); // Get the substring from the first non-space character

// Trim trailing whitespace
methodConvert.AddInstruction(OpCode.DUP); // Duplicate the string
methodConvert.AddInstruction(OpCode.SIZE); // Get the length of the string
methodConvert.AddInstruction(OpCode.DEC); // Decrement the length to get the last index
// done processing leading whitespace, start processing trailing whitespace
var loopStart2 = new JumpTarget();
var loopEnd2 = new JumpTarget();
loopStart2.Instruction = methodConvert.AddInstruction(OpCode.NOP);

methodConvert.AddInstruction(OpCode.DUP); // Duplicate the index
methodConvert.AddInstruction(OpCode.PUSH0); // Push 0
methodConvert.AddInstruction(OpCode.GT); // Check if index > 0
methodConvert.Jump(OpCode.JMPIFNOT, loopEnd2); // If not, exit the loop

methodConvert.AddInstruction(OpCode.DUP); // Duplicate the index
methodConvert.AddInstruction(OpCode.LDARG0); // Load the string
methodConvert.AddInstruction(OpCode.PICKITEM); // Get the character at the current index
methodConvert.Push((ushort)' '); // Push space character
methodConvert.AddInstruction(OpCode.EQUAL); // Check if character is a space
methodConvert.Jump(OpCode.JMPIFNOT, loopEnd2); // If not, exit the loop

methodConvert.AddInstruction(OpCode.DEC); // Decrement the index
methodConvert.Jump(OpCode.JMP, loopStart2); // Jump to the start of the loop

CheckEndIndex(loopEnd2);
PickCharEnd(); // pick a char to check
CheckWithin(loopEnd2);
MoveEndIndexAndLoop(loopStart2);
loopEnd2.Instruction = methodConvert.AddInstruction(OpCode.NOP);
GetString();
GetStartIndex();
GetEndIndex();
GetStartIndex();
methodConvert.AddInstruction(OpCode.SUB);
methodConvert.AddInstruction(OpCode.INC);
methodConvert.AddInstruction(OpCode.SUBSTR); // Get the substring up to the last non-space character

methodConvert.ChangeType(VM.Types.StackItemType.ByteString); // Convert the array to a byte string
return;

void InitStrLen()
{
GetString();
methodConvert.AddInstruction(OpCode.SIZE);
methodConvert.AccessSlot(OpCode.STLOC, strLen);
}

// Local function to get strLen
void GetStrLen() => methodConvert.AccessSlot(OpCode.LDLOC, strLen);

void InitStartIndex()
{
methodConvert.Push(0);
methodConvert.AccessSlot(OpCode.STLOC, startIndex);
}

void InitEndIndex()
{
GetStrLen();
methodConvert.AddInstruction(OpCode.DEC); // len-1
methodConvert.AccessSlot(OpCode.STLOC, endIndex);
}

// Local function to get endIndex
void GetEndIndex() => methodConvert.AccessSlot(OpCode.LDLOC, endIndex);

void GetString() => methodConvert.AddInstruction(OpCode.LDARG0); // Load the string

// Local function to get startIndex
void GetStartIndex() => methodConvert.AccessSlot(OpCode.LDLOC, startIndex);

void CheckStartIndex(JumpTarget loopEnd)
{
GetStartIndex();
GetStrLen();
methodConvert.AddInstruction(OpCode.LT); // Check if index < length
methodConvert.Jump(OpCode.JMPIFNOT, loopEnd); // If not, exit the loop
}

// Local function to set startIndex
void MoveStartIndexAndLoop(JumpTarget loopStart)
{
methodConvert.AccessSlot(OpCode.LDLOC, startIndex);
methodConvert.AddInstruction(OpCode.INC);
methodConvert.AccessSlot(OpCode.STLOC, startIndex);
methodConvert.Jump(OpCode.JMP, loopStart);
}

void PickCharStart()
{
GetString();
GetStartIndex();
methodConvert.AddInstruction(OpCode.PICKITEM); // Get the character at the current index
}

// Local function to set endIndex
void MoveEndIndexAndLoop(JumpTarget loopStart)
{
methodConvert.AccessSlot(OpCode.LDLOC, endIndex);
methodConvert.AddInstruction(OpCode.DEC);
methodConvert.AccessSlot(OpCode.STLOC, endIndex);
methodConvert.Jump(OpCode.JMP, loopStart);
}

void CheckWithin(JumpTarget loopEnd)
{
methodConvert.AddInstruction(OpCode.DUP);
methodConvert.Push((ushort)'\t');
methodConvert.Push((ushort)'\r' + 1);
methodConvert.AddInstruction(OpCode.WITHIN); // check if '\t' <= c <= '\r'
methodConvert.AddInstruction(OpCode.SWAP);

methodConvert.Push((ushort)' '); // Push space character
methodConvert.AddInstruction(OpCode.EQUAL); // Check if character is a space
methodConvert.AddInstruction(OpCode.BOOLOR); // check if '\t' <= c <= '\r' or ' ' == c

methodConvert.Jump(OpCode.JMPIFNOT, loopEnd); // If not, exit the loop
}

void CheckEndIndex(JumpTarget loopEnd)
{
GetEndIndex();
GetStartIndex();
methodConvert.AddInstruction(OpCode.GT); // Check if index > start
methodConvert.Jump(OpCode.JMPIFNOT, loopEnd); // If not, exit the loop
}

void PickCharEnd()
{
GetString();
GetEndIndex();
methodConvert.AddInstruction(OpCode.PICKITEM); // Get the character at the current index
}
}

private static void HandleStringTrimChar(MethodConvert methodConvert, SemanticModel model, IMethodSymbol symbol, ExpressionSyntax? instanceExpression, IReadOnlyList<SyntaxNode>? arguments)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class StringMethodUsageAnalyzer : DiagnosticAnalyzer
"Normalize", "PadLeft", "PadRight", "Remove",
"Replace", "Split",
"ToCharArray", "ToLower", "ToLowerInvariant",
"ToUpper", "ToUpperInvariant", "Trim", "TrimEnd",
"ToUpper", "ToUpperInvariant", "TrimEnd",
"TrimStart"
};

Expand Down
4 changes: 4 additions & 0 deletions tests/Neo.Compiler.CSharp.TestContracts/Contract_String.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,9 @@ public static string TestInterpolatedStringHandler()
$"ECPoint: {ecPointValue}, ByteString: {byteStringValue}, Bool: {boolValue}";
return str;
}
public static string TestTrim(string str)
{
return str.Trim();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ public abstract class Contract_String(Neo.SmartContract.Testing.SmartContractIni
{
#region Compiled data

public static Neo.SmartContract.Manifest.ContractManifest Manifest => Neo.SmartContract.Manifest.ContractManifest.Parse(@"{""name"":""Contract_String"",""groups"":[],""features"":{},""supportedstandards"":[],""abi"":{""methods"":[{""name"":""testMain"",""parameters"":[],""returntype"":""Void"",""offset"":0,""safe"":false},{""name"":""testEqual"",""parameters"":[],""returntype"":""Void"",""offset"":82,""safe"":false},{""name"":""testSubstring"",""parameters"":[],""returntype"":""Void"",""offset"":127,""safe"":false},{""name"":""testEmpty"",""parameters"":[],""returntype"":""String"",""offset"":163,""safe"":false},{""name"":""testIsNullOrEmpty"",""parameters"":[{""name"":""str"",""type"":""String""}],""returntype"":""Boolean"",""offset"":166,""safe"":false},{""name"":""testEndWith"",""parameters"":[{""name"":""str"",""type"":""String""}],""returntype"":""Boolean"",""offset"":181,""safe"":false},{""name"":""testContains"",""parameters"":[{""name"":""str"",""type"":""String""}],""returntype"":""Boolean"",""offset"":220,""safe"":false},{""name"":""testIndexOf"",""parameters"":[{""name"":""str"",""type"":""String""}],""returntype"":""Integer"",""offset"":237,""safe"":false},{""name"":""testInterpolatedStringHandler"",""parameters"":[],""returntype"":""String"",""offset"":252,""safe"":false}],""events"":[]},""permissions"":[{""contract"":""0xacce6fd80d44e1796aa0c2c625e9e4e0ce39efc0"",""methods"":[""itoa"",""memorySearch""]},{""contract"":""0xda65b600f7124ce6c79950c1772a36403104f2be"",""methods"":[""currentHash"",""getBlock""]}],""trusts"":[],""extra"":{""nef"":{""optimization"":""All""}}}");
public static Neo.SmartContract.Manifest.ContractManifest Manifest => Neo.SmartContract.Manifest.ContractManifest.Parse(@"{""name"":""Contract_String"",""groups"":[],""features"":{},""supportedstandards"":[],""abi"":{""methods"":[{""name"":""testMain"",""parameters"":[],""returntype"":""Void"",""offset"":0,""safe"":false},{""name"":""testEqual"",""parameters"":[],""returntype"":""Void"",""offset"":82,""safe"":false},{""name"":""testSubstring"",""parameters"":[],""returntype"":""Void"",""offset"":127,""safe"":false},{""name"":""testEmpty"",""parameters"":[],""returntype"":""String"",""offset"":163,""safe"":false},{""name"":""testIsNullOrEmpty"",""parameters"":[{""name"":""str"",""type"":""String""}],""returntype"":""Boolean"",""offset"":166,""safe"":false},{""name"":""testEndWith"",""parameters"":[{""name"":""str"",""type"":""String""}],""returntype"":""Boolean"",""offset"":181,""safe"":false},{""name"":""testContains"",""parameters"":[{""name"":""str"",""type"":""String""}],""returntype"":""Boolean"",""offset"":220,""safe"":false},{""name"":""testIndexOf"",""parameters"":[{""name"":""str"",""type"":""String""}],""returntype"":""Integer"",""offset"":237,""safe"":false},{""name"":""testInterpolatedStringHandler"",""parameters"":[],""returntype"":""String"",""offset"":252,""safe"":false},{""name"":""testTrim"",""parameters"":[{""name"":""str"",""type"":""String""}],""returntype"":""String"",""offset"":579,""safe"":false}],""events"":[]},""permissions"":[{""contract"":""0xacce6fd80d44e1796aa0c2c625e9e4e0ce39efc0"",""methods"":[""itoa"",""memorySearch""]},{""contract"":""0xda65b600f7124ce6c79950c1772a36403104f2be"",""methods"":[""currentHash"",""getBlock""]}],""trusts"":[],""extra"":{""nef"":{""optimization"":""All""}}}");

/// <summary>
/// Optimization: "All"
/// </summary>
public static Neo.SmartContract.NefFile Nef => Neo.IO.Helper.AsSerializable<Neo.SmartContract.NefFile>(Convert.FromBase64String(@"TkVGM1Rlc3RpbmdFbmdpbmUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS+8gQxQDYqd8FQmcfmTBL3ALZl2ghnZXRCbG9jawEAAQ++8gQxQDYqd8FQmcfmTBL3ALZl2gtjdXJyZW50SGFzaAAAAQ/A7znO4OTpJcbCoGp54UQN2G/OrARpdG9hAQABD8DvOc7g5OklxsKgannhRA3Yb86sDG1lbW9yeVNlYXJjaAIAAQ8AAP1DAlcDAAwETWFya3AMAHE3AQA3AAAUznIMB0hlbGxvLCBoiwwBIItpiwwXISBDdXJyZW50IHRpbWVzdGFtcCBpcyCLajcCAIsMAS6L2yhBz+dHlkBXAgAMBWhlbGxvcAwFaGVsbG9xaGmXJAsMBUZhbHNlIggMBFRydWVBz+dHlkBXAQAMCDAxMjM0NTY3cGgRS8pLn4xBz+dHlmgRFIxBz+dHlkAMAEBXAAF4StgkBsoQs0BFCEBXAAEMBXdvcmxkeErKUUrKShNSUJ9KECwIRUVFRQlAE1JTjNsol0BXAAEMBXdvcmxkeDcDABC4QFcAAQwFd29ybGR4NwMAQFcEAAQAAKDexa3JNTYAAAAAAAAAcAwiTlhWN1poSGl5TTFhSFh3cFZzUlpDNkJ3TkZQMmpnaFhBcXEMAwECA9swcgwHU0J5dGU6IADWNwIAiwwILCBCeXRlOiCLACo3AgCLDAosIFVTaG9ydDogiwHoAzcCAIsMAiwgi9soDAZVSW50OiACQEIPADcCAIsMCSwgVUxvbmc6IIsDABCl1OgAAAA3AgCLDAIsIIvbKIvbKAwMQmlnSW50ZWdlcjogaDcCAIsMCCwgQ2hhcjogiwBB2yiLDAosIFN0cmluZzogiwwFSGVsbG+LDAIsIIvbKIvbKAwJRUNQb2ludDogaYsMDiwgQnl0ZVN0cmluZzogiwwNU3lzdGVtLkJ5dGVbXYsMCCwgQm9vbDogiwgmCgwEVHJ1ZSIJDAVGYWxzZYvbKIvbKHNrQENTLRg="));
public static Neo.SmartContract.NefFile Nef => Neo.IO.Helper.AsSerializable<Neo.SmartContract.NefFile>(Convert.FromBase64String(@"TkVGM1Rlc3RpbmdFbmdpbmUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS+8gQxQDYqd8FQmcfmTBL3ALZl2ghnZXRCbG9jawEAAQ++8gQxQDYqd8FQmcfmTBL3ALZl2gtjdXJyZW50SGFzaAAAAQ/A7znO4OTpJcbCoGp54UQN2G/OrARpdG9hAQABD8DvOc7g5OklxsKgannhRA3Yb86sDG1lbW9yeVNlYXJjaAIAAQ8AAP2GAlcDAAwETWFya3AMAHE3AQA3AAAUznIMB0hlbGxvLCBoiwwBIItpiwwXISBDdXJyZW50IHRpbWVzdGFtcCBpcyCLajcCAIsMAS6L2yhBz+dHlkBXAgAMBWhlbGxvcAwFaGVsbG9xaGmXJAsMBUZhbHNlIggMBFRydWVBz+dHlkBXAQAMCDAxMjM0NTY3cGgRS8pLn4xBz+dHlmgRFIxBz+dHlkAMAEBXAAF4StgkBsoQs0BFCEBXAAEMBXdvcmxkeErKUUrKShNSUJ9KECwIRUVFRQlAE1JTjNsol0BXAAEMBXdvcmxkeDcDABC4QFcAAQwFd29ybGR4NwMAQFcEAAQAAKDexa3JNTYAAAAAAAAAcAwiTlhWN1poSGl5TTFhSFh3cFZzUlpDNkJ3TkZQMmpnaFhBcXEMAwECA9swcgwHU0J5dGU6IADWNwIAiwwILCBCeXRlOiCLACo3AgCLDAosIFVTaG9ydDogiwHoAzcCAIsMAiwgi9soDAZVSW50OiACQEIPADcCAIsMCSwgVUxvbmc6IIsDABCl1OgAAAA3AgCLDAIsIIvbKIvbKAwMQmlnSW50ZWdlcjogaDcCAIsMCCwgQ2hhcjogiwBB2yiLDAosIFN0cmluZzogiwwFSGVsbG+LDAIsIIvbKIvbKAwJRUNQb2ludDogaYsMDiwgQnl0ZVN0cmluZzogiwwNU3lzdGVtLkJ5dGVbXYsMCCwgQm9vbDogiwgmCgwEVHJ1ZSIJDAVGYWxzZYvbKIvbKHNrQFcDAXjKcBBxaJ1yaWi1JhV4ac5KGR67UAAgl6wmB2mccSLqamm3JhV4as5KGR67UAAgl6wmB2qdciLqeGlqaZ+cjEAbrJCF"));

#endregion

Expand Down Expand Up @@ -75,6 +75,12 @@ public abstract class Contract_String(Neo.SmartContract.Testing.SmartContractIni
[DisplayName("testSubstring")]
public abstract void TestSubstring();

/// <summary>
/// Unsafe method
/// </summary>
[DisplayName("testTrim")]
public abstract string? TestTrim(string? str);

#endregion

}
22 changes: 22 additions & 0 deletions tests/Neo.Compiler.CSharp.UnitTests/UnitTest_String.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,27 @@ public void Test_TestInterpolatedStringHandler()
Assert.AreEqual("SByte: -42, Byte: 42, UShort: 1000, UInt: 1000000, ULong: 1000000000000, BigInteger: 1000000000000000000000, Char: A, String: Hello, ECPoint: NXV7ZhHiyM1aHXwpVsRZC6BwNFP2jghXAq, ByteString: System.Byte[], Bool: True", Contract.TestInterpolatedStringHandler());
Assert.AreEqual(11313480, Engine.FeeConsumed.Value);
}

[TestMethod]
public void Test_TestTrim()
{
Assert.AreEqual("Hello, World!", Contract.TestTrim(" Hello, World! "));
AssertGasConsumed(1136010);

Assert.AreEqual("No Trim", Contract.TestTrim("No Trim"));
AssertGasConsumed(1118130);

Assert.AreEqual("", Contract.TestTrim(" "));
AssertGasConsumed(1124040);

// Test various whitespace characters
Assert.AreEqual("Trim Test", Contract.TestTrim("\t\n\r Trim Test \t\n\r"));
AssertGasConsumed(1153890);
Assert.AreEqual("Multiple Spaces", Contract.TestTrim(" Multiple Spaces "));
AssertGasConsumed(1144950);

Assert.AreEqual("Mix of Whitespace", Contract.TestTrim(" \t \n \r Mix of Whitespace \r \n \t "));
AssertGasConsumed(1180710);
}
}
}
Loading