Commits

Greg Ward  committed d146452

runtime: expand function arguments when appropriate (from an action)

Actions should only run in the build phase, so it's fine to do
ActionExpand() from them. And it's consistent: CommandAction was
already responsible for expanding the command string, so it makes
sense for FunctionCallAction to be responsible for expanding function
args.

Fixes the regression introduced by fa1ec190cb9a.

  • Participants
  • Parent commits fa1ec19

Comments (0)

Files changed (5)

File src/fubsy/runtime/action.go

 }
 
 func (self *FunctionCallAction) Execute(rt *Runtime) []error {
-	_, errs := rt.evaluateCall(self.fcall, logFunctionCall)
+	callable, args, errs := rt.prepareCall(self.fcall)
+	if len(errs) > 0 {
+		return errs
+	}
+	args, errs = rt.expandArgs(args)
+	_, errs = rt.evaluateCall(callable, args, logFunctionCall)
 	return errs
 }
 
-func logFunctionCall(expr *dsl.ASTFunctionCall, arglist types.FuList) {
-	argstrings := make([]string, len(arglist))
-	for i, arg := range arglist {
+func logFunctionCall(callable types.FuCallable, args types.ArgSource) {
+	argstrings := make([]string, len(args.Args()))
+	for i, arg := range args.Args() {
 		argstrings[i] = arg.String()
 	}
-	log.Info("%s(%s)", expr.Function(), strings.Join(argstrings, ", "))
+	log.Info("%s(%s)", callable.Name(), strings.Join(argstrings, ", "))
 }

File src/fubsy/runtime/execute.go

 	case *dsl.ASTAdd:
 		result, errs = self.evaluateAdd(expr)
 	case *dsl.ASTFunctionCall:
-		result, errs = self.evaluateCall(expr, nil)
+		var callable types.FuCallable
+		var args FunctionArgs
+		callable, args, errs = self.prepareCall(expr)
+		if len(errs) == 0 {
+			result, errs = self.evaluateCall(callable, args, nil)
+		}
 	case *dsl.ASTSelection:
 		_, result, errs = self.evaluateLookup(expr)
 	default:
 	return result, nil
 }
 
-func (self *Runtime) evaluateCall(
-	expr *dsl.ASTFunctionCall,
-	precall func(*dsl.ASTFunctionCall, types.FuList)) (
-	types.FuObject, []error) {
+func (self *Runtime) prepareCall(expr *dsl.ASTFunctionCall) (
+	callable types.FuCallable, args FunctionArgs, errs []error) {
 
+	// robj is the receiver object for a method call (foo in foo.x())
+	// value is the callable object (function or method) as a FuObject
 	var robj, value types.FuObject
-	var errs []error
+	args.runtime = self
 
 	// two cases to worry about here:
 	//    1. fn(...)
 	//    2. robj.meth(...)
 	astfunc := expr.Function()
 	if astselect, ok := astfunc.(*dsl.ASTSelection); ok {
-		// case 2: it's a method call; we need to keep track of the
-		// receiver object
+		// case 2: looks like a method call; we need to keep track of
+		// the receiver object
 		robj, value, errs = self.evaluateLookup(astselect)
 	} else {
 		// case 1: it's a normal function call, so robj stays nil
 		value, errs = self.evaluate(expr.Function())
 	}
 	if len(errs) > 0 {
-		return nil, errs
+		return
 	}
+	args.robj = robj
 
-	var err error
 	callable, ok := value.(types.FuCallable)
 	if !ok {
-		err = fmt.Errorf("not a function or method: '%s'", expr.Function())
-		return nil, []error{err}
+		errs = []error{
+			fmt.Errorf("not a function or method: '%s'", expr.Function())}
+		return
 	}
 
 	var astargs []dsl.ASTExpression
 	for i, astarg := range astargs {
 		arglist[i], errs = self.evaluate(astarg)
 		if len(errs) > 0 {
-			return nil, errs
+			return
 		}
 	}
+	args.args = arglist
+	errs = nil
+	return
+}
+
+func (self *Runtime) expandArgs(args FunctionArgs) (FunctionArgs, []error) {
+	xargs := FunctionArgs{
+		runtime: args.runtime,
+		robj:    args.robj,
+	}
+	var errs []error
+	xargs.args = make([]types.FuObject, len(args.args))
+	var err error
+	for i, arg := range args.args {
+		xargs.args[i], err = arg.ActionExpand(self.stack, nil)
+		if err != nil {
+			errs = append(errs, err)
+		}
+	}
+	return xargs, errs
+}
+
+func (self *Runtime) evaluateCall(
+	callable types.FuCallable,
+	args FunctionArgs,
+	precall func(types.FuCallable, types.ArgSource)) (
+	types.FuObject, []error) {
 
 	if precall != nil {
-		precall(expr, arglist)
+		precall(callable, args)
 	}
-
-	err = callable.CheckArgs(arglist)
+	err := callable.CheckArgs(args)
 	if err != nil {
 		return nil, []error{err}
 	}
-	args := FunctionArgs{
-		runtime: self,
-		robj:    robj,
-		args:    arglist,
-	}
 	return callable.Code()(args)
 }
 

File src/fubsy/runtime/execute_test.go

 	assertEvaluateFail(t, rt, "loc2: name not defined: 'b'", addnode)
 }
 
+func Test_prepareCall(t *testing.T) {
+	// this is never going to be called, so it's OK that it's nil
+	var fn_dummy func(args types.ArgSource) (types.FuObject, []error)
+	var dummy1, dummy2 types.FuCallable
+	dummy1 = types.NewFixedFunction("dummy1", 0, fn_dummy)
+	dummy2 = types.NewFixedFunction("dummy1", 1, fn_dummy)
+
+	rt := minimalRuntime()
+	ns := rt.Namespace()
+	ns.Assign("dummy1", dummy1)
+	ns.Assign("dummy2", dummy2)
+	ns.Assign("x", types.FuString("whee!"))
+
+	noargs := []dsl.ASTExpression{}
+	onearg := []dsl.ASTExpression{dsl.NewASTString("\"meep\"")}
+
+	var astcall *dsl.ASTFunctionCall
+	var callable types.FuCallable
+	var args FunctionArgs
+	var errs []error
+
+	// correct (no args) call to dummy1()
+	astcall = dsl.NewASTFunctionCall(dsl.NewASTName("dummy1"), noargs)
+	callable, args, errs = rt.prepareCall(astcall)
+	assert.Equal(t, 0, len(errs))
+	assert.Equal(t, dummy1, callable)
+	assert.Equal(t, []types.FuObject{}, args.args)
+
+	// and to dummy2()
+	astcall = dsl.NewASTFunctionCall(dsl.NewASTName("dummy2"), onearg)
+	callable, args, errs = rt.prepareCall(astcall)
+	assert.Equal(t, 0, len(errs))
+	assert.Equal(t, dummy2, callable)
+	assert.Equal(t, []types.FuObject{types.FuString("meep")}, args.args)
+
+	// attempt to call dummy2() incorrectly (1 arg, but it's an undefined name)
+	astcall = dsl.NewASTFunctionCall(
+		dsl.NewASTName("dummy2"),
+		[]dsl.ASTExpression{dsl.NewASTName("bogus")})
+	callable, _, errs = rt.prepareCall(astcall)
+	assert.Equal(t, 1, len(errs))
+	assert.Equal(t, "name not defined: 'bogus'", errs[0].Error())
+
+	// attempt to call non-existent function
+	astcall = dsl.NewASTFunctionCall(dsl.NewASTName("bogus"), noargs)
+	callable, _, errs = rt.prepareCall(astcall)
+	assert.Nil(t, callable)
+	assert.Equal(t, 1, len(errs))
+	assert.Equal(t, "name not defined: 'bogus'", errs[0].Error())
+
+	// attempt to call something that is not a function
+	astcall = dsl.NewASTFunctionCall(dsl.NewASTName("x"), noargs)
+	callable, _, errs = rt.prepareCall(astcall)
+	assert.Nil(t, callable)
+	assert.Equal(t, 1, len(errs))
+	assert.Equal(t, "not a function or method: 'x'", errs[0].Error())
+}
+
 func Test_evaluateCall(t *testing.T) {
 	// foo() takes no args and always succeeds;
 	// bar() takes exactly one arg and always fails
 		return nil, []error{
 			fmt.Errorf("bar failed (%s)", args.Arg(0))}
 	}
+	var foo, bar types.FuCallable
+	foo = types.NewFixedFunction("foo", 0, fn_foo)
+	bar = types.NewFixedFunction("bar", 1, fn_bar)
 
 	rt := minimalRuntime()
-	ns := rt.Namespace()
-	ns.Assign("foo", types.NewFixedFunction("foo", 0, fn_foo))
-	ns.Assign("bar", types.NewFixedFunction("bar", 1, fn_bar))
-	ns.Assign("src", types.FuString("main.c"))
+	args := FunctionArgs{runtime: rt}
 
 	var result types.FuObject
-	var errors []error
-
-	fooname := dsl.NewASTName("foo")
-	barname := dsl.NewASTName("bar")
-	noargs := []dsl.ASTExpression{}
-	onearg := []dsl.ASTExpression{dsl.NewASTString("\"meep\"")}
+	var errs []error
 
 	// call foo() correctly (no args)
-	ast := dsl.NewASTFunctionCall(fooname, noargs)
-	result, errors = rt.evaluateCall(ast, nil)
+	args.args = []types.FuObject{}
+	result, errs = rt.evaluateCall(foo, args, nil)
 	assert.Equal(t, types.FuString("foo!"), result)
-	assert.Equal(t, 0, len(errors))
+	assert.Equal(t, 0, len(errs))
 	assert.Equal(t, []string{"foo"}, calls)
 
 	// call foo() incorrectly (1 arg)
-	ast = dsl.NewASTFunctionCall(fooname, onearg)
-	result, errors = rt.evaluateCall(ast, nil)
-	assert.Equal(t, 1, len(errors))
+	args.args = []types.FuObject{types.FuString("meep")}
+	result, errs = rt.evaluateCall(foo, args, nil)
+	assert.Equal(t, 1, len(errs))
 	assert.Equal(t,
-		"function foo() takes no arguments (got 1)", errors[0].Error())
+		"function foo() takes no arguments (got 1)", errs[0].Error())
 	assert.Equal(t, []string{"foo"}, calls)
 
 	// call bar() correctly (1 arg)
