Skip to content

Commit 96a4736

Browse files
committed
Continued improvement of collation fidelity by passing BatchContext through Expression.GetSqlType, allowing proper collation information to reach places that were using Collation.Default.
1 parent ce454c7 commit 96a4736

125 files changed

Lines changed: 339 additions & 274 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Readonly struct, up to 4 inline slots (SQL Server's grammar limit). API: `Leaf`,
5656
`SimulatedSqlException` constructor is private; each error case is an `internal static` factory in a topical partial (`TypeErrors`, `SchemaErrors`, `ConstraintErrors`, `ResolutionErrors`, `QueryErrors`, `SyntaxErrors`). The number lands in `Data["HelpLink.EvtID"]`. **Grep for an existing factory before adding a new one.**
5757

5858
### Expression evaluation
59-
`Expression.Run(RuntimeContext runtime)` (runtime) and `Expression.GetSqlType(...)` (static, for projection schema) must agree on result type — drift breaks union/CASE/coalesce schema. `RuntimeContext` bundles `ResolveColumn` (per-row column lookup) and `Batch` (the executing `BatchContext`); expressions that need batch / session / database state read `runtime.Batch.*` directly. `BooleanExpression.Run` returns `bool?` (three-valued); WHERE/MERGE-ON exclude UNKNOWN, CHECK passes UNKNOWN. Aggregates: subclass `Aggregator` (`Add(SqlValue)` / `Result()`), register in `AggregateExpression`'s dispatch.
59+
`Expression.Run(RuntimeContext runtime)` (runtime) and `Expression.GetSqlType(BatchContext batch, ...)` (static, for projection schema) must agree on result type — drift breaks union/CASE/coalesce schema. Both take a `BatchContext` so result types that depend on the active database (notably the collation of literal / CAST / function-result string types) stay in parity between the parse-time schema and the runtime value. `RuntimeContext` bundles `ResolveColumn` (per-row column lookup) and `Batch` (the executing `BatchContext`); expressions that need batch / session / database state read `runtime.Batch.*` directly. `BooleanExpression.Run` returns `bool?` (three-valued); WHERE/MERGE-ON exclude UNKNOWN, CHECK passes UNKNOWN. Aggregates: subclass `Aggregator` (`Add(SqlValue)` / `Result()`), register in `AggregateExpression`'s dispatch.
6060

6161
### Context layering
6262
Six scopes, one home each. **Add new state to whichever class matches its true scope** — when in doubt, ask who outlives whom. The field roster on each class lives in the source XML docs; this section captures only the identity + load-bearing contracts.

SqlServerSimulator.Tests.Internal/Storage/PromoteStringConcatTests.cs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,50 +13,55 @@ public sealed class PromoteStringConcatTests
1313
{
1414
[TestMethod]
1515
public void Varchar_Plus_Varchar_SumsLengths() =>
16-
AreEqual(30, ((VarcharSqlType)SqlType.PromoteForArithmetic(VarcharSqlType.Get(10), VarcharSqlType.Get(20), '+')).length);
16+
AreEqual(30, ((VarcharSqlType)SqlType.PromoteForArithmetic(VarcharSqlType.Get(10, Collation.Default, Coercibility.CoercibleDefault), VarcharSqlType.Get(20, Collation.Default, Coercibility.CoercibleDefault), '+')).length);
1717

1818
[TestMethod]
1919
public void Varchar_Plus_Varchar_CapsAt8000() =>
20-
AreEqual(8000, ((VarcharSqlType)SqlType.PromoteForArithmetic(VarcharSqlType.Get(8000), VarcharSqlType.Get(100), '+')).length);
20+
AreEqual(8000, ((VarcharSqlType)SqlType.PromoteForArithmetic(VarcharSqlType.Get(8000, Collation.Default, Coercibility.CoercibleDefault), VarcharSqlType.Get(100, Collation.Default, Coercibility.CoercibleDefault), '+')).length);
2121

2222
[TestMethod]
2323
public void NVarchar_Plus_NVarchar_SumsLengths_CapsAt4000() =>
24-
AreEqual(4000, ((NVarcharSqlType)SqlType.PromoteForArithmetic(NVarcharSqlType.Get(3000), NVarcharSqlType.Get(2000), '+')).length);
24+
AreEqual(4000, ((NVarcharSqlType)SqlType.PromoteForArithmetic(NVarcharSqlType.Get(3000, Collation.Default, Coercibility.CoercibleDefault), NVarcharSqlType.Get(2000, Collation.Default, Coercibility.CoercibleDefault), '+')).length);
2525

2626
[TestMethod]
2727
public void Char_Plus_Varchar_DropsToVarcharOfCombinedLength() =>
28-
AreEqual(15, ((VarcharSqlType)SqlType.PromoteForArithmetic(SqlType.GetChar(5), VarcharSqlType.Get(10), '+')).length);
28+
AreEqual(15, ((VarcharSqlType)SqlType.PromoteForArithmetic(SqlType.GetChar(5), VarcharSqlType.Get(10, Collation.Default, Coercibility.CoercibleDefault), '+')).length);
2929

3030
[TestMethod]
3131
public void NChar_Plus_Varchar_PromotesToNVarcharOfCombinedLength() =>
32-
AreEqual(15, ((NVarcharSqlType)SqlType.PromoteForArithmetic(SqlType.GetNChar(5), VarcharSqlType.Get(10), '+')).length);
32+
AreEqual(15, ((NVarcharSqlType)SqlType.PromoteForArithmetic(SqlType.GetNChar(5), VarcharSqlType.Get(10, Collation.Default, Coercibility.CoercibleDefault), '+')).length);
3333

3434
[TestMethod]
3535
public void Varchar_Plus_NVarchar_PromotesToNVarcharOfCombinedLength() =>
36-
AreEqual(30, ((NVarcharSqlType)SqlType.PromoteForArithmetic(VarcharSqlType.Get(10), NVarcharSqlType.Get(20), '+')).length);
36+
AreEqual(30, ((NVarcharSqlType)SqlType.PromoteForArithmetic(VarcharSqlType.Get(10, Collation.Default, Coercibility.CoercibleDefault), NVarcharSqlType.Get(20, Collation.Default, Coercibility.CoercibleDefault), '+')).length);
3737

3838
[TestMethod]
3939
public void Unspecified_Plus_BoundedVarchar_DropsToUnspecified()
4040
{
4141
// Length 0 means "we don't know"; the result can't reliably claim
4242
// a sum, so it falls back to the length-unspecified form.
43-
var result = SqlType.PromoteForArithmetic(VarcharSqlType.Unspecified, VarcharSqlType.Get(10), '+');
44-
AreSame(VarcharSqlType.Unspecified, result);
43+
var result = SqlType.PromoteForArithmetic(VarcharSqlType.Get(0, Collation.Default, Coercibility.CoercibleDefault), VarcharSqlType.Get(10, Collation.Default, Coercibility.CoercibleDefault), '+');
44+
AreSame(VarcharSqlType.Get(0, Collation.Default, Coercibility.CoercibleDefault), result);
4545
}
4646

4747
[TestMethod]
4848
public void Text_Plus_Varchar_DropsToVarcharUnspecified()
4949
{
5050
// LOB family operands have no per-cell width to add — the fall-back
51-
// is the unspecified-length form of the right family.
52-
var result = SqlType.PromoteForArithmetic(SqlType.Text, VarcharSqlType.Get(10), '+');
53-
AreSame(VarcharSqlType.Unspecified, result);
51+
// is the unspecified-length form of the right family. Text carries
52+
// Implicit coercibility (it's a column-typed family), so the
53+
// Collation.Resolve hand-off yields Implicit rather than
54+
// CoercibleDefault.
55+
var result = SqlType.PromoteForArithmetic(SqlType.Text, VarcharSqlType.Get(10, Collation.Default, Coercibility.CoercibleDefault), '+');
56+
AreSame(VarcharSqlType.Get(0, Collation.Default, Coercibility.Implicit), result);
5457
}
5558

5659
[TestMethod]
5760
public void NText_Plus_Char_DropsToNVarcharUnspecified()
5861
{
62+
// NText carries Implicit; the char(5) bridge is also Implicit (via
63+
// SqlType.GetChar's static helper), so the resolved rank is Implicit.
5964
var result = SqlType.PromoteForArithmetic(SqlType.NText, SqlType.GetChar(5), '+');
60-
AreSame(NVarcharSqlType.Unspecified, result);
65+
AreSame(NVarcharSqlType.Get(0, Collation.Default, Coercibility.Implicit), result);
6166
}
6267
}

SqlServerSimulator.Tests/NameComparisonRegimeTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,4 +312,27 @@ public void ServerCollationName_SeededDatabase_CreateSchemaDBO_CoexistsWithDbo()
312312
AreEqual(2, sim.ExecuteScalar("SELECT COUNT(*) FROM sys.schemas WHERE name IN (N'dbo', N'DBO')"));
313313
}
314314

315+
// ===== Scalar-function result types carry the active DB collation =====
316+
// CHAR(N) / NCHAR(N) result types pin the active database's collation
317+
// rather than the storage-baseline Collation.Default, so a CS-DB
318+
// compare between two function-returned strings goes through the
319+
// CS comparer. Probe-confirmed against real SQL Server CS database
320+
// (2026-05-22): CHAR(65) = CHAR(97) returns 'neq' (literals don't
321+
// case-fold under CS).
322+
323+
[TestMethod]
324+
public void CsDatabase_CharFunctionResultUsesActiveCollation()
325+
=> AreEqual("neq",
326+
new Simulation { ServerCollationName = "SQL_Latin1_General_CP1_CS_AS" }
327+
.ExecuteScalar("SELECT IIF(CHAR(65) = CHAR(97), 'eq', 'neq')"));
328+
329+
[TestMethod]
330+
public void CsDatabase_NCharFunctionResultUsesActiveCollation()
331+
=> AreEqual("neq",
332+
new Simulation { ServerCollationName = "SQL_Latin1_General_CP1_CS_AS" }
333+
.ExecuteScalar("SELECT IIF(NCHAR(65) = NCHAR(97), 'eq', 'neq')"));
334+
335+
[TestMethod]
336+
public void CiDatabase_CharFunctionResult_StillEqualsUnderCi()
337+
=> AreEqual("eq", new Simulation().ExecuteScalar("SELECT IIF(CHAR(65) = CHAR(97), 'eq', 'neq')"));
315338
}

