12
$\begingroup$

Thus far, I see two things that can go wrong when implementing the ?: (ternary conditional) operator:

  1. PHP famously incorrectly implemented the ?: operator as being left-associative rather than right-associative, making it much harder to write else-if statements using ?:.
  2. As the StackOverflow user called rici warned me, my AEC-to-x86 compiler generates incorrect code for ?:. Namely, the code it produces calculates the results of the second operand and the third operand before calculating the result of the first operand (the condition), which can, according to rici, lead to unexpected divide-by-zero errors for expressions such as d = 0 ? 0 : n / d (if d is equal to zero; = is the equality operator in AEC, not an assignment operator). There does not seem to be a simple solution, given the way my AEC-to-x86 compiler is structured.

Are you aware of any other caveats you might run into when implementing the ?: operator in your language?

$\endgroup$

2 Answers 2

9
$\begingroup$

There's a particular issue in statically typed languages. The expression a ? b : c needs to have a type, but in most cases it's too strict to say that the types of b and c must be identical. For example, in Java:

  • cond ? 1 : 1.5 is allowed, where the branches are of type int and double respectively.
  • cond ? new Cat() : new Dog() is allowed, where Cat and Dog are different classes.

These are two genuinely different cases, both of which must be handled:

  • In the first case there is no common supertype of int and double, but there are rules for promoting from one primitive type to another. These rules must be specified, and the conversion operation must be inserted into the compiled output.
  • In the second case, there is a common supertype of Cat and Dog but it needs to be determined; it might be a superclass like Animal or Object, or perhaps some more complicated type based on what interfaces are shared by both classes, like Serializable & Pettable.

More abstractly, this means your type system needs to support a "least upper bound" (or join) operation, which determines the strictest common supertype of two types, possibly while also allowing implicit type conversions (like from int to double). This operation needs to not only be implemented in the compiler, but also specified since your users may depend on its behaviour.

Depending on the other features of your language (e.g. list literals), your type system might already have a "least upper bound" operation; however, for many simpler languages, the ?: ternary operator is the only language construct that requires it.

$\endgroup$
4
  • $\begingroup$ Thank you, you helped me discover (and fix) two bugs in my AEC-to-WebAssembly compiler: github.com/FlatAssembler/AECforWebAssembly/issues/19 $\endgroup$ Commented Jun 10, 2023 at 20:06
  • 1
    $\begingroup$ Swift allows both a ? b : c and if a { b } else { c } as expressions, but they type-check differently, for complicated reasons: swiftfiddle.com/xoialk2zarbznco2uslp6mt3sa $\endgroup$
    – Bbrk24
    Commented Jun 11, 2023 at 4:33
  • 1
    $\begingroup$ @Bbrk24 That sounds like it would make for a good answer ─ I'd be interested in reading about what the differences between those two forms are, and why. $\endgroup$
    – kaya3
    Commented Jun 11, 2023 at 16:00
  • $\begingroup$ As an expansion of a note mentioned in passing, Haskell does have a ternary operation that requires its operands to have the same concrete type: if a then b else c simply will not compile if b and c are not able to have the same type, for instance, the expression if a then (10 :: Int) else 0.5 will not compile because there is no way to make 0.5 manifest as an Int. $\endgroup$ Commented Jul 5, 2023 at 18:26
6
$\begingroup$

Going off of @kaya3's answer about type inference, that comes with its own set of footguns.

For example, consider this Swift code:

_ = [1].lazy.map { _ in print("Never called, because lazy") }
_ = true ? [1].lazy.map { _ in print("...but this is?") } : []

So, Swift has a somewhat complicated set of sequence and collection protocols. Sequence has a bunch of methods, like map and filter, that return arrays. LazySequenceProtocol has methods by the same names, but they return more lazy sequences. Putting .lazy after a sequence wraps it in a lazy sequence.

LazySequenceProtocol inherits from Sequence, so it still has the versions that return arrays. You can't normally call them because they're shadowed, but they exist. In the second line above, the [] in the false case forces the whole expression to be of type Array<_>, and the eager version is called.

For this reason, among others, Swift opted to disable this bidirectional type inference in if expressions. The following code does not compile*:

_ = if true {
    [1].lazy.map { _ in print("Can't be called if it doesn't compile") }
} else {
    []
}

Unfortunately, neither does this:

_ = if condition { 1 } else { 1.5 }

You have to explicitly write 1.0 or 1 as Double.

*Well, it doesn't compile in 5.8 regardless, because if expressions are a feature in 5.9+. At the time of writing, you have to use a nightly build of Swift in order to get a meaningful error message.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .