Support multi-variable selects and typed selects

This adds support for selecting on multiple variables at once, so that
you can do AND/OR combindations of them. For example:

select((
    arch(),
    os(),
), {
    ("arm64", "linux"): ["libfoo64"],
    (default, "linux"): ["libfoo"],
    (default, "windows"): ["libfoowindows"],
    (default, default): ["libbar"],
})

It also allows for select conditions to be boolean-typed. You can
write literal true and false without quotes to select on them. Currently
we don't have any boolean-typed variables though, so a fake one was
added for testing.

Bug: 323382414
Test: m nothing --no-skip-soong-tests
Change-Id: Ibe586e7b21865b8734027848cc421594cbd1d8cc
This commit is contained in:
Cole Faust 2024-04-10 14:57:34 -07:00
parent 09fe90e407
commit 3311debbb3
9 changed files with 846 additions and 320 deletions

View file

@ -571,37 +571,46 @@ func endPos(pos scanner.Position, n int) scanner.Position {
return pos return pos
} }
type SelectType int type ConfigurableCondition struct {
position scanner.Position
FunctionName string
Args []String
}
const ( func (c *ConfigurableCondition) Equals(other ConfigurableCondition) bool {
SelectTypeUnconfigured SelectType = iota // Used for selects with only one branch, which is "default" if c.FunctionName != other.FunctionName {
SelectTypeReleaseVariable return false
SelectTypeSoongConfigVariable
SelectTypeProductVariable
SelectTypeVariant
)
func (s SelectType) String() string {
switch s {
case SelectTypeUnconfigured:
return "unconfigured"
case SelectTypeReleaseVariable:
return "release variable"
case SelectTypeSoongConfigVariable:
return "soong config variable"
case SelectTypeProductVariable:
return "product variable"
case SelectTypeVariant:
return "variant"
default:
panic("unreachable")
} }
if len(c.Args) != len(other.Args) {
return false
}
for i := range c.Args {
if c.Args[i] != other.Args[i] {
return false
}
}
return true
}
func (c *ConfigurableCondition) String() string {
var sb strings.Builder
sb.WriteString(c.FunctionName)
sb.WriteRune('(')
for i, arg := range c.Args {
sb.WriteRune('"')
sb.WriteString(arg.Value)
sb.WriteRune('"')
if i < len(c.Args)-1 {
sb.WriteString(", ")
}
}
sb.WriteRune(')')
return sb.String()
} }
type Select struct { type Select struct {
KeywordPos scanner.Position // the keyword "select" KeywordPos scanner.Position // the keyword "select"
Typ SelectType Conditions []ConfigurableCondition
Condition String
LBracePos scanner.Position LBracePos scanner.Position
RBracePos scanner.Position RBracePos scanner.Position
Cases []*SelectCase // the case statements Cases []*SelectCase // the case statements
@ -640,8 +649,7 @@ func (s *Select) Type() Type {
} }
type SelectCase struct { type SelectCase struct {
// TODO: Support int and bool typed cases Patterns []Expression
Pattern String
ColonPos scanner.Position ColonPos scanner.Position
Value Expression Value Expression
} }
@ -656,7 +664,7 @@ func (c *SelectCase) String() string {
return "<select case>" return "<select case>"
} }
func (c *SelectCase) Pos() scanner.Position { return c.Pattern.LiteralPos } func (c *SelectCase) Pos() scanner.Position { return c.Patterns[0].Pos() }
func (c *SelectCase) End() scanner.Position { return c.Value.End() } func (c *SelectCase) End() scanner.Position { return c.Value.End() }
// UnsetProperty is the expression type of the "unset" keyword that can be // UnsetProperty is the expression type of the "unset" keyword that can be

View file

