Comptime questions

Hi,

I’m trying to understand more about comptime and test it’s limits. I know that what I “want” is not how it should be done in production, but I am curious on how the general mechanic works. I hope it is ok, if I use a C-Macro example to explain what I want:

Question1
If I use C-Macros I can use #ifdef to conditinally compile in code:

#define FOO
#ifdef FOO
int a = 42;
#endif
printf("%d\n", a);

This actually transforms the source code and if FOO is not defined the compilation will fail.
How to achieve this in zig? I tried something along those lines:

const foo = true;
if (comptime foo) {
const a: usize = 42;
}
std.debug.print("{d}\n", .{a});

I get an error during compilation because the variable a is not in scope anymore, as it was within the if-block. How can I create such a behavior (regardless whether it is a good idea).

Question2
Is it possible to define a string and create a variable name from it?
Something like:

#define NAME varname
int NAME = 3;

This will be transformed by the C-preprocessor to

int varname = 3;

I have no Idea how to tackle this in Zig. Maybe there exists some stdlib or builtin like

const name = "varname";
var @namefromstring(name) = 3;

Question 3
How can I build structs at compile time? All the examples I found seem to be from last year and out of date. I tried to tweak the stuff I found, but I do not know if I go into the right direction. Can someone please post a working example?

pub fn createtype() type {
    var fields: [2]std.builtin.Type.StructField = undefined;
    fields[0] = .{ //
        .name = "a",
        .type = bool,
        .default_value = null,
        .is_comptime = false,
        .alignment = 0,
    };  
    fields[1] = .{ //
        .name = "b",
        .type = i32,
        .default_value = null,
        .is_comptime = false,
        .alignment = 0,
    };  
        
    return @Type(.{
        .Struct = .{
            .layout = .Auto,
            .fields = fields,
            .decls = &.{},
            .is_tuple = false,
            .backing_integer = null,
        },  
    });     
}  
const mytype = createtype();
pub fn main() !void {    
    std.debug.print("{?}\n", .{mytype{ .a = true, .b = 42 }});
}   
  

Edit: Ok, with question three I was almost done. .Auto.auto and .fields takes an address, so .fields = &fields.

Thank you for your answers.

  1. That’s not possible without usingnamespace, so I would prefer solution like const a = if (foo) 42 else {}; a is void if foo is false thus it can’t be used. If you need to define some configuration using build-time specified options I believe you use build system for this, like this:
pub fn build(b: *std.Build) void {
  const target = b.standardTargetOptions(.{});
  const optimize = b.standardOptimizeOption(.{});

  const options = b.addOptions();
  const foo = b.option(bool, "foo", "Foo?") orelse false;
  if (foo) {
      options.addOption(comptime_int, "a", 42);
  } else {
      options.addOption(comptime_int, "b", 1337);
  }

  const exe = b.addExecutable(.{
      .name = "hello",
      .target = target,
      .optimize = optimize,
      // ...
  });
  // use @import("options") in your code to get access to `a' or `b'
  exe.root_module.addImport("options", options.createModule());
}
  1. Use you can do this using build system same way as in 1.

  2. Creating structs can be used to “solve” 1. and 2. but I find this somewhat ugly. As for creating there’s a little change in field naming after 0.13.0 so which version do you use?

1 Like

Thank you for your answer. While you were typing I solved question 3. I had to change .Auto to .auto, because the enum changed. and .fields had to get an address. I’m Using zig 0.13.

For question 1 and 2 I am a bit disapointed that there is no “easy” solution that somehow mimics the preprocessor, but I guess an easy solution would also be an easy footgun. I will play around with this and test what it can do.

Thanks again.

1 Like

For my part, I am very happy that Zig is not following preprocessor path :grin:
During my first week with Zig (coming from C++), I was confused by many of the choices it made, but I ended up appreciating each of them. I’m not very good at explaining stuff, but these strict-ish constraints are truly superior helpers when reading the code.

But if you really want to use preprocessor nothing stops you from @cInclude-ing .h file. (Just kidding :grin:)

3 Likes

I totally understand, and I am also quite happy about a lot of strict stuff that zig does that c does not. However, I am a big fan of testing the limits of languages and solving small puzzles in totally overengineered ways (as long as it never falls into the wrong hands). In C abusing the preprocessor is a lot of fun, also in Fortran I had a lot of fun searching for weird corner cases).

Coming from a C background, this is the key difference I’m finding with Zig.
Zig always seems to have a way (often exactly one) to do all the things I need. Conversely, it really won’t let me do the things which are bad.

Zig ends up being a bit more verbose - but it’s actually readable without holding loads of context in my head.

5 Likes

Just in case someone uses this thread in the future:

I found out that self made struct members can be referenced by their original string name with the @field builtin.