From ee068399addf416f349d686f4264e6ec0b4b018a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 12:48:40 +0000 Subject: [PATCH] feat(generator): SEM003 diagnostic + form-specific relationships (closes #58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each entry under integrals / derivatives / dotProducts / crossProducts in dimensions.json may now declare an explicit "forms" array (e.g. { "other": "Length", "result": "Torque", "forms": [3] }). When set, the generator only emits operators at those forms; when a listed form is missing on a participating dimension, it surfaces as the new SEM003 Roslyn diagnostic instead of being silently skipped. When "forms" is omitted, the generator falls back to per-relationship defaults — [0..4] for integrals/derivatives, [1..4] for dotProducts, [3] for crossProducts — preserving every emit decision currently in place. The Generated/ tree is unchanged. Migrated the one current crossProduct (Force × Length → Torque) to "forms": [3] explicitly to demonstrate the new field. Verified that swapping to "forms": [2, 3]" produces: error SEM003: Relationship in dimension 'Force' (crossProducts[Length -> Torque]) explicitly requests form V2, but 'Torque' does not declare that form. Doc updates: - CLAUDE.md generator-diagnostics list adds SEM003 with an example. - docs/strategy-unified-vector-quantities.md describes the optional "forms" field on each relationship. - AnalyzerReleases.Unshipped.md adds the SEM003 row. --- CLAUDE.md | 1 + .../AnalyzerReleases.Unshipped.md | 1 + .../Generators/QuantitiesGenerator.cs | 120 +++++++++++++++++- .../Metadata/dimensions.json | 2 +- .../Models/DimensionsMetadata.cs | 9 ++ docs/strategy-unified-vector-quantities.md | 2 + 6 files changed, 130 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 255bfe3..1634540 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -149,6 +149,7 @@ var converted = sourceString.As(); - Generator diagnostics: - **SEM001** — a relationship in `dimensions.json` references a dimension that does not exist (typo or rename). The operator is silently dropped. - **SEM002** — schema-level validation issue (missing `name`/`symbol`, empty `availableUnits`, duplicate type names, no vector forms declared). + - **SEM003** — a relationship's explicit `forms` list references a vector form not declared on a participating dimension. Use `forms` to constrain a relationship to specific vector forms (e.g. `crossProducts: [{ "other": "Length", "result": "Torque", "forms": [3] }]`); when omitted, the legacy "emit at every common form" behaviour is preserved. - See `docs/physics-generator.md` for the full schema and an end-to-end "add a dimension" walk-through. This file is the entry point. For deeper material: diff --git a/Semantics.SourceGenerators/AnalyzerReleases.Unshipped.md b/Semantics.SourceGenerators/AnalyzerReleases.Unshipped.md index 0513be6..dd3509b 100644 --- a/Semantics.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/Semantics.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -7,3 +7,4 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------ SEM001 | Semantics.SourceGenerators | Warning | Reports relationships in dimensions.json that reference unknown dimension names. SEM002 | Semantics.SourceGenerators | Warning | Reports schema-level validation issues in dimensions.json (missing fields, duplicate type names, etc). +SEM003 | Semantics.SourceGenerators | Warning | Reports a relationship whose explicit `forms` list references a vector form not declared on a participating dimension. diff --git a/Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs b/Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs index 47d9f3a..cd6b916 100644 --- a/Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs +++ b/Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs @@ -37,6 +37,14 @@ public class QuantitiesGenerator : GeneratorBase defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); + private static readonly DiagnosticDescriptor RelationshipFormMissing = new( + id: "SEM003", + title: "Relationship requires a vector form not declared on a participating dimension", + messageFormat: "Relationship in dimension '{0}' ({1}) explicitly requests form V{2}, but '{3}' does not declare that form. The operator will not be generated.", + category: "Semantics.SourceGenerators", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + public QuantitiesGenerator() : base("dimensions.json") { } /// @@ -244,7 +252,17 @@ private static List CollectAllOperators(SourceProductionContext co continue; } - int[] forms = [0, 1, 2, 3, 4]; + // For integrals the "Other" multiplier is V0 only; the form propagates + // between Self and Result, so SEM003 should fire if either Self or + // Result is missing a declared form. (V0-only Other was already + // rejected above via the v0Other null check.) + int[] forms = ResolveForms( + context, + integral, + [0, 1, 2, 3, 4], + dim, + resultDim, + $"integrals[{integral.Other} -> {integral.Result}]"); foreach (int vn in forms) { string? selfType = GetBaseTypeName(dim, vn); @@ -289,7 +307,13 @@ private static List CollectAllOperators(SourceProductionContext co continue; } - int[] forms = [0, 1, 2, 3, 4]; + int[] forms = ResolveForms( + context, + derivative, + [0, 1, 2, 3, 4], + dim, + resultDim, + $"derivatives[{derivative.Other} -> {derivative.Result}]"); foreach (int vn in forms) { string? selfType = GetBaseTypeName(dim, vn); @@ -340,8 +364,14 @@ private static List CollectAllProducts(SourceProductionContext cont continue; } - // Dot product for V1+ forms where both self and other have that form - int[] forms = [1, 2, 3, 4]; + // Dot product is undefined for V0; default forms are V1+. + int[] forms = ResolveForms( + context, + dot, + [1, 2, 3, 4], + dim, + otherDim, + $"dotProducts[{dot.Other} -> {dot.Result}]"); foreach (int vn in forms) { string? selfType = GetBaseTypeName(dim, vn); @@ -374,6 +404,23 @@ private static List CollectAllProducts(SourceProductionContext cont continue; } + // Cross product is intrinsically 3D. Default to V3 only; explicit Forms + // other than [3] are accepted but the operator emit below only handles V3. + // Pass resultDim so SEM003 surfaces when the declared form is missing on + // the result type too (e.g. Force × Length → Torque at V2: Torque has no V2). + int[] forms = ResolveForms( + context, + cross, + [3], + dim, + otherDim, + $"crossProducts[{cross.Other} -> {cross.Result}]", + resultDim); + if (Array.IndexOf(forms, 3) < 0) + { + continue; + } + string? selfV3 = GetBaseTypeName(dim, 3); string? otherV3 = GetBaseTypeName(otherDim, 3); string? resultV3 = GetBaseTypeName(resultDim, 3); @@ -418,6 +465,71 @@ private static void ReportUnknownReference(SourceProductionContext context, stri fieldPath)); } + /// + /// Resolves the forms at which a relationship should emit operators. When the metadata + /// declares explicitly, that list wins and + /// any form missing from one of the participating dimensions is reported as + /// SEM003. When the list is empty, returns + /// (which the caller filters silently — preserving the legacy behaviour for relationships + /// that haven't opted into form-specific declarations). + /// + private static int[] ResolveForms( + SourceProductionContext context, + RelationshipDefinition rel, + int[] defaultForms, + PhysicalDimension dim, + PhysicalDimension otherDim, + string fieldPath, + PhysicalDimension? resultDim = null) + { + if (rel.Forms.Count == 0) + { + return defaultForms; + } + + List kept = []; + foreach (int form in rel.Forms) + { + if (form < 0 || form > 4) + { + continue; + } + + if (GetBaseTypeName(dim, form) == null) + { + ReportFormMissing(context, dim.Name, fieldPath, form, dim.Name); + continue; + } + + if (GetBaseTypeName(otherDim, form) == null) + { + ReportFormMissing(context, dim.Name, fieldPath, form, otherDim.Name); + continue; + } + + if (resultDim != null && GetBaseTypeName(resultDim, form) == null) + { + ReportFormMissing(context, dim.Name, fieldPath, form, resultDim.Name); + continue; + } + + kept.Add(form); + } + + return [.. kept]; + } + + private static void ReportFormMissing(SourceProductionContext context, string owningDimension, string fieldPath, int form, string offendingDimension) + { + context.ReportDiagnostic(Diagnostic.Create( + RelationshipFormMissing, + Location.None, + owningDimension, + fieldPath, + form, + offendingDimension)); + } + private static Dictionary BuildUnitMap(UnitsMetadata units) { Dictionary map = []; diff --git a/Semantics.SourceGenerators/Metadata/dimensions.json b/Semantics.SourceGenerators/Metadata/dimensions.json index 4793129..c1a6bdb 100644 --- a/Semantics.SourceGenerators/Metadata/dimensions.json +++ b/Semantics.SourceGenerators/Metadata/dimensions.json @@ -563,7 +563,7 @@ { "other": "Length", "result": "Energy" } ], "crossProducts": [ - { "other": "Length", "result": "Torque" } + { "other": "Length", "result": "Torque", "forms": [3] } ] }, { diff --git a/Semantics.SourceGenerators/Models/DimensionsMetadata.cs b/Semantics.SourceGenerators/Models/DimensionsMetadata.cs index e0f9acd..6102f42 100644 --- a/Semantics.SourceGenerators/Models/DimensionsMetadata.cs +++ b/Semantics.SourceGenerators/Models/DimensionsMetadata.cs @@ -179,4 +179,13 @@ public class RelationshipDefinition { public string Other { get; set; } = string.Empty; public string Result { get; set; } = string.Empty; + + /// + /// Optional explicit list of vector forms (0..4) at which this relationship should + /// emit operators. When empty, the generator uses sensible defaults from the + /// relationship kind: integrals/derivatives default to all common forms, + /// dotProducts to V1+, crossProducts to V3 only. When set, missing forms + /// on either side surface as SEM003 diagnostics instead of being silently dropped. + /// + public List Forms { get; set; } = []; } diff --git a/docs/strategy-unified-vector-quantities.md b/docs/strategy-unified-vector-quantities.md index 55232cd..0ee05d5 100644 --- a/docs/strategy-unified-vector-quantities.md +++ b/docs/strategy-unified-vector-quantities.md @@ -223,6 +223,8 @@ The current `integrals` and `derivatives` lists are supplemented with `dotProduc - **`dotProducts`** (`Self · Other = Result`): Generates `.Dot()` methods on VN types (N >= 1) where both self and other have that VN form. Result is always V0 of the result dimension. - **`crossProducts`** (`Self × Other = Result`): Generates `.Cross()` methods only on V3 types where both self and other have V3 forms. Result is V3 of the result dimension. +Each entry may also declare an explicit `forms` array (e.g. `{ "other": "Length", "result": "Torque", "forms": [3] }`). When set, the generator only emits operators at those forms; if a listed form is missing on any participating dimension, it surfaces as the `SEM003` diagnostic instead of being silently dropped. When `forms` is omitted, the generator falls back to per-relationship defaults: `[0, 1, 2, 3, 4]` for integrals/derivatives, `[1, 2, 3, 4]` for dot products, `[3]` for cross products. + ### Complete Example: Velocity Dimension Given: