Skip to content

Fix nested this capture for traits(getMember, this, ...) - #5205

Open
niy-ati wants to merge 1 commit into
ldc-developers:masterfrom
niy-ati:fix-5203-getmember-nested-this
Open

Fix nested this capture for traits(getMember, this, ...)#5205
niy-ati wants to merge 1 commit into
ldc-developers:masterfrom
niy-ati:fix-5203-getmember-nested-this

Conversation

@niy-ati

@niy-ati niy-ati commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Nested functions that only touch an instance field through __traits(getMember, this, "member") never registered a nested reference to the enclosing member this. LDC then omitted vthis from the closure frame, which showed up as a segfault when the nested function ran (#5203).

After semantic analysis of getMember, when the lookup object is this, call checkNestedReference on the member function's vthis so closure building matches a direct field access.

Test plan

Added tests/dmd/runnable/issue5203.d covering a nested function returned as a delegate that reads a field via getMember on this.

Fixes #5203.

@niy-ati
niy-ati force-pushed the fix-5203-getmember-nested-this branch 4 times, most recently from 21ecdf4 to 46d1820 Compare July 20, 2026 07:44
Comment thread dmd/traits.d Outdated
return ErrorExp.get();
}
}
// wantsym returns the member symbol; using `this` as the lookup object

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this in upstream DMD? if not, if should go there first.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DMD seems to work fine without this, so the fix should probably be somewhere in our glue layer instead. [Unless it's one of the few LDC-specific frontend mods that cause this.]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review , looked at upstream DMD.

Upstream: On current dlang/dmd master, compiler/src/dmd/traits.d has the same getMember path and does not call checkNestedReference when the first argument is this, so this isn’t LDC-specific. I didn’t find an existing dlang/dmd issue for it yet.

approach I’ll open an issue + PR on dlang/dmd with the semantic fix and a runnable test, then align this LDC PR with however you prefer we track upstream (cherry-pick / wait for DMD merge / close LDC PR until sync).

CI: The remaining failures are from tests/dmd/runnable/issue5203.d, not merge approval. The test used return get2 (returns int) instead of return &get2 (delegate). I’m fixing it to use an lvalue + auto dg = s.nestedThunk(); assert(dg() == 42) and will push once we agree on upstream-first vs keeping this PR open as a tracker.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so this isn’t LDC-specific

The testcase doesn't fail with DMD v2.112: https://godbolt.org/z/sKcPabbE8

So what I meant is that DMD doesn't need the proposed frontend patch, so there must be something else LDC-specific that causes it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kinke Thanks ,that’s the distinction I missed.

I checked Godbolt with DMD 2.112: the testcase runs without a traits.d change, so I agree this isn’t “upstream DMD is broken the same way.” The ldc#5203 report is LDC-specific at runtime,

Next steps :

  1. Re-run the original ldc#5203 repro (and your Godbolt source) with dmd vs ldc on the same frontend line , compile and run (segfault is runtime).
  2. Trace whether vthis / nested frame is missing in LDC glue (gen/nested, closure lowering) vs a frontend path that only LDC exercises.
  3. Align issue5203.d with the reporter’s pattern before any further pushes here.

I’ll report back on this thread with dmd/ldc comparison results before pushing more frontend changes.

@kinke

kinke commented Jul 20, 2026

Copy link
Copy Markdown
Member

This agent seems too eager, not controlled sufficiently by a human. And no LLM disclosure in the commit messages, not ideal either.

@niy-ati

niy-ati commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

here is what I found.

Godbolt’s example and ldc issue #5203 are not the same shape.
with return get2 and no address taken, the nested function is usually invoked while get is still running, The original report is about taking the address of the nested function and calling it after the outer function has returned.

On LDC I traced the crash to missing capture metadata. When the nested body only reaches this through __traits(getMember, this, …), nothing adds vthis to closureVars. The nested closure code in the backend only builds the frame from that list, so there is no valid slot to load and i got the runtime failure.

locally I added an LDC only block under version (IN_LLVM) in traits.d that calls checkNestedReference when the lookup object is this, in the same spirit as the existing workaround in getThisSkipNestedFuncs.
also fixed the runnable test issue5203.d to use the escaping delegate pattern.

I am rebuilding ldc2 on my machine to run that test, soif it passes I will update the branch.
@kinke ,if you would rather i spend more time looking for a fix in the glue layer before I push, lemme know that.
thanks.

@niy-ati
niy-ati requested a review from kinke July 20, 2026 13:45
@kinke

kinke commented Jul 20, 2026

Copy link
Copy Markdown
Member

The nested closure code in the backend only builds the frame from that list

Right, so what does DMD do instead, in order not to hit this problem? Maybe it's using a whole different scheme and doesn't create a frame with the closure-vars, but uses the whole stack frame of the function, or something like that. In that case (again, just a guess), we wouldn't want that for LDC, so would need a workaround indeed.

@niy-ati

niy-ati commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

both compilers still use the same frontend lists.
When a closure is required, DMD’s buildClosure() in glue/toir.d only copies what is in fd.closureVars, and it asserts that list is non-empty. LDC builds the LLVM frame the same way in gen/nested.cpp from fd->closureVars.

The difference shows up when something was never added to that list.
dmd can still load an outer local through getEthis() and stack offsets in e2ir.d when inClosure is false. LDC goes through DtoNestedVariable() and a field index that only exists if the var was in closureVars when the frame type was built. If not, nestedIndex stays at -1 and we get the null storage path.

here it matches what you saw on Godbolt
nested called inside get() while the outer frame is still there can work on DMD without touching traits. The ldc#5203 repro is different because return &inner and calling the delegate after nestedThunk() returns needs a real capture and getMember(this, …) with wantsym never runs checkNestedReference on the enclosing this, so vthis never gets into closureVars. LDC then has nothing to put in the context struct.

The fix we have locally is checkNestedReference on the lookup this in traits.d, behind IN_LLVM, same idea as the existing block in getThisSkipNestedFuncs in expressionsem.d.
rebuilt ldc2 + issue5203.d is still the check.

…his, ...)

__traits(getMember, this, field) with wantsym never keeps a ThisExp, so
checkNestedReference was skipped and LDC omitted the outer this from the
nested frame (gen/nested). Gate the capture under version (IN_LLVM), matching
getThisSkipNestedFuncs. Regression covers an escaping nested delegate.
@niy-ati
niy-ati force-pushed the fix-5203-getmember-nested-this branch from 6620c06 to e4ea1ee Compare July 22, 2026 14:53
@niy-ati

niy-ati commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

@kinke branch updated:
IN_LLVM checkNestedReference in traits.d (same idea as
getThisSkipNestedFuncs), and the runnable test now uses an escaping delegate
(return &inner). CI is green as well, pls review and lemme know if any changes are required.

@niy-ati
niy-ati requested a review from thewilsonator July 23, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nested this register is not populated if only access is via getMember.

3 participants