Alessio Marchetti

About Blog

Examples of Metaprogramming - Part 2

Posted: 24 Jan 2026

This is the second part of a series of post. The previous and first post was Examples of Metaprogramming - Part 1. I wrote about a “card editor”, where I had multiple types of cards and i wanted to manipulated them sometimes uniformely (for example when I want to spawn a new card or move it) sometimes in a way specific to the type (for example when deciding what to render).

Warning: What I show here does not pretend to be optimal under any metric. It is meant to be a not-too-suboptimal exploration of what a powerful programming language can do. Unlike WWE, you can try this at home, but probably a more temperant use of fancy programming can be better for getting serious work done.

Card Data Structures

Jai has a very straightforward way of handling inheritance-compostion. In my example let’s say we want to restrict ourself to treat two kind of cards: Generator and Crafter. As a first step we gather all the data needed for the the handling of the “uniform” part of a card behavior. For simplicity let’s say we have a unique identifier for the card and the position (actually I used some other fields, but that’s not the point).

Card :: struct {
    id       : Id;
    position : Vector2;
}

An algebric data type approach would do something like this:

Card :: struct {
    entity   : Card_Entity;
    position : Vector2;
    data : tagged union {
        generator : struct {
            // generator fields
        };
        crafter : struct {
            // crafter fields
        };
    }
}

As a note, at the time when I implemented the system tagged unions existes only in user space in Jai. They have been introduced very recently but I haven’t looked into them yet.

There are pros and cons in using this approach. I tend to like it more than what i used instead, but as said in the disclaimer above, I wanted to use this project to explore new ideas.

The other approach is to use a compositional pattern like:

Generator :: struct {
    card : Card;
    // generator fields
}
Crafter :: struct {
    card : Card;
    // crafter fields
}

Jai makes this approach more ergonomic with two language constructs: using and #as. The keyword using associated to a field which is a struct allows the access to its members in a direct way, thus to get the position of a generator we can use the expression generator.position instead of generator.card.position. The other is #as allows an automatic casting (by selecting the field) a Generator to a Card. Thus if there is a function foo :: (c: Card) I can call foo(generator) instead of foo(generator.card).

With this the code becomes:

Generator :: struct {
    using #as card : Card;
    // generator fields
}
Crafter :: struct {
    using #as card : Card;
    // crafter fields
}

We can then have a an array of card types

meta_card_types :: .[Generator, Crafter];

that can be either written by hand or by gathering all the structs that have an using #as Card field in the metaprogram, exactly how we gathered the log functions in Examples of Metaprogramming - Part 1. I opted for the first one, because while developing it was easier to disable prototypal card types by just removing their entry from the array.

The Manager

At this point I introduced the Card_Manager, which is just a collection of cards in array form. It should support card operations, like “get card by id”, “get card by position”, “delete card”, and some specific card operations, like iteration over all Generator’s.

This is the definitions of the manager:

Card_Manager :: struct {
    // Inserts strings in the form of:
    // generator : Managed_Card_Array(Generator);
    #insert -> string {
        builder : String_Builder;

        for type : meta_card_types {
            info  := cast(*Type_Info_Struct, type);
            lower := get_meta_manager_name(type);
            print_to_builder(*builder, "% : Managed_Card_Array(%);\n", lower, info.name);
        }

        return builder_to_string(*builder);
    }
}

Here Managed_Card_Array is just an array of the type of its argument with an in-place linked list for reusing empty slots created on card deletion.

A thing that helps the search by id very easy is to encode the card type directly into the id. In this way the function can be implemented by directly choosing the correct field within the manager.

Iteration over cards of the same type is trivial, since it amounts to just iterating over the correct array. Iteration over all cards is again easy, since it is just iteration by type, concatenated for each type.

All of these use cases are written with an #insert in the same spirit as the code above. It is to be noted that Zig’s inline for construct (see Zig’s documentation) would be much more ergonomic than what i did here. However I believe an inline for can be written in Jai’s userspace, maybe it will just look “less native”.

Conclusion

In this post I presented a little example of usage of Jai’s metaprogramming.

Comments can be submitted on Hacker News. DMs at my contacts available at my Home Page are more than welcome!

With love,

– Alessio