Repository: mozilla/rhino
Pull request #1860: Fix `this` in `apply`/`call`
Author: login: andreabergia
Created: 2025-03-25T10:42:40Z | Merged: 2025-03-28T06:34:06Z

When `this` is missing, or null, or undefined when invoking a function via `apply` or `call`, it should be replaced with the global object only in non-strict mode, according to the spec.

Note that Rhino has a feature flag `FEATURE_OLD_UNDEF_NULL_THIS` that replaces a missing `this` with the global object always, for any function. This is enabled by default in older versions of the engine (<= 1_7).

The previous implementation was not doing the correct thing, for a couple of reasons:
- it did not account for strict and non-strict functions; it had the same behavior for all functions;
- it had a different behavior in interpreter and compiled mode due to the function peeling optimization for the interpreter of #1510, where a new, interpreter-only implementation was done.

This implementation fixes the problems above. I have had to add a new flag on functions to decide whether a function was strict or not, and to implement it in the code generation for compiled classes.
This fixes a number of test262 cases.

Spec ref: https://tc39.es/ecma262/#sec-function.prototype.apply

*Note:* this implementation removes the ad-hoc implementation for interpreter, so that compiled and interpreted mode work in the same way. This has the side effect of breaking continuations in the interpreter for `apply`/`call` though - I have had to remove some tests that were not working anymore. I am not sure how big a problem this is; it's possible to keep the duplicated code and fix it in two places if we prefer to keep that functionality.


Diff:
diff --git a/rhino/src/main/java/org/mozilla/javascript/InterpretedFunction.java b/rhino/src/main/java/org/mozilla/javascript/InterpretedFunction.java
index 35ec112874..8887e22d87 100644
--- a/rhino/src/main/java/org/mozilla/javascript/InterpretedFunction.java
+++ b/rhino/src/main/java/org/mozilla/javascript/InterpretedFunction.java
@@ -82,7 +82,7 @@ public String getFunctionName() {
     @Override
     public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
         if (!ScriptRuntime.hasTopCall(cx)) {
-            return ScriptRuntime.doTopCall(this, cx, scope, thisObj, args, idata.isStrict);
+            return ScriptRuntime.doTopCall(this, cx, scope, thisObj, args, isStrict());
         }
         return Interpreter.interpret(this, cx, scope, thisObj, args);
     }
@@ -98,7 +98,7 @@ public Object exec(Context cx, Scriptable scope) {
             // It will go through "call" path. but they are equivalent
             ret =
                     ScriptRuntime.doTopCall(
-                            this, cx, scope, scope, ScriptRuntime.emptyArgs, idata.isStrict);
+                            this, cx, scope, scope, ScriptRuntime.emptyArgs, isStrict());
         } else {
             ret = Interpreter.interpret(this, cx, scope, scope, ScriptRuntime.emptyArgs);
         }
@@ -169,4 +169,9 @@ boolean hasFunctionNamed(String name) {
     public boolean hasDefaultParameters() {
         return idata.argsHasDefaults;
     }
+
+    @Override
+    public boolean isStrict() {
+        return idata.isStrict;
+    }
 }
diff --git a/rhino/src/main/java/org/mozilla/javascript/Interpreter.java b/rhino/src/main/java/org/mozilla/javascript/Interpreter.java
index 433e983f5d..5ae0c1ad10 100644
--- a/rhino/src/main/java/org/mozilla/javascript/Interpreter.java
+++ b/rhino/src/main/java/org/mozilla/javascript/Interpreter.java
@@ -145,7 +145,7 @@ void initializeArgs(
                                         cx,
                                         scope,
                                         args,
-                                        idata.isStrict,
+                                        fnOrScript.isStrict(),
                                         idata.argsHasRest,
                                         homeObject);
                     } else {
@@ -155,7 +155,7 @@ void initializeArgs(
                                         cx,
                                         scope,
                                         args,
-                                        idata.isStrict,
+                                        fnOrScript.isStrict(),
                                         idata.argsHasRest,
                                         homeObject);
                     }
@@ -315,7 +315,7 @@ private boolean isStrictTopFrame() {
             for (; ; ) {
                 final CallFrame p = f.parentFrame;
                 if (p == null) {
-                    return f.idata.isStrict;
+                    return f.fnOrScript.isStrict();
                 }
                 f = p;
             }
@@ -3659,34 +3659,14 @@ private static Scriptable getApplyThis(
             int indexReg,
             Callable target,
             CallFrame frame) {
-        // This is a workaround for what is most likely a bug. We compute applyThis differently for
-        // interpreted and non-interpreted functions. It feels like exactly one of these two
-        // strategies should be correct, but choosing one or the other for all functions will break
-        // different sets of test262 tests.
-        if (target instanceof InterpretedFunction) {
-            Scriptable applyThis;
-            if (indexReg != 0) {
-                Object obj = stack[thisIdx];
-                if (obj == DOUBLE_MARK) obj = ScriptRuntime.wrapNumber(sDbl[thisIdx]);
-                applyThis = ScriptRuntime.toObjectOrNull(cx, obj, frame.scope);
-            } else {
-                applyThis = null;
-            }
-            if (applyThis == null) {
-                // This covers the case of args[0] == (null|undefined) as well.
-                applyThis = ScriptRuntime.getTopCallScope(cx);
-            }
-            return applyThis;
+        Object obj;
+        if (indexReg != 0) {
+            obj = stack[thisIdx];
+            if (obj == DOUBLE_MARK) obj = ScriptRuntime.wrapNumber(sDbl[thisIdx]);
         } else {
-            Object obj;
-            if (indexReg != 0) {
-                obj = stack[thisIdx];
-                if (obj == DOUBLE_MARK) obj = ScriptRuntime.wrapNumber(sDbl[thisIdx]);
-            } else {
-                obj = null;
-            }
-            return ScriptRuntime.getApplyOrCallThis(cx, frame.scope, obj, indexReg);
+            obj = null;
         }
+        return ScriptRuntime.getApplyOrCallThis(cx, frame.scope, obj, indexReg, target);
     }
 
     private static CallFrame initFrame(
diff --git a/rhino/src/main/java/org/mozilla/javascript/NativeFunction.java b/rhino/src/main/java/org/mozilla/javascript/NativeFunction.java
index 4b61c9439c..381e11cab7 100644
--- a/rhino/src/main/java/org/mozilla/javascript/NativeFunction.java
+++ b/rhino/src/main/java/org/mozilla/javascript/NativeFunction.java
@@ -117,4 +117,6 @@ protected boolean getParamOrVarConst(int index) {
         // from earlier Rhino versions. See Bugzilla #396117.
         return false;
     }
+
+    public abstract boolean isStrict();
 }
diff --git a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java
index e46a7bc1fe..64973db2a6 100644
--- a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java
+++ b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java
@@ -3071,7 +3071,7 @@ public static Object applyOrCall(
         int L = args.length;
         Callable function = getCallable(thisObj);
 
-        Scriptable callThis = getApplyOrCallThis(cx, scope, L == 0 ? null : args[0], L);
+        Scriptable callThis = getApplyOrCallThis(cx, scope, L == 0 ? null : args[0], L, function);
 
         Object[] callArgs;
         if (isApply) {
@@ -3090,22 +3090,41 @@ public static Object applyOrCall(
         return function.call(cx, scope, callThis, callArgs);
     }
 
-    static Scriptable getApplyOrCallThis(Context cx, Scriptable scope, Object arg0, int l) {
+    static Scriptable getApplyOrCallThis(
+            Context cx, Scriptable scope, Object arg0, int l, Callable target) {
         Scriptable callThis;
-        if (l != 0) {
-            callThis =
-                    arg0 == Undefined.instance
-                                    && !cx.hasFeature(Context.FEATURE_OLD_UNDEF_NULL_THIS)
-                            ? Undefined.SCRIPTABLE_UNDEFINED
-                            : toObjectOrNull(cx, arg0, scope);
+        if (cx.hasFeature(Context.FEATURE_OLD_UNDEF_NULL_THIS)) {
+            // Legacy behavior
+            if (l != 0) {
+                callThis = toObjectOrNull(cx, arg0, scope);
+            } else {
+                callThis = null;
+            }
+            if (callThis == null) {
+                // This covers the case of args[0] == (null|undefined) as well.
+                callThis = getTopCallScope(cx);
+            }
         } else {
-            callThis = null;
-        }
-        if (callThis == null && cx.hasFeature(Context.FEATURE_OLD_UNDEF_NULL_THIS)) {
-            callThis =
-                    getTopCallScope(
-                            cx); // This covers the case of args[0] == (null|undefined) as well.
+            // Spec-compliant behavior
+            if (l != 0) {
+                callThis =
+                        arg0 == Undefined.instance
+                                ? Undefined.SCRIPTABLE_UNDEFINED
+                                : toObjectOrNull(cx, arg0, scope);
+            } else {
+                callThis = Undefined.SCRIPTABLE_UNDEFINED;
+            }
+
+            // Replace missing this with global object only for non-strict functions
+            boolean missingCallThis =
+                    callThis == null || callThis == Undefined.SCRIPTABLE_UNDEFINED;
+            boolean isFunctionStrict =
+                    !(target instanceof NativeFunction) || ((NativeFunction) target).isStrict();
+            if (missingCallThis && !isFunctionStrict) {
+                callThis = getTopCallScope(cx);
+            }
         }
+
         return callThis;
     }
 
diff --git a/rhino/src/main/java/org/mozilla/javascript/optimizer/Codegen.java b/rhino/src/main/java/org/mozilla/javascript/optimizer/Codegen.java
index 96a9e15ca8..05130793ae 100644
--- a/rhino/src/main/java/org/mozilla/javascript/optimizer/Codegen.java
+++ b/rhino/src/main/java/org/mozilla/javascript/optimizer/Codegen.java
@@ -740,7 +740,8 @@ private void generateNativeFunctionOverrides(ClassFileWriter cfw, String rawSour
         final int Do_isGeneratorFunction = 6;
         final int Do_hasRestParameter = 7;
         final int Do_hasDefaultParameters = 8;
-        final int SWITCH_COUNT = 9;
+        final int Do_isStrict = 9;
+        final int SWITCH_COUNT = 10;
 
         for (int methodIndex = 0; methodIndex != SWITCH_COUNT; ++methodIndex) {
             if (methodIndex == Do_getRawSource && rawSource == null) {
@@ -791,6 +792,10 @@ private void generateNativeFunctionOverrides(ClassFileWriter cfw, String rawSour
                     methodLocals = 1; // Only this
                     cfw.startMethod("hasDefaultParameters", "()Z", ACC_PUBLIC);
                     break;
+                case Do_isStrict:
+                    methodLocals = 1; // Only this
+                    cfw.startMethod("isStrict", "()Z", ACC_PUBLIC);
+                    break;
                 default:
                     throw Kit.codeBug();
             }
@@ -957,6 +962,11 @@ private void generateNativeFunctionOverrides(ClassFileWriter cfw, String rawSour
                         cfw.add(ByteCode.ARETURN);
                         break;
 
+                    case Do_isStrict:
+                        cfw.addPush(n.isInStrictMode() ? 1 : 0);
+                        cfw.add(ByteCode.IRETURN);
+                        break;
+
                     default:
                         throw Kit.codeBug();
                 }
diff --git a/tests/src/test/java/org/mozilla/javascript/tests/UndefinedOrNullThisInFunctionCallOrApplyTest.java b/tests/src/test/java/org/mozilla/javascript/tests/UndefinedOrNullThisInFunctionCallOrApplyTest.java
index e9b37cac5e..51817ff20d 100644
--- a/tests/src/test/java/org/mozilla/javascript/tests/UndefinedOrNullThisInFunctionCallOrApplyTest.java
+++ b/tests/src/test/java/org/mozilla/javascript/tests/UndefinedOrNullThisInFunctionCallOrApplyTest.java
@@ -4,240 +4,225 @@
 
 package org.mozilla.javascript.tests;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-
-import java.util.HashMap;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.mozilla.javascript.BaseFunction;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
 import org.mozilla.javascript.Context;
+import org.mozilla.javascript.EcmaError;
 import org.mozilla.javascript.NativeArray;
-import org.mozilla.javascript.Scriptable;
 import org.mozilla.javascript.Undefined;
-
-public class UndefinedOrNullThisInFunctionCallOrApplyTest {
-
-    private Context cx;
-
-    private BaseFunction function;
-
-    @Before
-    public void setUp() throws Exception {
-        cx = Context.enter();
-        function =
-                new BaseFunction() {
-                    @Override
-                    public Object call(
-                            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
-                        System.out.println("QWE");
-                        return thisObj;
-                    }
-                };
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        Context.exit();
-    }
-
-    @Test
-    @Ignore
-    public void whenVersionGt17ThenPassNullAsThisObjJavaFunc() {
-        HashMap<String, Scriptable> bindings = new HashMap<String, Scriptable>();
-        bindings.put("F2", function);
-
-        cx.setLanguageVersion(Context.VERSION_1_8);
-        NativeArray arr =
-                (NativeArray) Evaluator.eval("[this, F2.apply(), F2.apply(undefined)];", bindings);
-
-        assertNotEquals(arr.get(0), arr.get(1));
-        assertNotEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
-
-        arr = (NativeArray) Evaluator.eval("[this, F2.apply(), F2.apply(null)];", bindings);
-        assertNotEquals(arr.get(0), arr.get(1));
-        assertNotEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
-
-        cx.setLanguageVersion(Context.VERSION_ES6);
-        arr = (NativeArray) Evaluator.eval("[this, F2.apply(), F2.apply(undefined)];", bindings);
-
-        assertNotEquals(arr.get(0), arr.get(1));
-        assertNotEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
-
-        arr = (NativeArray) Evaluator.eval("[this, F2.apply(), F2.apply(null)];", bindings);
-        assertNotEquals(arr.get(0), arr.get(1));
-        assertNotEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
-    }
-
-    @Test
-    @Ignore
-    public void whenVersionLtEq17ThenPassGlobalThisObjJavaFunc() {
-        HashMap<String, Scriptable> bindings = new HashMap<String, Scriptable>();
-        bindings.put("F2", function);
-
-        cx.setLanguageVersion(Context.VERSION_1_7);
-        NativeArray arr =
-                (NativeArray)
-                        Evaluator.eval(
-                                "{return this;};[this, F2.apply(), F2.apply(undefined)];",
-                                bindings);
-
-        assertEquals(arr.get(0), arr.get(1));
-        assertEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
-
-        arr = (NativeArray) Evaluator.eval("[this, F2.apply(), F2.apply(null)];", bindings);
-
-        assertEquals(arr.get(0), arr.get(1));
-        assertEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
-    }
-
-    @Test
-    public void whenVersionGt17ThenPassNullAsThisObjForApplyJS() {
-        cx.setLanguageVersion(Context.VERSION_1_8);
-        NativeArray arr =
-                (NativeArray)
-                        Evaluator.eval(
-                                cx,
-                                "function F2() {return this;};[this, F2.apply(), F2.apply(undefined)];");
-
-        assertNotEquals(arr.get(0), arr.get(1));
-        assertNotEquals(arr.get(0), arr.get(2));
-        assertNotEquals(arr.get(1), arr.get(2));
-        assertEquals(Undefined.instance, arr.get(2));
-        assertEquals(Undefined.SCRIPTABLE_UNDEFINED, arr.get(2));
-
-        arr =
-                (NativeArray)
-                        Evaluator.eval(
-                                cx,
-                                "function F2() {return this;};[this, F2.apply(), F2.apply(null)];");
-        assertNotEquals(arr.get(0), arr.get(1));
-        assertNotEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
-
-        cx.setLanguageVersion(Context.VERSION_ES6);
-        arr =
-                (NativeArray)
-                        Evaluator.eval(
-                                cx,
-                                "function F2() {return this;};[this, F2.apply(), F2.apply(undefined)];");
-
-        assertNotEquals(arr.get(0), arr.get(1));
-        assertNotEquals(arr.get(0), arr.get(2));
-        assertNotEquals(arr.get(1), arr.get(2));
-        assertEquals(Undefined.instance, arr.get(2));
-        assertEquals(Undefined.SCRIPTABLE_UNDEFINED, arr.get(2));
-
-        arr =
-                (NativeArray)
-                        Evaluator.eval(
-                                cx,
-                                "function F2() {return this;};[this, F2.apply(), F2.apply(null)];");
-        assertNotEquals(arr.get(0), arr.get(1));
-        assertNotEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
-    }
-
-    @Test
-    public void whenVersionLtEq17ThenPassGlobalThisObjForApplyJS() {
-        cx.setLanguageVersion(Context.VERSION_1_7);
-        NativeArray arr =
-                (NativeArray)
-                        Evaluator.eval(
-                                cx,
-                                "function F2() {return this;};[this, F2.apply(), F2.apply(undefined)];");
-
-        assertEquals(arr.get(0), arr.get(1));
-        assertEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
-
-        arr =
-                (NativeArray)
-                        Evaluator.eval(
-                                cx,
-                                "function F2() {return this;};[this, F2.apply(), F2.apply(null)];");
-
-        assertEquals(arr.get(0), arr.get(1));
-        assertEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
-    }
-
-    @Test
-    public void whenVersionGt17ThenPassNullAsThisObjForCallJS() {
-        cx.setLanguageVersion(Context.VERSION_1_8);
-        NativeArray arr =
-                (NativeArray)
-                        Evaluator.eval(
-                                cx,
-                                "function F2() {return this;};[this, F2.call(), F2.call(undefined)];");
-
-        assertNotEquals(arr.get(0), arr.get(1));
-        assertNotEquals(arr.get(0), arr.get(2));
-        assertNotEquals(arr.get(1), arr.get(2));
-        assertEquals(Undefined.instance, arr.get(2));
-        assertEquals(Undefined.SCRIPTABLE_UNDEFINED, arr.get(2));
-
-        arr =
-                (NativeArray)
-                        Evaluator.eval(
-                                cx,
-                                "function F2() {return this;};[this, F2.call(), F2.call(null)];");
-        assertNotEquals(arr.get(0), arr.get(1));
-        assertNotEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
-
-        cx.setLanguageVersion(Context.VERSION_ES6);
-        arr =
-                (NativeArray)
-                        Evaluator.eval(
-                                cx,
-                                "function F2() {return this;};[this, F2.call(), F2.call(undefined)];");
-
-        assertNotEquals(arr.get(0), arr.get(1));
-        assertNotEquals(arr.get(0), arr.get(2));
-        assertNotEquals(arr.get(1), arr.get(2));
-        assertEquals(Undefined.instance, arr.get(2));
-        assertEquals(Undefined.SCRIPTABLE_UNDEFINED, arr.get(2));
-
-        arr =
-                (NativeArray)
-                        Evaluator.eval(
-                                cx,
-                                "function F2() {return this;};[this, F2.call(), F2.call(null)];");
-        assertNotEquals(arr.get(0), arr.get(1));
-        assertNotEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
+import org.mozilla.javascript.testutils.Utils;
+
+class UndefinedOrNullThisInFunctionCallOrApplyTest {
+    @Nested
+    class LegacyVersion {
+        @Test
+        void applyStrictOrNonStrict() {
+            // Legacy mode replaces missing this with global object always, strict or non-strict
+            Utils.runWithAllModes(
+                    cx -> {
+                        cx.setLanguageVersion(Context.VERSION_1_7);
+                        NativeArray arr =
+                                (NativeArray)
+                                        Evaluator.eval(
+                                                cx,
+                                                "function F2() {return this;};[this, F2.apply(), F2.apply(undefined), F2.apply(null)];");
+
+                        assertSame(arr.get(0), arr.get(1));
+                        assertSame(arr.get(0), arr.get(2));
+                        assertSame(arr.get(0), arr.get(3));
+
+                        arr =
+                                (NativeArray)
+                                        Evaluator.eval(
+                                                cx,
+                                                "function F2() {'use strict'; return this;};[this, F2.apply(), F2.apply(undefined), F2.apply(null)];");
+
+                        assertSame(arr.get(0), arr.get(1));
+                        assertSame(arr.get(0), arr.get(2));
+                        assertSame(arr.get(0), arr.get(3));
+
+                        return null;
+                    });
+        }
+
+        @Test
+        void applyBuiltIn() {
+            Utils.runWithAllModes(
+                    cx -> {
+                        cx.setLanguageVersion(Context.VERSION_1_7);
+
+                        Object res = Evaluator.eval("Array.prototype.at.apply(undefined);");
+                        assertTrue(Undefined.isUndefined(res));
+
+                        return null;
+                    });
+        }
+
+        @Test
+        void callStrictOrNonStrict() {
+            // Legacy mode replaces missing this with global object always, strict or non strict
+            Utils.runWithAllModes(
+                    cx -> {
+                        cx.setLanguageVersion(Context.VERSION_1_7);
+                        NativeArray arr =
+                                (NativeArray)
+                                        Evaluator.eval(
+                                                cx,
+                                                "function F2() {return this;};[this, F2.call(), F2.call(undefined), F2.call(null)];");
+
+                        assertSame(arr.get(0), arr.get(1));
+                        assertSame(arr.get(0), arr.get(2));
+                        assertSame(arr.get(0), arr.get(3));
+
+                        arr =
+                                (NativeArray)
+                                        Evaluator.eval(
+                                                cx,
+                                                "function F2() {'use strict'; return this;};[this, F2.call(), F2.call(undefined), F2.call(null)];");
+
+                        assertSame(arr.get(0), arr.get(1));
+                        assertSame(arr.get(0), arr.get(2));
+                        assertSame(arr.get(0), arr.get(3));
+
+                        return null;
+                    });
+        }
+
+        @Test
+        void callBuiltIn() {
+            Utils.runWithAllModes(
+                    cx -> {
+                        cx.setLanguageVersion(Context.VERSION_1_7);
+
+                        Object res = Evaluator.eval("Array.prototype.at.call(undefined);");
+                        assertTrue(Undefined.isUndefined(res));
+
+                        return null;
+                    });
+        }
     }
 
-    @Test
-    public void whenVersionLtEq17ThenPassGlobalThisObjForCallJS() {
-        cx.setLanguageVersion(Context.VERSION_1_7);
-        NativeArray arr =
-                (NativeArray)
-                        Evaluator.eval(
-                                cx,
-                                "function F2() {return this;};[this, F2.call(), F2.call(undefined)];");
-
-        assertEquals(arr.get(0), arr.get(1));
-        assertEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
-
-        arr =
-                (NativeArray)
-                        Evaluator.eval(
-                                cx,
-                                "function F2() {return this;};[this, F2.call(), F2.call(null)];");
-
-        assertEquals(arr.get(0), arr.get(1));
-        assertEquals(arr.get(0), arr.get(2));
-        assertEquals(arr.get(1), arr.get(2));
+    @Nested
+    class ModernVersion {
+        @Test
+        void applyStrict() {
+            // Preserves null/undefined arguments
+            Utils.runWithAllModes(
+                    cx -> {
+                        cx.setLanguageVersion(Context.VERSION_1_8);
+                        NativeArray arr =
+                                (NativeArray)
+                                        Evaluator.eval(
+                                                cx,
+                                                "function F2() {'use strict'; return this;};[this, F2.apply(), F2.apply(undefined), F2.apply(null)];");
+                        assertNotEquals(arr.get(0), arr.get(1));
+                        assertNotEquals(arr.get(0), arr.get(2));
+                        assertNotEquals(arr.get(0), arr.get(3));
+                        assertSame(Undefined.SCRIPTABLE_UNDEFINED, arr.get(1));
+                        assertSame(Undefined.SCRIPTABLE_UNDEFINED, arr.get(2));
+                        assertNull(arr.get(3));
+
+                        return null;
+                    });
+        }
+
+        @Test
+        void applyNonStrict() {
+            // Replaces missing this with global object for non-strict functions
+            Utils.runWithAllModes(
+                    cx -> {
+                        cx.setLanguageVersion(Context.VERSION_1_8);
+                        NativeArray arr =
+                                (NativeArray)
+                                        Evaluator.eval(
+                                                cx,
+                                                "function F2() {return this;};[this, F2.apply(), F2.apply(undefined), F2.apply(null)];");
+
+                        assertSame(arr.get(0), arr.get(1));
+                        assertSame(arr.get(0), arr.get(2));
+                        assertSame(arr.get(0), arr.get(3));
+
+                        return null;
+                    });
+        }
+
+        @Test
+        void applyBuiltIn() {
+            // Preserves null/undefined arguments, and thus throws an error
+            Utils.runWithAllModes(
+                    cx -> {
+                        cx.setLanguageVersion(Context.VERSION_1_8);
+
+                        assertThrows(
+                                EcmaError.class,
+                                () -> Evaluator.eval("Array.prototype.at.apply(undefined);"));
+
+                        return null;
+                    });
+        }
+
+        @Test
+        void callStrict() {
+            // Preserves null/undefined arguments
+            Utils.runWithAllModes(
+                    cx -> {
+                        cx.setLanguageVersion(Context.VERSION_1_8);
+                        NativeArray arr =
+                                (NativeArray)
+                                        Evaluator.eval(
+                                                cx,
+                                                "function F2() {'use strict'; return this;};[this, F2.call(), F2.call(undefined), F2.call(null)];");
+
+                        assertNotEquals(arr.get(0), arr.get(1));
+                        assertNotEquals(arr.get(0), arr.get(2));
+                        assertNotEquals(arr.get(0), arr.get(3));
+                        assertSame(Undefined.SCRIPTABLE_UNDEFINED, arr.get(1));
+                        assertSame(Undefined.SCRIPTABLE_UNDEFINED, arr.get(2));
+                        assertNull(arr.get(3));
+
+                        return null;
+                    });
+        }
+
+        @Test
+        void callNonStrict() {
+            // Replaces missing this with global object for non-strict functions
+            Utils.runWithAllModes(
+                    cx -> {
+                        cx.setLanguageVersion(Context.VERSION_1_8);
+                        NativeArray arr =
+                                (NativeArray)
+                                        Evaluator.eval(
+                                                cx,
+                                                "function F2() {return this;};[this, F2.call(), F2.call(undefined), F2.call(null)];");
+
+                        assertSame(arr.get(0), arr.get(1));
+                        assertSame(arr.get(0), arr.get(2));
+                        assertSame(arr.get(0), arr.get(3));
+
+                        return null;
+                    });
+        }
+
+        @Test
+        void callBuiltIn() {
+            // Preserves null/undefined arguments, and thus throws an error
+            Utils.runWithAllModes(
+                    cx -> {
+                        cx.setLanguageVersion(Context.VERSION_1_8);
+
+                        assertThrows(
+                                EcmaError.class,
+                                () -> Evaluator.eval("Array.prototype.at.call(undefined);"));
+
+                        return null;
+                    });
+        }
     }
 }
diff --git a/tests/testsrc/test262.properties b/tests/testsrc/test262.properties
index 5ffc9a9003..93d62ccde0 100644
--- a/tests/testsrc/test262.properties
+++ b/tests/testsrc/test262.properties
@@ -652,7 +652,7 @@ built-ins/Error 5/41 (12.2%)
 
 ~built-ins/FinalizationRegistry
 
-built-ins/Function 175/508 (34.45%)
+built-ins/Function 149/508 (29.33%)
     internals/Call 2/2 (100.0%)
     internals/Construct 6/6 (100.0%)
     length/S15.3.5.1_A1_T3.js strict
@@ -666,19 +666,6 @@ built-ins/Function 175/508 (34.45%)
     prototype/apply/argarray-not-object-realm.js
     prototype/apply/not-a-constructor.js
     prototype/apply/resizable-buffer.js {unsupported: [resizable-arraybuffer]}
-    prototype/apply/S15.3.4.3_A3_T1.js compiled
-    prototype/apply/S15.3.4.3_A3_T2.js compiled
-    prototype/apply/S15.3.4.3_A3_T3.js compiled
-    prototype/apply/S15.3.4.3_A3_T4.js compiled
-    prototype/apply/S15.3.4.3_A3_T5.js compiled
-    prototype/apply/S15.3.4.3_A3_T6.js compiled-non-strict
-    prototype/apply/S15.3.4.3_A3_T7.js compiled
-    prototype/apply/S15.3.4.3_A3_T8.js compiled-non-strict
-    prototype/apply/S15.3.4.3_A5_T4.js compiled-non-strict
-    prototype/apply/S15.3.4.3_A7_T1.js compiled
-    prototype/apply/S15.3.4.3_A7_T2.js compiled
-    prototype/apply/S15.3.4.3_A7_T5.js compiled
-    prototype/apply/S15.3.4.3_A7_T7.js compiled
     prototype/apply/this-not-callable-realm.js
     prototype/bind/BoundFunction_restricted-properties.js
     prototype/bind/get-fn-realm.js
@@ -698,19 +685,6 @@ built-ins/Function 175/508 (34.45%)
     prototype/call/15.3.4.4-2-s.js strict
     prototype/call/15.3.4.4-3-s.js strict
     prototype/call/not-a-constructor.js
-    prototype/call/S15.3.4.4_A3_T1.js compiled
-    prototype/call/S15.3.4.4_A3_T2.js compiled
-    prototype/call/S15.3.4.4_A3_T3.js compiled
-    prototype/call/S15.3.4.4_A3_T4.js compiled
-    prototype/call/S15.3.4.4_A3_T5.js compiled
-    prototype/call/S15.3.4.4_A3_T6.js compiled-non-strict
-    prototype/call/S15.3.4.4_A3_T7.js compiled
-    prototype/call/S15.3.4.4_A3_T8.js compiled-non-strict
-    prototype/call/S15.3.4.4_A5_T4.js compiled-non-strict
-    prototype/call/S15.3.4.4_A6_T1.js compiled
-    prototype/call/S15.3.4.4_A6_T2.js compiled
-    prototype/call/S15.3.4.4_A6_T5.js compiled
-    prototype/call/S15.3.4.4_A6_T7.js compiled
     prototype/Symbol.hasInstance/name.js
     prototype/toString/async-arrow-function.js {unsupported: [async-functions]}
     prototype/toString/async-function-declaration.js {unsupported: [async-functions]}
@@ -3256,24 +3230,9 @@ language/destructuring 8/18 (44.44%)
     binding/initialization-requires-object-coercible-undefined.js
     binding/typedarray-backed-by-resizable-buffer.js {unsupported: [resizable-arraybuffer]}
 
-language/directive-prologue 18/62 (29.03%)
-    14.1-1-s.js interpreted-non-strict
-    14.1-10-s.js interpreted-non-strict
-    14.1-11-s.js interpreted-non-strict
-    14.1-12-s.js interpreted-non-strict
-    14.1-13-s.js interpreted-non-strict
-    14.1-14-s.js interpreted-non-strict
-    14.1-15-s.js interpreted-non-strict
-    14.1-16-s.js compiled-non-strict
-    14.1-17-s.js compiled-non-strict
-    14.1-2-s.js interpreted-non-strict
-    14.1-3-s.js compiled-non-strict
-    14.1-4-s.js compiled-non-strict
-    14.1-5-s.js compiled-non-strict
-    14.1-6-s.js compiled-non-strict
-    14.1-7-s.js compiled-non-strict
-    14.1-8-s.js interpreted-non-strict
-    14.1-9-s.js interpreted-non-strict
+language/directive-prologue 3/62 (4.84%)
+    14.1-4-s.js non-strict
+    14.1-5-s.js non-strict
     func-decl-inside-func-decl-parse.js non-strict
 
 language/eval-code 241/347 (69.45%)
@@ -5506,7 +5465,7 @@ language/expressions/yield 4/63 (6.35%)
     star-return-is-null.js
     star-rhs-iter-nrml-next-invoke.js
 
-language/function-code 122/217 (56.22%)
+language/function-code 100/217 (46.08%)
     10.4.3-1-1-s.js non-strict
     10.4.3-1-10-s.js non-strict
     10.4.3-1-100-s.js
@@ -5590,19 +5549,7 @@ language/function-code 122/217 (56.22%)
     10.4.3-1-64gs.js
     10.4.3-1-65-s.js
     10.4.3-1-65gs.js
-    10.4.3-1-66-s.js
-    10.4.3-1-66gs.js
-    10.4.3-1-67-s.js interpreted
-    10.4.3-1-67gs.js interpreted
-    10.4.3-1-68-s.js interpreted
-    10.4.3-1-68gs.js interpreted
     10.4.3-1-7-s.js strict
-    10.4.3-1-71-s.js
-    10.4.3-1-71gs.js
-    10.4.3-1-72-s.js interpreted
-    10.4.3-1-72gs.js interpreted
-    10.4.3-1-73-s.js interpreted
-    10.4.3-1-73gs.js interpreted
     10.4.3-1-76-s.js
     10.4.3-1-76gs.js
     10.4.3-1-77-s.js
@@ -5611,18 +5558,8 @@ language/function-code 122/217 (56.22%)
     10.4.3-1-78gs.js
     10.4.3-1-7gs.js strict
     10.4.3-1-8-s.js non-strict
-    10.4.3-1-86-s.js compiled-non-strict
-    10.4.3-1-86gs.js compiled-non-strict
-    10.4.3-1-87-s.js compiled-non-strict
-    10.4.3-1-87gs.js compiled-non-strict
     10.4.3-1-8gs.js non-strict
     10.4.3-1-9-s.js strict
-    10.4.3-1-90-s.js compiled-non-strict
-    10.4.3-1-90gs.js compiled-non-strict
-    10.4.3-1-91-s.js compiled-non-strict
-    10.4.3-1-91gs.js compiled-non-strict
-    10.4.3-1-92-s.js compiled-non-strict
-    10.4.3-1-92gs.js compiled-non-strict
     10.4.3-1-9gs.js strict
     block-decl-onlystrict.js strict
     eval-param-env-with-computed-key.js non-strict


Reviews:
user: login: rbri
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T11:02:53Z
user: login: andreabergia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T11:03:31Z
user: login: andreabergia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T11:28:40Z

Review comments:
diff_hunk: @@ -169,4 +169,9 @@ boolean hasFunctionNamed(String name) {
     public boolean hasDefaultParameters() {
         return idata.argsHasDefaults;
     }
+
+    @Override
+    public boolean isStrict() {
path: rhino/src/main/java/org/mozilla/javascript/InterpretedFunction.java
user: login: rbri
body: Should we use isStrict() instead of idata.isStrict at other places also?
created_at: 2025-03-25T11:02:52Z
author_association: COLLABORATOR
diff_hunk: @@ -169,4 +169,9 @@ boolean hasFunctionNamed(String name) {
     public boolean hasDefaultParameters() {
         return idata.argsHasDefaults;
     }
+
+    @Override
+    public boolean isStrict() {
path: rhino/src/main/java/org/mozilla/javascript/InterpretedFunction.java
user: login: andreabergia
body: Good call, I'll update that.
created_at: 2025-03-25T11:03:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -169,4 +169,9 @@ boolean hasFunctionNamed(String name) {
     public boolean hasDefaultParameters() {
         return idata.argsHasDefaults;
     }
+
+    @Override
+    public boolean isStrict() {
path: rhino/src/main/java/org/mozilla/javascript/InterpretedFunction.java
user: login: andreabergia
body: Done, thanks for the suggestion @rbri 
created_at: 2025-03-25T11:28:39Z
author_association: CONTRIBUTOR

PR comments:
user: login: rbri
created_at: 2025-03-25T11:04:29Z
body: test262.properties proves that this is another major step forward ❤️ 🚀 🧙‍♂️ 
author_association: COLLABORATOR
user: login: p-bakker
created_at: 2025-03-26T07:14:02Z
body: Have a look at #1444 wrt to continuations and handing of .call/apply.

I would think breaking those isn't acceptable, as people seem to rely of that working
author_association: COLLABORATOR
user: login: p-bakker
created_at: 2025-03-26T07:15:28Z
body: But kudo's on fixing this!

Any change of next supporting non-wrapped primitives as `this`? 🤞
author_association: COLLABORATOR
user: login: gbrail
created_at: 2025-03-26T15:02:44Z
body: Thanks -- I'm also encouraged by this but concerned about continuations. People absolutely use these a lot, although I suspect it's mostly legacy code. Can we find a way to keep that working?
author_association: COLLABORATOR
user: login: andreabergia
created_at: 2025-03-26T16:51:22Z
body: I think I've managed to restore the continuations in the latest commit, and to clean up the code a bit - there's now only _one_ implementation in `ScriptRuntime`. 🙂 

Oh, also, please squash this if accepted!
author_association: CONTRIBUTOR
user: login: gbrail
created_at: 2025-03-28T06:33:30Z
body: This looks good to me now too. Thanks!
author_association: COLLABORATOR