@ -28,6 +28,8 @@ var errTooManyErrors = errors.New("too many errors")
const maxErrors = 1 const maxErrors = 1
const default_select_branch_name = "__soong_conditions_default__"
type ParseError struct { type ParseError struct {
Err error Err error
Pos scanner.Position Pos scanner.Position
@ -177,7 +179,6 @@ func (p *parser) next() {
p.comments = append(p.comments, &CommentGroup{Comments: comments}) p.comments = append(p.comments, &CommentGroup{Comments: comments})
} }
} }
return
} }
func (p *parser) parseDefinitions() (defs []Definition) { func (p *parser) parseDefinitions() (defs []Definition) {
@ -386,11 +387,7 @@ func (p *parser) evaluateOperator(value1, value2 Expression, operator rune,
if _, ok := e2.(*Select); ok { if _, ok := e2.(*Select); ok {
// Promote e1 to a select so we can add e2 to it // Promote e1 to a select so we can add e2 to it
e1 = &Select{ e1 = &Select{
Typ: SelectTypeUnconfigured,
Cases: []*SelectCase{{ Cases: []*SelectCase{{
Pattern: String{
Value: "__soong_conditions_default__",
},
Value: e1, Value: e1,
}}, }},
} }
@ -563,49 +560,81 @@ func (p *parser) parseSelect() Expression {
result := &Select{ result := &Select{
KeywordPos: p.scanner.Position, KeywordPos: p.scanner.Position,
} }
p.accept(scanner.Ident) // Read the "select("
if !p.accept('(') {
return nil
}
switch p.scanner.TokenText() {
case "release_variable":
result.Typ = SelectTypeReleaseVariable
case "soong_config_variable":
result.Typ = SelectTypeSoongConfigVariable
case "product_variable":
result.Typ = SelectTypeProductVariable
case "variant":
result.Typ = SelectTypeVariant
default:
p.errorf("unknown select type %q, expected release_variable, soong_config_variable, product_variable, or variant", p.scanner.TokenText())
return nil
}
p.accept(scanner.Ident) p.accept(scanner.Ident)
if !p.accept('(') { if !p.accept('(') {
return nil return nil
} }
if s := p.parseStringValue(); s != nil { // If we see another '(', there's probably multiple conditions and there must
result.Condition = *s // be a ')' after. Set the multipleConditions variable to remind us to check for
} else { // the ')' after.
return nil multipleConditions := false
if p.tok == '(' {
multipleConditions = true
p.accept('(')
} }
if result.Typ == SelectTypeSoongConfigVariable { // Read all individual conditions
if !p.accept(',') { conditions := []ConfigurableCondition{}
for first := true; first || multipleConditions; first = false {
condition := ConfigurableCondition{
position: p.scanner.Position,
FunctionName: p.scanner.TokenText(),
}
if !p.accept(scanner.Ident) {
return nil return nil
} }
if s := p.parseStringValue(); s != nil { if !p.accept('(') {
result.Condition.Value += ":" + s.Value
} else {
return nil return nil
} }
for p.tok != ')' {
if s := p.parseStringValue(); s != nil {
condition.Args = append(condition.Args, *s)
} else {
return nil
}
if p.tok == ')' {
break
}
if !p.accept(',') {
return nil
}
}
p.accept(')')
for _, c := range conditions {
if c.Equals(condition) {
p.errorf("Duplicate select condition found: %s", c.String())
}
}
conditions = append(conditions, condition)
if multipleConditions {
if p.tok == ')' {
p.next()
break
}
if !p.accept(',') {
return nil
}
// Retry the closing parent to allow for a trailing comma
if p.tok == ')' {
p.next()
break
}
}
} }
if !p.accept(')') { if multipleConditions && len(conditions) < 2 {
p.errorf("Expected multiple select conditions due to the extra parenthesis, but only found 1. Please remove the extra parenthesis.")
return nil return nil
} }
result.Conditions = conditions
if !p.accept(',') { if !p.accept(',') {
return nil return nil
} }
@ -615,47 +644,79 @@ func (p *parser) parseSelect() Expression {
return nil return nil
} }
hasNonUnsetValue := false parseOnePattern := func() Expression {
for p.tok == scanner.String { switch p.tok {
c := &SelectCase{} case scanner.Ident:
if s := p.parseStringValue(); s != nil { switch p.scanner.TokenText() {
if strings.HasPrefix(s.Value, "__soong") { case "default":
p.errorf("select branch conditions starting with __soong are reserved for internal use") p.next()
return nil return &String{
LiteralPos: p.scanner.Position,
Value: default_select_branch_name,
}
case "true":
p.next()
return &Bool{
LiteralPos: p.scanner.Position,
Value: true,
}
case "false":
p.next()
return &Bool{
LiteralPos: p.scanner.Position,
Value: false,
}
default:
p.errorf("Expted a string, true, false, or default, got %s", p.scanner.TokenText())
} }
c.Pattern = *s case scanner.String:
} else { if s := p.parseStringValue(); s != nil {
return nil if strings.HasPrefix(s.Value, "__soong") {
p.errorf("select branch conditions starting with __soong are reserved for internal use")
return nil
}
return s
}
fallthrough
default:
p.errorf("Expted a string, true, false, or default, got %s", p.scanner.TokenText())
} }
c.ColonPos = p.scanner.Position return nil
if !p.accept(':') {
return nil
}
if p.tok == scanner.Ident && p.scanner.TokenText() == "unset" {
c.Value = UnsetProperty{Position: p.scanner.Position}
p.accept(scanner.Ident)
} else {
hasNonUnsetValue = true
c.Value = p.parseExpression()
}
if !p.accept(',') {
return nil
}
result.Cases = append(result.Cases, c)
} }
// Default must be last hasNonUnsetValue := false
if p.tok == scanner.Ident { for p.tok != '}' {
if p.scanner.TokenText() != "default" { c := &SelectCase{}
p.errorf("select cases can either be quoted strings or 'default' to match any value")
return nil if multipleConditions {
if !p.accept('(') {
return nil
}
for i := 0; i < len(conditions); i++ {
if p := parseOnePattern(); p != nil {
c.Patterns = append(c.Patterns, p)
} else {
return nil
}
if i < len(conditions)-1 {
if !p.accept(',') {
return nil
}
} else if p.tok == ',' {
// allow optional trailing comma
p.next()
}
}
if !p.accept(')') {
return nil
}
} else {
if p := parseOnePattern(); p != nil {
c.Patterns = append(c.Patterns, p)
} else {
return nil
}
} }
c := &SelectCase{Pattern: String{
LiteralPos: p.scanner.Position,
Value: "__soong_conditions_default__",
}}
p.accept(scanner.Ident)
c.ColonPos = p.scanner.Position c.ColonPos = p.scanner.Position
if !p.accept(':') { if !p.accept(':') {
return nil return nil
@ -676,9 +737,64 @@ func (p *parser) parseSelect() Expression {
// If all branches have the value "unset", then this is equivalent // If all branches have the value "unset", then this is equivalent
// to an empty select. // to an empty select.
if !hasNonUnsetValue { if !hasNonUnsetValue {
result.Typ = SelectTypeUnconfigured p.errorf("This select statement is empty, remove it")
result.Condition.Value = "" return nil
result.Cases = nil }
patternsEqual := func(a, b Expression) bool {
switch a2 := a.(type) {
case *String:
if b2, ok := b.(*String); ok {
return a2.Value == b2.Value
} else {
return false
}
case *Bool:
if b2, ok := b.(*Bool); ok {
return a2.Value == b2.Value
} else {
return false
}
default:
// true so that we produce an error in this unexpected scenario
return true
}
}
patternListsEqual := func(a, b []Expression) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if !patternsEqual(a[i], b[i]) {
return false
}
}
return true
}
for i, c := range result.Cases {
// Check for duplicates
for _, d := range result.Cases[i+1:] {
if patternListsEqual(c.Patterns, d.Patterns) {
p.errorf("Found duplicate select patterns: %v", c.Patterns)
return nil
}
}
// Check that the only all-default cases is the last one
if i < len(result.Cases)-1 {
isAllDefault := true
for _, x := range c.Patterns {
if x2, ok := x.(*String); !ok || x2.Value != default_select_branch_name {
isAllDefault = false
break
}
}
if isAllDefault {
p.errorf("Found a default select branch at index %d, expected it to be last (index %d)", i, len(result.Cases)-1)
return nil
}
}
} }
ty := UnsetType ty := UnsetType

View file

@ -1225,6 +1225,17 @@ func TestParserError(t *testing.T) {
input: "\x00", input: "\x00",
err: "invalid character NUL", err: "invalid character NUL",
}, },
{
name: "select with duplicate condition",
input: `
m {
foo: select((arch(), arch()), {
(default, default): true,
}),
}
`,
err: "Duplicate select condition found: arch()",
},
// TODO: test more parser errors // TODO: test more parser errors
} }

View file

@ -142,48 +142,83 @@ func (p *printer) printSelect(s *Select) {
if len(s.Cases) == 0 { if len(s.Cases) == 0 {
return return
} }
if len(s.Cases) == 1 && s.Cases[0].Pattern.Value == "__soong_conditions_default__" { if len(s.Cases) == 1 && len(s.Cases[0].Patterns) == 1 {
p.printExpression(s.Cases[0].Value) if str, ok := s.Cases[0].Patterns[0].(*String); ok && str.Value == default_select_branch_name {
p.pos = s.RBracePos p.printExpression(s.Cases[0].Value)
return p.pos = s.RBracePos
return
}
} }
p.requestSpace() p.requestSpace()
p.printToken("select(", s.KeywordPos) p.printToken("select(", s.KeywordPos)
switch s.Typ { multilineConditions := false
case SelectTypeSoongConfigVariable: if len(s.Conditions) > 1 {
p.printToken("soong_config_variable(", s.Condition.LiteralPos) p.printToken("(", s.KeywordPos)
parts := strings.Split(s.Condition.Value, ":") if s.Conditions[len(s.Conditions)-1].position.Line > s.KeywordPos.Line {
namespace := parts[0] multilineConditions = true
variable := parts[1] p.requestNewline()
p.printToken(strconv.Quote(namespace), s.Condition.LiteralPos) p.indent(p.curIndent() + 4)
p.printToken(",", s.Condition.LiteralPos) }
p.requestSpace() }
p.printToken(strconv.Quote(variable), s.Condition.LiteralPos) for i, c := range s.Conditions {
p.printToken(")", s.Condition.LiteralPos) p.printToken(c.FunctionName, c.position)
case SelectTypeReleaseVariable: p.printToken("(", c.position)
p.printToken("release_variable(", s.Condition.LiteralPos) for i, arg := range c.Args {
p.printToken(strconv.Quote(s.Condition.Value), s.Condition.LiteralPos) p.printToken(strconv.Quote(arg.Value), arg.LiteralPos)
p.printToken(")", s.Condition.LiteralPos) if i < len(c.Args)-1 {
case SelectTypeProductVariable: p.printToken(",", arg.LiteralPos)
p.printToken("product_variable(", s.Condition.LiteralPos) p.requestSpace()
p.printToken(strconv.Quote(s.Condition.Value), s.Condition.LiteralPos) }
p.printToken(")", s.Condition.LiteralPos) }
case SelectTypeVariant: p.printToken(")", p.pos)
p.printToken("variant(", s.Condition.LiteralPos) if len(s.Conditions) > 1 {
p.printToken(strconv.Quote(s.Condition.Value), s.Condition.LiteralPos) if multilineConditions {
p.printToken(")", s.Condition.LiteralPos) p.printToken(",", p.pos)
default: p.requestNewline()
panic("should be unreachable") } else if i < len(s.Conditions)-1 {
p.printToken(",", p.pos)
p.requestSpace()
}
}
}
if len(s.Conditions) > 1 {
if multilineConditions {
p.unindent(p.pos)
}
p.printToken(")", p.pos)
} }
p.printToken(", {", s.LBracePos) p.printToken(", {", s.LBracePos)
p.requestNewline() p.requestNewline()
p.indent(p.curIndent() + 4) p.indent(p.curIndent() + 4)
for _, c := range s.Cases { for _, c := range s.Cases {
p.requestNewline() p.requestNewline()
if c.Pattern.Value != "__soong_conditions_default__" { if len(c.Patterns) > 1 {
p.printToken(strconv.Quote(c.Pattern.Value), c.Pattern.LiteralPos) p.printToken("(", p.pos)
} else { }
p.printToken("default", c.Pattern.LiteralPos) for i, pat := range c.Patterns {
switch pat := pat.(type) {
case *String:
if pat.Value != default_select_branch_name {
p.printToken(strconv.Quote(pat.Value), pat.LiteralPos)
} else {
p.printToken("default", pat.LiteralPos)
}
case *Bool:
s := "false"
if pat.Value {
s = "true"
}
p.printToken(s, pat.LiteralPos)
default:
panic("Unhandled case")
}
if i < len(c.Patterns)-1 {
p.printToken(",", p.pos)
p.requestSpace()
}
}
if len(c.Patterns) > 1 {
p.printToken(")", p.pos)
} }
p.printToken(":", c.ColonPos) p.printToken(":", c.ColonPos)
p.requestSpace() p.requestSpace()

View file

@ -657,50 +657,44 @@ foo {
`, `,
}, },
{ {
name: "Select with only unsets is removed", name: "Multi-condition select",
input: ` input: `
foo { foo {
stuff: select(soong_config_variable("my_namespace", "my_variable"), { stuff: select((arch(), os()), {
"foo": unset, ("x86", "linux"): "a",
default: unset, (default, default): "b",
}), }),
} }
`, `,
output: ` output: `
foo { foo {
stuff: select((arch(), os()), {
("x86", "linux"): "a",
(default, default): "b",
}),
} }
`, `,
}, },
{ {
name: "Additions of unset selects are removed", name: "Multi-condition select with conditions on new lines",
input: ` input: `
foo { foo {
stuff: select(soong_config_variable("my_namespace", "my_variable"), { stuff: select((arch(),
"foo": "a", os()), {
default: "b", ("x86", "linux"): "a",
}) + select(soong_config_variable("my_namespace", "my_variable2"), { (default, default): "b",
"foo": unset,
default: unset,
}) + select(soong_config_variable("my_namespace", "my_variable3"), {
"foo": "c",
default: "d",
}), }),
} }
`, `,
// TODO(b/323382414): This is not good formatting, revisit later.
// But at least it removes the useless middle select
output: ` output: `
foo { foo {
stuff: select(soong_config_variable("my_namespace", "my_variable"), { stuff: select((
"foo": "a", arch(),
default: "b", os(),
}) + ), {
("x86", "linux"): "a",
select(soong_config_variable("my_namespace", "my_variable3"), { (default, default): "b",
"foo": "c", }),
default: "d",
}),
} }
`, `,
}, },

View file

@ -14,21 +14,18 @@
package proptools package proptools
import ( import (
"fmt"
"reflect" "reflect"
"slices" "slices"
"strconv"
"github.com/google/blueprint/parser" "strings"
) )
const default_select_branch_name = "__soong_conditions_default__"
type ConfigurableElements interface { type ConfigurableElements interface {
string | bool | []string string | bool | []string
} }
type ConfigurableEvaluator interface { type ConfigurableEvaluator interface {
EvaluateConfiguration(typ parser.SelectType, property, condition string) (string, bool) EvaluateConfiguration(condition ConfigurableCondition, property string) ConfigurableValue
PropertyErrorf(property, fmt string, args ...interface{}) PropertyErrorf(property, fmt string, args ...interface{})
} }
@ -38,6 +35,200 @@ type configurableMarker bool
var configurableMarkerType reflect.Type = reflect.TypeOf((*configurableMarker)(nil)).Elem() var configurableMarkerType reflect.Type = reflect.TypeOf((*configurableMarker)(nil)).Elem()
type ConfigurableCondition struct {
FunctionName string
Args []string
}
func (c *ConfigurableCondition) String() string {
var sb strings.Builder
sb.WriteString(c.FunctionName)
sb.WriteRune('(')
for i, arg := range c.Args {
sb.WriteString(strconv.Quote(arg))
if i < len(c.Args)-1 {
sb.WriteString(", ")
}
}
sb.WriteRune(')')
return sb.String()
}
type configurableValueType int
const (
configurableValueTypeString configurableValueType = iota
configurableValueTypeBool
configurableValueTypeUndefined
)
func (v *configurableValueType) patternType() configurablePatternType {
switch *v {
case configurableValueTypeString:
return configurablePatternTypeString
case configurableValueTypeBool:
return configurablePatternTypeBool
default:
panic("unimplemented")
}
}
func (v *configurableValueType) String() string {
switch *v {
case configurableValueTypeString:
return "string"
case configurableValueTypeBool:
return "bool"
case configurableValueTypeUndefined:
return "undefined"
default:
panic("unimplemented")
}
}
// ConfigurableValue represents the value of a certain condition being selected on.
// This type mostly exists to act as a sum type between string, bool, and undefined.
type ConfigurableValue struct {
typ configurableValueType
stringValue string
boolValue bool
}
func (c *ConfigurableValue) String() string {
switch c.typ {
case configurableValueTypeString:
return strconv.Quote(c.stringValue)
case configurableValueTypeBool:
if c.boolValue {
return "true"
} else {
return "false"
}
case configurableValueTypeUndefined:
return "undefined"
default:
panic("unimplemented")
}
}
func ConfigurableValueString(s string) ConfigurableValue {
return ConfigurableValue{
typ: configurableValueTypeString,
stringValue: s,
}
}
func ConfigurableValueBool(b bool) ConfigurableValue {
return ConfigurableValue{
typ: configurableValueTypeBool,
boolValue: b,
}
}
func ConfigurableValueUndefined() ConfigurableValue {
return ConfigurableValue{
typ: configurableValueTypeUndefined,
}
}
type configurablePatternType int
const (
configurablePatternTypeString configurablePatternType = iota
configurablePatternTypeBool
configurablePatternTypeDefault
)
func (v *configurablePatternType) String() string {
switch *v {
case configurablePatternTypeString:
return "string"
case configurablePatternTypeBool:
return "bool"
case configurablePatternTypeDefault:
return "default"
default:
panic("unimplemented")
}
}
type configurablePattern struct {
typ configurablePatternType
stringValue string
boolValue bool
}
func (p *configurablePattern) matchesValue(v ConfigurableValue) bool {
if p.typ == configurablePatternTypeDefault {
return true
}
if v.typ == configurableValueTypeUndefined {
return false
}
if p.typ != v.typ.patternType() {
return false
}
switch p.typ {
case configurablePatternTypeString:
return p.stringValue == v.stringValue
case configurablePatternTypeBool:
return p.boolValue == v.boolValue
default:
panic("unimplemented")
}
}
func (p *configurablePattern) matchesValueType(v ConfigurableValue) bool {
if p.typ == configurablePatternTypeDefault {
return true
}
if v.typ == configurableValueTypeUndefined {
return true
}
return p.typ == v.typ.patternType()
}
type configurableCase[T ConfigurableElements] struct {
patterns []configurablePattern
value *T
}
func (c *configurableCase[T]) Clone() configurableCase[T] {
return configurableCase[T]{
patterns: slices.Clone(c.patterns),
value: copyConfiguredValue(c.value),
}
}
type configurableCaseReflection interface {
initialize(patterns []configurablePattern, value interface{})
}
var _ configurableCaseReflection = &configurableCase[string]{}
func (c *configurableCase[T]) initialize(patterns []configurablePattern, value interface{}) {
c.patterns = patterns
c.value = value.(*T)
}
// for the given T, return the reflect.type of configurableCase[T]
func configurableCaseType(configuredType reflect.Type) reflect.Type {
// I don't think it's possible to do this generically with go's
// current reflection apis unfortunately
switch configuredType.Kind() {
case reflect.String:
return reflect.TypeOf(configurableCase[string]{})
case reflect.Bool:
return reflect.TypeOf(configurableCase[bool]{})
case reflect.Slice:
switch configuredType.Elem().Kind() {
case reflect.String:
return reflect.TypeOf(configurableCase[[]string]{})
}
}
panic("unimplemented")
}
// Configurable can wrap the type of a blueprint property, // Configurable can wrap the type of a blueprint property,
// in order to allow select statements to be used in bp files // in order to allow select statements to be used in bp files
// for that property. For example, for the property struct: // for that property. For example, for the property struct:
@ -51,11 +242,11 @@ var configurableMarkerType reflect.Type = reflect.TypeOf((*configurableMarker)(n
// //
// my_module { // my_module {
// property_a: "foo" // property_a: "foo"
// property_b: select soong_config_variable: "my_namespace" "my_variable" { // property_b: select(soong_config_variable("my_namespace", "my_variable"), {
// "value_1": "bar", // "value_1": "bar",
// "value_2": "baz", // "value_2": "baz",
// default: "qux", // default: "qux",
// } // })
// } // }
// //
// The configurable property holds all the branches of the select // The configurable property holds all the branches of the select
@ -67,9 +258,8 @@ var configurableMarkerType reflect.Type = reflect.TypeOf((*configurableMarker)(n
type Configurable[T ConfigurableElements] struct { type Configurable[T ConfigurableElements] struct {
marker configurableMarker marker configurableMarker
propertyName string propertyName string
typ parser.SelectType conditions []ConfigurableCondition
condition string cases []configurableCase[T]
cases map[string]*T
appendWrapper *appendWrapper[T] appendWrapper *appendWrapper[T]
} }
@ -112,40 +302,49 @@ func (c *Configurable[T]) GetOrDefault(evaluator ConfigurableEvaluator, defaultV
} }
func (c *Configurable[T]) evaluateNonTransitive(evaluator ConfigurableEvaluator) *T { func (c *Configurable[T]) evaluateNonTransitive(evaluator ConfigurableEvaluator) *T {
if c.typ == parser.SelectTypeUnconfigured { for i, case_ := range c.cases {
if len(c.conditions) != len(case_.patterns) {
evaluator.PropertyErrorf(c.propertyName, "Expected each case to have as many patterns as conditions. conditions: %d, len(cases[%d].patterns): %d", len(c.conditions), i, len(case_.patterns))
return nil
}
}
if len(c.conditions) == 0 {
if len(c.cases) == 0 { if len(c.cases) == 0 {
return nil return nil
} else if len(c.cases) != 1 { } else if len(c.cases) == 1 {
panic(fmt.Sprintf("Expected 0 or 1 branches in an unconfigured select, found %d", len(c.cases))) return c.cases[0].value
} else {
evaluator.PropertyErrorf(c.propertyName, "Expected 0 or 1 branches in an unconfigured select, found %d", len(c.cases))
return nil
} }
result, ok := c.cases[default_select_branch_name] }
if !ok { values := make([]ConfigurableValue, len(c.conditions))
actual := "" for i, condition := range c.conditions {
for k := range c.cases { values[i] = evaluator.EvaluateConfiguration(condition, c.propertyName)
actual = k }
foundMatch := false
var result *T
for _, case_ := range c.cases {
allMatch := true
for i, pat := range case_.patterns {
if !pat.matchesValueType(values[i]) {
evaluator.PropertyErrorf(c.propertyName, "Expected all branches of a select on condition %s to have type %s, found %s", c.conditions[i].String(), values[i].typ.String(), pat.typ.String())
return nil
}
if !pat.matchesValue(values[i]) {
allMatch = false
break
} }
panic(fmt.Sprintf("Expected the single branch of an unconfigured select to be %q, got %q", default_select_branch_name, actual))
} }
return result if allMatch && !foundMatch {
} result = case_.value
val, defined := evaluator.EvaluateConfiguration(c.typ, c.propertyName, c.condition) foundMatch = true
if !defined {
if result, ok := c.cases[default_select_branch_name]; ok {
return result
} }
evaluator.PropertyErrorf(c.propertyName, "%s %q was not defined", c.typ.String(), c.condition)
return nil
} }
if val == default_select_branch_name { if foundMatch {
panic("Evaluator cannot return the default branch")
}
if result, ok := c.cases[val]; ok {
return result return result
} }
if result, ok := c.cases[default_select_branch_name]; ok { evaluator.PropertyErrorf(c.propertyName, "%s had value %s, which was not handled by the select statement", c.conditions, values)
return result
}
evaluator.PropertyErrorf(c.propertyName, "%s %q had value %q, which was not handled by the select statement", c.typ.String(), c.condition, val)
return nil return nil
} }
@ -216,17 +415,16 @@ type configurableReflection interface {
// Same as configurableReflection, but since initialize needs to take a pointer // Same as configurableReflection, but since initialize needs to take a pointer
// to a Configurable, it was broken out into a separate interface. // to a Configurable, it was broken out into a separate interface.
type configurablePtrReflection interface { type configurablePtrReflection interface {
initialize(propertyName string, typ parser.SelectType, condition string, cases any) initialize(propertyName string, conditions []ConfigurableCondition, cases any)
} }
var _ configurableReflection = Configurable[string]{} var _ configurableReflection = Configurable[string]{}
var _ configurablePtrReflection = &Configurable[string]{} var _ configurablePtrReflection = &Configurable[string]{}
func (c *Configurable[T]) initialize(propertyName string, typ parser.SelectType, condition string, cases any) { func (c *Configurable[T]) initialize(propertyName string, conditions []ConfigurableCondition, cases any) {
c.propertyName = propertyName c.propertyName = propertyName
c.typ = typ c.conditions = conditions
c.condition = condition c.cases = cases.([]configurableCase[T])
c.cases = cases.(map[string]*T)
c.appendWrapper = &appendWrapper[T]{} c.appendWrapper = &appendWrapper[T]{}
} }
@ -243,7 +441,7 @@ func (c Configurable[T]) isEmpty() bool {
if c.appendWrapper != nil && !c.appendWrapper.append.isEmpty() { if c.appendWrapper != nil && !c.appendWrapper.append.isEmpty() {
return false return false
} }
return c.typ == parser.SelectTypeUnconfigured && len(c.cases) == 0 return len(c.cases) == 0
} }
func (c Configurable[T]) configuredType() reflect.Type { func (c Configurable[T]) configuredType() reflect.Type {
@ -267,15 +465,17 @@ func (c *Configurable[T]) clone() *Configurable[T] {
} }
} }
casesCopy := make(map[string]*T, len(c.cases)) conditionsCopy := make([]ConfigurableCondition, len(c.conditions))
for k, v := range c.cases { copy(conditionsCopy, c.conditions)
casesCopy[k] = copyConfiguredValue(v)
casesCopy := make([]configurableCase[T], len(c.cases))
for i, case_ := range c.cases {
casesCopy[i] = case_.Clone()
} }
return &Configurable[T]{ return &Configurable[T]{
propertyName: c.propertyName, propertyName: c.propertyName,
typ: c.typ, conditions: conditionsCopy,
condition: c.condition,
cases: casesCopy, cases: casesCopy,
appendWrapper: inner, appendWrapper: inner,
} }

View file

@ -20,8 +20,6 @@ import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"github.com/google/blueprint/parser"
) )
type appendPropertyTestCase struct { type appendPropertyTestCase struct {
@ -1260,38 +1258,72 @@ func appendPropertiesTestCases() []appendPropertyTestCase {
name: "Append configurable", name: "Append configurable",
dst: &struct{ S Configurable[[]string] }{ dst: &struct{ S Configurable[[]string] }{
S: Configurable[[]string]{ S: Configurable[[]string]{
typ: parser.SelectTypeSoongConfigVariable, conditions: []ConfigurableCondition{{
condition: "foo", FunctionName: "soong_config_variable",
cases: map[string]*[]string{ Args: []string{
"a": {"1", "2"}, "my_namespace",
}, "foo",
},
}},
cases: []configurableCase[[]string]{{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "a",
}},
value: &[]string{"1", "2"},
}},
appendWrapper: &appendWrapper[[]string]{}, appendWrapper: &appendWrapper[[]string]{},
}, },
}, },
src: &struct{ S Configurable[[]string] }{ src: &struct{ S Configurable[[]string] }{
S: Configurable[[]string]{ S: Configurable[[]string]{
typ: parser.SelectTypeReleaseVariable, conditions: []ConfigurableCondition{{
condition: "bar", FunctionName: "release_variable",
cases: map[string]*[]string{ Args: []string{
"b": {"3", "4"}, "bar",
}, },
}},
cases: []configurableCase[[]string]{{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "b",
}},
value: &[]string{"3", "4"},
}},
appendWrapper: &appendWrapper[[]string]{}, appendWrapper: &appendWrapper[[]string]{},
}, },
}, },
out: &struct{ S Configurable[[]string] }{ out: &struct{ S Configurable[[]string] }{
S: Configurable[[]string]{ S: Configurable[[]string]{
typ: parser.SelectTypeSoongConfigVariable, conditions: []ConfigurableCondition{{
condition: "foo", FunctionName: "soong_config_variable",
cases: map[string]*[]string{ Args: []string{
"a": {"1", "2"}, "my_namespace",
}, "foo",
},
}},
cases: []configurableCase[[]string]{{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "a",
}},
value: &[]string{"1", "2"},
}},
appendWrapper: &appendWrapper[[]string]{ appendWrapper: &appendWrapper[[]string]{
append: Configurable[[]string]{ append: Configurable[[]string]{
typ: parser.SelectTypeReleaseVariable, conditions: []ConfigurableCondition{{
condition: "bar", FunctionName: "release_variable",
cases: map[string]*[]string{ Args: []string{
"b": {"3", "4"}, "bar",
}, },
}},
cases: []configurableCase[[]string]{{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "b",
}},
value: &[]string{"3", "4"},
}},
appendWrapper: &appendWrapper[[]string]{}, appendWrapper: &appendWrapper[[]string]{},
}, },
}, },
@ -1303,38 +1335,72 @@ func appendPropertiesTestCases() []appendPropertyTestCase {
order: Prepend, order: Prepend,
dst: &struct{ S Configurable[[]string] }{ dst: &struct{ S Configurable[[]string] }{
S: Configurable[[]string]{ S: Configurable[[]string]{
typ: parser.SelectTypeSoongConfigVariable, conditions: []ConfigurableCondition{{
condition: "foo", FunctionName: "soong_config_variable",
cases: map[string]*[]string{ Args: []string{
"a": {"1", "2"}, "my_namespace",
}, "foo",
},
}},
cases: []configurableCase[[]string]{{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "a",
}},
value: &[]string{"1", "2"},
}},
appendWrapper: &appendWrapper[[]string]{}, appendWrapper: &appendWrapper[[]string]{},
}, },
}, },
src: &struct{ S Configurable[[]string] }{ src: &struct{ S Configurable[[]string] }{
S: Configurable[[]string]{ S: Configurable[[]string]{
typ: parser.SelectTypeReleaseVariable, conditions: []ConfigurableCondition{{
condition: "bar", FunctionName: "release_variable",
cases: map[string]*[]string{ Args: []string{
"b": {"3", "4"}, "bar",
}, },
}},
cases: []configurableCase[[]string]{{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "b",
}},
value: &[]string{"3", "4"},
}},
appendWrapper: &appendWrapper[[]string]{}, appendWrapper: &appendWrapper[[]string]{},
}, },
}, },
out: &struct{ S Configurable[[]string] }{ out: &struct{ S Configurable[[]string] }{
S: Configurable[[]string]{ S: Configurable[[]string]{
typ: parser.SelectTypeReleaseVariable, conditions: []ConfigurableCondition{{
condition: "bar", FunctionName: "release_variable",
cases: map[string]*[]string{ Args: []string{
"b": {"3", "4"}, "bar",
}, },
}},
cases: []configurableCase[[]string]{{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "b",
}},
value: &[]string{"3", "4"},
}},
appendWrapper: &appendWrapper[[]string]{ appendWrapper: &appendWrapper[[]string]{
append: Configurable[[]string]{ append: Configurable[[]string]{
typ: parser.SelectTypeSoongConfigVariable, conditions: []ConfigurableCondition{{
condition: "foo", FunctionName: "soong_config_variable",
cases: map[string]*[]string{ Args: []string{
"a": {"1", "2"}, "my_namespace",
}, "foo",
},
}},
cases: []configurableCase[[]string]{{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "a",
}},
value: &[]string{"1", "2"},
}},
appendWrapper: &appendWrapper[[]string]{}, appendWrapper: &appendWrapper[[]string]{},
}, },
}, },

View file

@ -356,10 +356,9 @@ func (ctx *unpackContext) unpackToConfigurable(propertyName string, property *pa
} }
result := Configurable[string]{ result := Configurable[string]{
propertyName: property.Name, propertyName: property.Name,
typ: parser.SelectTypeUnconfigured, cases: []configurableCase[string]{{
cases: map[string]*string{ value: &v.Value,
default_select_branch_name: &v.Value, }},
},
appendWrapper: &appendWrapper[string]{}, appendWrapper: &appendWrapper[string]{},
} }
return reflect.ValueOf(&result), true return reflect.ValueOf(&result), true
@ -374,10 +373,9 @@ func (ctx *unpackContext) unpackToConfigurable(propertyName string, property *pa
} }
result := Configurable[bool]{ result := Configurable[bool]{
propertyName: property.Name, propertyName: property.Name,
typ: parser.SelectTypeUnconfigured, cases: []configurableCase[bool]{{
cases: map[string]*bool{ value: &v.Value,
default_select_branch_name: &v.Value, }},
},
appendWrapper: &appendWrapper[bool]{}, appendWrapper: &appendWrapper[bool]{},
} }
return reflect.ValueOf(&result), true return reflect.ValueOf(&result), true
@ -392,9 +390,9 @@ func (ctx *unpackContext) unpackToConfigurable(propertyName string, property *pa
} }
switch configuredType.Elem().Kind() { switch configuredType.Elem().Kind() {
case reflect.String: case reflect.String:
var cases map[string]*[]string var value []string
if v.Values != nil { if v.Values != nil {
value := make([]string, 0, len(v.Values)) value = make([]string, len(v.Values))
itemProperty := &parser.Property{NamePos: property.NamePos, ColonPos: property.ColonPos} itemProperty := &parser.Property{NamePos: property.NamePos, ColonPos: property.ColonPos}
for i, expr := range v.Values { for i, expr := range v.Values {
itemProperty.Name = propertyName + "[" + strconv.Itoa(i) + "]" itemProperty.Name = propertyName + "[" + strconv.Itoa(i) + "]"
@ -404,14 +402,14 @@ func (ctx *unpackContext) unpackToConfigurable(propertyName string, property *pa
ctx.addError(err) ctx.addError(err)
return reflect.ValueOf(Configurable[[]string]{}), false return reflect.ValueOf(Configurable[[]string]{}), false
} }
value = append(value, exprUnpacked.Interface().(string)) value[i] = exprUnpacked.Interface().(string)
} }
cases = map[string]*[]string{default_select_branch_name: &value}
} }
result := Configurable[[]string]{ result := Configurable[[]string]{
propertyName: property.Name, propertyName: property.Name,
typ: parser.SelectTypeUnconfigured, cases: []configurableCase[[]string]{{
cases: cases, value: &value,
}},
appendWrapper: &appendWrapper[[]string]{}, appendWrapper: &appendWrapper[[]string]{},
} }
return reflect.ValueOf(&result), true return reflect.ValueOf(&result), true
@ -424,46 +422,81 @@ func (ctx *unpackContext) unpackToConfigurable(propertyName string, property *pa
case *parser.Select: case *parser.Select:
resultPtr := reflect.New(configurableType) resultPtr := reflect.New(configurableType)
result := resultPtr.Elem() result := resultPtr.Elem()
cases := reflect.MakeMapWithSize(reflect.MapOf(reflect.TypeOf(""), reflect.PointerTo(configuredType)), len(v.Cases)) conditions := make([]ConfigurableCondition, len(v.Conditions))
for i, cond := range v.Conditions {
args := make([]string, len(cond.Args))
for j, arg := range cond.Args {
args[j] = arg.Value
}
conditions[i] = ConfigurableCondition{
FunctionName: cond.FunctionName,
Args: args,
}
}
configurableCaseType := configurableCaseType(configuredType)
cases := reflect.MakeSlice(reflect.SliceOf(configurableCaseType), 0, len(v.Cases))
for i, c := range v.Cases { for i, c := range v.Cases {
p := &parser.Property{ p := &parser.Property{
Name: property.Name + "[" + strconv.Itoa(i) + "]", Name: property.Name + "[" + strconv.Itoa(i) + "]",
NamePos: c.ColonPos, NamePos: c.ColonPos,
Value: c.Value, Value: c.Value,
} }
patterns := make([]configurablePattern, len(c.Patterns))
for i, pat := range c.Patterns {
switch pat := pat.(type) {
case *parser.String:
if pat.Value == "__soong_conditions_default__" {
patterns[i].typ = configurablePatternTypeDefault
} else {
patterns[i].typ = configurablePatternTypeString
patterns[i].stringValue = pat.Value
}
case *parser.Bool:
patterns[i].typ = configurablePatternTypeBool
patterns[i].boolValue = pat.Value
default:
panic("unimplemented")
}
}
var value reflect.Value
// Map the "unset" keyword to a nil pointer in the cases map // Map the "unset" keyword to a nil pointer in the cases map
if _, ok := c.Value.(parser.UnsetProperty); ok { if _, ok := c.Value.(parser.UnsetProperty); ok {
cases.SetMapIndex(reflect.ValueOf(c.Pattern.Value), reflect.Zero(reflect.PointerTo(configuredType))) value = reflect.Zero(reflect.PointerTo(configuredType))
continue } else {
} var err error
switch configuredType.Kind() { switch configuredType.Kind() {
case reflect.String, reflect.Bool: case reflect.String, reflect.Bool:
val, err := propertyToValue(reflect.PointerTo(configuredType), p) value, err = propertyToValue(reflect.PointerTo(configuredType), p)
if err != nil { if err != nil {
ctx.addError(&UnpackError{ ctx.addError(&UnpackError{
err, err,
c.Value.Pos(), c.Value.Pos(),
}) })
return reflect.New(configurableType), false return reflect.New(configurableType), false
} }
cases.SetMapIndex(reflect.ValueOf(c.Pattern.Value), val) case reflect.Slice:
case reflect.Slice: if configuredType.Elem().Kind() != reflect.String {
if configuredType.Elem().Kind() != reflect.String { panic("This should be unreachable because ConfigurableElements only accepts slices of strings")
panic("This should be unreachable because ConfigurableElements only accepts slices of strings") }
} value, ok = ctx.unpackToSlice(p.Name, p, reflect.PointerTo(configuredType))
val, ok := ctx.unpackToSlice(p.Name, p, reflect.PointerTo(configuredType)) if !ok {
if !ok { return reflect.New(configurableType), false
return reflect.New(configurableType), false }
} default:
cases.SetMapIndex(reflect.ValueOf(c.Pattern.Value), val) panic("This should be unreachable because ConfigurableElements only accepts strings, boools, or slices of strings")
default: }
panic("This should be unreachable because ConfigurableElements only accepts strings, boools, or slices of strings")
} }
case_ := reflect.New(configurableCaseType)
case_.Interface().(configurableCaseReflection).initialize(patterns, value.Interface())
cases = reflect.Append(cases, case_.Elem())
} }
resultPtr.Interface().(configurablePtrReflection).initialize( resultPtr.Interface().(configurablePtrReflection).initialize(
property.Name, property.Name,
v.Typ, conditions,
v.Condition.Value,
cases.Interface(), cases.Interface(),
) )
if v.Append != nil { if v.Append != nil {

View file

@ -734,10 +734,9 @@ var validUnpackTestCases = []struct {
}{ }{
Foo: Configurable[string]{ Foo: Configurable[string]{
propertyName: "foo", propertyName: "foo",
typ: parser.SelectTypeUnconfigured, cases: []configurableCase[string]{{
cases: map[string]*string{ value: StringPtr("bar"),
default_select_branch_name: StringPtr("bar"), }},
},
appendWrapper: &appendWrapper[string]{}, appendWrapper: &appendWrapper[string]{},
}, },
}, },
@ -756,10 +755,9 @@ var validUnpackTestCases = []struct {
}{ }{
Foo: Configurable[bool]{ Foo: Configurable[bool]{
propertyName: "foo", propertyName: "foo",
typ: parser.SelectTypeUnconfigured, cases: []configurableCase[bool]{{
cases: map[string]*bool{ value: BoolPtr(true),
default_select_branch_name: BoolPtr(true), }},
},
appendWrapper: &appendWrapper[bool]{}, appendWrapper: &appendWrapper[bool]{},
}, },
}, },
@ -778,10 +776,9 @@ var validUnpackTestCases = []struct {
}{ }{
Foo: Configurable[[]string]{ Foo: Configurable[[]string]{
propertyName: "foo", propertyName: "foo",
typ: parser.SelectTypeUnconfigured, cases: []configurableCase[[]string]{{
cases: map[string]*[]string{ value: &[]string{"a", "b"},
default_select_branch_name: {"a", "b"}, }},
},
appendWrapper: &appendWrapper[[]string]{}, appendWrapper: &appendWrapper[[]string]{},
}, },
}, },
@ -804,12 +801,34 @@ var validUnpackTestCases = []struct {
}{ }{
Foo: Configurable[string]{ Foo: Configurable[string]{
propertyName: "foo", propertyName: "foo",
typ: parser.SelectTypeSoongConfigVariable, conditions: []ConfigurableCondition{{
condition: "my_namespace:my_variable", FunctionName: "soong_config_variable",
cases: map[string]*string{ Args: []string{
"a": StringPtr("a2"), "my_namespace",
"b": StringPtr("b2"), "my_variable",
default_select_branch_name: StringPtr("c2"), },
}},
cases: []configurableCase[string]{
{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "a",
}},
value: StringPtr("a2"),
},
{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "b",
}},
value: StringPtr("b2"),
},
{
patterns: []configurablePattern{{
typ: configurablePatternTypeDefault,
}},
value: StringPtr("c2"),
},
}, },
appendWrapper: &appendWrapper[string]{}, appendWrapper: &appendWrapper[string]{},
}, },
@ -837,22 +856,66 @@ var validUnpackTestCases = []struct {
}{ }{
Foo: Configurable[string]{ Foo: Configurable[string]{
propertyName: "foo", propertyName: "foo",
typ: parser.SelectTypeSoongConfigVariable, conditions: []ConfigurableCondition{{
condition: "my_namespace:my_variable", FunctionName: "soong_config_variable",
cases: map[string]*string{ Args: []string{
"a": StringPtr("a2"), "my_namespace",
"b": StringPtr("b2"), "my_variable",
default_select_branch_name: StringPtr("c2"), },
}},
cases: []configurableCase[string]{
{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "a",
}},
value: StringPtr("a2"),
},
{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "b",
}},
value: StringPtr("b2"),
},
{
patterns: []configurablePattern{{
typ: configurablePatternTypeDefault,
}},
value: StringPtr("c2"),
},
}, },
appendWrapper: &appendWrapper[string]{ appendWrapper: &appendWrapper[string]{
append: Configurable[string]{ append: Configurable[string]{
propertyName: "foo", propertyName: "foo",
typ: parser.SelectTypeSoongConfigVariable, conditions: []ConfigurableCondition{{
condition: "my_namespace:my_2nd_variable", FunctionName: "soong_config_variable",
cases: map[string]*string{ Args: []string{
"d": StringPtr("d2"), "my_namespace",
"e": StringPtr("e2"), "my_2nd_variable",
default_select_branch_name: StringPtr("f2"), },
}},
cases: []configurableCase[string]{
{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "d",
}},
value: StringPtr("d2"),
},
{
patterns: []configurablePattern{{
typ: configurablePatternTypeString,
stringValue: "e",
}},
value: StringPtr("e2"),
},
{
patterns: []configurablePattern{{
typ: configurablePatternTypeDefault,
}},
value: StringPtr("f2"),
},
}, },
appendWrapper: &appendWrapper[string]{}, appendWrapper: &appendWrapper[string]{},
}, },