I'm so new to Swift that I have a whole ocean behind my ears, but I figured I'd chime in anyway.
First, permitted trailing commas are a thing in many other languages that I've worked with and they make relatively common operations much easier. I'm a +1 for including them.
Second, if it's decided not to include them, you can do away with much of the pain by using leading commas instead of trailing ones:
I think you've just moved the problem we run into with a lack of trailing commas to the front of the list. I don't think the problem is diminished at all.
In my experience, trailing commas generally cause issues when you are:
Adding new items at the end of the list, or,
Rearranging items within the list
If you use leading commas then item 1 isn't an issue and item 2 is only an issue if you're moving something to the head of the list, which is tautologically less common than moving it elsewhere if there are more than two items in the list. Either way, a leading comma is highly visible and unlikely to be overlooked. Since you are presumably moving the entire line elsewhere or adding an entirely new line, this solves most of the issues.
This isn't a problem either way, as the compiler will catch any mistakes. My argument is that you have the same problems, just in different places. I wasn't arguing the probability of encountering the problems.
That's specific to Go's ASI. If you don't have a trailing comma, Go inserts a semicolon into the array literal, and obviously that causes parse errors.
The implementation for tuples and arguments/parameters was straightforward because a right parenthesis after a comma is a clear closing delimiter. However, for multiple conditions the grammar is more ambiguous. Consider the following code:
if true, {true}(), { value = 3 }
The pair ,{ is not a clear delimiter because it would make {true}() the if body and not a condition.
So what I did was catch the first closure that is not immediately-called and use it as the if body. As far as I've tested It's working and if you forget the parenthesis of what should be an immediately-called closure it won't parse successfully because the rest of the code will be malformed.
❌ error: consecutive statements on a line must be separated by ';'
if true, {true}, { a in a == 1 }(1), { value = 1 } else { value = 2 }
^
The part of the parser that deals with closures in control flow statement conditions already has some fairly ad-hoc rules around newlines. For example, a function call in a condition can have a trailing closure only if the trailing closure starts and ends on the same line:
// Valid `if` statement (trailing closure in condition)
if myFunc { $0 } {
body()
}
// Not valid
if myFunc {
$0
} {
body()
}
Likewise, the parser already has lookahead conditions to ensure that if it sees just a single brace-delimited structure, it's treated as the body and not a trailing closure:
// These braces must be the `if` body, not a trailing closure.
if myFunc { something }
someOtherThing()
There might be some cases I'm forgetting, but it seems like we should be able to apply the same logic here—the presence of a trailing comma is kind of irrelevant if we consider the following situations:
// Valid `if` statement (trailing closure in condition)
if myFunc { $0 }, {
body()
}
// Not valid (trailing comma is irrelevant, we'd already reject this
// from being parsed as a trailing closure, so it would parse as an
// `if` stmt followed by a comma)
if myFunc {
$0
}, {
body()
}
// These braces are now unambiguously the `if` body, not a trailing
// closure.
if myFunc, { something }
someOtherThing()
@ahoppen Can probably provide more detail here or correct any misstatements I may have made.
For some values of 'can': this compiles but with a warning not to do it. That we've baked in a sort of 'best effort' here going beyond diagnostics to being determinative of what's valid versus invalid Swift has always seemed weird to me.
Another interesting difference here is that we'd be introducing an ambiguity for "leading" closures, which have up to now been unconstrained, as opposed to trailing closures. Right now, although it's of debatable taste, you can write
if condition1, {
/*long closure body*/
}() {
/* long condition body */
}
or a custom binary or postfix operator could exist like
func <| <T>(lhs: (T) -> (U), rhs: T) -> U {
return lhs(rhs)
}
if condition2, {
/* long closure body */
} <| argument {
}
and these are accepted without judgment by the compiler today.
Although I would personally prefer a consistent rule that all comma-separated lists allow trailing commas, I think it's reasonable to raise the question of whether condition lists should be excluded. If your house brace style is "K&R style", then you're usually going to have a brace after the final condition already and wouldn't get the "clean diff" benefit from adding new conditions to the end of a list.
I've implemented another approach that barely changes the existing implementation and only touch the conditions if there's something wrong with the if body.
The following code currently parses a broken body:
❌ error: Expected '{' after 'if' condition
if true, f { $0 }, { true }(), { value = 1 } else { value = 0 }
So I reach the conditions and use the last condition as the body to fix the if statement.
The advantage over my previous implementation is that it gives better errors.
❌ error: function produces expected type 'Bool'; did you mean to call it with '()'?
if true, f { $0 }, { true }, { value = 1 } else { value = 0 }
^~~~~~~~
+1 to supporting trailing commas in contexts where there is no real reason (from a correctness POV) not to support them. It is incredibly frustrating and confusing that a trailing comma in a list is allowed but the same is not allowed for an initializer or function call.. that seems extremely arbitrary. I personally run into this kind of build error when refactoring or adding new parameters a lot.
The other discussion about whether or not this is "good style" is irrelevant IMO. Swift unlike Go for example, does not really enforce these kinds of style choices anywhere else so it seems odd to enforce some arbitrary style here.
The primary problem the Swift compiler already has is that it is a recursive descent parser that doesn't have a terminal symbol for a statement. We have compiler timeouts of 30secs and we have just plain old "giving up" compilation aborts that make it impossible to do development of larger scale apps without another window/screen dedicated to "git diff" to let you actually see what code you've changed and where a syntax error or type usage error may be confusing the compiler. Every time you get something to compile, you have to check that in as you next diff point. This mode of development is frustrating and time wasting. Adding another "open ended syntax" position should be carefully considered. The closing ')' is a good terminal symbol, but not if you have mismatched parens because the editor is adding matching symbols and you unknowingly confuse it into doing this some place out of direct view.
I have two different projects that I've stopped work on because I cannot get the compiler to tell me anything more than "I give up" after I started making changes to use some SwiftData objects in place of in memory structures. It's definitely broken for me.
Without more detail it's hard to say, but based on your description, the source of your problems is likely in SourceKit or in the type checker. As Joe said, the parser itself is not the performance issue here.
big +1 from me. especially for function calls / initializers with large parameter lists, where swiftformat breaks up arguments onto separate lines, failing to compile because of the trailing comma on the last arg is a constant paper cut. i'm usually jumping between typescript, swift, and rust every day, and swifts behavior is an outlier and frequently breaks my flow.
You say performance. I am telling you that the "COMPILER" does not finish compiling. That's not a performance issue, unless the "30 sec timeout reached" message is because you've decided that it should only take 30 sec to compile something without realizing your are in no way able to control how much of the CPU the compiler gets because it's a multi-user/multi-processing environment. I've never, ever experience a compiler with a timeout, nor have I experienced a compiler that can't tell you exactly why it cannot compile the input program source code. It just makes no sense to me. I've studied computer languages for decades and wrote compilers using lex/yacc. Recursive descent parsers have the problem that they need some kind of token to direct their behavior at each stage of the compilation. The fact that swift doesn't have ';' required is a complicating factor apparently, because the swift compiler cannot seem to find a terminal error condition that it can report to the user. The fact that it cannot get to a terminal state is the problem. Calling that a performance issue or not a performance issue doesn't solve the problem of me.