-	ast = dsl.NewASTFunctionCall(barname, onearg)
-	result, errors = rt.evaluateCall(ast, nil)
+	result, errs = rt.evaluateCall(bar, args, nil)
 	assert.Nil(t, result)
-	assert.Equal(t, 1, len(errors))
-	assert.Equal(t, "bar failed (\"meep\")", errors[0].Error())
+	assert.Equal(t, 1, len(errs))
+	assert.Equal(t, "bar failed (\"meep\")", errs[0].Error())
 	assert.Equal(t, []string{"foo", "bar"}, calls)
 
 	// call bar() incorrectly (no args)
-	ast = dsl.NewASTFunctionCall(barname, noargs)
-	result, errors = rt.evaluateCall(ast, nil)
+	args.args = nil
+	result, errs = rt.evaluateCall(bar, args, nil)
 	assert.Nil(t, result)
-	assert.Equal(t, 1, len(errors))
+	assert.Equal(t, 1, len(errs))
 	assert.Equal(t,
-		"function bar() takes exactly 1 arguments (got 0)", errors[0].Error())
-	assert.Equal(t, []string{"foo", "bar"}, calls)
+		"function bar() takes exactly 1 arguments (got 0)", errs[0].Error())
 
-	// call bar() incorrectly (1 arg, but it's an undefined name)
-	ast = dsl.NewASTFunctionCall(
-		barname, []dsl.ASTExpression{dsl.NewASTName("bogus")})
-	result, errors = rt.evaluateCall(ast, nil)
-	assert.Nil(t, result)
-	assert.Equal(t, 1, len(errors))
-	assert.Equal(t,
-		"name not defined: 'bogus'", errors[0].Error())
-
-	// attempt to call non-existent function
-	ast = dsl.NewASTFunctionCall(dsl.NewASTName("bogus"), onearg)
-	result, errors = rt.evaluateCall(ast, nil)
-	assert.Nil(t, result)
-	assert.Equal(t, 1, len(errors))
-	assert.Equal(t,
-		"name not defined: 'bogus'", errors[0].Error())
-
-	// attempt to call something that is not a function
-	ast = dsl.NewASTFunctionCall(dsl.NewASTName("src"), onearg)
-	result, errors = rt.evaluateCall(ast, nil)
-	assert.Nil(t, result)
-	assert.Equal(t, 1, len(errors))
-	assert.Equal(t,
-		"not a function or method: 'src'", errors[0].Error())
-
+	// check the sequence of calls
 	assert.Equal(t, []string{"foo", "bar"}, calls)
 }
 
 	calls := 0
 	fn_foo := func(args types.ArgSource) (types.FuObject, []error) {
 		calls++
-		return types.FuString("arg: " + args.Arg(0).String()), nil
+		return types.FuString("arg: " + args.Arg(0).ValueString()), nil
 	}
