-
-
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: A definition of naked functions based on comptime
evaluation
#21415
Comments
cc @andrewrk @jacobly0 @mlugg @Rexicon226 in particular for feedback on this. Even if we decide not to go with this approach, I think we should at least adopt the |
Great write-up, I really like this idea.
And I'll just re-iterate my response to this from the Zulip:
I disagree with this idea. Currently
When applying these ideas to naked functions, we see that the first point doesn't exist since we cannot semantically call naked functions. I feel like the second point is implied from the naked calling convention, which must be explicitly written out of course, and since it doesn't provide the user with any additional information, would be redundant. |
It's possible we'll have to apply
I wrote the rule this way mostly for simplicity. It is of course true that a sufficiently smart compiler (tm) could analyze the assembly of the naked function and determine that some optimizations could be done while preserving semantics. I'm just not aware of any compilers that are even remotely sophisticated enough to do this, and it would be much harder to accurately describe what "preserving semantics" actually means in this context. I think that unless we can come up with any compilers that actually do this kind of optimization, we shouldn't complicate the definition of naked functions for this. Also, a use case that would break without this rule would be a naked function with a bunch of placeholder machine code that gets patched at run time. We might just say we don't care about such an esoteric case. I don't feel strongly about it, but it's something to consider. |
Gotcha, both responses make sense to me.
To be clear, this is the exact use case where you'd write |
Ok, yeah, I see what you're saying. I'm still concerned about the idea of |
Are you sure about that? As far as I can tell, GCC doesn't treat inline asm any different in naked vs "normal" functions. The difference is just that in GCC/Clang, asm without outputs is basically just assumed to be volatile AFAICT. Zig instead approaches this by making non-volatile asm without output constraints a compile error. To what extent and in what contexts optimization of inline asm should be allowed (and is even useful) within Zig is a question worth asking, but I don't think it's one that we need to answer here. AFAICT, no contemporary compiler attempts to optimize inline asm (I tried to find any references to LLVM doing this and came up dry; if you have any, please share!). The only noteworthy effect the In fact, not requiring the To me it seems pretty clear-cut that inline asm in naked functions should obey the typical Aside from that issue, I'm fairly happy with the state of this proposal. It allows you to do fancy comptime control flow to figure out which asm to include if you want, but ultimately gives us a bulletproof definition of naked functions, which is straightforward for backends to deal with, relatively easy to specify (it's just a third way of interpreting fn foo() callconv(.Naked) noreturn {
asm volatile ("ret");
unreachable;
} I can imagine someone doing this with the intention of satisfying the Also, it's a little weird that the I kind of feel like that last point is a symptom of the problem that we're trying to use functions -- which are really a part of the type system -- to model something which is fundamentally untyped. It almost makes me inclined to propose removing // @nakedFunction(comptime asm: []const u8) *const anyopaque
// return value is ptr to the function consisting of `asm`.
// essentially minor syntax sugar over global asm plus `@extern`.
const foo = @nakedFunction(
\\ret
);
// in fact, hell, why not instead use RLS to figure out the return
// type, so you can type it correctly straight away?
const bar: *const fn () callconv(.C) void = @nakedFunction(
\\ret
); I don't want to dedicate any time to thinking about this right now, so I'll leave it up to others here to figure out if there's anything there. There might well not be, I just figured it was worth throwing that idea into the wild. |
Huh, I guess I misremembered. 🤔 In that case, and given the other points, I guess I'm fine with leaving the
Note, though, that the proposal explicitly disallows An alternative design here would just be to allow output constraints, but special-case
Yeah, I think the solution there would just have to be a smarter diagnostic message.
Some immediate thoughts:
I'll think a bit about this and see if I can come up with something satisfying based on your idea. |
Huh? No, you can have output constraints which go straight to globals etc without having a result from the
Yep -- I think the syntax within I think that's pretty much a non-issue once we include the existing input/output constraint syntax; we just need to change |
... oh. The only examples we have in the language reference use result outputs, and I never took notice of this comment: zig/doc/langref/Assembly Syntax Explained.zig Lines 37 to 40 in 8b82a0e
so this whole time I assumed Zig's syntax only supports result outputs. But ok, yeah, I agree then. |
Having thought some more on this, I think removing With that in mind, I think it's worth considering just using plain old function syntax but with an extra modifier: pub asm fn add(a: i32, b: i32) callconv(.C) i32 {
asm volatile (
\\ leal (%%rdi, %%rsi), %%eax
\\ ret
);
}
|
Background & Motivation
Some context:
start
: Avoid string concatenation in inline asm. #21056inline
functions called from.Naked
functions cause stack allocations #21193As can be seen from these issues, we're having to come up with a growing list of language rules to make naked functions work reliably, and I worry that we're going to keep running into edge cases in both the language and compiler implementation that will need special handling for naked functions. These rules add extra complexity throughout Sema, and they're really just trying to contend with a rather simple reality: All compilers that implement GCC-style inline assembly and naked functions, including LLVM, consider
asm
statements to be the only well-defined contents of naked functions. This is a reasonable stance for a compiler to take because, in the absence of an ABI-compliant prologue and epilogue, there are very few constructs that can be lowered correctly. It also makes a lot more sense if you think of naked functions as just being a convenient language feature for emitting a black box of machine instructions in function form, which is how GCC/Clang define them.Rather than this growing list of language rules and the implementation complexity that come with them, I'd like to suggest what I think will be a simpler way of specifying and implementing naked functions. This proposal will make Zig's naked functions match the GCC/Clang definition and therefore also conform to LLVM's requirements.
(Note that some elements of this proposal are not unique to it; for example, the
asm
restrictions that I describe below will have to be adopted in some form even if this particular proposal is rejected.)Proposal
A function definition annotated with
callconv(.Naked)
is known as a naked function. A naked function must have an empty parameter list and must havenoreturn
as its return type. A naked function cannot be annotated withinline
,noinline
, orextern
. A naked function cannot be called directly, instead requiring a function type cast first, at which point a more detailed signature can be supplied. The compiler treats a naked function as a black box for the purposes of optimization and code generation; reachable calls to a naked function cannot be inlined, reordered, or elided.The compiler will perform basic scaffolding for a naked function, such as defining the symbol in the resulting object file, but it will not emit any machine instructions in the function body - not even the usual prologue and epilogue. The programmer is expected to provide the implementation by way of inline assembly. By virtue of the
noreturn
return type, it is considered safety-unchecked undefined behavior for control to reach the end of a naked function. (Note: This is unchecked because the panic handler cannot be invoked from a naked function. That said, compilers are encouraged to (and do) insert a single faulting instruction at the end of a naked function, making debugging a bit easier.)A naked function has its body
comptime
-evaluated during semantic analysis; that is, its body is implicitly acomptime { ... }
block. During this evaluation,asm
expressions are recorded rather than executed, and have some additional restrictions (see below). Besides this, all the usualcomptime
rules apply. Aftercomptime
evaluation is done, the function's body is fully replaced with the machine instructions resulting from the concatenation and assembly of the recordedasm
expressions, in lexical order. No further compiler transformations are performed on the function body. (Note: The semantics here are very similar to container-levelcomptime
blocks and the wayasm
expressions are treated there.)In a naked function, there are some additional restrictions for
asm
expressions:volatile
annotation is not permitted. (Note:asm volatile
is a meaningless concept in naked functions because of their black box nature.)comptime
-known values.X
ands
.asm
expressions in naked functions have no meaningful result value.)Open Questions
comptime
-ness of the function body as a result of just thecallconv(.Naked)
syntax, to make the semantics explicit, we could require that naked functions always contain just a singlecomptime
block.noinline
as a required annotation on naked functions to make explicit the fact that the compiler is not allowed to inline naked functions.asm volatile
in naked functions. It's easy to argue thatvolatile
should be required to make explicit the fact thatasm
expressions in a naked function can never be dropped by the compiler. On the other hand, forbiddingvolatile
is consistent with container-levelasm
. I don't feel particularly strongly about either approach; I just think we should either require or forbidvolatile
, rather than status quo where we apply theasm volatile
rules that are used in regular function context.The text was updated successfully, but these errors were encountered: