Skip to content
Draft
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 CDTk.slnx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<Solution>
<Project Path="CSharp.CDTk.Tests/CSharp.CDTk.Tests.csproj" />
<Project Path="CSharp.CDTk/CDTk.csproj" Id="244c952d-d069-4072-82f7-ae8916b32814" />
</Solution>
25 changes: 25 additions & 0 deletions CSharp.CDTk.Tests/CSharp.CDTk.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CSharp.CDTk\CDTk.csproj" />
</ItemGroup>

</Project>
85 changes: 85 additions & 0 deletions CSharp.CDTk.Tests/UnitTest1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using CDTk;
using System.Globalization;
using System.Linq;

namespace CSharp.CDTk.Tests;

public class TranslationDeterminismTests
{
[Fact]
public void CompileSource_ProducesExpectedGoldenOutput()
{
var compiler = new Compiler(new MiniPythonGrammar(), new MiniJavaGrammar());

var output = new string(compiler.CompileSource("print(x)").ToChars());

Assert.Equal("System.out.println(x);", output);
}

[Fact]
public void CompileSource_Replay_IsDeterministic()
{
var compiler = new Compiler(new MiniPythonGrammar(), new MiniJavaGrammar());

var first = new string(compiler.CompileSource("print(x)").ToChars());
var second = new string(compiler.CompileSource("print(x)").ToChars());
var third = new string(new Compiler(new MiniPythonGrammar(), new MiniJavaGrammar()).CompileSource("print(x)").ToChars());

Assert.Equal(first, second);
Assert.Equal(second, third);
}

[Fact]
public void TranslationPlanBuilder_Rebuild_IsDeterministic()
{
var source = Translation.AnalyzeGrammar(new MiniPythonGrammar());
var target = Translation.AnalyzeGrammar(new MiniJavaGrammar());
var builder = new TranslationPlanBuilder(source, target);

var first = builder.Build();
var second = builder.Build();

var firstMappings = first.SelectedMappings
.OrderBy(p => p.Key, StringComparer.Ordinal)
.Select(p => $"{p.Key}:{p.Value.TargetRuleName}:{RenderMap(p.Value.ChildPositionMap)}")
.ToArray();
var secondMappings = second.SelectedMappings
.OrderBy(p => p.Key, StringComparer.Ordinal)
.Select(p => $"{p.Key}:{p.Value.TargetRuleName}:{RenderMap(p.Value.ChildPositionMap)}")
.ToArray();

Assert.Equal(firstMappings, secondMappings);
}

[Fact]
public void CompileSource_WithUnsupportedMapping_FailsDeterministically()
{
var compiler = new Compiler(new SingleTokenSourceGrammar(), new IncompatibleTargetGrammar());

var ex = Assert.Throws<InvalidOperationException>(() => compiler.CompileSource("x").ToChars());

Assert.Contains("Validation status", ex.Message, StringComparison.Ordinal);
Assert.Contains("Policy=strict-99.99-v2", ex.Message, StringComparison.Ordinal);
}

private static string RenderMap(IReadOnlyDictionary<int, int> map)
=> string.Join(",", map.OrderBy(p => p.Key).Select(p => $"{p.Key.ToString(CultureInfo.InvariantCulture)}->{p.Value.ToString(CultureInfo.InvariantCulture)}"));

private sealed class SingleTokenSourceGrammar : Grammar
{
public static readonly Rule Program = new(new Token(ToUnits("x")));

private static string[] ToUnits(string text)
=> text.Select(ch => $"U+{((int)ch).ToString("X4", CultureInfo.InvariantCulture)}").ToArray();
}

private sealed class IncompatibleTargetGrammar : Grammar
{
private static readonly Rule A = new(new Token(ToUnits("a")));
private static readonly Rule B = new(new Token(ToUnits("b")));
public static readonly Rule Program = A + B;

private static string[] ToUnits(string text)
=> text.Select(ch => $"U+{((int)ch).ToString("X4", CultureInfo.InvariantCulture)}").ToArray();
}
}
79 changes: 68 additions & 11 deletions CSharp.CDTk/Analysis/Analysis.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;

