Skip to content

Commit d7a8d72

Browse files
committed
Tokenizer position and advancement is now directly handled by the parser context.
1 parent fc66745 commit d7a8d72

3 files changed

Lines changed: 79 additions & 137 deletions

File tree

SqlServerSimulator/Parser/ParserContext.cs

Lines changed: 62 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -7,57 +7,36 @@ namespace SqlServerSimulator.Parser;
77
/// <summary>
88
/// Organizes relevant information for parsing of SQL commands.
99
/// </summary>
10-
internal sealed class ParserContext : IDisposable
10+
internal sealed class ParserContext(SimulatedDbCommand command)
1111
{
12-
public readonly SimulatedDbCommand Command;
13-
#if DEBUG
14-
private readonly TokenArrayEnumerator tokens;
15-
#else
16-
private readonly IEnumerator<Token> tokens;
17-
#endif
18-
public Token? Token { get; private set; }
19-
private readonly FrozenDictionary<string, (string Name, (DataType type, object? Value) TypeValue)> variables;
12+
#pragma warning disable CA2213 // Disposable fields should be disposed
13+
public readonly SimulatedDbCommand Command = command;
14+
#pragma warning restore CA2213 // Suppressed because ParserContext doesn't own the command object.
2015

21-
public ParserContext(SimulatedDbCommand command)
22-
{
23-
this.Command = command;
24-
25-
this.variables = command
26-
.Parameters
27-
.Cast<DbParameter>()
28-
.Select(parameter =>
29-
{
30-
var name = parameter.ParameterName;
31-
var type = DataType.GetByDbType(parameter.DbType);
32-
return (Name: name.StartsWith('@') ? name[1..] : name, TypeValue: (DataType: type, Value: parameter.Value is null ? null : type.ConvertFrom(parameter.Value)));
33-
})
34-
.ToFrozenDictionary(tuple => tuple.Name, StringComparer.InvariantCultureIgnoreCase);
16+
private readonly string commandText = string.IsNullOrEmpty(command.CommandText) ?
17+
throw new InvalidOperationException("ExecuteReader: CommandText property has not been initialized") :
18+
command.CommandText;
3519

36-
#if DEBUG
37-
this.tokens = new TokenArrayEnumerator(command.CommandText);
38-
#else
39-
this.tokens = TokenizerIterator(command.CommandText).GetEnumerator();
40-
#endif
41-
}
20+
/// <summary>
21+
/// The tokenizer position within <see cref="commandText"/>.
22+
/// </summary>
23+
private int index = -1;
4224

4325
/// <summary>
44-
/// Converts the redesigned tokenizer to a traditional enumerable for compatibility with the existing logic.
45-
/// This should be phased out eventually.
26+
/// The most recently identified token in the command string.
4627
/// </summary>
47-
static IEnumerable<Token> TokenizerIterator(string? commandText)
48-
{
49-
if (string.IsNullOrEmpty(commandText))
50-
throw new InvalidOperationException("ExecuteReader: CommandText property has not been initialized");
28+
public Token? Token { get; private set; }
5129

52-
var index = -1;
53-
while (Tokenizer.NextToken(commandText, ref index) is Token token)
30+
private readonly FrozenDictionary<string, (string Name, (DataType type, object? Value) TypeValue)> variables = command
31+
.Parameters
32+
.Cast<DbParameter>()
33+
.Select(parameter =>
5434
{
55-
if (token is Whitespace)
56-
continue;
57-
58-
yield return token;
59-
}
60-
}
35+
var name = parameter.ParameterName;
36+
var type = DataType.GetByDbType(parameter.DbType);
37+
return (Name: name.StartsWith('@') ? name[1..] : name, TypeValue: (DataType: type, Value: parameter.Value is null ? null : type.ConvertFrom(parameter.Value)));
38+
})
39+
.ToFrozenDictionary(tuple => tuple.Name, StringComparer.InvariantCultureIgnoreCase);
6140

6241
public Simulation Simulation => Command.simulation;
6342

