Skip to content

Commit 484ac4a

Browse files
committed
Add Elvish binding for Go interfaces.
A Go interface can now be implemented by an Elvish map of functions. For example, interface{ Foo(a int) string } can be implemented by an Elvish map [&foo-impl=...]. One way to think of this is that Go interfaces can now behave like TypeScript interfaces in Elvish-land. This is unfortunately not very seamless: Go doesn't support creating new interface implementations dynamically with reflection, so we have to create a "struct of func" implementation for each interface, where each field is a function that implements a method, and convert the Elvish map into that struct (using the existing structmap mechanism). The name of a field in a "struct of func" has to be different from the method it implements, so adopt the convention of an "Impl" suffix. Since we want the Elvish map to not have an "-impl" suffix, also implement the ability for a structmap to override the Elvish field name using a field tag. This is particularly useful for Etk bindings, and this commit contains a simple elvts test for the combobox component, whose gen-list state var is expected to return an interface.
1 parent 49db339 commit 484ac4a

7 files changed

Lines changed: 181 additions & 38 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,5 @@ cover
2828
/_*
2929
/elvish
3030
/branding/*.png
31+
*.stash
32+
stash.*

pkg/etk/comps/combobox_test.elvts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//each:combo-box-fixture
2+
3+
/////////////
4+
# rendering #
5+
/////////////
6+
7+
## empty query and empty list ##
8+
~> setup [&gen-list={|q| put [&len={ put 1 } &get={|i| put foo } &show={|i| styled foo}] 0}]
9+
render
10+
┌────────────────────────────────────────┐
11+
│ │
12+
│ ̅̂ │
13+
│foo │
14+
│########################################│
15+
└────────────────────────────────────────┘

pkg/etk/comps/zstructoffunc.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package comps
2+
3+
import (
4+
"src.elv.sh/pkg/etk"
5+
"src.elv.sh/pkg/ui"
6+
)
7+
8+
// TODO: Generate these automatically.
9+
10+
type listItemsStructOfFunc struct {
11+
LenImpl func() int `elvish:"len"`
12+
GetImpl func(i int) any `elvish:"get"`
13+
ShowImpl func(i int) ui.Text `elvish:"show"`
14+
}
15+
16+
var _ ListItems = listItemsStructOfFunc{}
17+
var _ = etk.RegisterStructOfFuncForInterface[listItemsStructOfFunc, ListItems]()
18+
19+
func (sof listItemsStructOfFunc) Len() int { return sof.LenImpl() }
20+
func (sof listItemsStructOfFunc) Get(i int) any { return sof.GetImpl(i) }
21+
func (sof listItemsStructOfFunc) Show(i int) ui.Text { return sof.ShowImpl(i) }
22+
23+
type styleLinerStructOfFunc struct {
24+
StyleLineImpl func(i int) ui.Styling
25+
}
26+
27+
var _ StyleLiner = styleLinerStructOfFunc{}
28+
var _ = etk.RegisterStructOfFuncForInterface[styleLinerStructOfFunc, StyleLiner]()
29+
30+
func (sof styleLinerStructOfFunc) StyleLine(i int) ui.Styling { return sof.StyleLineImpl(i) }

pkg/etk/etk.go

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@ package etk
33

44
import (
55
"fmt"
6-
"reflect"
76
"slices"
87
"strings"
98
"sync"
109

1110
"src.elv.sh/pkg/cli/term"
1211
"src.elv.sh/pkg/eval"
1312
"src.elv.sh/pkg/eval/vals"
14-
"src.elv.sh/pkg/must"
1513
"src.elv.sh/pkg/ui"
1614
)
1715

@@ -298,38 +296,6 @@ func (sv StateVar[T]) Swap(f func(T) T) {
298296
sv.set(f(val))
299297
}
300298

301-
// A variant of vals.ScanToGo, with additional support for adapting an Elvish
302-
// function to a Go function.
303-
func ScanToGo[T any](val any, fm *eval.Frame) (T, error) {
304-
var dst T
305-
err := vals.ScanToGo(val, &dst)
306-
if err == nil {
307-
return dst, nil
308-
}
309-
dstType := reflect.TypeFor[T]()
310-
if fn, ok := val.(eval.Callable); ok && dstType.Kind() == reflect.Func {
311-
// Adapt an Elvish function to a Go function
312-
return reflect.MakeFunc(dstType, func(args []reflect.Value) []reflect.Value {
313-
// TODO: Handle errors properly
314-
// TODO: Add intermediate "internal" entry to the traceback
315-
outs := must.OK1(fm.CaptureOutput(func(fm *eval.Frame) error {
316-
return fn.Call(fm, each(args, reflect.Value.Interface), eval.NoOpts)
317-
}))
318-
goOuts := make([]reflect.Value, dstType.NumOut())
319-
if len(outs) != len(goOuts) {
320-
panic("wrong number of outputs")
321-
}
322-
for i, out := range outs {
323-
goOutPtr := reflect.New(dstType.Out(i))
324-
must.OK(vals.ScanToGo(out, goOutPtr.Interface()))
325-
goOuts[i] = reflect.Indirect(goOutPtr)
326-
}
327-
return goOuts
328-
}).Interface().(T), nil
329-
}
330-
return zero[T](), err
331-
}
332-
333299
func (sv StateVar[T]) getAny() any {
334300
sv.mutex.RLock()
335301
defer sv.mutex.RUnlock()

pkg/etk/scan_to_go.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package etk
2+
3+
import (
4+
"reflect"
5+
6+
"src.elv.sh/pkg/eval"
7+
"src.elv.sh/pkg/eval/vals"
8+
"src.elv.sh/pkg/must"
9+
)
10+
11+
// A slightly nicer wrapper of scanToGo.
12+
func ScanToGo[T any](val any, fm *eval.Frame) (T, error) {
13+
var dst T
14+
err := scanToGo(val, &dst, fm)
15+
if err == nil {
16+
return dst, nil
17+
}
18+
return zero[T](), err
19+
}
20+
21+
// A variant of vals.ScanToGo,
22+
// with additional support for adapting an Elvish function to a Go function,
23+
// or an Elvish map to a Go interface.
24+
func scanToGo(val, ptr any, fm *eval.Frame) error {
25+
err := vals.ScanToGo(val, ptr)
26+
27+
dst := reflect.ValueOf(ptr).Elem()
28+
dstType := reflect.TypeOf(ptr).Elem()
29+
if fn, ok := val.(eval.Callable); ok && dstType.Kind() == reflect.Func {
30+
// Adapt an Elvish function to a Go function
31+
dst.Set(reflect.MakeFunc(dstType, func(args []reflect.Value) []reflect.Value {
32+
// TODO: Handle errors properly
33+
// TODO: Add intermediate "internal" entry to the traceback
34+
outs := must.OK1(fm.CaptureOutput(func(fm *eval.Frame) error {
35+
return fn.Call(fm, each(args, reflect.Value.Interface), eval.NoOpts)
36+
}))
37+
goOuts := make([]reflect.Value, dstType.NumOut())
38+
if len(outs) != len(goOuts) {
39+
panic("wrong number of outputs")
40+
}
41+
for i, out := range outs {
42+
goOutPtr := reflect.New(dstType.Out(i))
43+
must.OK(scanToGo(out, goOutPtr.Interface(), fm))
44+
goOuts[i] = reflect.Indirect(goOutPtr)
45+
}
46+
return goOuts
47+
}))
48+
return nil
49+
} else if structType, ok := structOfFuncForInterface[dstType]; ok {
50+
// Scan an Elvish map into a Go interface.
51+
if _, ok := val.(vals.Map); ok {
52+
// TODO: Accept other map-like types too
53+
// Create a zero value of the struct type, and fill all of its
54+
// fields from the map.
55+
structPtr := reflect.New(structType)
56+
err := vals.ScanFieldMapFromMap(
57+
val, structPtr.Interface(),
58+
vals.GetFieldMapKeys(structPtr.Elem().Interface()),
59+
vals.AllowExtraMapKey,
60+
func(src, ptr any, opts vals.ScanOpt) error {
61+
// TODO: Don't ignore opts
62+
return scanToGo(src, ptr, fm)
63+
})
64+
if err == nil {
65+
dst.Set(structPtr.Elem())
66+
return nil
67+
}
68+
}
69+
}
70+
return err
71+
}
72+
73+
// Maps an interface to its "struct of func" implementation.
74+
var structOfFuncForInterface = map[reflect.Type]reflect.Type{}
75+
76+
// Registers a "struct of func implementation" of an interface.
77+
//
78+
// For example, the following interface:
79+
//
80+
// type I interface {
81+
// Foo(a int)
82+
// Bar() int
83+
// }
84+
//
85+
// Can be implemented by the following "struct of func":
86+
//
87+
// type S struct {
88+
// FooImpl func(a int)
89+
// BarImpl func() int
90+
// }
91+
// func (s S) Foo(a int) { s.FooImpl(a) }
92+
// func (s S) Bar() int { return s.BarImpl() }
93+
//
94+
// (Each field has the "Impl" by convention.)
95+
//
96+
// And you would call this function like this to register their relationship:
97+
//
98+
// var _ = RegisterStructOfFuncForInterface[S, I]()
99+
//
100+
// (The function has a useless return value so that it can be called from the top level.)
101+
//
102+
// This registration allows [ScanToGo] to scan an Elvish map into a Go interface,
103+
// like:
104+
//
105+
// [&foo={|a| ... } & bar={ num 1 }]
106+
func RegisterStructOfFuncForInterface[S, I any]() struct{} {
107+
stype := reflect.TypeFor[S]()
108+
itype := reflect.TypeFor[I]()
109+
if stype.Kind() != reflect.Struct {
110+
panic("S must be a struct type")
111+
}
112+
szero := reflect.Zero(stype).Interface()
113+
if !vals.IsFieldMap(szero) {
114+
panic("S must be a field map")
115+
}
116+
if itype.Kind() != reflect.Interface {
117+
panic("I must be an interface type")
118+
}
119+
if !stype.AssignableTo(itype) {
120+
panic("S must implement I")
121+
}
122+
structOfFuncForInterface[itype] = stype
123+
return struct{}{}
124+
}

pkg/eval/vals/conversion.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func ScanToGoOpts(src, ptr any, opt ScanOpt) error {
149149
// Try to scan a field map.
150150
if keys := getFieldMapKeysT(dstType); keys != nil {
151151
if _, ok := src.(Map); ok || IsFieldMap(src) {
152-
return scanFieldMapFromMap(src, ptr, keys, opt)
152+
return ScanFieldMapFromMap(src, ptr, keys, opt, ScanToGoOpts)
153153
}
154154
}
155155
// Try to scan a slice.
@@ -242,7 +242,9 @@ func elvToRune(arg any) (rune, error) {
242242
return r, nil
243243
}
244244

245-
func scanFieldMapFromMap(src any, ptr any, dstKeys FieldMapKeys, opt ScanOpt) error {
245+
// TODO: Un-export this when the custom ScanToGo impl in pkg/etk is integrated.
246+
247+
func ScanFieldMapFromMap(src any, ptr any, dstKeys FieldMapKeys, opt ScanOpt, scan func(src, ptr any, opt ScanOpt) error) error {
246248
makeErr := func(keysDescription string) error {
247249
return errs.BadValue{
248250
// TODO: Add path information in error messages.
@@ -276,7 +278,7 @@ func scanFieldMapFromMap(src any, ptr any, dstKeys FieldMapKeys, opt ScanOpt) er
276278
}
277279
continue
278280
}
279-
err = ScanToGoOpts(srcValue, dst.Field(i).Addr().Interface(), opt)
281+
err = scan(srcValue, dst.Field(i).Addr().Interface(), opt)
280282
if err != nil {
281283
return err
282284
}

pkg/eval/vals/field_map.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ func makeFieldMapKeys(t reflect.Type) FieldMapKeys {
6161
if field.PkgPath != "" || field.Anonymous {
6262
return nil
6363
}
64-
keys[i] = strutil.CamelToDashed(field.Name)
64+
if tag, ok := field.Tag.Lookup("elvish"); ok && tag != "" {
65+
keys[i] = tag
66+
} else {
67+
keys[i] = strutil.CamelToDashed(field.Name)
68+
}
6569
}
6670
return keys
6771
}

0 commit comments

Comments
 (0)