Skip to content

Eliminate built type class dicts when necessarily empty#3416

Merged
hdgarrood merged 1 commit intopurescript:masterfrom
LiamGoodacre:feature/elim-empty-dicts
Jul 16, 2019
Merged

Eliminate built type class dicts when necessarily empty#3416
hdgarrood merged 1 commit intopurescript:masterfrom
LiamGoodacre:feature/elim-empty-dicts

Conversation

@LiamGoodacre
Copy link
Copy Markdown
Member

@LiamGoodacre LiamGoodacre commented Aug 21, 2018

Had a quick go at this. I want to switch to undefined instead of {}, but so far it seems to work. I want to test it on a lot more dictionary heavy code, though.
Fixes #2768

@LiamGoodacre
Copy link
Copy Markdown
Member Author

(Also bumped bower version in the test support code - it was failing to install dependencies)

@LiamGoodacre
Copy link
Copy Markdown
Member Author

LiamGoodacre commented Aug 22, 2018

Input:

data P t = P
class IsUnit unit | -> unit
instance isUnitInst :: IsUnit Unit

class IsUnit2 unit | -> unit
instance isUnitInst2 :: IsUnit unit => IsUnit2 unit

foo :: P _
foo = P :: forall unit . IsUnit unit => P unit

bar :: P _
bar = P :: forall unit . IsUnit2 unit => P unit

Previous output:

var foo = (function (dictIsUnit) { return P.value; })(isUnitInst);
var bar = (function (dictIsUnit2) { return P.value; })(isUnitInst2(isUnitInst));

Current progress:

var foo = (function (dictIsUnit) { return P.value; })({});
var bar = (function (dictIsUnit2) { return P.value; })({});

Goal:

var foo = P.value;
var bar = P.value;

@LiamGoodacre
Copy link
Copy Markdown
Member Author

LiamGoodacre commented Aug 22, 2018

Switched to undefined. For the above example it now produces:

var foo = P.value;
var bar = P.value;

@LiamGoodacre
Copy link
Copy Markdown
Member Author

I'm happy with this. Wasn't too keen on having to change externs for tracking type class emptiness, but I'm okay with it. 😄

mkNewClass :: Environment -> TypeClassData
mkNewClass env = makeTypeClassData args classMembers implies dependencies ctIsEmpty
where
ctIsEmpty = null classMembers && all (typeClassIsEmpty . findSuperClass) implies
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If there is a superclass, is it possible that we try to solve a constraint for the superclass when only an instance for the subclass is in scope, and then by looking up the superclass as a property on the subclass dictionary, we run into a problem at runtime?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess you might need a chain of two or more superclasses for it to become an issue though, otherwise you'll just get undefined anyway.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If there is a non-empty super class, then this class isn't considered empty. So there shouldn't be any issues with looking up super classes, as I don't mark those ones as unused.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it would be worth adding a test for this though (and maybe a few other cases) just to make sure we don't break that assumption in future. To be clear, the reasoning is that even if the subclass and superclass are empty, we'll never look up the superclass dictionary at runtime because it would be empty if we did. Right?

Copy link
Copy Markdown
Member Author

@LiamGoodacre LiamGoodacre Aug 22, 2018

Choose a reason for hiding this comment

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

You're right, I need to think more about super class lookup. I think the fix might just be moving the Unused hint slightly further up the dictionary expression. Thanks!

@LiamGoodacre
Copy link
Copy Markdown
Member Author

LiamGoodacre commented Aug 22, 2018

Pushed a fix for the super class lookup, but haven't properly tested yet.

@LiamGoodacre
Copy link
Copy Markdown
Member Author

With the latest code....

Example:

class WithArgEmpty t
instance withArgEmptyCheck :: WithArgEmpty Check
class WithArgEmpty t <= WithArgHasEmptySuper t where
  withArgHasEmptySuper :: t
instance withArgHasEmptySuperCheck :: WithArgHasEmptySuper Check where
  withArgHasEmptySuper = Check
whenAccessingSuperDict :: Check
whenAccessingSuperDict = foo Check where
  bar :: forall t . WithArgEmpty t => t -> t
  bar x = x
  foo :: forall t . WithArgHasEmptySuper t => t -> t
  foo x = bar x

Before:

