#How does Zig do stuff like this
1 messages · Page 1 of 1 (latest)
you dont, thankfully
fn rotl(x: anytype, n: usize) @TypeOf(x) {
return ((x) << (n)) | ((x) >> (8 * @sizeOf(x) - (n)));
}
// rotl is evaluated at compile-time
const a = rotl(7, 3);
You can also annotate the function with callconv(.Inline) to force inlining at all call sites
use inline fn rotl() please
for your sake, ill explain why this ROTL is awful. with c macro semantics
int val = ROTL(func(), 10);
translates to
int val = (((func()) << (func())) | ((func()) >> (8 * sizeof(func()) - (10))));
Yes, that's true, C's handling is far from impressive
This won't compile: @sizeOf only takes types and x << n won't work if x is anything but a comptime int.
inline fn rotl(x: anytype, n: anytype) @TypeOf(x) {
return (x << n) | (x >> (8*@sizeOf(@TypeOf(x)) - n));
}
It should work fine.
Just for clarity here: the answer to your original question is simply "it doesn't, you use a function". In C we sometimes don't like using functions for stuff like this because it can lead to actual functions in the binary (which is slower) and can prevent certain optimisations, especially in older compilers; in Zig we use sane defaults and have a good modern compiler, so functions work fine for stuff like this. If you want to be safe, you mark the function inline to tell it to be inlined at every call site, which means you'll get the same thing that an inline function in C theoretically does, which is the same as what a macro in C necessarily does.
The C preprocessor is quite powerful, but also very difficult to work with; you do lose a bit of its expressiveness in Zig (e.g. the ability to use macros at top-level to create a bunch of variables or whatever), but what you lose is basically hacky stuff that you probably shouldn't be doing in the first place
Well said, I dislike C's preprocessor.
In the past I used PL/I's preprocessor, that was much better designed than C's and rather a lot more powerful. It was effectively a small language that was executed at compile time, that language could manipulate source code.
Here's an example, it expands a loop into a series of statements:
%DECLARE I FIXED;
%DO I = 1 TO 10;
Z(I)=X(I)+Y(I);
%END;
%DEACTIVATE
the % denotes preprocessor statements
That loop is "executed" at compile time and the single assignment becomes a list of assignments for all values of I
Sure, that kind of macro exists and is in general both more flexible and (mostly) less horrific than C's, but Zig goes with its own approach. You can't modify the AST, which does limit some of what you can do, but the ability to execute arbitrary code at compile-time allows for a lot of things to be expressed in much more elegant ways than they might be able to with macros. In this case, we have a specific construct for unrolling a loop at compile-time, called inline while:
comptime var i = 1;
inline while (i <= 10) : (i += 1) {
z[i] = x[i] + y[i];
}
inline while does a kind of partial compile-time evaluation to unroll the loop at compile-time, but to allow runtime code within it
Does Zig do anything comparable?
Basically, there's no directly comparable feature, but most of the useful non-horrible stuff you can do with macros can be achieved in some other way with comptime
OK thanks
If you have any more specific examples you'd like to see translated to get a feel for it I'd be happy to oblige
Now a reason for my question, I read this article some weeks ago:
It was fascinating but having zero knowledge of Zig I wasn't really clear on what exactly he did
If you have the time, please read that, its highly complimentary of Zig and I wanted to understand exactly how the compile time stuff helped im
I've skimmed over it - were there any specific bits of code in it you wanted to expand on? Alternatively I can give a brief general overview of Zig's compile-time evaluation system
Well did Zig generate additional source code at compile time? did it somehow create the target specific initialization he speaks of, or is that not what he's talking about?
ahh it seems it does
inline for (cols) |x| {
x.port.pin_cnf[x.pin].modify(.{
.dir = .output,
.input = .disconnect,
});
he says of that "the inline for construct generates an unrolled loop at compile-time."
so that's somewhat analogous to that PL/I preprocessor stuff I mentioned.
If you're curious, see Example 3 on page 927 of this IBM manual
Okay, yeah, so let me give you an overview of this
Zig places importance on the concept of whether a given value is known at compile time ("comptime known"). When it's running over your code, you can annotate certain expressions and statements in ways that tell them to be partially or fully evaluated at compile-time.
In this case, we're interested in inline for. The way for loops themselves work in Zig is just that you loop over every element of an array or slice - for (vals) |x| in Zig is like for (auto x : vals) in C++. The difference with inline for is that the expression being looped over (in our case, cols) has to be comptime-known, and the compiler will in essence automatically duplicate the code inside the loop for every iteration the loop runs (because we know the value we're looping over at compile-time, we know how many iterations there will be and what the value is on each iteration). So the following two pieces of code are equivalent:
const vals = [3]u32{ 0, 42, 7 }; // because this is a 'const' variable whose initialization expression is comptime-known, its value will also be comptime-known
inline for (vals) |x| {
print(x);
}
// vs
print(0);
print(42);
print(7);
So in the case given, it expands into however many calls to ...modify, with the relevant values for x substituted into each one
sorry was away, just reading yr post now, much appreciated
@harsh veldt - Very much appreciated, that explains it very well.
So "inline" means "compile time"?
not "inline" in the old C sense?
It's sort of the same as in C, in that the contents are treated as if they were written in-place. It's just that Zig likes to reuse the syntax to apply to more than just functions.
OK thanks. Can an "inline" code block be called, executed at runtime or does the compiler complain if one tries?
anyway im readin this now
inline for and inline while work similarly to inline fn if you see each iteration of the loop as a function call. inline loops dont evaluate the entire loop at comptime, but they do evaluate the loop condition at comptime to unroll the loop – effectively inlining a loop body as if it were a function
inline's relation to comptime is only incidental. zig's inline entirely means "copy-paste instead of reusing". it just so happens that inlining loops requires that the loop conditional be evaluated at comptime. inlining functions has absolutely no relation to comptime.
THx
Its much closer to PL/I preprocessor than any other recent language. Although that language has largely faded away, it was hugely innovative at the time. Its lack of uptake is more due to politics and marketing than anything else.
This is pretty good documentation about it, starts on page 915 near the bottom.
It's done differently to Zig but they are rather close, code executed at compile time
Looking at this
fn multiply(a: i64, b: i64) i64 {
return a * b;
}
pub fn main() void {
const len = comptime multiply(4, 5);
const my_static_array: [len]u8 = undefined;
}
What happens at runtime, when a "comptime" prefixed statement is encountered?
In this case, multiply is run by the compiler, so that at runtime it's equivalent to len = 20
comptime forces the following expression to be evaluated at comptime. the resulting value is copy-pasted in its place.
Oh, OK , thanks, pretty neat!
Note that in this case, all arguments need to be known at comptime
so "comptime" prefixed code is not, cannnot, ever have any runtime behavior, it is - to all intents and purposes - a constant
note that although theres some automatic comptime, function calls from a runtime context are always runtime function calls unless you use the comptime keyword or the function returns a comptime-only type, such as type
correct
very nice, thx
a notable case of "automatic comptime" is when you branch on some comptime-known constant. this is used all over the place in conjunction with the compiler's lazy analysis. for example
const environment = if (@import("builtin").is_test) "production" else "test";
or more advanced
const environment = if (@import("builtin").is_test) @import("prod.zig") else @import("test.zig");
note that test.zig doesnt even need to exist if youre not building an executable in test mode
this is what powers all of zig's cross-platform apis. each function will branch on the comptime-known target os, libc-linkage, etc., to only compile code relevant for this specific compilation. if it tried to compile code that wasnt relevant, it would fail. how could you use inline assembly for x86_64 on aarch64?