From 9ec5e9e83198a3e0abd68c222f473c8995346fb0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Jan 2026 18:07:42 +0000 Subject: [PATCH] Validate function signatures in type annotations Previously, function-type annotations (e.g., `fn(i32): i32`) only checked that a value was a function, without validating parameter counts, types, return types, or async flags. This change adds proper signature validation. Changes: - Add validate_function_signature() to types.c for comprehensive signature checking including parameter count, types, return type, and async flag - Add types_compatible() helper for comparing types recursively - Add format_type() helper for generating readable error messages - Add type_copy() to ast.c for deep copying Type structures including all function-type fields (fn_param_types, fn_return_type, etc.) - Update expressions.c to use type_copy() for parameter and return types - Add parity test for function signature validation The validation properly handles: - Exact signature matches - Untyped functions with typed annotations (dynamic compatibility) - Numeric type compatibility (i32/i64/f32/f64) - Async/non-async function distinction - Optional parameters and defaults --- include/frontend/ast.h | 1 + .../interpreter/runtime/expressions.c | 68 +---- src/backends/interpreter/types.c | 267 +++++++++++++++++- src/frontend/parser/ast.c | 92 ++++++ .../function_signature_validation.expected | 11 + .../function_signature_validation.hml | 53 ++++ 6 files changed, 418 insertions(+), 74 deletions(-) create mode 100644 tests/parity/language/function_signature_validation.expected create mode 100644 tests/parity/language/function_signature_validation.hml diff --git a/include/frontend/ast.h b/include/frontend/ast.h index beb5b964..187f6ad9 100644 --- a/include/frontend/ast.h +++ b/include/frontend/ast.h @@ -546,6 +546,7 @@ Stmt* stmt_export_reexport(char **export_names, char **export_aliases, int num_e Stmt* stmt_import_ffi(const char *library_path); Stmt* stmt_extern_fn(const char *function_name, char **param_names, Type **param_types, int num_params, Type *return_type); Type* type_new(TypeKind kind); +Type* type_copy(Type *src); // Deep copy a Type (including function types) Type* type_compound(Type **types, int num_types); // Create compound type (A & B & C) Type* type_function(Type **param_types, char **param_names, int *param_optional, int *param_is_const, int num_params, char *rest_param_name, diff --git a/src/backends/interpreter/runtime/expressions.c b/src/backends/interpreter/runtime/expressions.c index 84b41bed..0ebc8a29 100644 --- a/src/backends/interpreter/runtime/expressions.c +++ b/src/backends/interpreter/runtime/expressions.c @@ -1431,42 +1431,10 @@ Value eval_expr(Expr *expr, Environment *env, ExecutionContext *ctx) { fn->param_names[i] = strdup(expr->as.function.param_names[i]); } - // Copy parameter types (may be NULL) + // Copy parameter types (may be NULL) - use type_copy for deep copy fn->param_types = malloc(sizeof(Type*) * expr->as.function.num_params); for (int i = 0; i < expr->as.function.num_params; i++) { - if (expr->as.function.param_types[i]) { - fn->param_types[i] = type_new(expr->as.function.param_types[i]->kind); - // Copy nullable flag - fn->param_types[i]->nullable = expr->as.function.param_types[i]->nullable; - // Copy type_name for custom types (enums and objects) - if (expr->as.function.param_types[i]->type_name) { - fn->param_types[i]->type_name = strdup(expr->as.function.param_types[i]->type_name); - } - // Copy element_type for arrays - if (expr->as.function.param_types[i]->element_type) { - fn->param_types[i]->element_type = type_new(expr->as.function.param_types[i]->element_type->kind); - fn->param_types[i]->element_type->nullable = expr->as.function.param_types[i]->element_type->nullable; - if (expr->as.function.param_types[i]->element_type->type_name) { - fn->param_types[i]->element_type->type_name = strdup(expr->as.function.param_types[i]->element_type->type_name); - } - } - // Copy compound types (for intersection types like A & B) - if (expr->as.function.param_types[i]->compound_types) { - Type *src = expr->as.function.param_types[i]; - fn->param_types[i]->num_compound_types = src->num_compound_types; - fn->param_types[i]->compound_types = malloc(sizeof(Type*) * src->num_compound_types); - for (int j = 0; j < src->num_compound_types; j++) { - // Shallow copy - compound constituents are simple types (TYPE_CUSTOM_OBJECT) - fn->param_types[i]->compound_types[j] = type_new(src->compound_types[j]->kind); - fn->param_types[i]->compound_types[j]->nullable = src->compound_types[j]->nullable; - if (src->compound_types[j]->type_name) { - fn->param_types[i]->compound_types[j]->type_name = strdup(src->compound_types[j]->type_name); - } - } - } - } else { - fn->param_types[i] = NULL; - } + fn->param_types[i] = type_copy(expr->as.function.param_types[i]); } // Store parameter defaults (AST expressions, not evaluated yet) @@ -1505,40 +1473,14 @@ Value eval_expr(Expr *expr, Environment *env, ExecutionContext *ctx) { // Copy rest parameter (varargs) if present if (expr->as.function.rest_param) { fn->rest_param = strdup(expr->as.function.rest_param); - if (expr->as.function.rest_param_type) { - fn->rest_param_type = type_new(expr->as.function.rest_param_type->kind); - fn->rest_param_type->nullable = expr->as.function.rest_param_type->nullable; - if (expr->as.function.rest_param_type->type_name) { - fn->rest_param_type->type_name = strdup(expr->as.function.rest_param_type->type_name); - } - } else { - fn->rest_param_type = NULL; - } + fn->rest_param_type = type_copy(expr->as.function.rest_param_type); } else { fn->rest_param = NULL; fn->rest_param_type = NULL; } - // Copy return type (may be NULL) - if (expr->as.function.return_type) { - fn->return_type = type_new(expr->as.function.return_type->kind); - // Copy nullable flag - fn->return_type->nullable = expr->as.function.return_type->nullable; - // Copy type_name for custom types (enums and objects) - if (expr->as.function.return_type->type_name) { - fn->return_type->type_name = strdup(expr->as.function.return_type->type_name); - } - // Copy element_type for arrays - if (expr->as.function.return_type->element_type) { - fn->return_type->element_type = type_new(expr->as.function.return_type->element_type->kind); - fn->return_type->element_type->nullable = expr->as.function.return_type->element_type->nullable; - if (expr->as.function.return_type->element_type->type_name) { - fn->return_type->element_type->type_name = strdup(expr->as.function.return_type->element_type->type_name); - } - } - } else { - fn->return_type = NULL; - } + // Copy return type (may be NULL) - use type_copy for deep copy + fn->return_type = type_copy(expr->as.function.return_type); // Store body AST (shared, not copied) fn->body = expr->as.function.body; diff --git a/src/backends/interpreter/types.c b/src/backends/interpreter/types.c index 35a62260..7b8f75b0 100644 --- a/src/backends/interpreter/types.c +++ b/src/backends/interpreter/types.c @@ -770,10 +770,246 @@ static const char* type_kind_to_string(TypeKind kind) { case TYPE_BUFFER: return "buffer"; case TYPE_ARRAY: return "array"; case TYPE_NULL: return "null"; + case TYPE_GENERIC_OBJECT: return "object"; + case TYPE_FUNCTION: return "function"; + case TYPE_INFER: return "any"; default: return "unknown"; } } +// Forward declaration for recursive type compatibility +static int types_compatible(Type *expected, Type *actual); + +// Helper to format a Type as a string (for error messages) +// Returns a malloc'd string that caller must free +static char* format_type(Type *type) { + if (!type) { + return strdup("any"); + } + + switch (type->kind) { + case TYPE_FUNCTION: { + // Calculate required buffer size + size_t len = 3; // "fn(" + if (type->fn_is_async) { + len += 6; // "async " + } + for (int i = 0; i < type->fn_num_params; i++) { + if (i > 0) len += 2; // ", " + if (type->fn_param_types && type->fn_param_types[i]) { + char *param_str = format_type(type->fn_param_types[i]); + len += strlen(param_str); + free(param_str); + } else { + len += 3; // "any" + } + } + len += 1; // ")" + if (type->fn_return_type) { + char *ret_str = format_type(type->fn_return_type); + len += 2 + strlen(ret_str); // ": type" + free(ret_str); + } + len += 1; // null terminator + + char *result = malloc(len + 16); // extra buffer for safety + char *p = result; + + if (type->fn_is_async) { + p += sprintf(p, "async "); + } + p += sprintf(p, "fn("); + for (int i = 0; i < type->fn_num_params; i++) { + if (i > 0) { + p += sprintf(p, ", "); + } + if (type->fn_param_types && type->fn_param_types[i]) { + char *param_str = format_type(type->fn_param_types[i]); + p += sprintf(p, "%s", param_str); + free(param_str); + } else { + p += sprintf(p, "any"); + } + } + p += sprintf(p, ")"); + if (type->fn_return_type) { + char *ret_str = format_type(type->fn_return_type); + p += sprintf(p, ": %s", ret_str); + free(ret_str); + } + return result; + } + + case TYPE_ARRAY: + if (type->element_type) { + char *elem_str = format_type(type->element_type); + char *result = malloc(strlen(elem_str) + 10); + sprintf(result, "array<%s>", elem_str); + free(elem_str); + return result; + } + return strdup("array"); + + case TYPE_CUSTOM_OBJECT: + if (type->type_name) { + return strdup(type->type_name); + } + return strdup("object"); + + default: + return strdup(type_kind_to_string(type->kind)); + } +} + +// Check if two types are compatible for function signature matching +// Returns 1 if compatible, 0 if not +static int types_compatible(Type *expected, Type *actual) { + // If expected is unspecified (NULL/infer), any actual type is compatible + if (!expected || expected->kind == TYPE_INFER) { + return 1; + } + + // If actual is unspecified, it's compatible (untyped code is dynamic) + if (!actual || actual->kind == TYPE_INFER) { + return 1; + } + + // For function types, recursively check signatures + if (expected->kind == TYPE_FUNCTION && actual->kind == TYPE_FUNCTION) { + // Check async flag + if (expected->fn_is_async != actual->fn_is_async) { + return 0; + } + + // Check parameter count + if (expected->fn_num_params != actual->fn_num_params) { + return 0; + } + + // Check each parameter type + for (int i = 0; i < expected->fn_num_params; i++) { + Type *exp_param = expected->fn_param_types ? expected->fn_param_types[i] : NULL; + Type *act_param = actual->fn_param_types ? actual->fn_param_types[i] : NULL; + if (!types_compatible(exp_param, act_param)) { + return 0; + } + } + + // Check return type + if (!types_compatible(expected->fn_return_type, actual->fn_return_type)) { + return 0; + } + + return 1; + } + + // For array types, check element type compatibility + if (expected->kind == TYPE_ARRAY && actual->kind == TYPE_ARRAY) { + return types_compatible(expected->element_type, actual->element_type); + } + + // For custom object types, check by name + if (expected->kind == TYPE_CUSTOM_OBJECT && actual->kind == TYPE_CUSTOM_OBJECT) { + if (expected->type_name && actual->type_name) { + return strcmp(expected->type_name, actual->type_name) == 0; + } + return 1; // If either has no name, be permissive + } + + // For primitive types, they must match exactly or be in the same numeric family + if (expected->kind == actual->kind) { + return 1; + } + + // Allow numeric type conversions (signed/unsigned/float within families) + if (is_numeric_type(expected->kind) && is_numeric_type(actual->kind)) { + return 1; // Numeric types are compatible with each other + } + + return 0; +} + +// Validate that a function value matches a function type annotation +// Returns NULL if valid, or an error message string (caller must free) if invalid +static char* validate_function_signature(Type *expected_type, Function *fn) { + // Check async flag + if (expected_type->fn_is_async && !fn->is_async) { + return strdup("expected async function but got non-async function"); + } + if (!expected_type->fn_is_async && fn->is_async) { + return strdup("expected non-async function but got async function"); + } + + // Check parameter count + // The function must have at least as many parameters as expected + // (extra parameters are OK if the function has defaults for them) + int expected_params = expected_type->fn_num_params; + int actual_params = fn->num_params; + + // Count required parameters in function (those without defaults) + int fn_required = 0; + for (int i = 0; i < actual_params; i++) { + if (!fn->param_defaults || !fn->param_defaults[i]) { + fn_required++; + } + } + + // Count optional parameters in expected type + int expected_required = 0; + for (int i = 0; i < expected_params; i++) { + if (!expected_type->fn_param_optional || !expected_type->fn_param_optional[i]) { + expected_required++; + } + } + + // Function must be able to accept the expected number of arguments + if (actual_params < expected_required) { + char *msg = malloc(128); + sprintf(msg, "expected function with at least %d parameter(s) but got %d", + expected_required, actual_params); + return msg; + } + + // If expected type specifies more params than function has, that's an error + // (unless function has rest params) + if (expected_params > actual_params && !fn->rest_param) { + char *msg = malloc(128); + sprintf(msg, "expected function with %d parameter(s) but got %d", + expected_params, actual_params); + return msg; + } + + // Check parameter types for matching positions + int params_to_check = expected_params < actual_params ? expected_params : actual_params; + for (int i = 0; i < params_to_check; i++) { + Type *expected_param = expected_type->fn_param_types ? expected_type->fn_param_types[i] : NULL; + Type *actual_param = fn->param_types ? fn->param_types[i] : NULL; + + if (!types_compatible(expected_param, actual_param)) { + char *exp_str = format_type(expected_param); + char *act_str = format_type(actual_param); + char *msg = malloc(256); + sprintf(msg, "parameter %d: expected %s but got %s", i + 1, exp_str, act_str); + free(exp_str); + free(act_str); + return msg; + } + } + + // Check return type + if (!types_compatible(expected_type->fn_return_type, fn->return_type)) { + char *exp_str = format_type(expected_type->fn_return_type); + char *act_str = format_type(fn->return_type); + char *msg = malloc(256); + sprintf(msg, "return type: expected %s but got %s", exp_str, act_str); + free(exp_str); + free(act_str); + return msg; + } + + return NULL; // Valid +} + // Helper to convert a value to a target type Value convert_to_type(Value value, Type *target_type, Environment *env, ExecutionContext *ctx) { if (!target_type) { @@ -787,13 +1023,26 @@ Value convert_to_type(Value value, Type *target_type, Environment *env, Executio TypeKind kind = target_type->kind; - // Handle function types early - just verify it's a function + // Handle function types early - validate signature if (kind == TYPE_FUNCTION) { - if (value.type == VAL_FUNCTION) { - return value; // TODO: Could validate parameter/return types + if (value.type != VAL_FUNCTION) { + fprintf(stderr, "Runtime error: Expected function value\n"); + exit(1); } - fprintf(stderr, "Runtime error: Expected function value\n"); - exit(1); + + // Validate function signature + Function *fn = value.as.as_function; + char *error = validate_function_signature(target_type, fn); + if (error) { + char *expected_str = format_type(target_type); + fprintf(stderr, "Runtime error: Function signature mismatch: %s\n", error); + fprintf(stderr, " Expected: %s\n", expected_str); + free(expected_str); + free(error); + exit(1); + } + + return value; } // Handle object and enum types (both use TYPE_CUSTOM_OBJECT at parse time) @@ -1174,12 +1423,8 @@ Value convert_to_type(Value value, Type *target_type, Environment *env, Executio exit(1); case TYPE_FUNCTION: - // Function types are used for type annotations on callbacks - // At runtime, we just verify it's a function - if (value.type == VAL_FUNCTION) { - return value; - } - fprintf(stderr, "Runtime error: Expected function value\n"); + // Function types are handled earlier in this function (with signature validation) + fprintf(stderr, "Runtime error: Internal error - function type not handled properly\n"); exit(1); case TYPE_SELF: diff --git a/src/frontend/parser/ast.c b/src/frontend/parser/ast.c index 70c61595..2c88a631 100644 --- a/src/frontend/parser/ast.c +++ b/src/frontend/parser/ast.c @@ -364,6 +364,98 @@ Type* type_new(TypeKind kind) { return type; } +Type* type_copy(Type *src) { + if (!src) return NULL; + + Type *type = malloc(sizeof(Type)); + type->kind = src->kind; + type->nullable = src->nullable; + + // Copy type_name + type->type_name = src->type_name ? strdup(src->type_name) : NULL; + + // Copy element_type (for arrays) + type->element_type = type_copy(src->element_type); + + // Copy compound types (for intersection types) + type->num_compound_types = src->num_compound_types; + if (src->num_compound_types > 0 && src->compound_types) { + type->compound_types = malloc(sizeof(Type*) * src->num_compound_types); + for (int i = 0; i < src->num_compound_types; i++) { + type->compound_types[i] = type_copy(src->compound_types[i]); + } + } else { + type->compound_types = NULL; + } + + // Copy type_args (for generic types) + type->num_type_args = src->num_type_args; + if (src->num_type_args > 0 && src->type_args) { + type->type_args = malloc(sizeof(Type*) * src->num_type_args); + for (int i = 0; i < src->num_type_args; i++) { + type->type_args[i] = type_copy(src->type_args[i]); + } + } else { + type->type_args = NULL; + } + + // Copy function type fields + type->fn_num_params = src->fn_num_params; + type->fn_is_async = src->fn_is_async; + + if (src->fn_num_params > 0) { + // Copy fn_param_types + if (src->fn_param_types) { + type->fn_param_types = malloc(sizeof(Type*) * src->fn_num_params); + for (int i = 0; i < src->fn_num_params; i++) { + type->fn_param_types[i] = type_copy(src->fn_param_types[i]); + } + } else { + type->fn_param_types = NULL; + } + + // Copy fn_param_names + if (src->fn_param_names) { + type->fn_param_names = malloc(sizeof(char*) * src->fn_num_params); + for (int i = 0; i < src->fn_num_params; i++) { + type->fn_param_names[i] = src->fn_param_names[i] ? strdup(src->fn_param_names[i]) : NULL; + } + } else { + type->fn_param_names = NULL; + } + + // Copy fn_param_optional + if (src->fn_param_optional) { + type->fn_param_optional = malloc(sizeof(int) * src->fn_num_params); + memcpy(type->fn_param_optional, src->fn_param_optional, sizeof(int) * src->fn_num_params); + } else { + type->fn_param_optional = NULL; + } + + // Copy fn_param_is_const + if (src->fn_param_is_const) { + type->fn_param_is_const = malloc(sizeof(int) * src->fn_num_params); + memcpy(type->fn_param_is_const, src->fn_param_is_const, sizeof(int) * src->fn_num_params); + } else { + type->fn_param_is_const = NULL; + } + } else { + type->fn_param_types = NULL; + type->fn_param_names = NULL; + type->fn_param_optional = NULL; + type->fn_param_is_const = NULL; + } + + // Copy rest parameter + type->fn_rest_param_name = src->fn_rest_param_name ? strdup(src->fn_rest_param_name) : NULL; + type->fn_rest_param_type = type_copy(src->fn_rest_param_type); + + // Copy return type + type->fn_return_type = type_copy(src->fn_return_type); + + return type; +} + Type* type_compound(Type **types, int num_types) { Type *type = malloc(sizeof(Type)); type->kind = TYPE_COMPOUND; diff --git a/tests/parity/language/function_signature_validation.expected b/tests/parity/language/function_signature_validation.expected new file mode 100644 index 00000000..77ae8f46 --- /dev/null +++ b/tests/parity/language/function_signature_validation.expected @@ -0,0 +1,11 @@ +7 +true +20 +14 +300 +42 +logging works +15 +72 +50 +all signature validations passed diff --git a/tests/parity/language/function_signature_validation.hml b/tests/parity/language/function_signature_validation.hml new file mode 100644 index 00000000..09cf9783 --- /dev/null +++ b/tests/parity/language/function_signature_validation.hml @@ -0,0 +1,53 @@ +// Test function signature validation for function type annotations +// This tests that function type annotations properly validate signatures + +// 1. Valid: exact match +let exact_match: fn(i32, i32): i32 = fn(a: i32, b: i32): i32 { return a + b; }; +print(exact_match(3, 4)); + +// 2. Valid: untyped function matches typed annotation (dynamic compatibility) +let untyped_fn: fn(i32, string): bool = fn(n, s) { return n > 0; }; +print(untyped_fn(5, "hello")); + +// 3. Valid: numeric types are compatible (i32 ↔ i64 in same family) +let numeric_compat: fn(i64): i64 = fn(x: i32): i32 { return x * 2; }; +print(numeric_compat(10)); + +// 4. Valid: async function with async type +async fn async_double(n: i32): i32 { return n * 2; } +let async_handler: async fn(i32): i32 = async_double; +print(join(spawn(async_handler, 7))); + +// 5. Valid: function with fewer specified types (partial typing) +let partial_typed: fn(i32, i32): i32 = fn(a: i32, b) { return a + b; }; +print(partial_typed(100, 200)); + +// 6. Valid: function type as parameter (higher-order function) +fn apply_op(op: fn(i32, i32): i32, x: i32, y: i32): i32 { + return op(x, y); +} +print(apply_op(fn(a, b) { return a * b; }, 6, 7)); + +// 7. Valid: function type with no return type (void) +let logger: fn(string): void = fn(msg: string) { print(msg); }; +logger("logging works"); + +// 8. Valid: curried function type +fn make_adder(n: i32): fn(i32): i32 { + return fn(x: i32): i32 { return x + n; }; +} +let add10 = make_adder(10); +print(add10(5)); + +// 9. Valid: type alias with function type +type BinaryOp = fn(i32, i32): i32; +let mul: BinaryOp = fn(a, b) { return a * b; }; +print(mul(8, 9)); + +// 10. Valid: function with optional/default parameter accepts typed fn +fn run_with_default(cb: fn(i32): i32, val?: 100) { + print(cb(val)); +} +run_with_default(fn(x: i32): i32 { return x / 2; }); + +print("all signature validations passed");