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
30 changes: 29 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The opt-in lives in `.sonarlint/sonar-local.props` (analyzer package) and `.sona
| `Semantics.Quantities.{Double,Float,Decimal}` | Props-only satellite packages. Each ships a `buildTransitive` props file (generated by `scripts/Generate-AliasProps.ps1`) that injects global-using aliases binding every quantity to one storage type, so consumers write `Mass` instead of `Mass<double>`. |
| `Semantics.Vocabulary` | **Shared source, not a project.** Resolves `dimensions.json` into the quantities and operators it describes and separates out what cannot be honoured. Compiled into both `Semantics.SourceGenerators` and `Semantics.Cpp` via `Compile Include`; see its README for why source rather than an assembly, and what that costs. |
| `Semantics.Cpp` | The C++ projection of the quantity vocabulary, in its own project because `ktsu.Coder` ships no `net8.0`. Reads `dimensions.json` and emits one C++ class per dimension, per vector form and per named overload, plus the declared relationships as operators. |
| `Semantics.Cpp.Test` | Its tests, including ones that compile the whole generated vocabulary with `g++`/`clang++`, assert what it means through `static_assert`, and check that a dimensionally wrong product — scalar or componentwise — is refused by the compiler. |
| `Semantics.Cpp.Test` | Its tests, including ones that compile the whole generated vocabulary with `g++`/`clang++`, assert what it means through `static_assert`, and check that a dimensionally wrong product — scalar or componentwise — is refused by the compiler. Also `SevenTargetProjectionTests`, which is not about C++ at all: see below. |
| `Semantics.Test` | MSTest project covering all of the above. |

## Semantic quantities architecture (the unified vector model)
Expand Down Expand Up @@ -198,6 +198,34 @@ vectors. And a relationship is emitted in the direction the metadata declares it
`Duration * Velocity3D` is not an overload; the .NET generator emits the commutative and inverse
forms as well, and matching it is a change to every form at once rather than part of this.

### Can the quantities be generated for languages other than C# and C++?

`SevenTargetProjectionTests` is the probe that answers it, and the answer so far is **five of
seven**. It builds one quantity — the magnitude form of `Length`, read from the real
`dimensions.json` — as a language-agnostic `ktsu.Coder` AST and writes it in all seven of that
library's targets. The neutral shape is the whole of what a generated magnitude is: a record struct
over a storage type, with a value and a unit factory.

C# comes out as exactly what `QuantitiesGenerator` writes today, which is the result that matters:
the AST is expressive enough for the quantities, so what the other six do is a question about those
languages rather than about the model.

**Two targets write source their own toolchain refuses**, both recorded upstream and neither fixable
here:

| Target | What comes out | Why |
|---|---|---|
| Go | `type Length struct` with no parameters, then `func LengthFromMeter(value T) Length[T]` | ktsu-dev/Coder#63 — a generic type is deliberately written down rather than emitted, and the constructor was not given the same treatment. `go vet` says `undefined: T`. |
| Python | `class Length(IVector0[Length[T], T])` | ktsu-dev/Coder#64 — Python evaluates a base list eagerly, so the self-type idiom every quantity is declared with raises `NameError` on import. |

The tests **pin** both rather than skipping them, so the day either is fixed upstream the test fails
and is updated to assert the fix.

The probe lives in `Semantics.Cpp.Test` because that is where the reader of `dimensions.json` is,
and that is itself the finding about this repository: `QuantityMetadata` and `MetadataProjection`
decide nothing about C++ and would have to be shared the way `Semantics.Vocabulary` already is
before a neutral projection could be a project of its own.

### Physical constants

`PhysicalConstants` is **generated** from `domains.json`. Public surface:
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<!-- The C++ projection of the quantity vocabulary. ktsu.Coder publishes net10.0 and net9.0
only, which is why Semantics.Cpp is a project of its own rather than part of a library
that also ships net8.0. -->
<PackageVersion Include="ktsu.Coder" Version="3.6.0" />
<PackageVersion Include="ktsu.Coder" Version="3.11.0" />
<!-- Source generator packages -->
<PackageVersion Include="ktsu.CodeBlocker" Version="2.0.4" />
<PackageVersion Include="ktsu.SourceGeneratorToolkit" Version="1.0.2" />
Expand Down
254 changes: 254 additions & 0 deletions Semantics.Cpp.Test/SevenTargetProjectionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Semantics.Cpp.Test;

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

using ktsu.Coder.Ast;
using ktsu.Coder.Languages;

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

using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Builds one quantity as a language-agnostic AST and writes it in all seven of ktsu.Coder's
/// targets, to find out what the vocabulary cannot yet say in each.
/// </summary>
/// <remarks>
/// This is a probe rather than a generator. <see cref="CppQuantityGenerator"/> emits the whole
/// vocabulary and is C++ throughout — friend operators, <c>Quantity&lt;Dimension&lt;…&gt;&gt;</c>,
/// the shipped prelude — so it answers nothing about whether the *same* declarations reach another
/// language. This builds the neutral shape instead: a record struct over a storage type, with a
/// value and a unit factory, which is what every generated quantity in this repository is.
/// <para>
/// It lives in this project because this is where the reader of <c>dimensions.json</c> is. That is
/// itself a finding: <see cref="QuantityMetadata"/> and <see cref="MetadataProjection"/> decide
/// nothing about C++ and would have to be shared the way <c>Semantics.Vocabulary</c> already is
/// before a neutral projection could be a project of its own.
/// </para>
/// <para>
/// <b>Two targets are known to produce source their own toolchain refuses</b>, and the tests below
/// pin that rather than skipping it, so the day either is fixed upstream the test fails and says
/// so. Both are recorded against ktsu.Coder — ktsu-dev/Coder#63 and ktsu-dev/Coder#64 — and
/// neither is anything this repository can fix.
/// </para>
/// </remarks>
[TestClass]
public sealed class SevenTargetProjectionTests
{
/// <summary>
/// The storage type every generated quantity is written over.
/// </summary>
private const string StorageParameter = "T : struct, INumber<T>";

/// <summary>
/// The one quantity, built once. Every test reads the same AST, because the point is that there
/// is only one and the targets differ around it.
/// </summary>
private static ClassDeclaration Quantity { get; } = Project(MagnitudeOfLength());

private static Dictionary<string, string> Written { get; } = Write(Quantity);

/// <summary>
/// Every target writes something, which is the claim the whole exercise rests on.
/// </summary>
[TestMethod]
public void EveryTargetWritesTheQuantity()
{
Assert.HasCount(7, Written);

foreach ((string language, string source) in Written)
{
Assert.IsFalse(
string.IsNullOrWhiteSpace(source),
$"{language} wrote nothing for a declaration every other target wrote.");

Assert.Contains("Length", source, $"{language} lost the quantity's name.");
Assert.Contains("FromMeter", source, $"{language} lost the unit factory.");
}
}

/// <summary>
/// C# comes out as exactly what this repository already generates by hand.
/// </summary>
/// <remarks>
/// The one target where the answer is checkable against something that exists: if the AST can
/// reach the declaration `QuantitiesGenerator` writes, the AST is expressive enough for the
/// quantities, and the other six are a question about those languages rather than about this
/// one.
/// </remarks>
[TestMethod]
public void CSharpMatchesWhatIsGeneratedToday()
{
string source = Written["csharp"];

Assert.Contains("public readonly partial record struct Length<T> : IVector0<Length<T>, T>", source);
Assert.Contains("where T : struct, INumber<T>", source);
Assert.Contains("public T Value { get; init; }", source);
Assert.Contains("public static Length<T> FromMeter(T value)", source);
}

/// <summary>
/// The targets that have type parameters carry the storage type onto the declaration.
/// </summary>
[TestMethod]
public void TheTargetsWithTypeParametersCarryTheStorageType()
{
Assert.Contains("template <typename T>", Written["cpp"]);
Assert.Contains("struct Length<T: INumber<T>>", Written["rust"].Replace("pub ", string.Empty, StringComparison.Ordinal));
}

/// <summary>
/// The targets without type parameters write the storage type down rather than inventing one.
/// </summary>
/// <remarks>
/// C has no generics at all and JavaScript has no types to be generic over, so a note is the
/// honest answer — the same decision `CompileTimeAssertion` gets in the targets that have no
/// assertion. What matters is that neither silently drops it.
/// </remarks>
[TestMethod]
public void TheTargetsWithoutTypeParametersWriteItDown()
{
Assert.Contains(StorageParameter, Written["c"]);
Assert.Contains(StorageParameter, Written["javascript"]);
}

/// <summary>
/// Go writes source the Go toolchain refuses, because it drops the type parameter from the type
/// and then spells the type as generic anyway.
/// </summary>
/// <remarks>
/// A generic type is deliberately written down rather than emitted, because a method on one
/// needs the parameters in three places and spelled two ways. The constructor was not given the
/// same treatment, so it returns <c>Length[T]</c> from a <c>Length</c> that takes no parameters,
/// and the field's type <c>T</c> is undefined. `go vet` says `undefined: T`.
/// <para>
/// Pinned rather than skipped: this asserts the inconsistency exists, so fixing it upstream
/// fails here and this test is updated to assert the fix instead. ktsu-dev/Coder#63.
/// </para>
/// </remarks>
[TestMethod]
public void GoSpellsAGenericTypeItDidNotDeclare()
{
string source = Written["go"];

Assert.Contains("type Length struct", source, "the type is written without parameters");
Assert.Contains("Length[T]", source, "and the constructor spells it with one anyway");
}

/// <summary>
/// Python writes a base that names the class being declared, which Python evaluates before the
/// class exists.
/// </summary>
/// <remarks>
/// `IVector0&lt;Length&lt;T&gt;, T&gt;` is the self-type idiom every quantity here is declared
/// with, and it is what C# needs to give an interface a method returning the implementing type.
/// Python evaluates a base list eagerly, so `class Length(IVector0[Length[T], T])` raises
/// `NameError: name 'Length' is not defined` on import. A string in the base list is the fix
/// Python has for this, and it is not something the AST currently says. ktsu-dev/Coder#64.
/// </remarks>
[TestMethod]
public void PythonNamesTheClassInsideItsOwnBases()
{
Assert.Contains("class Length(IVector0[Length[T], T])", Written["python"]);
}

/// <summary>
/// Builds the neutral declaration for one magnitude quantity.
/// </summary>
/// <param name="type">The quantity to project.</param>
/// <returns>The declaration, with no target in mind.</returns>
/// <remarks>
/// Deliberately the whole of what a generated magnitude is and no more: the type, what it is
/// written over, what it claims to be, the value it stores and one way to build it. Anything a
/// single target needs and the others cannot use belongs to that target's generator, not here.
/// </remarks>
private static ClassDeclaration Project(QuantityType type)
{
ClassDeclaration declaration = new(type.Name)
{
Kind = TypeDeclarationKind.Struct,
IsRecord = true,
IsReadOnly = true,
IsPartial = true,
Visibility = Visibility.Public,
};

declaration.Documentation.Add(type.Description);
declaration.TypeParameters.Add(TypeParameter.Parse(StorageParameter));
declaration.Interfaces.Add(TypeReference.Parse($"IVector0<{type.Name}<T>, T>"));

PropertyDeclaration value = new("Value")
{
Type = TypeReference.Parse("T"),
Visibility = Visibility.Public,
HasGetter = true,
HasSetter = true,
SetterIsInitOnly = true,
};
value.Documentation.Add("Gets the value, in SI base units.");
declaration.Members.Add(value);

FunctionDeclaration factory = new("FromMeter")
{
ReturnType = TypeReference.Parse($"{type.Name}<T>"),
IsStatic = true,
Visibility = Visibility.Public,
};
factory.Documentation.Add("Builds one from a value in meters.");
factory.Parameters.Add(new Parameter("value", "T"));
factory.Body.Add(new ReturnStatement(
new CallExpression("Create") { Arguments = { new VariableReference("value") } }));
declaration.Members.Add(factory);

return declaration;
}

/// <summary>
/// Writes one declaration in every target ktsu.Coder has.
/// </summary>
/// <param name="declaration">What to write.</param>
/// <returns>The source, by language id.</returns>
private static Dictionary<string, string> Write(ClassDeclaration declaration)
{
ILanguageGenerator[] generators =
[
new CSharpGenerator(),
new CppGenerator(),
new CGenerator(),
new RustGenerator(),
new GoGenerator(),
new PythonGenerator(),
new JavaScriptGenerator(),
];

return generators.ToDictionary(
generator => generator.LanguageId,
generator => generator.Generate(declaration),
StringComparer.Ordinal);
}

/// <summary>
/// Reads the real metadata and picks the magnitude form of Length out of it.
/// </summary>
/// <returns>The quantity to project.</returns>
/// <remarks>
/// The real vocabulary rather than a fixture, for the reason the C++ tests use it: a probe run
/// against something invented answers a question nobody asked.
/// </remarks>
private static QuantityType MagnitudeOfLength()
{
QuantityMetadata metadata = QuantityMetadata.Parse(
File.ReadAllText(Path.Join(AppContext.BaseDirectory, "Metadata", "dimensions.json")));

QuantityVocabulary vocabulary = QuantityVocabulary.FromDimensions(metadata.ToDeclarations());

return vocabulary.Types.Single(quantity =>
string.Equals(quantity.Name, "Length", StringComparison.Ordinal));
}
}
Loading