/* * 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; } 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 tree-side type expression into an ir Type_Ref. for now this only // resolves simple named types (primitives + user-named types already in the // table); compound and structural shapes get deferred to later commits. struct Type_Ref lower_intern_type_ref(struct Unit* unit, struct Tree_Type* tree_type) { 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; switch (tree_type->type) { case TREE_TYPE_NAME: { Type_Id id; if (lower_type_lookup_by_name(unit, tree_type->value.name.name, &id)) { ref.type_id = 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)); } return ref; } default: // compound types (T?, [T], (T1, T2), &T, etc.) arrive in a later commit. lower_emit_error_c( unit, tree_type->span, "unimplemented: only simple named types are supported for now"); return ref; } } void lower_declare_function( struct Unit* unit, struct String name, struct Tree_Expression* fn_expr, 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; } Function_Id id = array_length(&unit->functions.entries); struct Function* fn = function_new(id, name); fn->is_main = string_equals_c_str(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; array_push(&unit->functions.entries, &fn); struct Function_Name_To_Id mapping = { .name = name, .id = id }; array_push(&unit->functions.by_name, &mapping); } // extract `name = fun (...) ret { ... }` from a top-level statement, // or return false if it's not that shape. 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; } bool lower_match_type_decl(struct Tree_Statement* stmt) { 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; return true; } // pass 1 walks every top-level statement and collects declarations into // the unit's tables. function bodies stay as raw ast for pass 2 to lower. void lower_pass_1(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_declare_function(unit, name, fn_expr, stmt->span); continue; } if (lower_match_type_decl(stmt)) { lower_emit_error_c(unit, stmt->span, "unimplemented: type declarations"); 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"); } } void lower_tree(struct Tree* tree, struct Source_File source, struct Unit* unit) { (void)source; 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); }