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
421 changes: 421 additions & 0 deletions ci-logs/rust-lint-75003226910.log

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions csharp/.changeset/fix-issue-20-substitution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'Foundation.Data.Doublets.Cli': patch
---

Fixed self-link substitution with outgoing links by preserving unbound substitution parts from the matched link and rejecting unsupported link addresses during explicit creation.
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,57 @@ public void StringAliasesInVariableRestriction_ShouldConstrainMatchesToNamedLink
});
}

[Fact]
public void Issue20_SubstituteMatchedLinkAndOutgoingLink_ShouldPreserveExistingParts()
{
RunTestWithLinks(links =>
{
ProcessQuery(links, "(() ((1: 1 1) (18: 1 21) (19: 1 20) (20: 20 20) (21: 21 21)))");

ProcessQuery(links, "((($i: 1 21)) (($i: $s $t) ($i 20)))");

var allLinks = GetAllLinks(links);
Assert.Equal(6, allLinks.Count);
AssertLinkExists(allLinks, 1, 1, 1);
AssertLinkExists(allLinks, 18, 1, 21);
AssertLinkExists(allLinks, 19, 1, 20);
AssertLinkExists(allLinks, 20, 20, 20);
AssertLinkExists(allLinks, 21, 21, 21);

var outgoingLink = Assert.Single(allLinks, link => link.Source == 18 && link.Target == 20);
Assert.NotEqual(links.Constants.Null, outgoingLink.Index);
Assert.NotEqual(links.Constants.Any, outgoingLink.Index);
Assert.DoesNotContain(allLinks, link => link.Index == links.Constants.Any || link.Source == links.Constants.Any || link.Target == links.Constants.Any);
});
}

[Fact]
public void Issue20_SubstituteFullPointWithUnboundParts_ShouldKeepFullPoint()
{
RunTestWithLinks(links =>
{
ProcessQuery(links, "(() ((21: 21 21)))");

ProcessQuery(links, "(((21: 21 21)) ((21: $s $t)))");

var allLinks = GetAllLinks(links);
Assert.Single(allLinks);
AssertLinkExists(allLinks, 21, 21, 21);
Assert.DoesNotContain(allLinks, link => link.Source == links.Constants.Any || link.Target == links.Constants.Any);
});
}

[Fact]
public void EnsureCreated_WithSpecialAnyReference_ShouldThrowControlledException()
{
RunTestWithLinks(links =>
{
var exception = Assert.Throws<InvalidOperationException>(() => LinksExtensions.EnsureCreated(links, links.Constants.Any));

Assert.Contains("unsupported link address", exception.Message);
});
}

// Helper methods
private static void RunTestWithLinks(Action<NamedTypesDecorator<uint>> testAction, bool enableTracing = false)
{
Expand Down
141 changes: 119 additions & 22 deletions csharp/Foundation.Data.Doublets.Cli/AdvancedMixedQueryProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,17 +209,8 @@ public static void ProcessQuery(INamedTypesLinks<uint> links, Options options)
TraceIfEnabled(options, "[ProcessQuery] Some solutions lead to actual changes => building operations.");
foreach (var solution in solutions)
{
var substitutionLinks = substitutionInternalPatterns
.Select(pattern => ApplySolutionToPattern(links, solution, pattern))
.Where(link => link != null)
.Select(link => new DoubletLink(link!))
.ToList();

var restrictionLinks = restrictionInternalPatterns
.Select(pattern => ApplySolutionToPattern(links, solution, pattern))
.Where(link => link != null)
.Select(link => new DoubletLink(link!))
.ToList();
var substitutionLinks = ApplySolutionToPatterns(links, solution, substitutionInternalPatterns, isSubstitution: true);
var restrictionLinks = ApplySolutionToPatterns(links, solution, restrictionInternalPatterns, isSubstitution: false);

TraceIfEnabled(options,
"[ProcessQuery] For a solution => " +
Expand Down Expand Up @@ -437,7 +428,7 @@ private static void ApplyAllPlannedOperations(
TraceIfEnabled(options, $"[ApplyAllPlannedOperations] Deleting link => ID={before.Index}, S={before.Source}, T={before.Target}");
RemoveLinks(links, before, options);
}
else if (before.Index == 0 && after.Index != 0)
else if (before.Index == 0 && (after.Index != 0 || after.Source != 0 || after.Target != 0))
{
TraceIfEnabled(options, $"[ApplyAllPlannedOperations] Creating link => ID={after.Index}, S={after.Source}, T={after.Target}");
CreateOrUpdateLink(links, after, options);
Expand Down Expand Up @@ -703,16 +694,12 @@ private static bool DetermineIfSolutionIsNoOperation(
INamedTypesLinks<uint> links)
{
var substitutedRestrictions = restrictions
.Select(r => ApplySolutionToPattern(links, solution, r))
.Select(r => ApplySolutionToPattern(links, solution, r, isSubstitution: false))
.Where(link => link != null)
.Select(link => new DoubletLink(link!))
.ToList();

var substitutedSubstitutions = substitutions
.Select(s => ApplySolutionToPattern(links, solution, s))
.Where(link => link != null)
.Select(link => new DoubletLink(link!))
.ToList();
var substitutedSubstitutions = ApplySolutionToPatterns(links, solution, substitutions, isSubstitution: true);

substitutedRestrictions.Sort((a, b) => a.Index.CompareTo(b.Index));
substitutedSubstitutions.Sort((a, b) => a.Index.CompareTo(b.Index));
Expand Down Expand Up @@ -752,9 +739,12 @@ private static List<DoubletLink> ExtractMatchedLinks(
private static DoubletLink? ApplySolutionToPattern(
INamedTypesLinks<uint> links,
Dictionary<string, uint> solution,
Pattern? pattern)
Pattern? pattern,
bool isSubstitution = false,
HashSet<uint>? visitedIndexes = null)
{
if (pattern == null) return null;
visitedIndexes ??= new HashSet<uint>();

// Retrieve the ANY constant once for both leaf and composite cases
var anyConstant = links.Constants.Any;
Expand All @@ -766,20 +756,127 @@ private static List<DoubletLink> ExtractMatchedLinks(
}
else
{
uint resolvedIndex = ResolveId(links, pattern.Index, solution);
var sourceLink = ApplySolutionToPattern(links, solution, pattern.Source);
var targetLink = ApplySolutionToPattern(links, solution, pattern.Target);
uint resolvedIndex = ResolvePatternIndex(links, pattern.Index, solution, isSubstitution);
var sourceLink = ApplySolutionToPattern(links, solution, pattern.Source, isSubstitution, visitedIndexes);
var targetLink = ApplySolutionToPattern(links, solution, pattern.Target, isSubstitution, visitedIndexes);

uint resolvedSource = sourceLink?.Index ?? anyConstant;
uint resolvedTarget = targetLink?.Index ?? anyConstant;

PreserveExistingSubstitutionParts(links, solution, pattern, resolvedIndex, ref resolvedSource, ref resolvedTarget, isSubstitution, visitedIndexes);

if (resolvedSource == 0) resolvedSource = anyConstant;
if (resolvedTarget == 0) resolvedTarget = anyConstant;

return new DoubletLink(resolvedIndex, resolvedSource, resolvedTarget);
}
}

private static uint ResolvePatternIndex(
INamedTypesLinks<uint> links,
string identifier,
Dictionary<string, uint> solution,
bool isSubstitution)
{
if (isSubstitution && string.IsNullOrEmpty(identifier))
{
return links.Constants.Null;
}

if (isSubstitution && IsVariable(identifier) && !solution.ContainsKey(identifier))
{
return links.Constants.Null;
}

return ResolveId(links, identifier, solution);
}

private static List<DoubletLink> ApplySolutionToPatterns(
INamedTypesLinks<uint> links,
Dictionary<string, uint> solution,
List<Pattern> patterns,
bool isSubstitution)
{
var workingSolution = isSubstitution ? new Dictionary<string, uint>(solution) : solution;
return patterns
.Select(pattern => ApplySolutionToPattern(links, workingSolution, pattern, isSubstitution))
.Where(link => link != null)
.Select(link => new DoubletLink(link!))
.ToList();
}

private static void PreserveExistingSubstitutionParts(
INamedTypesLinks<uint> links,
Dictionary<string, uint> solution,
Pattern pattern,
uint resolvedIndex,
ref uint resolvedSource,
ref uint resolvedTarget,
bool isSubstitution,
HashSet<uint> visitedIndexes)
{
if (!isSubstitution || resolvedIndex == links.Constants.Null || resolvedIndex == links.Constants.Any || !links.Exists(resolvedIndex))
{
return;
}

if (!visitedIndexes.Add(resolvedIndex))
{
return;
}

try
{
var existingLink = new DoubletLink(links.GetLink(resolvedIndex));

if (ShouldPreserveExistingPart(pattern.Source, solution) && CanPreserveExistingPart(existingLink, existingLink.Source, visitedIndexes))
{
resolvedSource = existingLink.Source;
AssignVariableIfNeeded(pattern.Source!.Index, resolvedSource, solution);
}
else if (TryResolveVariablePart(pattern.Source, solution, out var boundSource))
{
resolvedSource = boundSource;
}

if (ShouldPreserveExistingPart(pattern.Target, solution) && CanPreserveExistingPart(existingLink, existingLink.Target, visitedIndexes))
{
resolvedTarget = existingLink.Target;
AssignVariableIfNeeded(pattern.Target!.Index, resolvedTarget, solution);
}
else if (TryResolveVariablePart(pattern.Target, solution, out var boundTarget))
{
resolvedTarget = boundTarget;
}
}
finally
{
visitedIndexes.Remove(resolvedIndex);
}
}

private static bool ShouldPreserveExistingPart(Pattern? partPattern, Dictionary<string, uint> solution)
{
return partPattern?.IsLeaf == true
&& IsVariable(partPattern.Index)
&& !solution.ContainsKey(partPattern.Index);
}

private static bool TryResolveVariablePart(Pattern? partPattern, Dictionary<string, uint> solution, out uint value)
{
value = default;
return partPattern?.IsLeaf == true
&& IsVariable(partPattern.Index)
&& solution.TryGetValue(partPattern.Index, out value);
}

private static bool CanPreserveExistingPart(DoubletLink existingLink, uint part, HashSet<uint> visitedIndexes)
{
return existingLink.IsFullPoint()
|| existingLink.IsPartialPoint()
|| !visitedIndexes.Contains(part);
}

private static void CreateOrUpdateLink(INamedTypesLinks<uint> links, DoubletLink linkDefinition, Options options)
{
var nullConstant = links.Constants.Null;
Expand Down
42 changes: 35 additions & 7 deletions csharp/Foundation.Data.Doublets.Cli/LinksExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Numerics;
using Platform.Converters;
using Platform.Data;
using Platform.Data.Doublets;

Expand All @@ -11,29 +10,58 @@ public static class LinksExtensions

public static void EnsureCreated<TLinkAddress>(this ILinks<TLinkAddress> links, Func<TLinkAddress> creator, params TLinkAddress[] addresses) where TLinkAddress : IUnsignedNumber<TLinkAddress>
{
var addressToUInt64Converter = CheckedConverter<TLinkAddress, TLinkAddress>.Default;
var uInt64ToAddressConverter = CheckedConverter<TLinkAddress, TLinkAddress>.Default;
var nonExistentAddresses = new HashSet<TLinkAddress>(addresses.Where(x => !links.Exists(x)));
if (nonExistentAddresses?.Count > 0)
var nonExistentAddresses = new HashSet<TLinkAddress>();
foreach (var address in addresses)
{
EnsureSupportedInternalReference(links, address);
if (!links.Exists(address))
{
nonExistentAddresses.Add(address);
}
}

if (nonExistentAddresses.Count > 0)
{
var max = nonExistentAddresses.Max()!;
max = uInt64ToAddressConverter.Convert(TLinkAddress.CreateTruncating(Math.Min(ulong.CreateTruncating(max), ulong.CreateTruncating(links.Constants.InternalReferencesRange.Maximum))));
var createdLinks = new List<TLinkAddress>();
var seenCreatedLinks = new HashSet<TLinkAddress>();
TLinkAddress createdLink;

do
{
createdLink = creator();
EnsureSupportedInternalReference(links, createdLink);

if (!seenCreatedLinks.Add(createdLink))
{
throw new InvalidOperationException($"Link creation returned address {createdLink} more than once before reaching target {max}.");
}

if (Comparer<TLinkAddress>.Default.Compare(createdLink, max) > 0)
{
throw new InvalidOperationException($"Link creation produced address {createdLink} beyond requested target {max}.");
}

createdLinks.Add(createdLink);
}
while (createdLink != max);

for (var i = 0; i < createdLinks.Count; i++)
{
if (!nonExistentAddresses.Contains(createdLinks[i]))
if (!nonExistentAddresses.Contains(createdLinks[i]) && links.Exists(createdLinks[i]))
{
links.Delete(createdLinks[i]);
}
}
}
}

private static void EnsureSupportedInternalReference<TLinkAddress>(ILinks<TLinkAddress> links, TLinkAddress address) where TLinkAddress : IUnsignedNumber<TLinkAddress>
{
if (!links.Constants.IsInternalReference(address))
{
throw new InvalidOperationException($"Cannot ensure unsupported link address {address}. Only non-zero internal references in the supported range can be created.");
}
}
}
}
Loading
Loading