From dbfb9c3a90a4c268d67c0de7aca09e5ddf3cf644 Mon Sep 17 00:00:00 2001 From: Felipe Pena Date: Sat, 14 Jan 2023 11:20:12 -0300 Subject: [PATCH] cgen, checker: var type checking at compile-time (#16951) --- vlib/v/checker/comptime.v | 2 +- vlib/v/checker/if.v | 11 ++ .../v/checker/tests/unknown_comptime_expr.out | 67 ++++----- vlib/v/checker/tests/unknown_comptime_expr.vv | 23 ++-- vlib/v/gen/c/comptime.v | 130 +++++++++--------- vlib/v/tests/var_type_is_checking2_test.v | 60 ++++++++ vlib/v/tests/var_type_is_checking_test.v | 46 +++++++ 7 files changed, 226 insertions(+), 113 deletions(-) create mode 100644 vlib/v/tests/var_type_is_checking2_test.v create mode 100644 vlib/v/tests/var_type_is_checking_test.v diff --git a/vlib/v/checker/comptime.v b/vlib/v/checker/comptime.v index c43073a3c9..6f005153e5 100644 --- a/vlib/v/checker/comptime.v +++ b/vlib/v/checker/comptime.v @@ -551,7 +551,7 @@ fn (mut c Checker) comptime_if_branch(cond ast.Expr, pos token.Pos) ComptimeBran } else { .skip } - } else if cond.left in [ast.SelectorExpr, ast.TypeNode] { + } else if cond.left in [ast.Ident, ast.SelectorExpr, ast.TypeNode] { // `$if method.@type is string` c.expr(cond.left) return .unknown diff --git a/vlib/v/checker/if.v b/vlib/v/checker/if.v index bb76022b07..f196e92cba 100644 --- a/vlib/v/checker/if.v +++ b/vlib/v/checker/if.v @@ -94,6 +94,17 @@ fn (mut c Checker) if_expr(mut node ast.IfExpr) ast.Type { } else { .skip } + } else if right is ast.ComptimeType && left is ast.Ident + && (left as ast.Ident).info is ast.IdentVar { + is_comptime_type_is_expr = true + if var := left.scope.find_var(left.name) { + checked_type := c.unwrap_generic(var.typ) + skip_state = if c.table.is_comptime_type(checked_type, right as ast.ComptimeType) { + .eval + } else { + .skip + } + } } else { got_type := c.unwrap_generic((right as ast.TypeNode).typ) sym := c.table.sym(got_type) diff --git a/vlib/v/checker/tests/unknown_comptime_expr.out b/vlib/v/checker/tests/unknown_comptime_expr.out index b97e13d8b1..f7eb905b4f 100644 --- a/vlib/v/checker/tests/unknown_comptime_expr.out +++ b/vlib/v/checker/tests/unknown_comptime_expr.out @@ -1,42 +1,35 @@ -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 {} +vlib/v/checker/tests/unknown_comptime_expr.vv:7:6: error: `foo` is mut and may have changed since its definition + 5 | fn main() { + 6 | mut foo := 0 + 7 | $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 {} + 8 | } + 9 | +vlib/v/checker/tests/unknown_comptime_expr.vv:11:6: error: definition of `bar` is unknown at compile time + 9 | + 10 | bar := unknown_at_ct() + 11 | $if bar == 0 { | ~~~ - 9 | } - 10 | -vlib/v/checker/tests/unknown_comptime_expr.vv:13:6: error: undefined ident: `huh` - 11 | fn if_is() { - 12 | s := S1{} - 13 | $if huh.typ is T {} + 12 | } + 13 | } +vlib/v/checker/tests/unknown_comptime_expr.vv:17:6: error: undefined ident: `huh` + 15 | fn if_is() { + 16 | s := S1{} + 17 | $if huh.typ is T { | ~~~ - 14 | $if s is int {} - 15 | $if s.i is 5 {} -vlib/v/checker/tests/unknown_comptime_expr.vv:14:6: error: invalid `$if` condition: expected a type or a selector expression or an interface check - 12 | s := S1{} - 13 | $if huh.typ is T {} - 14 | $if s is int {} - | ^ - 15 | $if s.i is 5 {} - 16 | $if s.i is T {} -vlib/v/checker/tests/unknown_comptime_expr.vv:15:13: error: invalid `$if` condition: expected a type - 13 | $if huh.typ is T {} - 14 | $if s is int {} - 15 | $if s.i is 5 {} + 18 | } + 19 | $if s is int { +vlib/v/checker/tests/unknown_comptime_expr.vv:21:13: error: invalid `$if` condition: expected a type + 19 | $if s is int { + 20 | } + 21 | $if s.i is 5 { | ^ - 16 | $if s.i is T {} - 17 | } -vlib/v/checker/tests/unknown_comptime_expr.vv:16:13: error: unknown type `T` - 14 | $if s is int {} - 15 | $if s.i is 5 {} - 16 | $if s.i is T {} + 22 | } + 23 | $if s.i is T { +vlib/v/checker/tests/unknown_comptime_expr.vv:23:13: error: unknown type `T` + 21 | $if s.i is 5 { + 22 | } + 23 | $if s.i is T { | ^ - 17 | } - 18 | + 24 | } + 25 | } diff --git a/vlib/v/checker/tests/unknown_comptime_expr.vv b/vlib/v/checker/tests/unknown_comptime_expr.vv index c3734aeb8b..336adf4f97 100644 --- a/vlib/v/checker/tests/unknown_comptime_expr.vv +++ b/vlib/v/checker/tests/unknown_comptime_expr.vv @@ -1,22 +1,29 @@ -fn unknown_at_ct() int { return 0 } +fn unknown_at_ct() int { + return 0 +} fn main() { mut foo := 0 - $if foo == 0 {} + $if foo == 0 { + } bar := unknown_at_ct() - $if bar == 0 {} + $if bar == 0 { + } } fn if_is() { s := S1{} - $if huh.typ is T {} - $if s is int {} - $if s.i is 5 {} - $if s.i is T {} + $if huh.typ is T { + } + $if s is int { + } + $if s.i is 5 { + } + $if s.i is T { + } } struct S1 { i int } - diff --git a/vlib/v/gen/c/comptime.v b/vlib/v/gen/c/comptime.v index 9a51190d05..89be81f220 100644 --- a/vlib/v/gen/c/comptime.v +++ b/vlib/v/gen/c/comptime.v @@ -378,6 +378,32 @@ fn (mut g Gen) comptime_if(node ast.IfExpr) { } } +fn (mut g Gen) get_expr_type(cond ast.Expr) ast.Type { + match cond { + ast.Ident { + return g.unwrap_generic(cond.obj.typ) + } + ast.TypeNode { + return g.unwrap_generic(cond.typ) + } + ast.SelectorExpr { + if cond.gkind_field == .typ { + return g.unwrap_generic(cond.name_type) + } else { + name := '${cond.expr}.${cond.field_name}' + if name in g.comptime_var_type_map { + return g.comptime_var_type_map[name] + } else { + return g.unwrap_generic(cond.typ) + } + } + } + else { + return ast.void_type + } + } +} + // returns the value of the bool comptime expression and if next branches may be discarded // returning `false` means the statements inside the $if can be skipped fn (mut g Gen) comptime_if_cond(cond ast.Expr, pkg_exist bool) (bool, bool) { @@ -420,42 +446,11 @@ fn (mut g Gen) comptime_if_cond(cond ast.Expr, pkg_exist bool) (bool, bool) { } .key_is, .not_is { left := cond.left - mut name := '' - if left is ast.TypeNode && cond.right is ast.ComptimeType { - checked_type := g.unwrap_generic(left.typ) - is_true := g.table.is_comptime_type(checked_type, cond.right) - if cond.op == .key_is { - if is_true { - g.write('1') - } else { - g.write('0') - } - return is_true, true - } else { - if is_true { - g.write('0') - } else { - g.write('1') - } - return !is_true, true - } - } - mut exp_type := ast.Type(0) - got_type := (cond.right as ast.TypeNode).typ - // Handle `$if x is Interface {` - // mut matches_interface := 'false' - if left is ast.TypeNode && cond.right is ast.TypeNode - && g.table.sym(got_type).kind == .interface_ { - // `$if Foo is Interface {` - interface_sym := g.table.sym(got_type) - if interface_sym.info is ast.Interface { - // q := g.table.sym(interface_sym.info.types[0]) - checked_type := g.unwrap_generic(left.typ) - // TODO PERF this check is run twice (also in the checker) - // store the result in a field - is_true := g.table.does_type_implement_interface(checked_type, - got_type) - // true // exp_type in interface_sym.info.types + if left in [ast.TypeNode, ast.Ident, ast.SelectorExpr] + && cond.right in [ast.ComptimeType, ast.TypeNode] { + exp_type := g.get_expr_type(left) + if cond.right is ast.ComptimeType { + is_true := g.table.is_comptime_type(exp_type, cond.right) if cond.op == .key_is { if is_true { g.write('1') @@ -463,7 +458,7 @@ fn (mut g Gen) comptime_if_cond(cond ast.Expr, pkg_exist bool) (bool, bool) { g.write('0') } return is_true, true - } else if cond.op == .not_is { + } else { if is_true { g.write('0') } else { @@ -471,27 +466,37 @@ fn (mut g Gen) comptime_if_cond(cond ast.Expr, pkg_exist bool) (bool, bool) { } return !is_true, true } - // matches_interface = '/*iface:$got_type $exp_type*/ true' - //} - } - } else if left is ast.SelectorExpr { - if left.gkind_field == .typ { - exp_type = g.unwrap_generic(left.name_type) } else { - name = '${left.expr}.${left.field_name}' - exp_type = g.comptime_var_type_map[name] - } - } else if left is ast.TypeNode { - // this is only allowed for generics currently, otherwise blocked by checker - exp_type = g.unwrap_generic(left.typ) - } + got_type := g.unwrap_generic((cond.right as ast.TypeNode).typ) + got_sym := g.table.sym(got_type) - if cond.op == .key_is { - g.write('${exp_type.idx()} == ${got_type.idx()} && ${exp_type.has_flag(.option)} == ${got_type.has_flag(.option)}') - return exp_type == got_type, true - } else { - g.write('${exp_type.idx()} != ${got_type.idx()}') - return exp_type != got_type, true + if got_sym.kind == .interface_ && got_sym.info is ast.Interface { + is_true := g.table.does_type_implement_interface(exp_type, + got_type) + if cond.op == .key_is { + if is_true { + g.write('1') + } else { + g.write('0') + } + return is_true, true + } else if cond.op == .not_is { + if is_true { + g.write('0') + } else { + g.write('1') + } + return !is_true, true + } + } + if cond.op == .key_is { + g.write('${exp_type.idx()} == ${got_type.idx()} && ${exp_type.has_flag(.option)} == ${got_type.has_flag(.option)}') + return exp_type == got_type, true + } else { + g.write('${exp_type.idx()} != ${got_type.idx()}') + return exp_type != got_type, true + } + } } } .eq, .ne { @@ -523,17 +528,7 @@ fn (mut g Gen) comptime_if_cond(cond ast.Expr, pkg_exist bool) (bool, bool) { .key_in, .not_in { if (cond.left is ast.TypeNode || cond.left is ast.SelectorExpr) && cond.right is ast.ArrayInit { - mut checked_type := ast.Type(0) - if cond.left is ast.SelectorExpr { - if cond.left.gkind_field == .typ { - checked_type = g.unwrap_generic(cond.left.name_type) - } else { - name := '${cond.left.expr}.${cond.left.field_name}' - checked_type = g.comptime_var_type_map[name] - } - } else { - checked_type = g.unwrap_generic((cond.left as ast.TypeNode).typ) - } + checked_type := g.get_expr_type(cond.left) for expr in cond.right.exprs { if expr is ast.ComptimeType { @@ -767,6 +762,7 @@ fn (mut g Gen) comptime_for(node ast.ComptimeFor) { g.writeln('\t${node.val_var}.indirections = ${field.typ.nr_muls()};') // g.comptime_var_type_map['${node.val_var}.typ'] = styp + g.comptime_var_type_map['${node.val_var}.unaliased_typ'] = unaliased_styp g.stmts(node.stmts) i++ g.writeln('}') diff --git a/vlib/v/tests/var_type_is_checking2_test.v b/vlib/v/tests/var_type_is_checking2_test.v new file mode 100644 index 0000000000..95b4546f45 --- /dev/null +++ b/vlib/v/tests/var_type_is_checking2_test.v @@ -0,0 +1,60 @@ +type MyAlias = string +type MyAlias2 = rune + +struct Foo { + a string + b ?string + c MyAlias + d MyAlias2 + e []int +} + +fn is_t[T](val T) bool { + $if val is T { + return true + } + return false +} + +fn test_main() { + a := Foo{} + $for field in Foo.fields { + $if field.unaliased_typ is ?string { + assert field.name == 'b' + println('is option string') + } $else $if field.unaliased_typ is string { + assert field.name in ['a', 'c'] + println('is string') + } $else $if field.unaliased_typ is MyAlias { + assert false + println('is string') + } $else $if field.typ is MyAlias2 { + assert field.name == 'd' + println('is MyAlias') + } $else $if field.typ is []int { + assert field.name == 'e' + println('is int') + } $else { + assert false + } + } +} + +fn test_generic() { + a := Foo{} + $for field in Foo.fields { + if is_t(field) { + println('T') + assert true + } else { + assert false + } + val := a.$(field.name) + if is_t(val) { + println('T') + assert true + } else { + assert false + } + } +} diff --git a/vlib/v/tests/var_type_is_checking_test.v b/vlib/v/tests/var_type_is_checking_test.v new file mode 100644 index 0000000000..fdc10d15b7 --- /dev/null +++ b/vlib/v/tests/var_type_is_checking_test.v @@ -0,0 +1,46 @@ +struct Test { + a int + b ?int +} + +fn test_main() { + hh := 1 // i64(4) + $if hh is $Int { + println('int') + assert true + } $else $if hh !is $Int { + println('string') + assert false + } + + b := 1.2 + $if b is $Int { + assert false + } $else $if b is $Float { + println('float') + assert true + } + + c := true + $if c is bool { + println('bool') + assert true + } + + d := Test{} + $if d.b is ?int { + println('?int') + assert true + } + $if d.a is ?int { + println('?int') + assert false + } $else $if d.a is int { + println('int') + assert true + } + + $if d is Test { + println('Test') + } +}