Skip to content

Commit

Permalink
Add support for expressions in syscfg values
Browse files Browse the repository at this point in the history
This adds support for evaluating syscfg values as expressions. To make
it compatible with existing code, syscfg value is only evaluated as
expression if its type is explicitly set to "expr", i.e.:

syscfg.defs:
  FOO:
    description: ...
    type: expr
    value: 1 + 2 + 3

Following tokens are allowed in expressions:
- literals (integers and strings)
- identifiers (references to other syscfg values)
- parentheses
- binary operators (arthmetic, relational and boolean)
- unary operator (boolean negation)
- built-in function calls

Most of operators support only integer values. Strings are supported by
"==" and "!=" only.

Available built-in functions are:
- min(a,b) - returns lesser of "a" and "b"
- max(a,b) - returns greater of "a" and "b"
- in_range(v,a,b) - returns if "v" is inside [a,b] range
- clamp(v,a,b) - clamps "v" to be inside [a,b] range
- ite(v,a,b) - if-then-else, returns "a" if "v", otherwise returns "b"
- in_set(v,...) - returns if "v" is one of remaining arguments

Note: all arguments to build-in functions shall be integer only, except
for "a" and "b" in ite() and all arguments in in_set().
  • Loading branch information
andrzej-kaczmarek committed Mar 23, 2022
1 parent 763d5c0 commit e1d34cb
Show file tree
Hide file tree
Showing 5 changed files with 428 additions and 6 deletions.
1 change: 1 addition & 0 deletions newt/resolve/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@ func (r *Resolver) reloadCfg() (bool, error) {

cfg.AddInjectedSettings()
cfg.ResolveValueRefs()
cfg.EvaluateExpressions()

// Determine if any new settings have been added or if any existing
// settings have changed.
Expand Down
363 changes: 363 additions & 0 deletions newt/syscfg/eval.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,363 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package syscfg

import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"mynewt.apache.org/newt/util"
"strconv"
)

func int2bool(x int) bool {
return x != 0
}

func bool2int(b bool) int {
if b {
return 1
}

return 0
}

func (cfg *Cfg) exprEvalLiteral(e *ast.BasicLit) (interface{}, error) {
kind := e.Kind
val := e.Value

switch kind {
case token.INT:
return strconv.Atoi(val)
case token.STRING:
return val, nil
}

return 0, util.FmtNewtError("Invalid exprEvalLiteral used in expression")
}

func (cfg *Cfg) exprEvalBinaryExpr(e *ast.BinaryExpr) (int, error) {
switch e.Op {
case token.ADD:
case token.SUB:
case token.MUL:
case token.QUO:
case token.REM:
case token.LAND:
case token.LOR:
case token.EQL:
case token.LSS:
case token.GTR:
case token.NEQ:
case token.LEQ:
case token.GEQ:
default:
return 0, util.FmtNewtError("Invalid \"%s\" operator in expression", e.Op.String())
}

var x interface{}
var y interface{}
var err error

x, err = cfg.exprEvalNode(e.X)
if err != nil {
return 0, err
}
y, err = cfg.exprEvalNode(e.Y)
if err != nil {
return 0, err
}

xv, xok := x.(int)
yv, yok := y.(int)

if xok != yok {
return 0, util.FmtNewtError("Mismatched types for \"%s\" operator in expression", e.Op.String())
}

ret := 0

if xok {
switch e.Op {
case token.ADD:
ret = xv + yv
case token.SUB:
ret = xv - yv
case token.MUL:
ret = xv * yv
case token.QUO:
ret = xv / yv
case token.REM:
ret = xv % yv
case token.LAND:
ret = bool2int(int2bool(xv) && int2bool(yv))
case token.LOR:
ret = bool2int(int2bool(xv) || int2bool(yv))
case token.EQL:
ret = bool2int(xv == yv)
case token.LSS:
ret = bool2int(xv < yv)
case token.GTR:
ret = bool2int(xv > yv)
case token.NEQ:
ret = bool2int(xv != yv)
case token.LEQ:
ret = bool2int(xv <= yv)
case token.GEQ:
ret = bool2int(xv >= yv)
}
} else {
// Each node is evaluated to int/string only so below assertions
// should never fail
switch e.Op {
case token.EQL:
ret = bool2int(x.(string) == y.(string))
case token.NEQ:
ret = bool2int(x.(string) != y.(string))
default:
return 0, util.FmtNewtError("Operator \"%s\" not supported for string literals",
e.Op.String())
}
}

return ret, nil
}

func (cfg *Cfg) exprEvalUnaryExpr(e *ast.UnaryExpr) (int, error) {
if e.Op != token.NOT {
return 0, util.FmtNewtError("Invalid \"%s\" operator in expression", e.Op.String())
}

x, err := cfg.exprEvalNode(e.X)
if err != nil {
return 0, err
}

xv, ok := x.(int)
if !ok {
return 0, util.FmtNewtError("String literals not applicable for \"%s\" operator", e.Op.String())
}

ret := bool2int(!int2bool(xv))

return ret, nil
}

func (cfg *Cfg) exprEvalCallExpr(e *ast.CallExpr) (interface{}, error) {
f := e.Fun.(*ast.Ident)
expectedArgc := -1
minArgc := -1

switch f.Name {
case "min", "max":
expectedArgc = 2
case "in_range", "clamp", "ite":
expectedArgc = 3
case "in_set":
minArgc = 2
default:
return 0, util.FmtNewtError("Invalid function in expression: \"%s\"", f.Name)
}

argc := len(e.Args)

if expectedArgc > 0 && argc != expectedArgc {
return 0, util.FmtNewtError("Invalid number of arguments for \"%s\": expected %d, got %d",
f.Name, expectedArgc, argc)
}

if minArgc > 0 && argc < minArgc {
return 0, util.FmtNewtError("Invalid number of arguments for \"%s\": expected at least %d, got %d",
f.Name, minArgc, argc)
}

argv := []interface{}{}
argvs := []string{}
for _, node := range e.Args {
arg, err := cfg.exprEvalNode(node)
if err != nil {
return 0, err
}

argv = append(argv, arg)
argvs = append(argvs, fmt.Sprintf("%v", arg))
}

var ret interface{}

switch f.Name {
case "min":
a, ok1 := argv[0].(int)
b, ok2 := argv[1].(int)
if !ok1 || !ok2 {
return 0, util.FmtNewtError("Invalid argument type for \"%s\"", f.Name)
}
ret = util.Min(a, b)
case "max":
a, ok1 := argv[0].(int)
b, ok2 := argv[1].(int)
if !ok1 || !ok2 {
return 0, util.FmtNewtError("Invalid argument type for \"%s\"", f.Name)
}
ret = util.Max(a, b)
case "clamp":
v, ok1 := argv[0].(int)
a, ok2 := argv[1].(int)
b, ok3 := argv[2].(int)
if !ok1 || !ok2 || !ok3 {
return 0, util.FmtNewtError("Invalid argument type for \"%s\"", f.Name)
}
if v < a {
ret = a
} else if v > b {
ret = b
} else {
ret = v
}
case "ite":
v, ok1 := argv[0].(int)
if !ok1 {
return 0, util.FmtNewtError("Invalid argument type for \"%s\"", f.Name)
}
if v != 0 {
ret = argv[1]
} else {
ret = argv[2]
}
case "in_range":
v, ok1 := argv[0].(int)
a, ok2 := argv[1].(int)
b, ok3 := argv[2].(int)
if !ok1 || !ok2 || !ok3 {
return 0, util.FmtNewtError("Invalid argument type for \"%s\"", f.Name)
}
ret = bool2int(v >= a && v <= b)
case "in_set":
m := make(map[interface{}]struct{})
for _, arg := range argv[1:] {
m[arg] = struct{}{}
}
_, ok := m[argv[0]]
ret = bool2int(ok)
}

return ret, nil
}

func (cfg *Cfg) exprEvalIdentifier(e *ast.Ident) (interface{}, error) {
name := e.Name

entry, ok := cfg.Settings[name]
if !ok {
return 0, util.FmtNewtError("Undefined identifier referenced: %s", name)
}

var val interface{}
var err error

switch entry.EvalState {
case CFG_EVAL_STATE_NONE:
entry, err = cfg.evalEntry(entry)
val = entry.EvalValue
case CFG_EVAL_STATE_RUNNING:
err = util.FmtNewtError("Circular identifier dependency in expression")
case CFG_EVAL_STATE_SUCCESS:
val = entry.EvalValue
case CFG_EVAL_STATE_FAILED:
err = util.FmtNewtError("")
}

return val, err
}

func (cfg *Cfg) exprEvalNode(node ast.Node) (interface{}, error) {
switch e := node.(type) {
case *ast.BasicLit:
return cfg.exprEvalLiteral(e)
case *ast.BinaryExpr:
return cfg.exprEvalBinaryExpr(e)
case *ast.UnaryExpr:
return cfg.exprEvalUnaryExpr(e)
case *ast.CallExpr:
return cfg.exprEvalCallExpr(e)
case *ast.Ident:
return cfg.exprEvalIdentifier(e)
case *ast.ParenExpr:
return cfg.exprEvalNode(e.X)
}

return 0, util.FmtNewtError("Invalid token in expression")
}

func (cfg *Cfg) evalEntry(entry CfgEntry) (CfgEntry, error) {
name := entry.Name

if entry.EvalState != CFG_EVAL_STATE_NONE {
panic("This should never happen :>")
}

entry.EvalState = CFG_EVAL_STATE_RUNNING
cfg.Settings[name] = entry

entry.EvalOrigValue = entry.Value

node, _ := parser.ParseExpr(entry.Value)
newVal, err := cfg.exprEvalNode(node)
if err != nil {
entry.EvalState = CFG_EVAL_STATE_FAILED
entry.EvalError = err
cfg.Settings[entry.Name] = entry
cfg.InvalidExpressions[entry.Name] = struct{}{}
err = util.FmtNewtError("")
return entry, err
}

switch val := newVal.(type) {
case int:
entry.EvalValue = val
entry.Value = strconv.Itoa(val)
case string:
entry.EvalValue = val
entry.Value = val
default:
panic("This should never happen :>")
}

entry.EvalState = CFG_EVAL_STATE_SUCCESS
cfg.Settings[entry.Name] = entry

return entry, nil
}

func (cfg *Cfg) Evaluate(name string) {
entry := cfg.Settings[name]

switch entry.EvalState {
case CFG_EVAL_STATE_NONE:
cfg.evalEntry(entry)
case CFG_EVAL_STATE_RUNNING:
panic("This should never happen :>")
case CFG_EVAL_STATE_SUCCESS:
// Already evaluated
case CFG_EVAL_STATE_FAILED:
// Already evaluated
}
}
1 change: 1 addition & 0 deletions newt/syscfg/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var cfgSettingNameTypeMap = map[string]CfgSettingType{
"raw": CFG_SETTING_TYPE_RAW,
"task_priority": CFG_SETTING_TYPE_TASK_PRIO,
"flash_owner": CFG_SETTING_TYPE_FLASH_OWNER,
"expr": CFG_SETTING_TYPE_EXPRESSION,
}

var cfgSettingNameStateMap = map[string]CfgSettingState{
Expand Down

0 comments on commit e1d34cb

Please sign in to comment.