diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index af887db1bb..2e64009048 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -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 { diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 0cd0ef2c23..99b6f7c6ea 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -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 diff --git a/vlib/v/checker/tests/custom_comptime_define_error.out b/vlib/v/checker/tests/custom_comptime_define_error.out index 156b08e298..b916d1d3dd 100644 --- a/vlib/v/checker/tests/custom_comptime_define_error.out +++ b/vlib/v/checker/tests/custom_comptime_define_error.out @@ -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') diff --git a/vlib/v/checker/tests/unknown_comptime_expr.out b/vlib/v/checker/tests/unknown_comptime_expr.out new file mode 100644 index 0000000000..d8d874ad2d --- /dev/null +++ b/vlib/v/checker/tests/unknown_comptime_expr.out @@ -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 | } diff --git a/vlib/v/checker/tests/unknown_comptime_expr.vv b/vlib/v/checker/tests/unknown_comptime_expr.vv new file mode 100644 index 0000000000..fb92811bca --- /dev/null +++ b/vlib/v/checker/tests/unknown_comptime_expr.vv @@ -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 {} +} diff --git a/vlib/v/gen/cgen.v b/vlib/v/gen/cgen.v index c0f806ed85..3f6dccb243 100644 --- a/vlib/v/gen/cgen.v +++ b/vlib/v/gen/cgen.v @@ -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] diff --git a/vlib/v/gen/comptime.v b/vlib/v/gen/comptime.v index 1856b5b37d..f89293d358 100644 --- a/vlib/v/gen/comptime.v +++ b/vlib/v/gen/comptime.v @@ -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') + } } } diff --git a/vlib/v/tests/comptime_if_expr_test.v b/vlib/v/tests/comptime_if_expr_test.v new file mode 100644 index 0000000000..9961b09a38 --- /dev/null +++ b/vlib/v/tests/comptime_if_expr_test.v @@ -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 + } +}