Skip to content

Conversation

@demizer
Copy link
Contributor

@demizer demizer commented Jan 25, 2026

Summary

Fix C compilation error when unused functions return fixed arrays of custom structs.

Fixes #26439

Changes

  • Skip generating fn_ret wrapper structs when the element type is a non-builtin struct that isn't marked as used
  • Add test case for the fix

Problem

When skip_unused is enabled, functions returning [N]CustomStruct that are never called cause C compilation errors:

error: ';' expected (got "main__QuadVertex")

The wrapper struct (_v_Array_fixed_main__QuadVertex_4) was generated, but the element type (main__QuadVertex) was not, because skip_unused determined it was unused.

Solution

In write_array_fixed_return_types(), skip generating the wrapper struct if its element type is an unused custom struct:

elem_sym := g.table.sym(info.elem_type)
if g.pref.skip_unused && elem_sym.kind == .struct && !elem_sym.is_builtin()
    && elem_sym.idx !in g.table.used_features.used_syms {
    continue
}

Test plan

  • Added vlib/v/tests/fns/unused_fn_fixed_array_ret_test.v
  • Verified test fails without fix, passes with fix
  • v test vlib/v/tests/fns/ passes (146/146)

When `skip_unused` is enabled, generating wrapper structs for fn_ret
ArrayFixed types that reference unused custom structs causes C errors.

Skip generating the wrapper when the element type struct is not in
used_syms.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@demizer
Copy link
Contributor Author

demizer commented Jan 25, 2026

test is best effort, this bug presents itself on a 57k+ line library that is not public so it's hard to test.

@demizer
Copy link
Contributor Author

demizer commented Jan 25, 2026

Also, used claude code to find and fix the bug. So added authored-by, I can remove if that is not desired.

@JalonSolov
Copy link
Contributor

We don't mind if you use AI to help you. As long as you use it responsibly, and check for correctness, it's fine.

@spytheman spytheman requested a review from felipensp January 25, 2026 19:11
@spytheman
Copy link
Member

@felipensp the PR looks good to me (thank you @demizer), but I wonder if there is another alternative fix, done in vlib/v/markused/ to prevent the issue. Can you please take a look at it too, when you have time?

@demizer
Copy link
Contributor Author

demizer commented Jan 25, 2026

Will take a look.

@demizer
Copy link
Contributor Author

demizer commented Jan 25, 2026

This is the patch for vlib fix that passes with test-all, seems more complex to me with the added state tracking. Perhaps this can be optimized.

