[Pitch] Compiler-Level Support for Variadic Wrappers

Introduction

Swift macros provide developers with powerful tools for code generation and transformation. However, handling variadic parameters in Swift often requires manual wrapping, which can lead to verbose and error-prone code. This pitch proposes a new compiler-level macro, @VariadicWrapper, to simplify and automate the wrapping of variadic parameters using custom property wrappers.

The proposed macro enhances code ergonomics by reducing boilerplate and improving clarity while maintaining Swift's strong type system.

Motivation

Variadic parameters are widely used in Swift for functions that accept a variable number of arguments. However, developers often need to manually wrap these parameters in custom types or property wrappers for additional functionality (e.g., validation, transformation, or encapsulation). This process is repetitive and prone to errors.

Example of Current Manual Wrapping:

swift

func testFunction(values: Int...) {
    let wrappedValues = Wrapper(wrappedValue: values)
    print(wrappedValues)
}

This approach introduces boilerplate code that must be repeated across multiple functions. Instead, we propose automating this process at the compiler level using the @VariadicWrappermacro.

Proposed Solution:

swift

@VariadicWrapper
func testFunction(values: Int...) {
    print(values)
}

// Compiler-generated code:
func testFunction_macro(values: Int...) {
    testFunction(values: Wrapper(wrappedValue: [values]))
}

This transformation improves code readability and reduces developer effort while ensuring type safety.

The @VariadicWrapper macro will be implemented as a built-in attached peer macro that transforms function declarations with variadic parameters. The transformation will generate a companion function that wraps variadic parameters in a custom property wrapper (Wrapper) automatically.

Key Features:

  1. Automatic Wrapping: Variadic parameters are wrapped in a Wrapper type without manual intervention.
  2. Type Safety: The macro ensures that the wrapped types conform to Swift's type system.
  3. Improved Ergonomics: Reduces repetitive boilerplate code and improves readability.

Detailed Design

Transformation Logic

The @VariadicWrapper macro will:

  1. Detect function declarations annotated with @VariadicWrapper.
  2. Identify variadic parameters in the function signature.
  3. Generate a companion function that wraps variadic parameters using the specified property wrapper (Wrapper).

Example Transformation:

Input:

swift

@VariadicWrapper
func testFunction(values: Int...) {
    print(values)
}

Generated Code:

swift

func testFunction_macro(values: Int...) {
    testFunction(values: Wrapper(wrappedValue: [values]))
}

Type Checking

The compiler will ensure that:

  1. The Wrapper type is valid for the parameter's base type.
  2. Transformed functions are correctly type-checked during semantic analysis.

Example Use Cases

Case 1: Logging Wrapped Values

swift

@VariadicWrapper
func logValues(values: Int...) {
    print("Wrapped values:", values)
}

Generated Code:

swift

func logValues_macro(values: Int...) {
    logValues(values: Wrapper(wrappedValue: [values]))
}

Case 2: Validation of Variadic Parameters

swift

@propertyWrapper struct ValidatedArray<T> {
    var wrappedValue: [T]
    init(wrappedValue: [T]) {
        self.wrappedValue = wrappedValue.filter { $0 > 0 }
    }
}

@VariadicWrapper
func validateValues(values: Int...) {
    print("Validated values:", values)
}

Generated Code:

swift

func validateValues_macro(values: Int...) {
    validateValues(values: ValidatedArray(wrappedValue: [values]))
}

Source Compatibility

This feature is additive and does not break existing Swift code. Functions without the @VariadicWrapper annotation will remain unaffected.

Alternatives Considered

  1. Manual Wrapping
    Developers could continue manually wrapping variadic parameters, but this approach increases boilerplate code and reduces clarity.
  2. Library-Level Solutions
    Implementing similar functionality at the library level would not leverage the compiler's capabilities, leading to less efficient code generation.

Implementation Plan

  1. Extend Macro System
    Modify Swift's macro processor to support @VariadicWrapper transformations.
  2. Modify Syntactic Transformations
    Extend SwiftSyntax to detect and rewrite function declarations with variadic parameters.
  3. Integrate with Semantic Analysis
    Ensure type checking applies correctly to transformed functions.
  4. Test Cases
    Add tests to validate the behavior of @VariadicWrapper under various scenarios.

Conclusion

The proposed @VariadicWrapper macro simplifies handling of variadic parameters by automating their wrapping at the compiler level. This feature improves code ergonomics, reduces boilerplate, and adheres to Swift's strong type system principles.

feedback is welcome from the community on this pitch!

1 Like

Hi, Arihant. Your proposal has a lot of examples, but none of them seem to show exactly what you're thinking this would do. Let's start with this example:

@VariadicWrapper
func testFunction(values: Int...) {
    print(values)
}

func callTestFunction() {
  testFunction(values: 0, 1, 2)
}

What are you expecting as the result after macro transformation?

Also, in your examples, the macro always seems to introduce uses of a specific wrapper type, but it's not at all apparent how the macro decides which type to use.

Do parameter wrappers not work with variadic parameters? Maybe this is all just a straightforward change to add that.

4 Likes

Also, in your examples, the macro always seems to introduce uses of a specific wrapper type, but it's not at all apparent how the macro decides which type to use.

Sorry for the confusing and incomplete references to my example
Here is a revised approach to my example :

Revised Approach

@VariadicWrapper(using: ValidatedArray<Int>.self)
func validateValues(values: Int...) {
    print("Validated values:", values.wrappedValue) 
}

This aligns with Swift's pattern in @OptionSet macros where generated types are explicitly specified.

Do parameter wrappers not work with variadic parameters? Maybe this is all just a straightforward change to add that.

No , currently, parameter wrappers don’t work with variadic parameters in Swift, but it does seem like a straightforward change to extend support for them.

As for the test cases :

The macro needs to either:

  1. Automatically unwrap the wrapper:
  • Improved Readability: Developers can write cleaner code without repetitive calls to access the wrapped values.
  1. Modify the function body to use values.wrappedValue :
    ensuring that any operations performed within the function are applied to the actual values rather than the wrapper instance.

here is the revised proposed implementation of the test :

// Before expansion
@VariadicWrapper(using: Wrapper<Int>.self)
func testFunction(values: Int...) {
    print(values.wrappedValue)
}

// After expansion
func testFunction(values: [Int]) {
    let values = Wrapper(wrappedValue: values)
    print(values.wrappedValue)
}

Why Macros Instead of Enhanced Parameter Wrappers?

  1. Variadic Transformation: Existing @propertyWrapper operates on individual parameters, not collected variadics.
  2. Type Erasure: Macros can handle type preservation where Array<T> would erase tuple type information.
  3. Compile-Time Safety: Macros enable validation before wrapper initialization.

Okay, so that is just an internal parameter wrapper working on the received variadic arguments, which (as usual) are represented as an Array in the callee. I don’t know why you keep saying this has any sort of static safety advantages.

1 Like

Thank you for your feedback! I appreciate your point about variadic arguments being represented as an array in the callee, and I understand the concern about whether this proposal truly offers static safety advantages.

I’d like to emphasize that the value of this proposal lies in automating repetitive wrapping logic and enforcing consistent transformations across functions. While it may not introduce entirely new static safety guarantees compared to manually wrapping an array, it does reduce developer error by centralizing these transformations into a macro.

As i have stated before , parameter wrappers don’t work with variadic parameters in Swift we ought to decide whether or not variadic parameters should be supported with external property wrappers and either diagnose the declaration or rather find some other way to make it compile.

As I want to work on [GSoC 2025] Re-implement property wrappers with macros this solution is just me trying to explore the codebase and seeing if i can optimize this behaviour of parameter wrapper working on the received variadic arguments by defining macros in the bigger scope.

If the pitch seems like a useless addition and doesnt hold any ground I would be happy to explore other solutions on the basis of your feedback.

Sure, but the transformation you're applying is exactly the parameter wrapper transformation, and you'd get exactly the same automation and consistency (add an attribute, have a consistent transformation applied) from just using a parameter wrapper. I recommend reading SE-0293 if you haven't. Tagging @hborla, who co-wrote that proposal.

It seems that we already allow parameter wrappers to be applied to variadic parameters, both implementation-detail and API-level, so I don't think anything needs to change here unless there's something I'm missing.

Generalizing parameter wrappers using macros is a commendable idea, and it's certainly a fair question to ask how far we should take generalizing existing macro-like language features rather than allowing programs to get the same effect with macros. Since this is apparently covered by the existing feature, though, there isn't much to say.

4 Likes

I appriciate the feedback and would definately read the proposal SE-0293

I was keen to know if you have a proposal to what changes you think would be appropriate.

If there are specific design questions or challenges regarding implementing parameter wrappers with variadic parameters, I would love to collaborate on finding solutions. Your insights would be invaluable as we explore this further.

hi @John_McCall it's been some time since i was able to further discuss this topic. I came up with a plan where we do the following to tackle the problem :

The solution modifies the type checker to properly handle property wrappers when applied to variadic parameters. When a property wrapper is applied to a variadic parameter:

  1. The wrapper is applied to the array type that represents the variadic parameter

  2. The compiler ensures proper type checking and validation: we check if it a vardic parameter or not .

If it is not a vardic parameter we go the default way {the way it currently works} but if so then we declare custom behaviour in the typecheker.cpp file like this.

