1// Copyright 2023 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14package proptools 15 16import ( 17 "fmt" 18 "reflect" 19 "slices" 20 "strconv" 21 "strings" 22 23 "github.com/google/blueprint/optional" 24) 25 26// ConfigurableOptional is the same as ShallowOptional, but we use this separate 27// name to reserve the ability to switch to an alternative implementation later. 28type ConfigurableOptional[T any] struct { 29 shallowOptional optional.ShallowOptional[T] 30} 31 32// IsPresent returns true if the optional contains a value 33func (o *ConfigurableOptional[T]) IsPresent() bool { 34 return o.shallowOptional.IsPresent() 35} 36 37// IsEmpty returns true if the optional does not have a value 38func (o *ConfigurableOptional[T]) IsEmpty() bool { 39 return o.shallowOptional.IsEmpty() 40} 41 42// Get() returns the value inside the optional. It panics if IsEmpty() returns true 43func (o *ConfigurableOptional[T]) Get() T { 44 return o.shallowOptional.Get() 45} 46 47// GetOrDefault() returns the value inside the optional if IsPresent() returns true, 48// or the provided value otherwise. 49func (o *ConfigurableOptional[T]) GetOrDefault(other T) T { 50 return o.shallowOptional.GetOrDefault(other) 51} 52 53type ConfigurableElements interface { 54 string | bool | []string 55} 56 57type ConfigurableEvaluator interface { 58 EvaluateConfiguration(condition ConfigurableCondition, property string) ConfigurableValue 59 PropertyErrorf(property, fmt string, args ...interface{}) 60} 61 62// configurableMarker is just so that reflection can check type of the first field of 63// the struct to determine if it is a configurable struct. 64type configurableMarker bool 65 66var configurableMarkerType reflect.Type = reflect.TypeOf((*configurableMarker)(nil)).Elem() 67 68// ConfigurableCondition represents a condition that is being selected on, like 69// arch(), os(), soong_config_variable("namespace", "variable"), or other variables. 70// It's represented generically as a function name + arguments in blueprint, soong 71// interprets the function name and args into specific variable values. 72// 73// ConfigurableCondition is treated as an immutable object so that it may be shared 74// between different configurable properties. 75type ConfigurableCondition struct { 76 functionName string 77 args []string 78} 79 80func NewConfigurableCondition(functionName string, args []string) ConfigurableCondition { 81 return ConfigurableCondition{ 82 functionName: functionName, 83 args: slices.Clone(args), 84 } 85} 86 87func (c ConfigurableCondition) FunctionName() string { 88 return c.functionName 89} 90 91func (c ConfigurableCondition) NumArgs() int { 92 return len(c.args) 93} 94 95func (c ConfigurableCondition) Arg(i int) string { 96 return c.args[i] 97} 98 99func (c *ConfigurableCondition) String() string { 100 var sb strings.Builder 101 sb.WriteString(c.functionName) 102 sb.WriteRune('(') 103 for i, arg := range c.args { 104 sb.WriteString(strconv.Quote(arg)) 105 if i < len(c.args)-1 { 106 sb.WriteString(", ") 107 } 108 } 109 sb.WriteRune(')') 110 return sb.String() 111} 112 113type configurableValueType int 114 115const ( 116 configurableValueTypeString configurableValueType = iota 117 configurableValueTypeBool 118 configurableValueTypeUndefined 119) 120 121func (v *configurableValueType) patternType() configurablePatternType { 122 switch *v { 123 case configurableValueTypeString: 124 return configurablePatternTypeString 125 case configurableValueTypeBool: 126 return configurablePatternTypeBool 127 default: 128 panic("unimplemented") 129 } 130} 131 132func (v *configurableValueType) String() string { 133 switch *v { 134 case configurableValueTypeString: 135 return "string" 136 case configurableValueTypeBool: 137 return "bool" 138 case configurableValueTypeUndefined: 139 return "undefined" 140 default: 141 panic("unimplemented") 142 } 143} 144 145// ConfigurableValue represents the value of a certain condition being selected on. 146// This type mostly exists to act as a sum type between string, bool, and undefined. 147type ConfigurableValue struct { 148 typ configurableValueType 149 stringValue string 150 boolValue bool 151} 152 153func (c *ConfigurableValue) String() string { 154 switch c.typ { 155 case configurableValueTypeString: 156 return strconv.Quote(c.stringValue) 157 case configurableValueTypeBool: 158 if c.boolValue { 159 return "true" 160 } else { 161 return "false" 162 } 163 case configurableValueTypeUndefined: 164 return "undefined" 165 default: 166 panic("unimplemented") 167 } 168} 169 170func ConfigurableValueString(s string) ConfigurableValue { 171 return ConfigurableValue{ 172 typ: configurableValueTypeString, 173 stringValue: s, 174 } 175} 176 177func ConfigurableValueBool(b bool) ConfigurableValue { 178 return ConfigurableValue{ 179 typ: configurableValueTypeBool, 180 boolValue: b, 181 } 182} 183 184func ConfigurableValueUndefined() ConfigurableValue { 185 return ConfigurableValue{ 186 typ: configurableValueTypeUndefined, 187 } 188} 189 190type configurablePatternType int 191 192const ( 193 configurablePatternTypeString configurablePatternType = iota 194 configurablePatternTypeBool 195 configurablePatternTypeDefault 196) 197 198func (v *configurablePatternType) String() string { 199 switch *v { 200 case configurablePatternTypeString: 201 return "string" 202 case configurablePatternTypeBool: 203 return "bool" 204 case configurablePatternTypeDefault: 205 return "default" 206 default: 207 panic("unimplemented") 208 } 209} 210 211// ConfigurablePattern represents a concrete value for a ConfigurableCase. 212// Currently this just means the value of whatever variable is being looked 213// up with the ConfigurableCase, but in the future it may be expanded to 214// match multiple values (e.g. ranges of integers like 3..7). 215// 216// ConfigurablePattern can represent different types of values, like 217// strings vs bools. 218// 219// ConfigurablePattern must be immutable so it can be shared between 220// different configurable properties. 221type ConfigurablePattern struct { 222 typ configurablePatternType 223 stringValue string 224 boolValue bool 225} 226 227func NewStringConfigurablePattern(s string) ConfigurablePattern { 228 return ConfigurablePattern{ 229 typ: configurablePatternTypeString, 230 stringValue: s, 231 } 232} 233 234func NewBoolConfigurablePattern(b bool) ConfigurablePattern { 235 return ConfigurablePattern{ 236 typ: configurablePatternTypeBool, 237 boolValue: b, 238 } 239} 240 241func NewDefaultConfigurablePattern() ConfigurablePattern { 242 return ConfigurablePattern{ 243 typ: configurablePatternTypeDefault, 244 } 245} 246 247func (p *ConfigurablePattern) matchesValue(v ConfigurableValue) bool { 248 if p.typ == configurablePatternTypeDefault { 249 return true 250 } 251 if v.typ == configurableValueTypeUndefined { 252 return false 253 } 254 if p.typ != v.typ.patternType() { 255 return false 256 } 257 switch p.typ { 258 case configurablePatternTypeString: 259 return p.stringValue == v.stringValue 260 case configurablePatternTypeBool: 261 return p.boolValue == v.boolValue 262 default: 263 panic("unimplemented") 264 } 265} 266 267func (p *ConfigurablePattern) matchesValueType(v ConfigurableValue) bool { 268 if p.typ == configurablePatternTypeDefault { 269 return true 270 } 271 if v.typ == configurableValueTypeUndefined { 272 return true 273 } 274 return p.typ == v.typ.patternType() 275} 276 277// ConfigurableCase represents a set of ConfigurablePatterns 278// (exactly 1 pattern per ConfigurableCase), and a value to use 279// if all of the patterns are matched. 280// 281// ConfigurableCase must be immutable so it can be shared between 282// different configurable properties. 283type ConfigurableCase[T ConfigurableElements] struct { 284 patterns []ConfigurablePattern 285 value *T 286} 287 288type configurableCaseReflection interface { 289 initialize(patterns []ConfigurablePattern, value interface{}) 290} 291 292var _ configurableCaseReflection = &ConfigurableCase[string]{} 293 294func NewConfigurableCase[T ConfigurableElements](patterns []ConfigurablePattern, value *T) ConfigurableCase[T] { 295 // Clone the values so they can't be modified from soong 296 patterns = slices.Clone(patterns) 297 return ConfigurableCase[T]{ 298 patterns: patterns, 299 value: copyConfiguredValuePtr(value), 300 } 301} 302 303func (c *ConfigurableCase[T]) initialize(patterns []ConfigurablePattern, value interface{}) { 304 c.patterns = patterns 305 c.value = value.(*T) 306} 307 308// for the given T, return the reflect.type of configurableCase[T] 309func configurableCaseType(configuredType reflect.Type) reflect.Type { 310 // I don't think it's possible to do this generically with go's 311 // current reflection apis unfortunately 312 switch configuredType.Kind() { 313 case reflect.String: 314 return reflect.TypeOf(ConfigurableCase[string]{}) 315 case reflect.Bool: 316 return reflect.TypeOf(ConfigurableCase[bool]{}) 317 case reflect.Slice: 318 switch configuredType.Elem().Kind() { 319 case reflect.String: 320 return reflect.TypeOf(ConfigurableCase[[]string]{}) 321 } 322 } 323 panic("unimplemented") 324} 325 326// for the given T, return the reflect.type of Configurable[T] 327func configurableType(configuredType reflect.Type) (reflect.Type, error) { 328 // I don't think it's possible to do this generically with go's 329 // current reflection apis unfortunately 330 switch configuredType.Kind() { 331 case reflect.String: 332 return reflect.TypeOf(Configurable[string]{}), nil 333 case reflect.Bool: 334 return reflect.TypeOf(Configurable[bool]{}), nil 335 case reflect.Slice: 336 switch configuredType.Elem().Kind() { 337 case reflect.String: 338 return reflect.TypeOf(Configurable[[]string]{}), nil 339 } 340 } 341 return nil, fmt.Errorf("configurable structs can only contain strings, bools, or string slices, found %s", configuredType.String()) 342} 343 344// Configurable can wrap the type of a blueprint property, 345// in order to allow select statements to be used in bp files 346// for that property. For example, for the property struct: 347// 348// my_props { 349// Property_a: string, 350// Property_b: Configurable[string], 351// } 352// 353// property_b can then use select statements: 354// 355// my_module { 356// property_a: "foo" 357// property_b: select(soong_config_variable("my_namespace", "my_variable"), { 358// "value_1": "bar", 359// "value_2": "baz", 360// default: "qux", 361// }) 362// } 363// 364// The configurable property holds all the branches of the select 365// statement in the bp file. To extract the final value, you must 366// call Evaluate() on the configurable property. 367// 368// All configurable properties support being unset, so there is 369// no need to use a pointer type like Configurable[*string]. 370type Configurable[T ConfigurableElements] struct { 371 marker configurableMarker 372 propertyName string 373 inner *configurableInner[T] 374} 375 376type configurableInner[T ConfigurableElements] struct { 377 single singleConfigurable[T] 378 replace bool 379 next *configurableInner[T] 380} 381 382// singleConfigurable must be immutable so it can be reused 383// between multiple configurables 384type singleConfigurable[T ConfigurableElements] struct { 385 conditions []ConfigurableCondition 386 cases []ConfigurableCase[T] 387} 388 389// Ignore the warning about the unused marker variable, it's used via reflection 390var _ configurableMarker = Configurable[string]{}.marker 391 392func NewConfigurable[T ConfigurableElements](conditions []ConfigurableCondition, cases []ConfigurableCase[T]) Configurable[T] { 393 for _, c := range cases { 394 if len(c.patterns) != len(conditions) { 395 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))) 396 } 397 } 398 // Clone the slices so they can't be modified from soong 399 conditions = slices.Clone(conditions) 400 cases = slices.Clone(cases) 401 return Configurable[T]{ 402 inner: &configurableInner[T]{ 403 single: singleConfigurable[T]{ 404 conditions: conditions, 405 cases: cases, 406 }, 407 }, 408 } 409} 410 411func (c *Configurable[T]) AppendSimpleValue(value T) { 412 value = copyConfiguredValue(value) 413 // This may be a property that was never initialized from a bp file 414 if c.inner == nil { 415 c.inner = &configurableInner[T]{ 416 single: singleConfigurable[T]{ 417 cases: []ConfigurableCase[T]{{ 418 value: &value, 419 }}, 420 }, 421 } 422 return 423 } 424 c.inner.appendSimpleValue(value) 425} 426 427// Get returns the final value for the configurable property. 428// A configurable property may be unset, in which case Get will return nil. 429func (c *Configurable[T]) Get(evaluator ConfigurableEvaluator) ConfigurableOptional[T] { 430 result := c.inner.evaluate(c.propertyName, evaluator) 431 return configuredValuePtrToOptional(result) 432} 433 434// GetOrDefault is the same as Get, but will return the provided default value if the property was unset. 435func (c *Configurable[T]) GetOrDefault(evaluator ConfigurableEvaluator, defaultValue T) T { 436 result := c.inner.evaluate(c.propertyName, evaluator) 437 if result != nil { 438 // Copy the result so that it can't be changed from soong 439 return copyConfiguredValue(*result) 440 } 441 return defaultValue 442} 443 444func (c *configurableInner[T]) evaluate(propertyName string, evaluator ConfigurableEvaluator) *T { 445 if c == nil { 446 return nil 447 } 448 if c.next == nil { 449 return c.single.evaluateNonTransitive(propertyName, evaluator) 450 } 451 if c.replace { 452 return replaceConfiguredValues( 453 c.single.evaluateNonTransitive(propertyName, evaluator), 454 c.next.evaluate(propertyName, evaluator), 455 ) 456 } else { 457 return appendConfiguredValues( 458 c.single.evaluateNonTransitive(propertyName, evaluator), 459 c.next.evaluate(propertyName, evaluator), 460 ) 461 } 462} 463 464func (c *singleConfigurable[T]) evaluateNonTransitive(propertyName string, evaluator ConfigurableEvaluator) *T { 465 for i, case_ := range c.cases { 466 if len(c.conditions) != len(case_.patterns) { 467 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)) 468 return nil 469 } 470 } 471 if len(c.conditions) == 0 { 472 if len(c.cases) == 0 { 473 return nil 474 } else if len(c.cases) == 1 { 475 return c.cases[0].value 476 } else { 477 evaluator.PropertyErrorf(propertyName, "Expected 0 or 1 branches in an unconfigured select, found %d", len(c.cases)) 478 return nil 479 } 480 } 481 values := make([]ConfigurableValue, len(c.conditions)) 482 for i, condition := range c.conditions { 483 values[i] = evaluator.EvaluateConfiguration(condition, propertyName) 484 } 485 foundMatch := false 486 nonMatchingIndex := 0 487 var result *T 488 for _, case_ := range c.cases { 489 allMatch := true 490 for i, pat := range case_.patterns { 491 if !pat.matchesValueType(values[i]) { 492 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()) 493 return nil 494 } 495 if !pat.matchesValue(values[i]) { 496 allMatch = false 497 nonMatchingIndex = i 498 break 499 } 500 } 501 if allMatch && !foundMatch { 502 result = case_.value 503 foundMatch = true 504 } 505 } 506 if foundMatch { 507 return result 508 } 509 510 evaluator.PropertyErrorf(propertyName, "%s had value %s, which was not handled by the select statement", c.conditions[nonMatchingIndex].String(), values[nonMatchingIndex].String()) 511 return nil 512} 513 514func appendConfiguredValues[T ConfigurableElements](a, b *T) *T { 515 if a == nil && b == nil { 516 return nil 517 } 518 switch any(a).(type) { 519 case *[]string: 520 var a2 []string 521 var b2 []string 522 if a != nil { 523 a2 = *any(a).(*[]string) 524 } 525 if b != nil { 526 b2 = *any(b).(*[]string) 527 } 528 result := make([]string, len(a2)+len(b2)) 529 idx := 0 530 for i := 0; i < len(a2); i++ { 531 result[idx] = a2[i] 532 idx += 1 533 } 534 for i := 0; i < len(b2); i++ { 535 result[idx] = b2[i] 536 idx += 1 537 } 538 return any(&result).(*T) 539 case *string: 540 a := String(any(a).(*string)) 541 b := String(any(b).(*string)) 542 result := a + b 543 return any(&result).(*T) 544 case *bool: 545 // Addition of bools will OR them together. This is inherited behavior 546 // from how proptools.ExtendBasicType works with non-configurable bools. 547 result := false 548 if a != nil { 549 result = result || *any(a).(*bool) 550 } 551 if b != nil { 552 result = result || *any(b).(*bool) 553 } 554 return any(&result).(*T) 555 default: 556 panic("Should be unreachable") 557 } 558} 559 560func replaceConfiguredValues[T ConfigurableElements](a, b *T) *T { 561 if b != nil { 562 return b 563 } 564 return a 565} 566 567// configurableReflection is an interface that exposes some methods that are 568// helpful when working with reflect.Values of Configurable objects, used by 569// the property unpacking code. You can't call unexported methods from reflection, 570// (at least without unsafe pointer trickery) so this is the next best thing. 571type configurableReflection interface { 572 setAppend(append any, replace bool, prepend bool) 573 configuredType() reflect.Type 574 clone() any 575 isEmpty() bool 576 printfInto(value string) error 577} 578 579// Same as configurableReflection, but since initialize needs to take a pointer 580// to a Configurable, it was broken out into a separate interface. 581type configurablePtrReflection interface { 582 initialize(propertyName string, conditions []ConfigurableCondition, cases any) 583} 584 585var _ configurableReflection = Configurable[string]{} 586var _ configurablePtrReflection = &Configurable[string]{} 587 588func (c *Configurable[T]) initialize(propertyName string, conditions []ConfigurableCondition, cases any) { 589 c.propertyName = propertyName 590 c.inner = &configurableInner[T]{ 591 single: singleConfigurable[T]{ 592 conditions: conditions, 593 cases: cases.([]ConfigurableCase[T]), 594 }, 595 } 596} 597 598func (c Configurable[T]) setAppend(append any, replace bool, prepend bool) { 599 a := append.(Configurable[T]) 600 if a.inner.isEmpty() { 601 return 602 } 603 c.inner.setAppend(a.inner, replace, prepend) 604 if c.inner == c.inner.next { 605 panic("pointer loop") 606 } 607} 608 609func (c *configurableInner[T]) setAppend(append *configurableInner[T], replace bool, prepend bool) { 610 if c.isEmpty() { 611 *c = *append.clone() 612 } else if prepend { 613 if replace && c.alwaysHasValue() { 614 // The current value would always override the prepended value, so don't do anything 615 return 616 } 617 // We're going to replace the head node with the one from append, so allocate 618 // a new one here. 619 old := &configurableInner[T]{ 620 single: c.single, 621 replace: c.replace, 622 next: c.next, 623 } 624 *c = *append.clone() 625 curr := c 626 for curr.next != nil { 627 curr = curr.next 628 } 629 curr.next = old 630 curr.replace = replace 631 } else { 632 // If we're replacing with something that always has a value set, 633 // we can optimize the code by replacing our entire append chain here. 634 if replace && append.alwaysHasValue() { 635 *c = *append.clone() 636 } else { 637 curr := c 638 for curr.next != nil { 639 curr = curr.next 640 } 641 curr.next = append.clone() 642 curr.replace = replace 643 } 644 } 645} 646 647func (c *configurableInner[T]) appendSimpleValue(value T) { 648 if c.next == nil { 649 c.replace = false 650 c.next = &configurableInner[T]{ 651 single: singleConfigurable[T]{ 652 cases: []ConfigurableCase[T]{{ 653 value: &value, 654 }}, 655 }, 656 } 657 } else { 658 c.next.appendSimpleValue(value) 659 } 660} 661 662func (c Configurable[T]) printfInto(value string) error { 663 return c.inner.printfInto(value) 664} 665 666func (c *configurableInner[T]) printfInto(value string) error { 667 for c != nil { 668 if err := c.single.printfInto(value); err != nil { 669 return err 670 } 671 c = c.next 672 } 673 return nil 674} 675 676func (c *singleConfigurable[T]) printfInto(value string) error { 677 for _, c := range c.cases { 678 if c.value == nil { 679 continue 680 } 681 switch v := any(c.value).(type) { 682 case *string: 683 if err := printfIntoString(v, value); err != nil { 684 return err 685 } 686 case *[]string: 687 for i := range *v { 688 if err := printfIntoString(&((*v)[i]), value); err != nil { 689 return err 690 } 691 } 692 } 693 } 694 return nil 695} 696 697func printfIntoString(s *string, configValue string) error { 698 count := strings.Count(*s, "%") 699 if count == 0 { 700 return nil 701 } 702 703 if count > 1 { 704 return fmt.Errorf("list/value variable properties only support a single '%%'") 705 } 706 707 if !strings.Contains(*s, "%s") { 708 return fmt.Errorf("unsupported %% in value variable property") 709 } 710 711 *s = fmt.Sprintf(*s, configValue) 712 713 return nil 714} 715 716func (c Configurable[T]) clone() any { 717 return Configurable[T]{ 718 propertyName: c.propertyName, 719 inner: c.inner.clone(), 720 } 721} 722 723func (c *configurableInner[T]) clone() *configurableInner[T] { 724 if c == nil { 725 return nil 726 } 727 return &configurableInner[T]{ 728 // We don't need to clone the singleConfigurable because 729 // it's supposed to be immutable 730 single: c.single, 731 replace: c.replace, 732 next: c.next.clone(), 733 } 734} 735 736func (c *configurableInner[T]) isEmpty() bool { 737 if c == nil { 738 return true 739 } 740 if !c.single.isEmpty() { 741 return false 742 } 743 return c.next.isEmpty() 744} 745 746func (c Configurable[T]) isEmpty() bool { 747 return c.inner.isEmpty() 748} 749 750func (c *singleConfigurable[T]) isEmpty() bool { 751 if c == nil { 752 return true 753 } 754 if len(c.cases) > 1 { 755 return false 756 } 757 if len(c.cases) == 1 && c.cases[0].value != nil { 758 return false 759 } 760 return true 761} 762 763func (c *configurableInner[T]) alwaysHasValue() bool { 764 for curr := c; curr != nil; curr = curr.next { 765 if curr.single.alwaysHasValue() { 766 return true 767 } 768 } 769 return false 770} 771 772func (c *singleConfigurable[T]) alwaysHasValue() bool { 773 if len(c.cases) == 0 { 774 return false 775 } 776 for _, c := range c.cases { 777 if c.value == nil { 778 return false 779 } 780 } 781 return true 782} 783 784func (c Configurable[T]) configuredType() reflect.Type { 785 return reflect.TypeOf((*T)(nil)).Elem() 786} 787 788func copyConfiguredValuePtr[T ConfigurableElements](t *T) *T { 789 if t == nil { 790 return nil 791 } 792 switch t2 := any(*t).(type) { 793 case []string: 794 result := any(slices.Clone(t2)).(T) 795 return &result 796 default: 797 x := *t 798 return &x 799 } 800} 801 802func configuredValuePtrToOptional[T ConfigurableElements](t *T) ConfigurableOptional[T] { 803 if t == nil { 804 return ConfigurableOptional[T]{optional.NewShallowOptional(t)} 805 } 806 switch t2 := any(*t).(type) { 807 case []string: 808 result := any(slices.Clone(t2)).(T) 809 return ConfigurableOptional[T]{optional.NewShallowOptional(&result)} 810 default: 811 return ConfigurableOptional[T]{optional.NewShallowOptional(t)} 812 } 813} 814 815func copyConfiguredValue[T ConfigurableElements](t T) T { 816 switch t2 := any(t).(type) { 817 case []string: 818 return any(slices.Clone(t2)).(T) 819 default: 820 return t 821 } 822} 823 824// PrintfIntoConfigurable replaces %s occurrences in strings in Configurable properties 825// with the provided string value. It's intention is to support soong config value variables 826// on Configurable properties. 827func PrintfIntoConfigurable(c any, value string) error { 828 return c.(configurableReflection).printfInto(value) 829} 830