The LLVM Project Blog

LLVM Project News and Details from the Trenches

GSoC 2026: Extending Clang API Notes for C++: Overload-Specific Annotations for Functions and Methods

Hi! I’m Dominic Stöcker, and during Google Summer of Code 2026 I worked on extending Clang API Notes for C++ with overload-specific annotations for functions and methods.

My mentors were Gábor Horváth, John Hui, and Egor Zhdan.

This project was originally accepted as a small, 8-week project. As the implementation scope grew, it was expanded to a large, 12-week project.

I was interested in this project because it connects Clang’s semantic analysis with Swift interoperability. What made the project especially interesting to me was that a concise user-facing YAML extension had to be connected to Clang’s internal model of declarations, types, overloads, and source-level type spelling.

The goal of the project was to let API Notes target individual C++ function and method overloads. The work covered the public YAML design, parsing, binary serialization, declaration lookup, Sema integration, type normalization, diagnostics, and member-function object qualifiers.

Background

API Notes allow additional information to be attached to declarations without modifying their original source headers. They are used in particular to refine how C, Objective-C, and C++ APIs are exposed to Swift.

Previously, an API Notes entry could identify a C++ function or method primarily by name. This is not sufficient when several overloads share that name:

struct Widget {
  void setValue(int);
  void setValue(double);
};

A name-only entry for setValue applies to the whole overload set, so it cannot assign different annotations to setValue(int) and setValue(double).

The main challenge was therefore not only to select individual overloads, but to do so without changing the behavior of existing name-only API Notes files.

Selecting C++ Overloads

API Notes are structured according to the kind of declaration they apply to, like Functions, Tags, and Methods. The C++ overload selectors I designed for my project extend the Functions and Methods schemas with an optional Where block that narrows a name-based lookup to a specific C++ overload.

Tags:
  - Name: Widget
    Methods:
      - Name: setValue
        Where:
          Parameters:
            - int
        SwiftName: setIntValue(_:)

      - Name: setValue
        Where:
          Parameters:
            - double
        SwiftName: setDoubleValue(_:)

This API Notes file uses the Where.Parameters selector to apply separate SwiftName annotations to the int- and double-parameter overloads of setValue.

Conceptually, this is equivalent to applying separate Swift name annotations to the two overloads:

struct Widget {
  SWIFT_NAME(setIntValue(_:)) void setValue(int);
  SWIFT_NAME(setDoubleValue(_:)) void setValue(double);
};

The same selector is available for overloaded global functions:

Functions:
  - Name: makeWidget
    Where:
      Parameters:
        - int
    SwiftName: makeWidgetFromInt(_:)

  - Name: makeWidget
    Where:
      Parameters:
        - double
    SwiftName: makeWidgetFromDouble(_:)

Name-only entries preserve their current behavior:

Tags:
  - Name: Widget
    Methods:
      - Name: setValue
        Availability: nonswift

This entry still applies to every method named setValue in Widget. Omitting Where.Parameters leaves the parameter list unconstrained. Adding Where.Parameters narrows the entry to a specific explicit parameter list.

An explicitly empty parameter list has a different meaning:

Methods:
  - Name: build
    Where:
      Parameters: []
    SwiftName: buildWithoutArguments()

Where.Parameters: [] selects a declaration with no explicit source parameters. This differs from the name-only case above, which does not constrain parameters at all.

The parameter selector describes the complete explicit parameter list, so parameter count and order are significant.

Matching C++ Types

Matching parameter types requires more than comparing raw strings.

Consider a method whose parameter uses a type alias:

using Count = int;

struct Counter {
  void setCount(Count);
};

An API Notes author may write a selector using either Count or int. Both are useful, but they do not express exactly the same intent.

The implemented lookup first tries the written or sugared type from the declaration. If no matching entry exists, it can fall back to an appropriately desugared representation.

This gives an alias-specific selector precedence when both forms exist, while still allowing an underlying-type selector to work as a fallback.

The implementation also normalizes spelling differences that should not affect overload identity. This includes insignificant whitespace, spacing around punctuation such as pointers, references, template commas, and template closers, and narrow spelling cases such as unsigned versus unsigned int.

Selector-only nullability is stripped during selector formation because API Notes can add or replace nullability independently. This includes nested nullability in pointer, array, and function-pointer type components.

Top-level const and volatile also need special handling:

void process(int);
void process(const int);

These do not declare different C++ overloads. The selector representation therefore removes top-level qualifiers in cases where they do not contribute to overload identity, including the supported by-value and top-level pointer cases.

Because top-level const is stripped in this position, the second process declaration is treated as a redeclaration of the first rather than as a separate overload.

The goal is not to implement complete semantic type equivalence. Instead, the matching policy preserves useful source-level distinctions while normalizing differences that should not select separate overloads.

Matching the Implicit Object

Explicit parameters are not enough to distinguish every C++ method overload. Member functions can differ through qualifiers on their implicit object parameter. A common example is operator[], the subscript operator, with separate const and non-const overloads:

struct Buffer {
  Element &operator[](int);
  const Element &operator[](int) const;
};

Both overloads have the same name and explicit parameter list. Where.Parameters can identify the int parameter, but it cannot distinguish the non-const overload from the const overload by itself.

The selector model therefore includes an Object constraint:

Tags:
  - Name: Buffer
    Methods:
      - Name: operator[]
        Where:
          Parameters:
            - int
          Object:
            Const: false
        SwiftName: mutableElement(at:)

      - Name: operator[]
        Where:
          Parameters:
            - int
          Object:
            Const: true
        SwiftName: element(at:)

The Object selector describes const, volatile, and reference qualification on the implicit C++ object.

The same model can also distinguish lvalue- and rvalue-qualified methods:

struct Builder {
  Result build() &;
  Result build() &&;
  Result build() const &;
};

An API Notes file can combine the empty explicit-parameter selector with Object.Ref to select each ref-qualified overload:

Tags:
  - Name: Builder
    Methods:
      - Name: build
        Where:
          Parameters: []
          Object:
            Ref: lvalue
        SwiftName: buildFromLValue()

      - Name: build
        Where:
          Parameters: []
          Object:
            Ref: rvalue
        SwiftName: buildFromRValue()

      - Name: build
        Where:
          Parameters: []
          Object:
            Const: true
            Ref: lvalue
        SwiftName: buildFromConstLValue()

Here, Where.Parameters: [] identifies the empty explicit parameter list, while Where.Object.Ref distinguishes lvalue and rvalue receivers.

As with Where.Parameters, omitted properties remain unconstrained, while present properties narrow the candidate set.

Representing these qualifiers under Object follows the C++ language model more closely than treating the receiver as an ordinary parameter at a special position. These properties belong to the implicit object parameter, not to the method’s explicit parameter list.

Static methods and non-member functions do not have an implicit object parameter. Applying an Object constraint to such declarations is therefore invalid and can be diagnosed.

Implementation and Results

Supporting overload-aware API Notes required changes throughout the existing pipeline:

YAML
  -> API Notes model
  -> binary serialization
  -> declaration lookup
  -> overload-specific filtering
  -> Sema applies API Notes effects

The implementation was split into focused patches.

The first changes added YAML parsing and data-model support so that multiple entries with the same declaration name but different selectors could be represented.

The next part extended the binary API Notes format. Overload-specific entries must remain distinct when API Notes are compiled and later loaded again. The serialization also preserves the difference between an omitted parameter constraint and an explicitly empty parameter list.

The Sema integration preserves legacy name-only lookup while adding overload-specific matching: it first finds API Notes by declaration context and name, then uses the declaration’s explicit parameter types to select the overload-specific entry and apply its effects.

Additional work adds diagnostics for malformed, duplicate, and unmatched selectors. The normalization patch refines selector lookup for aliases, nullability, qualifiers, and supported spelling differences, while the object-qualifier patch adds Where.Object matching. A small diagnostics follow-up makes generic -Wapinotes warnings visible for system headers, which matters because API Notes are commonly attached to SDK headers. Tests cover the parser, serialization, Sema lookup, normalization behavior, aliases, default arguments, static methods, zero-parameter declarations, and object-qualified member functions.

The result is an overload-aware selector model that supports both global functions and C++ methods while preserving existing API Notes behavior.

Future Template Design

The overload selector model only applies to concrete methods and functions. As part of my project, I also explored how the overload selector model could be extended to support C++ templates in the future. Function template matching is not currently implemented, but this design work helped identify which parts of the selector model should remain extensible.

Ordinary Where.Parameters matching works for concrete function parameter types. Templates add several harder questions. One selector might need to identify a function template or one of its specializations, such as f<int>. Another selector might need to match an ordinary function overload whose parameter type contains a class template specialization or dependent template parameter.

For example, dependent types can refer to template parameters directly, through nested dependent names, or as part of a larger type:

template <typename T> void f(const T &);
template <typename Container> void f(const typename Container::value_type &);
template <typename Key, typename Value>
void f(const std::pair<Key, Value> &);

Relying only on source names such as T, Container, Key, or Value would be fragile because template parameter names can differ across redeclarations and library implementations. The design notes therefore explored structural template-parameter identity using depth and index, matching concrete template arguments for specializations such as f<int>, and a possible mapping from stable template-parameter identities to readable names so dependent types such as const T & can still be written clearly.

One possible direction was to assign stable identities to template parameters in API Notes, using depth and index, and then map those identities to readable local names used in dependent parameter spellings such as const T &. This would avoid relying on redeclaration-local source names while still keeping API Notes readable. The template design document contains illustrative future syntax for these ideas. That syntax is rejected by the current API Notes implementation.

Reflections

This project gave me practical experience working across several parts of Clang, from YAML parsing and API Notes serialization to declaration lookup, type representation, diagnostics, and Sema integration. One of the main lessons was that preserving existing API Notes behavior was as important as adding the new selector support. Name-only notes still need to match as they did before, while overload-specific notes need clear rules for matching and diagnostics. It also taught me how valuable small, focused upstream patches are: splitting the parser, serialization, Sema, diagnostics, normalization, and object-qualifier work made each part easier to review, test, and revise.

The project also made me more interested in continuing to work on Clang, especially in areas where C++ language support and Swift interoperability meet.

Acknowledgements

I would like to thank my mentors, Gábor Horváth, John Hui, and Egor Zhdan, for their thoughtful reviews, engaging design discussions, and support throughout Google Summer of Code.

Their feedback helped preserve compatibility with existing API Notes while extending the model to support overload matching, type normalization, and C++ object qualifiers.