namespace CDTk
{
Expand Down Expand Up @@ -149,10 +151,7 @@ internal CategoricalSemanticDiagram Parse(string input)

int inputLength = span.Length;

var full = results
.Where(r => r.End == inputLength)
.OrderByDescending(r => r.End - r.Start)
.FirstOrDefault();
var full = SelectDeterministicBestResult(results.Where(r => r.End == inputLength));

if (full == null)
{
Expand All @@ -169,7 +168,12 @@ internal CategoricalSemanticDiagram Parse(string input)
// Infer a start rule: a non-token rule that is never referenced as a child
private Rule InferStartRule()
{
var all = _grammar.GetAllRules().Select(r => r.ResolvedRule).Distinct().ToList();
var all = _grammar.GetAllRules()
.Select(r => r.ResolvedRule)
.Distinct()
.OrderBy(r => r.Name ?? string.Empty, StringComparer.Ordinal)
.ThenBy(r => r.Type.ToString(), StringComparer.Ordinal)
.ToList();
var allSet = new HashSet<Rule>(all);
var referenced = new HashSet<Rule>();

Expand Down Expand Up @@ -775,10 +779,9 @@ private CsdNode BuildCsdFromParseNode(ParseNode pn, CategoricalSemanticDiagram d
// Pick the child whose end position matches this node's end — that
// selects the longest (correct) parse when multiple completions have
// been accumulated on the same node due to GLL ambiguity.
var bestChild = pn.Children
.Where(c => c.End == pn.End)
.OrderByDescending(c => c.End - c.Start)
.FirstOrDefault() ?? pn.Children[0];
var bestChild = SelectDeterministicBestNode(pn.Children.Where(c => c.End == pn.End))
?? SelectDeterministicBestNode(pn.Children)
?? pn.Children[0];

var chosen = BuildCsdFromParseNode(bestChild, diagram);
var name = rule.Name ?? "AnonymousChoice";
Expand Down Expand Up @@ -872,8 +875,14 @@ private static IEnumerable<ParseNode> SelectSequenceChildren(ParseNode node)
// Fallback: deduplicate by start position (take longest per start).
return node.Children
.GroupBy(c => c.Start)
.Select(g => g.OrderByDescending(c => c.End - c.Start).First())
.OrderBy(c => c.Start);
.Select(g => g
.OrderByDescending(c => c.End - c.Start)
.ThenBy(c => c.End)
.ThenBy(c => GetDeterministicNodeKey(c), StringComparer.Ordinal)
.First())
.OrderBy(c => c.Start)
.ThenBy(c => c.End)
.ThenBy(c => GetDeterministicNodeKey(c), StringComparer.Ordinal);
}

private static string GetCategoryOfNode(CsdNode node)
Expand All @@ -887,6 +896,54 @@ private static string GetCategoryOfNode(CsdNode node)
_ => "Unknown"
};
}

private static ParseResult? SelectDeterministicBestResult(IEnumerable<ParseResult> results)
{
return results
.OrderByDescending(r => r.End - r.Start)
.ThenBy(r => r.Start)
.ThenBy(r => r.End)
.ThenBy(r => GetDeterministicNodeKey(r.Node), StringComparer.Ordinal)
.FirstOrDefault();
}

private static ParseNode? SelectDeterministicBestNode(IEnumerable<ParseNode> nodes)
{
return nodes
.OrderByDescending(n => n.End - n.Start)
.ThenBy(n => n.Start)
.ThenBy(n => n.End)
.ThenBy(n => GetDeterministicNodeKey(n), StringComparer.Ordinal)
.FirstOrDefault();
}

private static string GetDeterministicNodeKey(ParseNode node)
{
var builder = new StringBuilder();
AppendDeterministicNodeKey(node, builder);
return builder.ToString();
}

private static void AppendDeterministicNodeKey(ParseNode node, StringBuilder builder)
{
builder.Append(node.Rule.Name ?? string.Empty);
builder.Append('|');
builder.Append(node.Rule.Type.ToString());
builder.Append('|');
builder.Append(node.Start.ToString(CultureInfo.InvariantCulture));
builder.Append('|');
builder.Append(node.End.ToString(CultureInfo.InvariantCulture));
builder.Append('|');
builder.Append(node.Text ?? string.Empty);
builder.Append('|');
builder.Append(node.Children.Count.ToString(CultureInfo.InvariantCulture));
foreach (var child in node.Children.OrderBy(c => c.Start).ThenBy(c => c.End).ThenBy(c => c.Rule.Name ?? string.Empty, StringComparer.Ordinal))
{
builder.Append('{');
AppendDeterministicNodeKey(child, builder);
builder.Append('}');
}
}
}

