diff --git a/Userland/Libraries/LibJS/AST.cpp b/Userland/Libraries/LibJS/AST.cpp index f0e9975873..060623995b 100644 --- a/Userland/Libraries/LibJS/AST.cpp +++ b/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()); diff --git a/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp index a345067b0c..3a7ff1ca66 100644 --- a/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp @@ -528,7 +528,7 @@ ThrowCompletionOr 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()) diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index be62d21cdf..02bcce5378 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/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) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index c1269ff966..849f49b893 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/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 {}") \ diff --git a/Userland/Libraries/LibJS/Runtime/ShadowRealm.cpp b/Userland/Libraries/LibJS/Runtime/ShadowRealm.cpp index f5ff6757e9..0d966394e8 100644 --- a/Userland/Libraries/LibJS/Runtime/ShadowRealm.cpp +++ b/Userland/Libraries/LibJS/Runtime/ShadowRealm.cpp @@ -4,6 +4,10 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include +#include +#include +#include #include #include @@ -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 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(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(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 get_wrapped_value(GlobalObject& global_object, Realm& caller_realm, Value value) { diff --git a/Userland/Libraries/LibJS/Runtime/ShadowRealm.h b/Userland/Libraries/LibJS/Runtime/ShadowRealm.h index d3639d05dd..79b203a3ce 100644 --- a/Userland/Libraries/LibJS/Runtime/ShadowRealm.h +++ b/Userland/Libraries/LibJS/Runtime/ShadowRealm.h @@ -32,6 +32,7 @@ private: ExecutionContext m_execution_context; // [[ExecutionContext]] }; +ThrowCompletionOr perform_shadow_realm_eval(GlobalObject&, StringView source_text, Realm& caller_realm, Realm& eval_realm); ThrowCompletionOr get_wrapped_value(GlobalObject&, Realm& caller_realm, Value); } diff --git a/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.cpp b/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.cpp index 9b9433461f..7221118254 100644 --- a/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.cpp @@ -5,6 +5,7 @@ */ #include +#include #include 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(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)); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.h b/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.h index 8fda68cc99..8aa0e41b07 100644 --- a/Userland/Libraries/LibJS/Runtime/ShadowRealmPrototype.h +++ b/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); }; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/ShadowRealm/ShadowRealm.prototype.evaluate.js b/Userland/Libraries/LibJS/Tests/builtins/ShadowRealm/ShadowRealm.prototype.evaluate.js new file mode 100644 index 0000000000..8f01cec94f --- /dev/null +++ b/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"); + }); +});