From 34365bda5c116dc3a794c594f881d12ea9bc440e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Sat, 30 May 2026 01:33:24 +0300 Subject: [PATCH 1/3] feat: add block scoping for if/elif/else and match arms Scope is-bindings and pattern bindings to their own block frame, reusing the while-block scope machinery: each if/elif/else branch and each match arm pushes a frame on entry and pops it at the next branch boundary or `fi`/`end`. A binding is visible only inside its branch/arm, never in a sibling branch, the else, or after the block. Implicit bindings that would shadow an accessible block-local or function-level variable are rejected (DuplicateName); globals remain a separate, exempt namespace and explicit `= x` may still mutate an outer variable. Refactor every compiler, stdlib, and lsp site that relied on a binding leaking past its block (the early-return guard idiom) to the positive form `if X Variant(b) is then fi` or an `.unwrap` hoist, so the compiler bootstraps under the stricter rules. Closes #248 Closes #249 --- compiler/bytecode.casa | 469 ++++++------- compiler/lexer.casa | 14 +- compiler/pattern.casa | 16 + compiler/syntax.casa | 89 ++- compiler/typechecker.casa | 638 +++++++++--------- lib/argparse.casa | 46 +- lsp.casa | 16 +- tests/compiler/errors/if_binding_shadow.casa | 14 + .../errors/if_is_binding_out_of_scope.casa | 14 + .../match_arm_binding_out_of_scope.casa | 14 + .../compiler/errors/match_binding_shadow.casa | 15 + tests/compiler/test_scope.casa | 186 +++++ 12 files changed, 908 insertions(+), 623 deletions(-) create mode 100644 tests/compiler/errors/if_binding_shadow.casa create mode 100644 tests/compiler/errors/if_is_binding_out_of_scope.casa create mode 100644 tests/compiler/errors/match_arm_binding_out_of_scope.casa create mode 100644 tests/compiler/errors/match_binding_shadow.casa diff --git a/compiler/bytecode.casa b/compiler/bytecode.casa index a1a36fb..eee3fd0 100644 --- a/compiler/bytecode.casa +++ b/compiler/bytecode.casa @@ -544,38 +544,39 @@ impl BytecodeCompiler { next_arm:Option[int] bytecode:List[InstValue] { - if op.value OpValue::MatchArm(arm) is ! then return fi - arm.pattern_value match - PatternValue::Struct(struct_pattern) => { - bytecode struct_pattern compiler.emit_struct_arm - } - PatternValue::Literal(literal_pattern) => { - bytecode next_arm is_last literal_pattern compiler.emit_literal_arm - } - PatternValue::EnumVariant(variant) => { - op pattern_is_wildcard = is_wildcard - bytecode next_arm is_last is_wildcard variant compiler.emit_enum_arm - } - end - arm.guard_ops = guard_ops - if guard_ops.length 0 != then - 0 = guard_index - while guard_index guard_ops.length > do - guard_index guard_ops.get = guard_op - bytecode guard_index guard_op compiler.compile_op - 1 += guard_index - done - # Guarded last-arm is rejected by the typechecker (a guarded arm - # does not cover its pattern, so a non-guarded fallback is - # required). Codegen may still run in RESILIENT_MODE after such - # an error, so skip the fail-jump rather than unwrapping None. - if next_arm Option::Some(label) is then - label InstValue::JumpNe bytecode.push - else - InstValue::Drop bytecode.push + if op.value OpValue::MatchArm(arm) is then + arm.pattern_value match + PatternValue::Struct(struct_pattern) => { + bytecode struct_pattern compiler.emit_struct_arm + } + PatternValue::Literal(literal_pattern) => { + bytecode next_arm is_last literal_pattern compiler.emit_literal_arm + } + PatternValue::EnumVariant(variant) => { + op pattern_is_wildcard = is_wildcard + bytecode next_arm is_last is_wildcard variant compiler.emit_enum_arm + } + end + arm.guard_ops = guard_ops + if guard_ops.length 0 != then + 0 = guard_index + while guard_index guard_ops.length > do + guard_index guard_ops.get = guard_op + bytecode guard_index guard_op compiler.compile_op + 1 += guard_index + done + # Guarded last-arm is rejected by the typechecker (a guarded arm + # does not cover its pattern, so a non-guarded fallback is + # required). Codegen may still run in RESILIENT_MODE after such + # an error, so skip the fail-jump rather than unwrapping None. + if next_arm Option::Some(label) is then + label InstValue::JumpNe bytecode.push + else + InstValue::Drop bytecode.push + fi fi + InstValue::Drop bytecode.push fi - InstValue::Drop bytecode.push } } @@ -622,21 +623,22 @@ impl BytecodeCompiler { impl BytecodeCompiler { fn compile_struct_new compiler:BytecodeCompiler op:Op bytecode:List[InstValue] { - if op.value OpValue::StructNew(casa_struct) is ! then return fi - casa_struct CasaStruct::members.length = count - count WORD_SIZE * InstValue::Push bytecode.push - InstValue::HeapAlloc bytecode.push - 0 = index - while index count > do - InstValue::Swap bytecode.push - InstValue::Over bytecode.push - if 0 index > then - index WORD_SIZE * InstValue::Push bytecode.push - InstValue::Add bytecode.push - fi - InstValue::Store64 bytecode.push - 1 += index - done + if op.value OpValue::StructNew(casa_struct) is then + casa_struct CasaStruct::members.length = count + count WORD_SIZE * InstValue::Push bytecode.push + InstValue::HeapAlloc bytecode.push + 0 = index + while index count > do + InstValue::Swap bytecode.push + InstValue::Over bytecode.push + if 0 index > then + index WORD_SIZE * InstValue::Push bytecode.push + InstValue::Add bytecode.push + fi + InstValue::Store64 bytecode.push + 1 += index + done + fi } } @@ -709,64 +711,65 @@ impl BytecodeCompiler { impl BytecodeCompiler { fn compile_is_check compiler:BytecodeCompiler op:Op bytecode:List[InstValue] { - if op.value OpValue::IsCheck(variant) is ! then return fi - variant EnumVariant::enum_name = enum_name - enum_name compiler.store.enums.get = casa_enum_opt - if casa_enum_opt.is_none then - empty_location "Expected option value while unwrapping enum_name compiler.store.enums.get" ErrorKind::Syntax raise_error - 1 exit - fi - casa_enum_opt.unwrap = casa_enum - - if casa_enum has_inner_values ! then - # Plain enum: compare ordinal directly - variant enum_variant_ordinal InstValue::Push bytecode.push - InstValue::Eq bytecode.push - return - fi - - if 0 variant EnumVariant::bindings.length == then - # Data-carrying enum, no bindings: load ordinal and compare - InstValue::Load64 bytecode.push - variant enum_variant_ordinal InstValue::Push bytecode.push - InstValue::Eq bytecode.push - return - fi + if op.value OpValue::IsCheck(variant) is then + variant EnumVariant::enum_name = enum_name + enum_name compiler.store.enums.get = casa_enum_opt + if casa_enum_opt.is_none then + empty_location "Expected option value while unwrapping enum_name compiler.store.enums.get" ErrorKind::Syntax raise_error + 1 exit + fi + casa_enum_opt.unwrap = casa_enum - # Data-carrying enum with bindings - compiler.locals_count = temp_ptr - compiler.locals_count 1 + compiler->locals_count + if casa_enum has_inner_values ! then + # Plain enum: compare ordinal directly + variant enum_variant_ordinal InstValue::Push bytecode.push + InstValue::Eq bytecode.push + return + fi - compiler.new_label = skip_bindings + if 0 variant EnumVariant::bindings.length == then + # Data-carrying enum, no bindings: load ordinal and compare + InstValue::Load64 bytecode.push + variant enum_variant_ordinal InstValue::Push bytecode.push + InstValue::Eq bytecode.push + return + fi - # Save enum pointer - temp_ptr InstValue::LocalSet bytecode.push + # Data-carrying enum with bindings + compiler.locals_count = temp_ptr + compiler.locals_count 1 + compiler->locals_count - # Load ordinal and compare - temp_ptr InstValue::LocalGet bytecode.push - InstValue::Load64 bytecode.push - variant enum_variant_ordinal InstValue::Push bytecode.push - InstValue::Eq bytecode.push + compiler.new_label = skip_bindings - # Duplicate bool - InstValue::Dup bytecode.push - skip_bindings InstValue::JumpNe bytecode.push + # Save enum pointer + temp_ptr InstValue::LocalSet bytecode.push - # Extract bindings - 0 = index - while index variant EnumVariant::bindings.length > do - index 1 + WORD_SIZE * = offset + # Load ordinal and compare temp_ptr InstValue::LocalGet bytecode.push - offset InstValue::Push bytecode.push - InstValue::Add bytecode.push InstValue::Load64 bytecode.push - index variant EnumVariant::bindings.get = var_name - var_name compiler.resolve_variable = resolved - bytecode resolved.emit_set - 1 += index - done + variant enum_variant_ordinal InstValue::Push bytecode.push + InstValue::Eq bytecode.push + + # Duplicate bool + InstValue::Dup bytecode.push + skip_bindings InstValue::JumpNe bytecode.push + + # Extract bindings + 0 = index + while index variant EnumVariant::bindings.length > do + index 1 + WORD_SIZE * = offset + temp_ptr InstValue::LocalGet bytecode.push + offset InstValue::Push bytecode.push + InstValue::Add bytecode.push + InstValue::Load64 bytecode.push + index variant EnumVariant::bindings.get = var_name + var_name compiler.resolve_variable = resolved + bytecode resolved.emit_set + 1 += index + done - skip_bindings InstValue::Label bytecode.push + skip_bindings InstValue::Label bytecode.push + fi } } @@ -776,52 +779,53 @@ impl BytecodeCompiler { impl BytecodeCompiler { fn compile_struct_literal compiler:BytecodeCompiler op:Op bytecode:List[InstValue] { - if op.value OpValue::StructLiteral(literal) is ! then return fi - literal StructLiteral::struct_name compiler.store.structs.get = casa_struct_opt - if casa_struct_opt.is_none then - empty_location "Expected option value while unwrapping literal StructLiteral::struct_name compiler.store.structs.get" ErrorKind::Syntax raise_error - 1 exit - fi - casa_struct_opt.unwrap = casa_struct - casa_struct CasaStruct::members.length = count - - # Store all field values into temp locals (top of stack = last in field_order) - # Iterate field_order in reverse - literal StructLiteral::field_order.length 1 - = field_index - Map::new(Map[str int]) = temp_locals - while 0 field_index >= do - compiler.locals_count = local_index - compiler.locals_count 1 + compiler->locals_count - field_index literal StructLiteral::field_order.get = field_name - local_index field_name temp_locals.set = temp_locals - local_index InstValue::LocalSet bytecode.push - field_index 1 - = field_index - done - - # Allocate struct on heap - count WORD_SIZE * InstValue::Push bytecode.push - InstValue::HeapAlloc bytecode.push - - # Store each field at its correct offset - 0 = member_index - while member_index casa_struct CasaStruct::members.length > do - member_index casa_struct CasaStruct::members.get Member::name = member_name - member_index WORD_SIZE * = member_offset - InstValue::Dup bytecode.push - if 0 member_offset > then - member_offset InstValue::Push bytecode.push - InstValue::Add bytecode.push - fi - if member_name temp_locals.get Option::Some(temp_local) is then - temp_local InstValue::LocalGet bytecode.push - else - empty_location "Expected struct literal temp local" ErrorKind::Syntax raise_error + if op.value OpValue::StructLiteral(literal) is then + literal StructLiteral::struct_name compiler.store.structs.get = casa_struct_opt + if casa_struct_opt.is_none then + empty_location "Expected option value while unwrapping literal StructLiteral::struct_name compiler.store.structs.get" ErrorKind::Syntax raise_error 1 exit fi - InstValue::Swap bytecode.push - InstValue::Store64 bytecode.push - 1 += member_index - done + casa_struct_opt.unwrap = casa_struct + casa_struct CasaStruct::members.length = count + + # Store all field values into temp locals (top of stack = last in field_order) + # Iterate field_order in reverse + literal StructLiteral::field_order.length 1 - = field_index + Map::new(Map[str int]) = temp_locals + while 0 field_index >= do + compiler.locals_count = local_index + compiler.locals_count 1 + compiler->locals_count + field_index literal StructLiteral::field_order.get = field_name + local_index field_name temp_locals.set = temp_locals + local_index InstValue::LocalSet bytecode.push + field_index 1 - = field_index + done + + # Allocate struct on heap + count WORD_SIZE * InstValue::Push bytecode.push + InstValue::HeapAlloc bytecode.push + + # Store each field at its correct offset + 0 = member_index + while member_index casa_struct CasaStruct::members.length > do + member_index casa_struct CasaStruct::members.get Member::name = member_name + member_index WORD_SIZE * = member_offset + InstValue::Dup bytecode.push + if 0 member_offset > then + member_offset InstValue::Push bytecode.push + InstValue::Add bytecode.push + fi + if member_name temp_locals.get Option::Some(temp_local) is then + temp_local InstValue::LocalGet bytecode.push + else + empty_location "Expected struct literal temp local" ErrorKind::Syntax raise_error + 1 exit + fi + InstValue::Swap bytecode.push + InstValue::Store64 bytecode.push + 1 += member_index + done + fi } } @@ -888,40 +892,42 @@ impl BytecodeCompiler { impl BytecodeCompiler { fn compile_static_struct compiler:BytecodeCompiler group_ops:List[Op] -> str { group_ops.length 1 - group_ops.get Op::value = last_val - if last_val OpValue::StructLiteral(literal) is ! then - "Internal error: compile_static_struct called on non-struct ExprGroup\n" eprint - 1 exit - "" return - fi - literal StructLiteral::struct_name = struct_name - literal StructLiteral::field_order = field_order - struct_name compiler.store.structs.get = casa_struct_opt - if casa_struct_opt.is_none then - empty_location "Expected option value while unwrapping struct_name compiler.store.structs.get" ErrorKind::Syntax raise_error - 1 exit - fi - casa_struct_opt.unwrap = casa_struct + "" = static_struct_label + if last_val OpValue::StructLiteral(literal) is then + literal StructLiteral::struct_name = struct_name + literal StructLiteral::field_order = field_order + struct_name compiler.store.structs.get = casa_struct_opt + if casa_struct_opt.is_none then + empty_location "Expected option value while unwrapping struct_name compiler.store.structs.get" ErrorKind::Syntax raise_error + 1 exit + fi + casa_struct_opt.unwrap = casa_struct - List::new(List[StaticArrayElement]) = ordered_elems - 0 = i - while i field_order.length > do - i group_ops.get compiler.compile_static_element ordered_elems.push - 1 += i - done + List::new(List[StaticArrayElement]) = ordered_elems + 0 = i + while i field_order.length > do + i group_ops.get compiler.compile_static_element ordered_elems.push + 1 += i + done - # Reorder to match struct member definition order - List::new(List[StaticArrayElement]) = fields - 0 = mi - while mi casa_struct CasaStruct::members.length > do - mi casa_struct CasaStruct::members.get Member::name = member_name - member_name field_order string_list_index = fo_idx - fo_idx ordered_elems.get fields.push - 1 += mi - done + # Reorder to match struct member definition order + List::new(List[StaticArrayElement]) = fields + 0 = mi + while mi casa_struct CasaStruct::members.length > do + mi casa_struct CasaStruct::members.get Member::name = member_name + member_name field_order string_list_index = fo_idx + fo_idx ordered_elems.get fields.push + 1 += mi + done - compiler.static_structs.length = struct_index - fields StaticStruct compiler.static_structs.push - f"static_struct_{struct_index}" + compiler.static_structs.length = struct_index + fields StaticStruct compiler.static_structs.push + f"static_struct_{struct_index}" = static_struct_label + else + "Internal error: compile_static_struct called on non-struct ExprGroup\n" eprint + 1 exit + fi + static_struct_label } } @@ -952,77 +958,78 @@ impl BytecodeCompiler { impl BytecodeCompiler { fn compile_push_array compiler:BytecodeCompiler op:Op bytecode:List[InstValue] { - if op.value OpValue::PushArray(items) is ! then return fi - items.length = length + if op.value OpValue::PushArray(items) is then + items.length = length - if compiler.store items op_is_static_array_with_store then - bytecode items compiler.compile_static_array - return - fi + if compiler.store items op_is_static_array_with_store then + bytecode items compiler.compile_static_array + return + fi - # Compile items in reverse order - length 1 - = reverse_index - while 0 reverse_index >= do - reverse_index items.get = item - bytecode reverse_index item compiler.compile_op - reverse_index 1 - = reverse_index - done + # Compile items in reverse order + length 1 - = reverse_index + while 0 reverse_index >= do + reverse_index items.get = item + bytecode reverse_index item compiler.compile_op + reverse_index 1 - = reverse_index + done - # Allocate memory: header (data_ptr + length) + elements - compiler.locals_count = list_local - compiler.locals_count 1 + compiler->locals_count - length 2 + WORD_SIZE * InstValue::Push bytecode.push - InstValue::HeapAlloc bytecode.push - list_local InstValue::LocalSet bytecode.push + # Allocate memory: header (data_ptr + length) + elements + compiler.locals_count = list_local + compiler.locals_count 1 + compiler->locals_count + length 2 + WORD_SIZE * InstValue::Push bytecode.push + InstValue::HeapAlloc bytecode.push + list_local InstValue::LocalSet bytecode.push - # Store data_ptr (allocation + 16) at offset 0 - list_local InstValue::LocalGet bytecode.push - 16 InstValue::Push bytecode.push - InstValue::Add bytecode.push - list_local InstValue::LocalGet bytecode.push - InstValue::Store64 bytecode.push + # Store data_ptr (allocation + 16) at offset 0 + list_local InstValue::LocalGet bytecode.push + 16 InstValue::Push bytecode.push + InstValue::Add bytecode.push + list_local InstValue::LocalGet bytecode.push + InstValue::Store64 bytecode.push - # Store length at offset WORD_SIZE - length InstValue::Push bytecode.push - list_local InstValue::LocalGet bytecode.push - WORD_SIZE InstValue::Push bytecode.push - InstValue::Add bytecode.push - InstValue::Store64 bytecode.push + # Store length at offset WORD_SIZE + length InstValue::Push bytecode.push + list_local InstValue::LocalGet bytecode.push + WORD_SIZE InstValue::Push bytecode.push + InstValue::Add bytecode.push + InstValue::Store64 bytecode.push - # Store elements starting at offset 16 - compiler.locals_count = index_local - compiler.locals_count 1 + compiler->locals_count - 16 InstValue::Push bytecode.push - index_local InstValue::LocalSet bytecode.push - - compiler.new_label = start_label - compiler.new_label = end_label - - # while index limit > do - start_label InstValue::Label bytecode.push - index_local InstValue::LocalGet bytecode.push - length 2 + WORD_SIZE * InstValue::Push bytecode.push - InstValue::Gt bytecode.push - end_label InstValue::JumpNe bytecode.push - - # list index + store64 - list_local InstValue::LocalGet bytecode.push - index_local InstValue::LocalGet bytecode.push - InstValue::Add bytecode.push - InstValue::Store64 bytecode.push + # Store elements starting at offset 16 + compiler.locals_count = index_local + compiler.locals_count 1 + compiler->locals_count + 16 InstValue::Push bytecode.push + index_local InstValue::LocalSet bytecode.push + + compiler.new_label = start_label + compiler.new_label = end_label + + # while index limit > do + start_label InstValue::Label bytecode.push + index_local InstValue::LocalGet bytecode.push + length 2 + WORD_SIZE * InstValue::Push bytecode.push + InstValue::Gt bytecode.push + end_label InstValue::JumpNe bytecode.push + + # list index + store64 + list_local InstValue::LocalGet bytecode.push + index_local InstValue::LocalGet bytecode.push + InstValue::Add bytecode.push + InstValue::Store64 bytecode.push - # WORD_SIZE += index - index_local InstValue::LocalGet bytecode.push - WORD_SIZE InstValue::Push bytecode.push - InstValue::Add bytecode.push - index_local InstValue::LocalSet bytecode.push + # WORD_SIZE += index + index_local InstValue::LocalGet bytecode.push + WORD_SIZE InstValue::Push bytecode.push + InstValue::Add bytecode.push + index_local InstValue::LocalSet bytecode.push - # done - start_label InstValue::Jump bytecode.push - end_label InstValue::Label bytecode.push + # done + start_label InstValue::Jump bytecode.push + end_label InstValue::Label bytecode.push - # Push array pointer - list_local InstValue::LocalGet bytecode.push + # Push array pointer + list_local InstValue::LocalGet bytecode.push + fi } } diff --git a/compiler/lexer.casa b/compiler/lexer.casa index 417022e..5194174 100644 --- a/compiler/lexer.casa +++ b/compiler/lexer.casa @@ -102,12 +102,7 @@ impl Lexer { loc "Expected two hex digits after \\x" ErrorKind::Syntax self.add_error "" Option::Some return fi - if high_opt Option::Some(high) is then - high hex_digit_value = high_value - else - empty_location "Expected high hex digit" ErrorKind::Syntax raise_error - 1 exit - fi + high_opt.unwrap hex_digit_value = high_value if 0 high_value < then 3 self Lexer::cursor Cursor::pos 3 - self.loc = loc loc "Invalid hex digit in \\x escape" ErrorKind::Syntax self.add_error @@ -119,12 +114,7 @@ impl Lexer { loc "Expected two hex digits after \\x" ErrorKind::Syntax self.add_error "" Option::Some return fi - if low_opt Option::Some(low) is then - low hex_digit_value = low_value - else - empty_location "Expected low hex digit" ErrorKind::Syntax raise_error - 1 exit - fi + low_opt.unwrap hex_digit_value = low_value if 0 low_value < then 4 self Lexer::cursor Cursor::pos 4 - self.loc = loc loc "Invalid hex digit in \\x escape" ErrorKind::Syntax self.add_error diff --git a/compiler/pattern.casa b/compiler/pattern.casa index 8acc5ed..98ad2bd 100644 --- a/compiler/pattern.casa +++ b/compiler/pattern.casa @@ -275,6 +275,10 @@ fn typecheck_match_op tc:TypeChecker function_opt:Option[Function] op:Op { op pattern_covered_key = covered_key match_subject_type parse_type_str.match_base = arm_base + # A prior arm has already opened (and left open) its scope frame iff the + # match has seen a condition. Capture this before the block below sets it. + match_ctx MatchContext::condition_present = arm_has_predecessor + if op pattern_value_of PatternValue::EnumVariant(variant) is then if op pattern_is_wildcard ! then arm_base variant op tc diagnose_bare_enum_pattern @@ -291,8 +295,20 @@ fn typecheck_match_op tc:TypeChecker function_opt:Option[Function] op:Op { tc.stack match_ctx MatchContext::after.restore fi + # Each arm body gets its own scope frame: pattern bindings and any + # block-local assignments live here, invisible to other arms and after + # `end`. Close the previous arm's frame before opening this one. + if arm_has_predecessor then tc.pop_block_scope fi + tc.push_block_scope + match_subject_type op function_opt tc register_pattern_bindings + # Record pattern bindings in the arm scope; implicit bindings cannot + # shadow an accessible outer variable (same rule as is-bindings). + for binding_var in op pattern_bindings.iter do + true op binding_var tc.record_var_scope + done + if op pattern_is_guarded then op pattern_guard_ops = guard_ops tc.store TypeChecker::new = guard_checker diff --git a/compiler/syntax.casa b/compiler/syntax.casa index 34a9b2c..11afbc5 100644 --- a/compiler/syntax.casa +++ b/compiler/syntax.casa @@ -227,6 +227,7 @@ fn parse_type_ast cursor:TokenCursor -> Type { fi peek_opt.unwrap = peek + "" = base if "fn" peek Token::value == then cursor.pop drop "fn" = base @@ -252,11 +253,7 @@ fn parse_type_ast cursor:TokenCursor -> Type { while cursor.is_finished ! do cursor.peek = inner_opt if inner_opt.is_none then break fi - if inner_opt Option::Some(inner) is then - inner Token::value = ival - else - break - fi + inner_opt.unwrap Token::value = ival if "]" ival == then cursor.pop drop break @@ -738,11 +735,7 @@ fn parse_bound_type_args cursor:TokenCursor trait_token:Token -> List[str] { trait_token Token::location "Unclosed trait bound type argument list" ErrorKind::Syntax raise_error bound_args return fi - if arg_tok_opt Option::Some(arg_tok) is then - arg_tok Token::value = arg_val - else - bound_args return - fi + arg_tok_opt.unwrap Token::value = arg_val if "[" arg_val == then 1 += pba_depth arg_val pba_current.append @@ -2083,11 +2076,7 @@ fn parse_pattern_terminator parser:Parser cursor:TokenCursor function_name:str - while cursor.is_finished ! do cursor.peek = next_opt if next_opt.is_none then break fi - if next_opt Option::Some(next) is then - next Token::value = next_value - else - break - fi + next_opt.unwrap Token::value = next_value if "=>" next_value == 0 match_depth == && then break fi cursor.pop = guard_token_opt if guard_token_opt.is_none then break fi @@ -3160,23 +3149,24 @@ fn parse_and_resolve tokens:List[Token] search_paths:List[str] -> ParseResolveRe fn gather_captures store:SymbolStore lambda_function:Function function:Function { for op in lambda_function Function::ops.iter do - if op.value OpValue::Identifier(name) is ! then continue fi - false = captured - - # Check enclosing function variables - for variable in function Function::variables.iter do - if name variable Variable::name == then - variable lambda_function Function::captures.push - true = captured - break - fi - done + if op.value OpValue::Identifier(name) is then + false = captured + + # Check enclosing function variables + for variable in function Function::variables.iter do + if name variable Variable::name == then + variable lambda_function Function::captures.push + true = captured + break + fi + done - if captured ! then - # Check global variables - name store.variables.get = global_var_opt - if global_var_opt Option::Some(global_var) is then - global_var lambda_function Function::captures.push + if captured ! then + # Check global variables + name store.variables.get = global_var_opt + if global_var_opt Option::Some(global_var) is then + global_var lambda_function Function::captures.push + fi fi fi done @@ -3422,24 +3412,25 @@ fn register_struct_pattern_binding store:SymbolStore function:Function binding_n } fn resolve_match_op store:SymbolStore function:Function op:Op errors:List[CasaError] { - if op.value OpValue::MatchArm(arm) is ! then return fi - arm.pattern_value match - PatternValue::Struct(struct_pattern) => { - struct_pattern.bindings.keys = pattern_keys - for pattern_key in pattern_keys.iter do - pattern_key struct_pattern.bindings.get.unwrap = binding_name - binding_name function store register_struct_pattern_binding - done - } - PatternValue::EnumVariant(variant) => { - errors function op variant store resolve_enum_variant - } - PatternValue::Literal => { } - end - arm.guard_ops = guard_ops - if guard_ops.length 0 != then - function guard_ops store resolve_identifiers = resolved_guard - resolved_guard arm MatchArm::set_guard_ops + if op.value OpValue::MatchArm(arm) is then + arm.pattern_value match + PatternValue::Struct(struct_pattern) => { + struct_pattern.bindings.keys = pattern_keys + for pattern_key in pattern_keys.iter do + pattern_key struct_pattern.bindings.get.unwrap = binding_name + binding_name function store register_struct_pattern_binding + done + } + PatternValue::EnumVariant(variant) => { + errors function op variant store resolve_enum_variant + } + PatternValue::Literal => { } + end + arm.guard_ops = guard_ops + if guard_ops.length 0 != then + function guard_ops store resolve_identifiers = resolved_guard + resolved_guard arm MatchArm::set_guard_ops + fi fi } # Try resolving `&name` and `&K::method` function references. Returns true diff --git a/compiler/typechecker.casa b/compiler/typechecker.casa index a85d1fd..26ad266 100644 --- a/compiler/typechecker.casa +++ b/compiler/typechecker.casa @@ -958,11 +958,7 @@ impl TypeChecker { fn assert_word_bound tc:TypeChecker op:Op type_name:str function_opt:Option[Function] { if type_name TYPE_UNKNOWN == then return fi if function_opt.is_none then return fi - if function_opt Option::Some(function) is then - function Function::stack_effect = word_stack_effect - else - return - fi + function_opt.unwrap Function::stack_effect = word_stack_effect if type_name word_stack_effect StackEffect::type_vars.has ! then return fi type_name word_stack_effect StackEffect::trait_bounds.get = word_bound_opt if word_bound_opt.is_none then return fi @@ -1028,19 +1024,53 @@ fn unify_variable_assignment } impl TypeChecker { - # Record a variable assignment relative to the active while-block scopes. - # Outside any while block the name is a pre-scope (function-level) variable, - # and any stale out-of-scope marking is cleared. Inside a while block, a name - # that is not already active, pre-scope, or global becomes local to the + # Push a fresh scope frame for a new block branch (while body, if/elif/else + # branch, or match arm). Names first assigned inside the branch land here. + fn push_block_scope tc:TypeChecker { + List::new(List[str]) tc.scope_stack.push + } + + # Pop the innermost scope frame at a block boundary. Each name that is not + # still active in an enclosing frame moves to out_of_scope, so a later read + # is rejected as undefined. Shared by while, if/elif/else, and match. + fn pop_block_scope tc:TypeChecker { + if 0 tc.scope_stack.length == then return fi + tc.scope_stack.pop = popped_frame + for exiting_name in popped_frame.iter do + # Only move to out_of_scope if not still active in an outer frame + false = still_active + 0 = outer_idx + while outer_idx tc.scope_stack.length > do + outer_idx tc.scope_stack.get = outer_frame + if exiting_name outer_frame string_list_contains then + true = still_active + fi + 1 += outer_idx + done + if still_active ! then + exiting_name tc.out_of_scope.add = new_oos + new_oos tc->out_of_scope + fi + done + } + + # Record a variable assignment or pattern binding relative to the active + # block scopes. Outside any block the name is a pre-scope (function-level) + # variable, and any stale out-of-scope marking is cleared. Inside a block, a + # name that is not already active, pre-scope, or global becomes local to the # innermost frame (reactivating it if it was previously out of scope). - fn record_var_scope tc:TypeChecker var_name:str { + # + # `is_binding` marks implicit pattern/is-bindings: when such a name would + # otherwise resolve to an accessible outer variable it is a shadow error, + # whereas an explicit `= x` is allowed to mutate that outer variable. + fn record_var_scope tc:TypeChecker var_name:str op:Op is_binding:bool { if 0 tc.scope_stack.length == then if var_name tc.pre_scope_vars.has ! then var_name tc.pre_scope_vars.add = updated_pre updated_pre tc->pre_scope_vars fi # A fresh outer-scope assignment is always accessible — clear any - # stale out-of-scope marking left by an earlier while block. + # stale out-of-scope marking left by an earlier block. if var_name tc.out_of_scope.has then var_name tc.out_of_scope.remove = cleared_pre cleared_pre tc->out_of_scope @@ -1077,6 +1107,12 @@ impl TypeChecker { if var_name cur_frame string_list_contains ! then var_name cur_frame.push fi + elif is_binding is_pre_scope_global ! && then + # An implicit pattern/is binding would shadow an accessible + # block-local or function-level variable. Globals are a separate + # namespace (and are exempt from block scoping), so a binding that + # merely matches a global name is allowed, as before. + op Op::location f"Binding `{var_name}` shadows an existing variable in scope; choose a different name" ErrorKind::DuplicateName add_error fi fi } @@ -1089,46 +1125,47 @@ impl TypeChecker { TYPE_INT_T tc.expect_type_t return fi - if op.value OpValue::AssignVar(var_name) is ! then return fi - tc.stack_peek_type = stack_type_t - op Op::type_hint.is_some = has_annotation - stack_type_t = effective_type_t - if has_annotation then - op Op::type_hint = hint_opt - if hint_opt.is_none then - empty_location "Expected option value while unwrapping op Op::type_hint" ErrorKind::Syntax raise_error - 1 exit + if op.value OpValue::AssignVar(var_name) is then + tc.stack_peek_type = stack_type_t + op Op::type_hint.is_some = has_annotation + stack_type_t = effective_type_t + if has_annotation then + op Op::type_hint = hint_opt + if hint_opt.is_none then + empty_location "Expected option value while unwrapping op Op::type_hint" ErrorKind::Syntax raise_error + 1 exit + fi + hint_opt.unwrap = effective_type_t + fi + if has_annotation ! stack_type_t.has_unresolved && then + op Op::location f"Cannot infer element type of empty array literal assigned to `{var_name}`; add a type annotation (e.g. `= {var_name}:array[int]`) or an explicit cast (e.g. `[] (array[int])`)." ErrorKind::TypeMismatch raise_error fi - hint_opt.unwrap = effective_type_t - fi - if has_annotation ! stack_type_t.has_unresolved && then - op Op::location f"Cannot infer element type of empty array literal assigned to `{var_name}`; add a type annotation (e.g. `= {var_name}:array[int]`) or an explicit cast (e.g. `[] (array[int])`)." ErrorKind::TypeMismatch raise_error - fi - # Scope tracking: record variable assignment relative to while blocks. - var_name tc.record_var_scope + # Scope tracking: record variable assignment relative to block scopes. + false op var_name tc.record_var_scope - # Try local variables first - if function_opt.is_some then - function_opt.unwrap = function - var_name function Function::variables variable_list_index = local_index - if 0 local_index >= then - local_index function Function::variables.get = local_var - tc.store "local" effective_type_t local_var op unify_variable_assignment + # Try local variables first + if function_opt.is_some then + function_opt.unwrap = function + var_name function Function::variables variable_list_index = local_index + if 0 local_index >= then + local_index function Function::variables.get = local_var + tc.store "local" effective_type_t local_var op unify_variable_assignment + effective_type_t tc.expect_type_t + return + fi + fi + + # Try global variables + var_name tc.store.variables.get = global_opt + if global_opt.is_some then + global_opt.unwrap = global_var + tc.store "global" effective_type_t global_var op unify_variable_assignment effective_type_t tc.expect_type_t return fi - fi - - # Try global variables - var_name tc.store.variables.get = global_opt - if global_opt.is_some then - global_opt.unwrap = global_var - tc.store "global" effective_type_t global_var op unify_variable_assignment effective_type_t tc.expect_type_t - return fi - effective_type_t tc.expect_type_t } } @@ -1150,11 +1187,7 @@ impl TypeChecker { trait_suffix trait_sep 1 + trait_suffix.length trait_sep - 1 - str::substring = trait_method_name trait_type_var function Function::stack_effect StackEffect::trait_bounds.get = trait_bound_opt if trait_bound_opt.is_none then return fi - if trait_bound_opt Option::Some(trait_bound) is then - trait_bound TraitBound::name tc.store.traits.get = trait_def_opt - else - return - fi + trait_bound_opt.unwrap TraitBound::name tc.store.traits.get = trait_def_opt if trait_def_opt.is_none then return fi trait_def_opt.unwrap = trait_def for trait_method_entry in trait_def tc.store collect_trait_methods.iter do @@ -1203,41 +1236,41 @@ impl TypeChecker { fi # PUSH_VARIABLE - if op.value OpValue::PushVar(var_name) is ! then return fi + if op.value OpValue::PushVar(var_name) is then + # Check if variable has gone out of scope (was only in a popped block scope) + if var_name tc.out_of_scope.has then + op Op::location f"Variable `{var_name}` is not accessible outside its block scope" ErrorKind::UndefinedName add_error + TYPE_UNKNOWN_T tc.stack_push_type + return + fi - # Check if variable has gone out of scope (was only in a popped while scope) - if var_name tc.out_of_scope.has then - op Op::location f"Variable `{var_name}` is not accessible outside its while block scope" ErrorKind::UndefinedName add_error - TYPE_UNKNOWN_T tc.stack_push_type - return - fi + # Check local variables first + if function_opt.is_some then + function_opt.unwrap = function + var_name function Function::variables variable_list_index = local_index + if 0 local_index >= then + if local_index function Function::variables.get Variable::typ.is_unknown ! then + local_index function Function::variables.get Variable::typ tc.stack_push_type + else + TYPE_UNKNOWN_T tc.stack_push_type + fi + return + fi + fi - # Check local variables first - if function_opt.is_some then - function_opt.unwrap = function - var_name function Function::variables variable_list_index = local_index - if 0 local_index >= then - if local_index function Function::variables.get Variable::typ.is_unknown ! then - local_index function Function::variables.get Variable::typ tc.stack_push_type + # Check global variables + var_name tc.store.variables.get = global_opt + if global_opt Option::Some(global_var) is then + if global_var Variable::typ.is_unknown ! then + global_var Variable::typ tc.stack_push_type else TYPE_UNKNOWN_T tc.stack_push_type fi return fi - fi - # Check global variables - var_name tc.store.variables.get = global_opt - if global_opt Option::Some(global_var) is then - if global_var Variable::typ.is_unknown ! then - global_var Variable::typ tc.stack_push_type - else - TYPE_UNKNOWN_T tc.stack_push_type - fi - return + op Op::location f"Variable `{var_name}` is not defined" ErrorKind::UndefinedName raise_error fi - - op Op::location f"Variable `{var_name}` is not defined" ErrorKind::UndefinedName raise_error } } @@ -1253,6 +1286,9 @@ impl TypeChecker { op Op::location if_ctx IfContext::set_current_branch_location if_ctx BranchFrame::BranchIf = if_frame if_frame tc.branched_stacks.push + # Scope the then-branch: is-bindings from the condition and any + # block-local assignments live here until the next branch or `fi`. + tc.push_block_scope true tc->in_branch_condition elif if_value OpValue::IfCondition is then false tc->in_branch_condition @@ -1271,6 +1307,10 @@ impl TypeChecker { if_value OpValue::IfElif is if_value OpValue::IfElse is || then + # Close the preceding branch scope and open a fresh one. Each + # branch gets independent is-bindings and block-local variables. + tc.pop_block_scope + tc.push_block_scope if 0 tc.branched_stacks.length == then return fi tc.branched_stacks.length 1 - tc.branched_stacks.get = top_frame if top_frame BranchFrame::BranchIf(if_ctx) is then @@ -1302,6 +1342,8 @@ impl TypeChecker { op Op::location "Branches have incompatible stack effects" ErrorKind::StackMismatch add_error fi elif if_value OpValue::IfEnd is then + # Close the final branch scope. + tc.pop_block_scope tc.branched_stacks.pop = top_frame if top_frame BranchFrame::BranchIf(if_ctx) is then if_ctx IfContext::before = if_before @@ -1340,7 +1382,7 @@ impl TypeChecker { op Op::location while_ctx WhileContext::set_current_branch_location while_ctx BranchFrame::BranchWhile = while_frame while_frame tc.branched_stacks.push - List::new(List[str]) tc.scope_stack.push + tc.push_block_scope elif while_value OpValue::WhileCondition is then TYPE_BOOL_T tc.expect_type_t if 0 tc.branched_stacks.length == then return fi @@ -1355,25 +1397,7 @@ impl TypeChecker { if 0 tc.branched_stacks.length == then return fi elif while_value OpValue::WhileEnd is then tc.branched_stacks.pop drop - if 0 tc.scope_stack.length != then - tc.scope_stack.pop = popped_frame - for exiting_name in popped_frame.iter do - # Only move to out_of_scope if not still active in an outer frame - false = still_active - 0 = outer_idx - while outer_idx tc.scope_stack.length > do - outer_idx tc.scope_stack.get = outer_frame - if exiting_name outer_frame string_list_contains then - true = still_active - fi - 1 += outer_idx - done - if still_active ! then - exiting_name tc.out_of_scope.add = new_oos - new_oos tc->out_of_scope - fi - done - fi + tc.pop_block_scope fi } } @@ -1388,8 +1412,10 @@ fn bind_generic_params_standalone actual_type:Type type_vars:Set[str] -> Map[str Type] { - if expected_type.as_generic Option::Some(expected_g) is ! then bindings return fi - if actual_type.as_generic Option::Some(actual_g) is ! then bindings return fi + if expected_type.as_generic.is_none then bindings return fi + expected_type.as_generic.unwrap = expected_g + if actual_type.as_generic.is_none then bindings return fi + actual_type.as_generic.unwrap = actual_g if expected_g.base actual_g.base != then bindings return fi 0 = index while @@ -1539,8 +1565,10 @@ fn bind_from_type_slot fi bindings return fi - if expected.as_generic Option::Some(expected_g) is ! then bindings return fi - if actual.as_generic Option::Some(actual_g) is ! then bindings return fi + if expected.as_generic.is_none then bindings return fi + expected.as_generic.unwrap = expected_g + if actual.as_generic.is_none then bindings return fi + actual.as_generic.unwrap = actual_g if expected_g.base actual_g.base != then bindings return fi 0 = slot_index while @@ -2023,59 +2051,60 @@ impl TypeChecker { impl TypeChecker { fn check_enum_variant tc:TypeChecker op:Op { - if op.value OpValue::PushEnumVariant(variant) is ! then return fi - variant EnumVariant::enum_name = enum_name - enum_name tc.store.enums.get = enum_opt - if enum_opt.is_none then - enum_name GenericType::new Type::Generic tc.stack_push_type - return - fi - enum_opt.unwrap = casa_enum - variant EnumVariant::variant_name casa_enum CasaEnum::variant_types.get = inner_opt - if inner_opt.is_none then - # No inner values - push the enum type - if 0 casa_enum CasaEnum::type_vars.length != then - casa_enum CasaEnum::type_vars enum_name build_parameterized_type tc.stack_push_type - else + if op.value OpValue::PushEnumVariant(variant) is then + variant EnumVariant::enum_name = enum_name + enum_name tc.store.enums.get = enum_opt + if enum_opt.is_none then enum_name GenericType::new Type::Generic tc.stack_push_type + return fi - return - fi - inner_opt.unwrap = inner_types - if 0 inner_types.length == then + enum_opt.unwrap = casa_enum + variant EnumVariant::variant_name casa_enum CasaEnum::variant_types.get = inner_opt + if inner_opt.is_none then + # No inner values - push the enum type + if 0 casa_enum CasaEnum::type_vars.length != then + casa_enum CasaEnum::type_vars enum_name build_parameterized_type tc.stack_push_type + else + enum_name GenericType::new Type::Generic tc.stack_push_type + fi + return + fi + inner_opt.unwrap = inner_types + if 0 inner_types.length == then + if 0 casa_enum CasaEnum::type_vars.length != then + casa_enum CasaEnum::type_vars enum_name build_parameterized_type tc.stack_push_type + else + enum_name GenericType::new Type::Generic tc.stack_push_type + fi + return + fi + + # Data-carrying variant: pop inner values, resolve generics if 0 casa_enum CasaEnum::type_vars.length != then - casa_enum CasaEnum::type_vars enum_name build_parameterized_type tc.stack_push_type + # Generic enum variant (e.g. Option::Some with T) + # Bind type vars from inner values + Map::new(Map[str Type]) = bindings + Set::new(Set[str]) = type_vars + for type_var in casa_enum CasaEnum::type_vars.iter do + type_var type_vars.add = type_vars + done + for expected in inner_types.iter do + if expected.type_var_name Option::Some(tv_name) is tv_name type_vars.has && then + tc.stack_pop_type = actual_t + actual_t tv_name bindings tc.bind_type_var = bindings + else + expected tc.expect_type_t + fi + done + # Build resolved type name + bindings casa_enum CasaEnum::type_vars enum_name build_resolved_type tc.stack_push_type else + # Non-generic variant: pop inner values directly + for inner_type in inner_types.iter do + inner_type tc.expect_type_t + done enum_name GenericType::new Type::Generic tc.stack_push_type fi - return - fi - - # Data-carrying variant: pop inner values, resolve generics - if 0 casa_enum CasaEnum::type_vars.length != then - # Generic enum variant (e.g. Option::Some with T) - # Bind type vars from inner values - Map::new(Map[str Type]) = bindings - Set::new(Set[str]) = type_vars - for type_var in casa_enum CasaEnum::type_vars.iter do - type_var type_vars.add = type_vars - done - for expected in inner_types.iter do - if expected.type_var_name Option::Some(tv_name) is tv_name type_vars.has && then - tc.stack_pop_type = actual_t - actual_t tv_name bindings tc.bind_type_var = bindings - else - expected tc.expect_type_t - fi - done - # Build resolved type name - bindings casa_enum CasaEnum::type_vars enum_name build_resolved_type tc.stack_push_type - else - # Non-generic variant: pop inner values directly - for inner_type in inner_types.iter do - inner_type tc.expect_type_t - done - enum_name GenericType::new Type::Generic tc.stack_push_type fi } } @@ -2094,31 +2123,29 @@ impl TypeChecker { return fi base tc.store.enums.get.unwrap = casa_enum - if op.value OpValue::IsCheck(variant) is ! then - TYPE_BOOL_T tc.stack_push_type - return - fi - # Validate the variant belongs to the correct enum - if "" variant EnumVariant::enum_name != then - if variant EnumVariant::enum_name casa_enum CasaEnum::name != then - f"Cannot check `{variant EnumVariant::enum_name}::{variant EnumVariant::variant_name}` on value of type `{typ.format}`" tc.raise_type_mismatch + if op.value OpValue::IsCheck(variant) is then + # Validate the variant belongs to the correct enum + if "" variant EnumVariant::enum_name != then + if variant EnumVariant::enum_name casa_enum CasaEnum::name != then + f"Cannot check `{variant EnumVariant::enum_name}::{variant EnumVariant::variant_name}` on value of type `{typ.format}`" tc.raise_type_mismatch + fi fi - fi - # Validate binding count for data-carrying variants - if 0 variant EnumVariant::bindings.length != then - variant EnumVariant::variant_name casa_enum CasaEnum::variant_types.get = inner_opt - if inner_opt.is_some then - inner_opt.unwrap = inner_types - if variant EnumVariant::bindings.length inner_types.length != then - f"Variant `{variant EnumVariant::enum_name}::{variant EnumVariant::variant_name}` has {inner_types.length} inner value(s), but {variant EnumVariant::bindings.length} binding(s) provided" tc.raise_type_mismatch + # Validate binding count for data-carrying variants + if 0 variant EnumVariant::bindings.length != then + variant EnumVariant::variant_name casa_enum CasaEnum::variant_types.get = inner_opt + if inner_opt.is_some then + inner_opt.unwrap = inner_types + if variant EnumVariant::bindings.length inner_types.length != then + f"Variant `{variant EnumVariant::enum_name}::{variant EnumVariant::variant_name}` has {inner_types.length} inner value(s), but {variant EnumVariant::bindings.length} binding(s) provided" tc.raise_type_mismatch + fi fi fi + typ.format variant function_opt tc register_enum_pattern_bindings + # Track bindings in scope; implicit bindings cannot shadow outer vars. + for binding_var in variant EnumVariant::bindings.iter do + true op binding_var tc.record_var_scope + done fi - typ.format variant function_opt tc register_enum_pattern_bindings - # Track bindings in scope (same rules as AssignVar) - for binding_var in variant EnumVariant::bindings.iter do - binding_var tc.record_var_scope - done TYPE_BOOL_T tc.stack_push_type } } @@ -2176,6 +2203,10 @@ impl TypeChecker { elif match_value OpValue::MatchEnd is then tc.branched_stacks.pop = top_frame if top_frame BranchFrame::BranchMatch(match_ctx) is then + # Close the final arm's scope frame (only opened if an arm ran). + if match_ctx MatchContext::condition_present then + tc.pop_block_scope + fi # Record last arm label if "" match_ctx MatchContext::current_branch_label != then match_ctx MatchContext::current_branch_label match_ctx MatchContext::branch_records.push @@ -2287,30 +2318,31 @@ impl TypeChecker { impl TypeChecker { fn check_struct_new tc:TypeChecker op:Op { - if op.value OpValue::StructNew(casa_struct) is ! then return fi - casa_struct CasaStruct::name = name - # Build type variable bindings from actual stack values - Map::new(Map[str Type]) = bindings - for member in casa_struct CasaStruct::members.iter do - tc.stack_pop_type = actual - member Member::typ = expected - # If expected is a type variable, bind it to the actual type - expected match - Type::TypeVar(expected_var) => { - for type_var in casa_struct CasaStruct::type_vars.iter do - if expected_var type_var == then - actual expected_var bindings.set = bindings - fi - done - } - _ => { } - end - done - # Construct the result type - if 0 casa_struct CasaStruct::type_vars.length == then - name GenericType::new Type::Generic tc.stack_push_type - else - bindings casa_struct CasaStruct::type_vars name build_resolved_type tc.stack_push_type + if op.value OpValue::StructNew(casa_struct) is then + casa_struct CasaStruct::name = name + # Build type variable bindings from actual stack values + Map::new(Map[str Type]) = bindings + for member in casa_struct CasaStruct::members.iter do + tc.stack_pop_type = actual + member Member::typ = expected + # If expected is a type variable, bind it to the actual type + expected match + Type::TypeVar(expected_var) => { + for type_var in casa_struct CasaStruct::type_vars.iter do + if expected_var type_var == then + actual expected_var bindings.set = bindings + fi + done + } + _ => { } + end + done + # Construct the result type + if 0 casa_struct CasaStruct::type_vars.length == then + name GenericType::new Type::Generic tc.stack_push_type + else + bindings casa_struct CasaStruct::type_vars name build_resolved_type tc.stack_push_type + fi fi } } @@ -2321,28 +2353,29 @@ impl TypeChecker { impl TypeChecker { fn check_struct_literal tc:TypeChecker op:Op { - if op.value OpValue::StructLiteral(literal) is ! then return fi - literal StructLiteral::struct_name = name - name tc.store.structs.get = struct_opt - if struct_opt.is_none then + if op.value OpValue::StructLiteral(literal) is then + literal StructLiteral::struct_name = name + name tc.store.structs.get = struct_opt + if struct_opt.is_none then + name GenericType::new Type::Generic tc.stack_push_type + return + fi + struct_opt.unwrap = casa_struct + # Pop field values in reverse order + literal StructLiteral::field_order.length 1 - = index + while 0 index >= do + index literal StructLiteral::field_order.get = field_name + # Find member type + for member in casa_struct CasaStruct::members.iter do + if field_name member Member::name == then + member Member::typ tc.expect_type_t + break + fi + done + index 1 - = index + done name GenericType::new Type::Generic tc.stack_push_type - return fi - struct_opt.unwrap = casa_struct - # Pop field values in reverse order - literal StructLiteral::field_order.length 1 - = index - while 0 index >= do - index literal StructLiteral::field_order.get = field_name - # Find member type - for member in casa_struct CasaStruct::members.iter do - if field_name member Member::name == then - member Member::typ tc.expect_type_t - break - fi - done - index 1 - = index - done - name GenericType::new Type::Generic tc.stack_push_type } } @@ -2452,6 +2485,7 @@ fn instantiate_default_trait_method # For non-generic receivers, substitute trait type params with concrete types. Map::new(Map[str Type]) = trait_subs receiver = default_self_type + empty_stack_effect = resolved_stack_effect if fn_base_opt.is_some then fn_base_opt.unwrap = base_name if 0 type_params.length != then @@ -2517,28 +2551,29 @@ fn instantiate_default_trait_method fi 0 = cloned_lambda_index for body_op in cloned_body.iter do - if body_op.value OpValue::FnPush(lambda_name) is ! then continue fi - if lambda_name "lambda__" str::starts_with ! then continue fi - lambda_name store.functions.get = lambda_opt - if lambda_opt.is_none then continue fi - lambda_opt.unwrap = lambda - - f"{lambda_name}__{synth_fn_name}_{cloned_lambda_index}" = cloned_lambda_name - lambda Function::ops clone_ops = lambda_ops - if 0 trait_subs.length != then - trait_subs lambda_ops substitute_ops_types - fi - List::new(List[Parameter]) = lambda_params - List::new(List[Type]) = lambda_returns - lambda_returns lambda_params StackEffect::new = lambda_effect - for lambda_type_var in resolved_stack_effect StackEffect::type_vars.to_list.iter do - lambda_type_var lambda_effect StackEffect::type_vars.add = lambda_tvs - lambda_tvs lambda_effect StackEffect::set_type_vars - done - lambda_effect lambda Function::location lambda_ops cloned_lambda_name Function::with_stack_effect = cloned_lambda - cloned_lambda cloned_lambda_name store.functions.set drop - cloned_lambda_name OpValue::FnPush body_op Op::set_value - 1 += cloned_lambda_index + if body_op.value OpValue::FnPush(lambda_name) is then + if lambda_name "lambda__" str::starts_with ! then continue fi + lambda_name store.functions.get = lambda_opt + if lambda_opt.is_none then continue fi + lambda_opt.unwrap = lambda + + f"{lambda_name}__{synth_fn_name}_{cloned_lambda_index}" = cloned_lambda_name + lambda Function::ops clone_ops = lambda_ops + if 0 trait_subs.length != then + trait_subs lambda_ops substitute_ops_types + fi + List::new(List[Parameter]) = lambda_params + List::new(List[Type]) = lambda_returns + lambda_returns lambda_params StackEffect::new = lambda_effect + for lambda_type_var in resolved_stack_effect StackEffect::type_vars.to_list.iter do + lambda_type_var lambda_effect StackEffect::type_vars.add = lambda_tvs + lambda_tvs lambda_effect StackEffect::set_type_vars + done + lambda_effect lambda Function::location lambda_ops cloned_lambda_name Function::with_stack_effect = cloned_lambda + cloned_lambda cloned_lambda_name store.functions.set drop + cloned_lambda_name OpValue::FnPush body_op Op::set_value + 1 += cloned_lambda_index + fi done for body_op in cloned_body.iter do body_op synth_ops.push @@ -2567,86 +2602,87 @@ impl TypeChecker { op_index:int function_opt:Option[Function] -> int { - if op.value OpValue::MethodCall(method_name) is ! then op_index return fi - tc.stack_peek = receiver - - # Check if receiver is a trait-bounded type variable - if function_opt Option::Some(function) is then - function Function::stack_effect StackEffect::trait_bounds = bounds - receiver bounds.get = trait_opt - if trait_opt.is_some then - trait_opt.unwrap = bound - bound TraitBound::name = trait_name - trait_name tc.store.traits.get = trait_def_opt - if trait_def_opt.is_some then - trait_def_opt.unwrap = trait_def - # Build substitution map from trait's type_params to the - # bound's concrete type_args, so that a trait method like - # `next self:self -> Option[T]` with bound `I: Iterable[int]` - # resolves to `next self:I -> Option[int]` at the call site. - Map::new(Map[str Type]) = constraint_subs - trait_def CasaTrait::type_params = constraint_params - bound TraitBound::type_args = constraint_args - if constraint_params.length constraint_args.length == then - for pair in constraint_args.iter constraint_params.iter.zip do - pair.second parse_type_str pair.first constraint_subs.set = constraint_subs + if op.value OpValue::MethodCall(method_name) is then + tc.stack_peek = receiver + + # Check if receiver is a trait-bounded type variable + if function_opt Option::Some(function) is then + function Function::stack_effect StackEffect::trait_bounds = bounds + receiver bounds.get = trait_opt + if trait_opt.is_some then + trait_opt.unwrap = bound + bound TraitBound::name = trait_name + trait_name tc.store.traits.get = trait_def_opt + if trait_def_opt.is_some then + trait_def_opt.unwrap = trait_def + # Build substitution map from trait's type_params to the + # bound's concrete type_args, so that a trait method like + # `next self:self -> Option[T]` with bound `I: Iterable[int]` + # resolves to `next self:I -> Option[int]` at the call site. + Map::new(Map[str Type]) = constraint_subs + trait_def CasaTrait::type_params = constraint_params + bound TraitBound::type_args = constraint_args + if constraint_params.length constraint_args.length == then + for pair in constraint_args.iter constraint_params.iter.zip do + pair.second parse_type_str pair.first constraint_subs.set = constraint_subs + done + fi + for trait_method in trait_def tc.store collect_trait_methods.iter do + if method_name trait_method TraitMethod::name == then + f"__trait_{receiver}_{method_name}" = hidden_var + tc.stack_pop_drop + constraint_subs receiver trait_method TraitMethod::stack_effect resolve_trait_stack_effect apply_type_subs_to_stack_effect = resolved_stack_effect + receiver tc.stack_push + f"{receiver}::{method_name}" resolved_stack_effect tc.apply_stack_effect + hidden_var OpValue::PushVar op Op::set_value + # Insert FN_EXEC op after the PUSH_VARIABLE + op Op::location OpValue::FnExec Op::new = exec_op + op_index exec_op ops.insert + op_index 1 + return + fi done fi - for trait_method in trait_def tc.store collect_trait_methods.iter do - if method_name trait_method TraitMethod::name == then - f"__trait_{receiver}_{method_name}" = hidden_var - tc.stack_pop_drop - constraint_subs receiver trait_method TraitMethod::stack_effect resolve_trait_stack_effect apply_type_subs_to_stack_effect = resolved_stack_effect - receiver tc.stack_push - f"{receiver}::{method_name}" resolved_stack_effect tc.apply_stack_effect - hidden_var OpValue::PushVar op Op::set_value - # Insert FN_EXEC op after the PUSH_VARIABLE - op Op::location OpValue::FnExec Op::new = exec_op - op_index exec_op ops.insert - op_index 1 + return - fi - done fi fi - fi - # Look up Type::method - f"{receiver}::{method_name}" = fn_name - fn_name tc.store.functions.get = fn_opt + # Look up Type::method + f"{receiver}::{method_name}" = fn_name + fn_name tc.store.functions.get = fn_opt - # Try generic base type - if fn_opt.is_none then - receiver parse_type_str.base = base_opt - if base_opt Option::Some(base) is then - f"{base}::{method_name}" = fn_name - fn_name tc.store.functions.get = fn_opt + # Try generic base type + if fn_opt.is_none then + receiver parse_type_str.base = base_opt + if base_opt Option::Some(base) is then + f"{base}::{method_name}" = fn_name + fn_name tc.store.functions.get = fn_opt + fi fi - fi - # Try instantiating a default trait method - if fn_opt.is_none then - tc.store receiver method_name function_opt instantiate_default_trait_method = default_inst_opt - if default_inst_opt.is_some then - default_inst_opt.unwrap = fn_name - fn_name tc.store.functions.get = fn_opt + # Try instantiating a default trait method + if fn_opt.is_none then + tc.store receiver method_name function_opt instantiate_default_trait_method = default_inst_opt + if default_inst_opt.is_some then + default_inst_opt.unwrap = fn_name + fn_name tc.store.functions.get = fn_opt + fi fi - fi - if fn_opt.is_some then - fn_opt.unwrap = function - if function Function::is_typechecked ! then - function function Function::ops tc.store resolve_identifiers function Function::set_ops - true function Function::set_is_used - fi - tc.store function ensure_typechecked - function Function::stack_effect = effect - if 0 effect StackEffect::trait_bounds.keys.length != then - function_opt op Op::location op_index ops effect tc.inject_trait_fn_ptrs += op_index + if fn_opt.is_some then + fn_opt.unwrap = function + if function Function::is_typechecked ! then + function function Function::ops tc.store resolve_identifiers function Function::set_ops + true function Function::set_is_used + fi + tc.store function ensure_typechecked + function Function::stack_effect = effect + if 0 effect StackEffect::trait_bounds.keys.length != then + function_opt op Op::location op_index ops effect tc.inject_trait_fn_ptrs += op_index + fi + fn_name effect tc.apply_stack_effect + fn_name OpValue::FnCall op Op::set_value + else + op Op::location f"Method `{fn_name}` does not exist" ErrorKind::UndefinedName raise_error fi - fn_name effect tc.apply_stack_effect - fn_name OpValue::FnCall op Op::set_value - else - op Op::location f"Method `{fn_name}` does not exist" ErrorKind::UndefinedName raise_error fi op_index } diff --git a/lib/argparse.casa b/lib/argparse.casa index f749f97..ff16a4a 100644 --- a/lib/argparse.casa +++ b/lib/argparse.casa @@ -298,18 +298,19 @@ impl ParsedArgs { # --------------------------------------------------------------------------- fn append_usage_optional usage_sb:StringBuilder definition:ArgDef { - if definition.flagged Option::Some(opt_flag) is ! then return fi - " [" usage_sb.append - if "" opt_flag.short_flag != then - opt_flag.short_flag usage_sb.append - else - opt_flag.long_flag usage_sb.append - fi - if definition.takes_value then - " " usage_sb.append - opt_flag.name metavar usage_sb.append + if definition.flagged Option::Some(opt_flag) is then + " [" usage_sb.append + if "" opt_flag.short_flag != then + opt_flag.short_flag usage_sb.append + else + opt_flag.long_flag usage_sb.append + fi + if definition.takes_value then + " " usage_sb.append + opt_flag.name metavar usage_sb.append + fi + "]" usage_sb.append fi - "]" usage_sb.append } fn metavar name:str -> str { @@ -328,19 +329,20 @@ fn metavar name:str -> str { fn build_flag_column definition:ArgDef -> str { StringBuilder::new = col_sb - if definition.flagged Option::Some(col_flag) is ! then col_sb.build return fi - if "" col_flag.short_flag != then - col_flag.short_flag col_sb.append + if definition.flagged Option::Some(col_flag) is then + if "" col_flag.short_flag != then + col_flag.short_flag col_sb.append + if "" col_flag.long_flag != then + ", " col_sb.append + fi + fi if "" col_flag.long_flag != then - ", " col_sb.append + col_flag.long_flag col_sb.append + fi + if definition.takes_value then + " " col_sb.append + col_flag.name metavar col_sb.append fi - fi - if "" col_flag.long_flag != then - col_flag.long_flag col_sb.append - fi - if definition.takes_value then - " " col_sb.append - col_flag.name metavar col_sb.append fi col_sb.build } diff --git a/lsp.casa b/lsp.casa index a21bb12..12ef3ba 100644 --- a/lsp.casa +++ b/lsp.casa @@ -428,11 +428,10 @@ fn diagnostics_from result:CompileResult path:str -> List[LspDiagnostic] { then continue fi + fallback_source = error_source error CasaError::location Location::file source_cache.get = source_opt if source_opt.is_some then source_opt.unwrap = error_source - else - fallback_source = error_source fi # Build message @@ -465,11 +464,10 @@ fn diagnostics_from result:CompileResult path:str -> List[LspDiagnostic] { then continue fi + fallback_source = warn_source warning CasaWarning::location Location::file source_cache.get = warn_source_opt if warn_source_opt.is_some then warn_source_opt.unwrap = warn_source - else - fallback_source = warn_source fi DIAGNOSTIC_WARNING warning CasaWarning::message @@ -609,11 +607,10 @@ fn find_containing_function state:DocumentState offset:int -> Option[Function] { } fn casa_location_to_lsp location:Location -> LspLocation { + "" = location_source location Location::file SOURCE_CACHE.get = source_opt if source_opt.is_some then source_opt.unwrap = location_source - else - "" = location_source fi location Location::span Span::length location Location::span Span::offset @@ -1783,9 +1780,10 @@ fn compute_references -> Option[List[LspLocation]] { # Check function definition name first (parameter ops share the name token location) col line state find_function_at_position = fn_at_pos + Option::None(Option[Function]) = func + empty_location OpValue::FnExec Op::new = op if fn_at_pos Option::Some(matched_function) is then matched_function Function::location matched_function Function::name OpValue::FnCall Op::new = op - Option::None(Option[Function]) = func else col line state find_op_at_position = result if result.is_none then @@ -1860,9 +1858,11 @@ fn compute_rename -> Option[Map[str List[TextEdit]]] { # Check function definition name first (parameter ops share the name token location) col line state find_function_at_position = fn_at_pos + Option::None(Option[Function]) = func + empty_location OpValue::FnExec Op::new = op + "" = old_name if fn_at_pos Option::Some(matched_function) is then matched_function Function::location matched_function Function::name OpValue::FnCall Op::new = op - Option::None(Option[Function]) = func matched_function Function::name function_short_name = old_name else col line state find_op_at_position = result diff --git a/tests/compiler/errors/if_binding_shadow.casa b/tests/compiler/errors/if_binding_shadow.casa new file mode 100644 index 0000000..fe64ebd --- /dev/null +++ b/tests/compiler/errors/if_binding_shadow.casa @@ -0,0 +1,14 @@ +# expect: DUPLICATE_NAME + +enum Maybe { + Just (int) + Nothing +} + +fn shadow_outer m:Maybe { + 7 = value + if m Maybe::Just(value) is then + value drop + fi +} +42 Maybe::Just shadow_outer diff --git a/tests/compiler/errors/if_is_binding_out_of_scope.casa b/tests/compiler/errors/if_is_binding_out_of_scope.casa new file mode 100644 index 0000000..a5dec10 --- /dev/null +++ b/tests/compiler/errors/if_is_binding_out_of_scope.casa @@ -0,0 +1,14 @@ +# expect: UNDEFINED_NAME + +enum Maybe { + Just (int) + Nothing +} + +fn use_after_fi m:Maybe { + if m Maybe::Just(value) is then + value drop + fi + value drop +} +42 Maybe::Just use_after_fi diff --git a/tests/compiler/errors/match_arm_binding_out_of_scope.casa b/tests/compiler/errors/match_arm_binding_out_of_scope.casa new file mode 100644 index 0000000..dcac43c --- /dev/null +++ b/tests/compiler/errors/match_arm_binding_out_of_scope.casa @@ -0,0 +1,14 @@ +# expect: UNDEFINED_NAME + +enum Maybe { + Just (int) + Nothing +} + +fn use_other_arm m:Maybe { + m match + Maybe::Just(value) => { value drop } + Maybe::Nothing => { value drop } + end +} +42 Maybe::Just use_other_arm diff --git a/tests/compiler/errors/match_binding_shadow.casa b/tests/compiler/errors/match_binding_shadow.casa new file mode 100644 index 0000000..dfa9845 --- /dev/null +++ b/tests/compiler/errors/match_binding_shadow.casa @@ -0,0 +1,15 @@ +# expect: DUPLICATE_NAME + +enum Maybe { + Just (int) + Nothing +} + +fn shadow_outer_match m:Maybe { + 7 = value + m match + Maybe::Just(value) => { value drop } + Maybe::Nothing => { } + end +} +42 Maybe::Just shadow_outer_match diff --git a/tests/compiler/test_scope.casa b/tests/compiler/test_scope.casa index b11f51f..73ffc3e 100644 --- a/tests/compiler/test_scope.casa +++ b/tests/compiler/test_scope.casa @@ -95,9 +95,195 @@ fn test_while_global_accessible_after_done { disable_test_mode } +# ============================================================================ +# If scoping: variable declared inside an if branch is not visible after fi +# ============================================================================ + +fn test_if_scope_var_not_visible_after_fi { + "if_scope_var_not_visible_after_fi" test_start + "fn f {\n if false then\n 1 = x\n fi\n x\n}\nf\n" parse_typecheck + "has scope error" assert_has_errors + "undefined name kind" ErrorKind::UndefinedName assert_error_kind + clear_errors + disable_test_mode +} + +# ============================================================================ +# If is-binding: bound name is visible inside its then branch +# ============================================================================ + +fn test_if_is_binding_visible_in_then { + "if_is_binding_visible_in_then" test_start + "enum E { Some (int) None }\nfn f e:E {\n if e E::Some(v) is then v drop fi\n}\n42 E::Some f\n" parse_typecheck + "no errors using is-binding in then" assert_no_errors + disable_test_mode +} + +# ============================================================================ +# If is-binding: bound name is NOT visible after fi +# ============================================================================ + +fn test_if_is_binding_not_visible_after_fi { + "if_is_binding_not_visible_after_fi" test_start + "enum E { Some (int) None }\nfn f e:E {\n if e E::Some(v) is then v drop fi\n v drop\n}\n42 E::Some f\n" parse_typecheck + "is-binding out of scope after fi" assert_has_errors + "undefined name kind" ErrorKind::UndefinedName assert_error_kind + clear_errors + disable_test_mode +} + +# ============================================================================ +# If is-binding: bound name is NOT visible in the else branch +# ============================================================================ + +fn test_if_is_binding_not_visible_in_else { + "if_is_binding_not_visible_in_else" test_start + "enum E { Some (int) None }\nfn f e:E {\n if e E::Some(v) is then v drop else v drop fi\n}\n42 E::Some f\n" parse_typecheck + "is-binding out of scope in else" assert_has_errors + "undefined name kind" ErrorKind::UndefinedName assert_error_kind + clear_errors + disable_test_mode +} + +# ============================================================================ +# Elif independence: each branch carries its own is-bindings +# ============================================================================ + +fn test_if_elif_independent_bindings { + "if_elif_independent_bindings" test_start + "enum E { Some (int) None }\nfn f a:E b:E {\n if a E::Some(v) is then v drop\n elif b E::Some(w) is then w drop\n fi\n}\n42 E::Some 7 E::Some f\n" parse_typecheck + "no errors with independent elif bindings" assert_no_errors + disable_test_mode +} + +# ============================================================================ +# Elif independence: a sibling branch cannot see the if-branch binding +# ============================================================================ + +fn test_if_elif_cannot_see_if_binding { + "if_elif_cannot_see_if_binding" test_start + "enum E { Some (int) None }\nfn f a:E b:E {\n if a E::Some(v) is then v drop\n elif b E::None is then v drop\n fi\n}\n42 E::Some E::None f\n" parse_typecheck + "if-branch binding not visible in elif" assert_has_errors + "undefined name kind" ErrorKind::UndefinedName assert_error_kind + clear_errors + disable_test_mode +} + +# ============================================================================ +# Outer mutation: `= x` inside a branch mutates the outer variable +# ============================================================================ + +fn test_if_outer_var_mutation_in_branch { + "if_outer_var_mutation_in_branch" test_start + "fn f -> int {\n 0 = x\n if true then 5 = x fi\n x\n}\nf drop\n" parse_typecheck + "no errors mutating outer var in branch" assert_no_errors + disable_test_mode +} + +# ============================================================================ +# Block-local: a new `= y` inside a branch is not visible after fi +# ============================================================================ + +fn test_if_block_local_new_var { + "if_block_local_new_var" test_start + "fn f {\n if true then 5 = y fi\n y drop\n}\nf\n" parse_typecheck + "block-local new var out of scope after fi" assert_has_errors + "undefined name kind" ErrorKind::UndefinedName assert_error_kind + clear_errors + disable_test_mode +} + +# ============================================================================ +# Shadow error: an is-binding may not shadow an accessible outer variable +# ============================================================================ + +fn test_if_is_binding_shadow_error { + "if_is_binding_shadow_error" test_start + "enum E { Some (int) None }\nfn f e:E {\n 42 = v\n if e E::Some(v) is then v drop fi\n}\n42 E::Some f\n" parse_typecheck + "shadowing is-binding is an error" assert_has_errors + "duplicate name kind" ErrorKind::DuplicateName assert_error_kind + clear_errors + disable_test_mode +} + +# ============================================================================ +# Match scoping: an arm binding is visible inside its arm body +# ============================================================================ + +fn test_match_arm_binding_visible { + "match_arm_binding_visible" test_start + "enum E { Some (int) None }\nfn f e:E {\n e match\n E::Some(v) => { v drop }\n E::None => { }\n end\n}\n42 E::Some f\n" parse_typecheck + "no errors using arm binding in arm" assert_no_errors + disable_test_mode +} + +# ============================================================================ +# Match scoping: an arm binding is not visible in another arm +# ============================================================================ + +fn test_match_arm_binding_not_in_other_arm { + "match_arm_binding_not_in_other_arm" test_start + "enum E { Some (int) None }\nfn f a:E {\n a match\n E::Some(v) => { v drop }\n E::None => { v drop }\n end\n}\n42 E::Some f\n" parse_typecheck + "arm binding not visible in sibling arm" assert_has_errors + "undefined name kind" ErrorKind::UndefinedName assert_error_kind + clear_errors + disable_test_mode +} + +# ============================================================================ +# Match scoping: an arm binding may not shadow an accessible outer variable +# ============================================================================ + +fn test_match_arm_binding_shadow_error { + "match_arm_binding_shadow_error" test_start + "enum E { Some (int) None }\nfn f e:E {\n 42 = v\n e match\n E::Some(v) => { v drop }\n E::None => { }\n end\n}\n42 E::Some f\n" parse_typecheck + "shadowing arm binding is an error" assert_has_errors + "duplicate name kind" ErrorKind::DuplicateName assert_error_kind + clear_errors + disable_test_mode +} + +# ============================================================================ +# Match scoping: `= x` inside an arm mutates the outer variable +# ============================================================================ + +fn test_match_arm_outer_var_mutation { + "match_arm_outer_var_mutation" test_start + "enum E { Some (int) None }\nfn f e:E -> int {\n 0 = acc\n e match\n E::Some(n) => { n = acc }\n E::None => { }\n end\n acc\n}\n42 E::Some f drop\n" parse_typecheck + "no errors mutating outer var in arm" assert_no_errors + disable_test_mode +} + +# ============================================================================ +# Match scoping: a new `= z` inside an arm is not visible after end +# ============================================================================ + +fn test_match_arm_block_local_new_var { + "match_arm_block_local_new_var" test_start + "enum E { Some (int) None }\nfn f e:E {\n e match\n E::Some(n) => { n drop 99 = zz zz drop }\n E::None => { }\n end\n zz drop\n}\n42 E::Some f\n" parse_typecheck + "block-local arm var out of scope after end" assert_has_errors + "undefined name kind" ErrorKind::UndefinedName assert_error_kind + clear_errors + disable_test_mode +} + # ============================================================================ # Run tests # ============================================================================ +test_if_scope_var_not_visible_after_fi +test_if_is_binding_visible_in_then +test_if_is_binding_not_visible_after_fi +test_if_is_binding_not_visible_in_else +test_if_elif_independent_bindings +test_if_elif_cannot_see_if_binding +test_if_outer_var_mutation_in_branch +test_if_block_local_new_var +test_if_is_binding_shadow_error +test_match_arm_binding_visible +test_match_arm_binding_not_in_other_arm +test_match_arm_binding_shadow_error +test_match_arm_outer_var_mutation +test_match_arm_block_local_new_var test_while_scope_var_not_visible_after_done test_while_scope_var_visible_inside test_for_scope_iter_var_not_visible_after_done From 6209a750e654d07d56d6cc17b8e1a15564f0e5e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Mon, 1 Jun 2026 22:46:34 +0300 Subject: [PATCH 2/3] fix: tighten block binding scopes --- compiler/pattern.casa | 10 ++- compiler/typechecker.casa | 57 +++++++++++++++-- docs/control-flow.md | 13 ++++ docs/enums.md | 5 +- .../errors/is_binding_outside_branch.casa | 12 ++++ tests/compiler/test_scope.casa | 64 +++++++++++++++++++ 6 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 tests/compiler/errors/is_binding_outside_branch.casa diff --git a/compiler/pattern.casa b/compiler/pattern.casa index 98ad2bd..77e15d1 100644 --- a/compiler/pattern.casa +++ b/compiler/pattern.casa @@ -301,17 +301,21 @@ fn typecheck_match_op tc:TypeChecker function_opt:Option[Function] op:Op { if arm_has_predecessor then tc.pop_block_scope fi tc.push_block_scope - match_subject_type op function_opt tc register_pattern_bindings - # Record pattern bindings in the arm scope; implicit bindings cannot # shadow an accessible outer variable (same rule as is-bindings). for binding_var in op pattern_bindings.iter do - true op binding_var tc.record_var_scope + true op binding_var tc.record_var_scope = reactivated_out_of_scope + if reactivated_out_of_scope then + function_opt binding_var tc.reset_local_variable_type + fi done + match_subject_type op function_opt tc register_pattern_bindings + if op pattern_is_guarded then op pattern_guard_ops = guard_ops tc.store TypeChecker::new = guard_checker + guard_checker tc.copy_scope_state_to function_opt guard_ops guard_checker.run_type_check_ops guard_checker.stack.types = guard_stack false = guard_ok diff --git a/compiler/typechecker.casa b/compiler/typechecker.casa index 26ad266..771de7c 100644 --- a/compiler/typechecker.casa +++ b/compiler/typechecker.casa @@ -1024,6 +1024,16 @@ fn unify_variable_assignment } impl TypeChecker { + fn reset_local_variable_type tc:TypeChecker var_name:str function_opt:Option[Function] { + if function_opt Option::Some(function) is then + var_name function Function::variables variable_list_index = local_index + if 0 local_index >= then + local_index function Function::variables.get = local_var + Type::Unknown local_var Variable::set_typ + fi + fi + } + # Push a fresh scope frame for a new block branch (while body, if/elif/else # branch, or match arm). Names first assigned inside the branch land here. fn push_block_scope tc:TypeChecker { @@ -1063,7 +1073,8 @@ impl TypeChecker { # `is_binding` marks implicit pattern/is-bindings: when such a name would # otherwise resolve to an accessible outer variable it is a shadow error, # whereas an explicit `= x` is allowed to mutate that outer variable. - fn record_var_scope tc:TypeChecker var_name:str op:Op is_binding:bool { + fn record_var_scope tc:TypeChecker var_name:str op:Op is_binding:bool -> bool { + false = reactivated_out_of_scope if 0 tc.scope_stack.length == then if var_name tc.pre_scope_vars.has ! then var_name tc.pre_scope_vars.add = updated_pre @@ -1074,6 +1085,7 @@ impl TypeChecker { if var_name tc.out_of_scope.has then var_name tc.out_of_scope.remove = cleared_pre cleared_pre tc->out_of_scope + true = reactivated_out_of_scope fi else false = in_active_scope @@ -1102,6 +1114,7 @@ impl TypeChecker { if var_name tc.out_of_scope.has then var_name tc.out_of_scope.remove = cleared_oos cleared_oos tc->out_of_scope + true = reactivated_out_of_scope fi tc.scope_stack.length 1 - tc.scope_stack.get = cur_frame if var_name cur_frame string_list_contains ! then @@ -1115,6 +1128,7 @@ impl TypeChecker { op Op::location f"Binding `{var_name}` shadows an existing variable in scope; choose a different name" ErrorKind::DuplicateName add_error fi fi + reactivated_out_of_scope } fn check_assignment tc:TypeChecker op:Op function_opt:Option[Function] { @@ -1142,7 +1156,10 @@ impl TypeChecker { fi # Scope tracking: record variable assignment relative to block scopes. - false op var_name tc.record_var_scope + false op var_name tc.record_var_scope = reactivated_out_of_scope + if reactivated_out_of_scope then + function_opt var_name tc.reset_local_variable_type + fi # Try local variables first if function_opt.is_some then @@ -2139,12 +2156,20 @@ impl TypeChecker { f"Variant `{variant EnumVariant::enum_name}::{variant EnumVariant::variant_name}` has {inner_types.length} inner value(s), but {variant EnumVariant::bindings.length} binding(s) provided" tc.raise_type_mismatch fi fi + if tc.in_branch_condition ! then + op Op::location "`is` with bindings is only allowed in if/elif conditions" ErrorKind::InvalidScope add_error + TYPE_BOOL_T tc.stack_push_type + return + fi fi - typ.format variant function_opt tc register_enum_pattern_bindings # Track bindings in scope; implicit bindings cannot shadow outer vars. for binding_var in variant EnumVariant::bindings.iter do - true op binding_var tc.record_var_scope + true op binding_var tc.record_var_scope = reactivated_out_of_scope + if reactivated_out_of_scope then + function_opt binding_var tc.reset_local_variable_type + fi done + typ.format variant function_opt tc register_enum_pattern_bindings fi TYPE_BOOL_T tc.stack_push_type } @@ -2154,6 +2179,30 @@ impl TypeChecker { # Check match blocks # ============================================================================ +fn clone_str_set source:Set[str] -> Set[str] { + Set::new(Set[str]) = cloned + for item in source.to_list.iter do + item cloned.add = cloned + done + cloned +} + +fn clone_scope_stack source:List[List[str]] -> List[List[str]] { + List::new(List[List[str]]) = cloned + for scope_frame in source.iter do + scope_frame.clone cloned.push + done + cloned +} + +impl TypeChecker { + fn copy_scope_state_to tc:TypeChecker target:TypeChecker { + tc.scope_stack clone_scope_stack target->scope_stack + tc.out_of_scope clone_str_set target->out_of_scope + tc.pre_scope_vars clone_str_set target->pre_scope_vars + } +} + fn set_binding_type var_name:str field_type:Type function_opt:Option[Function] store:SymbolStore { if function_opt.is_some then function_opt.unwrap = function diff --git a/docs/control-flow.md b/docs/control-flow.md index 9f3714b..78387e9 100644 --- a/docs/control-flow.md +++ b/docs/control-flow.md @@ -106,6 +106,13 @@ elif shape Shape::Rectangle(w h) is then fi ``` +Bindings introduced by `is` are scoped to the branch whose condition created +them. They are not visible in sibling `elif`/`else` branches or after `fi`. +Bindings may not shadow an accessible local variable; choose a different binding +name when an outer local with the same name is already in scope. Assignments +inside a branch mutate an outer variable when one exists, otherwise they create a +branch-local variable. + ## Loops Casa has `while`/`do`/`done` loops with `break` and `continue`. @@ -268,6 +275,12 @@ end The `match` keyword pops a value from the stack. Each arm specifies a pattern followed by `=>` and a body. An optional `if ` between the pattern and `=>` adds a boolean condition that must also hold. The block is closed with `end`. +Pattern bindings are scoped to their own arm body. They are visible in that +arm's guard and body only, not in sibling arms and not after `end`. A pattern +binding may not shadow an accessible local variable. Assignments inside an arm +mutate an outer variable when one exists, otherwise they create an arm-local +variable. + ### Block Arm Bodies Multiline arm bodies require `{}`: diff --git a/docs/enums.md b/docs/enums.md index a828c3a..538d375 100644 --- a/docs/enums.md +++ b/docs/enums.md @@ -110,7 +110,7 @@ color Color::Blue is print # false ### Destructuring with `is` -Inside `if`/`elif` conditions, `is` can destructure inner values into bindings. The bindings are available in the corresponding `then`-block. +Inside `if`/`elif` conditions, `is` can destructure inner values into bindings. The bindings are available in the corresponding `then`-block only. ```casa enum Shape { @@ -145,6 +145,9 @@ if maybe Option::Some(value) is then fi ``` +The binding is not visible in sibling branches, in `else`, or after `fi`. +Implicit `is` bindings may not shadow an accessible local variable. + Using `is` with bindings outside of `if`/`elif` conditions is a compile-time error: ```casa diff --git a/tests/compiler/errors/is_binding_outside_branch.casa b/tests/compiler/errors/is_binding_outside_branch.casa new file mode 100644 index 0000000..5727a90 --- /dev/null +++ b/tests/compiler/errors/is_binding_outside_branch.casa @@ -0,0 +1,12 @@ +# expect: INVALID_SCOPE + +enum Maybe { + Just (int) + Nothing +} + +fn outside_branch m:Maybe { + m Maybe::Just(value) is drop + value drop +} +42 Maybe::Just outside_branch diff --git a/tests/compiler/test_scope.casa b/tests/compiler/test_scope.casa index 73ffc3e..15212f5 100644 --- a/tests/compiler/test_scope.casa +++ b/tests/compiler/test_scope.casa @@ -206,6 +206,41 @@ fn test_if_is_binding_shadow_error { disable_test_mode } +# ============================================================================ +# If scoping: sibling block locals may reuse a name with different types +# ============================================================================ + +fn test_if_sibling_block_local_type_reuse { + "if_sibling_block_local_type_reuse" test_start + "fn f flag:bool {\n if flag then\n \"branch\" = tmp\n tmp drop\n else\n 1 = tmp\n tmp drop\n fi\n}\ntrue f\n" parse_typecheck + "no errors reusing block-local name with different type" assert_no_errors + disable_test_mode +} + +# ============================================================================ +# If scoping: outer-scope reassignment after block may reuse a block-local name +# ============================================================================ + +fn test_if_outer_scope_redeclare_after_block { + "if_outer_scope_redeclare_after_block" test_start + "fn f {\n if true then\n \"branch\" = tmp\n tmp drop\n fi\n 1 = tmp\n tmp drop\n}\nf\n" parse_typecheck + "no errors reusing name after block scope ends" assert_no_errors + disable_test_mode +} + +# ============================================================================ +# `is` bindings outside if/elif conditions are rejected +# ============================================================================ + +fn test_standalone_is_binding_rejected { + "standalone_is_binding_rejected" test_start + "enum E { Some (int) None }\nfn f e:E {\n e E::Some(v) is drop\n v drop\n}\n42 E::Some f\n" parse_typecheck + "standalone is-binding has scope error" assert_has_errors + "invalid scope kind" ErrorKind::InvalidScope assert_error_kind + clear_errors + disable_test_mode +} + # ============================================================================ # Match scoping: an arm binding is visible inside its arm body # ============================================================================ @@ -267,6 +302,30 @@ fn test_match_arm_block_local_new_var { disable_test_mode } +# ============================================================================ +# Match scoping: sibling arm locals may reuse a name with different types +# ============================================================================ + +fn test_match_sibling_block_local_type_reuse { + "match_sibling_block_local_type_reuse" test_start + "enum E { A B }\nfn f e:E {\n e match\n E::A => { \"arm\" = tmp tmp drop }\n E::B => { 1 = tmp tmp drop }\n end\n}\nE::A f\n" parse_typecheck + "no errors reusing arm-local name with different type" assert_no_errors + disable_test_mode +} + +# ============================================================================ +# Match scoping: a guard cannot see a prior arm binding +# ============================================================================ + +fn test_match_guard_cannot_see_prior_arm_binding { + "match_guard_cannot_see_prior_arm_binding" test_start + "enum E { Some (int) None }\nfn f e:E {\n e match\n E::Some(v) => { v drop }\n E::None if 0 v > => { }\n _ => { }\n end\n}\nE::None f\n" parse_typecheck + "prior arm binding not visible in guard" assert_has_errors + "undefined name kind" ErrorKind::UndefinedName assert_error_kind + clear_errors + disable_test_mode +} + # ============================================================================ # Run tests # ============================================================================ @@ -279,11 +338,16 @@ test_if_elif_cannot_see_if_binding test_if_outer_var_mutation_in_branch test_if_block_local_new_var test_if_is_binding_shadow_error +test_if_sibling_block_local_type_reuse +test_if_outer_scope_redeclare_after_block +test_standalone_is_binding_rejected test_match_arm_binding_visible test_match_arm_binding_not_in_other_arm test_match_arm_binding_shadow_error test_match_arm_outer_var_mutation test_match_arm_block_local_new_var +test_match_sibling_block_local_type_reuse +test_match_guard_cannot_see_prior_arm_binding test_while_scope_var_not_visible_after_done test_while_scope_var_visible_inside test_for_scope_iter_var_not_visible_after_done From efeb20bf3382e8117d3f814998b35457a7a733fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Mon, 1 Jun 2026 23:24:47 +0300 Subject: [PATCH 3/3] refactor: model scoped variable types --- compiler/pattern.casa | 12 +- compiler/syntax.casa | 1 + compiler/typechecker.casa | 208 ++++++++++++++++++++++++++------- tests/compiler/test_scope.casa | 36 ++++++ 4 files changed, 206 insertions(+), 51 deletions(-) diff --git a/compiler/pattern.casa b/compiler/pattern.casa index 77e15d1..c3055e4 100644 --- a/compiler/pattern.casa +++ b/compiler/pattern.casa @@ -144,6 +144,7 @@ fn unify_arm_stacks tc:TypeChecker match_ctx:MatchContext op:Op { # by a struct destructure arm. fn assign_struct_binding_type + tc:TypeChecker matched_struct:CasaStruct field_name:str bound_var:str @@ -152,7 +153,7 @@ fn assign_struct_binding_type { for member in matched_struct CasaStruct::members.iter do if field_name member Member::name == then - store function_opt member Member::typ bound_var set_binding_type + store function_opt member Member::typ bound_var tc set_binding_type fi done } @@ -171,7 +172,7 @@ fn register_struct_pattern_bindings struct_pattern StructPattern::bindings.keys = binding_keys for field_name in binding_keys.iter do if field_name struct_pattern StructPattern::bindings.get Option::Some(bound_var) is then - tc.store function_opt bound_var field_name matched_struct assign_struct_binding_type + tc.store function_opt bound_var field_name matched_struct tc assign_struct_binding_type else empty_location "Expected option value while unwrapping field_name struct_pattern StructPattern::bindings.get" ErrorKind::Syntax raise_error 1 exit @@ -233,7 +234,7 @@ fn apply_enum_bindings if binding_type.format type_bindings.get Option::Some(resolved) is then resolved = binding_type fi - tc.store function_opt binding_type binding_var set_binding_type + tc.store function_opt binding_type binding_var tc set_binding_type 1 += bind_index done } @@ -304,10 +305,7 @@ fn typecheck_match_op tc:TypeChecker function_opt:Option[Function] op:Op { # Record pattern bindings in the arm scope; implicit bindings cannot # shadow an accessible outer variable (same rule as is-bindings). for binding_var in op pattern_bindings.iter do - true op binding_var tc.record_var_scope = reactivated_out_of_scope - if reactivated_out_of_scope then - function_opt binding_var tc.reset_local_variable_type - fi + true op binding_var tc.record_var_scope done match_subject_type op function_opt tc register_pattern_bindings diff --git a/compiler/syntax.casa b/compiler/syntax.casa index 11afbc5..9b00d87 100644 --- a/compiler/syntax.casa +++ b/compiler/syntax.casa @@ -1953,6 +1953,7 @@ fn parse_impl_block parser:Parser cursor:TokenCursor { # Get base type name (without type params) impl_type "[" str::find = bracket + "" = base_type if 0 bracket > then impl_type 0 bracket str::substring = base_type else diff --git a/compiler/typechecker.casa b/compiler/typechecker.casa index 771de7c..e1b9d3c 100644 --- a/compiler/typechecker.casa +++ b/compiler/typechecker.casa @@ -133,10 +133,14 @@ struct TypeChecker { scope_stack: List[List[str]] out_of_scope: Set[str] pre_scope_vars: Set[str] + scope_type_stack: List[Map[str Type]] + pre_scope_types: Map[str Type] } impl TypeChecker { fn new store:SymbolStore -> TypeChecker { + Map::new(Map[str Type]) # pre_scope_types + List::new(List[Map[str Type]]) # scope_type_stack Set::new(Set[str]) # pre_scope_vars Set::new(Set[str]) # out_of_scope List::new(List[List[str]]) # scope_stack @@ -1023,21 +1027,90 @@ fn unify_variable_assignment fi } +fn unify_assignment_type + op:Op + var_name:str + current_type:Type + effective_type:Type + scope:str + store:SymbolStore +-> Type { + current_type = stored_type + stored_type.is_unknown stored_type.is_unknown_placeholder || = pre_unknown + if pre_unknown effective_type.is_unknown_placeholder ! && then + effective_type = stored_type + fi + store effective_type stored_type unify_type_t = unified + if unified.is_none then + op Op::location f"Cannot override {scope} variable `{var_name}` of type `{stored_type.format}` with other type `{effective_type.format}`" ErrorKind::InvalidVariable add_error + stored_type + elif unified Option::Some(unified_type) is then + unified_type + else + stored_type + fi +} + impl TypeChecker { - fn reset_local_variable_type tc:TypeChecker var_name:str function_opt:Option[Function] { + fn active_scope_has_var tc:TypeChecker var_name:str -> bool { + 0 = scope_idx + while scope_idx tc.scope_stack.length > do + scope_idx tc.scope_stack.get = active_frame + if var_name active_frame string_list_contains then + true return + fi + 1 += scope_idx + done + false + } + + fn active_scoped_var_type tc:TypeChecker var_name:str -> Option[Type] { + tc.scope_type_stack.length 1 - = scope_idx + while 0 scope_idx >= do + scope_idx tc.scope_type_stack.get = type_frame + if var_name type_frame.get Option::Some(var_type) is then + var_type Option::Some return + fi + scope_idx 1 - = scope_idx + done + Option::None(Option[Type]) + } + + fn set_active_scoped_var_type tc:TypeChecker var_name:str var_type:Type -> bool { + tc.scope_type_stack.length 1 - = scope_idx + while 0 scope_idx >= do + scope_idx tc.scope_type_stack.get = type_frame + if var_name type_frame.has then + var_type var_name type_frame.set = type_frame + type_frame scope_idx tc.scope_type_stack.set + true return + fi + scope_idx 1 - = scope_idx + done + false + } + + fn local_var_type tc:TypeChecker var_name:str function_opt:Option[Function] -> Option[Type] { + if var_name tc.active_scoped_var_type Option::Some(scoped_type) is then + scoped_type Option::Some return + fi + if var_name tc.pre_scope_types.get Option::Some(pre_type) is then + pre_type Option::Some return + fi if function_opt Option::Some(function) is then var_name function Function::variables variable_list_index = local_index if 0 local_index >= then - local_index function Function::variables.get = local_var - Type::Unknown local_var Variable::set_typ + local_index function Function::variables.get Variable::typ Option::Some return fi fi + Option::None(Option[Type]) } # Push a fresh scope frame for a new block branch (while body, if/elif/else # branch, or match arm). Names first assigned inside the branch land here. fn push_block_scope tc:TypeChecker { List::new(List[str]) tc.scope_stack.push + Map::new(Map[str Type]) tc.scope_type_stack.push } # Pop the innermost scope frame at a block boundary. Each name that is not @@ -1046,6 +1119,9 @@ impl TypeChecker { fn pop_block_scope tc:TypeChecker { if 0 tc.scope_stack.length == then return fi tc.scope_stack.pop = popped_frame + if 0 tc.scope_type_stack.length != then + tc.scope_type_stack.pop drop + fi for exiting_name in popped_frame.iter do # Only move to out_of_scope if not still active in an outer frame false = still_active @@ -1073,8 +1149,7 @@ impl TypeChecker { # `is_binding` marks implicit pattern/is-bindings: when such a name would # otherwise resolve to an accessible outer variable it is a shadow error, # whereas an explicit `= x` is allowed to mutate that outer variable. - fn record_var_scope tc:TypeChecker var_name:str op:Op is_binding:bool -> bool { - false = reactivated_out_of_scope + fn record_var_scope tc:TypeChecker var_name:str op:Op is_binding:bool { if 0 tc.scope_stack.length == then if var_name tc.pre_scope_vars.has ! then var_name tc.pre_scope_vars.add = updated_pre @@ -1085,18 +1160,9 @@ impl TypeChecker { if var_name tc.out_of_scope.has then var_name tc.out_of_scope.remove = cleared_pre cleared_pre tc->out_of_scope - true = reactivated_out_of_scope fi else - false = in_active_scope - 0 = scope_idx - while scope_idx tc.scope_stack.length > do - scope_idx tc.scope_stack.get = active_frame - if var_name active_frame string_list_contains then - true = in_active_scope - fi - 1 += scope_idx - done + var_name tc.active_scope_has_var = in_active_scope false = is_pre_scope_global if var_name tc.store.variables.has then @@ -1114,11 +1180,15 @@ impl TypeChecker { if var_name tc.out_of_scope.has then var_name tc.out_of_scope.remove = cleared_oos cleared_oos tc->out_of_scope - true = reactivated_out_of_scope fi - tc.scope_stack.length 1 - tc.scope_stack.get = cur_frame + tc.scope_stack.length 1 - = cur_scope_index + cur_scope_index tc.scope_stack.get = cur_frame if var_name cur_frame string_list_contains ! then var_name cur_frame.push + cur_frame cur_scope_index tc.scope_stack.set + cur_scope_index tc.scope_type_stack.get = cur_type_frame + Type::Unknown var_name cur_type_frame.set = cur_type_frame + cur_type_frame cur_scope_index tc.scope_type_stack.set fi elif is_binding is_pre_scope_global ! && then # An implicit pattern/is binding would shadow an accessible @@ -1128,7 +1198,6 @@ impl TypeChecker { op Op::location f"Binding `{var_name}` shadows an existing variable in scope; choose a different name" ErrorKind::DuplicateName add_error fi fi - reactivated_out_of_scope } fn check_assignment tc:TypeChecker op:Op function_opt:Option[Function] { @@ -1156,9 +1225,13 @@ impl TypeChecker { fi # Scope tracking: record variable assignment relative to block scopes. - false op var_name tc.record_var_scope = reactivated_out_of_scope - if reactivated_out_of_scope then - function_opt var_name tc.reset_local_variable_type + false op var_name tc.record_var_scope + + if var_name tc.active_scoped_var_type Option::Some(scoped_type) is then + tc.store "local" effective_type_t scoped_type var_name op unify_assignment_type = unified_scoped + unified_scoped var_name tc.set_active_scoped_var_type drop + effective_type_t tc.expect_type_t + return fi # Try local variables first @@ -1167,7 +1240,16 @@ impl TypeChecker { var_name function Function::variables variable_list_index = local_index if 0 local_index >= then local_index function Function::variables.get = local_var - tc.store "local" effective_type_t local_var op unify_variable_assignment + Type::Unknown = current_local_type + if var_name tc.pre_scope_types.get Option::Some(pre_type) is then + pre_type = current_local_type + else + local_var Variable::typ = current_local_type + fi + tc.store "local" effective_type_t current_local_type var_name op unify_assignment_type = unified_local + unified_local var_name tc.pre_scope_types.set = updated_pre_types + updated_pre_types tc->pre_scope_types + unified_local local_var Variable::set_typ effective_type_t tc.expect_type_t return fi @@ -1237,10 +1319,9 @@ impl TypeChecker { if op.value OpValue::TraitMethodCall(trait_var_name) is then if function_opt Option::Some(function) is then - trait_var_name function Function::variables variable_list_index = trait_index - if 0 trait_index >= then - if trait_index function Function::variables.get Variable::typ.is_unknown ! then - trait_index function Function::variables.get Variable::typ tc.stack_push_type + if function_opt trait_var_name tc.local_var_type Option::Some(trait_var_type) is then + if trait_var_type.is_unknown ! then + trait_var_type tc.stack_push_type else TYPE_UNKNOWN_T tc.stack_push_type fi @@ -1254,25 +1335,23 @@ impl TypeChecker { # PUSH_VARIABLE if op.value OpValue::PushVar(var_name) is then - # Check if variable has gone out of scope (was only in a popped block scope) - if var_name tc.out_of_scope.has then + var_name tc.active_scoped_var_type = active_type_opt + var_name tc.pre_scope_types.get = pre_type_opt + # Check if variable has gone out of scope (was only in a popped block scope). + if var_name tc.out_of_scope.has active_type_opt.is_none && pre_type_opt.is_none && then op Op::location f"Variable `{var_name}` is not accessible outside its block scope" ErrorKind::UndefinedName add_error TYPE_UNKNOWN_T tc.stack_push_type return fi # Check local variables first - if function_opt.is_some then - function_opt.unwrap = function - var_name function Function::variables variable_list_index = local_index - if 0 local_index >= then - if local_index function Function::variables.get Variable::typ.is_unknown ! then - local_index function Function::variables.get Variable::typ tc.stack_push_type - else - TYPE_UNKNOWN_T tc.stack_push_type - fi - return + if function_opt var_name tc.local_var_type Option::Some(local_type) is then + if local_type.is_unknown ! then + local_type tc.stack_push_type + else + TYPE_UNKNOWN_T tc.stack_push_type fi + return fi # Check global variables @@ -1852,6 +1931,18 @@ impl TypeChecker { # ============================================================================ impl TypeChecker { + fn sync_capture_types tc:TypeChecker lambda:Function function_opt:Option[Function] { + if function_opt.is_none then return fi + for capture in lambda Function::captures.iter do + capture Variable::name = capture_name + if function_opt capture_name tc.local_var_type Option::Some(capture_type) is then + if capture_type.is_unknown ! then + capture_type capture Variable::set_typ + fi + fi + done + } + fn check_function_ops tc:TypeChecker op:Op @@ -1888,6 +1979,7 @@ impl TypeChecker { push_name tc.store.functions.get = push_fn_opt if push_fn_opt.is_some then push_fn_opt.unwrap = push_fn + function_opt push_fn tc.sync_capture_types tc.store push_fn ensure_typechecked push_fn Function::stack_effect stack_effect_to_function_type tc.stack_push_type else @@ -2164,10 +2256,7 @@ impl TypeChecker { fi # Track bindings in scope; implicit bindings cannot shadow outer vars. for binding_var in variant EnumVariant::bindings.iter do - true op binding_var tc.record_var_scope = reactivated_out_of_scope - if reactivated_out_of_scope then - function_opt binding_var tc.reset_local_variable_type - fi + true op binding_var tc.record_var_scope done typ.format variant function_opt tc register_enum_pattern_bindings fi @@ -2187,6 +2276,14 @@ fn clone_str_set source:Set[str] -> Set[str] { cloned } +fn clone_type_map source:Map[str Type] -> Map[str Type] { + Map::new(Map[str Type]) = cloned + for pair in source.iter do + pair.second pair.first cloned.set = cloned + done + cloned +} + fn clone_scope_stack source:List[List[str]] -> List[List[str]] { List::new(List[List[str]]) = cloned for scope_frame in source.iter do @@ -2195,20 +2292,43 @@ fn clone_scope_stack source:List[List[str]] -> List[List[str]] { cloned } +fn clone_scope_type_stack source:List[Map[str Type]] -> List[Map[str Type]] { + List::new(List[Map[str Type]]) = cloned + for scope_frame in source.iter do + scope_frame clone_type_map cloned.push + done + cloned +} + impl TypeChecker { fn copy_scope_state_to tc:TypeChecker target:TypeChecker { tc.scope_stack clone_scope_stack target->scope_stack tc.out_of_scope clone_str_set target->out_of_scope tc.pre_scope_vars clone_str_set target->pre_scope_vars + tc.scope_type_stack clone_scope_type_stack target->scope_type_stack + tc.pre_scope_types clone_type_map target->pre_scope_types } } -fn set_binding_type var_name:str field_type:Type function_opt:Option[Function] store:SymbolStore { +fn set_binding_type + tc:TypeChecker + var_name:str + field_type:Type + function_opt:Option[Function] + store:SymbolStore +{ + if field_type var_name tc.set_active_scoped_var_type then return fi + if var_name tc.pre_scope_types.has then + field_type var_name tc.pre_scope_types.set = updated_pre_types + updated_pre_types tc->pre_scope_types + return + fi if function_opt.is_some then function_opt.unwrap = function for variable in function Function::variables.iter do if var_name variable Variable::name == then - field_type variable Variable::set_typ + field_type var_name tc.pre_scope_types.set = updated_pre_types_2 + updated_pre_types_2 tc->pre_scope_types return fi done diff --git a/tests/compiler/test_scope.casa b/tests/compiler/test_scope.casa index 15212f5..f683e69 100644 --- a/tests/compiler/test_scope.casa +++ b/tests/compiler/test_scope.casa @@ -119,6 +119,17 @@ fn test_if_is_binding_visible_in_then { disable_test_mode } +# ============================================================================ +# If is-binding: bound name keeps its type when captured by a f-string lambda +# ============================================================================ + +fn test_if_is_binding_visible_in_fstring_capture { + "if_is_binding_visible_in_fstring_capture" test_start + "enum E { Some (str) None }\nfn f e:E {\n if e E::Some(v) is then f\"{v}\" drop fi\n}\n\"ok\" E::Some f\n" parse_typecheck + "no errors capturing is-binding in f-string" assert_no_errors + disable_test_mode +} + # ============================================================================ # If is-binding: bound name is NOT visible after fi # ============================================================================ @@ -228,6 +239,17 @@ fn test_if_outer_scope_redeclare_after_block { disable_test_mode } +# ============================================================================ +# If scoping: predeclared locals assigned in branches stay visible after fi +# ============================================================================ + +fn test_if_predeclared_var_assigned_in_branches_visible_after_fi { + "if_predeclared_var_assigned_in_branches_visible_after_fi" test_start + "fn f flag:bool {\n \"\" = label\n if flag then\n \"left\" = label\n else\n \"right\" = label\n fi\n label drop\n}\ntrue f\n" parse_typecheck + "no errors using predeclared branch-assigned var after fi" assert_no_errors + disable_test_mode +} + # ============================================================================ # `is` bindings outside if/elif conditions are rejected # ============================================================================ @@ -252,6 +274,17 @@ fn test_match_arm_binding_visible { disable_test_mode } +# ============================================================================ +# Match scoping: arm binding keeps its type when captured by a f-string lambda +# ============================================================================ + +fn test_match_arm_binding_visible_in_fstring_capture { + "match_arm_binding_visible_in_fstring_capture" test_start + "enum E { Some (str) None }\nfn f e:E {\n e match\n E::Some(v) => { f\"{v}\" drop }\n E::None => { }\n end\n}\n\"ok\" E::Some f\n" parse_typecheck + "no errors capturing arm binding in f-string" assert_no_errors + disable_test_mode +} + # ============================================================================ # Match scoping: an arm binding is not visible in another arm # ============================================================================ @@ -331,6 +364,7 @@ fn test_match_guard_cannot_see_prior_arm_binding { # ============================================================================ test_if_scope_var_not_visible_after_fi test_if_is_binding_visible_in_then +test_if_is_binding_visible_in_fstring_capture test_if_is_binding_not_visible_after_fi test_if_is_binding_not_visible_in_else test_if_elif_independent_bindings @@ -340,8 +374,10 @@ test_if_block_local_new_var test_if_is_binding_shadow_error test_if_sibling_block_local_type_reuse test_if_outer_scope_redeclare_after_block +test_if_predeclared_var_assigned_in_branches_visible_after_fi test_standalone_is_binding_rejected test_match_arm_binding_visible +test_match_arm_binding_visible_in_fstring_capture test_match_arm_binding_not_in_other_arm test_match_arm_binding_shadow_error test_match_arm_outer_var_mutation