var withArgEmptyCheck = WithArgEmpty;
var withArgHasEmptySuperCheck = new WithArgHasEmptySuper(function () {
    return withArgEmptyCheck;
}, Check.value);
var whenAccessingSuperDict = (function () {
    var bar = function (dictWithArgEmpty) {
        return function (x) {
            return x;
        };
    };
    var foo = function (dictWithArgHasEmptySuper) {
        return function (x) {
            return bar(dictWithArgHasEmptySuper.WithArgEmpty0())(x);
        };
    };
    return foo(withArgHasEmptySuperCheck)(Check.value);
})();

After:

var withArgEmptyCheck = WithArgEmpty;
var withArgHasEmptySuperCheck = new WithArgHasEmptySuper(function () {
    return undefined;
}, Check.value);
var whenAccessingSuperDict = (function () {
    var bar = function (dictWithArgEmpty) {
        return function (x) {
            return x;
        };
    };
    var foo = function (dictWithArgHasEmptySuper) {
        return function (x) {
            return bar()(x);
        };
    };
    return foo(withArgHasEmptySuperCheck)(Check.value);
})();

That is, the call to bar in foo no-longer bothers to look up the super class dict. But even if it did, it would be undefined anyway.

@LiamGoodacre
Copy link
Copy Markdown
Member Author

I think this is ready to look at again. I've also added a test.

@garyb
Copy link
Copy Markdown
Member

garyb commented Aug 23, 2018

This is pretty exciting! My silence on it is not due to lack of enthusiasm, just lack of time 😄

@doolse
Copy link
Copy Markdown

doolse commented Sep 26, 2018

Really looking forward to this being released! 👍
It inspired me to create purescript-tscompat because the idea of zero runtime overhead typeclasses was too enticing.

{ typeClassDependencies
, typeClassIsEmpty
} <- case M.lookup className' classesInScope of
Nothing -> throwError . errorMessage $ UnknownClass className'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this a sign for a programming error in the compiler, or an actual user facing error?

In the first case I'd prefer an internalError, and in the second case this really ought to have a source span (unless this is wrapped somewhere and gets a source span)

Copy link
Copy Markdown
Member Author

@LiamGoodacre LiamGoodacre Nov 25, 2018

Choose a reason for hiding this comment

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

It's the first case, I believe. Will change to internalError, thanks!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Actually, turns out, it's not the first case. 😅

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The errors appear to be propagated through typesOf and into the type checker; where source spans get added and the errors re-thrown.

Copy link
Copy Markdown
Member

@garyb garyb left a comment

Choose a reason for hiding this comment

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

Woo!

@hdgarrood
Copy link
Copy Markdown
Contributor

Apart from the conflicts (which are very minor), is there anything getting in the way of merging this?

@garyb
Copy link
Copy Markdown
Member

garyb commented Jan 7, 2019

Not as far as I'm concerned, I forgot it wasn't merged already actually!

@LiamGoodacre
Copy link
Copy Markdown
Member Author

@hdgarrood, I don't think so. I'll try to get this updated tonight.

@LiamGoodacre
Copy link
Copy Markdown
Member Author

After rebasing, it seems to get stuck on the PartialTCO.purs test. Haven't yet had time to look into why though. Will push here and tag with 'needs more work'.

@natefaubion
Copy link
Copy Markdown
Contributor

natefaubion commented Jan 8, 2019

Here is a comparison of the output of master vs this branch:
https://gist.github.com/natefaubion/fa442e3c075bebf82f6026776bf16733

Specifically lines 20-22. It appears to be writing to the wrong variable.

@justinwoo justinwoo mentioned this pull request Jan 12, 2019
3 tasks
@reactormonk
Copy link
Copy Markdown

All tests on our work projects run just fine.

@hdgarrood
Copy link
Copy Markdown
Contributor

I've just tested this on our code at work and it seems to work fine for me too. Shall we try to get this merged? @LiamGoodacre has the approach changed much since this was last reviewed, or is there anything in particular you think wants looking at? Since this passed review already I'm tempted to just merge it.

@LiamGoodacre
Copy link
Copy Markdown
Member Author

@hdgarrood I'm happy to have this merged now 👍

@hdgarrood hdgarrood merged commit 063aeb9 into purescript:master Jul 16, 2019
@hdgarrood
Copy link
Copy Markdown
Contributor

Nice, thanks! :)

@LiamGoodacre LiamGoodacre deleted the feature/elim-empty-dicts branch July 16, 2019 19:18
matthew-hilty pushed a commit to matthew-hilty/purescript that referenced this pull request Aug 2, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove runtime overhead of typeclass instances with no dictionary

8 participants