-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Pragmas #5239
Comments
Whats wrong with keywords? |
Nothing -- but, every time we add a new one, it's a breaking change. This slows down iteration on the language. |
By the way |
Ok, I'll get rid of that then. |
I like the symmetry of this proposal. The language already has builtin functions, but it's missing builtin properties, which instead pollute the global namespace in a somewhat ad-hoc fashion. I would argue for a slightly different syntax, though: @{runtimeSafety=true, optimization=.fast}
@{comptime} // same as @{comptime=true} |
Another example where this could be useful is #5177. I think a pragma/property |
You're showing symptoms related to Dlang's increasing complexity with adding much of a visual pollution for still hypothetical benefits. |
The way I see it, this proposal does in fact add quite a lot of power and flexibility. Since Zig aims to be a low-level and all-purpose language, it needs to handle lots and lots of corner-cases that cannot be implemented in user-land without compiler support. Examples: specifying calling conventions, layouts, alignments; making use of machine-specific features, including parallelism; type introspection, etc. Some of these features might deserve their own keywords or special syntax, but most are too obscure for that. Thus, it is good to have some systematic way to introduce such compiler features without disturbing the main language. Otherwise, every proposal for a fairly rare, but occasionally necessary feature is liable to trigger a lengthy discussion about how it can be integrated into the language without messing up the existing syntax and imposing cognitive overload on programmers. In many cases the conclusion will inevitably be that it cannot be easily done and it's just not worth it. This is, the way I see it, the main reason why Zig has builtins (of which there are already more than 100). It makes the language less reluctant to correctly handle corner cases. This proposal merely extends the concept of a builtin to include not only functions but also properties.
Visual noise is a valid concern. E.g., personally I dislike Zig's decision to dispense with top-level declarations in favor of |
@zzyxyzz @kenaryn |
This is a really good point. There are a lot of small but important use cases that must be handled in the compiler but don't deserve their own keyword. All major C/C++ compilers, rust, nim, and even go all have solutions to this problem. Zig's builtins get us most of the way there, but can only handle attributes that occur in a place where an expression makes sense. C++ gets around this problem in two ways:
Going through the list of supported annotations in clang, most of them are already supported in Zig:
The ones that are currently unsupported are:
Of the ones we don't have, some of these are things that we should probably support for certain platforms. They definitely don't deserve keywords though. So I think this qualifies as a motivating example for why something like this is necessary. But this proposal raises a really big question: Where would we draw the line between keywords and attributes? Should It's also really important that these properties can be controlled by comptime-known conditions. A lot of these settings may change based on build options or for different platforms. This is the sort of thing that you would usually control with
This feels unnecessary to me. Zig doesn't have macros, so you won't end up accidentally putting two of these together. Anything that goes in an expression context should remain a builtin expression. Those don't pollute the namespace so there's no reason to change it. Existing builtins that modify their enclosing scope should be changed to be attributes placed either before the open brace of the scope or as the first token after it. (only one of these should be allowed, I'm just indecisive about which).
(sorry, nitpick) Pragma doesn't actually really mean anything, aside from being used in C++. Apparently it's short for pragmatic information, which I guess makes sense kind of, but arguably that's what 100% of the code should be. I'd go for something like 'Attributes' instead. |
@zzyxyzz
@EleanorNB
I think I'm failing to see how this doesn't disturb the language, nor how this syntax is any more powerful / concise than what is currently in place. I can see how this "unifies" some keywords and builtins in a sense, but is that entirely needed? My main issue with this proposal is that it's adding symbols in places that it doesn't really need to, like
I don't really see it? You're doing the exact same thing as before, except this time it's within curly braces. If your stance is that the language is being bloated by keywords and builtins, I don't think this is solving that issue. It's merely taking what we have and changing the syntax to something that is less clear. And along with this system, we'd also need to keep keywords (for obvious reasons) and I'd assume we'd need builtins still for the few compiler functions that don't fit within this model. |
The difference is previously language-level changes can now be done without potentially breaking existing code or increasing syntax complexity because they're in their own namespace. The point isn't to "unify" things, it's to isolate them -- to decouple compiler magic from userspace. I'm not saying that all keywords and builtins should become pragmas. I like keywords and builtins. Keywords as declarations are good. Builtins as functions are good. Just some keywords don't deserve to be globally defined (async var? packed fn?), and some builtins don't make sense as functions (set eval branch quota?! Set align stack?! Why?! There's no control flow here, even at comptime!). Current solutions are either shoehorns or overly destructive. This provides a better model. Also, @SpexGuy, comptime configuration can still be done with e.g. |
I see your point here. I think the actual heuristic I'm following is this: Keywords and symbols are allowed to affect language semantics. Attributes can affect codegen but cannot modify semantics. Looking over the entire list of things that you can tell clang that you can't currently specify in Zig, not a single one of them will change whether or not a program compiles (except arguably runtime cpu feature dispatch, but that one would need a major facelift anyway in order to make it into zig). Under this rule, pretty much everything that's currently a keyword should remain a keyword. Runtime safety settings, float modes, and cold paths would change to attributes (though the execution definition of One of the amazing things about Zig to me is how it's implemented this huge set of compiler-specific attributes using a much smaller and simpler set of keywords and expression builtins alongside comptime execution. Making it easier to add keywords is great if your goal is to add more keywords, but the thing I love about Zig is that it's been forced not to do that. It's distilled a large number of concepts into a small number of primitives that mesh together in ways that are numerous but still predictable. Avoiding introducing new keywords is aligned with the goal of keeping the language semantics simple. This restricted definition of attributes would allow for specialized code generation hints when necessary, without enabling lots of new keywords that make the language (the concepts, not the syntax) more complicated. The goal as I understand it is for Zig's semantics to be pretty much final by 1.0. Any changes after that are necessarily breaking, and should be very rare. This syntax would allow us to introduce niche compiler hints in minor revisions without breaking code, and would allow the parser to be forwards-compatible with those changes. But overall, Zig isn't going to be an evolving language like C++ or rust. It's going to be done and mostly static and stable, like C. If I'm wrong about this, please let me know.
This is a slippery slope. At some point, the entire language is "compiler magic". Why does
But the others I agree with. Things like |
That's pretty much the opposite of the heuristic I'm using. To me, modifying semantics is the whole point of attributes. That's why I proposed a rigid tuple syntax as opposed to a more flexible struct syntax -- so that, like keywords, users can't go insane with remote configuration. When you're declaring a thing, as opposed to a property of a thing, or you're defining unique syntax, or you're affecting types, or you're doing something unique, that's when you use a keyword. Basically, if the answer to the question "What's this?" is unique (not "it's an attribute" or "it's a compiler hint"), it's a use case for a keyword. I wouldn't mind keywords for the other things as well, but you need to know all of them to know which names to avoid in your own code. That goes against point 9 of the Zen, and it's the reason builtins have As for compiler magic -- yes, every language is made of compiler magic. One of the main points of Zig, though, is the strict separation between language and userspace -- you can't define a function that looks like a builtin, and there are no builtins that look like functions you can define; you can't define the format of a declaration, and the language can't tell you what to declare. Keywords violate this principle by using the same namespace as user definitions, and of course there's a place for them, but I don't think we should be so flippant about their presence. You could just as easily argue that keywords are a slippery slope -- want a feature? Use a keyword! Have keywords for everything! Memorise 50 pages of keywords to know what you can't declare! (See how easy it is to misrepresent a position?) Also, implementing new attributes won't change the semantics of existing code. The only way they can possibly break things that worked before is if they reserve a name that was being used, which this proposal makes impossible. And it's awfully bold of you to assume we'll get everything right the first time, or that nothing will change in computing for the entire lifespan of Zig. (Or are you suggesting that things shouldn't change because we have them right already, or that Zig should be limited to particular domains should the field expand? There's really no charitable way to interpret what you said.) I was not aware of |
Taking cues from #5177, I've added a way for all fields to be configurable, but not so easy that users will do it without thinking. |
I'm sorry if I've misrepresented your position. That wasn't my intent. I'm having a hard time understanding exactly how to codify your proposed rule, and I was trying to show that the rule of "compiler magic should use
This is a fair point, and if the proposal was to unilaterally remove all reserved words or to put some token on either all reserved words or all variable names I would understand how this simplifies the language. But the fact that it keeps some keywords and removes others in a way that I find hard to predict is why I'm not sold on it. I believe that what follows is the complete list of reserved words in Zig. Some have multiple uses and appear multiple times in the list. Maybe picking exactly which ones would be changed to pragmas (and why/why not) and specifying exactly where pragmas are allowed to appear will help us to come upon clearer rules, and will make for a more concrete proposal. Type Declaration Builtin Identifiers Modifiers Control Flow
This syntax leaves a lot of questions. Does
That's rather harsh. I had some motivation for saying what I did, and I felt that it was a good idea to do so. I understand if you disagree, and I'm not certain that this is the official stance of the Zig project, but surely you must see that there are benefits to stability, especially in something like a language. It's the whole reason Zig builds static binaries by default instead of linking DLLs. This isn't something I said off the cuff or without significant thought and consideration of the trade-offs involved. I'll go into some more detail about it. C is mostly static and stable, but not entirely. It's changed a little bit in the last 20 years, but not much. C11 introduced alignment, atomics, noreturn, and a few other things. C18 managed to avoid making any semantic or syntax changes. I really believe that it's possible for a language's semantics to be stable in the long term, as long as they are sufficiently complete at launch. It may need to add some builtins or hints as technology evolves, but it would take a major technological revolution to require changing the semantics of the language. If new assembly operations are introduced, or new compiler optimizations, Zig will be able to handle them with builtins or hints that don't change semantics. Programming methodologies may evolve, but Zig probably isn't going to adapt to meet them. Zig's simplicity is derived from it being targeted towards a certain style of programming, and I don't think that's going to change. If a new programming paradigm is introduced that sweeps the world but is inconvenient to use in Zig, then people who want to program that way may have to switch to a different language. This is consistent with the design decisions we've made on operator overloading, inheritance, and encapsulation. If properties of hardware change so dramatically that Zig's semantics aren't valid, then it will invalidate the underlying assumptions of the Zig language and people should use a more specialized language for that hardware. If we find that there is some major flaw in Zig's semantics that must be addressed, then that will be fixed in a major breaking revision of the language. But we have a rollover plan if this has to happen. Keywords are allowed to change in this case, and that isn't a problem. The bottom line is, the amount of control you have over the output machine code in Zig is on par with C. Assuming the compiler supports the necessary targets and has the necessary compiler hints and builtins, Zig will be unusable for a computing profession on the same day as C99 is. Zig avoids the long tail that C++ and Rust are travelling down of fixing all the little corner cases of ownership and slowly improving templates, by not having ownership semantics in the language and using the same semantics for templates and runtime. Because Zig avoids all that complexity, it should be possible to ensure that Zig's semantic set is sufficiently complete.
Zig 1.0.0 isn't the first try. It's not what we roll over to after 0.9.0, it doesn't have a set deadline, and we can postpone it until it's ready. I think we can get everything (or close to everything) right at 1.0 because we decide exactly when 1.0 happens. I think it's possible for a simple language like Zig to ship with a complete semantic set that doesn't require tweaks, except to support new hardware or optimizer hints. 1.0 is the label we put on the release when we are satisfied that the language semantics are complete for all current hardware and all current use cases, and it's time to stop changing the language and start optimizing the compiler. The current release cycle allows us to try things and review ideas and push the boundaries of the semantics to figure out where they are inadequate. We may make mistakes with 1.0, and we have a fallback plan if we do, but if there are major problems with the syntax or major improvements to be made then the mistake was in failing to do our due diligence before labeling the release 1.0. |
Tbh, my idea was for the rule to be a bit flexible. Ergonomics is a top priority as well, after all, just after simplicity. However, I can now see the confusion that an inconsistent rule could cause, so I've amended the proposal. The proposal is not to remove all keywords, that would be impractical and overly verbose. Just to remove as many as makes sense -- to leave only those such that the language can be considered "complete", in some sense. Admittedly, there is still some intuition involved, but that's inevitable in design (why are Perhaps a better rule would be: if a keyword can be removed and leave the program in a consistent state (strictly regarding types and syntax), and preserve number and order of statements, it should be a pragma. So of your list, the following would become pragmas: Type Declaration Builtin Identifiers Modifiers
Control Flow
Everything else, I agree, we should be able to get that absolutely rock-solid and stable by 1.0.0. We're just about there already, even. Under this proposal, I understand what you were trying to say, and it is definitely a worthwhile goal, and definitely more attainable than it would be in any other language, I just don't think we should count on it. We don't want to have to bump the major version every time there's a use case for another keyword -- that's how valid features get delayed until enough other deficiencies are accumulated to justify another version, or if we don't go that route, how we rapidly reach version 50.0.0. Yes, maybe that's catastrophising, but the only unrecoverable mistake in software is failure to plan for the future. (Also, when I say "new features", I don't mean new language features. I agree it would be against our principles to chase after every shiny new methodology, and in fact the rule proposed above for pragma inclusion would not allow that. I mean new machine features. We're supposed to be a language for the next 50 years, and as long as new machines are built according to von Neumann architecture, our assumptions will be valid -- but new optimisations and new architecture details could emerge in that time, and if we can adapt to them, we should. Maybe our biggest killer feature over C, the thing we can do that no one else has been able to yet, is staying power. Programs written today could be compiled for machines built 200 years from now. Aiming high? Sure. So, in the words of our benevolent dictator, let's channel our "I'll fucking show them" energy.) Also, yes, I've been harsh. Sorry, my biggest trigger is not being heard, and having my words misinterpreted can set that off, even when the fault is mine for not communicating clearly. I'm working on that. |
I'm in favor of this, and I think it's indeed a better version of #4285 just as claimed. The syntax looks cleaner while being just as expressive. Pros:
Cons:
While these cons are not trivial, I think the pros listed above are valid as well, and there are still features under consideration that will require new keywords or something like pragmas:
Those listed above are concrete proposals. Could easily imagine others:
Workarounds for the cons:
Leaving some more hypothetical "workarounds" for discussion:
TLDR: Trade-off between improved expressiveness/scaling/flexibility and reduced ergonomics/readability/consistency. |
I would like to propose a slightly different syntax, introduce a special keyword (instead of a sigil as in the original proposal) to designate a pragma (although i prefer the term 'attribute', which seems more in line with the equivalent way to configure such features in C/C++). This keyword would simply accept a struct literal with configuration options for the entity that it is applied on. For example: fn a() attribute(.{.linksection = .text}) void {
}
const A = struct attribute(.{.packed = true}) {
}; I believe this has a few improvements over the proposed syntax:
I can still see a few drawbacks to this syntax, though:
I also have some generic opinions and remarks for pragmas/attributes:
|
Just out of curiosity, based on this list, how many non-mutually exclusive pragmas may a function have at most? |
Under status quo, how many keywords can annotate a function at most? That many. |
On further consideration, this would encourage feature creep and language heterogeneity. We have formatting tools, we don't necessarily need to maintain legacy to infinity -- and as long as we provide good enough facilities to interface directly with machine code (cough cough #5241 cough cough), this has no advantages. |
Actually, there is a case for this: independent language extensions. Say someone wants to create a graphics API akin to CUDA/OpenCL, but using Zig instead of C/C++ -- adding all the necessary features to Zig proper would be impractical and bloating, but implementing them in language-space in the fork would be laughably incompatible. We may define this as a standard sequence, and either ignore or error on it in the base implementation, giving extension implementors free reign. This may be construed as encouraging gratuitous forks, creating confusion around compiler versions -- however, the way I see it, people who want something unique from the language will fork it, and unless we make it easier for these forks to track upstream, they'll diverge. |
As pointed out by Felix on Discord, if it doesn't make sense to extend Zig proper, it will make more sense to create a completely different language. We want to encourage the right tool for the job -- either this might be Zig, or it definitely is not. There is no in between, and we don't want to fool people into thinking there is. |
In the Nim sense. This'll be especially helpful when Zag comes in. Loosely inspired by #4285, but with the problems removed.
(N.B.: I am bad at commitment. Assume this is a living document.)
A pragma is a special kind of tuple, written with at-braces:
@{}
(combination of "hey, compiler, look over here" and "there's a bunch of stuff in here"). Each field has special significance to the compiler -- fields cannot be user-defined. Pragmas express properties of language constructs -- wherever there's a use case for a keyword that only makes sense within the context of a specific construct, a pragma can be used instead:Thanks to their unique syntax, pragmas can be placed flexibly within declarations, are always optional, and can even be served in pieces (supplying the same pragmus twice to one construct is a compile error). Pragmas can only ever be used as literals -- only those fields that are explicitly exposed are configurable. This way, no more magic is possible than in status quo.
With this feature, we'll be able to extend the language with little to no friction, but arbitrary users will not -- the best of both worlds.
The text was updated successfully, but these errors were encountered: