This commit is contained in:
parent
4ab94410b7
commit
7a2b012833
|
@ -26,6 +26,7 @@ func main() {
|
||||||
ucl.WithModule(builtins.Strs()),
|
ucl.WithModule(builtins.Strs()),
|
||||||
ucl.WithModule(builtins.Lists()),
|
ucl.WithModule(builtins.Lists()),
|
||||||
ucl.WithModule(builtins.Time()),
|
ucl.WithModule(builtins.Time()),
|
||||||
|
ucl.WithModule(builtins.Fns()),
|
||||||
)
|
)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -6,7 +6,7 @@ require (
|
||||||
github.com/alecthomas/participle/v2 v2.1.1
|
github.com/alecthomas/participle/v2 v2.1.1
|
||||||
github.com/chzyer/readline v1.5.1
|
github.com/chzyer/readline v1.5.1
|
||||||
github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f
|
github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.10.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -17,4 +17,5 @@ require (
|
||||||
go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect
|
go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
|
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b // indirect
|
||||||
)
|
)
|
||||||
|
|
3
go.sum
3
go.sum
|
@ -22,6 +22,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw=
|
go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw=
|
||||||
|
@ -33,3 +34,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b h1:Oymcj66pgyJ2CtGk9lPh06P4FOekllE1iPehDwaL0vw=
|
||||||
|
lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
|
||||||
|
|
|
@ -636,6 +636,52 @@ func (mi mappedIter) Next(ctx context.Context) (Object, error) {
|
||||||
return mi.inv.invoke(ctx, mi.args.fork([]Object{v}))
|
return mi.inv.invoke(ctx, mi.args.fork([]Object{v}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func inBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
|
||||||
|
if err := args.expectArgn(2); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r := args.args[1]
|
||||||
|
|
||||||
|
if args.args[0] == nil {
|
||||||
|
return BoolObject(false), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t := args.args[0].(type) {
|
||||||
|
case StringObject:
|
||||||
|
var rs string
|
||||||
|
if r != nil {
|
||||||
|
rs = r.String()
|
||||||
|
}
|
||||||
|
return BoolObject(strings.Contains(t.String(), rs)), nil
|
||||||
|
case Listable:
|
||||||
|
l := t.Len()
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
v := t.Index(i)
|
||||||
|
|
||||||
|
if ObjectsEqual(v, r) {
|
||||||
|
return BoolObject(true), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BoolObject(false), nil
|
||||||
|
case Hashable:
|
||||||
|
v := t.Value(r.String())
|
||||||
|
return BoolObject(v != nil), nil
|
||||||
|
case Iterable:
|
||||||
|
for t.HasNext() {
|
||||||
|
v, err := t.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ObjectsEqual(v, r) {
|
||||||
|
return BoolObject(true), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BoolObject(false), nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("expected listable")
|
||||||
|
}
|
||||||
|
|
||||||
func mapBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
|
func mapBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
|
||||||
if err := args.expectArgn(2); err != nil {
|
if err := args.expectArgn(2); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
64
ucl/builtins/fns.go
Normal file
64
ucl/builtins/fns.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package builtins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"lmika.dev/pkg/modash/moslice"
|
||||||
|
"ucl.lmika.dev/ucl"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Fns() ucl.Module {
|
||||||
|
return ucl.Module{
|
||||||
|
Name: "fns",
|
||||||
|
Builtins: map[string]ucl.BuiltinHandler{
|
||||||
|
"ident": fnsIdent,
|
||||||
|
"uni": fnsUni,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnsIdent(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||||
|
var inv ucl.Invokable
|
||||||
|
|
||||||
|
if err := args.Bind(&inv); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ucl.GoFunction(func(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||||
|
if args.NArgs() == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var x ucl.Object
|
||||||
|
if err := args.Bind(&x); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return inv.Invoke(ctx, x)
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnsUni(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||||
|
var inv ucl.Invokable
|
||||||
|
|
||||||
|
if err := args.Bind(&inv); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
restArgs := moslice.Map(args.RestAsObjects(), func(o ucl.Object) any { return o })
|
||||||
|
|
||||||
|
return ucl.GoFunction(func(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||||
|
fwdArgs := make([]any, len(restArgs)+1)
|
||||||
|
|
||||||
|
var o ucl.Object
|
||||||
|
if args.NArgs() != 0 {
|
||||||
|
if err := args.Bind(&o); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fwdArgs[0] = o
|
||||||
|
copy(fwdArgs[1:], restArgs)
|
||||||
|
|
||||||
|
return inv.Invoke(ctx, fwdArgs...)
|
||||||
|
}), nil
|
||||||
|
}
|
|
@ -1069,6 +1069,77 @@ func TestBuiltins_Seq(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuiltins_Call(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
expr string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{desc: "call simple", expr: `
|
||||||
|
call add [1 2]
|
||||||
|
`, want: "3\n"},
|
||||||
|
{desc: "call with proc", expr: `
|
||||||
|
call (proc { |x y| add $x $y }) [3 4]
|
||||||
|
`, want: "7\n"},
|
||||||
|
{desc: "meta call", expr: `
|
||||||
|
call "call" ["add" [1 3]]
|
||||||
|
`, want: "4\n"},
|
||||||
|
{desc: "curry proc", expr: `
|
||||||
|
proc curry { |name b|
|
||||||
|
proc { |a| call $name [$a $b] }
|
||||||
|
}
|
||||||
|
add2 = curry add 2
|
||||||
|
map [1 2 3] $add2 | cat
|
||||||
|
`, want: "[3 4 5]\n"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
outW := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
inst := New(WithOut(outW), WithTestBuiltin())
|
||||||
|
err := evalAndDisplay(ctx, inst, tt.expr)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, outW.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuiltins_In(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
expr string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{desc: "in str 1", expr: `in "absolute" "sol"`, want: "true\n"},
|
||||||
|
{desc: "in str 2", expr: `in "absolute" "not here"`, want: "false\n"},
|
||||||
|
{desc: "in list 1", expr: `in [1 2 3] 2`, want: "true\n"},
|
||||||
|
{desc: "in list 2", expr: `in [1 2 3] 4`, want: "false\n"},
|
||||||
|
{desc: "in map as key 1", expr: `in [a:1 b:2 c:3] a`, want: "true\n"},
|
||||||
|
{desc: "in map as key 2", expr: `in [a:1 b:2 c:3] gad`, want: "false\n"},
|
||||||
|
{desc: "in itr 1", expr: `in (itr) 2`, want: "true\n"},
|
||||||
|
{desc: "in itr 2", expr: `in (itr) 8`, want: "false\n"},
|
||||||
|
{desc: "in itr 3", expr: `itr = itr ; in $itr 2 ; head $itr`, want: "3\n"},
|
||||||
|
{desc: "in itr 4", expr: `itr = itr ; in $itr 8 ; head $itr`, want: "(nil)\n"},
|
||||||
|
{desc: "in nil", expr: `in () 4`, want: "false\n"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
outW := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
inst := New(WithOut(outW), WithTestBuiltin())
|
||||||
|
err := evalAndDisplay(ctx, inst, tt.expr)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, outW.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuiltins_Map(t *testing.T) {
|
func TestBuiltins_Map(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
desc string
|
desc string
|
||||||
|
|
|
@ -68,6 +68,7 @@ func New(opts ...InstOption) *Inst {
|
||||||
rootEC.addCmd("filter", invokableFunc(filterBuiltin))
|
rootEC.addCmd("filter", invokableFunc(filterBuiltin))
|
||||||
rootEC.addCmd("reduce", invokableFunc(reduceBuiltin))
|
rootEC.addCmd("reduce", invokableFunc(reduceBuiltin))
|
||||||
rootEC.addCmd("head", invokableFunc(firstBuiltin))
|
rootEC.addCmd("head", invokableFunc(firstBuiltin))
|
||||||
|
rootEC.addCmd("in", invokableFunc(inBuiltin))
|
||||||
|
|
||||||
rootEC.addCmd("keys", invokableFunc(keysBuiltin))
|
rootEC.addCmd("keys", invokableFunc(keysBuiltin))
|
||||||
|
|
||||||
|
|
19
ucl/objs.go
19
ucl/objs.go
|
@ -523,6 +523,25 @@ func (i invokableFunc) invoke(ctx context.Context, args invocationArgs) (Object,
|
||||||
return i(ctx, args)
|
return i(ctx, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GoFunction func(ctx context.Context, args CallArgs) (any, error)
|
||||||
|
|
||||||
|
func (gf GoFunction) String() string {
|
||||||
|
return "(proc)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gf GoFunction) Truthy() bool {
|
||||||
|
return gf != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gf GoFunction) invoke(ctx context.Context, args invocationArgs) (Object, error) {
|
||||||
|
v, err := gf(ctx, CallArgs{args: args})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fromGoValue(v)
|
||||||
|
}
|
||||||
|
|
||||||
type blockObject struct {
|
type blockObject struct {
|
||||||
block *astBlock
|
block *astBlock
|
||||||
closedEC *evalCtx
|
closedEC *evalCtx
|
||||||
|
|
|
@ -34,6 +34,10 @@ func (ca *CallArgs) Bind(vars ...interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ca *CallArgs) RestAsObjects() []Object {
|
||||||
|
return ca.args.args
|
||||||
|
}
|
||||||
|
|
||||||
func (ca *CallArgs) CanBind(vars ...interface{}) bool {
|
func (ca *CallArgs) CanBind(vars ...interface{}) bool {
|
||||||
if len(ca.args.args) < len(vars) {
|
if len(ca.args.args) < len(vars) {
|
||||||
return false
|
return false
|
||||||
|
@ -107,17 +111,30 @@ func (ca CallArgs) bindArg(v interface{}, arg Object) error {
|
||||||
*t, _ = toGoValue(arg)
|
*t, _ = toGoValue(arg)
|
||||||
return nil
|
return nil
|
||||||
case *Invokable:
|
case *Invokable:
|
||||||
i, ok := arg.(invokable)
|
switch ait := arg.(type) {
|
||||||
if !ok {
|
case invokable:
|
||||||
return errors.New("exepected invokable")
|
|
||||||
}
|
|
||||||
*t = Invokable{
|
*t = Invokable{
|
||||||
inv: i,
|
inv: ait,
|
||||||
eval: ca.args.eval,
|
eval: ca.args.eval,
|
||||||
inst: ca.args.inst,
|
inst: ca.args.inst,
|
||||||
ec: ca.args.ec,
|
ec: ca.args.ec,
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
case StringObject:
|
||||||
|
iv := ca.args.ec.lookupInvokable(string(ait))
|
||||||
|
if iv == nil {
|
||||||
|
return errors.New("'" + string(ait) + "' is not invokable")
|
||||||
|
}
|
||||||
|
*t = Invokable{
|
||||||
|
inv: iv,
|
||||||
|
eval: ca.args.eval,
|
||||||
|
inst: ca.args.inst,
|
||||||
|
ec: ca.args.ec,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.New("exepected invokable")
|
||||||
|
}
|
||||||
case *Listable:
|
case *Listable:
|
||||||
i, ok := arg.(Listable)
|
i, ok := arg.(Listable)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
Loading…
Reference in a new issue