Skip to content

Commit

Permalink
Allow inheritance of _init from more than one level up (#289)
Browse files Browse the repository at this point in the history
* Allow inheritance of _init from more than one level up

Currently _init is exempted from "fat inheritance", and _init inheritance works by a strange workaround where the base class exactly one level up is checked for _init at constructor time. This appears to be done so that :super() can work properly, but it means that _init cannot be inherited from a grandparent.

This patch adds a _parent_with_init field to all relevant classes which points to the most recent ancestor with an _init implementation. This simplifies both __call and _class and might actually be a performance improvement (it removes a loop).

* Add changelog line and tests for grandparent _init

* Add tests: Invocation of a : function inherited from a grandparent; and ensure :super() fails when it ought to fail.

* Prevent a stack overflow when a particular class :super() edge case is hit.

This causes the "createK" test in test-class.lua to fail with a "null function" error instead of a stack overflow.

* Improved fix for :super infinite loop problem (avoid second rawset)
  • Loading branch information
mcclure authored Sep 22, 2020
1 parent 7c1205d commit d95c4d2
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 16 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.x.x (upcoming)

## Fixes

- In `pl.class`, `_init` can now be inherited from grandparent (or older ancestor) classes. (#289)

## 1.8.0 (2020-08-05)

### New features
Expand Down
36 changes: 20 additions & 16 deletions lua/pl/class.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,29 @@ local compat
-- this trickery is necessary to prevent the inheritance of 'super' and
-- the resulting recursive call problems.
local function call_ctor (c,obj,...)
-- nice alias for the base class ctor
local base = rawget(c,'_base')
if base then
local parent_ctor = rawget(base,'_init')
while not parent_ctor do
base = rawget(base,'_base')
if not base then break end
parent_ctor = rawget(base,'_init')
local init = rawget(c,'_init')
local parent_with_init = rawget(c,'_parent_with_init')

if parent_with_init then
if not init then -- inheriting an init
init = rawget(parent_with_init, '_init')
parent_with_init = rawget(parent_with_init, '_parent_with_init')
end
if parent_ctor then
if parent_with_init then -- super() points to one above whereever _init came from
rawset(obj,'super',function(obj,...)
call_ctor(base,obj,...)
call_ctor(parent_with_init,obj,...)
end)
end
else
-- Without this, calling super() where none exists will sometimes loop and stack overflow
rawset(obj,'super',nil)
end
local res = c._init(obj,...)
rawset(obj,'super',nil)

local res = init(obj,...)
if parent_with_init then -- If this execution of call_ctor set a super, unset it
rawset(obj,'super',nil)
end

return res
end

Expand Down Expand Up @@ -146,6 +152,7 @@ local function _class(base,c_arg,c)
c.__index = c
setmetatable(c,mt)
if not plain then
if base and rawget(base,'_init') then c._parent_with_init = base end -- For super and inherited init
c._init = nil
end

Expand All @@ -160,15 +167,12 @@ local function _class(base,c_arg,c)
if not obj then obj = {} end
setmetatable(obj,c)

if rawget(c,'_init') then -- explicit constructor
if rawget(c,'_init') or rawget(c,'_parent_with_init') then -- constructor exists
local res = call_ctor(c,obj,...)
if res then -- _if_ a ctor returns a value, it becomes the object...
obj = res
setmetatable(obj,c)
end
elseif base and rawget(base,'_init') then -- default constructor
-- make sure that any stuff from the base class is initialized!
call_ctor(base,obj,...)
end

if base and rawget(base,'_post_init') then
Expand Down
75 changes: 75 additions & 0 deletions tests/test-class.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ function B:foo ()
self.eee = 1
end

function B:foo2 ()
self.g = 8
end

asserteq(B(),{a=1,b=2})

-- can continue this chain
Expand All @@ -47,6 +51,77 @@ c = C()
c:foo()

asserteq(c,{a=1,b=2,c=3,eee=1})

-- test indirect inherit

D = class(C)

E = class(D)

function E:_init ()
self:super()
self.e = 4
end

function E:foo ()
-- recommended way to call inherited version of method...
self.eeee = 5
C.foo(self)
end

F = class(E)

function F:_init ()
self:super()
self.f = 6
end

f = F()
f:foo()
f:foo2() -- Test : invocation inherits this function from all the way up in B

asserteq(f,{a=1,b=2,c=3,eee=1,e=4,eeee=5,f=6,g=8})

-- Test that inappropriate calls to super() fail gracefully

G = class() -- Class with no init

H = class(G) -- Class with an init that wrongly calls super()

function H:_init()
self:super() -- Notice: G has no _init
end

I = class(H) -- Inherits the init with a bad super
J = class(I) -- Grandparent-inits the init with a bad super

K = class(J) -- Has an init, which calls the init with a bad super

function K:_init()
self:super()
end

local function createG()
return G()
end

local function createH() -- Wrapper function for pcall
return H()
end

local function createJ()
return J()
end

local function createK()
return K()
end

assert(pcall(createG)) -- Should succeed
assert(not pcall(createH)) -- These three should fail
assert(not pcall(createJ))
assert(not pcall(createK))

--- class methods!
assert(c:is_a(C))
assert(c:is_a(B))
Expand Down

0 comments on commit d95c4d2

Please sign in to comment.