Working out the tests

This commit is contained in:
Leon Mika 2024-04-18 22:24:19 +10:00
parent 1cf10478e2
commit 7e06f38802
7 changed files with 189 additions and 9 deletions

View File

@ -62,6 +62,23 @@ func toUpperBuiltin(ctx context.Context, inStream stream, args invocationArgs) (
}, nil
}
func eqBuiltin(ctx context.Context, args invocationArgs) (object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
l := args.args[0]
r := args.args[1]
switch lv := l.(type) {
case strObject:
if rv, ok := r.(strObject); ok {
return boolObject(lv == rv), nil
}
}
return boolObject(false), nil
}
func concatBuiltin(ctx context.Context, args invocationArgs) (object, error) {
var sb strings.Builder
@ -122,6 +139,23 @@ func mapBuiltin(ctx context.Context, inStream stream, args invocationArgs) (obje
}, nil
}
func firstBuiltin(ctx context.Context, inStream stream, args invocationArgs) (object, error) {
args, strm, err := args.streamableSource(inStream)
if err != nil {
return nil, err
}
defer strm.close()
x, err := strm.next()
if errors.Is(err, io.EOF) {
return nil, nil
} else if err != nil {
return x, nil
}
return x, nil
}
type fileLinesStream struct {
filename string
f *os.File

View File

@ -8,24 +8,30 @@ type evalCtx struct {
vars map[string]object
}
func (ec *evalCtx) forkAndIsolate() *evalCtx {
newEc := &evalCtx{parent: ec}
newEc.root = newEc
return newEc
}
func (ec *evalCtx) fork() *evalCtx {
return &evalCtx{parent: ec, root: ec.root}
}
func (ec *evalCtx) addCmd(name string, inv invokable) {
if ec.commands == nil {
ec.commands = make(map[string]invokable)
if ec.root.commands == nil {
ec.root.commands = make(map[string]invokable)
}
ec.commands[name] = inv
ec.root.commands[name] = inv
}
func (ec *evalCtx) addMacro(name string, inv macroable) {
if ec.macros == nil {
ec.macros = make(map[string]macroable)
if ec.root.macros == nil {
ec.root.macros = make(map[string]macroable)
}
ec.macros[name] = inv
ec.root.macros[name] = inv
}
func (ec *evalCtx) setVar(name string, val object) {

View File

@ -3,6 +3,7 @@ package cmdlang
import (
"context"
"errors"
"log"
"strconv"
)
@ -76,6 +77,7 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentStream strea
switch {
case ast.Name.Ident != nil:
name := *ast.Name.Ident
log.Printf("--> invoking: %v", name)
// Regular command
if cmd := ec.lookupInvokable(name); cmd != nil {
@ -83,7 +85,7 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentStream strea
} else if macro := ec.lookupMacro(name); macro != nil {
return e.evalMacro(ctx, ec, currentStream, ast, macro)
} else {
return nil, errors.New("unknown command")
return nil, errors.New("unknown command: " + name)
}
case len(ast.Args) > 0:
nameElem, err := e.evalArg(ctx, ec, ast.Name)

View File

@ -34,7 +34,9 @@ func New(opts ...InstOption) *Inst {
rootEC.addCmd("call", invokableFunc(callBuiltin))
rootEC.addCmd("map", invokableStreamFunc(mapBuiltin))
rootEC.addCmd("head", invokableStreamFunc(firstBuiltin))
rootEC.addCmd("eq", invokableFunc(eqBuiltin))
rootEC.addCmd("cat", invokableFunc(concatBuiltin))
rootEC.addMacro("if", macroFunc(ifBuiltin))
@ -86,6 +88,7 @@ func (inst *Inst) eval(ctx context.Context, expr string) (object, error) {
eval := evaluator{inst: inst}
// TODO: this should be a separate forkAndIsolate() session
return eval.evalScript(ctx, inst.rootEC, ast)
}

View File

@ -42,7 +42,7 @@ func TestInst_Eval(t *testing.T) {
// Lists
{desc: "list 1", expr: `firstarg ["1" "2" "3"]`, want: []any{"1", "2", "3"}},
{desc: "list 2", expr: `set one "one" ; firstarg [$one (pipe "two" | toUpper) "three"]`, want: []any{"one", "TWO", "three"}},
{desc: "list 2", expr: `set one "one" ; firstarg [$one (pipe "two" | toUpper | head) "three"]`, want: []any{"one", "TWO", "three"}},
{desc: "list 3", expr: `firstarg []`, want: []any{}},
// Maps
@ -52,7 +52,7 @@ func TestInst_Eval(t *testing.T) {
set one "one" ; set n1 "1"
firstarg [
$one:$n1
(firstarg "two" | toUpper):(firstarg "2" | toUpper)
(firstarg "two" | toUpper | head):(firstarg "2" | toUpper | head)
three:"3"
]`, want: map[string]any{"one": "1", "TWO": "2", "three": "3"}},
{desc: "map 4", expr: `firstarg [:]`, want: map[string]any{}},

View File

@ -46,6 +46,19 @@ func (s strObject) Truthy() bool {
return string(s) != ""
}
type boolObject bool
func (b boolObject) String() string {
if b {
return "(true)"
}
return "(false))"
}
func (b boolObject) Truthy() bool {
return bool(b)
}
func toGoValue(obj object) (interface{}, bool) {
switch v := obj.(type) {
case nil:

View File

@ -201,3 +201,125 @@ func TestBuiltins_ForEach(t *testing.T) {
})
}
}
func TestBuiltins_Procs(t *testing.T) {
tests := []struct {
desc string
expr string
want string
}{
{desc: "simple procs", expr: `
proc greet {
echo "Hello, world"
}
greet
greet`, want: "Hello, world\nHello, world\n(nil)\n"},
{desc: "multiple procs", expr: `
proc greet { |what|
echo "Hello, " $what
}
proc greetWorld { greet "world" }
proc greetMoon { greet "moon" }
proc greetTheThing { |what| greet (cat "the " $what) }
greetWorld
greetMoon
greetTheThing "sun"
`, want: "Hello, world\nHello, moon\nHello, the sun\n(nil)\n"},
{desc: "recursive procs", expr: `
proc four4 { |xs|
if (eq $xs "xxxx") {
$xs
}
four4 (cat $xs "x")
}
four4
`, want: "xxxx\n"},
{desc: "closures", expr: `
proc makeGreeter { |greeting|
proc { |what|
echo $greeting ", " $what
}
}
set helloGreater (makeGreeter "Hello")
$helloGreater "world"
set goodbye (makeGreeter "Goodbye cruel")
$goodbye "world"
call (makeGreeter "Quick") "call me"
`, want: "Hello, world\nGoodbye cruel, world\nQuick, call me\n(nil)\n"},
{desc: "modifying closed over variables", expr: `
proc makeSetter {
set bla "X"
proc appendToBla { |x|
set bla (cat $bla $x)
}
}
set er (makeSetter)
call $er "xxx"
call $er "yyy"
`, want: "Xxxx\nXxxxyyy(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())
})
}
}
func TestBuiltins_Map(t *testing.T) {
tests := []struct {
desc string
expr string
want string
}{
{desc: "map list", expr: `
proc makeUpper { |x| $x | toUpper }
map ["a" "b" "c"] (proc { |x| makeUpper $x })
`, want: "A\nB\nC\n"},
{desc: "map list 2", expr: `
set makeUpper (proc { |x| $x | toUpper })
map ["a" "b" "c"] $makeUpper
`, want: "A\nB\nC\n"},
{desc: "map list with stream", expr: `
set makeUpper (proc { |x| $x | toUpper })
["a" "b" "c"] | map $makeUpper
`, want: "A\nB\nC\n"},
{desc: "map list with stream", expr: `
set makeUpper (proc { |x| $x | toUpper })
set l (["a" "b" "c"] | map $makeUpper)
echo $l
`, want: "[A B C]\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())
})
}
}