Aburi Linux graphical desktop

Graphical Linux desktop with Aburi-compiled kernel running in QEMU

Today we are releasing Version 0.2 of Aburi, an in-progress but relatively comprehensive modern C/C++ compiler that can already compile many real world C programs (now including the Linux Kernel) and parts of the C++ stdlib.

The codename for this release is “Derleyici,” which is Turkish for “compiler.” It comes from the root verb “derlemek,” meaning to compile or collect. That feels like a nice touch given that our semantic analysis layer is called “Collect.”

This version is a thorough rewrite. Both the frontend and backend were rebuilt from the ground up. I’ll keep the technical rundown as brief as I reasonably can while still giving you the useful bits as attention spans aren’t what they used to be and compiler(-adjacent) people tending to be busy. Myself included; my own weakness being Instagram Reels. I keep telling myself I should just delete the app, but it’s genuinely how I stay in touch with friends. I suspect the older crowd in the C toolchain world has their own time sinks, whether that’s Bluesky or, back when it was around, Cohost.

If the internals talk doesn’t interest you, feel free to jump straight to the “Current Status” section.

Growing Pains

Type system

In the old compiler (Classic Aburi), every type was represented with a CType base object, with concrete types represented with subobjects. New types came into existence whenever they were needed, either through the DeclarationParser or straight from whatever subsystem required them, one example being template-related types. The object-oriented design itself never really hurt us. We moved to static_cast almost immediately, since the subclass count was low and checking before a downcast was easy enough. The real problems were elsewhere.

Type caching existed, but only barely. Builtin types like int, long got cached and that was about it. Parse a pointer to int in two different places and you’d end up with two separate PointerType objects wrapping the exact same BuiltinType, even though those PointerTypes meant the same thing. That wasted memory, and worse, it forced frequent deep comparisons, walking through every data member one at a time and burning CPU cycles along the way. A string-keyed cache paired with pointer identity would have fixed this. We do lean on string caches in the new compiler, but I was reluctant to build around pointer identity, for reasons I’ll explain later.

QualType was its own source of confusion. As mentioned in my previous codebase, this codebase began life as a very small C-subset compiler, and as such I never expected to reach the point where QualType would be needed. When that day came, migrating everything written up to then to take QualType explicitly was simply too much work, and the models available at the time weren’t good enough to automate it either. So QualType became a sort of pass-through layer: hand one to a function expecting a CType and it would silently degrade into a CType. The obvious problem being any qualifier information could quietly vanish as the function worked on that CType and passed it along to other consumers.

struct CType {
    TypeKind kind;
    mutable uint32_t external_semantic_owner_id = 0;
    explicit CType(): kind(TypeKind::Other) {}
    explicit CType(TypeKind k) : kind(k) {}
    virtual ~CType() = default;
    // this is in bits!!!
    virtual int64_t getWidth() { return 0; }
    int64_t getWidthBytes() {
        return (getWidth() + 7) / 8; // round up to the nearest byte if neceessary
    }
    virtual bool isArithmetic() const { return false; }
    virtual bool isScalar()     const {
        return isArithmetic() || kind == TypeKind::Pointer ||
               kind == TypeKind::MemberPointer || kind == TypeKind::Enum ||
               kind == TypeKind::BlockPointer;
    }
  // ... virtual functions like isInteger, isFloatingPoint 
};

A snippet of the CType struct in Classic Aburi

Global State

In the code there is a global pointer to ASTContext with some other global variables. I think an agent added this sometime in the middle of a debugging session and I forgot why I allowed it but in retrospect it doesn’t make much sense to me and created more problems than it solved. We always have a ASTContext active at any given moment. I think it was for certain functions that operated on global state but we could have always converted those to be within/consume ASTContext. Regardless, an “ick” to be dealt with.

Semantic AST

Nothing about the old architecture bothered me more than this. The AST, which I modeled on Clang’s, was supposed to be mostly syntax, with semantic details attached only where needed. That held up fine in C mode, where there was barely any semantics to speak of (at least compared to C++). DeclContext took care of canonical and forward declarations and definitions for functions and variables. Expression nodes simply carried a QualType for their associated type. By the way, anyone digging through the code will notice I had single-cased every Expr node with QualType instead of having a single QualType on Expr like Clang does. I just never got around to refactoring it.

Then C++ features started landing and things got messy ….. fast. Look at FuncDecl:

