From eeb8009d5110c7eef71a7420a0f0c6896f2b62c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 02:19:00 +0000 Subject: [PATCH 1/2] Find out whether the quantities reach seven languages, not two MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Answer: five of seven, and the two that fail do so for reasons recorded upstream rather than anything here. ## The bump this needed first `ktsu.Coder` was pinned at 3.6.0, which predates every node the question depends on — `CallExpression`, `TypeParameter`, `TypeConstraint`, `PropertyDeclaration`, `Annotation`, `ClassDeclaration.Interfaces`, and the Go generator itself. 3.11.0 has all of them. The C++ projection needed no change across those five versions and its 27 tests, g++ and clang++ compiles included, pass unaltered. ## The probe `SevenTargetProjectionTests` builds one quantity — the magnitude form of `Length`, read from the real `dimensions.json` rather than a fixture — as a neutral AST, and writes it in all seven targets. The 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. That is the result worth having: the AST reaches the declaration this repository already generates, so what the other six do is a question about those languages rather than about the model. C++, Rust, C and JavaScript are each right for their own reasons — the two with type parameters carry the storage type, the two without write it down rather than inventing one. ## What it found Two targets write source their own toolchain refuses, and both were confirmed by running that toolchain rather than by reading the output: - Go: `go vet` says `undefined: T`. A generic type is deliberately written down rather than emitted, and the constructor was not given the same treatment, so it returns `Length[T]` from a `Length` that takes no parameters. ktsu-dev/Coder#63. - Python: `NameError: name 'Length' is not defined`. The self-type idiom every quantity is declared with cannot be a Python base, because a base list is evaluated eagerly. ktsu-dev/Coder#64. Both are pinned rather than skipped, so fixing either upstream fails the test here and it is updated to assert the fix. ## Where it lives `Semantics.Cpp.Test`, because that is where the reader of `dimensions.json` is. That is the finding about this repository rather than about Coder: `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. 33/33 C++ projection tests and 1139 Semantics tests pass. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QGCMUrT3jBgmANHNHcPmBf --- CLAUDE.md | 30 ++- Directory.Packages.props | 2 +- .../SevenTargetProjectionTests.cs | 255 ++++++++++++++++++ 3 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 Semantics.Cpp.Test/SevenTargetProjectionTests.cs diff --git a/CLAUDE.md b/CLAUDE.md index d1c18d5..381cfa5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`. | | `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) @@ -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: diff --git a/Directory.Packages.props b/Directory.Packages.props index 563856b..acf5a1d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,7 +16,7 @@ - + diff --git a/Semantics.Cpp.Test/SevenTargetProjectionTests.cs b/Semantics.Cpp.Test/SevenTargetProjectionTests.cs new file mode 100644 index 0000000..b5cee3d --- /dev/null +++ b/Semantics.Cpp.Test/SevenTargetProjectionTests.cs @@ -0,0 +1,255 @@ +// 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; + +/// +/// 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. +/// +/// +/// This is a probe rather than a generator. emits the whole +/// vocabulary and is C++ throughout — friend operators, Quantity<Dimension<…>>, +/// 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. +/// +/// It lives in this project because this is where the reader of dimensions.json is. That is +/// itself a finding: and 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. +/// +/// +/// Two targets are known to produce source their own toolchain refuses, 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. +/// +/// +[TestClass] +public sealed class SevenTargetProjectionTests +{ + /// + /// The storage type every generated quantity is written over. + /// + private const string StorageParameter = "T : struct, INumber"; + + /// + /// 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. + /// + private static ClassDeclaration Quantity { get; } = Project(MagnitudeOfLength()); + + private static Dictionary Written { get; } = Write(Quantity); + + /// + /// Every target writes something, which is the claim the whole exercise rests on. + /// + [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."); + } + } + + /// + /// C# comes out as exactly what this repository already generates by hand. + /// + /// + /// 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. + /// + [TestMethod] + public void CSharpMatchesWhatIsGeneratedToday() + { + string source = Written["csharp"]; + + Assert.Contains("public readonly partial record struct Length : IVector0, T>", source); + Assert.Contains("where T : struct, INumber", source); + Assert.Contains("public T Value { get; init; }", source); + Assert.Contains("public static Length FromMeter(T value)", source); + } + + /// + /// The targets that have type parameters carry the storage type onto the declaration. + /// + [TestMethod] + public void TheTargetsWithTypeParametersCarryTheStorageType() + { + Assert.Contains("template ", Written["cpp"]); + Assert.Contains("struct Length>", Written["rust"].Replace("pub ", string.Empty, StringComparison.Ordinal)); + } + + /// + /// The targets without type parameters write the storage type down rather than inventing one. + /// + /// + /// 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. + /// + [TestMethod] + public void TheTargetsWithoutTypeParametersWriteItDown() + { + Assert.Contains(StorageParameter, Written["c"]); + Assert.Contains(StorageParameter, Written["javascript"]); + } + + /// + /// Go writes source the Go toolchain refuses, because it drops the type parameter from the type + /// and then spells the type as generic anyway. + /// + /// + /// 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 Length[T] from a Length that takes no parameters, + /// and the field's type T is undefined. `go vet` says `undefined: T`. + /// + /// 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. + /// + /// + [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"); + } + + /// + /// Python writes a base that names the class being declared, which Python evaluates before the + /// class exists. + /// + /// + /// `IVector0<Length<T>, T>` 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 annotation or + /// 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. + /// + [TestMethod] + public void PythonNamesTheClassInsideItsOwnBases() + { + Assert.Contains("class Length(IVector0[Length[T], T])", Written["python"]); + } + + /// + /// Builds the neutral declaration for one magnitude quantity. + /// + /// The quantity to project. + /// The declaration, with no target in mind. + /// + /// 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. + /// + 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>")); + + 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}"), + 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; + } + + /// + /// Writes one declaration in every target ktsu.Coder has. + /// + /// What to write. + /// The source, by language id. + private static Dictionary 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); + } + + /// + /// Reads the real metadata and picks the magnitude form of Length out of it. + /// + /// The quantity to project. + /// + /// 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. + /// + 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)); + } +} From 4f06b81fb389300c93102751c08cc6c832b3ec04 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 02:36:37 +0000 Subject: [PATCH 2/2] Repair a doc comment my own edit garbled A find-and-replace left the tail of the sentence it replaced, so the Python test's remarks read "A string annotation or / A string in the base list is the fix". Nothing behavioural; the sentence now says one thing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QGCMUrT3jBgmANHNHcPmBf --- Semantics.Cpp.Test/SevenTargetProjectionTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Semantics.Cpp.Test/SevenTargetProjectionTests.cs b/Semantics.Cpp.Test/SevenTargetProjectionTests.cs index b5cee3d..09048a1 100644 --- a/Semantics.Cpp.Test/SevenTargetProjectionTests.cs +++ b/Semantics.Cpp.Test/SevenTargetProjectionTests.cs @@ -149,9 +149,8 @@ public void GoSpellsAGenericTypeItDidNotDeclare() /// `IVector0<Length<T>, T>` 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 annotation or - /// 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. + /// `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. /// [TestMethod] public void PythonNamesTheClassInsideItsOwnBases()