Support converting simple $(eval) expressions

While supporting $(eval) in the general case is
impossible, as it would require emitting code at
runtime, it is possible to handle some special cases
that are common throughout the code base.

Specifically, an assignement expression (where the
left hand side is constant) can be converted without
needing to evaluate strings as code, as its whole
right hand side is treated as one string.

However, this eval with an assignemnt can only be
used as a statement, not an expression. So it requires
the eval to be either a top-level expression, or nested
within other expressions that can be converted to
statements such as $(foreach) or $(if).

Bug: 226974242
Test: go test
Change-Id: Ifc52ef9ab7d62a69251918fcde5463f80a98a542
This commit is contained in:
Cole Faust
2022-03-28 14:02:50 -07:00
parent 4242115d59
commit f035d405d8
4 changed files with 301 additions and 78 deletions

View File

@@ -221,11 +221,9 @@ func (xi *interpolateExpr) emitListVarCopy(gctx *generationContext) {
} }
func (xi *interpolateExpr) transform(transformer func(expr starlarkExpr) starlarkExpr) starlarkExpr { func (xi *interpolateExpr) transform(transformer func(expr starlarkExpr) starlarkExpr) starlarkExpr {
argsCopy := make([]starlarkExpr, len(xi.args)) for i := range xi.args {
for i, arg := range xi.args { xi.args[i] = xi.args[i].transform(transformer)
argsCopy[i] = arg.transform(transformer)
} }
xi.args = argsCopy
if replacement := transformer(xi); replacement != nil { if replacement := transformer(xi); replacement != nil {
return replacement return replacement
} else { } else {
@@ -591,11 +589,9 @@ func (cx *callExpr) transform(transformer func(expr starlarkExpr) starlarkExpr)
if cx.object != nil { if cx.object != nil {
cx.object = cx.object.transform(transformer) cx.object = cx.object.transform(transformer)
} }
argsCopy := make([]starlarkExpr, len(cx.args)) for i := range cx.args {
for i, arg := range cx.args { cx.args[i] = cx.args[i].transform(transformer)
argsCopy[i] = arg.transform(transformer)
} }
cx.args = argsCopy
if replacement := transformer(cx); replacement != nil { if replacement := transformer(cx); replacement != nil {
return replacement return replacement
} else { } else {
@@ -769,3 +765,35 @@ func isEmptyString(expr starlarkExpr) bool {
x, ok := expr.(*stringLiteralExpr) x, ok := expr.(*stringLiteralExpr)
return ok && x.literal == "" return ok && x.literal == ""
} }
func negateExpr(expr starlarkExpr) starlarkExpr {
switch typedExpr := expr.(type) {
case *notExpr:
return typedExpr.expr
case *inExpr:
typedExpr.isNot = !typedExpr.isNot
return typedExpr
case *eqExpr:
typedExpr.isEq = !typedExpr.isEq
return typedExpr
case *binaryOpExpr:
switch typedExpr.op {
case ">":
typedExpr.op = "<="
return typedExpr
case "<":
typedExpr.op = ">="
return typedExpr
case ">=":
typedExpr.op = "<"
return typedExpr
case "<=":
typedExpr.op = ">"
return typedExpr
default:
return &notExpr{expr: expr}
}
default:
return &notExpr{expr: expr}
}
}

View File

@@ -86,7 +86,7 @@ var knownFunctions = map[string]interface {
"filter": &simpleCallParser{name: baseName + ".filter", returnType: starlarkTypeList}, "filter": &simpleCallParser{name: baseName + ".filter", returnType: starlarkTypeList},
"filter-out": &simpleCallParser{name: baseName + ".filter_out", returnType: starlarkTypeList}, "filter-out": &simpleCallParser{name: baseName + ".filter_out", returnType: starlarkTypeList},
"firstword": &firstOrLastwordCallParser{isLastWord: false}, "firstword": &firstOrLastwordCallParser{isLastWord: false},
"foreach": &foreachCallPaser{}, "foreach": &foreachCallParser{},
"if": &ifCallParser{}, "if": &ifCallParser{},
"info": &makeControlFuncParser{name: baseName + ".mkinfo"}, "info": &makeControlFuncParser{name: baseName + ".mkinfo"},
"is-board-platform": &simpleCallParser{name: baseName + ".board_platform_is", returnType: starlarkTypeBool, addGlobals: true}, "is-board-platform": &simpleCallParser{name: baseName + ".board_platform_is", returnType: starlarkTypeBool, addGlobals: true},
@@ -117,6 +117,17 @@ var knownFunctions = map[string]interface {
"wildcard": &simpleCallParser{name: baseName + ".expand_wildcard", returnType: starlarkTypeList}, "wildcard": &simpleCallParser{name: baseName + ".expand_wildcard", returnType: starlarkTypeList},
} }
// The same as knownFunctions, but returns a []starlarkNode instead of a starlarkExpr
var knownNodeFunctions = map[string]interface {
parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) []starlarkNode
}{
"eval": &evalNodeParser{},
"if": &ifCallNodeParser{},
"inherit-product": &inheritProductCallParser{loadAlways: true},
"inherit-product-if-exists": &inheritProductCallParser{loadAlways: false},
"foreach": &foreachCallNodeParser{},
}
// These are functions that we don't implement conversions for, but // These are functions that we don't implement conversions for, but
// we allow seeing their definitions in the product config files. // we allow seeing their definitions in the product config files.
var ignoredDefines = map[string]bool{ var ignoredDefines = map[string]bool{
@@ -846,15 +857,19 @@ func (ctx *parseContext) findMatchingPaths(pattern []string) []string {
return res return res
} }
func (ctx *parseContext) handleInheritModule(v mkparser.Node, args *mkparser.MakeString, loadAlways bool) []starlarkNode { type inheritProductCallParser struct {
loadAlways bool
}
func (p *inheritProductCallParser) parse(ctx *parseContext, v mkparser.Node, args *mkparser.MakeString) []starlarkNode {
args.TrimLeftSpaces() args.TrimLeftSpaces()
args.TrimRightSpaces() args.TrimRightSpaces()
pathExpr := ctx.parseMakeString(v, args) pathExpr := ctx.parseMakeString(v, args)
if _, ok := pathExpr.(*badExpr); ok { if _, ok := pathExpr.(*badExpr); ok {
return []starlarkNode{ctx.newBadNode(v, "Unable to parse argument to inherit")} return []starlarkNode{ctx.newBadNode(v, "Unable to parse argument to inherit")}
} }
return ctx.handleSubConfig(v, pathExpr, loadAlways, func(im inheritedModule) starlarkNode { return ctx.handleSubConfig(v, pathExpr, p.loadAlways, func(im inheritedModule) starlarkNode {
return &inheritNode{im, loadAlways} return &inheritNode{im, p.loadAlways}
}) })
} }
@@ -873,19 +888,12 @@ func (ctx *parseContext) handleVariable(v *mkparser.Variable) []starlarkNode {
// $(error xxx) // $(error xxx)
// $(call other-custom-functions,...) // $(call other-custom-functions,...)
// inherit-product(-if-exists) gets converted to a series of statements, if name, args, ok := ctx.maybeParseFunctionCall(v, v.Name); ok {
// not just a single expression like parseReference returns. So handle it if kf, ok := knownNodeFunctions[name]; ok {
// separately at the beginning here. return kf.parse(ctx, v, args)
if strings.HasPrefix(v.Name.Dump(), "call inherit-product,") { }
args := v.Name.Clone()
args.ReplaceLiteral("call inherit-product,", "")
return ctx.handleInheritModule(v, args, true)
}
if strings.HasPrefix(v.Name.Dump(), "call inherit-product-if-exists,") {
args := v.Name.Clone()
args.ReplaceLiteral("call inherit-product-if-exists,", "")
return ctx.handleInheritModule(v, args, false)
} }
return []starlarkNode{&exprNode{expr: ctx.parseReference(v, v.Name)}} return []starlarkNode{&exprNode{expr: ctx.parseReference(v, v.Name)}}
} }
@@ -1030,49 +1038,19 @@ func (ctx *parseContext) parseCompare(cond *mkparser.Directive) starlarkExpr {
otherOperand = xLeft otherOperand = xLeft
} }
not := func(expr starlarkExpr) starlarkExpr {
switch typedExpr := expr.(type) {
case *inExpr:
typedExpr.isNot = !typedExpr.isNot
return typedExpr
case *eqExpr:
typedExpr.isEq = !typedExpr.isEq
return typedExpr
case *binaryOpExpr:
switch typedExpr.op {
case ">":
typedExpr.op = "<="
return typedExpr
case "<":
typedExpr.op = ">="
return typedExpr
case ">=":
typedExpr.op = "<"
return typedExpr
case "<=":
typedExpr.op = ">"
return typedExpr
default:
return &notExpr{expr: expr}
}
default:
return &notExpr{expr: expr}
}
}
// If we've identified one of the operands as being a string literal, check // If we've identified one of the operands as being a string literal, check
// for some special cases we can do to simplify the resulting expression. // for some special cases we can do to simplify the resulting expression.
if otherOperand != nil { if otherOperand != nil {
if stringOperand == "" { if stringOperand == "" {
if isEq { if isEq {
return not(otherOperand) return negateExpr(otherOperand)
} else { } else {
return otherOperand return otherOperand
} }
} }
if stringOperand == "true" && otherOperand.typ() == starlarkTypeBool { if stringOperand == "true" && otherOperand.typ() == starlarkTypeBool {
if !isEq { if !isEq {
return not(otherOperand) return negateExpr(otherOperand)
} else { } else {
return otherOperand return otherOperand
} }
@@ -1228,6 +1206,37 @@ func (ctx *parseContext) parseCompareStripFuncResult(directive *mkparser.Directi
right: xValue, isEq: !negate} right: xValue, isEq: !negate}
} }
func (ctx *parseContext) maybeParseFunctionCall(node mkparser.Node, ref *mkparser.MakeString) (name string, args *mkparser.MakeString, ok bool) {
ref.TrimLeftSpaces()
ref.TrimRightSpaces()
words := ref.SplitN(" ", 2)
if !words[0].Const() {
return "", nil, false
}
name = words[0].Dump()
args = mkparser.SimpleMakeString("", words[0].Pos())
if len(words) >= 2 {
args = words[1]
}
args.TrimLeftSpaces()
if name == "call" {
words = args.SplitN(",", 2)
if words[0].Empty() || !words[0].Const() {
return "", nil, false
}
name = words[0].Dump()
if len(words) < 2 {
args = &mkparser.MakeString{}
} else {
args = words[1]
}
}
ok = true
return
}
// parses $(...), returning an expression // parses $(...), returning an expression
func (ctx *parseContext) parseReference(node mkparser.Node, ref *mkparser.MakeString) starlarkExpr { func (ctx *parseContext) parseReference(node mkparser.Node, ref *mkparser.MakeString) starlarkExpr {
ref.TrimLeftSpaces() ref.TrimLeftSpaces()
@@ -1242,7 +1251,7 @@ func (ctx *parseContext) parseReference(node mkparser.Node, ref *mkparser.MakeSt
// If it is a single word, it can be a simple variable // If it is a single word, it can be a simple variable
// reference or a function call // reference or a function call
if len(words) == 1 && !isMakeControlFunc(refDump) && refDump != "shell" { if len(words) == 1 && !isMakeControlFunc(refDump) && refDump != "shell" && refDump != "eval" {
if strings.HasPrefix(refDump, soongNsPrefix) { if strings.HasPrefix(refDump, soongNsPrefix) {
// TODO (asmundak): if we find many, maybe handle them. // TODO (asmundak): if we find many, maybe handle them.
return ctx.newBadExpr(node, "SOONG_CONFIG_ variables cannot be referenced, use soong_config_get instead: %s", refDump) return ctx.newBadExpr(node, "SOONG_CONFIG_ variables cannot be referenced, use soong_config_get instead: %s", refDump)
@@ -1281,28 +1290,14 @@ func (ctx *parseContext) parseReference(node mkparser.Node, ref *mkparser.MakeSt
return ctx.newBadExpr(node, "unknown variable %s", refDump) return ctx.newBadExpr(node, "unknown variable %s", refDump)
} }
expr := &callExpr{name: words[0].Dump(), returnType: starlarkTypeUnknown} if name, args, ok := ctx.maybeParseFunctionCall(node, ref); ok {
args := mkparser.SimpleMakeString("", words[0].Pos()) if kf, found := knownFunctions[name]; found {
if len(words) >= 2 { return kf.parse(ctx, node, args)
args = words[1]
}
args.TrimLeftSpaces()
if expr.name == "call" {
words = args.SplitN(",", 2)
if words[0].Empty() || !words[0].Const() {
return ctx.newBadExpr(node, "cannot handle %s", refDump)
}
expr.name = words[0].Dump()
if len(words) < 2 {
args = &mkparser.MakeString{}
} else { } else {
args = words[1] return ctx.newBadExpr(node, "cannot handle invoking %s", name)
} }
}
if kf, found := knownFunctions[expr.name]; found {
return kf.parse(ctx, node, args)
} else { } else {
return ctx.newBadExpr(node, "cannot handle invoking %s", expr.name) return ctx.newBadExpr(node, "cannot handle %s", refDump)
} }
} }
@@ -1486,9 +1481,46 @@ func (p *ifCallParser) parse(ctx *parseContext, node mkparser.Node, args *mkpars
} }
} }
type foreachCallPaser struct{} type ifCallNodeParser struct{}
func (p *foreachCallPaser) parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) starlarkExpr { func (p *ifCallNodeParser) parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) []starlarkNode {
words := args.Split(",")
if len(words) != 2 && len(words) != 3 {
return []starlarkNode{ctx.newBadNode(node, "if function should have 2 or 3 arguments, found "+strconv.Itoa(len(words)))}
}
ifn := &ifNode{expr: ctx.parseMakeString(node, words[0])}
cases := []*switchCase{
{
gate: ifn,
nodes: ctx.parseNodeMakeString(node, words[1]),
},
}
if len(words) == 3 {
cases = append(cases, &switchCase{
gate: &elseNode{},
nodes: ctx.parseNodeMakeString(node, words[2]),
})
}
if len(cases) == 2 {
if len(cases[1].nodes) == 0 {
// Remove else branch if it has no contents
cases = cases[:1]
} else if len(cases[0].nodes) == 0 {
// If the if branch has no contents but the else does,
// move them to the if and negate its condition
ifn.expr = negateExpr(ifn.expr)
cases[0].nodes = cases[1].nodes
cases = cases[:1]
}
}
return []starlarkNode{&switchNode{ssCases: cases}}
}
type foreachCallParser struct{}
func (p *foreachCallParser) parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) starlarkExpr {
words := args.Split(",") words := args.Split(",")
if len(words) != 3 { if len(words) != 3 {
return ctx.newBadExpr(node, "foreach function should have 3 arguments, found "+strconv.Itoa(len(words))) return ctx.newBadExpr(node, "foreach function should have 3 arguments, found "+strconv.Itoa(len(words)))
@@ -1520,6 +1552,71 @@ func (p *foreachCallPaser) parse(ctx *parseContext, node mkparser.Node, args *mk
} }
} }
func transformNode(node starlarkNode, transformer func(expr starlarkExpr) starlarkExpr) {
switch a := node.(type) {
case *ifNode:
a.expr = a.expr.transform(transformer)
case *switchCase:
transformNode(a.gate, transformer)
for _, n := range a.nodes {
transformNode(n, transformer)
}
case *switchNode:
for _, n := range a.ssCases {
transformNode(n, transformer)
}
case *exprNode:
a.expr = a.expr.transform(transformer)
case *assignmentNode:
a.value = a.value.transform(transformer)
case *foreachNode:
a.list = a.list.transform(transformer)
for _, n := range a.actions {
transformNode(n, transformer)
}
}
}
type foreachCallNodeParser struct{}
func (p *foreachCallNodeParser) parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) []starlarkNode {
words := args.Split(",")
if len(words) != 3 {
return []starlarkNode{ctx.newBadNode(node, "foreach function should have 3 arguments, found "+strconv.Itoa(len(words)))}
}
if !words[0].Const() || words[0].Empty() || !identifierFullMatchRegex.MatchString(words[0].Strings[0]) {
return []starlarkNode{ctx.newBadNode(node, "first argument to foreach function must be a simple string identifier")}
}
loopVarName := words[0].Strings[0]
list := ctx.parseMakeString(node, words[1])
if list.typ() != starlarkTypeList {
list = &callExpr{
name: baseName + ".words",
returnType: starlarkTypeList,
args: []starlarkExpr{list},
}
}
actions := ctx.parseNodeMakeString(node, words[2])
// TODO(colefaust): Replace transforming code with something more elegant
for _, action := range actions {
transformNode(action, func(expr starlarkExpr) starlarkExpr {
if varRefExpr, ok := expr.(*variableRefExpr); ok && varRefExpr.ref.name() == loopVarName {
return &identifierExpr{loopVarName}
}
return nil
})
}
return []starlarkNode{&foreachNode{
varName: loopVarName,
list: list,
actions: actions,
}}
}
type wordCallParser struct{} type wordCallParser struct{}
func (p *wordCallParser) parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) starlarkExpr { func (p *wordCallParser) parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) starlarkExpr {
@@ -1630,6 +1727,31 @@ func (p *mathMaxOrMinCallParser) parse(ctx *parseContext, node mkparser.Node, ar
} }
} }
type evalNodeParser struct{}
func (p *evalNodeParser) parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) []starlarkNode {
parser := mkparser.NewParser("Eval expression", strings.NewReader(args.Dump()))
nodes, errs := parser.Parse()
if errs != nil {
return []starlarkNode{ctx.newBadNode(node, "Unable to parse eval statement")}
}
if len(nodes) == 0 {
return []starlarkNode{}
} else if len(nodes) == 1 {
switch n := nodes[0].(type) {
case *mkparser.Assignment:
if n.Name.Const() {
return ctx.handleAssignment(n)
}
case *mkparser.Comment:
return []starlarkNode{&commentNode{strings.TrimSpace("#" + n.Comment)}}
}
}
return []starlarkNode{ctx.newBadNode(node, "Eval expression too complex; only assignments and comments are supported")}
}
func (ctx *parseContext) parseMakeString(node mkparser.Node, mk *mkparser.MakeString) starlarkExpr { func (ctx *parseContext) parseMakeString(node mkparser.Node, mk *mkparser.MakeString) starlarkExpr {
if mk.Const() { if mk.Const() {
return &stringLiteralExpr{mk.Dump()} return &stringLiteralExpr{mk.Dump()}
@@ -1654,6 +1776,16 @@ func (ctx *parseContext) parseMakeString(node mkparser.Node, mk *mkparser.MakeSt
return NewInterpolateExpr(parts) return NewInterpolateExpr(parts)
} }
func (ctx *parseContext) parseNodeMakeString(node mkparser.Node, mk *mkparser.MakeString) []starlarkNode {
// Discard any constant values in the make string, as they would be top level
// string literals and do nothing.
result := make([]starlarkNode, 0, len(mk.Variables))
for i := range mk.Variables {
result = append(result, ctx.handleVariable(&mk.Variables[i])...)
}
return result
}
// Handles the statements whose treatment is the same in all contexts: comment, // Handles the statements whose treatment is the same in all contexts: comment,
// assignment, variable (which is a macro call in reality) and all constructs that // assignment, variable (which is a macro call in reality) and all constructs that
// do not handle in any context ('define directive and any unrecognized stuff). // do not handle in any context ('define directive and any unrecognized stuff).
@@ -1698,6 +1830,7 @@ func (ctx *parseContext) handleSimpleStatement(node mkparser.Node) []starlarkNod
if result == nil { if result == nil {
result = []starlarkNode{} result = []starlarkNode{}
} }
return result return result
} }

View File

@@ -1313,6 +1313,11 @@ BOOT_KERNEL_MODULES_FILTER_2 := $(foreach m,$(BOOT_KERNEL_MODULES_LIST),%/$(m))
FOREACH_WITH_IF := $(foreach module,\ FOREACH_WITH_IF := $(foreach module,\
$(BOOT_KERNEL_MODULES_LIST),\ $(BOOT_KERNEL_MODULES_LIST),\
$(if $(filter $(module),foo.ko),,$(error module "$(module)" has an error!))) $(if $(filter $(module),foo.ko),,$(error module "$(module)" has an error!)))
# Same as above, but not assigning it to a variable allows it to be converted to statements
$(foreach module,\
$(BOOT_KERNEL_MODULES_LIST),\
$(if $(filter $(module),foo.ko),,$(error module "$(module)" has an error!)))
`, `,
expected: `load("//build/make/core:product_config.rbc", "rblf") expected: `load("//build/make/core:product_config.rbc", "rblf")
@@ -1324,6 +1329,10 @@ def init(g, handle):
g["BOOT_KERNEL_MODULES_LIST"] += ["bar.ko"] g["BOOT_KERNEL_MODULES_LIST"] += ["bar.ko"]
g["BOOT_KERNEL_MODULES_FILTER_2"] = ["%%/%s" % m for m in g["BOOT_KERNEL_MODULES_LIST"]] g["BOOT_KERNEL_MODULES_FILTER_2"] = ["%%/%s" % m for m in g["BOOT_KERNEL_MODULES_LIST"]]
g["FOREACH_WITH_IF"] = [("" if rblf.filter(module, "foo.ko") else rblf.mkerror("product.mk", "module \"%s\" has an error!" % module)) for module in g["BOOT_KERNEL_MODULES_LIST"]] g["FOREACH_WITH_IF"] = [("" if rblf.filter(module, "foo.ko") else rblf.mkerror("product.mk", "module \"%s\" has an error!" % module)) for module in g["BOOT_KERNEL_MODULES_LIST"]]
# Same as above, but not assigning it to a variable allows it to be converted to statements
for module in g["BOOT_KERNEL_MODULES_LIST"]:
if not rblf.filter(module, "foo.ko"):
rblf.mkerror("product.mk", "module \"%s\" has an error!" % module)
`, `,
}, },
{ {
@@ -1472,6 +1481,34 @@ LOCAL_PATH := $(call my-dir)
def init(g, handle): def init(g, handle):
cfg = rblf.cfg(handle) cfg = rblf.cfg(handle)
`,
},
{
desc: "Evals",
mkname: "product.mk",
in: `
$(eval)
$(eval MY_VAR := foo)
$(eval # This is a test of eval functions)
$(eval $(TOO_COMPLICATED) := bar)
$(foreach x,$(MY_LIST_VAR), \
$(eval PRODUCT_COPY_FILES += foo/bar/$(x):$(TARGET_COPY_OUT_VENDOR)/etc/$(x)) \
$(if $(MY_OTHER_VAR),$(eval PRODUCT_COPY_FILES += $(MY_OTHER_VAR):foo/bar/$(x))) \
)
`,
expected: `load("//build/make/core:product_config.rbc", "rblf")
def init(g, handle):
cfg = rblf.cfg(handle)
g["MY_VAR"] = "foo"
# This is a test of eval functions
rblf.mk2rbc_error("product.mk:5", "Eval expression too complex; only assignments and comments are supported")
for x in rblf.words(g.get("MY_LIST_VAR", "")):
rblf.setdefault(handle, "PRODUCT_COPY_FILES")
cfg["PRODUCT_COPY_FILES"] += ("foo/bar/%s:%s/etc/%s" % (x, g.get("TARGET_COPY_OUT_VENDOR", ""), x)).split()
if g.get("MY_OTHER_VAR", ""):
cfg["PRODUCT_COPY_FILES"] += ("%s:foo/bar/%s" % (g.get("MY_OTHER_VAR", ""), x)).split()
`, `,
}, },
} }

View File

@@ -294,3 +294,28 @@ func (ssw *switchNode) emit(gctx *generationContext) {
ssCase.emit(gctx) ssCase.emit(gctx)
} }
} }
type foreachNode struct {
varName string
list starlarkExpr
actions []starlarkNode
}
func (f *foreachNode) emit(gctx *generationContext) {
gctx.newLine()
gctx.writef("for %s in ", f.varName)
f.list.emit(gctx)
gctx.write(":")
gctx.indentLevel++
hasStatements := false
for _, a := range f.actions {
if _, ok := a.(*commentNode); !ok {
hasStatements = true
}
a.emit(gctx)
}
if !hasStatements {
gctx.emitPass()
}
gctx.indentLevel--
}