2

I played around with Swift again today and was in need of a undefined() function. Basically a function that can be any type you want but crashes when it's actually run/evaluated. That's useful if you haven't had the time to implement a certain expression yet but want to see if the program type checks.

I also implemented a Result<T> and a (questionable) function chain that returns the left Result<> if not successful, otherwise the right Result<>. Therefore, the signature is chain<L,R>(l : Result<L>, r : @autoclosure () -> Result<R>) -> Result<R>. I defined the right Result<> as @autoclosure because there's no need to evaluate it iff the left is a failure.

I'm not interested in the usefulness (or improvements) for any of this, I'm just interested why my program crashes in the line marked [L3].

Please note that

  • as expected [L1] works fine (|| is lazy evaluated)
  • [L2] works fine (chain is also lazy in its right argument)

But, weirdly

  • [L3] crashes the program evaluating undefined()

From my understanding L2 and L3 should be equivalent at runtime: L3 is just telling the type checker something it already knows... The same effect happens by the way if you change the type of undefined to () -> Result<T> (instead of () -> T), then it all works even without the as Result<String>.

This is all my code:

import Foundation

enum Result<T> {
    case Success(@autoclosure () -> T);
    case Failure(@autoclosure () -> NSError)
}

func undefined<T>(file:StaticString=__FILE__, line:UWord=__LINE__) -> T {
    fatalError("undefined", file:file, line:line)
}

func chain<L,R>(l : Result<L>, r : @autoclosure () -> Result<R>) -> Result<R> {
    switch(l) {
    case .Failure(let f):
        return .Failure(f())
    case .Success(let _):
        return r()
    }
}

func testEvaluation() {
    let error = NSError(domain: NSPOSIXErrorDomain, code: Int(ENOENT), userInfo: nil)
    let failure : Result<String> = .Failure(error)
    assert(true || undefined() as Bool) // [L1]: works
    let x : Result<String> = chain(failure, undefined() as Result<String>) // [L2]: works
    print("x = \(x)\n")
    let y : Result<String> = chain(failure, undefined()) // [L3]: CRASHES THE PROGRAM EVALUATING undefined()
    print("y = \(y)\n")
}

testEvaluation()

Believe it or not, the output of the program is:

x = (Enum Value)
fatal error: undefined: file main.swift, line 27
Illegal instruction: 4

That can't be right! Why would as Result<String> (which is the only difference between L2 and L3) change the output of the program? That should be entirely handled in the type checker and that means at compile time, no? Compiler bug?

The quickest way for you to reproduce this should be:

  1. copy all my code into the clipboard
  2. cd /tmp
  3. pbpaste > main.swift
  4. xcrun -sdk macosx swiftc main.swift
  5. ./main

actual output:

x = (Enum Value)
fatal error: undefined: file main.swift, line 27
Illegal instruction: 4

expected output:

x = (Enum Value)
y = (Enum Value)
sepp2k
  • 363,768
  • 54
  • 674
  • 675
Johannes Weiss
  • 52,533
  • 16
  • 102
  • 136
  • Smells like a bug. When you add a `println(file)` right before the `fatalError` it will print without crashing. – qwerty_so Jan 17 '15 at 20:59
  • @ThomasKilian, added `print("--> \(file):\(line)\n")` and now it crashes as `x = (Enum Value)\n --> main.swift:28\n fatal error: undefined: file main.swift, line 28 ` – Johannes Weiss Jan 17 '15 at 21:14
  • exactly the same except that line 27 is now line 28 as I added the print... – Johannes Weiss Jan 17 '15 at 21:21
  • I was not correctly saying it "the print does not crash, but the fatalError". So it must be something in fatalError that got hick-ups. Sorry for the bad formulation. – qwerty_so Jan 17 '15 at 21:31
  • It seems that at [L3], the compiler infers the type `T` of `undefined()` as `() -> Result` and not as `Result` and then does not wrap it into a closure at all. – Btw, with `chain(failure, { undefined() }() )` is works as expected – Martin R Jan 17 '15 at 22:14
  • @MartinR but even if it infers it as `() -> Result` (which is a closure) it should still not run it. And yes, I tried the `chain(failure, { undefined() }() ) as well and it works... – Johannes Weiss Jan 17 '15 at 23:11
  • 1
    What I meant is that the *return value* of `undefined()` (which is the value of the type placeholder ``) is inferred as `() -> Result`. So for some reason, `undefined()` is not wrapped into an autoclosure. The compiler only generates a *call* to `undefined()` and passes the return value (which is a `() -> Result` to the `chain()` function. If you set a breakpoint in `undefined()` then you'll see that `T` is something like `(Builtin.RawPointer) () -> Module.Result`. – Martin R Jan 17 '15 at 23:23
  • ah, thanks @MartinR now I understand, that makes sense! Still a bug though... – Johannes Weiss Jan 17 '15 at 23:27
  • Yes, I think so. I tried to reproduce the problem in a simpler case but was not successful. Passing undefined() to a function with an auto closure parameter does not generally have this effect. – Martin R Jan 17 '15 at 23:33
  • Surprised that `undefined` compiles at all without a `noreturn` attribute: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Attributes.html – Joseph Lord Jan 17 '15 at 23:50
  • @JosephLord the `noreturn` is in `fatalError`, that's why. But the normal definition of `undefined` is `undefined = undefined` anyway so the original implementation (and the only valid one without `noreturn`) was `func undefined(file:StaticString=__FILE__, line:UWord=__LINE__) -> T { fatalError("undefined", file:file, line:line); return undefined() }`. But then since we have `noreturn` the `return` is not needed. – Johannes Weiss Jan 18 '15 at 00:02
  • filed radar now: rdar://19510188 || http://openradar.appspot.com/radar?id=4811986399395840 – Johannes Weiss Jan 18 '15 at 00:03
  • FWIW, I also removed the `fatalError` completely and made `func undefined(file:StaticString=__FILE__, line:UWord=__LINE__) -> T { return undefined() }`. Behaviour is the same except that it crashes with stack overflow and not assertion. But still: It evaluates the expression when it shouldn't have. – Johannes Weiss Jan 18 '15 at 00:07

1 Answers1

0

Confirmed as a bug by Swift developer Joe Groff via Twitter.

Johannes Weiss
  • 52,533
  • 16
  • 102
  • 136