585 lines
22 KiB
Lua
Executable File

--------------------------------------------------------------------------------
---------------------- ## ##### ##### ###### -----------------------
---------------------- ## ## ## ## ## ## ## -----------------------
---------------------- ## ## ## ## ## ###### -----------------------
---------------------- ## ## ## ## ## ## -----------------------
---------------------- ###### ##### ##### ## -----------------------
---------------------- -----------------------
----------------------- Lua Object-Oriented Programming ------------------------
--------------------------------------------------------------------------------
-- Project: LOOP - Lua Object-Oriented Programming --
-- Release: 2.3 beta --
-- Title : Scoped Class Model --
-- Author : Renato Maia <maia@inf.puc-rio.br> --
--------------------------------------------------------------------------------
-- Exported API: --
-- class(class, ...) --
-- new(class, ...) --
-- classof(object) --
-- isclass(class) --
-- instanceof(object, class) --
-- memberof(class, name) --
-- members(class) --
-- superclass(class) --
-- subclassof(class, super) --
-- supers(class) --
-- allmembers(class) --
-- --
-- methodfunction(method) --
-- methodclass(method) --
-- this(object) --
-- priv(object, [class]) --
-- prot(object) --
--------------------------------------------------------------------------------
-- TODO:
-- Test replacement of all members of a scope by ClassProxy[scope] = { ... }
-- Test static method call.
-- Define a default __eq metamethod that compares the object references.
-- The best way to relink member with numeric, string and boolean values.
-- Replace conditional compiler by static function constructors.
--------------------------------------------------------------------------------
local type = type
local pairs = pairs
local assert = assert
local ipairs = ipairs
local setmetatable = setmetatable
local unpack = unpack
local require = require
local rawset = rawset
local getmetatable = getmetatable
local rawget = rawget
local debug = require "debug"
local table = require "loop.table"
module "loop.scoped" -- [[VERBOSE]] local verbose = require "loop.verbose"
--------------------------------------------------------------------------------
local ObjectCache = require "loop.collection.ObjectCache"
local OrderedSet = require "loop.collection.OrderedSet"
local base = require "loop.multiple"
--------------------------------------------------------------------------------
table.copy(require "loop.cached", _M)
--------------------------------------------------------------------------------
--- SCOPED DATA CHAIN ----------------------------------------------------------
--------------------------------------------------------------------------------
-- maps private and protected state objects to the object (public) table
local Object = setmetatable({}, { __mode = "kv" })
--------------------------------------------------------------------------------
local function newprotected(self, object) -- [[VERBOSE]] verbose:scoped("new 'protected' for 'public' ",object)
local protected = self.class()
Object[protected] = object
return protected
end
local function ProtectedPool(members) -- [[VERBOSE]] verbose:scoped "new protected pool"
return ObjectCache {
class = base.class(members),
retrieve = newprotected,
}
end
--------------------------------------------------------------------------------
local function newprivate(self, outter) -- [[VERBOSE]] verbose:scoped(true, "retrieving 'private' for reference ",outter)
local object = Object[outter] -- [[VERBOSE]] verbose:scoped("'public' is ",object or outter)
local private = rawget(self, object)
if not private then
private = self.class() -- [[VERBOSE]] verbose:scoped("new 'private' created: ",private)
if object then
Object[private] = object -- [[VERBOSE]] verbose:scoped("'public' ",object," registered for the new 'private' ",private)
self[object] = private -- [[VERBOSE]] verbose:scoped("new 'private' ",private," stored at the pool for 'public' ",object)
else
Object[private] = outter -- [[VERBOSE]] verbose:scoped("'public' ",outter," registered for the new 'private' ",private)
end -- [[VERBOSE]] else verbose:scoped("reusing 'private' ",private," associated to 'public'")
end -- [[VERBOSE]] verbose:scoped(false, "returning 'private' ",private," for reference ",outter)
return private
end
local function PrivatePool(members) -- [[VERBOSE]] verbose:scoped{"new private pool", members = members}
return ObjectCache {
class = base.class(members),
retrieve = newprivate,
}
end
--------------------------------------------------------------------------------
local function bindto(class, member)
if type(member) == "function" then -- [[VERBOSE]] verbose:scoped("new method closure for ",member)
local pool
local method = member
member = function (self, ...)
pool = rawget(class, getmetatable(self)) -- [[VERBOSE]] verbose:scoped("method call on reference ",self," (pool: ",pool,")")
if pool
then return method(pool[self], ...)
else return method(self, ...)
end
end
end
return member
end
--------------------------------------------------------------------------------
local ConditionalCompiler = require "loop.compiler.Conditional"
local indexer = ConditionalCompiler {
{[[local Public = select(1, ...) ]],"private or protected" },
{[[local meta = select(2, ...) ]],"not (newindex and public and nilindex)" },
{[[local class = select(3, ...) ]],"newindex or private" },
{[[local bindto = select(4, ...) ]],"newindex" },
{[[local newindex = select(5, ...) ]],"newindex and not nilindex" },
{[[local index = select(5, ...) ]],"index and not nilindex" },
{[[local registry = class.registry ]],"private" },
{[[local result ]],"index and (private or protected)" },
{[[return function (state, name, value) ]],"newindex" },
{[[return function (state, name) ]],"index" },
{[[ result = meta[name] ]],"index and (private or protected)" },
{[[ return meta[name] ]],"index and public" },
{[[ or index[name] ]],"index and tableindex" },
{[[ or index(state, name) ]],"index and functionindex" },
{[[ if result == nil then ]],"index and (private or protected)" },
{[[ if meta[name] == nil then ]],"newindex and (private or protected or not nilindex)" },
{[[ state = Public[state] ]],"private or protected" },
{[[ local Protected = registry[getmetatable(state)] ]],"private" },
{[[ if Protected then state = Protected[state] end ]],"private" },
{[[ return state[name] ]],"index and (private or protected)" },
{[[ state[name] = bindto(class, value) ]],"newindex and (private or protected) and nilindex" },
{[[ newindex[name] = bindto(class, value) ]],"newindex and tableindex" },
{[[ return newindex(state, name, bindto(class, value))]],"newindex and functionindex" },
{[[ else ]],"newindex and (private or protected or not nilindex)" },
{[[ return rawset(state, name, bindto(class, value)) ]],"newindex" },
{[[ end ]],"private or protected or (newindex and not nilindex)" },
{[[ return result ]],"index and (private or protected)" },
{[[end ]]},
}
local function createindexer(class, scope, action)
local meta = class:getmeta(scope)
local index = meta["__"..action]
local indextype = type(index).."index"
local codename = table.concat({scope,action,index and ("with "..indextype)}," ")
return indexer:execute({
[action] = true,
[scope] = true,
[indextype] = true,
},
Object,
meta,
class,
bindto,
index
)
end
local function unwrap(meta, tag)
local indexer
local key = "__"..tag
local func = assert(meta[key], "no indexer found in scoped class metatable.")
local name, value
local i = 1
repeat
name, value = debug.getupvalue(func, i)
i = i + 1
until name == nil or name == tag
return value
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function supersiterator(stack, class)
class = stack[class]
if class then
for _, super in ipairs(class.supers) do
stack:insert(super, class)
end
return class
end
end
local function hierarchyof(class)
local stack = OrderedSet()
stack:push(class)
return supersiterator, stack, OrderedSet.firstkey
end
--------------------------------------------------------------------------------
local function publicproxy_call(_, object)
return this(object)
end
local function protectedproxy_call(_, object)
return prot(object)
end
local function privateproxy_call(_, object, class)
return priv(object, class)
end
--------------------------------------------------------------------------------
local ScopedClass = base.class({}, CachedClass)
function ScopedClass:getmeta(scope)
return self[scope] and self[scope].class
or
(scope == "public") and self.class
or
nil
end
function ScopedClass:getmembers(scope)
return self.members[scope]
end
function ScopedClass:__init(class)
if not class then class = { public = {} } end
-- adjust class definition to use scoped member tables
if type(class.public) ~= "table" then
if
(type(class.protected) == "table")
or
(type(class.private) == "table")
then
class.public = {}
else
local public = table.copy(class)
table.clear(class)
class.public = public
end
end
-- initialize scoped cached class
self = CachedClass.__init(self, class)
self.registry = { [self.class] = false }
-- define scoped class proxy for public state
rawset(self.proxy, "public", setmetatable({}, {
__call = publicproxy_call,
__index = self.class,
__newindex = function(_, field, value)
self:updatefield(field, value, "public")
end,
}))
return self
end
function ScopedClass:addsubclass(class)
CachedClass.addsubclass(self, class)
local public = class.class
for super in hierarchyof(self) do
local registry = super.registry
if registry then -- if super is a scoped class
registry[public] = false
super[public] = super.private
end
end
end
function ScopedClass:removesubclass(class)
CachedClass.removesubclass(self, class)
local public = self.class
local protected = self:getmeta("protected")
local private = self:getmeta("private")
for super in hierarchyof(self) do
local registry = super.registry
if registry then -- if super is a scoped class
registry[public] = nil
super[public] = nil
if protected then
registry[protected] = nil
super[protected] = nil
end
if private then
registry[private] = nil
super[private] = nil
end
end
end
end
local function copymembers(class, source, destiny)
if source then
if not destiny then destiny = {} end
for field, value in pairs(source) do
destiny[field] = bindto(class, value)
end
end
return destiny
end
function ScopedClass:updatemembers()
--
-- metatables to collect with current members
--
local public = table.clear(self.class)
local protected
local private
--
-- copy inherited members
--
local publicindex, publicnewindex
local protectedindex, protectednewindex
local superclasses = self.supers
for i = #superclasses, 1, -1 do
local super = superclasses[i]
-- copy members from superclass metatables
public = table.copy(super.class, public)
if base.instanceof(super, ScopedClass) then
-- copy protected members from superclass metatables
protected = table.copy(super:getmeta("protected"), protected)
-- extract the __index and __newindex values
publicindex = unwrap(public, "index") or publicindex
publicnewindex = unwrap(public, "newindex") or publicnewindex
if protected then
protectedindex = unwrap(protected, "index") or protectedindex
protectednewindex = unwrap(protected, "newindex") or protectednewindex
end
end
end
public.__index = publicindex
public.__newindex = publicnewindex
if protected then
protected.__index = protectedindex
protected.__newindex = protectednewindex
end
--
-- copy members defined in the class
--
public = copymembers(self, self.members.public, public)
protected = copymembers(self, self.members.protected, protected)
private = copymembers(self, self.members.private, private)
--
-- setup public metatable with proper indexers
--
public.__index = createindexer(self, "public", "index")
public.__newindex = createindexer(self, "public", "newindex")
--
-- setup proper protected state features: pool, proxy and indexers
--
if protected then
if not self.protected then
-- create state object pool and class proxy for protected state
self.protected = ProtectedPool(protected)
rawset(self.proxy, "protected", setmetatable({}, {
__call = protectedproxy_call,
__index = protected,
__newindex = function(_, field, value)
self:updatefield(field, value, "protected")
end,
}))
-- register new pool in superclasses
local protected_pool = self.protected
for super in hierarchyof(self) do
local registry = super.registry
if registry then
registry[public] = protected_pool
registry[protected] = false
local pool = super.private
if pool then
super[public] = pool
super[protected] = pool
else
super[public] = protected_pool
end
end
end
else
-- update current metatable with new members
protected = table.copy(protected, table.clear(self.protected.class))
end
-- setup metatable with proper indexers
protected.__index = createindexer(self, "protected", "index")
protected.__newindex = createindexer(self, "protected", "newindex")
elseif self.protected then
-- remove old pool from registry in superclasses
local protected_pool = self.protected
for super in hierarchyof(self) do
local registry = super.registry
if registry then
registry[public] = false
registry[protected_pool.class] = nil
super[public] = super.private
super[protected_pool.class] = nil
end
end
-- remove state object pool and class proxy for protected state
self.protected = nil
rawset(self.proxy, "protected", nil)
end
--
-- setup proper private state features: pool, proxy and indexers
--
if private then
if not self.private then
-- create state object pool and class proxy for private state
self.private = PrivatePool(private)
rawset(self.proxy, "private", setmetatable({}, {
__call = privateproxy_call,
__index = private,
__newindex = function(_, field, value)
self:updatefield(field, value, "private")
end
}))
-- registry new pool in superclasses
local private_pool = self.private
local pool = self.protected or Object
for _, super in ipairs(superclasses) do
for class in hierarchyof(super) do
local registry = class.registry
if registry then -- if class is a scoped class
registry[private] = pool
class[private] = class.private_pool or pool
end
end
end
for meta in pairs(self.registry) do
self[meta] = private_pool
end
else
-- update current metatable with new members
private = table.copy(private, table.clear(self:getmeta("private")))
end
-- setup metatable with proper indexers
private.__index = createindexer(self, "private", "index")
private.__newindex = createindexer(self, "private", "newindex")
elseif self.private then
-- remove old pool from registry in superclasses
local private_pool = self.private
for _, super in ipairs(superclasses) do
for class in hierarchyof(super) do
local registry = class.registry
if registry then -- if class is a scoped class
registry[private_pool.class] = nil
class[private_pool.class] = nil
end
end
end
for meta, pool in pairs(self.registry) do
self[meta] = pool or nil
end
-- remove state object pool and class proxy for private state
self.private = nil
rawset(self.proxy, "private", nil)
end
end
function ScopedClass:updatefield(name, member, scope) -- [[VERBOSE]] verbose:scoped(true, "updating field ",name," on scope ",scope," with value ",member)
member = bindto(self, member)
if not scope then
if
(
name == "public"
or
name == "protected"
or
name == "private"
) and (
member == nil
or
type(member) == "table"
)
then -- [[VERBOSE]] verbose:scoped("updating scope field")
self.members[name] = member
return self:updatemembers() -- [[VERBOSE]] , verbose:scoped(false, "whole scope field updated")
end
scope = "public"
end
-- Update member list
local members = self:getmembers(scope)
members[name] = member
-- Create new member linkage and get old linkage
local metatable = self:getmeta(scope)
local old = metatable[name]
-- Replace old linkage for the new one
metatable[name] = member
if scope ~= "private" then
local queue = OrderedSet()
for sub in pairs(self.subs) do
queue:enqueue(sub)
end
while queue:head() do
local current = queue:dequeue()
metatable = current:getmeta(scope)
members = current:getmembers(scope)
if members and (members[name] == nil) then
for _, super in ipairs(current.supers) do
local super_meta = super:getmeta(scope)
if super_meta[name] ~= nil then
if super_meta[name] ~= metatable[name] then
metatable[name] = super_meta[name]
for sub in pairs(current.subs) do
queue:enqueue(sub)
end
end
break
end
end
end
end
end -- [[VERBOSE]] verbose:scoped(false, "field updated")
return old
end
--------------------------------------------------------------------------------
function class(class, ...)
class = getclass(class) or ScopedClass(class)
class:updatehierarchy(...)
class:updateinheritance()
return class.proxy
end
--------------------------------------------------------------------------------
local cached_classof = classof
function classof(object)
return cached_classof(this(object))
end
-------------------------------------------------------------------------------
function methodfunction(method)
local name, value = debug.getupvalue(method, 5)
assert(name == "method", "Oops! Got the wrong upvalue in 'methodfunction'")
return value
end
--------------------------------------------------------------------------------
function methodclass(method)
local name, value = debug.getupvalue(method, 3)
assert(name == "class", "Oops! Got the wrong upvalue in 'methodclass'")
return value.proxy
end
--------------------------------------------------------------------------------
function this(object)
return Object[object] or object
end
--------------------------------------------------------------------------------
function priv(object, class)
if not class then class = classof(object) end
class = getclass(class)
if class and class.private then
if base.classof(object) == class.private.class
then return object -- private object
else return class.private[object] -- protected or public object
end
end
end
--------------------------------------------------------------------------------
function prot(object)
local class = getclass(classof(object))
if class and class.protected then
if base.classof(object) == class.protected.class
then return object -- protected object
else return class.protected[this(object)] -- private or public object
end
end
end