[Pitch] Expose assert configuration functions in standard library


(Erica Sadun) #1

Back in March, I somewhat foolishly agreed to pick up the gauntlet for a series of community-requested proposals centered on build configurations. Requested items included:

A way to test for destination platforms like Apple, Linux, Windows, Unix-like, UIKit-supporting, etc
A way to test for Simulator/Emulator vs Hardware targets
A way to test for debug builds
A way to test for platform conditions (bigendian, littlendian, bitwidth 32 and 64, objc-interop, see lib/Basic/LangOptions.cpp)

This splintered down into a series of draft proposals. The first adopted proposal, SE-0075 <https://github.com/apple/swift-evolution/blob/master/proposals/0075-import-test.md> adds a build configuration import test similar to Clang's __has_include. It lets you test whether a module like UIKit is available, letting you customize code for specific modules.

Next up on my list is debug-specific coding. Summarizing to date:

There's a general consensus that a debug state occurs when assertions can fire and are not disabled by compile-time optimizations.
The concept of "debug" is nuanced enough that introducing a single #if debug build configuration test is insufficient for substantial set of community members who interacted in previous discussions and Swift developers who have sent me feedback outside this list.
Conditioning debug on Xcode debug/release schemes is a no-go.
Hidden helper functions already exist in Swift.
Members of the core team believe using build configurations is the wrong point to conditionalize code.

Joe Groff wrote, "We specifically avoided making debug/release an #if condition because we considered #if to be the wrong point at which to start conditionalizing code generation for assertions. Though the final executable image's behavior is unavoidably dependent on whether asserts are enabled, we didn't want the SIL for inlineable code to be, since that would mean libraries with inlineable code would need to ship three times the amount of serialized SIL to support the right behavior in -Onone, -O, and -Ounchecked builds. Instead, the standard library has some hidden helper functions, _isDebugAssertConfiguration, _isReleaseAssertConfiguration, and _isFastAssertConfiguration, which are guaranteed to be constant-folded away before final code generation."

My pitch: I want to promote these three helper functions to the standard library and remove their underscore prefixes. My primary use case is to limit logging and development-servicing functions (for example, statistical measurements) to "debug" builds. I believe a sufficient quorum of the community has similar needs that would be served by making these first class "listed" functions.

Doing so:

Eliminates the build configuration approach
Eliminates the need to define what "debug" means
Conditions configuration testing on assertion firing state not Xcode schemes or build flags (e.g. -D debug)
Uses already-existing global functions, requiring no coding

Thoughts?

-- E
p.s. I'd warmly welcome any third party assistance with the outstanding requests


(Jacob Bandes-Storch) #2

Am I understanding correctly that

    -Onone → _isDebugAssertConfiguration is true
    -O → _isReleaseAssertConfiguration is true
    -Ounchecked → _isFastAssertConfiguration is true

?

If the goal is to expose the optimization level to user code, might I
suggest something like

    enum OptimizationLevel {
        case none
        case standard
        case unchecked
    }

    var optimizationLevel: OptimizationLevel { get }

IMO, users may still want to control certain things with per-configuration
flags, but that's still achievable with -DX and #if X. Exposing the
optimization level makes sense to me.

···

On Sun, May 29, 2016 at 10:59 AM, Erica Sadun via swift-evolution < swift-evolution@swift.org> wrote:

Back in March, I somewhat foolishly agreed to pick up the gauntlet for a
series of community-requested proposals centered on build configurations.
Requested items included:

   - A way to test for destination platforms like Apple, Linux,
   Windows, Unix-like, UIKit-supporting, etc
   - A way to test for Simulator/Emulator vs Hardware targets
   - A way to test for debug builds
   - A way to test for platform conditions (bigendian, littlendian,
   bitwidth 32 and 64, objc-interop, see lib/Basic/LangOptions.cpp)

This splintered down into a series of draft proposals. The first adopted
proposal, SE-0075
<https://github.com/apple/swift-evolution/blob/master/proposals/0075-import-test.md> adds
a build configuration import test similar to Clang's __has_include. It lets
you test whether a module like UIKit is available, letting you customize
code for specific modules.

Next up on my list is debug-specific coding. Summarizing to date:

   - There's a general consensus that a debug state occurs when
   assertions can fire and are not disabled by compile-time optimizations.
   - The concept of "debug" is nuanced enough that introducing a single #if
   debug build configuration test is insufficient for substantial set of
   community members who interacted in previous discussions and Swift
   developers who have sent me feedback outside this list.
   - Conditioning debug on Xcode debug/release schemes is a no-go.
   - Hidden helper functions already exist in Swift.
   - Members of the core team believe using build configurations is the
   wrong point to conditionalize code.

Joe Groff wrote, "We specifically avoided making debug/release an #if
condition because we considered #if to be the wrong point at which to start
conditionalizing code generation for assertions. Though the final
executable image's behavior is unavoidably dependent on whether asserts are
enabled, we didn't want the SIL for inlineable code to be, since that would
mean libraries with inlineable code would need to ship three times the
amount of serialized SIL to support the right behavior in -Onone, -O, and
-Ounchecked builds. Instead, the standard library has some hidden helper
functions, _isDebugAssertConfiguration, _isReleaseAssertConfiguration, and
_isFastAssertConfiguration, which are guaranteed to be constant-folded away
before final code generation."

