Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
119 changes: 119 additions & 0 deletions Semantics.Cpp.Test/StrictFloorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Semantics.Cpp.Test;

using System;

using ktsu.Semantics.Cpp;
using ktsu.Semantics.Vocabulary;

using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Covers which declared constraint opts a V0 overload into the stricter floor, which is the one
/// rule about <c>physicalConstraints</c> that both projections of <c>dimensions.json</c> read.
/// </summary>
/// <remarks>
/// The rule is that <c>minExclusive: "0"</c> <em>specifically</em> opts in, and it is the
/// documented one: CLAUDE.md's design decision #4 says the strict guard is what that value asks
/// for. A reader that tested whether a constraints object was present instead answered the same
/// for the three sites the metadata declares today -- <c>Wavelength</c>, <c>Period</c> and
/// <c>HalfLife</c>, all <c>{ "minExclusive": "0" }</c> -- and would have answered differently for
/// the first constraint of any other kind, giving one declared quantity two domains depending on
/// which language it was generated into. That is ktsu-dev/Semantics#218.
/// <para>
/// So these are driven by a metadata document written for the purpose rather than by the real one:
/// the case that separates the two rules is a constraint the real file does not declare yet, and
/// the point is to fail on the day it does.
/// </para>
/// </remarks>
[TestClass]
public sealed class StrictFloorTests
{
/// <summary>
/// One dimension, whose magnitude form carries an overload per way of declaring -- or not
/// declaring -- a floor. Only <c>ZeroFloor</c> asks for the strict one.
/// </summary>
private const string Metadata = """
{
"physicalDimensions": [
{
"name": "Length",
"dimensionalFormula": { "length": 1 },
"quantities": {
"vector0": {
"base": "Length",
"overloads": [
{ "name": "NoConstraints", "description": "Declares no constraints at all." },
{ "name": "EmptyConstraints", "description": "Declares a constraints object with no floor in it.", "physicalConstraints": { } },
{ "name": "OtherFloor", "description": "Declares a floor that is not zero.", "physicalConstraints": { "minExclusive": "1" } },
{ "name": "ZeroFloor", "description": "Declares the floor that opts into the strict guard.", "physicalConstraints": { "minExclusive": "0" } }
]
}
}
}
]
}
""";

private static CppQuantityOutput Output { get; } = new CppQuantityGenerator(
new CppQuantityOptions { Namespace = "holo" }).Generate(QuantityMetadata.Parse(Metadata));

/// <summary>
/// The value the rule is written around gets the strict comparison, which is the behaviour the
/// three real constraint sites rely on.
/// </summary>
[TestMethod]
public void GuardsAZeroFloorStrictly() =>
Assert.Contains("assert(value.count() > 0", Output.Files["ZeroFloor.hpp"], StringComparison.Ordinal);

/// <summary>
/// A constraints object with no floor in it is not an opt-in. <c>MinExclusive</c> defaults to
/// empty, so a reader testing the object for null rather than reading its value turns the strict
/// guard on here -- and the C# generator, which reads the value, leaves it off.
/// </summary>
[TestMethod]
public void DoesNotGuardEmptyConstraintsStrictly() =>
Assert.Contains(
"assert(value.count() >= 0",
Output.Files["EmptyConstraints.hpp"],
StringComparison.Ordinal);

/// <summary>
/// Neither is a floor of some other value. Nothing about <c>minExclusive: "1"</c> says a
/// quantity may not be zero -- it says considerably more than that -- and no guard for it is
/// emitted, so the strict comparison would be an assertion the metadata never asked for.
/// </summary>
[TestMethod]
public void DoesNotGuardANonZeroFloorStrictly() =>
Assert.Contains("assert(value.count() >= 0", Output.Files["OtherFloor.hpp"], StringComparison.Ordinal);

/// <summary>
/// An overload with no constraints at all keeps its magnitude form's own floor, as does the base
/// it refines. This is the case the two rules always agreed about.
/// </summary>
[TestMethod]
public void GuardsAnUnconstrainedOverloadAsAMagnitude()
{
Assert.Contains("assert(value.count() >= 0", Output.Files["NoConstraints.hpp"], StringComparison.Ordinal);
Assert.Contains("assert(value.count() >= 0", Output.Files["Length.hpp"], StringComparison.Ordinal);
}

/// <summary>
/// The rule itself, stated once and asked by both readers. Testing it here rather than through
/// each projection is the point of it being one function: the C# generator and this one cannot
/// answer differently, because there is no longer a second answer for them to hold.
/// </summary>
[TestMethod]
[DataRow("0", true, "the documented opt-in")]
[DataRow(null, false, "no constraints object at all")]
[DataRow("", false, "a constraints object carrying no floor")]
[DataRow("1", false, "a floor of some other value")]
[DataRow("0.0", false, "a floor that is zero but is not spelled the way the rule names it")]
[DataRow(" 0", false, "a floor that would only match if it were trimmed first")]
public void ReadsTheStrictFloorOffTheValue(string? minExclusive, bool expected, string because) =>
Assert.AreEqual(
expected,
OverloadDeclaration.IsStrictFloor(minExclusive),
$"minExclusive {minExclusive ?? "(null)"} is {because}.");
}
10 changes: 7 additions & 3 deletions Semantics.Cpp/MetadataProjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,14 @@ [.. dimension.DotProducts.Select(Relationship)],
// A constraint is carried as the flag the vocabulary reads rather than as its value: only a
// strict-positive floor is declared anywhere, and what the vocabulary needs is whether it is
// there. The value itself is the C# generator's business, which is where the guard is emitted.
// Read off the constraint's value rather than off the presence of the object holding it, so a
// constraint of some other kind, when one is added, does not silently turn the strict floor on.
// Which values mean "strict" is the vocabulary's rule rather than this reader's, so it is asked
// rather than restated: reading the presence of the object instead of its value is #218, and a
// copy of the rule per reader is how the two projections got to hold different ones.
private static OverloadDeclaration Overload(MetadataOverload overload) =>
new(overload.Name, overload.Description, overload.PhysicalConstraints?.MinExclusive == "0");
new(
overload.Name,
overload.Description,
OverloadDeclaration.IsStrictFloor(overload.PhysicalConstraints?.MinExclusive));

private static RelationshipDeclaration Relationship(MetadataRelationship relationship) =>
new(relationship.Other, relationship.Result, [.. relationship.Forms]);
Expand Down
11 changes: 7 additions & 4 deletions Semantics.SourceGenerators/Models/VocabularyProjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,14 @@ [.. dimension.DotProducts.Select(Relationship)],

// The constraint is carried as the flag the vocabulary reads rather than as its value: what the
// vocabulary needs is whether a stricter floor is declared. The value itself stays here, where
// the Vector0Guards.EnsurePositive call is emitted from — and the flag is read off that value
// rather than off the presence of the object holding it, so a constraint of some other kind,
// when one is added, does not silently turn the strict floor on.
// the Vector0Guards.EnsurePositive call is emitted from — and which values mean "strict" is the
// vocabulary's rule rather than this reader's, so it is asked rather than restated: a copy of
// the rule per reader is how the two projections came to hold different ones in #218.
private static OverloadDeclaration Overload(OverloadDefinition overload) =>
new(overload.Name, overload.Description, overload.PhysicalConstraints?.MinExclusive == "0");
new(
overload.Name,
overload.Description,
OverloadDeclaration.IsStrictFloor(overload.PhysicalConstraints?.MinExclusive));

private static RelationshipDeclaration Relationship(RelationshipDefinition relationship) =>
new(relationship.Other, relationship.Result, [.. relationship.Forms]);
Expand Down
29 changes: 28 additions & 1 deletion Semantics.Vocabulary/DimensionDeclaration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace ktsu.Semantics.Vocabulary;

using System;
using System.Collections.Generic;

/// <summary>
Expand Down Expand Up @@ -66,7 +67,33 @@ internal sealed record FormDeclaration(string Base, IReadOnlyList<OverloadDeclar
/// A flag rather than the constraint itself, because the constraint's value is the only one either
/// side declares and what the vocabulary needs from it is whether it is there.
/// </remarks>
internal sealed record OverloadDeclaration(string Name, string Description, bool IsStrictlyPositive);
internal sealed record OverloadDeclaration(string Name, string Description, bool IsStrictlyPositive)
{
/// <summary>
/// The one <c>physicalConstraints.minExclusive</c> value that opts an overload into the stricter
/// floor.
/// </summary>
internal const string StrictFloor = "0";

/// <summary>
/// Decides whether a declared <c>physicalConstraints.minExclusive</c> opts into the stricter
/// floor.
/// </summary>
/// <param name="minExclusive">
/// The value the overload declares, null when it declares no constraints at all, and empty when
/// it declares a constraints object without this field.
/// </param>
/// <returns>Whether the overload is strictly positive.</returns>
/// <remarks>
/// One implementation because there are two readers. The rule is that
/// <c>minExclusive: "0"</c> <em>specifically</em> opts in -- not that constraints are present --
/// so a constraint of some other kind, when one is added, does not silently turn the strict
/// floor on in one projection and leave it off in the other. Reading the presence of the object
/// instead is ktsu-dev/Semantics#218, and a copy of the rule per reader is how it got there.
/// </remarks>
internal static bool IsStrictFloor(string? minExclusive) =>
string.Equals(minExclusive, StrictFloor, StringComparison.Ordinal);
}

/// <summary>One declared relationship between dimensions.</summary>
/// <param name="Other">The dimension on the other side of the operator.</param>
Expand Down
Loading