/* * Checks that variables are assigned and have allowed values * * Copyright © 2025 Samuel Lidén Borell * * SPDX-License-Identifier: EUPL-1.2+ OR LGPL-2.1-or-later */ #include #include "compiler.h" static unsigned char scope_level = 0; static unsigned char in_else[MAX_SCOPE_LEVEL+1] = { 0 }; static int block_terminates = 0; static bool warned_about_unreachability = false; void varstate_function_start(void) { assert(scope_level == 0); block_terminates = 0; warned_about_unreachability = false; } void varstate_enter_scope(void) { if (scope_level >= MAX_SCOPE_LEVEL) { assert(scope_level == MAX_SCOPE_LEVEL); error("Scope nesting deeper than 100 levels."); } scope_level++; in_else[scope_level] = false; } static void check_maybe_unassigned(struct Var *var) { if (var->ifelse_assigned_level == scope_level) { /* Previous block leaves it unassigned */ error("Variable is assigned in one case but not " "the preceeding one"); var->assigned_at_level = OUT_OF_SCOPE; } } void varstate_leave_scope(enum ScopeLeaveKind kind) { struct Var *var; assert(scope_level > 0); for (var = current_func->vardecls; var; var = var->next) { if (var->declared_at_level == scope_level) { var->declared_at_level = OUT_OF_SCOPE; var->assigned_at_level = OUT_OF_SCOPE; } if (var->assigned_at_level == scope_level) { /* TODO don't "merge" varstate if block_terminates */ var->assigned_at_level--; switch (kind) { case SLK_NORMAL: check_maybe_unassigned(var); break; case SLK_NON_EXHAUSTIVE: if (!var->is_modifiable) { error("Final variable (non-`!`) is might be left in " "uninitialized state here"); } check_maybe_unassigned(var); var->ifelse_assigned_level = scope_level; break; case SLK_LOOP: if (!var->is_modifiable) { error("Loop modifies a final (non-`!`) variable"); } var->ifelse_assigned_level = scope_level; break; default: unreachable(); } } } scope_level--; if (block_terminates) { block_terminates--; } } void varstate_else(void) { struct Var *var; for (var = current_func->vardecls; var; var = var->next) { if (var->declared_at_level == scope_level) { var->declared_at_level = OUT_OF_SCOPE; var->assigned_at_level = OUT_OF_SCOPE; } if (var->assigned_at_level == scope_level) { /* TODO don't "merge" varstate if block_terminates */ check_maybe_unassigned(var); if (var->assigned_at_level != OUT_OF_SCOPE) { var->ifelse_assigned_level = scope_level; } } } in_else[scope_level] = true; if (block_terminates) { block_terminates--; } } static void terminate_block(void) { block_terminates++; warned_about_unreachability = false; } void varstate_return(void) { terminate_block(); } void varstate_break(void) { terminate_block(); /* TODO this one needs to find the containing loop */ } void varstate_continue(void) { terminate_block(); /* TODO this one needs to find the containing loop */ } void varstate_mark_declared(struct Var *var) { assert(var->declared_at_level == NOT_YET_DECLARED); var->declared_at_level = scope_level; } /* TODO there are already checks for this inside the parser. Check if that covers all cases, and if so, replace this one with an assert(). */ static void require_in_scope(const struct Var *var) { if (var->declared_at_level > scope_level) { assert(var->declared_at_level == NOT_YET_DECLARED || var->declared_at_level == OUT_OF_SCOPE); error(var->declared_at_level == NOT_YET_DECLARED ? "Variable hasn't been declared yet" : "Variable has gone out of scope here"); } } static bool in_any_else_upto(int upto_level) { int i; assert(upto_level >= 0 && upto_level <= MAX_SCOPE_LEVEL); for (i = scope_level; i > upto_level; i--) { if (in_else[i]) { return true; } } return false; } void varstate_mark_assigned(struct Var *var) { require_in_scope(var); if (!var->is_modifiable && var->assigned_at_level != NOT_YET_ASSIGNED && var->ifelse_assigned_level != scope_level) { error(var->ifelse_assigned_level == NOT_IFELSE_ASSIGNED ? "Variable is not marked with `!` and may not be modified" : "Nested conditional in else is too complex " "for variable initialization check"); } /* FIXME this will not work with e.g. if ... return else x = 1 end - this can be solved through a special varstate_noreturn(); function that sets a flag (for that scope, i.e. in a array), that in turn causes varstate_leave_scope/varstate_else to work differently (basically it should skip most/all checks and updates) - note that after return/continue/break, the block MUST end (or a warning should be reported). - also add checks that all code paths of a function return a value (if the function returns a value) */ /* This might be a bit too strict (it disallows declarations far out from the usage scope, if the usage is nested in if/else) */ if (in_any_else_upto(var->declared_at_level) && var->assigned_at_level >= scope_level && var->ifelse_assigned_level != scope_level) { error("Variable is not assigned in preceeding block"); } if (var->ifelse_assigned_level != scope_level) { if (var->assigned_at_level > scope_level) { var->assigned_at_level = scope_level; } } else { if (var->assigned_at_level < scope_level) { var->assigned_at_level = scope_level; } } var->ifelse_assigned_level = NOT_IFELSE_ASSIGNED; } void varstate_require_assigned(const struct Var *var) { if (var->is_funcparam || var->is_giveme) { return; } require_in_scope(var); if (var->assigned_at_level > scope_level || var->ifelse_assigned_level == scope_level) { error(var->assigned_at_level == NOT_YET_ASSIGNED ? "Variable hasn't been assigned yet" : "Variable might not be assigned here"); } } void varstate_warn_if_unreachable(void) { if (block_terminates != 0 && !warned_about_unreachability) { warning("Unreachable code"); warned_about_unreachability = true; } } /* TODO none-ness checking */