SE-0196 — Compiler Diagnostic Directives

Alexander Momchilov brought up the idea of using TODO and warning as functions in the standard library with special compiler magic that will warn on their uses.
...
While these could be useful, I think #warning and #errorhave uses beyond just marking unfinished code that would be unwieldy or impossible with just an expression-oriented approach.

My proposal for TODO was in addition to #warning and #error. Warnings, errors, todos and fixmes are all widely adopted in the community (the Swift community, and the broader programming community), and I think that we should have first class support for them. This could allow programmers to customize how to handle todos and fixmes (e.g. ignore todos, make fixmes warnings. Or warn on todos, error on fixmes, etc.), in the same way we can get our compiler to ignore warnings, emit warnings, or promote warnings to errors.

From the proposal:

Erik Little refined that to instead use special directives warning and error in expression position, like:

let somethingSuspect = #warning("This is really the wrong function to call, but I'm being lazy", suspectFunction())

However, I think there's not much of a benefit to this syntax vs. just adding a warning above the line:

#warning "This is really the wrong function to call, but I'm being lazy"
let somethingSuspect = suspectFunction()

The first bit of code guarantees that the warning/error/todo/fix me is coupled to the default value. You can't remove the warning and forget to change the value and you can't can't the value and forget to change the warning. @anandabits Put it well:

Don't you think this could be better handled if warning/todo/fixme were expressions with parameters they passthrough:

return #warning(0, "Incomplete implementation: return number of row") // or `#todo`, `#fixme`

That way the default value and the message associated with it are more tightly coupled

Has there been any discussing into how to get a #warning to make the message appear at the call site, rather than immediately? In a similar fashion to #file, #line:

/*1*/ func printLineNumber(_ lineNumber: Int = #line) {
/*2*/  	print(lineNumber)
/*3*/ }
/*4*/ 
/*5*/ printLineNumber() // => 5, not 2

similarly:

/*1*/ func someCustomWarningHandler() {
/*2*/      #callSiteWarning("Uh oh!") // (crappy) strawman syntax
/*3*/ }
/*4*/ 
/*5*/ someCustomWarningHandler() // emit warning here, not on line 2

Runtime warnings imght be off topic, but I think we should pursue this (in this proposal or otherwise). Opening up the runtime warning system could be really useful

There is no real need for additional directives here, we can just overload the function like directive #warning(). Furthermore todo and fixme are just warnings as well.

// Bikeshedding
func #warning<T>(
  _ message: String,
  at mode: DiagnoseMode = .compiletime,
  value: T
) -> T {
  // emit warning
  return value
}

func #warning(_ message: String, at mode: DiagnoseMode = .compiletime) {
  #warning(message, at: mode, value: ())
}

#warning("message at compile time")
#warning("message at runtime", at: .runtime)
#warning("I'm lazy at compile time", value: 42)
#warning("I'm lazy at rumtime", at: .runtime, defaultValue: 42)

I don't think this should be assumed. I see great value in treating fixmes as errors, and todos as warnings. Others might disagree and want fixmes as warnings and todos as silent. I think we should accommodate that.

#todo and #fixme carry more semantic information than a #warning, the same way Date carries more semantic information than a String that encodes a date. Even if a #warning could be used to encode it, #todo and #fixme are a stronger type, per se.

1 Like

Any specific example in mind? It could only be me, but I don't see the difference between #fixme #todo and #warning except for the name.

The recommended way to do call-site / use-site warnings today is deprecation: @available(*, deprecated, message="Uh oh!"). It is a bit verbose, I admit, and some people have mentioned wanting to have a way to produce warnings that don't say "deprecated", but those are both improvements that should be independent of Harlan's proposal.

1 Like

@AlexanderM that could be accomplished if #warning and #error accepted file and line arguments in addition to the message argument.

// produces a warning with the specified `message` at the site specified by `file` and `line`
// and returns a value if specified, calling fatalError if no value was provided
func TODO<T>(_ value: T? = nil, _ message: StaticString? = nil, file: StaticString = #file, line: UInt = #line) -> T {
    #warning(message ?? "TODO", file: file, line: line)
    if let value = value {
        return value
    } else {
        fatalError()
    }
}

This is the kind of flexibility I would like to see. It allows us to define our own warning-producing functions. I'm not sure how feasible implementation of this exact mechanism would be, however.

1 Like

I'm very much in favour of adding #warning and #error directives, preferably with message in parenthesis. I'm intrigued by the idea of runtime warnings, not sure if errors should also be available in such mode. One concern I have though is the syntax, currently runtime directives use @directive, should we implement runtime warnings shouldn't they use @warning too?