SqlServerSimulator/BuiltInResources.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ private static Dictionary<string, CatalogView> BuildCatalogViews()
139139
// Real SQL Server has many more columns; the shipped subset covers
140140
// the dominant query shapes. type is char(2) with trailing space
141141
// ('U ') — probe-confirmed.
142-
var charTwo = CharSqlType.Get(2);
142+
var charTwo = CharSqlType.Get(2, Collation.Default, Coercibility.CoercibleDefault);
143143
var tableType = SqlValue.FromChar(charTwo, "U ");
144144
var tableTypeDesc = SqlValue.FromNVarchar("USER_TABLE");
145145
var notMsShipped = SqlValue.FromBoolean(false);
@@ -826,7 +826,7 @@ private static Dictionary<string, CatalogView> BuildCatalogViews()
826826
new("major_id", SqlType.Int32, null, false),
827827
new("minor_id", SqlType.Int32, null, false),
828828
new("name", SqlType.SystemName, 128, false),
829-
new("value", NVarcharSqlType.MaxForm, SqlType.MaxLengthSentinel, true),
829+
new("value", NVarcharSqlType.Get(-1, Collation.Default, Coercibility.CoercibleDefault), SqlType.MaxLengthSentinel, true),
830830
};
831831
var extendedPropertiesView = new CatalogView("extended_properties", extendedPropertiesColumns, EnumerateSysExtendedProperties);
832832

