Skip to content

Commit f41624b

Browse files
Merge pull request #73 from ktsu-dev/claude/issue-58-sem003
feat(generator): SEM003 diagnostic + form-specific relationships (closes #58)
2 parents 21d6ad8 + ee06839 commit f41624b

6 files changed

Lines changed: 130 additions & 5 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ var converted = sourceString.As<SourceType, TargetType>();
149149
- Generator diagnostics:
150150
- **SEM001** — a relationship in `dimensions.json` references a dimension that does not exist (typo or rename). The operator is silently dropped.
151151
- **SEM002** — schema-level validation issue (missing `name`/`symbol`, empty `availableUnits`, duplicate type names, no vector forms declared).
152+
- **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.
152153
- See `docs/physics-generator.md` for the full schema and an end-to-end "add a dimension" walk-through.
153154

154155
This file is the entry point. For deeper material:

Semantics.SourceGenerators/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Rule ID | Category | Severity | Notes
77
--------|----------|----------|------
88
SEM001 | Semantics.SourceGenerators | Warning | Reports relationships in dimensions.json that reference unknown dimension names.
99
SEM002 | Semantics.SourceGenerators | Warning | Reports schema-level validation issues in dimensions.json (missing fields, duplicate type names, etc).
10+
SEM003 | Semantics.SourceGenerators | Warning | Reports a relationship whose explicit `forms` list references a vector form not declared on a participating dimension.

Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ public class QuantitiesGenerator : GeneratorBase<DimensionsMetadata>
3737
defaultSeverity: DiagnosticSeverity.Warning,
3838
isEnabledByDefault: true);
3939

40+
private static readonly DiagnosticDescriptor RelationshipFormMissing = new(
41+
id: "SEM003",
42+
title: "Relationship requires a vector form not declared on a participating dimension",
43+
messageFormat: "Relationship in dimension '{0}' ({1}) explicitly requests form V{2}, but '{3}' does not declare that form. The operator will not be generated.",
44+
category: "Semantics.SourceGenerators",
45+
defaultSeverity: DiagnosticSeverity.Warning,
46+
isEnabledByDefault: true);
47+
4048
public QuantitiesGenerator() : base("dimensions.json") { }
4149

4250
/// <summary>
@@ -244,7 +252,17 @@ private static List<OperatorInfo> CollectAllOperators(SourceProductionContext co
244252
continue;
245253
}
246254

