Bläddra i källkod

LibJS: Implement ShadowRealm.prototype.evaluate()

Linus Groh 3 år sedan
förälder
incheckning
c70784bb82

+ 3 - 0
Userland/Libraries/LibJS/AST.cpp

@@ -150,6 +150,9 @@ Value BlockStatement::execute(Interpreter& interpreter, GlobalObject& global_obj
 
 Value Program::execute(Interpreter& interpreter, GlobalObject& global_object) const
 {
+    // FIXME: This tries to be "ScriptEvaluation" and "evaluating scriptBody" at once. It shouldn't.
+    //        Clean this up and update perform_eval() / perform_shadow_realm_eval()
+
     InterpreterNodeScope node_scope { interpreter, *this };
 
     VERIFY(interpreter.lexical_environment() && interpreter.lexical_environment()->is_global_environment());

+ 1 - 1
Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp

@@ -528,7 +528,7 @@ ThrowCompletionOr<Value> perform_eval(Value x, GlobalObject& caller_realm, Calle
 
     auto& interpreter = vm.interpreter();
     TemporaryChange scope_change_strict(vm.running_execution_context().is_strict_mode, strict_eval);
-    // Note: We specifically use evaluate_statements here since we don't want to use global_declaration_instantiation from Program::execute.
+    // FIXME: We need to use evaluate_statements() here because Program::execute() calls global_declaration_instantiation() when it shouldn't
     auto eval_result = program->evaluate_statements(interpreter, caller_realm);
 
     if (auto* exception = vm.exception())

+ 1 - 0
Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h

@@ -151,6 +151,7 @@ namespace JS {
     P(errors)                                \
     P(escape)                                \
     P(eval)                                  \
+    P(evaluate)                              \
     P(every)                                 \
     P(exchange)                              \
     P(exec)                                  \

+ 1 - 0
Userland/Libraries/LibJS/Runtime/ErrorTypes.h

@@ -171,6 +171,7 @@
     M(RegExpObjectRepeatedFlag, "Repeated RegExp flag '{}'")                                                                            \
     M(RestrictedFunctionPropertiesAccess, "Restricted function properties like 'callee', 'caller' and 'arguments' may "                 \
                                           "not be accessed in strict mode")                                                             \
+    M(ShadowRealmEvaluateAbruptCompletion, "The evaluated script did not complete normally")                                            \
     M(ShadowRealmWrappedValueNonFunctionObject, "Wrapped value must be primitive or a function object, got {}")                         \
     M(SpeciesConstructorDidNotCreate, "Species constructor did not create {}")                                                          \
     M(SpeciesConstructorReturned, "Species constructor returned {}")                                                                    \

+ 123 - 0
Userland/Libraries/LibJS/Runtime/ShadowRealm.cpp

@@ -4,6 +4,10 @@
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
+#include <LibJS/Lexer.h>
+#include <LibJS/Parser.h>
+#include <LibJS/Runtime/AbstractOperations.h>
+#include <LibJS/Runtime/DeclarativeEnvironment.h>
 #include <LibJS/Runtime/ShadowRealm.h>
 #include <LibJS/Runtime/WrappedFunction.h>
 
@@ -23,6 +27,125 @@ void ShadowRealm::visit_edges(Visitor& visitor)
     visitor.visit(&m_shadow_realm);
 }
 
+// 3.1.1 PerformShadowRealmEval ( sourceText, callerRealm, evalRealm ), https://tc39.es/proposal-shadowrealm/#sec-performshadowrealmeval
+ThrowCompletionOr<Value> perform_shadow_realm_eval(GlobalObject& global_object, StringView source_text, Realm& caller_realm, Realm& eval_realm)
+{
+    auto& vm = global_object.vm();
+
+    // 1. Assert: Type(sourceText) is String.
+    // 2. Assert: callerRealm is a Realm Record.
+    // 3. Assert: evalRealm is a Realm Record.
+
+    // 4. Perform ? HostEnsureCanCompileStrings(callerRealm, evalRealm).
+    // FIXME: We don't have this host-defined abstract operation yet.
+
+    // 5. Perform the following substeps in an implementation-defined order, possibly interleaving parsing and error detection:
+
+    // a. Let script be ParseText(! StringToCodePoints(sourceText), Script).
+    auto parser = Parser(Lexer(source_text));
+    auto program = parser.parse_program();
+
+    // b. If script is a List of errors, throw a SyntaxError exception.
+    if (parser.has_errors()) {
+        auto& error = parser.errors()[0];
+        return vm.throw_completion<JS::SyntaxError>(global_object, error.to_string());
+    }
+
+    // c. If script Contains ScriptBody is false, return undefined.
+    if (program->children().is_empty())
+        return js_undefined();
+
+    // d. Let body be the ScriptBody of script.
+    // e. If body Contains NewTarget is true, throw a SyntaxError exception.
+    // f. If body Contains SuperProperty is true, throw a SyntaxError exception.
+    // g. If body Contains SuperCall is true, throw a SyntaxError exception.
+    // FIXME: Implement these, we probably need a generic way of scanning the AST for certain nodes.
+
+    // 6. Let strictEval be IsStrict of script.
+    auto strict_eval = program->is_strict_mode();
+
+    // 7. Let runningContext be the running execution context.
+    // NOTE: This would be unused due to step 11 and is omitted for that reason.
+
+    // 8. Let lexEnv be NewDeclarativeEnvironment(evalRealm.[[GlobalEnv]]).
+    Environment* lexical_environment = new_declarative_environment(eval_realm.global_environment());
+
+    // 9. Let varEnv be evalRealm.[[GlobalEnv]].
+    Environment* variable_environment = &eval_realm.global_environment();
+
+    // 10. If strictEval is true, set varEnv to lexEnv.
+    if (strict_eval)
+        variable_environment = lexical_environment;
+
+    // 11. If runningContext is not already suspended, suspend runningContext.
+    // NOTE: We don't support this concept yet.
+
+    // 12. Let evalContext be a new ECMAScript code execution context.
+    auto eval_context = ExecutionContext { vm.heap() };
+
+    // 13. Set evalContext's Function to null.
+    eval_context.function = nullptr;
+
+    // 14. Set evalContext's Realm to evalRealm.
+    eval_context.realm = &eval_realm;
+
+    // 15. Set evalContext's ScriptOrModule to null.
+    // FIXME: Our execution context struct currently does not track this item.
+
+    // 16. Set evalContext's VariableEnvironment to varEnv.
+    eval_context.variable_environment = variable_environment;
+
+    // 17. Set evalContext's LexicalEnvironment to lexEnv.
+    eval_context.lexical_environment = lexical_environment;
+
+    // Non-standard
+    eval_context.is_strict_mode = strict_eval;
+
+    // 18. Push evalContext onto the execution context stack; evalContext is now the running execution context.
+    vm.push_execution_context(eval_context, eval_realm.global_object());
+
+    // 19. Let result be EvalDeclarationInstantiation(body, varEnv, lexEnv, null, strictEval).
+    auto eval_result = eval_declaration_instantiation(vm, eval_realm.global_object(), program, variable_environment, lexical_environment, strict_eval);
+
+    Completion result;
+
+    // 20. If result.[[Type]] is normal, then
+    if (!eval_result.is_throw_completion()) {
+        // TODO: Optionally use bytecode interpreter?
+        // FIXME: We need to use evaluate_statements() here because Program::execute() calls global_declaration_instantiation() when it shouldn't
+        // a. Set result to the result of evaluating body.
+        auto result_value = program->evaluate_statements(vm.interpreter(), eval_realm.global_object());
+        if (auto* exception = vm.exception())
+            result = throw_completion(exception->value());
+        else if (!result_value.is_empty())
+            result = normal_completion(result_value);
+        else
+            result = Completion {}; // Normal completion with no value
+    }
+
+    // 21. If result.[[Type]] is normal and result.[[Value]] is empty, then
+    if (result.type() == Completion::Type::Normal && !result.has_value()) {
+        // a. Set result to NormalCompletion(undefined).
+        result = normal_completion(js_undefined());
+    }
+
+    // 22. Suspend evalContext and remove it from the execution context stack.
+    // NOTE: We don't support this concept yet.
+    vm.pop_execution_context();
+
+    // 23. Resume the context that is now on the top of the execution context stack as the running execution context.
+    // NOTE: We don't support this concept yet.
+
+    // 24. If result.[[Type]] is not normal, throw a TypeError exception.
+    if (result.type() != Completion::Type::Normal)
+        return vm.throw_completion<TypeError>(global_object, ErrorType::ShadowRealmEvaluateAbruptCompletion);
+
+    // 25. Return ? GetWrappedValue(callerRealm, result.[[Value]]).
+    return get_wrapped_value(global_object, caller_realm, result.value());
+
+    // NOTE: Also see "Editor's Note" in the spec regarding the TypeError above.
+}
+
 // 3.1.3 GetWrappedValue ( callerRealm, value ), https://tc39.es/proposal-shadowrealm/#sec-getwrappedvalue
 ThrowCompletionOr<Value> get_wrapped_value(GlobalObject& global_object, Realm& caller_realm, Value value)
 {

+ 1 - 0
Userland/Libraries/LibJS/Runtime/ShadowRealm.h

@@ -32,6 +32,7 @@ private:
     ExecutionContext m_execution_context; // [[ExecutionContext]]
 };
 
+ThrowCompletionOr<Value> perform_shadow_realm_eval(GlobalObject&, StringView source_text, Realm& caller_realm, Realm& eval_realm);
 ThrowCompletionOr<Value> get_wrapped_value(GlobalObject&, Realm& caller_realm, Value);
 
 }

+ 31 - 0
Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.cpp

@@ -5,6 +5,7 @@
  */
 
 #include <LibJS/Runtime/GlobalObject.h>
+#include <LibJS/Runtime/ShadowRealm.h>
 #include <LibJS/Runtime/ShadowRealmPrototype.h>
 
 namespace JS {
@@ -20,8 +21,38 @@ void ShadowRealmPrototype::initialize(GlobalObject& global_object)
     auto& vm = this->vm();
     Object::initialize(global_object);
 
+    u8 attr = Attribute::Writable | Attribute::Configurable;
+    define_native_function(vm.names.evaluate, evaluate, 1, attr);
+
     // 3.4.3 ShadowRealm.prototype [ @@toStringTag ], https://tc39.es/proposal-shadowrealm/#sec-shadowrealm.prototype-@@tostringtag
     define_direct_property(*vm.well_known_symbol_to_string_tag(), js_string(vm, vm.names.ShadowRealm.as_string()), Attribute::Configurable);
 }
 
+// 3.4.1 ShadowRealm.prototype.evaluate ( sourceText ), https://tc39.es/proposal-shadowrealm/#sec-shadowrealm.prototype.evaluate
+JS_DEFINE_NATIVE_FUNCTION(ShadowRealmPrototype::evaluate)
+{
+    auto source_text = vm.argument(0);
+
+    // 1. Let O be this value.
+    // 2. Perform ? ValidateShadowRealmObject(O).
+    auto* object = typed_this_object(global_object);
+    if (vm.exception())
+        return {};
+
+    // 3. If Type(sourceText) is not String, throw a TypeError exception.
+    if (!source_text.is_string()) {
+        vm.throw_exception<TypeError>(global_object, ErrorType::NotAString, source_text);
+        return {};
+    }
+
+    // 4. Let callerRealm be the current Realm Record.
+    auto* caller_realm = vm.current_realm();
+
+    // 5. Let evalRealm be O.[[ShadowRealm]].
+    auto& eval_realm = object->shadow_realm();
+
+    // 6. Return ? PerformShadowRealmEval(sourceText, callerRealm, evalRealm).
+    return TRY_OR_DISCARD(perform_shadow_realm_eval(global_object, source_text.as_string().string(), *caller_realm, eval_realm));
+}
+
 }

+ 3 - 0
Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.h

@@ -18,6 +18,9 @@ public:
     explicit ShadowRealmPrototype(GlobalObject&);
     virtual void initialize(GlobalObject&) override;
     virtual ~ShadowRealmPrototype() override = default;
+
+private:
+    JS_DECLARE_NATIVE_FUNCTION(evaluate);
 };
 
 }

