#How does Zig do stuff like this

1 messages · Page 1 of 1 (latest)

strong ice
#

C lets us do this:

#define ROTL(X, N) (((X) << (N)) | ((X) >> (8 * sizeof(X) - (N))))

That text 'ROTL' in the source gets replaced by that expansion, before compilation.

How does Zig's compile time feature do that kind of thing?

#

How does Zig do stuff like this

keen plover
#

you dont, thankfully

sonic dove
#
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

gray crown
#

use inline fn rotl() please

keen plover
#

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))));
strong ice
#

Yes, that's true, C's handling is far from impressive

thorn glacier
strong ice
#

But how would this get handled:

#
a = rotl(x, y);
thorn glacier
#

It should work fine.

harsh veldt
#

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

strong ice
#

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

harsh veldt
#

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

strong ice
#

Does Zig do anything comparable?

harsh veldt
#

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

strong ice
#

OK thanks

harsh veldt
#

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

strong ice
#

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

harsh veldt
#

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

strong ice
#

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

harsh veldt
#

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

strong ice
#

sorry was away, just reading yr post now, much appreciated

strong ice
#

@harsh veldt - Very much appreciated, that explains it very well.

#

So "inline" means "compile time"?

#

not "inline" in the old C sense?

strong veldt
#

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.

strong ice
#

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

keen plover
#

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

keen plover
strong ice
#

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

strong ice
#

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?

strong veldt
#

In this case, multiply is run by the compiler, so that at runtime it's equivalent to len = 20

keen plover
#

comptime forces the following expression to be evaluated at comptime. the resulting value is copy-pasted in its place.

strong ice
#

Oh, OK , thanks, pretty neat!

strong veldt
#

Note that in this case, all arguments need to be known at comptime

strong ice
#

so "comptime" prefixed code is not, cannnot, ever have any runtime behavior, it is - to all intents and purposes - a constant

keen plover
#

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

strong ice
#

very nice, thx

keen plover
#

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?