Added the 'in' builtin and fns:uni
Some checks failed
Build / build (push) Failing after 1m3s

This commit is contained in:
Leon Mika 2025-07-17 01:21:42 +02:00
parent 4ab94410b7
commit 7a2b012833
9 changed files with 233 additions and 10 deletions

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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
View 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
}

View file

@ -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

View file

@ -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))

View file

@ -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

View file

@ -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 {