Jump to content

Module:Lua class: Difference between revisions

From Wikipedia, the free encyclopedia
Content deleted Content added
m Fixed bug where "type" calls below _G.type used the local (original) function
Turned overridden libraryUtil local, but still accessible to other modules. Added warnings. Made error handling go brrr. Implemented hashing. Completed error levels. Optimized closures. Returned cls_private from "class". And a little more.
Line 1: Line 1:
libraryUtil = require('libraryUtil') -- overridden for new types and exceptions
local libraryUtil = require('libraryUtil') -- overridden for new types and exceptions
local warn = require('Module:Warning')


local classes, instances = {}, {} -- registry of all complete/internal class and instance objects (with some exceptions)
local classes, instances = {}, {} -- registry of all complete/internal class and instance objects and their pointers
local inst_private_mts, inst_public_mts = {}, {} -- for each class since they are immutable
local inst_private_mts, inst_public_mts = {}, {} -- for each class since they are mostly immutable


local una_metamethods = {__ipairs=1, __pairs=1, __tostring=1, __unm=1}
local una_metamethods = {__ipairs=1, __pairs=1, __tostring=1, __unm=1}
local bin_metamethods = {__add=1, __concat=1, __div=1, __eq=1, __le=1, __lt=1, __mod=1, __mul=1, __pow=1, __sub=1}
local bin_metamethods = {__add=1, __concat=1, __div=1, __eq=1, __le=1, __lt=1, __mod=1, __mul=1, __pow=1, __sub=1}
local oth_metamethods = {__call=1, __index=1, __newindex=1, __init=1}
local oth_metamethods = {__call=1, __index=1, __newindex=1, __init=1}
local not_metamethods = {__name=1, __bases=1, __methods=1, __slots=1, __protected=1} -- and __class
local not_metamethods = {__name=1, __bases=1, __methods=1, __classmethods=1, __staticmethods=1, __normalmethods=1, __slots=1, __protected=1}
-- __class and __hash




local function private_read(self_private, key)
local function objtostr(obj)
local copy = {}
if not not_metamethods[key] then
for key, val in pairs(obj) do
return instances[self_private][key] -- instance should be clean of misbahaved keys so that __index(cls_private, key) handles it
copy[key] = type(val) == 'function' and 'function' or val
end
end
return mw.text.jsonEncode(copy, mw.text.JSON_PRETTY)
error(('AttributeError: unauthorized read attempt of internal "%s"'):format(key), 2)
end
end


local inst_mt = {
local function private_read_custom(self_private, key)
__index = function (self, key)
if not not_metamethods[key] then
if tonumber(key) or key == 'hash' or key == '__hash' then -- don't search numeric keys in classes and hash isn't inheritable
local self = instances[self_private]
return nil
local value = self.__class.__index(self_private, key) -- custom __index can handle misbehaved keys
if value == nil then
value = self[key] -- same reason of private_read for not checking key type
end
end
return self.__class[key] -- key could be invalid here without issues as __index(cls_private, key) would handle it
return value
end,
__tostring = objtostr--
}

local function private_read(self_private, key)
return instances[self_private][key] -- instance should be clean of invalid keys so that __index(cls_private, key) handles them
end

local function private_read_custom(self_private, key)
if not_metamethods[key] then
error(("AttributeError: unauthorized read attempt of internal '%s'"):format(key), 2)
end
local self = instances[self_private]
local value = self.__class.__index(self_private, key) -- custom __index can handle invalid keys
if value == nil then
return self[key] -- same reason of private_read for not checking key validity
end
end
return value
error(('AttributeError: unauthorized read attempt of internal "%s"'):format(key), 2)
end
end


Line 32: Line 49:
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'})
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'})
local self = instances[self_private]
local self = instances[self_private]
if type(key) == 'string' then
if tonumber(key) or not classes[self.__class].__methods[key] and key:sub(1,2) ~= '__' then
local cls = classes[self.__class]
self[key] = value
if cls.__normalmethods[key] or key:sub(1,2) == '__' and key ~= '__hash' then
else
error(('AttributeError: forbidden write attempt {%s: %s} to immutable instance method'):format(key, tostring(value)), 2)
error(("AttributeError: forbidden write attempt {%s: %s} to immutable method or invalid key"):format(key, tostring(value)), 2)
elseif key:find('[^_%w]') or key:find('^%d') then
error(("AttributeError: invalid attribute name '%s'"):format(key), 2)
elseif key == '__hash' and self.__hash ~= nil then
error("AttributeError: forbidden update attempt to immutable __hash", 2)
end
end
end
self[key] = value
end
end


local function private_write_custom(self_private, key, value)
local function private_write_custom(self_private, key, value)
local self = instances[self_private]
local self = instances[self_private]
if type(key) ~= 'string' or not classes[self.__class].__methods[key] and key:sub(1,2) ~= '__' then
local cls = classes[self.__class]
local keyType = type(key)
if not self.__class.__newindex(self_private, key, value) then -- custom __newindex can handle misbehaved keys
if keyType == 'string' and (cls.__normalmethods[key] or key:sub(1,2) == '__' and key ~= '__hash') then
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'})
error(("AttributeError: forbidden write attempt {%s: %s} to immutable method or invalid key"):format(key, tostring(value)), 2)
self[key] = value
end
if cls.__newindex(self_private, key, value) == false then -- custom __newindex can handle invalid keys
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'})
if keyType == 'string' then
if key:find('[^_%w]') or key:find('^%d') then
error(("AttributeError: invalid attribute name '%s'"):format(key), 2)
elseif key == '__hash' and self.__hash ~= nil then
error("AttributeError: forbidden update attempt to immutable __hash", 2)
end
end
end
self[key] = value
else
error(('AttributeError: forbidden write attempt {%s: %s} to immutable instance method'):format(key, tostring(value)), 2)
end
end
end
end

local function objtostr(obj)
local copy = {}
for key, val in pairs(obj) do
copy[key] = type(val) == 'function' and 'function' or val
end
return mw.text.jsonEncode(copy, mw.text.JSON_PRETTY)
end

local inst_mt = {
__index = function (self, key)
if tonumber(key) then
return nil -- don't search numeric keys in classes
end
return self.__class[key] -- key could be misbehaved here without issues as __index(cls_private, key) would handle it
end,
__tostring = objtostr--
}


local function public_read(self_public, key)
local function public_read(self_public, key)
if type(key) ~= 'string' or key:sub(1,1) ~= '_' then
if type(key) == 'string' and key:sub(1,1) == '_' then
error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2)
return instances[instances[self_public]][key] -- same reason of private_read...
end
end
return instances[instances[self_public]][key] -- same reason of private_read...
error(('AttributeError: unauthorized read attempt of nonpublic "%s"'):format(key), 2)
end
end


local function public_read_custom(self_public, key)
local function public_read_custom(self_public, key)
if type(key) ~= 'string' or key:sub(1,1) ~= '_' then
if type(key) == 'string' and key:sub(1,1) == '_' then
error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2)
local self = instances[instances[self_public]]
local value = self.__class.__index(instances[self_public], key)
if value == nil then
value = self[key] -- same reason of private_read...
end
return value
end
end
local self = instances[instances[self_public]]
error(('AttributeError: unauthorized read attempt of nonpublic "%s"'):format(key), 2)
local value = self.__class.__index(instances[self_public], key)
if value == nil then
return self[key] -- same reason of private_read...
end
return value
end
end


