Skip to content

Fast path for __traits(getMember, ...) in is() type expressions#22542

Closed
CyberShadow wants to merge 1 commit intodlang:masterfrom
CyberShadow:fix-getMember-type-context-circular-dep
Closed

Fast path for __traits(getMember, ...) in is() type expressions#22542
CyberShadow wants to merge 1 commit intodlang:masterfrom
CyberShadow:fix-getMember-type-context-circular-dep

Conversation

@CyberShadow
Copy link
Member

Generated by Claude Opus 4.6, usual disclaimers apply.


Problem

D's compile-time introspection allows probing member return types via is(__traits(getMember, T, "name") R == return). This is useful in ORM frameworks, serialization libraries, and other metaprogramming patterns where a type's methods iterate allMembers to discover which members return a specific type:

struct Column { string name; }

struct Table(T) {
    @Column("id") T id;
    @Column("data") T data;

    static bool hasIntReturning() {
        static foreach (name; __traits(allMembers, Table)) {{
            static if (is(__traits(getMember, Table, name) R == return)) {
                static if (is(R == int))
                    return true;
            }
        }}
        return false;
    }

    auto save() {
        enum h = hasIntReturning();
        return h ? 1 : 0;
    }
}

static assert(!Table!int.hasIntReturning());  // Error

This fails with two separate issues:

  1. Circular dependency: is(__traits(getMember, Table, "save") ...) triggers expressionSemanticfunctionSemanticfunctionSemantic3 (body compilation) on save() to infer its return type. But save()'s body references hasIntReturning(), which is already being compiled — circular dependency.

  2. Silently broken: Even without the circular dependency, is(__traits(getMember, S, "func") R == return) always evaluates to false on current master. getMember resolves to a Dsymbol (FuncDeclaration), not a Type, so the IsExp handler's trySemantic cannot convert it to a TypeFunction — the return type query silently fails.

Analysis

The chain that produces the circular dependency error is:

  1. static assert evaluates hasIntReturning() via CTFE, which starts compiling its body (semantic3)
  2. The body iterates allMembers, which includes "save". For that member, __traits(getMember, Table, "save") inside the is() expression calls TypeTraits.resolvesemanticTraits(getMember)expressionSemantic
  3. This triggers functionSemantic on save()
  4. save() has an auto return type, so functionSemantic eagerly calls functionSemantic3 (body compilation) to infer the return type
  5. functionSemantic3 ungags errors even inside trySemantic's gagged context, so the circular dependency error propagates
  6. save()'s body contains enum h = hasIntReturning(), which tries to CTFE hasIntReturning()circular dependency

The separate "always false" issue occurs because TypeTraits.resolve for a function member returns a Dsymbol (the FuncDeclaration), not a Type. The IsExp handler expects a Type from trySemantic, gets null, and evaluates the condition as false.

Observation

is(__traits(getMember, T, "name") R == return) only needs the member's type — specifically, its TypeFunction. For function declarations, fd.type is populated after dsymbolSemantic (pass 1), which resolves explicit return types and parameter types without compiling the function body. For auto return methods where .next (return type) is still null, the is(... == return) condition can simply evaluate to false — the return type genuinely isn't known yet, and that's the correct answer without needing to force body compilation.

Similarly, for variable members, vd.type is available after dsymbolSemantic and is sufficient for type comparisons.

Proposal

Add a fast path in the IsExp handler (expressionsem.d) that detects when e.targ is a TypeTraits wrapping __traits(getMember, T, "name") and resolves the member's type via lightweight sym.search() instead of full semanticTraitsexpressionSemantic. This:

  • Fixes is(__traits(getMember, T, "func") R == return) by returning the TypeFunction directly
  • Prevents circular dependency errors by avoiding functionSemantic/functionSemantic3
  • Falls back to the existing trySemantic path for any case the fast path can't handle

The fast path is scoped to IsExp only — it does not affect alias declarations or other type contexts where TypeTraits.resolve needs to return the full Dsymbol.

Implementation

A package helper getMemberTypeFastPath is added to compiler/src/dmd/typesem.d. It is called from the IsExp handler in compiler/src/dmd/expressionsem.d, before the existing trySemantic call:

  1. Pattern match: Check that e.targ is a TypeTraits whose inner expression is __traits(getMember, ...) with exactly 2 arguments
  2. Cheap argument resolution: TemplateInstance_semanticTiargs on the getMember arguments — resolves type and string without body compilation
  3. Extract member name: CTFE-interpret the second argument to get the string, convert to Identifier
  4. Get aggregate symbol: getDsymbol() on the first argument
  5. Symbol table lookup: sym.search(loc, id) — pure symbol table lookup, no semantic analysis
  6. Return the type: fd.type for FuncDeclaration, vd.type for VarDeclaration

The helper returns null at any step that fails, falling back to the existing full-semantic path. This ensures no behavioral regressions — if the fast path can't handle a case, the old code takes over transparently.

Trade-offs

auto return types resolve to false, not inferred: For methods with auto return types where the return type hasn't been inferred yet (.next is null), is(__traits(getMember, T, "func") R == return) evaluates to false rather than triggering body compilation to infer the type. This is the correct conservative answer — the return type genuinely isn't available without body compilation, and forcing it is what causes the circular dependency. Code that needs the inferred return type can use the two-step form:

// Fast path (no body compilation — auto returns resolve to false):
is(__traits(getMember, T, "name") R == return)

// Full semantic path (triggers body compilation for inference):
is(typeof(__traits(getMember, T, "name")) R == return)

Scoped to IsExp only: The fast path is deliberately not placed in TypeTraits.resolve because that would change behavior for alias declarations — alias member = __traits(getMember, S, "x") would alias the member's type instead of the member symbol. By limiting the fast path to IsExp, only is(...) expressions are affected, which is the context where a Type (not Dsymbol) is the correct result.

Test

compiler/test/compilable/test_getMember_type_fwdref.d covers:

  1. Basic: is(__traits(getMember, S, "func") R == return) resolves explicit return types (broken on master — always false)
  2. Basic: is(__traits(getMember, S, "x") == int) resolves field types
  3. Template struct: allMembers + is(getMember == return) loop with auto save() method — circular dependency on master
  4. Mixin template: Same pattern with mixin template
  5. Compatibility: typeof(__traits(getMember, ...)) still works as before

Add lightweight resolution for `is(__traits(getMember, T, "name") ...)`
that uses `sym.search()` to resolve the member type without triggering
expressionSemantic/functionSemantic. This prevents circular dependency
errors when iterating allMembers on types with auto-return methods, and
also fixes `is(__traits(getMember, T, "func") R == return)` which
previously always returned false because getMember resolved to a Dsymbol
rather than a Type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dlang-bot
Copy link
Contributor

Thanks for your pull request, @CyberShadow!

Bugzilla references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

Testing this PR locally

If you don't have a local development environment setup, you can use Digger to test this PR:

dub run digger -- build "master + dmd#22542"

@CyberShadow CyberShadow marked this pull request as draft February 9, 2026 12:55
@CyberShadow
Copy link
Member Author

(I assume the same objection as in #22541 (review) applies.)

Copy link
Contributor

@dkorpel dkorpel left a comment

Choose a reason for hiding this comment

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

Yes, it does

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

Labels

AI Generated Code that is generated by an LLM AI.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants