mirror of
https://github.com/vlang/v.git
synced 2023-08-10 21:13:21 +03:00
checker: initial support for evaluating expressions at compile time (#7248)
This commit is contained in:
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')
|
||||
|
13
vlib/v/checker/tests/unknown_comptime_expr.out
Normal file
13
vlib/v/checker/tests/unknown_comptime_expr.out
Normal 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 | }
|
9
vlib/v/checker/tests/unknown_comptime_expr.vv
Normal file
9
vlib/v/checker/tests/unknown_comptime_expr.vv
Normal 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 {}
|
||||
}
|
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
30
vlib/v/tests/comptime_if_expr_test.v
Normal file
30
vlib/v/tests/comptime_if_expr_test.v
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user