[Git][ghc/ghc][wip/io-manager-deadlock-detection] 9 commits: Add a long Note [Deadlock detection]
by Duncan Coutts (@dcoutts) 25 Sep '26
by Duncan Coutts (@dcoutts) 25 Sep '26
25 Sep '26
Duncan Coutts pushed to branch wip/io-manager-deadlock-detection at Glasgow Haskell Compiler / GHC
Commits:
78bd27ee by Duncan Coutts at 2026-09-25T00:36:44+01:00
Add a long Note [Deadlock detection]
It describes the historical and modern designs and their trade-offs.
The point is we've now unified the code for deadlock detection between
the threaded and non-threaded ways, by changing the non-threaded to
follow the same design as the threaded.
- - - - -
d2fd72bc by Duncan Coutts at 2026-09-25T00:36:44+01:00
Refactor the utils providing the I/O timeout for different system APIs
We have to provide a timeout in three different time formats (int
milliseconds, struct timeval *, and struct timespec *). These are for
platform APIs including poll, select, ppoll, pselect, epoll_wait,
epoll_wait2 and in future kevent and io_uring.
Previously we had one util for each format that both worked out what
the timeout should be and converted it into the particular format. This
duplicates some logic and it also makes it awkward to modify the timeout
value (since there's no common format).
Modifying the timeout value is exactly what we want to do, in a
subsequent commit, where we want to adjust the timeout to take account
of idle GC.
- - - - -
6852893f by Duncan Coutts at 2026-09-25T00:36:44+01:00
Update the old select I/O manager to use the new timeout utils
This will make it much easier to adjust the timeout to take account of
idle GC.
- - - - -
b7396c21 by Duncan Coutts at 2026-09-25T12:11:25+01:00
Add opportunistic deadlock detection without idle GC
Our general design is now that comprehensive deadlock detection relies
on idle GC. This allows us to detect partial deadlocks that are not
system deadlocks. But it's not prompt.
It's a nice to have feature to be able to promptly detect the cases that
are system deadlocks. There's no hope to do this in the threaded RTS
with -N > 1, but it can be done for the non-threaded RTS and _could_ be
done for the threaded RTS with -N1.
We can integrate this feature into the general design without too many
contortions. We just need the I/O managers to check before sleeping if
they would be about to sleep indefinately with no I/O and no registered
signal handlers. If so, they can notify the idle GC that we're
deadlocked and we schedule and idle GC.
- - - - -
a5374fcb by Duncan Coutts at 2026-09-25T12:11:25+01:00
Add idle GC mode for !HAVE_PREEMPTION
We have to do idle GC (and thus deadlock detection) differently when we
do not have the ticker and the ability to interrupt the I/O manager
when it's blocking.
- - - - -
e4b06281 by Duncan Coutts at 2026-09-25T12:11:25+01:00
Add and use USED_IF_PREEMPTION to avoid warnings
- - - - -
d9aa5a88 by Duncan Coutts at 2026-09-25T12:11:26+01:00
Add performDeadlockDetection and export via ghc-experimental
When documenting the idle GC and deadlock detection behaviour in the
user guide, it becomes obiovus that in some cases one would want manual
control over deadlock detection.
In particular, for applications that disable idle GC, for debugging or
robustness, they may want to invoke deadlock detecting GC manually.
Bind it in ghc-internal GHC.Internal.System.Mem and re-export via
ghc-experimental in System.Mem.Experimental. It is clearly
implementation specific behaviour, especially since in GHC the deadlock
detection is tied to doing a major GC.
- - - - -
3711b41a by Duncan Coutts at 2026-09-25T12:11:26+01:00
Add tests for deadlock detection, issue #26408
We have three tests:
T26408a: make sure that we can detect a /partial deadlock/ that is not
also a /system deadlock/
T26408b: make sure that we can detect a /system deadlock/ promptly
without relying on idle GC
T26408c: make sure that we do not falsely detect a deadlock when there is
nothing to do except wait on signals
- - - - -
f3468063 by Duncan Coutts at 2026-09-25T12:11:26+01:00
Update the user guide with the revised idle GC behaviour
It's now not just for the threaded RTS, but general.
Add a new section in the RTS chapter: Idle GC and deadlock detection.
Provide general documentation on both topics. In particular, document
the behaviour of deadlock detection and how it depends on idle GC.
Move the existing entries for -I and -Iw to this section.
Add a changelog entry.
- - - - -
29 changed files:
- + changelog.d/idle-gc-and-deadlock-detection
- docs/users_guide/profiling.rst
- docs/users_guide/runtime_control.rst
- libraries/ghc-experimental/src/System/Mem/Experimental.hs
- libraries/ghc-internal/src/GHC/Internal/System/Mem.hs
- rts/IdleGC.c
- rts/IdleGC.h
- rts/RtsFlags.c
- rts/RtsSymbols.c
- rts/Schedule.c
- rts/Timer.c
- rts/include/rts/OSThreads.h
- rts/include/rts/storage/GC.h
- rts/posix/Poll.c
- rts/posix/Select.c
- rts/posix/SelectBis.c
- rts/posix/Timeout.c
- rts/posix/Timeout.h
- rts/win32/AsyncMIO.c
- testsuite/tests/interface-stability/ghc-experimental-exports.stdout
- testsuite/tests/interface-stability/ghc-experimental-exports.stdout-mingw32
- testsuite/tests/rts/Makefile
- + testsuite/tests/rts/T26408a.hs
- + testsuite/tests/rts/T26408a.stderr
- + testsuite/tests/rts/T26408b.hs
- + testsuite/tests/rts/T26408b.stderr
- + testsuite/tests/rts/T26408c.hs
- + testsuite/tests/rts/T26408c.stdout
- testsuite/tests/rts/all.T
Changes:
=====================================
changelog.d/idle-gc-and-deadlock-detection
=====================================
@@ -0,0 +1,22 @@
+section: rts
+issues: #27040 #26408
+mrs: !14820
+synopsis: Idle GC and deadlock detection in the non-threaded RTS now behave the same as in the threaded RTS
+description: {
+Idle GC now works in the non-threaded RTS and is enabled by default.
+
+Deadlock detection in the non-threaded RTS now follows the same design and
+behaviour as in the threaded RTS. In particular, it can detect partial
+deadlocks where some but not all threads are deadlocked. The detection is no
+longer prompt however, as the detection runs as part of idle GC, which (by
+default) is performed 0.3 seconds after all capabilities become idle.
+
+The user guide now documents that deadlock detection relies on idle GC,
+and thus turning off idle GC disables deadlock detection. This is true
+of the threaded RTS in previous GHC releases but it was not previously
+documented behaviour.
+
+Internally, the idle GC and deadlock detection now follow the same design and
+use 99% the same code between the non-threaded and threaded RTS. Having a
+single design improves the comprehension and maintainability of the RTS code.
+}
=====================================
docs/users_guide/profiling.rst
=====================================
@@ -691,11 +691,13 @@ enclosed between ``+RTS ... -RTS`` as usual):
order to increase the resolution of the time profiler.
Using a value of zero disables the RTS clock completely, and has the
- effect of disabling timers that depend on it: the context switch
- timer and the heap profiling timer. Context switches will still
- happen, but deterministically and at a rate much faster than normal.
- Disabling the interval timer is useful for debugging, because it
- eliminates a source of non-determinism at runtime.
+ effect of disabling the timers and features that depend on it: the context
+ switch timer, the heap profiling timer, the eventlog flush timer, and the
+ idle GC timer. Context switches will still happen, but deterministically
+ and at a rate much faster than normal. Disabling the interval timer is
+ useful for debugging, because it eliminates a source of non-determinism at
+ runtime. Note however that disabling the idle GC timer not only disables
+ idle GC but also deadlock detection (see rts-flag:`-I`).
.. rts-flag:: -xc
=====================================
docs/users_guide/runtime_control.rst
=====================================
@@ -731,61 +731,6 @@ performance.
the default small ``-A`` value is suboptimal, as it can be in
programs that create large amounts of long-lived data.
-.. rts-flag:: -I ⟨seconds⟩
-
- :default: 0.3 seconds in the threaded runtime, 0 in the non-threaded runtime
-
- .. index::
- single: idle GC
-
- Set the amount of idle time which must pass before a idle GC is
- performed. Setting ``-I0`` disables the idle GC.
-
- In the threaded and SMP versions of the RTS (see :ghc-flag:`-threaded`,
- :ref:`options-linker`), a major GC is automatically performed if the
- runtime has been idle (no Haskell computation has been running) for a
- period of time.
-
- For an interactive application, it is probably a good idea to use
- the idle GC, because this will allow finalizers to run and
- deadlocked threads to be detected in the idle time when no Haskell
- computation is happening. Also, it will mean that a GC is less
- likely to happen when the application is busy, and so responsiveness
- may be improved. However, if the amount of live data in the heap is
- particularly large, then the idle GC can cause a significant delay,
- and too small an interval could adversely affect interactive
- responsiveness.
-
- The idle period timer only resets after some activity
- by a Haskell thread. If your program is doing literally nothing then
- after the first idle collection is triggered then no more future collections
- will be scheduled until more work is performed.
-
- This is an experimental feature, please let us know if it causes
- problems and/or could benefit from further tuning.
-
-.. rts-flag:: -Iw ⟨seconds⟩
-
- :default: 0 seconds
-
- .. index::
- single: idle GC
-
- Set the minimum wait time between runs of the idle GC.
-
- By default, if idle GC is enabled in the threaded runtime, a major
- GC will be performed every time the process goes idle for a
- sufficiently long duration (see :rts-flag:`-I ⟨seconds⟩`). For
- large server processes accepting regular but infrequent requests
- (e.g., once per second), an expensive, major GC may run after
- every request. As an alternative to shutting off idle GC entirely
- (with ``-I0``), a minimum wait time between idle GCs can be
- specified with this flag. For example, ``-Iw60`` will ensure that
- an idle GC runs at most once per minute.
-
- This is an experimental feature, please let us know if it causes
- problems and/or could benefit from further tuning.
-
.. rts-flag:: -ki ⟨size⟩
:default: 1k
@@ -1011,6 +956,116 @@ performance.
``GHC.Stats.getRTSStats`` from Haskell.
+Idle GC and deadlock detection
+------------------------------
+
+The RTS performs idle GC and deadlock detection. These features are more
+closely related and dependent than one might expect. In particular, deadlock
+detection relies on idle GC.
+
+The primary purpose of idle GC is to reclaim memory and return it to the system
+when the application has been idle for a short time. It also gives an
+opportunity for finalisers to run more promptly. See :rts-flag:`-I ⟨seconds⟩`.
+
+Deadlock detection is a debugging feature to try to discover when some or all
+Haskell threads are deadlocked. For the purpose of this explanation we define:
+
+ * a "partial deadlock" to be a set of threads that are deadlocked; and
+ * a "system deadlock" is when all threads are deadlocked.
+
+The deadlock detection is implemented by the GC, and it is capable of detecting
+partial deadlocks. If a deadlock is discovered, the RTS will throw an
+asynchronous exception to (some or all of) the blocked threads to enable
+progress and enable better reporting of the problem. In particular it throws
+the async exceptions ``BlockedIndefinitelyOnMVar`` or
+``BlockedIndefinitelyOnSTM``, as appropriate.
+
+The deadlock detection can be run during a major GC, but it is more expensive
+than a normal major GC so it is not run during normal major GCs. The deadlock
+detection mode of GC is run by the idle GC. This is on the basis that when the
+application is idle is when we are most likely to find a deadlock, and also
+that the extra time cost is unlikely to be a problem because the application is
+already idle.
+
+There are a few consequence of this:
+
+ * Deadlock detection relies on idle GC and will not run if idle GC is disabled
+ with ``-I0``.
+ * Deadlock detection will not run if the application remains busy, and thus
+ idle GC is not invoked.
+ * Deadlock detection is not prompt: it will only occur after the idle GC
+ delay, depending on both :rts-flag:`-I ⟨seconds⟩` and
+ :rts-flag:`-Iw ⟨seconds⟩`.
+
+In the non-threaded RTS there is additional behaviour: system deadlocks will be
+detected promptly, and this works even if idle GC is otherwise disabled.
+
+One cannot rely on deadlock detection as part of the normal program behaviour.
+It should only be used for debugging. Firstly, deadlock detection can be
+disabled or indefinately deferred due to activity. Secondly, the set of threads
+that deadlock detection throws exceptions to is not guaranteed to be minimal:
+a thread waiting on a deadlocked thread may also have an exception thrown to it
+even if it could make progress after the deadlocked thread is terminated.
+
+Deadlock detection can be invoked manually using ``performDeadlockDetection``
+from ``System.Mem.Experimental``.
+
+
+.. rts-flag:: -I ⟨seconds⟩
+
+ :default: 0.3 seconds
+
+ .. index::
+ single: idle GC
+
+ A major GC is automatically performed if the runtime has been idle (no
+ Haskell computation has been running) for a period of time. Set the amount
+ of idle time which must pass before a idle GC is performed.
+
+ Setting ``-I0`` disables the idle GC. This also has the unfortunate side
+ effect of disabling thread deadlock detection (the implementation of which
+ uses the idle GC).
+
+ For an interactive application, it is probably a good idea to use
+ the idle GC, because this will allow finalizers to run and
+ deadlocked threads to be detected in the idle time when no Haskell
+ computation is happening. Also, it will mean that a GC is less
+ likely to happen when the application is busy, and so responsiveness
+ may be improved. However, if the amount of live data in the heap is
+ particularly large, then the idle GC can cause a significant delay,
+ and too small an interval could adversely affect interactive
+ responsiveness.
+
+ The idle period timer only resets after some activity
+ by a Haskell thread. If your program is doing literally nothing then
+ after the first idle collection is triggered then no more future collections
+ will be scheduled until more work is performed.
+
+ Please let us know if it causes problems and/or could benefit from further
+ tuning.
+
+.. rts-flag:: -Iw ⟨seconds⟩
+
+ :default: 0 seconds
+
+ .. index::
+ single: idle GC
+
+ Set the minimum wait time between runs of the idle GC.
+
+ By default (and if idle GC is not disabled) a major
+ GC will be performed every time the process goes idle for a
+ sufficiently long duration (see :rts-flag:`-I ⟨seconds⟩`). For
+ large server processes accepting regular but infrequent requests
+ (e.g., once per second), an expensive, major GC may run after
+ every request. As an alternative to shutting off idle GC entirely
+ (with ``-I0``), a minimum wait time between idle GCs can be
+ specified with this flag. For example, ``-Iw60`` will ensure that
+ an idle GC runs at most once per minute.
+
+ This is an experimental feature, please let us know if it causes
+ problems and/or could benefit from further tuning.
+
.. _rts-options-statistics:
=====================================
libraries/ghc-experimental/src/System/Mem/Experimental.hs
=====================================
@@ -1,5 +1,10 @@
module System.Mem.Experimental
- ( setGlobalAllocationLimitHandler
+ (
+ -- * Garbage collection
+ performDeadlockDetection
+
+ -- * Allocation counter and limits
+ , setGlobalAllocationLimitHandler
, AllocationLimitKillBehaviour(..)
, getAllocationCounterFor
, setAllocationCounterFor
@@ -8,3 +13,4 @@ module System.Mem.Experimental
)
where
import GHC.Internal.AllocationLimitHandler
+import GHC.Internal.System.Mem
=====================================
libraries/ghc-internal/src/GHC/Internal/System/Mem.hs
=====================================
@@ -19,9 +19,10 @@ module GHC.Internal.System.Mem
(
-- * Garbage collection
performGC
+ , performMinorGC
, performMajorGC
, performBlockingMajorGC
- , performMinorGC
+ , performDeadlockDetection
-- * Allocation counter and limits
, setAllocationCounter
@@ -52,3 +53,16 @@ foreign import ccall "performBlockingMajorGC" performBlockingMajorGC :: IO ()
--
-- @since base-4.7.0.0
foreign import ccall "performGC" performMinorGC :: IO ()
+
+-- | Triggers an immediate major garbage collection in deadlock detection mode.
+-- This will detect any threads that are deadlocked and throw asynchronous
+-- exceptions to them.
+--
+-- Normally, deadlock detection is done automatically and it is rarely useful
+-- to invoke manually. However, since deadlock detection in GHC is performed
+-- by idle GC, which some applications disable, then it may be useful to
+-- invoke deadlock detection manually for the purpose of debugging or
+-- resilliance.
+--
+-- @since ghc-internal-10.100.0
+foreign import ccall "performDeadlockDetection" performDeadlockDetection :: IO ()
=====================================
rts/IdleGC.c
=====================================
@@ -54,6 +54,198 @@
automatic GCs. Both timers must expire before an automatic GC is triggered.
See issue #11134 for additional detail.
+
+ Historically, the non-threaded runtime did not perform idle GC at all, and
+ thus implemented deadlock detection differently. See Note [Deadlock detection]
+ for the current and historical designs.
+
+ The design for idle GC is (now) the same for the threaded and non-threaded
+ RTS ways, with the only difference being in how wakeUpRts works. However, in
+ the non-threaded RTS mode without preemption there is a design difference in
+ how to schedule idle GC. See Note [Idle GC without preemption].
+
+
+Note [Idle GC without preemption]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+On non-threaded platforms without pre-emption, e.g. wasm, we have to use a
+different design for scheduling idle GC (and thus deadlock detection, see
+Note [Deadlock detection]).
+
+Instead of a ticker thread that interrupts the idle capability after some delay,
+we arrange for the I/O manager to only sleep for a maximum of the idle GC delay
+time. The function getNextIdleGcDelayTime() returns this time, accounting for
+the interIdleGCWait.
+
+Then if that timeout is reached the I/O manager calls notifyIdleGcIdle(),
+which schedules an idle GC (by seting the recent activity state). The I/O
+manager must then behave as if it had been interrupted (as happens with the
+threaded idle GC design).
+
+See adjustTimeoutForIdleGc() and handleIdleGcTimeout(), which are the helper
+functions used by the in-RTS I/O managers on posix platforms to implement this.
+
+See Note [GC During Idle Time] for the general design.
+
+
+Note [Deadlock detection]
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For the purpose of this explanation we define:
+ * a /partial deadlock/ to be a set of threads that are deadlocked; and
+ * a /system deadlock/ is when all threads are deadlocked.
+
+Obviously, we can have a partial deadlock without having a system
+deadlock. The design goal of deadlock detection is to guarantee to
+detect (and resolve) system deadlock, but to also try to detect (and
+resolve) partial deadlocks.
+
+There are two designs that the RTS has used for deadlock detection: a
+simple historical design originally used in the non-threaded RTS and a
+modern design for the threaded RTS. These days we use the modern design
+in both the threaded and non-threaded RTS.
+
+A high level way to think about the two designs is as follows:
+ 1. the historical design looks for situations in which there *must* be
+ a system deadlock; whereas
+ 2. the modern design looks for partial deadlocks opportunistically,
+ with the guarantee that if the overall system is deadlocked that we
+ will *eventually* detect this.
+
+An advantage of the historical design is that it will detect system
+deadlock promptly. A disadvantage is that it will never detect a
+partial deadlock (that isn't also a system deadlock).
+
+The modern design can detect partial deadlock, but it is not guaranteed
+to detect system deadlock promptly, just eventually.
+
+The mechanism for deadlock detection is garbage collection. GC can be
+instructed to look for deadlocked threads and if it finds them to throw
+exceptions to one or more threads involved in the deadlock. This
+mechanism can find partial deadlocks. It is however expensive -- more
+expensive than a normal major GC. So the difference in the historical
+and modern designs is in when we do this expensive GC check.
+
+The historical design
+---------------------
+
+When there was just one capability, as in the single threaded RTS, it
+is possible to follow a very simple design. When there are no runnable
+threads, and no threads blocked on pending I/O or on timers then there
+*must* be a deadlock. And thus running deadlock detection promptly in
+this situation is guaranteed to find the deadlock and wake up one or
+more threads. Thus we can guarantee afterwards that there are runnable
+threads.
+
+There are a couple problems with this design, but the biggest problem
+is that it cannot be extended to multiple capabilities. When there are
+multiple capabilities then the fact that there are no runnable threads
+on the current capability says nothing about runnable threads on other
+capabilities. Runnable threads elsewhere might wake up threads on this
+capability, and so there is no implication that there is a deadlock.
+
+The other problems with this design are:
+ 1. it cannot find genuine deadlocks when there are any unrelated
+ threads blocked on I/O or timers (see issue #26408); and
+ 2. it requires treating signals specially.
+
+The problem with signals is that they're a weird kind of I/O. Threads
+do not block waiting on signals. Rather signals can have handlers such
+that when a signal arrives, a new thread is started to execute the
+handler. This means it doesn't neatly fit into the condition "no
+threads blocked on pending I/O or on timers". And if we did shoehorn it
+into that definition then we would not look for deadlocks if there were
+any signal handlers registered, and we would still end up with no
+runnable threads after skipping deadlock detection, which violates the
+post-condition that there be runnable threads. So the solution was that
+after deadlock detection, if there are still no runnable threads and
+there are registered signal handlers then we conclude we must wait for
+a signal to be received -- which will start a thread and thus we will
+end up with runnable threads. But of course this is horrible: we have
+entangled two features far too tightly: deadlock detection with a weird
+-- and platform specific -- kind of I/O.
+
+The modern design
+-----------------
+
+A change of perspective is required. Instead of thinking of conditions
+in which there must be a deadlock, we simply look for deadlocks in such
+a way in which we will eventually find deadlocks if they exist. A
+benefit of this approach is that we can find deadlocks that the simple
+approach cannot. For example we can find deadlocks when there are
+unrelated threads blocked on I/O or timers (see issue #26408).
+
+The question is when to run GC in its more expensive deadlock detection
+mode. We obviously do not want to do it too frequently. The design
+choice is to do it during idle GC, at least sometimes. Idle GC is only
+run some time after all capabilities go idle. This is a good
+opportunity. We know there are no runnable threads on the capability,
+so there *might* be a deadlock, and when there's nothing else to do is
+also a good moment to do a more expensive GC.
+
+The idle GC is controlled by the RecentActivity status, which
+progresses through 4 stages: yes, maybe_no, inactive, done_gc. We only
+invoke a deadlock-detecting major GC in the inactive state. We get into
+the inactive state when:
+ * the timer tick goes off
+ * we were already in the maybe_no state (which itself requires no
+ activity on any capability for a whole timer tick)
+ * idle GC is enabled
+ * it's been long enough since the most recent idle GC.
+This timer tick also wakes up the I/O manager to ensue we get back to
+the scheduler, and thus to scheduleDetectDeadlock.
+
+Note that this means that deadlock detection is disabled if users
+disable idle GC (by setting +RTS -I0). Historically, idle GC was not
+used by default in the non-threaded RTS, but the modern design relies
+on it, so it is enabled by default in all cases.
+
+But if idle GC is enabled, then if there is a full system deadlock then
+eventually we will run a major GC with deadlock detection and detect
+and resolve the deadlock. It is not prompt. It must wait at least for
+an idle GC, which by default is 0.3s after all capabilities go idle.
+
+Furthermore, there is no post-condition for scheduleDetectDeadlock,
+because of the non-prompt "eventually" nature of the deadlock detection
+design. In particular there can still be no runnable threads. In the
+threaded RTS if there's no runnable threads after this we will yield the
+capability, while in the non-threaded we will ask the I/O manager to
+block and wait for I/O, timers or signals.
+
+See also Note [Deadlock detection under the nonmoving collector].
+
+
+Note [Deadlock detection without idle GC]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In Note [Deadlock detection] above we noted that:
+
+> the historical design looks for situations in which there *must* be
+ a system deadlock.
+
+We can still take advantage of these situations, but instead of basing the
+whole design around it, we use it opportunistically, and in a way that involves
+minimal changes to the general design.
+
+Specifically, in the case of a single capability (so we have a global view), if
+the I/O manager would be about to block indefinitely, with no I/O pending, then
+we have a strong suspicion that there is a system deadlock.
+
+In this case, I/O manager informs the idle GC system via notifyIdleGcDeadlock.
+Typically it will then behave as if the I/O manager had been interrupted and
+return to the scheduler. We can do this promptly, and we can do so even if idle
+GC in general is disabled (e.g. via -I0). This fits into the existing design
+because it can behave just as if there had been an idle GC timeout.
+
+There is one very subtle case to note: we can have situations where there's no
+deadlock detected but yet there are no runnable threads, no timers and no
+pending I/O. There can also be signals with signal handlers that could wake up
+and unblock things (e.g. if a handler closure has access to a TVar/MVar that
+other threads are blocked on). To make this case work, the I/O manager has to
+ask notifyIdleGcDeadlock if it should should return to the scheduler, or
+continue and block. And notifyIdleGcDeadlock will tell it to continue if an
+idle GC has already been completed (with no intervening activity) since this is
+the situation where there is no deadlock. See test T26408c.
*/
@@ -112,6 +304,8 @@ bool isIdleGcPending(void)
return (getRecentActivity() == ACTIVITY_INACTIVE);
}
+#if defined(THREADED_IDLEGC)
+
/* - countdown for minimum idle time before we start a GC (set by -I) */
static int idle_ticks_to_gc = 0;
@@ -169,6 +363,58 @@ void handleIdleGcTick(void)
}
}
+#else // !defined(THREADED_IDLEGC)
+
+static Time last_idle_gc_time = 0;
+
+Time getNextIdleGcDelayTime(void)
+{
+ if (getRecentActivity() == ACTIVITY_DONE_GC) return -1; // indefinite
+
+ Time now = getProcessElapsedTime();
+ Time delay =
+ stg_max (last_idle_gc_time + RtsFlags.GcFlags.interIdleGCWait - now,
+ RtsFlags.GcFlags.idleGCDelayTime);
+ ASSERT(delay >= 0);
+ return delay;
+}
+
+void notifyIdleGcIdle(void)
+{
+ if (RtsFlags.GcFlags.doIdleGC && getRecentActivity() < ACTIVITY_INACTIVE) {
+ setRecentActivity(ACTIVITY_INACTIVE);
+ }
+}
+
+#endif // defined(THREADED_IDLEGC)
+
+/* If the I/O manager can see that there is very likely a deadlock (no I/O and
+ * no timers) then it lets us know. If we have not already done an idle GC then
+ * we schedule one idle GC. If we did already do an idle GC and thus did not
+ * find a deadlock then we'll arrange to continue and block (waiting for signals
+ * only).
+ */
+bool notifyIdleGcDeadlock(void)
+{
+ /* We do this even if RtsFlags.GcFlags.doIdleGC is turned off.
+ *
+ * But if we've already done a GC and didn't detect anything,
+ * i.e. ACTIVITY_DONE_GC, then continue and block. This will
+ * wait for signals that could unwedge things.
+ *
+ * Caution: if we get this wrong we can loop doing deadlock detecting GCs
+ * that find nothing! See test T26408c that checks we get this right.
+ */
+ if (getRecentActivity() < ACTIVITY_INACTIVE) {
+ setRecentActivity(ACTIVITY_INACTIVE);
+ /* Instruct to interrupt, return to scheduler and do idle GC. */
+ return true;
+ } else {
+ /* Instruct not to interrupt, to continue and block on signals. */
+ return false;
+ }
+}
+
void notifyIdleGcActive(void)
{
switch (getRecentActivity())
@@ -208,6 +454,9 @@ void notifyIdleGcDone(bool force_major)
setRecentActivity(ACTIVITY_DONE_GC);
#if !defined(PROFILING)
pauseTimer();
+#endif
+#if !defined(THREADED_IDLEGC)
+ last_idle_gc_time = getProcessElapsedTime();
#endif
break;
}
=====================================
rts/IdleGC.h
=====================================
@@ -12,6 +12,15 @@
#include "BeginPrivate.h"
+/* See Note [GC During Idle Time] */
+
+#if defined(HAVE_PREEMPTION)
+#define THREADED_IDLEGC
+#else
+/* See Note [Idle GC without preemption] */
+#endif
+
+
/* Are we in a state where we are want an idle GC to occur?
* Used in Capability globalWorkToDo() and Schedule scheduleDetectDeadlock.
*/
@@ -23,7 +32,24 @@ void notifyIdleGcActive(void);
/* Called from schedule(), specifically scheduleDoGC() */
void notifyIdleGcDone(bool force_major);
+/* Called from I/O managers before waiting if there is almost certainly
+ * a deadlock.
+ */
+bool notifyIdleGcDeadlock(void);
+
+#if defined(THREADED_IDLEGC)
+
/* Called from handle_tick() */
void handleIdleGcTick(void);
+#else // !defined(THREADED_IDLEGC)
+
+/* Called from I/O managers before waiting */
+Time getNextIdleGcDelayTime(void);
+
+/* Called from I/O managers after waiting */
+void notifyIdleGcIdle(void);
+
+#endif
+
#include "EndPrivate.h"
=====================================
rts/RtsFlags.c
=====================================
@@ -1914,8 +1914,13 @@ static void normaliseRtsOpts (void)
// If the master timer is disabled, turn off the other timers.
if (RtsFlags.MiscFlags.tickInterval == 0) {
RtsFlags.ConcFlags.ctxtSwitchTime = 0;
+#if defined(HAVE_PREEMPTION)
RtsFlags.GcFlags.idleGCDelayTime = 0;
RtsFlags.GcFlags.doIdleGC = false;
+#else
+ /* We use idleGCDelayTime and doIdleGC without timers.
+ * See Note [Idle GC without preemption] */
+#endif
RtsFlags.ProfFlags.heapProfileInterval = 0;
}
@@ -1928,11 +1933,16 @@ static void normaliseRtsOpts (void)
RtsFlags.MiscFlags.tickInterval);
}
+#if defined(HAVE_PREEMPTION)
if (RtsFlags.GcFlags.idleGCDelayTime > 0) {
RtsFlags.MiscFlags.tickInterval =
stg_min(RtsFlags.GcFlags.idleGCDelayTime,
RtsFlags.MiscFlags.tickInterval);
}
+#else
+ /* We use idleGCDelayTime and doIdleGC without timers.
+ * See Note [Idle GC without preemption] */
+#endif
if (RtsFlags.ProfFlags.heapProfileInterval > 0) {
RtsFlags.MiscFlags.tickInterval =
=====================================
rts/RtsSymbols.c
=====================================
@@ -696,6 +696,7 @@ extern char **environ;
SymI_HasProto(performGC) \
SymI_HasProto(performMajorGC) \
SymI_HasProto(performBlockingMajorGC) \
+ SymI_HasProto(performDeadlockDetection) \
SymI_HasProto(prog_argc) \
SymI_HasProto(prog_argv) \
SymI_HasDataProto(stg_putMVarzh) \
=====================================
rts/Schedule.c
=====================================
@@ -298,6 +298,7 @@ schedule (Capability *initialCapability, Task *task)
schedulePushWork(cap,task);
if (emptyRunQueue(cap)) {
+ /* When we have no threads to run, we *might* have a deadlock. */
scheduleDetectDeadlock(&cap,task);
}
@@ -825,18 +826,19 @@ schedulePushWork(Capability *cap USED_IF_THREADS,
}
/* ----------------------------------------------------------------------------
- * Detect deadlock conditions and attempt to resolve them.
+ * Perform idle GC and deadlock detection.
+ *
+ * We don't make any decisions right here, we just invoke GC with deadlock
+ * detection if the idle GC tracking subsystem has decided that we're in
+ * a state where we should (via isIdleGcPending).
+ *
+ * See Note [Deadlock detection].
+ *
* ------------------------------------------------------------------------- */
static void
scheduleDetectDeadlock (Capability **pcap, Task *task)
{
- /*
- * Detect deadlock: when we have no threads to run, there are no
- * threads blocked, waiting for I/O, or sleeping, and all the
- * other tasks are waiting for work, we must have a deadlock of
- * some description.
- */
if (isIdleGcPending()) {
debugTrace(DEBUG_sched, "maybe deadlocked, forcing major GC...");
@@ -2698,7 +2700,7 @@ freeScheduler( void )
-------------------------------------------------------------------------- */
static void
-performGC_(bool force_major, bool nonconcurrent)
+performGC_(bool force_major, bool deadlock_detect, bool nonconcurrent)
{
Task *task;
Capability *cap = NULL;
@@ -2711,7 +2713,7 @@ performGC_(bool force_major, bool nonconcurrent)
// TODO: do we need to traceTask*() here?
waitForCapability(&cap,task);
- scheduleDoGC(&cap,task,force_major,false,false,nonconcurrent);
+ scheduleDoGC(&cap,task,force_major,false,deadlock_detect,nonconcurrent);
releaseCapability(cap);
exitMyTask();
}
@@ -2719,19 +2721,33 @@ performGC_(bool force_major, bool nonconcurrent)
void
performGC(void)
{
- performGC_(false, false);
+ performGC_(false /*force_major*/,
+ false /*deadlock_detect*/,
+ false /*nonconcurrent*/);
}
void
performMajorGC(void)
{
- performGC_(true, false);
+ performGC_(true /*force_major*/,
+ false /*deadlock_detect*/,
+ false /*nonconcurrent*/);
}
void
performBlockingMajorGC(void)
{
- performGC_(true, true);
+ performGC_(true /*force_major*/,
+ false /*deadlock_detect*/,
+ true /*nonconcurrent*/);
+}
+
+void
+performDeadlockDetection(void)
+{
+ performGC_(true /*force_major*/,
+ true /*deadlock_detect*/,
+ false /*nonconcurrent*/);
}
/* ---------------------------------------------------------------------------
=====================================
rts/Timer.c
=====================================
@@ -68,7 +68,9 @@ handle_tick(int unused STG_UNUSED)
}
#endif
+#if defined(THREADED_IDLEGC)
handleIdleGcTick();
+#endif
}
void initTimer(void)
=====================================
rts/include/rts/OSThreads.h
=====================================
@@ -54,6 +54,12 @@
// however at the moment we cannot due to issue #27346.
#endif
+#if defined(HAVE_PREEMPTION)
+#define USED_IF_PREEMPTION
+#else
+#define USED_IF_PREEMPTION STG_UNUSED
+#endif
+
#if defined(HAVE_PTHREAD_H) && !defined(mingw32_HOST_OS)
#if defined(CMINUSMINUS)
=====================================
rts/include/rts/storage/GC.h
=====================================
@@ -290,6 +290,7 @@ void setAllocLimitKill(bool, bool);
void performGC(void);
void performMajorGC(void);
void performBlockingMajorGC(void);
+void performDeadlockDetection(void); // Implemented as a mode of major GC
/* -----------------------------------------------------------------------------
The CAF table - used to let us revert CAFs in GHCi
=====================================
rts/posix/Poll.c
=====================================
@@ -369,13 +369,14 @@ void pollCompletedTimeoutsOrIOPoll(CapIOManager *iomgr)
if (!isEmptyClosureTable(&iomgr->aiop_table)) {
+ int n_aiops = sizeClosureTable(&iomgr->aiop_table);
#if defined(HAVE_PREEMPTION)
/* the full_poll_table includes interrupt_fd_r */
- nfds_t nfds = sizeClosureTable(&iomgr->aiop_table) + 1;
+ nfds_t nfds = n_aiops + 1;
struct pollfd *poll_table = iomgr->full_poll_table;
#else
/* the aiop_poll_table does not include interrupt_fd_r */
- nfds_t nfds = sizeClosureTable(&iomgr->aiop_table) + 0;
+ nfds_t nfds = n_aiops + 0;
struct pollfd *poll_table = iomgr->aiop_poll_table;
#endif
@@ -387,13 +388,13 @@ void pollCompletedTimeoutsOrIOPoll(CapIOManager *iomgr)
int res = ppoll(poll_table, nfds, &tv, NULL);
debugTrace(DEBUG_iomanager,
- "ppoll(nfds = %lu, timeout.sec = 0, timeout.nsec = 0) = %d",
+ "ppoll(nfds = %lu, timeout = 0) = %d",
(unsigned long) nfds, res);
#else
int res = poll(poll_table, nfds, 0);
debugTrace(DEBUG_iomanager,
- "poll(nfds = %lu, timeout_ms = 0) = %d",
+ "poll(nfds = %lu, timeout = 0) = %d",
(unsigned long) nfds, res);
#endif
if (res == 0) {
@@ -448,43 +449,54 @@ bool awaitCompletedTimeoutsOrIOPoll(CapIOManager *iomgr)
*/
bool wait = emptyRunQueue(iomgr->cap);
+ int n_aiops = sizeClosureTable(&iomgr->aiop_table);
#if defined(HAVE_PREEMPTION)
/* the full_poll_table includes interrupt_fd_r */
- nfds_t nfds = sizeClosureTable(&iomgr->aiop_table) + 1;
+ nfds_t nfds = n_aiops + 1;
struct pollfd *poll_table = iomgr->full_poll_table;
#else
/* the aiop_poll_table does not include interrupt_fd_r */
- nfds_t nfds = sizeClosureTable(&iomgr->aiop_table) + 0;
+ nfds_t nfds = n_aiops + 0;
struct pollfd *poll_table = iomgr->aiop_poll_table;
#endif
/* Decide if we are going to wait if no I/O is ready, either:
* poll only, wait indefinitely, or wait until a timeout.
*/
-#if defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1
- struct timespec ts, *timeout_ns;
- timeout_ns = timeoutInNanoseconds(iomgr, wait, now, &ts);
-#else
- int timeout_ms = timeoutInMilliseconds(iomgr, wait, now);
+ Time timeout = timeoutWaitTime(iomgr, wait, now);
+
+ if (RTS_UNLIKELY(timeout == -1 && n_aiops == 0)) {
+ /* See Note [Deadlock detection without idle GC] */
+ interrupt = notifyIdleGcDeadlock();
+ if (interrupt) break;
+ }
+
+#if !defined(THREADED_IDLEGC)
+ /* Without threaded idle GC, poll() does not get interrupted for an
+ * idle GC. Instead we just limit our wait time to the idle gc delay
+ * time, and if that timeout is reached then we schedule an idle GC
+ * (see handleIdleGcTimeout below).
+ */
+ bool any_pending_io = nfds > 0;
+ int timeout_idlegc_delay;
+ adjustTimeoutForIdleGc(any_pending_io, &timeout, &timeout_idlegc_delay);
#endif
/* Check for I/O readiness, possibly waiting. */
#if defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1
+ struct timespec ts, *timeout_ns = timeoutAsTimespec(timeout, &ts);
int res = ppoll(poll_table, nfds, timeout_ns, NULL);
debugTrace(DEBUG_iomanager,
- "ppoll(nfds = %lu, timeout.sec = %lld, timeout.nsec = %lld)"
- " = %d",
- (unsigned long) nfds,
- (long long) (timeout_ns == NULL ? -1 : timeout_ns->tv_sec),
- (long long) (timeout_ns == NULL ? 0 : timeout_ns->tv_nsec),
- res);
+ "ppoll(nfds = %lu, timeout = %" FMT_Int64 " ns) = %d",
+ (unsigned long) nfds, timeout, res);
#else
+ int timeout_ms = timeoutAsPollTimeout(timeout);
int res = poll(poll_table, nfds, timeout_ms);
debugTrace(DEBUG_iomanager,
- "poll(nfds = %lu, timeout_ms = %d) = %d",
- nfds, timeout_ms, res);
+ "poll(nfds = %lu, timeout = %d ms) = %d",
+ (unsigned long) nfds, timeout_ms, res);
#endif
if (res == 0) {
@@ -493,10 +505,9 @@ bool awaitCompletedTimeoutsOrIOPoll(CapIOManager *iomgr)
* occurred before any I/O became ready. Either way, the do-while
* loop condition will handle it.
*/
-#if defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1
- ASSERT(timeout_ns != NULL);
-#else
- ASSERT(timeout_ms != -1);
+ ASSERT(timeout != -1);
+#if !defined(THREADED_IDLEGC)
+ handleIdleGcTimeout(timeout_idlegc_delay, &interrupt);
#endif
} else if (res > 0) {
@@ -554,7 +565,7 @@ static void reportPollError(int res, nfds_t nfds)
}
-void interruptIOManagerPoll(CapIOManager *iomgr)
+void interruptIOManagerPoll(CapIOManager *iomgr USED_IF_PREEMPTION)
{
#if defined(HAVE_PREEMPTION)
sendFdWakeup(iomgr->interrupt_fd_w);
=====================================
rts/posix/Select.c
=====================================
@@ -22,6 +22,7 @@
#include "Stats.h"
#include "GetTime.h"
#include "FdWakeup.h"
+#include "Timeout.h"
# if defined(HAVE_SYS_SELECT_H)
# include <sys/select.h>
@@ -73,14 +74,14 @@ void initCapabilityIOManagerSelect(CapIOManager *iomgr)
#endif
}
-void freeCapabilityIOManagerSelect(CapIOManager *iomgr)
+void freeCapabilityIOManagerSelect(CapIOManager *iomgr USED_IF_PREEMPTION)
{
#if defined(HAVE_PREEMPTION)
closeFdWakeup(iomgr->interrupt_fd_r, iomgr->interrupt_fd_w);
#endif
}
-void interruptIOManagerSelect(CapIOManager *iomgr)
+void interruptIOManagerSelect(CapIOManager *iomgr USED_IF_PREEMPTION)
{
#if defined(HAVE_PREEMPTION)
sendFdWakeup(iomgr->interrupt_fd_w);
@@ -345,47 +346,38 @@ awaitCompletedTimeoutsOrIOSelect(CapIOManager *iomgr, bool wait)
}
}
+ Time timeout;
if (!wait) {
// just poll
- tv.tv_sec = 0;
- tv.tv_usec = 0;
- ptv = &tv;
+ timeout = 0;
} else if (iomgr->sleeping_queue != END_TSO_QUEUE) {
- /* SUSv2 allows implementations to have an implementation defined
- * maximum timeout for select(2). The standard requires
- * implementations to silently truncate values exceeding this maximum
- * to the maximum. Unfortunately, OSX and the BSD don't comply with
- * SUSv2, instead opting to return EINVAL for values exceeding a
- * timeout of 1e8.
- *
- * Select returning an error crashes the runtime in a bad way. To
- * play it safe we truncate any timeout to 31 days, as SUSv2 requires
- * any implementations maximum timeout to be larger than this.
- *
- * Truncating the timeout is not an issue, because if nothing
- * interesting happens when the timeout expires, we'll see that the
- * thread still wants to be blocked longer and simply block on a new
- * iteration of select(2).
- */
- const time_t max_seconds = 2678400; // 31 * 24 * 60 * 60
-
- Time min = LowResTimeToTime(
- iomgr->sleeping_queue->block_info.target - now
- );
- tv.tv_sec = TimeToSeconds(min);
- if (tv.tv_sec < max_seconds) {
- tv.tv_usec = TimeToUS(min) % 1000000;
- } else {
- tv.tv_sec = max_seconds;
- tv.tv_usec = 0;
- }
- ptv = &tv;
+ timeout =
+ LowResTimeToTime(iomgr->sleeping_queue->block_info.target - now);
} else {
- ptv = NULL;
+ timeout = -1;
}
+ if (RTS_UNLIKELY(timeout == -1 &&
+ iomgr->blocked_queue_hd == END_TSO_QUEUE)) {
+ /* See Note [Deadlock detection without idle GC] */
+ interrupt = notifyIdleGcDeadlock();
+ if (interrupt) break;
+ }
+
+#if !defined(THREADED_IDLEGC)
+ /* Without threaded idle GC, select() does not get interrupted for an
+ * idle GC. Instead we just limit our wait time to the idle gc delay
+ * time, and if that timeout is reached then we schedule an idle GC
+ * (see handleIdleGcTimeout below).
+ */
+ bool any_pending_io = maxfd > 0;
+ int timeout_idlegc_delay;
+ adjustTimeoutForIdleGc(any_pending_io, &timeout, &timeout_idlegc_delay);
+#endif
+
/* Check for any interesting events */
+ ptv = timeoutAsTimeval(timeout, &tv);
while ((numFound = select(maxfd+1, &rfd, &wfd, NULL, ptv)) < 0) {
if (errno != EINTR) {
if ( errno == EBADF ) {
@@ -422,6 +414,10 @@ awaitCompletedTimeoutsOrIOSelect(CapIOManager *iomgr, bool wait)
}
}
+#if !defined(THREADED_IDLEGC)
+ if (numFound == 0) handleIdleGcTimeout(timeout_idlegc_delay, &interrupt);
+#endif
+
#if defined(HAVE_PREEMPTION)
/* If the interrupt_fd_r is ready, collect it */
if (FD_ISSET(iomgr->interrupt_fd_r, &rfd)) {
=====================================
rts/posix/SelectBis.c
=====================================
@@ -361,6 +361,10 @@ void pollCompletedTimeoutsOrIOSelectBis(CapIOManager *iomgr)
/* Poll for I/O readiness, without waiting. */
struct timeval tv = (struct timeval) { .tv_sec = 0, .tv_usec = 0 };
int res = select(maxfd+1, iomgr->rfds, iomgr->wfds, NULL, &tv);
+ debugTrace(DEBUG_iomanager,
+ "select(nfds = %lu, timeout = 0) = %d",
+ (unsigned long) maxfd+1, res);
+
if (res == 0) {
/* There is no I/O ready. We'll return to the scheduler. */
@@ -422,19 +426,44 @@ bool awaitCompletedTimeoutsOrIOSelectBis(CapIOManager *iomgr)
/* Decide if we are going to wait if no I/O is ready, either:
* poll only, wait indefinitely, or wait until a timeout.
*/
- struct timeval tv, *timeout_us;
- timeout_us = timeoutInMicroseconds(iomgr, wait, now, &tv);
+ Time timeout = timeoutWaitTime(iomgr, wait, now);
+
+ if (RTS_UNLIKELY(timeout == -1 &&
+ isEmptyClosureTable(&iomgr->aiop_table))) {
+ /* See Note [Deadlock detection without idle GC] */
+ interrupt = notifyIdleGcDeadlock();
+ if (interrupt) break;
+ }
+
+#if !defined(THREADED_IDLEGC)
+ /* Without threaded idle GC, select() does not get interrupted for an
+ * idle GC. Instead we just limit our wait time to the idle gc delay
+ * time, and if that timeout is reached then we schedule an idle GC
+ * (see handleIdleGcTimeout below).
+ */
+ bool any_pending_io = maxfd >= 0;
+ int timeout_idlegc_delay;
+ adjustTimeoutForIdleGc(any_pending_io, &timeout, &timeout_idlegc_delay);
+#endif
/* Check for I/O readiness, possibly waiting. */
+ struct timeval tv, *timeout_us = timeoutAsTimeval(timeout, &tv);
int res = select(maxfd+1, iomgr->rfds, iomgr->wfds, NULL, timeout_us);
+ debugTrace(DEBUG_iomanager,
+ "select(nfds = %d, timeout = %" FMT_Int64 " us) = %d",
+ maxfd+1, TimeToUS(timeout), res);
+
if (res == 0) {
/* Success but there is no I/O ready. This can happen either if we
* were not blocking or were in a timed wait and the timeout
* occurred before any I/O became ready. Either way, the do-while
* loop condition will handle it.
*/
- ASSERT(timeout_us != NULL);
+ ASSERT(timeout != -1);
+#if !defined(THREADED_IDLEGC)
+ handleIdleGcTimeout(timeout_idlegc_delay, &interrupt);
+#endif
} else if (res > 0) {
int ncompletions = res;
@@ -546,7 +575,7 @@ static void processBadFds(CapIOManager *iomgr)
}
-void interruptIOManagerSelectBis(CapIOManager *iomgr)
+void interruptIOManagerSelectBis(CapIOManager *iomgr USED_IF_PREEMPTION)
{
#if defined(HAVE_PREEMPTION)
sendFdWakeup(iomgr->interrupt_fd_w);
=====================================
rts/posix/Timeout.c
=====================================
@@ -135,11 +135,11 @@ static void notifyTimeoutCompletion(CapIOManager *iomgr, StgTimeout *timeout)
}
-/* poll() expect a timeout in milliseconds, with special
- * values of -1 for indefinite wait, and 0 for no waiting.
+/* Returns the duration until the next timeout, with special values 0 for
+ * no timeout and -1 for infinite timeout. Use one of the timeoutAs* functions
+ * to convert into the form expected by platform APIs.
*/
-#if !(defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1)
-int timeoutInMilliseconds(CapIOManager *iomgr, bool wait, Time now)
+Time timeoutWaitTime(CapIOManager *iomgr, bool wait, Time now)
{
if (!wait) {
/* Don't wait, just poll. */
@@ -152,7 +152,22 @@ int timeoutInMilliseconds(CapIOManager *iomgr, bool wait, Time now)
/* Any expired timeouts should have been cleared, so we must be waiting
* for a timeout in the future. */
ASSERT(waittime > 0);
+ return waittime;
+ } else {
+ /* No timeouts, wait forever */
+ return -1;
+ }
+}
+
+
+/* Convert the result of timeoutWaitTime into the timeout representation
+ * used by poll(). This representation uses millisecond precision with special
+ * values 0 and -1 for no wait and indefinite wait.
+ */
+int timeoutAsPollTimeout(Time waittime)
+{
+ if (waittime > 0) {
/* SUSv2 allows implementations to have an implementation defined
* maximum timeout for poll(2). It does not specify any limits on
* this maximum however. It is safe to pick a low limit, because if
@@ -170,38 +185,25 @@ int timeoutInMilliseconds(CapIOManager *iomgr, bool wait, Time now)
return (waittime_ms < max_timeout_ms) ? waittime_ms : max_timeout_ms;
} else {
- /* No timeouts, wait forever */
- return -1;
+ /* We must be in the 0 or -1 special cases */
+ ASSERT(waittime == 0 || waittime == -1);
+ return (int) waittime;
}
}
-#endif
-/* ppoll() expect a timeout in nanoseconds, using
- * struct timespec * with special values of NULL for indefinite wait,
- * and 0 for no waiting.
+/* Convert the result of timeoutWaitTime into a 'struct timespec *' which is
+ * the timeout representation used by many modern APIs: ppoll(), pselect(),
+ * epoll_wait2(), kevent() and io_uring_enter2(). This representation uses
+ * nanosecond precision with NULL for indefinite wait, and 0 for no waiting.
*/
-#if (defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1)
-struct timespec *timeoutInNanoseconds(CapIOManager *iomgr, bool wait,
- Time now, struct timespec *tv)
+struct timespec *timeoutAsTimespec(Time waittime, struct timespec *tv)
{
- if (!wait) {
- /* Don't wait, just poll. */
- *tv = (struct timespec) { .tv_sec = 0, .tv_nsec = 0 };
- return tv;
-
- } else if (!isEmptyTimeoutQueue(iomgr->timeout_queue)) {
- Time waketime = findMinWaketimeTimeoutQueue(iomgr->timeout_queue);
- Time waittime = waketime - now;
-
- /* Any expired timeouts should have been cleared, so we must be waiting
- * for a timeout in the future. */
- ASSERT(waittime > 0);
-
- /* See comments above in timeoutInMilliseconds about the max timeout */
+ if (waittime > 0) {
+ /* See comments above about the max timeout */
const int64_t max_timeout_ms = INT_MAX / 1000;
+ const Time max_timeout_ns = MSToTime(max_timeout_ms);
- const Time max_timeout_ns = MSToTime(max_timeout_ms);
if (waittime < max_timeout_ns) {
/* Time in nanoseconds to separate seconds and nanoseconds */
*tv = (struct timespec) {
@@ -217,25 +219,26 @@ struct timespec *timeoutInNanoseconds(CapIOManager *iomgr, bool wait,
return tv;
} else {
- /* No timeouts, wait forever */
- return NULL;
+ /* We must be in the 0 or -1 special cases */
+ ASSERT(waittime == 0 || waittime == -1);
+ if (waittime == 0) {
+ /* Don't wait, just poll. */
+ *tv = (struct timespec) { .tv_sec = 0, .tv_nsec = 0 };
+ return tv;
+ } else {
+ /* No timeouts, wait forever */
+ return NULL;
+ }
}
}
-#endif
-/* select() expect a timeout in microseconds, using struct timeval * with
- * special values of NULL for indefinite wait, and 0 for no waiting.
+/* Convert the result of timeoutWaitTime into a 'struct timeval *' which is
+ * the timeout representation used by select(). This representation uses
+ * microsecond precision with NULL for indefinite wait, and 0 for no waiting.
*/
-#if defined(IOMGR_ENABLED_SELECTBIS)
-struct timeval *timeoutInMicroseconds(CapIOManager *iomgr, bool wait,
- Time now, struct timeval *tv)
+struct timeval *timeoutAsTimeval(Time waittime, struct timeval *tv)
{
- if (!wait) {
- /* Don't wait, just poll. */
- *tv = (struct timeval) { .tv_sec = 0, .tv_usec = 0 };
- return tv;
-
- } else if (!isEmptyTimeoutQueue(iomgr->timeout_queue)) {
+ if (waittime > 0) {
/* SUSv2 allows implementations to have an implementation defined
* maximum timeout for select(2). The standard requires
* implementations to silently truncate values exceeding this maximum
@@ -254,13 +257,6 @@ struct timeval *timeoutInMicroseconds(CapIOManager *iomgr, bool wait,
*/
const time_t max_seconds = 2678400; // 31 * 24 * 60 * 60
- Time waketime = findMinWaketimeTimeoutQueue(iomgr->timeout_queue);
- Time waittime = waketime - now;
-
- /* Any expired timeouts should have been cleared, so we must be waiting
- * for a timeout in the future. */
- ASSERT(waittime > 0);
-
tv->tv_sec = TimeToSeconds(waittime);
if (tv->tv_sec < max_seconds) {
tv->tv_usec = TimeToUS(waittime) % 1000000;
@@ -271,7 +267,60 @@ struct timeval *timeoutInMicroseconds(CapIOManager *iomgr, bool wait,
return tv;
} else {
- return NULL;
+ /* We must be in the 0 or -1 special cases */
+ ASSERT(waittime == 0 || waittime == -1);
+
+ if (waittime == 0) {
+ /* Don't wait, just poll. */
+ *tv = (struct timeval) { .tv_sec = 0, .tv_usec = 0 };
+ return tv;
+ } else {
+ /* No timeouts, wait forever */
+ return NULL;
+ }
+ }
+}
+
+
+#if !defined(THREADED_IDLEGC)
+/* Utilities for handling the non-threaded idle GC variation.
+ *
+ * See Note [Idle GC without preemption]
+ *
+ * Before calling poll()/select() etc, adjust the timeout to account for the
+ * idle GC delay time.
+ *
+ * If the timeout occurs, call handleIdleGcTimeout, which will schedule an idle
+ * GC if the timeout was reached.
+ *
+ * The timeout_is_idlegc_delay must be passed from adjustTimeoutForIdleGc to
+ * handleIdleGcTimeout.
+ */
+void adjustTimeoutForIdleGc(bool any_pending_io,
+ Time *timeout, /* in/out */
+ bool *timeout_is_idlegc_delay /* out */)
+{
+ Time idlegc_delay = getNextIdleGcDelayTime();
+ /* Note that idlegc_delay can be -1 for indefinite wait, if we've already
+ * done an idle GC and there's been no activity since.
+ */
+
+ if (RtsFlags.GcFlags.doIdleGC &&
+ (*timeout == -1 || (idlegc_delay > 0 && idlegc_delay < *timeout))) {
+ /* The idle GC delay will be the next timeout. */
+ *timeout = idlegc_delay;
+ *timeout_is_idlegc_delay = true;
+
+ } else {
+ *timeout_is_idlegc_delay = false;
+ }
+}
+
+void handleIdleGcTimeout(int timeout_is_idlegc_delay, bool *interrupt)
+{
+ if (timeout_is_idlegc_delay) {
+ notifyIdleGcIdle();
+ *interrupt = true;
}
}
#endif
=====================================
rts/posix/Timeout.h
=====================================
@@ -10,6 +10,8 @@
#pragma once
+#include "IdleGC.h" // To get definition of THREADED_IDLEGC
+
#include "BeginPrivate.h"
bool syncDelayTimeout(CapIOManager *iomgr, StgTSO *tso, HsInt us_delay);
@@ -26,34 +28,49 @@ void syncDelayCancelTimeout(CapIOManager *iomgr, StgTSO *tso);
*/
void processTimeoutCompletions(CapIOManager *iomgr, Time now);
-/* Utility to compute the timeout wait time (in milliseconds) between now and
- * the next timer expiry (if any), or no waiting (if !wait).
+#if defined(IOMGR_ENABLED_POLL) \
+ || defined(IOMGR_ENABLED_SELECTBIS)
+/* Compute the timeout wait time between now and the next timer expiry (if any)
+ * using the given IOManager's timeout_queue.
+ *
+ * Use one of the timeoutAs* functions to convert into the form expected by
+ * particular platform APIs.
*
- * This is intended to be used with poll() which expect a
- * timeout in milliseconds, with special values of -1 for indefinite wait,
- * and 0 for no waiting.
+ * It returns the wait time duration as a Time (i.e. nanoseconds), but with
+ * special values 0 for no timeout and -1 for indefinite timeout.
*/
-#if !(defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1)
-int timeoutInMilliseconds(CapIOManager *iomgr, bool wait, Time now);
+Time timeoutWaitTime(CapIOManager *iomgr, bool wait, Time now);
#endif
-/* As above, but a timeout in nanoseconds. This is intended to be used with
- * ppoll() which expect struct timespec *, with special
- * values of NULL for indefinite wait, and 0 for no waiting.
+/* Convert the result of timeoutWaitTime into the timeout representation
+ * used by poll(). This representation uses millisecond precision with special
+ * values 0 and -1 for no wait and indefinite wait.
*/
-#if (defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1)
-struct timespec *timeoutInNanoseconds(CapIOManager *iomgr, bool wait,
- Time now, struct timespec *tv);
-#endif
+int timeoutAsPollTimeout(Time waittime);
+
+/* Convert the result of timeoutWaitTime into a 'struct timeval *' which is
+ * the timeout representation used by select(). This representation uses
+ * microsecond precision with NULL for indefinite wait, and 0 for no waiting.
+ */
+struct timeval *timeoutAsTimeval(Time waittime, struct timeval *tv);
+
+/* Convert the result of timeoutWaitTime into a 'struct timespec *' which is
+ * the timeout representation used by many modern APIs: ppoll(), pselect(),
+ * epoll_wait2(), kevent() and io_uring_enter2(). This representation uses
+ * nanosecond precision with NULL for indefinite wait, and 0 for no waiting.
+ */
+struct timespec *timeoutAsTimespec(Time waittime, struct timespec *tv);
-/* As above, but a timeout in microseconds. This is intended to be used with
- * select() which expect struct timespec *, with special values of NULL for
- * indefinite wait, and 0 for no waiting.
+#if !defined(THREADED_IDLEGC)
+/* Utilities for handling the non-threaded idle GC variation.
+ *
+ * See Note [Idle GC without preemption]
*/
-#if defined(IOMGR_ENABLED_SELECTBIS)
-struct timeval *timeoutInMicroseconds(CapIOManager *iomgr, bool wait,
- Time now, struct timeval *tv);
+void adjustTimeoutForIdleGc(bool any_pending_io,
+ Time *timeout, /* in/out */
+ bool *timeout_is_idlegc_delay /* out */);
+void handleIdleGcTimeout(int idlegc_status, bool *interrupt);
#endif
#include "EndPrivate.h"
=====================================
rts/win32/AsyncMIO.c
=====================================
@@ -231,6 +231,12 @@ awaitRequests(bool wait)
{
CapIOManager *iomgr = MainCapability.iomgr;
+ if (RTS_UNLIKELY(issued_reqs == 0)) {
+ /* See Note [Deadlock detection without idle GC]
+ * Note that the issued_reqs covers both I/O and timers. */
+ if (notifyIdleGcDeadlock()) return false;
+ }
+
start:
#if 0
fprintf(stderr, "awaitRequests(): %d %d %d\n",
=====================================
testsuite/tests/interface-stability/ghc-experimental-exports.stdout
=====================================
@@ -13642,6 +13642,7 @@ module System.Mem.Experimental where
disableAllocationLimitFor :: GHC.Internal.Conc.Sync.ThreadId -> GHC.Internal.Types.IO ()
enableAllocationLimitFor :: GHC.Internal.Conc.Sync.ThreadId -> GHC.Internal.Types.IO ()
getAllocationCounterFor :: GHC.Internal.Conc.Sync.ThreadId -> GHC.Internal.Types.IO GHC.Internal.Int.Int64
+ performDeadlockDetection :: GHC.Internal.Types.IO ()
setAllocationCounterFor :: GHC.Internal.Int.Int64 -> GHC.Internal.Conc.Sync.ThreadId -> GHC.Internal.Types.IO ()
setGlobalAllocationLimitHandler :: AllocationLimitKillBehaviour -> GHC.Internal.Maybe.Maybe (GHC.Internal.Conc.Sync.ThreadId -> GHC.Internal.Types.IO ()) -> GHC.Internal.Types.IO ()
=====================================
testsuite/tests/interface-stability/ghc-experimental-exports.stdout-mingw32
=====================================
@@ -13645,6 +13645,7 @@ module System.Mem.Experimental where
disableAllocationLimitFor :: GHC.Internal.Conc.Sync.ThreadId -> GHC.Internal.Types.IO ()
enableAllocationLimitFor :: GHC.Internal.Conc.Sync.ThreadId -> GHC.Internal.Types.IO ()
getAllocationCounterFor :: GHC.Internal.Conc.Sync.ThreadId -> GHC.Internal.Types.IO GHC.Internal.Int.Int64
+ performDeadlockDetection :: GHC.Internal.Types.IO ()
setAllocationCounterFor :: GHC.Internal.Int.Int64 -> GHC.Internal.Conc.Sync.ThreadId -> GHC.Internal.Types.IO ()
setGlobalAllocationLimitHandler :: AllocationLimitKillBehaviour -> GHC.Internal.Maybe.Maybe (GHC.Internal.Conc.Sync.ThreadId -> GHC.Internal.Types.IO ()) -> GHC.Internal.Types.IO ()
=====================================
testsuite/tests/rts/Makefile
=====================================
@@ -157,3 +157,11 @@ HSC2HS_OPTS = --cc="$(TEST_CC)" $(addprefix --cflag=,$(TEST_CC_OPTS)) --ld=$(TES
IOManager.hs: IOManager.hsc
'$(HSC2HS)' $(HSC2HS_OPTS) $<
+
+.PHONY: T26408c
+T26408c:
+ "$(TEST_HC)" $(TEST_HC_OPTS) -rtsopts T26408c.hs; \
+ ./T26408c +RTS -I0 -T & \
+ sleep 1; \
+ kill -s USR1 $$!; \
+ wait $$!
=====================================
testsuite/tests/rts/T26408a.hs
=====================================
@@ -0,0 +1,54 @@
+import Control.Concurrent
+import Control.Concurrent.STM
+import Control.Exception
+import Control.Monad
+
+-- | Test to make sure that we can detect a /partial deadlock/ that is /not/
+-- also a /system deadlock/: that is we can detect a set of deadlocked threads
+-- even when there are other unrelated threads that are waiting on I\/O or
+-- timeouts.
+--
+-- Historically this did not work in the non-threaded RTS which would only do
+-- deadlock detection if there was a system deadlock: no runnable threads /and/
+-- no pending I\/O or timers.
+--
+-- See Note [Deadlock detection]
+--
+main :: IO ()
+main = do
+
+ -- Set up two threads that are deadlocked on each other
+ aDone <- newTVarIO False
+ bDone <- newTVarIO False
+ let blockingThread theirDone ourDone =
+ atomically $ do
+ done <- readTVar theirDone
+ guard done
+ writeTVar ourDone True
+ _ <- forkIO (blockingThread bDone aDone)
+ _ <- forkIO (blockingThread aDone bDone)
+
+ -- Set up another thread that is blocked on a long timeout.
+ --
+ -- We use a timeout rather than I/O as it's more portable, whereas I/O waits
+ -- are different between posix and windows I/O managers.
+ --
+ -- One gotcha is that when the timeout completes then the deadlock will be
+ -- detected again (since the bug is about I/O or timeouts masking deadlock
+ -- detection). So for a reliable test the timeout used here must be longer
+ -- than the test framework's own timeout. So we use maxBound, and we adjust
+ -- the test framework's timeout to be short (see run_timeout_multiplier).
+ _ <- forkIO (threadDelay maxBound)
+
+ -- Wait on the two deadlocked threads to terminate. This is now a partial
+ -- deadlock but not a system deadlock (the main thread and the first two
+ -- threads are deadlocked, but the one waiting on the timeout is ok).
+ --
+ -- We now expect that the threads that are deadlocked are detected as such
+ -- and an exception is thrown to some or all of them.
+ --
+ -- Note that if this fails, the test itself will effectively deadlock and
+ -- will rely on the test framework's timeout.
+ atomically $ do
+ status <- mapM readTVar [aDone, bDone]
+ guard (or status)
=====================================
testsuite/tests/rts/T26408a.stderr
=====================================
@@ -0,0 +1,3 @@
+T26408a: Uncaught exception ghc-internal:GHC.Internal.IO.Exception.BlockedIndefinitelyOnSTM:
+
+thread blocked indefinitely in an STM transaction
=====================================
testsuite/tests/rts/T26408b.hs
=====================================
@@ -0,0 +1,43 @@
+import Control.Concurrent
+import Control.Concurrent.STM
+import Control.Exception
+import Control.Monad
+
+import System.Posix.Signals
+
+-- | Test to make sure that we can detect a /system deadlock/ promptly which
+-- means without relying on idle GC. To check this we have to turn off idle GC,
+-- (+RTS -I0) which is what normally triggers deadlock detection.
+--
+-- This is only expected to work in the non-threaded RTS. In principle this
+-- test could also work with the threaded RTS when using only a single
+-- capability, if the I\/O managers can support it. Currently that is not the
+-- case.
+--
+-- See Note [Deadlock detection]
+--
+main :: IO ()
+main = do
+
+ -- Set up two threads that are deadlocked on each other
+ aDone <- newTVarIO False
+ bDone <- newTVarIO False
+ let blockingThread theirDone ourDone =
+ atomically $ do
+ done <- readTVar theirDone
+ guard done
+ writeTVar ourDone True
+ _ <- forkIO (blockingThread bDone aDone)
+ _ <- forkIO (blockingThread aDone bDone)
+
+ -- Wait on the two deadlocked threads to terminate. This is now a system
+ -- deadlock, involving all three threads (the main thread and the other two).
+ --
+ -- We now expect that the threads that are deadlocked are detected as such
+ -- and an exception is thrown to some or all of them.
+ --
+ -- Note that if this fails, the test itself will effectively deadlock and
+ -- will rely on the test framework's timeout.
+ atomically $ do
+ status <- mapM readTVar [aDone, bDone]
+ guard (or status)
=====================================
testsuite/tests/rts/T26408b.stderr
=====================================
@@ -0,0 +1,3 @@
+T26408b: Uncaught exception ghc-internal:GHC.Internal.IO.Exception.BlockedIndefinitelyOnSTM:
+
+thread blocked indefinitely in an STM transaction
=====================================
testsuite/tests/rts/T26408c.hs
=====================================
@@ -0,0 +1,61 @@
+import Control.Concurrent
+import Control.Concurrent.STM
+import Control.Exception
+import Control.Monad
+
+import System.Posix.Signals
+import GHC.Stats.Experimental
+
+-- | Test to make sure that we do not falsely detect a deadlock when there is
+-- nothing to do except wait on signals. This is to make sure that our prompt
+-- system deadlock detection is not too eager.
+--
+-- We arrange a partial deadlock that can be broken if a signal arrives and
+-- the signal handler can unblock things.
+--
+-- This should /not/ result in any deadlock being detected, and no other odd
+-- behaviour, like getting caught in a loop and doing too many idle GCs. So we
+-- count and report the number of major GCs.
+--
+main :: IO ()
+main = do
+
+ -- Set up two threads that are deadlocked on each other
+ aDone <- newTVarIO False
+ bDone <- newTVarIO False
+ let blockingThread theirDone ourDone =
+ atomically $ do
+ done <- readTVar theirDone
+ guard done
+ writeTVar ourDone True
+
+ installHandler userDefinedSignal1
+ (Catch $ do
+ atomically $ writeTVar aDone True
+ >> writeTVar bDone True
+ putStrLn "caught SIGUSR1")
+ Nothing
+
+ _ <- forkIO (blockingThread bDone aDone)
+ _ <- forkIO (blockingThread aDone bDone)
+ yield
+
+ -- Wait on the deadlocked threads to terminate. We now expect that the threads
+ -- that are deadlocked are detected as such and an exception is raised.
+ -- Note that if this fails, the test itself will effectively deadlock and
+ -- will rely on the test framework's timeout.
+ putStrLn "waiting on deadlocked threads..."
+ atomically $ do
+ status <- mapM readTVar [aDone, bDone]
+ guard (or status)
+
+ -- We expect exactly 1 major GC: the one triggered for deadlock detection
+ -- where we find no deadlock. We should then sleep without doing more GC.
+ -- An easy mistake would be looping: detecting probable deadlock, do idle GC.
+ -- See notifyIdleGcDeadlock().
+ stats <- getRTSStats
+ putStrLn ("terminated normally, major GCs = " ++ show (major_gcs stats))
+ -- This test _could_ be sensitive to changes in default heap sizes etc. If
+ -- this number ends up as 2 for example, then adjust the RTS opts (in the
+ -- makefile) so that the heap is big enough that normally no major GC would
+ -- be needed.
=====================================
testsuite/tests/rts/T26408c.stdout
=====================================
@@ -0,0 +1,5 @@
+[1 of 2] Compiling Main ( T26408c.hs, T26408c.o )
+[2 of 2] Linking T26408c
+waiting on deadlocked threads...
+caught SIGUSR1
+terminated normally, major GCs = 1
=====================================
testsuite/tests/rts/all.T
=====================================
@@ -682,6 +682,20 @@ test('T22859',
omit_ways(llvm_ways + ['nonmoving_thr', 'nonmoving_thr_sanity'])],
compile_and_run, ['-with-rtsopts -A8K'])
+# Deadlock detection testsL
+# A: check we can detect a partial deadlock that is not a system deadlock
+test('T26408a', [exit_code(1), run_timeout_multiplier(0.1)], compile_and_run, [''])
+# B: check we can detect system deadlock with idle GC turned off (+RTS -I0)
+test('T26408b', [omit_ways(threaded_ways), extra_run_opts('+RTS -I0 -RTS'),
+ exit_code(1), run_timeout_multiplier(0.1)],
+ compile_and_run, ['-rtsopts'])
+# C: check we do not falsely detect deadlock when blocked only on signals
+test('T26408c', [run_timeout_multiplier(0.1)], makefile_test, ['T26408c'])
+# TODO: change omit_ways(threaded_ways) above
+# to all_io_managers_except['mio', 'winio']
+# and use +RTS -N1 with threaded ways.
+
+
# These tests need access to the internal RTS headers.
# TODO: there is probably some cleaner way to do this, and it should probably
# be guarded for in-tree tests, since it cannot work against an arbitrary
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/c20f42b2694c5e470e3286be0ca911…
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/c20f42b2694c5e470e3286be0ca911…
You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
1
0
[Git][ghc/ghc][wip/rafl/hpc-instances] 10 commits: Change the C types of hs_{bitrev,bswap}{16,32} to match the Haskell types
by Florian Ragwitz (@rafl) 25 Sep '26
by Florian Ragwitz (@rafl) 25 Sep '26
25 Sep '26
Florian Ragwitz pushed to branch wip/rafl/hpc-instances at Glasgow Haskell Compiler / GHC
Commits:
545fd4fb by ARATA Mizuki at 2026-09-22T15:03:24-04:00
Change the C types of hs_{bitrev,bswap}{16,32} to match the Haskell types
bitReverse{16,32}# and byteSwap{16,32}# have the type
Word# -> Word#, but their C correspondents took and
returned StgWord{16,32}.
The mismatch on bitReverse16# led to the problem reported
in #27299. Although no similar issue is reported for
bitReverse32# and byteSwap{16,32}#, it seemed wise to
change them proactively.
This commit fixes the mismatch by changing the C type.
Fixes #27299
- - - - -
bf17f289 by fendor at 2026-09-22T15:04:05-04:00
Don't lose instances from shadowed imported modules with the same name
When we look for instances below the current module, we need to check
the unit id as well when we check for whether the module is the current
module, otherwise we ignore modules we import that have the same name as
the import module.
Adds regression test.
- - - - -
53eead15 by Wolfgang Jeltsch at 2026-09-24T20:12:03-04:00
Correct the documentation of `explicitUnits`
Resolves #27787.
- - - - -
35bf6f4b by Simon Jakobi at 2026-09-24T20:12:49-04:00
Remove stale specHeader bullet from Note [Don't make fillers for constraint types]
The Note said that Specialise.specHeader also calls mkAbsentFiller.
abfc224a27c removed that call: the specialiser no longer drops dead
arguments at all, see Note [Do not drop dead args from specialisations]
in GHC.Core.Opt.Specialise.
Assisted-by: Claude Fable 5.1
- - - - -
5cace21b by Florian Ragwitz at 2026-09-25T03:35:29-07:00
hpc: Test desired HPC behaviour for class instances
- - - - -
526e0adf by Florian Ragwitz at 2026-09-25T03:35:29-07:00
Extend TcGblEnv with a mapping from instance method Ids to DFunIds
We want to use this in HPC to provide better support for instance methods.
This is working towards #17155.
- - - - -
bd9f26ce by Florian Ragwitz at 2026-09-25T03:35:29-07:00
hpc: Do not create top-level boxes for generated instance methods
Users care about which of their written methods are or aren't covered, not
whether all of the inherited methods are covered as well. Those should be
covered where they are implemented.
We achieve that by stopping to generate top-level boxes which will often appear
as uncovered with no clear indication as to why, and no way to fix other than
excercising code the user has not written.
Fixes #17155, #15932.
- - - - -
32c257cf by Florian Ragwitz at 2026-09-25T03:35:29-07:00
hpc: Provide coverage information for class instances
After fixing #17155, we no longer create top-level boxes for the source spans
of instance heads (previously that was the source span used for
inherited/generated instance methods).
With those boxes gone, we can introduce a new top-level box for each class
instance with that instance head source span, and provide more meaningful
coverage semantics for it.
We make the use of any of the instance's methods (user-written or generated)
tick this box, providing the programmer with useful information about whether
or not the instance was exercised at run-time, and whether the instance might
be unnecessary.
- - - - -
9c854b13 by Florian Ragwitz at 2026-09-25T03:35:29-07:00
hpc: Don't allocate boxes for methodless classes
With the plumbing we currently have, these could never be covered, which is not
helpful to hpc users. This could be improved upon further.
- - - - -
edd502c9 by Florian Ragwitz at 2026-09-25T03:35:29-07:00
Add a changelog entry for new HPC features
- - - - -
28 changed files:
- + changelog.d/26610
- + changelog.d/hpc-classinsts
- compiler/GHC/Core/Opt/WorkWrap/Utils.hs
- compiler/GHC/Driver/Env.hs
- compiler/GHC/HsToCore.hs
- compiler/GHC/HsToCore/Ticks.hs
- compiler/GHC/Tc/Module.hs
- compiler/GHC/Tc/TyCl/Instance.hs
- compiler/GHC/Tc/Types.hs
- compiler/GHC/Tc/Utils/Monad.hs
- compiler/GHC/Unit/State.hs
- rts/include/stg/Prim.h
- rts/prim/bitrev.c
- rts/prim/bswap.c
- + testsuite/tests/ghci/prog-mhu008/Makefile
- + testsuite/tests/ghci/prog-mhu008/a/M.hs
- + testsuite/tests/ghci/prog-mhu008/all.T
- + testsuite/tests/ghci/prog-mhu008/b/M.hs
- + testsuite/tests/ghci/prog-mhu008/prog-mhu008.script
- + testsuite/tests/ghci/prog-mhu008/prog-mhu008.stdout
- + testsuite/tests/ghci/prog-mhu008/unitA
- + testsuite/tests/ghci/prog-mhu008/unitB
- + testsuite/tests/hpc/instmeths/instmeths.hs
- + testsuite/tests/hpc/instmeths/instmeths.stdout
- + testsuite/tests/hpc/instmeths/test.T
- + testsuite/tests/primops/should_run/T27299.hs
- + testsuite/tests/primops/should_run/T27299.stdout
- testsuite/tests/primops/should_run/all.T
Changes:
=====================================
changelog.d/26610
=====================================
@@ -0,0 +1,7 @@
+section: compiler
+synopsis: Fix instance resolution for shadowed modules
+description:
+ Instances imported from modules with the same module name as the importing module
+ were dropped accidentally since we didn't account for the unit id of the imported module.
+mrs: !16648
+issues: #26610
=====================================
changelog.d/hpc-classinsts
=====================================
@@ -0,0 +1,23 @@
+section: compiler
+synopsis: Improve coverage reporting for class instances and their methods
+description:
+
+ Coverage reporting (:ghc-flag:`-fhpc`) now treats class instances more
+ naturally.
+
+ Compiler-generated instance methods, such as inherited default methods, no
+ longer receive separate top-level coverage boxes. Explicitly implemented
+ instance methods retain their own declaration coverage.
+
+ This means that it is no longer necessary to exercise all of a class' methods
+ in order for it to appear "covered".
+
+ Additionally, top-level coverage boxes are now created for all instances of
+ classes with methods, and ticked by the use of *any* of the class' methods.
+ This indicates that an instance was in fact used at run-time.
+
+ Note that classes without any instance methods are currently excluded from
+ this, and won't have any coverage boxes created.
+
+mrs: !16632
+issues: #17155 #15932
=====================================
compiler/GHC/Core/Opt/WorkWrap/Utils.hs
=====================================
@@ -1367,13 +1367,8 @@ It's surprisingly subtle.
application; see test `T27627f`.
TL;DR: we play safe: we never make an absent filler for any /constraint-kinded/ type,
-using `isPredTy` to decide.
-
- * `mkAbsentFiller` returns `Nothing` for any constraint type, so worker/wrapper
- keeps the real argument.
-
- * `Specialise.specHeader` calls `mkAbsentFiller` too, so it likewise keeps the
- dead dictionary argument rather than dropping it for a filler.
+using `isPredTy` to decide: `mkAbsentFiller` returns `Nothing` for any constraint
+type, so worker/wrapper keeps the real argument.
Prior failed approaches
=====================================
compiler/GHC/Driver/Env.hs
=====================================
@@ -263,13 +263,13 @@ hugCompleteSigsBelow hsc uid mn = foldr (++) [] <$>
-- | Find instances visible from the given set of imports
hugInstancesBelow :: HscEnv -> UnitId -> ModuleNameWithIsBoot -> IO (InstEnv, [FamInst])
hugInstancesBelow hsc_env uid mnwib = do
- let mn = gwib_mod mnwib
+ let mn = mkModule uid (gwib_mod mnwib)
(insts, famInsts) <-
unzip . concat <$>
hugSomeThingsBelowUs (\mod_info ->
let details = hm_details mod_info
-- Don't include instances for the current module
- in if moduleName (mi_module (hm_iface mod_info)) == mn
+ in if fmap toUnitId (mi_module (hm_iface mod_info)) == mn
then []
else [(md_insts details, md_fam_insts details)])
True -- Include -hi-boot
=====================================
compiler/GHC/HsToCore.hs
=====================================
@@ -147,6 +147,7 @@ deSugar hsc_env
tcg_tcs = tcs,
tcg_default_exports = defaults,
tcg_insts = insts,
+ tcg_inst_meths = inst_meths,
tcg_fam_insts = fam_insts,
tcg_complete_matches = complete_matches,
tcg_self_boot = self_boot
@@ -170,7 +171,8 @@ deSugar hsc_env
(hsc_logger hsc_env)
(initTicksConfig (hsc_dflags hsc_env))
mod mod_loc
- export_set (typeEnvTyCons type_env) binds
+ export_set (typeEnvTyCons type_env)
+ insts inst_meths binds
else return (binds, Nothing)
; let modBreaks
| Just (_, _, breakpointSpecs) <- m_tickInfo
=====================================
compiler/GHC/HsToCore/Ticks.hs
=====================================
@@ -5,7 +5,7 @@
{-
(c) Galois, 2006
(c) University of Glasgow, 2007
-(c) Florian Ragwitz, 2025
+(c) Florian Ragwitz, 2025-2026
-}
module GHC.HsToCore.Ticks
@@ -24,6 +24,8 @@ import GHC.Unit
import GHC.Core.Type
import GHC.Core.TyCon
+import GHC.Core.InstEnv
+import GHC.Core.Class
import GHC.Data.Maybe
import GHC.Data.FastString
@@ -56,6 +58,7 @@ import Data.Foldable (toList)
import Trace.Hpc.Mix
import Data.Bifunctor (second)
+import Data.Functor
import Data.List.NonEmpty (NonEmpty (..))
import Data.Set (Set)
import qualified Data.Set as Set
@@ -100,11 +103,15 @@ addTicksToBinds
-- isExportedId doesn't work yet (the desugarer
-- hasn't set it), so we have to work from this set.
-> [TyCon] -- ^ Type constructors in this module
+ -> [ClsInst] -- ^ Class instances in this module
+ -> IdEnv DFunId -- ^ Mapping from class method Ids to the DFunIds
+ -- of the class instance they belong to. See Note
+ -- [Instance Method Coverage].
-> LHsBinds GhcTc
-> IO (LHsBinds GhcTc, Maybe (FilePath, SizedSeq Tick, SizedSeq Tick))
addTicksToBinds logger cfg
- mod mod_loc exports tyCons binds
+ mod mod_loc exports tyCons insts inst_meths binds
| let passes = ticks_passes cfg
, not (null passes)
, Just orig_file <- ml_hs_file mod_loc = do
@@ -129,8 +136,13 @@ addTicksToBinds logger cfg
, this_mod = mod
, tickishType = tickish
, recSelBinds = emptyVarEnv
+ , instMeths = inst_meths
+ , instTicks = emptyVarEnv
}
- (binds',_,st') = unTM (addTickLHsBinds binds) env st
+ addTick = do
+ instTicks <- allocInstTicks insts
+ withEnv (\e -> e{ instTicks }) $ addTickLHsBinds binds
+ (binds',_,st') = unTM addTick env st
in (binds', st')
(binds1,st) = foldr tickPass (binds, initTTState) passes
@@ -231,7 +243,7 @@ addTickLHsBind :: LHsBind GhcTc -> TM (LHsBind GhcTc)
addTickLHsBind (L pos (XHsBindsLR bind@(AbsBinds { abs_binds = binds
, abs_exports = abs_exports
}))) =
- withEnv (add_rec_sels . add_inlines . add_exports) $ do
+ withEnv (add_inst_meths . add_rec_sels . add_inlines . add_exports) $ do
binds' <- addTickLHsBinds binds
return $ L pos $ XHsBindsLR $ bind { abs_binds = binds' }
where
@@ -259,19 +271,24 @@ addTickLHsBind (L pos (XHsBindsLR bind@(AbsBinds { abs_binds = binds
| ABE{ abe_poly, abe_mono } <- abs_exports
, RecSelId{} <- [idDetails abe_poly] ] }
+ add_inst_meths env =
+ env{ instMeths = instMeths env `extendVarEnvList`
+ [ (abe_mono, dfun)
+ | ABE{ abe_poly, abe_mono } <- abs_exports
+ , Just dfun <- [lookupVarEnv (instMeths env) abe_poly] ] }
+
addTickLHsBind (L pos (funBind@(FunBind { fun_id = L _ id, fun_matches = matches }))) = do
let name = getOccString id
decl_path <- getPathEntry
density <- getDensity
+ env <- getEnv
- inline_ids <- liftM inlines getEnv
-- See Note [inline sccs]
let inline = isInlinePragma (idInlinePragma id)
- || id `elemVarSet` inline_ids
+ || id `elemVarSet` inlines env
-- See Note [inline sccs]
- tickish <- tickishType `liftM` getEnv
- case tickish of { ProfNotes | inline -> return (L pos funBind); _ -> do
+ case tickishType env of { ProfNotes | inline -> return (L pos funBind); _ -> do
-- See Note [Record-selector ticks]
selTicks <- recSelTick id
@@ -294,16 +311,26 @@ addTickLHsBind (L pos (funBind@(FunBind { fun_id = L _ id, fun_matches = matches
toplev = null decl_path
exported = idName id `elemNameSet` exported_names
- tick <- if not blackListed &&
- shouldTickBind density toplev exported simple inline
+ -- See Note [Instance Method Coverage].
+ let generatedInstMeth = density == TickForCoverage
+ && id `elemVarEnv` instMeths env
+ && isGenerated (mg_origin (mg_ext matches))
+
+ tick <- if not generatedInstMeth
+ && not blackListed
+ && shouldTickBind density toplev exported simple inline
then
bindTick density name (locA pos) fvs
else
return Nothing
+ -- See Note [Instance Method Coverage].
+ instTick <- instMethTick id
+
let mbCons = maybe Prelude.id (:)
return $ L pos $ funBind { fun_matches = mg
- , fun_ext = second (tick `mbCons`) (fun_ext funBind) }
+ , fun_ext = second ((instTick `mbCons`) . (tick `mbCons`))
+ (fun_ext funBind) }
} }
where
-- See Note [Record-selector ticks]
@@ -338,6 +365,32 @@ addTickLHsBind (L pos (funBind@(FunBind { fun_id = L _ id, fun_matches = matches
-- This scheme could be extended further in the future, making coverage for
-- positional constructor fields mean that the field was accessed at run-time.
+-- Note [Instance Method Coverage]
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-- Users expect source-level semantics for coverage reports (see #17155,
+-- #15932). That is, code they've written themselves should be marked as covered
+-- iff it is evaluated at run-time. Given that the implementation of class
+-- instances creates top-level bindings for all methods of an instance,
+-- inherited and hand-written, we need to go out of our way to create top-level
+-- tick boxes only for methods which correspond to user-written code.
+--
+-- To do so, 'addTicksToBinds' receives a mapping from class instance 'Id's to
+-- the 'DFunId's of the dictionary backing the associated instance. When
+-- creating tick boxes for top-level bindings, we can then decide if a given
+-- 'FunBind' corresponds to an inherited/generated instance method of a class
+-- and skip creating the tick box for it.
+--
+-- Additionally, we also want to inform the user whether or not their class
+-- instance as a whole has been used at run-time. To do so, we create a single
+-- tick box at the instance head and tick that box when any method of that
+-- instance is evaluated.
+--
+-- As marker/tag-like class instances without any methods would always appear
+-- as uncovered using the above mechanism, we exclude them when creating
+-- instance tick boxes. This could potentially be improved in the future by
+-- tracking if any functions requiring an instance of an empty class have been
+-- evaluated.
+
-- TODO: Revisit this
addTickLHsBind (L pos (pat@(PatBind { pat_lhs = lhs
, pat_rhs = rhs
@@ -1098,6 +1151,8 @@ data TickTransEnv = TTE { fileName :: FastString
, this_mod :: Module
, tickishType :: TickishType
, recSelBinds :: IdEnv DVarSet
+ , instMeths :: IdEnv DFunId
+ , instTicks :: IdEnv CoreTickish
}
-- deriving Show
@@ -1238,6 +1293,18 @@ isBlackListed (RealSrcSpan pos _) = TM $ \ env st -> (Set.member pos (blackList
isBlackListed GeneratedSrcSpan{} = return False
isBlackListed UnhelpfulSpan{} = return False
+allocInstTicks :: [ClsInst] -> TM (IdEnv CoreTickish)
+allocInstTicks insts =
+ ifDensity TickForCoverage (mkVarEnv . catMaybes <$> mapM alloc insts) (pure emptyVarEnv)
+ where
+ alloc ClsInst{ is_dfun, is_cls }
+ | null (classMethods is_cls) = pure Nothing
+ | otherwise = fmap (is_dfun,) <$> allocATickBox (TopLevelBox [getOccString is_dfun])
+ False True (getSrcSpan is_dfun) noFVs
+
+instMethTick :: Id -> TM (Maybe CoreTickish)
+instMethTick id = getEnv <&> \e -> lookupVarEnv (instMeths e) id >>= lookupVarEnv (instTicks e)
+
-- the tick application inherits the source position of its
-- expression argument to support nested box allocations
allocTickBox :: BoxLabel -> Bool -> Bool -> SrcSpan -> TM (HsExpr GhcTc)
=====================================
compiler/GHC/Tc/Module.hs
=====================================
@@ -1766,7 +1766,7 @@ tcTopSrcDecls (HsGroup { hs_tyclds = tycl_decls,
-- Second pass over class and instance declarations,
-- now using the kind-checked decls
traceTc "Tc6" empty ;
- inst_binds <- tcInstDecls2 (tyClGroupTyClDecls tycl_decls) inst_infos ;
+ (inst_binds, inst_meths) <- tcInstDecls2 (tyClGroupTyClDecls tycl_decls) inst_infos ;
-- Foreign exports
traceTc "Tc7" empty ;
@@ -1798,8 +1798,9 @@ tcTopSrcDecls (HsGroup { hs_tyclds = tycl_decls,
, tcg_anns = tcg_anns tcg_env ++ annotations
, tcg_ann_env = extendAnnEnvList (tcg_ann_env tcg_env) annotations
, tcg_fords = tcg_fords tcg_env ++ foe_decls ++ fi_decls
- , tcg_dus = tcg_dus tcg_env `plusDU` usesOnly fo_fvs } } ;
+ , tcg_dus = tcg_dus tcg_env `plusDU` usesOnly fo_fvs
-- tcg_dus: see Note [Newtype constructor usage in foreign declarations]
+ , tcg_inst_meths = tcg_inst_meths tcg_env `plusVarEnv` inst_meths } } ;
-- See Note [Newtype constructor usage in foreign declarations]
addUsedGREs NoDeprecationWarnings (bagToList fo_gres) ;
=====================================
compiler/GHC/Tc/TyCl/Instance.hs
=====================================
@@ -1285,7 +1285,7 @@ takes a slightly different approach.
********************************************************************* -}
tcInstDecls2 :: [LTyClDecl GhcRn] -> [InstInfo GhcRn]
- -> TcM (LHsBinds GhcTc)
+ -> TcM (LHsBinds GhcTc, IdEnv DFunId)
-- (a) From each class declaration,
-- generate any default-method bindings
-- (b) From each instance decl
@@ -1302,11 +1302,11 @@ tcInstDecls2 tycl_decls inst_decls
-- Add the default method Ids (again)
-- (they were already added in GHC.Tc.TyCl.Utils.tcAddImplicits)
-- See Note [Default methods in the type environment]
- ; inst_binds_s <- tcExtendGlobalValEnv dm_ids $
- mapM tcInstDecl2 inst_decls
+ ; (inst_binds_s, inst_meths_s) <- unzip <$> (tcExtendGlobalValEnv dm_ids $
+ mapM tcInstDecl2 inst_decls)
-- Done
- ; return (dm_binds ++ concat inst_binds_s) }
+ ; return (dm_binds ++ concat inst_binds_s, plusVarEnvList inst_meths_s) }
{- Note [Default methods in the type environment]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1322,12 +1322,12 @@ So right here in tcInstDecls2 we must re-extend the type envt with
the default method Ids replete with their INLINE pragmas. Urk.
-}
-tcInstDecl2 :: InstInfo GhcRn -> TcM (LHsBinds GhcTc)
+tcInstDecl2 :: InstInfo GhcRn -> TcM (LHsBinds GhcTc, IdEnv DFunId)
-- Returns a binding for the dfun
tcInstDecl2 (InstInfo { iSpec = ispec, iBinds = ibinds })
- = recoverM (return emptyLHsBinds) $
- setSrcSpan loc $
- addErrCtxt (instDeclCtxt2 dfun_ty) $
+ = recoverM (return (emptyLHsBinds, emptyVarEnv)) $
+ setSrcSpan loc $
+ addErrCtxt (instDeclCtxt2 dfun_ty) $
do { -- Instantiate the instance decl with skolem constants
(skol_info, inst_tyvars, dfun_theta, clas, inst_tys) <- tcSkolDFunType dfun_ty
; dfun_ev_vars <- newEvVars dfun_theta
@@ -1345,7 +1345,7 @@ tcInstDecl2 (InstInfo { iSpec = ispec, iBinds = ibinds })
-- See Note [Typechecking plan for instance declarations]
; dfun_ev_binds_var <- newTcEvBinds
; let dfun_ev_binds = TcEvBinds dfun_ev_binds_var
- ; (tclvl, (sc_meth_ids, sc_meth_binds, sc_meth_implics))
+ ; (tclvl, (sc_meth_ids, sc_meth_binds, sc_meth_implics, meth_ids))
<- pushTcLevelM $
do { (sc_ids, sc_binds, sc_implics)
<- tcSuperClasses skol_info dfun_id clas inst_tyvars
@@ -1359,7 +1359,8 @@ tcInstDecl2 (InstInfo { iSpec = ispec, iBinds = ibinds })
; return ( sc_ids ++ meth_ids
, sc_binds ++ meth_binds
- , sc_implics `unionBags` meth_implics ) }
+ , sc_implics `unionBags` meth_implics
+ , meth_ids ) }
; imp <- newImplication
; emitImplication $
@@ -1416,7 +1417,7 @@ tcInstDecl2 (InstInfo { iSpec = ispec, iBinds = ibinds })
, abs_binds = [dict_bind]
, abs_sig = True }
- ; return (L loc' main_bind : sc_meth_binds)
+ ; return (L loc' main_bind : sc_meth_binds, mkVarEnv $ (,dfun_id) <$> meth_ids)
}
where
dfun_id = instanceDFunId ispec
=====================================
compiler/GHC/Tc/Types.hs
=====================================
@@ -157,6 +157,7 @@ import GHC.Types.Name.Env
import GHC.Types.Name.Set
import GHC.Types.Avail
import GHC.Types.Var
+import GHC.Types.Var.Env
import GHC.Types.TypeEnv
import GHC.Types.SourceFile
import GHC.Types.SrcLoc
@@ -530,6 +531,9 @@ data TcGblEnv
-- NB. BangPattern is to fix a leak, see #15111
tcg_fam_inst_env :: !FamInstEnv, -- ^ Ditto for family instances
-- NB. BangPattern is to fix a leak, see #15111
+ tcg_inst_meths :: IdEnv DFunId,
+ -- ^ Maps instance method Ids to the DFunId of the instance they belong to.
+ -- See Note [Class Instance Coverage] in GHC.HsToCore.Ticks.
tcg_ann_env :: AnnEnv, -- ^ And for annotations
tcg_complete_match_env :: CompleteMatches,
-- ^ The complete matches for all /home-package/ modules;
=====================================
compiler/GHC/Tc/Utils/Monad.hs
=====================================
@@ -409,6 +409,7 @@ initTcGblEnv hsc_env hsc_src keep_rn_syntax mod loc =
, tcg_knot_vars = hsc_type_env_vars hsc_env
, tcg_known_key_maps = known_key_maps_var
, tcg_inst_env = emptyInstEnv
+ , tcg_inst_meths = emptyVarEnv
, tcg_fam_inst_env = emptyFamInstEnv
, tcg_ann_env = emptyAnnEnv
, tcg_complete_match_env = []
=====================================
compiler/GHC/Unit/State.hs
=====================================
@@ -369,10 +369,15 @@ data UnitState = UnitState {
-- is always mentioned before the units it depends on.
preloadUnits :: [UnitId],
- -- | Units which we explicitly depend on (from a command line flag).
- -- We'll use this to generate version macros and the unused packages warning. The
- -- original flag which was used to bring the unit into scope is recorded for the
- -- -Wunused-packages warning.
+ -- | The units that can be used by the respective home unit. Despite the
+ -- identifier, this does not just cover units that are enabled explicitly
+ -- using flags but also units from the package database that are enabled
+ -- implicitly in case @-hide-all-packages@ is absent. The former are paired
+ -- with 'Just' applied to the respective flags, the latter are paired with
+ -- 'Nothing'.
+ --
+ -- This field is used for generating version macros and “unused packages”
+ -- warnings.
explicitUnits :: [(Unit, Maybe PackageArg)],
homeUnitDepends :: Set UnitId,
@@ -907,7 +912,7 @@ mkUnitState logger unit_index cfg = do
let pkgname_map = listToUFM [ (unitPackageName p, unitInstanceOf p)
| p <- pkgs2
]
- -- The explicitUnits accurately reflects the set of units we have turned
+ -- The explicitUnits accurately reflects the set of units that are turned
-- on; as such, it also is the only way one can come up with requirements.
-- The requirement context is directly based off of this: we simply
-- look for nested unit IDs that are directly fed holes: the requirements
=====================================
rts/include/stg/Prim.h
=====================================
@@ -59,16 +59,16 @@ StgWord hs_xchg32(StgWord x, StgWord val);
StgWord64 hs_xchg64(StgWord x, StgWord64 val);
/* rts/prim/bswap.c */
-StgWord16 hs_bswap16(StgWord16 x);
-StgWord32 hs_bswap32(StgWord32 x);
+StgWord hs_bswap16(StgWord x);
+StgWord hs_bswap32(StgWord x);
StgWord64 hs_bswap64(StgWord64 x);
/* rts/prim/bitrev.c
This was done as part of issue #16164.
See Note [Bit reversal primop] for more details about the implementation.*/
StgWord hs_bitrev8(StgWord x);
-StgWord16 hs_bitrev16(StgWord16 x);
-StgWord32 hs_bitrev32(StgWord32 x);
+StgWord hs_bitrev16(StgWord x);
+StgWord hs_bitrev32(StgWord x);
StgWord64 hs_bitrev64(StgWord64 x);
/* rts/prim/longlong.c */
=====================================
rts/prim/bitrev.c
=====================================
@@ -36,8 +36,8 @@ hs_bitrev8(StgWord x)
return x;
}
-StgWord16
-hs_bitrev16(StgWord16 x)
+StgWord
+hs_bitrev16(StgWord x)
{
x = ((x >> 1) & 0x5555) | ((x & 0x5555) << 1 );
x = ((x >> 2) & 0x3333) | ((x & 0x3333) << 2 );
@@ -46,14 +46,14 @@ hs_bitrev16(StgWord16 x)
return x;
}
-StgWord32
-hs_bitrev32(StgWord32 x)
+StgWord
+hs_bitrev32(StgWord x)
{
- x = ((x >> 1) & 0x55555555) | ((x & 0x55555555) << 1 );
- x = ((x >> 2) & 0x33333333) | ((x & 0x33333333) << 2 );
- x = ((x >> 4) & 0x0F0F0F0F) | ((x & 0x0F0F0F0F) << 4 );
- x = ((x >> 8) & 0x00FF00FF) | ((x & 0x00FF00FF) << 8 );
- x = ( x >> 16 ) | ( x << 16 );
+ x = ((x >> 1) & 0x55555555) | ((x & 0x55555555) << 1 );
+ x = ((x >> 2) & 0x33333333) | ((x & 0x33333333) << 2 );
+ x = ((x >> 4) & 0x0F0F0F0F) | ((x & 0x0F0F0F0F) << 4 );
+ x = ((x >> 8) & 0x00FF00FF) | ((x & 0x00FF00FF) << 8 );
+ x = ((x >> 16) & 0x0000FFFF) | ((x & 0x0000FFFF) << 16);
return x;
}
=====================================
rts/prim/bswap.c
=====================================
@@ -1,16 +1,18 @@
#include "Rts.h"
-StgWord16
-hs_bswap16(StgWord16 x)
+StgWord
+hs_bswap16(StgWord x)
{
- return ((x >> 8) | (x << 8));
+ x = (StgWord16)x;
+ return (StgWord16)((x >> 8) | (x << 8));
}
-StgWord32
-hs_bswap32(StgWord32 x)
+StgWord
+hs_bswap32(StgWord x)
{
- return ((x >> 24) | ((x >> 8) & 0xff00) |
- (x << 24) | ((x & 0xff00) << 8));
+ x = (StgWord32)x;
+ return (StgWord32)((x >> 24) | ((x >> 8) & 0xff00) |
+ (x << 24) | ((x & 0xff00) << 8));
}
StgWord64
=====================================
testsuite/tests/ghci/prog-mhu008/Makefile
=====================================
@@ -0,0 +1,3 @@
+TOP=../../..
+include $(TOP)/mk/boilerplate.mk
+include $(TOP)/mk/test.mk
=====================================
testsuite/tests/ghci/prog-mhu008/a/M.hs
=====================================
@@ -0,0 +1,4 @@
+module M (T (..)) where
+
+newtype T = MkT Int
+ deriving (Eq, Ord)
=====================================
testsuite/tests/ghci/prog-mhu008/all.T
=====================================
@@ -0,0 +1,5 @@
+test('prog-mhu008',
+ [extra_files(['a/', 'b/', 'unitA', 'unitB']),
+ cmd_prefix('ghciWayFlags=' + config.ghci_way_flags),
+ req_interp],
+ ghci_multiunit_script, [['unitA', 'unitB'], 'prog-mhu008.script'])
=====================================
testsuite/tests/ghci/prog-mhu008/b/M.hs
=====================================
@@ -0,0 +1,11 @@
+{-# LANGUAGE PackageImports #-}
+
+module M
+ ( module Export
+ , cmp
+ ) where
+
+import "a" M as Export
+
+cmp :: T -> T -> Bool
+cmp x y = x <= y
=====================================
testsuite/tests/ghci/prog-mhu008/prog-mhu008.script
=====================================
@@ -0,0 +1,5 @@
+:set -XPackageImports
+import "a" M qualified as MA
+import "b" M qualified as MB
+
+MB.cmp (MA.MkT 1) (MA.MkT 2)
=====================================
testsuite/tests/ghci/prog-mhu008/prog-mhu008.stdout
=====================================
@@ -0,0 +1 @@
+True
=====================================
testsuite/tests/ghci/prog-mhu008/unitA
=====================================
@@ -0,0 +1,5 @@
+-this-unit-id a-0-inplace
+-this-package-name a
+-i
+-ia
+M
=====================================
testsuite/tests/ghci/prog-mhu008/unitB
=====================================
@@ -0,0 +1,6 @@
+-this-unit-id b-0-inplace
+-this-package-name b
+-package-id a-0-inplace
+-i
+-ib
+M
=====================================
testsuite/tests/hpc/instmeths/instmeths.hs
=====================================
@@ -0,0 +1,57 @@
+import Data.List
+import Data.Maybe
+import Trace.Hpc.Mix
+import Trace.Hpc.Reflect
+import Trace.Hpc.Tix
+import Trace.Hpc.Util
+
+class Foo a where
+ defMeth, defMeth', reqMeth :: a -> String
+ defMeth = const "class default"
+ defMeth' = const "class default"
+
+newtype T a = T a deriving (Show, Eq, Functor)
+instance Foo (T a) where
+ reqMeth = const "Foo (T a) instance"
+
+newtype T' a = T' a
+
+instance Foo (T' a) where
+ reqMeth = const "Foo (T' a) instance"
+ defMeth = const "Foo (T' a) instance"
+
+-- no method use could "cover" this instance, so we (for now) omit the top-level box for it
+class Marker a
+instance Marker (T Bool)
+
+interesting :: (Int, MixEntry) -> Maybe (Int, HpcPos, String)
+interesting (n, (pos, TopLevelBox [name])) | isInteresting name = Just (n, pos, name)
+ where isInteresting = (||) <$> isInst <*> isMeth
+ isInst = ("$f" `isPrefixOf`)
+ isMeth = (||) <$> (`elem` ["(==)", "(/=)", "show", "showList", "showsPrec", "fmap", "(<$)"])
+ <*> ("Meth" `isSubsequenceOf`)
+interesting _ = Nothing
+
+-- candidate for HPC.Utils, maybe?
+sourceAt :: String -> HpcPos -> String
+sourceAt src pos
+ | l1 == l2 = take (c2 - c1 + 1) . drop (c1 - 1) $ ls !! (l1 - 1)
+ | otherwise = intercalate "\n" $ first : (middle ++ [last])
+ where
+ (l1, c1, l2, c2) = fromHpcPos pos
+ ls = lines src
+ first = drop (c1 - 1) $ ls !! (l1 - 1)
+ middle = take (l2 - l1 - 1) $ drop l1 ls
+ last = take c2 $ ls !! (l2 - 1)
+
+main :: IO ()
+main = do
+ print (T 23 == (succ <$> T 22)) -- tick Eq and Functor by using any of their methods
+ print (reqMeth (T' 1)) -- tick `Foo (T' a)` and that instance's `reqMeth`
+
+ Mix source _ _ _ mixEntries <- readMix [".hpc"] (Left "Main")
+ src <- readFileUtf8 source
+ let boxes = mapMaybe interesting $ zip [0 ..] mixEntries
+ Tix [TixModule "Main" _ _ counts] <- examineTix
+ mapM_ print [ (counts !! n, path, sourceAt src pos)
+ | (n, pos, path) <- sortOn (\(_, _, s) -> s) boxes ]
=====================================
testsuite/tests/hpc/instmeths/instmeths.stdout
=====================================
@@ -0,0 +1,12 @@
+True
+"Foo (T' a) instance"
+(1,"$fEqT","Eq")
+(0,"$fFooT","Foo (T a)")
+(1,"$fFooT'","Foo (T' a)")
+(1,"$fFunctorT","Functor")
+(0,"$fShowT","Show")
+(0,"defMeth","defMeth = const \"class default\"")
+(0,"defMeth","defMeth = const \"Foo (T' a) instance\"")
+(0,"defMeth'","defMeth' = const \"class default\"")
+(0,"reqMeth","reqMeth = const \"Foo (T a) instance\"")
+(1,"reqMeth","reqMeth = const \"Foo (T' a) instance\"")
=====================================
testsuite/tests/hpc/instmeths/test.T
=====================================
@@ -0,0 +1,5 @@
+setTestOpts([omit_ghci, when(fast(), skip), js_skip])
+
+test('instmeths',
+ [ignore_extension],
+ compile_and_run, ['-fhpc'])
=====================================
testsuite/tests/primops/should_run/T27299.hs
=====================================
@@ -0,0 +1,23 @@
+{-# LANGUAGE MagicHash #-}
+import GHC.Exts
+import GHC.Word
+import Numeric
+
+main :: IO ()
+main = do
+ putStrLn $ showHex (W# (bitReverse8# 0x55##)) ""
+ putStrLn $ showHex (W# (bitReverse8# 0xAA##)) ""
+ putStrLn $ showHex (W# (bitReverse16# 0x5555##)) ""
+ putStrLn $ showHex (W# (bitReverse16# 0xAAAA##)) ""
+ putStrLn $ showHex (W# (bitReverse32# 0x5555_5555##)) ""
+ putStrLn $ showHex (W# (bitReverse32# 0xAAAA_AAAA##)) ""
+ putStrLn $ showHex (W# (byteSwap16# 0xABCD##)) ""
+ putStrLn $ showHex (W# (byteSwap32# 0xABCD_EF12##)) ""
+ putStrLn $ showHex (W# (noinline bitReverse8# 0x55##)) ""
+ putStrLn $ showHex (W# (noinline bitReverse8# 0xAA##)) ""
+ putStrLn $ showHex (W# (noinline bitReverse16# 0x5555##)) ""
+ putStrLn $ showHex (W# (noinline bitReverse16# 0xAAAA##)) ""
+ putStrLn $ showHex (W# (noinline bitReverse32# 0x5555_5555##)) ""
+ putStrLn $ showHex (W# (noinline bitReverse32# 0xAAAA_AAAA##)) ""
+ putStrLn $ showHex (W# (noinline byteSwap16# 0xABCD##)) ""
+ putStrLn $ showHex (W# (noinline byteSwap32# 0xABCD_EF12##)) ""
=====================================
testsuite/tests/primops/should_run/T27299.stdout
=====================================
@@ -0,0 +1,16 @@
+aa
+55
+aaaa
+5555
+aaaaaaaa
+55555555
+cdab
+12efcdab
+aa
+55
+aaaa
+5555
+aaaaaaaa
+55555555
+cdab
+12efcdab
=====================================
testsuite/tests/primops/should_run/all.T
=====================================
@@ -91,3 +91,4 @@ test('T24496'
]
, compile_and_run, ['-O'])
test('T6026', normal, compile_and_run, [''])
+test('T27299', normal, compile_and_run, [''])
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/c97d7901a78d7237ac4e5c515542d1…
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/c97d7901a78d7237ac4e5c515542d1…
You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
1
0
[Git][ghc/ghc] Pushed new branch wip/sjakobi/T27847-rethrow-sites
by Simon Jakobi (@sjakobi) 25 Sep '26
by Simon Jakobi (@sjakobi) 25 Sep '26
25 Sep '26
Simon Jakobi pushed new branch wip/sjakobi/T27847-rethrow-sites at Glasgow Haskell Compiler / GHC
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/tree/wip/sjakobi/T27847-rethrow-sites
You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
1
0
[Git][ghc/ghc][wip/jeltsch/explicit-units-to-available-units] 45 commits: rts: Fix missing memory barrier in eval_thunk_selector (#27477)
by Wolfgang Jeltsch (@jeltsch) 25 Sep '26
by Wolfgang Jeltsch (@jeltsch) 25 Sep '26
25 Sep '26
Wolfgang Jeltsch pushed to branch wip/jeltsch/explicit-units-to-available-units at Glasgow Haskell Compiler / GHC
Commits:
06eee015 by Luite Stegeman at 2026-09-08T06:03:06+02:00
rts: Fix missing memory barrier in eval_thunk_selector (#27477)
unchain_thunk_selectors() was missing an ACQUIRE_LOAD for the
indirectee, leading to segfaults and corruption during GC on
weakly-ordered architectures.
Fixes #27477
- - - - -
f8b2bd8f by Alan Zimmerman at 2026-09-08T09:39:12-04:00
EPA Fix HsCmdDo exact print with comments
Exact printing of HsCmdDo was ignoring the location for the do
statements, and this is an annotation that can have comments in it.
Update it so we print the statements as a unit, including any
comments.
Also add the result of auditing that we capture comments in all needed
places, noting that the remaining Anno SrcSpan instances are benign.
- - - - -
430967ab by Duncan Coutts at 2026-09-09T11:07:01+01:00
Reorder cmm decls in HeapStackCheck for a better logical grouping
And put more section headers in to deliniate the groups.
We're about to add more here, so better to organise it first.
- - - - -
344080fd by Duncan Coutts at 2026-09-09T11:07:01+01:00
Add raisePrimIOException and add it to RTS<->ghc-internal API
The raisePrimIOException is a new helper function that I/O primops will
use to help them report I/O errors. This is implemented in Haskell
(since that's a lot easier), but has a calling convention that is
easy(ish) to use from Cmm in the I/O primops.
So we add it to the RTS API struct, and since we'll use it from Cmm we
also need a field accessor macro for cmm (in deriveConstants).
See the Note about how we cannot have nice things due to async
exceptions and thunks preventing us from using catch.
- - - - -
cfdb3399 by Duncan Coutts at 2026-09-09T11:07:01+01:00
Add new blocking functions for I/O primops
See the Note [Thread blocking for new I/O primops],
and the Note [Calling convention for raisePrimIOException].
The point is, it will allow us to report synchronous exceptions from I/O
primops, and do so much more flexibly.
Previously the I/O managers could only report async exceptions and only
nullary exceptions. This was OK historically, but no good as we add
more I/O managers and expand the range of I/O operations we support.
- - - - -
f09c09d8 by Duncan Coutts at 2026-09-09T11:07:01+01:00
Change the encoding of results from the I/O manager to I/O primops
Previously we just had async continue or heap overflow.
We now extend what we can report with synchronous success, and
synchronous failure with an errno.
See Note [Encoding of result of I/O manager operations]
We don't use these two new cases yet, but we will. In particular an
epoll I/O manager needs to be able to report synchronous success or
failure for waitRead#/waitWrite#.
- - - - -
30f074d9 by Duncan Coutts at 2026-09-09T11:07:01+01:00
Switch waitRead/Write# to use new blocking return frames
and update the I/O managers to set the result before resuming the
blocked threads.
This makes it possible for I/O managers to report synchronous exceptions
from the I/O primops, but that will be done in a subsequent commit.
- - - - -
a8786ced by Duncan Coutts at 2026-09-09T11:07:01+01:00
Switch Poll and Select I/O managers to report sync exceptions
rather than using raiseAsync with blockedOnBadFD_closure.
This uses the new mechanism in the blocking frame return code to report
synchronous exceptions.
- - - - -
7a9df79a by Duncan Coutts at 2026-09-09T11:07:01+01:00
Remove now-unused blockedOnBadFD
It was previously thrown by the select and poll I/O managers, but now
they use raisePrimIOException (with an EBADF errno).
- - - - -
6b0424e3 by Duncan Coutts at 2026-09-09T11:07:01+01:00
Improve the docs for delay# waitRead# and waitWrite#
Document that the waitRead/Write# can throw exceptions (this was true
before too), and that all of them are async exception cancellation
points.
- - - - -
afeb2c30 by Duncan Coutts at 2026-09-09T20:08:58-04:00
Enable printf warnings for trace functions and fix resulting warnings
Most of the existing printf-style functions are annotated with
attributes to enable gcc/clang warnings for the printf format string,
but several trace functions in Trace.h were missing this annotation.
Enable them, and fix the resulting warnings.
- - - - -
9a442c93 by Alan Zimmerman at 2026-09-09T20:09:36-04:00
EPA: Exact print ConDeclGADT without custom enterAnn
!16321 brought in explicit capture of parens in a ConDeclGADT.
The ExactPrint update introduced a modification of the fundamental
function in exact printing, `enterAnn`, by splitting it into a version
allowing injection of functionality normally handled by the
ExactPrint class methods.
This commit refactors that code, to restore the prior `enterAnn`
version, by following the convention in ExactPrint of introducing a
helper data structure with its own `ExactPrint` instance to achieve
the same effect.
- - - - -
4e9297ea by Simon Jakobi at 2026-09-11T08:32:45-04:00
ci: Actually pass --ignore-perf-failures to the testsuite driver
684c0018d9 changed the IGNORE_PERF_FAILURES handling to append to
RUNTEST_ARGS with bash array syntax, but the three use sites still
expand RUNTEST_ARGS as a scalar, which yields only the first array
element. The appended flag was therefore dropped in every job, so
perf improvements kept failing Marge Bot batches and master pipelines,
contrary to #19562.
Append as a string instead, matching the existing --force-colors
append in test_hadrian.
Fixes #27563.
Assisted-by: Claude Fable 5.1
- - - - -
4f78dfdf by Simon Jakobi at 2026-09-11T08:33:24-04:00
testsuite: Give T26537 a 1.5x compile timeout
Its -fregs-graph compile step tends to time out in unoptimized builds
(#27188, #27776).
Assisted-by: Claude Fable 5.1
- - - - -
130286e3 by Alan Zimmerman at 2026-09-11T08:34:08-04:00
EPA: Remove ListBanana / ListParens from AnnListBrackets
This is a step towards making AnnList only represent
lists of items which can have either braces or layout.
- - - - -
a9827d13 by Simon Jakobi at 2026-09-11T18:53:32-04:00
Reject ~, UNPACK and multiplicity annotations on type data fields
Only `!` was rejected so far. `~`, UNPACK and multiplicity annotations
were silently accepted. None of them make sense at the type level, so
restriction (R3) of Note [Type data declarations] now covers all of
them.
Fixes #27732.
Assisted-by: Claude Fable 5.1
- - - - -
c71d5e23 by Alan Zimmerman at 2026-09-11T18:54:11-04:00
EPA: Add a Note for the exact print main loop
- - - - -
7a108e43 by Simon Jakobi at 2026-09-12T18:35:59-04:00
FastString: Drop mkFastStringWith's constructor callback
All three callers passed the same callback, a partial application of
mkNewFastStringShortByteString to the string being interned. That
partial application is allocated as a closure before the table lookup,
on the common hit path too, although the callback is needed only after
a miss.
Drop the parameter and call mkNewFastStringShortByteString directly
after a miss. Since nothing is passed "with" anymore, rename the
function to internSB.
Suggested by Simon PJ in #27528:
https://gitlab.haskell.org/ghc/ghc/-/work_items/27528#note_687031
Assisted-by: Claude Fable 5
- - - - -
82c73b22 by Alan Zimmerman at 2026-09-12T18:36:38-04:00
EPA: More targeted HsDo exact print annotation
HsDo is multi-purpose, as encoded in its HsDoFlavour field. Some of
these are in a layout context (DoExpr, MDoExpr), others are not
(ListComp, MonadComp).
We are moving towards using AnnList only in layout contexts, so we
switch the HsDo TTG annotation from holding an AnnList for this, to
holding
Either (EpToken "[", EpToken "]") AnnList
This also allows us to trim down AnnListBrackets to only have braces
or None, thereby opening the door for unification with the existing
layout context data type EpLayout.
- - - - -
749d2448 by Luite Stegeman at 2026-09-15T14:07:01-04:00
rts: handle large CONSTR closures in compacting GC
The function update_fwd_large in the compacting GC could run into
an unexpected object with the following error:
internal error: update_fwd_large: unknown/strange object 1
Closure type 1 is CONSTR. This patch adds cases for CONSTR and
CONSTR_NOCAF.
This is the same problem as #27434 (large AP closures, fixed by
cca0d58963), which missed this case.
Fixes #27649
- - - - -
177d6155 by Luite Stegeman at 2026-09-15T14:07:02-04:00
testsuite: fix T27434 test with compacting GC
Ensure that the interpreter is actually run with the compacting GC
- - - - -
5796aa63 by Luite Stegeman at 2026-09-15T14:07:49-04:00
rts: initialise the stack frame header for mask_frame and apply_mask_frame
We must leave the stack in consistent state before jumping to
mask_frame or apply_mask_frame because they may result. Failing
to do so could lead to a crash if there were waiting exceptions.
Fixes #27651
- - - - -
2486cc92 by Simon Peyton Jones at 2026-09-15T14:08:36-04:00
Re-introduce the "weird SpecDict" case
In this commit
commit f80375dd4945350a1d784e436975b48b9c736eaf
Author: Simon Peyton Jones <simon.peytonjones(a)gmail.com>
Date: Sun Jun 29 15:26:58 2025 +0100
Refactor of Specialise.hs
I removed a test for a dictionary with unbound type variables. This
turned out to be wrong; see #27629.
So this MR re-introduces it.
- - - - -
8e09a46f by Duncan Coutts at 2026-09-15T20:11:35-04:00
Refactor (and rename) removeFromQueues, to simplify I/O managers
Rename it to unblockAndAppendToRunQueue which better reflects what it is
intended to do.
The post-condition for unblockAndAppendToRunQueue is that the TSO is on
the run queue or it is in the process of migrating to another cap.
Previously it achieved that by always directly adding the TSO to the run
queue itself.
But this actually made things more complicated for the I/O managers,
because it meant they needed a separate code path for notifying for
cancellation compared to notifying for completion. The general
notification code would always add the TSO to the run queue itself.
So the improvement is to allow different cases in
unblockAndAppendToRunQueue to achieve the same outcome in different
ways: either directly adding to the run queue or calling helper
functions that do so themselves.
This then allows the new I/O managers to share code between the sync and
async cancellation, and to reuse their notifyIOCompletion helpers for
cancellation. This avoids a source of bugs where the completion path may
be updated but the cancellation path may be forgotten, or similarly in
future for sync/async operations.
Update all the existing in-RTS I/O managers, and the posix timeout code.
- - - - -
d668782c by Duncan Coutts at 2026-09-15T20:11:35-04:00
Document that awaitCompletedTimeoutsOrIO expects an empty run queue
This was true before but implicit and not relied on much. It's better to
be explicit, and allow things to depend on it.
- - - - -
11d938bb by Duncan Coutts at 2026-09-15T20:11:35-04:00
Store the I/O opcode and fd in the StgAsyncIOOp
This will be useful in several I/O managers and it is handy for logging
and debugging.
It also doesn't increase the size of the StgAsyncIOOp structure. There
was enough spare padding space already.
Update the poll I/O manager to set the new fields.
Add a helper function to convert the enum IOReadOrWrite into the enum
IOOpCode. Also change IOReadOrWrite to be an enum without a typedef, for
consistency with other enumerations in IOManager.h
- - - - -
bfa87882 by Duncan Coutts at 2026-09-15T20:11:36-04:00
Add a new I/O manager based on select()
Yes, this is the second such I/O manager, but it is a modern
re-implementation based on the new in-RTS I/O manager infrastructure. So
it is cleaner and faster than the old select I/O manager.
Why do we need another I/O manager based on select? Why isn't the poll()
one good enough as a baseline portable unix I/O manager? Because macOS.
Apple Inc. is why we cannot have nice things.
The man page for poll on macOS documents the fact that it does not work.
At least, it does not work for all files. Specifically, it does not work
for device files. Whereas macOS select() does work for device files.
Aaaaarg!
We _do_ want to deprecate and remove the old select I/O manager, but due to
macOS we cannot do that until we have a replacement. This is that
replacement. Until of course a nice new k-queue I/O manager arrives,
which could become the new default for macOS and FreeBSD.
Interestingly, this select I/O manager is actually faster than the poll
one, on Linix, in some circumstances: specifically when many Haskell
threads are waiting on the same fd. The poll I/O manager does O(n) work
for n threads waiting on I/O, whereas the select one does O(fds) work
for the number of fds that threads are waiting on. Usually this is 1:1,
so it's not noticable, but one can concoct extreme benchmarks to show
the difference.
- - - - -
8e9de15a by Duncan Coutts at 2026-09-15T20:11:36-04:00
Minor updates in the poll I/O manager to keep in sync with select
This keeps it in sync with select one. The changes are based on code review
while implementing the new select I/O manager. The two I/O managers are so
similar in structure that it makes sense to try to minimise the diff between
them. This should aid understanding, and fixes to both in future.
- - - - -
222668bb by Duncan Coutts at 2026-09-15T20:11:36-04:00
Document the new select I/O manager in the user guide
in the RTS section about I/O managers.
And add a changelog entry.
- - - - -
0f48735b by Cheng Shao at 2026-09-15T20:12:23-04:00
Revert "wasm: workaround WebKit bug in dyld"
This reverts commit c9b8465c2c338176fcab9d197e9d31f4aee11f68. The
underlying webkit bug has been fixed in
https://commits.webkit.org/314219@main. Closes #27564.
- - - - -
809294f1 by Luite Stegeman at 2026-09-15T20:13:08-04:00
rts: make stg_threadLabelzh return a valid pointer for unlabeled threads.
This fixes a segfault in the GC caused by stg_threadLabelzh returning
a 0 pointer in a GC pointer field.
stg_threadLabelzh returns a tuple of type (# Int#, ByteArray# #). If a
thread has no label, the second field is unused. We must still return
a valid heap object pointer.
Instead of returning 0, we now return stg_DEAD_SLOT_closure.
fixes #27618
- - - - -
e2d57026 by Luite Stegeman at 2026-09-15T20:13:54-04:00
JS: fix selector thunk handling
fix a bug where entering a selector thunk could lead to a crash
if the selected field was still a thunk.
fixes #27788
- - - - -
1b4a622f by sheaf at 2026-09-17T02:39:13-04:00
Improvements to type/data family handling
This commit makes several improvements to the treatment of type and data
families in GHC:
- We consistently use "type family" instead of "type synonym family"
to avoid a confusion with type synonyms. For example:
- OpenSynFamilyTyCon ==> OpenTypeFamilyTyCon
- ClosedSynFamilyTyCon => ClosedTypeFamilyTyCon
- Various functions that were specific to data families have been
renamed to make this clear. For example:
- tyConFamInst_maybe ==> tyConDataFamInst_maybe
- isFamInstTyCon ==> isDataFamInstTyCon
- 'FamTyConFlav' has been refactored so that all closed type families
share a single 'ClosedTypeFamilyTyCon' constructor.
The corresponding 'IfaceFamTyConFlav' type was updated similarly.
This is then used to tidy up the implementation of
GHC.Core.FamInstEnv.reduceTyFamApp_maybe.
- - - - -
c5aa9098 by sheaf at 2026-09-17T02:39:13-04:00
Small refactor of lookupFamInstEnv
This commit reworks the code for looking up family instances in
GHC.Core.FamInstEnv, reducing duplication a little bit.
No change in behaviour.
- - - - -
a3ad4588 by sheaf at 2026-09-17T02:39:14-04:00
Use a type family for boxing unboxed types
This commit overhauls the way in which unboxed types are boxed, as per
the updated Note [Boxing constructors] in GHC.Builtin.WiredIn.Types.Box.
The main change is to allow boxing to be parametrised by representation,
e.g. making 'Box ty' a valid type. We achieve this by implementing 'Box'
using a closed type family:
newtype Box (a :: TYPE r) = Box (BoxTF r)
type BoxTF :: RuntimeRep -> Type
type family BoxTF r where
BoxTF IntRep = BoxInt
BoxTF Int8Rep = BoxInt8
BoxTF WordRep = BoxWord
BoxTF LiftedRep = Any @Type
BoxTF (TupleRep [r1, r2]) = (BoxTF r1, BoxTF r2)
...
BoxTF (SumRep rs) = BoxSum rs
This new design solves the following problems:
- It allows boxing a type whose 'RuntimeRep' has not yet been
determined (e.g. because we only learn the 'RuntimeRep' after
constraint solving). That is, 'Box @r a' is a perfectly valid type
even if 'r' is an unfilled metavariable. This is a requirement
to fix #25065 (in which we need to box a type whose 'RuntimeRep'
is only filled in during constraint solving).
- It handles boxing of unboxed tuples and unboxed sums, via recursive
boxing. See Note [Boxing TupleRep and SumRep] in
GHC.Builtin.WiredIn.Types.Box.
'Box', 'BoxTF' and all of the boxing data constructors are defined in
a new 'ghc-internal' module, 'GHC.Internal.Box'. This module contains
nothing but wired-in declarations, and GHC never loads its interface on
account of a wired-in thing (GHC.Builtin.Modules.isWiredInOnlyModule);
see Note [Loading instances for wired-in things] in GHC.Iface.Load.
The individual boxing data constructors are deliberately left as an
internal implementation detail, and not exported. The intended API is
via the two new magic 'Id's:
box :: forall {r} (a :: TYPE r). a -> Box a
unbox :: forall {r} (a :: TYPE r). Box a -> a
exported from 'GHC.PrimOps' (in 'ghc-experimental'). The old boxing
data types (IntBox, WordBox, FloatBox, DoubleBox, DictBox) are no longer
exported from base (CLC proposal #421).
We use this new design to consistently desugar:
- parallel comprehensions (GHC.HsToCore.ListComp.{deListComp,dsMcStmt})
- then/group/using statements (GHC.HsToCore.ListComp.{dsTransStmt,dsMcStmt})
- recursive do blocks (mdo/rec) (GHC.HsToCore.Expr.dsDo)
- arrows (GHC.HsToCore.Arrows)
by consistently using 'box'/'unbox' to deal with unboxed types being put
into big tuples. We also adapt 'GHC.Tc.Gen.Match.{tcMcStmt,tcDoStmt}'
to create fresh /representation-polymorphic/ type variables for the
binders that we are going to put into a big tuple (previously, GHC only
allowed lifted types in that position).
These desugaring of box and unbox is explained in
Note [Desugaring box & unbox] in GHC.Core.Make.Box. Roughly speaking,
the desugaring looks like:
box @r @ty e ==> K e
unbox @r @ty e ==> case e of K x -> x
where 'K' is the boxing data constructor at RuntimeRep 'r'. It's
slightly more complicated than that in practice: we first need to
insert a cast (see Note [The canonical type of a RuntimeRep]) using the
new 'CanonicalProv' 'UnivCoProvenance', and unboxed tuples/sums need
recursive logic explained in Note [box/unbox for unboxed tuples and sums],
including a specific 'BoxSum' datatypes for unboxed sum (which also
needed a wired-in type-level list indexing type family (!!)).
The desugaring happens in two ways:
- Occurrences of 'box'/'unbox' emitted by the typechecker are
eliminated in the 'ds_app_var' case of GHC.HsToCore.Expr.
- Other code that needs to box/unbox emits the appropriate code inline.
This includes the big tuple machinery of GHC.Core.Make used by the
desugarer (GHC.HsToCore.{Utils,Binds,Arrows,ListComp}) as well as
the float-out pass (GHC.Core.Opt.SetLevels).
Core Lint checks that no occurrence of 'box'/'unbox' survives desugaring.
On the way, we overhaul the litany of big tuple functions by introducing
data BigTupleLayout = BoxedElements | BareElements
to distinguish the two kinds of "big tuples" that GHC routinely deals
with; for example 'mkBigCoreVarTupSolo' becomes 'mkBigCoreVarTup BareElements'.
All of this code is moved to the new module GHC.Core.Make.BigTuple.
Fixes #20864 #22336 #22473 #25065 #27331
-------------------------
Metric Decrease:
T9872a
-------------------------
- - - - -
4cf7628a by Evgeny Malyshev at 2026-09-17T02:39:56-04:00
Fix the CompactionFailed documentation link to compact
The old Haddock reference links into base instead of ghc-compact.
Use an explicit URL because ghc-compact depends on base, not vice versa.
Fixes #27558
Assisted-By: OpenAI Codex
- - - - -
098bba70 by Facundo Domínguez at 2026-09-17T14:57:41-04:00
Remove the call to hoistFlag from processCmdLineP
This call was undesirable because it was causing a reconstruction
of the list of flag processors.
Even relying on list fusion would not be sufficient because we want
to replace the list with a data structure with faster lookups to
address #25763.
The removal is done in two moves:
* We move CmdLineP to CmdLine.hs
* Then we move the effect of hoisting to processArgs via a new function
hoistEwM
- - - - -
e01799c7 by Facundo Domínguez at 2026-09-17T14:57:42-04:00
Do not create duplicate flag processors for the -W flag
This makes simpler implementing the command line parser with a trie map
holding flag processors. If we were to allow duplicate flag processors
for any flag, the trie map would need to be a multimap.
- - - - -
3875b257 by Facundo Domínguez at 2026-09-17T14:57:42-04:00
Replace the list of flag processors with a trie map in `processArgs`
Previously `GHC.Driver.CmdLine.findArg` was called for each
command-line argument, doing a linear search on the list of flag
processors each time. This was somewhat costly as the list of flag
processor has near 2000 elements.
A lookup with a trie map is faster in general, but the speed up is
only noticeable when calling `GHC.Driver.CmdLine.processArgs` sufficient
times, so the amount of lookups piles up. This could happen in large
projects with many units each having many dependencies. If only a
module needs to be recompiled, the user still needs to wait for all of
the flag parsing to complete.
The benchmark at testsuite/tests/perf/compiler/cmdline-parser shows a
10x speed up when using the trie.
The trie map is implemented in `GHC.Data.StringTrie`.
The design is explained in Note [Processing command line argument fast]
in GHC.Driver.Session.
The commit also:
* adds two property-based tests for trie map operations,
* adds a benchmark of the command line parser, and
* renames the predicate `arg_ok` to `valid_flag_suffix` to decide what
flag processor should be used when there are multiple matching
prefixes on a flag.
Fixes #25763.
- - - - -
5236634a by Andreas Klebinger at 2026-09-17T14:58:24-04:00
Compile Word64{Map,Set} with -O2 always.
This is the hottest code and even if we build GHC with -O0 otherwise -O2
pays off for these modules.
Fixes #27814.
- - - - -
545fd4fb by ARATA Mizuki at 2026-09-22T15:03:24-04:00
Change the C types of hs_{bitrev,bswap}{16,32} to match the Haskell types
bitReverse{16,32}# and byteSwap{16,32}# have the type
Word# -> Word#, but their C correspondents took and
returned StgWord{16,32}.
The mismatch on bitReverse16# led to the problem reported
in #27299. Although no similar issue is reported for
bitReverse32# and byteSwap{16,32}#, it seemed wise to
change them proactively.
This commit fixes the mismatch by changing the C type.
Fixes #27299
- - - - -
bf17f289 by fendor at 2026-09-22T15:04:05-04:00
Don't lose instances from shadowed imported modules with the same name
When we look for instances below the current module, we need to check
the unit id as well when we check for whether the module is the current
module, otherwise we ignore modules we import that have the same name as
the import module.
Adds regression test.
- - - - -
53eead15 by Wolfgang Jeltsch at 2026-09-24T20:12:03-04:00
Correct the documentation of `explicitUnits`
Resolves #27787.
- - - - -
35bf6f4b by Simon Jakobi at 2026-09-24T20:12:49-04:00
Remove stale specHeader bullet from Note [Don't make fillers for constraint types]
The Note said that Specialise.specHeader also calls mkAbsentFiller.
abfc224a27c removed that call: the specialiser no longer drops dead
arguments at all, see Note [Do not drop dead args from specialisations]
in GHC.Core.Opt.Specialise.
Assisted-by: Claude Fable 5.1
- - - - -
22f48db2 by Wolfgang Jeltsch at 2026-09-25T13:39:08+03:00
Change `explicitUnits` to `availableUnits`
The name `explicitUnits` is wrong, since only those units that are
paired with `Just` values in this field are made available explicitly.
- - - - -
250 changed files:
- .gitlab/ci.sh
- + changelog.d/25763-cmdline-opt
- + changelog.d/26610
- + changelog.d/27732
- + changelog.d/T27629
- + changelog.d/T27788-js-selector
- + changelog.d/boxing-family
- + changelog.d/clc-421
- + changelog.d/fix-compacting-gc-constr-27649
- + changelog.d/fix-control0-mask-trampoline
- + changelog.d/fix-parallel-gc-selector-race-27477
- + changelog.d/fix-threadlabel-segfault-27618
- + changelog.d/select-io-manager
- compiler/GHC/Builtin.hs
- compiler/GHC/Builtin/KnownKeys.hs
- compiler/GHC/Builtin/Modules.hs
- compiler/GHC/Builtin/Uniques.hs
- compiler/GHC/Builtin/WiredIn/Ids.hs
- compiler/GHC/Builtin/WiredIn/Prim.hs
- compiler/GHC/Builtin/WiredIn/TypeLits.hs
- compiler/GHC/Builtin/WiredIn/Types.hs
- + compiler/GHC/Builtin/WiredIn/Types/Box.hs
- compiler/GHC/Builtin/primops.txt.pp
- compiler/GHC/Core/Coercion.hs
- compiler/GHC/Core/Coercion/Axiom.hs
- compiler/GHC/Core/DataCon.hs
- compiler/GHC/Core/FamInstEnv.hs
- compiler/GHC/Core/Lint.hs
- compiler/GHC/Core/Make.hs
- + compiler/GHC/Core/Make/BigTuple.hs
- + compiler/GHC/Core/Make/Box.hs
- compiler/GHC/Core/Opt/SetLevels.hs
- compiler/GHC/Core/Opt/Specialise.hs
- compiler/GHC/Core/Opt/WorkWrap/Utils.hs
- compiler/GHC/Core/TyCo/Ppr.hs
- compiler/GHC/Core/TyCo/Rep.hs
- compiler/GHC/Core/TyCon.hs
- compiler/GHC/Core/Type.hs
- compiler/GHC/CoreToStg.hs
- compiler/GHC/CoreToStg/AddImplicitBinds.hs
- compiler/GHC/Data/FastString.hs
- + compiler/GHC/Data/StringTrie.hs
- compiler/GHC/Data/Word64Map/Internal.hs
- compiler/GHC/Data/Word64Map/Strict/Internal.hs
- compiler/GHC/Data/Word64Set/Internal.hs
- compiler/GHC/Driver/CmdLine.hs
- compiler/GHC/Driver/Downsweep.hs
- compiler/GHC/Driver/Env.hs
- compiler/GHC/Driver/Main/Compile.hs
- compiler/GHC/Driver/Main/Passes.hs
- compiler/GHC/Driver/Make.hs
- compiler/GHC/Driver/Session.hs
- compiler/GHC/Hs/Doc.hs
- compiler/GHC/Hs/Expr.hs
- compiler/GHC/Hs/Extension/Pass.hs
- compiler/GHC/Hs/Pat.hs
- compiler/GHC/Hs/Utils.hs
- compiler/GHC/HsToCore/Arrows.hs
- compiler/GHC/HsToCore/Binds.hs
- compiler/GHC/HsToCore/Expr.hs
- compiler/GHC/HsToCore/ListComp.hs
- compiler/GHC/HsToCore/Utils.hs
- compiler/GHC/Iface/Decl.hs
- compiler/GHC/Iface/Load.hs
- compiler/GHC/Iface/Rename.hs
- compiler/GHC/Iface/Syntax.hs
- compiler/GHC/Iface/Tidy.hs
- compiler/GHC/IfaceToCore.hs
- compiler/GHC/Parser.y
- compiler/GHC/Parser/Annotation.hs
- compiler/GHC/Parser/PostProcess.hs
- compiler/GHC/Rename/Module.hs
- compiler/GHC/Stg/Lint.hs
- compiler/GHC/StgToJS/Apply.hs
- compiler/GHC/StgToJS/CodeGen.hs
- compiler/GHC/StgToJS/Symbols.hs
- compiler/GHC/SysTools/Cpp.hs
- compiler/GHC/Tc/Deriv.hs
- compiler/GHC/Tc/Deriv/Generate.hs
- compiler/GHC/Tc/Deriv/Generics.hs
- compiler/GHC/Tc/Errors/Types.hs
- compiler/GHC/Tc/Gen/HsType.hs
- compiler/GHC/Tc/Gen/Match.hs
- compiler/GHC/Tc/Gen/Pat.hs
- compiler/GHC/Tc/Gen/Splice.hs
- compiler/GHC/Tc/Instance/Family.hs
- compiler/GHC/Tc/Instance/Typeable.hs
- compiler/GHC/Tc/Module.hs
- compiler/GHC/Tc/Solver/Default.hs
- compiler/GHC/Tc/Solver/FunDeps.hs
- compiler/GHC/Tc/TyCl.hs
- compiler/GHC/Tc/TyCl/Instance.hs
- compiler/GHC/Tc/Utils/Backpack.hs
- compiler/GHC/Types/CompleteMatch.hs
- compiler/GHC/Types/Id.hs
- compiler/GHC/Types/Id/Info.hs
- compiler/GHC/Types/Id/Make.hs
- compiler/GHC/Types/TyThing.hs
- compiler/GHC/Types/TyThing/Ppr.hs
- compiler/GHC/Types/Unique.hs
- compiler/GHC/Unit/State.hs
- compiler/ghc.cabal.in
- docs/users_guide/exts/type_data.rst
- docs/users_guide/runtime_control.rst
- ghc/GHC/Driver/Session/Mode.hs
- libraries/base/src/GHC/Base.hs
- libraries/base/src/GHC/Exts.hs
- libraries/base/src/GHC/RTS/Flags.hs
- libraries/ghc-internal/ghc-internal.cabal.in
- libraries/ghc-internal/include/RtsIfaceSymbols.h
- + libraries/ghc-internal/src/GHC/Internal/Box.hs
- libraries/ghc-internal/src/GHC/Internal/Conc/IO.hs
- libraries/ghc-internal/src/GHC/Internal/Event/Thread.hs
- libraries/ghc-internal/src/GHC/Internal/Exts.hs
- libraries/ghc-internal/src/GHC/Internal/IO/Exception.hs
- libraries/ghc-internal/src/GHC/Internal/RTS/Flags.hsc
- libraries/ghc-internal/src/GHC/Internal/TypeNats.hs
- libraries/ghc-internal/src/GHC/Internal/Types.hs
- linters/lint-codes/LintCodes/Static.hs
- rts/Capability.c
- rts/ContinuationOps.cmm
- rts/HeapStackCheck.cmm
- rts/IOManager.c
- rts/IOManager.h
- rts/IOManagerInternals.h
- rts/Messages.c
- rts/Prelude.h
- rts/PrimOps.cmm
- rts/Printer.c
- rts/RaiseAsync.c
- rts/STM.c
- rts/Schedule.c
- rts/Sparks.c
- rts/StableName.c
- rts/StgMiscClosures.cmm
- rts/ThreadPaused.c
- rts/Threads.c
- rts/Trace.h
- rts/configure.ac
- rts/include/rts/Flags.h
- rts/include/rts/RtsToHsIface.h
- rts/include/rts/storage/Closures.h
- rts/include/stg/MiscClosures.h
- rts/include/stg/Prim.h
- rts/posix/Poll.c
- rts/posix/Poll.h
- rts/posix/Select.c
- + rts/posix/SelectBis.c
- + rts/posix/SelectBis.h
- rts/posix/Timeout.c
- rts/posix/Timeout.h
- rts/prim/bitrev.c
- rts/prim/bswap.c
- rts/rts.cabal
- rts/sm/Compact.c
- rts/sm/Evac.c
- rts/sm/GC.c
- rts/sm/GCUtils.c
- rts/sm/MBlock.c
- rts/sm/NonMoving.c
- rts/sm/NonMovingMark.c
- rts/sm/Storage.c
- rts/sm/Sweep.c
- testsuite/tests/MiniQuickCheck.hs
- testsuite/tests/arrows/should_run/all.T
- + testsuite/tests/arrows/should_run/arrowrun005.hs
- + testsuite/tests/arrows/should_run/arrowrun005.stdout
- testsuite/tests/codeGen/should_run/all.T
- testsuite/tests/count-deps/CountDepsParser.stdout
- + testsuite/tests/deSugar/should_compile/T25065a.hs
- testsuite/tests/deSugar/should_compile/all.T
- + testsuite/tests/deSugar/should_run/T20864.hs
- + testsuite/tests/deSugar/should_run/T20864.stdout
- + testsuite/tests/deSugar/should_run/T25065b.hs
- + testsuite/tests/deSugar/should_run/T25065b.stdout
- testsuite/tests/deSugar/should_run/all.T
- testsuite/tests/driver/all.T
- + testsuite/tests/driver/stringtrie.hs
- + testsuite/tests/driver/stringtrie.stdout
- testsuite/tests/ghc-api/T25121_status.stdout
- + testsuite/tests/ghci/prog-mhu008/Makefile
- + testsuite/tests/ghci/prog-mhu008/a/M.hs
- + testsuite/tests/ghci/prog-mhu008/all.T
- + testsuite/tests/ghci/prog-mhu008/b/M.hs
- + testsuite/tests/ghci/prog-mhu008/prog-mhu008.script
- + testsuite/tests/ghci/prog-mhu008/prog-mhu008.stdout
- + testsuite/tests/ghci/prog-mhu008/unitA
- + testsuite/tests/ghci/prog-mhu008/unitB
- + testsuite/tests/indexed-types/should_run/T27331.hs
- + testsuite/tests/indexed-types/should_run/T27331.stdout
- + testsuite/tests/indexed-types/should_run/T27331b.hs
- + testsuite/tests/indexed-types/should_run/T27331b.stdout
- + testsuite/tests/indexed-types/should_run/T27331b_Plugin.hs
- testsuite/tests/indexed-types/should_run/all.T
- testsuite/tests/interface-stability/base-exports.stdout
- testsuite/tests/interface-stability/base-exports.stdout-javascript-unknown-ghcjs
- testsuite/tests/interface-stability/base-exports.stdout-mingw32
- testsuite/tests/interface-stability/ghc-experimental-exports.stdout
- testsuite/tests/interface-stability/ghc-experimental-exports.stdout-mingw32
- testsuite/tests/interface-stability/ghc-prim-exports.stdout
- testsuite/tests/interface-stability/ghc-prim-exports.stdout-mingw32
- + testsuite/tests/javascript/T27788.hs
- + testsuite/tests/javascript/T27788.stdout
- testsuite/tests/javascript/all.T
- testsuite/tests/linters/notes.stdout
- testsuite/tests/parser/should_compile/DumpParsedAstComments.stderr
- testsuite/tests/parser/should_compile/DumpSemis.stderr
- + testsuite/tests/perf/compiler/T25763.stderr
- testsuite/tests/perf/compiler/all.T
- + testsuite/tests/primops/should_run/T27299.hs
- + testsuite/tests/primops/should_run/T27299.stdout
- testsuite/tests/primops/should_run/all.T
- testsuite/tests/printer/Test20297.stdout
- + testsuite/tests/rts/T27477.hs
- + testsuite/tests/rts/T27477.stdout
- + testsuite/tests/rts/T27618.hs
- + testsuite/tests/rts/T27618.stdout
- + testsuite/tests/rts/T27649.hs
- + testsuite/tests/rts/T27649.stdout
- testsuite/tests/rts/all.T
- + testsuite/tests/rts/continuations/T27651.hs
- + testsuite/tests/rts/continuations/T27651.stdout
- testsuite/tests/rts/continuations/all.T
- testsuite/tests/simplCore/should_compile/Makefile
- + testsuite/tests/simplCore/should_compile/T27629.hs
- + testsuite/tests/simplCore/should_compile/T27629.stderr
- + testsuite/tests/simplCore/should_compile/T27629Plugin.hs
- testsuite/tests/simplCore/should_compile/all.T
- + testsuite/tests/type-data/should_fail/T27732a.hs
- + testsuite/tests/type-data/should_fail/T27732a.stderr
- + testsuite/tests/type-data/should_fail/T27732b.hs
- + testsuite/tests/type-data/should_fail/T27732b.stderr
- + testsuite/tests/type-data/should_fail/T27732c.hs
- + testsuite/tests/type-data/should_fail/T27732c.stderr
- + testsuite/tests/type-data/should_fail/T27732d.hs
- + testsuite/tests/type-data/should_fail/T27732d.stderr
- + testsuite/tests/type-data/should_fail/T27732e.hs
- + testsuite/tests/type-data/should_fail/T27732e.stderr
- + testsuite/tests/type-data/should_fail/T27732f.hs
- + testsuite/tests/type-data/should_fail/T27732f.stderr
- testsuite/tests/type-data/should_fail/TDStrictnessGADT.stderr
- testsuite/tests/type-data/should_fail/TDStrictnessH98.stderr
- testsuite/tests/type-data/should_fail/all.T
- + testsuite/tests/warnings/should_fail/WExpectedNoArgError.stderr
- testsuite/tests/warnings/should_fail/all.T
- utils/check-exact/ExactPrint.hs
- utils/check-exact/Utils.hs
- utils/deriveConstants/Main.hs
- utils/haddock/haddock-api/src/Haddock/Convert.hs
- utils/jsffi/dyld.mjs
The diff was not included because it is too large.
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/613de6413d2d090889752ccd2d9ca6…
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/613de6413d2d090889752ccd2d9ca6…
You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
1
0
[Git][ghc/ghc] Deleted branch wip/jeltsch/explicit-units-doc-fix
by Wolfgang Jeltsch (@jeltsch) 25 Sep '26
by Wolfgang Jeltsch (@jeltsch) 25 Sep '26
25 Sep '26
Wolfgang Jeltsch deleted branch wip/jeltsch/explicit-units-doc-fix at Glasgow Haskell Compiler / GHC
--
You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
1
0
25 Sep '26
Simon Peyton Jones pushed to branch wip/T27844 at Glasgow Haskell Compiler / GHC
Commits:
ed00d124 by Simon Peyton Jones at 2026-09-25T11:11:38+01:00
More fancy footwork
Needs documentation. Key point is that finish_no_unify does a second
checkTyEq which avoids the problem in #27844
- - - - -
3 changed files:
- compiler/GHC/Tc/Solver/Equality.hs
- compiler/GHC/Tc/Solver/Monad.hs
- compiler/GHC/Tc/Utils/Unify.hs
Changes:
=====================================
compiler/GHC/Tc/Solver/Equality.hs
=====================================
@@ -406,11 +406,13 @@ canonicaliseEquality
-- hold.
canonicaliseEquality ev eq_rel ty1 ty2
- = Stage $ do { traceTcS "canonicaliseEquality" $
+ = Stage $ do { traceTcS "canonicaliseEquality {" $
vcat [ ppr ev, ppr eq_rel, ppr ty1, ppr ty2 ]
; rdr_env <- getGlobalRdrEnvTcS
; fam_insts <- getFamInstEnvs
- ; can_eq_nc initCanEqState rdr_env fam_insts ev eq_rel ty1 ty1 ty2 ty2 }
+ ; res <- can_eq_nc initCanEqState rdr_env fam_insts ev eq_rel ty1 ty1 ty2 ty2
+ ; traceTcS "canonicaliseEquality }" (ppr res)
+ ; return res }
-- | Main worker function for 'canonicaliseEquality'.
--
@@ -2205,65 +2207,44 @@ canEqCanLHSFinish_try_unification ev eq_rel swapped lhs rhs
, TyVarLHS lhs_tv <- lhs
= do { given_eq_lvl <- getInnermostGivenEqLevel
; case simpleUnifyCheck UC_Solver given_eq_lvl lhs_tv rhs of
- SUC_CanUnify ->
- unify lhs_tv (mkReflRedn Nominal rhs)
- SUC_CannotUnify
- | Just can_rhs <- canTyFamEqLHS_maybe rhs
- -> swap_and_finish lhs_tv can_rhs -- See Note [Orienting TyVarLHS/TyFamLHS]
- | otherwise
- -> finish_no_unify
- SUC_NotSure ->
+ SUC_CannotUnify -> finish_no_unify
+ SUC_CanUnify -> do_unification lhs_tv (mkReflRedn Nominal rhs)
+ SUC_NotSure ->
-- We have a touchable unification variable on the left,
-- and the top-shape check succeeded. These are both guaranteed
-- by the fact that simpleUnifyCheck did not return SUC_CannotUnify.
do { let flags = unifyingLHSMetaTyVar_TEFTask ev lhs_tv
; check_result <- wrapTcS (checkTyEqRhs flags rhs)
+ ; traceTcS "canEqCanLHSFinish_try_unification" $
+ vcat [ text "lhs" <+> ppr lhs
+ , text "rhs" <+> ppr rhs
+ , text "check_result" <+> ppr check_result ]
; case check_result of
- PuOK cts rhs_redn ->
- do { emitWork cts
- ; unify lhs_tv rhs_redn }
- PuFail reason
- | Just can_rhs <- canTyFamEqLHS_maybe rhs
- -> swap_and_finish lhs_tv can_rhs -- See Note [Orienting TyVarLHS/TyFamLHS]
-{-
- | reason `cterHasOnlyProblems` do_not_prevent_rewriting
- ->
- -- ContinueWith, to allow using this constraint for
- -- rewriting (e.g. alpha[2] ~ beta[3]).
- do { new_ev <- rewriteEqEvidenceSwapOnly ev eq_rel swapped lhs rhs
- ; continueWith $ Right $
- EqCt { eq_ev = new_ev, eq_eq_rel = eq_rel
- , eq_lhs = lhs , eq_rhs = rhs }
- }
--}
- | otherwise
- -> try_irred reason
- }
+ PuFail {} -> finish_no_unify
+ PuOK cts rhs_redn -> do { emitWork cts
+ ; do_unification lhs_tv rhs_redn }
}
-- Otherwise unification is off the table
| otherwise
= finish_no_unify
where
- -- We can't unify, but this equality can go in the inert set
+ -- We can't unify, but we can see if this equality can go in the inert set
-- and be used to rewrite other constraints.
- finish_no_unify =
- canEqCanLHSFinish_no_unification ev eq_rel swapped lhs rhs
+ finish_no_unify = canEqCanLHSFinish_no_unification ev eq_rel swapped lhs rhs
-- We can't unify, and this equality should not be used to rewrite
-- other constraints (e.g. because it has an occurs check).
-- So add it to the inert Irreds.
- try_irred reason =
- tryIrredInstead reason ev eq_rel swapped lhs rhs
+ try_irred reason = tryIrredInstead reason ev eq_rel swapped lhs rhs
-- We can't unify as-is, and want to flip the equality around.
-- Example: alpha ~ F tys, flip it around to become the canonical
-- equality f tys ~ alpha.
- swap_and_finish tv can_rhs =
- swapAndFinish ev eq_rel swapped (mkTyVarTy tv) can_rhs
+ swap_and_finish tv can_rhs = swapAndFinish ev eq_rel swapped (mkTyVarTy tv) can_rhs
-- Phew! Finally! We can unify; go ahead and do so.
- unify tv rhs_redn =
+ do_unification tv rhs_redn =
do { -- In the common case where rhs_redn is Refl, we don't need to rewrite
-- the evidence, even if swapped=IsSwapped. Suppose the original was
-- [W] co : Int ~ alpha
@@ -2312,26 +2293,25 @@ canEqCanLHSFinish_no_unification ev eq_rel swapped lhs rhs
-- If we had F a ~ G (F a), which gives an occurs check,
-- then swap it to G (F a) ~ F a, which does not
+ -- See Note [Orienting TyVarLHS/TyFamLHS]
+ -- ToDo: fix me
-- However `swap_for_size` above will orient it with (G (F a)) on
-- the left anwyway. `swap_for_rewriting` "wins", but that doesn't
-- matter: in the occurs check case swap_for_rewriting will be moot.
-- TL;DR: the next four lines of code are redundant
-- I'm leaving them here in case they become relevant again
--- | TyFamLHS {} <- lhs
--- , Just can_rhs <- canTyFamEqLHS_maybe rhs
--- , reason `cterHasOnlyProblem` cteSolubleOccurs
--- -> swapAndFinish ev eq_rel swapped lhs_ty can_rhs
--- | otherwise
+ | TyFamLHS {} <- lhs
+ , Just can_rhs <- canTyFamEqLHS_maybe rhs
+ , reason `cterHasOnlyProblem` cteSolubleOccurs
+ -> swapAndFinish ev eq_rel swapped lhs_ty can_rhs
-{-
| reason `cterHasOnlyProblems` do_not_prevent_rewriting
-> do { new_ev <- rewriteEqEvidenceSwapOnly ev eq_rel swapped lhs rhs
; continueWith $ Right $
EqCt { eq_ev = new_ev, eq_eq_rel = eq_rel
, eq_lhs = lhs , eq_rhs = rhs }
}
--}
-
+
| otherwise
-> tryIrredInstead reason ev eq_rel swapped lhs rhs
@@ -2358,11 +2338,9 @@ canEqCanLHSFinish_no_unification ev eq_rel swapped lhs rhs
--
-- Concrete-ness: alpha[conc] ~ b[sk]
-- We can use it to rewrite; we still have to solve the original
-{-
do_not_prevent_rewriting :: CheckTyEqResult
do_not_prevent_rewriting = cteProblem cteSkolemEscape S.<>
cteProblem cteConcrete
--}
----------------------
swapAndFinish :: CtEvidence -> EqRel -> SwapFlag
=====================================
compiler/GHC/Tc/Solver/Monad.hs
=====================================
@@ -2309,8 +2309,8 @@ checkTypeEq :: CtEvidence -> EqRel -> CanEqLHS -> TcType
checkTypeEq ev eq_rel lhs rhs =
case ev of
CtGiven {} ->
- do { traceTcS "checkTypeEq {" (vcat [ text "lhs:" <+> ppr lhs
- , text "rhs:" <+> ppr rhs ])
+ do { traceTcS "checkTypeEq (given) {" (vcat [ text "lhs:" <+> ppr lhs
+ , text "rhs:" <+> ppr rhs ])
; check_result <- wrapTcS (checkTyEqRhs given_flags rhs)
; traceTcS "checkTypeEq }" (ppr check_result)
; case check_result of
@@ -2319,7 +2319,12 @@ checkTypeEq ev eq_rel lhs rhs =
; emitWork new_givens
; updInertSet (addCycleBreakerBindings prs)
; return (pure redn) } }
- CtWanted {} -> wrapTcS (checkTyEqRhs wanted_flags rhs)
+ CtWanted {} -> do { traceTcS "checkTypeEq (wanted) {" $
+ vcat [ text "lhs:" <+> ppr lhs
+ , text "rhs:" <+> ppr rhs ]
+ ; check_result <- wrapTcS (checkTyEqRhs wanted_flags rhs)
+ ; traceTcS "checkTypeEq }" (ppr check_result)
+ ; return check_result }
where
wanted_flags :: TyEqFlags TcM Ct
wanted_flags = notUnifying_TEFTask occ_prob lhs
=====================================
compiler/GHC/Tc/Utils/Unify.hs
=====================================
@@ -3619,13 +3619,18 @@ simpleUnifyCheck :: UnifyCheckCaller -> TcLevel -> TcTyVar -> TcType -> SimpleUn
-- unification might still be OK, but it'll take more work to do
-- (use the full 'checkTypeEq').
--
--- * Rejects if lhs_tv occurs in rhs_ty (occurs check)
--- * Rejects foralls unless
--- lhs_tv is RuntimeUnk (used by GHCi debugger)
--- or is a QL instantiation variable
--- * Rejects a non-concrete type if lhs_tv is concrete
--- * Rejects type families unless fam_ok=True
--- * Does a level-check for type variables, to avoid skolem escape
+-- It returns SUC_CannotUnify for
+-- * Touchability failure
+-- * Top level-shape failure
+--
+-- It returns SUC_NotSure for
+-- * Occurs check: lhs_tv occurs in rhs_ty
+-- * Type families unless fam_ok=True
+-- * Foralls unless
+-- lhs_tv is RuntimeUnk (used by GHCi debugger)
+-- or is a QL instantiation variable
+-- * Concreteness: lhs_tv is concrete and rhs is a non-concrete type
+-- * Skolem-escape: a level-check for type variables, to avoid skolem escape
--
-- This function is pretty heavily used, so it's optimised not to allocate
simpleUnifyCheck caller given_eq_lvl lhs_tv rhs
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/ed00d124ab0e0ae0ac7c419926da424…
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/ed00d124ab0e0ae0ac7c419926da424…
You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
1
0
[Git][ghc/ghc][wip/sjakobi/T27847-ghci-rethrow-backtrace] ghc -e: Drop GHC-internal exception context before printing
by Simon Jakobi (@sjakobi) 25 Sep '26
by Simon Jakobi (@sjakobi) 25 Sep '26
25 Sep '26
Simon Jakobi pushed to branch wip/sjakobi/T27847-ghci-rethrow-backtrace at Glasgow Haskell Compiler / GHC
Commits:
df7760a2 by Simon Jakobi at 2026-09-25T11:21:46+02:00
ghc -e: Drop GHC-internal exception context before printing
Since 2ab02c579a9, a failing `ghc -e` or runghc script printed a
backtrace for every rethrow on the way out of a GHCi command,
pointing into GHC's own sources (#27847).
Rather than fix each rethrow site, clear the context in ghc -e's
top-level handler, as interactive GHCi effectively does already. The
user program's own context arrives as message text via
toSerializableException, so only GHC-internal frames are lost. This
also drops the pre-existing WhileHandling block from
prettyPrintGhcErrors.
Also remove a stale sentence from the SerializableException comment.
Fixes #27847.
Assisted-by: Claude Opus 5.5
- - - - -
5 changed files:
- compiler/GHC/Utils/Exception.hs
- ghc/GHCi/UI.hs
- libraries/ghci/GHCi/Message.hs
- testsuite/tests/ghc-e/should_fail/T18441fail7.stderr
- testsuite/tests/ghc-e/should_run/ghc-e005.stderr
Changes:
=====================================
compiler/GHC/Utils/Exception.hs
=====================================
@@ -11,6 +11,7 @@ import GHC.Prelude.Basic
import GHC.IO (catchException)
import Control.Exception as CE hiding (assert)
+import Control.Exception.Context (emptyExceptionContext)
import Control.Monad.IO.Class
import Control.Monad.Catch
@@ -24,4 +25,8 @@ handleIO = flip catchIO
tryIO :: IO a -> IO (Either IOException a)
tryIO = CE.try
+-- | Remove the context, such as backtraces and annotations, from an exception.
+dropExceptionContext :: SomeException -> SomeException
+dropExceptionContext e = toException (ExceptionWithContext emptyExceptionContext e)
+
type ExceptionMonad m = (MonadCatch m, MonadThrow m, MonadMask m, MonadIO m)
=====================================
ghc/GHCi/UI.hs
=====================================
@@ -1079,8 +1079,12 @@ runGHCi paths maybe_exprs = do
-- current progname in the exception text:
-- <progname>: <exception>
liftIO $ withProgName (progname st)
- $ topHandler e
-- this used to be topHandlerFastExit, see #2228
+ $ topHandler
+ -- The program's own context is already part of the
+ -- message (see toSerializableException). The rest
+ -- comes from GHC itself (#27847).
+ $ dropExceptionContext e
runInputTWithPrefs defaultPrefs defaultSettings $ do
-- make `ghc -e` exit nonzero on failure, see #7962, #9916, #17560, #18441
_ <- runCommands' hdle
=====================================
libraries/ghci/GHCi/Message.hs
=====================================
@@ -466,8 +466,7 @@ data LoadedDLL
-- * UserInterrupt has a special meaning
-- * In ghc -e, exitWith should exit with the appropriate exit code
--
--- So all we need to do is distinguish UserInterrupt and ExitCode, and
--- all other exceptions can be represented by their 'show' string.
+-- So all we need to do is distinguish UserInterrupt and ExitCode.
--
data SerializableException
= EUserInterrupt
=====================================
testsuite/tests/ghc-e/should_fail/T18441fail7.stderr
=====================================
@@ -1,16 +1,4 @@
<interactive>: Uncaught exception ghc-10.1-inplace:GHC.Utils.Panic.GhcException:
IO error: "Abcde" does not exist
-
-While handling ghc-10.1-inplace:GHC.Utils.Panic.GhcException:
- |
- | IO error: "Abcde" does not exist
- |
- | HasCallStack backtrace:
- | throw, called at compiler/GHC/Utils/Panic.hs:180:21 in ghc-10.1-inplace:GHC.Utils.Panic
- | throwGhcException, called at ghc/GHCi/UI.hs:2851:21 in ghc-bin-10.1.20260801-inplace:GHCi.UI
-
-HasCallStack backtrace:
- throwIO, called at compiler/GHC/Utils/Error.hs:513:19 in ghc-10.1-inplace:GHC.Utils.Error
-
1
=====================================
testsuite/tests/ghc-e/should_run/ghc-e005.stderr
=====================================
@@ -4,8 +4,3 @@ foo
HasCallStack backtrace:
error, called at ghc-e005.hs:12:10 in main:Main
-
-
-HasCallStack backtrace:
- throwIO, called at ghc\GHCi\UI.hs:1655:31 in ghc-bin-10.1.20260629-inplace:GHCi.UI
-
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/df7760a2ddc57864a5fe4e2acdd5e42…
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/df7760a2ddc57864a5fe4e2acdd5e42…
You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
1
0
Simon Peyton Jones pushed to branch wip/T27828 at Glasgow Haskell Compiler / GHC
Commits:
5ce02260 by Simon Peyton Jones at 2026-09-25T09:42:02+01:00
Wibbles
- - - - -
2 changed files:
- compiler/GHC/Core/FamInstEnv.hs
- + testsuite/tests/indexed-types/should_compile/T27828a.hs
Changes:
=====================================
compiler/GHC/Core/FamInstEnv.hs
=====================================
@@ -604,8 +604,8 @@ data BranchCompatibility
| BC_Incompat -- LHSs unify with S, and S(rhs1)/=S(rhs2)
compareBranches :: CoAxBranch -> CoAxBranch -> BranchCompatibility
-compareBranches b1@(CoAxBranch { cab_lhs = lhs1, cab_rhs = rhs1 })
- b2@(CoAxBranch { cab_lhs = lhs2, cab_rhs = rhs2 })
+compareBranches (CoAxBranch { cab_lhs = lhs1, cab_rhs = rhs1 })
+ (CoAxBranch { cab_lhs = lhs2, cab_rhs = rhs2 })
= case tcUnifyTysFG alwaysBindFam alwaysBindTv commonlhs1 commonlhs2 of
-- Here we need the cab_tvs of the two branches to be disinct.
-- See Note [CoAxBranch type variables] in GHC.Core.Coercion.Axiom.
=====================================
testsuite/tests/indexed-types/should_compile/T27828a.hs
=====================================
@@ -0,0 +1,16 @@
+{-# LANGUAGE TypeFamilies #-}
+
+module T27828a where
+
+import Data.Proxy
+
+type family F a where
+ F Int = Int -- B1
+ F Char = Bool -- B2
+ F a = a -- B3
+
+h :: (F a ~ Int) => Proxy a -> ()
+h = error "urk"
+
+k :: ()
+k = h Proxy -- [W] F alpha ~ Int
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/5ce02260ed21bba53c07ac5b242b8a2…
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/5ce02260ed21bba53c07ac5b242b8a2…
You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
1
0
[Git][ghc/ghc][master] Remove stale specHeader bullet from Note [Don't make fillers for constraint types]
by Marge Bot (@marge-bot) 24 Sep '26
by Marge Bot (@marge-bot) 24 Sep '26
24 Sep '26
Marge Bot pushed to branch master at Glasgow Haskell Compiler / GHC
Commits:
35bf6f4b by Simon Jakobi at 2026-09-24T20:12:49-04:00
Remove stale specHeader bullet from Note [Don't make fillers for constraint types]
The Note said that Specialise.specHeader also calls mkAbsentFiller.
abfc224a27c removed that call: the specialiser no longer drops dead
arguments at all, see Note [Do not drop dead args from specialisations]
in GHC.Core.Opt.Specialise.
Assisted-by: Claude Fable 5.1
- - - - -
1 changed file:
- compiler/GHC/Core/Opt/WorkWrap/Utils.hs
Changes:
=====================================
compiler/GHC/Core/Opt/WorkWrap/Utils.hs
=====================================
@@ -1367,13 +1367,8 @@ It's surprisingly subtle.
application; see test `T27627f`.
TL;DR: we play safe: we never make an absent filler for any /constraint-kinded/ type,
-using `isPredTy` to decide.
-
- * `mkAbsentFiller` returns `Nothing` for any constraint type, so worker/wrapper
- keeps the real argument.
-
- * `Specialise.specHeader` calls `mkAbsentFiller` too, so it likewise keeps the
- dead dictionary argument rather than dropping it for a filler.
+using `isPredTy` to decide: `mkAbsentFiller` returns `Nothing` for any constraint
+type, so worker/wrapper keeps the real argument.
Prior failed approaches
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/35bf6f4bcf06675565992725df62e83…
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/35bf6f4bcf06675565992725df62e83…
You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
1
0
[Git][ghc/ghc][master] Correct the documentation of `explicitUnits`
by Marge Bot (@marge-bot) 24 Sep '26
by Marge Bot (@marge-bot) 24 Sep '26
24 Sep '26
Marge Bot pushed to branch master at Glasgow Haskell Compiler / GHC
Commits:
53eead15 by Wolfgang Jeltsch at 2026-09-24T20:12:03-04:00
Correct the documentation of `explicitUnits`
Resolves #27787.
- - - - -
1 changed file:
- compiler/GHC/Unit/State.hs
Changes:
=====================================
compiler/GHC/Unit/State.hs
=====================================
@@ -369,10 +369,15 @@ data UnitState = UnitState {
-- is always mentioned before the units it depends on.
preloadUnits :: [UnitId],
- -- | Units which we explicitly depend on (from a command line flag).
- -- We'll use this to generate version macros and the unused packages warning. The
- -- original flag which was used to bring the unit into scope is recorded for the
- -- -Wunused-packages warning.
+ -- | The units that can be used by the respective home unit. Despite the
+ -- identifier, this does not just cover units that are enabled explicitly
+ -- using flags but also units from the package database that are enabled
+ -- implicitly in case @-hide-all-packages@ is absent. The former are paired
+ -- with 'Just' applied to the respective flags, the latter are paired with
+ -- 'Nothing'.
+ --
+ -- This field is used for generating version macros and “unused packages”
+ -- warnings.
explicitUnits :: [(Unit, Maybe PackageArg)],
homeUnitDepends :: Set UnitId,
@@ -907,7 +912,7 @@ mkUnitState logger unit_index cfg = do
let pkgname_map = listToUFM [ (unitPackageName p, unitInstanceOf p)
| p <- pkgs2
]
- -- The explicitUnits accurately reflects the set of units we have turned
+ -- The explicitUnits accurately reflects the set of units that are turned
-- on; as such, it also is the only way one can come up with requirements.
-- The requirement context is directly based off of this: we simply
-- look for nested unit IDs that are directly fed holes: the requirements
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/53eead150b93e0a9b372ea32318f77f…
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/53eead150b93e0a9b372ea32318f77f…
You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
1
0