| // Copyright 2023 Google Inc. All rights reserved. |
| // |
| // Licensed 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 proptools |
| |
| import ( |
| "fmt" |
| "reflect" |
| "slices" |
| "strconv" |
| "strings" |
| |
| "github.com/google/blueprint/optional" |
| ) |
| |
| // ConfigurableOptional is the same as ShallowOptional, but we use this separate |
| // name to reserve the ability to switch to an alternative implementation later. |
| type ConfigurableOptional[T any] struct { |
| shallowOptional optional.ShallowOptional[T] |
| } |
| |
| // IsPresent returns true if the optional contains a value |
| func (o *ConfigurableOptional[T]) IsPresent() bool { |
| return o.shallowOptional.IsPresent() |
| } |
| |
| // IsEmpty returns true if the optional does not have a value |
| func (o *ConfigurableOptional[T]) IsEmpty() bool { |
| return o.shallowOptional.IsEmpty() |
| } |
| |
| // Get() returns the value inside the optional. It panics if IsEmpty() returns true |
| func (o *ConfigurableOptional[T]) Get() T { |
| return o.shallowOptional.Get() |
| } |
| |
| // GetOrDefault() returns the value inside the optional if IsPresent() returns true, |
| // or the provided value otherwise. |
| func (o *ConfigurableOptional[T]) GetOrDefault(other T) T { |
| return o.shallowOptional.GetOrDefault(other) |
| } |
| |
| type ConfigurableElements interface { |
| string | bool | []string |
| } |
| |
| type ConfigurableEvaluator interface { |
| EvaluateConfiguration(condition ConfigurableCondition, property string) ConfigurableValue |
| PropertyErrorf(property, fmt string, args ...interface{}) |
| } |
| |
| // configurableMarker is just so that reflection can check type of the first field of |
| // the struct to determine if it is a configurable struct. |
| type configurableMarker bool |
| |
| var configurableMarkerType reflect.Type = reflect.TypeOf((*configurableMarker)(nil)).Elem() |
| |
| // ConfigurableCondition represents a condition that is being selected on, like |
| // arch(), os(), soong_config_variable("namespace", "variable"), or other variables. |
| // It's represented generically as a function name + arguments in blueprint, soong |
| // interprets the function name and args into specific variable values. |
| // |
| // ConfigurableCondition is treated as an immutable object so that it may be shared |
| // between different configurable properties. |
| type ConfigurableCondition struct { |
| functionName string |
| args []string |
| } |
| |
| func NewConfigurableCondition(functionName string, args []string) ConfigurableCondition { |
| return ConfigurableCondition{ |
| functionName: functionName, |
| args: slices.Clone(args), |
| } |
| } |
| |
| func (c ConfigurableCondition) FunctionName() string { |
| return c.functionName |
| } |
| |
| func (c ConfigurableCondition) NumArgs() int { |
| return len(c.args) |
| } |
| |
| func (c ConfigurableCondition) Arg(i int) string { |
| return c.args[i] |
| } |
| |
| 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") |
| } |
| } |
| |
| // ConfigurablePattern represents a concrete value for a ConfigurableCase. |
| // Currently this just means the value of whatever variable is being looked |
| // up with the ConfigurableCase, but in the future it may be expanded to |
| // match multiple values (e.g. ranges of integers like 3..7). |
| // |
| // ConfigurablePattern can represent different types of values, like |
| // strings vs bools. |
| // |
| // ConfigurablePattern must be immutable so it can be shared between |
| // different configurable properties. |
| type ConfigurablePattern struct { |
| typ configurablePatternType |
| stringValue string |
| boolValue bool |
| } |
| |
| func NewStringConfigurablePattern(s string) ConfigurablePattern { |
| return ConfigurablePattern{ |
| typ: configurablePatternTypeString, |
| stringValue: s, |
| } |
| } |
| |
| func NewBoolConfigurablePattern(b bool) ConfigurablePattern { |
| return ConfigurablePattern{ |
| typ: configurablePatternTypeBool, |
| boolValue: b, |
| } |
| } |
| |
| func NewDefaultConfigurablePattern() ConfigurablePattern { |
| return ConfigurablePattern{ |
| typ: configurablePatternTypeDefault, |
| } |
| } |
| |
| 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() |
| } |
| |
| // ConfigurableCase represents a set of ConfigurablePatterns |
| // (exactly 1 pattern per ConfigurableCase), and a value to use |
| // if all of the patterns are matched. |
| // |
| // ConfigurableCase must be immutable so it can be shared between |
| // different configurable properties. |
| type ConfigurableCase[T ConfigurableElements] struct { |
| patterns []ConfigurablePattern |
| value *T |
| } |
| |
| type configurableCaseReflection interface { |
| initialize(patterns []ConfigurablePattern, value interface{}) |
| } |
| |
| var _ configurableCaseReflection = &ConfigurableCase[string]{} |
| |
| func NewConfigurableCase[T ConfigurableElements](patterns []ConfigurablePattern, value *T) ConfigurableCase[T] { |
| // Clone the values so they can't be modified from soong |
| patterns = slices.Clone(patterns) |
| return ConfigurableCase[T]{ |
| patterns: patterns, |
| value: copyConfiguredValue(value), |
| } |
| } |
| |
| 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") |
| } |
| |
| // for the given T, return the reflect.type of Configurable[T] |
| func configurableType(configuredType reflect.Type) (reflect.Type, error) { |
| // 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(Configurable[string]{}), nil |
| case reflect.Bool: |
| return reflect.TypeOf(Configurable[bool]{}), nil |
| case reflect.Slice: |
| switch configuredType.Elem().Kind() { |
| case reflect.String: |
| return reflect.TypeOf(Configurable[[]string]{}), nil |
| } |
| } |
| return nil, fmt.Errorf("configurable structs can only contain strings, bools, or string slices, found %s", configuredType.String()) |
| } |
| |
| // Configurable can wrap the type of a blueprint property, |
| // in order to allow select statements to be used in bp files |
| // for that property. For example, for the property struct: |
| // |
| // my_props { |
| // Property_a: string, |
| // Property_b: Configurable[string], |
| // } |
| // |
| // property_b can then use select statements: |
| // |
| // my_module { |
| // property_a: "foo" |
| // property_b: select(soong_config_variable("my_namespace", "my_variable"), { |
| // "value_1": "bar", |
| // "value_2": "baz", |
| // default: "qux", |
| // }) |
| // } |
| // |
| // The configurable property holds all the branches of the select |
| // statement in the bp file. To extract the final value, you must |
| // call Evaluate() on the configurable property. |
| // |
| // All configurable properties support being unset, so there is |
| // no need to use a pointer type like Configurable[*string]. |
| type Configurable[T ConfigurableElements] struct { |
| marker configurableMarker |
| propertyName string |
| inner *configurableInner[T] |
| } |
| |
| type configurableInner[T ConfigurableElements] struct { |
| single singleConfigurable[T] |
| replace bool |
| next *configurableInner[T] |
| } |
| |
| // singleConfigurable must be immutable so it can be reused |
| // between multiple configurables |
| type singleConfigurable[T ConfigurableElements] struct { |
| conditions []ConfigurableCondition |
| cases []ConfigurableCase[T] |
| } |
| |
| // Ignore the warning about the unused marker variable, it's used via reflection |
| var _ configurableMarker = Configurable[string]{}.marker |
| |
| func NewConfigurable[T ConfigurableElements](conditions []ConfigurableCondition, cases []ConfigurableCase[T]) Configurable[T] { |
| for _, c := range cases { |
| if len(c.patterns) != len(conditions) { |
| panic(fmt.Sprintf("All configurables cases must have as many patterns as the configurable has conditions. Expected: %d, found: %d", len(conditions), len(c.patterns))) |
| } |
| } |
| // Clone the slices so they can't be modified from soong |
| conditions = slices.Clone(conditions) |
| cases = slices.Clone(cases) |
| return Configurable[T]{ |
| inner: &configurableInner[T]{ |
| single: singleConfigurable[T]{ |
| conditions: conditions, |
| cases: cases, |
| }, |
| }, |
| } |
| } |
| |
| // Get returns the final value for the configurable property. |
| // A configurable property may be unset, in which case Get will return nil. |
| func (c *Configurable[T]) Get(evaluator ConfigurableEvaluator) ConfigurableOptional[T] { |
| result := c.inner.evaluate(c.propertyName, evaluator) |
| return configuredValuePtrToOptional(result) |
| } |
| |
| // GetOrDefault is the same as Get, but will return the provided default value if the property was unset. |
| func (c *Configurable[T]) GetOrDefault(evaluator ConfigurableEvaluator, defaultValue T) T { |
| result := c.inner.evaluate(c.propertyName, evaluator) |
| if result != nil { |
| // Copy the result so that it can't be changed from soong |
| return copyAndDereferenceConfiguredValue(result) |
| } |
| return defaultValue |
| } |
| |
| func (c *configurableInner[T]) evaluate(propertyName string, evaluator ConfigurableEvaluator) *T { |
| if c == nil { |
| return nil |
| } |
| if c.next == nil { |
| return c.single.evaluateNonTransitive(propertyName, evaluator) |
| } |
| if c.replace { |
| return replaceConfiguredValues( |
| c.single.evaluateNonTransitive(propertyName, evaluator), |
| c.next.evaluate(propertyName, evaluator), |
| ) |
| } else { |
| return appendConfiguredValues( |
| c.single.evaluateNonTransitive(propertyName, evaluator), |
| c.next.evaluate(propertyName, evaluator), |
| ) |
| } |
| } |
| |
| func (c *singleConfigurable[T]) evaluateNonTransitive(propertyName string, evaluator ConfigurableEvaluator) *T { |
| for i, case_ := range c.cases { |
| if len(c.conditions) != len(case_.patterns) { |
| evaluator.PropertyErrorf(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 { |
| return nil |
| } else if len(c.cases) == 1 { |
| return c.cases[0].value |
| } else { |
| evaluator.PropertyErrorf(propertyName, "Expected 0 or 1 branches in an unconfigured select, found %d", len(c.cases)) |
| return nil |
| } |
| } |
| values := make([]ConfigurableValue, len(c.conditions)) |
| for i, condition := range c.conditions { |
| values[i] = evaluator.EvaluateConfiguration(condition, propertyName) |
| } |
| foundMatch := false |
| nonMatchingIndex := 0 |
| var result *T |
| for _, case_ := range c.cases { |
| allMatch := true |
| for i, pat := range case_.patterns { |
| if !pat.matchesValueType(values[i]) { |
| evaluator.PropertyErrorf(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 |
| nonMatchingIndex = i |
| break |
| } |
| } |
| if allMatch && !foundMatch { |
| result = case_.value |
| foundMatch = true |
| } |
| } |
| if foundMatch { |
| return result |
| } |
| |
| evaluator.PropertyErrorf(propertyName, "%s had value %s, which was not handled by the select statement", c.conditions[nonMatchingIndex].String(), values[nonMatchingIndex].String()) |
| return nil |
| } |
| |
| func appendConfiguredValues[T ConfigurableElements](a, b *T) *T { |
| if a == nil && b == nil { |
| return nil |
| } |
| switch any(a).(type) { |
| case *[]string: |
| var a2 []string |
| var b2 []string |
| if a != nil { |
| a2 = *any(a).(*[]string) |
| } |
| if b != nil { |
| b2 = *any(b).(*[]string) |
| } |
| result := make([]string, len(a2)+len(b2)) |
| idx := 0 |
| for i := 0; i < len(a2); i++ { |
| result[idx] = a2[i] |
| idx += 1 |
| } |
| for i := 0; i < len(b2); i++ { |
| result[idx] = b2[i] |
| idx += 1 |
| } |
| return any(&result).(*T) |
| case *string: |
| a := String(any(a).(*string)) |
| b := String(any(b).(*string)) |
| result := a + b |
| return any(&result).(*T) |
| case *bool: |
| // Addition of bools will OR them together. This is inherited behavior |
| // from how proptools.ExtendBasicType works with non-configurable bools. |
| result := false |
| if a != nil { |
| result = result || *any(a).(*bool) |
| } |
| if b != nil { |
| result = result || *any(b).(*bool) |
| } |
| return any(&result).(*T) |
| default: |
| panic("Should be unreachable") |
| } |
| } |
| |
| func replaceConfiguredValues[T ConfigurableElements](a, b *T) *T { |
| if b != nil { |
| return b |
| } |
| return a |
| } |
| |
| // configurableReflection is an interface that exposes some methods that are |
| // helpful when working with reflect.Values of Configurable objects, used by |
| // the property unpacking code. You can't call unexported methods from reflection, |
| // (at least without unsafe pointer trickery) so this is the next best thing. |
| type configurableReflection interface { |
| setAppend(append any, replace bool, prepend bool) |
| configuredType() reflect.Type |
| clone() any |
| isEmpty() bool |
| printfInto(value string) error |
| } |
| |
| // Same as configurableReflection, but since initialize needs to take a pointer |
| // to a Configurable, it was broken out into a separate interface. |
| type configurablePtrReflection interface { |
| initialize(propertyName string, conditions []ConfigurableCondition, cases any) |
| } |
| |
| var _ configurableReflection = Configurable[string]{} |
| var _ configurablePtrReflection = &Configurable[string]{} |
| |
| func (c *Configurable[T]) initialize(propertyName string, conditions []ConfigurableCondition, cases any) { |
| c.propertyName = propertyName |
| c.inner = &configurableInner[T]{ |
| single: singleConfigurable[T]{ |
| conditions: conditions, |
| cases: cases.([]ConfigurableCase[T]), |
| }, |
| } |
| } |
| |
| func (c Configurable[T]) setAppend(append any, replace bool, prepend bool) { |
| a := append.(Configurable[T]) |
| if a.inner.isEmpty() { |
| return |
| } |
| c.inner.setAppend(a.inner, replace, prepend) |
| if c.inner == c.inner.next { |
| panic("pointer loop") |
| } |
| } |
| |
| func (c *configurableInner[T]) setAppend(append *configurableInner[T], replace bool, prepend bool) { |
| if c.isEmpty() { |
| *c = *append.clone() |
| } else if prepend { |
| if replace && c.alwaysHasValue() { |
| // The current value would always override the prepended value, so don't do anything |
| return |
| } |
| // We're going to replace the head node with the one from append, so allocate |
| // a new one here. |
| old := &configurableInner[T]{ |
| single: c.single, |
| replace: c.replace, |
| next: c.next, |
| } |
| *c = *append.clone() |
| curr := c |
| for curr.next != nil { |
| curr = curr.next |
| } |
| curr.next = old |
| curr.replace = replace |
| } else { |
| // If we're replacing with something that always has a value set, |
| // we can optimize the code by replacing our entire append chain here. |
| if replace && append.alwaysHasValue() { |
| *c = *append.clone() |
| } else { |
| curr := c |
| for curr.next != nil { |
| curr = curr.next |
| } |
| curr.next = append.clone() |
| curr.replace = replace |
| } |
| } |
| } |
| |
| func (c Configurable[T]) printfInto(value string) error { |
| return c.inner.printfInto(value) |
| } |
| |
| func (c *configurableInner[T]) printfInto(value string) error { |
| for c != nil { |
| if err := c.single.printfInto(value); err != nil { |
| return err |
| } |
| c = c.next |
| } |
| return nil |
| } |
| |
| func (c *singleConfigurable[T]) printfInto(value string) error { |
| for _, c := range c.cases { |
| if c.value == nil { |
| continue |
| } |
| switch v := any(c.value).(type) { |
| case *string: |
| if err := printfIntoString(v, value); err != nil { |
| return err |
| } |
| case *[]string: |
| for i := range *v { |
| if err := printfIntoString(&((*v)[i]), value); err != nil { |
| return err |
| } |
| } |
| } |
| } |
| return nil |
| } |
| |
| func printfIntoString(s *string, configValue string) error { |
| count := strings.Count(*s, "%") |
| if count == 0 { |
| return nil |
| } |
| |
| if count > 1 { |
| return fmt.Errorf("list/value variable properties only support a single '%%'") |
| } |
| |
| if !strings.Contains(*s, "%s") { |
| return fmt.Errorf("unsupported %% in value variable property") |
| } |
| |
| *s = fmt.Sprintf(*s, configValue) |
| |
| return nil |
| } |
| |
| func (c Configurable[T]) clone() any { |
| return Configurable[T]{ |
| propertyName: c.propertyName, |
| inner: c.inner.clone(), |
| } |
| } |
| |
| func (c *configurableInner[T]) clone() *configurableInner[T] { |
| if c == nil { |
| return nil |
| } |
| return &configurableInner[T]{ |
| // We don't need to clone the singleConfigurable because |
| // it's supposed to be immutable |
| single: c.single, |
| replace: c.replace, |
| next: c.next.clone(), |
| } |
| } |
| |
| func (c *configurableInner[T]) isEmpty() bool { |
| if c == nil { |
| return true |
| } |
| if !c.single.isEmpty() { |
| return false |
| } |
| return c.next.isEmpty() |
| } |
| |
| func (c Configurable[T]) isEmpty() bool { |
| return c.inner.isEmpty() |
| } |
| |
| func (c *singleConfigurable[T]) isEmpty() bool { |
| if c == nil { |
| return true |
| } |
| if len(c.cases) > 1 { |
| return false |
| } |
| if len(c.cases) == 1 && c.cases[0].value != nil { |
| return false |
| } |
| return true |
| } |
| |
| func (c *configurableInner[T]) alwaysHasValue() bool { |
| for curr := c; curr != nil; curr = curr.next { |
| if curr.single.alwaysHasValue() { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func (c *singleConfigurable[T]) alwaysHasValue() bool { |
| if len(c.cases) == 0 { |
| return false |
| } |
| for _, c := range c.cases { |
| if c.value == nil { |
| return false |
| } |
| } |
| return true |
| } |
| |
| func (c Configurable[T]) configuredType() reflect.Type { |
| return reflect.TypeOf((*T)(nil)).Elem() |
| } |
| |
| func copyConfiguredValue[T ConfigurableElements](t *T) *T { |
| if t == nil { |
| return nil |
| } |
| switch t2 := any(*t).(type) { |
| case []string: |
| result := any(slices.Clone(t2)).(T) |
| return &result |
| default: |
| x := *t |
| return &x |
| } |
| } |
| |
| func configuredValuePtrToOptional[T ConfigurableElements](t *T) ConfigurableOptional[T] { |
| if t == nil { |
| return ConfigurableOptional[T]{optional.NewShallowOptional(t)} |
| } |
| switch t2 := any(*t).(type) { |
| case []string: |
| result := any(slices.Clone(t2)).(T) |
| return ConfigurableOptional[T]{optional.NewShallowOptional(&result)} |
| default: |
| return ConfigurableOptional[T]{optional.NewShallowOptional(t)} |
| } |
| } |
| |
| func copyAndDereferenceConfiguredValue[T ConfigurableElements](t *T) T { |
| switch t2 := any(*t).(type) { |
| case []string: |
| return any(slices.Clone(t2)).(T) |
| default: |
| return *t |
| } |
| } |
| |
| // PrintfIntoConfigurable replaces %s occurrences in strings in Configurable properties |
| // with the provided string value. It's intention is to support soong config value variables |
| // on Configurable properties. |
| func PrintfIntoConfigurable(c any, value string) error { |
| return c.(configurableReflection).printfInto(value) |
| } |