/* * transpiler from a catskill intermediate translation unit, * to the c programming language, as the main translation target. * * walks a fully-lowered translation unit and emits (slightly ugly) * c source code. * * Copyright (c) 2025-2026, Mel G. * * SPDX-License-Identifier: MPL-2.0 */ #pragma once #include "catboot.h" // the output of the transpilation pass. // used for both file output and string buffer output. struct Transpile_Output { FILE* file; struct String_Buffer string; }; struct Transpile_Output transpile_output_from_file(FILE* file) { check(file != nil, "transpile output file is nil"); return (struct Transpile_Output){ .file = file, .string = string_buffer_empty(), }; } #define TRANSPILE_OUTPUT_MAX_LENGTH 262144 // 256 KiB struct Transpile_Output transpile_output_from_string(void) { return (struct Transpile_Output){ .file = nil, .string = string_buffer_new(TRANSPILE_OUTPUT_MAX_LENGTH), }; } #define TRANSPILE_OUTPUT_MAX_WRITE_LENGTH 4096 void transpile_output_write(struct Transpile_Output* output, const ascii* format, ...) { va_list args; va_start(args, format); if (output->file) { vfprintf(output->file, format, args); } else { ascii buffer[TRANSPILE_OUTPUT_MAX_WRITE_LENGTH]; int written = vsnprintf(buffer, TRANSPILE_OUTPUT_MAX_WRITE_LENGTH, format, args); check(written >= 0 && (uint)written < TRANSPILE_OUTPUT_MAX_LENGTH, "transpile output string buffer overflow"); string_buffer_append_c_str(&output->string, buffer); } va_end(args); } struct String transpile_output_string(struct Transpile_Output* output) { check(output->file == nil, "transpile output is not a string"); return string_buffer_to_string(&output->string); } FILE* transpile_output_file(struct Transpile_Output* output) { check(output->file != nil, "transpile output is not a file"); return output->file; } struct Transpiler { struct Transpile_Output output; struct Unit* unit; }; void transpiler_new(struct Transpiler* transpiler, struct Transpile_Output output) { *transpiler = (struct Transpiler){ .output = output, }; } #define TRANSPILE_PREAMBLE DATA_FOR_VISIT(struct Transpiler, transpiler); #define TRANSPILE_WRITE(...) transpile_output_write(&transpiler->output, __VA_ARGS__) // look up a type by id in the active translation unit. // nil if the identifier is unknown. struct Type* transpile_type_at(struct Transpiler* transpiler, Type_Id id) { if (id >= array_length(&transpiler->unit->types.entries)) return nil; return *array_at(struct Type*, &transpiler->unit->types.entries, id); } // the c-side form of a primitive catskill type. // nil if the given id is not one of the primitives. const ascii* transpile_primitive_c_name(struct Transpiler* transpiler, Type_Id id) { // TODO: far-out, eventually we want to wrap these types not only in an // empty "primitive" type shape, but actually define these concretely // as real types with functions in the catskill standard library! struct Type_Table* t = &transpiler->unit->types; if (id == t->primitive_int_id) return "integer"; if (id == t->primitive_uint_id) return "uint"; if (id == t->primitive_bool_id) return "bool"; if (id == t->primitive_string_id) return "struct String"; if (id == t->primitive_float_id) return "real"; if (id == t->primitive_byte_id) return "byte"; if (id == t->primitive_ascii_id) return "ascii"; if (id == t->primitive_void_id) return "void"; return nil; } // emit the c form of a use of a type. void transpile_visit_type_ref(struct Visit* visit, struct Type_Ref* ref) { TRANSPILE_PREAMBLE struct Type* target = transpile_type_at(transpiler, ref->type_id); const ascii* primitive = transpile_primitive_c_name(transpiler, ref->type_id); if (primitive) { TRANSPILE_WRITE("%s", primitive); } else if (target && (target->kind == TYPE_STRUCTURE || target->kind == TYPE_VARIANT)) { TRANSPILE_WRITE("struct %s", string_c_str(target->name)); } else if (target) { TRANSPILE_WRITE("%s", string_c_str(target->name)); } else { TRANSPILE_WRITE("/* unknown type %lu */", ref->type_id); } // TODO: the other mods! FOR_EACH_ARRAY (enum Type_Modifier, mod, &ref->mods) { if (*mod == TYPE_MOD_REFERENCE) TRANSPILE_WRITE("*"); } } void transpile_visit_type_forward_structure(struct Visit* visit, struct Type* type) { TRANSPILE_PREAMBLE TRANSPILE_WRITE("struct %s;\n", string_c_str(type->name)); } void transpile_visit_type_forward_variant(struct Visit* visit, struct Type* type) { TRANSPILE_PREAMBLE TRANSPILE_WRITE("struct %s;\n", string_c_str(type->name)); } void transpile_visit_type_forward_function(struct Visit* visit, struct Type* type) { TRANSPILE_PREAMBLE // referenced function types are emitted as a typedef TRANSPILE_WRITE("typedef "); VISIT(visit_type_ref, &type->value.function.return_type); TRANSPILE_WRITE(" (*%s)(", string_c_str(type->name)); if (array_length(&type->value.function.params) == 0) { TRANSPILE_WRITE("void"); } else { bool first = true; FOR_EACH_ARRAY (struct Type_Ref, param, &type->value.function.params) { if (!first) TRANSPILE_WRITE(", "); VISIT(visit_type_ref, param); first = false; } if (type->value.function.variadic) TRANSPILE_WRITE(", ..."); } TRANSPILE_WRITE(");\n"); } void transpile_visit_type_definition_alias(struct Visit* visit, struct Type* type) { TRANSPILE_PREAMBLE // aliases map to typedefs // TODO: maybe aliases are too much sugar for the lowered representation, // technically any reference to an alias could directly map to the underlying type. struct Type_Ref bare = { .type_id = type->value.alias.target_id, .mods = { 0 } }; TRANSPILE_WRITE("typedef "); VISIT(visit_type_ref, &bare); TRANSPILE_WRITE(" %s;\n", string_c_str(type->name)); } void transpile_visit_type_definition_structure(struct Visit* visit, struct Type* type) { TRANSPILE_PREAMBLE TRANSPILE_WRITE("struct %s {\n", string_c_str(type->name)); FOR_EACH_ARRAY (struct Field, field, &type->value.structure.fields) { TRANSPILE_WRITE(" "); VISIT(visit_type_ref, &field->type); TRANSPILE_WRITE(" %s;\n", string_c_str(field->name)); } TRANSPILE_WRITE("};\n"); } void transpile_visit_type_definition_variant(struct Visit* visit, struct Type* type) { TRANSPILE_PREAMBLE TRANSPILE_WRITE("struct %s {\n", string_c_str(type->name)); TRANSPILE_WRITE(" uint32 tag;\n"); bool any_payload = false; FOR_EACH_ARRAY (struct Variant_Case, one_case, &type->value.variant.cases) { if (one_case->has_payload) { any_payload = true; break; } } if (any_payload) { TRANSPILE_WRITE(" union {\n"); FOR_EACH_ARRAY (struct Variant_Case, one_case, &type->value.variant.cases) { if (!one_case->has_payload) continue; TRANSPILE_WRITE(" "); VISIT(visit_type_ref, &one_case->payload); TRANSPILE_WRITE(" %s;\n", string_c_str(one_case->name)); } TRANSPILE_WRITE(" } as;\n"); } TRANSPILE_WRITE("};\n"); } // transpilation pass 1 visit table // emits forward declarations of types struct Visit_Table transpile_forward_visit_functions = { .visit_type_ref = transpile_visit_type_ref, .visit_type_structure = transpile_visit_type_forward_structure, .visit_type_variant = transpile_visit_type_forward_variant, .visit_type_function = transpile_visit_type_forward_function, }; // transpilation pass 2 visit table // emits full definitions of types struct Visit_Table transpile_definition_visit_functions = { .visit_type_ref = transpile_visit_type_ref, .visit_type_alias = transpile_visit_type_definition_alias, .visit_type_structure = transpile_visit_type_definition_structure, .visit_type_variant = transpile_visit_type_definition_variant, .visit_type_function = walk_type_primitive, // no-op, already done in pass 1 }; // walk a lowered translation unit and emit c source into the transpiler's output. // the order of the translation unit, and thus the final source output is fixed. // preamble, then imports, type forwards, type definitions in topological dependency // order, function forwads, function definitions, and runtime trailer (if main present). int transpile_unit(struct Transpiler* transpiler, struct Unit* unit) { transpiler->unit = unit; visit_table_fill_defaults(&transpile_forward_visit_functions); visit_table_fill_defaults(&transpile_definition_visit_functions); struct Visit visit_value = { .user_data = transpiler }; struct Visit* visit = &visit_value; TRANSPILE_WRITE("#include \"core.c\"\n"); FOR_EACH_ARRAY (struct Import, import, &unit->imports) { TRANSPILE_WRITE("#include \"%.*s\"\n", (int)import->path.length, import->path.data); } // trigger pass 1 walk visit->table = &transpile_forward_visit_functions; FOR_EACH_ARRAY (struct Type*, type_slot, &unit->types.entries) { VISIT(visit_type, *type_slot); } // trigger pass 2 walk visit->table = &transpile_definition_visit_functions; FOR_EACH_ARRAY (Type_Id, ordered_id, &unit->type_emission_order) { struct Type* type = *array_at(struct Type*, &unit->types.entries, *ordered_id); VISIT(visit_type, type); } bool main_takes_args = false; bool has_main = false; FOR_EACH_ARRAY (struct Function*, function_slot, &unit->functions.entries) { struct Function* function = *function_slot; if (function->is_main) { has_main = true; main_takes_args = function->main_takes_args; } } if (has_main) { if (main_takes_args) TRANSPILE_WRITE("#define CATSKILL_MAIN_TAKES_ARGS\n"); TRANSPILE_WRITE("#include \"runtime.c\"\n"); } return 0; }