247-
int[] forms = [0, 1, 2, 3, 4];
255+
// For integrals the "Other" multiplier is V0 only; the form propagates
256+
// between Self and Result, so SEM003 should fire if either Self or
257+
// Result is missing a declared form. (V0-only Other was already
258+
// rejected above via the v0Other null check.)
259+
int[] forms = ResolveForms(
260+
context,
261+
integral,
262+
[0, 1, 2, 3, 4],
263+
dim,
264+
resultDim,
265+
$"integrals[{integral.Other} -> {integral.Result}]");
248266
foreach (int vn in forms)
249267
{
250268
string? selfType = GetBaseTypeName(dim, vn);
@@ -289,7 +307,13 @@ private static List<OperatorInfo> CollectAllOperators(SourceProductionContext co
289307
continue;
290308
}
291309

292-
int[] forms = [0, 1, 2, 3, 4];
310+
int[] forms = ResolveForms(
311+
context,
312+
derivative,
313+
[0, 1, 2, 3, 4],
314+
dim,
315+
resultDim,
316+
$"derivatives[{derivative.Other} -> {derivative.Result}]");
293317
foreach (int vn in forms)
294318
{
295319
string? selfType = GetBaseTypeName(dim, vn);
@@ -340,8 +364,14 @@ private static List<ProductInfo> CollectAllProducts(SourceProductionContext cont
340364
continue;
341365
}
342366

343-
// Dot product for V1+ forms where both self and other have that form
344-
int[] forms = [1, 2, 3, 4];
367+
// Dot product is undefined for V0; default forms are V1+.
368+
int[] forms = ResolveForms(
369+
context,
370+
dot,
371+
[1, 2, 3, 4],
372+
dim,
373+
otherDim,
374+
$"dotProducts[{dot.Other} -> {dot.Result}]");
345375
foreach (int vn in forms)
346376
{
347377
string? selfType = GetBaseTypeName(dim, vn);
@@ -374,6 +404,23 @@ private static List<ProductInfo> CollectAllProducts(SourceProductionContext cont
374404
continue;
375405
}
376406

407+
// Cross product is intrinsically 3D. Default to V3 only; explicit Forms
408+
// other than [3] are accepted but the operator emit below only handles V3.
409+
// Pass resultDim so SEM003 surfaces when the declared form is missing on
410+
// the result type too (e.g. Force × Length → Torque at V2: Torque has no V2).
411+
int[] forms = ResolveForms(
412+
context,
413+
cross,
414+
[3],
415+
dim,
416+
otherDim,
417+
$"crossProducts[{cross.Other} -> {cross.Result}]",
418+
resultDim);
419+
if (Array.IndexOf(forms, 3) < 0)
420+
{
421+
continue;
422+
}
423+
377424
string? selfV3 = GetBaseTypeName(dim, 3);
378425
string? otherV3 = GetBaseTypeName(otherDim, 3);
379426
string? resultV3 = GetBaseTypeName(resultDim, 3);
@@ -418,6 +465,71 @@ private static void ReportUnknownReference(SourceProductionContext context, stri
418465
fieldPath));
419466
}
420467

468+
/// <summary>
469+
/// Resolves the forms at which a relationship should emit operators. When the metadata
470+
/// declares <see cref="RelationshipDefinition.Forms"/> explicitly, that list wins and
471+
/// any form missing from one of the participating dimensions is reported as
472+
/// <c>SEM003</c>. When the list is empty, returns <paramref name="defaultForms"/>
473+
/// (which the caller filters silently — preserving the legacy behaviour for relationships
474+
/// that haven't opted into form-specific declarations).
475+
/// </summary>
476+
private static int[] ResolveForms(
477+
SourceProductionContext context,
478+
RelationshipDefinition rel,
479+
int[] defaultForms,
480+
PhysicalDimension dim,
481+
PhysicalDimension otherDim,
482+
string fieldPath,
483+
PhysicalDimension? resultDim = null)
484+
{
485+
if (rel.Forms.Count == 0)
486+
{
487+
return defaultForms;
488+
}
489+
490+
List<int> kept = [];
491+
foreach (int form in rel.Forms)
492+
{
493+
if (form < 0 || form > 4)
494+
{
495+
continue;
496+
}
497+
498+
if (GetBaseTypeName(dim, form) == null)
499+
{
500+
ReportFormMissing(context, dim.Name, fieldPath, form, dim.Name);
501+
continue;
502+
}
503+
504+
if (GetBaseTypeName(otherDim, form) == null)
505+
{
506+
ReportFormMissing(context, dim.Name, fieldPath, form, otherDim.Name);
507+
continue;
508+
}
509+
510+
if (resultDim != null && GetBaseTypeName(resultDim, form) == null)
511+
{
512+
ReportFormMissing(context, dim.Name, fieldPath, form, resultDim.Name);
513+
continue;
514+
}
515+
516+
kept.Add(form);
517+
}
518+
519+
return [.. kept];
520+
}
521+
522+
private static void ReportFormMissing(SourceProductionContext context, string owningDimension, string fieldPath, int form, string offendingDimension)
523+
{
524+
context.ReportDiagnostic(Diagnostic.Create(
525+
RelationshipFormMissing,
526+
Location.None,
527+
owningDimension,
528+
fieldPath,
529+
form,
530+
offendingDimension));
531+
}
532+
421533
private static Dictionary<string, UnitDefinition> BuildUnitMap(UnitsMetadata units)
422534
{
423535
Dictionary<string, UnitDefinition> map = [];

Semantics.SourceGenerators/Metadata/dimensions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@
563563
{ "other": "Length", "result": "Energy" }
564564
],
565565
"crossProducts": [
566-
{ "other": "Length", "result": "Torque" }
566+
{ "other": "Length", "result": "Torque", "forms": [3] }
567567
]
568568
},
569569
{

Semantics.SourceGenerators/Models/DimensionsMetadata.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,13 @@ public class RelationshipDefinition
179179
{
180180
public string Other { get; set; } = string.Empty;
181181
public string Result { get; set; } = string.Empty;
182+
183+
/// <summary>
184+
/// Optional explicit list of vector forms (0..4) at which this relationship should
185+
/// emit operators. When empty, the generator uses sensible defaults from the
186+
/// relationship kind: <c>integrals</c>/<c>derivatives</c> default to all common forms,
187+
/// <c>dotProducts</c> to V1+, <c>crossProducts</c> to V3 only. When set, missing forms
188+
/// on either side surface as <c>SEM003</c> diagnostics instead of being silently dropped.
189+
/// </summary>
190+
public List<int> Forms { get; set; } = [];
182191
}

docs/strategy-unified-vector-quantities.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ The current `integrals` and `derivatives` lists are supplemented with `dotProduc
223223
- **`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.
224224
- **`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.
225225

226+
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.
227+
226228
### Complete Example: Velocity Dimension
227229

228230
Given:

0 commit comments

Comments
 (0)