@@ -77,8 +56,7 @@ static IEnumerable<Token> TokenizerIterator(string? commandText)
7756
/// </summary>
7857
public void MoveNextOptional()
7958
{
80-
var enumerator = this.tokens;
81-
this.Token = enumerator.MoveNext() ? enumerator.Current : null;
59+
_ = MoveNext();
8260
}
8361

8462
/// <summary>
@@ -88,8 +66,7 @@ public void MoveNextOptional()
8866
/// <returns>The next token if the enumerator was advanced, otherwise null.</returns>
8967
public Token? GetNextOptional()
9068
{
91-
var enumerator = this.tokens;
92-
return this.Token = enumerator.MoveNext() ? enumerator.Current : null;
69+
return MoveNext() ? this.Token : null;
9370
}
9471

9572
/// <summary>
@@ -100,9 +77,8 @@ public void MoveNextOptional()
10077
/// <exception cref="SimulatedSqlException">Incorrect syntax near '{token}'.</exception>
10178
public Token GetNextRequired()
10279
{
103-
var enumerator = this.tokens;
104-
var previous = enumerator.Current;
105-
return enumerator.MoveNext() ? this.Token = enumerator.Current : throw SimulatedSqlException.SyntaxErrorNear(previous);
80+
var previous = this.Token;
81+
return MoveNext() ? this.Token! : throw SimulatedSqlException.SyntaxErrorNear(previous);
10682
}
10783

10884
/// <summary>
@@ -115,16 +91,9 @@ public Token GetNextRequired()
11591
public T GetNextRequired<T>()
11692
where T : Token
11793
{
118-
var enumerator = this.tokens;
119-
var previous = enumerator.Current;
94+
var previous = this.Token;
12095

121-
if (enumerator.MoveNext() && enumerator.Current is T current)
122-
{
123-
this.Token = current;
124-
return current;
125-
}
126-
127-
throw SimulatedSqlException.SyntaxErrorNear(previous);
96+
return MoveNext() && this.Token is T current ? current : throw SimulatedSqlException.SyntaxErrorNear(previous);
12897
}
12998

13099
/// <summary>
@@ -135,10 +104,10 @@ public T GetNextRequired<T>()
135104
public void MoveNextRequired<T>()
136105
where T : Token
137106
{
138-
var enumerator = this.tokens;
139-
var previous = enumerator.Current;
107+
var previous = this.Token;
140108

141-
this.Token = enumerator.MoveNext() && enumerator.Current is T current ? (Token)current : throw SimulatedSqlException.SyntaxErrorNear(previous);
109+
if (!MoveNext() || this.Token is not T)
110+
throw SimulatedSqlException.SyntaxErrorNear(previous);
142111
}
143112

144113
/// <summary>
@@ -147,98 +116,55 @@ public void MoveNextRequired<T>()
147116
/// <exception cref="SimulatedSqlException">Incorrect syntax near '{token}'.</exception>
148117
public void MoveNextRequired()
149118
{
150-
var enumerator = this.tokens;
151-
var previous = enumerator.Current;
152-
this.Token = enumerator.MoveNext() ? enumerator.Current : throw SimulatedSqlException.SyntaxErrorNear(previous);
119+
var previous = this.Token;
120+
if (!MoveNext())
121+
throw SimulatedSqlException.SyntaxErrorNear(previous);
153122
}
154123

155-
private bool isDisposed;
156-
157-
private void Dispose(bool disposing)
124+
private bool MoveNext()
158125
{
159-
if (isDisposed)
160-
return;
161-
162-
if (disposing)
126+
while (Tokenizer.NextToken(commandText, ref index) is Token token)
163127
{
164-
this.tokens.Dispose();
165-
this.Token = null;
128+
if (token is Whitespace)
129+
continue;
130+
131+
this.Token = token;
132+
return true;
166133
}
167134

168-
isDisposed = true;
169-
}
170-
171-
public void Dispose()
172-
{
173-
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
174-
Dispose(true);
175-
GC.SuppressFinalize(this);
135+
this.Token = null;
136+
return false;
176137
}
177138

