Repository: vlang/v
Pull request #23848: checker: add checking for comptime assign without comptime if checking
Author: login: felipensp
Created: 2025-03-03T10:57:40Z | Merged: 2025-03-05T19:28:14Z

Fix #23796

Linked issue #23796: Generic -- error: cannot convert 'int' to 'struct string'
![Image](https://github.com/user-attachments/assets/973ba77c-7681-4c59-9ad1-68d4d7f8e12d)

playground

https://play.vlang.io/p/b4d18365e5

> [!NOTE]
> You can use the 👍 reaction to increase the issue's priority for developers.
>
> Please note that only the 👍 reaction to the issue itself counts as a vote.
> Other reactions and those to comments will not be taken into account.
Linked issue comments: user: login: huly-for-github[bot]
created_at: 2025-02-23T15:55:14Z
body: <p>Connected to <b><a href="https://huly.app/guest/vlang-66f40c4d-a476b54c67-771fdd?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsaW5rSWQiOiI2N2JiNDRlMjEwYmZkZmE2YTg5MGRhYzQiLCJndWVzdCI6InRydWUiLCJlbWFpbCI6IiNndWVzdEBoYy5lbmdpbmVlcmluZyIsIndvcmtzcGFjZSI6InctYWxleGFuZGVyLXZsYW5nLTY2ZjQwYzRkLWE0NzZiNTRjNjctNzcxZmRjIn0.i0qzs-aQyQFA943ZCEV-lmMhu_9ZMo1KiMzEWnDPdKs">Huly&reg;: V_0.6-22206</a></b></p>
author_association: NONE
user: login: felipensp
created_at: 2025-02-23T19:21:40Z
body: The checking must be `$if field.typ is int {` instead.
author_association: MEMBER
user: login: felipensp
created_at: 2025-02-25T12:02:46Z
body: It is not exactly an error, it is as the comptime stuff works. To avoid such error you have to prevent the assignment being generated to the wrong type, once it is traversing all fields of the struct with different types.

Maybe should we add some warning? notice? What do you think @medvednikov  @spytheman 
author_association: MEMBER
user: login: JalonSolov
created_at: 2025-02-25T13:13:16Z
body: It is an error in that you don't see any problems until the C compiler throws an error.  V should always catch things that that (if it can), before it ever gets to the C compiler.

Think of it this way... if V can't catch it, what is the native backend supposed to do?  Generate code that fails at runtime?
author_association: COLLABORATOR
user: login: Jengro777
created_at: 2025-02-25T13:43:00Z
body: > It is an error in that you don't see any problems until the C compiler throws an error.  V should always catch things that that (if it can), before it ever gets to the C compiler.
> 
> Think of it this way... if V can't catch it, what is the native backend supposed to do?  Generate code that fails at runtime?

It would be nice to optimize the prompt copy.
author_association: CONTRIBUTOR

Diff:
diff --git a/vlib/v/checker/assign.v b/vlib/v/checker/assign.v
index 89c1c165f9d535..69fb39880e84e9 100644
--- a/vlib/v/checker/assign.v
+++ b/vlib/v/checker/assign.v
@@ -187,6 +187,11 @@ fn (mut c Checker) assign_stmt(mut node ast.AssignStmt) {
 				&& c.table.sym(left_type).kind in [.array, .map, .struct]
 		}
 		if c.comptime.comptime_for_field_var != '' && mut left is ast.ComptimeSelector {
+			if c.comptime.has_different_types && node.right[i].is_literal()
+				&& !c.comptime.inside_comptime_if {
+				c.error('mismatched types: check field type with \$if to avoid this problem',
+					node.right[i].pos())
+			}
 			left_type = c.comptime.comptime_for_field_type
 			c.expected_type = c.unwrap_generic(left_type)
 		}
diff --git a/vlib/v/checker/comptime.v b/vlib/v/checker/comptime.v
index e25440191ac09d..dac0ba8d3fe906 100644
--- a/vlib/v/checker/comptime.v
+++ b/vlib/v/checker/comptime.v
@@ -271,6 +271,8 @@ fn (mut c Checker) comptime_for(mut node ast.ComptimeFor) {
 					return
 				}
 			}
+			has_different_types := fields.len > 1
+				&& !fields.all(c.check_basic(it.typ, fields[0].typ))
 			for field in fields {
 				c.push_new_comptime_info()
 				c.comptime.inside_comptime_for = true
@@ -283,6 +285,7 @@ fn (mut c Checker) comptime_for(mut node ast.ComptimeFor) {
 				c.type_resolver.update_ct_type(node.val_var, c.field_data_type)
 				c.type_resolver.update_ct_type('${node.val_var}.typ', node.typ)
 				c.comptime.comptime_for_field_type = field.typ
+				c.comptime.has_different_types = has_different_types
 				c.stmts(mut node.stmts)
 
 				unwrapped_expr_type := c.unwrap_generic(field.typ)
@@ -809,6 +812,7 @@ fn (mut c Checker) comptime_if_cond(mut cond ast.Expr, pos token.Pos) ComptimeBr
 					} else if cond.left in [ast.Ident, ast.SelectorExpr, ast.TypeNode] {
 						// `$if method.type is string`
 						c.expr(mut cond.left)
+						c.comptime.inside_comptime_if = true
 						if mut cond.left is ast.SelectorExpr && cond.right is ast.ComptimeType {
 							comptime_type := cond.right as ast.ComptimeType
 							if c.comptime.is_comptime_selector_type(cond.left) {
@@ -1075,6 +1079,8 @@ fn (mut c Checker) push_new_comptime_info() {
 	c.type_resolver.info_stack << type_resolver.ResolverInfo{
 		saved_type_map:               c.type_resolver.type_map.clone()
 		inside_comptime_for:          c.comptime.inside_comptime_for
+		inside_comptime_if:           c.comptime.inside_comptime_if
+		has_different_types:          c.comptime.has_different_types
 		comptime_for_variant_var:     c.comptime.comptime_for_variant_var
 		comptime_for_field_var:       c.comptime.comptime_for_field_var
 		comptime_for_field_type:      c.comptime.comptime_for_field_type
@@ -1091,6 +1097,8 @@ fn (mut c Checker) pop_comptime_info() {
 	old := c.type_resolver.info_stack.pop()
 	c.type_resolver.type_map = old.saved_type_map.clone()
 	c.comptime.inside_comptime_for = old.inside_comptime_for
+	c.comptime.inside_comptime_if = old.inside_comptime_if
+	c.comptime.has_different_types = old.has_different_types
 	c.comptime.comptime_for_variant_var = old.comptime_for_variant_var
 	c.comptime.comptime_for_field_var = old.comptime_for_field_var
 	c.comptime.comptime_for_field_type = old.comptime_for_field_type
diff --git a/vlib/v/checker/if.v b/vlib/v/checker/if.v
index eecb72628fa873..543cfb7f4f8294 100644
--- a/vlib/v/checker/if.v
+++ b/vlib/v/checker/if.v
@@ -53,6 +53,10 @@ fn (mut c Checker) if_expr(mut node ast.IfExpr) ast.Type {
 	mut skip_state := ComptimeBranchSkipState.unknown
 	mut found_branch := false // Whether a matching branch was found- skip the rest
 	mut is_comptime_type_is_expr := false // if `$if T is string`
+	last_in_comptime_if := c.comptime.inside_comptime_if
+	defer {
+		c.comptime.inside_comptime_if = last_in_comptime_if
+	}
 	for i in 0 .. node.branches.len {
 		mut branch := node.branches[i]
 		if branch.cond is ast.ParExpr && !c.pref.translated && !c.file.is_translated {
diff --git a/vlib/v/checker/tests/comptime_selector_assign.out b/vlib/v/checker/tests/comptime_selector_assign.out
new file mode 100644
index 00000000000000..a17b7f28ca5658
--- /dev/null
+++ b/vlib/v/checker/tests/comptime_selector_assign.out
@@ -0,0 +1,7 @@
+vlib/v/checker/tests/comptime_selector_assign.vv:18:24: error: mismatched types: check field type with $if to avoid this problem
+   16 |                 typ.$(field.name) = 2
+   17 |             }
+   18 |             typ.$(field.name) = 3
+      |                                 ^
+   19 |         }
+   20 |     }
diff --git a/vlib/v/checker/tests/comptime_selector_assign.vv b/vlib/v/checker/tests/comptime_selector_assign.vv
new file mode 100644
index 00000000000000..d3e759a6539d9b
--- /dev/null
+++ b/vlib/v/checker/tests/comptime_selector_assign.vv
@@ -0,0 +1,31 @@
+import x.json2 { Any, raw_decode }
+
+struct Income {
+mut:
+	email string
+	code  int
+}
+
+pub fn structuring[T](res Any) T {
+	mut typ := T{}
+	res_map := res.as_map()
+
+	$for field in T.fields {
+		if field.name in res_map {
+			$if field.typ is int {
+				typ.$(field.name) = 2
+			}
+			typ.$(field.name) = 3
+		}
+	}
+	return typ
+}
+
+fn main() {
+	res := raw_decode('{
+        "email": ["sdvsdv", "sds"],
+        "code": 12
+    }')!.as_map()
+
+	structuring[Income](res)
+}
diff --git a/vlib/v/type_resolver/type_resolver.v b/vlib/v/type_resolver/type_resolver.v
index 01530ee95fcd9d..05a8f9b49c390f 100644
--- a/vlib/v/type_resolver/type_resolver.v
+++ b/vlib/v/type_resolver/type_resolver.v
@@ -15,6 +15,9 @@ pub mut:
 	comptime_loop_id int
 	// $for
 	inside_comptime_for bool
+	// $if
+	inside_comptime_if  bool
+	has_different_types bool
 	// .variants
 	comptime_for_variant_var string
 	// .fields


Reviews:
user: login: spytheman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T19:57:03Z
user: login: spytheman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T12:26:55Z
user: login: spytheman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T12:27:43Z
user: login: felipensp
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T12:36:34Z
user: login: JalonSolov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T12:40:22Z
user: login: spytheman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T12:50:03Z
user: login: spytheman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T12:50:57Z
user: login: spytheman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T12:51:59Z
user: login: felipensp
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T12:54:57Z
user: login: felipensp
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T13:02:45Z
user: login: felipensp
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T13:03:13Z

Review comments:
diff_hunk: @@ -187,6 +187,11 @@ fn (mut c Checker) assign_stmt(mut node ast.AssignStmt) {
 				&& c.table.sym(left_type).kind in [.array, .map, .struct]
 		}
 		if c.comptime.comptime_for_field_var != '' && mut left is ast.ComptimeSelector {
+			if c.comptime.has_different_types && node.right[i].is_literal()
+				&& !c.comptime.inside_comptime_if {
+				c.error('you should check the field type with \$if to avoid generating wrong assigning code',
+					node.right[i].pos())
path: vlib/v/checker/assign.v
user: login: spytheman
body: @JalonSolov what do you think about the error message?
created_at: 2025-03-03T19:57:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -283,6 +285,7 @@ fn (mut c Checker) comptime_for(mut node ast.ComptimeFor) {
 				c.type_resolver.update_ct_type(node.val_var, c.field_data_type)
 				c.type_resolver.update_ct_type('${node.val_var}.typ', node.typ)
 				c.comptime.comptime_for_field_type = field.typ
+				c.comptime.has_different_types = has_different_types
path: vlib/v/checker/comptime.v
user: login: spytheman
body: where/what restores the previous value of that new field?
created_at: 2025-03-05T12:26:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -283,6 +285,7 @@ fn (mut c Checker) comptime_for(mut node ast.ComptimeFor) {
 				c.type_resolver.update_ct_type(node.val_var, c.field_data_type)
 				c.type_resolver.update_ct_type('${node.val_var}.typ', node.typ)
 				c.comptime.comptime_for_field_type = field.typ
+				c.comptime.has_different_types = has_different_types
path: vlib/v/checker/comptime.v
user: login: spytheman
body: there is a defer block for the other one, should not there be for this one as well?
created_at: 2025-03-05T12:27:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -283,6 +285,7 @@ fn (mut c Checker) comptime_for(mut node ast.ComptimeFor) {
 				c.type_resolver.update_ct_type(node.val_var, c.field_data_type)
 				c.type_resolver.update_ct_type('${node.val_var}.typ', node.typ)
 				c.comptime.comptime_for_field_type = field.typ
+				c.comptime.has_different_types = has_different_types
path: vlib/v/checker/comptime.v
user: login: felipensp
body: > where/what restores the previous value of that new field?

It is stored on the stack when pushing the comptime state and restored when poping it.
created_at: 2025-03-05T12:36:34Z
author_association: MEMBER
diff_hunk: @@ -187,6 +187,11 @@ fn (mut c Checker) assign_stmt(mut node ast.AssignStmt) {
 				&& c.table.sym(left_type).kind in [.array, .map, .struct]
 		}
 		if c.comptime.comptime_for_field_var != '' && mut left is ast.ComptimeSelector {
+			if c.comptime.has_different_types && node.right[i].is_literal()
+				&& !c.comptime.inside_comptime_if {
+				c.error('you should check the field type with \$if to avoid generating wrong assigning code',
path: vlib/v/checker/assign.v
user: login: JalonSolov
body: ```suggestion
				c.error('mismatched types: check field type with \$if to avoid this problem',
```
created_at: 2025-03-05T12:40:21Z
author_association: COLLABORATOR
diff_hunk: @@ -283,6 +285,7 @@ fn (mut c Checker) comptime_for(mut node ast.ComptimeFor) {
 				c.type_resolver.update_ct_type(node.val_var, c.field_data_type)
 				c.type_resolver.update_ct_type('${node.val_var}.typ', node.typ)
 				c.comptime.comptime_for_field_type = field.typ
+				c.comptime.has_different_types = has_different_types
path: vlib/v/checker/comptime.v
user: login: spytheman
body: why is not the same mechanism used for the other bool field?
created_at: 2025-03-05T12:50:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -283,6 +285,7 @@ fn (mut c Checker) comptime_for(mut node ast.ComptimeFor) {
 				c.type_resolver.update_ct_type(node.val_var, c.field_data_type)
 				c.type_resolver.update_ct_type('${node.val_var}.typ', node.typ)
 				c.comptime.comptime_for_field_type = field.typ
+				c.comptime.has_different_types = has_different_types
path: vlib/v/checker/comptime.v
user: login: spytheman
body: I am concerned mostly about setting the field for a given scope, and then the new state, affecting unrelated scopes, by being sticky.
created_at: 2025-03-05T12:50:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -283,6 +285,7 @@ fn (mut c Checker) comptime_for(mut node ast.ComptimeFor) {
 				c.type_resolver.update_ct_type(node.val_var, c.field_data_type)
 				c.type_resolver.update_ct_type('${node.val_var}.typ', node.typ)
 				c.comptime.comptime_for_field_type = field.typ
+				c.comptime.has_different_types = has_different_types
path: vlib/v/checker/comptime.v
user: login: spytheman
body: if you are sure, that it is not a problem, fine - I have not analyzed in details the situations where it could be, just noticed a pattern, that in the past led to bugs.
created_at: 2025-03-05T12:51:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -283,6 +285,7 @@ fn (mut c Checker) comptime_for(mut node ast.ComptimeFor) {
 				c.type_resolver.update_ct_type(node.val_var, c.field_data_type)
 				c.type_resolver.update_ct_type('${node.val_var}.typ', node.typ)
 				c.comptime.comptime_for_field_type = field.typ
+				c.comptime.has_different_types = has_different_types
path: vlib/v/checker/comptime.v
user: login: felipensp
body: > why is not the same mechanism used for the other bool field?

Oh, just noticed I forgot all the reset state stuff. Thanks
created_at: 2025-03-05T12:54:57Z
author_association: MEMBER
diff_hunk: @@ -283,6 +285,7 @@ fn (mut c Checker) comptime_for(mut node ast.ComptimeFor) {
 				c.type_resolver.update_ct_type(node.val_var, c.field_data_type)
 				c.type_resolver.update_ct_type('${node.val_var}.typ', node.typ)
 				c.comptime.comptime_for_field_type = field.typ
+				c.comptime.has_different_types = has_different_types
path: vlib/v/checker/comptime.v
user: login: felipensp
body: The `c.comptime.has_different_types` is kept for whole current comptime state lifecycle, but the `c.comptime.inside_comptime_if` turns on and off on each `$if ... is TypeNode` inside `$for `. Because this it has the `defer` and other does not.
created_at: 2025-03-05T13:02:45Z
author_association: MEMBER
diff_hunk: @@ -187,6 +187,11 @@ fn (mut c Checker) assign_stmt(mut node ast.AssignStmt) {
 				&& c.table.sym(left_type).kind in [.array, .map, .struct]
 		}
 		if c.comptime.comptime_for_field_var != '' && mut left is ast.ComptimeSelector {
+			if c.comptime.has_different_types && node.right[i].is_literal()
+				&& !c.comptime.inside_comptime_if {
+				c.error('you should check the field type with \$if to avoid generating wrong assigning code',
path: vlib/v/checker/assign.v
user: login: felipensp
body: Thanks
created_at: 2025-03-05T13:03:13Z
author_association: MEMBER

PR comments:
user: login: huly-for-github[bot]
created_at: 2025-03-03T10:57:50Z
body: <p>Connected to <b><a href="https://huly.app/guest/vlang-66f40c4d-a476b54c67-771fdd?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsaW5rSWQiOiI2N2M1OGIyZGU1NDcyMzg5NTE4MmU3NTIiLCJndWVzdCI6InRydWUiLCJlbWFpbCI6IiNndWVzdEBoYy5lbmdpbmVlcmluZyIsIndvcmtzcGFjZSI6InctYWxleGFuZGVyLXZsYW5nLTY2ZjQwYzRkLWE0NzZiNTRjNjctNzcxZmRjIn0.gjwfhtKL5TWzL1IN0AxvqvUWUlID9vTR_oyG79drpw8">Huly&reg;: V_0.6-22253</a></b></p>
author_association: NONE
user: login: spytheman
created_at: 2025-03-05T12:24:25Z
body: Well, I guess we can change the message later if needed.
author_association: CONTRIBUTOR
user: login: spytheman
created_at: 2025-03-05T13:00:38Z
body: The `report-missing-fn-doc` CI job failure is unrelated (it is due to the other PRs merged into master).
author_association: CONTRIBUTOR