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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ var converted = sourceString.As<SourceType, TargetType>();
- 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:
Expand Down
1 change: 1 addition & 0 deletions Semantics.SourceGenerators/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
120 changes: 116 additions & 4 deletions Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ public class QuantitiesGenerator : GeneratorBase<DimensionsMetadata>
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") { }

/// <summary>
Expand Down Expand Up @@ -244,7 +252,17 @@ private static List<OperatorInfo> 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);
Expand Down Expand Up @@ -289,7 +307,13 @@ private static List<OperatorInfo> 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);
Expand Down Expand Up @@ -340,8 +364,14 @@ private static List<ProductInfo> 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);
Expand Down Expand Up @@ -374,6 +404,23 @@ private static List<ProductInfo> 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);
Expand Down Expand Up @@ -418,6 +465,71 @@ private static void ReportUnknownReference(SourceProductionContext context, stri
fieldPath));
}

/// <summary>
/// Resolves the forms at which a relationship should emit operators. When the metadata
/// declares <see cref="RelationshipDefinition.Forms"/> explicitly, that list wins and
/// any form missing from one of the participating dimensions is reported as
/// <c>SEM003</c>. When the list is empty, returns <paramref name="defaultForms"/>
/// (which the caller filters silently — preserving the legacy behaviour for relationships
/// that haven't opted into form-specific declarations).
/// </summary>
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<int> 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<string, UnitDefinition> BuildUnitMap(UnitsMetadata units)
{
Dictionary<string, UnitDefinition> map = [];
Expand Down
2 changes: 1 addition & 1 deletion Semantics.SourceGenerators/Metadata/dimensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@
{ "other": "Length", "result": "Energy" }
],
"crossProducts": [
{ "other": "Length", "result": "Torque" }
{ "other": "Length", "result": "Torque", "forms": [3] }
]
},
{
Expand Down
9 changes: 9 additions & 0 deletions Semantics.SourceGenerators/Models/DimensionsMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,13 @@ public class RelationshipDefinition
{
public string Other { get; set; } = string.Empty;
public string Result { get; set; } = string.Empty;

/// <summary>
/// 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: <c>integrals</c>/<c>derivatives</c> default to all common forms,
/// <c>dotProducts</c> to V1+, <c>crossProducts</c> to V3 only. When set, missing forms
/// on either side surface as <c>SEM003</c> diagnostics instead of being silently dropped.
/// </summary>
public List<int> Forms { get; set; } = [];
}
2 changes: 2 additions & 0 deletions docs/strategy-unified-vector-quantities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading