Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Swift proposal for a "macro" for generating comma-separated lists.

Entity Duplication

Introduction

This is a proposal to add a macro-like facility to automatically create lists.

Swift-evolution thread:

Motivation

Variadic function parameters and arguments take comma-separated lists of items (parameters and arguments, respectively). Variadic generics would to the same thing for their respective parameters and arguments. All of these consume comma-separated lists.

The items of these variadic lists have to be declared manually. There is currently no producer of comma-separated lists, the dual functionality that can automate list creation.

The following is an example of a conversion function between homogenous tuples and proposed fixed-size arrays, using C++-like syntax for variadic generics and Rust-like syntax for array declarations:

func array<T, U...>(from: (T, ...U)) -> [T; 1 + #countOf(U)] where ...(U == T)

Every element of the tuple has to be of the same type. If so, the function returns a fixed-size array of the same number of direct sub-objects that the tuple has, with the tuple element type as the array element type.

The problem is converting in the other direction:

func tuple<T, let N: Int>(from: [T; N]) -> ???

The return type is a tuple consisting of type T repeated N times. The tokens for T have to mechanically appear N times with commas between them.

Some other languages have a macro facility to work with source code entities before the compiler proper can see them. The most (in)famous ones are from the (Objective-)C(++) family, which basically dumps the text of whatever it generates directly into the source. The result doesn't even have to be syntactically valid except for what's needed at the preprocessor parsing stage. And the preprocessor level is too early to see constructs like N above as their values instead of token(s).

Other languages like Rust have a more sophisticated macro system. The creations of Rust macros have to follow the Rust parsing syntax; i.e. be valid according to the Rust AST (abstract syntax tree). Names are not fully resolved, so the N above may still not be useable to generate to correct number of tuple elements. [I'm not completely sure, since I don't really know Rust.]

Describe the problems that this proposal seeks to address. If the problem is that some common pattern is currently hard to express, show how one can currently get a similar effect and describe its drawbacks. If it's completely new functionality that cannot be emulated, motivate why this new functionality would help Swift developers create better Swift code.

Proposed solution

The solution is to add a duplication facility that creates lists of comma-separated entities from a given entity template that matches one of a restricted set of parsing productions. The duplication call is neither a text nor token replacement; it is treated as a variant of the expected type of list.

The format of a duplication call has to be handled by the two first stages of translation, parsing and semantic analysis. The call has to be resolved by the semantic analysis phase, where the later phases only see the code post-replacement.

The specification of the number of duplicates to be made therefore has to be simple enough for the early translation phases to understand. So, the duplicate count generally has to be an nonnegative integer literal.

The entity to be duplicated doesn't have to identical for each item. Since the number of duplicated items is known, there is a way to insert the index of a duplicated item into the item itself. When there are nested duplication calls, there are ways to for a duplicated item to refer to the index of any of the duplication calls it's nested in.

A duplication index is the other way to express a duplicate count. Duplication calls are evaluated from the outside in. These two policies let a duplication index be (part of) the count of a nested duplication call.

[Future Directions: Right now, a duplication count must be exactly either an integer literal or a duplication index. They cannot be mixed since there is no operations to do it. That would require compile-time expression evaluation, which is not in Swift right now.]

Describe your solution to the problem. Provide examples and describe how they work. Show how your solution is better than current workarounds: is it cleaner, safer, or more efficient?

Detailed design

A duplication call consists of the #dup keyword followed by a pair of parentheses containing the duplicate count, a separating semicolon, and the entity template. For instance:

#dup(6 ; Int)

would duplicate Int six times. The copies are comma-separated, so the call shall have the effect of:

Int, Int, Int, Int, Int, Int

in the code.

At first glance, the grammar could be like:

duplicated-list#dup ( duplication-count ; duplicatable-list )

duplicatable-listidentifier-list

duplicatable-listtuple-type-element-list | function-type-argument-list | type-inheritance-list

duplicatable-listexpression-list | array-literal-items | dictionary-literal-items | closure-parameter-list | capture-list-items | tuple-element-list | function-call-argument-list

duplicatable-listcondition-list | case-item-list

duplicatable-listpattern-initializer-list | parameter-list | union-style-enum-case-list | raw-value-style-enum-case-list | precedence-group-names

duplicatable-listtuple-pattern-element-list

duplicatable-listgeneric-parameter-list | requirement-list | generic-argument-list

But this may involve too much look-ahead, too many possibilities to consider at once, and it doesn't cover nested duplication calls. So we'll handle each type duplication call as a case of the list it wants to repeat (but now risk infinite recursion).

Duplication Counter

The duplication index is referenced by a duplication placeholder, formed with two dollar signs followed by a decimal digit sequence. The placeholder of "$$0" tracks the index of the innermost duplication call containing the placeholder. A placeholder of "$$1" tracks the index of the duplication call immediately enclosing the innermost one. Increasing placeholder values match the index for more encompassing duplication calls. It is a compile-time error to have a placeholder value equal or greater than the number of nested duplication calls.

The placeholders; notated by $$n here, where n is a nonnegative integer; affected by a given duplication call are the ones where n matches the nesting the placeholder has relative to the call. Among the $$0 placeholders in a call, only the ones directly in the entity template are affected. For $$1 placeholders, only the ones directly in any immediately nested duplication calls are affected by the outer call. The only $$2 placeholders affected by a call are the ones nested exactly two calls down, etc. Unaffected placeholders either get resolved by other duplication calls or cause an error. As an example:

#dup(6 ; $$0 + myfunc(#dup(4 ; $$1 * $$0)))

The outer duplication call will determine the value of the outer $$0 and the $$1, while the inner call will determine the value of the inner $$0. If any of the placeholders was replaced with a $$2 or higher (or the first placeholder replaced with a $$1), an error would be reported since there's not enough nesting for those placeholder values to match any calls.

When a duplication count contains a placeholder, the placeholder is not relative to is owning duplication call, but to the call surrounding it (or an error if there's no such call). For example:

#dup(5 ; #dup($$0; $$0))

The first $$0 is replaced during the outer call's expansion, thus determining the count of the inner call. The second $$0 is replaced during the inner call's expansion.

Empty Lists

A duplication call is considered empty if it either has a duplication count of zero or if its entity template has no direct list items that are not duplication calls and all present directly nested calls are also considered empty.

Duplication

When a duplication call is empty, then the call is replaced with the empty token sequence. Otherwise, the call is replaced by the specified number of copies of the entity template, separated from each other (if more than one) by commas. Within each copy, affected duplication placeholders are replaced by a decimal digit sequence representing the zero-based index the given copy has relative to the expanded list. Adjacent affected placeholders have distinct digit sequences, not a single mushed one, even if the separation causes a parsing error.

Replacement during a duplication call happens in two passes. The first pass performs the replacement without expanding any nested calls, but still resolving any affected duplication placeholders within those calls. Now that any directly nested calls whose duplicate counts were dependent on the outer call have been resolved, the second pass replaces all those calls with their expanded lists. (The second pass doesn't complete until those directly nested calls finish their replacements, which in turn wait until all the indirectly nested calls, if any, finish their replacements.)

After a duplication call is completely replaced, any sequences of consecutive commas without any tokens between them are collapsed to a single comma. If the call was a direct item of a surrounding list, and was both empty and the first item of the surrounding list, then the separating comma is removed when the call is replaced by the empty token sequence. A similar thing happens when an empty call is the last item of its surrounding list, and the surrounding list doesn't permit a trailing comma. (As of Swift 4, only array and dictionary literal item lists permit trailing commas.)

Shorthand Argument Placeholders

The shorthand names of a closure's arguments are based off nonnegative integer indexes. Since that's what a duplication call can deal in, the two constructs can be used together with parentheses:

$($$0)

The above identifier points to the shorthand argument with the index number of the value filled in by the placeholder.

func combine4<T, U, V, W, Result>(t: T, u: U, v: V, w: W, closureOfFour: (T, U, V, W) throws -> Result) rethrows -> Result {
    return try closureOfFour(t, u, v, w)
}

func hashSum<T, U, V, W: Integer>(t: T, u: U, v: V, w: W) -> Int {
    return combine4(t, u, v, w) {
        return [#dup(4; $($$0).hashValue)].reduce(0, +)
    }
}

[Note: For now, you have to track the count of arguments to pass to the duplication call.]

Grammar

Add a production to the "Grammar of an Identifier":

implicit-parameter-name$ ( duplication-index-placeholder )

Modify a production of the "Grammar of a Literal":

literalnumeric-literal­ | string-literal­ | boolean-literal | nil-literal­ | duplication-index-placeholder

Add a production to the same grammar:

duplication-index-placeholder$$ decimal-digits

Add a production to the "Grammar of an Explicit Member Expression":

explicit-member-expressionpostfix-expression . duplication-index-placeholder

Under Generic Parameters and Arguments, add the "Grammar of a Duplication Directive":

duplication-countinteger-literal | duplication-index-placeholder

duplication-directive-head#dup ( duplication-count ;

duplication-directive-tail)

duplicated-identifier-listduplication-directive-head identifier-list duplication-directive-tail

duplicated-tuple-type-element-listduplication-directive-head tuple-type-element-list duplication-directive-tail

duplicated-function-type-argument-listduplication-directive-head function-type-argument-list duplication-directive-tail

duplicated-type-inheritance-listduplication-directive-head type-inheritance-list duplication-directive-tail

duplicated-expression-listduplication-directive-head expression-list duplication-directive-tail

duplicated-array-literal-itemsduplication-directive-head array-literal-items duplication-directive-tail

duplicated-dictionary-literal-itemsduplication-directive-head dictionary-literal-items duplication-directive-tail

duplicated-closure-parameter-listduplication-directive-head closure-parameter-list duplication-directive-tail

duplicated-capture-list-itemsduplication-directive-head capture-list-items duplication-directive-tail

duplicated-tuple-element-listduplication-directive-head tuple-element-list duplication-directive-tail

duplicated-function-call-argument-listduplication-directive-head function-call-argument-list duplication-directive-tail

duplicated-condition-listduplication-directive-head condition-list duplication-directive-tail

duplicated-case-item-listduplication-directive-head case-item-list duplication-directive-tail

duplicated-pattern-initializer-listduplication-directive-head pattern-initializer-list duplication-directive-tail

duplicated-parameter-listduplication-directive-head parameter-list duplication-directive-tail

duplicated-union-style-enum-case-listduplication-directive-head union-style-enum-case-list duplication-directive-tail

duplicated-raw-value-style-enum-case-listduplication-directive-head raw-value-style-enum-case-list duplication-directive-tail

duplicated-precedence-group-namesduplication-directive-head precedence-group-names duplication-directive-tail

duplicated-tuple-pattern-element-listduplication-directive-head tuple-pattern-element-list duplication-directive-tail

duplicated-generic-parameter-listduplication-directive-head generic-parameter-list duplication-directive-tail

duplicated-requirement-listduplication-directive-head requirement-list duplication-directive-tail

duplicated-generic-argument-listduplication-directive-head generic-argument-list duplication-directive-tail

Add these productions to their siblings scattered across many grammars:

identifier-listduplicated-identifier-list | duplicated-identifier-list , identifier-list

function-type-argument-listduplicated-function-type-argument-list | duplicated-function-type-argument-list , function-type-argument-list

type-inheritance-listduplicated-type-inheritance-list | duplicated-type-inheritance-list , type-inheritance-list

expression-listduplicated-expression-list | duplicated-expression-list , expression-list

array-literal-itemsduplicated-array-literal-items ,-opt | duplicated-array-literal-items , array-literal-items

dictionary-literal-itemsduplicated-dictionary-literal-items ,-opt | duplicated-dictionary-literal-items , dictionary-literal-items

closure-parameter-listduplicated-closure-parameter-list | duplicated-closure-parameter-list , closure-parameter-list

capture-list-itemsduplicated-capture-list-items | duplicated-capture-list-items , capture-list-items

function-call-argument-listduplicated-function-call-argument-list | duplicated-function-call-argument-list , function-call-argument-list

condition-listduplicated-condition-list | duplicated-condition-list , condition-list

case-item-listduplicated-case-item-list | duplicated-case-item-list , case-item-list

pattern-initializer-listduplicated-pattern-initializer-list | duplicated-pattern-initializer-list , pattern-initializer-list

parameter-listduplicated-parameter-list | duplicated-parameter-list , parameter-list

union-style-enum-case-listduplicated-union-style-enum-case-list | duplicated-union-style-enum-case-list , union-style-enum-case-list

raw-value-style-enum-case-listduplicated-raw-value-style-enum-case-list | duplicated-raw-value-style-enum-case-list , raw-value-style-enum-case-list

precedence-group-namesduplicated-precedence-group-names | duplicated-precedence-group-names , precedence-group-names

tuple-pattern-element-listduplicated-tuple-pattern-element-list | duplicated-tuple-pattern-element-list , tuple-pattern-element-list

generic-parameter-listduplicated-generic-parameter-list | duplicated-generic-parameter-list , generic-parameter-list

requirement-listduplicated-requirement-list | duplicated-requirement-list , requirement-list

generic-argument-listduplicated-generic-argument-list | duplicated-generic-argument-list , generic-argument-list

Add to the "Grammar of a Tuple Type":

tuple-type( duplicated-tuple-type-element-list ) | ( duplicated-tuple-type-element-list , tuple-type-element-list )

tuple-type-element-listduplicated-tuple-type-element-list | duplicated-tuple-type-element-list , tuple-type-element-list

Add to the "Grammar of a Tuple Expression":

tuple-expression( duplicated-tuple-element-list ) | ( duplicated-tuple-element-list , tuple-element-list )

tuple-element-listduplicated-tuple-element-list | duplicated-tuple-element-list , tuple-element-list

Duplication Lists that Cannot Be Empty

If a given surrounding entity only contains the given kind of duplication calls, without any direct items of what's being duplicated, then at least one call must be non-empty:

  • duplicated-identifier-list as part of a closure-parameter-clause.
  • duplicated-function-type-argument-list as part of a function-type-argument-clause when a terminating ellipsis ("...") is present.
  • duplicated-type-inheritance-list as part of a type-inheritance-clause.
  • duplicated-dictionary-literal-items as part of a dictionary-literal.
  • duplicated-capture-list-items as part of a capture-list.
  • duplicated-function-call-argument-list as part of a self-subscript-expression, superclass-subscript-expression, or subscript-expression.
  • duplicated-condition-list as part of a while-statement, if-statement, or guard-statement.
  • duplicated-case-item-list as part of a case-label.
  • duplicated-pattern-initializer-list as part of a constant-declaration or variable-declaration.
  • duplicated-union-style-enum-case-list as part of a union-style-enum-case-clause.
  • duplicated-raw-value-style-enum-case-list as part of a raw-value-style-enum-case-clause.
  • duplicated-precedence-group-names as part of a precedence-group-relation.
  • duplicated-generic-parameter-list as part of a generic-parameter-clause.
  • duplicated-requirement-list as part of a generic-where-clause.
  • duplicated-generic-argument-list as part of a generic-argument-clause.

These are determined by the surrounding entity not already having an equivalent to an empty list in the grammar using the items' list's bracketing.

Describe the design of the solution in detail. If it involves new syntax in the language, show the additions and changes to the Swift grammar. If it's a new API, show the full API and its documentation comments detailing what it does. The detail in this section should be sufficient for someone who is not one of the authors to be able to reasonably implement the feature.

Source compatibility

The proposal is strictly additive, so there should not be any source compatibility problems.

Effect on ABI stability

As the proposal is additive, no existing features are affected. Furthermore, the proposal actions only affect the parsing and semantic analysis stages, so the ABI level never sees the effects.

Effect on API resilience

The proposal is additive and does not affect existing API or ABI.

Alternatives considered

The main alternative is to do nothing, and not have automated list generation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.