struct FuncDecl: Decl {
    FuncDecl(const std::string name, const std::shared_ptr<CType> type, std::vector<std::unique_ptr<Decl>> parameters,
             std::unique_ptr<Stmt> body, std::unordered_set<std::string> stmt_labels = {},
             StorageClass storage_class = StorageClass::NONE, bool is_inline = false, SrcLoc loc = SrcLoc())
             : FuncDecl(DeclKind::FuncDecl, name, type, std::move(parameters), std::move(body),
                        std::move(stmt_labels), storage_class, is_inline, loc) {}
    FuncDecl(SrcLoc loc = SrcLoc()): FuncDecl(DeclKind::FuncDecl, loc) {}

    std::string name;
    std::shared_ptr<CType> type;
    std::unordered_set<std::string> stmt_labels;
    std::vector<std::unique_ptr<Decl>> parameters;
    std::unique_ptr<Stmt> body;
    std::vector<TemplateArgument> explicit_specialization_arguments;
    std::shared_ptr<Scope> scope;
    const std::string* asm_label;
    QualType friend_access_type = nullptr;
    StorageClass storage_class;
    std::unique_ptr<Expr> trailing_requires_clause;
    bool has_explicit_specialization_argument_list = false;
    uint8_t is_inline : 1;
    uint8_t has_prior_non_inline_declaration : 1;
    // todo: combine constexpr/consteval fields?
    uint8_t is_constexpr : 1;
    uint8_t is_consteval : 1;
    uint8_t is_deleted : 1;
    uint8_t is_defaulted : 1;
    uint8_t is_defaulted_on_first_declaration : 1;
    uint8_t has_deferred_defaulted_body : 1;
    uint8_t language_linkage : 2;

Building one of these big AST nodes was a pain. Every new field meant touching the constructor, and sometimes every place that constructed the node. The awkward part was that different creators wanted different fields populated. A Collect routine for C-style constructs might set up one group of fields, while the C++ out-of-line function collector cared about a whole different set. Coordinating all of that through a single giant constructor, or even several constructors, turned into a headache.

A fair question at this point: why not push all that data into a side table and let the FuncDecl pointer index into it? I passed on that because I didn’t want pointer identity spreading any further through the codebase.

Honestly, my objection to pointer identity is a more personal one. It technically works (albeit with major well-documented caveats), but it feels wrong to me as its too implicit. What I really wanted was something like a SemanticIDs to identify semantic objects and had I kept the AST around, I probably would have gotten to that eventually. An ID carries a much stronger invariant than a pointer, since a pointer could mean anything (and is not created/assigned by us). It also makes rollback and tentative parsing far easier. If we create a type inside a tentative parse window, then bail out, checking that the ID is now invalid is trivial with a generation marker.

Memory was another sore spot. The AST got genuinely bloated; in one instance I watched a 2 MB source file turn into 30 MB of compiler memory. It doesn’t help that you pay for fields you never touch: Plenty of the fields above serve no purpose when compiling plain C or ordinary declarations in C++ mode, and asm_label is used almost never. Worse, the design scatters accesses all over the place, which is rough on memory locality.

At some point I started wondering whether the AST deserved to survive at all. Conventional compiler wisdom says keeping one is a good idea, and to be fair, tracking semantics really is unavoidable in C++. Templates can’t be implemented without a semantic system, and requires clauses in concepts flat-out demand a preserved expression syntax tree. Still, we are not trying to compete with Clang, and definitely not on the level of syntax analysis that IDEs, language servers, and clang-format call for. So I went with the second route, which the section on the new compiler covers.

Serialization was also on my mind. Modules need it, and it opens the door to some interesting ideas around faster compilation. Getting it right under the AST’s structure would have been much harder than with the new approach, giving me one more reason to walk away.

Adding new AST nodes can be difficult

For the compiler to get anything done, whether that’s template work, ast2llvm, or anything else, it has to walk down the AST. There are generic ways to pull this off, of course. The very first version of the semantic analysis system, which ran strictly after parsing, actually used an ASTTransformer virtual class, where each consumer could override the node visitor with whatever function it needed (I looked into using std::variant but it didn’t seem like a good solution with large amount of nodes we have, and compile-time is a big concern as we do a lot of edit-build-debug cycles)


std::unique_ptr<Stmt> ASTTransformer::transformStmt(Stmt *stmt) {
    if (auto *s = dynamic_cast<CompoundStmt *>(stmt)) return transformCompoundStmt(s);
    if (auto *s = dynamic_cast<Decl2Stmt *>(stmt)) return transformDecl2Stmt(s);
    if (auto *s = dynamic_cast<ReturnStmt *>(stmt)) return transformReturnStmt(s);
    if (auto *s = dynamic_cast<IfStmt *>(stmt)) return transformIfStmt(s);
    if (auto *s = dynamic_cast<CaseStmt *>(stmt)) return transformCaseStmt(s);
    if (auto *s = dynamic_cast<DefaultStmt *>(stmt)) return transformDefaultStmt(s);
    if (auto *s = dynamic_cast<LabeledStmt *>(stmt)) return transformLabeledStmt(s);
    if (auto *s = dynamic_cast<GoToStmt *>(stmt)) return transformGoToStmt(s);
    if (auto *s = dynamic_cast<SwitchStmt *>(stmt)) return transformSwitchStmt(s);
    if (auto *s = dynamic_cast<WhileStmt *>(stmt)) return transformWhileStmt(s);
    if (auto *s = dynamic_cast<DoWhileStmt *>(stmt)) return transformDoWhileStmt(s);
    if (auto *s = dynamic_cast<ForStmt *>(stmt)) return transformForStmt(s);
    if (auto *s = dynamic_cast<ContinueStmt *>(stmt)) return transformContinueStmt(s);
    if (auto *s = dynamic_cast<BreakStmt *>(stmt)) return transformBreakStmt(s);
    if (auto *s = dynamic_cast<EmptyStmt *>(stmt)) return transformEmptyStmt(s);
    if (auto *e = dynamic_cast<Expr *>(stmt)) return transformExpr(e);
    return nullptr;
}

(Yes I know there are better ways of doing this.)

I scrapped that approach pretty quickly. Every visitor site had its own ideas about when to visit nodes and what to do once it got there (for example: when encountering a function declaration, one visitor might process the body before the arguments, while another does the opposite), so a generic visitor pattern ended up being harder to implement, not easier. It would have bled performance too, with all the dyn_cast calls and virtual dispatch. And even setting that aside, it left a nagging problem: every time someone added a new AST node, someone had to go hunt down each visitor and figure out whether it needed a new switch branch or virtual override for that node.

Let me give a concrete example, one I hinted at in my previous blog post. If you’ve spent time with intermediate or advanced C++, you’ve probably run into decltype. The meaning of a decltype expression depends on parentheses. Example:

#include <iostream>
#include <type_traits>

int main() {
    int x = 67; 

    // 1. Without parentheses: yields the declared type (int)
    using Type1 = decltype(x);

    // 2. With parentheses: evaluates (x) as an lvalue expression, yielding an lvalue reference (int&)
    using Type2 = decltype((x));

    // Verification
    std::cout << std::boolalpha;
    std::cout << "decltype(x)  is int&?  " << std::is_same_v<Type1, int&> << "\n"; // false
    std::cout << "decltype(x)  is int?   " << std::is_same_v<Type1, int>  << "\n"; // true

    std::cout << "decltype((x)) is int&? " << std::is_same_v<Type2, int&> << "\n"; // true
    std::cout << "decltype((x)) is int?  " << std::is_same_v<Type2, int>  << "\n"; // false
}

The parentheses change what the code means, meaning we needed a ParenExpr node. The alternative was slapping an isParen field onto existing AST nodes, which struck me as much messier. I hadn’t added one earlier because plain C never needed it; anything wrapped in parentheses landed in the right place in the AST implicitly via the recursive descent expression parser. When I finally tried to add the ParenExpr node, the number of regressions were so bad that I suspect a few are still lurking unfixed (In the commit message I joked there were more regressions introduced then bunkers in Enver Hoxha’s Albania). To be fair, most new nodes don’t have as large of an impact ParenExpr does, but it was still a headache I had no desire for. Adding new fields to existing nodes resulted in similar struggle.

The failure to keep every visitor pattern in sync as the AST evolved also caused a pile of templating bugs, since the template subsystem depended on deep cloning the AST. Had I stuck with the AST, I would have looked for some way to define general behavior for nodes, so we wouldn’t have to inspect quite so many visitor sites.

Defining AST boundaries can be difficult

Figuring out where each syntax construct belongs turned into its own headache. Does it deserve a brand-new node, or should existing ones just grow extra fields? I described the AST as Clang-like earlier, but it was never an 100% carbon clone. Places where the two designs differed increased, and before long the underlying assumptions had drifted so far apart that copying Clang directly stopped being realistic. A butterfly effect, basically.

A concrete example: take C++ class function declarations and definitions. Should they live inside FuncDecl, or should we roll our own CppClassFuncDecl? The new node could subclass FuncDecl, though inheritance alone wouldn’t fully neutralize the trade-offs. Squeezing class functions into FuncDecl would cut down on visitor work, but in return plain C declarations would end up carrying fields they never use (like trailing_requires_clause), and the initializer complexity mentioned earlier would get worse. Going the other route with a dedicated CppClassFuncDecl softens these problems, yet the cost just moves elsewhere: every visitor and every AST consumer now needs to be touched.

New Architecture

The compiler’s new architecture flattens the semantic state and AST into what’s known as a “Collect IR.” That’s a complete departure from the syntax tree-based designs behind GCC and mainline Clang. The closest relatives are ClangIR, Carbon’s SemIR, and Rust’s MIR, but with a twist: we build the CIR straight from the parser rather than passing through an intermediate form like a syntax tree or AST. A syntax tree does still get generated, but it is not the main source of truth. As far as C++ compilation goes, this is almost uncharted territory. Carbon and Rust are entirely different languages, and ClangIR doesn’t support C++ at the level we do: the website says “C++ has missing parts” but their documentation could be outdated. Plus it still lowers from a fully built Clang AST as its source of truth.

There are CIR instructions for binary operations, unary operations, stack operations, and lvalue/rvalue conversions, which Classic Aburi used to handle by inserting synthetic nodes, among many others. C and C++ code gets flattened down into this CIR form. We do give up direct source fidelity (SrcLoc is still stored within CIR but we no longer have 1-1 representation of source code in memory), but as I said above, that was never a priority for me, and nearly everything else gets better in return. Every instruction comes in at roughly the same size, with operands and side tables pulled in where appropriate. Memory usage and memory locality both improve enormously.

Honestly, the biggest win for me right now isn’t some brand-new feature. It’s that the problems described above are gone, or at least sharply reduced. Forgetting to update switch tables is no longer a concern. No more functions littered with dyn_cast calls.

CIR in Action

The following C code:

  int add(int a, int b) {
      return a + b;
  }

  int main(void) {
      return add(30, 37);
  }

Is equivalent to the following in CIR:

 fn @add(%1: int, %2: int) -> int {
  ^entry(%1: int, %2: int):
    branch ^param.place
  ^param.place:
    %3: place<int> = local_place @a
    store (%3, %1)
    branch ^param.place.2
  ^param.place.2:
    %5: place<int> = local_place @b
    store (%5, %2)
    branch ^expr.lvalue_to_rvalue
  ^expr.lvalue_to_rvalue:
    %7: int = lvalue_to_rvalue (%3)
    branch ^expr.lvalue_to_rvalue.2
  ^expr.lvalue_to_rvalue.2:
    %8: int = lvalue_to_rvalue (%5)
    branch ^expr.binary
  ^expr.binary:
    %9: int = binary + (%7, %8)
    branch ^stmt.return
  ^stmt.return:
    return %9
  }

  fn @main() -> int {
  ^entry:
    branch ^expr.int
  ^expr.int:
    %10: int = integer_literal 30 ; 30
    branch ^expr.int.2
  ^expr.int.2:
    %11: int = integer_literal 37 ; 37
    branch ^expr.call
  ^expr.call:
    %12: int = call @add (%10, %11)
    branch ^stmt.return
  ^stmt.return:
    return %12
  }

What used to be an AST node is now blocks in CIR, as you can probably tell. Structs, classes, and types each get their own structure, far more compact and organized than anything the old compiler had. Since CIR constructs sit at a lower level than AST nodes, lowering to LLVM gets easier. Running certain verification checks becomes simpler too, which goes a long way toward catching miscompiles, and the constexpr engine is much easier to reason about. CIR has plenty of potential uses: A Rust Miri-style interpreter, safety extensions, and plenty more are within reach.

Modules also become far less painful to implement, because serializing a rigid, well-defined list of instructions is so much easier than serializing an AST that never stops changing.

“Instantiating on the Ceiling”1 - how we handle template patterns in CIR

As CIR is not 1-1 representation of the source code like the AST is, it means we can’t represent templates in the same way we used to. Our current design relies on a hole-plus-ceiling model. While parsing function templates, we first convert every non-dependent expression into CIR, or at least try to, falling back to token replay in some cases. Dependent expressions get “holed” up are reparsed at instantiation time. The wrinkle is that dependent expressions can reference non-dependent variables, and those still have to respect two-phase lookup. Class templates, meanwhile, are entirely token based. Our answer to all this is the declaration ceiling. Each definition receives a monotonically increasing number, and anything defined after the number recorded at the template’s point of definition is ignored.

All of this is somewhat of a simplification and I plan on writing a more detailed blog post regarding the subsystem.

Things I’m still working on

Technical debt is still everywhere, some embarrassing and blatant. The compiler is also embarrassingly slow with compiling C++ stdlib headers as my current focus is on correctness. My previous blog post covered most of it, but here are a few more items worth calling out:

Rejecting invalid code

Plenty of invalid code slips through without rejection, or dies with a hard error instead of a clean diagnostic. The priority at the moment is making sure correct code compiles correctly. Over time rejecting bad code will move up the list. Funnily enough the mainstream compilers also do not reject all invalid code. Take the following snippet:

struct Reader {
    template<class T>
    static int inspect(T& value) { return value.secret; }
};
int use() {
    class Vault {
        friend class Reader;
        int secret;
    public:
        Vault() : secret(67) {}
    };
    Vault vault;
    return Reader::inspect(vault);
}
int main() { return use(); }

This is invalid because the friend keyword in Vault is not supposed to find the global Reader struct per dcl.type.elab. GCC rejects it but Clang and Aburi accept it. (This is tracked as CWG issue 2634)

In a few compatibility-sensitive cases we may deliberately follow established implementation behavior over the current wording of the standard. Take this code snippet for example, generated as a reduction while bringing up stdlib support:

struct Value {
    Value(int, int) {}
};

struct Node {
    explicit Node(int, int) {}
};

int pick(Value&&) {
    return 67;
}

int pick(Node&&) {
    return 1;
}

int main() {
    return pick({20, 47});
}

There are two nested overload-resolution processes here. The outer one chooses between pick(Value&&) and pick(Node&&). To determine whether each function is viable, the compiler performs constructor overload resolution for the corresponding parameter type using the braced-init-list.

For the Node&& candidate, that inner overload resolution selects explicit Node(int, int). Although over.match.list says, “In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed,” that restriction applies only when the initialization is part of the final result of overload resolution.

Therefore, the compiler must not discard pick(Node&&) merely because its hypothetical parameter initialization would ultimately use an explicit constructor. Both pick overloads remain viable during the outer overload resolution, and neither is better than the other, so the call is ambiguous. Only if pick(Node&&) were selected as the final overload would the explicit-constructor rule make the program ill-formed.

As you can tell this is counterintuitive. This is tracked in CWG Issue 1228 and GCC (correctly) rejects the code but Clang does not and “peeks through” to pick the Value struct thus returning 67. We also allow the code to compile, with the output binary also returning 67. Seeing how the GCC maintainer said he prefers EDG/Clang’s behavior here, it seems like one of those times where its better to slightly deviate from the spec.

New features

Some of features below work on isolated examples, but they need heavier review and real battle-testing before I write about them in further depth. Like I said in my previous post, the hard part of shipping a feature is making sure it plays nicely with everything else. For completeness, here’s the non-exhaustive list:

