Skip to content

MergeUsages writes null targets and wrong sources: the two-argument Link<T> constructor is the params overload #515

Description

@konard

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions