Skip to content

Modification through a call result that is a field of the receiver (≺) does not reach the receiver #80

Closed

Description

@bnaudts

A getter that walks a parent chain before returning a field:

static class Wrapper { int x; }
static class Node { Wrapper wrapper; int type; Node parent;
    Wrapper getWrapper() { Node node = this; while (node.type == 1) { node = node.parent; } return node.wrapper; } }

static void viaLoop(Node parent) { Wrapper w = parent.getWrapper(); w.x = 1; }   // parent: unmodified — wrong
static void viaField(Node parent) { Wrapper w = parent.wrapper; w.x = 1; }       // parent: modified — right
  • viaField: MOD[0:parent, parent.wrapper], parent modified.
  • viaLoop: [-] --> - MOD[], parent unmodified. This is the unsafe direction.

On ws/maddi-null, getWrapper's summary is empty ([] --> -): the loop's reassignment of node drops the link. With #72 4a (ws/maddi-null-72) the summary is right: getWrapper≺this.parent, getWrapper≺this. The caller still gets nothing from it, though. w has no link to 0:parent after the call, so w.x = 1 does not reach parent.

Expected: w ≺ 0:parent (or 0:parent.parent) in the caller, and parent modified.

Seen in fernflower. ClassesProcessor.ClassNode.getWrapper() has exactly this shape. Callers like NestedClassProcessor.setLambdaVars(parent, child) modify parent.getWrapper().getMethods().getWithKey(..) and keep parent unmodified, on both sides.

On ws/maddi-null, the chain also leaks the callee's this: method ∈ this.methods.§es, where this is ClassWrapper's, untranslated. MODREACH then marked these static methods modifying. #72 4a removes the leak, and that accounts for six of its fernflower nonModifying false→true flips.

Found while checking #72 4a's fernflower A/B.

Activity

  1. bnaudts commented on Oct 28, 2026

    @bnaudts
    Author

    A likely place: WriteLinksAndModification.isInvalidFieldContainment. w ≺ 0:parent has a real local on the from side that is not structurally part of parent, so the filter drops it. Its NOTE records a group-aware relaxation that was tried and reverted (TestCast / TestInstanceOf, the "≺-family cluster"). A fix has to tell "the call result is a field of the receiver" apart from an accessor copy, probably at the call site, as the pattern-binding mark does for deconstruction. A related precedent is the comment on expression-scoped fields just below, parent.wrapperNode().map ≺ $rv ← parent.

    Not started; I haven't verified this location yet.

  2. bnaudts commented on Oct 29, 2026

    @bnaudts
    Author

    Fixed on ws/maddi-null-80 (d526ad63, 14478b2b; gates NOCALLRESULTFIELD, NOGETTERRECEIVER): a call result ≺ its receiver keeps the containment through reduceLinks, and an inlined getter on a call result keeps the receiver's evaluation.

    Open: a modified element of a collection reached from a call result, parent.getWrapper().getMethods().getWithKey(key).varproc.getThisVars().put(..) (fernflower NestedClassProcessor.setLambdaVars). method ∈ $rv.methods.list.§$s composed with $rv ≺ parent gives only method ≤ 0:parent.parent, and notLinkedToModified never carries a modification back over ≤. Adding ≥ as deep-only (tried, NOOBJECTGRAPHMOD) fixes it but makes read-only iteration deep-modifying (TestModificationThroughView) and loses a structural modification (TestKotlinAnalyzerControl.clearAll).

    Idea: name the unknown field. getWrapper ≺ this says "a field of this, unknown which"; as a virtual field getWrapper ← this.§f the call site gets $rv ← 0:parent.§f, and everything downstream is an ordinary path rooted at parent (method ∈ 0:parent.§f.methods.list.§$s), which the existing ∋ completion carries back. The read-only iterator stays apart (list.iterator() gets ≤ from the library summary, never ≺). Prerequisite: #86 (the call-site path translation drops a level). Check first how the virtual-field matching treats §f (vf/virtual-fields.md): it must not make w ← parent.§f match every write parent.x = ...

  3. bnaudts commented on Nov 3, 2026

    @bnaudts
    Author

    Rebased onto the #72 stack + #89 fix; now on ws/maddi-null as 6400774 and 3601648 (gates NOCALLRESULTFIELD, NOGETTERRECEIVER).

    Link suite 530/18 skipped/0 failed, analyzer 679/2/0.

    Fernflower (PARALLEL=1), A = both gates set, B = the fix:

    A B
    time 43 min 46 min
    cycle-protection trips 275 300
    work-ceiling retries (#92) 117 137

    Modification verdicts that change, all toward modifying (the safe direction):

    • DomHelper.detectStatement(Statement): nonModifying true → false
    • TryHelper.addTryWithResourceJ11(..): nonModifying true → false (it calls DecompilerContext.getCounterContainer().getCounterAndIncrement(..), a modification through a call result)
    • field DecompilerContext.classProcessor: unmodified true → false

    Still open, as before: NestedClassProcessor.setLambdaVars (the ≤ composition through a collection element; idea: name the unknown field as §f, after #86).

    The fix costs 25 more trips and 20 more retries on fernflower; those add to #92.

  4. bnaudts commented on Nov 5, 2026

    @bnaudts
    Author

    The main case is merged into devel (6400774, 3601648; gates NOCALLRESULTFIELD, NOGETTERRECEIVER). The remaining case, setLambdaVars, moves to 96. The trips and retries the fix adds belong to #92.

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