local function public_write(self_public, key, value)
local function public_write(self_public, key, value)
if type(key) == 'string' and key:sub(1,1) == '_' then
error(('AttributeError: unauthorized write attempt of nonpublic {%s: %s}'):format(key, tostring(value)), 2)
end
local self = instances[instances[self_public]]
local self = instances[instances[self_public]]
local cls = classes[self.__class]
local cls = classes[self.__class]
if cls.__methods[key] then
if type(key) == 'string' then
if key:sub(1,1) == '_' then
error(('AttributeError: forbidden write attempt {%s: %s} to immutable instance method'):format(key, tostring(value)), 2)
error(("AttributeError: unauthorized write attempt of nonpublic {%s: %s}"):format(key, tostring(value)), 2)
elseif cls.__normalmethods[key] then
error(("AttributeError: forbidden write attempt {%s: %s} to immutable method"):format(key, tostring(value)), 2)
end
end
end
if self[key] == nil and not cls.__slots[key] then -- if instance and __slots are valid, no danger of creating misbehaved attributes
if self[key] == nil and not cls.__slots[key] then -- if instance and __slots are valid, no danger of creating invalid attributes
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) -- otherwise error message would not make sense
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) -- otherwise error message would not make sense
error(('AttributeError: public attribute creation attempt {%s: %s} not expected by __slots'):format(key, tostring(value)), 2)
error(("AttributeError: public attribute creation attempt {%s: %s} not expected by __slots"):format(key, tostring(value)), 2)
end
end
self[key] = value
self[key] = value
Line 105: Line 119:


local function public_write_custom(self_public, key, value)
local function public_write_custom(self_public, key, value)
if type(key) == 'string' and key:sub(1,1) == '_' then
error(('AttributeError: unauthorized write attempt of nonpublic {%s: %s}'):format(key, tostring(value)), 2)
end
local self = instances[instances[self_public]]
local self = instances[instances[self_public]]
local cls = classes[self.__class]
local cls = classes[self.__class]
if cls.__methods[key] then
if type(key) == 'string' then
if key:sub(1,1) == '_' then
error(('AttributeError: forbidden write attempt {%s: %s} to immutable instance method'):format(key, tostring(value)), 2)
error(("AttributeError: unauthorized write attempt of nonpublic {%s: %s}"):format(key, tostring(value)), 2)
elseif cls.__normalmethods[key] then
error(("AttributeError: forbidden write attempt {%s: %s} to immutable method"):format(key, tostring(value)), 2)
end
end
end
if not cls.__newindex(instances[self_public], key, value) then
if cls.__newindex(instances[self_public], key, value) == false then
if self[key] == nil and not cls.__slots[key] then
if self[key] == nil and not cls.__slots[key] then
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) -- otherwise error message...
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) -- otherwise error message...
error(('AttributeError: public attribute creation attempt {%s: %s} not expected by __slots'):format(key, tostring(value)), 2)
error(("AttributeError: public attribute creation attempt {%s: %s} not expected by __slots"):format(key, tostring(value)), 2)
end
end
self[key] = value
self[key] = value
Line 124: Line 139:
local function constructor(wrapper, ...)
local function constructor(wrapper, ...)
if select('#', ...) ~= 1 or type(...) ~= 'table' then
if select('#', ...) ~= 1 or type(...) ~= 'table' then
error('SyntaxError: incorrect instance constructor syntax, should be: Class{arg1, arg2..., kw1=kwarg1, kw2=kwarg2...}', 2)
error("SyntaxError: incorrect instance constructor syntax, should be: Class{arg1, arg2..., kw1=kwarg1, kw2=kwarg2...}", 2)
end
end
local self = {} -- __new
local self = {} -- __new
local cls_private = classes[classes[wrapper]] and classes[wrapper] or wrapper
local cls_private = classes[classes[wrapper]] and classes[wrapper] or wrapper
self.__class = cls_private
self.__class = cls_private
setmetatable(self, inst_mt)


local self_private = {} -- wrapper
local self_private = {} -- wrapper
Line 142: Line 158:
end
end
mt.__call = cls.__call
mt.__call = cls.__call
mt.__metatable = 'unauthorized access attempt of wrapper object metatable'
mt.__metatable = "unauthorized access attempt of wrapper object metatable"


inst_private_mts[cls] = mt
inst_private_mts[cls] = mt
Line 152: Line 168:
local __init = cls.__init
local __init = cls.__init
if __init and __init(self_private, ...) then
if __init and __init(self_private, ...) then
error('TypeError: __init must not return a var-list')
error("TypeError: __init must not return a var-list")
end
end


for key in pairs(cls.__methods) do
for key in pairs(cls.__methods) do
local func = cls[key] -- index once to save time in future calls
self[key] = function (...) return cls[key](self_private, ...) end
self[key] = function (...) return func(self_private, ...) end
end
end


if cls._hash then -- not inheritable
setmetatable(self, inst_mt)
self.hash = function () return cls._hash(self_private) end
self.hash() -- construction of self is finalized at this point, so immutable hash can be safely set
end


local self_public = {}
local self_public = {}
Line 170: Line 190:
for key in pairs(una_metamethods) do
for key in pairs(una_metamethods) do
if cls[key] then
if cls[key] then
mt[key] = function (a) return cls[key](instances[a]) end
local func = cls[key]
mt[key] = function (a) return func(instances[a]) end
end
end
end
end
for key in pairs(bin_metamethods) do
for key in pairs(bin_metamethods) do
if cls[key] then
if cls[key] then
mt[key] = function (a, b) return cls[key](instances[a], instances[b]) end
local func = cls[key]
mt[key] = function (a, b) return func(instances[a], instances[b]) end
end
end
end
end
if cls.__call then
mt.__call = function (self_public, ...) return cls.__call(instances[self_public], ...) end
local func = cls.__call
mt.__metatable = 'unauthorized access attempt of wrapper object metatable'
mt.__call = function (self_public, ...) return func(instances[self_public], ...) end
end
mt.__metatable = "unauthorized access attempt of wrapper object metatable"


