/* * lowering pass to create an ir from a catskill source tree. * * the idea is to fully de-sugar a catskill source file * into an ir that can be very easily re-expressed into * a low-level language, like our transpilation target, c. * * the lowering pass is itself split into two passes, one * initial pass, which collects all top-level type & function * declarations to build up a table of all available language objects, * and then a second pass going over each function body. * this de-couples usage of the functions and types from their ordering * allowing for more free-flowing files than c would allow. * * additionally, we handle type dependencies by collecting every * direct reference a type has to another, and topologically * sort the types to create the correct ordering of them, * pointing out any unbreakable cycles to the user as they come up. * * Copyright (c) 2026, Mel G. * * SPDX-License-Identifier: MPL-2.0 */ #pragma once #include "catboot.h" void lower_emit_error(struct Unit* unit, struct Span span, struct String message) { struct Diagnostic d = { .severity = DIAGNOSTIC_ERROR, .span = span, .message = message, }; array_push(&unit->diagnostics, &d); unit->had_error = true; } void lower_emit_error_c(struct Unit* unit, struct Span span, const ascii* message) { lower_emit_error(unit, span, string_from_c_string(message)); } bool lower_type_lookup_by_name(struct Unit* unit, struct String name, Type_Id* out_id) { FOR_EACH_ARRAY(struct Type_Name_To_Id, mapping, &unit->types.by_name, { if (string_equals(mapping.name, name)) { *out_id = mapping.id; return true; } }) return false; } bool lower_function_lookup_by_name(struct Unit* unit, struct String name, Function_Id* out_id) { FOR_EACH_ARRAY(struct Function_Name_To_Id, mapping, &unit->functions.by_name, { if (string_equals(mapping.name, name)) { *out_id = mapping.id; return true; } }) return false; } // registers a basic type object for a primitive, so other types // can depend on it and reference it in the same manner as any other type. Type_Id lower_seed_primitive(struct Unit* unit, const ascii* name) { Type_Id id = array_length(&unit->types.entries); struct String name_str = string_from_static_c_string(name); struct Type* type = type_new(id, TYPE_PRIMITIVE, name_str, span_empty()); array_push(&unit->types.entries, &type); struct Type_Name_To_Id mapping = { .name = name_str, .id = id }; array_push(&unit->types.by_name, &mapping); return id; } // turns a source type expression into a concrete type reference object. // resolving named types from the type table. // unwraps any type modifiers (like references, arrays, etc.), // adding them into the type reference object. struct Type_Ref lower_intern_type_ref(struct Unit* unit, struct Tree_Type* tree_type) { // TODO: support synthetic structural types. struct Type_Ref ref = { .type_id = unit->types.primitive_void_id, .mods = array_new(enum Type_Modifier, 4), }; if (!tree_type || tree_type->type == TREE_TYPE_NONE) return ref; struct Tree_Type* current = tree_type; while (current && current->type == TREE_TYPE_REFERENCE) { enum Type_Modifier mod = TYPE_MOD_REFERENCE; array_push(&ref.mods, &mod); current = current->value.reference.referenced_type; } if (!current || current->type == TREE_TYPE_NONE) return ref; switch (current->type) { case TREE_TYPE_NAME: { Type_Id id; // at this point we know all top-level types (independent of order), // if we fail to find the type referenced by the name, we know for a fact // this type does not exist. if (lower_type_lookup_by_name(unit, current->value.name.name, &id)) { ref.type_id = id; } else { lower_emit_error( unit, current->span, string_concatenate( ARG_ASCII, "undefined type '", ARG_STRING, current->value.name.name, ARG_ASCII, "'", ARG_END)); } return ref; } default: lower_emit_error_c( unit, current->span, "unimplemented: only named types and references are supported for now"); return ref; } } // adds a by-value dependency. by-pointer references don't add — a forward // declaration of the target is enough to use it through a pointer. void lower_add_dependency(struct Type* type, struct Type_Ref ref) { if (array_length(&ref.mods) > 0) { enum Type_Modifier outer = *array_at(enum Type_Modifier, &ref.mods, 0); if (outer == TYPE_MOD_REFERENCE) return; } array_push(&type->depends_on, &ref.type_id); } // is this source statement a function? // if it is, unwrap it and return true, otherwise false. bool lower_match_function_decl( struct Tree_Statement* stmt, struct String* out_name, struct Tree_Expression** out_fn_expr) { if (stmt->kind != TREE_STATEMENT_EXPRESSION) return false; struct Tree_Expression* expr = stmt->value.expression.inner; if (!expr || expr->kind != TREE_EXPRESSION_BINARY_OPERATION) return false; if (expr->value.binary_operator.operation != BINARY_ASSIGN) return false; struct Tree_Expression* lhs = expr->value.binary_operator.left_operand; struct Tree_Expression* rhs = expr->value.binary_operator.right_operand; if (!lhs || lhs->kind != TREE_EXPRESSION_NAME) return false; if (!rhs || rhs->kind != TREE_EXPRESSION_FUNCTION) return false; *out_name = lhs->value.name.name; *out_fn_expr = rhs; return true; } // is this source statement a type? // if it is, unwrap it and return true, otherwise false. bool lower_match_type_decl( struct Tree_Statement* stmt, struct String* out_name, struct Tree_Type** out_tree_type) { if (stmt->kind != TREE_STATEMENT_EXPRESSION) return false; struct Tree_Expression* expr = stmt->value.expression.inner; if (!expr || expr->kind != TREE_EXPRESSION_BINARY_OPERATION) return false; if (expr->value.binary_operator.operation != BINARY_ASSIGN) return false; struct Tree_Expression* lhs = expr->value.binary_operator.left_operand; struct Tree_Expression* rhs = expr->value.binary_operator.right_operand; if (!lhs || lhs->kind != TREE_EXPRESSION_NAME) return false; if (!rhs || rhs->kind != TREE_EXPRESSION_TYPE) return false; *out_name = lhs->value.name.name; *out_tree_type = rhs->value.type.type; return true; } // register a function shell object, only listing a name and assigning a unique identifier. bool lower_register_function_shell(struct Unit* unit, struct String name, struct Span span) { Function_Id existing; if (lower_function_lookup_by_name(unit, name, &existing)) { lower_emit_error( unit, span, string_concatenate( ARG_ASCII, "duplicate function '", ARG_STRING, name, ARG_ASCII, "'", ARG_END)); return false; } Function_Id id = array_length(&unit->functions.entries); struct Function* fn = function_new(id, name); array_push(&unit->functions.entries, &fn); struct Function_Name_To_Id mapping = { .name = name, .id = id }; array_push(&unit->functions.by_name, &mapping); return true; } // register a type shell object, only listing a name and assigning a unique identifier. // used to resolve references to types which are defined out-of-order in the source. bool lower_register_type_shell(struct Unit* unit, struct String name, struct Span span) { Type_Id existing; if (lower_type_lookup_by_name(unit, name, &existing)) { lower_emit_error( unit, span, string_concatenate( ARG_ASCII, "duplicate type '", ARG_STRING, name, ARG_ASCII, "'", ARG_END)); return false; } Type_Id id = array_length(&unit->types.entries); struct Type* type = type_new(id, TYPE_NONE, name, span); type->depends_on = array_new(Type_Id, 16); array_push(&unit->types.entries, &type); struct Type_Name_To_Id mapping = { .name = name, .id = id }; array_push(&unit->types.by_name, &mapping); return true; } void lower_fill_function_signature( struct Unit* unit, struct Function* fn, struct Tree_Expression* fn_expr) { fn->is_main = string_equals_c_str(fn->name, "main"); fn->return_type = lower_intern_type_ref(unit, fn_expr->value.function.header.return_type); fn->params = array_new(struct Param, 16); bool variadic = false; FOR_EACH ( struct Tree_Type*, param_type, fn_expr->value.function.header.parameters_type_and_name) { struct Param p = { .name = param_type->value_name, .type = lower_intern_type_ref(unit, param_type), }; array_push(&fn->params, &p); if (param_type->variadic) variadic = true; } fn->variadic = variadic; fn->main_takes_args = fn->is_main && array_length(&fn->params) > 0; fn->ast_body = &fn_expr->value.function.body; fn->body = nil; } void lower_fill_type_body(struct Unit* unit, struct Type* type, struct Tree_Type* tree_type) { switch (tree_type->type) { case TREE_TYPE_NAME: { type->kind = TYPE_ALIAS; Type_Id target_id; if (lower_type_lookup_by_name(unit, tree_type->value.name.name, &target_id)) { type->value.alias.target_id = target_id; // aliases always need their target's full definition. array_push(&type->depends_on, &target_id); } else { lower_emit_error( unit, tree_type->span, string_concatenate( ARG_ASCII, "undefined type '", ARG_STRING, tree_type->value.name.name, ARG_ASCII, "'", ARG_END)); } break; } case TREE_TYPE_STRUCTURE: { type->kind = TYPE_STRUCTURE; type->value.structure.fields = array_new(struct Field, 16); FOR_EACH (struct Tree_Type*, tree_field, tree_type->value.structure.fields) { struct Field f = { .name = tree_field->value_name, .type = lower_intern_type_ref(unit, tree_field), }; array_push(&type->value.structure.fields, &f); lower_add_dependency(type, f.type); } break; } case TREE_TYPE_VARIANT: { type->kind = TYPE_VARIANT; type->value.variant.cases = array_new(struct Variant_Case, 16); uint32 next_tag = 0; FOR_EACH (struct Tree_Type*, tree_case, tree_type->value.variant.variants) { struct Variant_Case c = { .name = tree_case->value_name, .tag = next_tag++, .has_payload = tree_case->type != TREE_TYPE_NONE, .payload = { 0 }, }; if (c.has_payload) { c.payload = lower_intern_type_ref(unit, tree_case); lower_add_dependency(type, c.payload); } array_push(&type->value.variant.cases, &c); } break; } case TREE_TYPE_FUNCTION: { type->kind = TYPE_FUNCTION; struct Tree_Function_Header* header = &tree_type->value.function.header; type->value.function.return_type = lower_intern_type_ref(unit, header->return_type); lower_add_dependency(type, type->value.function.return_type); type->value.function.params = array_new(struct Type_Ref, 16); bool variadic = false; FOR_EACH (struct Tree_Type*, param_type, header->parameters_type_and_name) { struct Type_Ref ref = lower_intern_type_ref(unit, param_type); array_push(&type->value.function.params, &ref); lower_add_dependency(type, ref); if (param_type->variadic) variadic = true; } type->value.function.variadic = variadic; break; } case TREE_TYPE_CLASS: lower_emit_error_c(unit, tree_type->span, "unimplemented: class types"); break; default: // structural compound forms (array, maybe, tuple, map) at top level // belong to step 10 (structural canonicalization). lower_emit_error_c(unit, tree_type->span, "unimplemented: this top-level type form"); break; } } // lowering pass 1, sub-pass a // collection of every single declaration of a type of function, // alongside with initial registration of any referenced dependencies. void lower_pass_1_register_shells(struct Unit* unit, struct Tree* tree) { FOR_EACH (struct Tree_Statement*, stmt, tree->top_level_statements) { struct String name; struct Tree_Expression* fn_expr; if (lower_match_function_decl(stmt, &name, &fn_expr)) { lower_register_function_shell(unit, name, stmt->span); continue; } struct Tree_Type* tree_type; if (lower_match_type_decl(stmt, &name, &tree_type)) { lower_register_type_shell(unit, name, stmt->span); continue; } } } // lowering pass 1, sub-pass b // walking over all top-level functions and types, fully filling out // their definitions. // now that sub-pass a has registered all top-level definitions, we can // finally build out the type reference dag within the translation unit. void lower_pass_1_fill_bodies(struct Unit* unit, struct Tree* tree) { FOR_EACH (struct Tree_Statement*, stmt, tree->top_level_statements) { struct String name; struct Tree_Expression* fn_expr; if (lower_match_function_decl(stmt, &name, &fn_expr)) { Function_Id id; // TODO: actually go into the body, only the signature for now. if (lower_function_lookup_by_name(unit, name, &id)) { struct Function* fn = *array_at(struct Function*, &unit->functions.entries, id); if (!fn->ast_body) lower_fill_function_signature(unit, fn, fn_expr); } continue; } struct Tree_Type* tree_type; if (lower_match_type_decl(stmt, &name, &tree_type)) { Type_Id id; if (lower_type_lookup_by_name(unit, name, &id)) { struct Type* type = *array_at(struct Type*, &unit->types.entries, id); if (type->kind == TYPE_NONE) lower_fill_type_body(unit, type, tree_type); } continue; } if (stmt->kind == TREE_STATEMENT_PRAGMA) { lower_emit_error_c(unit, stmt->span, "unimplemented: top-level pragmas"); continue; } lower_emit_error_c(unit, stmt->span, "unsupported top-level statement"); } } // lowering pass 1 // collects all top-level definitions into the translation unit's // tables and fully maps out the references between them. void lower_pass_1(struct Unit* unit, struct Tree* tree) { lower_pass_1_register_shells(unit, tree); lower_pass_1_fill_bodies(unit, tree); } // return line number for the given byte position in the source. (1-based) uint lower_span_to_line(struct String source, struct Span span) { uint line = 1; uint upto = span.start; if (upto > string_length(source)) upto = string_length(source); for (uint i = 0; i < upto; ++i) { if (string_at(source, i) == '\n') line++; } return line; } // builds a nice & friendly cycle detection message and emits it as a diagnostic. // `chain` is the path we walked while looking for the cycle, and // `cycle_start` is the chain index at which the cycle closes. void lower_report_type_cycle( struct Unit* unit, struct Source_File source, struct _Array* chain, uint cycle_start) { struct String_Buffer buf = string_buffer_new(512); string_buffer_append_c_str(&buf, "type cycle detected:\n"); uint chain_len = array_length(chain); for (uint k = cycle_start; k < chain_len; ++k) { Type_Id current_id = *array_at(Type_Id, chain, k); Type_Id next_id; if (k + 1 < chain_len) next_id = *array_at(Type_Id, chain, k + 1); else next_id = *array_at(Type_Id, chain, cycle_start); struct Type* current = *array_at(struct Type*, &unit->types.entries, current_id); struct Type* next = *array_at(struct Type*, &unit->types.entries, next_id); uint line = lower_span_to_line(source.source, current->span); ascii line_buf[256]; snprintf( line_buf, sizeof line_buf, " %s (line %lu) depends on %s\n", string_c_str(current->name), line, string_c_str(next->name)); string_buffer_append_c_str(&buf, line_buf); } string_buffer_append_c_str( &buf, "hint: break the cycle with a reference (use `&T` instead of `T`). :)"); Type_Id origin_id = *array_at(Type_Id, chain, cycle_start); struct Type* origin = *array_at(struct Type*, &unit->types.entries, origin_id); lower_emit_error(unit, origin->span, string_buffer_to_string(&buf)); } // topologically sorting the unit's type dependency graph. // implemented through kahn's algorithm. // see: https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm // final ordering for the type emission is stable, types declared earlier // are always first to emit when their vertex in-degree is 0. // cycles reported as diagnostic, partial emission order will still be completed. void lower_topological_sort_dependency_graph(struct Unit* unit, struct Source_File source) { const uint done_sentinel = (uint)-1; // marks vertecies that have been processed uint n = array_length(&unit->types.entries); if (n == 0) return; // for i, holds in-degree for type i. Array(uint) in_degree = array_new(uint, n); for (uint i = 0; i < n; ++i) { struct Type* type = *array_at(struct Type*, &unit->types.entries, i); uint deg = array_length(&type->depends_on); array_push(&in_degree, °); } // for i, holds every index of types that depends on type i. Array(struct _Array) reverse_deps = array_new(struct _Array, n); for (uint i = 0; i < n; ++i) { struct _Array slot = _array_new(sizeof(Type_Id), 16); array_push(&reverse_deps, &slot); } for (uint i = 0; i < n; ++i) { struct Type* type = *array_at(struct Type*, &unit->types.entries, i); Type_Id me = (Type_Id)i; FOR_EACH_ARRAY(Type_Id, dep, &type->depends_on, { struct _Array* slot = array_at(struct _Array, &reverse_deps, dep); _array_push(slot, &me); }) } uint output_count = 0; while (output_count < n) { Type_Id chosen = (Type_Id)-1; for (uint i = 0; i < n; ++i) { if (*array_at(uint, &in_degree, i) == 0) { chosen = (Type_Id)i; break; } } if (chosen == (Type_Id)-1) break; // cycle array_push(&unit->type_emission_order, &chosen); output_count++; *array_at(uint, &in_degree, chosen) = done_sentinel; struct _Array* dependents = array_at(struct _Array, &reverse_deps, chosen); FOR_EACH_ARRAY(Type_Id, dependent, dependents, { uint* d = array_at(uint, &in_degree, dependent); if (*d != done_sentinel) (*d)--; }) } if (output_count == n) return; // we've got a cycle! walk starting from the lowest type that's // still pending, following edges until we find type we already saw. // we revisit an id. Type_Id start = (Type_Id)-1; for (uint i = 0; i < n; ++i) { if (*array_at(uint, &in_degree, i) != done_sentinel) { start = (Type_Id)i; break; } } if (start == (Type_Id)-1) return; Array(Type_Id) chain = array_new(Type_Id, 64); array_push(&chain, &start); Type_Id current = start; while (true) { struct Type* type = *array_at(struct Type*, &unit->types.entries, current); Type_Id next = (Type_Id)-1; FOR_EACH_ARRAY(Type_Id, dep, &type->depends_on, { if (*array_at(uint, &in_degree, dep) != done_sentinel) { next = dep; break; } }) if (next == (Type_Id)-1) return; // not a real cycle uint chain_len = array_length(&chain); uint found_at = chain_len; for (uint k = 0; k < chain_len; ++k) { if (*array_at(Type_Id, &chain, k) == next) { found_at = k; break; } } if (found_at < chain_len) { lower_report_type_cycle(unit, source, &chain, found_at); return; } array_push(&chain, &next); current = next; } } void lower_tree(struct Tree* tree, struct Source_File source, struct Unit* unit) { unit->types.entries = array_new(struct Type*, 256); unit->types.by_hash = array_new(struct Type_Hash_To_Id, 256); unit->types.by_name = array_new(struct Type_Name_To_Id, 256); unit->functions.entries = array_new(struct Function*, 256); unit->functions.by_name = array_new(struct Function_Name_To_Id, 256); unit->imports = array_new(struct Import, 32); unit->type_emission_order = array_new(Type_Id, 256); unit->had_error = false; unit->diagnostics = array_new(struct Diagnostic, 64); unit->types.primitive_int_id = lower_seed_primitive(unit, "int"); unit->types.primitive_uint_id = lower_seed_primitive(unit, "uint"); unit->types.primitive_bool_id = lower_seed_primitive(unit, "bool"); unit->types.primitive_string_id = lower_seed_primitive(unit, "string"); unit->types.primitive_float_id = lower_seed_primitive(unit, "float"); unit->types.primitive_byte_id = lower_seed_primitive(unit, "byte"); unit->types.primitive_ascii_id = lower_seed_primitive(unit, "ascii"); unit->types.primitive_void_id = lower_seed_primitive(unit, "void"); if (tree) lower_pass_1(unit, tree); lower_topological_sort_dependency_graph(unit, source); }