checker: initial support for evaluating expressions at compile time (#7248)
							parent
							
								
									c4e76e6a59
								
							
						
					
					
						commit
						ca2c082a5e
					
				|  | @ -1122,6 +1122,13 @@ pub fn (expr Expr) is_expr() bool { | |||
| 	return true | ||||
| } | ||||
| 
 | ||||
| pub fn (expr Expr) is_lit() bool { | ||||
| 	return match expr { | ||||
| 		BoolLiteral, StringLiteral, IntegerLiteral { true } | ||||
| 		else { false } | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // check if stmt can be an expression in C
 | ||||
| pub fn (stmt Stmt) check_c_expr() ? { | ||||
| 	match stmt { | ||||
|  |  | |||
|  | @ -4056,6 +4056,9 @@ pub fn (mut c Checker) if_expr(mut node ast.IfExpr) table.Type { | |||
| fn (mut c Checker) comp_if_branch(cond ast.Expr, pos token.Position) bool { | ||||
| 	// TODO: better error messages here
 | ||||
| 	match cond { | ||||
| 		ast.BoolLiteral { | ||||
| 			return !cond.val | ||||
| 		} | ||||
| 		ast.ParExpr { | ||||
| 			return c.comp_if_branch(cond.expr, pos) | ||||
| 		} | ||||
|  | @ -4087,17 +4090,39 @@ fn (mut c Checker) comp_if_branch(cond ast.Expr, pos token.Position) bool { | |||
| 					return l && r // skip (return true) only if both should be skipped
 | ||||
| 				} | ||||
| 				.key_is, .not_is { | ||||
| 					// $if method.@type is string
 | ||||
| 					// TODO better checks here, will be done in comp. for PR
 | ||||
| 					if cond.left !is ast.SelectorExpr || cond.right !is ast.Type { | ||||
| 						c.error('invalid `\$if` condition', cond.pos) | ||||
| 					if cond.left is ast.SelectorExpr && cond.right is ast.Type { | ||||
| 						// $if method.@type is string
 | ||||
| 					} else { | ||||
| 						c.error('invalid `\$if` condition: $cond.left', cond.pos) | ||||
| 					} | ||||
| 				} | ||||
| 				.eq, .ne { | ||||
| 					// $if method.args.len == 1
 | ||||
| 					// TODO better checks here, will be done in comp. for PR
 | ||||
| 					if cond.left !is ast.SelectorExpr || cond.right !is ast.IntegerLiteral { | ||||
| 						c.error('invalid `\$if` condition', cond.pos) | ||||
| 					if cond.left is ast.SelectorExpr && cond.right is ast.IntegerLiteral { | ||||
| 						// $if method.args.len == 1
 | ||||
| 					} else if cond.left is ast.Ident { | ||||
| 						// $if version == 2
 | ||||
| 						left_type := c.expr(cond.left) | ||||
| 						right_type := c.expr(cond.right) | ||||
| 						expr := c.find_definition(cond.left) or { | ||||
| 							c.error(err, cond.left.pos) | ||||
| 							return false | ||||
| 						} | ||||
| 						if !c.check_types(right_type, left_type) { | ||||
| 							left_name := c.table.type_to_str(left_type) | ||||
| 							right_name := c.table.type_to_str(right_type) | ||||
| 							c.error('mismatched types `$left_name` and `$right_name`', | ||||
| 								cond.pos) | ||||
| 						} | ||||
| 						// :)
 | ||||
| 						// until `v.eval` is stable, I can't think of a better way to do this
 | ||||
| 						different := expr.str() != cond.right.str() | ||||
| 						return if cond.op == .eq { | ||||
| 							different | ||||
| 						} else { | ||||
| 							!different | ||||
| 						} | ||||
| 					} else { | ||||
| 						c.error('invalid `\$if` condition: ${typeof(cond.left)}', cond.pos) | ||||
| 					} | ||||
| 				} | ||||
| 				else { | ||||
|  | @ -4123,10 +4148,25 @@ fn (mut c Checker) comp_if_branch(cond ast.Expr, pos token.Position) bool { | |||
| 					'no_bounds_checking' { return cond.name !in c.pref.compile_defines_all } | ||||
| 					else { return false } | ||||
| 				} | ||||
| 			} else { | ||||
| 				if cond.name !in c.pref.compile_defines_all { | ||||
| 					c.error('unknown \$if value', pos) | ||||
| 			} else if cond.name !in c.pref.compile_defines_all { | ||||
| 				// `$if some_var {}`
 | ||||
| 				typ := c.expr(cond) | ||||
| 				scope := c.file.scope.innermost(pos.pos) | ||||
| 				obj := scope.find(cond.name) or { | ||||
| 					c.error('unknown var: `$cond.name`', pos) | ||||
| 					return false | ||||
| 				} | ||||
| 				expr := c.find_obj_definition(obj) or { | ||||
| 					c.error(err, cond.pos) | ||||
| 					return false | ||||
| 				} | ||||
| 				if !c.check_types(typ, table.bool_type) { | ||||
| 					type_name := c.table.type_to_str(typ) | ||||
| 					c.error('non-bool type `$type_name` used as \$if condition', cond.pos) | ||||
| 				} | ||||
| 				// :)
 | ||||
| 				// until `v.eval` is stable, I can't think of a better way to do this
 | ||||
| 				return !(expr as ast.BoolLiteral).val | ||||
| 			} | ||||
| 		} | ||||
| 		else { | ||||
|  | @ -4136,6 +4176,41 @@ fn (mut c Checker) comp_if_branch(cond ast.Expr, pos token.Position) bool { | |||
| 	return false | ||||
| } | ||||
| 
 | ||||
| fn (mut c Checker) find_definition(ident ast.Ident) ?ast.Expr { | ||||
| 	match ident.kind { | ||||
| 		.unresolved, .blank_ident { return none } | ||||
| 		.variable, .constant { return c.find_obj_definition(ident.obj) } | ||||
| 		.global { return error('$ident.name is a global variable') } | ||||
| 		.function { return error('$ident.name is a function') } | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| fn (mut c Checker) find_obj_definition(obj ast.ScopeObject) ?ast.Expr { | ||||
| 	// TODO: remove once we have better type inference
 | ||||
| 	mut name := '' | ||||
| 	match obj { | ||||
| 		ast.Var, ast.ConstField, ast.GlobalField { name = obj.name } | ||||
| 	} | ||||
| 	mut expr := ast.Expr{} | ||||
| 	if obj is ast.Var { | ||||
| 		if obj.is_mut { | ||||
| 			return error('`$name` is mut and may have changed since its definition') | ||||
| 		} | ||||
| 		expr = obj.expr | ||||
| 	} else if obj is ast.ConstField { | ||||
| 		expr = obj.expr | ||||
| 	} else { | ||||
| 		return error('`$name` is a global variable and is unknown at compile time') | ||||
| 	} | ||||
| 	if expr is ast.Ident { | ||||
| 		return c.find_definition(expr as ast.Ident) // TODO: smartcast
 | ||||
| 	} | ||||
| 	if !expr.is_lit() { | ||||
| 		return error('definition of `$name` is unknown at compile time') | ||||
| 	} | ||||
| 	return expr | ||||
| } | ||||
| 
 | ||||
| fn (c &Checker) has_return(stmts []ast.Stmt) ?bool { | ||||
| 	// complexity means either more match or ifs
 | ||||
| 	mut has_complexity := false | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| vlib/v/checker/tests/custom_comptime_define_error.vv:6:9: error: unknown $if value | ||||
| vlib/v/checker/tests/custom_comptime_define_error.vv:6:13: error: undefined ident: `mysymbol` | ||||
|     4 |             println('optional compitme define works') | ||||
|     5 |         } | ||||
|     6 |         $if mysymbol { | ||||
|       |         ~~~~~~~~~~~~ | ||||
|       |             ~~~~~~~~ | ||||
|     7 |             // this will produce a checker error when `-d mysymbol` is not given on the CLI | ||||
|     8 |             println('non optional comptime define works') | ||||
|  |  | |||
|  | @ -0,0 +1,13 @@ | |||
| vlib/v/checker/tests/unknown_comptime_expr.vv:5:6: error: `foo` is mut and may have changed since its definition | ||||
|     3 | fn main() { | ||||
|     4 |     mut foo := 0 | ||||
|     5 |     $if foo == 0 {} | ||||
|       |         ~~~ | ||||
|     6 |  | ||||
|     7 |     bar := unknown_at_ct() | ||||
| vlib/v/checker/tests/unknown_comptime_expr.vv:8:6: error: definition of `bar` is unknown at compile time | ||||
|     6 |  | ||||
|     7 |     bar := unknown_at_ct() | ||||
|     8 |     $if bar == 0 {} | ||||
|       |         ~~~ | ||||
|     9 | } | ||||
|  | @ -0,0 +1,9 @@ | |||
| fn unknown_at_ct() int { return 0 } | ||||
| 
 | ||||
| fn main() { | ||||
| 	mut foo := 0 | ||||
| 	$if foo == 0 {} | ||||
| 
 | ||||
| 	bar := unknown_at_ct() | ||||
| 	$if bar == 0 {} | ||||
| } | ||||
|  | @ -5163,7 +5163,7 @@ fn op_to_fn_name(name string) string { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| fn (mut g Gen) comp_if_to_ifdef(name string, is_comptime_optional bool) string { | ||||
| fn (mut g Gen) comp_if_to_ifdef(name string, is_comptime_optional bool) ?string { | ||||
| 	match name { | ||||
| 		// platforms/os-es:
 | ||||
| 		'windows' { | ||||
|  | @ -5285,11 +5285,10 @@ fn (mut g Gen) comp_if_to_ifdef(name string, is_comptime_optional bool) string { | |||
| 				(g.pref.compile_defines_all.len > 0 && name in g.pref.compile_defines_all) { | ||||
| 				return 'CUSTOM_DEFINE_$name' | ||||
| 			} | ||||
| 			verror('bad os ifdef name "$name"') // should never happen, caught in the checker
 | ||||
| 			return error('bad os ifdef name "$name"') // should never happen, caught in the checker
 | ||||
| 		} | ||||
| 	} | ||||
| 	// verror('bad os ifdef name "$name"')
 | ||||
| 	return '' | ||||
| 	return none | ||||
| } | ||||
| 
 | ||||
| [inline] | ||||
|  |  | |||
|  | @ -184,6 +184,9 @@ fn (mut g Gen) comp_if(node ast.IfExpr) { | |||
| 
 | ||||
| fn (mut g Gen) comp_if_expr(cond ast.Expr) { | ||||
| 	match cond { | ||||
| 		ast.BoolLiteral { | ||||
| 			g.expr(cond) | ||||
| 		} | ||||
| 		ast.ParExpr { | ||||
| 			g.write('(') | ||||
| 			g.comp_if_expr(cond.expr) | ||||
|  | @ -194,7 +197,10 @@ fn (mut g Gen) comp_if_expr(cond ast.Expr) { | |||
| 			g.comp_if_expr(cond.right) | ||||
| 		} | ||||
| 		ast.PostfixExpr { | ||||
| 			ifdef := g.comp_if_to_ifdef((cond.expr as ast.Ident).name, true) | ||||
| 			ifdef := g.comp_if_to_ifdef((cond.expr as ast.Ident).name, true) or { | ||||
| 				verror(err) | ||||
| 				return | ||||
| 			} | ||||
| 			g.write('defined($ifdef)') | ||||
| 		} | ||||
| 		ast.InfixExpr { | ||||
|  | @ -213,15 +219,19 @@ fn (mut g Gen) comp_if_expr(cond ast.Expr) { | |||
| 				} | ||||
| 				.eq, .ne { | ||||
| 					// TODO Implement `$if method.args.len == 1`
 | ||||
| 					g.write('1') | ||||
| 				} | ||||
| 				else {} | ||||
| 			} | ||||
| 		} | ||||
| 		ast.Ident { | ||||
| 			ifdef := g.comp_if_to_ifdef(cond.name, false) | ||||
| 			ifdef := g.comp_if_to_ifdef(cond.name, false) or { 'true' } // handled in checker
 | ||||
| 			g.write('defined($ifdef)') | ||||
| 		} | ||||
| 		else {} | ||||
| 		else { | ||||
| 			// should be unreachable, but just in case
 | ||||
| 			g.write('1') | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,30 @@ | |||
| const ( | ||||
| 	version              = 123 | ||||
| 	disable_opt_features = true | ||||
| ) | ||||
| 
 | ||||
| // NB: the `unknown_fn()` calls are here on purpose, to make sure that anything
 | ||||
| // that doesn't match a compile-time condition is not even parsed.
 | ||||
| fn test_ct_expressions() { | ||||
| 	foo := version | ||||
| 	bar := foo | ||||
| 	$if bar == 123 { | ||||
| 		assert true | ||||
| 	} $else { | ||||
| 		unknown_fn() | ||||
| 	} | ||||
| 
 | ||||
| 	$if bar != 123 { | ||||
| 		unknown_fn() | ||||
| 	} $else $if bar != 124 { | ||||
| 		assert true | ||||
| 	} $else { | ||||
| 		unknown_fn() | ||||
| 	} | ||||
| 
 | ||||
| 	$if !disable_opt_features { | ||||
| 		unknown_fn() | ||||
| 	} $else { | ||||
| 		assert true | ||||
| 	} | ||||
| } | ||||
		Loading…
	
		Reference in New Issue