My pitch: I want to promote these three helper functions to the standard
library and remove their underscore prefixes. My primary use case is to
limit logging and development-servicing functions (for example, statistical
measurements) to "debug" builds. I believe a sufficient quorum of the
community has similar needs that would be served by making these first
class "listed" functions.

Doing so:

   - Eliminates the build configuration approach
   - Eliminates the need to define what "debug" means
   - Conditions configuration testing on assertion firing state not Xcode
   schemes or build flags (e.g. -D debug)
   - Uses already-existing global functions, requiring no coding

Thoughts?

-- E
p.s. I'd warmly welcome any third party assistance with the outstanding
requests

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brent Royal-Gordon) #3

My pitch: I want to promote these three helper functions to the standard library and remove their underscore prefixes.

These functions currently have implementations like this:

  @_transparent
  @warn_unused_result
  public // @testable
  func _isDebugAssertConfiguration() -> Bool {
    // The values for the assert_configuration call are:
    // 0: Debug
    // 1: Release
    // 2: Fast
    return Int32(Builtin.assert_configuration()) == 0
  }

I think how this works is:

* @_transparent makes sure these functions are always inlined at the call site.
* Most things in the standard library are *also* @_transparent.
* Therefore, after both (or more!) inlinings happen, you get the `Builtin.assert_configuration()` of the code calling into the standard library.

Needless to say, this is *extremely* weird and magical, and I'm skeptical of the idea that we should expose it as a normal function call.

I think a better design which would accurately convey its magic is to add a type to the standard library:

  enum BuildKind: Int32 { case debug, release, unchecked }

(Note: the names in this could use some bikeshedding. Put that aside.)

And then add a `#buildKind` compiler substitution which is equivalent to:

  BuildKind(rawValue: Int32(Builtin.assert_configuration()))

Now you can surround your debug-only code with `#buildKind == .debug`. Or you can capture the *call site's* build kind with a default parameter:

  func log(_ message: String, level: LogLevel = .info, buildKind: BuildKind = #buildKind)

Even the standard library might be able to do this if it wanted to, allowing your code to enable or disable asserts based on whether your caller's code is in debug mode or not:

  func assert(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = default, file: StaticString= #file, line: UInt = #line, buildKind: BuildKind = #buildKind)

(I wouldn't suggest that every stdlib member add such a default parameter; most should continue to rely on `@_transparent`. But I think that could be useful for calls like `assert()` and `precondition()`.

···

--
Brent Royal-Gordon
Architechies


(Erica Sadun) #4

Or, to be honest:

/// Offers user-facing public assert configuration test
@_transparent
public
func isDebugAssertConfiguration() -> Bool {
  return _isDebugAssertConfiguration()
}

which covers, I believe, about 98% of the demand for this feature

-- E

···

On May 31, 2016, at 11:21 PM, Brent Royal-Gordon <brent@architechies.com> wrote:

My pitch: I want to promote these three helper functions to the standard library and remove their underscore prefixes.

These functions currently have implementations like this:

  @_transparent
  @warn_unused_result
  public // @testable
  func _isDebugAssertConfiguration() -> Bool {
    // The values for the assert_configuration call are:
    // 0: Debug
    // 1: Release
    // 2: Fast
    return Int32(Builtin.assert_configuration()) == 0
  }

I think how this works is:

* @_transparent makes sure these functions are always inlined at the call site.
* Most things in the standard library are *also* @_transparent.
* Therefore, after both (or more!) inlinings happen, you get the `Builtin.assert_configuration()` of the code calling into the standard library.

Needless to say, this is *extremely* weird and magical, and I'm skeptical of the idea that we should expose it as a normal function call.

I think a better design which would accurately convey its magic is to add a type to the standard library:

  enum BuildKind: Int32 { case debug, release, unchecked }

(Note: the names in this could use some bikeshedding. Put that aside.)

And then add a `#buildKind` compiler substitution which is equivalent to:

  BuildKind(rawValue: Int32(Builtin.assert_configuration()))

Now you can surround your debug-only code with `#buildKind == .debug`. Or you can capture the *call site's* build kind with a default parameter:

  func log(_ message: String, level: LogLevel = .info, buildKind: BuildKind = #buildKind)

Even the standard library might be able to do this if it wanted to, allowing your code to enable or disable asserts based on whether your caller's code is in debug mode or not:

  func assert(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = default, file: StaticString= #file, line: UInt = #line, buildKind: BuildKind = #buildKind)

(I wouldn't suggest that every stdlib member add such a default parameter; most should continue to rely on `@_transparent`. But I think that could be useful for calls like `assert()` and `precondition()`.

--
Brent Royal-Gordon
Architechies


(Erica Sadun) #5

Following up to myself, now that I'm actually using this, I realize that avoiding using a build configuration test might not be the best idea from the developer experience side of things:

main.swift:7:9: warning: will never be executed
        print("Not debug assert configuration")
        ^
main.swift:4:8: note: condition always evaluates to true
    if isDebugAssertConfiguration() {
       ^
Testing debug assertion

Whereas going with if #if debugassert() would simply make code "disappear".

-- E

···

On Jun 1, 2016, at 9:15 AM, Erica Sadun <erica@ericasadun.com> wrote:

Or, to be honest:

/// Offers user-facing public assert configuration test
@_transparent
public
func isDebugAssertConfiguration() -> Bool {
  return _isDebugAssertConfiguration()
}

which covers, I believe, about 98% of the demand for this feature

-- E