Improve nested functions' visibility or order-dependency

Yes, this is what I meant.

So, I just removed it. Still can be found in the reply's history. My brain wasn't working at the time when I posted it. It also is kind of irrelevant to the pitch anyway.

I will trust the ability of this forum to expand all quotes to show the related texts (and codes). If it doesn't expand, well, I will re-quote to include the code.


I'm sure we can leave out all that crash the compiler. They are bugs in some way since compiler should never crash.


This is very consistent with the model. You declared a closure, then immediately call it. It would quickly translate to

func foo() {
    func _unnamed() {
        bar()
    }
    _unnamed()
    if Bool.random() { return }
    func bar() {
        print("bar")
    }
}
foo()

which compiles (and runs, printing bar).


It doesn't compile on my machine... :thinking: because bar

Cannot capture 'incrementCount', which would use 'count' before it is declared.

Xcode 11.3.1 (11C504) btw.


Type declarations seem to be much safer than function declaration, and so have much more lenient positioning. You can't meaningfully capture local variable anyway.

func foo() {
    let a = 9
    S.bar()
    return
    struct S {
        static func bar() {
            print(a)
        }
    }
}

This doesn't compile, because

Struct declaration cannot close over value 'a' defined in outer scope

I remember a discussion somewhere in this forum that function capturing must happen at call site, not declaration site. So all relevant variables must be declared before the call site. Every example here seems to support that model.

Another rule is already mentioned by @BigSur that a function must be declared before it can be called, which makes sense to me.

I don't see what you mean here. Of course the crash is caused by a compiler bug, that's (part of) why I filed the bug report. We cannot "leave out all that crashes the compiler". The crashing example is interesting because it's not obvious if it's intended to compile or result in a compile time error. (And both the compiler crash and the compile time error talking about a "variable" bar instead of function bar, might possibly be indicating that there are more bugs and inconsistencies in the compiler related to the ordering of nested functions.)

I don't get this either. Again, I think you're ignoring the greater question (and demonstrated inconsistency) and focus on explaining an obvious detail. Of course I know that I'm using an immediately invoked closure expression, and that it could very roughly be translated to a function declaration followed by a call to that function. The important thing here though, is that it demonstrated that bar() was declared after it was called (via the immediately invoked closure expression).

So, would you say that the function bar is "declared before it is called" here:

// This program compiles and runs, printing bar:
func foo() {
    {
        bar()
    }() // <--- bar is called here (via the immediately invoked closure expr)
    if true {
        return
    }
    func bar() {  // <--- bar is declared here
        print("bar")
    }
}
foo()

?

It's not only declared after it's called, it's even declared after the outer function foo returns, at a place in the code where no statement will be evaluated. And note that I'm not saying that this shouldn't compile. I think it should, as it currently does. What I'm saying is that the other two examples in the bug report should compile too.

1 Like

Perhaps we talked past each other here. We can use any example when we start designing, but one that crashes the compiler is not useful for when we're still figuring out what the compiler is doing. From what I've read so far, we seem to be on that phase–figuring out the current rules. I take it that way because you said

as if the compiler already rejects it. I pointed out that it didn't reject the code. It was just confused (and perhaps so was I :stuck_out_tongue: ).

I'm saying that closure doesn't add anything to the table. We can just talk about function declaration/usage inside a function. No use in confusing ourself further. Most discussion will apply almost trivially to closures (or the other way around) anyway.

I agree.

1 Like

That's an important question, and not obvious one to spot from the example (with all the closure shenanigan).

In so far, I gathered that there are 2 scopes, the current scope, and the inner scope.

The current scope is at the same level as the function declaration. The inner scope is all that is inside any function declaration.

Using function in the current scope requires that you call if after declaration, while inner scope can use any functions in the current scope (which is outer scope from inner scope perspective). There's an extra rule that, at call site, all that needs to be captured are initialized (one of you example managed to violate this rule, but I couldn't compile it on any of my machines).

So that's why

func foo() {
    func _unnamed() {
        bar() <-- inner scope, can use any function
    }
    _unnamed() <-- current scope, can use only foo
    if true {
        return
    }
    func bar() {
        print("bar") <-- inner scope
    }
}

Well, I think that there are clearly some inconsistencies in the compiler: Either all three examples of the bug report should compile, or none of them should compile. Currently, one compiles, one crashes compiler and one results in a compile time error.

OK, fine, that's not the impression I got earlier : )

To be clear, I think case A and B should do the same thing, whether to accept or reject it. Perhaps case C too as one quickly have A and B as the work around.

Though I'm not sure how we can find a consistent rule that in turn accept it with as much leniency as the current design.

I didn't test it yesterday, and assumed it would compile. Just tested it now, and it didn't compile for me either. I'm using the same Xcode version. What's your IDE @Jens ?

IIRC, part of the reason nested function works the way it is, is that it must support 2 things,

  • capturing
  • recursion

It was the same discussion I mentioned earlier. If only I could find it...

I've not managed to violate that rule as far as I know. And I have not questioned it, I've merely demonstrated that it exists. It's the other (imho) inconsistencies that I've pointed out, but I've already done that as clear as I possibly can in the bug report, and I don't think further speculation will lead anywhere, I prefer to wait for somebody who knows the intended rules to explain them.

Ah, that compiles only with a recent snapshot (I happened to have that toolchain selected in Xcode without thinking about it at the time) .

That is:

// This program compiles with dev snapshot 2020-02-16,
// but not with eg the default toolchain of Xcode 11.3.1 (11C504):
func foo() {
    func bar() {
        incrementCount()
    }
    // bar() // <--- Uncomment and it will not compile with dev snapshot either. 
    var count = 0
    func incrementCount() {
        count += 1
        print(count)
    }
}
foo()

This thread would definitely greatly benefit from some compiler people's opinions. As far as the conversations have gone, we are still not certain of the rationales behind the current design. Maybe someone from the core team could chime in?

1 Like

Found it. About the reason why closures (functions) capture variables at call site.

2 Likes

Yes, but as far as I can see, the main question of this thread is not answered by that, or maybe I'm missing something?

Again, all that I'm really wondering about is succinctly expressed in the bug report, and it doesn't involve any capturing.

This makes sense. In a way, what is needed is to extend this declaration-at-call-site feature to all members in a function I guess? For example:

func foo() {
    let x = bar()
    func bar() -> Int { return 1 }
}

In the example above, if both x and bar are declared at call site, then the problem would be solved?

However, why is the nested function case so special? If it were a struct/class/enum/protocol, this wouldn't be a problem:

struct Foo {
    static let x = bar()
    static func bar() -> Int { return 1 }
}

Furthermore, the declaration-at-call-site feature seems to have gone much further, so long as it's not within a function. For example, the following code compiles, but will have runtime error:

struct Foo {
    static let a: Int = b
    static let b: Int = a
    static func printA() { print(a) }
}
Foo.printA()

Right, I’m still running in my own head.

The real constraint is that call site is after all captured variables. Right now that constraint are cleverly split into two rules:

  • Function declaration is beneath capture list.
  • Call site is beneath function declaration.

Which does explain the how C violates the rule, and how A side steps it by calling bar from the inner scope instead of current scope. The thread seems to be partially about getting rid of the second rule.

I believe that is one way the rule can be relaxed. If we remove the second rule (from above), and replace it with

  • Call site is beneath all captured list.

It’s probably a workable setup. Though that may also complicate the rule set.

They can’t capture anything anyway. It’s probably safe to put it anywhere.

Funny that. Mine’s full of compile errors about using a and b before self.

Oops... my bad. a and b are supposed to be static. Fixed it now.