Commits

Greg Ward committed fa1ec19

runtime: do not expand values in evaluateCall()

If we're calling functions in build rules, then it's appropriate to
ActionExpand() arguments. But functions can be called from any phase,
and evaluateCall() doesn't know which phase it's in. It will have to
rely on someone else to ActionExpand() each value in the build phase
-- NOT DONE YET!

Comments (0)

Files changed (4)

examples/java-hello/naive.fubsy

     # jar file when any production source file changes
     mainjar: mainsrc {
         classdir = "classes/main"
-        mkdir(classdir)
+        # gratuitous use of string expansion for test/demonstration purposes
+        mkdir("$classdir")
         "javac -d $classdir $mainsrc"
         "jar -cf $TARGET -C $classdir ."
         remove(classdir)
     # code
     testjar: testsrc + mainjar {
         classdir = "classes/test"
+        # same as above mkdir(), but without relying on string expansion
         mkdir(classdir)
         "javac -d $classdir -classpath $mainjar:$junit $testsrc"
         "jar -cf $TARGET -C $classdir ."

src/fubsy/runtime/execute.go

 
 	var astargs []dsl.ASTExpression
 	var arglist types.FuList
-	var argobj types.FuObject
 	astargs = expr.Args()
 	arglist = make(types.FuList, len(astargs))
 	for i, astarg := range astargs {
 		}
 	}
 
-	argobj, err = arglist.ActionExpand(self.stack, nil)
-	if err != nil {
-		return nil, []error{err}
-	}
-	arglist = argobj.List()
-
 	if precall != nil {
 		precall(expr, arglist)
 	}

src/fubsy/runtime/execute_test.go

 	barname := dsl.NewASTName("bar")
 	noargs := []dsl.ASTExpression{}
 	onearg := []dsl.ASTExpression{dsl.NewASTString("\"meep\"")}
-	exparg := []dsl.ASTExpression{dsl.NewASTString("\">$src<\"")}
 
 	// call foo() correctly (no args)
 	ast := dsl.NewASTFunctionCall(fooname, noargs)
 	assert.Equal(t, "bar failed (\"meep\")", errors[0].Error())
 	assert.Equal(t, []string{"foo", "bar"}, calls)
 
-	// call bar() with an arg that needs to be expanded
-	ast = dsl.NewASTFunctionCall(barname, exparg)
-	result, errors = rt.evaluateCall(ast, nil)
-	assert.Nil(t, result)
-	assert.Equal(t, 1, len(errors))
-	assert.Equal(t, "bar failed (\">main.c<\")", errors[0].Error())
-	assert.Equal(t, []string{"foo", "bar", "bar"}, calls)
-
-	// again, but this time expansion fails (undefined name)
-	exparg = []dsl.ASTExpression{dsl.NewASTString("\"a $bogus value\"")}
-	ast = dsl.NewASTFunctionCall(barname, exparg)
-	result, errors = rt.evaluateCall(ast, nil)
-	assert.Nil(t, result)
-	assert.Equal(t, 1, len(errors))
-	assert.Equal(t, "undefined variable 'bogus' in string", errors[0].Error())
-	assert.Equal(t, []string{"foo", "bar", "bar"}, calls)
-
 	// call bar() incorrectly (no args)
 	ast = dsl.NewASTFunctionCall(barname, noargs)
 	result, errors = rt.evaluateCall(ast, nil)
 	assert.Equal(t, 1, len(errors))
 	assert.Equal(t,
 		"function bar() takes exactly 1 arguments (got 0)", errors[0].Error())
-	assert.Equal(t, []string{"foo", "bar", "bar"}, calls)
+	assert.Equal(t, []string{"foo", "bar"}, calls)
 
 	// call bar() incorrectly (1 arg, but it's an undefined name)
 	ast = dsl.NewASTFunctionCall(
 	assert.Equal(t,
 		"not a function or method: 'src'", errors[0].Error())
 
-	assert.Equal(t, []string{"foo", "bar", "bar"}, calls)
+	assert.Equal(t, []string{"foo", "bar"}, calls)
+}
+
+func Test_evaluateCall_no_expand(t *testing.T) {
+	calls := 0
+	fn_foo := func(args types.ArgSource) (types.FuObject, []error) {
+		calls++
+		return types.FuString("arg: " + args.Arg(0).String()), nil
+	}
+	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
+
+	// 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)
+	assert.Equal(t, 1, calls)
+	assert.Equal(t, types.FuString("arg: \">$src<\""), result)
+	if len(errs) != 0 {
+		t.Errorf("expected no errors, but got: %v", errs)
+	}
+
+	// now make a value that expands to three values
+	expansion := types.MakeFuList("a", "b", "c")
+	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)
+	assert.Equal(t, 2, calls)
+	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) {

src/fubsy/types/basictypes.go

 	return "list"
 }
 
+// stub implementation of FuObject (for use in tests)
+type StubObject struct {
+	name string
+
+	// value returned by ActionExpand() (if nil, return self)
+	expansion FuObject
+
+	// so attribute Lookup() works
+	ValueMap
+}
+
+func NewStubObject(name string, expansion FuObject) StubObject {
+	return StubObject{name: name, expansion: expansion}
+}
+
+func (self StubObject) String() string {
+	return "\"" + self.name + "\""
+}
+
+func (self StubObject) ValueString() string {
+	return self.name
+}
+
+func (self StubObject) CommandString() string {
+	return ShellQuote(self.name)
+}
+
+func (self StubObject) Equal(other_ FuObject) bool {
+	other, ok := other_.(StubObject)
+	return ok && other.name == self.name
+}
+
+func (self StubObject) Add(other FuObject) (FuObject, error) {
+	panic("not implemented")
+}
+
+func (self StubObject) List() []FuObject {
+	return []FuObject{self}
+}
+
+func (self StubObject) ActionExpand(ns Namespace, ctx *ExpandContext) (FuObject, error) {
+	if self.expansion == nil {
+		return self, nil
+	}
+	return self.expansion, nil
+}
+
+func (self StubObject) Typename() string {
+	return "stub"
+}
+
 func unsupportedOperation(self FuObject, other FuObject, detail string) error {
 	return fmt.Errorf("unsupported operation: "+detail,
 		other.Typename(), self.Typename())