Add Context::DeepFreeze
Change-Id: I1002944931fa7705048457e2cd2c39494923c750 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3691125 Reviewed-by: Camillo Bruni <cbruni@chromium.org> Commit-Queue: Russ Hamilton <behamilton@google.com> Reviewed-by: Leszek Swirski <leszeks@chromium.org> Cr-Commit-Position: refs/heads/main@{#85710}
This commit is contained in:
parent
d367ee2ac6
commit
2833957c77
@ -9,6 +9,7 @@
|
||||
|
||||
#include "v8-data.h" // NOLINT(build/include_directory)
|
||||
#include "v8-local-handle.h" // NOLINT(build/include_directory)
|
||||
#include "v8-maybe.h" // NOLINT(build/include_directory)
|
||||
#include "v8-snapshot.h" // NOLINT(build/include_directory)
|
||||
#include "v8config.h" // NOLINT(build/include_directory)
|
||||
|
||||
@ -163,6 +164,13 @@ class V8_EXPORT Context : public Data {
|
||||
*/
|
||||
void Exit();
|
||||
|
||||
/**
|
||||
* Attempts to recursively freeze all objects reachable from this context.
|
||||
* Some objects (generators, iterators, non-const closures) can not be frozen
|
||||
* and will cause this method to throw an error.
|
||||
*/
|
||||
Maybe<void> DeepFreeze();
|
||||
|
||||
/** Returns the isolate associated with a current context. */
|
||||
Isolate* GetIsolate();
|
||||
|
||||
|
214
src/api/api.cc
214
src/api/api.cc
@ -47,6 +47,7 @@
|
||||
#include "src/compiler-dispatcher/lazy-compile-dispatcher.h"
|
||||
#include "src/date/date.h"
|
||||
#include "src/objects/primitive-heap-object.h"
|
||||
#include "src/utils/identity-map.h"
|
||||
#if V8_ENABLE_WEBASSEMBLY
|
||||
#include "src/debug/debug-wasm-objects.h"
|
||||
#endif // V8_ENABLE_WEBASSEMBLY
|
||||
@ -85,6 +86,7 @@
|
||||
#include "src/objects/embedder-data-slot-inl.h"
|
||||
#include "src/objects/hash-table-inl.h"
|
||||
#include "src/objects/heap-object.h"
|
||||
#include "src/objects/instance-type.h"
|
||||
#include "src/objects/js-array-buffer-inl.h"
|
||||
#include "src/objects/js-array-inl.h"
|
||||
#include "src/objects/js-collection-inl.h"
|
||||
@ -6717,6 +6719,218 @@ Local<Value> v8::Context::GetSecurityToken() {
|
||||
return Utils::ToLocal(token_handle);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool MayContainObjectsToFreeze(i::InstanceType obj_type) {
|
||||
if (i::InstanceTypeChecker::IsString(obj_type)) return false;
|
||||
if (i::InstanceTypeChecker::IsSharedFunctionInfo(obj_type)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsJSReceiverSafeToFreeze(i::InstanceType obj_type) {
|
||||
DCHECK(i::InstanceTypeChecker::IsJSReceiver(obj_type));
|
||||
switch (obj_type) {
|
||||
case i::JS_OBJECT_TYPE:
|
||||
case i::JS_GLOBAL_OBJECT_TYPE:
|
||||
case i::JS_GLOBAL_PROXY_TYPE:
|
||||
case i::JS_PRIMITIVE_WRAPPER_TYPE:
|
||||
case i::JS_FUNCTION_TYPE:
|
||||
/* Function types */
|
||||
case i::BIGINT64_TYPED_ARRAY_CONSTRUCTOR_TYPE:
|
||||
case i::BIGUINT64_TYPED_ARRAY_CONSTRUCTOR_TYPE:
|
||||
case i::FLOAT32_TYPED_ARRAY_CONSTRUCTOR_TYPE:
|
||||
case i::FLOAT64_TYPED_ARRAY_CONSTRUCTOR_TYPE:
|
||||
case i::INT16_TYPED_ARRAY_CONSTRUCTOR_TYPE:
|
||||
case i::INT32_TYPED_ARRAY_CONSTRUCTOR_TYPE:
|
||||
case i::INT8_TYPED_ARRAY_CONSTRUCTOR_TYPE:
|
||||
case i::UINT16_TYPED_ARRAY_CONSTRUCTOR_TYPE:
|
||||
case i::UINT32_TYPED_ARRAY_CONSTRUCTOR_TYPE:
|
||||
case i::UINT8_CLAMPED_TYPED_ARRAY_CONSTRUCTOR_TYPE:
|
||||
case i::UINT8_TYPED_ARRAY_CONSTRUCTOR_TYPE:
|
||||
case i::JS_ARRAY_CONSTRUCTOR_TYPE:
|
||||
case i::JS_PROMISE_CONSTRUCTOR_TYPE:
|
||||
case i::JS_REG_EXP_CONSTRUCTOR_TYPE:
|
||||
case i::JS_CLASS_CONSTRUCTOR_TYPE:
|
||||
/* Prototype Types */
|
||||
case i::JS_ARRAY_ITERATOR_PROTOTYPE_TYPE:
|
||||
case i::JS_ITERATOR_PROTOTYPE_TYPE:
|
||||
case i::JS_MAP_ITERATOR_PROTOTYPE_TYPE:
|
||||
case i::JS_OBJECT_PROTOTYPE_TYPE:
|
||||
case i::JS_PROMISE_PROTOTYPE_TYPE:
|
||||
case i::JS_REG_EXP_PROTOTYPE_TYPE:
|
||||
case i::JS_SET_ITERATOR_PROTOTYPE_TYPE:
|
||||
case i::JS_SET_PROTOTYPE_TYPE:
|
||||
case i::JS_STRING_ITERATOR_PROTOTYPE_TYPE:
|
||||
case i::JS_TYPED_ARRAY_PROTOTYPE_TYPE:
|
||||
/* */
|
||||
case i::JS_ARRAY_TYPE:
|
||||
return true;
|
||||
#if V8_ENABLE_WEBASSEMBLY
|
||||
case i::WASM_ARRAY_TYPE:
|
||||
case i::WASM_STRUCT_TYPE:
|
||||
#endif // V8_ENABLE_WEBASSEMBLY
|
||||
case i::JS_PROXY_TYPE:
|
||||
return true;
|
||||
// These types are known not to freeze.
|
||||
case i::JS_MAP_KEY_ITERATOR_TYPE:
|
||||
case i::JS_MAP_KEY_VALUE_ITERATOR_TYPE:
|
||||
case i::JS_MAP_VALUE_ITERATOR_TYPE:
|
||||
case i::JS_SET_KEY_VALUE_ITERATOR_TYPE:
|
||||
case i::JS_SET_VALUE_ITERATOR_TYPE:
|
||||
case i::JS_GENERATOR_OBJECT_TYPE:
|
||||
case i::JS_ASYNC_FUNCTION_OBJECT_TYPE:
|
||||
case i::JS_ASYNC_GENERATOR_OBJECT_TYPE:
|
||||
case i::JS_ARRAY_ITERATOR_TYPE: {
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
// TODO(behamilton): Handle any types that fall through here.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ObjectVisitorDeepFreezer : i::ObjectVisitor {
|
||||
public:
|
||||
explicit ObjectVisitorDeepFreezer(i::Isolate* isolate) : isolate_(isolate) {}
|
||||
|
||||
bool DeepFreeze(i::Handle<i::Context> context) {
|
||||
bool success = VisitObject(*i::Handle<i::HeapObject>::cast(context));
|
||||
DCHECK_EQ(success, !error_.has_value());
|
||||
if (!success) {
|
||||
THROW_NEW_ERROR_RETURN_VALUE(
|
||||
isolate_, NewTypeError(error_->msg_id, error_->name), false);
|
||||
}
|
||||
|
||||
for (const auto& obj : objects_to_freeze_) {
|
||||
MAYBE_RETURN_ON_EXCEPTION_VALUE(
|
||||
isolate_,
|
||||
i::JSReceiver::SetIntegrityLevel(isolate_, obj, i::FROZEN,
|
||||
i::kThrowOnError),
|
||||
false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void VisitPointers(i::HeapObject host, i::ObjectSlot start,
|
||||
i::ObjectSlot end) final {
|
||||
VisitPointersImpl(start, end);
|
||||
}
|
||||
void VisitPointers(i::HeapObject host, i::MaybeObjectSlot start,
|
||||
i::MaybeObjectSlot end) final {
|
||||
VisitPointersImpl(start, end);
|
||||
}
|
||||
void VisitMapPointer(i::HeapObject host) final {
|
||||
VisitPointer(host, host.map_slot());
|
||||
}
|
||||
void VisitCodePointer(i::HeapObject host, i::CodeObjectSlot slot) final {}
|
||||
void VisitCodeTarget(i::InstructionStream host, i::RelocInfo* rinfo) final {}
|
||||
void VisitEmbeddedPointer(i::InstructionStream host,
|
||||
i::RelocInfo* rinfo) final {}
|
||||
void VisitCustomWeakPointers(i::HeapObject host, i::ObjectSlot start,
|
||||
i::ObjectSlot end) final {}
|
||||
|
||||
private:
|
||||
struct ErrorInfo {
|
||||
i::MessageTemplate msg_id;
|
||||
i::Handle<i::String> name;
|
||||
};
|
||||
|
||||
template <typename TSlot>
|
||||
void VisitPointersImpl(TSlot start, TSlot end) {
|
||||
for (TSlot current = start; current < end; ++current) {
|
||||
typename TSlot::TObject object = current.load(isolate_);
|
||||
i::HeapObject heap_object;
|
||||
if (object.GetHeapObjectIfStrong(&heap_object)) {
|
||||
if (!VisitObject(heap_object)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool VisitObject(i::HeapObject obj) {
|
||||
DCHECK(!error_.has_value());
|
||||
DCHECK(!obj.is_null());
|
||||
|
||||
i::DisallowGarbageCollection no_gc;
|
||||
i::InstanceType obj_type = obj.map().instance_type();
|
||||
|
||||
// Skip common types that can't contain items to freeze.
|
||||
if (!MayContainObjectsToFreeze(obj_type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!done_list_.insert(obj).second) {
|
||||
// If we couldn't insert (because it is already in the set) then we're
|
||||
// done.
|
||||
return true;
|
||||
}
|
||||
|
||||
// For contexts we need to ensure that all accessible locals are const.
|
||||
// If not they could be replaced to bypass freezing.
|
||||
if (i::InstanceTypeChecker::IsContext(obj_type)) {
|
||||
i::ScopeInfo scope_info = i::Context::cast(obj).scope_info();
|
||||
for (auto it : i::ScopeInfo::IterateLocalNames(&scope_info, no_gc)) {
|
||||
if (scope_info.ContextLocalMode(it->index()) !=
|
||||
i::VariableMode::kConst) {
|
||||
DCHECK(!error_.has_value());
|
||||
error_ = ErrorInfo{i::MessageTemplate::kCannotDeepFreezeValue,
|
||||
i::handle(it->name(), isolate_)};
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (i::InstanceTypeChecker::IsJSReceiver(obj_type)) {
|
||||
i::Handle<i::JSReceiver> receiver =
|
||||
i::handle(i::JSReceiver::cast(obj), isolate_);
|
||||
if (!IsJSReceiverSafeToFreeze(obj_type)) {
|
||||
DCHECK(!error_.has_value());
|
||||
error_ = ErrorInfo{i::MessageTemplate::kCannotDeepFreezeObject,
|
||||
i::handle(receiver->class_name(), isolate_)};
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save this to freeze after we are done. Freezing triggers garbage
|
||||
// collection which doesn't work well with this visitor pattern, so we
|
||||
// delay it until after.
|
||||
objects_to_freeze_.push_back(receiver);
|
||||
|
||||
} else {
|
||||
DCHECK(!i::InstanceTypeChecker::IsContext(obj_type) &&
|
||||
!i::InstanceTypeChecker::IsJSReceiver(obj_type));
|
||||
}
|
||||
|
||||
DCHECK(!error_.has_value());
|
||||
obj.Iterate(isolate_, this);
|
||||
// Iterate sets error_ on failure. We should propagate errors.
|
||||
return !error_.has_value();
|
||||
}
|
||||
|
||||
i::Isolate* isolate_;
|
||||
std::unordered_set<i::Object, i::Object::Hasher> done_list_;
|
||||
std::vector<i::Handle<i::JSReceiver>> objects_to_freeze_;
|
||||
base::Optional<ErrorInfo> error_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Maybe<void> Context::DeepFreeze() {
|
||||
i::Handle<i::Context> env = Utils::OpenHandle(this);
|
||||
i::Isolate* i_isolate = env->GetIsolate();
|
||||
|
||||
// TODO(behamilton): Incorporate compatibility improvements similar to NodeJS:
|
||||
// https://github.com/nodejs/node/blob/main/lib/internal/freeze_intrinsics.js
|
||||
// These need to be done before freezing.
|
||||
|
||||
Local<Context> context = Utils::ToLocal(env);
|
||||
ENTER_V8_NO_SCRIPT(i_isolate, context, Context, DeepFreeze, Nothing<void>(),
|
||||
i::HandleScope);
|
||||
ObjectVisitorDeepFreezer vfreezer(i_isolate);
|
||||
has_pending_exception = !vfreezer.DeepFreeze(env);
|
||||
|
||||
RETURN_ON_FAILED_EXECUTION_PRIMITIVE(void);
|
||||
return JustVoid();
|
||||
}
|
||||
|
||||
v8::Isolate* Context::GetIsolate() {
|
||||
i::Handle<i::Context> env = Utils::OpenHandle(this);
|
||||
return reinterpret_cast<Isolate*>(env->GetIsolate());
|
||||
|
@ -711,7 +711,10 @@ namespace internal {
|
||||
T(OptionalChainingNoSuper, "Invalid optional chain from super property") \
|
||||
T(OptionalChainingNoTemplate, "Invalid tagged template on optional chain") \
|
||||
/* AggregateError */ \
|
||||
T(AllPromisesRejected, "All promises were rejected")
|
||||
T(AllPromisesRejected, "All promises were rejected") \
|
||||
T(CannotDeepFreezeObject, "Cannot DeepFreeze object of type %") \
|
||||
T(CannotDeepFreezeValue, "Cannot DeepFreeze non-const value %")
|
||||
|
||||
enum class MessageTemplate {
|
||||
#define TEMPLATE(NAME, STRING) k##NAME,
|
||||
MESSAGE_TEMPLATES(TEMPLATE)
|
||||
|
@ -144,6 +144,7 @@ class RuntimeCallTimer final {
|
||||
V(BigUint64Array_New) \
|
||||
V(BooleanObject_BooleanValue) \
|
||||
V(BooleanObject_New) \
|
||||
V(Context_DeepFreeze) \
|
||||
V(Context_New) \
|
||||
V(Context_NewRemoteContext) \
|
||||
V(DataView_New) \
|
||||
|
@ -29553,3 +29553,265 @@ TEST(WasmAbortStreamingAfterContextDisposal) {
|
||||
wasm_streaming.reset();
|
||||
}
|
||||
#endif // V8_ENABLE_WEBASSEMBLY
|
||||
|
||||
TEST(DeepFreezeIncompatibleTypes) {
|
||||
const int numCases = 7;
|
||||
struct {
|
||||
const char* script;
|
||||
const char* exception;
|
||||
} test_cases[numCases] = {
|
||||
{
|
||||
R"(
|
||||
"use strict"
|
||||
let foo = 1;
|
||||
)",
|
||||
"TypeError: Cannot DeepFreeze non-const value foo"},
|
||||
{
|
||||
R"(
|
||||
"use strict"
|
||||
const foo = 1;
|
||||
const generator = function*() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
}
|
||||
const gen = generator();
|
||||
)",
|
||||
"TypeError: Cannot DeepFreeze object of type Generator"},
|
||||
{
|
||||
R"(
|
||||
"use strict"
|
||||
const incrementer = (function() {
|
||||
let a = 1;
|
||||
return function() { a += 1; return a; };
|
||||
})();
|
||||
)",
|
||||
"TypeError: Cannot DeepFreeze non-const value a"},
|
||||
{
|
||||
R"(
|
||||
let a = new Number();
|
||||
)",
|
||||
"TypeError: Cannot DeepFreeze non-const value a"},
|
||||
{
|
||||
R"(
|
||||
const a = [0, 1, 2, 3, 4, 5];
|
||||
var it = a[Symbol.iterator]();
|
||||
function foo() {
|
||||
return it.next().value;
|
||||
}
|
||||
foo();
|
||||
)",
|
||||
"TypeError: Cannot DeepFreeze object of type Array Iterator"},
|
||||
{
|
||||
R"(
|
||||
const a = "0123456789";
|
||||
var it = a[Symbol.iterator]();
|
||||
function foo() {
|
||||
return it.next().value;
|
||||
}
|
||||
foo();
|
||||
)",
|
||||
"TypeError: Cannot DeepFreeze object of type Object"},
|
||||
{R"(
|
||||
const a = "0123456789";
|
||||
var it = a.matchAll(/\d/g);
|
||||
function foo() {
|
||||
return it.next().value;
|
||||
}
|
||||
foo();
|
||||
)",
|
||||
"TypeError: Cannot DeepFreeze object of type Object"},
|
||||
};
|
||||
|
||||
for (int idx = 0; idx < numCases; idx++) {
|
||||
LocalContext env;
|
||||
v8::Isolate* isolate = env->GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
v8::Local<v8::Context> context = env.local();
|
||||
v8::Maybe<void> maybe_success = v8::Nothing<void>();
|
||||
CompileRun(context, test_cases[idx].script);
|
||||
v8::TryCatch tc(isolate);
|
||||
maybe_success = context->DeepFreeze();
|
||||
CHECK(maybe_success.IsNothing());
|
||||
CHECK(tc.HasCaught());
|
||||
v8::String::Utf8Value uS(isolate, tc.Exception());
|
||||
std::string exception(*uS, uS.length());
|
||||
CHECK_EQ(std::string(test_cases[idx].exception), exception);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DeepFreezeIsFrozen) {
|
||||
const int numCases = 10;
|
||||
struct {
|
||||
const char* script;
|
||||
const char* exception;
|
||||
int32_t expected;
|
||||
} test_cases[numCases] = {
|
||||
{// Closure
|
||||
R"(
|
||||
const incrementer = (function() {
|
||||
const a = {b: 1};
|
||||
return function() { a.b += 1; return a.b; };
|
||||
})();
|
||||
const foo = function() { return incrementer(); }
|
||||
foo();
|
||||
)",
|
||||
nullptr, 2},
|
||||
{
|
||||
R"(
|
||||
const incrementer = (function() {
|
||||
const a = {b: 1};
|
||||
return function() { a.b += 1; return a.b; };
|
||||
})();
|
||||
const foo = function() { return incrementer(); }
|
||||
foo();
|
||||
)",
|
||||
nullptr, 2},
|
||||
{// Array
|
||||
R"(
|
||||
const a = [0, -1, -2];
|
||||
const foo = function() { a[0] += 1; return a[0]; }
|
||||
)",
|
||||
nullptr, 0},
|
||||
{
|
||||
R"(
|
||||
const a = [0, -1, -2];
|
||||
const foo = function() { a[0] += 1; return a[0]; }
|
||||
)",
|
||||
nullptr, 0},
|
||||
{// Wrapper Objects
|
||||
R"(
|
||||
const a = {b: new Number()};
|
||||
const foo = function() {
|
||||
a.b = new Number(a.b + 1);
|
||||
return a.b.valueOf();
|
||||
}
|
||||
)",
|
||||
nullptr, 0},
|
||||
{// Functions
|
||||
// Assignment to constant doesn't work.
|
||||
R"(
|
||||
const foo = function() {
|
||||
foo = function() { return 2;}
|
||||
return 1;
|
||||
}
|
||||
)",
|
||||
"TypeError: Assignment to constant variable.", 0},
|
||||
{
|
||||
R"(
|
||||
const a = {b: {c: {d: {e: {f: 1}}}}};
|
||||
const foo = function() {
|
||||
a.b.c.d.e.f += 1;
|
||||
return a.b.c.d.e.f;
|
||||
}
|
||||
)",
|
||||
nullptr, 1},
|
||||
{
|
||||
R"(
|
||||
const foo = function() {
|
||||
if (!('count' in globalThis))
|
||||
globalThis.count = 1;
|
||||
++count;
|
||||
return count;
|
||||
}
|
||||
)",
|
||||
"ReferenceError: count is not defined", 0},
|
||||
{
|
||||
R"(
|
||||
const countPrototype = {
|
||||
get() {
|
||||
return 1;
|
||||
},
|
||||
};
|
||||
const count = Object.create(countPrototype);
|
||||
function foo() {
|
||||
const curr_count = count.get();
|
||||
count.prototype = { get() { return curr_count + 1; }};
|
||||
return count.get();
|
||||
}
|
||||
)",
|
||||
nullptr, 1},
|
||||
{
|
||||
R"(
|
||||
const a = (function(){
|
||||
function A(){};
|
||||
A.o = 1;
|
||||
return new A();
|
||||
})();
|
||||
function foo() {
|
||||
a.constructor.o++;
|
||||
return a.constructor.o;
|
||||
}
|
||||
)",
|
||||
nullptr, 1},
|
||||
};
|
||||
for (int idx = 0; idx < numCases; idx++) {
|
||||
LocalContext env;
|
||||
v8::Isolate* isolate = env->GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
v8::Local<v8::Context> context = env.local();
|
||||
v8::Maybe<void> maybe_success = v8::Nothing<void>();
|
||||
v8::TryCatch tc(isolate);
|
||||
v8::MaybeLocal<v8::Value> status =
|
||||
CompileRun(context, test_cases[idx].script);
|
||||
CHECK(!status.IsEmpty());
|
||||
CHECK(!tc.HasCaught());
|
||||
|
||||
maybe_success = context->DeepFreeze();
|
||||
CHECK(!tc.HasCaught());
|
||||
status = CompileRun(context, "foo()");
|
||||
|
||||
if (test_cases[idx].exception) {
|
||||
CHECK(tc.HasCaught());
|
||||
v8::String::Utf8Value uS(isolate, tc.Exception());
|
||||
std::string exception(*uS, uS.length());
|
||||
CHECK_EQ(std::string(test_cases[idx].exception), exception);
|
||||
} else {
|
||||
CHECK(!tc.HasCaught());
|
||||
CHECK(!status.IsEmpty());
|
||||
ExpectInt32("foo()", test_cases[idx].expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DeepFreezeAllowsSyntax) {
|
||||
const int numCases = 2;
|
||||
struct {
|
||||
const char* script;
|
||||
int32_t expected;
|
||||
} test_cases[numCases] = {
|
||||
{
|
||||
R"(
|
||||
const a = 1;
|
||||
function foo() {
|
||||
let b = 4;
|
||||
b += 1;
|
||||
return a + b;
|
||||
}
|
||||
)",
|
||||
6,
|
||||
},
|
||||
{
|
||||
R"(
|
||||
var a = 1;
|
||||
function foo() {
|
||||
let b = 4;
|
||||
b += 1;
|
||||
return a + b;
|
||||
}
|
||||
)",
|
||||
6,
|
||||
}}; // TODO(behamilton): Add more cases that should be supported.
|
||||
for (int idx = 0; idx < numCases; idx++) {
|
||||
LocalContext env;
|
||||
v8::Isolate* isolate = env->GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
v8::Local<v8::Context> context = env.local();
|
||||
v8::Maybe<void> maybe_success = v8::Nothing<void>();
|
||||
v8::MaybeLocal<v8::Value> status =
|
||||
CompileRun(context, test_cases[idx].script);
|
||||
CHECK(!status.IsEmpty());
|
||||
maybe_success = context->DeepFreeze();
|
||||
CHECK(!maybe_success.IsNothing());
|
||||
ExpectInt32("foo()", test_cases[idx].expected);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user