From 88ed3df58c852782b63eee773924c229c2401a37 Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Mon, 4 Jun 2018 18:35:11 -0700 Subject: [PATCH] Overhaul to remove duplicated code and ensure speedier constructor calls for duplicate tables. --- limmutable.c | 336 ++++++++++++++------------------------------------- tests.lua | 58 ++++++--- 2 files changed, 130 insertions(+), 264 deletions(-) diff --git a/limmutable.c b/limmutable.c index 77f38ab..4482c98 100644 --- a/limmutable.c +++ b/limmutable.c @@ -50,202 +50,42 @@ static int WEAK_VALUE_METATABLE; // constructed classes to have the same metatable: // {__new=Lcreate_instance, __tostring=function(cls) return cls.name or 'immutable(...)' end} static int SHARED_CLASS_METATABLE; +static int SUPER_METHODS; typedef struct { int hash; size_t len; } immutable_info_t; -static int Lcreate_instance(lua_State *L) -{ - int num_args = lua_gettop(L)-1; - lua_getfield(L, 1, "__fields"); - int num_fields = lua_objlen(L, -1); - lua_pop(L, 1); - // arg 1: class table, ... - lua_getfield(L, 1, "__new"); - if (! lua_isnil(L, -1)) { - // stack: cls, ..., __new - lua_pushvalue(L, 1); - // stack: cls, ..., __new, cls - lua_rotate(L, 2, 2); - // stack: cls, __new, cls, ... - lua_call(L, num_args+1, LUA_MULTRET); - num_args = lua_gettop(L)-1; - // stack: cls, ... - } else { - lua_pop(L, 1); - } - - // Compute the hash: - unsigned long long ull_hash = 0x9a937c4d; // Seed - for (lua_Integer i=1; i <=(lua_Integer)num_fields || i <=(lua_Integer)num_args; i++) { - unsigned long long item_hash; - int type = i > num_args ? LUA_TNIL : lua_type(L, 1+i); - switch (type) { - case LUA_TNIL: case LUA_TNONE: - // Arbitrarily chosen value - item_hash = 0x97167da9; - break; - case LUA_TNUMBER: - { - // Cast float bits to integer - lua_Number num = lua_tonumber(L, 1+i); - item_hash = *((lua_Integer*)&num); - if (item_hash == 0) { - item_hash = 0x2887c992; - } - break; - } - case LUA_TBOOLEAN: - // Arbitrarily chosen values - item_hash = lua_toboolean(L, 1+i)? 0x82684f71 : 0x88d66f2a; - break; - case LUA_TTABLE: - case LUA_TFUNCTION: - case LUA_TUSERDATA: - case LUA_TTHREAD: - case LUA_TLIGHTUSERDATA: - item_hash = (1000003 * type) ^ (lua_Integer)lua_topointer(L, 1+i); - break; - case LUA_TSTRING: - { - // Algorithm taken from Lua 5.3's implementation - size_t len; - const char *str = lua_tolstring(L, 1+i, &len); - item_hash = len ^ 0xd2e9e9ac; // Arbitrary seed - size_t step = (len >> LUAI_HASHLIMIT) + 1; - for (; len >= step; len -= step) - item_hash ^= ((item_hash<<5) + (item_hash>>2) + (unsigned char)(str[len - 1])); - break; - } - default: - item_hash = 0; - } - ull_hash = (1000003 * ull_hash) ^ item_hash; - } - int hash = (int)ull_hash; - - lua_getfield(L, 1, "__instances"); - - // Stack: [__instances] - // Find bucket - lua_rawgeti(L, -1, hash); - // Stack: [__instances, bucket] - if (lua_isnil(L, -1)) { - // Make a new bucket - // Stack: [__instances, nil] - lua_pop(L, 1); - // Stack: [__instances] - lua_createtable(L, 0, 1); - // Stack: [__instances, bucket] - lua_pushlightuserdata(L, (void*)&WEAK_KEY_METATABLE); - lua_gettable(L, LUA_REGISTRYINDEX); - // Stack: [__instances, bucket, {__mode='k'}] - lua_setmetatable(L, -2); - // Stack: [__instances, bucket] - lua_pushvalue(L, -1); - // Stack: [__instances, bucket, bucket] - lua_rawseti(L, -3, hash); - // Stack: [__instances, bucket] - } else { - // Stack: [__instances, bucket] - // scan bucket - lua_pushnil(L); - while (lua_next(L, -2) != 0) { // for hash_collider_inst, hash_collider in pairs(bucket) do - // Stack: [__instances, bucket, hash_collider_inst, hash_collider] - // Shallow equality check: - lua_pushnil(L); - while (lua_next(L, -2) != 0) { // for i, collider_value in pairs(hash_collider) do - // Stack: [__instances, bucket, hash_collider_inst, hash_collider, i, value] - if (!lua_rawequal(L, -1, 1+lua_tonumber(L, -2))) { // If the i'th entry doesn't match the i'th arg - // Stack: [__instances, bucket, hash_collider_inst, hash_collider, i, value] - lua_pop(L, 3); - // Stack: [__instances, bucket, hash_collider_inst] - goto next_bucket_item; - } else { - // Stack: [__instances, bucket, hash_collider_inst, hash_collider, i, value] - lua_pop(L, 1); - // Stack: [__instances, bucket, hash_collider_inst, hash_collider, i] - } - } - // bucket item matches - // Stack: [__instances, bucket, hash_collider_inst, hash_collider] - lua_pop(L, 1); - return 1; - - next_bucket_item: ; - } - } - lua_insert(L, -2); - // Stack: [bucket, __instances] - lua_pop(L, 1); - // Stack: [bucket] - int bucket_index = lua_gettop(L); - - // Failed to find an existing instance, so create a new one - // Stack: [bucket] - immutable_info_t *userdata = (immutable_info_t*)lua_newuserdata(L, sizeof(immutable_info_t)); - // Stack [bucket, inst_userdata] - int userdata_index = lua_gettop(L); - userdata->hash = hash; - userdata->len = num_fields > num_args ? num_fields : num_args; - - lua_pushvalue(L, 1); - // Stack [bucket, inst_userdata, cls] - lua_setmetatable(L, -2); - // Stack [bucket, inst_userdata] - lua_createtable(L, userdata->len, 0); // Create the table to store the instance's data - // Stack [bucket, inst_userdata, inst_table] - for (lua_Integer i=1; i <= (lua_Integer)num_args; i++) { - lua_pushvalue(L, i+1); - // Stack [bucket, inst_userdata, inst_table, arg #1+i] - lua_rawseti(L, -2, i); - } - - // Set up a ref to the bucket so its lifetime is tied to inst_userdata - lua_getfield(L, 1, "__buckets"); - // Stack [bucket, inst_userdata, inst_table, __buckets] - lua_pushvalue(L, userdata_index); - // Stack [bucket, inst_userdata, inst_table, __buckets, inst_userdata] - lua_pushvalue(L, bucket_index); - // Stack [bucket, inst_userdata, inst_table, __buckets, inst_userdata, bucket] - lua_settable(L, -3); - lua_pop(L, 1); - - // Stack [bucket, inst_userdata, inst_table] - lua_pushvalue(L, userdata_index); - // Stack [bucket, inst_userdata, inst_table, inst_userdata] - lua_rotate(L, bucket_index, 1); - // Stack [inst_userdata, bucket, inst_userdata, inst_table] - lua_settable(L, -3); // buckets[inst_userdata] = inst_table - lua_pop(L, 1); - // Stack [inst_userdata] - lua_gc(L, LUA_GCSTEP, 3); - return 1; +#define GET(i) if (from_table) {\ + if (i <= num_fields) {\ + lua_rawgeti(L, fields_index, i);\ + } else {\ + lua_pushinteger(L, i-num_fields);\ + }\ + lua_gettable(L, 2);\ +} else {\ + if (i <= num_args) {\ + lua_pushvalue(L, 1+i);\ + } else {\ + lua_pushnil(L);\ + }\ } -static int Lfrom_table(lua_State *L) +static inline int _create_instance(lua_State *L, int from_table) { + size_t num_args = lua_gettop(L)-1; lua_getfield(L, 1, "__fields"); int fields_index = lua_gettop(L); - int num_fields = lua_objlen(L, -1); - int num_values = num_fields + lua_objlen(L, 2); + size_t num_fields = lua_objlen(L, -1); + size_t num_values = from_table ? (num_fields + lua_objlen(L, 2)) : (num_args >= num_fields ? num_args : num_fields); - // Compute the hash: + // Compute the hash and populate the values table: + // Stack: [fields] unsigned long long ull_hash = 0x9a937c4d; // Seed - for (lua_Integer i=1; i <=(lua_Integer)num_values; i++) { + for (size_t i=1; i <= num_values; i++) { unsigned long long item_hash; - // Stack: [fields] - if (i <= num_fields) { - lua_rawgeti(L, fields_index, i); - // Stack: [fields, fields[i]] - lua_gettable(L, 2); - // Stack: [fields, table[fields[i]]] - } else { - lua_rawgeti(L, 2, i - num_fields); - // Stack: [fields, table[i-num_fields]] - } + GET(i); // Stack: [fields, value[i]] int type = lua_type(L, -1); switch (type) { @@ -295,7 +135,6 @@ static int Lfrom_table(lua_State *L) int hash = (int)ull_hash; lua_getfield(L, 1, "__instances"); - // Stack: [fields, __instances] // Find bucket lua_rawgeti(L, -1, hash); @@ -320,91 +159,92 @@ static int Lfrom_table(lua_State *L) // Stack: [fields, __instances, bucket] // scan bucket lua_pushnil(L); - while (lua_next(L, -2) != 0) { // for hash_collider_inst, hash_collider in pairs(bucket) do - // Stack: [fields, __instances, bucket, hash_collider_inst, hash_collider] + while (lua_next(L, -2) != 0) { // for hash_collider_inst, collider_table in pairs(bucket) do + // Stack: [fields, __instances, bucket, hash_collider_inst, collider_table] // Shallow equality check: - lua_pushnil(L); - while (lua_next(L, -2) != 0) { // for i, collider_value in pairs(hash_collider) do - // Stack: [fields, __instances, bucket, hash_collider_inst, hash_collider, i, value] - int i = lua_tonumber(L, -2); - if (i <= num_fields) { - lua_rawgeti(L, fields_index, i); - lua_gettable(L, 2); - } else { - lua_rawgeti(L, 2, i-num_fields); - } - // Stack: [fields, __instances, bucket, hash_collider_inst, hash_collider, i, value, table_value] - if (!lua_rawequal(L, -1, -2)) { // If the i'th entry doesn't match the i'th arg - // Stack: [fields, __instances, bucket, hash_collider_inst, hash_collider, i, value, table_value] - lua_pop(L, 4); - // Stack: [fields, __instances, bucket, hash_collider_inst] + immutable_info_t *collider_info = (immutable_info_t*)lua_touserdata(L, -2); + if (collider_info->len != num_values) { + lua_pop(L, 1); + goto next_bucket_item; + } + for (size_t i = 1; i <= num_values; i++) { + lua_rawgeti(L, -1, i); + GET(i); + // Stack: [fields, __instances, bucket, hash_collider_inst, collider_table, collider_val, inst_val] + if (! lua_rawequal(L, -1, -2)) { // If the i'th entry doesn't match the i'th arg + lua_pop(L, 3); goto next_bucket_item; } else { - // Stack: [fields, __instances, bucket, hash_collider_inst, hash_collider, i, value, table_value] lua_pop(L, 2); - // Stack: [fields, __instances, bucket, hash_collider_inst, hash_collider, i] } } // bucket item matches - // Stack: [fields, __instances, bucket, hash_collider_inst, hash_collider] + // Stack: [fields, __instances, bucket, hash_collider_inst, collider_table] lua_pop(L, 1); return 1; next_bucket_item: ; } } - lua_insert(L, -2); - // Stack: [fields, bucket, __instances] - lua_pop(L, 1); - // Stack: [fields, bucket] + // Stack: [fields, __instances, bucket] int bucket_index = lua_gettop(L); // Failed to find an existing instance, so create a new one - // Stack: [fields, bucket] immutable_info_t *userdata = (immutable_info_t*)lua_newuserdata(L, sizeof(immutable_info_t)); - // Stack [fields, bucket, inst_userdata] + // Stack: [fields, __instances, bucket, inst_userdata] int userdata_index = lua_gettop(L); userdata->hash = hash; userdata->len = num_values; lua_pushvalue(L, 1); - // Stack [fields, bucket, inst_userdata, cls] + // Stack: [fields, __instances, bucket, inst_userdata, cls] lua_setmetatable(L, -2); - // Stack [fields, bucket, inst_userdata] - lua_createtable(L, userdata->len, 0); // Create the table to store the instance's data - // Stack [fields, bucket, inst_userdata, inst_table] - for (lua_Integer i=1; i <= (lua_Integer)num_fields; i++) { - lua_rawgeti(L, fields_index, i); - lua_gettable(L, 2); - lua_rawseti(L, -2, i); - } - for (lua_Integer i=num_fields+1; i <= num_values; i++) { - lua_rawgeti(L, 2, i-num_fields); - lua_rawseti(L, -2, i); - } + // Stack: [fields, __instances, bucket, inst_userdata] // Set up a ref to the bucket so its lifetime is tied to inst_userdata lua_getfield(L, 1, "__buckets"); - // Stack [fields, bucket, inst_userdata, inst_table, __buckets] + // Stack: [fields, __instances, bucket, inst_userdata, __buckets] lua_pushvalue(L, userdata_index); - // Stack [fields, bucket, inst_userdata, inst_table, __buckets, inst_userdata] + // Stack: [fields, __instances, bucket, inst_userdata, __buckets, inst_userdata] lua_pushvalue(L, bucket_index); - // Stack [fields, bucket, inst_userdata, inst_table, __buckets, inst_userdata, bucket] + // Stack: [fields, __instances, bucket, inst_userdata, __buckets, inst_userdata, bucket] lua_settable(L, -3); lua_pop(L, 1); + // Stack: [fields, __instances, bucket, inst_userdata] - // Stack [fields, bucket, inst_userdata, inst_table] lua_pushvalue(L, userdata_index); - // Stack [fields, bucket, inst_userdata, inst_table, inst_userdata] - lua_rotate(L, bucket_index, 1); - // Stack [fields, inst_userdata, bucket, inst_userdata, inst_table] - lua_settable(L, -3); // buckets[inst_userdata] = inst_table - lua_pop(L, 1); - // Stack [fields, inst_userdata] + // Stack: [fields, __instances, bucket, inst_userdata, inst_userdata] + lua_createtable(L, num_values, 0); + for (size_t i=1; i <= num_values; i++) { + GET(i); + lua_rawseti(L, -2, i); + } + // Stack: [fields, __instances, bucket, inst_userdata, inst_userdata, inst_table] + lua_settable(L, -4); // buckets[inst_userdata] = inst_table + // Stack: [fields, __instances, bucket, inst_userdata] lua_gc(L, LUA_GCSTEP, 3); return 1; } +static int Lcreate_instance(lua_State *L) +{ + int num_args = lua_gettop(L)-1; + lua_getfield(L, 1, "__new"); + if (! lua_isnil(L, -1)) { + lua_pushvalue(L, 1); + lua_rotate(L, 2, 2); + lua_call(L, num_args+1, LUA_MULTRET); + } else { + lua_pop(L, 1); + } + return _create_instance(L, 0); +} + +static int Lfrom_table(lua_State *L) +{ + return _create_instance(L, 1); +} + static int Lis_instance(lua_State *L) { if (lua_type(L, 2) != LUA_TUSERDATA) { @@ -490,10 +330,7 @@ static int Lindex(lua_State *L) { int ret = Linst_index(L); if (ret > 0) { - if (ret > 1) { - lua_pop(L, ret-1); - } - return 1; + return ret; } // Fall back to class: lua_getmetatable(L, 1); @@ -512,7 +349,6 @@ static int Ltostring(lua_State *L) { luaL_Buffer b; luaL_buffinit(L, &b); - if (! lua_getmetatable(L, 1)) { luaL_error(L, "invalid type"); } @@ -584,6 +420,7 @@ static int Lnexti(lua_State *L) if (! lua_getmetatable(L, 1)) { luaL_error(L, "invalid type"); } + int cls_index = lua_gettop(L); // Stack: [mt] lua_getfield(L, -1, "__instances"); // Stack: [mt, buckets] @@ -592,7 +429,10 @@ static int Lnexti(lua_State *L) luaL_error(L, "invalid type"); } int i = lua_isnil(L, 2) ? 1 : lua_tointeger(L, 2)+1; - if (i > (int)info->len) { + lua_getfield(L, cls_index, "__fields"); + int num_fields = lua_objlen(L, -1); + lua_pop(L, 1); + if (i + num_fields > (int)info->len) { return 0; } lua_rawgeti(L, -1, info->hash); @@ -606,11 +446,7 @@ static int Lnexti(lua_State *L) // Stack: [mt, buckets, bucket, inst_table] lua_pushinteger(L, i); // Stack: [mt, buckets, bucket, inst_table, i] - lua_getfield(L, -5, "__fields"); - // Stack: [mt, buckets, bucket, inst_table, i, __fields] - i += lua_objlen(L, -1); - lua_pop(L, 1); - lua_rawgeti(L, -2, i); + lua_rawgeti(L, -2, i + num_fields); // Stack: [mt, buckets, bucket, inst_table, i, table[i]] return 2; } @@ -707,15 +543,10 @@ static int Lhash(lua_State *L) static const luaL_Reg Rinstance_metamethods[] = { {"__len", Llen}, - {"__rawlen", Llen}, {"__index", Lindex}, - {"__rawindex", Linst_index}, {"__tostring", Ltostring}, - {"__rawtostring", Ltostring}, {"__ipairs", Lipairs}, - {"__rawipairs", Lipairs}, {"__pairs", Lpairs}, - {"__rawpairs", Lpairs}, {"__hash", Lhash}, {"from_table", Lfrom_table}, {"is_instance", Lis_instance}, @@ -748,6 +579,10 @@ static int Lmake_class(lua_State *L) // Stack: [CLS] } + lua_pushlightuserdata(L, (void*)&SUPER_METHODS); + lua_gettable(L, LUA_REGISTRYINDEX); + lua_setfield(L, -2, "__super"); + // Stack: [CLS] lua_createtable(L, 0, 32); // Rough guess: at least 32 instances concurrently // Stack: [CLS, __instances] @@ -830,6 +665,11 @@ static const luaL_Reg Rclass_metamethods[] = LUALIB_API int luaopen_immutable(lua_State *L) { + lua_pushlightuserdata(L, (void*)&SUPER_METHODS); + lua_createtable(L, 0, 8); + luaL_register(L,NULL,Rinstance_metamethods); + lua_settable(L, LUA_REGISTRYINDEX); + lua_pushlightuserdata(L, (void*)&WEAK_VALUE_METATABLE); lua_createtable(L, 0, 1); lua_pushstring(L, "v"); diff --git a/tests.lua b/tests.lua index 551ad1d..a77853f 100644 --- a/tests.lua +++ b/tests.lua @@ -31,6 +31,7 @@ local function test(description, fn) io.write("\r"..description.."\n"..reset) print(reset..red..(err or "")..reset) num_errors = num_errors + 1 + --os.exit(true) else io.write(reset..dim.."\r.......................................") io.write(reset.."["..green.."PASSED"..reset.."]\r") @@ -45,6 +46,7 @@ end) test("Creating class", function() Vec = immutable({"x","y"}, { + name="Vec", len2=function(self) return self.x*self.x + self.y*self.y end, @@ -52,9 +54,6 @@ test("Creating class", function() local cls = getmetatable(self) return cls(self.x+other.x, self.y+other.y) end, - __tostring=function(self) - return "Vec("..tostring(self.x)..", "..tostring(self.y)..")" - end, }) end) @@ -72,6 +71,10 @@ test("Testing # operator", function() assert(#T(1,2,3) == 3) end) +test("Testing # operator for mixed table", function() + assert(#Vec(1,2,3,4,5) == 3) +end) + test("Testing method", function() assert(v:len2() == 10) end) @@ -81,10 +84,10 @@ test("Testing tostring", function() end) test("Testing from_table", function() - assert(Vec:from_table({x=1,y=2}) == Vec(1,2)) - assert(Vec:from_table({3, x=1,y=2}) == Vec(1,2,3)) - assert(Vec:from_table(setmetatable({y=3}, {__index={x=1}})) == v) - assert(Vec:from_table({x=1}) == Vec(1, nil)) + assert(Vec:from_table({x=1,y=2}) == Vec(1,2), "assertion 1 failed!") + assert(Vec:from_table({3, x=1,y=2}) == Vec(1,2,3), "assertion 2 failed!") + assert(Vec:from_table(setmetatable({y=3}, {__index={x=1}})) == v, "assertion 3 failed!") + assert(Vec:from_table({x=1}) == Vec(1, nil), "assertion 4 failed!") end) test("Testing from_table for tuples", function() @@ -175,9 +178,9 @@ test("Testing __rawindex", function() classvar = 99, __index=function(self,k) local cls = getmetatable(self) - local inst_val, found = cls.__rawindex(self, k) + local inst_val, found = cls.__super.__index(self, k) if found then return inst_val end - return cls[k] or tostring(k)..tostring(self.x) + return cls[k] or tostring(k)..tostring(cls.__super.__index(self, 'x')) end, }) local f = Foo(23, nil) @@ -260,31 +263,54 @@ if _VERSION == "Lua 5.2" or _VERSION == "Lua 5.3" then local T = immutable() local t = T(1,4,9,nil,16,nil) local checks = {1,4,9,nil,16,nil} - local passed = 0 + local passed, iterations = 0, 0 for i,v in ipairs(t) do assert(checks[i] == v) passed = passed + 1 + iterations = iterations + 1 end - assert(passed == 6) - passed = 0 + assert(passed == 6 and iterations == passed) + passed, iterations = 0, 0 for k,v in pairs(t) do assert(checks[k] == v) passed = passed + 1 + iterations = iterations + 1 end - assert(passed == 6) + assert(passed == 6 and iterations == passed) end) test("Testing table iteration", function() local Foo = immutable({"x", "y", "z","w"}) local f = Foo(1,nil,2,nil) local checks = {1,nil,2,nil} - local passed = 0 + local passed, iterations = 0, 0 checks = {x=1,y=nil,z=2,w=nil} for k,v in pairs(f) do assert(checks[k] == v) passed = passed + 1 + iterations = iterations + 1 end - assert(passed == 4) + assert(passed == 4 and iterations == passed) + end) + + test("Testing mixed iteration", function() + local T = immutable({"skip"}) + local t = T('a','b','c') + local checks = {'b','c'} + local passed, iterations = 0, 0 + for i,v in ipairs(t) do + assert(checks[i] == v, "checks["..i.."] ~= "..v) + passed = passed + 1 + iterations = iterations + 1 + end + assert(passed == 2 and iterations == passed, "ipairs failed") + checks = {skip='a','b','c'} + passed = 0 + for k,v in pairs(t) do + assert(checks[k] == v, "checks["..k.."] ~= "..v) + passed = passed + 1 + end + assert(passed == 3, "pairs failed") end) end @@ -331,7 +357,7 @@ test("Testing __rawindex", function() classvar = 99, __index = function(self, key) local cls = getmetatable(self) - local value, found = cls.__rawindex(self, key) + local value, found = cls.__super.__index(self, key) if not found then return cls[key] else