/** @file GcosTestutil.cpp * * @author Roland Conybeare, Apr 2026 **/ #include "GcosTestutil.hpp" #include "MockCollector.hpp" #include #include #include #include #include #include #include #include #include #include #include #include namespace ut { using xo::scm::ListOps; using xo::scm::DList; using xo::scm::DBoolean; using xo::scm::DInteger; using xo::mm::ACollector; using xo::mm::DMockCollector; using xo::mm::X1VerifyStats; using xo::mm::MutationLog; using xo::mm::GCObjectStore; using xo::mm::AGCObject; using xo::mm::AAllocator; using xo::mm::DArena; using xo::mm::AllocInfo; using xo::mm::Role; using xo::mm::Generation; using xo::mm::c_max_generation; using xo::mm::object_age; using xo::rng::xoshiro256ss; using xo::facet::TypeRegistry; using xo::facet::obj; using xo::facet::typeseq; using xo::facet::impl_for; using xo::scope; using xo::xtag; /** Create two isomorphic object graphs. * Each graph comprises a single DList cell * that points to itself **/ void GcosTestutil::selfcycle_object_graph(std::vector * p_v1, GCObjectStore * p_gcos, std::vector * p_v2, DArena * arena2) { auto alloc1 = obj(p_gcos->new_space()); auto alloc2 = obj(arena2); auto t1 = DBoolean::box(alloc1, true); auto t2 = DBoolean::box(alloc2, true); auto l1 = ListOps::cons(alloc1, t1, ListOps::nil()); auto l2 = ListOps::cons(alloc2, t2, ListOps::nil()); // shortcut. Can get away with skipping mm_do_assign(), // because we know lhs of assignment is in the youngest generation l1->head_ = l1; // l1->assign_head(gc, l1); // need collector facet l2->head_ = l2; // l2->assign_head(gc, l2); // need collector facet p_v1->push_back(Recd(l1, sizeof(DList), typeseq::id())); p_v2->push_back(Recd(l2, sizeof(DList), typeseq::id())); } /** Create two isomorphic random object graphs containing @p n_obj nodes * Using a few basic data types from xo-object2 * DBoolean * DList * * Generated objects stored in @p *p_gcos. * Individual items pushed to @p *p_v. * * Isomorphic copy in @p *p_arena2, * with individual items pushed to @p *p_v2. * * For each i in rance the node (*p_v)[i] is isomorphic to (*p_v2)[i] * (*p_v)[i] allocated entirely from @p p_gcos->new_space() * (*p_v2)[i] allocated entirely from @p p_arena2 **/ void GcosTestutil::random_object_graph(uint32_t n_new_obj, uint32_t n_assign, bool debug_flag, xoshiro256ss * p_rgen, std::vector * p_v, GCObjectStore * p_gcos, std::vector * p_v2, DArena * p_arena2) { scope log(XO_DEBUG(debug_flag)); if (n_new_obj == 0 && n_assign == 0) return; // TODO: combine // alloc setup w/ gco_construct_ab_object_graphs() bolierplate for (uint32_t i_obj = 0; i_obj < n_new_obj; ++i_obj) { auto alloc = obj(p_gcos->new_space()); uint32_t sample = (*p_rgen)() % 100; // randomly-constructed node in object graph obj xi; uint64_t alloc_z; typeseq tseq; // 2nd allocator for copy of object model auto alloc2 = obj(p_arena2); // isomorphic node destined for arena2 obj xi2; if (sample < 50) { // create a DBoolean bool value = ((*p_rgen)() % 2 == 0); xi = DBoolean::box(alloc, value); alloc_z = sizeof(DBoolean); tseq = typeseq::id(); xi2 = DBoolean::box(alloc2, value); } else { // create a DList cell, with random {car, cdr} obj car = ListOps::nil(); obj cdr = ListOps::nil(); obj car2 = ListOps::nil(); obj cdr2 = ListOps::nil(); auto z = p_v->size(); if (z > 0) { // random car { uint32_t i = ((*p_rgen)() % z); car = p_v->at(i).gco_; car2 = p_v2->at(i).gco_; } // random cdr { uint32_t i = ((*p_rgen)() % z); // is v[i] a list cell? { auto tmp = obj::from(p_v->at(i).gco_); if (tmp) cdr = tmp; } { auto tmp2 = obj::from(p_v2->at(i).gco_); if (tmp2) cdr2 = tmp2; } } } xi = ListOps::cons(alloc, car, cdr); alloc_z = sizeof(DList); tseq = typeseq::id(); xi2 = ListOps::cons(alloc2, car2, cdr2); } p_v->push_back(Recd(xi, alloc_z, tseq)); // also save parallel copy p_v2->push_back(Recd(xi2, alloc_z, tseq)); } // also make some random modifications, // so that it's possible to create cycles. for (uint32_t j = 0; j < n_assign; ++j) { // choose an object at random uint32_t lhs_ix = (*p_rgen)() % p_v->size(); assert(lhs_ix < p_v->size()); // is it a list cell? auto xj1 = obj::from((*p_v)[lhs_ix].gco_); auto xj2 = obj::from((*p_v2)[lhs_ix].gco_); if (xj1) { assert(xj2); // flip a coin -- try modifying one of {car, cdr} uint32_t sample = (*p_rgen)() % 100; if (sample < 50) { // modify head. skip usual gc write-barrier stuff uint32_t rhs_ix = (*p_rgen)() % p_v->size(); auto rhs1 = (*p_v)[rhs_ix].gco_; auto rhs2 = (*p_v2)[rhs_ix].gco_; if (log) { log("replacing edge in random object graph"); log(xtag("n-obj", p_v->size())); log(xtag("lhs-ix", lhs_ix)); log(xtag("rhs-ix", rhs_ix)); log(xtag("rhs.tname", TypeRegistry::id2name(rhs1._typeseq()))); } // rhs1 could even be xj1 itself (in which case rhs2 is xj2) xj1->head_ = rhs1; xj2->head_ = rhs2; } else { // don't modify DList.rest_, risks losing acyclic propertly. // GCObjectStore handles this, but DList.size() assumes // list is acyclic } } } } /*random_object_graph*/ void GcosTestutil::gcos_install_test_types(bool do_type_registration, GCObjectStore * p_gcos) { // verify that GCOS recongnizes as registered, // the types we intend using for unit test if (do_type_registration) { { REQUIRE(p_gcos->install_type(impl_for())); REQUIRE(p_gcos->is_type_installed(typeseq::id())); } { REQUIRE(p_gcos->install_type(impl_for())); REQUIRE(p_gcos->is_type_installed(typeseq::id())); } { REQUIRE(p_gcos->install_type(impl_for())); REQUIRE(p_gcos->is_type_installed(typeseq::id())); } } } void GcosTestutil::gcos_verify_arena_partitioning(uint32_t n_gen, size_t gc_size, const GCObjectStore & gcos) { Generation g0{0}; Generation g1{1}; Generation gn{n_gen}; // verify basic arena partitioning + sizing REQUIRE(g0 != g1); REQUIRE(gcos.new_space()); REQUIRE(gcos.new_space() == gcos.get_space(Role::to_space(), g0)); REQUIRE(gcos.new_space()->reserved() >= gc_size); REQUIRE(gcos.from_space(g0)); for (Generation gi = g1; gi < n_gen; ++gi) { // all configured generations exist REQUIRE(gcos.to_space(gi)); REQUIRE(gcos.from_space(gi)); // to- and from- space are distinct REQUIRE(gcos.to_space(gi) != gcos.from_space(gi)); // arenas for different generations are distinct for (Generation gj = g0; gj < gi; ++gj) { REQUIRE(gcos.to_space(gi) != gcos.to_space(gj)); REQUIRE(gcos.from_space(gi) != gcos.to_space(gj)); REQUIRE(gcos.to_space(gi) != gcos.from_space(gj)); REQUIRE(gcos.from_space(gi) != gcos.to_space(gj)); } } // generations that weren't requested, don't exist if (gn < c_max_generation) { REQUIRE(!gcos.to_space(gn)); REQUIRE(!gcos.from_space(gn)); } } void GcosTestutil::gcos_verify_vacant(uint32_t n_gen, size_t gc_size, const GCObjectStore & gcos) { Generation g0{0}; Generation gn{n_gen}; // verify we have non-zero space! { for (Generation gi = g0; gi < gn; ++gi) { INFO(tostr(xtag("gi", gi))); REQUIRE(gcos.to_space(gi)->allocated() == 0); REQUIRE(gcos.to_space(gi)->reserved() >= gc_size); REQUIRE(gcos.from_space(gi)->allocated() == 0); REQUIRE(gcos.from_space(gi)->reserved() >= gc_size); } } } /** Generate two copies of a random object graph for test case @p tc. * Store first graph in @p *p_x1_v, allocating * entirely from @p p_gcos new-space. * Store second graph in @p *p_x2_v, allocating * entirely from @p p_arena2. * Use random number generator @p_rgen * * @p loop_index counts iteration with one gc-like phase. **/ void GcosTestutil::gcos_construct_ab_object_graphs(TestSequence test_seq, TestGraphType obj_graph_type, uint32_t n_i0_test_obj, uint32_t n_i0_test_assign, uint32_t n_i1_test_obj, uint32_t n_i1_test_assign, bool debug_flag, MutationLogStore * p_mls, GCObjectStore * p_gcos, DArena * p_arena2, uint32_t loop_index, std::vector * p_x1_v, std::vector * p_x2_v, xoshiro256ss * p_rgen) { /** TestSequence memory layout * * test_seq * | * v * TestSequence * +-----------+ cmd_seq_[] (shared, sentinel-terminated) * | cmd_seq_ |---> +-------+-------+-------+-------+-------+-------+ * +-----------+ | step0 | step1 | step2 | step3 | step4 | SENTL | * | phases_ |-\ +-------+-------+-------+-------+-------+-------+ * +-----------+ | ix: 0 1 2 3 4 5 * | * | phases_[] (sentinel-terminated) * \-> +-----------+-----------+ * | Phase 0 | SENTINEL | * | lo_ix_=0 | lo_ix_=-1 | * | hi_ix_=5 | hi_ix_=-1 | * | mlog_new_ | mlog_new_ | * +-----------+-----------+ * * Phase.lo_ix_ / hi_ix_ index into test_seq->cmd_seq_[]. * Phase 0 executes cmd_seq_[lo_ix_ .. hi_ix_), i.e. steps 0..4. * Sentinel phase has lo_ix_ == -1. * * Each Step has {cmd_, arg0_ix_, arg1_ix_}. * arg0_ix_ and arg1_ix_ index into x1_v[] / x2_v[], * referring to objects created by earlier steps. * * Example (seq1): * step0: {make_bool, 0, 0} -> x1_v[0] = #f * step1: {make_bool, 1, 0} -> x1_v[1] = #t * step2: {make_nil, 0, 0} -> x1_v[2] = () * step3: {make_cons, 0, 2} -> x1_v[3] = cons(x1_v[0], x1_v[2]) = (#f) * step4: {assign_head, 3, 1} -> set-car!(x1_v[3], x1_v[1]) => (#t) **/ if (!test_seq.is_sentinel()) { // Explicit command sequence. // Each command creates a new node or modifies an existing one // 1. Sequence of commands for this call. // Will be phases[loop_index] if well-defined. // 2. Expected effect on mutation log // Phase * phase_expect = nullptr; { Phase * p_phase = test_seq.phases_; if (test_seq.phases_) { for (uint32_t i = 0; i < loop_index; ++i) { if (!p_phase->is_sentinel()) ++p_phase; else p_phase = nullptr; } } phase_expect = p_phase; } Step * cmd_seq = test_seq.cmd_seq_; if (phase_expect && cmd_seq) { // Do scripted sequence only. // For this phases that is // cmd_seq[ix] // for // phase_expect->lo_ix_ <= ix < phase_expect->hi_ix_ auto alloc = obj(p_gcos->new_space()); auto alloc2 = obj(p_arena2); DMockCollector mock(p_mls, p_gcos); auto mockgc = obj(&mock); for (int32_t ix = phase_expect->lo_ix_, hi = phase_expect->hi_ix_; ix < hi; ++ix) { const Step & cmd = cmd_seq[ix]; bool is_alloc = false; obj xi; obj xi2; uint64_t alloc_z = 0; typeseq tseq; switch (cmd.cmd_) { case Step::Cmd::sentinel: assert(false); // unreachable break; case Step::Cmd::make_nil: // TODO combine with code in random_object_graph() { is_alloc = true; xi = ListOps::nil(); alloc_z = 0; // not in gcos space tseq = typeseq::id(); xi2 = ListOps::nil(); REQUIRE(xi._typeseq() == tseq); REQUIRE(xi2._typeseq() == tseq); } break; case Step::Cmd::make_cons: // TODO combine with code in random_object_graph() { auto h1 = p_x1_v->at(cmd.arg0_ix_).gco_; auto r1 = obj::from(p_x1_v->at(cmd.arg1_ix_).gco_); auto h2 = p_x2_v->at(cmd.arg0_ix_).gco_; auto r2 = obj::from(p_x2_v->at(cmd.arg1_ix_).gco_); is_alloc = true; xi = ListOps::cons(alloc, h1, r1); alloc_z = sizeof(DList); tseq = typeseq::id(); xi2 = ListOps::cons(alloc2, h2, r2); } break; case Step::Cmd::make_bool: // TODO combine with code in random_object_graph() { bool value = (cmd.arg0_ix_ > 0); is_alloc = true; xi = DBoolean::box(alloc, value); alloc_z = sizeof(DBoolean); tseq = typeseq::id(); xi2 = DBoolean::box(alloc2, value); } break; case Step::Cmd::make_int: { int value = cmd.arg0_ix_; is_alloc = true; xi = DInteger::box(alloc, value); alloc_z = sizeof(DInteger); tseq = typeseq::id(); xi2 = DInteger::box(alloc2, value); } break; case Step::Cmd::assign_root: { is_alloc = false; auto z1 = p_x1_v->at(cmd.arg1_ix_).alloc_z_; auto tseq1 = p_x1_v->at(cmd.arg1_ix_).tseq_; auto rhs1 = p_x1_v->at(cmd.arg1_ix_).gco_; auto z2 = p_x2_v->at(cmd.arg1_ix_).alloc_z_; auto tseq2 = p_x2_v->at(cmd.arg1_ix_).tseq_; auto rhs2 = p_x2_v->at(cmd.arg1_ix_).gco_; p_x1_v->at(cmd.arg0_ix_).alloc_z_ = z1; p_x1_v->at(cmd.arg0_ix_).tseq_ = tseq1; p_x1_v->at(cmd.arg0_ix_).gco_ = rhs1; p_x2_v->at(cmd.arg0_ix_).alloc_z_ = z2; p_x2_v->at(cmd.arg0_ix_).tseq_ = tseq2; p_x2_v->at(cmd.arg0_ix_).gco_ = rhs2; } break; case Step::Cmd::assign_head: { is_alloc = false; auto lhs1 = obj::from(p_x1_v->at(cmd.arg0_ix_).gco_); auto rhs1 = p_x1_v->at(cmd.arg1_ix_).gco_; auto lhs2 = obj::from(p_x2_v->at(cmd.arg0_ix_).gco_); auto rhs2 = p_x2_v->at(cmd.arg1_ix_).gco_; assert(lhs1); assert(!lhs1->is_empty()); assert(lhs2); assert(!lhs2->is_empty()); assert(p_mls); assert(mockgc); lhs1->assign_head_gc(mockgc, rhs1); // alloc2 is ord arena -> no mlog } break; } if (is_alloc) { p_x1_v->push_back(Recd(xi, alloc_z, tseq)); p_x2_v->push_back(Recd(xi2, alloc_z, tseq)); } } // check expected results for (Generation gi{0}; gi + 1 < Generation(p_mls->config().n_generation_); ++gi) { MutationLog * mlog = p_mls->get_mlog(Role::to_space(), gi); REQUIRE(mlog); REQUIRE(mlog->size() == phase_expect->mlog_new_z_[gi]); } } } else { switch (obj_graph_type) { case TestGraphType::fixed: assert(false); // unreachable break; case TestGraphType::selfcycle: if (loop_index == 0) { GcosTestutil::selfcycle_object_graph(p_x1_v, p_gcos, p_x2_v, p_arena2); } break; case TestGraphType::random: { uint32_t n_test_obj = ((loop_index == 0) ? n_i0_test_obj : n_i1_test_obj); uint32_t n_test_assign = ((loop_index == 0) ? n_i0_test_assign : n_i1_test_assign); GcosTestutil::random_object_graph(n_test_obj, n_test_assign, debug_flag, p_rgen, p_x1_v, p_gcos, p_x2_v, p_arena2); } break; } } //x1_v.push_back(Recd(DBoolean::box(alloc, true), // sizeof(DBoolean), // typeseq::id())); } void GcosTestutil::gcos_verify_consistency(GCObjectStore * p_gcos) { // traverses stored objects, updates counters // in verify_stats (= gco.p_verify_stats_, via ctor) // p_gcos->verify_ok(); X1VerifyStats * verify_stats = p_gcos->verify_stats(); INFO(tostr(xtag("n_gc_root", verify_stats->n_gc_root_), xtag("n_ext", verify_stats->n_ext_), xtag("n_from", verify_stats->n_from_), xtag("n_to", verify_stats->n_to_), xtag("n_fwd", verify_stats->n_fwd_), xtag("n_age_ok", verify_stats->n_age_ok_), xtag("n_age_bad", verify_stats->n_age_bad_), xtag("n_no_iface", verify_stats->n_no_iface_))); REQUIRE(verify_stats->is_ok()); } void GcosTestutil::gcos_verify_ab_equivalence(const std::vector & x1_v, const std::vector & x2_v) { REQUIRE(x1_v.size() == x2_v.size()); for (size_t i = 0, n = x1_v.size(); i < n; ++i) { REQUIRE(x1_v[i].alloc_z_ == x2_v[i].alloc_z_); REQUIRE(x1_v[i].tseq_ == x2_v[i].tseq_); REQUIRE(x1_v[i].gco_._typeseq() == x1_v[i].tseq_); REQUIRE(x2_v[i].gco_._typeseq() == x2_v[i].tseq_); } } void GcosTestutil::gcos_verify_allocinfo(const GCObjectStore & gcos, uint32_t loop_index, const std::vector & x1_v) { // gcos can reveal info about allocs for (size_t i = 0, n = x1_v.size(); i < n; ++i) { const auto & x1 = x1_v.at(i); // x1 could be a global, such as ListOps::nil() if (x1.alloc_z_ > 0) { REQUIRE(gcos.contains_allocated(Role::to_space(), x1.gco_.data())); AllocInfo obj_info = gcos.alloc_info((std::byte *)x1.gco_.data()); REQUIRE(obj_info.size() >= x1.alloc_z_); REQUIRE(obj_info.payload().first == (std::byte *)x1.gco_.data()); REQUIRE(obj_info.tseq() == x1.tseq_.seqno()); // also can use header2size / header2tseq convenience functions REQUIRE(gcos.header2size(obj_info.header()) == obj_info.size()); REQUIRE(gcos.header2age(obj_info.header()) <= object_age{loop_index}); REQUIRE(gcos.header2tseq(obj_info.header()) == obj_info.tseq()); REQUIRE(gcos.is_forwarding_header(obj_info.header()) == false); } else { REQUIRE(!gcos.contains(Role::to_space(), x1.gco_.data())); REQUIRE(!gcos.contains(Role::from_space(), x1.gco_.data())); } } } void GcosTestutil::gcos_verify_gen0_only_allocated(uint32_t n_gen, const GCObjectStore & gcos, uint32_t loop_index, const std::vector & x1_v) { Generation g0{0}; Generation gn{n_gen}; // new objects appear in to-space for generation 0. for (Generation gi = g0; gi < gn; ++gi) { INFO(tostr(xtag("gi", gi))); if (loop_index == 0) { if ((gi == 0) && (x1_v.size() > 0)) { // conceivable that x1_v[] only contains non-gco objects //REQUIRE(gcos.to_space(gi)->allocated() > 0); } else { REQUIRE(gcos.to_space(gi)->allocated() == 0); } } REQUIRE(gcos.from_space(gi)->allocated() == 0); } } void GcosTestutil::gcos_verify_gen0_fromspace_only_allocated(uint32_t n_gen, const GCObjectStore & gcos, uint32_t loop_index, Generation upto, const std::vector & x1_v) { Generation g0{0}; Generation gn{n_gen}; for (Generation gi = g0; gi < gn; ++gi) { if (gi < upto) { // we're collecting generation gi. // Before we begin, to-space had better be empty // (everthing in gi is in from-space) REQUIRE(gcos.to_space(gi)->allocated() == 0); } else { // we're not collecting generation gi. // from-space must be empty. // May have content in to-space REQUIRE(gcos.from_space(gi)->allocated() == 0); } } for (size_t i = 0, n = x1_v.size(); i < n; ++i) { const auto & x1 = x1_v.at(i); // x1 should be in gen g from-space (with g < upto) // or in gen g to-space (with g >= upto) if (x1.alloc_z_ > 0) { Generation g_from = gcos.generation_of(Role::from_space(), x1.gco_.data()); Generation g_to = gcos.generation_of(Role::to_space(), x1.gco_.data()); if (g_to.is_sentinel()) { // if not in to-space, must be in from-space REQUIRE(!g_from.is_sentinel()); // + for some gen we're collecting REQUIRE(g_from < upto); REQUIRE(gcos.contains(Role::from_space(), x1.gco_.data())); REQUIRE(gcos.contains_allocated(Role::from_space(), x1.gco_.data())); } else { // if in to-space, must not be in from-space REQUIRE(g_from.is_sentinel()); // + for some gen we're not collecting REQUIRE(g_to >= upto); REQUIRE(gcos.contains(Role::to_space(), x1.gco_.data())); REQUIRE(gcos.contains_allocated(Role::to_space(), x1.gco_.data())); } AllocInfo obj_info = gcos.alloc_info((std::byte *)x1.gco_.data()); REQUIRE(obj_info.size() >= x1.alloc_z_); REQUIRE(obj_info.payload().first == (std::byte *)x1.gco_.data()); REQUIRE(obj_info.tseq() == x1.tseq_.seqno()); } else { REQUIRE(!gcos.contains(Role::to_space(), x1.gco_.data())); REQUIRE(!gcos.contains(Role::from_space(), x1.gco_.data())); } } } void GcosTestutil::gcos_move_roots_and_verify(bool do_type_registration, GCObjectStore * p_gcos, Generation upto, const std::vector & x1_v, const std::vector & x2_v, bool debug_flag) { scope log(XO_DEBUG(debug_flag)); Generation g1{1}; // try moving everything to to-space. // For this to week we must have registered the type, // so gc knows how to traverse it // for (size_t i = 0, n = x1_v.size(); i < n; ++i) { const auto & x1 = x1_v.at(i); const auto & x2 = x2_v.at(i); log && log("moving roots"); log && log(xtag("i", i), xtag("n", n), xtag("x1.tseq_", x1.tseq_), xtag("x1.tname", TypeRegistry::id2name(x1.tseq_))); if (do_type_registration) { /* Action of this loop iteration: * * gcos arena2 * +------------+-----------+ +--------+ * | from | to | | | * | | | | | * | +----+ | +-----+ | | +----+ | * | | x1 |---->| x1p | | | | x2 | | * | +----+ | +-----+ | | +----+ | * | | | | | * +------------+-----------+ +--------+ * * Before: * x1, x2 have the same shape * After * x1 forward to x1p * x1p and x2 have the same shape */ // note: since members of x1_v[] can refer to each other, // it's possible that x1.gco_ is already a forwarding pointer // before we call deep_move_root(). AGCObject * x1p_iface = p_gcos->lookup_type(x1.tseq_); REQUIRE(x1p_iface); // snapshot root before moving obj x1_gco = x1.gco_; // modifies x1.gco_ in place auto x1p_data = p_gcos->deep_move_root(x1p_iface, (void **)&(x1.gco_.data_), upto); REQUIRE(x1p_data); REQUIRE(x1p_data == x1.gco_.data_); obj x1p_gco(x1p_iface, x1p_data); // obj (x1_gco) now forwarding pointer (to x1p_gco = x1.gco_) GcosTestutil::gcos_verify_forwarding(*p_gcos, upto, x1, x1_gco); // obj1p in to-space, same contents as original obj gcos_verify_forwarding_destination(*p_gcos, x1, x1p_gco); // x1p_gco must look like x2.gco REQUIRE(x1p_gco._typeseq() == x2.gco_._typeseq()); gcos_verify_forwarded_ab_equivalence(x1p_gco, x2.gco_); } else { // can still try to move something. // but will fail since type isn't registered auto x1p_data = p_gcos->deep_move_root(x1.gco_.iface(), (void **)&(x1.gco_.data_), g1); // control here under normal GC use // would represent a configuration fail REQUIRE(x1p_data == nullptr); } } } void GcosTestutil::gcos_verify_forwarding(const GCObjectStore & gcos, Generation upto, const Recd & x1, obj x1_gco) { if (x1.alloc_z_ > 0) { REQUIRE((gcos.contains_allocated(Role::from_space(), x1_gco.data()) || gcos.contains_allocated(Role::to_space(), x1_gco.data()))); AllocInfo obj_info = gcos.alloc_info((std::byte *)x1_gco.data()); INFO(tostr(xtag("obj_info.tseq", obj_info.tseq()), xtag("obj_info.tname", TypeRegistry::id2name(typeseq(obj_info.tseq()))))); REQUIRE(obj_info.size() >= x1.alloc_z_); REQUIRE(obj_info.payload().first == (std::byte *)x1_gco.data()); if (obj_info.is_forwarding_tseq()) { /* object was forwarded, so got collected */ REQUIRE(obj_info.is_forwarding_tseq()); } else { /* not forwarded is ok iff in generation g >= upto */ Generation g = gcos.generation_of(Role::to_space(), x1_gco.data()); REQUIRE(g >= upto); } // if (!obj_info.is_forwarding_tseq()) // print_backtrace_dwarf(true /*demangle*/); // REQUIRE(obj_info.is_forwarding_tseq()); } } void GcosTestutil::gcos_verify_forwarding_destination(const GCObjectStore & gcos, const Recd & x1, obj x1p_gco) { if (x1.alloc_z_ > 0) { REQUIRE(gcos.contains_allocated(Role::to_space(), x1p_gco.data())); AllocInfo obj1p_info = gcos.alloc_info((std::byte *)x1p_gco.data()); REQUIRE(obj1p_info.size() >= x1.alloc_z_); REQUIRE(obj1p_info.payload().first == (std::byte *)x1p_gco.data()); REQUIRE(obj1p_info.tseq() == x1.tseq_.seqno()); REQUIRE(x1p_gco.data() != nullptr); REQUIRE(gcos.contains(Role::to_space(), x1p_gco.data())); REQUIRE(gcos.contains_allocated(Role::to_space(), x1p_gco.data())); } } void GcosTestutil::gcos_verify_forwarded_ab_equivalence(obj x1p_gco, obj x2_gco) { // written out polymorphic comparison bool match_attempted = false; // match DBoolean.. { auto x1p_b = obj::from(x1p_gco); auto x2_b = obj::from(x2_gco); if (x1p_b && x2_b) { match_attempted = true; REQUIRE(x1p_b->value() == x2_b->value()); } } // match DInteger.. { auto x1p_b = obj::from(x1p_gco); auto x2_b = obj::from(x2_gco); if (x1p_b && x2_b) { match_attempted = true; REQUIRE(x1p_b->value() == x2_b->value()); } } // match DList.. { auto x1p_b = obj::from(x1p_gco); auto x2_b = obj::from(x2_gco); if (x1p_b && x2_b) { match_attempted = true; // TODO: we could figure out the index in {x1_v[], x2_v[]} // of x*_b {head, rest} respectively, // and verify they're consistent. REQUIRE(x1p_b->head()._typeseq() == x2_b->head()._typeseq()); REQUIRE(x1p_b->size() == x2_b->size()); if (x1p_b->rest()) { REQUIRE(x2_b->rest()); } else { // unreachable, since using sentinel objectd for nil list REQUIRE(x2_b->rest() == nullptr); } } } REQUIRE(match_attempted); } } /*namespace ut*/ /* end GcosTestutil.cpp */