loading...
Cover image for How I use `variant` in the Leaf compiler

How I use `variant` in the Leaf compiler

mortoray profile image edA‑qa mort‑ora‑y Originally published at mortoray.com ・3 min read

Variants are data types that can store different types of values in them, as opposed to one fixed type. In contrast to a generic object or an untyped variable, there is a limited number of storable types. I use these in my Leaf compiler in two key places: logging, and type information.

I'm still using boost::variant as opposed to std::variant. I'm waiting for widespread deployment of C++17 compilers before upgrading (such as with new long-term Ubuntu releases).

Logging

I recently added a new logging system to Leaf. The caller provides an error reference and contextual data when making a log entry. variant is used to capture different types of contextual information. I previously used variant this way in my low latency logging system.

logger.error( "cerr-unknown-identifier", logger::item_symbol( symbol.name ) );

cerr-unknown-identifier is an error message identifier. It's a key to an error message stored in a YAML file, this particular error is:

cerr-unknown-identifier:
    text: Unknown identifier `{symbol}`

Note the placeholder {symbol} there. Somehow the logger needs to replace that with a symbol. That's what the logger::item_symbol( symbol.name ) argument provides.

But what about other types? For example, an argument mismatch call involves more information:

mismatch-argcount:
    text: The function `{symbol}` expects {expect} argument(s), you provided {actual}.

The caller passes more arguments to error than before.

logger.error( 'mismatch-argcount', logger::item_symbol( symbol.name ), 
    logger::item_expect( func_arg_count ), logger::item_actual( call_args.size() ) );

Different items carry different types of information. The function calls mask item construction, but each of these item_ functions returns a logger::item.

typedef boost::variant<
        std::string,
        int64_t,
        source_location> item_variant_t;
struct item {
    item_type_t type;
    item_variant_t value;
};

The type carries the semantic meaning, such as i_symbol or i_actual. The value contains the data associated with this item. It's a variant type that allows either a string, an int64_t, or a source_location.

The error function has a few variations accepting lists of items and individual items. A macro is most often used to make these calls as it adds items for line and source information.

Type Traits

Leaf's type system has two parallel systems: concrete types and type specifiers. Type specifiers allow incomplete types and type constraints. We see the specifiers in leaf source code.

var pine : optional integer 32bit
var cone : optional = 25
var twig : float high = 1/2

optional integer 32bit contains three parts, stored in a type_spec::part structure. The type_spec doesn't know much about what these parts mean, only how to store them. It has a std::vector<part> list of parts.

As the value type of each part varies significantly, I use a variant to store all the possibilities.

struct part {
    part_type_t type;
    boost::variant<
        bool,
        extr_type,
        extr_type::reference_t,
        int,
        intr_type_compat::type_t,
        intr_type::fun_class_t,
        intr_type_function::access_t,
        intr_type_tuple::pack_t,
        shared_ptr<intr_type const>, //MUST not be an instance!
        std::string,
        type_spec_symbol,
        intr_type_function::convention_t
    > value;
    //sub-parametrics
    std::vector<shared_ptr<type_spec const>> sub;
    //attached expression
    shared_ptr<node const> node_expr;
    shared_ptr<expression const> expr;
};

Because working with a variant can be burdensome, and also wanting stronger typing, users of type_spec don't use the variant directly. All access goes through template functions.

// creating the `optional integer 32bit` specifier
type_spec ts.
ts.set<pt_data_bitsize>( 32 );
ts.set<pt_optional>( true );
ts.set<pt_fun_class>( intr_type::integer );

The set call prevents associating the wrong type of information with the part. It has this signature:

template<part_type_t PT>
part & set( typename part_type_descriptor<PT>::type value )

I'm mapping an enum value to type information with specialized templates. The setup is hidden behind macros, but here's the pt_optional one for example:

template<> struct part_type_descriptor<pt_optional> {
    typedef bool type;
};

There's a matching if_get function, which returns the value of a particular part if it exists. This approach adds strong-typing and hides the complexity of working with variant inside the type_spec class.

template<part_type_t PT>
boost::optional<typename part_type_descriptor<PT>::type> if_get() const {

I like C++'s ability to map an enum value into a concrete type. It, along with variant, is the key to making type_spec type-safe while holding variable types of information. This template flexibility is one of the things that attracts me to C++.

What about you?

Let me know how you use variant in your code? Even if it's not C++, the concept exists in other languages. Alternatively, tell me about how you'd like to use variant but have only a generic object type available.

Posted on by:

mortoray profile

edA‑qa mort‑ora‑y

@mortoray

I'm a creative writer and adventurous programmer. I cook monsters.

Discussion

markdown guide