inst_public_mts[cls] = mt
inst_public_mts[cls] = mt
Line 192: Line 217:
local function multi_inheritance(cls, key)
local function multi_inheritance(cls, key)
for _, base in ipairs(cls.__bases) do
for _, base in ipairs(cls.__bases) do
if key:sub(1,1) ~= '_' or base.__protected[key] or key:sub(1,2) == '__' and key ~= '__name' then
if key:sub(1,1) ~= '_' or base.__protected[key] or key:sub(1,2) == '__' and key ~= '__name' and key ~= '__hash' then
local value = base[key]
local value = base[key]
if value ~= nil then
if value ~= nil then
Line 209: Line 234:
__call = constructor,
__call = constructor,
__index = function (cls_private, key)
__index = function (cls_private, key)
if not not_metamethods[key] then
if type(key) ~= 'string' then
warn(("AttributeWarning: index '%s' type should be string, %s given"):format(tostring(key), type(key)), 2)
libraryUtil.checkTypeMultiForIndex(key, {'string'})
elseif not_metamethods[key] then
local cls = classes[cls_private]
error(("AttributeError: unauthorized read attempt of internal '%s'"):format(key), 2)
local value = cls[key]
if type(value) == 'table' and not cls.__slots[key] then
elseif key:find('[^_%w]') or key:find('^%d') then
warn(("AttributeWarning: index '%s' should be a valid Lua name"):format(key), 2)
end
local cls = classes[cls_private]
local value = cls[key]
if not cls.__slots[key] then
local valueType = type(value)
if valueType == 'table' then
return mw.clone(value) -- because class attributes are immutable by default
return mw.clone(value) -- because class attributes are immutable by default
elseif valueType == 'set' then --should list be clone or deep copy?
return value.copy()
end
end
return value
end
end
return value
error(('AttributeError: unauthorized read attempt of internal "%s"'):format(key), 2)
end,
end,
__newindex = function (cls_private, key, value)
__newindex = function (cls_private, key, value)
local cls = classes[cls_private]
local cls = classes[cls_private]
if cls.__slots[key] then -- __slots should be valid, so no need to check key type before
if not cls.__slots[key] and key ~= '__hash' then -- __slots should be valid, so no need to check key validity before
cls[key] = value
else
libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message would not make sense
libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message would not make sense
error(('AttributeError: write attempt {%s: %s} not expected by __slots'):format(key, tostring(value)), 2)
error(("AttributeError: write attempt {%s: %s} not expected by __slots"):format(key, tostring(value)), 2)
elseif key == '__hash' and cls.__hash ~= nil then
error("AttributeError: forbidden update attempt to immutable __hash", 2)
end
end
cls[key] = value
end,
end,
__metatable = 'unauthorized access attempt of wrapper object metatable'
__metatable = "unauthorized access attempt of wrapper object metatable"
}
}


Line 235: Line 269:
__call = constructor,
__call = constructor,
__index = function (cls_public, key)
__index = function (cls_public, key)
libraryUtil.checkTypeMultiForIndex(key, {'string'})
if type(key) ~= 'string' then
warn(("AttributeWarning: index '%s' type should be string, %s given"):format(tostring(key), type(key)), 2)
if key:sub(1,1) ~= '_' then
elseif key:sub(1,1) == '_' then
local value = classes[classes[cls_public]][key]
error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2)
if type(value) == 'table' then
elseif key:find('[^_%w]') or key:find('^%d') then
return mw.clone(value) -- all class attributes are immutable in the public scope
warn(("AttributeWarning: index '%s' should be a valid Lua name"):format(key), 2)
end
return value
end
end
local value = classes[classes[cls_public]][key]
error(('AttributeError: unauthorized read attempt of nonpublic "%s"'):format(key), 2)
local valueType = type(value)
if valueType == 'table' then
return mw.clone(value) -- all class attributes are immutable in the public scope
elseif valueType == 'set' then
return value.copy()
end
return value
end,
end,
__newindex = function (cls_public, key, value)
__newindex = function (cls_public, key, value)
libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message...
libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message...
error(('AttributeError: forbidden write attempt of {%s: %s}; a class is immutable in public scope'):format(key, tostring(value)), 2)
error(("AttributeError: forbidden write attempt of {%s: %s}; a class is immutable in public scope"):format(key, tostring(value)), 2)
end,
end,
__metatable = 'unauthorized access attempt of wrapper object metatable'
__metatable = "unauthorized access attempt of wrapper object metatable"
}
}

math.randomseed(os.time())

local function hash(obj_private)
if not obj_private.__hash then -- not inheritable
obj_private.__hash = math.floor(math.random() * 2^32) -- due to math.random bug in Lua 5.1
end
return obj_private.__hash
end


function class(...)
function class(...)
Line 258: Line 307:
local idx
local idx
if type(args[1]) == 'string' then
if type(args[1]) == 'string' then
cls.__name = args[1]
local __name = args[1]
if __name:find('%W') or __name:find('^%d') then
error(("ValueError: class '%s' must be a valid Lua name without '_'s"):format(__name), 2)
end
cls.__name = __name
idx = 2
idx = 2
else
else
Line 269: Line 322:
cls.__bases[#cls.__bases+1] = classes[classes[args[i]]]
cls.__bases[#cls.__bases+1] = classes[classes[args[i]]]
end
end
setmetatable(cls, cls_mt)


local kwargs = args[#args]
local kwargs = args[#args]
libraryUtil.checkType('class', #args, kwargs, 'table')
libraryUtil.checkType('class', #args, kwargs, 'table')
if kwargs.__name or kwargs.__bases then
if kwargs.__name ~= nil or kwargs.__bases ~= nil then
error('ValueError: __name and unpacked __bases must be passed as optional first args to "class"')
error("ValueError: __name and unpacked __bases must be passed as optional first args to 'class'", 2)
end
end


cls.__slots = {}
cls.__slots = {}
if kwargs.__slots then
for _, slot in ipairs(kwargs.__slots) do
if slot:sub(1,2) ~= '__' then
cls.__slots[slot] = true
else
error(('ValueError: slot "%s" has forbidden namespace'):format(slot))
end
end
kwargs.__slots = nil
end
local mt = {
local mt = {
__index = function (__slots, key) -- multi_inheritance
__index = function (__slots, key) -- multi_inheritance
for _, base in ipairs(cls.__bases) do
for _, base in ipairs(cls.__bases) do
if key:sub(1,1) ~= '_' or base.__protected[key] then
if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__slots[key] then
return true
if base.__slots[key] then
return true
end
end
end
end
end
Line 299: Line 341:
}
}
setmetatable(cls.__slots, mt)
setmetatable(cls.__slots, mt)
if kwargs.__slots ~= nil then

libraryUtil.checkTypeForNamedArg('class', '__slots', kwargs.__slots, 'table')
cls.__protected = {}
for i, slot in ipairs(kwargs.__slots) do
if kwargs.__protected then
libraryUtil.checkType('__slots', i, slot, 'string')
for _, key in ipairs(kwargs.__protected) do
if key:sub(1,1) == '_' and key:sub(2,2) ~= '_' then
if slot:find('[^_%w]') or slot:find('^%d') then
error(("ValueError: invalid slot name '%s'"):format(slot), 2)
cls.__protected[key] = true
elseif slot:sub(1,2) == '__' then
else
error(('ValueError: the namespace of "%s" is not manually protectable'):format(key))
error(("ValueError: slot '%s' has forbidden namespace"):format(slot), 2)
elseif rawget(cls.__slots, slot) then
warn(("ValueWarning: duplicated slot '%s'"):format(slot), 2)
elseif kwargs[slot] ~= nil or cls.__slots[slot] then
error(("ValueError: slot '%s' is predefined in class or allocated in __slots of bases"):format(slot), 2)
end
end
cls.__slots[slot] = true
end
end
kwargs.__protected = nil
kwargs.__slots = nil
end
end

cls.__protected = {}
mt = {
mt = {
__index = function (__protected, key)
__index = function (__protected, key)
Line 321: Line 370:
}
}
setmetatable(cls.__protected, mt)
setmetatable(cls.__protected, mt)
if kwargs.__protected ~= nil then
libraryUtil.checkTypeForNamedArg('class', '__protected', kwargs.__protected, 'table')
for i, key in ipairs(kwargs.__protected) do
libraryUtil.checkType('__protected', i, key, 'string')
if key:sub(1,1) ~= '_' or key:sub(2,2) == '_' then
error(("ValueError: the namespace of '%s' is not manually protectable"):format(key), 2)
elseif key == '_hash' then
error("ValueError: forbidden attempt to protect _hash which is not inheritable", 2)
elseif rawget(cls.__protected, key) then
warn(("ValueWarning: duplicated '%s' in __protected"):format(key), 2)
elseif cls.__protected[key] then
error(("ValueError: '%s' is already allocated in __protected of bases"):format(key), 2)
elseif kwargs[key] == nil then -- key validity will be checked further ahead
error(("ValueError: attempt to protect undefined '%s'"):format(key), 2)
end
cls.__protected[key] = true
end
kwargs.__protected = nil
end


if kwargs.__methods then
if kwargs.__methods ~= nil then
error('ValueError: __classmethods and __staticmethods should be passed as optional attributes instead of __methods')
error("ValueError: __classmethods and __staticmethods should be passed as optional attributes instead of __methods", 2)
elseif kwargs.hash ~= nil or kwargs.__hash ~= nil then
error("ValueError: forbidden attempt to define hash or __hash which are set internally", 2)
end
end

cls.__normalmethods = {} -- used in instance write methods
mt = {
__index = function (__normalmethods, key)
return cls.__methods[key] or cls.__classmethods[key] or cls.__staticmethods[key]
end
}
setmetatable(cls.__normalmethods, mt)


local cls_private = {} -- wrapper
local cls_private = {} -- wrapper
Line 330: Line 408:
classes[cls_private] = cls
classes[cls_private] = cls


if kwargs.__classmethods then
cls.__classmethods = {}
mt = {
for _, key in ipairs(kwargs.__classmethods) do
__index = function (__classmethods, key)
for _, base in ipairs(cls.__bases) do
if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__classmethods[key] then
return true
end
end
end
}
setmetatable(cls.__classmethods, mt)
if kwargs.__classmethods ~= nil then
libraryUtil.checkTypeForNamedArg('class', '__classmethods', kwargs.__classmethods, 'table')
for i, key in ipairs(kwargs.__classmethods) do
libraryUtil.checkType('__classmethods', i, key, 'string')
if key:find('[^_%w]') or key:find('^%d') then
error(("ValueError: invalid classmethod name '%s'"):format(key), 2)
elseif key:sub(1,2) == '__' then
error(("ValueError: classmethod '%s' has forbidden namespace"):format(key), 2)
elseif key == '_hash' then
error("ValueError: invalid classmethod _hash, classes have their own hash classmethod", 2)
elseif rawget(cls.__classmethods, key) then
error(("ValueError: duplicated '%s' in __classmethods"):format(key), 2)
elseif not cls.__classmethods[key] and cls[key] ~= nil then
error(("ValueError: forbidden attempt to convert '%s' non-classmethod to classmethod"):format(key), 2)
end
libraryUtil.checkTypeForNamedArg('class', key, kwargs[key], 'function')

cls.__classmethods[key] = true
local func = kwargs[key]
local func = kwargs[key]
cls[key] = function (...) return func(cls_private, ...) end
cls[key] = function (...) return func(cls_private, ...) end
Line 339: Line 444:
end
end


cls.__normalmethods.hash = true
local staticmethods = {}
cls.hash = function () return hash(cls_private) end -- classes are always hashable so this is independent from _hash
if kwargs.__staticmethods then
cls.hash()
for _, key in ipairs(kwargs.__staticmethods) do

staticmethods[key] = true
if kwargs.__eq == nil and kwargs._hash == nil then
kwargs._hash = hash
end
if kwargs._hash ~= nil then
libraryUtil.checkTypeForNamedArg('class', '_hash', kwargs._hash, 'function')
cls.__normalmethods._hash = true
cls._hash = kwargs._hash
kwargs._hash = nil
end

cls.__staticmethods = {}
mt = {
__index = function (__staticmethods, key)
for _, base in ipairs(cls.__bases) do
if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__staticmethods[key] then
return true
end
end
end
}
setmetatable(cls.__staticmethods, mt)
if kwargs.__staticmethods ~= nil then
libraryUtil.checkTypeForNamedArg('class', '__staticmethods', kwargs.__staticmethods, 'table')
for i, key in ipairs(kwargs.__staticmethods) do
libraryUtil.checkType('__staticmethods', i, key, 'string')
if key:sub(1,2) == '__' then
error(("ValueError: staticmethod '%s' has forbidden namespace"):format(key), 2)
elseif rawget(cls.__staticmethods, key) then
warn(("ValueWarning: duplicated staticmethod '%s'"):format(key), 2)
elseif not cls.__staticmethods[key] and cls[key] ~= nil then
error(("ValueError: forbidden attempt to convert '%s' non-staticmethod to staticmethod"):format(key), 2)
end
libraryUtil.checkTypeForNamedArg('class', key, kwargs[key], 'function')
cls.__staticmethods[key] = true
end
end
kwargs.__staticmethods = nil
kwargs.__staticmethods = nil
Line 358: Line 497:
local valid = false
local valid = false
for key, val in pairs(kwargs) do
for key, val in pairs(kwargs) do
if type(key) ~= 'string' then
if key:sub(1,2) == '__' and not una_metamethods[key] and not bin_metamethods[key] and not oth_metamethods[key] then
error(('ValueError: unrecognized metamethod or unauthorized internal attribute {%s: %s}'):format(key, tostring(val)))
error(("TypeError: invalid attribute name '%s' (string expected, got %s)"):format(tostring(key), type(key)), 2)
elseif key:find('[^_%w]') or key:find('^%d') then
error(("ValueError: invalid attribute name '%s'"):format(key), 2)
elseif key:sub(1,2) == '__' and not una_metamethods[key] and not bin_metamethods[key] and not oth_metamethods[key] then
error(("ValueError: unrecognized metamethod or unauthorized internal attribute {%s: %s}"):format(key, tostring(val)), 2)
end
end
cls[key] = val
cls[key] = val
if type(val) == 'function' then
if type(val) == 'function' then
if not staticmethods[key] and key:sub(1,2) ~= '__' then
if not cls.__staticmethods[key] and key:sub(1,2) ~= '__' then -- classmethods and _hash were already removed from kwargs
cls.__methods[key] = true
cls.__methods[key] = true
end
end
Line 371: Line 514:
end
end
end
end
if not valid then
assert(valid, 'AssertionError: a (sub)class must have at least one functional method')
error("ValueError: a (sub)class must have at least one functional method", 2)

end
setmetatable(cls, cls_mt)


local cls_public = {}
local cls_public = {}
setmetatable(cls_public, cls_public_mt)
setmetatable(cls_public, cls_public_mt)
classes[cls_public] = cls_private
classes[cls_public] = cls_private
return cls_public, cls_private -- cls_private should not be accesible outside the module which defined it!
return cls_public
end --make it a proxy to try to enforce behavior?
end




Line 517: Line 660:
local try_clause = args[1]
local try_clause = args[1]


assert(args[2] == 'except', 'AssertionError: missing required except clause')
if args[2] ~= 'except' then
error("SyntaxError: missing required except clause", 3)
end
local except_clauses = {}
local except_clauses = {}
local i = 3
local i = 3
local argType, exceptionTypes = nil, {string=1, table=1}
repeat
repeat
libraryUtil.checkTypeMulti('try', i, args[i], {'string', 'table', 'function'}, 4)
libraryUtil.checkTypeMulti('try', i, args[i], {'string', 'table', 'function'}, 4)
if ({string=1, table=1})[type(args[i])] then
argType = type(args[i])
if exceptionTypes[argType] then
libraryUtil.checkType('try', i+1, args[i+1], 'function', nil, 4)
libraryUtil.checkType('try', i+1, args[i+1], 'function', nil, 4)
except_clauses[#except_clauses+1] = {exceptions={}, handler=args[i+1]}
except_clauses[#except_clauses+1] = {exceptions={}, handler=args[i+1]}
if type(args[i]) == 'string' then
if argType == 'string' then
except_clauses[#except_clauses].exceptions[args[i]] = true
except_clauses[#except_clauses].exceptions[args[i]] = true
else
else
for _, exception in ipairs(args[i]) do
for _, exception in ipairs(args[i]) do
if type(exception) ~= 'string' then
if type(exception) ~= 'string' then
error(('TypeError: invalid exception type in except (string expected, got %s)'):format(type(exception)))
error(("TypeError: invalid exception type in except (string expected, got %s)"):format(type(exception)), 3)
end
end
except_clauses[#except_clauses].exceptions[exception] = true
except_clauses[#except_clauses].exceptions[exception] = true
Line 545: Line 692:
local else_clause, finally_clause
local else_clause, finally_clause
if args[i-1] == 'except' then
if args[i-1] == 'except' then
error('SyntaxError: except after except clause without specific exceptions, which should be the last')
error("SyntaxError: except after except clause without specific exceptions, which should be the last", 3)
elseif args[i-1] == 'else' then
elseif args[i-1] == 'else' then
libraryUtil.checkType('try', i, args[i], 'function', nil, 4)
libraryUtil.checkType('try', i, args[i], 'function', nil, 4)
Line 558: Line 705:


if args[i-1] ~= nil then
if args[i-1] ~= nil then
error(('SyntaxError: unexpected arguments #%d–#%d to "try"'):format(i-1, #args), 3)
error(("SyntaxError: unexpected arguments #%d–#%d to 'try'"):format(i-1, #args), 3)
end
end
return try_clause, except_clauses, else_clause, finally_clause
return try_clause, except_clauses, else_clause, finally_clause
Line 571: Line 718:
for _, except in ipairs(except_clauses) do
for _, except in ipairs(except_clauses) do
if except.exceptions[errtype] or #except.exceptions == 0 then
if except.exceptions[errtype] or #except.exceptions == 0 then
handled, message = pcall(except.handler())
handled, message = pcall(except.handler)
break
break
end
end
Line 588: Line 735:
end
end
if not success and message then
if not success and message then
error(message)
error(message, 0) -- what should be the level?
end
end
end
end

Revision as of 22:48, 21 July 2021

local libraryUtil = require('libraryUtil') -- overridden for new types and exceptions
local warn = require('Module:Warning')

local classes, instances = {}, {} -- registry of all complete/internal class and instance objects and their pointers
local inst_private_mts, inst_public_mts = {}, {} -- for each class since they are mostly immutable

local una_metamethods = {__ipairs=1, __pairs=1, __tostring=1, __unm=1}
local bin_metamethods = {__add=1, __concat=1, __div=1, __eq=1, __le=1, __lt=1, __mod=1, __mul=1, __pow=1, __sub=1}
local oth_metamethods = {__call=1, __index=1, __newindex=1, __init=1}
local not_metamethods = {__name=1, __bases=1, __methods=1, __classmethods=1, __staticmethods=1, __normalmethods=1, __slots=1, __protected=1}
	-- __class and __hash


local function objtostr(obj)
	local copy = {}
	for key, val in pairs(obj) do
		copy[key] = type(val) == 'function' and 'function' or val
	end
	return mw.text.jsonEncode(copy, mw.text.JSON_PRETTY)
end

local inst_mt = {
	__index = function (self, key)
		if tonumber(key) or key == 'hash' or key == '__hash' then -- don't search numeric keys in classes and hash isn't inheritable
			return nil
		end
		return self.__class[key] -- key could be invalid here without issues as __index(cls_private, key) would handle it
	end,
	__tostring = objtostr--
}

local function private_read(self_private, key)
	return instances[self_private][key] -- instance should be clean of invalid keys so that __index(cls_private, key) handles them
end

local function private_read_custom(self_private, key)
	if not_metamethods[key] then
		error(("AttributeError: unauthorized read attempt of internal '%s'"):format(key), 2)
	end
	local self = instances[self_private]
	local value = self.__class.__index(self_private, key) -- custom __index can handle invalid keys
	if value == nil then
		return self[key] -- same reason of private_read for not checking key validity
	end
	return value
end

local function private_write(self_private, key, value)
	libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'})
	local self = instances[self_private]
	if type(key) == 'string' then
		local cls = classes[self.__class]
		if cls.__normalmethods[key] or key:sub(1,2) == '__' and key ~= '__hash' then
			error(("AttributeError: forbidden write attempt {%s: %s} to immutable method or invalid key"):format(key, tostring(value)), 2)
		elseif key:find('[^_%w]') or key:find('^%d') then
			error(("AttributeError: invalid attribute name '%s'"):format(key), 2)
		elseif key == '__hash' and self.__hash ~= nil then
			error("AttributeError: forbidden update attempt to immutable __hash", 2)
		end
	end
	self[key] = value
end

local function private_write_custom(self_private, key, value)
	local self = instances[self_private]
	local cls = classes[self.__class]
	local keyType = type(key)
	if keyType == 'string' and (cls.__normalmethods[key] or key:sub(1,2) == '__' and key ~= '__hash') then
		error(("AttributeError: forbidden write attempt {%s: %s} to immutable method or invalid key"):format(key, tostring(value)), 2)
	end
	if cls.__newindex(self_private, key, value) == false then -- custom __newindex can handle invalid keys
		libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'})
		if keyType == 'string' then
			if key:find('[^_%w]') or key:find('^%d') then
				error(("AttributeError: invalid attribute name '%s'"):format(key), 2)
			elseif key == '__hash' and self.__hash ~= nil then
				error("AttributeError: forbidden update attempt to immutable __hash", 2)
			end
		end
		self[key] = value
	end
end

local function public_read(self_public, key)
	if type(key) == 'string' and key:sub(1,1) == '_' then
		error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2)
	end
	return instances[instances[self_public]][key] -- same reason of private_read...
end

local function public_read_custom(self_public, key)
	if type(key) == 'string' and key:sub(1,1) == '_' then
		error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2)
	end
	local self = instances[instances[self_public]]
	local value = self.__class.__index(instances[self_public], key)
	if value == nil then
		return self[key] -- same reason of private_read...
	end
	return value
end

local function public_write(self_public, key, value)
	local self = instances[instances[self_public]]
	local cls = classes[self.__class]
	if type(key) == 'string' then
		if key:sub(1,1) == '_' then
			error(("AttributeError: unauthorized write attempt of nonpublic {%s: %s}"):format(key, tostring(value)), 2)
		elseif cls.__normalmethods[key] then
			error(("AttributeError: forbidden write attempt {%s: %s} to immutable method"):format(key, tostring(value)), 2)
		end
	end
	if self[key] == nil and not cls.__slots[key] then -- if instance and __slots are valid, no danger of creating invalid attributes
		libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) -- otherwise error message would not make sense
		error(("AttributeError: public attribute creation attempt {%s: %s} not expected by __slots"):format(key, tostring(value)), 2)
	end
	self[key] = value
end

local function public_write_custom(self_public, key, value)
	local self = instances[instances[self_public]]
	local cls = classes[self.__class]
	if type(key) == 'string' then
		if key:sub(1,1) == '_' then
			error(("AttributeError: unauthorized write attempt of nonpublic {%s: %s}"):format(key, tostring(value)), 2)
		elseif cls.__normalmethods[key] then
			error(("AttributeError: forbidden write attempt {%s: %s} to immutable method"):format(key, tostring(value)), 2)
		end
	end
	if cls.__newindex(instances[self_public], key, value) == false then
		if self[key] == nil and not cls.__slots[key] then
			libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) -- otherwise error message...
			error(("AttributeError: public attribute creation attempt {%s: %s} not expected by __slots"):format(key, tostring(value)), 2)
		end
		self[key] = value
	end
end

local function constructor(wrapper, ...)
	if select('#', ...) ~= 1 or type(...) ~= 'table' then
		error("SyntaxError: incorrect instance constructor syntax, should be: Class{arg1, arg2..., kw1=kwarg1, kw2=kwarg2...}", 2)
	end
	local self = {} -- __new
	local cls_private = classes[classes[wrapper]] and classes[wrapper] or wrapper
	self.__class = cls_private
	setmetatable(self, inst_mt)

	local self_private = {} -- wrapper
	local cls = classes[cls_private]

	local mt = inst_private_mts[cls]
	if not mt then
		mt = {}
		mt.__index = cls.__index and private_read_custom or private_read
		mt.__newindex = cls.__newindex and private_write_custom or private_write
		for key in pairs(una_metamethods) do
			mt[key] = cls[key]
		end
		mt.__call = cls.__call
		mt.__metatable = "unauthorized access attempt of wrapper object metatable"

		inst_private_mts[cls] = mt
	end

	setmetatable(self_private, mt)
	instances[self_private] = self

	local __init = cls.__init
	if __init and __init(self_private, ...) then
		error("TypeError: __init must not return a var-list")
	end

	for key in pairs(cls.__methods) do
		local func = cls[key] -- index once to save time in future calls
		self[key] = function (...) return func(self_private, ...) end
	end

	if cls._hash then -- not inheritable
		self.hash = function () return cls._hash(self_private) end
		self.hash() -- construction of self is finalized at this point, so immutable hash can be safely set
	end

	local self_public = {}

	mt = inst_public_mts[cls]
	if not mt then
		mt = {}
		mt.__index = cls.__index and public_read_custom or public_read
		mt.__newindex = cls.__newindex and public_write_custom or public_write
		for key in pairs(una_metamethods) do
			if cls[key] then
				local func = cls[key]
				mt[key] = function (a) return func(instances[a]) end
			end
		end
		for key in pairs(bin_metamethods) do
			if cls[key] then
				local func = cls[key]
				mt[key] = function (a, b) return func(instances[a], instances[b]) end
			end
		end
		if cls.__call then
			local func = cls.__call
			mt.__call = function (self_public, ...) return func(instances[self_public], ...) end
		end
		mt.__metatable = "unauthorized access attempt of wrapper object metatable"

		inst_public_mts[cls] = mt
	end

	setmetatable(self_public, mt)
	instances[self_public] = self_private
	return self_public
end


local function multi_inheritance(cls, key)
	for _, base in ipairs(cls.__bases) do
		if key:sub(1,1) ~= '_' or base.__protected[key] or key:sub(1,2) == '__' and key ~= '__name' and key ~= '__hash' then
			local value = base[key]
			if value ~= nil then
				return value
			end
		end
	end
end

local cls_mt = {
	__index = multi_inheritance,
	__tostring = objtostr--
}

local cls_private_mt = {
	__call = constructor,
	__index = function (cls_private, key)
		if type(key) ~= 'string' then
			warn(("AttributeWarning: index '%s' type should be string, %s given"):format(tostring(key), type(key)), 2)
		elseif not_metamethods[key] then
			error(("AttributeError: unauthorized read attempt of internal '%s'"):format(key), 2)
		elseif key:find('[^_%w]') or key:find('^%d') then
			warn(("AttributeWarning: index '%s' should be a valid Lua name"):format(key), 2)
		end
		local cls = classes[cls_private]
		local value = cls[key]
		if not cls.__slots[key] then
			local valueType = type(value)
			if valueType == 'table' then
				return mw.clone(value) -- because class attributes are immutable by default
			elseif valueType == 'set' then --should list be clone or deep copy?
				return value.copy()
			end
		end
		return value
	end,
	__newindex = function (cls_private, key, value)
		local cls = classes[cls_private]
		if not cls.__slots[key] and key ~= '__hash' then -- __slots should be valid, so no need to check key validity before
			libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message would not make sense
			error(("AttributeError: write attempt {%s: %s} not expected by __slots"):format(key, tostring(value)), 2)
		elseif key == '__hash' and cls.__hash ~= nil then
			error("AttributeError: forbidden update attempt to immutable __hash", 2)
		end
		cls[key] = value
	end,
	__metatable = "unauthorized access attempt of wrapper object metatable"
}

local cls_public_mt = {
	__call = constructor,
	__index = function (cls_public, key)
		if type(key) ~= 'string' then
			warn(("AttributeWarning: index '%s' type should be string, %s given"):format(tostring(key), type(key)), 2)
		elseif key:sub(1,1) == '_' then
			error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2)
		elseif key:find('[^_%w]') or key:find('^%d') then
			warn(("AttributeWarning: index '%s' should be a valid Lua name"):format(key), 2)
		end
		local value = classes[classes[cls_public]][key]
		local valueType = type(value)
		if valueType == 'table' then
			return mw.clone(value) -- all class attributes are immutable in the public scope
		elseif valueType == 'set' then
			return value.copy()
		end
		return value
	end,
	__newindex = function (cls_public, key, value)
		libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message...
		error(("AttributeError: forbidden write attempt of {%s: %s}; a class is immutable in public scope"):format(key, tostring(value)), 2)
	end,
	__metatable = "unauthorized access attempt of wrapper object metatable"
}

math.randomseed(os.time())

local function hash(obj_private)
	if not obj_private.__hash then -- not inheritable
		obj_private.__hash = math.floor(math.random() * 2^32) -- due to math.random bug in Lua 5.1
	end
	return obj_private.__hash
end

function class(...)
	local args = {...}
	local cls = {} -- internal

	local idx
	if type(args[1]) == 'string' then
		local __name = args[1]
		if __name:find('%W') or __name:find('^%d') then
			error(("ValueError: class '%s' must be a valid Lua name without '_'s"):format(__name), 2)
		end
		cls.__name = __name
		idx = 2
	else
		idx = 1
	end

	cls.__bases = {}
	for i = idx, #args-1 do
		libraryUtil.checkType('class', i, args[i], 'class')
		cls.__bases[#cls.__bases+1] = classes[classes[args[i]]]
	end
	setmetatable(cls, cls_mt)

	local kwargs = args[#args]
	libraryUtil.checkType('class', #args, kwargs, 'table')
	if kwargs.__name ~= nil or kwargs.__bases ~= nil then
		error("ValueError: __name and unpacked __bases must be passed as optional first args to 'class'", 2)
	end

	cls.__slots = {}
	local mt = {
		__index = function (__slots, key) -- multi_inheritance
			for _, base in ipairs(cls.__bases) do
				if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__slots[key] then
					return true
				end
			end
		end
	}
	setmetatable(cls.__slots, mt)
	if kwargs.__slots ~= nil then
		libraryUtil.checkTypeForNamedArg('class', '__slots', kwargs.__slots, 'table')
		for i, slot in ipairs(kwargs.__slots) do
			libraryUtil.checkType('__slots', i, slot, 'string')
			if slot:find('[^_%w]') or slot:find('^%d') then
				error(("ValueError: invalid slot name '%s'"):format(slot), 2)
			elseif slot:sub(1,2) == '__' then
				error(("ValueError: slot '%s' has forbidden namespace"):format(slot), 2)
			elseif rawget(cls.__slots, slot) then
				warn(("ValueWarning: duplicated slot '%s'"):format(slot), 2)
			elseif kwargs[slot] ~= nil or cls.__slots[slot] then
				error(("ValueError: slot '%s' is predefined in class or allocated in __slots of bases"):format(slot), 2)
			end
			cls.__slots[slot] = true
		end
		kwargs.__slots = nil
	end

	cls.__protected = {}
	mt = {
		__index = function (__protected, key)
			for _, base in ipairs(cls.__bases) do
				if base.__protected[key] then
					return true
				end
			end
		end
	}
	setmetatable(cls.__protected, mt)
	if kwargs.__protected ~= nil then
		libraryUtil.checkTypeForNamedArg('class', '__protected', kwargs.__protected, 'table')
		for i, key in ipairs(kwargs.__protected) do
			libraryUtil.checkType('__protected', i, key, 'string')
			if key:sub(1,1) ~= '_' or key:sub(2,2) == '_' then
				error(("ValueError: the namespace of '%s' is not manually protectable"):format(key), 2)
			elseif key == '_hash' then
				error("ValueError: forbidden attempt to protect _hash which is not inheritable", 2)
			elseif rawget(cls.__protected, key) then
				warn(("ValueWarning: duplicated '%s' in __protected"):format(key), 2)
			elseif cls.__protected[key] then
				error(("ValueError: '%s' is already allocated in __protected of bases"):format(key), 2)
			elseif kwargs[key] == nil then -- key validity will be checked further ahead
				error(("ValueError: attempt to protect undefined '%s'"):format(key), 2)
			end
			cls.__protected[key] = true
		end
		kwargs.__protected = nil
	end

	if kwargs.__methods ~= nil then
		error("ValueError: __classmethods and __staticmethods should be passed as optional attributes instead of __methods", 2)
	elseif kwargs.hash ~= nil or kwargs.__hash ~= nil then
		error("ValueError: forbidden attempt to define hash or __hash which are set internally", 2)
	end

	cls.__normalmethods = {} -- used in instance write methods
	mt = {
		__index = function (__normalmethods, key)
			return cls.__methods[key] or cls.__classmethods[key] or cls.__staticmethods[key]
		end
	}
	setmetatable(cls.__normalmethods, mt)

	local cls_private = {} -- wrapper
	setmetatable(cls_private, cls_private_mt)
	classes[cls_private] = cls

	cls.__classmethods = {}
	mt = {
		__index = function (__classmethods, key)
			for _, base in ipairs(cls.__bases) do
				if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__classmethods[key] then
					return true
				end
			end
		end
	}
	setmetatable(cls.__classmethods, mt)
	if kwargs.__classmethods ~= nil then
		libraryUtil.checkTypeForNamedArg('class', '__classmethods', kwargs.__classmethods, 'table')
		for i, key in ipairs(kwargs.__classmethods) do
			libraryUtil.checkType('__classmethods', i, key, 'string')
			if key:find('[^_%w]') or key:find('^%d') then
				error(("ValueError: invalid classmethod name '%s'"):format(key), 2)
			elseif key:sub(1,2) == '__' then
				error(("ValueError: classmethod '%s' has forbidden namespace"):format(key), 2)
			elseif key == '_hash' then
				error("ValueError: invalid classmethod _hash, classes have their own hash classmethod", 2)
			elseif rawget(cls.__classmethods, key) then
				error(("ValueError: duplicated '%s' in __classmethods"):format(key), 2)
			elseif not cls.__classmethods[key] and cls[key] ~= nil then
				error(("ValueError: forbidden attempt to convert '%s' non-classmethod to classmethod"):format(key), 2)
			end
			libraryUtil.checkTypeForNamedArg('class', key, kwargs[key], 'function')

			cls.__classmethods[key] = true
			local func = kwargs[key]
			cls[key] = function (...) return func(cls_private, ...) end
			kwargs[key] = nil
		end
		kwargs.__classmethods = nil
	end

	cls.__normalmethods.hash = true
	cls.hash = function () return hash(cls_private) end -- classes are always hashable so this is independent from _hash
	cls.hash()

	if kwargs.__eq == nil and kwargs._hash == nil then
		kwargs._hash = hash
	end
	if kwargs._hash ~= nil then
		libraryUtil.checkTypeForNamedArg('class', '_hash', kwargs._hash, 'function')
		cls.__normalmethods._hash = true
		cls._hash = kwargs._hash
		kwargs._hash = nil
	end

	cls.__staticmethods = {}
	mt = {
		__index = function (__staticmethods, key)
			for _, base in ipairs(cls.__bases) do
				if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__staticmethods[key] then
					return true
				end
			end
		end
	}
	setmetatable(cls.__staticmethods, mt)
	if kwargs.__staticmethods ~= nil then
		libraryUtil.checkTypeForNamedArg('class', '__staticmethods', kwargs.__staticmethods, 'table')
		for i, key in ipairs(kwargs.__staticmethods) do
			libraryUtil.checkType('__staticmethods', i, key, 'string')
			if key:sub(1,2) == '__' then
				error(("ValueError: staticmethod '%s' has forbidden namespace"):format(key), 2)
			elseif rawget(cls.__staticmethods, key) then
				warn(("ValueWarning: duplicated staticmethod '%s'"):format(key), 2)
			elseif not cls.__staticmethods[key] and cls[key] ~= nil then
				error(("ValueError: forbidden attempt to convert '%s' non-staticmethod to staticmethod"):format(key), 2)
			end
			libraryUtil.checkTypeForNamedArg('class', key, kwargs[key], 'function')
			cls.__staticmethods[key] = true
		end
		kwargs.__staticmethods = nil
	end

	cls.__methods = {}
	for _, base in ipairs(cls.__bases) do
		for key in pairs(base.__methods) do
			if key:sub(1,1) ~= '_' or base.__protected[key] then
				cls.__methods[key] = true
			end
		end
	end

	local valid = false
	for key, val in pairs(kwargs) do
		if type(key) ~= 'string' then
			error(("TypeError: invalid attribute name '%s' (string expected, got %s)"):format(tostring(key), type(key)), 2)
		elseif key:find('[^_%w]') or key:find('^%d') then
			error(("ValueError: invalid attribute name '%s'"):format(key), 2)
		elseif key:sub(1,2) == '__' and not una_metamethods[key] and not bin_metamethods[key] and not oth_metamethods[key] then
			error(("ValueError: unrecognized metamethod or unauthorized internal attribute {%s: %s}"):format(key, tostring(val)), 2)
		end
		cls[key] = val
		if type(val) == 'function' then
			if not cls.__staticmethods[key] and key:sub(1,2) ~= '__' then -- classmethods and _hash were already removed from kwargs
				cls.__methods[key] = true
			end
			if key ~= '__init' then -- __init does not qualify to a functional/proper class
				valid = true
			end
		end
	end
	if not valid then
		error("ValueError: a (sub)class must have at least one functional method", 2)
	end

	local cls_public = {}
	setmetatable(cls_public, cls_public_mt)
	classes[cls_public] = cls_private
	return cls_public, cls_private -- cls_private should not be accesible outside the module which defined it!
end								   --make it a proxy to try to enforce behavior?


local function rissubclass2(class, classinfo)
	if class == classinfo then
		return true
	end
	for _, base in ipairs(class.__bases) do
		if rissubclass2(base, classinfo) then
			return true
		end
	end
	return false
end

local function rissubclass1(class, classinfo, parent, level)
	libraryUtil.checkTypeMulti(parent, 2, classinfo, {'class', 'table'}, level)
	if classes[classinfo] then
		return rissubclass2(class, classes[classes[classinfo]])
	end
	for i = 1, #classinfo do
		if rissubclass1(class, classinfo[i], parent, level+1) then
			return true
		end
	end
	return false
end

function issubclass(class, classinfo)
	libraryUtil.checkType('issubclass', 1, class, 'class')
	class = classes[class]
	return rissubclass1(classes[class] or class, classinfo, 'issubclass', 4)
end

function isinstance(instance, classinfo)
	instance = instances[instance]
	if instance then -- because named (ClassName) instances would fail with checkType
		return rissubclass1(classes[instance.__class], classinfo, 'isinstance', 4)
	end
	error(("TypeError: bad argument #1 to 'isinstance' (instance expected, got %s)"):format(type(instance)), 2)
end


local _type = type
type = function (value)
	local t = _type(value)
	if t == 'table' then
		if classes[value] then
			return 'class'
		elseif instances[value] then
			return classes[instances[value].__class].__name or 'instance' -- should __name be directly readable instead?
		end
	end
	return t
end


libraryUtil.checkType = function (name, argIdx, arg, expectType, nilOk, level)
	if arg == nil and nilOk then
		return
	end
	if type(arg) ~= expectType then
		error(("TypeError: bad argument #%d to '%s' (%s expected, got %s)"):format(argIdx, name, expectType, type(arg)), level or 3)
	end
end

libraryUtil.checkTypeMulti = function (name, argIdx, arg, expectTypes, level)
	local argType = type(arg)
	for _, expectType in ipairs(expectTypes) do
		if argType == expectType then
			return
		end
	end
	local n = #expectTypes
	local typeList
	if n > 1 then
		typeList = table.concat(expectTypes, ', ', 1, n-1) .. ' or ' .. expectTypes[n]
	else
		typeList = expectTypes[1]
	end
	error(("TypeError: bad argument #%d to '%s' (%s expected, got %s)"):format(argIdx, name, typeList, type(arg)), level or 3)
end

libraryUtil.checkTypeForIndex = function (index, value, expectType, level)
	if type(value) ~= expectType then
		error(("TypeError: value for index '%s' must be %s, %s given"):format(index, expectType, type(value)), level or 3)
	end
end

libraryUtil.checkTypeMultiForIndex = function (index, expectTypes, level)
	local indexType = type(index)
	for _, expectType in ipairs(expectTypes) do
		if indexType == expectType then
			return
		end
	end
	local n = #expectTypes
	local typeList
	if n > 1 then
		typeList = table.concat(expectTypes, ', ', 1, n-1) .. ' or ' .. expectTypes[n]
	else
		typeList = expectTypes[1]
	end
	error(("TypeError: index '%s' must be %s, %s given"):format(index, typeList, type(index)), level or 3)
end

libraryUtil.checkTypeForNamedArg = function (name, argName, arg, expectType, nilOk, level)
	if arg == nil and nilOk then
		return
	end
	if type(arg) ~= expectType then
		error(("TypeError: bad named argument %s to '%s' (%s expected, got %s)"):format(argName, name, expectType, type(arg)), level or 3)
	end
end

libraryUtil.checkTypeMultiForNamedArg = function (name, argName, arg, expectTypes, level)
	local argType = type(arg)
	for _, expectType in ipairs(expectTypes) do
		if argType == expectType then
			return
		end
	end
	local n = #expectTypes
	local typeList
	if n > 1 then
		typeList = table.concat(expectTypes, ', ', 1, n-1) .. ' or ' .. expectTypes[n]
	else
		typeList = expectTypes[1]
	end
	error(("TypeError: bad named argument %s to '%s' (%s expected, got %s)"):format(argName, name, typeList, type(arg)), level or 3)
end


local function try_parser(...)
	local args = {...}
	libraryUtil.checkType('try', 1, args[1], 'function', nil, 4)
	local try_clause = args[1]

	if args[2] ~= 'except' then
		error("SyntaxError: missing required except clause", 3)
	end
	local except_clauses = {}
	local i = 3
	local argType, exceptionTypes = nil, {string=1, table=1}
	repeat
		libraryUtil.checkTypeMulti('try', i, args[i], {'string', 'table', 'function'}, 4)
		argType = type(args[i])
		if exceptionTypes[argType] then
			libraryUtil.checkType('try', i+1, args[i+1], 'function', nil, 4)
			except_clauses[#except_clauses+1] = {exceptions={}, handler=args[i+1]}
			if argType == 'string' then
				except_clauses[#except_clauses].exceptions[args[i]] = true
			else
				for _, exception in ipairs(args[i]) do
					if type(exception) ~= 'string' then
						error(("TypeError: invalid exception type in except (string expected, got %s)"):format(type(exception)), 3)
					end
					except_clauses[#except_clauses].exceptions[exception] = true
				end
			end
			i = i + 3
		else
			except_clauses[#except_clauses+1] = {exceptions={}, handler=args[i]}
			i = i + 2
			break
		end
	until args[i-1] ~= 'except'

	local else_clause, finally_clause
	if args[i-1] == 'except' then
		error("SyntaxError: except after except clause without specific exceptions, which should be the last", 3)
	elseif args[i-1] == 'else' then
		libraryUtil.checkType('try', i, args[i], 'function', nil, 4)
		else_clause = args[i]
		i = i + 2
	end
	if args[i-1] == 'finally' then
		libraryUtil.checkType('try', i, args[i], 'function', nil, 4)
		finally_clause = args[i]
		i = i + 2
	end

	if args[i-1] ~= nil then
		error(("SyntaxError: unexpected arguments #%d–#%d to 'try'"):format(i-1, #args), 3)
	end
	return try_clause, except_clauses, else_clause, finally_clause
end

function try(...)
	local try_clause, except_clauses, else_clause, finally_clause = try_parser(...)

	local function errhandler(message)
		local errtype = mw.text.split(message, ':')[1]
		local handled = false
		for _, except in ipairs(except_clauses) do
			if except.exceptions[errtype] or #except.exceptions == 0 then
				handled, message = pcall(except.handler)
				break
			end
		end
		if not handled then
			return message
		end
	end

	local success, message = xpcall(try_clause, errhandler)
	if else_clause and success then
		success, message = pcall(else_clause)
	end
	if finally_clause then
		finally_clause()
	end
	if not success and message then
		error(message, 0) -- what should be the level?
	end
end

return classes, instances--