From d95f7866d228d3e7a4f5f3e7c0abf2f752087cff Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Wed, 22 Oct 2025 16:37:28 +0300 Subject: [PATCH] pref,cgen: add `-no-closures` option to detect closure usage earlier (for emscripten or for less well supported platforms) --- .github/workflows/other_ci.yml | 5 ++++- vlib/v/compiler_errors_test.v | 3 +++ vlib/v/gen/c/cgen.v | 3 +++ vlib/v/gen/c/fn.v | 3 +++ vlib/v/help/build/build-c.txt | 9 +++++++++ vlib/v/pref/pref.v | 4 ++++ vlib/v/tests/no_closures/method_closure.out | 7 +++++++ vlib/v/tests/no_closures/method_closure.vv | 13 +++++++++++++ vlib/v/tests/no_closures/simple_closure.out | 7 +++++++ vlib/v/tests/no_closures/simple_closure.vv | 8 ++++++++ 10 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 vlib/v/tests/no_closures/method_closure.out create mode 100644 vlib/v/tests/no_closures/method_closure.vv create mode 100644 vlib/v/tests/no_closures/simple_closure.out create mode 100644 vlib/v/tests/no_closures/simple_closure.vv diff --git a/.github/workflows/other_ci.yml b/.github/workflows/other_ci.yml index 481565e8f90f3f..4a0386d1567d21 100644 --- a/.github/workflows/other_ci.yml +++ b/.github/workflows/other_ci.yml @@ -137,7 +137,10 @@ jobs: # NB: this does not mean it runs, but at least keeps it from regressing - name: Ensure V can be compiled with -autofree - run: ./v -autofree -o v2 cmd/v + run: ./v -autofree cmd/v + + - name: Ensure V can be compiled with -no-closures + run: ./v -no-closures cmd/v - name: Shader examples can be built run: | diff --git a/vlib/v/compiler_errors_test.v b/vlib/v/compiler_errors_test.v index 8614564413b469..f41f75e745e42f 100644 --- a/vlib/v/compiler_errors_test.v +++ b/vlib/v/compiler_errors_test.v @@ -91,6 +91,7 @@ fn test_all() { global_run_dir := '${checker_dir}/globals_run' run_dir := '${checker_dir}/run' skip_unused_dir := 'vlib/v/tests/skip_unused' + no_closures_dir := 'vlib/v/tests/no_closures' checker_tests := get_tests_in_dir(checker_dir, false).filter(!it.contains('with_check_option')) parser_tests := get_tests_in_dir(parser_dir, false) @@ -100,6 +101,7 @@ fn test_all() { module_tests := get_tests_in_dir(module_dir, true) run_tests := get_tests_in_dir(run_dir, false) skip_unused_dir_tests := get_tests_in_dir(skip_unused_dir, false) + no_closures_tests := get_tests_in_dir(no_closures_dir, false) checker_with_check_option_tests := get_tests_in_dir(checker_with_check_option_dir, false) mut tasks := Tasks{ @@ -118,6 +120,7 @@ fn test_all() { tasks.add('', run_dir, 'run', '.run.out', run_tests, false) tasks.add('', checker_with_check_option_dir, '-check', '.out', checker_with_check_option_tests, false) + tasks.add('', no_closures_dir, '-no-closures run', '.out', no_closures_tests, false) tasks.run() if os.user_os() == 'linux' { diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index e0f1eeeb684c81..9b0b7e2bac80a2 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -4465,6 +4465,9 @@ fn (mut g Gen) selector_expr(node ast.SelectorExpr) { if name !in g.anon_fns { g.anon_fns << name g.gen_closure_fn(expr_styp, m, name) + if g.pref.no_closures { + g.error('a closure was generated for m.name: ${m.name}', node.pos) + } } } g.write('builtin__closure__closure_create(${name}, ') diff --git a/vlib/v/gen/c/fn.v b/vlib/v/gen/c/fn.v index 5dd2295d4a49fe..da0b9ac0b80534 100644 --- a/vlib/v/gen/c/fn.v +++ b/vlib/v/gen/c/fn.v @@ -421,6 +421,9 @@ fn (mut g Gen) gen_fn_decl(node &ast.FnDecl, skip bool) { node.is_c_variadic) if is_closure { g.nr_closures++ + if g.pref.no_closures { + g.error('a closure was generated for function', node.pos) + } } arg_str := g.out.after(arg_start_pos) if node.no_body || ((g.pref.use_cache && g.pref.build_mode != .build_module) && node.is_builtin diff --git a/vlib/v/help/build/build-c.txt b/vlib/v/help/build/build-c.txt index 9dd1ad5b996d80..612942a10f1ff5 100644 --- a/vlib/v/help/build/build-c.txt +++ b/vlib/v/help/build/build-c.txt @@ -322,6 +322,15 @@ see also `v help build`. user,gcboehm,eval user,gg_record_trace,skip + -no-closures + Produce a compile time error early, if V generates a closure. That happens implicitly, + if you try to treat methods as values, or explicitly through `fn [captures] (){}`. + This option is useful to prevent accidental introduction of closures for environments, + that do not support closures well (projects targeting wasm, or projects that have to be + ported for less supported platforms, that do not have implementations for the closure thunks). + Note: the CI for the V compiler, checks that V itself, can be compiled with `-no-closures`, + to ease porting. + -no-rsp By default, V passes all C compiler options to the backend C compiler in so called "response files" (https://gcc.gnu.org/wiki/Response_Files). diff --git a/vlib/v/pref/pref.v b/vlib/v/pref/pref.v index 2262b518dc1632..641ab921a3f189 100644 --- a/vlib/v/pref/pref.v +++ b/vlib/v/pref/pref.v @@ -188,6 +188,7 @@ pub mut: bare_builtin_dir string // Set by -bare-builtin-dir xyz/ . The xyz/ module should contain implementations of malloc, memset, etc, that are used by the rest of V's `builtin` module. That option is only useful with -freestanding (i.e. when is_bare is true). no_preludes bool // Prevents V from generating preludes in resulting .c files custom_prelude string // Contents of custom V prelude that will be prepended before code in resulting .c files + no_closures bool // Produce a compile time error, if a closure was generated for any reason (an implicit receiver method was stored, or an explicit `fn [captured]()`). cmain string // The name of the generated C main function. Useful with framework like code, that uses macros to re-define `main`, like SDL2 does. When set, V will always generate `int THE_NAME(int ___argc, char** ___argv){`, *no matter* the platform. lookup_path []string output_cross_c bool // true, when the user passed `-os cross` or `-cross` @@ -809,6 +810,9 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin res.skip_notes = true res.notes_are_errors = false } + '-no-closures' { + res.no_closures = true + } '-no-rsp' { res.no_rsp = true } diff --git a/vlib/v/tests/no_closures/method_closure.out b/vlib/v/tests/no_closures/method_closure.out new file mode 100644 index 00000000000000..e0d7373b0b8344 --- /dev/null +++ b/vlib/v/tests/no_closures/method_closure.out @@ -0,0 +1,7 @@ +vlib/v/tests/no_closures/method_closure.vv:9:15: cgen error: a closure was generated for m.name: member + 7 | + 8 | fn main() { + 9 | x := UInt(4).member // generate an implicit closure that captures the receiver 4 + | ~~~~~~ + 10 | res := x() + 11 | assert res == 40 diff --git a/vlib/v/tests/no_closures/method_closure.vv b/vlib/v/tests/no_closures/method_closure.vv new file mode 100644 index 00000000000000..0540b3e993000f --- /dev/null +++ b/vlib/v/tests/no_closures/method_closure.vv @@ -0,0 +1,13 @@ +type UInt = u32 + +fn (me UInt) member() u32 { + println('member called') + return me * 10 +} + +fn main() { + x := UInt(4).member // generate an implicit closure that captures the receiver 4 + res := x() + assert res == 40 + println('ok') +} diff --git a/vlib/v/tests/no_closures/simple_closure.out b/vlib/v/tests/no_closures/simple_closure.out new file mode 100644 index 00000000000000..63ef529f450b54 --- /dev/null +++ b/vlib/v/tests/no_closures/simple_closure.out @@ -0,0 +1,7 @@ +vlib/v/tests/no_closures/simple_closure.vv:3:8: cgen error: a closure was generated for function + 1 | fn main() { + 2 | my_var := 12 + 3 | c1 := fn [my_var] () int { + | ~~~~~~~~~~~~~~~~~~~~ + 4 | return my_var + 5 | } diff --git a/vlib/v/tests/no_closures/simple_closure.vv b/vlib/v/tests/no_closures/simple_closure.vv new file mode 100644 index 00000000000000..92b4bdd113a0c5 --- /dev/null +++ b/vlib/v/tests/no_closures/simple_closure.vv @@ -0,0 +1,8 @@ +fn main() { + my_var := 12 + c1 := fn [my_var] () int { + return my_var + } + assert c1() == 12 + println('ok') +}