bool TypeChecker::validateParameterPropertyWrapper(ParamDecl *param) {
  auto *attr = param->getAttachedPropertyWrapper();
  if (!attr)
    return false;

  // Allow property wrappers on variadic parameters
  if (param->isVariadic()) {
    Type wrappedType = applyPropertyWrapperToVariadicParam(param,
                                                          param->getType(),
                                                          attr);
    if (!wrappedType)
      return true;

    param->setType(wrappedType);
    return false;
  }

  // Existing parameter property wrapper validation...
  return false;
}
  1. The wrapped value maintains the variadic parameter's semantics
/// Add support for applying property wrappers to variadic parameters
Type TypeChecker::applyPropertyWrapperToVariadicParam(ParamDecl *param,
                                                    Type paramType,
                                                    CustomAttr *attr) {
  // Early exit if this isn't a variadic parameter
  if (!param->isVariadic())
    return paramType;

  // Get the array type that represents the variadic parameter
  auto arrayType = paramType->getAs<BoundGenericType>();
  if (!arrayType)
    return paramType;

  // Get the element type of the array
  Type elementType = arrayType->getGenericArgs()[0];

  // Create the property wrapper type for the element
  Type wrappedElementType = applyPropertyWrapperToType(elementType, attr);
  if (!wrappedElementType)
    return paramType;

  // Create a new array type with the wrapped element type
  return BoundGenericType::get(arrayType->getDecl(),
                              arrayType->getParent(),
                              { wrappedElementType });
}

I have made a PR draft to discuss the proposed changes, as this is my first PR i would expect nothing but tons of mistakes and I hope you guys bare with me:

Sorry, could you clarify what's wrong about how parameter wrappers currently apply to variadic parameters?

as we can see over in this thread :

Swift's type system doesn't know how to convert from Int to Int... when there's a property wrapper involved. Property wrappers in Swift are primarily designed for properties, not for function parameters, and especially not for variadic parameters.

When a variadic parameter is passed, Swift gathers the arguments into an array at the call site. A parameter wrapper would need special handling to either wrap the entire array or each individual element.

A parameter wrapper is designed to wrap a single value, whereas a variadic parameter represents a sequence of values. The wrapper would need to handle an array of values rather than a single value, which the current design doesn't support.

Okay. And you have a use case that requires an "API-level" parameter wrapper for a variadic parameter? Because none of the examples you've given so far actually do.

1 Like

Can you please mention the parameter use case you are referring to

For example, this code works today:

@propertyWrapper
struct ValidatedArray<T: Comparable & ExpressibleByIntegerLiteral> {
  var wrappedValue: [T]
  init(wrappedValue: [T]) {
      self.wrappedValue = wrappedValue.filter { $0 > 0 }
  }
}

func foo(@ValidatedArray values: Int...) {
  print(values)
}

foo(values: 1, -1, 2) // prints [1, 2]
3 Likes

Yes your example works @John_McCall but I feel like we need to generalize this beyond a specific example.

the ValidatedArray property wrapper is designed to handle [T] , aligns perfectly with how variadic parameters (Int... ) are represented under the hood. However, this is a narrow use case and does not generalize to other property wrappers, like Wrapper , that are not designed for arrays.

Variadic parameters cannot support property wrappers that are not inherently compatible with array types. The proposed changes resolve these issues by enhancing the compiler's type-checking logic, enabling consistent and reliable behavior for all property wrappers applied to variadic parameters.

for example this fails:

@propertyWrapper
struct Wrapper<Value> {
    var wrappedValue: Value
    init(wrappedValue: Value) {
        self.wrappedValue = wrappedValue
    }
    var projectedValue: Self { self }
}
func foo(@Wrapper x: Int...) {}

foo($x: Wrapper(wrappedValue: 0))

The compiler does not currently support using projected values ($x ) with variadic parameters, as there is no mechanism to resolve the projection for an array type.

The proposed changes will :

  1. Adapt Property Wrappers for Variadic Parameter
  2. Support Projected Values for Variadic Parameters
  3. this will reduce the amount of edge cases and inconsistencies by generalizing and make wrappers work uniformly across both single and vardic parameters.

I feel like your example's don't have any substance especially to add a new macro. Sure maybe variadic parameters need to work better with wrapped values but non of your examples really show that. And the linked issue doesn't really do anything. You may want to look at this for insight:

oh I think you must have mistaken my earlier approach of re implementing the property wrapper using macros in the discussion. It was a proposed idea that was already not viable. The new approach just simply Adds support for property wrappers on variadic parameters by modifying the handling in the type checker to add custom support for the vardic parameters.

I made a PR draft to show y proposed changes:

you can review it.