u/Inevitable-Spinach-7

How to move comptime results into the target binary when comptime can run FFI and I/O?

For some years now, I've wanted to combine the comptime capabilities of Zig and Jai into a single compiler while also learning about compilers and interpreters.
I'm building a compiler that extends Zig-style comptime so the same code that runs at runtime can run at comptime, including FFI and I/O (like Jai, but with Zig's "run what you can" model).

The hard part is moving comptime results into the binary:

  1. Host vs target. Comptime code uses the host layout for interop and is then converted to the target layout. Sizes (usize/isize), endianness and float behavior can differ. Types like f80 and f128 are also hardware-specific: their precision, rounding and in-memory layout can differ between host and target. The same function can then give different results, which breaks hash maps, lookup tables and binary formats built at comptime.
  2. Moving pointers into the binary. Results containing pointers must be relocated, connected data must stay connected, and everything must be converted to the target layout.
  3. Pointers whose value isn't clear. Examples are opaque pointers, untagged unions of a pointer and something else, and XOR linked lists, where the pointer isn't stored directly. There may be other cases I haven't thought of.
  4. Calling C at comptime. C can return memory with known bounds, or resources that are only valid during compilation. Those become invalid at runtime.
  5. Containers built at comptime. HashMap and ArrayList need the whole buffer, including uninitialized slots, but the size isn't always known:
    1. ArrayList stores {ptr, len} plus a separate capacity, so the allocation size isn't in the pointer and length.
    2. HashMap can keep a header next to the allocation and use pointer arithmetic to reach it.
    3. Runtime code may try to free a buffer that now lives in static memory.
    4. FFI has the same problem, since C can return memory with unknown bounds.

My current prgress:

  1. Host to target:
    1. with host specs first, then convert to the target layout.
    2. For types that differ, such as usize/isize, use intCast where possible.
    3. For hashes and similar code, type information would store the host and target size, alignment and so on. That would help the developer see where the two disagree and act accordingly.
    4. If the target's endianness differs from the host's, my best idea is to disable I/O and interop and interpret the code with the target's endianness.
  2. Baking pointers:
    1. When the pointee type is known, baking is easy.
    2. void* and untagged unions containing pointers are a hard error during baking.
    3. Each type would have a default userland function that bakes its value into the binary.
    4. Types like XOR linked lists, void* and untagged unions could override it to say how they should be baked.
  3. Compiler-created data. Data created by the compiler itself has no problem, and the userland bake function might cover the rest.
  4. Containers and FFI memory (problems 4 and 5). I have no solution yet, apart from maybe using the bake function.
  5. I don't have a solution yet for f80/f128 and other float types that differ.

Questions: Is there a cleaner model than host-then-convert?
Am I missing failure cases?
Is there any better solution or any problem I did not see?

reddit.com
u/Inevitable-Spinach-7 — 9 days ago