Compile-Time Constant Expressions for Swift

#62

This is why I was asking what you imagine the use cases for this feature would be. This is a very narrow, specialised attribute that I don't imagine will be used outside of the standard library and a few specialised modules (e.g. matrix and other math libraries, and Tensorflow for Swift, which the proposal author is working on). You seem to think it will be much more common than that, so I'm wondering why you think that.

Because you will try to use the result of f() in a context that requires compile time evaluation to work (e.g. it's part of a compile-time assertion or conditional statement, or it provides the length of a fixed size array). Why would you care otherwise if it's evaluated at compile-time or run-time if the semantics are the same? This is where I think the point of contention is. It seems you are thinking mostly of compiler optimisation, which is semi-related but can be (and already is) done without this feature and doesn't require an evolution proposal. My understanding is that it would help you more to think of a future “compile-time error when I try to multiply two matrices that don't have compatible dimensions” than “this program runs faster because some of it is evaluated at compile time”.

Resilience is obviously a focus for Swift, which isn't true of many languages. And most people can ignore @inlineable and similar because they're not writing a library that is performance-critical. They can similarly ignore @compilerEvaluable, at least as I understand it.

5 Likes
(Alexander Momchilov) #63

I think @compilerEvaluable is a grave mistake. It'll drastically hinder the utility of such a system, which is a shame given the effort that would be invested into it.

People writing public APIs will probably not generally consider the niche use case of "well, what if somebody eventually wants to evaluate this at compile time?". They won't mark their code as @compilerEvaluable, even if it happens to be. I can only imagine how many people will be forking projects to paste @compilerEvaluable everywhere for their own niche use. That's a crappy user experience.

I understand the argument about it setting up a contract as part of public API, but I think this is a better suited solution:

  • Any code can be called in a compiler evaluated context. Compile time errors are thrown if an unsupported operation is hit.
  • An attribute like @guarenteedCompilerEvaluable sets up a guarantee that whatever code is contained within can be evaluated at compile time. The author must work within that guarantee, or risk breaking API. Importantly: the absence of such an attribute does not prevent callers from calling this API in a compiler evaluated context. They're just "on their own" and subject to errors if the code isn't compiler evaluable.
(Joe Groff) #64

We could conceivably make a best effort to evaluate unannotated functions within the current module, but some sort of annotation is necessary at public API boundaries between modules, since we need to know to preserve the function bodies of compiler-evaluable functions in an interpretable form.

8 Likes
(TJ Usiyan) #65

This is a bit of a rephrasing of Joe's point but, without an explicit annotation, wouldn't it be unnecessarily fragile to use @compilerEvaluable code from third parties who aren't Apple?

If a third party makes an implementation change that 'breaks' the implicit @compilerEvaluable, you really don't have much visibility into that as a client.

(Howard Lovatt) #66

I don’t think the compiler deciding if it can evaluate something or not makes code fragile. The code still runs; an optimisation might be turned off, but it still runs.

Therefore @compilerEvaluable, @inlineable, etc. have no place in the public interface part of the ABI (they can be informative tags to save the compiler work - I have already checked these go ahead).

(Howard Lovatt) #67

No objection to the ABI containing a tag that says the compiler has checked this code and it is inlineable, compiler evaluable, etc. and here is the data structure the compiler needs to inline, evaluate, etc.

I think this is a much better ABI design and much better programming experience.

(Alexander Momchilov) #68

Yeah, but that beats the alternative: you could never do it at all, unless the author considers this niche use-case. The error would be compile time, so it's not exactly gotta hit you by surprise

(Joe Groff) #69

Module developers normally have an expectation that they can change the details of a function's implementation in newer versions of a module. This becomes harder with @inlinable and @compilerEvaluable, since client code can now import and copy or interpret the exact implementation of older versions of the module. In general Swift tries to prevent public API from making unintentional promises beyond what an API designer explicitly made, in order to maximize flexibility for the implementer to evolve their implementation. It would not be acceptable for upgrading a new, otherwise ABI- and API-compatible version of a library to suddenly cause its clients to stop compiling. I sympathize with your desire to avoid annotation burden and maintain compile-time flexibility, and it seems reasonable to me to make a best effort to evaluate local functions in a module without explicit annotation, but for public API, being compiler-evaluable has to be a conscious decision by the API designer.

6 Likes
(Alexander Momchilov) #70

It would not be acceptable for upgrading a new, otherwise ABI- and API-compatible version of a library to suddenly cause its clients to stop compiling.

That's up to the library author to decide. If users took advantage of non-guaranteed APIs that happened to be compiler evaluable, then that risk is on them, and the author would be justified to break their build. Perhaps there could be the opposite attribute to warn users not to use it, like "@CompilerEvaluabilityNotGuaranteed".

The alternative, that you cannot call APIs by an author who hadn't considered this (niche) use case, would limit your choice of libraries so much, in a way that is just absolutely crippling to the utility of this system.

I'll defer to Python's pragmatic mantra: "We're all adults here"

(Howard Lovatt) #71

It is very much the focus of Java and they leave this up to the compiler :frowning:.

I would argue that optimisation hints etc have no place in the all-future-versions of this function/property/etc will retain this feature section of the ABI.

For example, suppose you thought you could make the seed for a hash a constant, then latter you wanted a new hash seed to prevent DOS attacks. You would be stuck if you had marked your function compiler evaluable.

(Howard Lovatt) #72

This is like the extensible enum argument. The only ABIs that can be changed from under you are Apple ones. Therefore limit @inlinable, @compilerEvaluable, etc to Apple like @frozen is for enums.

(Howard Lovatt) #73

But I think it will spread. I am using your library and want to mark my function compiler evaluable and I can’t because you haven’t marked yours as compiler evaluable therefore I hassle you for a change and the virus spreads.

See other post, happy for this to be an Apple only annnotation like frozen is for enum.

(Joe Groff) #74

I agree that there's a separation between "binary-stable" and "source-only" API that's valuable, since there's a lot of nitpicking public imposes on you that isn't of interest to most small libraries that are distributed as source and vendored by their clients. Today, that distinction doesn't formally exist in the language yet (aside from the special handling of enums). It'd be a worthwhile idea to develop, since we could be a lot less ornery and more permissive with public things at source-only module boundaries, but we do need to support the binary-stable case too ("only Apple does this" is only true for Apple platforms, and only true in the near term, since third-party binary frameworks will eventually be supported), and it's easier to relax restrictions than try to impose them later.

2 Likes
(Howard Lovatt) #75

There is a distinct difference with Apple/Foundation stuff though; it gets changed from under you. Whereas you choose which version of a third party library you wish to use.

(Joe Groff) #76

Even in the source-only or third-party binary cases, though, you have the same resilience issues once you have more than one layer of transitive dependencies—if you use library A, which depends on version 1.1 of library B, but you need something from version 1.2 of library B, you can't unless 1.2 is compatible with library A's use of B. Your dependencies can still "get changed from under you" by other dependencies demanding newer versions of them.

3 Likes
(Lukas Stabe 🙃) #77

If I took some Swift library, built it into a shared object and packaged that for some Linux distribution, that could get changed from under you just as well as "Apple/Foundation stuff". There really is no distinction besides the fact that few people outside of Apple currently want to distribute Swift libraries like that, but with adoption of Swift on other platforms that will definitely change.

5 Likes
(Howard Lovatt) #78

But if library 1.2 changes the inline code, still inlineable but different code, then you can’t use version 1.2 because other parts of your code have the old 1.1 inline. In fact it is worse, inlineable gives a false sense of security.

These issues are best tackled with a module system. Which I agree is hard. Therefore suggest inlineable, compiler evaluable, etc not part of ABI until then.

(Félix Fischer) #79

But if there's any point at which to make the case for a breaking change, it's for a security patch. I don't see the problem with this use-case, but maybe I'm reading it wrong.

1 Like
(Tino) #80

I think in "same module" context, it would really be more convenient if @compilerEvaluable (although I'd be happy if someone finds a better name) wasn't mandatory. It's also quite common that the rules are a bit more relaxed here (no open needed for override).

As soon as module borders are crossed, I think it get's so much more tricky that it might make sense to split that into a later proposal.
Besides scenarios with code that attacks the machine running the compiler, all functions that cache or store data in the filesystem might cause strange side effects.

2 Likes
#81

I must admit that I'm still very confused by the responses here. Nobody has said why they think this will be a very commonly used attribute, and what they're going to use it for, but they're all concerned about the burden of writing it everywhere.

This is compiler optimisation again, which doesn't need an evolution proposal and is already done in the shipping compiler. If you're happy for code to run at either compile-time or run-time then great, you never need to use any attribute (except maybe @inlineable and similar to enable cross-module optimisation). If the code in question can generate a compile-time error though, then you care greatly about when it is evaluated (if vs #if, assert vs #assert).

You have the same problem if the compiler had just implicitly decided that the seed for the hash was compiler evaluable in any case that you would have a problem with the explicit annotation. Though I think I must be misunderstanding this because you call it an “optimisation hint” again, while I've been trying to explain that I don't think that's the right way to think of it. If you just want the compiler to optimise things then great news: firstly it already does, and secondly you can file bug reports to improve optimisations instead of writing evolution proposals. In other words, not tagging your code with @compilerEvaluable does not mean that it won't be evaluated by the compiler. It is implication (if it's @compilerEvaluable then it will be evaluated at compile-time) not iff (it will be evaluated at compile-time if and only if it's @compilerEvaluable).

I personally see this as mostly only useful in the library context (standard library and a few specialised math libraries), so separating out the cross-module part into a separate proposal doesn't seem very useful. Within a module you're already free to inline and evaluate at compile-time whatever you want to for optimisation reasons, and the remaining uses (e.g. compile-time asserts) seem likely to mostly involve standard library types and functions that will already have been tagged. Though perhaps everyone is just making a narrower point here, that if a function in your module is used in a context where compiler evaluation is required (e.g. a compile time assert), then the compiler should be aggressive about trying to optimise it to a constant (e.g. forcing inlining) even if it's not tagged. That seems fine to me, though additive.

6 Likes