@@ -866,7 +866,7 @@ private static Dictionary<string, CatalogView> BuildCatalogViews()
866866
new("grantee_principal_id", SqlType.Int32, null, false),
867867
new("grantor_principal_id", SqlType.Int32, null, false),
868868
new("type", charTwo, 2, false),
869-
new("permission_name", NVarcharSqlType.Get(128), 128, true),
869+
new("permission_name", NVarcharSqlType.Get(128, Collation.Default, Coercibility.CoercibleDefault), 128, true),
870870
new("state", charOne, 1, false),
871871
new("state_desc", SqlType.NVarchar, 60, true),
872872
};
@@ -1000,7 +1000,7 @@ private static Dictionary<string, CatalogView> BuildCatalogViews()
10001000
new("spatial_index_type_desc", SqlType.NVarchar, 60, true),
10011001
new("tessellation_scheme", SqlType.NVarchar, 60, true),
10021002
new("has_filter", SqlType.Bit, null, false),
1003-
new("filter_definition", NVarcharSqlType.MaxForm, null, true),
1003+
new("filter_definition", NVarcharSqlType.Get(-1, Collation.Default, Coercibility.CoercibleDefault), null, true),
10041004
new("auto_created", SqlType.Bit, null, true),
10051005
};
10061006
var spatialIndexesView = new CatalogView("spatial_indexes", spatialIndexesColumns, EnumerateSysSpatialIndexes);
@@ -1249,7 +1249,7 @@ private static IEnumerable<SqlValue[]> EnumerateSysExtendedProperties(Parser.Bat
12491249
SqlValue.FromInt32(key.MajorId),
12501250
SqlValue.FromInt32(key.MinorId),
12511251
SqlValue.FromSystemName(key.Name),
1252-
kvp.Value.IsNull ? SqlValue.Null(NVarcharSqlType.MaxForm) : kvp.Value.CoerceTo(NVarcharSqlType.MaxForm),
1252+
kvp.Value.IsNull ? SqlValue.Null(NVarcharSqlType.Get(-1, Collation.Default, Coercibility.CoercibleDefault)) : kvp.Value.CoerceTo(NVarcharSqlType.Get(-1, Collation.Default, Coercibility.CoercibleDefault)),
12531253
];
12541254
}
12551255
}
@@ -1301,7 +1301,7 @@ private static IEnumerable<SqlValue[]> EnumerateSysForeignKeys(Parser.BatchConte
13011301
var trueBit = SqlValue.FromBoolean(true);
13021302
var falseBit = SqlValue.FromBoolean(false);
13031303
var nullPrincipal = SqlValue.Null(SqlType.Int32);
1304-
var fkType = SqlValue.FromChar(CharSqlType.Get(2), "F ");
1304+
var fkType = SqlValue.FromChar(CharSqlType.Get(2, Collation.Default, Coercibility.CoercibleDefault), "F ");
13051305
var fkTypeDesc = SqlValue.FromNVarchar("FOREIGN_KEY_CONSTRAINT");
13061306
// key_index_id is the index id on the referenced table that satisfies
13071307
// the FK — the simulator doesn't model indexes so report 1 (the
@@ -1395,7 +1395,7 @@ private static IEnumerable<SqlValue[]> EnumerateSysCheckConstraints(Parser.Batch
13951395
var trueBit = SqlValue.FromBoolean(true);
13961396
var falseBit = SqlValue.FromBoolean(false);
13971397
var nullPrincipal = SqlValue.Null(SqlType.Int32);
1398-
var ckType = SqlValue.FromChar(CharSqlType.Get(2), "C ");
1398+
var ckType = SqlValue.FromChar(CharSqlType.Get(2, Collation.Default, Coercibility.CoercibleDefault), "C ");
13991399
var ckTypeDesc = SqlValue.FromNVarchar("CHECK_CONSTRAINT");
14001400
var falseDbCollation = SqlValue.FromBoolean(false);
14011401
foreach (var schema in database.Schemas.Values)
@@ -1454,7 +1454,7 @@ private static IEnumerable<SqlValue[]> EnumerateSysKeyConstraints(Parser.BatchCo
14541454
var trueBit = SqlValue.FromBoolean(true);
14551455
var falseBit = SqlValue.FromBoolean(false);
14561456
var nullPrincipal = SqlValue.Null(SqlType.Int32);
1457-
var charTwo = CharSqlType.Get(2);
1457+
var charTwo = CharSqlType.Get(2, Collation.Default, Coercibility.CoercibleDefault);
14581458
var pkType = SqlValue.FromChar(charTwo, "PK");
14591459
var uqType = SqlValue.FromChar(charTwo, "UQ");
14601460
var pkTypeDesc = SqlValue.FromNVarchar("PRIMARY_KEY_CONSTRAINT");
@@ -1507,7 +1507,7 @@ private static IEnumerable<SqlValue[]> EnumerateSysDefaultConstraints(Parser.Bat
15071507
var trueBit = SqlValue.FromBoolean(true);
15081508
var falseBit = SqlValue.FromBoolean(false);
15091509
var nullPrincipal = SqlValue.Null(SqlType.Int32);
1510-
var dfType = SqlValue.FromChar(CharSqlType.Get(2), "D ");
1510+
var dfType = SqlValue.FromChar(CharSqlType.Get(2, Collation.Default, Coercibility.CoercibleDefault), "D ");
15111511
var dfTypeDesc = SqlValue.FromNVarchar("DEFAULT_CONSTRAINT");
15121512
foreach (var schema in database.Schemas.Values)
15131513
{
@@ -1996,7 +1996,7 @@ private static IEnumerable<SqlValue[]> EnumerateSysFullTextCatalogs(Parser.Batch
19961996
/// </summary>
19971997
private static IEnumerable<SqlValue[]> EnumerateSysFullTextIndexes(Parser.BatchContext batch, Database database)
19981998
{
1999-
var charOneType = CharSqlType.Get(1);
1999+
var charOneType = CharSqlType.Get(1, Collation.Default, Coercibility.CoercibleDefault);
20002000
var trueBit = SqlValue.FromBoolean(true);
20012001
var autoCode = SqlValue.FromChar(charOneType, "A");
20022002
var autoDesc = SqlValue.FromNVarchar("AUTO");
@@ -2094,7 +2094,7 @@ private static IEnumerable<SqlValue[]> EnumerateSysXmlSchemaCollections(Parser.B
20942094
/// </summary>
20952095
private static IEnumerable<SqlValue[]> EnumerateSysXmlIndexes(Parser.BatchContext batch, Database database)
20962096
{
2097-
var charOneType = CharSqlType.Get(1);
2097+
var charOneType = CharSqlType.Get(1, Collation.Default, Coercibility.CoercibleDefault);
20982098
var typeCode = SqlValue.FromByte(3);
20992099
var typeDesc = SqlValue.FromNVarchar("XML");
21002100
var falseBit = SqlValue.FromBoolean(false);
@@ -2168,7 +2168,7 @@ private static IEnumerable<SqlValue[]> EnumerateSysSpatialIndexes(Parser.BatchCo
21682168
var falseBit = SqlValue.FromBoolean(false);
21692169
var zeroByte = SqlValue.FromByte(0);
21702170
var oneInt = SqlValue.FromInt32(1);
2171-
var nullDesc = SqlValue.Null(NVarcharSqlType.MaxForm);
2171+
var nullDesc = SqlValue.Null(NVarcharSqlType.Get(-1, Collation.Default, Coercibility.CoercibleDefault));
21722172
var geometryTypeDesc = SqlValue.FromNVarchar("GEOMETRY");
21732173
var geographyTypeDesc = SqlValue.FromNVarchar("GEOGRAPHY");
21742174
foreach (var schema in database.Schemas.Values)

SqlServerSimulator/Parser/Expression.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,12 +393,19 @@ private static void ParseWithinGroupOrderBy(AggregateExpression aggregate, Parse
393393

394394
/// <summary>
395395
/// Static type-of resolver for projection planning: returns the
396-
/// <see cref="SqlType"/> this expression will produce, given a
397-
/// resolver that maps column-name parts to their declared types. Lets a
398-
/// SELECT plan its output schema before any rows are read.
396+
/// <see cref="SqlType"/> this expression will produce, given the active
397+
/// batch (for database-affine state — in particular the database
398+
/// collation that result-type defaults should carry) and a resolver that
399+
/// maps column-name parts to their declared types. Lets a SELECT plan
400+
/// its output schema before any rows are read; the batch parameter must
401+
/// agree with the one passed to <see cref="Run"/> so the
402+
/// projection-schema type and the produced value's <see cref="SqlValue.Type"/>
403+
/// stay in parity (drift breaks union / CASE / coalesce schema and the
404+
/// row-encoder's type validation).
399405
/// </summary>
406+
/// <param name="batch">The active batch context, used to resolve database-affine defaults like the active collation.</param>
400407
/// <param name="resolveColumnType">Callback that, given a multi-part column name, returns its declared type or throws if unresolvable.</param>
401-
public abstract SqlType GetSqlType(Func<MultiPartName, SqlType> resolveColumnType);
408+
public abstract SqlType GetSqlType(BatchContext batch, Func<MultiPartName, SqlType> resolveColumnType);
402409

403410
/// <summary>
404411
/// Diagnostic-only string rendering, surfaced to debuggers via

SqlServerSimulator/Parser/Expressions/AbsoluteValue.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ public override SqlValue Run(RuntimeContext runtime)
4141
};
4242
}
4343

44-
public override SqlType GetSqlType(Func<MultiPartName, SqlType> resolveColumnType)
45-
=> MathScalars.WidenForResult(this.source.GetSqlType(resolveColumnType));
44+
public override SqlType GetSqlType(BatchContext batch, Func<MultiPartName, SqlType> resolveColumnType)
45+
=> MathScalars.WidenForResult(this.source.GetSqlType(batch, resolveColumnType));
4646

4747
/// <remarks>
4848
/// .NET's <c>Math.Abs(long)</c> throws <c>OverflowException</c> on

SqlServerSimulator/Parser/Expressions/AggregateExpression.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,15 @@ internal void BindResult(SqlValue value)
126126
public override SqlValue Run(RuntimeContext runtime) =>
127127
this.resultBound ? this.cachedResult : throw new InvalidOperationException("AggregateExpression.Run was called before its result was bound; this indicates the Selection executor didn't recognize it as an aggregate.");
128128

129-
public override SqlType GetSqlType(Func<MultiPartName, SqlType> resolveColumnType) => this.Kind switch
129+
public override SqlType GetSqlType(BatchContext batch, Func<MultiPartName, SqlType> resolveColumnType) => this.Kind switch
130130
{
131131
AggregateKind.Count => SqlType.Int32,
132132
AggregateKind.CountBig or AggregateKind.ApproxCountDistinct => SqlType.BigInt,
133133
AggregateKind.ChecksumAgg => SqlType.Int32,
134134
AggregateKind.Stdev or AggregateKind.StdevP or AggregateKind.Var or AggregateKind.VarP => SqlType.Float,
135-
AggregateKind.Max or AggregateKind.Min or AggregateKind.StringAgg => this.Operand!.GetSqlType(resolveColumnType),
136-
AggregateKind.Sum => DeriveSumResultType(this.Operand!.GetSqlType(resolveColumnType)),
137-
AggregateKind.Avg => DeriveAvgResultType(this.Operand!.GetSqlType(resolveColumnType)),
135+
AggregateKind.Max or AggregateKind.Min or AggregateKind.StringAgg => this.Operand!.GetSqlType(batch, resolveColumnType),
136+
AggregateKind.Sum => DeriveSumResultType(this.Operand!.GetSqlType(batch, resolveColumnType)),
137+
AggregateKind.Avg => DeriveAvgResultType(this.Operand!.GetSqlType(batch, resolveColumnType)),
138138
_ => throw new InvalidOperationException($"Unknown aggregate kind {this.Kind}."),
139139
};
140140

SqlServerSimulator/Parser/Expressions/Ascii.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public override SqlValue Run(RuntimeContext runtime)
3232
return SqlValue.FromInt32(firstByte[0]);
3333
}
3434

35-
public override SqlType GetSqlType(Func<MultiPartName, SqlType> resolveColumnType) => SqlType.Int32;
35+
public override SqlType GetSqlType(BatchContext batch, Func<MultiPartName, SqlType> resolveColumnType) => SqlType.Int32;
3636

3737
internal override string DebugDisplay() => $"ASCII({this.source.DebugDisplay()})";
3838
}

0 commit comments

Comments
 (0)