checker: initial support for evaluating expressions at compile time (#7248)

pull/7605/head
spaceface777 2020-12-11 04:46:06 +01:00 committed by GitHub
parent c4e76e6a59
commit ca2c082a5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 163 additions and 20 deletions

View File

@ -1122,6 +1122,13 @@ pub fn (expr Expr) is_expr() bool {
return true 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 // check if stmt can be an expression in C
pub fn (stmt Stmt) check_c_expr() ? { pub fn (stmt Stmt) check_c_expr() ? {
match stmt { match stmt {

View File

@ -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 { fn (mut c Checker) comp_if_branch(cond ast.Expr, pos token.Position) bool {
// TODO: better error messages here // TODO: better error messages here
match cond { match cond {
ast.BoolLiteral {
return !cond.val
}
ast.ParExpr { ast.ParExpr {
return c.comp_if_branch(cond.expr, pos) 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 return l && r // skip (return true) only if both should be skipped
} }
.key_is, .not_is { .key_is, .not_is {
// $if method.@type is string if cond.left is ast.SelectorExpr && cond.right is ast.Type {
// TODO better checks here, will be done in comp. for PR // $if method.@type is string
if cond.left !is ast.SelectorExpr || cond.right !is ast.Type { } else {
c.error('invalid `\$if` condition', cond.pos) c.error('invalid `\$if` condition: $cond.left', cond.pos)
} }
} }
.eq, .ne { .eq, .ne {
// $if method.args.len == 1 if cond.left is ast.SelectorExpr && cond.right is ast.IntegerLiteral {
// TODO better checks here, will be done in comp. for PR // $if method.args.len == 1
if cond.left !is ast.SelectorExpr || cond.right !is ast.IntegerLiteral { } else if cond.left is ast.Ident {
c.error('invalid `\$if` condition', cond.pos) // $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 { 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 } 'no_bounds_checking' { return cond.name !in c.pref.compile_defines_all }
else { return false } else { return false }
} }
} else { } else if cond.name !in c.pref.compile_defines_all {
if cond.name !in c.pref.compile_defines_all { // `$if some_var {}`
c.error('unknown \$if value', pos) 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 { else {
@ -4136,6 +4176,41 @@ fn (mut c Checker) comp_if_branch(cond ast.Expr, pos token.Position) bool {
return false 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 { fn (c &Checker) has_return(stmts []ast.Stmt) ?bool {
// complexity means either more match or ifs // complexity means either more match or ifs
mut has_complexity := false mut has_complexity := false

View File

@ -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') 4 | println('optional compitme define works')
5 | } 5 | }
6 | $if mysymbol { 6 | $if mysymbol {
| ~~~~~~~~~~~~ | ~~~~~~~~
7 | // this will produce a checker error when `-d mysymbol` is not given on the CLI 7 | // this will produce a checker error when `-d mysymbol` is not given on the CLI
8 | println('non optional comptime define works') 8 | println('non optional comptime define works')

View File

@ -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 | }

View File

@ -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 {}
}

View File

@ -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 { match name {
// platforms/os-es: // platforms/os-es:
'windows' { '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) { (g.pref.compile_defines_all.len > 0 && name in g.pref.compile_defines_all) {
return 'CUSTOM_DEFINE_$name' 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 none
return ''
} }
[inline] [inline]

View File

@ -184,6 +184,9 @@ fn (mut g Gen) comp_if(node ast.IfExpr) {
fn (mut g Gen) comp_if_expr(cond ast.Expr) { fn (mut g Gen) comp_if_expr(cond ast.Expr) {
match cond { match cond {
ast.BoolLiteral {
g.expr(cond)
}
ast.ParExpr { ast.ParExpr {
g.write('(') g.write('(')
g.comp_if_expr(cond.expr) 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) g.comp_if_expr(cond.right)
} }
ast.PostfixExpr { 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)') g.write('defined($ifdef)')
} }
ast.InfixExpr { ast.InfixExpr {
@ -213,15 +219,19 @@ fn (mut g Gen) comp_if_expr(cond ast.Expr) {
} }
.eq, .ne { .eq, .ne {
// TODO Implement `$if method.args.len == 1` // TODO Implement `$if method.args.len == 1`
g.write('1')
} }
else {} else {}
} }
} }
ast.Ident { 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)') g.write('defined($ifdef)')
} }
else {} else {
// should be unreachable, but just in case
g.write('1')
}
} }
} }

View File

@ -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
}
}