+	foo := types.NewFixedFunction("foo", 1, fn_foo)
 	rt := minimalRuntime()
-	ns := rt.Namespace()
-	ns.Assign("foo", types.NewFixedFunction("foo", 1, fn_foo))
-	fooname := dsl.NewASTName("foo")
-
-	var ast *dsl.ASTFunctionCall
-	var args []dsl.ASTExpression
+	args := FunctionArgs{runtime: rt}
 
 	// call bar() with an arg that needs to be expanded to test that
 	// expansion does *not* happen -- evaluateCall() doesn't know
 	// which phase it's in, so it has to rely on someone else to
 	// ActionExpand() each value in the build phase
-	args = []dsl.ASTExpression{dsl.NewASTString("\">$src<\"")}
-	ast = dsl.NewASTFunctionCall(fooname, args)
-	result, errs := rt.evaluateCall(ast, nil)
+	args.args = []types.FuObject{types.FuString(">$src<")}
+	result, errs := rt.evaluateCall(foo, args, nil)
 	assert.Equal(t, 1, calls)
-	assert.Equal(t, types.FuString("arg: \">$src<\""), result)
+	assert.Equal(t, types.FuString("arg: >$src<"), result)
 	if len(errs) != 0 {
 		t.Errorf("expected no errors, but got: %v", errs)
 	}
 	var val types.FuObject = types.NewStubObject("val", expansion)
 	valexp, _ := val.ActionExpand(nil, nil)
 	assert.Equal(t, expansion, valexp) // this actually tests StubObject
