Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 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
aa5b2e2
fix(grammar): make is int, is set and is list check their type
Trosper3 Oct 1, 2026
8ba6e46
feat(parser): read instance strings in one pass
Trosper3 Oct 1, 2026
3935e69
fix(grammar): report shape and constraint failures as SpadeParseExcep…
Trosper3 Oct 1, 2026
42d4975
feat(collections): add multisets
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
bf35f77
Merge branch 'fix/12-unorderedcross-edges' into fix/10-type-constraints
Trosper3 Oct 12, 2026
cfe1889
Merge branch 'fix/10-type-constraints' into fix/2-instance-scanner
Trosper3 Oct 12, 2026
b6bc585
Merge branch 'fix/2-instance-scanner' into feat/1-multisets
Trosper3 Oct 12, 2026
a3debad
feat(definition): describe a definition's parts without parsing an in…
Trosper3 Oct 13, 2026
fedce94
feat(errors): name the part, element and missing value on SpadeParseE…
Trosper3 Oct 13, 2026
e928cae
feat(grammar): add an is string type check
Trosper3 Oct 13, 2026
e427582
feat(parser): allow quoted values that contain , { } ( )
Trosper3 Oct 13, 2026
2e89bc1
feat(grammar): add a number type for decimal values
Trosper3 Oct 13, 2026
1dd60a5
Read int with the invariant culture, like number
Trosper3 Oct 14, 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
6e3c2d0
Merge branch 'fix/12-unorderedcross-edges' into fix/10-type-constraints
Trosper3 Oct 15, 2026
c6e951c
Merge branch 'fix/10-type-constraints' into fix/2-instance-scanner
Trosper3 Oct 15, 2026
8f68aa3
style: run dotnet format
Trosper3 Oct 15, 2026
bbac0c6
Merge branch 'fix/2-instance-scanner' into feat/1-multisets
Trosper3 Oct 15, 2026
920a883
style: run dotnet format
Trosper3 Oct 15, 2026
986ec44
Merge branch 'feat/1-multisets' into feat/18-describe-definition
Trosper3 Oct 15, 2026
65be272
style: run dotnet format
Trosper3 Oct 15, 2026
cebae92
Merge branch 'feat/18-describe-definition' into feat/19-error-part
Trosper3 Oct 15, 2026
d258795
style: run dotnet format
Trosper3 Oct 15, 2026
b4834b1
Merge branch 'feat/19-error-part' into feat/22-is-string
Trosper3 Oct 15, 2026
485dac3
style: run dotnet format
Trosper3 Oct 15, 2026
1241c15
Merge branch 'feat/22-is-string' into feat/23-number
Trosper3 Oct 15, 2026
1e73384
style: run dotnet format
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
0a3262d
Merge branch 'fix/12-unorderedcross-edges' into fix/10-type-constraints
Trosper3 Oct 15, 2026
ad07a09
Merge branch 'fix/10-type-constraints' into fix/2-instance-scanner
Trosper3 Oct 15, 2026
615b81c
Merge branch 'fix/2-instance-scanner' into feat/1-multisets
Trosper3 Oct 15, 2026
4c3677f
Merge branch 'feat/1-multisets' into feat/18-describe-definition
Trosper3 Oct 15, 2026
1b50981
Merge branch 'feat/18-describe-definition' into feat/19-error-part
Trosper3 Oct 15, 2026
c37cff5
Merge branch 'feat/19-error-part' into feat/22-is-string
Trosper3 Oct 15, 2026
0c19bac
Merge branch 'feat/22-is-string' into feat/23-number
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
0302438
Merge branch 'fix/12-unorderedcross-edges' into fix/10-type-constraints
Trosper3 Oct 15, 2026
3ee32cb
Merge branch 'fix/10-type-constraints' into fix/2-instance-scanner
Trosper3 Oct 15, 2026
f9e9732
Merge branch 'fix/2-instance-scanner' into feat/1-multisets
Trosper3 Oct 15, 2026
f537955
Merge branch 'feat/1-multisets' into feat/18-describe-definition
Trosper3 Oct 15, 2026
2a594f7
Merge branch 'feat/18-describe-definition' into feat/19-error-part
Trosper3 Oct 15, 2026
08fe49b
Merge branch 'feat/19-error-part' into feat/22-is-string
Trosper3 Oct 15, 2026
97b1a06
Merge branch 'feat/22-is-string' into feat/23-number
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
26214bd
Merge branch 'fix/12-unorderedcross-edges' into fix/10-type-constraints
Trosper3 Oct 15, 2026
c8f3b9d
Merge branch 'fix/10-type-constraints' into fix/2-instance-scanner
Trosper3 Oct 15, 2026
2c08f26
Merge branch 'fix/2-instance-scanner' into feat/1-multisets
Trosper3 Oct 15, 2026
6255615
Merge branch 'feat/1-multisets' into feat/18-describe-definition
Trosper3 Oct 15, 2026
b959337
Merge branch 'feat/18-describe-definition' into feat/19-error-part
Trosper3 Oct 15, 2026
3c292e6
Merge branch 'feat/19-error-part' into feat/22-is-string
Trosper3 Oct 15, 2026
79fcfd1
Merge branch 'feat/22-is-string' into feat/23-number
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
62cc7d5
Merge branch 'fix/12-unorderedcross-edges' into fix/10-type-constraints
Trosper3 Oct 15, 2026
d3a486c
Merge branch 'fix/10-type-constraints' into fix/2-instance-scanner
Trosper3 Oct 15, 2026
17e7b77
Merge branch 'fix/2-instance-scanner' into feat/1-multisets
Trosper3 Oct 15, 2026
7041a2e
fix(collections): freeze multisets once hashed
Trosper3 Oct 15, 2026
c7a5966
Merge branch 'feat/1-multisets' into feat/18-describe-definition
Trosper3 Oct 15, 2026
ad69499
Merge branch 'feat/18-describe-definition' into feat/19-error-part
Trosper3 Oct 15, 2026
ac1faa3
Merge branch 'feat/19-error-part' into feat/22-is-string
Trosper3 Oct 15, 2026
b045d63
Merge branch 'feat/22-is-string' into feat/23-number
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
b84c517
Merge branch 'fix/12-unorderedcross-edges' into fix/10-type-constraints
Trosper3 Oct 16, 2026
a5d2b6c
Merge branch 'fix/10-type-constraints' into fix/2-instance-scanner
Trosper3 Oct 16, 2026
8cd90fb
Merge branch 'fix/2-instance-scanner' into feat/1-multisets
Trosper3 Oct 16, 2026
e3d13ce
Merge branch 'feat/1-multisets' into feat/18-describe-definition
Trosper3 Oct 16, 2026
a18d45f
Merge branch 'feat/18-describe-definition' into feat/19-error-part
Trosper3 Oct 16, 2026
fd5fe03
Merge branch 'feat/19-error-part' into feat/22-is-string
Trosper3 Oct 16, 2026
13d691b
Merge branch 'feat/22-is-string' into feat/23-number
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
1c29c7e
Merge branch 'fix/12-unorderedcross-edges' into fix/10-type-constraints
Trosper3 Oct 16, 2026
9427f94
Merge branch 'fix/10-type-constraints' into fix/2-instance-scanner
Trosper3 Oct 16, 2026
daae482
Merge branch 'fix/2-instance-scanner' into feat/1-multisets
Trosper3 Oct 16, 2026
a2391d9
Merge branch 'feat/1-multisets' into feat/18-describe-definition
Trosper3 Oct 16, 2026
d263a79
Merge branch 'feat/18-describe-definition' into feat/19-error-part
Trosper3 Oct 16, 2026
cc2d436
Merge branch 'feat/19-error-part' into feat/22-is-string
Trosper3 Oct 16, 2026
e9113de
Merge branch 'feat/22-is-string' into feat/23-number
Trosper3 Oct 16, 2026
6ed9711
test(collections): cover union, cross and the shape checks
Trosper3 Oct 16, 2026
0853316
test(grammar): cover union, intersection and the parse tree
Trosper3 Oct 16, 2026
4a01a8b
test(grammar): record definitions parse() doesn't handle yet
Trosper3 Oct 16, 2026
f3f5a42
ci(rbs): raise the coverage floor to 80%
Trosper3 Oct 16, 2026
570bc05
fix(parser): reject malformed definition strings instead of guessing
Trosper3 Oct 16, 2026
23397e0
Merge branch 'feat/18-describe-definition' into feat/19-error-part
Trosper3 Oct 16, 2026
9691ebc
Merge branch 'feat/19-error-part' into feat/22-is-string
Trosper3 Oct 16, 2026
d1411ae
Merge branch 'feat/22-is-string' into feat/23-number
Trosper3 Oct 16, 2026
364a64f
Merge branch 'feat/23-number' into test/coverage-80
Trosper3 Oct 16, 2026
9102b62
test(grammar): drop the malformed-definition gap, now fixed
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
273 changes: 273 additions & 0 deletions DefinitionDescription.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
using Antlr4.Runtime;
using Antlr4.Runtime.Misc;

using SPADE.Grammar;

namespace SPADE;

/// <summary>
/// What a definition string's constraints say a named part is.
/// </summary>
public enum PartKind
{
/// <summary>No constraint says what the part is.</summary>
Any,

/// <summary>"is set".</summary>
Set,

/// <summary>"is multiset".</summary>
Multiset,

/// <summary>"is list".</summary>
List,

/// <summary>"is int".</summary>
Int,

/// <summary>"is number": a decimal number such as 2.5, -0.75 or .5; every int is one.</summary>
Number,

/// <summary>"is string": a single value, not a set, list or multiset.</summary>
String,

/// <summary>"subset X", with no "is" type; <see cref="DefinitionPart.Of"/> holds X.</summary>
Subset,

/// <summary>"is X" or "in X" where X is another part or a set expression; <see cref="DefinitionPart.Of"/> holds X.</summary>
Member,
}

/// <summary>
/// One named part of a definition string's format, such as N or E in "{(N,E) | ...}".
/// </summary>
public class DefinitionPart
{
/// <summary>The name as written in the format, e.g. "E".</summary>
public string Name { get; }

/// <summary>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".</summary>
public PartKind Kind { get; }

/// <summary>
/// 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.
/// </summary>
public string? Of { get; }

/// <summary>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.</summary>
public IReadOnlyList<int> Path { get; }

/// <summary>Every top-level constraint that names this part, as written, in definition order.</summary>
public IReadOnlyList<string> Constraints { get; }

internal DefinitionPart(string name, PartKind kind, string? of, IReadOnlyList<int> path, IReadOnlyList<string> constraints)
{
Name = name;
Kind = kind;
Of = of;
Path = path;
Constraints = constraints;
}

public override string ToString()
{
return Of == null ? $"{Name}: {Kind}" : $"{Name}: {Kind} of {Of}";
}
}

/// <summary>
/// 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.
/// </summary>
public class DefinitionDescription
{
/// <summary>The format with whitespace removed, e.g. "((N,E),K)".</summary>
public string Outline { get; }

/// <summary>The named parts, in the order they appear in <see cref="Outline"/>.</summary>
public IReadOnlyList<DefinitionPart> Parts { get; }

private readonly Dictionary<string, DefinitionPart> byName;

private DefinitionDescription(string outline, List<DefinitionPart> parts)
{
Outline = outline;
Parts = parts.AsReadOnly();
byName = new();
foreach (DefinitionPart part in parts)
byName.TryAdd(part.Name, part);
}

/// <summary>The part with this name. Throws KeyNotFoundException when the outline has no such name.</summary>
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}");
}
}

/// <summary>Whether the outline has a part with this name.</summary>
public bool Contains(string name)
{
return byName.ContainsKey(name);
}

/// <summary>Looks up a part by name without throwing.</summary>
public bool TryGetPart(string name, out DefinitionPart? part)
{
return byName.TryGetValue(name, out part);
}

/// <summary>
/// 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.
/// </summary>
public static DefinitionDescription Parse(string definition)
{
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 |");

// Walk the outline once for its text and the path to every name.
List<(string name, List<int> path)> names = new();
string outline = WalkOutline(format, new List<int>(), names);
HashSet<string> nameSet = names.Select(n => n.name).ToHashSet();

Dictionary<string, List<string>> constraints = new();
Dictionary<string, List<collParser.ConstraintContext>> 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<string> subjects = new();
Subjects(constraint, subjects);
foreach (string subject in subjects.Where(nameSet.Contains))
constraints[subject].Add(text);
}

List<DefinitionPart> parts = new();
HashSet<string> seen = new();
foreach ((string name, List<int> 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<int> path, List<(string, List<int>)> names)
{
if (format.TERM() != null)
{
string name = format.TERM().GetText();
names.Add((name, new List<int>(path)));
return name;
}

collParser.FormatContext[] children = format.format();
List<string> 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<string> 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/number/string" wins the kind; Of comes from the first subset, else the first membership.
private static (PartKind, string?) Classify(List<collParser.ConstraintContext> constraints, HashSet<string> 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,
"string" => PartKind.String,
_ => null,
};
type ??= builtIn;
continue;
}
if (obj.BUILT_IN_SET() != null)
{
type ??= objText == "number" ? PartKind.Number : 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 text") isn't something SPADE knows, 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));
}
}
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
5 changes: 4 additions & 1 deletion Documentation/DefinitionString.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ 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 by a definition and then an object.

### Definition
- "is" checks the type of a collection. It can be followed by "set", "list", or "int", and constrains the 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 the 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.

### Objects
Expand Down
Loading
Loading