+ 96 - 0
Userland/Libraries/LibJS/Tests/builtins/ShadowRealm/ShadowRealm.prototype.evaluate.js

@@ -0,0 +1,96 @@
+describe("normal behavior", () => {
+    test("length is 1", () => {
+        expect(ShadowRealm.prototype.evaluate).toHaveLength(1);
+    });
+
+    test("basic functionality", () => {
+        const shadowRealm = new ShadowRealm();
+        expect(shadowRealm.evaluate("globalThis.foo = 'bar';")).toBe("bar");
+        expect(shadowRealm.foo).toBeUndefined();
+        expect(shadowRealm.evaluate("foo;")).toBe("bar");
+        expect(shadowRealm.evaluate("foo;")).toBe("bar");
+    });
+
+    test("global object initialization", () => {
+        // Currently uses a plain JS::GlobalObject, i.e. no TestRunnerGlobalObject functions are available on the
+        // shadow realm's global object. This may change in the future, update the test accordingly.
+        const shadowRealm = new ShadowRealm();
+        expect(shadowRealm.evaluate("globalThis.isStrictMode")).toBeUndefined();
+    });
+
+    test("strict mode behavior", () => {
+        const shadowRealm = new ShadowRealm();
+        // NOTE: We don't have access to the isStrictMode() test helper inside the shadow realm, see the comment in the test above.
+
+        // sloppy mode
+        expect(shadowRealm.evaluate("(function() { return !this; })()")).toBe(false);
+        // strict mode
+        expect(shadowRealm.evaluate("'use strict'; (function() { return !this; })()")).toBe(true);
+        // Only the parsed script's strict mode changes the strictEval value used for EvalDeclarationInstantiation
+        expect(
+            (function () {
+                "use strict";
+                return shadowRealm.evaluate("(function() { return !this; })()");
+            })()
+        ).toBe(false);
+    });
+
+    test("wrapped function object", () => {
+        const shadowRealm = new ShadowRealm();
+
+        const string = shadowRealm.evaluate("(function () { return 'foo'; })")();
+        expect(string).toBe("foo");
+
+        const wrappedFunction = shadowRealm.evaluate("(function () { return 'foo'; })");
+        expect(wrappedFunction()).toBe("foo");
+        expect(typeof wrappedFunction).toBe("function");
+        expect(Object.getPrototypeOf(wrappedFunction)).toBe(Function.prototype);
+
+        expect(() => {
+            shadowRealm.evaluate("(function () { throw Error(); })")();
+        }).toThrowWithMessage(
+            TypeError,
+            "Call of wrapped target function did not complete normally"
+        );
+    });
+});
+
+describe("errors", () => {
+    test("throws for non-string input", () => {
+        const shadowRealm = new ShadowRealm();
+        const values = [
+            [undefined, "undefined"],
+            [42, "42"],
+            [new String(), "[object StringObject]"],
+        ];
+        for (const [value, errorString] of values) {
+            expect(() => {
+                shadowRealm.evaluate(value);
+            }).toThrowWithMessage(TypeError, `${errorString} is not a string`);
+        }
+    });
+
+    test("throws if non-function object is returned from evaluation", () => {
+        const shadowRealm = new ShadowRealm();
+        const values = [
+            ["[]", "[object Array]"],
+            ["({})", "[object Object]"],
+            ["new String()", "[object StringObject]"],
+        ];
+        for (const [value, errorString] of values) {
+            expect(() => {
+                shadowRealm.evaluate(value);
+            }).toThrowWithMessage(
+                TypeError,
+                `Wrapped value must be primitive or a function object, got ${errorString}`
+            );
+        }
+    });
+
+    test("any exception is changed to a TypeError", () => {
+        const shadowRealm = new ShadowRealm();
+        expect(() => {
+            shadowRealm.evaluate("(() => { throw 42; })()");
+        }).toThrowWithMessage(TypeError, "The evaluated script did not complete normally");
+    });
+});