Compare commits

...

10 commits

Author SHA1 Message Date
Leon Mika 3a88c0c777 Renambed builtins_test
All checks were successful
Build / build (push) Successful in 2m10s
2025-05-28 21:39:31 +10:00
Leon Mika e71699d5a7 Added list:uniq and the !nil builtin 2025-05-28 21:39:03 +10:00
Leon Mika 41b4fdb003 Switch to exposing the existing HashObject
All checks were successful
Build / build (push) Successful in 2m5s
2025-05-27 21:42:13 +10:00
Leon Mika 03e6878524 Fixed nil panic
Some checks failed
Build / build (push) Failing after 1m35s
2025-05-27 21:21:10 +10:00
Leon Mika e869e6c9bd Added HashObject
Some checks failed
Build / build (push) Failing after 1m34s
2025-05-27 21:09:48 +10:00
Leon Mika 3076897eb7 Merge branch 'scratch/merge'
All checks were successful
Build / build (push) Successful in 2m4s
2025-05-25 12:37:17 +10:00
Leon Mika f1cdf6dd93 Added facility for defining top-level commands from invokables 2025-05-25 12:36:27 +10:00
Leon Mika ba6d42acbb Fixed bug which was not isolating procs 2025-05-25 10:08:56 +10:00
Leon Mika d8460f69bc Added separate Eval mode with option to isolate the environment
This allows for keeping variables isolated
2025-05-25 09:50:25 +10:00
lmika 8a9a40f67c Merge pull request 'Alternate signing constructs' (#1) from feature/assign into main
All checks were successful
Build / build (push) Successful in 2m7s
Reviewed-on: #1
2025-05-24 00:12:23 +00:00
21 changed files with 418 additions and 91 deletions

View file

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

View file

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

View file

@ -188,7 +188,7 @@ func eqBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
l := args.args[0]
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) {
@ -199,7 +199,7 @@ func neBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
l := args.args[0]
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) {
@ -223,7 +223,7 @@ func leBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err != nil {
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) {
@ -247,7 +247,7 @@ func geBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err != nil {
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) {
@ -286,7 +286,7 @@ func notBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
var errObjectsNotEqual = errors.New("objects not equal")
func objectsEqual(l, r Object) bool {
func ObjectsEqual(l, r Object) bool {
if l == nil || r == nil {
return l == nil && r == nil
}
@ -314,7 +314,7 @@ func objectsEqual(l, r Object) bool {
return false
}
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
}
}
@ -332,7 +332,7 @@ func objectsEqual(l, r Object) bool {
rkv := rv.Value(k)
if rkv == nil {
return errObjectsNotEqual
} else if !objectsEqual(lkv, rkv) {
} else if !ObjectsEqual(lkv, rkv) {
return errObjectsNotEqual
}
return nil
@ -370,6 +370,18 @@ func strBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
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) {
if err := args.expectArgn(1); err != nil {
return nil, err
@ -690,7 +702,7 @@ func filterBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
}
return &newList, nil
case Hashable:
newHash := hashObject{}
newHash := HashObject{}
if err := t.Each(func(k string, v Object) error {
if m, err := inv.invoke(ctx, args.fork([]Object{StringObject(k), v})); err != nil {
return err
@ -1161,7 +1173,7 @@ func procBuiltin(ctx context.Context, args macroArgs) (Object, error) {
obj := procObject{args.eval, args.ec, blockObj.block}
if procName != "" {
args.ec.addCmd(procName, obj)
args.ec.addUserCmd(procName, obj)
}
return obj, nil
}

View file

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

View file

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

View file

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

View file

@ -11,6 +11,7 @@ func Lists() ucl.Module {
Name: "lists",
Builtins: map[string]ucl.BuiltinHandler{
"first": listFirst,
"uniq": listUniq,
},
}
}
@ -58,3 +59,69 @@ func listFirst(ctx context.Context, args ucl.CallArgs) (any, error) {
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,7 +34,38 @@ func TestLists_First(t *testing.T) {
ucl.WithModule(builtins.Itrs()),
ucl.WithModule(builtins.Lists()),
)
res, err := inst.Eval(context.Background(), tt.eval)
res, err := inst.EvalString(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 {
assert.Error(t, err)
} else {

View file

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

View file

@ -28,7 +28,7 @@ func TestOS_Env(t *testing.T) {
inst := ucl.New(
ucl.WithModule(builtins.OS()),
)
res, err := inst.Eval(context.Background(), tt.eval)
res, err := inst.EvalString(context.Background(), tt.eval)
assert.NoError(t, err)
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 {
sb.WriteString(tok)
}
sb.WriteString(t.Index(i).String())
sb.WriteString(ucl.ObjectToString(t.Index(i)))
}
return sb.String(), nil
case ucl.Iterable:
@ -120,7 +120,7 @@ func join(ctx context.Context, args ucl.CallArgs) (any, error) {
} else {
first = false
}
sb.WriteString(v.String())
sb.WriteString(ucl.ObjectToString(v))
}
return sb.String(), nil

View file

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

View file

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

View file

@ -151,7 +151,7 @@ func TestBuiltins_Echo(t *testing.T) {
outW := bytes.NewBuffer(nil)
inst := New(WithOut(outW), WithTestBuiltin())
res, err := inst.Eval(ctx, tt.expr)
res, err := inst.EvalString(ctx, tt.expr)
assert.NoError(t, err)
assert.Nil(t, res)
@ -1309,7 +1309,7 @@ func TestBuiltins_Keys(t *testing.T) {
}, nil
})
res, err := inst.Eval(ctx, tt.expr)
res, err := inst.EvalString(ctx, tt.expr)
assert.NoError(t, err)
assert.Len(t, res, len(tt.wantItems))
for _, i := range tt.wantItems {
@ -1349,7 +1349,7 @@ func TestBuiltins_Filter(t *testing.T) {
inst := New(WithOut(outW), WithTestBuiltin())
res, err := inst.Eval(ctx, tt.expr)
res, err := inst.EvalString(ctx, tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
@ -1375,7 +1375,7 @@ func TestBuiltins_Reduce(t *testing.T) {
inst := New(WithOut(outW), WithTestBuiltin())
res, err := inst.Eval(ctx, tt.expr)
res, err := inst.EvalString(ctx, tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
@ -1404,7 +1404,7 @@ func TestBuiltins_Head(t *testing.T) {
inst := New(WithOut(outW), WithTestBuiltin())
res, err := inst.Eval(ctx, tt.expr)
res, err := inst.EvalString(ctx, tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
@ -1459,7 +1459,7 @@ func TestBuiltins_LtLeGtLe(t *testing.T) {
inst.SetVar("true", true)
inst.SetVar("false", false)
eqRes, err := inst.Eval(ctx, tt.expr)
eqRes, err := inst.EvalString(ctx, tt.expr)
if tt.wantErr {
assert.Error(t, err)
@ -1527,11 +1527,11 @@ func TestBuiltins_EqNe(t *testing.T) {
inst.SetVar("true", true)
inst.SetVar("false", false)
eqRes, err := inst.Eval(ctx, tt.expr)
eqRes, err := inst.EvalString(ctx, tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, eqRes)
neRes, err := inst.Eval(ctx, strings.ReplaceAll(tt.expr, "eq", "ne"))
neRes, err := inst.EvalString(ctx, strings.ReplaceAll(tt.expr, "eq", "ne"))
assert.NoError(t, err)
assert.Equal(t, !tt.want, neRes)
})
@ -1562,7 +1562,7 @@ func TestBuiltins_Str(t *testing.T) {
inst := New(WithOut(outW), WithTestBuiltin())
eqRes, err := inst.Eval(ctx, tt.expr)
eqRes, err := inst.EvalString(ctx, tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, eqRes)
})
@ -1598,7 +1598,7 @@ func TestBuiltins_Int(t *testing.T) {
inst := New(WithOut(outW), WithTestBuiltin())
eqRes, err := inst.Eval(ctx, tt.expr)
eqRes, err := inst.EvalString(ctx, tt.expr)
if tt.wantErr {
assert.Error(t, err)
} else {
@ -1654,7 +1654,7 @@ func TestBuiltins_AddSubMupDivMod(t *testing.T) {
inst := New(WithOut(outW), WithTestBuiltin())
eqRes, err := inst.Eval(ctx, tt.expr)
eqRes, err := inst.EvalString(ctx, tt.expr)
if tt.wantErr {
assert.Error(t, err)
} else {
@ -1705,7 +1705,7 @@ func TestBuiltins_AndOrNot(t *testing.T) {
inst.SetVar("true", true)
inst.SetVar("false", false)
eqRes, err := inst.Eval(ctx, tt.expr)
eqRes, err := inst.EvalString(ctx, tt.expr)
if tt.wantErr {
assert.Error(t, err)
} else {
@ -1742,7 +1742,45 @@ func TestBuiltins_Cat(t *testing.T) {
inst.SetVar("true", true)
inst.SetVar("false", false)
eqRes, err := inst.Eval(ctx, tt.expr)
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.Equal(t, tt.want, eqRes)
})
@ -1750,7 +1788,7 @@ func TestBuiltins_Cat(t *testing.T) {
}
func evalAndDisplay(ctx context.Context, inst *Inst, expr string) error {
res, err := inst.eval(ctx, expr)
res, err := inst.eval(ctx, strings.NewReader(expr), evalOptions{})
if err != nil {
return err
}

View file

@ -1,18 +1,17 @@
package ucl
type evalCtx struct {
root *evalCtx
parent *evalCtx
commands map[string]invokable
macros map[string]macroable
vars map[string]Object
pseudoVars map[string]pseudoVar
root *evalCtx
parent *evalCtx
commands map[string]invokable
macros map[string]macroable
vars map[string]Object
pseudoVars map[string]pseudoVar
userCommandFrame bool // Frame to use for user-defined commands
}
func (ec *evalCtx) forkAndIsolate() *evalCtx {
newEc := &evalCtx{parent: ec}
newEc.root = newEc
return newEc
return &evalCtx{parent: ec, root: ec.root, userCommandFrame: true}
}
func (ec *evalCtx) fork() *evalCtx {
@ -27,6 +26,25 @@ func (ec *evalCtx) addCmd(name string, inv invokable) {
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) {
if ec.root.macros == nil {
ec.root.macros = make(map[string]macroable)
@ -61,6 +79,9 @@ func (ec *evalCtx) setOrDefineVar(name string, val Object) {
func (ec *evalCtx) getVar(name string) (Object, bool) {
if ec.vars == nil {
if ec.parent == nil {
return nil, false
}
return ec.parent.getVar(name)
}

View file

@ -241,7 +241,7 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec
return mph.get(ctx, *n.PseudoVar)
}
return nil, errors.New("unknown pseudo-variable: " + *n.Var)
return nil, errors.New("unknown pseudo-variable: " + *n.PseudoVar)
case n.MaybeSub != nil:
sub := n.MaybeSub.Sub
if sub == nil {
@ -294,11 +294,11 @@ func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astList
if loh.EmptyList {
return &ListObject{}, nil
} else if loh.EmptyHash {
return hashObject{}, nil
return HashObject{}, nil
}
if firstIsHash := loh.Elements[0].Right != nil; firstIsHash {
h := hashObject{}
h := HashObject{}
for _, el := range loh.Elements {
if el.Right == nil {
return nil, errors.New("miss-match of lists and hash")

View file

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

View file

@ -3,6 +3,7 @@ package ucl_test
import (
"bytes"
"context"
"strings"
"ucl.lmika.dev/ucl"
"github.com/stretchr/testify/assert"
@ -20,7 +21,7 @@ func TestInst_Eval(t *testing.T) {
{desc: "simple string", expr: `firstarg "hello"`, want: "hello"},
{desc: "simple int 1", expr: `firstarg 123`, want: 123},
{desc: "simple int 2", expr: `firstarg -234`, want: -234},
{desc: "simple ident", expr: `firstarg a-test`, want: "a-test"},
{desc: "simple ident 1", expr: `firstarg a-test`, want: "a-test"},
// String interpolation
{desc: "interpolate string 1", expr: `$what = "world" ; firstarg "hello $what"`, want: "hello world"},
@ -113,7 +114,7 @@ func TestInst_Eval(t *testing.T) {
outW := bytes.NewBuffer(nil)
inst := ucl.New(ucl.WithOut(outW), ucl.WithTestBuiltin())
res, err := inst.Eval(ctx, tt.expr)
res, err := inst.EvalString(ctx, tt.expr)
if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr)
@ -129,6 +130,117 @@ 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) {
tests := []struct {
desc string
@ -155,7 +267,7 @@ func TestInst_SetPseudoVar(t *testing.T) {
inst.SetPseudoVar("bar", bar)
inst.SetMissingPseudoVarHandler(missingPseudoVarType{})
res, err := inst.Eval(t.Context(), tt.expr)
res, err := inst.EvalString(t.Context(), tt.expr)
if tt.wantErr {
assert.Error(t, err)

View file

@ -110,9 +110,9 @@ func (i iteratorObject) Truthy() bool {
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 {
return "[:]"
}
@ -131,19 +131,19 @@ func (s hashObject) String() string {
return sb.String()
}
func (s hashObject) Truthy() bool {
func (s HashObject) Truthy() bool {
return len(s) > 0
}
func (s hashObject) Len() int {
func (s HashObject) Len() int {
return len(s)
}
func (s hashObject) Value(k string) Object {
func (s HashObject) Value(k string) Object {
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 {
if err := fn(k, v); err != nil {
return err
@ -219,7 +219,7 @@ func toGoValue(obj Object) (interface{}, bool) {
xs = append(xs, x)
}
return xs, true
case hashObject:
case HashObject:
xs := make(map[string]interface{})
for k, va := range v {
x, ok := toGoValue(va)
@ -680,3 +680,10 @@ func isBreakErr(err error) bool {
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

@ -81,6 +81,10 @@ func (inst *Inst) SetBuiltin(name string, fn BuiltinHandler) {
inst.rootEC.addCmd(name, userBuiltin{fn: fn})
}
func (inst *Inst) SetBuiltinInvokable(name string, fn Invokable) {
inst.rootEC.addCmd(name, fn.inv)
}
type userBuiltin struct {
fn func(ctx context.Context, args CallArgs) (any, error)
}

View file

@ -24,7 +24,7 @@ func TestInst_SetBuiltin(t *testing.T) {
return x + y, nil
})
res, err := inst.Eval(context.Background(), `add2 "Hello, " "World"`)
res, err := inst.EvalString(context.Background(), `add2 "Hello, " "World"`)
assert.NoError(t, err)
assert.Equal(t, "Hello, World", res)
})
@ -44,7 +44,7 @@ func TestInst_SetBuiltin(t *testing.T) {
return x + y, nil
})
res, err := inst.Eval(context.Background(), `add2 "Hello, " "World"`)
res, err := inst.EvalString(context.Background(), `add2 "Hello, " "World"`)
assert.NoError(t, err)
assert.Equal(t, "Hello, World", res)
})
@ -85,7 +85,7 @@ func TestInst_SetBuiltin(t *testing.T) {
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
res, err := inst.Eval(context.Background(), tt.expr)
res, err := inst.EvalString(context.Background(), tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
@ -108,7 +108,7 @@ func TestInst_SetBuiltin(t *testing.T) {
return pair{x, y}, nil
})
res, err := inst.Eval(context.Background(), `add2 "Hello" "World"`)
res, err := inst.EvalString(context.Background(), `add2 "Hello" "World"`)
assert.NoError(t, err)
assert.Equal(t, pair{"Hello", "World"}, res)
})
@ -149,7 +149,7 @@ func TestInst_SetBuiltin(t *testing.T) {
return x.x + ":" + x.y, nil
})
res, err := inst.Eval(context.Background(), tt.expr)
res, err := inst.EvalString(context.Background(), tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
@ -176,7 +176,7 @@ func TestInst_SetBuiltin(t *testing.T) {
return []string{"1", "2", "3"}, nil
})
res, err := inst.Eval(context.Background(), tt.expr)
res, err := inst.EvalString(context.Background(), tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
assert.Equal(t, tt.wantOut, outW.String())
@ -206,11 +206,11 @@ func TestCallArgs_Bind(t *testing.T) {
return ds.DoString(), nil
})
va, err := inst.Eval(ctx, `dostr (sa)`)
va, err := inst.EvalString(ctx, `dostr (sa)`)
assert.NoError(t, err)
assert.Equal(t, "do string A: a val", va)
vb, err := inst.Eval(ctx, `dostr (sb)`)
vb, err := inst.EvalString(ctx, `dostr (sb)`)
assert.NoError(t, err)
assert.Equal(t, "do string B: foo bar", vb)
})
@ -240,7 +240,7 @@ func TestCallArgs_Bind(t *testing.T) {
return fmt.Sprintf("[%v]", v), nil
})
res, err := inst.Eval(ctx, tt.eval)
res, err := inst.EvalString(ctx, tt.eval)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
@ -295,7 +295,7 @@ func TestCallArgs_CanBind(t *testing.T) {
return nil, nil
})
_, err := inst.Eval(ctx, tt.eval)
_, err := inst.EvalString(ctx, tt.eval)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
@ -332,7 +332,7 @@ func TestCallArgs_CanBind(t *testing.T) {
return h.Value(k), nil
})
res, err := inst.Eval(ctx, tt.eval)
res, err := inst.EvalString(ctx, tt.eval)
if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, res)
@ -370,7 +370,7 @@ func TestCallArgs_CanBind(t *testing.T) {
ctx := context.Background()
res, err := inst.Eval(ctx, `wrap { |x| toUpper $x }`)
res, err := inst.EvalString(ctx, `wrap { |x| toUpper $x }`)
assert.NoError(t, err)
assert.Equal(t, "[[HELLO]]", res)
})
@ -401,7 +401,7 @@ func TestCallArgs_CanBind(t *testing.T) {
assert.NoError(t, err)
assert.Nil(t, before)
res, err := inst.Eval(ctx, `wrap { |x| toUpper $x }`)
res, err := inst.EvalString(ctx, `wrap { |x| toUpper $x }`)
assert.NoError(t, err)
assert.Nil(t, res)
@ -437,7 +437,7 @@ func TestCallArgs_MissingCommandHandler(t *testing.T) {
return fmt.Sprintf("was %v", name), nil
}))
res, err := inst.Eval(ctx, tt.eval)
res, err := inst.EvalString(ctx, tt.eval)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
@ -460,27 +460,27 @@ func TestCallArgs_IsTopLevel(t *testing.T) {
return nil, nil
})
_, err := inst.Eval(ctx, `lvl "one"`)
_, err := inst.EvalString(ctx, `lvl "one"`)
assert.NoError(t, err)
assert.True(t, res["one"])
_, err = inst.Eval(ctx, `echo (lvl "two")`)
_, err = inst.EvalString(ctx, `echo (lvl "two")`)
assert.NoError(t, err)
assert.True(t, res["two"])
_, err = inst.Eval(ctx, `proc doLvl { |n| lvl $n } ; doLvl "three"`)
_, err = inst.EvalString(ctx, `proc doLvl { |n| lvl $n } ; doLvl "three"`)
assert.NoError(t, err)
assert.False(t, res["three"])
_, err = inst.Eval(ctx, `doLvl "four"`)
_, err = inst.EvalString(ctx, `doLvl "four"`)
assert.NoError(t, err)
assert.False(t, res["four"])
_, err = inst.Eval(ctx, `["a"] | map { |x| doLvl "five" ; $x }`)
_, err = inst.EvalString(ctx, `["a"] | map { |x| doLvl "five" ; $x }`)
assert.NoError(t, err)
assert.False(t, res["five"])
_, err = inst.Eval(ctx, `if 1 { lvl "six" }`)
_, err = inst.EvalString(ctx, `if 1 { lvl "six" }`)
assert.NoError(t, err)
assert.True(t, res["six"])
})