checker: initial support for evaluating expressions at compile time (#7248)
parent
c4e76e6a59
commit
ca2c082a5e
|
@ -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 {
|
||||||
|
|
|
@ -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 cond.left is ast.SelectorExpr && cond.right is ast.Type {
|
||||||
// $if method.@type is string
|
// $if method.@type is string
|
||||||
// TODO better checks here, will be done in comp. for PR
|
} else {
|
||||||
if cond.left !is ast.SelectorExpr || cond.right !is ast.Type {
|
c.error('invalid `\$if` condition: $cond.left', cond.pos)
|
||||||
c.error('invalid `\$if` condition', cond.pos)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.eq, .ne {
|
.eq, .ne {
|
||||||
|
if cond.left is ast.SelectorExpr && cond.right is ast.IntegerLiteral {
|
||||||
// $if method.args.len == 1
|
// $if method.args.len == 1
|
||||||
// TODO better checks here, will be done in comp. for PR
|
} else if cond.left is ast.Ident {
|
||||||
if cond.left !is ast.SelectorExpr || cond.right !is ast.IntegerLiteral {
|
// $if version == 2
|
||||||
c.error('invalid `\$if` condition', cond.pos)
|
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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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 | }
|
|
@ -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 {
|
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]
|
||||||
|
|
|
@ -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')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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