traefik / yaegi

Yaegi is Another Elegant Go Interpreter
https://pkg.go.dev/github.com/traefik/yaegi
Apache License 2.0
6.78k stars 341 forks source link

Still problems assigning to variables in other packages from `main`, whether in "binary" or fully-interpreted code #1632

Open theclapp opened 1 month ago

theclapp commented 1 month ago

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

func TestIssue1632(t *testing.T) {
    var j int

    i := interp.New(interp.Options{})
    if err := i.Use(interp.Exports{
        "pkg/pkg": map[string]reflect.Value{
            "J": reflect.ValueOf(&j).Elem(),
        },
    }); err != nil {
        t.Fatal(err)
    }
    i.ImportUsed()

    _, err := i.Eval(`func f(i int) int { return i }`)
    if err != nil {
        t.Fatal(err)
    }
    testJ := func(initJ int, src, expected string) {
        t.Helper()
        t.Run(src, func(t *testing.T) {
            t.Helper()
            j = initJ
            assertEval(t, i, src, "", expected)
        })
    }

    // These all work.
    testJ(0, "pkg.J = 1; pkg.J", "1")
    testJ(0, "pkg.J = pkg.J + 1; pkg.J", "1")
    testJ(0, "f(1)", "1")
    testJ(1, "f(pkg.J)", "1")
    testJ(0, "pkg.J += 1; pkg.J", "1")
    testJ(0, "pkg.J += 1; f(*&pkg.J)", "1")
    testJ(0, "pkg.J++; f(*&pkg.J)", "1")
    testJ(0, "*&pkg.J = f(1); pkg.J", "1")
    testJ(0, "k := 1; pkg.J = k; pkg.J", "1")
    testJ(0, "k := 1; *&pkg.J = f(k); pkg.J", "1")
    testJ(0, "pkg.J += 1; f(*&pkg.J+1)", "2")

    // These all fail
    testJ(0, "pkg.J += 1; f(pkg.J)", "1")            // get 0
    testJ(0, "pkg.J += 1; f(pkg.J+1)", "2")          // get 1
    testJ(0, "pkg.J++; f(pkg.J)", "1")               // get 0
    testJ(0, "pkg.J = pkg.J + 1; f(pkg.J)", "1")     // get 0
    testJ(1, "pkg.J = pkg.J + pkg.J; f(pkg.J)", "2") // get 1
    testJ(2, "pkg.J = f(1); pkg.J", "1")             // get 2 (this one's especially surprising)
    testJ(0, "k := 1; pkg.J = f(k); pkg.J", "1")     // get 0
    testJ(0, "pkg.J = 1; k := pkg.J; k", "1")        // get 0

    // Try these tests with strictly interpreted code
    i = interp.New(interp.Options{})
    _, err = i.Eval(`package pkg; var J int`)
    if err != nil {
        t.Fatal(err)
    }
    _, err = i.Eval(`package main
import "pkg"
func f(i int) int { return i }`)
    if err != nil {
        t.Fatal(err)
    }

    testJ = func(initJ int, src, expected string) {
        t.Helper()
        t.Run(src, func(t *testing.T) {
            t.Helper()

            res, err := i.Eval(fmt.Sprintf("pkg.J = %d; pkg.J", initJ))
            if err != nil {
                t.Fatal(err)
            }
            if res.Interface().(int) != initJ {
                t.Fatalf("Expected pkg.J to be %d, got %v", initJ, res)
            }
            assertEval(t, i, src, "", expected)
        })
    }

    // These all still succeed
    testJ(0, "pkg.J = 1; pkg.J", "1")
    testJ(0, "f(1)", "1")
    testJ(1, "f(pkg.J)", "1")
    testJ(0, "pkg.J += 1; pkg.J", "1")
    testJ(0, "pkg.J += 1; f(*&pkg.J)", "1")
    testJ(0, "pkg.J++; f(*&pkg.J)", "1")
    testJ(0, "*&pkg.J = f(1); pkg.J", "1")
    testJ(0, "k := 1; pkg.J = k; pkg.J", "1")
    testJ(0, "k := 1; *&pkg.J = f(k); pkg.J", "1")
    testJ(0, "pkg.J += 1; f(*&pkg.J+1)", "2")

    // These all succeed but don't above
    testJ(0, "pkg.J += 1; f(pkg.J)", "1")
    testJ(0, "pkg.J += 1; f(pkg.J+1)", "2")
    testJ(0, "pkg.J++; f(pkg.J)", "1")
    testJ(0, "pkg.J = 1; k := pkg.J; k", "1")

    // This fails but didn't used to
    testJ(0, "pkg.J = pkg.J + 1; pkg.J", "1") // get 0

    // These all still fail
    testJ(0, "pkg.J = pkg.J + 1; f(pkg.J)", "1")     // get 0
    testJ(1, "pkg.J = pkg.J + pkg.J; f(pkg.J)", "2") // get 1
    testJ(2, "pkg.J = f(1); pkg.J", "1")             // get 2
    testJ(0, "k := 1; pkg.J = f(k); pkg.J", "1")     // get 0
}

Expected result

All tests pass

Got

--- FAIL: TestIssue1632 (0.00s)
    --- FAIL: TestIssue1632/pkg.J_+=_1;_f(pkg.J) (0.00s)
        interp_eval_test.go:1974: got 0, want 1
    --- FAIL: TestIssue1632/pkg.J_+=_1;_f(pkg.J+1) (0.00s)
        interp_eval_test.go:1975: got 1, want 2
    --- FAIL: TestIssue1632/pkg.J++;_f(pkg.J) (0.00s)
        interp_eval_test.go:1976: got 0, want 1
    --- FAIL: TestIssue1632/pkg.J_=_pkg.J_+_1;_f(pkg.J) (0.00s)
        interp_eval_test.go:1977: got 0, want 1
    --- FAIL: TestIssue1632/pkg.J_=_pkg.J_+_pkg.J;_f(pkg.J) (0.00s)
        interp_eval_test.go:1978: got 1, want 2
    --- FAIL: TestIssue1632/pkg.J_=_f(1);_pkg.J (0.00s)
        interp_eval_test.go:1979: got 2, want 1
    --- FAIL: TestIssue1632/k_:=_1;_pkg.J_=_f(k);_pkg.J (0.00s)
        interp_eval_test.go:1980: got 0, want 1
    --- FAIL: TestIssue1632/pkg.J_=_1;_k_:=_pkg.J;_k (0.00s)
        interp_eval_test.go:1981: got 0, want 1
    --- FAIL: TestIssue1632/pkg.J_=_pkg.J_+_1;_pkg.J#01 (0.00s)
        interp_eval_test.go:2031: got 0, want 1
    --- FAIL: TestIssue1632/pkg.J_=_pkg.J_+_1;_f(pkg.J)#01 (0.00s)
        interp_eval_test.go:2034: got 0, want 1
    --- FAIL: TestIssue1632/pkg.J_=_pkg.J_+_pkg.J;_f(pkg.J)#01 (0.00s)
        interp_eval_test.go:2035: got 1, want 2
    --- FAIL: TestIssue1632/pkg.J_=_f(1);_pkg.J#01 (0.00s)
        interp_eval_test.go:2036: got 2, want 1
    --- FAIL: TestIssue1632/k_:=_1;_pkg.J_=_f(k);_pkg.J#01 (0.00s)
        interp_eval_test.go:2037: got 0, want 1
FAIL
FAIL    github.com/traefik/yaegi/interp 0.357s
FAIL

Yaegi Version

381e045

Additional Notes

Related: #1623 .

Obviously I changed assertEval to use Errorf instead of Fatalf.

theclapp commented 1 month ago

Sometimes the problem is in assigning to pkg.J, sometimes it's with passing pkg.J to a function. It's like Yaegi decides (in the cfg code, maybe?) that pkg.J is a constant, evaluates it once at compile-time, and doesn't later re-evaluate it when it should (at run-time). The use of *&pkg.J gets around that.