Compare commits

..

No commits in common. "main" and "feature/assign" have entirely different histories.

22 changed files with 210 additions and 827 deletions

View file

@ -14,7 +14,7 @@ import (
type NoResults struct{} type NoResults struct{}
func (r *REPL) EvalAndDisplay(ctx context.Context, expr string) error { func (r *REPL) EvalAndDisplay(ctx context.Context, expr string) error {
res, err := r.inst.EvalString(ctx, expr) res, err := r.inst.Eval(ctx, expr)
if err != nil { if err != nil {
if errors.Is(err, ucl.ErrNotConvertable) { if errors.Is(err, ucl.ErrNotConvertable) {
return nil return nil

View file

@ -98,7 +98,6 @@ type astDotSuffix struct {
} }
type astDot struct { type astDot struct {
Pos lexer.Position
Arg astCmdArg `parser:"@@"` Arg astCmdArg `parser:"@@"`
DotSuffix []astDotSuffix `parser:"( DOT @@ )*"` DotSuffix []astDotSuffix `parser:"( DOT @@ )*"`
} }
@ -149,7 +148,7 @@ var scanner = lexer.MustStateful(lexer.Rules{
{"NL", `[;\n][; \n\t]*`, nil}, {"NL", `[;\n][; \n\t]*`, nil},
{"PIPE", `\|`, nil}, {"PIPE", `\|`, nil},
{"EQ", `=`, nil}, {"EQ", `=`, nil},
{"Ident", `[-!?]*[a-zA-Z_!?-][\w-!?]*`, nil}, {"Ident", `[-]*[a-zA-Z_][\w-!?]*`, nil},
}, },
"String": { "String": {
{"Escaped", `\\.`, nil}, {"Escaped", `\\.`, nil},
@ -173,6 +172,6 @@ var scanner = lexer.MustStateful(lexer.Rules{
var parser = participle.MustBuild[astScript](participle.Lexer(scanner), var parser = participle.MustBuild[astScript](participle.Lexer(scanner),
participle.Elide("Whitespace", "Comment")) participle.Elide("Whitespace", "Comment"))
func parse(fname string, r io.Reader) (*astScript, error) { func parse(r io.Reader) (*astScript, error) {
return parser.Parse("test", r) return parser.Parse("test", r)
} }

View file

@ -6,8 +6,6 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"github.com/alecthomas/participle/v2/lexer"
) )
func echoBuiltin(ctx context.Context, args invocationArgs) (Object, error) { func echoBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
@ -190,7 +188,7 @@ func eqBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
l := args.args[0] l := args.args[0]
r := args.args[1] r := args.args[1]
return BoolObject(ObjectsEqual(l, r)), nil return BoolObject(objectsEqual(l, r)), nil
} }
func neBuiltin(ctx context.Context, args invocationArgs) (Object, error) { func neBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
@ -201,7 +199,7 @@ func neBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
l := args.args[0] l := args.args[0]
r := args.args[1] r := args.args[1]
return BoolObject(!ObjectsEqual(l, r)), nil return BoolObject(!objectsEqual(l, r)), nil
} }
func ltBuiltin(ctx context.Context, args invocationArgs) (Object, error) { func ltBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
@ -225,7 +223,7 @@ func leBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return BoolObject(isLess || ObjectsEqual(args.args[0], args.args[1])), nil return BoolObject(isLess || objectsEqual(args.args[0], args.args[1])), nil
} }
func gtBuiltin(ctx context.Context, args invocationArgs) (Object, error) { func gtBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
@ -249,7 +247,7 @@ func geBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return BoolObject(isGreater || ObjectsEqual(args.args[0], args.args[1])), nil return BoolObject(isGreater || objectsEqual(args.args[0], args.args[1])), nil
} }
func andBuiltin(ctx context.Context, args invocationArgs) (Object, error) { func andBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
@ -283,12 +281,12 @@ func notBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
return nil, err return nil, err
} }
return BoolObject(!isTruthy(args.args[0])), nil return BoolObject(!args.args[0].Truthy()), nil
} }
var errObjectsNotEqual = errors.New("objects not equal") var errObjectsNotEqual = errors.New("objects not equal")
func ObjectsEqual(l, r Object) bool { func objectsEqual(l, r Object) bool {
if l == nil || r == nil { if l == nil || r == nil {
return l == nil && r == nil return l == nil && r == nil
} }
@ -316,7 +314,7 @@ func ObjectsEqual(l, r Object) bool {
return false return false
} }
for i := 0; i < lv.Len(); i++ { for i := 0; i < lv.Len(); i++ {
if !ObjectsEqual(lv.Index(i), rv.Index(i)) { if !objectsEqual(lv.Index(i), rv.Index(i)) {
return false return false
} }
} }
@ -334,7 +332,7 @@ func ObjectsEqual(l, r Object) bool {
rkv := rv.Value(k) rkv := rv.Value(k)
if rkv == nil { if rkv == nil {
return errObjectsNotEqual return errObjectsNotEqual
} else if !ObjectsEqual(lkv, rkv) { } else if !objectsEqual(lkv, rkv) {
return errObjectsNotEqual return errObjectsNotEqual
} }
return nil return nil
@ -372,18 +370,6 @@ func strBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
return StringObject(args.args[0].String()), nil return StringObject(args.args[0].String()), nil
} }
func notNilBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(1); err != nil {
return nil, err
}
if args.args[0] == nil {
return BoolObject(false), nil
}
return BoolObject(true), nil
}
func intBuiltin(ctx context.Context, args invocationArgs) (Object, error) { func intBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(1); err != nil { if err := args.expectArgn(1); err != nil {
return nil, err return nil, err
@ -511,10 +497,7 @@ func lenBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
return IntObject(0), nil return IntObject(0), nil
} }
func indexLookup(ctx context.Context, obj, elem Object, pos lexer.Position) (Object, error) { func indexLookup(ctx context.Context, obj, elem Object) (Object, error) {
if obj == nil {
return nil, nil
}
switch v := obj.(type) { switch v := obj.(type) {
case Listable: case Listable:
intIdx, ok := elem.(IntObject) intIdx, ok := elem.(IntObject)
@ -530,48 +513,13 @@ func indexLookup(ctx context.Context, obj, elem Object, pos lexer.Position) (Obj
case Hashable: case Hashable:
strIdx, ok := elem.(StringObject) strIdx, ok := elem.(StringObject)
if !ok { if !ok {
return nil, nil return nil, errors.New("expected string for Hashable")
} }
return v.Value(string(strIdx)), nil return v.Value(string(strIdx)), nil
default:
return nil, notIndexableError(pos)
} }
return nil, nil return nil, nil
} }
func indexAssign(ctx context.Context, obj, elem, toVal Object, pos lexer.Position) (_ Object, err error) {
if obj == nil {
return nil, assignToNilIndex(pos)
}
switch v := obj.(type) {
case ModListable:
intIdx, ok := elem.(IntObject)
if !ok {
return nil, nil
}
if int(intIdx) >= 0 && int(intIdx) < v.Len() {
err = v.SetIndex(int(intIdx), toVal)
} else if int(intIdx) < 0 && int(intIdx) >= -v.Len() {
err = v.SetIndex(v.Len()+int(intIdx), toVal)
}
if err != nil {
return nil, err
}
return toVal, nil
case ModHashable:
strIdx, ok := elem.(StringObject)
if !ok {
return nil, nil
}
err = v.SetValue(string(strIdx), toVal)
if err != nil {
return nil, err
}
return toVal, nil
}
return nil, notModIndexableError(pos)
}
func indexBuiltin(ctx context.Context, args invocationArgs) (Object, error) { func indexBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(1); err != nil { if err := args.expectArgn(1); err != nil {
return nil, err return nil, err
@ -579,7 +527,7 @@ func indexBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
val := args.args[0] val := args.args[0]
for _, idx := range args.args[1:] { for _, idx := range args.args[1:] {
newVal, err := indexLookup(ctx, val, idx, lexer.Position{}) newVal, err := indexLookup(ctx, val, idx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -742,7 +690,7 @@ func filterBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
} }
return &newList, nil return &newList, nil
case Hashable: case Hashable:
newHash := HashObject{} newHash := hashObject{}
if err := t.Each(func(k string, v Object) error { if err := t.Each(func(k string, v Object) error {
if m, err := inv.invoke(ctx, args.fork([]Object{StringObject(k), v})); err != nil { if m, err := inv.invoke(ctx, args.fork([]Object{StringObject(k), v})); err != nil {
return err return err
@ -1213,7 +1161,7 @@ func procBuiltin(ctx context.Context, args macroArgs) (Object, error) {
obj := procObject{args.eval, args.ec, blockObj.block} obj := procObject{args.eval, args.ec, blockObj.block}
if procName != "" { if procName != "" {
args.ec.addUserCmd(procName, obj) args.ec.addCmd(procName, obj)
} }
return obj, nil return obj, nil
} }

View file

@ -45,7 +45,7 @@ func TestCSV_ReadRecord(t *testing.T) {
ucl.WithOut(&bfr), ucl.WithOut(&bfr),
) )
_, err := inst.EvalString(context.Background(), tt.eval) _, err := inst.Eval(context.Background(), tt.eval)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.wantOut, bfr.String()) assert.Equal(t, tt.wantOut, bfr.String())
}) })

View file

@ -29,7 +29,7 @@ func TestFS_Cat(t *testing.T) {
inst := ucl.New( inst := ucl.New(
ucl.WithModule(builtins.FS(testFS)), ucl.WithModule(builtins.FS(testFS)),
) )
res, err := inst.EvalString(context.Background(), tt.eval) res, err := inst.Eval(context.Background(), tt.eval)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, res) assert.Equal(t, tt.want, res)
}) })

View file

@ -26,7 +26,7 @@ func TestItrs_ToList(t *testing.T) {
inst := ucl.New( inst := ucl.New(
ucl.WithModule(builtins.Itrs()), ucl.WithModule(builtins.Itrs()),
) )
res, err := inst.EvalString(context.Background(), tt.eval) res, err := inst.Eval(context.Background(), tt.eval)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {

View file

@ -11,7 +11,6 @@ func Lists() ucl.Module {
Name: "lists", Name: "lists",
Builtins: map[string]ucl.BuiltinHandler{ Builtins: map[string]ucl.BuiltinHandler{
"first": listFirst, "first": listFirst,
"uniq": listUniq,
}, },
} }
} }
@ -59,69 +58,3 @@ func listFirst(ctx context.Context, args ucl.CallArgs) (any, error) {
return newList, nil return newList, nil
} }
func eachListOrIterItem(ctx context.Context, o ucl.Object, f func(int, ucl.Object) error) error {
switch t := o.(type) {
case ucl.Listable:
for i := 0; i < t.Len(); i++ {
if err := f(i, t.Index(i)); err != nil {
return err
}
}
return nil
case ucl.Iterable:
idx := 0
for t.HasNext() {
v, err := t.Next(ctx)
if err != nil {
return err
}
if err := f(idx, v); err != nil {
return err
}
idx++
}
}
return errors.New("expected listable")
}
type uniqKey struct {
sVal string
iVal int
}
func listUniq(ctx context.Context, args ucl.CallArgs) (any, error) {
var (
what ucl.Object
)
if err := args.Bind(&what); err != nil {
return nil, err
}
seen := make(map[uniqKey]bool)
found := ucl.NewListObject()
if err := eachListOrIterItem(ctx, what, func(idx int, v ucl.Object) error {
var key uniqKey
switch v := v.(type) {
case ucl.StringObject:
key = uniqKey{sVal: string(v)}
case ucl.IntObject:
key = uniqKey{iVal: int(v)}
default:
return errors.New("expected string or int")
}
if !seen[key] {
seen[key] = true
found.Append(v)
}
return nil
}); err != nil {
return nil, err
}
return found, nil
}

View file

@ -34,38 +34,7 @@ func TestLists_First(t *testing.T) {
ucl.WithModule(builtins.Itrs()), ucl.WithModule(builtins.Itrs()),
ucl.WithModule(builtins.Lists()), ucl.WithModule(builtins.Lists()),
) )
res, err := inst.EvalString(context.Background(), tt.eval) res, err := inst.Eval(context.Background(), tt.eval)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
}
})
}
}
func TestLists_Uniq(t *testing.T) {
tests := []struct {
desc string
eval string
want any
wantErr bool
}{
{desc: "uniq 1", eval: `lists:uniq [a a a a b b b c c c]`, want: []any{"a", "b", "c"}},
{desc: "uniq 2", eval: `lists:uniq [1 2 1 3 2 4 2 5 3]`, want: []any{1, 2, 3, 4, 5}},
{desc: "uniq 3", eval: `lists:uniq [1 a 2 b 3 b 2 a 1]5`, want: []any{1, "a", 2, "b", 3}},
{desc: "uniq err 1", eval: `lists:uniq [[1 2 3] [a:2] ()]`, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
inst := ucl.New(
ucl.WithModule(builtins.Itrs()),
ucl.WithModule(builtins.Lists()),
)
res, err := inst.EvalString(context.Background(), tt.eval)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {

View file

@ -34,7 +34,7 @@ func TestLog_Puts(t *testing.T) {
})), })),
) )
res, err := inst.EvalString(context.Background(), tt.eval) res, err := inst.Eval(context.Background(), tt.eval)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {

View file

@ -28,7 +28,7 @@ func TestOS_Env(t *testing.T) {
inst := ucl.New( inst := ucl.New(
ucl.WithModule(builtins.OS()), ucl.WithModule(builtins.OS()),
) )
res, err := inst.EvalString(context.Background(), tt.eval) res, err := inst.Eval(context.Background(), tt.eval)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, res) assert.Equal(t, tt.want, res)
}) })

View file

@ -103,7 +103,7 @@ func join(ctx context.Context, args ucl.CallArgs) (any, error) {
if i > 0 { if i > 0 {
sb.WriteString(tok) sb.WriteString(tok)
} }
sb.WriteString(ucl.ObjectToString(t.Index(i))) sb.WriteString(t.Index(i).String())
} }
return sb.String(), nil return sb.String(), nil
case ucl.Iterable: case ucl.Iterable:
@ -120,7 +120,7 @@ func join(ctx context.Context, args ucl.CallArgs) (any, error) {
} else { } else {
first = false first = false
} }
sb.WriteString(ucl.ObjectToString(v)) sb.WriteString(v.String())
} }
return sb.String(), nil return sb.String(), nil

View file

@ -28,7 +28,7 @@ func TestStrs_ToUpper(t *testing.T) {
inst := ucl.New( inst := ucl.New(
ucl.WithModule(builtins.Strs()), ucl.WithModule(builtins.Strs()),
) )
res, err := inst.EvalString(context.Background(), tt.eval) res, err := inst.Eval(context.Background(), tt.eval)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
@ -59,7 +59,7 @@ func TestStrs_ToLower(t *testing.T) {
inst := ucl.New( inst := ucl.New(
ucl.WithModule(builtins.Strs()), ucl.WithModule(builtins.Strs()),
) )
res, err := inst.EvalString(context.Background(), tt.eval) res, err := inst.Eval(context.Background(), tt.eval)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
@ -90,7 +90,7 @@ func TestStrs_Trim(t *testing.T) {
inst := ucl.New( inst := ucl.New(
ucl.WithModule(builtins.Strs()), ucl.WithModule(builtins.Strs()),
) )
res, err := inst.EvalString(context.Background(), tt.eval) res, err := inst.Eval(context.Background(), tt.eval)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
@ -122,7 +122,7 @@ func TestStrs_HasPrefix(t *testing.T) {
inst := ucl.New( inst := ucl.New(
ucl.WithModule(builtins.Strs()), ucl.WithModule(builtins.Strs()),
) )
res, err := inst.EvalString(context.Background(), tt.eval) res, err := inst.Eval(context.Background(), tt.eval)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
@ -160,7 +160,7 @@ func TestStrs_Split(t *testing.T) {
inst := ucl.New( inst := ucl.New(
ucl.WithModule(builtins.Strs()), ucl.WithModule(builtins.Strs()),
) )
res, err := inst.EvalString(context.Background(), tt.eval) res, err := inst.Eval(context.Background(), tt.eval)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
@ -183,7 +183,6 @@ func TestStrs_Join(t *testing.T) {
{desc: "join 3", eval: `strs:join [a b c] ""`, want: "abc"}, {desc: "join 3", eval: `strs:join [a b c] ""`, want: "abc"},
{desc: "join 4", eval: `strs:join [a b c]`, want: "abc"}, {desc: "join 4", eval: `strs:join [a b c]`, want: "abc"},
{desc: "join 5", eval: `strs:join (itrs:from [a b c]) ","`, want: "a,b,c"}, {desc: "join 5", eval: `strs:join (itrs:from [a b c]) ","`, want: "a,b,c"},
{desc: "join 6", eval: `strs:join [a () c () e]`, want: "ace"},
} }
for _, tt := range tests { for _, tt := range tests {
@ -192,7 +191,7 @@ func TestStrs_Join(t *testing.T) {
ucl.WithModule(builtins.Itrs()), ucl.WithModule(builtins.Itrs()),
ucl.WithModule(builtins.Strs()), ucl.WithModule(builtins.Strs()),
) )
res, err := inst.EvalString(context.Background(), tt.eval) res, err := inst.Eval(context.Background(), tt.eval)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {

View file

@ -25,7 +25,7 @@ func TestTime_FromUnix(t *testing.T) {
inst := ucl.New( inst := ucl.New(
ucl.WithModule(builtins.Time()), ucl.WithModule(builtins.Time()),
) )
res, err := inst.EvalString(context.Background(), tt.eval) res, err := inst.Eval(context.Background(), tt.eval)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
@ -47,7 +47,7 @@ func TestTime_Sleep(t *testing.T) {
ucl.WithModule(builtins.Time()), ucl.WithModule(builtins.Time()),
) )
_, err := inst.EvalString(ctx, `time:sleep 1`) _, err := inst.Eval(ctx, `time:sleep 1`)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, "context canceled", err.Error()) assert.Equal(t, "context canceled", err.Error())
assert.True(t, time.Now().Sub(st) < time.Second) assert.True(t, time.Now().Sub(st) < time.Second)

View file

@ -7,11 +7,12 @@ type evalCtx struct {
macros map[string]macroable macros map[string]macroable
vars map[string]Object vars map[string]Object
pseudoVars map[string]pseudoVar pseudoVars map[string]pseudoVar
userCommandFrame bool // Frame to use for user-defined commands
} }
func (ec *evalCtx) forkAndIsolate() *evalCtx { func (ec *evalCtx) forkAndIsolate() *evalCtx {
return &evalCtx{parent: ec, root: ec.root, userCommandFrame: true} newEc := &evalCtx{parent: ec}
newEc.root = newEc
return newEc
} }
func (ec *evalCtx) fork() *evalCtx { func (ec *evalCtx) fork() *evalCtx {
@ -26,25 +27,6 @@ func (ec *evalCtx) addCmd(name string, inv invokable) {
ec.root.commands[name] = inv ec.root.commands[name] = inv
} }
func (ec *evalCtx) addUserCmd(name string, inv invokable) {
frame := ec
for frame != nil {
if frame.userCommandFrame {
break
}
frame = frame.parent
}
if frame == nil {
panic("no user command frame found")
}
if frame.commands == nil {
frame.commands = make(map[string]invokable)
}
frame.commands[name] = inv
}
func (ec *evalCtx) addMacro(name string, inv macroable) { func (ec *evalCtx) addMacro(name string, inv macroable) {
if ec.root.macros == nil { if ec.root.macros == nil {
ec.root.macros = make(map[string]macroable) ec.root.macros = make(map[string]macroable)
@ -79,9 +61,6 @@ func (ec *evalCtx) setOrDefineVar(name string, val Object) {
func (ec *evalCtx) getVar(name string) (Object, bool) { func (ec *evalCtx) getVar(name string) (Object, bool) {
if ec.vars == nil { if ec.vars == nil {
if ec.parent == nil {
return nil, false
}
return ec.parent.getVar(name) return ec.parent.getVar(name)
} }

View file

@ -3,7 +3,6 @@ package ucl
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/alecthomas/participle/v2/lexer" "github.com/alecthomas/participle/v2/lexer"
) )
@ -13,9 +12,6 @@ var (
var ( var (
tooManyFinallyBlocksError = newBadUsage("try needs at most 1 finally") tooManyFinallyBlocksError = newBadUsage("try needs at most 1 finally")
notIndexableError = newBadUsage("index only support on lists and hashes")
notModIndexableError = newBadUsage("list or hash cannot be modified")
assignToNilIndex = newBadUsage("assigning to nil index value")
) )
type errorWithPos struct { type errorWithPos struct {

View file

@ -5,8 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"github.com/alecthomas/participle/v2/lexer"
) )
type evaluator struct { type evaluator struct {
@ -207,7 +205,7 @@ func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (Object,
} }
} }
res, err = indexLookup(ctx, res, idx, n.Pos) res, err = indexLookup(ctx, res, idx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -220,51 +218,7 @@ func (e evaluator) assignDot(ctx context.Context, ec *evalCtx, n astDot, toVal O
return e.assignArg(ctx, ec, n.Arg, toVal) return e.assignArg(ctx, ec, n.Arg, toVal)
} }
val, err := e.evalArgForDotAssign(ctx, ec, n.Arg) return nil, errors.New("TODO")
if err != nil {
return nil, err
}
for i, dot := range n.DotSuffix {
isLast := i == len(n.DotSuffix)-1
var idx Object
if dot.KeyIdent != nil {
idx = StringObject(dot.KeyIdent.String())
} else {
idx, err = e.evalPipeline(ctx, ec, dot.Pipeline)
if err != nil {
return nil, err
}
}
if isLast {
val, err = indexAssign(ctx, val, idx, toVal, n.Pos)
} else {
val, err = indexLookup(ctx, val, idx, n.Pos)
}
if err != nil {
return nil, err
}
}
return val, nil
}
func (e evaluator) evalArgForDotAssign(ctx context.Context, ec *evalCtx, n astCmdArg) (Object, error) {
// Special case for dot assigns of 'a.b = c' where a is actually a var deref (i.e. $a)
// which is unnecessary for assignments. Likewise, having '$a.b = c' should be dissallowed
switch {
case n.Ident != nil:
if v, ok := ec.getVar(n.Ident.String()); ok {
return v, nil
}
return nil, nil
case n.Var != nil:
return nil, errors.New("cannot assign to a dereferenced variable")
}
return e.evalArg(ctx, ec, n)
} }
func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Object, error) { func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Object, error) {
@ -287,7 +241,7 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec
return mph.get(ctx, *n.PseudoVar) return mph.get(ctx, *n.PseudoVar)
} }
return nil, errors.New("unknown pseudo-variable: " + *n.PseudoVar) return nil, errors.New("unknown pseudo-variable: " + *n.Var)
case n.MaybeSub != nil: case n.MaybeSub != nil:
sub := n.MaybeSub.Sub sub := n.MaybeSub.Sub
if sub == nil { if sub == nil {
@ -304,13 +258,12 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec
func (e evaluator) assignArg(ctx context.Context, ec *evalCtx, n astCmdArg, toVal Object) (Object, error) { func (e evaluator) assignArg(ctx context.Context, ec *evalCtx, n astCmdArg, toVal Object) (Object, error) {
switch { switch {
case n.Ident != nil:
ec.setOrDefineVar(n.Ident.String(), toVal)
return toVal, nil
case n.Literal != nil: case n.Literal != nil:
return nil, errors.New("cannot assign to a literal value") // We may use this for variable setting?
return nil, errors.New("cannot assign to a literal")
case n.Var != nil: case n.Var != nil:
return nil, errors.New("cannot assign to a dereferenced variable") ec.setOrDefineVar(*n.Var, toVal)
return toVal, nil
case n.PseudoVar != nil: case n.PseudoVar != nil:
pvar, ok := ec.getPseudoVar(*n.PseudoVar) pvar, ok := ec.getPseudoVar(*n.PseudoVar)
if ok { if ok {
@ -341,11 +294,11 @@ func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astList
if loh.EmptyList { if loh.EmptyList {
return &ListObject{}, nil return &ListObject{}, nil
} else if loh.EmptyHash { } else if loh.EmptyHash {
return HashObject{}, nil return hashObject{}, nil
} }
if firstIsHash := loh.Elements[0].Right != nil; firstIsHash { if firstIsHash := loh.Elements[0].Right != nil; firstIsHash {
h := HashObject{} h := hashObject{}
for _, el := range loh.Elements { for _, el := range loh.Elements {
if el.Right == nil { if el.Right == nil {
return nil, errors.New("miss-match of lists and hash") return nil, errors.New("miss-match of lists and hash")
@ -475,7 +428,7 @@ func (e evaluator) interpolateLongIdent(ctx context.Context, ec *evalCtx, n *ast
} }
} }
res, err = indexLookup(ctx, res, idx, lexer.Position{}) res, err = indexLookup(ctx, res, idx)
if err != nil { if err != nil {
return "", err return "", err
} }

View file

@ -53,9 +53,7 @@ type Module struct {
} }
func New(opts ...InstOption) *Inst { func New(opts ...InstOption) *Inst {
rootEC := &evalCtx{ rootEC := &evalCtx{}
userCommandFrame: true,
}
rootEC.root = rootEC rootEC.root = rootEC
rootEC.addCmd("echo", invokableFunc(echoBuiltin)) rootEC.addCmd("echo", invokableFunc(echoBuiltin))
@ -81,8 +79,6 @@ func New(opts ...InstOption) *Inst {
rootEC.addCmd("str", invokableFunc(strBuiltin)) rootEC.addCmd("str", invokableFunc(strBuiltin))
rootEC.addCmd("int", invokableFunc(intBuiltin)) rootEC.addCmd("int", invokableFunc(intBuiltin))
rootEC.addCmd("!nil", invokableFunc(notNilBuiltin))
rootEC.addCmd("add", invokableFunc(addBuiltin)) rootEC.addCmd("add", invokableFunc(addBuiltin))
rootEC.addCmd("sub", invokableFunc(subBuiltin)) rootEC.addCmd("sub", invokableFunc(subBuiltin))
rootEC.addCmd("mup", invokableFunc(mupBuiltin)) rootEC.addCmd("mup", invokableFunc(mupBuiltin))
@ -145,25 +141,8 @@ func (inst *Inst) Out() io.Writer {
return inst.out return inst.out
} }
type EvalOption func(*evalOptions) func (inst *Inst) Eval(ctx context.Context, expr string) (any, error) {
res, err := inst.eval(ctx, expr)
func WithSubEnv() EvalOption {
return func(opts *evalOptions) {
opts.forkEnv = true
}
}
func (inst *Inst) Eval(ctx context.Context, r io.Reader, options ...EvalOption) (any, error) {
opts := evalOptions{
filename: "unnamed",
forkEnv: false,
}
for _, opt := range options {
opt(&opts)
}
res, err := inst.eval(ctx, r, opts)
if err != nil { if err != nil {
if errors.Is(err, ErrHalt) { if errors.Is(err, ErrHalt) {
return nil, nil return nil, nil
@ -179,29 +158,16 @@ func (inst *Inst) Eval(ctx context.Context, r io.Reader, options ...EvalOption)
return goRes, nil return goRes, nil
} }
func (inst *Inst) EvalString(ctx context.Context, expr string) (any, error) { func (inst *Inst) eval(ctx context.Context, expr string) (Object, error) {
return inst.Eval(ctx, strings.NewReader(expr)) ast, err := parse(strings.NewReader(expr))
}
type evalOptions struct {
filename string
forkEnv bool
}
func (inst *Inst) eval(ctx context.Context, r io.Reader, opts evalOptions) (Object, error) {
ast, err := parse(opts.filename, r)
if err != nil { if err != nil {
return nil, err return nil, err
} }
eval := evaluator{inst: inst} eval := evaluator{inst: inst}
env := inst.rootEC // TODO: this should be a separate forkAndIsolate() session
if opts.forkEnv { return eval.evalScript(ctx, inst.rootEC, ast)
env = env.forkAndIsolate()
}
return eval.evalScript(ctx, env, ast)
} }
type PseudoVarHandler interface { type PseudoVarHandler interface {

View file

@ -3,13 +3,10 @@ package ucl_test
import ( import (
"bytes" "bytes"
"context" "context"
"strings"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
"testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"testing"
) )
func TestInst_Eval(t *testing.T) { func TestInst_Eval(t *testing.T) {
@ -18,24 +15,23 @@ func TestInst_Eval(t *testing.T) {
expr string expr string
want any want any
wantObj bool wantObj bool
wantAnErr bool
wantErr error wantErr error
}{ }{
{desc: "simple string", expr: `firstarg "hello"`, want: "hello"}, {desc: "simple string", expr: `firstarg "hello"`, want: "hello"},
{desc: "simple int 1", expr: `firstarg 123`, want: 123}, {desc: "simple int 1", expr: `firstarg 123`, want: 123},
{desc: "simple int 2", expr: `firstarg -234`, want: -234}, {desc: "simple int 2", expr: `firstarg -234`, want: -234},
{desc: "simple ident 1", expr: `firstarg a-test`, want: "a-test"}, {desc: "simple ident", expr: `firstarg a-test`, want: "a-test"},
// String interpolation // String interpolation
{desc: "interpolate string 1", expr: `what = "world" ; firstarg "hello $what"`, want: "hello world"}, {desc: "interpolate string 1", expr: `$what = "world" ; firstarg "hello $what"`, want: "hello world"},
{desc: "interpolate string 2", expr: `what = "world" ; when = "now" ; firstarg "$when, hello $what"`, want: "now, hello world"}, {desc: "interpolate string 2", expr: `$what = "world" ; $when = "now" ; firstarg "$when, hello $what"`, want: "now, hello world"},
{desc: "interpolate string 3", expr: `what = "world" ; when = "now" ; firstarg "${when}, hello ${what}"`, want: "now, hello world"}, {desc: "interpolate string 3", expr: `$what = "world" ; $when = "now" ; firstarg "${when}, hello ${what}"`, want: "now, hello world"},
{desc: "interpolate string 4", expr: `crazy = [far: "unknown"] ; firstarg "hello ${crazy.far}"`, want: "hello unknown"}, {desc: "interpolate string 4", expr: `$crazy = [far: "unknown"] ; firstarg "hello ${crazy.far}"`, want: "hello unknown"},
{desc: "interpolate string 5", expr: `oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(1)}"`, want: "hello thither"}, {desc: "interpolate string 5", expr: `$oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(1)}"`, want: "hello thither"},
{desc: "interpolate string 6", expr: `oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 1 1)}"`, want: "hello yonder"}, {desc: "interpolate string 6", expr: `$oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 1 1)}"`, want: "hello yonder"},
{desc: "interpolate string 7", expr: `oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 2 | sub (sub 2 1) | sub 1)}"`, want: "hello hither"}, {desc: "interpolate string 7", expr: `$oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 2 | sub (sub 2 1) | sub 1)}"`, want: "hello hither"},
{desc: "interpolate string 8", expr: `words = ["old": ["hither" "thither" "yonder"] "new": ["near" "far"]] ; firstarg "hello ${words.old.(2)}"`, want: "hello yonder"}, {desc: "interpolate string 8", expr: `$words = ["old": ["hither" "thither" "yonder"] "new": ["near" "far"]] ; firstarg "hello ${words.old.(2)}"`, want: "hello yonder"},
{desc: "interpolate string 9", expr: `what = "world" ; firstarg "hello $($what)"`, want: "hello world"}, {desc: "interpolate string 9", expr: `$what = "world" ; firstarg "hello $($what)"`, want: "hello world"},
{desc: "interpolate string 10", expr: `firstarg "hello $([1 2 3] | len)"`, want: "hello 3"}, {desc: "interpolate string 10", expr: `firstarg "hello $([1 2 3] | len)"`, want: "hello 3"},
{desc: "interpolate string 11", expr: `firstarg "hello $(add (add 1 2) 3)"`, want: "hello 6"}, {desc: "interpolate string 11", expr: `firstarg "hello $(add (add 1 2) 3)"`, want: "hello 6"},
{desc: "interpolate string 12", expr: `firstarg ("$(add 2 (add 1 1)) + $([1 2 3].(1) | cat ("$("")")) = $(("$(add 2 (4))"))")`, want: "4 + 2 = 6"}, {desc: "interpolate string 12", expr: `firstarg ("$(add 2 (add 1 1)) + $([1 2 3].(1) | cat ("$("")")) = $(("$(add 2 (4))"))")`, want: "4 + 2 = 6"},
@ -62,71 +58,53 @@ func TestInst_Eval(t *testing.T) {
// Multi-statements // Multi-statements
{desc: "multi 1", expr: `firstarg "hello" ; firstarg "world"`, want: "world"}, {desc: "multi 1", expr: `firstarg "hello" ; firstarg "world"`, want: "world"},
{desc: "multi 2", expr: `list "hello" | toUpper ; firstarg "world"`, want: "world"}, {desc: "multi 2", expr: `list "hello" | toUpper ; firstarg "world"`, want: "world"},
{desc: "multi 3", expr: `new = "this is new" ; firstarg $new`, want: "this is new"}, {desc: "multi 3", expr: `$new = "this is new" ; firstarg $new`, want: "this is new"},
// Lists // Lists
{desc: "list 1", expr: `firstarg ["1" "2" "3"]`, want: []any{"1", "2", "3"}}, {desc: "list 1", expr: `firstarg ["1" "2" "3"]`, want: []any{"1", "2", "3"}},
{desc: "list 2", expr: `one = "one" ; firstarg [$one (list "two" | map { |x| toUpper $x } | head) "three"]`, want: []any{"one", "TWO", "three"}}, {desc: "list 2", expr: `$one = "one" ; firstarg [$one (list "two" | map { |x| toUpper $x } | head) "three"]`, want: []any{"one", "TWO", "three"}},
{desc: "list 3", expr: `firstarg []`, want: []any{}}, {desc: "list 3", expr: `firstarg []`, want: []any{}},
{desc: "list 4", expr: `x = ["a" "b" "c"] ; firstarg [$x.(2) $x.(1) $x.(0)]`, want: []any{"c", "b", "a"}}, {desc: "list 4", expr: `$x = ["a" "b" "c"] ; firstarg [$x.(2) $x.(1) $x.(0)]`, want: []any{"c", "b", "a"}},
// Maps // Maps
{desc: "map 1", expr: `firstarg [one:"1" two:"2" three:"3"]`, want: map[string]any{"one": "1", "two": "2", "three": "3"}}, {desc: "map 1", expr: `firstarg [one:"1" two:"2" three:"3"]`, want: map[string]any{"one": "1", "two": "2", "three": "3"}},
{desc: "map 2", expr: `firstarg ["one":"1" "two":"2" "three":"3"]`, want: map[string]any{"one": "1", "two": "2", "three": "3"}}, {desc: "map 2", expr: `firstarg ["one":"1" "two":"2" "three":"3"]`, want: map[string]any{"one": "1", "two": "2", "three": "3"}},
{desc: "map 3", expr: ` {desc: "map 3", expr: `
one = "one" ; n1 = "1" $one = "one" ; $n1 = "1"
firstarg [ firstarg [
$one:$n1 $one:$n1
(list "two" | map { |x| toUpper $x } | head):(list "2" | map { |x| toUpper $x } | head) (list "two" | map { |x| toUpper $x } | head):(list "2" | map { |x| toUpper $x } | head)
three:"3" three:"3"
]`, want: map[string]any{"one": "1", "TWO": "2", "three": "3"}}, ]`, want: map[string]any{"one": "1", "TWO": "2", "three": "3"}},
{desc: "map 4", expr: `firstarg [:]`, want: map[string]any{}}, {desc: "map 4", expr: `firstarg [:]`, want: map[string]any{}},
{desc: "map 5", expr: `x = ["a" "b" "c"] ; firstarg ["one":$x.(2) "two":$x.(1) "three":$x.(0)]`, want: map[string]any{"one": "c", "two": "b", "three": "a"}}, {desc: "map 5", expr: `$x = ["a" "b" "c"] ; firstarg ["one":$x.(2) "two":$x.(1) "three":$x.(0)]`, want: map[string]any{"one": "c", "two": "b", "three": "a"}},
{desc: "map 6", expr: `x = [a:"A" b:"B" c:"C"] ; firstarg ["one":$x.c "two":$x.b "three":$x.a]`, want: map[string]any{"one": "C", "two": "B", "three": "A"}}, {desc: "map 6", expr: `$x = [a:"A" b:"B" c:"C"] ; firstarg ["one":$x.c "two":$x.b "three":$x.a]`, want: map[string]any{"one": "C", "two": "B", "three": "A"}},
// Dots // Dots
{desc: "dot expr 1", expr: `x = [1 2 3] ; $x.(0)`, want: 1}, {desc: "dot expr 1", expr: `$x = [1 2 3] ; $x.(0)`, want: 1},
{desc: "dot expr 2", expr: `x = [1 2 3] ; $x.(1)`, want: 2}, {desc: "dot expr 2", expr: `$x = [1 2 3] ; $x.(1)`, want: 2},
{desc: "dot expr 3", expr: `x = [1 2 3] ; $x.(2)`, want: 3}, {desc: "dot expr 3", expr: `$x = [1 2 3] ; $x.(2)`, want: 3},
{desc: "dot expr 4", expr: `x = [1 2 3] ; $x.(3)`, want: nil}, {desc: "dot expr 4", expr: `$x = [1 2 3] ; $x.(3)`, want: nil},
{desc: "dot expr 5", expr: `x = [1 2 3] ; $x.(add 1 1)`, want: 3}, {desc: "dot expr 5", expr: `$x = [1 2 3] ; $x.(add 1 1)`, want: 3},
{desc: "dot expr 6", expr: `x = [1 2 3] ; $x.(-1)`, want: 3}, {desc: "dot expr 6", expr: `$x = [1 2 3] ; $x.(-1)`, want: 3},
{desc: "dot expr 7", expr: `x = [1 2 3] ; $x.(-2)`, want: 2}, {desc: "dot expr 7", expr: `$x = [1 2 3] ; $x.(-2)`, want: 2},
{desc: "dot expr 8", expr: `x = [1 2 3] ; $x.(-3)`, want: 1}, {desc: "dot expr 8", expr: `$x = [1 2 3] ; $x.(-3)`, want: 1},
{desc: "dot expr 9", expr: `x = [1 2 3] ; $x.(-4)`, want: nil}, {desc: "dot expr 9", expr: `$x = [1 2 3] ; $x.(-4)`, want: nil},
{desc: "dot idents 1", expr: `x = [alpha:"hello" bravo:"world"] ; $x.alpha`, want: "hello"}, {desc: "dot idents 1", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.alpha`, want: "hello"},
{desc: "dot idents 2", expr: `x = [alpha:"hello" bravo:"world"] ; $x.bravo`, want: "world"}, {desc: "dot idents 2", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.bravo`, want: "world"},
{desc: "dot idents 3", expr: `x = [alpha:"hello" bravo:"world"] ; $x.charlie`, want: nil}, {desc: "dot idents 3", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.charlie`, want: nil},
{desc: "dot idents 4", expr: `x = [alpha:"hello" bravo:"world"] ; $x.("alpha")`, want: "hello"}, {desc: "dot idents 4", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("alpha")`, want: "hello"},
{desc: "dot idents 5", expr: `x = [alpha:"hello" bravo:"world"] ; $x.("bravo")`, want: "world"}, {desc: "dot idents 5", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("bravo")`, want: "world"},
{desc: "dot idents 6", expr: `x = [alpha:"hello" bravo:"world"] ; $x.("charlie")`, want: nil}, {desc: "dot idents 6", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("charlie")`, want: nil},
{desc: "dot idents 7", expr: `x = [MORE:"stuff"] ; $x.("more" | toUpper)`, want: "stuff"}, {desc: "dot idents 7", expr: `$x = [MORE:"stuff"] ; $x.("more" | toUpper)`, want: "stuff"},
{desc: "dot idents 8", expr: `x = [MORE:"stuff"] ; $x.(toUpper ("more"))`, want: "stuff"}, {desc: "dot idents 8", expr: `$x = [MORE:"stuff"] ; $x.(toUpper ("more"))`, want: "stuff"},
{desc: "dot idents 9", expr: `x = [MORE:"stuff"] ; $x.y`, want: nil}, {desc: "dot idents 9", expr: `$x = [MORE:"stuff"] ; x.y`, want: nil},
{desc: "dot err 1", expr: `x = [1 2 3] ; $x.Hello`, want: nil},
{desc: "dot err 2", expr: `x = [1 2 3] ; $x.("world")`, want: nil},
{desc: "dot err 4", expr: `x = [a:1 b:2] ; $x.(5)`, want: nil},
{desc: "dot err 3", expr: `x = [a:1 b:2] ; $x.(0)`, want: nil},
{desc: "dot err 5", expr: `x = 123 ; $x.(5)`, wantAnErr: true},
{desc: "dot err 6", expr: `x = 123 ; $x.Five`, wantAnErr: true},
{desc: "parse comments 1", expr: parseComments1, wantObj: true, wantErr: nil}, {desc: "parse comments 1", expr: parseComments1, wantObj: true, wantErr: nil},
{desc: "parse comments 2", expr: parseComments2, wantObj: true, wantErr: nil}, {desc: "parse comments 2", expr: parseComments2, wantObj: true, wantErr: nil},
{desc: "parse comments 3", expr: parseComments3, wantObj: true, wantErr: nil}, {desc: "parse comments 3", expr: parseComments3, wantObj: true, wantErr: nil},
{desc: "parse comments 4", expr: parseComments4, wantObj: true, wantErr: nil}, {desc: "parse comments 4", expr: parseComments4, wantObj: true, wantErr: nil},
// Assign dots
{desc: "assign dot 1", expr: `x = [1 2 3] ; x.(0) = 4 ; "$x"`, want: "[4 2 3]"},
{desc: "assign dot 2", expr: `x = [1 2 3] ; x.(1) = 5 ; "$x"`, want: "[1 5 3]"},
{desc: "assign dot 3", expr: `x = [1 2 3] ; x.(-1) = 6 ; "$x"`, want: "[1 2 6]"},
{desc: "assign dot 4", expr: `y = [a:1 b:2] ; y.a = "hello" ; "$y"`, want: `[a:hello b:2]`},
{desc: "assign dot 5", expr: `y = [a:1 b:2] ; y.b = "world" ; "$y"`, want: `[a:1 b:world]`},
{desc: "assign dot 6", expr: `y = [a:"b" b:2] ; y.($y.a) = "world" ; "$y"`, want: `[a:b b:world]`},
{desc: "assign dot 7", expr: `z = [a:[1 2] b:[3 3]] ; z.a.(1) = 3 ; "$z"`, want: `[a:[1 3] b:[3 3]]`},
{desc: "assign dot 8", expr: `z = [[1 2] [3 4]] ; z.(1).(0) = 5 ; "$z"`, want: `[[1 2] [5 4]]`},
{desc: "assign dot 7", expr: `z = [[a:1 b:2] [c:3 d:4]] ; z.(1).a = 5 ; "$z"`, want: `[[a:1 b:2] [a:5 c:3 d:4]]`},
} }
for _, tt := range tests { for _, tt := range tests {
@ -135,12 +113,10 @@ func TestInst_Eval(t *testing.T) {
outW := bytes.NewBuffer(nil) outW := bytes.NewBuffer(nil)
inst := ucl.New(ucl.WithOut(outW), ucl.WithTestBuiltin()) inst := ucl.New(ucl.WithOut(outW), ucl.WithTestBuiltin())
res, err := inst.EvalString(ctx, tt.expr) res, err := inst.Eval(ctx, tt.expr)
if tt.wantErr != nil { if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr) assert.ErrorIs(t, err, tt.wantErr)
} else if tt.wantAnErr {
assert.Error(t, err)
} else if tt.wantObj { } else if tt.wantObj {
assert.NoError(t, err) assert.NoError(t, err)
_, isObj := res.(ucl.Object) _, isObj := res.(ucl.Object)
@ -153,117 +129,6 @@ func TestInst_Eval(t *testing.T) {
} }
} }
func TestInst_Eval_WithSubEnv(t *testing.T) {
t.Run("global symbols should not leak across environments", func(t *testing.T) {
ctx := t.Context()
inst := ucl.New()
res, err := inst.Eval(ctx, strings.NewReader(`a = "hello" ; $a`), ucl.WithSubEnv())
assert.NoError(t, err)
assert.Equal(t, "hello", res)
res, err = inst.Eval(ctx, strings.NewReader(`$a`), ucl.WithSubEnv())
assert.NoError(t, err)
assert.Nil(t, res)
})
t.Run("environments should not leak when using hooks", func(t *testing.T) {
tests := []struct {
descr string
eval1 string
eval2 string
want1 any
want2 any
}{
{
descr: "reading vars",
eval1: `a = "hello" ; hook { $a }`,
eval2: `a = "world" ; hook { $a }`,
want1: "hello",
want2: "world",
},
{
descr: "modifying vars",
eval1: `a = "hello" ; hook { a = "new value" ; $a }`,
eval2: `a = "world" ; hook { $a }`,
want1: "new value",
want2: "world",
},
{
descr: "defining procs",
eval1: `proc say_hello { "hello" } ; hook { say_hello }`,
eval2: `proc say_hello { "world" } ; hook { say_hello }`,
want1: "hello",
want2: "world",
},
{
descr: "exporting procs 1",
eval1: `export say_hello { "hello" } ; hook { say_hello }`,
eval2: `hook { say_hello }`,
want1: "hello",
want2: "hello",
},
{
descr: "exporting procs 2",
eval1: `a = "hello" ; export say_hello { a = "world"; $a } ; hook { say_hello }`,
eval2: `a = "other" ; hook { say_hello }`,
want1: "world",
want2: "world",
},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
ctx := t.Context()
hooks := make([]ucl.Invokable, 0)
inst := ucl.New()
inst.SetBuiltin("hook", func(ctx context.Context, args ucl.CallArgs) (any, error) {
var hookProc ucl.Invokable
if err := args.Bind(&hookProc); err != nil {
return nil, err
}
hooks = append(hooks, hookProc)
return nil, nil
})
inst.SetBuiltin("export", func(ctx context.Context, args ucl.CallArgs) (any, error) {
var (
name string
hookProc ucl.Invokable
)
if err := args.Bind(&name, &hookProc); err != nil {
return nil, err
}
inst.SetBuiltinInvokable(name, hookProc)
return nil, nil
})
res, err := inst.Eval(ctx, strings.NewReader(tt.eval1), ucl.WithSubEnv())
assert.NoError(t, err)
assert.Nil(t, res)
res, err = inst.Eval(ctx, strings.NewReader(tt.eval2), ucl.WithSubEnv())
assert.NoError(t, err)
assert.Nil(t, res)
h1, err := hooks[0].Invoke(ctx, ucl.CallArgs{})
assert.NoError(t, err)
assert.Equal(t, tt.want1, h1)
h2, err := hooks[1].Invoke(ctx, ucl.CallArgs{})
assert.NoError(t, err)
assert.Equal(t, tt.want2, h2)
})
}
})
}
func TestInst_SetPseudoVar(t *testing.T) { func TestInst_SetPseudoVar(t *testing.T) {
tests := []struct { tests := []struct {
desc string desc string
@ -290,7 +155,7 @@ func TestInst_SetPseudoVar(t *testing.T) {
inst.SetPseudoVar("bar", bar) inst.SetPseudoVar("bar", bar)
inst.SetMissingPseudoVarHandler(missingPseudoVarType{}) inst.SetMissingPseudoVarHandler(missingPseudoVarType{})
res, err := inst.EvalString(t.Context(), tt.expr) res, err := inst.Eval(t.Context(), tt.expr)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)

View file

@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -36,13 +35,9 @@ type ModListable interface {
// Insert adds a new item to the list. idx can be a positive // Insert adds a new item to the list. idx can be a positive
// number from 0 to len(), in which case the object will be inserted // number from 0 to len(), in which case the object will be inserted
// at that position, shifting all other elements to the right. // at that position. If idx is negative, then the item will be inserted
// If idx is negative, then the item will be inserted
// at that position from the right. // at that position from the right.
Insert(idx int, obj Object) error Insert(idx int, obj Object) error
// SetIndex replaces the item at index position idx with obj.
SetIndex(idx int, obj Object) error
} }
type Hashable interface { type Hashable interface {
@ -51,11 +46,6 @@ type Hashable interface {
Each(func(k string, v Object) error) error Each(func(k string, v Object) error) error
} }
type ModHashable interface {
Hashable
SetValue(k string, val Object) error
}
type ListObject []Object type ListObject []Object
func NewListObject() *ListObject { func NewListObject() *ListObject {
@ -90,11 +80,6 @@ func (s *ListObject) Index(i int) Object {
return (*s)[i] return (*s)[i]
} }
func (s *ListObject) SetIndex(i int, toVal Object) error {
(*s)[i] = toVal
return nil
}
type StringListObject []string type StringListObject []string
func (ss StringListObject) String() string { func (ss StringListObject) String() string {
@ -125,24 +110,16 @@ func (i iteratorObject) Truthy() bool {
return i.Iterable.HasNext() return i.Iterable.HasNext()
} }
type HashObject map[string]Object type hashObject map[string]Object
func (s HashObject) String() string { func (s hashObject) String() string {
if len(s) == 0 { if len(s) == 0 {
return "[:]" return "[:]"
} }
// Return the keys in sorted order
keys := make([]string, 0, len(s))
for k := range s {
keys = append(keys, k)
}
sort.Strings(keys)
sb := strings.Builder{} sb := strings.Builder{}
sb.WriteString("[") sb.WriteString("[")
for _, k := range keys { for k, v := range s {
v := s[k]
if sb.Len() != 1 { if sb.Len() != 1 {
sb.WriteString(" ") sb.WriteString(" ")
} }
@ -154,19 +131,19 @@ func (s HashObject) String() string {
return sb.String() return sb.String()
} }
func (s HashObject) Truthy() bool { func (s hashObject) Truthy() bool {
return len(s) > 0 return len(s) > 0
} }
func (s HashObject) Len() int { func (s hashObject) Len() int {
return len(s) return len(s)
} }
func (s HashObject) Value(k string) Object { func (s hashObject) Value(k string) Object {
return s[k] return s[k]
} }
func (s HashObject) Each(fn func(k string, v Object) error) error { func (s hashObject) Each(fn func(k string, v Object) error) error {
for k, v := range s { for k, v := range s {
if err := fn(k, v); err != nil { if err := fn(k, v); err != nil {
return err return err
@ -175,11 +152,6 @@ func (s HashObject) Each(fn func(k string, v Object) error) error {
return nil return nil
} }
func (s HashObject) SetValue(k string, val Object) error {
s[k] = val
return nil
}
type StringObject string type StringObject string
func (s StringObject) String() string { func (s StringObject) String() string {
@ -247,7 +219,7 @@ func toGoValue(obj Object) (interface{}, bool) {
xs = append(xs, x) xs = append(xs, x)
} }
return xs, true return xs, true
case HashObject: case hashObject:
xs := make(map[string]interface{}) xs := make(map[string]interface{})
for k, va := range v { for k, va := range v {
x, ok := toGoValue(va) x, ok := toGoValue(va)
@ -263,8 +235,6 @@ func toGoValue(obj Object) (interface{}, bool) {
return v, true return v, true
case proxyObject: case proxyObject:
return v.p, true return v.p, true
case OpaqueObject:
return v.v, true
case listableProxyObject: case listableProxyObject:
return v.orig.Interface(), true return v.orig.Interface(), true
case structProxyObject: case structProxyObject:
@ -710,10 +680,3 @@ func isBreakErr(err error) bool {
return errors.As(err, &errBreak{}) || errors.As(err, &errReturn{}) || errors.Is(err, ErrHalt) return errors.As(err, &errBreak{}) || errors.As(err, &errReturn{}) || errors.Is(err, ErrHalt)
} }
func ObjectToString(obj Object) string {
if obj == nil {
return ""
}
return obj.String()
}

View file

@ -5,10 +5,9 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/stretchr/testify/assert"
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
type testIterator struct { type testIterator struct {
@ -58,19 +57,6 @@ func WithTestBuiltin() InstOption {
return &a, nil return &a, nil
})) }))
i.rootEC.addCmd("rearrange", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) {
var as ListObject = make([]Object, 0)
for _, a := range args.args {
vs, ok := args.kwargs[a.String()]
if ok {
as = append(as, vs.Index(0))
}
}
return &as, nil
}))
i.rootEC.addCmd("error", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) { i.rootEC.addCmd("error", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) == 0 { if len(args.args) == 0 {
return nil, errors.New("an error occurred") return nil, errors.New("an error occurred")
@ -145,10 +131,10 @@ func TestBuiltins_Echo(t *testing.T) {
echo "world" # command after this echo "world" # command after this
; ;
`, want: "Hello\nworld\n"}, `, want: "Hello\nworld\n"},
{desc: "interpolated string 1", expr: `what = "world" ; echo "Hello, $what"`, want: "Hello, world\n"}, {desc: "interpolated string 1", expr: `$what = "world" ; echo "Hello, $what"`, want: "Hello, world\n"},
{desc: "interpolated string 2", expr: `what = "world" ; echo "Hello, \$what"`, want: "Hello, $what\n"}, {desc: "interpolated string 2", expr: `$what = "world" ; echo "Hello, \$what"`, want: "Hello, $what\n"},
{desc: "interpolated string 3", expr: `echo "separate\nlines\n\tand tabs"`, want: "separate\nlines\n\tand tabs\n"}, {desc: "interpolated string 3", expr: `echo "separate\nlines\n\tand tabs"`, want: "separate\nlines\n\tand tabs\n"},
{desc: "interpolated string 4", expr: `what = "Hello" ; where = "world" ; echo "$what, $where"`, want: "Hello, world\n"}, {desc: "interpolated string 4", expr: `$what = "Hello" ; $where = "world" ; echo "$what, $where"`, want: "Hello, world\n"},
{desc: "interpolated string 5", expr: ` {desc: "interpolated string 5", expr: `
for [123 "foo" true ()] { |x| for [123 "foo" true ()] { |x|
echo "[[$x]]" echo "[[$x]]"
@ -165,7 +151,7 @@ func TestBuiltins_Echo(t *testing.T) {
outW := bytes.NewBuffer(nil) outW := bytes.NewBuffer(nil)
inst := New(WithOut(outW), WithTestBuiltin()) inst := New(WithOut(outW), WithTestBuiltin())
res, err := inst.EvalString(ctx, tt.expr) res, err := inst.Eval(ctx, tt.expr)
assert.NoError(t, err) assert.NoError(t, err)
assert.Nil(t, res) assert.Nil(t, res)
@ -181,19 +167,19 @@ func TestBuiltins_If(t *testing.T) {
want string want string
}{ }{
{desc: "single then", expr: ` {desc: "single then", expr: `
x = "Hello" $x = "Hello"
if $x { if $x {
echo "true" echo "true"
}`, want: "true\n(nil)\n"}, }`, want: "true\n(nil)\n"},
{desc: "single then and else", expr: ` {desc: "single then and else", expr: `
x = "Hello" $x = "Hello"
if $x { if $x {
echo "true" echo "true"
} else { } else {
echo "false" echo "false"
}`, want: "true\n(nil)\n"}, }`, want: "true\n(nil)\n"},
{desc: "single then, elif and else", expr: ` {desc: "single then, elif and else", expr: `
x = "Hello" $x = "Hello"
if $y { if $y {
echo "y is true" echo "y is true"
} elif $x { } elif $x {
@ -202,14 +188,14 @@ func TestBuiltins_If(t *testing.T) {
echo "nothings x" echo "nothings x"
}`, want: "x is true\n(nil)\n"}, }`, want: "x is true\n(nil)\n"},
{desc: "single then and elif, no else", expr: ` {desc: "single then and elif, no else", expr: `
x = "Hello" $x = "Hello"
if $y { if $y {
echo "y is true" echo "y is true"
} elif $x { } elif $x {
echo "x is true" echo "x is true"
}`, want: "x is true\n(nil)\n"}, }`, want: "x is true\n(nil)\n"},
{desc: "single then, two elif, and else", expr: ` {desc: "single then, two elif, and else", expr: `
x = "Hello" $x = "Hello"
if $z { if $z {
echo "z is true" echo "z is true"
} elif $y { } elif $y {
@ -227,15 +213,15 @@ func TestBuiltins_If(t *testing.T) {
} else { } else {
echo "none is true" echo "none is true"
}`, want: "none is true\n(nil)\n"}, }`, want: "none is true\n(nil)\n"},
{desc: "compressed then", expr: `x = "Hello" ; if $x { echo "true" }`, want: "true\n(nil)\n"}, {desc: "compressed then", expr: `$x = "Hello" ; if $x { echo "true" }`, want: "true\n(nil)\n"},
{desc: "compressed else", expr: `if $x { echo "true" } else { echo "false" }`, want: "false\n(nil)\n"}, {desc: "compressed else", expr: `if $x { echo "true" } else { echo "false" }`, want: "false\n(nil)\n"},
{desc: "compressed if", expr: `if $x { echo "x" } elif $y { echo "y" } else { echo "false" }`, want: "false\n(nil)\n"}, {desc: "compressed if", expr: `if $x { echo "x" } elif $y { echo "y" } else { echo "false" }`, want: "false\n(nil)\n"},
{desc: "if of itr 1", expr: `i = itr ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, {desc: "if of itr 1", expr: `$i = itr ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
{desc: "if of itr 2", expr: `i = itr ; for (seq 1) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, {desc: "if of itr 2", expr: `$i = itr ; for (seq 1) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
{desc: "if of itr 3", expr: `i = itr ; for (seq 3) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"}, {desc: "if of itr 3", expr: `$i = itr ; for (seq 3) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"},
{desc: "if of itr 4", expr: `i = (itr | map { |x| add 2 $x }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, {desc: "if of itr 4", expr: `$i = (itr | map { |x| add 2 $x }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
{desc: "if of itr 5", expr: `i = (itr | filter { |x| () }) ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"}, {desc: "if of itr 5", expr: `$i = (itr | filter { |x| () }) ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"},
{desc: "if of itr 6", expr: `i = (itr | filter { |x| 1 }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, {desc: "if of itr 6", expr: `$i = (itr | filter { |x| 1 }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
} }
for _, tt := range tests { for _, tt := range tests {
@ -295,53 +281,53 @@ func TestBuiltins_While(t *testing.T) {
want string want string
}{ }{
{desc: "iterate while true 1", expr: ` {desc: "iterate while true 1", expr: `
x = 0 $x = 0
while (lt $x 5) { while (lt $x 5) {
echo $x echo $x
x = add $x 1 $x = (add $x 1)
} }
echo "done"`, want: "0\n1\n2\n3\n4\ndone\n(nil)\n"}, echo "done"`, want: "0\n1\n2\n3\n4\ndone\n(nil)\n"},
{desc: "iterate while true 2", expr: ` {desc: "iterate while true 2", expr: `
x = 20 $x = 20
while (lt $x 5) { while (lt $x 5) {
echo $x echo $x
x = (add $x 1) $x = (add $x 1)
} }
echo "done"`, want: "done\n(nil)\n"}, echo "done"`, want: "done\n(nil)\n"},
{desc: "iterate while true with pipeline", expr: ` {desc: "iterate while true with pipeline", expr: `
x = 0 $x = 0
while (lt $x 5) { while (lt $x 5) {
echo $x echo $x
x = (add $x 1) $x = (add $x 1)
if (ge $x 3) { if (ge $x 3) {
break "Ahh" break "Ahh"
} }
} | echo " was the break" } | echo " was the break"
echo "done"`, want: "0\n1\n2\nAhh was the break\ndone\n(nil)\n"}, echo "done"`, want: "0\n1\n2\nAhh was the break\ndone\n(nil)\n"},
{desc: "iterate for ever with break 1", expr: ` {desc: "iterate for ever with break 1", expr: `
x = 0 $x = 0
while { while {
echo $x echo $x
x = add $x 1 $x = (add $x 1)
if (ge $x 5) { if (ge $x 5) {
break break
} }
} }
echo "done"`, want: "0\n1\n2\n3\n4\ndone\n(nil)\n"}, echo "done"`, want: "0\n1\n2\n3\n4\ndone\n(nil)\n"},
{desc: "iterate for ever with break 2", expr: ` {desc: "iterate for ever with break 2", expr: `
x = 0 $x = 0
echo (while { echo (while {
echo $x echo $x
x = add $x 1 $x = add $x 1
if (ge $x 5) { if (ge $x 5) {
break $x break $x
} }
}) })
`, want: "0\n1\n2\n3\n4\n5\n(nil)\n"}, `, want: "0\n1\n2\n3\n4\n5\n(nil)\n"},
{desc: "iterate for ever with continue", expr: ` {desc: "iterate for ever with continue", expr: `
x = 0 $x = 0
while { while {
x = (add $x 1) $x = (add $x 1)
if (or (eq $x 2) (eq $x 4)) { if (or (eq $x 2) (eq $x 4)) {
echo "quack" echo "quack"
continue continue
@ -501,10 +487,10 @@ func TestBuiltins_Procs(t *testing.T) {
} }
} }
helloGreater = makeGreeter "Hello" $helloGreater = makeGreeter "Hello"
$helloGreater "world" $helloGreater "world"
goodbye = makeGreeter "Goodbye cruel" $goodbye = makeGreeter "Goodbye cruel"
$goodbye "world" $goodbye "world"
call (makeGreeter "Quick") ["call me"] call (makeGreeter "Quick") ["call me"]
@ -512,19 +498,16 @@ func TestBuiltins_Procs(t *testing.T) {
`, want: "Hello, world\nGoodbye cruel, world\nQuick, call me\n(nil)\n"}, `, want: "Hello, world\nGoodbye cruel, world\nQuick, call me\n(nil)\n"},
{desc: "modifying closed over variables", expr: ` {desc: "modifying closed over variables", expr: `
proc makeSetter { proc makeSetter {
bla = "X" $bla = "X"
proc appendToBla { |x| proc appendToBla { |x|
bla = cat $bla $x $bla = cat $bla $x
} }
} }
er = makeSetter $er = makeSetter
echo (call $er ["xxx"]) echo (call $er ["xxx"])
echo (call $er ["yyy"]) echo (call $er ["yyy"])
`, want: "Xxxx\nXxxxyyy\n(nil)\n"}, `, want: "Xxxx\nXxxxyyy\n(nil)\n"},
{desc: "calling with kwargs", expr: `
echo (call rearrange [b a] [a:"ey" b:"bee"])
`, want: "[bee ey]\n(nil)\n"},
} }
for _, tt := range tests { for _, tt := range tests {
@ -623,7 +606,7 @@ func TestBuiltins_Return(t *testing.T) {
echo "world" echo "world"
} }
proc greet { proc greet {
what = (greetWhat) $what = (greetWhat)
echo "Hello, " $what echo "Hello, " $what
} }
@ -687,7 +670,7 @@ func TestBuiltins_Return(t *testing.T) {
proc test-thing { proc test-thing {
for [1 2 3] { |x| for [1 2 3] { |x|
myClosure = proc { echo $x } $myClosure = proc { echo $x }
do-thing $myClosure do-thing $myClosure
} }
} }
@ -714,12 +697,12 @@ func TestBuiltins_Return(t *testing.T) {
proc test-thing { proc test-thing {
[1 2 3] | map { |x| [1 2 3] | map { |x|
myProc = proc { echo $x } $myProc = proc { echo $x }
proc { do-thing $myProc } proc { do-thing $myProc }
} }
} }
hello = "xx" $hello = "xx"
for (test-thing) { |y| call $y ; echo $hello } for (test-thing) { |y| call $y ; echo $hello }
`, want: "1\nxx\n2\nxx\n3\nxx\n(nil)\n"}, `, want: "1\nxx\n2\nxx\n3\nxx\n(nil)\n"},
{desc: "check closure 7", expr: ` {desc: "check closure 7", expr: `
@ -728,15 +711,15 @@ func TestBuiltins_Return(t *testing.T) {
} }
proc test-thing { proc test-thing {
f = 0 $f = 0
[1 2 3] | map { |x| [1 2 3] | map { |x|
myProc = proc { echo $f } $myProc = proc { echo $f }
f = (add $f 1) $f = (add $f 1)
proc { do-thing $myProc } proc { do-thing $myProc }
} }
} }
hello = "xx" $hello = "xx"
for (test-thing) { |y| call $y ; echo $hello } for (test-thing) { |y| call $y ; echo $hello }
`, want: "3\nxx\n3\nxx\n3\nxx\n(nil)\n"}, `, want: "3\nxx\n3\nxx\n3\nxx\n(nil)\n"},
{desc: "check closure 7", expr: ` {desc: "check closure 7", expr: `
@ -745,16 +728,16 @@ func TestBuiltins_Return(t *testing.T) {
} }
proc test-thing { proc test-thing {
f = 1 $f = 1
[1 2 3] | map { |x| [1 2 3] | map { |x|
g = $f $g = $f
myProc = (proc { echo $g }) $myProc = (proc { echo $g })
f = (add $f 1) $f = (add $f 1)
proc { do-thing $myProc } proc { do-thing $myProc }
} }
} }
hello = "xx" $hello = "xx"
for (test-thing) { |y| call $y ; echo $hello } for (test-thing) { |y| call $y ; echo $hello }
`, want: "1\nxx\n2\nxx\n3\nxx\n(nil)\n"}, `, want: "1\nxx\n2\nxx\n3\nxx\n(nil)\n"},
} }
@ -917,11 +900,11 @@ func TestBuiltins_Try(t *testing.T) {
} }
`, want: "Hello\nCatch me: bang\nAlways\n", wantErr: "boom"}, `, want: "Hello\nCatch me: bang\nAlways\n", wantErr: "boom"},
{desc: "try 12", expr: ` {desc: "try 12", expr: `
a = try { "e" } catch { "f" } $a = try { "e" } catch { "f" }
echo $a echo $a
`, want: "e\n(nil)\n"}, `, want: "e\n(nil)\n"},
{desc: "try 13", expr: ` {desc: "try 13", expr: `
a = try { error "bang" } catch { "f" } $a = try { error "bang" } catch { "f" }
echo $a echo $a
`, want: "f\n(nil)\n"}, `, want: "f\n(nil)\n"},
{desc: "try 14", expr: ` {desc: "try 14", expr: `
@ -1081,12 +1064,12 @@ func TestBuiltins_Map(t *testing.T) {
map ["a" "b" "c"] (proc { |x| makeUpper $x }) map ["a" "b" "c"] (proc { |x| makeUpper $x })
`, want: "A\nB\nC\n"}, `, want: "A\nB\nC\n"},
{desc: "map list 2", expr: ` {desc: "map list 2", expr: `
makeUpper = proc { |x| $x | toUpper } $makeUpper = proc { |x| $x | toUpper }
map ["a" "b" "c"] $makeUpper map ["a" "b" "c"] $makeUpper
`, want: "A\nB\nC\n"}, `, want: "A\nB\nC\n"},
{desc: "map list with pipe", expr: ` {desc: "map list with pipe", expr: `
makeUpper = proc { |x| $x | toUpper } $makeUpper = proc { |x| $x | toUpper }
["a" "b" "c"] | map $makeUpper ["a" "b" "c"] | map $makeUpper
`, want: "A\nB\nC\n"}, `, want: "A\nB\nC\n"},
@ -1094,15 +1077,15 @@ func TestBuiltins_Map(t *testing.T) {
map ["a" "b" "c"] { |x| toUpper $x } map ["a" "b" "c"] { |x| toUpper $x }
`, want: "A\nB\nC\n"}, `, want: "A\nB\nC\n"},
{desc: "map list with stream", expr: ` {desc: "map list with stream", expr: `
makeUpper = proc { |x| toUpper $x } $makeUpper = proc { |x| toUpper $x }
l = ["a" "b" "c"] | map $makeUpper $l = ["a" "b" "c"] | map $makeUpper
echo $l echo $l
`, want: "[A B C]\n(nil)\n"}, `, want: "[A B C]\n(nil)\n"},
{desc: "map itr stream", expr: ` {desc: "map itr stream", expr: `
add2 = proc { |x| add $x 2 } $add2 = proc { |x| add $x 2 }
l = itr | map $add2 $l = itr | map $add2
for $l { |x| echo $x } for $l { |x| echo $x }
`, want: "3\n4\n5\n(nil)\n"}, `, want: "3\n4\n5\n(nil)\n"},
} }
@ -1326,7 +1309,7 @@ func TestBuiltins_Keys(t *testing.T) {
}, nil }, nil
}) })
res, err := inst.EvalString(ctx, tt.expr) res, err := inst.Eval(ctx, tt.expr)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, res, len(tt.wantItems)) assert.Len(t, res, len(tt.wantItems))
for _, i := range tt.wantItems { for _, i := range tt.wantItems {
@ -1356,7 +1339,7 @@ func TestBuiltins_Filter(t *testing.T) {
}}, }},
{desc: "filter map 3", expr: `filter [alpha:"hello" bravo:"world"] { |k v| eq $v "alpha" }`, want: map[string]any{}}, {desc: "filter map 3", expr: `filter [alpha:"hello" bravo:"world"] { |k v| eq $v "alpha" }`, want: map[string]any{}},
{desc: "filter itr 1", expr: `s = "" ; itr | filter { |x| ne $x 2 } | for { |x| s = "$s $x" }; $s`, want: " 1 3"}, {desc: "filter itr 1", expr: `$s = "" ; itr | filter { |x| ne $x 2 } | for { |x| $s = "$s $x" }; $s`, want: " 1 3"},
} }
for _, tt := range tests { for _, tt := range tests {
@ -1366,7 +1349,7 @@ func TestBuiltins_Filter(t *testing.T) {
inst := New(WithOut(outW), WithTestBuiltin()) inst := New(WithOut(outW), WithTestBuiltin())
res, err := inst.EvalString(ctx, tt.expr) res, err := inst.Eval(ctx, tt.expr)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, res) assert.Equal(t, tt.want, res)
}) })
@ -1392,7 +1375,7 @@ func TestBuiltins_Reduce(t *testing.T) {
inst := New(WithOut(outW), WithTestBuiltin()) inst := New(WithOut(outW), WithTestBuiltin())
res, err := inst.EvalString(ctx, tt.expr) res, err := inst.Eval(ctx, tt.expr)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, res) assert.Equal(t, tt.want, res)
}) })
@ -1408,10 +1391,10 @@ func TestBuiltins_Head(t *testing.T) {
{desc: "head list 1", expr: `head [1 2 3]`, want: 1}, {desc: "head list 1", expr: `head [1 2 3]`, want: 1},
{desc: "head itr 1", expr: `head (itr)`, want: 1}, {desc: "head itr 1", expr: `head (itr)`, want: 1},
{desc: "head itr 2", expr: `h = (itr) ; head $h`, want: 1}, {desc: "head itr 2", expr: `$h = (itr) ; head $h`, want: 1},
{desc: "head itr 3", expr: `h = (itr) ; head $h ; head $h`, want: 2}, {desc: "head itr 3", expr: `$h = (itr) ; head $h ; head $h`, want: 2},
{desc: "head itr 4", expr: `h = (itr) ; head $h ; head $h ; head $h`, want: 3}, {desc: "head itr 4", expr: `$h = (itr) ; head $h ; head $h ; head $h`, want: 3},
{desc: "head itr 5", expr: `h = (itr) ; head $h ; head $h ; head $h ; head $h`, want: nil}, {desc: "head itr 5", expr: `$h = (itr) ; head $h ; head $h ; head $h ; head $h`, want: nil},
} }
for _, tt := range tests { for _, tt := range tests {
@ -1421,7 +1404,7 @@ func TestBuiltins_Head(t *testing.T) {
inst := New(WithOut(outW), WithTestBuiltin()) inst := New(WithOut(outW), WithTestBuiltin())
res, err := inst.EvalString(ctx, tt.expr) res, err := inst.Eval(ctx, tt.expr)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, res) assert.Equal(t, tt.want, res)
}) })
@ -1476,7 +1459,7 @@ func TestBuiltins_LtLeGtLe(t *testing.T) {
inst.SetVar("true", true) inst.SetVar("true", true)
inst.SetVar("false", false) inst.SetVar("false", false)
eqRes, err := inst.EvalString(ctx, tt.expr) eqRes, err := inst.Eval(ctx, tt.expr)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
@ -1544,11 +1527,11 @@ func TestBuiltins_EqNe(t *testing.T) {
inst.SetVar("true", true) inst.SetVar("true", true)
inst.SetVar("false", false) inst.SetVar("false", false)
eqRes, err := inst.EvalString(ctx, tt.expr) eqRes, err := inst.Eval(ctx, tt.expr)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, eqRes) assert.Equal(t, tt.want, eqRes)
neRes, err := inst.EvalString(ctx, strings.ReplaceAll(tt.expr, "eq", "ne")) neRes, err := inst.Eval(ctx, strings.ReplaceAll(tt.expr, "eq", "ne"))
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, !tt.want, neRes) assert.Equal(t, !tt.want, neRes)
}) })
@ -1579,7 +1562,7 @@ func TestBuiltins_Str(t *testing.T) {
inst := New(WithOut(outW), WithTestBuiltin()) inst := New(WithOut(outW), WithTestBuiltin())
eqRes, err := inst.EvalString(ctx, tt.expr) eqRes, err := inst.Eval(ctx, tt.expr)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, eqRes) assert.Equal(t, tt.want, eqRes)
}) })
@ -1615,7 +1598,7 @@ func TestBuiltins_Int(t *testing.T) {
inst := New(WithOut(outW), WithTestBuiltin()) inst := New(WithOut(outW), WithTestBuiltin())
eqRes, err := inst.EvalString(ctx, tt.expr) eqRes, err := inst.Eval(ctx, tt.expr)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
@ -1671,7 +1654,7 @@ func TestBuiltins_AddSubMupDivMod(t *testing.T) {
inst := New(WithOut(outW), WithTestBuiltin()) inst := New(WithOut(outW), WithTestBuiltin())
eqRes, err := inst.EvalString(ctx, tt.expr) eqRes, err := inst.Eval(ctx, tt.expr)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
@ -1722,7 +1705,7 @@ func TestBuiltins_AndOrNot(t *testing.T) {
inst.SetVar("true", true) inst.SetVar("true", true)
inst.SetVar("false", false) inst.SetVar("false", false)
eqRes, err := inst.EvalString(ctx, tt.expr) eqRes, err := inst.Eval(ctx, tt.expr)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
@ -1747,7 +1730,7 @@ func TestBuiltins_Cat(t *testing.T) {
{desc: "cat 6", expr: `cat "array = " [1 3 2 4]`, want: "array = [1 3 2 4]"}, {desc: "cat 6", expr: `cat "array = " [1 3 2 4]`, want: "array = [1 3 2 4]"},
{desc: "cat 7", expr: `cat 1 $true 3 [4]`, want: "1true3[4]"}, {desc: "cat 7", expr: `cat 1 $true 3 [4]`, want: "1true3[4]"},
{desc: "cat 8", expr: `cat`, want: ""}, {desc: "cat 8", expr: `cat`, want: ""},
{desc: "cat 9", expr: `x = ["a" "b" "c"] ; cat "array = " [1 $x.(0) $x.(2) $x.(1)]`, want: "array = [1 a c b]"}, {desc: "cat 9", expr: `$x = ["a" "b" "c"] ; cat "array = " [1 $x.(0) $x.(2) $x.(1)]`, want: "array = [1 a c b]"},
} }
for _, tt := range tests { for _, tt := range tests {
@ -1759,74 +1742,7 @@ func TestBuiltins_Cat(t *testing.T) {
inst.SetVar("true", true) inst.SetVar("true", true)
inst.SetVar("false", false) inst.SetVar("false", false)
eqRes, err := inst.EvalString(ctx, tt.expr) eqRes, err := inst.Eval(ctx, tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, eqRes)
})
}
}
func TestBuiltins_Not(t *testing.T) {
tests := []struct {
desc string
expr string
want any
}{
{desc: "not 1", expr: `not 1`, want: false},
{desc: "not 2", expr: `not 0`, want: true},
{desc: "not 3", expr: `not ()`, want: true},
{desc: "not 4", expr: `not $true`, want: false},
{desc: "not 5", expr: `not $false`, want: true},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
ctx := context.Background()
outW := bytes.NewBuffer(nil)
inst := New(WithOut(outW), WithTestBuiltin())
inst.SetVar("true", true)
inst.SetVar("false", false)
eqRes, err := inst.EvalString(ctx, tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, eqRes)
})
}
}
func TestBuiltins_NotNil(t *testing.T) {
tests := []struct {
desc string
expr string
want any
}{
{desc: "not nil 1", expr: `!nil "hello"`, want: true},
{desc: "not nil 2", expr: `!nil ""`, want: true},
{desc: "not nil 3", expr: `!nil 4`, want: true},
{desc: "not nil 4", expr: `!nil 0`, want: true},
{desc: "not nil 5", expr: `!nil $true`, want: true},
{desc: "not nil 6", expr: `!nil $false`, want: true},
{desc: "not nil 7", expr: `!nil [1 2 3]`, want: true},
{desc: "not nil 8", expr: `!nil []`, want: true},
{desc: "not nil 9", expr: `!nil [a:1 b:21]`, want: true},
{desc: "not nil 10", expr: `!nil [:]`, want: true},
{desc: "not nil 11", expr: `!nil ()`, want: false},
{desc: "not nil 12", expr: `[1 () 2 () 3] | filter !nil | reduce "" { |x a| "$a $x" }`, want: " 1 2 3"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
ctx := context.Background()
outW := bytes.NewBuffer(nil)
inst := New(WithOut(outW), WithTestBuiltin())
inst.SetVar("true", true)
inst.SetVar("false", false)
eqRes, err := inst.EvalString(ctx, tt.expr)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, eqRes) assert.Equal(t, tt.want, eqRes)
}) })
@ -1834,7 +1750,7 @@ func TestBuiltins_NotNil(t *testing.T) {
} }
func evalAndDisplay(ctx context.Context, inst *Inst, expr string) error { func evalAndDisplay(ctx context.Context, inst *Inst, expr string) error {
res, err := inst.eval(ctx, strings.NewReader(expr), evalOptions{}) res, err := inst.eval(ctx, expr)
if err != nil { if err != nil {
return err return err
} }

View file

@ -81,10 +81,6 @@ func (inst *Inst) SetBuiltin(name string, fn BuiltinHandler) {
inst.rootEC.addCmd(name, userBuiltin{fn: fn}) inst.rootEC.addCmd(name, userBuiltin{fn: fn})
} }
func (inst *Inst) SetBuiltinInvokable(name string, fn Invokable) {
inst.rootEC.addCmd(name, fn.inv)
}
type userBuiltin struct { type userBuiltin struct {
fn func(ctx context.Context, args CallArgs) (any, error) fn func(ctx context.Context, args CallArgs) (any, error)
} }
@ -163,12 +159,10 @@ func (ca CallArgs) bindArg(v interface{}, arg Object) error {
switch t := arg.(type) { switch t := arg.(type) {
case proxyObject: case proxyObject:
return bindProxyObject(v, reflect.ValueOf(t.p)) return bindProxyObject(v, reflect.ValueOf(t.p))
case OpaqueObject:
return bindProxyObject(v, reflect.ValueOf(t.v))
case listableProxyObject: case listableProxyObject:
return bindProxyObject(v, t.orig) return bindProxyObject(v, t.v)
case structProxyObject: case structProxyObject:
return bindProxyObject(v, t.orig) return bindProxyObject(v, t.v)
} }
return bindProxyObject(v, reflect.ValueOf(arg)) return bindProxyObject(v, reflect.ValueOf(arg))
@ -187,9 +181,9 @@ func canBindArg(v interface{}, arg Object) bool {
case proxyObject: case proxyObject:
return canBindProxyObject(v, reflect.ValueOf(t.p)) return canBindProxyObject(v, reflect.ValueOf(t.p))
case listableProxyObject: case listableProxyObject:
return canBindProxyObject(v, t.orig) return canBindProxyObject(v, t.v)
case structProxyObject: case structProxyObject:
return canBindProxyObject(v, t.orig) return canBindProxyObject(v, t.v)
} }
return true return true

View file

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -25,64 +24,11 @@ func TestInst_SetBuiltin(t *testing.T) {
return x + y, nil return x + y, nil
}) })
res, err := inst.EvalString(context.Background(), `add2 "Hello, " "World"`) res, err := inst.Eval(context.Background(), `add2 "Hello, " "World"`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "Hello, World", res) assert.Equal(t, "Hello, World", res)
}) })
t.Run("simple builtin accepting and returning pointers", func(t *testing.T) {
type point struct {
x, y int
}
tests := []struct {
descr string
expr string
want string
}{
{descr: "pass via args", expr: `vec 1 2 | vadd`, want: "3"},
{descr: "pass via vars", expr: `x = (vec 2 3) ; vadd $x`, want: "5"},
{descr: "pass twice", expr: `vadd (vadd2 (vec 1 2) (vec 3 4))`, want: "10"},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
inst := ucl.New()
inst.SetBuiltin("vec", func(ctx context.Context, args ucl.CallArgs) (any, error) {
var x, y int
if err := args.Bind(&x, &y); err != nil {
return nil, err
}
return &point{x, y}, nil
})
inst.SetBuiltin("vadd", func(ctx context.Context, args ucl.CallArgs) (any, error) {
var v *point
if err := args.Bind(&v); err != nil {
return nil, err
}
return v.x + v.y, nil
})
inst.SetBuiltin("vadd2", func(ctx context.Context, args ucl.CallArgs) (any, error) {
var v, u *point
if err := args.Bind(&v, &u); err != nil {
return nil, err
}
return &point{v.x + u.x, v.y + u.y}, nil
})
res, err := inst.EvalString(context.Background(), tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, fmt.Sprint(res))
})
}
})
t.Run("bind shift arguments", func(t *testing.T) { t.Run("bind shift arguments", func(t *testing.T) {
inst := ucl.New() inst := ucl.New()
inst.SetBuiltin("add2", func(ctx context.Context, args ucl.CallArgs) (any, error) { inst.SetBuiltin("add2", func(ctx context.Context, args ucl.CallArgs) (any, error) {
@ -98,7 +44,7 @@ func TestInst_SetBuiltin(t *testing.T) {
return x + y, nil return x + y, nil
}) })
res, err := inst.EvalString(context.Background(), `add2 "Hello, " "World"`) res, err := inst.Eval(context.Background(), `add2 "Hello, " "World"`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "Hello, World", res) assert.Equal(t, "Hello, World", res)
}) })
@ -139,7 +85,7 @@ func TestInst_SetBuiltin(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) { t.Run(tt.descr, func(t *testing.T) {
res, err := inst.EvalString(context.Background(), tt.expr) res, err := inst.Eval(context.Background(), tt.expr)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, res) assert.Equal(t, tt.want, res)
}) })
@ -159,10 +105,10 @@ func TestInst_SetBuiltin(t *testing.T) {
return nil, err return nil, err
} }
return ucl.Opaque(pair{x, y}), nil return pair{x, y}, nil
}) })
res, err := inst.EvalString(context.Background(), `add2 "Hello" "World"`) res, err := inst.Eval(context.Background(), `add2 "Hello" "World"`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, pair{"Hello", "World"}, res) assert.Equal(t, pair{"Hello", "World"}, res)
}) })
@ -178,7 +124,7 @@ func TestInst_SetBuiltin(t *testing.T) {
want string want string
}{ }{
{descr: "pass via args", expr: `join (add2 "left" "right")`, want: "left:right"}, {descr: "pass via args", expr: `join (add2 "left" "right")`, want: "left:right"},
{descr: "pass via vars", expr: `x = (add2 "blue" "green") ; join $x`, want: "blue:green"}, {descr: "pass via vars", expr: `$x = (add2 "blue" "green") ; join $x`, want: "blue:green"},
} }
for _, tt := range tests { for _, tt := range tests {
@ -191,7 +137,7 @@ func TestInst_SetBuiltin(t *testing.T) {
return nil, err return nil, err
} }
return ucl.Opaque(pair{x, y}), nil return pair{x, y}, nil
}) })
inst.SetBuiltin("join", func(ctx context.Context, args ucl.CallArgs) (any, error) { inst.SetBuiltin("join", func(ctx context.Context, args ucl.CallArgs) (any, error) {
var x pair var x pair
@ -203,50 +149,7 @@ func TestInst_SetBuiltin(t *testing.T) {
return x.x + ":" + x.y, nil return x.x + ":" + x.y, nil
}) })
res, err := inst.EvalString(context.Background(), tt.expr) res, err := inst.Eval(context.Background(), tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
}
})
t.Run("builtin operating on and returning proxy object for pointers", func(t *testing.T) {
type pair struct {
x, y string
}
tests := []struct {
descr string
expr string
want string
}{
{descr: "pass via args", expr: `join (add2 "left" "right")`, want: "left:right"},
{descr: "pass via vars", expr: `x = (add2 "blue" "green") ; join $x`, want: "blue:green"},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
inst := ucl.New()
inst.SetBuiltin("add2", func(ctx context.Context, args ucl.CallArgs) (any, error) {
var x, y string
if err := args.Bind(&x, &y); err != nil {
return nil, err
}
return ucl.Opaque(&pair{x, y}), nil
})
inst.SetBuiltin("join", func(ctx context.Context, args ucl.CallArgs) (any, error) {
var x *pair
if err := args.Bind(&x); err != nil {
return nil, err
}
return x.x + ":" + x.y, nil
})
res, err := inst.EvalString(context.Background(), tt.expr)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, res) assert.Equal(t, tt.want, res)
}) })
@ -273,7 +176,7 @@ func TestInst_SetBuiltin(t *testing.T) {
return []string{"1", "2", "3"}, nil return []string{"1", "2", "3"}, nil
}) })
res, err := inst.EvalString(context.Background(), tt.expr) res, err := inst.Eval(context.Background(), tt.expr)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, res) assert.Equal(t, tt.want, res)
assert.Equal(t, tt.wantOut, outW.String()) assert.Equal(t, tt.wantOut, outW.String())
@ -303,11 +206,11 @@ func TestCallArgs_Bind(t *testing.T) {
return ds.DoString(), nil return ds.DoString(), nil
}) })
va, err := inst.EvalString(ctx, `dostr (sa)`) va, err := inst.Eval(ctx, `dostr (sa)`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "do string A: a val", va) assert.Equal(t, "do string A: a val", va)
vb, err := inst.EvalString(ctx, `dostr (sb)`) vb, err := inst.Eval(ctx, `dostr (sb)`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "do string B: foo bar", vb) assert.Equal(t, "do string B: foo bar", vb)
}) })
@ -337,7 +240,7 @@ func TestCallArgs_Bind(t *testing.T) {
return fmt.Sprintf("[%v]", v), nil return fmt.Sprintf("[%v]", v), nil
}) })
res, err := inst.EvalString(ctx, tt.eval) res, err := inst.Eval(ctx, tt.eval)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, res) assert.Equal(t, tt.want, res)
}) })
@ -392,7 +295,7 @@ func TestCallArgs_CanBind(t *testing.T) {
return nil, nil return nil, nil
}) })
_, err := inst.EvalString(ctx, tt.eval) _, err := inst.Eval(ctx, tt.eval)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, res) assert.Equal(t, tt.want, res)
}) })
@ -429,7 +332,7 @@ func TestCallArgs_CanBind(t *testing.T) {
return h.Value(k), nil return h.Value(k), nil
}) })
res, err := inst.EvalString(ctx, tt.eval) res, err := inst.Eval(ctx, tt.eval)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
assert.Nil(t, res) assert.Nil(t, res)
@ -467,7 +370,7 @@ func TestCallArgs_CanBind(t *testing.T) {
ctx := context.Background() ctx := context.Background()
res, err := inst.EvalString(ctx, `wrap { |x| toUpper $x }`) res, err := inst.Eval(ctx, `wrap { |x| toUpper $x }`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "[[HELLO]]", res) assert.Equal(t, "[[HELLO]]", res)
}) })
@ -498,7 +401,7 @@ func TestCallArgs_CanBind(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Nil(t, before) assert.Nil(t, before)
res, err := inst.EvalString(ctx, `wrap { |x| toUpper $x }`) res, err := inst.Eval(ctx, `wrap { |x| toUpper $x }`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Nil(t, res) assert.Nil(t, res)
@ -534,7 +437,7 @@ func TestCallArgs_MissingCommandHandler(t *testing.T) {
return fmt.Sprintf("was %v", name), nil return fmt.Sprintf("was %v", name), nil
})) }))
res, err := inst.EvalString(ctx, tt.eval) res, err := inst.Eval(ctx, tt.eval)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, res) assert.Equal(t, tt.want, res)
}) })
@ -557,27 +460,27 @@ func TestCallArgs_IsTopLevel(t *testing.T) {
return nil, nil return nil, nil
}) })
_, err := inst.EvalString(ctx, `lvl "one"`) _, err := inst.Eval(ctx, `lvl "one"`)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, res["one"]) assert.True(t, res["one"])
_, err = inst.EvalString(ctx, `echo (lvl "two")`) _, err = inst.Eval(ctx, `echo (lvl "two")`)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, res["two"]) assert.True(t, res["two"])
_, err = inst.EvalString(ctx, `proc doLvl { |n| lvl $n } ; doLvl "three"`) _, err = inst.Eval(ctx, `proc doLvl { |n| lvl $n } ; doLvl "three"`)
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, res["three"]) assert.False(t, res["three"])
_, err = inst.EvalString(ctx, `doLvl "four"`) _, err = inst.Eval(ctx, `doLvl "four"`)
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, res["four"]) assert.False(t, res["four"])
_, err = inst.EvalString(ctx, `["a"] | map { |x| doLvl "five" ; $x }`) _, err = inst.Eval(ctx, `["a"] | map { |x| doLvl "five" ; $x }`)
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, res["five"]) assert.False(t, res["five"])
_, err = inst.EvalString(ctx, `if 1 { lvl "six" }`) _, err = inst.Eval(ctx, `if 1 { lvl "six" }`)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, res["six"]) assert.True(t, res["six"])
}) })