  • Native non-optimizing backend for Aarch64, x64, and OpenRISC
  • Coroutines (preliminary support)
    • The creator of coroutines, Gor Nishanov, sadly passed away not too long ago. To honor his memory I will make a follow up post detailing how exactly coroutines were implemented in the compiler. The coroutine implementation is a prime example of the new architecture already paying dividends.
  • Support for compiling Linux binaries, plus getting the compiler itself running on Linux
  • LLVM backend is now using LLVM 22 instead of the ancient LLVM 18.

Adinkra C++ standard library

We have created a small c++ standard library called “Adinkra”. Aside from the obvious, it also sounds like “Dinkum” (if you can’t tell, say “Adinkraware” and “Dinkumware” out loud). Dinkumware obviously referring to the revered Dinkumware C++ libraries.

The goal is to 1) help reduce compile time for debug builds 2) allow real C++ code to compile with Aburi today (bringing up the real c++ standard library takes a while). It can be used with other compilers (personally tested with Apple Clang) and provides a modest compile time speedup on various programs, including ones as complex as JavaScriptCore.

Current Status

C and C++ support has moved past where Classic Aburi left off, albeit there are a few regressions when it comes to rejecting invalid code. Aburi can build the Linux kernel for aarch64 through the LLVM backend. QEMU virt kernels build, along with kernels for the Raspberry Pi 4 and the Rockchip rk3588. I haven’t tested those last two: I’m planning to pick up one of those SBCs and see how it holds up, and I suspect debugging miscompiles on physical hardware would be a much tougher job :) Building those kernels and their device drivers turned out to be a great way to shake out bugs and harden our C support. Here’s another picture of Linux userland booting:

Aburi Linux userland booting

Astute readers will already know that getting the Linux kernel to compile doesn’t carry the weight it once did. So an Alpine Linux config was created where the kernel itself is built with Aburi while gcc and clang handle everything else for the time being. (I am a Fedora user for when I do use Linux, but Alpine, which I only learned about a week ago funnily enough, seemed like a good lightweight option for a demonstration). A setup like that hammers the kernel and its code far harder than launching a single program from a terminal shell. You can see a picture of it in action near the top of the blog post. On top of that, NetBSD and FreeBSD on aarch64 have also compiled with Aburi (or at least they did at one point. Regressions happen.). A reproduction script and instructions for Linux can be found here.

The C++ side is still in the stdlib bring-up phase. The rewrite may have wiped the slate clean, but we’re catching up fast. <type_traits> and <utility> both compile now, and runtime code works too, which matters because parsing is one battle and instantiation is another entirely. With EDG planning to sunset their C++ frontend, Aburi could become a genuinely useful contribution to the C++ community if we manage “complete” the frontend (“complete” meaning it correctly compiles most modern C++ code), but of course that’s a very high bar to clear (EDG is considered by some to be the world’s best C++ compiler writers).

Aburi remains primarily a macOS compiler, targeting Darwin aarch64. It runs on Linux and can compile for Linux, but I haven’t put that path through comprehensive testing, so there may be bugs I haven’t found or fixed, possibly some embarrassing ones. GCC’s libc++ hasn’t been tested either.