I think I already outlined the purpose: Having #fixme and #todo as separate can allow the compiler's response to them to be customization using flags, similar to SWIFT_TREAT_WARNINGS_AS_ERRORS.

Furthermore, there are #warnings that aren't #TODO or #FIXME related. We should make a distinction between them, even if a programmer ultimately sets the compiler to treat all 3 as just warnings.

1 Like

Wouldn't that require some form of StaticInt? The line would have to guaranteed to be known at compile time.

@jrose I had forgotten that a message argument was available on @available. But it is odd for a function that isn't really deprecated to have to specify itself as deprecated. This also does not allow the warning message to be customized by the call site which also seems very useful.

I described the behavior I would like to see in a code in a previous post. I suspect that the exact mechanism I described may not be workable but I hope something similar might be possible. I think it is very relevant to the discussion of this proposal even if the result is that it ends up being an alternative or a future direction.

1 Like

I see your point there, but I still have the impression that this still would require tooling support for a different set of messages. Don't get me wrong, I'd love to see a set of fix-me's, todo's and warnings in Xcode sorted and differentiated whether those were at compile time or at runtime. However this makes this proposal much more complex and the probability of it's success is lowered. Furthermore it requires a full implementation as well.

I think we should keep it simple but keep the door open for more. ;) On the other hand if there is someone who can implement all that, then forget my concerns and go for it.

Yes, it would probably need something like that. That's why I hedged on the exact mechanism. I expect it would need to look a little bit different. My goal is to demonstrate the kind of function I would like to be able to write.

I'd have to look into it in more detail, but it doesn't seem that difficult
Here's what the proposal says about the behaviour of #warning and #error:

Upon parsing a error directive, the Swift compiler will emit the provided string literal as an error, pointing to the beginning of the string, and ignore the directive.

Upon parsing a warning directive, the Swift compiler will emit the provided string literal as a warning, pointing to the beginning of the string, and ignore the directive.

#todo aren't that much harder (same applied for #fixme):

Upon parsing a #todo directive, the Swift compiler will consult the compiler flag for the treatment of #todo, and either do nothing, or emit the provided string literal as a warning or as an error, pointing to the beginning of the string, and ignore the directive.

I vote yes. We've talked about this for years. This is the right direction for Swift, useful on both its own merits and widely used elsewhere. In terms of naming, both #warning and #error are simple and obvious.

The proposed syntax looks out of step with other build directives. It doesn't feel Swifty without parentheses. I see I am not alone in this preference.

With regard to feedback about #message, I see no reason to include both warnings and messages. The set of #warning and #error is sufficient in my mind, creating a minimal additive change. Unifying them into a single directive differentiating #emit("warning", "text") and #emit("error", "text") is roughly 500% uglier than #warning and #error, but it would allow future expansion to "message" if that direction would ever arise.

I am hesitant to co-mingle run-time and build-time uses of #warning. Something like nonfatalWarning(_ message: String = "#file:#line warning") or similar could parallel fatalError. (I'm not sure it would add value, but I'd like to throw this into the mix if run-time is a real concern.)

Please be clear about whether the directives should be leveled to permit or exclude suppression by compiler "hide warnings" options. For example, #warning("This must be implemented before deployment", ignoreWarningSuppression) or something like that would allow overriding the compiler flags for SPM and other source code sources.

4 Likes

This problem partly ties in with compile-time functions. It would be interesting if the method of denoting such functions were to prefix the name with #.

I'm in favour of adding this directives but as it has been mentioned already I would prefer the syntax to include parenthesis. Not only that but I would like to see it accepting file and line parameters, with automatic default values. That would allow people to define custom functions like TODO/FIXME that behave as they want. @anandabits TODO example is a good one.
This would give an expected default behaviour as we have in other languages but would add the flexibility of defining user level warnings without adding more burden on the compiler. This is usually done at the moment with some scripts but I feel that this solution would be much nicer.

What is your evaluation of the proposal?

Strong +1

Is the problem being addressed significant enough to warrant a change to Swift?

Yes. When it's needed, there is simply no good alternative other than build scripts such as XcodeIssueGenerator.

Does this proposal fit well with the feel and direction of Swift?

I think there is a need to really lay out what the future of Swift precompiled directives are going to look like, but this certainly matches what we currently have.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I currently use XcodeIssueGenerator to call out warnings and TODOs. This doesn't cover a library that conditionally creates a compiler error when it is used in the wrong context though.

1 Like