Is this correct StaticString capturing behavior?

I wonder why the obtain function captures test while it's not really executed inside the scope of the test function.

func obtain(_ s: StaticString = #function) -> StaticString {
  return s
}

func test(
  _ s1: StaticString = #function,
  _ s2: StaticString = obtain(),
  _ s3: StaticString = obtain(#function)
) {
  print(s1, s2, s3, #function)
}

func foo() {
  test()
}

foo() // prints "foo() test test test"

2 Likes

Actually when you set default value to s1, s2 and s3, it execute inside the scope of test when there's no parameter in your call test().

But if that was true s1 would return ˋtestˋ as well, but it returns ˋfoo()ˋ as expected. So I don‘t understand the difference between s1 and s2 or s3 and would also expect those to return ˋfoo()ˋ rather than ˋtestˋ.

If this really is wrong and a bug I‘ll file it, but I want to be certain. Maybe someone with insights in the compiler of that area can provide a detailed answer.

1 Like

I looked into this Expressions

The document said the value of function is:

The name of the declaration in which it appears.

So in test() s1 is passed from foo(), in obtain from s2 and s3 is test passed from test()

To verify this, you can run the code:

func obtain(_ s: StaticString = #function) -> StaticString {
    return s
}

func test(
    _ s1: StaticString = #function,
    _ s2: StaticString = obtain(),
    _ s3: StaticString = obtain(#function)
    ) {
    
    print(obtain(#function)) // prints 'test'
    print(#function) // prints 'test'
    print(s1, s2, s3, #function) // prints "foo() test test test"
}

func foo() {
    print(#function) // prints 'foo()'
    test()
}

foo()

This does not make any sense to me. s2 and s3 are clearly not inside the scope of the test function. Don't get me wrong, the values of s2 and s3 are, but not default values and the calls to obtain are not, from my point of view. Both values have to be generated and passed to test. I don't know if function parameters are * lazy * or something but calling obtain inside test does not mean that s2 is inside the scope of test, similarly how s3 has no visibility of s2 which in the same scope it would have.

To put it in a different perspective, I expect the above example to behave like this:

func obtain(_ s: StaticString = #function) -> StaticString {
  return s
}

func test(
  s1: StaticString = #function,
  s2: StaticString = obtain(),
  s3: StaticString = obtain(#function)
) {
  print(s1, s2, s3, #function) // prints "foo() test test test"
}

func bar() {
  test(s2: obtain(), s3: obtain(#function))
}

bar() // prints "bar() bar() bar() test(s1:s2:s3:)"

The difference between test function with parameters or not is like this:

func obtain(_ s: StaticString = #function) -> StaticString {
    print(s) // prints bar() first and then prints test(s1:s2:s3:) next
    return s
}

func test(
    s1: StaticString = #function,
    s2: StaticString = obtain(),
    s3: StaticString = obtain(#function)
    ) {
    
    print(s1, s2, s3, #function) //
}

func bar() {
    
    test(s2: obtain(), s3: obtain(#function)) // prints bar() bar() bar() test(s1:s2:s3:)
    
    test()// prints bar() test(s1:s2:s3:) test(s1:s2:s3:) test(s1:s2:s3:)
    
}

bar()

Have any ideas?

That's my main question. Why is this like this and when are default values evaluated?

I would expect them to be evaluated inside foo's body which then leads to the assumption that

func foo() {
  test(s2: obtain(), s3: obtain(#function))
  test()
}

should behave the same.

I read the document again Document

This is what the document says:

Inside a function, the value of  `#function`  is the name of that function, inside a method it is 
the name of that method, inside a property getter or setter it is the name of that property,
 inside special members like  `init`  or  `subscript`  it is the name of that keyword, 
and at the top level of a file it is the name of the current module.

When used as the default value of a function or method parameter, the special literal’s value is determined when the default value expression is evaluated at the call site.

1. func logFunctionName(string: String = #function) {
2. print(string)
3. }
4. func myFunction() {
5. logFunctionName() // Prints "myFunction()".
6. }

Can this explain your question?

For me the "inside something" is always inside the body which is { ... } that's why test(s1: StaticString = #function) will return the outer scope not the name of the function where it's declared.

I really think this is a bug.

As the document says, when #function is the default value of a parameter, it evaluates to the name of the function or method that calls it. So:

  • s1 evaluates to "foo" because foo calls test, which has such a parameter
  • s2 evaluates to "test" because foo calls test, which calls obtain, which has such a parameter

In all other circumstances, #function evaluates to the name of the function it's lexically "inside" (that's why TSPL says it's the "name of the declaration in which it appears"). This has nothing to do with where default arguments are evaluated because it's a literal expression that isn't evaluated like, say, a function call. That's why there's the # in front of it. So:

  • s3 evaluates to "test" because foo calls test, which calls obtain with an explicit argument #function, which evaluates to test because the argument is written in the declaration for test

This is SR-6913, and I'm actually sympathetic to changing the behavior here.

Some version of this is important if you do something like this:

var globalFallbackValue: Int?

func globalFallbackAssertingIfNotSet(file: StaticString = #file, line: Int = #line) -> Int {
  guard let result = globalFallbackValue else {
    preconditionFailure("global fallback not set yet", file: file, line: line)
  }
  return result
}

func test(_ x: Int = globalFallbackAssertingIfNotSet()) {
  print(x)
}

test()

In this case, the code that should get blamed for the precondition violation is the place where the call to globalFallbackAssertingIfNotSet is written. (Okay, I guess that's arguable, but I didn't want to come up with a less contrived example.) To make that happen, the #file/#line in the arguments of globalFallbackAssertingIfNotSet have to be evaluated in the context of the declaration of test, not the call. But we could say that's different because it's not explicitly written in the call to globalFallbackAssertingIfNotSet.

Sorry for late response but can you clarify what values you expect in your example, I'm not sure I can follow it.

 1. var globalFallbackValue: Int?
 2. 
 3. func globalFallbackAssertingIfNotSet(file: StaticString = #file, line: Int = #line) -> Int {
 4.  guard let result = globalFallbackValue else {
 5.    preconditionFailure("global fallback not set yet", file: file, line: line)
 6.  }
 7.  return result
 8. }
 9. 
10. func test(_ x: Int = globalFallbackAssertingIfNotSet()) {
11.  print(x)
12. }
13.
14. test()

I expect this to point to line 10 when the failure happens, not line 14. That's what it does today, and I think it should continue to do so even if we make the change we've been talking about. I can see arguments both ways, though.

Okay I see.