Added the foreach loop

This commit is contained in:
Leon Mika 2024-04-16 22:28:12 +10:00
parent ea31e568b6
commit c0f5cfb180
6 changed files with 83 additions and 6 deletions

View file

@ -30,7 +30,8 @@ type astListOrHash struct {
} }
type astBlock struct { type astBlock struct {
Statements []*astStatements `parser:"LC NL? @@ NL? RC"` Names []string `parser:"LC NL? (PIPE @Ident+ PIPE NL?)?"`
Statements []*astStatements `parser:"@@ NL? RC"`
} }
type astCmdArg struct { type astCmdArg struct {

View file

@ -123,7 +123,7 @@ func ifBuiltin(ctx context.Context, args macroArgs) (object, error) {
} }
if guard, err := args.evalArg(ctx, 0); err == nil && isTruthy(guard) { if guard, err := args.evalArg(ctx, 0); err == nil && isTruthy(guard) {
return args.evalBlock(ctx, 1) return args.evalBlock(ctx, 1, nil, false)
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
@ -137,7 +137,7 @@ func ifBuiltin(ctx context.Context, args macroArgs) (object, error) {
} }
if guard, err := args.evalArg(ctx, 0); err == nil && isTruthy(guard) { if guard, err := args.evalArg(ctx, 0); err == nil && isTruthy(guard) {
return args.evalBlock(ctx, 1) return args.evalBlock(ctx, 1, nil, false)
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
@ -146,7 +146,7 @@ func ifBuiltin(ctx context.Context, args macroArgs) (object, error) {
} }
if args.identIs(ctx, 0, "else") && args.nargs() > 1 { if args.identIs(ctx, 0, "else") && args.nargs() > 1 {
return args.evalBlock(ctx, 1) return args.evalBlock(ctx, 1, nil, false)
} else if args.nargs() == 0 { } else if args.nargs() == 0 {
// no elif or else // no elif or else
return nil, nil return nil, nil
@ -154,3 +154,36 @@ func ifBuiltin(ctx context.Context, args macroArgs) (object, error) {
return nil, errors.New("malformed if-elif-else") return nil, errors.New("malformed if-elif-else")
} }
func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) {
if args.nargs() < 2 {
return nil, errors.New("need at least 2 arguments")
}
items, err := args.evalArg(ctx, 0)
if err != nil {
return nil, err
}
var last object
switch t := items.(type) {
case listObject:
for _, v := range t {
last, err = args.evalBlock(ctx, 1, []object{v}, true) // TO INCLUDE: the index
if err != nil {
return nil, err
}
}
case hashObject:
for k, v := range t {
last, err = args.evalBlock(ctx, 1, []object{strObject(k), v}, true)
if err != nil {
return nil, err
}
}
// TODO: streams
}
return last, nil
}

View file

@ -7,6 +7,10 @@ type evalCtx struct {
vars map[string]object vars map[string]object
} }
func (ec *evalCtx) fork() *evalCtx {
return &evalCtx{parent: ec}
}
func (ec *evalCtx) addCmd(name string, inv invokable) { func (ec *evalCtx) addCmd(name string, inv invokable) {
if ec.commands == nil { if ec.commands == nil {
ec.commands = make(map[string]invokable) ec.commands = make(map[string]invokable)

View file

@ -32,6 +32,7 @@ func New(opts ...InstOption) *Inst {
rootEC.addCmd("cat", invokableFunc(catBuiltin)) rootEC.addCmd("cat", invokableFunc(catBuiltin))
rootEC.addMacro("if", macroFunc(ifBuiltin)) rootEC.addMacro("if", macroFunc(ifBuiltin))
rootEC.addMacro("foreach", macroFunc(foreachBuiltin))
//rootEC.addCmd("testTimebomb", invokableStreamFunc(errorTestBuiltin)) //rootEC.addCmd("testTimebomb", invokableStreamFunc(errorTestBuiltin))

View file

@ -123,7 +123,7 @@ func (ma macroArgs) evalArg(ctx context.Context, n int) (object, error) {
return ma.eval.evalArg(ctx, ma.ec, ma.ast.Args[ma.argShift+n]) return ma.eval.evalArg(ctx, ma.ec, ma.ast.Args[ma.argShift+n])
} }
func (ma macroArgs) evalBlock(ctx context.Context, n int) (object, error) { func (ma macroArgs) evalBlock(ctx context.Context, n int, args []object, pushScope bool) (object, error) {
obj, err := ma.evalArg(ctx, n) obj, err := ma.evalArg(ctx, n)
if err != nil { if err != nil {
return nil, err return nil, err
@ -134,7 +134,17 @@ func (ma macroArgs) evalBlock(ctx context.Context, n int) (object, error) {
return nil, errors.New("not a block object") return nil, errors.New("not a block object")
} }
return ma.eval.evalBlock(ctx, ma.ec, block.block) ec := ma.ec
if pushScope {
ec = ec.fork()
}
for i, n := range block.block.Names {
if i < len(args) {
ec.setVar(n, args[i])
}
}
return ma.eval.evalBlock(ctx, ec, block.block)
} }
type invocationArgs struct { type invocationArgs struct {

View file

@ -172,3 +172,31 @@ func TestBuiltins_If(t *testing.T) {
}) })
} }
} }
func TestBuiltins_ForEach(t *testing.T) {
tests := []struct {
desc string
expr string
want string
}{
{desc: "iterate over list", expr: `
foreach ["1" "2" "3"] { |v|
echo $v
}`, want: "1\n2\n3\n(nil)\n"},
{desc: "iterate over map", expr: `
foreach [a:"1"] { |k v| echo $k "=" $v }`, want: "a=1\n(nil)\n"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
ctx := context.Background()
outW := bytes.NewBuffer(nil)
inst := New(WithOut(outW), WithTestBuiltin())
err := inst.EvalAndDisplay(ctx, tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, outW.String())
})
}
}