Inline/comptime: weird results

Finally managed to get this to work, but it feels like inline should not be necessary here: ghext/src/ghext.zig at 111d78439b0d3ce885a5205ace1933e91724334f · charlesrocket/ghext · GitHub

without inline I am getting cast errors or unresolved comptime

src/ghext.zig:216:21: error: expected type '*ghext', found '*const ghext'
    const head = ghx.hash(HashLen.Short, Worktree.Unchecked);
                 ~~~^~~~~
src/ghext.zig:216:21: note: cast discards const qualifier
src/ghext.zig:142:19: note: parameter type declared here
pub fn hash(self: *Ghext, comptime length: HashLen, check: Worktree) []const u8 {
                  ^~~~~~

If inline is removed, only one (hash_short) test is passing.

Since the hash function does not modify self you can fix the error by changing pub fn hash(self: *Ghext,... to pub fn hash(self: *const Ghext,...

Nope, it only works with inline. If I drop it and enforce const:

src/ghext.zig:155:17: error: unable to resolve comptime value
                head ++ "-unverified"
                ^~~~
src/ghext.zig:155:17: note: slice being concatenated must be comptime-known

The ++ concatenation operation concatenates arrays. You are attempting to use it to concatenate slices and return a slice, which does not work. Even in the Short case where your slices have comptime length, where will the memory for the concatenated slices live? The only reason it is currently working with inline is because you are calling the function with Unchecked and thus the concatenation portion is not being evaluted. If you call the current inline version with Checked then you get the same compile error.

Checked also works, but only hash_short is passing. ++ / ** works for both slices and arrays.

Added more tests (Checked + Unchecked) and now getting unresolved comptime with inline as well :face_with_head_bandage:

I suspect that hash_short is passing because self.head[0..7] coerces to *const [7]u8 so the compiler knows how much memory head ++ "..." will take up at comptime. It can’t know that when the length of self.head is runtime-known.

As permutationlock has already said, you can’t use ++ on a slice with runtime known length because where would you store it? The compiler would have to reserve std.math.maxInt(usize) + "...".len bytes for your concatenation to cover every possible length and that isn’t possible/feasible.

I’m surprised that this code works with inline probably there’s some lazy compilation magic going on there.

Generally you can use std.mem.concat() instead of ++ to make this work, of course this requires an allocator. But since you’re dealing with commit hashes here which have a fixed length you can also just use a std.BoundedArray or something similiar.

self.head is also comptime. But I guess I am introducing a mutation somewhere down the line.
The idea is to pull a commit hash to append to a version string, so everything must be comptime. Ill check the BoundedArray, thanks! Tho, the issue is still quite confusing because it should never work as far as I can tell. But it does, in some cases.

How is self.head comptime? You’re getting it from a child process at runtime or am I missing something?

When I mark it as comptime compiler complains about the redundancy. Maybe I pay too much attention to the compiler complaints. The init function uses a var to return a comptime struct, or at least thats the idea (const head = try arr.toOwnedSlice();). readWithGit() should be done in comptime and it worked so far. Maybe theres something else going on there.

I am surprised that you can concatenate slices with runtime contents and comptime length. You have to be very careful where you use that because it is “silently” using a single location in static memory (or maybe its on the stack and this illegal/undefined behavior?).

const std = @import("std");

fn foo(slice: []const u8) []const u8 {
    return slice[0..8] ++ " \\";
}

test {
    const str = "hello this is a string";
    const foo_res = foo(str);
    try std.testing.expectEqualSlices(u8, "hello th \\", foo_res);
    const foo_res2 = foo("I hope this doesn't overwrite my first string");
    try std.testing.expectEqualSlices(u8, "I hope t \\", foo_res2, );
    try std.testing.expectEqualSlices(u8, "hello th \\", foo_res);
}

Yea I am kinda lost too) For some reason everything worked like a clock until I decided to handle both long and short hashes via hash().

Where exactly does the compiler complain about that? As soon as you use an allocator a value cannot be comptime anymore (unless your allocator works at comptime as well which std.testing.allocator does not) because the memory allocated by the allocator comes from the OS at runtime and cannot be known to the compiler. As far as I’m aware Zig also can’t execute child processes at comptime.
Try explicitly forcing comptime with var ghx = comptime try Ghext.init(std.testing.allocator); it won’t compile.

Yeah I think this returns a dangling stack pointer. String literals are usually stored in .rodata AFAIK and you can’t exactly do that with a runtime-known slice/array pointer. Although in your example this might actually be the case because all values are comptime-known.

EDIT: just confirming this:

const literal = "asdhfjklsadfhsdjklafhsdajfklsahdfljksadhfkhaksjr"; // 48 chars

export fn concat(slc: *const [8]u8) *const [8 + literal.len]u8 {
    return slc ++ literal;
}

export fn use(slc: *const [8]u8) u32 {
    const str = slc ++ literal;
    var result: u32 = 0;
    for (0..str.len - 8) |i| {
        for (str[i..][0..8]) |c| result += c;
    }
    return result;
}

With -O ReleaseFast -mcpu x86_64:

concat:
// doesn't even perform concatenation, just returns stack pointer
        push    rbp
        mov     rbp, rsp
        lea     rax, [rbp - 57]
        pop     rbp
        ret
use:
        push    rbp
// copy slc param to stack[-56..-48]
        mov     rbp, rsp
        mov     rax, qword ptr [rdi]
        mov     qword ptr [rbp - 56], rax
// copy literal to stack[-48..0]
        movups  xmm0, xmmword ptr [rip + __anon_194]
        movups  xmmword ptr [rbp - 48], xmm0
        movups  xmm0, xmmword ptr [rip + __anon_194+16]
        movups  xmmword ptr [rbp - 32], xmm0
        movdqu  xmm0, xmmword ptr [rip + __anon_194+31]
        movdqu  xmmword ptr [rbp - 17], xmm0

        [... loop etc ...]

        pop     rbp
        ret

__anon_194:
        .asciz  "asdhfjklsadfhsdjklafhsdajfklsahdfljksadhfkhaksjr"

(godbolt)

Apparently the ++ indeed concatenates the slices on the stack.

Yea that makes sense about the allocator. It complains if I do head: comptime []const u8,

The comptime only applies to the type here and since types are always comptime-known it is indeed redundant. You would have to do comptime head: []const u8, then the compiler would complain that you have to supply an initial comptime-known value for head (which would be immutable at runtime) or pass head as a separate comptime parameter to your functions.

But you probably don’t want head to be comptime here anyways because then you would have to recompile your entire program every time you want to use it.

Thats kinda the idea; I need that hash of the last commit appended to a version string only when I build the application. But now I am not sure if its doable with git binary. But I need it to be able to get hashes when the application does not have .git directory (bsd ports builds, etc).

Ah sorry I’ve missed until now that your code is supposed to be called from a build script. In that case why not use the std.Build API, e.g. std.Build.addSystemCommand()?
head also doesn’t have to be comptime-known within your code, it only needs to be comptime-known from the perspective of the code compiled by the build script, which it will be if it is generated during the build process.

The build script is one option. I also want to cover usage from within the application via same hash(). Tho, maybe I should drop that idea about non-build script use case.