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

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 {
// 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

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')
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')

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 {
// 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]

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) {
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')
}
}
}

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