Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interpreted closures mixed with "binary" functions don't capture values correctly #1634

Open
theclapp opened this issue May 17, 2024 · 1 comment
Labels
bug Something isn't working

Comments

@theclapp
Copy link
Contributor

theclapp commented May 17, 2024

The following test case TestIssue1634 in interp/interp_eval_test.go triggers an unexpected result

func TestIssue1634(t *testing.T) {
	TLogf := t.Logf

	type CPE struct {
		E string
	}

	type CP struct {
		Es []*CPE
	}

	type Child struct {
		I int
		E string
		F func() string
	}

	FL := func(children []*Child) []string {
		s := []string{}
		for _, child := range children {
			s = append(s, child.F())
		}
		return s
	}

	R := func(i int, eE string, f func() string) *Child {
		TLogf("inside R: %d, %s", i, eE)
		return &Child{
			I: i,
			E: eE,
			F: f,
		}
	}

	var CpL func(cp *CP) []string
	// Having this on a line by itself makes it easy to copy into the interpreted
	// code below.
	CpL = func(cp *CP) []string {
		var ch []*Child
		for i, e := range cp.Es {
			i, e := i, e
			ch = append(ch,
				R(i, e.E, func() string {
					TLogf("inside R's f: %d, %v", i, e.E)
					return e.E
				}),
			)
		}
		return FL(ch)
	}

	var Cp *CP
	initCp := func() {
		Cp = &CP{
			Es: []*CPE{
				{E: "foo"},
				{E: "bar"},
				{E: "baz"},
			},
		}
	}
	initCp()

	// Verify that this works in standard Go
	t.Logf("Standard Go:")
	s := CpL(Cp)
	if expected, got := []string{"foo", "bar", "baz"}, s; !reflect.DeepEqual(expected, got) {
		t.Fatalf("Standard Go: CpL: Expected %v, got %v", expected, got)
	}

	i := interp.New(interp.Options{})
	err := i.Use(stdlib.Symbols)
	if err != nil {
		t.Fatalf("i.Use: Expected nil, got %v", err)
	}
	i.Use(interp.Exports{
		"pkg/pkg": {
			// vars
			"Cp":    reflect.ValueOf(&Cp).Elem(),
			"CpL":   reflect.ValueOf(&CpL).Elem(),
			"TLogf": reflect.ValueOf(&TLogf).Elem(),

			// funcs
			"FL": reflect.ValueOf(FL),
			"R":  reflect.ValueOf(R),

			// types
			"CP":    reflect.ValueOf((*CP)(nil)),
			"Child": reflect.ValueOf((*Child)(nil)),
		},
	})
	i.ImportUsed()

	initCp()
	_, err = i.Eval(`
package main

import . "pkg"

func main() {
	CpL = func(cp *CP) []string {
		var ch []*Child
		for i, e := range cp.Es {
			i, e := i, e
			ch = append(ch,
				R(i, e.E, func() string {
					TLogf("inside R's f: %d, %v", i, e.E)
					return e.E
				}),
			)
		}
		return FL(ch)
	}
}
	`)
	if err != nil {
		t.Fatalf("i.Eval: Expected nil, got %v", err)
	}
	t.Logf("Interpreted Go:")
	s = CpL(Cp)
	if expected, got := []string{"foo", "bar", "baz"}, s; !reflect.DeepEqual(expected, got) {
		t.Fatalf("Interpreted Go CpL: Expected %v, got %v", expected, got)
	}
}

Expected result

All tests pass; interpreted output matches compiled output

Got

% go test ./interp -run TestIssue1634
--- FAIL: TestIssue1634 (0.01s)
    interp_eval_test.go:2104: Standard Go:
    interp_eval_test.go:2066: inside R: 0, foo
    interp_eval_test.go:2066: inside R: 1, bar
    interp_eval_test.go:2066: inside R: 2, baz
    interp_eval_test.go:2083: inside R's f: 0, foo
    interp_eval_test.go:2083: inside R's f: 1, bar
    interp_eval_test.go:2083: inside R's f: 2, baz
    interp_eval_test.go:2158: Interpreted Go:
    interp_eval_test.go:2066: inside R: 0, foo
    interp_eval_test.go:2066: inside R: 1, bar
    interp_eval_test.go:2066: inside R: 2, baz
    value.go:596: inside R's f: 2, baz
    value.go:596: inside R's f: 2, baz
    value.go:596: inside R's f: 2, baz
    interp_eval_test.go:2161: Interpreted Go CpL: Expected [foo bar baz], got [baz baz baz]
FAIL
FAIL    github.com/traefik/yaegi/interp 0.365s
FAIL

Yaegi Version

381e045

Additional Notes

No response

@theclapp theclapp changed the title Interpreted closures mixed with "binary" functions don't capture values correctly Interpreted closures mixed with "binary" functions don't capture for loop values correctly May 20, 2024
@ldez ldez added the bug Something isn't working label May 20, 2024
@theclapp
Copy link
Contributor Author

Pulling the closure that captures the loop variables out of the arglist of the "binary" function R works, if that helps:

Change this:

for i, e := range cp.Es {
	i, e := i, e
	ch = append(ch,
		// breaks
		R(i, e.E, func() string {
			TLogf("inside R's f: %d, %v", i, e.E)
			return e.E
		}),
	)
}

To this:

for i, e := range cp.Es {
	i, e := i, e
	// works
	f := func() string {
		TLogf("inside R's f: %d, %v", i, e.E)
		return e.E
	}
	ch = append(ch, R(i, e.E, f))
}

Also, I think that the fact that these are loop variables is a red herring, I think this is probably a problem with any closure being passed as a literal to a "binary" function.

Unfortunately, the code I'm playing with is awash with such things. :)

@theclapp theclapp changed the title Interpreted closures mixed with "binary" functions don't capture for loop values correctly Interpreted closures mixed with "binary" functions don't capture values correctly May 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants