Compiler error using custom operators, nil coalescing, and the ExpressibleByStringLiteral protocol

I know from the title this probably sounds like it's going to be rather complex and confusing, so I'll try keep the code and explanation as simple as possible. Basically I have a class acting as a data structure that can be queried using a custom operator. The query is an enum that can be created using a String literal. The result of the query is optional, but when I use the custom operator and the nil coalescing operator to provide a default value, a compiler error occurs.

Here is a greatly simplified implementation of the structure and other code:

class Structure {
    private var values: [String: String] = [:]
    
    func get(query: Query) -> String? {
        switch query {
        case .key(let key):
            return values[key]
        }
    }
}

enum Query: ExpressibleByStringLiteral {
    case key(String)
    
    init(stringLiteral string: String) {
        self = .key(string)
    }
}

precedencegroup QueryPrecedence {
    associativity: left
    higherThan: NilCoalescingPrecedence
}

infix operator % : QueryPrecedence

func % (structure: Structure, query: Query) -> String? {
    return structure.get(query: query)
}

(If you're wondering why the enum only has one case, that's just because of the way I've simplified the code - the actual code uses more cases, which is why I'm not just querying with Strings directly)

In most cases, this code works perfectly fine. The following lines of code all compile without any issues:

let string = structure.get(query: "test")
let string = structure.get(query: "test") ?? "Default value"
let string = structure % "test"

However, the using both the custom operator and the nil coalescing operator like this:

let string = structure % "test" ?? "Default value"

Results in a compiler error: Cannot convert value of type 'Query' to expected argument type 'String'

As far as I can tell, the compiler is attempting to evaluate the "Default value" literal as a Query, ignoring the precedence that states that the custom operator should be evaluated first. Even using brackets to create an explicit order - (structure % "test") ?? "Default value" - still produces the same error. Forcing the literal to be treated as a String - structure % "test" ?? "Default value" as String - does compile normally.

My guess is that this is some very minor bug in the compiler, but maybe there is some other explanation for it - I'm curious to know if that's the case. It's definitely not a scenario that's likely to be encountered often.

I should add that the error isn't just related to Strings - if the Query enum implements ExpressibleByIntegerLiteral (for example querying with indices) and the type returned by the query is also Int then the same error occurs. However, mixing different types (like using a string literal query to retrieve an integer) works fine.

1 Like

You cannot define the precedence of the infix operator % because it already exists and has a precedence (MultiplicationPrecedence). Feel free to file a bug against the compiler for not giving you a useful diagnostic warning about that. (For obvious reasons, you cannot redefine the precedence of existing operators.)

Edit 2: By golly, Swift for some reason actually allows you to change the precedence of existing operators on redefinition. Please don't do this.


Edit: Independent of that, however, it does look like you've revealed a bug; there are several shortcuts to type resolution that are required to allow operators and literals to be used together in expressions that are solvable in reasonable time, and one of those clearly backfires here. cc @hborla

1 Like

One of these shortcuts used to merge together the type variables for literals of the same kind in operator expressions (e.g., the two different string literals "test" and "default value") so that those types wouldn't need to be solved for independently during type inference. Obviously this was incorrect, because you can write a valid operator expression where string literals have different types, like in this case. I've already removed this "optimization" and it should be fixed in Swift 5.4.

EDIT: I double checked and this code compiles successfully on main (I didn't check release/5.4 but I'm almost positive the work is there as well)

5 Likes

Interestingly, the % operator is not the one I've been using in the actual code - I just happened to use it in the playground I was testing in to narrow down the source of the problem, completely forgetting that it already exists. I'm actually surprised it's possible to redeclare it at all. On the other hand, given that the multiplication precedence should be higher than the nil coalescence precedence already, you'd think that other than the colliding precedence declarations it should still work as expected. And yet if I remove the operator redeclaration entirely and just create a function using the already-existing % operator, the same error still occurs.

The really strange part is that the operator I'm using in the full project actually does seem to work in the playground despite not working in the project, and I'm not sure what could be causing the discrepancies since as far as I can tell they're both declared in the exact same way. In fact that seems to be the case for every operator I've tried in the last few minutes except % - they're all working in the playground but error in the actual project.

I can't reproduce the issue where the code behaves differently between Xcode project and Playground, but I can get the code to compile in both by changing the operator from % to one that doesn't exist in the standard library (e.g., ***). Without actually looking at what type inference does in this case, I'm guessing the difference is that the one I defined doesn't have overloads, so many of the types can be resolved immediately.

EDIT: Could it be the case that you have overloads of your custom operator in your Xcode project, but you don't in your isolated Playground?

1 Like

Aha - yes, that seems to be the issue! Adding overloads in the playground does indeed result in the compiler errors.

1 Like

Off-topic: while looking for the stdlib operator declarations, I stumbled upon this:

The Swift grammar doesn't allow comma-separated identifiers after an operator declaration and an error should be thrown. Does anyone know if those annotations actually do anything? Without context they look like a way to define different precedences based on protocol conformances or types of the operands involved (or maybe just mere hints to the compiler to limit the first search space when those operators are encountered during type checking).

See here.

2 Likes

Note that the experimental implementation of operator designated types has since been removed from the compiler, so -solver-enable-operator-designated-types will no longer work.

1 Like