1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 13:17:35 +00:00

LibJS: Implement ShadowRealm.prototype.evaluate()

This commit is contained in:
Linus Groh 2021-10-13 21:40:33 +01:00
parent b0ee7f38d0
commit c70784bb82
9 changed files with 260 additions and 1 deletions

View file

@ -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());

View file

@ -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())

View file

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

View file

@ -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 {}") \

View file

@ -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)
{

View file

@ -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);
}

View file

@ -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));
}
}

View file

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

View file

@ -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");
});
});