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:
parent
b0ee7f38d0
commit
c70784bb82
9 changed files with 260 additions and 1 deletions
|
@ -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());
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -151,6 +151,7 @@ namespace JS {
|
|||
P(errors) \
|
||||
P(escape) \
|
||||
P(eval) \
|
||||
P(evaluate) \
|
||||
P(every) \
|
||||
P(exchange) \
|
||||
P(exec) \
|
||||
|
|
|
@ -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 {}") \
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ public:
|
|||
explicit ShadowRealmPrototype(GlobalObject&);
|
||||
virtual void initialize(GlobalObject&) override;
|
||||
virtual ~ShadowRealmPrototype() override = default;
|
||||
|
||||
private:
|
||||
JS_DECLARE_NATIVE_FUNCTION(evaluate);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue