From 88bdc624ec5b7f53ed2ef5131e4bc007f10740aa Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:19:25 -0600 Subject: [PATCH 01/35] fix(collections): hash sets independently of element order Equal sets such as {1,2} and {2,1} used to get different hash codes, because GetHashCode hashed ToString(), which lists elements in the order they were written. Any set of sets could then miss a lookup: {{1,2}}.Contains({2,1}) was False and {{1,2},{2,1}} kept both. Sets now combine their element hashes with an order-independent sum, lists hash their elements in order, and values hash their text. Equals now also checks that both sides are the same kind (value, list or set). Before, an empty set compared equal to an empty list and to any plain value. Adds a test project (tests/SPADE.Tests, xUnit) with tests for both. Closes #9 --- DiscreteParser.csproj | 2 + Documentation/development.md | 9 +++ UtilCollection.cs | 29 ++++++++- tests/SPADE.Tests/SPADE.Tests.csproj | 25 ++++++++ .../UtilCollectionEqualityTests.cs | 61 +++++++++++++++++++ 5 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 tests/SPADE.Tests/SPADE.Tests.csproj create mode 100644 tests/SPADE.Tests/UtilCollectionEqualityTests.cs diff --git a/DiscreteParser.csproj b/DiscreteParser.csproj index a83145c..32a344b 100644 --- a/DiscreteParser.csproj +++ b/DiscreteParser.csproj @@ -9,6 +9,8 @@ Russell Phillips false false + + $(DefaultItemExcludes);tests/** diff --git a/Documentation/development.md b/Documentation/development.md index f936fcb..78728f6 100644 --- a/Documentation/development.md +++ b/Documentation/development.md @@ -1,6 +1,15 @@ # Development Feel free to contribute! Also feel free to open any issues on [github](https://github.com/Jetison333/SPADE) with bugs or feature requests. +## Tests +The tests live in tests/SPADE.Tests and use xUnit. Run them from the repository root with + +```bash +dotnet test tests/SPADE.Tests +``` + +The test project targets .NET 8, so you need the .NET 8 SDK or newer. The library itself still targets .NET 6. + ## antlr If you need to edit the grammar inside of coll.g4 file, use the following commands (you will need to install antlr4): diff --git a/UtilCollection.cs b/UtilCollection.cs index f63d236..ea3ebcb 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -273,6 +273,9 @@ public override bool Equals(object? obj) UtilCollection other = (UtilCollection)obj; + // A value, a list and a set are never equal to each other, even when empty. + if (isValue != other.isValue || isOrdered != other.isOrdered) return false; + if (isValue) { return value == other.value; @@ -296,7 +299,31 @@ public bool NotEquals(object? obj) public override int GetHashCode() { - return ToString().GetHashCode(); + if (isValue) + { + return value!.GetHashCode(); + } + + if (isOrdered) + { + HashCode hash = new HashCode(); + hash.Add(isOrdered); + foreach (UtilCollection item in list) + { + hash.Add(item); + } + return hash.ToHashCode(); + } + + // Sets are equal regardless of order, so combine element hashes with + // an order-independent sum rather than hashing ToString(), which + // follows insertion order. + int sum = 0; + foreach (UtilCollection item in set) + { + sum = unchecked(sum + item.GetHashCode()); + } + return HashCode.Combine(isOrdered, set.Count, sum); } public List ToList() diff --git a/tests/SPADE.Tests/SPADE.Tests.csproj b/tests/SPADE.Tests/SPADE.Tests.csproj new file mode 100644 index 0000000..1ff3916 --- /dev/null +++ b/tests/SPADE.Tests/SPADE.Tests.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + false + + Exe + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/tests/SPADE.Tests/UtilCollectionEqualityTests.cs b/tests/SPADE.Tests/UtilCollectionEqualityTests.cs new file mode 100644 index 0000000..1dc1a52 --- /dev/null +++ b/tests/SPADE.Tests/UtilCollectionEqualityTests.cs @@ -0,0 +1,61 @@ +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// Equal collections must hash the same, or sets of sets lose lookups (#9). +public class UtilCollectionEqualityTests +{ + private static UtilCollection C(string s) => new UtilCollection(s); + + [Theory] + [InlineData("{1,2}", "{2,1}")] + [InlineData("{1,2,3}", "{3,1,2}")] + [InlineData("{{1,2},{3}}", "{{3},{2,1}}")] + [InlineData("{(1,2),(3,4)}", "{(3,4),(1,2)}")] + [InlineData("({1,2},4)", "({2,1},4)")] + [InlineData("{}", "{}")] + public void EqualCollectionsHaveEqualHashCodes(string a, string b) + { + Assert.Equal(C(a), C(b)); + Assert.Equal(C(a).GetHashCode(), C(b).GetHashCode()); + } + + [Fact] + public void SetOfSetsContainsReorderedMember() + { + Assert.True(C("{{1,2}}").Contains(C("{2,1}"))); + } + + [Fact] + public void SetOfSetsDropsReorderedDuplicate() + { + Assert.Equal(1, C("{{1,2},{2,1}}").Count()); + } + + [Fact] + public void IntersectMatchesReorderedSets() + { + Assert.Equal(1, C("{{1,2}}").Intersect(C("{{2,1}}")).Count()); + } + + [Fact] + public void UnionMergesReorderedSets() + { + Assert.Equal(1, C("{{1,2}}").Union(C("{{2,1}}")).Count()); + } + + [Theory] + [InlineData("(1,2)", "(2,1)")] + [InlineData("{1,2}", "(1,2)")] + [InlineData("{}", "()")] + [InlineData("{}", "x")] + [InlineData("()", "x")] + [InlineData("{1}", "1")] + [InlineData("{1,2}", "{1,2,3}")] + public void DifferentCollectionsAreNotEqual(string a, string b) + { + Assert.NotEqual(C(a), C(b)); + Assert.NotEqual(C(b), C(a)); + } +} From 0f38e6fad2358a4399ea1a442c3c1e08dee910be Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:26:40 -0600 Subject: [PATCH 02/35] fix(grammar): accept set edges in unorderedcross again Since d7e982e, an unorderedcross membership check rejected every normal undirected edge such as {a,b}. The added line checked whether each node inside the edge was a list, and plain nodes never are. The check now looks at the edge itself: a plain value is not an edge, so it fails the constraint instead of crashing while being enumerated. List edges such as (a,b) are still accepted, as in the released 0.1.2, because existing grammars for directed graphs rely on it. Closes #12 --- grammar/desciptors/SetDescriptor.cs | 5 +- tests/SPADE.Tests/UnorderedCrossTests.cs | 69 ++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/SPADE.Tests/UnorderedCrossTests.cs diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index 79432a4..4157cac 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -84,9 +84,12 @@ public SetDescriptor UnorderedCross(SetDescriptor other) { Func _isMember = (a) => { + // Each member is a collection of nodes, like the edge {a,b}; a plain value is not. + // Lists such as (a,b) are still accepted because existing grammars rely on it (#12). + if (a.IsValue()) return false; + foreach (UtilCollection item in a) { - if (!item.IsOrdered()) return false; if (!isMember(item) && !other.isMember(item)) { return false; diff --git a/tests/SPADE.Tests/UnorderedCrossTests.cs b/tests/SPADE.Tests/UnorderedCrossTests.cs new file mode 100644 index 0000000..fcdab60 --- /dev/null +++ b/tests/SPADE.Tests/UnorderedCrossTests.cs @@ -0,0 +1,69 @@ +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// Undirected edges must be sets of nodes, such as {a,b} (#12). +public class UnorderedCrossTests +{ + private const string Graph = "{(N,E) | N is set, E subset N unorderedcross N}"; + private const string GraphWithK = "{((N,E),K) | N is set, E subset N unorderedcross N, K is int}"; + + private static StringParser Parse(string grammar, string instance) + { + StringParser parser = new StringParser(grammar); + parser.parse(instance); + return parser; + } + + [Theory] + [InlineData("({a,b,c},{{a,b},{b,c}})")] + [InlineData("({1,2,3},{{2,1},{3,1}})")] + [InlineData("({a,b},{})")] + public void AcceptsSetEdges(string instance) + { + StringParser parser = Parse(Graph, instance); + Assert.Equal(new UtilCollection(instance)[1], parser["E"]); + } + + [Fact] + public void AcceptsGraphWithK() + { + StringParser parser = Parse(GraphWithK, "(({1,2,3},{{1,2},{2,3}}),2)"); + Assert.Equal("2", parser["K"].ToString()); + } + + // Released versions accept list edges, and some grammars for directed + // graphs rely on it, so this keeps that behavior until they move to cross (#12). + [Theory] + [InlineData("({a,b,c},{(a,b)})")] + [InlineData("({a,b,c},{{a,b},(b,c)})")] + public void StillAcceptsListEdges(string instance) + { + StringParser parser = Parse(Graph, instance); + Assert.Equal(new UtilCollection(instance)[1], parser["E"]); + } + + [Fact] + public void RejectsListEdgesWithUnknownNodes() + { + Assert.ThrowsAny(() => Parse(Graph, "({a,b,c},{(a,d)})")); + } + + [Theory] + [InlineData("({a,b,c},{{a,d}})")] + [InlineData("({a,b,c},{{x,y}})")] + public void RejectsEdgesWithUnknownNodes(string instance) + { + Assert.ThrowsAny(() => Parse(Graph, instance)); + } + + [Fact] + public void RejectsPlainValueEdgeAsConstraintFailure() + { + // A plain value is not an edge. It should fail the constraint, not + // crash while trying to enumerate the value. + Exception e = Assert.ThrowsAny(() => Parse(Graph, "({a,b},{a})")); + Assert.IsNotType(e); + } +} From 52b710fce7a77828f65198686295e3f5ec0084e1 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:33:14 -0600 Subject: [PATCH 03/35] fix(grammar): reject list edges in unorderedcross An unorderedcross member is an unordered pair, so it must be a set such as {a,b}. A list such as (a,b) is an ordered pair, which is what cross describes, so it is now rejected here too. This is what d7e982e set out to do. Grammars for directed graphs that use unorderedcross with list edges need to switch to cross. Refs #12 --- grammar/desciptors/SetDescriptor.cs | 6 +++--- tests/SPADE.Tests/UnorderedCrossTests.cs | 13 ++++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index 4157cac..f90b869 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -84,9 +84,9 @@ public SetDescriptor UnorderedCross(SetDescriptor other) { Func _isMember = (a) => { - // Each member is a collection of nodes, like the edge {a,b}; a plain value is not. - // Lists such as (a,b) are still accepted because existing grammars rely on it (#12). - if (a.IsValue()) return false; + // Each member is a set of nodes, like the edge {a,b}. A list such as (a,b) + // is an ordered pair and belongs to cross, and a plain value is not an edge. + if (a.IsValue() || a.IsOrdered()) return false; foreach (UtilCollection item in a) { diff --git a/tests/SPADE.Tests/UnorderedCrossTests.cs b/tests/SPADE.Tests/UnorderedCrossTests.cs index fcdab60..45682d5 100644 --- a/tests/SPADE.Tests/UnorderedCrossTests.cs +++ b/tests/SPADE.Tests/UnorderedCrossTests.cs @@ -33,21 +33,20 @@ public void AcceptsGraphWithK() Assert.Equal("2", parser["K"].ToString()); } - // Released versions accept list edges, and some grammars for directed - // graphs rely on it, so this keeps that behavior until they move to cross (#12). + // A list edge is an ordered pair, which belongs to cross, not unorderedcross. [Theory] [InlineData("({a,b,c},{(a,b)})")] [InlineData("({a,b,c},{{a,b},(b,c)})")] - public void StillAcceptsListEdges(string instance) + public void RejectsListEdges(string instance) { - StringParser parser = Parse(Graph, instance); - Assert.Equal(new UtilCollection(instance)[1], parser["E"]); + Assert.ThrowsAny(() => Parse(Graph, instance)); } [Fact] - public void RejectsListEdgesWithUnknownNodes() + public void CrossAcceptsListEdges() { - Assert.ThrowsAny(() => Parse(Graph, "({a,b,c},{(a,d)})")); + StringParser parser = Parse("{(N,E) | N is set, E subset N cross N}", "({a,b,c},{(a,b),(b,a)})"); + Assert.Equal(2, parser["E"].Count()); } [Theory] From aa5b2e22330eabe4805b76168dbf0b9c0188f722 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:39:54 -0600 Subject: [PATCH 04/35] fix(grammar): make is int, is set and is list check their type "N is int" checked nothing at all: int reaches the constraint as a set descriptor, and the "is" branch only handled the set, list and int keywords. So abc, {1} and (1,2) all passed. It now requires a single value that parses as an integer. "S is set" accepted a plain value, because a value is not ordered. It now requires an unordered collection. "is list" already rejected values and sets; it is now covered by tests. Closes #10 --- grammar/codeGen.cs | 7 ++ grammar/desciptors/ConstraintDescriptor.cs | 6 +- tests/SPADE.Tests/TypeConstraintTests.cs | 87 ++++++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 tests/SPADE.Tests/TypeConstraintTests.cs diff --git a/grammar/codeGen.cs b/grammar/codeGen.cs index 83ea6ef..07bcf6b 100644 --- a/grammar/codeGen.cs +++ b/grammar/codeGen.cs @@ -125,6 +125,13 @@ public void ExitConstraint([NotNull] collParser.ConstraintContext context) { constraintDescriptorStack.Push(new ConstraintDescriptor(context.TERM().ToString()!, context.DEFINITION().ToString()!, context.@object().BUILT_IN().ToString()!)); } + else if (context.@object().BUILT_IN_SET() != null && context.DEFINITION().ToString() == "is") + { + // "N is int" is a type check, like "N is set". The integer set descriptor + // pushed for it is only needed by other definitions, such as "S subset int". + setDescriptorStack.Pop(); + constraintDescriptorStack.Push(new ConstraintDescriptor(context.TERM().ToString()!, context.DEFINITION().ToString()!, context.@object().BUILT_IN_SET().ToString()!)); + } else { constraintDescriptorStack.Push(new ConstraintDescriptor(context.TERM().ToString()!, context.DEFINITION().ToString()!, setDescriptorStack.Pop())); diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 70ff95e..2ae1af6 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -33,7 +33,7 @@ public void check(Dictionary map) switch (built_in) { case "set": - if (map[term].IsOrdered()) + if (map[term].IsValue() || map[term].IsOrdered()) { throw new Exception($"Constraint {term} {definition} {built_in} failed because {term} was not a set"); } @@ -45,9 +45,9 @@ public void check(Dictionary map) } break; case "int": - if (!map[term].IsValue()) + if (!map[term].IsValue() || !int.TryParse(map[term].ToString(), out _)) { - throw new Exception($"Constraint {term} {definition} {built_in} failed because {term} was not a value"); + throw new Exception($"Constraint {term} {definition} {built_in} failed because {map[term]} was not an integer"); } break; } diff --git a/tests/SPADE.Tests/TypeConstraintTests.cs b/tests/SPADE.Tests/TypeConstraintTests.cs new file mode 100644 index 0000000..40194f9 --- /dev/null +++ b/tests/SPADE.Tests/TypeConstraintTests.cs @@ -0,0 +1,87 @@ +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// "is int", "is set" and "is list" must check the type they name (#10). +public class TypeConstraintTests +{ + private static void Parse(string grammar, string instance) + { + new StringParser(grammar).parse(instance); + } + + [Theory] + [InlineData("3")] + [InlineData("-12")] + [InlineData("0")] + public void IsIntAcceptsIntegers(string instance) + { + Parse("{N | N is int}", instance); + } + + [Theory] + [InlineData("abc")] + [InlineData("1.5")] + [InlineData("{1}")] + [InlineData("(1,2)")] + public void IsIntRejectsNonIntegers(string instance) + { + Assert.ThrowsAny(() => Parse("{N | N is int}", instance)); + } + + [Fact] + public void IsIntChecksNestedFields() + { + string grammar = "{((N,E),K) | N is set, E subset N unorderedcross N, K is int}"; + Parse(grammar, "(({1,2},{{1,2}}),2)"); + Assert.ThrowsAny(() => Parse(grammar, "(({1,2},{{1,2}}),two)")); + } + + [Fact] + public void IsIntChecksInsideSetComprehension() + { + string grammar = "{(N,E) | N is set, E subset {(e, w) | e is N unorderedcross N, w is int}}"; + Parse(grammar, "({1,2,3},{({1,2},4),({2,3},5)})"); + Assert.ThrowsAny(() => Parse(grammar, "({1,2,3},{({1,2},heavy)})")); + } + + [Theory] + [InlineData("{1,2}")] + [InlineData("{}")] + public void IsSetAcceptsSets(string instance) + { + Parse("{S | S is set}", instance); + } + + [Theory] + [InlineData("balanced")] + [InlineData("(1,2)")] + public void IsSetRejectsValuesAndLists(string instance) + { + Assert.ThrowsAny(() => Parse("{S | S is set}", instance)); + } + + [Theory] + [InlineData("(1,2)")] + [InlineData("()")] + public void IsListAcceptsLists(string instance) + { + Parse("{L | L is list}", instance); + } + + [Theory] + [InlineData("balanced")] + [InlineData("{1,2}")] + public void IsListRejectsValuesAndSets(string instance) + { + Assert.ThrowsAny(() => Parse("{L | L is list}", instance)); + } + + [Fact] + public void SubsetIntStillChecksEveryElement() + { + Parse("{S | S subset int}", "{1,2,3}"); + Assert.ThrowsAny(() => Parse("{S | S subset int}", "{1,2,x}")); + } +} From 8ba6e46b6b6cd15f008e1b9679cdf53d2bbfa828 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:00:30 -0600 Subject: [PATCH 05/35] feat(parser): read instance strings in one pass Replaces the substring-based reader in UtilCollection(string) with InstanceScanner, which walks the input once. Whitespace (#2): spaces, tabs and line breaks around { } ( ) , and at either end of the input are skipped, so {2, 3} now holds 3 and a trailing new line no longer breaks an instance. Inside a value, whitespace next to [ ] ; : = is dropped, plain spaces are kept (New York), and a run with a tab or line break becomes one space. These match the rules Redux applies before parsing today. Errors (#4): malformed input throws SpadeParseException with the position, what was expected and what was found, e.g. "expected , or } at position 5, found end of input" for {1,2. Input that used to be misread without an error, such as {1,2 or {1,2}junk, is now rejected. Time and depth (#7): parsing is linear in the input's length, and ToString builds its text in one buffer. Nesting deeper than 256 levels is rejected instead of overflowing the stack and crashing the process. 40,000 elements took 104 seconds before; 100,000 now take well under a second. Closes #2 Closes #7 Refs #4 --- Documentation/InstanceString.md | 29 ++++- InstanceScanner.cs | 152 ++++++++++++++++++++++ SpadeParseException.cs | 32 +++++ UtilCollection.cs | 121 ++++++----------- tests/SPADE.Tests/InstanceScannerTests.cs | 146 +++++++++++++++++++++ 5 files changed, 396 insertions(+), 84 deletions(-) create mode 100644 InstanceScanner.cs create mode 100644 SpadeParseException.cs create mode 100644 tests/SPADE.Tests/InstanceScannerTests.cs diff --git a/Documentation/InstanceString.md b/Documentation/InstanceString.md index 3b529e9..9fbb377 100644 --- a/Documentation/InstanceString.md +++ b/Documentation/InstanceString.md @@ -11,4 +11,31 @@ So a set may look something like {a,b,c}. A list **V**, will be represented usin ``` (e_0,...,e_k) ``` -as such, an example of a list could look something like (1,2,3). Also note that since an element of a set or list may also be a set of a list, so a set of lists can be represented as {(1,2),(2,3)} \ No newline at end of file +as such, an example of a list could look something like (1,2,3). Also note that since an element of a set or list may also be a set of a list, so a set of lists can be represented as {(1,2),(2,3)} + +## Whitespace +Whitespace is ignored where it can't be part of a value, so an instance can be spread over several lines: + +``` +{ + (1, 2), + (2, 3) +} +``` + +reads the same as `{(1,2),(2,3)}`. The rules are: + +- Spaces, tabs and line breaks before or after `{ } ( ) ,`, and at the start and end of the input, are skipped. +- Inside a value, whitespace next to `[ ] ; : =` is dropped, so `x1 : True` reads as `x1:True`. +- Other whitespace inside a value is kept. Plain spaces stay as typed, so `{New York, Los Angeles}` holds `New York`. A run containing a tab or line break becomes a single space, so a value wrapped onto two lines, `New` and `York`, reads as `New York`. + +## Errors +An instance string that can't be read throws `SpadeParseException`, which says where the problem is, what was expected and what was found: + +``` +expected , or } at position 5, found end of input +``` + +for `{1,2`. Its `Position` (counting from 1), `Expected` and `Found` properties hold the same details. Nothing is guessed: unbalanced braces, extra text after the instance, empty elements such as `{1,,2}`, and empty input are all errors. + +Sets and lists can be nested up to 256 levels deep. Deeper input is rejected with a `SpadeParseException` rather than risking a stack overflow. diff --git a/InstanceScanner.cs b/InstanceScanner.cs new file mode 100644 index 0000000..12cdce3 --- /dev/null +++ b/InstanceScanner.cs @@ -0,0 +1,152 @@ +using System.Text; + +namespace SPADE; + +/// +/// Reads an instance string such as ({1,2},{(1,2)}) into a +/// in one pass, so the time taken grows in proportion to the input's length. +/// +/// +/// Whitespace rules (#2): +/// +/// Whitespace before or after { } ( ) , and at either end of the input is skipped. +/// Inside a value, whitespace next to [ ] ; : = is dropped, so x1 : True reads as x1:True. +/// Any other whitespace inside a value is kept. A run of plain spaces stays exactly as typed +/// (New York); a run containing a tab or line break becomes one space, since a line break +/// inside a value is a wrap (New\nYork reads as New York). +/// +/// These match the rules Redux applied before parsing, so that layer can be removed. +/// +internal class InstanceScanner +{ + /// + /// Deepest nesting of sets and lists accepted. Far beyond any real instance; it stops + /// deeply nested input from overflowing the stack and crashing the process (#7). + /// + public const int MaxDepth = 256; + + // Characters that end a value. + private const string Structural = "{}(),"; + + // Characters inside a value that whitespace never belongs next to. + private const string ValueDelimiters = "[];:="; + + private readonly string text; + private int pos; + + private InstanceScanner(string text) + { + this.text = text; + } + + public static UtilCollection Parse(string text) + { + if (text == null) throw new ArgumentNullException(nameof(text)); + return new InstanceScanner(text).ParseInstance(); + } + + private UtilCollection ParseInstance() + { + SkipWhitespace(); + if (AtEnd()) throw Error("a value, { or ("); + UtilCollection result = ParseElement(0); + SkipWhitespace(); + if (!AtEnd()) throw Error("end of input"); + return result; + } + + // Expects pos at the first character of an element, after any whitespace. + private UtilCollection ParseElement(int depth) + { + char c = text[pos]; + if (c == '{' || c == '(') return ParseCollection(depth + 1); + if (Structural.IndexOf(c) >= 0) throw Error("a value, { or ("); + return ParseValue(); + } + + private UtilCollection ParseCollection(int depth) + { + if (depth > MaxDepth) throw Error($"at most {MaxDepth} levels of nesting"); + + char open = text[pos]; + char close = open == '{' ? '}' : ')'; + pos++; + + List items = new(); + SkipWhitespace(); + if (!AtEnd() && text[pos] == close) + { + pos++; + } + else + { + while (true) + { + SkipWhitespace(); + if (AtEnd()) throw Error("a value, { or ("); + items.Add(ParseElement(depth)); + + SkipWhitespace(); + if (AtEnd() || (text[pos] != ',' && text[pos] != close)) throw Error($", or {close}"); + if (text[pos++] == close) break; + } + } + + if (open == '(') return new UtilCollection(items); + return new UtilCollection(items.ToHashSet()); + } + + private UtilCollection ParseValue() + { + int start = pos; + while (!AtEnd() && Structural.IndexOf(text[pos]) < 0) pos++; + return UtilCollection.FromValue(NormalizeValue(start, pos)); + } + + // Applies the whitespace rules to text[start..end). text[start] is never whitespace. + private string NormalizeValue(int start, int end) + { + StringBuilder sb = new(end - start); + int i = start; + while (i < end) + { + if (!char.IsWhiteSpace(text[i])) + { + sb.Append(text[i++]); + continue; + } + + int j = i; + bool plainSpaces = true; + while (j < end && char.IsWhiteSpace(text[j])) + { + if (text[j] != ' ') plainSpaces = false; + j++; + } + + bool trailing = j == end; + bool besideDelimiter = ValueDelimiters.IndexOf(sb[sb.Length - 1]) >= 0 + || (!trailing && ValueDelimiters.IndexOf(text[j]) >= 0); + if (!trailing && !besideDelimiter) + { + if (plainSpaces) sb.Append(text, i, j - i); + else sb.Append(' '); + } + i = j; + } + return sb.ToString(); + } + + private void SkipWhitespace() + { + while (!AtEnd() && char.IsWhiteSpace(text[pos])) pos++; + } + + private bool AtEnd() => pos >= text.Length; + + private SpadeParseException Error(string expected) + { + string found = AtEnd() ? "end of input" : text[pos].ToString(); + return new SpadeParseException(pos + 1, expected, found); + } +} diff --git a/SpadeParseException.cs b/SpadeParseException.cs new file mode 100644 index 0000000..be2ca02 --- /dev/null +++ b/SpadeParseException.cs @@ -0,0 +1,32 @@ +namespace SPADE; + +/// +/// Thrown when an instance string can't be read or doesn't fit its definition string. +/// Every bad-input case throws this type, so callers can tell bad input apart from a bug. +/// +public class SpadeParseException : FormatException +{ + /// Character position in the instance string, counting from 1, when the error has one. + public int? Position { get; } + + /// What the parser expected at , when the error has one. + public string? Expected { get; } + + /// What the parser found instead, when the error has one. + public string? Found { get; } + + /// A syntax error at a position, e.g. "expected , or } at position 14, found x". + public SpadeParseException(int position, string expected, string found) + : base($"expected {expected} at position {position}, found {found}") + { + Position = position; + Expected = expected; + Found = found; + } + + /// An error with no single position, such as a failed constraint. + public SpadeParseException(string message) + : base(message) + { + } +} diff --git a/UtilCollection.cs b/UtilCollection.cs index ea3ebcb..6a70fe7 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Runtime.Serialization; +using System.Text; namespace SPADE; @@ -20,39 +21,26 @@ public UtilCollection() } + /// + /// Reads an instance string such as ({1,2},{(1,2)}). Throws + /// if it isn't a well-formed value, set or list. + /// See for how whitespace is handled. + /// public UtilCollection(string instance) { - if (instance[0] == '{') - { - isOrdered = false; - isValue = false; - instance = instance.Substring(1, instance.Length - 2); - while (instance != "") - { - string item = findMatchingBrace(instance); - set.Add(new UtilCollection(item)); - instance = instance.Substring(item.Length).TrimStart(','); - } - } - else if (instance[0] == '(') - { - isOrdered = true; - isValue = false; - instance = instance.Substring(1, instance.Length - 2); - while (instance != "") - { - string item = findMatchingBrace(instance); - list.Add(new UtilCollection(item)); - instance = instance.Substring(item.Length).TrimStart(','); - } - } - else - { - isValue = true; - value = instance; - } + UtilCollection parsed = InstanceScanner.Parse(instance); + set = parsed.set; + list = parsed.list; + value = parsed.value; + isOrdered = parsed.isOrdered; + isValue = parsed.isValue; } - + + internal static UtilCollection FromValue(string value) + { + return new UtilCollection { value = value, isValue = true }; + } + public UtilCollection(HashSet set_) { isOrdered = false; @@ -85,43 +73,6 @@ public UtilCollection(HashSet set__) isOrdered = false; } - private string findMatchingBrace(string instance) - { - Dictionary braces = new Dictionary - { - { '(', ')' }, - { '{', '}' }, - }; - - if (!braces.ContainsKey(instance[0])) - { - return instance.Split(',')[0]; - } - - Stack stack = new Stack(); - - for (int i = 0; i < instance.Length; i++) - { - if (braces.ContainsKey(instance[i])) - { - stack.Push(instance[i]); - } - else if (braces.Values.Contains(instance[i])) - { - char openBracket = stack.Pop(); - if (braces[openBracket] != instance[i]) - { - throw new Exception("Braces not matched"); - } - if (stack.Count() == 0) - { - return instance.Substring(0, i + 1); - } - } - } - throw new Exception("Braces not matched"); - } - public UtilCollection this[int index] { get { @@ -344,25 +295,29 @@ override public string ToString() { return value!; } - - string str = ""; - if (isOrdered) - str += "("; - else - str += "{"; - foreach (var item in this) + StringBuilder sb = new StringBuilder(); + AppendTo(sb); + return sb.ToString(); + } + + // Builds the text in one buffer; adding strings with += copied it once per element. + private void AppendTo(StringBuilder sb) + { + if (isValue) { - //Console.WriteLine(item.ToString()); - str += item.ToString() + ","; + sb.Append(value); + return; } - str = str.TrimEnd(','); - if (isOrdered) - str += ")"; - else - str += "}"; - - return str; + sb.Append(isOrdered ? '(' : '{'); + bool first = true; + foreach (UtilCollection item in ToList()) + { + if (!first) sb.Append(','); + item.AppendTo(sb); + first = false; + } + sb.Append(isOrdered ? ')' : '}'); } } diff --git a/tests/SPADE.Tests/InstanceScannerTests.cs b/tests/SPADE.Tests/InstanceScannerTests.cs new file mode 100644 index 0000000..4954f24 --- /dev/null +++ b/tests/SPADE.Tests/InstanceScannerTests.cs @@ -0,0 +1,146 @@ +using System.Diagnostics; +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// Reading instance strings: whitespace (#2), clear errors (#4), and bounded time and depth (#7). +public class InstanceScannerTests +{ + private static UtilCollection C(string s) => new UtilCollection(s); + + // --- Whitespace (#2) --- + + [Theory] + [InlineData("{2, 3}", "{2,3}")] + [InlineData("{1,2}\n", "{1,2}")] + [InlineData(" \t{ 1 ,\r\n 2 }\r\n", "{1,2}")] + [InlineData("( {1, 2} , 4 )", "({1,2},4)")] + [InlineData("{\n (a, b),\n (c, d)\n}", "{(a,b),(c,d)}")] + [InlineData(" 3 ", "3")] + public void SkipsWhitespaceAroundStructure(string input, string expected) + { + Assert.Equal(C(expected), C(input)); + Assert.Equal(expected, C(input).ToString()); + } + + [Fact] + public void SpacedSetContainsItsValues() + { + Assert.True(C("{2, 3}").Contains(C("3"))); + } + + [Theory] + [InlineData("{New York, Los Angeles}", "New York")] + [InlineData("{x1 | !x2, x3}", "x1 | !x2")] + [InlineData("{a b}", "a b")] + public void KeepsSpacesInsideValues(string input, string value) + { + Assert.True(C(input).Contains(C(value))); + Assert.Equal(value, C(input).ToList().First(v => v.ToString().Contains(' ')).ToString()); + } + + [Theory] + [InlineData("{New\nYork}", "New York")] + [InlineData("{New \r\n York}", "New York")] + [InlineData("{a\tb}", "a b")] + public void LineBreaksAndTabsInsideValuesBecomeOneSpace(string input, string value) + { + Assert.Equal("{" + value + "}", C(input).ToString()); + } + + [Theory] + [InlineData("{x1 : True}", "{x1:True}")] + [InlineData("{a = 1, b= 2}", "{a=1,b=2}")] + [InlineData("(1 ; 2 ; 3)", "(1;2;3)")] + [InlineData("{[ 1 ]}", "{[1]}")] + public void DropsWhitespaceBesideDelimitersInsideValues(string input, string expected) + { + Assert.Equal(expected, C(input).ToString()); + } + + // --- Errors (#4) --- + + [Theory] + [InlineData("", "expected a value, { or ( at position 1, found end of input")] + [InlineData(" ", "expected a value, { or ( at position 4, found end of input")] + [InlineData("{1,2", "expected , or } at position 5, found end of input")] + [InlineData("{1,2}junk", "expected end of input at position 6, found j")] + [InlineData("{1,(2))}", "expected , or } at position 7, found )")] + [InlineData("{(1,2},3)", "expected , or ) at position 6, found }")] + [InlineData("}", "expected a value, { or ( at position 1, found }")] + [InlineData("{1,,2}", "expected a value, { or ( at position 4, found ,")] + [InlineData("{1,}", "expected a value, { or ( at position 4, found }")] + [InlineData("{,1}", "expected a value, { or ( at position 2, found ,")] + [InlineData("f(x)", "expected end of input at position 2, found (")] + [InlineData("{1, 2} x", "expected end of input at position 8, found x")] + public void MalformedInputThrowsWithPosition(string input, string message) + { + SpadeParseException e = Assert.Throws(() => C(input)); + Assert.Equal(message, e.Message); + } + + [Fact] + public void ErrorExposesPositionExpectedAndFound() + { + SpadeParseException e = Assert.Throws(() => C("{1,2}junk")); + Assert.Equal(6, e.Position); + Assert.Equal("end of input", e.Expected); + Assert.Equal("j", e.Found); + } + + [Fact] + public void ParseErrorIsAFormatException() + { + Assert.IsAssignableFrom(Assert.Throws(() => C("{"))); + } + + // --- Depth and time (#7) --- + + // InstanceScanner.MaxDepth, which is internal. + private const int MaxDepth = 256; + + private static string Nested(int depth) => new string('{', depth) + new string('}', depth); + + [Fact] + public void AcceptsNestingUpToTheLimit() + { + C(Nested(MaxDepth)); + } + + [Theory] + [InlineData(MaxDepth + 1)] + [InlineData(100_000)] + public void RejectsDeeperNestingWithoutCrashing(int depth) + { + SpadeParseException e = Assert.Throws(() => C(Nested(depth))); + Assert.Equal(MaxDepth + 1, e.Position); + } + + [Fact] + public void LargeFlatSetParsesQuickly() + { + // 40,000 elements took 104 seconds before; this is 100,000. + string input = "{" + string.Join(", ", Enumerable.Range(0, 100_000)) + "}"; + Stopwatch timer = Stopwatch.StartNew(); + UtilCollection set = C(input); + string text = set.ToString(); + timer.Stop(); + + Assert.Equal(100_000, set.Count()); + Assert.Equal(input.Replace(" ", "").Length, text.Length); + Assert.True(timer.Elapsed < TimeSpan.FromSeconds(5), $"took {timer.Elapsed}"); + } + + [Fact] + public void DeepNestingParsesQuickly() + { + // Depth 4,000 took 103 seconds before; the limit is lower, so check many limit-depth parses. + string input = Nested(MaxDepth); + Stopwatch timer = Stopwatch.StartNew(); + for (int i = 0; i < 100; i++) C(input); + timer.Stop(); + + Assert.True(timer.Elapsed < TimeSpan.FromSeconds(5), $"took {timer.Elapsed}"); + } +} From 3935e696e8470d9bf5bd22ff853bb5b63a989e58 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:00:31 -0600 Subject: [PATCH 06/35] fix(grammar): report shape and constraint failures as SpadeParseException Input that is well formed but doesn't fit the definition string threw whatever happened to fail: InvalidOperationException ("Cannot index into a set"), ArgumentOutOfRangeException, or a bare Exception whose message said PLACEHOLDER. All of these now throw SpadeParseException with a message that names what failed: - A format such as (N,E) checks that it got a list of exactly that many elements. A longer list used to be accepted and the extra elements dropped. - Constraint failures quote the constraint as written, e.g. "Constraint E subset N unorderedcross N failed: {1,9} is not a member". Long values are shortened. - subset on a plain value, and cross on anything that isn't a pair, fail the constraint instead of crashing. Closes #4 --- Documentation/example.md | 19 ++++++ grammar/codeGen.cs | 14 ++-- grammar/desciptors/ConstraintDescriptor.cs | 33 ++++++++-- grammar/desciptors/SetDescriptor.cs | 3 +- grammar/desciptors/formatDescriptor.cs | 6 +- tests/SPADE.Tests/DefinitionErrorTests.cs | 76 ++++++++++++++++++++++ tests/SPADE.Tests/TypeConstraintTests.cs | 12 ++-- tests/SPADE.Tests/UnorderedCrossTests.cs | 6 +- 8 files changed, 148 insertions(+), 21 deletions(-) create mode 100644 tests/SPADE.Tests/DefinitionErrorTests.cs diff --git a/Documentation/example.md b/Documentation/example.md index df23deb..2b7dbe7 100644 --- a/Documentation/example.md +++ b/Documentation/example.md @@ -28,3 +28,22 @@ foreach (UtilCollection c in graph["E"]) ``` Above is example code for parsing a graph type problem. The StringParser class is provided with a [definition string](DefinitionString.md), and then with a [instance string](InstanceString.md). It then parses and verifies the instance string. Now, you can access items of that problem by using a key access, which returns a [UtilCollection](utilcollection.md) class, which can then be further used however you need to use it. + +## Handling bad input + +Every problem with an instance string throws `SpadeParseException`: text that can't be read, a shape that doesn't match the definition string, or a failed constraint. Catch it to report bad input, for example as a 400 response: + +```c# +try +{ + graph.parse(instanceString); +} +catch (SpadeParseException e) +{ + // e.g. "expected , or } at position 5, found end of input" + // or "Constraint E subset N cross N failed: (A,X) is not a member" + Console.WriteLine(e.Message); +} +``` + +Any other exception type means a bug in SPADE or in the definition string, not bad input. diff --git a/grammar/codeGen.cs b/grammar/codeGen.cs index 07bcf6b..9aaa8f0 100644 --- a/grammar/codeGen.cs +++ b/grammar/codeGen.cs @@ -35,6 +35,12 @@ public void Parse(UtilCollection utilCollection) setDescriptor!.isMember(utilCollection); } + // A rule's text as written in the definition string, spacing included, for error messages. + private static string SourceText(ParserRuleContext context) + { + return context.Start.InputStream.GetText(Interval.Of(context.Start.StartIndex, context.Stop.StopIndex)); + } + /// /// Enter a parse tree produced by . /// @@ -91,7 +97,7 @@ public void ExitFormat([NotNull] collParser.FormatContext context) lst.Add(formatStack.Pop()); } lst.Reverse(); // have to reverse the list, since the leftmost should be the one that was popped first - formatStack.Push(new FormatDescriptor(lst)); + formatStack.Push(new FormatDescriptor(lst, SourceText(context))); } if (context.TERM() != null) { @@ -123,18 +129,18 @@ public void ExitConstraint([NotNull] collParser.ConstraintContext context) { if (context.@object().BUILT_IN() != null) { - constraintDescriptorStack.Push(new ConstraintDescriptor(context.TERM().ToString()!, context.DEFINITION().ToString()!, context.@object().BUILT_IN().ToString()!)); + constraintDescriptorStack.Push(new ConstraintDescriptor(context.TERM().ToString()!, context.DEFINITION().ToString()!, context.@object().BUILT_IN().ToString()!, SourceText(context))); } else if (context.@object().BUILT_IN_SET() != null && context.DEFINITION().ToString() == "is") { // "N is int" is a type check, like "N is set". The integer set descriptor // pushed for it is only needed by other definitions, such as "S subset int". setDescriptorStack.Pop(); - constraintDescriptorStack.Push(new ConstraintDescriptor(context.TERM().ToString()!, context.DEFINITION().ToString()!, context.@object().BUILT_IN_SET().ToString()!)); + constraintDescriptorStack.Push(new ConstraintDescriptor(context.TERM().ToString()!, context.DEFINITION().ToString()!, context.@object().BUILT_IN_SET().ToString()!, SourceText(context))); } else { - constraintDescriptorStack.Push(new ConstraintDescriptor(context.TERM().ToString()!, context.DEFINITION().ToString()!, setDescriptorStack.Pop())); + constraintDescriptorStack.Push(new ConstraintDescriptor(context.TERM().ToString()!, context.DEFINITION().ToString()!, setDescriptorStack.Pop(), SourceText(context))); } } /// diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 2ae1af6..1cbd7c8 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -8,19 +8,36 @@ class ConstraintDescriptor String definition; SetDescriptor? set; String? built_in; + // The constraint as written in the definition string, e.g. "E subset N unorderedcross N". + String text; - public ConstraintDescriptor(String _term, String _definition, SetDescriptor _set) + public ConstraintDescriptor(String _term, String _definition, SetDescriptor _set, String _text) { term = _term; definition = _definition; set = _set; + text = _text; } - public ConstraintDescriptor(String _term, String _definition, String _built_in) + public ConstraintDescriptor(String _term, String _definition, String _built_in, String _text) { term = _term; definition = _definition; built_in = _built_in; + text = _text; + } + + // Values in error messages are shortened, so a huge instance doesn't make a huge message. + public static string Describe(UtilCollection collection) + { + const int maxLength = 60; + string s = collection.ToString(); + return s.Length <= maxLength ? s : s.Substring(0, maxLength) + "..."; + } + + private SpadeParseException Failure(string reason) + { + return new SpadeParseException($"Constraint {text} failed: {reason}"); } @@ -35,19 +52,19 @@ public void check(Dictionary map) case "set": if (map[term].IsValue() || map[term].IsOrdered()) { - throw new Exception($"Constraint {term} {definition} {built_in} failed because {term} was not a set"); + throw Failure($"{Describe(map[term])} is not a set"); } break; case "list": if (map[term].IsUnordered()) { - throw new Exception($"Constraint {term} {definition} {built_in} failed because {term} was not a list"); + throw Failure($"{Describe(map[term])} is not a list"); } break; case "int": if (!map[term].IsValue() || !int.TryParse(map[term].ToString(), out _)) { - throw new Exception($"Constraint {term} {definition} {built_in} failed because {map[term]} was not an integer"); + throw Failure($"{Describe(map[term])} is not an integer"); } break; } @@ -56,11 +73,15 @@ public void check(Dictionary map) case "subset": if (set != null) { + if (map[term].IsValue()) + { + throw Failure($"{Describe(map[term])} is not a set"); + } foreach (UtilCollection item in map[term]) { if (!set.isMember(item)) { - throw new Exception($"Constraint {term} {definition} PLACEHOLDER failed because {item} was not a subset of PLACEHOLDER"); + throw Failure($"{Describe(item)} is not a member"); } } } diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index f90b869..282cb65 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -75,7 +75,8 @@ public SetDescriptor(Dictionary _map, FormatDescriptor f public SetDescriptor Cross(SetDescriptor other) { - Func _isMember = (a) => isMember(a[0]) && other.isMember(a[1]); + Func _isMember = (a) => + a.IsOrdered() && a.Count() == 2 && isMember(a[0]) && other.isMember(a[1]); Func _genSet = () => genSet().Cross(other.genSet()); return new SetDescriptor(map, _isMember, _genSet); } diff --git a/grammar/desciptors/formatDescriptor.cs b/grammar/desciptors/formatDescriptor.cs index d86f1ab..bdce081 100644 --- a/grammar/desciptors/formatDescriptor.cs +++ b/grammar/desciptors/formatDescriptor.cs @@ -13,10 +13,14 @@ public FormatDescriptor(String term) parseFunc = (map, coll) => {map[term] = coll;}; } - public FormatDescriptor(List lst) + public FormatDescriptor(List lst, String text) { parseFunc = (map, coll) => { + if (!coll.IsOrdered() || coll.Count() != lst.Count()) + { + throw new SpadeParseException($"expected a list of {lst.Count()} elements for {text}, found {ConstraintDescriptor.Describe(coll)}"); + } for (int i = 0; i < lst.Count(); i++) { lst[i].parseFunc(map, coll[i]); diff --git a/tests/SPADE.Tests/DefinitionErrorTests.cs b/tests/SPADE.Tests/DefinitionErrorTests.cs new file mode 100644 index 0000000..05f3ab9 --- /dev/null +++ b/tests/SPADE.Tests/DefinitionErrorTests.cs @@ -0,0 +1,76 @@ +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// Input that is well formed but doesn't fit the definition string throws SpadeParseException +// with a message naming what failed (#4). +public class DefinitionErrorTests +{ + private const string Graph = "{(N,E) | N is set, E subset N unorderedcross N}"; + + private static SpadeParseException Fails(string grammar, string instance) + { + return Assert.Throws(() => new StringParser(grammar).parse(instance)); + } + + [Theory] + [InlineData("({1,2},{{1,2}},extra)", "expected a list of 2 elements for (N,E), found ({1,2},{{1,2}},extra)")] + [InlineData("({1,2})", "expected a list of 2 elements for (N,E), found ({1,2})")] + [InlineData("{{1,2},{{1,2}}}", "expected a list of 2 elements for (N,E), found ")] + [InlineData("graph", "expected a list of 2 elements for (N,E), found graph")] + public void WrongShapeNamesTheFormat(string instance, string messageStart) + { + Assert.StartsWith(messageStart, Fails(Graph, instance).Message); + } + + [Fact] + public void FailedSubsetNamesConstraintAndMember() + { + Assert.Equal( + "Constraint E subset N unorderedcross N failed: {1,9} is not a member", + Fails(Graph, "({1,2},{{1,9}})").Message); + } + + [Fact] + public void SubsetOfAValueFailsCleanly() + { + Assert.Equal( + "Constraint E subset N unorderedcross N failed: edges is not a set", + Fails(Graph, "({1,2},edges)").Message); + } + + [Theory] + [InlineData("{N | N is int}", "abc", "Constraint N is int failed: abc is not an integer")] + [InlineData("{S | S is set}", "word", "Constraint S is set failed: word is not a set")] + [InlineData("{L | L is list}", "{1}", "Constraint L is list failed: {1} is not a list")] + public void FailedTypeCheckNamesConstraint(string grammar, string instance, string message) + { + Assert.Equal(message, Fails(grammar, instance).Message); + } + + [Fact] + public void LongValuesAreShortenedInMessages() + { + string big = "{" + string.Join(",", Enumerable.Range(0, 1000)) + "}"; + string message = Fails("{L | L is list}", big).Message; + Assert.True(message.Length < 150, message); + Assert.Contains("...", message); + } + + [Theory] + [InlineData("({a,b},{{a,b}})")] + [InlineData("({a,b},{(a)})")] + [InlineData("({a,b},{(a,b,a)})")] + [InlineData("({a,b},{x})")] + public void CrossRejectsNonPairsCleanly(string instance) + { + Fails("{(N,E) | N is set, E subset N cross N}", instance); + } + + [Fact] + public void SyntaxErrorsComeThroughStringParser() + { + Assert.Equal("expected a value, { or ( at position 9, found end of input", Fails(Graph, "({1,2},{").Message); + } +} diff --git a/tests/SPADE.Tests/TypeConstraintTests.cs b/tests/SPADE.Tests/TypeConstraintTests.cs index 40194f9..f8857e0 100644 --- a/tests/SPADE.Tests/TypeConstraintTests.cs +++ b/tests/SPADE.Tests/TypeConstraintTests.cs @@ -27,7 +27,7 @@ public void IsIntAcceptsIntegers(string instance) [InlineData("(1,2)")] public void IsIntRejectsNonIntegers(string instance) { - Assert.ThrowsAny(() => Parse("{N | N is int}", instance)); + Assert.Throws(() => Parse("{N | N is int}", instance)); } [Fact] @@ -35,7 +35,7 @@ public void IsIntChecksNestedFields() { string grammar = "{((N,E),K) | N is set, E subset N unorderedcross N, K is int}"; Parse(grammar, "(({1,2},{{1,2}}),2)"); - Assert.ThrowsAny(() => Parse(grammar, "(({1,2},{{1,2}}),two)")); + Assert.Throws(() => Parse(grammar, "(({1,2},{{1,2}}),two)")); } [Fact] @@ -43,7 +43,7 @@ public void IsIntChecksInsideSetComprehension() { string grammar = "{(N,E) | N is set, E subset {(e, w) | e is N unorderedcross N, w is int}}"; Parse(grammar, "({1,2,3},{({1,2},4),({2,3},5)})"); - Assert.ThrowsAny(() => Parse(grammar, "({1,2,3},{({1,2},heavy)})")); + Assert.Throws(() => Parse(grammar, "({1,2,3},{({1,2},heavy)})")); } [Theory] @@ -59,7 +59,7 @@ public void IsSetAcceptsSets(string instance) [InlineData("(1,2)")] public void IsSetRejectsValuesAndLists(string instance) { - Assert.ThrowsAny(() => Parse("{S | S is set}", instance)); + Assert.Throws(() => Parse("{S | S is set}", instance)); } [Theory] @@ -75,13 +75,13 @@ public void IsListAcceptsLists(string instance) [InlineData("{1,2}")] public void IsListRejectsValuesAndSets(string instance) { - Assert.ThrowsAny(() => Parse("{L | L is list}", instance)); + Assert.Throws(() => Parse("{L | L is list}", instance)); } [Fact] public void SubsetIntStillChecksEveryElement() { Parse("{S | S subset int}", "{1,2,3}"); - Assert.ThrowsAny(() => Parse("{S | S subset int}", "{1,2,x}")); + Assert.Throws(() => Parse("{S | S subset int}", "{1,2,x}")); } } diff --git a/tests/SPADE.Tests/UnorderedCrossTests.cs b/tests/SPADE.Tests/UnorderedCrossTests.cs index 45682d5..03fcfec 100644 --- a/tests/SPADE.Tests/UnorderedCrossTests.cs +++ b/tests/SPADE.Tests/UnorderedCrossTests.cs @@ -39,7 +39,7 @@ public void AcceptsGraphWithK() [InlineData("({a,b,c},{{a,b},(b,c)})")] public void RejectsListEdges(string instance) { - Assert.ThrowsAny(() => Parse(Graph, instance)); + Assert.Throws(() => Parse(Graph, instance)); } [Fact] @@ -54,7 +54,7 @@ public void CrossAcceptsListEdges() [InlineData("({a,b,c},{{x,y}})")] public void RejectsEdgesWithUnknownNodes(string instance) { - Assert.ThrowsAny(() => Parse(Graph, instance)); + Assert.Throws(() => Parse(Graph, instance)); } [Fact] @@ -62,7 +62,7 @@ public void RejectsPlainValueEdgeAsConstraintFailure() { // A plain value is not an edge. It should fail the constraint, not // crash while trying to enumerate the value. - Exception e = Assert.ThrowsAny(() => Parse(Graph, "({a,b},{a})")); + Exception e = Assert.Throws(() => Parse(Graph, "({a,b},{a})")); Assert.IsNotType(e); } } From 42d49758337bcf3a7c6dd84202b10b92ddc7d9b6 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:18:38 -0600 Subject: [PATCH 07/35] feat(collections): add multisets A set drops repeated elements, so {3,3,2,2} was read as {3,2}. Problems defined on multisets, such as Partition and Subset Sum, then solved a different instance from the one written. A definition string can now mark a collection with "is multiset": {N | N is multiset, N subset int} reads {3,3,2,2} as four numbers. It applies before the other constraints wherever it is written. Without it, braces still mean a set, so nothing changes for existing definitions or callers. To make this possible, a set read from an instance string keeps its elements as written, and AsMultiset() turns them into a multiset. Multisets count every occurrence in Count(), enumeration and ToString(), compare by count in any order, and have order-independent hash codes. Union and Intersect use the larger and smaller count. New CountOf() and IsSubsetOf() compare by count, so {7,7} is in {7,7,1} but not in {7,1}. A set and a multiset are never equal, and "is set" rejects a multiset. Adds the multiset keyword to coll.g4; the lexer is regenerated with ANTLR 4.13.2, the version that generated the existing files. Closes #1 --- Documentation/DefinitionString.md | 1 + Documentation/InstanceString.md | 5 + Documentation/utilcollection.md | 14 +- InstanceScanner.cs | 2 +- UtilCollection.cs | 127 ++++++++++- grammar/coll.g4 | 1 + grammar/collLexer.cs | 91 ++++---- grammar/collLexer.interp | 2 +- grammar/desciptors/ConstraintDescriptor.cs | 12 +- grammar/desciptors/SetDescriptor.cs | 2 + tests/SPADE.Tests/MultisetTests.cs | 231 +++++++++++++++++++++ 11 files changed, 435 insertions(+), 53 deletions(-) create mode 100644 tests/SPADE.Tests/MultisetTests.cs diff --git a/Documentation/DefinitionString.md b/Documentation/DefinitionString.md index 0babe8a..7517caa 100644 --- a/Documentation/DefinitionString.md +++ b/Documentation/DefinitionString.md @@ -15,6 +15,7 @@ After the symbol "|" is a list of assertions or *constraints* which must be true ### Definition - "is" checks thse type of a collection. It can be followed by set, list, or int, and constraints that collection to be that type. An example is "U is set" which constrains U to be a set +- "is multiset" marks a collection written with curly braces as a multiset, so repeated elements are kept instead of dropped. For example, "{N | N is multiset, N subset int}" reads {3,3,2,2} as four numbers. It applies before the other constraints, wherever it is written. See [multisets](InstanceString.md#multisets). - "subset" checks that the collection is a subset of the object on the right. An example is "A subset B" which constrains A to be a subset of B. ### Objects diff --git a/Documentation/InstanceString.md b/Documentation/InstanceString.md index 9fbb377..9003652 100644 --- a/Documentation/InstanceString.md +++ b/Documentation/InstanceString.md @@ -39,3 +39,8 @@ expected , or } at position 5, found end of input for `{1,2`. Its `Position` (counting from 1), `Expected` and `Found` properties hold the same details. Nothing is guessed: unbalanced braces, extra text after the instance, empty elements such as `{1,,2}`, and empty input are all errors. Sets and lists can be nested up to 256 levels deep. Deeper input is rejected with a `SpadeParseException` rather than risking a stack overflow. + +## Multisets +A multiset is written with curly braces, like a set: `{3,3,2,2}`. Read on its own, that is a set, and the repeated elements are dropped, leaving `{3,2}`. When the definition string marks the collection with `is multiset`, every element is kept, so `{3,3,2,2}` has four elements. + +Two multisets are equal when they hold the same elements the same number of times, in any order. A set and a multiset are never equal, even with the same elements. diff --git a/Documentation/utilcollection.md b/Documentation/utilcollection.md index f287b49..d5aed84 100644 --- a/Documentation/utilcollection.md +++ b/Documentation/utilcollection.md @@ -54,7 +54,10 @@ Returns a new UtilCollections object that is the union of the two collections. O Returns if the two UtilCollections objects are equal to each other. Comparison is done by value, so two different instances of the UtilCollections class that have the same structure and elements are compared, it will return true. ### Count -Returns the length of the collection. +Returns the length of the collection. For a multiset, repeated elements are counted every time they occur. + +### CountOf +Returns how many times an element occurs: 0 or 1 in a set, any number in a multiset or list. ### ToList Returns a list representing the elements of the collection. If the collection is ordered, it returns the same list. If the collection is unordered, it is cast to a list and then returned. @@ -68,6 +71,15 @@ Checks if the collection contains another UtilCollection object. Comparison is d ### Add Adds a UtilCollection object to the collection. +### AsMultiset +Returns the same elements as a multiset. For a set read from an instance string, this brings back the repeated elements the set dropped, so `{3,3,2,2}` gives four elements. Only sets and multisets can be read as multisets. + +### IsMultiset +Returns true if the collection is a multiset. + +### IsSubsetOf +Returns true if every element occurs in the other collection at least as many times as it does in this one. For multisets this compares counts, so `{7,7}` is in `{7,7,1}` but not in `{7,1}`. Both collections must be sets or multisets. + ### parseInt Attempts to parse and return itself as an integer. diff --git a/InstanceScanner.cs b/InstanceScanner.cs index 12cdce3..beb8082 100644 --- a/InstanceScanner.cs +++ b/InstanceScanner.cs @@ -93,7 +93,7 @@ private UtilCollection ParseCollection(int depth) } if (open == '(') return new UtilCollection(items); - return new UtilCollection(items.ToHashSet()); + return UtilCollection.FromSetItems(items); } private UtilCollection ParseValue() diff --git a/UtilCollection.cs b/UtilCollection.cs index 6a70fe7..927fe77 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -15,6 +15,15 @@ public class UtilCollection private bool isOrdered; private bool isValue; + // A multiset keeps every occurrence in `list`, in input order, and how often each element + // occurs in `counts` (#1). It is unordered, like a set. + private bool isMultiset; + private Dictionary? counts; + + // For a set read from an instance string: every element as written, duplicates included, + // so the set can still be read as a multiset if its definition says so. + private List? occurrences; + public UtilCollection() { @@ -34,6 +43,9 @@ public UtilCollection(string instance) value = parsed.value; isOrdered = parsed.isOrdered; isValue = parsed.isValue; + isMultiset = parsed.isMultiset; + counts = parsed.counts; + occurrences = parsed.occurrences; } internal static UtilCollection FromValue(string value) @@ -41,6 +53,11 @@ internal static UtilCollection FromValue(string value) return new UtilCollection { value = value, isValue = true }; } + internal static UtilCollection FromSetItems(List items) + { + return new UtilCollection(items.ToHashSet()) { occurrences = items }; + } + public UtilCollection(HashSet set_) { isOrdered = false; @@ -87,13 +104,19 @@ public UtilCollection this[int index] public void Add(UtilCollection item) { - if(isOrdered) + if (isMultiset) + { + list.Add(item); + counts![item] = CountOf(item) + 1; + } + else if(isOrdered) { list.Add(item); } else { set.Add(item); + occurrences?.Add(item); } } @@ -101,7 +124,7 @@ public IEnumerator GetEnumerator() { if (isValue) throw new InvalidOperationException("Cannot enumerate over a value"); - if (isOrdered) + if (isOrdered || isMultiset) return list.GetEnumerator(); else return set.GetEnumerator(); @@ -150,8 +173,56 @@ public bool IsValue() return isValue; } + public bool IsMultiset() + { + return isMultiset; + } + + /// + /// The same elements as a multiset. For a set read from an instance string, this brings back + /// the duplicates the set dropped, so {3,3,2,2} gives four elements. Only for sets and multisets. + /// + public UtilCollection AsMultiset() + { + if (isValue || isOrdered) throw new InvalidOperationException("Only a set can be read as a multiset"); + if (isMultiset) return this; + + UtilCollection multiset = new UtilCollection { isMultiset = true, counts = new() }; + foreach (UtilCollection item in occurrences ?? set.ToList()) + { + multiset.Add(item); + } + return multiset; + } + + /// How many times item occurs: 0 or 1 in a set, any number in a multiset or list. + public int CountOf(UtilCollection item) + { + if (isValue) throw new InvalidOperationException("Cannot count in a value"); + if (isMultiset) return counts!.TryGetValue(item, out int n) ? n : 0; + if (isOrdered) return list.Count(x => x.Equals(item)); + return set.Contains(item) ? 1 : 0; + } + + /// + /// True if every element occurs in other at least as often as it does here, so the multiset + /// {7,7} is in {7,7,1} but not in {7,1}. Only for sets and multisets. + /// + public bool IsSubsetOf(UtilCollection other) + { + if (isValue || isOrdered || other.isValue || other.isOrdered) + throw new InvalidOperationException("IsSubsetOf needs two sets or multisets"); + + IEnumerable distinct = isMultiset ? counts!.Keys : set; + return distinct.All(item => CountOf(item) <= other.CountOf(item)); + } + public bool Contains(UtilCollection collection) { + if (isMultiset) + { + return counts!.ContainsKey(collection); + } if (isOrdered) { return list.Contains(collection); @@ -163,7 +234,7 @@ public bool Contains(UtilCollection collection) } public int Count() { - if (isOrdered) + if (isOrdered || isMultiset) { return list.Count; } @@ -181,6 +252,17 @@ public int parseInt() public UtilCollection Intersect(UtilCollection other) { if (isOrdered != other.isOrdered) throw new InvalidOperationException("Can't take the intersection of a set and a list"); + if (isMultiset != other.isMultiset) throw new InvalidOperationException("Can't take the intersection of a set and a multiset"); + if (isMultiset) + { + // Each element occurs as often as in whichever side has fewer. + UtilCollection result = new UtilCollection { isMultiset = true, counts = new() }; + foreach (UtilCollection item in list) + { + if (result.CountOf(item) < other.CountOf(item)) result.Add(item); + } + return result; + } if (!isOrdered) { return new UtilCollection(set.Intersect(other.set).ToHashSet()); @@ -194,6 +276,18 @@ public UtilCollection Intersect(UtilCollection other) public UtilCollection Union(UtilCollection other) { if (isOrdered != other.isOrdered) throw new InvalidOperationException("Can't take the union of a set and a list"); + if (isMultiset != other.isMultiset) throw new InvalidOperationException("Can't take the union of a set and a multiset"); + if (isMultiset) + { + // Each element occurs as often as in whichever side has more. + UtilCollection result = new UtilCollection { isMultiset = true, counts = new() }; + foreach (UtilCollection item in list) result.Add(item); + foreach (UtilCollection item in other.list) + { + if (result.CountOf(item) < other.CountOf(item)) result.Add(item); + } + return result; + } if (!isOrdered) { return new UtilCollection(set.Union(other.set).ToHashSet()); @@ -224,14 +318,21 @@ public override bool Equals(object? obj) UtilCollection other = (UtilCollection)obj; - // A value, a list and a set are never equal to each other, even when empty. - if (isValue != other.isValue || isOrdered != other.isOrdered) return false; + // A value, a list, a set and a multiset are never equal to each other, even when empty. + if (isValue != other.isValue || isOrdered != other.isOrdered || isMultiset != other.isMultiset) return false; if (isValue) { return value == other.value; } + if (isMultiset) + { + return list.Count == other.list.Count + && counts!.Count == other.counts!.Count + && counts.All(entry => other.CountOf(entry.Key) == entry.Value); + } + if (isOrdered) { return list.SequenceEqual(other.list); @@ -266,6 +367,17 @@ public override int GetHashCode() return hash.ToHashCode(); } + if (isMultiset) + { + // Order-independent, like a set, but every occurrence counts. + int total = 0; + foreach (UtilCollection item in list) + { + total = unchecked(total + item.GetHashCode()); + } + return HashCode.Combine(isOrdered, isMultiset, list.Count, total); + } + // Sets are equal regardless of order, so combine element hashes with // an order-independent sum rather than hashing ToString(), which // follows insertion order. @@ -279,6 +391,11 @@ public override int GetHashCode() public List ToList() { + if (isMultiset) + { + // A copy, so changes to it can't get out of step with the counts. + return new List(list); + } if (isOrdered) { return list; diff --git a/grammar/coll.g4 b/grammar/coll.g4 index 99a2afd..2fab116 100644 --- a/grammar/coll.g4 +++ b/grammar/coll.g4 @@ -41,6 +41,7 @@ BIN_OP : 'union' | 'unorderedcross' ; BUILT_IN : 'set' + | 'multiset' | 'list' | 'graph' | 'weightedGraph' diff --git a/grammar/collLexer.cs b/grammar/collLexer.cs index e2a7c82..ea7df59 100644 --- a/grammar/collLexer.cs +++ b/grammar/collLexer.cs @@ -94,7 +94,7 @@ static collLexer() { } } private static int[] _serializedATN = { - 4,0,14,159,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7, + 4,0,14,167,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7, 6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,1,0, 1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1, 5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,3,5,59,8,5,1,6,1,6,1,6,1,6,1, @@ -102,49 +102,52 @@ static collLexer() { 8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8, 1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,3,8,113,8,8,1,9,1, 9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9, - 1,9,1,9,1,9,1,9,1,9,1,9,3,9,140,8,9,1,10,1,10,1,10,1,10,1,11,4,11,147, - 8,11,11,11,12,11,148,1,12,4,12,152,8,12,11,12,12,12,153,1,12,1,12,1,13, - 1,13,0,0,14,1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12, - 25,13,27,14,1,0,3,4,0,46,46,48,57,65,90,97,122,3,0,9,10,13,13,32,32,1, - 0,44,44,173,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0, - 0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21, - 1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,1,29,1,0,0,0,3,31,1,0,0, - 0,5,33,1,0,0,0,7,35,1,0,0,0,9,37,1,0,0,0,11,58,1,0,0,0,13,60,1,0,0,0,15, - 74,1,0,0,0,17,112,1,0,0,0,19,139,1,0,0,0,21,141,1,0,0,0,23,146,1,0,0,0, - 25,151,1,0,0,0,27,157,1,0,0,0,29,30,5,123,0,0,30,2,1,0,0,0,31,32,5,125, - 0,0,32,4,1,0,0,0,33,34,5,40,0,0,34,6,1,0,0,0,35,36,5,41,0,0,36,8,1,0,0, - 0,37,38,5,124,0,0,38,10,1,0,0,0,39,40,5,97,0,0,40,41,5,110,0,0,41,59,5, - 100,0,0,42,43,5,111,0,0,43,59,5,114,0,0,44,45,5,110,0,0,45,46,5,97,0,0, - 46,47,5,110,0,0,47,59,5,100,0,0,48,49,5,120,0,0,49,50,5,111,0,0,50,59, - 5,114,0,0,51,52,5,110,0,0,52,53,5,111,0,0,53,59,5,114,0,0,54,55,5,120, - 0,0,55,56,5,110,0,0,56,57,5,111,0,0,57,59,5,114,0,0,58,39,1,0,0,0,58,42, - 1,0,0,0,58,44,1,0,0,0,58,48,1,0,0,0,58,51,1,0,0,0,58,54,1,0,0,0,59,12, - 1,0,0,0,60,61,5,110,0,0,61,62,5,111,0,0,62,63,5,116,0,0,63,14,1,0,0,0, - 64,65,5,105,0,0,65,75,5,115,0,0,66,67,5,105,0,0,67,75,5,110,0,0,68,69, - 5,115,0,0,69,70,5,117,0,0,70,71,5,98,0,0,71,72,5,115,0,0,72,73,5,101,0, - 0,73,75,5,116,0,0,74,64,1,0,0,0,74,66,1,0,0,0,74,68,1,0,0,0,75,16,1,0, - 0,0,76,77,5,117,0,0,77,78,5,110,0,0,78,79,5,105,0,0,79,80,5,111,0,0,80, - 113,5,110,0,0,81,82,5,105,0,0,82,83,5,110,0,0,83,84,5,116,0,0,84,85,5, - 101,0,0,85,86,5,114,0,0,86,87,5,115,0,0,87,88,5,101,0,0,88,89,5,99,0,0, - 89,90,5,116,0,0,90,91,5,105,0,0,91,92,5,111,0,0,92,113,5,110,0,0,93,94, - 5,99,0,0,94,95,5,114,0,0,95,96,5,111,0,0,96,97,5,115,0,0,97,113,5,115, - 0,0,98,99,5,117,0,0,99,100,5,110,0,0,100,101,5,111,0,0,101,102,5,114,0, - 0,102,103,5,100,0,0,103,104,5,101,0,0,104,105,5,114,0,0,105,106,5,101, - 0,0,106,107,5,100,0,0,107,108,5,99,0,0,108,109,5,114,0,0,109,110,5,111, - 0,0,110,111,5,115,0,0,111,113,5,115,0,0,112,76,1,0,0,0,112,81,1,0,0,0, - 112,93,1,0,0,0,112,98,1,0,0,0,113,18,1,0,0,0,114,115,5,115,0,0,115,116, - 5,101,0,0,116,140,5,116,0,0,117,118,5,108,0,0,118,119,5,105,0,0,119,120, - 5,115,0,0,120,140,5,116,0,0,121,122,5,103,0,0,122,123,5,114,0,0,123,124, - 5,97,0,0,124,125,5,112,0,0,125,140,5,104,0,0,126,127,5,119,0,0,127,128, - 5,101,0,0,128,129,5,105,0,0,129,130,5,103,0,0,130,131,5,104,0,0,131,132, - 5,116,0,0,132,133,5,101,0,0,133,134,5,100,0,0,134,135,5,71,0,0,135,136, - 5,114,0,0,136,137,5,97,0,0,137,138,5,112,0,0,138,140,5,104,0,0,139,114, - 1,0,0,0,139,117,1,0,0,0,139,121,1,0,0,0,139,126,1,0,0,0,140,20,1,0,0,0, - 141,142,5,105,0,0,142,143,5,110,0,0,143,144,5,116,0,0,144,22,1,0,0,0,145, - 147,7,0,0,0,146,145,1,0,0,0,147,148,1,0,0,0,148,146,1,0,0,0,148,149,1, - 0,0,0,149,24,1,0,0,0,150,152,7,1,0,0,151,150,1,0,0,0,152,153,1,0,0,0,153, - 151,1,0,0,0,153,154,1,0,0,0,154,155,1,0,0,0,155,156,6,12,0,0,156,26,1, - 0,0,0,157,158,7,2,0,0,158,28,1,0,0,0,7,0,58,74,112,139,148,153,1,6,0,0 + 1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,3,9,148,8,9,1, + 10,1,10,1,10,1,10,1,11,4,11,155,8,11,11,11,12,11,156,1,12,4,12,160,8,12, + 11,12,12,12,161,1,12,1,12,1,13,1,13,0,0,14,1,1,3,2,5,3,7,4,9,5,11,6,13, + 7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,1,0,3,4,0,46,46,48,57,65,90, + 97,122,3,0,9,10,13,13,32,32,1,0,44,44,182,0,1,1,0,0,0,0,3,1,0,0,0,0,5, + 1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0, + 0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27, + 1,0,0,0,1,29,1,0,0,0,3,31,1,0,0,0,5,33,1,0,0,0,7,35,1,0,0,0,9,37,1,0,0, + 0,11,58,1,0,0,0,13,60,1,0,0,0,15,74,1,0,0,0,17,112,1,0,0,0,19,147,1,0, + 0,0,21,149,1,0,0,0,23,154,1,0,0,0,25,159,1,0,0,0,27,165,1,0,0,0,29,30, + 5,123,0,0,30,2,1,0,0,0,31,32,5,125,0,0,32,4,1,0,0,0,33,34,5,40,0,0,34, + 6,1,0,0,0,35,36,5,41,0,0,36,8,1,0,0,0,37,38,5,124,0,0,38,10,1,0,0,0,39, + 40,5,97,0,0,40,41,5,110,0,0,41,59,5,100,0,0,42,43,5,111,0,0,43,59,5,114, + 0,0,44,45,5,110,0,0,45,46,5,97,0,0,46,47,5,110,0,0,47,59,5,100,0,0,48, + 49,5,120,0,0,49,50,5,111,0,0,50,59,5,114,0,0,51,52,5,110,0,0,52,53,5,111, + 0,0,53,59,5,114,0,0,54,55,5,120,0,0,55,56,5,110,0,0,56,57,5,111,0,0,57, + 59,5,114,0,0,58,39,1,0,0,0,58,42,1,0,0,0,58,44,1,0,0,0,58,48,1,0,0,0,58, + 51,1,0,0,0,58,54,1,0,0,0,59,12,1,0,0,0,60,61,5,110,0,0,61,62,5,111,0,0, + 62,63,5,116,0,0,63,14,1,0,0,0,64,65,5,105,0,0,65,75,5,115,0,0,66,67,5, + 105,0,0,67,75,5,110,0,0,68,69,5,115,0,0,69,70,5,117,0,0,70,71,5,98,0,0, + 71,72,5,115,0,0,72,73,5,101,0,0,73,75,5,116,0,0,74,64,1,0,0,0,74,66,1, + 0,0,0,74,68,1,0,0,0,75,16,1,0,0,0,76,77,5,117,0,0,77,78,5,110,0,0,78,79, + 5,105,0,0,79,80,5,111,0,0,80,113,5,110,0,0,81,82,5,105,0,0,82,83,5,110, + 0,0,83,84,5,116,0,0,84,85,5,101,0,0,85,86,5,114,0,0,86,87,5,115,0,0,87, + 88,5,101,0,0,88,89,5,99,0,0,89,90,5,116,0,0,90,91,5,105,0,0,91,92,5,111, + 0,0,92,113,5,110,0,0,93,94,5,99,0,0,94,95,5,114,0,0,95,96,5,111,0,0,96, + 97,5,115,0,0,97,113,5,115,0,0,98,99,5,117,0,0,99,100,5,110,0,0,100,101, + 5,111,0,0,101,102,5,114,0,0,102,103,5,100,0,0,103,104,5,101,0,0,104,105, + 5,114,0,0,105,106,5,101,0,0,106,107,5,100,0,0,107,108,5,99,0,0,108,109, + 5,114,0,0,109,110,5,111,0,0,110,111,5,115,0,0,111,113,5,115,0,0,112,76, + 1,0,0,0,112,81,1,0,0,0,112,93,1,0,0,0,112,98,1,0,0,0,113,18,1,0,0,0,114, + 115,5,115,0,0,115,116,5,101,0,0,116,148,5,116,0,0,117,118,5,109,0,0,118, + 119,5,117,0,0,119,120,5,108,0,0,120,121,5,116,0,0,121,122,5,105,0,0,122, + 123,5,115,0,0,123,124,5,101,0,0,124,148,5,116,0,0,125,126,5,108,0,0,126, + 127,5,105,0,0,127,128,5,115,0,0,128,148,5,116,0,0,129,130,5,103,0,0,130, + 131,5,114,0,0,131,132,5,97,0,0,132,133,5,112,0,0,133,148,5,104,0,0,134, + 135,5,119,0,0,135,136,5,101,0,0,136,137,5,105,0,0,137,138,5,103,0,0,138, + 139,5,104,0,0,139,140,5,116,0,0,140,141,5,101,0,0,141,142,5,100,0,0,142, + 143,5,71,0,0,143,144,5,114,0,0,144,145,5,97,0,0,145,146,5,112,0,0,146, + 148,5,104,0,0,147,114,1,0,0,0,147,117,1,0,0,0,147,125,1,0,0,0,147,129, + 1,0,0,0,147,134,1,0,0,0,148,20,1,0,0,0,149,150,5,105,0,0,150,151,5,110, + 0,0,151,152,5,116,0,0,152,22,1,0,0,0,153,155,7,0,0,0,154,153,1,0,0,0,155, + 156,1,0,0,0,156,154,1,0,0,0,156,157,1,0,0,0,157,24,1,0,0,0,158,160,7,1, + 0,0,159,158,1,0,0,0,160,161,1,0,0,0,161,159,1,0,0,0,161,162,1,0,0,0,162, + 163,1,0,0,0,163,164,6,12,0,0,164,26,1,0,0,0,165,166,7,2,0,0,166,28,1,0, + 0,0,7,0,58,74,112,147,156,161,1,6,0,0 }; public static readonly ATN _ATN = diff --git a/grammar/collLexer.interp b/grammar/collLexer.interp index be2869f..aec2949 100644 --- a/grammar/collLexer.interp +++ b/grammar/collLexer.interp @@ -56,4 +56,4 @@ mode names: DEFAULT_MODE atn: -[4, 0, 14, 159, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 59, 8, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 75, 8, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 113, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 140, 8, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 4, 11, 147, 8, 11, 11, 11, 12, 11, 148, 1, 12, 4, 12, 152, 8, 12, 11, 12, 12, 12, 153, 1, 12, 1, 12, 1, 13, 1, 13, 0, 0, 14, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 1, 0, 3, 4, 0, 46, 46, 48, 57, 65, 90, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 44, 44, 173, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 1, 29, 1, 0, 0, 0, 3, 31, 1, 0, 0, 0, 5, 33, 1, 0, 0, 0, 7, 35, 1, 0, 0, 0, 9, 37, 1, 0, 0, 0, 11, 58, 1, 0, 0, 0, 13, 60, 1, 0, 0, 0, 15, 74, 1, 0, 0, 0, 17, 112, 1, 0, 0, 0, 19, 139, 1, 0, 0, 0, 21, 141, 1, 0, 0, 0, 23, 146, 1, 0, 0, 0, 25, 151, 1, 0, 0, 0, 27, 157, 1, 0, 0, 0, 29, 30, 5, 123, 0, 0, 30, 2, 1, 0, 0, 0, 31, 32, 5, 125, 0, 0, 32, 4, 1, 0, 0, 0, 33, 34, 5, 40, 0, 0, 34, 6, 1, 0, 0, 0, 35, 36, 5, 41, 0, 0, 36, 8, 1, 0, 0, 0, 37, 38, 5, 124, 0, 0, 38, 10, 1, 0, 0, 0, 39, 40, 5, 97, 0, 0, 40, 41, 5, 110, 0, 0, 41, 59, 5, 100, 0, 0, 42, 43, 5, 111, 0, 0, 43, 59, 5, 114, 0, 0, 44, 45, 5, 110, 0, 0, 45, 46, 5, 97, 0, 0, 46, 47, 5, 110, 0, 0, 47, 59, 5, 100, 0, 0, 48, 49, 5, 120, 0, 0, 49, 50, 5, 111, 0, 0, 50, 59, 5, 114, 0, 0, 51, 52, 5, 110, 0, 0, 52, 53, 5, 111, 0, 0, 53, 59, 5, 114, 0, 0, 54, 55, 5, 120, 0, 0, 55, 56, 5, 110, 0, 0, 56, 57, 5, 111, 0, 0, 57, 59, 5, 114, 0, 0, 58, 39, 1, 0, 0, 0, 58, 42, 1, 0, 0, 0, 58, 44, 1, 0, 0, 0, 58, 48, 1, 0, 0, 0, 58, 51, 1, 0, 0, 0, 58, 54, 1, 0, 0, 0, 59, 12, 1, 0, 0, 0, 60, 61, 5, 110, 0, 0, 61, 62, 5, 111, 0, 0, 62, 63, 5, 116, 0, 0, 63, 14, 1, 0, 0, 0, 64, 65, 5, 105, 0, 0, 65, 75, 5, 115, 0, 0, 66, 67, 5, 105, 0, 0, 67, 75, 5, 110, 0, 0, 68, 69, 5, 115, 0, 0, 69, 70, 5, 117, 0, 0, 70, 71, 5, 98, 0, 0, 71, 72, 5, 115, 0, 0, 72, 73, 5, 101, 0, 0, 73, 75, 5, 116, 0, 0, 74, 64, 1, 0, 0, 0, 74, 66, 1, 0, 0, 0, 74, 68, 1, 0, 0, 0, 75, 16, 1, 0, 0, 0, 76, 77, 5, 117, 0, 0, 77, 78, 5, 110, 0, 0, 78, 79, 5, 105, 0, 0, 79, 80, 5, 111, 0, 0, 80, 113, 5, 110, 0, 0, 81, 82, 5, 105, 0, 0, 82, 83, 5, 110, 0, 0, 83, 84, 5, 116, 0, 0, 84, 85, 5, 101, 0, 0, 85, 86, 5, 114, 0, 0, 86, 87, 5, 115, 0, 0, 87, 88, 5, 101, 0, 0, 88, 89, 5, 99, 0, 0, 89, 90, 5, 116, 0, 0, 90, 91, 5, 105, 0, 0, 91, 92, 5, 111, 0, 0, 92, 113, 5, 110, 0, 0, 93, 94, 5, 99, 0, 0, 94, 95, 5, 114, 0, 0, 95, 96, 5, 111, 0, 0, 96, 97, 5, 115, 0, 0, 97, 113, 5, 115, 0, 0, 98, 99, 5, 117, 0, 0, 99, 100, 5, 110, 0, 0, 100, 101, 5, 111, 0, 0, 101, 102, 5, 114, 0, 0, 102, 103, 5, 100, 0, 0, 103, 104, 5, 101, 0, 0, 104, 105, 5, 114, 0, 0, 105, 106, 5, 101, 0, 0, 106, 107, 5, 100, 0, 0, 107, 108, 5, 99, 0, 0, 108, 109, 5, 114, 0, 0, 109, 110, 5, 111, 0, 0, 110, 111, 5, 115, 0, 0, 111, 113, 5, 115, 0, 0, 112, 76, 1, 0, 0, 0, 112, 81, 1, 0, 0, 0, 112, 93, 1, 0, 0, 0, 112, 98, 1, 0, 0, 0, 113, 18, 1, 0, 0, 0, 114, 115, 5, 115, 0, 0, 115, 116, 5, 101, 0, 0, 116, 140, 5, 116, 0, 0, 117, 118, 5, 108, 0, 0, 118, 119, 5, 105, 0, 0, 119, 120, 5, 115, 0, 0, 120, 140, 5, 116, 0, 0, 121, 122, 5, 103, 0, 0, 122, 123, 5, 114, 0, 0, 123, 124, 5, 97, 0, 0, 124, 125, 5, 112, 0, 0, 125, 140, 5, 104, 0, 0, 126, 127, 5, 119, 0, 0, 127, 128, 5, 101, 0, 0, 128, 129, 5, 105, 0, 0, 129, 130, 5, 103, 0, 0, 130, 131, 5, 104, 0, 0, 131, 132, 5, 116, 0, 0, 132, 133, 5, 101, 0, 0, 133, 134, 5, 100, 0, 0, 134, 135, 5, 71, 0, 0, 135, 136, 5, 114, 0, 0, 136, 137, 5, 97, 0, 0, 137, 138, 5, 112, 0, 0, 138, 140, 5, 104, 0, 0, 139, 114, 1, 0, 0, 0, 139, 117, 1, 0, 0, 0, 139, 121, 1, 0, 0, 0, 139, 126, 1, 0, 0, 0, 140, 20, 1, 0, 0, 0, 141, 142, 5, 105, 0, 0, 142, 143, 5, 110, 0, 0, 143, 144, 5, 116, 0, 0, 144, 22, 1, 0, 0, 0, 145, 147, 7, 0, 0, 0, 146, 145, 1, 0, 0, 0, 147, 148, 1, 0, 0, 0, 148, 146, 1, 0, 0, 0, 148, 149, 1, 0, 0, 0, 149, 24, 1, 0, 0, 0, 150, 152, 7, 1, 0, 0, 151, 150, 1, 0, 0, 0, 152, 153, 1, 0, 0, 0, 153, 151, 1, 0, 0, 0, 153, 154, 1, 0, 0, 0, 154, 155, 1, 0, 0, 0, 155, 156, 6, 12, 0, 0, 156, 26, 1, 0, 0, 0, 157, 158, 7, 2, 0, 0, 158, 28, 1, 0, 0, 0, 7, 0, 58, 74, 112, 139, 148, 153, 1, 6, 0, 0] \ No newline at end of file +[4, 0, 14, 167, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 59, 8, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 75, 8, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 113, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 148, 8, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 4, 11, 155, 8, 11, 11, 11, 12, 11, 156, 1, 12, 4, 12, 160, 8, 12, 11, 12, 12, 12, 161, 1, 12, 1, 12, 1, 13, 1, 13, 0, 0, 14, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 1, 0, 3, 4, 0, 46, 46, 48, 57, 65, 90, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 44, 44, 182, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 1, 29, 1, 0, 0, 0, 3, 31, 1, 0, 0, 0, 5, 33, 1, 0, 0, 0, 7, 35, 1, 0, 0, 0, 9, 37, 1, 0, 0, 0, 11, 58, 1, 0, 0, 0, 13, 60, 1, 0, 0, 0, 15, 74, 1, 0, 0, 0, 17, 112, 1, 0, 0, 0, 19, 147, 1, 0, 0, 0, 21, 149, 1, 0, 0, 0, 23, 154, 1, 0, 0, 0, 25, 159, 1, 0, 0, 0, 27, 165, 1, 0, 0, 0, 29, 30, 5, 123, 0, 0, 30, 2, 1, 0, 0, 0, 31, 32, 5, 125, 0, 0, 32, 4, 1, 0, 0, 0, 33, 34, 5, 40, 0, 0, 34, 6, 1, 0, 0, 0, 35, 36, 5, 41, 0, 0, 36, 8, 1, 0, 0, 0, 37, 38, 5, 124, 0, 0, 38, 10, 1, 0, 0, 0, 39, 40, 5, 97, 0, 0, 40, 41, 5, 110, 0, 0, 41, 59, 5, 100, 0, 0, 42, 43, 5, 111, 0, 0, 43, 59, 5, 114, 0, 0, 44, 45, 5, 110, 0, 0, 45, 46, 5, 97, 0, 0, 46, 47, 5, 110, 0, 0, 47, 59, 5, 100, 0, 0, 48, 49, 5, 120, 0, 0, 49, 50, 5, 111, 0, 0, 50, 59, 5, 114, 0, 0, 51, 52, 5, 110, 0, 0, 52, 53, 5, 111, 0, 0, 53, 59, 5, 114, 0, 0, 54, 55, 5, 120, 0, 0, 55, 56, 5, 110, 0, 0, 56, 57, 5, 111, 0, 0, 57, 59, 5, 114, 0, 0, 58, 39, 1, 0, 0, 0, 58, 42, 1, 0, 0, 0, 58, 44, 1, 0, 0, 0, 58, 48, 1, 0, 0, 0, 58, 51, 1, 0, 0, 0, 58, 54, 1, 0, 0, 0, 59, 12, 1, 0, 0, 0, 60, 61, 5, 110, 0, 0, 61, 62, 5, 111, 0, 0, 62, 63, 5, 116, 0, 0, 63, 14, 1, 0, 0, 0, 64, 65, 5, 105, 0, 0, 65, 75, 5, 115, 0, 0, 66, 67, 5, 105, 0, 0, 67, 75, 5, 110, 0, 0, 68, 69, 5, 115, 0, 0, 69, 70, 5, 117, 0, 0, 70, 71, 5, 98, 0, 0, 71, 72, 5, 115, 0, 0, 72, 73, 5, 101, 0, 0, 73, 75, 5, 116, 0, 0, 74, 64, 1, 0, 0, 0, 74, 66, 1, 0, 0, 0, 74, 68, 1, 0, 0, 0, 75, 16, 1, 0, 0, 0, 76, 77, 5, 117, 0, 0, 77, 78, 5, 110, 0, 0, 78, 79, 5, 105, 0, 0, 79, 80, 5, 111, 0, 0, 80, 113, 5, 110, 0, 0, 81, 82, 5, 105, 0, 0, 82, 83, 5, 110, 0, 0, 83, 84, 5, 116, 0, 0, 84, 85, 5, 101, 0, 0, 85, 86, 5, 114, 0, 0, 86, 87, 5, 115, 0, 0, 87, 88, 5, 101, 0, 0, 88, 89, 5, 99, 0, 0, 89, 90, 5, 116, 0, 0, 90, 91, 5, 105, 0, 0, 91, 92, 5, 111, 0, 0, 92, 113, 5, 110, 0, 0, 93, 94, 5, 99, 0, 0, 94, 95, 5, 114, 0, 0, 95, 96, 5, 111, 0, 0, 96, 97, 5, 115, 0, 0, 97, 113, 5, 115, 0, 0, 98, 99, 5, 117, 0, 0, 99, 100, 5, 110, 0, 0, 100, 101, 5, 111, 0, 0, 101, 102, 5, 114, 0, 0, 102, 103, 5, 100, 0, 0, 103, 104, 5, 101, 0, 0, 104, 105, 5, 114, 0, 0, 105, 106, 5, 101, 0, 0, 106, 107, 5, 100, 0, 0, 107, 108, 5, 99, 0, 0, 108, 109, 5, 114, 0, 0, 109, 110, 5, 111, 0, 0, 110, 111, 5, 115, 0, 0, 111, 113, 5, 115, 0, 0, 112, 76, 1, 0, 0, 0, 112, 81, 1, 0, 0, 0, 112, 93, 1, 0, 0, 0, 112, 98, 1, 0, 0, 0, 113, 18, 1, 0, 0, 0, 114, 115, 5, 115, 0, 0, 115, 116, 5, 101, 0, 0, 116, 148, 5, 116, 0, 0, 117, 118, 5, 109, 0, 0, 118, 119, 5, 117, 0, 0, 119, 120, 5, 108, 0, 0, 120, 121, 5, 116, 0, 0, 121, 122, 5, 105, 0, 0, 122, 123, 5, 115, 0, 0, 123, 124, 5, 101, 0, 0, 124, 148, 5, 116, 0, 0, 125, 126, 5, 108, 0, 0, 126, 127, 5, 105, 0, 0, 127, 128, 5, 115, 0, 0, 128, 148, 5, 116, 0, 0, 129, 130, 5, 103, 0, 0, 130, 131, 5, 114, 0, 0, 131, 132, 5, 97, 0, 0, 132, 133, 5, 112, 0, 0, 133, 148, 5, 104, 0, 0, 134, 135, 5, 119, 0, 0, 135, 136, 5, 101, 0, 0, 136, 137, 5, 105, 0, 0, 137, 138, 5, 103, 0, 0, 138, 139, 5, 104, 0, 0, 139, 140, 5, 116, 0, 0, 140, 141, 5, 101, 0, 0, 141, 142, 5, 100, 0, 0, 142, 143, 5, 71, 0, 0, 143, 144, 5, 114, 0, 0, 144, 145, 5, 97, 0, 0, 145, 146, 5, 112, 0, 0, 146, 148, 5, 104, 0, 0, 147, 114, 1, 0, 0, 0, 147, 117, 1, 0, 0, 0, 147, 125, 1, 0, 0, 0, 147, 129, 1, 0, 0, 0, 147, 134, 1, 0, 0, 0, 148, 20, 1, 0, 0, 0, 149, 150, 5, 105, 0, 0, 150, 151, 5, 110, 0, 0, 151, 152, 5, 116, 0, 0, 152, 22, 1, 0, 0, 0, 153, 155, 7, 0, 0, 0, 154, 153, 1, 0, 0, 0, 155, 156, 1, 0, 0, 0, 156, 154, 1, 0, 0, 0, 156, 157, 1, 0, 0, 0, 157, 24, 1, 0, 0, 0, 158, 160, 7, 1, 0, 0, 159, 158, 1, 0, 0, 0, 160, 161, 1, 0, 0, 0, 161, 159, 1, 0, 0, 0, 161, 162, 1, 0, 0, 0, 162, 163, 1, 0, 0, 0, 163, 164, 6, 12, 0, 0, 164, 26, 1, 0, 0, 0, 165, 166, 7, 2, 0, 0, 166, 28, 1, 0, 0, 0, 7, 0, 58, 74, 112, 147, 156, 161, 1, 6, 0, 0] \ No newline at end of file diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 1cbd7c8..1459e98 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -35,6 +35,9 @@ public static string Describe(UtilCollection collection) return s.Length <= maxLength ? s : s.Substring(0, maxLength) + "..."; } + // "N is multiset" changes how N is read, so it runs before the other constraints (#1). + public bool DeclaresMultiset => definition == "is" && built_in == "multiset"; + private SpadeParseException Failure(string reason) { return new SpadeParseException($"Constraint {text} failed: {reason}"); @@ -50,7 +53,7 @@ public void check(Dictionary map) switch (built_in) { case "set": - if (map[term].IsValue() || map[term].IsOrdered()) + if (map[term].IsValue() || map[term].IsOrdered() || map[term].IsMultiset()) { throw Failure($"{Describe(map[term])} is not a set"); } @@ -61,6 +64,13 @@ public void check(Dictionary map) throw Failure($"{Describe(map[term])} is not a list"); } break; + case "multiset": + if (map[term].IsValue() || map[term].IsOrdered()) + { + throw Failure($"{Describe(map[term])} is not a multiset"); + } + map[term] = map[term].AsMultiset(); + break; case "int": if (!map[term].IsValue() || !int.TryParse(map[term].ToString(), out _)) { diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index 282cb65..9d755e4 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -53,6 +53,8 @@ public SetDescriptor(Dictionary _map, BuiltInSet builtIn public SetDescriptor(Dictionary _map, FormatDescriptor formatDescriptor, List constraintDescriptors) { map = _map; + // OrderBy is stable, so the other constraints keep their order. + constraintDescriptors = constraintDescriptors.OrderBy(c => c.DeclaresMultiset ? 0 : 1).ToList(); isMember = (collection) => { formatDescriptor.parseFunc(map, collection); diff --git a/tests/SPADE.Tests/MultisetTests.cs b/tests/SPADE.Tests/MultisetTests.cs new file mode 100644 index 0000000..fa31c5d --- /dev/null +++ b/tests/SPADE.Tests/MultisetTests.cs @@ -0,0 +1,231 @@ +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// Multisets keep duplicates when the definition string asks for them (#1). +public class MultisetTests +{ + private static UtilCollection M(string s) => new UtilCollection(s).AsMultiset(); + + // --- Reading and counting --- + + [Fact] + public void SetsStillDropDuplicatesByDefault() + { + UtilCollection set = new UtilCollection("{3,3,2,2}"); + Assert.False(set.IsMultiset()); + Assert.Equal(2, set.Count()); + } + + [Fact] + public void AsMultisetBringsBackDuplicates() + { + UtilCollection multiset = M("{3,3,2,2}"); + Assert.True(multiset.IsMultiset()); + Assert.True(multiset.IsUnordered()); + Assert.Equal(4, multiset.Count()); + Assert.Equal(2, multiset.CountOf(new UtilCollection("3"))); + Assert.Equal(0, multiset.CountOf(new UtilCollection("9"))); + Assert.True(multiset.Contains(new UtilCollection("2"))); + } + + [Fact] + public void EnumeratesEveryOccurrenceInInputOrder() + { + UtilCollection multiset = M("{3, 2, 3, 2}"); + Assert.Equal(new[] { "3", "2", "3", "2" }, multiset.ToList().Select(x => x.ToString())); + Assert.Equal("{3,2,3,2}", multiset.ToString()); + int seen = 0; + foreach (UtilCollection _ in multiset) seen++; + Assert.Equal(4, seen); + } + + [Fact] + public void KeepsDuplicateTuplesAndNestedCollections() + { + Assert.Equal(2, M("{(10,60),(10,60)}").Count()); + Assert.Equal(2, M("{{1,2},{2,1}}").Count()); + } + + [Fact] + public void AddKeepsCounts() + { + UtilCollection set = new UtilCollection("{1,1}"); + set.Add(new UtilCollection("1")); + Assert.Equal(1, set.Count()); + Assert.Equal(3, set.AsMultiset().CountOf(new UtilCollection("1"))); + + UtilCollection multiset = M("{1}"); + multiset.Add(new UtilCollection("1")); + Assert.Equal(2, multiset.CountOf(new UtilCollection("1"))); + } + + [Fact] + public void SetBuiltInCodeBecomesMultisetOfItsElements() + { + UtilCollection set = new UtilCollection(new HashSet { "a", "b" }); + Assert.Equal(2, set.AsMultiset().Count()); + } + + [Theory] + [InlineData("(1,1)")] + [InlineData("x")] + public void OnlySetsBecomeMultisets(string input) + { + Assert.Throws(() => new UtilCollection(input).AsMultiset()); + } + + // --- Equality and hashing --- + + [Theory] + [InlineData("{3,3,2}", "{2,3,3}")] + [InlineData("{(1,2),(1,2),3}", "{3,(1,2),(1,2)}")] + [InlineData("{}", "{}")] + public void EqualWhenCountsMatchInAnyOrder(string a, string b) + { + Assert.Equal(M(a), M(b)); + Assert.Equal(M(a).GetHashCode(), M(b).GetHashCode()); + } + + [Theory] + [InlineData("{3,3,2}", "{3,2,2}")] + [InlineData("{3,3,2}", "{3,2}")] + [InlineData("{3}", "{3,3}")] + public void NotEqualWhenCountsDiffer(string a, string b) + { + Assert.NotEqual(M(a), M(b)); + } + + [Fact] + public void SetAndMultisetAreNeverEqual() + { + Assert.NotEqual(new UtilCollection("{1,2}"), M("{1,2}")); + Assert.NotEqual(M("{1,2}"), new UtilCollection("{1,2}")); + } + + [Fact] + public void MultisetsWorkAsHashSetKeys() + { + HashSet seen = new() { M("{1,1,2}") }; + Assert.Contains(M("{2,1,1}"), seen); + Assert.DoesNotContain(M("{1,2,2}"), seen); + } + + // --- Sub-multisets, union, intersection --- + + [Theory] + [InlineData("{7,7}", "{7,7,1}", true)] + [InlineData("{7,7}", "{7,1}", false)] + [InlineData("{}", "{1}", true)] + [InlineData("{1,2}", "{2,1,3}", true)] + public void IsSubsetOfComparesCounts(string a, string b, bool expected) + { + Assert.Equal(expected, M(a).IsSubsetOf(M(b))); + } + + [Fact] + public void IsSubsetOfAlsoWorksForSets() + { + Assert.True(new UtilCollection("{1,2}").IsSubsetOf(new UtilCollection("{1,2,3}"))); + Assert.False(new UtilCollection("{1,4}").IsSubsetOf(new UtilCollection("{1,2,3}"))); + Assert.Throws(() => new UtilCollection("(1)").IsSubsetOf(new UtilCollection("{1}"))); + } + + [Fact] + public void UnionTakesTheLargerCount() + { + Assert.Equal(M("{1,1,1,2,3}"), M("{1,1,2}").Union(M("{1,1,1,3}"))); + } + + [Fact] + public void IntersectTakesTheSmallerCount() + { + Assert.Equal(M("{1,1}"), M("{1,1,2}").Intersect(M("{1,1,1,3}"))); + } + + [Fact] + public void CannotMixSetsAndMultisets() + { + Assert.Throws(() => M("{1}").Union(new UtilCollection("{1}"))); + Assert.Throws(() => new UtilCollection("{1}").Intersect(M("{1}"))); + } + + // --- Definition strings --- + + private static StringParser Parse(string grammar, string instance) + { + StringParser parser = new StringParser(grammar); + parser.parse(instance); + return parser; + } + + [Theory] + [InlineData("{N | N is multiset, N subset int}")] + [InlineData("{N | N subset int, N is multiset}")] + public void IsMultisetKeepsDuplicatesWhereverItIsWritten(string grammar) + { + UtilCollection n = Parse(grammar, "{3, 3, 2, 2}")["N"]; + Assert.True(n.IsMultiset()); + Assert.Equal(4, n.Count()); + } + + [Fact] + public void OtherConstraintsCheckEveryOccurrence() + { + SpadeParseException e = Assert.Throws( + () => Parse("{N | N is multiset, N subset int}", "{3,3,x}")); + Assert.Equal("Constraint N subset int failed: x is not a member", e.Message); + } + + [Theory] + [InlineData("(3,3)")] + [InlineData("3")] + public void IsMultisetNeedsBraces(string instance) + { + SpadeParseException e = Assert.Throws(() => Parse("{N | N is multiset}", instance)); + Assert.Equal($"Constraint N is multiset failed: {instance} is not a multiset", e.Message); + } + + [Fact] + public void IsSetRejectsAMultiset() + { + Assert.Throws(() => Parse("{N | N is multiset, N is set}", "{1,1}")); + } + + [Fact] + public void FieldsNotMarkedStaySets() + { + StringParser parser = Parse("{(S,T) | S is multiset, T is set}", "({1,1},{2,2})"); + Assert.Equal(2, parser["S"].Count()); + Assert.Equal(1, parser["T"].Count()); + } + + // The Redux grammars proposed in #1. + + [Fact] + public void PartitionKeepsRepeatedNumbers() + { + UtilCollection n = Parse("{N | N is multiset, N subset int}", "{1,7,12,15,33,12,11,5,6,9,21,18}")["N"]; + Assert.Equal(12, n.Count()); + Assert.Equal(150, n.ToList().Sum(x => x.parseInt())); + } + + [Fact] + public void SubsetSumCertificateIsCheckedByCount() + { + UtilCollection s = Parse("{(S,T) | S is multiset, S subset int, T is int}", "({7,7,1},14)")["S"]; + UtilCollection k = Parse("{K | K is multiset}", "{7,7}")["K"]; + Assert.True(k.IsSubsetOf(s)); + Assert.False(Parse("{K | K is multiset}", "{7,7,7}")["K"].IsSubsetOf(s)); + } + + [Fact] + public void KnapsackKeepsIdenticalItems() + { + string grammar = "{(i, w, v) | i is multiset, i subset int cross int, w is int, v is int}"; + UtilCollection items = Parse(grammar, "({(10,60),(10,60),(20,100)},50,220)")["i"]; + Assert.Equal(3, items.Count()); + Assert.Equal(2, items.CountOf(new UtilCollection("(10,60)"))); + } +} From a3debad5ccc3261f3eb8c2eb71dcfb3a43f9f13f Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:15:18 -0600 Subject: [PATCH 08/35] feat(definition): describe a definition's parts without parsing an instance StringParser.Definition reads the definition string on first use and returns its outline and each named part's kind, object, path and constraints, so a caller can build an editor for a problem before any instance exists. --- DefinitionDescription.cs | 302 ++++++++++++++++++ Documentation/example.md | 25 ++ StringParser.cs | 8 + .../SPADE.Tests/DefinitionDescriptionTests.cs | 202 ++++++++++++ 4 files changed, 537 insertions(+) create mode 100644 DefinitionDescription.cs create mode 100644 tests/SPADE.Tests/DefinitionDescriptionTests.cs diff --git a/DefinitionDescription.cs b/DefinitionDescription.cs new file mode 100644 index 0000000..1cde2ab --- /dev/null +++ b/DefinitionDescription.cs @@ -0,0 +1,302 @@ +using Antlr4.Runtime; +using Antlr4.Runtime.Misc; + +using SPADE.Grammar; + +namespace SPADE; + +/// +/// What a definition string's constraints say a named part is. +/// +public enum PartKind +{ + /// No constraint says what the part is. + Any, + + /// "is set". + Set, + + /// "is multiset". + Multiset, + + /// "is list". + List, + + /// "is int". + Int, + + /// "subset X", with no "is" type; holds X. + Subset, + + /// "is X" or "in X" where X is another part or a set expression; holds X. + Member, +} + +/// +/// One named part of a definition string's format, such as N or E in "{(N,E) | ...}". +/// +public class DefinitionPart +{ + /// The name as written in the format, e.g. "E". + public string Name { get; } + + /// What the part's constraints say it is. An "is set", "is multiset", "is list" or "is int" constraint wins over "subset" or "in". + public PartKind Kind { get; } + + /// + /// The object the part is a subset or member of, as written, e.g. "N unorderedcross N" for "E subset N unorderedcross N". + /// Taken from the first "subset" constraint, else the first "is X" or "in X" constraint; null when there is neither. + /// + public string? Of { get; } + + /// Child indices from the outline's root to this part, e.g. [0,1] for E in "((N,E),K)". Empty when the outline is a single name. + public IReadOnlyList Path { get; } + + /// Every top-level constraint that names this part, as written, in definition order. + public IReadOnlyList Constraints { get; } + + internal DefinitionPart(string name, PartKind kind, string? of, IReadOnlyList path, IReadOnlyList constraints) + { + Name = name; + Kind = kind; + Of = of; + Path = path; + Constraints = constraints; + } + + public override string ToString() + { + return Of == null ? $"{Name}: {Kind}" : $"{Name}: {Kind} of {Of}"; + } +} + +/// +/// The names and types a definition string declares, read without parsing an instance. +/// Only the outermost set's format and constraints describe parts; sets nested inside a constraint are part of that constraint's text. +/// +public class DefinitionDescription +{ + /// The format with whitespace removed, e.g. "((N,E),K)". + public string Outline { get; } + + /// The named parts, in the order they appear in . + public IReadOnlyList Parts { get; } + + private readonly Dictionary byName; + + private DefinitionDescription(string outline, List parts) + { + Outline = outline; + Parts = parts.AsReadOnly(); + byName = new(); + foreach (DefinitionPart part in parts) + byName.TryAdd(part.Name, part); + } + + /// The part with this name. Throws KeyNotFoundException when the outline has no such name. + public DefinitionPart this[string name] + { + get + { + if (byName.TryGetValue(name, out DefinitionPart? part)) return part; + throw new KeyNotFoundException($"The definition {Outline} has no part named {name}"); + } + } + + /// Whether the outline has a part with this name. + public bool Contains(string name) + { + return byName.ContainsKey(name); + } + + /// Looks up a part by name without throwing. + public bool TryGetPart(string name, out DefinitionPart? part) + { + return byName.TryGetValue(name, out part); + } + + /// + /// Reads a definition string such as "{(N,E) | N is set, E subset N unorderedcross N}". + /// Throws FormatException naming the definition string when it can't be read. + /// + public static DefinitionDescription Parse(string definition) + { + if (definition == null) throw new FormatException("Definition string is null"); + + ThrowingErrorListener listener = new(definition); + collLexer lexer = new(new AntlrInputStream(definition)); + lexer.RemoveErrorListeners(); + lexer.AddErrorListener(listener); + collParser parser = new(new CommonTokenStream(lexer)); + parser.RemoveErrorListeners(); + parser.AddErrorListener(listener); + + collParser.SetContext set = parser.start().set(); + collParser.FormatContext? format = set.format(); + if (format == null) + throw new FormatException($"Definition string \"{definition}\" is a list of values, not a format followed by |"); + + // Walk the outline once for its text and the path to every name. + List<(string name, List path)> names = new(); + string outline = WalkOutline(format, new List(), names); + HashSet nameSet = names.Select(n => n.name).ToHashSet(); + + Dictionary> constraints = new(); + Dictionary> simple = new(); + foreach (string name in nameSet) + { + constraints[name] = new(); + simple[name] = new(); + } + + foreach (collParser.ConstraintContext constraint in set.constraint()) + { + string text = SourceText(constraint); + if (constraint.TERM() != null) + { + string subject = constraint.TERM().GetText(); + if (!nameSet.Contains(subject)) continue; + constraints[subject].Add(text); + simple[subject].Add(constraint); + continue; + } + + // and / or / not: listed under every part it constrains, but says nothing about kind. + HashSet subjects = new(); + Subjects(constraint, subjects); + foreach (string subject in subjects.Where(nameSet.Contains)) + constraints[subject].Add(text); + } + + List parts = new(); + HashSet seen = new(); + foreach ((string name, List path) in names) + { + if (!seen.Add(name)) continue; + (PartKind kind, string? of) = Classify(simple[name], nameSet); + parts.Add(new DefinitionPart(name, kind, of, path.AsReadOnly(), constraints[name].AsReadOnly())); + } + + return new DefinitionDescription(outline, parts); + } + + // The format's text without whitespace, recording the child-index path to each name. + private static string WalkOutline(collParser.FormatContext format, List path, List<(string, List)> names) + { + if (format.TERM() != null) + { + string name = format.TERM().GetText(); + names.Add((name, new List(path))); + return name; + } + + collParser.FormatContext[] children = format.format(); + List texts = new(); + for (int i = 0; i < children.Length; i++) + { + path.Add(i); + texts.Add(WalkOutline(children[i], path, names)); + path.RemoveAt(path.Count - 1); + } + string open = format.Start.Text; + string close = open == "{" ? "}" : ")"; + return open + string.Join(",", texts) + close; + } + + // The names a compound constraint constrains: the subject of each simple constraint inside it. + private static void Subjects(collParser.ConstraintContext constraint, HashSet subjects) + { + if (constraint.TERM() != null) + { + subjects.Add(constraint.TERM().GetText()); + return; + } + foreach (collParser.ConstraintContext inner in constraint.constraint()) + Subjects(inner, subjects); + } + + // An "is set/multiset/list/int" wins the kind; Of comes from the first subset, else the first membership. + private static (PartKind, string?) Classify(List constraints, HashSet names) + { + PartKind? type = null; + string? subsetOf = null; + string? memberOf = null; + + foreach (collParser.ConstraintContext constraint in constraints) + { + string definition = constraint.DEFINITION().GetText(); + collParser.ObjectContext obj = constraint.@object(); + string objText = SourceText(obj); + + if (definition == "subset") + { + subsetOf ??= objText; + continue; + } + + if (definition == "is") + { + if (obj.BUILT_IN() != null) + { + PartKind? builtIn = objText switch + { + "set" => PartKind.Set, + "multiset" => PartKind.Multiset, + "list" => PartKind.List, + _ => null, + }; + type ??= builtIn; + continue; + } + if (obj.BUILT_IN_SET() != null) + { + type ??= PartKind.Int; + continue; + } + } + + // "is X" / "in X": a member of another part or of a set expression. A bare word that names + // no part (e.g. "is string") isn't something SPADE knows yet, so it says nothing. + if (obj.TERM() != null && !names.Contains(objText)) continue; + memberOf ??= objText; + } + + string? of = subsetOf ?? memberOf; + if (type != null) return (type.Value, of); + if (subsetOf != null) return (PartKind.Subset, subsetOf); + if (memberOf != null) return (PartKind.Member, memberOf); + return (PartKind.Any, null); + } + + // A rule's text as written in the definition string, spacing included. + private static string SourceText(ParserRuleContext context) + { + return context.Start.InputStream.GetText(Interval.Of(context.Start.StartIndex, context.Stop.StopIndex)); + } + + // Turns the first lexer or parser error into a FormatException naming the definition string. + private class ThrowingErrorListener : IAntlrErrorListener, IAntlrErrorListener + { + private readonly string definition; + + public ThrowingErrorListener(string definition) + { + this.definition = definition; + } + + public void SyntaxError(TextWriter output, IRecognizer recognizer, int offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) + { + Fail(charPositionInLine, msg); + } + + public void SyntaxError(TextWriter output, IRecognizer recognizer, IToken offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) + { + Fail(charPositionInLine, msg); + } + + private void Fail(int charPositionInLine, string msg) + { + throw new FormatException($"Definition string \"{definition}\" can't be read: {msg} at position {charPositionInLine + 1}"); + } + } +} diff --git a/Documentation/example.md b/Documentation/example.md index 2b7dbe7..d4ab799 100644 --- a/Documentation/example.md +++ b/Documentation/example.md @@ -47,3 +47,28 @@ catch (SpadeParseException e) ``` Any other exception type means a bug in SPADE or in the definition string, not bad input. + +## Describing a definition string + +`Definition` tells you the names and types a [definition string](DefinitionString.md) declares, without needing an instance string. This is useful for building an editor or form for a problem before any instance exists. + +```c# +StringParser problem = new("{((N,E),K) | N is set, E subset N unorderedcross N, K is int}"); +DefinitionDescription d = problem.Definition; + +Console.WriteLine(d.Outline); // ((N,E),K) + +foreach (DefinitionPart part in d.Parts) +{ + // N: Set at [0,0] + // E: Subset of N unorderedcross N at [0,1] + // K: Int at [1] + Console.WriteLine($"{part} at [{string.Join(",", part.Path)}]"); +} + +Console.WriteLine(d["E"].Constraints[0]); // E subset N unorderedcross N +``` + +Each part has a `Name`, a `Kind` (`Set`, `Multiset`, `List`, `Int`, `Subset`, `Member`, or `Any` when nothing says what it is), `Of` (what it is a subset or member of, as written), `Path` (where it sits in the outline), and `Constraints` (every constraint naming it, as written). Only the outer format's names are parts; names inside a nested set, like `a` in `S subset {a | a subset U}`, are not. + +`Definition` is read the first time you use it, so creating a `StringParser` never throws. If the definition string can't be read, `Definition` throws a `FormatException` naming it. `DefinitionDescription.Parse(definitionString)` does the same without a `StringParser`. diff --git a/StringParser.cs b/StringParser.cs index 23fb6a1..638630e 100644 --- a/StringParser.cs +++ b/StringParser.cs @@ -14,6 +14,14 @@ public class StringParser private bool hasParsed; + private DefinitionDescription? definition; + + /// + /// The names and types the definition string declares, read without parsing an instance. + /// Read on first use; throws FormatException if the definition string can't be read. + /// + public DefinitionDescription Definition => definition ??= DefinitionDescription.Parse(format); + public StringParser(string f) { format = f; diff --git a/tests/SPADE.Tests/DefinitionDescriptionTests.cs b/tests/SPADE.Tests/DefinitionDescriptionTests.cs new file mode 100644 index 0000000..7c04c5e --- /dev/null +++ b/tests/SPADE.Tests/DefinitionDescriptionTests.cs @@ -0,0 +1,202 @@ +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// StringParser.Definition describes a definition string's names and types without an instance (#18). +public class DefinitionDescriptionTests +{ + private static DefinitionDescription Describe(string grammar) + { + return new StringParser(grammar).Definition; + } + + [Fact] + public void NestedOutlineHasPathsAndKinds() + { + DefinitionDescription d = Describe("{((N,E),K) | N is set, E subset N unorderedcross N, K is int}"); + + Assert.Equal("((N,E),K)", d.Outline); + Assert.Equal(new[] { "N", "E", "K" }, d.Parts.Select(p => p.Name)); + + Assert.Equal(PartKind.Set, d["N"].Kind); + Assert.Null(d["N"].Of); + Assert.Equal(new[] { 0, 0 }, d["N"].Path); + + Assert.Equal(PartKind.Subset, d["E"].Kind); + Assert.Equal("N unorderedcross N", d["E"].Of); + Assert.Equal(new[] { 0, 1 }, d["E"].Path); + Assert.Equal(new[] { "E subset N unorderedcross N" }, d["E"].Constraints); + + Assert.Equal(PartKind.Int, d["K"].Kind); + Assert.Equal(new[] { 1 }, d["K"].Path); + } + + [Fact] + public void NestedSetNamesAreNotParts() + { + DefinitionDescription d = Describe("{(N,E) | N is set, E subset {(e,w) | e is N unorderedcross N, w is int}}"); + + Assert.Equal(new[] { "N", "E" }, d.Parts.Select(p => p.Name)); + Assert.Equal(PartKind.Subset, d["E"].Kind); + Assert.Equal("{(e,w) | e is N unorderedcross N, w is int}", d["E"].Of); + Assert.False(d.Contains("e")); + Assert.False(d.Contains("w")); + } + + [Theory] + [InlineData("{(N,E) | N is set, E subset {(u,v) | u is N, v is N}}", "{(u,v) | u is N, v is N}")] + [InlineData("{(U,S) | U is set, S subset {a | a subset U}}", "{a | a subset U}")] + public void SubsetOfASetComprehension(string grammar, string of) + { + DefinitionDescription d = Describe(grammar); + + Assert.Equal(2, d.Parts.Count); + Assert.Equal(PartKind.Set, d.Parts[0].Kind); + Assert.Equal(PartKind.Subset, d.Parts[1].Kind); + Assert.Equal(of, d.Parts[1].Of); + } + + [Fact] + public void IsMultisetWinsKindAndSubsetGivesOf() + { + DefinitionDescription d = Describe("{(S,T) | S is multiset, S subset int, T is int}"); + + Assert.Equal(PartKind.Multiset, d["S"].Kind); + Assert.Equal("int", d["S"].Of); + Assert.Equal(new[] { "S is multiset", "S subset int" }, d["S"].Constraints); + } + + [Fact] + public void IsSetWithSubsetKeepsSet() + { + DefinitionDescription d = Describe("{(N,A) | N is set, A is set, A subset N}"); + + Assert.Equal(PartKind.Set, d["A"].Kind); + Assert.Equal("N", d["A"].Of); + } + + [Fact] + public void SingleNameOutlineHasEmptyPath() + { + DefinitionDescription d = Describe("{N | N is set}"); + + Assert.Equal("N", d.Outline); + Assert.Empty(d["N"].Path); + Assert.Equal(PartKind.Set, d["N"].Kind); + } + + [Fact] + public void ListKinds() + { + DefinitionDescription d = Describe("{(T,D,P) | T is list, D is list, P is list}"); + + Assert.All(d.Parts, p => Assert.Equal(PartKind.List, p.Kind)); + Assert.Equal(new[] { 2 }, d["P"].Path); + } + + [Fact] + public void OutlineDropsWhitespace() + { + DefinitionDescription d = Describe("{(i, w, v) | i subset int cross int, w is int, v is int}"); + + Assert.Equal("(i,w,v)", d.Outline); + Assert.Equal(PartKind.Subset, d["i"].Kind); + Assert.Equal("int cross int", d["i"].Of); + } + + [Fact] + public void MembersOfOtherPartsAndExpressions() + { + DefinitionDescription d = Describe("{(N,A,u,E) | N is set, A is set, u is N, E is N cross A cross N}"); + + Assert.Equal(PartKind.Member, d["u"].Kind); + Assert.Equal("N", d["u"].Of); + Assert.Equal(PartKind.Member, d["E"].Kind); + Assert.Equal("N cross A cross N", d["E"].Of); + } + + [Fact] + public void UnconstrainedAndUnknownTypesAreAny() + { + DefinitionDescription d = Describe("{(A,B,C) | B is string, C is graph}"); + + Assert.Equal(PartKind.Any, d["A"].Kind); + Assert.Empty(d["A"].Constraints); + Assert.Equal(PartKind.Any, d["B"].Kind); + Assert.Null(d["B"].Of); + Assert.Equal(new[] { "B is string" }, d["B"].Constraints); + Assert.Equal(PartKind.Any, d["C"].Kind); + } + + [Fact] + public void CompoundConstraintsAreListedButDontSetKind() + { + DefinitionDescription d = Describe("{(A,B) | A is set or B is set, not A is int}"); + + Assert.Equal(PartKind.Any, d["A"].Kind); + Assert.Equal(new[] { "A is set or B is set", "not A is int" }, d["A"].Constraints); + Assert.Equal(new[] { "A is set or B is set" }, d["B"].Constraints); + } + + [Fact] + public void ConstraintsOnUnknownNamesAreIgnored() + { + DefinitionDescription d = Describe("{N | N is set, X is int}"); + + Assert.Single(d.Parts); + Assert.False(d.Contains("X")); + } + + [Fact] + public void BraceOutlineKeepsBraces() + { + DefinitionDescription d = Describe("{{A,B} | A is set, B is set}"); + + Assert.Equal("{A,B}", d.Outline); + Assert.Equal(new[] { 1 }, d["B"].Path); + } + + [Fact] + public void UnknownNameThrowsKeyNotFound() + { + DefinitionDescription d = Describe("{N | N is set}"); + + KeyNotFoundException e = Assert.Throws(() => d["E"]); + Assert.Contains("E", e.Message); + Assert.False(d.TryGetPart("E", out _)); + Assert.True(d.TryGetPart("N", out DefinitionPart? n)); + Assert.Equal("N", n!.Name); + } + + [Fact] + public void BadDefinitionThrowsOnlyWhenRead() + { + StringParser parser = new("not a definition"); + + FormatException e = Assert.Throws(() => parser.Definition); + Assert.Contains("not a definition", e.Message); + } + + [Fact] + public void ParseDirectlyMatchesTheProperty() + { + Assert.Equal("(N,E)", DefinitionDescription.Parse("{(N,E) | N is set, E subset N cross N}").Outline); + Assert.Throws(() => DefinitionDescription.Parse("{(N,E) | N is}")); + } + + [Fact] + public void WorksBeforeAndAfterParse() + { + StringParser graph = new("{(N,E) | N is set, E subset N unorderedcross N}"); + + DefinitionDescription before = graph.Definition; + graph.parse("({1,2,3},{{1,2},{2,3}})"); + DefinitionDescription after = graph.Definition; + + Assert.Same(before, after); + Assert.Equal(PartKind.Subset, after["E"].Kind); + Assert.Equal(new UtilCollection("{1,2,3}"), graph["N"]); + Assert.Equal(new UtilCollection("{{1,2},{2,3}}"), graph["E"]); + } +} From fedce94bedc5b42ad7ebb295fb7db332e3bab0dc Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:50:41 -0600 Subject: [PATCH 09/35] feat(errors): name the part, element and missing value on SpadeParseException SpadeParseException gets Part, Element, MissingValue and ExpectedIn, so a caller can find the part an error belongs to without reading the message. Failures inside a nested set belong to the outer part and member. Messages are unchanged. Closes #19 --- Documentation/example.md | 17 +++ SpadeParseException.cs | 49 ++++++ grammar/desciptors/ConstraintDescriptor.cs | 39 +++-- grammar/desciptors/SetDescriptor.cs | 65 +++++++- grammar/desciptors/formatDescriptor.cs | 5 + tests/SPADE.Tests/ErrorPartTests.cs | 170 +++++++++++++++++++++ 6 files changed, 332 insertions(+), 13 deletions(-) create mode 100644 tests/SPADE.Tests/ErrorPartTests.cs diff --git a/Documentation/example.md b/Documentation/example.md index d4ab799..410e1da 100644 --- a/Documentation/example.md +++ b/Documentation/example.md @@ -46,6 +46,23 @@ catch (SpadeParseException e) } ``` +To act on an error without reading its message, use its properties. When a constraint or type check fails, `Part` names the part of the outer format it belongs to and `Element` holds the member or value that failed, in full even when the message shortens it. When that element is missing a piece from another part, `MissingValue` and `ExpectedIn` name the piece and the part: + +```c# +StringParser graph = new("{(N,E) | N is set, E subset N unorderedcross N}"); +try +{ + graph.parse("({1,2},{{1,9}})"); +} +catch (SpadeParseException e) +{ + // "Constraint E subset N unorderedcross N failed: {1,9} is not a member" + Console.WriteLine($"{e.Part} {e.Element} {e.MissingValue} {e.ExpectedIn}"); // E {1,9} 9 N +} +``` + +A failure inside a nested set, like `w is int` in `E subset {(e,w) | ...}`, belongs to the outer part (`E`) and the member of it that failed. `MissingValue` and `ExpectedIn` are null when the set isn't built from parts, as in `S subset int`, and all four are null for text that can't be read or a shape that doesn't match the definition string. + Any other exception type means a bug in SPADE or in the definition string, not bad input. ## Describing a definition string diff --git a/SpadeParseException.cs b/SpadeParseException.cs index be2ca02..5e1042e 100644 --- a/SpadeParseException.cs +++ b/SpadeParseException.cs @@ -15,6 +15,29 @@ public class SpadeParseException : FormatException /// What the parser found instead, when the error has one. public string? Found { get; } + /// + /// The name of the part the error belongs to, from the outermost format of the definition string, + /// e.g. "E" for a failed E subset N unorderedcross N. Set for failed constraints and type checks, + /// including those inside a nested set, which belong to the part that set constrains. + /// Null for syntax errors and for an instance whose shape doesn't match the format. + /// + public string? Part { get; internal init; } + + /// + /// The member or value of that failed, in full, e.g. "{1,9}" for an edge that + /// isn't a member, or the whole value for a failed type check. The message may shorten it; this doesn't. + /// + public string? Element { get; internal init; } + + /// + /// When failed because a piece of it is missing from another part, that piece, + /// e.g. "9" for the edge {1,9} when N is {1,2}. Null when the set isn't built from parts, such as int. + /// + public string? MissingValue { get; internal init; } + + /// The part is missing from, e.g. "N". Null when is. + public string? ExpectedIn { get; internal init; } + /// A syntax error at a position, e.g. "expected , or } at position 14, found x". public SpadeParseException(int position, string expected, string found) : base($"expected {expected} at position {position}, found {found}") @@ -29,4 +52,30 @@ public SpadeParseException(string message) : base(message) { } + + // A copy with the same message and position, for an outer constraint to claim an inner error + // as its own (#19). The original stays as the inner exception. + private SpadeParseException(SpadeParseException inner) + : base(inner.Message, inner) + { + Position = inner.Position; + Expected = inner.Expected; + Found = inner.Found; + Part = inner.Part; + Element = inner.Element; + MissingValue = inner.MissingValue; + ExpectedIn = inner.ExpectedIn; + } + + /// The same error, message unchanged, belonging to another part and element. + internal SpadeParseException BelongingTo(string part, string element, string? missingValue, string? expectedIn) + { + return new SpadeParseException(this) + { + Part = part, + Element = element, + MissingValue = missingValue, + ExpectedIn = expectedIn, + }; + } } diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 1459e98..427fc33 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -38,9 +38,17 @@ public static string Describe(UtilCollection collection) // "N is multiset" changes how N is read, so it runs before the other constraints (#1). public bool DeclaresMultiset => definition == "is" && built_in == "multiset"; - private SpadeParseException Failure(string reason) + // The failure belongs to this constraint's name, so a caller can find the part without reading + // the message (#19). The element is kept in full; the message shortens it. + private SpadeParseException Failure(string reason, UtilCollection element, (string Value, string Part)? missing = null) { - return new SpadeParseException($"Constraint {text} failed: {reason}"); + return new SpadeParseException($"Constraint {text} failed: {reason}") + { + Part = term, + Element = element.ToString(), + MissingValue = missing?.Value, + ExpectedIn = missing?.Part, + }; } @@ -55,26 +63,26 @@ public void check(Dictionary map) case "set": if (map[term].IsValue() || map[term].IsOrdered() || map[term].IsMultiset()) { - throw Failure($"{Describe(map[term])} is not a set"); + throw Failure($"{Describe(map[term])} is not a set", map[term]); } break; case "list": if (map[term].IsUnordered()) { - throw Failure($"{Describe(map[term])} is not a list"); + throw Failure($"{Describe(map[term])} is not a list", map[term]); } break; case "multiset": if (map[term].IsValue() || map[term].IsOrdered()) { - throw Failure($"{Describe(map[term])} is not a multiset"); + throw Failure($"{Describe(map[term])} is not a multiset", map[term]); } map[term] = map[term].AsMultiset(); break; case "int": if (!map[term].IsValue() || !int.TryParse(map[term].ToString(), out _)) { - throw Failure($"{Describe(map[term])} is not an integer"); + throw Failure($"{Describe(map[term])} is not an integer", map[term]); } break; } @@ -85,13 +93,26 @@ public void check(Dictionary map) { if (map[term].IsValue()) { - throw Failure($"{Describe(map[term])} is not a set"); + throw Failure($"{Describe(map[term])} is not a set", map[term]); } foreach (UtilCollection item in map[term]) { - if (!set.isMember(item)) + bool member; + try + { + member = set.isMember(item); + } + catch (SpadeParseException e) + { + // A constraint inside a nested set failed, such as w is int in + // {(e,w) | ...}. The message stays, but the error belongs to this part + // and item. A missing piece is dropped if its part is a nested name. + bool nested = e.ExpectedIn == null || set.declaredNames.Contains(e.ExpectedIn); + throw e.BelongingTo(term, item.ToString(), nested ? null : e.MissingValue, nested ? null : e.ExpectedIn); + } + if (!member) { - throw Failure($"{Describe(item)} is not a member"); + throw Failure($"{Describe(item)} is not a member", item, set.FindMissing(item)); } } } diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index 9d755e4..3e71df3 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -16,6 +16,14 @@ class SetDescriptor public Dictionary map; + // Why an item isn't a member: the first piece of it missing from a part named in the set + // expression, with that part, or null when the set isn't built from parts (#19). + private Func missing = (a) => null; + + // Names declared by nested set definitions in this expression, such as e and w in + // {(e,w) | ...}. Errors about them belong to the part the nested set constrains. + public HashSet declaredNames = new(); + public SetDescriptor(Dictionary _map) { map = _map; @@ -29,6 +37,15 @@ public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet, Func _missing) + : this(left.map, _isMember, _genSet) + { + missing = _missing; + declaredNames.UnionWith(left.declaredNames); + declaredNames.UnionWith(right.declaredNames); + } public SetDescriptor(Dictionary _map, UtilCollection collection) { map = _map; @@ -41,6 +58,7 @@ public SetDescriptor(Dictionary _map, String name) map = _map; isMember = (checkedColl) => map[name].Contains(checkedColl); genSet = () => map[name]; + missing = (checkedColl) => map[name].Contains(checkedColl) ? null : (checkedColl.ToString(), name); } public SetDescriptor(Dictionary _map, BuiltInSet builtInSet) @@ -53,6 +71,7 @@ public SetDescriptor(Dictionary _map, BuiltInSet builtIn public SetDescriptor(Dictionary _map, FormatDescriptor formatDescriptor, List constraintDescriptors) { map = _map; + declaredNames.UnionWith(formatDescriptor.names); // OrderBy is stable, so the other constraints keep their order. constraintDescriptors = constraintDescriptors.OrderBy(c => c.DeclaresMultiset ? 0 : 1).ToList(); isMember = (collection) => @@ -74,13 +93,36 @@ public SetDescriptor(Dictionary _map, FormatDescriptor f return new SetDescriptor(_isMember, _genSet); */ + /// + /// For an item that isn't a member: the first piece of it missing from a part named in the set + /// expression, and that part. Null when no such piece is found, e.g. for int or a set + /// literal, or when the item has the wrong shape (a value where cross needs a pair). + /// + public (string Value, string Part)? FindMissing(UtilCollection item) + { + try + { + return missing(item); + } + catch (SpadeParseException) + { + // A nested set that throws reports its own error, not a missing piece. + return null; + } + } + public SetDescriptor Cross(SetDescriptor other) { Func _isMember = (a) => a.IsOrdered() && a.Count() == 2 && isMember(a[0]) && other.isMember(a[1]); Func _genSet = () => genSet().Cross(other.genSet()); - return new SetDescriptor(map, _isMember, _genSet); + Func _missing = (a) => + { + if (!a.IsOrdered() || a.Count() != 2) return null; + return !isMember(a[0]) ? missing(a[0]) : other.missing(a[1]); + }; + return new SetDescriptor(this, other, _isMember, _genSet, _missing); } public SetDescriptor UnorderedCross(SetDescriptor other) @@ -115,7 +157,19 @@ public SetDescriptor UnorderedCross(SetDescriptor other) } return set; }; - return new SetDescriptor(map, _isMember, _genSet); + Func _missing = (a) => + { + if (a.IsValue() || a.IsOrdered()) return null; + foreach (UtilCollection item in a) + { + if (!isMember(item) && !other.isMember(item)) + { + return missing(item) ?? other.missing(item); + } + } + return null; + }; + return new SetDescriptor(this, other, _isMember, _genSet, _missing); } public SetDescriptor Union(SetDescriptor other) @@ -123,14 +177,17 @@ public SetDescriptor Union(SetDescriptor other) Func _isMember = (a) => isMember(a) || other.isMember(a); Func _genSet = () => genSet().Union(other.genSet()); - return new SetDescriptor(map, _isMember, _genSet); + // Missing from both sides; the left one is named. + Func _missing = (a) => missing(a) ?? other.missing(a); + return new SetDescriptor(this, other, _isMember, _genSet, _missing); } public SetDescriptor Intersect(SetDescriptor other) { Func _isMember = (a) => isMember(a) && other.isMember(a); Func _genSet = () => genSet().Intersect(other.genSet()); - return new SetDescriptor(map, _isMember, _genSet); + Func _missing = (a) => !isMember(a) ? missing(a) : other.missing(a); + return new SetDescriptor(this, other, _isMember, _genSet, _missing); } } \ No newline at end of file diff --git a/grammar/desciptors/formatDescriptor.cs b/grammar/desciptors/formatDescriptor.cs index bdce081..79321e7 100644 --- a/grammar/desciptors/formatDescriptor.cs +++ b/grammar/desciptors/formatDescriptor.cs @@ -8,13 +8,18 @@ class FormatDescriptor { public Action, UtilCollection> parseFunc; + // The names this format declares, e.g. N and E for (N,E). + public List names = new(); + public FormatDescriptor(String term) { + names.Add(term); parseFunc = (map, coll) => {map[term] = coll;}; } public FormatDescriptor(List lst, String text) { + names = lst.SelectMany(f => f.names).ToList(); parseFunc = (map, coll) => { if (!coll.IsOrdered() || coll.Count() != lst.Count()) diff --git a/tests/SPADE.Tests/ErrorPartTests.cs b/tests/SPADE.Tests/ErrorPartTests.cs new file mode 100644 index 0000000..46a131d --- /dev/null +++ b/tests/SPADE.Tests/ErrorPartTests.cs @@ -0,0 +1,170 @@ +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// A failed constraint names the part it belongs to, the element that failed, and the piece missing +// from another part, so a caller doesn't have to read the message (#19). +public class ErrorPartTests +{ + private const string Graph = "{(N,E) | N is set, E subset N unorderedcross N}"; + private const string Digraph = "{(N,E) | N is set, E subset N cross N}"; + // "e is N unorderedcross N" is not checked yet (see #14); these tests fail on w instead. + private const string WeightedGraph = "{(N,E) | N is set, E subset {(e,w) | e is N unorderedcross N, w is int}}"; + + private static SpadeParseException Fails(string grammar, string instance) + { + return Assert.Throws(() => new StringParser(grammar).parse(instance)); + } + + [Fact] + public void FailedSubsetNamesPartElementAndMissingValue() + { + SpadeParseException e = Fails(Graph, "({1,2},{{1,9}})"); + Assert.Equal("Constraint E subset N unorderedcross N failed: {1,9} is not a member", e.Message); + Assert.Equal("E", e.Part); + Assert.Equal("{1,9}", e.Element); + Assert.Equal("9", e.MissingValue); + Assert.Equal("N", e.ExpectedIn); + } + + [Fact] + public void CrossNamesTheMissingNode() + { + SpadeParseException e = Fails(Digraph, "({a,b},{(a,b),(a,c)})"); + Assert.Equal("E", e.Part); + Assert.Equal("(a,c)", e.Element); + Assert.Equal("c", e.MissingValue); + Assert.Equal("N", e.ExpectedIn); + } + + [Fact] + public void CrossOfTwoPartsNamesTheSideThatIsMissingTheValue() + { + SpadeParseException e = Fails("{(A,B,R) | A is set, B is set, R subset A cross B}", "({1,2},{x,y},{(1,x),(1,z)})"); + Assert.Equal("R", e.Part); + Assert.Equal("z", e.MissingValue); + Assert.Equal("B", e.ExpectedIn); + } + + [Fact] + public void CrossWithTheWrongShapeHasNoMissingValue() + { + SpadeParseException e = Fails(Digraph, "({a,b},{(a,b,a)})"); + Assert.Equal("E", e.Part); + Assert.Equal("(a,b,a)", e.Element); + Assert.Null(e.MissingValue); + Assert.Null(e.ExpectedIn); + } + + [Fact] + public void SubsetOfIntHasNoMissingValue() + { + SpadeParseException e = Fails("{S | S subset int}", "{1,x}"); + Assert.Equal("Constraint S subset int failed: x is not a member", e.Message); + Assert.Equal("S", e.Part); + Assert.Equal("x", e.Element); + Assert.Null(e.MissingValue); + Assert.Null(e.ExpectedIn); + } + + [Fact] + public void SubsetOfAValueNamesTheWholeValue() + { + SpadeParseException e = Fails(Graph, "({1,2},edges)"); + Assert.Equal("E", e.Part); + Assert.Equal("edges", e.Element); + Assert.Null(e.MissingValue); + } + + [Fact] + public void FailedIntCheckNamesPartAndValue() + { + SpadeParseException e = Fails("{(N,K) | N is set, K is int}", "({1,2},x)"); + Assert.Equal("Constraint K is int failed: x is not an integer", e.Message); + Assert.Equal("K", e.Part); + Assert.Equal("x", e.Element); + Assert.Null(e.MissingValue); + Assert.Null(e.ExpectedIn); + } + + [Fact] + public void FailedSetCheckNamesPart() + { + SpadeParseException e = Fails(Graph, "((1,2),{{1,2}})"); + Assert.Equal("N", e.Part); + Assert.Equal("(1,2)", e.Element); + } + + [Fact] + public void NestedFailureBelongsToTheOuterPart() + { + SpadeParseException e = Fails(WeightedGraph, "({1,2},{({1,2},3),({1,2},x)})"); + Assert.Equal("Constraint w is int failed: x is not an integer", e.Message); + Assert.Equal("E", e.Part); + Assert.Equal("({1,2},x)", e.Element); + Assert.Null(e.MissingValue); + Assert.Null(e.ExpectedIn); + } + + [Fact] + public void NestedShapeErrorBelongsToTheOuterPart() + { + SpadeParseException e = Fails(WeightedGraph, "({1,2},{({1,2},3,4)})"); + Assert.StartsWith("expected a list of 2 elements for (e,w), found", e.Message); + Assert.Equal("E", e.Part); + Assert.Equal("({1,2},3,4)", e.Element); + } + + [Fact] + public void NestedMissingValueKeepsATopLevelPart() + { + SpadeParseException e = Fails("{(N,E) | N is set, E subset {(e,w) | e subset N, w is int}}", "({1,2},{({1,9},3)})"); + Assert.Equal("Constraint e subset N failed: 9 is not a member", e.Message); + Assert.Equal("E", e.Part); + Assert.Equal("({1,9},3)", e.Element); + Assert.Equal("9", e.MissingValue); + Assert.Equal("N", e.ExpectedIn); + } + + [Fact] + public void NestedMissingValueDropsANestedPart() + { + SpadeParseException e = Fails("{(N,E) | N is set, E subset {(a,b) | a is set, b subset a}}", "({1},{({1},{2})})"); + Assert.Equal("E", e.Part); + Assert.Equal("({1},{2})", e.Element); + Assert.Null(e.MissingValue); + Assert.Null(e.ExpectedIn); + } + + [Fact] + public void ElementIsInFullWhenTheMessageShortensIt() + { + string big = "{" + string.Join(",", Enumerable.Range(0, 1000)) + "}"; + SpadeParseException e = Fails("{L | L is list}", big); + Assert.Contains("...", e.Message); + Assert.Equal("L", e.Part); + Assert.Equal(new UtilCollection(big).ToString(), e.Element); + Assert.True(e.Element!.Length > 1000); + } + + [Fact] + public void SyntaxErrorHasNoPart() + { + SpadeParseException e = Fails(Graph, "({1,2},{"); + Assert.Equal("expected a value, { or ( at position 9, found end of input", e.Message); + Assert.Equal(9, e.Position); + Assert.Null(e.Part); + Assert.Null(e.Element); + Assert.Null(e.MissingValue); + Assert.Null(e.ExpectedIn); + } + + [Fact] + public void ShapeErrorHasNoPart() + { + SpadeParseException e = Fails(Graph, "({1,2})"); + Assert.Null(e.Part); + Assert.Null(e.Element); + } +} From e928cae955629ddb1bac6c48a10e7d867adb8b76 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:57:14 -0600 Subject: [PATCH 10/35] feat(grammar): add an is string type check --- DefinitionDescription.cs | 10 +- Documentation/DefinitionString.md | 3 +- Documentation/example.md | 2 +- grammar/coll.g4 | 1 + grammar/collLexer.cs | 96 ++++++++++--------- grammar/collLexer.interp | 2 +- grammar/desciptors/ConstraintDescriptor.cs | 7 ++ .../SPADE.Tests/DefinitionDescriptionTests.cs | 17 +++- tests/SPADE.Tests/TypeConstraintTests.cs | 55 +++++++++++ 9 files changed, 138 insertions(+), 55 deletions(-) diff --git a/DefinitionDescription.cs b/DefinitionDescription.cs index 1cde2ab..100dac9 100644 --- a/DefinitionDescription.cs +++ b/DefinitionDescription.cs @@ -25,6 +25,9 @@ public enum PartKind /// "is int". Int, + /// "is string": a single value, not a set, list or multiset. + String, + /// "subset X", with no "is" type; holds X. Subset, @@ -40,7 +43,7 @@ public class DefinitionPart /// The name as written in the format, e.g. "E". public string Name { get; } - /// What the part's constraints say it is. An "is set", "is multiset", "is list" or "is int" constraint wins over "subset" or "in". + /// What the part's constraints say it is. An "is set", "is multiset", "is list", "is int" or "is string" constraint wins over "subset" or "in". public PartKind Kind { get; } /// @@ -215,7 +218,7 @@ private static void Subjects(collParser.ConstraintContext constraint, HashSet constraints, HashSet names) { PartKind? type = null; @@ -243,6 +246,7 @@ private static (PartKind, string?) Classify(List c "set" => PartKind.Set, "multiset" => PartKind.Multiset, "list" => PartKind.List, + "string" => PartKind.String, _ => null, }; type ??= builtIn; @@ -256,7 +260,7 @@ private static (PartKind, string?) Classify(List c } // "is X" / "in X": a member of another part or of a set expression. A bare word that names - // no part (e.g. "is string") isn't something SPADE knows yet, so it says nothing. + // no part (e.g. "is text") isn't something SPADE knows, so it says nothing. if (obj.TERM() != null && !names.Contains(objText)) continue; memberOf ??= objText; } diff --git a/Documentation/DefinitionString.md b/Documentation/DefinitionString.md index 7517caa..8717c7d 100644 --- a/Documentation/DefinitionString.md +++ b/Documentation/DefinitionString.md @@ -14,8 +14,9 @@ In the definition string, first there is a *format string*, which is the part to After the symbol "|" is a list of assertions or *constraints* which must be true of the instance, separated by commas that are assumed to be true for a valid problem instance. The language used for defining constraints in SPADE is modeled after the language of Set theory. Each constraint says that some named element is either a basic object, or related to the other objects in some way. A constraint is a terminal (one of the named collections from the format string) followed a defintion and then an object. ### Definition -- "is" checks thse type of a collection. It can be followed by set, list, or int, and constraints that collection to be that type. An example is "U is set" which constrains U to be a set +- "is" checks the type of a collection. It can be followed by set, list, int or string, and constrains that collection to be that type. An example is "U is set" which constrains U to be a set - "is multiset" marks a collection written with curly braces as a multiset, so repeated elements are kept instead of dropped. For example, "{N | N is multiset, N subset int}" reads {3,3,2,2} as four numbers. It applies before the other constraints, wherever it is written. See [multisets](InstanceString.md#multisets). +- "is string" checks that a collection is a single value, not a set, list or multiset. Any value counts, a word like `kitten`, words with spaces like `New York`, or a number like `5`. For example, "{(x,y) | x is string, y is string}" accepts (kitten,sitting) and rejects (kitten,{sitting}). Since `string` is a keyword, it can't be used as a name in the format string or a constraint. - "subset" checks that the collection is a subset of the object on the right. An example is "A subset B" which constrains A to be a subset of B. ### Objects diff --git a/Documentation/example.md b/Documentation/example.md index 410e1da..d29fcac 100644 --- a/Documentation/example.md +++ b/Documentation/example.md @@ -86,6 +86,6 @@ foreach (DefinitionPart part in d.Parts) Console.WriteLine(d["E"].Constraints[0]); // E subset N unorderedcross N ``` -Each part has a `Name`, a `Kind` (`Set`, `Multiset`, `List`, `Int`, `Subset`, `Member`, or `Any` when nothing says what it is), `Of` (what it is a subset or member of, as written), `Path` (where it sits in the outline), and `Constraints` (every constraint naming it, as written). Only the outer format's names are parts; names inside a nested set, like `a` in `S subset {a | a subset U}`, are not. +Each part has a `Name`, a `Kind` (`Set`, `Multiset`, `List`, `Int`, `String`, `Subset`, `Member`, or `Any` when nothing says what it is), `Of` (what it is a subset or member of, as written), `Path` (where it sits in the outline), and `Constraints` (every constraint naming it, as written). Only the outer format's names are parts; names inside a nested set, like `a` in `S subset {a | a subset U}`, are not. `Definition` is read the first time you use it, so creating a `StringParser` never throws. If the definition string can't be read, `Definition` throws a `FormatException` naming it. `DefinitionDescription.Parse(definitionString)` does the same without a `StringParser`. diff --git a/grammar/coll.g4 b/grammar/coll.g4 index 2fab116..0f9ec17 100644 --- a/grammar/coll.g4 +++ b/grammar/coll.g4 @@ -45,6 +45,7 @@ BUILT_IN : 'set' | 'list' | 'graph' | 'weightedGraph' + | 'string' ; BUILT_IN_SET: 'int'; TERM : [a-zA-Z0-9.]+ ; diff --git a/grammar/collLexer.cs b/grammar/collLexer.cs index ea7df59..dc1918b 100644 --- a/grammar/collLexer.cs +++ b/grammar/collLexer.cs @@ -94,7 +94,7 @@ static collLexer() { } } private static int[] _serializedATN = { - 4,0,14,167,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7, + 4,0,14,173,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7, 6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,1,0, 1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1, 5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,3,5,59,8,5,1,6,1,6,1,6,1,6,1, @@ -102,52 +102,54 @@ static collLexer() { 8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8, 1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,3,8,113,8,8,1,9,1, 9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9, - 1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,3,9,148,8,9,1, - 10,1,10,1,10,1,10,1,11,4,11,155,8,11,11,11,12,11,156,1,12,4,12,160,8,12, - 11,12,12,12,161,1,12,1,12,1,13,1,13,0,0,14,1,1,3,2,5,3,7,4,9,5,11,6,13, - 7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,1,0,3,4,0,46,46,48,57,65,90, - 97,122,3,0,9,10,13,13,32,32,1,0,44,44,182,0,1,1,0,0,0,0,3,1,0,0,0,0,5, - 1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0, - 0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27, - 1,0,0,0,1,29,1,0,0,0,3,31,1,0,0,0,5,33,1,0,0,0,7,35,1,0,0,0,9,37,1,0,0, - 0,11,58,1,0,0,0,13,60,1,0,0,0,15,74,1,0,0,0,17,112,1,0,0,0,19,147,1,0, - 0,0,21,149,1,0,0,0,23,154,1,0,0,0,25,159,1,0,0,0,27,165,1,0,0,0,29,30, - 5,123,0,0,30,2,1,0,0,0,31,32,5,125,0,0,32,4,1,0,0,0,33,34,5,40,0,0,34, - 6,1,0,0,0,35,36,5,41,0,0,36,8,1,0,0,0,37,38,5,124,0,0,38,10,1,0,0,0,39, - 40,5,97,0,0,40,41,5,110,0,0,41,59,5,100,0,0,42,43,5,111,0,0,43,59,5,114, - 0,0,44,45,5,110,0,0,45,46,5,97,0,0,46,47,5,110,0,0,47,59,5,100,0,0,48, - 49,5,120,0,0,49,50,5,111,0,0,50,59,5,114,0,0,51,52,5,110,0,0,52,53,5,111, - 0,0,53,59,5,114,0,0,54,55,5,120,0,0,55,56,5,110,0,0,56,57,5,111,0,0,57, - 59,5,114,0,0,58,39,1,0,0,0,58,42,1,0,0,0,58,44,1,0,0,0,58,48,1,0,0,0,58, - 51,1,0,0,0,58,54,1,0,0,0,59,12,1,0,0,0,60,61,5,110,0,0,61,62,5,111,0,0, - 62,63,5,116,0,0,63,14,1,0,0,0,64,65,5,105,0,0,65,75,5,115,0,0,66,67,5, - 105,0,0,67,75,5,110,0,0,68,69,5,115,0,0,69,70,5,117,0,0,70,71,5,98,0,0, - 71,72,5,115,0,0,72,73,5,101,0,0,73,75,5,116,0,0,74,64,1,0,0,0,74,66,1, - 0,0,0,74,68,1,0,0,0,75,16,1,0,0,0,76,77,5,117,0,0,77,78,5,110,0,0,78,79, - 5,105,0,0,79,80,5,111,0,0,80,113,5,110,0,0,81,82,5,105,0,0,82,83,5,110, - 0,0,83,84,5,116,0,0,84,85,5,101,0,0,85,86,5,114,0,0,86,87,5,115,0,0,87, - 88,5,101,0,0,88,89,5,99,0,0,89,90,5,116,0,0,90,91,5,105,0,0,91,92,5,111, - 0,0,92,113,5,110,0,0,93,94,5,99,0,0,94,95,5,114,0,0,95,96,5,111,0,0,96, - 97,5,115,0,0,97,113,5,115,0,0,98,99,5,117,0,0,99,100,5,110,0,0,100,101, - 5,111,0,0,101,102,5,114,0,0,102,103,5,100,0,0,103,104,5,101,0,0,104,105, - 5,114,0,0,105,106,5,101,0,0,106,107,5,100,0,0,107,108,5,99,0,0,108,109, - 5,114,0,0,109,110,5,111,0,0,110,111,5,115,0,0,111,113,5,115,0,0,112,76, - 1,0,0,0,112,81,1,0,0,0,112,93,1,0,0,0,112,98,1,0,0,0,113,18,1,0,0,0,114, - 115,5,115,0,0,115,116,5,101,0,0,116,148,5,116,0,0,117,118,5,109,0,0,118, - 119,5,117,0,0,119,120,5,108,0,0,120,121,5,116,0,0,121,122,5,105,0,0,122, - 123,5,115,0,0,123,124,5,101,0,0,124,148,5,116,0,0,125,126,5,108,0,0,126, - 127,5,105,0,0,127,128,5,115,0,0,128,148,5,116,0,0,129,130,5,103,0,0,130, - 131,5,114,0,0,131,132,5,97,0,0,132,133,5,112,0,0,133,148,5,104,0,0,134, - 135,5,119,0,0,135,136,5,101,0,0,136,137,5,105,0,0,137,138,5,103,0,0,138, - 139,5,104,0,0,139,140,5,116,0,0,140,141,5,101,0,0,141,142,5,100,0,0,142, - 143,5,71,0,0,143,144,5,114,0,0,144,145,5,97,0,0,145,146,5,112,0,0,146, - 148,5,104,0,0,147,114,1,0,0,0,147,117,1,0,0,0,147,125,1,0,0,0,147,129, - 1,0,0,0,147,134,1,0,0,0,148,20,1,0,0,0,149,150,5,105,0,0,150,151,5,110, - 0,0,151,152,5,116,0,0,152,22,1,0,0,0,153,155,7,0,0,0,154,153,1,0,0,0,155, - 156,1,0,0,0,156,154,1,0,0,0,156,157,1,0,0,0,157,24,1,0,0,0,158,160,7,1, - 0,0,159,158,1,0,0,0,160,161,1,0,0,0,161,159,1,0,0,0,161,162,1,0,0,0,162, - 163,1,0,0,0,163,164,6,12,0,0,164,26,1,0,0,0,165,166,7,2,0,0,166,28,1,0, - 0,0,7,0,58,74,112,147,156,161,1,6,0,0 + 1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1, + 9,1,9,1,9,3,9,154,8,9,1,10,1,10,1,10,1,10,1,11,4,11,161,8,11,11,11,12, + 11,162,1,12,4,12,166,8,12,11,12,12,12,167,1,12,1,12,1,13,1,13,0,0,14,1, + 1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,1, + 0,3,4,0,46,46,48,57,65,90,97,122,3,0,9,10,13,13,32,32,1,0,44,44,189,0, + 1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0, + 0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23, + 1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,1,29,1,0,0,0,3,31,1,0,0,0,5,33,1,0,0, + 0,7,35,1,0,0,0,9,37,1,0,0,0,11,58,1,0,0,0,13,60,1,0,0,0,15,74,1,0,0,0, + 17,112,1,0,0,0,19,153,1,0,0,0,21,155,1,0,0,0,23,160,1,0,0,0,25,165,1,0, + 0,0,27,171,1,0,0,0,29,30,5,123,0,0,30,2,1,0,0,0,31,32,5,125,0,0,32,4,1, + 0,0,0,33,34,5,40,0,0,34,6,1,0,0,0,35,36,5,41,0,0,36,8,1,0,0,0,37,38,5, + 124,0,0,38,10,1,0,0,0,39,40,5,97,0,0,40,41,5,110,0,0,41,59,5,100,0,0,42, + 43,5,111,0,0,43,59,5,114,0,0,44,45,5,110,0,0,45,46,5,97,0,0,46,47,5,110, + 0,0,47,59,5,100,0,0,48,49,5,120,0,0,49,50,5,111,0,0,50,59,5,114,0,0,51, + 52,5,110,0,0,52,53,5,111,0,0,53,59,5,114,0,0,54,55,5,120,0,0,55,56,5,110, + 0,0,56,57,5,111,0,0,57,59,5,114,0,0,58,39,1,0,0,0,58,42,1,0,0,0,58,44, + 1,0,0,0,58,48,1,0,0,0,58,51,1,0,0,0,58,54,1,0,0,0,59,12,1,0,0,0,60,61, + 5,110,0,0,61,62,5,111,0,0,62,63,5,116,0,0,63,14,1,0,0,0,64,65,5,105,0, + 0,65,75,5,115,0,0,66,67,5,105,0,0,67,75,5,110,0,0,68,69,5,115,0,0,69,70, + 5,117,0,0,70,71,5,98,0,0,71,72,5,115,0,0,72,73,5,101,0,0,73,75,5,116,0, + 0,74,64,1,0,0,0,74,66,1,0,0,0,74,68,1,0,0,0,75,16,1,0,0,0,76,77,5,117, + 0,0,77,78,5,110,0,0,78,79,5,105,0,0,79,80,5,111,0,0,80,113,5,110,0,0,81, + 82,5,105,0,0,82,83,5,110,0,0,83,84,5,116,0,0,84,85,5,101,0,0,85,86,5,114, + 0,0,86,87,5,115,0,0,87,88,5,101,0,0,88,89,5,99,0,0,89,90,5,116,0,0,90, + 91,5,105,0,0,91,92,5,111,0,0,92,113,5,110,0,0,93,94,5,99,0,0,94,95,5,114, + 0,0,95,96,5,111,0,0,96,97,5,115,0,0,97,113,5,115,0,0,98,99,5,117,0,0,99, + 100,5,110,0,0,100,101,5,111,0,0,101,102,5,114,0,0,102,103,5,100,0,0,103, + 104,5,101,0,0,104,105,5,114,0,0,105,106,5,101,0,0,106,107,5,100,0,0,107, + 108,5,99,0,0,108,109,5,114,0,0,109,110,5,111,0,0,110,111,5,115,0,0,111, + 113,5,115,0,0,112,76,1,0,0,0,112,81,1,0,0,0,112,93,1,0,0,0,112,98,1,0, + 0,0,113,18,1,0,0,0,114,115,5,115,0,0,115,116,5,101,0,0,116,154,5,116,0, + 0,117,118,5,109,0,0,118,119,5,117,0,0,119,120,5,108,0,0,120,121,5,116, + 0,0,121,122,5,105,0,0,122,123,5,115,0,0,123,124,5,101,0,0,124,154,5,116, + 0,0,125,126,5,108,0,0,126,127,5,105,0,0,127,128,5,115,0,0,128,154,5,116, + 0,0,129,130,5,103,0,0,130,131,5,114,0,0,131,132,5,97,0,0,132,133,5,112, + 0,0,133,154,5,104,0,0,134,135,5,119,0,0,135,136,5,101,0,0,136,137,5,105, + 0,0,137,138,5,103,0,0,138,139,5,104,0,0,139,140,5,116,0,0,140,141,5,101, + 0,0,141,142,5,100,0,0,142,143,5,71,0,0,143,144,5,114,0,0,144,145,5,97, + 0,0,145,146,5,112,0,0,146,154,5,104,0,0,147,148,5,115,0,0,148,149,5,116, + 0,0,149,150,5,114,0,0,150,151,5,105,0,0,151,152,5,110,0,0,152,154,5,103, + 0,0,153,114,1,0,0,0,153,117,1,0,0,0,153,125,1,0,0,0,153,129,1,0,0,0,153, + 134,1,0,0,0,153,147,1,0,0,0,154,20,1,0,0,0,155,156,5,105,0,0,156,157,5, + 110,0,0,157,158,5,116,0,0,158,22,1,0,0,0,159,161,7,0,0,0,160,159,1,0,0, + 0,161,162,1,0,0,0,162,160,1,0,0,0,162,163,1,0,0,0,163,24,1,0,0,0,164,166, + 7,1,0,0,165,164,1,0,0,0,166,167,1,0,0,0,167,165,1,0,0,0,167,168,1,0,0, + 0,168,169,1,0,0,0,169,170,6,12,0,0,170,26,1,0,0,0,171,172,7,2,0,0,172, + 28,1,0,0,0,7,0,58,74,112,153,162,167,1,6,0,0 }; public static readonly ATN _ATN = diff --git a/grammar/collLexer.interp b/grammar/collLexer.interp index aec2949..81908b3 100644 --- a/grammar/collLexer.interp +++ b/grammar/collLexer.interp @@ -56,4 +56,4 @@ mode names: DEFAULT_MODE atn: -[4, 0, 14, 167, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 59, 8, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 75, 8, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 113, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 148, 8, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 4, 11, 155, 8, 11, 11, 11, 12, 11, 156, 1, 12, 4, 12, 160, 8, 12, 11, 12, 12, 12, 161, 1, 12, 1, 12, 1, 13, 1, 13, 0, 0, 14, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 1, 0, 3, 4, 0, 46, 46, 48, 57, 65, 90, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 44, 44, 182, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 1, 29, 1, 0, 0, 0, 3, 31, 1, 0, 0, 0, 5, 33, 1, 0, 0, 0, 7, 35, 1, 0, 0, 0, 9, 37, 1, 0, 0, 0, 11, 58, 1, 0, 0, 0, 13, 60, 1, 0, 0, 0, 15, 74, 1, 0, 0, 0, 17, 112, 1, 0, 0, 0, 19, 147, 1, 0, 0, 0, 21, 149, 1, 0, 0, 0, 23, 154, 1, 0, 0, 0, 25, 159, 1, 0, 0, 0, 27, 165, 1, 0, 0, 0, 29, 30, 5, 123, 0, 0, 30, 2, 1, 0, 0, 0, 31, 32, 5, 125, 0, 0, 32, 4, 1, 0, 0, 0, 33, 34, 5, 40, 0, 0, 34, 6, 1, 0, 0, 0, 35, 36, 5, 41, 0, 0, 36, 8, 1, 0, 0, 0, 37, 38, 5, 124, 0, 0, 38, 10, 1, 0, 0, 0, 39, 40, 5, 97, 0, 0, 40, 41, 5, 110, 0, 0, 41, 59, 5, 100, 0, 0, 42, 43, 5, 111, 0, 0, 43, 59, 5, 114, 0, 0, 44, 45, 5, 110, 0, 0, 45, 46, 5, 97, 0, 0, 46, 47, 5, 110, 0, 0, 47, 59, 5, 100, 0, 0, 48, 49, 5, 120, 0, 0, 49, 50, 5, 111, 0, 0, 50, 59, 5, 114, 0, 0, 51, 52, 5, 110, 0, 0, 52, 53, 5, 111, 0, 0, 53, 59, 5, 114, 0, 0, 54, 55, 5, 120, 0, 0, 55, 56, 5, 110, 0, 0, 56, 57, 5, 111, 0, 0, 57, 59, 5, 114, 0, 0, 58, 39, 1, 0, 0, 0, 58, 42, 1, 0, 0, 0, 58, 44, 1, 0, 0, 0, 58, 48, 1, 0, 0, 0, 58, 51, 1, 0, 0, 0, 58, 54, 1, 0, 0, 0, 59, 12, 1, 0, 0, 0, 60, 61, 5, 110, 0, 0, 61, 62, 5, 111, 0, 0, 62, 63, 5, 116, 0, 0, 63, 14, 1, 0, 0, 0, 64, 65, 5, 105, 0, 0, 65, 75, 5, 115, 0, 0, 66, 67, 5, 105, 0, 0, 67, 75, 5, 110, 0, 0, 68, 69, 5, 115, 0, 0, 69, 70, 5, 117, 0, 0, 70, 71, 5, 98, 0, 0, 71, 72, 5, 115, 0, 0, 72, 73, 5, 101, 0, 0, 73, 75, 5, 116, 0, 0, 74, 64, 1, 0, 0, 0, 74, 66, 1, 0, 0, 0, 74, 68, 1, 0, 0, 0, 75, 16, 1, 0, 0, 0, 76, 77, 5, 117, 0, 0, 77, 78, 5, 110, 0, 0, 78, 79, 5, 105, 0, 0, 79, 80, 5, 111, 0, 0, 80, 113, 5, 110, 0, 0, 81, 82, 5, 105, 0, 0, 82, 83, 5, 110, 0, 0, 83, 84, 5, 116, 0, 0, 84, 85, 5, 101, 0, 0, 85, 86, 5, 114, 0, 0, 86, 87, 5, 115, 0, 0, 87, 88, 5, 101, 0, 0, 88, 89, 5, 99, 0, 0, 89, 90, 5, 116, 0, 0, 90, 91, 5, 105, 0, 0, 91, 92, 5, 111, 0, 0, 92, 113, 5, 110, 0, 0, 93, 94, 5, 99, 0, 0, 94, 95, 5, 114, 0, 0, 95, 96, 5, 111, 0, 0, 96, 97, 5, 115, 0, 0, 97, 113, 5, 115, 0, 0, 98, 99, 5, 117, 0, 0, 99, 100, 5, 110, 0, 0, 100, 101, 5, 111, 0, 0, 101, 102, 5, 114, 0, 0, 102, 103, 5, 100, 0, 0, 103, 104, 5, 101, 0, 0, 104, 105, 5, 114, 0, 0, 105, 106, 5, 101, 0, 0, 106, 107, 5, 100, 0, 0, 107, 108, 5, 99, 0, 0, 108, 109, 5, 114, 0, 0, 109, 110, 5, 111, 0, 0, 110, 111, 5, 115, 0, 0, 111, 113, 5, 115, 0, 0, 112, 76, 1, 0, 0, 0, 112, 81, 1, 0, 0, 0, 112, 93, 1, 0, 0, 0, 112, 98, 1, 0, 0, 0, 113, 18, 1, 0, 0, 0, 114, 115, 5, 115, 0, 0, 115, 116, 5, 101, 0, 0, 116, 148, 5, 116, 0, 0, 117, 118, 5, 109, 0, 0, 118, 119, 5, 117, 0, 0, 119, 120, 5, 108, 0, 0, 120, 121, 5, 116, 0, 0, 121, 122, 5, 105, 0, 0, 122, 123, 5, 115, 0, 0, 123, 124, 5, 101, 0, 0, 124, 148, 5, 116, 0, 0, 125, 126, 5, 108, 0, 0, 126, 127, 5, 105, 0, 0, 127, 128, 5, 115, 0, 0, 128, 148, 5, 116, 0, 0, 129, 130, 5, 103, 0, 0, 130, 131, 5, 114, 0, 0, 131, 132, 5, 97, 0, 0, 132, 133, 5, 112, 0, 0, 133, 148, 5, 104, 0, 0, 134, 135, 5, 119, 0, 0, 135, 136, 5, 101, 0, 0, 136, 137, 5, 105, 0, 0, 137, 138, 5, 103, 0, 0, 138, 139, 5, 104, 0, 0, 139, 140, 5, 116, 0, 0, 140, 141, 5, 101, 0, 0, 141, 142, 5, 100, 0, 0, 142, 143, 5, 71, 0, 0, 143, 144, 5, 114, 0, 0, 144, 145, 5, 97, 0, 0, 145, 146, 5, 112, 0, 0, 146, 148, 5, 104, 0, 0, 147, 114, 1, 0, 0, 0, 147, 117, 1, 0, 0, 0, 147, 125, 1, 0, 0, 0, 147, 129, 1, 0, 0, 0, 147, 134, 1, 0, 0, 0, 148, 20, 1, 0, 0, 0, 149, 150, 5, 105, 0, 0, 150, 151, 5, 110, 0, 0, 151, 152, 5, 116, 0, 0, 152, 22, 1, 0, 0, 0, 153, 155, 7, 0, 0, 0, 154, 153, 1, 0, 0, 0, 155, 156, 1, 0, 0, 0, 156, 154, 1, 0, 0, 0, 156, 157, 1, 0, 0, 0, 157, 24, 1, 0, 0, 0, 158, 160, 7, 1, 0, 0, 159, 158, 1, 0, 0, 0, 160, 161, 1, 0, 0, 0, 161, 159, 1, 0, 0, 0, 161, 162, 1, 0, 0, 0, 162, 163, 1, 0, 0, 0, 163, 164, 6, 12, 0, 0, 164, 26, 1, 0, 0, 0, 165, 166, 7, 2, 0, 0, 166, 28, 1, 0, 0, 0, 7, 0, 58, 74, 112, 147, 156, 161, 1, 6, 0, 0] \ No newline at end of file +[4, 0, 14, 173, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 59, 8, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 75, 8, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 113, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 154, 8, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 4, 11, 161, 8, 11, 11, 11, 12, 11, 162, 1, 12, 4, 12, 166, 8, 12, 11, 12, 12, 12, 167, 1, 12, 1, 12, 1, 13, 1, 13, 0, 0, 14, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 1, 0, 3, 4, 0, 46, 46, 48, 57, 65, 90, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 44, 44, 189, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 1, 29, 1, 0, 0, 0, 3, 31, 1, 0, 0, 0, 5, 33, 1, 0, 0, 0, 7, 35, 1, 0, 0, 0, 9, 37, 1, 0, 0, 0, 11, 58, 1, 0, 0, 0, 13, 60, 1, 0, 0, 0, 15, 74, 1, 0, 0, 0, 17, 112, 1, 0, 0, 0, 19, 153, 1, 0, 0, 0, 21, 155, 1, 0, 0, 0, 23, 160, 1, 0, 0, 0, 25, 165, 1, 0, 0, 0, 27, 171, 1, 0, 0, 0, 29, 30, 5, 123, 0, 0, 30, 2, 1, 0, 0, 0, 31, 32, 5, 125, 0, 0, 32, 4, 1, 0, 0, 0, 33, 34, 5, 40, 0, 0, 34, 6, 1, 0, 0, 0, 35, 36, 5, 41, 0, 0, 36, 8, 1, 0, 0, 0, 37, 38, 5, 124, 0, 0, 38, 10, 1, 0, 0, 0, 39, 40, 5, 97, 0, 0, 40, 41, 5, 110, 0, 0, 41, 59, 5, 100, 0, 0, 42, 43, 5, 111, 0, 0, 43, 59, 5, 114, 0, 0, 44, 45, 5, 110, 0, 0, 45, 46, 5, 97, 0, 0, 46, 47, 5, 110, 0, 0, 47, 59, 5, 100, 0, 0, 48, 49, 5, 120, 0, 0, 49, 50, 5, 111, 0, 0, 50, 59, 5, 114, 0, 0, 51, 52, 5, 110, 0, 0, 52, 53, 5, 111, 0, 0, 53, 59, 5, 114, 0, 0, 54, 55, 5, 120, 0, 0, 55, 56, 5, 110, 0, 0, 56, 57, 5, 111, 0, 0, 57, 59, 5, 114, 0, 0, 58, 39, 1, 0, 0, 0, 58, 42, 1, 0, 0, 0, 58, 44, 1, 0, 0, 0, 58, 48, 1, 0, 0, 0, 58, 51, 1, 0, 0, 0, 58, 54, 1, 0, 0, 0, 59, 12, 1, 0, 0, 0, 60, 61, 5, 110, 0, 0, 61, 62, 5, 111, 0, 0, 62, 63, 5, 116, 0, 0, 63, 14, 1, 0, 0, 0, 64, 65, 5, 105, 0, 0, 65, 75, 5, 115, 0, 0, 66, 67, 5, 105, 0, 0, 67, 75, 5, 110, 0, 0, 68, 69, 5, 115, 0, 0, 69, 70, 5, 117, 0, 0, 70, 71, 5, 98, 0, 0, 71, 72, 5, 115, 0, 0, 72, 73, 5, 101, 0, 0, 73, 75, 5, 116, 0, 0, 74, 64, 1, 0, 0, 0, 74, 66, 1, 0, 0, 0, 74, 68, 1, 0, 0, 0, 75, 16, 1, 0, 0, 0, 76, 77, 5, 117, 0, 0, 77, 78, 5, 110, 0, 0, 78, 79, 5, 105, 0, 0, 79, 80, 5, 111, 0, 0, 80, 113, 5, 110, 0, 0, 81, 82, 5, 105, 0, 0, 82, 83, 5, 110, 0, 0, 83, 84, 5, 116, 0, 0, 84, 85, 5, 101, 0, 0, 85, 86, 5, 114, 0, 0, 86, 87, 5, 115, 0, 0, 87, 88, 5, 101, 0, 0, 88, 89, 5, 99, 0, 0, 89, 90, 5, 116, 0, 0, 90, 91, 5, 105, 0, 0, 91, 92, 5, 111, 0, 0, 92, 113, 5, 110, 0, 0, 93, 94, 5, 99, 0, 0, 94, 95, 5, 114, 0, 0, 95, 96, 5, 111, 0, 0, 96, 97, 5, 115, 0, 0, 97, 113, 5, 115, 0, 0, 98, 99, 5, 117, 0, 0, 99, 100, 5, 110, 0, 0, 100, 101, 5, 111, 0, 0, 101, 102, 5, 114, 0, 0, 102, 103, 5, 100, 0, 0, 103, 104, 5, 101, 0, 0, 104, 105, 5, 114, 0, 0, 105, 106, 5, 101, 0, 0, 106, 107, 5, 100, 0, 0, 107, 108, 5, 99, 0, 0, 108, 109, 5, 114, 0, 0, 109, 110, 5, 111, 0, 0, 110, 111, 5, 115, 0, 0, 111, 113, 5, 115, 0, 0, 112, 76, 1, 0, 0, 0, 112, 81, 1, 0, 0, 0, 112, 93, 1, 0, 0, 0, 112, 98, 1, 0, 0, 0, 113, 18, 1, 0, 0, 0, 114, 115, 5, 115, 0, 0, 115, 116, 5, 101, 0, 0, 116, 154, 5, 116, 0, 0, 117, 118, 5, 109, 0, 0, 118, 119, 5, 117, 0, 0, 119, 120, 5, 108, 0, 0, 120, 121, 5, 116, 0, 0, 121, 122, 5, 105, 0, 0, 122, 123, 5, 115, 0, 0, 123, 124, 5, 101, 0, 0, 124, 154, 5, 116, 0, 0, 125, 126, 5, 108, 0, 0, 126, 127, 5, 105, 0, 0, 127, 128, 5, 115, 0, 0, 128, 154, 5, 116, 0, 0, 129, 130, 5, 103, 0, 0, 130, 131, 5, 114, 0, 0, 131, 132, 5, 97, 0, 0, 132, 133, 5, 112, 0, 0, 133, 154, 5, 104, 0, 0, 134, 135, 5, 119, 0, 0, 135, 136, 5, 101, 0, 0, 136, 137, 5, 105, 0, 0, 137, 138, 5, 103, 0, 0, 138, 139, 5, 104, 0, 0, 139, 140, 5, 116, 0, 0, 140, 141, 5, 101, 0, 0, 141, 142, 5, 100, 0, 0, 142, 143, 5, 71, 0, 0, 143, 144, 5, 114, 0, 0, 144, 145, 5, 97, 0, 0, 145, 146, 5, 112, 0, 0, 146, 154, 5, 104, 0, 0, 147, 148, 5, 115, 0, 0, 148, 149, 5, 116, 0, 0, 149, 150, 5, 114, 0, 0, 150, 151, 5, 105, 0, 0, 151, 152, 5, 110, 0, 0, 152, 154, 5, 103, 0, 0, 153, 114, 1, 0, 0, 0, 153, 117, 1, 0, 0, 0, 153, 125, 1, 0, 0, 0, 153, 129, 1, 0, 0, 0, 153, 134, 1, 0, 0, 0, 153, 147, 1, 0, 0, 0, 154, 20, 1, 0, 0, 0, 155, 156, 5, 105, 0, 0, 156, 157, 5, 110, 0, 0, 157, 158, 5, 116, 0, 0, 158, 22, 1, 0, 0, 0, 159, 161, 7, 0, 0, 0, 160, 159, 1, 0, 0, 0, 161, 162, 1, 0, 0, 0, 162, 160, 1, 0, 0, 0, 162, 163, 1, 0, 0, 0, 163, 24, 1, 0, 0, 0, 164, 166, 7, 1, 0, 0, 165, 164, 1, 0, 0, 0, 166, 167, 1, 0, 0, 0, 167, 165, 1, 0, 0, 0, 167, 168, 1, 0, 0, 0, 168, 169, 1, 0, 0, 0, 169, 170, 6, 12, 0, 0, 170, 26, 1, 0, 0, 0, 171, 172, 7, 2, 0, 0, 172, 28, 1, 0, 0, 0, 7, 0, 58, 74, 112, 153, 162, 167, 1, 6, 0, 0] \ No newline at end of file diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 427fc33..2beac42 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -85,6 +85,13 @@ public void check(Dictionary map) throw Failure($"{Describe(map[term])} is not an integer", map[term]); } break; + case "string": + // Any single value is a string, a number-looking one like 5 included. + if (!map[term].IsValue()) + { + throw Failure($"{Describe(map[term])} is not a string", map[term]); + } + break; } break; diff --git a/tests/SPADE.Tests/DefinitionDescriptionTests.cs b/tests/SPADE.Tests/DefinitionDescriptionTests.cs index 7c04c5e..a87a107 100644 --- a/tests/SPADE.Tests/DefinitionDescriptionTests.cs +++ b/tests/SPADE.Tests/DefinitionDescriptionTests.cs @@ -119,16 +119,29 @@ public void MembersOfOtherPartsAndExpressions() [Fact] public void UnconstrainedAndUnknownTypesAreAny() { - DefinitionDescription d = Describe("{(A,B,C) | B is string, C is graph}"); + DefinitionDescription d = Describe("{(A,B,C) | B is text, C is graph}"); Assert.Equal(PartKind.Any, d["A"].Kind); Assert.Empty(d["A"].Constraints); Assert.Equal(PartKind.Any, d["B"].Kind); Assert.Null(d["B"].Of); - Assert.Equal(new[] { "B is string" }, d["B"].Constraints); + Assert.Equal(new[] { "B is text" }, d["B"].Constraints); Assert.Equal(PartKind.Any, d["C"].Kind); } + [Fact] + public void IsStringIsTheStringKind() + { + DefinitionDescription d = Describe("{(x,y,S) | x is string, y is string, S is set, S is string}"); + + Assert.Equal(PartKind.String, d["x"].Kind); + Assert.Null(d["x"].Of); + Assert.Equal(new[] { "x is string" }, d["x"].Constraints); + Assert.Equal(PartKind.String, d["y"].Kind); + // The first "is" type wins, as for the other types. + Assert.Equal(PartKind.Set, d["S"].Kind); + } + [Fact] public void CompoundConstraintsAreListedButDontSetKind() { diff --git a/tests/SPADE.Tests/TypeConstraintTests.cs b/tests/SPADE.Tests/TypeConstraintTests.cs index f8857e0..701e9d7 100644 --- a/tests/SPADE.Tests/TypeConstraintTests.cs +++ b/tests/SPADE.Tests/TypeConstraintTests.cs @@ -84,4 +84,59 @@ public void SubsetIntStillChecksEveryElement() Parse("{S | S subset int}", "{1,2,3}"); Assert.Throws(() => Parse("{S | S subset int}", "{1,2,x}")); } + + // "is string" accepts any single value and rejects a collection (#22). + [Theory] + [InlineData("kitten", "kitten")] + [InlineData("New York", "New York")] + [InlineData(" New York ", "New York")] + [InlineData("5", "5")] + [InlineData("q0", "q0")] + public void IsStringAcceptsSingleValues(string instance, string value) + { + StringParser parser = new("{S | S is string}"); + parser.parse(instance); + Assert.True(parser["S"].IsValue()); + Assert.Equal(value, parser["S"].ToString()); + } + + [Theory] + [InlineData("{S | S is string}", "{a,b}", "{a,b}")] + [InlineData("{S | S is string}", "{}", "{}")] + [InlineData("{S | S is string}", "(a,b)", "(a,b)")] + [InlineData("{S | S is string}", "()", "()")] + [InlineData("{S | S is multiset, S is string}", "{a,a}", "{a,a}")] + public void IsStringRejectsCollections(string grammar, string instance, string element) + { + SpadeParseException e = Assert.Throws(() => Parse(grammar, instance)); + Assert.Equal($"Constraint S is string failed: {element} is not a string", e.Message); + Assert.Equal("S", e.Part); + Assert.Equal(element, e.Element); + Assert.Null(e.MissingValue); + } + + [Fact] + public void IsStringChecksNestedFields() + { + string grammar = "{(x,y) | x is string, y is string}"; + Parse(grammar, "(kitten,sitting)"); + SpadeParseException e = Assert.Throws(() => Parse(grammar, "(kitten,{sitting})")); + Assert.Equal("y", e.Part); + } + + [Fact] + public void IsStringInADfaDefinition() + { + string grammar = "{((N,A,E,S,F),I) | N is set, A is set, E is N cross A cross N, S is string, F is set, I is string}"; + StringParser parser = new(grammar); + parser.parse("(({q0,q1},{0,1},{(q0,0,q1),(q1,1,q0)},q0,{q1}),0110)"); + Assert.Equal("q0", parser["S"].ToString()); + Assert.Equal("0110", parser["I"].ToString()); + Assert.Equal(PartKind.String, parser.Definition["S"].Kind); + Assert.Equal(PartKind.String, parser.Definition["I"].Kind); + + SpadeParseException e = Assert.Throws(() => Parse(grammar, "(({q0,q1},{0,1},{(q0,0,q1)},{q0},{q1}),0110)")); + Assert.Equal("Constraint S is string failed: {q0} is not a string", e.Message); + Assert.Equal("S", e.Part); + } } From e427582f0522567436154848cf293627e25e5635 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:59:38 -0600 Subject: [PATCH 11/35] feat(parser): allow quoted values that contain , { } ( ) --- Documentation/DefinitionString.md | 2 +- Documentation/InstanceString.md | 9 ++ InstanceScanner.cs | 54 ++++++++++- UtilCollection.cs | 9 +- tests/SPADE.Tests/QuotedValueTests.cs | 125 ++++++++++++++++++++++++++ 5 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 tests/SPADE.Tests/QuotedValueTests.cs diff --git a/Documentation/DefinitionString.md b/Documentation/DefinitionString.md index 8717c7d..fe79855 100644 --- a/Documentation/DefinitionString.md +++ b/Documentation/DefinitionString.md @@ -16,7 +16,7 @@ After the symbol "|" is a list of assertions or *constraints* which must be true ### Definition - "is" checks the type of a collection. It can be followed by set, list, int or string, and constrains that collection to be that type. An example is "U is set" which constrains U to be a set - "is multiset" marks a collection written with curly braces as a multiset, so repeated elements are kept instead of dropped. For example, "{N | N is multiset, N subset int}" reads {3,3,2,2} as four numbers. It applies before the other constraints, wherever it is written. See [multisets](InstanceString.md#multisets). -- "is string" checks that a collection is a single value, not a set, list or multiset. Any value counts, a word like `kitten`, words with spaces like `New York`, or a number like `5`. For example, "{(x,y) | x is string, y is string}" accepts (kitten,sitting) and rejects (kitten,{sitting}). Since `string` is a keyword, it can't be used as a name in the format string or a constraint. +- "is string" checks that a collection is a single value, not a set, list or multiset. Any value counts, a word like `kitten`, words with spaces like `New York`, or a number like `5`. For example, "{(x,y) | x is string, y is string}" accepts (kitten,sitting) and rejects (kitten,{sitting}). A value with commas or brackets in it can be written in quotes, like "hello, world"; see [quoted values](InstanceString.md#quoted-values). Since `string` is a keyword, it can't be used as a name in the format string or a constraint. - "subset" checks that the collection is a subset of the object on the right. An example is "A subset B" which constrains A to be a subset of B. ### Objects diff --git a/Documentation/InstanceString.md b/Documentation/InstanceString.md index 9003652..c759ee0 100644 --- a/Documentation/InstanceString.md +++ b/Documentation/InstanceString.md @@ -29,6 +29,15 @@ reads the same as `{(1,2),(2,3)}`. The rules are: - Inside a value, whitespace next to `[ ] ; : =` is dropped, so `x1 : True` reads as `x1:True`. - Other whitespace inside a value is kept. Plain spaces stay as typed, so `{New York, Los Angeles}` holds `New York`. A run containing a tab or line break becomes a single space, so a value wrapped onto two lines, `New` and `York`, reads as `New York`. +## Quoted values +A value can be written in double quotes, so it can hold text that would otherwise end it: `"hello, world"` is one value, and `{"a,b", c}` is a set of two. Inside quotes, everything is kept exactly as written, including `{ } ( ) ,` and all whitespace. Two escapes exist inside quotes: `\"` for a quote and `\\` for a backslash. Any other backslash in quotes is an error, as is a missing closing quote (`expected closing " at position 5, found end of input` for `"abc`). + +The quotes aren't part of the value. `"kitten"` and `kitten` are the same value, so `"5"` passes `is int`, and a quoted value passes `is string`. + +A quote can only start a value, so `ab"c"` is an error. Outside quotes a backslash is an ordinary character. + +When a set or list is turned back into text, a value inside it is quoted only if it has to be: it contains `{ } ( ) , "`, is empty, or has whitespace that the rules above would change. Every other value is written as before, so `{"a,b",c}` prints as `{"a,b",c}` and `{"a","b"}` prints as `{a,b}`. A value on its own prints as its text, without quotes. + ## Errors An instance string that can't be read throws `SpadeParseException`, which says where the problem is, what was expected and what was found: diff --git a/InstanceScanner.cs b/InstanceScanner.cs index beb8082..70a8a4f 100644 --- a/InstanceScanner.cs +++ b/InstanceScanner.cs @@ -16,6 +16,11 @@ namespace SPADE; /// inside a value is a wrap (New\nYork reads as New York). /// /// These match the rules Redux applied before parsing, so that layer can be removed. +/// +/// Quoted values (#22): a value written in double quotes, "hello, world", is kept exactly as +/// written and may contain { } ( ) , and any whitespace. Inside quotes, \" is a quote and +/// \\ a backslash; no other escape exists. A quote can't appear inside an unquoted value. +/// /// internal class InstanceScanner { @@ -28,6 +33,10 @@ internal class InstanceScanner // Characters that end a value. private const string Structural = "{}(),"; + // Characters that end an unquoted value: the structural ones, and a quote, which may only + // start a value (#22). + private const string ValueEnd = "{}(),\""; + // Characters inside a value that whitespace never belongs next to. private const string ValueDelimiters = "[];:="; @@ -61,6 +70,7 @@ private UtilCollection ParseElement(int depth) char c = text[pos]; if (c == '{' || c == '(') return ParseCollection(depth + 1); if (Structural.IndexOf(c) >= 0) throw Error("a value, { or ("); + if (c == '"') return ParseQuotedValue(); return ParseValue(); } @@ -99,12 +109,50 @@ private UtilCollection ParseCollection(int depth) private UtilCollection ParseValue() { int start = pos; - while (!AtEnd() && Structural.IndexOf(text[pos]) < 0) pos++; - return UtilCollection.FromValue(NormalizeValue(start, pos)); + while (!AtEnd() && ValueEnd.IndexOf(text[pos]) < 0) pos++; + return UtilCollection.FromValue(NormalizeValue(text, start, pos)); + } + + // Expects pos at the opening quote. The value is the text up to the closing quote, unescaped. + private UtilCollection ParseQuotedValue() + { + pos++; + StringBuilder sb = new(); + while (true) + { + if (AtEnd()) throw Error("closing \""); + char c = text[pos++]; + if (c == '"') break; + if (c == '\\') + { + if (AtEnd() || (text[pos] != '"' && text[pos] != '\\')) throw Error("\\\" or \\\\ after \\"); + c = text[pos++]; + } + sb.Append(c); + } + return UtilCollection.FromValue(sb.ToString()); + } + + /// + /// Whether a value must be written in quotes to be read back as itself: it is empty, contains + /// { } ( ) , ", or has whitespace the unquoted rules would change. + /// + internal static bool NeedsQuotes(string value) + { + if (value.Length == 0) return true; + if (value.IndexOfAny(ValueEnd.ToCharArray()) >= 0) return true; + if (char.IsWhiteSpace(value[0])) return true; + return NormalizeValue(value, 0, value.Length) != value; + } + + /// The value in quotes, with " and \ escaped as \" and \\. + internal static string Quote(string value) + { + return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; } // Applies the whitespace rules to text[start..end). text[start] is never whitespace. - private string NormalizeValue(int start, int end) + private static string NormalizeValue(string text, int start, int end) { StringBuilder sb = new(end - start); int i = start; diff --git a/UtilCollection.cs b/UtilCollection.cs index 927fe77..6b47c94 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -406,6 +406,11 @@ public List ToList() } } + /// + /// A set or list as an instance string that reads back as the same collection. A value inside it + /// is quoted when it has to be, such as {"a,b",c}. A value on its own is its text, unquoted, + /// so "hello, world" read alone gives hello, world. + /// override public string ToString() { if (isValue) @@ -423,7 +428,9 @@ private void AppendTo(StringBuilder sb) { if (isValue) { - sb.Append(value); + // Inside a collection, a value that couldn't be read back as written, such as + // "hello, world", is quoted (#22). Every other value is written as it is. + sb.Append(InstanceScanner.NeedsQuotes(value!) ? InstanceScanner.Quote(value!) : value); return; } diff --git a/tests/SPADE.Tests/QuotedValueTests.cs b/tests/SPADE.Tests/QuotedValueTests.cs new file mode 100644 index 0000000..0657c57 --- /dev/null +++ b/tests/SPADE.Tests/QuotedValueTests.cs @@ -0,0 +1,125 @@ +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// A value in double quotes may hold , { } ( ) and any whitespace, exactly as written (#22). +public class QuotedValueTests +{ + [Theory] + [InlineData("\"hello, world\"", "hello, world")] + [InlineData("\"{a} (b)\"", "{a} (b)")] + [InlineData("\" two spaces \t tab \"", " two spaces \t tab ")] + [InlineData("\"x : y\"", "x : y")] + [InlineData("\"\"", "")] + [InlineData(" \"padded\" ", "padded")] + [InlineData("\"say \\\"hi\\\"\"", "say \"hi\"")] + [InlineData("\"C:\\\\temp\"", "C:\\temp")] + public void QuotedValueKeepsItsTextExactly(string instance, string value) + { + UtilCollection c = new(instance); + Assert.True(c.IsValue()); + Assert.Equal(value, c.ToString()); + } + + [Fact] + public void QuotedValueInASetIsOneElement() + { + UtilCollection c = new("{\"a,b\", c}"); + Assert.Equal(2, c.Count()); + Assert.True(c.Contains(new UtilCollection("\"a,b\""))); + Assert.True(c.Contains(new UtilCollection("c"))); + } + + [Fact] + public void QuotingAPlainValueChangesNothing() + { + Assert.Equal(new UtilCollection("{a,b}"), new UtilCollection("{\"a\", \"b\"}")); + Assert.Equal(new UtilCollection("{a,b}").GetHashCode(), new UtilCollection("{\"b\",a}").GetHashCode()); + Assert.Equal("{a,b}", new UtilCollection("{\"a\",\"b\"}").ToString()); + } + + [Fact] + public void SetsWithQuotedValuesCompareByValue() + { + UtilCollection x = new("{\"a,b\",\"c d\"}"); + UtilCollection y = new("{\"c d\", \"a,b\"}"); + Assert.Equal(x, y); + Assert.Equal(x.GetHashCode(), y.GetHashCode()); + Assert.NotEqual(x, new UtilCollection("{a,b,c d}")); + } + + [Theory] + [InlineData("{\"a,b\",c}", "{\"a,b\",c}")] + // A backslash means nothing outside quotes, so C:\temp needs none. + [InlineData("(\"say \\\"hi\\\"\", \"C:\\\\temp\")", "(\"say \\\"hi\\\"\",C:\\temp)")] + [InlineData("(\"a\\\\\\\"b, c\")", "(\"a\\\\\\\"b, c\")")] + [InlineData("(\" lead\",\"trail \",\"a\tb\",\"x : y\",\"\")", "(\" lead\",\"trail \",\"a\tb\",\"x : y\",\"\")")] + [InlineData("({\"{1}\"},(\"(2)\"))", "({\"{1}\"},(\"(2)\"))")] + public void ToStringQuotesOnlyWhatNeedsItAndReadsBack(string instance, string written) + { + UtilCollection c = new(instance); + Assert.Equal(written, c.ToString()); + Assert.Equal(c, new UtilCollection(c.ToString())); + } + + [Theory] + [InlineData("({New York, x1:True, a[1]}, (1,2))")] + [InlineData("{C:\\temp, 5, -3.5}")] + public void ValuesThatDontNeedQuotesPrintAsBefore(string instance) + { + UtilCollection c = new(instance); + Assert.DoesNotContain("\"", c.ToString()); + Assert.Equal(c, new UtilCollection(c.ToString())); + } + + [Theory] + [InlineData("\"abc", 5, "closing \"", "end of input")] + [InlineData("{\"a,b}", 7, "closing \"", "end of input")] + [InlineData("\"a\\nb\"", 4, "\\\" or \\\\ after \\", "n")] + [InlineData("\"a\\", 4, "\\\" or \\\\ after \\", "end of input")] + public void BadQuotesAreSyntaxErrors(string instance, int position, string expected, string found) + { + SpadeParseException e = Assert.Throws(() => new UtilCollection(instance)); + Assert.Equal(position, e.Position); + Assert.Equal(expected, e.Expected); + Assert.Equal(found, e.Found); + } + + [Theory] + [InlineData("{ab\"c\"}", 4, ", or }")] + [InlineData("{\"a\"b}", 5, ", or }")] + [InlineData("ab\"c\"", 3, "end of input")] + public void AQuoteCantBePartOfAnUnquotedValue(string instance, int position, string expected) + { + SpadeParseException e = Assert.Throws(() => new UtilCollection(instance)); + Assert.Equal(position, e.Position); + Assert.Equal(expected, e.Expected); + } + + [Fact] + public void QuotedValuesPassIsString() + { + StringParser parser = new("{(x,y) | x is string, y is string}"); + parser.parse("(\"hello, world\", \"{not a set}\")"); + Assert.Equal("hello, world", parser["x"].ToString()); + Assert.Equal("{not a set}", parser["y"].ToString()); + } + + [Fact] + public void QuotedNumberIsStillAnInt() + { + StringParser parser = new("{(K,S) | K is int, S subset int}"); + parser.parse("(\"5\", {\"1\", 2})"); + Assert.Equal(5, parser["K"].parseInt()); + Assert.Equal(2, parser["S"].Count()); + } + + [Fact] + public void QuotedValuesAreMembersByValue() + { + StringParser parser = new("{(N,E) | N is set, E subset N cross N}"); + parser.parse("({\"New York, NY\", Boston}, {(\"New York, NY\", Boston)})"); + Assert.Equal(2, parser["N"].Count()); + } +} From 2e89bc1a09083b578c30ca50a6ee17bdab75c2a6 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:05:02 -0600 Subject: [PATCH 12/35] feat(grammar): add a number type for decimal values --- DefinitionDescription.cs | 9 +- Documentation/DefinitionString.md | 3 +- Documentation/example.md | 2 +- grammar/BuiltInSet.cs | 22 ++- grammar/codeGen.cs | 7 +- grammar/coll.g4 | 2 +- grammar/coll.interp | 2 +- grammar/coll.tokens | 1 - grammar/collLexer.cs | 102 +++++----- grammar/collLexer.interp | 4 +- grammar/collLexer.tokens | 1 - grammar/collParser.cs | 3 +- grammar/desciptors/ConstraintDescriptor.cs | 7 + grammar/desciptors/SetDescriptor.cs | 9 +- tests/SPADE.Tests/NumberTests.cs | 214 +++++++++++++++++++++ 15 files changed, 320 insertions(+), 68 deletions(-) create mode 100644 tests/SPADE.Tests/NumberTests.cs diff --git a/DefinitionDescription.cs b/DefinitionDescription.cs index 100dac9..a66bd13 100644 --- a/DefinitionDescription.cs +++ b/DefinitionDescription.cs @@ -25,6 +25,9 @@ public enum PartKind /// "is int". Int, + /// "is number": a decimal number such as 2.5, -0.75 or .5; every int is one. + Number, + /// "is string": a single value, not a set, list or multiset. String, @@ -43,7 +46,7 @@ public class DefinitionPart /// The name as written in the format, e.g. "E". public string Name { get; } - /// What the part's constraints say it is. An "is set", "is multiset", "is list", "is int" or "is string" constraint wins over "subset" or "in". + /// What the part's constraints say it is. An "is set", "is multiset", "is list", "is int", "is number" or "is string" constraint wins over "subset" or "in". public PartKind Kind { get; } /// @@ -218,7 +221,7 @@ private static void Subjects(collParser.ConstraintContext constraint, HashSet constraints, HashSet names) { PartKind? type = null; @@ -254,7 +257,7 @@ private static (PartKind, string?) Classify(List c } if (obj.BUILT_IN_SET() != null) { - type ??= PartKind.Int; + type ??= objText == "number" ? PartKind.Number : PartKind.Int; continue; } } diff --git a/Documentation/DefinitionString.md b/Documentation/DefinitionString.md index fe79855..1e8399a 100644 --- a/Documentation/DefinitionString.md +++ b/Documentation/DefinitionString.md @@ -14,8 +14,9 @@ In the definition string, first there is a *format string*, which is the part to After the symbol "|" is a list of assertions or *constraints* which must be true of the instance, separated by commas that are assumed to be true for a valid problem instance. The language used for defining constraints in SPADE is modeled after the language of Set theory. Each constraint says that some named element is either a basic object, or related to the other objects in some way. A constraint is a terminal (one of the named collections from the format string) followed a defintion and then an object. ### Definition -- "is" checks the type of a collection. It can be followed by set, list, int or string, and constrains that collection to be that type. An example is "U is set" which constrains U to be a set +- "is" checks the type of a collection. It can be followed by set, list, int, number or string, and constrains that collection to be that type. An example is "U is set" which constrains U to be a set - "is multiset" marks a collection written with curly braces as a multiset, so repeated elements are kept instead of dropped. For example, "{N | N is multiset, N subset int}" reads {3,3,2,2} as four numbers. It applies before the other constraints, wherever it is written. See [multisets](InstanceString.md#multisets). +- "is number" checks that a collection is a single decimal number, such as a coordinate or a weight. It accepts an optional `+` or `-` sign, then digits with an optional fractional part, or a fraction with a leading dot: `5`, `-3`, `+7`, `2.5`, `-0.75`, `.5` and `-.5` are all numbers. It rejects exponents (`1e3`, `2E-4`), `NaN` and infinities, a trailing dot (`5.`), more than one dot (`1.2.3`), a sign or dot on its own, thousands separators (`"1,000"`), whitespace inside the number, and anything too large to hold as a double. The decimal point is always `.`, whatever language the machine is set to, so `"2,5"` is not a number. Every value `int` accepts is also a `number`, and a quoted value like `"2.5"` counts the same as `2.5`. Like `int`, `number` is also a set: "S subset number" and "P subset number cross number" (points with real coordinates, as in Convex Hull) both work, as does "P subset {(x,y) | x is number, y is number}". Since `number` is a keyword, it can't be used as a name in the format string or a constraint. - "is string" checks that a collection is a single value, not a set, list or multiset. Any value counts, a word like `kitten`, words with spaces like `New York`, or a number like `5`. For example, "{(x,y) | x is string, y is string}" accepts (kitten,sitting) and rejects (kitten,{sitting}). A value with commas or brackets in it can be written in quotes, like "hello, world"; see [quoted values](InstanceString.md#quoted-values). Since `string` is a keyword, it can't be used as a name in the format string or a constraint. - "subset" checks that the collection is a subset of the object on the right. An example is "A subset B" which constrains A to be a subset of B. diff --git a/Documentation/example.md b/Documentation/example.md index d29fcac..cd7db56 100644 --- a/Documentation/example.md +++ b/Documentation/example.md @@ -86,6 +86,6 @@ foreach (DefinitionPart part in d.Parts) Console.WriteLine(d["E"].Constraints[0]); // E subset N unorderedcross N ``` -Each part has a `Name`, a `Kind` (`Set`, `Multiset`, `List`, `Int`, `String`, `Subset`, `Member`, or `Any` when nothing says what it is), `Of` (what it is a subset or member of, as written), `Path` (where it sits in the outline), and `Constraints` (every constraint naming it, as written). Only the outer format's names are parts; names inside a nested set, like `a` in `S subset {a | a subset U}`, are not. +Each part has a `Name`, a `Kind` (`Set`, `Multiset`, `List`, `Int`, `Number`, `String`, `Subset`, `Member`, or `Any` when nothing says what it is), `Of` (what it is a subset or member of, as written), `Path` (where it sits in the outline), and `Constraints` (every constraint naming it, as written). Only the outer format's names are parts; names inside a nested set, like `a` in `S subset {a | a subset U}`, are not. `Definition` is read the first time you use it, so creating a `StringParser` never throws. If the definition string can't be read, `Definition` throws a `FormatException` naming it. `DefinitionDescription.Parse(definitionString)` does the same without a `StringParser`. diff --git a/grammar/BuiltInSet.cs b/grammar/BuiltInSet.cs index 948aba7..9d5953b 100644 --- a/grammar/BuiltInSet.cs +++ b/grammar/BuiltInSet.cs @@ -1,2 +1,22 @@ +using System.Globalization; +using System.Text.RegularExpressions; + namespace SPADE.Grammar.Enums; -enum BuiltInSet {integers} \ No newline at end of file +enum BuiltInSet {integers, numbers} + +// What "number" accepts, shared by "x is number" and "S subset number" (#23). +static class NumberValue +{ + // An optional sign, then digits with an optional fractional part, or a leading-dot fraction: + // 5, -3, +7, 2.5, -0.75, .5, -.5. No exponent, no trailing dot, no separators or whitespace. + // [0-9] rather than \d, which also matches non-ASCII digits; \z rather than $, which allows a trailing newline. + private static readonly Regex Pattern = new(@"^[+-]?([0-9]+(\.[0-9]+)?|\.[0-9]+)\z", RegexOptions.CultureInvariant); + + /// Whether a single value's text is a finite decimal number, read the same on every machine. + public static bool IsNumber(string text) + { + return Pattern.IsMatch(text) + && double.TryParse(text, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out double value) + && double.IsFinite(value); + } +} diff --git a/grammar/codeGen.cs b/grammar/codeGen.cs index 9aaa8f0..11ba11f 100644 --- a/grammar/codeGen.cs +++ b/grammar/codeGen.cs @@ -133,8 +133,8 @@ public void ExitConstraint([NotNull] collParser.ConstraintContext context) } else if (context.@object().BUILT_IN_SET() != null && context.DEFINITION().ToString() == "is") { - // "N is int" is a type check, like "N is set". The integer set descriptor - // pushed for it is only needed by other definitions, such as "S subset int". + // "N is int" and "N is number" are type checks, like "N is set". The set descriptor + // pushed for them is only needed by other definitions, such as "S subset int". setDescriptorStack.Pop(); constraintDescriptorStack.Push(new ConstraintDescriptor(context.TERM().ToString()!, context.DEFINITION().ToString()!, context.@object().BUILT_IN_SET().ToString()!, SourceText(context))); } @@ -163,7 +163,8 @@ public void ExitObject([NotNull] collParser.ObjectContext context) if (context.BUILT_IN_SET() != null) { - setDescriptorStack.Push(new SetDescriptor(map, BuiltInSet.integers)); + BuiltInSet builtInSet = context.BUILT_IN_SET().GetText() == "number" ? BuiltInSet.numbers : BuiltInSet.integers; + setDescriptorStack.Push(new SetDescriptor(map, builtInSet)); } if (context.TERM() != null) diff --git a/grammar/coll.g4 b/grammar/coll.g4 index 0f9ec17..2d241bf 100644 --- a/grammar/coll.g4 +++ b/grammar/coll.g4 @@ -47,7 +47,7 @@ BUILT_IN : 'set' | 'weightedGraph' | 'string' ; -BUILT_IN_SET: 'int'; +BUILT_IN_SET: 'int' | 'number'; TERM : [a-zA-Z0-9.]+ ; WS : [ \t\r\n]+ -> skip ; CM : [,] ; diff --git a/grammar/coll.interp b/grammar/coll.interp index 6e7deb4..04a6b32 100644 --- a/grammar/coll.interp +++ b/grammar/coll.interp @@ -10,7 +10,7 @@ null null null null -'int' +null null null null diff --git a/grammar/coll.tokens b/grammar/coll.tokens index 15510d8..397a1f9 100644 --- a/grammar/coll.tokens +++ b/grammar/coll.tokens @@ -18,4 +18,3 @@ CM=14 ')'=4 '|'=5 'not'=7 -'int'=11 diff --git a/grammar/collLexer.cs b/grammar/collLexer.cs index dc1918b..583fc16 100644 --- a/grammar/collLexer.cs +++ b/grammar/collLexer.cs @@ -59,8 +59,7 @@ public collLexer(ICharStream input, TextWriter output, TextWriter errorOutput) } private static readonly string[] _LiteralNames = { - null, "'{'", "'}'", "'('", "')'", "'|'", null, "'not'", null, null, null, - "'int'" + null, "'{'", "'}'", "'('", "')'", "'|'", null, "'not'" }; private static readonly string[] _SymbolicNames = { null, null, null, null, null, null, "BIN_LOG_OP", "NOT", "DEFINITION", @@ -94,7 +93,7 @@ static collLexer() { } } private static int[] _serializedATN = { - 4,0,14,173,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7, + 4,0,14,180,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7, 6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,1,0, 1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1, 5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,3,5,59,8,5,1,6,1,6,1,6,1,6,1, @@ -103,53 +102,56 @@ static collLexer() { 1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,3,8,113,8,8,1,9,1, 9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9, 1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1, - 9,1,9,1,9,3,9,154,8,9,1,10,1,10,1,10,1,10,1,11,4,11,161,8,11,11,11,12, - 11,162,1,12,4,12,166,8,12,11,12,12,12,167,1,12,1,12,1,13,1,13,0,0,14,1, - 1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,1, - 0,3,4,0,46,46,48,57,65,90,97,122,3,0,9,10,13,13,32,32,1,0,44,44,189,0, - 1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0, - 0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23, - 1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,1,29,1,0,0,0,3,31,1,0,0,0,5,33,1,0,0, - 0,7,35,1,0,0,0,9,37,1,0,0,0,11,58,1,0,0,0,13,60,1,0,0,0,15,74,1,0,0,0, - 17,112,1,0,0,0,19,153,1,0,0,0,21,155,1,0,0,0,23,160,1,0,0,0,25,165,1,0, - 0,0,27,171,1,0,0,0,29,30,5,123,0,0,30,2,1,0,0,0,31,32,5,125,0,0,32,4,1, - 0,0,0,33,34,5,40,0,0,34,6,1,0,0,0,35,36,5,41,0,0,36,8,1,0,0,0,37,38,5, - 124,0,0,38,10,1,0,0,0,39,40,5,97,0,0,40,41,5,110,0,0,41,59,5,100,0,0,42, - 43,5,111,0,0,43,59,5,114,0,0,44,45,5,110,0,0,45,46,5,97,0,0,46,47,5,110, - 0,0,47,59,5,100,0,0,48,49,5,120,0,0,49,50,5,111,0,0,50,59,5,114,0,0,51, - 52,5,110,0,0,52,53,5,111,0,0,53,59,5,114,0,0,54,55,5,120,0,0,55,56,5,110, - 0,0,56,57,5,111,0,0,57,59,5,114,0,0,58,39,1,0,0,0,58,42,1,0,0,0,58,44, - 1,0,0,0,58,48,1,0,0,0,58,51,1,0,0,0,58,54,1,0,0,0,59,12,1,0,0,0,60,61, - 5,110,0,0,61,62,5,111,0,0,62,63,5,116,0,0,63,14,1,0,0,0,64,65,5,105,0, - 0,65,75,5,115,0,0,66,67,5,105,0,0,67,75,5,110,0,0,68,69,5,115,0,0,69,70, - 5,117,0,0,70,71,5,98,0,0,71,72,5,115,0,0,72,73,5,101,0,0,73,75,5,116,0, - 0,74,64,1,0,0,0,74,66,1,0,0,0,74,68,1,0,0,0,75,16,1,0,0,0,76,77,5,117, - 0,0,77,78,5,110,0,0,78,79,5,105,0,0,79,80,5,111,0,0,80,113,5,110,0,0,81, - 82,5,105,0,0,82,83,5,110,0,0,83,84,5,116,0,0,84,85,5,101,0,0,85,86,5,114, - 0,0,86,87,5,115,0,0,87,88,5,101,0,0,88,89,5,99,0,0,89,90,5,116,0,0,90, - 91,5,105,0,0,91,92,5,111,0,0,92,113,5,110,0,0,93,94,5,99,0,0,94,95,5,114, - 0,0,95,96,5,111,0,0,96,97,5,115,0,0,97,113,5,115,0,0,98,99,5,117,0,0,99, - 100,5,110,0,0,100,101,5,111,0,0,101,102,5,114,0,0,102,103,5,100,0,0,103, - 104,5,101,0,0,104,105,5,114,0,0,105,106,5,101,0,0,106,107,5,100,0,0,107, - 108,5,99,0,0,108,109,5,114,0,0,109,110,5,111,0,0,110,111,5,115,0,0,111, - 113,5,115,0,0,112,76,1,0,0,0,112,81,1,0,0,0,112,93,1,0,0,0,112,98,1,0, - 0,0,113,18,1,0,0,0,114,115,5,115,0,0,115,116,5,101,0,0,116,154,5,116,0, - 0,117,118,5,109,0,0,118,119,5,117,0,0,119,120,5,108,0,0,120,121,5,116, - 0,0,121,122,5,105,0,0,122,123,5,115,0,0,123,124,5,101,0,0,124,154,5,116, - 0,0,125,126,5,108,0,0,126,127,5,105,0,0,127,128,5,115,0,0,128,154,5,116, - 0,0,129,130,5,103,0,0,130,131,5,114,0,0,131,132,5,97,0,0,132,133,5,112, - 0,0,133,154,5,104,0,0,134,135,5,119,0,0,135,136,5,101,0,0,136,137,5,105, - 0,0,137,138,5,103,0,0,138,139,5,104,0,0,139,140,5,116,0,0,140,141,5,101, - 0,0,141,142,5,100,0,0,142,143,5,71,0,0,143,144,5,114,0,0,144,145,5,97, - 0,0,145,146,5,112,0,0,146,154,5,104,0,0,147,148,5,115,0,0,148,149,5,116, - 0,0,149,150,5,114,0,0,150,151,5,105,0,0,151,152,5,110,0,0,152,154,5,103, - 0,0,153,114,1,0,0,0,153,117,1,0,0,0,153,125,1,0,0,0,153,129,1,0,0,0,153, - 134,1,0,0,0,153,147,1,0,0,0,154,20,1,0,0,0,155,156,5,105,0,0,156,157,5, - 110,0,0,157,158,5,116,0,0,158,22,1,0,0,0,159,161,7,0,0,0,160,159,1,0,0, - 0,161,162,1,0,0,0,162,160,1,0,0,0,162,163,1,0,0,0,163,24,1,0,0,0,164,166, - 7,1,0,0,165,164,1,0,0,0,166,167,1,0,0,0,167,165,1,0,0,0,167,168,1,0,0, - 0,168,169,1,0,0,0,169,170,6,12,0,0,170,26,1,0,0,0,171,172,7,2,0,0,172, - 28,1,0,0,0,7,0,58,74,112,153,162,167,1,6,0,0 + 9,1,9,1,9,3,9,154,8,9,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,10,3,10, + 165,8,10,1,11,4,11,168,8,11,11,11,12,11,169,1,12,4,12,173,8,12,11,12,12, + 12,174,1,12,1,12,1,13,1,13,0,0,14,1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17, + 9,19,10,21,11,23,12,25,13,27,14,1,0,3,4,0,46,46,48,57,65,90,97,122,3,0, + 9,10,13,13,32,32,1,0,44,44,197,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7, + 1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0, + 0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,1,29, + 1,0,0,0,3,31,1,0,0,0,5,33,1,0,0,0,7,35,1,0,0,0,9,37,1,0,0,0,11,58,1,0, + 0,0,13,60,1,0,0,0,15,74,1,0,0,0,17,112,1,0,0,0,19,153,1,0,0,0,21,164,1, + 0,0,0,23,167,1,0,0,0,25,172,1,0,0,0,27,178,1,0,0,0,29,30,5,123,0,0,30, + 2,1,0,0,0,31,32,5,125,0,0,32,4,1,0,0,0,33,34,5,40,0,0,34,6,1,0,0,0,35, + 36,5,41,0,0,36,8,1,0,0,0,37,38,5,124,0,0,38,10,1,0,0,0,39,40,5,97,0,0, + 40,41,5,110,0,0,41,59,5,100,0,0,42,43,5,111,0,0,43,59,5,114,0,0,44,45, + 5,110,0,0,45,46,5,97,0,0,46,47,5,110,0,0,47,59,5,100,0,0,48,49,5,120,0, + 0,49,50,5,111,0,0,50,59,5,114,0,0,51,52,5,110,0,0,52,53,5,111,0,0,53,59, + 5,114,0,0,54,55,5,120,0,0,55,56,5,110,0,0,56,57,5,111,0,0,57,59,5,114, + 0,0,58,39,1,0,0,0,58,42,1,0,0,0,58,44,1,0,0,0,58,48,1,0,0,0,58,51,1,0, + 0,0,58,54,1,0,0,0,59,12,1,0,0,0,60,61,5,110,0,0,61,62,5,111,0,0,62,63, + 5,116,0,0,63,14,1,0,0,0,64,65,5,105,0,0,65,75,5,115,0,0,66,67,5,105,0, + 0,67,75,5,110,0,0,68,69,5,115,0,0,69,70,5,117,0,0,70,71,5,98,0,0,71,72, + 5,115,0,0,72,73,5,101,0,0,73,75,5,116,0,0,74,64,1,0,0,0,74,66,1,0,0,0, + 74,68,1,0,0,0,75,16,1,0,0,0,76,77,5,117,0,0,77,78,5,110,0,0,78,79,5,105, + 0,0,79,80,5,111,0,0,80,113,5,110,0,0,81,82,5,105,0,0,82,83,5,110,0,0,83, + 84,5,116,0,0,84,85,5,101,0,0,85,86,5,114,0,0,86,87,5,115,0,0,87,88,5,101, + 0,0,88,89,5,99,0,0,89,90,5,116,0,0,90,91,5,105,0,0,91,92,5,111,0,0,92, + 113,5,110,0,0,93,94,5,99,0,0,94,95,5,114,0,0,95,96,5,111,0,0,96,97,5,115, + 0,0,97,113,5,115,0,0,98,99,5,117,0,0,99,100,5,110,0,0,100,101,5,111,0, + 0,101,102,5,114,0,0,102,103,5,100,0,0,103,104,5,101,0,0,104,105,5,114, + 0,0,105,106,5,101,0,0,106,107,5,100,0,0,107,108,5,99,0,0,108,109,5,114, + 0,0,109,110,5,111,0,0,110,111,5,115,0,0,111,113,5,115,0,0,112,76,1,0,0, + 0,112,81,1,0,0,0,112,93,1,0,0,0,112,98,1,0,0,0,113,18,1,0,0,0,114,115, + 5,115,0,0,115,116,5,101,0,0,116,154,5,116,0,0,117,118,5,109,0,0,118,119, + 5,117,0,0,119,120,5,108,0,0,120,121,5,116,0,0,121,122,5,105,0,0,122,123, + 5,115,0,0,123,124,5,101,0,0,124,154,5,116,0,0,125,126,5,108,0,0,126,127, + 5,105,0,0,127,128,5,115,0,0,128,154,5,116,0,0,129,130,5,103,0,0,130,131, + 5,114,0,0,131,132,5,97,0,0,132,133,5,112,0,0,133,154,5,104,0,0,134,135, + 5,119,0,0,135,136,5,101,0,0,136,137,5,105,0,0,137,138,5,103,0,0,138,139, + 5,104,0,0,139,140,5,116,0,0,140,141,5,101,0,0,141,142,5,100,0,0,142,143, + 5,71,0,0,143,144,5,114,0,0,144,145,5,97,0,0,145,146,5,112,0,0,146,154, + 5,104,0,0,147,148,5,115,0,0,148,149,5,116,0,0,149,150,5,114,0,0,150,151, + 5,105,0,0,151,152,5,110,0,0,152,154,5,103,0,0,153,114,1,0,0,0,153,117, + 1,0,0,0,153,125,1,0,0,0,153,129,1,0,0,0,153,134,1,0,0,0,153,147,1,0,0, + 0,154,20,1,0,0,0,155,156,5,105,0,0,156,157,5,110,0,0,157,165,5,116,0,0, + 158,159,5,110,0,0,159,160,5,117,0,0,160,161,5,109,0,0,161,162,5,98,0,0, + 162,163,5,101,0,0,163,165,5,114,0,0,164,155,1,0,0,0,164,158,1,0,0,0,165, + 22,1,0,0,0,166,168,7,0,0,0,167,166,1,0,0,0,168,169,1,0,0,0,169,167,1,0, + 0,0,169,170,1,0,0,0,170,24,1,0,0,0,171,173,7,1,0,0,172,171,1,0,0,0,173, + 174,1,0,0,0,174,172,1,0,0,0,174,175,1,0,0,0,175,176,1,0,0,0,176,177,6, + 12,0,0,177,26,1,0,0,0,178,179,7,2,0,0,179,28,1,0,0,0,8,0,58,74,112,153, + 164,169,174,1,6,0,0 }; public static readonly ATN _ATN = diff --git a/grammar/collLexer.interp b/grammar/collLexer.interp index 81908b3..f9d8a96 100644 --- a/grammar/collLexer.interp +++ b/grammar/collLexer.interp @@ -10,7 +10,7 @@ null null null null -'int' +null null null null @@ -56,4 +56,4 @@ mode names: DEFAULT_MODE atn: -[4, 0, 14, 173, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 59, 8, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 75, 8, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 113, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 154, 8, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 4, 11, 161, 8, 11, 11, 11, 12, 11, 162, 1, 12, 4, 12, 166, 8, 12, 11, 12, 12, 12, 167, 1, 12, 1, 12, 1, 13, 1, 13, 0, 0, 14, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 1, 0, 3, 4, 0, 46, 46, 48, 57, 65, 90, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 44, 44, 189, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 1, 29, 1, 0, 0, 0, 3, 31, 1, 0, 0, 0, 5, 33, 1, 0, 0, 0, 7, 35, 1, 0, 0, 0, 9, 37, 1, 0, 0, 0, 11, 58, 1, 0, 0, 0, 13, 60, 1, 0, 0, 0, 15, 74, 1, 0, 0, 0, 17, 112, 1, 0, 0, 0, 19, 153, 1, 0, 0, 0, 21, 155, 1, 0, 0, 0, 23, 160, 1, 0, 0, 0, 25, 165, 1, 0, 0, 0, 27, 171, 1, 0, 0, 0, 29, 30, 5, 123, 0, 0, 30, 2, 1, 0, 0, 0, 31, 32, 5, 125, 0, 0, 32, 4, 1, 0, 0, 0, 33, 34, 5, 40, 0, 0, 34, 6, 1, 0, 0, 0, 35, 36, 5, 41, 0, 0, 36, 8, 1, 0, 0, 0, 37, 38, 5, 124, 0, 0, 38, 10, 1, 0, 0, 0, 39, 40, 5, 97, 0, 0, 40, 41, 5, 110, 0, 0, 41, 59, 5, 100, 0, 0, 42, 43, 5, 111, 0, 0, 43, 59, 5, 114, 0, 0, 44, 45, 5, 110, 0, 0, 45, 46, 5, 97, 0, 0, 46, 47, 5, 110, 0, 0, 47, 59, 5, 100, 0, 0, 48, 49, 5, 120, 0, 0, 49, 50, 5, 111, 0, 0, 50, 59, 5, 114, 0, 0, 51, 52, 5, 110, 0, 0, 52, 53, 5, 111, 0, 0, 53, 59, 5, 114, 0, 0, 54, 55, 5, 120, 0, 0, 55, 56, 5, 110, 0, 0, 56, 57, 5, 111, 0, 0, 57, 59, 5, 114, 0, 0, 58, 39, 1, 0, 0, 0, 58, 42, 1, 0, 0, 0, 58, 44, 1, 0, 0, 0, 58, 48, 1, 0, 0, 0, 58, 51, 1, 0, 0, 0, 58, 54, 1, 0, 0, 0, 59, 12, 1, 0, 0, 0, 60, 61, 5, 110, 0, 0, 61, 62, 5, 111, 0, 0, 62, 63, 5, 116, 0, 0, 63, 14, 1, 0, 0, 0, 64, 65, 5, 105, 0, 0, 65, 75, 5, 115, 0, 0, 66, 67, 5, 105, 0, 0, 67, 75, 5, 110, 0, 0, 68, 69, 5, 115, 0, 0, 69, 70, 5, 117, 0, 0, 70, 71, 5, 98, 0, 0, 71, 72, 5, 115, 0, 0, 72, 73, 5, 101, 0, 0, 73, 75, 5, 116, 0, 0, 74, 64, 1, 0, 0, 0, 74, 66, 1, 0, 0, 0, 74, 68, 1, 0, 0, 0, 75, 16, 1, 0, 0, 0, 76, 77, 5, 117, 0, 0, 77, 78, 5, 110, 0, 0, 78, 79, 5, 105, 0, 0, 79, 80, 5, 111, 0, 0, 80, 113, 5, 110, 0, 0, 81, 82, 5, 105, 0, 0, 82, 83, 5, 110, 0, 0, 83, 84, 5, 116, 0, 0, 84, 85, 5, 101, 0, 0, 85, 86, 5, 114, 0, 0, 86, 87, 5, 115, 0, 0, 87, 88, 5, 101, 0, 0, 88, 89, 5, 99, 0, 0, 89, 90, 5, 116, 0, 0, 90, 91, 5, 105, 0, 0, 91, 92, 5, 111, 0, 0, 92, 113, 5, 110, 0, 0, 93, 94, 5, 99, 0, 0, 94, 95, 5, 114, 0, 0, 95, 96, 5, 111, 0, 0, 96, 97, 5, 115, 0, 0, 97, 113, 5, 115, 0, 0, 98, 99, 5, 117, 0, 0, 99, 100, 5, 110, 0, 0, 100, 101, 5, 111, 0, 0, 101, 102, 5, 114, 0, 0, 102, 103, 5, 100, 0, 0, 103, 104, 5, 101, 0, 0, 104, 105, 5, 114, 0, 0, 105, 106, 5, 101, 0, 0, 106, 107, 5, 100, 0, 0, 107, 108, 5, 99, 0, 0, 108, 109, 5, 114, 0, 0, 109, 110, 5, 111, 0, 0, 110, 111, 5, 115, 0, 0, 111, 113, 5, 115, 0, 0, 112, 76, 1, 0, 0, 0, 112, 81, 1, 0, 0, 0, 112, 93, 1, 0, 0, 0, 112, 98, 1, 0, 0, 0, 113, 18, 1, 0, 0, 0, 114, 115, 5, 115, 0, 0, 115, 116, 5, 101, 0, 0, 116, 154, 5, 116, 0, 0, 117, 118, 5, 109, 0, 0, 118, 119, 5, 117, 0, 0, 119, 120, 5, 108, 0, 0, 120, 121, 5, 116, 0, 0, 121, 122, 5, 105, 0, 0, 122, 123, 5, 115, 0, 0, 123, 124, 5, 101, 0, 0, 124, 154, 5, 116, 0, 0, 125, 126, 5, 108, 0, 0, 126, 127, 5, 105, 0, 0, 127, 128, 5, 115, 0, 0, 128, 154, 5, 116, 0, 0, 129, 130, 5, 103, 0, 0, 130, 131, 5, 114, 0, 0, 131, 132, 5, 97, 0, 0, 132, 133, 5, 112, 0, 0, 133, 154, 5, 104, 0, 0, 134, 135, 5, 119, 0, 0, 135, 136, 5, 101, 0, 0, 136, 137, 5, 105, 0, 0, 137, 138, 5, 103, 0, 0, 138, 139, 5, 104, 0, 0, 139, 140, 5, 116, 0, 0, 140, 141, 5, 101, 0, 0, 141, 142, 5, 100, 0, 0, 142, 143, 5, 71, 0, 0, 143, 144, 5, 114, 0, 0, 144, 145, 5, 97, 0, 0, 145, 146, 5, 112, 0, 0, 146, 154, 5, 104, 0, 0, 147, 148, 5, 115, 0, 0, 148, 149, 5, 116, 0, 0, 149, 150, 5, 114, 0, 0, 150, 151, 5, 105, 0, 0, 151, 152, 5, 110, 0, 0, 152, 154, 5, 103, 0, 0, 153, 114, 1, 0, 0, 0, 153, 117, 1, 0, 0, 0, 153, 125, 1, 0, 0, 0, 153, 129, 1, 0, 0, 0, 153, 134, 1, 0, 0, 0, 153, 147, 1, 0, 0, 0, 154, 20, 1, 0, 0, 0, 155, 156, 5, 105, 0, 0, 156, 157, 5, 110, 0, 0, 157, 158, 5, 116, 0, 0, 158, 22, 1, 0, 0, 0, 159, 161, 7, 0, 0, 0, 160, 159, 1, 0, 0, 0, 161, 162, 1, 0, 0, 0, 162, 160, 1, 0, 0, 0, 162, 163, 1, 0, 0, 0, 163, 24, 1, 0, 0, 0, 164, 166, 7, 1, 0, 0, 165, 164, 1, 0, 0, 0, 166, 167, 1, 0, 0, 0, 167, 165, 1, 0, 0, 0, 167, 168, 1, 0, 0, 0, 168, 169, 1, 0, 0, 0, 169, 170, 6, 12, 0, 0, 170, 26, 1, 0, 0, 0, 171, 172, 7, 2, 0, 0, 172, 28, 1, 0, 0, 0, 7, 0, 58, 74, 112, 153, 162, 167, 1, 6, 0, 0] \ No newline at end of file +[4, 0, 14, 180, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 59, 8, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 75, 8, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 113, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 154, 8, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 3, 10, 165, 8, 10, 1, 11, 4, 11, 168, 8, 11, 11, 11, 12, 11, 169, 1, 12, 4, 12, 173, 8, 12, 11, 12, 12, 12, 174, 1, 12, 1, 12, 1, 13, 1, 13, 0, 0, 14, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 1, 0, 3, 4, 0, 46, 46, 48, 57, 65, 90, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 44, 44, 197, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 1, 29, 1, 0, 0, 0, 3, 31, 1, 0, 0, 0, 5, 33, 1, 0, 0, 0, 7, 35, 1, 0, 0, 0, 9, 37, 1, 0, 0, 0, 11, 58, 1, 0, 0, 0, 13, 60, 1, 0, 0, 0, 15, 74, 1, 0, 0, 0, 17, 112, 1, 0, 0, 0, 19, 153, 1, 0, 0, 0, 21, 164, 1, 0, 0, 0, 23, 167, 1, 0, 0, 0, 25, 172, 1, 0, 0, 0, 27, 178, 1, 0, 0, 0, 29, 30, 5, 123, 0, 0, 30, 2, 1, 0, 0, 0, 31, 32, 5, 125, 0, 0, 32, 4, 1, 0, 0, 0, 33, 34, 5, 40, 0, 0, 34, 6, 1, 0, 0, 0, 35, 36, 5, 41, 0, 0, 36, 8, 1, 0, 0, 0, 37, 38, 5, 124, 0, 0, 38, 10, 1, 0, 0, 0, 39, 40, 5, 97, 0, 0, 40, 41, 5, 110, 0, 0, 41, 59, 5, 100, 0, 0, 42, 43, 5, 111, 0, 0, 43, 59, 5, 114, 0, 0, 44, 45, 5, 110, 0, 0, 45, 46, 5, 97, 0, 0, 46, 47, 5, 110, 0, 0, 47, 59, 5, 100, 0, 0, 48, 49, 5, 120, 0, 0, 49, 50, 5, 111, 0, 0, 50, 59, 5, 114, 0, 0, 51, 52, 5, 110, 0, 0, 52, 53, 5, 111, 0, 0, 53, 59, 5, 114, 0, 0, 54, 55, 5, 120, 0, 0, 55, 56, 5, 110, 0, 0, 56, 57, 5, 111, 0, 0, 57, 59, 5, 114, 0, 0, 58, 39, 1, 0, 0, 0, 58, 42, 1, 0, 0, 0, 58, 44, 1, 0, 0, 0, 58, 48, 1, 0, 0, 0, 58, 51, 1, 0, 0, 0, 58, 54, 1, 0, 0, 0, 59, 12, 1, 0, 0, 0, 60, 61, 5, 110, 0, 0, 61, 62, 5, 111, 0, 0, 62, 63, 5, 116, 0, 0, 63, 14, 1, 0, 0, 0, 64, 65, 5, 105, 0, 0, 65, 75, 5, 115, 0, 0, 66, 67, 5, 105, 0, 0, 67, 75, 5, 110, 0, 0, 68, 69, 5, 115, 0, 0, 69, 70, 5, 117, 0, 0, 70, 71, 5, 98, 0, 0, 71, 72, 5, 115, 0, 0, 72, 73, 5, 101, 0, 0, 73, 75, 5, 116, 0, 0, 74, 64, 1, 0, 0, 0, 74, 66, 1, 0, 0, 0, 74, 68, 1, 0, 0, 0, 75, 16, 1, 0, 0, 0, 76, 77, 5, 117, 0, 0, 77, 78, 5, 110, 0, 0, 78, 79, 5, 105, 0, 0, 79, 80, 5, 111, 0, 0, 80, 113, 5, 110, 0, 0, 81, 82, 5, 105, 0, 0, 82, 83, 5, 110, 0, 0, 83, 84, 5, 116, 0, 0, 84, 85, 5, 101, 0, 0, 85, 86, 5, 114, 0, 0, 86, 87, 5, 115, 0, 0, 87, 88, 5, 101, 0, 0, 88, 89, 5, 99, 0, 0, 89, 90, 5, 116, 0, 0, 90, 91, 5, 105, 0, 0, 91, 92, 5, 111, 0, 0, 92, 113, 5, 110, 0, 0, 93, 94, 5, 99, 0, 0, 94, 95, 5, 114, 0, 0, 95, 96, 5, 111, 0, 0, 96, 97, 5, 115, 0, 0, 97, 113, 5, 115, 0, 0, 98, 99, 5, 117, 0, 0, 99, 100, 5, 110, 0, 0, 100, 101, 5, 111, 0, 0, 101, 102, 5, 114, 0, 0, 102, 103, 5, 100, 0, 0, 103, 104, 5, 101, 0, 0, 104, 105, 5, 114, 0, 0, 105, 106, 5, 101, 0, 0, 106, 107, 5, 100, 0, 0, 107, 108, 5, 99, 0, 0, 108, 109, 5, 114, 0, 0, 109, 110, 5, 111, 0, 0, 110, 111, 5, 115, 0, 0, 111, 113, 5, 115, 0, 0, 112, 76, 1, 0, 0, 0, 112, 81, 1, 0, 0, 0, 112, 93, 1, 0, 0, 0, 112, 98, 1, 0, 0, 0, 113, 18, 1, 0, 0, 0, 114, 115, 5, 115, 0, 0, 115, 116, 5, 101, 0, 0, 116, 154, 5, 116, 0, 0, 117, 118, 5, 109, 0, 0, 118, 119, 5, 117, 0, 0, 119, 120, 5, 108, 0, 0, 120, 121, 5, 116, 0, 0, 121, 122, 5, 105, 0, 0, 122, 123, 5, 115, 0, 0, 123, 124, 5, 101, 0, 0, 124, 154, 5, 116, 0, 0, 125, 126, 5, 108, 0, 0, 126, 127, 5, 105, 0, 0, 127, 128, 5, 115, 0, 0, 128, 154, 5, 116, 0, 0, 129, 130, 5, 103, 0, 0, 130, 131, 5, 114, 0, 0, 131, 132, 5, 97, 0, 0, 132, 133, 5, 112, 0, 0, 133, 154, 5, 104, 0, 0, 134, 135, 5, 119, 0, 0, 135, 136, 5, 101, 0, 0, 136, 137, 5, 105, 0, 0, 137, 138, 5, 103, 0, 0, 138, 139, 5, 104, 0, 0, 139, 140, 5, 116, 0, 0, 140, 141, 5, 101, 0, 0, 141, 142, 5, 100, 0, 0, 142, 143, 5, 71, 0, 0, 143, 144, 5, 114, 0, 0, 144, 145, 5, 97, 0, 0, 145, 146, 5, 112, 0, 0, 146, 154, 5, 104, 0, 0, 147, 148, 5, 115, 0, 0, 148, 149, 5, 116, 0, 0, 149, 150, 5, 114, 0, 0, 150, 151, 5, 105, 0, 0, 151, 152, 5, 110, 0, 0, 152, 154, 5, 103, 0, 0, 153, 114, 1, 0, 0, 0, 153, 117, 1, 0, 0, 0, 153, 125, 1, 0, 0, 0, 153, 129, 1, 0, 0, 0, 153, 134, 1, 0, 0, 0, 153, 147, 1, 0, 0, 0, 154, 20, 1, 0, 0, 0, 155, 156, 5, 105, 0, 0, 156, 157, 5, 110, 0, 0, 157, 165, 5, 116, 0, 0, 158, 159, 5, 110, 0, 0, 159, 160, 5, 117, 0, 0, 160, 161, 5, 109, 0, 0, 161, 162, 5, 98, 0, 0, 162, 163, 5, 101, 0, 0, 163, 165, 5, 114, 0, 0, 164, 155, 1, 0, 0, 0, 164, 158, 1, 0, 0, 0, 165, 22, 1, 0, 0, 0, 166, 168, 7, 0, 0, 0, 167, 166, 1, 0, 0, 0, 168, 169, 1, 0, 0, 0, 169, 167, 1, 0, 0, 0, 169, 170, 1, 0, 0, 0, 170, 24, 1, 0, 0, 0, 171, 173, 7, 1, 0, 0, 172, 171, 1, 0, 0, 0, 173, 174, 1, 0, 0, 0, 174, 172, 1, 0, 0, 0, 174, 175, 1, 0, 0, 0, 175, 176, 1, 0, 0, 0, 176, 177, 6, 12, 0, 0, 177, 26, 1, 0, 0, 0, 178, 179, 7, 2, 0, 0, 179, 28, 1, 0, 0, 0, 8, 0, 58, 74, 112, 153, 164, 169, 174, 1, 6, 0, 0] \ No newline at end of file diff --git a/grammar/collLexer.tokens b/grammar/collLexer.tokens index 15510d8..397a1f9 100644 --- a/grammar/collLexer.tokens +++ b/grammar/collLexer.tokens @@ -18,4 +18,3 @@ CM=14 ')'=4 '|'=5 'not'=7 -'int'=11 diff --git a/grammar/collParser.cs b/grammar/collParser.cs index 4cbbd98..447963f 100644 --- a/grammar/collParser.cs +++ b/grammar/collParser.cs @@ -46,8 +46,7 @@ public const int }; private static readonly string[] _LiteralNames = { - null, "'{'", "'}'", "'('", "')'", "'|'", null, "'not'", null, null, null, - "'int'" + null, "'{'", "'}'", "'('", "')'", "'|'", null, "'not'" }; private static readonly string[] _SymbolicNames = { null, null, null, null, null, null, "BIN_LOG_OP", "NOT", "DEFINITION", diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 2beac42..791b932 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -1,4 +1,5 @@ using SPADE; +using SPADE.Grammar.Enums; namespace SPADE.Grammar.descriptor; @@ -85,6 +86,12 @@ public void check(Dictionary map) throw Failure($"{Describe(map[term])} is not an integer", map[term]); } break; + case "number": + if (!map[term].IsValue() || !NumberValue.IsNumber(map[term].ToString())) + { + throw Failure($"{Describe(map[term])} is not a number", map[term]); + } + break; case "string": // Any single value is a string, a number-looking one like 5 included. if (!map[term].IsValue()) diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index 3e71df3..ad7c2e3 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -64,6 +64,13 @@ public SetDescriptor(Dictionary _map, String name) public SetDescriptor(Dictionary _map, BuiltInSet builtInSet) { map = _map; + if (builtInSet == BuiltInSet.numbers) + { + // A single value only: a set or list's text is never a number anyway, but say so. + isMember = (checkedColl) => checkedColl.IsValue() && NumberValue.IsNumber(checkedColl.ToString()); + genSet = () => throw new Exception("Can't generate infinite number set"); + return; + } isMember = (checkedColl) => int.TryParse(checkedColl.ToString(), out _); genSet = () => throw new Exception("Can't generate infinite integer set"); } @@ -95,7 +102,7 @@ public SetDescriptor(Dictionary _map, FormatDescriptor f /// /// For an item that isn't a member: the first piece of it missing from a part named in the set - /// expression, and that part. Null when no such piece is found, e.g. for int or a set + /// expression, and that part. Null when no such piece is found, e.g. for int, number or a set /// literal, or when the item has the wrong shape (a value where cross needs a pair). /// public (string Value, string Part)? FindMissing(UtilCollection item) diff --git a/tests/SPADE.Tests/NumberTests.cs b/tests/SPADE.Tests/NumberTests.cs new file mode 100644 index 0000000..e7d0b75 --- /dev/null +++ b/tests/SPADE.Tests/NumberTests.cs @@ -0,0 +1,214 @@ +using System.Globalization; +using System.Text; +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// "number" is a decimal type next to "int": a sign, digits and an optional fraction, read with the +// invariant culture. Every int is a number; exponents, NaN and infinities are not (#23). +public class NumberTests +{ + private static void Parse(string grammar, string instance) + { + new StringParser(grammar).parse(instance); + } + + private static SpadeParseException Fails(string grammar, string instance) + { + return Assert.Throws(() => new StringParser(grammar).parse(instance)); + } + + // 400 digits: a valid shape, but too big for a double. + private static readonly string Huge = new('9', 400); + + [Theory] + [InlineData("5")] + [InlineData("-3")] + [InlineData("+7")] + [InlineData("2.5")] + [InlineData("-0.75")] + [InlineData(".5")] + [InlineData("-.5")] + [InlineData("+.5")] + [InlineData("0")] + [InlineData("0.0")] + [InlineData("007.50")] + [InlineData("\"2.5\"")] + [InlineData(" 2.5 ")] + public void IsNumberAcceptsDecimals(string instance) + { + Parse("{B | B is number}", instance); + } + + [Theory] + [InlineData("1e3", "1e3")] + [InlineData("2E-4", "2E-4")] + [InlineData("NaN", "NaN")] + [InlineData("Infinity", "Infinity")] + [InlineData("-Infinity", "-Infinity")] + [InlineData("∞", "∞")] + [InlineData("1.2.3", "1.2.3")] + [InlineData("5.", "5.")] + [InlineData(".", ".")] + [InlineData("-", "-")] + [InlineData("+", "+")] + [InlineData("--5", "--5")] + [InlineData("x", "x")] + [InlineData("0x1A", "0x1A")] + [InlineData("1 000", "1 000")] + [InlineData("\"1,000\"", "1,000")] + [InlineData("\"2,5\"", "2,5")] + [InlineData("\" 2.5\"", " 2.5")] + [InlineData("\"2.5 \"", "2.5 ")] + [InlineData("\"\"", "")] + [InlineData("{1,2}", "{1,2}")] + [InlineData("(1,2)", "(1,2)")] + [InlineData("{}", "{}")] + public void IsNumberRejectsNonNumbers(string instance, string described) + { + SpadeParseException e = Fails("{B | B is number}", instance); + Assert.Equal($"Constraint B is number failed: {described} is not a number", e.Message); + } + + [Fact] + public void IsNumberRejectsAValueTooBigForADouble() + { + SpadeParseException e = Fails("{B | B is number}", Huge); + Assert.EndsWith(" is not a number", e.Message); + Assert.Equal("B", e.Part); + Assert.Equal(Huge, e.Element); + } + + [Fact] + public void FailedNumberCheckNamesPartAndValue() + { + SpadeParseException e = Fails("{(N,K) | N is set, K is number}", "({1,2},x)"); + Assert.Equal("Constraint K is number failed: x is not a number", e.Message); + Assert.Equal("K", e.Part); + Assert.Equal("x", e.Element); + Assert.Null(e.MissingValue); + Assert.Null(e.ExpectedIn); + } + + [Fact] + public void QuotedNumberIsANumber() + { + StringParser parser = new("{(K,S) | K is number, S subset number}"); + parser.parse("(\"2.5\", {\"-0.75\", 3})"); + Assert.Equal("2.5", parser["K"].ToString()); + Assert.Equal(2, parser["S"].Count()); + } + + [Theory] + [InlineData("3")] + [InlineData("-12")] + [InlineData("+4")] + [InlineData("0")] + [InlineData("\"5\"")] + [InlineData("2147483647")] + [InlineData("-2147483648")] + public void EveryIntIsANumber(string instance) + { + Parse("{N | N is int}", instance); + Parse("{N | N is number}", instance); + Parse("{S | S subset number}", "{" + instance + "}"); + } + + [Fact] + public void IntStillRejectsDecimals() + { + SpadeParseException e = Fails("{N | N is int}", "2.5"); + Assert.Equal("Constraint N is int failed: 2.5 is not an integer", e.Message); + } + + [Fact] + public void SubsetNumberAcceptsDecimals() + { + StringParser parser = new("{S | S subset number}"); + parser.parse("{1, 2.5, -.5, +7}"); + Assert.Equal(4, parser["S"].Count()); + } + + [Theory] + [InlineData("{1, 2.5, x}", "x")] + [InlineData("{1, 1e3}", "1e3")] + [InlineData("{1, {2}}", "{2}")] + public void SubsetNumberRejectsANonNumber(string instance, string bad) + { + SpadeParseException e = Fails("{S | S subset number}", instance); + Assert.Equal($"Constraint S subset number failed: {bad} is not a member", e.Message); + Assert.Equal("S", e.Part); + Assert.Equal(bad, e.Element); + Assert.Null(e.MissingValue); + Assert.Null(e.ExpectedIn); + } + + // Convex Hull: a set of points with real coordinates, written either way. + [Theory] + [InlineData("{P | P subset number cross number}")] + [InlineData("{P | P subset {(x,y) | x is number, y is number}}")] + public void ConvexHullPointsAreNumberPairs(string grammar) + { + StringParser parser = new(grammar); + parser.parse("{(0,0), (2.5,-1), (-.5,3.25), (+4,\"1.5\")}"); + Assert.Equal(4, parser["P"].Count()); + } + + [Fact] + public void NumberCrossNumberRejectsANonNumberCoordinate() + { + SpadeParseException e = Fails("{P | P subset number cross number}", "{(0,0), (2.5,north)}"); + Assert.Equal("Constraint P subset number cross number failed: (2.5,north) is not a member", e.Message); + Assert.Equal("P", e.Part); + Assert.Equal("(2.5,north)", e.Element); + Assert.Null(e.MissingValue); + Assert.Null(e.ExpectedIn); + } + + [Fact] + public void NestedNumberCheckBelongsToTheOuterPart() + { + SpadeParseException e = Fails("{P | P subset {(x,y) | x is number, y is number}}", "{(0,0), (2.5,1e3)}"); + Assert.Equal("P", e.Part); + Assert.Contains("1e3 is not a number", e.Message); + } + + [Fact] + public void NumberCrossNumberRejectsAValue() + { + SpadeParseException e = Fails("{P | P subset number cross number}", "{(0,0), 5}"); + Assert.Equal("Constraint P subset number cross number failed: 5 is not a member", e.Message); + Assert.Null(e.MissingValue); + } + + [Fact] + public void NumbersDoNotDependOnTheMachineCulture() + { + CultureInfo saved = CultureInfo.CurrentCulture; + try + { + // German writes 2,5 for two and a half. + CultureInfo.CurrentCulture = new CultureInfo("de-DE"); + Parse("{B | B is number}", "2.5"); + Parse("{S | S subset number}", "{2.5, -.75}"); + SpadeParseException e = Fails("{B | B is number}", "\"2,5\""); + Assert.Equal("Constraint B is number failed: 2,5 is not a number", e.Message); + Fails("{S | S subset number}", "{\"2,5\"}"); + } + finally + { + CultureInfo.CurrentCulture = saved; + } + } + + [Fact] + public void DefinitionReportsNumber() + { + DefinitionDescription d = new StringParser("{((A,B),C) | A is int, B is number, C subset number cross number}").Definition; + Assert.Equal(PartKind.Int, d["A"].Kind); + Assert.Equal(PartKind.Number, d["B"].Kind); + Assert.Equal(PartKind.Subset, d["C"].Kind); + Assert.Equal("number cross number", d["C"].Of); + } +} From 1dd60a5d8bbe61b58e2a1a5dddc7e1434cbdf883 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:25:55 -0600 Subject: [PATCH 13/35] Read int with the invariant culture, like number "x is int", "S subset int" and UtilCollection.parseInt used the machine's culture, so a culture with another minus sign could refuse "-5". They now share IntValue, which parses with CultureInfo.InvariantCulture as NumberValue does for number (#23). --- UtilCollection.cs | 2 +- grammar/BuiltInSet.cs | 15 ++++++++++++ grammar/desciptors/ConstraintDescriptor.cs | 2 +- grammar/desciptors/SetDescriptor.cs | 2 +- tests/SPADE.Tests/TypeConstraintTests.cs | 28 ++++++++++++++++++++++ 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/UtilCollection.cs b/UtilCollection.cs index 6b47c94..83df0ec 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -246,7 +246,7 @@ public int Count() public int parseInt() { - return int.Parse(ToString()); + return SPADE.Grammar.Enums.IntValue.Parse(ToString()); } public UtilCollection Intersect(UtilCollection other) diff --git a/grammar/BuiltInSet.cs b/grammar/BuiltInSet.cs index 9d5953b..2ceb73c 100644 --- a/grammar/BuiltInSet.cs +++ b/grammar/BuiltInSet.cs @@ -20,3 +20,18 @@ public static bool IsNumber(string text) && double.IsFinite(value); } } + +// What "int" accepts, shared by "x is int", "S subset int" and UtilCollection.parseInt. Read with the +// invariant culture, like "number", so a machine's culture (its minus sign, say) never changes the answer. +static class IntValue +{ + public static bool TryParse(string text, out int value) + { + return int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out value); + } + + public static int Parse(string text) + { + return int.Parse(text, NumberStyles.Integer, CultureInfo.InvariantCulture); + } +} diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 791b932..24aa869 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -81,7 +81,7 @@ public void check(Dictionary map) map[term] = map[term].AsMultiset(); break; case "int": - if (!map[term].IsValue() || !int.TryParse(map[term].ToString(), out _)) + if (!map[term].IsValue() || !IntValue.TryParse(map[term].ToString(), out _)) { throw Failure($"{Describe(map[term])} is not an integer", map[term]); } diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index ad7c2e3..7b53d50 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -71,7 +71,7 @@ public SetDescriptor(Dictionary _map, BuiltInSet builtIn genSet = () => throw new Exception("Can't generate infinite number set"); return; } - isMember = (checkedColl) => int.TryParse(checkedColl.ToString(), out _); + isMember = (checkedColl) => IntValue.TryParse(checkedColl.ToString(), out _); genSet = () => throw new Exception("Can't generate infinite integer set"); } diff --git a/tests/SPADE.Tests/TypeConstraintTests.cs b/tests/SPADE.Tests/TypeConstraintTests.cs index 701e9d7..cf2e42a 100644 --- a/tests/SPADE.Tests/TypeConstraintTests.cs +++ b/tests/SPADE.Tests/TypeConstraintTests.cs @@ -1,3 +1,4 @@ +using System.Globalization; using SPADE; using Xunit; @@ -139,4 +140,31 @@ public void IsStringInADfaDefinition() Assert.Equal("Constraint S is string failed: {q0} is not a string", e.Message); Assert.Equal("S", e.Part); } + + [Fact] + public void IntsDoNotDependOnTheMachineCulture() + { + CultureInfo saved = CultureInfo.CurrentCulture; + try + { + // A culture whose minus sign is not '-' (some cultures use U+2212): "-5" must still be an int. + CultureInfo culture = (CultureInfo)CultureInfo.InvariantCulture.Clone(); + culture.NumberFormat.NegativeSign = "~"; + CultureInfo.CurrentCulture = culture; + + Parse("{N | N is int}", "-5"); + Parse("{S | S subset int}", "{-5, 3}"); + StringParser parser = new("{N | N is int}"); + parser.parse("-5"); + Assert.Equal(-5, parser["N"].parseInt()); + + SpadeParseException e = Assert.Throws(() => Parse("{N | N is int}", "~5")); + Assert.Equal("Constraint N is int failed: ~5 is not an integer", e.Message); + Assert.Throws(() => Parse("{S | S subset int}", "{~5}")); + } + finally + { + CultureInfo.CurrentCulture = saved; + } + } } From ac55d26f76870d43e2ba94d89c9771d36b0dd884 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:17:20 -0600 Subject: [PATCH 14/35] ci: run the Redux Build System (rbs) report, soft for now rbs runs audit, format-check, lint (dotnet build) and unit tests with a coverage floor inside the repo's devcontainer, the same way it does for Redux. It reports without failing (soft) while ci.yml stays the gate; publish.yml keeps doing the NuGet release, since rbs has no NuGet push. - SPADE.slnx: the dotnet engine needs a solution. It names only DiscreteParser.csproj for now; the test project joins it with the first fix PR. ci.yml and publish.yml name DiscreteParser.csproj explicitly, so the MSB1011 a bare `dotnet build` now gives does not affect them. - rbs.toml: coverage floor 70; the goal is 80%+. - .devcontainer: SDK 10 (reads .slnx, builds the net6.0 library) plus the .NET 8 runtime the tests run on. - .gitignore: rbs's .rbs/ work directory and report.md. --- .devcontainer/devcontainer.json | 29 +++++++++++++++++++++++++++++ .github/workflows/rbs.yml | 23 +++++++++++++++++++++++ .gitignore | 2 ++ SPADE.slnx | 3 +++ rbs.toml | 9 +++++++++ 5 files changed, 66 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/workflows/rbs.yml create mode 100644 SPADE.slnx create mode 100644 rbs.toml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..ca172ea --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,29 @@ +{ + "name": "SPADE .NET Dev Container", + // SDK 10 reads SPADE.slnx (the .slnx format needs SDK 9.0.200+) and still builds the net6.0 library. + "image": "mcr.microsoft.com/dotnet/sdk:10.0", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "username": "vscode", + "installZsh": false, + "upgradePackages": false + }, + // The tests target net8.0, so they need the .NET 8 runtime next to SDK 10. + "ghcr.io/devcontainers/features/dotnet:2": { + "version": "none", + "dotnetRuntimeVersions": "8.0" + }, + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { "moby": false }, + "ghcr.io/reduxisu/features/rbs:1": {} + }, + "remoteUser": "vscode", + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csharp" + ] + } + }, + "postStartCommand": "if [ \"$(stat -c %U /workspaces/SPADE)\" != \"vscode\" ]; then sudo chown -R vscode:vscode /workspaces/SPADE; fi", + "postCreateCommand": "dotnet --list-sdks && dotnet --list-runtimes" +} diff --git a/.github/workflows/rbs.yml b/.github/workflows/rbs.yml new file mode 100644 index 0000000..b4b1899 --- /dev/null +++ b/.github/workflows/rbs.yml @@ -0,0 +1,23 @@ +name: rbs + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + pull-requests: write + packages: write + +concurrency: + group: rbs-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-test: + uses: ReduxISU/Redux_Build_System/.github/workflows/ci.yml@main + secrets: inherit + with: + # Report only while ci.yml is still the gate. + soft: true diff --git a/.gitignore b/.gitignore index 75ef810..c5be391 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ bin/ obj/ DF.sln +.rbs/ +report.md diff --git a/SPADE.slnx b/SPADE.slnx new file mode 100644 index 0000000..b5c1ef7 --- /dev/null +++ b/SPADE.slnx @@ -0,0 +1,3 @@ + + + diff --git a/rbs.toml b/rbs.toml new file mode 100644 index 0000000..cfbb4f0 --- /dev/null +++ b/rbs.toml @@ -0,0 +1,9 @@ +# Redux Build System pipeline config. See github.com/ReduxISU/Redux_Build_System. +# SPADE is a NuGet library with no image, so it has no [artifact] / [integration] / [push]; +# publish.yml still does the NuGet release. +engine = "dotnet" +solution = "SPADE.slnx" + +[unit-test] +# The standard we're holding to. Floor 70%; the goal is 80%+. New code needs tests to keep it above the floor. +coverage-min = 70 From 4d63d9a1203eeaec223547fd6fe3273a9c026868 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:18:14 -0600 Subject: [PATCH 15/35] style: run dotnet format Whitespace only: dotnet format SPADE.slnx, so rbs's format-check starts clean. Brace placement, indentation and blank lines; no code changes. --- StringParser.cs | 7 +++-- UtilCollection.cs | 36 ++++++++++++---------- grammar/BuiltInSet.cs | 3 +- grammar/codeGen.cs | 9 +++--- grammar/desciptors/ConstraintDescriptor.cs | 22 ++++++------- grammar/desciptors/SetDescriptor.cs | 14 ++++----- grammar/desciptors/formatDescriptor.cs | 4 +-- 7 files changed, 50 insertions(+), 45 deletions(-) diff --git a/StringParser.cs b/StringParser.cs index 23fb6a1..ead7bc7 100644 --- a/StringParser.cs +++ b/StringParser.cs @@ -7,11 +7,12 @@ using SPADE.Grammar; namespace SPADE; + public class StringParser { readonly string format; public Dictionary map = new(); - + private bool hasParsed; public StringParser(string f) @@ -24,7 +25,7 @@ public void parse(string instance) { hasParsed = true; AntlrInputStream stream = new AntlrInputStream(format); - collLexer lex = new collLexer(stream); + collLexer lex = new collLexer(stream); CommonTokenStream tokens = new CommonTokenStream(lex); collParser parser = new collParser(tokens); @@ -35,7 +36,7 @@ public void parse(string instance) gen.Parse(new UtilCollection(instance)); - map = gen.map; + map = gen.map; } diff --git a/UtilCollection.cs b/UtilCollection.cs index f63d236..42e9a4c 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -52,14 +52,14 @@ public UtilCollection(string instance) value = instance; } } - + public UtilCollection(HashSet set_) { isOrdered = false; set = new(); foreach (string i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -69,7 +69,7 @@ public UtilCollection(HashSet> set_) set = new(); foreach (HashSet i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -120,23 +120,25 @@ private string findMatchingBrace(string instance) } } throw new Exception("Braces not matched"); - } + } public UtilCollection this[int index] { - get { + get + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - return list[index]; - } - set { + return list[index]; + } + set + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - list[index] = value; - } + list[index] = value; + } } public void Add(UtilCollection item) { - if(isOrdered) + if (isOrdered) { list.Add(item); } @@ -182,7 +184,7 @@ public void assertOrdered() public void assertUnordered() { - if(isOrdered) throw new InvalidOperationException("Ordered"); + if (isOrdered) throw new InvalidOperationException("Ordered"); } public bool IsUnordered() @@ -239,7 +241,7 @@ public UtilCollection Intersect(UtilCollection other) throw new NotImplementedException(); } } - + public UtilCollection Union(UtilCollection other) { if (isOrdered != other.isOrdered) throw new InvalidOperationException("Can't take the union of a set and a list"); @@ -258,9 +260,9 @@ public UtilCollection Cross(UtilCollection other) UtilCollection ret = new UtilCollection(); foreach (UtilCollection a in this) { - foreach(UtilCollection b in other) + foreach (UtilCollection b in other) { - UtilCollection add = new UtilCollection(new List{a,b}); + UtilCollection add = new UtilCollection(new List { a, b }); ret.Add(add); } } @@ -317,14 +319,14 @@ override public string ToString() { return value!; } - + string str = ""; if (isOrdered) str += "("; else str += "{"; - foreach (var item in this) + foreach (var item in this) { //Console.WriteLine(item.ToString()); str += item.ToString() + ","; diff --git a/grammar/BuiltInSet.cs b/grammar/BuiltInSet.cs index 948aba7..3c48b97 100644 --- a/grammar/BuiltInSet.cs +++ b/grammar/BuiltInSet.cs @@ -1,2 +1,3 @@ namespace SPADE.Grammar.Enums; -enum BuiltInSet {integers} \ No newline at end of file + +enum BuiltInSet { integers } \ No newline at end of file diff --git a/grammar/codeGen.cs b/grammar/codeGen.cs index 83ea6ef..5be12d7 100644 --- a/grammar/codeGen.cs +++ b/grammar/codeGen.cs @@ -10,18 +10,19 @@ namespace SPADE.Grammar; -class codeGen : IcollListener { +class codeGen : IcollListener +{ public Dictionary map; public Stack formatStack; - public SetDescriptor ?setDescriptor; + public SetDescriptor? setDescriptor; public Stack setDescriptorStack; public Stack constraintDescriptorStack; public Stack> constraintDescriptorStackStack; - public codeGen() : base() + public codeGen() : base() { map = new(); formatStack = new(); @@ -88,7 +89,7 @@ public void ExitFormat([NotNull] collParser.FormatContext context) List lst = new(); for (int i = 0; i < context.format().Length; i++) { - lst.Add(formatStack.Pop()); + lst.Add(formatStack.Pop()); } lst.Reverse(); // have to reverse the list, since the leftmost should be the one that was popped first formatStack.Push(new FormatDescriptor(lst)); diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 70ff95e..e673dfa 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -11,16 +11,16 @@ class ConstraintDescriptor public ConstraintDescriptor(String _term, String _definition, SetDescriptor _set) { - term = _term; - definition = _definition; - set = _set; + term = _term; + definition = _definition; + set = _set; } public ConstraintDescriptor(String _term, String _definition, String _built_in) { - term = _term; - definition = _definition; - built_in = _built_in; + term = _term; + definition = _definition; + built_in = _built_in; } @@ -56,13 +56,13 @@ public void check(Dictionary map) case "subset": if (set != null) { - foreach (UtilCollection item in map[term]) + foreach (UtilCollection item in map[term]) + { + if (!set.isMember(item)) { - if (!set.isMember(item)) - { - throw new Exception($"Constraint {term} {definition} PLACEHOLDER failed because {item} was not a subset of PLACEHOLDER"); - } + throw new Exception($"Constraint {term} {definition} PLACEHOLDER failed because {item} was not a subset of PLACEHOLDER"); } + } } break; } diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index 79432a4..1df471e 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -10,7 +10,7 @@ namespace SPADE.Grammar.descriptor; class SetDescriptor { - public Func isMember; + public Func isMember; public Func genSet; @@ -23,7 +23,7 @@ public SetDescriptor(Dictionary _map) genSet = () => new UtilCollection(""); } - public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) + public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) { map = _map; isMember = _isMember; @@ -60,7 +60,7 @@ public SetDescriptor(Dictionary _map, FormatDescriptor f { constraintDescriptor.check(map); } - return true; + return true; }; genSet = () => throw new NotImplementedException("generating every set descriptor not implemented"); @@ -82,7 +82,7 @@ public SetDescriptor Cross(SetDescriptor other) public SetDescriptor UnorderedCross(SetDescriptor other) { - Func _isMember = (a) => + Func _isMember = (a) => { foreach (UtilCollection item in a) { @@ -91,10 +91,10 @@ public SetDescriptor UnorderedCross(SetDescriptor other) { return false; } - } + } return true; }; - Func _genSet = () => + Func _genSet = () => { UtilCollection set = new("{}"); foreach (UtilCollection first in genSet()) @@ -114,7 +114,7 @@ public SetDescriptor UnorderedCross(SetDescriptor other) public SetDescriptor Union(SetDescriptor other) { - + Func _isMember = (a) => isMember(a) || other.isMember(a); Func _genSet = () => genSet().Union(other.genSet()); return new SetDescriptor(map, _isMember, _genSet); diff --git a/grammar/desciptors/formatDescriptor.cs b/grammar/desciptors/formatDescriptor.cs index d86f1ab..a60adfa 100644 --- a/grammar/desciptors/formatDescriptor.cs +++ b/grammar/desciptors/formatDescriptor.cs @@ -10,7 +10,7 @@ class FormatDescriptor public FormatDescriptor(String term) { - parseFunc = (map, coll) => {map[term] = coll;}; + parseFunc = (map, coll) => { map[term] = coll; }; } public FormatDescriptor(List lst) @@ -19,7 +19,7 @@ public FormatDescriptor(List lst) { for (int i = 0; i < lst.Count(); i++) { - lst[i].parseFunc(map, coll[i]); + lst[i].parseFunc(map, coll[i]); } }; } From e895be27aeaf92ce00cc436ebfe66bee794aa63c Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:21:25 -0600 Subject: [PATCH 16/35] test: add the test project to the solution and collect coverage SPADE.slnx now names tests/SPADE.Tests, so rbs's unit-test stage runs it. coverlet.collector writes the cobertura report the coverage floor reads; without it the project gets no coverage gate at all. Same version as Redux's test project. --- SPADE.slnx | 3 +++ rbs.toml | 2 +- tests/SPADE.Tests/SPADE.Tests.csproj | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/SPADE.slnx b/SPADE.slnx index b5c1ef7..13d1b80 100644 --- a/SPADE.slnx +++ b/SPADE.slnx @@ -1,3 +1,6 @@ + + + diff --git a/rbs.toml b/rbs.toml index cfbb4f0..cc44659 100644 --- a/rbs.toml +++ b/rbs.toml @@ -5,5 +5,5 @@ engine = "dotnet" solution = "SPADE.slnx" [unit-test] -# The standard we're holding to. Floor 70%; the goal is 80%+. New code needs tests to keep it above the floor. +# The standard we're holding to. Floor 70%; the goal is 80%+. Line coverage is about 8% (Oct 2026, UtilCollection equality tests only); new code needs tests to reach the floor. coverage-min = 70 diff --git a/tests/SPADE.Tests/SPADE.Tests.csproj b/tests/SPADE.Tests/SPADE.Tests.csproj index 1ff3916..7e362f8 100644 --- a/tests/SPADE.Tests/SPADE.Tests.csproj +++ b/tests/SPADE.Tests/SPADE.Tests.csproj @@ -20,6 +20,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + From 8f68aa39bd3e3b888fe06b3516d52f817a1072c1 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:27:28 -0600 Subject: [PATCH 17/35] style: run dotnet format Whitespace only: dotnet format SPADE.slnx on this branch's code. --- StringParser.cs | 7 +++-- UtilCollection.cs | 28 +++++++++--------- grammar/BuiltInSet.cs | 3 +- grammar/codeGen.cs | 9 +++--- grammar/desciptors/ConstraintDescriptor.cs | 34 +++++++++++----------- grammar/desciptors/SetDescriptor.cs | 14 ++++----- grammar/desciptors/formatDescriptor.cs | 4 +-- 7 files changed, 52 insertions(+), 47 deletions(-) diff --git a/StringParser.cs b/StringParser.cs index 23fb6a1..ead7bc7 100644 --- a/StringParser.cs +++ b/StringParser.cs @@ -7,11 +7,12 @@ using SPADE.Grammar; namespace SPADE; + public class StringParser { readonly string format; public Dictionary map = new(); - + private bool hasParsed; public StringParser(string f) @@ -24,7 +25,7 @@ public void parse(string instance) { hasParsed = true; AntlrInputStream stream = new AntlrInputStream(format); - collLexer lex = new collLexer(stream); + collLexer lex = new collLexer(stream); CommonTokenStream tokens = new CommonTokenStream(lex); collParser parser = new collParser(tokens); @@ -35,7 +36,7 @@ public void parse(string instance) gen.Parse(new UtilCollection(instance)); - map = gen.map; + map = gen.map; } diff --git a/UtilCollection.cs b/UtilCollection.cs index 6a70fe7..6dfd345 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -47,7 +47,7 @@ public UtilCollection(HashSet set_) set = new(); foreach (string i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -57,7 +57,7 @@ public UtilCollection(HashSet> set_) set = new(); foreach (HashSet i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -75,19 +75,21 @@ public UtilCollection(HashSet set__) public UtilCollection this[int index] { - get { + get + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - return list[index]; - } - set { + return list[index]; + } + set + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - list[index] = value; - } + list[index] = value; + } } public void Add(UtilCollection item) { - if(isOrdered) + if (isOrdered) { list.Add(item); } @@ -133,7 +135,7 @@ public void assertOrdered() public void assertUnordered() { - if(isOrdered) throw new InvalidOperationException("Ordered"); + if (isOrdered) throw new InvalidOperationException("Ordered"); } public bool IsUnordered() @@ -190,7 +192,7 @@ public UtilCollection Intersect(UtilCollection other) throw new NotImplementedException(); } } - + public UtilCollection Union(UtilCollection other) { if (isOrdered != other.isOrdered) throw new InvalidOperationException("Can't take the union of a set and a list"); @@ -209,9 +211,9 @@ public UtilCollection Cross(UtilCollection other) UtilCollection ret = new UtilCollection(); foreach (UtilCollection a in this) { - foreach(UtilCollection b in other) + foreach (UtilCollection b in other) { - UtilCollection add = new UtilCollection(new List{a,b}); + UtilCollection add = new UtilCollection(new List { a, b }); ret.Add(add); } } diff --git a/grammar/BuiltInSet.cs b/grammar/BuiltInSet.cs index 948aba7..3c48b97 100644 --- a/grammar/BuiltInSet.cs +++ b/grammar/BuiltInSet.cs @@ -1,2 +1,3 @@ namespace SPADE.Grammar.Enums; -enum BuiltInSet {integers} \ No newline at end of file + +enum BuiltInSet { integers } \ No newline at end of file diff --git a/grammar/codeGen.cs b/grammar/codeGen.cs index 9aaa8f0..76b9934 100644 --- a/grammar/codeGen.cs +++ b/grammar/codeGen.cs @@ -10,18 +10,19 @@ namespace SPADE.Grammar; -class codeGen : IcollListener { +class codeGen : IcollListener +{ public Dictionary map; public Stack formatStack; - public SetDescriptor ?setDescriptor; + public SetDescriptor? setDescriptor; public Stack setDescriptorStack; public Stack constraintDescriptorStack; public Stack> constraintDescriptorStackStack; - public codeGen() : base() + public codeGen() : base() { map = new(); formatStack = new(); @@ -94,7 +95,7 @@ public void ExitFormat([NotNull] collParser.FormatContext context) List lst = new(); for (int i = 0; i < context.format().Length; i++) { - lst.Add(formatStack.Pop()); + lst.Add(formatStack.Pop()); } lst.Reverse(); // have to reverse the list, since the leftmost should be the one that was popped first formatStack.Push(new FormatDescriptor(lst, SourceText(context))); diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 1cbd7c8..254d010 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -13,18 +13,18 @@ class ConstraintDescriptor public ConstraintDescriptor(String _term, String _definition, SetDescriptor _set, String _text) { - term = _term; - definition = _definition; - set = _set; - text = _text; + term = _term; + definition = _definition; + set = _set; + text = _text; } public ConstraintDescriptor(String _term, String _definition, String _built_in, String _text) { - term = _term; - definition = _definition; - built_in = _built_in; - text = _text; + term = _term; + definition = _definition; + built_in = _built_in; + text = _text; } // Values in error messages are shortened, so a huge instance doesn't make a huge message. @@ -73,17 +73,17 @@ public void check(Dictionary map) case "subset": if (set != null) { - if (map[term].IsValue()) - { - throw Failure($"{Describe(map[term])} is not a set"); - } - foreach (UtilCollection item in map[term]) + if (map[term].IsValue()) + { + throw Failure($"{Describe(map[term])} is not a set"); + } + foreach (UtilCollection item in map[term]) + { + if (!set.isMember(item)) { - if (!set.isMember(item)) - { - throw Failure($"{Describe(item)} is not a member"); - } + throw Failure($"{Describe(item)} is not a member"); } + } } break; } diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index 282cb65..e2607d3 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -10,7 +10,7 @@ namespace SPADE.Grammar.descriptor; class SetDescriptor { - public Func isMember; + public Func isMember; public Func genSet; @@ -23,7 +23,7 @@ public SetDescriptor(Dictionary _map) genSet = () => new UtilCollection(""); } - public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) + public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) { map = _map; isMember = _isMember; @@ -60,7 +60,7 @@ public SetDescriptor(Dictionary _map, FormatDescriptor f { constraintDescriptor.check(map); } - return true; + return true; }; genSet = () => throw new NotImplementedException("generating every set descriptor not implemented"); @@ -83,7 +83,7 @@ public SetDescriptor Cross(SetDescriptor other) public SetDescriptor UnorderedCross(SetDescriptor other) { - Func _isMember = (a) => + Func _isMember = (a) => { // Each member is a set of nodes, like the edge {a,b}. A list such as (a,b) // is an ordered pair and belongs to cross, and a plain value is not an edge. @@ -95,10 +95,10 @@ public SetDescriptor UnorderedCross(SetDescriptor other) { return false; } - } + } return true; }; - Func _genSet = () => + Func _genSet = () => { UtilCollection set = new("{}"); foreach (UtilCollection first in genSet()) @@ -118,7 +118,7 @@ public SetDescriptor UnorderedCross(SetDescriptor other) public SetDescriptor Union(SetDescriptor other) { - + Func _isMember = (a) => isMember(a) || other.isMember(a); Func _genSet = () => genSet().Union(other.genSet()); return new SetDescriptor(map, _isMember, _genSet); diff --git a/grammar/desciptors/formatDescriptor.cs b/grammar/desciptors/formatDescriptor.cs index bdce081..6198412 100644 --- a/grammar/desciptors/formatDescriptor.cs +++ b/grammar/desciptors/formatDescriptor.cs @@ -10,7 +10,7 @@ class FormatDescriptor public FormatDescriptor(String term) { - parseFunc = (map, coll) => {map[term] = coll;}; + parseFunc = (map, coll) => { map[term] = coll; }; } public FormatDescriptor(List lst, String text) @@ -23,7 +23,7 @@ public FormatDescriptor(List lst, String text) } for (int i = 0; i < lst.Count(); i++) { - lst[i].parseFunc(map, coll[i]); + lst[i].parseFunc(map, coll[i]); } }; } From 920a883b1f90625e3e2e2fa385765373581f658a Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:28:28 -0600 Subject: [PATCH 18/35] style: run dotnet format Whitespace only: dotnet format SPADE.slnx on this branch's code. --- StringParser.cs | 7 +++-- UtilCollection.cs | 28 +++++++++--------- grammar/BuiltInSet.cs | 3 +- grammar/codeGen.cs | 9 +++--- grammar/desciptors/ConstraintDescriptor.cs | 34 +++++++++++----------- grammar/desciptors/SetDescriptor.cs | 14 ++++----- grammar/desciptors/formatDescriptor.cs | 4 +-- 7 files changed, 52 insertions(+), 47 deletions(-) diff --git a/StringParser.cs b/StringParser.cs index 23fb6a1..ead7bc7 100644 --- a/StringParser.cs +++ b/StringParser.cs @@ -7,11 +7,12 @@ using SPADE.Grammar; namespace SPADE; + public class StringParser { readonly string format; public Dictionary map = new(); - + private bool hasParsed; public StringParser(string f) @@ -24,7 +25,7 @@ public void parse(string instance) { hasParsed = true; AntlrInputStream stream = new AntlrInputStream(format); - collLexer lex = new collLexer(stream); + collLexer lex = new collLexer(stream); CommonTokenStream tokens = new CommonTokenStream(lex); collParser parser = new collParser(tokens); @@ -35,7 +36,7 @@ public void parse(string instance) gen.Parse(new UtilCollection(instance)); - map = gen.map; + map = gen.map; } diff --git a/UtilCollection.cs b/UtilCollection.cs index 927fe77..5923bf3 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -64,7 +64,7 @@ public UtilCollection(HashSet set_) set = new(); foreach (string i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -74,7 +74,7 @@ public UtilCollection(HashSet> set_) set = new(); foreach (HashSet i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -92,14 +92,16 @@ public UtilCollection(HashSet set__) public UtilCollection this[int index] { - get { + get + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - return list[index]; - } - set { + return list[index]; + } + set + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - list[index] = value; - } + list[index] = value; + } } public void Add(UtilCollection item) @@ -109,7 +111,7 @@ public void Add(UtilCollection item) list.Add(item); counts![item] = CountOf(item) + 1; } - else if(isOrdered) + else if (isOrdered) { list.Add(item); } @@ -156,7 +158,7 @@ public void assertOrdered() public void assertUnordered() { - if(isOrdered) throw new InvalidOperationException("Ordered"); + if (isOrdered) throw new InvalidOperationException("Ordered"); } public bool IsUnordered() @@ -272,7 +274,7 @@ public UtilCollection Intersect(UtilCollection other) throw new NotImplementedException(); } } - + public UtilCollection Union(UtilCollection other) { if (isOrdered != other.isOrdered) throw new InvalidOperationException("Can't take the union of a set and a list"); @@ -303,9 +305,9 @@ public UtilCollection Cross(UtilCollection other) UtilCollection ret = new UtilCollection(); foreach (UtilCollection a in this) { - foreach(UtilCollection b in other) + foreach (UtilCollection b in other) { - UtilCollection add = new UtilCollection(new List{a,b}); + UtilCollection add = new UtilCollection(new List { a, b }); ret.Add(add); } } diff --git a/grammar/BuiltInSet.cs b/grammar/BuiltInSet.cs index 948aba7..3c48b97 100644 --- a/grammar/BuiltInSet.cs +++ b/grammar/BuiltInSet.cs @@ -1,2 +1,3 @@ namespace SPADE.Grammar.Enums; -enum BuiltInSet {integers} \ No newline at end of file + +enum BuiltInSet { integers } \ No newline at end of file diff --git a/grammar/codeGen.cs b/grammar/codeGen.cs index 9aaa8f0..76b9934 100644 --- a/grammar/codeGen.cs +++ b/grammar/codeGen.cs @@ -10,18 +10,19 @@ namespace SPADE.Grammar; -class codeGen : IcollListener { +class codeGen : IcollListener +{ public Dictionary map; public Stack formatStack; - public SetDescriptor ?setDescriptor; + public SetDescriptor? setDescriptor; public Stack setDescriptorStack; public Stack constraintDescriptorStack; public Stack> constraintDescriptorStackStack; - public codeGen() : base() + public codeGen() : base() { map = new(); formatStack = new(); @@ -94,7 +95,7 @@ public void ExitFormat([NotNull] collParser.FormatContext context) List lst = new(); for (int i = 0; i < context.format().Length; i++) { - lst.Add(formatStack.Pop()); + lst.Add(formatStack.Pop()); } lst.Reverse(); // have to reverse the list, since the leftmost should be the one that was popped first formatStack.Push(new FormatDescriptor(lst, SourceText(context))); diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 1459e98..4c64106 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -13,18 +13,18 @@ class ConstraintDescriptor public ConstraintDescriptor(String _term, String _definition, SetDescriptor _set, String _text) { - term = _term; - definition = _definition; - set = _set; - text = _text; + term = _term; + definition = _definition; + set = _set; + text = _text; } public ConstraintDescriptor(String _term, String _definition, String _built_in, String _text) { - term = _term; - definition = _definition; - built_in = _built_in; - text = _text; + term = _term; + definition = _definition; + built_in = _built_in; + text = _text; } // Values in error messages are shortened, so a huge instance doesn't make a huge message. @@ -83,17 +83,17 @@ public void check(Dictionary map) case "subset": if (set != null) { - if (map[term].IsValue()) - { - throw Failure($"{Describe(map[term])} is not a set"); - } - foreach (UtilCollection item in map[term]) + if (map[term].IsValue()) + { + throw Failure($"{Describe(map[term])} is not a set"); + } + foreach (UtilCollection item in map[term]) + { + if (!set.isMember(item)) { - if (!set.isMember(item)) - { - throw Failure($"{Describe(item)} is not a member"); - } + throw Failure($"{Describe(item)} is not a member"); } + } } break; } diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index 9d755e4..a63a163 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -10,7 +10,7 @@ namespace SPADE.Grammar.descriptor; class SetDescriptor { - public Func isMember; + public Func isMember; public Func genSet; @@ -23,7 +23,7 @@ public SetDescriptor(Dictionary _map) genSet = () => new UtilCollection(""); } - public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) + public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) { map = _map; isMember = _isMember; @@ -62,7 +62,7 @@ public SetDescriptor(Dictionary _map, FormatDescriptor f { constraintDescriptor.check(map); } - return true; + return true; }; genSet = () => throw new NotImplementedException("generating every set descriptor not implemented"); @@ -85,7 +85,7 @@ public SetDescriptor Cross(SetDescriptor other) public SetDescriptor UnorderedCross(SetDescriptor other) { - Func _isMember = (a) => + Func _isMember = (a) => { // Each member is a set of nodes, like the edge {a,b}. A list such as (a,b) // is an ordered pair and belongs to cross, and a plain value is not an edge. @@ -97,10 +97,10 @@ public SetDescriptor UnorderedCross(SetDescriptor other) { return false; } - } + } return true; }; - Func _genSet = () => + Func _genSet = () => { UtilCollection set = new("{}"); foreach (UtilCollection first in genSet()) @@ -120,7 +120,7 @@ public SetDescriptor UnorderedCross(SetDescriptor other) public SetDescriptor Union(SetDescriptor other) { - + Func _isMember = (a) => isMember(a) || other.isMember(a); Func _genSet = () => genSet().Union(other.genSet()); return new SetDescriptor(map, _isMember, _genSet); diff --git a/grammar/desciptors/formatDescriptor.cs b/grammar/desciptors/formatDescriptor.cs index bdce081..6198412 100644 --- a/grammar/desciptors/formatDescriptor.cs +++ b/grammar/desciptors/formatDescriptor.cs @@ -10,7 +10,7 @@ class FormatDescriptor public FormatDescriptor(String term) { - parseFunc = (map, coll) => {map[term] = coll;}; + parseFunc = (map, coll) => { map[term] = coll; }; } public FormatDescriptor(List lst, String text) @@ -23,7 +23,7 @@ public FormatDescriptor(List lst, String text) } for (int i = 0; i < lst.Count(); i++) { - lst[i].parseFunc(map, coll[i]); + lst[i].parseFunc(map, coll[i]); } }; } From 65be2724581b7b7db06aabbbacdac20c6b03f7d1 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:29:30 -0600 Subject: [PATCH 19/35] style: run dotnet format Whitespace only: dotnet format SPADE.slnx on this branch's code. --- StringParser.cs | 7 +++-- UtilCollection.cs | 28 +++++++++--------- grammar/BuiltInSet.cs | 3 +- grammar/codeGen.cs | 9 +++--- grammar/desciptors/ConstraintDescriptor.cs | 34 +++++++++++----------- grammar/desciptors/SetDescriptor.cs | 14 ++++----- grammar/desciptors/formatDescriptor.cs | 4 +-- 7 files changed, 52 insertions(+), 47 deletions(-) diff --git a/StringParser.cs b/StringParser.cs index 638630e..13e68ad 100644 --- a/StringParser.cs +++ b/StringParser.cs @@ -7,11 +7,12 @@ using SPADE.Grammar; namespace SPADE; + public class StringParser { readonly string format; public Dictionary map = new(); - + private bool hasParsed; private DefinitionDescription? definition; @@ -32,7 +33,7 @@ public void parse(string instance) { hasParsed = true; AntlrInputStream stream = new AntlrInputStream(format); - collLexer lex = new collLexer(stream); + collLexer lex = new collLexer(stream); CommonTokenStream tokens = new CommonTokenStream(lex); collParser parser = new collParser(tokens); @@ -43,7 +44,7 @@ public void parse(string instance) gen.Parse(new UtilCollection(instance)); - map = gen.map; + map = gen.map; } diff --git a/UtilCollection.cs b/UtilCollection.cs index 927fe77..5923bf3 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -64,7 +64,7 @@ public UtilCollection(HashSet set_) set = new(); foreach (string i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -74,7 +74,7 @@ public UtilCollection(HashSet> set_) set = new(); foreach (HashSet i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -92,14 +92,16 @@ public UtilCollection(HashSet set__) public UtilCollection this[int index] { - get { + get + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - return list[index]; - } - set { + return list[index]; + } + set + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - list[index] = value; - } + list[index] = value; + } } public void Add(UtilCollection item) @@ -109,7 +111,7 @@ public void Add(UtilCollection item) list.Add(item); counts![item] = CountOf(item) + 1; } - else if(isOrdered) + else if (isOrdered) { list.Add(item); } @@ -156,7 +158,7 @@ public void assertOrdered() public void assertUnordered() { - if(isOrdered) throw new InvalidOperationException("Ordered"); + if (isOrdered) throw new InvalidOperationException("Ordered"); } public bool IsUnordered() @@ -272,7 +274,7 @@ public UtilCollection Intersect(UtilCollection other) throw new NotImplementedException(); } } - + public UtilCollection Union(UtilCollection other) { if (isOrdered != other.isOrdered) throw new InvalidOperationException("Can't take the union of a set and a list"); @@ -303,9 +305,9 @@ public UtilCollection Cross(UtilCollection other) UtilCollection ret = new UtilCollection(); foreach (UtilCollection a in this) { - foreach(UtilCollection b in other) + foreach (UtilCollection b in other) { - UtilCollection add = new UtilCollection(new List{a,b}); + UtilCollection add = new UtilCollection(new List { a, b }); ret.Add(add); } } diff --git a/grammar/BuiltInSet.cs b/grammar/BuiltInSet.cs index 948aba7..3c48b97 100644 --- a/grammar/BuiltInSet.cs +++ b/grammar/BuiltInSet.cs @@ -1,2 +1,3 @@ namespace SPADE.Grammar.Enums; -enum BuiltInSet {integers} \ No newline at end of file + +enum BuiltInSet { integers } \ No newline at end of file diff --git a/grammar/codeGen.cs b/grammar/codeGen.cs index 9aaa8f0..76b9934 100644 --- a/grammar/codeGen.cs +++ b/grammar/codeGen.cs @@ -10,18 +10,19 @@ namespace SPADE.Grammar; -class codeGen : IcollListener { +class codeGen : IcollListener +{ public Dictionary map; public Stack formatStack; - public SetDescriptor ?setDescriptor; + public SetDescriptor? setDescriptor; public Stack setDescriptorStack; public Stack constraintDescriptorStack; public Stack> constraintDescriptorStackStack; - public codeGen() : base() + public codeGen() : base() { map = new(); formatStack = new(); @@ -94,7 +95,7 @@ public void ExitFormat([NotNull] collParser.FormatContext context) List lst = new(); for (int i = 0; i < context.format().Length; i++) { - lst.Add(formatStack.Pop()); + lst.Add(formatStack.Pop()); } lst.Reverse(); // have to reverse the list, since the leftmost should be the one that was popped first formatStack.Push(new FormatDescriptor(lst, SourceText(context))); diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 1459e98..4c64106 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -13,18 +13,18 @@ class ConstraintDescriptor public ConstraintDescriptor(String _term, String _definition, SetDescriptor _set, String _text) { - term = _term; - definition = _definition; - set = _set; - text = _text; + term = _term; + definition = _definition; + set = _set; + text = _text; } public ConstraintDescriptor(String _term, String _definition, String _built_in, String _text) { - term = _term; - definition = _definition; - built_in = _built_in; - text = _text; + term = _term; + definition = _definition; + built_in = _built_in; + text = _text; } // Values in error messages are shortened, so a huge instance doesn't make a huge message. @@ -83,17 +83,17 @@ public void check(Dictionary map) case "subset": if (set != null) { - if (map[term].IsValue()) - { - throw Failure($"{Describe(map[term])} is not a set"); - } - foreach (UtilCollection item in map[term]) + if (map[term].IsValue()) + { + throw Failure($"{Describe(map[term])} is not a set"); + } + foreach (UtilCollection item in map[term]) + { + if (!set.isMember(item)) { - if (!set.isMember(item)) - { - throw Failure($"{Describe(item)} is not a member"); - } + throw Failure($"{Describe(item)} is not a member"); } + } } break; } diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index 9d755e4..a63a163 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -10,7 +10,7 @@ namespace SPADE.Grammar.descriptor; class SetDescriptor { - public Func isMember; + public Func isMember; public Func genSet; @@ -23,7 +23,7 @@ public SetDescriptor(Dictionary _map) genSet = () => new UtilCollection(""); } - public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) + public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) { map = _map; isMember = _isMember; @@ -62,7 +62,7 @@ public SetDescriptor(Dictionary _map, FormatDescriptor f { constraintDescriptor.check(map); } - return true; + return true; }; genSet = () => throw new NotImplementedException("generating every set descriptor not implemented"); @@ -85,7 +85,7 @@ public SetDescriptor Cross(SetDescriptor other) public SetDescriptor UnorderedCross(SetDescriptor other) { - Func _isMember = (a) => + Func _isMember = (a) => { // Each member is a set of nodes, like the edge {a,b}. A list such as (a,b) // is an ordered pair and belongs to cross, and a plain value is not an edge. @@ -97,10 +97,10 @@ public SetDescriptor UnorderedCross(SetDescriptor other) { return false; } - } + } return true; }; - Func _genSet = () => + Func _genSet = () => { UtilCollection set = new("{}"); foreach (UtilCollection first in genSet()) @@ -120,7 +120,7 @@ public SetDescriptor UnorderedCross(SetDescriptor other) public SetDescriptor Union(SetDescriptor other) { - + Func _isMember = (a) => isMember(a) || other.isMember(a); Func _genSet = () => genSet().Union(other.genSet()); return new SetDescriptor(map, _isMember, _genSet); diff --git a/grammar/desciptors/formatDescriptor.cs b/grammar/desciptors/formatDescriptor.cs index bdce081..6198412 100644 --- a/grammar/desciptors/formatDescriptor.cs +++ b/grammar/desciptors/formatDescriptor.cs @@ -10,7 +10,7 @@ class FormatDescriptor public FormatDescriptor(String term) { - parseFunc = (map, coll) => {map[term] = coll;}; + parseFunc = (map, coll) => { map[term] = coll; }; } public FormatDescriptor(List lst, String text) @@ -23,7 +23,7 @@ public FormatDescriptor(List lst, String text) } for (int i = 0; i < lst.Count(); i++) { - lst[i].parseFunc(map, coll[i]); + lst[i].parseFunc(map, coll[i]); } }; } From d25879548358510d6091d5033779f5aec376f924 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:30:37 -0600 Subject: [PATCH 20/35] style: run dotnet format Whitespace only: dotnet format SPADE.slnx on this branch's code. --- StringParser.cs | 7 +-- UtilCollection.cs | 28 ++++++----- grammar/BuiltInSet.cs | 3 +- grammar/codeGen.cs | 9 ++-- grammar/desciptors/ConstraintDescriptor.cs | 56 +++++++++++----------- grammar/desciptors/SetDescriptor.cs | 16 +++---- grammar/desciptors/formatDescriptor.cs | 4 +- 7 files changed, 64 insertions(+), 59 deletions(-) diff --git a/StringParser.cs b/StringParser.cs index 638630e..13e68ad 100644 --- a/StringParser.cs +++ b/StringParser.cs @@ -7,11 +7,12 @@ using SPADE.Grammar; namespace SPADE; + public class StringParser { readonly string format; public Dictionary map = new(); - + private bool hasParsed; private DefinitionDescription? definition; @@ -32,7 +33,7 @@ public void parse(string instance) { hasParsed = true; AntlrInputStream stream = new AntlrInputStream(format); - collLexer lex = new collLexer(stream); + collLexer lex = new collLexer(stream); CommonTokenStream tokens = new CommonTokenStream(lex); collParser parser = new collParser(tokens); @@ -43,7 +44,7 @@ public void parse(string instance) gen.Parse(new UtilCollection(instance)); - map = gen.map; + map = gen.map; } diff --git a/UtilCollection.cs b/UtilCollection.cs index 927fe77..5923bf3 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -64,7 +64,7 @@ public UtilCollection(HashSet set_) set = new(); foreach (string i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -74,7 +74,7 @@ public UtilCollection(HashSet> set_) set = new(); foreach (HashSet i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -92,14 +92,16 @@ public UtilCollection(HashSet set__) public UtilCollection this[int index] { - get { + get + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - return list[index]; - } - set { + return list[index]; + } + set + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - list[index] = value; - } + list[index] = value; + } } public void Add(UtilCollection item) @@ -109,7 +111,7 @@ public void Add(UtilCollection item) list.Add(item); counts![item] = CountOf(item) + 1; } - else if(isOrdered) + else if (isOrdered) { list.Add(item); } @@ -156,7 +158,7 @@ public void assertOrdered() public void assertUnordered() { - if(isOrdered) throw new InvalidOperationException("Ordered"); + if (isOrdered) throw new InvalidOperationException("Ordered"); } public bool IsUnordered() @@ -272,7 +274,7 @@ public UtilCollection Intersect(UtilCollection other) throw new NotImplementedException(); } } - + public UtilCollection Union(UtilCollection other) { if (isOrdered != other.isOrdered) throw new InvalidOperationException("Can't take the union of a set and a list"); @@ -303,9 +305,9 @@ public UtilCollection Cross(UtilCollection other) UtilCollection ret = new UtilCollection(); foreach (UtilCollection a in this) { - foreach(UtilCollection b in other) + foreach (UtilCollection b in other) { - UtilCollection add = new UtilCollection(new List{a,b}); + UtilCollection add = new UtilCollection(new List { a, b }); ret.Add(add); } } diff --git a/grammar/BuiltInSet.cs b/grammar/BuiltInSet.cs index 948aba7..3c48b97 100644 --- a/grammar/BuiltInSet.cs +++ b/grammar/BuiltInSet.cs @@ -1,2 +1,3 @@ namespace SPADE.Grammar.Enums; -enum BuiltInSet {integers} \ No newline at end of file + +enum BuiltInSet { integers } \ No newline at end of file diff --git a/grammar/codeGen.cs b/grammar/codeGen.cs index 9aaa8f0..76b9934 100644 --- a/grammar/codeGen.cs +++ b/grammar/codeGen.cs @@ -10,18 +10,19 @@ namespace SPADE.Grammar; -class codeGen : IcollListener { +class codeGen : IcollListener +{ public Dictionary map; public Stack formatStack; - public SetDescriptor ?setDescriptor; + public SetDescriptor? setDescriptor; public Stack setDescriptorStack; public Stack constraintDescriptorStack; public Stack> constraintDescriptorStackStack; - public codeGen() : base() + public codeGen() : base() { map = new(); formatStack = new(); @@ -94,7 +95,7 @@ public void ExitFormat([NotNull] collParser.FormatContext context) List lst = new(); for (int i = 0; i < context.format().Length; i++) { - lst.Add(formatStack.Pop()); + lst.Add(formatStack.Pop()); } lst.Reverse(); // have to reverse the list, since the leftmost should be the one that was popped first formatStack.Push(new FormatDescriptor(lst, SourceText(context))); diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 427fc33..00ef65e 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -13,18 +13,18 @@ class ConstraintDescriptor public ConstraintDescriptor(String _term, String _definition, SetDescriptor _set, String _text) { - term = _term; - definition = _definition; - set = _set; - text = _text; + term = _term; + definition = _definition; + set = _set; + text = _text; } public ConstraintDescriptor(String _term, String _definition, String _built_in, String _text) { - term = _term; - definition = _definition; - built_in = _built_in; - text = _text; + term = _term; + definition = _definition; + built_in = _built_in; + text = _text; } // Values in error messages are shortened, so a huge instance doesn't make a huge message. @@ -91,30 +91,30 @@ public void check(Dictionary map) case "subset": if (set != null) { - if (map[term].IsValue()) + if (map[term].IsValue()) + { + throw Failure($"{Describe(map[term])} is not a set", map[term]); + } + foreach (UtilCollection item in map[term]) + { + bool member; + try { - throw Failure($"{Describe(map[term])} is not a set", map[term]); + member = set.isMember(item); } - foreach (UtilCollection item in map[term]) + catch (SpadeParseException e) { - bool member; - try - { - member = set.isMember(item); - } - catch (SpadeParseException e) - { - // A constraint inside a nested set failed, such as w is int in - // {(e,w) | ...}. The message stays, but the error belongs to this part - // and item. A missing piece is dropped if its part is a nested name. - bool nested = e.ExpectedIn == null || set.declaredNames.Contains(e.ExpectedIn); - throw e.BelongingTo(term, item.ToString(), nested ? null : e.MissingValue, nested ? null : e.ExpectedIn); - } - if (!member) - { - throw Failure($"{Describe(item)} is not a member", item, set.FindMissing(item)); - } + // A constraint inside a nested set failed, such as w is int in + // {(e,w) | ...}. The message stays, but the error belongs to this part + // and item. A missing piece is dropped if its part is a nested name. + bool nested = e.ExpectedIn == null || set.declaredNames.Contains(e.ExpectedIn); + throw e.BelongingTo(term, item.ToString(), nested ? null : e.MissingValue, nested ? null : e.ExpectedIn); } + if (!member) + { + throw Failure($"{Describe(item)} is not a member", item, set.FindMissing(item)); + } + } } break; } diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index 3e71df3..5e10958 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -10,7 +10,7 @@ namespace SPADE.Grammar.descriptor; class SetDescriptor { - public Func isMember; + public Func isMember; public Func genSet; @@ -31,7 +31,7 @@ public SetDescriptor(Dictionary _map) genSet = () => new UtilCollection(""); } - public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) + public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) { map = _map; isMember = _isMember; @@ -39,7 +39,7 @@ public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet, Func _missing) + private SetDescriptor(SetDescriptor left, SetDescriptor right, Func _isMember, Func _genSet, Func _missing) : this(left.map, _isMember, _genSet) { missing = _missing; @@ -81,7 +81,7 @@ public SetDescriptor(Dictionary _map, FormatDescriptor f { constraintDescriptor.check(map); } - return true; + return true; }; genSet = () => throw new NotImplementedException("generating every set descriptor not implemented"); @@ -127,7 +127,7 @@ public SetDescriptor Cross(SetDescriptor other) public SetDescriptor UnorderedCross(SetDescriptor other) { - Func _isMember = (a) => + Func _isMember = (a) => { // Each member is a set of nodes, like the edge {a,b}. A list such as (a,b) // is an ordered pair and belongs to cross, and a plain value is not an edge. @@ -139,10 +139,10 @@ public SetDescriptor UnorderedCross(SetDescriptor other) { return false; } - } + } return true; }; - Func _genSet = () => + Func _genSet = () => { UtilCollection set = new("{}"); foreach (UtilCollection first in genSet()) @@ -174,7 +174,7 @@ public SetDescriptor UnorderedCross(SetDescriptor other) public SetDescriptor Union(SetDescriptor other) { - + Func _isMember = (a) => isMember(a) || other.isMember(a); Func _genSet = () => genSet().Union(other.genSet()); // Missing from both sides; the left one is named. diff --git a/grammar/desciptors/formatDescriptor.cs b/grammar/desciptors/formatDescriptor.cs index 79321e7..ce9faeb 100644 --- a/grammar/desciptors/formatDescriptor.cs +++ b/grammar/desciptors/formatDescriptor.cs @@ -14,7 +14,7 @@ class FormatDescriptor public FormatDescriptor(String term) { names.Add(term); - parseFunc = (map, coll) => {map[term] = coll;}; + parseFunc = (map, coll) => { map[term] = coll; }; } public FormatDescriptor(List lst, String text) @@ -28,7 +28,7 @@ public FormatDescriptor(List lst, String text) } for (int i = 0; i < lst.Count(); i++) { - lst[i].parseFunc(map, coll[i]); + lst[i].parseFunc(map, coll[i]); } }; } From 485dac3434ec9a56b79034a158c765028a2d6be0 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:31:39 -0600 Subject: [PATCH 21/35] style: run dotnet format Whitespace only: dotnet format SPADE.slnx on this branch's code. --- StringParser.cs | 7 +-- UtilCollection.cs | 28 ++++++----- grammar/BuiltInSet.cs | 3 +- grammar/codeGen.cs | 9 ++-- grammar/desciptors/ConstraintDescriptor.cs | 56 +++++++++++----------- grammar/desciptors/SetDescriptor.cs | 16 +++---- grammar/desciptors/formatDescriptor.cs | 4 +- 7 files changed, 64 insertions(+), 59 deletions(-) diff --git a/StringParser.cs b/StringParser.cs index 638630e..13e68ad 100644 --- a/StringParser.cs +++ b/StringParser.cs @@ -7,11 +7,12 @@ using SPADE.Grammar; namespace SPADE; + public class StringParser { readonly string format; public Dictionary map = new(); - + private bool hasParsed; private DefinitionDescription? definition; @@ -32,7 +33,7 @@ public void parse(string instance) { hasParsed = true; AntlrInputStream stream = new AntlrInputStream(format); - collLexer lex = new collLexer(stream); + collLexer lex = new collLexer(stream); CommonTokenStream tokens = new CommonTokenStream(lex); collParser parser = new collParser(tokens); @@ -43,7 +44,7 @@ public void parse(string instance) gen.Parse(new UtilCollection(instance)); - map = gen.map; + map = gen.map; } diff --git a/UtilCollection.cs b/UtilCollection.cs index 6b47c94..4d72dfc 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -64,7 +64,7 @@ public UtilCollection(HashSet set_) set = new(); foreach (string i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -74,7 +74,7 @@ public UtilCollection(HashSet> set_) set = new(); foreach (HashSet i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -92,14 +92,16 @@ public UtilCollection(HashSet set__) public UtilCollection this[int index] { - get { + get + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - return list[index]; - } - set { + return list[index]; + } + set + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - list[index] = value; - } + list[index] = value; + } } public void Add(UtilCollection item) @@ -109,7 +111,7 @@ public void Add(UtilCollection item) list.Add(item); counts![item] = CountOf(item) + 1; } - else if(isOrdered) + else if (isOrdered) { list.Add(item); } @@ -156,7 +158,7 @@ public void assertOrdered() public void assertUnordered() { - if(isOrdered) throw new InvalidOperationException("Ordered"); + if (isOrdered) throw new InvalidOperationException("Ordered"); } public bool IsUnordered() @@ -272,7 +274,7 @@ public UtilCollection Intersect(UtilCollection other) throw new NotImplementedException(); } } - + public UtilCollection Union(UtilCollection other) { if (isOrdered != other.isOrdered) throw new InvalidOperationException("Can't take the union of a set and a list"); @@ -303,9 +305,9 @@ public UtilCollection Cross(UtilCollection other) UtilCollection ret = new UtilCollection(); foreach (UtilCollection a in this) { - foreach(UtilCollection b in other) + foreach (UtilCollection b in other) { - UtilCollection add = new UtilCollection(new List{a,b}); + UtilCollection add = new UtilCollection(new List { a, b }); ret.Add(add); } } diff --git a/grammar/BuiltInSet.cs b/grammar/BuiltInSet.cs index 948aba7..3c48b97 100644 --- a/grammar/BuiltInSet.cs +++ b/grammar/BuiltInSet.cs @@ -1,2 +1,3 @@ namespace SPADE.Grammar.Enums; -enum BuiltInSet {integers} \ No newline at end of file + +enum BuiltInSet { integers } \ No newline at end of file diff --git a/grammar/codeGen.cs b/grammar/codeGen.cs index 9aaa8f0..76b9934 100644 --- a/grammar/codeGen.cs +++ b/grammar/codeGen.cs @@ -10,18 +10,19 @@ namespace SPADE.Grammar; -class codeGen : IcollListener { +class codeGen : IcollListener +{ public Dictionary map; public Stack formatStack; - public SetDescriptor ?setDescriptor; + public SetDescriptor? setDescriptor; public Stack setDescriptorStack; public Stack constraintDescriptorStack; public Stack> constraintDescriptorStackStack; - public codeGen() : base() + public codeGen() : base() { map = new(); formatStack = new(); @@ -94,7 +95,7 @@ public void ExitFormat([NotNull] collParser.FormatContext context) List lst = new(); for (int i = 0; i < context.format().Length; i++) { - lst.Add(formatStack.Pop()); + lst.Add(formatStack.Pop()); } lst.Reverse(); // have to reverse the list, since the leftmost should be the one that was popped first formatStack.Push(new FormatDescriptor(lst, SourceText(context))); diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 2beac42..9be9756 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -13,18 +13,18 @@ class ConstraintDescriptor public ConstraintDescriptor(String _term, String _definition, SetDescriptor _set, String _text) { - term = _term; - definition = _definition; - set = _set; - text = _text; + term = _term; + definition = _definition; + set = _set; + text = _text; } public ConstraintDescriptor(String _term, String _definition, String _built_in, String _text) { - term = _term; - definition = _definition; - built_in = _built_in; - text = _text; + term = _term; + definition = _definition; + built_in = _built_in; + text = _text; } // Values in error messages are shortened, so a huge instance doesn't make a huge message. @@ -98,30 +98,30 @@ public void check(Dictionary map) case "subset": if (set != null) { - if (map[term].IsValue()) + if (map[term].IsValue()) + { + throw Failure($"{Describe(map[term])} is not a set", map[term]); + } + foreach (UtilCollection item in map[term]) + { + bool member; + try { - throw Failure($"{Describe(map[term])} is not a set", map[term]); + member = set.isMember(item); } - foreach (UtilCollection item in map[term]) + catch (SpadeParseException e) { - bool member; - try - { - member = set.isMember(item); - } - catch (SpadeParseException e) - { - // A constraint inside a nested set failed, such as w is int in - // {(e,w) | ...}. The message stays, but the error belongs to this part - // and item. A missing piece is dropped if its part is a nested name. - bool nested = e.ExpectedIn == null || set.declaredNames.Contains(e.ExpectedIn); - throw e.BelongingTo(term, item.ToString(), nested ? null : e.MissingValue, nested ? null : e.ExpectedIn); - } - if (!member) - { - throw Failure($"{Describe(item)} is not a member", item, set.FindMissing(item)); - } + // A constraint inside a nested set failed, such as w is int in + // {(e,w) | ...}. The message stays, but the error belongs to this part + // and item. A missing piece is dropped if its part is a nested name. + bool nested = e.ExpectedIn == null || set.declaredNames.Contains(e.ExpectedIn); + throw e.BelongingTo(term, item.ToString(), nested ? null : e.MissingValue, nested ? null : e.ExpectedIn); } + if (!member) + { + throw Failure($"{Describe(item)} is not a member", item, set.FindMissing(item)); + } + } } break; } diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index 3e71df3..5e10958 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -10,7 +10,7 @@ namespace SPADE.Grammar.descriptor; class SetDescriptor { - public Func isMember; + public Func isMember; public Func genSet; @@ -31,7 +31,7 @@ public SetDescriptor(Dictionary _map) genSet = () => new UtilCollection(""); } - public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) + public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) { map = _map; isMember = _isMember; @@ -39,7 +39,7 @@ public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet, Func _missing) + private SetDescriptor(SetDescriptor left, SetDescriptor right, Func _isMember, Func _genSet, Func _missing) : this(left.map, _isMember, _genSet) { missing = _missing; @@ -81,7 +81,7 @@ public SetDescriptor(Dictionary _map, FormatDescriptor f { constraintDescriptor.check(map); } - return true; + return true; }; genSet = () => throw new NotImplementedException("generating every set descriptor not implemented"); @@ -127,7 +127,7 @@ public SetDescriptor Cross(SetDescriptor other) public SetDescriptor UnorderedCross(SetDescriptor other) { - Func _isMember = (a) => + Func _isMember = (a) => { // Each member is a set of nodes, like the edge {a,b}. A list such as (a,b) // is an ordered pair and belongs to cross, and a plain value is not an edge. @@ -139,10 +139,10 @@ public SetDescriptor UnorderedCross(SetDescriptor other) { return false; } - } + } return true; }; - Func _genSet = () => + Func _genSet = () => { UtilCollection set = new("{}"); foreach (UtilCollection first in genSet()) @@ -174,7 +174,7 @@ public SetDescriptor UnorderedCross(SetDescriptor other) public SetDescriptor Union(SetDescriptor other) { - + Func _isMember = (a) => isMember(a) || other.isMember(a); Func _genSet = () => genSet().Union(other.genSet()); // Missing from both sides; the left one is named. diff --git a/grammar/desciptors/formatDescriptor.cs b/grammar/desciptors/formatDescriptor.cs index 79321e7..ce9faeb 100644 --- a/grammar/desciptors/formatDescriptor.cs +++ b/grammar/desciptors/formatDescriptor.cs @@ -14,7 +14,7 @@ class FormatDescriptor public FormatDescriptor(String term) { names.Add(term); - parseFunc = (map, coll) => {map[term] = coll;}; + parseFunc = (map, coll) => { map[term] = coll; }; } public FormatDescriptor(List lst, String text) @@ -28,7 +28,7 @@ public FormatDescriptor(List lst, String text) } for (int i = 0; i < lst.Count(); i++) { - lst[i].parseFunc(map, coll[i]); + lst[i].parseFunc(map, coll[i]); } }; } From 1e73384b5191c8252773bd73f6b7f1c61083bc45 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:32:42 -0600 Subject: [PATCH 22/35] style: run dotnet format Whitespace only: dotnet format SPADE.slnx on this branch's code. --- StringParser.cs | 7 +-- UtilCollection.cs | 28 ++++++----- grammar/BuiltInSet.cs | 3 +- grammar/codeGen.cs | 9 ++-- grammar/desciptors/ConstraintDescriptor.cs | 56 +++++++++++----------- grammar/desciptors/SetDescriptor.cs | 16 +++---- grammar/desciptors/formatDescriptor.cs | 4 +- 7 files changed, 64 insertions(+), 59 deletions(-) diff --git a/StringParser.cs b/StringParser.cs index 638630e..13e68ad 100644 --- a/StringParser.cs +++ b/StringParser.cs @@ -7,11 +7,12 @@ using SPADE.Grammar; namespace SPADE; + public class StringParser { readonly string format; public Dictionary map = new(); - + private bool hasParsed; private DefinitionDescription? definition; @@ -32,7 +33,7 @@ public void parse(string instance) { hasParsed = true; AntlrInputStream stream = new AntlrInputStream(format); - collLexer lex = new collLexer(stream); + collLexer lex = new collLexer(stream); CommonTokenStream tokens = new CommonTokenStream(lex); collParser parser = new collParser(tokens); @@ -43,7 +44,7 @@ public void parse(string instance) gen.Parse(new UtilCollection(instance)); - map = gen.map; + map = gen.map; } diff --git a/UtilCollection.cs b/UtilCollection.cs index 83df0ec..58bce79 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -64,7 +64,7 @@ public UtilCollection(HashSet set_) set = new(); foreach (string i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -74,7 +74,7 @@ public UtilCollection(HashSet> set_) set = new(); foreach (HashSet i in set_) { - set.Add(new UtilCollection(i)); + set.Add(new UtilCollection(i)); } } @@ -92,14 +92,16 @@ public UtilCollection(HashSet set__) public UtilCollection this[int index] { - get { + get + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - return list[index]; - } - set { + return list[index]; + } + set + { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); - list[index] = value; - } + list[index] = value; + } } public void Add(UtilCollection item) @@ -109,7 +111,7 @@ public void Add(UtilCollection item) list.Add(item); counts![item] = CountOf(item) + 1; } - else if(isOrdered) + else if (isOrdered) { list.Add(item); } @@ -156,7 +158,7 @@ public void assertOrdered() public void assertUnordered() { - if(isOrdered) throw new InvalidOperationException("Ordered"); + if (isOrdered) throw new InvalidOperationException("Ordered"); } public bool IsUnordered() @@ -272,7 +274,7 @@ public UtilCollection Intersect(UtilCollection other) throw new NotImplementedException(); } } - + public UtilCollection Union(UtilCollection other) { if (isOrdered != other.isOrdered) throw new InvalidOperationException("Can't take the union of a set and a list"); @@ -303,9 +305,9 @@ public UtilCollection Cross(UtilCollection other) UtilCollection ret = new UtilCollection(); foreach (UtilCollection a in this) { - foreach(UtilCollection b in other) + foreach (UtilCollection b in other) { - UtilCollection add = new UtilCollection(new List{a,b}); + UtilCollection add = new UtilCollection(new List { a, b }); ret.Add(add); } } diff --git a/grammar/BuiltInSet.cs b/grammar/BuiltInSet.cs index 2ceb73c..e2c90ad 100644 --- a/grammar/BuiltInSet.cs +++ b/grammar/BuiltInSet.cs @@ -2,7 +2,8 @@ using System.Text.RegularExpressions; namespace SPADE.Grammar.Enums; -enum BuiltInSet {integers, numbers} + +enum BuiltInSet { integers, numbers } // What "number" accepts, shared by "x is number" and "S subset number" (#23). static class NumberValue diff --git a/grammar/codeGen.cs b/grammar/codeGen.cs index 11ba11f..d268846 100644 --- a/grammar/codeGen.cs +++ b/grammar/codeGen.cs @@ -10,18 +10,19 @@ namespace SPADE.Grammar; -class codeGen : IcollListener { +class codeGen : IcollListener +{ public Dictionary map; public Stack formatStack; - public SetDescriptor ?setDescriptor; + public SetDescriptor? setDescriptor; public Stack setDescriptorStack; public Stack constraintDescriptorStack; public Stack> constraintDescriptorStackStack; - public codeGen() : base() + public codeGen() : base() { map = new(); formatStack = new(); @@ -94,7 +95,7 @@ public void ExitFormat([NotNull] collParser.FormatContext context) List lst = new(); for (int i = 0; i < context.format().Length; i++) { - lst.Add(formatStack.Pop()); + lst.Add(formatStack.Pop()); } lst.Reverse(); // have to reverse the list, since the leftmost should be the one that was popped first formatStack.Push(new FormatDescriptor(lst, SourceText(context))); diff --git a/grammar/desciptors/ConstraintDescriptor.cs b/grammar/desciptors/ConstraintDescriptor.cs index 24aa869..bd6c95a 100644 --- a/grammar/desciptors/ConstraintDescriptor.cs +++ b/grammar/desciptors/ConstraintDescriptor.cs @@ -14,18 +14,18 @@ class ConstraintDescriptor public ConstraintDescriptor(String _term, String _definition, SetDescriptor _set, String _text) { - term = _term; - definition = _definition; - set = _set; - text = _text; + term = _term; + definition = _definition; + set = _set; + text = _text; } public ConstraintDescriptor(String _term, String _definition, String _built_in, String _text) { - term = _term; - definition = _definition; - built_in = _built_in; - text = _text; + term = _term; + definition = _definition; + built_in = _built_in; + text = _text; } // Values in error messages are shortened, so a huge instance doesn't make a huge message. @@ -105,30 +105,30 @@ public void check(Dictionary map) case "subset": if (set != null) { - if (map[term].IsValue()) + if (map[term].IsValue()) + { + throw Failure($"{Describe(map[term])} is not a set", map[term]); + } + foreach (UtilCollection item in map[term]) + { + bool member; + try { - throw Failure($"{Describe(map[term])} is not a set", map[term]); + member = set.isMember(item); } - foreach (UtilCollection item in map[term]) + catch (SpadeParseException e) { - bool member; - try - { - member = set.isMember(item); - } - catch (SpadeParseException e) - { - // A constraint inside a nested set failed, such as w is int in - // {(e,w) | ...}. The message stays, but the error belongs to this part - // and item. A missing piece is dropped if its part is a nested name. - bool nested = e.ExpectedIn == null || set.declaredNames.Contains(e.ExpectedIn); - throw e.BelongingTo(term, item.ToString(), nested ? null : e.MissingValue, nested ? null : e.ExpectedIn); - } - if (!member) - { - throw Failure($"{Describe(item)} is not a member", item, set.FindMissing(item)); - } + // A constraint inside a nested set failed, such as w is int in + // {(e,w) | ...}. The message stays, but the error belongs to this part + // and item. A missing piece is dropped if its part is a nested name. + bool nested = e.ExpectedIn == null || set.declaredNames.Contains(e.ExpectedIn); + throw e.BelongingTo(term, item.ToString(), nested ? null : e.MissingValue, nested ? null : e.ExpectedIn); } + if (!member) + { + throw Failure($"{Describe(item)} is not a member", item, set.FindMissing(item)); + } + } } break; } diff --git a/grammar/desciptors/SetDescriptor.cs b/grammar/desciptors/SetDescriptor.cs index 7b53d50..f72e521 100644 --- a/grammar/desciptors/SetDescriptor.cs +++ b/grammar/desciptors/SetDescriptor.cs @@ -10,7 +10,7 @@ namespace SPADE.Grammar.descriptor; class SetDescriptor { - public Func isMember; + public Func isMember; public Func genSet; @@ -31,7 +31,7 @@ public SetDescriptor(Dictionary _map) genSet = () => new UtilCollection(""); } - public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) + public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet) { map = _map; isMember = _isMember; @@ -39,7 +39,7 @@ public SetDescriptor(Dictionary _map, Func _isMember, Func _genSet, Func _missing) + private SetDescriptor(SetDescriptor left, SetDescriptor right, Func _isMember, Func _genSet, Func _missing) : this(left.map, _isMember, _genSet) { missing = _missing; @@ -88,7 +88,7 @@ public SetDescriptor(Dictionary _map, FormatDescriptor f { constraintDescriptor.check(map); } - return true; + return true; }; genSet = () => throw new NotImplementedException("generating every set descriptor not implemented"); @@ -134,7 +134,7 @@ public SetDescriptor Cross(SetDescriptor other) public SetDescriptor UnorderedCross(SetDescriptor other) { - Func _isMember = (a) => + Func _isMember = (a) => { // Each member is a set of nodes, like the edge {a,b}. A list such as (a,b) // is an ordered pair and belongs to cross, and a plain value is not an edge. @@ -146,10 +146,10 @@ public SetDescriptor UnorderedCross(SetDescriptor other) { return false; } - } + } return true; }; - Func _genSet = () => + Func _genSet = () => { UtilCollection set = new("{}"); foreach (UtilCollection first in genSet()) @@ -181,7 +181,7 @@ public SetDescriptor UnorderedCross(SetDescriptor other) public SetDescriptor Union(SetDescriptor other) { - + Func _isMember = (a) => isMember(a) || other.isMember(a); Func _genSet = () => genSet().Union(other.genSet()); // Missing from both sides; the left one is named. diff --git a/grammar/desciptors/formatDescriptor.cs b/grammar/desciptors/formatDescriptor.cs index 79321e7..ce9faeb 100644 --- a/grammar/desciptors/formatDescriptor.cs +++ b/grammar/desciptors/formatDescriptor.cs @@ -14,7 +14,7 @@ class FormatDescriptor public FormatDescriptor(String term) { names.Add(term); - parseFunc = (map, coll) => {map[term] = coll;}; + parseFunc = (map, coll) => { map[term] = coll; }; } public FormatDescriptor(List lst, String text) @@ -28,7 +28,7 @@ public FormatDescriptor(List lst, String text) } for (int i = 0; i < lst.Count(); i++) { - lst[i].parseFunc(map, coll[i]); + lst[i].parseFunc(map, coll[i]); } }; } From 7145b7d0626ad239596b3846a64525c43ac5312b Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:35:34 -0600 Subject: [PATCH 23/35] ci: run the devcontainer on SDK 10 alone The tests move to net10.0, so the container no longer needs the .NET 8 runtime feature next to the SDK 10 image. SDK 10 still reads SPADE.slnx and builds the net6.0 library. --- .devcontainer/devcontainer.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ca172ea..80dd4f7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,11 +8,6 @@ "installZsh": false, "upgradePackages": false }, - // The tests target net8.0, so they need the .NET 8 runtime next to SDK 10. - "ghcr.io/devcontainers/features/dotnet:2": { - "version": "none", - "dotnetRuntimeVersions": "8.0" - }, "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { "moby": false }, "ghcr.io/reduxisu/features/rbs:1": {} }, From 8829639cebe18b7f67b054ee5c1af3a6bc929686 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:37:20 -0600 Subject: [PATCH 24/35] test: run the tests on .NET 10 The test project moves from net8.0 to net10.0, and ci.yml and publish.yml install only the 10.0.x SDK, which still builds the net6.0 library. DiscreteParser.csproj stays on net6.0, the NuGet package's target. The test packages (xunit.v3 3.2.2, xunit.runner.visualstudio 4.0.0, Microsoft.NET.Test.Sdk 18.9.0, coverlet.collector 10.1.0) run on net10.0 unchanged. development.md now says the tests need the .NET 10 SDK. --- .github/workflows/ci.yml | 4 +--- .github/workflows/publish.yml | 4 +--- Documentation/development.md | 2 +- tests/SPADE.Tests/SPADE.Tests.csproj | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55bb012..ef9143a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,9 +12,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: - dotnet-version: | - 6.0.x - 8.0.x + dotnet-version: 10.0.x - name: Build run: dotnet build DiscreteParser.csproj -c Release # The test project arrives with the first fix PR; until then there is nothing to run. diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index aa42087..fcdc2b1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,9 +15,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: - dotnet-version: | - 6.0.x - 8.0.x + dotnet-version: 10.0.x - name: Check the tag matches the project version run: | version=$(grep -oP '(?<=)[^<]+' DiscreteParser.csproj) diff --git a/Documentation/development.md b/Documentation/development.md index 78728f6..8b23bbf 100644 --- a/Documentation/development.md +++ b/Documentation/development.md @@ -8,7 +8,7 @@ The tests live in tests/SPADE.Tests and use xUnit. Run them from the repository dotnet test tests/SPADE.Tests ``` -The test project targets .NET 8, so you need the .NET 8 SDK or newer. The library itself still targets .NET 6. +The test project targets .NET 10, so you need the .NET 10 SDK or newer. The library itself still targets .NET 6, and SDK 10 builds it. ## antlr diff --git a/tests/SPADE.Tests/SPADE.Tests.csproj b/tests/SPADE.Tests/SPADE.Tests.csproj index 7e362f8..2073855 100644 --- a/tests/SPADE.Tests/SPADE.Tests.csproj +++ b/tests/SPADE.Tests/SPADE.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable false From 082150dc14a2e71d8965c062187a2b36ed0d7eff Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:53:34 -0600 Subject: [PATCH 25/35] ci: keep the coverage note in rbs.toml free of a per-branch figure The measured figure changes with every PR in the stack, so the comment states only the floor and the goal; the rbs report shows the current number. --- rbs.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rbs.toml b/rbs.toml index cc44659..cfbb4f0 100644 --- a/rbs.toml +++ b/rbs.toml @@ -5,5 +5,5 @@ engine = "dotnet" solution = "SPADE.slnx" [unit-test] -# The standard we're holding to. Floor 70%; the goal is 80%+. Line coverage is about 8% (Oct 2026, UtilCollection equality tests only); new code needs tests to reach the floor. +# The standard we're holding to. Floor 70%; the goal is 80%+. New code needs tests to keep it above the floor. coverage-min = 70 From dbc816b0ed86037c3841f6af6d31cc8fd5cbfd78 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:10:48 -0600 Subject: [PATCH 26/35] fix(collections): freeze a collection once it is hashed A HashSet or Dictionary files each element under the hash it had when it went in. UtilCollection is mutable, so changing a collection (or anything nested in it) after it has been hashed left it under a stale hash: Contains and equality lookups missed it and duplicates slipped in, with no error. The first GetHashCode() call now freezes the collection. A list's or set's hash is built from its elements' GetHashCode(), so they freeze too, all the way down. Add and the indexer setter throw InvalidOperationException on a frozen collection. The hash is computed once and cached. The List and HashSet constructors copy their argument instead of keeping the caller's reference, and ToList() on a list returns a copy, so a frozen collection can't be changed from outside either. --- Documentation/utilcollection.md | 20 ++- UtilCollection.cs | 37 ++++- .../SPADE.Tests/UtilCollectionFreezeTests.cs | 156 ++++++++++++++++++ 3 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 tests/SPADE.Tests/UtilCollectionFreezeTests.cs diff --git a/Documentation/utilcollection.md b/Documentation/utilcollection.md index 89e68b1..66ebd72 100644 --- a/Documentation/utilcollection.md +++ b/Documentation/utilcollection.md @@ -35,7 +35,7 @@ Hamiltonian path is a graph problem, and the string follows the formal definitio UtilCollections provides a few Utility methods to help process and extract the instance information. Each method works for both ordered and unordered collections, unless specified otherwise. ### Indexing -If the collection is ordered, then you can index into the list using square brackets to get each element of the list, the same as you would for an array. Returns a UtilCollections object. +If the collection is ordered, then you can index into the list using square brackets to get each element of the list, the same as you would for an array. Returns a UtilCollections object. Assigning to an index (`list[0] = item`) replaces that element, unless the list has been hashed (see [Collections freeze once hashed](#collections-freeze-once-hashed)). ### Enumeration Using foreach loops to iterate over each element in the collection is also possible, for example, using a UtilCollection object named collection: @@ -57,7 +57,7 @@ Returns True if the two UtilCollections objects are equal to each other and Fals Returns the length of the collection. ### ToList -Returns a list representing the elements of the collection. If the collection is ordered, it returns the same list. If the collection is unordered, it is cast to a list and then returned. +Returns a list representing the elements of the collection. If the collection is ordered, it returns a copy of the list, so changing the copy doesn't change the collection. If the collection is unordered, it is cast to a list and then returned. ### ToString Returns the string representation of the UtilCollection. @@ -66,7 +66,21 @@ Returns the string representation of the UtilCollection. Checks if the collection contains another UtilCollection object. Comparison is done by value. Returns true if it contains it, false if it doesn't. ### Add -Adds a UtilCollection object to the collection. +Adds a UtilCollection object to the collection. Throws `InvalidOperationException` if the collection has been hashed (see below). + +### Collections freeze once hashed +A `HashSet` or `Dictionary` files each element under the hash code it had when it went in. If the element changed afterwards, it would sit under a stale hash: `Contains` would miss it and duplicates could slip in, with no error. So the first time a UtilCollection is hashed, it freezes, and `Add` or assigning to an index throws `InvalidOperationException`. That happens when it is added to a set (including a UtilCollection set), used as a dictionary key, or put inside a list or set that is itself hashed, since hashing a collection hashes everything inside it. + +Build each collection fully, then add it to a set: + +```c# +UtilCollection edge = new UtilCollection("{}"); +edge.Add(new UtilCollection("1")); +edge.Add(new UtilCollection("2")); +edges.Add(edge); // edge is frozen from here on +``` + +A collection that has never been hashed can still be changed freely. The constructors that take a `List` or `HashSet` copy it, so changing the original afterwards doesn't change the collection. ### parseInt Attempts to parse and return itself as an integer. diff --git a/UtilCollection.cs b/UtilCollection.cs index 6d7f426..1b4ab49 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -14,6 +14,12 @@ public class UtilCollection private bool isOrdered; private bool isValue; + // Set the first time this collection is hashed. A HashSet keeps an element under the hash it + // had when it went in, so changing the element (or anything inside it) afterwards would leave + // it under a stale hash, and lookups would quietly miss it. Once frozen, every change throws. + // Hashing a list or set hashes its elements, so they freeze too, all the way down. + private bool frozen; + private int hashCode; public UtilCollection() { @@ -73,15 +79,16 @@ public UtilCollection(HashSet> set_) } } + // Both copy, so later changes to the caller's list or set can't change this collection. public UtilCollection(List list__) { - list = list__; + list = new List(list__); isOrdered = true; } public UtilCollection(HashSet set__) { - set = set__; + set = new HashSet(set__); isOrdered = false; } @@ -132,12 +139,14 @@ public UtilCollection this[int index] set { if (!isOrdered) throw new InvalidOperationException("Cannot index into a set"); + ThrowIfFrozen(); list[index] = value; } } public void Add(UtilCollection item) { + ThrowIfFrozen(); if (isOrdered) { list.Add(item); @@ -299,7 +308,28 @@ public bool NotEquals(object? obj) return true; } + private void ThrowIfFrozen() + { + if (frozen) + { + throw new InvalidOperationException( + "This collection has been hashed (it is in a set or used as a key) and can't be changed; " + + "build it fully before adding it to a set."); + } + } + + // The first call freezes the collection, so the hash can't change and is worked out only once. public override int GetHashCode() + { + if (!frozen) + { + hashCode = ComputeHashCode(); + frozen = true; + } + return hashCode; + } + + private int ComputeHashCode() { if (isValue) { @@ -332,7 +362,8 @@ public List ToList() { if (isOrdered) { - return list; + // A copy, so changes to it can't reach a collection that may already be frozen. + return new List(list); } else { diff --git a/tests/SPADE.Tests/UtilCollectionFreezeTests.cs b/tests/SPADE.Tests/UtilCollectionFreezeTests.cs new file mode 100644 index 0000000..10362cf --- /dev/null +++ b/tests/SPADE.Tests/UtilCollectionFreezeTests.cs @@ -0,0 +1,156 @@ +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// A collection freezes once it is hashed, so it can't end up in a set under a stale hash. +public class UtilCollectionFreezeTests +{ + private static UtilCollection C(string s) => new UtilCollection(s); + + [Fact] + public void AddThrowsOnceInAHashSet() + { + UtilCollection item = C("{1,2}"); + HashSet outer = new() { item }; + + InvalidOperationException e = Assert.Throws(() => item.Add(C("3"))); + Assert.Contains("has been hashed", e.Message); + Assert.Equal(2, item.Count()); + Assert.Contains(C("{2,1}"), outer); + } + + [Fact] + public void AddThrowsOnceUsedAsADictionaryKey() + { + UtilCollection key = C("(1,2)"); + Dictionary counts = new() { [key] = 1 }; + + Assert.Throws(() => key.Add(C("3"))); + Assert.Equal(1, counts[C("(1,2)")]); + } + + [Fact] + public void AddingToASetFreezesTheElement() + { + UtilCollection outer = C("{}"); + UtilCollection item = C("(1,2)"); + outer.Add(item); + + Assert.Throws(() => item.Add(C("3"))); + Assert.True(outer.Contains(C("(1,2)"))); + } + + [Fact] + public void ChildOfAHashedListIsFrozen() + { + UtilCollection child = C("{1}"); + UtilCollection pair = new UtilCollection(new List { child, C("x") }); + _ = new HashSet { pair }; + + Assert.Throws(() => child.Add(C("2"))); + } + + [Fact] + public void GrandchildOfAHashedSetIsFrozen() + { + UtilCollection outer = C("{({1},a)}"); + _ = outer.GetHashCode(); + UtilCollection pair = outer.ToList()[0]; + UtilCollection grandchild = pair[0]; + + Assert.Throws(() => grandchild.Add(C("2"))); + } + + [Fact] + public void IndexerSetterThrowsOnAHashedList() + { + UtilCollection list = C("(1,2)"); + _ = new HashSet { list }; + + Assert.Throws(() => list[0] = C("3")); + Assert.Equal("(1,2)", list.ToString()); + } + + [Fact] + public void HashIsTheSameOnEveryCall() + { + UtilCollection item = C("{(1,2),{3}}"); + Assert.Equal(item.GetHashCode(), item.GetHashCode()); + Assert.Equal(C("{{3},(1,2)}").GetHashCode(), item.GetHashCode()); + } + + [Fact] + public void ListConstructorCopiesItsArgument() + { + List source = new() { C("1"), C("2") }; + UtilCollection list = new UtilCollection(source); + source.Add(C("3")); + source[0] = C("9"); + + Assert.Equal("(1,2)", list.ToString()); + } + + [Fact] + public void SetConstructorCopiesItsArgument() + { + HashSet source = new() { C("1"), C("2") }; + UtilCollection set = new UtilCollection(source); + source.Add(C("3")); + source.Remove(C("1")); + + Assert.Equal(C("{1,2}"), set); + } + + [Fact] + public void ToListOfAListIsACopy() + { + UtilCollection list = C("(1,2)"); + _ = list.GetHashCode(); + list.ToList().Add(C("3")); + + Assert.Equal(2, list.Count()); + } + + [Fact] + public void BuildingFullyThenInsertingWorks() + { + UtilCollection edges = C("{}"); + foreach ((string a, string b) in new[] { ("1", "2"), ("2", "3"), ("2", "1") }) + { + UtilCollection edge = C("{}"); + edge.Add(C(a)); + edge.Add(C(b)); + edges.Add(edge); + } + + // {1,2} and {2,1} are the same edge. + Assert.Equal(2, edges.Count()); + Assert.True(edges.Contains(C("{2,1}"))); + Assert.True(edges.Contains(C("{3,2}"))); + Assert.False(edges.Contains(C("{1,3}"))); + } + + [Fact] + public void UnhashedCollectionsCanStillChange() + { + UtilCollection set = C("{1}"); + set.Add(C("2")); + UtilCollection list = C("(1,2)"); + list.Add(C("3")); + list[0] = C("0"); + + Assert.Equal(C("{1,2}"), set); + Assert.Equal("(0,2,3)", list.ToString()); + } + + [Fact] + public void EqualsDoesNotFreeze() + { + UtilCollection set = C("{1}"); + Assert.Equal(C("{1}"), set); + set.Add(C("2")); + + Assert.Equal(2, set.Count()); + } +} From 7041a2e5ed0709a1b1c888c7a0e7e1f23d87be4f Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:16:36 -0600 Subject: [PATCH 27/35] fix(collections): freeze multisets once hashed Multisets go through the same Add guard, so a hashed multiset can't change its occurrences or counts. FromSetItems now copies the item list it keeps as the set's occurrences instead of holding the caller's list. Tests cover a hashed multiset refusing Add, its elements freezing as they go in, AsMultiset still working on a frozen set, and unhashed multisets changing freely. --- Documentation/utilcollection.md | 2 +- UtilCollection.cs | 3 +- .../SPADE.Tests/UtilCollectionFreezeTests.cs | 48 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Documentation/utilcollection.md b/Documentation/utilcollection.md index 87f2354..fec064f 100644 --- a/Documentation/utilcollection.md +++ b/Documentation/utilcollection.md @@ -72,7 +72,7 @@ Checks if the collection contains another UtilCollection object. Comparison is d Adds a UtilCollection object to the collection. Throws `InvalidOperationException` if the collection has been hashed (see below). ### Collections freeze once hashed -A `HashSet` or `Dictionary` files each element under the hash code it had when it went in. If the element changed afterwards, it would sit under a stale hash: `Contains` would miss it and duplicates could slip in, with no error. So the first time a UtilCollection is hashed, it freezes, and `Add` or assigning to an index throws `InvalidOperationException`. That happens when it is added to a set (including a UtilCollection set), used as a dictionary key, or put inside a list or set that is itself hashed, since hashing a collection hashes everything inside it. +A `HashSet` or `Dictionary` files each element under the hash code it had when it went in. If the element changed afterwards, it would sit under a stale hash: `Contains` would miss it and duplicates could slip in, with no error. So the first time a UtilCollection is hashed, it freezes, and `Add` or assigning to an index throws `InvalidOperationException`. That happens when it is added to a set (including a UtilCollection set), used as a dictionary key, or put inside a list or set that is itself hashed, since hashing a collection hashes everything inside it. Multisets freeze the same way. Build each collection fully, then add it to a set: diff --git a/UtilCollection.cs b/UtilCollection.cs index 8cced7b..449b47c 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -59,9 +59,10 @@ internal static UtilCollection FromValue(string value) return new UtilCollection { value = value, isValue = true }; } + // Copies items, like the constructors, so the caller's list can't change this set later. internal static UtilCollection FromSetItems(List items) { - return new UtilCollection(items.ToHashSet()) { occurrences = items }; + return new UtilCollection(items.ToHashSet()) { occurrences = new List(items) }; } public UtilCollection(HashSet set_) diff --git a/tests/SPADE.Tests/UtilCollectionFreezeTests.cs b/tests/SPADE.Tests/UtilCollectionFreezeTests.cs index 10362cf..b7eebc2 100644 --- a/tests/SPADE.Tests/UtilCollectionFreezeTests.cs +++ b/tests/SPADE.Tests/UtilCollectionFreezeTests.cs @@ -153,4 +153,52 @@ public void EqualsDoesNotFreeze() Assert.Equal(2, set.Count()); } + + // --- Multisets (#1) --- + + private static UtilCollection M(string s) => new UtilCollection(s).AsMultiset(); + + [Fact] + public void AddThrowsOnAHashedMultiset() + { + UtilCollection multiset = M("{3,3,2}"); + HashSet outer = new() { multiset }; + + Assert.Throws(() => multiset.Add(C("3"))); + Assert.Equal(2, multiset.CountOf(C("3"))); + Assert.Contains(M("{2,3,3}"), outer); + } + + [Fact] + public void ElementsOfAHashedMultisetAreFrozen() + { + UtilCollection element = C("(1,2)"); + UtilCollection multiset = M("{}"); + multiset.Add(element); + multiset.Add(C("(1,2)")); + + // Counting an element hashes it, so it freezes as soon as it goes in. + Assert.Throws(() => element.Add(C("3"))); + Assert.Equal(2, multiset.CountOf(C("(1,2)"))); + } + + [Fact] + public void AsMultisetOfAHashedSetStillKeepsDuplicates() + { + UtilCollection set = C("{3,3,2}"); + _ = set.GetHashCode(); + + Assert.Throws(() => set.Add(C("3"))); + Assert.Equal(3, set.AsMultiset().Count()); + } + + [Fact] + public void UnhashedMultisetsCanStillChange() + { + UtilCollection multiset = M("{3}"); + multiset.Add(C("3")); + + Assert.Equal(2, multiset.CountOf(C("3"))); + Assert.Equal(M("{3,3}").GetHashCode(), multiset.GetHashCode()); + } } From 45eea963bf4b9e3120e05e5ac883e3432f5f99d3 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:04:39 -0600 Subject: [PATCH 28/35] fix(collections): make the cached hash safe to read from several threads GetHashCode writes the hash and then sets frozen. Without a barrier, another thread could see frozen before the hash and return 0. frozen is now volatile, so a thread that sees it also sees the hash; two threads computing it at once get the same value. A frozen collection can't change, so it is safe to share between threads; building one still isn't, as with List and HashSet. Documented, with a test that races threads to hash a fresh nested set. --- Documentation/utilcollection.md | 2 ++ UtilCollection.cs | 4 +++- tests/SPADE.Tests/UtilCollectionFreezeTests.cs | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Documentation/utilcollection.md b/Documentation/utilcollection.md index 66ebd72..8c63907 100644 --- a/Documentation/utilcollection.md +++ b/Documentation/utilcollection.md @@ -82,6 +82,8 @@ edges.Add(edge); // edge is frozen from here on A collection that has never been hashed can still be changed freely. The constructors that take a `List` or `HashSet` copy it, so changing the original afterwards doesn't change the collection. +A frozen collection can't change, so it is safe to share between threads: hashing, comparing, `Contains` and enumerating it from several threads at once all work. Building a collection is not thread-safe, the same as `List` and `HashSet`: fill it on one thread, then share it. + ### parseInt Attempts to parse and return itself as an integer. diff --git a/UtilCollection.cs b/UtilCollection.cs index 1b4ab49..d434588 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -18,7 +18,9 @@ public class UtilCollection // had when it went in, so changing the element (or anything inside it) afterwards would leave // it under a stale hash, and lookups would quietly miss it. Once frozen, every change throws. // Hashing a list or set hashes its elements, so they freeze too, all the way down. - private bool frozen; + // volatile: GetHashCode writes hashCode before frozen, so a thread that sees frozen also sees + // the hash. A frozen collection is safe to read from several threads. + private volatile bool frozen; private int hashCode; public UtilCollection() diff --git a/tests/SPADE.Tests/UtilCollectionFreezeTests.cs b/tests/SPADE.Tests/UtilCollectionFreezeTests.cs index 10362cf..50fb709 100644 --- a/tests/SPADE.Tests/UtilCollectionFreezeTests.cs +++ b/tests/SPADE.Tests/UtilCollectionFreezeTests.cs @@ -144,6 +144,21 @@ public void UnhashedCollectionsCanStillChange() Assert.Equal("(0,2,3)", list.ToString()); } + [Fact] + public void HashingFromManyThreadsAgrees() + { + // Each round starts unfrozen, so the threads race to compute and cache the hash. + for (int round = 0; round < 200; round++) + { + UtilCollection set = C("{({1,2},{3,4}),({5,6},(7,8)),9,{10,{11,12}}}"); + int expected = C("{({1,2},{3,4}),({5,6},(7,8)),9,{10,{11,12}}}").GetHashCode(); + int[] seen = new int[8]; + Parallel.For(0, seen.Length, i => seen[i] = set.GetHashCode()); + + Assert.All(seen, h => Assert.Equal(expected, h)); + } + } + [Fact] public void EqualsDoesNotFreeze() { From 2b8b0187620ee4d42d33c81ce133f8644fa74779 Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:11:22 -0600 Subject: [PATCH 29/35] feat(collections): add Freeze() and document using SPADE from several threads Freezing used to happen only as a side effect of hashing, so a list that was built but never hashed could still be changed while other threads read it. Freeze() freezes a collection and everything inside it on purpose and returns it; IsFrozen() reports it. Documentation/threads.md says what is safe to share (frozen collections, sets) and what isn't (a StringParser while it parses, a collection still being built), with the parse-once, freeze, then share pattern. --- Documentation/index.md | 1 + Documentation/threads.md | 35 +++++++++++++++++++ Documentation/utilcollection.md | 2 ++ UtilCollection.cs | 13 +++++++ .../SPADE.Tests/UtilCollectionFreezeTests.cs | 31 ++++++++++++++++ 5 files changed, 82 insertions(+) create mode 100644 Documentation/threads.md diff --git a/Documentation/index.md b/Documentation/index.md index ef2580f..5737189 100644 --- a/Documentation/index.md +++ b/Documentation/index.md @@ -7,4 +7,5 @@ SPADE is a tool that parses strings representing instances of discrete math type - [Instance Strings](InstanceString.md) - [Definition String](DefinitionString.md) - [Utilcollections](utilcollection.md) + - [Using SPADE from several threads](threads.md) - [SPADE Development](development.md) \ No newline at end of file diff --git a/Documentation/threads.md b/Documentation/threads.md new file mode 100644 index 0000000..0a8ff18 --- /dev/null +++ b/Documentation/threads.md @@ -0,0 +1,35 @@ +# Using SPADE from several threads + +SPADE's objects follow the usual .NET rule: build on one thread, then share. Once an instance has been parsed and frozen, any number of threads can read it at the same time. + +## Safe to share + +- **A frozen `UtilCollection`.** Hashing, `Equals`, `Contains`, `Count`, indexing, enumerating and `ToString` are all safe from several threads at once. A frozen collection can't change, and its hash is computed once and published safely. +- **Sets as lookup tables.** Everything inside a set is frozen as soon as it goes in, so a shared `HashSet` can't be changed under another thread (as long as no thread adds to the set itself). + +## Not safe to share + +- **A `StringParser` while it parses.** `parse()` replaces the parser's results, so two threads parsing with the same `StringParser` overwrite each other. Use one `StringParser` per thread, or per parse; they are cheap to create. Reading the results of a finished parse from several threads is fine. +- **A collection that is still being built.** Don't call `Add`, or assign to an index, while another thread reads the collection. The same is true of `List` and `HashSet`. + +## Recommended pattern + +Parse once, freeze the result, then hand it to the workers: + +```c# +StringParser parser = new("{((N,E),K) | N is set, E subset N unorderedcross N, K is int}"); +parser.parse(instance); + +UtilCollection nodes = parser["N"].Freeze(); +UtilCollection edges = parser["E"].Freeze(); + +Dictionary degree = new(); +Parallel.ForEach(nodes.ToList(), node => +{ + // Every thread reads the same frozen collections. + int d = edges.ToList().Count(edge => edge.Contains(node)); + lock (degree) degree[node.ToString()] = d; +}); +``` + +`Freeze()` freezes a collection and everything inside it, and returns the same collection. After that, any attempt to change it throws `InvalidOperationException`, so a stray `Add` in a worker fails loudly instead of corrupting shared data. `IsFrozen()` tells you whether a collection is frozen. diff --git a/Documentation/utilcollection.md b/Documentation/utilcollection.md index 8c63907..6a29fa0 100644 --- a/Documentation/utilcollection.md +++ b/Documentation/utilcollection.md @@ -71,6 +71,8 @@ Adds a UtilCollection object to the collection. Throws `InvalidOperationExceptio ### Collections freeze once hashed A `HashSet` or `Dictionary` files each element under the hash code it had when it went in. If the element changed afterwards, it would sit under a stale hash: `Contains` would miss it and duplicates could slip in, with no error. So the first time a UtilCollection is hashed, it freezes, and `Add` or assigning to an index throws `InvalidOperationException`. That happens when it is added to a set (including a UtilCollection set), used as a dictionary key, or put inside a list or set that is itself hashed, since hashing a collection hashes everything inside it. +To freeze a collection on purpose, call `Freeze()`. It freezes the collection and everything inside it, and returns the collection, so `new UtilCollection(text).Freeze()` works. `IsFrozen()` says whether a collection is frozen. + Build each collection fully, then add it to a set: ```c# diff --git a/UtilCollection.cs b/UtilCollection.cs index d434588..85f2b04 100644 --- a/UtilCollection.cs +++ b/UtilCollection.cs @@ -310,6 +310,19 @@ public bool NotEquals(object? obj) return true; } + // Freezes this collection and everything inside it, so it can be shared between threads, or + // kept, knowing nothing will change it. Hashing does the freezing, all the way down. + public UtilCollection Freeze() + { + GetHashCode(); + return this; + } + + public bool IsFrozen() + { + return frozen; + } + private void ThrowIfFrozen() { if (frozen) diff --git a/tests/SPADE.Tests/UtilCollectionFreezeTests.cs b/tests/SPADE.Tests/UtilCollectionFreezeTests.cs index 50fb709..820ae5d 100644 --- a/tests/SPADE.Tests/UtilCollectionFreezeTests.cs +++ b/tests/SPADE.Tests/UtilCollectionFreezeTests.cs @@ -159,6 +159,37 @@ public void HashingFromManyThreadsAgrees() } } + [Fact] + public void FreezeLocksTheCollectionAndEverythingInside() + { + UtilCollection child = C("(1,2)"); + UtilCollection list = new UtilCollection(new List { child, C("{3}") }); + + Assert.Same(list, list.Freeze()); + Assert.True(list.IsFrozen()); + Assert.True(child.IsFrozen()); + Assert.Throws(() => list.Add(C("4"))); + Assert.Throws(() => child[0] = C("0")); + Assert.Equal("((1,2),{3})", list.ToString()); + } + + [Fact] + public void FreezeTwiceIsHarmlessAndKeepsTheHash() + { + UtilCollection set = C("{1,{2,3}}"); + int hash = set.Freeze().GetHashCode(); + + Assert.Equal(hash, set.Freeze().GetHashCode()); + Assert.Equal(C("{{3,2},1}").GetHashCode(), hash); + } + + [Fact] + public void NewCollectionsAreNotFrozen() + { + Assert.False(C("{1,2}").IsFrozen()); + Assert.False(C("(1,2)").IsFrozen()); + } + [Fact] public void EqualsDoesNotFreeze() { From 6ed97111b8700b1f6fc7d435011044a3041b9c4d Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:41:26 -0600 Subject: [PATCH 30/35] test(collections): cover union, cross and the shape checks UtilCollection's union (sets and multisets), cross, the assertPair / assertCount / assertOrdered / assertUnordered checks, NotEquals and the set-of-string-sets constructor had no tests. They now check the result of each operation, the error message for mixing a set with a list or a multiset, and that list union and intersection still say they aren't implemented rather than guessing at an order. --- .../UtilCollectionOperationTests.cs | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 tests/SPADE.Tests/UtilCollectionOperationTests.cs diff --git a/tests/SPADE.Tests/UtilCollectionOperationTests.cs b/tests/SPADE.Tests/UtilCollectionOperationTests.cs new file mode 100644 index 0000000..78ee3ef --- /dev/null +++ b/tests/SPADE.Tests/UtilCollectionOperationTests.cs @@ -0,0 +1,144 @@ +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// Set operations, shape checks and the other UtilCollection helpers callers use on parsed parts. +public class UtilCollectionOperationTests +{ + private static UtilCollection C(string s) => new UtilCollection(s); + + [Fact] + public void SetOfStringSetsBuildsNestedSets() + { + UtilCollection built = new UtilCollection(new HashSet> + { + new HashSet { "1", "2" }, + new HashSet { "3" }, + }); + + Assert.True(built.IsUnordered()); + Assert.Equal(C("{{2,1},{3}}"), built); + Assert.True(built.Contains(C("{1,2}"))); + } + + [Fact] + public void UnionOfSetsHasEveryElementOnce() + { + Assert.Equal(C("{1,2,3}"), C("{1,2}").Union(C("{2,3}"))); + } + + [Fact] + public void UnionOfMultisetsKeepsTheLargerCount() + { + UtilCollection union = C("{1,1,2}").AsMultiset().Union(C("{1,2,2,3}").AsMultiset()); + + Assert.True(union.IsMultiset()); + Assert.Equal(C("{1,1,2,2,3}").AsMultiset(), union); + Assert.Equal(2, union.CountOf(C("1"))); + Assert.Equal(2, union.CountOf(C("2"))); + Assert.Equal(1, union.CountOf(C("3"))); + } + + [Fact] + public void UnionAndIntersectionRejectMixedKinds() + { + InvalidOperationException listUnion = Assert.Throws(() => C("{1}").Union(C("(1)"))); + Assert.Equal("Can't take the union of a set and a list", listUnion.Message); + + InvalidOperationException multisetUnion = Assert.Throws(() => C("{1}").Union(C("{1}").AsMultiset())); + Assert.Equal("Can't take the union of a set and a multiset", multisetUnion.Message); + + InvalidOperationException listIntersect = Assert.Throws(() => C("(1)").Intersect(C("{1}"))); + Assert.Equal("Can't take the intersection of a set and a list", listIntersect.Message); + + InvalidOperationException multisetIntersect = Assert.Throws(() => C("{1}").AsMultiset().Intersect(C("{1}"))); + Assert.Equal("Can't take the intersection of a set and a multiset", multisetIntersect.Message); + } + + // Lists have no union or intersection yet; saying so beats guessing at an order. + [Fact] + public void UnionAndIntersectionOfListsAreNotImplemented() + { + Assert.Throws(() => C("(1,2)").Union(C("(2,3)"))); + Assert.Throws(() => C("(1,2)").Intersect(C("(2,3)"))); + } + + [Fact] + public void CrossPairsEveryElementOfTheLeftWithEveryElementOfTheRight() + { + UtilCollection cross = C("{1,2}").Cross(C("{a,b}")); + + Assert.True(cross.IsUnordered()); + Assert.Equal(C("{(1,a),(1,b),(2,a),(2,b)}"), cross); + } + + [Fact] + public void CrossOfListsIsASetOfPairs() + { + UtilCollection cross = C("(1,1)").Cross(C("(x)")); + + // (1,x) twice collapses to one pair, since the result is a set. + Assert.Equal(C("{(1,x)}"), cross); + Assert.Equal(C("(1,x)"), cross.ToList().Single()); + } + + [Fact] + public void CrossWithAnEmptySetIsEmpty() + { + Assert.Equal(0, C("{1,2}").Cross(C("{}")).Count()); + } + + [Fact] + public void AssertPairAcceptsOnlyListsOfTheGivenLength() + { + Assert.Null(Record.Exception(() => C("(1,2)").assertPair())); + Assert.Null(Record.Exception(() => C("(1,2,3)").assertPair(3))); + + Assert.Equal("Not a list of correct length", Assert.Throws(() => C("(1,2,3)").assertPair()).Message); + Assert.Equal("Not a list of correct length", Assert.Throws(() => C("{1,2}").assertPair()).Message); + Assert.Equal("Not a list of correct length", Assert.Throws(() => C("(1,2)").assertPair(3)).Message); + } + + [Fact] + public void AssertCountChecksTheNumberOfElements() + { + Assert.Null(Record.Exception(() => C("{1,2,3}").assertCount(3))); + Assert.Null(Record.Exception(() => C("(1,1)").assertCount(2))); + + // A set drops its duplicate, so {1,1} has one element. + InvalidOperationException e = Assert.Throws(() => C("{1,1}").assertCount(2)); + Assert.Equal("collection has incorrect length", e.Message); + } + + [Fact] + public void AssertOrderedAndUnorderedTellListsFromSets() + { + Assert.Null(Record.Exception(() => C("(1,2)").assertOrdered())); + Assert.Null(Record.Exception(() => C("{1,2}").assertUnordered())); + + Assert.Equal("Not ordered", Assert.Throws(() => C("{1,2}").assertOrdered()).Message); + Assert.Equal("Ordered", Assert.Throws(() => C("(1,2)").assertUnordered()).Message); + } + + [Theory] + [InlineData("{1,2}", "{2,1}", false)] + [InlineData("(1,2)", "(1,2)", false)] + [InlineData("(1,2)", "(2,1)", true)] + [InlineData("{1,2}", "(1,2)", true)] + [InlineData("{1,2}", "{1}", true)] + [InlineData("a", "a", false)] + [InlineData("a", "b", true)] + public void NotEqualsIsTheOppositeOfEquals(string a, string b, bool notEqual) + { + Assert.Equal(notEqual, C(a).NotEquals(C(b))); + Assert.Equal(!notEqual, C(a).Equals(C(b))); + } + + [Fact] + public void NotEqualToNullOrAnotherType() + { + Assert.True(C("{1}").NotEquals(null)); + Assert.True(C("1").NotEquals("1")); + } +} From 0853316ddc07f090c2ccfc5848ffcc3a167140cd Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:41:27 -0600 Subject: [PATCH 31/35] test(grammar): cover union, intersection and the parse tree Definitions using union and intersection were never parsed in a test. SetOperationTests checks that each accepts the right elements and that a failure names the part, the element, the missing value and the side it is missing from (#19), and does the same for a cross pair with one bad half. DefinitionDescriptionTests now covers DefinitionPart.ToString, a definition that is a list of values, and the exact message and position for lexer and parser errors. GrammarListenerTests walks real definitions with a collBaseListener subclass, pinning down the tree coll.g4 gives: rule and token order, left-nested binary operators, logical operators with not, list and set literals, and that malformed definitions are counted as syntax errors. This is the generated parser's code, reached through grammars it reads. --- .../SPADE.Tests/DefinitionDescriptionTests.cs | 27 ++++ tests/SPADE.Tests/GrammarListenerTests.cs | 115 ++++++++++++++++++ tests/SPADE.Tests/SetOperationTests.cs | 101 +++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 tests/SPADE.Tests/GrammarListenerTests.cs create mode 100644 tests/SPADE.Tests/SetOperationTests.cs diff --git a/tests/SPADE.Tests/DefinitionDescriptionTests.cs b/tests/SPADE.Tests/DefinitionDescriptionTests.cs index a87a107..dde301c 100644 --- a/tests/SPADE.Tests/DefinitionDescriptionTests.cs +++ b/tests/SPADE.Tests/DefinitionDescriptionTests.cs @@ -191,6 +191,33 @@ public void BadDefinitionThrowsOnlyWhenRead() Assert.Contains("not a definition", e.Message); } + [Fact] + public void PartToStringShowsKindAndWhatItIsOf() + { + DefinitionDescription d = Describe("{(N,E) | N is set, E subset N cross N}"); + + Assert.Equal("N: Set", d["N"].ToString()); + Assert.Equal("E: Subset of N cross N", d["E"].ToString()); + } + + [Fact] + public void ListOfValuesIsNotADefinition() + { + FormatException e = Assert.Throws(() => Describe("{a,b}")); + Assert.Equal("Definition string \"{a,b}\" is a list of values, not a format followed by |", e.Message); + } + + // Lexer errors (a character the grammar has no token for) and parser errors both give the position. + [Theory] + [InlineData("{N | N is set#}", "token recognition error at: '#' at position 14")] + [InlineData("{N | N is set", "mismatched input '' expecting {'}', CM} at position 14")] + [InlineData("{N | N is set}}", "extraneous input '}' expecting at position 15")] + public void SyntaxErrorsGiveThePosition(string grammar, string reason) + { + FormatException e = Assert.Throws(() => Describe(grammar)); + Assert.Equal($"Definition string \"{grammar}\" can't be read: {reason}", e.Message); + } + [Fact] public void ParseDirectlyMatchesTheProperty() { diff --git a/tests/SPADE.Tests/GrammarListenerTests.cs b/tests/SPADE.Tests/GrammarListenerTests.cs new file mode 100644 index 0000000..913e887 --- /dev/null +++ b/tests/SPADE.Tests/GrammarListenerTests.cs @@ -0,0 +1,115 @@ +using Antlr4.Runtime; +using Antlr4.Runtime.Tree; +using Xunit; + +namespace SPADE.Tests; + +// The generated parser and collBaseListener are public, so a caller can walk a definition string's +// parse tree. These pin down the tree coll.g4 gives for each kind of definition. +public class GrammarListenerTests +{ + // Records each rule as it is entered and each token as it is visited; every other callback + // is collBaseListener's. + private class Recorder : collBaseListener + { + private readonly collParser parser; + public readonly List Rules = new(); + public readonly List Tokens = new(); + + public Recorder(collParser parser) + { + this.parser = parser; + } + + public override void EnterEveryRule(ParserRuleContext context) + { + Rules.Add(parser.RuleNames[context.RuleIndex]); + } + + public override void VisitTerminal(ITerminalNode node) + { + Tokens.Add(node.GetText()); + } + } + + private static (Recorder Recorder, collParser.StartContext Tree, collParser Parser) Walk(string definition) + { + collLexer lexer = new(new AntlrInputStream(definition)); + collParser parser = new(new CommonTokenStream(lexer)); + parser.RemoveErrorListeners(); + collParser.StartContext tree = parser.start(); + Recorder recorder = new(parser); + ParseTreeWalker.Default.Walk(recorder, tree); + return (recorder, tree, parser); + } + + [Fact] + public void WalksEveryRuleAndTokenInOrder() + { + var (recorder, _, parser) = Walk("{(N,E) | N is set, E subset N cross N}"); + + Assert.Equal(0, parser.NumberOfSyntaxErrors); + Assert.Equal( + new[] { "start", "set", "format", "format", "format", "constraint", "object", "constraint", "object", "object", "object" }, + recorder.Rules); + Assert.Equal( + new[] { "{", "(", "N", ",", "E", ")", "|", "N", "is", "set", ",", "E", "subset", "N", "cross", "N", "}", "" }, + recorder.Tokens); + } + + [Fact] + public void BinaryOperatorsNestToTheLeft() + { + var (_, tree, _) = Walk("{(A,B,C,D) | D subset A union B intersection C}"); + + collParser.ObjectContext obj = tree.set().constraint(0).@object(); + Assert.Equal("intersection", obj.BIN_OP().GetText()); + Assert.Equal("AunionB", obj.@object(0).GetText()); + Assert.Equal("union", obj.@object(0).BIN_OP().GetText()); + Assert.Equal("C", obj.@object(1).TERM().GetText()); + } + + [Fact] + public void LogicalOperatorsAndNotFormOneConstraint() + { + var (recorder, tree, parser) = Walk("{(A,B) | A is set or not B is int}"); + + Assert.Equal(0, parser.NumberOfSyntaxErrors); + collParser.ConstraintContext constraint = Assert.Single(tree.set().constraint()); + Assert.Equal("or", constraint.BIN_LOG_OP().GetText()); + Assert.Equal("Aisset", constraint.constraint(0).GetText()); + Assert.NotNull(constraint.constraint(1).NOT()); + Assert.Equal("int", constraint.constraint(1).constraint(0).@object().BUILT_IN_SET().GetText()); + Assert.Equal(4, recorder.Rules.Count(r => r == "constraint")); + } + + [Fact] + public void ListAndSetLiteralsAreObjects() + { + var (recorder, tree, parser) = Walk("{(A,B) | A subset (a,b), B subset {c,d}}"); + + Assert.Equal(0, parser.NumberOfSyntaxErrors); + collParser.ListContext list = tree.set().constraint(0).@object().list(); + Assert.Equal(new[] { "a", "b" }, list.TERM().Select(t => t.GetText())); + Assert.Single(list.CM()); + + collParser.SetContext literal = tree.set().constraint(1).@object().set(); + Assert.Null(literal.format()); + Assert.Equal(new[] { "c", "d" }, literal.TERM().Select(t => t.GetText())); + Assert.Contains("list", recorder.Rules); + Assert.Equal(2, recorder.Rules.Count(r => r == "set")); + } + + // With the default listeners off, the parser still recovers and counts what it skipped. + [Theory] + [InlineData("{N | N is set")] + [InlineData("{N | N is set}}")] + [InlineData("{N | N is (}")] + [InlineData("{N | N subset (a,}")] + [InlineData("{(N,) | N is set}")] + public void MalformedDefinitionsAreCountedAsSyntaxErrors(string definition) + { + var (_, _, parser) = Walk(definition); + Assert.True(parser.NumberOfSyntaxErrors > 0); + } +} diff --git a/tests/SPADE.Tests/SetOperationTests.cs b/tests/SPADE.Tests/SetOperationTests.cs new file mode 100644 index 0000000..7c1b2a7 --- /dev/null +++ b/tests/SPADE.Tests/SetOperationTests.cs @@ -0,0 +1,101 @@ +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// union, intersection and cross in a definition's set expressions, and which part a failure names. +public class SetOperationTests +{ + private const string Union = "{(A,B,C) | A is set, B is set, C subset A union B}"; + private const string Intersection = "{(A,B,C) | A is set, B is set, C subset A intersection B}"; + + private static StringParser Parse(string grammar, string instance) + { + StringParser parser = new StringParser(grammar); + parser.parse(instance); + return parser; + } + + private static SpadeParseException Fails(string grammar, string instance) + { + return Assert.Throws(() => new StringParser(grammar).parse(instance)); + } + + [Theory] + [InlineData("({1},{2},{1,2})")] + [InlineData("({1},{2},{2})")] + [InlineData("({1,2},{2,3},{})")] + public void UnionAcceptsElementsOfEitherSide(string instance) + { + StringParser parser = Parse(Union, instance); + Assert.Equal(new UtilCollection(instance)[2], parser["C"]); + } + + // Missing from both sides; the left part is the one named. + [Fact] + public void UnionRejectsElementsOfNeitherSide() + { + SpadeParseException e = Fails(Union, "({1},{2},{1,3})"); + + Assert.Equal("Constraint C subset A union B failed: 3 is not a member", e.Message); + Assert.Equal("C", e.Part); + Assert.Equal("3", e.Element); + Assert.Equal("3", e.MissingValue); + Assert.Equal("A", e.ExpectedIn); + } + + [Fact] + public void UnionWithIntAcceptsIntegers() + { + const string grammar = "{(N,E) | N is set, E subset N union int}"; + Assert.Equal(new UtilCollection("{a,3,-4}"), Parse(grammar, "({a},{a,3,-4})")["E"]); + + SpadeParseException e = Fails(grammar, "({a},{a,3,b})"); + Assert.Equal("b", e.Element); + Assert.Equal("N", e.ExpectedIn); + } + + [Fact] + public void IntersectionAcceptsElementsOfBothSides() + { + StringParser parser = Parse(Intersection, "({1,2},{2,3},{2})"); + Assert.Equal(new UtilCollection("{2}"), parser["C"]); + } + + // The first side the element is missing from is named. + [Theory] + [InlineData("({1,2},{2,3},{1})", "1", "B")] + [InlineData("({1,2},{2,3},{3})", "3", "A")] + [InlineData("({1,2},{2,3},{4})", "4", "A")] + public void IntersectionNamesTheSideTheElementIsMissingFrom(string instance, string element, string expectedIn) + { + SpadeParseException e = Fails(Intersection, instance); + + Assert.Equal($"Constraint C subset A intersection B failed: {element} is not a member", e.Message); + Assert.Equal("C", e.Part); + Assert.Equal(element, e.MissingValue); + Assert.Equal(expectedIn, e.ExpectedIn); + } + + [Fact] + public void CrossNamesTheMissingHalfOfAPair() + { + SpadeParseException e = Fails("{(N,E) | N is set, E subset N cross N}", "({a},{(a,b)})"); + + Assert.Equal("(a,b)", e.Element); + Assert.Equal("b", e.MissingValue); + Assert.Equal("N", e.ExpectedIn); + } + + // A value is not a pair, so there is no missing half to name. + [Fact] + public void CrossNamesNoMissingPieceForANonPair() + { + SpadeParseException e = Fails("{(N,E) | N is set, E subset N cross N}", "({a},{b})"); + + Assert.Equal("E", e.Part); + Assert.Equal("b", e.Element); + Assert.Null(e.MissingValue); + Assert.Null(e.ExpectedIn); + } +} From 4a01a8ba684d7d4f4c1c365b0c31f134a516209c Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:41:27 -0600 Subject: [PATCH 32/35] test(grammar): record definitions parse() doesn't handle yet coll.g4 accepts these, but StringParser.parse doesn't handle them: logical operators and not throw NullReferenceException, a set or list literal after subset throws InvalidOperationException (Stack empty), "x in A" is never checked, and a malformed definition is printed to the console and read as best ANTLR can instead of throwing. Each test states the wanted behaviour and is skipped, with today's behaviour as the reason, until it's fixed. --- tests/SPADE.Tests/ConstraintGapTests.cs | 51 +++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/SPADE.Tests/ConstraintGapTests.cs diff --git a/tests/SPADE.Tests/ConstraintGapTests.cs b/tests/SPADE.Tests/ConstraintGapTests.cs new file mode 100644 index 0000000..6a0bae2 --- /dev/null +++ b/tests/SPADE.Tests/ConstraintGapTests.cs @@ -0,0 +1,51 @@ +using SPADE; +using Xunit; + +namespace SPADE.Tests; + +// Definitions coll.g4 accepts but StringParser.parse doesn't handle. Each test states the behaviour +// wanted and is skipped until it's fixed; the reason says what happens today. +public class ConstraintGapTests +{ + private static StringParser Parse(string grammar, string instance) + { + StringParser parser = new StringParser(grammar); + parser.parse(instance); + return parser; + } + + [Fact(Skip = "Bug: not, and, or and the other logical operators parse, but parse() throws NullReferenceException in codeGen.ExitConstraint")] + public void LogicalConstraintsAreChecked() + { + Assert.Equal("a", Parse("{N | not N is int}", "a")["N"].ToString()); + Assert.Throws(() => Parse("{N | not N is int}", "5")); + Assert.Equal(new UtilCollection("{a}"), Parse("{N | N is set or N is list}", "{a}")["N"]); + Assert.Throws(() => Parse("{N | N is set and N is list}", "{a}")); + } + + [Fact(Skip = "Bug: a set or list literal on the right of subset throws InvalidOperationException (Stack empty) from codeGen.ExitSet")] + public void SubsetOfALiteralIsChecked() + { + Assert.Equal(new UtilCollection("{a}"), Parse("{N | N subset {a,b}}", "{a}")["N"]); + SpadeParseException e = Assert.Throws(() => Parse("{N | N subset {a,b}}", "{c}")); + Assert.Equal("N", e.Part); + Assert.Equal("c", e.Element); + } + + [Fact(Skip = "Bug: 'x in A' is never checked, so ({1},2) is accepted (ConstraintDescriptor.check has no 'in' case; see #14)")] + public void InChecksMembership() + { + Assert.Equal("1", Parse("{(A,x) | A is set, x in A}", "({1},1)")["x"].ToString()); + SpadeParseException e = Assert.Throws(() => Parse("{(A,x) | A is set, x in A}", "({1},2)")); + Assert.Equal("x", e.Part); + } + + [Theory(Skip = "Bug: parse() keeps ANTLR's default error recovery, so a malformed definition is printed to the console and read as best it can, instead of throwing like Definition does")] + [InlineData("{N | N is set")] + [InlineData("{N | N is set#}")] + [InlineData("{N | N is set}}")] + public void MalformedDefinitionThrowsOnParse(string grammar) + { + Assert.Throws(() => Parse(grammar, "{a}")); + } +} From f3f5a4257391f7b78f3f58c8070c51a99009290c Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:41:27 -0600 Subject: [PATCH 33/35] ci(rbs): raise the coverage floor to 80% Line coverage, counted the way rbs counts it (generated parser included), is now about 93%, up from about 82%. The floor was 70% with 80% as the goal; 80% is now the floor. --- rbs.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rbs.toml b/rbs.toml index cfbb4f0..a259daf 100644 --- a/rbs.toml +++ b/rbs.toml @@ -5,5 +5,5 @@ engine = "dotnet" solution = "SPADE.slnx" [unit-test] -# The standard we're holding to. Floor 70%; the goal is 80%+. New code needs tests to keep it above the floor. -coverage-min = 70 +# The standard we're holding to. Floor 80%. New code needs tests to keep it above the floor. +coverage-min = 80 From 570bc058442ac1e366a4d48d338b8469425cc5da Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:56:16 -0600 Subject: [PATCH 34/35] fix(parser): reject malformed definition strings instead of guessing StringParser kept ANTLR's default error listener on the definition string, which printed the error to the console and recovered with a best guess, so "{N | N is set" (missing brace), "{N | N is set#}" and "{N | N is set}}" were accepted. #4 rules out best-guess parsing, and Definition already rejected the same strings. The throwing listener moves out of DefinitionDescription into its own internal class, with one Parse method both use. The StringParser constructor now reads the definition string through it, so a malformed one throws the same FormatException as Definition, naming the string and the position of the first error, before any instance is parsed. Nothing is printed. parse() walks the tree read at construction instead of reading the definition string again on every call. --- DefinitionDescription.cs | 38 +--------- Documentation/example.md | 4 +- StringParser.cs | 20 ++--- ThrowingErrorListener.cs | 53 +++++++++++++ .../SPADE.Tests/DefinitionDescriptionTests.cs | 75 ++++++++++++++++++- 5 files changed, 138 insertions(+), 52 deletions(-) create mode 100644 ThrowingErrorListener.cs diff --git a/DefinitionDescription.cs b/DefinitionDescription.cs index 1cde2ab..bbbbbb7 100644 --- a/DefinitionDescription.cs +++ b/DefinitionDescription.cs @@ -121,17 +121,7 @@ public bool TryGetPart(string name, out DefinitionPart? part) /// public static DefinitionDescription Parse(string definition) { - if (definition == null) throw new FormatException("Definition string is null"); - - ThrowingErrorListener listener = new(definition); - collLexer lexer = new(new AntlrInputStream(definition)); - lexer.RemoveErrorListeners(); - lexer.AddErrorListener(listener); - collParser parser = new(new CommonTokenStream(lexer)); - parser.RemoveErrorListeners(); - parser.AddErrorListener(listener); - - collParser.SetContext set = parser.start().set(); + collParser.SetContext set = ThrowingErrorListener.Parse(definition).set(); collParser.FormatContext? format = set.format(); if (format == null) throw new FormatException($"Definition string \"{definition}\" is a list of values, not a format followed by |"); @@ -273,30 +263,4 @@ private static string SourceText(ParserRuleContext context) { return context.Start.InputStream.GetText(Interval.Of(context.Start.StartIndex, context.Stop.StopIndex)); } - - // Turns the first lexer or parser error into a FormatException naming the definition string. - private class ThrowingErrorListener : IAntlrErrorListener, IAntlrErrorListener - { - private readonly string definition; - - public ThrowingErrorListener(string definition) - { - this.definition = definition; - } - - public void SyntaxError(TextWriter output, IRecognizer recognizer, int offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) - { - Fail(charPositionInLine, msg); - } - - public void SyntaxError(TextWriter output, IRecognizer recognizer, IToken offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) - { - Fail(charPositionInLine, msg); - } - - private void Fail(int charPositionInLine, string msg) - { - throw new FormatException($"Definition string \"{definition}\" can't be read: {msg} at position {charPositionInLine + 1}"); - } - } } diff --git a/Documentation/example.md b/Documentation/example.md index c14a4a0..ba61487 100644 --- a/Documentation/example.md +++ b/Documentation/example.md @@ -46,7 +46,7 @@ catch (SpadeParseException e) } ``` -Any other exception type means a bug in SPADE or in the definition string, not bad input. +Any other exception type means a bug in SPADE or in the definition string, not bad input. A definition string that can't be read throws `FormatException` as soon as the `StringParser` is created, before any instance is parsed. ## Describing a definition string @@ -71,4 +71,4 @@ Console.WriteLine(d["E"].Constraints[0]); // E subset N unorderedcross N Each part has a `Name`, a `Kind` (`Set`, `Multiset`, `List`, `Int`, `Subset`, `Member`, or `Any` when nothing says what it is), `Of` (what it is a subset or member of, as written), `Path` (where it sits in the outline), and `Constraints` (every constraint naming it, as written). Only the outer format's names are parts; names inside a nested set, like `a` in `S subset {a | a subset U}`, are not. -`Definition` is read the first time you use it, so creating a `StringParser` never throws. If the definition string can't be read, `Definition` throws a `FormatException` naming it. `DefinitionDescription.Parse(definitionString)` does the same without a `StringParser`. +Creating a `StringParser` reads its definition string, so a malformed one, such as `{N | N is set` with its closing brace missing, throws a `FormatException` naming it and the position of the first error, instead of being guessed at. `Definition` is worked out the first time you use it. `DefinitionDescription.Parse(definitionString)` reads a definition string the same way without a `StringParser`, and throws the same `FormatException`. diff --git a/StringParser.cs b/StringParser.cs index 13e68ad..11dbe51 100644 --- a/StringParser.cs +++ b/StringParser.cs @@ -15,16 +15,25 @@ public class StringParser private bool hasParsed; + // The definition string's parse tree, read once when the parser is made. + private readonly IParseTree definitionTree; + private DefinitionDescription? definition; /// /// The names and types the definition string declares, read without parsing an instance. - /// Read on first use; throws FormatException if the definition string can't be read. + /// Read on first use; throws FormatException when the definition string is a list of values, not a format followed by |. /// public DefinitionDescription Definition => definition ??= DefinitionDescription.Parse(format); + /// + /// Makes a parser for instances of a definition string such as "{(N,E) | N is set, E subset N unorderedcross N}". + /// Throws FormatException naming the definition string, and the position of the first error, when it can't be read; + /// a malformed definition is rejected rather than guessed at. + /// public StringParser(string f) { + definitionTree = ThrowingErrorListener.Parse(f); format = f; hasParsed = false; } @@ -32,15 +41,8 @@ public StringParser(string f) public void parse(string instance) { hasParsed = true; - AntlrInputStream stream = new AntlrInputStream(format); - collLexer lex = new collLexer(stream); - CommonTokenStream tokens = new CommonTokenStream(lex); - collParser parser = new collParser(tokens); - - parser.BuildParseTree = true; - IParseTree parseTree = parser.start(); codeGen gen = new codeGen(); - ParseTreeWalker.Default.Walk(gen, parseTree); + ParseTreeWalker.Default.Walk(gen, definitionTree); gen.Parse(new UtilCollection(instance)); diff --git a/ThrowingErrorListener.cs b/ThrowingErrorListener.cs new file mode 100644 index 0000000..0f46be1 --- /dev/null +++ b/ThrowingErrorListener.cs @@ -0,0 +1,53 @@ +using Antlr4.Runtime; + +using SPADE.Grammar; + +namespace SPADE; + +/// +/// Turns the first lexer or parser error into a FormatException naming the definition string, +/// instead of ANTLR's default of printing the error and guessing past it (#4). +/// +internal class ThrowingErrorListener : IAntlrErrorListener, IAntlrErrorListener +{ + private readonly string definition; + + public ThrowingErrorListener(string definition) + { + this.definition = definition; + } + + /// + /// Reads a definition string into its parse tree. Throws FormatException naming the definition string, + /// and the position of the first error, when it can't be read. Nothing is printed. + /// + public static collParser.StartContext Parse(string definition) + { + if (definition == null) throw new FormatException("Definition string is null"); + + ThrowingErrorListener listener = new(definition); + collLexer lexer = new(new AntlrInputStream(definition)); + lexer.RemoveErrorListeners(); + lexer.AddErrorListener(listener); + collParser parser = new(new CommonTokenStream(lexer)); + parser.RemoveErrorListeners(); + parser.AddErrorListener(listener); + parser.BuildParseTree = true; + return parser.start(); + } + + public void SyntaxError(TextWriter output, IRecognizer recognizer, int offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) + { + Fail(charPositionInLine, msg); + } + + public void SyntaxError(TextWriter output, IRecognizer recognizer, IToken offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) + { + Fail(charPositionInLine, msg); + } + + private void Fail(int charPositionInLine, string msg) + { + throw new FormatException($"Definition string \"{definition}\" can't be read: {msg} at position {charPositionInLine + 1}"); + } +} diff --git a/tests/SPADE.Tests/DefinitionDescriptionTests.cs b/tests/SPADE.Tests/DefinitionDescriptionTests.cs index 7c04c5e..0c53274 100644 --- a/tests/SPADE.Tests/DefinitionDescriptionTests.cs +++ b/tests/SPADE.Tests/DefinitionDescriptionTests.cs @@ -170,14 +170,81 @@ public void UnknownNameThrowsKeyNotFound() } [Fact] - public void BadDefinitionThrowsOnlyWhenRead() + public void BadDefinitionThrowsWhenTheParserIsMade() { - StringParser parser = new("not a definition"); - - FormatException e = Assert.Throws(() => parser.Definition); + FormatException e = Assert.Throws(() => new StringParser("not a definition")); Assert.Contains("not a definition", e.Message); } + // A malformed definition string is rejected, not guessed at (#4). + [Theory] + [InlineData("{N | N is set", "Definition string \"{N | N is set\" can't be read: mismatched input '' expecting {'}', CM} at position 14")] + [InlineData("{N | N is set#}", "Definition string \"{N | N is set#}\" can't be read: token recognition error at: '#' at position 14")] + [InlineData("{N | N is set}}", "Definition string \"{N | N is set}}\" can't be read: extraneous input '}' expecting at position 15")] + public void MalformedDefinitionThrowsWithPosition(string definition, string message) + { + FormatException e = Assert.Throws(() => new StringParser(definition)); + Assert.Equal(message, e.Message); + } + + [Theory] + [InlineData("{N | N is set")] + [InlineData("{N | N is set#}")] + [InlineData("{N | N is set}}")] + [InlineData("not a definition")] + public void StringParserAndDefinitionGiveTheSameError(string definition) + { + FormatException fromParser = Assert.Throws(() => new StringParser(definition)); + FormatException fromDefinition = Assert.Throws(() => DefinitionDescription.Parse(definition)); + Assert.Equal(fromDefinition.Message, fromParser.Message); + } + + [Fact] + public void MalformedDefinitionPrintsNothing() + { + TextWriter oldOut = Console.Out; + TextWriter oldError = Console.Error; + StringWriter output = new(); + try + { + Console.SetOut(output); + Console.SetError(output); + Assert.Throws(() => new StringParser("{N | N is set#}")); + Assert.Throws(() => new StringParser("{N | N is set")); + } + finally + { + Console.SetOut(oldOut); + Console.SetError(oldError); + } + Assert.Equal("", output.ToString()); + } + + [Theory] + [InlineData("{N | N is set}", "{1,2}")] + [InlineData("{(N,E) | N is set, E subset N unorderedcross N}", "({1,2},{{1,2}})")] + [InlineData("{((N,E),K) | N is set, E subset N unorderedcross N, K is int}", "(({1,2},{{1,2}}),1)")] + [InlineData("{(N,E) | N is set, E subset {(e, w) | e is N unorderedcross N, w is int}}", "({1,2},{({1,2},3)})")] + public void ValidDefinitionsStillConstructAndParse(string definition, string instance) + { + StringParser parser = new(definition); + parser.parse(instance); + Assert.NotEmpty(parser.map); + } + + [Fact] + public void OneParserParsesSeveralInstances() + { + StringParser parser = new("{(N,E) | N is set, E subset N unorderedcross N}"); + + parser.parse("({1,2},{{1,2}})"); + Assert.Equal(new UtilCollection("{1,2}"), parser["N"]); + + parser.parse("({a,b,c},{{a,c}})"); + Assert.Equal(new UtilCollection("{a,b,c}"), parser["N"]); + Assert.Equal(new UtilCollection("{{a,c}}"), parser["E"]); + } + [Fact] public void ParseDirectlyMatchesTheProperty() { From 9102b62ba35f06e87402187c8f31fbe98ef1408f Mon Sep 17 00:00:00 2001 From: Michael Trosper <140276486+Trosper3@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:00:32 -0600 Subject: [PATCH 35/35] test(grammar): drop the malformed-definition gap, now fixed StringParser now rejects a malformed definition string when it is made, and DefinitionDescriptionTests (#24) checks the same three strings, their exact messages and that nothing is printed. The skipped theory here would only repeat them, so it goes; the three gaps left are #40, #41 and #14. --- tests/SPADE.Tests/ConstraintGapTests.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/SPADE.Tests/ConstraintGapTests.cs b/tests/SPADE.Tests/ConstraintGapTests.cs index 6a0bae2..4ba653e 100644 --- a/tests/SPADE.Tests/ConstraintGapTests.cs +++ b/tests/SPADE.Tests/ConstraintGapTests.cs @@ -39,13 +39,4 @@ public void InChecksMembership() SpadeParseException e = Assert.Throws(() => Parse("{(A,x) | A is set, x in A}", "({1},2)")); Assert.Equal("x", e.Part); } - - [Theory(Skip = "Bug: parse() keeps ANTLR's default error recovery, so a malformed definition is printed to the console and read as best it can, instead of throwing like Definition does")] - [InlineData("{N | N is set")] - [InlineData("{N | N is set#}")] - [InlineData("{N | N is set}}")] - public void MalformedDefinitionThrowsOnParse(string grammar) - { - Assert.Throws(() => Parse(grammar, "{a}")); - } }