178139
#if DEBUG
179-
public override string? ToString() => this.tokens.ToString();
180-
181140
/// <summary>
182-
/// Simplifies parser debugging by providing a useful string representation of the token enumeration.
183-
/// Specifically, when calling <see cref="ToString"/> tokens are separated by '·' and the <see cref="Current"/> token is wrapped by '»' and '«'.
141+
/// Returns a string representation of the tokenized command.
142+
/// The <see cref="Token"/> token is wrapped by '»' and '«'.
184143
/// </summary>
185-
/// <param name="command">The SQL command to process.</param>
186-
/// <remarks>This should only be included in debug builds because it reduces parsing efficiency.</remarks>
187-
private sealed class TokenArrayEnumerator(string? command) : IEnumerator<Token>
144+
/// <returns>The string representation.</returns>
145+
public override string ToString()
188146
{
189-
/// <summary>
190-
/// Retains the full results of tokenization.
191-
/// This is less efficient than streaming the results, but enables this class's debugging-friendly <see cref="ToString"/>.
192-
/// </summary>
193-
private readonly Token[] source = [.. TokenizerIterator(command)];
194-
195-
public int Index { get; private set; } = -1;
196-
197-
public Token Current => source[Index];
198-
199-
object System.Collections.IEnumerator.Current => Current;
200-
201-
public void Dispose()
147+
var command = this.commandText;
148+
Span<char> result = stackalloc char[command.Length + 2];
149+
if (index < 0)
202150
{
151+
result[0] = '»';
152+
result[1] = '«';
153+
command.CopyTo(result[2..]);
203154
}
204-
205-
public bool MoveNext()
155+
else if (index >= command.Length)
206156
{
207-
var newIndex = checked(Index + 1);
208-
if (newIndex >= source.Length)
209-
return false;
210-
211-
Index = newIndex;
212-
return true;
157+
command.CopyTo(result);
158+
result[^2] = '»';
159+
result[^1] = '«';
213160
}
214-
215-
public void Reset() => Index = -1;
216-
217-
/// <summary>
218-
/// Returns a string representation of the tokenized command.
219-
/// Tokens are separated by '·' and the <see cref="Current"/> token is wrapped by '»' and '«'.
220-
/// </summary>
221-
/// <returns>The string representation.</returns>
222-
public override string ToString()
161+
else
223162
{
224-
var result = new System.Text.StringBuilder();
225-
var source = this.source;
226-
227-
for (var i = 0; i < source.Length; i++)
228-
{
229-
var token = source[i];
230-
if (i != Index)
231-
{
232-
_ = result.Append(token).Append('·');
233-
continue;
234-
}
235-
236-
_ = result.Append('»').Append(token).Append('«').Append('·');
237-
}
238-
239-
return result.ToString(0, result.Length - 1);
240-
163+
System.Diagnostics.Debug.Assert(this.Token is not null);
164+
this.Token.Highlight(result);
241165
}
166+
167+
return new string(result);
242168
}
243169
#endif
244170
}

SqlServerSimulator/Parser/Token.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,20 @@ private protected Token(string command, int index, int length)
2727

2828
// This is used for various error messages even though tokens are not directly accessible to user code.
2929
public sealed override string ToString() => command.Substring(index, length);
30+
31+
#if DEBUG
32+
/// <summary>
33+
/// Identifies this token within the scope of the full command by wrapping it with '»' and '«';
34+
/// </summary>
35+
public void Highlight(Span<char> result)
36+
{
37+
var command = this.command.AsSpan();
38+
39+
command[..this.index].CopyTo(result);
40+
result[index] = '»';
41+
this.Source.CopyTo(result[(index + 1)..]);
42+
result[index + 1 + this.length] = '«';
43+
command[(index + length)..].CopyTo(result[(index + length + 2)..]);
44+
}
45+
#endif
3046
}

SqlServerSimulator/Simulation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public Simulation()
3030

3131
internal IEnumerable<SimulatedStatementOutcome> CreateResultSetsForCommand(SimulatedDbCommand command)
3232
{
33-
using var context = new ParserContext(command);
33+
var context = new ParserContext(command);
3434

3535
while (context.GetNextOptional() is not null)
3636
{

0 commit comments

Comments
 (0)