Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
88bdc62
fix(collections): hash sets independently of element order
Trosper3 Oct 1, 2026
0f38e6f
fix(grammar): accept set edges in unorderedcross again
Trosper3 Oct 1, 2026
52b710f
fix(grammar): reject list edges in unorderedcross
Trosper3 Oct 1, 2026
b044325
Merge remote-tracking branch 'upstream/main' into fix/9-set-hashing
Trosper3 Oct 12, 2026
75a183e
Merge branch 'fix/9-set-hashing' into fix/12-unorderedcross-edges
Trosper3 Oct 12, 2026
ac55d26
ci: run the Redux Build System (rbs) report, soft for now
Trosper3 Oct 15, 2026
4d63d9a
style: run dotnet format
Trosper3 Oct 15, 2026
1f883b0
Merge branch 'ci/rbs' into fix/9-set-hashing
Trosper3 Oct 15, 2026
e895be2
test: add the test project to the solution and collect coverage
Trosper3 Oct 15, 2026
89de00a
Merge branch 'fix/9-set-hashing' into fix/12-unorderedcross-edges
Trosper3 Oct 15, 2026
7145b7d
ci: run the devcontainer on SDK 10 alone
Trosper3 Oct 15, 2026
75ed99a
Merge branch 'ci/rbs' into fix/9-set-hashing
Trosper3 Oct 15, 2026
8829639
test: run the tests on .NET 10
Trosper3 Oct 15, 2026
23a93e3
Merge branch 'fix/9-set-hashing' into fix/12-unorderedcross-edges
Trosper3 Oct 15, 2026
082150d
ci: keep the coverage note in rbs.toml free of a per-branch figure
Trosper3 Oct 15, 2026
5ce502a
Merge branch 'fix/9-set-hashing' into fix/12-unorderedcross-edges
Trosper3 Oct 15, 2026
278e91f
Merge remote-tracking branch 'rx/main' into ci/rbs
Trosper3 Oct 15, 2026
32af284
Merge branch 'ci/rbs' into fix/9-set-hashing
Trosper3 Oct 15, 2026
0e82c8a
Merge branch 'fix/9-set-hashing' into fix/12-unorderedcross-edges
Trosper3 Oct 15, 2026
dbc816b
fix(collections): freeze a collection once it is hashed
Trosper3 Oct 15, 2026
1a78795
Merge branch 'fix/9-set-hashing' into fix/12-unorderedcross-edges
Trosper3 Oct 15, 2026
45eea96
fix(collections): make the cached hash safe to read from several threads
Trosper3 Oct 16, 2026
8be5c33
Merge branch 'fix/9-set-hashing' into fix/12-unorderedcross-edges
Trosper3 Oct 16, 2026
2b8b018
feat(collections): add Freeze() and document using SPADE from several…
Trosper3 Oct 16, 2026
107ff3b
Merge branch 'fix/9-set-hashing' into fix/12-unorderedcross-edges
Trosper3 Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"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
},
"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"
}
4 changes: 1 addition & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 '(?<=<Version>)[^<]+' DiscreteParser.csproj)
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/rbs.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
bin/
obj/
DF.sln
.rbs/
report.md
2 changes: 2 additions & 0 deletions DiscreteParser.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<Authors>Russell Phillips</Authors>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
<!-- The test project has its own csproj; keep its files out of the library. -->
<DefaultItemExcludes>$(DefaultItemExcludes);tests/**</DefaultItemExcludes>
</PropertyGroup>

<ItemGroup>
Expand Down
9 changes: 9 additions & 0 deletions Documentation/development.md
Original file line number Diff line number Diff line change
@@ -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 10, so you need the .NET 10 SDK or newer. The library itself still targets .NET 6, and SDK 10 builds it.


## antlr
If you need to edit the grammar inside of coll.g4 file, use the following commands (you will need to install antlr4):
Expand Down
1 change: 1 addition & 0 deletions Documentation/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
35 changes: 35 additions & 0 deletions Documentation/threads.md
Original file line number Diff line number Diff line change
@@ -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<UtilCollection>` 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<string, int> 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.
24 changes: 21 additions & 3 deletions Documentation/utilcollection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand All @@ -66,7 +66,25 @@ 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.

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#
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<UtilCollection>` or `HashSet<UtilCollection>` 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.
Expand Down
6 changes: 6 additions & 0 deletions SPADE.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Solution>
<Folder Name="/tests/">
<Project Path="tests/SPADE.Tests/SPADE.Tests.csproj" />
</Folder>
<Project Path="DiscreteParser.csproj" />
</Solution>
7 changes: 4 additions & 3 deletions StringParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
using SPADE.Grammar;

namespace SPADE;

public class StringParser
{
readonly string format;
public Dictionary<string, UtilCollection> map = new();

private bool hasParsed;

public StringParser(string f)
Expand All @@ -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);

Expand All @@ -35,7 +36,7 @@ public void parse(string instance)
gen.Parse(new UtilCollection(instance));


map = gen.map;
map = gen.map;

}

Expand Down
Loading
Loading