public sealed class GrammarCsdBuilder
Expand Down
44 changes: 40 additions & 4 deletions CSharp.CDTk/CDTk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ private void AssignRuleNames()
BindingFlags.Static |
BindingFlags.FlattenHierarchy);

foreach (var field in fields)
foreach (var field in fields.OrderBy(f => f.Name, StringComparer.Ordinal))
{
if (field.FieldType == typeof(Rule))
{
Expand All @@ -713,7 +713,7 @@ internal IEnumerable<Rule> GetAllRules()
BindingFlags.Static |
BindingFlags.FlattenHierarchy);

foreach (var f in fields)
foreach (var f in fields.OrderBy(field => field.Name, StringComparer.Ordinal))
{
if (f.FieldType == typeof(Rule))
{
Expand Down Expand Up @@ -856,6 +856,8 @@ private CompilationSnapshot Execute()
.UseSemantics(semantics);

translation = translator.TranslateDetailed(sourceDiagram.Root);
var replayTranslation = translator.TranslateDetailed(sourceDiagram.Root);
var reproducibleTranslationOutput = AreExecutionOutputsEquivalent(translation, replayTranslation);
Exception? targetParseException = null;
var roundTripStable = true;
try
Expand Down Expand Up @@ -890,7 +892,17 @@ private CompilationSnapshot Execute()
roundTripStable = false;
}

assessment = TranslationReliabilityEngine.Assess(plan, translation, sourceDiagram, targetDiagram, targetParseException, semantics, policy, roundTripStable, reproduciblePlanSelection);
assessment = TranslationReliabilityEngine.Assess(
plan,
translation,
sourceDiagram,
targetDiagram,
targetParseException,
semantics,
policy,
roundTripStable,
reproduciblePlanSelection,
reproducibleTranslationOutput);
if (assessment.IsReliable)
break;

Expand All @@ -903,7 +915,7 @@ private CompilationSnapshot Execute()
var failure = assessment == null || assessment.Violations.Count == 0
? "Translation reliability gate failed."
: string.Join(" ", assessment.Violations);
throw new InvalidOperationException(failure);
throw new InvalidOperationException($"{failure} Policy={policy.PolicyVersion}.");
}

_cached = new CompilationSnapshot(translation.Text, targetDiagram);
Expand Down Expand Up @@ -933,6 +945,30 @@ private static bool ArePlanSelectionsEquivalent(StructuralTranslationPlan first,
return true;
}

private static bool AreExecutionOutputsEquivalent(TranslationExecutionResult first, TranslationExecutionResult second)
{
if (!string.Equals(first.Text, second.Text, StringComparison.Ordinal))
return false;
if (first.Diagnostics.Count != second.Diagnostics.Count)
return false;

for (var i = 0; i < first.Diagnostics.Count; i++)
{
var left = first.Diagnostics[i];
var right = second.Diagnostics[i];
if (left.Severity != right.Severity ||
!string.Equals(left.Code, right.Code, StringComparison.Ordinal) ||
!string.Equals(left.Message, right.Message, StringComparison.Ordinal) ||
!string.Equals(left.SourceRuleName, right.SourceRuleName, StringComparison.Ordinal) ||
!string.Equals(left.TargetRuleName, right.TargetRuleName, StringComparison.Ordinal))
{
return false;
}
}

return true;
}

private sealed class CompilationSnapshot
{
public CompilationSnapshot(string generatedText, CategoricalSemanticDiagram targetDiagram)
Expand Down
Loading