kairav

July 9, 2026, 12:37pm 1

Hey all,
I fell in love with zig while exploring it this summer and I kind of look at it as a language that can actually be the forefront of low level parts of ML/AI in the future. Hence, I have been working on zcore, a tensor library in Zig, and wanted to share it early while the core design is still open to change rather than waiting until it’s “finished.”

Where it’s at right now: shape/strides/indexing, bounds-checked get/at, slicing, transpose, resize, broadcasting shape logic, and zero/fill initializers. No arithmetic ops yet (add/mul/matmul etc.) : that’s next, and I’d genuinely like input on the approach before I commit to it.

A few design decisions I’d love pushback on:

  • Tensor owns an allocator per-instance rather than taking one per-call … trying to keep the API ergonomic but I dont know if this is the right tradeoff.
  • Strides/shape are computed and stored on the heap alongside data, with from_slice supporting adopting external memory.
  • Type-restricted at comptime to int/float/bool.

To be honest, a huge way that my this project is different for me is that i have tried to keep this production grade from the start, while still actually focusing on an aim of mine: a well enough documented code that every single beginner too can understand well.comments are meant to be thorough enough for the code to double as a learning resource, not just a working library.
There’s an implementation plan in the repo (implementation_plan.md) laying out the phases if you want the fuller picture, plus a style guide too.

Repo: https://github.com/ka1rav6/zcore

It’s early and small (~1000 lines), so if you’ve got 10 minutes to skim it, I’d rather hear “this approach will bite you later” now than after I’ve built arithmetic ops on top of it. It is the first time I am trying to scale a project like this so all my design decisions have been made looking at the code bases of other people… and i would love some honest criticism.


A note on me: I am a student at a tier one engineering college who loves math, ML and low level programming

xash July 9, 2026, 5:47pm 2

Welcome to ziggit (:

I skimmed to code and got a bit of feedback on the code:

Using field names like _data is discouraged by the style guide: Documentation - The Zig Programming Language

It depends on your wanted API, but I’d expect IndexOutOfBounds not to be a runtime error, but a usage error. You could skip the whole error-mechanism and assert that the index is not out of bounds with a fn isInBounds or something like that.

At least you can allocate size and stride in one go to reduce allocations and potential fragmentation.

Then on the API itself:

Tensor owns an allocator per-instance rather than taking one per-call … trying to keep the API ergonomic but I dont know if this is the right tradeoff.

In most zig code the approach is the other way around (e.g. the std containers are moving to variants without allocator fields; you pass the allocator in the functions.) Though I see you are also trying to auto manage memory management with owns_memory. Just like you can const slice = buffer[10..20] I would trust the user to correctly manage this. Much less ergonomic for the API, but you could also have the tensors not own the data, e.g.

const tensor : Tensor(f32) = try .initEmpty(gpa, &.{ 100, 100 }); // data is null
defer tensor.deinit(gpa);

const data = try tensor.zeroes(gpa); // only now 100 * 100 f32s got allocated
defer gpa.free(data);

const view = tensor.view(gpa, &.{ 10, 10 }, &.{ 20, 20 });
defer view.deinit(gpa);

– not sure if it is worth it. (: (There is also the whole possibility to make dimensionality comptime known & not needing to allocate shape/stride – but that’s with different tradeoffs)

WeeBull July 11, 2026, 3:22pm 3

A few comments, starting with project setup.

  • For STYLE_GUIDE.md, it’s probably best just to defer to the guide in the Zig documentation. Over time it’ll be less hassle to adopt the “language style”. Otherwise your API will look odd in other projects.
  • You’ve got two indentation sizes specified. One in STYLE_GUIDE.md (3) and one in CONTRIBUTING.md (4)
  • “Every function must have tests” might be a little too strict. I tend to prefer “All code should be covered by tests”.

…and on to code…

  • The managed / unmanaged question (i.e. does a data structure keep a private allocator handle or not). You’ll note that the std library is moving towards “unmanaged” (i.e explicit allocator passing per call). Not only with ArrayList but across the board..
    For me the biggest advantage is that it becomes very obvious which calls are definitely not able to allocate memory. Memory allocation is a potentially expensive operation, and being able to know that fn tadd(this: Tensor, other: Tensor) void will not allocate memory, can’t fail due to lack of memory and therefore must be an inplace operation is all useful information.
    It might seem like a lot of parameter passing at the moment, because you’re dealing with the allocation code, but I think later on you’ll find that relatively few methods need an allocator.
  • Tensors vs Matrices: Currently you only supprt two dimensional tensors – Matrices. That’s fine, but it’s colouring your API. You have terms like “row” and “column”, but that doesn’t scale to tensors. You would have the first and second axis (or zeroth and first). You have transpose(), but the more general version would be swap_axes(). I would suggest you need to use general terms if it’s a tensor library, or matrix terms if it’s a matrix library.
  • Be aware that a high performance matrix library will probably iterate over the data in non-obvious orders. Cache misses are your enemy, so you’ll work in blocks that fit in cache. Try not to do anything that will limit how you access data in the future. I can’t see anything yet, but heads up.
  • .broadcast_to() looks like a footgun to me. It creates a new tensor that shares data with the original tensor. When the original tensor is .destoy()-ed it will free() the data underneath this one, leading the use-after-free issues. Similar with .slice(). I think if you want to support these copy-less views of the tensor, it might need a more comprehensive abstraction. Maybe others can suggest an approach.

…trivial points…

  • utils.zig: Anything named that becomes a dumping ground. Move those two functions into core.zig where they are used and just don’t export them. Helper functions are fine. A module of helper functions is a code-smell.
  • Tensor.destroy() would normally be called Tensor.deinit()
  • .get_shape() / .get_strides(): Getters and Setters are Java nonsense IMHO. Lose the noise.
  • .fill() is just @memset(), which you used for full()
  • .set_whole() is @memcpy