Software Integrity

 

Swift: Close to greatness in programming language design, Part 1

swift programming language design

As we are taking our first steps toward a Coverity Static Analysis solution for the Swift programming language, I am discovering one of the most challenging languages yet for Coverity. This is simply because many of the easy-to-make, easy-to-find mistakes in other programming languages were designed to be difficult or impossible in Swift. However, some mistakes that could easily have been excluded were not. Swift is a very good language that easily could have been great.

Background

Coverity was founded on a static bug-finding tool for C/C++ that uniquely balanced false positives and false negatives, held no reverence for soundness, and scaled nearly linearly to analyzing large codebases. It was the killer app for many large-scale must-be-correct C++ development shops, such as Synopsys, who after years as a loyal customer acquired Coverity as a company in 2014.

In the process of expanding its market, both for C/C++ analysis and analysis of other languages, Coverity added support for Java, C#, JavaScript, Objective-C, PHP, Python, and Ruby. The last four I would currently describe as “basic” support, as we carefully balance breadth and depth in our support for analyzing what potential customers are writing. Basic static analysis support focuses on some of the easier, usually INTRAprocedural defect patterns, so that we can find valuable defects in a new language before a huge investment in all the supporting infrastructure for deep INTERprocedural defects.

As for me, I have played a significant role in almost all of our new language support initiatives, which I hope gives me some perspective. I certainly have no pro-Apple bias, as I can’t recall ever buying an Apple product. (My biases are generally pro-Java and anti-C/C++ even though most code written by me and my team is C++.) I haven’t used Swift in any significant way, so I can’t make sweeping judgments about its overall usability. I’m giving a preliminary perspective based on the applicability of static defect patterns to Swift.

And yes, I’m familiar with languages like ML, Haskell, and F#. I would say I’m generally a fan and they generally avoid issues I bring up here. Despite some use in industry, they are not generally accepted as industrial programming languages.

Defect patterns part 1: Rookie mistakes

Finally some fun stuff. Let’s run down some defect patterns and see how Swift stacks up against other languages. For this first post on Swift, let’s consider some of the simplest, best known defect patterns, which I’ll roughly classify as “rookie mistakes.” Most of these are rare in production code but still cause havoc and indicate poor language design choices when applicable.

This post will be followed by two more, covering defect patterns of generally increasing intrigue and/or commonality in production code.

Avoided: Missing break (a.k.a. fall-through)

Perhaps the most classic of simple language design bugs is the implicit case fall-through in C switch statements, embarrassingly inherited by C++, Objective-C/C++, Java, JavaScript, and PHP:

switch(x) {
case 1:
    y = firstSomething();
    // forgot 'break'; implicit fall-through
case 2:
    y = secondSomething();
}

I like the C# “goto case” solution but Python, Ruby, and Swift have also avoided the problem. Swift arguably has additional safeguards by requiring switch statements to be exhaustive. However, it’s not clear to me that Swift has actually solved what I call “the exhaustive switch problem.”

The exhaustive switch problem arises from the desire to have both (a) compile-time checking that no new enumerator has been added to an enumerated type without a corresponding case in each applicable switch statement and (b) runtime checking that the value being switched on is in the range of what was considered exhaustive at compile time. (In Java, for example, differences in linked code between compile time and run time can quietly render a switch non-exhaustive. In C/C++, loose typing and/or memory corruption can render a switch non-exhaustive.) If you include a default case in these languages, you get the runtime check without the compile-time check. If you don’t include a default, you get the compile-time check, but it’s difficult and awkward to also get the runtime check. Fail.

Avoided: Nesting-indent mismatch, dangling else, stray semicolon

Can you imagine how many bugs would have been averted if C had required curly-braces around sub-statements? Yes, it’s 2017 and we’re still dealing with crap this trivial. Such a requirement probably would have averted the Apple ‘goto fail’ bug. Perhaps it’s no coincidence that Apple is now championing a high-hygiene language. For the record, here are a couple ways things can go wrong:

if (checkOne())
    oneA();
    oneB(); // executed regardless of checkOne()

or

if (checkOne())
    if (checkTwo())
        onTwo();
else
    onNotOne(); // actually executed on checkOne() && !checkTwo()

or

if (checkOne());
    onOne(); // executed regardless of checkOne()

And all of Java, C#, JavaScript, Objective-C/C++, and PHP decided to inherit this problematic oddity. Kudos first to Python for making indentation significant, and to Swift for requiring curly-braces, which I expect to be as effective and more accepted by the masses.

Avoided: Assignment where compare meant

Although generally in the realm of super-rookie C/C++ mistakes, even Java and C# permit assignment in a boolean conditional context:

if (b = false) { // meant ==
    something(); // never happens
}

Although, the problem is extremely rare in Java and C# because they do not have a boolean interpretation of other values.

Swift aggressively excludes this bug by disallowing the result of = to be used in any boolean context. (Python implements related measures.)

Probably avoided: Equality confusion

A classic bug in Java is to use reference equality (==) where object equality is intended (.equals), such as on strings. C# tried to fix that by allowing overloading of the == operator to do object equality, but then utterly confused things by retaining the Equals method. C# also left it awkward to invoke reference equality when that is desired.

It seems that Swift has a largely clean, non-confusing approach to equality. The == and != operators are customizable and generally what you want for testing value equality. Testing reference equality is nearly as easy, with === or !==, though apparently you can overload these also! I hope that is not abused.

Unlike most rookie mistakes, this one is not so rare in production code, even excluding safe uses of reference equality (such as interned strings).

Largely avoided: Forgotten ‘else’

Although there’s no Coverity checker for this (pretty rare; often benign), there’s a potential bug with forgetting ‘else’ in ‘else if’:

if (x == 1) {
    y = 10;
} if (x == 2) { // forgot 'else'; y = 10 always overwritten
    y = 100;
} else {
    y = 1000;
}

Unlike other “basic” defect patterns, this one seems tricky to avoid as a language designer. Although that looks like legal Swift, its odd semicolon rules actually come to the rescue:

ERROR at line 3, col 3: consecutive statements on a line must be separated by ';'

If we use newlines after the close braces, the bug is still possible in Swift, but quite forgivably:

if (x == 1) {
    y = 10;
}
if (x == 2) { // forgot 'else'; y = 10 always overwritten
    y = 100;
}
else {
    y = 1000;
}

Continue reading with Part 2 of this series.