Added macros and the if macro
This commit is contained in:
parent
730dc46095
commit
d6cc449b40
|
@ -7,14 +7,19 @@ import (
|
|||
)
|
||||
|
||||
type astLiteral struct {
|
||||
Str *string `parser:"@String"`
|
||||
Ident *string `parser:" | @Ident"`
|
||||
Str *string `parser:"@String"`
|
||||
}
|
||||
|
||||
type astBlock struct {
|
||||
Statements []*astStatements `parser:"LC NL? @@ NL? RC"`
|
||||
}
|
||||
|
||||
type astCmdArg struct {
|
||||
Literal *astLiteral `parser:"@@"`
|
||||
Ident *string `parser:"| @Ident"`
|
||||
Var *string `parser:"| DOLLAR @Ident"`
|
||||
Sub *astPipeline `parser:"| LP @@ RP"`
|
||||
Block *astBlock `parser:"| @@"`
|
||||
}
|
||||
|
||||
type astCmd struct {
|
||||
|
@ -29,29 +34,30 @@ type astPipeline struct {
|
|||
|
||||
type astStatements struct {
|
||||
First *astPipeline `parser:"@@"`
|
||||
Rest []*astPipeline `parser:"( (SEMICL | NL)+ @@ )*"` // TODO: also add support for newlines
|
||||
Rest []*astPipeline `parser:"( NL+ @@ )*"` // TODO: also add support for newlines
|
||||
}
|
||||
|
||||
type astBlock struct {
|
||||
Statements *astStatements `parser:"'{' "`
|
||||
type astScript struct {
|
||||
Statements *astStatements `parser:"NL* @@ NL*"`
|
||||
}
|
||||
|
||||
var scanner = lexer.MustStateful(lexer.Rules{
|
||||
"Root": {
|
||||
{"Whitespace", `[ ]`, nil},
|
||||
{"NL", `\n\s*`, nil},
|
||||
{"Whitespace", `[ \t]+`, nil},
|
||||
{"String", `"(\\"|[^"])*"`, nil},
|
||||
{"DOLLAR", `\$`, nil},
|
||||
{"LP", `\(`, nil},
|
||||
{"RP", `\)`, nil},
|
||||
{"SEMICL", `;`, nil},
|
||||
{"LC", `\{`, nil},
|
||||
{"RC", `\}`, nil},
|
||||
{"NL", `[;\n][; \n\t]*`, nil},
|
||||
{"PIPE", `\|`, nil},
|
||||
{"Ident", `\w+`, nil},
|
||||
},
|
||||
})
|
||||
var parser = participle.MustBuild[astStatements](participle.Lexer(scanner),
|
||||
var parser = participle.MustBuild[astScript](participle.Lexer(scanner),
|
||||
participle.Elide("Whitespace"))
|
||||
|
||||
func parse(r io.Reader) (*astStatements, error) {
|
||||
func parse(r io.Reader) (*astScript, error) {
|
||||
return parser.Parse("test", r)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package cmdlang
|
|||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
@ -14,6 +15,7 @@ func echoBuiltin(ctx context.Context, args invocationArgs) (object, error) {
|
|||
if _, err := fmt.Fprintln(args.inst.Out()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var line strings.Builder
|
||||
|
@ -83,6 +85,10 @@ func (f *fileLinesStream) String() string {
|
|||
return fmt.Sprintf("fileLinesStream{file: %v}", f.filename)
|
||||
}
|
||||
|
||||
func (f *fileLinesStream) Truthy() bool {
|
||||
return true // ??
|
||||
}
|
||||
|
||||
func (f *fileLinesStream) next() (object, error) {
|
||||
var err error
|
||||
|
||||
|
@ -111,25 +117,40 @@ func (f *fileLinesStream) close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
func errorTestBuiltin(ctx context.Context, inStream stream, args invocationArgs) (object, error) {
|
||||
return &timeBombStream{inStream, 2}, nil
|
||||
}
|
||||
|
||||
type timeBombStream struct {
|
||||
in stream
|
||||
x int
|
||||
}
|
||||
|
||||
func (ms *timeBombStream) next() (object, error) {
|
||||
if ms.x > 0 {
|
||||
ms.x--
|
||||
return ms.in.next()
|
||||
func ifBuiltin(ctx context.Context, args macroArgs) (object, error) {
|
||||
if args.nargs() < 2 {
|
||||
return nil, errors.New("need at least 2 arguments")
|
||||
}
|
||||
return nil, errors.New("BOOM")
|
||||
}
|
||||
|
||||
func (ms *timeBombStream) close() error {
|
||||
return ms.in.close()
|
||||
if guard, err := args.evalArg(ctx, 0); err == nil && isTruthy(guard) {
|
||||
return args.evalBlock(ctx, 1)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args.shift(2)
|
||||
for args.identIs(ctx, 0, "elif") {
|
||||
args.shift(1)
|
||||
|
||||
if args.nargs() < 2 {
|
||||
return nil, errors.New("need at least 2 arguments")
|
||||
}
|
||||
|
||||
if guard, err := args.evalArg(ctx, 0); err == nil && isTruthy(guard) {
|
||||
return args.evalBlock(ctx, 1)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args.shift(2)
|
||||
}
|
||||
|
||||
if args.identIs(ctx, 0, "else") && args.nargs() > 1 {
|
||||
return args.evalBlock(ctx, 1)
|
||||
} else if args.nargs() == 0 {
|
||||
// no elif or else
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("malformed if-elif-else")
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
package cmdlang
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type evalCtx struct {
|
||||
parent *evalCtx
|
||||
commands map[string]invokable
|
||||
macros map[string]macroable
|
||||
vars map[string]object
|
||||
}
|
||||
|
||||
|
@ -18,6 +15,14 @@ func (ec *evalCtx) addCmd(name string, inv invokable) {
|
|||
ec.commands[name] = inv
|
||||
}
|
||||
|
||||
func (ec *evalCtx) addMacro(name string, inv macroable) {
|
||||
if ec.macros == nil {
|
||||
ec.macros = make(map[string]macroable)
|
||||
}
|
||||
|
||||
ec.macros[name] = inv
|
||||
}
|
||||
|
||||
func (ec *evalCtx) setVar(name string, val object) {
|
||||
if ec.vars == nil {
|
||||
ec.vars = make(map[string]object)
|
||||
|
@ -39,16 +44,30 @@ func (ec *evalCtx) getVar(name string) (object, bool) {
|
|||
return nil, false
|
||||
}
|
||||
|
||||
func (ec *evalCtx) lookupCmd(name string) (invokable, error) {
|
||||
for e := ec; e != nil; e = e.parent {
|
||||
if e.commands == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if cmd, ok := e.commands[name]; ok {
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func (ec *evalCtx) lookupInvokable(name string) invokable {
|
||||
if ec == nil {
|
||||
return nil
|
||||
}
|
||||
return nil, errors.New("name " + name + " not found")
|
||||
|
||||
for e := ec; e != nil; e = e.parent {
|
||||
if cmd, ok := e.commands[name]; ok {
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
|
||||
return ec.parent.lookupInvokable(name)
|
||||
}
|
||||
|
||||
func (ec *evalCtx) lookupMacro(name string) macroable {
|
||||
if ec == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for e := ec; e != nil; e = e.parent {
|
||||
if cmd, ok := e.macros[name]; ok {
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
|
||||
return ec.parent.lookupMacro(name)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,22 @@ type evaluator struct {
|
|||
inst *Inst
|
||||
}
|
||||
|
||||
func (e evaluator) evalBlock(ctx context.Context, ec *evalCtx, n *astBlock) (lastRes object, err error) {
|
||||
// TODO: push scope?
|
||||
|
||||
for _, s := range n.Statements {
|
||||
lastRes, err = e.evalStatement(ctx, ec, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return lastRes, nil
|
||||
}
|
||||
|
||||
func (e evaluator) evalScript(ctx context.Context, ec *evalCtx, n *astScript) (lastRes object, err error) {
|
||||
return e.evalStatement(ctx, ec, n.Statements)
|
||||
}
|
||||
|
||||
func (e evaluator) evalStatement(ctx context.Context, ec *evalCtx, n *astStatements) (object, error) {
|
||||
res, err := e.evalPipeline(ctx, ec, n.First)
|
||||
if err != nil {
|
||||
|
@ -60,11 +76,16 @@ func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline
|
|||
}
|
||||
|
||||
func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentStream stream, ast *astCmd) (object, error) {
|
||||
cmd, err := ec.lookupCmd(ast.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if cmd := ec.lookupInvokable(ast.Name); cmd != nil {
|
||||
return e.evalInvokable(ctx, ec, currentStream, ast, cmd)
|
||||
} else if macro := ec.lookupMacro(ast.Name); macro != nil {
|
||||
return e.evalMacro(ctx, ec, currentStream, ast, macro)
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown command")
|
||||
}
|
||||
|
||||
func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentStream stream, ast *astCmd, cmd invokable) (object, error) {
|
||||
args, err := slices.MapWithError(ast.Args, func(a astCmdArg) (object, error) {
|
||||
return e.evalArg(ctx, ec, a)
|
||||
})
|
||||
|
@ -87,18 +108,30 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentStream strea
|
|||
return cmd.invoke(ctx, invArgs)
|
||||
}
|
||||
|
||||
func (e evaluator) evalMacro(ctx context.Context, ec *evalCtx, currentStream stream, ast *astCmd, cmd macroable) (object, error) {
|
||||
return cmd.invokeMacro(ctx, macroArgs{
|
||||
eval: e,
|
||||
ec: ec,
|
||||
currentStream: currentStream,
|
||||
ast: ast,
|
||||
})
|
||||
}
|
||||
|
||||
func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (object, error) {
|
||||
switch {
|
||||
case n.Literal != nil:
|
||||
return e.evalLiteral(ctx, ec, n.Literal)
|
||||
case n.Ident != nil:
|
||||
return strObject(*n.Ident), nil
|
||||
case n.Var != nil:
|
||||
v, ok := ec.getVar(*n.Var)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown variable %s", *n.Var)
|
||||
if v, ok := ec.getVar(*n.Var); ok {
|
||||
return v, nil
|
||||
}
|
||||
return v, nil
|
||||
return nil, nil
|
||||
case n.Sub != nil:
|
||||
return e.evalSub(ctx, ec, n.Sub)
|
||||
case n.Block != nil:
|
||||
return blockObject{block: n.Block}, nil
|
||||
}
|
||||
return nil, errors.New("unhandled arg type")
|
||||
}
|
||||
|
@ -111,8 +144,6 @@ func (e evaluator) evalLiteral(ctx context.Context, ec *evalCtx, n *astLiteral)
|
|||
return nil, err
|
||||
}
|
||||
return strObject(uq), nil
|
||||
case n.Ident != nil:
|
||||
return strObject(*n.Ident), nil
|
||||
}
|
||||
return nil, errors.New("unhandled literal type")
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ func New(opts ...InstOption) *Inst {
|
|||
rootEC.addCmd("toUpper", invokableStreamFunc(toUpperBuiltin))
|
||||
rootEC.addCmd("cat", invokableFunc(catBuiltin))
|
||||
|
||||
rootEC.addMacro("if", macroFunc(ifBuiltin))
|
||||
|
||||
//rootEC.addCmd("testTimebomb", invokableStreamFunc(errorTestBuiltin))
|
||||
|
||||
rootEC.setVar("hello", strObject("world"))
|
||||
|
@ -76,7 +78,7 @@ func (inst *Inst) eval(ctx context.Context, expr string) (object, error) {
|
|||
|
||||
eval := evaluator{inst: inst}
|
||||
|
||||
return eval.evalStatement(ctx, inst.rootEC, ast)
|
||||
return eval.evalScript(ctx, inst.rootEC, ast)
|
||||
}
|
||||
|
||||
func (inst *Inst) EvalAndDisplay(ctx context.Context, expr string) error {
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"context"
|
||||
"github.com/lmika/cmdlang-proto/cmdlang"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
@ -38,8 +37,6 @@ func TestInst_Eval(t *testing.T) {
|
|||
{desc: "multi 1", expr: `firstarg "hello" ; firstarg "world"`, want: "world"},
|
||||
{desc: "multi 2", expr: `pipe "hello" | toUpper ; firstarg "world"`, want: "world"}, // TODO: assert for leaks
|
||||
{desc: "multi 3", expr: `set new "this is new" ; firstarg $new`, want: "this is new"},
|
||||
|
||||
{desc: "multi-line 1", expr: "echo \"Hello\" \n echo \"world\"", want: "world"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
@ -56,7 +53,7 @@ func TestInst_Eval(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestInst_Builtins_Echo(t *testing.T) {
|
||||
func TestBuiltins_Echo(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
expr string
|
||||
|
@ -65,8 +62,98 @@ func TestInst_Builtins_Echo(t *testing.T) {
|
|||
{desc: "no args", expr: `echo`, want: "\n"},
|
||||
{desc: "single arg", expr: `echo "hello"`, want: "hello\n"},
|
||||
{desc: "dual args", expr: `echo "hello " "world"`, want: "hello world\n"},
|
||||
{desc: "args to singleton stream", expr: `echo "aye" "bee" "see" | toUpper`, want: "AYEBEESEE\n"},
|
||||
{desc: "multi-line 1", expr: joinLines(`echo "Hello"`, `echo "world"`), want: "Hello\nworld"},
|
||||
{desc: "multi-line 1", expr: `
|
||||
echo "Hello"
|
||||
echo "world"
|
||||
`, want: "Hello\nworld\n"},
|
||||
{desc: "multi-line 2", expr: `
|
||||
echo "Hello"
|
||||
|
||||
|
||||
echo "world"
|
||||
`, want: "Hello\nworld\n"},
|
||||
{desc: "multi-line 3", expr: `
|
||||
|
||||
;;;
|
||||
echo "Hello"
|
||||
;
|
||||
|
||||
echo "world"
|
||||
;
|
||||
`, want: "Hello\nworld\n"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
outW := bytes.NewBuffer(nil)
|
||||
|
||||
inst := cmdlang.New(cmdlang.WithOut(outW), cmdlang.WithTestBuiltin())
|
||||
res, err := inst.Eval(ctx, tt.expr)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, res)
|
||||
assert.Equal(t, tt.want, outW.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltins_If(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
expr string
|
||||
want string
|
||||
}{
|
||||
{desc: "single then", expr: `
|
||||
set x "Hello"
|
||||
if $x {
|
||||
echo "true"
|
||||
}`, want: "true\n(nil)\n"},
|
||||
{desc: "single then and else", expr: `
|
||||
set x "Hello"
|
||||
if $x {
|
||||
echo "true"
|
||||
} else {
|
||||
echo "false"
|
||||
}`, want: "true\n(nil)\n"},
|
||||
{desc: "single then, elif and else", expr: `
|
||||
set x "Hello"
|
||||
if $y {
|
||||
echo "y is true"
|
||||
} elif $x {
|
||||
echo "x is true"
|
||||
} else {
|
||||
echo "nothings x"
|
||||
}`, want: "x is true\n(nil)\n"},
|
||||
{desc: "single then and elif, no else", expr: `
|
||||
set x "Hello"
|
||||
if $y {
|
||||
echo "y is true"
|
||||
} elif $x {
|
||||
echo "x is true"
|
||||
}`, want: "x is true\n(nil)\n"},
|
||||
{desc: "single then, two elif, and else", expr: `
|
||||
set x "Hello"
|
||||
if $z {
|
||||
echo "z is true"
|
||||
} elif $y {
|
||||
echo "y is true"
|
||||
} elif $x {
|
||||
echo "x is true"
|
||||
}`, want: "x is true\n(nil)\n"},
|
||||
{desc: "single then, two elif, and else, expecting else", expr: `
|
||||
if $z {
|
||||
echo "z is true"
|
||||
} elif $y {
|
||||
echo "y is true"
|
||||
} elif $x {
|
||||
echo "x is true"
|
||||
} else {
|
||||
echo "none is true"
|
||||
}`, want: "none is true\n(nil)\n"},
|
||||
{desc: "compressed then", expr: `set 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 if", expr: `if $x { echo "x" } elif $y { echo "y" } else { echo "false" }`, want: "false\n(nil)\n"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
@ -82,7 +169,3 @@ func TestInst_Builtins_Echo(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func joinLines(ls ...string) string {
|
||||
return strings.Join(ls, "\n")
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
type object interface {
|
||||
String() string
|
||||
Truthy() bool
|
||||
}
|
||||
|
||||
type strObject string
|
||||
|
@ -17,6 +18,10 @@ func (s strObject) String() string {
|
|||
return string(s)
|
||||
}
|
||||
|
||||
func (s strObject) Truthy() bool {
|
||||
return string(s) != ""
|
||||
}
|
||||
|
||||
func toGoValue(obj object) (interface{}, bool) {
|
||||
switch v := obj.(type) {
|
||||
case nil:
|
||||
|
@ -28,6 +33,57 @@ func toGoValue(obj object) (interface{}, bool) {
|
|||
return nil, false
|
||||
}
|
||||
|
||||
type macroArgs struct {
|
||||
eval evaluator
|
||||
ec *evalCtx
|
||||
currentStream stream
|
||||
ast *astCmd
|
||||
argShift int
|
||||
}
|
||||
|
||||
func (ma macroArgs) nargs() int {
|
||||
return len(ma.ast.Args[ma.argShift:])
|
||||
}
|
||||
|
||||
func (ma *macroArgs) shift(n int) {
|
||||
ma.argShift += n
|
||||
}
|
||||
|
||||
func (ma macroArgs) identIs(ctx context.Context, n int, expectedIdent string) bool {
|
||||
if n >= len(ma.ast.Args[ma.argShift:]) {
|
||||
return false
|
||||
}
|
||||
|
||||
lit := ma.ast.Args[ma.argShift+n].Ident
|
||||
if lit == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return *lit == expectedIdent
|
||||
}
|
||||
|
||||
func (ma macroArgs) evalArg(ctx context.Context, n int) (object, error) {
|
||||
if n >= len(ma.ast.Args[ma.argShift:]) {
|
||||
return nil, errors.New("not enough arguments") // FIX
|
||||
}
|
||||
|
||||
return ma.eval.evalArg(ctx, ma.ec, ma.ast.Args[ma.argShift+n])
|
||||
}
|
||||
|
||||
func (ma macroArgs) evalBlock(ctx context.Context, n int) (object, error) {
|
||||
obj, err := ma.evalArg(ctx, n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, ok := obj.(blockObject)
|
||||
if !ok {
|
||||
return nil, errors.New("not a block object")
|
||||
}
|
||||
|
||||
return ma.eval.evalBlock(ctx, ma.ec, block.block)
|
||||
}
|
||||
|
||||
type invocationArgs struct {
|
||||
inst *Inst
|
||||
ec *evalCtx
|
||||
|
@ -58,6 +114,10 @@ type invokable interface {
|
|||
invoke(ctx context.Context, args invocationArgs) (object, error)
|
||||
}
|
||||
|
||||
type macroable interface {
|
||||
invokeMacro(ctx context.Context, args macroArgs) (object, error)
|
||||
}
|
||||
|
||||
type streamInvokable interface {
|
||||
invokable
|
||||
invokeWithStream(context.Context, stream, invocationArgs) (object, error)
|
||||
|
@ -78,3 +138,28 @@ func (i invokableStreamFunc) invoke(ctx context.Context, args invocationArgs) (o
|
|||
func (i invokableStreamFunc) invokeWithStream(ctx context.Context, inStream stream, args invocationArgs) (object, error) {
|
||||
return i(ctx, inStream, args)
|
||||
}
|
||||
|
||||
type blockObject struct {
|
||||
block *astBlock
|
||||
}
|
||||
|
||||
func (bo blockObject) String() string {
|
||||
return "block"
|
||||
}
|
||||
|
||||
func (bo blockObject) Truthy() bool {
|
||||
return len(bo.block.Statements) > 0
|
||||
}
|
||||
|
||||
type macroFunc func(ctx context.Context, args macroArgs) (object, error)
|
||||
|
||||
func (i macroFunc) invokeMacro(ctx context.Context, args macroArgs) (object, error) {
|
||||
return i(ctx, args)
|
||||
}
|
||||
|
||||
func isTruthy(obj object) bool {
|
||||
if obj == nil {
|
||||
return false
|
||||
}
|
||||
return obj.Truthy()
|
||||
}
|
||||
|
|
|
@ -74,6 +74,10 @@ func (s *singletonStream) String() string {
|
|||
return s.t.String()
|
||||
}
|
||||
|
||||
func (s *singletonStream) Truthy() bool {
|
||||
return !s.consumed
|
||||
}
|
||||
|
||||
func (s *singletonStream) next() (object, error) {
|
||||
if s.consumed {
|
||||
return nil, io.EOF
|
||||
|
@ -93,6 +97,10 @@ func (s *listIterStream) String() string {
|
|||
return fmt.Sprintf("listIterStream{list: %v}", s.list)
|
||||
}
|
||||
|
||||
func (s *listIterStream) Truthy() bool {
|
||||
return len(s.list) > s.cusr
|
||||
}
|
||||
|
||||
func (s *listIterStream) next() (o object, err error) {
|
||||
if s.cusr >= len(s.list) {
|
||||
return nil, io.EOF
|
||||
|
@ -115,6 +123,10 @@ func (ms mapFilterStream) String() string {
|
|||
return fmt.Sprintf("mapFilterStream{in: %v}", ms.in)
|
||||
}
|
||||
|
||||
func (ms mapFilterStream) Truthy() bool {
|
||||
return true // ???
|
||||
}
|
||||
|
||||
func (ms mapFilterStream) next() (object, error) {
|
||||
for {
|
||||
u, err := ms.in.next()
|
||||
|
|
Loading…
Reference in a new issue