Compare commits

..

11 Commits

36 changed files with 577 additions and 316 deletions

15
ast.go
View File

@ -5,6 +5,7 @@
package expr package expr
import ( import (
"errors"
"strings" "strings"
"git.portale-stac.it/go-pkg/expr/kern" "git.portale-stac.it/go-pkg/expr/kern"
@ -106,13 +107,23 @@ func (ast *ast) Finish() {
} }
func (ast *ast) Eval(ctx kern.ExprContext) (result any, err error) { func (ast *ast) Eval(ctx kern.ExprContext) (result any, err error) {
defer func() {
if r := recover(); r != nil {
if errVal, ok := r.(error); ok {
err = errVal
} else {
err = errors.New("unexpected error while evaluating the expression")
}
}
}()
ast.Finish() ast.Finish()
if ast.root != nil { if ast.root != nil {
// initDefaultVars(ctx) // initDefaultVars(ctx)
if ast.forest != nil { if ast.forest != nil {
for _, root := range ast.forest { for _, tree := range ast.forest {
if result, err = root.Compute(ctx); err == nil { if result, err = tree.Compute(ctx); err == nil {
ctx.UnsafeSetVar(kern.ControlLastResult, result) ctx.UnsafeSetVar(kern.ControlLastResult, result)
} else { } else {
//err = fmt.Errorf("error in expression nr %d: %v", i+1, err) //err = fmt.Errorf("error in expression nr %d: %v", i+1, err)

View File

@ -43,7 +43,7 @@ func isStringFunc(ctx kern.ExprContext, name string, args map[string]any) (resul
} }
func isFractionFunc(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) { func isFractionFunc(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) {
result = kern.IsFract(args[kern.ParamValue]) result = kern.IsFraction(args[kern.ParamValue])
return return
} }

View File

@ -34,7 +34,7 @@ func doImport(ctx kern.ExprContext, name string, dirList []string, it kern.Itera
var sourceFilepath string var sourceFilepath string
for v, err = it.Next(); err == nil; v, err = it.Next() { for v, err = it.Next(); err == nil; v, err = it.Next() {
if err = checkStringParamExpected(name, v, it.Index()); err != nil { if err = checkStringParamExpected(name, v, int(it.Index())); err != nil {
break break
} }
if sourceFilepath, err = makeFilepath(v.(string), dirList); err != nil { if sourceFilepath, err = makeFilepath(v.(string), dirList); err != nil {

View File

@ -41,7 +41,7 @@ func doAdd(ctx kern.ExprContext, name string, it kern.Iterator, count, level int
return return
} }
} }
} else if err = checkNumberParamExpected(name, v, count, level, it.Index()); err != nil { } else if err = checkNumberParamExpected(name, v, count, level, int(it.Index())); err != nil {
break break
} }
count++ count++
@ -116,7 +116,7 @@ func doMul(ctx kern.ExprContext, name string, it kern.Iterator, count, level int
} }
} }
} else { } else {
if err = checkNumberParamExpected(name, v, count, level, it.Index()); err != nil { if err = checkNumberParamExpected(name, v, count, level, int(it.Index())); err != nil {
break break
} }
} }

View File

@ -17,8 +17,8 @@ const fileReadTextIteratorType = "fileReadTextIterator"
type fileReadTextIterator struct { type fileReadTextIterator struct {
osReader *osReader osReader *osReader
index int index int64
count int count int64
line string line string
autoClose bool autoClose bool
} }
@ -38,7 +38,7 @@ func (it *fileReadTextIterator) String() string {
return fmt.Sprintf("$(%s@<nil>)", fileReadTextIteratorType) return fmt.Sprintf("$(%s@<nil>)", fileReadTextIteratorType)
} }
func (it *fileReadTextIterator) Count() int { func (it *fileReadTextIterator) Count() int64 {
return it.count return it.count
} }
@ -62,7 +62,7 @@ func (it *fileReadTextIterator) Current() (item any, err error) {
return return
} }
func (it *fileReadTextIterator) Index() int { func (it *fileReadTextIterator) Index() int64 {
return it.index return it.index
} }

View File

@ -16,8 +16,8 @@ type dataCursor struct {
ctx kern.ExprContext ctx kern.ExprContext
initState bool // true if no item has produced yet (this replace di initial Next() call in the contructor) initState bool // true if no item has produced yet (this replace di initial Next() call in the contructor)
// cursorValid bool // true if resource is nil or if clean has not yet been called // cursorValid bool // true if resource is nil or if clean has not yet been called
index int index int64
count int count int64
current any current any
lastErr error lastErr error
resource any resource any
@ -298,10 +298,10 @@ func (dc *dataCursor) Next() (current any, err error) { // must return io.EOF af
// return // return
// } // }
func (dc *dataCursor) Index() int { func (dc *dataCursor) Index() int64 {
return dc.index - 1 return dc.index - 1
} }
func (dc *dataCursor) Count() int { func (dc *dataCursor) Count() int64 {
return dc.count return dc.count
} }

View File

@ -23,8 +23,8 @@ const (
type DictIterator struct { type DictIterator struct {
a *kern.DictType a *kern.DictType
count int count int64
index int index int64
keys []any keys []any
iterMode dictIterMode iterMode dictIterMode
} }
@ -127,9 +127,9 @@ func NewMapIterator(m map[any]any) (it *DictIterator) {
} }
func (it *DictIterator) String() string { func (it *DictIterator) String() string {
var l = 0 var l = int64(0)
if it.a != nil { if it.a != nil {
l = len(*it.a) l = int64(len(*it.a))
} }
return fmt.Sprintf("$({#%d})", l) return fmt.Sprintf("$({#%d})", l)
} }
@ -159,13 +159,13 @@ func (it *DictIterator) CallOperation(name string, args map[string]any) (v any,
case kern.CountName: case kern.CountName:
v = it.count v = it.count
case kern.KeyName: case kern.KeyName:
if it.index >= 0 && it.index < len(it.keys) { if it.index >= 0 && it.index < int64(len(it.keys)) {
v = it.keys[it.index] v = it.keys[it.index]
} else { } else {
err = io.EOF err = io.EOF
} }
case kern.ValueName: case kern.ValueName:
if it.index >= 0 && it.index < len(it.keys) { if it.index >= 0 && it.index < int64(len(it.keys)) {
a := *(it.a) a := *(it.a)
v = a[it.keys[it.index]] v = a[it.keys[it.index]]
} else { } else {
@ -178,7 +178,7 @@ func (it *DictIterator) CallOperation(name string, args map[string]any) (v any,
} }
func (it *DictIterator) Current() (item any, err error) { func (it *DictIterator) Current() (item any, err error) {
if it.index >= 0 && it.index < len(it.keys) { if it.index >= 0 && it.index < int64(len(it.keys)) {
switch it.iterMode { switch it.iterMode {
case dictIterModeKeys: case dictIterModeKeys:
item = it.keys[it.index] item = it.keys[it.index]
@ -204,11 +204,11 @@ func (it *DictIterator) Next() (item any, err error) {
return return
} }
func (it *DictIterator) Index() int { func (it *DictIterator) Index() int64 {
return it.index return it.index
} }
func (it *DictIterator) Count() int { func (it *DictIterator) Count() int64 {
return it.count return it.count
} }

View File

@ -11,6 +11,7 @@ import (
"strings" "strings"
"git.portale-stac.it/go-pkg/expr/kern" "git.portale-stac.it/go-pkg/expr/kern"
"git.portale-stac.it/go-pkg/expr/util"
) )
func EvalString(ctx kern.ExprContext, source string) (result any, err error) { func EvalString(ctx kern.ExprContext, source string) (result any, err error) {
@ -38,7 +39,7 @@ func EvalStringA(source string, args ...Arg) (result any, err error) {
func EvalStringV(source string, args []Arg) (result any, err error) { func EvalStringV(source string, args []Arg) (result any, err error) {
ctx := NewSimpleStoreWithoutGlobalContext() ctx := NewSimpleStoreWithoutGlobalContext()
for _, arg := range args { for _, arg := range args {
if kern.IsFunc(arg.Value) { if util.IsFunc(arg.Value) {
if f, ok := arg.Value.(kern.FuncTemplate); ok { if f, ok := arg.Value.(kern.FuncTemplate); ok {
functor := kern.NewGolangFunctor(f) functor := kern.NewGolangFunctor(f)
// ctx.RegisterFunc(arg.Name, functor, 0, -1) // ctx.RegisterFunc(arg.Name, functor, 0, -1)

View File

@ -13,6 +13,7 @@ import (
"strings" "strings"
"git.portale-stac.it/go-pkg/expr/kern" "git.portale-stac.it/go-pkg/expr/kern"
"git.portale-stac.it/go-pkg/expr/util"
) )
const ( const (
@ -85,7 +86,7 @@ func searchAmongPath(filename string, dirList []string) (filePath string) {
} }
for _, dir := range dirList { for _, dir := range dirList {
if dir, err = kern.ExpandPath(dir); err != nil { if dir, err = util.ExpandPath(dir); err != nil {
continue continue
} }
if fullPath := path.Join(dir, filename); isFile(fullPath) { if fullPath := path.Join(dir, filename); isFile(fullPath) {
@ -108,7 +109,7 @@ func isPathRelative(filePath string) bool {
} }
func makeFilepath(filename string, dirList []string) (filePath string, err error) { func makeFilepath(filename string, dirList []string) (filePath string, err error) {
if filename, err = kern.ExpandPath(filename); err != nil { if filename, err = util.ExpandPath(filename); err != nil {
return return
} }

137
int-iterator.go Normal file
View File

@ -0,0 +1,137 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// int-iterator.go
package expr
import (
"fmt"
"io"
"slices"
"git.portale-stac.it/go-pkg/expr/kern"
)
type IntIterator struct {
count int64
index int64
start int64
stop int64
step int64
}
func NewIntIterator(args []any) (it *IntIterator, err error) {
var argc int = 0
if args != nil {
argc = len(args)
}
it = &IntIterator{count: 0, index: -1, start: 0, stop: 0, step: 1}
if argc >= 1 {
if it.stop, err = kern.ToGoInt64(args[0], "start index"); err != nil {
return
}
if argc >= 2 {
it.start = it.stop
if it.stop, err = kern.ToGoInt64(args[1], "stop index"); err != nil {
return
}
if argc >= 3 {
if it.step, err = kern.ToGoInt64(args[2], "step"); err != nil {
return
}
} else if it.start > it.stop {
it.step = -1
}
}
}
if it.step == 0 {
err = fmt.Errorf("step cannot be zero")
return
}
if it.start < it.stop && it.step < 0 {
err = fmt.Errorf("step cannot be negative when start < stop")
return
}
if it.start > it.stop && it.step > 0 {
err = fmt.Errorf("step cannot be positive when start > stop")
return
}
it.Reset()
return
}
func (it *IntIterator) String() string {
return fmt.Sprintf("$(%d..%d..%d)", it.start, it.stop, it.step)
}
func (it *IntIterator) TypeName() string {
return "IntIterator"
}
func (it *IntIterator) HasOperation(name string) bool {
yes := slices.Contains([]string{kern.NextName, kern.ResetName, kern.IndexName, kern.CountName, kern.CurrentName}, name)
return yes
}
func (it *IntIterator) CallOperation(name string, args map[string]any) (v any, err error) {
switch name {
case kern.NextName:
v, err = it.Next()
case kern.ResetName:
err = it.Reset()
case kern.CleanName:
err = it.Clean()
case kern.IndexName:
v = int64(it.Index())
case kern.CurrentName:
v, err = it.Current()
case kern.CountName:
v = it.count
default:
err = kern.ErrNoOperation(name)
}
return
}
func (it *IntIterator) Current() (item any, err error) {
if it.start <= it.stop {
if it.index >= it.start && it.index < it.stop {
item = it.index
} else {
err = io.EOF
}
} else {
if it.index > it.stop && it.index <= it.start {
item = it.index
} else {
err = io.EOF
}
}
return
}
func (it *IntIterator) Next() (item any, err error) {
it.index += it.step
if item, err = it.Current(); err != io.EOF {
it.count++
}
return
}
func (it *IntIterator) Index() int64 {
return it.index
}
func (it *IntIterator) Count() int64 {
return it.count
}
func (it *IntIterator) Reset() error {
it.index = it.start - it.step
it.count = 0
return nil
}
func (it *IntIterator) Clean() error {
return nil
}

27
kern/bool.go Normal file
View File

@ -0,0 +1,27 @@
// Copyright (c) 2024-2026 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// string.go
package kern
func IsBool(v any) (ok bool) {
_, ok = v.(bool)
return ok
}
func ToBool(v any) (b bool, ok bool) {
ok = true
switch x := v.(type) {
case string:
b = len(x) > 0
case float64:
b = x != 0.0
case int64:
b = x != 0
case bool:
b = x
default:
ok = false
}
return
}

View File

@ -12,6 +12,11 @@ import (
type DictType map[any]any type DictType map[any]any
func IsDict(v any) (ok bool) {
_, ok = v.(*DictType)
return ok
}
func MakeDict() (dict *DictType) { func MakeDict() (dict *DictType) {
d := make(DictType) d := make(DictType)
dict = &d dict = &d
@ -138,6 +143,16 @@ func (dict *DictType) HasKey(target any) (ok bool) {
return return
} }
func (dict *DictType) SetItem(key any, value any) (err error) {
(*dict)[key] = value
return
}
func (dict *DictType) GetItem(key any) (value any, err error) {
value = (*dict)[key]
return
}
func (dict *DictType) Clone() (c *DictType) { func (dict *DictType) Clone() (c *DictType) {
c = newDict(nil) c = newDict(nil)
for k, v := range *dict { for k, v := range *dict {
@ -154,11 +169,6 @@ func (dict *DictType) Merge(second *DictType) {
} }
} }
func (dict *DictType) SetItem(key any, value any) (err error) {
(*dict)[key] = value
return
}
//////////////// ////////////////
type DictFormat interface { type DictFormat interface {

23
kern/float.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright (c) 2024-2026 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// float.go
package kern
func IsFloat(v any) (ok bool) {
_, ok = v.(float64)
return ok
}
func AnyFloat(v any) (float float64, ok bool) {
ok = true
switch floatval := v.(type) {
case float32:
float = float64(floatval)
case float64:
float = floatval
default:
ok = false
}
return
}

View File

@ -367,3 +367,15 @@ func IsFraction(v any) (ok bool) {
_, ok = v.(*FractionType) _, ok = v.(*FractionType)
return ok return ok
} }
// func IsFract(v any) (ok bool) {
// _, ok = v.(*FractionType)
// return ok
// }
func IsRational(v any) (ok bool) {
if _, ok = v.(*FractionType); !ok {
_, ok = v.(int64)
}
return ok
}

View File

@ -14,6 +14,11 @@ type FuncTemplate func(ctx ExprContext, name string, args map[string]any) (resul
type DeepFuncTemplate func(a, b any) (eq bool, err error) type DeepFuncTemplate func(a, b any) (eq bool, err error)
func IsFunctor(v any) (ok bool) {
_, ok = v.(Functor)
return
}
// ---- Common functor definition // ---- Common functor definition
type BaseFunctor struct { type BaseFunctor struct {
info ExprFunc info ExprFunc

View File

@ -30,8 +30,8 @@ type Iterator interface {
fmt.Stringer fmt.Stringer
Next() (item any, err error) // must return io.EOF after the last item Next() (item any, err error) // must return io.EOF after the last item
Current() (item any, err error) Current() (item any, err error)
Index() int Index() int64
Count() int Count() int64
HasOperation(name string) bool HasOperation(name string) bool
CallOperation(name string, args map[string]any) (value any, err error) CallOperation(name string, args map[string]any) (value any, err error)
} }
@ -45,3 +45,8 @@ type ExtIterator interface {
func ErrNoOperation(name string) error { func ErrNoOperation(name string) error {
return fmt.Errorf("no %s() function defined in the data-source", name) return fmt.Errorf("no %s() function defined in the data-source", name)
} }
func IsIterator(v any) (ok bool) {
_, ok = v.(Iterator)
return
}

View File

@ -12,6 +12,11 @@ import (
type ListType []any type ListType []any
func IsList(v any) (ok bool) {
_, ok = v.(*ListType)
return ok
}
func NewListA(listAny ...any) (list *ListType) { func NewListA(listAny ...any) (list *ListType) {
if listAny == nil { if listAny == nil {
listAny = []any{} listAny = []any{}

88
kern/number.go Normal file
View File

@ -0,0 +1,88 @@
// Copyright (c) 2024-2026 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// number.go
package kern
import (
"fmt"
)
func IsInteger(v any) (ok bool) {
_, ok = v.(int64)
return ok
}
func IsNumber(v any) (ok bool) {
return IsFloat(v) || IsInteger(v)
}
func IsNumOrFract(v any) (ok bool) {
return IsFloat(v) || IsInteger(v) || IsFraction(v)
}
func IsNumberString(v any) (ok bool) {
return IsString(v) || IsNumber(v)
}
func NumAsFloat(v any) (f float64) {
var ok bool
if f, ok = v.(float64); !ok {
if fract, ok := v.(*FractionType); ok {
f = fract.ToFloat()
} else {
i, _ := v.(int64)
f = float64(i)
}
}
return
}
func AnyInteger(v any) (i int64, ok bool) {
ok = true
switch intval := v.(type) {
case int:
i = int64(intval)
case uint8:
i = int64(intval)
case uint16:
i = int64(intval)
case uint64:
i = int64(intval)
case uint32:
i = int64(intval)
case int8:
i = int64(intval)
case int16:
i = int64(intval)
case int32:
i = int64(intval)
case int64:
i = intval
default:
ok = false
}
return
}
func ToGoInt(value any, description string) (i int, err error) {
if valueInt64, ok := value.(int64); ok {
i = int(valueInt64)
} else if valueInt, ok := value.(int); ok {
i = valueInt
} else {
err = fmt.Errorf("%s expected integer, got %s (%v)", description, TypeName(value), value)
}
return
}
func ToGoInt64(value any, description string) (i int64, err error) {
if valueInt64, ok := value.(int64); ok {
i = valueInt64
} else if valueInt, ok := value.(int); ok {
i = int64(valueInt)
} else {
err = fmt.Errorf("%s expected integer, got %s (%v)", description, TypeName(value), value)
}
return
}

23
kern/string.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright (c) 2024-2026 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// string.go
package kern
import (
"fmt"
)
func IsString(v any) (ok bool) {
_, ok = v.(string)
return ok
}
func ToGoString(value any, description string) (s string, err error) {
if s, ok := value.(string); ok {
return s, nil
} else {
err = fmt.Errorf("%s expected string, got %s (%v)", description, TypeName(value), value)
}
return
}

View File

@ -1,232 +0,0 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// utils.go
package kern
import (
"fmt"
"reflect"
)
func IsString(v any) (ok bool) {
_, ok = v.(string)
return ok
}
func IsInteger(v any) (ok bool) {
_, ok = v.(int64)
return ok
}
func IsFloat(v any) (ok bool) {
_, ok = v.(float64)
return ok
}
func IsBool(v any) (ok bool) {
_, ok = v.(bool)
return ok
}
func IsList(v any) (ok bool) {
_, ok = v.(*ListType)
return ok
}
func IsDict(v any) (ok bool) {
_, ok = v.(*DictType)
return ok
}
func IsFract(v any) (ok bool) {
_, ok = v.(*FractionType)
return ok
}
func IsRational(v any) (ok bool) {
if _, ok = v.(*FractionType); !ok {
_, ok = v.(int64)
}
return ok
}
func IsNumber(v any) (ok bool) {
return IsFloat(v) || IsInteger(v)
}
func IsNumOrFract(v any) (ok bool) {
return IsFloat(v) || IsInteger(v) || IsFraction(v)
}
func IsNumberString(v any) (ok bool) {
return IsString(v) || IsNumber(v)
}
func IsFunctor(v any) (ok bool) {
_, ok = v.(Functor)
return
}
func IsIterator(v any) (ok bool) {
_, ok = v.(Iterator)
return
}
func NumAsFloat(v any) (f float64) {
var ok bool
if f, ok = v.(float64); !ok {
if fract, ok := v.(*FractionType); ok {
f = fract.ToFloat()
} else {
i, _ := v.(int64)
f = float64(i)
}
}
return
}
func ToBool(v any) (b bool, ok bool) {
ok = true
switch x := v.(type) {
case string:
b = len(x) > 0
case float64:
b = x != 0.0
case int64:
b = x != 0
case bool:
b = x
default:
ok = false
}
return
}
func IsFunc(v any) bool {
return reflect.TypeOf(v).Kind() == reflect.Func
}
func AnyInteger(v any) (i int64, ok bool) {
ok = true
switch intval := v.(type) {
case int:
i = int64(intval)
case uint8:
i = int64(intval)
case uint16:
i = int64(intval)
case uint64:
i = int64(intval)
case uint32:
i = int64(intval)
case int8:
i = int64(intval)
case int16:
i = int64(intval)
case int32:
i = int64(intval)
case int64:
i = intval
default:
ok = false
}
return
}
func FromGenericAny(v any) (exprAny any, ok bool) {
if v != nil {
if exprAny, ok = v.(bool); ok {
return
}
if exprAny, ok = v.(string); ok {
return
}
if exprAny, ok = AnyInteger(v); ok {
return
}
if exprAny, ok = AnyFloat(v); ok {
return
}
if exprAny, ok = v.(*DictType); ok {
return
}
if exprAny, ok = v.(*ListType); ok {
return
}
}
return
}
func AnyFloat(v any) (float float64, ok bool) {
ok = true
switch floatval := v.(type) {
case float32:
float = float64(floatval)
case float64:
float = floatval
default:
ok = false
}
return
}
func CopyMap[K comparable, V any](dest, source map[K]V) map[K]V {
for k, v := range source {
dest[k] = v
}
return dest
}
// func CloneMap[K comparable, V any](source map[K]V) map[K]V {
// dest := make(map[K]V, len(source))
// return CopyMap(dest, source)
// }
func CopyFilteredMap[K comparable, V any](dest, source map[K]V, filter func(key K) (accept bool)) map[K]V {
// fmt.Printf("--- Clone with filter %p\n", filter)
if filter == nil {
return CopyMap(dest, source)
} else {
for k, v := range source {
if filter(k) {
// fmt.Printf("\tClone var %q\n", k)
dest[k] = v
}
}
}
return dest
}
func CloneFilteredMap[K comparable, V any](source map[K]V, filter func(key K) (accept bool)) map[K]V {
dest := make(map[K]V, len(source))
return CopyFilteredMap(dest, source, filter)
}
func ToGoInt(value any, description string) (i int, err error) {
if valueInt64, ok := value.(int64); ok {
i = int(valueInt64)
} else if valueInt, ok := value.(int); ok {
i = valueInt
} else {
err = fmt.Errorf("%s expected integer, got %s (%v)", description, TypeName(value), value)
}
return
}
func ToGoString(value any, description string) (s string, err error) {
if s, ok := value.(string); ok {
return s, nil
} else {
err = fmt.Errorf("%s expected string, got %s (%v)", description, TypeName(value), value)
}
return
}
func ForAll[T, V any](ts []T, fn func(T) V) []V {
result := make([]V, len(ts))
for i, t := range ts {
result[i] = fn(t)
}
return result
}

View File

@ -14,36 +14,36 @@ import (
type ListIterator struct { type ListIterator struct {
a *kern.ListType a *kern.ListType
count int count int64
index int index int64
start int start int64
stop int stop int64
step int step int64
} }
func NewListIterator(list *kern.ListType, args []any) (it *ListIterator) { func NewListIterator(list *kern.ListType, args []any) (it *ListIterator) {
var argc int = 0 var argc int = 0
listLen := len(([]any)(*list)) listLen := int64(len(([]any)(*list)))
if args != nil { if args != nil {
argc = len(args) argc = len(args)
} }
it = &ListIterator{a: list, count: 0, index: -1, start: 0, stop: listLen - 1, step: 1} it = &ListIterator{a: list, count: 0, index: -1, start: 0, stop: listLen - 1, step: 1}
if argc >= 1 { if argc >= 1 {
if i, err := kern.ToGoInt(args[0], "start index"); err == nil { if i, err := kern.ToGoInt64(args[0], "start index"); err == nil {
if i < 0 { if i < 0 {
i = listLen + i i = listLen + i
} }
it.start = i it.start = i
} }
if argc >= 2 { if argc >= 2 {
if i, err := kern.ToGoInt(args[1], "stop index"); err == nil { if i, err := kern.ToGoInt64(args[1], "stop index"); err == nil {
if i < 0 { if i < 0 {
i = listLen + i i = listLen + i
} }
it.stop = i it.stop = i
} }
if argc >= 3 { if argc >= 3 {
if i, err := kern.ToGoInt(args[2], "step"); err == nil { if i, err := kern.ToGoInt64(args[2], "step"); err == nil {
if i < 0 { if i < 0 {
i = -i i = -i
} }
@ -61,14 +61,14 @@ func NewListIterator(list *kern.ListType, args []any) (it *ListIterator) {
} }
func NewArrayIterator(array []any) (it *ListIterator) { func NewArrayIterator(array []any) (it *ListIterator) {
it = &ListIterator{a: (*kern.ListType)(&array), count: 0, index: -1, start: 0, stop: len(array) - 1, step: 1} it = &ListIterator{a: (*kern.ListType)(&array), count: 0, index: -1, start: 0, stop: int64(len(array)) - 1, step: 1}
return return
} }
func (it *ListIterator) String() string { func (it *ListIterator) String() string {
var l = 0 var l = int64(0)
if it.a != nil { if it.a != nil {
l = len(*it.a) l = int64(len(*it.a))
} }
return fmt.Sprintf("$([#%d])", l) return fmt.Sprintf("$([#%d])", l)
} }
@ -106,13 +106,13 @@ func (it *ListIterator) CallOperation(name string, args map[string]any) (v any,
func (it *ListIterator) Current() (item any, err error) { func (it *ListIterator) Current() (item any, err error) {
a := *(it.a) a := *(it.a)
if it.start <= it.stop { if it.start <= it.stop {
if it.stop < len(a) && it.index >= it.start && it.index <= it.stop { if it.stop < int64(len(a)) && it.index >= it.start && it.index <= it.stop {
item = a[it.index] item = a[it.index]
} else { } else {
err = io.EOF err = io.EOF
} }
} else { } else {
if it.start < len(a) && it.index >= it.stop && it.index <= it.start { if it.start < int64(len(a)) && it.index >= it.stop && it.index <= it.start {
item = a[it.index] item = a[it.index]
} else { } else {
err = io.EOF err = io.EOF
@ -130,11 +130,11 @@ func (it *ListIterator) Next() (item any, err error) {
return return
} }
func (it *ListIterator) Index() int { func (it *ListIterator) Index() int64 {
return it.index return it.index
} }
func (it *ListIterator) Count() int { func (it *ListIterator) Count() int64 {
return it.count return it.count
} }

View File

@ -136,6 +136,11 @@ func evalIterator(ctx kern.ExprContext, opTerm *term) (v any, err error) {
if args, err = evalSibling(ctx, opTerm.children, nil); err == nil { if args, err = evalSibling(ctx, opTerm.children, nil); err == nil {
v = NewListIterator(list, args) v = NewListIterator(list, args)
} }
} else if intVal, ok := firstChildValue.(int64); ok {
var args []any
if args, err = evalSibling(ctx, opTerm.children, intVal); err == nil {
v, err = NewIntIterator(args)
}
} else { } else {
var list []any var list []any
if list, err = evalSibling(ctx, opTerm.children, firstChildValue); err == nil { if list, err = evalSibling(ctx, opTerm.children, firstChildValue); err == nil {

View File

@ -6,6 +6,7 @@ package expr
import ( import (
"git.portale-stac.it/go-pkg/expr/kern" "git.portale-stac.it/go-pkg/expr/kern"
"git.portale-stac.it/go-pkg/expr/util"
) )
//-------- assign term //-------- assign term
@ -84,7 +85,7 @@ func evalAssign(ctx kern.ExprContext, opTerm *term) (v any, err error) {
if info := functor.GetFunc(); info != nil { if info := functor.GetFunc(); info != nil {
ctx.RegisterFunc(leftTerm.Source(), info.Functor(), info.ReturnType(), info.Params()) ctx.RegisterFunc(leftTerm.Source(), info.Functor(), info.ReturnType(), info.Params())
} else if funcDef, ok := functor.(*exprFunctor); ok { } else if funcDef, ok := functor.(*exprFunctor); ok {
paramSpecs := kern.ForAll(funcDef.params, func(p kern.ExprFuncParam) kern.ExprFuncParam { return p }) paramSpecs := util.ForAll(funcDef.params, func(p kern.ExprFuncParam) kern.ExprFuncParam { return p })
ctx.RegisterFunc(leftTerm.Source(), functor, kern.TypeAny, paramSpecs) ctx.RegisterFunc(leftTerm.Source(), functor, kern.TypeAny, paramSpecs)
} else { } else {

View File

@ -44,6 +44,17 @@ func evalDot(ctx kern.ExprContext, opTerm *term) (v any, err error) {
} else { } else {
err = indexTerm.tk.ErrorExpectedGot("identifier") err = indexTerm.tk.ErrorExpectedGot("identifier")
} }
case *kern.DictType:
s := opTerm.children[1].symbol()
if s == SymVariable || s == SymString {
src := opTerm.children[1].Source()
if len(src) > 1 && src[0] == '"' && src[len(src)-1] == '"' {
src = src[1 : len(src)-1]
}
v, err = unboxedValue.GetItem(src)
} else if rightValue, err = opTerm.children[1].Compute(ctx); err == nil {
v, err = unboxedValue.GetItem(rightValue)
}
default: default:
if rightValue, err = opTerm.children[1].Compute(ctx); err == nil { if rightValue, err = opTerm.children[1].Compute(ctx); err == nil {
err = opTerm.errIncompatibleTypes(leftValue, rightValue) err = opTerm.errIncompatibleTypes(leftValue, rightValue)

View File

@ -382,6 +382,15 @@ func (parser *parser) parseItem(scanner *scanner, ctx parserContext, termSymbols
} }
func (parser *parser) Parse(scanner *scanner, termSymbols ...Symbol) (tree *ast, err error) { func (parser *parser) Parse(scanner *scanner, termSymbols ...Symbol) (tree *ast, err error) {
defer func() {
if r := recover(); r != nil {
if errVal, ok := r.(error); ok {
err = errVal
} else {
err = errors.New("unexpected error while parsing the expression")
}
}
}()
termSymbols = append(termSymbols, SymEos) termSymbols = append(termSymbols, SymEos)
return parser.parseGeneral(scanner, allowMultiExpr, termSymbols...) return parser.parseGeneral(scanner, allowMultiExpr, termSymbols...)
} }
@ -455,15 +464,6 @@ func (parser *parser) parseGeneral(scanner *scanner, ctx parserContext, termSymb
//fmt.Println("Token:", tk) //fmt.Println("Token:", tk)
if firstToken { if firstToken {
changePrefix(tk) changePrefix(tk)
// if tk.Sym == SymMinus {
// tk.Sym = SymChangeSign
// } else if tk.Sym == SymPlus {
// tk.Sym = SymUnchangeSign
// } else if tk.IsSymbol(SymStar) {
// tk.SetSymbol(SymDereference)
// } else if tk.IsSymbol(SymExclamation) {
// tk.SetSymbol(SymNot)
// }
firstToken = false firstToken = false
} }
@ -471,12 +471,13 @@ func (parser *parser) parseGeneral(scanner *scanner, ctx parserContext, termSymb
case SymOpenRound: case SymOpenRound:
var subTree *ast var subTree *ast
if subTree, err = parser.parseGeneral(scanner, ctx, SymClosedRound); err == nil { if subTree, err = parser.parseGeneral(scanner, ctx, SymClosedRound); err == nil {
exprTerm := newExprTerm(subTree.root) if subTree.root == nil {
err = tree.addTerm(exprTerm) err = tk.ErrorExpectedGotString("expression", "()")
currentTerm = exprTerm } else {
// subTree.root.priority = priValue exprTerm := newExprTerm(subTree.root)
// err = tree.addTerm(newExprTerm(subTree.root)) err = tree.addTerm(exprTerm)
// currentTerm = subTree.root currentTerm = exprTerm
}
} }
case SymFuncCall: case SymFuncCall:
var funcCallTerm *term var funcCallTerm *term

View File

@ -9,6 +9,7 @@ import (
"slices" "slices"
"git.portale-stac.it/go-pkg/expr/kern" "git.portale-stac.it/go-pkg/expr/kern"
"git.portale-stac.it/go-pkg/expr/util"
// "strings" // "strings"
) )
@ -61,8 +62,8 @@ func (ctx *SimpleStore) GetGlobal() (globalCtx kern.ExprContext) {
func (ctx *SimpleStore) Clone() kern.ExprContext { func (ctx *SimpleStore) Clone() kern.ExprContext {
clone := &SimpleStore{ clone := &SimpleStore{
global: ctx.global, global: ctx.global,
varStore: kern.CloneFilteredMap(ctx.varStore, filterRefName), varStore: util.CloneFilteredMap(ctx.varStore, filterRefName),
funcStore: kern.CloneFilteredMap(ctx.funcStore, filterRefName), funcStore: util.CloneFilteredMap(ctx.funcStore, filterRefName),
} }
return clone return clone
} }
@ -138,7 +139,7 @@ func (ctx *SimpleStore) UnsafeSetVar(varName string, value any) {
func (ctx *SimpleStore) SetVar(varName string, value any) { func (ctx *SimpleStore) SetVar(varName string, value any) {
// fmt.Printf("[%p] SetVar(%v, %v)\n", ctx, varName, value) // fmt.Printf("[%p] SetVar(%v, %v)\n", ctx, varName, value)
if allowedValue, ok := kern.FromGenericAny(value); ok { if allowedValue, ok := util.FromGenericAny(value); ok {
ctx.varStore[varName] = allowedValue ctx.varStore[varName] = allowedValue
} else { } else {
panic(fmt.Errorf("unsupported type %T of value %v", value, value)) panic(fmt.Errorf("unsupported type %T of value %v", value, value))

View File

@ -14,7 +14,7 @@ func TestFuncRun(t *testing.T) {
inputs := []inputType{ inputs := []inputType{
/* 1 */ {`builtin "iterator"; it=$(1,2,3); run(it)`, nil, nil}, /* 1 */ {`builtin "iterator"; it=$(1,2,3); run(it)`, nil, nil},
/* 2 */ {`builtin "iterator"; run($(1,2,3), func(index,item){item+10})`, nil, nil}, /* 2 */ {`builtin "iterator"; run($(1,2,3), func(index,item){item+10})`, nil, nil},
/* 3 */ {`builtin "iterator"; run($(1,2,3), func(index,item){status=status+item; true}, {"status":0})`, int64(6), nil}, /* 3 */ {`builtin "iterator"; run($(4), func(index,item){status=status+item; true}, {"status":0})`, int64(6), nil},
/* 4 */ {`builtin ["iterator", "fmt"]; run($(1,2,3), func(index,item){println(item+10)})`, nil, nil}, /* 4 */ {`builtin ["iterator", "fmt"]; run($(1,2,3), func(index,item){println(item+10)})`, nil, nil},
/* 5 */ {`builtin "iterator"; run(nil)`, nil, `paramter "iterator" must be an iterator, passed <nil> [nil]`}, /* 5 */ {`builtin "iterator"; run(nil)`, nil, `paramter "iterator" must be an iterator, passed <nil> [nil]`},
/* 6 */ {`builtin "iterator"; run($(1,2,3), nil)`, nil, nil}, /* 6 */ {`builtin "iterator"; run($(1,2,3), nil)`, nil, nil},
@ -26,6 +26,6 @@ func TestFuncRun(t *testing.T) {
//t.Setenv("EXPR_PATH", ".") //t.Setenv("EXPR_PATH", ".")
runTestSuiteSpec(t, section, inputs, 10) // runTestSuiteSpec(t, section, inputs, 3)
// runTestSuite(t, section, inputs) runTestSuite(t, section, inputs)
} }

View File

@ -107,13 +107,13 @@ func doTest(t *testing.T, ctx kern.ExprContext, section string, input *inputType
} }
if !eq /*gotResult != input.wantResult*/ { if !eq /*gotResult != input.wantResult*/ {
t.Errorf("%d: `%s` -> result = %v [%s], want = %v [%s]", count, input.source, gotResult, kern.TypeName(gotResult), input.wantResult, kern.TypeName(input.wantResult)) t.Errorf(">>>%s/%d: `%s` -> result = %v [%s], want = %v [%s]", section, count, input.source, gotResult, kern.TypeName(gotResult), input.wantResult, kern.TypeName(input.wantResult))
good = false good = false
} }
if gotErr != wantErr { if gotErr != wantErr {
if wantErr == nil || gotErr == nil || (gotErr.Error() != wantErr.Error()) { if wantErr == nil || gotErr == nil || (gotErr.Error() != wantErr.Error()) {
t.Errorf("%d: %s -> got-err = <%v>, expected-err = <%v>", count, input.source, gotErr, wantErr) t.Errorf(">>>%s/%d: %s -> got-err = <%v>, expected-err = <%v>", section, count, input.source, gotErr, wantErr)
good = false good = false
} }
} }

View File

@ -39,6 +39,9 @@ func TestDictParser(t *testing.T) {
//"b":2, //"b":2,
"c":3 "c":3
}`, map[any]any{"a": 1, "c": 3}, nil}, }`, map[any]any{"a": 1, "c": 3}, nil},
/* 13 */ {`D={"a":1, "b":2}; D."a"`, int64(1), nil},
/* 14 */ {`D={"a":1, "b":2}; D.a`, int64(1), nil},
/* 15 */ {`D={1:"a", 2:"b", 3:"c"}; D.(1+2)`, "c", nil},
} }
succeeded := 0 succeeded := 0

View File

@ -38,8 +38,13 @@ func TestIteratorParser(t *testing.T) {
/* 23 */ {`builtin "os.file"; fileReadIterator("test-file.txt") map ${_index}`, kern.NewList([]any{int64(0), int64(1)}), nil}, /* 23 */ {`builtin "os.file"; fileReadIterator("test-file.txt") map ${_index}`, kern.NewList([]any{int64(0), int64(1)}), nil},
/* 24 */ {`builtin "os.file"; #(fileReadIterator("test-file.txt") filter (#${_} == 2))`, int64(0), nil}, /* 24 */ {`builtin "os.file"; #(fileReadIterator("test-file.txt") filter (#${_} == 2))`, int64(0), nil},
/* 25 */ {`builtin "os.file"; #(fileReadIterator("test-file.txt") filter (#${_} == 3))`, int64(2), nil}, /* 25 */ {`builtin "os.file"; #(fileReadIterator("test-file.txt") filter (#${_} == 3))`, int64(2), nil},
/* 26 */ {`#($(10) map ${_})`, int64(10), nil},
/* 27 */ {`#($(10,0) map ${_})`, int64(10), nil},
/* 28 */ {`$(10) digest ${_}`, int64(9), nil},
/* 29 */ {`$(10,0) digest ${_}`, int64(1), nil},
/* 30 */ {`$(10,0,-2) digest ${_}`, int64(2), nil},
} }
// runTestSuiteSpec(t, section, inputs, 23) // runTestSuiteSpec(t, section, inputs, 10)
runTestSuite(t, section, inputs) runTestSuite(t, section, inputs)
} }

View File

@ -147,3 +147,15 @@ func TestGeneralParser(t *testing.T) {
// runTestSuiteSpec(t, section, inputs, 114) // runTestSuiteSpec(t, section, inputs, 114)
runTestSuite(t, section, inputs) runTestSuite(t, section, inputs)
} }
func TestSpecialParser(t *testing.T) {
section := "Parser"
inputs := []inputType{
/* 1 */ {`()`, nil, "[1:2] expected expression, got `()`"},
}
// t.Setenv("EXPR_PATH", ".")
// runTestSuiteSpec(t, section, inputs, 114)
runTestSuite(t, section, inputs)
}

View File

@ -12,7 +12,7 @@ import (
"path" "path"
"testing" "testing"
"git.portale-stac.it/go-pkg/expr/kern" "git.portale-stac.it/go-pkg/expr/util"
) )
func TestExpandPathRootOk(t *testing.T) { func TestExpandPathRootOk(t *testing.T) {
@ -21,7 +21,7 @@ func TestExpandPathRootOk(t *testing.T) {
// wantErr := errors.New(`test expected string, got list ([])`) // wantErr := errors.New(`test expected string, got list ([])`)
wantErr := error(nil) wantErr := error(nil)
gotValue, gotErr := kern.ExpandPath(source) gotValue, gotErr := util.ExpandPath(source)
if gotErr != nil && gotErr.Error() != wantErr.Error() { if gotErr != nil && gotErr.Error() != wantErr.Error() {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`, t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
@ -38,7 +38,7 @@ func TestExpandPathRootSubDirOk(t *testing.T) {
// wantErr := errors.New(`test expected string, got list ([])`) // wantErr := errors.New(`test expected string, got list ([])`)
wantErr := error(nil) wantErr := error(nil)
gotValue, gotErr := kern.ExpandPath(source) gotValue, gotErr := util.ExpandPath(source)
if gotErr != nil && gotErr.Error() != wantErr.Error() { if gotErr != nil && gotErr.Error() != wantErr.Error() {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`, t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
@ -56,7 +56,7 @@ func TestExpandPathUser(t *testing.T) {
// wantErr := errors.New(`test expected string, got list ([])`) // wantErr := errors.New(`test expected string, got list ([])`)
wantErr := error(nil) wantErr := error(nil)
gotValue, gotErr := kern.ExpandPath(source) gotValue, gotErr := util.ExpandPath(source)
if gotErr != nil { if gotErr != nil {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`, t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
@ -74,7 +74,7 @@ func TestExpandPathEnv(t *testing.T) {
// wantErr := errors.New(`test expected string, got list ([])`) // wantErr := errors.New(`test expected string, got list ([])`)
wantErr := error(nil) wantErr := error(nil)
gotValue, gotErr := kern.ExpandPath(source) gotValue, gotErr := util.ExpandPath(source)
if gotErr != nil { if gotErr != nil {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`, t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
@ -90,7 +90,7 @@ func TestExpandPathErr(t *testing.T) {
wantValue := "~fake-user/test" wantValue := "~fake-user/test"
wantErr := errors.New(`user: unknown user fake-user`) wantErr := errors.New(`user: unknown user fake-user`)
gotValue, gotErr := kern.ExpandPath(source) gotValue, gotErr := util.ExpandPath(source)
if gotErr != nil && gotErr.Error() != wantErr.Error() { if gotErr != nil && gotErr.Error() != wantErr.Error() {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`, t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
@ -108,7 +108,7 @@ func TestExpandPathUserErr(t *testing.T) {
// wantErr := errors.New(`test expected string, got list ([])`) // wantErr := errors.New(`test expected string, got list ([])`)
wantErr := error(nil) wantErr := error(nil)
gotValue, gotErr := kern.ExpandPath(source) gotValue, gotErr := util.ExpandPath(source)
if gotErr != nil { if gotErr != nil {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`, t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,

View File

@ -10,6 +10,7 @@ import (
"testing" "testing"
"git.portale-stac.it/go-pkg/expr/kern" "git.portale-stac.it/go-pkg/expr/kern"
"git.portale-stac.it/go-pkg/expr/util"
) )
func TestIsString(t *testing.T) { func TestIsString(t *testing.T) {
@ -121,6 +122,32 @@ func TestToIntErr(t *testing.T) {
} }
} }
func TestToInt64Ok(t *testing.T) {
source := int64(64)
wantValue := int64(64)
wantErr := error(nil)
gotValue, gotErr := kern.ToGoInt64(source, "test")
if gotErr != nil || gotValue != wantValue {
t.Errorf("toInt64(%v, \"test\") gotValue=%v, gotErr=%v -> wantValue=%v, wantErr=%v",
source, gotValue, gotErr, wantValue, wantErr)
}
}
func TestToInt64Err(t *testing.T) {
source := uint64(64)
wantValue := int64(0)
wantErr := errors.New(`test expected integer, got uint64 (64)`)
gotValue, gotErr := kern.ToGoInt64(source, "test")
if gotErr.Error() != wantErr.Error() || gotValue != wantValue {
t.Errorf("toInt64(%v, \"test\") gotValue=%v, gotErr=%v -> wantValue=%v, wantErr=%v",
source, gotValue, gotErr, wantValue, wantErr)
}
}
func TestAnyInteger(t *testing.T) { func TestAnyInteger(t *testing.T) {
type inputType struct { type inputType struct {
source any source any
@ -161,7 +188,7 @@ func TestAnyInteger(t *testing.T) {
func TestCopyMap(t *testing.T) { func TestCopyMap(t *testing.T) {
source := map[string]int{"one": 1, "two": 2, "three": 3} source := map[string]int{"one": 1, "two": 2, "three": 3}
dest := make(map[string]int) dest := make(map[string]int)
result := kern.CopyMap(dest, source) result := util.CopyMap(dest, source)
if !reflect.DeepEqual(result, source) { if !reflect.DeepEqual(result, source) {
t.Errorf("utils.CopyMap() failed") t.Errorf("utils.CopyMap() failed")
} }

View File

@ -4,7 +4,7 @@
// All rights reserved. // All rights reserved.
// utils-unix.go // utils-unix.go
package kern package util
import ( import (
"os" "os"

View File

@ -4,7 +4,7 @@
// All rights reserved. // All rights reserved.
// utils-unix.go // utils-unix.go
package kern package util
import ( import (
"os" "os"

79
util/utils.go Normal file
View File

@ -0,0 +1,79 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// utils.go
package util
import (
"reflect"
"git.portale-stac.it/go-pkg/expr/kern"
)
func IsFunc(v any) bool {
return reflect.TypeOf(v).Kind() == reflect.Func
}
func FromGenericAny(v any) (exprAny any, ok bool) {
if v != nil {
if exprAny, ok = v.(bool); ok {
return
}
if exprAny, ok = v.(string); ok {
return
}
if exprAny, ok = kern.AnyInteger(v); ok {
return
}
if exprAny, ok = kern.AnyFloat(v); ok {
return
}
if exprAny, ok = v.(*kern.DictType); ok {
return
}
if exprAny, ok = v.(*kern.ListType); ok {
return
}
}
return
}
func CopyMap[K comparable, V any](dest, source map[K]V) map[K]V {
for k, v := range source {
dest[k] = v
}
return dest
}
// func CloneMap[K comparable, V any](source map[K]V) map[K]V {
// dest := make(map[K]V, len(source))
// return CopyMap(dest, source)
// }
func CopyFilteredMap[K comparable, V any](dest, source map[K]V, filter func(key K) (accept bool)) map[K]V {
// fmt.Printf("--- Clone with filter %p\n", filter)
if filter == nil {
return CopyMap(dest, source)
} else {
for k, v := range source {
if filter(k) {
// fmt.Printf("\tClone var %q\n", k)
dest[k] = v
}
}
}
return dest
}
func CloneFilteredMap[K comparable, V any](source map[K]V, filter func(key K) (accept bool)) map[K]V {
dest := make(map[K]V, len(source))
return CopyFilteredMap(dest, source, filter)
}
func ForAll[T, V any](ts []T, fn func(T) V) []V {
result := make([]V, len(ts))
for i, t := range ts {
result[i] = fn(t)
}
return result
}