// SPDX-License-Identifier: MIT #include "typecheck_internal.h" void check_block(TypeChecker *tc, ASTNode *block, int depth) { tc_enter_scope(tc); ASTNode *stmt = block->block.statements; int seen_terminator = 0; Token terminator_token = {0}; while (stmt) { // Warn if we see code after a terminating statement if (seen_terminator && stmt->kind != NODE_LABEL) { const char *rule = tc->pctx->config->misra_mode ? "MISRA Rule 2.1: " : ""; const char *hints[] = {"Remove unreachable code or restructure control flow", NULL}; char msg[256]; snprintf(msg, sizeof(msg), "%sUnreachable code detected", rule); tc_error_with_hints(tc, stmt->token, msg, hints); seen_terminator = 0; // Only warn once per block } if (tc->pctx->config->misra_mode) { // Rule 2.2: There shall be no dead code (expressions with no effect) // We ignore call expressions here as they are handled by Rule 17.7 if (stmt->kind >= NODE_EXPR_BINARY && stmt->kind <= NODE_EXPR_SLICE && stmt->kind != NODE_EXPR_CALL) { if (!tc_expr_has_side_effects(stmt)) { tc_error(tc, stmt->token, "MISRA Rule 2.2: expression statement has no effect"); } } } int old_stmt_ctx = tc->is_stmt_context; tc->is_stmt_context = 1; check_node(tc, stmt, depth + 1); tc->is_stmt_context = old_stmt_ctx; // Track terminating statements if (stmt->kind == NODE_RETURN || stmt->kind == NODE_BREAK || stmt->kind == NODE_CONTINUE || stmt->kind == NODE_GOTO) { seen_terminator = 1; terminator_token = stmt->token; } stmt = stmt->next; } (void)terminator_token; // May be used for enhanced diagnostics later tc_exit_scope(tc); } void check_var_decl(TypeChecker *tc, ASTNode *node, int depth) { if (node->var_decl.type_info) { misra_check_pointer_nesting(tc->pctx, node->var_decl.type_info, node->token); } // MISRA: Mark type as used mark_type_as_used(tc, node->var_decl.type_info); // Initialize lifetime depth for escape analysis if this is a local variable if (node->var_decl.type_info && tc->current_func) { node->var_decl.type_info->lifetime_depth = tc->current_depth; } if (node->var_decl.init_expr) { if (node->type_info) { node->var_decl.init_expr->type_info = node->type_info; } int old_stmt_ctx = tc->is_stmt_context; tc->is_stmt_context = 0; check_node(tc, node->var_decl.init_expr, depth + 1); tc->is_stmt_context = old_stmt_ctx; Type *decl_type = node->var_decl.type_info; Type *init_type = node->var_decl.init_expr->type_info; if (decl_type && init_type) { apply_implicit_struct_pointer_conversions(tc, &node->var_decl.init_expr, decl_type); init_type = node->var_decl.init_expr->type_info; // The declared type retains its original lifetime metadata (from init or explicit decl) // If initialization exists, check compatibility if (node->var_decl.type_info) { check_type_compatibility(tc, node->var_decl.type_info, init_type, node->token, node->var_decl.init_expr, 0); } if (tc->pctx->config->misra_mode) { misra_check_pointer_conversion(tc->pctx, decl_type, init_type, node->token); // Rule 9.3: Arrays shall not be partially initialized. if (decl_type->kind == TYPE_ARRAY && init_type->kind == TYPE_ARRAY) { if (node->var_decl.init_expr->kind == NODE_EXPR_ARRAY_LITERAL) { if (decl_type->array_size != init_type->array_size) { tc_error(tc, node->token, "MISRA Rule 9.3"); } } } } } // Move Analysis: Check if the initializer moves a non-copy value. check_move_for_rvalue(tc, node->var_decl.init_expr); } if (node->type_info) { misra_check_pointer_nesting(tc->pctx, node->var_decl.type_info, node->token); } // If type is not explicit, we should ideally infer it from init_expr. Type *t = node->type_info; if (!t && node->var_decl.init_expr) { t = type_clone(node->var_decl.init_expr->type_info); // Ensure inferred local variables get the correct scope depth for escape analysis if (t && tc->current_func) { t->lifetime_depth = tc->current_depth; } node->type_info = t; node->var_decl.type_info = t; } misra_check_reserved_identifier(tc->pctx, node->var_decl.name, node->token); tc_add_symbol(tc, node->var_decl.name, t, node->token, 0); ZenSymbol *new_sym = tc_lookup(tc, node->var_decl.name); if (new_sym) { new_sym->is_static = node->var_decl.is_static; mark_symbol_valid(tc->pctx, new_sym, node); } if (tc->pctx->config->misra_mode && t && t->kind == TYPE_ARRAY) { // Rule 8.11: Array with external linkage shall have explicit size ZenSymbol *existing = tc_lookup(tc, node->var_decl.name); int is_static = (existing && existing->is_static) || (node->var_decl.is_static); int is_local = (existing && existing->is_local) || (tc->current_func != NULL); misra_check_external_array_size(tc->pctx, t, node->token, is_static, is_local); // Rule 18.8: No variable length arrays // In Zen C, all [T; N] arrays have constant size N, so Rule 18.8 is satisfied. // We only report if Zen somehow allowed non-constant sizes (which it doesn't in fixed-size // arrays). } } static ASTNode *loop_body(ASTNode *loop) { switch (loop->kind) { case NODE_LOOP: return loop->loop_stmt.body; case NODE_WHILE: return loop->while_stmt.body; case NODE_FOR: return loop->for_stmt.body; case NODE_FOR_RANGE: return loop->for_range.body; default: return NULL; } } // Returns 1 if `node` contains a `break` that can exit the loop whose label is // `loop_label` (NULL = unlabeled). `nested` is 1 while inside a loop nested // within the loop we are testing, so unlabeled breaks there do not count. static int stmt_has_exit_break(ASTNode *node, const char *loop_label, int nested) { if (!node) { return 0; } switch (node->kind) { case NODE_BREAK: if (node->break_stmt.target_label) { return loop_label != NULL && strcmp(node->break_stmt.target_label, loop_label) == 0; } return !nested; case NODE_BLOCK: for (ASTNode *s = node->block.statements; s; s = s->next) { if (stmt_has_exit_break(s, loop_label, nested)) { return 1; } } return 0; case NODE_IF: return stmt_has_exit_break(node->if_stmt.then_body, loop_label, nested) || stmt_has_exit_break(node->if_stmt.else_body, loop_label, nested); case NODE_MATCH: for (ASTNode *c = node->match_stmt.cases; c; c = c->next) { if (stmt_has_exit_break(c, loop_label, nested)) { return 1; } } return 0; case NODE_MATCH_CASE: return stmt_has_exit_break(node->match_case.body, loop_label, nested); case NODE_DEFER: return stmt_has_exit_break(node->defer_stmt.stmt, loop_label, nested); case NODE_LOOP: case NODE_WHILE: case NODE_FOR: case NODE_FOR_RANGE: // Nested loop: unlabeled breaks here target the nested loop, but a // labeled break naming our loop still exits ours. return stmt_has_exit_break(loop_body(node), loop_label, 1); case NODE_RETURN: case NODE_CONTINUE: default: return 0; } } // Returns 1 if `loop` (a NODE_LOOP) contains a `break` that can exit it. static int stmt_loop_has_exit_break(ASTNode *loop) { return stmt_has_exit_break(loop->loop_stmt.body, loop->loop_stmt.loop_label, 0); } int stmt_always_returns(ASTNode *stmt) { if (!stmt) { return 0; } switch (stmt->kind) { case NODE_RETURN: return 1; case NODE_BLOCK: return block_always_returns(stmt); case NODE_IF: // Both branches must return for if to always return if (stmt->if_stmt.then_body && stmt->if_stmt.else_body) { return stmt_always_returns(stmt->if_stmt.then_body) && stmt_always_returns(stmt->if_stmt.else_body); } return 0; case NODE_MATCH: { if (!stmt->match_stmt.cases) { return 0; } int has_default = 0; ASTNode *case_node = stmt->match_stmt.cases; while (case_node) { if (case_node->kind == NODE_MATCH_CASE) { if (!stmt_always_returns(case_node->match_case.body)) { return 0; } if (case_node->match_case.is_default) { has_default = 1; } } case_node = case_node->next; } return has_default; } case NODE_LOOP: // An infinite `loop` only leaves via `break` or `return`. With no // exit-break it can never fall through (it returns from within or // diverges), so it satisfies the always-returns requirement. This // mirrors the move analysis, which already treats a break-less // infinite loop as unreachable-after. return !stmt_loop_has_exit_break(stmt); default: return 0; } } int block_always_returns(ASTNode *block) { if (!block || block->kind != NODE_BLOCK) { return 0; } ASTNode *stmt = block->block.statements; if (!stmt) { return 0; } // Walk all statements except the last while (stmt->next) { if (stmt_always_returns(stmt)) { return 1; } stmt = stmt->next; } // The last statement is the implicit return value. // Check if it always returns (RETURN, IF with both branches, MATCH with default), // or if it's an expression (which serves as the function's return value). if (stmt_always_returns(stmt)) { return 1; } if (stmt->kind == NODE_BLOCK) { return block_always_returns(stmt); } // Any expression type can serve as an implicit return value if (stmt->kind == NODE_EXPR_BINARY || stmt->kind == NODE_EXPR_UNARY || stmt->kind == NODE_EXPR_LITERAL || stmt->kind == NODE_EXPR_VAR || stmt->kind == NODE_EXPR_CALL || stmt->kind == NODE_EXPR_MEMBER || stmt->kind == NODE_EXPR_INDEX || stmt->kind == NODE_EXPR_CAST || stmt->kind == NODE_EXPR_SIZEOF || stmt->kind == NODE_EXPR_STRUCT_INIT || stmt->kind == NODE_EXPR_ARRAY_LITERAL || stmt->kind == NODE_EXPR_TUPLE_LITERAL || stmt->kind == NODE_EXPR_SLICE || stmt->kind == NODE_TERNARY || stmt->kind == NODE_MATCH) { return 1; } return 0; } void check_function(TypeChecker *tc, ASTNode *node, int depth) { if (!node) { return; } misra_check_param_nesting(tc->pctx, node); // Mark arg types as used for (int i = 0; i < node->func.count; i++) { mark_type_as_used(tc, node->func.arg_types[i]); } // Mark return type as used mark_type_as_used(tc, node->func.ret_type_info); // Rule Zen 1.4: Reserved identifiers misra_check_reserved_identifier(tc->pctx, node->func.name, node->token); tc->current_func = node; tc_enter_scope(tc); int prev_unreachable = tc->is_unreachable; tc->is_unreachable = 0; tc->func_return_count = 0; MoveState *prev_move_state = tc->pctx->move_state; tc->pctx->move_state = move_state_create(NULL); for (int i = 0; i < node->func.count; i++) { if (node->func.param_names && node->func.param_names[i]) { Type *param_type = (node->func.arg_types && node->func.arg_types[i]) ? node->func.arg_types[i] : NULL; misra_check_tuple_size(tc->pctx, param_type, node->token); misra_check_pointer_nesting(tc->pctx, param_type, node->token); misra_check_reserved_identifier(tc->pctx, node->func.param_names[i], node->token); tc_add_symbol(tc, node->func.param_names[i], param_type, node->token, tc->pctx->config->misra_mode); } } if (node->func.ret_type_info) { misra_check_tuple_size(tc->pctx, node->func.ret_type_info, node->token); misra_check_pointer_nesting(tc->pctx, node->func.ret_type_info, node->token); // Lifetime Elision result was already computed in pre-pass for named functions. // For lambdas or if it somehow missed the pre-pass, ensure it's set. if (node->func.elide_from_idx == -1) { // Simple re-run if needed } } check_node(tc, node->func.body, depth + 1); // Control flow analysis: Check if non-void function always returns const char *ret_type = node->func.ret_type; int is_void = !ret_type || strcmp(ret_type, "void") == 0; // Special case: 'main' is allowed to fall off the end (C99 implicit return 0) int is_main = node->func.name && strcmp(node->func.name, "main") == 0; if (is_main && is_void) { warn_void_main(node->token); } if (!is_void && !is_main && node->func.body) { if (!block_always_returns(node->func.body)) { char msg[MAX_SHORT_MSG_LEN]; snprintf(msg, sizeof(msg), "Function '%s' may not return a value on all code paths", node->func.name); const char *hints[] = {"Ensure all execution paths return a value", "Consider adding a default return at the end of the function", NULL}; tc_ctrl_flow_error(tc, node->token, msg, hints); } } move_state_free(tc->pctx->move_state); tc->pctx->move_state = prev_move_state; // MISRA audits before leaving function scope if (tc->pctx->config->misra_mode && tc->pctx->current_scope) { for (int i = 0; i < node->func.count; i++) { if (node->func.param_names && node->func.param_names[i]) { ZenSymbol *psym = symbol_lookup_local(tc->pctx->current_scope, node->func.param_names[i]); if (psym) { // Rule 2.7: Unused parameter if (!psym->is_used) { misra_check_unused_param(tc->pctx, psym->name, psym->decl_token); } // Rule 8.13: Pointer to const if (psym->type_info && psym->type_info->kind == TYPE_POINTER) { Type *inner = resolve_alias(psym->type_info->inner); if (inner && !inner->is_const && !psym->type_info->is_const && !psym->is_written_to) { misra_check_const_ptr_param(tc->pctx, psym->name, psym->decl_token); } } } } } } // MISRA Rule 15.5: A function shall have a single point of exit at the end. if (tc->pctx->config->misra_mode && tc->func_return_count > 1) { char msg[MAX_ERROR_MSG_LEN]; snprintf(msg, sizeof(msg), "MISRA Rule 15.5: function '%s' has %d return points (must have 1)", node->func.name ? node->func.name : "anonymous", tc->func_return_count); tc_error(tc, node->token, msg); } tc->is_unreachable = prev_unreachable; tc_exit_scope(tc); tc->current_func = NULL; } void tc_check_trait(TypeChecker *tc, ASTNode *node, int depth) { ASTNode *method = node->trait.methods; while (method) { check_node(tc, method, depth + 1); method = method->next; } } void tc_check_impl(TypeChecker *tc, ASTNode *node, int depth) { // Skip templates if (node->impl.struct_name && strchr(node->impl.struct_name, '<')) { return; } ASTNode *method = node->impl.methods; while (method) { check_node(tc, method, depth + 1); method = method->next; } } void tc_check_impl_trait(TypeChecker *tc, ASTNode *node, int depth) { // Skip templates if (node->impl_trait.target_type && strchr(node->impl_trait.target_type, '<')) { return; } ASTNode *method = node->impl_trait.methods; while (method) { check_node(tc, method, depth + 1); method = method->next; } } void check_loop_passes(TypeChecker *tc, ASTNode *node, int depth) { if (!node) { return; } RECURSION_GUARD_TOKEN(tc->pctx, node->token, ); if (depth > 1024) { tc_error(tc, node->token, "Expression too deep"); RECURSION_EXIT(tc->pctx); return; } MoveState *prev_break = tc->loop_break_state; MoveState *prev_cont = tc->loop_continue_state; tc->loop_break_state = NULL; tc->loop_continue_state = NULL; MoveState *initial_state = tc->pctx->move_state; MoveState *loop_start = initial_state ? move_state_clone(initial_state) : NULL; MoveState *outer_start_state = tc->loop_start_state; tc->loop_start_state = loop_start; int outer_in_pass2 = tc->in_loop_pass2; tc->in_loop_pass2 = 0; int outer_break_count = tc->loop_break_count; tc->loop_break_count = 0; int initial_unreachable = tc->is_unreachable; // Pass 1: standard typecheck and move check tc->is_unreachable = 0; // The loop start is assumed reachable if we got here switch (node->kind) { case NODE_WHILE: { misra_check_compound_body(tc->pctx, node->while_stmt.body, "while"); int old_stmt_ctx = tc->is_stmt_context; tc->is_stmt_context = 0; check_node(tc, node->while_stmt.condition, depth + 1); tc->is_stmt_context = old_stmt_ctx; if (node->while_stmt.condition && node->while_stmt.condition->type_info) { Type *cond_type = resolve_alias(node->while_stmt.condition->type_info); if (tc->pctx->config->misra_mode) { if (cond_type->kind != TYPE_BOOL) { misra_check_condition_boolean(tc->pctx, node->while_stmt.condition->type_info, node->while_stmt.condition->token); } int inv; if (is_expression_invariant(tc, node->while_stmt.condition, &inv)) { misra_check_invariant_condition(tc->pctx, node->while_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[] = {"While conditions must be boolean, integer, or pointer", NULL}; tc_error_with_hints(tc, node->while_stmt.condition->token, "Condition must be a truthy type", hints); } } check_node(tc, node->while_stmt.body, depth + 1); break; } case NODE_FOR: misra_check_compound_body(tc->pctx, node->for_stmt.body, "for"); tc_enter_scope(tc); // For loop init variable is scoped check_node(tc, node->for_stmt.init, depth + 1); // Loop start is conceptually here for FOR if (loop_start) { move_state_free(loop_start); } loop_start = tc->pctx->move_state ? move_state_clone(tc->pctx->move_state) : NULL; tc->loop_start_state = loop_start; check_node(tc, node->for_stmt.condition, depth + 1); if (node->for_stmt.condition && node->for_stmt.condition->type_info) { Type *cond_type = resolve_alias(node->for_stmt.condition->type_info); if (tc->pctx->config->misra_mode) { if (cond_type->kind != TYPE_BOOL) { misra_check_condition_boolean(tc->pctx, node->for_stmt.condition->type_info, node->for_stmt.condition->token); } int inv; if (is_expression_invariant(tc, node->for_stmt.condition, &inv)) { misra_check_invariant_condition(tc->pctx, node->for_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[] = {"For conditions must be boolean, integer, or pointer", NULL}; tc_error_with_hints(tc, node->for_stmt.condition->token, "Condition must be a truthy type", hints); } } check_node(tc, node->for_stmt.body, depth + 1); check_node(tc, node->for_stmt.step, depth + 1); // step happens after body if (tc->pctx->config->misra_mode && node->for_stmt.step) { if (node->for_stmt.step->kind == NODE_EXPR_BINARY) { const char *step_op = node->for_stmt.step->binary.op; if (strstr(step_op, "=") && node->for_stmt.step->binary.left->type_info) { misra_check_loop_counter_float(tc->pctx, node->for_stmt.step->binary.left->type_info, node->for_stmt.step->token); } } } break; case NODE_FOR_RANGE: check_node(tc, node->for_range.start, depth + 1); check_node(tc, node->for_range.end, depth + 1); // Loop start conceptually here if (loop_start) { move_state_free(loop_start); } loop_start = tc->pctx->move_state ? move_state_clone(tc->pctx->move_state) : NULL; tc->loop_start_state = loop_start; check_node(tc, node->for_range.body, depth + 1); break; case NODE_LOOP: check_node(tc, node->loop_stmt.body, depth + 1); break; default: break; } tc->loop_break_count = 0; // Reset for pass 2 (avoid double counts) // Determine next iter state based on continue and fallthrough MoveState *fallthrough_state = tc->pctx->move_state; int fallthrough_unreachable = tc->is_unreachable; MoveState *next_iter_state = NULL; if (!fallthrough_unreachable && fallthrough_state) { move_state_merge_into(&next_iter_state, fallthrough_state); } if (tc->loop_continue_state) { move_state_merge_into(&next_iter_state, tc->loop_continue_state); } // Pass 2: Re-run with next_iter_state to catch use-after-move across iterations if (next_iter_state) { int prev_move_checks_only = tc->move_checks_only; tc->move_checks_only = 1; // suppress type errors tc->in_loop_pass2 = 1; tc->pctx->move_state = move_state_clone(next_iter_state); tc->is_unreachable = 0; tc->loop_break_state = NULL; tc->loop_continue_state = NULL; // Re-run appropriate parts switch (node->kind) { case NODE_WHILE: check_node(tc, node->while_stmt.condition, depth + 1); check_node(tc, node->while_stmt.body, depth + 1); break; case NODE_FOR: check_node(tc, node->for_stmt.condition, depth + 1); check_node(tc, node->for_stmt.body, depth + 1); check_node(tc, node->for_stmt.step, depth + 1); break; case NODE_FOR_RANGE: check_node(tc, node->for_range.body, depth + 1); break; case NODE_LOOP: check_node(tc, node->loop_stmt.body, depth + 1); break; default: break; } if (tc->pctx->move_state) { move_state_free(tc->pctx->move_state); } if (tc->loop_break_state) { move_state_free(tc->loop_break_state); } if (tc->loop_continue_state) { move_state_free(tc->loop_continue_state); } tc->move_checks_only = prev_move_checks_only; } // Compute final move state exiting the loop MoveState *final_state = NULL; // Loops can exit via condition falsification (next_iter_state) or breaks if (next_iter_state) { // Assume infinite loops (like NODE_LOOP) don't exit naturally unless broken, // but for safety we'll merge next_iter_state for all, treating condition as maybe false. if (node->kind != NODE_LOOP) { move_state_merge_into(&final_state, next_iter_state); } } if (tc->loop_break_state) { move_state_merge_into(&final_state, tc->loop_break_state); } // Cleanup Pass 1 states if (tc->loop_break_state) { move_state_free(tc->loop_break_state); } if (tc->loop_continue_state) { move_state_free(tc->loop_continue_state); } tc->loop_break_count = outer_break_count; if (next_iter_state) { move_state_free(next_iter_state); } if (loop_start) { move_state_free(loop_start); } // Restore outer context if (node->kind == NODE_FOR) { tc_exit_scope(tc); } // If the loop is an infinite loop and has no breaks, it is unconditionally unreachable after. if (node->kind == NODE_LOOP && !final_state) { tc->is_unreachable = 1; } else if (final_state) { tc->is_unreachable = 0; } else { tc->is_unreachable = initial_unreachable; } if (tc->pctx->move_state) { move_state_free(tc->pctx->move_state); } tc->pctx->move_state = final_state ? final_state : initial_state; tc->loop_break_state = prev_break; tc->loop_continue_state = prev_cont; tc->loop_start_state = outer_start_state; tc->in_loop_pass2 = outer_in_pass2; RECURSION_EXIT(tc->pctx); } // MAIN DISPATCH: check_node