diff --git a/vlib/v/ast/table.v b/vlib/v/ast/table.v
index c3b7a25e5..7c008fe81 100644
--- a/vlib/v/ast/table.v
+++ b/vlib/v/ast/table.v
@@ -11,31 +11,32 @@ import v.token
 @[heap; minify]
 pub struct UsedFeatures {
 pub mut:
-	dump           bool            // filled in by markused
-	anon_fn        bool            // fn () { }
-	auto_str       bool            // auto str fns
-	auto_str_ptr   bool            // auto str fns for ptr type
-	auto_str_arr   bool            // auto str fns for array
-	arr_prepend    bool            // arr.prepend()
-	arr_insert     bool            // arr.insert()
-	arr_first      bool            // arr.first()
-	arr_last       bool            // arr.last()
-	arr_pop_left   bool            // arr.pop_left()
-	arr_pop        bool            // arr.pop()
-	arr_delete     bool            // arr.delete()
-	arr_reverse    bool            // arr.reverse()
-	arr_map        bool            // []map[key]value
-	print_options  bool            // print option type
-	safe_int       bool            // needs safe int comparison
-	print_types    map[int]bool    // print() idx types
-	used_fns       map[string]bool // filled in by markused
-	used_consts    map[string]bool // filled in by markused
-	used_globals   map[string]bool // filled in by markused
-	used_syms      map[int]bool    // filled in by markused
-	used_veb_types []Type          // veb context types, filled in by checker
-	used_maps      int             // how many times maps were used, filled in by markused
-	used_none      int             // how many times `none` was used, filled in by markused
-	used_closures  int             // number of used closures, either directly with `fn [state] () {}`, or indirectly (though `instance.method` promotions)
+	dump              bool            // filled in by markused
+	anon_fn           bool            // fn () { }
+	auto_str          bool            // auto str fns
+	auto_str_ptr      bool            // auto str fns for ptr type
+	auto_str_arr      bool            // auto str fns for array
+	arr_prepend       bool            // arr.prepend()
+	arr_insert        bool            // arr.insert()
+	arr_first         bool            // arr.first()
+	arr_last          bool            // arr.last()
+	arr_pop_left      bool            // arr.pop_left()
+	arr_pop           bool            // arr.pop()
+	arr_delete        bool            // arr.delete()
+	arr_reverse       bool            // arr.reverse()
+	arr_map           bool            // []map[key]value
+	print_options     bool            // print option type
+	safe_int          bool            // needs safe int comparison
+	print_types       map[int]bool    // print() idx types
+	used_fns          map[string]bool // filled in by markused
+	used_consts       map[string]bool // filled in by markused
+	used_globals      map[string]bool // filled in by markused
+	used_syms         map[int]bool    // filled in by markused
+	used_fn_ret_types map[int]bool    // filled in by markused
+	used_veb_types    []Type          // veb context types, filled in by checker
+	used_maps         int             // how many times maps were used, filled in by markused
+	used_none         int             // how many times `none` was used, filled in by markused
+	used_closures     int             // number of used closures, either directly with `fn [state] () {}`, or indirectly (though `instance.method` promotions)
 	// json             bool            // json is imported
 	comptime_calls map[string]bool // resolved name to call on comptime
 	comptime_syms  map[Type]bool   // resolved syms (generic)
diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v
index 5391666ce..f906d978b 100644
--- a/vlib/v/gen/c/cgen.v
+++ b/vlib/v/gen/c/cgen.v
@@ -2029,6 +2029,9 @@ pub fn (mut g Gen) write_array_fixed_return_types() {
 			// unresolved sizes e.g. [unknown_const]int
 			continue
 		}
+		if g.pref.skip_unused && sym.idx !in g.table.used_features.used_fn_ret_types {
+			continue
+		}
 		mut fixed_elem_name := g.styp(info.elem_type.set_nr_muls(0))
 		if info.elem_type.is_ptr() {
 			fixed_elem_name += '*'.repeat(info.elem_type.nr_muls())
diff --git a/vlib/v/markused/markused.v b/vlib/v/markused/markused.v
index 6a4205efa..49579f3da 100644
--- a/vlib/v/markused/markused.v
+++ b/vlib/v/markused/markused.v
@@ -313,6 +313,7 @@ pub fn mark_used(mut table ast.Table, mut pref_ pref.Preferences, ast_files []&a
 	table.used_features.used_consts = walker.used_consts.move()
 	table.used_features.used_globals = walker.used_globals.move()
 	table.used_features.used_syms = walker.used_syms.move()
+	table.used_features.used_fn_ret_types = walker.used_fn_ret_types.move()
 	table.used_features.used_closures = walker.used_closures
 
 	if trace_skip_unused {
diff --git a/vlib/v/markused/walker.v b/vlib/v/markused/walker.v
index 0c39a1bfe..50160bd31 100644
--- a/vlib/v/markused/walker.v
+++ b/vlib/v/markused/walker.v
@@ -9,24 +9,25 @@ import v.pref
 
 pub struct Walker {
 pub mut:
-	table           &ast.Table        = unsafe { nil }
-	features        &ast.UsedFeatures = unsafe { nil }
-	used_fns        map[string]bool // used_fns['println'] == true
-	trace_enabled   bool
-	used_consts     map[string]bool // used_consts['os.args'] == true
-	used_globals    map[string]bool
-	used_fields     map[string]bool
-	used_structs    map[string]bool
-	used_types      map[ast.Type]bool
-	used_syms       map[int]bool
-	used_arr_method map[string]bool
-	used_map_method map[string]bool
-	used_none       int // _option_none
-	used_option     int // _option_ok
-	used_result     int // _result_ok
-	used_panic      int // option/result propagation
-	used_closures   int // fn [x] (){}, and `instance.method` used in an expression
-	pref            &pref.Preferences = unsafe { nil }
+	table             &ast.Table        = unsafe { nil }
+	features          &ast.UsedFeatures = unsafe { nil }
+	used_fns          map[string]bool // used_fns['println'] == true
+	trace_enabled     bool
+	used_consts       map[string]bool // used_consts['os.args'] == true
+	used_globals      map[string]bool
+	used_fields       map[string]bool
+	used_structs      map[string]bool
+	used_types        map[ast.Type]bool
+	used_syms         map[int]bool
+	used_fn_ret_types map[int]bool
+	used_arr_method   map[string]bool
+	used_map_method   map[string]bool
+	used_none         int // _option_none
+	used_option       int // _option_ok
+	used_result       int // _result_ok
+	used_panic        int // option/result propagation
+	used_closures     int // fn [x] (){}, and `instance.method` used in an expression
+	pref              &pref.Preferences = unsafe { nil }
 mut:
 	all_fns       map[string]ast.FnDecl
 	all_consts    map[string]ast.ConstField
@@ -539,6 +540,7 @@ fn (mut w Walker) expr(node_ ast.Expr) {
 			w.expr(node.expr)
 			w.features.dump = true
 			w.mark_by_type(node.expr_type)
+			w.mark_fn_ret_array_fixed(node.expr_type)
 		}
 		ast.SpawnExpr {
 			if node.is_expr {
@@ -1107,6 +1109,7 @@ pub fn (mut w Walker) call_expr(mut node ast.CallExpr) {
 		receiver_typ = node.receiver_concrete_type
 	}
 	w.mark_by_type(node.return_type)
+	w.mark_fn_ret_array_fixed(node.return_type)
 	mut stmt := w.all_fns[fn_name] or { return }
 	if !stmt.should_be_skipped && stmt.name == node.name {
 		if !node.is_method || receiver_typ == stmt.receiver.typ {
@@ -1135,6 +1138,7 @@ pub fn (mut w Walker) call_expr(mut node ast.CallExpr) {
 							w.used_option++
 						}
 					}
+					w.mark_fn_ret_array_fixed(concrete_type)
 				}
 			}
 		}
@@ -1188,12 +1192,34 @@ pub fn (mut w Walker) mark_fn_ret_and_params(return_type ast.Type, params []ast.
 			w.used_result++
 		}
 		w.mark_by_type(return_type.clear_option_and_result())
+		w.mark_fn_ret_array_fixed(return_type)
 	}
 	for param in params {
 		w.mark_by_type(param.typ)
 	}
 }
 
+fn (mut w Walker) mark_fn_ret_array_fixed(return_type ast.Type) {
+	if return_type.idx() == 0 {
+		return
+	}
+	final_sym := w.table.final_sym(return_type)
+	if final_sym.info is ast.ArrayFixed {
+		fn_ret_idx := w.table.find_or_register_array_fixed(final_sym.info.elem_type, final_sym.info.size,
+			final_sym.info.size_expr, true)
+		w.used_fn_ret_types[fn_ret_idx] = true
+	} else if final_sym.info is ast.MultiReturn {
+		for typ in final_sym.info.types {
+			inner_sym := w.table.final_sym(typ)
+			if inner_sym.info is ast.ArrayFixed {
+				fn_ret_idx := w.table.find_or_register_array_fixed(inner_sym.info.elem_type,
+					inner_sym.info.size, inner_sym.info.size_expr, true)
+				w.used_fn_ret_types[fn_ret_idx] = true
+			}
+		}
+	}
+}
+
 @[inline]
 pub fn (mut w Walker) mark_by_sym_name(name string) {
 	if sym := w.table.find_sym(name) {
@@ -1395,6 +1421,10 @@ fn (mut w Walker) mark_resource_dependencies() {
 		if w.table.dumps.keys().any(ast.Type(u32(it)).has_flag(.option)) {
 			w.fn_by_name('str_intp')
 		}
+		for dump_type, _ in w.table.dumps {
+			typ := ast.idx_to_type(dump_type)
+			w.mark_fn_ret_array_fixed(typ)
+		}
 	}
 	if w.features.auto_str_ptr {
 		w.fn_by_name('isnil')

@spytheman
Copy link
Member

@demizer thank you again.
The change in the PR is less invasive, and good enough for now.

Copy link
Member

@spytheman spytheman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work.

@spytheman spytheman merged commit 06dd19b into vlang:master Jan 26, 2026
90 checks passed
@demizer demizer deleted the fix-unused-fn-fixed-array-ret branch January 26, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cgen: error for unused functions returning fixed arrays of custom structs

3 participants