-	ns.Assign("val", val)
 
 	// call foo() with that expandable value, and make sure it is
 	// really called with the unexpanded value
-	args = []dsl.ASTExpression{dsl.NewASTName("val")}
-	ast = dsl.NewASTFunctionCall(fooname, args)
-	result, errs = rt.evaluateCall(ast, nil)
+	args.args[0] = val
+	result, errs = rt.evaluateCall(foo, args, nil)
 	assert.Equal(t, 2, calls)
-	assert.Equal(t, types.FuString("arg: \"val\""), result)
+	assert.Equal(t, types.FuString("arg: val"), result)
 	if len(errs) != 0 {
 		t.Errorf("expected no errors, but got: %v", errs)
 	}
 
 func Test_evaluateCall_method(t *testing.T) {
 	// construct AST for "a.b.c(x)"
-	args := []dsl.ASTExpression{dsl.NewASTName("x")}
-	ast := dsl.NewASTFunctionCall(
+	astargs := []dsl.ASTExpression{dsl.NewASTName("x")}
+	astcall := dsl.NewASTFunctionCall(
 		dsl.NewASTSelection(
 			dsl.NewASTSelection(dsl.NewASTName("a"), "b"), "c"),
-		args)
+		astargs)
 
 	// make sure a is an object with attributes, and b is one of them
 	// (N.B. having FileNodes be attributes of one another is weird
 	ns.Assign("x", types.FuString("hello"))
 
 	// what the hell, let's test the precall feature too
-	var precalledExpr dsl.ASTExpression
-	var precalledArgs types.FuObject
-	precall := func(expr *dsl.ASTFunctionCall, args types.FuList) {
-		precalledExpr = expr
+	var precalledCallable types.FuCallable
+	var precalledArgs types.ArgSource
+	precall := func(callable types.FuCallable, args types.ArgSource) {
+		precalledCallable = callable
 		precalledArgs = args
 	}
 
-	result, errs := rt.evaluateCall(ast, precall)
-	assert.Equal(t, precalledExpr, ast)
-	assert.Equal(t, precalledArgs, types.MakeFuList("hello"))
+	callable, args, errs := rt.prepareCall(astcall)
+	assert.Equal(t, "c", callable.(*types.FuFunction).Name())
+	assert.True(t, args.robj == bobj)
+	assert.Equal(t, 0, len(errs))
+
+	result, errs := rt.evaluateCall(callable, args, precall)
+	assert.Equal(t, precalledCallable, callable)
+	assert.Equal(t,
+		(types.FuList)(precalledArgs.Args()), types.MakeFuList("hello"))
 	assert.Nil(t, result)
 	if len(errs) == 1 {
 		assert.Equal(t,

File src/fubsy/types/callables.go

 
 	// check that the arguments being passed are valid for this function,
 	// returning a user-targeted error object if not
-	CheckArgs(args []FuObject) error
+	CheckArgs(args ArgSource) error
 }
 
 type FuFunction struct {
 	return self.code
 }
 
-func (self *FuFunction) CheckArgs(args []FuObject) error {
-	nargs := len(args)
+func (self *FuFunction) CheckArgs(args ArgSource) error {
+	nargs := len(args.Args())
 	if self.minargs == 0 && self.maxargs == 0 && nargs > 0 {
 		return fmt.Errorf("function %s takes no arguments (got %d)",
 			self, nargs)

File src/fubsy/types/callables_test.go

 
 func Test_FuFunction_CheckArgs_fixed(t *testing.T) {
 	val := FuString("a")
-	args := []FuObject{}
+	args := StubArgSource([]FuObject{})
 	fn := NewFixedFunction("meep", 0, nil)
 
 	err := fn.CheckArgs(args)
 func Test_FuFunction_CheckArgs_minmax(t *testing.T) {
 	fn := NewVariadicFunction("bar", 2, 4, nil)
 	val := FuString("a")
-	args := []FuObject{val}
+	args := StubArgSource([]FuObject{val})
 	err := fn.CheckArgs(args)
 	assert.Equal(t,
 		"function bar() requires at least 2 arguments (got 1)", err.Error())
 func Test_FuFunction_CheckArgs_unlimited(t *testing.T) {
 	fn := NewVariadicFunction("println", 0, -1, nil)
 	val := FuString("a")
-	args := []FuObject{val}
+	args := StubArgSource([]FuObject{val})
 
 	err := fn.CheckArgs(args)
 	assert.Nil(t, err)
 	err = fn.CheckArgs(args)
 	assert.Nil(t, err)
 }
+
+type StubArgSource []FuObject
+
+func (self StubArgSource) Receiver() FuObject {
+	return nil
+}
+
+func (self StubArgSource) Args() []FuObject {
+	return self
+}
+
+func (self StubArgSource) Arg(i int) FuObject {
+	return self[i]
+}
+
+func (self StubArgSource) KeywordArgs() ValueMap {
+	panic("not implemented")
+}
+
+func (self StubArgSource) KeywordArg(name string) (FuObject, bool) {
+	panic("not implemented")
+}