DEV Community

Cover image for How I use `variant` in the Leaf compiler
edA‑qa mort‑ora‑y
edA‑qa mort‑ora‑y

Posted on • Originally published at mortoray.com

7 2

How I use `variant` in the Leaf compiler

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 ) );
Enter fullscreen mode Exit fullscreen mode

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}`
Enter fullscreen mode Exit fullscreen mode

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}.
Enter fullscreen mode Exit fullscreen mode

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() ) );
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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 );
Enter fullscreen mode Exit fullscreen mode

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 )
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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 {
Enter fullscreen mode Exit fullscreen mode

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.

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more