//////////////////////////////////////////////////////////////////// // // This file contains the code to compare the contents of tables. // The top level functions in this file are: // // World::diff_numbered_tables // World::diff_tangible_databases // World::patch_numbered_tables // World::patch_tangible_databases // // It also contains these unit testing support routines: // // table.diffcompare // table.diffapply // // This file also contains all the support code needed to implement // this stuff. // //////////////////////////////////////////////////////////////////// #include "luastack.hpp" #include "streambuffer.hpp" #include "table.hpp" #include "world.hpp" // Given a table and an tnmap, return the table number of the table. // Returns zero if the table doesn't have a table number. // static int get_table_number(LuaCoreStack &MLS, LuaSlot mval, LuaSlot mtnmap) { lua_State *L = MLS.state(); lua_pushvalue(L, mval.index()); lua_rawget(L, mtnmap.index()); int result = 0; if (lua_type(L, -1) == LUA_TNUMBER) { result = lua_tointeger(L, -1); } lua_pop(L, 1); return result; } static bool equivalent_values(LuaCoreStack &MLS, LuaSlot mval, LuaSlot mtnmap, LuaCoreStack &SLS, LuaSlot sval, LuaSlot stnmap) { switch (MLS.xtype(mval)) { case LUA_TBOOLEAN: { if (SLS.type(sval) != LUA_TBOOLEAN) return false; return MLS.tryboolean(mval) == SLS.tryboolean(sval); } case LUA_TNUMBER: { if (SLS.type(sval) != LUA_TNUMBER) return false; return MLS.trynumber(mval) == SLS.trynumber(sval); } case LUA_TSTRING: { if (SLS.type(sval) != LUA_TSTRING) return false; return MLS.trystring(mval) == SLS.trystring(sval); } case LUA_TLIGHTUSERDATA: { if (SLS.type(sval) != LUA_TLIGHTUSERDATA) return false; return MLS.trytoken(mval) == SLS.trytoken(sval); } case LUA_TFUNCTION: { // Cannot really compare. Just return true if the types match. return SLS.type(sval) == MLS.type(mval); } case LUA_TT_GENERAL: { int midx = get_table_number(MLS, mval, mtnmap); if (midx == 0) { return SLS.isnil(sval); } int sidx = get_table_number(SLS, sval, stnmap); return midx == sidx; } case LUA_TT_CLASS: { if (SLS.xtype(sval) != LUA_TT_CLASS) return false; // What if it's an ill-formed class? return MLS.classname(mval) == SLS.classname(sval); } case LUA_TT_TANGIBLE: { if (SLS.xtype(sval) != LUA_TT_TANGIBLE) return false; // What if it's an ill-formed tangible? return MLS.tanid(mval) == SLS.tanid(sval); } case LUA_TT_GLOBALENV: { return (SLS.xtype(sval) == LUA_TT_GLOBALENV); } default: // We're forcing anything else to go to NIL, // and once it's NIL, we consider it 'equal'. return SLS.type(sval) == LUA_TNIL; } } static void transmit_value(LuaCoreStack &MLS, LuaSlot mval, LuaSlot mtnmap, StreamBuffer *sb) { switch (MLS.xtype(mval)) { case LUA_TBOOLEAN: { sb->write_uint8(LUA_TBOOLEAN); sb->write_bool(*MLS.tryboolean(mval)); return; } case LUA_TNUMBER: { sb->write_uint8(LUA_TNUMBER); sb->write_double(*MLS.trynumber(mval)); return; } case LUA_TSTRING: { sb->write_uint8(LUA_TSTRING); sb->write_string(*MLS.trystring(mval)); return; } case LUA_TLIGHTUSERDATA: { sb->write_uint8(LUA_TLIGHTUSERDATA); sb->write_uint64((*MLS.trytoken(mval)).value); return; } case LUA_TT_GENERAL: { int midx = get_table_number(MLS, mval, mtnmap); if (midx == 0) { sb->write_uint8(LUA_TNIL); } else { sb->write_uint8(LUA_TT_GENERAL); sb->write_uint32(midx); } return; } case LUA_TT_CLASS: { sb->write_uint8(LUA_TT_CLASS); sb->write_string(MLS.classname(mval)); return; } case LUA_TT_TANGIBLE: { sb->write_uint8(LUA_TT_TANGIBLE); sb->write_int64(MLS.tanid(mval)); return; } case LUA_TT_GLOBALENV: { sb->write_uint8(LUA_TT_GLOBALENV); return; } default: sb->write_uint8(LUA_TNIL); return; } } static void transmit_value_debug_string(StreamBuffer *sb, eng::ostringstream &oss) { int kind = sb->read_uint8(); switch (kind) { case LUA_TBOOLEAN: { bool b = sb->read_bool(); oss << (b ? "true":"false"); return; } case LUA_TNUMBER: { oss << sb->read_double(); return; } case LUA_TSTRING: { oss << sb->read_string(); return; } case LUA_TLIGHTUSERDATA: { LuaToken token(sb->read_uint64()); oss << "[" << token.str() << "]"; return; } case LUA_TT_GENERAL: { oss << "table " << sb->read_int32(); return; } case LUA_TT_CLASS: { oss << "class " << sb->read_string(); return; } case LUA_TT_TANGIBLE: { oss << "tan " << sb->read_int64(); return; } case LUA_TT_GLOBALENV: { oss << "globals"; return; } case LUA_TNIL: { oss << "nil"; return; } default: assert(false); // Should not get here. } } static bool diff_tables(LuaCoreStack &SLS0, LuaSlot stnmap, LuaSlot stab, LuaCoreStack &MLS0, LuaSlot mtnmap, LuaSlot mtab, bool cmeta, StreamBuffer *sb) { LuaVar skey, mkey, sval, mval, mnil; LuaExtStack SLS(SLS0.state(), skey, sval); LuaExtStack MLS(MLS0.state(), mkey, mval, mnil); assert(MLS.istable(mtab)); assert(SLS.istable(stab)); MLS.set(mnil, LuaNil); int nupdates = 0; sb->write_int32(0); int wc = sb->total_writes(); MLS.set(mkey, LuaNil); while (MLS.next(mtab, mkey, mval)) { if (!MLS.issortablekey(mkey)) continue; MLS.movesortablekey(mkey, SLS, skey); SLS.rawget(sval, stab, skey); if (!equivalent_values(MLS, mval, mtnmap, SLS, sval, stnmap)) { transmit_value(MLS, mkey, mtnmap, sb); transmit_value(MLS, mval, mtnmap, sb); nupdates += 1; } } SLS.set(skey, LuaNil); while (SLS.next(stab, skey, sval)) { if (!SLS.issortablekey(skey)) continue; SLS.movesortablekey(skey, MLS, mkey); MLS.rawget(mval, mtab, mkey); if (MLS.isnil(mval)) { transmit_value(MLS, mkey, mtnmap, sb); transmit_value(MLS, mval, mtnmap, sb); nupdates += 1; } } if (cmeta) { SLS.getmetatable(sval, stab); MLS.getmetatable(mval, mtab); if (!equivalent_values(MLS, mval, mtnmap, SLS, sval, stnmap)) { transmit_value(MLS, mnil, mtnmap, sb); transmit_value(MLS, mval, mtnmap, sb); nupdates += 1; } } sb->overwrite_int32(wc, nupdates); return (nupdates > 0); } static eng::string diff_tables_debug_string(StreamBuffer *sb) { eng::vector sorted; eng::ostringstream oss; int ndiffs = sb->read_int32(); for (int i = 0; i < ndiffs; i++) { transmit_value_debug_string(sb, oss); oss << "="; transmit_value_debug_string(sb, oss); sorted.push_back(oss.str()); oss.str(""); } std::sort(sorted.begin(), sorted.end()); for (const eng::string &s : sorted) { oss << s << ";"; } return oss.str(); } static void set_transmitted_value(LuaCoreStack &LS, LuaSlot tangibles, LuaSlot ntmap, LuaSlot target, StreamBuffer *sb, const char *dbinfo, DebugCollector *dbc) { int kind = sb->read_uint8(); switch (kind) { case LUA_TBOOLEAN: { bool value = sb->read_bool(); DebugLine(dbc) << dbinfo << (value ? "true" : "false"); LS.set(target, value); return; } case LUA_TNUMBER: { double value = sb->read_double(); DebugLine(dbc) << dbinfo << value; LS.set(target, value); return; } case LUA_TSTRING: { eng::string value = sb->read_string(); DebugLine(dbc) << dbinfo << "'" << value << "'"; LS.set(target, value); return; } case LUA_TLIGHTUSERDATA: { LuaToken value(sb->read_uint64()); DebugLine(dbc) << dbinfo << "[" << value.str() << "]"; LS.set(target, value); return; } case LUA_TT_GENERAL: { int index = sb->read_int32(); DebugLine(dbc) << dbinfo << "table " << index; LS.rawget(target, ntmap, index); return; } case LUA_TT_CLASS: { eng::string value = sb->read_string(); DebugLine(dbc) << dbinfo << "class " << value; LS.makeclass(target, value); return; } case LUA_TT_TANGIBLE: { int64_t id = sb->read_int64(); DebugLine(dbc) << dbinfo << "tan " << id; LS.maketan(target, id); return; } case LUA_TT_GLOBALENV: { DebugLine(dbc) << dbinfo << "global env"; LS.getglobaltable(target); return; } case LUA_TNIL: { DebugLine(dbc) << dbinfo << "nil"; LS.set(target, LuaNil); return; } default: assert(false); // Should not get here. } } static void patch_table(LuaCoreStack &LS0, LuaSlot tangibles, LuaSlot ntmap, LuaSlot tab, StreamBuffer *sb, DebugCollector *dbc) { LuaVar key, val; LuaExtStack LS(LS0.state(), key, val); int ndiffs = sb->read_int32(); for (int i = 0; i < ndiffs; i++) { set_transmitted_value(LS, tangibles, ntmap, key, sb, "key=", dbc); set_transmitted_value(LS, tangibles, ntmap, val, sb, "val=", dbc); if (LS.isnil(key)) { LS.setmetatable(tab, val); } else { LS.rawset(tab, key, val); } } } void World::patch_numbered_tables(StreamBuffer *sb, DebugCollector *dbc) { lua_State *L = state(); LuaVar tangibles, ntmap, tab; LuaExtStack LS(L, tangibles, ntmap, tab); LS.rawget(tangibles, LuaRegistry, "tangibles"); LS.rawget(ntmap, LuaRegistry, "ntmap"); assert(LS.istable(tangibles)); assert(LS.istable(ntmap)); int nmodified = sb->read_int32(); for (int i = 0; i < nmodified; i++) { int index = sb->read_int32(); LS.rawget(tab, ntmap, index); assert(LS.istable(tab)); DebugHeader(dbc) << "Lua Table " << index << ":"; patch_table(LS, tangibles, ntmap, tab, sb, dbc); } } void World::diff_numbered_tables(lua_State *master, StreamBuffer *sb) { lua_State *synch = state(); LuaVar sntmap, mntmap, stnmap, mtnmap, stab, mtab; LuaExtStack SLS(synch, sntmap, stnmap, stab); LuaExtStack MLS(master, mntmap, mtnmap, mtab); SLS.rawget(sntmap, LuaRegistry, "ntmap"); MLS.rawget(mntmap, LuaRegistry, "ntmap"); SLS.rawget(stnmap, LuaRegistry, "tnmap"); MLS.rawget(mtnmap, LuaRegistry, "tnmap"); int m_ntables = MLS.rawlen(mntmap); int s_ntables = SLS.rawlen(sntmap); assert(m_ntables == s_ntables); sb->write_int32(0); int write_count_after = sb->total_writes(); int nmodified = 0; int s_top = lua_gettop(synch); int m_top = lua_gettop(master); for (int id = 1; id <= m_ntables; id++) { MLS.rawget(mtab, mntmap, id); if (MLS.istable(mtab)) { SLS.rawget(stab, sntmap, id); assert(SLS.istable(stab)); int tw = sb->total_writes(); sb->write_int32(id); nmodified += 1; if (!diff_tables(SLS, stnmap, stab, MLS, mtnmap, mtab, true, sb)) { sb->unwrite_to(tw); nmodified -= 1; } } assert(lua_gettop(synch) == s_top); assert(lua_gettop(master) == m_top); } sb->overwrite_int32(write_count_after, nmodified); } void World::patch_tangible_databases(StreamBuffer *sb, DebugCollector *dbc) { lua_State *L = state(); LuaVar tangibles, ntmap, tab; LuaExtStack LS(L, tangibles, ntmap, tab); LS.rawget(tangibles, LuaRegistry, "tangibles"); LS.rawget(ntmap, LuaRegistry, "ntmap"); assert(LS.istable(tangibles)); assert(LS.istable(ntmap)); int nmodified = sb->read_int32(); for (int i = 0; i < nmodified; i++) { int64_t id = sb->read_int64(); LS.rawget(tab, tangibles, id); assert(LS.istable(tab)); DebugHeader(dbc) << "Tangible DB " << id << ":"; patch_table(LS, tangibles, ntmap, tab, sb, dbc); } } void World::diff_tangible_databases(const IdVector &basis, lua_State *master, StreamBuffer *sb) { lua_State *synch = state(); LuaVar stnmap, mtnmap, stangibles, mtangibles, stab, mtab; LuaExtStack SLS(synch, stnmap, stangibles, stab); LuaExtStack MLS(master, mtnmap, mtangibles, mtab); SLS.rawget(stnmap, LuaRegistry, "tnmap"); MLS.rawget(mtnmap, LuaRegistry, "tnmap"); SLS.rawget(stangibles, LuaRegistry, "tangibles"); MLS.rawget(mtangibles, LuaRegistry, "tangibles"); sb->write_int32(0); int write_count_after = sb->total_writes(); int nmodified = 0; int s_top = lua_gettop(synch); int m_top = lua_gettop(master); for (int64_t id : basis) { MLS.rawget(mtab, mtangibles, id); SLS.rawget(stab, stangibles, id); assert(MLS.istable(mtab)); assert(SLS.istable(stab)); int tw = sb->total_writes(); sb->write_int64(id); nmodified += 1; if (!diff_tables(SLS, stnmap, stab, MLS, mtnmap, mtab, false, sb)) { sb->unwrite_to(tw); nmodified -= 1; } assert(lua_gettop(synch) == s_top); assert(lua_gettop(master) == m_top); } sb->overwrite_int32(write_count_after, nmodified); } LuaDefine(table_diffcompare, "mtnmap,mtab,stnmap,stab", "for unit testing only") { LuaArg mtnmap, mtab, mstnmap, mstab; LuaRet dbgstring; LuaVar tthread; LuaDefStack MLS(L, mtnmap, mtab, mstnmap, mstab, dbgstring, tthread); // Check the arguments. MLS.cktable(mtnmap, "mtnmap"); MLS.cktable(mstnmap, "mstnmap"); MLS.cktable(mtab, "mtab"); MLS.cktable(mstab, "mstab"); // Create a temporary thread to be the 'synch model'. We'll use the // existing thread as the 'master model'. Move two tables to the synch thread. lua_State *synch = lua_newthread(L); lua_replace(L, tthread.index()); lua_pushvalue(L, mstnmap.index()); lua_pushvalue(L, mstab.index()); lua_xmove(L, synch, 2); LuaArg stnmap,stab; LuaDefStack SLS(synch, stnmap, stab); // Call tablecmp_diff. StreamBuffer sb; diff_tables(SLS, stnmap, stab, MLS, mtnmap, mtab, true, &sb); // Convert the output to a debug string. MLS.set(dbgstring, diff_tables_debug_string(&sb)); return MLS.result(); } LuaDefine(table_diffapply, "mtnmap,mtab,mstab", "for unit testing only") { LuaArg mtnmap, mtab, mstab; LuaRet eql, eqlstr, rtab; LuaVar tthread, tangibles, mntmap, key, val; LuaDefStack MLS(L, mtnmap, mtab, mstab, eql, eqlstr, rtab, tthread, tangibles, mntmap, key, val); // Check the arguments. MLS.cktable(mtnmap, "mtnmap"); MLS.cktable(mtab, "mtab"); MLS.cktable(mstab, "mstab"); // Get the tangibles map. MLS.rawget(tangibles, LuaRegistry, "tangibles"); // Invert the tnmap to make the ntmap. MLS.set(mntmap, LuaNewTable); MLS.set(key, LuaNil); while (MLS.next(mtnmap, key, val)) { MLS.rawset(mntmap, val, key); } // Create a temporary thread to be the 'synch model'. We'll use the // existing thread as the 'master model'. lua_State *synch = lua_newthread(L); lua_replace(L, tthread.index()); lua_pushvalue(L, mtnmap.index()); lua_pushvalue(L, mstab.index()); lua_xmove(L, synch, 2); LuaArg stnmap, stab; LuaDefStack SLS(synch, stnmap, stab); // Call diff_tables and patch_tables StreamBuffer sb; diff_tables(SLS, stnmap, stab, MLS, mtnmap, mtab, true, &sb); patch_table(MLS, tangibles, mntmap, mstab, &sb, nullptr); bool eq = table_equal(MLS, mstab, mtab); MLS.set(eql, eq); if (eq) { MLS.set(eqlstr, "tables equal"); } else { MLS.set(eqlstr, "tables were supposed to be equal"); } MLS.set(rtab, stab); return MLS.result(); }