Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow inheritance of _init from more than one level up #289

Merged
merged 5 commits into from
Sep 22, 2020

Conversation

mcclure
Copy link
Contributor

@mcclure mcclure commented Feb 7, 2019

Addresses #288. My smoke test is

lua5.1 -e 'class = require("pl.class") class.A() function A:_init(x) print("A", x) end A(3) class.B(A) B(4) class.C(B) C(5) class.D(C) function D:_init(x) print("D", x) end D(6) class.E(D) E(7) class.F(E) function F:_init(x) print("F", x, "THEN") self:super(x) end F(8) class.G(F) G(9) class.H(G) function H:_init(x) print("H", x, "THEN") self:super(x) end H(10) class.I(H) I(11)'

Explanation:

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).

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).
end
if parent_ctor then
if parent_with_init then -- super() points to one above whereever _init came from
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: Currently i think there is a nasty edge case here where if you erroneously call super() from an inherited _init, but that inherited _init does not legitimately have a super _init to call, you go into an infinite loop. This could be fixed if there were an else here that rawset 'super' to nil or error()ed.

@coveralls
Copy link

coveralls commented Feb 7, 2019

Coverage Status

Coverage decreased (-0.005%) to 84.719% when pulling dc2bb9b on mcclure:grandparent-init into f9f06de on stevedonovan:master.

@mcclure
Copy link
Contributor Author

mcclure commented Jul 23, 2019

Hello @stevedonovan— is this repo still considering pull requests? Is there a way I could reformulate this patch, or a patch to fix the outstanding "broken super() from init()" issue I mention in the comment above, that would make this patch more likely to be accepted? Thanks.

@alerque
Copy link
Member

alerque commented Oct 19, 2019

@Tieske I just had to hack around this on yet another project. Is there something I can to to help this along? I'd be happy to test or code or whatever if it will help this fix (or some fix for grandparent inheritance) to land.

@mcclure
Copy link
Contributor Author

mcclure commented Oct 19, 2019

If the "coverage decreased" is the problem— I am not sure how to use the coverage bot, but as far as I know all new added lines are covered by a test. I think the only reason coverage decreased was that my patch deletes more lines than it adds.

@Tieske
Copy link
Member

Tieske commented Aug 24, 2020

the coverage problem can be either deleting more lines than adding, or it is a coveralls problem, in the sense that it takes only the last report it receives (windows and *nixes have different coverage so depending on who finishes last the coverage changes)

@Tieske
Copy link
Member

Tieske commented Aug 24, 2020

as for the problem; the class system is profoundly broken for this exact issue you ran into. You cannot do inheritance while using the : operator. When using : you can only pass self, and only on first level inheritance this works, on 2nd level, you need the additional class.

So I've been thinking about rewriteing the whole Class thing in a non-fat inheritance style. Because we have seen so little reports about this issue, I think we can safely state that inheritance is 98% single. And then the extra metatable lookups will not be an issue.

@mcclure
Copy link
Contributor Author

mcclure commented Aug 26, 2020

@Tieske :

"You cannot do inheritance while using the : operator. When using : you can only pass self, and only on first level inheritance this works, on 2nd level, you need the additional class"

I don't understand what this means. I use 2nd level inheritance all the time with the : operator and it works fine. The only thing that doesn't work is 2nd level inheritance of _init, and in my testing this patch corrects that.

I don't know if rewriting "class" is a good idea or not; personally I'd suggest do whatever is most efficient. What I would say is that rewriting "class" sounds like a task that could take time, and involve decisions and tradeoffs to be made. On the other hand accepting this patch has no downside, and you could do it immediately.

Fixing the bug I describe in this comment could also be done quickly, and I think also would not come with performance downsides (because I think the else case would only trigger when you are in the failure case the else case handles).

@alerque
Copy link
Member

alerque commented Aug 26, 2020

I second what @mcclure is saying here. I realize there are deeper issues with the class system, but this is a glaring issue that is clearly not functioning at all ... as in if you use the code as documented nothing happens, it's a noop. Anybody with working code has gotten there with a work around, not with the documented way of doing this. With this patch it works as documented. I've tested this on several projects and even run patched version of Penlight on one just for this. I don't understand why a low cost lightweight fix should be held back.

