Ctrl K
ComunidadGuías…

src/analysis/typecheck.c

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// SPDX-License-Identifier: MIT
#include "../utils/colors.h"
#include "typecheck_internal.h"
#include "../constants.h"

#include "typecheck.h"
#include "comptime_interpreter.h"
#include "diagnostics/diagnostics.h"
#include "move_check.h"
#include "platform/misra.h"
#include <ctype.h>
#include <string.h>

// External helpers from parser

// ** Internal Helpers **

// The variant a match pattern names. The parser stores it mangled
// (`Shape__Circle`); `Shape::Circle` and a bare `Circle` are accepted too. NULL
// for patterns that are not a single enum variant (`_`, `A || B`, literals).
static const char *match_pattern_variant(const char *pattern, const char *enum_name)
{
    if (!pattern || strstr(pattern, "||") || strchr(pattern, ',') || strstr(pattern, " or "))
    {
        return NULL;
    }
    size_t enum_len = enum_name ? strlen(enum_name) : 0;
    if (enum_len && strncmp(pattern, enum_name, enum_len) == 0)
    {
        const char *rest = pattern + enum_len;
        if (strncmp(rest, "__", 2) == 0 || strncmp(rest, "::", 2) == 0)
        {
            return rest + 2;
        }
    }
    const char *sep = NULL;
    const char *p = pattern;
    while ((p = strstr(p, "::")) != NULL)
    {
        sep = p;
        p += 2;
    }
    return sep ? sep + 2 : pattern;
}

// Type of the `index`-th of `count` bindings of a match case, read from the
// payload of the variant the pattern names. NULL when it cannot be told (not an
// enum, unknown variant, unresolved generic): the caller keeps UNSAFE_ANY.
static Type *match_binding_type(TypeChecker *tc, Type *scrutinee, const char *pattern, int index,
                                int count)
{
    Type *t = get_inner_type(scrutinee);
    while (t && t->kind == TYPE_POINTER)
    {
        t = get_inner_type(t->inner);
    }
    const char *variant = t ? match_pattern_variant(pattern, t->name) : NULL;
    if (!t || !t->name || !variant)
    {
        return NULL;
    }
    ASTNode *def = find_struct_def(tc->pctx, t->name);
    if (!def || def->kind != NODE_ENUM)
    {
        return NULL;
    }
    ASTNode *v = def->enm.variants;
    while (v)
    {
        if (v->kind == NODE_ENUM_VARIANT && v->variant.name &&
            strcmp(v->variant.name, variant) == 0)
        {
            Type *payload = v->variant.payload;
            if (!payload)
            {
                return NULL;
            }
            if (count == 1)
            {
                return payload;
            }
            // Several bindings destructure a tuple payload: one per field.
            ASTNode *tuple = payload->name ? find_struct_def(tc->pctx, payload->name) : NULL;
            if (!tuple || tuple->kind != NODE_STRUCT)
            {
                return NULL;
            }
            ASTNode *field = tuple->strct.fields;
            int i = 0;
            while (field)
            {
                if (field->kind == NODE_FIELD)
                {
                    if (i == index)
                    {
                        // Tuple structs registered for payloads only keep the
                        // field type as text.
                        if (!field->type_info && field->field.type)
                        {
                            return type_from_string_helper(tc->pctx, field->field.type);
                        }
                        return field->type_info;
                    }
                    i++;
                }
                field = field->next;
            }
            return NULL;
        }
        v = v->next;
    }
    return NULL;
}

void check_node(TypeChecker *tc, ASTNode *node, int depth)
{
    if (!node || !tc)
    {
        return;
    }
    RECURSION_GUARD_TOKEN(tc->pctx, node->token, );
    if (depth > 1024)
    {
        tc_error(tc, node->token, "Expression too deep");
        RECURSION_EXIT(tc->pctx);
        return;
    }

    switch (node->kind)
    {
    case NODE_ROOT:
    {
        ASTNode *child = node->root.children;
        while (child)
        {
            check_node(tc, child, depth + 1);
            child = child->next;
        }
    }
    break;
    case NODE_BLOCK:
        check_block(tc, node, depth + 1);
        break;
    case NODE_VAR_DECL:
        check_var_decl(tc, node, depth + 1);
        break;
    case NODE_FUNCTION:
        check_function(tc, node, depth + 1);
        break;
    case NODE_TRAIT:
        tc_check_trait(tc, node, depth + 1);
        break;
    case NODE_IMPL:
        tc_check_impl(tc, node, depth + 1);
        break;
    case NODE_IMPL_TRAIT:
        tc_check_impl_trait(tc, node, depth + 1);
        break;
    case NODE_IMPORT:
        check_node(tc, node->import_stmt.module_root, depth + 1);
        break;
    case NODE_EXPR_VAR:
        check_expr_var(tc, node);
        break;
    case NODE_EXPR_LITERAL:
        check_expr_literal(tc, node);
        break;
    case NODE_RETURN:
        tc->func_return_count++;
        if (node->ret.value)
        {
            // A returned expression is used: do not apply the statement-context
            // rules (notably Rule 17.7, unused return value) to calls inside it.
            int old_stmt_ctx = tc->is_stmt_context;
            tc->is_stmt_context = 0;
            check_node(tc, node->ret.value, depth + 1);
            tc->is_stmt_context = old_stmt_ctx;
        }
        // Check return type compatibility with function
        if (tc->current_func)
        {
            const char *ret_type = tc->current_func->func.ret_type;
            int func_is_void = !ret_type || strcmp(ret_type, "void") == 0;

            if (func_is_void && node->ret.value)
            {
                tc_error(tc, node->token, "Return with value in void function");
            }
            else if (!func_is_void && !node->ret.value)
            {
                char msg[MAX_SHORT_MSG_LEN];
                const char *rule = tc->pctx->config->misra_mode ? "MISRA Rule 2.1: " : "";
                snprintf(msg, sizeof(msg), "%sReturn without value in function returning '%s'",
                         rule, ret_type);
                tc_error(tc, node->token, msg);
            }
            else if (node->ret.value && tc->current_func->func.ret_type_info)
            {
                apply_implicit_struct_pointer_conversions(tc, &node->ret.value,
                                                          tc->current_func->func.ret_type_info);
                Type *returned = node->ret.value->type_info;
                Type *expected = get_inner_type(tc->current_func->func.ret_type_info);
                // In a method whose `self` is a pointer, `return self;` returns the
                // value: codegen emits `return *self;` (see codegen_stmt.c).
                if (returned && expected && returned->kind == TYPE_POINTER &&
                    expected->kind != TYPE_POINTER && node->ret.value->kind == NODE_EXPR_VAR &&
                    strcmp(node->ret.value->var_ref.name, "self") == 0)
                {
                    returned = returned->inner;
                }
                check_type_compatibility(tc, tc->current_func->func.ret_type_info, returned,
                                         node->token, node->ret.value, 0);
            }
        }
        tc->is_unreachable = 1;
        break;

    // Control flow with nested nodes.
    case NODE_IF:
    {
        int old_stmt_ctx = tc->is_stmt_context;
        tc->is_stmt_context = 0;
        check_node(tc, node->if_stmt.condition, depth + 1);
        tc->is_stmt_context = old_stmt_ctx;

        // Validate condition is boolean-compatible
        if (node->if_stmt.condition && node->if_stmt.condition->type_info)
        {
            Type *cond_type = resolve_alias(node->if_stmt.condition->type_info);
            if (tc->pctx->config->misra_mode)
            {
                misra_check_condition_boolean(tc->pctx, node->if_stmt.condition->type_info,
                                              node->if_stmt.condition->token);
                int inv;
                if (is_expression_invariant(tc, node->if_stmt.condition, &inv))
                {
                    misra_check_invariant_condition(tc->pctx, node->if_stmt.condition->token);
                }
            }
            else if (cond_type->kind != TYPE_BOOL && !is_integer_type(cond_type) &&
                     cond_type->kind != TYPE_POINTER && cond_type->kind != TYPE_STRING)
            {
                const char *hints[] = {"If conditions must be boolean, integer, or pointer", NULL};
                tc_error_with_hints(tc, node->if_stmt.condition->token,
                                    "Condition must be a truthy type", hints);
            }
        }

        MoveState *initial_state = tc->pctx->move_state;
        int initial_unreachable = tc->is_unreachable;

        misra_check_compound_body(tc->pctx, node->if_stmt.then_body, "if");
        if (node->if_stmt.else_body)
        {
            if (node->if_stmt.else_body->kind == NODE_IF)
            {
                misra_check_terminal_else(tc->pctx, node);
            }
            else
            {
                misra_check_compound_body(tc->pctx, node->if_stmt.else_body, "else");
            }
        }

        if (initial_state)
        {
            tc->pctx->move_state = move_state_clone(initial_state);
        }
        check_node(tc, node->if_stmt.then_body, depth + 1);
        MoveState *after_then = tc->pctx->move_state;
        int then_unreachable = tc->is_unreachable;

        MoveState *after_else = NULL;
        int else_unreachable = initial_unreachable;
        tc->is_unreachable = initial_unreachable; // Reset for else branch

        if (node->if_stmt.else_body)
        {
            if (initial_state)
            {
                tc->pctx->move_state = move_state_clone(initial_state);
            }
            check_node(tc, node->if_stmt.else_body, depth + 1);
            after_else = tc->pctx->move_state;
            else_unreachable = tc->is_unreachable;
        }

        tc->pctx->move_state = initial_state;

        if (initial_state)
        {
            MoveState *merge_a = then_unreachable ? NULL : after_then;
            MoveState *merge_b =
                else_unreachable ? NULL : (node->if_stmt.else_body ? after_else : initial_state);

            // Only merge reachable paths
            move_state_merge(initial_state, merge_a, merge_b);

            if (after_then)
            {
                move_state_free(after_then);
            }
            if (after_else)
            {
                move_state_free(after_else);
            }
        }

        tc->is_unreachable = then_unreachable && else_unreachable;
        break;
    }
    case NODE_MATCH:
        check_node(tc, node->match_stmt.expr, depth + 1);
        misra_check_match_stmt(tc->pctx, node);
        // Visit each match case
        {
            MoveState *match_initial_state = tc->pctx->move_state;
            MoveState *merged_state = NULL;
            int match_initial_unreachable = tc->is_unreachable;
            int all_unreachable = 1;

            ASTNode *mcase = node->match_stmt.cases;
            int has_default = 0;
            int clause_count = 0;
            (void)clause_count;

            while (mcase)
            {
                if (mcase->kind == NODE_MATCH_CASE)
                {
                    if (mcase->match_case.is_default)
                    {
                        has_default = 1;
                    }
                    clause_count++;

                    if (match_initial_state)
                    {
                        tc->pctx->move_state = move_state_clone(match_initial_state);
                    }
                    tc->is_unreachable = match_initial_unreachable;

                    tc_enter_scope(tc);
                    if (mcase->match_case.binding_count > 0)
                    {
                        for (int i = 0; i < mcase->match_case.binding_count; i++)
                        {
                            char *bname = mcase->match_case.binding_names[i];
                            if (bname)
                            {
                                Type *bt = match_binding_type(tc, node->match_stmt.expr->type_info,
                                                              mcase->match_case.pattern, i,
                                                              mcase->match_case.binding_count);
                                if (!bt)
                                {
                                    bt = type_new(TYPE_UNSAFE_ANY);
                                }
                                if (mcase->match_case.binding_refs &&
                                    mcase->match_case.binding_refs[i])
                                {
                                    bt = type_new_ptr(bt);
                                }
                                tc_add_symbol(tc, bname, bt, mcase->token, 0);
                            }
                        }
                    }

                    check_node(tc, mcase->match_case.body, depth + 1);
                    tc_exit_scope(tc);

                    // MISRA Rule 16.3: An unconditional break or return shall terminate every
                    // switch-clause
                    if (tc->pctx->config->misra_mode && !tc->is_unreachable &&
                        mcase->match_case.body && mcase->match_case.body->kind == NODE_BLOCK &&
                        mcase->match_case.body->block.statements)
                    {
                        tc_error(tc, mcase->token,
                                 "MISRA Rule 16.3: match case must end in break or return");
                    }

                    if (!tc->is_unreachable)
                    {
                        all_unreachable = 0;
                        if (tc->pctx->move_state)
                        {
                            move_state_merge_into(&merged_state, tc->pctx->move_state);
                        }
                    }

                    if (tc->pctx->move_state && tc->pctx->move_state != match_initial_state)
                    {
                        move_state_free(tc->pctx->move_state);
                    }
                }
                mcase = mcase->next;
            }

            if (!has_default)
            {
                all_unreachable = 0;
                if (match_initial_state)
                {
                    move_state_merge_into(&merged_state, match_initial_state);
                }

                if (!tc->pctx->config->misra_mode)
                {
                    const char *hints[] = {"Add a default '_' case to handle all possibilities",
                                           NULL};
                    tc_error_with_hints(tc, node->token,
                                        "Match may not be exhaustive (no default case)", hints);
                }

                misra_check_match_stmt(tc->pctx, node);
            }

            if (match_initial_state && merged_state)
            {
                tc->pctx->move_state = merged_state;
            }
            else if (!merged_state)
            {
                tc->pctx->move_state = match_initial_state;
            }

            tc->is_unreachable = all_unreachable;
        }
        break;
    case NODE_STRUCT:
    case NODE_ENUM:
    case NODE_TYPE_ALIAS:
        if (node->kind == NODE_STRUCT)
        {
            misra_check_reserved_identifier(tc->pctx, node->strct.name, node->token);
            misra_check_struct_decl(tc->pctx, node);
            if (node->strct.is_union)
            {
                misra_check_union(tc->pctx, node->token);
            }
        }
        else if (node->kind == NODE_ENUM)
        {
            misra_check_reserved_identifier(tc->pctx, node->enm.name, node->token);
        }
        else if (node->kind == NODE_TYPE_ALIAS)
        {
            misra_check_reserved_identifier(tc->pctx, node->type_alias.alias, node->token);
        }
        break;
    case NODE_WHILE:
    case NODE_FOR:
        check_loop_passes(tc, node, depth + 1);
        break;
    case NODE_EXPR_BINARY:
        check_expr_binary(tc, node, depth + 1);
        break;
    case NODE_EXPR_UNARY:
        check_expr_unary(tc, node, depth + 1);
        break;
    case NODE_EXPR_CALL:
        check_expr_call(tc, node, depth + 1);
        break;
    case NODE_EXPR_INDEX:
        check_node(tc, node->index.array, depth + 1);
        check_node(tc, node->index.index, depth + 1);

        if (node->index.array->type_info)
        {
            Type *t = node->index.array->type_info;
            int is_ptr = 0;
            if (t->kind == TYPE_POINTER && t->inner && t->inner->kind == TYPE_STRUCT)
            {
                t = t->inner;
                is_ptr = 1;
            }

            // Pointers must use direct array indexing (base[i]), not the
            // __index/__get operator overload: taking the address of the
            // overload's return value is not an lvalue. This matches the
            // parser/codegen policy ("Pointers should use array indexing by
            // default, not operator overload").
            if (t->kind == TYPE_STRUCT && t->name && !is_ptr)
            {
                size_t tname_len = strlen(t->name);
                char *mangled_idx = xmalloc(tname_len + sizeof("__index"));
                snprintf(mangled_idx, tname_len + sizeof("__index"), "%s__index", t->name);
                char *mangled_get = xmalloc(tname_len + sizeof("__get"));
                snprintf(mangled_get, tname_len + sizeof("__get"), "%s__get", t->name);

                FuncSig *sig = find_func(tc->pctx, mangled_idx);
                char *method_name = NULL;
                if (sig)
                {
                    method_name = "index";
                }
                else
                {
                    sig = find_func(tc->pctx, mangled_get);
                    if (sig)
                    {
                        method_name = "get";
                    }
                }

                if (method_name)
                {
                    ASTNode *array = node->index.array;
                    ASTNode *idx = node->index.index;

                    node->kind = NODE_EXPR_CALL;
                    memset(&node->call, 0, sizeof(node->call));

                    ASTNode *callee = ast_create(NODE_EXPR_MEMBER);
                    callee->token = node->token;
                    callee->member.target = array;
                    callee->member.field = xstrdup(method_name);
                    callee->member.is_pointer_access = is_ptr;

                    node->call.callee = callee;
                    node->call.args = idx;

                    check_expr_call(tc, node, depth + 1);
                    zfree(mangled_idx);
                    zfree(mangled_get);
                    break;
                }
                zfree(mangled_idx);
                zfree(mangled_get);
            }
            // The overload lookup above looks through a pointer to a struct;
            // the element type comes from the array/pointer type itself.
            if (is_ptr)
            {
                t = node->index.array->type_info;
            }
            if (t->kind == TYPE_ARRAY || t->kind == TYPE_POINTER || t->kind == TYPE_VECTOR)
            {
                if (t->kind == TYPE_VECTOR && !t->inner && t->name)
                {
                    ASTNode *def = find_struct_def(tc->pctx, t->name);
                    if (def && def->kind == NODE_STRUCT && def->strct.fields)
                    {
                        t->inner = def->strct.fields->type_info;
                    }
                }
                // Propagate lifetime from array/slice to the indexed element
                node->type_info = type_clone(t->inner);
                if (node->type_info && node->index.array->type_info)
                {
                    node->type_info->lifetime_depth = node->index.array->type_info->lifetime_depth;
                }
            }
        }

        // Validate index is integer
        if (node->index.index && node->index.index->type_info)
        {
            if (!is_integer_type(node->index.index->type_info))
            {
                const char *hints[] = {"Array indices must be integers", NULL};
                tc_error_with_hints(tc, node->index.index->token, "Non-integer array index", hints);
            }
        }
        break;
    case NODE_EXPR_MEMBER:
        if (node->member.field && strcmp(node->member.field, "forget") == 0)
        {
            // .forget() is a consuming operation: it is valid on a value that
            // was already moved into a container (e.g. `vec.push(x); x.forget();`),
            // so the use-after-move report is suppressed for the receiver.
            tc->is_forget_receiver = 1;
            check_node(tc, node->member.target, depth + 1);
            tc->is_forget_receiver = 0;
        }
        else
        {
            check_node(tc, node->member.target, depth + 1);
        }
        if (node->member.target && node->member.target->type_info)
        {
            Type *target_type = get_inner_type(node->member.target->type_info);
            if (target_type->kind == TYPE_STRUCT && target_type->name)
            {
                if (tc->pctx->config->misra_mode)
                {
                    ZenSymbol *struct_sym =
                        symbol_lookup_kind(tc->pctx->global_scope, target_type->name, SYM_STRUCT);
                    if (struct_sym)
                    {
                        struct_sym->is_dereferenced = 1;
                    }
                }
                ASTNode *struct_def = find_struct_def(tc->pctx, target_type->name);
                if (struct_def)
                {
                    ASTNode *field = struct_def->strct.fields;
                    while (field)
                    {
                        if (field->kind == NODE_FIELD && field->field.name &&
                            strcmp(field->field.name, node->member.field) == 0)
                        {
                            // Propagate lifetime from struct container to the member access result
                            node->type_info = type_clone(field->type_info);
                            if (node->type_info && node->member.target->type_info)
                            {
                                // Depth must be at least that of the container.
                                // (If field itself is static/global, it will be 0, but container's
                                // depth will override)
                                node->type_info->lifetime_depth =
                                    node->member.target->type_info->lifetime_depth;
                            }
                            break;
                        }
                        field = field->next;
                    }
                }
            }

            if (!node->type_info)
            {
                int is_ptr = 0;
                char *alloc_name = NULL;
                char *struct_name =
                    resolve_struct_name_from_type(tc->pctx, target_type, &is_ptr, &alloc_name);

                if (struct_name)
                {
                    char buf[MAX_ERROR_MSG_LEN];
                    snprintf(buf, sizeof(buf), "%s__%s", struct_name, node->member.field);
                    char *mangled = merge_underscores(buf);

                    FuncSig *sig = find_func(tc->pctx, mangled);
                    if (sig)
                    {
                        node->type_info = sig->ret_type;
                    }
                    zfree(mangled);
                }
                if (alloc_name)
                {
                    zfree(alloc_name);
                }
            }
        }
        if (!node->type_info)
        {
            // Fallback for failed lookups
            node->type_info = type_new(TYPE_UNKNOWN);
        }

        if (!tc->is_assign_lhs)
        {
            check_use_validity(tc, node);
        }
        break;
    case NODE_DEFER:
        // Check the deferred statement
        check_node(tc, node->defer_stmt.stmt, depth + 1);
        break;
    case NODE_EXPECT:
    case NODE_ASSERT:
        // Check assert/expect condition
        {
            int old_stmt_ctx = tc->is_stmt_context;
            tc->is_stmt_context = 0;
            check_node(tc, node->assert_stmt.condition, depth + 1);
            tc->is_stmt_context = old_stmt_ctx;
        }
        if (node->assert_stmt.condition && node->assert_stmt.condition->type_info)
        {
            Type *cond_type = resolve_alias(node->assert_stmt.condition->type_info);
            if (tc->pctx->config->misra_mode)
            {
                misra_check_condition_boolean(tc->pctx, node->assert_stmt.condition->type_info,
                                              node->assert_stmt.condition->token);
            }
            else if (cond_type->kind != TYPE_BOOL && !is_integer_type(cond_type) &&
                     cond_type->kind != TYPE_POINTER && cond_type->kind != TYPE_STRING)
            {
                const char *hints[] = {
                    "Assert/expect conditions must be boolean, integer, or pointer", NULL};
                tc_error_with_hints(tc, node->assert_stmt.condition->token,
                                    "Assert/expect condition must be a truthy type", hints);
            }
        }
        break;
    case NODE_TEST:
    {
        MoveState *prev_move_state = tc->pctx->move_state;
        tc->pctx->move_state = move_state_create(NULL);

        check_node(tc, node->test_stmt.body, depth + 1);

        move_state_free(tc->pctx->move_state);
        tc->pctx->move_state = prev_move_state;
        break;
    }

    case NODE_EXPR_CAST:
        // Check the expression being cast
        check_node(tc, node->cast.expr, depth + 1);
        // Could add cast safety checks here (e.g., narrowing, pointer-to-int)
        if (node->cast.expr && node->cast.expr->type_info && node->cast.target_type)
        {
            Type *source_type = resolve_alias(node->cast.expr->type_info);
            Type *target_type = type_from_string_helper(tc->pctx, node->cast.target_type);

            if (tc->pctx->config->misra_mode && target_type)
            {
                misra_check_cast(tc->pctx, target_type, source_type, node->token,
                                 is_composite_expression(node->cast.expr));
                misra_check_pointer_conversion(tc->pctx, target_type, source_type, node->token);
                misra_check_void_ptr_cast(tc->pctx, target_type, source_type, node->token);
                if (target_type->kind == TYPE_POINTER)
                {
                    misra_check_null_pointer_constant(tc->pctx, node, node->token);
                }
            }

            // Warn on pointer-to-integer casts (potential data loss)
            if (source_type->kind == TYPE_POINTER)
            {
                const char *target = node->cast.target_type;
                if (strcmp(target, "i8") == 0 || strcmp(target, "i16") == 0 ||
                    strcmp(target, "u8") == 0 || strcmp(target, "u16") == 0)
                {
                    const char *hints[] = {"Pointer-to-small-integer casts may lose address bits",
                                           NULL};
                    tc_error_with_hints(tc, node->token, "Potentially unsafe pointer cast", hints);
                }
            }
            node->type_info = target_type;
            mark_type_as_used(tc, target_type);
        }
        break;
    case NODE_EXPR_ARRAY_LITERAL:
    {
        misra_check_initializer_side_effects(tc->pctx, node);
        ASTNode *elem = node->array_literal.elements;
        Type *elem_type = NULL;
        int count = 0;
        while (elem)
        {
            check_node(tc, elem, depth + 1);
            if (!elem_type && elem->type_info && elem->type_info->kind != TYPE_UNKNOWN)
            {
                elem_type = elem->type_info;
            }
            count++;
            elem = elem->next;
        }
        if (elem_type)
        {
            node->type_info = type_new_array(elem_type, count);
        }
        else
        {
            node->type_info = type_new_array(type_new(TYPE_UNKNOWN), count);
        }
    }
    break;
    case NODE_EXPR_TUPLE_LITERAL:
    {
        misra_check_initializer_side_effects(tc->pctx, node);
        ASTNode *elem = node->tuple_literal.elements;
        while (elem)
        {
            check_node(tc, elem, depth + 1);
            elem = elem->next;
        }
    }
    break;
    case NODE_EXPR_STRUCT_INIT:
        misra_check_initializer_side_effects(tc->pctx, node);
        check_struct_init(tc, node, depth + 1);
        break;
    case NODE_LOOP:
        check_loop_passes(tc, node, depth + 1);
        break;
    case NODE_TERNARY:
        check_node(tc, node->ternary.cond, depth + 1);
        check_node(tc, node->ternary.true_expr, depth + 1);
        check_node(tc, node->ternary.false_expr, depth + 1);
        // Validate condition
        if (node->ternary.cond && node->ternary.cond->type_info)
        {
            Type *t = node->ternary.cond->type_info;
            if (tc->pctx->config->misra_mode)
            {
                misra_check_condition_boolean(tc->pctx, node->ternary.cond->type_info,
                                              node->ternary.cond->token);
                int inv;
                if (is_expression_invariant(tc, node->ternary.cond, &inv))
                {
                    misra_check_invariant_condition(tc->pctx, node->ternary.cond->token);
                }
            }
            else if (t->kind != TYPE_BOOL && !is_integer_type(t) && t->kind != TYPE_POINTER)
            {
                tc_error(tc, node->ternary.cond->token, "Ternary condition must be truthy");
            }
        }
        // Validate branch compatibility
        if (node->ternary.true_expr && node->ternary.false_expr)
        {
            Type *t1 = node->ternary.true_expr->type_info;
            Type *t2 = node->ternary.false_expr->type_info;
            if (t1 && t2)
            {
                // Loose compatibility check
                if (!check_type_compatibility(tc, t1, t2, node->token, NULL, 0))
                {
                    // Error reported by check_type_compatibility
                }
                else
                {
                    node->type_info = t1; // Inherit type
                }
            }
        }
        break;
    case NODE_ASM:
        for (int i = 0; i < node->asm_stmt.output_count; i++)
        {
            ZenSymbol *sym = tc_lookup(tc, node->asm_stmt.outputs[i]);
            if (!sym)
            {
                char msg[MAX_SHORT_MSG_LEN];
                if (tc->pctx->config->misra_mode)
                {
                    snprintf(msg, sizeof(msg),
                             "Undefined output variable in inline assembly: '%s' (MISRA Rule 17.3)",
                             node->asm_stmt.outputs[i]);
                }
                else
                {
                    snprintf(msg, sizeof(msg), "Undefined output variable in inline assembly: '%s'",
                             node->asm_stmt.outputs[i]);
                }
                tc_error(tc, node->token, msg);
            }
            else if (sym->type_info)
            {
                int width = get_asm_register_size(sym->type_info);
                if (width > node->asm_stmt.register_size)
                {
                    node->asm_stmt.register_size = width;
                }
            }
        }
        for (int i = 0; i < node->asm_stmt.input_count; i++)
        {
            ZenSymbol *sym = tc_lookup(tc, node->asm_stmt.inputs[i]);
            if (!sym)
            {
                char msg[MAX_SHORT_MSG_LEN];
                if (tc->pctx->config->misra_mode)
                {
                    snprintf(msg, sizeof(msg),
                             "Undefined input variable in inline assembly: '%s' (MISRA Rule 17.3)",
                             node->asm_stmt.inputs[i]);
                }
                else
                {
                    snprintf(msg, sizeof(msg), "Undefined input variable in inline assembly: '%s'",
                             node->asm_stmt.inputs[i]);
                }
                tc_error(tc, node->token, msg);
            }
            else if (sym->type_info)
            {
                int width = get_asm_register_size(sym->type_info);
                if (width > node->asm_stmt.register_size)
                {
                    node->asm_stmt.register_size = width;
                }
            }
        }
        if (node->asm_stmt.register_size > 64)
        {
            char msg[MAX_SHORT_MSG_LEN];
            snprintf(msg, sizeof(msg),
                     "Unsupported register size is required in inline assembly: %i bits",
                     node->asm_stmt.register_size);
            tc_error(tc, node->token, msg);
        }
        break;
    case NODE_LAMBDA:
        check_expr_lambda(tc, node, depth + 1);
        break;
    case NODE_EXPR_SIZEOF:
        if (node->size_of.expr)
        {
            check_node(tc, node->size_of.expr, depth + 1);

            if (tc->pctx->config->misra_mode && tc_expr_has_side_effects(node->size_of.expr))
            {
                misra_check_side_effects_sizeof(tc->pctx, node->size_of.expr);
            }
        }
        node->type_info = type_new(TYPE_I32);
        break;
    case NODE_FOR_RANGE:
        check_loop_passes(tc, node, depth + 1);
        break;
    case NODE_EXPR_SLICE:
        // Check slice target and indices
        check_node(tc, node->slice.array, depth + 1);
        check_node(tc, node->slice.start, depth + 1);
        check_node(tc, node->slice.end, depth + 1);
        break;
    case NODE_DESTRUCT_VAR:
        if (node->destruct.init_expr)
        {
            check_node(tc, node->destruct.init_expr, depth + 1);
        }
        break;
    case NODE_BREAK:
        if (tc->loop_break_count > 0)
        {
            misra_check_iteration_termination(tc->pctx, node->token);
        }
        tc->loop_break_count++;

        if (tc->move_checks_only)
        {
            // No-op
        }
        else if (tc->pctx->move_state)
        {
            move_state_merge_into(&tc->loop_break_state, tc->pctx->move_state);
        }
        tc->is_unreachable = 1;
        break;
    case NODE_GOTO:
        if (tc->pctx->config->misra_mode)
        {
            ZenSymbol *lbl = tc_lookup(tc, node->goto_stmt.label_name);
            if (lbl && lbl->decl_token.line != 0)
            {
                misra_check_goto_constraint(tc->pctx, node->token, lbl->decl_token);
            }
        }
        misra_check_goto(tc->pctx, node->token);
        tc->is_unreachable = 1;
        break;

    case NODE_CONTINUE:
        if (tc->pctx->move_state)
        {
            move_state_merge_into(&tc->loop_continue_state, tc->pctx->move_state);
        }
        tc->is_unreachable = 1;
        break;
    case NODE_VA_START:
    case NODE_VA_END:
    case NODE_VA_COPY:
    case NODE_VA_ARG:
        misra_check_stdarg(tc->pctx, node->token);
        break;

    case NODE_RAW_STMT:
        misra_check_raw_block(tc->pctx, node->token);
        if (node->raw_stmt.interp_expr_count > 0)
        {
            // The `{...}` expressions of an f-string were parsed and typed when the
            // string was; check them only for moves (use after move, consumed values).
            int prev_move_checks_only = tc->move_checks_only;
            tc->move_checks_only = 1;
            ASTNode **expr = node->raw_stmt.interp_exprs;
            ASTNode **end = expr + node->raw_stmt.interp_expr_count;
            for (; expr < end; expr++)
            {
                check_node(tc, *expr, depth + 1);
            }
            tc->move_checks_only = prev_move_checks_only;
        }
        break;
    case NODE_PREPROC_DIRECTIVE:
        // Rule Zen 1.4 is already handled by parser_audit_preprocessor
        break;
    case NODE_PLUGIN:
        misra_check_plugin_block(tc->pctx, node->token);
        break;
    case NODE_LABEL:
        if (tc->pctx->config->misra_mode)
        {
            ZenSymbol *lbl =
                symbol_add(tc->pctx->current_scope, node->label_stmt.label_name, SYM_LABEL);
            if (lbl)
            {
                lbl->decl_token = node->token;
            }
        }
        break;
    case NODE_COMPTIME:
    {
        // Register comptime builtins for the body
        register_comptime_builtins(tc->pctx);

        // Type-check the comptime body
        ASTNode *stmt = node->comptime.body;
        while (stmt)
        {
            check_node(tc, stmt, depth + 1);
            stmt = stmt->next;
        }

        // Interpret the comptime body
        char *output =
            interpret_comptime(tc->pctx, node->comptime.body, tc->pctx->current_filename);
        if (!output)
        {
            break;
        }

        // Parse generated source code
        if (output[0])
        {
            if (node->comptime.in_block)
            {
                // Inside a function the output is statements, not declarations.
                size_t len = strlen(output);
                char *wrapped = xmalloc(len + 5);
                snprintf(wrapped, len + 5, "{ %s }", output);
                Lexer out_l;
                lexer_init(&out_l, wrapped, tc->pctx->config, tc->pctx->current_filename);
                ASTNode *block = parse_block(tc->pctx, &out_l);
                node->comptime.generated = block ? block->block.statements : NULL;
            }
            else
            {
                Lexer out_l;
                lexer_init(&out_l, output, tc->pctx->config, tc->pctx->current_filename);
                node->comptime.generated = parse_program_nodes(tc->pctx, &out_l);
            }

            // Type-check generated nodes
            ASTNode *gen = node->comptime.generated;
            while (gen)
            {
                check_node(tc, gen, depth + 1);
                gen = gen->next;
            }
        }
        zfree(output);
        break;
    }

    default:
        // Generic recursion for lists and other nodes.
        // Special case for Return to trigger move?
        if (node->kind == NODE_RETURN && node->ret.value)
        {
            // If returning a value, check if it can be moved.
            check_move_for_rvalue(tc, node->ret.value);
        }
        break;
    }
    RECURSION_EXIT(tc->pctx);
}

static void infer_node_lifetime(TypeChecker *tc, ASTNode *node)
{
    if (!node || node->kind != NODE_FUNCTION)
    {
        return;
    }

    FuncSig *fsig = find_func(tc->pctx, node->func.name);
    if (!fsig)
    {
        return;
    }

    // Default to local argument scope (depth 1)
    int inferred_depth = 1;
    int ptr_param_count = 0;
    int self_depth = -1;
    int elide_idx = -1;

    for (int i = 0; i < fsig->total_args; i++)
    {
        Type *t = (fsig->arg_types && fsig->arg_types[i]) ? fsig->arg_types[i] : NULL;
        if (t && t->kind == TYPE_POINTER)
        {
            ptr_param_count++;
            // Parameters are always at least depth 1 (argument scope)
            if (t->lifetime_depth == 0)
            {
                t->lifetime_depth = 1;
            }

            if (node->func.param_names && node->func.param_names[i] &&
                strcmp(node->func.param_names[i], "self") == 0)
            {
                self_depth = t->lifetime_depth;
                elide_idx = i;
            }
        }
    }

    if (self_depth != -1)
    {
        inferred_depth = self_depth;
    }
    else if (ptr_param_count == 1)
    {
        for (int i = 0; i < fsig->total_args; i++)
        {
            Type *t = (fsig->arg_types && fsig->arg_types[i]) ? fsig->arg_types[i] : NULL;
            if (t && t->kind == TYPE_POINTER)
            {
                inferred_depth = t->lifetime_depth;
                elide_idx = i;
                break;
            }
        }
    }

    node->func.elide_from_idx = elide_idx;
    fsig->elide_from_idx = elide_idx;

    // Update the return type depth in the signature
    if (fsig->ret_type && fsig->ret_type->kind == TYPE_POINTER)
    {
        fsig->ret_type->lifetime_depth = inferred_depth;
    }

    // Also update AST node if types are already resolved there
    if (node->func.ret_type_info && node->func.ret_type_info->kind == TYPE_POINTER)
    {
        node->func.ret_type_info->lifetime_depth = inferred_depth;
    }
}

static void check_program_prepass(TypeChecker *tc, ASTNode *root, int depth)
{
    if (!root || root->kind != NODE_ROOT)
    {
        return;
    }
    RECURSION_GUARD_TOKEN(tc->pctx, root->token, );

    if (depth > 64)
    {
        RECURSION_EXIT(tc->pctx);
        return;
    }

    ASTNode *n = root->root.children;
    while (n)
    {
        if (n->kind == NODE_ROOT)
        {
            check_program_prepass(tc, n, depth + 1);
        }
        else if (n->kind == NODE_FUNCTION)
        {
            infer_node_lifetime(tc, n);
        }
        else if (n->kind == NODE_IMPL)
        {
            ASTNode *method = n->impl.methods;
            while (method)
            {
                if (method->kind == NODE_FUNCTION)
                {
                    infer_node_lifetime(tc, method);
                }
                method = method->next;
            }
        }
        else if (n->kind == NODE_IMPL_TRAIT)
        {
            ASTNode *method = n->impl_trait.methods;
            while (method)
            {
                if (method->kind == NODE_FUNCTION)
                {
                    infer_node_lifetime(tc, method);
                }
                method = method->next;
            }
        }
        else if (n->kind == NODE_IMPORT)
        {
            // Imports are conceptually ROOTs of their own module
            check_program_prepass(tc, n->import_stmt.module_root, depth + 1);
        }
        n = n->next;
    }
    RECURSION_EXIT(tc->pctx);
}

// ** Entry Point **

int check_program(ParserContext *ctx, ASTNode *root)
{
    TypeChecker tc = {0};
    tc.pctx = ctx;
    ctx->current_scope = ctx->global_scope;

    if (!ctx->move_state)
    {
        ctx->move_state = move_state_create(NULL);
    }

    check_program_prepass(&tc, root, 0);

    check_node(&tc, root, 0);
    root->is_checked = 1;

    // Fixed-point iteration to handle secondary instantiations
    int changed = 1;
    while (changed)
    {
        changed = 0;
        ASTNode *inst_func = ctx->instantiated_funcs;
        while (inst_func)
        {
            if (!inst_func->is_checked)
            {
                check_node(&tc, inst_func, 0);
                inst_func->is_checked = 1;
                changed = 1;
                // Restart from head to catch newly added instantiations that might be before us
                break;
            }
            inst_func = inst_func->next;
        }
    }

    if (ctx->move_state)
    {
        move_state_free(ctx->move_state);
        ctx->move_state = NULL;
    }

    if (tc.pctx->config->misra_mode)
    {
        misra_audit_unused_symbols(tc.pctx);
        misra_audit_block_scope(tc.pctx);
        misra_audit_identifier_uniqueness(tc.pctx);
    }

    if (ctx->compiler->error_count > 0)
    {
        fprintf(stderr,
                COLOR_BOLD COLOR_RED "     error" COLOR_RESET
                                     ": semantic analysis found %d error%s\n",
                ctx->compiler->error_count, ctx->compiler->error_count == 1 ? "" : "s");
        return 1;
    }

    return 0;
}

int check_moves_only(ParserContext *ctx, ASTNode *root)
{
    TypeChecker tc = {0};
    tc.pctx = ctx;
    tc.move_checks_only = 1;
    ctx->current_scope = ctx->global_scope;

    if (!ctx->move_state)
    {
        ctx->move_state = move_state_create(NULL);
    }

    check_node(&tc, root, 0);

    if (ctx->move_state)
    {
        move_state_free(ctx->move_state);
        ctx->move_state = NULL;
    }

    return ctx->compiler->error_count;
}