Windows support is a different beast altogether, especially on the C++ side with its vaguely documented ABI. I’m open to adding it, but I don’t own a (good enough) Windows machine to test on.

Conclusion

Thanks for sticking with it to the end, or at least scrolling this far. As mentioned earlier, I know attention spans especially among fellow Gen Zs are short. None of this is production ready. It’s alpha quality, buggy, and incomplete. Before anything resembling a 1.0 release, there will be a extensive re-audit of the important, foundational parts of the compiler (the codebase might be big but we can prioritize based on the importance of the code and the likeliness for bugs/hidden invariants. The C++ template subsystem will get significantly more auditing compared to i386 stuff). In the future, assuming misaligned AI hasn’t resulted in the end of humanity, better models will make it possible to do more comprehensive human-led source audits. With “recursive self improvement” (the AIs training themselves) seemingly within reach it seems plausible there is room for models which are an order of magnitude better compared to models of today, making debugging code so much easier. Of course, a future AGI/ASI will obviously be able to one-shot a compiler like this from scratch but we will probably have bigger problems by then :)

Regarding certain “presentation” choices: Normally I would have slapped a boring-ass name on the project, written a strictly technical boring-ass blog post, and kept my presence minimal on anything unrelated to the technical work. Nobody would have known about my interest in a certain empire that lasted 623 years and what became of its European colonies and principalities, or the former British and French holdings on the western side of humanity’s original continent. That playbook worked fine for me before LLMs existed, but we’re now in an era where code is abundant and people value uniqueness and humanity. I’ve been programming for years but only recently recognized the importance of “building in public” (just look at the cooked new grad tech job market). Funnily enough, letting readers see the non-technical side of who I am has brought more eyes to the technical work itself.

This release is dedicated to celebrate/commemorate/remember the independence/national/republic days of the below mentioned countries:

  • Macedonia or North Macedonia (differs depending on who you talk to)
  • A handful of former French West African colonies: Côte d’Ivoire, Benin, Niger, and Burkina Faso
  • Switzerland
  • Singapore

Happy compiling! And if there is anything unclear or you want clarification, do not hesitate to reach out.


  1. A tongue-in-cheek reference to Lionel Richie’s “Dancing On The Ceiling” song. ↩︎