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