about summary refs log tree commit diff
path: root/boot/transpile.c
diff options
context:
space:
mode:
authorMel <mel@rnrd.eu>2026-05-26 00:50:09 +0200
committerMel <mel@rnrd.eu>2026-05-26 00:50:09 +0200
commit900b412b052a0f745f0853d6703e4bafbb0a3ce5 (patch)
treee04b65b395d4761e5b88fa1e9a792c3996ff3dc1 /boot/transpile.c
parente8400cc704eeb74f3c464f7a0822c556cb52e3fd (diff)
downloadcatskill-900b412b052a0f745f0853d6703e4bafbb0a3ce5.tar.zst
catskill-900b412b052a0f745f0853d6703e4bafbb0a3ce5.zip
Lambda function emission, thin/fat calling conventions for (future) closed-over state
Signed-off-by: Mel <mel@rnrd.eu>
Diffstat (limited to 'boot/transpile.c')
-rw-r--r--boot/transpile.c188
1 files changed, 160 insertions, 28 deletions
diff --git a/boot/transpile.c b/boot/transpile.c
index 9c94706..9b8f420 100644
--- a/boot/transpile.c
+++ b/boot/transpile.c
@@ -146,7 +146,9 @@ transpile_visit_type_ref(struct Visit* visit, struct Type_Ref* ref)
 
     if (primitive) {
         TRANSPILE_WRITE("%s", primitive);
-    } else if (target && (target->kind == TYPE_STRUCTURE || target->kind == TYPE_VARIANT)) {
+    } else if (target
+               && (target->kind == TYPE_STRUCTURE || target->kind == TYPE_VARIANT
+                   || target->kind == TYPE_FUNCTION)) {
         TRANSPILE_WRITE("struct %s", string_c_str(target->name));
     } else if (target) {
         TRANSPILE_WRITE("%s", string_c_str(target->name));
@@ -174,27 +176,23 @@ transpile_visit_type_forward_variant(struct Visit* visit, struct Type* type)
     TRANSPILE_WRITE("struct %s;\n", string_c_str(type->name));
 }
 
+// emit the fat closure struct that represents any function-typed value.
+// bare (normal) functions wrap into this via a thunk with a nil state.
 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 ");
+    TRANSPILE_WRITE("struct %s {\n", string_c_str(type->name));
+    TRANSPILE_WRITE("    void* state;\n");
+    TRANSPILE_WRITE("    ");
     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(" (*call)(void* state");
+    FOR_EACH_ARRAY (struct Type_Ref, param, &type->value.function.params) {
+        TRANSPILE_WRITE(", ");
+        VISIT(visit_type_ref, param);
     }
-    TRANSPILE_WRITE(");\n");
+    if (type->value.function.variadic) TRANSPILE_WRITE(", ...");
+    TRANSPILE_WRITE(");\n};\n");
 }
 
 void
@@ -280,6 +278,52 @@ transpile_function_c_name(struct Function* function)
     return string_c_str(function->name);
 }
 
+// the c-side name of the thunk that wraps a function as a fat closure value.
+struct String
+transpile_function_thunk_name(struct Function* function)
+{
+    struct String_Buffer buf = string_buffer_new(64);
+    string_buffer_append_c_str(&buf, "__cat_thunk_");
+    string_buffer_append_c_str(&buf, transpile_function_c_name(function));
+    return string_buffer_to_string(&buf);
+}
+
+// emit thunk wrapper for a normal function, allowing them to be used
+// like closures.
+void
+transpile_function_emit_thunk(struct Visit* visit, struct Function* function)
+{
+    TRANSPILE_PREAMBLE
+
+    VISIT(visit_type_ref, &function->return_type);
+    struct String thunk_name = transpile_function_thunk_name(function);
+    TRANSPILE_WRITE(" %s(void* state", string_c_str(thunk_name));
+    FOR_EACH_ARRAY (struct Param, param, &function->params) {
+        TRANSPILE_WRITE(", ");
+        VISIT(visit_type_ref, &param->type);
+        TRANSPILE_WRITE(" %s", string_c_str(param->name));
+    }
+    if (function->variadic) TRANSPILE_WRITE(", ...");
+    TRANSPILE_WRITE(") {\n    (void)state;\n    ");
+
+    // non-returning functions can't return a value, crazy!
+    struct Type* return_type = transpile_type_at(transpiler, function->return_type.type_id);
+    bool returns_void =
+        return_type && return_type->id == transpiler->unit->types.primitive_void_id
+        && array_length(&function->return_type.mods) == 0;
+    if (!returns_void) TRANSPILE_WRITE("return ");
+    TRANSPILE_WRITE("%s(", transpile_function_c_name(function));
+    bool first = true;
+    FOR_EACH_ARRAY (struct Param, param, &function->params) {
+        if (!first) TRANSPILE_WRITE(", ");
+        TRANSPILE_WRITE("%s", string_c_str(param->name));
+        first = false;
+    }
+    TRANSPILE_WRITE(");\n}\n");
+}
+
+void transpile_emit_indent(struct Transpiler* transpiler);
+
 // emits c-side format of a function header.
 // shared by the signature and body passes.
 void
@@ -289,15 +333,21 @@ transpile_function_emit_signature(struct Visit* visit, struct Function* function
 
     VISIT(visit_type_ref, &function->return_type);
     TRANSPILE_WRITE(" %s(", transpile_function_c_name(function));
-    if (array_length(&function->params) == 0) {
+    bool need_separator = false;
+    if (function->synthetic) {
+        // synthetic functions implicitly take in state
+        TRANSPILE_WRITE("void* state");
+        need_separator = true;
+    }
+
+    if (array_length(&function->params) == 0 && !need_separator) {
         TRANSPILE_WRITE("void");
     } else {
-        bool first = true;
         FOR_EACH_ARRAY (struct Param, param, &function->params) {
-            if (!first) TRANSPILE_WRITE(", ");
+            if (need_separator) TRANSPILE_WRITE(", ");
             VISIT(visit_type_ref, &param->type);
             TRANSPILE_WRITE(" %s", string_c_str(param->name));
-            first = false;
+            need_separator = true;
         }
         if (function->variadic) TRANSPILE_WRITE(", ...");
     }
@@ -310,6 +360,19 @@ transpile_visit_function_signature(struct Visit* visit, struct Function* functio
     TRANSPILE_PREAMBLE
     transpile_function_emit_signature(visit, function);
     TRANSPILE_WRITE(";\n");
+
+    // a thunk only exists for thin, named functions referenced as values!
+    if (function->synthetic || function->closure_type_id == 0) return;
+
+    VISIT(visit_type_ref, &function->return_type);
+    struct String thunk_name = transpile_function_thunk_name(function);
+    TRANSPILE_WRITE(" %s(void* state", string_c_str(thunk_name));
+    FOR_EACH_ARRAY (struct Param, param, &function->params) {
+        TRANSPILE_WRITE(", ");
+        VISIT(visit_type_ref, &param->type);
+    }
+    if (function->variadic) TRANSPILE_WRITE(", ...");
+    TRANSPILE_WRITE(");\n");
 }
 
 void
@@ -317,13 +380,32 @@ transpile_visit_function_body(struct Visit* visit, struct Function* function)
 {
     TRANSPILE_PREAMBLE
     transpile_function_emit_signature(visit, function);
-    TRANSPILE_WRITE(" ");
-    if (function->body) {
-        VISIT(visit_block, function->body);
-    } else {
-        TRANSPILE_WRITE("{\n}");
+
+    if (!function->body) {
+        TRANSPILE_WRITE(" {\n}\n");
+        return;
     }
+
+    // add a void cast for the implicit state argument, in case synthetic
+    // function does not need to use it
+    if (function->synthetic) {
+        TRANSPILE_WRITE(" {\n");
+        transpiler->indent_level++;
+        transpile_emit_indent(transpiler);
+        TRANSPILE_WRITE("(void)state;\n");
+        FOR_EACH_ARRAY (struct Statement*, statement, &function->body->statements)
+            VISIT(visit_statement, *statement);
+        transpiler->indent_level--;
+        transpile_emit_indent(transpiler);
+        TRANSPILE_WRITE("}\n");
+        return;
+    }
+
+    TRANSPILE_WRITE(" ");
+    VISIT(visit_block, function->body);
     TRANSPILE_WRITE("\n");
+
+    if (function->closure_type_id != 0) transpile_function_emit_thunk(visit, function);
 }
 
 void
@@ -448,14 +530,33 @@ transpile_visit_expression_call(struct Visit* visit, struct Expression* expressi
 {
     TRANSPILE_PREAMBLE
 
-    transpile_emit_expression(visit, expression->value.call.subject, TRANSPILE_PRECEDENCE_POSTFIX);
-    TRANSPILE_WRITE("(");
+    struct Expression* subject = expression->value.call.subject;
+    bool is_indirect = expression->value.call.is_indirect;
+
+    if (!is_indirect && subject && subject->kind == EXPRESSION_FUNCTION_REF) {
+        // direct calls emit the subject bare
+        struct Function* fn = *array_at(
+            struct Function*, &transpiler->unit->functions.entries,
+            subject->value.function_ref.function_id);
+        TRANSPILE_WRITE("%s", transpile_function_c_name(fn));
+    } else if (is_indirect) {
+        // route through the fat closure value's call slot, passing state
+        transpile_emit_expression(visit, subject, TRANSPILE_PRECEDENCE_POSTFIX);
+        TRANSPILE_WRITE(".call(");
+        transpile_emit_expression(visit, subject, TRANSPILE_PRECEDENCE_POSTFIX);
+        TRANSPILE_WRITE(".state");
+    } else {
+        transpile_emit_expression(visit, subject, TRANSPILE_PRECEDENCE_POSTFIX);
+    }
+
+    if (!is_indirect) TRANSPILE_WRITE("(");
+
     Array(struct Call_Argument) args = expression->value.call.arguments;
     uint max_slot = 0;
     FOR_EACH_ARRAY (struct Call_Argument, a, &args) {
         if (a->slot > max_slot) max_slot = a->slot;
     }
-    bool first = true;
+    bool first = !is_indirect;
     for (uint s = 0; s <= max_slot; ++s) {
         FOR_EACH_ARRAY (struct Call_Argument, a, &args) {
             if (a->slot != s) continue;
@@ -519,6 +620,36 @@ transpile_visit_expression_construct(struct Visit* visit, struct Expression* exp
 }
 
 void
+transpile_visit_expression_function_ref(struct Visit* visit, struct Expression* expression)
+{
+    TRANSPILE_PREAMBLE
+
+    struct Function* fn = *array_at(
+        struct Function*, &transpiler->unit->functions.entries,
+        expression->value.function_ref.function_id);
+
+    // we only get here when the function reference is explicitly a *value*,
+    // any direct calls are filtered out earlier.
+    struct Type* closure_type =
+        transpile_type_at(transpiler, expression->value.function_ref.closure_type_id);
+
+    const ascii* call_name;
+    struct String thunk_name = string_empty();
+    if (fn->synthetic) {
+        // synthetic functions are already fat-by-default, can call directly
+        call_name = transpile_function_c_name(fn);
+    } else {
+        // thin named functions go through their generated thunk to match the
+        // fat calling convention
+        thunk_name = transpile_function_thunk_name(fn);
+        call_name = string_c_str(thunk_name);
+    }
+
+    TRANSPILE_WRITE(
+        "(struct %s){ .state = nil, .call = %s }", string_c_str(closure_type->name), call_name);
+}
+
+void
 transpile_visit_expression_increment_decrement(struct Visit* visit, struct Expression* expression)
 {
     TRANSPILE_PREAMBLE
@@ -718,6 +849,7 @@ struct Visit_Table transpile_body_visit_functions = {
     .visit_expression_cast = transpile_visit_expression_cast,
     .visit_expression_construct = transpile_visit_expression_construct,
     .visit_expression_increment_decrement = transpile_visit_expression_increment_decrement,
+    .visit_expression_function_ref = transpile_visit_expression_function_ref,
 };
 
 // walk a lowered translation unit and emit c source into the transpiler's output.