claudepache / es-legacy-function-reflection

JavaScript: Legacy reflection features on functions needed for web compatibility
Creative Commons Zero v1.0 Universal
3 stars 2 forks source link

Proposed arguments semantics don't match implementations #12

Open anba opened 4 years ago

anba commented 4 years ago

From Value returned by .arguments:

When queried on non-censored functions, [...] an Arguments object reflecting the actual arguments passed during the function call or constructor invocation.

This doesn't match the behaviour observable in JSC, V8, and SpiderMonkey.

Results for the below test case:

Function f1 in JSC, V8, and SpiderMonkey:

0
1

=> .arguments reflects the current parameter value, not the original value passed to the function.

Function f2 in JSC, V8, and SpiderMonkey:

0
0
0
1

=> .arguments reflects the original value passed to the function. (This matches the current proposal spec text.)

Function f3 in JSC and V8:

0
0

=> .arguments reflects the original value passed to the function. (This matches the current proposal spec text.)

Function f3 in SpiderMonkey:

0
1

=> .arguments reflects the current parameter value, not the original value passed to the function.

Function f4 in JSC, V8, and SpiderMonkey:

0
0

=> .arguments reflects the original value passed to the function. (This matches the current proposal spec text.)

Test case:

// - Modifies a parameter.
// - Doesn't explicitly refer to the implicit `arguments` binding.
function f1(a) {
    print(f1.arguments[0]);
    a = 1;
    print(f1.arguments[0]);
}
f1(0);

print("---");

// - Modifies a parameter.
// - Explicitly refers to the implicit `arguments` binding.
function f2(a) {
    print(f2.arguments[0]);
    print(arguments[0]);
    a = 1
    print(f2.arguments[0]);
    print(arguments[0]);
}
f2(0);

print("---");

// - Modifies a parameter.
// - Doesn't explicitly refer to the implicit `arguments` binding.
// - Contains direct eval.
function f3(a) {
    eval("");
    print(f3.arguments[0]);
    a = 1
    print(f3.arguments[0]);
}
f3(0);

print("---");

// - Modifies a parameter.
// - Doesn't explicitly refer to the implicit `arguments` binding.
// - The modified parameter is closed over.
function f4(a) {
    print(f4.arguments[0]);
    a = 1;
    print(f4.arguments[0]);
    return () => a;
}
f4(0);
anba commented 4 years ago

JIT interactions may also lead to more issues. For example in V8, f1 starts to have different results when jitted:

function assertEq(a, e, i) {
    if (!Object.is(a, e)) {
        throw new Error(`${a} != ${e} at iteration ${i}`);
    }
}

// - Modifies a parameter.
// - Doesn't explicitly refer to the implicit `arguments` binding.
function f1(a) {
    assertEq(f1.arguments[0], 0, i);
    a = 1;
    assertEq(f1.arguments[0], 1, i);
}

for (var i = 0; i < 10_000_000; ++i) {
    f1(0);
}
claudepache commented 4 years ago

Interesting. At this point, I wonder whether it is better to leave some parts of Function.prototype.arguments as implementation-defined.

anba commented 4 years ago

Yes, I think I kind of agree to leave it implementation-defined for now. Maybe we can align implementations at a later time.

I've also added another test case (f4) to cover closed over bindings.

claudepache commented 4 years ago

I wonder whether there are situations where implementations are not even able to recover some parameter value, either original or current?

claudepache commented 4 years ago

I've compiled your test cases (including the “jit interaction” one) in tests-arguments-wild.html

Interestingly, the outcome is different in Safari when the web inspector is closed or when it is open. When it is open, f1.arguments[0] reflects the original value passed to the function (and all tests are passed).

claudepache commented 4 years ago

I’ve updated analysis.md#value-returned-by-arguments with the results of the tests. For convenience, I write them also in this comment:

Each integer-indexed value of the object returned by f.arguments reflects:

In the most basic cases, semantics (B) is chosen. The implementation switches to semantics (A) if any of the following condition is satifsied:

anba commented 4 years ago

I think default parameters will also trigger case (A).

anba commented 4 years ago

Firefox and Chrome also use (B) when the function contains a with-statement, for example with({});, whereas Safari uses (A).

Firefox may also use (B) even though arguments is mentioned, for example when delete arguments occurs in the function. Safari and Chrome use (A) in that case.

claudepache commented 4 years ago

The first edition of ECMA-262, describes a mechanism for “provid[ing] compatibility with a form of program syntax that is now discouraged: to access the arguments object for function f within the body of f by using the expression f.arguments” (page 34, section 10.1.6); that is, f.arguments was supposed to give access to the same object as arguments, which followed semantics (B).

Apparently, implementations have found convenient to take liberties with the original definition of f.arguments, one of them is to switch to semantics (A) in various cases.

I wonder if, as of today, it is web-compatible to unconditionally switch to (A)?