From 61f1b513694407b48013c89bceaa96565801bd6c Mon Sep 17 00:00:00 2001 From: kbkpbot Date: Sun, 1 Mar 2026 10:59:08 +0800 Subject: [PATCH] cgen: fix inline if expression returning closure (fixes #26595) Fixed a bug where inline if expressions could not return a closure when: - Local variables are declared in the if block before the closure literal - The else branch returns an existing function-pointer variable The fix ensures that function pointer types in if expressions are generated correctly, using inline function pointer declarations instead of relying on typedefs that may not exist for closure-specific type names. Co-authored-by: iFlow CLI --- vlib/v/gen/c/if.v | 13 +- vlib/v/tests/closure_in_if_expr_test.v | 202 +++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 vlib/v/tests/closure_in_if_expr_test.v diff --git a/vlib/v/gen/c/if.v b/vlib/v/gen/c/if.v index 8d38fa6da91e49..de9789d1957a7e 100644 --- a/vlib/v/gen/c/if.v +++ b/vlib/v/gen/c/if.v @@ -282,9 +282,18 @@ fn (mut g Gen) if_expr(node ast.IfExpr) { // nested if on return stmt g.write2(g.styp(g.unwrap_generic(g.last_if_option_type)), ' ') } else { - g.write('${styp} ') + // For function types, generate the function pointer declaration inline + // to avoid issues with closure-specific type names that lack typedefs + resolved_sym := g.table.sym(resolved_typ) + if resolved_sym.kind == .function && resolved_sym.info is ast.FnType + && !resolved_typ.has_option_or_result() { + g.write_fn_ptr_decl(&resolved_sym.info, tmp) + g.writeln('; /* if prepend */') + } else { + g.write('${styp} ') + g.writeln('${tmp}; /* if prepend */') + } } - g.writeln('${tmp}; /* if prepend */') g.set_current_pos_as_last_stmt_pos() } if g.infix_left_var_name.len > 0 { diff --git a/vlib/v/tests/closure_in_if_expr_test.v b/vlib/v/tests/closure_in_if_expr_test.v new file mode 100644 index 00000000000000..f605012fefe718 --- /dev/null +++ b/vlib/v/tests/closure_in_if_expr_test.v @@ -0,0 +1,202 @@ +// Test for closures in if expressions +// Regression test for https://github.com/vlang/v/issues/26595 +// +// Bug: inline `if` expression cannot return a closure when +// local variables are declared in the `if` block before the +// closure literal, and the else branch returns an existing +// function-pointer variable. +// +// The fix ensures that function pointer types in if expressions +// are generated correctly, using inline function pointer declarations +// instead of relying on typedefs that may not exist for closure types. +module main + +struct Event { +mut: + value int +} + +struct Cfg { + delay int + callback fn (int, mut Event) = unsafe { nil } +} + +// Test closure with captured variable in if expression +fn test_closure_in_if_expr_with_capture() { + cond := true + + result := if cond { + tag := 'captured' + fn [tag] (x int, mut e Event) { + assert tag == 'captured' + } + } else { + fn (x int, mut e Event) {} + } + + mut e := Event{} + result(0, mut e) +} + +// Test two closures with captures in if expression +fn test_two_closures_in_if_expr() { + cond1 := true + cond2 := false + + // Test first branch taken + result1 := if cond1 { + tag := 'first' + fn [tag] (x int, mut e Event) { + assert tag == 'first' + } + } else { + tag := 'second' + fn [tag] (x int, mut e Event) { + assert tag == 'second' + } + } + + mut e := Event{} + result1(0, mut e) + + // Test second branch taken + result2 := if cond2 { + tag := 'first' + fn [tag] (x int, mut e Event) { + assert tag == 'first' + } + } else { + tag := 'second' + fn [tag] (x int, mut e Event) { + assert tag == 'second' + } + } + + result2(0, mut e) +} + +// Test closure in if expression with function type return +// This is the core pattern from issue #26595: +// - if branch has local declarations before closure literal +// - else branch returns struct field (function pointer) +fn resolve(cfg &Cfg) fn (int, mut Event) { + result := if cfg.delay > 0 { + tag := 'tag' + fn [cfg, tag] (x int, mut e Event) { + assert tag == 'tag' + assert cfg.delay == 100 + } + } else { + cfg.callback + } + return result +} + +fn test_closure_with_struct_field_capture() { + callback := fn (x int, mut e Event) { + e.value = 42 + } + + cfg := Cfg{ + delay: 100 + callback: callback + } + + resolved_fn := resolve(&cfg) + mut e := Event{} + resolved_fn(0, mut e) + + // Test the else branch + cfg2 := Cfg{ + delay: 0 + callback: callback + } + resolved_fn2 := resolve(&cfg2) + mut e2 := Event{} + resolved_fn2(0, mut e2) + assert e2.value == 42 +} + +// Test nested if expressions with closures +fn test_nested_if_expr_with_closures() { + outer_cond := true + inner_cond := true + + result := if outer_cond { + tag1 := 'outer' + if inner_cond { + tag2 := 'inner' + fn [tag1, tag2] () string { + return '${tag1}_${tag2}' + } + } else { + fn [tag1] () string { + return tag1 + } + } + } else { + fn () string { + return 'none' + } + } + + assert result() == 'outer_inner' +} + +// Test closure in if expression with different return types +fn test_closure_returning_value() { + cond := true + + result := if cond { + multiplier := 2 + fn [multiplier] (n int) int { + return n * multiplier + } + } else { + fn (n int) int { + return n + } + } + + assert result(5) == 10 +} + +// Test the exact pattern from issue #26595 +fn test_issue_26595_pattern() { + struct Event2 {} + + struct Cfg2 { + delay int + callback fn (&int, mut Event2) = unsafe { nil } + } + + resolve2 := fn (cfg &Cfg2) fn (&int, mut Event2) { + // This pattern previously caused C compilation error: + // "error: expected ';' after expression" + // because the closure type name lacked a typedef + result := if cfg.delay > 0 { + tag := 'tag' + fn [cfg, tag] (x &int, mut e Event2) { + dump('${tag}') + } + } else { + cfg.callback + } + return result + } + + resolve2(&Cfg2{ + delay: 2000 + callback: fn (x &int, mut e Event2) {} + }) +} + +fn main() { + test_closure_in_if_expr_with_capture() + test_two_closures_in_if_expr() + test_closure_with_struct_field_capture() + test_nested_if_expr_with_closures() + test_closure_returning_value() + test_issue_26595_pattern() + println('All tests passed!') +}