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

WIP: sketch out a reference API #417

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

dcoutts
Copy link
Collaborator

@dcoutts dcoutts commented Oct 4, 2024

As opposed to a referencing counting API.

@dcoutts dcoutts force-pushed the dcoutts/refcount-vs-reference branch from d01ed4e to 30aa13a Compare October 28, 2024 10:11
The focus is on a Ref to an object not an object with a RefCounter.
Each Ref must be released exactly once. This is easier to dynamically
check in debug builds, compared to the existing API which simply
increments and decrements reference counts.

This commit just adds the new API, leaving the existing API as well.
@dcoutts dcoutts force-pushed the dcoutts/refcount-vs-reference branch from 30aa13a to 0f240ba Compare October 28, 2024 10:13
Copy link
Collaborator

@jorisdral jorisdral left a comment

Choose a reason for hiding this comment

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

The following are just a few comments that came up while scanning the PR. I'll let @mheinzel and you discuss the details of the approach, if that's okay

Overall, I think the sketch could benefit from a small example (with instances for HasFinaliser and HasSharedFinaliser), so that it is clear how all the moving parts interact

Comment on lines +313 to +315
assertNotReleased released
unsharedFinaliser obj
unsafeIOToPrim $ writeIORef released True
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if we should care about atomicity/exception safety here, and in other places in the module. Is it fine to do a simple check beforehand, or should we check that it is not released during the run of the finaliser? If an async exception happens, is it fine if we have run the finaliser but haven't updated the IORef yet?

Regardless of what approach we pick, we should probably comment on the guarantees somewhere in this module

unsafeDeRef (Ref obj) = obj
#else
unsafeDeRef (Ref obj released) =
unsafeDupablePerformIO (assertNotReleased released)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you explain why we use the dupable variant here? Just asking because I'm not too familiar with when to use which unsafePerformIO variant

#endif

-- | If the object is still alive, obtain a /new/ normal reference. The normal
-- rules for 'Ref' apply, including the need to eventually calling 'releaseRef'.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
-- rules for 'Ref' apply, including the need to eventually calling 'releaseRef'.
-- rules for 'Ref' apply, including the need to eventually call 'releaseRef'.

Copy link
Collaborator

@mheinzel mheinzel left a comment

Choose a reason for hiding this comment

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

Looks nice!

The debugging/assertion functionality could still be improved, I think. Would an error from assertNotReleased currently tell you much about which (type of) resource wasn't released? Also, we don't find out yet if some Ref just never gets closed, right? For that we'd need some global state or rely on GHC finalisers, as we discussed at some point.

The API seems sensible, though, which I think is the main point of the PR.

I also left some comments on minor details.

#endif

#ifdef NO_IGNORE_ASSERTS
assertNotReleased :: PrimMonad m => IORef Bool -> m ()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we want a HasCallStack constraint here?

--
class HasFinaliser obj where
type FinaliserM obj :: Type -> Type
unsharedFinaliser :: FinaliserM obj ~ m => obj -> m ()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Was there some complication that led you to using a type family and equality constraints, as opposed to HasFinaliser m obj, potentially with a functional dependency?

Comment on lines +321 to +328
#ifndef NO_IGNORE_ASSERTS
withRef :: Ref obj -> (obj -> m a) -> m a
withRef (Ref obj) f = f obj
#else
withRef :: PrimMonad m => Ref obj -> (obj -> m a) -> m a
withRef (Ref obj released) f = assertNotReleased released
>> f obj
#endif
Copy link
Collaborator

Choose a reason for hiding this comment

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

It might be nicer to have the same type signature in both cases, so we can't run into unused constraint warnings at the usage sites that only show up in non-debug builds. This means we'd have to deal with the redundant constraint issue here, but it's probably the right place.

Comment on lines +354 to +355
released' <- unsafeIOToPrim $ newIORef False
return (Ref obj released')
Copy link
Collaborator

Choose a reason for hiding this comment

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

This part is just newRef, which even already does the CPP, so potentially only assertNotReleased would need to be conditional (but only CPP-ing half of the function might not make the code clearer).

Comment on lines +382 to +394
#ifndef NO_IGNORE_ASSERTS
deRefWeak (WeakRef obj) = do
success <- tryIncrementRefCounter (getRefCounter obj)
if success then return (Just (Ref obj))
else return Nothing
#else
deRefWeak (WeakRef obj) = do
success <- tryIncrementRefCounter (getRefCounter obj)
if success
then do released <- unsafeIOToPrim $ newIORef False
return (Just (Ref obj released))
else return Nothing
#endif
Copy link
Collaborator

Choose a reason for hiding this comment

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

This CPP could all go away using newRef, though, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants