Summary
ILinksExtensions.MergeUsages corrupts every link it re-points. Both substitutions it builds land in the wrong slots, so a merged usage ends up with a null target and a source taken from the wrong half of the doublet.
Reproduction
Runnable, dependency-pinned reproduction (Platform.Data.Doublets 0.18.1, no decorators, so nothing but MergeUsages can touch the links):
https://github.com/link-foundation/link-cli/tree/issue-100-f2e0ccb162ad/docs/case-studies/issue-100/evidence/csharp-merge-usages
using var links = new UnitedMemoryLinks<uint>(databaseFilename);
var one = links.CreateAndUpdate(links.Constants.Null, links.Constants.Null); // merged away
var two = links.CreateAndUpdate(links.Constants.Null, links.Constants.Null); // survives
var three = links.CreateAndUpdate(links.Constants.Null, links.Constants.Null); // unrelated
var usageAsSource = links.CreateAndUpdate(one, three); // (4: 1 3)
var usageAsTarget = links.CreateAndUpdate(three, one); // (5: 3 1)
links.MergeUsages(one, two);
Actual output:
before: (1: 0 0) (2: 0 0) (3: 0 0) (4: 1 3) (5: 3 1)
after: (1: 0 0) (2: 0 0) (3: 0 0) (4: 3 0) (5: 2 0)
BUG usage as source: expected (4: 2 3), got (4: 3 0)
BUG usage as target: expected (5: 3 2), got (5: 2 0)
Re-pointing a usage must replace only the half that named one: (4: 1 3) should become (4: 2 3) and (5: 3 1) should become (5: 3 2). Instead both links lose their target and get a source copied from the wrong place.
Root cause
ILinksExtensions.cs#L1214 and #L1226:
var substitution = new Link<TLinkAddress>(newLinkIndex, links.GetTarget(usageAsSource));
...
var substitution = new Link<TLinkAddress>(links.GetTarget(usageAsTarget), newLinkIndex);
Both calls bind to public Link(params TLinkAddress[] values) (Link.cs#L63) — there is no (source, target) constructor, only (index, source, target) and the params catch-all. SetValues reads a two-element list as (index, source) with a default target (Link.cs#L171-175):
case 2:
index = values[0];
source = values[1];
target = default;
break;
So new Link<uint>(2, 3) is (index: 2, source: 3, target: 0), not (source: 2, target: 3). That explains (4: 3 0) exactly: source 3 is the intended target, target is default.
The second loop has a second, independent bug: for a usage-as-target the half that must survive is the source, but the code reads links.GetTarget(usageAsTarget) — the very address being merged away. Even with the constructor fixed it would write (5: 1 2) instead of (5: 3 2).
Suggested fix
var index = links.GetIndex(usageAsSource);
var substitution = new Link<TLinkAddress>(index, newLinkIndex, links.GetTarget(usageAsSource));
...
var index = links.GetIndex(usageAsTarget);
var substitution = new Link<TLinkAddress>(index, links.GetSource(usageAsTarget), newLinkIndex);
It is worth grepping for other two-argument new Link<...>(a, b) call sites — the params overload makes this mistake silent at compile time.
Notes
Summary
ILinksExtensions.MergeUsagescorrupts every link it re-points. Both substitutions it builds land in the wrong slots, so a merged usage ends up with a null target and a source taken from the wrong half of the doublet.Reproduction
Runnable, dependency-pinned reproduction (Platform.Data.Doublets 0.18.1, no decorators, so nothing but
MergeUsagescan touch the links):https://github.com/link-foundation/link-cli/tree/issue-100-f2e0ccb162ad/docs/case-studies/issue-100/evidence/csharp-merge-usages
Actual output:
Re-pointing a usage must replace only the half that named
one:(4: 1 3)should become(4: 2 3)and(5: 3 1)should become(5: 3 2). Instead both links lose their target and get a source copied from the wrong place.Root cause
ILinksExtensions.cs#L1214and#L1226:Both calls bind to
public Link(params TLinkAddress[] values)(Link.cs#L63) — there is no(source, target)constructor, only(index, source, target)and theparamscatch-all.SetValuesreads a two-element list as(index, source)with a default target (Link.cs#L171-175):So
new Link<uint>(2, 3)is(index: 2, source: 3, target: 0), not(source: 2, target: 3). That explains(4: 3 0)exactly: source3is the intended target, target isdefault.The second loop has a second, independent bug: for a usage-as-target the half that must survive is the source, but the code reads
links.GetTarget(usageAsTarget)— the very address being merged away. Even with the constructor fixed it would write(5: 1 2)instead of(5: 3 2).Suggested fix
It is worth grepping for other two-argument
new Link<...>(a, b)call sites — theparamsoverload makes this mistake silent at compile time.Notes
MergeAndDeletecallsMergeUsages, so it inherits the defect.doublets0.5.0src/decorators/mod.rsnotes "MergeUsagesre-points sources and targets correctly; the C# version writes null targets because of aparamsconstructor mix-up."