Overhaul the class system if you want, I have no objection. But in the mean time why not let this bit of code that works for people using the current system get released so we don't have to keep patching? This patch not getting attention was the single most prominent issue that made me request the previous maintainer of Penlight consider adding people to the maintainer crew. I offered to help then and I can still offer to help now (although time is slimmer now than a year ago when I offered) — what do we need to do to get this merged? Either in present form or with any revisions that need made...

CHANGELOG.md Outdated
@@ -25,6 +25,7 @@
- Fixed placeholder expressions being evaluated assuming wrong binary operator associativity (e.g. `_1-(_2+_3)` was evaluated as `(_1-_2)+_3`.
- Fixed placeholder expressions being evaluated as if unary operators take precedence over power operator (e.g. `(-_1)^_2`) was evaluated as `-(_1^2)`).
- Fixed vulnerable backtracking pattern in `pl.stringx.strip` (#275)
- In `pl.class`, `_init` can now be inherited from grandparent (or older ancestor) classes. (#289)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs moving to a new/upcomming changelog entry.

@Tieske
Copy link
Member

Tieske commented Aug 26, 2020

@mcclure can you fix the changelog entry, and the edge case you mentioned before (preferably with test cases)

@mcclure
Copy link
Contributor Author

mcclure commented Aug 26, 2020

Thanks. I may not be able to get to this until next week, but I will get on that.

…nd ensure :super() fails when it ought to fail.
…s hit.

This causes the "createK" test in test-class.lua to fail with a "null function" error instead of a stack overflow.
@mcclure
Copy link
Contributor Author

mcclure commented Aug 28, 2020

Hm. Okay.

So in 84ad32b I fixed the changelog, and I added (1) a test for nth level inheritance using the : operator, because @Tieske was worried about that, and (2) a test for the "nasty edge case" I mention in my comment above. So that is all fine.

However, I also (in fbf33c1) fixed the "nasty edge case", and that did not turn out so well.

Will explain below:

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

@mcclure mcclure Aug 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These asserts test a series of similar things.
createG: This asserts G, a normal class with no _init, can be constructed.
createH: H inherits G, and defines an _init which calls :super(). The assert expects this should fail, with an error, because there is no such super-_init.
createJ: J inherits I which inherits H, and inherits H's _init, the bad one that calls :super() when it shouldn't. The assert expects this should fail too.
createK: K inherits J. K has an _init which calls super(), which will wind up calling H's _init, which is the one that calls :super() when it shouldn't. The assert expects this should fail.

All four asserts pass (meaning, the last three functions fail with an error), regardless of whether you are running 84ad32b or fbf33c1. The problem is which error. createH and createJ fail with "called nil function", which is good, because super is nil. But unless you include the fix in fbf33c1 createK fails with a stack overflow error.

Here's why:

end)
end
else
rawset(obj,'super',nil)
Copy link
Contributor Author

@mcclure mcclure Aug 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

… The problem in the createK test is that when K() invokes call_ctor, it sets the "super" field to do a call_ctor(H._init).
call_ctor(H._init) immediately reaches the if parent_with_init then branch. It does not take it-- correctly-- because H has no superclass with an _init.
However, because the if parent_with_init then branch is skipped, self.super is unchanged... and it still points to function() call_ctor(H._init) end!
So we go into an infinite loop of call_ctor->super->init->call_ctor->super->init.
The only fix for this I can figure (I tried a few things) is to add an else clause here that rawsets self.super to nil.
That way, K._init's self.super is cleaned up before H._init is called, and the createK() test fails with "super is null" as desired.

[EDIT: This comment is obsolete, I came up with a better solution, see comment below]
However: I don't like this fix.

The problem is that this adds a rawset() to every object construction (a superfluous one, since we also rawset 'super' to nil after calling init!), but it only helps in a very specific error case (you wrote an _init that calls super() where it shouldn't and then you inherited THAT _init and invoked it via super()-- this is the only path that misbehaves). And the "fix" only results in getting a different kind of error. So this is taking on a global performance cost for better error reporting in an edge case.

If this were my project, I would throw away fbf33c1 and merge 84ad32b. I don't think the createK "fix" is worth the inefficiency.

The pain of this though is I think in LuaJIT on some platforms, stack overflows might result in a crash instead of a nice perror-catchable error.
(I have not run the createK() test in luajit because run.lua requires LuaFileSystem and I have not yet worked out how to get LuaRocks set up with command-line luajit.)

Okay I guess I just typed a lot about this but it is kind of a nuanced issue :(

[There was another paragraph here but I deleted it in the morning after doing some more testing.]

@mcclure
Copy link
Contributor Author

mcclure commented Aug 28, 2020

One last note: I don't think that the super() stack overflow problem (the "nasty edge case") should be considered an impediment to merging this patch, for a simple reason: The stack overflow is present even without introducing this patch. If I take the test-class.lua from this patch and test it in 59dc58a, I can reproduce the stack overflow. I have to make a slight change to test-class.lua: I have to delete I = class(H) and change J's definition to J = class(H) (because without this PR, grandparent inheritance doesn't work, so without the change we are not testing the same thing). With that tweak, constructing J() enters an infinite loop in the pre-PR Penlight just as it does with the PR.

[EDIT: This comment is obsolete, I came up with a better solution, see comment below]
So basically, 84ad32b fixes the grandparent bug without introducing any new bugs, and you have the option of additionally merging fbf33c1 which fixes a second pre-existing bug in exchange for a small inefficiency.

@mcclure
Copy link
Contributor Author

mcclure commented Aug 28, 2020

Okay… I apologize for the many posts :P

I think d38454b is my final submission. It avoids the second, duplicate rawset on each invocation, at the cost of an additional branch on each invocation (using parent_with_init, which is conveniently already a true value iff the second rawset is needed). I expect (but have not benchmarked) that the branch is less costly than the second rawset.

To recap, your choices are to merge 84ad32b, in which case you fix the grandparent bug with what I expect will be only a performance improvement; or merge d38454b ,in which case you additionally fix the pre-existing bad-:super bug (ie you replace a recursion loop, stack overflow and potential crash on LuaJIT, with a nice "null function" error) in exchange for one additional branch on every object construction.

@mcclure
Copy link
Contributor Author

mcclure commented Sep 10, 2020

Hi, any updates on this? I'm sorry if I made this complicated, but I think either of 84ad32b or d38454b would be an objective improvement over the current code (and the code coverage bot is no longer complaining! 🙂).

@Tieske Tieske merged commit d95c4d2 into lunarmodules:master Sep 22, 2020
@Tieske
Copy link
Member

Tieske commented Sep 22, 2020

thx @mcclure !

Tieske added a commit to Kong/kong that referenced this pull request Sep 22, 2020
  - `compat.wrap` a `coroutine.wrap` implementation that works like plain Lua
    on OpenResty. (#342)

  - In `pl.class`, `_init` can now be inherited from grandparent (or older ancestor) classes. [#289](lunarmodules/Penlight#289)
  - Using `compat.wrap` it now fixes `dir.dirtree`, `pl.lexer` on OpenResty. [#342](lunarmodules/Penlight#342)
bungle pushed a commit to Kong/kong that referenced this pull request Sep 23, 2020
  - `compat.wrap` a `coroutine.wrap` implementation that works like plain Lua
    on OpenResty. (#342)

  - In `pl.class`, `_init` can now be inherited from grandparent (or older ancestor) classes. [#289](lunarmodules/Penlight#289)
  - Using `compat.wrap` it now fixes `dir.dirtree`, `pl.lexer` on OpenResty. [#342](lunarmodules/Penlight#342)
netbsd-srcmastr pushed a commit to NetBSD/pkgsrc that referenced this pull request Nov 23, 2020
## 1.9.1 (2020-09-27)

 - fix: dir.walk [#350](lunarmodules/Penlight#350)


## 1.9.1 (2020-09-24)

 - released to superseed the 1.9.0 version which was retagged in git after some
   distro's already had picked it up. This version is identical to 1.8.1.

## 1.8.1 (2020-09-24) (replacing a briefly released but broken 1.9.0 version)

## Fixes

  - In `pl.class`, `_init` can now be inherited from grandparent (or older ancestor) classes. [#289](lunarmodules/Penlight#289)
  - Fixes `dir`, `lexer`, and `permute` to no longer use coroutines. [#344](lunarmodules/Penlight#344)

## 1.8.0 (2020-08-05)

### New features

  - `pretty.debug` quickly dumps a set of values to stdout for debug purposes

### Changes

  - `pretty.write`: now also sorts non-string keys [#319](lunarmodules/Penlight#319)
  - `stringx.count` has an extra option to allow overlapping matches
    [#326](lunarmodules/Penlight#326)
  - added an extra changelog entry for `types.is_empty` on the 1.6.0 changelog, due
    to additional fixed behaviour not called out appropriately [#313](lunarmodules/Penlight#313)
  - `path.packagepath` now returns a proper error message with names tried if
    it fails

### Fixes

  - Fix: `stringx.rfind` now properly works with overlapping matches
    [#314](lunarmodules/Penlight#314)
  - Fix: `package.searchpath` (in module `pl.compat`)
    [#328](lunarmodules/Penlight#328)
  - Fix: `path.isabs` now reports drive + relative-path as `false`, eg. "c:some/path" (Windows only)
  - Fix: OpenResty coroutines, used by `dir.dirtree`, `pl.lexer`, `pl.permute`. If
    available the original coroutine functions are now used [#329](lunarmodules/Penlight#329)
  - Fix: in `pl.strict` also predefine global `_PROMPT2`
  - Fix: in `pl.strict` apply `tostring` to the given name, in case it is not a string.
  - Fix: the lexer would not recognize numbers without leading zero; "-.123".
    See [#315](lunarmodules/Penlight#315)
netbsd-srcmastr pushed a commit to NetBSD/pkgsrc that referenced this pull request May 13, 2021
## 1.10.0 (2021-04-27)

 - deprecate: `permute.iter`, renamed to `permute.order_iter` (removal later)
   [#360](lunarmodules/Penlight#360)
 - deprecate: `permute.table`, renamed to `permute.order_table` (removal later)
   [#360](lunarmodules/Penlight#360)
 - deprecate: `Date` module (removal later)
   [#367](lunarmodules/Penlight#367)
 - feat: `permute.list_iter` to iterate over different sets of values
   [#360](lunarmodules/Penlight#360)
 - feat: `permute.list_table` generate table with different sets of values
   [#360](lunarmodules/Penlight#360)
 - feat: Lua 5.4 'warn' compatibility function
   [#366](lunarmodules/Penlight#366)
 - feat: deprecation functionality `utils.raise_deprecation`
   [#361](lunarmodules/Penlight#361)
 - feat: `utils.splitv` now takes same args as `split`
   [#373](lunarmodules/Penlight#373)
 - fix: `dir.rmtree` failed to remove symlinks to directories
   [#365](lunarmodules/Penlight#365)
 - fix: `pretty.write` could error out on failing metamethods (Lua 5.3+)
   [#368](lunarmodules/Penlight#368)
 - fix: `app.parse` now correctly parses values containing '=' or ':'
   [#373](lunarmodules/Penlight#373)
 - fix: `dir.makepath` failed to create top-level directories
   [#372](lunarmodules/Penlight#372)
 - overhaul: `array2d` module was updated, got additional tests and several
   documentation updates
   [#377](lunarmodules/Penlight#377)
 - feat: `aray2d` now accepts negative indices
 - feat: `array2d.row` added to align with `column`
 - fix: bad error message in `array2d.map`
 - fix: `array2d.flatten` now ensures to deliver a 'square' result if `nil` is
   encountered
 - feat: `array2d.transpose` added
 - feat: `array2d.swap_rows` and `array2d.swap_cols` now return the array
 - fix: `aray2d.range` correctly recognizes `R` column in spreadsheet format, was
   mistaken for `R1C1` format.
 - fix: `aray2d.range` correctly recognizes 2 char column in spreadsheet format
 - feat: `array2d.default_range` added (previously private)
 - feat: `array2d.set` if used with a function now passes `i,j` to the function
   in line with the `new` implementation.
 - fix: `array2d.iter` didn't properly iterate the indices
   [#376](lunarmodules/Penlight#376)
 - feat: `array2d.columns` now returns a second value; the column index
 - feat: `array2d.rows` added to be in line with `columns`


## 1.9.2 (2020-09-27)

 - fix: dir.walk [#350](lunarmodules/Penlight#350)


## 1.9.1 (2020-09-24)

 - released to superseed the 1.9.0 version which was retagged in git after some
   distro's already had picked it up. This version is identical to 1.8.1.

## 1.8.1 (2020-09-24) (replacing a briefly released but broken 1.9.0 version)

## Fixes

  - In `pl.class`, `_init` can now be inherited from grandparent (or older ancestor) classes. [#289](lunarmodules/Penlight#289)
  - Fixes `dir`, `lexer`, and `permute` to no longer use coroutines. [#344](lunarmodules/Penlight#344)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants