Why a tagged union? How the AST struct represents every node type, and how to add your own without breaking existing code.
An Abstract Syntax Tree represents the hierarchical structure of a program after parsing. In first, the AST uses the classic C pattern of a tagged union — a single struct type that can hold any node variant, with a tag field indicating which variant is active.
typedef struct AST {
Kind tag; // which variant is active
union {
struct AST_PROGRAM AST_PROGRAM;
struct AST_EXPRESSION_LIST AST_EXPRESSION_LIST;
struct AST_BINARY_EXPRESSION AST_BINARY_EXPRESSION;
struct AST_IDENTIFIER AST_IDENTIFIER;
struct AST_BOOLEAN AST_BOOLEAN;
struct AST_NUMBER AST_NUMBER;
struct AST_STRING AST_STRING;
struct AST_ASSIGNMENT AST_ASSIGNMENT;
struct AST_VARIABLE_DECLARATION AST_VARIABLE_DECLARATION;
struct AST_UNARY AST_UNARY;
struct AST_CONDITIONAL AST_CONDITIONAL;
struct AST_BLOCK AST_BLOCK;
struct AST_FUNCTION AST_FUNCTION;
struct AST_CALL AST_CALL;
} data;
} AST;
Every node is the same size in memory (the size of the largest variant — currently AST_CONDITIONAL, which holds a growable array of when arms). The tag field tells the interpreter which data member is valid to read, e.g. ast->data.AST_BINARY_EXPRESSION.left.
In object-oriented languages you would model this with a base class and subclasses for each node type. In C, the tagged union is the idiomatic equivalent — and it keeps everything in a single allocation without pointer indirection to vtables.
With -Wswitch enabled (part of -Wextra, which every target builds with), the compiler warns if any switch (ast->tag) does not handle every Kind value. Adding a new tag triggers compile-time warnings in every switch that does not handle it. The compiler audits your code for you.
Kind also has an AST_DECLARATION value that is never actually used as a node's tag. struct AST_DECLARATION is a plain helper struct (a name/init pair), used as the element type of the array inside AST_VARIABLE_DECLARATION — one per binding in a comma-separated let. It rides along in the Kind enum without ever tagging a real node.
first already added AST_UNARY, AST_BOOLEAN, AST_CONDITIONAL, AST_BLOCK, AST_FUNCTION, and AST_CALL since this pattern was first adopted — a good sign the design scales. Pick a node type from the syntax that's still aspirational in examples/ (built-in I/O, e.g. read(...) / write(...), or string interpolation, e.g. "{n}{newline}") and sketch the new Kind value, its struct AST_* shape, and which existing switch statements in parser.c and interpreter.c would need a new case.